MCP 协议拆解:从 JSON-RPC 信封到 Agent 全链路
这篇文章是 Agent 工程化学习(Phase 04)的阶段总结。目标是把 MCP 协议从"能用 SDK 跑起来"提升到"看得懂每条消息在干什么、每层抽象在解决什么问题"。内容来自实际的课程练习和代码编写过程,不是规范翻译。
适合已经能跑通一个 MCP Server 但还不清楚内部运作原理的读者。
MCP 解决了什么问题
AI Agent 要调用外部能力(读文件、查数据库、发请求),每个 Host 应用各自实现一套工具接口,同一个工具在不同应用里要写两遍------类似 USB 标准出现之前,每个外设都配自己的专用接口。
MCP(Model Context Protocol)就是这个统一接口标准。任何 Host(CatDesk、Cursor、Claude Desktop)都能连接任何 MCP Server,一个 Server 写一次、到处能用。
它的定位和 HTTP 类似:HTTP 统一了浏览器和服务器之间的通信格式,MCP 统一了 AI 应用和工具提供者之间的通信格式。
三层架构:Host、Client、Server
MCP 把参与方分成三层,每层只管自己的事:
Host 是用户面对的 AI 应用。它管 UI、管对话历史、管 LLM 调用,内部包含一个或多个 Client 实例。
Client 是协议适配层。一个 Client 对应一个 Server 连接,负责三件事:管连接生命周期、翻译工具格式(MCP Schema ↔ AI SDK Schema)、路由调用请求到正确的 Server。
Server 是能力提供者。它作为独立进程运行,通过传输层和 Client 通信,暴露 Tools / Resources / Prompts 三类能力。
为什么要把 Client 单独抽出来?关注点分离。Host 只关心"我有哪些工具可以用",Server 只关心"收到请求怎么执行",中间的连接管理、格式转换、传输层适配全部封装在 Client 里。这样换 LLM 框架只改 Client 的翻译逻辑,换传输方式只改 Client 的底层实现,上下两层都不用动。
类比浏览器:你写前端代码只管发 fetch 和收数据,TCP 连接池、TLS 握手、HTTP/2 多路复用都是浏览器网络栈(Client 层)帮你做的。
底层通信格式:JSON-RPC 2.0
MCP 所有消息(无论走 stdio 还是 HTTP)都用 JSON-RPC 2.0 格式封装。这是"血管系统"------理解它就理解了消息在网络上到底长什么样。
JSON-RPC 只有三种信封:
Request (请求)------有 id,期望对方回一个 Response:
json
{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "read_file", "arguments": { "path": "config.json" } } }
Response (响应)------id 和 Request 配对,说明是回复哪个请求:
json
{ "jsonrpc": "2.0", "id": 1, "result": { "content": [{ "type": "text", "text": "文件内容..." }] } }
Notification (通知)------没有 id,不期望回复:
json
{ "jsonrpc": "2.0", "method": "notifications/initialized" }
三个关键认知:
id 是异步配对机制。Client 同时发了 3 个请求(id=1,2,3),Server 可以乱序返回,Client 靠 id 匹配"这条回复对应我哪个请求"。Notification 没有 id,因为没人需要回复它。
错误码是标准化的。-32601 Method not found、-32602 Invalid params 是 JSON-RPC 规范定义的,Server 返回错误时应该用这些码,不能随便编数字。
SDK 帮你做了所有序列化/反序列化。生产环境不需要手动 parse,但理解信封格式能帮你在 Inspector 里看懂消息流、在出问题时知道去哪里排查。
三阶段生命周期
一个 MCP 连接从建立到关闭经历三个阶段:
Phase 1 的核心是能力协商 。Client 告诉 Server "我支持 roots 和 sampling",Server 告诉 Client "我有 tools 和 resources"。后续操作不能超出对方声明的能力------比如 Client 没声明 sampling,Server 就不能调 sampling/createMessage。
initialized 为什么是 Notification 而不是 Request?因为 Server 不需要确认,Client 只是单向通知"我准备好了,可以进入工作阶段"。
Phase 3 没有结构化的 shutdown 方法。关闭由传输层信号完成------stdio 场景下关闭 stdin/stdout,HTTP 场景下 DELETE session。
六大原语
MCP 定义了六种能力原语,分成两组:
Server 暴露给 Client 的(3 个):
- Tools:Agent 主动调用的操作,可能有副作用(写文件、发请求)。LLM 在推理过程中自主决定是否调用。
- Resources :只读数据,用 URI 寻址(如
file://workspace/info)。通常在对话开始前预加载给 LLM 当背景知识,也可以在推理过程中按需读取。 - Prompts:预定义的提示词模板。Host 展示给用户选择,用户触发后注入对话。
Client 暴露给 Server 的(3 个):
- Roots:告知 Server 可以访问哪些工作区路径。
- Sampling:Server 请求 Client 侧的 LLM 生成内容(反向调用)。
- Elicitation:Server 向用户提问获取输入。
日常开发最常接触的是前三个。其中 Tool 是核心------Agent 的"动手能力"全靠 Tool 实现。
Resource 和 Tool 的区别
容易混淆的点:Resource 的 URI(如 file://workspace/info)看起来像文件路径,但它是 Server 自己定义的虚拟地址。当 Client 调 readResource("file://workspace/info") 时,Server 不是去磁盘找文件,而是执行注册时绑定的 async 函数------可能是读目录拼 JSON、查数据库、调 API,什么都行。
Tool 和 Resource 的本质区别在于谁触发 和有没有副作用:
- Resource:Host 在对话开始前预加载,给 LLM 提供背景知识。无副作用,类似 GET 请求。
- Tool:LLM 在推理过程中自主决定调用。可能有副作用(写文件、删记录),类似 POST 请求。
Tool Schema 设计原则
Tool 的 Schema 决定了 LLM 能不能在正确的时机选对正确的工具。四条规则:
命名用 snake_case + 动宾结构。 read_file、list_directory、search_files------LLM 一看名字就知道这个工具干什么。不用 fileManager、handleData 这种模糊的命名。
描述用两句话公式。 第一句说使用场景,第二句划定边界:
perl
Use when you need to read the contents of a file at a given path.
Do not use for directories or binary files.
第二句"不要用来做什么"至关重要------LLM 经常在相似工具之间犹豫,边界描述帮它排除错误选项。
粒度选 Atomic 而非 Monolithic。 read_file + write_file 优于 file_manager(action: "read"|"write"|"delete")。原因是 LLM 更容易选对精确工具,而不是先选到一个大工具再决定传什么 action 参数。
inputSchema 每个字段都要有 description。 LLM 需要知道每个参数是什么、什么格式、有什么约束。用 required 明确必选参数,用 enum 约束取值范围。
完整调用链路:一条消息的旅程
把所有层串起来看一次完整的调用:
几个容易误解的点:
LLM 不是"建议你用工具",而是直接输出调用指令。整个过程是自动化的 Agent Loop,用户无感。
Host 拿到工具结果后不是直接展示给用户,而是塞回 LLM。让 LLM 基于结果组织回答------它可能总结、可能继续调另一个工具,直到觉得任务完成才输出文本。
一次对话可能循环多轮。用户说"帮我找所有 .ts 文件然后读 package.json",LLM 会先调 search_files,看到结果后再调 read_file,两轮工具调用完成后给出最终回答。
如何观测每一层
调试 MCP 应用时,不同层需要不同工具:
| 想看的层 | 观测方式 | 能看到什么 |
|---|---|---|
| LLM 决策(Host 层) | AI SDK 的 onStepFinish 回调 |
每轮选了什么工具、传了什么参数、finishReason 是 tool-calls 还是 stop |
| Client ↔ Server 通信 | Client 的 verbose 模式或 JSON-RPC 追踪 |
工具调用耗时、返回内容预览、错误信息 |
| JSON-RPC 原始信封 | traceJsonRpc 模式或 MCP Inspector |
每条消息的完整 JSON:id、method、params、result |
MCP Inspector 是官方提供的图形化调试工具,用法:
bash
npx @modelcontextprotocol/inspector npx tsx src/04-tools-mcp/03-file-server/index.ts .
它让你手动充当 Client 角色:点 tools/list 看注册了什么工具,点 tools/call 填参数测试执行。Inspector 能看到 Client↔Server 这段的所有 JSON-RPC 消息,但看不到 LLM 决策------那部分需要在 Host 代码里加 onStepFinish 回调。
写一个 MCP Server 的标准流程
五步闭环:
1. 声明身份------创建 McpServer 实例,给个名字和版本号。
2. 注册能力 ------用 registerTool / registerResource / registerPrompt 把你的能力挂上去。每个 Tool 定义 name、description、inputSchema、handler。
3. 连接传输层 ------new StdioServerTransport() + server.connect(transport)。SDK 自动处理 initialize 握手。
4. Inspector 调试------不需要写 Client 代码就能验证 Server 是否正确。手动测试每个 Tool 的输入输出。
5. 接入 Host------把 Server 配置到 CatDesk / Cursor 的 MCP 配置中,Agent 就能用了。
生产环境中,你写的代码 90% 都在做两件事:定义 Schema(告诉 LLM 我能干什么)和实现 Handler(真正干活的逻辑)。握手、路由、序列化全交给 SDK。
收束
MCP 的核心设计哲学:Server 声明自己能做什么(Schema),Client 负责连接和翻译,Host 让 LLM 自主决定调什么。每层只管自己的事,靠 JSON-RPC 信封在中间传递。
理解到这个层次后,后续的 Inspector 实操(Day 13)和自定义 Server 开发就是在这个骨架上填肉了。