前言

过去一年里,"给大模型接工具"这件事几乎成了每个后端团队的必修课。但在实际落地过程中,几乎所有人都踩过同一类坑:每接入一个外部能力——查数据库的、调内部 RPC 的、读文件系统的——就要自己定义一套工具描述格式、自己处理参数 schema、自己写调用循环,十个服务就写出十套互不兼容的"工具胶水层"。更麻烦的是,工具的实现者和使用者往往属于不同团队、不同语言栈,约定全靠口头文档,改一个参数就要全链路重新对齐。这就是工具集成碎片化的问题:没有标准协议,每对"模型应用 ↔ 外部系统"的组合都要重新发明一次轮子。

Model Context Protocol(MCP)就是为解决这个碎片化问题而生的开放协议。官方把它定义为"一种标准化协议,让 AI 模型能够以结构化的方式与外部工具和资源交互",可以把它理解成 AI 模型与真实世界之间的一座桥:数据库、API、文件系统都通过一致的接口暴露出来。而 Spring AI 作为 Java 生态里系统化拥抱 AI 工程的框架,从较早期就完整接入了 MCP,并提供了开箱即用的 Boot Starter 自动装配。

本篇文章采用"官方文档带读"的方式,以 Spring AI 2.0.1 版本官方文档中《MCP Client Boot Starter》一章为主线(辅以《Model Context Protocol (MCP)》概览章),逐段拆解 MCP 客户端在 Spring Boot 应用中的装配方式、配置模型与扩展点。读完本文,你应该能够:独立完成一个 MCP 客户端的接入、选对传输方式、把远端 MCP 工具喂给 ChatClient、理解它与 Spring AI 原生 Tool Calling 的分工,并对生产部署中的超时、鉴权与进程生命周期问题有明确预案。

需要提前说明:本文所有配置项、默认值、类名均来自当前版本(2.0.1)官方文档,若后续版本有变动,以对应版本的官方文档为准。

整体架构与核心概念

在进入配置细节之前,先把协议的角色划分和分层架构讲清楚,这是后续所有配置项的"语义地基"。

MCP 的世界里只有两个角色。客户端(Client)是发起方,通常就是你的 AI 应用:它负责连接 MCP 服务器、做协议版本协商与能力协商、以 JSON-RPC 通信、发现并调用远端工具、访问资源、处理提示词模板;服务端(Server)是能力提供方,负责把工具暴露给客户端、以 URI 方式管理资源、提供提示词模板、发送结构化日志与通知。一个客户端可以连接多个服务端,每个连接产生一个独立的客户端实例——这一点在后文配置多个 connections 时会反复用到。

从 Java SDK 的实现看,MCP Java SDK 遵循清晰的三层架构:

┌─────────────────────────────────────────────────────┐
│ Client/Server 层   McpClient      McpServer         │  应用逻辑与协议操作
├─────────────────────────────────────────────────────┤
│ Session 层         McpSession / McpClientSession /  │  通信模式与连接状态
│                    McpServerSession                 │
├─────────────────────────────────────────────────────┤
│ Transport 层       McpTransport  (JSON-RPC 序列化)   │  消息传输与序列化
│   ├─ StdioClientTransport      (子进程 stdio)        │
│   ├─ HttpClientSseClientTransport (JDK HttpClient)   │
│   ├─ StreamableHttp / Stateless Streamable HTTP      │
│   └─ WebFluxSse / WebFlux Streamable (WebFlux 版)    │
└─────────────────────────────────────────────────────┘
        ▲                                   ▲
        │  连接 1 (如 stdio)                 │  连接 2 (如 SSE)
   MCP Server A (文件系统)            MCP Server B (内部搜索)

理解了"客户端—会话—传输"这三层,Spring AI Boot Starter 做的事就可以一句话概括:把传输层、会话层的创建全部自动化,把你写的 YAML 配置翻译成一组 McpSyncClient / McpAsyncClient Bean,再把服务端发现的工具适配成 Spring AI 工具执行框架认识的 ToolCallbackProvider。官方列出的 starter 能力清单值得抄一遍:多客户端实例管理、(可选的)自动初始化、对 STDIO / Http-SSE / Streamable HTTP 多种命名传输的支持、与 Spring AI 工具执行框架集成、工具过滤、工具名前缀定制、应用上下文关闭时的资源清理,以及通过 customizer 定制客户端创建过程。

