☰
SpringAI MCP Server 实战:从零搭建AI工具调用服务
2026/10/9 3:59:47 网站建设 项目流程

做 AI 应用开发这一年,我最有体感的转折点是:把 SpringAI 的 MCP Server 接进项目之后,AI 服务的开发体验第一次有了 Spring Boot 那种"写业务就行"的踏实感。过去一段时间,工具接口写了一大堆,还得想办法让模型知道什么时候调、参数怎么传、结果怎么解析——每个模型一套适配逻辑,维护成本高得怀疑人生。MCP 协议把这块统一以后,加上 SpringAI 的封装,工具就是一个带注解的方法,连接、协议、发现机制全部交给框架。

这篇文章适合两类读者:一类是 Java/Spring 背景、首次遇到"让大模型调内部工具"需求的开发者;另一类是已经在用 SpringAI、但还在手动拼 JSON Schema、被工具调用链路搞到头大的朋友。我会从 MCP 到底解决什么问题讲起,拆 SpringAI 的封装思路,再带你把一个能查订单的 MCP Server 从零跑通,最后把我在生产环境踩过的坑一次列清楚。原理和实操都讲,只给结论不给原因的文章最害人。

1. MCP 到底补上了什么:先弄清楚它解决的真实痛点

1.1 大模型天生没有"手"和"眼"

很多人第一次接触 MCP 时,容易把它当成又一个"AI 框架"来学。我的建议是反过来:先看问题,再看协议。

大模型本身再强,也不会有你们公司的订单数据,更不会知道你们的库存接口该怎么调。用户问一句"订单 TX20250101001 金额多少",模型要么瞎编一个数字,要么只能告诉你"我查不了"。要让模型真正干活,只有两条路:一条是把数据塞进上下文,让它在知识层面回答,这就是大家熟悉的 RAG;另一条是给模型提供可调用的工具,让它在动作层面把事情办了。RAG 适合"查资料",工具调用适合"做业务"。MCP Server 解决的就是第二条路里最麻烦的那部分——工具怎么被模型发现、怎么被调用、结果怎么回来。

在没有 MCP 之前,这个流程每家模型各搞一套。今天接 A 模型,要按它的格式写函数定义;明天换 B 模型,又得重写一层适配。工具多了以后,光是维护"哪个模型对应哪份工具描述"就够呛。我见过不少团队最后干脆把工具描述全塞进系统提示词里,换来的是上下文爆炸、模型选择越来越不准。

1.2 MCP 的三个角色与一条调用链

MCP(Model Context Protocol)把系统分成三个角色:

  • Host:用户直接面对的 AI 应用,比如聊天界面、IDE 插件。
  • Client:Host 内部负责和 MCP Server 通信的连接器。
  • Server:提供工具的一方,也就是我们今天要搭建的东西。

一次完整调用对应这个链条:ChatClient 向某个 MCP Server 发起会话并拿到工具清单(tools/list),把工具清单交给大模型做决策,模型如果决定要查订单,会返回一个"调用工具 query_order,参数是订单号"的指令,ChatClient 再把这个指令转成 MCP 的 tools/call 请求发给 Server,Server 执行真正的方法,把结果返回,模型基于结果给出最终回答。

这个设计的好处是"工具提供方"和"模型应用方"彻底解耦。工具写成 MCP Server 之后,OpenAI 可以用,Claude 可以用,你们公司自研的模型也可以用,不用给每家单独做适配。就像把充电口统一成 USB-C,设备不用再为每种硬件配一根专属线。

1.3 为什么 Java 团队尤其该关注 SpringAI

MCP 官方当然提供了多语言 SDK,直接用 SDK 写 Server 完全可行,我也这么干过。但问题在于:连接管理、生命周期、线程模型、序列化配置,这些杂活全得自己写,而且很容易和项目里已有的 Spring 体系"各玩各的"。SpringAI 的价值,是让 MCP Server 变成一个普通的 Spring Boot 应用——你写好带注解的 Java 方法,剩下的事交给自动配置和容器。

