MCP 工具设计最容易出问题的地方,不是协议本身,而是把“给模型一个函数”误当成“模型能稳定完成任务”。工具名、参数形状、返回内容、错误信息、权限边界,都会直接影响 Agent 是否能做出正确决策。好的 MCP tool 设计,本质上是一次上下文工程:把模型需要的判断线索放在合适的位置,同时不要把业务系统的复杂度原封不动塞给它。
工具不是 API 镜像,而是任务接口
很多团队会把已有 REST API 或内部 SDK 原样包装成 MCP tool:getUserById、listOrders、updateRecord。这对人类工程师清楚,对模型却经常不够。
模型调用工具时要回答几个问题:
- 这个工具适合解决当前任务吗?
- 必填参数从哪里来?
- 如果结果为空,下一步应该怎么办?
- 返回值里哪些字段值得继续推理?
因此,工具应该围绕任务命名,而不只是围绕数据表命名。比如客服场景里,find_customer_recent_orders 往往比 list_orders 更有用,因为前者已经编码了“客服排障”的意图。
可以这样设计工具描述:
{
"name": "find_customer_recent_orders",
"description": "Find recent orders for a customer when investigating delivery, refund, or payment issues. Use this before checking a single order if the user did not provide an order ID.",
"inputSchema": {
"type": "object",
"properties": {
"customer_email": {
"type": "string",
"description": "Customer email address from the conversation or CRM record."
},
"days": {
"type": "integer",
"description": "Lookback window in days. Use 30 unless the user asks for a different period.",
"default": 30
}
},
"required": ["customer_email"]
}
}
这里的重点不是 JSON 格式本身,而是描述里包含了调用时机、默认策略和参数来源。模型少猜一次,系统就少一次不稳定。
参数越贴近决策,调用越稳
一个常见反模式是把内部系统的全部参数暴露出来:分页游标、排序字段、状态枚举、内部租户 ID、调试开关。这样做看似灵活,实际会把模型拖进实现细节。
更稳的做法是把参数分成三类:
- 任务必需参数:没有它就无法执行,比如
customer_email、order_id。 - 可安全默认的参数:比如最近 30 天、最多返回 5 条。
- 不应暴露给模型的参数:比如内部权限标记、数据库索引选择、低层分页策略。
如果确实需要复杂查询,可以用受控枚举,而不是任意字符串:
{
"name": "search_support_articles",
"description": "Search approved support articles for troubleshooting steps. Use this before giving procedural instructions to customers.",
"inputSchema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "A short search phrase, not a full customer message."
},
"issue_type": {
"type": "string",
"enum": ["delivery", "refund", "payment", "account", "technical"],
"description": "Classify the issue so results can be filtered."
}
},
"required": ["query", "issue_type"]
}
}
这个设计牺牲了一些通用性,但换来更可预测的调用。MCP tool 的目标不是替模型打开所有门,而是给它几扇标牌清楚的门。
返回值要服务下一步推理
工具返回值也会出问题。很多实现直接返回数据库行或第三方 API 原始响应,字段多、命名内部化、失败信息含糊。模型会被迫在噪声里找信号。
更好的返回值应该做到三点:
- 只返回完成任务需要的字段。
- 明确区分“没有结果”和“调用失败”。
- 给出可执行的下一步线索。
例如订单查询工具可以返回这样的结构:
{
"status": "found",
"orders": [
{
"order_id": "ord_123",
"created_at": "2026-02-18",
"payment_status": "paid",
"fulfillment_status": "delayed",
"customer_visible_summary": "Package is delayed and has not received a carrier scan in 3 days."
}
],
"next_action_hint": "If the customer asks for compensation, check refund eligibility before promising a refund."
}
这不是让工具替模型完成全部思考,而是把系统事实整理成模型能继续工作的材料。
可以这样实践:写一个最小 MCP 风格工具清单检查器
下面这个 Python 脚本不依赖真实 MCP SDK。它用于在代码评审或 CI 中检查工具定义是否包含关键上下文:描述、参数说明、必填字段和返回提示。你可以把 TOOLS 替换成项目里的 MCP tool 定义导出结果。
运行方式:保存为 check_mcp_tools.py,执行 python check_mcp_tools.py。
from __future__ import annotations
TOOLS = [
{
"name": "find_customer_recent_orders",
"description": "Find recent orders for a customer when investigating delivery, refund, or payment issues. Use this before checking a single order if the user did not provide an order ID.",
"inputSchema": {
"type": "object",
"properties": {
"customer_email": {
"type": "string",
"description": "Customer email address from the conversation or CRM record.",
},
"days": {
"type": "integer",
"description": "Lookback window in days. Use 30 unless the user asks for a different period.",
"default": 30,
},
},
"required": ["customer_email"],
},
"returns": {
"description": "Returns matching orders plus a next_action_hint for follow-up investigation."
},
},
{
"name": "list_orders",
"description": "List orders.",
"inputSchema": {
"type": "object",
"properties": {
"q": {"type": "string"}
},
},
},
]
def audit_tool(tool: dict) -> list[str]:
issues: list[str] = []
name = tool.get("name", "<unnamed>")
description = tool.get("description", "")
if len(description.split()) < 12:
issues.append(f"{name}: description is too short to guide tool choice")
schema = tool.get("inputSchema", {})
properties = schema.get("properties", {})
required = set(schema.get("required", []))
if not properties:
issues.append(f"{name}: no input properties defined")
for prop_name, prop in properties.items():
if "description" not in prop:
issues.append(f"{name}.{prop_name}: missing parameter description")
for prop_name in properties:
prop = properties[prop_name]
has_default = "default" in prop
if prop_name not in required and not has_default:
issues.append(
f"{name}.{prop_name}: optional parameter has no default; model may guess"
)
returns = tool.get("returns", {})
if "description" not in returns:
issues.append(f"{name}: missing return description or next-step guidance")
return issues
def main() -> None:
all_issues = []
for tool in TOOLS:
all_issues.extend(audit_tool(tool))
if not all_issues:
print("All MCP tool definitions passed the context audit.")
return
print("MCP tool design issues:")
for issue in all_issues:
print(f"- {issue}")
if __name__ == "__main__":
main()
示例输出会指出 list_orders 的几个问题:描述太短、参数缺少说明、返回值没有上下文。这类检查不能替代真实评测,但能把最常见的设计失误提前拦下来。
取舍:少而准,还是多而全
MCP tool 设计没有唯一答案。工具太少,模型需要在一个大工具里拼复杂参数;工具太多,模型又会在选择工具时迷路。比较实用的原则是:
- 高频任务做成明确工具,低频操作保留给人工或后台流程。
- 有副作用的工具要更窄,比如退款、删除、发邮件,最好增加确认步骤和幂等键。
- 查询工具可以稍宽,写入工具必须保守。
- 工具描述要写“什么时候用”和“什么时候不用”。
- 返回值要告诉模型事实,不要返回未经整理的内部对象。
落地时不要一次性把整个系统暴露给 Agent。先挑一个闭环任务,例如“查询订单并判断下一步客服动作”,围绕它设计 3 到 5 个工具,再用真实对话回放评估调用是否稳定。MCP 的价值不在于工具数量,而在于模型拿到的上下文是否足够清楚、边界是否足够硬。