乐于分享
好东西不私藏

Spring AI 收紧 MCP 工具暴露后:Agent 网关要默认拒绝转发

Spring AI 收紧 MCP 工具暴露后:Agent 网关要默认拒绝转发

摘要: Spring AI 2.0.0-M5 中,MCP Server 增加了 spring.ai.mcp.server.expose-mcp-client-tools 这一工具暴露开关,并把 MCP Client 发现到的工具默认挡在 Server 外。这个变化看起来只是一个配置项,实际提醒我们:Agent 网关不应该默认转发所有工具,工具可见性要成为一条显式的架构边界。

很多团队接入 MCP 时,第一反应是把它当成工具聚合层:

多个 MCP Server   -> MCP Client 聚合工具   -> Spring AI 应用接入   -> 再作为 MCP Server 提供给上层 Agent 

这条链路很顺手。一个 Java 应用既可以消费外部 MCP 工具,也可以把内部能力包装成 MCP Server。再往前一步,它甚至可以把自己从下游 MCP Client 拿到的工具重新暴露出去,变成一个“工具中继”。

问题也出在这里。

工具一旦被重新暴露,权限、语义和审计边界就被拉长了。上层 Agent 看到的是一个统一的 MCP Server,但真实调用可能会继续穿透到搜索、文件、数据库、工单、支付、云平台或内部系统。调用链越长,越难回答几个关键问题:

  • 这个工具为什么会出现在当前 Agent 面前
  • 它是本应用自己提供的,还是从下游转来的
  • 它能读写哪些系统
  • 它失败时应该由谁兜底
  • 它的调用日志应该落在哪里

如果默认把所有下游工具都透传出去,MCP 网关就会从“受控入口”变成“能力扩散器”。Agent 开发早期追求接入速度,这种便利很诱人;但一旦进入团队级应用,它就是风险放大的起点。

默认拒绝比默认转发更像生产设计

spring.ai.mcp.server.expose-mcp-client-tools 的重要性不在于名字,而在于默认策略。

过去很多集成问题的默认值是“能连就先连上”。对传统后端接口来说,这通常还能接受,因为调用方是确定的服务,权限由认证、网关和服务端代码共同约束。

Agent 工具不是这样。工具暴露给模型之后,调用方不再只是一个稳定的业务模块,而是一个会根据上下文自主选择动作的推理系统。它可能把原本只为内部流程准备的工具组合到另一条任务链里,也可能在提示词、历史消息和工具描述影响下产生超出预期的调用顺序。

所以,工具暴露的默认值应该从“可用优先”切到“最小可见”。

spring:
ai:
mcp:
server:
expose-mcp-client-tools:false

这不是保守,而是把架构责任放回正确的位置:每一个能被 Agent 看见的工具,都应该经过命名、分组、权限、审计和失败处理的评估。下游 MCP Client 发现到工具,不代表上游 MCP Server 就应该继续发布它。

MCP 网关要区分三类工具

一个面向 Agent 的 MCP 网关,至少应该把工具分成三类。

第一类是本地业务工具。

这些工具由当前应用直接实现,边界清楚,通常可以和业务权限、租户、审计日志直接绑定。例如 query_customer_profilecreate_support_ticketapprove_refund_request

第二类是下游基础工具。

这些工具来自其他 MCP Server,负责搜索、文件、数据库、浏览器、代码仓库、云资源等基础能力。它们能力强,但上下文敏感,不应该不加选择地交给所有上层 Agent。

第三类是组合工具。

组合工具内部可能调用多个下游工具,但对上层只暴露一个更窄的业务动作。例如不要把“数据库查询工具”和“工单写入工具”一起交给模型,而是暴露 summarize_customer_issue 或 prepare_refund_review。这样上层 Agent 拿到的是业务语义,不是原始操作面。

raw tools        下游 MCP 工具,不默认外放 business tools   当前服务直接提供,可按业务权限开放 composed tools   内部编排多个工具,对外只给窄接口 

生产环境里,真正应该被广泛暴露的往往是第二类工具的“受限包装”,而不是第二类工具本身。

工具转发需要一张允许清单

如果确实需要把部分下游 MCP 工具重新暴露给上层,最好把它设计成显式允许清单,而不是打开全局转发。

