package mcp import ( "context" "encoding/json" "fmt" "strings" mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp" ) // ToolInfo 是 MCP 服务器暴露的一把工具。 type ToolInfo struct { // Name 是调用该工具用的标识。 Name string // Description 说明工具做什么。 Description string // InputSchema 是工具的 JSON Schema,通常是 // {"type":"object","properties":{...}}。 InputSchema map[string]any } // ContentBlock 是工具调用结果里的一个内容块(MCP content block)。 type ContentBlock struct { // Type 是块类型,最常见的是 "text";非文本块只保留类型标记。 Type string `json:"type"` // Text 是文本载荷。 Text string `json:"text"` } // ToolResult 是一次 tools/call 的结构化结果。 type ToolResult struct { // Content 是服务器返回的内容块。 Content []ContentBlock // Structured 是服务器返回的结构化结果(structuredContent),可能为 nil。 Structured any // IsError 表示服务器把这次调用标成了错误。 IsError bool } // toolInfoFrom 把 SDK 的工具描述转成本包的 ToolInfo。 func toolInfoFrom(t *mcpsdk.Tool) ToolInfo { return ToolInfo{ Name: t.Name, Description: t.Description, InputSchema: schemaMap(t.InputSchema), } } // mcpTool 把一把 MCP 工具适配成 pkg/plugins/sdk 的 Tool 接口, // 使其能被注册进 registry、被 agent 当作普通工具调用。字段不可变,可并发使用。 type mcpTool struct { client *Client info ToolInfo } // Name 返回 MCP 工具标识。 func (t *mcpTool) Name() string { return t.info.Name } // Description 返回 MCP 工具描述。 func (t *mcpTool) Description() string { return t.info.Description } // Parameters 返回 MCP input schema;服务器未给 schema 时返回无约束空对象 schema。 func (t *mcpTool) Parameters() (map[string]any, error) { if t.info.InputSchema == nil { return emptyObjectSchema(), nil } return t.info.InputSchema, nil } // Execute 调用 MCP 工具并返回其结果文本。 // 参数先经 input schema 缓存校验 + 补默认值(design/09 §1.1), // 文本块全为空时退回结构化结果的 JSON 文本,保证模型总能看见东西。 func (t *mcpTool) Execute(ctx context.Context, args any) (any, error) { params, err := normalizeArgs(args) if err != nil { return nil, err } res, err := t.client.callTool(ctx, t.info.Name, params, t.info.InputSchema) if err != nil { return nil, err } if text := contentText(res.Content); text != "" { return text, nil } return structuredText(res.Structured) } // contentText 把工具结果里的文本块拼成一个字符串,忽略非文本块。 func contentText(content []ContentBlock) string { var b strings.Builder for _, blk := range content { if blk.Type == "" || blk.Type == "text" { b.WriteString(blk.Text) } } return b.String() } // structuredText 把结构化结果序列化成 JSON 文本;无结构化结果时返回空串。 func structuredText(structured any) (string, error) { if structured == nil { return "", nil } data, err := json.Marshal(structured) if err != nil { return "", fmt.Errorf("marshal structured tool result: %w", err) } return string(data), nil } // toContentBlocks 把 SDK 内容块转成本包结构。非文本块只保留类型标记, // 不把 base64 大块塞进模型上下文。 func toContentBlocks(content []mcpsdk.Content) []ContentBlock { out := make([]ContentBlock, 0, len(content)) for _, blk := range content { if text, ok := blk.(*mcpsdk.TextContent); ok { out = append(out, ContentBlock{Type: "text", Text: text.Text}) continue } out = append(out, ContentBlock{Type: contentTypeOf(blk)}) } return out } // contentTypeOf 返回内容块的线上类型名(如 image / audio / resource); // 未知类型统一记作 non-text。 func contentTypeOf(blk mcpsdk.Content) string { switch blk.(type) { case *mcpsdk.ImageContent: return "image" case *mcpsdk.AudioContent: return "audio" case *mcpsdk.ResourceLink, *mcpsdk.EmbeddedResource: return "resource" case *mcpsdk.ToolUseContent: return "tool_use" case *mcpsdk.ToolResultContent: return "tool_result" default: return "non-text" } }