// Copyright (c) 2022 Alibaba Group Holding Ltd. // // Licensed 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 ( "encoding/json" "errors" "fmt" "net" "strconv" "strings" "sync" "time" apiv1 "github.com/alibaba/higress/api/networking/v1" "github.com/alibaba/higress/pkg/common" common2 "github.com/alibaba/higress/pkg/ingress/kube/common" "github.com/alibaba/higress/pkg/ingress/kube/mcpserver" provider "github.com/alibaba/higress/registry" "github.com/alibaba/higress/registry/memory" "github.com/golang/protobuf/ptypes/wrappers" "github.com/nacos-group/nacos-sdk-go/v2/common/constant" "github.com/nacos-group/nacos-sdk-go/v2/model" "go.uber.org/atomic" "istio.io/api/networking/v1alpha3" "istio.io/istio/pkg/config" "istio.io/istio/pkg/config/constants" "istio.io/istio/pkg/config/schema/gvk" "istio.io/istio/pkg/log" "istio.io/istio/pkg/util/sets" ) const ( DefaultInitTimeout = time.Second * 10 DefaultNacosTimeout = 5000 DefaultNacosLogLevel = "info" DefaultNacosLogDir = "/var/log/nacos/log/mcp/log" DefaultNacosCacheDir = "/var/log/nacos/log/mcp/cache" DefaultNacosNotLoadCache = true DefaultNacosLogMaxAge = 3 DefaultNacosLogMaxSize = 64 DefaultNacosLogMaxBackups = 3 DefaultRefreshInterval = time.Second * 30 DefaultRefreshIntervalLimit = time.Second * 10 ) var ( supportedProtocols = map[string]bool{ provider.HttpProtocol: true, provider.HttpsProtocol: true, provider.McpSSEProtocol: true, provider.McpStreamableProtocol: true, } protocolUpstreamTypeMapping = map[string]string{ provider.HttpProtocol: mcpserver.UpstreamTypeRest, provider.HttpsProtocol: mcpserver.UpstreamTypeRest, provider.McpSSEProtocol: mcpserver.UpstreamTypeSSE, provider.McpStreamableProtocol: mcpserver.UpstreamTypeStreamable, } routeRewriteProtocols = map[string]bool{ provider.McpSSEProtocol: true, provider.McpStreamableProtocol: true, } mcpServerRewriteProtocols = map[string]bool{ provider.McpSSEProtocol: true, } ) var mcpServerLog = log.RegisterScope("McpServer", "Nacos Mcp Server Watcher process.") type watcher struct { provider.BaseWatcher apiv1.RegistryConfig watchingConfig map[string]bool RegistryType provider.ServiceRegistryType Status provider.WatcherStatus registryClient *NacosRegistryClient cache memory.Cache mutex *sync.Mutex stop chan struct{} isStop bool updateCacheWhenEmpty bool namespace string clusterId string authOption provider.AuthOption } type WatcherOption func(w *watcher) func NewWatcher(cache memory.Cache, opts ...WatcherOption) (provider.Watcher, error) { w := &watcher{ watchingConfig: make(map[string]bool), RegistryType: "nacos3", Status: provider.UnHealthy, cache: cache, mutex: &sync.Mutex{}, stop: make(chan struct{}), } w.NacosRefreshInterval = int64(DefaultRefreshInterval) for _, opt := range opts { opt(w) } if w.NacosNamespace == "" { w.NacosNamespace = w.NacosNamespaceId } w.NacosGroups = []string{"mcp-server"} mcpServerLog.Infof("new nacos mcp server watcher with config Name:%s", w.Name) clientConfig := constant.NewClientConfig( constant.WithTimeoutMs(DefaultNacosTimeout), constant.WithLogLevel(DefaultNacosLogLevel), constant.WithLogDir(DefaultNacosLogDir), constant.WithCacheDir(DefaultNacosCacheDir), constant.WithNotLoadCacheAtStart(DefaultNacosNotLoadCache), constant.WithLogRollingConfig(&constant.ClientLogRollingConfig{ MaxAge: DefaultNacosLogMaxAge, MaxSize: DefaultNacosLogMaxSize, MaxBackups: DefaultNacosLogMaxBackups, }), constant.WithUpdateCacheWhenEmpty(w.updateCacheWhenEmpty), constant.WithNamespaceId(w.NacosNamespaceId), constant.WithAccessKey(w.NacosAccessKey), constant.WithSecretKey(w.NacosSecretKey), constant.WithUsername(w.authOption.NacosUsername), constant.WithPassword(w.authOption.NacosPassword), ) initTimer := time.NewTimer(DefaultInitTimeout) serverConfig := []constant.ServerConfig{ *constant.NewServerConfig(w.Domain, uint64(w.Port)), } success := make(chan struct{}) go func() { client, err := NewMcpRegistryClient(clientConfig, serverConfig, w.NacosNamespaceId) if err == nil { w.registryClient = client close(success) } else { mcpServerLog.Errorf("can not create registry client, err:%v", err) } }() select { case <-initTimer.C: return nil, errors.New("new nacos mcp server watcher timeout") case <-success: return w, nil } } func WithNacosNamespaceId(nacosNamespaceId string) WatcherOption { return func(w *watcher) { if nacosNamespaceId == "" { w.NacosNamespaceId = "public" } else { w.NacosNamespaceId = nacosNamespaceId } } } func WithNacosNamespace(nacosNamespace string) WatcherOption { return func(w *watcher) { w.NacosNamespace = nacosNamespace } } func WithNacosGroups(nacosGroups []string) WatcherOption { return func(w *watcher) { w.NacosGroups = nacosGroups } } func WithNacosAddressServer(nacosAddressServer string) WatcherOption { return func(w *watcher) { w.NacosAddressServer = nacosAddressServer } } func WithNacosAccessKey(nacosAccessKey string) WatcherOption { return func(w *watcher) { w.NacosAccessKey = nacosAccessKey } } func WithNacosSecretKey(nacosSecretKey string) WatcherOption { return func(w *watcher) { w.NacosSecretKey = nacosSecretKey } } func WithNacosRefreshInterval(refreshInterval int64) WatcherOption { return func(w *watcher) { if refreshInterval < int64(DefaultRefreshIntervalLimit) { refreshInterval = int64(DefaultRefreshIntervalLimit) } w.NacosRefreshInterval = refreshInterval } } func WithType(t string) WatcherOption { return func(w *watcher) { w.Type = t } } func WithName(name string) WatcherOption { return func(w *watcher) { w.Name = name } } func WithDomain(domain string) WatcherOption { return func(w *watcher) { w.Domain = domain } } func WithPort(port uint32) WatcherOption { return func(w *watcher) { w.Port = port } } func WithMcpExportDomains(exportDomains []string) WatcherOption { return func(w *watcher) { w.McpServerExportDomains = exportDomains } } func WithMcpBaseUrl(url string) WatcherOption { return func(w *watcher) { w.McpServerBaseUrl = url } } func WithEnableMcpServer(enable *wrappers.BoolValue) WatcherOption { return func(w *watcher) { w.EnableMCPServer = enable } } func WithNamespace(ns string) WatcherOption { return func(w *watcher) { w.namespace = ns } } func WithClusterId(id string) WatcherOption { return func(w *watcher) { w.clusterId = id } } func WithAuthOption(authOption provider.AuthOption) WatcherOption { return func(w *watcher) { w.authOption = authOption } } func (w *watcher) Run() { ticker := time.NewTicker(time.Duration(w.NacosRefreshInterval)) defer ticker.Stop() w.Status = provider.ProbeWatcherStatus(w.Domain, strconv.FormatUint(uint64(w.Port), 10)) err := w.fetchAllMcpConfig() if err != nil { mcpServerLog.Errorf("first fetch mcp server config failed, err:%v", err) } else { w.Ready(true) } for { select { case <-ticker.C: err := w.fetchAllMcpConfig() if err != nil { mcpServerLog.Errorf("fetch mcp server config failed, err:%v", err) } else { w.Ready(true) } case <-w.stop: return } } } func (w *watcher) fetchAllMcpConfig() error { w.mutex.Lock() defer w.mutex.Unlock() if w.isStop { return nil } mcpConfigs, err := w.registryClient.ListMcpServer() if err != nil { return fmt.Errorf("list mcp server failed ,error %s", err.Error()) } fetchedConfigs := map[string]bool{} for _, c := range mcpConfigs { fetchedConfigs[c.Id] = true } for key := range w.watchingConfig { if _, exist := fetchedConfigs[key]; !exist { if err = w.registryClient.CancelListenToServer(key); err != nil { return fmt.Errorf("cancel listen mcp server config %s failed, error %s", key, err.Error()) } mcpServerLog.Infof("cancel listen mcp server config %s success", key) delete(w.watchingConfig, key) // clean cache for this config w.cache.UpdateConfigCache(config.GroupVersionKind{}, key, nil, true) w.UpdateService() } } subscribeFailed := atomic.NewBool(false) for key := range fetchedConfigs { if _, exist := w.watchingConfig[key]; !exist { err = w.registryClient.ListenToMcpServer(key, w.mcpServerListener(key)) if err != nil { mcpServerLog.Errorf("subscribe mcp server failed, dataId %v, errors: %v", key, err) subscribeFailed.Store(true) } else { mcpServerLog.Infof("subscribe mcp server success, dataId:%s", key) w.watchingConfig[key] = true } } } if subscribeFailed.Load() { return errors.New("subscribe services failed") } return nil } func (w *watcher) mcpServerListener(dataId string) func(info *McpServerConfig) { return func(info *McpServerConfig) { defer w.UpdateService() mcpServerLog.Infof("mcp server config callback, dataId %s", dataId) mcpServer := &provider.McpServer{} if err := json.Unmarshal([]byte(info.ServerSpecConfig), mcpServer); err != nil { mcpServerLog.Errorf("unmarshal config data to mcp server error:%v, dataId:%s", err, dataId) } // TODO support stdio and dubbo protocol if !supportedProtocols[mcpServer.Protocol] { return } if err := w.processServerConfig(dataId, info.ServiceInfo, mcpServer); err != nil { mcpServerLog.Errorf("process mcp server config error:%v, dataId:%s", err, dataId) } if err := w.processToolConfig(dataId, info.ToolsSpecConfig, info.Credentials, mcpServer); err != nil { mcpServerLog.Errorf("process tool config error:%v, dataId:%s", err, dataId) } } } func (w *watcher) processServerConfig(dataId string, services *model.Service, mcpServer *provider.McpServer) error { serviceHost := getServiceFullHostFromMcpServer(mcpServer) // generate se for mcp server serviceEntry := generateServiceEntry(serviceHost, services) if serviceEntry != nil { se := &config.Config{ Meta: config.Meta{ GroupVersionKind: gvk.ServiceEntry, Name: fmt.Sprintf("%s-%s", provider.IstioMcpAutoGeneratedSeName, strings.TrimSuffix(dataId, ".json")), Namespace: "mcp", }, Spec: serviceEntry, } w.cache.UpdateConfigCache(gvk.ServiceEntry, dataId, se, false) } // generate vs for mcp server virtualService := w.buildVirtualServiceForMcpServer(mcpServer, dataId, serviceHost, serviceEntry) if virtualService != nil { w.cache.UpdateConfigCache(gvk.VirtualService, dataId, virtualService, false) ms := w.buildMcpServerForMcpServer(virtualService.Spec.(*v1alpha3.VirtualService), dataId, mcpServer) w.cache.UpdateConfigCache(mcpserver.GvkMcpServer, dataId, ms, false) } // if protocol is sse, we should apply ConsistentHash policy for this service // if protocol is https, we should apply tls policy for this service destinationRule := generateDrForMcpServer(serviceHost, mcpServer.Protocol) if destinationRule != nil { dr := &config.Config{ Meta: config.Meta{ GroupVersionKind: gvk.DestinationRule, Name: fmt.Sprintf("%s-%s", provider.IstioMcpAutoGeneratedDrName, strings.TrimSuffix(dataId, ".json")), Namespace: w.namespace, }, Spec: destinationRule, } w.cache.UpdateConfigCache(gvk.DestinationRule, dataId, dr, false) } return nil } func (w *watcher) processToolConfig(dataId, data string, credentials map[string]interface{}, server *provider.McpServer) error { if server.Protocol != provider.HttpProtocol && server.Protocol != provider.HttpsProtocol { return nil } toolsDescription := &provider.McpToolConfig{} if err := json.Unmarshal([]byte(data), toolsDescription); err != nil { return fmt.Errorf("unmarshal toolsDescriptionRef to mcp tool config error:%v, data %v", err, data) } routeName := fmt.Sprintf("%s-%s", provider.IstioMcpAutoGeneratedHttpRouteName, strings.TrimSuffix(dataId, ".json")) rule := &provider.McpServerRule{ MatchRoute: []string{routeName}, Server: &provider.ServerConfig{ Name: server.Name, Config: map[string]interface{}{}, }, } rule.Server.Config["credentials"] = credentials var allowTools []string for _, t := range toolsDescription.Tools { convertTool := &provider.McpTool{Name: t.Name, Description: t.Description} toolMeta := toolsDescription.ToolsMeta[t.Name] if toolMeta != nil && toolMeta.Enabled { allowTools = append(allowTools, t.Name) } argsPosition, err := getArgsPositionFromToolMeta(toolMeta) if err != nil { mcpServerLog.Errorf("get args position from tool meta error:%v, tool name %v", err, t.Name) } requiredMap := sets.Set[string]{} for _, s := range t.InputSchema.Required { requiredMap.Insert(s) } for argsName, args := range t.InputSchema.Properties { convertArgs, err := parseMcpArgs(args) if err != nil { mcpServerLog.Errorf("parse mcp args error:%v, tool name %v, args name %v", err, t.Name, argsName) continue } convertArgs.Name = argsName convertArgs.Required = requiredMap.Contains(argsName) if pos, exist := argsPosition[argsName]; exist { convertArgs.Position = pos } convertTool.Args = append(convertTool.Args, convertArgs) mcpServerLog.Debugf("parseMcpArgs, toolArgs:%v", convertArgs) } requestTemplate, err := getRequestTemplateFromToolMeta(toolMeta) if err != nil { mcpServerLog.Errorf("get request template from tool meta error:%v, tool name %v", err, t.Name) continue } else { convertTool.RequestTemplate = requestTemplate } responseTemplate, err := getResponseTemplateFromToolMeta(toolMeta) if err != nil { mcpServerLog.Errorf("get response template from tool meta error:%v, tool name %v", err, t.Name) continue } else { convertTool.ResponseTemplate = responseTemplate } rule.Tools = append(rule.Tools, convertTool) } rule.AllowTools = allowTools wasmPluginConfig := &config.Config{ Meta: config.Meta{ GroupVersionKind: gvk.WasmPlugin, Namespace: w.namespace, }, Spec: rule, } w.cache.UpdateConfigCache(gvk.WasmPlugin, dataId, wasmPluginConfig, false) return nil } func (w *watcher) buildVirtualServiceForMcpServer(server *provider.McpServer, dataId, serviceName string, se *v1alpha3.ServiceEntry) *config.Config { if server == nil { return nil } // if there is no export domain, use default * hosts := w.McpServerExportDomains if len(hosts) == 0 { hosts = []string{"*"} } // find gateway resources by host var gateways []string for _, host := range hosts { cleanHost := common2.CleanHost(host) // namespace/name, name format: (istio cluster id)-host gateways = append(gateways, w.namespace+"/"+ common2.CreateConvertedName(w.clusterId, cleanHost), common2.CreateConvertedName(constants.IstioIngressGatewayName, cleanHost)) } routeName := fmt.Sprintf("%s-%s", provider.IstioMcpAutoGeneratedHttpRouteName, strings.TrimSuffix(dataId, ".json")) // path format: /{base-path}/{mcp-server-name} mergePath := "/" + server.Name if w.McpServerBaseUrl != "" && w.McpServerBaseUrl != "/" { mergePath = strings.TrimSuffix(w.McpServerBaseUrl, "/") + mergePath } vs := &v1alpha3.VirtualService{ Hosts: hosts, Gateways: gateways, Http: []*v1alpha3.HTTPRoute{{ Name: routeName, // We need to use both exact and prefix matches here to ensure a proper matching. // Also otherwise, prefix rewrite won't work correctly for Streamable HTTP transport, either. // Example: // Assume mergePath=/mcp/test prefixRewrite=/ requestPath=/mcp/test/abc // If we only use prefix match, the rewritten path will be //abc. Match: []*v1alpha3.HTTPMatchRequest{ { Uri: &v1alpha3.StringMatch{ MatchType: &v1alpha3.StringMatch_Exact{ Exact: mergePath, }, }, }, { Uri: &v1alpha3.StringMatch{ MatchType: &v1alpha3.StringMatch_Prefix{ Prefix: mergePath + "/", }, }, }, }, Route: []*v1alpha3.HTTPRouteDestination{{ Destination: &v1alpha3.Destination{ Host: serviceName, }, }}, }}, } // we should rewrite path for sse and streamble if routeRewriteProtocols[server.Protocol] { vs.Http[0].Rewrite = &v1alpha3.HTTPRewrite{ Uri: "/", } } // we should rewrite host for dns service if se != nil && se.Resolution == v1alpha3.ServiceEntry_DNS && len(se.Endpoints) > 0 { if vs.Http[0].Rewrite == nil { vs.Http[0].Rewrite = &v1alpha3.HTTPRewrite{ Authority: se.Endpoints[0].Address, } } else { vs.Http[0].Rewrite.Authority = se.Endpoints[0].Address } } mcpServerLog.Debugf("construct virtualservice %v", vs) return &config.Config{ Meta: config.Meta{ GroupVersionKind: gvk.VirtualService, Name: fmt.Sprintf("%s-%s", provider.IstioMcpAutoGeneratedVsName, dataId), Namespace: w.namespace, }, Spec: vs, } } func (w *watcher) buildMcpServerForMcpServer(vs *v1alpha3.VirtualService, dataId string, server *provider.McpServer) *config.Config { if vs == nil { return nil } domains := w.McpServerExportDomains if len(domains) == 0 { domains = []string{"*"} } name := fmt.Sprintf("%s-%s", provider.IstioMcpAutoGeneratedMcpServerName, strings.TrimSuffix(dataId, ".json")) httpRoute := vs.Http[0] pathMatchValue := "" for _, match := range httpRoute.Match { if match.Uri != nil && match.Uri.GetExact() != "" { pathMatchValue = match.Uri.GetExact() break } } protocol := server.Protocol mcpServer := &mcpserver.McpServer{ Name: name, Domains: domains, PathMatchType: mcpserver.PrefixMatchType, PathMatchValue: pathMatchValue, UpstreamType: protocolUpstreamTypeMapping[protocol], } if mcpServerRewriteProtocols[protocol] { mcpServer.EnablePathRewrite = true mcpServer.PathRewritePrefix = "/" } mcpServerLog.Debugf("construct mcpserver %v", mcpServer) return &config.Config{ Meta: config.Meta{ GroupVersionKind: mcpserver.GvkMcpServer, Name: name, Namespace: w.namespace, }, Spec: mcpServer, } } func generateDrForMcpServer(host, protocol string) *v1alpha3.DestinationRule { switch protocol { case provider.McpSSEProtocol: return &v1alpha3.DestinationRule{ Host: host, TrafficPolicy: &v1alpha3.TrafficPolicy{ LoadBalancer: &v1alpha3.LoadBalancerSettings{ LbPolicy: &v1alpha3.LoadBalancerSettings_ConsistentHash{ ConsistentHash: &v1alpha3.LoadBalancerSettings_ConsistentHashLB{ HashKey: &v1alpha3.LoadBalancerSettings_ConsistentHashLB_UseSourceIp{ UseSourceIp: true, }, }, }, }, }, } case provider.HttpsProtocol: return &v1alpha3.DestinationRule{ Host: host, TrafficPolicy: &v1alpha3.TrafficPolicy{ Tls: &v1alpha3.ClientTLSSettings{ Mode: v1alpha3.ClientTLSSettings_SIMPLE, }, }, } } return nil } func parseMcpArgs(args interface{}) (*provider.ToolArgs, error) { argsData, err := json.Marshal(args) if err != nil { return nil, err } toolArgs := &provider.ToolArgs{} if err = json.Unmarshal(argsData, toolArgs); err != nil { return nil, err } return toolArgs, nil } func getArgsPositionFromToolMeta(toolMeta *provider.ToolsMeta) (map[string]string, error) { result := map[string]string{} if toolMeta == nil { return result, nil } toolTemplate := toolMeta.Templates for kind, meta := range toolTemplate { switch kind { case provider.JsonGoTemplateType: templateData, err := json.Marshal(meta) if err != nil { return result, err } template := &provider.JsonGoTemplate{} if err = json.Unmarshal(templateData, template); err != nil { return result, err } result = mergeMaps(result, template.ArgsPosition) default: return result, fmt.Errorf("unsupport tool meta type %v", kind) } } return result, nil } func getRequestTemplateFromToolMeta(toolMeta *provider.ToolsMeta) (*provider.RequestTemplate, error) { if toolMeta == nil { return nil, nil } toolTemplate := toolMeta.Templates for kind, meta := range toolTemplate { switch kind { case provider.JsonGoTemplateType: templateData, err := json.Marshal(meta) if err != nil { return nil, err } template := &provider.JsonGoTemplate{} if err = json.Unmarshal(templateData, template); err != nil { return nil, err } return &template.RequestTemplate, nil default: return nil, fmt.Errorf("unsupport tool meta type") } } return nil, nil } func getResponseTemplateFromToolMeta(toolMeta *provider.ToolsMeta) (*provider.ResponseTemplate, error) { if toolMeta == nil { return nil, nil } toolTemplate := toolMeta.Templates for kind, meta := range toolTemplate { switch kind { case provider.JsonGoTemplateType: templateData, err := json.Marshal(meta) if err != nil { return nil, err } template := &provider.JsonGoTemplate{} if err = json.Unmarshal(templateData, template); err != nil { return nil, err } return &template.ResponseTemplate, nil default: return nil, fmt.Errorf("unsupport tool meta type") } } return nil, nil } func mergeMaps(maps ...map[string]string) map[string]string { if len(maps) == 0 { return nil } res := make(map[string]string, len(maps[0])) for _, m := range maps { for k, v := range m { res[k] = v } } return res } func getServiceFullHostFromMcpServer(server *provider.McpServer) string { if server == nil || server.RemoteServerConfig == nil || server.RemoteServerConfig.ServiceRef == nil { return "" } groupName := server.RemoteServerConfig.ServiceRef.GroupName if groupName == "DEFAULT_GROUP" { groupName = "DEFAULT-GROUP" } namespace := server.RemoteServerConfig.ServiceRef.NamespaceId serviceName := server.RemoteServerConfig.ServiceRef.ServiceName suffix := strings.Join([]string{groupName, namespace, string(provider.Nacos)}, common.DotSeparator) host := strings.Join([]string{serviceName, suffix}, common.DotSeparator) return host } func generateServiceEntry(host string, services *model.Service) *v1alpha3.ServiceEntry { if services == nil || len(services.Hosts) == 0 { return nil } portList := make([]*v1alpha3.ServicePort, 0) endpoints := make([]*v1alpha3.WorkloadEntry, 0) for _, service := range services.Hosts { protocol := common.HTTP if service.Metadata != nil && service.Metadata["protocol"] != "" { protocol = common.ParseProtocol(service.Metadata["protocol"]) } port := &v1alpha3.ServicePort{ Name: protocol.String(), Number: uint32(service.Port), Protocol: protocol.String(), } if len(portList) == 0 { portList = append(portList, port) } endpoint := &v1alpha3.WorkloadEntry{ Address: service.Ip, Ports: map[string]uint32{port.Protocol: port.Number}, Labels: service.Metadata, } endpoints = append(endpoints, endpoint) } se := &v1alpha3.ServiceEntry{ Hosts: []string{host}, Ports: portList, Location: v1alpha3.ServiceEntry_MESH_INTERNAL, Resolution: getNacosServiceResolution(services), Endpoints: endpoints, } return se } func isValidIP(ipStr string) bool { ip := net.ParseIP(ipStr) return ip != nil } func getNacosServiceResolution(services *model.Service) v1alpha3.ServiceEntry_Resolution { ipEndpoints := 0 dnsEndpoints := 0 for _, service := range services.Hosts { if isValidIP(service.Ip) { ipEndpoints = ipEndpoints + 1 } else { dnsEndpoints = dnsEndpoints + 1 } } if ipEndpoints > 0 && dnsEndpoints > 0 { mcpServerLog.Errorf("nacos service %v has both ip and dns endpoints, set to ip resolution ", services.Name) return v1alpha3.ServiceEntry_STATIC } if ipEndpoints > 0 { return v1alpha3.ServiceEntry_STATIC } return v1alpha3.ServiceEntry_DNS } func (w *watcher) Stop() { w.mutex.Lock() defer w.mutex.Unlock() for key := range w.watchingConfig { err := w.registryClient.CancelListenToServer(key) if err == nil { delete(w.watchingConfig, key) w.cache.UpdateConfigCache(config.GroupVersionKind{}, key, nil, true) mcpServerLog.Infof("cancel listen to mcp server config %v", key) } } w.isStop = true w.UpdateService() close(w.stop) w.Ready(false) w.registryClient.CloseClient() } func (w *watcher) IsHealthy() bool { return w.Status == provider.Healthy } func (w *watcher) GetRegistryType() string { return w.RegistryType.String() }