mirror of
https://github.com/alibaba/higress.git
synced 2026-03-01 07:00:50 +08:00
556 lines
16 KiB
Go
556 lines
16 KiB
Go
// 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"
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"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/clients/naming_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"
|
|
)
|
|
|
|
const McpServerVersionGroup = "mcp-server-versions"
|
|
const McpServerSpecGroup = "mcp-server"
|
|
const McpToolSpecGroup = "mcp-tools"
|
|
const SystemConfigIdPrefix = "system-"
|
|
const CredentialPrefix = "credentials-"
|
|
const DefaultNacosListConfigMode = "blur"
|
|
|
|
const ListMcpServerConfigIdPattern = "*mcp-versions.json"
|
|
|
|
const DefaultNacosListConfigPageSize = 50
|
|
|
|
type ServerSpecInfo struct {
|
|
RemoteServerConfig *RemoteServerConfig `json:"remoteServerConfig"`
|
|
}
|
|
|
|
type RemoteServerConfig struct {
|
|
ServiceRef *ServiceRef `json:"serviceRef"`
|
|
}
|
|
|
|
type ServiceRef struct {
|
|
ServiceName string `json:"serviceName"`
|
|
GroupName string `json:"groupName"`
|
|
NamespaceId string `json:"namespaceId"`
|
|
}
|
|
|
|
type NacosRegistryClient struct {
|
|
namespaceId string
|
|
configClient config_client.IConfigClient
|
|
namingClient naming_client.INamingClient
|
|
servers map[string]*ServerContext
|
|
}
|
|
|
|
type VersionedMcpServerInfo struct {
|
|
serverInfo *BasicMcpServerInfo
|
|
version string
|
|
}
|
|
|
|
type ServerContext struct {
|
|
id string
|
|
versionedMcpServerInfo *VersionedMcpServerInfo
|
|
serverChangeListener McpServerListener
|
|
configsMap map[string]*ConfigListenerWrap
|
|
serviceInfo *model.Service
|
|
namingCallback func(services []model.Instance, err error)
|
|
}
|
|
|
|
type McpServerConfig struct {
|
|
ServerSpecConfig string
|
|
ToolsSpecConfig string
|
|
ServiceInfo *model.Service
|
|
Credentials map[string]interface{}
|
|
}
|
|
|
|
type ConfigListenerWrap struct {
|
|
dataId string
|
|
group string
|
|
data string
|
|
listener func(namespace, group, dataId, data string)
|
|
}
|
|
|
|
type BasicMcpServerInfo struct {
|
|
Name string `json:"name"`
|
|
Id string `json:"id"`
|
|
FrontProtocol string `json:"frontProtocol"`
|
|
Protocol string `json:"protocol"`
|
|
}
|
|
|
|
type VersionsMcpServerInfo struct {
|
|
BasicMcpServerInfo
|
|
LatestPublishedVersion string `json:"latestPublishedVersion"`
|
|
Versions []*VersionDetail `json:"versionDetails"`
|
|
}
|
|
|
|
type VersionDetail struct {
|
|
Version string `json:"version"`
|
|
IsLatest bool `json:"is_latest"`
|
|
}
|
|
|
|
type McpServerListener func(info *McpServerConfig)
|
|
|
|
func NewMcpRegistryClient(clientConfig *constant.ClientConfig, serverConfig []constant.ServerConfig, namespaceId string) (*NacosRegistryClient, error) {
|
|
clientParam := vo.NacosClientParam{
|
|
ClientConfig: clientConfig,
|
|
ServerConfigs: serverConfig,
|
|
}
|
|
configClient, err := clients.NewConfigClient(clientParam)
|
|
namingClient, err := clients.NewNamingClient(clientParam)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &NacosRegistryClient{
|
|
namespaceId: namespaceId,
|
|
configClient: configClient,
|
|
namingClient: namingClient,
|
|
servers: map[string]*ServerContext{},
|
|
}, nil
|
|
}
|
|
|
|
func (n *NacosRegistryClient) listMcpServerConfigs() ([]model.ConfigItem, error) {
|
|
currentPageNum := 1
|
|
result := make([]model.ConfigItem, 0)
|
|
for {
|
|
configPage, err := n.configClient.SearchConfig(vo.SearchConfigParam{
|
|
Search: DefaultNacosListConfigMode,
|
|
DataId: ListMcpServerConfigIdPattern,
|
|
Group: McpServerVersionGroup,
|
|
PageNo: currentPageNum,
|
|
PageSize: DefaultNacosListConfigPageSize,
|
|
})
|
|
|
|
if err != nil {
|
|
mcpServerLog.Errorf("List mcp server configs for page size %d, page number %d error %v", currentPageNum, DefaultNacosListConfigPageSize, err)
|
|
break
|
|
}
|
|
|
|
if configPage == nil {
|
|
mcpServerLog.Errorf("List mcp server configs for page size %d, page number %d null", currentPageNum, DefaultNacosListConfigPageSize)
|
|
break
|
|
}
|
|
|
|
result = append(result, configPage.PageItems...)
|
|
|
|
if configPage.PageNumber >= configPage.PagesAvailable {
|
|
break
|
|
}
|
|
|
|
currentPageNum += 1
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// ListMcpServer List all mcp server from nacos mcp registry /**
|
|
func (n *NacosRegistryClient) ListMcpServer() ([]BasicMcpServerInfo, error) {
|
|
configs, err := n.listMcpServerConfigs()
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var result []BasicMcpServerInfo
|
|
for _, config := range configs {
|
|
mcpServerBasicConfig, err := n.configClient.GetConfig(vo.ConfigParam{
|
|
Group: McpServerVersionGroup,
|
|
DataId: config.DataId,
|
|
})
|
|
|
|
if err != nil {
|
|
mcpServerLog.Errorf("Get mcp server version config (dataId: %s) error, %v", config.DataId, err)
|
|
continue
|
|
}
|
|
|
|
if mcpServerBasicConfig == "" {
|
|
mcpServerLog.Infof("get empty mcp server version config (dataId: %s)", config.DataId)
|
|
continue
|
|
}
|
|
|
|
mcpServer := BasicMcpServerInfo{}
|
|
err = json.Unmarshal([]byte(mcpServerBasicConfig), &mcpServer)
|
|
if err != nil {
|
|
mcpServerLog.Errorf("Parse mcp server version config error %v", err)
|
|
continue
|
|
}
|
|
|
|
if !isMcpServerShouldBeDiscoveryForGateway(mcpServer) {
|
|
mcpServerLog.Infof("mcp server %s don't need to be discovered for gateway, skip it", mcpServerBasicConfig)
|
|
continue
|
|
}
|
|
|
|
result = append(result, mcpServer)
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func isMcpServerShouldBeDiscoveryForGateway(info BasicMcpServerInfo) bool {
|
|
return "mcp-sse" == info.FrontProtocol || "mcp-streamable" == info.FrontProtocol
|
|
}
|
|
|
|
// ListenToMcpServer Listen to mcp server config and backend service
|
|
func (n *NacosRegistryClient) ListenToMcpServer(id string, listener McpServerListener) error {
|
|
versionConfigId := fmt.Sprintf("%s-mcp-versions.json", id)
|
|
serverVersionConfig, err := n.configClient.GetConfig(vo.ConfigParam{
|
|
Group: McpServerVersionGroup,
|
|
DataId: versionConfigId,
|
|
})
|
|
if err != nil {
|
|
mcpServerLog.Errorf("Get mcp server(id: %s) version config error, %v", id, err)
|
|
} else {
|
|
mcpServerLog.Infof("Get mcp server(id: %s) version config success, config is:\n %v", id, serverVersionConfig)
|
|
}
|
|
|
|
versionConfigCallBack := func(namespace string, group string, dataId string, content string) {
|
|
mcpServerLog.Infof("Call back to mcp server %s", id)
|
|
info := VersionsMcpServerInfo{}
|
|
err = json.Unmarshal([]byte(content), &info)
|
|
if err != nil {
|
|
mcpServerLog.Errorf("Parse mcp server (id: %s) version config callback error, %v", id, err)
|
|
return
|
|
}
|
|
latestVersion := info.LatestPublishedVersion
|
|
|
|
ctx := n.servers[id]
|
|
if ctx.versionedMcpServerInfo == nil {
|
|
ctx.versionedMcpServerInfo = &VersionedMcpServerInfo{}
|
|
}
|
|
ctx.versionedMcpServerInfo.serverInfo = &info.BasicMcpServerInfo
|
|
|
|
// first time the version is empty so it will trigger the change finally.
|
|
if ctx.versionedMcpServerInfo.version != latestVersion {
|
|
ctx.versionedMcpServerInfo.version = latestVersion
|
|
n.onServerVersionChanged(ctx)
|
|
n.triggerMcpServerChange(id)
|
|
}
|
|
}
|
|
|
|
n.servers[id] = &ServerContext{
|
|
id: id,
|
|
serverChangeListener: listener,
|
|
configsMap: map[string]*ConfigListenerWrap{
|
|
McpServerVersionGroup: {
|
|
dataId: versionConfigId,
|
|
group: McpServerVersionGroup,
|
|
listener: versionConfigCallBack,
|
|
},
|
|
},
|
|
}
|
|
|
|
// trigger callback manually
|
|
versionConfigCallBack(n.namespaceId, McpServerVersionGroup, versionConfigId, serverVersionConfig)
|
|
// Listen after get config to avoid multi-callback on same version
|
|
err = n.configClient.ListenConfig(vo.ConfigParam{
|
|
Group: McpServerVersionGroup,
|
|
DataId: versionConfigId,
|
|
OnChange: versionConfigCallBack,
|
|
})
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (n *NacosRegistryClient) onServerVersionChanged(ctx *ServerContext) {
|
|
id := ctx.versionedMcpServerInfo.serverInfo.Id
|
|
version := ctx.versionedMcpServerInfo.version
|
|
configsMap := map[string]string{
|
|
McpServerSpecGroup: fmt.Sprintf("%s-%s-mcp-server.json", id, version),
|
|
McpToolSpecGroup: fmt.Sprintf("%s-%s-mcp-tools.json", id, version),
|
|
}
|
|
|
|
for group, dataId := range configsMap {
|
|
configsKey := fmt.Sprintf(SystemConfigIdPrefix+"%s@@%s", id, group)
|
|
// Only listen to the last version of the server, so we should exist and cancel it first
|
|
if data, exist := ctx.configsMap[configsKey]; exist {
|
|
err := n.cancelListenToConfig(data)
|
|
if err != nil {
|
|
mcpServerLog.Errorf("cancel listen to old config %v error %v", dataId, err)
|
|
}
|
|
}
|
|
|
|
configListenerWrap, err := n.ListenToConfig(ctx, dataId, group)
|
|
if err != nil {
|
|
mcpServerLog.Errorf("listen to config %v error %v", dataId, err)
|
|
continue
|
|
}
|
|
ctx.configsMap[configsKey] = configListenerWrap
|
|
}
|
|
}
|
|
|
|
func (n *NacosRegistryClient) triggerMcpServerChange(id string) {
|
|
if context, exist := n.servers[id]; exist {
|
|
if config := mapConfigMapToServerConfig(context); config != nil {
|
|
context.serverChangeListener(config)
|
|
}
|
|
}
|
|
}
|
|
|
|
func mapConfigMapToServerConfig(ctx *ServerContext) *McpServerConfig {
|
|
result := &McpServerConfig{
|
|
Credentials: map[string]interface{}{},
|
|
}
|
|
configMaps := ctx.configsMap
|
|
for key, data := range configMaps {
|
|
if strings.HasPrefix(key, SystemConfigIdPrefix) {
|
|
group := strings.Split(key, "@@")[1]
|
|
if group == McpServerSpecGroup {
|
|
result.ServerSpecConfig = data.data
|
|
} else if group == McpToolSpecGroup {
|
|
result.ToolsSpecConfig = data.data
|
|
}
|
|
} else if strings.HasPrefix(key, CredentialPrefix) {
|
|
credentialId := strings.ReplaceAll(key, CredentialPrefix, "")
|
|
var credData interface{}
|
|
if err := json.Unmarshal([]byte(data.data), &credData); err != nil {
|
|
mcpServerLog.Errorf("parse credential %v error %v", credentialId, err)
|
|
// keep origin data if data is not an object
|
|
result.Credentials[credentialId] = data.data
|
|
} else {
|
|
result.Credentials[credentialId] = credData
|
|
}
|
|
}
|
|
}
|
|
|
|
result.ServiceInfo = ctx.serviceInfo
|
|
return result
|
|
}
|
|
|
|
func (n *NacosRegistryClient) replaceTemplateAndExactConfigsItems(ctx *ServerContext, config *ConfigListenerWrap) map[string]*ConfigListenerWrap {
|
|
result := map[string]*ConfigListenerWrap{}
|
|
compile := regexp.MustCompile("\\$\\{nacos\\.([a-zA-Z0-9-_:\\\\.]+/[a-zA-Z0-9-_:\\\\.]+)}")
|
|
allConfigs := compile.FindAllString(config.data, -1)
|
|
allConfigsMap := map[string]string{}
|
|
for _, config := range allConfigs {
|
|
allConfigsMap[config] = config
|
|
}
|
|
|
|
newContent := config.data
|
|
for _, data := range allConfigsMap {
|
|
dataIdAndGroup := strings.ReplaceAll(data, "${nacos.", "")
|
|
dataIdAndGroup = dataIdAndGroup[0 : len(dataIdAndGroup)-1]
|
|
dataIdAndGroupArray := strings.Split(dataIdAndGroup, "/")
|
|
dataId := strings.TrimSpace(dataIdAndGroupArray[0])
|
|
group := strings.TrimSpace(dataIdAndGroupArray[1])
|
|
configWrap, err := n.ListenToConfig(ctx, dataId, group)
|
|
if err != nil {
|
|
mcpServerLog.Errorf("extract configs %v from content error %v", dataId, err)
|
|
continue
|
|
}
|
|
result[CredentialPrefix+configWrap.group+"_"+configWrap.dataId] = configWrap
|
|
newContent = strings.Replace(newContent, data, ".config.credentials."+group+"_"+dataId, -1)
|
|
}
|
|
|
|
config.data = newContent
|
|
return result
|
|
}
|
|
|
|
func (n *NacosRegistryClient) resetNacosTemplateConfigs(ctx *ServerContext, config *ConfigListenerWrap) {
|
|
newCredentials := n.replaceTemplateAndExactConfigsItems(ctx, config)
|
|
|
|
credentialsNeedDelete := []string{}
|
|
|
|
// cancel all old config listener
|
|
for key, wrap := range ctx.configsMap {
|
|
if strings.HasPrefix(key, CredentialPrefix) {
|
|
if _, ok := newCredentials[key]; !ok {
|
|
credentialsNeedDelete = append(credentialsNeedDelete, key)
|
|
err := n.cancelListenToConfig(wrap)
|
|
if err != nil {
|
|
mcpServerLog.Errorf("cancel listen to old credential listener error %v", err)
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, credentialKey := range credentialsNeedDelete {
|
|
delete(ctx.configsMap, credentialKey)
|
|
}
|
|
|
|
for _, data := range newCredentials {
|
|
ctx.configsMap[CredentialPrefix+data.group+"_"+data.dataId] = data
|
|
}
|
|
}
|
|
|
|
func (n *NacosRegistryClient) refreshServiceListenerIfNeeded(ctx *ServerContext, serverConfig string) {
|
|
var serverInfo ServerSpecInfo
|
|
err := json.Unmarshal([]byte(serverConfig), &serverInfo)
|
|
if err != nil {
|
|
mcpServerLog.Errorf("parse server config error %v", err)
|
|
return
|
|
}
|
|
|
|
if serverInfo.RemoteServerConfig != nil && serverInfo.RemoteServerConfig.ServiceRef != nil {
|
|
ref := serverInfo.RemoteServerConfig.ServiceRef
|
|
if ctx.serviceInfo != nil {
|
|
if ctx.serviceInfo.Name == ref.ServiceName && ctx.serviceInfo.GroupName == ref.GroupName {
|
|
return
|
|
}
|
|
|
|
err := n.namingClient.Unsubscribe(&vo.SubscribeParam{
|
|
GroupName: ctx.serviceInfo.GroupName,
|
|
ServiceName: ctx.serviceInfo.Name,
|
|
SubscribeCallback: ctx.namingCallback,
|
|
})
|
|
if err != nil {
|
|
mcpServerLog.Errorf("unsubscribe service error:%v, groupName:%s, serviceName:%s", err, ctx.serviceInfo.GroupName, ctx.serviceInfo.Name)
|
|
}
|
|
}
|
|
|
|
service, err := n.namingClient.GetService(vo.GetServiceParam{
|
|
GroupName: ref.GroupName,
|
|
ServiceName: ref.ServiceName,
|
|
})
|
|
|
|
if err != nil {
|
|
mcpServerLog.Errorf("get service error:%v, groupName:%s, serviceName:%s", err, ref.GroupName, ref.ServiceName)
|
|
return
|
|
}
|
|
|
|
ctx.serviceInfo = &service
|
|
|
|
if ctx.namingCallback == nil {
|
|
ctx.namingCallback = func(services []model.Instance, err error) {
|
|
if ctx.serviceInfo == nil {
|
|
ctx.serviceInfo = &model.Service{
|
|
GroupName: ctx.serviceInfo.GroupName,
|
|
Name: ctx.serviceInfo.Name,
|
|
}
|
|
}
|
|
|
|
ctx.serviceInfo.Name = ref.ServiceName
|
|
ctx.serviceInfo.GroupName = ref.GroupName
|
|
ctx.serviceInfo.Hosts = services
|
|
n.triggerMcpServerChange(ctx.id)
|
|
}
|
|
}
|
|
|
|
err = n.namingClient.Subscribe(&vo.SubscribeParam{
|
|
GroupName: ctx.serviceInfo.GroupName,
|
|
ServiceName: ctx.serviceInfo.Name,
|
|
SubscribeCallback: ctx.namingCallback,
|
|
})
|
|
if err != nil {
|
|
mcpServerLog.Errorf("subscribe service error:%v, groupName:%s, serviceName:%s", err, ctx.serviceInfo.GroupName, ctx.serviceInfo.Name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (n *NacosRegistryClient) ListenToConfig(ctx *ServerContext, dataId string, group string) (*ConfigListenerWrap, error) {
|
|
wrap := ConfigListenerWrap{
|
|
dataId: dataId,
|
|
group: group,
|
|
}
|
|
|
|
configListener := func(namespace, group, dataId, data string) {
|
|
if ctx.serverChangeListener != nil && wrap.data != data {
|
|
wrap.data = data
|
|
|
|
if group == McpToolSpecGroup {
|
|
n.resetNacosTemplateConfigs(ctx, &wrap)
|
|
} else if group == McpServerSpecGroup {
|
|
n.refreshServiceListenerIfNeeded(ctx, data)
|
|
}
|
|
|
|
n.triggerMcpServerChange(ctx.versionedMcpServerInfo.serverInfo.Id)
|
|
}
|
|
}
|
|
|
|
config, err := n.configClient.GetConfig(vo.ConfigParam{
|
|
DataId: dataId,
|
|
Group: group,
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
wrap.listener = configListener
|
|
wrap.data = config
|
|
if group == McpToolSpecGroup {
|
|
n.resetNacosTemplateConfigs(ctx, &wrap)
|
|
} else if group == McpServerSpecGroup {
|
|
n.refreshServiceListenerIfNeeded(ctx, wrap.data)
|
|
}
|
|
|
|
err = n.configClient.ListenConfig(vo.ConfigParam{
|
|
DataId: dataId,
|
|
Group: group,
|
|
OnChange: configListener,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &wrap, nil
|
|
}
|
|
|
|
func (n *NacosRegistryClient) cancelListenToConfig(wrap *ConfigListenerWrap) error {
|
|
return n.configClient.CancelListenConfig(vo.ConfigParam{
|
|
DataId: wrap.dataId,
|
|
Group: wrap.group,
|
|
OnChange: wrap.listener,
|
|
})
|
|
}
|
|
|
|
func (n *NacosRegistryClient) CancelListenToServer(id string) error {
|
|
if server, exist := n.servers[id]; exist && server != nil {
|
|
defer delete(n.servers, id)
|
|
|
|
for _, wrap := range server.configsMap {
|
|
if wrap != nil {
|
|
err := n.configClient.CancelListenConfig(vo.ConfigParam{
|
|
DataId: wrap.dataId,
|
|
Group: wrap.group,
|
|
OnChange: wrap.listener,
|
|
})
|
|
|
|
if err != nil {
|
|
mcpServerLog.Errorf("cancel listen config error:%v, dataId:%s, group:%s", err, wrap.dataId, wrap.group)
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
|
|
if server.serviceInfo != nil {
|
|
err := n.namingClient.Unsubscribe(&vo.SubscribeParam{
|
|
GroupName: server.serviceInfo.GroupName,
|
|
ServiceName: server.serviceInfo.Name,
|
|
SubscribeCallback: server.namingCallback,
|
|
})
|
|
if err != nil {
|
|
mcpServerLog.Errorf("unsubscribe service error:%v, groupName:%s, serviceName:%s", err, server.serviceInfo.GroupName, server.serviceInfo.Name)
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (n *NacosRegistryClient) CloseClient() {
|
|
n.namingClient.CloseClient()
|
|
n.configClient.CloseClient()
|
|
}
|