自研 MCP Server 上线前必须过的 12 项检查
把内部系统暴露成 MCP Server 给 Agent 用,风险远大于写个 API。这份清单覆盖工具描述、参数校验、幂等、权限边界、超时与日志,逐条可勾选,附反例代码。
MCP Server 写起来很轻松:注册几个 tool,转发到内部 API,半天就能通。但它上线后面向的是会自己决定调用什么、传什么参数、调几次的 Agent。传统 API 假设调用方理性且参数合理,Agent 不满足这个假设。下面 12 项检查,都是我在真实踩坑之后补进清单的。
一、工具层
1. 工具描述写清”什么时候不要用” 只写”查询订单状态”不够。要写”仅用于已知订单号的情况;若只有手机号,请先用 search_customer”。缺少边界描述,是工具选错的第一大原因。
2. 工具数量控制在单域 7 个以内 一次暴露 30 个工具,模型的选错率会明显上升,token 也白白浪费。按业务域拆成多个 Server,或在同一 Server 内做分组加载。
3. 名称与参数用可猜的语义命名
get_order_status(order_id) 好过 query(data)。参数名同理:start_date / end_date,不要 s / e。Agent 靠名字推断含义,命名就是你的 prompt。
二、入参与校验
4. 参数做严格 schema,禁掉额外字段
用 JSON Schema 把 required、enum、pattern 全部锁死。字符串日期写 pattern: "^\\d{4}-\\d{2}-\\d{2}$",别指望模型每次都给对。
5. 服务端二次校验,绝不信任调用方 下面是典型的错误写法:
@server.tool()
def refund(order_id: str, amount: float):
return api.post(f"/orders/{order_id}/refund", json={"amount": amount})
order_id 被拼进 URL,amount 不做上限检查——模型传 999999 或 ../admin 就是一次事故。必须做:格式白名单、金额上限、订单归属校验(这个订单是不是当前租户的)。
6. 危险参数不给默认值
force=True、dry_run=False 这类参数不要设默认值,强制模型显式填写,逼它在调用前”想一下”。
三、执行层
7. 写操作必须幂等
Agent 会重试。带 idempotency_key(由业务主键派生,不是你随机生成)是底线。没有幂等键的退款接口,等于给 Agent 发了一把可以重复扣款的枪。
8. 超时分级,且小于上游 Agent 超时 内部调用 3 秒、外部 8 秒,并且保证 Server 一定先于 Agent 返回。否则 Agent 已放弃,你的请求还在跑,重复执行就来了。
9. 返回体做裁剪
返回整个订单对象(200 个字段)会吃掉上下文预算,还会干扰模型判断。只回必要字段,长列表加 limit 和 has_more,并给出 next_cursor。返回里同时给”人话摘要 + 结构化数据”,摘要帮模型理解,结构化数据帮它下一步取参。
四、权限与可观测
10. 权限跟着用户走,不是跟着服务走 最危险的设计是用一个高权限服务账号代所有用户调用。必须把调用方身份透传(token 或用户上下文),在 Server 侧做租户与角色校验。Agent 说”帮张三查订单”,实际必须验”当前会话是张三”。
11. 每次调用留完整审计 记录:who(真实用户)、what(工具+参数摘要)、when、result_code、耗时、trace_id。写操作的审计要保留原始参数,事后追查时”模型传了什么”是唯一证据。
12. 上线前做一次”恶意 Agent 演练” 拿一个故意捣乱的 Agent 跑一遍:乱传日期、传空、传超长字符串、连续调 20 次退款、问不属于自己的订单。任何一项没被正确拒绝,就不能上线。
一个容易被忽略的收尾
工具的错误信息也是给模型看的输入。返回 500 Internal Error 模型无从修正;返回 错误:订单号格式应为 ORD-开头 + 8 位数字,你传入的是 "1234",模型下一次就能自己改对。错误信息是工具的一部分,值得当成产品文案来写。
这 12 项没有一项是”高级技巧”,全是基本功。但正是这些基本功,决定了你的 MCP Server 是能用,还是敢用。