mirror of
https://github.com/alibaba/higress.git
synced 2026-06-09 12:47:28 +08:00
feat : support mcp server auto discovery for nacos registry (#2122)
Co-authored-by: 澄潭 <zty98751@alibaba-inc.com>
This commit is contained in:
155
registry/mcp_model.go
Normal file
155
registry/mcp_model.go
Normal 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"`
|
||||
}
|
||||
@@ -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 := ®istry.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
|
||||
}
|
||||
|
||||
|
||||
180
registry/nacos/mcpserver/util.go
Normal file
180
registry/nacos/mcpserver/util.go
Normal 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()
|
||||
}
|
||||
989
registry/nacos/mcpserver/watcher.go
Normal file
989
registry/nacos/mcpserver/watcher.go
Normal 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
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user