导读 :上一篇文章(MCP-01)从协议层面拆解了 MCP 的消息格式、传输层设计和生命周期管理。本篇进入实战环节------如何用 Spring AI 搭建一个生产级的 MCP Server。文章将结合 Dream-SaaS 项目中的真实踩坑经验,覆盖工具注册的两种方案、Server 架构分层设计、参数校验与错误处理,并重点解读 2026 年 7 月 28 日发布的 MCP 无状态新规范------这是 MCP 协议自发布以来最大的一次架构变更,直接影响了 Server 端的开发方式。
一、工具注册:两种方案的选型与踩坑
MCP Server 的核心工作就是"把工具暴露出去"。Spring AI 提供了两种工具注册方式,各有适用场景。在 Dream-SaaS 项目的开发过程中,两种方式都用过,也踩了不少坑。
1.1 方案 A:@McpTool + annotation-scanner(声明式)
这是 Spring AI 为 MCP 专门设计的注解方案。在方法上标注 @McpTool,参数标注 @McpToolParam,框架启动时通过 annotation-scanner 自动扫描并注册为 MCP 工具:
bash
@Component
public class McpService {
@McpTool(name = "hello", description = "一个Mcp Server 的测试方法")
public String hello(
@McpToolParam(description = "请求的类型") String type,
@McpToolParam(description = "请求的名称") String name) {
return new TestTool().Hello(type, name);
}
}
配合 YAML 配置启用扫描:
bash
spring:
ai:
mcp:
server:
enabled: true
annotation-scanner:
enabled: true
type: sync
protocol: streamable
streamable-http:
mcp-endpoint: /mcp
这种方式的优点是简洁直观:注解即文档,配置即启用。适合工具数量有限、逻辑相对简单的场景。缺点是工具类必须被 Spring 扫描到,且注解与 MCP 协议强耦合------如果后续想把同一个方法同时作为普通 REST 接口暴露,或者同时注册到 Spring AI 的 Agent 工具链中,需要额外处理。
1.2 方案 B:@Tool + McpToolContributor(模块化)
Dream-SaaS 项目最终采用的方案。工具方法使用 Spring AI 通用的 @Tool 注解,再通过实现 McpToolContributor 接口将工具对象贡献给 MCP Server:
bash
@Component
public class McpService {
@Tool(name = "hello", description = "一个Mcp Server 的测试方法")
public String hello(
@ToolParam(description = "请求的类型") String type,
@ToolParam(description = "请求的名称") String name) {
return new TestTool().Hello(type, name);
}
}
bash
@Component
public class TestToolContributor implements McpToolContributor {
@Autowired
private McpService mcpService;
@Override
public String moduleId() {
return "test";
}
@Override
public Object[] toolObjects() {
return new Object[] { mcpService };
}
}
这种方案的核心优势在于模块化 。每个业务模块实现自己的 McpToolContributor,通过 moduleId() 标识归属,toolObjects() 返回包含 @Tool 方法的 Bean 实例。Spring AI 框架在启动时聚合所有 Contributor,统一注册到 MCP Server。
在 Dream-SaaS 项目中,我们有十几个业务模块(代码审查、文档搜索、数据库查询、API 网关调用等),每个模块各自实现 Contributor。当工具数量超过 50 个后,这种分而治之的方式远比在一个大类上堆注解要清晰------每个模块的工具定义、参数校验、错误处理都在各自的模块内完成。
此外,@Tool 是 Spring AI 的通用注解。同一个工具方法可以同时被 MCP Server 暴露给外部 Client,也可以被本地的 Agent 工具链直接调用------无需为 MCP 和非 MCP 场景维护两套代码。
1.3 两种方案的对比
| 对比维度 | 方案 A:@McpTool + Scanner | 方案 B:@Tool + Contributor |
|---|---|---|
| 注解类型 | @McpTool / @McpToolParam | @Tool / @ToolParam |
| 注册方式 | annotation-scanner 自动扫描 | McpToolContributor 手动聚合 |
| 模块化支持 | 弱,所有工具平铺 | 强,按 moduleId 分模块 |
| 复用性 | MCP 专用注解,难复用 | @Tool 通用,可同时用于 Agent 和 MCP |
| 适用场景 | 工具数量少、逻辑简单 | 多模块、大型项目 |
| 配置复杂度 | 低(一个 enabled 开关) | 中(需要实现 Contributor 接口) |
1.4 踩坑记录
在 Dream-SaaS 的开发过程中,工具注册环节踩了几个典型的坑,记录如下:
坑 1:@Tool ≠ @McpTool。 annotation-scanner 只扫描 @McpTool,不扫描 @Tool。如果开启了 annotation-scanner 但用了 @Tool 注解,工具不会被注册,且没有报错------属于"静默失败",排查成本很高。最终我们在启动日志中加了工具注册汇总,快速发现"注册了 0 个工具"的异常。
坑 2:同一工具名不能双开两种方案。 如果同一个工具名(如 "hello")同时出现在 @McpTool(通过 scanner 注册)和 McpToolContributor 中,启动时会抛出 IllegalArgumentException。两种方案只能选一种,或者确保工具名不冲突。
坑 3:toolObjects() 返回的是 Bean 而非 Method。 这是一个容易混淆的点------toolObjects() 需要返回包含工具方法的对象实例(Bean),而不是方法引用。框架会在这些 Bean 上扫描 @Tool 注解来提取工具定义。如果返回 null 或空数组,不会报错,只是该 Contributor 的工具不会被注册。
坑 4:同进程 Server + Client 自连不稳定。 在测试环境中,如果 MCP Server 和 MCP Client 跑在同一个 Spring Boot 进程中(同一个端口),会出现连接不稳定的问题。具体表现为偶尔超时、偶尔返回空结果。建议测试时使用两个进程、两个端口,模拟真实的部署拓扑。
二、Server 架构分层设计
一个生产级的 MCP Server 不是"写几个 @Tool 方法就上线"。当工具数量超过 20 个、接入多个 LLM 应用后,需要清晰的分层架构来保证可维护性。以下是 Dream-SaaS 项目中总结的四层架构模型:
bash
┌──────────────────────────────────────────────────────────┐
│ 协议层(Protocol Layer) │
│ MCP 协议适配、JSON-RPC 编解码、传输层选择 │
├──────────────────────────────────────────────────────────┤
│ 路由层(Routing Layer) │
│ 工具注册表、请求分发、moduleId 路由 │
├──────────────────────────────────────────────────────────┤
│ 工具层(Tool Layer) │
│ 参数校验、权限检查、限流、日志审计 │
├──────────────────────────────────────────────────────────┤
│ 执行层(Execution Layer) │
│ 业务逻辑实现、外部服务调用、结果封装 │
└──────────────────────────────────────────────────────────┘
2.1 协议层
由 Spring AI 框架自动处理。配置 spring.ai.mcp.server.protocol=streamable 后,框架会自动启用 Streamable HTTP 传输,处理 JSON-RPC 消息的编解码、请求/响应匹配、错误格式化。
完整的 Server 端配置示例:
bash
server:
port: 8091
spring:
ai:
mcp:
server:
enabled: true
name: dream-saas-mcp-server
version: 1.0.0
type: sync
protocol: streamable
streamable-http:
mcp-endpoint: /mcp
annotation-scanner:
enabled: false # 使用 McpToolContributor 方案时关闭
tool-info:
# 工具描述是否包含参数的完整 Schema
include-full-schema: true
开发者无需关心协议细节------这也是选择 Spring AI 而非从零实现 MCP 协议栈的主要原因。
2.2 路由层
路由层负责将 tools/call 请求分发到正确的工具方法。方案 B 中的 McpToolContributor 就是路由层的一部分------框架根据 moduleId 和工具名构建注册表。
在大型项目中,可以进一步扩展路由层,实现高级路由功能:
bash
@Component
public class AdvancedToolRouter {
private final Map<String, ToolHandler> registry = new ConcurrentHashMap<>();
public void register(String moduleId, String toolName, ToolHandler handler) {
String key = moduleId + ":" + toolName;
registry.put(key, handler);
}
public ToolHandler resolve(String toolName) {
// 优先精确匹配,其次模糊匹配
ToolHandler handler = registry.get(toolName);
if (handler == null) {
// 尝试跨模块查找
handler = registry.values().stream()
.filter(h -> h.supports(toolName))
.findFirst()
.orElse(null);
}
return handler;
}
}
这种扩展路由可以实现按租户路由(不同租户看到不同的工具集)、按环境路由(灰度/生产)等功能。
2.3 工具层
工具层是参数校验、权限检查和日志审计的统一入口。在 Dream-SaaS 项目中,我们实现了一个通用的工具调用拦截器:
bash
@Component
public class ToolCallInterceptor {
private static final Logger log = LoggerFactory.getLogger(ToolCallInterceptor.class);
public String intercept(String toolName, Map<String, Object> args,
Callable<String> delegate) throws Exception {
long start = System.currentTimeMillis();
log.info("[MCP-TOOL] invoke: {} args={}", toolName, truncate(args, 200));
try {
String result = delegate.call();
long cost = System.currentTimeMillis() - start;
log.info("[MCP-TOOL] success: {} cost={}ms result={}",
toolName, cost, truncate(result, 200));
return result;
} catch (Exception e) {
long cost = System.currentTimeMillis() - start;
log.error("[MCP-TOOL] failed: {} cost={}ms error={}",
toolName, cost, e.getMessage());
throw e;
}
}
private String truncate(Object obj, int maxLen) {
String s = String.valueOf(obj);
return s.length() > maxLen ? s.substring(0, maxLen) + "..." : s;
}
}
日志关键字 [MCP-TOOL] 方便 grep 过滤。工具层的价值在于:业务逻辑开发者不需要关心横切关注点,只需专注实现工具功能。
2.4 执行层
执行层是真正的业务逻辑。每个工具方法调用具体的业务服务------数据库查询、外部 API 调用、文件操作等。执行层返回的结果需要封装为 MCP 协议要求的格式:
bash
@Tool(name = "searchDocs", description = "搜索项目文档,返回最匹配的文档片段")
public String searchDocs(
@ToolParam(description = "搜索关键词") String query,
@ToolParam(description = "返回结果数量", required = false) Integer topK) {
int k = (topK != null) ? topK : 5;
List<DocResult> results = docSearchService.search(query, k);
// MCP 工具返回值会自动封装为 content 数组
return results.stream()
.map(r -> String.format("【%s】\n%s\n(相关度: %.2f)",
r.getTitle(), r.getContent(), r.getScore()))
.collect(Collectors.joining("\n---\n"));
}
Spring AI 会自动将 String 返回值封装为 {"type": "text", "text": "..."} 格式,开发者不需要手动构造 JSON。
三、参数校验与错误处理
MCP Server 的参数校验比传统 REST API 更关键------因为调用方不是人类,而是 LLM。LLM 生成的参数可能类型不匹配、缺少必填字段、甚至传入完全无关的参数。
3.1 JSON Schema 自动校验
Spring AI 会根据 @ToolParam 或 @McpToolParam 注解自动生成 JSON Schema,并在 tools/list 响应中返回给 Client:
bash
{
"name": "searchDocs",
"description": "搜索项目文档,返回最匹配的文档片段",
"inputSchema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "搜索关键词"
},
"topK": {
"type": "integer",
"description": "返回结果数量",
"default": 5
}
},
"required": ["query"]
}
}
LLM 根据这个 Schema 生成参数,框架在工具调用前会自动做一轮 Schema 校验。如果参数不合法,直接返回 -32602(Invalid params)错误,不会进入业务逻辑。
3.2 增强校验:自定义校验器
对于复杂的业务校验(如"query 长度不超过 500 字符"、"topK 必须在 1-50 之间"),可以在工具方法内做前置校验:
bash
@Tool(name = "searchDocs", description = "搜索项目文档")
public String searchDocs(
@ToolParam(description = "搜索关键词") String query,
@ToolParam(description = "返回数量") Integer topK) {
// 前置校验
if (query == null || query.isBlank()) {
throw new McpToolException(-32602, "query 不能为空");
}
if (query.length() > 500) {
throw new McpToolException(-32602, "query 长度不能超过500字符");
}
if (topK != null && (topK < 1 || topK > 50)) {
throw new McpToolException(-32602, "topK 必须在1-50之间");
}
// 业务逻辑
return docSearchService.search(query, topK != null ? topK : 5);
}
3.3 结构化错误码设计
在 Dream-SaaS 项目中,我们设计了一套结构化的错误码体系,与 MCP 协议的 JSON-RPC 错误码对应:
| 错误码 | 错误类型 | 触发场景 |
|---|---|---|
| -32602 | 参数校验失败 | 类型不匹配、必填字段缺失、枚举值越界 |
| -32603 | 内部执行错误 | 业务异常、外部服务超时、数据库连接失败 |
| -32001 | 工具不存在 | 请求了未注册的工具名 |
| -32002 | 权限不足 | 当前用户/租户无权调用该工具 |
| -32003 | 资源限制 | 请求频率超限、并发数超限、配额耗尽 |
错误响应的 data 字段携带结构化信息,方便 Client 端做自动化的错误处理:
bash
{
"jsonrpc": "2.0",
"id": "req-42",
"error": {
"code": -32602,
"message": "参数校验失败: topK 必须在1-50之间",
"data": {
"field": "topK",
"value": 100,
"constraint": "range[1,50]",
"suggestion": "将 topK 设置为 1 到 50 之间的整数"
}
}
}
data.suggestion 字段是专门为 LLM 设计的------当 LLM 收到这个错误时,可以根据 suggestion 自动修正参数并重试,而不需要将错误传递给用户。
四、MCP 7/28 无状态新规范:协议级架构变更
2026 年 7 月 28 日,MCP 规范发布了一次重大更新。这次更新的核心方向是全面无状态化------移除 initialize 握手、取消协议层会话机制、让每个请求自包含。这是 MCP 自发布以来最大的一次架构变更。
4.1 变更详情
移除 initialize 握手和 Mcp-Session-Id
旧规范要求 Client 在首次连接时发送 initialize 请求完成握手,Server 分配 Mcp-Session-Id,后续所有请求都必须携带这个 Session ID。这意味着 Client 和 Server 之间形成了有状态的绑定关系。
新规范直接移除了这一机制。没有握手,没有 Session ID。Client 可以直接发送 tools/list 或 tools/call 请求,无需预先建立会话。
每个请求自包含
旧规范中,客户端信息(名称、版本、能力)在握手时一次性传递。新规范要求每个请求在 _meta 字段中携带协议版本、客户端身份和能力声明:
bash
{
"jsonrpc": "2.0",
"id": "req-1",
"method": "tools/call",
"params": {
"name": "searchDocs",
"arguments": { "query": "MCP 规范" }
},
"_meta": {
"protocolVersion": "2026-07-28",
"clientInfo": {
"name": "dream-saas-agent",
"version": "2.0.0"
},
"capabilities": {
"tools": {}
}
}
}
这意味着每个请求都是独立的、自描述的------Server 不需要维护任何客户端状态。
新增 HTTP Header
新规范引入了三个关键 Header:
| Header | 用途 | 示例值 |
|---|---|---|
MCP-Protocol-Version |
协议版本 | 2026-07-28 |
Mcp-Method |
请求方法名 | tools/call |
Mcp-Name |
工具名称 | searchDocs |
这些 Header 使得网关和负载均衡器无需解析 JSON Body 就能做出路由决策。例如 Nginx 可以基于 Mcp-Method 做差异化路由:
bash
# 工具调用请求路由到计算密集型后端
if ($http_mcp_method = "tools/call") {
proxy_pass http://mcp-compute-backend;
}
# 工具列表请求路由到轻量级后端
if ($http_mcp_method = "tools/list") {
proxy_pass http://mcp-meta-backend;
}
工具列表缓存
新规范为 tools/list 等响应引入了协议级的缓存控制:
bash
{
"jsonrpc": "2.0",
"id": "list-1",
"result": {
"tools": [...],
"_meta": {
"ttlMs": 300000,
"cacheScope": "global"
}
}
}
ttlMs:缓存有效期(毫秒),300000 = 5 分钟cacheScope:缓存作用域,global表示所有 Client 共享缓存,client表示按 Client 隔离
新增 server/discover RPC
作为 initialize 握手的替代,新规范提供了可选的 server/discover RPC 方法,用于能力发现。Client 可以选择调用它来获取 Server 的能力信息,也可以不调用------直接发送工具调用请求。这把"能力协商"从必须步骤变成了可选步骤。
4.2 新旧规范完整对比
| 维度 | 旧规范(2025-11-25) | 新规范(2026-07-28) |
|---|---|---|
| 连接模式 | 先握手,后会话绑定 | 无握手,直接请求 |
| 会话机制 | 强制携带 Mcp-Session-Id | 无协议层会话 |
| 网关路由 | 需解析 JSON Body | 读取 Mcp-Method 头即可 |
| 客户端信息 | 握手时一次性传递 | 每个请求 _meta 自包含 |
| 工具列表缓存 | 无原生支持 | 协议级 ttl + 作用域控制 |
| 负载均衡 | 必须粘性会话 | 轮询即可 |
| 长任务实现 | SSE 长连接绑定实例 | 任务句柄轮询 |
| 授权 | 基础 OAuth,可选 PKCE | 强制 OAuth 2.1 + PKCE |
| JSON Schema | draft-07 | 2020-12 |
| 错误码 | 自定义扩展 | 回归 JSON-RPC 标准码(-32020~-32099 保留) |
| 扩展机制 | 非正式 | Tasks 成为正式扩展(io.modelcontextprotocol/tasks) |
| MCP Apps | 无 | 服务端渲染 HTML 沙箱 |
4.3 对 Java Server 开发的影响
新规范对 Spring AI MCP Server 的开发有以下几个直接影响:
开发简化。 不再需要维护 Session 状态,Server 端代码更简洁。工具调用方法无需检查 Session ID 的合法性,无需处理 Session 过期和重连逻辑。Spring AI 框架层面也会相应简化------不再需要 McpSessionManager 之类的组件。
部署友好。 无状态 Server 可以直接部署在 Kubernetes 等容器编排平台上,任意 Pod 都可以处理任意请求,无需粘性会话配置。负载均衡从"必须一致性哈希"降级为"简单轮询",运维成本显著降低。
bash
# Kubernetes 部署示例------无需 sessionAffinity
apiVersion: v1
kind: Service
metadata:
name: mcp-server
spec:
selector:
app: mcp-server
ports:
- port: 8091
targetPort: 8091
# 不需要 sessionAffinity: ClientIP
type: ClusterIP
网关简化。 通过 Mcp-Method 和 Mcp-Name Header,API 网关可以在不解包 JSON Body 的情况下完成路由、限流和鉴权。这对于使用 Spring Cloud Gateway 或 Nginx 的场景非常实用。
缓存优化。 tools/list 的 ttl 机制使得 Client 端可以做协议级的缓存管理,减少不必要的网络请求。对于工具列表不经常变化的场景,可以设置较长的 ttl(如 300000ms),显著降低首次调用延迟。
五、从有状态迁移到无状态:实操建议
对于已经基于旧规范开发的 MCP Server,迁移到新规范需要关注以下几个关键点。
5.1 移除 Session 管理逻辑
如果 Server 代码中有 Session 存储(如 Redis 中的 Session 数据、内存中的 Session Map),需要评估哪些数据是真正需要持久化的(如长任务的状态),哪些是协议层 Session 的残留(如握手信息)。前者保留,后者移除。
bash
// 旧代码------需要移除
public class OldMcpHandler {
private final Map<String, McpSession> sessions = new ConcurrentHashMap<>();
public void handleInitialize(String sessionId, InitParams params) {
sessions.put(sessionId, new McpSession(params));
}
}
// 新代码------无需 Session
public class NewMcpHandler {
// 每个请求自包含 _meta,无需维护 sessions
public void handleToolCall(JsonRpcRequest request) {
ClientInfo client = request.getMeta().getClientInfo();
// 直接使用请求中的信息
}
}
5.2 迁移长任务实现
旧规范中长任务依赖 SSE 长连接绑定到特定实例。新规范改为任务句柄(Task Handle)模式:
bash
旧模式:
Client ──── SSE长连接 ────► Server实例A(任务执行中...)
新模式:
Client ──── tools/call ──► Server实例A(返回任务句柄 task-123)
Client ──── 轮询状态 ────► Server实例B(通过任务存储查询进度)
Client ──── 获取结果 ────► Server实例C(任务完成,返回结果)
需要引入任务状态存储(如 Redis 或数据库),将任务进度与具体的 Server 实例解耦。
5.3 适配新 Header
如果使用了 API 网关,需要更新路由规则:
bash
# 旧:解析 Body 中的 method 字段(需要 ngx_http_json_module)
# 新:直接读取 Header
location /mcp {
# 基于 Header 路由
proxy_set_header X-Mcp-Method $http_mcp_method;
# 基于工具名做差异化限流
limit_req_zone $http_mcp_name zone=per_tool:10m rate=10r/s;
limit_req zone=per_tool burst=20;
proxy_pass http://mcp-backend;
}
5.4 升级 JSON Schema 版本
新规范将 JSON Schema 从 draft-07 升级到 2020-12。主要变化包括:
$ref现在可以与其它关键字共存(draft-07 中$ref会忽略同级其他关键字)- 新增
$defs替代definitions - 新增
prefixItems替代旧版 tuple 类型的items数组 unevaluatedItems和unevaluatedProperties提供更精确的校验
如果工具定义中使用了 2020-12 的新特性,需要同步更新 Server 端的 Schema 生成逻辑。Spring AI 的后续版本应该会跟进这一变更。
5.5 强化授权机制
新规范要求强制使用 OAuth 2.1 + PKCE,并且增加了 RFC 9207 的 iss 验证。同时,DCR(Dynamic Client Registration)被废弃,转向 CIMD(Client Identity Metadata Document)。
如果 Server 之前的授权是可选的或使用了非标准的鉴权方式,迁移时需要统一到 OAuth 2.1 标准。具体步骤:
- 接入标准 OAuth 2.1 授权服务器(如 Keycloak、Auth0)
- 实现 PKCE 流程(
code_challenge+code_verifier) - 验证 Token 中的
iss字段(RFC 9207 要求) - 迁移 DCR 配置到 CIMD 格式
六、工具组合模式
MCP Server 暴露的是单个工具,但在实际场景中,LLM Agent 往往需要组合多个工具来完成复杂任务。以下是三种常见的工具组合模式。
6.1 Chain 模式(链式调用)
工具按顺序执行,前一个工具的输出作为后一个工具的输入:
bash
搜索文档 → 提取关键信息 → 生成摘要 → 发送通知
这种模式适合有明确先后依赖关系的场景。Agent 在规划时确定工具调用顺序,逐步执行。Spring AI 的 Agent 框架原生支持这种模式------LLM 的 ReAct 循环本身就是 Chain 模式。
6.2 DAG 模式(有向无环图)
多个工具可以并行执行,结果在某个节点汇合:
bash
┌→ 代码扫描 ─────┐
文档分析 ─┤ ├→ 综合报告
└→ 依赖分析 ─────┘
DAG 模式适合可以并行的独立子任务。Agent 框架需要支持识别哪些工具可以并行调用。在实际实现中,可以通过 CompletableFuture 在 Java 端实现并行调用:
bash
public String generateReport(String projectId) {
CompletableFuture<DocAnalysis> docs = CompletableFuture.supplyAsync(
() -> docAnalysisTool.analyze(projectId));
CompletableFuture<CodeScan> code = CompletableFuture.supplyAsync(
() -> codeScanTool.scan(projectId));
CompletableFuture<DepAnalysis> deps = CompletableFuture.supplyAsync(
() -> depAnalysisTool.analyze(projectId));
// 等待所有并行任务完成
CompletableFuture.allOf(docs, code, deps).join();
return reportGenerator.merge(docs.join(), code.join(), deps.join());
}
注意:这种并行逻辑应该在单个 MCP 工具内部实现,而不是依赖 Agent 框架的并行调用能力。从 MCP 协议的角度看,这只是一个工具调用------内部的并行对协议层透明。
6.3 Sub-Agent 模式
将复杂的工具调用任务委托给一个子 Agent,由子 Agent 自主决定调用哪些工具、以什么顺序调用。主 Agent 只关心最终结果。
这种模式适合"分析代码质量"这类需要多步判断的复杂任务。子 Agent 有自己的 System Prompt 和工具集,可以独立完成多轮工具调用。
在 Dream-SaaS 项目中,Sub-Agent 模式用于"代码审查"场景------主 Agent 将代码审查任务委托给专门的审查 Sub-Agent,Sub-Agent 内部调用代码扫描工具、复杂度分析工具、安全漏洞检测工具等,最终返回一份结构化的审查报告。
七、权限控制与超时管理
7.1 权限控制:RBAC + OPA
MCP Server 暴露的工具可能有权限限制------某些工具只对特定角色开放。实现方式有两种:
方案一:RBAC 注解
bash
@Tool(name = "executeQuery", description = "执行 SQL 查询")
@RequiresRole("admin")
public String executeQuery(@ToolParam(description = "SQL语句") String sql) {
return jdbcTemplate.queryForList(sql).toString();
}
在工具层拦截器中检查权限,不通过则返回 -32002 错误。
方案二:OPA(Open Policy Agent)集中式策略
将权限策略外置到 OPA,Server 在执行工具前调用 OPA 做策略检查。这种方式适合策略复杂、需要动态调整的场景:
bash
public class OpaAuthzInterceptor {
private final OpaClient opaClient;
public boolean authorize(String toolName, String userId, Map<String, Object> context) {
OpaRequest request = OpaRequest.builder()
.input(Map.of(
"tool", toolName,
"user", userId,
"context", context
))
.build();
return opaClient.evaluate("mcp/authz/allow", request).getResult();
}
}
7.2 超时管理
MCP 工具调用的超时管理比传统 RPC 更复杂------LLM 可能在一次推理中调用多个工具,每个工具的执行时间差异很大(文档搜索 100ms,数据库查询 2s,外部 API 调用 10s+)。
建议分层设置超时:
| 超时维度 | 推荐值 | 说明 |
|---|---|---|
| 单工具调用超时 | 根据工具特性(5s~60s) | 在工具元数据中配置 |
| Agent 单轮推理超时 | 30s~120s | 包含所有工具调用时间 |
| MCP Client 连接超时 | 10s | 建立连接的上限 |
| MCP 请求超时 | 与工具超时对齐 | 单个 JSON-RPC 请求的超时 |
bash
@Tool(name = "slowApi", description = "调用外部 API(耗时较长)")
@ToolTimeout(value = 30, unit = TimeUnit.SECONDS)
public String slowApi(@ToolParam(description = "请求参数") String param) {
return externalApiClient.call(param, Duration.ofSeconds(25));
}
7.3 熔断策略
当某个工具的后端服务不可用时,应该快速失败而不是长时间等待。推荐使用 Resilience4j 实现熔断:
bash
@CircuitBreaker(name = "externalApi", fallbackMethod = "externalApiFallback")
@Tool(name = "externalApi", description = "调用外部 API")
public String externalApi(@ToolParam(description = "请求参数") String param) {
return externalApiClient.call(param);
}
public String externalApiFallback(String param, Exception e) {
return "服务暂时不可用,请稍后重试。(熔断器已触发)";
}
熔断配置建议:
bash
resilience4j:
circuitbreaker:
instances:
externalApi:
slidingWindowSize: 10 # 滑动窗口大小
failureRateThreshold: 50 # 失败率阈值 50%
waitDurationInOpenState: 30s # 熔断开启后等待时间
permittedNumberOfCallsInHalfOpenState: 3
八、Client 集成与验收
8.1 Spring AI MCP Client 配置
配置 MCP Client 连接 Server:
bash
spring:
ai:
mcp:
client:
enabled: true
streamable-http:
connections:
dream-saas-server:
url: http://localhost:8091
endpoint: /mcp
在 Agent 中使用 MCP 工具:
bash
@RestController
public class AgentController {
@Autowired
private McpSyncClient mcpClient;
@Autowired
private ChatClient chatClient;
@GetMapping("/chat")
public String chat(@RequestParam String message) {
// 获取 MCP Server 的工具列表
List<Tool> tools = mcpClient.listTools();
// 将工具注入 ChatClient
return chatClient.prompt()
.user(message)
.tools(tools)
.call()
.content();
}
}
8.2 验收方法
MCP Server 开发完成后,可以通过以下步骤验收:
步骤一:直接调用 tools/list
bash
curl -X POST http://localhost:8091/mcp \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": "test-1",
"method": "tools/list"
}'
预期返回所有注册的工具列表,包含名称、描述和参数 Schema。
步骤二:调用单个工具
bash
curl -X POST http://localhost:8091/mcp \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": "test-2",
"method": "tools/call",
"params": {
"name": "hello",
"arguments": {"type": "greeting", "name": "World"}
}
}'
预期返回正确的工具执行结果。
步骤三:异常参数测试
bash
curl -X POST http://localhost:8091/mcp \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": "test-3",
"method": "tools/call",
"params": {
"name": "hello",
"arguments": {}
}
}'
预期返回 -32602 错误(缺少必填参数)。
步骤四:LLM 集成测试
通过 Spring AI ChatClient 发送自然语言请求,验证 LLM 能正确选择并调用工具:
bash
用户: "帮我搜索一下关于 MCP 协议的文档"
预期: LLM 调用 searchDocs 工具,返回搜索结果
步骤五:性能压测
使用 JMeter 或 Gatling 对 MCP Server 做压力测试,验证在并发场景下的稳定性和响应时间。建议的压测指标:
| 指标 | 目标值 |
|---|---|
| tools/list P99 | < 50ms |
| tools/call P99(简单工具) | < 200ms |
| tools/call P99(复杂工具) | < 5s |
| 错误率 | < 0.1% |
| 最大并发 | 根据业务需求(通常 50-200) |
总结
本篇从实战角度全面拆解了 MCP Server 的开发要点:
- 工具注册:@McpTool + Scanner 适合简单场景,@Tool + McpToolContributor 适合多模块大型项目
- 架构分层:协议层→路由层→工具层→执行层,横切关注点在工具层统一处理
- 参数校验:JSON Schema 自动校验 + 结构化错误码,降低 LLM 生成错误参数的影响
- 无状态新规范:移除握手和会话,每个请求自包含,支持轮询负载均衡和协议级缓存
- 工具组合:Chain / DAG / Sub-Agent 三种模式,适配不同复杂度的业务场景
- 权限与容错:RBAC/OPA 权限控制 + 超时管理 + 熔断策略
- 验收流程:从 tools/list 到 LLM 集成测试的完整验收链路
MCP 7/28 无状态新规范是一次方向性的调整------从"有状态的会话协议"转向"无状态的请求协议"。这个转变与 Web 从 SOAP 到 REST 的演进逻辑一致:简单、可扩展、对基础设施友好。对于 Java 开发者来说,这意味着更少的样板代码、更灵活的部署方式、更好的云原生兼容性。
下一篇预告:MCP-03《MCP Client 集成与多工具编排》,将从 Client 视角出发,讲解如何在 Spring AI 应用中集成多个 MCP Server、实现工具组合编排的高级模式,以及生产环境中的监控告警和性能调优。
--- 深入理解 AI Agent · MCP 子系列 · 第 2 篇 ---
有问题评论区见,欢迎交流~
