系列文章导航:AI系列文章导航目录-持续更新中
第15课:深度学习MCP协议(Model Context Protocol)
📝 本文摘要 :本文从零讲解MCP协议(Model Context Protocol)------2024年底由Anthropic发布的Agent工具连接标准协议。内容包括:①MCP是什么(定义、为什么需要、它生效于Agent三层架构的哪一层 );②MCP架构详解(Host/Client/Server三层模型、三大能力Tools/Resources/Prompts 及案例、传输方式Stdio vs SSE vs Streamable HTTP 对比与安全性讨论);③MCP协议规范 (JSON-RPC 2.0基础、完整生命周期、所有核心方法一览、完整通信示例);④MCP Server开发实战(Python/Go SDK代码示例);⑤MCP vs Function Calling深度对比(层次关系、架构图解)。适合AI小白从零理解MCP的定位、原理和实践。
MCP是2024年底至今Agent领域最重要的基础设施。理解MCP,你才能理解Agent如何标准化地连接外部世界。
一、MCP是什么
1.1 一句话定义
MCP (Model Context Protocol) = Agent连接外部世界的标准协议
类比: MCP之于Agent ≈ HTTP之于Web ≈ USB之于外设
1.2 为什么需要MCP
一句话理解:没有MCP时,每个AI应用连接工具都要自己写一套代码,就像每个电器都用不同的插头一样混乱。MCP统一了插头标准,写一次工具,所有AI应用都能用。
没有MCP时的问题:
每个AI应用要连接外部工具,都需要:
1. 自己写工具接入代码
2. 自己定义工具描述格式
3. 自己处理认证鉴权
4. 自己管理工具生命周期
结果:
- 工具A在ChatGPT里能用,在Claude里不能用
- 每换一个AI应用,工具就要重写
- 大量重复劳动
类比: 就像每个浏览器都有自己的网络协议 → 混乱
统一HTTP后 → 任何网站在任何浏览器都能用
MCP做的就是这个统一:
- 任何MCP Server提供的工具,任何MCP Client都能用
- 写一次,到处运行
实际例子:
你写了一个"查询数据库"的MCP Server
→ Cursor能用它查数据库
→ Claude Desktop能用它查数据库
→ 你自己的Agent也能用它查数据库
→ 不需要写三份代码!
MCP到底生效于哪一层?(重要!)
很多初学者会困惑:MCP和LLM是什么关系?LLM能直接调用MCP吗?
答案:LLM完全不感知MCP的存在。MCP作用于"应用程序"和"工具"之间,不涉及LLM本体。
Agent系统本质上是三层架构:
┌──────────────────────────────────────────────────────────────────┐
│ 第1层: LLM模型本体(GPT/Claude等) │
│ │
│ 只做一件事: 根据tools定义,决定调哪个工具、传什么参数 │
│ 输入: messages + tools定义 │
│ 输出: "我要调get_weather,参数是{city:'北京'}" │
│ │
│ ⚠️ 它不知道MCP的存在,也不关心工具从哪来 │
│ ⚠️ 它只认Function Calling格式的tools数组 │
└──────────────────────────────────┬───────────────────────────────┘
│ Function Calling协议
│ (OpenAI/Anthropic的API格式)
┌──────────────────────────────────▼───────────────────────────────┐
│ 第2层: 应用程序 / Agent编排层(你写的代码) │
│ │
│ 做两件事: │
│ 1. 向上: 把工具列表转成LLM认识的tools格式,传给LLM │
│ 2. 向下: 拿到LLM的调用决策后,去实际执行工具 │
│ │
│ 这一层是"翻译官"和"执行者" │
│ 也是MCP Client所在的层! │
└───────────┬──────────────────────────────────────┬───────────────┘
│ 方式A: 直接调用 │ 方式B: MCP协议
│ (你自己写的函数) │ (标准化协议)
┌───────────▼───────────────┐ ┌───────────▼──────────────┐
│ 第3层-A: 本地工具 │ │ 第3层-B: MCP Server │
│ │ │ │
│ def get_weather(): │ │ @mcp.tool() │
│ return ... │ │ def get_weather(): │
│ │ │ return ... │
│ 直接在代码里定义和执行 │ │ 独立进程,标准化暴露 │
└───────────────────────────┘ └──────────────────────────┘
关键理解:LLM看到的永远是同一种格式
python
# 不管工具从哪来,LLM看到的都是这样:
tools = [
{"type": "function", "function": {"name": "get_weather", ...}},
{"type": "function", "function": {"name": "query_order", ...}},
]
# LLM不知道也不关心:
# - get_weather 是你手写的本地函数?
# - 还是从MCP Server动态获取的?
# - 还是从Skill系统注册的?
# 对LLM来说,它们都一样!
用一个具体例子对比"有MCP"和"没有MCP":
python
# ═══════════════════════════════════════════
# 没有MCP时:工具直接写在应用程序里
# ═══════════════════════════════════════════
def get_weather(city): # 工具实现在应用程序内部
return requests.get(f"https://api.weather.com/{city}")
tools = [{"type": "function", "function": {"name": "get_weather", ...}}]
response = openai.chat.completions.create(tools=tools, ...) # 传给LLM
# LLM返回: 调用get_weather(city="北京")
result = get_weather("北京") # 应用程序直接执行本地函数
# ═══════════════════════════════════════════
# 有MCP时:工具从MCP Server动态获取
# ═══════════════════════════════════════════
# 1. 应用程序先从MCP Server获取工具列表(MCP协议)
mcp_tools = await session.list_tools() # MCP协议: tools/list
# 2. 把MCP工具转换成LLM认识的格式(LLM完全不知道MCP的存在!)
tools = convert_mcp_to_openai_format(mcp_tools) # 转成{"type":"function",...}
# 3. 传给LLM(和没有MCP时完全一样!LLM的行为没有任何变化!)
response = openai.chat.completions.create(tools=tools, ...)
# 4. LLM返回: 调用get_weather(city="北京")(LLM的输出也完全一样!)
# 5. 应用程序通过MCP协议执行工具(这里不同!走MCP协议而非本地调用)
result = await session.call_tool("get_weather", {"city": "北京"}) # MCP协议: tools/call
一句话总结:
MCP解决的不是"LLM怎么决定调工具"的问题(那是Function Calling的事),
MCP解决的是"工具怎么被发现、管理和复用"的问题。
类比:
你去餐厅点菜(LLM决定调哪个工具)→ 这个过程不变
餐厅的食材从哪进货(工具从哪来)→ MCP统一了进货渠道
不管食材是从超市买的还是从农场直送的,
你点菜的方式不会变,菜单的格式也不会变。
1.3 MCP的发展时间线
2024.11 Anthropic发布MCP规范 v0.1
│ "我们希望AI应用连接工具的方式标准化"
│
2024.12 早期MCP Server出现
│ 文件系统、GitHub、数据库等基础MCP Server
│
2025.01 社区开始大量贡献MCP Server
│ MCP Server仓库涌现
│
2025.02 OpenAI宣布支持MCP
│ "MCP成为行业标准"的信号
│
2025.03 MCP SDK发布(Python/TypeScript/Java/Go等)
│ 开发MCP Server变得简单
│
2025.04 MCP规范更新,生态爆发
│ 数百个MCP Server可用
│ 主流AI应用开始原生支持MCP
│
2025.05 MCP成为事实标准
│ 类比"HTTP成为了Web的事实标准"
│
2026 MCP生态成熟 ← 你在这里
二、MCP的架构
2.1 核心概念
┌─────────────────┐ MCP协议 ┌─────────────────┐
│ MCP Client │ ←──────────────→ │ MCP Server │
│ (AI应用/Agent) │ │ (工具提供者) │
│ │ │ │
│ - 发起连接 │ │ - 暴露工具 │
│ - 调用工具 │ │ - 暴露资源 │
│ - 获取资源 │ │ - 提供提示模板 │
└─────────────────┘ └─────────────────┘
↑ ↑
│ │
AI应用层 工具层
(Claude/Cursor/ (数据库/API/
自己的Agent) 文件系统等)
2.2 MCP的三大能力
MCP Server可以向Client暴露三种东西:
1. Tools(工具)------ 最常用
Agent可以调用的函数
类比: Function Calling中的tools,但标准化了
例: query_database(sql), search_web(query), send_email(to, subject, body)
场景: Agent需要"做事"的时候
2. Resources(资源)------ 只读数据
Agent可以读取的数据(不是执行操作,而是获取信息)
类比: 文件系统中的文件、数据库中的记录
例: 用户配置文件、项目文档、日志文件
场景: Agent需要"查资料"的时候
3. Prompts(提示模板)------ 较少用
预定义的Prompt模板,可以被 Client调用
类比: 可复用的System Prompt片段
例: code_review_prompt, summarization_prompt
场景: 多个Agent共用同一套提示词模板
实际开发中,90%的时间你都在用Tools。
案例1:Tools(工具)------ "帮我做事"
场景:你开发了一个"天气查询"MCP Server,Agent可以调用它来查天气。
python
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("weather-service")
@mcp.tool()
def get_weather(city: str) -> str:
"""查询指定城市的实时天气
Args:
city: 城市名称,如"北京"、"上海"
"""
# 实际开发中这里会调用真实的天气API
weather_data = {
"北京": "晴,28°C,湿度45%,东北风3级",
"上海": "多云,26°C,湿度72%,东南风2级",
"深圳": "阵雨,30°C,湿度85%,南风4级",
}
return weather_data.get(city, f"未找到{city}的天气数据")
@mcp.tool()
def get_forecast(city: str, days: int = 3) -> str:
"""查询未来几天的天气预报
Args:
city: 城市名称
days: 预报天数,默认3天,最多7天
"""
return f"{city}未来{days}天预报: 明天晴转多云27°C, 后天小雨24°C, 大后天多云26°C"
if __name__ == "__main__":
mcp.run()
Agent使用时的效果:
用户: "深圳今天天气怎么样?"
Agent(LLM决策): 我需要调用get_weather工具
Agent(应用层): 通过MCP协议 → tools/call → get_weather(city="深圳")
MCP Server返回: "阵雨,30°C,湿度85%,南风4级"
Agent回复用户: "深圳今天有阵雨,气温30°C,湿度较高85%,建议带伞出门。"
Tools的特点:有副作用(可能改变外部状态),Agent主动调用,类似"执行一个动作"。
案例2:Resources(资源)------ "帮我查资料"
场景:你开发了一个"项目文档"MCP Server,Agent可以读取项目的各种文档。
python
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("project-docs")
# 资源是只读的,Agent可以"查阅"但不能"修改"
@mcp.resource("docs://readme")
def get_readme() -> str:
"""获取项目README文档"""
return """
# 电商订单系统
## 技术栈: Python + FastAPI + PostgreSQL
## 启动方式: uvicorn main:app --reload
## 数据库: PostgreSQL 15, 端口5432
"""
@mcp.resource("docs://api/{endpoint}")
def get_api_doc(endpoint: str) -> str:
"""获取指定API接口的文档
Args:
endpoint: API端点名称,如"orders"、"users"
"""
api_docs = {
"orders": "POST /api/orders - 创建订单\nGET /api/orders/{id} - 查询订单\nPUT /api/orders/{id}/cancel - 取消订单",
"users": "POST /api/users/register - 用户注册\nPOST /api/users/login - 用户登录\nGET /api/users/me - 获取当前用户信息",
}
return api_docs.get(endpoint, f"未找到{endpoint}的API文档")
@mcp.resource("docs://changelog")
def get_changelog() -> str:
"""获取项目更新日志"""
return """
v2.1.0 (2025-05-01): 新增退款功能
v2.0.0 (2025-03-15): 重构订单模块,支持分布式事务
v1.5.0 (2025-01-20): 新增优惠券系统
"""
if __name__ == "__main__":
mcp.run()
Agent使用时的效果:
用户: "这个项目用的什么数据库?怎么启动?"
Agent(LLM决策): 我需要查阅项目文档
Agent(应用层): 通过MCP协议 → resources/read → docs://readme
MCP Server返回: README内容
Agent回复用户: "项目使用PostgreSQL 15数据库(端口5432),启动命令是 uvicorn main:app --reload"
Resources vs Tools的区别:
Tools: "做事" → 发邮件、查数据库、调API(有副作用,可能改变世界)
Resources: "查资料" → 读文档、看配置、获取上下文(只读,不改变任何东西)
类比:
Tools = 你手里的工具(锤子、螺丝刀)→ 用来改变东西
Resources = 你桌上的参考资料(手册、图纸)→ 用来获取信息
案例3:Prompts(提示模板)------ "帮我组织语言"
场景:你的团队有多个Agent,都需要做代码审查,你把审查的提示词模板统一放在MCP Server里,所有Agent共用。
python
from mcp.server.fastmcp import FastMCP
import mcp.types as types
mcp = FastMCP("prompt-templates")
@mcp.prompt()
def code_review(language: str, code: str) -> str:
"""代码审查提示模板
Args:
language: 编程语言,如"Python"、"Go"
code: 需要审查的代码
"""
return f"""你是一位资深的{language}开发工程师,请对以下代码进行审查。
审查维度:
1. 代码正确性:是否有逻辑错误或边界问题
2. 性能:是否有性能瓶颈或可优化的地方
3. 安全性:是否有安全漏洞(SQL注入、XSS等)
4. 可读性:命名是否清晰、结构是否合理
5. 最佳实践:是否遵循{language}社区的最佳实践
代码:
{language.lower()}
{code}
请逐条给出审查意见,并提供改进建议。"""
@mcp.prompt()
def bug_report_analysis(error_log: str) -> str:
"""Bug分析提示模板
Args:
error_log: 错误日志内容
"""
return f"""你是一位经验丰富的排障工程师。请分析以下错误日志,给出:
1. 错误原因分析(根本原因是什么)
2. 影响范围评估(影响了哪些功能/用户)
3. 修复建议(具体的修复步骤)
4. 预防措施(如何避免再次发生)
错误日志:
{error_log}"""
@mcp.prompt()
def summarize_meeting(notes: str) -> str:
"""会议纪要总结模板
Args:
notes: 会议原始记录
"""
return f"""请将以下会议记录整理为结构化的会议纪要:
格式要求:
- 会议主题
- 参会人员
- 讨论要点(按议题分类)
- 决议事项(明确责任人和截止日期)
- 待跟进事项
原始记录:
{notes}"""
if __name__ == "__main__":
mcp.run()
Agent使用时的效果:
用户: "帮我review一下这段Python代码"
Agent(应用层): 通过MCP协议 → prompts/get → code_review(language="Python", code="...")
MCP Server返回: 完整的代码审查提示词模板(已填入参数)
Agent(应用层): 把这个模板作为prompt发给LLM
LLM: 按照模板的结构,逐条给出审查意见
Prompts的使用场景:
为什么不直接把提示词写在代码里?
1. 统一管理: 10个Agent都要做代码审查 → 改一处模板,所有Agent都更新
2. 版本控制: 模板可以独立迭代,不影响Agent代码
3. 专业分工: 提示词工程师维护模板,开发工程师维护Agent逻辑
类比:
Tools = 餐厅的厨具(锅碗瓢盆)
Resources = 餐厅的食材仓库(只能取用,不能改变)
Prompts = 餐厅的菜谱模板(标准化的烹饪流程,所有厨师共用)
三大能力总结对比
| 维度 | Tools | Resources | Prompts |
|---|---|---|---|
| 作用 | 执行操作 | 提供数据 | 提供模板 |
| 方向 | Agent → 外部世界 | 外部世界 → Agent | MCP → Agent的Prompt |
| 有副作用? | ✅ 可能改变状态 | ❌ 只读 | ❌ 只读 |
| 谁触发? | LLM决定调用 | Agent/用户主动读取 | Agent/用户主动获取 |
| 使用频率 | 90% | 8% | 2% |
| 类比 | 手里的工具 | 桌上的参考资料 | 标准操作手册 |
2.3 传输方式
MCP Client和Server之间需要通信,就像两个人说话需要选择"面对面说"还是"打电话"。
在讲具体方式之前,先理解几个基础概念:
📚 前置知识:短连接 vs 长连接
短连接(Short Connection):
每次通信都要"建立连接 → 发数据 → 断开连接"
就像打一个电话只说一句话就挂了,下次再打
例: 普通HTTP请求(请求完就断开)
长连接(Long/Persistent Connection):
建立一次连接后保持不断开,可以持续通信
就像打电话一直不挂,有话随时说
例: WebSocket、SSE
为什么Agent需要长连接?
Agent和工具之间可能需要频繁交互(一个任务可能调用多次工具)
如果每次都重新建立连接,开销太大
而且有些工具执行时间很长,需要Server主动推送进度
MCP支持三种传输方式(按发展顺序):
- Stdio(标准输入输出)------ "面对面说话"
- SSE(Server-Sent Events)------ "打电话,对方一直说"
- Streamable HTTP ------ "智能电话,按需切换"
2.3.1 Stdio(标准输入输出)------ "面对面说话"
一句话理解 :MCP Client直接启动MCP Server作为子进程,两者在同一台电脑上,通过"标准输入/输出"管道通信。就像两个程序之间用管道|连接一样。
什么是Stdio?
Stdio = Standard Input/Output = 标准输入输出
你在终端里用过管道吗?
cat file.txt | grep "hello"
这里 cat 的输出(stdout)→ 直接变成 grep 的输入(stdin)
两个程序通过管道通信,不需要网络!
MCP的Stdio模式就是这个原理:
MCP Client 启动 MCP Server 作为子进程
Client 写数据到 Server 的 stdin(输入)
Server 写数据到自己的 stdout(输出),Client 读取
这本质上是一个"长连接"------进程启动后管道一直存在,
Client和Server可以持续来回通信,直到进程结束。
工作流程图:
┌─────────────────────────────────────────────────────────────┐
│ 你的电脑(同一台机器) │
│ │
│ ┌──────────────┐ 管道通信 ┌─────────────┐ │
│ │ MCP Client │ ──── stdin(写入) ────→ │ MCP Server │ │
│ │ (如Cursor) │ ←─── stdout(读取) ──── │ (子进程) │ │
│ └──────────────┘ └─────────────┘ │
│ │ │ │
│ │ Client启动Server │ │
│ │ (类似: python server.py) │ │
│ └─────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
通信过程:
1. Client启动Server进程: spawn("python", ["server.py"])
2. Client往Server的stdin写JSON: {"method": "tools/list"}\n
3. Server处理后往自己的stdout写JSON: {"result": [...]}\n
4. Client读取Server的stdout得到结果
5. 管道一直保持,可以继续发送下一个请求(不需要重新连接)
配置示例(Claude Desktop / Cursor 的配置文件):
json
{
"mcpServers": {
"weather-service": {
"command": "python", // 用python启动
"args": ["/path/to/weather_server.py"], // Server脚本路径
"env": { // 环境变量(可选)
"API_KEY": "your-api-key"
}
},
"database-service": {
"command": "node", // 用node启动
"args": ["/path/to/db_server.js"]
}
}
}
Python代码示例(Client端连接Stdio Server):
python
import asyncio
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
async def main():
# 定义Server的启动参数
server_params = StdioServerParameters(
command="python", # 启动命令
args=["weather_mcp_server.py"], # Server脚本
env={"API_KEY": "xxx"} # 环境变量
)
# 启动Server子进程并建立连接(长连接,一直保持)
async with stdio_client(server_params) as (read, write):
async with ClientSession(read, write) as session:
# 初始化握手
await session.initialize()
# 现在可以调用工具了(在同一个连接上多次调用)
result = await session.call_tool("get_weather", {"city": "北京"})
print(result.content[0].text)
# 可以继续调用,不需要重新连接
result2 = await session.call_tool("get_weather", {"city": "上海"})
print(result2.content[0].text)
asyncio.run(main())
Stdio模式的特点:
✅ 优点:
- 零配置网络: 不需要IP、端口、域名,直接进程间通信
- 安全: 不暴露任何网络端口,外部无法访问
- 简单: 启动即用,适合本地开发和调试
- 低延迟: 进程间通信比网络通信快得多
- 天然长连接: 管道一直存在,无需维护连接状态
❌ 缺点:
- 只能本地: Client和Server必须在同一台机器上
- 一对一: 一个Server进程只能服务一个Client
- 生命周期绑定: Client关闭,Server也跟着关闭
- 不适合生产: 无法多人共享、无法远程访问
适用场景:
- 本地开发调试
- IDE插件(如Cursor、VS Code中的MCP)
- 个人使用的本地工具
2.3.2 SSE(Server-Sent Events)------ "打电话,对方一直说"
⚠️ 注意:SSE是MCP早期版本(2024.11 ~ 2025.03)使用的传输方式,后来被Streamable HTTP取代。但很多现有的MCP Server仍在使用SSE,所以你需要了解它。
一句话理解 :Client通过HTTP连接到远程Server,Server通过一个持续不断的HTTP长连接向Client推送消息。就像你打电话给客服,客服一直在线给你播报信息。
📚 什么是SSE?
SSE = Server-Sent Events = 服务器推送事件
先理解普通HTTP的问题:
普通HTTP是"一问一答"模式:
Client: "查一下天气" → Server: "晴天28°C"(连接断开)
Client: "再查一下明天" → Server: "多云25°C"(连接断开)
每次都要重新建立连接,而且Server不能主动找Client说话。
SSE解决了什么?
SSE是一个"单向长连接":
Client发起一个HTTP GET请求,连接建立后不断开
Server可以持续不断地往这个连接里推送数据
Client只需要监听就行
就像:
你打开收音机(建立连接)
电台一直在播(Server持续推送)
你只能听,不能通过收音机说话(单向:Server → Client)
那Client怎么发消息给Server?
SSE只解决了"Server → Client"的推送问题
"Client → Server"还是用普通的HTTP POST请求
所以MCP的SSE模式实际上是两条通道:
通道1: HTTP POST(Client → Server)发送请求
通道2: SSE长连接(Server → Client)推送响应和通知
SSE的工作流程图:
┌──────────────┐ ┌──────────────┐
│ MCP Client │ 互联网/内网 │ MCP Server │
│ (你的Agent) │ │ (远程服务) │
│ │ │ 端口:8080 │
│ │ │ │
│ ① 建立SSE长连接(一直不断开) │ │
│ │ ─── GET /sse ────────────→ │ │
│ │ ←── 连接保持,等待推送 ───── │ │
│ │ │ │
│ ② Client发送请求(普通HTTP POST) │ │
│ │ ─── POST /message ───────→ │ │
│ │ {method: "tools/call"} │ │
│ │ │ │
│ ③ Server通过SSE长连接推送响应 │ │
│ │ ←── SSE: data: {result} ── │ │
│ │ ←── SSE: data: {progress} ─ │ (可以持续推) │
│ │ ←── SSE: data: {done} ──── │ │
└──────────────┘ └──────────────┘
关键点:
- SSE连接是"长连接",建立后一直保持不断开
- Client通过POST发请求,Server通过SSE推响应
- Server可以主动推送通知(不需要Client先问)
SSE的数据格式(你在网络抓包时会看到的):
http
# Client建立SSE连接
GET /sse HTTP/1.1
Host: mcp-server.com:8080
Accept: text/event-stream ← 告诉Server我要SSE
# Server响应(注意Content-Type)
HTTP/1.1 200 OK
Content-Type: text/event-stream ← 表示这是SSE流
Cache-Control: no-cache ← 不缓存
Connection: keep-alive ← 保持连接
# Server持续推送的数据格式(每条消息用两个换行分隔)
event: endpoint
data: /message?sessionId=abc123
event: message
data: {"jsonrpc":"2.0","id":1,"result":{"tools":[...]}}
event: message
data: {"jsonrpc":"2.0","method":"notifications/progress","params":{"progress":50}}
event: message
data: {"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":"北京:晴28°C"}]}}
Python代码示例(SSE模式):
python
# === Server端 ===
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("weather-service")
@mcp.tool()
def get_weather(city: str) -> str:
"""查询天气"""
return f"{city}: 晴,28°C"
if __name__ == "__main__":
# 以SSE模式运行
mcp.run(
transport="sse", # 使用SSE传输
host="0.0.0.0", # 监听所有网络接口(允许远程访问)
port=8080 # 端口号
)
# 启动后,Server会暴露两个端点:
# GET /sse → SSE长连接端点
# POST /message → 接收Client请求的端点
python
# === Client端 ===
import asyncio
from mcp import ClientSession
from mcp.client.sse import sse_client
async def main():
# 连接远程MCP Server的SSE端点
server_url = "http://your-server.com:8080/sse"
async with sse_client(server_url) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
# 调用工具(和Stdio模式的代码一模一样!)
result = await session.call_tool("get_weather", {"city": "北京"})
print(result.content[0].text)
asyncio.run(main())
SSE模式的特点:
✅ 优点:
- 远程访问: Server可以部署在任何地方
- Server主动推送: 不需要Client轮询,Server有消息就推
- 兼容性好: 基于HTTP,防火墙友好,不需要特殊协议
- 自动重连: SSE标准支持断线自动重连
❌ 缺点:
- 单向推送: SSE只能Server→Client,Client→Server还要额外的POST
- 两条通道: 需要维护SSE连接 + POST端点,架构稍复杂
- 连接状态: 需要用sessionId关联SSE连接和POST请求
- 长连接开销: 每个Client都占用一个长连接,Server资源消耗大
- 不支持断点续传: 连接断了,之前的上下文就丢了
为什么后来被Streamable HTTP取代?
SSE模式要求Client必须先建立一个SSE长连接,然后才能通信。
这意味着:
1. 每个Client都要维持一个长连接(即使大部分时间没有数据传输)
2. 对于简单的"调一次工具就走"的场景,维持长连接太浪费
3. 负载均衡器和代理服务器对长连接的支持不够好
4. 无状态的serverless环境(如AWS Lambda)无法维持长连接
2.3.3 Streamable HTTP ------ "智能电话,按需切换"
这是MCP规范在2025年3月更新后推荐的传输方式,是SSE的升级版。
一句话理解:Client通过普通HTTP POST发送请求,Server可以选择"直接返回结果"(短连接)或"升级为SSE流式推送"(长连接)。就像打电话时,简单问题直接回答就挂了,复杂问题保持通话慢慢说。
📚 Streamable HTTP vs SSE 的核心区别
SSE模式(旧):
Client必须先建立SSE长连接 → 然后才能通信
就像:你必须先打通电话,然后才能问问题
问题:即使只问一个简单问题,也要一直保持通话
Streamable HTTP模式(新):
Client直接发HTTP POST请求 → Server决定怎么回
- 简单问题 → 直接HTTP响应返回(短连接,用完就断)
- 复杂问题 → 响应升级为SSE流,持续推送(长连接)
就像:发微信问问题
- 简单的 → 对方直接回一条消息
- 复杂的 → 对方发一连串语音消息
关键改进:
1. 不强制长连接: 简单请求可以是无状态的HTTP(对serverless友好)
2. 按需升级: 只有需要流式推送时才升级为SSE
3. 单一端点: 只需要一个URL(不像SSE需要/sse + /message两个)
4. 支持会话恢复: 通过Mcp-Session-Id头实现断点续传
Streamable HTTP的工作流程图:
┌──────────────┐ ┌──────────────┐
│ MCP Client │ │ MCP Server │
│ │ │ 端口:8080 │
│ │ │ 端点:/mcp │
│ │ │ │
│ 场景A: 简单请求(短连接,用完即断) │ │
│ │ ─── POST /mcp ───────────→ │ │
│ │ {method: "tools/list"} │ │
│ │ ←── 200 OK ────────────── │ │
│ │ {result: [...]} │ (连接断开) │
│ │ │ │
│ 场景B: 长任务(自动升级为SSE流) │ │
│ │ ─── POST /mcp ───────────→ │ │
│ │ {method: "tools/call", │ │
│ │ params: {name: │ │
│ │ "analyze_big_data"}} │ │
│ │ ←── 200 OK ────────────── │ │
│ │ Content-Type: │ │
│ │ text/event-stream │ (升级为SSE) │
│ │ ←── data: {progress: 10%} │ │
│ │ ←── data: {progress: 50%} │ (持续推送) │
│ │ ←── data: {progress: 100%} │ │
│ │ ←── data: {result: "..."} │ (完成断开) │
│ │ │ │
│ 场景C: 需要Server主动通知(可选SSE长连接) │ │
│ │ ─── GET /mcp ────────────→ │ │
│ │ ←── SSE流(持续监听通知)─── │ (可选的) │
└──────────────┘ └──────────────┘
关键区别:
- SSE模式: 必须先GET /sse建立长连接,否则无法通信
- Streamable HTTP: 直接POST /mcp就能通信,长连接是可选的
Streamable HTTP的会话管理:
📚 Session(会话)机制
问题:HTTP是无状态的,Server怎么知道多个请求来自同一个Client?
答案:通过 Mcp-Session-Id 请求头
流程:
1. Client首次请求(初始化):
POST /mcp
Body: {"method": "initialize", ...}
Server响应:
200 OK
Mcp-Session-Id: session_abc123 ← Server分配一个会话ID
Body: {"result": {"capabilities": ...}}
2. Client后续请求都带上这个ID:
POST /mcp
Mcp-Session-Id: session_abc123 ← 告诉Server"我是刚才那个Client"
Body: {"method": "tools/call", ...}
3. 好处:
- Server可以维护每个Client的状态(比如已订阅的资源)
- 连接断了可以重连(带上同一个session ID)
- 支持断点续传
⚠️ Session ID的安全问题:
你可能会想到一个问题:如果有人劫持了 Session ID,伪造身份怎么办?这个担忧完全合理!
📚 结论:确实存在安全风险,但MCP协议本身有意"不管"这件事
为什么说"有意不管"?
MCP协议的设计哲学:只定义通信格式和流程,安全问题交给传输层和部署层解决。
类比:
HTTP协议本身也没有加密、没有认证
→ 所以我们在HTTP之上加了TLS(变成HTTPS)
→ 所以我们在HTTP之上加了OAuth/JWT(认证鉴权)
MCP也是一样:
→ 协议本身只管"消息格式和交互流程"
→ 安全问题由部署时的基础设施解决
攻击场景分析:Session ID被劫持后会怎样?
1. 中间人攻击: 攻击者截获网络流量,拿到 Mcp-Session-Id
2. 日志泄露: Session ID 被记录在日志中,被攻击者获取
3. XSS攻击: 如果Client是Web应用,可能通过XSS窃取Session ID
如果攻击者拿到了Session ID:
→ 可以伪装成合法Client
→ 可以调用Server上的所有工具
→ 可以读取Server上的所有资源
→ 相当于"接管了你的Agent身份"
实际生产中的安全防护层次:
┌─────────────────────────────────────────────────────────────┐
│ 安全防护层次 │
│ │
│ 第1层: 传输加密(防中间人窃听) │
│ ───────────────────────────── │
│ 使用 HTTPS(TLS)而非 HTTP │
│ → 即使有人截获流量,也看不到Session ID的明文 │
│ → 这是最基本的,生产环境必须用HTTPS │
│ │
│ 第2层: 认证鉴权(防未授权访问) │
│ ───────────────────────────── │
│ 在MCP Server前面加认证: │
│ - API Key: 请求头带 Authorization: Bearer <token> │
│ - OAuth 2.0: 标准的授权流程 │
│ - mTLS: 双向证书认证(最安全) │
│ → 即使拿到Session ID,没有合法token也调不了 │
│ │
│ 第3层: Session安全加固 │
│ ───────────────────────────── │
│ - Session过期时间: 比如30分钟无活动就失效 │
│ - IP绑定: Session ID只能从创建时的IP使用 │
│ - 单设备绑定: 一个Session只能在一个Client上用 │
│ - Session轮转: 定期更换Session ID │
│ │
│ 第4层: 网络隔离(防外部访问) │
│ ───────────────────────────── │
│ - MCP Server部署在内网,不暴露公网 │
│ - 通过VPN/零信任网络访问 │
│ - 防火墙白名单 │
│ │
└─────────────────────────────────────────────────────────────┘
不同部署场景的安全策略:
场景1: 本地开发(Stdio模式)
风险: 几乎为零
原因: 根本不走网络,没有Session ID,进程间通信外部无法访问
措施: 不需要额外安全措施
场景2: 内网部署(团队共享MCP Server)
风险: 低
措施: HTTPS + API Key认证 + 内网隔离(不暴露公网)
场景3: 公网部署(开放给外部Client)
风险: 高
措施: HTTPS(必须)+ OAuth 2.0认证(必须)+ Session过期/IP绑定
+ 速率限制(防暴力攻击)+ 审计日志(记录谁调了什么)
和Web应用的Session安全对比:
其实这个问题和"Web应用的Session被劫持"是完全一样的:
Web应用:
浏览器Cookie里存了Session ID
→ 被XSS窃取 → 攻击者伪装成你登录
→ 防护: HTTPS + HttpOnly Cookie + CSRF Token + Session过期
MCP:
请求头里带了Mcp-Session-Id
→ 被中间人截获 → 攻击者伪装成合法Client
→ 防护: HTTPS + 认证Token + Session过期 + IP绑定
本质上是同一类问题,解决方案也类似。
MCP没有发明新的安全问题,也没有发明新的解决方案。
| 问题 | 答案 |
|---|---|
| Session ID被劫持有风险吗? | 有,和Web Session被劫持一样的风险 |
| MCP协议本身解决这个问题吗? | 不解决,协议只管通信格式 |
| 谁来解决? | 你(部署者),通过HTTPS+认证+Session策略 |
| Stdio模式有这个问题吗? | 没有,不走网络,没有Session ID |
| 生产环境最低要求? | HTTPS + 认证(API Key/OAuth) |
一句话总结:MCP 的 Session ID 安全问题和 Web 应用的 Cookie/Session 安全问题本质相同,解决方案也相同------加密传输(HTTPS)+ 身份认证(Token)+ Session 管理策略(过期/绑定/轮转)。MCP 协议本身不管安全,就像 HTTP 协议本身也不管安全一样,安全是部署层的责任。
Python代码示例(Streamable HTTP模式):
python
# === Server端 ===
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("weather-service")
@mcp.tool()
def get_weather(city: str) -> str:
"""查询天气(快速任务,直接返回)"""
return f"{city}: 晴,28°C"
@mcp.tool()
async def analyze_data(dataset: str) -> str:
"""分析大数据集(慢任务,会流式推送进度)"""
import asyncio
# 模拟耗时操作
await asyncio.sleep(5)
return f"数据集{dataset}分析完成:共1000条记录,异常率2.3%"
if __name__ == "__main__":
# 以Streamable HTTP模式运行
mcp.run(
transport="streamable-http", # 使用Streamable HTTP
host="0.0.0.0",
port=8080,
path="/mcp" # 单一端点路径
)
# 启动后,Server只暴露一个端点:
# POST /mcp → 接收所有请求(可能返回普通响应或SSE流)
# GET /mcp → 可选的SSE长连接(用于接收Server主动通知)
python
# === Client端 ===
import asyncio
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client
async def main():
# 连接远程MCP Server
server_url = "http://your-server.com:8080/mcp"
async with streamablehttp_client(server_url) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
# 快速任务 → Server直接返回(短连接行为)
result = await session.call_tool("get_weather", {"city": "北京"})
print(result.content[0].text)
# 慢任务 → Server自动升级为SSE流式推送
result = await session.call_tool("analyze_data", {"dataset": "sales_2025"})
print(result.content[0].text)
asyncio.run(main())
Streamable HTTP模式的特点:
✅ 优点:
- 灵活: 短任务用短连接,长任务自动升级为流
- Serverless友好: 不强制长连接,可以部署在Lambda/Cloud Functions
- 单一端点: 只需要一个URL,架构简单
- 会话恢复: 支持断线重连和断点续传
- 负载均衡友好: 无状态请求可以被任意路由
- 向后兼容: 可以同时支持SSE模式的Client
❌ 缺点:
- 较新: 部分老的MCP SDK可能还不支持
- 实现复杂: Server需要判断何时返回普通响应、何时升级为SSE
适用场景:
- 生产环境(推荐)
- 云原生部署
- 需要同时处理快速和慢速任务的场景
❓ 常见疑问:Server 怎么知道哪个操作需要"升级为 SSE"?
看了上面的代码,你可能会问:get_weather 是快速任务直接返回,analyze_data 是慢任务会流式推送------Server 是怎么自动判断的?
答案:Server 并不是"自动判断快慢"来决定是否升级为 SSE!
真相是:
- 框架对 get_weather 和 analyze_data 的处理方式其实是一样的
- 都是等函数执行完毕后返回结果
- 框架不会因为一个函数是 async 或执行时间长就自动"升级为SSE"
那什么时候才会真正用到 SSE 的"流式推送"能力?
→ 需要开发者主动编写流式推送的代码!
真正的流式推送:需要开发者主动调用进度 API
python
from mcp.server.fastmcp import FastMCP, Context
mcp = FastMCP("data-service")
# ❌ 这个虽然慢,但并不会自动升级为SSE流
# 它只是让Client等5秒,然后一次性返回结果
@mcp.tool()
async def analyze_data(dataset: str) -> str:
"""分析大数据集(实际上不会流式推送!)"""
import asyncio
await asyncio.sleep(5) # Client在这5秒里只能干等
return f"数据集{dataset}分析完成"
# ✅ 这个才是真正的流式推送!
# 通过 ctx.report_progress() 主动发送中间消息
@mcp.tool()
async def analyze_data_with_progress(dataset: str, ctx: Context) -> str:
"""分析大数据集(真正的流式推送进度)"""
import asyncio
# 主动发送进度通知 → 这才触发SSE流!
await ctx.report_progress(progress=0, total=100)
await asyncio.sleep(2)
await ctx.report_progress(progress=50, total=100)
await asyncio.sleep(2)
await ctx.report_progress(progress=100, total=100)
return f"数据集{dataset}分析完成:共1000条记录,异常率2.3%"
两种情况的对比:
情况1: analyze_data(没有中间消息)
─────────────────────────────────────────
Client POST /mcp → Server开始执行
(Client等待5秒...什么都收不到...)
← 最终响应: {"result": "分析完成"} ← 普通JSON响应,一次性返回
情况2: analyze_data_with_progress(有中间消息)
─────────────────────────────────────────
Client POST /mcp → Server开始执行
← SSE: {"method": "notifications/progress", "params": {"progress": 0, "total": 100}}
(等2秒)
← SSE: {"method": "notifications/progress", "params": {"progress": 50, "total": 100}}
(等2秒)
← SSE: {"method": "notifications/progress", "params": {"progress": 100, "total": 100}}
← SSE: {"result": "数据集big_data分析完成..."} ← 最终结果
只有当Server需要在最终结果之前发送中间消息时,SSE才有意义!
总结:谁决定用不用 SSE?
| 层级 | 决定什么 |
|---|---|
| MCP协议规范 | 规定了 Server 可以选择返回 JSON 或 SSE |
| 框架(FastMCP) | 如果有中间消息要发(如 progress),就用 SSE;否则用普通 JSON |
| 开发者 | 通过调用 ctx.report_progress() 等 API 主动触发中间消息 |
| 函数本身的快慢 | ❌ 不是判断依据! 慢函数如果没有中间消息,也不需要 SSE |
一句话总结:
SSE不是"慢就自动升级",而是"有中间消息要发才用"。
没有中间消息 → 普通JSON响应(不管函数执行多慢)
有中间消息(进度/日志/通知)→ SSE流式响应
这个选择由框架根据"是否有中间消息需要推送"来决定,
而"是否有中间消息"取决于开发者是否主动调用了 ctx.report_progress() 等API。
2.3.4 三种传输方式完整对比
| 维度 | Stdio | SSE(旧) | Streamable HTTP(新) |
|---|---|---|---|
| 通信方式 | 进程间管道 | HTTP POST + SSE长连接 | HTTP POST(可升级为SSE) |
| 部署位置 | 必须同一台机器 | 可远程 | 可远程 |
| 连接模型 | 长连接(管道) | 强制长连接 | 按需长连接 |
| 端点数量 | 无(管道) | 2个(/sse + /message) | 1个(/mcp) |
| 并发能力 | 一对一 | 一对多 | 一对多 |
| Serverless支持 | ❌ | ❌(需要长连接) | ✅ |
| 会话恢复 | ❌ | ❌ | ✅(Mcp-Session-Id) |
| Server主动推送 | ✅(通过stdout) | ✅(通过SSE) | ✅(通过SSE或GET /mcp) |
| 规范状态 | 持续支持 | 已废弃(但仍广泛使用) | 当前推荐 |
| 适用场景 | 本地开发 | 早期远程部署 | 生产环境 |
发展历程:
2024.11 MCP发布 → 支持 Stdio + SSE
Stdio用于本地,SSE用于远程
2025.03 MCP规范更新 → 新增 Streamable HTTP
SSE被标记为"deprecated"(废弃)
Streamable HTTP成为推荐的远程传输方式
2025.06 现状
- Stdio: 本地开发仍然用它(不会被废弃)
- SSE: 大量现存Server仍在用(需要了解)
- Streamable HTTP: 新项目推荐用它
类比进化:
Stdio = 面对面说话(永远需要)
SSE = 固定电话(能用但有局限)
Streamable HTTP = 智能手机(灵活、功能全)
重要:对Client代码来说,三种模式几乎透明!
python
# 唯一的区别就是连接方式不同,后续调用工具的代码完全一样:
# ① Stdio模式
async with stdio_client(server_params) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
result = await session.call_tool("get_weather", {"city": "北京"}) # ← 一样
# ② SSE模式(旧)
async with sse_client("http://server:8080/sse") as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
result = await session.call_tool("get_weather", {"city": "北京"}) # ← 一样
# ③ Streamable HTTP模式(新)
async with streamablehttp_client("http://server:8080/mcp") as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
result = await session.call_tool("get_weather", {"city": "北京"}) # ← 一样
# 这就是协议的好处:底层传输方式变了,上层使用方式不变!
# 类比:不管你用WiFi、4G还是网线上网,打开浏览器的方式都一样。
实际开发中怎么选?
本地开发/调试 → Stdio
你在本地写代码、调试MCP Server
Cursor/Claude Desktop连接本地Server
快速迭代,不需要部署
远程部署(新项目) → Streamable HTTP
Server部署到云服务器
团队所有人的Agent都连接这个Server
加上认证、监控、日志
支持serverless部署
远程部署(维护老项目) → SSE
如果你接手的项目已经在用SSE
不需要急着迁移,SSE仍然能正常工作
新功能可以逐步迁移到Streamable HTTP
三、MCP协议规范
- 协议规范原文:https://spec.modelcontextprotocol.io
- GitHub仓库:https://github.com/modelcontextprotocol/specification
- 官方文档:https://modelcontextprotocol.io/docs
- SDK仓库 :
- Python SDK: https://github.com/modelcontextprotocol/python-sdk
- TypeScript SDK: https://github.com/modelcontextprotocol/typescript-sdk
3.1 协议基础:JSON-RPC 2.0
一句话理解:MCP的所有通信都使用JSON-RPC 2.0格式------这是一种"用JSON格式来远程调用函数"的标准。
📚 什么是JSON-RPC?
JSON = JavaScript Object Notation(一种数据格式,你肯定见过)
RPC = Remote Procedure Call(远程过程调用)
合起来:用JSON格式来"远程调用函数"
为什么MCP选择JSON-RPC 2.0?
1. 简单: 就是JSON,人类可读,调试方便
2. 标准化: 2.0是成熟的规范,有明确的请求/响应/错误格式
3. 语言无关: 任何语言都能处理JSON
4. 轻量: 不像gRPC需要protobuf编译,不像SOAP那么臃肿
类比:
如果MCP是"打电话",那JSON-RPC就是"说话的语法规则"
双方约定好:我说什么格式的话,你回什么格式的话
JSON-RPC 2.0的三种消息类型:
MCP中所有通信都是以下三种消息之一:
1. Request(请求)------ Client问Server问题
2. Response(响应)------ Server回答Client的问题
3. Notification(通知)------ 单向通知,不需要回复
消息类型1:Request(请求)
Client向Server发送请求,期望得到回复。
json
{
"jsonrpc": "2.0", // 固定值,表示使用JSON-RPC 2.0协议
"id": 1, // 请求ID,用于匹配响应(Server回复时会带上同一个id)
"method": "tools/call", // 要调用的方法名(类似函数名)
"params": { // 方法的参数(类似函数参数)
"name": "query_database",
"arguments": {
"sql": "SELECT * FROM orders WHERE id = 'ORD001'"
}
}
}
各字段解释:
"jsonrpc": "2.0" → 协议版本,永远是"2.0",不要改
"id": 1 → 请求编号,Client自己递增(1, 2, 3...)
Server回复时会带上同一个id,这样Client就知道
"这个回复是对我第1个请求的回答"
"method" → 你想调用什么方法(MCP定义了一组标准方法)
"params" → 方法的参数(不同方法参数不同)
消息类型2:Response(响应)
Server回复Client的请求。分为"成功"和"失败"两种。
json
// ✅ 成功响应
{
"jsonrpc": "2.0",
"id": 1, // 和请求的id对应!Client靠这个匹配
"result": { // 成功时返回result字段
"content": [
{
"type": "text",
"text": "{\"id\": \"ORD001\", \"status\": \"已签收\", \"amount\": 299.0}"
}
]
}
}
// ❌ 失败响应
{
"jsonrpc": "2.0",
"id": 1, // 同样要带上请求的id
"error": { // 失败时返回error字段(没有result)
"code": -32602, // 错误码(JSON-RPC标准定义了一些错误码)
"message": "工具不存在: query_database2"
}
}
JSON-RPC标准错误码:
-32700 → Parse error(JSON格式错误,解析失败)
-32600 → Invalid Request(请求格式不对)
-32601 → Method not found(方法不存在)
-32602 → Invalid params(参数不对)
-32603 → Internal error(Server内部错误)
消息类型3:Notification(通知)
单向消息,发送方不期望收到回复。注意:没有id字段!
json
// Server通知Client:工具列表发生了变化
{
"jsonrpc": "2.0",
"method": "notifications/tools/list_changed"
// 没有id字段!因为不需要回复
}
// Client通知Server:初始化完成
{
"jsonrpc": "2.0",
"method": "notifications/initialized"
// 没有id字段!
}
Request vs Notification 的区别:
Request: 有id → 期望回复 → "我问你答"
Notification: 没id → 不期望回复 → "我通知你一声"
什么时候用Notification?
- 状态变更通知("我的工具列表更新了,你重新获取一下")
- 进度更新("当前进度50%")
- 心跳保活("我还活着")
3.2 MCP的生命周期(完整通信流程)
一次完整的MCP通信,从连接建立到断开,经历以下阶段:
┌─────────────────────────────────────────────────────────────────┐
│ MCP完整生命周期 │
│ │
│ 阶段1: 初始化(握手) │
│ ───────────────── │
│ Client → Server: initialize请求(告诉Server我支持什么能力) │
│ Server → Client: initialize响应(告诉Client我支持什么能力) │
│ Client → Server: initialized通知(确认初始化完成) │
│ │
│ 阶段2: 能力发现 │
│ ───────────────── │
│ Client → Server: tools/list(你有哪些工具?) │
│ Server → Client: 返回工具列表 │
│ Client → Server: resources/list(你有哪些资源?) │
│ Server → Client: 返回资源列表 │
│ │
│ 阶段3: 正常使用(可以反复进行) │
│ ───────────────── │
│ Client → Server: tools/call(调用某个工具) │
│ Server → Client: 返回工具执行结果 │
│ Client → Server: resources/read(读取某个资源) │
│ Server → Client: 返回资源内容 │
│ ...(可以调用多次) │
│ │
│ 阶段4: 关闭连接 │
│ ───────────────── │
│ Client关闭连接 / Server关闭连接 │
│ │
└─────────────────────────────────────────────────────────────────┘
阶段1详解:初始化握手
json
// ① Client → Server: "你好,我是XXX,我支持这些能力"
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-03-26", // Client支持的MCP协议版本
"capabilities": { // Client支持的能力
"roots": {"listChanged": true} // Client支持根目录变更通知
},
"clientInfo": { // Client的身份信息
"name": "my-agent",
"version": "1.0.0"
}
}
}
// ② Server → Client: "你好,我是XXX,我支持这些能力"
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2025-03-26", // Server支持的协议版本
"capabilities": { // Server支持的能力
"tools": {"listChanged": true}, // 我有工具,且工具列表可能变化
"resources": {}, // 我有资源
"prompts": {} // 我有提示模板
},
"serverInfo": { // Server的身份信息
"name": "order-service",
"version": "2.0.0"
}
}
}
// ③ Client → Server: "好的,初始化完成,我们可以开始了"
{
"jsonrpc": "2.0",
"method": "notifications/initialized"
// 注意:这是Notification,没有id
}
为什么需要握手?
1. 版本协商: 确认双方使用的协议版本兼容
2. 能力交换: Client知道Server有什么能力(有工具?有资源?)
Server知道Client支持什么(支持通知?支持采样?)
3. 身份确认: 双方知道对方是谁(名字、版本)
类比: 就像两个人第一次见面:
"你好,我是张三,我会做饭和开车"
"你好,我是李四,我会修电脑和弹吉他"
"好的,那我们开始合作吧"
3.3 核心方法详解
MCP协议定义了一组完整的标准方法,覆盖了从连接建立到功能调用的所有场景。以下是 MCP 协议所有方法的完整清单:
| 分类 | 方法名 | 方向 | 功能简介 |
|---|---|---|---|
| 生命周期 | initialize |
Client → Server | 发起连接握手,交换双方支持的协议版本和能力声明 |
notifications/initialized |
Client → Server | 通知Server初始化已完成,可以开始正常通信 | |
ping |
双向 | 心跳检测,确认对方是否仍然存活 | |
| 工具(Tools) | tools/list |
Client → Server | 列出Server提供的所有可用工具及其参数定义 |
tools/call |
Client → Server | 调用指定工具并传入参数,获取执行结果 | |
notifications/tools/list_changed |
Server → Client | 通知Client工具列表已变更,需要重新获取 | |
| 资源(Resources) | resources/list |
Client → Server | 列出Server提供的所有可用资源 |
resources/read |
Client → Server | 读取指定URI的资源内容 | |
resources/templates/list |
Client → Server | 列出所有资源模板(带参数的动态URI模式) | |
resources/subscribe |
Client → Server | 订阅指定资源的变更通知 | |
resources/unsubscribe |
Client → Server | 取消订阅指定资源的变更通知 | |
notifications/resources/list_changed |
Server → Client | 通知Client资源列表已变更 | |
notifications/resources/updated |
Server → Client | 通知Client已订阅的某个资源内容已更新 | |
| 提示模板(Prompts) | prompts/list |
Client → Server | 列出Server提供的所有提示模板 |
prompts/get |
Client → Server | 获取指定模板填入参数后的完整prompt内容 | |
notifications/prompts/list_changed |
Server → Client | 通知Client提示模板列表已变更 | |
| 采样(Sampling) | sampling/createMessage |
Server → Client | Server请求Client调用LLM生成内容(反向调用) |
| 根目录(Roots) | roots/list |
Server → Client | Server请求Client提供其可访问的根目录列表 |
notifications/roots/list_changed |
Client → Server | 通知Server根目录列表已变更 | |
| 日志(Logging) | logging/setLevel |
Client → Server | 设置Server的日志输出级别(debug/info/warn/error等) |
notifications/message |
Server → Client | Server向Client推送日志消息 | |
| 补全(Completion) | completion/complete |
Client → Server | 请求Server对资源URI或prompt参数进行自动补全建议 |
| 进度与取消 | notifications/progress |
双向 | 报告长时间操作的进度(如百分比) |
notifications/cancelled |
双向 | 通知对方某个请求已被取消 |
💡 方向说明:
Client → Server:由Host/Client主动发起请求
Server → Client:由Server主动发起(通知或反向请求)
双向:任何一方都可以发起
💡 方法命名规则:
xxx/list:列出某类资源的清单
xxx/call或xxx/get:执行具体操作
notifications/xxx:单向通知(无需响应),用于事件推送
notifications/xxx/list_changed:某类资源的列表发生了变化
下面我们对最常用的方法进行详细讲解:
工具相关方法
json
// ─── tools/list: 列出所有可用工具 ───
// Client → Server
{
"jsonrpc": "2.0", "id": 2,
"method": "tools/list"
}
// Server → Client(返回工具列表)
{
"jsonrpc": "2.0", "id": 2,
"result": {
"tools": [
{
"name": "query_order", // 工具名称
"description": "查询订单信息", // 工具描述(会展示给LLM看)
"inputSchema": { // 参数的JSON Schema定义
"type": "object",
"properties": {
"order_id": {
"type": "string",
"description": "订单编号"
}
},
"required": ["order_id"]
}
},
{
"name": "create_refund",
"description": "为订单创建退款申请",
"inputSchema": {
"type": "object",
"properties": {
"order_id": {"type": "string", "description": "订单编号"},
"reason": {"type": "string", "description": "退款原因"}
},
"required": ["order_id", "reason"]
}
}
]
}
}
json
// ─── tools/call: 调用指定工具 ───
// Client → Server
{
"jsonrpc": "2.0", "id": 3,
"method": "tools/call",
"params": {
"name": "query_order", // 要调用哪个工具
"arguments": { // 传什么参数
"order_id": "ORD001"
}
}
}
// Server → Client(返回执行结果)
{
"jsonrpc": "2.0", "id": 3,
"result": {
"content": [ // 结果内容(数组,可以有多个)
{
"type": "text", // 内容类型:text/image/resource
"text": "{\"status\": \"已签收\", \"amount\": 299.0, \"item\": \"蓝牙耳机\"}"
}
],
"isError": false // 是否执行出错
}
}
资源相关方法
json
// ─── resources/list: 列出所有可用资源 ───
// Server → Client
{
"jsonrpc": "2.0", "id": 4,
"result": {
"resources": [
{
"uri": "docs://readme", // 资源的唯一标识(URI格式)
"name": "项目README", // 资源名称
"description": "项目说明文档", // 资源描述
"mimeType": "text/plain" // 内容类型
},
{
"uri": "docs://api/orders",
"name": "订单API文档",
"description": "订单相关接口文档",
"mimeType": "text/plain"
}
]
}
}
// ─── resources/read: 读取指定资源 ───
// Client → Server
{
"jsonrpc": "2.0", "id": 5,
"method": "resources/read",
"params": {
"uri": "docs://readme" // 要读取哪个资源
}
}
// Server → Client
{
"jsonrpc": "2.0", "id": 5,
"result": {
"contents": [
{
"uri": "docs://readme",
"mimeType": "text/plain",
"text": "# 电商订单系统\n## 技术栈: Python + FastAPI..."
}
]
}
}
提示模板相关方法
json
// ─── prompts/list: 列出所有提示模板 ───
{
"jsonrpc": "2.0", "id": 6,
"result": {
"prompts": [
{
"name": "code_review",
"description": "代码审查提示模板",
"arguments": [ // 模板需要的参数
{"name": "language", "description": "编程语言", "required": true},
{"name": "code", "description": "待审查代码", "required": true}
]
}
]
}
}
// ─── prompts/get: 获取指定模板(填入参数后的完整prompt)───
// Client → Server
{
"jsonrpc": "2.0", "id": 7,
"method": "prompts/get",
"params": {
"name": "code_review",
"arguments": {
"language": "Python",
"code": "def add(a, b): return a + b"
}
}
}
// Server → Client(返回填充好参数的完整prompt)
{
"jsonrpc": "2.0", "id": 7,
"result": {
"messages": [
{
"role": "user",
"content": {
"type": "text",
"text": "你是一位资深的Python开发工程师,请对以下代码进行审查...\n\ndef add(a, b): return a + b"
}
}
]
}
}
3.4 完整通信示例(从连接到调用工具)
把上面的知识串起来,看一个完整的通信过程:
场景:你的Agent通过MCP连接到一个"订单查询"Server,然后调用工具查询订单
时间线:
─────────────────────────────────────────────────────────────────
[T1] Client → Server: initialize
"你好,我是my-agent v1.0"
[T2] Server → Client: initialize响应
"你好,我是order-service v2.0,我有tools和resources"
[T3] Client → Server: initialized通知
"好的,初始化完成"
[T4] Client → Server: tools/list
"你有哪些工具?"
[T5] Server → Client: tools/list响应
"我有query_order和create_refund两个工具"
[T6] Client把工具列表转成LLM认识的格式,传给LLM
(这一步不走MCP协议,是应用程序内部逻辑)
[T7] LLM决定调用query_order(order_id="ORD001")
(这一步也不走MCP协议,是LLM的Function Calling输出)
[T8] Client → Server: tools/call
"请执行query_order,参数是{order_id: 'ORD001'}"
[T9] Server → Client: tools/call响应
"结果是:已签收,299元,蓝牙耳机"
[T10] Client把结果传回给LLM,LLM生成最终回答
(这一步不走MCP协议)
─────────────────────────────────────────────────────────────────
注意:T6、T7、T10不走MCP协议!
MCP只管"应用程序 ↔ 工具Server"之间的通信
"应用程序 ↔ LLM"之间走的是各家模型的API(OpenAI API / Anthropic API等)
3.5 协议版本与兼容性
MCP协议版本历史:
2024-11-05 → 初始版本(支持Stdio + SSE)
2025-03-26 → 当前最新版本(新增Streamable HTTP,SSE标记废弃)
版本协商规则:
Client和Server在initialize时交换版本号
如果版本不兼容,Server应该返回错误,拒绝连接
实际中大部分SDK会自动处理版本协商,你不需要手动管
四、MCP Server开发实战
4.1 用Python SDK开发MCP Server
python
# 安装: pip install mcp
from mcp.server.fastmcp import FastMCP
# 创建MCP Server
mcp = FastMCP("order-service")
# 定义工具
@mcp.tool()
def query_order(order_id: str) -> str:
"""查询订单信息
Args:
order_id: 订单编号
"""
orders = {
"ORD001": {"status": "已签收", "amount": 299.0, "item": "蓝牙耳机"},
"ORD002": {"status": "配送中", "amount": 89.5, "item": "手机壳"},
}
import json
order = orders.get(order_id, {"status": "未找到"})
return json.dumps(order, ensure_ascii=False)
@mcp.tool()
def create_refund(order_id: str, reason: str) -> str:
"""为订单创建退款申请
Args:
order_id: 订单编号
reason: 退款原因
"""
return f"退款申请已创建: 订单{order_id}, 原因: {reason}"
# 定义资源
@mcp.resource("order://{order_id}")
def get_order_resource(order_id: str) -> str:
"""获取订单资源"""
orders = {
"ORD001": "订单ORD001: 蓝牙耳机, 299元, 已签收",
"ORD002": "订单ORD002: 手机壳, 89.5元, 配送中",
}
return orders.get(order_id, "订单不存在")
# 定义提示模板
@mcp.prompt()
def refund_assistant(order_id: str) -> str:
"""退款助手提示模板"""
return f"""你是一个退款处理助手。
请查询订单{order_id}的信息,判断是否符合退款条件。
如果符合,使用create_refund工具创建退款申请。"""
# 运行
if __name__ == "__main__":
mcp.run()
4.2 运行MCP Server
bash
# Stdio模式(本地开发)
python order_mcp_server.py
# 在Claude Desktop等MCP Client中配置:
# claude_desktop_config.json
{
"mcpServers": {
"order-service": {
"command": "python",
"args": ["/path/to/order_mcp_server.py"]
}
}
}
4.3 用Go开发MCP Server
go
// Go SDK: go get github.com/mark3labs/mcp-go
package main
import (
"context"
"encoding/json"
"fmt"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
func main() {
s := server.NewMCPServer(
"order-service",
"1.0.0",
)
// 添加工具
tool := mcp.NewTool("query_order",
mcp.WithDescription("查询订单信息"),
mcp.WithString("order_id",
mcp.Required(),
mcp.Description("订单编号"),
),
)
s.AddTool(tool, func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
orderID, _ := req.Params.Arguments["order_id"].(string)
result := map[string]string{
"ORD001": "已签收, 299元, 蓝牙耳机",
}
order, ok := result[orderID]
if !ok {
order = "订单不存在"
}
return mcp.NewToolResultText(order), nil
})
// 运行
if err := server.ServeStdio(s); err != nil {
fmt.Printf("Server error: %v\n", err)
}
}
五、MCP vs Function Calling
| 维度 | Function Calling | MCP |
|---|---|---|
| 定义方式 | 每次请求内嵌定义 | 独立Server暴露 |
| 可复用性 | 低(每次都要传) | 高(Server可被多个Client复用) |
| 认证 | 应用自己处理 | 协议层支持 |
| 发现 | 应用预先知道 | Client动态发现 |
| 状态 | 无状态 | 支持有状态连接 |
| 生态 | 各家不互通 | 标准化互通 |
| 适用 | 简单工具调用 | 生产级工具生态 |
关系:MCP不是替代Function Calling,而是在其之上建立了标准化层。两者作用于不同的环节:
┌─────────────────────────────────────────────────────────────────────────────┐
│ 完整调用链路 │
└─────────────────────────────────────────────────────────────────────────────┘
① Function Calling ② MCP协议
(LLM ↔ 应用程序之间) (应用程序 ↔ 外部工具之间)
┌──────────────────────┐ ┌──────────────────────────┐
│ │ │ │
┌──────┐ ┌─▼──────────┐ ┌──────▼───────┐ │ ┌────────────┐ ┌─────▼──────┐
│ │ │ │ │ │ │ │ │ │ │
│ 用户 │───▶│ LLM模型 │───▶│ AI应用程序 │──┼─▶│ MCP Client │───▶│ MCP Server │──▶ 实际API
│ │ │(GPT/Claude)│◀───│ (Agent框架) │◀─┼──│ │◀───│ │ (数据库/
└──────┘ │ │ │ │ │ └────────────┘ └────────────┘ 网络/文件)
└────────────┘ └──────────────┘ │
│ │ │
└──────────────────────┘ └──────────────────────────┘
● LLM通过Function Calling告诉应用程序: ● 应用程序通过MCP协议连接外部工具:
"我想调用 get_weather 工具" - 自动发现Server有哪些工具
→ 应用程序负责实际执行 - 用标准格式调用工具
- 一个Client可连接多个Server
一句话总结:Function Calling 解决的是"LLM如何表达调用意图",MCP 解决的是"应用程序如何连接和管理外部工具"。
📝 作业
作业1:开发一个简单的MCP Server
实现一个"计算器MCP Server",支持加减乘除和幂运算。
参考答案:
python
# calculator_mcp_server.py
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("calculator")
@mcp.tool()
def add(a: float, b: float) -> str:
"""加法运算
Args:
a: 第一个数
b: 第二个数
"""
return str(a + b)
@mcp.tool()
def subtract(a: float, b: float) -> str:
"""减法运算
Args:
a: 被减数
b: 减数
"""
return str(a - b)
@mcp.tool()
def multiply(a: float, b: float) -> str:
"""乘法运算
Args:
a: 第一个数
b: 第二个数
"""
return str(a * b)
@mcp.tool()
def divide(a: float, b: float) -> str:
"""除法运算
Args:
a: 被除数
b: 除数
"""
if b == 0:
return "错误: 除数不能为零"
return str(a / b)
@mcp.tool()
def power(base: float, exponent: float) -> str:
"""幂运算
Args:
base: 底数
exponent: 指数
"""
return str(base ** exponent)
if __name__ == "__main__":
mcp.run()
作业2:用MCP Client测试你的Server
python
# test_mcp_client.py
import asyncio
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
async def test_calculator():
server_params = StdioServerParameters(
command="python",
args=["calculator_mcp_server.py"],
)
async with stdio_client(server_params) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
# 列出可用工具
tools = await session.list_tools()
print("可用工具:")
for tool in tools.tools:
print(f" - {tool.name}: {tool.description}")
# 调用工具
result = await session.call_tool("add", {"a": 3, "b": 5})
print(f"\n3 + 5 = {result.content[0].text}")
result = await session.call_tool("multiply", {"a": 4, "b": 7})
print(f"4 × 7 = {result.content[0].text}")
result = await session.call_tool("power", {"base": 2, "exponent": 10})
print(f"2^10 = {result.content[0].text}")
asyncio.run(test_calculator())
下一篇文章见:AI系列文章导航目录-持续更新中