MCP 协议及其三种能力
MCP(Model Context Protocol)把「模型怎么接外部世界」标准化成一层协议,让工具、数据、提示模板不再每家各写一套适配。
一、把适配收敛成一层协议
没有标准协议时,N 个应用要接 M 个数据源,就得写 N×M 份适配代码,而且每一份的调用约定、错误语义、鉴权方式都各不相同。MCP 把这件事压成 N+M:数据源侧实现一次 Server,应用侧实现一次 Client,双方按同一套协议对话。
第二个收益常被忽略:工具描述从「写在提示词里」变成「由服务端自述」。以前每接一个能力,就要把它的用法、参数、注意事项手抄进提示词,改一次就要同步多处;现在这些描述由 Server 自己提供,可发现、可版本化、可复用。
需要说清的是它的定位:MCP 是协议层,解决「工具怎么被发现、怎么被调用」;模型决定要不要调、传什么参数,那是模型的能力层(Function Calling)。两者是互补关系,不是替代关系。本文基于 2026-07-28 版本的规范展开(截至 2026 年 10 月为当前最新版本,也是自发布以来最大的一次修订)。
二、工具、资源与提示模板
Server 可以对外提供三类东西,区分它们的要点是由谁触发。
Tools(工具) —— 由模型决定调用。每个工具带一段说明和一个 JSON Schema 描述入参,模型看到的是这些描述,据此决定调不调、传什么。这是三者里唯一「模型主动」的一类,也是绝大多数集成的主要形态。
Resources(资源) —— 由应用决定读取。它是可被读取的数据(文件内容、数据库记录、接口响应),模型不会自己去拿,而是由应用决定在什么时候、以什么方式把它塞进上下文。想让模型看到一份文档,正确做法是应用先读 Resource,再把内容作为上下文的一部分给它。
Prompts(提示模板) —— 由用户显式触发。它是服务端预置的模板(比如「把这段代码改成 XXX 的风格」),通常表现为界面上的一个斜杠命令或按钮,用户点了才执行。它和 Tools 的区别很关键:Tools 是模型自选,Prompts 是用户指定。
一个工具声明大致长这样——注意描述文本本身会进入上下文,它既是说明也是指令:
server.registerTool('searchOrder', {
description: '按订单号查询订单状态。只读操作,不会修改任何数据。',
inputSchema: {
type: 'object',
properties: { orderId: { type: 'string', description: '订单号,形如 202601010001' } },
required: ['orderId'],
additionalProperties: false,
},
async handler({ orderId }) {
return { content: [{ type: 'text', text: await queryOrder(orderId) }] }
},
})
顺带一提,新版本里工具定义已改用完整的 JSON Schema 2020-12,参数契约的表达力比早期版本强不少;list 类结果也支持带缓存提示,避免客户端反复拉取工具目录。
三、stdio 与 HTTP 传输
stdio —— 本地进程通信。客户端以子进程方式启动 Server,通过标准输入输出交换消息。零网络配置、没有端口与防火墙问题,是本地集成最简单可靠的方式。缺点是只能本机单实例,无法多租户共享。
Streamable HTTP —— 远程服务。客户端 POST 到单一端点,服务端可以返回一个 JSON 结果,也可以返回一个与本次请求绑定的 SSE 流用于推送进度与最终结果。
这一版有个重要变化值得单独说:协议层变成了无状态的。早期版本要先 initialize 握手、拿到 Mcp-Session-Id,后续每个请求都带上它——这意味着客户端被钉在签发会话的那台实例上,横向扩展需要粘性路由和共享会话存储。
新版本取消了握手与会话头,改为每个请求自描述:协议版本、客户端信息与能力都放在请求体的 _meta 里,任何一台实例都能处理任何一个请求。取而代之的是一个可缓存的 server/discover 方法,客户端需要时主动查询服务端能力。
POST /mcp HTTP/1.1
Content-Type: application/json
Accept: application/json, text/event-stream
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: search
{"jsonrpc":"2.0","id":1,"method":"tools/call",
"params":{"name":"search","arguments":{"q":"otters"},
"_meta":{"io.modelcontextprotocol/clientInfo":{"name":"my-app","version":"1.0"}}}}
注意那三个请求头:它们把方法名、目标名、协议版本暴露给基础设施,网关因此可以不解析 JSON 体就做路由、限流与鉴权。相应地也多了一条纪律:头部与请求体不一致会被直接拒绝(协议保留了专门的错误码)。这也意味着不要把密码、令牌、个人信息放进这些头里——它们可能被代理与日志记录下来。
对工程实践的影响很直接:新建的远程 MCP 服务应按无状态、单端点、可水平扩展来设计;仍在使用早期 HTTP+SSE 传输的系统应当规划迁移;本地 stdio 模式则不受这次会话变更影响。
工具是能直接产生副作用的,调用前必须有一道确认:
if (readOnlyTools.has(name)) return callTool(name, args)
// 写操作要么在白名单里,要么让用户确认一次
if (!allowlist.includes(name)) {
const ok = await askUser(`允许调用 ${name} 吗?`, args)
if (!ok) return { content: [{ type: 'text', text: '用户已拒绝' }] }
}
return callTool(name, args)
四、权限与信任边界
三条必须记住。
第一,接一个第三方 Server 等于把工具执行权交出去。 连上之后,对方声明的工具就会出现在模型的候选列表里。所以必须有白名单机制,不能让用户随手填一个地址就连上去。
第二,工具描述本身就是指令。 工具的描述文本会进入上下文,模型会照着它行动——这意味着恶意描述可以诱导模型做越权操作(常说的工具投毒)。描述要当成不可信输入看待:来自第三方的描述应当经过审核,敏感能力不要依赖描述来保证约束。
第三,写操作要人工确认。 写文件、发请求、删数据、下单付款这类动作自动执行,等于把风险直接放行。
这里还有一个工程上的注意点:新版本取消了「服务端主动向客户端发起请求」这种常驻双向模式,改为 MRTR(多轮往返请求)——服务端需要额外信息时,返回一个标明「需要输入」的结果,客户端补上信息后再用原请求重试。
这个变化带来一条很实在的要求:可能被重试的操作必须幂等,或者接受一个幂等键。否则一次网络中断就可能把「创建一个工单」变成「创建两个工单」。早期版本里基于 SSE 事件 ID 的续传机制也一并移除了,重试的语义更清晰,但责任落到了工具实现者身上。
授权方面,新版本向 OAuth/OIDC 的实践靠拢(例如校验签发方、动态客户端注册改为基于客户端元数据文档),并新增了面向机器对机器场景的客户端凭证扩展。协议同时给出了正式的废弃策略,旧能力至少有 12 个月的迁移窗口——这对生产系统是个好消息,不必一夜间重写。
// Resources 由应用决定读取,再作为上下文交给模型(模型不会自己直接拿)
const doc = await client.readResource('file:///spec/design.md')
const context = `参考文档:\n${doc.text}`
// 之后把 context 作为上下文的一部分传给模型,而不是等模型主动去取
协议层就是普通的 JSON-RPC,理解这一点就不需要把它想复杂:
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "searchOrder",
"arguments": { "id": "A-20260101-001" }
}
}
五、它不等于某个模型的能力
MCP 不是 Function Calling 的替代品——一个是协议层(工具怎么被发现与调用),一个是模型能力层(模型决定怎么调),二者互补。
Prompts 也不是给模型自动用的,它由用户显式触发,与 Tools 的触发方不同。
连上了也不等于安全:协议标准化的是通信格式,不是权限边界。谁能调、调了要不要人确认,仍然要自己定义。
没有会话也不代表做不了多轮——多轮状态可以由工具自己铸造一个句柄(比如 basket_id),让模型在后续调用里作为参数传回来,这与 HTTP API 的做法一致。
接 MCP 之前先想清楚权限边界——协议让连接变简单了,但「谁能调、调了要不要人确认、失败了能不能安全重试」仍然是你的责任。
协议本身在 modelcontextprotocol.io,能力划分与传输层定义以官方为准。