现在你应该掌握了:MCP 协议中客户端与服务端各自的职责,以及 Java SDK 的三层架构——后面每个配置项都能在这个图里找到自己的位置。

MCP 要解决的碎片化问题与协议定位

先把协议定位说透。在 MCP 之前,一个 AI 应用要调用外部能力,通常的做法是把函数签名塞进 prompt、让模型输出结构化 JSON 再自行解析——这条路有两个结构性弱点:一是 schema 的表达能力与校验全靠每个应用自己实现;二是工具提供方没有任何标准方式描述"我能做什么",接入新工具永远意味着新写一套胶水代码。

MCP 的定位就是把"工具是什么、怎么被发现、怎么被调用、调用过程如何交换元数据"全部协议化:服务端以标准化的能力清单(tools / resources / prompts)宣告自己,客户端在初始化阶段完成协议版本与能力协商,之后的调用全部走 JSON-RPC 消息。由于协议与传输解耦,同一套能力描述既可以通过子进程管道暴露(stdio),也可以通过 HTTP 暴露(SSE、Streamable HTTP),这让"本地小工具"和"远端企业级服务"可以进入同一个模型上下文。

对于 Java 开发者,MCP 还有一层额外意义:它是跨语言的。MCP 生态里大量现成服务端是 Node/Python 写的(比如官方的 filesystem server),你的 Spring Boot 应用完全不需要重写它们,通过 stdio 拉起进程即可直接消费。这正是"协议化"带来的复利。

现在你应该掌握了:MCP 解决的是工具集成的标准化问题,其定位是"模型上下文的标准协议层",并且传输与能力描述解耦。

两种 Starter:标准版与 WebFlux 版

官方文档把客户端集成收敛为两个 starter,选择它们就是选择传输实现:

Starter 传输实现 适用场景
spring-ai-starter-mcp-client STDIO + 基于 JDK HttpClient 的 SSE / Streamable-HTTP / Stateless Streamable-HTTP 默认选择、同步阻塞式应用
spring-ai-starter-mcp-client-webflux STDIO + 基于 WebFlux 的 SSE / Streamable-HTTP / Stateless Streamable-HTTP 响应式应用;官方明确建议生产部署 SSE 与 StreamableHttp 时使用

Maven 坐标如下(使用 spring-ai-bom 时无需显式版本):

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-mcp-client</artifactId>
</dependency>

注意三个事实:第一,每个到 MCP server 的连接都会创建一个新的 MCP client 实例,多连接是常态而非特例;第二,客户端类型可以选 SYNC 或 ASYNC,但全部客户端必须统一,不支持混用;第三,官方文档对生产部署的建议非常直接——SSE 与 StreamableHttp 场景用 WebFlux 版。这个建议背后的工程直觉是:SSE 是长连接,JDK HttpClient 的阻塞模型在高并发长连接管理下不如 WebFlux 的反应式栈省心。

现在你应该掌握了:两个 starter 的差异仅在传输实现,选型第一问是"你的应用是不是 WebFlux 栈"。

传输方式逐一带读:stdio、SSE 与 Streamable HTTP

STDIO:拉起一个子进程

STDIO 传输把 MCP server 当作本地子进程,通过标准输入输出通信。它的配置前缀是 spring.ai.mcp.client.stdio,核心是一张命名连接表,每条连接可指定 command、args、env:

spring:
  ai:
    mcp:
      client:
        stdio:
          root-change-notification: true
          connections:
            server1:
              command: /path/to/server
              args:
                - --port=8080
                - --mode=production
              env:
                API_KEY: your-api-key
                DEBUG: "true"

除 YAML 外,还可以用外部 JSON 文件描述连接,且格式与 Claude Desktop 的 mcpServers 配置完全兼容(spring.ai.mcp.client.stdio.servers-configuration: classpath:mcp-servers.json)。这意味着你在桌面端调好的 MCP 配置可以直接搬进服务端应用:

{
  "mcpServers": {
    "filesystem": {
      "command": "npx",
      "args": [
        "-y",
        "@modelcontextprotocol/server-filesystem",
        "/Users/username/Desktop"
      ]
    }
  }
}

