feat(mcp): 协议层换成官方 Model Context Protocol SDK,删自研实现(裁决 10.4.6 + AGENTS §9.5)
- 新增依赖:github.com/modelcontextprotocol/go-sdk v1.8.0(direct)+ github.com/google/jsonschema-go v0.4.3 (SDK 自带依赖,直接用于 schema 校验/默认值,避免再造一份);go.sum 纯新增 - 删除自研协议层:rpc.go / stdio.go / http.go / sse.go 及其测试 - 用 SDK 的成熟逻辑:命令传输(stdio 子进程)/ SSE / Streamable HTTP、协议版本协商与 -32022 回退、keepalive(含失败阈值)、tools/list_changed 订阅、能力协商(InitializeResult.Capabilities) - 保留自研的四件事(其余零自研):配置→传输、发现→registry 注册、能力族推断、schema 缓存(Prepare 命中不重复解析) - 能力闸诚实化:服务器声明 capabilities 但不含 tools 时 ListTools/DiscoverTools 返回 ErrCapabilityNotDeclared(不静默空集);完全不声明 capabilities 的老式服务器仍按 legacy 处理 - 复核:build 绿 + capability 全包测试绿(mcp 42 用例,含真子进程 stdio / httptest Streamable HTTP / 内存传输三类假 server,不依赖外网)+ `-race` 干净;变异 2 条(featuresFrom 恒 true / 删 applySchema 调用)已还原 - 无 REST/WS/env/数据目录变更;包内 API 有增删(见回报),仓外调用方签名未变(未改 cmd/cyrene 与 plugin/manager)
This commit is contained in:
1 parent
7b05b4bfa9
commit
a1083a322f
26 files changed
+1903
-3011
No files matched your search
+104
-24
@@ -2,47 +2,91 @@ package mcp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
)
|
||||
|
||||
// mcpTool adapts an MCP tool to the pkg/plugins/sdk Tool interface so it can be
|
||||
// registered and invoked like any built-in tool. It is immutable and safe for
|
||||
// concurrent use.
|
||||
// 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 returns the MCP tool identifier.
|
||||
func (t *mcpTool) Name() string {
|
||||
return t.info.Name
|
||||
}
|
||||
// Name 返回 MCP 工具标识。
|
||||
func (t *mcpTool) Name() string { return t.info.Name }
|
||||
|
||||
// Description returns the MCP tool description.
|
||||
func (t *mcpTool) Description() string {
|
||||
return t.info.Description
|
||||
}
|
||||
// Description 返回 MCP 工具描述。
|
||||
func (t *mcpTool) Description() string { return t.info.Description }
|
||||
|
||||
// Parameters returns the MCP tool input schema, defaulting to an empty object
|
||||
// schema when the server omits one.
|
||||
// Parameters 返回 MCP input schema;服务器未给 schema 时返回无约束空对象 schema。
|
||||
func (t *mcpTool) Parameters() (map[string]any, error) {
|
||||
if t.info.InputSchema == nil {
|
||||
return map[string]any{"type": "object", "properties": map[string]any{}}, nil
|
||||
return emptyObjectSchema(), nil
|
||||
}
|
||||
return t.info.InputSchema, nil
|
||||
}
|
||||
|
||||
// Execute invokes the MCP tool and returns its concatenated text content.
|
||||
// Execute 调用 MCP 工具并返回其结果文本。
|
||||
// 参数先经 input schema 缓存校验 + 补默认值(design/09 §1.1),
|
||||
// 文本块全为空时退回结构化结果的 JSON 文本,保证模型总能看见东西。
|
||||
func (t *mcpTool) Execute(ctx context.Context, args any) (any, error) {
|
||||
res, err := t.client.CallTool(ctx, t.info.Name, args)
|
||||
params, err := normalizeArgs(args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return contentText(res.Content), nil
|
||||
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 concatenates the text blocks of a tool result into a single
|
||||
// string, ignoring non-text blocks.
|
||||
// contentText 把工具结果里的文本块拼成一个字符串,忽略非文本块。
|
||||
func contentText(content []ContentBlock) string {
|
||||
var b strings.Builder
|
||||
for _, blk := range content {
|
||||
@@ -53,11 +97,47 @@ func contentText(content []ContentBlock) string {
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// toContentBlocks converts wire content blocks to the public content block type.
|
||||
func toContentBlocks(wire []contentBlock) []ContentBlock {
|
||||
out := make([]ContentBlock, 0, len(wire))
|
||||
for _, c := range wire {
|
||||
out = append(out, ContentBlock{Type: c.Type, Text: c.Text})
|
||||
// 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"
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user