Spring AI MCP Server 怎么接入 Claude Code:Spring Boot 用 @McpTool 暴露接口(Spring AI 2.0.2 实测)

导读:想让 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。


参考链接


你们打算先把哪类接口暴露给 AI:查订单、查配置,还是查日志?现在用的是 Spring AI 1.x 还是已经上了 2.0?评论区说说,我挑典型场景写下一篇的工具设计。

觉得有用的话点个赞、收个藏,接 MCP 的时候翻得到。

相关推荐
学长毕设1 小时前
基于springboot的实验室设备管理系统
java·spring boot·后端
学长毕设2 小时前
基于springboot的社区养老监护系统的设计与实现
java·spring boot·后端
wno7042 小时前
Springboot国际化信息(i18n)解析
java·spring boot·后端
paopaokaka_luck3 小时前
社区服务平台(加权协同过滤推荐、Jieba中文词频分析、WebSocket实时聊天、求助处理闭环、 活动积分闭环、ECharts多维分析)
java·前端·spring boot·mysql·echarts·mybatis
不会写代码的女程序猿4 小时前
Spring Boot+Layui构建企业级电子招投标系统源码
spring boot·招投标系统·招采系统·电子招投标
仙逆GPT6 小时前
ChatGPT、Codex工程机制:Agent把@Transactional方法改成异步执行,为什么数据会出现部分提交?
spring boot·chatgpt·codex·chatgptplus·chatgptpro
切糕师学AI6 小时前
K8s 滚动更新与 Spring Boot 优雅停机实战:如何做到真正的平滑发布?
spring boot·云原生·容器·kubernetes·k8s
LazyHunter97 小时前
Spring Boot 3 微服务实践指南
java·spring boot
zzu123zsw7 小时前
从 SPI 契约到独立 ClassLoader 隔离:Spring Boot 插件热插拔架构设计与内存泄露防御
java·spring boot·后端·架构设计