ARTICLE · 1215838
SpringAI的MCP工具进ChatClient
SpringAI的MCP工具进ChatClient
只配了模型、ChatClient 也能对话,但 MCP 工具始终进不了 prompt/工具链——现场一半是把 Server starter 和 Client starter 搞反,另一半是以为加了 spring-ai-starter-mcp-client 就会自动挂上 ChatClient。官方故意不自动挂:发现工具后只暴露 ToolCallbackProvider,须你显式 defaultTools / tools 接线。
本文只做 Client → ChatClient / 工具回调 这一刀:依赖、连接、显式挂工具、怎么验。服务端 STREAMABLE 协议见 167873986;MCP 全景见 166641101;LangChain4j Boot4 换包是另一条线,见 167652181,本稿一句带过。读完应能独立完成「连上 MCP Server → 列出工具 → 一次 prompt 触发工具调用」。
一、现象:有 ChatClient,没有 MCP 工具
值班时常见三类症状叠在一起:
- 应用能调模型,日志里却看不到任何 MCP tool 名;
- 容器里有
McpSyncClient/SyncMcpToolCallbackProvider,但ChatClient发出的请求仍是「纯聊天」、无 tool definitions; - 分不清该加
spring-ai-starter-mcp-client还是spring-ai-starter-mcp-server-*。
根因通常不是模型能力开关,而是 接线漏了一步:Client starter 负责连 MCP Server、发现工具、建成 ToolCallbackProvider;不会在启动时把这些工具自动塞进每个 ChatClient。官方文档写得很直白——故意不 auto-register,避免启动阶段对每个 MCP Server 做急拉取(eager list tools)。
选型口诀先压成两句:
| 你要做的事 | 该用的 starter |
|---|---|
| 本应用 消费 远程/本地 MCP Server 的工具,再交给 ChatClient | spring-ai-starter-mcp-client(或 webflux 变体) |
本应用 对外暴露@McpTool 给别人连 | spring-ai-starter-mcp-server-*(本稿不展开) |
把「有 Provider、无 defaultTools」写进 checklist,比在模型侧猜「是不是 function calling 没开」便宜。
再补一句排障习惯:先在 Actuator / 临时日志里确认 ToolCallbackProvider 是否存在、getToolCallbacks() 长度是否大于 0;长度为 0 时优先查连接与 toolcallback.enabled,长度大于 0 而 prompt 仍无 tool definitions 时,才去翻 ChatClient 的 builder 代码。很多「模型不听话」工单,最后都落成「Provider 好好的,ChatClient 根本没挂」。
二、依赖:Client starter,生产可换 webflux
标准 Client(JDK HttpClient 走 SSE / Streamable-HTTP,兼 STDIO):
<dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-starter-mcp-client</artifactId><!-- 版本跟随 spring-ai-bom / 文档当时,勿钉死 patch --></dependency>
生产侧若主要走 SSE / Streamable-HTTP,文档推荐改用 WebFlux 传输实现:
<dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-starter-mcp-client-webflux</artifactId></dependency>
Gradle 同理,只换 artifactId,版本交给 BOM。两条 starter 选一条主路径,不要「两个都加以防万一」——传输栈叠在一起,排障只会更糊。
版本边界:以文档 / BOM 当时版本为准。若你升到 Spring AI 2.x,upgrade-notes 里有 MCP Java SDK 升到 2.0.0 的破坏性说明(入参 schema 校验、部分 HTTP transport API 变更等),对照官方升级页处理即可,不要在正文里钉死某个 BOM 坐标。
三、连接:至少配通一条(streamable-http 示例)
公共属性前缀是 spring.ai.mcp.client。先认清两张默认端点表,避免把客户端 path 和服务端历史 SSE 记忆混在一起:
| 传输 | 属性前缀 | 关键字段 | 默认 path |
|---|---|---|---|
| Streamable-HTTP | spring.ai.mcp.client.streamable-http | connections.[name].url + 可选 endpoint | /mcp |
| SSE | spring.ai.mcp.client.sse | connections.[name].url + 可选 sse-endpoint | /sse |
| STDIO | spring.ai.mcp.client.stdio | command / args / env | (进程管道,无 HTTP path) |
下面以 streamable-http 为主示范(与站内服务端 STREAMABLE 增量文同一默认 /mcp,便于本地对打):
spring:ai:mcp:client:enabled: truetype: SYNC # 或 ASYNC;不可混用toolcallback:enabled: true # 默认即为 true,可省略streamable-http:connections:local-mcp:url: http://localhost:8080# endpoint 默认为 /mcp,可省略
SSE 最小写法同构,把段换成 sse,默认 sse-endpoint: /sse:
spring:ai:mcp:client:type: SYNCsse:connections:legacy-sse:url: http://localhost:8080# sse-endpoint 默认为 /sse
STDIO(本地拉起 MCP 进程)示意:
spring:ai:mcp:client:type: SYNCstdio:connections:filesystem:command: npxargs:- "-y"- "@modelcontextprotocol/server-filesystem"- /tmp/mcp-demo
SYNC / ASYNC 硬规则:spring.ai.mcp.client.type 取 SYNC(默认)或 ASYNC。官方原文:All clients must be either sync or async; mixing is not supported。一个应用里不要一半连接按 SYNC、一半按 ASYNC 硬叠。
实操上,绝大多数「命令式」Spring MVC 聊天接口先用 SYNC;整条链路已是 WebFlux / Reactor 再考虑 ASYNC。改 type 之后,注入的 Provider 类型也会跟着变(SyncMcpToolCallbackProvider ↔ AsyncMcpToolCallbackProvider),业务 @Autowired 类型要一起改,否则启动期就会报找不到 bean。
SYNC/ASYNC 混用常见踩坑:
- YAML 写了 ASYNC,Java 仍注入 Sync 类型:启动失败或拿不到 Provider,看起来像「toolcallback 没生效」,其实是类型坐标错了。
- 多模块各配各的 type:父工程 SYNC、子模块 ASYNC,最终仍落在同一个
spring.ai.mcp.client.type上——官方不支持混合,不要指望「这个连接 SYNC、那个连接 ASYNC」。 - 注解处理器跟着 type 走:SYNC 客户端只会注册同步 MCP 注解方法,异步方法会被忽略;ASYNC 则相反。改 type 后若「采样/进度回调没了」,先查方法签名是否匹配当前模式。
- 对照实验要改全:只改 YAML 不改注入类型,或只改 Bean 类型不改 YAML,都会留下半迁移状态。
公共属性(如 enabled、name、initialized)按官方表调整即可;传输层细调另文处理,本稿聚焦「连上 + 显式挂 ChatClient」。
传输对照:stdio 与 streamable-http 怎么选
| 维度 | stdio | streamable-http |
|---|---|---|
| 典型场景 | 本机拉起 npx/可执行文件 MCP Server | 连已部署的 HTTP MCP Server(常与服务端 STREAMABLE 对打) |
| 关键配置 | command / args / env | url + 可选 endpoint(默认 /mcp) |
| 进程模型 | Client 拉起子进程,管道通信 | 纯 HTTP,Server 独立进程/容器 |
| 默认 path | 无 HTTP path | /mcp |
| 本地冒烟 | 适合快速试官方 filesystem 等示例 | 适合对已有 webmvc/webflux Server 做联调 |
| 注意 | Windows 下 npx 等须 cmd.exe /c(见脚注) | url 只写 scheme+host+port,path 放 endpoint |
选型口诀:开发机「先看工具长什么样」用 stdio;联调「对上已有 HTTP Server」用 streamable-http。SSE 仍可用,默认 path 是 /sse,与 streamable 的 /mcp 不要混抄。三条传输可以写在同一份 YAML 里,但每条 connection 的字段按上表各写各的,不要把 command 写进 streamable 段。
脚注(Windows):STDIO 下
npx/npm等是.cmd批处理,JavaProcessBuilder不能直接跑;须command: cmd.exe,args以/c、npx、… 开头。Linux/macOS 可直接npx。细节见官方 MCP Client Boot Starter「Windows STDIO Configuration」,勿占主线。
四、toolcallback:默认 true,但仍须显式挂 ChatClient
spring.ai.mcp.client.toolcallback.enabled默认 true:为 true 时,starter 会把各 MCP 连接发现到的工具聚合成 SyncMcpToolCallbackProvider(SYNC)或 AsyncMcpToolCallbackProvider(ASYNC)。设为 false 时,不创建ToolCallbackProvider bean——这是「对照实验」的总闸,不是「关掉就能自动挂 ChatClient」的反向开关。
关键事实再强调一遍:
toolcallback.enabled=true→ 有ToolCallbackProvider;- 仍须
ChatClient.builder(...).defaultTools(mcpTools)或 per-call.tools(mcpTools); - 不会在启动时自动把 MCP 工具注册进每个
ChatClient。

