
去年底一个同事问我,怎么让 Claude 直接查我们系统的订单状态。我的第一反应是:写个 Function Calling,把接口描述塞进 Prompt 里,让模型按格式返回参数,再在业务层拦截调用。折腾了两天,跑通了,但代码丑得不忍看——换一个模型就要重新适配一套格式,换 Cursor 就要再折腾一遍。
后来发现 MCP,五分钟把同一个接口接上了 Claude Desktop,Cursor 里也直接可用,代码就加了三个注解。
这篇把这个过程完整写出来,代码可以直接复制跑。
两个技术经常被混为一谈,但本质上解决的不是同一个问题。
Function Calling 是 Prompt 级别的能力。你在请求里告诉模型「有这几个函数,参数格式如下」,模型决定要不要调用,返回一个结构化调用指令,你的代码去执行,再把结果塞回 Prompt。整个过程绑定在一次模型请求里,工具描述随 Prompt 走,换个模型得重写一遍格式,换个 AI 客户端得重新适配一套。
MCP(Model Context Protocol)是 协议级别的能力。你的服务独立部署成一个 MCP Server,任何支持 MCP 协议的客户端——Claude Desktop、Cursor、自己写的 Agent——都能发现和调用你的工具,不依赖具体模型,一次开发到处复用。
Anthropic 在 2024 年 11 月开源 MCP 规范,目前 Claude、ChatGPT、VS Code Copilot、Cursor、Gemini 等主流 AI 工具都已支持,已有 5000+ 个公开 MCP Server。可以理解为 AI 工具调用的 USB-C 接口:以前每根线的接口不一样,现在统一了。

Function Calling vs MCP 架构对比示意图
MCP 协议里有三种原语:Tool(AI 可以执行的操作,有副作用)、Resource(AI 可以读取的数据,幂等只读)、Prompt(预置的提示词模板)。Spring AI 为三者都提供了对应注解。
Spring AI 1.0.0-M6 开始提供 MCP Server Starters,根据传输协议选一个(协议怎么选后面讲):
<!-- 生产首选:WebMVC + SSE 协议,支持多客户端同时连接 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
</dependency>
<!-- 响应式技术栈用这个 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webflux</artifactId>
</dependency>
<!-- 纯本地工具,不需要网络,Claude Desktop 用子进程方式拉起 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server</artifactId>
</dependency>
BOM 版本管理(推荐,避免版本冲突):
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>1.0.0-M7</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
前置要求:Java 17+,Spring Boot 3.2+。
在任何 Spring Bean 的方法上加 @McpTool,Spring AI 自动扫描注册成 MCP Tool,JSON Schema 也是自动生成的,不需要手写。
实际例子,把订单查询接口暴露出去:
import org.springframework.ai.mcp.spring.annotation.McpTool;
import org.springframework.ai.mcp.spring.annotation.McpToolParam;
import org.springframework.stereotype.Component;
@Component
publicclass OrderMcpTools {
privatefinal OrderService orderService;
public OrderMcpTools(OrderService orderService) {
this.orderService = orderService;
}
@McpTool(
name = "queryOrder",
description = "根据订单号查询订单状态和详情,支持查询最近 90 天内的订单。" +
"返回字段包括:订单状态、实付金额、下单时间、收货地址、物流单号。"
)
public OrderDetail queryOrder(
@McpToolParam(description = "订单号,格式为 ORD-XXXXXXXXXX,10 位数字后缀", required = true)
String orderId,
@McpToolParam(description = "是否返回商品明细列表,默认 false,true 时额外返回每个商品的名称和数量")
boolean includeItems
) {
// 直接复用现有业务逻辑,不需要改一行
return orderService.getOrderDetail(orderId, includeItems);
}
@McpTool(
name = "listRecentOrders",
description = "查询用户最近的订单列表,返回最多 20 条,按下单时间倒序排列"
)
public List<OrderSummary> listRecentOrders(
@McpToolParam(description = "用户 ID,Long 类型", required = true) Long userId,
@McpToolParam(description = "查询天数范围,1-90 之间,默认 30") int days
) {
return orderService.listRecentOrders(userId, days);
}
}
description 的写法很关键——这是模型决定「要不要调用这个工具、传什么参数」的主要依据,不是给开发者看的注释。「订单号,格式为 ORD-XXXXXXXXXX,10 位数字后缀」比「the order ID」有用得多,模型在推断参数时会参考这个描述。
application.yml 里开启注解扫描:
spring:
ai:
mcp:
server:
type: SYNC # SYNC 同步 / ASYNC 响应式
protocol: SSE # 传输协议,后面详细讲
annotation-scanner:
enabled: true
正常启动 Spring Boot 应用,MCP Server 就跑起来了。

