// MCP 工具到 Engine Tool 接口的桥接. // // MCPToolBridge 将 MCP 服务器提供的工具包装为 Engine 的 Tool 接口, // 使 MCP 工具可以像内置工具一样被 Engine 调度和执行. // // 工具名格式(升华改进(ELEVATED)): // 早期实现 无冲突时用短名 "search",有冲突才用 "server/search",行为不一致. // 我们始终用 mcp__serverName__toolName 格式(由 Manager.AllTools 统一生成), // Bridge 层不再做碰撞检测,职责更纯粹:类型转换 + 调用路由. // // 转换流程: // 1. MCPTool.InputSchema → Tool.InputSchema()(支持 oneOf / anyOf / default) // 2. Tool.Execute() → Manager.CallTool() → ToolCallResult // 3. ToolCallResult.Content(text/image/resource)→ tools.Result.Output package mcp import ( "context" "encoding/json" "fmt" "strings" "git.flytoex.net/yuanwei/flyto-agent/pkg/tools" ) // MCPToolBridge 将一个 MCP 工具桥接为 Engine 的 Tool 接口. type MCPToolBridge struct { // tool 是 MCP 工具定义(原始名称,不含 mcp__ 前缀) tool MCPTool // serverName 是提供该工具的 MCP 服务器名称 serverName string // manager 用于实际调用工具 manager *Manager // fullName 是对外暴露的完整工具名(mcp__serverName__toolName) fullName string } // NewMCPToolBridge 创建一个使用 mcp__serverName__toolName 格式的桥接实例. // // 相较于早期方案的两个构造函数(无前缀 / 有前缀),我们统一为一个, // 因为现在所有 MCP 工具都必须使用前缀格式. func NewMCPToolBridge(tool MCPTool, serverName string, manager *Manager) *MCPToolBridge { return &MCPToolBridge{ tool: tool, serverName: serverName, manager: manager, fullName: mcpToolPrefix + serverName + "__" + tool.Name, } } // Name 返回工具完整名称(mcp__serverName__toolName). func (b *MCPToolBridge) Name() string { return b.fullName } // Description 返回工具描述,追加 MCP 服务器来源信息. func (b *MCPToolBridge) Description(_ context.Context) string { desc := b.tool.Description if desc == "" { desc = "MCP tool from server " + b.serverName } return desc + " (via MCP: " + b.serverName + ")" } // InputSchema 返回工具的 JSON Schema 输入定义. // // 升华改进(ELEVATED): 早期方案仅透传 MCP 工具的 inputSchema,不做任何转换. // 我们补充了 oneOf/anyOf/default 字段的处理-- // 部分 MCP 服务器(如 filesystem,search 服务器)会使用这些关键字, // 而 engine 的 JSON Schema 验证器需要它们才能正确推断参数类型. // 替代方案:<在验证器层忽略 oneOf/anyOf> - 否决,会导致必填参数校验失败. func (b *MCPToolBridge) InputSchema() json.RawMessage { if b.tool.InputSchema != nil { return normalizeInputSchema(b.tool.InputSchema) } return json.RawMessage(`{"type":"object","properties":{}}`) } // normalizeInputSchema 规范化 JSON Schema: // - 保留 oneOf / anyOf / allOf // - 保留 default 字段 // - 确保顶层有 type: object(如果缺失) func normalizeInputSchema(raw json.RawMessage) json.RawMessage { var schema map[string]json.RawMessage if err := json.Unmarshal(raw, &schema); err != nil { // 非 object schema,原样返回 return raw } // 如果顶层没有 type 字段,且也没有 oneOf/anyOf/allOf,补 "object" if _, hasType := schema["type"]; !hasType { if _, hasOneOf := schema["oneOf"]; !hasOneOf { if _, hasAnyOf := schema["anyOf"]; !hasAnyOf { if _, hasAllOf := schema["allOf"]; !hasAllOf { schema["type"] = json.RawMessage(`"object"`) } } } } // 重新序列化(只有在修改了 schema 时才有损耗,通常直接返回原始值) if _, modified := schema["type"]; modified { if out, err := json.Marshal(schema); err == nil { return out } } return raw } // Execute 执行 MCP 工具. func (b *MCPToolBridge) Execute(ctx context.Context, input json.RawMessage, _ tools.ProgressFunc) (*tools.Result, error) { var args map[string]any if len(input) > 0 { if err := json.Unmarshal(input, &args); err != nil { return nil, fmt.Errorf("mcp bridge: parse input: %w", err) } } result, err := b.manager.CallTool(b.serverName, b.tool.Name, args) if err != nil { return &tools.Result{ Output: fmt.Sprintf("MCP tool error: %v", err), IsError: true, }, nil } return &tools.Result{ Output: convertToolResult(result), IsError: result.IsError, }, nil } // Metadata 返回工具元数据. // MCP 工具保守设置:不可并发(服务端状态未知),非只读(可能有副作用). func (b *MCPToolBridge) Metadata() tools.Metadata { return tools.Metadata{ ConcurrencySafe: false, ReadOnly: false, Destructive: false, SearchHint: "mcp " + b.serverName + " " + b.tool.Name, } } // maxToolResultTextBytes 是单条 text 内容允许的最大字节数(1MB). // 升华改进(ELEVATED): 恶意 MCP 服务器可返回超大文本撑爆内存或注入垃圾上下文. // 替代方案:<可配置上限 + 流式截断> - 当前硬编码 1MB 满足大多数工具响应场景. const maxToolResultTextBytes = 1 << 20 // 1MB // convertToolResult 将 MCP ToolCallResult 转换为文本输出. func convertToolResult(result *ToolCallResult) string { if result == nil || len(result.Content) == 0 { return "(no output)" } var parts []string for _, item := range result.Content { switch item.Type { case "text": if item.Text != "" { text := item.Text if len(text) > maxToolResultTextBytes { text = text[:maxToolResultTextBytes] + "\n[内容已截断,超过 1MB 上限]" } parts = append(parts, text) } case "image": parts = append(parts, fmt.Sprintf("[image: %s]", item.MimeType)) case "resource": parts = append(parts, fmt.Sprintf("[resource: %s]", item.URI)) default: if item.Text != "" { text := item.Text if len(text) > maxToolResultTextBytes { text = text[:maxToolResultTextBytes] + "\n[内容已截断,超过 1MB 上限]" } parts = append(parts, text) } } } if len(parts) == 0 { return "(no text output)" } return strings.Join(parts, "\n") } // BridgeAllTools 将管理器中所有 MCP 工具桥接为 Engine Tool 并注册到注册表. // // 工具名冲突:若注册表中已有同名工具,跳过并记录(不强制覆盖). // 返回成功注册的工具数量. func BridgeAllTools(manager *Manager, registry *tools.Registry) int { allTools := manager.AllTools() registered := 0 for _, mcpTool := range allTools { // AllTools() 已经在工具名上加了 mcp__ 前缀, // 但 Bridge 需要原始工具名(不含前缀)来调用 Manager.CallTool. // 通过 parseToolFullName 还原 serverName + 原始 toolName. serverName, origToolName := parseToolFullName(mcpTool.Name) if serverName == "" { // 不应发生:AllTools 保证 mcp__ 前缀格式 continue } // 用原始工具名构造 MCPTool(Name 字段存原始名,Bridge 内部加前缀) origTool := mcpTool origTool.Name = origToolName bridge := NewMCPToolBridge(origTool, serverName, manager) if _, exists := registry.Get(bridge.Name()); exists { // 已有同名工具(极少发生,两个服务器同名且同服务器名),跳过 continue } if err := registry.Register(bridge); err == nil { registered++ } } return registered }