大数据智能运维实战(十一):运维 MCP 的工程化——权限、限流、审计与错误处理

系列:Spring AI + MCP------大数据智能运维实战

本篇目标:用通用教学方案给查询工具加上访问约束、范围限制、超时、审计和明确的错误语义。

阅读基础:第七至十篇;沿用 lab-07-12,Java 17、Spring Boot 4.1.0、Spring AI 2.0.1。

案例说明:全部对象和数据为虚构;本机令牌与固定角色用于教学,不是生产身份系统的替代品。

1. 只读工具,也不能想查多少就查多少

"我们又不重启机器、不改任务,只查数据,还需要做这么多限制吗?"小周问。

小林把两个请求摆出来:一个查某次运行 35 分钟内的日志;另一个想查所有节点一个月的日志。

它们都叫只读查询,对系统的影响却完全不同。

还有权限问题。能查看任务状态的人,不一定能读取日志;能访问青禾教学范围的人,也不因此获得其他范围的数据。

前面几篇已经把工具的输入和返回讲清楚。本篇把每一次查询真正经过的服务端边界展开:先确认谁在查、允许查什么,再决定是否执行、执行多久,以及失败以后怎么解释。

2. 身份来自已经验证的请求,不来自模型填写的参数

一个容易踩坑的设计是:

json 复制代码
{
  "userId": "admin",
  "role": "operator",
  "runId": "run-current-0917"
}

如果服务端相信模型提供的 role,权限判断就变成了"调用者说自己是谁,就算谁"。

教学工程换一种做法:启动时从环境读取两组不同的随机令牌,入口验证 Bearer 值,再映射到两个固定身份。映射结果由服务端写入请求上下文,工具参数中没有用户或角色字段。

教学身份 运行、关联、指标 日志
ops-reader 可读 可读
viewer 可读 不可读

两个身份都只访问固定的 qinghe-demo 样本。未知对象统一返回"对象不存在或不可访问",不会为了验证一个猜测的标识而扫描其他环境。

在无状态 HTTP 教学传输中,工具通过 McpTransportContext 获取入口已经建立的身份。Spring AI 对注解工具与上下文参数提供支持,具体支持范围与传输类型有关。Spring AI MCP 注解服务端文档

本实验默认只监听 127.0.0.1,只演示本机 CLI 调用,带浏览器 Origin 的请求会被拒绝。生产环境的令牌签发、有效期、撤销、受众校验、TLS 和网关集成需要另行实现,不能把这两组环境令牌直接部署为公共服务的完整认证方案。

3. 每个工具都走同一道边界

图 1:模型提供业务查询参数;身份、权限和执行预算由服务端掌握。

五个工具都通过 QueryBoundary.execute 执行。顺序是:

text 复制代码
入口认证
  → 工具权限与调用额度
  → 对象、参数和范围校验
  → 在受限工作池中执行查询
  → 返回结构化结果
  → 记录工具调用审计

这里有两层权限需要区分。

一层是"这个身份能不能调用日志工具"。viewer 即使拿到别人返回的日志游标,也先被日志权限检查拒绝。

另一层是"这个对象是否属于允许访问的范围"。本实验只有一个固定范围,用内存白名单演示;真实适配器必须在查询数据源时带上服务端确定的范围条件,不能先把全平台数据查回来,再交给模型过滤。

readOnlyHint=true 只是向客户端描述工具属性,并不会自动完成鉴权或阻止错误查询。MCP 工具规范也要求服务端落实输入验证、访问控制等约束。MCP 工具规范

这些规则不能只写在系统提示词里。模型没有遵守提示时,服务端仍然要拒绝越界请求。

4. 限制要落到可执行的数字上

教学工程采用一组容易复现的参数:

约束 本实验值 解决什么问题
单次日志或指标窗口 最多 60 分钟 避免随意扩大查询范围
每页日志数量 1---100 条 限制单次返回体量
单条日志消息 最多 160 个字符 避免长消息占满上下文,并明确截断
每身份工具额度 每自然分钟 60 次 限制持续调用
查询工作池 4 个工作线程,8 个排队位置 限制同时占用资源的查询数
单次工具等待上限 500 毫秒,包含排队等待 及时返回明确超时
日志游标 120 秒,最多保留 256 项 限制续查状态占用

这些数字是教学配置,不是生产容量建议。真实值要根据数据源响应、用户需求和容量评估确定。

每自然分钟 60 次采用固定窗口计数,分钟边界附近仍可能出现短时突发;生产需要更平滑的限制时,可以使用合适的令牌桶或滑动窗口实现。多实例部署也不能各自在内存里计数,却声称有一个全局额度。

工作池和超时是互补的。超时让调用方及时得到响应,但后台查询可能不理会中断。本实验的慢源使用可中断等待,所以取消能够结束它;实际数据库、HTTP 客户端或日志检索服务还要配置自己的超时和取消机制。

因此不能说"Future 取消了,底层所有资源就一定释放了"。受限线程池只是把影响限制在明确范围,不能替代下游取消能力。

5. 查不到、没权限、查询失败,要让助手看得出区别

图 2:数据缺失是一种查询结果;执行失败则意味着没有得到可据此判断的完整结果。

本实验在工具成功执行时返回统一外层:

json 复制代码
{
  "status": "OK",
  "requestId": "本次查询标识",
  "observedAt": "2026-09-17T09:40+08:00",
  "snapshotId": "teaching-20260917-v1",
  "data": {"availability": "NO_DATA", "points": []}
}

这是字段示意。真实 requestId 每次生成,文章不要求它固定不变。

