Skip to content

🐛 fix(sdk): MCP/LSP 域十项修复——孤儿进程回收、连接退避、spawn 环境白名单与写超时 - #440

Merged
CavinHuang merged 9 commits into
mainfrom
fix/sdk-mcp-lsp
Aug 22, 2026
Merged

🐛 fix(sdk): MCP/LSP 域十项修复——孤儿进程回收、连接退避、spawn 环境白名单与写超时#440
CavinHuang merged 9 commits into
mainfrom
fix/sdk-mcp-lsp

Conversation

@CavinHuang

Copy link
Copy Markdown
Owner

概述

修复 sdk 全量 review 提出的 10 个 issue(MCP + LSP 域),每个 commit 独立可跑绿。

Issue 修复
#311 (P1) connectMCPServer catch 内 best-effort close()——listTools 抛错后已 spawn 的 stdio 子进程回收,孤儿进程不再累积
#312 (P1) ensureConnected 失败负缓存指数退避 5s→60s + force 绕过(sidecar 手动诊断走 force)——挂死 server 不再阻塞每次 run 启动 30-60s
#380 (P1 安全) LSP server spawn 环境改 getDefaultEnvironment 最小白名单 + server.env 显式合并——项目可控 lsp.json 不再被动读取宿主 API key/token(对齐 MCP 路径既有收口)
#327 (P1) LSP stdin 写入加 10s 超时并触发既有 fail()/重启链路——僵死语言服务器不再永久挂起 Write/Edit 全路径
#325 wrapper 名去重提升 manager 实例级集合(同步段内 release+claim 防跨 server 窃名、断开回收)——大小写折叠碰撞后工具静默覆盖消除
#326 新建 mcp/naming.ts 共享 util:规范化 + 64 字符截断 + shortHash 消歧,两拼装点收口——非法字符直通 provider 致整轮 400 关闭
#375 callTool 在途 disconnect 时结果 undefined 即抛 protocol_error,不再把空文本伪装成成功喂给模型
#384 defaultClientFactory 接 tools/list_changed,generation+client 双校验地重拉 listTools 原地刷新——server 动态增删工具不再陈旧至重连
#328 SwiftLint 适配器按实际解析 shell 选调用符(shellKind 提升 utils 共享)——win32 Git Bash 下行首 & 语法错误消除
#374 lspmux 探测缓存按 resolve(cwd) 键控 Map,正向 TTL 30s/负向 5min,启动失败主动失效回退直连

验证

Closes #311, closes #312, closes #325, closes #326, closes #327, closes #328, closes #374, closes #375, closes #380, closes #384

🤖 Generated with Claude Code

@CavinHuang

Copy link
Copy Markdown
Owner Author

Code Review

