面向 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(客户端侧,服务端无需模型) 场景:电商「订单查询 / 商品搜索」
阅读地图(先花 3 分钟看这里)
这篇文章解决什么问题
你写了一个 Spring Boot 应用,想让大模型能「查数据库、调接口、操作文件」——但每个外部能力都单独写一遍适配,N 个应用 × M 个工具,适配代码爆炸。
MCP 就是干这个的:把「外部工具/数据源」统一成一套标准协议。工具方实现一次(Server),任何 AI 应用都能按协议调用(Client),一次实现、到处接入。这篇文章教你把这件事用 Spring AI 完整落地——从「协议是什么」到「怎么写 Server 暴露工具」,再到「本地和第三方两类客户端怎么调」。
写给谁
- 有 Java / Spring Boot 基础,想给应用接入外部工具(数据库、GitHub、文件系统、第三方服务)的工程师;
- 已经会 Function Calling(
@Tool),想知道「MCP 和它什么关系、什么时候该用 MCP」的人; - 想把自己团队的内部系统封装成工具,供多个 AI 应用(Claude Desktop / Claude Code / 其它系统)复用的人。
不需要你懂协议细节,会写 Java 就行——Spring AI 已经把 JSON-RPC 握手、消息编解码全部封装好了。
一条主线 + 一个贯穿案例
整篇文章走一条主线——MCP 的「提供」与「调用」两侧:
提供服务端:业务逻辑 → @McpTool 注解 → 自动暴露为 MCP 工具(stdio / HTTP 两种传输)
调用客户端:Spring Boot / Claude Desktop / Claude Code → 通过协议发现工具 → LLM 自动调用
所有代码围绕同一个例子(后文反复用到):
一个电商 MCP Server 暴露两个工具:
get_order(按订单号查订单)、search_products(按关键字搜商品)。你会在服务端用注解把它暴露出去,再在客户端让大模型「自己决定」调哪个工具来回答用户问题。
术语速查(精简版)
完整版见附录 · 术语表。先扫一眼,后面遇到不慌。
| 词 | 一句话 |
|---|---|
| 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 | 模型输出「调哪个函数 + 传什么参」,由应用执行 |
快速上手:15 分钟跑通最小闭环
目标:先看到结果,再回头理解原理。 这里只写「最小能跑通」的代码——一个 Server 暴露工具、一个 Client 让大模型调用它。完整版见第 5、6 章。
1. 准备环境
只需要一个云端 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跑通只要几分钱,首次开通一般有免费额度。
2. 服务端:一个注解暴露一个工具
新建 ecommerce-mcp-server 项目,pom 只加一个依赖(完整 pom 见第 5 章):
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
</dependency>
然后写这个工具类:
// 一个 @McpTool 注解,方法就变成了一个可被远程调用的 MCP 工具
@Component
public class EcommerceTools {
public record OrderItem(String productId, String productName, int quantity, double price) {
}
public record Order(String orderId, String userId, String status, List<OrderItem> items, double totalAmount) {
}
@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 等你调。
3. 客户端:让大模型自动调用它
新建 ecommerce-mcp-client 项目,pom 加两个依赖(模型 + MCP 客户端),application.yml 里「连上」刚才的 Server:
spring:
ai:
dashscope:
api-key: ${DASHSCOPE_API_KEY}
chat:
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);
};
}
💡
ToolCallbackProvider mcpTools不用自己写@Bean注册:MCP 场景下,这个 Bean 由spring-ai-starter-mcp-client的自动装配帮你生成,直接注入即可。前提:① 引入了 mcp-client starter;② yml 配好了 MCP Server 连接;③spring.ai.mcp.client.toolcallback.enabled=true(默认开启)。
4. 运行,预期输出
先启动服务端,再启动客户端:
# 终端 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 → 拿到结果再组织语言回答。
第一部分 · 理论篇
第 1 章 MCP 是什么、解决什么问题
1.1 什么是 MCP:从「N × M」到「N + M」
先抛开「协议」这种大词,用大白话讲清楚 MCP 到底省了什么力气。
没有 MCP:两两配对、各写一遍。 假设你有 3 个 AI 应用,要接 4 个外部工具(数据库、GitHub、搜索、文件)。每个应用想用每个工具,都得单独写一段「怎么连这个工具」的适配代码,而且各写各的、互不通用——3 × 4 = 12 段适配代码。
有了 MCP:两边各遵守一次标准。 工具侧,每个工具只实现一次 MCP(4 次);应用侧,每个应用只支持一次 MCP(3 次)。一共 3 + 4 = 7 次。而且应用和工具越多、差距越悬殊——10 个应用 × 10 个工具,前者要 100 段、后者只要 20 次,差 5 倍。这就是「N × M 降到 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 的核心价值,就四条:
- 解耦:工具提供方(Server)和工具消费方(Client)各自独立演进,通过协议对接;
- 跨语言:协议基于 JSON-RPC 2.0,Java / Python / TypeScript 客户端都能调同一个 Server;
- 复用:社区已有数千个现成 MCP Server(GitHub、PostgreSQL、Slack、文件系统、浏览器等),按统一协议接入成本极低;
- 标准化上下文:不止「调函数」,还统一了「读资源」「取提示词模板」的上下文供给方式。
1.2 MCP 与 Function Calling 的关系
这是最容易混淆的一点,先在这里讲清楚。两者不冲突,而且终点是同一条链路:
| Function Calling | MCP | |
|---|---|---|
| 工具定义位置 | 应用内部,@Tool 方法 | 外部/第三方服务,独立的 Server 进程 |
| 接入方式 | 编译期就在应用里 | 运行时通过协议发现、握手、调用 |
| 生命周期 | 与应用同生共死 | 独立部署、独立升级、可被多应用共享 |
| 典型场景 | 应用自己的业务函数 | 数据库、GitHub、外部物流/仓储系统等 |
💡 在 Spring AI 里,两者的终点是同一件事:MCP 工具经
ToolCallbackProvider自动转成和@Tool完全一样的ToolCallback,最终都参与同一次 Function Calling。换句话说,MCP 是把「外部的工具」统一成「应用内可调用的工具」的标准桥梁。
1.3 什么时候该用 MCP(选型)
不是所有工具都要上 MCP,先判断:
| 场景 | 建议 |
|---|---|
| 工具只被自己一个应用用,逻辑简单 | 直接用 @Tool(Function Calling),别上 MCP |
| 工具要被多个应用 / 多语言 / 跨团队复用 | 上 MCP,一次实现到处接入 |
| 接入第三方现成能力(GitHub、数据库、搜索) | 上 MCP,社区有现成 Server 直接连 |
| 想把内部系统封装成「标准工具」对外开放 | 上 MCP,独立部署、独立升级、带鉴权 |
一句话:MCP 的价值在「复用」和「跨边界」。单应用内部的自有函数,
@Tool更省事;要共享、要接第三方、要跨语言,就上 MCP。
1.4 生态现状
MCP 发布后迅速成为事实标准,OpenAI、Google、微软、Anthropic 等都已支持。Spring AI 从 1.0 起深度集成 MCP,到 1.1.2 已支持 tools / resources / prompts 三类原语,以及 stdio / SSE / Streamable HTTP 三种传输。本文后续全部基于 Spring AI 1.1.2 展开。
第 2 章 架构与核心概念
本章解决什么问题:把 MCP 的「黑盒」打开,让你看懂它由哪几层组成——三类角色、三大原语、三种传输、一种消息格式。这是后面读 Spring AI 封装、排查问题的基础。
2.1 Client–Host–Server 三层架构
MCP 官方定义了三类角色:
┌─────────────────────────────────────────────────────────────┐
│ MCP Host(宿主,如 Claude Desktop、你的 Spring Boot 应用) │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ MCP Client(客户端,1:1 连接一个 Server) │ │
│ │ · 发起握手、协商能力 │ │
│ │ · 发请求调工具 / 读资源 / 取提示词 │ │
│ └──────────────────────────┬───────────────────────┘ │
└───────────────────────────────┼─────────────────────────────┘
│ JSON-RPC 2.0(传输层:stdio / HTTP)
▼
┌───────────────────────┐
│ MCP Server(服务端) │
│ · 暴露 tools │
│ · 暴露 resources │
│ · 暴露 prompts │
└───────────────────────┘
- Host:承载 AI 对话的宿主应用(Claude Desktop、你的 Spring Boot 应用)。它内部可以管理多个 Client。
- Client:与单个 Server 维持 1:1 连接,负责握手、收发消息。
- Server:提供工具/资源/提示词的独立进程或服务。
📌 通信是双向的。客户端可以调 Server 的工具,Server 也可以反向请求客户端的能力(如
sampling让宿主调用 LLM)。不过日常开发 99% 的场景只用到「客户端调 Server」,本文也只讲这个方向。
2.2 三大原语:tools / resources / prompts
| 原语 | 作用 | 关键方法 | 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 章会分别演示两种用法。
2.3 传输层:stdio / Streamable HTTP / SSE
「传输」就是客户端和服务端之间用哪根「管子」传 JSON 消息。三种管子,从「近」到「远」:
| 传输 | 机制 | 适用场景 | 备注 |
|---|---|---|---|
| stdio | 本地子进程,通过 stdin/stdout 通信 | 本地工具、开发调试 | 最简单;进程生命周期由客户端管理 |
| Streamable HTTP | HTTP POST + 可选 SSE 流 | 远程服务、生产环境 | 1.1 新增的新一代传输,生产推荐 |
| SSE | HTTP + Server-Sent Events | 远程服务(旧方案) | 逐步被 Streamable HTTP 取代 |
逐个用大白话拆开:
-
stdio(本机当面说):客户端把 Server 当成本地程序直接「启动」(
java -jar xxx.jar),靠程序的标准输入/输出(命令行里print出来、键盘敲进去的内容)来回传 JSON,根本不走网络。进程由客户端拉起、也由它关掉——所以只适合本机、单人调试。 -
SSE(旧广播,两条线):走网络。服务端先开一条 HTTP 长连接,专门「单向推送」消息给你(Server-Sent Events);客户端要反向发消息,得走另一个 HTTP 端点。等于两条线各管一个方向,长连接对负载均衡/反向代理不友好、断线重连也麻烦——已被淘汰,别主动用。
-
Streamable HTTP(一问一答,一条线):走网络,但只用一个端点(默认
POST /mcp)。客户端每次发一个请求,服务端按需回:内容少就回一个 JSON,要连发多条消息就回一个 SSE 流。等于普通 HTTP 一问一答,天然适配网关/代理/CDN,还能断线重连——生产首选。
选型建议:
- 本地工具、单人调试 → stdio(最省事,连网络都不走);
- 部署到服务器、供多应用/多用户调用 → Streamable HTTP(本文服务端实战以此为主)。
2.4 JSON-RPC 2.0 消息层
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资源不存在等。
第 3 章 生命周期与一次完整调用
3.1 连接状态机
一次 MCP 连接从建立到关闭,经历四个状态:
Created ──(客户端发 initialize)──► Initialized ──(客户端发 notifications/initialized)──► Ready ──(关闭)──► Shutdown
- Created:连接建立(stdio 进程拉起 / HTTP 握手建立),尚未初始化;
- Initialized:客户端发
initialize,服务端返回能力; - Ready:客户端发
notifications/initialized确认,进入就绪态,可正常收发业务消息; - Shutdown:关闭连接(stdio 关闭 stdin/stdout,HTTP 断开)。
3.2 能力协商(capabilities)
initialize 的响应里,服务端声明自己能提供什么(capabilities),客户端据此决定调用策略:
| 能力 | 含义 |
|---|---|
tools | 提供可调用工具 |
resources | 提供只读资源 |
prompts | 提供提示词模板 |
logging | 支持日志通知 |
sampling(客户端侧) | 客户端允许服务端反向请求 LLM 采样 |
Spring AI 1.1.2 服务端默认开启 tools / resources / prompts 三项能力。
3.3 完整消息流(一次工具调用)
把第 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 的封装就不会觉得黑盒了。
第二部分 · 实战篇
第 4 章 项目准备
4.1 环境与版本矩阵
| 组件 | 版本 | 说明 |
|---|---|---|
| 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 端。这正是「解耦」的体现——工具方和模型方彻底分离。
4.2 工程结构(两个项目)
本文实战拆成服务端和客户端两个 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
4.3 数据模型与「内存数据」约定
为了让读者无需数据库即可运行,本文用内存 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) {}
第 5 章 MCP Server:提供工具(核心)
5.1 pom.xml
服务端用 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等注解开箱即用。
5.2 application.yml
spring:
ai:
mcp:
server:
name: ecommerce-mcp-server # 可选,服务端标识
version: 1.0.0
protocol: STREAMABLE # 传输协议:STREAMABLE / SSE / STATELESS(不填默认 SSE)
protocol 决定服务端用哪种 HTTP 传输,可选三种值(不填默认 SSE):
| 协议 | 传输方式 | 会话 | 适用场景 |
|---|---|---|---|
STREAMABLE | 单端点 POST /mcp,可回单个 JSON 或 SSE 流 | 有状态(Mcp-Session-Id) | 生产推荐,对 LB / 代理 / CDN 友好 |
SSE | 两个端点:SSE 长连接 + POST 消息端点 | 有状态长连接 | 旧项目、需兼容 |
STATELESS | 同 STREAMABLE | 无状态,请求各自独立 | 微服务、serverless、水平扩容 |
STREAMABLE(Streamable HTTP)是 MCP 2025-03-26 规范引入的新一代传输,取代 SSE:单个POST /mcp端点,客户端每次发一个 JSON-RPC 请求,服务端按需回单个 JSON 响应、或对「需要多条消息」的场景回 SSE 流。STATELESS是 Streamable HTTP 的无状态变体:不维护会话、每次请求自包含,请求可打到任意实例,适合多副本水平扩容和 serverless。SSE是旧传输(两个端点 + 长连接),对负载均衡不友好,不填protocol时默认走它;新项目直接用STREAMABLE。
本文服务端统一用 STREAMABLE,端点即默认的 POST /mcp。
5.3 启动类与业务服务
// 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;
}
}
5.4 暴露工具:@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.springaicommunity.mcp.annotation.McpTool;
import org.springaicommunity.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省略时默认用方法名;这里显式给蛇形命名,便于跨语言客户端识别。- 方法签名里的 Java 类型(record、
List、基本类型)会自动生成 JSON Schema 作为工具的inputSchema,无需手写。
📌
@McpToolvs@Tool:Spring AI 1.1.2 也支持把带@Tool的方法经服务端tool-callback-converter(默认true)自动转成 MCP 工具。但推荐直接用@McpTool——它是 MCP 原生注解,支持参数级@McpToolParam描述,语义更精确。
5.5 资源与提示词:三类原语齐活
// mcp/EcommerceResources.java —— 只读资源
package com.example.mcp.mcp;
import com.example.mcp.service.EcommerceService;
import org.springaicommunity.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(name = "product_catalog", uri = "product://catalog",
description = "当前在售商品目录(JSON 列表)", mimeType = "application/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.springaicommunity.mcp.annotation.McpPrompt;
import org.springaicommunity.mcp.annotation.McpArg;
import org.springframework.stereotype.Component;
@Component
public class EcommercePrompts {
@McpPrompt(name = "order_refund_prompt",
description = "生成售后/退货场景的客服话术提示词")
public String orderRefundPrompt(@McpArg(name = "orderId", description = "订单号") String orderId) {
return "用户要申请退货,涉及订单号 " + orderId + "。请用礼貌、专业的客服语气,说明退货流程和注意事项。";
}
}
💡 提示词内容来自方法返回值:
@McpPrompt没有template属性,模板就是方法拼好返回的字符串(参数用@McpArg声明、从方法入参拿到)。
💡 服务端声明、客户端手动取:
@McpResource/@McpPrompt只是服务端用来「对外声明我有哪些资源 / 提示词模板」的注解,只负责声明,不负责客户端怎么用。客户端这边,tools 有toolcallback.enabled=true会自动包装成ToolCallback参与 Function Calling;但 resources / prompts 没有这种自动包装,客户端必须用McpSyncClient的listResources()/readResource()/listPrompts()/getPrompt()手动取(见 6.5 节)。
5.6 stdio 模式(本地调用)
本地用 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 方式拉起。
5.7 安全提醒(重要)
Streamable HTTP / SSE 默认暴露的是一个无鉴权的 JSON-RPC 端点。换句话说,只要能访问到 http://你的机器:8080/mcp,任何人都能:
| 端点方法 | 别人能干什么 |
|---|---|
tools/list | 枚举你暴露了哪些工具(信息泄露) |
tools/call | 直接调用你的工具——查订单、搜商品、甚至写操作 |
resources/read | 读你暴露的资源 |
prompts/get | 取你的提示词模板 |
而这些工具背后往往是订单、商品、用户数据(PII)。HTTP 模式裸暴露 ≈ 把内部能力大门敞开。生产前至少做四件事:
- 传输:stdio 只在本机进程间通信、不开端口,相对安全(本地调试首选);要走网络就上 HTTPS,别用明文 HTTP。
- 鉴权:网关层加 API Key / OAuth / JWT,或用 Spring Security 保护
/mcp端点;别靠「URL 猜不到」这种隐蔽式安全。 - 最小权限:只
@McpTool暴露必要方法;写操作/删除类工具要么不暴露,要么单独加鉴权 + 审计;对入参做校验。 - 留痕:记录每次
tools/call的入参/出参,便于审计与排查。
一句话:HTTP 模式的 MCP Server 默认等于「无鉴权的远程函数调用接口」,上线必须 HTTPS + 鉴权 + 最小权限,缺一不可。(完整生产清单见第 8 章)
下面给一个最简示例:用 Spring Security 给 /mcp 加 HTTP Basic 鉴权。
<!-- 服务端 pom 增加安全依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
// mcp/McpSecurityConfig.java
package com.example.mcp.mcp;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class McpSecurityConfig {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.csrf(AbstractHttpConfigurer::disable) // JSON-RPC POST 无浏览器会话,关掉 CSRF
.authorizeHttpRequests(auth -> auth
.requestMatchers("/mcp").authenticated() // 只锁 MCP 端点,其余放行
.anyRequest().permitAll())
.httpBasic(Customizer.withDefaults()) // 用户名/密码
.build();
}
@Bean
UserDetailsService users() {
return new InMemoryUserDetailsManager(
User.withUsername("mcp")
.password("{noop}secret") // 生产换成 BCryptPasswordEncoder
.roles("MCP")
.build());
}
}
要点:
csrf(...).disable()必须关:MCP 走 JSON-RPC POST,没有浏览器会话,默认的 CSRF 防护会直接挡住无 token 的 POST。{noop}是「明文密码」标记,仅演示用;生产用new BCryptPasswordEncoder()并去掉{noop}前缀。- 客户端要带上凭证:Server 加锁后,第 6 章的客户端连接时也要在 header 里带上用户名/密码(HTTP Basic),否则握手会被 401 拒掉。
- 想要 API Key / OAuth:把
.httpBasic(...)换成自定义OncePerRequestFilter校验X-API-Keyheader,或.oauth2ResourceServer(oauth2 -> oauth2.jwt(...))校验 JWT,机制相同、只是换一种凭证。
第 6 章 MCP Client:调用工具(本地 + 第三方)
6.1 先看清:客户端能调哪几类 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上。下面用一份配置把四类都演示出来。
6.2 pom.xml
客户端需要「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>
6.3 application.yml(同时接本地 + 第三方)
下面是完整的连接配置,四类各演示一个(实际用哪个就留哪个,别全开):
spring:
ai:
# ① 模型:百炼 DashScope(只客户端需要)
dashscope:
api-key: ${DASHSCOPE_API_KEY} # 百炼 API Key(环境变量注入,勿硬编码)
chat:
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即可。
6.4 LLM 自动调用 MCP 工具
接入后,ToolCallbackProvider 里已经装好了所有 MCP 工具(本地 + 第三方的),直接挂到 ChatClient 上即可,LLM 会自己决定要不要调、调哪个:
// EcommerceMcpClientApplication.java
package com.example.mcp;
import org.springframework.ai.chat.client.ChatClient;
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快速暴露问题。
6.5 手动 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();
6.6 多 server 重名处理
接入多个 MCP server 时(本地 + 第三方),工具可能互相重名或与本地 @Tool 重名。Spring AI 提供:
McpToolNamePrefixGenerator:给 MCP 工具统一加前缀(如ecommerce_get_order);McpToolFilter:按需过滤掉不想要的工具。
6.7 连接加了鉴权的 Server
第 5.7 节给 Server 加了鉴权后,客户端连接就要带凭证。注意:1.1.2 的声明式配置(spring.ai.mcp.client.streamable-http.connections.*)只支持 url,不支持 header / 用户名密码,所以要连加了锁的 Server,得走编程式 McpSyncClient,用 customizeRequest / customizeClient 带上凭证。
方式一:API Key(自定义 header)
McpSyncClient client = McpClient.sync(
HttpClientStreamableHttpTransport.builder("http://localhost:8080")
.customizeRequest(req -> req.header("X-API-Key", "secret")) // 加 API Key 头
.build())
.requestTimeout(java.time.Duration.ofSeconds(60))
.build();
client.initialize();
方式二:HTTP Basic(用户名 / 密码)
McpSyncClient client = McpClient.sync(
HttpClientStreamableHttpTransport.builder("http://localhost:8080")
.customizeClient(c -> c.authenticator(new java.net.Authenticator() {
@Override
protected java.net.PasswordAuthentication getPasswordAuthentication() {
return new java.net.PasswordAuthentication("mcp", "secret".toCharArray());
}
}))
.build())
.build();
client.initialize();
⚠️ 代价:编程式
McpSyncClient是「手动调用」——不像声明式那样自动把工具包进ToolCallback挂到ChatClient,你得自己listTools()/callTool()(见 6.5)。1.1.2 里「声明式自动包装」和「带凭证」目前是二选一。
第 7 章 其它客户端调用(Claude Desktop / Claude Code)
本章解决什么问题:MCP 最大的价值在于「跨应用、跨语言」。同一个 Server,除了第 6 章的 Spring Boot 客户端,还能被 Claude Desktop / Claude Code 等任何支持 MCP 的应用直接调用。本章演示这两种最常见的接入方式。
7.1 Claude Desktop
编辑 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」了。
7.2 Claude Code
方式 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
7.3 附录:curl 验证底层报文 & MCP Inspector
调试时可用 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)图形化地做初始化、列工具、调工具的联调。
第三部分 · 最佳实践与排查
第 8 章 最佳实践
本章解决什么问题:从「能跑」到「能上线」。三条主线:生产安全、连接容错、第三方 Server 治理。
8.1 生产安全
- HTTP 传输默认无鉴权,必须加 Spring Security / 网关层 API Key 或 OAuth;
- 工具能力遵循最小权限:只暴露必要方法,写操作类工具务必加鉴权与审计;
- 生产连远程 server 优先 Streamable HTTP;本地工具用 stdio。
8.2 连接与容错
- 懒连接 vs 急连接:默认客户端首次调工具才建连接(启动慢不代表没连上);开发期设
initialized: true快速暴露配置错误; - 设置合理的
requestTimeout,工具执行过长要有超时与降级; listTools/listResources/listPrompts支持分页(传cursor,循环到nextCursor为空)。
8.3 第三方 MCP Server 治理
接入第三方 Server 时,因为它运行的是「别人的代码」,要格外谨慎:
| 关注点 | 建议 |
|---|---|
| 信任边界 | 优先用官方/大厂维护的 Server,社区 Server 先审代码再接入 |
| 权限最小化 | filesystem 这类 Server 只给它最小目录、只读权限 |
| 输入校验 | 工具参数最终可能落库/落盘,服务端对参数做校验 |
| 可观测 | 记录每次工具调用的入参/出参,便于审计与排错 |
第 9 章 高频坑速查
| 坑 | 现象 | 解法 | 章节 |
|---|---|---|---|
| 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 |
配置 key 速查
| 配置 | 作用 |
|---|---|
spring.ai.mcp.server.protocol | 服务端传输:STREAMABLE / SSE / STATELESS(默认 SSE) |
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(客户端) |
spring.ai.dashscope.chat.options.model | 模型名(如 qwen-plus) |
注解速查
| 注解 | 作用 |
|---|---|
@McpTool | 把方法暴露为 MCP 工具 |
@McpToolParam | 工具参数描述 |
@McpResource | 暴露只读资源 |
@McpPrompt | 暴露提示词模板(内容来自方法返回值,无 template 属性) |
@McpArg | 提示词参数声明(@McpPrompt 方法的入参) |
注:这些注解来自
org.springaicommunity:mcp-annotations(由spring-ai-mcp-annotations传递引入),包名是org.springaicommunity.mcp.annotation。
术语表
| 词 | 一句话 |
|---|---|
| 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 链路。