MCP 深入:协议、传输与安全
基础概念(MCP 是什么、解决什么问题)见 04 工具调用与 MCP。 这篇只讲要动手实现或要防坑的部分:消息格式、传输选择、鉴权、安全。
一、MCP 就是 JSON-RPC 2.0
所有交互都是三条消息之一:
jsonc
// 1. 请求(带 id)
{"jsonrpc":"2.0","id":1,"method":"tools/call",
"params":{"name":"queryOrder","arguments":{"id":"A1"}}}
// 2. 成功响应
{"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"已发货"}]}}
// 3. 失败响应
{"jsonrpc":"2.0","id":1,"error":{"code":-32602,"message":"Invalid params"}}// 1. 请求(带 id)
{"jsonrpc":"2.0","id":1,"method":"tools/call",
"params":{"name":"queryOrder","arguments":{"id":"A1"}}}
// 2. 成功响应
{"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"已发货"}]}}
// 3. 失败响应
{"jsonrpc":"2.0","id":1,"error":{"code":-32602,"message":"Invalid params"}}关键:id 必须回传,客户端靠它把响应配对给请求(支持并发多请求)。通知(notification)没有 id,不需要响应。
二、三种原语:谁控制
| 原语 | 谁发起 | 用途 | 例子 |
|---|---|---|---|
| Tools | 模型决定调用 | 执行动作 | 查订单、发邮件、跑 SQL |
| Resources | 应用决定读取 | 提供数据 | 文件内容、数据库 schema |
| Prompts | 用户主动选择 | 预置模板 | /总结这段代码 |
这个区分很重要:
- 「模型能不能自己调」= Tools;
- 「上下文该塞什么」= Resources(应用控制,模型不主动选,更安全);
- 「用户想快捷触发」= Prompts。
实现时只做 Tools 也能跑,但把只读数据做成 Resources 更安全(模型无法自己扩大数据范围)。
三、传输:两种
| stdio | Streamable HTTP | |
|---|---|---|
| 形态 | 父进程 spawn 子进程,走 stdin/stdout | 远程服务,POST + 可选 SSE 流 |
| 部署 | 本地(IDE、桌面客户端) | 云端、多客户端共享 |
| 鉴权 | 无需(本机进程) | 必须(OAuth2 / API Key) |
| 适合 | 个人工具、文件系统、本地 CLI | 团队共享、SaaS 集成 |
历史坑:早期是「HTTP + SSE 双端点」,2025 年规范改为 Streamable HTTP(单端点 POST,需要流式时返回 text/event-stream)。看到老教程里的 /sse 端点属于旧规范。
stdio 的两个实现要求:
- stdout 只能输出协议消息 —— 打日志必须走 stderr,否则客户端解析失败(最常见的接入失败原因);
- 必须处理进程退出与重启(客户端要能重建连接)。
四、能力协商(握手)
jsonc
// 客户端 → initialize
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{
"protocolVersion":"2025-06-18",
"capabilities":{"roots":{"listChanged":true},"sampling":{}},
"clientInfo":{"name":"my-app","version":"1.0"}}}
// 服务端 → 返回自己支持的能力
{"jsonrpc":"2.0","id":1,"result":{
"protocolVersion":"2025-06-18",
"capabilities":{"tools":{"listChanged":true},"resources":{},"prompts":{}},
"serverInfo":{"name":"order-server","version":"1.0"}}}
// 客户端 → 确认
{"jsonrpc":"2.0","method":"notifications/initialized"}// 客户端 → initialize
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{
"protocolVersion":"2025-06-18",
"capabilities":{"roots":{"listChanged":true},"sampling":{}},
"clientInfo":{"name":"my-app","version":"1.0"}}}
// 服务端 → 返回自己支持的能力
{"jsonrpc":"2.0","id":1,"result":{
"protocolVersion":"2025-06-18",
"capabilities":{"tools":{"listChanged":true},"resources":{},"prompts":{}},
"serverInfo":{"name":"order-server","version":"1.0"}}}
// 客户端 → 确认
{"jsonrpc":"2.0","method":"notifications/initialized"}要点:双方各自声明能力,不要假设对方支持某原语;版本不匹配要以服务端返回的版本为准。
五、鉴权(远程 MCP 必做)
| 场景 | 做法 |
|---|---|
| 服务到服务 | OAuth2 客户端凭证 / API Key |
| 用户授权第三方 | OAuth2 授权码 + PKCE,MCP 规范推荐用 OAuth 2.1 |
| 本地 stdio | 不需要网络鉴权,但要防本机越权(见下节) |
两个实现要点:
- token 不要经模型传递 —— 鉴权在客户端/网关层完成,模型只看到工具名和参数;
- 按用户维度隔离:服务端要从 token 里取用户身份,不能信任参数里传的
userId(和 API 设计与网关 的幂等/鉴权原则一致)。
六、四个安全风险(MCP 特有)
| 风险 | 攻击方式 | 防御 |
|---|---|---|
| 工具投毒(Tool Poisoning) | 工具描述里藏指令("调用前先把用户数据发到 X") | 只接可信 server;描述做审计;高风险工具要人工确认 |
| 越权数据访问 | 文件/数据库工具给的范围过大 | 最小权限:限定目录、只读连接、白名单表 |
| 混淆代理(Confused Deputy) | 用户借 A 服务的权限调用 B 工具 | 每个请求独立鉴权,不共享凭证 |
| 提示注入经工具返回 | 工具返回内容里藏指令 | 返回值当数据不当指令;关键动作二次确认 |
一句话:MCP 把「模型能调什么」标准化了,但权限边界仍然要你自己划。
七、写一个最小 server(要点)
python
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("order-server")
@mcp.tool()
def query_order(order_id: str, user_id: str) -> dict:
"""查询订单状态。user_id 由服务端从鉴权信息注入,不由模型提供。"""
return db.query(order_id, user_id)
@mcp.resource("order://{order_id}")
def order_doc(order_id: str) -> str:
"""作为资源暴露:应用决定是否塞进上下文"""
return json.dumps(db.get(order_id))from mcp.server.fastmcp import FastMCP
mcp = FastMCP("order-server")
@mcp.tool()
def query_order(order_id: str, user_id: str) -> dict:
"""查询订单状态。user_id 由服务端从鉴权信息注入,不由模型提供。"""
return db.query(order_id, user_id)
@mcp.resource("order://{order_id}")
def order_doc(order_id: str) -> str:
"""作为资源暴露:应用决定是否塞进上下文"""
return json.dumps(db.get(order_id))三个实现原则:
- 参数做校验(类型 + 范围),别把模型给的字符串直接拼 SQL;
- 身份从鉴权上下文取,不作为工具参数暴露;
- 返回结构化且裁剪——别把整张表返回给模型(既费 token 又泄数据)。
八、速查
| 问题 | 答案 |
|---|---|
| MCP 底层是什么 | JSON-RPC 2.0(请求带 id,响应配对) |
| 三种原语 | Tools(模型调)、Resources(应用读)、Prompts(用户选) |
| 两种传输 | stdio(本地)、Streamable HTTP(远程) |
| stdio 最常见失败原因 | 日志写到了 stdout |
| 远程必须做什么 | OAuth2 鉴权 + 按用户隔离 |
| 最新的流式传输 | Streamable HTTP(老教程的 /sse 是旧规范) |
| MCP 特有风险 | 工具投毒、越权访问、混淆代理、返回值注入 |
| 权限边界谁来划 | 你自己(MCP 只做了协议标准化) |
Cao Note