自己写一个 MCP Server:工具设计比协议难十倍
MCP 的 SDK 已封装好握手和传输,真正的难点是工具粒度、参数设计、错误信息怎么写。本文按开发顺序走一遍,附三个上线后才暴露的设计错误。
MCP 已是 Agent 接外部能力的默认协议。真正写起来,难点从来不是协议本身——SDK 把 transport、握手、能力协商都封装好了——而是工具设计。
一、先想清楚给谁用
- 通用 Agent(Claude Desktop、Cursor):工具名和描述是模型唯一的信息来源,必须自解释,宁可啰嗦。
- 内部业务 Agent:可以依赖系统提示词补上下文,工具能更精简。
这决定工具粒度和描述详细程度。
二、粒度:别把 REST API 一对一翻译
新手最常犯的错是把现有 20 个接口直接变成 20 个工具。模型每次在 20 个里挑,选错率极高。
实际经验是三层:
- 资源级:
get_user、list_orders,目的单一,可以保留; - 场景级:
find_order_by_customer_email,把常见两步合成一步,减少往返; - 禁止:
execute_api(path, method, body)这种万能工具,等于把 API 文档整个丢给模型。
判断标准很简单:如果参数里有一个是「选路」用的(method、endpoint),说明粒度错了。
三、参数设计的四条规则
- 能用枚举就别用自由字符串,
status="all"比status="any"稳定; - 每个参数都要有 description,模型看不到你的内部文档;
- 给默认值,模型经常漏参数;
- 返回结构化数据,不要返回自然语言让模型自己整理。
四、错误信息是给模型看的
这是上线后最常见的返工点。原始异常栈对模型毫无意义,它只会回用户「出错了」。
错误要写成可行动的句子:
BAD: ValidationError: 422
GOOD: 参数 status 必须是 open/closed/all,收到 "pending",
请确认用户要的是 open 还是 closed。
同时区分两类:可重试(参数错、找不到)告诉它怎么改,它会自己重试;不可重试(权限不足、服务宕机)明确说不要重试,直接告知用户。
五、三个上线后暴露的坑
工具数量膨胀。 加到第 30 个工具后模型开始乱选。解法是按领域拆成多个 MCP Server 按需加载,别全塞一个。
写操作缺幂等。 create_invoice 被重复调用,客户收到两张发票。所有写操作都要支持幂等键或去重窗口。
返回体过大。 一次 list_orders 返回 500 条 JSON 直接撑爆上下文。服务端要截断并明确告知「仅返回前 50 条」。
六、测试怎么测
别只测「工具能跑通」。准备 20~30 个典型用户请求,看模型选中的工具和参数是否合理。这个用例集比单元测试更有价值,因为它测的是接口对模型是否友好,而这才决定 MCP 的质量。
写 MCP Server 的本质不是写 API,是写一份给模型看的、可执行的 API 文档。