Spring AI MCP Server怎么接入Claude Code的完整流程
导读:想让 Claude Code 直接查业务系统里的数据,最省事的办法是在 Spring Boot 里起一个 MCP Server。这篇用 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 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
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 启动类:
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 包下:
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 启动
# 在项目根目录打包并启动 mvn -DskipTests package java -jar target/order-mcp-server-0.0.1.jar
启动日志里看到这几行就说明工具注册上了:
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。
# 第一步:握手,-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 里配的名字和说明:
{"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 通知,再列工具、调工具:
# 第二步:取会话 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 里:
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 能看到原因:
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 配置就三行:
{
"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)来验证,方便写进脚本:
# --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 服务的工具,回答控制在三行内。"
输出:
SO20261028001:状态 PAID,金额 199.00 PAID 状态订单:SO20261028001、SO20261029001
两个工具都被调到了,数据和内存里的一致。
本机 ~/.claude/settings.json 的权限模式是 auto,去掉 --allowedTools 也能直接调通。默认权限模式下 -p 没法弹确认框,没放行的工具大概率会被拒(这一点本文没有单独验证),写脚本时建议总是显式写 --allowedTools。
六、几个坑
6.1 工具默认被标成"破坏性操作"
MCP 协议里每个工具可以带几个提示字段,告诉客户端这个工具是不是只读、会不会改数据。第一版代码我没写 annotations,tools/list 返回的是:
"annotations":{"title":"","readOnlyHint":false,"destructiveHint":true,"idempotentHint":false,"openWorldHint":true}一个只读的查询方法,默认被标成了"非只读、有破坏性、会访问外部世界"。这些字段是给客户端参考的,客户端可以据此决定要不要额外确认。Claude Code 具体怎么用这几个字段,本文没有验证。我的建议是只读工具一律显式写上,加上之后返回变成:
"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怎么接入Claude Code的完整流程的详细内容,更多关于Spring AI MCP Server接入Claude Code的资料请关注脚本之家其它相关文章!
相关文章
Lombok中@EqualsAndHashCode注解的使用及说明
这篇文章主要介绍了Lombok中@EqualsAndHashCode注解的使用及说明,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教2023-03-03
浅析如何将多个SpringBoot项目打包到一个Docker容器中
在现代软件开发中,Docker已成为一种流行的容器化技术,能够简化应用的部署和管理,本文将详细介绍如何将多个Spring Boot项目打包到一个Docker容器中,希望对大家有所帮助2024-10-10
记一次Maven项目改造成SpringBoot项目的过程实践
本文主要介绍了Maven项目改造成SpringBoot项目的过程实践,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下2022-03-03
关于SpringBoot配置文件application.properties的路径问题
这篇文章主要介绍了关于SpringBoot配置文件application.properties的路径问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教2022-08-08
Java中的ThreadLocal与ThreadLocalMap详解
这篇文章主要介绍了Java中的ThreadLocal与ThreadLocalMap详解,ThreadLocal 是一个线程局部变量,其实的功用非常简单,就是为每一个使用该变量的线程都提供一个变量值的副本,是Java中一种较为特殊的线程绑定机制,需要的朋友可以参考下2023-09-09


最新评论