目录
- 一、先说一个所有人都遇到过的问题
- 二、MCP 的答案
- 三、MCP 和 Function Calling 是什么关系
- 四、协议长什么样
- 五、环境准备
- 六、这个专栏怎么读
- 七、五个会浪费你几小时的坑
- 八、自检清单
- 附:延伸阅读
一、先说一个所有人都遇到过的问题
假设你手上有一套内部系统:数据库、文件服务、几个内部 API。现在想让 AI 能用它们。
你写了一套工具定义,接到 Claude 上。跑通了。
然后老板说,我们也要接 GPT。于是你把那套工具定义改一改,又接了一遍。
再然后说本地也要跑一个开源模型。第三遍。
三个模型,五个工具,十五份胶水代码。 而且每加一个新工具,三个地方都要改。
这不是什么新鲜困境。它长得和十几年前的编辑器插件问题一模一样:VS Code 要为每个语言写一套补全逻辑,Eclipse 也要写一套,Vim 还要写一套。
后来 LSP(Language Server Protocol)解决了它——语言方实现一个 Server,编辑器方实现一个 Client,中间用统一协议说话。
MCP 是同一套思路,只不过对象从「编辑器 × 语言」换成了「模型 × 工具」。
二、MCP 的答案
MCP 把那个矩阵拆成两半:
工具1 工具2 工具3 工具4 工具5
Claude ✍️ ✍️ ✍️ ✍️ ✍️
GPT ✍️ ✍️ ✍️ ✍️ ✍️
本地模型 ✍️ ✍️ ✍️ ✍️ ✍️
乘法变加法:工具方实现一次 MCP Server,模型方实现一次 MCP Client。3 + 5 = 8,而且新增工具只影响工具侧。
顺带解决了一个更隐蔽的问题:语言绑定。
一个 MCP Server 是 Python 写的还是 Java 写的,Client 完全不需要知道。它只关心两件事——用什么命令能把这个进程拉起来,以及怎么在它的标准输入输出上说话。
┌──────────────────────────────────────────────────────┐
│ Host(你的应用) │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Client A │ │ Client B │ │ Client C │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
└───────┼──────────────┼──────────────┼────────────────┘
│ │ │
┌────▼────┐ ┌────▼────┐ ┌────▼────┐
│Server 1 │ │Server 2 │ │Server 3 │
│ Python │ │ Java │ │ Node │
└─────────┘ └─────────┘ └─────────┘
⚠️ 有个细节新手经常画错:Client 和 Server 是 1:1 的。 是「一个 Host 持有多个 Client」,不是「一个 Client 连多个 Server」。
三、MCP 和 Function Calling 是什么关系
这是我见过被问得最多、也答错最多的一个问题。
先说结论:它们不是竞品,是两层,互相正交。
┌─────────────────────────────────────────────────────────┐
│ Function Calling(模型侧) │
│ 模型怎么表达「我想调用某个工具」 │
│ 比如:Claude 返回 stop_reason="tool_use" + 一个 tool_use 块 │
└─────────────────────────────────────────────────────────┘
↕ 正交
┌─────────────────────────────────────────────────────────┐
│ MCP(工具侧) │
│ 工具怎么被发现、怎么被调用 │
│ 比如:tools/list 列出有什么,tools/call 执行 │
└─────────────────────────────────────────────────────────┘
为什么容易混? 因为两者经常一起出现,而且都用 JSON Schema 描述参数。看起来就像同一个东西的两种叫法。
三个能帮你记牢的事实:
| 事实 | 说明 |
|---|---|
| MCP Server 可以完全不用 LLM | 任何普通程序都能调它。本系列第二篇就故意这么写 |
| Function calling 可以调非 MCP 的工具 | 你手写几十行 JSON Schema 就是,根本不涉及 MCP |
| 两者需要一层薄胶水才能连起来 | 把 MCP 工具转成 Claude 工具格式。第三篇会写,只有 30 行 |
一个精准的区分方法:
如果你在写提示词、工具定义、解析
tool_use—— 那是 function calling。 如果你在写启动子进程、收发 JSON-RPC、列工具清单 —— 那是 MCP。
本系列的 Demo 1 故意不接任何大模型,就是为了让这条分界线变得显而易见。等你亲眼看到「一个没有任何 AI 的程序也能调用 MCP 工具」,这个概念就再也混不了了。
四、协议长什么样
MCP 没有重新发明 RPC。底层就是 JSON-RPC 2.0,加一套约定好的方法名。
一次工具调用在线上是这样:
// 客户端 → 服务端
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "add",
"arguments": { "a": 3, "b": 4 }
}
}
// 服务端 → 客户端
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"content": [{ "type": "text", "text": "3 + 4 = 7" }]
}
}
常用的方法名就四个:
| 方法 | 作用 |
|---|---|
server/discover | 问服务端支持哪些版本、有哪些能力、它是谁 |
tools/list | 列出所有工具 |
tools/call | 调用某个工具 |
subscriptions/listen | 订阅变更通知(长活响应流) |
⚠️ 2026-07-28 规范变更:早期版本的
initialize握手和notifications/initialized通知都已删除;resources/subscribe也并入了subscriptions/listen。原因见 4.1。
4.1 从「握手」到「每个请求自带」
早先的模型是一次握手:客户端发 initialize,双方各自告诉对方「我支持什么」,谈定一次,之后都按这次谈的结果来。
2026-07-28 把这个握手删掉了,但”交换能力声明”的需求还在。 它只是换了个地方——每个请求都自带。
删掉握手的理由不是”握手不好”,而是握手要求服务端记住客户端是谁,这就引入了状态。而状态会挡住水平扩展:客户端一旦绑在某个服务端实例上,普通负载均衡就没法随便转发了。
换个角度说,规范现在要求:
all the information needed to process a request is contained in the request itself.
每个请求都是自包含的,服务端不需要、也不允许从上一个请求推断任何东西。
这里先纠正一个流传很广的说法 —— 很多人(包括本专栏早期版本)会说「MCP 是双向协议,Server 也能反过来请求 Client」。这句话在现行规范里已经不成立了。
【2026 规范变更】 从 2026-07-28 版规范起,MCP 明确了所有交互必须由客户端发起:
Servers MUST NOT initiate JSON-RPC requests, and clients do not send JSON-RPC responses.
也就是说,MCP 现在是客户端单向发起、双方声明能力的模型,不是双向协议。
那服务端需要「问用户一个问题」怎么办
用多轮往返(Multi Round-Trip Requests)把一次双向交互拆成两次单向:
Client → Server request (id: 1)
Client ← Server InputRequiredResult(我还需要这些输入)
Client → Server request (id: 2, 原参数 + inputResponses)
Client ← Server response
服务端不是「主动发起一个请求」,而是「回答:这个请求我还完不成」,然后客户端带着补充信息重试。需要用户输入(Elicitation)和需要客户端提供根目录(Roots)都走这条路。
为什么非要这么绕?因为传输层不支持。
stdin/stdout 是单向管道,HTTP 是请求-响应。想让服务端主动发起请求,就得让传输层支持双向或者长期保持连接。MCP 选择不这么做,而是用协议层的约定去绕开传输层的限制。
这个思路你会在很多地方见到:
HTTP 想服务器主动推送 → 长轮询 → SSE → WebSocket
MCP 想服务端主动提问 → 多轮往返(客户端重试)
同一个约束,不同的解法。 记住这一点,后面理解 MCP 的各种设计取舍会顺很多。
关于 Sampling:它被废弃了
Sampling 曾经是「服务端主动发起请求」最典型的应用 —— Server 说「帮我调一次 LLM」,这样 Server 不用内置模型和 API Key。
这个能力已在 2026-07-28 版规范中废弃(SEP-2577),官方建议新实现不要采用,已有实现迁移到直连 LLM Provider API。按规范的生命周期政策,它会在规范里至少保留 12 个月再考虑移除。
这里保留这段说明不是让你去用它,而是它被废弃的原因很值得理解:Sampling 是「服务端发起请求」这个模型最典型的应用,而这个模型本身被取消了。
那能力声明现在长什么样
需求没变,载体变了——从”握一次手”变成”每次都带”。
客户端在每个请求的 _meta 里带上:自己按哪个版本说话、有什么能力。
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "add",
"arguments": { "a": 3, "b": 4 },
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}
少了必填字段,服务端会直接把它判成畸形请求(-32602 Invalid params)。这就是”每次都带”的代价——多写一点,换来请求自包含。
服务端那边,能力声明同样要显式写:
.capabilities(McpServerCapabilities.builder().tools(true).build())
不声明的话,客户端依然不会来调 tools/list——这条没变。
一句话总结:握手没了,能力声明的纪律还在,只是从”一次谈定”变成了”每次重申”。
4.2 传输方式怎么选
| 传输 | 场景 | 怎么工作 |
|---|---|---|
| STDIO | 本地子进程 | 客户端 fork 出 server,用 stdin/stdout 通信 |
| Streamable HTTP | 远程服务 | 单个 HTTP endpoint,用 POST 发消息;需要流式返回时可选开启 SSE |
判断标准很简单:server 和 host 在同一台机器上 → STDIO;server 独立部署 → Streamable HTTP。
关于 Streamable HTTP 有两个常被搞混的点:
- 它不是双向的 —— 客户端 POST 一个请求,服务端可以立刻回一个 JSON,也可以回一条 request 作用域的 SSE 流(用来先推进度通知、最后推结果)。「流」是挂在这一个请求上的,不是一条常驻连接。
- SSE 是可选的 —— 简单场景直接回 JSON 就行,只有需要流式才用 SSE。
(历史上的 HTTP+SSE 传输已废弃,它用的是「GET 开一条长连接 + POST 发消息」的双端点模型,新项目不要用。)
本系列用 STDIO,因为它零网络配置、天然安全、部署简单。
五、环境准备
JDK 17+ MCP Java SDK 要求 17 以上,推荐 21
Maven 3.9+
IDEA
ANTHROPIC_API_KEY 第三篇开始需要,前两篇不需要
依赖就三块:
<properties>
<mcp.sdk.version>2.0.0</mcp.sdk.version>
<anthropic.sdk.version>2.34.0</anthropic.sdk.version>
</properties>
<dependencyManagement>
<dependency>
<groupId>io.modelcontextprotocol.sdk</groupId>
<artifactId>mcp-bom</artifactId>
<version>${mcp.sdk.version}</version>
<type>pom</type><scope>import</scope>
</dependency>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>io.modelcontextprotocol.sdk</groupId>
<artifactId>mcp</artifactId>
</dependency>
<dependency>
<groupId>com.anthropic</groupId>
<artifactId>anthropic-java</artifactId>
<version>${anthropic.sdk.version}</version>
</dependency>
</dependencies>
mcp 是个便捷包,等于 mcp-core + Jackson3 JSON 实现 + 传输实现。想精细控制可以只引 mcp-core。
验证环境:
mvn -q clean compile
编译过了就行。真正能不能跑,第二篇跑通第一个 Demo 就知道了。
六、这个专栏怎么读
四步递进,每一步解决一个具体的认知问题:
| 篇 | 做什么 | 要建立的认知 |
|---|---|---|
| 二 | 手写最小 MCP 链路(不接 LLM) | MCP 是「程序调工具」的协议 |
| 三 | 把 Claude 接进来 | agent 本质就是个 while 循环 |
| 四 | 做一个只读数据库 MCP | 模型看不见你的库,描述决定它的表现 |
| 五 | 加三道安全墙 | 安全必须做在模型管不到的地方 |
建议的顺序是:读一篇,写一遍,跑通,再读下一篇。
代码我放在了配套工程里,src/ 下的文件是空的,需要照着文档自己敲。这不是折腾——这套东西的坑很具体,抄一遍会全错过。
七、五个会浪费你几小时的坑
先列出来,后面每一步踩到时能对得上号。
① stdout 被污染 —— 头号杀手
症状:客户端报 JSON 解析错误,或者干脆卡住不动。
原因:STDIO 传输把 stdout 当成独占的 JSON-RPC 通道。任何打到 stdout 的东西都会污染协议流:
System.out.println("debug")—— 最常见- 日志框架默认输出到 stdout(Logback 默认就是)
- 某个依赖库自己 print 东西
解法:日志统一走 stderr。
② Windows 中文乱码
症状:文件读写正常,但 System.out.println 出来的中文全是乱码。
原因:Java 18+ 已经把 file.encoding 默认成 UTF-8 了,但 System.out 走的是 stdout.encoding,它在 Windows 上仍然跟随控制台代码页(中文系统是 GBK)。
解法:三个参数缺一不可。
-Dfile.encoding=UTF-8 -Dstdout.encoding=UTF-8 -Dstderr.encoding=UTF-8
注意:fork 子进程时也要带上,否则子进程的日志照样乱码。
③ 孤儿进程
症状:程序跑完,任务管理器里一堆 java 进程不走。
原因:官方 SDK 的 stdio server 在 stdin 关闭后不会自己退出。官方示例通常写成 new CountDownLatch(1).await()(永久阻塞),客户端一崩 server 就变孤儿。
解法:Server 端自己感知 stdin EOF。第四篇会给出完整实现。
④ -Dexec.args 不能用
症状:参数被当成主类名,报 ClassNotFoundException。
原因:exec-maven-plugin 会把 exec.args 插到主类名之前。
解法:定义一个自定义属性,在 <arguments> 里显式放在主类名后面:
<argument>${mainClass}</argument>
<argument>${appArgs}</argument>
用法就从 -Dexec.args="..." 变成 -DappArgs="..."。
⑤ 忘了 closeGracefully()
症状:同 ③,但原因在客户端侧。
解法:try/finally 兜底,别指望自己记得手动调。
八、自检清单
读到这里,你应该能回答:
- MCP 解决的核心问题是什么?(提示:想想 N×M)
- MCP 和 function calling 的区别,能用一句话说清吗?
- 一个 Host 可以连几个 Server?Client 和 Server 是什么比例?
- MCP 底层用的是什么通信协议?
- STDIO 和 Streamable HTTP 分别适合什么场景?
-
initialize握手被删除后,双方「交换能力声明」这件事改在哪里做了? - stdio server 最容易踩的坑是什么?
附:延伸阅读
相关资料
| 资源 | 说明 |
|---|---|
| Model Context Protocol 官方文档 | 协议规范、概念说明、各语言 SDK 索引 |
| modelcontextprotocol/java-sdk | 本系列使用的 Java SDK,README 里有完整的 API 示例 |
| JSON-RPC 2.0 规范 | MCP 的底层协议,只有一页,十分钟能读完 |
| Claude Tool Use 文档 | Function calling 侧的定义与循环模式 |
| Language Server Protocol | 同一套「标准化接口」思路在编辑器领域的先例,对照着看很有意思 |
关联笔记
- RAG 实战(一):RAG 是什么,以及环境怎么搭 —— 同样是「给模型补上下文」,一个补知识,一个补能力
- Java 学习笔记 —— 本系列代码用到的 JDBC、集合、异常处理基础
最后更新:2026-10-05