open-multi-agent-kit 0.98.5 → 1.0.0

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 (385) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/README.md +4 -3
  3. package/dist/bun/cli.d.ts.map +1 -1
  4. package/dist/bun/cli.js +1 -0
  5. package/dist/bun/cli.js.map +1 -1
  6. package/dist/bun/register-bundled-coding-agent.d.ts +10 -0
  7. package/dist/bun/register-bundled-coding-agent.d.ts.map +1 -0
  8. package/dist/bun/register-bundled-coding-agent.js +12 -0
  9. package/dist/bun/register-bundled-coding-agent.js.map +1 -0
  10. package/dist/cli/args.d.ts +1 -1
  11. package/dist/cli/args.d.ts.map +1 -1
  12. package/dist/cli/args.js +1 -1
  13. package/dist/cli/args.js.map +1 -1
  14. package/dist/cli/help.d.ts.map +1 -1
  15. package/dist/cli/help.js +2 -1
  16. package/dist/cli/help.js.map +1 -1
  17. package/dist/cli.d.ts.map +1 -1
  18. package/dist/cli.js +14 -2
  19. package/dist/cli.js.map +1 -1
  20. package/dist/commands/neo-cli.d.ts +9 -0
  21. package/dist/commands/neo-cli.d.ts.map +1 -0
  22. package/dist/commands/neo-cli.js +61 -0
  23. package/dist/commands/neo-cli.js.map +1 -0
  24. package/dist/commands/verified-run-cli.d.ts.map +1 -1
  25. package/dist/commands/verified-run-cli.js +2 -2
  26. package/dist/commands/verified-run-cli.js.map +1 -1
  27. package/dist/core/agent-session.d.ts +29 -2
  28. package/dist/core/agent-session.d.ts.map +1 -1
  29. package/dist/core/agent-session.js +126 -32
  30. package/dist/core/agent-session.js.map +1 -1
  31. package/dist/core/bundled-skills.d.ts +4 -0
  32. package/dist/core/bundled-skills.d.ts.map +1 -0
  33. package/dist/core/bundled-skills.js +32 -0
  34. package/dist/core/bundled-skills.js.map +1 -0
  35. package/dist/core/cli-diagnostics.d.ts +6 -0
  36. package/dist/core/cli-diagnostics.d.ts.map +1 -0
  37. package/dist/core/cli-diagnostics.js +20 -0
  38. package/dist/core/cli-diagnostics.js.map +1 -0
  39. package/dist/core/context-budget-headroom.d.ts +12 -0
  40. package/dist/core/context-budget-headroom.d.ts.map +1 -1
  41. package/dist/core/context-budget-headroom.js +35 -0
  42. package/dist/core/context-budget-headroom.js.map +1 -1
  43. package/dist/core/context-budget-v2-input-validation.d.ts +4 -0
  44. package/dist/core/context-budget-v2-input-validation.d.ts.map +1 -0
  45. package/dist/core/context-budget-v2-input-validation.js +79 -0
  46. package/dist/core/context-budget-v2-input-validation.js.map +1 -0
  47. package/dist/core/context-budget-v2-observability.d.ts +15 -0
  48. package/dist/core/context-budget-v2-observability.d.ts.map +1 -0
  49. package/dist/core/context-budget-v2-observability.js +35 -0
  50. package/dist/core/context-budget-v2-observability.js.map +1 -0
  51. package/dist/core/context-budget-v2-planned-items.d.ts +5 -0
  52. package/dist/core/context-budget-v2-planned-items.d.ts.map +1 -0
  53. package/dist/core/context-budget-v2-planned-items.js +40 -0
  54. package/dist/core/context-budget-v2-planned-items.js.map +1 -0
  55. package/dist/core/context-budget-v2-planner.d.ts.map +1 -1
  56. package/dist/core/context-budget-v2-planner.js +57 -38
  57. package/dist/core/context-budget-v2-planner.js.map +1 -1
  58. package/dist/core/context-budget-v2-selection.d.ts +19 -3
  59. package/dist/core/context-budget-v2-selection.d.ts.map +1 -1
  60. package/dist/core/context-budget-v2-selection.js +35 -57
  61. package/dist/core/context-budget-v2-selection.js.map +1 -1
  62. package/dist/core/context-budget-v2-tiers.d.ts.map +1 -1
  63. package/dist/core/context-budget-v2-tiers.js +9 -42
  64. package/dist/core/context-budget-v2-tiers.js.map +1 -1
  65. package/dist/core/context-budget-v2-types.d.ts +8 -1
  66. package/dist/core/context-budget-v2-types.d.ts.map +1 -1
  67. package/dist/core/context-budget-v2-types.js.map +1 -1
  68. package/dist/core/devin-harness-dispatch.d.ts +12 -0
  69. package/dist/core/devin-harness-dispatch.d.ts.map +1 -0
  70. package/dist/core/devin-harness-dispatch.js +12 -0
  71. package/dist/core/devin-harness-dispatch.js.map +1 -0
  72. package/dist/core/devin-harness.d.ts +53 -0
  73. package/dist/core/devin-harness.d.ts.map +1 -0
  74. package/dist/core/devin-harness.js +112 -0
  75. package/dist/core/devin-harness.js.map +1 -0
  76. package/dist/core/domain-dispatch.d.ts +4 -1
  77. package/dist/core/domain-dispatch.d.ts.map +1 -1
  78. package/dist/core/domain-dispatch.js +5 -0
  79. package/dist/core/domain-dispatch.js.map +1 -1
  80. package/dist/core/domain-loadouts-provider-harness.d.ts +12 -0
  81. package/dist/core/domain-loadouts-provider-harness.d.ts.map +1 -0
  82. package/dist/core/domain-loadouts-provider-harness.js +122 -0
  83. package/dist/core/domain-loadouts-provider-harness.js.map +1 -0
  84. package/dist/core/domain-loadouts.d.ts +2 -33
  85. package/dist/core/domain-loadouts.d.ts.map +1 -1
  86. package/dist/core/domain-loadouts.js +3 -56
  87. package/dist/core/domain-loadouts.js.map +1 -1
  88. package/dist/core/domain-profile.d.ts +40 -0
  89. package/dist/core/domain-profile.d.ts.map +1 -0
  90. package/dist/core/domain-profile.js +7 -0
  91. package/dist/core/domain-profile.js.map +1 -0
  92. package/dist/core/extensions/bundled-virtual-modules.d.ts +15 -0
  93. package/dist/core/extensions/bundled-virtual-modules.d.ts.map +1 -0
  94. package/dist/core/extensions/bundled-virtual-modules.js +63 -0
  95. package/dist/core/extensions/bundled-virtual-modules.js.map +1 -0
  96. package/dist/core/extensions/loader.d.ts.map +1 -1
  97. package/dist/core/extensions/loader.js +2 -42
  98. package/dist/core/extensions/loader.js.map +1 -1
  99. package/dist/core/extensions/runner.d.ts +1 -0
  100. package/dist/core/extensions/runner.d.ts.map +1 -1
  101. package/dist/core/extensions/runner.js +6 -0
  102. package/dist/core/extensions/runner.js.map +1 -1
  103. package/dist/core/extensions/types.d.ts +10 -0
  104. package/dist/core/extensions/types.d.ts.map +1 -1
  105. package/dist/core/extensions/types.js.map +1 -1
  106. package/dist/core/grok-harness-dispatch.d.ts +6 -20
  107. package/dist/core/grok-harness-dispatch.d.ts.map +1 -1
  108. package/dist/core/grok-harness-dispatch.js +6 -55
  109. package/dist/core/grok-harness-dispatch.js.map +1 -1
  110. package/dist/core/grok-harness.d.ts +7 -9
  111. package/dist/core/grok-harness.d.ts.map +1 -1
  112. package/dist/core/grok-harness.js +9 -23
  113. package/dist/core/grok-harness.js.map +1 -1
  114. package/dist/core/harness-skills.d.ts +20 -0
  115. package/dist/core/harness-skills.d.ts.map +1 -0
  116. package/dist/core/harness-skills.js +36 -0
  117. package/dist/core/harness-skills.js.map +1 -0
  118. package/dist/core/index.d.ts +2 -1
  119. package/dist/core/index.d.ts.map +1 -1
  120. package/dist/core/index.js +2 -1
  121. package/dist/core/index.js.map +1 -1
  122. package/dist/core/loadout-runtime-state.d.ts +25 -0
  123. package/dist/core/loadout-runtime-state.d.ts.map +1 -0
  124. package/dist/core/loadout-runtime-state.js +39 -0
  125. package/dist/core/loadout-runtime-state.js.map +1 -0
  126. package/dist/core/loadout-runtime.d.ts +4 -20
  127. package/dist/core/loadout-runtime.d.ts.map +1 -1
  128. package/dist/core/loadout-runtime.js +6 -13
  129. package/dist/core/loadout-runtime.js.map +1 -1
  130. package/dist/core/mcp/client.d.ts +27 -6
  131. package/dist/core/mcp/client.d.ts.map +1 -1
  132. package/dist/core/mcp/client.js +80 -20
  133. package/dist/core/mcp/client.js.map +1 -1
  134. package/dist/core/mcp/manager.d.ts +10 -1
  135. package/dist/core/mcp/manager.d.ts.map +1 -1
  136. package/dist/core/mcp/manager.js +68 -9
  137. package/dist/core/mcp/manager.js.map +1 -1
  138. package/dist/core/mcp/protocol.d.ts +9 -3
  139. package/dist/core/mcp/protocol.d.ts.map +1 -1
  140. package/dist/core/mcp/protocol.js +61 -15
  141. package/dist/core/mcp/protocol.js.map +1 -1
  142. package/dist/core/mcp/stdio-transport.d.ts +12 -1
  143. package/dist/core/mcp/stdio-transport.d.ts.map +1 -1
  144. package/dist/core/mcp/stdio-transport.js +30 -2
  145. package/dist/core/mcp/stdio-transport.js.map +1 -1
  146. package/dist/core/mcp-descriptor-injection.d.ts +26 -0
  147. package/dist/core/mcp-descriptor-injection.d.ts.map +1 -0
  148. package/dist/core/mcp-descriptor-injection.js +25 -0
  149. package/dist/core/mcp-descriptor-injection.js.map +1 -0
  150. package/dist/core/mcp-public-presets.d.ts +1 -3
  151. package/dist/core/mcp-public-presets.d.ts.map +1 -1
  152. package/dist/core/mcp-public-presets.js +3 -15
  153. package/dist/core/mcp-public-presets.js.map +1 -1
  154. package/dist/core/model-registry.d.ts.map +1 -1
  155. package/dist/core/model-registry.js +24 -3
  156. package/dist/core/model-registry.js.map +1 -1
  157. package/dist/core/model-resolver.d.ts +1 -41
  158. package/dist/core/model-resolver.d.ts.map +1 -1
  159. package/dist/core/model-resolver.js +2 -49
  160. package/dist/core/model-resolver.js.map +1 -1
  161. package/dist/core/neo/catalog.d.ts +20 -0
  162. package/dist/core/neo/catalog.d.ts.map +1 -0
  163. package/dist/core/neo/catalog.js +50 -0
  164. package/dist/core/neo/catalog.js.map +1 -0
  165. package/dist/core/neo/setup.d.ts +3 -0
  166. package/dist/core/neo/setup.d.ts.map +1 -0
  167. package/dist/core/neo/setup.js +47 -0
  168. package/dist/core/neo/setup.js.map +1 -0
  169. package/dist/core/provider-default-models.d.ts +43 -0
  170. package/dist/core/provider-default-models.d.ts.map +1 -0
  171. package/dist/core/provider-default-models.js +51 -0
  172. package/dist/core/provider-default-models.js.map +1 -0
  173. package/dist/core/provider-display-names.d.ts.map +1 -1
  174. package/dist/core/provider-display-names.js +2 -0
  175. package/dist/core/provider-display-names.js.map +1 -1
  176. package/dist/core/provider-error-classification.d.ts +57 -0
  177. package/dist/core/provider-error-classification.d.ts.map +1 -0
  178. package/dist/core/provider-error-classification.js +103 -0
  179. package/dist/core/provider-error-classification.js.map +1 -0
  180. package/dist/core/provider-harness-dispatch.d.ts +63 -0
  181. package/dist/core/provider-harness-dispatch.d.ts.map +1 -0
  182. package/dist/core/provider-harness-dispatch.js +60 -0
  183. package/dist/core/provider-harness-dispatch.js.map +1 -0
  184. package/dist/core/provider-resilience.d.ts +10 -0
  185. package/dist/core/provider-resilience.d.ts.map +1 -1
  186. package/dist/core/provider-resilience.js +36 -3
  187. package/dist/core/provider-resilience.js.map +1 -1
  188. package/dist/core/provider-usage-commandcode.d.ts +9 -0
  189. package/dist/core/provider-usage-commandcode.d.ts.map +1 -0
  190. package/dist/core/provider-usage-commandcode.js +198 -0
  191. package/dist/core/provider-usage-commandcode.js.map +1 -0
  192. package/dist/core/provider-usage-devin.d.ts +18 -0
  193. package/dist/core/provider-usage-devin.d.ts.map +1 -0
  194. package/dist/core/provider-usage-devin.js +71 -0
  195. package/dist/core/provider-usage-devin.js.map +1 -0
  196. package/dist/core/provider-usage-text.d.ts +5 -0
  197. package/dist/core/provider-usage-text.d.ts.map +1 -0
  198. package/dist/core/provider-usage-text.js +15 -0
  199. package/dist/core/provider-usage-text.js.map +1 -0
  200. package/dist/core/provider-usage-types.d.ts +2 -1
  201. package/dist/core/provider-usage-types.d.ts.map +1 -1
  202. package/dist/core/provider-usage-types.js.map +1 -1
  203. package/dist/core/provider-usage.d.ts +1 -2
  204. package/dist/core/provider-usage.d.ts.map +1 -1
  205. package/dist/core/provider-usage.js +18 -7
  206. package/dist/core/provider-usage.js.map +1 -1
  207. package/dist/core/resource-admission.d.ts +36 -0
  208. package/dist/core/resource-admission.d.ts.map +1 -1
  209. package/dist/core/resource-admission.js +59 -0
  210. package/dist/core/resource-admission.js.map +1 -1
  211. package/dist/core/resource-loader.d.ts.map +1 -1
  212. package/dist/core/resource-loader.js +2 -2
  213. package/dist/core/resource-loader.js.map +1 -1
  214. package/dist/core/run-execution-api.d.ts +1 -1
  215. package/dist/core/run-execution-api.d.ts.map +1 -1
  216. package/dist/core/run-execution-api.js.map +1 -1
  217. package/dist/core/run-usage-ledger.d.ts +47 -0
  218. package/dist/core/run-usage-ledger.d.ts.map +1 -0
  219. package/dist/core/run-usage-ledger.js +162 -0
  220. package/dist/core/run-usage-ledger.js.map +1 -0
  221. package/dist/core/run-usage-operation.d.ts +8 -0
  222. package/dist/core/run-usage-operation.d.ts.map +1 -0
  223. package/dist/core/run-usage-operation.js +12 -0
  224. package/dist/core/run-usage-operation.js.map +1 -0
  225. package/dist/core/sdk.d.ts.map +1 -1
  226. package/dist/core/sdk.js +20 -16
  227. package/dist/core/sdk.js.map +1 -1
  228. package/dist/core/session-failure-cause.d.ts.map +1 -1
  229. package/dist/core/session-failure-cause.js +14 -5
  230. package/dist/core/session-failure-cause.js.map +1 -1
  231. package/dist/core/session-prompt-lifecycle.d.ts +6 -0
  232. package/dist/core/session-prompt-lifecycle.d.ts.map +1 -1
  233. package/dist/core/session-prompt-lifecycle.js +47 -2
  234. package/dist/core/session-prompt-lifecycle.js.map +1 -1
  235. package/dist/core/session-termination.d.ts.map +1 -1
  236. package/dist/core/session-termination.js +4 -2
  237. package/dist/core/session-termination.js.map +1 -1
  238. package/dist/core/subagent-lane-authority.d.ts +28 -0
  239. package/dist/core/subagent-lane-authority.d.ts.map +1 -0
  240. package/dist/core/subagent-lane-authority.js +119 -0
  241. package/dist/core/subagent-lane-authority.js.map +1 -0
  242. package/dist/core/subagent-lane-contract.d.ts +114 -0
  243. package/dist/core/subagent-lane-contract.d.ts.map +1 -0
  244. package/dist/core/subagent-lane-contract.js +18 -0
  245. package/dist/core/subagent-lane-contract.js.map +1 -0
  246. package/dist/core/subagent-lane-launcher.d.ts +3 -6
  247. package/dist/core/subagent-lane-launcher.d.ts.map +1 -1
  248. package/dist/core/subagent-lane-launcher.js +53 -12
  249. package/dist/core/subagent-lane-launcher.js.map +1 -1
  250. package/dist/core/subagent-orchestration.d.ts +4 -17
  251. package/dist/core/subagent-orchestration.d.ts.map +1 -1
  252. package/dist/core/subagent-orchestration.js.map +1 -1
  253. package/dist/core/verified-run/broker.d.ts.map +1 -1
  254. package/dist/core/verified-run/broker.js +4 -1
  255. package/dist/core/verified-run/broker.js.map +1 -1
  256. package/dist/core/verified-run/dag-phase.d.ts +1 -1
  257. package/dist/core/verified-run/dag-phase.d.ts.map +1 -1
  258. package/dist/core/verified-run/dag-phase.js +46 -8
  259. package/dist/core/verified-run/dag-phase.js.map +1 -1
  260. package/dist/core/verified-run/dag-projection.d.ts.map +1 -1
  261. package/dist/core/verified-run/dag-projection.js +25 -12
  262. package/dist/core/verified-run/dag-projection.js.map +1 -1
  263. package/dist/core/verified-run/dag-types.d.ts +11 -0
  264. package/dist/core/verified-run/dag-types.d.ts.map +1 -1
  265. package/dist/core/verified-run/dag-types.js.map +1 -1
  266. package/dist/core/verified-run/event-parser.d.ts.map +1 -1
  267. package/dist/core/verified-run/event-parser.js +1 -0
  268. package/dist/core/verified-run/event-parser.js.map +1 -1
  269. package/dist/core/verified-run/owned-execution.d.ts +1 -0
  270. package/dist/core/verified-run/owned-execution.d.ts.map +1 -1
  271. package/dist/core/verified-run/owned-execution.js +7 -1
  272. package/dist/core/verified-run/owned-execution.js.map +1 -1
  273. package/dist/core/verified-run/process-projection.d.ts +13 -0
  274. package/dist/core/verified-run/process-projection.d.ts.map +1 -0
  275. package/dist/core/verified-run/process-projection.js +82 -0
  276. package/dist/core/verified-run/process-projection.js.map +1 -0
  277. package/dist/core/verified-run/projection.d.ts.map +1 -1
  278. package/dist/core/verified-run/projection.js +11 -55
  279. package/dist/core/verified-run/projection.js.map +1 -1
  280. package/dist/core/verified-run/run-types.d.ts +1 -0
  281. package/dist/core/verified-run/run-types.d.ts.map +1 -1
  282. package/dist/core/verified-run/run-types.js.map +1 -1
  283. package/dist/core/verified-run/task-execution.d.ts +9 -0
  284. package/dist/core/verified-run/task-execution.d.ts.map +1 -0
  285. package/dist/core/verified-run/task-execution.js +29 -0
  286. package/dist/core/verified-run/task-execution.js.map +1 -0
  287. package/dist/core/verified-run/writer-projection.d.ts.map +1 -1
  288. package/dist/core/verified-run/writer-projection.js +6 -5
  289. package/dist/core/verified-run/writer-projection.js.map +1 -1
  290. package/dist/core/workload-permit-pool.d.ts +1 -1
  291. package/dist/core/workload-permit-pool.d.ts.map +1 -1
  292. package/dist/core/workload-permit-pool.js +3 -0
  293. package/dist/core/workload-permit-pool.js.map +1 -1
  294. package/dist/guardrails/strict-evidence-approval-adapter.d.ts +34 -0
  295. package/dist/guardrails/strict-evidence-approval-adapter.d.ts.map +1 -0
  296. package/dist/guardrails/strict-evidence-approval-adapter.js +70 -0
  297. package/dist/guardrails/strict-evidence-approval-adapter.js.map +1 -0
  298. package/dist/index.d.ts +3 -0
  299. package/dist/index.d.ts.map +1 -1
  300. package/dist/index.js +1 -0
  301. package/dist/index.js.map +1 -1
  302. package/dist/main.d.ts.map +1 -1
  303. package/dist/main.js +11 -18
  304. package/dist/main.js.map +1 -1
  305. package/dist/modes/acp/acp-agent.d.ts +24 -0
  306. package/dist/modes/acp/acp-agent.d.ts.map +1 -0
  307. package/dist/modes/acp/acp-agent.js +133 -0
  308. package/dist/modes/acp/acp-agent.js.map +1 -0
  309. package/dist/modes/acp/acp-mode.d.ts +4 -0
  310. package/dist/modes/acp/acp-mode.d.ts.map +1 -0
  311. package/dist/modes/acp/acp-mode.js +34 -0
  312. package/dist/modes/acp/acp-mode.js.map +1 -0
  313. package/dist/modes/acp/acp-session.d.ts +4 -0
  314. package/dist/modes/acp/acp-session.d.ts.map +1 -0
  315. package/dist/modes/acp/acp-session.js +76 -0
  316. package/dist/modes/acp/acp-session.js.map +1 -0
  317. package/dist/modes/acp/acp-transport.d.ts +5 -0
  318. package/dist/modes/acp/acp-transport.d.ts.map +1 -0
  319. package/dist/modes/acp/acp-transport.js +90 -0
  320. package/dist/modes/acp/acp-transport.js.map +1 -0
  321. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  322. package/dist/modes/interactive/interactive-mode.js +3 -5
  323. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  324. package/dist/modes/interactive/resource-description.d.ts +3 -0
  325. package/dist/modes/interactive/resource-description.d.ts.map +1 -0
  326. package/dist/modes/interactive/resource-description.js +15 -0
  327. package/dist/modes/interactive/resource-description.js.map +1 -0
  328. package/docs/audit-revalidation-2026-09-17.md +77 -0
  329. package/docs/devin-harness.md +150 -0
  330. package/docs/docs.json +4 -0
  331. package/docs/ecraf-normalization.md +83 -0
  332. package/docs/environment-variables.md +2 -1
  333. package/docs/index.md +1 -0
  334. package/docs/loadout-domains/README.md +2 -1
  335. package/docs/loadout-domains/devin-harness.md +72 -0
  336. package/docs/metrics.md +57 -16
  337. package/docs/model-catalog-refresh.md +51 -1
  338. package/docs/models.md +1 -1
  339. package/docs/neo.md +134 -0
  340. package/docs/providers.md +143 -1
  341. package/docs/release-audit-0.98.5.md +17 -0
  342. package/docs/release-audit-0.99.0.md +68 -0
  343. package/docs/run-protocol.md +4 -3
  344. package/docs/run-usage-ledger.md +56 -0
  345. package/docs/runtime-algorithms.md +48 -1
  346. package/docs/sdk.md +9 -5
  347. package/docs/settings.md +1 -1
  348. package/docs/skills.md +1 -1
  349. package/docs/startup-resource-labels-testing.md +47 -0
  350. package/docs/tb21-audit.md +18 -4
  351. package/docs/usage.md +7 -1
  352. package/docs/verified-run-remaining-design.md +881 -0
  353. package/docs/verified-run-testing.md +91 -0
  354. package/docs/verified-run.md +30 -9
  355. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  356. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  357. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  358. package/examples/extensions/gondolin/package-lock.json +2 -2
  359. package/examples/extensions/gondolin/package.json +1 -1
  360. package/examples/extensions/sandbox/package-lock.json +2 -2
  361. package/examples/extensions/sandbox/package.json +1 -1
  362. package/examples/extensions/subagent/adaptive-agent-runtime.ts +14 -1
  363. package/examples/extensions/subagent/graph-result.ts +48 -0
  364. package/examples/extensions/subagent/index.ts +246 -111
  365. package/examples/extensions/subagent/managed-process-tree.ts +42 -0
  366. package/examples/extensions/subagent/managed-process.test.ts +24 -0
  367. package/examples/extensions/subagent/managed-process.ts +93 -112
  368. package/examples/extensions/subagent/subagent-runtime-types.ts +17 -1
  369. package/examples/extensions/subagent/subagent-stream.ts +161 -0
  370. package/examples/extensions/terminal-browser/README.md +54 -0
  371. package/examples/extensions/terminal-browser/bridge-protocol.ts +29 -0
  372. package/examples/extensions/terminal-browser/bridge.ts +219 -0
  373. package/examples/extensions/terminal-browser/browser-surface.ts +288 -0
  374. package/examples/extensions/terminal-browser/index.ts +215 -0
  375. package/examples/extensions/terminal-browser/placeholders.ts +49 -0
  376. package/examples/extensions/with-deps/package-lock.json +2 -2
  377. package/examples/extensions/with-deps/package.json +1 -1
  378. package/npm-shrinkwrap.json +18 -18
  379. package/package.json +8 -7
  380. package/resources/neo/skills/omk-browser/SKILL.md +32 -0
  381. package/resources/neo/skills/omk-code-review/SKILL.md +24 -0
  382. package/resources/neo/skills/omk-computeruse/SKILL.md +34 -0
  383. package/resources/neo/skills/omk-mcp-setup/SKILL.md +48 -0
  384. package/resources/neo/skills/omk-research/SKILL.md +24 -0
  385. package/resources/neo/skills/omk-site/SKILL.md +28 -0