Windows 平台有一个官方文档特意强调的坑:npx、npm、node 在 Windows 上是 .cmd 批处理文件,而 Java 的 ProcessBuilder(StdioClientTransport 内部使用)不能直接执行批处理文件,必须包一层 cmd.exe /c。需要这样处理的常见命令包括 npx.cmd、npm.cmd、Microsoft Store 安装的 python.cmd、pip.cmd、mvn.cmd、gradle.cmd 以及一切自定义 .cmd/.bat 脚本。跨平台的做法是在应用里做 OS 探测,程序化构建客户端,且务必加 @ConditionalOnMissingBean(McpSyncClient.class) 避免与 JSON 文件触发的自动装配冲突。

SSE:拆分 base url 与 endpoint

SSE 传输适用于"server 主动推事件"的模型,配置前缀 spring.ai.mcp.client.sse,默认端点后缀是 /sse:

spring:
  ai:
    mcp:
      client:
        sse:
          connections:
            server1:
              url: http://localhost:8080
            mcp-hub:
              url: http://localhost:3000
              sse-endpoint: /mcp-hub/sse/cf9ec4527e3c4a2cbb149a85ea45ab01

官方文档专门给出了一条 URL 拆分准则:拿到一个完整 SSE URL 时,要拆成"只含协议、主机、端口"的 base url 和"以 / 开头、含完整路径与查询参数"的 endpoint。例如 https://api.service.com/v2/events?key=secret 应拆成 url: api.service.com 加 sse-endpoint: /v2/events?key=secret。连接 404 时的排查顺序也是文档点名的:先核对 base url 是否只含 scheme/host/port,再核对 sse-endpoint 是否以 / 开头且含完整路径与 query,最后用浏览器或 curl 直接验证完整 URL 可达。

Streamable HTTP:协议演进方向

Streamable HTTP 是 MCP 协议演进中替代/统一 SSE 的方向,同样支持有状态与无状态(Stateless)两种模式,配置前缀 spring.ai.mcp.client.streamable-http,默认端点后缀是 /mcp:

spring:
  ai:
    mcp:
      client:
        streamable-http:
          connections:
            server1:
              url: http://localhost:8080
            server2:
              url: http://otherserver:8081
              endpoint: /custom-sse

三者对比如下:

维度 STDIO SSE Streamable HTTP
通信对象 本地子进程 远端 HTTP 服务 远端 HTTP 服务
典型用途 复用 Node/Python 生态现成 server 推送式事件流 新一代统一传输,支持有/无状态
关键配置 command/args/env url + sse-endpoint url + endpoint
默认端点 无 /sse /mcp
生产建议 注意子进程生命周期 推荐 WebFlux starter 推荐 WebFlux starter

现在你应该掌握了:三种传输的适用边界、各自的配置形状、URL 拆分准则,以及 Windows stdio 的 cmd.exe /c 特例。

客户端类型与自定义扩展点

SYNC 还是 ASYNC

spring.ai.mcp.client.type 控制客户端类型,默认 SYNC。两个细节必须记住:一是全部客户端要么全 SYNC 要么全 ASYNC,不能混用;二是注解扫描存在类型对齐——SYNC 客户端只会注册同步的 MCP 注解方法,异步方法会被静默忽略,反之亦然。

McpCustomizer:官方留的"总阀门"

自动装配通过 McpCustomizer<McpClient.SyncSpec>(异步对应 AsyncSpec)开放客户端定制,可定制的内容几乎覆盖协议全部交互面:请求超时(spec.requestTimeout(...))、文件系统 Roots 暴露、sampling 处理器(server 反向请求 LLM 补全的标准机制,客户端由此掌控模型访问权与密钥,server 不需要任何 API key)、elicitation 处理器(server 经客户端向用户索要补充信息)、进度通知、工具/资源/提示词列表变更通知,以及结构化日志。customizer 的 customize(String serverConfigurationName, ...) 第一个参数就是连接名,你可以按连接名做差异化配置;Spring 容器里的 customizer 会被自动装配自动发现并应用。

@Component
public class CustomMcpSyncClientCustomizer implements McpClientCustomizer {

