2.3 通信协议
🎯 学习目标:掌握 MCP 的消息格式、标准方法、错误处理机制,并能动手实现两种标准传输
⏱️ 预计时间:45分钟
📊 难度等级:⭐⭐
📌 本节定位:上一节 1.5 协议规范 讲的是规范的结构;本节讲怎么用——消息长什么样、方法怎么调、代码怎么写。内容对齐官方 2026-07-28 版本。
📡 MCP通信协议概述
MCP 基于 JSON-RPC 2.0,定义了客户端与服务器之间的"语言规则"。
🎪 用一次通话来理解
MCP 的一次交互,像打一通客服电话——但和旧版不同的是:每通电话都是独立的,客服不记得你上次说了什么。
📞 MCP 的一次请求
──────────────────────────────────────────────
📋 身份声明(_meta) - 我是谁、我说哪个版本、我支持什么
🗣️ 说话格式(JSON-RPC) - 统一的表达方式
🔄 对话规则(消息模式) - 谁先说、怎么回应、怎么中断
✅ 结果标记(resultType)- 这次是说完话了,还是要你补充信息
📝 通话记录(日志追踪) - 记录所有交流这个"每通电话互相独立"的设计就是无状态。它换来了一个很大的好处:服务器可以水平扩展——任何一台机器都能处理任何一条请求,不需要粘性会话。
🔧 协议基础架构
📊 协议栈层次
各层职责:
| 层 | 职责 | 关键点 |
|---|---|---|
| 🔝 应用层 | MCP 业务逻辑 | 由你实现:工具、资源、提示词 |
| 🔀 消息模式层 | 定义交互形状 | 请求-响应、MRTR、订阅通知,与传输无关 |
| 📨 消息层 | JSON-RPC 2.0 编解码 | 请求/结果/错误/通知四种消息 |
| 🚀 传输层 | 分帧与投递 | 只定义"消息怎么送",不定义"消息什么意思" |
| 🌐 底层信道 | 字节流 | stdio 进程流、HTTP 连接 |
💡 一个关键认知:传输是绑定(binding),不是语义。同一条
tools/call在 stdio 和 Streamable HTTP 上意思完全一样,只是送货方式不同。
🎯 JSON-RPC 2.0基础
// 请求(Request)—— 必须有 id,不得为 null
{
"jsonrpc": "2.0",
"id": "req-001",
"method": "tools/call",
"params": { ... }
}
// 结果响应(Result Response)—— result 里必须有 resultType
{
"jsonrpc": "2.0",
"id": "req-001",
"result": {
"resultType": "complete",
// ... 方法特定字段
}
}
// 错误响应(Error Response)—— code 必须是整数
{
"jsonrpc": "2.0",
"id": "req-001",
"error": {
"code": -32602,
"message": "Invalid params"
}
}
// 通知(Notification)—— 不得有 id
{
"jsonrpc": "2.0",
"method": "notifications/progress",
"params": { ... }
}三条容易踩的约束:
- 请求
id必须是字符串或整数,不得为null(比基础 JSON-RPC 更严) - 同一个发送方,
id不得与自己已发出但未收到响应的请求重复 - 错误的
code必须是整数——不能写"INVALID_PARAMS"这种字符串
🔄 MCP消息类型
📋 三种核心消息类型
1. 🔍 请求消息(Request)
只能由客户端发往服务器,用于发起操作:
{
"jsonrpc": "2.0",
"id": "calc-001",
"method": "tools/call",
"params": {
"name": "calculator",
"arguments": { "operation": "add", "a": 123, "b": 456 },
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": { "name": "my-client", "version": "1.0.0" },
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}⚠️ 和旧版最大的差别:这条请求里没有"先握手"这一步。协议版本、客户端身份、客户端能力全都塞在
params._meta里,服务器拿到就能处理,无需任何连接上下文。
2. ✅ 响应消息(Response)
// 成功响应 —— 注意 resultType
{
"jsonrpc": "2.0",
"id": "calc-001",
"result": {
"resultType": "complete",
"content": [
{ "type": "text", "text": "579" }
],
"isError": false,
"_meta": {
"io.modelcontextprotocol/serverInfo": { "name": "CalculatorServer", "version": "1.0.0" }
}
}
}
// 错误响应 —— code 是整数
{
"jsonrpc": "2.0",
"id": "calc-001",
"error": {
"code": -32602,
"message": "Invalid params: 不支持的运算操作 modulo",
"data": { "operation": "modulo" }
}
}区分两种"失败"很重要:
- 协议层失败 → 用
error字段 + 整数错误码- 工具业务失败(比如"除数不能为零")→ 用成功响应 +
isError: true,让 LLM 看到错误内容并自行调整
3. 📢 通知消息(Notification)
单向消息,不得有 id,接收方不得回复:
// 进度通知(走它所属请求的响应流)
{
"jsonrpc": "2.0",
"method": "notifications/progress",
"params": {
"progressToken": "tok-1",
"progress": 50,
"total": 100,
"message": "正在分析数据..."
}
}
// 列表变更通知(走 subscriptions/listen 流)
{
"jsonrpc": "2.0",
"method": "notifications/tools/list_changed",
"params": {
"_meta": { "io.modelcontextprotocol/subscriptionId": 1 }
}
}🚀 MCP标准方法
📖 方法分类总览
🔍 发现服务器能力
无状态模型的入口。服务器必须实现:
// 请求
{
"jsonrpc": "2.0",
"id": "discover-1",
"method": "server/discover",
"params": {
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}
// 响应
{
"jsonrpc": "2.0",
"id": "discover-1",
"result": {
"resultType": "complete",
"supportedVersions": ["2026-07-28"],
"capabilities": {
"tools": { "listChanged": true },
"resources": { "listChanged": true }
},
"instructions": "提供数学计算与天气查询工具",
"ttlMs": 3600000,
"cacheScope": "public"
}
}一次请求就能知道:服务器支持哪些协议版本、有哪些能力、叫什么名字。比分别探测 tools/list、prompts/list、resources/list 省事得多。
🛠️ 工具相关方法
tools/list - 获取工具列表
// 请求
{
"jsonrpc": "2.0",
"id": "tools-list-001",
"method": "tools/list",
"params": {
"cursor": null,
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}
// 响应
{
"jsonrpc": "2.0",
"id": "tools-list-001",
"result": {
"resultType": "complete",
"tools": [
{
"name": "calculator",
"title": "基础计算器",
"description": "执行加减乘除四则运算",
"inputSchema": {
"type": "object",
"properties": {
"operation": { "type": "string", "enum": ["add", "subtract", "multiply", "divide"] },
"a": { "type": "number" },
"b": { "type": "number" }
},
"required": ["operation", "a", "b"]
}
}
],
"ttlMs": 300000,
"cacheScope": "public"
}
}📌 两个新字段别漏:
ttlMs告诉客户端这份列表能缓存多久(毫秒),cacheScope决定共享中间层能否缓存("public"/"private")。规范要求列表类与读取类结果都实现CacheableResult。另外服务器应该以确定顺序返回工具——顺序稳定了,客户端才能可靠缓存,LLM 的提示缓存命中率也更高。
tools/call - 调用工具
// 请求
{
"jsonrpc": "2.0",
"id": "calc-001",
"method": "tools/call",
"params": {
"name": "calculator",
"arguments": { "operation": "add", "a": 123, "b": 456 },
"_meta": { "io.modelcontextprotocol/protocolVersion": "2026-07-28", "io.modelcontextprotocol/clientCapabilities": {} }
}
}
// 成功
{
"jsonrpc": "2.0",
"id": "calc-001",
"result": {
"resultType": "complete",
"content": [{ "type": "text", "text": "计算结果:123 + 456 = 579" }],
"isError": false
}
}
// 工具业务失败(协议层仍是成功响应)
{
"jsonrpc": "2.0",
"id": "calc-001",
"result": {
"resultType": "complete",
"content": [{ "type": "text", "text": "错误:除数不能为零" }],
"isError": true
}
}📚 资源相关方法
资源用 URI 标识,供客户端读取上下文数据。
// resources/list 请求
{
"jsonrpc": "2.0",
"id": "res-list-001",
"method": "resources/list",
"params": { "_meta": { "io.modelcontextprotocol/protocolVersion": "2026-07-28", "io.modelcontextprotocol/clientCapabilities": {} } }
}
// 响应
{
"jsonrpc": "2.0",
"id": "res-list-001",
"result": {
"resultType": "complete",
"resources": [
{
"uri": "file:///home/user/documents/report.txt",
"name": "报告文件",
"description": "项目进度报告",
"mimeType": "text/plain"
}
],
"ttlMs": 60000,
"cacheScope": "private"
}
}// resources/read 请求
{
"jsonrpc": "2.0",
"id": "read-001",
"method": "resources/read",
"params": {
"uri": "file:///home/user/documents/report.txt",
"_meta": { "io.modelcontextprotocol/protocolVersion": "2026-07-28", "io.modelcontextprotocol/clientCapabilities": {} }
}
}
// 响应
{
"jsonrpc": "2.0",
"id": "read-001",
"result": {
"resultType": "complete",
"contents": [
{
"uri": "file:///home/user/documents/report.txt",
"mimeType": "text/plain",
"text": "项目进度报告\n\n本月完成情况:\n1. 需求分析 - 100%\n2. 设计阶段 - 80%"
}
],
"ttlMs": 60000,
"cacheScope": "private"
}
}⚠️ 资源不存在时的错误码是
-32602(Invalid Params),不再是旧版的-32002。
📡 订阅资源变更
旧版的 resources/subscribe / resources/unsubscribe 已删除,改为 subscriptions/listen 开一条长流:
// 开订阅流
{
"jsonrpc": "2.0",
"id": 1,
"method": "subscriptions/listen",
"params": {
"notifications": {
"toolsListChanged": true,
"resourceSubscriptions": ["file:///home/user/documents/report.txt"]
},
"_meta": { "io.modelcontextprotocol/protocolVersion": "2026-07-28", "io.modelcontextprotocol/clientCapabilities": {} }
}
}
// 服务器必须先回确认(流的第一条消息)
{
"jsonrpc": "2.0",
"method": "notifications/subscriptions/acknowledged",
"params": {
"_meta": { "io.modelcontextprotocol/subscriptionId": 1 },
"notifications": {
"toolsListChanged": true,
"resourceSubscriptions": ["file:///home/user/documents/report.txt"]
}
}
}
// 之后是变更通知
{
"jsonrpc": "2.0",
"method": "notifications/resources/updated",
"params": {
"_meta": { "io.modelcontextprotocol/subscriptionId": 1 },
"uri": "file:///home/user/documents/report.txt"
}
}🔒 错误处理机制
📋 错误码分区
MCP 把 JSON-RPC 的 -32000 ~ -32099 分成了两段:
| 区间 | 归属 | 处理方式 |
|---|---|---|
-32000 ~ -32019 | 历史遗留 | 不再新增;除 -32002 外不得赋予任何特定含义 |
-32020 ~ -32099 | MCP 规范保留 | 实现不得发出未定义的码 |
常用错误码:
| 错误码 | 名称 | 含义 | 示例场景 |
|---|---|---|---|
-32700 | Parse error | JSON 解析错误 | 无效的 JSON 格式 |
-32600 | Invalid request | 无效请求 | 请求体不符合 JSON-RPC |
-32601 | Method not found | 方法不存在 | 调用了不支持的方法 |
-32602 | Invalid params | 参数无效 | 参数类型错误、请求缺少必需 _meta 字段、资源不存在 |
-32603 | Internal error | 服务器内部错误 | 未捕获异常 |
-32020 | HeaderMismatch | 头部与请求体元数据不一致 | Streamable HTTP 的 Mcp-* 头与 body 冲突 |
-32021 | MissingRequiredClientCapability | 缺少必需客户端能力 | data.requiredCapabilities 列出缺失项 |
-32022 | UnsupportedProtocolVersion | 协议版本不受支持 | data.supported 列出支持的版本 |
旧版遗留的
-32002(资源不存在)客户端仍应接受——因为对方可能还是旧服务器。
🛠️ 错误处理示例
协议层错误(服务器直接拒绝):
{
"jsonrpc": "2.0",
"id": "req-001",
"error": {
"code": -32602,
"message": "Invalid params: 工具参数校验失败",
"data": {
"tool": "calculator",
"errors": [
{
"field": "operation",
"message": "必须是 add, subtract, multiply, divide 之一",
"received": "modulo"
}
]
}
}
}版本不匹配:
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32022,
"message": "Unsupported protocol version",
"data": {
"supported": ["2026-07-28", "2025-11-25"],
"requested": "2025-03-26"
}
}
}业务错误(工具执行失败但协议正常):
{
"jsonrpc": "2.0",
"id": "req-001",
"result": {
"resultType": "complete",
"content": [{ "type": "text", "text": "错误:目标文件不存在:/data/missing.csv" }],
"isError": true
}
}💡 怎么选? 如果错误是调用方修参数就能解决的 → 用
error+-32602;如果错误是工具运行时遇到的问题、且 LLM 应该看到的 → 用isError: true,把错误描述放进content。
🔄 传输层实现
🌐 两种标准传输
1. 📟 stdio 传输
本地进程通信的唯一推荐方式。客户端拉起服务器子进程,通过标准流对话。
// Node.js stdio 传输:换行分隔 + 缓冲处理
class StdioTransport {
constructor() {
this.messageBuffer = '';
}
send(message) {
// 必须 UTF-8、以换行结尾、内部不得含换行
process.stdout.write(JSON.stringify(message) + '\n');
}
onMessage(callback) {
process.stdin.setEncoding('utf8');
process.stdin.on('data', (chunk) => {
this.messageBuffer += chunk;
// 按换行切分;最后一段可能是半条消息,留到下次
const lines = this.messageBuffer.split('\n');
this.messageBuffer = lines.pop();
for (const line of lines) {
if (!line.trim()) continue;
try {
callback(JSON.parse(line));
} catch (error) {
// 注意:日志写 stderr,绝不能写 stdout
process.stderr.write(`消息解析失败: ${error.message}\n`);
}
}
});
}
}⚠️ 最容易犯的错误:往
stdout打日志。stdout是协议通道,混入任何非 JSON-RPC 的内容都会破坏分帧。日志一律走stderr。
2. 🌊 Streamable HTTP 传输
每条消息一次 HTTP POST,发往同一个 MCP 端点:
// Streamable HTTP 传输:注意必需的头部
class StreamableHttpTransport {
constructor(endpoint, protocolVersion) {
this.endpoint = endpoint;
this.protocolVersion = protocolVersion;
this.requestId = 1;
}
async request(method, params, name) {
const id = this.requestId++;
const message = {
jsonrpc: '2.0',
id,
method,
params: {
...params,
_meta: {
...(params?._meta || {}),
'io.modelcontextprotocol/protocolVersion': this.protocolVersion,
'io.modelcontextprotocol/clientInfo': { name: 'my-client', version: '1.0.0' },
'io.modelcontextprotocol/clientCapabilities': {}
}
}
};
const headers = {
'Content-Type': 'application/json',
// 接受 JSON 或 SSE 流
'Accept': 'application/json, text/event-stream',
// 版本镜像头:请求体才是真相来源,两者必须一致
'MCP-Protocol-Version': this.protocolVersion,
// 让中间层无需解析请求体即可路由与鉴权
'Mcp-Method': method
};
// 资源名 / 工具名 / 提示词名(适用时)
if (name) headers['Mcp-Name'] = name;
const response = await fetch(this.endpoint, {
method: 'POST',
headers,
body: JSON.stringify(message)
});
if (response.status === 202) return null; // 通知已被接受
if (!response.ok) {
const body = await response.json().catch(() => null);
throw new MCPError(body?.error?.message || `HTTP ${response.status}`, body?.error?.code);
}
const contentType = response.headers.get('content-type') || '';
if (contentType.includes('text/event-stream')) {
return this.consumeStream(response); // 请求作用域 SSE
}
return (await response.json()).result;
}
// SSE 流按事件逐条解析
async *consumeStream(response) {
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const events = buffer.split('\n\n');
buffer = events.pop();
for (const raw of events) {
for (const line of raw.split('\n')) {
if (line.startsWith('data:')) {
const payload = line.slice(5).trim();
if (payload) yield JSON.parse(payload);
}
}
}
}
}
}📌 取消请求怎么发? stdio 上发
notifications/cancelled;Streamable HTTP 上直接关闭该请求的响应流。📌 流断了怎么办? 2026-07-28 已移除 SSE 的可恢复性与消息重投(
Last-Event-ID没了)。流一断,在途请求就丢了——必须用新的请求 ID 重新发起,不能"续传"。
3. 🔌 自定义传输
如果 stdin/stdout 与 HTTP 都不合适(比如内网高性能 IPC),可以自定义。硬性要求:
- 必须保留 JSON-RPC 消息格式、消息模式、每请求元数据模型
- 跑在可靠双向字节流上时(Unix domain socket、TCP),应该直接复用 stdio 的换行分帧,不要另造一套
- 应该把自己的连接建立、分帧、取消方式写清楚
🎯 协议最佳实践
✅ 实现建议
1. 请求生命周期管理
class MCPConnection {
constructor(transport, protocolVersion = '2026-07-28') {
this.transport = transport;
this.protocolVersion = protocolVersion;
this.pendingRequests = new Map();
this.requestTimeout = 30000;
this.capabilities = {}; // 客户端自身能力,每请求都要带上
this.setupMessageHandler();
}
buildMeta(extra = {}) {
// 每个请求都自带元数据 —— 这是无状态模型的核心
return {
'io.modelcontextprotocol/protocolVersion': this.protocolVersion,
'io.modelcontextprotocol/clientInfo': { name: 'my-client', version: '1.0.0' },
'io.modelcontextprotocol/clientCapabilities': this.capabilities,
...extra
};
}
async request(method, params = {}) {
const id = this.generateRequestId();
const message = {
jsonrpc: '2.0',
id,
method,
params: { ...params, _meta: this.buildMeta(params._meta) }
};
return new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => {
this.pendingRequests.delete(id);
reject(new MCPError(`请求超时: ${method}`, -32603));
}, this.requestTimeout);
this.pendingRequests.set(id, { resolve, reject, timeoutId, method });
this.transport.send(message);
});
}
handleResponse(message) {
const { id, result, error } = message;
const pending = this.pendingRequests.get(id);
if (!pending) return;
clearTimeout(pending.timeoutId);
this.pendingRequests.delete(id);
if (error) {
pending.reject(new MCPError(error.message, error.code, error.data));
return;
}
// 兼容旧服务器:缺 resultType 时按 complete 处理
const resultType = result?.resultType ?? 'complete';
if (resultType === 'input_required') {
// MRTR:收集输入后带 inputResponses 重试原请求
pending.resolve(this.handleInputRequired(pending.method, result));
} else if (resultType === 'complete') {
pending.resolve(result);
} else {
pending.reject(new MCPError(`无法识别的 resultType: ${resultType}`, -32603));
}
}
async handleInputRequired(method, interimResult) {
const inputResponses = {};
for (const [key, req] of Object.entries(interimResult.inputRequests || {})) {
inputResponses[key] = await this.collectInput(req); // 由你的 UI 层实现
}
// 注意:重试是一个全新的请求(新的 id)
return this.request(method, { ...interimResult.requestState, inputResponses });
}
}2. 消息校验
class MessageValidator {
static validateRequest(message) {
const errors = [];
if (message.jsonrpc !== '2.0') {
errors.push('jsonrpc 字段必须是 "2.0"');
}
if (!message.method || typeof message.method !== 'string') {
errors.push('method 字段必须是非空字符串');
}
if (message.id === undefined) {
errors.push('请求必须包含 id');
}
if (message.id === null) {
errors.push('id 不得为 null');
}
if (message.id !== undefined &&
typeof message.id !== 'string' &&
typeof message.id !== 'number') {
errors.push('id 必须是字符串或数字');
}
// 无状态模型的必备校验
const meta = message.params?._meta;
if (!meta?.['io.modelcontextprotocol/protocolVersion']) {
errors.push('缺少 _meta.io.modelcontextprotocol/protocolVersion');
}
if (!meta?.['io.modelcontextprotocol/clientCapabilities']) {
errors.push('缺少 _meta.io.modelcontextprotocol/clientCapabilities');
}
return errors;
}
static validateResultResponse(message) {
const errors = [];
if (message.jsonrpc !== '2.0') {
errors.push('jsonrpc 字段必须是 "2.0"');
}
if (message.id === undefined) {
errors.push('响应必须包含 id');
}
if (message.result === undefined && message.error === undefined) {
errors.push('响应必须包含 result 或 error 字段');
}
if (message.result !== undefined && message.error !== undefined) {
errors.push('响应不能同时包含 result 和 error');
}
if (message.result !== undefined && typeof message.result.resultType !== 'string') {
errors.push('兼容提示:缺少 resultType,客户端应按 "complete" 处理');
}
if (message.error !== undefined && !Number.isInteger(message.error.code)) {
errors.push('error.code 必须是整数');
}
return errors;
}
}⚠️ 常见实现错误清单
| 错误做法 | 正确做法 |
|---|---|
往 stdout 打日志 | 日志写 stderr |
| 认为"连上就是会话",缓存连接级状态 | 服务器每请求独立处理;跨请求状态用显式句柄 |
收到响应不带 id 校验就直接 resolve | 用 id 匹配待响应请求;无 id 只能是通知 |
用字符串错误码("INVALID_PARAMS") | 用整数错误码(-32602) |
流断了用 Last-Event-ID 续传 | 用新的请求 ID 重新发起 |
工具业务失败时返回 error | 返回 isError: true 的成功响应 |
忘了回 resultType | 每个结果都必须带 |
🎯 本节小结
通过这一小节,你应该已经掌握了:
- ✅ 协议基础:JSON-RPC 2.0 的四种消息、四条硬约束
- ✅ 消息类型:请求、结果响应、错误响应、通知的正确写法
- ✅ 标准方法:
server/discover、工具、资源、提示词、订阅的调用方式 - ✅ 错误处理:
-32020~-32022规范码、历史遗留码的兼容策略、协议错误与业务错误的分工 - ✅ 传输层:stdio 与 Streamable HTTP 的完整实现模式
- ✅ 最佳实践:请求生命周期管理、每请求元数据构造、消息校验
🤔 协议思考
- 💭 无状态设计下,如果你有一个需要 3 次调用才能完成的业务流程,该怎么组织状态?
- 🔍 为什么规范要求
tools/list返回确定顺序?这对 LLM 的性能有什么实际影响? - 🛠️ 在 Streamable HTTP 上,什么情况下服务器该返回 JSON、什么情况下该返回 SSE 流?
- 📋 如果你要兼容一个只会
initialize的老客户端,服务器端最小改动是什么?
理解了通信协议的细节,想知道数据在整个 MCP 系统中是如何流转的吗?