Skip to content

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基础 ​

javascript
// 请求(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": { ... }
}

三条容易踩的约束:

  1. 请求 id 必须是字符串或整数,不得为 null(比基础 JSON-RPC 更严)
  2. 同一个发送方,id 不得与自己已发出但未收到响应的请求重复
  3. 错误的 code 必须是整数——不能写 "INVALID_PARAMS" 这种字符串

🔄 MCP消息类型 ​

📋 三种核心消息类型 ​

1. 🔍 请求消息(Request) ​

只能由客户端发往服务器,用于发起操作:

javascript
{
    "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) ​

javascript
// 成功响应 —— 注意 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,接收方不得回复:

javascript
// 进度通知(走它所属请求的响应流)
{
    "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标准方法 ​

📖 方法分类总览 ​

🔍 发现服务器能力 ​

无状态模型的入口。服务器必须实现:

javascript
// 请求
{
    "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 - 获取工具列表 ​

javascript
// 请求
{
    "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 - 调用工具 ​

javascript
// 请求
{
    "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 标识,供客户端读取上下文数据。

javascript
// 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"
    }
}
javascript
// 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 开一条长流:

javascript
// 开订阅流
{
    "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 ~ -32099MCP 规范保留实现不得发出未定义的码

常用错误码:

错误码名称含义示例场景
-32700Parse errorJSON 解析错误无效的 JSON 格式
-32600Invalid request无效请求请求体不符合 JSON-RPC
-32601Method not found方法不存在调用了不支持的方法
-32602Invalid params参数无效参数类型错误、请求缺少必需 _meta 字段、资源不存在
-32603Internal error服务器内部错误未捕获异常
-32020HeaderMismatch头部与请求体元数据不一致Streamable HTTP 的 Mcp-* 头与 body 冲突
-32021MissingRequiredClientCapability缺少必需客户端能力data.requiredCapabilities 列出缺失项
-32022UnsupportedProtocolVersion协议版本不受支持data.supported 列出支持的版本

旧版遗留的 -32002(资源不存在)客户端仍应接受——因为对方可能还是旧服务器。

🛠️ 错误处理示例 ​

协议层错误(服务器直接拒绝):

javascript
{
    "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"
                }
            ]
        }
    }
}

版本不匹配:

javascript
{
    "jsonrpc": "2.0",
    "id": 1,
    "error": {
        "code": -32022,
        "message": "Unsupported protocol version",
        "data": {
            "supported": ["2026-07-28", "2025-11-25"],
            "requested": "2025-03-26"
        }
    }
}

业务错误(工具执行失败但协议正常):

javascript
{
    "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 传输 ​

本地进程通信的唯一推荐方式。客户端拉起服务器子进程,通过标准流对话。

javascript
// 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 端点:

javascript
// 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. 请求生命周期管理 ​

javascript
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. 消息校验 ​

javascript
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 的完整实现模式
  • ✅ 最佳实践:请求生命周期管理、每请求元数据构造、消息校验

🤔 协议思考 ​

  1. 💭 无状态设计下,如果你有一个需要 3 次调用才能完成的业务流程,该怎么组织状态?
  2. 🔍 为什么规范要求 tools/list 返回确定顺序?这对 LLM 的性能有什么实际影响?
  3. 🛠️ 在 Streamable HTTP 上,什么情况下服务器该返回 JSON、什么情况下该返回 SSE 流?
  4. 📋 如果你要兼容一个只会 initialize 的老客户端,服务器端最小改动是什么?

理解了通信协议的细节,想知道数据在整个 MCP 系统中是如何流转的吗?

👉 下一小节:2.4 数据流分析

内容依据 MCP 官方规范整理 · 规范以 官方文档 为准