十项逐项核对:七项(#325/#326/#327/#328/#374/#375/#384)真实落地、方向正确、测试钉死有效;但三项与 main 已合入实现重复,且分支 CONFLICTING——须 rebase 去重后才能合

重复冲突项(阻塞)

# 状态 处置建议
#311 孤儿回收 PR#437 已合入等价实现,issue 已 CLOSED 删除对应 commit
#312 负缓存退避 PR#437 已合入 nextRetryAt 方案;两套退避选项名并存会打架 移植 force 绕过(main 缺失的真增量)进 main 方案
#380 spawn 白名单 PR#443 已合入 getDefaultEnvironment 逐参等价 移植 env 解析+merge(LSPMUX_SERVER 覆盖问题)进 main 代码

rebase 后需人工核对 workspace-mcp-manager 探测点同时保留 main 的 mapPublicError 包装与本分支 {force:true}

真增量七项核验(rebase 后原样保留)

非阻塞 P3

另注意合并顺序协调:PR#444(dead-code)同样触碰 mcp/client.ts / sdk-mcp-server.ts / README.md,先合者会制造新冲突。

@CavinHuang

Copy link
Copy Markdown
Owner Author

Code Review

七项新修复(#384 listChanged 订阅/#325 takenWrapperNames/#327 stdin 写超时/#374 lspmux 键控/#375 callTool 守卫/#328 SwiftLint win32/#405 seam 化测试)全部实证正确,但基线落后 main 三个已合 PR(#437 关掉 #311/#312,#443 关掉 #380),需 rebase 解冲突后再合

[P1·合并操作风险] 原样合入将回退 main 已有修复

merge-tree 实测 4 文件冲突,其中两处若按分支版本解决会静默丢弃:

[P2] disconnect() 不清自身负缓存

与 PR 描述"显式 disconnect 清零"矛盾(靠 force 兜底);main 版行为是清零的。rebase 时建议顺手对齐。

[P3] 死代码上的重复投资

client.ts/sdk-mcp-server.ts 属 #388 判定的死代码路径,#311/#326 hunk 是对死代码的重复修复;真实承接面是存活的 manager.ts 栈(#444 将删除死路径,故 #440 先合、#444 后 rebase)。

测试

seam 化(setDefaultMcpSdkClientConstructor/setLspmuxProbeSpawn)替代 mock.module,符合 CI bun 1.3.13 严格导出约束 ✓;seam 未泄漏进公共导出 ✓

结论

修复合并基线后可合——rebase 弃重复 hunk,按上述方案解四处冲突。

TaTaLiao and others added 8 commits August 22, 2026 22:53
- ensureConnected 失败负缓存:指数退避(5s 起步倍增封顶 60s),
  挂死 server 不再每个 run 阻塞整个连接超时;connect/testServer
  支持 force 绕过,sidecar 手动探测走 force。
- wrapper 工具名去重提升为 manager 实例级集合:server id 大小写
  折叠后跨 server 碰撞不再静默互相覆盖,断开/重建时回收旧名。
- callTool 结果为 undefined 时抛 protocol_error:在途 disconnect
  不再把空结果伪装成成功喂给模型(合法空结果是对象)。
- 默认 client factory 订阅 tools/list_changed:带 generation 校验
  地重拉 listTools 原地刷新工具表,动态增删工具不再陈旧到重连。
- 新增 mcp/naming 共享 util:normalizeMcpServerId/ToolName +
  buildMcpToolName(64 字符截断 + hash 消歧),client.ts 与
  sdk-mcp-server 两处拼装点统一收口,engine 用量报告前缀同步。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
win32 无条件加 PowerShell "& " 调用符,但 Bash 工具在 win32 优先
走 Git Bash,行首 & 是语法错误,SwiftLint 诊断全量静默丢失。
shellKind 判定从 bash.ts 提升到 utils/shell-invocation 共享导出,
适配器改用与 Bash 工具一致的 resolveShellInvocation+shellKind
判定:仅 PowerShell 方言加 "&",Git Bash 不加。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
探测缓存原为模块级单例:不分 cwd 导致跨工作区误判,且 5 分钟
TTL 内 running 不复检,守护进程启/停双向滞后。改为 Map 按
resolve(cwd) 键控,正向 TTL 压到 30s、负向保 5min;新增
invalidateLspmuxCache,LspClient 启动 mux'd server 失败时主动
失效缓存,下次解析回退直连。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
send() 对 stdin.write 回调无限期等待且全通知链路无超时,LSP 僵死
即 Write/Edit 永久挂起。所有通知路径都汇聚于 send(),在此处统一
加 Promise.race 写超时(默认 10s)并在超时时 fail() 标记客户端
死亡,一处改动覆盖全部通知路径,调用方经 onDead 进入既有重启链。
新增 setLspWriteTimeout 供运行时与测试调整。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
LspClient.start 原样 {...process.env} 全量继承宿主环境,而 lsp.json
由项目侧可控,任意 command 可读取宿主 API key/token(MCP 两路径
早已收口 getDefaultEnvironment,LSP 未同步)。spawn env 改为官方
SDK 最小默认环境 + server.env 显式白名单合并;normalizeServerConfig
补 record.env 透传入口(过滤非字符串值),resolveAvailableServer 对
lspmux 包装 env 改覆盖为合并避免丢失用户配置。测试以注入宿主变量
钉死不泄漏、白名单变量透传、PATH 可用。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
lspmux 探测 spawn 与 MCP 默认 client 构造器原经 mock.module 整模块
替换,bun test 单进程套件内全局生效且 factory 缺失具名导出,组合跑
时污染后续文件(CI bun 1.3.13 更严必炸)。生产代码零行为变化:
lspmux 新增 setLspmuxProbeSpawn、manager 新增
setDefaultMcpSdkClientConstructor 两个模块级可覆盖 seam,测试改注
入 fake。reviewer 五文件组合复跑零串扰。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
#312 退避已由 main 以 failureRetryBaseMs/MaxMs + nextRetryAt 实现,
本分支原退避时序断言与之重复,收敛为单个 force 旁路测试并换用
main 的选项名。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
collectLspAdapterDiagnostics 依赖 resolveLspExecutable 找到
swiftlint:POSIX 下探针要求 X_OK,测试只 writeFile 未 chmod,CI
Linux 上解析失败静默返回 undefined;且裸名解析会兜底扫宿主 PATH,
结果不确定。改为显式 toolConfig 注入绝对路径 + POSIX 补 chmod。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@CavinHuang
CavinHuang merged commit c6145fd into main Aug 22, 2026
4 checks passed
CavinHuang pushed a commit that referenced this pull request Aug 22, 2026
- packages/sdk/src/lsp/ 5 文件 modify/delete 冲突:保留 #456 的删除
- #440 期间新增的 lspmux.test.ts 随子系统一并删除(#456 意图整体移除)
- typecheck 6/6 绿;sdk/sidecar/shared/web 受影响测试 242 用例全绿

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment