"如果说 MCP 是 AI 世界的 USB 接口,那 Spring AI 就是帮你焊好接口、包好外壳、还贴心地附赠了说明书的那个工具人。"
目录
- [写在前面:MCP 是个啥?](#写在前面:MCP 是个啥? "#%E5%86%99%E5%9C%A8%E5%89%8D%E9%9D%A2mcp-%E6%98%AF%E4%B8%AA%E5%95%A5")
- [Spring AI 版本演进史](#Spring AI 版本演进史 "#spring-ai-%E7%89%88%E6%9C%AC%E6%BC%94%E8%BF%9B%E5%8F%B2")
- [从零搭建一个 MCP Server](#从零搭建一个 MCP Server "#%E4%BB%8E%E9%9B%B6%E6%90%AD%E5%BB%BA%E4%B8%80%E4%B8%AA-mcp-server")
- 核心注解详解
- 配置参数大全
- 传输协议的前世今生
- 进阶玩法
- 常见踩坑实录
写在前面:MCP 是个啥?
MCP (Model Context Protocol) 是 Anthropic 提出的一套开放协议,让 AI 模型能够像调用 API 一样调用外部工具。你可以把它理解为:
css
传统 Web 开发:浏览器 <--HTTP--> 服务器
MCP 世界: AI 模型 <--MCP--> 你的工具服务
以前你想让 AI 查个天气、算个数,得靠 prompt 里硬塞一堆上下文。现在有了 MCP,AI 可以主动说:"嘿,我需要查一下北京的天气",然后你的 MCP Server 就屁颠屁颠地把结果递过去了。
一句话总结:MCP 让 AI 从"只会动嘴"进化成了"能动手"。
Spring AI 版本演进史
Spring AI 的发展速度堪比坐上了火箭------从最初的"实验品"到现在的"生产级框架",每个版本都在疯狂堆料。
官网版本全景图 (截至 2026 年 4 月)
打开 Spring AI 官网,你会看到这样一幅"版本大家族合影":
| 版本 | 标签 | 状态说明 |
|---|---|---|
| 1.1.4 | CURRENT |
1.1.x 系列最新稳定版,本项目正在使用 |
| 1.0.5 | CURRENT |
1.0.x 系列最新稳定版,长期维护分支 |
| 2.0.0-SNAPSHOT | SNAPSHOT |
2.0 开发快照,每日构建,喜欢刺激的可以试试 |
| 2.0.0-M4 | PRE |
2.0 第四个里程碑预览版,功能逐步冻结中 |
| 1.1.5-SNAPSHOT | SNAPSHOT |
1.1.x 的下一个补丁版快照 |
小科普:
CURRENT= 生产可用的稳定版;PRE= 预览版,功能基本成型但还在打磨;SNAPSHOT= 每日构建的开发版,随时可能翻天覆地,慎用于生产。
看到没?Spring AI 同时维护着 两条稳定线 (1.0.x 和 1.1.x),外加一条正在孵化的 2.0 线。这在 Spring 生态里很常见------老用户有安全网,新用户有最新功能,冒险家有尝鲜渠道。各取所需,和谐共处。
版本演进时间线
scss
0.8.x 1.0.0-M1~M6 1.0.0 GA 1.0.5 未来
| | | | |
v v v v v
[拓荒时代] --> [里程碑打磨] --> [首个正式版!] --> [稳定维护] --> [持续修补]
|
| (分支)
v
1.1.0 -------> 1.1.4 -------> 1.1.5
[鸟枪换炮] [当前推荐] [快照开发中]
|
| (分支)
v
2.0.0-M1~M4 --> 2.0.0
[下一代预览] [未来可期]
各版本核心特性详解
1.0.x 系列 ------ "稳如老狗"
最新稳定版:1.0.5 CURRENT
Spring AI 1.0.x 是第一个正式发布的版本系列。如果你的项目已经在用 1.0.x 且跑得好好的,没必要急着升级------官方还在持续维护呢。
核心能力:
@Tool注解:终于不用手写 JSON Schema 了!一个注解搞定工具声明@ToolParam注解:给参数加上描述,让 AI 知道该传什么- SSE 传输协议:基于 Server-Sent Events 的实时通信,支持远程部署
- stdio 模式:通过标准输入输出通信,适合本地工具
MethodToolCallbackProvider:把普通 Java 方法变成 MCP 工具的魔法转换器
java
// 1.0.x 时代的写法(至今仍然适用)
@Tool(description = "Get the current time")
public String getCurrentTime() {
return LocalDateTime.now().toString();
}
1.0.x 的不足:SSE 协议在复杂网络环境下(代理、负载均衡)容易出问题,像是一个只能在实验室里跑的原型。也正因为这个痛点,才有了 1.1.x 的诞生。
1.1.x 系列 ------ "好用且能打"
最新稳定版:1.1.4 CURRENT | 开发中:1.1.5-SNAPSHOT
1.1.0 是一次质的飞跃,也是目前新项目的最佳选择。三个重磅更新:
1. Streamable HTTP 协议
告别 SSE 的各种水土不服!Streamable HTTP 使用标准的 HTTP 请求/响应模型,天然兼容各种代理、网关和负载均衡器。
yaml
# 一行配置,从此告别 SSE
spring.ai.mcp.server.protocol: STREAMABLE
2. 注解扫描器 (Annotation Scanner)
1.1.0 引入了自动扫描机制------只要你的类是 Spring Bean 且方法标了 @Tool,框架就能自动发现并注册。不再需要手动配置 ToolCallbackProvider Bean(当然手动配置依然支持,看你心情)。
yaml
# 默认就是 true,写不写都行,但写出来显得专业
spring.ai.mcp.server.annotation-scanner.enabled: true
3. MCP SDK 持续升级
从 0.8.x 一路升到 0.17.0,每个版本都在完善协议实现。Spring AI BOM 帮你管好了版本,不用操心依赖冲突。
2.0.0 系列 ------ "未来已来"
里程碑版:2.0.0-M4 PRE | 开发快照:2.0.0-SNAPSHOT
2.0 是 Spring AI 的下一个大版本,目前已经推进到第四个里程碑 (M4)。虽然官方还没正式公布完整的 changelog,但从里程碑节奏来看,2.0 GA 正在加速靠近。
从 M4 的标签可以推测,2.0 大概率会带来:
- API 层面的重大重构和优化(大版本号升级的惯例)
- 更完善的 MCP 协议支持
- 可能的 breaking changes(所以才需要大版本号)
尝鲜提醒: 2.0.0-M4 (
PRE) 可以用于技术预研和 POC,但别拿来上生产------除非你喜欢半夜被 oncall 电话叫醒的感觉。2.0.0-SNAPSHOT 就更刺激了,每天一个惊喜(或惊吓),适合 Spring AI 贡献者和极限运动爱好者。
版本选择建议
markdown
你是什么类型的开发者?
/ | \
稳健型 进取型 冒险型
| | |
1.0.5 1.1.4 2.0.0-M4
(老项目 (新项目 (技术预研
维护) 首选!) 尝鲜)
| 你的情况 | 推荐版本 | 理由 |
|---|---|---|
| 新项目,从零开始 | 1.1.4 | 最新稳定版,Streamable HTTP 是正确答案 |
| 老项目,已在用 1.0.x | 1.0.5 | 先升到 1.0.5 修补漏洞,再择机迁移到 1.1.x |
| 想提前适配 2.0 | 2.0.0-M4 | 开个分支试试水,但别合进 main |
| 想给 Spring AI 贡献代码 | 2.0.0-SNAPSHOT | 欢迎入坑,社区需要你 |
本项目的选择:1.1.4------稳定、功能全、Streamable HTTP 加持,没毛病。
从零搭建一个 MCP Server
第一步:创建项目
最低配 pom.xml,简洁得像一首俳句:
xml
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.4</version>
</parent>
<properties>
<java.version>21</java.version>
<spring-ai.version>1.1.4</spring-ai.version>
</properties>
<dependencies>
<!-- 就这一个依赖,没了 -->
<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>
你没看错,核心依赖只有一个。Spring AI BOM 会帮你搞定 MCP SDK、Jackson、Reactor 等一票传递依赖。这就是 Spring Boot "约定优于配置" 哲学的魅力------它替你做了 90% 的决定,而且大部分时候做得还不错。
Starter 选择指南
Spring AI 提供了多个 MCP Server Starter,别选错了:
| Starter | 传输方式 | 适用场景 |
|---|---|---|
spring-ai-starter-mcp-server |
stdio | 本地工具,客户端管理进程生命周期 |
spring-ai-starter-mcp-server-webmvc |
HTTP/SSE (同步) | Web 部署,本项目用的就是这个 |
spring-ai-starter-mcp-server-webflux |
HTTP/SSE (响应式) | 高并发场景,需要 WebFlux 技术栈 |
第二步:写启动类
java
@SpringBootApplication
public class McpWebServerDemo {
public static void main(String[] args) {
SpringApplication.run(McpWebServerDemo.class, args);
}
}
对,就这么朴实无华。Spring Boot 的启动类从来不需要花里胡哨。
第三步:写工具类
这是整个项目最有趣的部分------定义 AI 可以调用的工具:
java
@Service
public class DemoTools {
@Tool(description = "Get the current date and time")
public String getCurrentTime() {
return LocalDateTime.now()
.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
}
@Tool(description = "Perform basic arithmetic calculations (+, -, *, /)")
public String calculate(
@ToolParam(description = "First operand") double a,
@ToolParam(description = "Arithmetic operator: +, -, *, /") String operator,
@ToolParam(description = "Second operand") double b) {
double result = switch (operator) {
case "+" -> a + b;
case "-" -> a - b;
case "*" -> a * b;
case "/" -> b != 0 ? a / b : Double.NaN;
default -> throw new IllegalArgumentException("Unknown operator: " + operator);
};
return String.format("%s %s %s = %s", a, operator, b, result);
}
@Tool(description = "Query weather information for a city (mock data for demo)")
public String queryWeather(
@ToolParam(description = "City name, e.g. Beijing, Shanghai") String city) {
// 实际项目中这里可以调用真实的天气 API
Map<String, String> mockWeather = Map.of(
"Beijing", "Sunny, 25°C, humidity 40%",
"Shanghai", "Cloudy, 22°C, humidity 65%"
);
return String.format("Weather in %s: %s", city,
mockWeather.getOrDefault(city, "Clear, 23°C (default mock data)"));
}
}
注意看,每个方法上面的 @Tool(description = "...") 就是告诉 AI:"嘿,我能干这个事,描述如下。" AI 在需要的时候就会来调用你。
第四步:注册工具(可选)
java
@Configuration
public class McpToolConfig {
@Bean
public ToolCallbackProvider tools(DemoTools demoTools) {
return MethodToolCallbackProvider.builder()
.toolObjects(demoTools)
.build();
}
}
为什么说"可选"? 因为从 Spring AI 1.1.0 开始,注解扫描器默认开启,它会自动发现所有标注了
@Tool的 Spring Bean 方法。但手动注册的好处是:你可以精确控制哪些工具暴露出去。在生产环境中,"能干"和"该干"是两码事。
第五步:配置文件
yaml
server:
port: 8080
spring:
ai:
mcp:
server:
name: mcp-web-demo
version: 1.0.0
protocol: STREAMABLE
启动! ./mvnw spring-boot:run,然后你就拥有了一个运行在 http://localhost:8080/mcp 的 MCP 服务器。
就这?就这。五步搞定,比泡一碗方便面还简单。
核心注解详解
@Tool ------ 工具的身份证
@Tool 注解标记一个方法为 MCP 工具,AI 可以通过 MCP 协议调用它。
java
@Tool(description = "这里写工具的功能描述,AI 靠这段话决定什么时候调用你")
public String myTool() { ... }
| 属性 | 类型 | 必填 | 说明 |
|---|---|---|---|
description |
String | 是 | 工具功能描述,越清晰越好。这是 AI 理解你工具用途的唯一依据 |
name |
String | 否 | 工具名称,默认使用方法名。建议用 camelCase |
returnDirect |
boolean | 否 | 是否将结果直接返回给用户而非 AI。默认 false |
description 写法的艺术:
java
// 反面教材 ------ AI:这啥?能干啥?我要不要用?算了不用了
@Tool(description = "process data")
// 正面教材 ------ AI:哦!查天气用这个!
@Tool(description = "Query real-time weather information for a given city name, "
+ "returns temperature, humidity, and weather conditions")
description 是你和 AI 之间的"契约"。写得越清楚,AI 越知道什么时候该用、什么时候不该用。把它当成 Javadoc 来写就对了------只不过你的读者从人类变成了 AI。
@ToolParam ------ 参数的说明书
@ToolParam 为方法参数添加描述,帮助 AI 理解该传什么值。
java
public String calculate(
@ToolParam(description = "First operand") double a,
@ToolParam(description = "Arithmetic operator: +, -, *, /") String operator,
@ToolParam(description = "Second operand") double b
) { ... }
| 属性 | 类型 | 必填 | 说明 |
|---|---|---|---|
description |
String | 推荐 | 参数含义描述 |
required |
boolean | 否 | 是否必填,默认 true |
小贴士:
@ToolParam不是必须的------不加的话,AI 只能看到参数名和类型,靠猜。但你想让 AI 猜你的意思吗?
支持的参数类型
Spring AI 会自动将参数转换为 JSON Schema,以下类型开箱即用:
| Java 类型 | JSON Schema 类型 | 示例 |
|---|---|---|
String |
string | "hello" |
int / Integer / long / Long |
integer | 42 |
double / Double / float / Float |
number | 3.14 |
boolean / Boolean |
boolean | true |
List<T> |
array | [1, 2, 3] |
Map<String, T> |
object | {"key": "value"} |
| POJO | object (展开所有字段) | {"name": "test", "age": 18} |
enum |
string (带 enum 约束) | "MONDAY" |
返回值约定
方法可以返回任意类型,Spring AI 会自动序列化为 JSON 字符串发给 AI。但有几个最佳实践:
java
// 推荐:返回 String,格式可控
@Tool(description = "...")
public String myTool() {
return "结果清晰明了";
}
// 也行:返回对象,自动 JSON 序列化
@Tool(description = "...")
public UserInfo getUser(Long id) {
return userService.findById(id); // 会被序列化为 {"name": "...", "age": ...}
}
// 不推荐:返回 void ------ AI 收到空响应会很困惑
@Tool(description = "...")
public void doSomething() { ... } // AI:???干完了没?结果呢?
配置参数大全
这是本文最"枯燥"但最有用的部分。泡杯茶,我们一个一个说。
最小化配置 ------ 三行就能跑
先上结论,一个 MCP Server 真正需要配的只有三行:
yaml
spring:
ai:
mcp:
server:
name: mcp-web-demo # 你是谁
version: 1.0.0 # 你是哪个版本
protocol: STREAMABLE # 你用什么协议说话
没了?没了。其余全有默认值。
- 端口?默认
8080- 端点路径?默认
/mcp- Tool 能力?默认开启
- 注解扫描?默认开启
- 服务器类型?默认同步
Spring Boot 的哲学:能不让你写的,绝不让你多写一个字。
对应的最小 pom.xml 依赖也只有一个:
xml
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
</dependency>
就这个组合,mvn spring-boot:run 一敲,http://localhost:8080/mcp 就活了。
如果你想更完整地了解"还能配什么",请继续往下看。
完整配置树
yaml
spring:
ai:
mcp:
server:
# =============================================
# 基础配置 ------ 你的 MCP 服务器的"名片"
# =============================================
name: mcp-web-demo # [必填] 服务名称
version: 1.0.0 # [必填] 服务版本
type: SYNC # 服务器类型,默认 SYNC
protocol: STREAMABLE # 传输协议,默认 SSE(但推荐 STREAMABLE)
# =============================================
# 能力声明 ------ 告诉客户端 "我能干什么"
# =============================================
capabilities:
tool: true # 工具能力 (默认 true)
resource: true # 资源能力 (默认 true)
prompt: true # 提示词能力 (默认 true)
completion: true # 补全能力 (默认 true)
# =============================================
# 变更通知 ------ 当服务端内容变化时主动推送
# =============================================
tool-change-notification: true
resource-change-notification: true
prompt-change-notification: true
# =============================================
# Streamable HTTP 配置
# =============================================
streamable-http:
mcp-endpoint: /mcp # MCP 端点路径 (默认 /mcp)
disallow-delete: false # 禁止客户端 DELETE 请求 (默认 false)
keep-alive-interval: 30s # 长连接保活间隔 (默认无)
# =============================================
# SSE 配置(已废弃,不推荐使用)
# =============================================
sse-message-endpoint: /mcp/message
base-url:
# =============================================
# 注解扫描器
# =============================================
annotation-scanner:
enabled: true # 自动扫描 @Tool 注解 (默认 true)
逐项解析
name & version
yaml
name: mcp-web-demo
version: 1.0.0
这两个是你 MCP 服务器的"身份证"。客户端连接时能看到这些信息,方便调试和管理。
建议 version 跟着你项目的实际版本走,别一直写 1.0.0 然后功能已经迭代到第 37 版了。(别问我怎么知道的。)
type ------ 同步 vs 异步
yaml
type: SYNC # 或 ASYNC
| 类型 | 底层实现 | 适用场景 |
|---|---|---|
| SYNC | McpSyncServer |
大多数场景。工具执行快、逻辑简单 |
| ASYNC | McpAsyncServer |
工具执行耗时长(如调用外部 API、数据库大查询) |
选择困难症患者请看这里: 用
SYNC。如果你不确定要不要用ASYNC,那你就不需要用ASYNC。当你的工具开始因为超时被 AI 抱怨的时候,再切过去也来得及。
protocol ------ 传输协议
yaml
protocol: STREAMABLE # 或 SSE
这是 1.1.0 最重要的新配置项。详见下一章 传输协议的前世今生。
capabilities ------ 能力声明
yaml
capabilities:
tool: true # Tool:让 AI 调用你的方法
resource: true # Resource:向 AI 暴露数据(文件、数据库内容等)
prompt: true # Prompt:提供预定义的 prompt 模板
completion: true # Completion:提供自动补全建议
MCP 协议定义了四种能力。对于大多数项目,你只用到 tool。但框架默认全开,因为开着又不收费。
如果你确定不用某个能力,关掉它可以减少握手时的信息交换量(虽然这点优化约等于没有):
yaml
capabilities:
tool: true
resource: false # 不提供资源
prompt: false # 不提供 prompt 模板
completion: false # 不提供补全
变更通知
yaml
tool-change-notification: true
resource-change-notification: true
prompt-change-notification: true
当服务端的 Tool/Resource/Prompt 发生变化时(比如热更新了一个新工具),主动通知客户端刷新。在开发阶段很有用,生产环境中如果你的工具列表不会动态变化,可以关掉。
streamable-http 配置块
yaml
streamable-http:
mcp-endpoint: /mcp # 端点路径
disallow-delete: false # 是否禁止 DELETE
keep-alive-interval: 30s # 保活间隔
mcp-endpoint :MCP 服务的 URL 路径。改成 /api/mcp 也行,客户端连接时对应改就好。
disallow-delete :MCP 协议中,客户端可以发送 DELETE 请求来关闭会话。如果你出于安全考虑不想让客户端主动断开,设为 true。
keep-alive-interval :对于长时间运行的 SSE 连接,定期发送心跳包防止连接被代理或防火墙切断。支持 Duration 格式:30s、1m、PT30S 都行。
annotation-scanner
yaml
annotation-scanner:
enabled: true
这个配置决定了 Spring AI 是否自动扫描所有 Spring Bean 中的 @Tool 方法并注册为 MCP 工具。
设为 false 的话,你必须手动通过 ToolCallbackProvider Bean 注册工具。适合那些对安全性要求极高、想要精确控制暴露面的场景。
传输协议的前世今生
MCP 支持三种传输方式,每种都有自己的"人设":
stdio ------ 本地青梅竹马
css
[AI 客户端] --stdin/stdout--> [你的 Java 进程]
- 通过标准输入/输出通信
- 客户端负责启动和管理你的进程
- 优点:简单、零网络配置
- 缺点:只能本地用,无法远程共享
- 人话:AI 直接把你的程序跑起来,然后俩人通过管道悄悄话
bash
# Claude Code 配置 stdio 模式
claude mcp add my-tool -- java -jar my-tool.jar
SSE (Server-Sent Events) ------ 曾经的浪漫
css
[AI 客户端] --HTTP POST--> [服务器] --SSE stream--> [AI 客户端]
- 客户端用 POST 发请求,服务器用 SSE 流式返回
- 优点:支持远程部署、支持流式响应
- 缺点:单向流、代理兼容性差、连接容易断
SSE 就像短信时代的异地恋------能通信但很不稳定,动不动就断线。
Spring AI 1.1.0 起,SSE 已被标记为 deprecated。 它还能用,但官方不推荐了。
Streamable HTTP ------ 当代最优解
css
[AI 客户端] <--HTTP--> [服务器]
- 使用标准 HTTP 协议,支持请求/响应和流式两种模式
- 优点:兼容所有 HTTP 基础设施(代理、CDN、负载均衡、API 网关)
- 缺点:想了半天,没有
Streamable HTTP 就像 4G 网络时代的视频通话------稳定、双向、随时在线。
这就是为什么本项目选择了 protocol: STREAMABLE。
三种协议对比总结
| 特性 | stdio | SSE | Streamable HTTP |
|---|---|---|---|
| 部署方式 | 本地进程 | 远程服务 | 远程服务 |
| 网络要求 | 无 | 需要 HTTP | 需要 HTTP |
| 代理/CDN 兼容 | N/A | 差 | 优 |
| 双向通信 | 是 | 半双工 | 是 |
| 流式响应 | 是 | 是 | 是 |
| Spring AI 支持版本 | 1.0.0+ | 1.0.0+ (deprecated) | 1.1.0+ |
| 推荐度 | 本地开发用 | 请迁移 | 生产推荐 |
进阶玩法
1. 注入 Spring 生态的一切
@Tool 方法所在的类就是普通的 Spring Bean,这意味着你可以注入任何 Spring 管理的组件:
java
@Service
public class DatabaseTools {
private final JdbcTemplate jdbcTemplate;
private final RedisTemplate<String, String> redis;
private final UserRepository userRepo;
public DatabaseTools(JdbcTemplate jdbcTemplate,
RedisTemplate<String, String> redis,
UserRepository userRepo) {
this.jdbcTemplate = jdbcTemplate;
this.redis = redis;
this.userRepo = userRepo;
}
@Tool(description = "Execute a read-only SQL query against the database")
public String queryDatabase(
@ToolParam(description = "SQL SELECT statement") String sql) {
// 注意:生产环境一定要做 SQL 注入防护!
List<Map<String, Object>> results = jdbcTemplate.queryForList(sql);
return results.toString();
}
@Tool(description = "Look up user information by username")
public String findUser(
@ToolParam(description = "Username to search for") String username) {
return userRepo.findByUsername(username)
.map(User::toString)
.orElse("User not found: " + username);
}
}
Spring 全家桶(JPA、MyBatis、Redis、Kafka、Elasticsearch......)都是你的弹药库。MCP 工具只是个入口,背后可以连接整个企业级后端。
2. 多工具类组织
当工具多了,别都塞在一个类里:
java
// 按领域拆分
@Service public class TimeTools { ... } // 时间相关
@Service public class WeatherTools { ... } // 天气相关
@Service public class DatabaseTools { ... } // 数据库操作
@Service public class FileTools { ... } // 文件操作
手动注册时在 Config 类中列举:
java
@Bean
public ToolCallbackProvider tools(TimeTools time, WeatherTools weather,
DatabaseTools db, FileTools file) {
return MethodToolCallbackProvider.builder()
.toolObjects(time, weather, db, file)
.build();
}
或者开启注解扫描器(默认开启),什么都不用配。框架自动帮你收集所有 @Tool 方法。
3. 异步工具处理
对于耗时操作,使用 ASYNC 模式:
yaml
spring.ai.mcp.server.type: ASYNC
java
@Tool(description = "Generate a complex report (may take a while)")
public String generateReport(
@ToolParam(description = "Report type") String type) {
// 这个操作可能耗时 30 秒
return reportService.generate(type);
}
4. 安全防护
MCP 服务器暴露在网络上时,安全性至关重要:
java
// 通过 Spring Security 或自定义 Filter 实现认证
@Component
public class McpAuthFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) {
String token = request.getHeader("Authorization");
if (!isValid(token)) {
response.setStatus(401);
return;
}
chain.doFilter(request, response);
}
}
客户端配置(.mcp.json):
json
{
"mcpServers": {
"my-server": {
"type": "http",
"url": "http://localhost:8080/mcp",
"headers": {
"Authorization": "Bearer my-secret-key"
}
}
}
}
常见踩坑实录
坑 1:Java 版本不对
go
error: switch expressions are not supported in -source 17
Spring AI 1.1.x 推荐 Java 21。如果你用了 switch 表达式等新语法,确保 pom.xml 里:
xml
<java.version>21</java.version>
坑 2:用了 SSE Starter 却配了 STREAMABLE
yaml
# pom.xml 里引的是 spring-ai-starter-mcp-server-webmvc
# 但 application.yml 里写了
protocol: SSE # 这个组合是可以的
# 反过来,用 stdio starter 配 STREAMABLE ------ 启动直接报错
规则: WebMvc/WebFlux Starter 支持 SSE 和 STREAMABLE。stdio Starter 只支持 stdio。别混搭。
坑 3:@Tool 方法没被发现
检查清单:
- 方法所在类是否是 Spring Bean(有
@Service、@Component等注解)? annotation-scanner.enabled是否被意外设为false?- 如果用手动注册,
MethodToolCallbackProvider.builder().toolObjects(...)里是否遗漏了你的 Bean?
坑 4:description 写得太模糊
java
// AI 完全不知道什么时候该用这个工具
@Tool(description = "do something")
// AI:你是在为难我
description 是 AI 识别和调用你工具的唯一线索。 想象你在写一份招聘启事------不写清楚岗位职责,谁知道要不要来应聘?
坑 5:返回值太大
如果你的工具返回了一个巨大的 JSON(比如几万条数据库记录),AI 的上下文窗口会被撑爆。
java
// 反面教材
@Tool(description = "Query all users")
public List<User> getAllUsers() {
return userRepo.findAll(); // 返回 10 万条记录,AI 当场去世
}
// 正面教材
@Tool(description = "Query users with pagination")
public List<User> getUsers(
@ToolParam(description = "Page number, starting from 0") int page,
@ToolParam(description = "Page size, max 50") int size) {
return userRepo.findAll(PageRequest.of(page, Math.min(size, 50))).getContent();
}
写在最后
Spring AI 让 MCP Server 的开发变得前所未有的简单:
- 一个 Starter 搞定所有依赖
- 两个注解 (
@Tool+@ToolParam)定义工具 - 三行配置 启动服务
从某种意义上说,写一个 MCP Server 比写一个 REST Controller 还简单------你甚至不需要操心 URL 映射、请求序列化、响应格式这些破事。
AI 不再只是聊天的搭子,而是你应用的调用者。 而 Spring AI 让这个过程变得像写普通业务代码一样自然。
去吧,写你的第一个 MCP 工具,让 AI 替你打工。
本文档基于 Spring AI 1.1.4 + Spring Boot 3.4.4 编写,配套项目源码就在本仓库中。 如果这篇文档帮到了你,给个 Star,不帮到你......那就提个 Issue 来骂我吧。