15-大模型智能体开发工程师:深度学习MCP协议(Model Context Protocol)

系列文章导航: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协议规范

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/callxxx/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系列文章导航目录-持续更新中

相关推荐
程序员佳佳1 小时前
深度解析:向量引擎如何影响AI内容收录?附3个月实测数据
人工智能·gpt·自动化·ai写作·codex
feng14561 小时前
OpenSREClaw - AI 本体论思维
运维·人工智能
༒࿈南林࿈༒1 小时前
国家医保局 API 加密体系逆向全记录——SM2签名 + SM4加解密 + SHA256 头签名
爬虫·大模型应用·mcp·skills
zhangxingchao1 小时前
AI应用开发八:RAG相关技术总结
前端·人工智能·后端
码农小旋风1 小时前
国内使用 Claude 的 5 种路径:网页、订阅、API 和企业方案怎么选
人工智能·chatgpt
清水寺小和尚1 小时前
MCP 协议拆解:从 JSON-RPC 信封到 Agent 全链路
人工智能
机器之心2 小时前
当Token飙到天文数字,高通用「计算连续体」重搭智能体新基建
人工智能·openai
weixin_468466852 小时前
液态神经网络新手入门与实战指南
人工智能·深度学习·神经网络·ai·机器视觉·液态神经网络
机器之心2 小时前
一夜之间,ChatGPT与Codex合并了
人工智能·openai