
去年做AI助手那阵子最让我头疼的不是大模型幻觉而是“接口对接”。公司内部有订单查询、库存查询、物流追踪好几个服务每个都要给大模型写一套工具描述还得分别适配不同厂商的function calling规范。更崩溃的是工具是动态变化的——新增一个业务工具就要改一遍JSON Schema再重新部署一次。后来切到MCP协议配合Spring AI把这套链路重新捋了一遍才觉得这事终于理清楚了。这次就把SpringBoot 3.x Spring AI JDK 17技术栈下通过SSE协议暴露MCP服务、供大模型调用工具的全过程写出来包括服务端和客户端各自要做什么以及我在真实项目里踩过的那些坑。1. 为什么我最终转向MCPFunction Calling的对接困境与协议破局1.1 那段写工具描述写到想吐的日子如果你做过Function Calling一定经历过这种循环大模型厂商A给的工具格式是JSON Schema厂商B也说是JSON Schema但字段命名、参数约束、返回格式要求就是不一样。一套业务工具要写三份描述每份还都得控制token长度生怕描述太长把上下文撑爆。更麻烦的是工具一多参数之间还有依赖关系比如“查库存”必须依赖“先查门店”这种逻辑在Function Calling里根本表达不清楚只能靠提示词硬塞效果全靠运气。我当时的体感是大模型本身的能力进步很快但把内部工具连接到大模型的“最后一公里”反而成了整个项目里最耗时的部分。1.2 MCP的破局思路MCP全称Model Context Protocol模型上下文协议。它的核心想法很朴素与其让每个工具提供方各自定义一套对接规范不如定义一套统一的工具描述格式和统一的消息交互方式。工具方只要实现一次MCP服务端任何支持MCP的AI应用都能直接发现并调用这些工具。打个比方以前给相机配闪光灯不同品牌接口不同你得买对应的转接头MCP相当于把相机和闪光灯的接口统一成了标准热靴所有设备都按一个规范来接。这个思路落地之后解决的不只是格式问题还有一个更关键的体验变化工具的发现和调用不再写死在代码里。AI应用在运行时通过MCP协议向服务端发起“列出所有工具”的请求拿到工具清单和描述再根据用户问题动态选择工具调用。新增工具时服务端那边加一个方法、加个注解客户端完全不用改。1.3 传输方式的三选一MCP协议支持三种传输方式我分别说下适用场景传输方式特点适合场景stdio客户端启动子进程通过标准输入输出交互本地AI Agent、MCP CLI工具大模型和工具在同一个进程内SSE基于HTTP长连接服务端向客户端单向推送跨进程、跨机器的工具调用Spring服务对外暴露工具Streamable HTTPMCP最新推荐的HTTP传输用POSTSSE组合生产环境推荐兼容性好部署简单标题里说要讲SSE这也是目前Spring生态里集成度最高、资料相对完整的一种方式。SSE全称Server-Sent Events是HTTP协议上的服务端推送技术。它跟WebSocket最大的区别是单向服务端可以主动往客户端推数据但客户端到服务端的消息走普通HTTP POST。MCP服务的消息流模式——工具列表、调用结果需要从服务端推向客户端——用SSE这种轻量方案正合适。1.4 为什么Java/Spring团队必须关注Python社区的AI Agent框架多MCP的样板代码遍地都是。但Java后端成熟的团队大量业务代码都跑在SpringBoot里如果要把这些存量业务能力开放给大模型指望开发团队用Python重新实现一遍不现实。Spring AI从1.0版本开始把MCP作为一等公民支持提供了spring-ai-starter-mcp-server和spring-ai-starter-mcp-client两个starter。传统Spring开发者不需要学习MCP底层协议细节只需要写一个带注解的Java方法剩下的协议封装、SSE连接管理、消息序列化全部由框架搞定。这才是Java生态里接入MCP的正确姿势。2. 版本与工程准备JDK 17和SpringBoot 3.x的配套选型2.1 SpringBoot 3.x为什么要JDK 17起步很多人在项目初始化时就卡在第一步本地JDK是8而SpringBoot 3.x和Spring AI对JDK版本的要求是硬性的。SpringBoot 3.x从设计之初就把JDK 17作为基线不是能用、是必须。原因是Spring Framework 6全面拥抱Jakarta EE 9命名空间同时基于JDK 17的字节码级别做了编译优化。硬要在JDK 8上跑SpringBoot 3也不是完全不行但会遇到各种诡异的类加载问题而且官方根本不提供支持。Spring AI 1.0的要求同样是JDK 17以上。如果你还在用JDK 8写着老SpringMVC项目想集成MCP基本等于要把整个工程升级到SpringBoot 3这是一次牵一发动全身的改造。但从我的实践经验看SpringBoot 3 JDK 17的组合在内存占用、启动速度、虚拟线程Virtual Threads方面的收益非常明显特别是高并发下IO密集型任务虚拟线程能把吞吐量拉高好几倍。2.2 JDK 17安装的实操细节版本选型明确后第一件实际工作是安装JDK 17。这里分享几个我在Windows和Linux两条路上都踩过的细节。Windows安装优先选择二进制安装包比如Temurin、Corretto发行版一路Next装完。装完后必须手动配置两个环境变量JAVA_HOMEC:\Program Files\Eclipse Adoptium\jdk-17.0.13.11-hotspot Path%JAVA_HOME%\bin;%Path%配置完后在命令行执行java -version验证。这里有个很容易踩的坑如果你机器上装过其他版本的JDKPath里可能出现多个java路径命令行执行到的还是旧版。解决方案是把 %JAVA_HOME%\bin 移到Path最前面或者去系统Path里把旧JDK的路径删掉。Linux安装Linux服务器上我习惯用tar.gz解压方式因为不依赖具体包管理器各发行版通用tar -zxvf jdk-17_linux-x64_bin.tar.gz -C /usr/local/java/ vi /etc/profile # 在文件末尾追加 export JAVA_HOME/usr/local/java/jdk-17.0.13 export PATH$JAVA_HOME/bin:$PATH source /etc/profile这里有个容易忽略的点如果你服务器的CPU是ARM架构比如某些云上默认就是ARM实例下载时必须选aarch64版本不然会直接报cannot execute binary file。我在一台国产化服务器上就吃过这个亏下载x64的包怎么都跑不起来最后换ARM版本一下就好了。如果你用的是Maven或Gradle构建项目还需要在IDE和构建工具里都确认一遍JDK版本IDEA的Project Structure、Maven的JAVA_HOME、Gradle的JVM配置三处要保持一致任何一处不一致都会导致编译或运行时报错。2.3 工程基础结构父POM与依赖版本我习惯把MCP集成拆成两个工程实践工具提供方服务端和AI应用方客户端。职责分离部署也灵活。下面是一份可直接参考的父POM配置parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.4.1/version relativePath/ /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 /dependencyManagementSpringBoot版本我建议直接上3.4.x。3.2和3.3也能跑但Spring AI 1.0在设计时就是基于SpringBoot 3.4做的兼容测试你拿3.2版本去配Spring AI 1.0大概率会遇到依赖传递时拉取到不兼容的SpringFramework版本。2.4 版本组合建议我踩了几次坑之后整理出一份比较省心的版本组合组件版本说明JDK17长期支持版够用且稳定SpringBoot3.4.x对Spring AI兼容性最好Spring AI1.0.0GA版本MCP支持完整Maven3.9旧版本Maven解析Spring AI BOM会有问题这套组合在我实际项目中验证过跑SSE模式的MCP服务端和客户端都没有遇到版本层面的硬伤。如果你要用GradleGradle 8.5以上版本问题不大。3. 服务端实现Spring AI把业务工具发布成MCP服务的完整步骤3.1 引入服务端依赖与SSE配置服务端工程的pom.xml需要引入web和支持MCP Server的依赖dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-mcp-server/artifactId /dependency /dependenciesMCP server本身需要跑在Web容器里所以spring-boot-starter-web是必须的。加完依赖后真正的核心配置就那么几行spring: application: name: mcp-server-demo ai: mcp: server: sse: enabled: true name: order-tool-server version: 1.0.0解释一下这些配置项的含义spring.ai.mcp.server.sse.enabledtrue是开启SSE传输方式的总开关不配置这个MCP服务端默认只会走stdio对远程调用没有任何意义。name和version是MCP协议握手时用的标识客户端连上来会先做一次initialize握手交换服务端信息这两个字段会出现在握手响应里。启动应用后如果你看日志里有类似MCP server endpoints exposed: /sse, /mcp/message的输出说明SSE端点已经注册成功。3.2 Tool注解从普通方法到MCP工具Spring AI把MCP工具抽象得非常简练核心就是Tool注解。看一个实战例子我封装了一个订单查询工具import org.springframework.ai.tool.annotation.Tool; import org.springframework.stereotype.Component; Component public class OrderQueryTools { Tool(description 根据订单号查询订单当前状态和物流进度当用户询问我的订单到哪了、发货没有、物流进度时调用) public String queryOrderStatus(String orderId) { // 这里可以继续封装FeignClient、Dubbo接口或者JDBC查询 if (A1001.equals(orderId)) { return 订单已发货当前物流北京市朝阳区中转站预计48小时内送达; } else if (A1002.equals(orderId)) { return 订单正在拣货中预计24小时内发出; } return 未查询到该订单请核对订单号; } }这个方法的逻辑并不复杂重点是几件事第一Tool注解所在类必须被Spring容器管理所以加Component。如果类没有被Spring扫描到Spring AI启动阶段扫描不到任何工具MCP服务端暴露的工具列表就是空的你在调试时会发现客户端能连上但始终没有工具可调用。第二description字段极其重要。大模型决定“这个用户问题该不该调用这个工具”完全依赖这段描述做语义匹配。我见过很多人在这只写“查询订单”四个字结果大模型在用户问“什么时候发货”时愣是不调用因为描述里看不出发货也归它管。好的描述要包含三层信息这个工具是干什么的、什么场景下应该调用、返回什么内容。第三方法参数尽量用简单类型。MCP在传输参数时是JSON序列化大模型负责把用户问题里的信息映射到参数上参数越复杂映射越容易出偏差。如果参数是QueryOrderRequest这种嵌套对象大模型经常漏字段。我是把复杂请求参数打平成多个简单类型参数牺牲一点优雅换调用准度。3.3 启动后如何确认MCP服务已就绪服务启动后不要急着写客户端先用基础的HTTP工具验证一下MCP服务是否正常。SSE端点可以通过curl验证curl -N http://localhost:8080/sse如果你看到一串SSE事件流输出其中包含类似event: endpoint和data: /mcp/message的内容说明服务端的SSE通道已经建立。endpoint事件是MCP over SSE的第一个握手步骤它告诉客户端后续你通过POST请求发消息时请求要打到哪个URL。如果想进一步确认工具列表有没有正常暴露可以向消息端点发一个JSON-RPC请求curl -X POST http://localhost:8080/mcp/message \ -H Content-Type: application/json \ -H Accept: application/json, text/event-stream \ -d {jsonrpc:2.0,id:1,method:tools/list,params:{}}正常的响应里会携带服务端注册的工具列表。这一步验证通过说明工具已经成功发布为MCP服务客户端后续调用才有基础。3.4 工具组织的几个建议一个服务端不会只放一个工具。以我实际项目为例订单查询、库存查询、退换货进度查询可能散落在不同的业务模块。我的组织方式是按业务域拆分不同的Component类比如OrderQueryTools、InventoryTools、LogisticsTools每个类里面放该域的若干工具方法。工具方法的命名采用“动词业务对象”的标准比如queryOrderStatus、createReturnOrder语义清晰方便大模型理解。服务端不要做鉴权之外的复杂业务逻辑工具方法应该只做参数校验和调用下游服务把结果原样返回。MCP工具是大模型和业务系统之间的“路由层”不是业务逻辑的家。如果工具数量上了规模比如超过20个还需要考虑工具描述的token成本。每次会话初始化时客户端都要拉取全量工具列表工具描述越长占用的上下文越多。这时候要么精简描述要么拆成多个MCP服务端按需订阅。4. 客户端接入大模型通过SSE协议调用工具的真实链路4.1 客户端依赖模型SDK与MCP客户端客户端这边本质是一个SpringBoot应用但它要做两件事连接大模型服务连接MCP服务端。依赖如下dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model-openai/artifactId /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-mcp-client/artifactId /dependency /dependenciesspring-ai-starter-model-openai是模型适配层它兼容OpenAI规范的模型服务包括常见的国内大模型网关。spring-ai-starter-mcp-client负责和MCP服务端建立SSE连接、管理工具发现的整个生命周期。配置上客户端比服务端多一点需要在application.yml中同时配置模型参数和MCP连接信息spring: ai: openai: api-key: ${LLM_API_KEY} base-url: ${LLM_BASE_URL:https://api.openai.com} chat: options: model: gpt-4o-mini mcp: client: enabled: true type: SYNC sse: url: http://localhost:8080/ssespring.ai.mcp.client.sse.url指向MCP服务端的SSE地址这是客户端“找到工具”的关键入口。4.2 编写ChatClient剩下的交给自动发现客户端业务代码极其简单因为MCP工具的注册和发现完全是自动的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.Builder chatClientBuilder) { this.chatClient chatClientBuilder .defaultSystem(你是一个订单助手回答用户问题时要基于工具返回的真实数据不要编造。) .build(); } GetMapping(/chat) public String chat(RequestParam String q) { return chatClient.prompt(q).call().content(); } }看到区别没有这里没有任何一个地方手动指定“我要调用订单查询工具”。Spring AI的MCP客户端会自动向MCP服务端发起初始化握手、拉取工具列表然后把工具列表注册到ChatClient的内部工具库。大模型在生成回复时如果判断需要调用某个工具会自动组装参数通过MCP客户端发请求到服务端拿到返回结果后再组织语言。这个自动发现机制是我最推崇MCP的地方。后续如果MCP服务端新增一个“查发票”的工具客户端代码一行不用改只需要服务端那边加一个Tool方法客户端下次启动时就能自动发现并调用。4.3 一次完整调用的链路拆解从提问到工具结果返回一条用户消息“帮我查一下订单A1001到哪了”在MCP over SSE链路上是这么走的客户端启动时MCP客户端通过POST /mcp/message发送initialize请求完成协议握手。客户端发送tools/list请求MCP服务端返回所有Tool方法清单包括queryOrderStatus的描述和参数Schema。用户在大模型应用界面提问“帮我查一下订单A1001到哪了”。ChatClient把这个用户问题发给大模型大模型在看到系统提示和可用工具列表后判断“这个问题需要调用queryOrderStatus工具参数是A1001”返回一个工具调用请求。Spring AI把工具调用请求转交给MCP客户端MCP客户端通过POST /mcp/message发送tools/call请求参数里带上方法名queryOrderStatus和参数{orderId:A1001}。MCP服务端执行queryOrderStatus方法拿到“订单已发货当前物流北京市朝阳区中转站预计48小时内送达”的结果通过SSE连接把结果推回客户端。客户端把工具返回结果再次交给大模型大模型基于这个事实性数据生成最终的自然语言回答“您的订单已发货目前在中转站预计48小时内送达。”整个链路对大模型应用层是透明的你在代码里看不到任何MCP协议的痕迹但协议的全部价值都在背后体现。4.4 核心配置项的调优参考MCP客户端的配置项里有几个我在项目里用过之后觉得值得留意的参数配置项默认值说明spring.ai.mcp.client.typeSYNC同步调用适合请求响应模式ASYNC更适合流式场景spring.ai.mcp.client.request-timeout20s客户端向MCP服务端发起请求的超时时间内网通常不用改跨公网建议调大spring.ai.mcp.client.sse.connect-timeout10s建立SSE连接的超时时间如果工具方法执行本身就要十几秒比如查一个复杂的报表request-timeout保持默认的20秒很容易超时。我的做法是把超时调到60秒但要在服务端配合实现异步工具调用或者提前返回“任务已接收”的机制避免客户端长时间阻塞。5. 踩坑记录版本兼容、SSE断流与工具描述的影响5.1 版本不匹配导致的NoSuchMethodError第一次集成时我复用的老项目SpringBoot版本是3.2.5Spring AI版本是1.0.0-M2。启动一切正常但客户端一调用工具就报NoSuchMethodError堆栈指向Spring Framework的某个类。排查过程是这样的先把堆栈完整打出来定位到异常类是从哪个jar包加载的。用mvn dependency:tree检查发现Spring AI 1.0.0-M2依赖的Spring Framework版本高于3.2.5自带的版本Maven仲裁时保留了一个较低版本导致类文件不兼容。解决方案不是去排除依赖而是把SpringBoot统一升到3.4.x。升级之后再跑问题消失。我的经验是Spring AI和SpringBoot属于强耦合版本匹配不能靠Maven自动仲裁必须在父POM里用BOM统一锁定。5.2 SSE连接断流的排查全流程SSE长连接在开发环境一切正常部署到测试环境后发现一个问题MCP服务端运行一段时间后客户端就再也调不动工具了。一次完整的排查链路长这样第一步看客户端日志。发现EventSource连接被关闭客户端SDK尝试自动重连但重连后立刻又被断开反复循环。第二步看服务端日志。服务端没有任何异常Tomcat连接池也没有报错。说明不是业务代码抛异常而是连接被网络层切断。第三步检查前置网关配置。测试环境有Nginx做流量转发SSE长连接在Nginx层如果一段时间没有数据流动会被proxy_read_timeout默认的60秒切断。这是最典型的SSE断流原因。Nginx针对SSE需要特殊配置把超时时间调大location /sse { proxy_pass http://backend-server; proxy_set_header Connection ; proxy_http_version 1.1; proxy_read_timeout 3600s; proxy_buffering off; }第四步另一个隐蔽的坑Nginx默认开启proxy_buffering会把后端返回的数据缓冲起来再转发给客户端这让SSE的“实时推送”变成了一坨一坨的批量传输客户端等半天才收到一次数据。必须设置proxy_buffering off关闭缓冲。第五步如果网关没问题再检查MCP服务的Tomcat自身。SpringBoot内嵌Tomcat的keepAliveTimeout默认是60秒如果SSE连接长时间空闲Tomcat主动断开。这个参数要配合调大server: tomcat: keep-alive-timeout: 3600000这个排查链路走下来我对SSE在真实网络环境里的稳定性有了更深的理解SSE协议本身很简单但生产环境的连接寿命取决于你整条链路上所有组件的超时配置。5.3 工具描述是AI调用准不准的分水岭有一次我改造一个库存查询工具起初描述只写了“查询库存”结果大模型在用户问“这个商品还有货吗”的时候不调用反而在用户问“推荐一个商品”时调用了库存工具。这种语义偏差的问题极难排查因为代码层面一切正常工具也确实被调用了只是调用的时机不对。后来我把描述改成“根据商品编码查询可售库存和剩余数量。当用户询问‘有没有货’、‘库存还剩多少’、‘是否可购买’时调用”准确率明显提升。工具描述的写法是有方法论沉淀的我的模板是根据[参数A]查询[业务对象]的[具体信息]。当用户询问[典型问题1]、[典型问题2]、[典型问题3]时使用。返回内容包含[返回字段说明]。核心是让大模型在语义空间里找到你的工具和用户问题之间的映射关系。这段描述的质量直接决定了MCP工具被调用的准确率是MCP集成中比代码本身更值得打磨的地方。5.4 生产环境安全给MCP服务加上认证MCP协议的SSE端点如果暴露在公网任何人都可以连接并调用你的工具这是生产环境不能接受的风险。我见过团队把Spring Boot服务部署到公网测试环境结果扫描器直接连上MCP端点把工具列表拉走了。Spring AI的MCP服务端支持通过配置开启Authorization认证。我的方案是在MCP服务端加一个拦截器校验请求头里的AuthorizationComponent public class McpAuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String token request.getHeader(Authorization); if (!Bearer your-secret-token.equals(token)) { response.setStatus(401); return false; } return true; } }客户端配置时给每个HTTP请求都带上同一个tokenspring: ai: mcp: client: headers: Authorization: Bearer your-secret-token两种配置方式配合MCP服务端和客户端之间有了最基本的身份确认。token的管理要放到配置中心或者环境变量里不要硬编码在代码仓库。我在实际使用中还发现一个问题如果MCP服务端和客户端都在同一个局域网内很多人嫌麻烦不配认证。但我还是建议至少在测试环境开启认证因为MCP的tools/list请求会把你的工具清单全量暴露出来这些信息本身就是业务细节不该让无关方拿到。最后再分享一个小技巧调试MCP链路时不要把客户端日志级别设成INFO。把Spring AI和MCP相关的日志调到DEBUG你会看到完整的握手过程、工具列表拉取和每次tools/call的请求响应报文。这个层面的日志比任何文档都更能帮你理解MCP协议在真实运行时的状态流转。