Spring AI MCP Server 内部架构图
踩坑记录:返回值必须可 JSON 序列化。如果 OrderDetail 里有 LocalDateTime,要确保 Jackson 加了时间模块(jackson-datatype-jsr310),否则模型收到的是序列化异常而不是工具执行结果。更隐蔽的是 Optional<T> 类型,Jackson 默认不处理,直接返回 T 或 null 比较稳。
Spring AI MCP Server 支持三种传输协议,选错了要么本地能用生产跑不了,要么部署方式不对:
协议 | 适用场景 | Starter 依赖 | yml 配置 |
|---|---|---|---|
STDIO | 本地工具、Claude Desktop 直接拉起进程 | spring-ai-starter-mcp-server | stdio: true |
SSE | 生产部署,多 AI 客户端同时连接 | spring-ai-starter-mcp-server-webmvc | protocol: SSE |
Streamable-HTTP | 云原生、K8s 横向扩容、负载均衡场景 | spring-ai-starter-mcp-server-webmvc | protocol: STREAMABLE |
STDIO 最容易踩的坑:Claude Desktop 用 STDIO 协议时,是把你的 Spring Boot 应用当成子进程直接拉起的。
这意味着服务不能有额外的 HTTP 端口监听(会报冲突),而且绝对不能用 System.out.println——标准输出被 MCP 协议占用,乱写会直接污染消息帧,导致工具调用解析失败。把所有日志配到文件或 stderr 就好:
<!-- logback.xml:STDIO 模式下把控制台输出全部转到 STDERR -->
<appender name="STDERR" class="ch.qos.logback.core.ConsoleAppender">
<target>System.err</target>
<encoder>
<pattern>%d{HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
生产环境用 SSE:保持持久 HTTP 连接,服务端可以主动推送工具执行进度。多个 AI 客户端可以同时连,服务正常作为 Spring Boot 应用独立部署,和现有 REST 接口共用同一个端口。
Streamable-HTTP 适合无状态部署:每次请求独立,没有持久连接,K8s 横向扩容时不需要考虑连接亲和性。如果你的服务部署在有负载均衡的环境里,优先考虑这个协议。
STDIO 模式(本地调试最方便):
找到 Claude Desktop 配置文件——macOS 在 ~/Library/Application Support/Claude/claude_desktop_config.json,Windows 在 %APPDATA%\Claude\claude_desktop_config.json——加入:
{
"mcpServers": {
"order-service": {
"command": "java",
"args": [
"-jar",
"/absolute/path/to/your-mcp-server.jar"
]
}
}
}
SSE 模式(服务已独立部署):
{
"mcpServers": {
"order-service": {
"url": "http://localhost:8080/sse"
}
}
}
如果服务需要鉴权,在请求头里带 Token:
{
"mcpServers": {
"order-service": {
"url": "http://your-server/sse",
"headers": { "Authorization": "Bearer your-token" }
}
}
}
重启 Claude Desktop 后,工具栏会出现锤子图标,点开能看到你注册的所有 MCP Tool 名称和描述。直接在对话框里问「帮我查一下订单 ORD-20260425001 的状态」,Claude 会自动识别出需要调用 queryOrder 工具,填入参数,执行,再把结果转成自然语言回复。

Claude Desktop 调用 MCP Server 完整时序图
@McpTool 适合「执行操作」(下单、更新状态),@McpResource 适合「读取数据」(商品详情、配置项)。区别在语义和使用时机:Resource 是幂等只读的,AI 客户端在构建上下文时会主动拉取,而不是等到要执行操作时才调用。
@Component
publicclass ProductMcpResources {
@McpResource(
uri = "product://{productId}/info",
name = "商品信息",
description = "根据商品 ID 获取商品基本信息,包括价格、库存状态、分类"
)
public String getProductInfo(String productId) {
Product product = productService.findById(productId);
// Resource 返回字符串,JSON 格式或结构化文本都行
return objectMapper.writeValueAsString(product);
}
@McpResource(
uri = "config://feature-flags",
name = "功能开关配置",
description = "返回当前所有功能开关的状态,供 AI 了解系统当前支持的能力"
)
public String getFeatureFlags() {
return featureFlagService.getAllFlags().toString();
}
}
实际用下来,@McpResource 在「让 AI 了解你的系统状态」这个场景特别有用。比如先让 AI 读一遍功能开关配置,再决定推荐哪些操作,逻辑上比每次调 Tool 干净。
Q:现有服务改造需要动多少代码?
加依赖、写一个 @Component 包装类调用现有 Service,在方法上加注解。不需要修改任何现有业务逻辑,原来的 REST 接口照常工作,两套并存,互不影响。
Q:description 怎么写才有效?
受众是 AI 模型,不是开发者。要说清楚:工具做什么、适合什么场景、参数有什么限制(格式、范围、枚举值、默认值)。「查询订单」比「根据订单号查询,仅支持 90 天内,返回状态和物流信息」少了太多有效信息,模型填参数时更容易出错。
Q:和 Spring AI 的 FunctionCallback 是什么关系?
完全独立的两套机制。FunctionCallback 是你自己写 AI 应用时,给模型 Prompt 绑定工具(Function Calling);@McpTool 是把你的服务暴露给外部 AI 客户端(MCP 协议)。可以同时用——比如你的服务既是一个 MCP Server 供外部客户端调,内部同时用 FunctionCallback 驱动自己的业务 AI 流程。
Q:STDIO 和 SSE 模式下日志怎么配?
STDIO 模式下 System.out 会污染 MCP 消息流,必须把所有日志重定向到文件或 stderr,把 logback.xml 里的 Console Appender target 改成 System.err。SSE 模式没这个限制,按正常配置就好。
Q:生产环境需要鉴权吗?
MCP 协议本身没有内置鉴权,默认任何人都能调你的 MCP Server。生产环境建议在 Spring Security 层加 HTTP Bearer Token 或 API Key 验证,然后在客户端配置文件里的 headers 字段带上凭证。
MCP 现在还在快速演进,Streamable-HTTP 是最近才定稿的传输协议,比 SSE 更适合云原生无状态部署场景。Spring AI 的注解 API 在各个 milestone 版本之间有调整,遇到问题先确认用的版本和文档是否对应。
说到底,MCP 解决的核心问题是「工具定义和模型解耦」——你写一次,任何支持 MCP 的 AI 客户端都能用,不用跟着每个模型的格式调整。这个方向是对的,生态成熟度也在快速追上来。
下一篇打算写 MCP 的认证鉴权实战,以及多个 MCP Server 组合的 Agent 编排——这两块坑比较多,整理好了会推送。如果身边有同事在做 AI 工具接入的选型,这篇可以直接转给他,省得从头翻文档。