Spring AI 2.0 MCP Annotations:把 Agent 工具从“方法”升级为“可治理契约”
如果说 MCP 网关解决的是“工具入口如何被治理”,那么 MCP Annotations 解决的是“工具本身如何被平台理解”。两者合起来,Java 团队才能把 Agent 工具从 Demo 能力推进到企业级可运营能力。
分享日期:2026-06-24
主题:Java / Spring AI 2.0.0 / MCP Annotations / Spring Boot 4.1.0 / Spring Cloud 2025.1.2 / Agent Tool Contract / Security / Observability
版本背景:截至 2026-06-24,Spring 官方项目页显示 Spring AI2.0.0、Spring Boot4.1.0、Spring Cloud2025.1.2为当前入口;Spring Cloud 兼容矩阵同时标明2025.1.xOakwood 对应 Spring Boot4.0.x,真实项目组合需以 BOM、Release Notes 和 start.spring.io 生成结果为准。
1. 为什么今天值得关注
过去几天我们已经聊过 Spring AI 2.0、MCP 网关治理、模型观测、评估、递归 Advisors、Spring Modulith 事件一致性、Spring Batch 承载 Agent 长任务等主题。今天适合补上更底层的一块:Agent 工具到底应该怎样定义,才能从 Demo 方法变成生产级契约。
很多团队接入 Agent 时,最开始的工具定义通常很直接:
@Tool(description = "Query order status by order id")
OrderStatus findOrderStatus(String orderId) {
return orderService.findStatus(orderId);
}
这在单体应用、内部验证、只读查询场景下足够快。但一旦工具开始跨服务、跨团队、跨 Agent 复用,问题会马上出现:
- 方法名是不是稳定的工具标识?
- 参数是否有 JSON Schema,可以被客户端、平台和测试用例理解?
- 工具是只读、幂等、破坏性写入,还是会访问外部世界?
- 结果能不能生成输出 Schema,让调用方判断结构是否稳定?
- 工具返回的资源、提示模板、补全能力怎样被发现和版本化?
- 工具长时间执行时,进度、日志、用户补充信息怎样回传?
- 当工具列表变化时,Agent 的本地工具缓存怎样刷新?
Spring AI 2.0 的 MCP Annotations 正是围绕这些问题给 Java 团队提供的工程入口。它不是简单地把 @Tool 换成 @McpTool,而是把工具、资源、提示模板、补全、日志、进度和用户交互都放进 MCP 这条标准化边界里。
一句话:如果 Spring AI 的 Tool Calling 解决“模型如何请求工具”,MCP Annotations 解决的就是“工具如何以可发现、可描述、可治理的契约暴露给 Agent 生态”。
2. 版本坐标与事实边界
今天这篇分享基于下面几个官方事实:
- Spring AI 2.0.0:官方项目页展示为当前版本,参考文档已经包含 MCP Client Boot Starters、MCP Server Boot Starters、MCP Annotations、MCP Security、Tool Calling、Observability、Evaluation 等页面。
- Spring Boot 4.1.0:官方项目页展示为当前 Spring Boot 入口,提供自动配置、Actuator、Micrometer、Security、WebMVC / WebFlux 等运行底座。
- Spring Cloud 2025.1.2:官方项目页展示为当前 Spring Cloud 入口;页面中的兼容矩阵说明
2025.1.xOakwood 对应 Spring Boot4.0.x。因此如果项目已经升到 Boot4.1.x,需要等待或确认 Cloud 侧兼容说明,不要只按“最新版本”硬拼。 - MCP Security:Spring AI 参考文档中该页仍标注为 WIP,并说明相关模块来自
spring-ai-community/mcp-security,是社区驱动项目,尚未被 Spring AI 或 MCP 项目正式背书。生产系统可以借鉴设计方向,但要按自身安全基线做验证。
这几个事实决定了今天的落地建议:MCP Annotations 可以放心作为 Spring AI 2.0 的主线能力研究和试点,但安全模块、Cloud 组合、Boot 小版本组合要按项目实际 BOM 和官方兼容矩阵收敛。
3. 从本地方法到 MCP 契约
Spring AI MCP Server Annotations 提供了一组声明式注解,用于把 Spring Bean 中的方法注册为 MCP 能力:
| 注解 | 作用 | 更适合表达什么 |
|---|---|---|
@McpTool |
暴露可调用工具 | 查询、计算、创建草稿、触发低风险动作 |
@McpToolParam |
描述工具参数 | 参数含义、是否必填、Schema 生成 |
@McpResource |
暴露资源 URI | 配置、文档片段、只读业务对象 |
@McpPrompt |
暴露提示模板 | 可复用 Agent 工作说明、领域 Prompt |
@McpComplete |
暴露补全能力 | prompt 参数补全、resource URI 补全 |
一个库存查询工具可以这样写:
import org.springframework.ai.mcp.annotation.McpTool;
import org.springframework.ai.mcp.annotation.McpToolParam;
import org.springframework.stereotype.Component;
@Component
class InventoryMcpTools {
private final InventoryApplicationService inventory;
private final CurrentUser currentUser;
InventoryMcpTools(InventoryApplicationService inventory, CurrentUser currentUser) {
this.inventory = inventory;
this.currentUser = currentUser;
}
@McpTool(
name = "inventory.findAvailableStock",
title = "Find Available Stock",
description = "Find available stock for a SKU in the current authenticated tenant.",
generateOutputSchema = true,
annotations = @McpTool.McpAnnotations(
readOnlyHint = true,
destructiveHint = false,
idempotentHint = true,
openWorldHint = false
)
)
StockView findAvailableStock(
@McpToolParam(description = "Business SKU code", required = true)
String sku) {
return inventory.findAvailableStock(currentUser.tenantId(), sku);
}
}
record StockView(String sku, int available, String region, String freshness) {
}
这里的重点不是“少写代码”,而是把几个以前藏在方法实现里的事实显式化:
name是工具契约名,不要依赖 Java 方法名自然演进。title面向 UI 和用户展示,description面向模型理解。generateOutputSchema = true能让非基础返回类型生成输出 Schema。readOnlyHint、destructiveHint、idempotentHint、openWorldHint是客户端提示,但不能当成安全策略本身。tenantId来自认证上下文,不来自模型参数。
生产系统的工具定义应该像 API 定义,而不是像“给模型开的内部后门”。
4. Tool Hints 不是权限系统,但能驱动治理策略
Spring AI 文档里 @McpTool.McpAnnotations 提供了几个很实用的提示字段:
| Hint | 推荐理解 | 平台侧可以怎么用 |
|---|---|---|
readOnlyHint |
工具不修改环境 | 默认允许自动调用,但仍要鉴权和限流 |
destructiveHint |
工具可能产生破坏性更新 | 默认要求人工确认或审批流 |
idempotentHint |
相同参数重复调用无额外影响 | 允许重试,但要配合幂等键和审计 |
openWorldHint |
工具会访问外部实体 | 更严格的出站网络、脱敏和内容审查 |
这些字段的价值在于让工具目录变得“可计算”。平台可以根据它们生成策略:
agent-tool-policy:
defaults:
read-only:
auto-call: true
max-retry: 2
audit-level: summary
idempotent-write:
auto-call: true
require-idempotency-key: true
max-retry: 1
audit-level: full-metadata
destructive-write:
auto-call: false
require-human-approval: true
audit-level: full
open-world:
require-egress-policy: true
redact-response: true
但要强调:Tool Hint 不是鉴权结果。 它只能帮助客户端和平台做工具选择、展示、确认、重试和审计。真正的权限判断仍然必须发生在 Gateway、Spring Security 和业务服务内部。
一个可落地的规则是:
模型能看到的工具描述,只影响“是否应该调用”;服务端安全上下文,才决定“是否允许执行”。
5. 资源和提示模板也要契约化
MCP 不只暴露工具。很多 Agent 应用真正混乱的地方,是把资源和 Prompt 放在代码、配置、数据库、运营后台之间到处复制。@McpResource 和 @McpPrompt 可以把这些能力也纳入统一发现和治理。
比如给 Agent 暴露一个只读配置资源:
@Component
class PolicyResources {
private final PolicyRepository policyRepository;
PolicyResources(PolicyRepository policyRepository) {
this.policyRepository = policyRepository;
}
@McpResource(
uri = "policy://risk/{policyId}",
name = "risk-policy",
title = "Risk Policy",
description = "Read a published risk policy by policy id.",
mimeType = "application/json",
annotations = @McpResource.McpAnnotations(
audience = { Role.ASSISTANT },
priority = 0.9
)
)
PolicyDocument readRiskPolicy(String policyId) {
return policyRepository.findPublished(policyId)
.orElseThrow(() -> new PolicyNotFoundException(policyId));
}
}
资源设计要注意三件事:
- URI 是契约,不要把数据库表名、内部主键规则和临时路径暴露出去。
- 返回值要面向 Agent 任务裁剪,不要把完整业务对象无脑返回。
audience、priority、lastModified这类元数据应服务于工具选择、缓存、刷新和审计。
提示模板同样建议版本化:
@Component
class AgentPrompts {
@McpPrompt(
name = "ticket-risk-review-v2",
title = "Ticket Risk Review",
description = "Review a support ticket and classify risk with citations."
)
GetPromptResult ticketRiskReview(
@McpArg(name = "tenantPolicy", required = true) String tenantPolicy,
@McpArg(name = "ticketText", required = true) String ticketText) {
String prompt = """
Review the support ticket under the provided tenant policy.
Return riskLevel, reason, requiredActions, and citations.
Policy:
%s
Ticket:
%s
""".formatted(tenantPolicy, ticketText);
return GetPromptResult.builder(List.of(
new PromptMessage(Role.USER, TextContent.builder(prompt).build())))
.description("Ticket risk review prompt v2")
.build();
}
}
企业里不要只治理工具,不治理 Prompt。Prompt 一旦能被多个 Agent 复用,就应该像 API 一样有命名、版本、负责人、灰度和回滚。
6. Client Annotations:把服务器通知接回 Agent 运行时
Server Annotations 解决“服务端如何暴露能力”,Client Annotations 解决“客户端如何处理 MCP Server 发回来的通知和交互请求”。Spring AI 文档中提到的典型注解包括:
@McpLogging:处理 MCP Server 的日志通知。@McpProgress:处理长任务进度通知。@McpToolListChanged:处理工具列表变化通知。@McpElicitation:处理服务器向用户请求额外信息。@McpSampling:处理服务器请求客户端进行模型采样。
这些注解都需要通过 clients 参数绑定到具体 MCP client 连接名。这个设计很重要,因为生产系统通常不会只有一个 MCP Server。
@Component
class McpClientEventHandlers {
private final ToolRegistry toolRegistry;
private final AgentRunStore agentRunStore;
McpClientEventHandlers(ToolRegistry toolRegistry, AgentRunStore agentRunStore) {
this.toolRegistry = toolRegistry;
this.agentRunStore = agentRunStore;
}
@McpProgress(clients = "inventory-tools")
void onInventoryProgress(ProgressNotification notification) {
agentRunStore.updateToolProgress(
notification.progressToken(),
notification.progress(),
notification.total(),
notification.message());
}
@McpToolListChanged(clients = "inventory-tools")
void onInventoryToolsChanged(List<McpSchema.Tool> tools) {
toolRegistry.replaceTools("inventory-tools", tools);
}
@McpLogging(clients = "inventory-tools")
void onInventoryLog(LoggingMessageNotification notification) {
agentRunStore.appendToolLog(
"inventory-tools",
notification.level().name(),
notification.data());
}
}
这让 Agent 平台能处理几个生产问题:
- 长任务不再只靠 HTTP 等待,可以把进度写入任务表或 WebSocket 推给前端。
- MCP Server 工具变更后,Agent 的工具索引可以自动刷新。
- 工具日志可以纳入同一个 trace / conversation / toolCallId。
- 服务端需要补充信息时,可以通过 elicitation 转成人工确认或表单输入。
这里有一个边界要守住:不要让 MCP Server 任意触发用户输入收集。 @McpElicitation 适合受控的人机协同场景,比如“确认是否创建变更单”“补充审批理由”“选择候选资源”。它不应该成为绕过前端表单、权限和审计的通道。
7. MCP Server Boot Starter 的协议选择
Spring AI MCP Server Boot Starter 文档说明,Server 可以通过不同 starter 和 spring.ai.mcp.server.protocol 配置多种传输方式:
| 场景 | 推荐方向 |
|---|---|
| 本地开发、CLI 工具、子进程集成 | STDIO |
| 独立 HTTP 服务、需要连接保持或多消息 | Streamable HTTP |
| 云原生无状态部署、请求间不保留会话 | Stateless |
| 旧 SSE 形态 | 2.0.0 后优先评估 Streamable HTTP 替代 |
WebMVC 形态示例:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
</dependency>
spring:
ai:
mcp:
server:
name: inventory-mcp-server
protocol: STREAMABLE
type: SYNC
annotation-scanner:
enabled: true
如果工具方法是同步实现,配置 SYNC 更简单;如果工具天然是非阻塞 I/O、需要 Reactor 类型返回值,再考虑 ASYNC。不要为了“看起来更先进”把所有工具改成异步。同步工具的限流、超时、线程池隔离和幂等性做好,通常比盲目引入响应式栈更重要。
8. 传递安全上下文:不要信任模型参数
MCP 工具最容易出事的地方,是把模型输入当成用户身份或权限上下文。例如:
@McpTool(description = "Find invoices by tenant")
List<InvoiceView> findInvoices(String tenantId, String customerId) {
return invoiceService.findInvoices(tenantId, customerId);
}
这段代码的问题很明显:tenantId 来自模型参数,模型可能被用户提示词影响,也可能在上下文混乱时传错租户。正确方向是:业务身份来自认证链路,模型只能提供业务查询条件。
@McpTool(
name = "invoice.findByCustomer",
description = "Find invoices for a customer in the current authenticated tenant.",
annotations = @McpTool.McpAnnotations(readOnlyHint = true, openWorldHint = false)
)
List<InvoiceView> findInvoices(
McpSyncRequestContext requestContext,
@McpToolParam(description = "Customer id visible to the current user", required = true)
String customerId) {
AuthContext auth = AuthContext.from(requestContext.transportContext());
return invoiceService.findInvoices(auth.tenantId(), auth.userId(), customerId);
}
Spring AI MCP Server 文档也提到,可以通过 transport provider 的 contextExtractor 把 HTTP 头等传输层信息放进 McpTransportContext。但生产实现里不要只把 Authorization 字符串往里塞完就结束,建议做成明确的安全上下文:
record AuthContext(String tenantId, String userId, Set<String> roles, String agentId) {
static AuthContext from(McpTransportContext context) {
String token = (String) context.get("authorization");
JwtClaims claims = TokenVerifier.verify(token);
return new AuthContext(
claims.tenantId(),
claims.subject(),
claims.roles(),
claims.agentId());
}
}
还要补三条生产约束:
- 工具参数只能表达业务条件,不能表达权限。
- 返回值要按当前用户权限裁剪字段。
- 审计日志要记录认证得到的
tenantId、userId、agentId,而不是模型声称的身份。
9. Spring Cloud 放在哪里
MCP Annotations 让工具定义更规范,但不负责完整的平台治理。Spring Cloud 的角色仍然很清晰:
| Spring Cloud 能力 | 在 MCP Agent 架构里的位置 |
|---|---|
| Gateway | MCP Server 统一入口、路由、Token Relay、限流、熔断 |
| Config | 工具开关、工具分组、Prompt 版本、Agent Profile |
| Circuit Breaker | 外部模型、MCP Server、业务服务的失败隔离 |
| Stream | 工具调用事件、审批事件、评估事件的异步流转 |
| Task | 有限生命周期的 Agent 工具任务或批处理任务 |
| Kubernetes | 服务发现、配置、滚动发布和运行环境治理 |
推荐架构如下:
flowchart LR
UI[Admin UI / Agent Client] --> AgentApi[Spring Boot Agent API]
AgentApi --> ChatClient[Spring AI ChatClient]
ChatClient --> McpClient[MCP Client]
McpClient --> Gateway[Spring Cloud Gateway]
Gateway --> ServerA[Inventory MCP Server]
Gateway --> ServerB[Ticket MCP Server]
Gateway --> ServerC[Policy MCP Server]
ServerA --> ToolA[@McpTool]
ServerB --> ToolB[@McpTool / @McpResource]
ServerC --> Prompt[@McpPrompt]
Gateway --> Security[Spring Security / OAuth2 / API Key]
AgentApi --> Registry[Tool Registry / Policy]
AgentApi --> Audit[Audit / Metrics / Trace]
和 6 月 18 日那篇 MCP 网关治理不同,今天的重点在工具契约层:Gateway 管入口,Annotations 管能力描述,业务服务管真实权限,Agent 平台管工具选择和审计。
10. 工具契约注册表:从注解到平台能力
只在代码里写注解还不够。工具数量超过 20 个后,团队通常需要一个工具契约注册表,记录每个工具的治理元数据:
tools:
- name: inventory.findAvailableStock
owner: supply-chain-platform
server: inventory-tools
version: 2026-06-24
risk: read-only
readOnlyHint: true
destructiveHint: false
idempotentHint: true
openWorldHint: false
allowedAgents:
- order-support-agent
- replenishment-agent
allowedRoles:
- SUPPORT_AGENT
- INVENTORY_VIEWER
maxQpsPerTenant: 20
timeout: 3s
audit:
includeArguments: summary
includeResult: metadata
retention: 90d
注册表的来源可以分阶段:
- MVP 阶段:应用启动时扫描 MCP 工具列表,写入内部表。
- 平台阶段:CI 阶段导出工具 Schema,提交到 Git 管理。
- 生产阶段:工具契约进入审批流程,只有批准后的工具能被 Agent Profile 引用。
这能解决几个实际问题:
- 谁负责这个工具。
- 哪些 Agent 能调用。
- 是否允许自动调用。
- 调用失败能不能重试。
- 是否需要人工确认。
- 参数和结果如何审计。
- 工具下线是否会影响已有 Agent。
如果没有注册表,工具注解只是“更漂亮的暴露方式”;有了注册表,注解才会变成平台治理输入。
11. 进度、日志和人工补充:把长工具调用做成可运营任务
很多 MCP 工具不是毫秒级查询,而是几秒到几分钟的操作,例如:
- 拉取一批工单并生成风险摘要。
- 扫描仓库配置并生成修复计划。
- 查询多个系统汇总客户画像。
- 运行一个外部评估任务。
这类工具如果只返回最终字符串,线上体验会很差。建议把工具调用拆成四个状态:
| 状态 | 处理方式 |
|---|---|
SUBMITTED |
记录 toolCallId、agentRunId、参数摘要 |
RUNNING |
通过 @McpProgress 更新进度 |
WAITING_FOR_INPUT |
通过 @McpElicitation 进入人工补充或确认 |
COMPLETED / FAILED |
写入结果摘要、错误码、耗时和重试信息 |
Agent 侧可以把这些状态落进自己的运行记录:
record AgentToolCallState(
String agentRunId,
String toolCallId,
String mcpClientName,
String toolName,
String status,
double progress,
String message,
String errorCode,
Instant updatedAt) {
}
这样排障时就能回答:
- 是模型没有选择工具,还是工具执行失败?
- 是 Gateway 限流,还是 MCP Server 超时?
- 是业务鉴权拒绝,还是参数校验失败?
- 是工具已经发出人工补充请求,但用户没有确认?
- 是工具列表变更后 Agent 使用了旧缓存?
Agent 平台的成熟度,很多时候就体现在这些“非理想路径”上。
12. 高风险写操作:默认生成计划,不默认执行动作
MCP Annotations 让工具暴露变简单,也让危险动作更容易被包装成工具。建议把工具按风险分层:
| 风险级别 | 示例 | 默认策略 |
|---|---|---|
| L0 只读 | 查询库存、读取政策、查工单状态 | 可自动调用 |
| L1 低风险幂等写 | 创建草稿、保存分析结果、写审计事件 | 可自动调用,但必须有幂等键 |
| L2 业务影响写 | 创建工单、提交审批、发送通知 | 生成待确认动作 |
| L3 高风险动作 | 删除数据、退款、改权限、执行生产变更 | 禁止模型直接执行 |
高风险场景下,工具应该返回计划,而不是直接改系统:
@McpTool(
name = "change.createRemediationPlan",
description = "Create a remediation plan for a risky production change. Does not execute the change.",
generateOutputSchema = true,
annotations = @McpTool.McpAnnotations(
readOnlyHint = false,
destructiveHint = false,
idempotentHint = true,
openWorldHint = false
)
)
RemediationPlan createRemediationPlan(
@McpToolParam(description = "Change request id", required = true)
String changeId,
@McpToolParam(description = "Observed risk summary", required = true)
String riskSummary) {
return changePlanner.plan(changeId, riskSummary);
}
record RemediationPlan(
String planId,
String changeId,
List<String> recommendedSteps,
List<String> rollbackSteps,
boolean requiresHumanApproval) {
}
真正执行动作的 API 保留在审批系统或工作流系统里,由明确的用户动作触发。这样 Agent 仍然能提高效率,但不会绕过组织已有的变更控制。
13. 测试策略:不要只测方法返回值
MCP 工具测试至少要覆盖五层:
- 契约测试:工具名、参数 Schema、输出 Schema、Hints 是否符合预期。
- 鉴权测试:不同 tenant、user、role、agentId 是否得到正确允许或拒绝。
- 数据裁剪测试:返回值是否泄露内部字段、跨租户数据或敏感信息。
- 重试测试:幂等工具重复调用是否不会产生额外副作用。
- 通知测试:进度、日志、工具列表变更、人工补充是否能被客户端 handler 正确处理。
可以在 CI 中生成工具清单快照:
{
"server": "inventory-tools",
"version": "2026-06-24",
"tools": [
{
"name": "inventory.findAvailableStock",
"readOnlyHint": true,
"destructiveHint": false,
"idempotentHint": true,
"openWorldHint": false,
"inputSchemaHash": "sha256:...",
"outputSchemaHash": "sha256:..."
}
]
}
当快照变化时,要求代码评审回答三个问题:
- 这是兼容变更还是破坏性变更?
- 哪些 Agent Profile 会受到影响?
- 是否需要同步更新 Prompt、评估集和权限策略?
这比线上才发现“工具描述改了,模型开始乱调”要可靠得多。
14. 从 @Tool 迁移到 MCP Annotations 的建议路径
如果团队已经有一批 Spring AI 本地 @Tool,不要一口气全部改成远程 MCP。建议按风险和复用价值迁移:
第一步:盘点现有工具,按只读、幂等写、业务影响写、高风险写分层。
第二步:挑 1 到 3 个只读工具迁移到 @McpTool,显式补齐 name、title、description、参数描述和 Hints。
第三步:为这些工具建立最小注册表,绑定 owner、allowedAgents、timeout、auditLevel。
第四步:引入 MCP Client handler,至少接入 @McpProgress、@McpLogging、@McpToolListChanged。
第五步:把工具入口放到 Gateway 后面,补齐认证、限流、熔断、审计和 traceId 透传。
第六步:对写工具只先暴露“创建草稿 / 生成计划 / 提交待审批任务”,不要直接暴露最终执行动作。
第七步:再考虑动态工具搜索、跨团队工具市场、灰度和多 Agent 复用。
迁移成功的标志不是“工具都变成 MCP 了”,而是“工具有契约、有策略、有审计、有回滚路径”。
15. 今日结论
Spring AI 2.0 MCP Annotations 的价值,不是让 Java 方法多一个注解,而是给 Agent 工具体系补上契约层。
今天可以带走三条实践原则:
- 工具名、参数、返回值、Hints 都是契约:不要把它们当成临时描述文本。
- 注解描述不等于安全边界:鉴权、租户隔离、字段脱敏、限流和审计必须落在平台和业务服务中。
- 高风险工具默认生成计划,不默认执行动作:Agent 适合提出建议和组织信息,真正改变生产系统的动作要经过明确授权。
参考资料
更多推荐
所有评论(0)