MCP协议规范
摘要:随着大语言模型(LLM)生态向 Agent(智能体)与复杂工具调用(Tool Calling)深度演进,如何让 AI 客户端(如 Claude Desktop、Cursor、VS Code)与海量外部数据源、工具链无缝解耦对接,成为了行业的核心痛点。由 Anthropic 主导发起的MCP(Model Context Protocol,模型上下文协议)正在迅速成为大模型时代的“USB 接口协议”。
MCP 的底层通信机制完全建立在成熟、轻量级的JSON-RPC 2.0规范之上。本文将系统拆解 MCP 协议的整体架构、JSON-RPC 2.0 报文规范、传输层映射(stdio 与 SSE)、初始化握手生命周期、三大核心功能原语(Tools, Resources, Prompts)的请求与响应映射,并手把手带你使用纯 Python 从零手写一个符合规范的 MCP 服务端,最后总结生产落地的核心避坑指南。
一、 背景与愿景:为什么需要 MCP?
在 MCP 出现之前,大语言模型与外部工具/数据源的对接面临着经典的M×N 复杂度网状困境:
传统架构(M×N 复杂网状连接): [ Claude Desktop ] ─── (单独开发) ───> [ PostgreSQL ] [ Cursor IDE ] ─── (单独开发) ───> [ GitHub API ] [ Custom Agent ] ─── (单独开发) ───> [ Local Files ]每一个 AI 主控端(Host/Client)如果要接入一个新的数据源或工具(如 GitHub、PostgreSQL、本地文件系统),都需要为该数据源单独编写一套适配器;反之,工具开发者如果希望自己的服务被多种 AI 客户端支持,也必须为每一个 IDE 和聊天客户端适配 API。
MCP 协议引入了类似LSP(Language Server Protocol,语言服务协议)的解耦思想,通过标准化的通信接口将架构简化为1+N 标准拓扑:
MCP 架构(解耦后的星型标准拓扑): [ Client: Claude Desktop ] \ / [ Server: PostgreSQL ] [ Client: Cursor IDE ] ───> (MCP Protocol) <─── [ Server: GitHub API ] [ Client: Custom Agent ] / (JSON-RPC 2.0) \ [ Server: Local Files ]为什么选择 JSON-RPC 2.0 作为底层传输规范?
MCP 官方选用了JSON-RPC 2.0作为其消息序列化与调用的基石。其核心原因如下:
极为轻量且语言无关:JSON 是现代软件工程中使用最广泛的数据交换格式,任何编程语言都能零门槛解析。
规范定义严谨:JSON-RPC 2.0 明确区分了“双向请求-响应(Request-Response)”与“单向通知(Notification)”,天生具备处理复杂异步交互的能力。
传输层解耦:JSON-RPC 2.0 仅定义消息结构,不强绑定物理传输层。这使得 MCP 可以完美运行在本地进程间通信(stdio)以及远程网络传输(HTTP SSE)之上。
二、 MCP 架构总览与传输层(Transport Layer)
在深入 JSON-RPC 2.0 报文之前,我们首先需要搞清楚 MCP 的物理与逻辑架构。
2.1 角色定义
MCP Client(客户端/主控端):发起连接并管理 LLM 生命周期的应用(例如 Claude Desktop、Cursor、自定义 Agent 框架)。Client 负责将用户的意图转化为对 Server 的请求,并决定何时将 Server 返回的上下文喂给 LLM。
MCP Server(服务端/上下文提供者):独立的进程或远程服务,负责暴露具体的工具(Tools)、只读资源(Resources)或提示词模板(Prompts)。
LLM(大语言模型):位于 Client 后端的推理引擎。注意:LLM 并不直接与 MCP Server 通信,所有的交互均由 Client 中转和路由。
┌───────────────────────────────────────────────────────────┐ │ MCP Client │ │ │ │ ┌───────────────┐ 通信中转 ┌────────────────┐ │ │ │ LLM Engine │ <─────────────> │ MCP Protocol │ │ │ └───────────────┘ │ JSON-RPC 2.0 │ │ └─────────────────────────────────────└────────┬───────┘────┘ │ 物理传输通道 (stdio / SSE) │ ┌──────────────────────────────────────────────▼────────────┐ │ MCP Server │ │ │ │ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │ │ │ Tools (执行) │ │Resources(只读)│ │ Prompts(模板) │ │ │ └───────────────┘ └───────────────┘ └───────────────┘ │ └───────────────────────────────────────────────────────────┘2.2 两大传输通道实现规范
MCP 官方定义了两种标准的物理传输通道:
1. stdio(标准输入/输出通道)
用于本地进程间通信(IPC)。Client 以子进程(Subprocess)的形式启动 MCP Server 进程。
Client -> Server:Client 将 JSON-RPC 报文按行写入 Server 的
stdin(标准输入)。Server -> Client:Server 将 JSON-RPC 报文按行写入自身的
stdout(标准输出)。规范要求:
每条 JSON-RPC 报文必须是压缩为单行的 JSON 文本,并以换行符(
\n或\r\n)结尾。stderr(标准错误)被严格保留用于输出调试日志。Server 绝不能将任何 JSON-RPC 报文发送到 stderr,也决不能将普通文本日志打印到 stdout(否则会导致 Client 的 JSON 解析器崩溃)。
2. SSE(Server-Sent Events)+ HTTP POST(远程网络通道)
用于跨机器远程网络通信。
客户端订阅通道(SSE):Client 向 Server 发起 HTTP GET 请求建立 SSE 长连接。Server 通过该长连接向 Client 推送 JSON-RPC 响应或通知。
客户端发送通道(HTTP POST):Client 向 Server 指定的端点发送 HTTP POST 请求,消息体为 JSON-RPC 请求报文。
三、 核心通信规范:JSON-RPC 2.0 在 MCP 中的报文全解
JSON-RPC 2.0 是一个无状态、轻量级的远程过程调用(RPC)协议。在 MCP 中,所有收发的文本必须严格遵守以下四大基本消息结构。
3.1 请求对象(Request Object)
当 Client 或 Server 需要调用对方的方法并期待返回结果时发送。JSON
{ "jsonrpc": "2.0", "id": 1001, "method": "tools/call", "params": { "name": "calculate_tax", "arguments": { "income": 50000 } } }jsonrpc(字符串,必填):必须准确为"2.0"。method(字符串,必填):调用的 RPC 方法名称(如tools/call)。params(对象/数组,选填):方法所需的参数。在 MCP 规范中,params几乎总是键值对 JSON 对象。id(字符串/整数,必填):请求的唯一标识符。接收方在处理完该请求后,必须在对应的响应对象中原样返回该id。
3.2 成功响应对象(Response Object - Success)
当接收方成功处理请求后返回。
{ "jsonrpc": "2.0", "id": 1001, "result": { "content": [ { "type": "text", "text": "计算结果:预扣预缴税额为 4500 元。" } ], "isError": false } }jsonrpc(字符串,必填):必须为"2.0"。id(字符串/整数,必填):必须与对应 Request 中的id完全一致。result(任意 JSON 类型,必填):调用成功时返回的数据载荷。
3.3 错误响应对象(Response Object - Error)
当请求解析失败、方法不存在、参数校验失败或执行过程中抛出异常时返回。
{ "jsonrpc": "2.0", "id": 1001, "error": { "code": -32602, "message": "Invalid params: argument 'income' must be a positive number.", "data": { "field": "income", "expected": "number > 0" } } }jsonrpc(字符串,必填):必须为"2.0"。id(字符串/整数,必填):与 Request 中的id一致;若请求因 JSON 解析失败无法获取id,则必须返回null。error(对象,必填):包含以下字段:code(整数,必填):错误码。message(字符串,必填):简短的错误描述。data(任意类型,选填):包含错误的额外调试上下文。
JSON-RPC 2.0 与 MCP 错误码映射表
| 错误码(Code) | 错误类型 | 含义说明 |
-32700 | Parse Error | 服务端接收到的不是合法的 JSON 文本。 |
-32600 | Invalid Request | 发送的 JSON 不符合 JSON-RPC 2.0 请求对象结构。 |
-32601 | Method Not Found | 调用的 MCP 方法(如tools/call_wrong)不存在。 |
-32602 | Invalid Params | 方法的参数不符合 Schema 约束(例如缺失必填项)。 |
-32603 | Internal Error | MCP 服务端内部抛出了未捕获的运行时异常。 |
-32000到-32099 | Server Error | MCP 协议预留的服务端自定义业务错误区段。 |
3.4 通知对象(Notification Object)
单向发送的消息,不需要也不允许接收方做出任何响应。通常用于状态变更提醒、日志推送或取消操作。
{ "jsonrpc": "2.0", "method": "notifications/resources/updated", "params": { "uri": "file:///workspace/config.json" } }核心特征:绝对不包含
id字段!如果包含了id,接收方就会将其误判为普通 Request。
四、 MCP 生命周期与握手协商机制
任何一个符合规范的 MCP 会话,都必须经历严格的生命周期三阶段:初始化握手 ➔ 正常业务交互 ➔ 优雅关闭。
4.1 初始化握手时序图
在连接建立之初,Client 与 Server 必须通过握手确认彼此支持的协议版本与能力集(Capabilities)。
MCP Client MCP Server │ │ ├────────── 1. initialize Request (id: 1) ───────────►│ │ (clientInfo, capabilities) │ │ │ │◄───────── 2. initialize Response (id: 1) ───────────┤ │ (serverInfo, capabilities) │ │ │ ├─────── 3. notifications/initialized Notification ───►│ │ │ │ ==== 握手完成 ==== │ │ │ ├────────── 4. 正常业务请求 (tools/list 等) ───────────►│4.2 握手报文实战拆解
步骤 1:Client 发起initialize请求
{ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2024-11-05", "capabilities": { "roots": { "listChanged": true }, "sampling": {} }, "clientInfo": { "name": "Cursor-IDE", "version": "0.42.0" } } }protocolVersion:Client 支持的 MCP 协议版本号(当前最新主流标准为2024-11-05)。capabilities:告知 Server 本 Client 支持哪些高级能力(如根目录变动通知、Sampling 采样等)。
步骤 2:Server 回复initialize响应
{ "jsonrpc": "2.0", "id": 1, "result": { "protocolVersion": "2024-11-05", "capabilities": { "tools": { "listChanged": true }, "resources": { "subscribe": true, "listChanged": true }, "prompts": { "listChanged": false }, "logging": {} }, "serverInfo": { "name": "enterprise-db-mcp", "version": "1.2.0" } } }Server 在响应中明确宣告自己能提供哪些功能:
"tools":支持工具调用,且工具列表变更时会发出 notification。"resources":支持只读资源提取,且支持客户端订阅变更。
步骤 3:Client 发送notifications/initialized确认
{ "jsonrpc": "2.0", "method": "notifications/initialized" }收到此通知后, Server 方可正式开启业务请求处理。
五、 MCP 三大核心功能原语及其 JSON-RPC 映射
MCP 将 AI 交互的上下文能力抽象为三大核心原语:Tools(工具)、Resources(资源)与Prompts(提示词)。
下面我们逐一拆解它们的 JSON-RPC 报文映射。
5.1 Tools(工具原语):可执行的函数调用
Tools 允许 LLM 通过 MCP Server 执行具有副作用的操作(例如发起 API 请求、写入文件、执行 SQL 查询)。
1. 获取工具列表 (tools/list)
Client 请求:
{ "jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {} }Server 响应:
{ "jsonrpc": "2.0", "id": 2, "result": { "tools": [ { "name": "execute_sql", "description": "在只读副本上执行安全 SQL 查询", "inputSchema": { "type": "object", "properties": { "query": { "type": "string", "description": "标准 SQL 查询语句" }, "limit": { "type": "integer", "default": 100 } }, "required": ["query"] } } ] } }inputSchema必须严格遵循JSON Schema (Draft-07/2020-12)规范,LLM 将依据此 Schema 生成参数。
2. 调用工具 (tools/call)
当 LLM 决定调用该工具时,Client 发起以下请求:
Client 请求:
{ "jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": { "name": "execute_sql", "arguments": { "query": "SELECT id, name, email FROM users LIMIT 2;", "limit": 2 } } }Server 响应:
{ "jsonrpc": "2.0", "id": 3, "result": { "content": [ { "type": "text", "text": "[{\"id\": 1, \"name\": \"Alice\", \"email\": \"alice@example.com\"}, {\"id\": 2, \"name\": \"Bob\", \"email\": \"bob@example.com\"}]" } ], "isError": false } }content支持多模态,可包含text、image(base64 编码图片)或嵌入的resource对象。即使工具执行业务逻辑报错(例如 SQL 语法错误),通常也会设置
isError: true并将错误信息放在content中返回,而不是直接抛出 JSON-RPC 级别的error,这有助于 LLM 观察错误并进行自我纠错。
5.2 Resources(资源原语):只读上下文数据
Resources 类似于 HTTP 的GET接口,用于为 LLM 提供只读的数据源(如日志文件、数据库 Schema、API 挡板数据)。每一个资源由一个唯一的URI标识。
1. 读取具体资源 (resources/read)
Client 请求:
{ "jsonrpc": "2.0", "id": 4, "method": "resources/read", "params": { "uri": "postgres://main-db/schemas/public" } }Server 响应:
{ "jsonrpc": "2.0", "id": 4, "result": { "contents": [ { "uri": "postgres://main-db/schemas/public", "mimeType": "application/json", "text": "{\"tables\": [\"users\", \"orders\", \"products\"]}" } ] } }5.3 Prompts(提示词原语):预定义可复用模板
Prompts 允许 Server 暴露可复用的 Prompt 模版,方便用户在 Client 侧一键加载特定的专业角色或工作流。
1. 获取展开后的提示词 (prompts/get)
Client 请求:
{ "jsonrpc": "2.0", "id": 5, "method": "prompts/get", "params": { "name": "code_review", "arguments": { "language": "python" } } }Server 响应:
{ "jsonrpc": "2.0", "id": 5, "result": { "description": "Python 代码审查模板", "messages": [ { "role": "user", "content": { "type": "text", "text": "请作为资深 Python 专家,对提交的代码进行 Clean Code 审查,重点关注 PEP8 规范与异步性能问题。" } } ] } }六、 MCP 高级特性:双向通信与反向采样(Sampling)
传统的 RPC 通常是单向的“客户端请求,服务端响应”。然而基于 JSON-RPC 2.0,MCP 允许 Server 反向向 Client 发起请求!
其中最出彩的高级特性就是Sampling(采样原语)。
6.1 什么是 Sampling?
有时,MCP Server 在执行某个复杂任务时,自身需要借助于 LLM 的能力(比如把一段庞大的日志做一次预总结)。通过 Sampling,Server 可以向 Client 抛出sampling/createMessage请求,要求 Client 调用其连接的大模型进行一次嵌套推理,再将结果回复给 Server!
Server Client │ │ ├─────── 1. sampling/createMessage Request ──────────►│ │ (messages, maxTokens, systemPrompt) │ │ │ 2. Client 转发给 │ │ 外部 LLM 推理 │ │ │◄────── 3. sampling/createMessage Response ──────────┤ │ (model, role: assistant, content) │6.2 Sampling 报文示例
Server 发起反向请求:
{ "jsonrpc": "2.0", "id": "server-req-99", "method": "sampling/createMessage", "params": { "messages": [ { "role": "user", "content": { "type": "text", "text": "请提取以下日志中的报错堆栈摘要:\nERROR 2026-08-05 ..." } } ], "maxTokens": 200 } }这种机制极大提升了 Server 的智能化上限,使其无需硬编码额外的 LLM API Key,即可复用 Client 已有的模型推理通道。
七、 实战:零依赖手写 Python MCP Server
为了帮助你彻底掌握底层细节,下面我们不依赖任何现成的 MCP 高级 SDK,仅使用 Python 原生的sys.stdin、sys.stdout和json模块,手写一个完全符合 JSON-RPC 2.0 规范的本地 stdio MCP Server。
7.1 Python 代码实现(custom_mcp_server.py)
import sys import json import traceback def log_debug(msg: str): """注意:所有日志必须写入 stderr,绝对不能打到 stdout!""" sys.stderr.write(f"[MCP-SERVER-LOG] {msg}\n") sys.stderr.flush() def send_response(response_dict: dict): """将 JSON-RPC 响应打印至 stdout,并紧跟换行符刷新""" output = json.dumps(response_dict, ensure_ascii=False) sys.stdout.write(output + "\n") sys.stdout.flush() def handle_initialize(req_id, params): return { "jsonrpc": "2.0", "id": req_id, "result": { "protocolVersion": "2024-11-05", "capabilities": { "tools": {"listChanged": False} }, "serverInfo": { "name": "handcrafted-python-mcp", "version": "1.0.0" } } } def handle_tools_list(req_id): return { "jsonrpc": "2.0", "id": req_id, "result": { "tools": [ { "name": "add_numbers", "description": "计算两个数字的和", "inputSchema": { "type": "object", "properties": { "a": {"type": "number", "description": "第一个数字"}, "b": {"type": "number", "description": "第二个数字"} }, "required": ["a", "b"] } } ] } } def handle_tools_call(req_id, params): tool_name = params.get("name") args = params.get("arguments", {}) if tool_name == "add_numbers": a = args.get("a", 0) b = args.get("b", 0) result_val = a + b return { "jsonrpc": "2.0", "id": req_id, "result": { "content": [ { "type": "text", "text": f"计算结果:{a} + {b} = {result_val}" } ], "isError": False } } else: return { "jsonrpc": "2.0", "id": req_id, "error": { "code": -32601, "message": f"未知的工具名称: {tool_name}" } } def main(): log_debug("MCP Server 启动,等待 stdin 报文...") for line in sys.stdin: line = line.strip() if not line: continue log_debug(f"收到原生报文: {line}") # 1. 尝试解析 JSON try: req = json.loads(line) except json.JSONDecodeError: send_response({ "jsonrpc": "2.0", "id": None, "error": {"code": -32700, "message": "Parse error: 非法的 JSON 文本"} }) continue # 2. 校验是否符合 JSON-RPC 2.0 请求/通知基础结构 if req.get("jsonrpc") != "2.0": send_response({ "jsonrpc": "2.0", "id": req.get("id"), "error": {"code": -32600, "message": "Invalid Request: jsonrpc 版本必须为 '2.0'"} }) continue method = req.get("method") req_id = req.get("id") # 3. 如果是通知 (Notification),无需回复 if req_id is None and method == "notifications/initialized": log_debug("收到 Client 握手完成通知,初始化流程就绪!") continue # 4. 根据 Method 进行路由分发 try: if method == "initialize": resp = handle_initialize(req_id, req.get("params", {})) elif method == "tools/list": resp = handle_tools_list(req_id) elif method == "tools/call": resp = handle_tools_call(req_id, req.get("params", {})) else: resp = { "jsonrpc": "2.0", "id": req_id, "error": {"code": -32601, "message": f"Method not found: {method}"} } send_response(resp) except Exception as e: log_debug(f"执行异常: {traceback.format_exc()}") send_response({ "jsonrpc": "2.0", "id": req_id, "error": {"code": -32603, "message": f"Internal error: {str(e)}"} }) if __name__ == "__main__": main()7.2 客户端命令行模拟测试
我们可以通过控制台重定向输入,直接使用交互方式验证该 MCP Server 是否符合标准:
输入 1(发送 initialize 请求):
{"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {"protocolVersion": "2024-11-05"}}输出 1:
{"jsonrpc": "2.0", "id": 1, "result": {"protocolVersion": "2024-11-05", "capabilities": {"tools": {"listChanged": false}}, "serverInfo": {"name": "handcrafted-python-mcp", "version": "1.0.0"}}}输入 2(发送 initialized 通知):
{"jsonrpc": "2.0", "method": "notifications/initialized"}(服务端 stderr 日志输出,stdout 无响应,符合规范)
输入 3(发送 tools/call 调用计算):
{"jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": {"name": "add_numbers", "arguments": {"a": 12, "b": 30}}}输出 3:
{"jsonrpc": "2.0", "id": 2, "result": {"content": [{"type": "text", "text": "计算结果:12 + 30 = 42"}], "isError": false}}八、 生产级开发避坑指南与最佳实践
在基于 JSON-RPC 2.0 开发生产级别的 MCP 服务端时,以下几点是极易踩坑的“重灾区”:
1. Stdio 缓冲区刷新陷阱(Buffer Flushing)
在 Python 中使用print()或sys.stdout.write()时,操作系统通常默认开启行缓冲区或块缓冲区。如果你忘记调用sys.stdout.flush(),消息会滞留在内存缓冲区中。客户端会以为服务端陷入挂起死锁,最终触发超时报错。
规避方案:每次写完 stdout 后必须立即强制
flush()。在 Python 中可以加上环境标识PYTHONUNBUFFERED=1。
2. Stdout 污染问题
很多第三方 Python 库(例如 PyTorch、TensorFlow 或某些日志库)在 import 时会自动向stdout打印横幅或调试信息。这会破坏单行 JSON-RPC 报文结构,导致 Client 端报Parse error (-32700)。
规避方案:任何打印调试信息的逻辑必须强行重定向到
sys.stderr。
3. 请求 ID 类型匹配一致性
JSON-RPC 2.0 规定,请求的id可以是字符串或整数。服务端在构造响应时,必须保持原始类型完全一致(如果请求的id是字符串"abc",响应的id绝对不能变成数字或被丢掉 Quotes)。
4. 严谨的 JSON Schema 定义
tools/list暴露的参数 Schema 是 LLM 生成代码的主要参考依据。务必明确标注required数组以及属性的description。避免使用复杂且深层嵌套的多态 Schema,这能大幅提升 LLM 工具调用的成功率。
九、 总结
MCP(Model Context Protocol)的出现,标志着大模型应用开发正从“散兵游勇式的定制时代”迈入“标准化接口的工业化时代”。
通过选择成熟稳健的JSON-RPC 2.0规范作为骨架,MCP 成功兼顾了轻量性、跨语言扩展性与异步双向通信能力。理解并熟练掌握 JSON-RPC 2.0 的报文细节,不仅能帮助开发者更高效地构建高质量的 MCP Server 扩展组件,也为深入探索 Agent 智能体协同机制打下了坚实的工程基础。
