Spring AI MCP Server怎么接入Claude Code的完整流程

 更新时间:2026年11月05日 09:25:00   作者:aiks_yuan  
想知道怎么让Claude Code直接查你Spring Boot里的订单数据,这篇教程用SpringAI 2.0.2从零搭一个MCP Server,手把手带你用curl走通协议,再接到Claude Code里实际调用,还会告诉你文档里没写的坑,比如SSE废弃、工具默认被标成破坏性操作这些,需要的朋友可以参考下

导读:想让 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 AI2.0.2(2.0 线最新正式版)
Spring Boot4.1.2(Spring AI 2.0.2 的 starter 依赖的就是这个版本)
JDK17.0.12
Maven3.6.3
Claude Code2.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.protocolstreamable可选 SSE、STREAMABLE、STATELESS,SSE 从 2.0.0 起废弃
spring.ai.mcp.server.streamable-http.mcp-endpoint/mcpStreamable HTTP 的端点路径
spring.ai.mcp.server.name / versionmcp-server / 1.0.0握手时返回给客户端的服务名和版本
spring.ai.mcp.server.typesyncsync 只注册同步方法,async 只注册异步方法
spring.ai.mcp.server.annotation-scanner.enabledtrue扫描 @McpTool 等注解
spring.ai.mcp.server.request-timeout20s请求超时

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_JSONAccept 头只写了 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/messageStreamable 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的资料请关注脚本之家其它相关文章!

相关文章

  • Java 集合概览(小结)

    Java 集合概览(小结)

    这篇文章主要介绍了Java 集合概览(小结),详细的介绍了集合的概念和接口等。小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-06-06
  • Lombok中@EqualsAndHashCode注解的使用及说明

    Lombok中@EqualsAndHashCode注解的使用及说明

    这篇文章主要介绍了Lombok中@EqualsAndHashCode注解的使用及说明,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-03-03
  • 浅析如何将多个SpringBoot项目打包到一个Docker容器中

    浅析如何将多个SpringBoot项目打包到一个Docker容器中

    在现代软件开发中,Docker已成为一种流行的容器化技术,能够简化应用的部署和管理,本文将详细介绍如何将多个Spring Boot项目打包到一个Docker容器中,希望对大家有所帮助
    2024-10-10
  • java中transient关键字的作用解析

    java中transient关键字的作用解析

    这篇文章主要介绍了java中transient关键字的作用解析,日常业务中,为了安全起见,有些敏感信息我们不希望在网络间被传输可以使用transient对字段进行修饰,不进行序列化,则返回获取到的字段为null,需要的朋友可以参考下
    2023-11-11
  • java更改图片大小示例分享

    java更改图片大小示例分享

    这篇文章主要介绍了java更改图片大小示例,方法中指定路径 ,旧文件名称 ,新文件名称,n 改变倍数就可以完成更改图片大小,需要的朋友可以参考下
    2014-03-03
  • 记一次Maven项目改造成SpringBoot项目的过程实践

    记一次Maven项目改造成SpringBoot项目的过程实践

    本文主要介绍了Maven项目改造成SpringBoot项目的过程实践,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-03-03
  • java前后端传值,参数有集合类型的数据时的两种操作方式

    java前后端传值,参数有集合类型的数据时的两种操作方式

    这篇文章主要介绍了java前后端传值,参数有集合类型的数据时的两种操作方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2023-11-11
  • 关于SpringBoot配置文件application.properties的路径问题

    关于SpringBoot配置文件application.properties的路径问题

    这篇文章主要介绍了关于SpringBoot配置文件application.properties的路径问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-08-08
  • Java中的ThreadLocal与ThreadLocalMap详解

    Java中的ThreadLocal与ThreadLocalMap详解

    这篇文章主要介绍了Java中的ThreadLocal与ThreadLocalMap详解,ThreadLocal 是一个线程局部变量,其实的功用非常简单,就是为每一个使用该变量的线程都提供一个变量值的副本,是Java中一种较为特殊的线程绑定机制,需要的朋友可以参考下
    2023-09-09
  • Java实现生产者消费者问题与读者写者问题详解

    Java实现生产者消费者问题与读者写者问题详解

    这篇文章主要介绍了Java实现生产者消费者问题与读者写者问题详解,小编觉得挺不错的,这里分享给大家,供需要的亲朋好友参考。
    2017-11-11

最新评论