云计算百科
云计算领域专业知识百科平台

深入理解 AI Agent · MCP 子系列 #01:MCP 协议全解—从消息格式到传输层的完整拆解


导读:2024 年 11 月,Anthropic 发布了 MCP(Model Context Protocol)——一个用于标准化 LLM 应用与外部工具、数据源之间通信的开放协议。半年后,MCP 已经成为 AI Agent 领域的事实标准:OpenAI、Google、LangChain、Spring AI 等主流平台纷纷接入。MCP 不是又一个 RPC 框架,而是专为大模型交互设计的"能力接口规范"——它定义的不是工具怎么实现,而是工具怎么被发现、被理解、被调用。本文将从协议层面完整拆解 MCP 的设计逻辑,帮读者建立对 MCP 的系统认知。


一、从 Function Calling 到 MCP:工具调用的标准化之路

1.1 碎片化的工具调用现状

在 MCP 出现之前,LLM 调用外部工具这件事已经存在了好几年,但一直是"能用但很碎"的状态。每个 LLM 厂商都有一套自己的工具调用格式:

厂商工具调用格式参数 Schema
OpenAI function JSON Schema(自定义子集)
Anthropic tool_use JSON Schema
DashScope(阿里) tool_use JSON Schema(略有差异)
Google Gemini function_declarations OpenAPI 3.0 子集

参数 Schema 的格式、错误码定义、响应结构各不相同。开发者为每个平台维护一套适配层,同一套工具代码无法跨平台复用。

这种碎片化带来三个具体问题:

工具碎片化。 一个数据库查询工具,在 LangChain 中可以工作,但给 Spring AI 用就需要重新包装。写一个搜索工具,给 Claude 用一套代码,给 GPT 用又一套。代码重复率极高。

生态割裂。 工具开发者无法"写一次,到处用"。没有人有动力为每个平台各写一套适配,导致工具生态无法形成正向飞轮。

能力发现缺失。 LLM 在运行时不知道可用工具有哪些、每个工具的参数格式是什么。传统做法是在 prompt 中手动拼接工具描述,随着工具数量增长,维护成本急剧上升。

1.2 Function Calling ≠ MCP

Function Calling 和 Tool Use 解决的是"LLM 怎么表达想调用什么工具"——属于 LLM 的输出格式规范。MCP 解决的是更上层的问题:"工具在哪里、怎么发现、怎么调用、怎么跨平台共享"——属于完整的通信协议。

维度Function CallingMCP
层次 LLM 输出格式 通信协议
标准化程度 厂商锁定(OpenAI / Anthropic 各一套) 跨厂商开放标准
能力发现 无,需手动传入工具列表 内置能力协商机制,运行时动态发现
生态复用 工具绑定特定平台 工具一次开发,任何 MCP Client 可调用
传输方式 不涉及(由 HTTP API 承载) 支持 stdio / SSE / Streamable HTTP

类比来看:HTTP 定义了 Web 服务之间怎么对话,gRPC 定义了微服务之间怎么对话,而 MCP 定义的是 LLM Agent 和工具之间的对话方式。Function Calling 是"LLM 说了什么",MCP 是"整条通信链路怎么建起来"。两者是互补关系,不是替代关系。


二、核心架构:Host / Client / Server 三角关系

2.1 三个核心角色

MCP 协议定义了三个核心角色,形成一个清晰的三角关系:

角色职责示例
Host 托管 Client 的宿主应用,面向用户的入口 Claude Desktop、VS Code、IDE 插件
Client 与 Server 建立连接,发起工具调用请求 LLM 应用、Agent 框架中的 MCP 模块
Server 提供 Tools / Resources / Prompts 的服务端 搜索工具、数据库工具、文件系统工具

这三者的关系是:Host 是用户直接接触的应用程序,它内部嵌入了一个或多个 Client。每个 Client 可以与一个或多个 Server 建立连接,聚合它们的工具能力。

以 Claude Desktop 为例:

  • Claude Desktop 是 Host——用户看到和操作的应用
  • 它内部运行着一个 MCP Client 模块
  • 这个 Client 可以同时连接一个文件系统 Server、一个搜索 Server、一个数据库 Server
  • 当 LLM 需要调用工具时,Client 负责将请求路由到正确的 Server,再把结果返回给 LLM

