diff --git a/internal/ai/runtime/internal/engine/service.go b/internal/ai/runtime/internal/engine/service.go index 76e2e67..2a2cf9a 100644 --- a/internal/ai/runtime/internal/engine/service.go +++ b/internal/ai/runtime/internal/engine/service.go @@ -109,7 +109,7 @@ func (s *Service) Run(ctx context.Context, req Request) (*Summary, error) { collector.Data.Skill.RouteReason = summary.SkillRouteReason collector.Data.Skill.RouteTrace = summary.SkillRouteTrace - agent, err := s.agentFactory.BuildCustomerServiceAgent(ctx, req.AIAgent, req.AIConfig, req.SelectedSkill, filteredToolDefs, req.ExtraTools, req.ExtraToolCodes, collector) + agent, err := s.agentFactory.BuildCustomerServiceAgent(ctx, req.AIAgent, req.AIConfig, req.SelectedSkill, filteredToolDefs, nil, req.ExtraTools, req.ExtraToolCodes, collector) if err != nil { summary.Status = "error" summary.ErrorMessage = err.Error() @@ -233,7 +233,7 @@ func (s *Service) Resume(ctx context.Context, req ResumeRequest) (*Summary, erro collector.Data.Input.ToolCodes = append(collector.Data.Input.ToolCodes, summary.ToolCodes...) collector.Data.Model.Provider = string(req.AIConfig.Provider) collector.Data.Model.Name = req.AIConfig.ModelName - agent, err := s.agentFactory.BuildCustomerServiceAgent(ctx, req.AIAgent, req.AIConfig, nil, toolDefs, req.ExtraTools, req.ExtraToolCodes, collector) + agent, err := s.agentFactory.BuildCustomerServiceAgent(ctx, req.AIAgent, req.AIConfig, nil, nil, nil, req.ExtraTools, req.ExtraToolCodes, collector) if err != nil { summary.Status = "error" summary.ErrorMessage = err.Error() diff --git a/internal/ai/runtime/internal/impl/factory/agent_factory.go b/internal/ai/runtime/internal/impl/factory/agent_factory.go index 31f7aa0..b1245f2 100644 --- a/internal/ai/runtime/internal/impl/factory/agent_factory.go +++ b/internal/ai/runtime/internal/impl/factory/agent_factory.go @@ -11,6 +11,7 @@ import ( einoagents "cs-agent/internal/ai/runtime/internal/impl/agents" einocallbacks "cs-agent/internal/ai/runtime/internal/impl/callbacks" "cs-agent/internal/models" + "cs-agent/internal/pkg/toolx" "github.com/cloudwego/eino/adk" einobasetool "github.com/cloudwego/eino/components/tool" @@ -32,7 +33,8 @@ func NewAgentFactory() *AgentFactory { } func (f *AgentFactory) BuildCustomerServiceAgent(ctx context.Context, aiAgent *models.AIAgent, aiConfig *models.AIConfig, - selectedSkill *models.SkillDefinition, toolDefinitions []einoadapter.MCPToolDefinition, extraTools []einobasetool.BaseTool, extraToolCodes map[string]string, + selectedSkill *models.SkillDefinition, instructionToolDefinitions []einoadapter.MCPToolDefinition, mcpToolDefinitions []einoadapter.MCPToolDefinition, + extraTools []einobasetool.BaseTool, extraToolCodes map[string]string, collector *einocallbacks.RuntimeTraceCollector) (*einoagents.CustomerServiceAgent, error) { if aiAgent == nil || aiConfig == nil { return nil, nil @@ -41,7 +43,7 @@ func (f *AgentFactory) BuildCustomerServiceAgent(ctx context.Context, aiAgent *m if err != nil { return nil, err } - baseTools, err := f.toolFactory.BuildBaseToolsByDefinitions(ctx, toolDefinitions) + baseTools, err := f.toolFactory.BuildBaseToolsByDefinitions(ctx, mcpToolDefinitions) if err != nil { return nil, err } @@ -50,20 +52,40 @@ func (f *AgentFactory) BuildCustomerServiceAgent(ctx context.Context, aiAgent *m allTools = append(allTools, baseTools...) handlers := make([]adk.ChatModelAgentMiddleware, 0, 1) if collector != nil { - toolMetadataBy := make(map[string]einocallbacks.ToolMetadata, len(toolDefinitions)) - for _, item := range toolDefinitions { + toolMetadataBy := make(map[string]einocallbacks.ToolMetadata, len(mcpToolDefinitions)+len(extraToolCodes)) + for _, item := range mcpToolDefinitions { toolMetadataBy[item.ModelName] = einocallbacks.ToolMetadata{ ToolCode: item.ToolCode, ServerCode: item.ServerCode, ToolName: item.ToolName, } } + for modelName, toolCode := range extraToolCodes { + modelName = strings.TrimSpace(modelName) + toolCode = strings.TrimSpace(toolCode) + if modelName == "" || toolCode == "" { + continue + } + serverCode, toolName := "", "" + if toolCode == toolx.BuiltinToolSearchToolCode { + serverCode = toolx.BuiltinToolCatalogServerCode + toolName = toolx.BuiltinToolSearchToolName + } else if toolCode == toolx.BuiltinCreateTicketConfirmToolCode { + serverCode = toolx.BuiltinToolCatalogServerCode + toolName = toolx.BuiltinCreateTicketConfirmToolName + } + toolMetadataBy[modelName] = einocallbacks.ToolMetadata{ + ToolCode: toolCode, + ServerCode: serverCode, + ToolName: toolName, + } + } handlers = append(handlers, einocallbacks.NewRuntimeTraceHandler(collector, toolMetadataBy)) } inner, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{ Name: strings.TrimSpace(aiAgent.Name), Description: strings.TrimSpace(aiAgent.Description), - Instruction: buildAgentInstruction(aiAgent, selectedSkill, toolDefinitions, extraToolCodes), + Instruction: buildAgentInstruction(aiAgent, selectedSkill, instructionToolDefinitions, extraToolCodes), Model: chatModel, ToolsConfig: adk.ToolsConfig{ ToolsNodeConfig: compose.ToolsNodeConfig{ @@ -87,7 +109,16 @@ func buildAgentInstruction(aiAgent *models.AIAgent, selectedSkill *models.SkillD if skillInstruction := buildSelectedSkillInstruction(selectedSkill, toolDefinitions); skillInstruction != "" { appendixParts = append(appendixParts, skillInstruction) } - if hasToolCode(extraToolCodes, "builtin/create_ticket_with_confirmation") { + if hasToolCode(extraToolCodes, toolx.BuiltinToolSearchToolCode) { + appendixParts = append(appendixParts, strings.TrimSpace(` +当你需要使用长尾 MCP 能力时,优先使用 tool_search 工具,并遵守以下规则: +1. 先用 query 搜索候选工具,再根据返回的 toolCode 选择真正的目标工具。 +2. 只有在你已经明确要调用哪个动态工具时,才传入 toolCode 和 arguments 进行执行。 +3. 不要臆造 toolCode;必须以 tool_search 返回的候选结果为准。 +4. 如果当前已有固定内置工具可以完成任务,优先使用固定工具,不要滥用 tool_search。 +`)) + } + if hasToolCode(extraToolCodes, toolx.BuiltinCreateTicketConfirmToolCode) { appendixParts = append(appendixParts, strings.TrimSpace(` 你可以在确认信息充分后调用 create_ticket_with_confirmation 工具来创建工单,但必须遵守以下规则: 1. 只有在用户明确表达希望提交工单、投诉、报障、售后处理等诉求时,才考虑调用该工具。 diff --git a/internal/ai/runtime/service.go b/internal/ai/runtime/service.go index 2822744..c354cbb 100644 --- a/internal/ai/runtime/service.go +++ b/internal/ai/runtime/service.go @@ -19,6 +19,7 @@ func newService() *service { return &service{ runtime: engine.NewService(), registry: registry.NewRegistry( + tools.NewToolSearchTool(), tools.NewCreateTicketConfirmTool(), ), } @@ -94,7 +95,7 @@ func (s *service) prepareToolsForRun(req *Request) error { AIAgent: req.AIAgent, AIConfig: req.AIConfig, UserMessage: req.UserMessage, - AllowedToolCodes: resolveAllowedToolCodes(req.AIAgent, req.SelectedSkill), + AllowedToolCodes: expandRuntimeAllowedToolCodes(resolveAllowedToolCodes(req.AIAgent, req.SelectedSkill)), }) if err != nil { return err @@ -109,9 +110,10 @@ func (s *service) prepareToolsForResume(req *ResumeRequest) error { return nil } toolSet, err := s.registry.Resolve(registry.Context{ - Conversation: req.Conversation, - AIAgent: req.AIAgent, - AIConfig: req.AIConfig, + Conversation: req.Conversation, + AIAgent: req.AIAgent, + AIConfig: req.AIConfig, + AllowedToolCodes: expandRuntimeAllowedToolCodes(parseAgentAllowedToolCodes(req.AIAgent)), }) if err != nil { return err @@ -275,3 +277,36 @@ func resolveAllowedToolCodes(aiAgent *models.AIAgent, skill *models.SkillDefinit return ret } } + +func expandRuntimeAllowedToolCodes(items []string) []string { + ret := make([]string, 0, len(items)+1) + hasMCPTool := false + for _, item := range items { + item = strings.TrimSpace(item) + if item == "" { + continue + } + ret = append(ret, item) + serverCode, toolName := toolx.SplitMCPToolCode(item) + if serverCode != "" && toolName != "" { + hasMCPTool = true + } + } + if hasMCPTool { + ret = appendIfMissingString(ret, toolx.BuiltinToolSearchToolCode) + } + return ret +} + +func appendIfMissingString(items []string, value string) []string { + value = strings.TrimSpace(value) + if value == "" { + return items + } + for _, item := range items { + if strings.TrimSpace(item) == value { + return items + } + } + return append(items, value) +} diff --git a/internal/ai/runtime/tools/tool_search_tool.go b/internal/ai/runtime/tools/tool_search_tool.go new file mode 100644 index 0000000..4d3ca7a --- /dev/null +++ b/internal/ai/runtime/tools/tool_search_tool.go @@ -0,0 +1,313 @@ +package tools + +import ( + "context" + "encoding/json" + "fmt" + "slices" + "strings" + + "cs-agent/internal/ai/mcps" + "cs-agent/internal/ai/runtime/registry" + "cs-agent/internal/pkg/toolx" + + einotool "github.com/cloudwego/eino/components/tool" + "github.com/cloudwego/eino/schema" + einojsonschema "github.com/eino-contrib/jsonschema" + orderedmap "github.com/wk8/go-ordered-map/v2" +) + +const ( + ToolSearchToolCode = toolx.BuiltinToolSearchToolCode + ToolSearchToolName = toolx.BuiltinToolSearchToolName +) + +type ToolSearchTool struct { + allowedToolCodes []string +} + +func NewToolSearchTool() *ToolSearchTool { + return &ToolSearchTool{} +} + +func (t *ToolSearchTool) Name() string { + return ToolSearchToolName +} + +func (t *ToolSearchTool) Code() string { + return ToolSearchToolCode +} + +func (t *ToolSearchTool) Enabled(ctx registry.Context) bool { + return len(filterAllowedMCPToolCodes(ctx.AllowedToolCodes)) > 0 +} + +func (t *ToolSearchTool) Build(ctx registry.Context) (einotool.BaseTool, error) { + if !t.Enabled(ctx) { + return nil, nil + } + return &ToolSearchTool{ + allowedToolCodes: filterAllowedMCPToolCodes(ctx.AllowedToolCodes), + }, nil +} + +func (t *ToolSearchTool) Info(ctx context.Context) (*schema.ToolInfo, error) { + return &schema.ToolInfo{ + Name: ToolSearchToolName, + Desc: "当你需要使用当前会话允许的长尾 MCP 工具时,先调用本工具搜索合适的 toolCode;确认目标后,可再次调用本工具并传入 toolCode 与 arguments 代理执行。不要用它替代明确固定的内置流程工具。", + ParamsOneOf: schema.NewParamsOneOfByJSONSchema(&einojsonschema.Schema{ + Version: einojsonschema.Version, + Type: "object", + Properties: orderedmap.New[string, *einojsonschema.Schema](orderedmap.WithInitialData( + orderedmap.Pair[string, *einojsonschema.Schema]{ + Key: "query", + Value: &einojsonschema.Schema{ + Type: "string", + Description: "要搜索的工具意图、能力或关键词;当只想列出候选工具时使用。", + }, + }, + orderedmap.Pair[string, *einojsonschema.Schema]{ + Key: "toolCode", + Value: &einojsonschema.Schema{ + Type: "string", + Description: "已确定目标后要调用的 MCP toolCode,例如 mcp_server/tool_name。", + }, + }, + orderedmap.Pair[string, *einojsonschema.Schema]{ + Key: "arguments", + Value: &einojsonschema.Schema{ + Type: "object", + Description: "调用目标工具时传入的参数对象。", + AdditionalProperties: &einojsonschema.Schema{}, + }, + }, + )), + }), + Extra: map[string]any{ + "toolCode": ToolSearchToolCode, + }, + }, nil +} + +func (t *ToolSearchTool) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...einotool.Option) (string, error) { + if t == nil { + return "", fmt.Errorf("tool search tool is nil") + } + req, err := parseToolSearchRequest(argumentsInJSON) + if err != nil { + return "", err + } + if req.ToolCode != "" { + return t.invokeTargetTool(ctx, req.ToolCode, req.Arguments) + } + return t.searchCandidates(ctx, req.Query) +} + +type toolSearchRequest struct { + Query string + ToolCode string + Arguments map[string]any +} + +type toolSearchCandidate struct { + ToolCode string `json:"toolCode"` + ServerCode string `json:"serverCode"` + ToolName string `json:"toolName"` + Title string `json:"title,omitempty"` + Description string `json:"description,omitempty"` +} + +func parseToolSearchRequest(argumentsInJSON string) (*toolSearchRequest, error) { + argumentsInJSON = strings.TrimSpace(argumentsInJSON) + if argumentsInJSON == "" { + return &toolSearchRequest{}, nil + } + raw := make(map[string]any) + if err := json.Unmarshal([]byte(argumentsInJSON), &raw); err != nil { + return nil, fmt.Errorf("invalid tool_search arguments: %w", err) + } + req := &toolSearchRequest{ + Query: strings.TrimSpace(getStringValue(raw, "query")), + ToolCode: strings.TrimSpace(getStringValue(raw, "toolCode")), + } + if value, ok := raw["arguments"]; ok { + args, ok := value.(map[string]any) + if !ok { + return nil, fmt.Errorf("tool_search arguments must be an object") + } + req.Arguments = args + } + return req, nil +} + +func (t *ToolSearchTool) searchCandidates(ctx context.Context, query string) (string, error) { + candidates, err := t.loadAllowedCandidates(ctx) + if err != nil { + return "", err + } + matched := filterCandidatesByQuery(candidates, query) + if len(matched) == 0 { + return "未找到匹配的动态工具,请换个关键词,或继续向用户追问后再搜索。", nil + } + if len(matched) > 8 { + matched = matched[:8] + } + buf, err := json.Marshal(map[string]any{ + "query": strings.TrimSpace(query), + "total": len(matched), + "candidates": matched, + }) + if err != nil { + return "", err + } + return string(buf), nil +} + +func (t *ToolSearchTool) invokeTargetTool(ctx context.Context, toolCode string, arguments map[string]any) (string, error) { + toolCode = strings.TrimSpace(toolCode) + serverCode, toolName := toolx.SplitMCPToolCode(toolCode) + if serverCode == "" || toolName == "" { + return "", fmt.Errorf("tool_search 只支持调用 MCP toolCode") + } + if !containsToolCode(t.allowedToolCodes, toolCode) { + return "", fmt.Errorf("目标工具未被当前会话授权") + } + result, err := mcps.Runtime.CallTool(ctx, serverCode, toolName, cloneArguments(arguments)) + if err != nil { + return "", err + } + return buildToolCallResultSummary(result), nil +} + +func (t *ToolSearchTool) loadAllowedCandidates(ctx context.Context) ([]toolSearchCandidate, error) { + serverToToolCodes := make(map[string]map[string]struct{}) + for _, toolCode := range t.allowedToolCodes { + serverCode, toolName := toolx.SplitMCPToolCode(toolCode) + if serverCode == "" || toolName == "" { + continue + } + if _, ok := serverToToolCodes[serverCode]; !ok { + serverToToolCodes[serverCode] = make(map[string]struct{}) + } + serverToToolCodes[serverCode][toolCode] = struct{}{} + } + serverCodes := make([]string, 0, len(serverToToolCodes)) + for serverCode := range serverToToolCodes { + serverCodes = append(serverCodes, serverCode) + } + slices.Sort(serverCodes) + ret := make([]toolSearchCandidate, 0) + for _, serverCode := range serverCodes { + tools, err := mcps.Runtime.ListTools(ctx, serverCode) + if err != nil { + return nil, err + } + allowed := serverToToolCodes[serverCode] + for _, item := range tools { + toolCode := toolx.BuildMCPToolCode(serverCode, item.Name) + if _, ok := allowed[toolCode]; !ok { + continue + } + ret = append(ret, toolSearchCandidate{ + ToolCode: toolCode, + ServerCode: serverCode, + ToolName: strings.TrimSpace(item.Name), + Title: strings.TrimSpace(item.Title), + Description: strings.TrimSpace(item.Description), + }) + } + } + return ret, nil +} + +func filterAllowedMCPToolCodes(input []string) []string { + if len(input) == 0 { + return nil + } + ret := make([]string, 0, len(input)) + for _, item := range input { + item = strings.TrimSpace(item) + serverCode, toolName := toolx.SplitMCPToolCode(item) + if serverCode == "" || toolName == "" { + continue + } + ret = append(ret, item) + } + return ret +} + +func containsToolCode(items []string, target string) bool { + target = strings.TrimSpace(target) + if target == "" { + return false + } + for _, item := range items { + if strings.TrimSpace(item) == target { + return true + } + } + return false +} + +func filterCandidatesByQuery(candidates []toolSearchCandidate, query string) []toolSearchCandidate { + query = strings.TrimSpace(strings.ToLower(query)) + if query == "" { + return candidates + } + ret := make([]toolSearchCandidate, 0, len(candidates)) + for _, item := range candidates { + searchText := strings.ToLower(strings.Join([]string{ + item.ToolCode, + item.ServerCode, + item.ToolName, + item.Title, + item.Description, + }, "\n")) + if strings.Contains(searchText, query) { + ret = append(ret, item) + } + } + return ret +} + +func cloneArguments(input map[string]any) map[string]any { + if len(input) == 0 { + return map[string]any{} + } + ret := make(map[string]any, len(input)) + for key, value := range input { + ret[key] = value + } + return ret +} + +func buildToolCallResultSummary(result *mcps.ToolCallResult) string { + if result == nil { + return "" + } + lines := make([]string, 0, len(result.Content)+2) + if result.IsError { + lines = append(lines, "tool returned an error") + } + if result.StructuredContent != nil { + if data, err := json.Marshal(result.StructuredContent); err == nil { + lines = append(lines, string(data)) + } + } + for _, item := range result.Content { + switch item.Type { + case "text": + if text := strings.TrimSpace(item.Text); text != "" { + lines = append(lines, text) + } + default: + if item.Data == nil { + continue + } + if data, err := json.Marshal(item.Data); err == nil { + lines = append(lines, string(data)) + } + } + } + return strings.TrimSpace(strings.Join(lines, "\n")) +} diff --git a/internal/controllers/console/ai_agent_controller.go b/internal/controllers/console/ai_agent_controller.go index cf5c939..99977c5 100644 --- a/internal/controllers/console/ai_agent_controller.go +++ b/internal/controllers/console/ai_agent_controller.go @@ -176,7 +176,10 @@ func buildAIAgentResponse(item *models.AIAgent) response.AIAgentResponse { } serverCode := strings.TrimSpace(tool.ServerCode) toolName := strings.TrimSpace(tool.ToolName) - if toolCode == toolx.BuiltinCreateTicketConfirmToolCode { + if toolCode == toolx.BuiltinToolSearchToolCode { + serverCode = toolx.BuiltinToolCatalogServerCode + toolName = toolx.BuiltinToolSearchToolName + } else if toolCode == toolx.BuiltinCreateTicketConfirmToolCode { serverCode = toolx.BuiltinToolCatalogServerCode toolName = toolx.BuiltinCreateTicketConfirmToolName } else if parsedServerCode, parsedToolName := toolx.SplitMCPToolCode(toolCode); parsedServerCode != "" && parsedToolName != "" { @@ -184,12 +187,22 @@ func buildAIAgentResponse(item *models.AIAgent) response.AIAgentResponse { toolName = parsedToolName } title := strings.TrimSpace(tool.Title) - if title == "" && toolCode == toolx.BuiltinCreateTicketConfirmToolCode { - title = toolx.BuiltinCreateTicketConfirmToolTitle + if title == "" { + switch toolCode { + case toolx.BuiltinToolSearchToolCode: + title = toolx.BuiltinToolSearchToolTitle + case toolx.BuiltinCreateTicketConfirmToolCode: + title = toolx.BuiltinCreateTicketConfirmToolTitle + } } description := strings.TrimSpace(tool.Description) - if description == "" && toolCode == toolx.BuiltinCreateTicketConfirmToolCode { - description = toolx.BuiltinCreateTicketConfirmToolDescription + if description == "" { + switch toolCode { + case toolx.BuiltinToolSearchToolCode: + description = toolx.BuiltinToolSearchToolDescription + case toolx.BuiltinCreateTicketConfirmToolCode: + description = toolx.BuiltinCreateTicketConfirmToolDescription + } } ret.DirectTools = append(ret.DirectTools, response.AIAgentMCPToolResponse{ ToolCode: toolCode, diff --git a/internal/pkg/toolx/builtin_tools.go b/internal/pkg/toolx/builtin_tools.go index 5184c45..1b90dea 100644 --- a/internal/pkg/toolx/builtin_tools.go +++ b/internal/pkg/toolx/builtin_tools.go @@ -2,6 +2,10 @@ package toolx const ( BuiltinToolCatalogServerCode = "builtin" + BuiltinToolSearchToolCode = "builtin/tool_search" + BuiltinToolSearchToolName = "tool_search" + BuiltinToolSearchToolTitle = "搜索并调用动态工具" + BuiltinToolSearchToolDescription = "用于搜索当前允许使用的 MCP 工具,并在确认目标 toolCode 后动态调用该工具。适合处理长尾工具,不应替代固定内置流程工具。" BuiltinCreateTicketConfirmToolCode = "builtin/create_ticket_with_confirmation" BuiltinCreateTicketConfirmToolName = "create_ticket_with_confirmation" BuiltinCreateTicketConfirmToolTitle = "创建工单并发起确认" diff --git a/internal/services/tool_catalog_service.go b/internal/services/tool_catalog_service.go index d33c00d..38816ef 100644 --- a/internal/services/tool_catalog_service.go +++ b/internal/services/tool_catalog_service.go @@ -32,21 +32,7 @@ type MCPToolCatalogItem struct { func (s *toolCatalogService) ListMCPTools(ctx context.Context) ([]MCPToolCatalogItem, error) { cfg := config.Current() - if !cfg.MCP.Enabled { - return nil, errorsx.InvalidParam("MCP未启用") - } - if len(cfg.MCP.Servers) == 0 { - return nil, nil - } - serverCodes := make([]string, 0, len(cfg.MCP.Servers)) - for serverCode, server := range cfg.MCP.Servers { - if !server.Enabled { - continue - } - serverCodes = append(serverCodes, serverCode) - } - slices.Sort(serverCodes) - ret := make([]MCPToolCatalogItem, 0) + ret := make([]MCPToolCatalogItem, 0, 2) ret = append(ret, MCPToolCatalogItem{ ToolCode: toolx.BuiltinCreateTicketConfirmToolCode, ServerCode: toolx.BuiltinToolCatalogServerCode, @@ -55,6 +41,25 @@ func (s *toolCatalogService) ListMCPTools(ctx context.Context) ([]MCPToolCatalog Title: toolx.BuiltinCreateTicketConfirmToolTitle, Description: toolx.BuiltinCreateTicketConfirmToolDescription, }) + if !cfg.MCP.Enabled { + return ret, nil + } + ret = append(ret, MCPToolCatalogItem{ + ToolCode: toolx.BuiltinToolSearchToolCode, + ServerCode: toolx.BuiltinToolCatalogServerCode, + ToolName: toolx.BuiltinToolSearchToolName, + SourceType: toolx.BuiltinToolCatalogServerCode, + Title: toolx.BuiltinToolSearchToolTitle, + Description: toolx.BuiltinToolSearchToolDescription, + }) + serverCodes := make([]string, 0, len(cfg.MCP.Servers)) + for serverCode, server := range cfg.MCP.Servers { + if !server.Enabled { + continue + } + serverCodes = append(serverCodes, serverCode) + } + slices.Sort(serverCodes) for _, serverCode := range serverCodes { tools, err := mcps.Runtime.ListTools(ctx, serverCode) if err != nil { @@ -86,7 +91,8 @@ func (s *toolCatalogService) ValidateToolCode(toolCode string) error { if toolCode == "" { return errorsx.InvalidParam("toolCode不能为空") } - if toolCode == toolx.BuiltinCreateTicketConfirmToolCode { + switch toolCode { + case toolx.BuiltinToolSearchToolCode, toolx.BuiltinCreateTicketConfirmToolCode: return nil } serverCode, toolName := toolx.SplitMCPToolCode(toolCode)