    @Override
    public void customize(String serverConfigurationName, McpClient.SyncSpec spec) {
        spec.requestTimeout(Duration.ofSeconds(30));
        spec.progressConsumer((ProgressNotification progress) -> {
            // 处理进度通知
        });
        spec.toolsChangeConsumer((List tools) -> {
            // 服务端工具列表变化时刷新本地注册表
        });
    }
}

工具过滤与工具名前缀

连多个 server 时,两个扩展点决定"哪些工具能进模型上下文"以及"工具叫什么名字":

  • McpToolFilter:test(McpConnectionInfo, McpSchema.Tool) 返回 true 表示纳入。典型用法是按连接排除受限客户端的工具、只放行 allowed_ 前缀的工具、剔除描述含 experimental 的工具。注意文档明确要求容器里只应有一个 McpToolFilter Bean,多个过滤条件请合并成一个复合过滤器。
  • McpToolNamePrefixGenerator:解决多 server 工具重名冲突。默认实现 DefaultMcpToolNamePrefixGenerator 会把非字母数字字符替换为下划线(my-tool → my_tool),重名时追加计数前缀(第二个 search 变成 alt_1_search),且保证最终名称不超过 64 字符;McpToolNamePrefixGenerator.noPrefix() 可关闭前缀,但官方警告多 server 场景下重名会抛 IllegalStateException。McpConnectionInfo 里能拿到客户端能力、客户端信息与服务端初始化结果,足够拼出"serverName_v版本_工具名"这类可审计的名字。

ToolContext 到 MCP 元数据的桥

Spring AI 的 ToolContext 可以通过 ToolContextToMcpMetaConverter 转成 MCP 工具调用的元数据,默认实现会滤掉 MCP 内部交换键与空值、透传其余条目——典型用法是把 progressToken 放进 toolContext,让 MCP 端跟踪长任务进度:

String response = ChatClient.create(chatModel)
        .prompt("Tell me more about the customer with ID 42")
        .toolContext(Map.of("progressToken", "my-progress-token"))
        .call()
        .content();

若不想透传任何上下文,注册 ToolContextToMcpMetaConverter.noOp() 即可。

现在你应该掌握了:客户端类型约束、customizer 能摸到的全部协议交互面,以及过滤、改名、元数据这三类扩展点的注册方式与默认行为。

把 MCP 工具接入 ChatClient

配置完成后,Spring 容器里会出现两类 Bean:客户端实例列表(List<McpSyncClient> 或 List<McpAsyncClient>),以及在 spring.ai.mcp.client.toolcallback.enabled=true(默认)时由全部 MCP 工具聚合而成的 ToolCallbackProvider:

@Autowired
private SyncMcpToolCallbackProvider toolCallbackProvider;

ToolCallback[] toolCallbacks = toolCallbackProvider.getToolCallbacks();

把这个 provider 交给 ChatClient,MCP 工具就进入了模型的工具调用循环:

String answer = ChatClient.create(chatModel)
        .prompt("帮我查一下客户 42 的最近订单")
        .toolCallbacks(toolCallbackProvider)
        .call()
        .content();

官方示例工程(如 Brave Web Search Chatbot、Default MCP Client Starter、WebFlux MCP Client Starter)演示的正是这条链路。如果你不希望 MCP 工具自动进入工具执行框架,设 spring.ai.mcp.client.toolcallback.enabled=false,此时不会创建任何 ToolCallbackProvider。

与 Spring AI 原生 Tool Calling 的关系与差异

这是读者最容易混淆的一节。Spring AI 原生 Tool Calling 指的是 @Tool 注解方法直接注册为 MethodToolCallback,在同一个 JVM 内被模型调用;MCP 则是跨进程/跨网络的协议。两者在 ChatClient 层面汇合——都呈现为 ToolCallback——但来源、治理方式与运维属性完全不同:

维度 原生 Tool Calling MCP 工具
工具实现位置 本应用 JVM 内 独立进程/远端服务
描述与 schema 来源 注解 + 反射生成 MCP 初始化协商所得
治理手段 Java 代码约束 McpToolFilter / 前缀生成器 / 元数据转换器
复用范围 仅本应用 任何 MCP 客户端(含其他语言)
失效模式 方法异常 网络超时、进程崩溃、协议协商失败

