本地能跑通,不等于你敢上线

如果你最近在做 Spring AI + MCP,大概率已经经历过这个阶段:

  • tools/list 通了
  • 模型能选中工具了
  • 本地 demo 也确实跑起来了
  • 于是团队里开始有人说,可以准备往测试环境推了

这时候最容易出现一种错觉:

既然 MCP 已经接通了,剩下的无非就是把功能再补一补。

但真正危险的地方,往往不是“接不通”,而是“接通以后你把什么东西暴露出去了”。

我见过不少项目,卡在的都不是协议层,而是上线前才开始冒头的这些问题:

  • 一个本来只想查数据的 tool,最后被做成了半个管理后台入口
  • 用户态和服务态授权没拆开,结果谁来调用都拿同一套权限
  • Prompt 里写了“只允许当前租户”,服务端却没有真正做强校验
  • 排障时把日志全开,最后先把 prompt、参数、返回内容一起送进了日志系统
  • 自动发现和自动注册一开,团队自己都说不清当前环境到底暴露了哪些工具

说白了,MCP 连通只解决了“能不能跑”,没有解决“能不能安全地跑到线上”。

真正把系统推到测试环境、灰度环境,甚至生产环境后,问题往往不是出在 MCP 连不上,而是出在下面这些地方:

  • 认证到底是用户态,还是服务态
  • 工具到底暴露了多少能力
  • 多租户数据边界是不是靠模型“自觉遵守”
  • 日志和追踪到底该记到什么程度
  • 自动发现、自动注册、自动初始化是不是把风险一起自动放大了

如果你最近正在用 Spring Boot + Spring AI + MCP 做 Agent、助手、Copilot 或企业内部智能平台,这篇建议你直接留下。
因为下面这 6 个坑,几乎每一个都不是“写得优不优雅”的问题,而是“上线后会不会翻车”的问题。


这篇不讲怎么接通,只讲怎么避免上线后出事

Spring 官方这几个月在持续推进 AI 和 MCP 相关能力,但官方资料也已经把一个信号讲得很清楚:

  • MCP 不只是一个“工具调用协议”,它还涉及认证和授权边界[^1]
  • Spring AI 在 MCP 场景下已经给出了专门的安全支持方案和自动配置能力[^2]
  • Spring AI 的工具调用、可观测性、日志内容导出,本身就带着明显的安全取舍[3][4]

也就是说,今天的 Spring AI 接 MCP,已经不是“能连通就算做完”的阶段了。
真正的门槛,是你有没有把权限、安全、隔离和审计这些后面的事一起设计进去。

先看一张总览图。

MCP Online Risk Path This diagram shows the key control points when a Spring AI application connects to MCP, from user request through model routing, tool execution, authorization, tenant isolation, and audit logging.

Yes

No

Yes

No

Yes

No

Yes

No

User request

Spring AI application

Model decides tool call

MCP client or server

Auth boundary clear

Tool scope minimal

Tenant isolation enforced

Audit safe and useful

Ready for online rollout

Online incident


第一个坑:把“连通成功”当成“安全验收通过”

很多人做第一轮联调时,验收标准只有三个:

  • tools/list 能返回
  • 模型能选中工具
  • 工具能成功执行一次

但这顶多说明链路通了,不说明边界对了。

MCP 在官方授权文档里已经明确把认证、授权放到了协议层面考虑,尤其是基于 HTTP 的远程 MCP 场景,客户端和服务器需要通过标准授权机制建立可信访问关系。[^1]
Spring 团队在讲 MCP 安全接入时,也明确强调了令牌传播、客户端认证、资源服务器保护这些问题。[2][5]

换句话说,真正要验收的不是“能不能调”,而是:

你该验什么 只看连通时经常漏掉什么
谁能访问 MCP Server 把内网能访问的人都默认当成可信调用方
调用拿的是什么令牌 用户令牌和服务令牌混在一起
某个工具是否该被当前会话调用 工具一旦注册成功,所有请求都能碰到它
拒绝访问时会发生什么 没有明确的 401、403 处理和回退逻辑

更现实一点地说,本地演示环境里“连通成功”经常只是说明:

你在一个低风险、低并发、单用户、单租户场景下,看到了 happy path。

这和上线是两回事。

怎么改

  • 把“调用成功率”从验收表里降级成基础项
  • 把“未授权时是否拒绝”“越权时是否拦截”“错误时是否可审计”加进主验收项
  • 对每个 MCP Server 分开定义信任边界,不要因为都是内部系统就默认互信

第二个坑:没有分清用户态授权和服务态授权

这是接 MCP 时最容易在架构上埋雷的点。

很多团队一开始默认只有一种身份:

  • 要么把所有请求都当成“当前用户在发起”
  • 要么把所有请求都当成“系统服务自己在发起”

实际上一到线上,这两类流量往往必须拆开。