对 Java 团队来说,这意味着两个非常实际的好处。第一,不需要为了 AI 工具引入新的开发语言和范式,项目里现成的 Service、Repository 方法,只要符合工具方法的要求,加个注解就能被 AI 调用。第二,Spring 生态的既有能力——鉴权、事务、监控、配置中心——都可以直接用,不用再造一套轮子。这也是我觉得"像 Spring Boot 一样简单"这个说法并不过分的原因:开发体验是真的顺着 Spring 的老思路走的。

环节传统做法SpringAI MCP Server
工具定义手写 JSON Schema,漏一个字段就废@Tool 注解自动生成
接口暴露每个工具一个接口加一堆文档框架自动暴露 MCP 端点
模型适配每换一个模型重写一套客户端统一走 MCP 协议
安全与监控自己从零搭复用 Spring 生态能力

2. SpringAI 把协议复杂度藏哪了:从 Starter 到 @Tool 的层层封装

2.1 Starter 与自动配置:把"装配"交还给框架

Spring Boot 之所以开发效率高,核心就是 Starter 加自动配置:你引入一个依赖,框架在启动时悄悄把一堆 Bean、一堆默认行为装配好。SpringAI 沿用了同一套思路。

引入 spring-ai-starter-mcp-server 之后,应用启动时框架会做三件事:扫描 Spring 容器里所有带 @Tool 注解的 public 方法;把方法签名转成 MCP 需要的 JSON Schema 工具描述;按配置启动对应的传输通道并注册协议端点。你写的业务代码和协议代码之间,被框架隔开了,这其实就是 Spring Boot 一直以来的"约定优于配置"。

pom 里大概长这样:

<dependencyManagement> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-bom</artifactId> <version>1.0.0</version> <type>pom</type> <scope>import</scope> </dependency> </dependencyManagement> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-mcp-server</artifactId> </dependency>

我建议依赖版本用 BOM 统一管理,不要自己写死一个数字散在各处。Spring AI 的版本迭代比较快,具体版本号要以官方仓库为准,但用 BOM 管理版本这个习惯不会变。

注意:Spring AI 的版本迭代很快,示例中的依赖属性和注解包名可能随版本调整。动手前先确认你锁定的版本,再看对应版本的官方文档,别盲抄。

2.2 两种主要传输方式:STDIO 与 HTTP+SSE 怎么选

MCP Server 不是只能跑成 Web 服务,它有两种主流传输方式,很多人第一次就在这里选错。

STDIO 模式:Server 以子进程的方式被客户端拉起,双方通过标准输入输出通信。优点是启动快、不需要网络和鉴权,适合本地工具,比如把公司内部命令封装给本地 AI 编辑器用。缺点是 Server 很难被远程共享,生命周期跟随客户端进程。

HTTP+SSE 模式:Server 是一个独立的 Web 服务,客户端通过 SSE 建立事件通道,通过 POST 发送请求。适合把工具部署成远程服务,多个 AI 应用共享一套工具,也能配合网关做鉴权和负载均衡。缺点是需要处理网络、超时、会话保持这些常规 Web 问题。

我的选择逻辑很简单:工具是给本机开发工具用的,用 STDIO;工具是要给团队多个 AI 前端共享的,用 HTTP+SSE。Spring Boot 应用天然有 Web 容器,绝大多数后端场景直接选 SSE 就行。MCP 规范本身还在演进,后续还会有 WebSocket、Streamable HTTP 等新传输方式,但"本地选进程、远程选服务"这个判断标准可以一直用。

spring: ai: mcp: server: transport: SSE name: order-mcp-server version: 1.0.0

2.3 @Tool 注解:方法签名就是工具协议

SpringAI 暴露工具的核心 API 就是 @Tool 注解。一个最简单的工具长这样:

@Service public class OrderToolService { @Tool(name = "query_order", description = "根据订单号查询订单金额、状态、支付方式、下单时间,用于回答订单相关查询问题") public OrderVO queryOrder(@ToolParam(description = "订单号,以 TX 开头,例如 TX20250101001") String orderNo) { // 实际业务逻辑 } }

三个细节值得注意。第一,name 是给模型看的唯一标识,要稳定、简短,最好全小写下划线风格,不要今天 tools 明天 tool 地改,模型在会话里容易认不出。第二,description 是模型决定"什么时候调用、要不要调用"的主要依据,写清楚能力边界和典型场景,比写内部实现细节有用得多。第三,@ToolParam 的 description 要包含格式示例和取值范围,模型生成参数时才能猜得准。

框架会把 Java 方法签名自动转换成 JSON Schema:基础类型、String、数值、布尔、枚举、简单的 record/DTO 都支持得很好。方法是 public、类被 Spring 管理,这是两个硬性要求。

2.4 与官方 MCP SDK 的分工

底层上,SpringAI 并不是重新实现了一遍 MCP,而是把官方 MCP Java SDK 包了一层 Spring 化的壳。连接管理、消息循环、序列化这些脏活还是 SDK 在干,SpringAI 主要负责生命周期托管、注解扫描、和 Spring 容器里其他 Bean 的协作。这也意味着,真遇到协议层面的特殊需求,你仍然可以直接拿到底层 SDK 对象去扩展,不至于被框架封死。理解了这层分工,排查问题时就不会在 Spring 和协议之间来回猜。

3. 实操复现:搭一个能查订单的 MCP Server,并让客户端连上它

这一节我们做一个完整可跑的东西:一个暴露订单查询工具的 MCP Server,再用 SpringAI 客户端连上去,让大模型通过自然语言完成一次工具调用。

3.1 工程骨架与依赖清单

建议用 Spring Boot 3.3.x 以上的版本,Java 17 起步。SpringAI 目前对 Spring Boot 3.x 支持得最稳,如果你还在维护 Spring Boot 2.x 的老项目,想直接引入会踩版本坑,这个后面单独说。

pom 依赖如下(版本统一交给 spring-ai-bom 管理):

<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.3.5</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-mcp-server</artifactId> </dependency> </dependencies>

spring-boot-starter-web 是为了让 SSE 传输有 Web 容器可依。如果选 STDIO 模式,可以把 Web starter 去掉,但大多数后端场景用 SSE,就保留着。

3.2 写几个能真正落地的工具方法

先定义一个简单的订单对象和内存仓储,方便演示:

public record OrderDO(String orderNo, BigDecimal amount, String status, LocalDateTime createdAt) { } @Repository public class OrderRepository { private final Map<String, OrderDO> store = new ConcurrentHashMap<>(); public OrderRepository() { store.put("TX20250101001", new OrderDO("TX20250101001", new BigDecimal("128.50"), "PAID", LocalDateTime.now().minusDays(1))); store.put("TX20250101002", new OrderDO("TX20250101002", new BigDecimal("399.00"), "SHIPPED", LocalDateTime.now().minusHours(3))); } public Optional<OrderDO> findByOrderNo(String orderNo) { return Optional.ofNullable(store.get(orderNo)); } }

然后写工具方法:

@Service public class OrderToolService { private final OrderRepository orderRepository; public OrderToolService(OrderRepository orderRepository) { this.orderRepository = orderRepository; } @Tool(name = "query_order", description = "根据订单号查询订单详情,返回是否成功、提示信息、订单号、金额、状态、创建时间。仅用于查询,不修改订单") public OrderVO queryOrder(@ToolParam(description = "订单号,以 TX 开头,例如 TX20250101001") String orderNo) { return orderRepository.findByOrderNo(orderNo) .map(o -> new OrderVO(true, "查询成功", o.orderNo(), o.amount(), o.status(), o.createdAt())) .orElse(new OrderVO(false, "未找到订单 " + orderNo + ",请确认订单号是否正确", orderNo, null, null, null)); } }

这里我使用了一个 OrderVO 来包装返回结果,success 和 message 字段是为了让模型能基于明确语义回答用户,而不是自己编。这个设计在后面第 4 节会详细讲。

启动应用后,在启动日志里会看到类似 "Registered 1 tool" 的信息,接着就能看到 MCP 端点路径,一般形如 /mcp/sse 和 /mcp/message(具体路径以你当前版本的日志为准)。至此,一个 MCP Server 已经能跑了。

3.3 第一步验证:用 MCP Inspector 检查工具

开发阶段我几乎不用客户端代码来验证 Server,而是直接用 MCP Inspector,它是 MCP 官方的调试工具:

npx @modelcontextprotocol/inspector

打开界面,把 SSE 地址填成 http://localhost:18080/mcp/sse(按你实际端口和路径改),建立连接后能看到工具列表、每个工具的 JSON Schema,甚至可以直接发起调用。这一步能非常直观地确认:工具列表有没有、参数描述对不对、返回值能不能正确序列化。我建议任何 MCP Server 写完都先过一遍 Inspector,比写测试代码快太多。

3.4 第二步验证:用 SpringAI 客户端串起完整链路

Server 没问题后,再建一个 Spring Boot 客户端工程,引入 MCP 客户端 starter 和模型 starter:

<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-mcp-client</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-model-openai</artifactId> </dependency>

客户端配置里,把连接指向我们的 MCP Server(不同版本属性名可能有微调,以文档为准):

spring: ai: mcp: client: connections: order-server: type: SSE url: http://localhost:18080/mcp/sse

然后写一个最简单的测试:

@SpringBootTest class McpClientTest { @Autowired private ChatClient chatClient; @Test void queryOrderThroughMcp() { String answer = chatClient.prompt("订单 TX20250101001 金额是多少?").call().content(); System.out.println(answer); } }

跑起来你会发现,整个过程中你既没有在提示词里写工具清单,也没有手动拼接任何 JSON Schema。客户端通过 MCP 拿到工具后,自动把它们注入模型上下文;模型决定调用 query_order 并生成参数;客户端通过 MCP 把调用转发给 Server;结果经模型组织成一句话返回。这正是"像 Spring Boot 一样简单"最直观的体现:复杂链路在框架内部跑,往外露的只有业务代码。

4. 调用链路拆解:从"用户提问"到"Java 方法执行"之间发生了什么

4.1 一次工具调用的四个阶段

接完上面这个 Demo,值得把链路再拆细一点,否则出了问题你都不知道在哪一环断的。

第一阶段,工具发现。客户端与 MCP Server 建立会话时,会通过 tools/list 拉取全部工具的 JSON Schema。这些 Schema 就是模型眼里"你的系统能干什么"的完整描述。

第二阶段,模型决策。用户提问进入客户端后,客户端把对话历史和工具 Schema 一起交给模型。模型判断"这个问题需要查订单",于是返回一个结构化的工具调用指令,包含工具名和参数 JSON。这一步完全取决于模型的 Function Calling 能力,和 MCP 没有直接关系。

第三阶段,协议调用。客户端识别出这个工具来自 MCP Server,把模型返回的指令转换成 tools/call 请求,发给 Server。Server 反序列化参数,通过代理机制调用你写的 @Tool 方法。

第四阶段,结果回填。Java 方法返回的对象被序列化成 JSON,回到客户端,客户端把它作为新消息交给模型。模型参考结果组织最终回答。

日常排错时,先判断问题出在哪个阶段:工具列表空,多半是第一阶段;模型该调用的工具没调用,多半是第二阶段;调用报错或超时,多半是第三阶段;模型胡言乱语,多半是第四阶段结果语义不清导致。

4.2 模型凭什么选中你的工具:描述与 Schema 质量

模型的工具选择本质上是文本匹配,它在 JSON Schema 里看到的名字和描述,就是它决策的全部依据。糟糕的描述往往长这样:"处理订单"。模型看到这种描述,不知道什么时候调、参数填什么,只能瞎猜。好一点的描述会把边界也划清楚:"根据订单号查询订单金额、状态、支付方式、下单时间,仅用于查询,不用于创建或修改订单。订单号以 TX 开头,例如 TX20250101001。若订单不存在则返回 success=false 并说明原因。"

另一个容易被忽略的问题是工具数量。暴露几十个工具,会让每次请求的 token 开销明显变大,模型选择错误率也跟着上升。不要把所有方法一股脑注解成 @Tool,业务上真正需要被 AI 调用的,往往就那么几个。SpringAI 也支持在客户端侧做工具裁剪,实践里"够用就好"比"越多越好"更可靠。

4.3 工具执行失败时,别让模型"编答案"

工具方法执行失败的常见写法是直接抛异常。模型拿到一段 Java 堆栈信息后,经常把异常文本当成业务结果,或者干脆编一个"系统错误",用户体验非常糟糕。

更推荐的做法是让返回值自带语义状态。我个人习惯用一个简单的包装结构:

public record OrderVO(boolean success, String message, String orderNo, BigDecimal amount, String status, LocalDateTime createdAt) { }

查询不到订单时,返回 success=false、message="未找到订单 TXxxx,请确认订单号是否正确"。模型看到这段文字,就知道该怎么回复用户,而不是自己脑补。这个原则我会写进团队的工具开发规范:工具返回值必须让模型"一眼看懂状态",异常、空值、边界情况都要有面向用户的解释性信息。

4.4 澄清一个概念:MCP 与 Function Calling 不是替代关系

这是新手最容易混的一点。MCP 和各家模型的 Function Calling 解决的是不同层的问题:Function Calling 是模型在推理层"决定调用哪一个工具并生成参数"的机制;MCP 是"工具如何被发现、如何被调用、结果如何传输"的协议层。SpringAI 的厉害之处在于把这两层整合好了——你写一个 @Tool 方法,框架既生成模型需要的 Schema,也通过 MCP 完成工具发现与调用传输。说人话就是:Function Calling 是模型脑子里的决策,MCP 是连接你和模型之间的通道,它们不是二选一的关系。

5. 生产环境避坑清单:我在真实项目里踩过的几个坑

这些坑不保证覆盖所有情况,但每一个都是我在实际项目里花过时间才定位到的。

5.1 启动即报错:别在 Spring Boot 2.x 上硬凑

有次在一个老系统旁边加 MCP 服务,同事图省事直接在 Spring Boot 2.7 工程里引入 starter,启动瞬间就是 NoSuchMethodError,翻日志才看到是类版本不兼容。SpringAI 对 Spring Boot 3.x 和 Java 17 是有要求的,这不是配一配就能绕过去的事。如果你的老系统短期无法升级,不要硬凑,建议单独起一个 Spring Boot 3 的 MCP Server 服务,通过你们现有的 RPC 或 HTTP 连到老系统,把工具能力包一层出来。这样 MCP 侧的运行环境干净,老系统也不被拖累。

5.2 一直连不上:SSE 端点路径与网关缓冲

现象:客户端配置了 http://localhost:18080 却一直握手失败。根因往往是路径问题——MCP 服务器端的端点通常带路径,类似 /mcp/sse 和 /mcp/message,不是根路径。不同版本路径可能不一样,最可靠的信息来源是启动日志。排查链路我一般是这么走的:先看 Server 日志确认端点路径,再核对客户端 URL,然后直接用 curl 请求 SSE 端点看有没有 text/event-stream 的响应。如果前面都正常但连上后事件流总是断,就要检查反向代理是不是开了响应缓冲——SSE 本质是长连接流式响应,网关一旦缓冲,事件就被卡住发不出去。

5.3 工具列表为空:方法可见性与 Bean 托管

有次我在 MCP Inspector 里看不到任何工具,代码里 @Tool 明明还在。最后发现那个方法被写成包私有,SpringAI 默认只扫描 public 方法,扫不到自然没有工具。另一个常见原因是类没有被 Spring 管理,比如忘了加 @Service 或者 @Component,方法写得再漂亮也没用。快速定位的办法是看启动日志里注册工具的数量:如果显示注册了 0 个,优先检查这两点。

5.4 参数转换翻车:LocalDate、BigDecimal、枚举的本土化处理

工具能正常注册,不代表调用时参数能正常解析。我遇到过 LocalDate 参数被模型传成带时间的字符串,也遇到过 BigDecimal 被转成科学计数法。这类问题根源在于 JSON Schema 生成和 Jackson 反序列化的规则不完全一致。解决办法有几种:一是尽量用基础类型做参数,日期用字符串并在 description 里写死格式,比如"yyyy-MM-dd";二是金额用分为单位的整数传递,避免浮点精度问题;三是枚举用字符串常量并在 description 里列出可选值。工具方法是给人机交互设计的,入参越简单,模型越不容易出错。

5.5 超时与重复执行:异步化加幂等设计

工具执行慢,客户端和模型那边容易超时;超时之后编排框架可能重试,导致工具被重复调用。如果是查询类工具,重复执行问题不大,但如果是扣款、发通知这类非幂等工具,就要格外小心。我的处理原则是:长时间任务不要同步阻塞在工具调用的请求线程里,改成"提交任务 + 查询状态"两步工具;任何可能产生副作用的工具,参数里带一个客户端生成的 requestId,服务端做去重。MCP 调用链比较长,链路里任何一环都可能重试,从设计上默认"会被重试"比事后补救省心得多。

5.6 模型手里多了把刀:权限校验与 Prompt 注入

工具暴露给模型后,最大的隐性风险是越权。模型本身没有"业务校验"的概念,它生成一个用户ID就调你的工具,你的工具就得像外部接口一样做权限验证,不能因为调用来自 AI 就跳过校验。敏感信息在返回给模型前要做脱敏和最小化处理,工具能查哪些数据、不能查哪些数据,宁可收窄,不可放任。Prompt 注入也要重视:用户输入可能诱导模型调用危险工具。工具描述写得越清楚"能做什么、不能做什么",执行侧再配合白名单校验,两层保障才踏实。最后,每个工具调用都要有完整的审计日志,谁调的、哪个会话调的、参数是什么、结果摘要是什么,一条都不能少。

5.7 版本漂移:三个月前的教程为什么现在跑不通

Spring AI 迭代真的很快,小版本之间属性名、注解包名都可能调整。我写这篇内容时用的示例,过几个月再看可能就有细节对不上。所以我的习惯是:用 spring-ai-bom 把版本锁死,不追求最新;升级版本时拿 MCP Inspector 把核心工具挨个回归一遍;教程和博客只当思路参考,具体配置以当前版本官方文档和源码为准。这不光是 SpringAI 的问题,整个 AI 工程领域现在还处于"基础层天天变"的阶段,锁定版本、小步升级、回归验证,是唯一稳妥的姿势。

附一个速查表,方便排查:

现象可能根因快速处理
启动类版本错误Boot 2.x / JDK8独立服务升级 Boot3
连不上 ServerSSE 路径不对或网关缓冲查日志路径、关缓冲
工具列表为空非 public 方法/类未托管检查注解与可见性
参数转换错误序列化规则不一致基础类型传参+写格式
超时/重复调用同步长任务/链路重试异步化+requestId 去重
越权操作工具未做权限校验行级权限+审计日志
教程跑不通版本漂移BOM 锁版本+回归验证

6. 我现在的落地姿势与几点个人体会

6.1 我目前的主力落地方式

经过这几个项目的折腾,我现在给团队搭 AI 工具链路基本固定成一套组合拳:需要暴露给模型的能力,单独用一个 Spring Boot 3 的 MCP Server 服务承载,不塞进老业务系统;客户端统一走 spring-ai-starter-mcp-client;工具返回值一律用带 success/message 的结构体;每个工具上线前,先用 MCP Inspector 人工点一遍,再看一版实际对话测试。这套流程虽然朴素,但我用下来比任何花哨架构都稳。

6.2 给刚上手的人两个小建议

一是工具描述写完,先找个不懂技术的同事读一遍,问他"这段描述你知道什么时候该用这个工具吗",如果他犹豫,说明描述还不够清楚,模型大概率也会犹豫。二是一次性暴露的工具不要贪多,先上两三个核心工具,跑通全链路,再逐步扩展。SpringAI 的 MCP Server 确实让我找回了"写普通 Spring Boot 接口"的那种踏实感,但它毕竟还在快速发展期,保持对协议本身的理解,比背熟任何一个 API 都要重要。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询