You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
【提案】MCP 工具从不写 deferLoading:一个配置开关实现按需加载 / Proposal: per-server defer in mcp-client
#11718
On a desktop profile with several MCP servers mounted, one session's first request carried 473 tool definitions / 561,657 characters of JSON schema (~137k tokens by chars/4; the provider reported 136,832 cached prompt tokens for that request), 457 of which the task never touched.
A third-party plugin that rewrites the model-visible surface in system-prompt/assemble cuts that to 16 definitions / 11,149 characters, but it recomputes the surface every turn: the first request after a rewrite reported cacheRead = 0 and 155,572 uncached input tokens.
The deferred-loading pipeline itself already exists end to end (dsh-tools → dsh-system-prompt → dsh-llm-deepseek / dsh-llm-pi-ai → dsh-session tool-addition protocol → dsh-llm's projectToolUpdates). The only missing link is that @deepseek-ai/dsh-mcp-client never sets deferLoading.
Proposal: a per-server defer?: boolean (default false) threaded into createMcpToolDefinition. Five hunks, ~6 lines. Default-off means zero behaviour change, because routes without toolUpdate strip the flag and send everything exactly as today (verified).
Correction (added after re-checking master):ctx.tools.register(definition, { when }) already exists upstream (packages/core/tools/README.md:69, packages/core/tools/src/index.ts:128, version 0.2.2-alpha.2): the condition is evaluated once per model step and, when false, the tool disappears from the executor description, PTC bindings and execution. So on-demand visibility needs no new capability on master, and my earlier statement that plugins are forced to rewrite system-prompt/assemble was wrong. It does not exist in the published 0.2.1-rc.5, where register(definition) takes no options.
That narrows the proposal to two points that still hold: (1) when changes header.tools itself, so the tool block — and the cache prefix from that point on — changes; defer_loading keeps the declarations in the request and only keeps them out of context. (2) Tools registered by someone else still cannot be marked: master exposes register/restrict/get/executable/resolve/definitions/schemas/execute/guard but no update/adopt, so a plugin can only apply when to tools it registers itself.
A fork with the five hunks plus two harnesses (15 assertions, all passing) is ready to share.
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
实测规模
一台装了多个 MCP server 的桌面 profile(逆向、桌面自动化、浏览器、办公),会话首个请求里:
我们用第三方插件在
system-prompt/assemble重写模型可见面,能压到 16 个 / 11,149 字符。但它每轮都要重算可见面:重写后的第一个请求cacheRead = 0、155,572 个未缓存输入 token。可见集一变,前缀缓存就得整段重建——用缓存的钱换上下文的钱。而链路其实已经全在了
读
0.2.1-rc.5,延迟加载的每一段都已实现:deferLoadingdsh-tools/lib/index.js:864dsh-system-prompt/lib/index.js:370-374defer_loadingdsh-llm-deepseek/lib/index.js:1731dsh-llm-pi-ai/lib/index.js:1270tool-addition/tool-removal协议dsh-session/lib/types/surface.js:275-289dsh-agent-loop/lib/index.js:1256-1272dsh-llm/lib/index.js:786(projectToolUpdates)dsh-llm-pi-ai/lib/index.js:789、dsh-llm-deepseek/lib/index.js:25唯一的断点:
@deepseek-ai/dsh-mcp-client注册工具时从不写deferLoading——它的lib/index.js里 grepdefer是空的。于是每个 MCP 工具都是常驻的、每一轮都在。提案
在
packages/mcp/mcp-client/src/index.ts的两种传输配置里各加一个defer?: boolean,经ToolBridgeOptions传到createMcpToolDefinition,在那里写deferLoading: true:5 个 hunk、约 6 行运行时逻辑。
默认
false意味着对所有既有部署零行为改变:该标记只在声明了toolUpdate的路由上生效;没有声明的路由,projectToolUpdates会把标记剥掉、照常全量下发。证据
我们 fork 了发布包、打上这 5 个 hunk,跑了两组 harness(15 条断言全部通过):
defer: true让 3/3 个 fixture 工具带标记;defer: false/ 省略时与上游逐字节一致;stdio 与 streamable-http 两种传输都接受该选项addition-only把未激活的延迟工具挡在请求外、激活后补一条tool-addition;in-history全量下发且标记与消息都保留补丁与 harness 可以随时贴上来。
更正与边界(发文后复核)
复核 upstream master 后更正一处:
ctx.tools.register(definition, { when })已经是官方 API(packages/core/tools/README.md:69、packages/core/tools/src/index.ts:128,版本0.2.2-alpha.2):条件每个模型步骤求值一次,为 false 时该工具从执行器描述、PTC 绑定与执行中一并消失。所以"按需出现 / 消失"在 master 上不需要任何新能力,也不需要重写提示词面——我先前写成"插件只能重写system-prompt/assemble"是错的。但它在
0.2.1-rc.5的发布包里不存在:该版本register(definition)没有第二个参数(实测发布产物)。修正之后,本提案的价值点收窄为两条,仍然成立:
defer独有的是"前缀稳定"。when让工具从header.tools里消失,工具块一变,前缀缓存从工具块起失效;deferLoading让声明留在请求里、只把内容挡在 context 外,前缀不动。register/restrict/get/executable/resolve/definitions/schemas/execute/guard,仍然没有update/adopt:插件只能用when管自己注册的工具,改不了别人已注册的那些。English summary
On a desktop profile with several MCP servers mounted, one session's first request carried 473 tool definitions / 561,657 characters of JSON schema (~137k tokens by chars/4; the provider reported 136,832 cached prompt tokens for that request), 457 of which the task never touched.
A third-party plugin that rewrites the model-visible surface in
system-prompt/assemblecuts that to 16 definitions / 11,149 characters, but it recomputes the surface every turn: the first request after a rewrite reportedcacheRead = 0and 155,572 uncached input tokens.The deferred-loading pipeline itself already exists end to end (
dsh-tools→dsh-system-prompt→dsh-llm-deepseek/dsh-llm-pi-ai→dsh-sessiontool-addition protocol →dsh-llm'sprojectToolUpdates). The only missing link is that@deepseek-ai/dsh-mcp-clientnever setsdeferLoading.Proposal: a per-server
defer?: boolean(defaultfalse) threaded intocreateMcpToolDefinition. Five hunks, ~6 lines. Default-off means zero behaviour change, because routes withouttoolUpdatestrip the flag and send everything exactly as today (verified).Correction (added after re-checking master):
ctx.tools.register(definition, { when })already exists upstream (packages/core/tools/README.md:69,packages/core/tools/src/index.ts:128, version0.2.2-alpha.2): the condition is evaluated once per model step and, when false, the tool disappears from the executor description, PTC bindings and execution. So on-demand visibility needs no new capability on master, and my earlier statement that plugins are forced to rewritesystem-prompt/assemblewas wrong. It does not exist in the published0.2.1-rc.5, whereregister(definition)takes no options.That narrows the proposal to two points that still hold: (1)
whenchangesheader.toolsitself, so the tool block — and the cache prefix from that point on — changes;defer_loadingkeeps the declarations in the request and only keeps them out of context. (2) Tools registered by someone else still cannot be marked: master exposesregister/restrict/get/executable/resolve/definitions/schemas/execute/guardbut noupdate/adopt, so a plugin can only applywhento tools it registers itself.A fork with the five hunks plus two harnesses (15 assertions, all passing) is ready to share.
附件 / Attachments:补丁(5 hunk)与两组 harness(15 条断言)已放在 gist:
https://gist.github.com/W117C/53e42c89ea106747785cf90a715cc4b6
All reactions