你的 Java 服务里已经有一堆写好的方法:查订单、查库存、发通知。现在要接大模型,让 AI 助手能"动手"调这些方法,而不是只会聊天。问题来了——OpenAI 的 Function Calling、Spring AI 的 @Tool、LangChain4j 的工具定义,格式各不相同,你不想为每家客户端都重写一遍适配。MCP(Model Context Protocol,模型上下文协议)就是来解决这件事的:一套统一标准,写一次,Claude、Cursor、各种 Agent 都能直接调用。这篇文章用 Spring AI 2.0 手写一个 MCP Server,把 Java 方法暴露成大模型能用的工具,代码完整可跑。

一、这个问题到底是什么

大模型只会"说",不会"做"。你跟它聊得再欢,它也没法真的去查你的数据库、调你的下单接口。要让它真正办事,你得给它"工具"——也就是把你代码里的方法包一层,告诉大模型"有这么一个能力,参数长这样,你可以调用它"。

麻烦出在"包一层"这一步。不同框架对"工具"的定义方式完全不一样:

  • OpenAI 用的是 Function Calling,要求你用 JSON Schema 描述每个函数的参数;
  • Spring AI 用 @Tool 注解;
  • LangChain4j 用 @Tool 注解但参数写法和 Spring AI 又不同;
  • 各家 Agent 平台、各家 IDE 的 AI 插件,各自又是一套。

结果就是:同一个"查订单"方法,你想接 Claude Desktop 得写一遍适配,接 Cursor 再写一遍,接自家 Agent 平台还得再来一遍。三份几乎一样的胶水代码,维护起来是噩梦。

MCP 就是来解决这个"碎片化"的。它是 Anthropic 在 2024 年底提出的一套开放协议,定义的是"AI 应用(客户端)"和"提供能力的程序(服务端)“之间怎么通信。你可以把它理解成 USB-C 接口:以前每个设备要配一种专用线,现在一根 Type-C 线全通用。MCP 之于 AI 工具,就是那个"统一接口”。

这篇文章要交付三样东西:第一,用 Spring AI 2.0 的 @McpTool 注解把一个 Java 方法变成 MCP 工具;第二,解释背后 JSON Schema 自动生成的原理;第三,跑起来并用 MCP Inspector 验证。学完你就能把团队现有的 Java 服务,低成本地"升级"成任何大模型都能调用的工具仓库。

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

先看 MCP 的整体架构,一共三个角色:

  • Host(宿主应用):最终用户使用的那个软件,比如 Claude Desktop、Cursor 编辑器、你自己的 App。它负责发起对话。
  • Client(客户端):跑在 Host 内部的一层,负责和 Server 建立连接、收发消息。普通开发者通常不用管它。
  • Server(服务端):你写的、真正提供能力的那部分,里面装着一堆工具、资源和提示词。

一次工具调用的完整流程是:用户在 Claude Desktop 里问"查一下 ORD-001 这个订单",Host 里的 Client 连上你的 Server,Server 告诉 Client"我有 query_order 这个工具",Client 再把工具清单和用户的问题一起交给大模型,大模型决定调用 query_order 并给出参数,最后这个调用又通过 Client 传回你的 Server 真正执行。

MCP 有三种传输协议,决定了 Server 以什么方式被连接:

协议连接方式典型场景
STDIO标准输入输出,进程内本机工具,比如 Claude Desktop 本地插件
Streamable HTTPHTTP,独立进程远程部署,微服务,多客户端共享
StatelessHTTP,无会话状态云原生、无状态部署

(SSE 协议在 Spring AI 2.0 里已经废弃,官方建议改用 Streamable HTTP。)

一个 Server 能暴露三种能力:**Tools(工具)**是让大模型执行动作的,比如查订单、取消订单;**Resources(资源)**是给大模型提供上下文数据的,比如一段配置、一份文档;**Prompts(提示词)**是预置好的提示词模板,供客户端复用。本文重点讲 Tools,顺带演示 Resources。

Spring AI 2.0 的关键设计是注解驱动。你给方法标上 @McpTool,Spring 启动时会扫描所有 @Component Bean 里的这些方法,然后做两件事:

  1. 用反射读方法签名,自动生成 JSON Schema——也就是这份工具的"说明书",里面写着工具叫啥、每个参数叫什么类型、哪些是必填。
  2. 把方法包成一个回调,注册进 MCP Server。

为什么 JSON Schema 这么重要?因为大模型"调用工具"靠的不是直接执行你的代码,而是读说明书。它先看 Schema 知道 query_order 要一个 orderId 参数、参数是字符串、必填,然后才吐出"我要调 query_order,orderId 传 ORD-001"。所以 Schema 写得好不好,直接决定大模型能不能正确调用。Spring AI 帮你自动生成,省掉了手写 JSON 这一步,也避免了手写和代码不一致的坑。

一个容易忽略的点:@McpTool 方法的返回类型也会影响大模型。如果返回的是 String 或 int 这类简单类型,直接转成文本回给模型就行;如果返回的是自定义对象(比如订单信息),你得把 generateOutputSchema 设成 true,让 Spring 额外生成一份"输出结构说明",模型才知道返回值里有哪些字段。

三、实战:手把手写代码

我们做一个"订单服务 MCP Server",暴露两个工具(查订单、取消订单)和一个资源(查配置)。环境要求 JDK 21,用 Maven 构建。

先看 POM。这里只显式声明一个 MCP Server starter,它会把 Spring MVC、MCP SDK 这些依赖一起带进来:

<?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.1</version>
        <relativePath/>
    </parent>

    <groupId>com.example</groupId>
    <artifactId>mcp-order-server</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>mcp-order-server</name>

    <properties>
        <java.version>21</java.version>
        <spring-ai.version>2.0.1</spring-ai.version>
    </properties>

    <dependencies>
        <!-- 这个 starter 会传递引入 spring-boot-starter-web 和 MCP SDK -->
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
        </dependency>
    </dependencies>

    <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>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

这段配置在干嘛:spring-boot-starter-parent 固定到 4.1.1,这是当前最新的 GA 版本;spring-ai-bom 用 2.0.1 统一管理 Spring AI 各模块版本,避免版本对不齐;spring-ai-starter-mcp-server-webmvc 是 WebMVC 传输的 MCP Server starter。版本号都查过 Maven Central,是最新 GA。

配置文件 src/main/resources/application.yml:

spring:
  application:
    name: mcp-order-server
  ai:
    mcp:
      server:
        name: order-server
        version: 1.0.0
        protocol: STREAMABLE

protocol: STREAMABLE 指定用 Streamable HTTP 传输,这是官方推荐的远程方式(SSE 已废弃)。不写的话默认也是 STREAMABLE,但显式写出来更清楚。HTTP 端点的默认路径是 /mcp。

主启动类:

package com.example.mcporderserver;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class McpOrderServerApplication {

    public static void main(String[] args) {
        SpringApplication.run(McpOrderServerApplication.class, args);
    }
}

这是标准的 Spring Boot 启动入口,没有特殊配置。注解扫描默认开启,会自动发现下面带 @McpTool 的方法。

订单信息的数据类,用一个 Java record:

package com.example.mcporderserver;

public record OrderInfo(
        String orderId,
        String customerName,
        String status,
        double amount
) {
}

这个 record 就是工具的返回值类型。因为它是自定义对象,后面在 @McpTool 上要开 generateOutputSchema,让大模型知道返回结构里有哪些字段。

核心的工具类:

package com.example.mcporderserver;

import org.springframework.ai.mcp.annotation.McpTool;
import org.springframework.ai.mcp.annotation.McpToolParam;
import org.springframework.stereotype.Component;

@Component
public class OrderTools {

    @McpTool(
            name = "query_order",
            description = "根据订单号查询订单信息,返回订单状态和金额",
            generateOutputSchema = true
    )
    public OrderInfo queryOrder(
            @McpToolParam(description = "订单号,例如 ORD-20240904-001", required = true) String orderId) {
        // 模拟查询数据库。实际项目里把这里换成 service 或 repository 的调用。
        return new OrderInfo(orderId, "张三", "已发货", 299.00);
    }

    @McpTool(
            name = "cancel_order",
            description = "取消一个未发货的订单,返回操作结果"
    )
    public String cancelOrder(
            @McpToolParam(description = "要取消的订单号", required = true) String orderId) {
        if (orderId == null || orderId.isBlank()) {
            throw new IllegalArgumentException("订单号不能为空");
        }
        return "订单 " + orderId + " 已成功取消";
    }
}

逐行讲一下:@McpTool 标记这是个工具方法,name 是工具的唯一标识(大模型调用时用这个名字),description 是给大模型看的说明——描述写得越清楚,大模型越知道什么时候该调它。@McpToolParam 描述参数,required = true 表示这个参数必填。queryOrder 返回 OrderInfo 自定义对象,所以 generateOutputSchema = true。cancelOrder 里抛的 IllegalArgumentException 是 RuntimeException,Spring AI 会把它转成"错误结果"回传给大模型,大模型看到后可以自行修正重试。

再演示一个资源类,把配置暴露给大模型当上下文:

package com.example.mcporderserver;

import java.util.Map;

import org.springframework.ai.mcp.annotation.McpResource;
import org.springframework.stereotype.Component;

@Component
public class ConfigResource {

    private final Map<String, String> config = Map.of(
            "support_phone", "400-888-1234",
            "return_policy", "7天无理由退货,运费险由平台承担"
    );

    @McpResource(
            uri = "config://{key}",
            name = "AppConfig",
            title = "应用配置",
            description = "读取应用级配置项,例如客服电话、退换货政策",
            mimeType = "text/plain"
    )
    public String getConfig(String key) {
        return config.getOrDefault(key, "未找到配置项:" + key);
    }
}

@McpResource 的 uri 里用了 {key} 这个模板变量,方法参数 String key 会和它自动对应。大模型可以按需读取这些配置,而不必把它们塞进每次的系统提示词里。

跑起来:

mvn spring-boot:run

启动成功后,服务监听 8080 端口,MCP 端点在 http://localhost:8080/mcp。验证有两种方式:最直观的是用官方的 MCP Inspector(一个可视化调试面板),命令行执行 npx @modelcontextprotocol/inspector,选择 Streamable HTTP 传输,填入 http://localhost:8080/mcp,就能看到 query_order、cancel_order 两个工具和 AppConfig 资源,还能直接在面板里手动传参调用;如果你想用程序连,把之前学过的 Spring AI MCP Client 指向这个地址即可。

四、踩坑经验和最佳实践

第一,SSE 别再用,统一上 STREAMABLE。 Spring AI 2.0 里 SSE 协议已经标记废弃。如果你还在配置里写 spring.ai.mcp.server.protocol=SSE,虽然能跑但会看到废弃警告。新项目直接用 STREAMABLE,远程部署用 STATELESS。

第二,HTTP 传输默认没有鉴权,别裸奔到公网。 官方文档明确警告:HTTP 型 MCP Server 默认暴露一个无认证的 JSON-RPC 端点,任何能访问该端点的客户端都能列出并调用你的全部工具。对内网工具没问题,一旦要暴露到公网,必须前置一层安全边界(比如网关鉴权),或者干脆用 STDIO 走本机进程。

第三,返回自定义对象一定要开 generateOutputSchema。 如果你返回的是 record 或 POJO 却没开这个开关,模型只能拿到一个模糊的字符串,不知道返回值里有哪些字段,后续处理容易出错。反过来,返回 String、int 这类简单类型时不用开。

第四,描述写得好,工具才被用得对。 description 和 @McpToolParam 的 description 是给大模型看的"使用说明",不是给人看的注释。写清楚"什么时候用这个工具、参数是什么格式、参数从哪来",比写一堆 Java 注释有用得多。很多工具调用失败的根因不是代码,而是描述太含糊。

第五,区分 RuntimeException 和受检异常。 工具方法里抛 RuntimeException(比如参数校验失败),Spring AI 会把它转成错误结果回传给大模型,让模型能自我纠正;而抛受检异常(比如 IOException)会直接让这次工具调用失败、不让模型看到错误。想要"模型能兜底重试"就用 RuntimeException,想要"失败就硬停"就声明受检异常。

第六,工具方法的参数类型别乱用。 JSON Schema 是从 Java 类型自动推断的,所以参数尽量用 String、int、boolean、record 这些能干净映射到 JSON 的类型。用太复杂的泛型或 Optional 可能导致 Schema 生成不理想,必要时可以退回到手动接收 CallToolRequest 的方式自己解析。

五、性能对比和技术选型

先看三种传输协议怎么选,这是决定部署形态的第一件事:

协议部署复杂度延迟适用场景
STDIO低(进程内)最低本机工具、桌面 AI 插件
Streamable HTTP中(独立进程)低远程服务、多客户端共享
Stateless中(无状态)低云原生、弹性伸缩、无会话需求

再看实现方式怎么选:

  • 注解式(@McpTool):适合绝大多数场景。参数类型固定、能自动生成 Schema、代码最简洁。本文就是这条路。
  • 手动 CallToolRequest:适合参数结构动态变化、需要运行时自定义 Schema 的场景,灵活但要多写解析代码。
  • 直接手写 MCP SDK:适合需要精细控制协议细节的极端场景,学习成本和代码量都最高,一般没必要。

结论:日常团队里把现有 Java 服务"工具化",无脑上 Spring AI 的 @McpTool 注解式方案,配 STREAMABLE 或 STDIO 传输,性价比最高。别一上来就自己撸 SDK。

六、总结

MCP 解决的是一件事:让"写一次工具,处处可调用"成为现实,而不是为每个 AI 客户端重写一遍胶水代码。Spring AI 2.0 把这件事的复杂度压到了最低——一个 @McpTool 注解,方法签名自动变 JSON Schema,@Component 自动扫描注册,几乎零配置。

回顾本文:我们用 Spring Boot 4.1.1 + Spring AI 2.0.1 写了一个订单服务 MCP Server,暴露了查订单、取消订单两个工具和一个配置资源,代码完整可运行,用 MCP Inspector 就能验证。

记住三个关键点:用 STREAMABLE 协议;返回对象要开 generateOutputSchema;HTTP 传输上线前必须加鉴权。

下一步建议你把团队现有的一个真实 Service 方法包成 MCP 工具,接进你常用的 AI 客户端里体验一下,感受"工具写一次、处处复用"的爽感。

Logo

更多推荐