Skip to content

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 更安全(模型无法自己扩大数据范围)。

三、传输:两种 ​

stdioStreamable HTTP
形态父进程 spawn 子进程,走 stdin/stdout远程服务,POST + 可选 SSE 流
部署本地(IDE、桌面客户端)云端、多客户端共享
鉴权无需(本机进程)必须(OAuth2 / API Key)
适合个人工具、文件系统、本地 CLI团队共享、SaaS 集成

历史坑:早期是「HTTP + SSE 双端点」,2025 年规范改为 Streamable HTTP(单端点 POST,需要流式时返回 text/event-stream)。看到老教程里的 /sse 端点属于旧规范。

stdio 的两个实现要求:

  1. stdout 只能输出协议消息 —— 打日志必须走 stderr,否则客户端解析失败(最常见的接入失败原因);
  2. 必须处理进程退出与重启(客户端要能重建连接)。

四、能力协商(握手) ​

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不需要网络鉴权,但要防本机越权(见下节)

两个实现要点:

  1. token 不要经模型传递 —— 鉴权在客户端/网关层完成,模型只看到工具名和参数;
  2. 按用户维度隔离:服务端要从 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))

三个实现原则:

  1. 参数做校验(类型 + 范围),别把模型给的字符串直接拼 SQL;
  2. 身份从鉴权上下文取,不作为工具参数暴露;
  3. 返回结构化且裁剪——别把整张表返回给模型(既费 token 又泄数据)。

八、速查 ​

问题答案
MCP 底层是什么JSON-RPC 2.0(请求带 id,响应配对)
三种原语Tools(模型调)、Resources(应用读)、Prompts(用户选)
两种传输stdio(本地)、Streamable HTTP(远程)
stdio 最常见失败原因日志写到了 stdout
远程必须做什么OAuth2 鉴权 + 按用户隔离
最新的流式传输Streamable HTTP(老教程的 /sse 是旧规范)
MCP 特有风险工具投毒、越权访问、混淆代理、返回值注入
权限边界谁来划你自己(MCP 只做了协议标准化)