这里的 NO_DATA 表示查询成功,但这个教学对象没有相应指标样本。MCP 层 isError=false。它不表示指标为 0,更不表示系统健康。

如果查询超时,返回形态改为:

json 复制代码
{
  "status": "ERROR",
  "requestId": "本次查询标识",
  "observedAt": "2026-09-17T09:40+08:00",
  "snapshotId": "teaching-20260917-v1",
  "error": {
    "code": "TIMEOUT",
    "message": "数据源查询超时,未获得完整结果",
    "retryable": true
  }
}

此时 MCP 层 isError=true。HTTP 请求完成,也不意味着工具执行成功,客户端必须检查 CallToolResult。

工程通过 CallToolResult.builder() 同时写入结构化结果和相同内容的文本结果,便于不同客户端读取;没有另外宣称生成了业务输出 Schema。输入参数定义与服务端校验仍然保留,输出字段由测试核对。

另外还有两类失败不要混在一起:未通过入口认证的请求直接得到 HTTP 401;无法正常解析或调用不存在的方法等协议问题,由协议层处理。它们不是一条"查询成功、结果为空"的业务返回。

6. retryable=true,也不等于让助手一直重试

可以把错误处理约定写成一张表:

状态或错误 助手的合理动作
INVALID_ARGUMENT 检查参数,不原样反复调用
FORBIDDEN、NOT_FOUND_OR_FORBIDDEN 说明访问边界,不换标识试探
CURSOR_INVALID 在原范围内重新查询,并处理记录去重
RATE_LIMITED、BUSY 在总预算内退避;没有预算就停止
TIMEOUT 明确未取得结果,必要时在预算内重试或缩小范围
NO_DATA 说明样本缺失,不填 0,不报健康
MISSING、AMBIGUOUS 保留关联问题,补证据后再继续

retryable 只表示这类错误可能通过后续调用恢复,不保证下一次成功,也不授予无限重试权限。第十篇的演示脚本选择遇错停止,便于读者看清发生了什么。

把同一个错误包装成一段自然语言再抛回模型,远不如保留稳定错误码、是否可重试和查询标识。模型可以据此决定怎么解释,程序也能据此实施有限重试。

未知异常只返回通用错误与 requestId,不会把堆栈、连接信息或原始响应直接塞进助手上下文。

7. 审计记录"谁查了什么能力",不再复制一遍敏感内容

工具调用审计保留以下字段:

text 复制代码
requestId、真实查询时间、调用主体、授权范围、工具名、结果码、耗时

它不记录 Authorization 头、令牌、完整日志内容和原始参数。教学实现把审计写到应用日志,并保留最近 100 条内存记录供测试检查。

注意这里有两种时钟:业务 observedAt 固定为虚构场景的 2026-09-17 09:40,审计 at 是实际发起查询的时间。它们回答的问题不同,不应该伪造成同一个时刻。

当前审计覆盖进入公共工具边界的调用,HTTP 入口直接拒绝的请求不进入这份工具审计。实际部署还需要入口或网关访问审计、受控存储、留存周期和查询权限;内存里的 100 条记录不能当作长期审计系统。

想追到某次失败,先通过 requestId 找到对应调用,再由有权限的人查看服务端诊断信息。没必要为了排查方便,把敏感内容复制到每一层日志里。

8. 用几个反例证明边界真的生效

配套验证不只检查成功查询,还会实际运行这些情况:

  • 不带有效令牌:入口返回 HTTP 401。
  • viewer 调用日志工具:返回 FORBIDDEN,日志查询不执行。
  • 把日志条数改成 101,或查询超过 60 分钟:返回 INVALID_ARGUMENT。
  • 用其他范围的标识查询:返回 NOT_FOUND_OR_FORBIDDEN。
  • 模拟数据源等待 2 秒:工具在 500 毫秒预算附近返回 TIMEOUT,不能返回空数组。
  • 修改续查参数:返回 CURSOR_INVALID。

单元测试还会使用缩小的额度验证限流,检查审计不含查询正文,以及未知异常不会泄漏后端错误细节。

在工程目录执行:

bash 复制代码
mvn test package
python3 scripts/verify_mcp.py

这些检查能证明教学实现的特定行为,不代表它已经完成生产安全评估。下一篇会把它们放进更完整的验收矩阵:除了工具能执行,还要检查助手选得对不对、参数是否越界、证据有没有被误读。

相关推荐
琥珀.1 小时前
mysql中json数组转列表查询
linux·运维·服务器
智能运维指南2 小时前
研制过程“两张皮“怎么破:研制一体化支撑平台的定位、能力边界与落地前提
java·前端·人工智能·嘉为蓝鲸
dawnsky.liu2 小时前
在 RHEL 中使用 goose 实现远程主机管理
linux·运维·ai
2601_967212722 小时前
智能电源轨道技术原理、认证核查与场景适配说明
大数据·经验分享·安全
yz_aiks3 小时前
Spring AI 2.1 ChatMemory 已过时用什么替代:迁移到 Session Memory 实战(JDBC 存储与会话压缩)
java·spring boot·大模型·ai agent·spring ai
金銀銅鐵3 小时前
[Java] 用 GUI 展示 EnumMap 增删键值对时的内部变化
java·后端·ai编程
学长毕设3 小时前
基于SpringBoot的上门洗车服务系统设计与实现
java·spring boot·后端
KKKlucifer3 小时前
运营商非结构化文档安全管控体系建设实践
大数据·网络·安全
jason成都3 小时前
RTKLIB-Java 多基线联合RTK:FIX率从0%到91.5%的三层根因修复
java·gnss·rtklib·rtklib_java