┌───────────────────────────────────────────────────────────┐
│ Host(用户可见) │
│ ┌────────────────────────────────────────────────────┐ │
│ │ MCP Client(协议引擎) │ │
│ │ ┌──────────┼──────────┐ │ │
│ │ ▼ ▼ ▼ │ │
│ │ ┌──────────┐┌──────────┐┌──────────┐ │ │
│ │ │ Server A ││ Server B ││ Server C │ │ │
│ │ │ 搜索工具 ││ 数据库 ││ 文件系统 │ │ │
│ │ └──────────┘└──────────┘└──────────┘ │ │
│ └────────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────┘

2.2 解耦的关键价值

这种设计的关键价值在于解耦:

  • 工具开发者只需要写一个 Server,不用关心 Host 是什么
  • Host 开发者只需要实现 Client 协议,不用关心后端有哪些工具
  • LLM 只与 Client 交互,不感知 Server 的拓扑——Server 可以独立部署、独立扩缩容、独立发布

Client 和 Server 之间的通信基于 JSON-RPC 2.0 协议,通过可插拔的传输层(stdio / SSE / Streamable HTTP)承载。协议的设计将"消息格式"和"传输方式"完全解耦——同一条 JSON-RPC 消息可以走标准输入输出管道,也可以走 HTTP 长连接。


三、JSON-RPC 2.0:MCP 的消息格式

3.1 为什么选择 JSON-RPC 2.0

MCP 的底层通信基于 JSON-RPC 2.0 标准。选择 JSON-RPC 2.0 并非偶然:

对比维度JSON-RPC 2.0gRPCREST
编译要求 无需 proto 编译 需要 proto + 代码生成 无需
双向通信 原生支持 notifications 需要 streaming 配置 不支持
批量请求 原生支持 batching 不支持 不支持
包体大小 极小(纯 JSON) 中等(二进制编码) 较大
跨语言支持 几乎所有语言 主流语言 所有语言
调试友好度 高(人类可读) 低(二进制)

JSON-RPC 2.0 在轻量性和功能完备性之间取得了很好的平衡,特别适合 LLM Agent 这种需要频繁双向通信的场景。

3.2 三种消息类型

MCP 中的消息分为三种类型:请求(Request)、响应(Response) 和通知(Notification)。

请求消息(Request)

Client 向 Server 发起调用时发送请求,必须包含 id 字段用于匹配响应:

{
"jsonrpc": "2.0",
"id": "request-1",
"method": "tools/call",
"params": {
"name": "searchDocs",
"arguments": {
"query": "MCP 协议规范",
"topK": 5
}
}
}

各字段说明:

  • jsonrpc: 固定值 "2.0",标识协议版本
  • id: 请求唯一标识,可以是字符串或整数,用于匹配响应
  • method: 调用方法名,遵循 namespace/action 命名空间格式
  • params: 方法参数对象,arguments 内嵌工具的实际入参
响应消息(Response)

Server 返回结果,id 与请求一一对应:

{
"jsonrpc": "2.0",
"id": "request-1",
"result": {
"content": [
{
"type": "text",
"text": "MCP 协议使用 JSON-RPC 2.0 作为消息格式,设计目标是轻量、双向、跨平台…"
}
]
}
}

如果出现错误,响应用 error 字段替代 result(两者互斥,不能同时出现):

{
"jsonrpc": "2.0",
"id": "request-1",
"error": {
"code": -32602,
"message": "Invalid params: query is required",
"data": {
"field": "query",
"reason": "missing required parameter"
}
}
}

工程提示:error.data 是可选的附加数据字段。在工程实践中,建议将调试信息(如参数校验失败的字段名、堆栈摘要)放在 data 中,而不是塞进 message 字符串。这样 Client 端可以做结构化的错误处理,而不是依赖字符串解析。

通知消息(Notification)

通知是没有 id 的单向消息,不需要响应。用于 Server 主动推送事件,例如工具列表变更:

{
"jsonrpc": "2.0",
"method": "notifications/tools/list_changed"
}

注意:通知没有 id 字段,这是它和请求的核心区别。Server 发送通知后不会收到任何确认——这是设计上的取舍,追求低延迟而非可靠性。

3.3 方法命名空间

MCP 定义了标准的 method 命名空间,所有方法名遵循 namespace/action 格式:

方法名方向用途
initialize Client → Server 初始化握手
initialized Client → Server 握手完成确认(通知)
tools/list Client → Server 列出所有工具
tools/call Client → Server 调用指定工具
resources/list Client → Server 列出所有资源
resources/read Client → Server 读取指定资源
resources/subscribe Client → Server 订阅资源变更
prompts/list Client → Server 列出所有 Prompt 模板
prompts/get Client → Server 获取指定 Prompt
notifications/tools/list_changed Server → Client 工具列表已变更(通知)
notifications/resources/list_changed Server → Client 资源列表已变更(通知)
notifications/cancelled 双向 取消正在执行的请求

