第四话里,我们把工具放进了一个 ToolRegistry。工具多了以后,注册表比 if-else 强很多:可以发现工具、统一调用、返回结构化错误。
但它有一个隐含前提:工具和 Agent 在同一个进程,至少在同一套代码里。
如果天气服务是另一个 Python 进程,数据库工具是 Java 服务,GitHub 工具由另一个团队维护,第四话的注册表还能直接解决吗?不能。你需要给每一种外部工具写一套适配器,工具数量和接入方数量一增加,就会重新掉进 N×M 集成地狱。
这就是 MCP 要解决的问题。
ToolRegistry 管理进程内工具,MCP 标准化工具提供方和 Agent 客户端之间的边界。
一、旧世界:工具被绑在调用方代码里
第四话的最小闭环是:模型输出工具名和参数,注册表根据名称找到函数,函数执行后返回 ToolResponse。
LLM → ToolRegistry → Python 函数 → ToolResponse → LLM
这个结构对学习和单体应用非常好。你能清楚看到每一层的职责,也能自己控制错误码和参数校验。
问题出在边界。假设 Agent 需要 20 个工具,而这 20 个工具分布在 5 个独立服务里。没有统一协议时,客户端要知道每个服务如何启动、如何描述工具、如何传参数、如何返回结果。每接一个服务,都要重新写一套通信和适配代码。
这不是工具本身太多,而是每个客户端都在重复理解每个服务的私有接口。
二、MCP 把什么放到了协议层
MCP 可以先用三个角色理解:
- Host:承载 Agent 的应用,例如桌面助手、IDE 或你的 Python 程序;
- MCP Client:Host 内部的协议客户端,负责连接某一个 MCP Server;
- MCP Server:工具、资源或提示模板的提供方。
它们的关系不是“模型直接连接 MCP Server”,而是:
Host
└── MCP Client ── JSON-RPC/Transport ── MCP Server
└── tools
一次最小工具调用大致经历:
- Client 与 Server 初始化并协商协议版本和能力;
- Client 请求工具列表;
- Server 返回工具名称、描述和输入 Schema;
- Client 发起工具调用;
- Server 执行函数并返回内容或错误。
这里有一个重要边界:MCP 标准化的是“怎么发现和调用”,不是“下一步该调用什么”。任务规划仍然由 Agent 循环和模型负责,工具的权限、业务正确性和结果验证仍然由应用负责。
三、STDIO:先把跨进程边界跑通
MCP 支持多种传输方式。第八话先选择 STDIO,因为它最容易看清进程边界:Client 启动 Server 子进程,JSON-RPC 消息通过 stdin/stdout 传递。
STDIO 有一个很容易踩的坑:stdout 是协议通道,不是日志通道。 Server 如果把调试日志打印到 stdout,Client 读到的就不再是合法的 JSON-RPC 消息。日志应该写 stderr,或者通过 MCP 的 logging 能力发送。
生产环境更常用 Streamable HTTP:Server 可以独立部署,多个 Client 通过网络连接。但这同时带来认证、超时、并发、TLS、会话和部署问题。先跑通 STDIO,再讨论 HTTP,学习成本更低。
四、Python:用官方 SDK 跑通 Client/Server
前置环境
- Python 3.10+;本次用 Python 3.11.15 实测;
mcp[cli]==2.0.0;- 不需要 LLM API Key。
代码位于 GYA/c08-mcp/。Server 只有一个工具:
from mcp.server import MCPServer
mcp = MCPServer("c08-demo")
@mcp.tool()
def add(a: int, b: int) -> int:
"""Add two integers and return the result."""
return a + b
if __name__ == "__main__":
mcp.run()
类型注解不仅服务于 Python 类型检查,也帮助 SDK 生成工具输入 Schema。Client 不需要自己手写 a、b 的 JSON Schema,就能通过 tools/list 发现它。
async with stdio_client(server) as (read_stream, write_stream):
async with ClientSession(read_stream, write_stream) as client:
await client.initialize()
listed = await client.list_tools()
result = await client.call_tool("add", {"a": 2, "b": 3})
这里使用的是 mcp==2.0.0 的实际 API:stdio_client 返回读写流,再交给 ClientSession。在线文档中的更高层 Client(StdioServerParameters(...)) 写法属于更新的文档线,不能未经验证地复制到锁定版本。
运行:
cd GYA/c08-mcp
python3 -m venv .venv
source .venv/bin/activate
python3 -m pip install -r requirements.txt
python3 main.py
本次实测关键输出:
[1] 初始化 MCP Server: experimental={} ... tools=ToolsCapability(list_changed=False) ...
[2] 发现工具: ['add']
[3] 调用 add(2, 3): 5
[4] 错误场景: is_error=True; Unknown tool: missing_tool
Python SDK 把未知工具作为一个带 is_error=True 的工具结果返回,Client 不会因为这个工具级错误直接崩溃。
五、Java 21:同一个协议,不同的 SDK 表达
前置环境
- Java 21;
- Gradle Wrapper 8.14.2;
io.modelcontextprotocol.sdk:mcp:2.0.0;- 不需要 LLM API Key。
代码位于 GYA-Java/c08-mcp/。Java 版使用 StdioServerTransportProvider 暴露 Server,使用 StdioClientTransport 启动 Server 子进程。工具定义需要显式提供输入 Schema:
McpSyncServer server = McpServer.sync(transport)
.serverInfo("c08-java-demo", "1.0.0")
.capabilities(ServerCapabilities.builder().tools(true).build())
.toolCall(
Tool.builder("add", schema)
.description("Add two integers")
.build(),
(exchange, request) -> {
int a = ((Number) request.arguments().get("a")).intValue();
int b = ((Number) request.arguments().get("b")).intValue();
return CallToolResult.builder()
.content(List.of(new McpSchema.TextContent(Integer.toString(a + b))))
.build();
})
.build();
运行:
cd GYA-Java
JAVA_HOME=$(/usr/libexec/java_home -v 21) ./gradlew :c08-mcp:run
本次实测关键输出:
[1] 初始化 MCP Server: c08-java-demo
[2] 发现工具: [add]
[3] 调用 add(2, 3): 5
[4] 错误场景: McpError; Unknown tool: invalid_tool_name
Java SDK 2.0.0 对未知工具的处理和 Python 不一样:它返回 JSON-RPC 错误,客户端表现为 McpError。这不是“谁对谁错”,而是两个 SDK 对协议级错误的 API 映射不同。跨语言教程不能只比较正常输出,也要把错误语义写清楚。
六、MCP 和第四话 ToolRegistry 的边界
| 维度 | 第四话 ToolRegistry | 第八话 MCP |
|---|---|---|
| 工具位置 | 同一进程或代码库 | 独立进程或远程服务 |
| 发现方式 | 读取本地注册表 | 协议请求发现 |
| 调用边界 | 函数调用 | JSON-RPC + 传输协议 |
| 多语言 | 需要自己适配 | Client/Server 可以异构 |
| 主要价值 | 进程内工具管理 | 工具接入与复用 |
| 没有解决 | 任务规划、权限、结果验证 | 任务规划、权限、结果验证 |
MCP 不是 Agent 的替代品,也不是把任何函数自动变成可靠工具的魔法。它只是把原来散落在每个应用里的“如何连接工具”这部分共性抽出来,变成一套可复用的协议。
七、从 Demo 到生产还缺什么
这个 demo 故意没有 API Key,也没有真实业务副作用。生产 MCP Server 至少还需要:
- 输入 Schema 和业务层双重校验;
- 超时、重试和幂等策略;
- 认证、授权和最小权限;
- 不把凭据通过环境继承或日志泄露;
- 对工具结果做真实性和完整性验证;
- 对 Server、Client、工具调用和错误建立可观测性。
尤其要记住:MCP 的“能调用”不等于业务上的“允许调用”。 文件删除、支付、发邮件、修改生产数据,都需要在协议之外增加权限和人工审批边界。
下一篇:Skill 把“做法”变成资产
现在我们解决了“工具在哪里、怎么发现、怎么调用”。但还有一个问题:工具能调用,不代表 Agent 知道应该如何完成一个复杂任务。
下一篇进入 Skill:把触发条件、步骤、约束、脚本和验证方式封装成可复用能力。工具解决“能做什么”,Skill 解决“应该怎么做”。
参考资料与实测记录
- Model Context Protocol 官方 Python SDK(Python 3.11.15、
mcp[cli]==2.0.0,本轮验证记录:2026-08-21) - MCP Python SDK Client Transports(STDIO 传输说明,本轮核对:2026-08-21)
- Model Context Protocol 官方 Java SDK(Java 21、SDK 2.0.0,本轮验证记录:2026-08-21)
- MCP Java SDK Client(STDIO Client 与工具调用 API,本轮核对:2026-08-21)
- MCP Java SDK Server(STDIO Server 与 Tool Specification,本轮核对:2026-08-21)

留言
欢迎分享你的想法。评论提交后会在审核通过后显示。