Similarityoung commented on code in PR #953:
URL: https://github.com/apache/dubbo-go-pixiu/pull/953#discussion_r3487659072
##########
pkg/adapter/mcpserver/registrycenter.go:
##########
@@ -212,6 +184,53 @@ func (a *Adapter) Apply() error {
return nil
}
+func (a *Adapter) applyServerConfigEvent(reconciler *endpointReconciler, sink
mcpserver.ServerPublicationSink, registryName, serverId string, cfg
*model.McpServerConfig) {
+ if serverId == "" {
+ serverId = "default"
+ }
+
+ // Apply catalog and endpoints through one desired-state publication
path. If
+ // no runtime target is bound, skip the entire update so authorization
catalog
+ // and cluster endpoints cannot diverge.
+ if sink == nil {
+ logger.Infof("[dubbo-go-pixiu] mcp adapter update received from
server %s without a bound runtime publication sink", serverId)
+ return
+ }
+ runtimeID := sink.RuntimeID()
+ if runtimeID == "" {
+ logger.Errorf("[dubbo-go-pixiu] mcp adapter update received
from server %s but publication sink has no runtime id", serverId)
+ return
+ }
+ if err := reconciler.ValidateServerConfig(runtimeID, registryName,
serverId, cfg); err != nil {
+ logger.Errorf("[dubbo-go-pixiu] mcp adapter validate endpoints
for server %s error: %v", serverId, err)
+ return
+ }
+ if err := sink.ApplyMcpServerConfigByServer(serverId, cfg); err != nil {
Review Comment:
这里已经支持把 `cfg == nil` 传给 dynamic consumer 做删除,但 Nacos controller 在 server
从列表消失时只 `CancelListenToServer`,没有发布 `onChange(serverId, nil)` tombstone。因此真实
server 下线路径仍不会清理 catalog 和 endpoints。建议 controller 在移除 watched server 时显式发送
tombstone,并补 server 发布 tools 后从 registry 消失时 catalog/endpoints 都被清理的测试。
##########
pkg/filter/mcp/mcpserver/runtime.go:
##########
@@ -0,0 +1,148 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package mcpserver
+
+import (
+ "errors"
+ "fmt"
+ "sync"
+ "sync/atomic"
+)
+
+import (
+ "github.com/apache/dubbo-go-pixiu/pkg/model"
+)
+
+var (
+ runtimeIndexMu sync.RWMutex
+ runtimeIndex = map[string]*RuntimeState{}
+ runtimeSeq uint64
+ resetTestGlobals func()
+)
+
+var ErrDynamicConsumerUnavailable = errors.New("mcp dynamic consumer
unavailable")
+var ErrDynamicConsumerAmbiguous = errors.New("multiple mcp dynamic consumers
registered")
+
+// ServerPublicationSink is the dynamic registry publication target owned by
one
+// MCP runtime. Registry adapters bind to this sink once instead of
discovering a
+// runtime on every event.
+type ServerPublicationSink interface {
+ RuntimeID() string
+ ApplyMcpServerConfigByServer(serverId string, cfg
*model.McpServerConfig) error
+}
+
+func registerRuntime(runtime *RuntimeState) string {
+ if runtime == nil {
+ return ""
+ }
+ id := fmt.Sprintf("mcp-runtime-%d", atomic.AddUint64(&runtimeSeq, 1))
+ runtimeIndexMu.Lock()
+ runtimeIndex[id] = runtime
+ runtimeIndexMu.Unlock()
+ if runtime.dynamic != nil {
+ runtime.dynamic.setRuntimeID(id)
+ }
+ return id
+}
+
+func unregisterRuntime(id string) {
+ if id == "" {
+ return
+ }
+ runtimeIndexMu.Lock()
+ runtime := runtimeIndex[id]
+ delete(runtimeIndex, id)
+ runtimeIndexMu.Unlock()
+ if runtime != nil && runtime.dynamic != nil {
+ runtime.dynamic.setRuntimeID("")
+ }
+}
+
+// Stop releases all runtime-owned state. It does not affect other MCP filters.
+func (r *RuntimeState) Stop() {
+ if r == nil {
+ return
+ }
+ unregisterRuntime(r.id)
+ if r.sessionManager != nil {
+ r.sessionManager.Stop()
+ }
+ if r.plans != nil {
+ r.plans.Stop()
+ }
+}
+
+// DynamicConsumerForSingleRuntime returns the only registered dynamic
consumer.
+// Registry-center callbacks are process-global today, so multiple MCP filters
+// must be treated as ambiguous instead of guessing which runtime should
update.
+func DynamicConsumerForSingleRuntime() (*DynamicConsumer, error) {
+ runtimeIndexMu.RLock()
+ defer runtimeIndexMu.RUnlock()
+ switch len(runtimeIndex) {
+ case 0:
+ return nil, ErrDynamicConsumerUnavailable
+ case 1:
+ for _, runtime := range runtimeIndex {
+ if runtime.dynamic == nil {
+ return nil, ErrDynamicConsumerUnavailable
+ }
+ return runtime.dynamic, nil
+ }
+ default:
+ return nil, ErrDynamicConsumerAmbiguous
Review Comment:
多 MCP runtime 时这里直接返回 ambiguous,adapter 又没有配置维度来绑定目标 runtime;只要进程里有多个 MCP
server filter,动态 registry publication 就整体不可用。建议增加显式绑定规则(例如
adapter/filter/runtime id 或 endpoint 维度),或者在配置层限制并启动失败,避免运行时静默跳过动态更新。
##########
pkg/filter/mcp/mcpserver/dynamic.go:
##########
@@ -70,93 +87,139 @@ func NewDynamicConsumer(reg *ToolRegistry, sm
*transport.SessionManager, sseHand
}
}
-// ApplyMcpServerConfigByServer applies configuration by server ID
+func (d *DynamicConsumer) RuntimeID() string {
+ d.mu.RLock()
+ defer d.mu.RUnlock()
+ return d.runtimeID
+}
+
+func (d *DynamicConsumer) setRuntimeID(id string) {
+ d.mu.Lock()
+ defer d.mu.Unlock()
+ d.runtimeID = id
+}
+
+func (d *DynamicConsumer) SetGovernance(selector router.ToolSelector, plans
*router.SessionPlanStore) {
+ d.mu.Lock()
+ defer d.mu.Unlock()
+ d.selector = selector
+ d.plans = plans
+ d.governance = selector != nil && plans != nil
+}
+
+// ApplyMcpServerConfigByServer applies a dynamic tool catalog update by server
+// ID. Router configuration is intentionally not applied on this path; a
dynamic
+// payload that contains a router section is rejected before mutating tools so
+// the runtime cannot enter a tools=new/router=old partial state.
func (d *DynamicConsumer) ApplyMcpServerConfigByServer(serverId string, cfg
*model.McpServerConfig) error {
if cfg == nil {
return d.removeServerConfig(serverId)
}
+ if err := d.validateDynamicConfig(cfg); err != nil {
+ return err
+ }
- d.mu.Lock()
- defer d.mu.Unlock()
+ result, err := d.applyServerTools(serverId, cfg.Tools)
+ if err != nil {
+ return err
+ }
+ if d.logSkippedServerConfigApply(serverId, result) {
+ return nil
+ }
- // 1. Calculate new configuration fingerprint
- fingerprint := d.calculateFingerprint(cfg.Tools)
+ logger.Infof("[dubbo-go-pixiu] mcp server %s config applied: %d tools,
total servers: %d, merged tools: %d",
+ serverId, len(cfg.Tools), result.ServerCount,
result.MergedCount)
- // 2. Check if the server's configuration really needs to be updated
- if existingConfig, exists := d.serverConfigs[serverId]; exists {
- if existingConfig.Fingerprint == fingerprint {
- logger.Debugf("[dubbo-go-pixiu] mcp server %s config
unchanged (fp=%s), skipped", serverId, fingerprint)
- return nil
- }
+ d.notifyToolsListChanged()
+
+ return nil
+}
+
+func (d *DynamicConsumer) validateDynamicConfig(cfg *model.McpServerConfig)
error {
+ if cfg.Router != nil {
+ return errDynamicRouterUpdateUnsupported
}
+ if !d.governanceEnabled() {
+ return nil
+ }
+ if err := router.ValidateTools(cfg.Tools); err != nil {
+ return fmt.Errorf("invalid mcp tool router metadata: %w", err)
+ }
+ return nil
+}
- // 3. Debounce check (based on this server's configuration change time)
+func (d *DynamicConsumer) applyServerTools(serverId string, tools
[]model.ToolConfig) (serverConfigApplyResult, error) {
+ result := serverConfigApplyResult{Fingerprint:
d.calculateFingerprint(tools)}
now := time.Now()
- if existingConfig, exists := d.serverConfigs[serverId]; exists {
- // Skip only if this server is within debounce time
- if !existingConfig.LastApplied.IsZero() &&
now.Sub(existingConfig.LastApplied) < d.debounceTime {
- logger.Debugf("[dubbo-go-pixiu] mcp server %s debounce
active (elapsed=%v), skipped", serverId, now.Sub(existingConfig.LastApplied))
- return nil
- }
+
+ d.mu.Lock()
+ defer d.mu.Unlock()
+
+ existingConfig := d.serverConfigs[serverId]
+ if existingConfig != nil && existingConfig.Fingerprint ==
result.Fingerprint {
+ result.SkippedReason = serverConfigApplySkippedUnchanged
+ return result, nil
+ }
+ if elapsed, ok := d.serverConfigDebounceElapsed(existingConfig, now);
ok {
Review Comment:
这个 debounce 会跳过同一 server 在 500ms 内的第二次更新,即使 fingerprint
已经不同。`ApplyMcpServerConfigByServer` 对 caller 仍返回 nil,adapter 后续会继续按新 cfg
reconcile endpoints,导致 catalog 还是旧工具、cluster endpoints 已经是新配置。建议只跳过 fingerprint
相同的重复事件;不同 fingerprint 要么立即提交,要么 debounce 到最新 desired state 后统一提交,不能丢更新。
##########
pkg/adapter/mcpserver/registrycenter.go:
##########
@@ -157,47 +158,18 @@ func (a *Adapter) Apply() error {
continue
}
+ reconciler := a.endpoints
+ if reconciler == nil {
+ reconciler =
newEndpointReconciler(clusterManagerEndpointSink{})
+ a.endpoints = reconciler
+ }
+ registryName := k
+ sink, err := a.bindPublicationSink()
+ if err != nil {
+ logger.Infof("[dubbo-go-pixiu] mcp adapter registry %s
has no bound runtime publication sink: %v", registryName, err)
+ }
onChange := func(serverId string, cfg *model.McpServerConfig) {
- if cfg == nil {
- return
- }
-
- if serverId == "" {
- serverId = "default"
- }
-
- // 1) apply tools dynamically to registry for filter
usage
- if dc := mcpserver.GetOrInitDynamicConsumer(); dc !=
nil {
- if err :=
dc.ApplyMcpServerConfigByServer(serverId, cfg); err != nil {
- logger.Errorf("[dubbo-go-pixiu] mcp
adapter apply server %s config error: %v", serverId, err)
- }
- } else {
- logger.Infof("[dubbo-go-pixiu] mcp adapter
update received from server %s: tools=%d", serverId, len(cfg.Tools))
- }
- // 2) register endpoint for each tool using BackendURL
(host:port) into cluster named by tool.Name
- for _, tool := range cfg.Tools {
- if tool.BackendURL == "" {
- continue
- }
- result, err :=
util.ParseHostPortFromURL(tool.BackendURL)
- if err != nil {
- logger.Errorf("[dubbo-go-pixiu] mcp
adapter failed to parse BackendURL '%s' for tool '%s': %v",
- tool.BackendURL, tool.Name, err)
- continue
- }
- if result.UsedFallback {
- logger.Warnf("[dubbo-go-pixiu] mcp
adapter using fallback for tool '%s' with BackendURL '%s': %s",
- tool.Name, tool.BackendURL,
result.FallbackInfo)
- }
- endpointID := result.Host + ":" +
strconv.Itoa(result.Port)
-
server.GetClusterManager().SetEndpoint(tool.Cluster, &model.Endpoint{
- ID: endpointID,
- Address: model.SocketAddress{
- Address: result.Host,
- Port: result.Port,
- },
- })
- }
+ a.applyServerConfigEvent(reconciler, sink,
registryName, serverId, cfg)
Review Comment:
这里的 `sink` 是在 `Adapter.Apply()` 时绑定并被 `onChange` 闭包捕获的,但实际启动顺序里 adapter
manager 早于 listener/filter manager 初始化;MCP runtime 要到 HTTP connection manager
加载 MCP filter 后才注册。这样常规启动路径很容易先捕获到 `nil`,之后所有 registry event 都会被
`applyServerConfigEvent(..., sink=nil, ...)` 跳过。建议在每次 event 里懒绑定 publication
sink,或者调整启动依赖并加一个 adapter 先 Apply、filter 后 Apply、随后 event 仍可发布
catalog/endpoints 的集成测试。
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]