尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Spring AI集成MCP实战:让Java方法变成AI可调用的工具

Spring AI集成MCP实战:让Java方法变成AI可调用的工具 1. MCP到底在解决什么问题模型终于不再是只读的了如果你一直在跟Spring AI的学习节奏看到MCPModel Context Protocol模型上下文协议这三个字母时大概跟我一样先是兴奋然后懵。前面几篇学了ChatModel怎么对话、PromptTemplate怎么驱动、RAG怎么把知识塞给模型但到了RAG那篇结尾我其实有个挺别扭的发现模型依然只能读不能动。它能根据检索到的内容回答这个订单是什么状态但它没法自己去订单系统里查一下更没法替你点一下取消订单按钮。MCP就是来解决这个问题的。它是Anthropic在2024年底牵头提出的一个开放协议底层基于JSON-RPC 2.0核心目标特别朴素让AI应用能用统一、标准、安全的方式去调用外部工具和数据源。你可以把它想成AI界的USB-C接口——USB-C出现之前每个设备都有自己的接口充电器堆一抽屉MCP出现之前每个AI应用要对接一个外部系统就得单独写一套集成逻辑API地址、鉴权方式、参数格式全是各搞各的。有了MCP工具提供方只要写一个符合协议的MCP Server任何MCP Host比如Claude Desktop、Cursor、或者我们手里的Spring Boot应用都能直接连上自动发现它提供了哪些工具然后让模型按需调用。要理解MCP先记住三个角色MCP Host是承载AI应用的程序MCP Client是Host内部负责跟Server建立连接、维护会话的组件MCP Server是暴露能力的一方可能是本机子进程也可能是远程HTTP服务。Server对外提供三类能力原语Tools工具、Resources资源、Prompts提示词模板。最容易混淆的是Tools和Resources。Tools是可以执行的动作比如查订单、算运费、发消息有入参有返回结构模型判断需要时主动调用Resources是可以读取的数据比如一个文件、一张表、一段配置文件内容。简单说Tool让模型能做事Resource让模型能看资料。大部分业务场景我们用得最勤的是ToolsSpring AI的Tool注解就是干这个的。对Java开发者来说MCP最大的意义在于Spring AI已经把它做成了官方一等公民。你在项目里加一个starter写几个普通方法这些方法就能被任何MCP客户端发现和调用不用自己实现JSON-RPC那套握手逻辑。这篇文章我就按自己的学习路径从搭建Server、配置Client、到排坑完整走一遍。2. Spring AI对MCP支持的整体拆解Host、Client、Server三个角色怎么分工Spring AI从1.0.0-M里程碑版本就开始跟进MCP到1.0.0正式版MCP相关的API已经稳定下来了。先搞清楚一件事同样的Spring Boot应用在MCP里可以扮演三种角色而且可以同时扮演。第一种角色是Host。这是最常见的表述——你的Spring Boot应用里跑着ChatModel、维护着会话上下文用户在这里跟AI对话这个应用就是Host。Host负责理解用户意图、决定什么时候调用工具、把工具返回结果组织成最终答案。第二种角色是Client。当你的Spring Boot应用需要连接外部MCP Server时它内部就会创建MCP Client组件与远程Server握手、发现工具、发起调用。对应依赖是spring-ai-starter-mcp-client。第三种角色是Server。当你的Spring Boot应用想把内部的业务方法暴露成一套标准的MCP工具时它就变成了一台MCP Server对应依赖是spring-ai-starter-mcp-server。其他任何MCP Host都能连过来调用你暴露的方法。理解Spring AI的MCP架构核心是抓住一个抽象ToolCallback。在Spring AI里不管是本地用Tool注解写的函数工具还是从远程MCP Server发现的外部工具最终都会被封装成ToolCallback。ChatModel层根本不管工具是从哪来的它只看到一批统一的工具描述。这个设计非常聪明相当于把Function Calling和MCP彻底打通了。我画一下Client端的自动装配流程方便你理解应用启动时发生了什么spring-ai-starter-mcp-client读取application.yml里配置的连接列表McpClientManager为每个连接创建一个McpClient每个McpClient在启动阶段跟远程Server完成MCP初始化握手并调用tools/list拉取工具清单拉到的每个远程工具被转换成ToolCallback注册进Spring容器你在代码里注入ListToolCallback挂到ChatClient.Builder.defaultTools(...)上。Server端则是反过来spring-ai-starter-mcp-server会扫描容器里所有ToolCallbackProviderBean把其中封装的工具收集起来通过配置的传输方式暴露出去。角色Spring AI对应依赖一句话职责Host任何含ChatModel的应用承载AI对话与工具编排Clientspring-ai-starter-mcp-client连接外部Server把远程工具转成ToolCallbackServerspring-ai-starter-mcp-server把本地Tool方法发布为MCP协议工具关于版本我再啰嗦一句MCP相关API在Spring AI的里程碑版本里变动比较大比如早期工具回调接口叫ToolCallback还是ITool、配置属性前缀是spring.ai.mcp还是spring.ai.mcp.client不同小版本都有差异。我文章里的示例基于Spring AI 1.0.x写的如果你用的版本不一样配置文件报红是正常的以你当前版本的官方文档为准。3. 实战用Spring AI把Java方法发布成MCP Server3.1 新建工程并引入依赖先建一个普通的Spring Boot工程Java 17以上Spring Boot 3.4.x然后引入MCP Server依赖。注意Spring AI的依赖统一走spring-ai-bom管理不能只写版本号。dependencyManagement dependencies dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-bom/artifactId version1.0.0/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement实际依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-mcp-server/artifactId /dependency这里必须带上web starter因为MCP Server要跑HTTP传输没有web容器服务起不来。3.2 用Tool定义一个业务工具假设我们有个订单服务我现在想把根据订单号查询订单状态这个能力暴露给AI。定义一个订单工具类Component public class OrderTool { private final OrderService orderService; public OrderTool(OrderService orderService) { this.orderService orderService; } Tool(description 根据订单号查询订单当前状态订单号形如ORD20241201。查询不到时返回未找到订单) public String queryOrderStatus(String orderId) { Order order orderService.findByOrderId(orderId); if (order null) { return 未找到订单; } return 订单状态: order.getStatus() , 创建时间: order.getCreateTime(); } }两个细节值得注意。第一Tool的description一定要写清楚参数格式和返回语义因为模型完全靠这段描述来决定这个工具是干什么的、什么情况下该调用它、参数应该怎么填。description写得太笼统模型就可能在不该调用的时候调用或者把参数传错。第二工具方法的返回值会被序列化成JSON交给模型理解所以别返回一个巨大的领域对象模型看不明白不说还会白白消耗上下文窗口。返回一段结构清晰的摘要文本效果远好于返回对象。3.3 注册成ToolCallbackProvider这个工具类要生效还得把它封装进一个ToolCallbackProviderSpring AI的MCP Server starter会自动发现这类Bean。Configuration public class McpServerToolsConfig { Bean public ToolCallbackProvider orderTools(OrderTool orderTool) { return MethodToolCallbackProvider.builder() .toolObjects(orderTool) .build(); } }启动应用前在application.yml里配置服务名spring: application: name: order-mcp-server ai: mcp: server: name: order-server version: 1.0.0启动日志里会打印MCP Server的端点信息Spring AI 1.0.x默认会暴露基于SSE的端点。你不需要关心MCP协议里那些initialize、tools/call的报文细节starter全都封装好了。3.4 已有REST接口怎么改造成MCP工具很多人问Java怎么把REST接口发布为MCP这也是我学这节时最关心的点。我的结论是不要碰原来的Controller。Controller只是业务逻辑的HTTP表现层而MCP工具是同样业务逻辑的另一种表现层。正确做法是把工具方法写在Service层或者单独的Tool组件里复用同一个Service方法。看这个改造思路RestController public class OrderController { private final OrderService orderService; // 传统的REST接口保持不动 GetMapping(/orders/{orderId}) public Order getOrder(PathVariable String orderId) { return orderService.findByOrderId(orderId); } }Component public class OrderTool { private final OrderService orderService; public OrderTool(OrderService orderService) { this.orderService orderService; } Tool(description 根据订单号查询订单当前状态) public String queryOrderStatus(String orderId) { Order order orderService.findByOrderId(orderId); if (order null) { return 未找到订单; } return 订单状态: order.getStatus(); } }REST接口和MCP工具有本质区别。REST接口的URL、请求方式、鉴权规则都藏在文档里AI要调用就得提前知道这些信息还得自己拼参数而MCP工具通过规范化的JSON Schema把函数签名、参数约束、返回结构全部明明白白声明出来AI在运行时动态发现按schema调用天然适合Agent场景。所以如果你有系统想让AI操作与其费劲让模型去理解REST文档不如包一层Tool。3.5 验证Server是否发布成功最直接的验证方式不是curl而是写一个MCP Client连接过来。不过在线验证的话可以观察一个细节Server端日志里会打印已注册的工具列表确认看到query_order_status这个方法名就说明发布成功了。注意方法名从驼峰queryOrderStatus自动变成了下划线风格这是MCP工具命名的默认规范。4. 实战Spring AI作为客户端接入MCP Server并让模型自动调用工具4.1 引入客户端依赖并配置连接现在换一个Spring Boot工程来扮演Client端。依赖改成dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-mcp-client/artifactId /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model-openai/artifactId /dependency如果MCP Server就在另一台机器上通过SSE方式连接配置文件这样写spring: ai: mcp: client: sse: connections: - name: order-server url: http://localhost:8080还有一种更常见的模式是stdio就是MCP Server作为本机子进程通过标准输入输出通信。比如接一个操作系统文件访问的官方示例Serverspring: ai: mcp: client: stdio: connections: - name: filesystem command: npx args: [-y, modelcontextprotocol/server-filesystem, /tmp]stdio模式下Spring AI每次启动会拉起这个子进程通过管道跟它通信。好处是本地工具不需要暴露HTTP端口适合开发调试坏处是进程生命周期由客户端管理Server挂了整个应用可能起不来这点后面排坑时我会细说。4.2 把远程工具挂到ChatClient上客户端starter启动时会自动完成跟Server的握手和数据拉取容器里就有了来自远程的ToolCallback。剩下要做的事很直白把工具列表挂到ChatClient上。Configuration public class ChatClientConfig { Bean public ChatClient chatClient( ChatClient.Builder builder, ListToolCallback toolCallbacks) { return builder .defaultTools(toolCallbacks.toArray(new ToolCallback[0])) .build(); } }然后就是一个最简单的聊天接口RestController public class ChatController { private final ChatClient chatClient; public ChatController(ChatClient chatClient) { this.chatClient chatClient; } PostMapping(/chat) public String chat(RequestBody String question) { return chatClient.prompt(question).call().content(); } }4.3 工具被调用的完整链路当用户发起帮我查一下订单ORD20241201现在的状态时背后实际发生的事情是这样的请求带着用户问题发给ChatModel同时Spring AI把MCP Server发过来的工具schema一并塞进请求ChatModel看完问题觉得查订单状态这个意图跟query_order_status工具匹配于是返回一个工具调用指令Spring AI拦截到这个指令通过MCP Client发出tools/call请求远程Server执行方法并返回结果Spring AI把工具结果回传给ChatModel模型基于结果生成最终答案返回给用户。你可以把日志级别调低看到每一步logging: level: org.springframework.ai: DEBUG日志里会打印类似MCP tool [query_order_status] processed这样的记录看到它就说明整条链路通了。我建议第一次跑通时仔细看一遍日志比读十遍文档都有用。4.4 多个Server同时接入时的注意事项在同一个配置文件里可以挂多个连接这没问题。但多Server场景下最容易踩的坑是工具重名两个Server都暴露了一个叫get_order的工具ToolCallback注册到容器时就会冲突或者后注册的把先注册的覆盖掉。我的应对办法有三个按优先级排序第一个是在设计Server端工具时命名就带业务前缀比如order_query、logistics_query避免重名第二个是在客户端启动时根据连接名过滤ToolCallback只暴露当前对话需要的工具第三个是控制单次对话挂载的工具数量工具schema是要占token的挂三五十个工具上去光工具描述就可能把上下文塞满响应耗时也会明显变长。5. 我踩过的坑工具注册不上、超时无声失败、传输选型5.1 工具注册不上模型永远不调用这是最常见的翻车现场。代码看起来完全没问题工具类有ToolToolCallbackProvider也建了但问模型问题它就是不用工具兜底乱答。我排查下来原因通常是这几种第一种ChatClient里压根没挂工具。注意defaultTools(...)和defaultFunctions(...)一样必须在构建ChatClient时就传进去事后往已经构建好的实例里塞是不行的。第二种工具description写得太模糊。比如把description写成查询订单模型根本不知道这个工具是查订单状态还是查订单金额参数格式是什么。description写细了之后调用率立竿见影地提升。第三种工具方法返回了null或者异常被吞掉了。模型拿到异常信息后会以为工具调用失败干脆放弃工具走直接回答。这里务必让工具方法兜底返回明确的提示文本比如未找到订单。5.2 启动时MCP Client连接失败导致应用起不来MCP Client在应用启动阶段就要完成握手和tools/list这个设计有个副作用如果远端Server没启动或者网络不通整个Spring Boot应用会启动失败而且错误信息还藏在很深的异常栈里。我遇过一次配置文件里的URL端口写错结果本地起服务时一脸懵——查了半天才发现是客户端连接失败。解决办法没别的确认Server先启动、URL可达。如果Server地址在测试环境经常变可以考虑把连接配置挪到环境变量或配置中心避免改代码。5.3 stdio模式下子进程的问题用stdio连接本机MCP Server时坑更多。最常见的是npx命令本身找不到、Node版本太低、或者第一次运行要下载包导致启动超时。Windows环境下尤其麻烦command经常要写成cmdargs里带/c npx ...这种写法不同机器的shell行为还不一样。我的建议是本地开发图方便可以用stdio部署到服务器或者生产环境优先走HTTP传输。HTTP方式便于监控、便于负载均衡也省去了子进程管理的烦恼。5.4 工具调用超时且失败是无声的MCP工具调用有超时机制但默认超时时间在各种网络环境下不一定够用。如果远程工具本身要查数据库甚至调另一个外部服务响应很容易超过默认时限。超时之后模型的表现是假装没这回事直接基于已有信息作答整个过程没有任何报错。这种无声失败特别坑。排查方法是先把相关日志打开看CallMCPTool附近有没有超时记录然后在连接配置里把超时调大。注意Spring AI不同版本里这个配置项的位置略有不同一般是连接级配置里带requestTimeout或timeout这样语义的字段搜一下当前版本的McpClientOptions就能找到。5.5 SSE和Streamable HTTP传输方式怎么选MCP的传输层从stdio到HTTP经历了一个演进过程。早期HTTP传输用的是SSEServer-Sent Events客户端发起请求后通过长连接接收事件流。后来协议升级为Streamable HTTP思路更统一一个HTTP端点同时处理请求和流式响应不再需要单独的SSE连接管理。实际使用中Spring AI 1.0.x的Server端同时支持SSE和Streamable HTTP两种暴露方式Client端配置时要注意跟Server端匹配。新项目我建议优先用Streamable HTTP毕竟这是MCP协议当前推荐的正式传输方式社区里的新Server也都在往这个方向迁移。老项目如果Server只支持SSEClient也别硬换。传输方式适用场景注意事项stdio本地调试、Server与Client同机子进程生命周期由客户端管理Windows下命令写法特殊HTTP SSE跨主机调用、已有老版本Server依赖长连接注意代理与超时配置Streamable HTTP新项目推荐单端点处理请求与流式响应协议更简洁5.6 多Server接入时尽量动态发现工具而不是写死MCP最大的优势就是动态发现Server新增了一个工具Client端重启后自动就能看到不用改代码。这点跟Function Calling的静态注册完全不一样。我在多Server场景下习惯的做法是启动时打印一份当前挂载的工具清单放进日志里。一方面方便排查模型为什么不用某个工具另一方面也能在新增工具后快速确认有没有重名冲突。工具清单就是Agent的能力清单心里有数才知道模型能做什么。6. MCP学习路线和落地建议什么时候该上什么时候别上学到这MCP的骨架基本清晰了。但学完一个技术后我最怕的就是手里有锤子看什么都像钉子所以最后一节聊聊怎么判断要不要用MCP。如果你的场景只是给当前应用加一两个工具工具是固定的只有这个应用用那只用Tool做Function calling就够了完全没必要引入MCP的整套机制启动握手、子进程管理都是额外成本。反过来如果有多个应用要复用同一套工具或者工具集很大、经常动态增删再或者要接入第三方MCP Server比如社区里那些Figma、Chrome、数据库工具那MCP的价值就体现出来了——你不需要为每个工具写对接代码连上就能用。我个人的渐进式路线是这样的第一步在单个Spring Boot应用里把业务方法加上Tool跑通Function Calling感受模型什么时候调工具、参数怎么传、返回结果怎么影响最终回答。第二步当第二个服务也需要用到这些工具时把工具抽出来发布成一个独立MCP Server两个服务通过MCP Client去连。第三步再尝试接入几个社区现成的MCP Server感受动态发现和工具复用。这条路线每一步都有明确收益不浪费。架构层面的几个建议工具方法尽量无状态、可重入因为模型可能会用不同参数调用多次涉及写操作的工具体内一定要做权限校验MCP只解决协议问题不解决鉴权问题工具方法内部的异常一定要捕获并转成可读的文本返回而不是让异常直接抛给上层否则模型拿到的是一堆堆栈回答质量会急剧下降。学MCP给我最大的触动是工具这个抽象层次。以前写Function Calling每个函数都是写死的新增一个能力就要改代码重新部署。MCP把工具做成了可发现、可复用、跨应用共享的服务Agent应用的能力边界从代码里写死的那些变成了动态拉取到的一整张能力清单。这才是Agent能灵活编排各种外部系统的底层支撑。如果你也刚学到这一篇我给你一个最实在的建议别急着读协议源码先照着这篇文章把Server和Client两个工程跑起来互相调用一次亲眼看到日志里tools/list和tools/call的过程概念瞬间就牢固了。跑通之后再回头看协议细节你会觉得它不过如此。
返回列表