feat : support mcp server auto discovery for nacos registry (#2122)

Co-authored-by: 澄潭 <zty98751@alibaba-inc.com>
This commit is contained in:
EricaLiu
2025-04-28 21:58:17 +08:00
committed by GitHub
parent a2e4f944e9
commit 5bd20aa559
15 changed files with 1710 additions and 40 deletions

155
registry/mcp_model.go Normal file
View File

@@ -0,0 +1,155 @@
// 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 registry
const (
JsonGoTemplateType = "json-go-template"
IstioMcpAutoGeneratedPrefix = "istio-autogenerated-mcp"
IstioMcpAutoGeneratedVsName = IstioMcpAutoGeneratedPrefix + "-vs"
IstioMcpAutoGeneratedSeName = IstioMcpAutoGeneratedPrefix + "-se"
IstioMcpAutoGeneratedDrName = IstioMcpAutoGeneratedPrefix + "-dr"
IstioMcpAutoGeneratedHttpRouteName = IstioMcpAutoGeneratedPrefix + "-httproute"
DefaultMcpToolsGroup = "mcp-tools"
DefaultMcpCredentialsGroup = "credentials"
DefaultNacosServiceNamespace = "public"
StdioProtocol = "stdio"
HttpProtocol = "http"
DubboProtocol = "dubbo"
McpSSEProtocol = "mcp-sse"
McpStreambleProtocol = "mcp-streamble"
)
type McpToolArgsType string
// WasmPluginConfig Struct for mcp tool wasm plugin marshal
type WasmPluginConfig struct {
Rules []*McpServerRule `json:"_rules_"`
}
type McpServerRule struct {
MatchRoute []string `json:"_match_route_,omitempty"`
Server *ServerConfig `json:"server"`
Tools []*McpTool `json:"tools"`
}
type ServerConfig struct {
Name string `json:"name,omitempty"`
Config map[string]interface{} `json:"config,omitempty"`
AllowTools []string `json:"allowTools,omitempty"`
}
type McpTool struct {
Name string `json:"name,omitempty"`
Description string `json:"description,omitempty"`
Args []*ToolArgs `json:"args,omitempty"`
RequestTemplate *RequestTemplate `json:"requestTemplate"`
ResponseTemplate *ResponseTemplate `json:"responseTemplate"`
}
type ToolArgs struct {
Name string `json:"name,omitempty"`
Description string `json:"description,omitempty"`
Type string `json:"type,omitempty"`
Required bool `json:"required,omitempty"`
Default interface{} `json:"default,omitempty"`
Enum []interface{} `json:"enum,omitempty"`
Items []interface{} `json:"items,omitempty"`
Properties interface{} `json:"properties,omitempty"`
Position string `json:"position,omitempty"`
}
type RequestTemplate struct {
URL string `json:"url"`
Method string `json:"method"`
Headers []*RequestTemplateHeaders `json:"headers,omitempty"`
Body string `json:"body,omitempty"`
ArgsToJsonBody bool `json:"argsToJsonBody,omitempty"`
ArgsToUrlParam bool `json:"argsToUrlParam,omitempty"`
ArgsToFormBody bool `json:"argsToFormBody,omitempty"`
}
type RequestTemplateHeaders struct {
Key string `json:"key"`
Value string `json:"value"`
}
type ResponseTemplate struct {
Body string `json:"body,omitempty"`
PrependBody string `json:"prependBody,omitempty"`
AppendBody string `json:"appendBody,omitempty"`
}
// McpServer Struct for mcp server json unmarshal
type McpServer struct {
Protocol string `json:"protocol,omitempty"`
Name string `json:"name,omitempty"`
Description string `json:"description,omitempty"`
Version string `json:"version,omitempty"`
Enabled bool `json:"enabled,omitempty"`
RemoteServerConfig *RemoteServerConfig `json:"remoteServerConfig,omitempty"`
Credentials map[string]*CredentialRef `json:"credentials,omitempty"`
ToolsDescriptionRef string `json:"toolsDescriptionRef,omitempty"`
PromptDescriptionRef string `json:"promptDescriptionRef,omitempty"`
ResourceDescriptionRef string `json:"resourceDescriptionRef,omitempty"`
}
type RemoteServerConfig struct {
ServiceRef *ServiceRef `json:"serviceRef,omitempty"`
ExportPath string `json:"exportPath,omitempty"`
BackendProtocol string `json:"backendProtocol,omitempty"`
}
type CredentialRef struct {
Ref string `json:"ref,omitempty"`
}
type ServiceRef struct {
NamespaceId string `json:"namespaceId,omitempty"`
GroupName string `json:"groupName,omitempty"`
ServiceName string `json:"serviceName,omitempty"`
}
// McpToolConfig Struct for mcp tool json unmarshal
type McpToolConfig struct {
Tools []*ToolDescription `json:"tools,omitempty"`
ToolsMeta map[string]*ToolsMeta `json:"toolsMeta,omitempty"`
}
type ToolDescription struct {
Name string `json:"name,omitempty"`
Description string `json:"description,omitempty"`
InputSchema InputSchema `json:"inputSchema"`
}
type InputSchema struct {
Type string `json:"type,omitempty"`
Properties map[string]interface{} `json:"properties,omitempty"`
Required []string `json:"required,omitempty"`
}
type ToolsMeta struct {
InvokeContext map[string]string `json:"InvokeContext,omitempty"`
Enabled bool `json:"Enabled,omitempty"`
Templates map[string]interface{} `json:"Templates,omitempty"`
}
type JsonGoTemplate struct {
RequestTemplate RequestTemplate `json:"requestTemplate"`
ResponseTemplate ResponseTemplate `json:"responseTemplate"`
ArgsPosition map[string]string `json:"argsPosition,omitempty"`
}

View File

@@ -15,12 +15,21 @@
package memory
import (
"encoding/json"
"sort"
"strconv"
"sync"
"time"
higressconfig "github.com/alibaba/higress/pkg/config"
"github.com/alibaba/higress/registry"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/types/known/structpb"
"google.golang.org/protobuf/types/known/wrapperspb"
extensions "istio.io/api/extensions/v1alpha1"
"istio.io/api/networking/v1alpha3"
"istio.io/istio/pkg/config"
"istio.io/istio/pkg/config/schema/gvk"
"istio.io/pkg/log"
"github.com/alibaba/higress/pkg/common"
@@ -30,6 +39,8 @@ import (
type Cache interface {
UpdateServiceWrapper(service string, data *ServiceWrapper)
DeleteServiceWrapper(service string)
UpdateConfigCache(kind config.GroupVersionKind, key string, config *config.Config, forceDelete bool)
GetAllConfigs(kind config.GroupVersionKind) map[string]*config.Config
PurgeStaleService()
UpdateServiceEntryEndpointWrapper(service, ip, regionId, zoneId, protocol string, labels map[string]string)
GetServiceByEndpoints(requestVersions, endpoints map[string]bool, versionKey string, protocol common.Protocol) map[string][]string
@@ -44,6 +55,7 @@ func NewCache() Cache {
return &store{
mux: &sync.RWMutex{},
sew: make(map[string]*ServiceWrapper),
configs: make(map[string]map[string]*config.Config),
toBeUpdated: make([]*ServiceWrapper, 0),
toBeDeleted: make([]*ServiceWrapper, 0),
ip2services: make(map[string]map[string]bool),
@@ -54,12 +66,85 @@ func NewCache() Cache {
type store struct {
mux *sync.RWMutex
sew map[string]*ServiceWrapper
configs map[string]map[string]*config.Config
toBeUpdated []*ServiceWrapper
toBeDeleted []*ServiceWrapper
ip2services map[string]map[string]bool
deferedDelete map[string]struct{}
}
func (s *store) GetAllConfigs(kind config.GroupVersionKind) map[string]*config.Config {
s.mux.Lock()
defer s.mux.Unlock()
cfgs, exist := s.configs[kind.String()]
if !exist {
return map[string]*config.Config{}
}
if kind == gvk.WasmPlugin {
pluginConfig := &registry.WasmPluginConfig{}
var ns string
for _, cfg := range cfgs {
ns = cfg.Namespace
rule := cfg.Spec.(*registry.McpServerRule)
pluginConfig.Rules = append(pluginConfig.Rules, rule)
}
rulesBytes, err := json.Marshal(pluginConfig)
if err != nil {
log.Errorf("marshal mcp wasm plugin config error %v", err)
return map[string]*config.Config{}
}
pbs := &structpb.Struct{}
if err = protojson.Unmarshal(rulesBytes, pbs); err != nil {
log.Errorf("unmarshal mcp wasm plugin config error %v", err)
return map[string]*config.Config{}
}
wasmPlugin := &extensions.WasmPlugin{
ImagePullPolicy: extensions.PullPolicy_Always,
Phase: extensions.PluginPhase_UNSPECIFIED_PHASE,
Priority: &wrapperspb.Int32Value{Value: 30},
PluginConfig: pbs,
Url: higressconfig.McpServerWasmImageUrl,
}
return map[string]*config.Config{"wasm": &config.Config{
Meta: config.Meta{
GroupVersionKind: gvk.WasmPlugin,
Name: "istio-autogenerated-mcp-wasmplugin",
Namespace: ns,
},
Spec: wasmPlugin,
}}
}
return cfgs
}
func (s *store) UpdateConfigCache(kind config.GroupVersionKind, key string, cfg *config.Config, forceDelete bool) {
if cfg == nil && !forceDelete {
return
}
s.mux.Lock()
if forceDelete {
for _, allConfigs := range s.configs {
delete(allConfigs, key)
}
log.Infof("Delete config %s in cache", key)
} else {
if _, exist := s.configs[kind.String()]; !exist {
s.configs[kind.String()] = make(map[string]*config.Config)
}
if _, exist := s.configs[kind.String()][key]; exist {
log.Infof("Update kind %s config %s", kind.String(), key)
} else {
log.Infof("Add kind %s config %s", kind.String(), key)
}
s.configs[kind.String()][key] = cfg
}
s.mux.Unlock()
}
func (s *store) UpdateServiceEntryEndpointWrapper(service, ip, regionId, zoneId, protocol string, labels map[string]string) {
s.mux.Lock()
defer s.mux.Unlock()
@@ -226,6 +311,15 @@ func (s *store) GetAllDestinationRuleWrapper() []*ingress.WrapperDestinationRule
drwList = append(drwList, serviceEntryWrapper.DeepCopy().DestinationRuleWrapper)
}
}
configFromMcp := s.configs[gvk.DestinationRule.String()]
for _, cfg := range configFromMcp {
dr := cfg.Spec.(*v1alpha3.DestinationRule)
drwList = append(drwList, &ingress.WrapperDestinationRule{
DestinationRule: dr,
ServiceKey: ingress.ServiceKey{ServiceFQDN: dr.Host},
})
}
return drwList
}

View File

@@ -0,0 +1,180 @@
// 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 (
"fmt"
"github.com/nacos-group/nacos-sdk-go/v2/clients/config_client"
"github.com/nacos-group/nacos-sdk-go/v2/clients/naming_client"
"github.com/nacos-group/nacos-sdk-go/v2/model"
"github.com/nacos-group/nacos-sdk-go/v2/vo"
)
type MultiConfigListener struct {
configClient config_client.IConfigClient
onChange func(map[string]string)
configCache map[string]string
innerCallback func(string, string, string, string)
}
func NewMultiConfigListener(configClient config_client.IConfigClient, onChange func(map[string]string)) *MultiConfigListener {
result := &MultiConfigListener{
configClient: configClient,
configCache: make(map[string]string),
onChange: onChange,
}
result.innerCallback = func(namespace string, group string, dataId string, content string) {
result.configCache[group+DefaultJoiner+dataId] = content
result.onChange(result.configCache)
}
return result
}
func (l *MultiConfigListener) StartListen(configs []vo.ConfigParam) error {
for _, config := range configs {
content, err := l.configClient.GetConfig(vo.ConfigParam{
DataId: config.DataId,
Group: config.Group,
})
if err != nil {
return fmt.Errorf("get config %s/%s err: %v", config.Group, config.DataId, err)
}
l.configCache[config.Group+DefaultJoiner+config.DataId] = content
err = l.configClient.ListenConfig(vo.ConfigParam{
DataId: config.DataId,
Group: config.Group,
OnChange: l.innerCallback,
})
if err != nil {
return fmt.Errorf("listener to config %s/%s error: %w", config.Group, config.DataId, err)
}
}
l.onChange(l.configCache)
return nil
}
func (l *MultiConfigListener) Stop() {
l.configClient.CloseClient()
}
func (l *MultiConfigListener) CancelListen(configs []vo.ConfigParam) error {
for _, config := range configs {
if _, ok := l.configCache[config.Group+DefaultJoiner+config.DataId]; ok {
err := l.configClient.CancelListenConfig(vo.ConfigParam{
DataId: config.DataId,
Group: config.Group,
})
if err != nil {
return fmt.Errorf("cancel config %s/%s error: %w", config.Group, config.DataId, err)
}
delete(l.configCache, config.Group+config.DataId)
}
}
return nil
}
type ServiceCache struct {
services map[string]*NacosServiceRef
client naming_client.INamingClient
}
type NacosServiceRef struct {
refs map[string]func([]model.Instance)
callback func(services []model.Instance, err error)
instances *[]model.Instance
}
func NewServiceCache(client naming_client.INamingClient) *ServiceCache {
return &ServiceCache{
client: client,
services: make(map[string]*NacosServiceRef),
}
}
func (c *ServiceCache) AddListener(group string, serviceName string, key string, callback func([]model.Instance)) error {
uniqueServiceName := c.makeServiceUniqueName(group, serviceName)
if _, ok := c.services[uniqueServiceName]; !ok {
instances, err := c.client.SelectAllInstances(vo.SelectAllInstancesParam{
GroupName: group,
ServiceName: serviceName,
})
if err != nil {
return err
}
ref := &NacosServiceRef{
refs: map[string]func([]model.Instance){},
instances: &instances,
}
ref.callback = func(services []model.Instance, err error) {
ref.instances = &services
for _, refCallback := range ref.refs {
refCallback(*ref.instances)
}
}
c.services[uniqueServiceName] = ref
err = c.client.Subscribe(&vo.SubscribeParam{
GroupName: group,
ServiceName: serviceName,
SubscribeCallback: ref.callback,
})
if err != nil {
return err
}
}
ref := c.services[uniqueServiceName]
ref.refs[key] = callback
callback(*ref.instances)
return nil
}
func (c *ServiceCache) RemoveListener(group string, serviceName string, key string) error {
if ref, ok := c.services[c.makeServiceUniqueName(group, serviceName)]; ok {
delete(ref.refs, key)
if len(ref.refs) == 0 {
err := c.client.Unsubscribe(&vo.SubscribeParam{
GroupName: group,
ServiceName: serviceName,
SubscribeCallback: ref.callback,
})
delete(c.services, c.makeServiceUniqueName(group, serviceName))
if err != nil {
return err
}
}
}
return nil
}
func (c *ServiceCache) makeServiceUniqueName(group string, serviceName string) string {
return fmt.Sprintf("%s-%s", group, serviceName)
}
func (c *ServiceCache) Stop() {
c.client.CloseClient()
}

View File

@@ -0,0 +1,989 @@
// 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"
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/clients"
"github.com/nacos-group/nacos-sdk-go/v2/clients/config_client"
"github.com/nacos-group/nacos-sdk-go/v2/common/constant"
"github.com/nacos-group/nacos-sdk-go/v2/model"
"github.com/nacos-group/nacos-sdk-go/v2/vo"
"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
DefaultRefreshInterval = time.Second * 30
DefaultRefreshIntervalLimit = time.Second * 10
DefaultFetchPageSize = 50
DefaultJoiner = "@@"
NacosV3LabelKey = "isV3"
)
var mcpServerLog = log.RegisterScope("McpServer", "Nacos Mcp Server Watcher process.")
type watcher struct {
provider.BaseWatcher
apiv1.RegistryConfig
watchingConfig map[string]bool
watchingConfigRefs map[string]sets.Set[string]
configToConfigListener map[string]*MultiConfigListener
serviceCache map[string]*ServiceCache
configToService map[string]string
credentialKeyToName map[string]map[string]string
RegistryType provider.ServiceRegistryType
Status provider.WatcherStatus
configClient config_client.IConfigClient
serverConfig []constant.ServerConfig
cache memory.Cache
mutex *sync.Mutex
subMutex *sync.Mutex
callbackMutex *sync.Mutex
stop chan struct{}
isStop bool
updateCacheWhenEmpty bool
nacosClientConfig *constant.ClientConfig
namespace string
clusterId string
}
type WatcherOption func(w *watcher)
func NewWatcher(cache memory.Cache, opts ...WatcherOption) (provider.Watcher, error) {
w := &watcher{
watchingConfig: make(map[string]bool),
configToService: make(map[string]string),
watchingConfigRefs: make(map[string]sets.Set[string]),
configToConfigListener: make(map[string]*MultiConfigListener),
credentialKeyToName: make(map[string]map[string]string),
serviceCache: map[string]*ServiceCache{},
RegistryType: "nacos3",
Status: provider.UnHealthy,
cache: cache,
mutex: &sync.Mutex{},
subMutex: &sync.Mutex{},
callbackMutex: &sync.Mutex{},
stop: make(chan struct{}),
}
w.NacosRefreshInterval = int64(DefaultRefreshInterval)
for _, opt := range opts {
opt(w)
}
if w.NacosNamespace == "" {
w.NacosNamespace = w.NacosNamespaceId
}
mcpServerLog.Infof("new nacos mcp server watcher with config Name:%s", w.Name)
w.nacosClientConfig = constant.NewClientConfig(
constant.WithTimeoutMs(DefaultNacosTimeout),
constant.WithLogLevel(DefaultNacosLogLevel),
constant.WithLogDir(DefaultNacosLogDir),
constant.WithCacheDir(DefaultNacosCacheDir),
constant.WithNotLoadCacheAtStart(DefaultNacosNotLoadCache),
constant.WithLogRollingConfig(&constant.ClientLogRollingConfig{
MaxAge: DefaultNacosLogMaxAge,
}),
constant.WithUpdateCacheWhenEmpty(w.updateCacheWhenEmpty),
constant.WithNamespaceId(w.NacosNamespaceId),
constant.WithAccessKey(w.NacosAccessKey),
constant.WithSecretKey(w.NacosSecretKey),
)
initTimer := time.NewTimer(DefaultInitTimeout)
w.serverConfig = []constant.ServerConfig{
*constant.NewServerConfig(w.Domain, uint64(w.Port)),
}
success := make(chan struct{})
go func() {
configClient, err := clients.NewConfigClient(vo.NacosClientParam{
ClientConfig: w.nacosClientConfig,
ServerConfigs: w.serverConfig,
})
if err == nil {
w.configClient = configClient
close(success)
} else {
mcpServerLog.Errorf("can not create naming client, err:%v", err)
}
}()
select {
case <-initTimer.C:
return nil, errors.New("new nacos mcp server watcher timeout")
case <-success:
return w, nil
}
}
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 WithNacosNamespaceId(nacosNamespaceId string) WatcherOption {
return func(w *watcher) {
if nacosNamespaceId == "" {
w.NacosNamespaceId = "nacos-default-mcp"
} 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) {
if len(nacosGroups) == 0 {
w.NacosGroups = []string{"mcp-server"}
} else {
w.NacosGroups = nacosGroups
}
}
}
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 (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
}
fetchedConfigs := make(map[string]bool)
var tries int
isV3 := true
if w.EnableMCPServer != nil {
isV3 = w.EnableMCPServer.GetValue()
}
for _, groupName := range w.NacosGroups {
for page := 1; ; page++ {
ss, err := w.configClient.SearchConfig(vo.SearchConfigParam{
Group: groupName,
Search: "blur",
PageNo: page,
PageSize: DefaultFetchPageSize,
IsV3: isV3,
})
if err != nil {
if tries > 10 {
return err
}
mcpServerLog.Errorf("fetch nacos config list failed, err:%v, pageNo:%d", err, page)
page--
tries++
continue
}
for _, item := range ss.PageItems {
fetchedConfigs[groupName+DefaultJoiner+item.DataId] = true
}
if len(ss.PageItems) < DefaultFetchPageSize {
break
}
}
}
for key := range w.watchingConfig {
if _, exist := fetchedConfigs[key]; !exist {
s := strings.Split(key, DefaultJoiner)
err := w.unsubscribe(s[0], s[1])
if err != nil {
return err
}
delete(w.watchingConfig, key)
}
}
wg := sync.WaitGroup{}
subscribeFailed := atomic.NewBool(false)
watchingKeys := make(chan string, len(fetchedConfigs))
for key := range fetchedConfigs {
s := strings.Split(key, DefaultJoiner)
if _, exist := w.watchingConfig[key]; !exist {
wg.Add(1)
go func(k string) {
err := w.subscribe(s[0], s[1])
if err != nil {
subscribeFailed.Store(true)
mcpServerLog.Errorf("subscribe failed, group: %v, service: %v, errors: %v", s[0], s[1], err)
} else {
watchingKeys <- k
}
wg.Done()
}(key)
}
}
wg.Wait()
close(watchingKeys)
for key := range watchingKeys {
w.watchingConfig[key] = true
}
if subscribeFailed.Load() {
return errors.New("subscribe services failed")
}
return nil
}
func (w *watcher) unsubscribe(groupName string, dataId string) error {
mcpServerLog.Infof("unsubscribe mcp server, groupName:%s, dataId:%s", groupName, dataId)
defer w.UpdateService()
err := w.configClient.CancelListenConfig(vo.ConfigParam{
DataId: dataId,
Group: groupName,
})
if err != nil {
mcpServerLog.Errorf("unsubscribe mcp server error:%v, groupName:%s, dataId:%s", err, groupName, dataId)
return err
}
key := strings.Join([]string{w.Name, w.NacosNamespace, groupName, dataId}, DefaultJoiner)
w.configToConfigListener[key].Stop()
delete(w.watchingConfigRefs, key)
delete(w.configToConfigListener, key)
// remove service for this config
configKey := strings.Join([]string{groupName, dataId}, DefaultJoiner)
svcInfo := w.configToService[configKey]
split := strings.Split(svcInfo, DefaultJoiner)
svcNamespace := split[0]
svcGroup := split[1]
svcName := split[2]
if w.serviceCache[svcNamespace] != nil {
err = w.serviceCache[svcNamespace].RemoveListener(svcGroup, svcName, configKey)
if err != nil {
mcpServerLog.Errorf("remove service listener error:%v, groupName:%s, dataId:%s", err, groupName, dataId)
}
}
delete(w.configToService, configKey)
w.cache.UpdateConfigCache(config.GroupVersionKind{}, key, nil, true)
return nil
}
func (w *watcher) subscribe(groupName string, dataId string) error {
mcpServerLog.Infof("subscribe mcp server, groupName:%s, dataId:%s", groupName, dataId)
// first we get this config and callback manually
content, err := w.configClient.GetConfig(vo.ConfigParam{
DataId: dataId,
Group: groupName,
})
if err != nil {
mcpServerLog.Errorf("get config %s/%s err: %v", groupName, dataId, err)
} else {
w.getConfigCallback(w.NacosNamespace, groupName, dataId, content)
}
// second, we set callback for this config
err = w.configClient.ListenConfig(vo.ConfigParam{
DataId: dataId,
Group: groupName,
OnChange: w.getConfigCallback,
})
if err != nil {
mcpServerLog.Errorf("subscribe mcp server error:%v, groupName:%s, dataId:%s", err, groupName, dataId)
return err
}
return nil
}
func (w *watcher) getConfigCallback(namespace, group, dataId, data string) {
mcpServerLog.Infof("get config callback, namespace:%s, groupName:%s, dataId:%s", namespace, group, dataId)
if data == "" {
return
}
key := strings.Join([]string{w.Name, w.NacosNamespace, group, dataId}, DefaultJoiner)
routeName := fmt.Sprintf("%s-%s-%s", provider.IstioMcpAutoGeneratedHttpRouteName, group, strings.TrimSuffix(dataId, ".json"))
mcpServer := &provider.McpServer{}
if err := json.Unmarshal([]byte(data), mcpServer); err != nil {
mcpServerLog.Errorf("Unmarshal config data to mcp server error:%v, namespace:%s, groupName:%s, dataId:%s", err, namespace, group, dataId)
return
}
if mcpServer.Protocol == provider.StdioProtocol || mcpServer.Protocol == provider.DubboProtocol || mcpServer.Protocol == provider.McpSSEProtocol {
return
}
// process mcp service
w.subMutex.Lock()
defer w.subMutex.Unlock()
if err := w.buildServiceEntryForMcpServer(mcpServer, group, dataId); err != nil {
mcpServerLog.Errorf("build service entry for mcp server failed, namespace %v, group: %v, dataId %v, errors: %v", namespace, group, dataId, err)
}
// process mcp wasm
// only generate wasm plugin for http protocol mcp server
if mcpServer.Protocol != provider.HttpProtocol {
return
}
if _, exist := w.configToConfigListener[key]; !exist {
w.configToConfigListener[key] = NewMultiConfigListener(w.configClient, w.multiCallback(mcpServer, routeName, key))
}
if _, exist := w.watchingConfigRefs[key]; !exist {
w.watchingConfigRefs[key] = sets.New[string]()
}
listener := w.configToConfigListener[key]
curRef := sets.Set[string]{}
// add description ref
curRef.Insert(strings.Join([]string{provider.DefaultMcpToolsGroup, mcpServer.ToolsDescriptionRef}, DefaultJoiner))
// add credential ref
credentialNameMap := map[string]string{}
for name, ref := range mcpServer.Credentials {
credKey := strings.Join([]string{provider.DefaultMcpCredentialsGroup, ref.Ref}, DefaultJoiner)
curRef.Insert(credKey)
credentialNameMap[credKey] = name
}
w.callbackMutex.Lock()
w.credentialKeyToName[key] = credentialNameMap
w.callbackMutex.Unlock()
toBeAdd := curRef.Difference(w.watchingConfigRefs[key])
toBeDelete := w.watchingConfigRefs[key].Difference(curRef)
var toBeListen, toBeUnListen []vo.ConfigParam
for item, _ := range toBeAdd {
split := strings.Split(item, DefaultJoiner)
toBeListen = append(toBeListen, vo.ConfigParam{
Group: split[0],
DataId: split[1],
})
}
for item, _ := range toBeDelete {
split := strings.Split(item, DefaultJoiner)
toBeUnListen = append(toBeUnListen, vo.ConfigParam{
Group: split[0],
DataId: split[1],
})
}
// listen description and credential config
if len(toBeListen) > 0 {
if err := listener.StartListen(toBeListen); err != nil {
mcpServerLog.Errorf("listen config ref failed, group: %v, dataId %v, errors: %v", group, dataId, err)
}
}
// cancel listen description and credential config
if len(toBeUnListen) > 0 {
if err := listener.CancelListen(toBeUnListen); err != nil {
mcpServerLog.Errorf("cancel listen config ref failed, group: %v, dataId %v, errors: %v", group, dataId, err)
}
}
}
func (w *watcher) multiCallback(server *provider.McpServer, routeName, configKey string) func(map[string]string) {
callback := func(configs map[string]string) {
defer w.UpdateService()
mcpServerLog.Infof("callback, ref config changed: %s", configKey)
rule := &provider.McpServerRule{
MatchRoute: []string{routeName},
Server: &provider.ServerConfig{
Name: server.Name,
Config: map[string]interface{}{},
},
}
// process mcp credential
credentialConfig := map[string]interface{}{}
for key, data := range configs {
if strings.HasPrefix(key, provider.DefaultMcpToolsGroup) {
// skip mcp tool description
continue
}
var cred interface{}
if err := json.Unmarshal([]byte(data), &cred); err != nil {
mcpServerLog.Errorf("unmarshal credential data %v to map error:%v", key, err)
}
w.callbackMutex.Lock()
name := w.credentialKeyToName[configKey][key]
w.callbackMutex.Unlock()
credentialConfig[name] = cred
}
rule.Server.Config["credentials"] = credentialConfig
// process mcp tool description
var allowTools []string
for key, toolData := range configs {
if strings.HasPrefix(key, provider.DefaultMcpCredentialsGroup) {
// skip mcp credentials
continue
}
toolsDescription := &provider.McpToolConfig{}
if err := json.Unmarshal([]byte(toolData), toolsDescription); err != nil {
mcpServerLog.Errorf("unmarshal toolsDescriptionRef to mcp tool config error:%v", err)
}
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)
} 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)
} else {
convertTool.ResponseTemplate = responseTemplate
}
rule.Tools = append(rule.Tools, convertTool)
}
}
rule.Server.AllowTools = allowTools
wasmPluginConfig := &config.Config{
Meta: config.Meta{
GroupVersionKind: gvk.WasmPlugin,
Namespace: w.namespace,
},
Spec: rule,
}
w.cache.UpdateConfigCache(gvk.WasmPlugin, configKey, wasmPluginConfig, false)
}
return callback
}
func (w *watcher) buildServiceEntryForMcpServer(mcpServer *provider.McpServer, configGroup, dataId string) error {
if mcpServer == nil || mcpServer.RemoteServerConfig == nil || mcpServer.RemoteServerConfig.ServiceRef == nil {
return nil
}
mcpServerLog.Debugf("ServiceRef %v for %v", mcpServer.RemoteServerConfig.ServiceRef, dataId)
configKey := strings.Join([]string{configGroup, dataId}, DefaultJoiner)
serviceGroup := mcpServer.RemoteServerConfig.ServiceRef.GroupName
serviceNamespace := mcpServer.RemoteServerConfig.ServiceRef.NamespaceId
serviceName := mcpServer.RemoteServerConfig.ServiceRef.ServiceName
if serviceNamespace == "" {
serviceNamespace = provider.DefaultNacosServiceNamespace
}
// update config to service and unsubscribe old service
curSvcKey := strings.Join([]string{serviceNamespace, serviceGroup, serviceName}, DefaultJoiner)
if svcKey, exist := w.configToService[configKey]; exist && svcKey != curSvcKey {
split := strings.Split(svcKey, DefaultJoiner)
if svcCache, has := w.serviceCache[split[0]]; has {
if err := svcCache.RemoveListener(split[1], split[2], configKey); err != nil {
mcpServerLog.Errorf("remove listener error:%v", err)
}
}
}
w.configToService[configKey] = curSvcKey
if _, exist := w.serviceCache[serviceNamespace]; !exist {
namingConfig := constant.NewClientConfig(
constant.WithTimeoutMs(DefaultNacosTimeout),
constant.WithLogLevel(DefaultNacosLogLevel),
constant.WithLogDir(DefaultNacosLogDir),
constant.WithCacheDir(DefaultNacosCacheDir),
constant.WithNotLoadCacheAtStart(DefaultNacosNotLoadCache),
constant.WithLogRollingConfig(&constant.ClientLogRollingConfig{
MaxAge: DefaultNacosLogMaxAge,
}),
constant.WithUpdateCacheWhenEmpty(w.updateCacheWhenEmpty),
constant.WithNamespaceId(serviceNamespace),
constant.WithAccessKey(w.NacosAccessKey),
constant.WithSecretKey(w.NacosSecretKey),
)
client, err := clients.NewNamingClient(vo.NacosClientParam{
ClientConfig: namingConfig,
ServerConfigs: w.serverConfig,
})
if err == nil {
w.serviceCache[serviceNamespace] = NewServiceCache(client)
} else {
return fmt.Errorf("can not create naming client err:%v", err)
}
}
svcCache := w.serviceCache[serviceNamespace]
err := svcCache.AddListener(serviceGroup, serviceName, configKey, w.getServiceCallback(mcpServer, configGroup, dataId))
if err != nil {
return fmt.Errorf("add listener for dataId %v, service %s/%s error:%v", dataId, serviceGroup, serviceName, err)
}
return nil
}
func (w *watcher) getServiceCallback(server *provider.McpServer, configGroup, dataId string) func(services []model.Instance) {
groupName := server.RemoteServerConfig.ServiceRef.GroupName
if groupName == "DEFAULT_GROUP" {
groupName = "DEFAULT-GROUP"
}
namespace := server.RemoteServerConfig.ServiceRef.NamespaceId
serviceName := server.RemoteServerConfig.ServiceRef.ServiceName
path := server.RemoteServerConfig.ExportPath
protocol := server.Protocol
return func(services []model.Instance) {
defer w.UpdateService()
configKey := strings.Join([]string{w.Name, w.NacosNamespace, configGroup, dataId}, DefaultJoiner)
host := getNacosServiceFullHost(groupName, namespace, serviceName)
mcpServerLog.Infof("callback for %s/%s, serviceName : %s", configGroup, dataId, host)
serviceEntry := w.generateServiceEntry(host, services)
se := &config.Config{
Meta: config.Meta{
GroupVersionKind: gvk.ServiceEntry,
Name: fmt.Sprintf("%s-%s-%s", provider.IstioMcpAutoGeneratedSeName, configGroup, strings.TrimSuffix(dataId, ".json")),
Namespace: w.namespace,
},
Spec: serviceEntry,
}
if protocol == provider.McpSSEProtocol {
destinationRule := w.generateDrForSSEService(host)
dr := &config.Config{
Meta: config.Meta{
GroupVersionKind: gvk.DestinationRule,
Name: fmt.Sprintf("%s-%s-%s", provider.IstioMcpAutoGeneratedDrName, configGroup, strings.TrimSuffix(dataId, ".json")),
Namespace: w.namespace,
},
Spec: destinationRule,
}
w.cache.UpdateConfigCache(gvk.DestinationRule, configKey, dr, false)
}
w.cache.UpdateConfigCache(gvk.ServiceEntry, configKey, se, false)
vs := w.buildVirtualServiceForMcpServer(serviceEntry, configGroup, dataId, path, server.Name)
w.cache.UpdateConfigCache(gvk.VirtualService, configKey, vs, false)
}
}
func (w *watcher) buildVirtualServiceForMcpServer(serviceentry *v1alpha3.ServiceEntry, group, dataId, path, serverName string) *config.Config {
if serviceentry == nil {
return nil
}
hosts := w.McpServerExportDomains
if len(hosts) == 0 {
hosts = []string{"*"}
}
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-%s", provider.IstioMcpAutoGeneratedHttpRouteName, group, strings.TrimSuffix(dataId, ".json"))
mergePath := "/" + serverName
if w.McpServerBaseUrl != "/" {
mergePath = strings.TrimSuffix(w.McpServerBaseUrl, "/") + mergePath
}
if path != "/" {
mergePath = mergePath + "/" + strings.TrimPrefix(path, "/")
}
vs := &v1alpha3.VirtualService{
Hosts: hosts,
Gateways: gateways,
Http: []*v1alpha3.HTTPRoute{{
Name: routeName,
Match: []*v1alpha3.HTTPMatchRequest{{
Uri: &v1alpha3.StringMatch{
MatchType: &v1alpha3.StringMatch_Prefix{
Prefix: mergePath,
},
},
}},
Rewrite: &v1alpha3.HTTPRewrite{
Uri: path,
},
Route: []*v1alpha3.HTTPRouteDestination{{
Destination: &v1alpha3.Destination{
Host: serviceentry.Hosts[0],
Port: &v1alpha3.PortSelector{
Number: serviceentry.Ports[0].Number,
},
},
}},
}},
}
mcpServerLog.Debugf("construct virtualservice %v", vs)
return &config.Config{
Meta: config.Meta{
GroupVersionKind: gvk.VirtualService,
Name: fmt.Sprintf("%s-%s-%s", provider.IstioMcpAutoGeneratedVsName, group, dataId),
Namespace: w.namespace,
},
Spec: vs,
}
}
func (w *watcher) generateServiceEntry(host string, services []model.Instance) *v1alpha3.ServiceEntry {
portList := make([]*v1alpha3.ServicePort, 0)
endpoints := make([]*v1alpha3.WorkloadEntry, 0)
isDnsService := false
for _, service := range services {
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)
}
if !isValidIP(service.Ip) {
isDnsService = true
}
endpoint := &v1alpha3.WorkloadEntry{
Address: service.Ip,
Ports: map[string]uint32{port.Protocol: port.Number},
Labels: service.Metadata,
}
endpoints = append(endpoints, endpoint)
}
resolution := v1alpha3.ServiceEntry_STATIC
if isDnsService {
resolution = v1alpha3.ServiceEntry_DNS
}
se := &v1alpha3.ServiceEntry{
Hosts: []string{host},
Ports: portList,
Location: v1alpha3.ServiceEntry_MESH_INTERNAL,
Resolution: resolution,
Endpoints: endpoints,
}
return se
}
func (w *watcher) generateDrForSSEService(host string) *v1alpha3.DestinationRule {
dr := &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,
},
},
},
},
},
}
return dr
}
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 getNacosServiceFullHost(groupName, namespace, serviceName string) string {
suffix := strings.Join([]string{groupName, namespace, string(provider.Nacos)}, common.DotSeparator)
host := strings.Join([]string{serviceName, suffix}, common.DotSeparator)
return host
}
func (w *watcher) Stop() {
w.mutex.Lock()
defer w.mutex.Unlock()
mcpServerLog.Infof("unsubscribe all configs")
for key := range w.watchingConfig {
s := strings.Split(key, DefaultJoiner)
err := w.unsubscribe(s[0], s[1])
if err == nil {
delete(w.watchingConfig, key)
}
}
mcpServerLog.Infof("stop all service nameing client")
for _, client := range w.serviceCache {
client.Stop()
}
w.isStop = true
mcpServerLog.Infof("stop all config client")
mcpServerLog.Infof("watcher %v stop", w.Name)
close(w.stop)
w.Ready(false)
}
func (w *watcher) IsHealthy() bool {
return w.Status == provider.Healthy
}
func (w *watcher) GetRegistryType() string {
return w.RegistryType.String()
}
func isValidIP(ipStr string) bool {
ip := net.ParseIP(ipStr)
return ip != nil
}

