<< 返回博客
·5 分钟阅读

如何设计 110 个 MCP 工具,让 AI 安全操作生产仓库数据

闪仓 WMS 的 MCP Server 向 AI Agent 暴露了 110 个工具,覆盖商品、单据、仓库等 12 个业务域。本文详解工具的领域划分、读写分离、WARNING 前缀机制、租户锁定与启动时校验等安全设计。

如何设计 110 个 MCP 工具,让 AI 安全操作生产仓库数据

背景:AI Agent 需要操作真实仓库数据

闪仓 WMS 的 CLI 工具 fwh 内置了一个 MCP (Model Context Protocol) Server,支持 Claude Code、Cursor、Windsurf 等 AI 编程助手直接调用仓库管理系统的 API。这意味着 AI Agent 可以查询库存、审批单据、创建商品——操作的是真实的生产数据。

这带来了一个核心问题:如何在赋予 AI 足够能力的同时,确保它不会越权操作、误删数据、或访问其他租户的信息?

本文分享我们设计 110 个 MCP 工具时采用的安全架构。

按业务域组织:12 个注册函数,每个文件一个领域

110 个工具被划分到 12 个注册函数中,每个函数对应一个独立的 Go 源文件:

领域文件工具数示例
商品管理tools_goods.go11goods_list, goods_create, goods_delete
单据操作tools_bill.go27bill_list, bill_audit, purchase_input_create
仓库管理tools_warehouse.go4warehouse_list, warehouse_create
库存预警tools_alert.go6alert_overview, alert_set_config
待入库审批tools_stock.go4stock_pending, stock_approve_all
盘点任务tools_check.go9check_task_list, check_task_finish
人员与权限tools_people.go20staff_list, role_create, partner_add
BI 仪表盘tools_dashboard.go9bi_total_value, bi_line_chart, pos_sell
系统配置tools_config.go17config_read, print_template_create, account_update
其他出入库tools_other_bill.go2other_input_bill_create
身份查询tools_whoami.go1whoami

server.goServe 函数中,这 12 个注册函数被依次调用,每个都接收 enableWrites 参数来控制是否注册写工具。

读写分离:46 个只读工具 + 64 个写工具

每个注册函数内部都遵循相同模式:先注册只读工具,遇到 if !enableWrites { return } 就停下,写工具放在后面

func registerGoodsTools(s *mcpsdk.Server, svc *service.Service, enableWrites bool) {
    // 只读工具始终注册
    mcpsdk.AddTool(s, &mcpsdk.Tool{
        Name: "goods_list",
        Description: "List goods...",
    }, handler)

    if !enableWrites {
        return  // 到此为止,不注册写工具
    }

    // 写工具仅在 enableWrites=true 时注册
    mcpsdk.AddTool(s, &mcpsdk.Tool{
        Name: "goods_create",
        Description: "WARNING: this modifies production data...",
    }, handler)
}

CLI 默认 EnableWrites: false,用户必须显式传入 --enable-writes 标志才能开启写权限。这意味着 AI Agent 默认只能读取数据。

WARNING 前缀:对 AI 的语义级防护

所有 64 个写工具的 Description 都以 WARNING: 开头。这不是装饰——它是利用 LLM 的指令遵从特性来降低误操作风险。当 AI 看到 "WARNING: this permanently deletes a goods record" 时,它更倾向于在执行前向用户确认。

对于不可逆操作(删除、审批、全量操作),我们还额外要求一个 confirm 字段。Agent 必须将其设为精确的字符串 "YES" 才能执行:

func RequireConfirm(confirm string) error {
    if confirm != "YES" {
        return ErrNotConfirmed
    }
    return nil
}

这个双重保护(WARNING 描述 + confirm 字段)确保了破坏性操作需要 Agent 做出明确的、可审计的决策。

租户锁定:不可导出的 Go 结构体字段

多租户隔离是 WMS 的安全基石。后端通过 binding_user_id 字段隔离每个租户的数据。在 MCP Server 中,我们用 Go 语言的可见性规则来硬编码这一约束:

type Session struct {
    userID int  // 小写字母开头 = 不可导出,包外无法访问

    UserName string    `json:"user_name"`
    BaseURL  string    `json:"base_url"`
    LoggedAt time.Time `json:"logged_at"`
}

userID 字段是小写的,意味着:

  • 没有 setter 方法——一旦通过 NewSession() 创建,就无法修改
  • 包外不可见——MCP 工具代码、CLI 命令代码都无法直接读写它
  • 只有 UserID() 方法可以读取,而这个方法只被 internal/api 包调用

这构成了一个编译器级别的保证:binding_user_id 的值只能来自登录时后端返回的结果,不可能被 CLI 参数、环境变量或 MCP 工具参数覆盖。

validateInputShape:启动时拦截租户字段泄露

即使有上述机制,如果某个开发者不小心在工具输入结构体中添加了 binding_user_iduser_id 字段,AI Agent 就可能绕过租户隔离。为此,我们引入了一个启动时校验器:

var forbiddenInputFields = map[string]struct{}{
    "binding_user_id": {},
    "user_id":         {},
    "tenant_id":       {},
    // ... 及其变体
}

func validateInputShape[T any]() {
    // 反射遍历结构体的每个字段
    // 如果 json tag 命中 forbiddenInputFields → panic
}

每个工具在注册时都会调用 validateInputShape。如果任何输入类型暴露了被禁止的字段,程序在启动时直接 panic,而不是在运行时悄悄出错。这将安全漏洞从"运行时才发现"提前到了"启动时就崩溃"。

每次调用时的会话校验

除了启动时的静态校验,每个工具处理函数的第一行都调用 ensureSessionAlive

func ensureSessionAlive(svc *service.Service) error {
    fresh, err := config.LoadSession()
    // 如果会话文件被删除(用户运行了 fwh logout)→ 报错
    // 如果会话文件的 user_id 与内存中的不同 → 报错
}

这能捕获一个边缘场景:MCP Server 正在运行,用户在另一个终端中注销或切换了账号。

响应清洗:AI 不该看到密码和银行账号

后端返回的原始数据库行可能包含 loginPasswordbankAccount 等敏感字段。SanitizeResponse 函数在每个工具返回结果前自动清洗响应,剥离密码、遮蔽金融信息、移除 MyBatis Plus 的内部分页字段。

总结

闪仓 WMS 的 110 个 MCP 工具体现了"安全默认、逐步放权"的设计原则:

  1. 领域划分——12 个文件,职责清晰,便于审查
  2. 默认只读——写工具需显式启用
  3. 语义警告——WARNING 前缀 + confirm 字段双重保护
  4. 编译器级隔离——不可导出字段 + 无 setter = 租户锁无法绕过
  5. 启动时校验——validateInputShape 在程序启动时拦截潜在的租户字段泄露
  6. 运行时校验——每次调用都重新验证会话有效性
  7. 响应清洗——敏感数据永远不会到达 AI Agent

如果你也在构建面向 AI Agent 的工具层,欢迎参考这套方案。闪仓 WMS 的 MCP Server 源码位于 flash_warehouse_cli/internal/mcp/ 目录下。