MCP 规定的内容——三类消息与工具契约
一句话总结
MCP(Model Context Protocol)是连接 LLM 应用与外部工具、数据的开放协议。消息采用 JSON-RPC 2.0,连接是有状态的,双方会协商能力。这三句话就是规范概览在 “Key Details” 中写下的全部内容,其余都是建立在其上的约定。
为什么需要它
做一个智能体,就会接上工具:读取数据库的工具、打开文件的工具、调用内部 API 的工具。起初把这些工具写成三个函数,再给模型加上说明就完事了。问题出现在做第二个智能体应用、或者要用别的团队的工具时。工具的名称、参数、结果格式、错误处理、审批流程都要在每个应用里重新约定,工具这边也要为每个应用准备不同的接入代码。N 个应用和 M 个工具会产生 N×M 个适配器。
规范把这个问题比作语言服务器协议(LSP)。就像 LSP 用“编辑器 ↔ 语言服务器”这一种约定,终结了各编辑器分别实现语言支持的时代,MCP 也想用“LLM 应用 ↔ 工具服务器”这一种约定来终结这个问题。因此规范定义的不是模型的行为,而是消息的格式:谁先说话、必须回答什么、失败如何通知。
角色有三个。宿主(Host)是发起连接的 LLM 应用,客户端是宿主内部与某个服务器一对一相连的连接器,服务器是提供上下文和能力的一方。服务器能提供三样东西——资源(可读取的数据)、提示词(模板),以及本课程的主题工具(模型执行的函数)。
工作原理
消息有三种。基本协议定义了请求、响应和通知。请求带有 id 和 method,响应在同一个 id 下只携带 result 或 error 中的一个。通知没有 id,接收方不得回复(MUST NOT)。JSON-RPC 2.0 原文允许 id 为 null,但 MCP 更进一步,禁止 null,也禁止在同一会话中重复使用同一个 id。
{"jsonrpc": "2.0", "id": 1, "method": "tools/call",
"params": {"name": "count_orders", "arguments": {"status": "paid"}}}
连接有先后顺序。生命周期规定了初始化 → 运行 → 关闭三个阶段。客户端发送 initialize 请求,其中包含自己支持的协议版本、能力和实现信息;服务器则用自己的版本、能力(tools、resources、prompts、logging ……)和 serverInfo 作答。若服务器支持该版本,就返回同一版本,否则返回它所知道的另一个版本;客户端如果不认识该版本,就应当断开连接(SHOULD)。此后客户端发送 notifications/initialized 通知,才真正进入运行阶段。初始化之前,双方除 ping 之外都不应发送请求,这是规则。
工具靠两个方法就够了。工具规范中的 tools/list 返回工具列表,每个工具都有 name、description 和 inputSchema(JSON Schema)。模型读取这个 schema 来构造参数。tools/call 接收 name 和 arguments,返回 content 数组(文本、图片、音频、资源链接)。这里有一个重要的区分——错误分两种。未知的工具名或错误的参数这类请求本身有误的情况,用 JSON-RPC error(规范示例为 -32602)回答;工具在执行中失败(API 失败、违反业务规则)则在 result 里用 isError: true 加说明文本回答。后者是因为模型必须能读到它并重新尝试。
传输有两种。传输规范中的 stdio,是客户端把服务器作为子进程启动,向 stdin 写入请求,再从 stdout 读取响应。消息以换行符分隔,消息内部不得包含换行;服务器不得向 stdout 写入 MCP 消息以外的内容(MUST NOT),日志可以写到 stderr。Streamable HTTP 是由一个端点接收 POST 和 GET,必要时通过 SSE 推送多条消息的方式,它明确要求在本地运行时验证 Origin 头并只绑定到 127.0.0.1——这是为了防止浏览器里的网页借助 DNS 重绑定来操控我本地的 MCP 服务器。
在现场相遇的样子
本课程标题所说的事故是这样发生的。有人做了一个封装公司内部数据库的 MCP 服务器,只有一个工具 run_sql。智能体收到“帮我清理一下测试数据”的请求,生成并发送了 DROP TABLE orders,服务器照样执行了。这不是模型的错。规范把工具定义为由模型选择(model-controlled)的,因此必须始终有人可以拒绝的环节(SHOULD),服务器必须验证所有输入、实现访问控制并限制调用频率(MUST)。原因就是这些句子在代码里一处都没有。
在现场检查 MCP 服务器时,首先要确认三件事。tools/list 给出的工具有多窄(如果有万能工具,就等于预约了一次事故);破坏性工具有没有确认环节;还有 stdout 的洁净度——一行调试用的 print 就会让客户端的 JSON 解析器崩溃,表现为“服务器没有响应”,这种事在现场相当常见。本课程的三个实验依次处理这三件事。
下一项测验要确认什么
测验会考查:三种消息的区别(尤其是为什么不能回复通知)、initialize 的顺序与版本协商、两种错误的区别,以及 stdio 中 stdout 与 stderr 的作用。这些内容全都出自本文。