ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Spring AI MCP 客户端 Boot Starter 原理与实战指南

Spring AI MCP 客户端 Boot Starter 原理与实战指南 老实说搞了大半年 Spring AI 项目最让我头疼的从来不是让模型把话说漂亮而是让它真正动手干活。查数据库、翻文件、调内部接口这些事模型自己干不了得靠人写一堆胶水代码。我大概从 Spring AI 0.8 开始追 MCP 这个方向一路踩坑到 1.0 正式版后来又在 2.x 上折腾了一遍。这个系列写到第 68 篇今天就把 MCP 客户端 Boot Starter 这件事从头到尾捋清楚。MCP 是 Model Context Protocol 的缩写你可以先把它理解成AI 界的 USB-C 接口。Spring AI 官方在 1.0 GA 之后提供了非常成熟的客户端 Boot Starter配置好之后任意一个 MCP Server 暴露的工具都能自动变成 ChatClient 的调用能力。这篇笔记不会只给你配置片段我会把自动装配的原理、多 Server 管理、Resource 读取以及我实际踩过的坑全部讲一遍。适合那些想把 Spring AI 的 Agent 接进生产环境、又不想被 Function Calling 细节淹没的 Java 开发者。1. MCP 协议解决了 Spring AI 工具接入的哪三个问题1.1 没有 MCP 之前Agent 接外部工具有多痛苦先回顾一下没有 MCP 的时候我们是怎么给模型加能力的。假设你要让 AI 助手帮你查询 GitHub 仓库信息传统做法是走 Function Calling先写一段 JSON Schema 描述这个工具接收什么参数再写一个回调方法真正去调 GitHub API最后还要把这个工具注册给 ChatClient。这只是一个小工具如果接十个二十个每个都要维护参数定义、错误处理、鉴权、超时重试代码量非常可观。更麻烦的是换模型厂商。OpenAI 的 function calling 和 Anthropic 的 tool use 在消息格式和参数类型上不完全一致你的工具抽象如果绑死了某一家 API切换模型的时候整个适配层都要重写。我早期做过一次从 GPT-4 切到 Qwen 的迁移工具这块的改动比换模型本身还大当时就觉得这种玩法不可持续。1.2 MCP 的核心抽象Server 暴露Client 调用MCP 解决这个问题的方式用一句话说就是把工具从模型的私有能力变成协议上的公共资源。它借鉴了 LSPLanguage Server Protocol的思路定义了 Client 和 Server 两个角色。Server 负责实现具体能力并对外声明三类东西Tool、Resource、Prompt。Tool 是模型可以执行的函数Resource 是模型可以读取的数据Prompt 是预设的提示模板。Client 负责发现这些能力并在模型需要时发起调用。打个比方以前每个设备都有自己的充电线你出差要带一堆线MCP 要做的是把充电口统一成 USB-C。你的 AI 应用不再需要知道 GitHub Server 内部怎么实现的只需要按照 MCP 协议去连它、读它的工具清单、调用它的工具接口。协议层面的统一带来的最大好处是工具可以被任意 AI 应用复用也可以被任意模型使用。1.3 为什么用 Boot Starter而不是自己写 McpClientSpring AI 底层确实提供了McpClient这套 Java SDK你完全可以用它手动创建客户端、管理连接、拉取工具列表。但生产环境里你会碰到一堆琐碎问题连接的启动和关闭顺序、异常重连、工具列表如何转成模型能理解的调用格式、多个 Server 并存时名字冲突怎么处理。这些是每个项目都会遇到的问题不值得每个项目都重新发明一遍。Boot Starter 的价值在于把上述这些事变成配置和自动装配。你只需要在application.yml里声明连接信息启动后它自动把 MCP Server 的工具注册到统一的ToolCallingManager再挂到ChatClient上就能用。不是说让你完全不理解底层而是先跑起来、再深入降低上手门槛。2. Boot Starter 到底自动装配了哪些东西2.1 依赖长什么样以 Spring AI 1.0.0 GA 为例核心依赖是这几段parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.3.5/version /parent properties java.version17/java.version spring-ai.version1.0.0/spring-ai.version /properties dependencyManagement dependencies dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-bom/artifactId version${spring-ai.version}/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement实际的 MCP 客户端 Starter 要根据你的 Web 环境选。如果是普通的 Spring MVC 应用用带 webmvc 的那个dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-mcp-client-webmvc/artifactId /dependency如果你只是想做一个不跑 Web 容器的命令行工具做验证用基础包也行dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-mcp-client/artifactId /dependency另外别忘了一个模型服务商的 Starter比如spring-ai-starter-model-openai或者spring-ai-starter-model-dashscope。MCP 工具链本身跟模型无关但ChatClient总得有地方发请求。2.2 启动时发生了什么加上依赖之后Spring Boot 的McpClientAutoConfiguration会自动读取spring.ai.mcp.client.connections下面的配置逐条创建对应的 MCP Client。如果是STDIO类型它会用Runtime.exec启动配置里的命令进程如果是SSE或HTTP类型则会向配置的 URL 建立连接。建立连接之后客户端会执行一次listTools握手把 Server 上所有的工具定义拉下来。这些工具会被转换成 Spring AI 自己的ToolCallback注册进ToolCallingManager。后者的作用是管理当前应用里所有可用的工具并在模型推理过程中根据调用来执行真正的方法。所以你写代码的时候并不需要手工实例化任何 MCP 客户端。直接注入ToolCallingManager再在构建ChatClient时把它作为默认工具挂上去。整个链路是这样的ChatClient - ToolCallingManager - MCP Client - MCP Server每多配置一个连接就相当于给这条链路多接一条分支。2.3 核心配置项速查表我记得最常用的配置项大概有这些以 IDE 自动提示或官方配置元数据为准配置项含义示例值name连接名相当于这条 MCP 连接的 IDfilesystemtype连接类型STDIO/SSE/HTTPcommandSTDIO 类型要启动的命令npxargs启动参数列表[-y, modelcontextprotocol/server-filesystem, /data]env传给子进程的环境变量KEYVALUEurlSSE/HTTP 类型的服务地址http://localhost:8090/ssetoolNamePrefix工具名统一加的前缀fs_enabled是否启用该连接truelazyInitialization是否懒加载连接false我不建议死记这些参数名因为 Spring AI 版本更新偶尔会调整内部结构。最好的办法是打开 IDE 的配置自动提示或者去 jar 包里找spring-configuration-metadata.json看字段。3. 十分钟接上第一个 MCP Server配置与调用全流程3.1 环境准备本地验证需要 JDK 17、Spring Boot 3.3.x、Node.js 18。Node 主要是为了用npx启动官方示例 Server。如果你手头有自行开发的 MCP Server那更简单直接指到对应进程或端口就行。第一次尝试我建议用官方 filesystem Server它能让你立刻感受到 MCP 的威力模型可以直接读取本地目录结构。虽然这功能本身不复杂但关键让你看清整个工具链是如何运转的。3.2 配置 application.ymlspring: ai: mcp: client: connections: - name: fs type: STDIO command: npx args: - -y - modelcontextprotocol/server-filesystem - /Users/me/workspace/docs enabled: true这里我把args最后一项指向了你机器上的某个绝对路径。注意不同操作系统的路径写法不一样Windows 上要用反斜杠或者正斜杠转义建议统一用正斜杠。3.3 写一段能直接跑通的调用代码import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.tool.ToolCallingManager; import org.springframework.boot.CommandLineRunner; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; Configuration public class McpDemoConfig { Bean CommandLineRunner runMcpDemo(ChatClient.Builder builder, ToolCallingManager toolCallingManager) { return args - { ChatClient chatClient builder .defaultSystem(你是运维助手回答要简洁并给出依据) .defaultTools(toolCallingManager) .build(); String answer chatClient.prompt(列出 docs 目录下最近修改的3个文件) .call() .content(); System.out.println(AI 回答\n answer); }; } }这段代码里的关键只有一行.defaultTools(toolCallingManager)。它把 Boot Starter 自动收集到的所有 MCP 工具挂到了 ChatClient 上。模型在对话过程中判断自己需要读文件系统时就会触发工具调用。3.4 跑起来以后你会看到什么启动日志里通常会打印已注册的工具名。如果启动没报错说明 MCP Client 已经成功连上 Server工具列表也拉到了。你问它列出最近修改的3个文件正常的链路是模型看到问题判断需要一个文件列表工具。ToolCallingManager在工具注册表里找到对应的 filesystem 工具。通过 MCP Client 调用 Server 的工具方法。Server 在本地执行目录读取返回结构化结果。模型基于结果组织自然语言回答。看到上面五步在日志里一一发生基本上就说明 MCP 客户端这条路走通了。我建议第一遍先不要用流式输出用.call()把全链路跑稳再玩高级功能。4. 多个 MCP Server 并存命名空间隔离与连接管理4.1 一个应用同时连多个 Server实际项目中你十有八九要同时接多个数据源。比如一个连文件系统一个连天气服务一个连 GitHub。Boot Starter 的connections是列表结构多配几条就行spring: ai: mcp: client: connections: - name: filesystem type: STDIO command: npx args: [-y, modelcontextprotocol/server-filesystem, /home/user/data] enabled: true toolNamePrefix: fs_ - name: weather type: SSE url: http://localhost:8090/sse enabled: true toolNamePrefix: wx_启动时 Boot Starter 会对每条配置创建独立的 MCP 客户端。我这里故意配置了toolNamePrefix下面马上说为什么。4.2 工具重名模型会晕前缀是真的需要如果两个 Server 都暴露了一个query工具而你没有做任何隔离模型看到工具列表时可能会混淆甚至把应该发给天气服务的请求误发给文件系统服务。我在早期项目里就吃过这个亏模型在连续对话中调错了工具我排查了半天才发现是工具名冲突。解决办法就是给每个连接加toolNamePrefix。配置了之后模型实际看到的工具名会变成fs_query、wx_query这种带命名空间的名字。代价是模型在回复中可能会提到带前缀的名字但只要系统提示词里提前说清楚工具前缀对应哪个系统效果会好很多。注意toolNamePrefix只影响模型看到的工具名称不影响 Server 内部实际方法名。4.3 连接生命周期懒加载与优雅关闭每个 STDIO 连接都会拉起一个子进程SSE/HTTP 连接也会占用长连接。如果项目里配了七八个连接全部启动时握手会让应用启动时间明显变长甚至超过健康检查的阈值。lazyInitialization就是干这个的。对于非核心的、偶尔才用到的 Server设成true可以等第一次调用时再建立连接。代价是第一次触发时会有一段额外的等待时间你需要在超时配置上留一点余量。另外注意进程清理问题Spring AI 在关闭应用时会尽力关闭这些连接但如果你在容器里强制 kill 进程子进程可能变孤儿。所以生产环境我给出的建议是本地开发用 STDIO生产尽量用独立的 MCP Server SSE/HTTP。5. MCP Resource 实战把数据喂给模型而不是让它调工具5.1 Resource 和 Tool 到底有什么区别很多人刚开始接触 MCP容易把 Resource 和 Tool 混为一谈。从语义上讲Tool 是动词——执行操作比如查询天气、创建文件Resource 是名词——可读取的数据实体比如一篇运维手册、一份数据库表结构文档、一个配置文件。模型调用 Tool 是为了做某件事读取 Resource 是为了获取上下文。MCP Server 可以通过 URI 暴露 Resource比如file:///docs/deploy.md、db://schema/orders。客户端拿到 URI 后发起readResourceServer 返回内容。5.2 在 Spring AI 客户端里怎么读 ResourceBoot Starter 自动装配的核心目标是把 Tool 变成模型可调用的能力Resource 不会自动变成 ChatClient 的一部分。你要在代码里显式去读它再把读到的内容拼进提示词。基本套路是先拿到 MCP 客户端的 Bean然后// 伪代码演示具体 Bean 名称以你的版本为准 McpSyncClient mcpClient context.getBean(McpSyncClient.class); ServerResource resource mcpClient.listResources().stream() .filter(r - r.getUri().toString().endsWith(deploy.md)) .findFirst() .orElseThrow(); ReadResourceResult result mcpClient.readResource(resource.getUri()); String content result.getContents().stream() .map(c - c.getText()) .collect(Collectors.joining(\n));拿到content之后你可以把它作为ChatClient的 system 消息或者 user 消息的一部分发出去。为什么推荐这么做因为这样比让模型自己去搜索文档更省 token也更快。你直接告诉它下面是运维手册内容请基于它回答模型不需要在工具调用和结果解析上绕圈子。5.3 我的真实使用建议Resource 适合低频但稳定的数据比如配置规范、接口文档、项目结构说明。它不太适合动态数据比如实时的订单列表——那种情况你应该用 Tool 去查而不是让模型读一个静态资源。这里有个容易踩的坑如果你同时配置了读取同一个数据源的 Tool 和 Resource模型往往会倾向于调用工具因为它天生被训练成遇到问题就执行动作。如果你希望模型优先用 Resource需要在系统提示词里明确写回答问题时优先参考上下文中提供的文档。我试过几次效果差距还挺明显的。6. 我在这条路上踩过的坑版本、协议、参数与流式输出6.1 版本升级0.9 到 1.0 再到 2.xSpring AI 的 MCP 支持在 0.x 阶段变化非常快很多博客里的配置到 1.0 GA 就跑不通了。最典型的是配置前缀和包名都动过。到 1.0 之后趋于稳定但 2.x 开始又有一些 API 层面的调整比如工具管理相关的类从原来的分散注册收敛到了ToolCallingManager体系。我的习惯是每个版本升级都只看官方Migration Guide和Release Notes不要去旧博客抄配置。升级之后先用最小 demo 跑通链路再加业务逻辑。这条坑看起来很基础但真的浪费过我很长时间——有一次只是从 0.9 升到 1.0我天真地以为配置不用改结果启动直接报连接失败。6.2 STDIO 在容器环境不可用不是错觉STDIO 类型本质上是当前 JVM 进程去exec一个子进程。你的应用如果跑在 Docker 容器里镜像里必须预装 Node.js 和对应的 npx 包跑在 K8s 或 Serverless 环境里子进程的生命周期和重建逻辑会很麻烦更别说分布式多实例部署时每个实例都要拉一个子进程。所以生产环境我强烈建议把 MCP Server 单独部署暴露成 SSE 或 HTTP 端口让 Spring AI 通过远程连接过去。这样 MCP Server 的节点可以独立伸缩也方便监控。STDIO 只适合单机本地调试或者你用多实例部署且能接受资源浪费的场景。6.3 工具参数绑定失败字段全变 null 的真相MCP 工具的参数是用 JSON Schema 描述的MCP Client 拿到之后需要把模型生成的参数反序列化成 Java 对象。如果你在 Server 端用了 Java record 来定义参数又没有处理好字段名映射很可能出现调用成功但参数全是 null的情况。这是因为 JSON 里的snake_case字段和 Java 字段名对不上或者 record 的构造器默认行为不符合 Jackson 的预期。排查思路是先看 Server 返回的参数对象有没有值再看模型实际生成的工具调用参数 JSON。我建议在参数对象上用标准 POJO 加 getter/setter或者显式加JsonProperty。如果你确实喜欢 record那就要确保 Jackson 版本和配置支持 record 的构造器绑定并在参数描述里把字段名写得非常清楚。6.4 流式输出和工具调用别急着上 streamChatClient的.stream()是可以和工具调用共存的但生产环境里经常出现一个现象模型先流式输出了一段推理过程或者中间想法然后才发起工具调用拿到工具结果后又继续流式输出。如果你把这些中间片段直接推给前端用户体验会很奇怪还会造成上下文割裂。排查建议是先用.call()把逻辑跑通确认工具调用链路没问题再切流式。流式状态下要对输出做缓冲和过滤区分哪些是给用户看的最终文本哪些是工具调用的中间产物。多轮工具调用也要设置上限防止模型陷入调用-失败-再调用的死循环Spring AI 的相关参数你可以在配置和 API 文档里找一下没有的话直接在系统提示词里限制最多调用三次工具。6.5 工具描述写不好模型就不会用这一点可能比所有配置都重要。MCP Server 暴露工具时工具描述是模型决定要不要用、什么时候用的关键依据。描述写得太笼统模型会忽略它描述写得太复杂模型又会过度调用。我后来养成一个习惯每个工具的 description 都用什么时候用 参数含义 返回值说明三段式写清楚。这个经验在客户端使用其他团队写的 Server 时同样有效——如果某个工具总是没被调用先去看看它的描述是不是一大段废话。7. 对接百炼平台 Qwen 模型MCP 工具链的无缝迁移7.1 为什么单独聊百炼国内开发者现在用阿里云百炼平台上的 Qwen 系列模型非常多。很多人在 Spring AI 里接百炼时会有个错觉觉得换了模型服务商之后自己辛辛苦苦接好的 MCP 工具链也得重写。其实完全不用这正是 MCP 协议带来的好处。我在项目里做过一次从 OpenAI 兼容接口到百炼 DashScope 的切换。模型 Starter 换掉API Key 换掉模型名称换掉但 ChatClient 的构建方式、MCP 工具注册逻辑、工具调用链路一行都没动。7.2 引入 DashScope 模型 Starter在 pom 里加上dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model-dashscope/artifactId /dependency然后配置spring: ai: dashscope: api-key: ${DASHSCOPE_API_KEY} chat: options: model: qwen-max模型名称以你百炼控制台上实际开通的为准现在 Qwen 系列的版本更新很快建议直接去控制台确认不要看我文章里写的这个。值得说一下的是搜索热词里很多人关心 Spring AI 2.0 怎么连接百炼、Qwen 某个具体版本怎么配其实万变不离其宗先确认你用的 Spring AI 版本对应的 DashScope Starter 坐标再在配置里指定模型名。7.3 同一个 ChatClient 挂上 MCP 工具代码和前面完全一样ChatClient chatClient ChatClient.builder(chatModel) .defaultTools(toolCallingManager) .build(); String answer chatClient.prompt(查询本地仓库里最新的提交记录) .call() .content();这里chatModel由 DashScope Starter 自动创建toolCallingManager由 MCP Client Starter 自动创建。你不需要感知模型是 OpenAI 还是百炼工具调用流程在中间层被消化掉了。不过要提醒一句不同模型厂商的 function calling 能力有差异主要体现在单次请求能携带的 tool 数量、工具描述 token 上限、以及多工具并发调用的表现。Qwen 系列对大工具列表的容忍度不错但当 MCP Server 工具数量超过几十个时建议做工具路由或者按领域拆分成多个客户端不要一股脑全塞给模型。否则提示词里光是工具定义就占掉大量 token回答质量和成本都会受影响。7.4 云端 MCP Server 的一个部署思路如果你手头还没有独立的 MCP Server只想快速验证百炼 MCP可以在本地另起一个 Spring Boot 应用引入spring-ai-starter-mcp-server用Tool注解写几个工具方法然后通过 SSE 端口暴露出来。你的主应用用 SSE 连接它这在前后端分离的团队里也很实用MCP Server 由数据团队维护AI 应用团队只负责配置连接。这种部署模式很干净MCP Server 独立存活升级、扩容都跟应用本体无关。后续这个系列我也会专门写一篇 MCP Server 端的开发笔记把Tool注解、McpToolUtils这些工具怎么用详细讲一讲。8. 最后交代两句说回到 Boot Starter 本身我现在带新人做 Spring AI 项目第一步永远都是让他们拿官方 filesystem Server 跑通一遍 MCP 链路。不是因为文件系统这个功能多实用而是这个过程能把模型感知工具 - 发起调用 - 拿到结果 - 组织回答这条链路完整地展示出来。很多人刚开始接触 MCP 会被一堆概念吓到觉得又要学协议又要学配置但它底层解决的是非常实际的问题让外部系统变成模型可以按需调用的能力并且这个能力不绑定任何一家模型厂商。我个人实际项目里的体会是MCP 不是银弹。如果一个工具只有一个应用在用团队也稳定那直接写 Function Calling 反而更快。一旦你有多套应用要复用同一批工具或者团队之间要解耦工具实现与 AI 应用MCP 的价值才真正体现出来。还有一点是监控一定要早做。MCP 工具调用一旦进入生产延迟、失败率、调用次数这些指标最好通过日志和链路追踪记录下来否则出问题的时候你连是模型判断错了还是 Server 变慢了都分不清。
返回列表