实践上两者不是替代关系而是互补关系:高频、强类型、紧耦合业务逻辑的工具适合留在 JVM 内;需要跨团队复用、跨语言消费、或本身就是第三方生态产物的能力,应该包装成 MCP server 消费。二者可以出现在同一个 ChatClient 的工具列表里,Spring AI 对模型而言抹平了来源差异——但对你这个架构师而言,治理成本完全不同:MCP 工具要额外考虑远端可用性、超时与命名冲突。

现在你应该掌握了:MCP 工具与原生工具最终殊途同归于 ToolCallback,但治理与运维模型不同,选型应按"复用半径"来切。

生产部署注意事项:超时、鉴权与进程生命周期

官方文档在多处散落了对生产落地的提示,汇总成三类:

超时。 spring.ai.mcp.client.request-timeout 默认 20 秒,这是客户端请求的超时上限;需要更细粒度控制时通过 customizer 按连接调整(例如 spec.requestTimeout(Duration.ofSeconds(30)))。长任务应配合进度通知机制(@McpProgress 注解方法或 progressConsumer),而不是简单放大超时。

鉴权与凭据。 stdio 场景下,密钥通过 env 注入子进程环境变量,属于进程级隔离,但要注意别把 DEBUG: "true" 这类开关带进生产。HTTP 传输的鉴权没有专门配置项,惯例做法是把 token 编进 URL(文档示例中的 mcp-hub 连接即在路径段携带 token),或按需在自定义 transport 上挂请求头;官方另设有 MCP Security 章节(标注 WIP),相关细节以当前版本官方文档为准。值得一提的是 sampling 的反向鉴权模型:server 请求补全时不需要持有任何 LLM API key,密钥始终留在客户端侧,这是协议在密钥治理上的一个重要设计。

进程生命周期。 stdio 模式下 MCP server 是你应用的子进程,Spring 的 starter 承诺在应用上下文关闭时做资源清理,但"拉得起也要杀得掉":容器编排场景要确保 JVM 退出信号能传导到子进程,避免 npx/node 僵尸进程堆积;程序化声明客户端时按文档建议写 @Bean(destroyMethod = "close") 并搭配 @ConditionalOnMissingBean 防止装配冲突。SSE/Streamable HTTP 长连接则要面对连接重连与状态恢复,这也是为什么官方建议生产环境使用 WebFlux starter。另外,2.0 起传输实现从 MCP Java SDK 迁入 Spring AI 项目(坐标从 io.modelcontextprotocol.sdk 变为 org.springframework.ai,包名整体迁移),要求 MCP Java SDK 1.0.0 及以上;纯用 starter 自动装配的用户只需更新依赖坐标,Java 代码无需改动。

现在你应该掌握了:三类生产关注点的官方建议落点——超时用属性加 customizer 双层控制,凭据优先 env 注入与客户端侧持有,stdio 子进程与长连接的清理都要显式设计。

踩坑经验

❌ 错误做法 1:同一应用里部分连接配 SYNC、部分配 ASYNC。
✅ 正确做法:官方文档明确"所有客户端必须同为同步或异步,不支持混用"。spring.ai.mcp.client.type 是全局开关,选型在应用启动前就定死。

❌ 错误做法 2:Windows 上直接写 command: npx。
✅ 正确做法:npx 在 Windows 是 npx.cmd,ProcessBuilder 无法直接执行,必须写成 command: cmd.exe + args: ["/c", "npx", ...];需要跨平台时在代码里做 os.name 探测并程序化构建客户端。

❌ 错误做法 3:把完整 SSE URL 一股脑填进 url,如 url: http://localhost:3000/mcp-hub/sse/token123。
✅ 正确做法:拆成 url: http://localhost:3000 和 sse-endpoint: /mcp-hub/sse/token123;遇到 404 按"base url 只含 scheme/host/port → endpoint 以 / 开头含完整 query → curl 直连验证"的顺序排查。

❌ 错误做法 4:声明多个 McpToolFilter Bean 期望"层层过滤"。
✅ 正确做法:文档明确规定上下文中只应有一个 McpToolFilter,多个过滤条件合并为一个复合过滤器实现。

❌ 错误做法 5:多 server 消费时用 noPrefix() 保持"工具原名",结果启动抛 IllegalStateException。
✅ 正确做法:保留默认 DefaultMcpToolNamePrefixGenerator,或自定义一个带 server 名/版本前缀的生成器;只有确认所有 server 工具不重名时才考虑 noPrefix。

