ByteNoteByteNote
连接外部工具的统一标准:MCP 协议
字

字节笔记本

2026年10月24日 · 约 13 分钟读完

连接外部工具的统一标准:MCP 协议

API中转
¥120

通过 MCP 连接外部工具

本文是《从 0 写 Agent》系列第 28 章。这个系列基于开源项目 agents-go(OpenAI Agents SDK 的 Go 移植版),用 Go 从零实现一个生产级 Agent 运行时;本章讲「MCP 协议:连接外部工具的统一标准」。

到目前为止,Agent 的工具都是手写的 Go 函数。但真实世界有大量现成工具,GitHub 操作、数据库查询、文件系统、浏览器控制,你不可能全自己写。

MCP(Model Context Protocol)是连接外部工具的统一标准。agents-go 的 mcp 包是 MCP 客户端,把 MCP server 提供的工具接入 Agent。这一章拆它怎么适配,以及 schema 兼容这个大坑。


引子:为什么需要 MCP

没有 MCP 之前,每个 Agent 框架都要自己写工具集成,Claude Code 写一套、OpenAI 写一套、Cursor 写一套。工具不能复用,每个框架重复造轮子。

MCP 解决这个,一个标准协议,任何 MCP server 提供的工具,任何支持 MCP 的 Agent 都能用。 写一个 "GitHub MCP server",Claude Code、agents-go、Cursor 都能用它操作 GitHub。

agents-go 的 mcp 包就是 MCP 客户端,它连接 MCP server,把 server 的工具转换成 agents-go 的 Tool 接口。


安全:Claude Code 的安全实践

三种 Transport

MCP server 通过三种方式通信(mcp.go:122-144):

Transport怎么通信适合
Stdio启动子进程,stdin/stdout 通信本地工具(文件系统、git)
Streamable HTTPHTTP 连接(当前规范)远程服务、云端工具
SSE(已废弃)Server-Sent Events旧规范,新项目别用
go
// Stdio:启动一个子进程当 MCP server
server, _ := mcp.NewStdioServer(ctx, "filesystem",
    exec.Command("npx", "-y", "@modelcontextprotocol/server-filesystem", "/tmp"),
    mcp.Options{})

// Streamable HTTP:连远程 server
server, _ := mcp.NewStreamableHTTPServer(ctx, "github",
    "https://mcp.github.com/sse", mcp.Options{})

// 挂到 Agent 上
agent := &agents.Agent{
    Name: "助手",
    MCPServers: []agents.MCPServer{server},
}

Agent 不关心 transport 细节,MCP server 的工具被统一适配成 Tool 接口。


源码对照:从 MCP tool 到 agents.Tool

toolFor(mcp.go:233)把一个 MCP tool 适配成 agents-go 的 FunctionTool:

go
// 概念示意(mcp.go:233)
func (s *Server) toolFor(mcpTool) *agents.FunctionTool {
    return &agents.FunctionTool{
        Name: s.opts.ToolNamePrefix + mcpTool.Name,  // 可加前缀防冲突
        Description: mcpTool.Description,
        ParamsJSONSchema: schemaToMap(mcpTool.Schema),  // schema 适配
        OnInvoke: func(ctx, tc, argsJSON string) (any, error) {
            // 关键:用"原始未加前缀的名字"调用 server
            result, err := s.session.CallTool(ctx, mcpTool.Name, argsJSON)
            // 错误处理(见下面)
            return resultOutput(result), nil
        },
    }
}

注意一个细节,OnInvoke 里调 server 用的是 mcpTool.Name(原始未加前缀的名字),即使对外暴露的名字加了前缀(ToolNamePrefix)。因为 server 不认识你加的前缀,它只认原始名。


大坑:Schema 适配

这是 MCP 集成最难的部分。很多 MCP server 的 schema 和 OpenAI strict mode 不兼容。

OpenAI 的 strict mode 要求 schema 满足一堆约束(additionalProperties: false、所有字段有类型、不允许多余字段)。但很多 MCP server 的 schema 是宽松的,可能有 additionalProperties: true、可能有 union 类型、可能缺字段。

agents-go 的 schemaToMap(mcp.go:281)做 deep-copy + EnsureStrictJSONSchema 重写,把 MCP 的宽松 schema 转成 OpenAI strict 兼容的。

