上一篇文章里,我们讲了怎么用 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 把这一切封装好了。你只需要做三件事:

  1. 引入 spring-ai-starter-mcp-server 依赖
  2. 写一个普通的类,方法上加 @Tool 注解
  3. 配置一下暴露方式(SSE 还是 STDIO)

完事。剩下的协议细节,Spring AI 全包了。

二、底层原理到底怎么回事

要理解 Spring AI 的 MCP Server 是怎么工作的,先得理解 MCP 协议里两个角色的关系。

MCP 协议把参与方分成两边:MCP Server(服务端,提供工具的人)和 MCP Client(客户端,使用工具的人)。通信基于 JSON-RPC 2.0,也就是双方通过交换 JSON 格式的请求和响应来协作。一次完整的调用流程分三步:

  1. 初始化(Initialize):Client 连上 Server,双方交换协议版本和能力信息。这一步相当于握手——“你是哪个版本的协议?你支持哪些能力?”
  2. 工具发现(Tools/List):Client 问 Server:“你有哪些工具?” Server 返回工具清单,每个工具带名字、描述、参数 schema(参数长什么样)。
  3. 工具调用(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 是这个工具给大模型看的说明,@ToolParamdescription 是每个参数给大模型看的说明。方法返回值无所谓什么类型,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-webmvcspring-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 数组应该有三个工具:queryOrderlistOrdersupdateOrderStatus,每个都带 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 写不清楚,模型就乱传参。 这是工具调用成功率的第一杀手。updateOrderStatusnewStatus 参数,如果你只写"新状态"三个字,模型可能传"已发"、“Shipping”、"3"这种乱七八糟的值;把可选值"待付款、已付款、已发货、已完成、已取消"写全,模型才能规范传参。同理,参数有格式要求(比如订单号是字母开头+数字)也写进 description。规则:description 里写清"是什么、有哪些可选值、格式长什么样"。

坑三:工具数量太多会撑爆上下文。 MCP Client 每次对话都会拉取全部工具列表给模型。如果你的 Server 暴露了 50 个工具,每个工具的 schema 平均 500 token,光工具定义就吃掉 2.5 万 token。对策:把工具按域拆分到多个 MCP Server(订单一个、售后一个),或者用 @Toolname 属性统一命名空间(如 order_queryorder_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 开发者。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