❌ 错误做法 6:程序化定义 McpSyncClient Bean 时不加条件注解,与 JSON 配置的自动装配抢同一个 Bean。
✅ 正确做法:按文档要求加 @ConditionalOnMissingBean(McpSyncClient.class),并用 destroyMethod = "close" 保证上下文关闭时资源释放。

最佳实践清单

  1. 起步就用 spring-ai-bom 管理版本,starter 坐标不手写版本号;2.0 迁移用户优先核对传输类坐标的 groupId 变化。
  2. 应用是 WebFlux 栈、或 SSE/Streamable HTTP 上生产,直接选 spring-ai-starter-mcp-client-webflux。
  3. 客户端类型(SYNC/ASYNC)在立项时定死,并保证 MCP 注解方法的同步/异步风格与之一致。
  4. stdio 连接优先使用 Claude Desktop 兼容的 JSON 文件统一管理,团队桌面端与服务端共用一份配置。
  5. 超时采用"全局 request-timeout + customizer 按连接微调"的两层策略;长任务配进度通知而不是无限放大超时。
  6. 多 server 场景显式实现 McpToolFilter 与 McpToolNamePrefixGenerator,把"哪些工具进模型上下文、叫什么名字"变成可审计的代码。
  7. 用 @McpLogging、@McpSampling 等注解方法承接服务端通知,而不是自己挂底层 consumer;注意 clients 参数可按连接名限定作用范围。
  8. 生产环境给 stdio 子进程规划好随 JVM 退出而回收的机制,编排文件里显式处理信号传播。

总结

  1. MCP 的定位是模型与外部系统之间的标准协议层,把工具、资源、提示词的发现与调用全部协议化,从根本上消解工具集成的碎片化。
  2. Spring AI 的客户端集成由两个 starter 承担:标准版用 JDK HttpClient 实现 SSE/Streamable HTTP,WebFlux 版用 WebFlux 实现,后者是官方对生产部署的明确建议。
  3. 三种传输各有边界:stdio 面向本地子进程并天然兼容 Claude Desktop 配置格式;SSE 与 Streamable HTTP 面向远端服务,配置核心是 base url 与 endpoint 的正确拆分。
  4. MCP 工具通过 ToolCallbackProvider 与原生 @Tool 工具在 ChatClient 层汇合,但治理维度完全不同——过滤、命名前缀、元数据转换是 MCP 侧特有的三个控制点。
  5. 生产落地的三件套:超时双层控制、凭据最小化暴露(sampling 让 server 无需 LLM 密钥)、子进程与长连接的生命周期显式管理。
  6. 版本敏感点:Spring AI 2.0 要求 MCP Java SDK 1.0.0+,传输类坐标与包名整体迁移至 org.springframework.ai,细节以当前版本官方文档为准。

参考资料

  • Spring AI 官方文档 · MCP Client Boot Starter:https://docs.spring.io/spring-ai/reference/api/mcp/mcp-client-boot-starter-docs.html
  • Spring AI 官方文档 · Model Context Protocol (MCP) 概览:https://docs.spring.io/spring-ai/reference/api/mcp/mcp-overview.html
  • MCP 规范(Model Context Protocol Specification):https://modelcontextprotocol.io
  • Claude Desktop 配置格式参考:https://modelcontextprotocol.io/quickstart/user
  • Spring Boot 自动装配机制:https://docs.spring.io/spring-boot/reference/

附录一:客户端配置项全量速查

前面各节是按主线分散讲配置,这里把官方文档《Configuration Properties》一节的全部条目收拢成一张速查表,方便对照排查。通用属性统一挂在 spring.ai.mcp.client 前缀下:

属性 含义 默认值
enabled 启用/禁用 MCP 客户端 true
name MCP 客户端实例名称 spring-ai-mcp-client
version MCP 客户端实例版本 1.0.0
initialized 创建客户端时是否执行初始化(协议与能力协商) true
request-timeout 客户端请求超时时间 20s
type 客户端类型,全部实例必须统一为 SYNC 或 ASYNC SYNC
root-change-notification 是否对所有客户端开启 root 变更通知 true
toolcallback.enabled 是否把 MCP 工具接入 Spring AI 工具执行框架 true