关键,失败不能留下半改写的 schema(mcp.go:238 注释)。如果改到一半失败,要么完整改完,要么用原 schema,不能给模型一个"改了一半"的坏 schema。这是原子性考虑。

Options.Strict(mcp.go:30)可以关闭这个转换,如果你的 MCP server 本来就兼容 strict,或者转换出了问题,关掉它。


Options 的丰富配置

mcp.Options(mcp.go:22)提供了大量配置,几个重点:

AllowedTools / BlockedTools,静态过滤

go
Options{
    AllowedTools: []string{"read_file", "write_file"},  // 只暴露这两个
    BlockedTools: []string{"delete_file"},               // 永远不暴露
}

白名单/黑名单,在 ListTools 时过滤。

ToolFilter,动态过滤

go
Options{
    ToolFilter: func(ctx, rc, agent, toolName string) bool {
        // 根据 RunContext 动态决定
        if userIsAdmin(rc) { return true }  // 管理员看到所有工具
        return toolName != "delete_file"    // 普通用户看不到删除工具
    },
}

比静态过滤更灵活,可以根据运行时上下文决定。 适合权限控制。

CacheToolsList,缓存工具列表

go
Options{CacheToolsList: true}

MCP server 的工具列表第一次获取后缓存,后续 turn 不重新 list_tools(省网络往返)。但静态/动态过滤每轮都跑(在缓存的列表上过滤)。如果 server 的工具变了,调 InvalidateToolsCache。

ToolNamePrefix,防命名冲突

go
Options{ToolNamePrefix: "github_"}

多个 MCP server 可能有同名工具(比如两个 server 都有 "read_file")。加前缀变成 github_read_file 和 fs_read_file,避免冲突。但调用 server 时用原始名(去掉前缀)。

RequireApproval,MCP 工具的 HITL

go
Options{RequireApproval: func(toolName string) bool {
    return toolName == "delete_repo"  // 删仓库要审批
}}

给 MCP 工具加审批。 把第 12 章的 HITL 扩展到 MCP 工具。


设计权衡:MCP 错误回喂

MCP tool 执行出错,怎么处理?agents-go 的策略(mcp.go:253),错误回喂模型,让模型自愈:

go
// MCP tool 返回 IsError=true → 通过 DefaultToolErrorFunction 喂回模型
// 而不是中断整个 run

这和第 32 章的 Sandbox 相反,Sandbox 的错误处理是"基础设施错 abort,代码错回喂"。MCP 是"MCP 层的错误都回喂"。

为什么不同?因为 MCP tool 的错误通常是工具逻辑层面的(参数不对、权限不够、资源不存在),不是基础设施层的。让模型知道"这个工具失败了,错误是 X",模型可以调整参数重试或换工具。

对比 Sandbox,Sandbox 执行用户代码,代码本身的错误(exit code 非 0)是"正常输出"(喂回模型),但 Docker 容器启动失败(基础设施错)是"该 abort"。两种错误的语义不同,处理也不同。


OAuth 2.1 + PKCE

对于需要认证的 MCP server(比如 GitHub MCP),Options.OAuthHandler(mcp.go:58)支持 OAuth 2.1 授权码 + PKCE + 动态客户端注册。第 29 章详讲。


小结

读完这一章,你应该理解:

  1. MCP 是什么,连接外部工具的统一标准协议
  2. 三种 transport,Stdio(本地子进程)、Streamable HTTP(远程)、SSE(废弃)
  3. Schema 适配是大坑,MCP 宽松 schema 要转成 OpenAI strict,失败不能留半改写
  4. 丰富配置,静态/动态过滤、缓存、前缀防冲突、HITL 审批
  5. 错误回喂策略,MCP tool 错误喂回模型自愈,不中断 run

MCP 让 Agent 从"只用自己写的工具"变成"连接整个工具生态"。 下一章讲 MCP 的进阶,OAuth 认证、多模态结果回传。


下一篇

第 29 章,MCP 的进阶:OAuth、缓存、多模态。MCP server 返回的图片怎么传给模型?OAuth 2.1+PKCE 怎么做?ToolFilter 动态过滤的实战场景。


本文是《从 0 写 Agent》系列第 28 章,案例代码基于开源项目 agents-go(OpenAI Agents SDK 的 Go 移植版)。全系列目录见总览。

相关文章

分享: