夜雨聆风学习资料网

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 工具

值班时常见三类症状叠在一起:

  1. 应用能调模型,日志里却看不到任何 MCP tool 名;
  2. 容器里有 McpSyncClient / SyncMcpToolCallbackProvider,但 ChatClient 发出的请求仍是「纯聊天」、无 tool definitions;
  3. 分不清该加 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 的工具,再交给 ChatClientspring-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-HTTPspring.ai.mcp.client.streamable-httpconnections.[name].url + 可选 endpoint/mcp
SSEspring.ai.mcp.client.sseconnections.[name].url + 可选 sse-endpoint/sse
STDIOspring.ai.mcp.client.stdiocommand / args / env(进程管道,无 HTTP path)

下面以 streamable-http 为主示范(与站内服务端 STREAMABLE 增量文同一默认 /mcp,便于本地对打):

spring:  ai:    mcp:      client:        enabled: true        type: 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: SYNC        sse:          connections:            legacy-sse:              url: http://localhost:8080              # sse-endpoint 默认为 /sse

STDIO(本地拉起 MCP 进程)示意:

spring:  ai:    mcp:      client:        type: SYNC        stdio:          connections:            filesystem:              command: npx              args:                - "-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 混用常见踩坑:

  1. YAML 写了 ASYNC,Java 仍注入 Sync 类型:启动失败或拿不到 Provider,看起来像「toolcallback 没生效」,其实是类型坐标错了。
  2. 多模块各配各的 type:父工程 SYNC、子模块 ASYNC,最终仍落在同一个 spring.ai.mcp.client.type 上——官方不支持混合,不要指望「这个连接 SYNC、那个连接 ASYNC」。
  3. 注解处理器跟着 type 走:SYNC 客户端只会注册同步 MCP 注解方法,异步方法会被忽略;ASYNC 则相反。改 type 后若「采样/进度回调没了」,先查方法签名是否匹配当前模式。
  4. 对照实验要改全:只改 YAML 不改注入类型,或只改 Bean 类型不改 YAML,都会留下半迁移状态。

公共属性(如 enabled、name、initialized)按官方表调整即可;传输层细调另文处理,本稿聚焦「连上 + 显式挂 ChatClient」。

传输对照:stdio 与 streamable-http 怎么选

维度stdiostreamable-http
典型场景本机拉起 npx/可执行文件 MCP Server连已部署的 HTTP MCP Server(常与服务端 STREAMABLE 对打)
关键配置command / args / envurl + 可选 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 批处理,Java ProcessBuilder 不能直接跑;须 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」的反向开关。

关键事实再强调一遍:

  1. toolcallback.enabled=true → 有 ToolCallbackProvider;
  2. 仍须ChatClient.builder(...).defaultTools(mcpTools) 或 per-call .tools(mcpTools);
  3. 不会在启动时自动把 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 {    @Bean    ChatClient 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 文案。

冒烟清单可以压成四行:

  1. Client starter 在、Server starter 没误装成主路径;
  2. 至少一条 connection 通,默认 /mcp 或 /sse 没写反;
  3. type 全 SYNC 或全 ASYNC;
  4. 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 管线分开,勿混成「随便换个坐标」。
  • 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,轻量路径可以压成五步:

  1. 加对依赖:spring-ai-starter-mcp-client(生产 SSE/Streamable 可 …-webflux)。
  2. 配通一条连接:streamable-http 默认 /mcp,sse 默认 /sse,或 STDIO command/args。
  3. 认清总闸:toolcallback.enabled 默认 true;false 则无 Provider。
  4. 显式接线:defaultTools(mcpTools) 或 .tools(mcpTools)——不会自动挂。
  5. 三步验收:列工具名 → prompt 触发调用 → 关 toolcallback 对照。

把「Provider 是否存在」和「ChatClient 是否 defaultTools」拆成两次核对:一次只看自动配置,一次再看业务 Bean。混在同一巨大变更里时,评审者很容易只盯依赖坐标,漏掉仍缺失的那一行 .defaultTools(mcpTools)。

落地顺序固定成「先通连接、再挂 ChatClient、最后再谈工具过滤与命名」:连接不通时改 ChatClient 没意义;挂上之后再决定哪些进 defaultTools、哪些只适合 per-call .tools(...)。风险较高的破坏性工具,优先单次显式传入,不要一律全局放开。

相关学习资料