三条容易看漏的说明。第一,name 与 version 是客户端在 MCP 初始化握手中自报家门的身份标识,会出现在服务端的连接日志里,生产环境建议改成能标识业务应用的名字,排障时能立刻知道是谁在连。第二,initialized 默认 true 意味着客户端创建即完成初始化协商,工具列表在启动后即可用;若你有特殊的延迟初始化诉求,需要评估关掉它之后首次调用才协商的时延影响,具体行为以当前版本官方文档为准。第三,注解扫描有独立开关 spring.ai.mcp.client.annotation-scanner.enabled,默认 true;如果你完全用 customizer 编程式处理服务端通知,可以把它关掉以减少启动期扫描开销。

传输层的属性已在正文讲过,这里只强调官方文档确认的一个事实:stdio 的 connections.[name] 下的 command、args、env 三张表,与 JSON 文件中的 mcpServers 条目一一对应,二者选其一即可,同时出现时建议以代码侧显式装配为准并加条件注解防冲突。

附录二:服务端反向调用与注解处理机制

MCP 不是单向的"客户端调工具"。服务端在运行中可以向客户端发回五类消息:日志通知、sampling 请求(请求客户端代为完成 LLM 补全)、elicitation 请求(请客户端向用户索要补充信息)、进度通知,以及工具/资源/提示词列表变更通知。Spring AI 为这五类消息提供了声明式注解,starter 会自动扫描并注册带注解的方法:

@Component
public class McpClientHandlers {

    @McpLogging(clients = "server1")
    public void handleLoggingMessage(LoggingMessageNotification notification) {
        System.out.println("Received log: " + notification.level()
                + " - " + notification.data());
    }

    @McpSampling(clients = "server1")
    public CreateMessageResult handleSamplingRequest(CreateMessageRequest request) {
        String response = generateLLMResponse(request);
        return CreateMessageResult.builder(Role.ASSISTANT, response, "gpt-4")
                .build();
    }

    @McpProgress(clients = "server1")
    public void handleProgressNotification(ProgressNotification notification) {
        double percentage = notification.progress() * 100;
        System.out.println(String.format("Progress: %.2f%% - %s",
                percentage, notification.message()));
    }

    @McpToolListChanged(clients = "server1")
    public void handleToolListChanged(List<McpSchema.Tool> updatedTools) {
        // 服务端工具列表变化时刷新本地注册表
        toolRegistry.updateTools(updatedTools);
    }
}

三个使用要点。其一,clients 参数可按连接名限定注解方法的作用范围,不带该参数则对所有连接生效;其二,注解同时支持同步与异步实现,但注册规则与客户端类型严格对齐——SYNC 客户端只注册同步方法,异步方法被静默忽略,反之亦然,混写代码时这是最容易出现的"方法死活不触发"原因;其三,除上述四个之外官方列表还包含 @McpElicitation、@McpResourceListChanged、@McpPromptListChanged,覆盖面与 customizer 中的各类 consumer 完全等价,选注解还是选 consumer 取决于你更想要声明式扫描还是显式装配。

Sampling 值得单独再说一句,因为它是 MCP 协议设计里最精巧的一环:当服务端需要模型补全能力时,它不持有任何 LLM API key,而是把 CreateMessageRequest 发给客户端,由客户端(也就是你的 Spring 应用)用自己保管的密钥完成补全再回传结果。密钥治理的风险面因此完全收拢在客户端一侧。

附录三:三个落地场景推演

场景一:单体应用快速验证。 团队想在内部知识库问答里加一个"读取共享盘文档"的能力,而现成的 filesystem server 是 Node 写的。此时不需要任何 Java 开发:引入标准 starter,把 Claude Desktop 调好的 mcpServers JSON 放进 classpath:mcp-servers.json,启动后 SyncMcpToolCallbackProvider 自动聚合出工具列表交给 ChatClient,全程零自定义代码。这个场景的运维重点是 stdio 子进程的生命周期,JVM 退出时必须带走 npx/node 进程。