Spring AI 的 MCP 安全文档专门提到了一类很典型的问题:
应用启动时就可能要做 MCP 初始化,比如工具发现;但这时候压根还没有用户会话,也就不可能天然带上用户令牌。文档把这类问题称为 “hybrid flow” 场景,并建议显式区分启动阶段和用户请求阶段的认证策略。[^2]

这个问题一旦没拆清楚,后面通常会出现两种坏结果:

  • 系统启动依赖用户态令牌,结果一启动就卡死
  • 所有工具都走服务账号,结果上线后每个用户都拿着同一把“大钥匙”

你应该怎么理解这件事

可以把它想成两条完全不同的链路:

  1. 系统启动链路
    这条链路关心的是:应用能否完成初始化、能否发现工具、能否建立基础连接。

  2. 用户请求链路
    这条链路关心的是:当前用户有没有资格调用某个工具,调用后能看到哪些数据。

这两条链路如果共用一套令牌策略,十有八九后面会出事。

一个很值得你留意的官方细节

Spring AI 的 MCP 安全文档目前明确写了:

  • 支持基于 OAuth2 的安全客户端
  • 提供启动期认证和请求期认证的区分方案
  • 当前说明中还特别提到这套支持主要面向 McpSyncClient 场景[^2]

这意味着如果你现在的系统本身就是异步、反应式或多阶段代理编排架构,别看到 demo 通了就默认“这套认证流在线上一定自然成立”。

怎么改

  • 启动阶段和用户请求阶段分开设计认证策略
  • 明确哪些调用可以用服务账号,哪些调用必须带当前用户上下文
  • 把“用户身份传播失败”当成需要专门压测和回归测试的场景

第三个坑:把内部能力原封不动暴露成 MCP Tool

这个坑在本地特别不容易被意识到,因为一开始大家通常只想快点把功能跑出来。

Spring AI 的工具调用机制本质上是这样的:

  • 你把工具定义暴露给模型
  • 模型根据工具名称、描述和输入 schema 判断要不要调
  • 真正执行工具的是你的应用,不是模型[^3]

这句话听起来很普通,但里面有一个非常关键的结论:

工具定义本身,就是你的“能力暴露面”。

从机制上看,只要某个工具进入了模型可见范围,它的名字、描述、参数结构就已经开始影响模型决策。
所以如果你把内部服务接口原封不动包装成 MCP Tool,常见问题会马上跟上来:

  • 工具粒度过粗,一个 tool 就能做太多事
  • 参数过宽,用户可以间接触发高风险操作
  • 描述写得太“智能”,模型更容易在边界不清时误选工具
  • 把调试接口、管理接口、批量接口和普通查询接口混在一起注册

一个很实用的判断标准

不要问“这个接口能不能封成 tool”。
要问“如果模型把这个 tool 用错一次,代价能不能接受”。

如果答案是否定的,那这个工具就不该以当前形态暴露出去。

怎么改

  • 把高风险写操作和低风险读操作拆成不同工具集
  • 给工具做最小能力封装,不要把内部管理 API 直接抬到 MCP 层
  • 严格收窄 schema,只暴露当前场景真的需要的参数
  • 工具描述写清边界,不要为了“提高命中率”把描述写成万能入口

一个很常见但很危险的反例是这样:

@Tool(description = "Manage all user operations")
public String manageUser(String action, String userId, String tenantId, String payload) {
    // create / update / delete / reset / grant role ...
}

这种工具本地演示也许很方便,但一上线就几乎等于把一堆管理能力打包交给了模型路由层。

更稳妥的思路通常是:

@Tool(description = "Get current user's order summary")
public OrderSummary getCurrentUserOrderSummary() {
    // derive user and tenant from server-side context
}

这里最重要的不是代码长短,而是边界是不是被收紧了。


第四个坑:把多租户隔离写进 Prompt,却没写进服务端

这是很多 AI 项目最典型的“看起来有约束,实际上没边界”问题。

你当然可以在 system prompt 里写:

  • 只能访问当前租户数据
  • 不要查询其他部门信息
  • 只允许读取当前用户有权限的订单

但别忘了,Prompt 是行为引导,不是安全边界。

Spring AI 的工具调用文档里强调得很清楚:
模型不会直接访问你的 API,真正的工具执行发生在应用侧。[^3]
这句话反过来读,就是:

真正的权限控制,也必须发生在应用侧。

所以在多租户场景里,真正靠谱的做法从来不是让模型“理解租户隔离”,而是让服务端“强制执行租户隔离”。

线上最常见的错误写法

  • 让模型把 tenantId 当成普通参数传进来
  • 工具实现直接信任这个 tenantId
  • 最后依赖 Prompt 约束模型“不要传错”

这在单用户测试时往往没感觉,但到了线上,多轮对话、工具重试、上下文拼接、提示词污染一起叠加后,风险会变得非常实际。

更稳的做法

场景 不推荐 推荐
租户识别 模型传 tenantId 服务端从认证上下文解析
用户身份 工具参数里显式传 userId 服务端从安全上下文提取
数据过滤 先查全量再让模型挑 查询阶段就带租户和权限条件
越权兜底 靠 Prompt 提醒 靠服务端策略拒绝

