最近在忙一个 Java 后端接入大模型工具调用的项目,需求不复杂:让 AI Agent 能去查数据库、调内部 REST 接口,还要能实时拿结果。选型的时候卡了一下,最后定了SpringAI + MCP + SSE这条路线,整体跑下来比想象中省事,但坑也不少。这篇文章就围绕这三种东西的组合,讲清楚它们各自解决什么问题、SSE 方式的 MCP 通信到底是怎么建立起来的、服务端和客户端分别要怎么落地,以及我实操中踩过的那些坑。
先说结论:如果你是一个 Java 后端开发,想在现有 Spring Boot 项目里快速接入 AI 工具调用,SSE 方式是最接地气的一条路。你不需要额外引入 WebSocket 服务,不需要自己设计回调协议,SpringAI 已经把 MCP 客户端和服务端的骨架都搭好了,你要做的主要是三件事:暴露传输端点、注册工具、配置客户端。
适合谁看?打算用 Spring Boot 做 AI Agent 或者工具调用后端的开发者,尤其是那些想复用现有 REST API、又不想写复杂调度逻辑的团队。完全没接触过 MCP 的读者也能跟下来,我会先花一点篇幅把 MCP 和 SSE 之间的协作关系讲透,然后再进入实操。
1. 先把 MCP、SSE、SpringAI 三者关系理清楚
1.1 MCP 到底解决什么问题
MCP 全称 Model Context Protocol,本质上是给 AI 应用定义的一套统一接口协议,核心场景是让大模型能够以标准化的方式调用外部工具和数据源。你可以把它理解成 AI 世界的 USB-C 接口:以前不同设备各用各的充电口,现在统一成同一个标准,设备之间可以互相兼容。
落到实际业务里,MCP 有两个角色:MCP Server 负责暴露工具能力,MCP Client 负责连接并调用这些能力。AI 应用通常作为 Client 端,业务系统作为 Server 端。举个例子,你有一个订单查询接口/order/query,用 MCP 包裹之后,大模型在回答“帮我查一下最近一笔订单”时,就可以通过工具调用来触发这个接口,再把返回结果组织成自然语言回复给用户。
没有 MCP 能不能做 Agent?当然可以,自己写 function calling 的 JSON Schema、自己维护工具注册表、自己写调度逻辑,一样能跑通。但问题是生态不通用,这个项目里写的工具调用,换个 Agent 框架基本得重写。MCP 的价值在于协议标准化:社区里已经有大量现成的 MCP Server,比如 Playwright MCP 可以做浏览器自动化,Altium Designer 也能通过 MCP 暴露 AI 接口,连 Kali 和 x32dbg 这类偏安全、逆向的工具都有对应的 MCP 插件生态了。你用 Java 也好、Python 也好,只要实现同一个协议,就能直接复用这套生态。
1.2 SSE 在 MCP 里不只是“单向推送”
很多人第一次看到 SSE 会误以为它就是服务端往客户端推消息的单向通道,这是理解上最大的误区。标准 SSE 确实是单向的,但 MCP 的 SSE 传输设计,实际上构建了一条“下行长连接 + 上行回调端点”的双向通道。
具体流程是这样的:MCP Client 先向服务端发送一个 GET 请求到 SSE 端点,服务端保持这个连接不关闭,返回text/event-stream格式的流式数据。连接建立之后,服务端会通过这个流先发送一个endpoint事件,把消息接收地址告诉客户端。客户端拿到这个地址之后,后续的 JSON-RPC 请求全部通过 POST 发送到这个地址,服务端的处理结果、通知消息则继续走之前那条 SSE 流返回。
所以你不要把 MCP over SSE 理解成“服务端单方面推送”,它实际上是两段配合:一段是长连接的流式下行通道,一段是常规 POST 的上行请求通道。这个机制搞清楚之后,后面调试的时候你就能快速判断问题到底出在哪一段。
1.3 SpringAI 在这里扮演什么角色
SpringAI 是 Spring 官方推出的 AI 应用集成框架,类比 Spring Boot 对 Web 开发的意义,它把接入大模型、构建提示词、管理工具调用这些琐事做了一层封装。SpringAI 内部已经内置了 MCP 的 Client 和 Server 支持,所以你在 Spring Boot 项目里写 MCP 不需要自己处理协议细节,只要引入依赖、配置连接信息、注册工具类就行。
实际项目里,这套组合的典型架构是这样的:SpringAI 项目的后端同时部署了 MCP Client 和 MCP Server,Client 负责和大模型对话并解析工具调用意图,Server 负责把业务能力暴露成工具。两者通过 SSE 通信,大模型要调工具时,Client 把请求转发给 Server,Server 执行完毕后把结果沿 SSE 流推回给 Client,Client 再把结果送回给大模型生成最终回复。
2. 技术选型与前置准备
2.1 版本与依赖选择
SSE 方式对技术栈的约束很小,核心要求是 JDK 17+、Spring Boot 3.x。SpringAI 的 MCP 支持分为 WebMvc 和 WebFlux 两条路线,如果你项目里用的是 Spring MVC,就选 WebMvc 版本的依赖;如果你原本就是 WebFlux 响应式架构,那选 WebFlux 版本更顺。我个人推荐大多数业务系统用 WebMvc,理由很简单:Spring MVC 的生态兼容性最好,项目里已有的 Filter、拦截器、AOP 都能直接生效,而 WebFlux 的响应式模型和传统 Servlet 模型混用时容易出问题。
Maven 依赖结构大致需要三块:SpringAI 基础依赖、MCP Server 或 MCP Client 依赖、以及你实际使用的大模型厂商 SDK 依赖(比如 OpenAI 兼容接口的都行)。具体依赖坐标随 SpringAI 版本迭代变化较快,动手前先去 Maven 中央仓库看一眼当前稳定版的 artifactId,避免照抄旧博客导致版本对不上。
提示:这里要强调一句,SSE 本身是基于 HTTP 的通信协议,不涉及 WebSocket 服务,所以不需要额外引入 Netty 或者配置 WebSocket 端点。这是它比 WebSocket 方式轻量的主要原因。
2.2 SSE 和 WebSocket 在 MCP 场景下的取舍
MCP 官方定义的传输方式有两种:stdio 和 HTTP(包含 SSE)。stdio 方式适合本地进程间通信,比如你写一个命令行工具作为 MCP Server,用子进程方式启动,通过标准输入输出交换消息。SSE 方式则适合跨网络部署,Client 和 Server 可以是两台机器上的独立服务。
那为什么不直接用 WebSocket?WebSocket 双向通信能力确实比 SSE 强,但带来的复杂度也更高:需要额外的握手协商、心跳重连机制、连接状态管理。MCP 的 SSE 设计用了一个很取巧的办法,用一条长连接加上一个回调端点,既实现了双向效果,又把复杂度压到了 HTTP 的范畴内。对于大多数 Java 后端团队来说,这条路线学习成本和维护成本都最低。
浏览器前端场景还有一个点值得知道:如果你想把 MCP 的能力暴露给网页,很多方案是用 SharedWorker 来统一管理 SSE 连接。原因是页面刷新或者组件卸载会导致 EventSource 连接中断,而 SharedWorker 的生命周期独立于页面,可以实现连接复用和断线重连,避免每个标签页都建立一条 SSE 长连接。这个思路在实现轻量级“前端调 AI 工具”的场景里非常实用。
2.3 现有 REST 接口怎么快速转成 MCP 工具
热词里有一个“java rest接口快速转为mcp接口”,这个需求在实际项目里极其常见。团队里已经积累了大量稳定的 REST API,如果为了接 AI 重新实现一遍业务逻辑,纯属浪费。最合理的做法是写一个薄的适配层:MCP 工具方法内部直接把请求参数透传给现有 REST 接口,拿到结果后原样返回。工具方法的入参出参定义尽量保持和 REST 接口一致,这样映射成本最低。
不过这里有一个隐藏工作:工具说明(description)的质量会直接影响大模型的调用成功率。REST 接口本身不需要描述,但 MCP 工具必须写好描述,因为大模型是依靠描述来决定“这个场景该调哪个工具”的。描述写得太笼统,模型可能在你需要查订单的时候调了查库存的工具。
3. 服务端实操:从零搭建 MCP Server
3.1 创建项目并引入依赖
我先说服务端。在 Spring Boot 项目里创建一个 MCP Server,本质上就是暴露一个 SSE 端点。我手头的项目基于 Spring Boot 3.4.x 和 SpringAI 1.0.0 版本,pom 里的核心依赖长这样:
<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-mcp-server</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-mcp-server-webmvc</artifactId> </dependency>引入之后再配置文件里声明端点信息:
spring: ai: mcp: server: name: order-system-mcp version: 1.0.0 transport: webmvc-sse sse-endpoint: /mcp/sse这里sse-endpoint就是前面说的 GET 建连地址,客户端需要拿着这个地址来发起 SSE 连接。要注意的是,SpringAI 的 MCP Server 一般会自动注册一个用于 WebMvc 的端点,如果你配了 Spring Security,记得把这条路径加入白名单,否则握手阶段就会被拦截。
3.2 注册一个业务工具
依赖和配置就绪之后,注册业务工具的方式非常直观,用注解标记一个方法即可。我在项目里做了一个订单查询工具,接口逻辑比较简单,就是从数据库取数据然后返回 JSON:
@Component public class OrderMcpTools { @Tool(description = "根据订单编号查询订单详情,订单编号一般是数字字符串") public String queryOrderDetail(String orderId) { OrderEntity order = orderService.getById(orderId); if (order == null) { return "订单不存在"; } return order.toJsonString(); } }这个方法会被自动暴露为 MCP 工具,大模型那边的认知里就多了一个名为queryOrderDetail的函数,入参是orderId,返回 JSON 字符串。@Tool注解在这里做的核心事情就是生成工具描述 Schema,包含参数类型、含义、是否必填等元信息,这些信息会通过 MCP 协议发给 AI 客户端。
这里有个很容易忽略的细节:方法的返回类型最好是字符串,或者能明确序列化成 JSON 的类型。不要返回一个复杂的 Java 对象让框架去猜序列化方式,AI 模型侧拿到的是一个文本化的结果,格式越规整,后续解析的成功率越高。我习惯在工具方法内部手动序列化成 JSON 字符串再返回,这样对输出格式有完全的控制权。
3.3 工具描述怎么写才容易被 AI 正确调用
工具描述是很多人不重视但实际影响巨大的环节。大模型本身不具备“看代码”的能力,它判断该不该使用某个工具,完全依赖你提供的描述文本。描述里应该写清楚三件事:这个工具是干什么的、什么时候应该使用它、使用它需要提供什么关键信息。
举个反例,如果只写“订单查询工具”,模型面对“帮我查一下上周的退款订单”这种诉求时,可能直接放弃工具调用,因为它不确定这个工具是否支持按时间筛选。正确写法应该是“根据订单编号精确查询订单详情,适用于用户询问具体订单的状态、金额、物流信息等场景,不支持批量查询和按条件筛选”。这样描述出来,模型就知道什么时候该用、什么时候不该用,能有效减少误调用。
多个工具之间的描述风格要保持一致,别一个用口语一个用官方文档腔,模型的调用稳定性会更好。另外,系统提示词里可以补充一段“你在回答订单问题时优先使用工具获取实时数据,不要基于训练数据猜测”,这和工具描述配合使用,能够显著提升 Agent 的准确度。
3.4 服务端多工具与授权转发
实际系统一般不会只有一个工具,订单系统大概率还有退款查询、库存查询、用户信息查询等能力。把这些方法都放在一个@Component类里是没问题的,也可以按业务域拆成多个类,SpringAI 会自动扫描并注册所有带@Tool注解的方法。
这时候 Agent 就有了选择空间,模型会依据用户的问题,从工具列表里挑一个或连续调用多个。服务端如果有权限控制需求,可以在工具方法内部做校验,比如从当前上下文获取调用者身份,没有权限时直接返回“无权限访问”的提示文本。因为我这套 MCP Server 是在 Spring Boot 里的,原有的一些 AOP 切面、鉴权拦截器也能套用在工具方法上,这也是直接在 Spring 应用里做 MCP Server 的一大好处。
4. 客户端配置与联调过程
4.1 MCP Client 的依赖和配置
服务端把 SSD 端点暴露出来之后,接下来就是客户端逻辑。大部分情况下,客户端也是一个 Spring Boot 应用,通过 SpringAI 内置的 MCP Client 连接服务端。依赖大致如下:
<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-mcp-client</artifactId> </dependency>配置里指定服务端的 SSE 地址:
spring: ai: mcp: client: name: ai-agent transport: webmvc-sse sse-url: http://localhost:8080/mcp/sseSpringAI 会自动在应用启动时尝试连接这个地址,完成 SSE 握手并拉取服务端的工具列表。启动日志里如果能看到类似MCP tools registered的输出,说明客户端已经成功拿到了服务端暴露的工具。
4.2 交互会话的建立与调用时序
理解整条调用链路的时序,对排查问题特别有帮助。我拿上面那个订单查询工具举例,一次完整的调用过程是这样的:
- 用户向 AI 应用提问:“帮我查一下订单 20250315001 的状态。”
- AI 应用把问题和已注册的工具列表一起发给大模型。
- 大模型判断需要调用
queryOrderDetail工具,返回一个工具调用指令。 - SpringAI 客户端收到指令后,将工具调用请求以 JSON-RPC 格式 POST 到 SSE 握手时拿到的端点上。
- MCP Server 执行
queryOrderDetail方法,拿到结果后沿 SSE 连接推送给客户端。 - 客户端把工具返回结果拼进对话上下文,再次请求大模型生成最终回答。
- 大模型基于工具返回的订单状态,组织自然语言回复用户。
时序本身不复杂,但有几个点值得注意。第一,第 3 步的大模型调用本质上就是 function calling,SpringAI 只是把传统 function calling 的协议转换成了 MCP 协议;第二,工具返回结果的快慢取决于服务端业务执行效率,SSE 连接需要保持足够长的超时时间,不要让网关或者负载均衡器提前断开长连接;第三,如果 Agent 需要连续调用多个工具,比如先查订单再查库存,那就要在对话上下文中维护好中间状态,SpringAI 会帮我们做一部分,但代理解析复杂指令时依然可能出现工具调用顺序错误的情况。
4.3 前端场景下的 SSE 转发方案
如果你打算把 MCP 能力暴露给浏览器前端,直接用 EventSource 连接也是可以的,但实际项目中我建议在前端加一层 SharedWorker 管理。原因前面提过,浏览器页面的生命周期不可控,用户刷新页面就断连,多个标签页会建立多条重复连接,服务端资源浪费严重。
SharedWorker 方案的主要逻辑是:创建一个共享的 Worker 进程,在里面维护唯一的 SSE 连接,页面通过navigator.serviceWorker或SharedWorker与它通信。每次页面需要调用 AI 工具时,向 Worker 发消息,Worker 统一走 SSE 连接完成请求。这样即使页面关闭,Worker 里的连接也能短暂存活,配合自动重连机制可以大幅提升体验。
这个方案需要注意的坑是:SharedWorker 的兼容性在不同浏览器内核上还是有差异的,而且调试起来比普通页面代码麻烦,需要打开chrome://inspect里的 Worker 面板看日志。如果团队前端实力一般,退而求其次的办法是用页面级 EventSource 加心跳重连,实现虽简单但够用。
4.4 调用结果流式回传的设计
MCP 工具本身返回的是一个完整结果,但如果你把 AI 对话的最终回答也做成打字机效果往界面上推,就需要额外处理流式输出。架构上的做法是:AI 后端通过 SSE 往前端推大模型的增量 token,而 MCP 工具调用环节的耗时被包含在这个流式过程里。
实际体验上,如果工具执行时间较长,前端会出现一段“卡住不输出”的状态。好的产品设计会在 Agent 内部广播“正在调用订单查询工具”这类状态事件,再通过 SSE 提前推到前端展示,用户至少知道系统在工作。SpringAI 里没有内置这种状态广播机制,需要自己在工具调用前后手动埋点,通过自定义的 SSE emiiter 推给前端。这个细节很影响体验,建议做 Agent 产品时一定考虑进去。
5. 常见问题与排查技巧实录
5.1 SSE 连接建立不起来
这个是最常见的现象,客户端日志报连接失败或者直接超时。排查时先确认服务端端点是否可访问,用 curl 直接访问 SSE 地址看有没有响应:
curl -N http://localhost:8080/mcp/sse如果服务端正常,这里会看到text/event-stream的响应头和endpoint事件。如果 curl 被防火墙挡了或者返回 404,那就要检查 Spring Boot 的 context-path 配置、网关转发规则、以及 MCP 端点路径是否拼写一致。另一个高频原因是 Spring Security 拦截了未认证的 SSE 请求,SSE 连接无法携带常规的认证头时,要在 Security 配置里放行 MCP 相关路径。
5.2 长连接被网关或负载均衡器断开
SSE 连接长时间没有数据流动时,Nginx 或云服务商的负载均衡器可能主动断开空闲连接。客户端表现是服务端日志里连接正常,但客户端一直收不到新数据。
对应解决办法有三层:第一层是在 Nginx 配置里调大proxy_read_timeout,建议至少 300 秒;第二层是在应用层增加心跳机制,定期在 SSE 流里发送注释行(比如: keep-alive),让连接保持活跃;第三层是在客户端实现断线重连,根据内置事件reconnect的间隔自动重连。心跳机制这个点尤其重要,生产环境必须做,否则长连接在凌晨无人访问时大概率被静默回收。
5.3 WebFlux 和 WebMvc 依赖冲突
这是 SpringAI 相关项目里最容易踩的依赖炸弹。MCP Server 和 MCP Client 如果传输方式不一致,比如一个用 WebMvc-SSE、一个用 WebFlux-SSE,两个依赖同时存在时,Spring Boot 会报 Starter 冲突,提示类似spring-boot-starter-web与spring-boot-starter-webflux不能共存。
解决思路是保持两端的传输方式一致,都走 WebMvc 版本。如果服务端已经是用 WebFlux 写的老项目,那客户端也必须引入对应的 WebFlux 依赖,不能混用。我在项目里最开始就吃了这个亏,排查了半天才发现是两端传输实现不一致导致的启动失败。
5.4 工具方法里依赖注入失效
MCP 工具类如果是普通类,不交给 Spring 管理,那@AutoWired的依赖就没法注入,运行时报空指针。解决方案很简单:工具类上加@Component注解,注册成为 Spring Bean,工具方法里的业务对象依赖才能正常注入。
另外还有一个细节,工具方法执行时如果涉及数据库事务,注意自调用的方法不会生效 Spring 的事务代理。把事务边界控制在工具类之外的 Service 层,工具方法只做转发,这样切面逻辑才能生效。
5.5 模型不调用工具或工具参数传错
大模型回答了一堆内容但完全没调工具,大概率是工具描述不清晰,或者工具参数定义太复杂。模型对于“陌生工具”的判断很保守,描述写得不明确它宁可不用。优化方向是精简参数数量,能用一个字符串参数解决的不要拆成五个字段。
参数传错的情况更容易出现在枚举值或者日期格式上。MCP 的工具参数是自由文本,模型有概率编造一个不存在的枚举值。我的做法是在工具方法内部做容错解析,比如订单状态的枚举值接受PENDING、pending、待处理多种写法,统一转换后再查询。不把输入校验的全部期望寄托在模型身上,生产代码要自己兜底。
5.6 问题排查速查表
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
| 客户端连不上服务端 | SSE 路径错、Security 拦截、网关未转发 | 用 curl 验证端点,检查白名单与转发规则 |
| 连接建立后无数据 | 网关断开空闲连接 | 调大超时时间,增加心跳注释行 |
| 启动报 WebMvc/WebFlux 冲突 | 两端传输实现不一致 | 统一 WebMvc 或 WebFlux 依赖 |
| 工具调用空指针 | 工具类未交给 Spring 管理 | 工具类加 @Component |
| 模型不调用工具 | 描述不清、参数过多 | 重写工具描述,精简参数 |
| 工具返回值解析失败 | 返回了非 JSON 格式文本 | 工具内手动序列化为标准 JSON |
6. 一点实操经验
这段时间做下来,我最大的体会是:MCP 的价值不在于协议本身有多高明,而在于它把 AI 工具调用从一个“项目内约定”上升到了“生态标准”。你不需要再去纠结自己设计的工具调用格式和大模型怎么对齐,只要实现了 MCP,SpringAI、Python 生态、低代码平台、各种中间件就能直接对话。对 Java 团队来说,SpringAI 提供的这套 SSE 集成已经是目前成本最低的接入方式。
另一个经验是:如果你手头的系统已经有不少成熟的 REST 接口,不要急着从零实现 MCP 工具,先把这些接口薄薄地封装一层,做成 MCP 工具,让 AI 能直接调度。后续用到新的能力,再逐步扩展。开发调试时多利用客户端 startup 日志里打印的注册工具列表,确认描述和参数 Schema 是否符合预期;也可以写一个简单的测试页面,用 EventSource 直连服务端观察事件流,比在 AI 对话里绕一圈直观得多。
最后分享一个小技巧:在 SpringBoot 的配置里,可以针对 MCP 客户端单独开一份 profile,指向本地的开发环境,日常调试时直接用本地服务端联调,避免反复部署远程环境的等待。这套链路本身不复杂,把连接基础和工具描述做扎实,后续接再多的业务能力都会很省心。