如果说 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 AI 2.0.0、Spring Boot 4.1.0、Spring Cloud 2025.1.2 为当前入口;Spring Cloud 兼容矩阵同时标明 2025.1.x Oakwood 对应 Spring Boot 4.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.x Oakwood 对应 Spring Boot 4.0.x。因此如果项目已经升到 Boot 4.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。
  • readOnlyHintdestructiveHintidempotentHintopenWorldHint 是客户端提示,但不能当成安全策略本身。
  • 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));
    }
}

资源设计要注意三件事:

  1. URI 是契约,不要把数据库表名、内部主键规则和临时路径暴露出去。
  2. 返回值要面向 Agent 任务裁剪,不要把完整业务对象无脑返回。
  3. audienceprioritylastModified 这类元数据应服务于工具选择、缓存、刷新和审计。

提示模板同样建议版本化:

@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());
    }
}

还要补三条生产约束:

  1. 工具参数只能表达业务条件,不能表达权限。
  2. 返回值要按当前用户权限裁剪字段。
  3. 审计日志要记录认证得到的 tenantIduserIdagentId,而不是模型声称的身份。

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

注册表的来源可以分阶段:

  1. MVP 阶段:应用启动时扫描 MCP 工具列表,写入内部表。
  2. 平台阶段:CI 阶段导出工具 Schema,提交到 Git 管理。
  3. 生产阶段:工具契约进入审批流程,只有批准后的工具能被 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 工具测试至少要覆盖五层:

  1. 契约测试:工具名、参数 Schema、输出 Schema、Hints 是否符合预期。
  2. 鉴权测试:不同 tenant、user、role、agentId 是否得到正确允许或拒绝。
  3. 数据裁剪测试:返回值是否泄露内部字段、跨租户数据或敏感信息。
  4. 重试测试:幂等工具重复调用是否不会产生额外副作用。
  5. 通知测试:进度、日志、工具列表变更、人工补充是否能被客户端 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,显式补齐 nametitledescription、参数描述和 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 适合提出建议和组织信息,真正改变生产系统的动作要经过明确授权。

参考资料

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