3.4 标准错误码

MCP 沿用了 JSON-RPC 2.0 的标准错误码,并在此基础上扩展了部分 MCP 专用错误码:

错误码含义常见场景
-32700 Parse error JSON 格式错误,如缺少引号、括号不匹配
-32600 Invalid Request 请求格式不符合 MCP 规范
-32601 Method not found 调用了 Server 不支持的方法
-32602 Invalid params 工具参数不符合 JSON Schema
-32603 Internal error Server 内部异常
-32000 ~ -32099 Server error(保留段) Server 自定义错误(MCP 预留)

踩坑记录:在实际开发中,-32602(Invalid params)是最常见的错误。原因通常是 LLM 生成的参数类型与 JSON Schema 定义不匹配——比如 Schema 要求 integer,LLM 传了 "5"(字符串)。建议在 Server 端做宽松的类型转换,而不是直接拒绝。


四、传输层设计:stdio / SSE / Streamable HTTP

MCP 协议将「消息格式」和「传输方式」解耦,支持多种传输层。不同的传输方式适用于不同的部署场景。

4.1 stdio:本地进程通信

Client 启动 Server 作为子进程,通过 stdin 写入请求、通过 stdout 读取响应,消息以换行符分隔。

Client Server (子进程)
│ │
│──── stdin (JSON-RPC) ────────►│
│ │
│◄─── stdout (JSON-RPC) ───────│
│ │

配置示例(Claude Desktop 的 mcp-servers-config.json):

{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/dir"]
},
"code-review": {
"command": "java",
"args": ["-jar", "/opt/mcp-servers/code-review-server.jar"],
"env": {
"JAVA_OPTS": "-Xmx512m"
}
}
}
}

优点:零网络开销、天然进程隔离、无需端口分配。

缺点:无法跨机器通信、不支持多 Client 共享、Server 崩溃需要 Client 重新启动进程。

典型场景:Claude Desktop 等本地应用通过 command: npx 启动本地 MCP Server。

踩坑记录:stdio 模式下,Server 进程的 stderr 输出会被 Host 捕获并作为日志记录。如果 Server 在 stderr 中输出大量调试信息,可能影响 Host 的性能。生产环境中,建议将 Server 的日志输出到独立文件,而非 stderr。

4.2 SSE:HTTP 长连接流式传输

Server 作为 HTTP 服务运行。Client 先通过 GET 请求建立 SSE 长连接,再通过 POST 请求发送 JSON-RPC 消息,响应通过 SSE 流推送回来:

Client Server (HTTP 服务)
│ │
│──── GET /sse ────────────────►│
│◄─── SSE: event: endpoint ────│ ← 返回消息端点地址
│◄─── SSE: data: /messages ────│
│ │
│──── POST /messages ──────────►│ ← 发送 JSON-RPC 请求
│◄─── SSE: data: response ─────│ ← 通过 SSE 流接收响应

Nginx 反向代理配置(生产环境必做):

location /mcp/ {
proxy_pass http://mcp-backend;
proxy_http_version 1.1;

# 关键:关闭分块传输编码,SSE 不需要
chunked_transfer_encoding off;

# 关键:清除 Connection 头,保持长连接
proxy_set_header Connection '';

# 关键:将超时调大到 3600 秒以上
# Nginx 默认 60 秒超时断开,会导致 SSE 长连接频繁中断
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;

# 缓冲设置:关闭代理缓冲,确保实时推送
proxy_buffering off;
proxy_cache off;

# 标准代理头
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}

踩坑记录:SSE 长连接在 Nginx 反向代理下默认 60 秒超时断开,这是生产环境中最常见的故障之一。表现为工具调用偶尔超时、Client 频繁重连。解决方案是将 proxy_read_timeout 调大到 3600 秒以上。同时需要关闭 proxy_buffering,否则 Nginx 会缓冲 SSE 事件,导致 Client 收不到实时推送。

4.3 Streamable HTTP:新一代推荐方式

MCP 协议在 2025 年初引入了 Streamable HTTP 传输方式,作为 SSE 的替代方案。它将所有通信统一到标准 HTTP 请求/响应中,支持服务端按需升级为 SSE 流。