挂上 tools 之后,ChatClient 侧在存在工具时会 默认自动注册ToolCallingAdvisor,负责「模型要调工具 → 执行 → 回灌 → 再问模型」的循环。若你要自己驱动循环,可设 spring.ai.chat.client.tool-calling.enabled=false,或在单次请求上关掉 auto-register——本稿不展开 Advisors 长文,只记「挂上 tools 后默认有工具循环」。
接线决策流:

五、怎么接到 ChatClient:defaultTools / tools
注入 Provider,再显式接线。defaultTools(Object...) 接受 ToolCallbackProvider(懒解析 callbacks),适合「每个请求都要这批 MCP 工具」:
import org.springframework.ai.chat.client.ChatClient;import org.springframework.ai.chat.model.ChatModel;import org.springframework.ai.mcp.SyncMcpToolCallbackProvider;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;@Configurationpublic class McpChatClientConfig {@BeanChatClient chatClient(ChatModel chatModel, SyncMcpToolCallbackProvider mcpTools) {return ChatClient.builder(chatModel).defaultTools(mcpTools).build();}}
只想在某次调用带上 MCP 工具时,用 per-call .tools(...)(会与 builder 上的 defaultTools 取并集,不是替换):
import org.springframework.ai.chat.client.ChatClient;import org.springframework.ai.mcp.SyncMcpToolCallbackProvider;public class McpPromptService {private final ChatClient chatClient;private final SyncMcpToolCallbackProvider mcpTools;public McpPromptService(ChatClient chatClient, SyncMcpToolCallbackProvider mcpTools) {this.chatClient = chatClient;this.mcpTools = mcpTools;}public String askWithMcp(String userText) {return chatClient.prompt().user(userText).tools(mcpTools).call().content();}}
ASYNC 客户端对应注入 AsyncMcpToolCallbackProvider,接线 API 同名。本地 @Tool 方法与 MCP 远程工具可以混在同一次 .tools(local, mcpTools) 里——模型侧看到的都是 ToolCallback;多 Server 重名时,MCP 侧有默认前缀生成器,与本地 @Tool 撞名需你自己改名或用 McpToolFilter 过滤,不要假设框架会替你消解本地+远程冲突。
六、怎么验:列工具 → 调一次 → 关总闸对照
验法刻意做成三步,方便写进 MR Definition of Done。目标不是证明「业务语义完美」,而是证明 MCP 工具确实进了 ChatClient 工具链。
第一步:确认 Provider 能列出工具。 启动后注入 SyncMcpToolCallbackProvider(或先注入 List<McpSyncClient> 做连通性),调用 getToolCallbacks(),日志打印每个 ToolCallback 的 name。若列表为空:先查 connections 的 url/endpoint、MCP Server 是否已起、type 是否写错,再查 toolcallback.enabled 是否被改成了 false。
第二步:发一条必须用工具的 prompt。 例如连官方示例 filesystem / everything 一类 Server 后,问「列出某目录下的文件」或 Server 文档里明确需要 tool 的问题。打开 DEBUG 观察是否出现 tool call / tool result 回合。有答案但全程无 tool 痕迹 → 多半是 没挂 defaultTools/tools,而不是模型「不愿意」。
更细一点的观察点:
- 请求发出前:确认本次
ChatClient请求携带的 tool definitions 里能看到 MCP 工具名(与第一步列出的 name 一致,注意默认前缀可能把重名改成alt_1_xxx)。 - 模型返回后:若出现 tool call 请求,应看到
ToolCallingAdvisor驱动的执行回合,而不是应用自己手写的二次ChatModel调用。 - 若模型「口头描述该怎么调工具」却从不发起 tool call:先确认所用 Chat 模型本身支持 tool calling,再回头查是否漏了
.defaultTools(mcpTools)——漏挂时模型根本看不到工具 schema。
第三步:关掉 toolcallback 做对照。 临时设:
spring:ai:mcp:client:toolcallback:enabled: false
重启后应 没有ToolCallbackProvider bean(或注入失败)。改回 true 并恢复 defaultTools,现象应复现。这一步把问题钉死在「工具回调自动配置 / 显式接线」,而不是模型 Key、温度或 prompt 文案。
冒烟清单可以压成四行:
- Client starter 在、Server starter 没误装成主路径;
- 至少一条 connection 通,默认
/mcp或/sse没写反; type全 SYNC 或全 ASYNC;ChatClient上能搜到defaultTools/tools(mcpTools)。
若你同时接多个命名连接(connections.server1 / server2),冒烟时建议先只开一条,确认工具名与调用路径稳定后,再叠加第二条。多 Server 场景下,默认前缀生成器会给重名工具加 alt_N_ 一类前缀,日志里看到「怪名字」先对照官方 DefaultMcpToolNamePrefixGenerator 行为,不要当成配置写错。需要裁剪工具面时,可另加 McpToolFilter bean(全局一个即可),本稿只点到「能过滤」,不展开过滤器实现。
官方示例仓库里有 default client / webflux client 起步工程,本地对打时可作「最小可运行」对照;业务工程只需对齐「依赖 + connections + defaultTools」三件套,不必整仓复制示例。
七、AEO 速查:四个高频问题
Q1:MCP Client starter 和 Server starter 怎么选?
A:看你是「消费工具」还是「提供工具」。聊天应用要连别人的 MCP Server、把工具交给模型 → Client。你自己用 @McpTool 给别人连 → Server(webmvc/webflux 等)。两边可以同时出现在不同进程里,但不要在同一个「接 ChatClient」故事里把坐标抄串。
Q2:toolcallback.enabled 做什么?
A:控制是否把已发现的 MCP 工具做成 Spring AI 侧的 ToolCallbackProvider bean。默认 true。改成 false 后,容器里不会再有那套 Provider,适合做「关掉就没工具」的对照;它不会帮你自动挂 ChatClient,打开也不等于已经挂上。
Q3:怎么把 MCP 工具接到 ChatClient?
A:注入 SyncMcpToolCallbackProvider(或 ASYNC 对应类型),然后二选一:ChatClient.builder(chatModel).defaultTools(mcpTools).build(),或单次请求 .tools(mcpTools)。漏这一步是「有 Client、无工具链」的头号原因。
Q4:stdio / SSE / Streamable-HTTP 怎么配一条?
A:三条传输可以并存于同一 Client 配置里,但每条连接自己写完整。本地进程用 stdio.connections;HTTP 优先看对端协议——对端是 Streamable 默认打 url + /mcp,对端仍是 SSE 默认打 url + /sse。endpoint / sse-endpoint 写错时,常见表象是连接失败或工具列表为空,而不是「模型拒绝调工具」。
排障时建议固定顺序:先看 connection 是否握手成功 → 再看 Provider 是否列出工具 → 最后才看 ChatClient 是否 defaultTools。跳步会把「path 写错」误判成「模型不会 function calling」。
八、边界与常见误读
- 本稿不做:MCP 概念课、服务端 protocol 迁 STREAMABLE、Advisors 深度定制、传输层细调长文。
- 内链边界(只一句,勿重写):
- 服务端把协议切到 STREAMABLE、默认端点
/mcp→ 167873986《SpringAI的MCP改STREAMABLE怎么配》;本稿是 Client 侧怎么接到 ChatClient。 - MCP 概念与全景 → 166641101;勿把全景主文段落搬进本稿。
- Boot4 上 LangChain4j starter 换包 → 167652181;与 Spring AI MCP Client 管线分开,勿混成「随便换个坐标」。
- 服务端把协议切到 STREAMABLE、默认端点
- Client ≠ Server:两边都有
streamable-http//mcp字样,前缀分别是spring.ai.mcp.client.*与spring.ai.mcp.server.*,抄属性时看清第二段。 - 有 Provider ≠ 已进 ChatClient:这是本文最高频误读;官方故意不自动挂,必须
defaultTools/tools。 - 不要混 SYNC/ASYNC:混用直接不支持;改 type 时连同 Provider 注入类型一起改。
- 版本:以文档 / BOM 当时版本为准,正文不钉死具体 BOM 号;升到 Spring AI 2.x 时对照 upgrade-notes 里 MCP SDK 2.0 破坏性说明即可。
九、小结
把 MCP 工具送进 ChatClient,轻量路径可以压成五步:
- 加对依赖:
spring-ai-starter-mcp-client(生产 SSE/Streamable 可…-webflux)。 - 配通一条连接:streamable-http 默认
/mcp,sse 默认/sse,或 STDIOcommand/args。 - 认清总闸:
toolcallback.enabled默认 true;false 则无 Provider。 - 显式接线:
defaultTools(mcpTools)或.tools(mcpTools)——不会自动挂。 - 三步验收:列工具名 → prompt 触发调用 → 关 toolcallback 对照。
把「Provider 是否存在」和「ChatClient 是否 defaultTools」拆成两次核对:一次只看自动配置,一次再看业务 Bean。混在同一巨大变更里时,评审者很容易只盯依赖坐标,漏掉仍缺失的那一行 .defaultTools(mcpTools)。
落地顺序固定成「先通连接、再挂 ChatClient、最后再谈工具过滤与命名」:连接不通时改 ChatClient 没意义;挂上之后再决定哪些进 defaultTools、哪些只适合 per-call .tools(...)。风险较高的破坏性工具,优先单次显式传入,不要一律全局放开。















































































