测量并压缩 MCP 工具定义的令牌开销
文章解释 MCP 工具名称、描述和 JSON Schema 如何进入模型上下文,并介绍测量工具定义令牌成本的脚本。内容聚焦工具数量、冗长描述和枚举等膨胀来源,以及更精简的工具设计。
AI 深度解读
MCP 工具还没执行,工具定义就可能先吃掉一截上下文。原文提醒开发者:多数客户端会把工具名称、描述和参数 Schema 随请求发送给模型,接入的服务器越多,这笔固定开销越大。
作者给出的办法很直接:先测量,再按用户任务合并工具、压缩参数定义、按需加载工具组,同时控制返回结果。对正在开发 MCP Server,或者给 AI 编程助手挂了多个 MCP 的程序员,这套排查顺序值得参考。不过,文章提供的是测量方法和设计经验,没有展示优化前后的实测数据。
工具没调用,为什么也会消耗 token
客户端连接 MCP Server 后,会通过 tools/list 获取工具定义。每个工具包含名称、描述和 inputSchema,还可能带有 outputSchema、title、annotations。客户端再把这些信息转换成模型 API 接受的格式,随请求一起发送。
模型需要先知道有哪些工具、分别能做什么,才能决定调用谁。因此,工具定义也是提示词,需要占用上下文。
原文认为,很多 MCP Server 的问题出在设计方式:直接把 REST API 的每个端点包装成一个工具。20 个 CRUD 端点就变成 20 个工具,过滤条件和说明文字重复出现。参数里再塞进几百项枚举、ORM 的嵌套结构,工具清单很快就膨胀了。
这笔开销还会叠加。GitHub、数据库、浏览器、内部服务各挂一个 MCP,客户端如果全部发送,就得承担四份清单的成本,即使当前任务只需要其中一份。
Prompt caching 可以降低部分供应商对重复前缀的计费,但定义仍然占用上下文;工具列表变化也可能影响缓存复用。原文将缓存失效说得比较绝对,实际仍要看供应商的缓存规则和客户端组织请求的方式。
另一个问题是工具选择。get_user、get_user_by_email、search_users、list_users 同时存在,模型需要判断细微差别。作者认为这会增加选错工具的机会,但没有提供准确率对照实验。
先测整份工具清单,再找最重的几个
原文的测量脚本使用官方 Python SDK,通过 stdio 启动服务器,创建 ClientSession,执行 initialize() 和 list_tools(),再把工具转换为 Anthropic 接受的 name、description、input_schema。
测量的关键是差值:对同一条极短消息,分别计算带工具和不带工具的输入 token,二者相减。下面是原文负责计数的函数:
def count(client, tools=None) -> int:
kwargs = {"tools": tools} if tools else {}
resp = client.messages.count_tokens(
model=MODEL,
messages=[{"role": "user", "content": "hi"}],
**kwargs,
)
return resp.input_tokens
要看的是 messages 始终保持不变,变化只有 tools。脚本通过 ANTHROPIC_MODEL 环境变量选择生产环境实际使用的模型,而不是随便选一个模型估算。
完整脚本保存为 measure_tools.py 后,可以针对 Python 或 Node 启动命令运行:
python measure_tools.py python -m my_server
python measure_tools.py npx -y @acme/mcp-server
脚本先输出整份定义的总开销,再逐个测量工具并降序排列。作者提醒,单工具数字包含「请求中存在工具」的固定开销,适合比较谁更重,不能直接相加当成总量。
这里还有一个测量边界:脚本只转换了名称、描述和输入 Schema,没有计入客户端可能追加的其他字段、系统提示或适配信息。因此,它测的是这份转换后定义的增量,未必等于实际客户端的完整请求成本。
如果供应商提供 token counter,可以沿用同样思路。离线粗估则可以用 json.dumps(defs) 的长度除以 4,但原文只把它视为英文内容的大致参考,不建议拿这个数字对外发布。
合并工具,要围绕任务而不是追求数量少
作者把工具膨胀归为四类:一个端点一个工具、把描述写成文档、Schema 罗列过多细节,以及长期无人调用的管理工具。
对应的改法,是重新划分模型要完成的任务。
发票场景原先有 list_invoices、get_invoice、search_invoices、get_invoice_pdf_url 四个工具。作者将它们调整为两个:一个负责找发票,一个负责取详情。
from typing import Literal
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("billing")
@mcp.tool()
def find_invoices(
query: str | None = None,
customer_id: str | None = None,
status: Literal["draft", "open", "paid", "void"] | None = None,
since: str | None = None,
limit: int = 10,
) -> list[dict]:
"""Find invoices by text, customer, status or date (ISO date). Returns id, number, customer, total, status."""
...
@mcp.tool()
def invoice_details(invoice_id: str, include_pdf_url: bool = False) -> dict:
"""Full details for one invoice, optionally with a signed PDF link."""
...
这里要看 find_invoices:文本、客户、状态和日期查询进入同一个入口,描述同时交代返回哪些字段。include_pdf_url=False 则把 PDF 链接变成详情查询的可选内容。
这段代码展示了接口形状,没有实现查询逻辑,也不能据此认定功能完全等价。旧接口有 date_from、date_to、page,新示例只展示 since、limit。如果业务需要结束日期和翻页,仍然得补齐。
Schema 也可以按同样思路缩减。250 个国家代码不必全部塞进枚举,可以接受文本后由服务端解析,或者用 lookup_codes(kind, query) 按需查找。嵌套的日期过滤对象可以展开为顶层 since、until。像 customer_id 再配一句「客户的 ID」,信息增量很小;单位、格式、时区这些会影响执行结果的说明则应该保留。
该删的是不影响决策的重复信息,该保留的是执行所需的约束。
工具按需加载,结果也要限制大小
工具定义是固定成本,工具结果是调用后继续增长的成本。一个返回 200 行完整 ORM 数据的工具,可能比定义本身消耗更多上下文。
原文建议默认只返回 10 条左右,先给 ID 和关键字段,需要时再调用详情工具。长文本可以在服务端摘要,或者返回资源链接;同时要说明怎样获取更多结果。
工具清单本身也能分组加载。协议允许服务器声明 listChanged 能力,在列表变化时发送 notifications/tools/list_changed。这样可以先暴露核心工具,等用户选定项目,或调用 enable_admin_tools 后再启用额外工具。
但这个方案依赖客户端刷新列表的支持,必须针对实际客户端验证。
更简单的是启动时选择工具组。原文通过 BILLING_TOOLSETS 环境变量读取逗号分隔的配置,默认值为 read:读取组注册 find_invoices、invoice_details,写入组注册 issue_refund、void_invoice。支持人员和财务自动化可以使用不同配置。
这能减少暴露给模型的操作范围。不过,注册工具的开关只是控制入口,具体操作仍需要服务端权限检查。
我的判断
我认同文章的优先级:先量化定义开销,再决定改哪里。对维护 MCP Server 的团队,这比凭感觉缩短描述更有依据,也便于每次修改后比较总量和最重的五个工具。
但文章没有给出某个真实服务器优化前后的 token、延迟、费用和调用成功率。哪些工具通常最重、合并后是否更准确,目前都属于作者的经验判断。
合并工具也有边界。围绕同一个任务合并查询入口合理;把互不相关的能力塞进一个大工具,再让模型填写大量模式参数,可能只是把选择困难搬进 Schema。巨型枚举改成 lookup,同样可能增加一次调用,需要结合使用频率判断。
对已有系统,启动时分组通常比动态刷新更容易验证;对参数要求严格的业务,保留必要枚举和格式说明,也可能比追求最小定义更划算。优化目标应该是完成任务的整体成本和可靠性,不能只看清单少了多少 token。
能拿来做什么
- 给 AI 编程助手清点当前启用的 MCP,测量每份工具清单的开销,并确认客户端实际会发送哪些工具;低频服务可以按任务启用。
- 开发 Node 或 TypeScript MCP Server 时,先列用户任务,再决定工具边界;已有 OpenAPI 包装层可以从重复最多的查询工具开始调整。
- 在工具定义评审中检查描述、枚举和默认值,每次修改后重测总量与最重的五个工具,同时验证常见任务还能正确完成。
- 给列表工具设置较小的默认
limit,返回 ID 和关键字段,并提供明确的继续查询方式,避免一次调用把大量无关数据塞进上下文。