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