示意代码可以是这样的:

@Bean
ToolCallbackProvider exposedMcpTools(ToolCallbackProvider downstreamTools){
var allowedNames = Set.of(
"search_internal_docs",
"read_ticket_summary",
"create_draft_issue"
    );

var callbacks = Arrays.stream(downstreamTools.getToolCallbacks())
        .filter(tool -> allowedNames.contains(tool.getToolDefinition().name()))
        .toArray(ToolCallback[]::new);

return ToolCallbackProvider.from(callbacks);
}

重点不是这段代码本身,而是它背后的流程:

  • 允许清单要进入版本控制
  • 每个工具要有 owner
  • 每个工具要标注读写级别
  • 每个工具要说明适用 Agent
  • 写操作工具要有确认、人审或补偿路径
  • 工具描述变更要触发回归测试

很多 Agent 事故不是模型“突然失控”,而是工具边界本来就太宽。模型只是沿着已经暴露出来的能力继续推理。

不要把工具描述当权限系统

还有一个常见误区:在工具描述里写“仅用于查询,不要修改数据”,然后认为这就完成了权限控制。

工具描述只能帮助模型理解何时调用工具,不能替代权限系统。真正的控制应该在工具执行前后完成:

Agent 选择工具   -> 网关校验当前 Agent 是否可见   -> 校验当前用户和租户是否可调用   -> 校验参数是否越界   -> 执行工具   -> 记录结构化审计事件   -> 对敏感结果做输出过滤 

这条链路里,MCP Server 是很自然的控制点。它既知道暴露给上层的工具列表,也知道每次工具调用的名称、参数和结果。把下游工具原样转发出去,等于绕过了这层可以治理的边界。

对 Java 团队来说,Spring AI 的价值就在于它可以把这些控制放进熟悉的 Spring Boot 体系:配置、Bean、拦截器、指标、日志、安全上下文、测试都能一起工作。MCP 不必变成一堆散落在开发者电脑里的 JSON 配置。

评估一个 MCP 工具能否外放

判断某个工具能不能从下游转发到上游,可以用一张很小的检查表。

第一,它是否有清晰的业务语义。

run_sqlexecute_shellbrowser_click 这类工具太接近底层能力,除非运行在强沙箱里,否则不适合直接暴露给通用 Agent。更好的方式是封装成受限业务动作。

第二,它是否有稳定的输入输出契约。

Agent 工具越依赖自然语言描述,越难测试。能外放的工具应该有清晰 schema、错误码、超时、重试和结果结构。

第三,它是否支持租户和身份透传。

如果工具内部不知道当前用户是谁,也不知道当前租户是谁,那它很难承担生产调用。至少要能从上下文里拿到身份信息,并在服务端重新校验。

第四,它是否可审计。

工具调用日志不能只是一段模型对话。应该包含工具名、调用者、用户、租户、参数摘要、结果状态、耗时、关联任务和风险级别。

第五,它是否有降级和回滚设计。

读工具失败时可以返回空结果或提示稍后重试。写工具失败时要能避免半完成状态。高风险写工具最好先生成草稿、变更计划或审批单,而不是直接落库。

这些问题都回答清楚,再考虑把工具加入允许清单。

Agent 架构正在从“接工具”走向“管工具”

MCP 让工具接入变得标准化,这是好事。但标准化接入之后,真正的架构挑战会转向工具治理:哪些工具可以被发现,哪些工具可以被模型看到,哪些工具可以被某个用户调用,哪些调用必须暂停等待人工确认。

Spring AI 2.0.0-M5 里的这个配置项只是一个很小的工程信号,却很值得 AI 应用架构师关注。它说明框架层开始承认一个事实:Agent 应用里的工具不是普通依赖,而是外部行动能力;行动能力不应该因为技术上可转发,就自动穿透到更上层。

以后设计 MCP 网关时,不妨把默认原则写得更直接一点:

发现不等于暴露 暴露不等于授权 授权不等于免审计 

当团队能把这三件事分开,MCP 才会从“让 Agent 连上更多系统”的工具,变成“让 Agent 以可控方式使用系统”的架构层。

#Spring AI #MCP #Agent应用架构 #AI应用开发 #工具调用