@@ -11,6 +11,7 @@
11
11
  * - Connect work is deduplicated: concurrent `listToolDefinitions()` calls
12
12
  * share one in-flight connect per server.
13
13
  */
14
+ import { detectMcpDescriptorPromptInjection, MCP_QUARANTINE_PATTERN_SIGNAL_THRESHOLD } from "../mcp-public-presets.js";
14
15
  import { McpClient } from "./client.js";
15
16
  import { createMcpToolDefinition } from "./tools.js";
16
17
  export class McpManager {
@@ -21,7 +22,7 @@ export class McpManager {
21
22
  for (const config of options.servers) {
22
23
  if (this.runtimes.has(config.name))
23
24
  continue; // First definition wins; inventory already applies precedence.
24
- this.runtimes.set(config.name, { config, state: "idle", tools: [] });
25
+ this.runtimes.set(config.name, { config, state: "idle", tools: [], quarantinedTools: [], generation: 0 });
25
26
  }
26
27
  }
27
28
  /** Configured server names, in configuration order. */
@@ -36,6 +37,7 @@ export class McpManager {
36
37
  toolCount: runtime.tools.length,
37
38
  error: runtime.error,
38
39
  serverVersion: runtime.client?.serverInfo.version,
40
+ ...(runtime.quarantinedTools.length > 0 ? { quarantinedTools: runtime.quarantinedTools } : {}),
39
41
  }));
40
42
  }
