导读: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 厂商都有一套自己的工具调用格式:
| 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 解决的是更上层的问题:"工具在哪里、怎么发现、怎么调用、怎么跨平台共享"——属于完整的通信协议。
| 层次 | 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 并非偶然:
| 编译要求 | 无需 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 流。
核心设计思路:
请求示例:
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 的"能力清单":
| 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 理解工具用途的唯一依据。来看一个正反对比:
| ❌ 太模糊 | "审查代码" | ~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)"帮我审查一下这个方案",同事可能会问你几个问题、提出修改建议、甚至拒绝执行不合理的要求。
| 通信对象 | 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 无法准确判断工具的适用场景。
解决方案:
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 篇 —
有问题评论区见,欢迎交流~
网硕互联帮助中心




评论前必须登录!
注册