Featured image of post 第八话|MCP 协议:工具为什么需要一根 USB-C

第八话|MCP 协议:工具为什么需要一根 USB-C

用 Python 和 Java 21 跑通 MCP 的工具发现与调用。

系列 GYA(Get Your Agent) 第 8 / 12 篇
主题 AgentLLM教程MCP工具系统

第四话里,我们把工具放进了一个 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

一次最小工具调用大致经历:

  1. Client 与 Server 初始化并协商协议版本和能力;
  2. Client 请求工具列表;
  3. Server 返回工具名称、描述和输入 Schema;
  4. Client 发起工具调用;
  5. 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 不需要自己手写 ab 的 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 解决“应该怎么做”。

参考资料与实测记录

留言

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