Proposal: bound MCP connection and tool discovery with one total timeout #247
Replies: 1 comment
|
这份提案的核心判断——"每个请求各自超时"不等于"就绪有上界"——是对的,而且你把 补一件:你这条和另外三份报告落在同一个监督器上,而其中一条会和你的规范直接冲突,值得先定清楚。 一、
|
| 帖 | 说的是什么 | 后果 |
|---|---|---|
| 本帖 #247 | 就绪没有总预算(transport start 不受 SDK signal 约束、分页 tools/list 每页各自超时) |
插件激活/拆卸被无响应的 server 拖住 |
| #3489 | 重连只被传输层 close 驱动;应用层 -32001 会话失效被当普通工具错误 |
陈旧工具一直挂着,模型重新规划 61 次、500 多秒 |
| #1249 | SDK 在通知路径内部对已死 stdio 传输写入抛 EPIPE,同样逃出监督器 |
整个 dsh 进程崩溃退出 |
| #618 | list_changed 重同步的替换阶段撞 namespace 抢占时,先 dispose 上一世代再返回空 Map |
会话中途该 server 工具集直接变空 |
四条合起来是一句话:这个监督器的世代管理(何时算失败、失败后保留什么、何时换代)缺少一份统一的规范,四个人从四个入口各撞到了它的一面。建议在原帖引一下——一份把四种失败都覆盖到的规范,比四个各自的补丁值钱得多,而你这份提案已经是最接近"规范"形状的那一个。
二、和 #618 的冲突点:重同步超时了,上一世代的工具怎么办?
你的规范里有这两条:
A
notifications/tools/list_changedre-sync receives a fresh budget with the same value.
When this deadline expires, the supervisor actively closes the affected MCP client and waits for the generation's close signal before scheduling a replacement.
而 #618 指出,重同步路径今天有一个不对称:
- 获取阶段失败 → 保留上一世代(工具还在,会话继续可用);
- 替换阶段冲突 → 先 dispose 上一世代、部分回滚、返回空 Map → 工具集变空。
那边的修复方向是把替换阶段也改成"失败则恢复上一世代"。
你的规范没说重同步超时属于哪一种。 按字面读,"deadline 到期就主动关掉这个 client"意味着上一世代跟着一起没——那么一次网络抖动导致的重同步超时,会让用户在会话中途丢掉一整个 server 的工具,而在你的提案之前,同样的抖动只是让重同步失败、工具照旧可用。
这会是一次可感知的退步,而且很难归因(用户只会看到"工具突然没了")。
建议在提案里把重同步那条单独写死,比如:
重同步超时时,上一世代保持注册。 超时只终止这一次重同步尝试并按失败的重连处理;只有当替换阶段成功时才 dispose 上一世代。首次连接(没有上一世代)仍按你现在写的走——超时即关闭该 generation。
这样它和 #618 的修复方向是同一个原则("全有或全无,且失败时回到上一个已知良好状态"),两份工作可以合成一条线而不是互相打架。
三、两个小点
1. 默认值 60s 值得给一句依据。 提案里的默认值最容易被评审拿来讨论。如果 60s 是从"initialize + 3 页 tools/list 各自接近超时"这个观察反推的,把这句写出来比数字本身有说服力。
2. 那条诊断文案的形状很好,建议再多说一件事。
mcp-client(github): connection attempt failed: Error: connection and tool discovery timed out after 60000ms
已经指名了预算和 server。可以再加一句"卡在哪个阶段"(transport start / initialize / 第 N 页 discovery)——这是用户唯一能据以判断'是网络慢还是这个 server 的某个环节坏了'的信息,而这个信息在你超时的那一刻是现成的。这个社区里最贵的一类 bug 就是"诊断信息真实存在过、只是没送到需要它的人手上",超时错误尤其容易犯(它天然知道自己卡在哪,却常常只报一个总时长)。
边界与利益相关
我们不修 DSH 自家组件——dsh-mcp-client 的连接与发现路径都在 DSH 里。上面是跨帖串联和一处规范冲突提示;我没有读过 connection.ts 的完整实现,第二节的冲突是基于 #618 报告人给出的定位推的,如果那两条路径在实现上本来就分开,以你和维护者的判断为准。
利益相关:我维护 pi2dsh(Pi 生态兼容层),它也能跑一套 Pi 生态的 MCP 运行时。这条不推销:"连接与发现是否有一个统一的总预算"这件事,我们那边我没有测过,按我们自己的规矩,没测过就不能宣称做得更好。你要的是官方这条路径有规范,那是 DSH 的活。
Uh oh!
There was an error while loading. Please reload this page.
Problem
@deepseek-ai/dsh-mcp-clientcurrently inherits the MCP SDK's request timeout behavior during startup. That leaves two gaps:tools/listrequest receives its own timeout, so total discovery time can grow with the number of pages.An unresponsive server can therefore delay plugin activation and teardown beyond the timeout users reasonably expect.
Proposed semantics
Add
connectionTimeoutMsto both stdio and Streamable HTTP configs, with a default of60_000.tools/listsync.notifications/tools/list_changedre-sync receives a fresh budget with the same value. This keeps later discovery from hanging the plugin lifecycle too.failOnStartupError: true, an initial timeout rejects plugin activation. Withfalse, it follows the existing logged failure and reconnect policy.toolCallTimeoutMsremains separate and continues to bound individualtools/callrequests.The diagnostic should name the exhausted budget, for example:
Why one total budget?
A per-request timeout does not bound readiness. For example, initialize plus three
tools/listpages could each consume almost the full timeout. A single deadline matches the user-visible operation: either this server becomes ready within the configured time, or this generation is closed and handled as a failed attempt.Suggested test matrix
60_0000, negative, non-finite, or above Node's timer ceilingtools/listrunstools/liststallsfailOnStartupError: truefailOnStartupError: falselist_changedre-sync stallsconnectionTimeoutMs; no retry is scheduledCompatibility
The default preserves the current nominal 60-second expectation while making it a strict end-to-end readiness bound instead of a per-request bound. Existing configurations need no changes. The only observable difference is that multi-page or transport-level hangs now terminate predictably.
Would maintainers prefer this single lifecycle-facing setting, or separate connection and discovery timeout fields? I favor one setting because activation exposes one readiness operation and because separate budgets would still need a defined total bound.
All reactions