场景二:企业内网多团队复用。 搜索、工单、审批三个能力分别由三个团队以 Streamable HTTP 暴露为 MCP server。应用侧配置三条命名连接,通过 McpToolNamePrefixGenerator 把工具名拼成"团队前缀_工具名",再通过 McpToolFilter 只放行带 allowed_ 前缀的工具进入模型上下文——治理动作全部落在客户端配置里,三个团队的服务端零改动。这个场景务必选 WebFlux starter,因为三条都是长连接,生产并发下 JDK HttpClient 的阻塞模型不如反应式栈。

场景三:受监管行业的权限隔离。 某连接来自外部供应商,工具列表里混有实验性工具。做法是实现单个复合 McpToolFilter,按 McpConnectionInfo.clientInfo().name() 识别该连接并剔除描述含 experimental 的工具;同时用 ToolContextToMcpMetaConverter.noOp() 切断工具上下文到 MCP 元数据的透传,避免内部上下文信息随调用流出到第三方 server。两层过滤叠加,权限边界就完全由客户端代码掌控。

附录四:自动装配链路推演

把 starter 启动时做的事按顺序推演一遍,有助于理解前文所有扩展点的生效时机:Spring Boot 触发 MCP 客户端自动装配类,读取 spring.ai.mcp.client 及三个传输前缀下的配置,为每条命名连接按其传输类型构建对应的 transport(stdio 建 StdioClientTransport,SSE/Streamable HTTP 分别建 HttpClient 或 WebFlux 实现),随后应用容器中所有 McpCustomizer 按连接名逐条定制 spec,创建出 McpSyncClient 或 McpAsyncClient 实例并注入容器(因此你能直接注入 List<McpSyncClient>);若 initialized=true,创建即完成协议与能力协商。最后,若 toolcallback.enabled=true,自动装配把所有客户端发现到的工具经过 McpToolFilter 过滤、经 McpToolNamePrefixGenerator 改名后,聚合为 ToolCallbackProvider Bean——这就是 ChatClient 最终拿到的那个对象。理解这条链路的价值在于:任何一个环节出问题都有明确的排查锚点——连不上看 transport 配置,协商失败看服务端日志,工具缺失看 filter,名字异常看 prefix generator。

附录五:观测、排错与混合编排

MCP 链路一旦进入生产,可观测性的抓手比传统 RPC 多一层协议语义,建议按"连接—协商—调用—治理"四层布置日志。连接层记录每条命名连接的 transport 类型与目标地址;协商层记录 initialized 步骤的协议版本与能力交换结果;调用层借助 spring.ai.mcp.client.request-timeout 与 customizer 中的超时设置定位慢调用;治理层则为 McpToolFilter 的每次放行/拒绝打出含连接名与工具名的日志——这几乎是排查"模型为什么没拿到某工具"的唯一捷径。配合 @McpLogging 把服务端结构化日志并入应用日志体系后,一次完整调用的上下文就闭环了。

混合编排上,前文说过 MCP 工具与原生 @Tool 工具在 ChatClient 层殊途同归,落地时可以显式组合两类来源:

ToolCallback[] mcpTools = mcpToolCallbackProvider.getToolCallbacks();
ToolCallback[] localTools = ToolCallbacks.from(new CustomerTools());

String answer = ChatClient.create(chatModel)
        .prompt("查一下客户 42 的信用分,再和上月对比")
        .toolCallbacks(ArrayUtils.addAll(localTools, mcpTools))
        .call()
        .content();

这段代码的关键认知是:模型看到的只是一个扁平的工具数组,它并不知道也不关心哪个工具来自子进程、哪个来自本 JVM;但作为工程方的你必须关心——CustomerTools 里的方法异常走应用内异常体系,MCP 工具的失败则表现为 JSON-RPC 错误、超时或连接断开,两类失败模式要在统一的重试与降级策略里区别对待。最后一个排错速查口诀:连不上先验 transport 与 URL 拆分,工具缺失先查 filter 与 toolcallback.enabled,方法不触发先核对 SYNC/ASYNC 与注解方法风格,重名启动失败先看前缀生成器——绝大多数线上问题都能在这四个锚点内闭环。

需要补充的是,以上全部配置项与默认值的验证基准是二点零点一版本官方文档;MCP 规范与 Spring AI 的集成仍在快速演进,动手实践前请再核对当前版本的对应章节,以官方文档为准。