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

214 阅读24分钟

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

1.1 碎片化的工具调用现状

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

厂商工具调用格式参数 Schema
OpenAIfunctionJSON Schema(自定义子集)
Anthropictool_useJSON Schema
DashScope(阿里)tool_useJSON Schema(略有差异)
Google Geminifunction_declarationsOpenAPI 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 格式:

方法名方向用途
initializeClient → Server初始化握手
initializedClient → Server握手完成确认(通知)
tools/listClient → Server列出所有工具
tools/callClient → Server调用指定工具
resources/listClient → Server列出所有资源
resources/readClient → Server读取指定资源
resources/subscribeClient → Server订阅资源变更
prompts/listClient → Server列出所有 Prompt 模板
prompts/getClient → Server获取指定 Prompt
notifications/tools/list_changedServer → Client工具列表已变更(通知)
notifications/resources/list_changedServer → Client资源列表已变更(通知)
notifications/cancelled双向取消正在执行的请求

3.4 标准错误码

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

错误码含义常见场景
-32700Parse errorJSON 格式错误,如缺少引号、括号不匹配
-32600Invalid Request请求格式不符合 MCP 规范
-32601Method not found调用了 Server 不支持的方法
-32602Invalid params工具参数不符合 JSON Schema
-32603Internal errorServer 内部异常
-32000 ~ -32099Server 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 流。

核心设计思路:

  1. Client 向 Server 的单一端点(如 /mcp)发送 POST 请求,携带 JSON-RPC 消息
  2. Server 可以选择返回普通 JSON 响应(适用于简单的请求-响应模式)
  3. 或者通过 Content-Type: text/event-stream 升级为 SSE 流(适用于需要流式推送的场景)
  4. 无需预先建立长连接,也不需要像传统 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 插件无法跨机器,不支持多 ClientHost 管理进程生命周期
SSEHTTP 长连接远程 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: truePrompt 列表可能动态变化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 HTTPHTTP + 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 无法准确判断工具的适用场景。

解决方案:

  1. 描述中明确「做什么」+「返回什么」
  2. 使用 "When to Use / When NOT to Use" 模式
  3. 如果工具有 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 篇 —

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