@gajae-code/coding-agent 0.17.2 → 0.17.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (390) hide show
  1. package/CHANGELOG.md +267 -0
  2. package/dist/types/cli/daemon-cli.d.ts +3 -1
  3. package/dist/types/cli/github-release.d.ts +17 -0
  4. package/dist/types/cli/public-command-entry.d.ts +56 -0
  5. package/dist/types/cli/public-command-errors.d.ts +126 -0
  6. package/dist/types/cli/public-command-evidence.d.ts +99 -0
  7. package/dist/types/cli/public-command-help.d.ts +65 -0
  8. package/dist/types/cli/public-command-registry.d.ts +69 -0
  9. package/dist/types/cli/session-picker.d.ts +1 -1
  10. package/dist/types/cli/setup-cli.d.ts +1 -0
  11. package/dist/types/cli/skills-cli.d.ts +5 -0
  12. package/dist/types/cli.d.ts +3 -0
  13. package/dist/types/commands/daemon.d.ts +4 -50
  14. package/dist/types/commands/sdk.d.ts +20 -37
  15. package/dist/types/commands/setup.d.ts +3 -0
  16. package/dist/types/commands/skills.d.ts +9 -0
  17. package/dist/types/config/autorouting-tier-map.d.ts +1049 -3
  18. package/dist/types/config/file-lock.d.ts +13 -3
  19. package/dist/types/config/model-profile-activation.d.ts +45 -1
  20. package/dist/types/config/model-registry.d.ts +14 -3
  21. package/dist/types/config/model-resolver.d.ts +1 -1
  22. package/dist/types/config/settings-schema.d.ts +11 -0
  23. package/dist/types/coordinator-mcp/question-state.d.ts +6 -1
  24. package/dist/types/daemon/operator-contract.d.ts +5 -1
  25. package/dist/types/eval/js/worker-protocol.d.ts +2 -0
  26. package/dist/types/exec/bash-executor.d.ts +2 -0
  27. package/dist/types/extensibility/extensions/types.d.ts +12 -0
  28. package/dist/types/extensibility/gjc-plugins/registry.d.ts +4 -0
  29. package/dist/types/extensibility/gjc-plugins/runtime-adapters.d.ts +11 -2
  30. package/dist/types/extensibility/runtime-skill-discovery.d.ts +18 -0
  31. package/dist/types/extensibility/shared-events.d.ts +3 -1
  32. package/dist/types/gjc-runtime/session-state-lock.d.ts +61 -0
  33. package/dist/types/gjc-runtime/state-writer.d.ts +14 -1
  34. package/dist/types/harness-control-plane/control-endpoint.d.ts +4 -1
  35. package/dist/types/harness-control-plane/finalize.d.ts +14 -3
  36. package/dist/types/internal-urls/embedded-protocol.d.ts +9 -0
  37. package/dist/types/internal-urls/index.d.ts +1 -0
  38. package/dist/types/internal-urls/parse.d.ts +9 -1
  39. package/dist/types/main.d.ts +23 -1
  40. package/dist/types/modes/acp/acp-agent.d.ts +55 -1
  41. package/dist/types/modes/acp/startup-provenance.d.ts +16 -0
  42. package/dist/types/modes/components/custom-provider-wizard.d.ts +19 -1
  43. package/dist/types/modes/components/session-selector.d.ts +5 -2
  44. package/dist/types/modes/components/user-message-selector.d.ts +5 -2
  45. package/dist/types/modes/controllers/command-controller.d.ts +1 -1
  46. package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
  47. package/dist/types/modes/interactive-mode.d.ts +1 -1
  48. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +11 -1
  49. package/dist/types/modes/shared/agent-wire/workflow-gate-types.d.ts +14 -1
  50. package/dist/types/modes/types.d.ts +2 -2
  51. package/dist/types/runtime/process-lifecycle.d.ts +2 -0
  52. package/dist/types/runtime-mcp/loader.d.ts +2 -0
  53. package/dist/types/runtime-mcp/manager.d.ts +69 -6
  54. package/dist/types/runtime-mcp/pool.d.ts +17 -0
  55. package/dist/types/runtime-mcp/redaction.d.ts +20 -0
  56. package/dist/types/runtime-mcp/startup-policy.d.ts +6 -0
  57. package/dist/types/runtime-mcp/tool-bridge.d.ts +9 -0
  58. package/dist/types/runtime-mcp/transports/http.d.ts +11 -0
  59. package/dist/types/sdk/broker/broker.d.ts +17 -4
  60. package/dist/types/sdk/broker/ensure.d.ts +3 -0
  61. package/dist/types/sdk/broker/identity.d.ts +5 -1
  62. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +23 -3
  63. package/dist/types/sdk/broker/lifecycle.d.ts +15 -8
  64. package/dist/types/sdk/broker/managed-task-dag.d.ts +1335 -0
  65. package/dist/types/sdk/broker/managed-task-verification.d.ts +37 -0
  66. package/dist/types/sdk/broker/runtime.d.ts +1 -1
  67. package/dist/types/sdk/broker/session-index.d.ts +22 -3
  68. package/dist/types/sdk/broker/spawn-authority.d.ts +12 -0
  69. package/dist/types/sdk/broker/startup-failure.d.ts +4 -0
  70. package/dist/types/sdk/broker/stderr-drainer.d.ts +13 -0
  71. package/dist/types/sdk/bus/index.d.ts +19 -0
  72. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +7 -1
  73. package/dist/types/sdk/bus/telegram-daemon.d.ts +5 -0
  74. package/dist/types/sdk/cli/master-cli.d.ts +6 -10
  75. package/dist/types/sdk/cli/rows.d.ts +19 -1
  76. package/dist/types/sdk/cli/session-cli.d.ts +22 -5
  77. package/dist/types/sdk/client/client.d.ts +2 -0
  78. package/dist/types/sdk/guides/cli.d.ts +3 -4
  79. package/dist/types/sdk/guides/verify.d.ts +2 -0
  80. package/dist/types/sdk/host/host.d.ts +9 -0
  81. package/dist/types/sdk/host/session-runtime.d.ts +31 -0
  82. package/dist/types/sdk/host/types.d.ts +10 -0
  83. package/dist/types/sdk/index.d.ts +1 -0
  84. package/dist/types/sdk/prompt-deadline-flush.d.ts +85 -0
  85. package/dist/types/sdk/prompt-deadline-manager.d.ts +37 -0
  86. package/dist/types/sdk/prompt-tool-boundary.d.ts +73 -0
  87. package/dist/types/sdk/router/session-router.d.ts +2 -0
  88. package/dist/types/sdk/session.d.ts +3 -3
  89. package/dist/types/sdk/startup-capability.d.ts +2 -0
  90. package/dist/types/sdk/transport/relay.d.ts +1 -0
  91. package/dist/types/sdk/transport/serve-cli.d.ts +48 -4
  92. package/dist/types/sdk/transport/stdio.d.ts +5 -1
  93. package/dist/types/session/agent-session.d.ts +175 -23
  94. package/dist/types/session/fallback-chain-controller.d.ts +6 -6
  95. package/dist/types/session/fold-coordinator.d.ts +17 -3
  96. package/dist/types/session/internal/managed-session-storage.d.ts +7 -1
  97. package/dist/types/session/session-manager.d.ts +64 -1
  98. package/dist/types/session/session-work-lease.d.ts +14 -0
  99. package/dist/types/setup/model-onboarding-guidance.d.ts +1 -1
  100. package/dist/types/setup/provider-onboarding.d.ts +81 -1
  101. package/dist/types/task/types.d.ts +2 -0
  102. package/dist/types/tools/bash-interactive.d.ts +1 -0
  103. package/dist/types/tools/bash.d.ts +37 -11
  104. package/dist/types/tools/fetch.d.ts +10 -1
  105. package/dist/types/tools/job.d.ts +1 -1
  106. package/dist/types/tools/steer-fold.d.ts +31 -0
  107. package/dist/types/tools/subagent.d.ts +8 -1
  108. package/dist/types/utils/git.d.ts +28 -2
  109. package/dist/types/utils/mupdf-embedded.d.ts +2 -0
  110. package/dist/types/utils/mupdf.d.ts +4 -0
  111. package/dist/types/utils/session-title-generation.d.ts +6 -0
  112. package/dist/types/utils/title-generator.d.ts +11 -2
  113. package/dist/types/web/scrapers/types.d.ts +3 -0
  114. package/package.json +18 -10
  115. package/scripts/build-binary.ts +9 -3
  116. package/scripts/compile-args.ts +0 -9
  117. package/scripts/embed-mupdf.ts +52 -0
  118. package/scripts/generate-sdk-operation-inventory.ts +31 -1
  119. package/scripts/vendor-markit.ts +196 -0
  120. package/src/async/job-manager.ts +22 -1
  121. package/src/cli/daemon-cli.ts +53 -18
  122. package/src/cli/github-release.ts +87 -5
  123. package/src/cli/mcp-cli.ts +38 -4
  124. package/src/cli/public-command-entry.ts +450 -0
  125. package/src/cli/public-command-errors.ts +581 -0
  126. package/src/cli/public-command-evidence.ts +717 -0
  127. package/src/cli/public-command-help.ts +360 -0
  128. package/src/cli/public-command-registry.ts +541 -0
  129. package/src/cli/session-picker.ts +2 -1
  130. package/src/cli/setup-cli.ts +10 -1
  131. package/src/cli/skills-cli.ts +46 -2
  132. package/src/cli/stats-cli.ts +11 -1
  133. package/src/cli.ts +61 -1
  134. package/src/commands/daemon.ts +65 -69
  135. package/src/commands/sdk.ts +175 -117
  136. package/src/commands/setup.ts +2 -0
  137. package/src/commands/skills.ts +11 -0
  138. package/src/config/autorouting-tier-map.ts +543 -3
  139. package/src/config/file-lock.ts +206 -65
  140. package/src/config/model-discovery-manager.ts +4 -4
  141. package/src/config/model-profile-activation.ts +417 -66
  142. package/src/config/model-registry.ts +254 -36
  143. package/src/config/model-resolver.ts +6 -1
  144. package/src/config/settings-schema.ts +26 -2
  145. package/src/coordinator-mcp/policy.ts +10 -5
  146. package/src/coordinator-mcp/question-gate-codec.ts +17 -0
  147. package/src/coordinator-mcp/question-state.ts +179 -9
  148. package/src/coordinator-mcp/server.ts +521 -87
  149. package/src/coordinator-mcp/session-reaper.ts +6 -4
  150. package/src/crash/record-loader.ts +6 -2
  151. package/src/daemon/operator-contract.ts +26 -12
  152. package/src/defaults/gjc/skills/ultragoal/SKILL.md +1 -1
  153. package/src/eval/js/context-manager.ts +8 -8
  154. package/src/eval/js/worker-core.ts +9 -4
  155. package/src/eval/js/worker-protocol.ts +2 -0
  156. package/src/exec/bash-executor.ts +3 -0
  157. package/src/extensibility/extensions/runner.ts +52 -4
  158. package/src/extensibility/extensions/types.ts +10 -0
  159. package/src/extensibility/gjc-plugins/registry.ts +196 -37
  160. package/src/extensibility/gjc-plugins/runtime-adapters.ts +46 -8
  161. package/src/extensibility/runtime-skill-discovery.ts +61 -3
  162. package/src/extensibility/shared-events.ts +3 -1
  163. package/src/gjc-runtime/session-state-lock.ts +165 -24
  164. package/src/gjc-runtime/state-writer.ts +317 -6
  165. package/src/gjc-runtime/ultragoal-succession.ts +31 -3
  166. package/src/harness-control-plane/control-endpoint.ts +24 -6
  167. package/src/harness-control-plane/finalize.ts +312 -25
  168. package/src/harness-control-plane/owner.ts +323 -89
  169. package/src/internal-urls/docs-index.generated.ts +23 -20
  170. package/src/internal-urls/embedded-protocol.ts +83 -0
  171. package/src/internal-urls/index.ts +1 -0
  172. package/src/internal-urls/parse.ts +21 -1
  173. package/src/internal-urls/router.ts +4 -2
  174. package/src/main.ts +192 -74
  175. package/src/modes/acp/acp-agent.ts +973 -56
  176. package/src/modes/acp/startup-provenance.ts +59 -0
  177. package/src/modes/components/custom-provider-wizard.ts +308 -7
  178. package/src/modes/components/gjc-bundle-settings.ts +21 -1
  179. package/src/modes/components/oauth-selector.ts +9 -0
  180. package/src/modes/components/session-selector.ts +47 -4
  181. package/src/modes/components/tool-status-header.ts +72 -6
  182. package/src/modes/components/user-message-selector.ts +57 -15
  183. package/src/modes/controllers/command-controller.ts +58 -32
  184. package/src/modes/controllers/extension-ui-controller.ts +33 -7
  185. package/src/modes/controllers/input-controller.ts +4 -0
  186. package/src/modes/controllers/runtime-mcp-command-controller.ts +1 -1
  187. package/src/modes/controllers/selector-controller.ts +161 -15
  188. package/src/modes/interactive-mode.ts +21 -7
  189. package/src/modes/runtime-init.ts +1 -0
  190. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +124 -7
  191. package/src/modes/shared/agent-wire/workflow-gate-types.ts +16 -2
  192. package/src/modes/types.ts +2 -2
  193. package/src/prompts/agent-fragments/browser-aside.md +1 -0
  194. package/src/prompts/system/title-system.md +1 -1
  195. package/src/prompts/tools/bash.md +1 -1
  196. package/src/prompts/tools/job.md +1 -1
  197. package/src/prompts/tools/read.md +2 -0
  198. package/src/runtime/network-prewarm-service.test.ts +36 -1
  199. package/src/runtime/network-prewarm-service.ts +28 -3
  200. package/src/runtime/process-lifecycle.ts +22 -11
  201. package/src/runtime-mcp/config.ts +16 -1
  202. package/src/runtime-mcp/loader.ts +6 -3
  203. package/src/runtime-mcp/manager.ts +845 -109
  204. package/src/runtime-mcp/pool.test.ts +31 -0
  205. package/src/runtime-mcp/pool.ts +304 -28
  206. package/src/runtime-mcp/redaction.ts +42 -0
  207. package/src/runtime-mcp/startup-policy.ts +7 -0
  208. package/src/runtime-mcp/tool-bridge.ts +71 -0
  209. package/src/runtime-mcp/transports/http.ts +108 -25
  210. package/src/sdk/acp/adapter.ts +27 -8
  211. package/src/sdk/broker/broker.ts +1431 -198
  212. package/src/sdk/broker/ensure.ts +21 -3
  213. package/src/sdk/broker/identity.ts +22 -1
  214. package/src/sdk/broker/lifecycle-ledger.ts +95 -31
  215. package/src/sdk/broker/lifecycle.ts +690 -141
  216. package/src/sdk/broker/managed-task-dag.ts +1927 -0
  217. package/src/sdk/broker/managed-task-verification.ts +642 -0
  218. package/src/sdk/broker/runtime.ts +1 -1
  219. package/src/sdk/broker/session-index.ts +97 -4
  220. package/src/sdk/broker/spawn-authority.ts +175 -14
  221. package/src/sdk/broker/startup-failure.ts +80 -1
  222. package/src/sdk/broker/stderr-drainer.ts +59 -0
  223. package/src/sdk/broker/transport.ts +1 -0
  224. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  225. package/src/sdk/bus/chat-daemon-runtime.ts +1 -0
  226. package/src/sdk/bus/index.ts +707 -81
  227. package/src/sdk/bus/telegram-daemon-cli.ts +31 -2
  228. package/src/sdk/bus/telegram-daemon-contract.ts +7 -1
  229. package/src/sdk/bus/telegram-daemon.ts +10 -3
  230. package/src/sdk/cli/master-cli.ts +47 -36
  231. package/src/sdk/cli/rows.ts +30 -7
  232. package/src/sdk/cli/session-cli.ts +705 -184
  233. package/src/sdk/client/client.ts +18 -0
  234. package/src/sdk/guides/catalog.ts +2 -2
  235. package/src/sdk/guides/cli.ts +20 -62
  236. package/src/sdk/guides/verify.ts +14 -0
  237. package/src/sdk/host/host.ts +49 -5
  238. package/src/sdk/host/session-runtime.test.ts +184 -2
  239. package/src/sdk/host/session-runtime.ts +322 -101
  240. package/src/sdk/host/types.ts +12 -1
  241. package/src/sdk/host/websocket-transport.lifecycle.test.ts +84 -0
  242. package/src/sdk/host/websocket-transport.ts +40 -6
  243. package/src/sdk/index.ts +11 -0
  244. package/src/sdk/lifecycle/service.ts +2 -0
  245. package/src/sdk/prompt-deadline-flush.ts +356 -0
  246. package/src/sdk/prompt-deadline-manager.ts +133 -1
  247. package/src/sdk/prompt-tool-boundary.ts +121 -0
  248. package/src/sdk/protocol/operation-inventory.generated.json +106 -0
  249. package/src/sdk/router/session-router.ts +9 -1
  250. package/src/sdk/session.ts +763 -116
  251. package/src/sdk/startup-capability.ts +1 -1
  252. package/src/sdk/transport/relay.ts +71 -6
  253. package/src/sdk/transport/serve-cli.ts +397 -42
  254. package/src/sdk/transport/socket.ts +12 -11
  255. package/src/sdk/transport/stdio.ts +7 -3
  256. package/src/session/agent-session.ts +2472 -407
  257. package/src/session/fallback-chain-controller.ts +6 -7
  258. package/src/session/fold-coordinator.ts +24 -5
  259. package/src/session/internal/managed-session-scope.ts +68 -18
  260. package/src/session/internal/managed-session-storage.ts +334 -12
  261. package/src/session/session-manager.ts +382 -44
  262. package/src/session/session-work-lease.ts +34 -0
  263. package/src/setup/model-onboarding-guidance.ts +1 -1
  264. package/src/setup/provider-onboarding.ts +397 -24
  265. package/src/setup/provider-presets.json +2 -1
  266. package/src/slash-commands/builtin-registry.ts +259 -15
  267. package/src/task/executor.ts +46 -17
  268. package/src/task/index.ts +14 -2
  269. package/src/task/types.ts +2 -0
  270. package/src/tools/bash-interactive.ts +2 -0
  271. package/src/tools/bash.ts +118 -78
  272. package/src/tools/fetch.ts +140 -25
  273. package/src/tools/job.ts +151 -25
  274. package/src/tools/output-meta.ts +15 -2
  275. package/src/tools/path-utils.ts +27 -3
  276. package/src/tools/read.ts +3 -3
  277. package/src/tools/steer-fold.ts +107 -0
  278. package/src/tools/subagent-render.ts +24 -1
  279. package/src/tools/subagent.ts +27 -1
  280. package/src/tools/tool-catalog.generated.ts +29 -2
  281. package/src/tools/tool-errors.ts +4 -0
  282. package/src/utils/git.ts +53 -3
  283. package/src/utils/markit.ts +55 -41
  284. package/src/utils/mupdf-assets.d.ts +4 -0
  285. package/src/utils/mupdf-embedded.ts +4 -0
  286. package/src/utils/mupdf.ts +136 -0
  287. package/src/utils/session-title-generation.ts +29 -0
  288. package/src/utils/title-generator.ts +46 -2
  289. package/src/web/scrapers/types.ts +24 -3
  290. package/vendor/markit-ai/LICENSE +21 -0
  291. package/vendor/markit-ai/MANIFEST.json +412 -0
  292. package/vendor/markit-ai/dist/commands/config.d.ts +4 -0
  293. package/vendor/markit-ai/dist/commands/config.js +139 -0
  294. package/vendor/markit-ai/dist/commands/convert.d.ts +6 -0
  295. package/vendor/markit-ai/dist/commands/convert.js +113 -0
  296. package/vendor/markit-ai/dist/commands/formats.d.ts +2 -0
  297. package/vendor/markit-ai/dist/commands/formats.js +92 -0
  298. package/vendor/markit-ai/dist/commands/init.d.ts +2 -0
  299. package/vendor/markit-ai/dist/commands/init.js +29 -0
  300. package/vendor/markit-ai/dist/commands/onboard.d.ts +2 -0
  301. package/vendor/markit-ai/dist/commands/onboard.js +61 -0
  302. package/vendor/markit-ai/dist/commands/plugin.d.ts +4 -0
  303. package/vendor/markit-ai/dist/commands/plugin.js +58 -0
  304. package/vendor/markit-ai/dist/config.d.ts +26 -0
  305. package/vendor/markit-ai/dist/config.js +42 -0
  306. package/vendor/markit-ai/dist/converters/audio.d.ts +7 -0
  307. package/vendor/markit-ai/dist/converters/audio.js +104 -0
  308. package/vendor/markit-ai/dist/converters/csv.d.ts +7 -0
  309. package/vendor/markit-ai/dist/converters/csv.js +83 -0
  310. package/vendor/markit-ai/dist/converters/docx.d.ts +6 -0
  311. package/vendor/markit-ai/dist/converters/docx.js +57 -0
  312. package/vendor/markit-ai/dist/converters/epub.d.ts +8 -0
  313. package/vendor/markit-ai/dist/converters/epub.js +128 -0
  314. package/vendor/markit-ai/dist/converters/github.d.ts +18 -0
  315. package/vendor/markit-ai/dist/converters/github.js +148 -0
  316. package/vendor/markit-ai/dist/converters/html.d.ts +6 -0
  317. package/vendor/markit-ai/dist/converters/html.js +30 -0
  318. package/vendor/markit-ai/dist/converters/image.d.ts +6 -0
  319. package/vendor/markit-ai/dist/converters/image.js +124 -0
  320. package/vendor/markit-ai/dist/converters/ipynb.d.ts +6 -0
  321. package/vendor/markit-ai/dist/converters/ipynb.js +75 -0
  322. package/vendor/markit-ai/dist/converters/iwork.d.ts +20 -0
  323. package/vendor/markit-ai/dist/converters/iwork.js +391 -0
  324. package/vendor/markit-ai/dist/converters/json.d.ts +6 -0
  325. package/vendor/markit-ai/dist/converters/json.js +21 -0
  326. package/vendor/markit-ai/dist/converters/pdf/columns.d.ts +35 -0
  327. package/vendor/markit-ai/dist/converters/pdf/columns.js +93 -0
  328. package/vendor/markit-ai/dist/converters/pdf/extract.d.ts +19 -0
  329. package/vendor/markit-ai/dist/converters/pdf/extract.js +514 -0
  330. package/vendor/markit-ai/dist/converters/pdf/grid.d.ts +25 -0
  331. package/vendor/markit-ai/dist/converters/pdf/grid.js +795 -0
  332. package/vendor/markit-ai/dist/converters/pdf/headers.d.ts +24 -0
  333. package/vendor/markit-ai/dist/converters/pdf/headers.js +108 -0
  334. package/vendor/markit-ai/dist/converters/pdf/index.d.ts +19 -0
  335. package/vendor/markit-ai/dist/converters/pdf/index.js +136 -0
  336. package/vendor/markit-ai/dist/converters/pdf/mupdf-loader.js +9 -0
  337. package/vendor/markit-ai/dist/converters/pdf/render.d.ts +24 -0
  338. package/vendor/markit-ai/dist/converters/pdf/render.js +513 -0
  339. package/vendor/markit-ai/dist/converters/pdf/types.d.ts +75 -0
  340. package/vendor/markit-ai/dist/converters/pdf/types.js +1 -0
  341. package/vendor/markit-ai/dist/converters/plain-text.d.ts +6 -0
  342. package/vendor/markit-ai/dist/converters/plain-text.js +87 -0
  343. package/vendor/markit-ai/dist/converters/pptx.d.ts +8 -0
  344. package/vendor/markit-ai/dist/converters/pptx.js +289 -0
  345. package/vendor/markit-ai/dist/converters/rss.d.ts +11 -0
  346. package/vendor/markit-ai/dist/converters/rss.js +138 -0
  347. package/vendor/markit-ai/dist/converters/wikipedia.d.ts +6 -0
  348. package/vendor/markit-ai/dist/converters/wikipedia.js +33 -0
  349. package/vendor/markit-ai/dist/converters/xlsx.d.ts +8 -0
  350. package/vendor/markit-ai/dist/converters/xlsx.js +147 -0
  351. package/vendor/markit-ai/dist/converters/xml.d.ts +6 -0
  352. package/vendor/markit-ai/dist/converters/xml.js +18 -0
  353. package/vendor/markit-ai/dist/converters/yaml.d.ts +6 -0
  354. package/vendor/markit-ai/dist/converters/yaml.js +17 -0
  355. package/vendor/markit-ai/dist/converters/zip.d.ts +8 -0
  356. package/vendor/markit-ai/dist/converters/zip.js +57 -0
  357. package/vendor/markit-ai/dist/index.d.ts +30 -0
  358. package/vendor/markit-ai/dist/index.js +26 -0
  359. package/vendor/markit-ai/dist/main.d.ts +2 -0
  360. package/vendor/markit-ai/dist/main.js +191 -0
  361. package/vendor/markit-ai/dist/markit.d.ts +45 -0
  362. package/vendor/markit-ai/dist/markit.js +267 -0
  363. package/vendor/markit-ai/dist/plugins/api.d.ts +7 -0
  364. package/vendor/markit-ai/dist/plugins/api.js +42 -0
  365. package/vendor/markit-ai/dist/plugins/index.d.ts +4 -0
  366. package/vendor/markit-ai/dist/plugins/index.js +3 -0
  367. package/vendor/markit-ai/dist/plugins/installer.d.ts +25 -0
  368. package/vendor/markit-ai/dist/plugins/installer.js +176 -0
  369. package/vendor/markit-ai/dist/plugins/loader.d.ts +6 -0
  370. package/vendor/markit-ai/dist/plugins/loader.js +61 -0
  371. package/vendor/markit-ai/dist/plugins/types.d.ts +25 -0
  372. package/vendor/markit-ai/dist/plugins/types.js +1 -0
  373. package/vendor/markit-ai/dist/providers/anthropic.d.ts +2 -0
  374. package/vendor/markit-ai/dist/providers/anthropic.js +47 -0
  375. package/vendor/markit-ai/dist/providers/index.d.ts +21 -0
  376. package/vendor/markit-ai/dist/providers/index.js +56 -0
  377. package/vendor/markit-ai/dist/providers/openai.d.ts +2 -0
  378. package/vendor/markit-ai/dist/providers/openai.js +76 -0
  379. package/vendor/markit-ai/dist/providers/types.d.ts +26 -0
  380. package/vendor/markit-ai/dist/providers/types.js +1 -0
  381. package/vendor/markit-ai/dist/types.d.ts +36 -0
  382. package/vendor/markit-ai/dist/types.js +1 -0
  383. package/vendor/markit-ai/dist/utils/exit-codes.d.ts +4 -0
  384. package/vendor/markit-ai/dist/utils/exit-codes.js +4 -0
  385. package/vendor/markit-ai/dist/utils/output.d.ts +22 -0
  386. package/vendor/markit-ai/dist/utils/output.js +31 -0
  387. package/vendor/markit-ai/dist/utils/turndown.d.ts +8 -0
  388. package/vendor/markit-ai/dist/utils/turndown.js +70 -0
  389. package/vendor/markit-ai/package.json +74 -0
  390. package/vendor/markit-ai.patch +49 -0
