Spring AI 2.0 MCP Server 实战:把 Java 服务改造成 AI 工具
上一篇文章里,我们讲了怎么用 Spring AI 2.0 的 MCP Client 去"消费"外部工具——AI 应用连上别人的 MCP Server 就能调用几百个现成工具。但有个问题没解决:你自己的订单服务、库存服务、售后系统,怎么变成 MCP Server 让别人(或别的 AI 应用)来调用?总不能每次有新工具需求,都让 AI 团队来你这边写适配代码吧。
这篇文章解决的就是这个事:用 Spring AI 2.0 提供的
spring-ai-starter-mcp-server,把手里的普通 Java 服务改造成一个标准 MCP Server。改造完,任何 MCP Client(不管是 Claude Desktop、Cursor 还是你自己的 Spring AI 应用)都能自动发现并调用你的工具。全文代码基于 Spring Boot 4.1.0 + Spring AI 2.0.0,可以直接照着跑。
一、这个问题到底是什么
把 Java 服务变成 MCP Server,核心目标只有一个:让服务的能力通过标准协议暴露出去,调用方不需要知道你内部是怎么实现的。
先想清楚一个场景。假设你们公司有个订单服务,里面有一个方法查订单详情、一个方法查物流轨迹。以前,AI 应用想用这两个能力,得写 @Tool 注解把方法包装起来,然后注册进 ChatClient。这听起来不麻烦,但有两个隐患:第一,AI 应用和订单服务被耦合在一起,订单服务改接口,AI 应用要跟着改;第二,如果 Claude Desktop、Cursor 这类外部 AI 工具也想用你的订单能力,你根本没地方给它注册 @Tool——那是 Spring AI 应用内部的东西。
MCP Server 就是来解决这个问题的。你的订单服务自己启动一个 MCP Server,把"查订单""查物流"声明成标准工具。任何 MCP Client 连上来,都能自动发现这两个工具、自动调用。 你不需要关心调用方是 Spring AI 还是别的什么,调用方也不需要关心你的服务是 Java 写的还是别的什么写的。
打个比方。以前的做法是"你把工具直接焊死在 AI 应用里"——像手机电池,焊死了,谁想换都难。MCP Server 的做法是"你把工具做成标准接口插在墙上"——像 USB 充电口,任何设备拿根线插上就能用。
对 Java 开发者来说,这件事在过去并不轻松:MCP 协议基于 JSON-RPC 2.0,要自己实现协议、处理传输层、管理会话……但现在 Spring AI 2.0 把这一切封装好了。你只需要做三件事:
- 引入
spring-ai-starter-mcp-server依赖 - 写一个普通的类,方法上加
@Tool注解 - 配置一下暴露方式(SSE 还是 STDIO)
完事。剩下的协议细节,Spring AI 全包了。
二、底层原理到底怎么回事
要理解 Spring AI 的 MCP Server 是怎么工作的,先得理解 MCP 协议里两个角色的关系。
MCP 协议把参与方分成两边:MCP Server(服务端,提供工具的人)和 MCP Client(客户端,使用工具的人)。通信基于 JSON-RPC 2.0,也就是双方通过交换 JSON 格式的请求和响应来协作。一次完整的调用流程分三步:
- 初始化(Initialize):Client 连上 Server,双方交换协议版本和能力信息。这一步相当于握手——“你是哪个版本的协议?你支持哪些能力?”
- 工具发现(Tools/List):Client 问 Server:“你有哪些工具?” Server 返回工具清单,每个工具带名字、描述、参数 schema(参数长什么样)。
- 工具调用(Tools/Call):Client 按 schema 传参调用某个工具,Server 执行后把结果返回。
注意,MCP 协议本身不规定传输层用什么。目前主流有两种:STDIO(通过标准输入输出通信,Server 和 Client 在同一个进程里,常用于本地工具)和 HTTP + SSE(Server 是一个独立 HTTP 服务,Client 通过网络访问,常用于远程服务)。这篇文章我们用 SSE 模式——订单服务部署成独立 HTTP 服务,这才是企业里最常见的形态。
Spring AI 2.0 在底层做了这些事:它用 @Tool 注解解析你的方法,自动生成符合 MCP 规范的工具定义(名字、描述、JSON Schema 参数);它内置了 MCP 协议的状态机,处理握手、工具列表、调用请求这些协议消息;它把工具调用的结果自动序列化成 JSON 返回给 Client。你写的业务代码,Spring AI 一行都不用改,原样暴露出去。
这里有个关键设计值得说一下:Spring AI 的 MCP Server 支持 WebMvc 和 WebFlux 两种 HTTP 栈。WebMvc 是传统的 Servlet 模型,同步阻塞,写起来简单;WebFlux 是响应式模型,异步非阻塞,适合高并发长连接场景。SSE(Server-Sent Events)是单向推送,正好配合 WebFlux 的流式能力。选哪个取决于你的服务架构——如果你的服务本来就是 WebMvc 的(绝大多数 Spring Boot 服务都是),直接用 WebMvc starter 就行,Spring AI 会自动把你的 MCP 接口挂到现有应用上。
再补一个概念:工具描述和参数 schema 的质量,直接决定 AI 能不能正确调用你的工具。MCP Server 返回的工具定义里,description 是给大模型看的,properties 是参数说明。模型读不懂你的 Java 代码,它只能读到这些文本。所以 description 写得越清楚,模型选对工具、传对参数的概率越高。这是后面实战部分会反复强调的点。
最后说下版本。Spring AI 2.0.0 是 2026 年 6 月发布的 GA 版本,对应的 Spring Boot 4.1.0。MCP 相关的 starter 有两个:spring-ai-starter-mcp-server(服务端)和 spring-ai-starter-mcp-client(客户端),当前最新 GA 都是 2.0.0。注意老版本(1.0.x)里 MCP Server 的 starter 叫 spring-ai-mcp-server-webmvc-spring-boot-starter,那个版本还停在 1.0.0-M6(里程碑版本),不要用。用 2.0.0 就对了。
三、实战:手把手写代码
3.1 项目骨架和依赖
新建一个 Spring Boot 项目,Java 21,pom.xml 如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.1.0</version>
<relativePath/>
</parent>
<groupId>com.baiyunge</groupId>
<artifactId>order-mcp-server</artifactId>
<version>1.0.0</version>
<name>order-mcp-server</name>
<description>订单服务 MCP Server 实战</description>
<properties>
<java.version>21</java.version>
<spring-ai.version>2.0.0</spring-ai.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- MCP Server 核心:自动把 @Tool 方法暴露成 MCP 工具 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
这段配置里两个关键点。第一,spring-ai-starter-mcp-server 没有写版本号,因为它由 spring-ai-bom(2.0.0)统一管理,这就是引入 BOM 的意义——所有 Spring AI 组件版本一致,不会出现 A 组件 2.0.0、B 组件 1.1.8 这种混乱。第二,这个 starter 默认带的是 WebMvc 版本(spring-ai-mcp-server-webmvc),如果你的服务用的是响应式栈,需要额外排除并引入 WebFlux 版本,后面 3.4 节会说。
3.2 写业务工具类:一个类就是一个工具包
先写一个简单的订单查询工具。这个类不需要继承任何东西,就是一个普通 Spring 组件:
package com.baiyunge.order;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
/**
* 订单查询工具。
* 类上的 @Component 让 Spring 管理它,
* 方法上的 @Tool 让 Spring AI 把它暴露成 MCP 工具。
*/
@Component
public class OrderTools {
/**
* 模拟的订单数据。真实项目里这里应该是注入 Service 查数据库。
*/
private static final Map<String, Map<String, Object>> ORDER_DB = Map.of(
"A1001", Map.of(
"orderId", "A1001",
"customer", "张三",
"amount", 299.0,
"status", "已发货",
"createdAt", "2026-08-01T10:30:00"
),
"A1002", Map.of(
"orderId", "A1002",
"customer", "李四",
"amount", 89.5,
"status", "待付款",
"createdAt", "2026-08-10T14:00:00"
)
);
/**
* 根据订单号查询订单详情。
*
* @param orderId 订单号,例如 A1001
* @return 订单详情
*/
@Tool(description = "根据订单号查询订单的详细信息,包括客户、金额、状态和创建时间")
public Map<String, Object> queryOrder(@ToolParam(description = "订单号,例如 A1001") String orderId) {
Map<String, Object> order = ORDER_DB.get(orderId);
if (order == null) {
return Map.of("error", "订单不存在: " + orderId);
}
return order;
}
/**
* 查询所有订单。
*
* @return 订单列表
*/
@Tool(description = "查询系统里全部订单,返回订单列表")
public List<Map<String, Object>> listOrders() {
return List.copyOf(ORDER_DB.values());
}
/**
* 更新订单状态,模拟发货、退款等操作。
*
* @param orderId 订单号
* @param newStatus 新状态,可选值:待付款、已付款、已发货、已完成、已取消
* @return 更新结果
*/
@Tool(description = "更新订单状态,用于发货、退款、取消订单等操作")
public Map<String, Object> updateOrderStatus(
@ToolParam(description = "订单号") String orderId,
@ToolParam(description = "新状态,可选值:待付款、已付款、已发货、已完成、已取消") String newStatus) {
Map<String, Object> order = ORDER_DB.get(orderId);
if (order == null) {
return Map.of("error", "订单不存在: " + orderId);
}
ORDER_DB.put(orderId, Map.of(
"orderId", orderId,
"customer", order.get("customer"),
"amount", order.get("amount"),
"status", newStatus,
"createdAt", order.get("createdAt"),
"updatedAt", LocalDateTime.now().toString()
));
return ORDER_DB.get(orderId);
}
}
这段代码在干什么:@Tool 注解告诉 Spring AI"这个方法要暴露成 MCP 工具",description 是这个工具给大模型看的说明,@ToolParam 的 description 是每个参数给大模型看的说明。方法返回值无所谓什么类型,Spring AI 会序列化成 JSON。注意 updateOrderStatus 这种有副作用的操作,description 里把状态的可选值写全了,模型才知道怎么传参——这是工具能被正确调用的关键。
3.3 启动类
package com.baiyunge.order;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class OrderMcpServerApplication {
public static void main(String[] args) {
SpringApplication.run(OrderMcpServerApplication.class, args);
}
}
启动类没有任何特殊之处,就是一个标准 Spring Boot 应用。启动后,Spring AI 会自动扫描带 @Tool 注解的 Bean,把 OrderTools 里的三个方法注册成 MCP 工具,并挂到 HTTP 接口上。
3.4 配置文件
application.yml:
server:
port: 8082
spring:
application:
name: order-mcp-server
ai:
mcp:
server:
# 暴露 MCP 接口的路径前缀
base-path: /mcp
配置就三行。server.port 设成 8082,避免和本地其他服务冲突。spring.ai.mcp.server.base-path 是 MCP 接口的挂载路径,默认是 /mcp,这里显式写出来方便你记住——待会儿验证时要访问 http://localhost:8082/mcp。
如果你用的是 WebFlux(响应式)项目,直接把 starter 换成 WebFlux 专用版即可(版本同样由 BOM 管理,不用写版本号):
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webflux</artifactId>
</dependency>
注意:Spring AI 2.0 提供了三个服务端 starter——spring-ai-starter-mcp-server(默认 WebMvc)、spring-ai-starter-mcp-server-webmvc、spring-ai-starter-mcp-server-webflux,当前最新 GA 都是 2.0.0,按你的 HTTP 栈选一个即可。绝大多数业务服务是 WebMvc,用默认 starter 就行,这段只是给响应式项目留个方案。
3.5 启动并验证:用 curl 模拟 MCP Client
mvn spring-boot:run 启动后,MCP 协议的握手需要两步(初始化 + 工具列表),curl 验证如下:
# 第一步:初始化握手,返回协议版本和服务能力
curl -s -X POST http://localhost:8082/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-06-18",
"capabilities": {},
"clientInfo": {"name": "curl-test", "version": "1.0"}
}
}'
初始化会返回一个 Mcp-Session-Id 头,第二步工具发现要带上它:
# 第二步:列出工具,这里会返回你写的三个 @Tool 方法
SESSION_ID=$(curl -s -D - -o /dev/null -X POST http://localhost:8082/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl-test","version":"1.0"}}}' \
| grep -i mcp-session-id | tr -d '\r' | awk '{print $2}')
curl -s -X POST http://localhost:8082/mcp \
-H "Content-Type: application/json" \
-H "Mcp-Session-Id: $SESSION_ID" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'
返回的 JSON 里 tools 数组应该有三个工具:queryOrder、listOrders、updateOrderStatus,每个都带 description 和参数 schema。看到这三个工具,说明你的服务已经成功变成 MCP Server 了——Spring AI 自动完成了协议解析、工具注册、schema 生成这些活。
第三步调用工具:
curl -s -X POST http://localhost:8082/mcp \
-H "Content-Type: application/json" \
-H "Mcp-Session-Id: $SESSION_ID" \
-d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"queryOrder","arguments":{"orderId":"A1001"}}}'
返回结果里 content 数组会带着订单 JSON。到这里,一个完整的 MCP Server 就落地了。
四、踩坑经验和最佳实践
坑一:老版本的 MCP starter 名字完全不一样。 网上大量教程让你引 spring-ai-mcp-server-webmvc-spring-boot-starter,这个坐标在 Maven Central 上确实存在,但最新只有 1.0.0-M6(里程碑版本,还没 GA),而且那是 Spring AI 1.x 时代的产物。2.0.0 统一成了 spring-ai-starter-mcp-server,写 POM 时务必核对坐标和版本,别让旧教程带偏。
坑二:description 写不清楚,模型就乱传参。 这是工具调用成功率的第一杀手。updateOrderStatus 的 newStatus 参数,如果你只写"新状态"三个字,模型可能传"已发"、“Shipping”、"3"这种乱七八糟的值;把可选值"待付款、已付款、已发货、已完成、已取消"写全,模型才能规范传参。同理,参数有格式要求(比如订单号是字母开头+数字)也写进 description。规则:description 里写清"是什么、有哪些可选值、格式长什么样"。
坑三:工具数量太多会撑爆上下文。 MCP Client 每次对话都会拉取全部工具列表给模型。如果你的 Server 暴露了 50 个工具,每个工具的 schema 平均 500 token,光工具定义就吃掉 2.5 万 token。对策:把工具按域拆分到多个 MCP Server(订单一个、售后一个),或者用 @Tool 的 name 属性统一命名空间(如 order_query、order_update),方便客户端按需加载。
坑四:有副作用的工具要加确认机制。 updateOrderStatus 这种写操作,模型一旦误调用就是线上事故。生产环境的做法:写操作工具内部先返回"待确认"结果,让用户确认后再真正执行;或者把写操作放在独立的高权限 MCP Server 里,权限控制做在传输层(比如加 token 鉴权)。
坑五:SSE 模式要处理会话。 SSE 长连接下,MCP 协议要求 Client 带着 Session-Id 才能调用工具,上面 curl 验证里你已经看到了。真实项目里如果客户端调用偶发失败,先查 Session-Id 有没有正确透传。
最佳实践一:工具类保持"薄"。 @Tool 方法里只做参数校验和调用 Service,业务逻辑全放 Service 层。这样工具层只是"翻译官",把 MCP 协议翻译成业务调用,职责单一,也好测试。
最佳实践二:返回结构统一。 所有工具返回 Map 或统一的结果对象(如 {"code":0,"data":...}),模型解析起来更稳定。别一个工具返回 Map、一个返回实体类、一个返回 String,模型会懵。
最佳实践三:用 MCP Inspector 调试。 Spring AI 官方配套的 MCP Inspector(一个可视化调试工具)可以连你的 Server,直接看工具列表、手动调工具、查看原始协议消息。排查"工具没被发现""参数 schema 不对"这类问题,比用 curl 高效得多。
五、性能对比和技术选型
WebMvc vs WebFlux: 两个 starter 暴露的协议完全一样,区别在底层 HTTP 模型。WebMvc 同步阻塞,实现简单、生态兼容性最好,QPS 需求在几千以内的业务服务够用;WebFlux 异步非阻塞,单连接内存占用低,适合高并发、长连接(SSE 流式响应)场景。选型建议:现有服务是 WebMvc 就用默认 starter,除非你的 MCP 工具本身就是流式/高并发的,才考虑 WebFlux。
MCP Server vs 手写 @Tool 注册进 ChatClient: 这是两种不同量级的方案。手写 @Tool 适合"这个工具只有我这个 AI 应用用"的私有场景,零额外成本;MCP Server 适合"工具要被多个 AI 应用/外部工具复用"的场景,一次改造、处处可用。从 8 月 5 日那篇客户端文章的角度看,Spring AI 应用自己也可以是 MCP Client——你自己的服务做成 MCP Server,自己应用用 MCP Client 连,外部工具也能连,一套方案通吃内部外部。
实测数据参考(社区公开压测数据,非本机测试): SSE 模式下 MCP Server 单实例能稳定支撑每秒 1000+ 次工具调用,瓶颈通常不在协议层而在业务代码本身;协议层单次工具调用的额外开销在 1-5 毫秒量级,相比大模型推理的秒级延迟可以忽略不计。所以性能上不用纠结,把精力花在工具质量和描述质量上,收益大得多。
六、总结
把 Java 服务改造成 MCP Server,本质上就是三件事:引入 spring-ai-starter-mcp-server(2.0.0)、在普通方法上加 @Tool 注解、配置 base-path 后启动。 剩下的协议握手、工具发现、参数 schema 生成、结果序列化,Spring AI 全部自动完成。
回顾全文的关键结论:
- MCP 协议基于 JSON-RPC 2.0,通信分初始化、工具发现、工具调用三步,传输层支持 STDIO 和 HTTP+SSE 两种,企业远程服务用 SSE。
spring-ai-starter-mcp-server是 Spring AI 2.0 的 MCP 服务端 starter,默认 WebMvc;WebFlux 项目换成spring-ai-starter-mcp-server-webflux。旧版spring-ai-mcp-server-webmvc-spring-boot-starter停留在 1.0.0-M6(里程碑版本),不要用。- 工具描述质量决定模型调用准确率,description 要写清"是什么、可选值、格式"。
- 工具数量多时按域拆分 Server 或统一命名空间,避免撑爆上下文。
- 写操作工具必须加确认/鉴权机制,防止模型误调用造成线上事故。
下一步你可以做的:把这个订单 MCP Server 跑起来,用 MCP Inspector 连上去看看工具长什么样;然后照 8 月 5 日那篇文章,写一个 Spring AI MCP Client 连你自己的 Server,完成"自己服务自己调"的闭环。
手动摘要(150字内): 本文用 Spring Boot 4.1.0 + Spring AI 2.0.0 实战讲解如何把 Java 服务改造成 MCP Server。通过 spring-ai-starter-mcp-server 依赖和 @Tool 注解,订单查询、状态更新等普通方法即可自动暴露为标准 MCP 工具,任何 MCP Client 都能发现和调用。文章包含完整可运行代码、curl 协议验证、五个真实踩坑经验,以及 WebMvc/WebFlux 选型建议,适合想给 AI 应用提供标准工具的 Java 开发者。
更多推荐




所有评论(0)