View File

@@ -23,6 +23,7 @@ import (
"sync"
"time"
"github.com/alibaba/higress/registry/nacos/mcpserver"
"istio.io/pkg/log"
apiv1 "github.com/alibaba/higress/api/networking/v1"
@@ -50,9 +51,10 @@ type Reconciler struct {
serviceUpdate func()
client kube.Client
namespace string
clusterId string
}
func NewReconciler(serviceUpdate func(), client kube.Client, namespace string) *Reconciler {
func NewReconciler(serviceUpdate func(), client kube.Client, namespace, clusterId string) *Reconciler {
return &Reconciler{
Cache: memory.NewCache(),
registries: make(map[string]*apiv1.RegistryConfig),
@@ -60,6 +62,7 @@ func NewReconciler(serviceUpdate func(), client kube.Client, namespace string) *
serviceUpdate: serviceUpdate,
client: client,
namespace: namespace,
clusterId: clusterId,
}
}
@@ -183,6 +186,26 @@ func (r *Reconciler) generateWatcherFromRegistryConfig(registry *apiv1.RegistryC
nacosv2.WithNacosRefreshInterval(registry.NacosRefreshInterval),
nacosv2.WithAuthOption(authOption),
)
case string(Nacos3):
watcher, err = mcpserver.NewWatcher(
r.Cache,
mcpserver.WithType(registry.Type),
mcpserver.WithName(registry.Name),
mcpserver.WithNacosAddressServer(registry.NacosAddressServer),
mcpserver.WithDomain(registry.Domain),
mcpserver.WithPort(registry.Port),
mcpserver.WithNacosAccessKey(registry.NacosAccessKey),
mcpserver.WithNacosSecretKey(registry.NacosSecretKey),
mcpserver.WithNacosNamespaceId(registry.NacosNamespaceId),
mcpserver.WithNacosNamespace(registry.NacosNamespace),
mcpserver.WithNacosGroups(registry.NacosGroups),
mcpserver.WithNacosRefreshInterval(registry.NacosRefreshInterval),
mcpserver.WithMcpExportDomains(registry.McpServerExportDomains),
mcpserver.WithMcpBaseUrl(registry.McpServerBaseUrl),
mcpserver.WithEnableMcpServer(registry.EnableMCPServer),
mcpserver.WithClusterId(r.clusterId),
mcpserver.WithNamespace(r.namespace),
)
case string(Zookeeper):
watcher, err = zookeeper.NewWatcher(
r.Cache,

View File

@@ -25,6 +25,7 @@ const (
Consul ServiceRegistryType = "consul"
Nacos ServiceRegistryType = "nacos"
Nacos2 ServiceRegistryType = "nacos2"
Nacos3 ServiceRegistryType = "nacos3"
Static ServiceRegistryType = "static"
DNS ServiceRegistryType = "dns"
Healthy WatcherStatus = "healthy"