CH 08MCP:模型上下文协议
第 7 章我们把工具写在了自己工程里;本章用 MCP 把工具变成跨进程、跨语言、可治理的标准服务——就像 HTTP 统一了网页、JDBC 统一了数据库访问。本章无配套 lab 代码,所有操作按「自建工程」口径逐步带你做,每一行命令都能直接复制,每一步都能独立验收。
教学设计说明(教师参考):本章建议用「对比第 7 章」的方式切入——先让学生明白 MCP 不是新概念,而是把「进程内 @Tool」升级成「进程外标准服务」。实操上先走 stdio(零网络问题,10 分钟跑通),再讲远程 Streamable HTTP。教师务必反复强调两个 2.0 关键点:① 服务端注解是 @McpTool/@McpToolParam(包 org.springframework.ai.mcp.annotation),不是 @Tool;② MCP 工具不会自动注册到 ChatClient,必须显式传给 .defaultToolCallbacks(...),这是学生最容易栽的两个地方。
本章易掉队点(课前先点名,课上重点盯):
- 注解认错包:用的是
org.springframework.ai.mcp.annotation下的@McpTool/@McpToolParam,不是第 7 章的@Tool/@ToolParam。把本地工具和 MCP 工具搞混,工具永远不会被协议暴露。 - 漏了显式注册:客户端即使连上了 Server,工具也只是「备好」而非「给模型」。必须调用
.defaultToolCallbacks(mcpTools),否则模型像「突然不会用工具」。 - stdio 协议流污染:本地模式忘了
web-application-type: none,或工具里写了System.out.println,stdout(通信通道)被日志污染,客户端报「无法解析消息」。 - 默认传输变了:2.0 默认传输是 Streamable HTTP,本地 stdio 必须显式
spring.ai.mcp.server.stdio=true,否则 Server 不走标准输入输出。 - 客户端配置键漂移:2.0 用 yml 里的
stdio.connections.<名字>.command/args,不再以外部 JSON +servers-configuration为主。抄旧教程的 JSON 写法会连不上。 - 顺带提醒(仅作废弃语境,不展开):1.x 的
internalToolExecutionEnabled在 2.0 已不存在,工具循环改由ToolCallingAdvisor接管;记忆相关的PromptChatMemoryAdvisor在 2.0 也已删除,对话记忆键一律用ChatMemory.CONVERSATION_ID。本章不涉及记忆,遇到旧代码别照抄。
8.1MCP 解决什么问题:工具的「USB 接口标准」
先看没有 MCP 时的困境:每个 AI 应用都要把各系统的工具代码抄一遍,升级不同步、权限难管控、还受语言限制。
MCP 是什么:一层一层拆开讲
| 维度 | 说明 |
|---|---|
| 是什么 | MCP(Model Context Protocol,模型上下文协议)是一套标准通信协议,规定「AI 应用」和「外部能力(工具/数据/提示词)」之间怎么握手、怎么发现能力、怎么传参数和结果。它和语言、框架无关,谁实现都行。 |
| 为什么 | 没有标准,每接一个系统就要写一套私有对接代码;有标准,一个 Server 写一次,所有支持 MCP 的客户端复用。本质是把「工具对接」从「点对点硬编码」升级为「按规范插拔」。 |
| 没有会怎样 | 工具代码在多个 AI 应用里被反复复制(见上方 ❌ 图),升级一处要改 N 处、权限散落在各处、Python 写的工具 Java 直接调不了。 |
| 类比 | 就像 USB 接口:设备(工具)只要按 USB 规范做插头,任何带 USB 口的电脑(AI 应用)都能用;也像 HTTP 统一了网页访问、JDBC 统一了数据库访问。MCP 统一的是「AI 调用工具」这件事。 |
MCP 与第 7 章 @Tool 的关系
| 对比 | 第 7 章 @Tool(进程内) | MCP(进程外) |
|---|---|---|
| 部署形态 | 与 AI 应用同一个 JVM | 独立进程 / 独立服务 |
| 复用性 | 只能本应用用 | ✅ 所有支持 MCP 的应用都能用 |
| 语言 | Java | ✅ 任意(Java/Python/Node/Go…) |
| 权限与审计 | 混在应用代码里 | ✅ 可以独立做鉴权、限流、审计 |
| 调用开销 | 本地方法调用,纳秒级 | 进程间/网络通信,毫秒级 |
| 适用 | 项目内部、轻度使用 | ✅ 企业级、多系统共享、需要治理 |
@Tool 就够;
一旦出现「多个 AI 应用要用同一批工具」或「工具由别的团队维护」,就该上 MCP。
两者不冲突,可以混用——MCP 提供的工具在 Spring AI 里最终也会变成 ToolCallback,
和内嵌工具用同一套机制。
MCP 协议长什么样:一次调用的真实报文
理解「stdio 模式 stdout 被污染就会解析失败」,前提是知道 MCP 在传什么。MCP 基于 JSON-RPC 2.0,一次「用户问订单 → 模型调工具」背后的报文大致是这样三步:
| 步骤 | 方向 | 报文(简化) |
|---|---|---|
| ① 初始化握手 | Client → Server | {"method":"initialize","params":{"protocolVersion":"2025-06-18",...}} |
| ② 列工具 | Client → Server | {"method":"tools/list"} → 返回工具名 / 描述 / 参数 schema |
| ③ 调工具 | Client → Server | {"method":"tools/call","params":{"name":"queryOrderStatus","arguments":{"orderNo":"2024001"}}} |
关键点:这些报文全是纯文本 JSON,通过 stdout / stdin 一行一行传。一旦 Server 往 stdout 多吐了一行启动日志、或一个 System.out.println,Client 读到的就不是合法 JSON,立刻报「Unexpected token / 无法解析消息」。这就是为什么 8.4 节要关 Web 容器、禁 System.out——不是为了好看,是为了不让通信电线里灌杂音。
8.2架构与三大原语
三个角色:Host / Client / Server
| 角色 | 职责 | 本课程的对应实现 |
|---|---|---|
| Host | 承载 AI 应用与用户界面的一方(你的 Spring Boot 应用 / Claude Desktop / IDE) | 我们的 springai-lab 主应用 |
| Client | Host 内部的连接器,负责与某个 Server 建立 1:1 连接、协议握手、收发消息 | McpSyncClient / McpAsyncClient |
| Server | 暴露能力的一方,声明「我提供哪些工具 / 资源 / 提示词」 | 我们写的 MCP Server 应用 |
把三个角色的关系再讲一遍「为什么这样分」:
| 维度 | 说明 |
|---|---|
| 是什么 | 一个 Host(你的 AI 应用)内部会有 N 个 Client,每个 Client 一对一连一个 Server;Server 独立部署、独立暴露能力。 |
| 为什么 | 把「连接管理」和「能力提供」解耦:Server 只管把能力说清楚,Client 只管跟某一个 Server 打交道,Host 把两者拼起来喂给模型。 |
| 没有会怎样 | 如果 Host 直接硬编码连每个系统,系统一多就变成一团乱麻,换一个 Server 就要改 Host 代码。 |
| 类比 | Host 是你家(AI 应用),Client 是各个房间的「专用插座」,Server 是外面的「发电厂」。插座只负责跟某一个厂对接,你(模型)插上就能用电,不用关心电厂怎么发电。 |
Server 端 vs Client 端职责对照
| 职责 | Server 端(提供方) | Client 端(使用方) |
|---|---|---|
| 核心任务 | 把工具/资源/提示词声明出去,接收调用并返回结果 | 连上 Server,发现能力,把工具包装好交给模型 |
| 关键注解 / 类 | @McpTool / @McpToolParam | McpSyncClient / SyncMcpToolCallbackProvider |
| 谁来启动 | 独立进程(stdio 由 Host 拉起,或独立 Web 服务) | 由 Host 的 Spring 容器按配置自动建立连接 |
| 最易踩的坑 | stdio 模式污染协议流、忘了 @Component | 忘了显式注册工具到 ChatClient、配置键写错 |
Server 能暴露的三种能力(三大原语)
| 原语 | 含义 | 典型例子 | 谁发起 |
|---|---|---|---|
| Tools 最常用 |
可执行的「动作」,有参数、有返回值 | 查询订单、发送邮件、创建日程 | 模型决定 |
| Resources | 可读取的「数据」,像文件一样按 URI 访问 | file:///docs/a.md、db://orders/2024001 |
应用决定 |
| Prompts | 预定义的「提示词模板」,可带参数 | 「代码审查模板」「周报生成模板」 | 用户/应用选择 |
McpSyncClient vs McpAsyncClient。同步客户端的方法直接返回结果(listTools()、callTool(...)),写起来像普通 Java 方法,教学和最常用;异步客户端返回 Mono / Flux,适合高并发响应式应用。二者一一对应:Server 端 type: SYNC 配同步,客户端注入 SyncMcpToolCallbackProvider;选了异步就注入 AsyncMcpToolCallbackProvider。初学一律用 Sync,别给自己加难度。
8.3三种传输方式:怎么连
传输方式决定了「Host 和 Server 之间用什么通道通信」。先给一张总表,再逐个拆开讲。
| 传输方式 | 原理 | 优点 | 缺点 | 适用 |
|---|---|---|---|---|
| stdio | Host 以子进程方式启动 Server,通过标准输入输出通信 | 零网络配置、最简单、延迟极低 | 只能本机、Server 必须能被 Host 启动 | 本地工具 桌面应用 |
| SSE 旧 |
HTTP 长连接,服务端通过 Server-Sent Events 推送 | 可跨网络、可被反向代理 | 需要维护长连接、部分代理不友好 | 早期实现 |
| Streamable HTTP 推荐 |
标准 HTTP 请求 + 流式响应,无状态可扩展 | ✅ 最容易部署到 K8s、可水平扩展、易做网关鉴权 | 需要独立部署一个服务 | 企业级 远程 MCP |
stdio:本地联调的首选,但必须理解「stdout 就是电线」
| 维度 | 说明 |
|---|---|
| 是什么 | Host 像启动子进程一样拉起 Server,双方通过标准输入 stdin / 标准输出 stdout 收发 JSON-RPC 消息。没有端口、没有 HTTP。 |
| 为什么 | 零网络配置,本机一条命令就能跑通,最适合「先跑通再谈部署」的教学与本地开发。 |
| 没有会怎样 | 没有 stdio 你就得先搞定网络、端口、CORS、反向代理,而那不是本章重点。直接上 HTTP 会让你在「连不上」上浪费大量时间。 |
| 类比 | stdout 就是 Server 和 Host 之间的「电线」。任何往 stdout 多写一点的东西(启动日志、System.out.println)都等于往电线里灌杂音,对端收到的就不再是合法报文。 |
SSE 与 Streamable HTTP:远程形态
| 维度 | 说明 |
|---|---|
| SSE 是什么 | HTTP 长连接,服务端用 Server-Sent Events 持续往客户端推消息。曾是远程 MCP 的早期方案。 |
| Streamable HTTP 是什么 | 标准 HTTP 请求 + 流式响应,无状态、可水平扩展,是 2.0 的默认传输。 |
| 为什么推荐后者 | 无状态意味着前面可以放普通网关/负载均衡做鉴权和扩缩容;SSE 的长连接对反向代理不友好。 |
| 没有会怎样(选错) | 选 SSE:部分代理会断流、扩缩容麻烦;把 stdio 当成远程方案:跨机器根本连不上。 |
怎么选传输方式:一张决策表
| 你的场景 | 选哪个 |
|---|---|
| 本地先把流程跑通、单机工具、桌面应用 | stdio(零网络,10 分钟跑通) |
| 工具要被多个机器 / 多个服务共享、要上云、要网关鉴权 | Streamable HTTP(2.0 默认,推荐) |
| 已有基于 SSE 的旧服务、短期不想改 | SSE(但已 deprecated,新项目别选) |
| 想验证「跨语言复用」(如用官方 Node 版 filesystem Server) | stdio 或 Streamable HTTP 都行,看部署位置 |
课程实操顺序:先 stdio 跑通整条链路,再(选做)把 Server 改成 Streamable HTTP 独立部署。不要一上来就远程,否则你会花大量时间在「端口 / 代理 / 跨域」上,而不是学 MCP 本身。
command / args)。所以:① 你只需保证 jar 路径正确、能被 java -jar 拉起;② 主应用退出时,框架会一并结束这些子进程;③ 不要在外面单独再 java -jar 启一个 Server——那样会变成两个实例,客户端连的是它自己拉起的那一个。理解这点,你就明白为什么「Server 必须常驻、不能自己退出」。
8.4写一个 MCP Server
下面从零建一个独立工程 mcp-order-server。本章无配套 lab 代码,所以全程「自建」:所有路径、类名、片段都按下面逐步操作,不要照抄别的教程的目录。
① 新建工程并加依赖(建议独立工程)
MCP Server 通常做成独立应用,不要和主应用混在一起——这样它才能被多个 AI 应用共享,也便于独立部署与权限治理。
-
打开 start.spring.io,按下表选择:
选项 选择 Project / Language Maven / Java Spring Boot 4.1.x(不要选 3.x) Group / Artifact com.example/mcp-order-serverPackage name com.example.mcporderserverPackaging / Java Jar / 17 Dependencies 先不加任何依赖(MCP starter 不在初始选项里,我们手动加) Generate 下载 zip,解压到
D:/springai-lab/mcp-order-server,用 IDEA 打开(File → Open → 选中含pom.xml的那层)。-
打开
pom.xml,把下面片段完整粘贴进去(替换默认的依赖段,并补上spring-ai-bom的dependencyManagement)。注意版本基准:Spring AI 2.0.1 + Boot 4.1.x + Java 17。<!-- 新建工程 mcp-order-server,pom 片段 --> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>4.1.1</version> </parent> <dependencies> <!-- 方案 A:stdio 模式(最简,用于本地联调) --> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-mcp-server</artifactId> </dependency> <!-- 方案 B:WebMVC / Streamable HTTP 模式(用于远程服务) --> <!-- <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-mcp-server-webmvc</artifactId> </dependency> --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webmvc</artifactId> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-bom</artifactId> <version>2.0.1</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> 保存,等右下角 Maven 同步完成(首次约 1~3 分钟)。
② stdio 模式:一个配置文件 + 一个工具类
-
在
src/main/resources/下右键 → New → File,文件名精确填application.yml(若已存在就直接打开)。把下面内容整段粘贴覆盖:# src/main/resources/application.yml(本地联调用 stdio) spring: main: # ★ stdio 模式下必须关闭 Web 容器,否则启动日志会污染协议流(stdout 就是通信通道) web-application-type: none ai: mcp: server: name: order-mcp-server # 服务名,客户端会看到 version: 1.0.0 # ★ 2.0 变更:显式打开 stdio。Streamable HTTP 已成为默认传输, # 所以本地用 stdio 时必须显式声明,否则 server 不会走标准输入输出。 stdio: true # 执行类型:SYNC(同步)/ ASYNC(异步,返回 Mono/Flux) type: SYNC # 是否把工具变更通知给客户端 tool-change-notification: true
所以在 2.0 里:
- 本地联调:显式设
spring.ai.mcp.server.stdio=true(如上)。 - 远程部署(推荐形态):换成
spring-ai-starter-mcp-server-webmvc, 用spring.ai.mcp.server.protocol=STREAMABLE;也有STATELESS变体, 适合放在反向代理后面做水平扩展(代价是失去 MCP 的双向通信能力)。 - SSE:已被标记为 deprecated,新项目不要再选它。
③ 启动类
- 在包
com.example.mcporderserver上右键 → New → Java Class,名称精确填McpOrderServerApplication,把下面内容粘贴进去:
// McpOrderServerApplication.java
@SpringBootApplication
public class McpOrderServerApplication {
public static void main(String[] args) {
SpringApplication.run(McpOrderServerApplication.class, args);
}
}
④ 写工具类:用 @McpTool,不是 @Tool
- 在包
com.example.mcporderserver上右键 → New → Java Class,名称精确填OrderMcpTools。 - 把下面完整内容粘贴进
OrderMcpTools.java并保存。重点看两处:注解是@McpTool/@McpToolParam(包org.springframework.ai.mcp.annotation),类上有@Component让它成为 Spring bean。
// OrderMcpTools.java —— 注意:用 @McpTool,不是第 7 章的 @Tool!
@Component
public class OrderMcpTools {
/**
* ★ 2.0 变更:服务端暴露 MCP 能力要用 @McpTool / @McpToolParam,
* 而不是给本地工具用的 @Tool / @ToolParam。两者语义不同:
* @Tool = 让本应用的模型调用(第 7 章)
* @McpTool = 通过 MCP 协议暴露给外部客户端
* 包路径(2.0 起 MCP 注解已并入 Spring AI 本体):
* org.springframework.ai.mcp.annotation.McpTool
* org.springframework.ai.mcp.annotation.McpToolParam
*/
@McpTool(description = """
查询指定订单号的当前状态与预计发货时间。
当用户询问发货情况、物流状态时调用。
不要用于查询退款进度或账户余额。
""")
public OrderStatus queryOrderStatus(
@McpToolParam(description = "订单号,纯数字字符串,例如 2024001")
String orderNo) {
// 这里可以是真实数据库查询。教学演示用模拟数据。
return switch (orderNo) {
case "2024001" -> new OrderStatus(orderNo, "未发货", "2024-03-08");
case "2024002" -> new OrderStatus(orderNo, "已发货", "-");
default -> new OrderStatus(orderNo, "NOT_FOUND", "-");
};
}
@McpTool(description = "列出某个客户最近 5 笔订单的订单号与状态")
public List<OrderStatus> listRecentOrders(
@McpToolParam(description = "客户姓名") String customerName) {
return List.of(
new OrderStatus("2024001", "未发货", "2024-03-08"),
new OrderStatus("2023129", "已完成", "-"));
}
public record OrderStatus(String orderNo, String status, String promisedDate) {}
}
⑤ 关键一步:让 Server 发现你的工具
2.0 起这一步变得很简单:不用写任何配置类。
只要工具类是个 Spring bean 且方法上标了 @McpTool,
MCP Server 的自动配置会通过注解扫描把它自动注册出去:
@Component // ★ 必须是 Spring bean,否则扫描不到
public class OrderMcpTools {
@McpTool(description = "查询指定订单号的当前状态与预计发货时间")
public OrderStatus queryOrderStatus(
@McpToolParam(description = "订单号,例如 2024001") String orderNo) {
// …… 业务逻辑
}
}
// 就这样,不需要 @Configuration,不需要 ToolCallbackProvider。
注解扫描的包范围可以配置(默认扫应用主类所在包及其子包)。如果你确实需要程序化注册 (例如工具来自第三方对象、或要按环境动态决定暴露哪些),再退回到 Provider 写法:
@Configuration
public class McpServerConfig {
@Bean
public ToolCallbackProvider orderToolsProvider(OrderMcpTools orderMcpTools) {
// 把 bean 里的 @Tool 方法收集成工具(注意:这里是本地 @Tool 语义,
// 适用于"用代码手工装配 MCP Server"的场景)
return MethodToolCallbackProvider.builder()
.toolObjects(orderMcpTools)
.build();
}
}
McpToolFilter bean 按服务标识、工具名或描述筛选。
这在多租户或分级授权的场景里很关键:不要把内部管理类工具无意中暴露给所有客户端。
另外还有 spring.ai.mcp.server.expose-mcp-client-tools 控制是否把本应用作为客户端拿到的工具再对外暴露。
运行验证 ①:打包并确认 Server 能起
-
打开 IDEA 底部 Terminal(或系统终端,先 cd 到工程目录),执行:
# 进入工程目录(按你的实际路径) cd D:/springai-lab/mcp-order-server # 打包,跳过测试加快速度 mvn clean package -DskipTests看到什么算通过:末尾出现
BUILD SUCCESS,且target/下生成mcp-order-server-1.0.0.jar。[INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 12.345 s [INFO] mcp-order-server-1.0.0.jar 已生成于 target/ -
用官方 Inspector(网页调试工具)连上它,确认工具能被发现并调用:
# 在 mcp-order-server 工程目录下执行 npx @modelcontextprotocol/inspector java -jar target/mcp-order-server-1.0.0.jar看到什么算通过:终端提示打开了网页(通常
http://localhost:6274),在页面里能看到order-mcp-server声明的两个工具queryOrderStatus/listRecentOrders,填参数2024001调用能返回未发货。这代表 Server 本身完全正常。
- 忘了
web-application-type: none:容器启动会打印日志到 stdout, 而 stdout 正是 MCP 的通信通道,协议立刻被污染,客户端报「无法解析消息」。
排查:确认 yml 里spring.main.web-application-type=none,启动后进程不应监听任何 HTTP 端口(netstat看不到 8080)。 - 在工具方法里
System.out.println:同样是污染协议流。 调试请一律用log(输出到 stderr 或文件)。
排查:全局搜索System.out,替换为日志框架。 - 启动太快退出:stdio 模式下 Server 必须保持运行等待输入,
如果
main里没有任何阻塞机制,进程会直接结束。用 Spring Boot 的常驻模式即可(不要手动System.exit)。
若 mvn 报插件要求更高 Maven 版本:确认本地 Maven ≥ 3.6.3(见第 0 章 0.5 节),用 mvn -v 核对;若机器有多个 Maven,用 where mvn(Windows)定位实际解析到哪个,优先用项目 mvnw 或删除旧版本后重开终端。
java -jar target/mcp-order-server-1.0.0.jar(不要经 IDEA 启动),如果你看到屏幕上除了 JSON 之外还有任何文字、空行、或 Tomcat 启动横幅,说明 stdout 被污染了——客户端一定连不上。干净的 stdio Server 在终端里几乎「什么都不显示」(因为它只通过 stdin / stdout 跟客户端对话,不面向人)。看到横幅就回去检查 web-application-type: none 和代码里的 System.out。
mcp-order-server 独立工程——哪怕教学里只有主应用连它,这个「独立」的姿势也是生产级的正确姿势。
@McpTool/@McpToolParam(包 org.springframework.ai.mcp.annotation)+ @Component,2.0 自动扫描注册;stdio 模式必关 Web 容器、禁 System.out,否则协议流被污染。
8.5写一个 MCP Client(连上 Server)
客户端跑在主应用 springai-lab 里(它既是 Host 也是 Client)。下面逐步加依赖、写配置、写调试服务。
① 主应用加依赖
- 打开主应用
D:/springai-lab的pom.xml。 - 在
<dependencies>里粘贴下面这段(starter 坐标以官方为准,不要编造版本,交给spring-ai-bom管理):
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-client</artifactId>
</dependency>
② 声明要连接的 Server 列表(2.0 推荐 yml 写法)
- 打开主应用的
src/main/resources/application.yml,在spring.ai.mcp.client下粘贴下面的 stdio 连接配置。
先给你看一份备选的外部 JSON 写法(了解即可,本章以 yml 为准):
{
"mcpServers": {
// ① 我们自己的订单 MCP Server(stdio:本地启动一个 Java 进程)
"order-server": {
"command": "java",
"args": [
"-jar",
"D:/springai-lab/mcp-order-server/target/mcp-order-server-1.0.0.jar"
]
},
// ② 官方文件系统 Server(Node 实现,演示跨语言复用)
"filesystem": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-filesystem",
"D:/springai-lab/data" // ★ 授予访问的目录白名单
]
},
// ③ 远程 Streamable HTTP Server(生产形态)
"remote-weather": {
"url": "http://localhost:8081/mcp",
"type": "STREAMABLE",
"headers": {
"Authorization": "Bearer ${MCP_TOKEN}"
}
}
}
}
2.0 推荐写法:直接在 yml 里声明每个 stdio 连接,不再需要外部 JSON 文件,粘贴到主应用 application.yml:
# ★ 2.0 推荐写法:直接在 yml 里声明每个 stdio 连接,不再需要外部 JSON 文件
spring:
ai:
mcp:
client:
enabled: true
name: springai-lab
version: 1.0.0
# 一次性初始化所有 Server 连接(不等第一个请求才连)
initialized: true
# 把 MCP 工具包装成 ToolCallback(但要显式传给 ChatClient,见 8.6 节)
toolcallback:
enabled: true
stdio:
connections:
# ---- 我们自己的订单 MCP Server ----
order-server:
command: java
args:
- -jar
- D:/springai-lab/mcp-order-server/target/mcp-order-server-1.0.0.jar
# ---- 官方文件系统 Server(Node 实现) ----
filesystem:
command: npx
args:
- -y
- "@modelcontextprotocol/server-filesystem"
- D:/springai-lab/data # ★ 授权目录白名单
# 单次调用超时
request-timeout: 60s
- 从「外部 JSON 文件 +
servers-configuration」改为 yml 里的stdio.connections.<名字>.command/args。 上面的mcp-servers.json仍可作为备选方案保留,但以connections为准。 - 远程 Server 用
url+type(STREAMABLE/STATELESS)声明, 通过headers带鉴权信息。
spring.ai.mcp.client 看当前版本支持哪些键,比翻文档快。
mcp-servers.json 找不到或连接没建立,
优先用 IDEA 的 yml 自动补全把 spring.ai.mcp.client 展开看一遍当前版本支持的键,
再对照官方文档。这类「配置漂移」在新兴协议里很常见,属于正常现象。
③ 用代码直接操作 MCP 客户端(了解机制)
- 在主应用包
com.example.springai.ch08上右键 → New → Java Class,名称填McpInspectService。 - 粘贴下面内容。它注入
List<McpSyncClient>拿到全部连接,提供「列出工具」和「手工调工具」两个方法——这是 MCP 调试的黄金顺序。
容器里每个已配置的 Server 都会有一个 McpSyncClient bean。注入 List<McpSyncClient> 就能拿到全部连接,先手工列工具、再手工调工具——这是 MCP 调试的黄金顺序。
@Service
public class McpInspectService {
/**
* 容器里每个已配置的 Server 都会有一个 McpSyncClient bean。
* 用 List 注入可以拿到全部。
*/
private final List<McpSyncClient> clients;
public McpInspectService(List<McpSyncClient> clients) {
this.clients = clients;
}
/** 列出所有 Server 提供的工具——调试时最先跑这个 */
public void listAllTools() {
for (McpSyncClient client : clients) {
// 服务器信息(名字、版本)
var info = client.getServerInfo();
log.info("=== MCP Server: {} v{} ===",
info.name(), info.version());
// 工具清单
var tools = client.listTools();
tools.tools().forEach(t ->
log.info(" 工具: {} 描述: {} 参数: {}",
t.name(), t.description(), t.inputSchema()));
}
}
/** 手工调用一个工具(不经过模型)——用于验证 Server 端逻辑是否正确 */
public Object callToolManually(String toolName, Map<String, Object> args) {
for (McpSyncClient client : clients) {
boolean has = client.listTools().tools().stream()
.anyMatch(t -> t.name().equals(toolName));
if (has) {
var result = client.callTool(
new McpSchema.CallToolRequest(toolName, args));
return result.content();
}
}
throw new IllegalArgumentException("未找到工具:" + toolName);
}
}
运行验证 ②:主应用启动,确认连上 Server
-
确认
mcp-order-server已打包(target/*.jar存在),且 Node 已安装(filesystem Server 用npx)。然后启动主应用:# 在主应用目录下 cd D:/springai-lab mvn spring-boot:run看到什么算通过:启动日志里出现类似
Connected to MCP server 'order-server'、Connected to MCP server 'filesystem',且无Connection refused。若能正常监听 8080,说明两个 Server 都被拉起并握手成功。... o.s.a.m.c.DefaultMcpClientFactory : Connected to MCP server 'order-server' ... o.s.a.m.c.DefaultMcpClientFactory : Connected to MCP server 'filesystem' ... o.s.b.w.e.t.TomcatWebServer : Tomcat started on port 8080 -
另开一个终端,调一个运维接口把工具清单打印到日志(接口实现见 8.7 节,这里先验证连接本身):
# 触发工具清单打印(若此接口尚未实现,可暂时在 main 里调用 listAllTools()) curl http://localhost:8080/api/v1/mcp-assistant/tools看到什么算通过:主应用日志里出现
=== MCP Server: order-mcp-server v1.0.0 ===以及两个工具名,和=== MCP Server: filesystem ===及其工具列表。这代表「客户端连上了、工具被发现了」。
listAllTools() 确认工具被发现了,callToolManually() 确认 Server 端逻辑正确,
最后才接入模型。这样出问题时你能立刻判断是「连接问题」「Server 问题」还是「模型选择问题」。
- jar 路径错:
order-server的args里 jar 路径不存在。先用java -jar D:/springai-lab/mcp-order-server/target/mcp-order-server-1.0.0.jar手工跑一遍确认能起。 - Node/npx 没装:filesystem Server 用
npx,先手工执行npx -y @modelcontextprotocol/server-filesystem D:/springai-lab/data看能否拉起;首次下载慢,耐心等或挂镜像。 - 配置键没被识别:2.0 把连接配置放在
stdio.connections.*下。用 IDEA 把spring.ai.mcp.client展开,确认当前版本支持的键名,别照抄旧版 JSON 为主的写法。 - 启动特别慢:若
initialized: false,每个 Server 等到首次使用才连。设initialized: true让启动一次性建连。
为什么要 initialized: true?默认(false)时,客户端采用「惰性连接」:某个 Server 直到第一次被用到才去握手建连。教学里这会导致两个坑:① 启动明明没报错,第一次请求却卡顿好几秒(在连 Server);② 如果 Server 路径配错,要等到第一次调用才暴露。设成 true 让应用在启动时就把所有 Server 一次性连好,启动日志立刻能看到 Connected to MCP server ...,排查更直观。
remote-weather(url + type: STREAMABLE)只是展示「远程怎么声明」,本章实操不要求它跑通——因为它需要一个真实存在的远程 MCP 服务在 http://localhost:8081/mcp 监听。如果你想验证远程形态,先按 8.8 末尾把 order-server 改成 Streamable HTTP 独立部署,再把 order-server 的 stdio 配置换成对应的 url / type 即可,其余代码零改动。
stdio.connections.<名字>.command/args 声明要连的 Server;每个 Server 对应一个 McpSyncClient bean,先 listTools 再 callTool 是调试黄金顺序。
8.6把 MCP 工具交给模型:和第 7 章无缝衔接
工具发现流程:从 Server 到模型的四步
| 步 | 发生了什么 | 对应代码 |
|---|---|---|
| 1 | Server 启动,自动扫描 @McpTool bean,声明自己有哪些工具 | OrderMcpTools 上的注解 |
| 2 | Client 按 yml 配置连上 Server,listTools() 拿到工具清单 | McpSyncClient |
| 3 | 框架把每个 MCP 工具包装成 ToolCallback,汇总进 SyncMcpToolCallbackProvider | toolcallback.enabled=true |
| 4 | 你必须显式把 Provider 里的工具传给 .defaultToolCallbacks(...),模型才用得了 | McpChatClientConfig |
开启 spring.ai.mcp.client.toolcallback.enabled=true 后,
Spring AI 会把所有 MCP Server 提供的工具自动包装成 ToolCallback,
然后你就能像使用本地 @Tool 一样把它们交给模型。
- 在主应用包
com.example.springai.ch08上右键 → New → Java Class,名称填McpChatClientConfig。 - 粘贴下面内容。注意注入的是
SyncMcpToolCallbackProvider(异步客户端则注入AsyncMcpToolCallbackProvider),并显式调用.defaultToolCallbacks(mcpTools)。
@Configuration
public class McpChatClientConfig {
/**
* ★ 关键 bean:Spring AI 会把所有 MCP Server 的工具汇总成这个 Provider,
* 它是"MCP 世界"和"Spring AI 工具世界"之间的桥梁。
*
* ★★ 2.0 最容易踩的坑:MCP 工具【不会】自动注册到 ChatClient 上。
* 框架刻意这么设计——避免一个应用无意间把上百个外部工具全塞给模型。
* 你必须显式地把它传给 .defaultTools(...) 或 .tools(...),就像下面这样。
*/
@Bean("mcpAssistantClient")
public ChatClient mcpAssistantClient(
ChatModel chatModel,
// ★ 2.0 的具体类型是 SyncMcpToolCallbackProvider
// (异步客户端则注入 AsyncMcpToolCallbackProvider)
SyncMcpToolCallbackProvider mcpToolCallbackProvider) { // 自动装配
// 取出全部 MCP 工具(本质是 ToolCallback[],与第 7 章完全同类型)
ToolCallback[] mcpTools = mcpToolCallbackProvider.getToolCallbacks();
// 打印出来确认连上了几个工具——启动日志里能看到
org.slf4j.LoggerFactory.getLogger(McpChatClientConfig.class)
.info("已从 MCP Server 加载 {} 个工具", mcpTools.length);
return ChatClient.builder(chatModel)
.defaultSystem("""
你是"智汇"智能助手,可以访问文件系统和订单系统。
规则:
1. 涉及真实数据(文件、订单)时必须调用工具。
2. 需要读取或写入文件时,先确认路径在允许范围内。
3. 工具报错时,向用户说明原因并给出建议。
""")
.defaultToolCallbacks(mcpTools) // ★ 显式注册 MCP 工具(不写这行模型就用不了)
.build();
}
}
运行验证 ③:启动日志确认工具已加载
-
重启主应用(若已在运行先停掉),观察启动日志:
# 主应用目录 cd D:/springai-lab mvn spring-boot:run看到什么算通过:日志中出现
已从 MCP Server 加载 N 个工具(N 应包含 order 的两个 + filesystem 的多个,通常 ≥ 6)。这条日志来自上面McpChatClientConfig里的info(...),它证明「MCP 工具已被取出并准备好了」。... c.e.s.ch08.McpChatClientConfig : 已从 MCP Server 加载 8 个工具
ToolCallback。
你甚至可以混用:.defaultTools(localTools) 再 .defaultToolCallbacks(mcpTools),
模型根本分不出哪个是本地的、哪个是远程的。这就是协议标准化的力量。
- 确认 yml 里
spring.ai.mcp.client.toolcallback.enabled=true已开; - 确认
ChatClient上调用了.defaultToolCallbacks(mcpTools)(或.tools(...)); - 启动日志里应有「已从 MCP Server 加载 N 个工具」;没有就用 8.7 的
/tools接口复核; - 若仍为空,检查
SyncMcpToolCallbackProvider是否被正确注入(异步客户端要注入AsyncMcpToolCallbackProvider,类型不匹配会注入失败)。
顺带纠正一个过时认知(仅废弃语境):1.x 里的 internalToolExecutionEnabled 在 2.0 已不存在,工具循环改由 ToolCallingAdvisor 接管——不要在 2.0 的配置里找这个开关,也别在代码里设置它。同样地,记忆相关的 PromptChatMemoryAdvisor 在 2.0 也已删除,对话记忆键统一用 ChatMemory.CONVERSATION_ID(本章不涉及记忆,遇到旧代码别照抄)。
本地 @Tool 与 MCP 工具:注册方式对照
| 环节 | 第 7 章本地 @Tool | 本章 MCP 工具 |
|---|---|---|
| 工具来源 | 本工程里的 @Tool 方法 | 外部 Server 经协议暴露 |
| 交给模型的方式 | .defaultTools(...) | 取出 ToolCallback[] 后 .defaultToolCallbacks(...) |
| 是否自动注册 | 由 @Tool 扫描自动进 ChatClient | 否,必须显式传(2.0 铁律) |
| 模型看到的 | 工具名 + 描述 + schema | 完全相同,无差别 |
记住:本地 @Tool 和 MCP 工具最终都变成 ToolCallback,所以你完全可以把两者混着交给模型——本地一个、远程一个,模型分不清。唯一区别就是「MCP 那个需要你多写一行显式注册」。
SyncMcpToolCallbackProvider 失败 / 为 null 的两类原因:
- 类型不匹配:同步客户端就注入
SyncMcpToolCallbackProvider;若工程里混入了异步模式或你写成了AsyncMcpToolCallbackProvider,Spring 找不到对应类型会直接启动报错。确认 yml 没开异步、注入类型与客户端一致。 - 工具根本没被发现:若所有 Server 都没连上(路径错 / Node 没装),Provider 可能是空集合——此时
getToolCallbacks()返回长度 0 的数组,不报错但模型用不了工具。先用 8.5 的/tools确认连接和工具数。
@McpTool(description=...) 当成「给模型看的说明书」来写:① 说清它做什么(查订单状态);② 说清何时用(用户问发货 / 物流时);③ 说清何时不用(不要用于退款 / 余额)。像 8.4 的 queryOrderStatus 那样写,模型命中率会明显更高。模糊的「处理订单相关」描述,模型往往选择忽略。
ToolCallback 后,你必须显式 .defaultToolCallbacks(...),这是 2.0 头号铁律。
8.7完整示例:MCP 文件助手
做一个小助手:既能用我们自己写的订单 MCP Server,也能用官方文件系统 MCP Server(跨语言复用,Server 是 Node 写的)。重点是验证「MCP 工具要显式注册、且权限边界在 Server 端」。
端到端数据流:一次问答怎么走到 Server
把 8.7 的完整链路在脑子里过一遍,出问题才知道断在哪一层:
动手 checkpoint:把 order-server 的 jar 路径(D:/springai-lab/mcp-order-server/target/...)和 filesystem 的白名单目录(D:/springai-lab/data)改成你机器上的真实路径,否则 8.7 第 ②③④ 步会连不上或越权判定失效。路径错了不是 MCP 的问题,是配置问题。
① 先构建并验证 Server(回顾)
8.4 节已经做过这一步,这里再给一次完整命令,确保 jar 就绪:
# 打包 MCP Server
cd D:/springai-lab/mcp-order-server
mvn clean package -DskipTests
# 预期输出:BUILD SUCCESS,并在 target/ 下生成 mcp-order-server-1.0.0.jar
# 手工测试:往它的 stdin 里发一条 MCP 初始化消息(最原始的验证方式)
# 更推荐用官方 Inspector(见 8.8 节),可视化操作
java -jar target/mcp-order-server-1.0.0.jar
② 业务服务(含安全边界)
- 在包
com.example.springai.ch08上右键 → New → Java Class,名称填FileAssistantService。 - 粘贴下面内容。它注入名为
mcpAssistantClient的 ChatClient(即 8.6 节配的那个),把用户问题交给模型。
@Slf4j
@Service
public class FileAssistantService {
private final ChatClient mcpAssistantClient;
public FileAssistantService(
@Qualifier("mcpAssistantClient") ChatClient client) {
this.mcpAssistantClient = client;
}
public String ask(String question) {
// 把当前用户身份放进 ToolContext(虽然 MCP Server 目前没用它,
// 但这是标准做法,未来 Server 端做鉴权时会用到)
return mcpAssistantClient.prompt()
.user(question)
.toolContext(Map.of(
"userId", SecurityContextHolder.currentUserId(),
"tenantId", SecurityContextHolder.currentTenantId()))
.call()
.content();
}
}
③ 接口(含一个运维接口,用于排查工具注册情况)
- 在包
com.example.springai.ch08上右键 → New → Java Class,名称填McpAssistantController。 - 粘贴下面内容。其中
/tools接口会调用 8.5 节的listAllTools(),是排查「工具没注册上」的最快手段。
@RestController
@RequestMapping("/api/v1/mcp-assistant")
public class McpAssistantController {
private final FileAssistantService assistantService;
private final McpInspectService inspectService;
public McpAssistantController(FileAssistantService a, McpInspectService i) {
this.assistantService = a;
this.inspectService = i;
}
@PostMapping("/ask")
public Map<String, String> ask(@RequestBody Map<String, String> body) {
return Map.of("answer", assistantService.ask(body.get("question")));
}
/**
* 列出所有已连接的 MCP Server 及其工具。
* ★ 强烈建议保留这个接口——它是排查"工具没注册上"的最快手段。
* 生产环境记得加管理员权限。
*/
@GetMapping("/tools")
public Map<String, Object> listTools() {
inspectService.listAllTools();
return Map.of("ok", true, "message", "工具清单已打印到日志");
}
}
④ 端到端验证(四个用例)
下面四个用例覆盖「注册成功 / 远程调用 / 跨语言 / 越权拦截」。第 ④ 步是本章最有价值的验证。
-
先确认工具真的注册上了(调 8.5 已配好的
/tools):curl http://localhost:8080/api/v1/mcp-assistant/tools看到什么算通过:响应返回
{"ok":true,...},且主应用日志出现两个 Server 的工具清单。# 日志期望: # === MCP Server: order-mcp-server v1.0.0 === # 工具: queryOrderStatus 描述: 查询指定订单号的当前状态与预计发货时间… # 工具: listRecentOrders 描述: 列出某个客户最近 5 笔订单… # === MCP Server: filesystem v0.6.2 === # 工具: read_file / write_file / list_directory / … -
用远程 MCP 工具查订单(模型不知道这是远程的):
curl -X POST http://localhost:8080/api/v1/mcp-assistant/ask \ -H "Content-Type: application/json" \ -d '{"question":"订单 2024001 发货了吗?"}'看到什么算通过:答案包含「未发货 / 2024-03-08」,说明模型调用了
queryOrderStatus并把结果组织成了自然语言。 -
用文件系统 MCP 工具(跨语言:Server 是 Node 写的):
curl -X POST http://localhost:8080/api/v1/mcp-assistant/ask \ -H "Content-Type: application/json" \ -d '{"question":"列出 D:/springai-lab/data 目录下的所有文件"}'看到什么算通过:模型调用
list_directory并整理成清单返回——证明 Java 主应用成功驱动了一个 Node 实现的 Server。 -
越权测试:让文件助手读白名单外的目录:
curl -X POST http://localhost:8080/api/v1/mcp-assistant/ask \ -H "Content-Type: application/json" \ -d '{"question":"帮我读一下 C:/Windows/System32/drivers/etc/hosts 文件"}'看到什么算通过:模型尝试调用文件工具但被 Server 拒绝(返回错误/说明无权访问),因为白名单只授权了
D:/springai-lab/data。这验证了 MCP 的权限边界在 Server 端。
# ① 先看工具有没有注册上
curl http://localhost:8080/api/v1/mcp-assistant/tools
# 检查日志,期望看到类似:
# === MCP Server: order-mcp-server v1.0.0 ===
# 工具: queryOrderStatus 描述: 查询指定订单号的当前状态与预计发货时间…
# 工具: listRecentOrders 描述: 列出某个客户最近 5 笔订单…
# === MCP Server: filesystem v0.6.2 ===
# 工具: read_file / write_file / list_directory / …
# ② 用远程 MCP 工具查订单(模型不知道这是远程的)
curl -X POST http://localhost:8080/api/v1/mcp-assistant/ask \
-H "Content-Type: application/json" \
-d '{"question":"订单 2024001 发货了吗?"}'
# 期望:答案来自 order-mcp-server 的工具返回
# ③ 用文件系统 MCP 工具(跨语言:Server 是 Node 写的)
curl -X POST http://localhost:8080/api/v1/mcp-assistant/ask \
-H "Content-Type: application/json" \
-d '{"question":"列出 D:/springai-lab/data 目录下的所有文件"}'
# 期望:模型调用 list_directory 工具并整理成清单
# ④ 越权测试:让文件助手读白名单外的目录
curl -X POST http://localhost:8080/api/v1/mcp-assistant/ask \
-H "Content-Type: application/json" \
-d '{"question":"帮我读一下 C:/Windows/System32/drivers/etc/hosts 文件"}'
# 期望:MCP Server 拒绝访问(因为白名单只授权了 D:/springai-lab/data)
# ★ 这验证了 MCP 的价值:权限约束在 Server 端,而不是靠模型自觉
- Server 本身对不对?用 8.8 的 Inspector 直连,能列工具、能调用 → Server OK。
- 客户端连上了没?看启动日志
Connected to MCP server …,或调/tools看清单。 - 连接到 Server 但工具为空?检查
toolcallback.enabled与SyncMcpToolCallbackProvider注入。 - 工具有了但模型不用?回到「描述/提示词」(同第 7 章),并确认
.defaultToolCallbacks(...)已调用。
若 /ask 返回空答案或模型说「没有工具」:八成是漏了 8.6 的显式注册,或 /tools 显示工具数为 0(说明连接或注册环节断了,回到第 ②、③ 步)。
/tools 接口,看日志里工具数是不是 0——是 0 就回 8.5 / 8.6 查连接和显式注册;② 工具数正常但仍不用,多半是「工具描述写得太含糊」,模型不知道何时该调——把 @McpTool(description=...) 写具体(像 8.4 那样写明「何时调用、不要用于什么」);③ 确认 .defaultToolCallbacks(mcpTools) 真的被调用了,且没有在别处被一个没带工具的 ChatClient 覆盖。
端到端验收清单(提交作业前逐项打勾):
- ☐
/tools日志里能看到 order-mcp-server 和 filesystem 两个 Server 的工具; - ☐ 查订单 curl 返回了真实订单状态(不是「我不知道」);
- ☐ 列目录 curl 成功调用了 Node 实现的 list_directory;
- ☐ 读越权路径 curl 被 Server 拒绝(白名单生效);
- ☐ 故意删掉
.defaultToolCallbacks(...)后,模型明确「不会用工具」——你亲眼验证了铁律。
listRecentOrders 拿到订单号,再用 queryOrderStatus 查明细),也可能一句话里同时查订单和列目录。你不需要为此写任何额外代码——ToolCallingAdvisor 会循环执行:模型出工具调用 → 本地执行 → 结果回填 → 模型继续,直到模型给出最终自然语言。你只要在 system prompt 里把「何时用什么工具」写清楚即可。
/tools 看注册、查订单验远程、列目录验跨语言、读越权目录验 Server 端权限边界。
8.8调试与常见坑
用官方 Inspector 调试 Server
MCP Inspector 是一个网页版调试工具,能列出工具、手工填参数调用、看原始报文。它在定位「Server 端问题」时比看日志高效得多。
-
在
mcp-order-server工程目录下执行(需联网首次拉取):# MCP Inspector 是一个网页版调试工具,能列出工具、手工填参数调用、看原始报文 npx @modelcontextprotocol/inspector java -jar target/mcp-order-server-1.0.0.jar # 它会启动一个本地 Web 界面(通常是 http://localhost:6274) # 在里面你可以: # 1. 看到 Server 声明的全部工具与参数 schema # 2. 手工填参数调用,验证 Server 端逻辑 # 3. 查看完整的 JSON-RPC 请求/响应报文看到什么算通过:浏览器打开
http://localhost:6274,左侧列出queryOrderStatus/listRecentOrders,填2024001点调用,右侧返回订单状态 JSON。这证明 Server 端逻辑正确,之后再从客户端连它。
- 用 Inspector 直连 Server —— 确认 Server 本身没问题(工具能列出、能调用成功)。
- 用
listAllTools()从客户端看 —— 确认连接建立、工具被发现。 - 用
callToolManually()手工调 —— 确认客户端到 Server 的调用链路通。 - 最后才接模型 —— 如果前三步都过了但模型不用工具,那是「描述/提示词」的问题,不是 MCP 的问题。
常见坑排错表
| 现象 | 原因 | 解决 |
|---|---|---|
| 客户端报「无法解析消息 / Unexpected token」 | Server 的 stdout 被日志污染 | stdio 模式设 web-application-type: none;禁止 System.out.println;日志改到文件 |
| Server 进程启动后立刻退出 | main 方法跑完就结束了 | 用 Spring Boot 常驻;不要手动 exit |
Connection refused / 连不上 | Server jar 路径错、端口错、Server 没启动 | 用绝对路径;先手工 java -jar 跑一遍确认能起来 |
| 工具列表为空 | 没提供 ToolCallbackProvider bean,或 toolcallback.enabled=false | 检查 McpServerConfig;检查客户端的 toolcallback.enabled |
| 工具能列出但调用报错 | 参数 schema 与接收类型不匹配 | 用 Inspector 看参数 schema,核对 Java 方法签名 |
npx 相关 Server 连不上 | Node 未安装 / npx 首次下载慢 / 网络问题 | 先手工执行 npx -y @modelcontextprotocol/server-filesystem 看是否能跑起来 |
| 远程 HTTP Server 401 | 缺少 Authorization 头 | 在 mcp-servers.json 的 headers 里配置 token |
| 启动特别慢(每次几十秒) | initialized: false,每个 Server 等到首次使用时才连接 | 设 initialized: true,启动时一次性建立连接 |
| 模型不用 MCP 工具 | 工具描述不清;或工具太多;或忘了显式注册 | 用 8.7 的 /tools 接口确认工具真的注册上了;减小工具数量;确认 .defaultToolCallbacks(...) |
| 工具调用超时 | Server 端逻辑慢(如大文件读取) | 调大 request-timeout;Server 端做分页/限制 |
- Server 本身对不对?用 Inspector 直连,能列工具、能调用 → Server OK。
- 客户端连上了没?看启动日志有没有「Connected to MCP server …」,或调
/tools接口看清单。 - 连接到 Server 了但工具为空?检查
toolcallback.enabled与SyncMcpToolCallbackProvider注入。 - 工具有了但模型不用?回到「描述/提示词」(同第 7 章),并确认
.defaultToolCallbacks(...)已调用。
选做:把 stdio Server 改成 Streamable HTTP 独立部署
课程建议先 stdio 跑通,再试远程。把订单 Server 从「本地子进程」改成「独立 Web 服务」只需三步:
- 把
pom.xml里的spring-ai-starter-mcp-server换成spring-ai-starter-mcp-server-webmvc(并保留spring-boot-starter-webmvc)。 - 改
application.yml:删掉web-application-type: none和stdio: true,改为spring.ai.mcp.server.protocol=STREAMABLE(或STATELESS),并设server.port(如 8081)。 - 重新
mvn clean package,用java -jar直接启动(不再是子进程,而是一个常驻 Web 服务)。 - 主应用那边:把
stdio.connections.order-server整段删掉,改用远程声明(url: http://localhost:8081/mcp+type: STREAMABLE),其余代码一行不用动。
看到什么算通过:启动订单 Server 能看到 Tomcat 监听 8081;主应用启动日志仍出现 Connected to MCP server 'order-server';8.7 的 curl 验证全部照常通过。这证明「换传输方式对业务代码零侵入」——这正是协议标准化的好处。
8.9自测题
Q1. MCP 相比进程内 @Tool 的核心优势是什么?什么情况下不需要 MCP?
核心优势:跨进程、跨语言、可复用、可独立治理(鉴权限流审计),
让工具从「应用的一部分」变成「企业的公共能力」。
不需要 MCP 的情况:工具只服务于自己这一个应用、数量少、不需要独立权限管控——
这时候用 @Tool 更简单,且没有进程间通信开销。
Q2. Host、Client、Server 各自的职责是什么?
Host 是承载 AI 应用与用户界面的宿主程序(我们的 Spring Boot 应用); Client 是 Host 内部的连接器,与 Server 一对一连接,负责协议握手与消息收发; Server 是暴露能力(Tools/Resources/Prompts)的独立服务。
Q3. 为什么 stdio 模式下必须关闭 Web 容器、禁止 System.out?
因为 stdout 是 MCP 的通信通道,协议消息通过它传输。
Spring Boot 的启动日志、System.out.println 都会往 stdout 写内容,
导致客户端收到的不再是合法 JSON-RPC 消息,解析立刻失败。
解决办法:关闭 Web 容器、日志输出改到 stderr 或文件。
Q4. 从 MCP Server 拿到的工具,和本项目的 @Tool 方法,在传给模型时有区别吗?
没有区别。两者最终都被统一成 ToolCallback,
通过 .defaultTools(...) 或 .defaultToolCallbacks(...) 交给模型。
模型看到的只有「工具名 + 描述 + 参数 schema」,它无法区分工具是本地还是远程的。
这正是 MCP 的价值:在协议层统一,在实现层解耦。
Q5.【动手题】把第 7 章的「天气查询工具」改造成一个独立的 MCP Server,并用 Inspector 验证它能被外部调用。
步骤:① 新建工程,加 spring-ai-starter-mcp-server;
② 把 WeatherTools 类复制过来,把 @Tool/@ToolParam 改为 @McpTool/@McpToolParam(包 org.springframework.ai.mcp.annotation);
③ yml 里设 web-application-type: none 和 spring.ai.mcp.server.stdio=true;
④ mvn package 后用 npx @modelcontextprotocol/inspector java -jar target/*.jar 连接;
⑤ 在 Inspector 界面里调用 queryWeather,确认返回正确。
(注:若用 WebMVC 远程形态,配置键以官方文档为准。)
8.10实操作业与本章验收
课堂练习(课上 30 分钟,分组完成):
- 用 stdio 模式新建
mcp-order-server,照抄 8.4 的OrderMcpTools,mvn package后用 Inspector 连接,确认queryOrderStatus能被列出并调用成功。 - 在主应用配置里连接至少两个 Server(自写的订单 Server + 官方 filesystem),调用 8.7 的
/tools接口,确认两边工具都出现;再故意删掉.defaultToolCallbacks(mcpTools),观察模型是否「突然不会用工具了」——直观记住「MCP 工具必须显式注册」这条铁律。
课后作业(独立完成,提交构建与验证截图):
- 把第 7 章的天气工具改造为一个独立 MCP Server(stdio),用 Inspector 验证
queryWeather可被外部调用;再把该 Server 接到主应用的 ChatClient 上,用 curl 验证端到端问答。 - 写 250 字说明:结合 8.7 第 ④ 步,描述为什么「把文件系统访问做成 MCP Server、用目录白名单做权限」比「在主应用里直接写文件读取工具」更适合企业级治理。
- 能画出 Host / Client / Server 的关系图,并说出 stdio / SSE / Streamable HTTP 三种传输方式的区别与适用场景。
- 能独立写一个 stdio 模式的 MCP Server(用
@McpTool/@McpToolParam,包org.springframework.ai.mcp.annotation),并用 Inspector 调通。 - 能在主应用里配置 MCP Client,连上两个以上 Server(含一个 Node 实现的),并把工具显式注册到 ChatClient(
SyncMcpToolCallbackProvider→.defaultToolCallbacks(...))。 - 能解释「为什么 MCP 工具不会自动注册到 ChatClient、以及为什么 MCP 的权限边界比写在提示词里更可靠」。