Spring AI实战:用MCP协议打通大模型与外部工具调用
在Spring AI的学习路线上到了第四篇我终于觉得自己摸到了点真正面向未来的东西。前面几篇我们把基础的模型调用、结构化输出、以及Prompt Template玩得比较熟了但一直有一个问题困扰着我模型再聪明它的知识边界和行动边界也是死的。你不能让模型直接去查你数据库里的订单也不能让它直接在你公司的GitLab上建一个Issue。而MCPModel Context Protocol模型上下文协议的出现正好就是用来解决这件事的。这篇就把我自己折腾Spring AI集成MCP的完整过程、踩过的坑、以及我对这协议的理解一次性交代清楚。适合看的同学已经在用Spring AI做基础对话应用想进一步让大模型操作外部工具和数据源的Java开发者或者你只是想搞明白MCP到底是啥、Spring Boot项目里怎么落地MCP这篇文章同样能给你一份可以直接抄作业的参考。1. 先把MCP协议讲透它到底解决了什么问题1.1 大模型能力边界与MCP的破局思路先聊点概念。大模型本质上是一个“嘴巴很厉害但手脚不全”的家伙。它可以给你写出格式完美的SQL查询但它没法自己连接数据库执行这条SQL它可以帮你写一段Python脚本处理Excel但不会自己去读取你磁盘上的文件它知道Figma里有设计稿但如果你不专门给它接上Figma的API它连设计稿长什么样都不知道。之前大家是怎么解决这个问题的最常见的做法是函数调用Function Calling或者叫工具调用Tool Calling。你在代码里写一堆方法比如queryOrderById、sendEmail、createGitlabIssue然后把这些方法描述发给模型模型在回答的时候会决定“我需要调用哪个工具、参数是什么”然后你的程序解析这个调用请求、真正执行方法、再把结果返回给模型。这套机制本身没问题问题出在生态不统一。OpenAI有自己的一套Function Calling格式Claude有自己的一套工具调用格式各家SDK的本地方法注册方式也各不相同。你写了一套工具对接了OpenAI回头换成Claude工具代码可能要大改。更麻烦的是每接入一个外部数据源或系统你都要重新写一遍带鉴权的HTTP客户端、重新定义工具描述、重新处理错误和超时。MCP就是在这种背景下被推出来的它给“模型如何访问外部工具和数据”制定了一套统一标准就像USB-C接口统一了充电口一样。有了MCP之后一个MCP Server写好了任何支持MCP的宿主Host都能直接连上去用不用再给每个模型单独定制一套连接方案。1.2 Host、Client、Server三方角色与核心原语MCP的架构其实不复杂就三方角色Host、Client、Server。Host运行大模型应用的主程序比如Claude Desktop、Cursor这类IDE或者你自己写的Spring Boot服务。ClientHost内部和MCP Server建立连接、发起请求的组件。在Host进程内通常一个Server对应一个Client连接。Server真正执行工具逻辑的一方。它暴露出可供模型调用的工具Tools、可供读取的资源Resources和可供复用的提示模板Prompts。这里我强烈建议你记住MCP的三个核心原语因为后面看配置和代码时会反复用到。第一个是Tools模型主动发起的操作比如查天气、创建订单通常需要带参数执行完返回结果。第二个是Resources模型可以读取的数据但不会因此产生副作用类似把文件、数据库里的记录暴露成可读的URI比如mysql://orders/20250101。第三个是Prompts预定义好的提示词模板方便模型统一按某种格式去生成内容比如weekly-report模板自动带出上周数据格式。当初我学习的时候有一个感受不用把MCP想得太玄乎它本质上就是在调用函数这个老需求上做了一层标准化的封装。工具也好、资源也好最终落到客户端代码里都是注册给模型的一堆描述信息以及一个路由到具体执行函数的入口。1.3 传输链路解析stdio与HTTP/SSE怎么选MCP的传输方式目前主流有两种。第一种是stdio传输客户端在本地启动一个子进程和这个子进程通过标准输入输出stdin/stdout通信。这种方式最大的优点是部署简单、权限隔离好比如你想让AI能用本地的Blender或Jadx-GUI通过stdio拉起一个进程AI输出命令这个进程执行完全不需要开HTTP端口。但它有个明显限制子进程和Host必须在一台机器上。第二种是HTTP/SSE传输Server作为一个独立的HTTP服务运行Client通过HTTP或Server-Sent Events与服务端交互。这种方式天然适合分布式部署可以把MCP Server部署在远程服务器上多个Host共享同一个服务。Spring AI官方对两种方式都支持得很不错我在实际项目里的建议是本地小工具、进程管理优先选stdio跨系统、需要对外开放的工具优先选HTTP/SSE。关于HTTP模式目前规范还在演进早期是SSE模式后来社区在推Streamable HTTP模式Spring Boot的MCP实现里已经能配置。你如果不确定用哪种直接看MCP Server那边适合暴露什么协议Client跟着适配就行。2. Spring AI整合MCP的整体设计与依赖选择2.1 Spring AI官方MCP支持方式Spring AI从较新的版本开始把MCP的客户端和支持代码直接集成了进来定位是让Java/Spring生态的开发者可以用最少的样板代码把模型连上各种MCP Server。我自己的理解是Spring AI做MCP做了两件核心工作。第一它实现了MCP协议层的Java客户端和服务器端你可以不依赖任何外部中间件直接在Spring Boot里启动一个MCP Server端点同时也可以用内置客户端去连别的MCP Server。第二它把MCP Server暴露出来的工具和Spring AI的Tool Calling机制打通了模型决策该调用什么工具、参数如何填Spring AI会通过内部机制自动完成。这意味着什么意味着你之前在Spring AI里写Tool注解方法的那套经验可以直接平移过来。你不需要手写JSON-RPC请求不需要自己解析响应只要把MCP Server连进来注册成一个ToolCallback模型就能用了。注意MCP虽然本身不绑定任何大模型品牌但要让模型真的会“用”工具底层还是依赖模型支持工具调用能力。像OpenAI的gpt-4o这些主流模型都支持如果你测试时发现模型完全没有工具调用的动作先确认你用的模型是否支持。2.2 项目版本与依赖配置在写代码之前我先说一下我用的版本组合这组版本在我实测过程中最稳JDK 17Spring Boot 3.3.x 或 3.4.xSpring AI 1.0.0 正式版系列如果你之前用快照版或M系列建议尽早升级API有变化Spring AI的MCP功能相关依赖有两个核心starter按需引入!-- 如果你需要实现MCP Server端把带Tool的方法暴露给外部 -- dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model-mcp-server/artifactId /dependency !-- 如果你需要作为客户端连接其他的MCP Server -- dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model-mcp-client/artifactId /dependency再补一个你自己的模型依赖比如你最常用的OpenAI或者智谱或者Ollama否则光有MCP没有模型也没法跑推理。我这里是用的OpenAI兼容接口做的演示。这里有个依赖管理的小细节Spring AI的BOM要引入到dependencyManagement里否则版本号容易和Spring Boot对不上。用Spring Initializr初始化项目时直接选Spring AI相关模块让IDE帮你把BOM配好后面能省掉一堆头疼的问题。2.3 服务端工具开发的核心注解ToolMCP Server端开发最重要的一点是你要了解如何用Spring AI把普通Java方法变成模型可用的MCP工具。其实方法很简单就是给一个Bean方法加上Tool注解。我举个例子假设我们要让AI能查当前服务器时间这个工具的代码可能是这样的package com.example.mcpdemo.tools; import org.springframework.ai.tool.annotation.Tool; import org.springframework.stereotype.Component; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; Component public class ServerTimeTools { Tool(description 获取服务器当前时间返回格式为 yyyy-MM-dd HH:mm:ss) public String getCurrentTime() { return LocalDateTime.now().format(DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm:ss)); } }就这么简单。加了Tool注解的方法在MCP Server启动后就会自动被注册成工具元信息包括方法描述、参数结构。模型在对话中一旦觉得用户问的是时间相关需求就会发起对这个工具的调用。工具开发时最需要注意的坑是方法参数一定要加注释注释要写清楚含义。因为模型是看着注释和参数名来理解怎么填参的。如果你的代码是这样Tool(description 根据订单号查询订单状态) public String queryOrder(String id) {...}模型大概率不太清楚id到底传订单号还是用户ID。最好写成Tool(description 根据订单号查询订单状态) public String queryOrder(ToolParam(description 订单号例如DD202501001) String id) {...}这一步做得好不好直接决定模型调工具的成功率。你可以脑补一下模型就像一个刚入职的新员工它只能靠你自己写的帮助文档来理解函数怎么用文档写得含糊它自然会瞎猜。3. 手把手演示Spring Boot应用同时扮演Server和Client3.1 初始化工程与服务端工具实现这一节我演示一个既能作为MCP Server、也能作为MCP Client的工程。为什么这么设计因为一个比较典型的场景是你的Spring Boot服务要同时做两件事对外暴露自己的业务能力给其他AI宿主调用Server同时也要会调用别人的MCP服务Client。两者一起启动复用一个工程最省事。我建议你用Spring Initializr初始化项目手动加依赖时注意保持一致。这里给出最小化的Maven配置示例parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.4.1/version relativeParent/ /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 dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model-mcp-server/artifactId /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model-mcp-client/artifactId /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model-openai/artifactId /dependency /dependencies然后我在工程里写了一个模拟的订单系统工具方便后面的调用演示package com.example.mcpdemo.tools; import cn.hutool.json.JSONUtil; import org.springframework.ai.tool.annotation.Tool; import org.springframework.ai.tool.annotation.ToolParam; import org.springframework.stereotype.Component; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; Component public class OrderTools { private static final MapString, String ORDER_STATUS_MAP new ConcurrentHashMap(); static { ORDER_STATUS_MAP.put(DD202501001, 已发货预计1月20日送达); ORDER_STATUS_MAP.put(DD202501002, 商家已接单等待仓库出库); } Tool(description 根据订单号查询订单状态) public String queryOrderStatus( ToolParam(description 订单号例如 DD202501001) String orderId) { if (orderId null || orderId.isBlank()) { return 订单号不能为空; } String status ORDER_STATUS_MAP.get(orderId.trim()); if (status null) { return 未查到订单号 orderId 对应的订单信息; } return JSONUtil.toJsonStr(Map.of(orderId, orderId.trim(), status, status)); } }这个类很简单但已经把工具定义、参数说明、模拟数据返回都覆盖了。实际项目中你把ORDER_STATUS_MAP换成数据库查询就行整个机制是一样的。3.2 配置MCP Server并启动工程启动时Spring AI的自动配置会扫描容器中所有带Tool注解的方法把它们注册成MCP工具。我们要配置的是通过HTTP/SSE方式把这个Server暴露出去。在application.yml里这样配spring: ai: mcp: server: enabled: true name: order-server version: 1.0.0 transport: http http: base-path: /mcp这里把MCP Server的传输方式设成了HTTP服务路径是/mcp。Spring Boot应用启动后你的MCP Server地址就是http://localhost:8080/mcp。如果你要测试连接是否正常可以从官网的MCP Inspector工具连接这个地址也可以用其他支持MCP的客户端去连。我当初一开始用的默认配置不知道默认Transport是什么结果在客户端里怎么都连不上后来才搞清楚stdio和HTTP模式在配置上有本质区别这里你务必注意transport字段的设置。如果你想走stdio方式配置会不太一样。因为stdio模式一般是由外部宿主比如Claude Desktop来启动你的进程你需要告诉宿主怎么启动这个Spring Boot进程也就是命令和参数{ mcpServers: { order-server: { command: java, args: [-jar, /path/to/your-app.jar], env: {} } } }具体配置文件在宿主侧。在Spring Boot这一侧你只需要保证transport: stdio以及程序可以正常通过标准输入输出运行即可。注意这个时候就别引入Web场景了否则进程会一直在等待网络端口反而干扰stdio通信。3.3 客户端接入与工具调用服务端准备好了现在来看客户端。假设我们有一个全新的Spring AI服务要去连接上面这个MCP Server让它暴露的订单工具能被本地大模型调用。客户端这边最核心的是配置MCP Server列表让Spring AI启动时自动连上并拉取工具列表。先加依赖如果你只需要做客户端可以只引入client规格的starterdependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model-mcp-client/artifactId /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model-openai/artifactId /dependency然后配置里写MCP Server地址spring: ai: openai: api-key: ${OPENAI_API_KEY:demo} base-url: ${OPENAI_BASE_URL:https://api.openai.com} chat: options: model: gpt-4o mcp: client: connections: - name: order-server transport: http http: url: http://localhost:8080/mcp这段配置的意思是本地有一个MCP客户端连接名为order-server走HTTP协议访问http://localhost:8080/mcp。Spring AI的自动配置会在启动时向这个Server发起初始化握手拿到它暴露的工具列表并存到上下文中等待模型调用。要真正让模型在对话里用到这些工具我们需要在构造大模型对象时传入对应的ToolCallback。这里有两种常见做法第一种是直接在Service或Configuration里注入ToolCallbackProvider凡是容器里已有的ToolCallback都会自动注册到ChatClientpackage com.example.mcpdemo.client; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.tool.ToolCallbackProvider; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; Configuration public class McpClientConfig { Bean ChatClient chatClient(ChatClient.Builder builder, ToolCallbackProvider mcpToolCallbackProvider) { return builder .defaultTools(mcpToolCallbackProvider) .build(); } }这里defaultTools里的参数是一个ToolCallbackProviderSpring AI的MCP自动配置会自动注册一个基于容器中所有MCP连接构建出的Provider。你不需要手写工具列表模型用的时候会自动选择。更灵活的做法是在运行时按需注册。不过对于大多数场景启动时全量注册已经够用。唯一要注意的是工具如果太多可能会超过模型的上下文窗口限制这个我们在后面问题排查里细说。3.4 完整链路验证OpenAI问一句工具自动执行工程都串起来之后最简单的验证方式是写一个Controller用ChatClient发起对话。这里我写了个非常朴素的测试接口package com.example.mcpdemo.web; import org.springframework.ai.chat.client.ChatClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; RestController public class ChatController { private final ChatClient chatClient; public ChatController(ChatClient chatClient) { this.chatClient chatClient; } GetMapping(/chat) public String chat(RequestParam(defaultValue DD202501001) String orderId) { return chatClient.prompt(帮我查一下订单号 orderId 的状态) .call() .content(); } }启动整个工程后访问http://localhost:8080/chat如果一切正常你会得到一个类似这样的回答“订单DD202501001的状态为已发货预计1月20日送达。”再说得直白一点整个链路是用户说的话进入OpenAI模型 - 模型判断需要查订单状态发起一个queryOrderStatus的工具调用 - Spring AI的MCP客户端把调用请求通过HTTP发送到MCP Server端 - Server端执行我们写的订单查询工具 - 返回结果给客户端 - 模型基于结果生成自然人话回复给你。整个过程里用户没有直接访问任何订单数据库但AI却“掌握”了这个能力这就是MCP的价值。我实测这个过程时最直观的感受是你写的工具方法越规范模型调用的成功率越高而一旦工具描述写得含糊模型就会犹豫或者瞎编参数。所以工具描述和参数注释真不是给编译器看的是给模型看的。4. 真实环境中的常见问题与排查实录4.1 stdio模式进程启动失败或直接退出我在项目里混用过不少MCP宿主工具最常遇到的坑就是stdio模式的Server拉起后立刻退出导致宿主根本拿不到工具列表。问题原因大概率是以下几种启动命令不对。宿主侧配置的command必须是Java可执行文件的绝对路径或者能被系统PATH找到的java。如果你本机装了多个JDK建议在命令里写全路径。缺少jar包依赖。用spring-boot:run方式不一定能保持子进程稳定建议打包成可执行jar直接java -jar。系统输出被污染。stdio要求进程输出只走MCP协议如果代码里有一句System.out.println瞬间就会破坏协议。排查时可以先用命令行手跑一遍进程观察标准输出有没有多余日志。如果你排查半天还是失败一个好用的小技巧是先用MCP Inspector连一下本地ServerMCP Inspector本身就是调试MCP Server的图形化界面连上之后能看到协议层的请求和响应比在宿主里黑盒排查高效得多。4.2 HTTP/SSE模式下连接超时与鉴权配置HTTP/SSE模式的连接问题通常集中在两点。第一是跨域或网络不通MCP Server跑在内网客户端却在外网环境自然握手失败。先确认curl http://localhost:8080/mcp能正常返回再考虑上层问题。第二是鉴权配置现在的MCP Server很多都带了OAuth保护如果你在配置里没带上TokenClient握手阶段会被403挡下来。Spring AI的客户端配置支持自定义Header和鉴权参数。比如需要Bearer Token时可以在连接配置里加自定义Headerspring: ai: mcp: client: connections: - name: secure-server transport: http http: url: http://example.com/mcp headers: Authorization: Bearer xxxx如果Server端是用Spring Boot自己的MCP实现做的也可以通过拦截器或者Spring Security统一控制入口这块按你的业务安全要求来做。4.3 模型上下文窗口不足导致的工具失效如果你同时注册了几十个MCP工具会发现一个很有意思的现象模型在对话里表现得“智商下降”甚至完全忽略工具直接凭记忆给你瞎编答案。原因很可能是模型的上下文窗口被一堆工具定义占满了留给真实对话和推理的空间变少模型就容易出问题。解决方案有几个方向第一个精简工具数量只注册当前场景用得到的工具第二个把工具描述写得短小精准别把一大篇文章塞进description里第三个如果够新用支持动态工具选择或分组的MCP客户端特性只暴露与当前用户请求相关的工具。我自己在测试时发现工具描述从一长串文字压缩成三行要点之后模型调用率反而明显上升这个经验供你参考。4.4 依赖冲突、模型兼容性与版本坑Spring AI还在快速迭代阶段版本升级导致的API变动堪称家常便饭。我碰到过的比较典型的问题有两个。一是Spring AI版本和Spring Boot版本不兼容。Spring AI的里程碑版经常要求特定的Spring Boot版本如果强制降级或升级会出现自动配置不生效或者方法签名错误。解法很简单用Spring Initializr创建项目时直接指定目标版本别手动往老项目里塞新依赖。二是模型不支持工具调用或工具调用格式不一致。MCP是标准协议但模型供应商对工具调用的支持程度并不一样。比如某些轻量模型可能压根不支持结构化工具调用有些模型虽然支持但在流式输出时可能丢掉工具调用片段。遇到这种情况建议先换一个主流的强模型gpt-4o级别或Claude系列排除模型本身问题再检查流式处理逻辑。如果报错信息里出现了类似JsonSchema或ToolCalling Exception的字眼很大概率就是模型返回的工具调用参数与Java侧方法签名对不上。这时候去把ToolParam(description...)再写详细一点或者把参数类型改成更通用的String往往就能解决。4.5 排查工具失效的通用三板斧调试MCP会话是每个集成者必须掌握的技能给你一个我常用的思路总结。先开日志。Spring AI的MCP模块支持debug级别日志在application.yml里logging: level: org.springframework.ai: DEBUG io.modelcontextprotocol: DEBUG这个配置能看到协议层的所有请求和响应工具是否被发现、工具调用是否发出、响应是否成功一目了然。再进Inspector。拿MCP Inspector单独连Server不经过任何AI模型直接手动触发工具调用看工具本身是否有Bug。最后单独调模型测试先不用MCP工具直接让模型输出一个JSON格式的工具调用结果确认模型侧支持工具调用。这三步走下来80%的问题都能定位到具体环节而不是在客户端和服务端之间来回猜。5. 从MCP再往前一步它能衔接到哪些真实场景MCP这个概念在2025年热得发烫GitHub上随便一搜就是几千个MCP Server覆盖了Figma设计稿、Blender建模、Jenkins流水线、Kicad电路设计、通达信股票数据、Jadx-GUI逆向等等。这背后透露出一个趋势模型的能力不再局限于聊天而是开始渗透到各种专业软件和业务系统里。如果你正在用Cursor、Claude Desktop、Codex这类AI编码工具你会发现“配置MCP”正在成为标配操作。比如Figma MCP让AI能直接读取设计稿的图层和样式信息Blender MCP让AI能生成3D建模的基础操作BurpSuite MCP让做安全测试的人能用自然语言指挥抓包工具。而在Java后端世界里Spring AI帮你做到了同一件事把你的业务系统封装成标准工具任何MCP Host都能调用。我甚至可以设想一个场景公司的库存查询服务封装成MCP Server业务人员在自己熟悉的AI聊天工具里直接说“帮我查一下SKU 123的库存”整个查询链路就自动跑通了。这就是MCP替代传统固定GUI流程的一个现实写照。以前我们需要为每个系统开发对应的操作界面现在只要给AI一套标准工具接口它就能帮人类完成那些过去需要点很多按钮才能完成的操作。Spring AI作为Java生态里最成熟的AI框架它能把MCP Server和Client整合到我们最熟悉的Spring配置模型里这确实是一件对未来开发模式非常有参考价值的事。在我的实践里最让我兴奋的并不是几个简单的示例工具而是这种架构带来的扩展性。今天可以暴露一个订单查询工具明天可以加一个库存冻结工具后天可以加一个审批流提交工具。每新增一个工具只是多写一个Tool注解的Java方法而AI宿主那一侧完全不需要升级。这对我来说是真正的“端到端”自由。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →