导读:想让 Claude Code 直接查业务系统里的数据,最省事的办法是在 Spring Boot 里起一个 MCP Server。掘金上搜"Spring AI MCP Server",排前面的教程大多是 2025 年写的,讲的是 SSE 方式,而 Spring AI 2.0 已经把 SSE 标成废弃。这篇用 Spring AI 2.0.2 从零搭一个订单查询的 MCP Server,用 curl 把协议走一遍,再接到 Claude Code 里实际调一次,顺带把几个文档里没写的坑列出来。
一、先说清楚:MCP Server 是什么,为什么要用 Java 写
MCP(Model Context Protocol)是一套"AI 怎么调用外部工具"的约定。你可以把它理解成给 AI 看的接口文档 + 调用协议:服务端告诉 AI"我有哪些工具、每个工具要什么参数",AI 觉得需要时就按约定发请求过来。Claude Code、Cursor、Codex 这些编程工具都支持 MCP。
MCP Server 就是提供工具的那一端。Python、Node 写的 MCP Server 很多,但 Java 后端的业务数据、权限、事务都在 Spring 服务里,用 Python 再包一层,等于把业务逻辑抄一遍。Spring AI 提供了现成的 starter,在已有的 Spring Boot 项目里加一个依赖、写几个带注解的方法,就能把现有的 Service 暴露给 AI。
不管它会怎样:AI 只能看你贴给它的代码和日志,想知道"这个订单现在什么状态""这个配置线上是多少",还得你去查完再复制过来。
什么时候值得做:
| 场景 | 建议 |
|---|---|
| 排查问题时经常要查同几张表、同几个接口 | 值得,把查询封装成只读工具 |
| 想让 AI 帮忙改数据、发消息 | 先别做,写操作要先想清楚鉴权和审计 |
| 只是偶尔查一次 | 不值得,直接问 AI 写 SQL 更快 |
二、版本和环境
| 组件 | 版本 |
|---|---|
| Spring AI | 2.0.2(2.0 线最新正式版) |
| Spring Boot | 4.1.2(Spring AI 2.0.2 的 starter 依赖的就是这个版本) |
| JDK | 17.0.12 |
| Maven | 3.6.3 |
| Claude Code | 2.1.299 |
| MCP 协议版本 | 2025-06-18(握手时客户端发的值,服务端原样接受) |
⚠️ Spring AI 2.1 目前还是 RC1,本文没有用。2.0 和 1.x 的差别很大:1.x 时代的教程用
@Tool+ToolCallbackProvider注册工具、走/sse端点,照抄到 2.0 会碰到废弃警告或端点对不上。
三、搭一个订单查询的 MCP Server
3.1 pom.xml
只需要一个 starter,spring-ai-starter-mcp-server-webmvc 会带上 spring-boot-starter-web 和 MCP 注解支持:
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.2</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>order-mcp-server</artifactId>
<version>0.0.1</version>
<properties>
<java.version>17</java.version>
<spring-ai.version>2.0.2</spring-ai.version>
</properties>
<dependencyManagement>
<dependencies>
<!-- Spring AI 版本统一由 BOM 管 -->
<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>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
3.2 application.yml
yaml
server:
port: 18080
# 只监听本机,HTTP 端点默认没有鉴权
address: 127.0.0.1
spring:
ai:
mcp:
server:
name: order-mcp-server
version: 0.0.1
# 2.0 默认就是 STREAMABLE,写出来方便看
protocol: STREAMABLE
instructions: "订单查询工具,只读。"
几个常用配置项和默认值,从 2.0.2 的配置元数据里查出来的:
| 配置项 | 默认值 | 说明 |
|---|---|---|
spring.ai.mcp.server.protocol |
streamable |
可选 SSE、STREAMABLE、STATELESS,SSE 从 2.0.0 起废弃 |
spring.ai.mcp.server.streamable-http.mcp-endpoint |
/mcp |
Streamable HTTP 的端点路径 |
spring.ai.mcp.server.name / version |
mcp-server / 1.0.0 |
握手时返回给客户端的服务名和版本 |
spring.ai.mcp.server.type |
sync |
sync 只注册同步方法,async 只注册异步方法 |
spring.ai.mcp.server.annotation-scanner.enabled |
true |
扫描 @McpTool 等注解 |
spring.ai.mcp.server.request-timeout |
20s |
请求超时 |
3.3 写工具类
启动类就是普通的 Spring Boot 启动类:
java
package com.example.mcpdemo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class OrderMcpApplication {
public static void main(String[] args) {
SpringApplication.run(OrderMcpApplication.class, args);
}
}
工具类加 @Component,方法上加 @McpTool,参数上加 @McpToolParam。注解在 org.springframework.ai.mcp.annotation 包下:
java
package com.example.mcpdemo;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
import org.springframework.ai.mcp.annotation.McpTool;
import org.springframework.ai.mcp.annotation.McpToolParam;
import org.springframework.stereotype.Component;
@Component
public class OrderTools {
public record Order(String orderNo, String status, BigDecimal amount, String createdAt) {
}
// 演示用内存数据,实际项目里换成注入的 OrderService
private static final Map<String, Order> ORDERS = Map.of(
"SO20261028001", new Order("SO20261028001", "PAID", new BigDecimal("199.00"), "2026-10-28 09:12"),
"SO20261028002", new Order("SO20261028002", "SHIPPED", new BigDecimal("58.50"), "2026-10-28 10:40"),
"SO20261029001", new Order("SO20261029001", "PAID", new BigDecimal("1288.00"), "2026-10-29 08:05"));
@McpTool(name = "get_order", description = "按订单号查询订单详情,订单号形如 SO20261028001",
annotations = @McpTool.McpAnnotations(readOnlyHint = true, destructiveHint = false, idempotentHint = true, openWorldHint = false))
public Order getOrder(@McpToolParam(description = "订单号", required = true) String orderNo) {
Order order = ORDERS.get(orderNo);
if (order == null) {
throw new IllegalArgumentException("订单不存在: " + orderNo);
}
return order;
}
@McpTool(name = "list_orders_by_status", description = "按状态列出订单,状态取值 PAID / SHIPPED",
annotations = @McpTool.McpAnnotations(readOnlyHint = true, destructiveHint = false, idempotentHint = true, openWorldHint = false))
public List<Order> listOrdersByStatus(
@McpToolParam(description = "订单状态:PAID 或 SHIPPED", required = true) String status) {
return ORDERS.values().stream()
.filter(o -> o.status().equalsIgnoreCase(status))
.sorted((a, b) -> a.orderNo().compareTo(b.orderNo()))
.toList();
}
}
annotations 那一行为什么要写,第六节细说。description 要写清楚取值范围和格式,AI 就是靠这段文字决定什么时候调、参数怎么填。
3.4 启动
bash
# 在项目根目录打包并启动
mvn -DskipTests package
java -jar target/order-mcp-server-0.0.1.jar
启动日志里看到这几行就说明工具注册上了:
text
o.s.a.m.s.c.a.McpServerAutoConfiguration : Enable tools capabilities, notification: true
o.s.a.m.s.c.a.McpServerAutoConfiguration : Registered tools: 2
o.s.boot.tomcat.TomcatWebServer : Tomcat started on port 18080 (http) with context path '/'
日志里还会有三条 BeanPostProcessorChecker 的 WARN,说 McpServerAnnotationScannerAutoConfiguration 等几个 Bean "is not eligible for getting processed by all BeanPostProcessors"。这是 starter 自己的注解扫描器引起的,不影响工具注册,可以不管。
四、先用 curl 把协议走一遍
接 Claude Code 之前,先用 curl 确认服务端没问题。出了问题能分清是服务端的事还是客户端配置的事。
Streamable HTTP 的流程是:客户端先发 initialize,服务端在响应头里返回一个会话 ID,之后的请求都带上这个 ID。
bash
# 第一步:握手,-D 把响应头存下来,里面有 Mcp-Session-Id
curl -s -D headers.txt http://127.0.0.1:18080/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","version":"1"}}}'
响应体里能看到 application.yml 里配的名字和说明:
json
{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-06-18","capabilities":{"completions":{},"logging":{},"prompts":{"listChanged":true},"resources":{"subscribe":false,"listChanged":true},"tools":{"listChanged":true}},"serverInfo":{"name":"order-mcp-server","version":"0.0.1"},"instructions":"订单查询工具,只读。"}}
接下来取出会话 ID,发 initialized 通知,再列工具、调工具:
bash
# 第二步:取会话 ID
SID=$(grep -i mcp-session-id headers.txt | awk '{print $2}' | tr -d '\r')
# 第三步:通知服务端初始化完成,返回 202
curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:18080/mcp \
-H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \
-H "Mcp-Session-Id: $SID" \
-d '{"jsonrpc":"2.0","method":"notifications/initialized"}'
# 第四步:调用 get_order
curl -s http://127.0.0.1:18080/mcp \
-H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \
-H "Mcp-Session-Id: $SID" \
-d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"get_order","arguments":{"orderNo":"SO20261028001"}}}'
tools/call 的返回是 SSE 格式(event:message + data:),返回值被序列化成 JSON 字符串放在 content[0].text 里:
text
id:f71d530b-99e1-42a6-931b-b4afc642f639
event:message
data:{"jsonrpc":"2.0","id":3,"result":{"content":[{"type":"text","text":"{\"orderNo\":\"SO20261028001\",\"status\":\"PAID\",\"amount\":199.00,\"createdAt\":\"2026-10-28 09:12\"}"}],"isError":false}}
查一个不存在的订单号 SO404,方法里抛的异常不会变成 HTTP 500,而是 isError: true 加上异常信息,AI 能看到原因:
text
data:{"jsonrpc":"2.0","id":4,"result":{"content":[{"type":"text","text":"订单不存在: SO404\n订单不存在: SO404"}],"isError":true}}
异常信息重复了两遍,2.0.2 就是这样,不影响使用。
curl 阶段碰到的两个报错:
| 现象 | 原因 |
|---|---|
返回 Invalid Accept headers. Expected TEXT_EVENT_STREAM and APPLICATION_JSON |
Accept 头只写了 application/json,必须两个都写 |
tools/list 返回 HTTP 400 |
没带 Mcp-Session-Id,或者会话 ID 是上一次启动的(重启服务后旧 ID 失效) |
五、接入 Claude Code
5.1 写 MCP 配置文件
Claude Code 的 HTTP 类型 MCP 配置就三行:
json
{
"mcpServers": {
"order": {
"type": "http",
"url": "http://127.0.0.1:18080/mcp"
}
}
}
这个文件放哪有两种用法:
- 想让整个项目组都用:放在项目根目录,命名为
.mcp.json,提交到 git。也可以用命令生成:claude mcp add --transport http --scope project order http://127.0.0.1:18080/mcp,实测生成的内容和上面一模一样 - 只想临时测一下:随便放哪,启动 Claude Code 时用
--mcp-config指定
claude mcp add 的 --scope 有三个值:local(默认,只对自己在这个项目生效)、project(写 .mcp.json)、user(对自己所有项目生效)。
5.2 在 Claude Code 里调用
交互模式下,配好之后直接问"查一下订单 SO20261028001"就行。按官方文档的说法,项目级 .mcp.json 里的服务第一次会先弹窗让你批准,选错了可以用 claude mcp reset-project-choices 重置;-p 模式弹不了窗,会直接加载。
我用的是非交互模式(-p)来验证,方便写进脚本:
bash
# --strict-mcp-config 表示只用这个文件里的 MCP 服务,不加载本机其他配置
# --allowedTools 预先放行两个工具,命名规则是 mcp__<服务名>__<工具名>
claude -p --mcp-config mcp.json --strict-mcp-config \
--allowedTools "mcp__order__get_order" "mcp__order__list_orders_by_status" \
"查一下订单 SO20261028001 的状态和金额;再列出所有 PAID 状态的订单号。只用 order 这个 MCP 服务的工具,回答控制在三行内。"
输出:
text
SO20261028001:状态 PAID,金额 199.00
PAID 状态订单:SO20261028001、SO20261029001
两个工具都被调到了,数据和内存里的一致。
💡 本机
~/.claude/settings.json的权限模式是auto,去掉--allowedTools也能直接调通。默认权限模式下-p没法弹确认框,没放行的工具大概率会被拒(这一点本文没有单独验证),写脚本时建议总是显式写--allowedTools。
六、几个坑
6.1 工具默认被标成"破坏性操作"
MCP 协议里每个工具可以带几个提示字段,告诉客户端这个工具是不是只读、会不会改数据。第一版代码我没写 annotations,tools/list 返回的是:
json
"annotations":{"title":"","readOnlyHint":false,"destructiveHint":true,"idempotentHint":false,"openWorldHint":true}
一个只读的查询方法,默认被标成了"非只读、有破坏性、会访问外部世界"。这些字段是给客户端参考的,客户端可以据此决定要不要额外确认。Claude Code 具体怎么用这几个字段,本文没有验证。我的建议是只读工具一律显式写上,加上之后返回变成:
json
"annotations":{"title":"","readOnlyHint":true,"destructiveHint":false,"idempotentHint":true,"openWorldHint":false}
6.2 老教程的 SSE 写法
| 对比项 | 1.x 老教程 | 2.0.2 |
|---|---|---|
| 传输方式 | SSE,端点 /sse + /mcp/message |
Streamable HTTP,端点 /mcp |
| 注册工具 | @Tool + ToolCallbackProvider Bean |
@McpTool,注解扫描自动注册 |
| 协议配置 | 1.0.0 没有 protocol 配置项,只有 SSE |
默认 streamable,SSE 已废弃 |
| Claude Code 配置 | "type": "sse",url 写 /sse |
"type": "http",url 写 /mcp |
最常见的错是服务端已经是 2.0 默认的 Streamable HTTP,客户端还照着老教程配 "type": "sse" 和 /sse 地址,自然连不上。两边要对上:服务端 protocol 是什么,客户端 type 就配什么。
6.3 HTTP 端点默认不鉴权
官方文档写得很明确:SSE、Streamable HTTP、Stateless 三种 HTTP 传输默认都是不带鉴权 的。本文的示例把 server.address 设成 127.0.0.1,只有本机能连。如果要部署到测试环境给多人用,先加 Spring Security,再在 Claude Code 里用 claude mcp add --transport http ... --header "Authorization: Bearer <token>" 带上凭证。工具方法里要拿到请求头,文档给的办法是在传输层配置 contextExtractor,再通过 McpSyncRequestContext 读取,这部分本文没有实测。
6.4 工具别直接暴露 Mapper
@McpTool 方法可以注入任何 Bean,最省事的写法是直接把 Mapper 或者"执行任意 SQL"包成工具。我建议别这么做:AI 填参数是"猜"的,给它一个范围明确的业务方法(按订单号查、按状态查、限制返回条数),比给它一个万能查询安全得多,也更不容易查错。
七、速查表
| 要做的事 | 怎么做 |
|---|---|
| 加依赖 | spring-ai-starter-mcp-server-webmvc,用 spring-ai-bom 2.0.2 管版本 |
| 注册工具 | 方法加 @McpTool(name, description),参数加 @McpToolParam |
| 只读工具 | annotations = @McpTool.McpAnnotations(readOnlyHint = true, destructiveHint = false, ...) |
| 端点 | 默认 POST /mcp,改路径用 spring.ai.mcp.server.streamable-http.mcp-endpoint |
| curl 自测 | Accept 必须同时有 application/json 和 text/event-stream;initialize 之后带 Mcp-Session-Id |
| 项目级接入 | claude mcp add --transport http --scope project order http://127.0.0.1:18080/mcp |
| 脚本里调用 | claude -p --mcp-config mcp.json --strict-mcp-config --allowedTools "mcp__order__get_order" "..." |
八、常见问题
Q:工具方法返回对象,AI 收到的是什么?
Spring AI 会把返回值序列化成 JSON 字符串,放进 content[0].text。所以返回 record、POJO、List 都可以,字段名就是 AI 看到的名字,起名时别用 f1、col2 这种。
Q:想用 STDIO 方式,让 Claude Code 自己拉起 jar 可以吗?
可以,文档里 STDIO 对应的是 spring-ai-starter-mcp-server 这个 starter,配 spring.ai.mcp.server.stdio=true。本文没有测 STDIO 方式。我的倾向是:服务本来就在跑的,用 HTTP;纯本地小工具,用 STDIO 省掉端口。
Q:项目已经是 Spring Boot 3.x,能用 2.0.2 吗?
不能直接用。Spring AI 2.0.2 的 starter 依赖的是 Spring Boot 4.1.2。Boot 3.x 项目要么用 Spring AI 1.x(老写法),要么先升级 Boot。
Q:改了工具代码,Claude Code 要重启吗?
服务端重启后旧的会话 ID 失效,Claude Code 那边需要重新连接。交互模式下输入 /mcp 可以看各个服务的连接状态,官方文档给的重连命令是 /mcp reconnect all。
参考链接
- Spring AI MCP Server Boot Starter 文档:docs.spring.io/spring-ai/r...
- MCP 协议 Streamable HTTP 传输规范:modelcontextprotocol.io/specificati...
- Claude Code MCP 配置文档:code.claude.com/docs/en/mcp
- Spring AI 项目主页(版本列表):spring.io/projects/sp...
你们打算先把哪类接口暴露给 AI:查订单、查配置,还是查日志?现在用的是 Spring AI 1.x 还是已经上了 2.0?评论区说说,我挑典型场景写下一篇的工具设计。
觉得有用的话点个赞、收个藏,接 MCP 的时候翻得到。