1. 从 stdio 到 JSON-RPC:MCP 在 Agent 里到底怎么跑起来
MCP(Model Context Protocol)是一套让 Agent 与外部工具解耦的开放协议,它把「工具定义」和「工具执行」从智能体进程里拆出来,变成独立的 Server 端点。它适合谁?适合那些已经写过 Function Calling、但被「工具硬编码在业务代码里、换个宿主就得重写一遍」折磨过的开发者。我这次要做的,是一个最小可跑的本地 MCP Server:用 stdio 传输层承载 JSON-RPC 消息,暴露一个createRefund工具,再让 Agent 侧通过 TaoToken 统一 Key 把模型调用接上,最后完整验证一次工具调用链路。
很多人第一次接触 MCP 会误以为它是「Function Calling 的替代品」,其实两者根本不在同一层。Function Calling 是模型能力——你在请求里塞tools字段,模型回你一个tool_calls;MCP 是工程协议——它规定工具怎么被描述、怎么被调用、怎么把结果拿回来。模型侧看到的 schema 一模一样,变的只是「工具从哪来、在哪执行」。理解这一点,后面所有配置和排障才有主线。
这篇笔记按可跟做的顺序走:先讲清楚 stdio 和 JSON-RPC 的分工,再给出可复制的 server 配置片段,然后跑一次完整的tools/list+tools/call验证,最后把常见报错逐个对照排查。模型侧统一走 TaoToken 的 API 通道,Key 和 Base URL 只配一次,Agent 和 MCP Server 都能复用。
1.1 stdio 传输层:进程边界就是信任边界
stdio 是 MCP 最朴素的传输方式:Host 用ProcessBuilder拉起一个子进程,双方通过 stdin/stdout 交换 JSON-RPC 报文,stderr 专门留给日志。它的精妙之处在于——进程边界天然就是信任边界。你不需要服务发现、不需要端口分配、不需要鉴权、不需要 TLS,因为「我自己 fork 的子进程」本身就是权限来源。
这里有一条硬约定,也是新手最容易踩的坑:stdout 只能写协议数据,日志必须走 stderr。你在 server 里随手写一个console.log,就等于往协议通道里插了一段非法 JSON,客户端解析直接挂掉。我试过在调试时忘了这茬,连接莫名其妙断开,查了半小时才发现是日志打错了流。
stdio 的代价是它必然只能本地。协议定义它就是 client-launched subprocess,跨机器物理上不可能。所以本地工具、IDE 插件、开发调试用 stdio;团队共享、生产部署换 HTTP transport。这个选型后面还会展开。
1.2 JSON-RPC 消息格式:四条消息就是全部交互
MCP 的数据层基于 JSON-RPC 2.0,stdio 下就是按行分隔的 JSON 文本在管道里来回。一次完整的工具调用,核心就四条消息:
| 阶段 | 消息 | 方向 | 作用 |
|---|---|---|---|
| 握手 | initialize | Client → Server | 协商版本、交换能力声明 |
| 握手 | notifications/initialized | Client → Server | 通知,无 id、不回应 |
| 说明书 | tools/list | Client → Server | 拿 name + description + inputSchema |
| 执行 | tools/call | Client → Server | 传参数,拿 content[] 回来 |
工具定义本身就是一份 JSON Schema。比如createRefund的inputSchema里,itemName和reason都是 string 且 required。这份结构和 Function Calling 里tools字段的结构是同一类东西——模型看到的还是这份 schema,所以 Tool 设计原则(三段式描述、参数不超 3-4 个、不让模型猜 ID)在 MCP 下一条都不变。
1.3 Function Calling 与 MCP 的分工
把两者的对应关系记住,MCP 的交互就不会忘:
| Function Calling 侧 | MCP 侧 |
|---|---|
请求里的tools字段 | tools/list的返回值 |
模型返回的tool_calls | 触发tools/call的入参 |
回写给模型的tool消息 | tools/call返回的content[] |
所以一轮对话的模型请求次数不变,仍是 N+1 次。MCP 不增加模型调用,只增加 Agent 侧的 IPC 往返;token 成本也完全不变,因为 schema 逐字节相同。MCP 取代的不是 Function Calling 这个机制,而是「工具硬编码在智能体里」这种工程组织方式。
2. TaoToken 前置:统一 Key 与 API 通道
在动手写 server 之前,先把模型侧的通道准备好。MCP 本身不碰模型,但 Agent 要调模型来决策「调哪个工具」,这一步走 TaoToken 的 API 通道最省事——一个 Key、一个 Base URL,Agent 和后续的验证脚本都能复用。
TaoToken 在这里扮演的是「模型侧统一入口」:你不需要为每个模型单独维护一套鉴权和地址,把 Base URL 指向https://taotoken.net/api,Key 用同一个,模型 ID 按需切换即可。对 MCP 场景来说,这意味着 Agent 侧的工具 schema 发给哪个模型、用哪个 Key,都是配置项而不是代码改动。
2.1 拿到 Key 与确认 Base URL
登录后在控制台创建 API Key,复制出来先存到环境变量里,别硬编码进代码:
export TAOTOKEN_API_KEY="sk-你的key" export TAOTOKEN_BASE_URL="https://taotoken.net/api"Base URL 用https://taotoken.net/api,不要带任何多余路径。模型 ID 按你实际要用的填,比如claude-sonnet-4-20250514或gpt-4o,具体以控制台模型列表为准。
2.2 为什么 MCP 场景要统一 Key
MCP 的链路是「Host → Client → Server → 执行」,模型调用发生在 Host 侧。如果你有多个 Agent、多个 MCP Server,每个都配一套 Key 会很快失控。统一走 TaoToken 之后,Key 轮换只改一处,模型切换只改一个 Model ID,排障时也能确定「模型侧没问题」再往协议层查。
注意:Key 只放在环境变量或密钥管理里,不要写进
settings.json、auth.json这类会进版本库的文件。MCP Server 的配置里如果需要引用,用环境变量占位。
2.3 模型侧与协议侧的边界
再强调一次边界:TaoToken 管的是「模型怎么被调用」,MCP 管的是「工具怎么被描述和执行」。两者通过 Agent 的 Function Calling 逻辑衔接——Agent 从 MCP Server 拿到tools/list,转成模型请求里的tools字段,模型返回tool_calls,Agent 再转成tools/call发给 Server。这条链路里,TaoToken 只在「发模型请求」这一步出现。
3. 可复制配置:MCP Server 与客户端接入
这一节给出可直接复制的配置片段。分两部分:MCP Server 的启动配置,以及客户端(Host)如何声明这个 server。路径和字段名保持和实际一致,你改的时候只改路径和命令。
3.1 MCP Server 的 stdio 启动配置
假设你的 server 入口是mcp-server/dist/index.js,用 Node 启动。客户端侧的配置通常长这样(以通用 MCP 客户端配置为例):
{ "mcpServers": { "refund-server": { "command": "node", "args": ["mcp-server/dist/index.js"], "env": { "TAOTOKEN_API_KEY": "${TAOTOKEN_API_KEY}", "TAOTOKEN_BASE_URL": "https://taotoken.net/api" } } } }三个关键字段:command是可执行程序,args是参数数组,env是注入给子进程的环境变量。env里用${VAR}占位,避免把 Key 写死。
3.2 Server 端工具注册片段
Server 端注册工具时,name、description、inputSchema三件套缺一不可。description是模型选工具的全部依据,要写清楚「什么时候该调、什么时候不该调」:
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [{ name: "createRefund", description: "为用户发起退款申请。仅在用户已确认商品存在严重质量问题时调用,调用后款项按原路径退回", inputSchema: { type: "object", properties: { itemName: { type: "string", description: "商品名称或描述,用户没说清楚时填未知商品" }, reason: { type: "string", description: "质量问题的具体描述,例如袖口开线" } }, required: ["itemName", "reason"] } }] }));3.3 客户端接入的三件套
无论你用哪种客户端,接入一个 MCP Server 都要写全三件套:Base URL + Key + Model ID。Base URL 指向https://taotoken.net/api,Key 用TAOTOKEN_API_KEY,Model ID 按控制台填。这三项在 Agent 侧配置一次,MCP Server 侧只负责工具逻辑,不碰模型鉴权。
如果你用的是 Claude Code 这类工具,配置通常落在settings.json;如果是 Codex 系,可能在auth.json。不管文件名是什么,字段语义一致:地址、密钥、模型。三件套写全,缺一个就会在请求阶段报鉴权或模型不存在。
4. 验证请求:跑通一次完整的工具调用
配置写完,必须验证。验证分两步:先确认 server 本身能正确响应tools/list,再确认tools/call能真正执行并返回结果。
4.1 手动调协议验证 server
最快的方式是脱离框架,直接用管道喂 JSON 给 server:
printf '%s\n' \ '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"probe","version":"0.1.0"}}}' \ '{"jsonrpc":"2.0","method":"notifications/initialized"}' \ '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' \ | node mcp-server/dist/index.js预期看到initialize的响应里带serverInfo和capabilities,tools/list的响应里带tools数组。如果这一步就失败,问题在 server 本身,不用往客户端查。
4.2 验证 tools/call
确认tools/list正常后,再发一次调用:
printf '%s\n' \ '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"probe","version":"0.1.0"}}}' \ '{"jsonrpc":"2.0","method":"notifications/initialized"}' \ '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"createRefund","arguments":{"itemName":"连衣裙","reason":"袖口开线"}}}' \ | node mcp-server/dist/index.js预期返回content数组,里面是type: "text"的结果文本。同时 stderr 里应该能看到 server 打的执行日志——注意是 stderr,不是 stdout。
4.3 端到端验证
server 单独验证通过后,再走 Agent 端到端。Agent 侧会先发tools/list拿 schema,转成模型请求的tools字段,模型返回tool_calls,Agent 再发tools/call。整条链路跑通后,你会在日志里看到:模型请求 → tool_calls → tools/call → content → 第二次模型请求 → 最终话术。
5. 常见报错排查:401、local proxy failed、reading choices
这一节对照真实报错逐个排查。MCP 链路的报错可能出现在三个位置:模型侧、协议层、server 侧,定位顺序是先模型后协议再 server。
5.1 401 Unauthorized
模型请求返回 401,说明 Key 或 Base URL 有问题。检查三件套:Base URL 是不是https://taotoken.net/api,Key 是不是环境变量里那个,Model ID 是不是控制台里存在的。常见错误是把 Base URL 写成了带路径的地址,或者 Key 复制时带了空格。
5.2 local proxy failed
这个报错通常出现在客户端尝试连接 MCP Server 时。检查command和args路径是否正确,node是否在 PATH 里,dist/index.js是否真的存在。如果 server 启动就崩,客户端会报连接失败。先用 4.1 的手动管道验证 server 能独立跑起来。
5.3 reading choices 相关报错
这类报错一般出现在解析模型响应时,说明响应结构不符合预期。可能是模型返回了非标准格式,或者请求里的tools字段格式不对。检查 Agent 侧把tools/list的 schema 转成模型请求时,字段名和结构是否和模型 API 要求一致。
5.4 OAuth 相关报错
如果客户端报 OAuth 错误,说明它尝试走 OAuth 流程而不是 API Key。检查配置里是不是误填了 OAuth 相关字段,或者客户端默认走了 OAuth 模式。MCP 场景下模型侧用 API Key 即可,不需要 OAuth。
5.5 工具静默消失
最隐蔽的一类问题:server 挂了,但客户端只打了一条 warn,工具无声消失,模型变成「只说不做」。表现和「提示词没写要调用工具」一模一样,但根因完全不同。解决办法是显式设置failIfOneServerFails(true),宁可启动失败,不要静默降级。
6. 接入文档与后续步骤
排障和接入相关的细节,建议对照官方文档逐项核对。API Key 在控制台创建,接入文档里有完整的字段说明和示例。
模型侧的验证可以用模型对话页面直接试,确认 Key 和 Model ID 没问题再往 Agent 里接。如果你要长期跑编码类 Agent,Coding Plan 更适合持续调用场景。
- API Key 创建:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=api_keys
- 接入文档:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=doc
- 模型对话验证:https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=chat
- Coding Plan:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=coding_plan
最后留一个判断标准:这个工具需要被第二个宿主消费吗?需要就用 MCP,不需要就用进程内的 Function Calling。单进程内用 MCP 替代本地工具,是它最没有价值的用法——你付出了额外的进程、协议、故障模式,换回来的收益是零,因为收益全在「跨宿主」这个维度上。