核心设计思路:

  • Client 向 Server 的单一端点(如 /mcp)发送 POST 请求,携带 JSON-RPC 消息
  • Server 可以选择返回普通 JSON 响应(适用于简单的请求-响应模式)
  • 或者通过 Content-Type: text/event-stream 升级为 SSE 流(适用于需要流式推送的场景)
  • 无需预先建立长连接,也不需要像传统 SSE 那样先 GET 再 POST
  • 请求示例:

    POST /mcp HTTP/1.1
    Host: mcp-server.example.com
    Content-Type: application/json
    Accept: application/json, text/event-stream

    {
    "jsonrpc": "2.0",
    "id": "req-1",
    "method": "tools/call",
    "params": {
    "name": "searchDocs",
    "arguments": { "query": "MCP 协议" }
    }
    }

    普通响应(简单请求直接返回):

    HTTP/1.1 200 OK
    Content-Type: application/json

    {
    "jsonrpc": "2.0",
    "id": "req-1",
    "result": { "content": […] }
    }

    流式响应(服务端决定升级为 SSE):

    HTTP/1.1 200 OK
    Content-Type: text/event-stream

    event: message
    data: {"jsonrpc":"2.0","method":"notifications/progress","params":{"progress":0.5}}

    event: message
    data: {"jsonrpc":"2.0","id":"req-1","result":{"content":[…]}}

    优点:无需维护长连接,对基础设施更友好(Nginx、CDN、API 网关无需特殊配置);支持无状态部署(Server 可以横向扩展);兼容现有的 HTTP 中间件(认证、限流、日志)。

    Spring AI Streamable HTTP 配置示例:

    # Server 端
    spring:
    ai:
    mcp:
    server:
    enabled: true
    name: my-mcp-server
    version: 1.0.0
    type: SYNC

    # Client 端
    spring:
    ai:
    mcp:
    client:
    enabled: true
    streamable-http:
    connections:
    my-server:
    url: http://mcp-server:8091
    endpoint: /mcp
    timeout: 30000

    Session 管理机制:Streamable HTTP 引入了 Session 概念。Server 在首次响应中通过 Mcp-Session-Id 响应头返回会话 ID,Client 后续请求需携带该 ID:

    # 首次请求(无 Session)
    POST /mcp HTTP/1.1
    Content-Type: application/json
    {"jsonrpc":"2.0","id":"1","method":"initialize",…}

    # Server 响应(分配 Session)
    HTTP/1.1 200 OK
    Mcp-Session-Id: sess_abc123
    Content-Type: application/json
    {"jsonrpc":"2.0","id":"1","result":{…}}

    # 后续请求(携带 Session)
    POST /mcp HTTP/1.1
    Mcp-Session-Id: sess_abc123
    Content-Type: application/json
    {"jsonrpc":"2.0","id":"2","method":"tools/list",…}

    这种机制使得 Server 可以在需要时维护会话状态,同时支持无状态部署(此时 Session ID 仅用于路由)。

    4.4 三种传输方式对比

    传输方式通信模式适用场景注意事项连接管理
    stdio 本地管道 本地 CLI 工具、IDE 插件 无法跨机器,不支持多 Client Host 管理进程生命周期
    SSE HTTP 长连接 远程 Web 服务、微服务 需配置 Nginx 超时,维护长连接 Client 维护重连逻辑
    Streamable HTTP 标准 HTTP(可升级为流) 推荐默认方式 无状态友好,基础设施兼容好 无需长连接管理

    选型建议:本地开发调试用 stdio,生产环境优先选择 Streamable HTTP。如果 Client SDK 尚未支持 Streamable HTTP,退而选择 SSE,但务必做好 Nginx 超时配置和心跳保活。


    五、协议生命周期:从握手到调用

    Client 与 Server 建立连接后,必须按照严格的时序完成初始化握手,才能进入正常工作状态。整个生命周期分为四个阶段。

    5.1 阶段一:初始化握手

    握手是 Client 和 Server 建立信任的第一步。Client 发送 initialize 请求,携带自身支持的协议版本和能力:

    // Client → Server:initialize 请求
    {
    "jsonrpc": "2.0",
    "id": "init-1",
    "method": "initialize",
    "params": {
    "protocolVersion": "2025-03-26",
    "capabilities": {
    "tools": {},
    "resources": { "subscribe": true }
    },
    "clientInfo": {
    "name": "my-agent",
    "version": "1.0.0"
    }
    }
    }

    Server 返回 initialize 响应,确认协议版本、声明自身能力:

    // Server → Client:initialize 响应
    {
    "jsonrpc": "2.0",
    "id": "init-1",
    "result": {
    "protocolVersion": "2025-03-26",
    "capabilities": {
    "tools": { "listChanged": true },
    "resources": {},
    "prompts": {}
    },
    "serverInfo": {
    "name": "code-review-server",
    "version": "1.0.0"
    }
    }
    }

    随后,Client 发送 initialized 通知,确认握手完成:

    // Client → Server:initialized 通知(无需响应)
    {
    "jsonrpc": "2.0",
    "method": "notifications/initialized"
    }

    协议版本协商规则:protocolVersion 采用日期格式(如 "2025-03-26")。如果 Client 请求的版本 Server 不支持,Server 应返回错误,Client 降级重试。双方取都支持的最高兼容版本。

    5.2 阶段二:能力协商

    能力协商是 MCP 的关键设计。握手响应中的 capabilities 字段是 Server 的"能力清单":

    能力声明含义Client 行为
    tools: {} Server 提供可调用的工具 Client 可以调用 tools/list 和 tools/call
    tools.listChanged: true 工具列表可能动态变化 Client 监听 notifications/tools/list_changed
    resources: {} Server 提供只读数据源 Client 可以调用 resources/list 和 resources/read
    resources.subscribe: true 支持资源变化订阅 Client 可以调用 resources/subscribe
    prompts: {} 提供预定义 Prompt 模板 Client 可以调用 prompts/list 和 prompts/get
    prompts.listChanged: true Prompt 列表可能动态变化 Client 监听 notifications/prompts/list_changed

    Client 根据 Server 的能力声明决定后续交互方式——如果 Server 不声明 resources,Client 就不会尝试列出资源。这种双向声明机制使得协议具有良好的可扩展性。

    完整的握手 + 能力协商时序图:

    Client Server
    │ │
    │──── initialize ───────────────────────────────►│
    │ {protocolVersion, capabilities, clientInfo} │
    │ │
    │◄──── initialize response ─────────────────────│
    │ {protocolVersion, capabilities, serverInfo} │
    │ │
    │──── notifications/initialized ────────────────►│ (握手完成)
    │ │
    │ ← 根据 capabilities 决定后续行为 → │
    │ │
    │──── tools/list ───────────────────────────────►│ (仅当 Server 声明了 tools)
    │ │
    │◄──── tools/list response ─────────────────────│
    │ {tools: […]} │
    │ │
    │──── tools/call ───────────────────────────────►│ (调用具体工具)
    │ {name: "searchDocs", arguments: {…}} │
    │ │
    │◄──── tools/call response ─────────────────────│
    │ {content: [{type: "text", text: "…"}]} │

    能力协商还支持渐进式增强:Client 可以只请求自己需要的能力子集,Server 也可以根据 Client 的能力声明选择性地暴露工具。例如,一个面向简单 Agent 的 Client 可能只声明 tools 能力,Server 就不会暴露复杂的 resources 和 prompts。

    5.3 阶段三:工具发现

    握手完成后,Client 发送 tools/list 请求,获取 Server 上所有可用工具的列表。响应中每个工具包含名称、描述和参数 Schema(JSON Schema 格式):

    // tools/list 响应示例
    {
    "jsonrpc": "2.0",
    "id": "list-1",
    "result": {
    "tools": [
    {
    "name": "searchDocs",
    "description": "搜索项目文档,返回最匹配的文档片段",
    "inputSchema": {
    "type": "object",
    "properties": {
    "query": {
    "type": "string",
    "description": "搜索关键词"
    },
    "topK": {
    "type": "integer",
    "description": "返回结果数量",
    "default": 5
    }
    },
    "required": ["query"]
    }
    },
    {
    "name": "executeQuery",
    "description": "执行只读 SQL 查询,返回结果集",
    "inputSchema": {
    "type": "object",
    "properties": {
    "sql": {
    "type": "string",
    "description": "SELECT 语句(仅支持只读查询)"
    }
    },
    "required": ["sql"]
    }
    }
    ]
    }
    }

    这个列表在运行时相对稳定,Client 会缓存结果以避免每次 LLM 推理都发起网络请求。当 Server 声明了 listChanged: true 时,Client 会监听 notifications/tools/list_changed 通知,在工具变更时自动刷新缓存。

    性能数据:对于有 20+ 工具的大型系统,工具列表缓存可以将首次推理延迟降低 50-100ms。Spring AI 的 McpSyncClient 默认缓存工具列表,仅在收到变更通知时刷新。

    5.4 阶段四:工具调用

    一切就绪后,LLM 决定需要调用某个工具时,Client 发送 tools/call 请求,Server 执行工具逻辑并返回结果:

    // tools/call 请求
    {
    "jsonrpc": "2.0",
    "id": "call-1",
    "method": "tools/call",
    "params": {
    "name": "searchDocs",
    "arguments": {
    "query": "MCP 协议握手流程",
    "topK": 3
    }
    }
    }

    // tools/call 响应
    {
    "jsonrpc": "2.0",
    "id": "call-1",
    "result": {
    "content": [
    {
    "type": "text",
    "text": "MCP 握手流程分为四步:1) Client 发送 initialize 请求…2) Server 返回 capabilities…"
    },
    {
    "type": "text",
    "text": "能力协商阶段,Server 通过 capabilities 字段声明支持的工具类型…"
    }
    ],
    "isError": false
    }
    }

    Client 将结果注入 LLM 上下文,LLM 基于结果生成最终回答。这就是 MCP 的完整工作流:握手 → 协商 → 发现 → 调用,协议层面的事情到此结束。

    5.5 错误处理与重连机制

    除了协议层错误,网络层错误(连接断开、超时、Server 不可达)需要 Client 自行处理。生产环境中,健壮的重连机制是保障可用性的关键。

    Spring AI MCP Client 的内置重连策略:

    McpSyncClient client = McpClient.sync(transport)
    .clientId("my-agent-client")
    .clientVersion("1.0.0")
    // 重连配置
    .retryCount(3) // 最大重试次数
    .retryInterval(Duration.ofSeconds(1)) // 基础重试间隔
    .build();

    try {
    client.initialize();
    } catch (McpConnectionException e) {
    // 连接失败,Client 进入断开状态
    // 下次调用时会自动尝试重新连接
    log.error("MCP Server 连接失败: {}", e.getMessage());
    }

    重连间隔采用指数退避策略(1s → 2s → 4s),避免在网络故障时对 Server 造成"重连风暴"。超过最大重试次数后,Client 进入断开状态,等待下次调用时重新尝试连接。

    生产环境建议:在 Client 前加一层健康检查,定期发送探活请求,连续失败 3 次则标记 Server 为不可用,告警运维人员:

    @Component
    public class McpHealthIndicator implements HealthIndicator {

    @Autowired
    private McpSyncClient mcpClient;

    @Override
    public Health health() {
    try {
    // 用 listTools 做探活(轻量且幂等)
    mcpClient.listTools();
    return Health.up()
    .withDetail("server", "mcp-server")
    .withDetail("tools", mcpClient.listTools().tools().size())
    .build();
    } catch (Exception e) {
    return Health.down()
    .withDetail("error", e.getMessage())
    .build();
    }
    }
    }

    连接探测的选型细节:对于 SSE 传输,健康探测建议使用 TCP 端口探测而非 HTTP GET。原因是 GET 请求会打开 SSE 长连接并阻塞,影响正常的连接管理。TCP 探测只验证端口可达性,2 秒超时快速返回:

    // SSE 用 TCP 探测(不发 HTTP GET 避免阻塞)
    try (Socket socket = new Socket()) {
    socket.connect(new InetSocketAddress(host, port), 2000);
    return ProbeResult.ok(id);
    } catch (Exception e) {
    return ProbeResult.failed(id, e.getMessage());
    }


    六、三大原语:Tools / Resources / Prompts

    MCP Server 通过三种原语(Primitive)向外暴露能力。理解这三者的定位和区别,是正确使用 MCP 的前提。

    6.1 Tools(工具):可被 LLM 调用的函数

    Tool 是 MCP 最核心的原语。每个 Tool 有名称、描述、输入参数 Schema(JSON Schema 格式)和输出格式。LLM 通过 Tool 与外部世界交互——搜索文档、查询数据库、执行代码、调用 API。Tool 的核心特征是有副作用:它会执行操作、改变状态、返回新数据。

    描述(description)字段对 Tool 至关重要——它是 LLM 理解工具用途的唯一依据。来看一个正反对比:

    描述质量示例LLM 调用准确率
    ❌ 太模糊 "审查代码" ~60%(LLM 不确定何时调用)
    ⚠️ 一般 "对代码进行静态分析" ~75%
    ✅ 具体清晰 "对 Java 代码仓库进行静态分析,返回潜在缺陷列表和修复建议" ~95%

    经验法则:描述中应包含「做什么」和「返回什么」两个维度。更好的做法是加上「何时使用 / 何时不使用」的指引:

    @Tool(description = """
    审查 Java 代码并提供改进建议。
    当用户提交代码片段要求 review 时使用此工具。
    不要用于:代码格式化、编译检查等非审查场景。
    """)

    6.2 Resources(资源):可被读取的数据源

    Resource 提供只读数据,供 LLM 获取上下文信息。与 Tool 的区别在于:Tool 是"做事"(有副作用),Resource 是"看东西"(只读)。

    例如,一个文件系统 Server 可以暴露 file:///path/to/config.yml 格式的 Resource,让 LLM 在执行工具调用前先读取项目配置。Resource 支持 URI 寻址和可选的订阅机制——当资源内容发生变化时,Server 可以主动通知 Client。

    6.3 Prompts(提示模板):预定义的调用模板

    Prompt 是 Server 预定义的 Prompt 模板。Client 获取模板后填充参数,再发送给 LLM。例如,一个代码审查 Server 可以暴露一个 review-prompt 模板,自动填充仓库路径和分支信息,省去 Client 拼接复杂 Prompt 的工作。

    6.4 三者对比

    原语定位是否必需核心特征
    Tools 可执行的动作 有副作用,LLM 主动调用,参数需 JSON Schema
    Resources 只读的数据源 无副作用,提供上下文,支持 URI 寻址和订阅
    Prompts 预定义的模板 Server 端定义,Client 填充参数,降低拼接成本

    实际开发中,大多数 MCP Server 只暴露 Tools,这是 MCP 最核心的能力。Resources 和 Prompts 是可选增强——当需要让 LLM 感知结构化数据或复用复杂 Prompt 时,再引入这两者。


    七、MCP 与 A2A:两套协议,两个层面

    2025 年 4 月,Google 发布了 A2A(Agent-to-Agent)协议。很多人容易把 A2A 和 MCP 搞混,因为它们都涉及"Agent 连接"。但它们解决的是完全不同的问题。

    打一个比方:MCP 就像你用手机调用外卖 App——你(Agent)告诉 App(Tool)"我要一份宫保鸡丁",App 返回结果。整个过程 App 不会主动给你发消息,也不会拒绝你的请求。而 A2A 就像你和同事协作完成一个项目——你(Agent)告诉同事(Agent)"帮我审查一下这个方案",同事可能会问你几个问题、提出修改建议、甚至拒绝执行不合理的要求。

    维度MCPA2A
    通信对象 Agent ↔ Tool(无状态函数) Agent ↔ Agent(有状态实体)
    调用语义 函数调用,同步返回 任务委托,有生命周期和状态机
    谁发起 Agent 主动调用 Tool 双方都可以发起请求
    能否拒绝 不能,Tool 只是执行函数 可以,Agent 可以拒绝不合理请求
    状态管理 无状态 有状态(Task 状态机,异步轮询)
    传输方式 stdio / SSE / Streamable HTTP HTTP + JSON-RPC 2.0 / SSE / Webhook

    简单说:MCP 解决 Agent 怎么使用工具,A2A 解决 Agent 怎么与 Agent 协作。两者不是竞争关系,而是互补关系。

    一个 Agent 可以同时是 MCP Client(调用外部工具)和 A2A Endpoint(被其他 Agent 调用)。比如一个代码审查 Agent:它作为 MCP Client 调用安全扫描工具和代码质量工具,同时作为 A2A Endpoint 接收来自其他 Agent 的审查请求。

    ┌─────────────────────────────────────────────────┐
    │ 代码审查 Agent │
    │ │
    │ 作为 MCP Client(调用工具): │
    │ → 安全扫描工具 │
    │ → 代码质量检查工具 │
    │ → 依赖分析工具 │
    │ │
    │ 作为 A2A Endpoint(被调用): │
    │ ← 接收来自编排 Agent 的审查请求 │
    │ → 返回审查结果 + 进度通知 │
    └─────────────────────────────────────────────────┘


    八、工程实践中的常见问题与踩坑记录

    8.1 协议版本不兼容

    现象:Client 和 Server 握手失败,Server 返回 -32600 Invalid Request。

    原因:MCP 协议版本号采用日期格式(如 "2024-11-05"、"2025-03-26"),不同版本的 SDK 可能支持不同的协议版本。Spring AI 1.1.x 使用 MCP SDK 0.18.x,与 0.17.x 存在 breaking change。

    解决方案:在 pom.xml 中显式锁定 MCP SDK 版本:

    <dependency>
    <groupId>io.modelcontextprotocol</groupId>
    <artifactId>mcp-sdk-java-jackson</artifactId>
    <version>0.18.2</version>
    </dependency>

    8.2 SSE 长连接频繁断开

    现象:工具调用偶尔超时,Client 日志显示频繁重连。

    原因:Nginx 默认的 proxy_read_timeout 为 60 秒,SSE 长连接在 60 秒无数据传输后会被 Nginx 主动断开。

    解决方案:参考本文 4.2 节的 Nginx 配置,将超时调大至 3600 秒以上,同时关闭 proxy_buffering。

    8.3 工具描述不够精准导致 LLM 调错工具

    现象:LLM 频繁调用错误的工具,或者在不需要工具时强行调用。

    原因:工具的 description 太模糊,LLM 无法准确判断工具的适用场景。

    解决方案:

  • 描述中明确「做什么」+「返回什么」
  • 使用 "When to Use / When NOT to Use" 模式
  • 如果工具有 5 个以上,给每个工具加上领域前缀(如 db_query、db_schema、search_docs)
  • 8.4 多 Server 场景下的工具名冲突

    现象:两个 Server 暴露了同名工具,Client 路由到错误的 Server。

    解决方案:Spring AI 采用命名空间隔离策略,每个工具名称前缀加上 Server 名称:

    code-review.reviewCode → 路由到 code-review Server
    database.executeQuery → 路由到 database Server

    如果两个 Server 的工具名完全相同,前缀不同也不会冲突。

    8.5 诊断观测:工具调用的全链路日志

    生产环境中,工具调用的可观测性至关重要。建议通过装饰器模式为每个工具调用添加日志记录:

    public class LoggingToolCallback implements ToolCallback {
    private final ToolCallback delegate;

    @Override
    public String call(String args) {
    log.info("[MCP-TOOL] invoke: {} args={}",
    toolName, truncate(args, 200));
    long start = System.currentTimeMillis();
    try {
    String result = delegate.call(args);
    log.info("[MCP-TOOL] success: {} cost={}ms result={}",
    toolName, System.currentTimeMillis() – start,
    truncate(result, 200));
    return result;
    } catch (Exception e) {
    log.error("[MCP-TOOL] failed: {} cost={}ms error={}",
    toolName, System.currentTimeMillis() – start,
    e.getMessage());
    throw e;
    }
    }
    }

    日志关键字 [MCP-TOOL] 方便 grep 过滤。建议在服务启动时打印工具汇总报告,快速确认连接状态:

    MCP 连接状态: 3 个连接, 8 个工具
    code-review-local → ok (3 tools)
    zhipu-web-search → ok (1 tool)
    amap-maps → failed (timeout)

    8.6 并发与限流

    MCP Client(尤其是 LLM 驱动的 Agent)可能高频并发调用工具。实测中曾出现单个 Agent 在 1 秒内发起 200+ 次请求,直接打满后端服务线程池。

    建议实施两级限流:

    限流维度策略推荐值
    每 Client QPS 令牌桶(如 Guava RateLimiter) 单 Agent ≤ 10 req/s
    全局工具并发数 信号量(Semaphore) 单工具 ≤ 50 并发
    单工具超时 独立超时配置 根据工具特性设置(5s~60s)

    总结

    MCP 协议的核心设计可以浓缩为几个关键决策:

    • 用 JSON-RPC 2.0 做消息格式——比 gRPC 轻量,比 REST 更适合双向通信
    • 传输层与消息格式解耦——同一条消息可以走 stdio、SSE 或 Streamable HTTP,部署方式灵活
    • 能力协商机制——Client 和 Server 双向声明能力,避免无意义的请求和错误
    • 三大原语分工明确——Tools 做事、Resources 提供数据、Prompts 提供模板
    • Host/Client/Server 三角架构——工具生态与宿主应用完全解耦

    理解了这些设计决策,MCP 就不再是一个黑盒协议,而是一套有清晰工程逻辑的通信规范。协议本身并不复杂,真正的挑战在于如何基于它构建可靠的工具服务——这就是下一篇要解决的问题。


    下一篇预告:MCP-02《MCP Server 开发实战》,将从代码层面拆解如何用 Spring AI 构建一个生产级的 MCP Server——从 @Tool 注解定义工具,到 SSE 传输配置,到多模块工具聚合,到生产环境的连接管理和健康检查。

    — 深入理解 AI Agent · MCP 子系列 · 第 1 篇 —

    有问题评论区见,欢迎交流~

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 深入理解 AI Agent · MCP 子系列 #01:MCP 协议全解—从消息格式到传输层的完整拆解
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!