当多个 AI 应用都需要访问数据库、业务接口或文件系统时,为每组应用与工具分别编写适配层很快会带来重复开发和维护压力。MCP 通过统一协议连接工具提供方与调用方。接下来将结合 Spring AI 和一个电商查询案例,从协议原理入手,逐步完成服务端暴露、客户端发现与模型自动调用。
面向 Java 开发者的 MCP(Model Context Protocol)完整指南:先用「理论」把协议本质讲透,再用一个电商订单/商品查询 MCP Server 走完「提供工具 → 本地调用 → 第三方调用」全链路。
版本:Spring AI
1.1.2· Spring AI Alibaba1.1.2.2· Spring Boot3.5.9· JDK21+构建:Maven(spring-ai-alibaba-bom统一版本管理) 模型:阿里云百炼(DashScope)通义千问qwen-plus(客户端侧,服务端无需模型) 场景:电商「订单查询 / 商品搜索」
你写了一个 Spring Boot 应用,想让大模型能「查数据库、调接口、操作文件」——但每个外部能力都单独写一遍适配,N 个应用 × M 个工具,适配代码爆炸。
MCP 就是干这个的:把「外部工具/数据源」统一成一套标准协议。工具方实现一次(Server),任何 AI 应用都能按协议调用(Client),一次实现、到处接入。这篇文章教你把这件事用 Spring AI 完整落地——从「协议是什么」到「怎么写 Server 暴露工具」,再到「本地和第三方两类客户端怎么调」。
@Tool),想知道「MCP 和它什么关系、什么时候该用 MCP」的人;不需要你懂协议细节,会写 Java 就行——Spring AI 已经把 JSON-RPC 握手、消息编解码全部封装好了。
整篇文章走一条主线——MCP 的「提供」与「调用」两侧:
提供服务端:业务逻辑 → @McpTool 注解 → 自动暴露为 MCP 工具(stdio / HTTP 两种传输)
调用客户端:Spring Boot / Claude Desktop / Claude Code → 通过协议发现工具 → LLM 自动调用
所有代码围绕同一个例子(后文反复用到):
一个电商 MCP Server 暴露两个工具:
get_order(按订单号查订单)、search_products(按关键字搜商品)。你会在服务端用注解把它暴露出去,再在客户端让大模型「自己决定」调哪个工具来回答用户问题。
| 部分 | 章节 | 一句话 | 适合谁 |
|---|---|---|---|
| 前言 | 快速上手 | 15 分钟跑通最小闭环 | 所有人先读这个 |
| 第一部分 | 第 1 章 MCP 是什么 | 协议本质、价值、选型 | 想搞懂「为什么」的人 |
| 第一部分 | 第 2 章 架构与核心概念 | 角色、原语、传输层 | 必读,理解黑盒 |
| 第一部分 | 第 3 章 生命周期与一次调用 | 握手、协商、消息流 | 想看协议报文的人 |
| 第二部分 | 第 4 章 项目准备 | 环境、工程结构、数据模型 | 动手前必读 |
| 第二部分 | 第 5 章 MCP Server:提供工具 | 用 @McpTool 暴露工具 | 提供方必读 |
| 第二部分 | 第 6 章 MCP Client:调用工具 | 本地 + 第三方两类调用 | 调用方必读 |
| 第二部分 | 第 7 章 其它客户端调用 | Claude Desktop / Code / curl | 要跨应用接入必读 |
| 第三部分 | 第 8 章 最佳实践 | 安全、容错、第三方治理 | 要上线必读 |
| 第三部分 | 第 9 章 高频坑速查 | 一张表对症状 | 出问题查这里 |
| 附录 | 速查表 | 依赖 / 配置 / 注解 / 术语 | 遇到不熟的词 |
完整版见附录 · 术语表。先扫一眼,后面遇到不慌。
| 词 | 一句话 |
|---|---|
| MCP | 模型上下文协议:把「外部工具/数据源」统一成一套标准的 JSON-RPC 接口 |
| MCP Server | 工具提供方:暴露 tools / resources / prompts |
| MCP Client | 工具调用方:连接 Server、发现并调用工具 |
| MCP Host | 承载 AI 对话的宿主(Claude Desktop、你的 Spring Boot 应用) |
| tools / resources / prompts | 三大原语:可调用的函数 / 只读数据 / 提示词模板 |
| stdio | 本地子进程传输(stdin/stdout 通信) |
| Streamable HTTP | 基于 HTTP 的远程传输,生产推荐 |
| JSON-RPC 2.0 | MCP 的消息格式(请求 / 响应 / 通知) |
| Function Calling | 模型输出「调哪个函数 + 传什么参」,由应用执行 |
目标:先看到结果,再回头理解原理。 这里只写「最小能跑通」的代码——一个 Server 暴露工具、一个 Client 让大模型调用它。完整版见第 5、6 章。
只需要一个云端 API Key(客户端调模型用),服务端连 Key 都不用:
# ① 阿里云百炼:创建 API Key(https://bailian.console.aliyun.com/ → API-KEY 管理)
# ② 写入环境变量(Windows 用 setx,Linux/macOS 用 export)
setx DASHSCOPE_API_KEY "sk-xxxxxxxx"
用
qwen-plus跑通只要几分钱,首次开通一般有免费额度。
新建 ecommerce-mcp-server 项目,pom 只加一个依赖 spring-ai-starter-mcp-server-webmvc(完整 pom 见第 5 章),然后写这个工具类:
// 一个 @McpTool 注解,方法就变成了一个可被远程调用的 MCP 工具
@Component
public class EcommerceTools {
@McpTool(name = "get_order", description = "根据订单号查询订单详情")
public Order getOrder(@McpToolParam(description = "订单号") String orderId) {
return new Order(orderId, "U1001", "已发货",
List.of(new OrderItem("P1001", "华为 Mate 60 Pro", 1, 6999.00)), 6999.00);
}
}
application.yml 一行启动为 HTTP MCP Server:
spring:
ai:
mcp:
server:
protocol: STREAMABLE # 默认端点 POST /mcp
启动:mvn spring-boot:run,服务在 http://localhost:8080/mcp 等你调。
新建 ecommerce-mcp-client 项目,pom 加两个依赖(模型 + MCP 客户端),application.yml 里「连上」刚才的 Server:
spring:
ai:
dashscope:
api-key: ${DASHSCOPE_API_KEY}
ch@t:
options:
model: qwen-plus
mcp:
client:
toolcallback:
enabled: true # 关键:把 MCP 工具自动转成可被 LLM 调用的 ToolCallback
streamable-http:
connections:
ecommerce:
url: http://localhost:8080 # 连上 MCP Server
然后写一个 CommandLineRunner,把 MCP 工具挂到 ChatClient 上:
@Bean
CommandLineRunner demo(ChatClient.Builder builder, ToolCallbackProvider mcpTools) {
return args -> {
String answer = builder.build().prompt()
.user("帮我查订单 2024090100000001 的物流状态")
.toolCallbacks(mcpTools) // 注入 MCP 工具,LLM 自己决定调不调、调哪个
.call()
.content();
System.out.println(answer);
};
}
先启动服务端,再启动客户端:
# 终端 1:启动 Server
cd ecommerce-mcp-server && mvn spring-boot:run
# 终端 2:启动 Client
cd ecommerce-mcp-client && mvn spring-boot:run
订单 2024090100000001 当前状态为「已发货」,包含 1 件商品:华为 Mate 60 Pro,金额 6999.00 元。
看到这个,你已经跑通了 MCP 的完整链路:Server 用 @McpTool 暴露工具 → Client 通过协议发现工具 → 大模型「看懂」工具描述后自己决定调 get_order → 拿到结果再组织语言回答。
这段代码是「本地 HTTP 调用」的最小形态。后面会把它拆开讲透,并补齐你真正需要的:
| 你现在要解决的问题 | 去哪看 |
|---|---|
MCP 到底解决了什么问题、和 @Tool 什么区别 | 第 1 章 |
| 协议内bu长什么样(角色、原语、传输、握手) | 第 2、3 章 |
| 服务端怎么完整暴露 tools / resources / prompts | 第 5 章 |
| 怎么调第三方现成的 MCP Server(stdio / HTTP) | 第 6 章 |
| Claude Desktop / Claude Code 怎么接 | 第 7 章 |
| 上线要注意的安全、容错 | 第 8 章 |
本章解决什么问题:让你在动手前,搞懂 MCP 是什么、为什么需要它、以及它和已经会的 Function Calling 是什么关系。读完这章,你能判断「我的场景到底该不该上 MCP」。
在 MCP 出现之前,一个 AI 应用要接入 N 个外部工具/数据源,每个工具都要单独写一次适配代码;而 M 个工具要服务 N 个 AI 应用,适配工作量就是 N × M:
没有 MCP(每个应用 × 每个工具都要单独适配)
应用A ──► 数据库A 应用A ──► GitHub
应用B ──► 数据库A 应用B ──► GitHub
应用C ──► 数据库A 应用C ──► GitHub
(N × M 条连接,每条都要单独开发)
有了 MCP(统一协议,一次实现到处接入)
应用A ─┐
应用B ─┼── MCP 协议 ──► 数据库 MCP Server / GitHub MCP Server / ...
应用C ─┘
(N + M 条连接,工具侧只实现一次)
MCP(Model Context Protocol,模型上下文协议)是 Anthropic 于 2024 年底开源的开放标准协议,它把「AI 应用获取外部上下文和调用工具」这件事,抽象成一套统一的接口规范。
MCP 之于 AI 应用与工具,就像 USB-C 之于设备与配件:不是某个厂商的私有接口,而是所有厂商共同遵守的通用标准。一个设备(客户端)用 USB-C,就能插任意 USB-C 配件(工具/服务端),而不必为每个配件买转接头。
MCP 的核心价值,就四条:
这是最容易混淆的一点,先在这里讲清楚。两者不冲突,而且终点是同一条链路:
| Function Calling | MCP | |
|---|---|---|
| 工具定义位置 | 应用内部,@Tool 方法 | 外部/第三方服务,独立的 Server 进程 |
| 接入方式 | 编译期就在应用里 | 运行时通过协议发现、握手、调用 |
| 生命周期 | 与应用同生共死 | 独立部署、独立升级、可被多应用共享 |
| 典型场景 | 应用自己的业务函数 | 数据库、GitHub、外部物流/仓储系统等 |
? 在 Spring AI 里,两者的终点是同一件事:MCP 工具经
ToolCallbackProvider自动转成和@Tool完全一样的ToolCallback,最终都参与同一次 Function Calling。换句话说,MCP 是把「外部的工具」统一成「应用内可调用的工具」的标准桥梁。
不是所有工具都要上 MCP,先判断:
| 场景 | 建议 |
|---|---|
| 工具只被自己一个应用用,逻辑简单 | 直接用 @Tool(Function Calling),别上 MCP |
| 工具要被多个应用 / 多语言 / 跨团队复用 | 上 MCP,一次实现到处接入 |
| 接入第三方现成能力(GitHub、数据库、搜索) | 上 MCP,社区有现成 Server 直接连 |
| 想把内部系统封装成「标准工具」对外开放 | 上 MCP,独立部署、独立升级、带鉴权 |
一句话:MCP 的价值在「复用」和「跨边界」。单应用内部的自有函数,
@Tool更省事;要共享、要接第三方、要跨语言,就上 MCP。
MCP 发布后迅速成为事实标准,OpenAI、Google、微软、Anthropic 等都已支持。Spring AI 从 1.0 起深度集成 MCP,到 1.1.2 已支持 tools / resources / prompts 三类原语,以及 stdio / SSE / Streamable HTTP 三种传输。本文后续全部基于 Spring AI 1.1.2 展开。
本章要点回顾
- MCP 把「外部工具/数据源」统一成一套 JSON-RPC 标准接口,把 N×M 的适配降为 N+M。
- 类比 USB-C:一次实现、到处接入;核心价值是解耦、跨语言、复用、标准化上下文。
- MCP 与 Function Calling 不冲突:MCP 工具最终转成
ToolCallback,走同一条调用链路。- 选型:单应用内部函数用
@Tool;要复用/接第三方/跨语言才上 MCP。
本章解决什么问题:把 MCP 的「黑盒」打开,让你看懂它由哪几层组成——三类角色、三大原语、三种传输、一种消息格式。这是后面读 Spring AI 封装、排查问题的基础。
MCP 官方定义了三类角色:
┌─────────────────────────────────────────────────────────────┐
│ MCP Host(宿主,如 Claude Desktop、你的 Spring Boot 应用) │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ MCP Client(客户端,1:1 连接一个 Server) │ │
│ │ · 发起握手、协商能力 │ │
│ │ · 发请求调工具 / 读资源 / 取提示词 │ │
│ └──────────────────────────┬───────────────────────┘ │
└───────────────────────────────┼─────────────────────────────┘
│ JSON-RPC 2.0(传输层:stdio / HTTP)
▼
┌───────────────────────┐
│ MCP Server(服务端) │
│ · 暴露 tools │
│ · 暴露 resources │
│ · 暴露 prompts │
└───────────────────────┘
? 通信是双向的。客户端可以调 Server 的工具,Server 也可以反向请求客户端的能力(如
sampling让宿主调用 LLM)。不过日常开发 99% 的场景只用到「客户端调 Server」,本文也只讲这个方向。
| 原语 | 作用 | 关键方法 | Spring AI 1.1.2 支持 |
|---|---|---|---|
| tools | 可执行的功能(查订单、查数据库、写文件) | tools/list、tools/call | ✅ 完整支持(自动转 ToolCallback) |
| resources | 只读数据源(文件内容、数据库 schema) | resources/list、resources/read | ⚠️ 需手动用 McpSyncClient 读取 |
| prompts | 可复用的提示词模板 | prompts/list、prompts/get | ⚠️ 需手动用 McpSyncClient 读取 |
? tools 是「一等公民」,resources / prompts 是「二等公民」:Spring AI 1.1.2 把 tools 自动转成
ToolCallback参与 Function Calling;而 resources 和 prompts 没有自动集成,需要手动用McpSyncClient读取。第 6 章会分别演示两种用法。
| 传输 | 机制 | 适用场景 | 备注 |
|---|---|---|---|
| stdio | 本地子进程,通过 stdin/stdout 通信 | 本地工具、开发调试 | 最简单;进程生命周期由客户端管理 |
| Streamable HTTP | HTTP POST + 可选 SSE 流 | 远程服务、生产环境 | 1.1 新增的新一代传输,生产推荐 |
| SSE | HTTP + Server-Sent Events | 远程服务(旧方案) | 逐步被 Streamable HTTP 取代 |
选型建议:
MCP 在传输层之上统一使用 JSON-RPC 2.0 作为消息格式。三种消息类型:
| 类型 | 是否带 id | 是否期待响应 | 说明 |
|---|---|---|---|
| Request | ✅(非 null) | ✅ | 期待一个带相同 id 的响应 |
| Response | ✅(与请求相同 id) | — | 包含 result 或 error(二者只存其一) |
| Notification | ❌ | ❌ | 单向通知,无响应 |
三条真实报文示例(对照第 3 章会看到完整握手流程):
// 1. Request:客户端请求「初始化」
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-06-18",
"capabilities": {},
"clientInfo": { "name": "ecommerce-mcp-client", "version": "1.0.0" }
}
}
// 2. Response:服务端返回能力
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2025-06-18",
"capabilities": { "tools": {}, "resources": {}, "prompts": {} },
"serverInfo": { "name": "ecommerce-mcp-server", "version": "1.0.0" }
}
}
// 3. Notification:客户端通知「我已初始化完成」(无 id、无响应)
{ "jsonrpc": "2.0", "method": "notifications/initialized" }
标准 JSON-RPC 错误码:
-32700解析错误、-32600非法请求、-32601方法不存在、-32602参数非法、-32603内部错误;MCP 另加-32002资源不存在等。
本章要点回顾
- 三层角色:Host(宿主)→ Client(1:1 连一个 Server)→ Server(提供工具/资源/提示词)。
- 三大原语:tools(一等公民,自动转 ToolCallback)、resources、prompts(需手动 McpSyncClient)。
- 三种传输:stdio(本地)、Streamable HTTP(生产推荐)、SSE(旧方案)。
- 消息层统一 JSON-RPC 2.0:Request / Response / Notification 三种。
本章解决什么问题:把一次 MCP 连接从建立到调用工具的全过程串起来,让你理解「握手 → 协商 → 发现 → 调用」四步。以后看 Spring AI 的封装,就不会觉得黑盒。
一次 MCP 连接从建立到关闭,经历四个状态:
Created ──(客户端发 initialize)──► Initialized ──(客户端发 notifications/initialized)──► Ready ──(关闭)──► Shutdown
initialize,服务端返回能力;notifications/initialized 确认,进入就绪态,可正常收发业务消息;initialize 的响应里,服务端声明自己能提供什么(capabilities),客户端据此决定调用策略:
| 能力 | 含义 |
|---|---|
tools | 提供可调用工具 |
resources | 提供只读资源 |
prompts | 提供提示词模板 |
logging | 支持日志通知 |
sampling(客户端侧) | 客户端允许服务端反向请求 LLM 采样 |
Spring AI 1.1.2 服务端默认开启 tools / resources / prompts 三项能力。
把第 2.4 节的三条报文串起来,就是一次「连接 → 发现工具 → 调工具」的完整时序:
客户端 服务端
│── initialize ────────────────────────────► │ (1) 握手,协商协议版本与能力
│◄── initialize 响应(含 capabilities)────── │
│── notifications/initialized ─────────────► │ (2) 就绪
│ │
│── tools/list ────────────────────────────► │ (3) 发现:有哪些工具
│◄── 工具列表(name + description + inputSchema)│
│ │
│── tools/call (name=get_order, args=...) ─► │ (4) 调用
│◄── 结果(content 数组,isError 标志)─────── │
│ │
│── 关闭连接 ───────────────────────────────► │ (5) 结束
对应 tools/call 的请求与响应报文:
// 请求
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "get_order",
"arguments": { "orderId": "2024090100000001" }
}
}
// 响应
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"content": [
{ "type": "text", "text": "{"orderId":"2024090100000001","status":"已发货",...}" }
],
"isError": false
}
}
? 理论小结:MCP = JSON-RPC 2.0(消息层)+ tools/resources/prompts(原语)+ stdio/HTTP(传输层)+ initialize 握手(生命周期)。记住这条公式,后面看 Spring AI 的封装就不会觉得黑盒了。
本章要点回顾
- 状态机:Created → Initialized → Ready → Shutdown,握手由
initialize+notifications/initialized完成。- 能力协商:服务端在
initialize响应里声明 capabilities(tools/resources/prompts 等)。- 一次工具调用 = 握手 → 就绪 → tools/list 发现 → tools/call 调用 → 关闭。
本章解决什么问题:动手前把环境、工程结构、数据模型定下来。后面两章(服务端、客户端)的代码都基于这一章的约定。
| 组件 | 版本 | 说明 |
|---|---|---|
| JDK | 21+ | maven.compiler.release=21,record / 模式匹配开箱即用 |
| Spring Boot | 3.5.9 | 与 Spring AI 1.1.x 一一对应 |
| Spring AI | 1.1.2 | MCP 核心(由 Alibaba BOM 锁定) |
| Spring AI Alibaba | 1.1.2.2 | 跟踪 Spring AI 1.1.2,客户端调模型用 |
| 构建 | Maven | spring-ai-alibaba-bom 统一管理版本 |
| 模型 | 阿里百炼 Qwen(qwen-plus) | 仅客户端需要,需自备百炼 API Key |
? 一个关键认知:MCP Server 只「提供工具」,不需要模型、不需要 API Key;模型和 Key 都在 Client 端。这正是「解耦」的体现——工具方和模型方彻底分离。
本文实战拆成服务端和客户端两个 Maven 项目,职责清晰:
docker-hello/mcp/
├── ecommerce-mcp-server/ # ① MCP 服务端:暴露订单/商品工具(无模型依赖)
│ ├── pom.xml
│ └── src/main/
│ ├── java/com/example/mcp/
│ │ ├── EcommerceMcpServerApplication.java
│ │ ├── model/Product.java # 商品(record)
│ │ ├── model/Order.java # 订单(record)
│ │ ├── model/OrderItem.java # 订单明细(record)
│ │ ├── service/EcommerceService.java # 内存数据 + 查询逻辑
│ │ └── mcp/
│ │ ├── EcommerceTools.java # @McpTool 工具
│ │ ├── EcommerceResources.java# @McpResource 资源
│ │ └── EcommercePrompts.java # @McpPrompt 提示词
│ └── resources/application.yml
│
└── ecommerce-mcp-client/ # ② MCP 客户端:Spring Boot 应用消费上面的工具
├── pom.xml
└── src/main/
├── java/com/example/mcp/EcommerceMcpClientApplication.java
└── resources/application.yml
为了让读者无需数据库即可运行,本文用内存 List 种子数据承载订单/商品;生产环境把它换成 MyBatis-Plus + PostgreSQL(对应《AI 应用 - 电商智能客服》里的 Product / Order 业务表),MCP 层代码零改动。
// model/Product.java
public record Product(String id, String name, String category, double price, int stock) {}
// model/Order.java
public record Order(String orderId, String userId, String status,
List<OrderItem> items, double totalAmount) {}
// model/OrderItem.java
public record OrderItem(String productId, String productName, int quantity, double price) {}
本章解决什么问题:把你的业务能力「暴露」成标准 MCP 工具,供任何客户端调用。核心是
@McpTool注解——方法写完、注解标上,工具就自动暴露了,JSON Schema 也自动生成。
服务端用 WebMVC starter(支持 SSE / Streamable HTTP),只需一个依赖,不需要模型、不需要 API Key:
<?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>
<groupId>com.example</groupId>
<artifactId>ecommerce-mcp-server</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>ecommerce-mcp-server</name>
<properties>
<maven.compiler.release>21</maven.compiler.release>
<spring-boot.version>3.5.9</spring-boot.version>
<spring-ai.version>1.1.2</spring-ai.version>
</properties>
<dependencies>
<!-- MCP Server:WebMVC 传输(SSE / Streamable HTTP) -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<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>
<version>${spring-boot.version}</version>
</plugin>
</plugins>
</build>
</project>
? 服务端三种 starter 对照:
spring-ai-starter-mcp-server(stdio)、...-server-webmvc(SSE / Streamable HTTP)、...-server-webflux(响应式)。spring-ai-starter-mcp-server-webmvc会自动引入spring-ai-mcp-annotations,@McpTool等注解开箱即用。
spring:
ai:
mcp:
server:
name: ecommerce-mcp-server # 可选,服务端标识
version: 1.0.0
protocol: STREAMABLE # 服务端传输协议:SSE / STREAMABLE
仅此一行即可把服务启动为 Streamable HTTP MCP Server,默认端点 POST /mcp。
// EcommerceMcpServerApplication.java
package com.example.mcp;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class EcommerceMcpServerApplication {
public static void main(String[] args) {
SpringApplication.run(EcommerceMcpServerApplication.class, args);
}
}
// service/EcommerceService.java —— 内存数据 + 查询逻辑(生产替换为 MyBatis-Plus)
package com.example.mcp.service;
import com.example.mcp.model.Order;
import com.example.mcp.model.OrderItem;
import com.example.mcp.model.Product;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
@Service
public class EcommerceService {
private final List<Product> products = List.of(
new Product("P1001", "华为 Mate 60 Pro", "手机", 6999.00, 128),
new Product("P1002", "iPhone 15 Pro", "手机", 8999.00, 64),
new Product("P2001", "MacBook Air M3", "笔记本", 9499.00, 32),
new Product("P3001", "AirPods Pro 2", "耳机", 1899.00, 200)
);
private final List<Order> orders = List.of(
new Order("2024090100000001", "U1001", "已发货",
List.of(new OrderItem("P1001", "华为 Mate 60 Pro", 1, 6999.00)), 6999.00),
new Order("2024090100000002", "U1002", "待付款",
List.of(new OrderItem("P3001", "AirPods Pro 2", 2, 1899.00)), 3798.00)
);
/** 按订单号查询 */
public Optional<Order> getOrder(String orderId) {
return orders.stream().filter(o -> o.orderId().equals(orderId)).findFirst();
}
/** 按关键字搜索商品(匹配名称或分类) */
public List<Product> searchProducts(String keyword) {
return products.stream()
.filter(p -> p.name().contains(keyword) || p.category().contains(keyword))
.toList();
}
/** 全部商品(作为 resource 暴露) */
public List<Product> allProducts() {
return products;
}
}
@McpTool + @McpToolParam这是本文与 Function Calling 教程里「@Tool」的关键区别——MCP 原生注解 @McpTool 提供更精确的参数级描述:
// mcp/EcommerceTools.java
package com.example.mcp.mcp;
import com.example.mcp.model.Order;
import com.example.mcp.model.Product;
import com.example.mcp.service.EcommerceService;
import org.springframework.ai.mcp.annotation.McpTool;
import org.springframework.ai.mcp.annotation.McpToolParam;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
public class EcommerceTools {
private final EcommerceService service;
public EcommerceTools(EcommerceService service) {
this.service = service;
}
@McpTool(name = "get_order", description = "根据订单号查询订单详情(含状态、商品明细、金额)")
public Order getOrder(@McpToolParam(description = "订单号") String orderId) {
return service.getOrder(orderId)
.orElseThrow(() -> new IllegalArgumentException("订单不存在:" + orderId));
}
@McpTool(name = "search_products", description = "按关键字搜索商品(匹配名称或分类)")
public List<Product> searchProducts(@McpToolParam(description = "搜索关键字,如「手机」「耳机」") String keyword) {
return service.searchProducts(keyword);
}
}
要点:
description 不是注释,是「给 LLM 看的说明书」——LLM 靠它决定「什么时候调这个工具、传什么参数」。写清楚「入参含义 + 返回什么」,工具被正确调用的概率才高。name 省略时默认用方法名;这里显式给蛇形命名,便于跨语言客户端识别。List、基本类型)会自动生成 JSON Schema 作为工具的 inputSchema,无需手写。?
@McpToolvs@Tool:Spring AI 1.1.2 也支持把带@Tool的方法经服务端tool-callback-converter(默认true)自动转成 MCP 工具。但推荐直接用@McpTool——它是 MCP 原生注解,支持参数级@McpToolParam描述,语义更精确。
// mcp/EcommerceResources.java —— 只读资源
package com.example.mcp.mcp;
import com.example.mcp.service.EcommerceService;
import org.springframework.ai.mcp.annotation.McpResource;
import org.springframework.stereotype.Component;
import java.util.stream.Collectors;
@Component
public class EcommerceResources {
private final EcommerceService service;
public EcommerceResources(EcommerceService service) {
this.service = service;
}
@McpResource(description = "当前在售商品目录(JSON 列表)")
public String productCatalog() {
return service.allProducts().stream()
.map(p -> String.format("{"id":"%s","name":"%s","category":"%s","price":%.2f,"stock":%d}",
p.id(), p.name(), p.category(), p.price(), p.stock()))
.collect(Collectors.joining(",", "[", "]"));
}
}
// mcp/EcommercePrompts.java —— 提示词模板
package com.example.mcp.mcp;
import org.springframework.ai.mcp.annotation.McpPrompt;
import org.springframework.ai.mcp.annotation.PromptVariable;
import org.springframework.stereotype.Component;
@Component
public class EcommercePrompts {
@McpPrompt(name = "order_refund_prompt",
description = "生成售后/退货场景的客服话术提示词",
template = "用户要申请退货,涉及订单号 {{orderId}}。请用礼貌、专业的客服语气,说明退货流程和注意事项。")
public String orderRefundPrompt(@PromptVariable("orderId") String orderId) {
return "";
}
}
本地用 Claude Desktop / Claude Code 直连时,把进程作为子进程拉起的 stdio 模式最省事。改用 stdio starter 并关闭一切控制台输出(否则会污染 JSON 报文):
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server</artifactId>
</dependency>
spring.ai.mcp.server.stdio=true
spring.main.banner-mode=off
spring.main.web-application-type=none
logging.pattern.console=
打包后
java -jar target/ecommerce-mcp-server-0.0.1-SNAPSHOT.jar即可被客户端以 stdio 方式拉起。
Streamable HTTP / SSE 默认暴露的是无鉴权的 JSON-RPC 端点。对外提供 MCP 服务前必须加安全边界:
本章要点回顾
- 服务端只需
spring-ai-starter-mcp-server-webmvc一个依赖,不需要模型、不需要 API Key。@McpTool(+@McpToolParam)把方法暴露为工具,description 是「给 LLM 的说明书」。@McpResource/@McpPrompt补齐资源和提示词两类原语。- 传输选型:本地 stdio、远程 Streamable HTTP;HTTP 模式必须加鉴权。
本章解决什么问题:这是「调用侧」的核心。教你用一个 Spring Boot 客户端,同时接本地自己写的 Server 和第三方社区现成的 Server,并让大模型自动调用它们的工具。
「调用 MCP 工具」这件事,本质只有两个维度——工具是谁的(本地自己的 / 第三方社区的)× 用什么传输连(stdio / HTTP)。组合起来就是四类:
| 本地(自己写的 Server) | 第三方(社区现成 Server) | |
|---|---|---|
| stdio | java -jar 拉起自己的 jar | npx 拉起 filesystem 等社区 Server |
| Streamable HTTP | http://localhost:8080 连自己的服务 | 远程 URL https://xxx/mcp 连别人部署的 |
? 好消息:在 Spring AI 里,四类调用方式完全统一——不管本地还是第三方、stdio 还是 HTTP,对业务代码来说都只是「一段连接配置」,最终都变成
ToolCallback,挂在同一个ChatClient上。下面用一份配置把四类都演示出来。
客户端需要「MCP 客户端 starter + 大模型 starter」:
<?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>
<groupId>com.example</groupId>
<artifactId>ecommerce-mcp-client</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>ecommerce-mcp-client</name>
<properties>
<maven.compiler.release>21</maven.compiler.release>
<spring-boot.version>3.5.9</spring-boot.version>
<spring-ai-alibaba.version>1.1.2.2</spring-ai-alibaba.version>
</properties>
<!-- 不用 parent,改用 BOM 统一管理版本:Spring Boot + Spring AI Alibaba 各导入一份 -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- Spring AI Alibaba BOM 内已含 Spring AI BOM,MCP 客户端等 Spring AI 依赖的版本也由它统一管理 -->
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-bom</artifactId>
<version>${spring-ai-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- Web:提供 REST 接口(内含内嵌 Tomcat,Servlet 容器) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 云端模型:百炼 Qwen(DashScope 原生集成,不需要走 OpenAI 兼容端点) -->
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
</dependency>
<!-- MCP Client:把 MCP 工具自动转成 ToolCallback -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-client</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
</plugin>
</plugins>
</build>
</project>
下面是完整的连接配置,四类各演示一个(实际用哪个就留哪个,别全开):
spring:
ai:
# ① 模型:百炼 DashScope(只客户端需要)
dashscope:
api-key: ${DASHSCOPE_API_KEY} # 百炼 API Key(环境变量注入,勿硬编码)
ch@t:
options:
model: qwen-plus # 默认模型;复杂工具调用可切 qwen-max
# ② MCP 客户端
mcp:
client:
type: SYNC # Servlet 应用用 SYNC;WebFlux 应用用 ASYNC
initialized: true # 启动时就握手并加载工具(默认懒连接,见下方说明)
toolcallback:
enabled: true # 关键:把 MCP 工具自动转成 ToolCallback
# 传输一:stdio(本地子进程)
stdio:
connections:
ecommerce-local: # ① 本地:拉起自己打包的 ecommerce server jar
command: java
args:
- -jar
- D:/devworkspace/docker-hello/mcp/ecommerce-mcp-server/target/ecommerce-mcp-server-0.0.1-SNAPSHOT.jar
filesystem: # ② 第三方:社区 filesystem MCP server(需 Node.js)
command: npx
args:
- -y
- "@modelcontextprotocol/server-filesystem"
- "D:/tmp/mcp-files"
# 传输二:Streamable HTTP(远程服务)
streamable-http:
connections:
ecommerce-remote: # ③ 本地 HTTP:连自己部署的 Streamable HTTP server
url: http://localhost:8080
# ④ 第三方远程:把 url 换成别人部署的 MCP 服务地址即可,机制完全一样
# third-party:
# url: https://your-company-mcp.example.com/mcp
几个要点:
| 配置 | 作用 |
|---|---|
toolcallback.enabled: true | 最关键的开关:MCP 工具自动转成 ToolCallback,否则工具发现不了 |
initialized: true | 启动时立刻握手并加载工具(默认是懒连接,首次调工具才连) |
type: SYNC | Servlet 应用用 SYNC,WebFlux 用 ASYNC |
stdio.connections.<name>.command/args | stdio 用「命令 + 参数」拉起子进程 |
⚠️ Windows 下 stdio 拉 npx 的坑:Windows 的
npx其实是npx.cmd批处理脚本,不能被ProcessBuilder直接启动。上面command: npx在 Windows 会报错,要改成:command: cmd.exe args: - /c - npx - -y - "@modelcontextprotocol/server-filesystem" - "D:/tmp/mcp-files"Linux / macOS 则直接用
command: npx即可。
接入后,ToolCallbackProvider 里已经装好了所有 MCP 工具(本地 + 第三方的),直接挂到 ChatClient 上即可,LLM 会自己决定要不要调、调哪个:
// EcommerceMcpClientApplication.java
package com.example.mcp;
import [email protected];
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
public class EcommerceMcpClientApplication {
public static void main(String[] args) {
SpringApplication.run(EcommerceMcpClientApplication.class, args);
}
@Bean
CommandLineRunner demo(ChatClient.Builder builder, ToolCallbackProvider mcpTools) {
return args -> {
String answer = builder.build().prompt()
.user("帮我查订单 2024090100000001 的物流状态,另外有没有 1000 块以下的耳机?")
.toolCallbacks(mcpTools) // 注入 MCP 工具(含 get_order / search_products)
.call()
.content();
System.out.println(answer);
};
}
}
? 运行顺序:先启动服务端,再启动客户端。若用 stdio 方式,客户端会自动拉起 server 进程,无需手动启动服务端。
? 懒连接 vs 急连接:默认 MCP 客户端在第一次真正调用工具时才建立连接、完成握手。上面的
initialized: true改为启动时就连接并加载工具——好处是启动阶段就能发现配置错误,坏处是启动变慢。开发期建议开true快速暴露问题。
McpSyncClient:读 resources / prompts自动转 ToolCallback 只覆盖 tools;要读 resources / prompts,用 McpSyncClient:
import org.springframework.ai.mcp.client.McpClient;
import org.springframework.ai.mcp.client.McpSyncClient;
import org.springframework.ai.mcp.client.transport.HttpClientStreamableHttpTransport;
import org.springframework.ai.mcp.spec.McpSchema;
// 1. 建 Streamable HTTP 传输,连到 MCP server
HttpClientStreamableHttpTransport transport = HttpClientStreamableHttpTransport
.builder("http://localhost:8080")
.build();
// 2. 建同步客户端并初始化握手
McpSyncClient client = McpClient.sync(transport)
.requestTimeout(java.time.Duration.ofSeconds(60))
.build();
client.initialize();
// 3. 列工具 / 调工具
McpSchema.ListToolsResult tools = client.listTools();
McpSchema.CallToolResult result = client.callTool(
McpSchema.CallToolRequest.builder("get_order")
.arguments(java.util.Map.of("orderId", "2024090100000001"))
.build());
// 4. 读资源(自动转 ToolCallback 覆盖不到的能力)
McpSchema.ListResourcesResult resources = client.listResources();
// 读某个资源:client.readResource(McpSchema.ReadResourceRequest.builder("resource://uri").build());
// 5. 取提示词
McpSchema.ListPromptsResult prompts = client.listPrompts();
// 6. 用完关闭
client.closeGracefully();
接入多个 MCP server 时(本地 + 第三方),工具可能互相重名或与本地 @Tool 重名。Spring AI 提供:
McpToolNamePrefixGenerator:给 MCP 工具统一加前缀(如 ecommerce_get_order);McpToolFilter:按需过滤掉不想要的工具。本章要点回顾
- 调用侧只有两个维度:本地/第三方 × stdio/HTTP,四类在 Spring AI 里配置方式统一。
- 客户端需要「模型 + MCP client」两个 starter;
toolcallback.enabled是自动转ToolCallback的关键开关。- 本地工具用 stdio(
command+args)或 localhost HTTP;第三方远程改url即可,机制相同。- tools 自动挂
ChatClient;resources / prompts 需手动McpSyncClient。- Windows 拉 npx 要用
cmd.exe /c。
本章解决什么问题:MCP 最大的价值在于「跨应用、跨语言」。同一个 Server,除了第 6 章的 Spring Boot 客户端,还能被 Claude Desktop / Claude Code 等任何支持 MCP 的应用直接调用。本章演示这两种最常见的接入方式。
编辑 Claude Desktop 的配置文件 claude_desktop_config.json:
方式 A:stdio(本地拉起 jar)
{
"mcpServers": {
"ecommerce": {
"command": "java",
"args": ["-jar", "D:/devworkspace/docker-hello/mcp/ecommerce-mcp-server/target/ecommerce-mcp-server-0.0.1-SNAPSHOT.jar"]
}
}
}
方式 B:Streamable HTTP(远程服务)
{
"mcpServers": {
"ecommerce": {
"url": "http://localhost:8080/mcp"
}
}
}
重启 Claude Desktop 后,就能在对话里让它「查订单 2024090100000001」了。
方式 A:项目级 .mcp.json(放在项目根目录,团队共享)
{
"mcpServers": {
"ecommerce": {
"command": "java",
"args": ["-jar", "target/ecommerce-mcp-server-0.0.1-SNAPSHOT.jar"]
}
}
}
方式 B:claude mcp add 命令
claude mcp add ecommerce -- java -jar target/ecommerce-mcp-server-0.0.1-SNAPSHOT.jar
调试时可用 curl 直接打 Streamable HTTP 端点,确认协议层是否正常:
curl -X POST http://localhost:8080/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","version":"0"}}}'
更推荐用官方 MCP Inspector(npx @modelcontextprotocol/inspector)图形化地做初始化、列工具、调工具的联调。
本章解决什么问题:从「能跑」到「能上线」。三条主线:生产安全、连接容错、第三方 Server 治理。
initialized: true 快速暴露配置错误;requestTimeout,工具执行过长要有超时与降级;listTools / listResources / listPrompts 支持分页(传 cursor,循环到 nextCursor 为空)。接入第三方 Server 时,因为它运行的是「别人的代码」,要格外谨慎:
| 关注点 | 建议 |
|---|---|
| 信任边界 | 优先用官方/大厂维护的 Server,社区 Server 先审代码再接入 |
| 权限最小化 | filesystem 这类 Server 只给它最小目录、只读权限 |
| 输入校验 | 工具参数最终可能落库/落盘,服务端对参数做校验 |
| 可观测 | 记录每次工具调用的入参/出参,便于审计与排错 |
| 坑 | 现象 | 解法 | 章节 |
|---|---|---|---|
| stdio 日志污染 | 客户端解析 JSON 失败 | 关闭 banner / web / console 日志 | 5.6 |
| 工具重名 | 多个 server 工具冲突 | McpToolNamePrefixGenerator / McpToolFilter | 6.6 |
| 版本不对齐 | 自动配置失效、Bean 缺失 | Spring AI 1.1.2 ↔ Spring Boot 3.5.x ↔ JDK 21 | 4.1 |
toolcallback 没开 | 工具发现不了 | spring.ai.mcp.client.toolcallback.enabled=true | 6.3 |
| Windows 拉 npx 失败 | stdio 连接报错 | command: cmd.exe + args: /c npx ... | 6.3 |
| description 没写 | LLM 乱传参、不调工具 | 认真写 @McpTool/@McpToolParam 的 description | 5.4 |
| 客户端启动慢/不连 | 以为是没连上 | 默认懒连接;initialized: true 改急连接 | 6.4 |
| HTTP 裸暴露 | 工具被公网任意调用 | 加 API Key / OAuth 鉴权 | 5.7 |
| 用途 | artifact |
|---|---|
| MCP Server(stdio) | spring-ai-starter-mcp-server |
| MCP Server(WebMVC,SSE/Streamable HTTP) | spring-ai-starter-mcp-server-webmvc |
| MCP Server(WebFlux) | spring-ai-starter-mcp-server-webflux |
| MCP Client | spring-ai-starter-mcp-client |
| 云端模型(百炼 DashScope) | spring-ai-alibaba-starter-dashscope |
| 配置 | 作用 |
|---|---|
spring.ai.mcp.server.protocol | 服务端传输:SSE / STREAMABLE |
spring.ai.mcp.server.stdio | stdio 模式开关(stdio starter) |
spring.ai.mcp.client.enabled | 启用 MCP 客户端 |
spring.ai.mcp.client.type | SYNC(Servlet)/ ASYNC(WebFlux) |
spring.ai.mcp.client.initialized | 是否启动时握手加载工具(默认懒连接) |
spring.ai.mcp.client.toolcallback.enabled | 自动把 MCP 工具转 ToolCallback |
spring.ai.mcp.client.stdio.connections.<name>.command/args | stdio 连接(命令 + 参数) |
spring.ai.mcp.client.{sse,streamable-http}.connections.<name>.url | HTTP 连接地址 |
spring.ai.dashscope.api-key | 百炼 API Key(客户端) |
[email protected] | 模型名(如 qwen-plus) |
| 注解 | 作用 |
|---|---|
@McpTool | 把方法暴露为 MCP 工具 |
@McpToolParam | 工具参数描述 |
@McpResource | 暴露只读资源 |
@McpPrompt | 暴露提示词模板 |
@PromptVariable | 提示词模板变量 |
| 词 | 一句话 |
|---|---|
| MCP | 模型上下文协议:把外部工具/数据源统一成一套 JSON-RPC 接口 |
| MCP Server | 工具提供方,暴露 tools / resources / prompts |
| MCP Client | 工具调用方,连接 Server 并发现/调用工具 |
| MCP Host | 承载 AI 对话的宿主(Claude Desktop、Spring Boot 应用) |
| tools | 可调用的函数,自动转 ToolCallback |
| resources | 只读数据源,需 McpSyncClient 手动读 |
| prompts | 提示词模板,需 McpSyncClient 手动取 |
| stdio | 本地子进程传输(stdin/stdout) |
| Streamable HTTP | 基于 HTTP 的远程传输,生产推荐 |
| JSON-RPC 2.0 | MCP 的消息格式(Request / Response / Notification) |
| initialize | 握手请求,协商协议版本与能力 |
| capabilities | 能力声明(tools / resources / prompts 等) |
| Function Calling | 模型输出「调哪个函数 + 传什么参」,由应用执行 |
| ToolCallback | Spring AI 里「工具」的统一抽象,MCP 与 @Tool 的汇合点 |
一句口诀收官:MCP 用统一协议把「外部工具」变成「应用内工具」;Spring AI 里服务端用
@McpTool暴露,客户端用ToolCallbackProvider接入,最终都汇入同一条 Function Calling 链路。
Windows10安装ubuntu18.04双系统教程的方法步骤(图文)
Debian系统怎么设置任务栏显示位置?
Codex Reasoning Effort 如何针对不同任务调节成本和速度?
Codex 的 low、medium、high、xhigh 推理强度分别适合哪些场景?
Ubuntu系统怎么禁止软件更新? 不升级指定软件的技巧
少调模型多走代码:解析 resolve-harness 的确定性 Fast Path 运行时