首页 / 资讯中心 / 文章详情

learn-claude-code 实战:用 MCP 与 JSON-RPC 打通 Agent 工具链

learn-claude-code 实战:用 MCP 与 JSON-RPC 打通 Agent 工具链 ★ FEATURED ARTICLE
1. 从 learn-claude-code 的消息循环说起Agent 工具链到底卡在哪如果你跟着 learn-claude-code 的 s01 到 s05 一路写下来大概率会有一种“能跑但不够用”的感觉。s01 里那个while True消息循环加上一个写死的bash工具确实能让模型跑命令、看结果、再决定下一步。但一旦你想让它读文件、写文件、查文档、连数据库就得在TOOL_HANDLERS里一个个手写 handler工具声明TOOLS列表也跟着膨胀。这就是 learn-claude-code 项目里 Agent 与 LLM 协作机制最真实的痛点工具注册、调用、结果回传这条链路全靠自己维护。我在复现 s02_tool_use 的时候把bash、read_file、write_file、edit_file四个工具塞进分发表代码还能看。但到了 s19_mcp 那一章作者直接点破了问题如果没有 MCP每接一个系统你都要在 Agent 里手写一套工具逻辑Jira、部署、Notion、GitLab 各来一套Agent 会越来越臃肿。MCP 要解决的就是这件事——Agent 只实现一套通用的 MCP Client外部服务自己实现 MCP Server通过标准协议发现和调用外部工具。这篇文章聚焦的就是这条协议层链路。我会从 MCP 协议与 JSON-RPC 通信切入把工具注册、调用、结果回传的完整过程拆开给出可复制的 MCP 服务端配置片段和 JSON-RPC 请求示例再演示一次本地 Agent 工具调用的验证步骤。适合已经跑过 learn-claude-code 前几章、想搞清楚“协议层怎么支撑上层智能体行为”的读者。核心检索词就三个learn-claude-code 的 Agent 循环、MCP 协议、JSON-RPC 消息格式。搞懂这三者的关系你再看 s19_mcp_plugin 的代码就不会觉得是在看天书。先说结论MCP、JSON-RPC、stdio/HTTP 三者的关系可以用一句话概括MCP 是协议JSON-RPC 是 MCP 消息的编码格式stdio/HTTP/SSE 是传输这些 JSON-RPC 消息的通道。这个分层理解非常关键因为很多人第一次看 MCP 文档会懵——为什么一会儿说 MCP 方法一会儿又冒出jsonrpc: 2.0。其实它们不在一个层面上。MCP 定义的是“有哪些方法、方法语义是什么”比如tools/list、tools/call、initializeJSON-RPC 定义的是“这些方法怎么用 JSON 表达成请求和响应”传输层则决定这些 JSON 字符串是通过子进程的 stdin/stdout 走还是通过 HTTP 请求走。回到 learn-claude-code 的语境。s01 里模型返回的tool_useblock本质上是 Anthropic Messages API 的工具调用格式字段是type、id、name、input。而 MCP 里的工具调用走的是 JSON-RPC 的tools/call字段是jsonrpc、id、method、params。两者格式不同但要做的事情一样告诉执行方“我要调用哪个工具、参数是什么”然后拿回结果。Agent 的 MCP Client 干的活就是在这两种格式之间做转换——把模型给的tool_use翻译成 MCP 的tools/call再把 MCP 返回的result翻译成tool_result塞回消息历史。理解了这层转换s19 的代码逻辑就顺了。2. TaoToken 前置准备给 Agent 一个稳定的模型入口在动手接 MCP 之前得先保证你的 Agent 能稳定调到模型。learn-claude-code 的示例代码用的是 Anthropic SDK通过base_url和auth_token两个环境变量指定入口。这一步如果没配好后面 MCP 链路再对也跑不起来因为模型根本不会返回tool_use。我自己的做法是准备一个独立的模型接入入口把 base_url 和 key 放在.env里代码里只读环境变量。这样切换模型或换 key 的时候不用改代码。TaoToken 在这里的角色就是一个兼容 Anthropic 协议的模型接入点你可以在它的控制台里创建 API Key然后把它填到ANTHROPIC_BASE_URL和ANTHROPIC_AUTH_TOKEN里。注意 base_url 用https://taotoken.net/api不要带多余的路径后缀SDK 会自己拼/v1/messages。具体操作上先去控制台创建一个 API Key然后打开 API Keys 页面确认 key 已经生效。接着在项目根目录建一个.env文件内容大概是这样ANTHROPIC_BASE_URLhttps://taotoken.net/api ANTHROPIC_AUTH_TOKENsk-你的key MODEL_IDclaude-sonnet-4-20250514这里MODEL_ID是 learn-claude-code 代码里os.environ[MODEL_ID]读的那个变量必须和你在控制台看到的模型 ID 一致。我踩过的坑是一开始把MODEL_ID写成了展示名而不是实际 ID结果请求返回 404排查了半天才发现是模型名不对。所以创建完 key 之后顺手在模型对话页面确认一下当前可用的模型 ID复制准确的字符串。如果你更习惯用命令行工具管理这些配置TaoToken 也提供了 coding-plan 和 console 入口可以按需选用。但不管用哪种方式核心就三件套Base URL、API Key、Model ID。这三个东西在后面的 MCP 配置里还会再出现一次因为 MCP Server 如果也要调模型同样需要它们。我建议把这三件套单独记在一个地方避免每次都要翻控制台。配好之后先别急着上 MCP用 learn-claude-code 的 s01 脚本验证一下模型能不能通。跑python agents/s01_agent_loop.py输入pwd如果能看到模型返回tool_use并且执行了pwd命令说明模型入口是通的。这一步是整个链路的地基地基不稳后面全是玄学问题。验证通过后再进入 MCP 配置环节。3. 可复制配置MCP 服务端与 JSON-RPC 请求示例这一节是全文最实操的部分。我会给出一个最小可用的 MCP Server 配置以及对应的 JSON-RPC 请求示例你可以直接复制到自己的项目里改。先看 MCP Server 的配置。learn-claude-code 的 s19 用的是 mock server真实场景下你需要一个能跑的 MCP Server。以 Node.js 为例用官方 SDK 写一个最简单的 stdio server暴露一个search工具{ mcpServers: { docs: { command: node, args: [/absolute/path/to/docs-mcp-server.js], env: { DOCS_API_KEY: your-docs-key } } } }这个配置片段是 MCP Client 读取的告诉它怎么启动docs这个 server。command是启动命令args是参数env是传给子进程的环境变量。stdio 模式下Client 会启动这个子进程然后通过 stdin/stdout 收发 JSON-RPC 消息。如果你用的是 HTTP 传输配置会变成url字段指向远程 server 地址。对应的 MCP Server 代码骨架大概是这样注意ListToolsRequestSchema和CallToolRequestSchema这两个 handlerimport { Server } from modelcontextprotocol/sdk/server/index.js; import { StdioServerTransport } from modelcontextprotocol/sdk/server/stdio.js; import { CallToolRequestSchema, ListToolsRequestSchema } from modelcontextprotocol/sdk/types.js; const server new Server( { name: docs, version: 1.0.0 }, { capabilities: { tools: {} } } ); server.setRequestHandler(ListToolsRequestSchema, async () ({ tools: [ { name: search, description: Search documents, inputSchema: { type: object, properties: { query: { type: string } }, required: [query] } } ] })); server.setRequestHandler(CallToolRequestSchema, async (request) { const { name, arguments: args } request.params; if (name search) { return { content: [{ type: text, text: Found 3 documents about ${args.query}. }] }; } throw new Error(Unknown tool: ${name}); }); const transport new StdioServerTransport(); await server.connect(transport);这里有个很多人会问的点为什么代码里没有单独写tools/list的处理因为tools/list是 MCP 协议里的方法名而ListToolsRequestSchema是 SDK 对这个方法的封装。你用 SDK 写 server 时不需要直接写tools/list字符串SDK 已经帮你把协议方法名和 handler 对应好了。协议规定的方法名包括initialize、tools/list、tools/call、resources/list、prompts/list这些不是随便写的是 MCP 规范定死的。现在看 JSON-RPC 消息长什么样。Client 想问 server “你有哪些工具”MCP 语义是tools/listJSON-RPC 请求是{ jsonrpc: 2.0, id: 1, method: tools/list, params: {} }Server 返回{ jsonrpc: 2.0, id: 1, result: { tools: [ { name: search, description: Search documents, inputSchema: { type: object, properties: { query: { type: string } }, required: [query] } } ] } }注意请求和响应的id都是 1这是 JSON-RPC 的匹配机制Client 靠这个 id 知道响应对应哪个请求。调用工具时请求变成{ jsonrpc: 2.0, id: 2, method: tools/call, params: { name: search, arguments: { query: MCP transport } } }Server 返回{ jsonrpc: 2.0, id: 2, result: { content: [ { type: text, text: Found 3 documents about MCP transport. } ] } }这套 JSON-RPC 消息就是 MCP 协议在传输层实际跑的东西。Agent 的 MCP Client 负责把模型给的tool_use转成上面的tools/call再把result转回tool_result。如果你用 Claude Code 或者 Cline 这类工具它们的 MCP 配置也是这个结构只是配置文件路径不同。Claude Code 的配置通常在~/.claude/settings.json或项目级.mcp.jsonCline 则在 VS Code 的 settings 里。不管哪个核心字段都是command、args、env或url。4. 验证请求跑通一次本地 Agent 工具调用配置写好了接下来验证整条链路。我会用一个最小脚本模拟 Agent 从模型拿到tool_use转成 MCP 的tools/call再拿回结果的过程。先确认 MCP Server 能独立启动。在终端里跑node /absolute/path/to/docs-mcp-server.js如果它没有立刻退出而是等待 stdin 输入说明 stdio server 启动正常。你可以手动喂一条 JSON-RPC 请求进去测试echo {jsonrpc:2.0,id:1,method:tools/list,params:{}} | node /absolute/path/to/docs-mcp-server.js正常的话会输出一行 JSON包含tools数组。这一步能过说明 server 端的协议处理没问题。然后写一个最小的 MCP Client 验证脚本用 Node.js 的 SDKimport { Client } from modelcontextprotocol/sdk/client/index.js; import { StdioClientTransport } from modelcontextprotocol/sdk/client/stdio.js; const transport new StdioClientTransport({ command: node, args: [/absolute/path/to/docs-mcp-server.js] }); const client new Client({ name: test-client, version: 1.0.0 }); await client.connect(transport); const tools await client.listTools(); console.log(Tools:, tools.tools.map(t t.name)); const result await client.callTool({ name: search, arguments: { query: MCP transport } }); console.log(Result:, result.content[0].text); await client.close();跑这个脚本如果能看到Tools: [ search ]和Result: Found 3 documents about MCP transport.说明 Client 到 Server 的 JSON-RPC 链路是通的。这一步验证的是协议层还没涉及模型。接下来把它接进 learn-claude-code 的 Agent 循环。在 s19 的assemble_tool_pool()里Agent 会先只有内置工具模型决定调用connect_mcp后才创建 MCP Client 并注册外部工具。工具名会被改写成mcp__docs__search这种格式避免和内置工具冲突。当模型调用mcp__docs__search时handler 转发到docs_client.call_tool(search, {...})拿回结果再塞回消息历史。完整的验证流程是这样的启动 Agent输入Connect to the docs MCP server and search for MCP transport。观察日志应该能看到这几步模型先返回connect_mcp的tool_useAgent 创建 docs client注册mcp__docs__search和mcp__docs__get_version工具池刷新后模型返回mcp__docs__search的tool_usehandler 转发到 MCP Client发出tools/call请求server 返回结果Agent 把结果作为tool_result加回历史模型基于结果生成最终回答。如果这一步跑通了你就完整走了一遍“模型决策 → 工具调用 → 协议转换 → 结果回传 → 模型再决策”的闭环。这个闭环就是 learn-claude-code 里 Agent 与 LLM 协作机制的核心。s01 的消息循环是骨架MCP 是让骨架能长出无数工具的手臂。5. 本篇常见错排查401、local proxy failed、reading choices、OAuth这一节列几个我在接 MCP 和模型入口时真实遇到过的报错以及排查思路。这些错误在 learn-claude-code 的 issue 区和各种 Agent 群里出现频率很高。第一个是 401。这个基本是 key 的问题。先检查.env里的ANTHROPIC_AUTH_TOKEN有没有多余空格再确认 key 有没有过期或被禁用。如果用的是 TaoToken去 API Keys 页面看一眼 key 的状态。还有一种情况是 base_url 写错了比如多写了/v1导致 SDK 拼出来的路径变成/v1/v1/messages服务端返回 401 或 404。正确的 base_url 是https://taotoken.net/apiSDK 会自己拼/v1/messages。第二个是local proxy failed。这个报错通常出现在 MCP Client 启动 stdio server 的时候。原因可能是command路径不对或者args里的脚本文件不存在。排查方法是把command和args拼成一条命令在终端里手动跑一遍看能不能启动。如果手动能跑、Client 跑不了检查一下 Client 的工作目录和环境变量因为 stdio 子进程继承的是 Client 的环境不是你的 shell 环境。第三个是reading choices相关的报错。这个一般出现在模型返回的响应格式不符合预期时比如你用的模型不支持 tool use或者max_tokens设得太小导致响应被截断。learn-claude-code 的代码里max_tokens8000如果你改成很小的值模型可能还没输出完tool_use就被截断了解析时就会报错。另外确认一下MODEL_ID对应的模型是否支持 function calling有些轻量模型不支持。第四个是 OAuth 相关。如果你接的 MCP Server 需要 OAuth 鉴权比如某些远程 server配置里会有auth字段。这类 server 的排查重点是 token 有没有过期、scope 对不对。stdio 模式的本地 server 一般不需要 OAuth如果你遇到 OAuth 报错先确认是不是把远程 server 的配置错放到了 stdio 模式里。还有一个高频问题是工具名冲突。MCP 工具注册时会被加上mcp__server__tool前缀如果你手写的内置工具也叫类似名字可能会混淆。建议内置工具用简单名外部工具统一走前缀。另外如果模型一直不调用 MCP 工具检查一下assemble_tool_pool()有没有在connect_mcp之后重新执行工具池没刷新的话模型看不到新工具。最后提醒一个配置层面的坑Claude Code、Cline、Codex 这些工具的 MCP 配置格式略有不同但核心三件套是一样的——Base URL、API Key、Model ID。如果你在 Claude Code 里配 MCP记得同时确认模型入口的 base_url 和 key 也配对了因为 MCP 工具调用最终还是要走模型。三件套缺一个链路就断。6. 把协议层用起来从 learn-claude-code 到真实 Agent 工具链走到这里你应该已经能把 learn-claude-code 的 Agent 循环和 MCP 协议层串起来了。s01 的消息循环负责“不停转”s02 的工具分发表负责“知道调哪个”s19 的 MCP Client 负责“把外部工具接进来”。这三层叠在一起就是一个能动态扩展工具链的 Agent 骨架。如果你想把这条链路用到真实项目里我的建议是先从 stdio 模式的本地 MCP Server 开始把工具注册、调用、结果回传跑顺再考虑 HTTP 或 SSE 的远程 server。本地 server 的好处是调试方便JSON-RPC 消息可以直接在终端里看。等协议层稳定了再往上叠业务逻辑。模型入口这块保持 Base URL、API Key、Model ID 三件套的配置习惯不管换哪个模型或哪个接入点改.env就行代码不用动。需要长期跑编码任务或者 Agent 工作流的话可以了解一下 coding-plan它更适合持续性的调用场景。验证模型能力或者临时调试用模型对话页面就够了。接入文档里有完整的协议说明和示例遇到配置问题可以先翻一遍。最后留一个实用技巧在 Agent 循环里加一行日志把每次发给模型的messages长度和工具池里的工具名打出来。这样当模型不调工具、或者调了不存在的工具时你能一眼看出是工具池没刷新还是消息历史里缺了tool_result。这个日志我在调试 MCP 链路时加过省了很多猜的时间。协议层的东西看着抽象但把消息打出来对着看很快就清楚了。
阅读完成 · 觉得有帮助?
咨询建站