41
43
  /**
@@ -70,14 +72,26 @@ export class McpManager {
70
72
  toolCount: runtime.tools.length,
71
73
  error: runtime.error,
72
74
  serverVersion: runtime.client?.serverInfo.version,
75
+ ...(runtime.quarantinedTools.length > 0 ? { quarantinedTools: runtime.quarantinedTools } : {}),
73
76
  };
74
77
  }
75
- /** Terminate every connected server. Idempotent. */
78
+ /**
79
+ * Terminate every connected or connecting server. Idempotent.
80
+ *
81
+ * Bumping the generation invalidates any in-flight connect before its late
82
+ * completion can publish tools or flip the state back to `ready`, and the
83
+ * attempt's not-yet-published client is closed alongside the live one so no
84
+ * spawned process outlives the close.
85
+ */
76
86
  close() {
77
87
  for (const runtime of this.runtimes.values()) {
88
+ runtime.generation += 1;
78
89
  runtime.client?.close();
79
90
  runtime.client = undefined;
91
+ runtime.pendingClient?.close();
92
+ runtime.pendingClient = undefined;
80
93
  runtime.tools = [];
94
+ runtime.quarantinedTools = [];
81
95
  if (runtime.state === "ready" || runtime.state === "connecting")
82
96
  runtime.state = "idle";
83
97
  }
@@ -107,14 +121,22 @@ export class McpManager {
107
121
  return this.status();
108
122
  }
109
123
  async pingRuntime(runtime, timeoutMs) {
124
+ // Pin the probed client: a close or reconnect during the ping must not let
125
+ // this failure path dispose the replacement client.
126
+ const client = runtime.client;
127
+ if (!client)
128
+ return;
110
129
  try {
111
- await runtime.client?.ping(timeoutMs);
130
+ await client.ping(timeoutMs);
112
131
  }
113
132
  catch (error) {
133
+ if (runtime.client !== client)
134
+ return; // A newer owner already handled it.
114
135
  // Isolation point: only this server transitions; the rest stay ready.
115
- runtime.client?.close();
116
136
  runtime.client = undefined;
137
+ client.close();
117
138
  runtime.tools = [];
139
+ runtime.quarantinedTools = [];
118
140
  runtime.state = "failed";
119
141
  runtime.error = `health check failed: ${error instanceof Error ? error.message : String(error)}`;
120
142
  }
@@ -130,13 +152,19 @@ export class McpManager {
130
152
  if (runtime.connecting)
131
153
  return runtime.connecting;
132
154
  runtime.state = "connecting";
133
- const attempt = this.connectRuntime(runtime).finally(() => {
134
- runtime.connecting = undefined;
155
+ const generation = runtime.generation;
156
+ const attempt = this.connectRuntime(runtime, generation).finally(() => {
157
+ // Only this attempt may clear its own in-flight markers; a newer
158
+ // attempt's `connecting`/`pendingClient` belong to the newer generation.
159
+ if (runtime.connecting === attempt)
160
+ runtime.connecting = undefined;
161
+ if (runtime.generation === generation)
162
+ runtime.pendingClient = undefined;
135
163
  });
136
164
  runtime.connecting = attempt;
137
165
  return attempt;
138
166
  }
139
- async connectRuntime(runtime) {
167
+ async connectRuntime(runtime, generation) {
140
168
  const clientOptions = {
141
169
  name: runtime.config.name,
142
170
  clientInfo: this.options.clientInfo,
@@ -150,19 +178,50 @@ export class McpManager {
150
178
  },
151
179
  };
152
180
  const client = this.options.createClient?.(clientOptions) ?? new McpClient(clientOptions);
181
+ // Register before the first await so close() can dispose a client whose
182
+ // connect() promise has not resolved yet.
183
+ runtime.pendingClient = client;
184
+ const isCurrent = () => runtime.generation === generation && runtime.pendingClient === client;
153
185
  try {
154
186
  await client.connect();
187
+ if (!isCurrent()) {
188
+ // close() or a newer attempt superseded this one mid-handshake.
189
+ client.close();
190
+ return;
191
+ }
155
192
  const schemas = await client.listTools();
193
+ if (!isCurrent()) {
194
+ client.close();
195
+ return;
196
+ }
156
197
  runtime.client = client;
157
- runtime.tools = schemas.map((schema) => createMcpToolDefinition(runtime.config.name, client, schema, {
198
+ runtime.pendingClient = undefined;
199
+ // 서술자 주입 스크리닝: MCP 도구 설명은 신뢰할 수 없는 제3자 입력이다.
200
+ // admitPublicMcpServer와 동일 임계값(0.7)으로 차단 — 수입 시점에서 fail-closed.
201
+ const definitions = schemas.map((schema) => createMcpToolDefinition(runtime.config.name, client, schema, {
158
202
  callTimeoutMs: this.options.callTimeoutMs,
159
203
  }));
204
+ const admitted = [];
205
+ const quarantined = [];
206
+ for (const definition of definitions) {
207
+ if (detectMcpDescriptorPromptInjection(definition.description ?? "").patternSignal >
208
+ MCP_QUARANTINE_PATTERN_SIGNAL_THRESHOLD) {
209
+ quarantined.push(definition.name);
210
+ }
211
+ else {
212
+ admitted.push(definition);
213
+ }
214
+ }
215
+ runtime.tools = admitted;
216
+ runtime.quarantinedTools = quarantined;
160
217
  runtime.state = "ready";
161
218
  runtime.error = undefined;
162
219
  }
163
220
  catch (error) {
164
- // Isolation point: this server is out, every other server is unaffected.
165
221
  client.close();
222
+ if (!isCurrent())
223
+ return; // The current generation owns the state now.
224
+ // Isolation point: this server is out, every other server is unaffected.
166
225
  runtime.client = undefined;
167
226
  runtime.tools = [];
168
227
  runtime.state = "failed";
@@ -1 +1 @@
1
- {"version":3,"file":"manager.js","sourceRoot":"","sources":["../../../src/core/mcp/manager.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Lazy, crash-isolated MCP server manager.\n *\n * Design constraints that shaped this:\n * - A workspace can configure 25 servers. Spawning them all at startup would\n * trade a 0.6s cold start for a multi-second one, so connection is lazy and\n * only happens when tools are actually requested.\n * - One broken server must never take a session down. Every connect and every\n * listing is isolated; a failure is recorded as server status and the other\n * servers still contribute tools.\n * - Connect work is deduplicated: concurrent `listToolDefinitions()` calls\n * share one in-flight connect per server.\n */\n\nimport type { TSchema } from \"typebox\";\nimport type { ToolDefinition } from \"../extensions/types.ts\";\nimport { McpClient, type McpClientOptions } from \"./client.ts\";\nimport { createMcpToolDefinition, type McpToolDetails } from \"./tools.ts\";\n\nexport type McpServerState = \"idle\" | \"connecting\" | \"ready\" | \"failed\";\n\nexport interface McpServerConfig {\n\treadonly name: string;\n\treadonly command: string;\n\treadonly args?: readonly string[];\n\treadonly env?: Readonly<Record<string, string>>;\n\treadonly cwd?: string;\n\t/** Skip this server without removing it from configuration. */\n\treadonly disabled?: boolean;\n\treadonly requestTimeoutMs?: number;\n\treadonly handshakeTimeoutMs?: number;\n}\n\nexport interface McpServerStatus {\n\treadonly name: string;\n\treadonly state: McpServerState;\n\treadonly toolCount: number;\n\t/** Failure reason when `state` is `failed`. Never contains configured env values. */\n\treadonly error?: string;\n\treadonly serverVersion?: string;\n}\n\nexport interface McpManagerOptions {\n\treadonly servers: readonly McpServerConfig[];\n\treadonly cwd?: string;\n\treadonly clientInfo?: { readonly name: string; readonly version: string };\n\treadonly callTimeoutMs?: number;\n\t/** Injected for tests so the manager can be exercised without spawning processes. */\n\treadonly createClient?: (options: McpClientOptions) => McpClient;\n}\n\ninterface ServerRuntime {\n\treadonly config: McpServerConfig;\n\tstate: McpServerState;\n\tclient?: McpClient;\n\ttools: ToolDefinition<TSchema, McpToolDetails>[];\n\terror?: string;\n\tconnecting?: Promise<void>;\n}\n\nexport class McpManager {\n\tprivate readonly runtimes = new Map<string, ServerRuntime>();\n\tprivate readonly options: McpManagerOptions;\n\n\tconstructor(options: McpManagerOptions) {\n\t\tthis.options = options;\n\t\tfor (const config of options.servers) {\n\t\t\tif (this.runtimes.has(config.name)) continue; // First definition wins; inventory already applies precedence.\n\t\t\tthis.runtimes.set(config.name, { config, state: \"idle\", tools: [] });\n\t\t}\n\t}\n\n\t/** Configured server names, in configuration order. */\n\tget serverNames(): string[] {\n\t\treturn [...this.runtimes.keys()];\n\t}\n\n\t/** Current status per server. Safe to call before any connection attempt. */\n\tstatus(): McpServerStatus[] {\n\t\treturn [...this.runtimes.values()].map((runtime) => ({\n\t\t\tname: runtime.config.name,\n\t\t\tstate: runtime.state,\n\t\t\ttoolCount: runtime.tools.length,\n\t\t\terror: runtime.error,\n\t\t\tserverVersion: runtime.client?.serverInfo.version,\n\t\t}));\n\t}\n\n\t/**\n\t * Connect every enabled server that has not been attempted yet, then return\n\t * the union of their tools. Servers are connected concurrently and failures\n\t * are isolated, so a partial result is the normal outcome, not an error.\n\t */\n\tasync listToolDefinitions(): Promise<ToolDefinition<TSchema, McpToolDetails>[]> {\n\t\tawait Promise.all([...this.runtimes.values()].map((runtime) => this.ensureConnected(runtime)));\n\t\tconst tools: ToolDefinition<TSchema, McpToolDetails>[] = [];\n\t\tconst seen = new Set<string>();\n\t\tfor (const runtime of this.runtimes.values()) {\n\t\t\tfor (const tool of runtime.tools) {\n\t\t\t\tif (seen.has(tool.name)) continue; // Namespacing makes this rare; keep it deterministic anyway.\n\t\t\t\tseen.add(tool.name);\n\t\t\t\ttools.push(tool);\n\t\t\t}\n\t\t}\n\t\treturn tools;\n\t}\n\n\t/** Connect a single server by name. Returns its status either way. */\n\tasync connect(name: string): Promise<McpServerStatus> {\n\t\tconst runtime = this.runtimes.get(name);\n\t\tif (!runtime) {\n\t\t\treturn { name, state: \"failed\", toolCount: 0, error: `Unknown MCP server \"${name}\"` };\n\t\t}\n\t\tawait this.ensureConnected(runtime);\n\t\treturn {\n\t\t\tname,\n\t\t\tstate: runtime.state,\n\t\t\ttoolCount: runtime.tools.length,\n\t\t\terror: runtime.error,\n\t\t\tserverVersion: runtime.client?.serverInfo.version,\n\t\t};\n\t}\n\n\t/** Terminate every connected server. Idempotent. */\n\tclose(): void {\n\t\tfor (const runtime of this.runtimes.values()) {\n\t\t\truntime.client?.close();\n\t\t\truntime.client = undefined;\n\t\t\truntime.tools = [];\n\t\t\tif (runtime.state === \"ready\" || runtime.state === \"connecting\") runtime.state = \"idle\";\n\t\t}\n\t}\n\n\t/**\n\t * Verify that `ready` servers are actually alive with a protocol ping. A\n\t * failed ping closes the client and marks the server `failed`, so a\n\t * silently killed process stops masquerading as connected. Servers already\n\t * `failed` are re-attempted only when `reconnectFailed` is set — callers\n\t * should gate that behind a slow cadence so a permanently broken server is\n\t * not respawned every probe. Idle servers stay idle: connection is lazy by\n\t * design, and a status probe must not spawn processes.\n\t */\n\tasync checkHealth(options?: { pingTimeoutMs?: number; reconnectFailed?: boolean }): Promise<McpServerStatus[]> {\n\t\tconst work: Promise<void>[] = [];\n\t\tfor (const runtime of this.runtimes.values()) {\n\t\t\tif (runtime.state === \"ready\" && runtime.client) {\n\t\t\t\twork.push(this.pingRuntime(runtime, options?.pingTimeoutMs));\n\t\t\t} else if (runtime.state === \"failed\" && options?.reconnectFailed && !runtime.config.disabled) {\n\t\t\t\truntime.state = \"idle\";\n\t\t\t\truntime.error = undefined;\n\t\t\t\twork.push(this.ensureConnected(runtime));\n\t\t\t}\n\t\t}\n\t\tawait Promise.all(work);\n\t\treturn this.status();\n\t}\n\n\tprivate async pingRuntime(runtime: ServerRuntime, timeoutMs?: number): Promise<void> {\n\t\ttry {\n\t\t\tawait runtime.client?.ping(timeoutMs);\n\t\t} catch (error) {\n\t\t\t// Isolation point: only this server transitions; the rest stay ready.\n\t\t\truntime.client?.close();\n\t\t\truntime.client = undefined;\n\t\t\truntime.tools = [];\n\t\t\truntime.state = \"failed\";\n\t\t\truntime.error = `health check failed: ${error instanceof Error ? error.message : String(error)}`;\n\t\t}\n\t}\n\n\tprivate ensureConnected(runtime: ServerRuntime): Promise<void> {\n\t\tif (runtime.config.disabled) {\n\t\t\truntime.state = \"failed\";\n\t\t\truntime.error = \"disabled by configuration\";\n\t\t\treturn Promise.resolve();\n\t\t}\n\t\tif (runtime.state === \"ready\" || runtime.state === \"failed\") return Promise.resolve();\n\t\tif (runtime.connecting) return runtime.connecting;\n\n\t\truntime.state = \"connecting\";\n\t\tconst attempt = this.connectRuntime(runtime).finally(() => {\n\t\t\truntime.connecting = undefined;\n\t\t});\n\t\truntime.connecting = attempt;\n\t\treturn attempt;\n\t}\n\n\tprivate async connectRuntime(runtime: ServerRuntime): Promise<void> {\n\t\tconst clientOptions: McpClientOptions = {\n\t\t\tname: runtime.config.name,\n\t\t\tclientInfo: this.options.clientInfo,\n\t\t\trequestTimeoutMs: runtime.config.requestTimeoutMs,\n\t\t\thandshakeTimeoutMs: runtime.config.handshakeTimeoutMs,\n\t\t\ttransport: {\n\t\t\t\tcommand: runtime.config.command,\n\t\t\t\targs: runtime.config.args,\n\t\t\t\tenv: runtime.config.env,\n\t\t\t\tcwd: runtime.config.cwd ?? this.options.cwd,\n\t\t\t},\n\t\t};\n\t\tconst client = this.options.createClient?.(clientOptions) ?? new McpClient(clientOptions);\n\t\ttry {\n\t\t\tawait client.connect();\n\t\t\tconst schemas = await client.listTools();\n\t\t\truntime.client = client;\n\t\t\truntime.tools = schemas.map((schema) =>\n\t\t\t\tcreateMcpToolDefinition(runtime.config.name, client, schema, {\n\t\t\t\t\tcallTimeoutMs: this.options.callTimeoutMs,\n\t\t\t\t}),\n\t\t\t);\n\t\t\truntime.state = \"ready\";\n\t\t\truntime.error = undefined;\n\t\t} catch (error) {\n\t\t\t// Isolation point: this server is out, every other server is unaffected.\n\t\t\tclient.close();\n\t\t\truntime.client = undefined;\n\t\t\truntime.tools = [];\n\t\t\truntime.state = \"failed\";\n\t\t\truntime.error = error instanceof Error ? error.message : String(error);\n\t\t}\n\t}\n}\n"]}
1
+ {"version":3,"file":"manager.js","sourceRoot":"","sources":["../../../src/core/mcp/manager.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Lazy, crash-isolated MCP server manager.\n *\n * Design constraints that shaped this:\n * - A workspace can configure 25 servers. Spawning them all at startup would\n * trade a 0.6s cold start for a multi-second one, so connection is lazy and\n * only happens when tools are actually requested.\n * - One broken server must never take a session down. Every connect and every\n * listing is isolated; a failure is recorded as server status and the other\n * servers still contribute tools.\n * - Connect work is deduplicated: concurrent `listToolDefinitions()` calls\n * share one in-flight connect per server.\n */\n\nimport type { TSchema } from \"typebox\";\nimport type { ToolDefinition } from \"../extensions/types.ts\";\nimport { detectMcpDescriptorPromptInjection, MCP_QUARANTINE_PATTERN_SIGNAL_THRESHOLD } from \"../mcp-public-presets.ts\";\nimport { McpClient, type McpClientOptions } from \"./client.ts\";\nimport { createMcpToolDefinition, type McpToolDetails } from \"./tools.ts\";\n\nexport type McpServerState = \"idle\" | \"connecting\" | \"ready\" | \"failed\";\n\nexport interface McpServerConfig {\n\treadonly name: string;\n\treadonly command: string;\n\treadonly args?: readonly string[];\n\treadonly env?: Readonly<Record<string, string>>;\n\treadonly cwd?: string;\n\t/** Skip this server without removing it from configuration. */\n\treadonly disabled?: boolean;\n\treadonly requestTimeoutMs?: number;\n\treadonly handshakeTimeoutMs?: number;\n}\n\nexport interface McpServerStatus {\n\treadonly name: string;\n\treadonly state: McpServerState;\n\treadonly toolCount: number;\n\t/** Failure reason when `state` is `failed`. Never contains configured env values. */\n\treadonly error?: string;\n\treadonly serverVersion?: string;\n\t/** Tool names excluded because their descriptions carried prompt-injection payloads. */\n\treadonly quarantinedTools?: readonly string[];\n}\n\nexport interface McpManagerOptions {\n\treadonly servers: readonly McpServerConfig[];\n\treadonly cwd?: string;\n\treadonly clientInfo?: { readonly name: string; readonly version: string };\n\treadonly callTimeoutMs?: number;\n\t/** Injected for tests so the manager can be exercised without spawning processes. */\n\treadonly createClient?: (options: McpClientOptions) => McpClient;\n}\n\ninterface ServerRuntime {\n\treadonly config: McpServerConfig;\n\tstate: McpServerState;\n\tclient?: McpClient;\n\ttools: ToolDefinition<TSchema, McpToolDetails>[];\n\t/** Descriptor prompt-injection quarantines (namespaced tool names). */\n\tquarantinedTools: string[];\n\terror?: string;\n\tconnecting?: Promise<void>;\n\t/** Client owned by the in-flight connect attempt; not yet published. */\n\tpendingClient?: McpClient;\n\t/** Monotonic generation; a completed attempt may only publish when current. */\n\tgeneration: number;\n}\n\nexport class McpManager {\n\tprivate readonly runtimes = new Map<string, ServerRuntime>();\n\tprivate readonly options: McpManagerOptions;\n\n\tconstructor(options: McpManagerOptions) {\n\t\tthis.options = options;\n\t\tfor (const config of options.servers) {\n\t\t\tif (this.runtimes.has(config.name)) continue; // First definition wins; inventory already applies precedence.\n\t\t\tthis.runtimes.set(config.name, { config, state: \"idle\", tools: [], quarantinedTools: [], generation: 0 });\n\t\t}\n\t}\n\n\t/** Configured server names, in configuration order. */\n\tget serverNames(): string[] {\n\t\treturn [...this.runtimes.keys()];\n\t}\n\n\t/** Current status per server. Safe to call before any connection attempt. */\n\tstatus(): McpServerStatus[] {\n\t\treturn [...this.runtimes.values()].map((runtime) => ({\n\t\t\tname: runtime.config.name,\n\t\t\tstate: runtime.state,\n\t\t\ttoolCount: runtime.tools.length,\n\t\t\terror: runtime.error,\n\t\t\tserverVersion: runtime.client?.serverInfo.version,\n\t\t\t...(runtime.quarantinedTools.length > 0 ? { quarantinedTools: runtime.quarantinedTools } : {}),\n\t\t}));\n\t}\n\n\t/**\n\t * Connect every enabled server that has not been attempted yet, then return\n\t * the union of their tools. Servers are connected concurrently and failures\n\t * are isolated, so a partial result is the normal outcome, not an error.\n\t */\n\tasync listToolDefinitions(): Promise<ToolDefinition<TSchema, McpToolDetails>[]> {\n\t\tawait Promise.all([...this.runtimes.values()].map((runtime) => this.ensureConnected(runtime)));\n\t\tconst tools: ToolDefinition<TSchema, McpToolDetails>[] = [];\n\t\tconst seen = new Set<string>();\n\t\tfor (const runtime of this.runtimes.values()) {\n\t\t\tfor (const tool of runtime.tools) {\n\t\t\t\tif (seen.has(tool.name)) continue; // Namespacing makes this rare; keep it deterministic anyway.\n\t\t\t\tseen.add(tool.name);\n\t\t\t\ttools.push(tool);\n\t\t\t}\n\t\t}\n\t\treturn tools;\n\t}\n\n\t/** Connect a single server by name. Returns its status either way. */\n\tasync connect(name: string): Promise<McpServerStatus> {\n\t\tconst runtime = this.runtimes.get(name);\n\t\tif (!runtime) {\n\t\t\treturn { name, state: \"failed\", toolCount: 0, error: `Unknown MCP server \"${name}\"` };\n\t\t}\n\t\tawait this.ensureConnected(runtime);\n\t\treturn {\n\t\t\tname,\n\t\t\tstate: runtime.state,\n\t\t\ttoolCount: runtime.tools.length,\n\t\t\terror: runtime.error,\n\t\t\tserverVersion: runtime.client?.serverInfo.version,\n\t\t\t...(runtime.quarantinedTools.length > 0 ? { quarantinedTools: runtime.quarantinedTools } : {}),\n\t\t};\n\t}\n\n\t/**\n\t * Terminate every connected or connecting server. Idempotent.\n\t *\n\t * Bumping the generation invalidates any in-flight connect before its late\n\t * completion can publish tools or flip the state back to `ready`, and the\n\t * attempt's not-yet-published client is closed alongside the live one so no\n\t * spawned process outlives the close.\n\t */\n\tclose(): void {\n\t\tfor (const runtime of this.runtimes.values()) {\n\t\t\truntime.generation += 1;\n\t\t\truntime.client?.close();\n\t\t\truntime.client = undefined;\n\t\t\truntime.pendingClient?.close();\n\t\t\truntime.pendingClient = undefined;\n\t\t\truntime.tools = [];\n\t\t\truntime.quarantinedTools = [];\n\t\t\tif (runtime.state === \"ready\" || runtime.state === \"connecting\") runtime.state = \"idle\";\n\t\t}\n\t}\n\n\t/**\n\t * Verify that `ready` servers are actually alive with a protocol ping. A\n\t * failed ping closes the client and marks the server `failed`, so a\n\t * silently killed process stops masquerading as connected. Servers already\n\t * `failed` are re-attempted only when `reconnectFailed` is set — callers\n\t * should gate that behind a slow cadence so a permanently broken server is\n\t * not respawned every probe. Idle servers stay idle: connection is lazy by\n\t * design, and a status probe must not spawn processes.\n\t */\n\tasync checkHealth(options?: { pingTimeoutMs?: number; reconnectFailed?: boolean }): Promise<McpServerStatus[]> {\n\t\tconst work: Promise<void>[] = [];\n\t\tfor (const runtime of this.runtimes.values()) {\n\t\t\tif (runtime.state === \"ready\" && runtime.client) {\n\t\t\t\twork.push(this.pingRuntime(runtime, options?.pingTimeoutMs));\n\t\t\t} else if (runtime.state === \"failed\" && options?.reconnectFailed && !runtime.config.disabled) {\n\t\t\t\truntime.state = \"idle\";\n\t\t\t\truntime.error = undefined;\n\t\t\t\twork.push(this.ensureConnected(runtime));\n\t\t\t}\n\t\t}\n\t\tawait Promise.all(work);\n\t\treturn this.status();\n\t}\n\n\tprivate async pingRuntime(runtime: ServerRuntime, timeoutMs?: number): Promise<void> {\n\t\t// Pin the probed client: a close or reconnect during the ping must not let\n\t\t// this failure path dispose the replacement client.\n\t\tconst client = runtime.client;\n\t\tif (!client) return;\n\t\ttry {\n\t\t\tawait client.ping(timeoutMs);\n\t\t} catch (error) {\n\t\t\tif (runtime.client !== client) return; // A newer owner already handled it.\n\t\t\t// Isolation point: only this server transitions; the rest stay ready.\n\t\t\truntime.client = undefined;\n\t\t\tclient.close();\n\t\t\truntime.tools = [];\n\t\t\truntime.quarantinedTools = [];\n\t\t\truntime.state = \"failed\";\n\t\t\truntime.error = `health check failed: ${error instanceof Error ? error.message : String(error)}`;\n\t\t}\n\t}\n\n\tprivate ensureConnected(runtime: ServerRuntime): Promise<void> {\n\t\tif (runtime.config.disabled) {\n\t\t\truntime.state = \"failed\";\n\t\t\truntime.error = \"disabled by configuration\";\n\t\t\treturn Promise.resolve();\n\t\t}\n\t\tif (runtime.state === \"ready\" || runtime.state === \"failed\") return Promise.resolve();\n\t\tif (runtime.connecting) return runtime.connecting;\n\n\t\truntime.state = \"connecting\";\n\t\tconst generation = runtime.generation;\n\t\tconst attempt = this.connectRuntime(runtime, generation).finally(() => {\n\t\t\t// Only this attempt may clear its own in-flight markers; a newer\n\t\t\t// attempt's `connecting`/`pendingClient` belong to the newer generation.\n\t\t\tif (runtime.connecting === attempt) runtime.connecting = undefined;\n\t\t\tif (runtime.generation === generation) runtime.pendingClient = undefined;\n\t\t});\n\t\truntime.connecting = attempt;\n\t\treturn attempt;\n\t}\n\n\tprivate async connectRuntime(runtime: ServerRuntime, generation: number): Promise<void> {\n\t\tconst clientOptions: McpClientOptions = {\n\t\t\tname: runtime.config.name,\n\t\t\tclientInfo: this.options.clientInfo,\n\t\t\trequestTimeoutMs: runtime.config.requestTimeoutMs,\n\t\t\thandshakeTimeoutMs: runtime.config.handshakeTimeoutMs,\n\t\t\ttransport: {\n\t\t\t\tcommand: runtime.config.command,\n\t\t\t\targs: runtime.config.args,\n\t\t\t\tenv: runtime.config.env,\n\t\t\t\tcwd: runtime.config.cwd ?? this.options.cwd,\n\t\t\t},\n\t\t};\n\t\tconst client = this.options.createClient?.(clientOptions) ?? new McpClient(clientOptions);\n\t\t// Register before the first await so close() can dispose a client whose\n\t\t// connect() promise has not resolved yet.\n\t\truntime.pendingClient = client;\n\t\tconst isCurrent = () => runtime.generation === generation && runtime.pendingClient === client;\n\t\ttry {\n\t\t\tawait client.connect();\n\t\t\tif (!isCurrent()) {\n\t\t\t\t// close() or a newer attempt superseded this one mid-handshake.\n\t\t\t\tclient.close();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst schemas = await client.listTools();\n\t\t\tif (!isCurrent()) {\n\t\t\t\tclient.close();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\truntime.client = client;\n\t\t\truntime.pendingClient = undefined;\n\t\t\t// 서술자 주입 스크리닝: MCP 도구 설명은 신뢰할 수 없는 제3자 입력이다.\n\t\t\t// admitPublicMcpServer와 동일 임계값(0.7)으로 차단 — 수입 시점에서 fail-closed.\n\t\t\tconst definitions = schemas.map((schema) =>\n\t\t\t\tcreateMcpToolDefinition(runtime.config.name, client, schema, {\n\t\t\t\t\tcallTimeoutMs: this.options.callTimeoutMs,\n\t\t\t\t}),\n\t\t\t);\n\t\t\tconst admitted: typeof definitions = [];\n\t\t\tconst quarantined: string[] = [];\n\t\t\tfor (const definition of definitions) {\n\t\t\t\tif (\n\t\t\t\t\tdetectMcpDescriptorPromptInjection(definition.description ?? \"\").patternSignal >\n\t\t\t\t\tMCP_QUARANTINE_PATTERN_SIGNAL_THRESHOLD\n\t\t\t\t) {\n\t\t\t\t\tquarantined.push(definition.name);\n\t\t\t\t} else {\n\t\t\t\t\tadmitted.push(definition);\n\t\t\t\t}\n\t\t\t}\n\t\t\truntime.tools = admitted;\n\t\t\truntime.quarantinedTools = quarantined;\n\t\t\truntime.state = \"ready\";\n\t\t\truntime.error = undefined;\n\t\t} catch (error) {\n\t\t\tclient.close();\n\t\t\tif (!isCurrent()) return; // The current generation owns the state now.\n\t\t\t// Isolation point: this server is out, every other server is unaffected.\n\t\t\truntime.client = undefined;\n\t\t\truntime.tools = [];\n\t\t\truntime.state = \"failed\";\n\t\t\truntime.error = error instanceof Error ? error.message : String(error);\n\t\t}\n\t}\n}\n"]}
@@ -46,9 +46,15 @@ export interface DecodedLine {
46
46
  /**
47
47
  * Incremental newline-delimited JSON decoder.
48
48
  *
49
- * Holds at most one partial line. A line longer than {@link MAX_MESSAGE_LINE_BYTES}
50
- * is dropped with an error rather than buffered, so a runaway server cannot
51
- * exhaust memory.
49
+ * The limit is enforced in UTF-8 bytes against the raw frame, before JSON
50
+ * parsing, so the same message is accepted or rejected regardless of how the
51
+ * transport splits it into chunks. A line that exceeds
52
+ * {@link MAX_MESSAGE_LINE_BYTES} is reported once, its bytes are discarded
53
+ * without being retained, and decoding resynchronizes at the next newline —
54
+ * a runaway server cannot exhaust memory by growing the retained buffer.
55
+ *
56
+ * `maxLineBytes` must be a positive finite integer; anything else is a
57
+ * configuration error, not a size to guess at.
52
58
  */
53
59
  export declare function createLineDecoder(maxLineBytes?: number): {
54
60
  push(chunk: string): DecodedLine[];
@@ -1 +1 @@
1
- {"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../../../src/core/mcp/protocol.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,+FAA+F;AAC/F,eAAO,MAAM,sBAAsB,QAAmB,CAAC;AAEvD,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,MAAM,CAAC;AAExC,MAAM,WAAW,cAAc;IAC9B,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC;IACxB,QAAQ,CAAC,EAAE,EAAE,SAAS,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,mBAAmB;IACnC,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,gBAAgB;IAChC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,eAAe;IAC/B,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC;IACxB,QAAQ,CAAC,EAAE,EAAE,SAAS,CAAC;IACvB,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,KAAK,CAAC,EAAE,gBAAgB,CAAC;CAClC;AAED,MAAM,MAAM,cAAc,GAAG,cAAc,GAAG,mBAAmB,GAAG,eAAe,CAAC;AAMpF,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,IAAI,eAAe,CAM9E;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,IAAI,mBAAmB,CAEtF;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,IAAI,cAAc,CAK5E;AAED,sEAAsE;AACtE,wBAAgB,aAAa,CAAC,OAAO,EAAE,cAAc,GAAG,MAAM,CAQ7D;AAED,MAAM,WAAW,WAAW;IAC3B,oDAAoD;IACpD,QAAQ,CAAC,OAAO,CAAC,EAAE,cAAc,CAAC;IAClC,wDAAwD;IACxD,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,YAAY,GAAE,MAA+B,GAAG;IACjF,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,WAAW,EAAE,CAAC;IACnC,KAAK,IAAI,IAAI,CAAC;CACd,CAkCA;AAeD,2DAA2D;AAC3D,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,gBAAgB,GAAG,MAAM,CAGlE","sourcesContent":["/**\n * Pure JSON-RPC 2.0 framing for the MCP stdio transport.\n *\n * MCP stdio frames one JSON message per line on stdout; embedded newlines are\n * illegal, so decoding is a line split with a hard length ceiling. Nothing here\n * touches a process, a socket, or a clock — the transport owns all I/O so this\n * layer stays trivially testable.\n */\n\n/** Ceiling for a single decoded line. A server that exceeds it is malfunctioning, not slow. */\nexport const MAX_MESSAGE_LINE_BYTES = 16 * 1024 * 1024;\n\nexport type JsonRpcId = number | string;\n\nexport interface JsonRpcRequest {\n\treadonly jsonrpc: \"2.0\";\n\treadonly id: JsonRpcId;\n\treadonly method: string;\n\treadonly params?: unknown;\n}\n\nexport interface JsonRpcNotification {\n\treadonly jsonrpc: \"2.0\";\n\treadonly method: string;\n\treadonly params?: unknown;\n}\n\nexport interface JsonRpcErrorBody {\n\treadonly code: number;\n\treadonly message: string;\n\treadonly data?: unknown;\n}\n\nexport interface JsonRpcResponse {\n\treadonly jsonrpc: \"2.0\";\n\treadonly id: JsonRpcId;\n\treadonly result?: unknown;\n\treadonly error?: JsonRpcErrorBody;\n}\n\nexport type JsonRpcMessage = JsonRpcRequest | JsonRpcNotification | JsonRpcResponse;\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nexport function isJsonRpcResponse(message: unknown): message is JsonRpcResponse {\n\tif (!isRecord(message) || message.jsonrpc !== \"2.0\") return false;\n\tif (!(\"id\" in message)) return false;\n\tconst id = message.id;\n\tif (typeof id !== \"number\" && typeof id !== \"string\") return false;\n\treturn \"result\" in message || \"error\" in message;\n}\n\nexport function isJsonRpcNotification(message: unknown): message is JsonRpcNotification {\n\treturn isRecord(message) && message.jsonrpc === \"2.0\" && typeof message.method === \"string\" && !(\"id\" in message);\n}\n\nexport function isJsonRpcRequest(message: unknown): message is JsonRpcRequest {\n\tif (!isRecord(message) || message.jsonrpc !== \"2.0\") return false;\n\tif (typeof message.method !== \"string\") return false;\n\tconst id = message.id;\n\treturn typeof id === \"number\" || typeof id === \"string\";\n}\n\n/** Serialize one message as a single stdio frame (JSON + newline). */\nexport function encodeMessage(message: JsonRpcMessage): string {\n\tconst line = JSON.stringify(message);\n\tif (line.includes(\"\\n\")) {\n\t\t// JSON.stringify escapes newlines, so this is unreachable for valid input;\n\t\t// keep the guard so a future custom serializer cannot corrupt framing.\n\t\tthrow new Error(\"MCP frame contains a literal newline\");\n\t}\n\treturn `${line}\\n`;\n}\n\nexport interface DecodedLine {\n\t/** Parsed message, when the line was valid JSON. */\n\treadonly message?: JsonRpcMessage;\n\t/** Why the line was dropped, when it was not usable. */\n\treadonly error?: string;\n}\n\n/**\n * Incremental newline-delimited JSON decoder.\n *\n * Holds at most one partial line. A line longer than {@link MAX_MESSAGE_LINE_BYTES}\n * is dropped with an error rather than buffered, so a runaway server cannot\n * exhaust memory.\n */\nexport function createLineDecoder(maxLineBytes: number = MAX_MESSAGE_LINE_BYTES): {\n\tpush(chunk: string): DecodedLine[];\n\treset(): void;\n} {\n\tlet buffer = \"\";\n\tlet overflowed = false;\n\n\treturn {\n\t\tpush(chunk: string): DecodedLine[] {\n\t\t\tconst out: DecodedLine[] = [];\n\t\t\tbuffer += chunk;\n\n\t\t\tlet newlineIndex = buffer.indexOf(\"\\n\");\n\t\t\twhile (newlineIndex !== -1) {\n\t\t\t\tconst line = buffer.slice(0, newlineIndex).trim();\n\t\t\t\tbuffer = buffer.slice(newlineIndex + 1);\n\t\t\t\tif (overflowed) {\n\t\t\t\t\t// The oversized line ends here; resume clean at the next boundary.\n\t\t\t\t\toverflowed = false;\n\t\t\t\t} else if (line.length > 0) {\n\t\t\t\t\tout.push(decodeLine(line));\n\t\t\t\t}\n\t\t\t\tnewlineIndex = buffer.indexOf(\"\\n\");\n\t\t\t}\n\n\t\t\tif (!overflowed && buffer.length > maxLineBytes) {\n\t\t\t\tout.push({ error: `MCP frame exceeded ${maxLineBytes} bytes` });\n\t\t\t\toverflowed = true;\n\t\t\t\tbuffer = \"\";\n\t\t\t}\n\t\t\treturn out;\n\t\t},\n\t\treset(): void {\n\t\t\tbuffer = \"\";\n\t\t\toverflowed = false;\n\t\t},\n\t};\n}\n\nfunction decodeLine(line: string): DecodedLine {\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(line);\n\t} catch {\n\t\treturn { error: \"MCP frame is not valid JSON\" };\n\t}\n\tif (isJsonRpcResponse(parsed) || isJsonRpcNotification(parsed) || isJsonRpcRequest(parsed)) {\n\t\treturn { message: parsed };\n\t}\n\treturn { error: \"MCP frame is not a JSON-RPC 2.0 message\" };\n}\n\n/** Format a JSON-RPC error body for a human or a model. */\nexport function formatJsonRpcError(error: JsonRpcErrorBody): string {\n\tconst suffix = error.data === undefined ? \"\" : ` (${JSON.stringify(error.data)})`;\n\treturn `JSON-RPC error ${error.code}: ${error.message}${suffix}`;\n}\n"]}
1
+ {"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../../../src/core/mcp/protocol.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,+FAA+F;AAC/F,eAAO,MAAM,sBAAsB,QAAmB,CAAC;AAEvD,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,MAAM,CAAC;AAExC,MAAM,WAAW,cAAc;IAC9B,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC;IACxB,QAAQ,CAAC,EAAE,EAAE,SAAS,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,mBAAmB;IACnC,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,gBAAgB;IAChC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,eAAe;IAC/B,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC;IACxB,QAAQ,CAAC,EAAE,EAAE,SAAS,CAAC;IACvB,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,KAAK,CAAC,EAAE,gBAAgB,CAAC;CAClC;AAED,MAAM,MAAM,cAAc,GAAG,cAAc,GAAG,mBAAmB,GAAG,eAAe,CAAC;AAMpF,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,IAAI,eAAe,CAiB9E;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,IAAI,mBAAmB,CAEtF;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,IAAI,cAAc,CAK5E;AAED,sEAAsE;AACtE,wBAAgB,aAAa,CAAC,OAAO,EAAE,cAAc,GAAG,MAAM,CAQ7D;AAED,MAAM,WAAW,WAAW;IAC3B,oDAAoD;IACpD,QAAQ,CAAC,OAAO,CAAC,EAAE,cAAc,CAAC;IAClC,wDAAwD;IACxD,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,iBAAiB,CAAC,YAAY,GAAE,MAA+B,GAAG;IACjF,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,WAAW,EAAE,CAAC;IACnC,KAAK,IAAI,IAAI,CAAC;CACd,CA6DA;AAeD,2DAA2D;AAC3D,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,gBAAgB,GAAG,MAAM,CAGlE","sourcesContent":["/**\n * Pure JSON-RPC 2.0 framing for the MCP stdio transport.\n *\n * MCP stdio frames one JSON message per line on stdout; embedded newlines are\n * illegal, so decoding is a line split with a hard length ceiling. Nothing here\n * touches a process, a socket, or a clock — the transport owns all I/O so this\n * layer stays trivially testable.\n */\n\n/** Ceiling for a single decoded line. A server that exceeds it is malfunctioning, not slow. */\nexport const MAX_MESSAGE_LINE_BYTES = 16 * 1024 * 1024;\n\nexport type JsonRpcId = number | string;\n\nexport interface JsonRpcRequest {\n\treadonly jsonrpc: \"2.0\";\n\treadonly id: JsonRpcId;\n\treadonly method: string;\n\treadonly params?: unknown;\n}\n\nexport interface JsonRpcNotification {\n\treadonly jsonrpc: \"2.0\";\n\treadonly method: string;\n\treadonly params?: unknown;\n}\n\nexport interface JsonRpcErrorBody {\n\treadonly code: number;\n\treadonly message: string;\n\treadonly data?: unknown;\n}\n\nexport interface JsonRpcResponse {\n\treadonly jsonrpc: \"2.0\";\n\treadonly id: JsonRpcId;\n\treadonly result?: unknown;\n\treadonly error?: JsonRpcErrorBody;\n}\n\nexport type JsonRpcMessage = JsonRpcRequest | JsonRpcNotification | JsonRpcResponse;\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nexport function isJsonRpcResponse(message: unknown): message is JsonRpcResponse {\n\tif (!isRecord(message) || message.jsonrpc !== \"2.0\") return false;\n\tif (!(\"id\" in message)) return false;\n\tconst id = message.id;\n\tif (typeof id !== \"number\" && typeof id !== \"string\") return false;\n\t// JSON-RPC success and error are mutually exclusive; `error: null` is not a\n\t// valid error object, and a present error must carry its required fields.\n\tconst hasResult = \"result\" in message;\n\tconst hasError = \"error\" in message;\n\tif (hasResult === hasError) return false; // both present or both absent\n\tif (hasError) {\n\t\tconst error = message.error;\n\t\tif (!isRecord(error)) return false;\n\t\tif (typeof error.code !== \"number\" || !Number.isFinite(error.code)) return false;\n\t\tif (typeof error.message !== \"string\") return false;\n\t}\n\treturn true;\n}\n\nexport function isJsonRpcNotification(message: unknown): message is JsonRpcNotification {\n\treturn isRecord(message) && message.jsonrpc === \"2.0\" && typeof message.method === \"string\" && !(\"id\" in message);\n}\n\nexport function isJsonRpcRequest(message: unknown): message is JsonRpcRequest {\n\tif (!isRecord(message) || message.jsonrpc !== \"2.0\") return false;\n\tif (typeof message.method !== \"string\") return false;\n\tconst id = message.id;\n\treturn typeof id === \"number\" || typeof id === \"string\";\n}\n\n/** Serialize one message as a single stdio frame (JSON + newline). */\nexport function encodeMessage(message: JsonRpcMessage): string {\n\tconst line = JSON.stringify(message);\n\tif (line.includes(\"\\n\")) {\n\t\t// JSON.stringify escapes newlines, so this is unreachable for valid input;\n\t\t// keep the guard so a future custom serializer cannot corrupt framing.\n\t\tthrow new Error(\"MCP frame contains a literal newline\");\n\t}\n\treturn `${line}\\n`;\n}\n\nexport interface DecodedLine {\n\t/** Parsed message, when the line was valid JSON. */\n\treadonly message?: JsonRpcMessage;\n\t/** Why the line was dropped, when it was not usable. */\n\treadonly error?: string;\n}\n\n/**\n * Incremental newline-delimited JSON decoder.\n *\n * The limit is enforced in UTF-8 bytes against the raw frame, before JSON\n * parsing, so the same message is accepted or rejected regardless of how the\n * transport splits it into chunks. A line that exceeds\n * {@link MAX_MESSAGE_LINE_BYTES} is reported once, its bytes are discarded\n * without being retained, and decoding resynchronizes at the next newline —\n * a runaway server cannot exhaust memory by growing the retained buffer.\n *\n * `maxLineBytes` must be a positive finite integer; anything else is a\n * configuration error, not a size to guess at.\n */\nexport function createLineDecoder(maxLineBytes: number = MAX_MESSAGE_LINE_BYTES): {\n\tpush(chunk: string): DecodedLine[];\n\treset(): void;\n} {\n\tif (!Number.isSafeInteger(maxLineBytes) || maxLineBytes <= 0) {\n\t\tthrow new RangeError(`maxLineBytes must be a positive finite integer, got ${maxLineBytes}`);\n\t}\n\tlet buffer = \"\";\n\t/** UTF-8 byte length of `buffer` — the limit unit, not the JS string length. */\n\tlet bufferBytes = 0;\n\t/** True while the remainder of an oversized line is being discarded. */\n\tlet discarding = false;\n\n\treturn {\n\t\tpush(chunk: string): DecodedLine[] {\n\t\t\tconst out: DecodedLine[] = [];\n\t\t\tlet rest = chunk;\n\n\t\t\tif (discarding) {\n\t\t\t\t// Skip the tail of the rejected line without storing it.\n\t\t\t\tconst boundary = rest.indexOf(\"\\n\");\n\t\t\t\tif (boundary === -1) {\n\t\t\t\t\treturn out;\n\t\t\t\t}\n\t\t\t\tdiscarding = false;\n\t\t\t\trest = rest.slice(boundary + 1);\n\t\t\t}\n\n\t\t\tbuffer += rest;\n\t\t\tbufferBytes += Buffer.byteLength(rest, \"utf8\");\n\n\t\t\tlet newlineIndex = buffer.indexOf(\"\\n\");\n\t\t\twhile (newlineIndex !== -1) {\n\t\t\t\tconst line = buffer.slice(0, newlineIndex);\n\t\t\t\tconst lineBytes = Buffer.byteLength(line, \"utf8\");\n\t\t\t\tbuffer = buffer.slice(newlineIndex + 1);\n\t\t\t\tbufferBytes -= lineBytes + 1; // consumed line plus its newline\n\t\t\t\tconst trimmed = line.trim();\n\t\t\t\tif (trimmed.length === 0) {\n\t\t\t\t\tnewlineIndex = buffer.indexOf(\"\\n\");\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif (lineBytes > maxLineBytes) {\n\t\t\t\t\tout.push({ error: `MCP frame exceeded ${maxLineBytes} bytes` });\n\t\t\t\t} else {\n\t\t\t\t\tout.push(decodeLine(trimmed));\n\t\t\t\t}\n\t\t\t\tnewlineIndex = buffer.indexOf(\"\\n\");\n\t\t\t}\n\n\t\t\tif (bufferBytes > maxLineBytes) {\n\t\t\t\tout.push({ error: `MCP frame exceeded ${maxLineBytes} bytes` });\n\t\t\t\tbuffer = \"\";\n\t\t\t\tbufferBytes = 0;\n\t\t\t\tdiscarding = true;\n\t\t\t}\n\t\t\treturn out;\n\t\t},\n\t\treset(): void {\n\t\t\tbuffer = \"\";\n\t\t\tbufferBytes = 0;\n\t\t\tdiscarding = false;\n\t\t},\n\t};\n}\n\nfunction decodeLine(line: string): DecodedLine {\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(line);\n\t} catch {\n\t\treturn { error: \"MCP frame is not valid JSON\" };\n\t}\n\tif (isJsonRpcResponse(parsed) || isJsonRpcNotification(parsed) || isJsonRpcRequest(parsed)) {\n\t\treturn { message: parsed };\n\t}\n\treturn { error: \"MCP frame is not a JSON-RPC 2.0 message\" };\n}\n\n/** Format a JSON-RPC error body for a human or a model. */\nexport function formatJsonRpcError(error: JsonRpcErrorBody): string {\n\tconst suffix = error.data === undefined ? \"\" : ` (${JSON.stringify(error.data)})`;\n\treturn `JSON-RPC error ${error.code}: ${error.message}${suffix}`;\n}\n"]}
@@ -19,7 +19,22 @@ export function isJsonRpcResponse(message) {
19
19
  const id = message.id;
20
20
  if (typeof id !== "number" && typeof id !== "string")
21
21
  return false;
22
- return "result" in message || "error" in message;
22
+ // JSON-RPC success and error are mutually exclusive; `error: null` is not a
23
+ // valid error object, and a present error must carry its required fields.
24
+ const hasResult = "result" in message;
25
+ const hasError = "error" in message;
26
+ if (hasResult === hasError)
27
+ return false; // both present or both absent
28
+ if (hasError) {
29
+ const error = message.error;
30
+ if (!isRecord(error))
31
+ return false;
32
+ if (typeof error.code !== "number" || !Number.isFinite(error.code))
33
+ return false;
34
+ if (typeof error.message !== "string")
35
+ return false;
36
+ }
37
+ return true;
23
38
  }
24
39
  export function isJsonRpcNotification(message) {
25
40
  return isRecord(message) && message.jsonrpc === "2.0" && typeof message.method === "string" && !("id" in message);
@@ -45,40 +60,71 @@ export function encodeMessage(message) {
45
60
  /**
46
61
  * Incremental newline-delimited JSON decoder.
47
62
  *
48
- * Holds at most one partial line. A line longer than {@link MAX_MESSAGE_LINE_BYTES}
49
- * is dropped with an error rather than buffered, so a runaway server cannot
50
- * exhaust memory.
63
+ * The limit is enforced in UTF-8 bytes against the raw frame, before JSON
64
+ * parsing, so the same message is accepted or rejected regardless of how the
65
+ * transport splits it into chunks. A line that exceeds
66
+ * {@link MAX_MESSAGE_LINE_BYTES} is reported once, its bytes are discarded
67
+ * without being retained, and decoding resynchronizes at the next newline —
68
+ * a runaway server cannot exhaust memory by growing the retained buffer.
69
+ *
70
+ * `maxLineBytes` must be a positive finite integer; anything else is a
71
+ * configuration error, not a size to guess at.
51
72
  */
52
73
  export function createLineDecoder(maxLineBytes = MAX_MESSAGE_LINE_BYTES) {
74
+ if (!Number.isSafeInteger(maxLineBytes) || maxLineBytes <= 0) {
75
+ throw new RangeError(`maxLineBytes must be a positive finite integer, got ${maxLineBytes}`);
76
+ }
53
77
  let buffer = "";
54
- let overflowed = false;
78
+ /** UTF-8 byte length of `buffer` — the limit unit, not the JS string length. */
79
+ let bufferBytes = 0;
80
+ /** True while the remainder of an oversized line is being discarded. */
81
+ let discarding = false;
55
82
  return {
56
83
  push(chunk) {
57
84
  const out = [];
58
- buffer += chunk;
85
+ let rest = chunk;
86
+ if (discarding) {
87
+ // Skip the tail of the rejected line without storing it.
88
+ const boundary = rest.indexOf("\n");
89
+ if (boundary === -1) {
90
+ return out;
91
+ }
92
+ discarding = false;
93
+ rest = rest.slice(boundary + 1);
94
+ }
95
+ buffer += rest;
96
+ bufferBytes += Buffer.byteLength(rest, "utf8");
59
97
  let newlineIndex = buffer.indexOf("\n");
60
98
  while (newlineIndex !== -1) {
61
- const line = buffer.slice(0, newlineIndex).trim();
99
+ const line = buffer.slice(0, newlineIndex);
100
+ const lineBytes = Buffer.byteLength(line, "utf8");
62
101
  buffer = buffer.slice(newlineIndex + 1);
63
- if (overflowed) {
64
- // The oversized line ends here; resume clean at the next boundary.
65
- overflowed = false;
102
+ bufferBytes -= lineBytes + 1; // consumed line plus its newline
103
+ const trimmed = line.trim();
104
+ if (trimmed.length === 0) {
105
+ newlineIndex = buffer.indexOf("\n");
106
+ continue;
107
+ }
108
+ if (lineBytes > maxLineBytes) {
109
+ out.push({ error: `MCP frame exceeded ${maxLineBytes} bytes` });
66
110
  }
67
- else if (line.length > 0) {
68
- out.push(decodeLine(line));
111
+ else {
112
+ out.push(decodeLine(trimmed));
69
113
  }
70
114
  newlineIndex = buffer.indexOf("\n");
71
115
  }
72
- if (!overflowed && buffer.length > maxLineBytes) {
116
+ if (bufferBytes > maxLineBytes) {
73
117
  out.push({ error: `MCP frame exceeded ${maxLineBytes} bytes` });
74
- overflowed = true;
75
118
  buffer = "";
119
+ bufferBytes = 0;
120
+ discarding = true;
76
121
  }
77
122
  return out;
78
123
  },
79
124
  reset() {
80
125
  buffer = "";
81
- overflowed = false;
126
+ bufferBytes = 0;
127
+ discarding = false;
82
128
  },
83
129
  };
84
130
  }
@@ -1 +1 @@
1
- {"version":3,"file":"protocol.js","sourceRoot":"","sources":["../../../src/core/mcp/protocol.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Pure JSON-RPC 2.0 framing for the MCP stdio transport.\n *\n * MCP stdio frames one JSON message per line on stdout; embedded newlines are\n * illegal, so decoding is a line split with a hard length ceiling. Nothing here\n * touches a process, a socket, or a clock — the transport owns all I/O so this\n * layer stays trivially testable.\n */\n\n/** Ceiling for a single decoded line. A server that exceeds it is malfunctioning, not slow. */\nexport const MAX_MESSAGE_LINE_BYTES = 16 * 1024 * 1024;\n\nexport type JsonRpcId = number | string;\n\nexport interface JsonRpcRequest {\n\treadonly jsonrpc: \"2.0\";\n\treadonly id: JsonRpcId;\n\treadonly method: string;\n\treadonly params?: unknown;\n}\n\nexport interface JsonRpcNotification {\n\treadonly jsonrpc: \"2.0\";\n\treadonly method: string;\n\treadonly params?: unknown;\n}\n\nexport interface JsonRpcErrorBody {\n\treadonly code: number;\n\treadonly message: string;\n\treadonly data?: unknown;\n}\n\nexport interface JsonRpcResponse {\n\treadonly jsonrpc: \"2.0\";\n\treadonly id: JsonRpcId;\n\treadonly result?: unknown;\n\treadonly error?: JsonRpcErrorBody;\n}\n\nexport type JsonRpcMessage = JsonRpcRequest | JsonRpcNotification | JsonRpcResponse;\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nexport function isJsonRpcResponse(message: unknown): message is JsonRpcResponse {\n\tif (!isRecord(message) || message.jsonrpc !== \"2.0\") return false;\n\tif (!(\"id\" in message)) return false;\n\tconst id = message.id;\n\tif (typeof id !== \"number\" && typeof id !== \"string\") return false;\n\treturn \"result\" in message || \"error\" in message;\n}\n\nexport function isJsonRpcNotification(message: unknown): message is JsonRpcNotification {\n\treturn isRecord(message) && message.jsonrpc === \"2.0\" && typeof message.method === \"string\" && !(\"id\" in message);\n}\n\nexport function isJsonRpcRequest(message: unknown): message is JsonRpcRequest {\n\tif (!isRecord(message) || message.jsonrpc !== \"2.0\") return false;\n\tif (typeof message.method !== \"string\") return false;\n\tconst id = message.id;\n\treturn typeof id === \"number\" || typeof id === \"string\";\n}\n\n/** Serialize one message as a single stdio frame (JSON + newline). */\nexport function encodeMessage(message: JsonRpcMessage): string {\n\tconst line = JSON.stringify(message);\n\tif (line.includes(\"\\n\")) {\n\t\t// JSON.stringify escapes newlines, so this is unreachable for valid input;\n\t\t// keep the guard so a future custom serializer cannot corrupt framing.\n\t\tthrow new Error(\"MCP frame contains a literal newline\");\n\t}\n\treturn `${line}\\n`;\n}\n\nexport interface DecodedLine {\n\t/** Parsed message, when the line was valid JSON. */\n\treadonly message?: JsonRpcMessage;\n\t/** Why the line was dropped, when it was not usable. */\n\treadonly error?: string;\n}\n\n/**\n * Incremental newline-delimited JSON decoder.\n *\n * Holds at most one partial line. A line longer than {@link MAX_MESSAGE_LINE_BYTES}\n * is dropped with an error rather than buffered, so a runaway server cannot\n * exhaust memory.\n */\nexport function createLineDecoder(maxLineBytes: number = MAX_MESSAGE_LINE_BYTES): {\n\tpush(chunk: string): DecodedLine[];\n\treset(): void;\n} {\n\tlet buffer = \"\";\n\tlet overflowed = false;\n\n\treturn {\n\t\tpush(chunk: string): DecodedLine[] {\n\t\t\tconst out: DecodedLine[] = [];\n\t\t\tbuffer += chunk;\n\n\t\t\tlet newlineIndex = buffer.indexOf(\"\\n\");\n\t\t\twhile (newlineIndex !== -1) {\n\t\t\t\tconst line = buffer.slice(0, newlineIndex).trim();\n\t\t\t\tbuffer = buffer.slice(newlineIndex + 1);\n\t\t\t\tif (overflowed) {\n\t\t\t\t\t// The oversized line ends here; resume clean at the next boundary.\n\t\t\t\t\toverflowed = false;\n\t\t\t\t} else if (line.length > 0) {\n\t\t\t\t\tout.push(decodeLine(line));\n\t\t\t\t}\n\t\t\t\tnewlineIndex = buffer.indexOf(\"\\n\");\n\t\t\t}\n\n\t\t\tif (!overflowed && buffer.length > maxLineBytes) {\n\t\t\t\tout.push({ error: `MCP frame exceeded ${maxLineBytes} bytes` });\n\t\t\t\toverflowed = true;\n\t\t\t\tbuffer = \"\";\n\t\t\t}\n\t\t\treturn out;\n\t\t},\n\t\treset(): void {\n\t\t\tbuffer = \"\";\n\t\t\toverflowed = false;\n\t\t},\n\t};\n}\n\nfunction decodeLine(line: string): DecodedLine {\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(line);\n\t} catch {\n\t\treturn { error: \"MCP frame is not valid JSON\" };\n\t}\n\tif (isJsonRpcResponse(parsed) || isJsonRpcNotification(parsed) || isJsonRpcRequest(parsed)) {\n\t\treturn { message: parsed };\n\t}\n\treturn { error: \"MCP frame is not a JSON-RPC 2.0 message\" };\n}\n\n/** Format a JSON-RPC error body for a human or a model. */\nexport function formatJsonRpcError(error: JsonRpcErrorBody): string {\n\tconst suffix = error.data === undefined ? \"\" : ` (${JSON.stringify(error.data)})`;\n\treturn `JSON-RPC error ${error.code}: ${error.message}${suffix}`;\n}\n"]}
1
+ {"version":3,"file":"protocol.js","sourceRoot":"","sources":["../../../src/core/mcp/protocol.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Pure JSON-RPC 2.0 framing for the MCP stdio transport.\n *\n * MCP stdio frames one JSON message per line on stdout; embedded newlines are\n * illegal, so decoding is a line split with a hard length ceiling. Nothing here\n * touches a process, a socket, or a clock — the transport owns all I/O so this\n * layer stays trivially testable.\n */\n\n/** Ceiling for a single decoded line. A server that exceeds it is malfunctioning, not slow. */\nexport const MAX_MESSAGE_LINE_BYTES = 16 * 1024 * 1024;\n\nexport type JsonRpcId = number | string;\n\nexport interface JsonRpcRequest {\n\treadonly jsonrpc: \"2.0\";\n\treadonly id: JsonRpcId;\n\treadonly method: string;\n\treadonly params?: unknown;\n}\n\nexport interface JsonRpcNotification {\n\treadonly jsonrpc: \"2.0\";\n\treadonly method: string;\n\treadonly params?: unknown;\n}\n\nexport interface JsonRpcErrorBody {\n\treadonly code: number;\n\treadonly message: string;\n\treadonly data?: unknown;\n}\n\nexport interface JsonRpcResponse {\n\treadonly jsonrpc: \"2.0\";\n\treadonly id: JsonRpcId;\n\treadonly result?: unknown;\n\treadonly error?: JsonRpcErrorBody;\n}\n\nexport type JsonRpcMessage = JsonRpcRequest | JsonRpcNotification | JsonRpcResponse;\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nexport function isJsonRpcResponse(message: unknown): message is JsonRpcResponse {\n\tif (!isRecord(message) || message.jsonrpc !== \"2.0\") return false;\n\tif (!(\"id\" in message)) return false;\n\tconst id = message.id;\n\tif (typeof id !== \"number\" && typeof id !== \"string\") return false;\n\t// JSON-RPC success and error are mutually exclusive; `error: null` is not a\n\t// valid error object, and a present error must carry its required fields.\n\tconst hasResult = \"result\" in message;\n\tconst hasError = \"error\" in message;\n\tif (hasResult === hasError) return false; // both present or both absent\n\tif (hasError) {\n\t\tconst error = message.error;\n\t\tif (!isRecord(error)) return false;\n\t\tif (typeof error.code !== \"number\" || !Number.isFinite(error.code)) return false;\n\t\tif (typeof error.message !== \"string\") return false;\n\t}\n\treturn true;\n}\n\nexport function isJsonRpcNotification(message: unknown): message is JsonRpcNotification {\n\treturn isRecord(message) && message.jsonrpc === \"2.0\" && typeof message.method === \"string\" && !(\"id\" in message);\n}\n\nexport function isJsonRpcRequest(message: unknown): message is JsonRpcRequest {\n\tif (!isRecord(message) || message.jsonrpc !== \"2.0\") return false;\n\tif (typeof message.method !== \"string\") return false;\n\tconst id = message.id;\n\treturn typeof id === \"number\" || typeof id === \"string\";\n}\n\n/** Serialize one message as a single stdio frame (JSON + newline). */\nexport function encodeMessage(message: JsonRpcMessage): string {\n\tconst line = JSON.stringify(message);\n\tif (line.includes(\"\\n\")) {\n\t\t// JSON.stringify escapes newlines, so this is unreachable for valid input;\n\t\t// keep the guard so a future custom serializer cannot corrupt framing.\n\t\tthrow new Error(\"MCP frame contains a literal newline\");\n\t}\n\treturn `${line}\\n`;\n}\n\nexport interface DecodedLine {\n\t/** Parsed message, when the line was valid JSON. */\n\treadonly message?: JsonRpcMessage;\n\t/** Why the line was dropped, when it was not usable. */\n\treadonly error?: string;\n}\n\n/**\n * Incremental newline-delimited JSON decoder.\n *\n * The limit is enforced in UTF-8 bytes against the raw frame, before JSON\n * parsing, so the same message is accepted or rejected regardless of how the\n * transport splits it into chunks. A line that exceeds\n * {@link MAX_MESSAGE_LINE_BYTES} is reported once, its bytes are discarded\n * without being retained, and decoding resynchronizes at the next newline —\n * a runaway server cannot exhaust memory by growing the retained buffer.\n *\n * `maxLineBytes` must be a positive finite integer; anything else is a\n * configuration error, not a size to guess at.\n */\nexport function createLineDecoder(maxLineBytes: number = MAX_MESSAGE_LINE_BYTES): {\n\tpush(chunk: string): DecodedLine[];\n\treset(): void;\n} {\n\tif (!Number.isSafeInteger(maxLineBytes) || maxLineBytes <= 0) {\n\t\tthrow new RangeError(`maxLineBytes must be a positive finite integer, got ${maxLineBytes}`);\n\t}\n\tlet buffer = \"\";\n\t/** UTF-8 byte length of `buffer` — the limit unit, not the JS string length. */\n\tlet bufferBytes = 0;\n\t/** True while the remainder of an oversized line is being discarded. */\n\tlet discarding = false;\n\n\treturn {\n\t\tpush(chunk: string): DecodedLine[] {\n\t\t\tconst out: DecodedLine[] = [];\n\t\t\tlet rest = chunk;\n\n\t\t\tif (discarding) {\n\t\t\t\t// Skip the tail of the rejected line without storing it.\n\t\t\t\tconst boundary = rest.indexOf(\"\\n\");\n\t\t\t\tif (boundary === -1) {\n\t\t\t\t\treturn out;\n\t\t\t\t}\n\t\t\t\tdiscarding = false;\n\t\t\t\trest = rest.slice(boundary + 1);\n\t\t\t}\n\n\t\t\tbuffer += rest;\n\t\t\tbufferBytes += Buffer.byteLength(rest, \"utf8\");\n\n\t\t\tlet newlineIndex = buffer.indexOf(\"\\n\");\n\t\t\twhile (newlineIndex !== -1) {\n\t\t\t\tconst line = buffer.slice(0, newlineIndex);\n\t\t\t\tconst lineBytes = Buffer.byteLength(line, \"utf8\");\n\t\t\t\tbuffer = buffer.slice(newlineIndex + 1);\n\t\t\t\tbufferBytes -= lineBytes + 1; // consumed line plus its newline\n\t\t\t\tconst trimmed = line.trim();\n\t\t\t\tif (trimmed.length === 0) {\n\t\t\t\t\tnewlineIndex = buffer.indexOf(\"\\n\");\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif (lineBytes > maxLineBytes) {\n\t\t\t\t\tout.push({ error: `MCP frame exceeded ${maxLineBytes} bytes` });\n\t\t\t\t} else {\n\t\t\t\t\tout.push(decodeLine(trimmed));\n\t\t\t\t}\n\t\t\t\tnewlineIndex = buffer.indexOf(\"\\n\");\n\t\t\t}\n\n\t\t\tif (bufferBytes > maxLineBytes) {\n\t\t\t\tout.push({ error: `MCP frame exceeded ${maxLineBytes} bytes` });\n\t\t\t\tbuffer = \"\";\n\t\t\t\tbufferBytes = 0;\n\t\t\t\tdiscarding = true;\n\t\t\t}\n\t\t\treturn out;\n\t\t},\n\t\treset(): void {\n\t\t\tbuffer = \"\";\n\t\t\tbufferBytes = 0;\n\t\t\tdiscarding = false;\n\t\t},\n\t};\n}\n\nfunction decodeLine(line: string): DecodedLine {\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(line);\n\t} catch {\n\t\treturn { error: \"MCP frame is not valid JSON\" };\n\t}\n\tif (isJsonRpcResponse(parsed) || isJsonRpcNotification(parsed) || isJsonRpcRequest(parsed)) {\n\t\treturn { message: parsed };\n\t}\n\treturn { error: \"MCP frame is not a JSON-RPC 2.0 message\" };\n}\n\n/** Format a JSON-RPC error body for a human or a model. */\nexport function formatJsonRpcError(error: JsonRpcErrorBody): string {\n\tconst suffix = error.data === undefined ? \"\" : ` (${JSON.stringify(error.data)})`;\n\treturn `JSON-RPC error ${error.code}: ${error.message}${suffix}`;\n}\n"]}
@@ -15,6 +15,8 @@ import { type JsonRpcMessage } from "./protocol.ts";
15
15
  export declare const MAX_STDERR_TAIL_CHARS = 8192;
16
16
  /** Grace period between SIGTERM and SIGKILL on close. */
17
17
  export declare const DEFAULT_KILL_GRACE_MS = 2000;
18
+ /** Framed bytes allowed to sit in the child's stdin buffer before sends refuse. */
19
+ export declare const DEFAULT_MAX_PENDING_WRITE_BYTES: number;
18
20
  export interface StdioTransportOptions {
19
21
  readonly command: string;
20
22
  readonly args?: readonly string[];
@@ -24,6 +26,8 @@ export interface StdioTransportOptions {
24
26
  readonly killGraceMs?: number;
25
27
  /** Inherit the parent environment. Default `true`. */
26
28
  readonly inheritEnv?: boolean;
29
+ /** Framed bytes allowed to sit unwritten before `send` refuses. */
30
+ readonly maxPendingWriteBytes?: number;
27
31
  }
28
32
  export interface StdioTransportHandlers {
29
33
  readonly onMessage: (message: JsonRpcMessage) => void;
@@ -42,6 +46,9 @@ export declare class McpStdioTransport {
42
46
  private stderrTail;
43
47
  private exited;
44
48
  private killTimer;
49
+ /** Framed bytes written but not yet flushed to the child's stdin. */
50
+ private pendingWriteBytes;
51
+ private waitingForDrain;
45
52
  private readonly options;
46
53
  private readonly handlers;
47
54
  constructor(options: StdioTransportOptions, handlers: StdioTransportHandlers);
@@ -54,7 +61,11 @@ export declare class McpStdioTransport {
54
61
  * one failure path instead of two.
55
62
  */
56
63
  start(): void;
57
- /** Write one message. Returns `false` when the server is no longer writable. */
64
+ /**
65
+ * Write one message. Returns `false` when the server is no longer writable
66
+ * or the outbound buffer is already saturated — the client treats that as
67
+ * a send failure rather than queueing unbounded frames.
68
+ */
58
69
  send(message: JsonRpcMessage): boolean;
59
70
  /** Terminate the server: SIGTERM, then SIGKILL after the grace period. */
60
71
  close(): void;
@@ -1 +1 @@
1
- {"version":3,"file":"stdio-transport.d.ts","sourceRoot":"","sources":["../../../src/core/mcp/stdio-transport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH,OAAO,EAAoC,KAAK,cAAc,EAAE,MAAM,eAAe,CAAC;AAEtF,mEAAmE;AACnE,eAAO,MAAM,qBAAqB,OAAO,CAAC;AAC1C,yDAAyD;AACzD,eAAO,MAAM,qBAAqB,OAAO,CAAC;AAE1C,MAAM,WAAW,qBAAqB;IACrC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,QAAQ,CAAC,GAAG,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAChD,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,mCAAmC;IACnC,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,sDAAsD;IACtD,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,CAAC;CAC9B;AAED,MAAM,WAAW,sBAAsB;IACtC,QAAQ,CAAC,SAAS,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,IAAI,CAAC;IACtD,6DAA6D;IAC7D,QAAQ,CAAC,aAAa,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IAClD,yDAAyD;IACzD,QAAQ,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,KAAK,IAAI,CAAC;CAClG;AAED,qBAAa,iBAAiB;IAC7B,OAAO,CAAC,KAAK,CAA6C;IAC1D,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAuB;IAC/C,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,UAAU,CAAM;IACxB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,SAAS,CAA4C;IAC7D,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAwB;IAChD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAyB;IAElD,YAAY,OAAO,EAAE,qBAAqB,EAAE,QAAQ,EAAE,sBAAsB,EAI3E;IAED,iFAAiF;IACjF,IAAI,MAAM,IAAI,MAAM,CAEnB;IAED,IAAI,OAAO,IAAI,OAAO,CAErB;IAED;;;;OAIG;IACH,KAAK,IAAI,IAAI,CA+BZ;IAED,gFAAgF;IAChF,IAAI,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CASrC;IAED,0EAA0E;IAC1E,KAAK,IAAI,IAAI,CAuBZ;IAED,OAAO,CAAC,UAAU;CAUlB","sourcesContent":["/**\n * Child-process stdio transport for MCP.\n *\n * Owns exactly one server process: writes newline-delimited JSON to stdin,\n * decodes stdout into messages, keeps a bounded tail of stderr for diagnostics,\n * and reports exit exactly once. It never interprets MCP semantics — that is\n * the client's job.\n *\n * Every configured OMK MCP server uses a `command`, so stdio is the whole\n * transport surface today. HTTP servers would get a sibling module rather than\n * a flag on this one.\n */\n\nimport type { ChildProcessWithoutNullStreams } from \"node:child_process\";\nimport spawn from \"cross-spawn\";\nimport { createLineDecoder, encodeMessage, type JsonRpcMessage } from \"./protocol.ts\";\n\n/** How much stderr tail to keep per server for error reporting. */\nexport const MAX_STDERR_TAIL_CHARS = 8192;\n/** Grace period between SIGTERM and SIGKILL on close. */\nexport const DEFAULT_KILL_GRACE_MS = 2000;\n\nexport interface StdioTransportOptions {\n\treadonly command: string;\n\treadonly args?: readonly string[];\n\treadonly env?: Readonly<Record<string, string>>;\n\treadonly cwd?: string;\n\t/** Grace period before SIGKILL. */\n\treadonly killGraceMs?: number;\n\t/** Inherit the parent environment. Default `true`. */\n\treadonly inheritEnv?: boolean;\n}\n\nexport interface StdioTransportHandlers {\n\treadonly onMessage: (message: JsonRpcMessage) => void;\n\t/** Malformed frames are surfaced, never silently dropped. */\n\treadonly onDecodeError?: (reason: string) => void;\n\t/** Called at most once, with the exit code or signal. */\n\treadonly onExit: (info: { readonly code: number | null; readonly signal: string | null }) => void;\n}\n\nexport class McpStdioTransport {\n\tprivate child: ChildProcessWithoutNullStreams | undefined;\n\tprivate readonly decoder = createLineDecoder();\n\tprivate readonly killGraceMs: number;\n\tprivate stderrTail = \"\";\n\tprivate exited = false;\n\tprivate killTimer: ReturnType<typeof setTimeout> | undefined;\n\tprivate readonly options: StdioTransportOptions;\n\tprivate readonly handlers: StdioTransportHandlers;\n\n\tconstructor(options: StdioTransportOptions, handlers: StdioTransportHandlers) {\n\t\tthis.options = options;\n\t\tthis.handlers = handlers;\n\t\tthis.killGraceMs = options.killGraceMs ?? DEFAULT_KILL_GRACE_MS;\n\t}\n\n\t/** Bounded tail of the server's stderr. Empty when the server has been quiet. */\n\tget stderr(): string {\n\t\treturn this.stderrTail;\n\t}\n\n\tget running(): boolean {\n\t\treturn this.child !== undefined && !this.exited;\n\t}\n\n\t/**\n\t * Spawn the server process. Throws synchronously only on an invalid\n\t * configuration; a spawn failure surfaces through `onExit`, so callers have\n\t * one failure path instead of two.\n\t */\n\tstart(): void {\n\t\tif (this.child) throw new Error(\"MCP stdio transport already started\");\n\t\tconst env = this.options.inheritEnv === false ? { ...this.options.env } : { ...process.env, ...this.options.env };\n\t\tconst child = spawn(this.options.command, [...(this.options.args ?? [])], {\n\t\t\tcwd: this.options.cwd,\n\t\t\tenv,\n\t\t\tstdio: [\"pipe\", \"pipe\", \"pipe\"],\n\t\t\twindowsHide: true,\n\t\t}) as ChildProcessWithoutNullStreams;\n\t\tthis.child = child;\n\n\t\tchild.stdout.setEncoding(\"utf8\");\n\t\tchild.stdout.on(\"data\", (chunk: string) => {\n\t\t\tfor (const decoded of this.decoder.push(chunk)) {\n\t\t\t\tif (decoded.message) this.handlers.onMessage(decoded.message);\n\t\t\t\telse if (decoded.error) this.handlers.onDecodeError?.(decoded.error);\n\t\t\t}\n\t\t});\n\n\t\tchild.stderr.setEncoding(\"utf8\");\n\t\tchild.stderr.on(\"data\", (chunk: string) => {\n\t\t\tthis.stderrTail = `${this.stderrTail}${chunk}`.slice(-MAX_STDERR_TAIL_CHARS);\n\t\t});\n\n\t\t// A stdin EPIPE after the server exits is expected, not an error worth throwing.\n\t\tchild.stdin.on(\"error\", () => {});\n\t\tchild.on(\"error\", (error: Error) => {\n\t\t\tthis.stderrTail = `${this.stderrTail}${error.message}\\n`.slice(-MAX_STDERR_TAIL_CHARS);\n\t\t\tthis.settleExit(null, null);\n\t\t});\n\t\tchild.on(\"exit\", (code, signal) => this.settleExit(code, signal));\n\t}\n\n\t/** Write one message. Returns `false` when the server is no longer writable. */\n\tsend(message: JsonRpcMessage): boolean {\n\t\tconst child = this.child;\n\t\tif (!child || this.exited || !child.stdin.writable) return false;\n\t\ttry {\n\t\t\tchild.stdin.write(encodeMessage(message));\n\t\t\treturn true;\n\t\t} catch {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t/** Terminate the server: SIGTERM, then SIGKILL after the grace period. */\n\tclose(): void {\n\t\tconst child = this.child;\n\t\tif (!child || this.exited) return;\n\t\ttry {\n\t\t\tchild.stdin.end();\n\t\t} catch {\n\t\t\t// Already closed.\n\t\t}\n\t\ttry {\n\t\t\tchild.kill(\"SIGTERM\");\n\t\t} catch {\n\t\t\t// Already gone.\n\t\t}\n\t\tif (this.killTimer !== undefined) return;\n\t\tthis.killTimer = setTimeout(() => {\n\t\t\tthis.killTimer = undefined;\n\t\t\ttry {\n\t\t\t\tchild.kill(\"SIGKILL\");\n\t\t\t} catch {\n\t\t\t\t// Already gone.\n\t\t\t}\n\t\t}, this.killGraceMs);\n\t\tthis.killTimer.unref?.();\n\t}\n\n\tprivate settleExit(code: number | null, signal: string | null): void {\n\t\tif (this.exited) return;\n\t\tthis.exited = true;\n\t\tif (this.killTimer !== undefined) {\n\t\t\tclearTimeout(this.killTimer);\n\t\t\tthis.killTimer = undefined;\n\t\t}\n\t\tthis.decoder.reset();\n\t\tthis.handlers.onExit({ code, signal });\n\t}\n}\n"]}
1
+ {"version":3,"file":"stdio-transport.d.ts","sourceRoot":"","sources":["../../../src/core/mcp/stdio-transport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH,OAAO,EAAoC,KAAK,cAAc,EAAE,MAAM,eAAe,CAAC;AAEtF,mEAAmE;AACnE,eAAO,MAAM,qBAAqB,OAAO,CAAC;AAC1C,yDAAyD;AACzD,eAAO,MAAM,qBAAqB,OAAO,CAAC;AAC1C,mFAAmF;AACnF,eAAO,MAAM,+BAA+B,QAAmB,CAAC;AAEhE,MAAM,WAAW,qBAAqB;IACrC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,QAAQ,CAAC,GAAG,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAChD,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,mCAAmC;IACnC,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,sDAAsD;IACtD,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,CAAC;IAC9B,mEAAmE;IACnE,QAAQ,CAAC,oBAAoB,CAAC,EAAE,MAAM,CAAC;CACvC;AAED,MAAM,WAAW,sBAAsB;IACtC,QAAQ,CAAC,SAAS,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,IAAI,CAAC;IACtD,6DAA6D;IAC7D,QAAQ,CAAC,aAAa,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IAClD,yDAAyD;IACzD,QAAQ,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,KAAK,IAAI,CAAC;CAClG;AAED,qBAAa,iBAAiB;IAC7B,OAAO,CAAC,KAAK,CAA6C;IAC1D,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAuB;IAC/C,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,UAAU,CAAM;IACxB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,SAAS,CAA4C;IAC7D,qEAAqE;IACrE,OAAO,CAAC,iBAAiB,CAAK;IAC9B,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAwB;IAChD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAyB;IAElD,YAAY,OAAO,EAAE,qBAAqB,EAAE,QAAQ,EAAE,sBAAsB,EAI3E;IAED,iFAAiF;IACjF,IAAI,MAAM,IAAI,MAAM,CAEnB;IAED,IAAI,OAAO,IAAI,OAAO,CAErB;IAED;;;;OAIG;IACH,KAAK,IAAI,IAAI,CA+BZ;IAED;;;;OAIG;IACH,IAAI,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAyBrC;IAED,0EAA0E;IAC1E,KAAK,IAAI,IAAI,CAuBZ;IAED,OAAO,CAAC,UAAU;CAYlB","sourcesContent":["/**\n * Child-process stdio transport for MCP.\n *\n * Owns exactly one server process: writes newline-delimited JSON to stdin,\n * decodes stdout into messages, keeps a bounded tail of stderr for diagnostics,\n * and reports exit exactly once. It never interprets MCP semantics — that is\n * the client's job.\n *\n * Every configured OMK MCP server uses a `command`, so stdio is the whole\n * transport surface today. HTTP servers would get a sibling module rather than\n * a flag on this one.\n */\n\nimport type { ChildProcessWithoutNullStreams } from \"node:child_process\";\nimport spawn from \"cross-spawn\";\nimport { createLineDecoder, encodeMessage, type JsonRpcMessage } from \"./protocol.ts\";\n\n/** How much stderr tail to keep per server for error reporting. */\nexport const MAX_STDERR_TAIL_CHARS = 8192;\n/** Grace period between SIGTERM and SIGKILL on close. */\nexport const DEFAULT_KILL_GRACE_MS = 2000;\n/** Framed bytes allowed to sit in the child's stdin buffer before sends refuse. */\nexport const DEFAULT_MAX_PENDING_WRITE_BYTES = 16 * 1024 * 1024;\n\nexport interface StdioTransportOptions {\n\treadonly command: string;\n\treadonly args?: readonly string[];\n\treadonly env?: Readonly<Record<string, string>>;\n\treadonly cwd?: string;\n\t/** Grace period before SIGKILL. */\n\treadonly killGraceMs?: number;\n\t/** Inherit the parent environment. Default `true`. */\n\treadonly inheritEnv?: boolean;\n\t/** Framed bytes allowed to sit unwritten before `send` refuses. */\n\treadonly maxPendingWriteBytes?: number;\n}\n\nexport interface StdioTransportHandlers {\n\treadonly onMessage: (message: JsonRpcMessage) => void;\n\t/** Malformed frames are surfaced, never silently dropped. */\n\treadonly onDecodeError?: (reason: string) => void;\n\t/** Called at most once, with the exit code or signal. */\n\treadonly onExit: (info: { readonly code: number | null; readonly signal: string | null }) => void;\n}\n\nexport class McpStdioTransport {\n\tprivate child: ChildProcessWithoutNullStreams | undefined;\n\tprivate readonly decoder = createLineDecoder();\n\tprivate readonly killGraceMs: number;\n\tprivate stderrTail = \"\";\n\tprivate exited = false;\n\tprivate killTimer: ReturnType<typeof setTimeout> | undefined;\n\t/** Framed bytes written but not yet flushed to the child's stdin. */\n\tprivate pendingWriteBytes = 0;\n\tprivate waitingForDrain = false;\n\tprivate readonly options: StdioTransportOptions;\n\tprivate readonly handlers: StdioTransportHandlers;\n\n\tconstructor(options: StdioTransportOptions, handlers: StdioTransportHandlers) {\n\t\tthis.options = options;\n\t\tthis.handlers = handlers;\n\t\tthis.killGraceMs = options.killGraceMs ?? DEFAULT_KILL_GRACE_MS;\n\t}\n\n\t/** Bounded tail of the server's stderr. Empty when the server has been quiet. */\n\tget stderr(): string {\n\t\treturn this.stderrTail;\n\t}\n\n\tget running(): boolean {\n\t\treturn this.child !== undefined && !this.exited;\n\t}\n\n\t/**\n\t * Spawn the server process. Throws synchronously only on an invalid\n\t * configuration; a spawn failure surfaces through `onExit`, so callers have\n\t * one failure path instead of two.\n\t */\n\tstart(): void {\n\t\tif (this.child) throw new Error(\"MCP stdio transport already started\");\n\t\tconst env = this.options.inheritEnv === false ? { ...this.options.env } : { ...process.env, ...this.options.env };\n\t\tconst child = spawn(this.options.command, [...(this.options.args ?? [])], {\n\t\t\tcwd: this.options.cwd,\n\t\t\tenv,\n\t\t\tstdio: [\"pipe\", \"pipe\", \"pipe\"],\n\t\t\twindowsHide: true,\n\t\t}) as ChildProcessWithoutNullStreams;\n\t\tthis.child = child;\n\n\t\tchild.stdout.setEncoding(\"utf8\");\n\t\tchild.stdout.on(\"data\", (chunk: string) => {\n\t\t\tfor (const decoded of this.decoder.push(chunk)) {\n\t\t\t\tif (decoded.message) this.handlers.onMessage(decoded.message);\n\t\t\t\telse if (decoded.error) this.handlers.onDecodeError?.(decoded.error);\n\t\t\t}\n\t\t});\n\n\t\tchild.stderr.setEncoding(\"utf8\");\n\t\tchild.stderr.on(\"data\", (chunk: string) => {\n\t\t\tthis.stderrTail = `${this.stderrTail}${chunk}`.slice(-MAX_STDERR_TAIL_CHARS);\n\t\t});\n\n\t\t// A stdin EPIPE after the server exits is expected, not an error worth throwing.\n\t\tchild.stdin.on(\"error\", () => {});\n\t\tchild.on(\"error\", (error: Error) => {\n\t\t\tthis.stderrTail = `${this.stderrTail}${error.message}\\n`.slice(-MAX_STDERR_TAIL_CHARS);\n\t\t\tthis.settleExit(null, null);\n\t\t});\n\t\tchild.on(\"exit\", (code, signal) => this.settleExit(code, signal));\n\t}\n\n\t/**\n\t * Write one message. Returns `false` when the server is no longer writable\n\t * or the outbound buffer is already saturated — the client treats that as\n\t * a send failure rather than queueing unbounded frames.\n\t */\n\tsend(message: JsonRpcMessage): boolean {\n\t\tconst child = this.child;\n\t\tif (!child || this.exited || !child.stdin.writable) return false;\n\t\tconst frame = encodeMessage(message);\n\t\tconst frameBytes = Buffer.byteLength(frame, \"utf8\");\n\t\tconst cap = this.options.maxPendingWriteBytes ?? DEFAULT_MAX_PENDING_WRITE_BYTES;\n\t\tif (this.pendingWriteBytes + frameBytes > cap) return false;\n\t\ttry {\n\t\t\tconst flushed = child.stdin.write(frame);\n\t\t\tif (!flushed) {\n\t\t\t\t// Kernel/user buffer is saturated: account the frame and release it\n\t\t\t\t// on drain so the pending estimate tracks the real backlog.\n\t\t\t\tthis.pendingWriteBytes += frameBytes;\n\t\t\t\tif (!this.waitingForDrain) {\n\t\t\t\t\tthis.waitingForDrain = true;\n\t\t\t\t\tchild.stdin.once(\"drain\", () => {\n\t\t\t\t\t\tthis.waitingForDrain = false;\n\t\t\t\t\t\tthis.pendingWriteBytes = 0;\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn true;\n\t\t} catch {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t/** Terminate the server: SIGTERM, then SIGKILL after the grace period. */\n\tclose(): void {\n\t\tconst child = this.child;\n\t\tif (!child || this.exited) return;\n\t\ttry {\n\t\t\tchild.stdin.end();\n\t\t} catch {\n\t\t\t// Already closed.\n\t\t}\n\t\ttry {\n\t\t\tchild.kill(\"SIGTERM\");\n\t\t} catch {\n\t\t\t// Already gone.\n\t\t}\n\t\tif (this.killTimer !== undefined) return;\n\t\tthis.killTimer = setTimeout(() => {\n\t\t\tthis.killTimer = undefined;\n\t\t\ttry {\n\t\t\t\tchild.kill(\"SIGKILL\");\n\t\t\t} catch {\n\t\t\t\t// Already gone.\n\t\t\t}\n\t\t}, this.killGraceMs);\n\t\tthis.killTimer.unref?.();\n\t}\n\n\tprivate settleExit(code: number | null, signal: string | null): void {\n\t\tif (this.exited) return;\n\t\tthis.exited = true;\n\t\tthis.pendingWriteBytes = 0;\n\t\tthis.waitingForDrain = false;\n\t\tif (this.killTimer !== undefined) {\n\t\t\tclearTimeout(this.killTimer);\n\t\t\tthis.killTimer = undefined;\n\t\t}\n\t\tthis.decoder.reset();\n\t\tthis.handlers.onExit({ code, signal });\n\t}\n}\n"]}
@@ -16,6 +16,8 @@ import { createLineDecoder, encodeMessage } from "./protocol.js";
16
16
  export const MAX_STDERR_TAIL_CHARS = 8192;
17
17
  /** Grace period between SIGTERM and SIGKILL on close. */
18
18
  export const DEFAULT_KILL_GRACE_MS = 2000;
19
+ /** Framed bytes allowed to sit in the child's stdin buffer before sends refuse. */
20
+ export const DEFAULT_MAX_PENDING_WRITE_BYTES = 16 * 1024 * 1024;
19
21
  export class McpStdioTransport {
20
22
  child;
21
23
  decoder = createLineDecoder();
@@ -23,6 +25,9 @@ export class McpStdioTransport {
23
25
  stderrTail = "";
24
26
  exited = false;
25
27
  killTimer;
28
+ /** Framed bytes written but not yet flushed to the child's stdin. */
29
+ pendingWriteBytes = 0;
30
+ waitingForDrain = false;
26
31
  options;
27
32
  handlers;
28
33
  constructor(options, handlers) {
@@ -74,13 +79,34 @@ export class McpStdioTransport {
74
79
  });
75
80
  child.on("exit", (code, signal) => this.settleExit(code, signal));
76
81
  }
77
- /** Write one message. Returns `false` when the server is no longer writable. */
82
+ /**
83
+ * Write one message. Returns `false` when the server is no longer writable
84
+ * or the outbound buffer is already saturated — the client treats that as
85
+ * a send failure rather than queueing unbounded frames.
86
+ */
78
87
  send(message) {
79
88
  const child = this.child;
80
89
  if (!child || this.exited || !child.stdin.writable)
81
90
  return false;
91
+ const frame = encodeMessage(message);
92
+ const frameBytes = Buffer.byteLength(frame, "utf8");
93
+ const cap = this.options.maxPendingWriteBytes ?? DEFAULT_MAX_PENDING_WRITE_BYTES;
94
+ if (this.pendingWriteBytes + frameBytes > cap)
95
+ return false;
82
96
  try {
83
- child.stdin.write(encodeMessage(message));
97
+ const flushed = child.stdin.write(frame);
98
+ if (!flushed) {
99
+ // Kernel/user buffer is saturated: account the frame and release it
100
+ // on drain so the pending estimate tracks the real backlog.
101
+ this.pendingWriteBytes += frameBytes;
102
+ if (!this.waitingForDrain) {
103
+ this.waitingForDrain = true;
104
+ child.stdin.once("drain", () => {
105
+ this.waitingForDrain = false;
106
+ this.pendingWriteBytes = 0;
107
+ });
108
+ }
109
+ }
84
110
  return true;
85
111
  }
86
112
  catch {
@@ -121,6 +147,8 @@ export class McpStdioTransport {
121
147
  if (this.exited)
122
148
  return;
123
149
  this.exited = true;
150
+ this.pendingWriteBytes = 0;
151
+ this.waitingForDrain = false;
124
152
  if (this.killTimer !== undefined) {
125
153
  clearTimeout(this.killTimer);
126
154
  this.killTimer = undefined;