一句话总结就是:

能从服务端上下文拿到的信息,就不要交给模型自由填写。


第五个坑:日志和追踪不是开得越多越安全

很多后端团队在出问题时第一反应是:

那就把日志全开。

但 AI 场景里,日志开得太猛,往往会先把敏感信息送进日志系统。

Spring AI 的可观测性文档对这一点其实已经很谨慎了。官方默认是:

  • 不导出 prompt 内容
  • 不导出 completion 内容
  • 不记录 tool 的输入参数
  • 不记录 tool 的执行结果[^4]

这些默认值之所以保守,不是因为官方不重视排障,而是因为这些内容极有可能包含敏感数据。

看一下官方给出的相关配置项,你就能明白这个取舍非常明确:

spring.ai.chat.observations.log-prompt=false
spring.ai.chat.observations.log-completion=false
spring.ai.chat.client.observations.include-input=false
spring.ai.tools.observations.include-content=false

这就意味着,MCP 上线前你真正要做的不是“全关”或者“全开”,而是分层设计:

层级 应该记录什么
业务审计 谁在什么时间调用了哪个 tool,是否成功
运维排障 调用耗时、异常类型、重试次数、下游状态码
安全审计 是否越权、是否命中敏感工具、是否跨租户失败
内容日志 仅在极少数受控场景按需采集,默认不放开

怎么改

  • 默认延续 Spring AI 的保守设置,不要一上来就导出全部内容
  • 审计和排障拆开设计,不要让“看得见内容”成为唯一排障手段
  • 对高风险 tool 额外记录业务主键、租户信息、调用人,而不是全量 prompt

这件事做对了,你的系统才会既能排障,也不至于先把自己日志系统变成敏感数据池。


第六个坑:自动发现、自动注册、自动初始化一开,能力边界也跟着失控

自动化配置是 Spring 生态的优势,但在 MCP 场景里,它也会放大一个老问题:

你以为自己只是“省步骤”,实际上你可能把暴露面也一起放大了。

Spring AI 官方文档提到,MCP 客户端在启动阶段可能需要做工具发现;安全文档还专门给了用空的 ToolCallbackProvider 来阻止自动工具注册的做法。[^2]
这已经说明一个很现实的问题:

  • 自动发现能提高开发效率
  • 但如果环境、认证、工具清单没有被严控,自动发现也会把错误能力带进来

另一个必须注意的点是版本和模块成熟度。
Spring AI 当前 MCP 安全支持文档里明确写了,这部分能力带有明显的演进属性,并强调了其面向 MCP Java SDK 社区实现的背景。[^2]

这不代表不能用,而是代表你上线前别做这两件事:

  • 看到官方有 starter 就默认所有安全细节都已经替你兜底
  • 看到本地 autoconfigure 生效就默认线上环境的认证、初始化、工具集合也完全正确

一个很有代表性的配置片段

官方给出的一个思路,是在需要时主动阻止默认自动注册:

@Bean
ToolCallbackProvider toolCallbackProvider() {
    return ToolCallbackProvider.from(List.of());
}

这段代码的价值不在于“把功能关掉”,而在于提醒你:

工具是否进入模型可见范围,应该是显式决策,而不是顺手发生。

怎么改

  • 不要让所有环境都共享同一套自动发现策略
  • 工具清单做环境级控制,开发、测试、生产分开维护
  • 把启动初始化日志纳入验收,确认到底注册了哪些 tool
  • 每次发版都核对“本次到底新增暴露了哪些工具能力”

如果你真的准备上线,至少把这张检查单过一遍

Spring AI 接 MCP 上线前检查单
检查项 你要确认什么
认证链路 启动期和请求期是否拆开设计
令牌策略 用户令牌和服务令牌是否分开使用
工具边界 是否存在一个 tool 打包多个高风险能力
参数设计 是否仍在信任模型传入的租户或用户标识
服务端隔离 是否在服务端强制执行租户和权限条件
审计日志 是否能查到谁调用了什么、何时调用、结果如何
内容日志 是否避免把 prompt、tool 参数、结果默认全量落盘
自动注册 是否清楚当前环境到底暴露了哪些 tools
初始化流程 启动阶段是否依赖了不该依赖的用户态认证
回退机制 401、403、下游失败时是否有明确处理路径

这篇文章最想让你带走的一个结论

Spring AI 接 MCP,真正难的从来都不是“把协议接上”。
真正难的是你要在一个会自动路由工具、会跨上下文、会进入多租户业务的系统里,把边界重新定义一遍。

所以如果你问我,Spring AI 接 MCP 上线前最该先做什么,我的答案不是:

  • 先调通更多工具
  • 先做更复杂的 Prompt
  • 先接更多模型

而是:

先把认证边界、工具边界、租户边界和审计边界定清楚。

因为系统能跑,只说明它开始工作了。
系统敢上线,才说明它开始成熟了。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