@@ -35,6 +35,7 @@ import {
35
35
  MCPPoolLeaseReleaseError,
36
36
  } from "./pool";
37
37
  import type { MCPProtocolObservation } from "./protocol";
38
+ import { DEFAULT_MCP_STARTUP_WAIT_MS, MAX_MCP_STARTUP_WAIT_MS, MCP_STARTUP_WAIT_GRACE_MS } from "./startup-policy";
38
39
  import type { MCPToolDetails } from "./tool-bridge";
39
40
  import { DeferredMCPTool, MCPTool } from "./tool-bridge";
40
41
  import type { MCPToolCache } from "./tool-cache";
@@ -51,7 +52,7 @@ import type {
51
52
  MCPServerConnection,
52
53
  MCPToolDefinition,
53
54
  } from "./types";
54
- import { MCPExpectedFailure, MCPNotificationMethods } from "./types";
55
+ import { MCPExpectedFailure, MCPHttpRequestError, MCPJsonRpcError, MCPNotificationMethods } from "./types";
55
56
 
56
57
  type ToolLoadResult = {
57
58
  connection: MCPServerConnection;
@@ -101,16 +102,23 @@ type RetiredLeaseRelease = {
101
102
  promise: Promise<void>;
102
103
  };
103
104
 
104
- const STARTUP_TIMEOUT_MS = 250;
105
- const STARTUP_TIMEOUT_GRACE_MS = 500;
105
+ const STARTUP_TIMEOUT_MS = DEFAULT_MCP_STARTUP_WAIT_MS;
106
+ const STARTUP_TIMEOUT_GRACE_MS = MCP_STARTUP_WAIT_GRACE_MS;
106
107
  /**
107
108
  * Default ceiling on how long `discoverAndConnect` waits for a server batch to
108
109
  * come up. Deliberately short: a config with a large `timeout` must not be able
109
110
  * to hang ordinary startup. ACP lifecycle launches carry their own, larger
110
111
  * budget derived from the readiness deadline (see `maxStartupTimeoutMs`).
111
112
  */
112
- const MAX_STARTUP_TIMEOUT_MS = 1_750;
113
+ const MAX_STARTUP_TIMEOUT_MS = MAX_MCP_STARTUP_WAIT_MS;
113
114
  const DEFAULT_EXACT_CONFIG_STARTUP_TIMEOUT_MS = 30_000;
115
+ const MCP_STARTUP_TIMEOUT_PREFIX = "MCP server connection timed out during startup:";
116
+
117
+ export function isMCPStartupTimeoutError(error: unknown): boolean {
118
+ return typeof error === "string"
119
+ ? error.startsWith(MCP_STARTUP_TIMEOUT_PREFIX)
120
+ : error instanceof Error && error.message.startsWith(MCP_STARTUP_TIMEOUT_PREFIX);
121
+ }
114
122
 
115
123
  export function resolveStartupTimeoutMs(configs: MCPServerConfig[], maxStartupTimeoutMs?: number): number {
116
124
  const ceiling =
@@ -150,6 +158,24 @@ export function withinDeclaredConnectionWindow(config: MCPServerConfig, elapsedM
150
158
  return elapsedMs < timeout;
151
159
  }
152
160
 
161
+ /** Keep startup logs bounded and free of remote response text. */
162
+ function classifyMCPStartupFailure(error: unknown): string {
163
+ if (error instanceof MCPExpectedFailure) {
164
+ return error.cause === undefined ? "expected-mcp-failure" : classifyMCPStartupFailure(error.cause);
165
+ }
166
+ if (error instanceof MCPHttpRequestError) {
167
+ return Number.isInteger(error.status) && error.status >= 100 && error.status <= 599
168
+ ? `http-status:${error.status}`
169
+ : "http-request-error";
170
+ }
171
+ if (error instanceof MCPJsonRpcError) {
172
+ return Number.isInteger(error.code) && Math.abs(error.code) <= 1_000_000
173
+ ? `json-rpc-code:${error.code}`
174
+ : "json-rpc-error";
175
+ }
176
+ return "transport-error";
177
+ }
178
+
153
179
  function trackPromise<T>(promise: Promise<T>): TrackedPromise<T> {
154
180
  const tracked: TrackedPromise<T> = { promise, status: "pending" };
155
181
  promise.then(
@@ -260,6 +286,22 @@ export interface MCPLoadResult {
260
286
  exaApiKeys: string[];
261
287
  }
262
288
 
289
+ export type MCPToolCatalogPublication = "unpublished" | "published" | "fenced";
290
+
291
+ /**
292
+ * Atomic view of the manager's fenced tool catalog and its publication state.
293
+ *
294
+ * `unpublished` is reserved for the initial deferred window where no manager
295
+ * catalog has been published and no exact control is fencing it. An empty
296
+ * `published` or `fenced` catalog is authoritative and must not be replaced
297
+ * with an in-flight discovery result.
298
+ */
299
+ export interface MCPToolCatalogSnapshot {
300
+ tools: CustomTool<TSchema, MCPToolDetails>[];
301
+ publication: MCPToolCatalogPublication;
302
+ generation: number;
303
+ }
304
+
263
305
  /** Options for discovering and connecting to MCP servers */
264
306
  export interface MCPDiscoverOptions {
265
307
  /** Whether to load project-level config (default: true) */
@@ -288,10 +330,10 @@ export interface MCPManagerOptions {
288
330
  /** Restrict this instance to tools from an explicit MCP config. */
289
331
  toolsOnly?: boolean;
290
332
  /**
291
- * Ceiling for the startup wait, in milliseconds. Only ACP lifecycle launches
292
- * set this, so a slow ACP MCP handshake gets the readiness budget while every
293
- * other consumer keeps the short default. Non-positive or non-finite values
294
- * are ignored and the default applies.
333
+ * Ceiling for the startup wait, in milliseconds. ACP lifecycle launches and
334
+ * plugin MCP startup use bounded caller-owned budgets; other consumers keep
335
+ * the short default. Non-positive or non-finite values are ignored and the
336
+ * default applies.
295
337
  */
296
338
  maxStartupTimeoutMs?: number;
297
339
  /** Connection pool used for every physical MCP open/close. */
@@ -300,12 +342,49 @@ export interface MCPManagerOptions {
300
342
  sessionId?: string;
301
343
  /** Idle retention for shared pool entries. */
302
344
  sharedPoolIdleMs?: number;
345
+ /** Limit tool-cache reads and writes to this manager's conventional servers. */
346
+ toolCacheServerNames?: ReadonlySet<string>;
303
347
  /** Test seam for deterministic reconnect backoff scheduling. */
304
348
  sleep?: (milliseconds: number, signal?: AbortSignal) => Promise<void>;
305
349
  /** Test seam for fencing the acquisition-to-registration replacement race. */
306
350
  afterLeaseAcquiredForTests?: (name: string, lease: MCPPoolLease) => void | Promise<void>;
307
351
  }
308
352
 
353
+ export type ExactMcpServerControlStatus =
354
+ | "suspended"
355
+ | "already-suspended"
356
+ | "resumed"
357
+ | "not-suspended"
358
+ | "reconnected"
359
+ | "unavailable"
360
+ | "unknown-server";
361
+
362
+ export interface ExactMcpServerControlResult {
363
+ readonly name: string;
364
+ readonly status: ExactMcpServerControlStatus;
365
+ readonly toolCount: number;
366
+ }
367
+
368
+ /**
369
+ * A staged catalog the caller can validate against before it is published.
370
+ * `commit` is the single publication point; `abort` releases anything the
371
+ * preparation opened and leaves the manager on its predecessor.
372
+ */
373
+ export interface PreparedExactMcpCatalog {
374
+ readonly tools: CustomTool<TSchema, MCPToolDetails>[];
375
+ canCommit(): boolean;
376
+ commit(): Promise<void>;
377
+ abort(): Promise<void>;
378
+ }
379
+
380
+ export interface PreparedExactMcpServerControl extends PreparedExactMcpCatalog {
381
+ readonly result: ExactMcpServerControlResult;
382
+ /** Validate a resumed replacement before an atomic multi-server publication. */
383
+ precommit?(): void;
384
+ /** Publish after every replacement in the catalog has passed precommit. */
385
+ publish?(): void;
386
+ }
387
+
309
388
  /**
310
389
  * MCP Server Manager.
311
390
  *
@@ -342,6 +421,8 @@ export class MCPManager {
342
421
  readonly #retiredLeaseReleases = new Set<RetiredLeaseRelease>();
343
422
  #nextScopedOperationId = 1;
344
423
  #tools: CustomTool<TSchema, MCPToolDetails>[] = [];
424
+ #toolCatalogGeneration = 0;
425
+ #toolCatalogPublished = false;
345
426
  #pendingConnections = new Map<string, Promise<MCPServerConnection>>();
346
427
  #pendingConnectionControllers = new Map<string, AbortController>();
347
428
  #pendingToolLoads = new Map<string, Promise<ToolLoadResult>>();
@@ -351,7 +432,8 @@ export class MCPManager {
351
432
  #authStorage: AuthStorage | null = null;
352
433
  #inputRequestHandler: MCPInputRequestHandler | null = null;
353
434
  #onNotification?: (serverName: string, method: string, params: unknown) => void;
354
- #onToolsChanged?: (tools: CustomTool<TSchema, MCPToolDetails>[]) => void;
435
+ #onToolsChanged?: (tools: readonly Readonly<CustomTool<TSchema, MCPToolDetails>>[]) => void;
436
+ readonly #toolChangeListeners = new Set<(tools: readonly Readonly<CustomTool<TSchema, MCPToolDetails>>[]) => void>();
355
437
  #onResourcesChanged?: (serverName: string, uri: string) => void;
356
438
  #onPromptsChanged?: (serverName: string) => void;
357
439
  #notificationsEnabled = false;
@@ -365,18 +447,33 @@ export class MCPManager {
365
447
  #reconnectBackoffs = new Map<string, AbortController>();
366
448
  /** Preserved configs for reconnection after connection loss. */
367
449
  #serverConfigs = new Map<string, MCPServerConfig>();
450
+ // Exact MCP replacements retain cleanup ownership and cancel in-flight candidates.
451
+ /** Session-local operator suppression; never persisted to MCP config. */
452
+ #suppressedServers = new Set<string>();
453
+ /** Prepare-phase fence that prevents a concurrent close/rebind publication. */
454
+ #pendingExactSuspensions = new Set<string>();
455
+ /** Single per-server candidate axis shared against automatic reconnect. */
456
+ #pendingExactControlServers = new Set<string>();
368
457
  /** Monotonic epoch incremented on disconnectAll to invalidate stale reconnections. */
369
458
  #epoch = 0;
370
459
  #scopedLifecycle: "open" | "disconnecting" | "reconnecting" = "open";
371
460
  #scopedLifecycleEpoch = 0;
372
461
  readonly #toolsOnly: boolean;
462
+ #toolCacheServerNames: Set<string> | undefined;
373
463
  #toolsOnlyConfigLoaded = false;
464
+ #connectionSetMutationBlocked = false;
374
465
  #connectionSetSealed = false;
375
466
 
376
467
  #serverError(message: string): string {
377
468
  return this.#toolsOnly ? "MCP server unavailable" : message;
378
469
  }
379
470
 
471
+ #shouldCacheServerTools(name: string): boolean {
472
+ return (
473
+ this.toolCache !== null && (this.#toolCacheServerNames === undefined || this.#toolCacheServerNames.has(name))
474
+ );
475
+ }
476
+
380
477
  #removePendingAcquireCleanup(name: string, error: MCPPoolAcquireAbortError): void {
381
478
  const pending = this.#pendingAcquireCleanups.get(name);
382
479
  pending?.delete(error);
@@ -480,13 +577,27 @@ export class MCPManager {
480
577
  return this.#scopedLifecycleEpoch;
481
578
  }
482
579
  #assertConnectionSetMutable(): void {
483
- if (this.#connectionSetSealed) throw new Error("MCP manager connection set is sealed");
580
+ if (this.#connectionSetMutationBlocked) {
581
+ throw new Error(
582
+ this.#connectionSetSealed ? "MCP manager connection set is sealed" : "MCP manager connection set is frozen",
583
+ );
584
+ }
585
+ }
586
+
587
+ /** Block config mutations and reloads while keeping existing servers reconnectable. */
588
+ freezeConnectionSet(): void {
589
+ this.#connectionSetMutationBlocked = true;
484
590
  }
485
591
 
486
592
  sealConnectionSet(): void {
593
+ this.freezeConnectionSet();
487
594
  this.#connectionSetSealed = true;
488
595
  }
489
596
 
597
+ isConnectionSetMutationBlocked(): boolean {
598
+ return this.#connectionSetMutationBlocked;
599
+ }
600
+
490
601
  isConnectionSetSealed(): boolean {
491
602
  return this.#connectionSetSealed;
492
603
  }
@@ -539,7 +650,11 @@ export class MCPManager {
539
650
  }
540
651
 
541
652
  #scheduleSharedRebind(name: string, lease: MCPPoolLease): void {
542
- if (this.#scopedLifecycle === "disconnecting") return;
653
+ if (this.#pendingExactSuspensions.has(name) || this.#pendingExactControlServers.has(name)) {
654
+ this.#queueDeferredSharedRebind(name, lease, this.#epoch);
655
+ return;
656
+ }
657
+ if (this.#scopedLifecycle === "disconnecting" || this.#suppressedServers.has(name)) return;
543
658
  const active = this.#activeSharedRebinds.get(name);
544
659
  if (active) {
545
660
  if (active.lease !== lease) this.#queueDeferredSharedRebind(name, lease, this.#epoch);
@@ -562,6 +677,15 @@ export class MCPManager {
562
677
  #drainDeferredSharedRebinds(): void {
563
678
  if (this.#scopedLifecycle !== "open" || this.#deferredSharedRebinds.size === 0) return;
564
679
  for (const [name, pending] of this.#deferredSharedRebinds.entries()) {
680
+ // A suspended or in-flight control still owns this server. Keep the newest
681
+ // replacement queued so the manager rebinds after suppression clears instead
682
+ // of staying bound to a retired pool generation.
683
+ if (
684
+ this.#suppressedServers.has(name) ||
685
+ this.#pendingExactSuspensions.has(name) ||
686
+ this.#pendingExactControlServers.has(name)
687
+ )
688
+ continue;
565
689
  if (pending.managerEpoch !== this.#epoch) {
566
690
  this.#deferredSharedRebinds.delete(name);
567
691
  continue;
@@ -614,7 +738,14 @@ export class MCPManager {
614
738
  this.#handleServerNotification(name, event.method, event.params);
615
739
  return;
616
740
  }
617
- if (event.type !== "close" || this.#connectionSetSealed) return;
741
+ if (
742
+ event.type !== "close" ||
743
+ this.#connectionSetSealed ||
744
+ this.#suppressedServers.has(name) ||
745
+ this.#pendingExactSuspensions.has(name) ||
746
+ this.#pendingExactControlServers.has(name)
747
+ )
748
+ return;
618
749
  void this.reconnectServer(name);
619
750
  }),
620
751
  );
@@ -622,7 +753,12 @@ export class MCPManager {
622
753
 
623
754
  async #rebindAfterSharedRestart(name: string, lease: MCPPoolLease): Promise<void> {
624
755
  const managerEpoch = this.#epoch;
625
- if (this.#scopedLifecycle === "disconnecting") return;
756
+ if (
757
+ this.#scopedLifecycle === "disconnecting" ||
758
+ this.#pendingExactSuspensions.has(name) ||
759
+ this.#pendingExactControlServers.has(name)
760
+ )
761
+ return;
626
762
  if (this.#scopedLifecycle === "reconnecting") {
627
763
  this.#queueDeferredSharedRebind(name, lease, managerEpoch);
628
764
  return;
@@ -858,6 +994,7 @@ export class MCPManager {
858
994
  options: MCPManagerOptions = {},
859
995
  ) {
860
996
  this.#toolsOnly = options.toolsOnly === true;
997
+ this.#toolCacheServerNames = options.toolCacheServerNames ? new Set(options.toolCacheServerNames) : undefined;
861
998
  this.#maxStartupTimeoutMs = options.maxStartupTimeoutMs;
862
999
  this.#sleep = options.sleep ?? delay;
863
1000
  this.#afterLeaseAcquiredForTests = options.afterLeaseAcquiredForTests;
@@ -879,13 +1016,46 @@ export class MCPManager {
879
1016
  }
880
1017
 
881
1018
  /**
882
- * Set a callback to fire when any server's tools change.
1019
+ * Set a callback to receive a read-only catalog snapshot when server tools change.
883
1020
  */
884
- setOnToolsChanged(handler: (tools: CustomTool<TSchema, MCPToolDetails>[]) => void): void {
1021
+ setOnToolsChanged(handler: (tools: readonly Readonly<CustomTool<TSchema, MCPToolDetails>>[]) => void): void {
885
1022
  if (this.#toolsOnly) return;
886
1023
  this.#onToolsChanged = handler;
887
1024
  }
888
1025
 
1026
+ /** Subscribe to readonly catalog snapshots without replacing the manager owner's callback. */
1027
+ subscribeToToolsChanged(
1028
+ handler: (tools: readonly Readonly<CustomTool<TSchema, MCPToolDetails>>[]) => void,
1029
+ ): () => void {
1030
+ if (this.#toolsOnly) return () => {};
1031
+ this.#toolChangeListeners.add(handler);
1032
+ return () => {
1033
+ this.#toolChangeListeners.delete(handler);
1034
+ };
1035
+ }
1036
+
1037
+ #notifyToolsChanged(): void {
1038
+ const listeners = [...this.#toolChangeListeners];
1039
+ // Keep live MCPTool instances for reconnect behavior, but never expose the
1040
+ // manager-owned array; callback types make their tool descriptors readonly.
1041
+ const snapshot = this.#onToolsChanged || listeners.length > 0 ? Object.freeze([...this.#tools]) : undefined;
1042
+ if (snapshot && this.#onToolsChanged) {
1043
+ try {
1044
+ this.#onToolsChanged(snapshot);
1045
+ } catch (error) {
1046
+ logger.warn("MCP tool catalog owner callback failed", { error: classifyMCPStartupFailure(error) });
1047
+ }
1048
+ }
1049
+ if (!snapshot) return;
1050
+ for (const listener of listeners) {
1051
+ try {
1052
+ listener(snapshot);
1053
+ } catch (error) {
1054
+ logger.warn("MCP tool catalog listener failed", { error: classifyMCPStartupFailure(error) });
1055
+ }
1056
+ }
1057
+ }
1058
+
889
1059
  /**
890
1060
  * Set a callback to fire when any server's resources change.
891
1061
  */
@@ -1014,6 +1184,11 @@ export class MCPManager {
1014
1184
  nativeOnly: options?.nativeOnly,
1015
1185
  configPath: options?.configPath,
1016
1186
  });
1187
+ if (this.#toolCacheServerNames !== undefined) {
1188
+ this.#toolCacheServerNames = new Set(
1189
+ Object.keys(configs).filter(name => sources[name]?.provider !== "gjc-plugins"),
1190
+ );
1191
+ }
1017
1192
  const result = await this.#connectServers(configs, sources, options?.onConnecting);
1018
1193
  if (configurationWarning) result.errors.set("$config", "MCP configuration unavailable");
1019
1194
  result.exaApiKeys = exaApiKeys;
@@ -1067,6 +1242,13 @@ export class MCPManager {
1067
1242
  "MCP connection cleanup pending";
1068
1243
  errors.set(name, this.#serverError(message));
1069
1244
  reportedErrors.add(name);
1245
+ if (!this.#toolsOnly) {
1246
+ logger.warn("Skipping MCP autoload registration", {
1247
+ path: `mcp:${name}`,
1248
+ serverName: name,
1249
+ reason: message,
1250
+ });
1251
+ }
1070
1252
  continue;
1071
1253
  }
1072
1254
  if (sources[name]) {
@@ -1080,11 +1262,14 @@ export class MCPManager {
1080
1262
  // Skip if already connected.
1081
1263
  if (this.#connections.has(name)) {
1082
1264
  connectedServers.add(name);
1083
- allTools.push(
1084
- ...this.#tools.filter(
1085
- tool => (tool instanceof MCPTool || tool instanceof DeferredMCPTool) && tool.mcpServerName === name,
1086
- ),
1087
- );
1265
+ if (this.#isExactToolPublicationAllowed(name)) {
1266
+ allTools.push(
1267
+ ...this.#tools.filter(
1268
+ tool =>
1269
+ (tool instanceof MCPTool || tool instanceof DeferredMCPTool) && tool.mcpServerName === name,
1270
+ ),
1271
+ );
1272
+ }
1088
1273
  continue;
1089
1274
  }
1090
1275
 
@@ -1093,14 +1278,29 @@ export class MCPManager {
1093
1278
  this.#pendingToolLoads.has(name) ||
1094
1279
  this.#pendingReconnections.has(name)
1095
1280
  ) {
1281
+ if (!this.#toolsOnly) {
1282
+ logger.warn("Skipping MCP autoload registration", {
1283
+ path: `mcp:${name}`,
1284
+ serverName: name,
1285
+ reason: "connection already pending",
1286
+ });
1287
+ }
1096
1288
  continue;
1097
1289
  }
1098
1290
 
1099
1291
  // Validate config
1100
1292
  const validationErrors = validateServerConfig(name, config);
1101
1293
  if (validationErrors.length > 0) {
1102
- errors.set(name, this.#serverError(validationErrors.join("; ")));
1294
+ const reason = validationErrors.join("; ");
1295
+ errors.set(name, this.#serverError(reason));
1103
1296
  reportedErrors.add(name);
1297
+ if (!this.#toolsOnly) {
1298
+ logger.warn("Skipping MCP autoload registration", {
1299
+ path: `mcp:${name}`,
1300
+ serverName: name,
1301
+ reason,
1302
+ });
1303
+ }
1104
1304
  continue;
1105
1305
  }
1106
1306
 
@@ -1237,22 +1437,31 @@ export class MCPManager {
1237
1437
  return;
1238
1438
  this.#pendingToolLoads.delete(name);
1239
1439
  const reconnect = () => this.reconnectServer(name);
1240
- const customTools = MCPTool.fromTools(this.#connectionForLease(connection), serverTools, reconnect, {
1241
- noReplay: config.sharing === "shared",
1242
- inputHandler: () => this.#inputRequestHandler ?? undefined,
1243
- });
1440
+ const customTools = MCPTool.fromTools(
1441
+ this.#connectionForLease(connection),
1442
+ serverTools,
1443
+ reconnect,
1444
+ this.#exactToolOptions(name, config.sharing === "shared"),
1445
+ );
1244
1446
  this.#replaceServerTools(name, customTools);
1245
- if (!this.#toolsOnly) this.#onToolsChanged?.(this.#tools);
1246
- if (!this.#toolsOnly) void this.toolCache?.set(name, config, serverTools);
1447
+ if (!this.#toolsOnly) this.#notifyToolsChanged();
1448
+ if (!this.#toolsOnly && this.#shouldCacheServerTools(name))
1449
+ void this.toolCache?.set(name, config, serverTools);
1247
1450
  if (!this.#toolsOnly) await this.#loadServerResourcesAndPrompts(name, connection);
1248
1451
  })
1249
1452
  .catch(error => {
1250
1453
  this.#retainConnectionCleanupFailure(name, error);
1251
1454
  if (this.#pendingToolLoads.get(name) !== toolsPromise) return;
1252
1455
  this.#pendingToolLoads.delete(name);
1253
- if (!allowBackgroundLogging || reportedErrors.has(name) || this.#toolsOnly) return;
1456
+ if (!allowBackgroundLogging || this.#toolsOnly || reportedErrors.has(name)) return;
1254
1457
  const message = error instanceof Error ? error.message : String(error);
1255
- logger.error("MCP tool load failed", { path: `mcp:${name}`, error: message });
1458
+ errors.set(name, this.#serverError(message));
1459
+ reportedErrors.add(name);
1460
+ logger.error("MCP tool load failed", {
1461
+ path: `mcp:${name}`,
1462
+ serverName: name,
1463
+ error: classifyMCPStartupFailure(error),
1464
+ });
1256
1465
  });
1257
1466
  }
1258
1467
 
@@ -1308,6 +1517,7 @@ export class MCPManager {
1308
1517
  if (this.toolCache && !this.#toolsOnly) {
1309
1518
  await Promise.all(
1310
1519
  pendingTasks.map(async task => {
1520
+ if (!this.#shouldCacheServerTools(task.name)) return;
1311
1521
  const cached = await this.toolCache?.get(task.name, task.config);
1312
1522
  if (cached) {
1313
1523
  cachedTools.set(task.name, cached);
@@ -1316,41 +1526,55 @@ export class MCPManager {
1316
1526
  );
1317
1527
  }
1318
1528
 
1319
- const pendingWithoutCache = pendingTasks.filter(task => !cachedTools.has(task.name));
1320
- if (pendingWithoutCache.length > 0) {
1321
- // The startup wait elapsing means "stop blocking session start", not
1322
- // "this server failed". A server whose operator declared a `timeout`
1323
- // (`gjc mcp add --timeout`) asked to wait that long for it, so while it
1324
- // is still inside that window it keeps connecting in the background
1325
- // under `connectToServer`'s own timeout, and the background tool load
1326
- // adopts it. Only a server that declared no window, or already spent it,
1327
- // is torn down and reported. Exact-config (`toolsOnly`) startup still
1328
- // fails fast: it builds a catalog once, so a missing server is an error.
1329
- const startupElapsedMs = Date.now() - startupStartedAt;
1330
- for (const task of pendingWithoutCache) {
1331
- if (!this.#toolsOnly && withinDeclaredConnectionWindow(task.config, startupElapsedMs)) {
1332
- logger.warn("MCP server still connecting after the startup wait", {
1333
- path: `mcp:${task.name}`,
1334
- startupWaitMs: startupTimeoutMs,
1335
- declaredTimeoutMs: task.config.timeout,
1336
- });
1337
- continue;
1338
- }
1339
- const message = `MCP server connection timed out during startup: ${task.name}`;
1340
- errors.set(task.name, this.#serverError(message));
1341
- reportedErrors.add(task.name);
1342
- task.connectionAbort.abort(new Error(message));
1343
- if (this.#pendingConnections.has(task.name)) this.#pendingConnections.delete(task.name);
1344
- if (this.#pendingToolLoads.get(task.name) === task.toolsPromise)
1345
- this.#pendingToolLoads.delete(task.name);
1346
- this.#pendingConnectionControllers.delete(task.name);
1347
- void this.#disconnectServer(task.name).catch(error => {
1348
- this.#logLeaseReleaseFailure(task.name, undefined, error);
1529
+ // Cached tools are only a fallback snapshot; they do not exempt the live
1530
+ // connection from the startup cleanup contract. The startup wait elapsing
1531
+ // means "stop blocking session start", not "this server failed". A server
1532
+ // whose operator declared a `timeout` (`gjc mcp add --timeout`) asked to
1533
+ // wait that long for it, so while it is still inside that window it keeps
1534
+ // connecting in the background under `connectToServer`'s own timeout, and
1535
+ // the background tool load adopts it. Only a server that declared no
1536
+ // window, or already spent it, is torn down and reported. Exact-config
1537
+ // (`toolsOnly`) startup still fails fast: it builds a catalog once, so a
1538
+ // missing server is an error.
1539
+ // Abort and disconnect in the background: a misbehaving stdio/MCP transport can
1540
+ // ignore AbortSignal and keep startup blocked indefinitely, but it must not remain
1541
+ // registered if it eventually connects.
1542
+ const startupElapsedMs = Date.now() - startupStartedAt;
1543
+ for (const task of pendingTasks) {
1544
+ if (task.tracked.status !== "pending") continue;
1545
+ if (!this.#toolsOnly && withinDeclaredConnectionWindow(task.config, startupElapsedMs)) {
1546
+ logger.warn("MCP server still connecting after the startup wait", {
1547
+ path: `mcp:${task.name}`,
1548
+ serverName: task.name,
1549
+ startupWaitMs: startupTimeoutMs,
1550
+ declaredTimeoutMs: task.config.timeout,
1551
+ });
1552
+ continue;
1553
+ }
1554
+ const message = `MCP server connection timed out during startup: ${task.name}`;
1555
+ if (!this.#toolsOnly) {
1556
+ logger.warn("MCP server connection timed out during startup", {
1557
+ path: `mcp:${task.name}`,
1558
+ serverName: task.name,
1559
+ startupWaitMs: startupTimeoutMs,
1560
+ declaredTimeoutMs: task.config.timeout,
1561
+ remediation: "Set a per-server timeout with `gjc mcp add --timeout <ms>` for slower servers.",
1349
1562
  });
1350
1563
  }
1351
- // Abort and disconnect in the background: a misbehaving stdio/MCP transport can
1352
- // ignore AbortSignal and keep startup blocked indefinitely, but it must not remain
1353
- // registered if it eventually connects.
1564
+ errors.set(task.name, this.#serverError(message));
1565
+ reportedErrors.add(task.name);
1566
+ task.connectionAbort.abort(new Error(message));
1567
+ if (this.#pendingConnections.has(task.name)) this.#pendingConnections.delete(task.name);
1568
+ if (this.#pendingToolLoads.get(task.name) === task.toolsPromise)
1569
+ this.#pendingToolLoads.delete(task.name);
1570
+ this.#pendingConnectionControllers.delete(task.name);
1571
+ void this.#disconnectServer(task.name, {
1572
+ preserveConfig: this.#toolsOnly || cachedTools.has(task.name),
1573
+ preserveSource: cachedTools.has(task.name),
1574
+ preserveTools: cachedTools.has(task.name),
1575
+ }).catch(error => {
1576
+ this.#logLeaseReleaseFailure(task.name, undefined, error);
1577
+ });
1354
1578
  }
1355
1579
  }
1356
1580
 
@@ -1368,26 +1592,39 @@ export class MCPManager {
1368
1592
  continue;
1369
1593
  }
1370
1594
  connectedServers.add(name);
1371
- const reconnect = () => this.reconnectServer(name);
1372
- try {
1373
- allTools.push(
1374
- ...MCPTool.fromTools(this.#connectionForLease(connection), serverTools, reconnect, {
1375
- noReplay: task.config.sharing === "shared",
1376
- inputHandler: () => this.#inputRequestHandler ?? undefined,
1377
- }),
1378
- );
1379
- } catch (error) {
1380
- await this.#cleanupConnectionTasks(connectionTasks);
1381
- throw error;
1595
+ if (this.#isExactToolPublicationAllowed(name)) {
1596
+ const reconnect = () => this.reconnectServer(name);
1597
+ try {
1598
+ allTools.push(
1599
+ ...MCPTool.fromTools(
1600
+ this.#connectionForLease(connection),
1601
+ serverTools,
1602
+ reconnect,
1603
+ this.#exactToolOptions(name, task.config.sharing === "shared"),
1604
+ ),
1605
+ );
1606
+ } catch (error) {
1607
+ await this.#cleanupConnectionTasks(connectionTasks);
1608
+ throw error;
1609
+ }
1382
1610
  }
1383
1611
  } else if (task.tracked.status === "rejected") {
1384
1612
  const reason = task.tracked.reason;
1385
1613
  this.#retainConnectionCleanupFailure(name, reason);
1386
1614
  const message = reason instanceof Error ? reason.message : String(reason);
1615
+ if (!(this.#toolsOnly && reason instanceof MCPExpectedFailure)) {
1616
+ logger.warn("MCP server connection failed during startup", {
1617
+ path: `mcp:${name}`,
1618
+ serverName: name,
1619
+ error: classifyMCPStartupFailure(reason),
1620
+ remediation:
1621
+ "Check the server command and set --timeout <ms> when startup is expected to be slow.",
1622
+ });
1623
+ }
1387
1624
  errors.set(name, this.#serverError(message));
1388
1625
  reportedErrors.add(name);
1389
1626
  if (this.#toolsOnly && reason instanceof MCPExpectedFailure) {
1390
- await this.#disconnectServer(name).catch(error => {
1627
+ await this.#disconnectServer(name, { preserveConfig: true }).catch(error => {
1391
1628
  this.#logLeaseReleaseFailure(name, this.#connections.get(name), error);
1392
1629
  });
1393
1630
  }
@@ -1396,7 +1633,7 @@ export class MCPManager {
1396
1633
  }
1397
1634
  } else {
1398
1635
  const cached = cachedTools.get(name);
1399
- if (cached) {
1636
+ if (cached && this.#isExactToolPublicationAllowed(name)) {
1400
1637
  const source = this.#sources.get(name);
1401
1638
  const reconnect = () => this.reconnectServer(name);
1402
1639
  try {
@@ -1407,10 +1644,7 @@ export class MCPManager {
1407
1644
  () => this.#waitForConnection(name).then(connection => this.#connectionForLease(connection)),
1408
1645
  source,
1409
1646
  reconnect,
1410
- {
1411
- noReplay: task.config.sharing === "shared",
1412
- inputHandler: () => this.#inputRequestHandler ?? undefined,
1413
- },
1647
+ this.#exactToolOptions(name, task.config.sharing === "shared"),
1414
1648
  ),
1415
1649
  );
1416
1650
  } catch (error) {
@@ -1431,7 +1665,10 @@ export class MCPManager {
1431
1665
  }
1432
1666
 
1433
1667
  // Update cached tools
1434
- if (shouldPublishToolSnapshot) this.#tools = allTools;
1668
+ if (shouldPublishToolSnapshot) {
1669
+ this.#publishToolCatalog(allTools);
1670
+ this.#notifyToolsChanged();
1671
+ }
1435
1672
  allowBackgroundLogging = true;
1436
1673
 
1437
1674
  return {
@@ -1443,13 +1680,25 @@ export class MCPManager {
1443
1680
  }
1444
1681
 
1445
1682
  #replaceServerTools(name: string, tools: CustomTool<TSchema, MCPToolDetails>[]): void {
1446
- this.#tools = this.#tools.filter(
1683
+ if (!this.#isExactToolPublicationAllowed(name)) return;
1684
+ const publishedTools = this.#tools.filter(
1447
1685
  tool => !((tool instanceof MCPTool || tool instanceof DeferredMCPTool) && tool.mcpServerName === name),
1448
1686
  );
1449
- this.#tools.push(...tools);
1687
+ if (!this.#suppressedServers.has(name)) publishedTools.push(...tools);
1450
1688
  // Stable sort by name so reconnect order does not perturb the array.
1451
1689
  // See `sortMCPToolsByName` for the cache-stability rationale.
1452
- sortMCPToolsByName(this.#tools);
1690
+ sortMCPToolsByName(publishedTools);
1691
+ this.#publishToolCatalog(publishedTools);
1692
+ }
1693
+
1694
+ #exactToolOptions(
1695
+ name: string,
1696
+ noReplay: boolean,
1697
+ ): { noReplay: boolean; isAvailable?: () => boolean; inputHandler: () => MCPInputRequestHandler | undefined } {
1698
+ const inputHandler = () => this.#inputRequestHandler ?? undefined;
1699
+ return this.#toolsOnly
1700
+ ? { noReplay, isAvailable: () => !this.#suppressedServers.has(name), inputHandler }
1701
+ : { noReplay, inputHandler };
1453
1702
  }
1454
1703
 
1455
1704
  #triggerNotificationRefresh(serverName: string, kind: "tools" | "resources" | "prompts"): void {
@@ -1525,7 +1774,463 @@ export class MCPManager {
1525
1774
  * Get all loaded tools.
1526
1775
  */
1527
1776
  getTools(): CustomTool<TSchema, MCPToolDetails>[] {
1528
- return [...this.#tools];
1777
+ return this.#tools.filter(tool => !tool.mcpServerName || this.#isExactToolPublicationAllowed(tool.mcpServerName));
1778
+ }
1779
+
1780
+ /**
1781
+ * Read the fenced catalog and publication state atomically.
1782
+ *
1783
+ * An empty catalog is still authoritative after publication or while an
1784
+ * exact control is in flight. Deferred startup may fall back to its discovery
1785
+ * result only for the explicit `unpublished` state.
1786
+ */
1787
+ getToolCatalogSnapshot(): MCPToolCatalogSnapshot {
1788
+ const exactControlFence =
1789
+ this.#toolsOnly &&
1790
+ (this.#suppressedServers.size > 0 ||
1791
+ this.#pendingExactSuspensions.size > 0 ||
1792
+ this.#pendingExactControlServers.size > 0);
1793
+ return {
1794
+ tools: this.getTools(),
1795
+ publication: exactControlFence ? "fenced" : this.#toolCatalogPublished ? "published" : "unpublished",
1796
+ generation: this.#toolCatalogGeneration,
1797
+ };
1798
+ }
1799
+
1800
+ #publishToolCatalog(tools: CustomTool<TSchema, MCPToolDetails>[]): void {
1801
+ this.#tools = tools;
1802
+ this.#toolCatalogGeneration++;
1803
+ this.#toolCatalogPublished = true;
1804
+ }
1805
+
1806
+ #isExactToolPublicationAllowed(name: string): boolean {
1807
+ return (
1808
+ !this.#toolsOnly ||
1809
+ (!this.#suppressedServers.has(name) &&
1810
+ !this.#pendingExactSuspensions.has(name) &&
1811
+ !this.#pendingExactControlServers.has(name))
1812
+ );
1813
+ }
1814
+
1815
+ isExactServerSuppressed(name: string): boolean {
1816
+ this.#assertExactServerControl();
1817
+ return this.#suppressedServers.has(name);
1818
+ }
1819
+
1820
+ #exactServerToolCount(name: string): number {
1821
+ return this.#tools.reduce((count, tool) => (tool.mcpServerName === name ? count + 1 : count), 0);
1822
+ }
1823
+
1824
+ #assertExactServerControl(): void {
1825
+ if (!this.#toolsOnly) throw new Error("Exact MCP server controls require a tools-only manager");
1826
+ }
1827
+
1828
+ /**
1829
+ * Prepare one exact-config control without publishing manager state. Session
1830
+ * registry and prompt validation run against `tools`; commit is the single
1831
+ * publication point and abort releases only an unpublished candidate.
1832
+ */
1833
+ async prepareExactServerControl(
1834
+ action: "suspend" | "resume" | "reconnect",
1835
+ name: string,
1836
+ ): Promise<PreparedExactMcpServerControl> {
1837
+ this.#assertExactServerControl();
1838
+ if (!this.#serverConfigs.has(name) && !this.#connections.has(name)) {
1839
+ return this.#settledExactControl({ name, status: "unknown-server", toolCount: 0 });
1840
+ }
1841
+ if (action === "suspend") {
1842
+ if (this.#suppressedServers.has(name)) {
1843
+ return this.#settledExactControl({ name, status: "already-suspended", toolCount: 0 });
1844
+ }
1845
+ this.#pendingExactSuspensions.add(name);
1846
+ await this.#activeSharedRebinds.get(name)?.promise;
1847
+ const tools = this.#tools.filter(
1848
+ tool =>
1849
+ tool.mcpServerName !== name && (!tool.mcpServerName || !this.#suppressedServers.has(tool.mcpServerName)),
1850
+ );
1851
+ return {
1852
+ result: { name, status: "suspended", toolCount: 0 },
1853
+ tools,
1854
+ canCommit: () => true,
1855
+ commit: async () => {
1856
+ this.#suppressedServers.add(name);
1857
+ this.#pendingExactSuspensions.delete(name);
1858
+ this.#reconnectBackoffs.get(name)?.abort(new Error(`MCP server suspended: ${name}`));
1859
+ for (const tool of this.#tools) {
1860
+ if (tool.mcpServerName === name) (tool as MCPTool | DeferredMCPTool).invalidateForSessionControl();
1861
+ }
1862
+ await this.#activeSharedRebinds.get(name)?.promise;
1863
+ },
1864
+ abort: async () => {
1865
+ this.#pendingExactSuspensions.delete(name);
1866
+ },
1867
+ };
1868
+ }
1869
+ if (action === "resume" && !this.#suppressedServers.has(name)) {
1870
+ return this.#settledExactControl({
1871
+ name,
1872
+ status: "not-suspended",
1873
+ toolCount: this.#exactServerToolCount(name),
1874
+ });
1875
+ }
1876
+ if (action === "reconnect" && this.#suppressedServers.has(name)) {
1877
+ return this.#settledExactControl({ name, status: "unavailable", toolCount: 0 });
1878
+ }
1879
+ const automaticReconnect = this.#pendingReconnections.get(name);
1880
+ if (automaticReconnect) {
1881
+ this.#pendingExactControlServers.add(name);
1882
+ let connection: MCPServerConnection | null;
1883
+ try {
1884
+ connection = await automaticReconnect;
1885
+ } catch (error) {
1886
+ this.#pendingExactControlServers.delete(name);
1887
+ this.#drainDeferredSharedRebinds();
1888
+ throw error;
1889
+ }
1890
+ if (!connection) {
1891
+ this.#pendingExactControlServers.delete(name);
1892
+ this.#drainDeferredSharedRebinds();
1893
+ return this.#settledExactControl({ name, status: "unavailable", toolCount: 0 });
1894
+ }
1895
+ let candidateTools: CustomTool<TSchema, MCPToolDetails>[];
1896
+ try {
1897
+ candidateTools = await this.#buildExactServerTools(name, connection);
1898
+ } catch (error) {
1899
+ this.#pendingExactControlServers.delete(name);
1900
+ this.#drainDeferredSharedRebinds();
1901
+ throw error;
1902
+ }
1903
+ return this.#preparedCatalogCommit(
1904
+ name,
1905
+ action === "resume" ? "resumed" : "reconnected",
1906
+ candidateTools,
1907
+ undefined,
1908
+ connection,
1909
+ );
1910
+ }
1911
+ this.#pendingExactControlServers.add(name);
1912
+ const existing = this.#connections.get(name);
1913
+ if (action === "resume" && existing?.transport.connected) {
1914
+ try {
1915
+ const candidateTools = await this.#buildExactServerTools(name, existing);
1916
+ return this.#preparedCatalogCommit(name, "resumed", candidateTools, undefined, existing);
1917
+ } catch {
1918
+ // A mapped transport can close while suppressed; recover with an
1919
+ // unpublished physical candidate instead of exposing stale state.
1920
+ }
1921
+ }
1922
+ try {
1923
+ const candidate = await this.#prepareExactReplacement(name);
1924
+ return this.#preparedCatalogCommit(
1925
+ name,
1926
+ action === "resume" ? "resumed" : "reconnected",
1927
+ candidate.tools,
1928
+ candidate,
1929
+ undefined,
1930
+ );
1931
+ } catch (error) {
1932
+ const cleanupFailure = this.#retainConnectionCleanupFailure(name, error);
1933
+ if (cleanupFailure) {
1934
+ logger.error("MCP exact replacement cleanup failed", {
1935
+ path: `mcp:${name}`,
1936
+ error: cleanupFailure,
1937
+ });
1938
+ }
1939
+ this.#pendingExactControlServers.delete(name);
1940
+ this.#drainDeferredSharedRebinds();
1941
+ return this.#settledExactControl({ name, status: "unavailable", toolCount: 0 });
1942
+ }
1943
+ }
1944
+
1945
+ /** Clear all session-local suppression before this owner adopts a new identity. */
1946
+ async prepareExactServerControlReset(): Promise<PreparedExactMcpCatalog> {
1947
+ this.#assertExactServerControl();
1948
+ const prepared: PreparedExactMcpServerControl[] = [];
1949
+ for (const name of [...this.#suppressedServers]) {
1950
+ try {
1951
+ const candidate = await this.prepareExactServerControl("resume", name);
1952
+ if (candidate.result.status !== "resumed") {
1953
+ logger.warn("MCP server did not resume during identity reset", {
1954
+ path: `mcp:${name}`,
1955
+ status: candidate.result.status,
1956
+ });
1957
+ await candidate.abort();
1958
+ continue;
1959
+ }
1960
+ prepared.push(candidate);
1961
+ } catch (error) {
1962
+ // A suspended server's reachability must not block identity adoption.
1963
+ logger.warn("MCP server resume failed during identity reset", { path: `mcp:${name}`, error });
1964
+ }
1965
+ }
1966
+ const resumedNames = new Set(prepared.map(candidate => candidate.result.name));
1967
+ const carriedOver = this.#tools.filter(
1968
+ tool =>
1969
+ !tool.mcpServerName ||
1970
+ (!this.#suppressedServers.has(tool.mcpServerName) && !resumedNames.has(tool.mcpServerName)),
1971
+ );
1972
+ const resumedTools = prepared.flatMap(candidate =>
1973
+ candidate.tools.filter(tool => tool.mcpServerName === candidate.result.name),
1974
+ );
1975
+ const tools = [...carriedOver, ...resumedTools];
1976
+ sortMCPToolsByName(tools);
1977
+ return {
1978
+ tools,
1979
+ canCommit: () => prepared.every(candidate => candidate.canCommit()),
1980
+ commit: async () => {
1981
+ try {
1982
+ for (const candidate of prepared) {
1983
+ if (!candidate.precommit)
1984
+ throw new Error("Exact MCP reset candidate does not support atomic publication");
1985
+ candidate.precommit();
1986
+ }
1987
+ } catch (error) {
1988
+ await Promise.allSettled(prepared.map(candidate => candidate.abort()));
1989
+ throw error;
1990
+ }
1991
+ for (const candidate of prepared) {
1992
+ if (!candidate.publish) throw new Error("Exact MCP reset candidate does not support atomic publication");
1993
+ candidate.publish();
1994
+ }
1995
+ this.#publishToolCatalog(tools);
1996
+ this.#suppressedServers.clear();
1997
+ this.#pendingExactSuspensions.clear();
1998
+ this.#drainDeferredSharedRebinds();
1999
+ },
2000
+ abort: async () => {
2001
+ await Promise.allSettled(prepared.map(candidate => candidate.abort()));
2002
+ },
2003
+ };
2004
+ }
2005
+
2006
+ #settledExactControl(result: ExactMcpServerControlResult): PreparedExactMcpServerControl {
2007
+ return { result, tools: this.getTools(), canCommit: () => true, commit: async () => {}, abort: async () => {} };
2008
+ }
2009
+
2010
+ async #buildExactServerTools(
2011
+ name: string,
2012
+ connection: MCPServerConnection,
2013
+ ): Promise<CustomTool<TSchema, MCPToolDetails>[]> {
2014
+ const config = this.#serverConfigs.get(name) ?? connection.config;
2015
+ const serverTools = await listTools(this.#connectionForLease(connection));
2016
+ const reconnect = () => this.reconnectServer(name);
2017
+ return MCPTool.fromTools(
2018
+ this.#connectionForLease(connection),
2019
+ serverTools,
2020
+ reconnect,
2021
+ this.#exactToolOptions(name, config.sharing === "shared"),
2022
+ );
2023
+ }
2024
+
2025
+ async #preparedCatalogCommit(
2026
+ name: string,
2027
+ status: "resumed" | "reconnected",
2028
+ serverTools: CustomTool<TSchema, MCPToolDetails>[],
2029
+ candidate:
2030
+ | {
2031
+ connection: MCPServerConnection;
2032
+ lease: MCPPoolLease;
2033
+ canCommit(): boolean;
2034
+ publish(): void;
2035
+ abort(): Promise<void>;
2036
+ }
2037
+ | undefined,
2038
+ expectedConnection: MCPServerConnection | undefined,
2039
+ ): Promise<PreparedExactMcpServerControl> {
2040
+ const tools = [
2041
+ ...this.#tools.filter(
2042
+ tool =>
2043
+ tool.mcpServerName !== name && (!tool.mcpServerName || !this.#suppressedServers.has(tool.mcpServerName)),
2044
+ ),
2045
+ ...serverTools,
2046
+ ];
2047
+ sortMCPToolsByName(tools);
2048
+ if (this.#toolsOnly && new Set(tools.map(tool => tool.name)).size !== tools.length) {
2049
+ let abortError: unknown;
2050
+ try {
2051
+ await candidate?.abort();
2052
+ } catch (error) {
2053
+ abortError = error;
2054
+ }
2055
+ this.#pendingExactControlServers.delete(name);
2056
+ this.#drainDeferredSharedRebinds();
2057
+ if (abortError !== undefined) throw abortError;
2058
+ return this.#settledExactControl({ name, status: "unavailable", toolCount: 0 });
2059
+ }
2060
+ let settled = false;
2061
+ const precommit = (): void => {
2062
+ if (settled) return;
2063
+ if (
2064
+ candidate
2065
+ ? !candidate.canCommit()
2066
+ : expectedConnection &&
2067
+ (this.#connections.get(name) !== expectedConnection || !expectedConnection.transport.connected)
2068
+ ) {
2069
+ throw new Error(`MCP exact control predecessor changed before commit: ${name}`);
2070
+ }
2071
+ };
2072
+ const publish = (): void => {
2073
+ if (settled) return;
2074
+ candidate?.publish();
2075
+ settled = true;
2076
+ if (candidate) {
2077
+ this.#leaseByConnection.set(candidate.connection, candidate.lease);
2078
+ this.#connections.set(name, candidate.connection);
2079
+ this.#registerLease(name, candidate.connection);
2080
+ }
2081
+ for (const tool of this.#tools) {
2082
+ if (tool.mcpServerName === name) (tool as MCPTool | DeferredMCPTool).invalidateForSessionControl();
2083
+ }
2084
+ const publishedTools = [
2085
+ ...this.#tools.filter(
2086
+ tool =>
2087
+ tool.mcpServerName !== name &&
2088
+ (!tool.mcpServerName || !this.#suppressedServers.has(tool.mcpServerName)),
2089
+ ),
2090
+ ...serverTools,
2091
+ ];
2092
+ sortMCPToolsByName(publishedTools);
2093
+ this.#publishToolCatalog(publishedTools);
2094
+ this.#suppressedServers.delete(name);
2095
+ this.#pendingExactControlServers.delete(name);
2096
+ this.#drainDeferredSharedRebinds();
2097
+ };
2098
+ return {
2099
+ result: { name, status, toolCount: serverTools.length },
2100
+ tools,
2101
+ canCommit: () =>
2102
+ candidate
2103
+ ? candidate.canCommit()
2104
+ : !expectedConnection ||
2105
+ (this.#connections.get(name) === expectedConnection && expectedConnection.transport.connected),
2106
+ precommit,
2107
+ publish,
2108
+ commit: async () => {
2109
+ if (settled) return;
2110
+ try {
2111
+ precommit();
2112
+ publish();
2113
+ } catch (error) {
2114
+ let abortError: unknown;
2115
+ try {
2116
+ await candidate?.abort();
2117
+ } catch (cleanupError) {
2118
+ abortError = cleanupError;
2119
+ }
2120
+ this.#pendingExactControlServers.delete(name);
2121
+ this.#drainDeferredSharedRebinds();
2122
+ if (abortError !== undefined) {
2123
+ throw new AggregateError([error, abortError], `MCP exact control commit and cleanup failed: ${name}`);
2124
+ }
2125
+ throw error;
2126
+ }
2127
+ },
2128
+ abort: async () => {
2129
+ if (settled) return;
2130
+ let abortError: unknown;
2131
+ try {
2132
+ await candidate?.abort();
2133
+ } catch (cleanupError) {
2134
+ abortError = cleanupError;
2135
+ }
2136
+ settled = true;
2137
+ this.#pendingExactControlServers.delete(name);
2138
+ this.#drainDeferredSharedRebinds();
2139
+ if (abortError !== undefined) throw abortError;
2140
+ },
2141
+ };
2142
+ }
2143
+
2144
+ async #prepareExactReplacement(name: string): Promise<{
2145
+ connection: MCPServerConnection;
2146
+ lease: MCPPoolLease;
2147
+ tools: CustomTool<TSchema, MCPToolDetails>[];
2148
+ canCommit(): boolean;
2149
+ publish(): void;
2150
+ abort(): Promise<void>;
2151
+ }> {
2152
+ const config = this.#serverConfigs.get(name);
2153
+ if (!config) throw new Error(`MCP server unavailable: ${name}`);
2154
+ await this.#closePendingConnectionCleanup(name);
2155
+ const resolved = await this.#resolveAuthConfig(config);
2156
+ const abort = new AbortController();
2157
+ this.#pendingConnectionControllers.set(name, abort);
2158
+ let prepared:
2159
+ | {
2160
+ lease: MCPPoolLease;
2161
+ canCommit(): boolean;
2162
+ publish(): void;
2163
+ abort(): Promise<void>;
2164
+ }
2165
+ | undefined;
2166
+ const abortPrepared = async (): Promise<void> => {
2167
+ if (!prepared) return;
2168
+ try {
2169
+ await prepared.abort();
2170
+ } catch (error) {
2171
+ const cleanupFailure = this.#retainConnectionCleanupFailure(name, error);
2172
+ if (cleanupFailure) {
2173
+ logger.error("MCP exact replacement abort cleanup failed", {
2174
+ path: `mcp:${name}`,
2175
+ error: cleanupFailure,
2176
+ });
2177
+ }
2178
+ throw error;
2179
+ }
2180
+ };
2181
+ try {
2182
+ prepared = await this.#pool.prepareReplacement(name, resolved, {
2183
+ keyConfig: config,
2184
+ sharingMode:
2185
+ config.sharing === "shared" &&
2186
+ (resolved.type === "http" || resolved.type === "sse" || resolved.type === "stdio")
2187
+ ? "shared"
2188
+ : "per-session",
2189
+ sessionId: config.sharing === "shared" ? undefined : this.#sessionId,
2190
+ signal: abort.signal,
2191
+ advertiseRoots: false,
2192
+ effectiveCwd: resolved.type === "stdio" ? (resolved.cwd ?? this.cwd) : this.cwd,
2193
+ capabilityProfile: "tools-only",
2194
+ effectiveHeaders: resolved.type === "http" || resolved.type === "sse" ? resolved.headers : undefined,
2195
+ onRequest: (method, params) => this.#handleServerRequest(method, params),
2196
+ onCleanupPending: error => this.#retainPendingAcquireCleanup(name, error),
2197
+ });
2198
+ const connection = prepared.lease.connection;
2199
+ connection.config = config;
2200
+ const source = this.#sources.get(name) ?? this.#connections.get(name)?._source;
2201
+ if (source) connection._source = source;
2202
+ const definitions = await listTools(prepared.lease.connectionForLease());
2203
+ const reconnect = () => this.reconnectServer(name);
2204
+ const tools = MCPTool.fromTools(
2205
+ prepared.lease.connectionForLease(),
2206
+ definitions,
2207
+ reconnect,
2208
+ this.#exactToolOptions(name, config.sharing === "shared"),
2209
+ );
2210
+ return {
2211
+ connection,
2212
+ lease: prepared.lease,
2213
+ tools,
2214
+ canCommit: prepared.canCommit,
2215
+ publish: prepared.publish,
2216
+ abort: abortPrepared,
2217
+ };
2218
+ } catch (error) {
2219
+ this.#retainConnectionCleanupFailure(name, error);
2220
+ try {
2221
+ await abortPrepared();
2222
+ } catch (abortError) {
2223
+ throw new AggregateError(
2224
+ [error, abortError],
2225
+ `MCP exact replacement preparation and cleanup failed: ${name}`,
2226
+ );
2227
+ }
2228
+ throw error;
2229
+ } finally {
2230
+ if (this.#pendingConnectionControllers.get(name) === abort) {
2231
+ this.#pendingConnectionControllers.delete(name);
2232
+ }
2233
+ }
1529
2234
  }
1530
2235
 
1531
2236
  /**
@@ -1776,7 +2481,10 @@ export class MCPManager {
1776
2481
  await this.#disconnectServer(name);
1777
2482
  }
1778
2483
 
1779
- async #disconnectServer(name: string): Promise<void> {
2484
+ async #disconnectServer(
2485
+ name: string,
2486
+ options: { preserveConfig?: boolean; preserveSource?: boolean; preserveTools?: boolean } = {},
2487
+ ): Promise<void> {
1780
2488
  const nextEpoch = (this.#disconnectEpochs.get(name) ?? 0) + 1;
1781
2489
  this.#disconnectEpochs.set(name, nextEpoch);
1782
2490
  this.#pendingConnectionControllers.get(name)?.abort(new Error(`MCP server disconnected: ${name}`));
@@ -1786,8 +2494,8 @@ export class MCPManager {
1786
2494
  this.#pendingConnections.delete(name);
1787
2495
  this.#pendingToolLoads.delete(name);
1788
2496
  this.#pendingReconnections.delete(name);
1789
- this.#sources.delete(name);
1790
- this.#serverConfigs.delete(name);
2497
+ if (!options.preserveSource) this.#sources.delete(name);
2498
+ if (!options.preserveConfig) this.#serverConfigs.delete(name);
1791
2499
  this.#pendingResourceRefresh.delete(name);
1792
2500
  const connection = this.#connections.get(name);
1793
2501
 
@@ -1816,14 +2524,21 @@ export class MCPManager {
1816
2524
  if (this.#connections.get(name) === connection) this.#connections.delete(name);
1817
2525
  }
1818
2526
 
1819
- // Remove tools from this server and notify consumers
1820
- const hadTools = this.#tools.some(
1821
- tool => (tool instanceof MCPTool || tool instanceof DeferredMCPTool) && tool.mcpServerName === name,
1822
- );
1823
- this.#tools = this.#tools.filter(
1824
- tool => !((tool instanceof MCPTool || tool instanceof DeferredMCPTool) && tool.mcpServerName === name),
1825
- );
1826
- if (hadTools) this.#onToolsChanged?.(this.#tools);
2527
+ // Keep a published cache fallback available while its expired live connection closes.
2528
+ if (!options.preserveTools) {
2529
+ const hadTools = this.#tools.some(
2530
+ tool => (tool instanceof MCPTool || tool instanceof DeferredMCPTool) && tool.mcpServerName === name,
2531
+ );
2532
+ const remainingTools = this.#tools.filter(
2533
+ tool => !((tool instanceof MCPTool || tool instanceof DeferredMCPTool) && tool.mcpServerName === name),
2534
+ );
2535
+ if (hadTools) {
2536
+ this.#publishToolCatalog(remainingTools);
2537
+ this.#notifyToolsChanged();
2538
+ } else {
2539
+ this.#tools = remainingTools;
2540
+ }
2541
+ }
1827
2542
 
1828
2543
  // Notify prompt consumers so stale commands are cleared
1829
2544
  if (connection?.prompts?.length) this.#onPromptsChanged?.(name);
@@ -1926,8 +2641,13 @@ export class MCPManager {
1926
2641
  this.#pendingResourceRefresh.clear();
1927
2642
  this.#sources.clear();
1928
2643
  this.#serverConfigs.clear();
2644
+ this.#suppressedServers.clear();
2645
+ this.#pendingExactSuspensions.clear();
2646
+ this.#pendingExactControlServers.clear();
1929
2647
  this.#connections.clear();
1930
- this.#tools = [];
2648
+ const hadTools = this.#tools.length > 0;
2649
+ this.#publishToolCatalog([]);
2650
+ if (hadTools) this.#notifyToolsChanged();
1931
2651
  this.#subscribedResources.clear();
1932
2652
  const releaseFailures = [
1933
2653
  ...scopedReleaseFailures,
@@ -1966,6 +2686,8 @@ export class MCPManager {
1966
2686
  async reconnectServer(name: string): Promise<MCPServerConnection | null> {
1967
2687
  if (this.#connectionSetSealed) return null;
1968
2688
  if (this.#scopedLifecycle === "disconnecting") return null;
2689
+ if (this.#pendingExactControlServers.has(name)) return this.#connections.get(name) ?? null;
2690
+ if (this.#suppressedServers.has(name)) return null;
1969
2691
  const pending = this.#pendingReconnections.get(name);
1970
2692
  if (pending) return pending;
1971
2693
 
@@ -2163,6 +2885,9 @@ export class MCPManager {
2163
2885
  // while we were connecting (e.g. /mcp reload called disconnectAll).
2164
2886
  if (
2165
2887
  !this.#serverConfigs.has(name) ||
2888
+ this.#suppressedServers.has(name) ||
2889
+ this.#pendingExactSuspensions.has(name) ||
2890
+ this.#pendingExactControlServers.has(name) ||
2166
2891
  this.#epoch !== globalEpoch ||
2167
2892
  (this.#disconnectEpochs.get(name) ?? 0) !== disconnectEpoch ||
2168
2893
  (lifecycleEpoch !== undefined &&
@@ -2193,19 +2918,28 @@ export class MCPManager {
2193
2918
  }
2194
2919
  try {
2195
2920
  const serverTools = await listTools(this.#connectionForLease(connection));
2196
- if (!this.#isCurrentConnection(name, config, globalEpoch, disconnectEpoch, connection)) {
2921
+ // An exact control can claim this server while tools load; publishing here
2922
+ // would install a catalog the control never validated.
2923
+ if (
2924
+ !this.#isCurrentConnection(name, config, globalEpoch, disconnectEpoch, connection) ||
2925
+ this.#suppressedServers.has(name) ||
2926
+ this.#pendingExactSuspensions.has(name) ||
2927
+ this.#pendingExactControlServers.has(name)
2928
+ ) {
2197
2929
  const disconnectError = new Error(`Server "${name}" was disconnected during tool loading`);
2198
2930
  await this.#releaseLeasePreservingPrimary(name, connection);
2199
2931
  throw disconnectError;
2200
2932
  }
2201
2933
  const reconnect = () => this.reconnectServer(name);
2202
- const customTools = MCPTool.fromTools(this.#connectionForLease(connection), serverTools, reconnect, {
2203
- noReplay: config.sharing === "shared",
2204
- inputHandler: () => this.#inputRequestHandler ?? undefined,
2205
- });
2206
- void this.toolCache?.set(name, config, serverTools);
2934
+ const customTools = MCPTool.fromTools(
2935
+ this.#connectionForLease(connection),
2936
+ serverTools,
2937
+ reconnect,
2938
+ this.#exactToolOptions(name, config.sharing === "shared"),
2939
+ );
2940
+ if (this.#shouldCacheServerTools(name)) void this.toolCache?.set(name, config, serverTools);
2207
2941
  this.#replaceServerTools(name, customTools);
2208
- this.#onToolsChanged?.(this.#tools);
2942
+ this.#notifyToolsChanged();
2209
2943
  void this.#loadServerResourcesAndPrompts(name, connection);
2210
2944
  return connection;
2211
2945
  } catch (error) {
@@ -2267,15 +3001,17 @@ export class MCPManager {
2267
3001
  const serverTools = await listTools(facade);
2268
3002
  if (!this.#isCurrentConnection(name, connection.config, globalEpoch, disconnectEpoch, connection)) return;
2269
3003
  const reconnect = () => this.reconnectServer(name);
2270
- const customTools = MCPTool.fromTools(facade, serverTools, reconnect, {
2271
- noReplay: connection.config.sharing === "shared",
2272
- inputHandler: () => this.#inputRequestHandler ?? undefined,
2273
- });
2274
- void this.toolCache?.set(name, connection.config, serverTools);
3004
+ const customTools = MCPTool.fromTools(
3005
+ facade,
3006
+ serverTools,
3007
+ reconnect,
3008
+ this.#exactToolOptions(name, connection.config.sharing === "shared"),
3009
+ );
3010
+ if (this.#shouldCacheServerTools(name)) void this.toolCache?.set(name, connection.config, serverTools);
2275
3011
 
2276
3012
  // Replace tools from this server
2277
3013
  this.#replaceServerTools(name, customTools);
2278
- this.#onToolsChanged?.(this.#tools);
3014
+ this.#notifyToolsChanged();
2279
3015
  }
2280
3016
 
2281
3017
  /**