手写MCP Server,让大模型调用Java工具
你的 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 HTTP | HTTP,独立进程 | 远程部署,微服务,多客户端共享 |
| Stateless | HTTP,无会话状态 | 云原生、无状态部署 |
(SSE 协议在 Spring AI 2.0 里已经废弃,官方建议改用 Streamable HTTP。)
一个 Server 能暴露三种能力:**Tools(工具)**是让大模型执行动作的,比如查订单、取消订单;**Resources(资源)**是给大模型提供上下文数据的,比如一段配置、一份文档;**Prompts(提示词)**是预置好的提示词模板,供客户端复用。本文重点讲 Tools,顺带演示 Resources。
Spring AI 2.0 的关键设计是注解驱动。你给方法标上 @McpTool,Spring 启动时会扫描所有 @Component Bean 里的这些方法,然后做两件事:
- 用反射读方法签名,自动生成 JSON Schema——也就是这份工具的"说明书",里面写着工具叫啥、每个参数叫什么类型、哪些是必填。
- 把方法包成一个回调,注册进 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 客户端里体验一下,感受"工具写一次、处处复用"的爽感。
更多推荐


所有评论(0)