open-multi-agent-kit 0.99.0 → 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 (271) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +3 -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 +1 -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/core/agent-session.d.ts +29 -2
  25. package/dist/core/agent-session.d.ts.map +1 -1
  26. package/dist/core/agent-session.js +107 -30
  27. package/dist/core/agent-session.js.map +1 -1
  28. package/dist/core/bundled-skills.d.ts +4 -0
  29. package/dist/core/bundled-skills.d.ts.map +1 -0
  30. package/dist/core/bundled-skills.js +32 -0
  31. package/dist/core/bundled-skills.js.map +1 -0
  32. package/dist/core/cli-diagnostics.d.ts +6 -0
  33. package/dist/core/cli-diagnostics.d.ts.map +1 -0
  34. package/dist/core/cli-diagnostics.js +20 -0
  35. package/dist/core/cli-diagnostics.js.map +1 -0
  36. package/dist/core/context-budget-headroom.d.ts +12 -0
  37. package/dist/core/context-budget-headroom.d.ts.map +1 -1
  38. package/dist/core/context-budget-headroom.js +35 -0
  39. package/dist/core/context-budget-headroom.js.map +1 -1
  40. package/dist/core/context-budget-v2-input-validation.d.ts +4 -0
  41. package/dist/core/context-budget-v2-input-validation.d.ts.map +1 -0
  42. package/dist/core/context-budget-v2-input-validation.js +79 -0
  43. package/dist/core/context-budget-v2-input-validation.js.map +1 -0
  44. package/dist/core/context-budget-v2-observability.d.ts +15 -0
  45. package/dist/core/context-budget-v2-observability.d.ts.map +1 -0
  46. package/dist/core/context-budget-v2-observability.js +35 -0
  47. package/dist/core/context-budget-v2-observability.js.map +1 -0
  48. package/dist/core/context-budget-v2-planned-items.d.ts +5 -0
  49. package/dist/core/context-budget-v2-planned-items.d.ts.map +1 -0
  50. package/dist/core/context-budget-v2-planned-items.js +40 -0
  51. package/dist/core/context-budget-v2-planned-items.js.map +1 -0
  52. package/dist/core/context-budget-v2-planner.d.ts.map +1 -1
  53. package/dist/core/context-budget-v2-planner.js +57 -38
  54. package/dist/core/context-budget-v2-planner.js.map +1 -1
  55. package/dist/core/context-budget-v2-selection.d.ts +19 -3
  56. package/dist/core/context-budget-v2-selection.d.ts.map +1 -1
  57. package/dist/core/context-budget-v2-selection.js +35 -57
  58. package/dist/core/context-budget-v2-selection.js.map +1 -1
  59. package/dist/core/context-budget-v2-tiers.d.ts.map +1 -1
  60. package/dist/core/context-budget-v2-tiers.js +9 -42
  61. package/dist/core/context-budget-v2-tiers.js.map +1 -1
  62. package/dist/core/context-budget-v2-types.d.ts +8 -1
  63. package/dist/core/context-budget-v2-types.d.ts.map +1 -1
  64. package/dist/core/context-budget-v2-types.js.map +1 -1
  65. package/dist/core/extensions/bundled-virtual-modules.d.ts +15 -0
  66. package/dist/core/extensions/bundled-virtual-modules.d.ts.map +1 -0
  67. package/dist/core/extensions/bundled-virtual-modules.js +63 -0
  68. package/dist/core/extensions/bundled-virtual-modules.js.map +1 -0
  69. package/dist/core/extensions/loader.d.ts.map +1 -1
  70. package/dist/core/extensions/loader.js +2 -42
  71. package/dist/core/extensions/loader.js.map +1 -1
  72. package/dist/core/extensions/runner.d.ts +1 -0
  73. package/dist/core/extensions/runner.d.ts.map +1 -1
  74. package/dist/core/extensions/runner.js +6 -0
  75. package/dist/core/extensions/runner.js.map +1 -1
  76. package/dist/core/extensions/types.d.ts +10 -0
  77. package/dist/core/extensions/types.d.ts.map +1 -1
  78. package/dist/core/extensions/types.js.map +1 -1
  79. package/dist/core/index.d.ts +2 -1
  80. package/dist/core/index.d.ts.map +1 -1
  81. package/dist/core/index.js +2 -1
  82. package/dist/core/index.js.map +1 -1
  83. package/dist/core/loadout-runtime.d.ts +2 -8
  84. package/dist/core/loadout-runtime.d.ts.map +1 -1
  85. package/dist/core/loadout-runtime.js.map +1 -1
  86. package/dist/core/mcp/client.d.ts +27 -6
  87. package/dist/core/mcp/client.d.ts.map +1 -1
  88. package/dist/core/mcp/client.js +80 -20
  89. package/dist/core/mcp/client.js.map +1 -1
  90. package/dist/core/mcp/manager.d.ts +10 -1
  91. package/dist/core/mcp/manager.d.ts.map +1 -1
  92. package/dist/core/mcp/manager.js +68 -9
  93. package/dist/core/mcp/manager.js.map +1 -1
  94. package/dist/core/mcp/protocol.d.ts +9 -3
  95. package/dist/core/mcp/protocol.d.ts.map +1 -1
  96. package/dist/core/mcp/protocol.js +61 -15
  97. package/dist/core/mcp/protocol.js.map +1 -1
  98. package/dist/core/mcp/stdio-transport.d.ts +12 -1
  99. package/dist/core/mcp/stdio-transport.d.ts.map +1 -1
  100. package/dist/core/mcp/stdio-transport.js +30 -2
  101. package/dist/core/mcp/stdio-transport.js.map +1 -1
  102. package/dist/core/mcp-descriptor-injection.d.ts +26 -0
  103. package/dist/core/mcp-descriptor-injection.d.ts.map +1 -0
  104. package/dist/core/mcp-descriptor-injection.js +25 -0
  105. package/dist/core/mcp-descriptor-injection.js.map +1 -0
  106. package/dist/core/mcp-public-presets.d.ts +1 -3
  107. package/dist/core/mcp-public-presets.d.ts.map +1 -1
  108. package/dist/core/mcp-public-presets.js +3 -15
  109. package/dist/core/mcp-public-presets.js.map +1 -1
  110. package/dist/core/model-registry.d.ts.map +1 -1
  111. package/dist/core/model-registry.js +24 -3
  112. package/dist/core/model-registry.js.map +1 -1
  113. package/dist/core/neo/catalog.d.ts +20 -0
  114. package/dist/core/neo/catalog.d.ts.map +1 -0
  115. package/dist/core/neo/catalog.js +50 -0
  116. package/dist/core/neo/catalog.js.map +1 -0
  117. package/dist/core/neo/setup.d.ts +3 -0
  118. package/dist/core/neo/setup.d.ts.map +1 -0
  119. package/dist/core/neo/setup.js +47 -0
  120. package/dist/core/neo/setup.js.map +1 -0
  121. package/dist/core/provider-default-models.d.ts +1 -0
  122. package/dist/core/provider-default-models.d.ts.map +1 -1
  123. package/dist/core/provider-default-models.js +1 -0
  124. package/dist/core/provider-default-models.js.map +1 -1
  125. package/dist/core/provider-display-names.d.ts.map +1 -1
  126. package/dist/core/provider-display-names.js +1 -0
  127. package/dist/core/provider-display-names.js.map +1 -1
  128. package/dist/core/provider-error-classification.d.ts +57 -0
  129. package/dist/core/provider-error-classification.d.ts.map +1 -0
  130. package/dist/core/provider-error-classification.js +103 -0
  131. package/dist/core/provider-error-classification.js.map +1 -0
  132. package/dist/core/provider-resilience.d.ts +10 -0
  133. package/dist/core/provider-resilience.d.ts.map +1 -1
  134. package/dist/core/provider-resilience.js +36 -3
  135. package/dist/core/provider-resilience.js.map +1 -1
  136. package/dist/core/provider-usage-commandcode.d.ts +9 -0
  137. package/dist/core/provider-usage-commandcode.d.ts.map +1 -0
  138. package/dist/core/provider-usage-commandcode.js +198 -0
  139. package/dist/core/provider-usage-commandcode.js.map +1 -0
  140. package/dist/core/provider-usage-devin.d.ts +5 -3
  141. package/dist/core/provider-usage-devin.d.ts.map +1 -1
  142. package/dist/core/provider-usage-devin.js +15 -5
  143. package/dist/core/provider-usage-devin.js.map +1 -1
  144. package/dist/core/provider-usage-types.d.ts +1 -1
  145. package/dist/core/provider-usage-types.d.ts.map +1 -1
  146. package/dist/core/provider-usage-types.js.map +1 -1
  147. package/dist/core/provider-usage.d.ts.map +1 -1
  148. package/dist/core/provider-usage.js +6 -0
  149. package/dist/core/provider-usage.js.map +1 -1
  150. package/dist/core/resource-admission.d.ts +36 -0
  151. package/dist/core/resource-admission.d.ts.map +1 -1
  152. package/dist/core/resource-admission.js +59 -0
  153. package/dist/core/resource-admission.js.map +1 -1
  154. package/dist/core/resource-loader.d.ts.map +1 -1
  155. package/dist/core/resource-loader.js +2 -2
  156. package/dist/core/resource-loader.js.map +1 -1
  157. package/dist/core/run-usage-ledger.d.ts +47 -0
  158. package/dist/core/run-usage-ledger.d.ts.map +1 -0
  159. package/dist/core/run-usage-ledger.js +162 -0
  160. package/dist/core/run-usage-ledger.js.map +1 -0
  161. package/dist/core/run-usage-operation.d.ts +8 -0
  162. package/dist/core/run-usage-operation.d.ts.map +1 -0
  163. package/dist/core/run-usage-operation.js +12 -0
  164. package/dist/core/run-usage-operation.js.map +1 -0
  165. package/dist/core/sdk.d.ts.map +1 -1
  166. package/dist/core/sdk.js +5 -6
  167. package/dist/core/sdk.js.map +1 -1
  168. package/dist/core/session-failure-cause.d.ts.map +1 -1
  169. package/dist/core/session-failure-cause.js +14 -5
  170. package/dist/core/session-failure-cause.js.map +1 -1
  171. package/dist/core/session-prompt-lifecycle.d.ts +6 -0
  172. package/dist/core/session-prompt-lifecycle.d.ts.map +1 -1
  173. package/dist/core/session-prompt-lifecycle.js +47 -2
  174. package/dist/core/session-prompt-lifecycle.js.map +1 -1
  175. package/dist/core/session-termination.d.ts.map +1 -1
  176. package/dist/core/session-termination.js +4 -2
  177. package/dist/core/session-termination.js.map +1 -1
  178. package/dist/core/subagent-lane-authority.d.ts +28 -0
  179. package/dist/core/subagent-lane-authority.d.ts.map +1 -0
  180. package/dist/core/subagent-lane-authority.js +119 -0
  181. package/dist/core/subagent-lane-authority.js.map +1 -0
  182. package/dist/core/subagent-lane-contract.d.ts +114 -0
  183. package/dist/core/subagent-lane-contract.d.ts.map +1 -0
  184. package/dist/core/subagent-lane-contract.js +18 -0
  185. package/dist/core/subagent-lane-contract.js.map +1 -0
  186. package/dist/core/subagent-lane-launcher.d.ts +3 -6
  187. package/dist/core/subagent-lane-launcher.d.ts.map +1 -1
  188. package/dist/core/subagent-lane-launcher.js +53 -12
  189. package/dist/core/subagent-lane-launcher.js.map +1 -1
  190. package/dist/core/subagent-orchestration.d.ts +4 -17
  191. package/dist/core/subagent-orchestration.d.ts.map +1 -1
  192. package/dist/core/subagent-orchestration.js.map +1 -1
  193. package/dist/core/verified-run/broker.d.ts.map +1 -1
  194. package/dist/core/verified-run/broker.js +4 -1
  195. package/dist/core/verified-run/broker.js.map +1 -1
  196. package/dist/core/workload-permit-pool.d.ts +1 -1
  197. package/dist/core/workload-permit-pool.d.ts.map +1 -1
  198. package/dist/core/workload-permit-pool.js +3 -0
  199. package/dist/core/workload-permit-pool.js.map +1 -1
  200. package/dist/guardrails/strict-evidence-approval-adapter.d.ts +34 -0
  201. package/dist/guardrails/strict-evidence-approval-adapter.d.ts.map +1 -0
  202. package/dist/guardrails/strict-evidence-approval-adapter.js +70 -0
  203. package/dist/guardrails/strict-evidence-approval-adapter.js.map +1 -0
  204. package/dist/index.d.ts +3 -0
  205. package/dist/index.d.ts.map +1 -1
  206. package/dist/index.js +1 -0
  207. package/dist/index.js.map +1 -1
  208. package/dist/main.d.ts.map +1 -1
  209. package/dist/main.js +11 -18
  210. package/dist/main.js.map +1 -1
  211. package/dist/modes/acp/acp-agent.d.ts +24 -0
  212. package/dist/modes/acp/acp-agent.d.ts.map +1 -0
  213. package/dist/modes/acp/acp-agent.js +133 -0
  214. package/dist/modes/acp/acp-agent.js.map +1 -0
  215. package/dist/modes/acp/acp-mode.d.ts +4 -0
  216. package/dist/modes/acp/acp-mode.d.ts.map +1 -0
  217. package/dist/modes/acp/acp-mode.js +34 -0
  218. package/dist/modes/acp/acp-mode.js.map +1 -0
  219. package/dist/modes/acp/acp-session.d.ts +4 -0
  220. package/dist/modes/acp/acp-session.d.ts.map +1 -0
  221. package/dist/modes/acp/acp-session.js +76 -0
  222. package/dist/modes/acp/acp-session.js.map +1 -0
  223. package/dist/modes/acp/acp-transport.d.ts +5 -0
  224. package/dist/modes/acp/acp-transport.d.ts.map +1 -0
  225. package/dist/modes/acp/acp-transport.js +90 -0
  226. package/dist/modes/acp/acp-transport.js.map +1 -0
  227. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  228. package/dist/modes/interactive/interactive-mode.js +1 -0
  229. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  230. package/docs/audit-revalidation-2026-09-17.md +77 -0
  231. package/docs/devin-harness.md +33 -4
  232. package/docs/ecraf-normalization.md +83 -0
  233. package/docs/model-catalog-refresh.md +51 -1
  234. package/docs/models.md +1 -1
  235. package/docs/neo.md +134 -0
  236. package/docs/providers.md +72 -6
  237. package/docs/run-usage-ledger.md +56 -0
  238. package/docs/runtime-algorithms.md +44 -0
  239. package/docs/skills.md +1 -1
  240. package/docs/usage.md +2 -1
  241. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  242. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  243. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  244. package/examples/extensions/gondolin/package-lock.json +2 -2
  245. package/examples/extensions/gondolin/package.json +1 -1
  246. package/examples/extensions/sandbox/package-lock.json +2 -2
  247. package/examples/extensions/sandbox/package.json +1 -1
  248. package/examples/extensions/subagent/adaptive-agent-runtime.ts +14 -1
  249. package/examples/extensions/subagent/graph-result.ts +48 -0
  250. package/examples/extensions/subagent/index.ts +246 -111
  251. package/examples/extensions/subagent/managed-process-tree.ts +42 -0
  252. package/examples/extensions/subagent/managed-process.test.ts +24 -0
  253. package/examples/extensions/subagent/managed-process.ts +93 -112
  254. package/examples/extensions/subagent/subagent-runtime-types.ts +17 -1
  255. package/examples/extensions/subagent/subagent-stream.ts +161 -0
  256. package/examples/extensions/terminal-browser/README.md +54 -0
  257. package/examples/extensions/terminal-browser/bridge-protocol.ts +29 -0
  258. package/examples/extensions/terminal-browser/bridge.ts +219 -0
  259. package/examples/extensions/terminal-browser/browser-surface.ts +288 -0
  260. package/examples/extensions/terminal-browser/index.ts +215 -0
  261. package/examples/extensions/terminal-browser/placeholders.ts +49 -0
  262. package/examples/extensions/with-deps/package-lock.json +2 -2
  263. package/examples/extensions/with-deps/package.json +1 -1
  264. package/npm-shrinkwrap.json +18 -18
  265. package/package.json +8 -7
  266. package/resources/neo/skills/omk-browser/SKILL.md +32 -0
  267. package/resources/neo/skills/omk-code-review/SKILL.md +24 -0
  268. package/resources/neo/skills/omk-computeruse/SKILL.md +34 -0
  269. package/resources/neo/skills/omk-mcp-setup/SKILL.md +48 -0
  270. package/resources/neo/skills/omk-research/SKILL.md +24 -0
  271. package/resources/neo/skills/omk-site/SKILL.md +28 -0
@@ -14,10 +14,23 @@
14
14
  import { type StdioTransportOptions } from "./stdio-transport.ts";
15
15
  /** Protocol revision this client implements. */
16
16
  export declare const MCP_PROTOCOL_VERSION = "2025-06-18";
17
+ /**
18
+ * Protocol revisions this client accepts from a server. The handshake fails
19
+ * closed on any other value instead of guessing at forward compatibility.
20
+ */
21
+ export declare const SUPPORTED_PROTOCOL_VERSIONS: readonly string[];
17
22
  /** Default per-request deadline. */
18
23
  export declare const DEFAULT_REQUEST_TIMEOUT_MS = 30000;
19
24
  /** Default handshake deadline. Servers that install on first run need more room than a normal call. */
20
25
  export declare const DEFAULT_HANDSHAKE_TIMEOUT_MS = 60000;
26
+ /** Page ceiling for `tools/list` so a looping cursor cannot stall a session forever. */
27
+ export declare const MAX_LIST_PAGES = 128;
28
+ /** Tool ceiling across all `tools/list` pages for one server. */
29
+ export declare const MAX_LIST_TOOLS = 4096;
30
+ /** Overall `tools/list` deadline; per-request timeouts do not bound the total. */
31
+ export declare const LIST_TOOLS_TIMEOUT_MS = 120000;
32
+ /** In-flight request ceiling per server. */
33
+ export declare const MAX_PENDING_REQUESTS = 128;
21
34
  export interface McpToolSchema {
22
35
  readonly name: string;
23
36
  readonly description?: string;
@@ -28,12 +41,11 @@ export interface McpTextBlock {
28
41
  readonly type: "text";
29
42
  readonly text: string;
30
43
  }
31
- export interface McpImageBlock {
44
+ export type McpContentBlock = McpTextBlock | {
32
45
  readonly type: "image";
33
46
  readonly data: string;
34
47
  readonly mimeType: string;
35
- }
36
- export type McpContentBlock = McpTextBlock | McpImageBlock | {
48
+ } | {
37
49
  readonly type: string;
38
50
  readonly [key: string]: unknown;
39
51
  };
@@ -68,26 +80,33 @@ export declare class McpClient {
68
80
  private closed;
69
81
  private exitReason;
70
82
  private info;
83
+ private lastProtocolErrorReason;
71
84
  private readonly options;
72
85
  constructor(options: McpClientOptions);
73
86
  get name(): string;
74
87
  get serverInfo(): McpServerInfo;
75
88
  get ready(): boolean;
76
- /** Why the server is unusable, when it is. */
77
- get failure(): string | undefined;
78
89
  /**
79
90
  * Spawn the server and complete the MCP handshake. Safe to await once; a
80
91
  * second call after a failure re-throws the recorded reason rather than
81
92
  * silently reusing a dead process.
82
93
  */
83
94
  connect(): Promise<void>;
95
+ private assertInitializeResult;
84
96
  /**
85
97
  * Protocol-level liveness probe. Rejects when the server is closed, dead,
86
98
  * or fails to answer within `timeoutMs`, so callers can tell a silently
87
99
  * killed process apart from a genuinely connected one.
88
100
  */
89
101
  ping(timeoutMs?: number): Promise<void>;
90
- /** List the server's tools. Requires a completed handshake. */
102
+ /**
103
+ * List the server's tools. Requires a completed handshake.
104
+ *
105
+ * Pagination is bounded three ways — an overall deadline, a page count, and
106
+ * a tool count — and a repeated cursor is a protocol error, so a misbehaving
107
+ * server cannot keep the listing loop alive indefinitely. Duplicate tool
108
+ * names in one listing are rejected instead of silently overwritten.
109
+ */
91
110
  listTools(): Promise<McpToolSchema[]>;
92
111
  /**
93
112
  * Invoke a tool. A protocol-level failure rejects; a tool-level failure
@@ -97,6 +116,8 @@ export declare class McpClient {
97
116
  callTool(name: string, args: unknown, timeoutMs?: number): Promise<McpToolCallResult>;
98
117
  /** Terminate the server and reject anything still in flight. */
99
118
  close(): void;
119
+ /** Why the server is unusable, when it is; includes the latest decode failure. */
120
+ get failure(): string | undefined;
100
121
  private assertReady;
101
122
  private request;
102
123
  private handleMessage;
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../src/core/mcp/client.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Minimal MCP client: initialize handshake, `tools/list`, `tools/call`.\n *\n * Deliberately dependency-free. The upstream SDK carries a full server\n * implementation, resource/prompt/sampling surfaces, and its own transport\n * stack; OMK needs a client for three methods, so the client is ~200 lines of\n * request correlation over a transport this repo already owns.\n *\n * Fail-closed contract:\n * - Every request has a deadline. A hung server rejects, it does not stall a turn.\n * - A transport exit rejects every in-flight request with the server's stderr tail.\n * - Calling before a completed handshake throws instead of guessing.\n */\n\nimport {\n\tformatJsonRpcError,\n\tisJsonRpcResponse,\n\ttype JsonRpcId,\n\ttype JsonRpcMessage,\n\ttype JsonRpcResponse,\n} from \"./protocol.ts\";\nimport { McpStdioTransport, type StdioTransportOptions } from \"./stdio-transport.ts\";\n\n/** Protocol revision this client implements. */\nexport const MCP_PROTOCOL_VERSION = \"2025-06-18\";\n/** Default per-request deadline. */\nexport const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;\n/** Default handshake deadline. Servers that install on first run need more room than a normal call. */\nexport const DEFAULT_HANDSHAKE_TIMEOUT_MS = 60_000;\n\nexport interface McpToolSchema {\n\treadonly name: string;\n\treadonly description?: string;\n\treadonly inputSchema?: Record<string, unknown>;\n\treadonly title?: string;\n}\n\nexport interface McpTextBlock {\n\treadonly type: \"text\";\n\treadonly text: string;\n}\n\nexport interface McpImageBlock {\n\treadonly type: \"image\";\n\treadonly data: string;\n\treadonly mimeType: string;\n}\n\nexport type McpContentBlock = McpTextBlock | McpImageBlock | { readonly type: string; readonly [key: string]: unknown };\n\nexport interface McpToolCallResult {\n\treadonly content: readonly McpContentBlock[];\n\treadonly isError: boolean;\n\treadonly structuredContent?: unknown;\n}\n\nexport interface McpServerInfo {\n\treadonly name?: string;\n\treadonly version?: string;\n}\n\nexport interface McpClientOptions {\n\t/** Server label used in error messages. */\n\treadonly name: string;\n\treadonly transport: StdioTransportOptions;\n\treadonly requestTimeoutMs?: number;\n\treadonly handshakeTimeoutMs?: number;\n\t/** Client identity reported during the handshake. */\n\treadonly clientInfo?: { readonly name: string; readonly version: string };\n}\n\ninterface PendingRequest {\n\treadonly resolve: (result: unknown) => void;\n\treadonly reject: (error: Error) => void;\n\treadonly timer: ReturnType<typeof setTimeout>;\n\treadonly method: string;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nexport class McpClient {\n\tprivate readonly transport: McpStdioTransport;\n\tprivate readonly pending = new Map<JsonRpcId, PendingRequest>();\n\tprivate readonly requestTimeoutMs: number;\n\tprivate readonly handshakeTimeoutMs: number;\n\tprivate nextId = 1;\n\tprivate initialized = false;\n\tprivate closed = false;\n\tprivate exitReason: string | undefined;\n\tprivate info: McpServerInfo = {};\n\tprivate readonly options: McpClientOptions;\n\n\tconstructor(options: McpClientOptions) {\n\t\tthis.options = options;\n\t\tthis.requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;\n\t\tthis.handshakeTimeoutMs = options.handshakeTimeoutMs ?? DEFAULT_HANDSHAKE_TIMEOUT_MS;\n\t\tthis.transport = new McpStdioTransport(options.transport, {\n\t\t\tonMessage: (message) => this.handleMessage(message),\n\t\t\tonExit: ({ code, signal }) => this.handleExit(code, signal),\n\t\t});\n\t}\n\n\tget name(): string {\n\t\treturn this.options.name;\n\t}\n\n\tget serverInfo(): McpServerInfo {\n\t\treturn this.info;\n\t}\n\n\tget ready(): boolean {\n\t\treturn this.initialized && !this.closed;\n\t}\n\n\t/** Why the server is unusable, when it is. */\n\tget failure(): string | undefined {\n\t\treturn this.exitReason;\n\t}\n\n\t/**\n\t * Spawn the server and complete the MCP handshake. Safe to await once; a\n\t * second call after a failure re-throws the recorded reason rather than\n\t * silently reusing a dead process.\n\t */\n\tasync connect(): Promise<void> {\n\t\tif (this.closed) throw new Error(this.exitReason ?? `MCP server \"${this.options.name}\" is closed`);\n\t\tif (this.initialized) return;\n\t\tthis.transport.start();\n\n\t\tconst result = await this.request(\n\t\t\t\"initialize\",\n\t\t\t{\n\t\t\t\tprotocolVersion: MCP_PROTOCOL_VERSION,\n\t\t\t\tcapabilities: {},\n\t\t\t\tclientInfo: this.options.clientInfo ?? { name: \"omk\", version: \"0.0.0\" },\n\t\t\t},\n\t\t\tthis.handshakeTimeoutMs,\n\t\t);\n\t\tif (isRecord(result) && isRecord(result.serverInfo)) {\n\t\t\tconst { name, version } = result.serverInfo;\n\t\t\tthis.info = {\n\t\t\t\tname: typeof name === \"string\" ? name : undefined,\n\t\t\t\tversion: typeof version === \"string\" ? version : undefined,\n\t\t\t};\n\t\t}\n\t\tthis.initialized = true;\n\t\tthis.transport.send({ jsonrpc: \"2.0\", method: \"notifications/initialized\" });\n\t}\n\n\t/**\n\t * Protocol-level liveness probe. Rejects when the server is closed, dead,\n\t * or fails to answer within `timeoutMs`, so callers can tell a silently\n\t * killed process apart from a genuinely connected one.\n\t */\n\tasync ping(timeoutMs?: number): Promise<void> {\n\t\tthis.assertReady();\n\t\tawait this.request(\"ping\", {}, timeoutMs);\n\t}\n\n\t/** List the server's tools. Requires a completed handshake. */\n\tasync listTools(): Promise<McpToolSchema[]> {\n\t\tthis.assertReady();\n\t\tconst tools: McpToolSchema[] = [];\n\t\tlet cursor: string | undefined;\n\t\tdo {\n\t\t\tconst result = await this.request(\"tools/list\", cursor ? { cursor } : {});\n\t\t\tif (!isRecord(result) || !Array.isArray(result.tools)) return tools;\n\t\t\tfor (const raw of result.tools) {\n\t\t\t\tif (!isRecord(raw) || typeof raw.name !== \"string\" || raw.name.length === 0) continue;\n\t\t\t\ttools.push({\n\t\t\t\t\tname: raw.name,\n\t\t\t\t\tdescription: typeof raw.description === \"string\" ? raw.description : undefined,\n\t\t\t\t\ttitle: typeof raw.title === \"string\" ? raw.title : undefined,\n\t\t\t\t\tinputSchema: isRecord(raw.inputSchema) ? raw.inputSchema : undefined,\n\t\t\t\t});\n\t\t\t}\n\t\t\tcursor = typeof result.nextCursor === \"string\" && result.nextCursor.length > 0 ? result.nextCursor : undefined;\n\t\t} while (cursor);\n\t\treturn tools;\n\t}\n\n\t/**\n\t * Invoke a tool. A protocol-level failure rejects; a tool-level failure\n\t * resolves with `isError: true`, matching MCP semantics so the model sees\n\t * the server's own error text instead of a harness exception.\n\t */\n\tasync callTool(name: string, args: unknown, timeoutMs?: number): Promise<McpToolCallResult> {\n\t\tthis.assertReady();\n\t\tconst result = await this.request(\"tools/call\", { name, arguments: args ?? {} }, timeoutMs);\n\t\tif (!isRecord(result)) return { content: [], isError: false };\n\t\tconst content = Array.isArray(result.content) ? (result.content as McpContentBlock[]) : [];\n\t\treturn {\n\t\t\tcontent,\n\t\t\tisError: result.isError === true,\n\t\t\tstructuredContent: result.structuredContent,\n\t\t};\n\t}\n\n\t/** Terminate the server and reject anything still in flight. */\n\tclose(): void {\n\t\tif (this.closed) return;\n\t\tthis.transport.close();\n\t\tthis.handleExit(null, null);\n\t}\n\n\tprivate assertReady(): void {\n\t\tif (this.closed) throw new Error(this.exitReason ?? `MCP server \"${this.options.name}\" is closed`);\n\t\tif (!this.initialized) throw new Error(`MCP server \"${this.options.name}\" is not initialized`);\n\t}\n\n\tprivate request(method: string, params: unknown, timeoutMs?: number): Promise<unknown> {\n\t\tif (this.closed) {\n\t\t\treturn Promise.reject(new Error(this.exitReason ?? `MCP server \"${this.options.name}\" is closed`));\n\t\t}\n\t\tconst id = this.nextId++;\n\t\tconst effectiveTimeout = timeoutMs ?? this.requestTimeoutMs;\n\t\treturn new Promise<unknown>((resolve, reject) => {\n\t\t\tconst timer = setTimeout(() => {\n\t\t\t\tthis.pending.delete(id);\n\t\t\t\treject(new Error(`MCP server \"${this.options.name}\" timed out after ${effectiveTimeout}ms on ${method}`));\n\t\t\t}, effectiveTimeout);\n\t\t\ttimer.unref?.();\n\t\t\tthis.pending.set(id, { resolve, reject, timer, method });\n\n\t\t\tconst sent = this.transport.send({ jsonrpc: \"2.0\", id, method, params });\n\t\t\tif (!sent) {\n\t\t\t\tthis.pending.delete(id);\n\t\t\t\tclearTimeout(timer);\n\t\t\t\treject(new Error(`MCP server \"${this.options.name}\" is not writable (${method})`));\n\t\t\t}\n\t\t});\n\t}\n\n\tprivate handleMessage(message: JsonRpcMessage): void {\n\t\tif (!isJsonRpcResponse(message)) return; // Server-initiated requests/notifications are not used yet.\n\t\tconst response: JsonRpcResponse = message;\n\t\tconst pending = this.pending.get(response.id);\n\t\tif (!pending) return;\n\t\tthis.pending.delete(response.id);\n\t\tclearTimeout(pending.timer);\n\t\tif (response.error) {\n\t\t\tpending.reject(new Error(`MCP server \"${this.options.name}\": ${formatJsonRpcError(response.error)}`));\n\t\t\treturn;\n\t\t}\n\t\tpending.resolve(response.result);\n\t}\n\n\tprivate handleExit(code: number | null, signal: string | null): void {\n\t\tif (this.closed) return;\n\t\tthis.closed = true;\n\t\tthis.initialized = false;\n\t\tconst detail = signal ? `signal ${signal}` : code === null ? \"spawn failure\" : `exit code ${code}`;\n\t\tconst stderr = this.transport.stderr.trim();\n\t\tthis.exitReason = `MCP server \"${this.options.name}\" stopped (${detail})${stderr ? `: ${stderr}` : \"\"}`;\n\t\tconst reason = new Error(this.exitReason);\n\t\tfor (const [id, pending] of this.pending) {\n\t\t\tclearTimeout(pending.timer);\n\t\t\tthis.pending.delete(id);\n\t\t\tpending.reject(reason);\n\t\t}\n\t}\n}\n"]}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../src/core/mcp/client.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Minimal MCP client: initialize handshake, `tools/list`, `tools/call`.\n *\n * Deliberately dependency-free. The upstream SDK carries a full server\n * implementation, resource/prompt/sampling surfaces, and its own transport\n * stack; OMK needs a client for three methods, so the client is ~200 lines of\n * request correlation over a transport this repo already owns.\n *\n * Fail-closed contract:\n * - Every request has a deadline. A hung server rejects, it does not stall a turn.\n * - A transport exit rejects every in-flight request with the server's stderr tail.\n * - Calling before a completed handshake throws instead of guessing.\n */\n\nimport {\n\tformatJsonRpcError,\n\tisJsonRpcResponse,\n\ttype JsonRpcId,\n\ttype JsonRpcMessage,\n\ttype JsonRpcResponse,\n} from \"./protocol.ts\";\nimport { McpStdioTransport, type StdioTransportOptions } from \"./stdio-transport.ts\";\n\n/** Protocol revision this client implements. */\nexport const MCP_PROTOCOL_VERSION = \"2025-06-18\";\n/**\n * Protocol revisions this client accepts from a server. The handshake fails\n * closed on any other value instead of guessing at forward compatibility.\n */\nexport const SUPPORTED_PROTOCOL_VERSIONS: readonly string[] = [\"2024-11-05\", MCP_PROTOCOL_VERSION];\n/** Default per-request deadline. */\nexport const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;\n/** Default handshake deadline. Servers that install on first run need more room than a normal call. */\nexport const DEFAULT_HANDSHAKE_TIMEOUT_MS = 60_000;\n/** Page ceiling for `tools/list` so a looping cursor cannot stall a session forever. */\nexport const MAX_LIST_PAGES = 128;\n/** Tool ceiling across all `tools/list` pages for one server. */\nexport const MAX_LIST_TOOLS = 4096;\n/** Overall `tools/list` deadline; per-request timeouts do not bound the total. */\nexport const LIST_TOOLS_TIMEOUT_MS = 120_000;\n/** In-flight request ceiling per server. */\nexport const MAX_PENDING_REQUESTS = 128;\n\nexport interface McpToolSchema {\n\treadonly name: string;\n\treadonly description?: string;\n\treadonly inputSchema?: Record<string, unknown>;\n\treadonly title?: string;\n}\n\nexport interface McpTextBlock {\n\treadonly type: \"text\";\n\treadonly text: string;\n}\n\nexport type McpContentBlock =\n\t| McpTextBlock\n\t| { readonly type: \"image\"; readonly data: string; readonly mimeType: string }\n\t| { readonly type: string; readonly [key: string]: unknown };\n\nexport interface McpToolCallResult {\n\treadonly content: readonly McpContentBlock[];\n\treadonly isError: boolean;\n\treadonly structuredContent?: unknown;\n}\n\nexport interface McpServerInfo {\n\treadonly name?: string;\n\treadonly version?: string;\n}\n\nexport interface McpClientOptions {\n\t/** Server label used in error messages. */\n\treadonly name: string;\n\treadonly transport: StdioTransportOptions;\n\treadonly requestTimeoutMs?: number;\n\treadonly handshakeTimeoutMs?: number;\n\t/** Client identity reported during the handshake. */\n\treadonly clientInfo?: { readonly name: string; readonly version: string };\n}\n\ninterface PendingRequest {\n\treadonly resolve: (result: unknown) => void;\n\treadonly reject: (error: Error) => void;\n\treadonly timer: ReturnType<typeof setTimeout>;\n\treadonly method: string;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nexport class McpClient {\n\tprivate readonly transport: McpStdioTransport;\n\tprivate readonly pending = new Map<JsonRpcId, PendingRequest>();\n\tprivate readonly requestTimeoutMs: number;\n\tprivate readonly handshakeTimeoutMs: number;\n\tprivate nextId = 1;\n\tprivate initialized = false;\n\tprivate closed = false;\n\tprivate exitReason: string | undefined;\n\tprivate info: McpServerInfo = {};\n\tprivate lastProtocolErrorReason: string | undefined;\n\tprivate readonly options: McpClientOptions;\n\n\tconstructor(options: McpClientOptions) {\n\t\tthis.options = options;\n\t\tthis.requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;\n\t\tthis.handshakeTimeoutMs = options.handshakeTimeoutMs ?? DEFAULT_HANDSHAKE_TIMEOUT_MS;\n\t\tthis.transport = new McpStdioTransport(options.transport, {\n\t\t\tonMessage: (message) => this.handleMessage(message),\n\t\t\tonDecodeError: (reason) => {\n\t\t\t\tthis.lastProtocolErrorReason = reason;\n\t\t\t},\n\t\t\tonExit: ({ code, signal }) => this.handleExit(code, signal),\n\t\t});\n\t}\n\n\tget name(): string {\n\t\treturn this.options.name;\n\t}\n\n\tget serverInfo(): McpServerInfo {\n\t\treturn this.info;\n\t}\n\n\tget ready(): boolean {\n\t\treturn this.initialized && !this.closed;\n\t}\n\n\t/**\n\t * Spawn the server and complete the MCP handshake. Safe to await once; a\n\t * second call after a failure re-throws the recorded reason rather than\n\t * silently reusing a dead process.\n\t */\n\tasync connect(): Promise<void> {\n\t\tif (this.closed) throw new Error(this.exitReason ?? `MCP server \"${this.options.name}\" is closed`);\n\t\tif (this.initialized) return;\n\t\tthis.transport.start();\n\n\t\tconst result = await this.request(\n\t\t\t\"initialize\",\n\t\t\t{\n\t\t\t\tprotocolVersion: MCP_PROTOCOL_VERSION,\n\t\t\t\tcapabilities: {},\n\t\t\t\tclientInfo: this.options.clientInfo ?? { name: \"omk\", version: \"0.0.0\" },\n\t\t\t},\n\t\t\tthis.handshakeTimeoutMs,\n\t\t);\n\t\tthis.assertInitializeResult(result);\n\t\tthis.initialized = true;\n\t\tthis.transport.send({ jsonrpc: \"2.0\", method: \"notifications/initialized\" });\n\t}\n\n\t// The handshake is a negotiation, not a formality: reject a response that\n\t// does not carry a protocol version this client actually supports.\n\tprivate assertInitializeResult(result: unknown): void {\n\t\tif (!isRecord(result) || typeof result.protocolVersion !== \"string\") {\n\t\t\tthrow new Error(\n\t\t\t\t`MCP server \"${this.options.name}\" returned an invalid initialize result (missing protocolVersion)`,\n\t\t\t);\n\t\t}\n\t\tif (!SUPPORTED_PROTOCOL_VERSIONS.includes(result.protocolVersion)) {\n\t\t\tthrow new Error(\n\t\t\t\t`MCP server \"${this.options.name}\" requested unsupported protocol version \"${result.protocolVersion}\"`,\n\t\t\t);\n\t\t}\n\t\tif (isRecord(result.serverInfo)) {\n\t\t\tconst { name, version } = result.serverInfo;\n\t\t\tthis.info = {\n\t\t\t\tname: typeof name === \"string\" ? name : undefined,\n\t\t\t\tversion: typeof version === \"string\" ? version : undefined,\n\t\t\t};\n\t\t}\n\t}\n\n\t/**\n\t * Protocol-level liveness probe. Rejects when the server is closed, dead,\n\t * or fails to answer within `timeoutMs`, so callers can tell a silently\n\t * killed process apart from a genuinely connected one.\n\t */\n\tasync ping(timeoutMs?: number): Promise<void> {\n\t\tthis.assertReady();\n\t\tawait this.request(\"ping\", {}, timeoutMs);\n\t}\n\n\t/**\n\t * List the server's tools. Requires a completed handshake.\n\t *\n\t * Pagination is bounded three ways — an overall deadline, a page count, and\n\t * a tool count — and a repeated cursor is a protocol error, so a misbehaving\n\t * server cannot keep the listing loop alive indefinitely. Duplicate tool\n\t * names in one listing are rejected instead of silently overwritten.\n\t */\n\tasync listTools(): Promise<McpToolSchema[]> {\n\t\tthis.assertReady();\n\t\tconst tools: McpToolSchema[] = [];\n\t\tconst seenNames = new Set<string>();\n\t\tconst seenCursors = new Set<string>();\n\t\tconst deadline = Date.now() + LIST_TOOLS_TIMEOUT_MS;\n\t\tlet cursor: string | undefined;\n\t\tconst fail = (what: string): never => {\n\t\t\tthrow new Error(`MCP server \"${this.options.name}\" ${what}`);\n\t\t};\n\t\tfor (let page = 0; page < MAX_LIST_PAGES; page++) {\n\t\t\tconst remainingMs = deadline - Date.now();\n\t\t\tif (remainingMs <= 0) fail(`tools/list exceeded ${LIST_TOOLS_TIMEOUT_MS}ms`);\n\t\t\tconst result = await this.request(\n\t\t\t\t\"tools/list\",\n\t\t\t\tcursor ? { cursor } : {},\n\t\t\t\tMath.min(remainingMs, this.requestTimeoutMs),\n\t\t\t);\n\t\t\tconst record = isRecord(result) ? result : fail(\"returned an invalid tools/list result\");\n\t\t\tconst rawTools = Array.isArray(record.tools) ? record.tools : fail(\"returned an invalid tools/list result\");\n\t\t\tfor (const raw of rawTools) {\n\t\t\t\tif (!isRecord(raw) || typeof raw.name !== \"string\" || raw.name.length === 0) continue;\n\t\t\t\tif (seenNames.has(raw.name)) fail(`listed duplicate tool \"${raw.name}\"`);\n\t\t\t\tseenNames.add(raw.name);\n\t\t\t\ttools.push({\n\t\t\t\t\tname: raw.name,\n\t\t\t\t\tdescription: typeof raw.description === \"string\" ? raw.description : undefined,\n\t\t\t\t\ttitle: typeof raw.title === \"string\" ? raw.title : undefined,\n\t\t\t\t\tinputSchema: isRecord(raw.inputSchema) ? raw.inputSchema : undefined,\n\t\t\t\t});\n\t\t\t\tif (tools.length > MAX_LIST_TOOLS) fail(`exceeded ${MAX_LIST_TOOLS} tools`);\n\t\t\t}\n\t\t\tconst next =\n\t\t\t\ttypeof record.nextCursor === \"string\" && record.nextCursor.length > 0 ? record.nextCursor : undefined;\n\t\t\tif (!next) return tools;\n\t\t\tif (seenCursors.has(next)) fail(\"repeated tools/list cursor\");\n\t\t\tseenCursors.add(next);\n\t\t\tcursor = next;\n\t\t}\n\t\treturn fail(`exceeded ${MAX_LIST_PAGES} tools/list pages`);\n\t}\n\n\t/**\n\t * Invoke a tool. A protocol-level failure rejects; a tool-level failure\n\t * resolves with `isError: true`, matching MCP semantics so the model sees\n\t * the server's own error text instead of a harness exception.\n\t */\n\tasync callTool(name: string, args: unknown, timeoutMs?: number): Promise<McpToolCallResult> {\n\t\tthis.assertReady();\n\t\tconst result = await this.request(\"tools/call\", { name, arguments: args ?? {} }, timeoutMs);\n\t\t// A non-object result is a protocol violation, not an empty success —\n\t\t// normalizing it would hide a broken server from the caller.\n\t\tif (!isRecord(result)) {\n\t\t\tthrow new Error(`MCP server \"${this.options.name}\" returned an invalid tools/call result`);\n\t\t}\n\t\tconst content = Array.isArray(result.content) ? (result.content as McpContentBlock[]) : [];\n\t\treturn {\n\t\t\tcontent,\n\t\t\tisError: result.isError === true,\n\t\t\tstructuredContent: result.structuredContent,\n\t\t};\n\t}\n\n\t/** Terminate the server and reject anything still in flight. */\n\tclose(): void {\n\t\tif (this.closed) return;\n\t\tthis.transport.close();\n\t\tthis.handleExit(null, null);\n\t}\n\n\t/** Why the server is unusable, when it is; includes the latest decode failure. */\n\tget failure(): string | undefined {\n\t\treturn this.exitReason ?? this.lastProtocolErrorReason;\n\t}\n\n\tprivate assertReady(): void {\n\t\tif (this.closed) throw new Error(this.exitReason ?? `MCP server \"${this.options.name}\" is closed`);\n\t\tif (!this.initialized) throw new Error(`MCP server \"${this.options.name}\" is not initialized`);\n\t}\n\n\tprivate request(method: string, params: unknown, timeoutMs?: number): Promise<unknown> {\n\t\tif (this.closed) {\n\t\t\treturn Promise.reject(new Error(this.exitReason ?? `MCP server \"${this.options.name}\" is closed`));\n\t\t}\n\t\tif (this.pending.size >= MAX_PENDING_REQUESTS) {\n\t\t\treturn Promise.reject(\n\t\t\t\tnew Error(`MCP server \"${this.options.name}\" has ${MAX_PENDING_REQUESTS} pending requests (${method})`),\n\t\t\t);\n\t\t}\n\t\tconst id = this.nextId++;\n\t\tconst effectiveTimeout = timeoutMs ?? this.requestTimeoutMs;\n\t\treturn new Promise<unknown>((resolve, reject) => {\n\t\t\tconst timer = setTimeout(() => {\n\t\t\t\tthis.pending.delete(id);\n\t\t\t\treject(new Error(`MCP server \"${this.options.name}\" timed out after ${effectiveTimeout}ms on ${method}`));\n\t\t\t}, effectiveTimeout);\n\t\t\ttimer.unref?.();\n\t\t\tthis.pending.set(id, { resolve, reject, timer, method });\n\t\t\tif (!this.transport.send({ jsonrpc: \"2.0\", id, method, params })) {\n\t\t\t\tthis.pending.delete(id);\n\t\t\t\tclearTimeout(timer);\n\t\t\t\treject(new Error(`MCP server \"${this.options.name}\" is not writable (${method})`));\n\t\t\t}\n\t\t});\n\t}\n\n\tprivate handleMessage(message: JsonRpcMessage): void {\n\t\tif (!isJsonRpcResponse(message)) return; // Server-initiated requests/notifications are not used yet.\n\t\tconst response: JsonRpcResponse = message;\n\t\tconst pending = this.pending.get(response.id);\n\t\tif (!pending) return;\n\t\tthis.pending.delete(response.id);\n\t\tclearTimeout(pending.timer);\n\t\tif (response.error) {\n\t\t\tpending.reject(new Error(`MCP server \"${this.options.name}\": ${formatJsonRpcError(response.error)}`));\n\t\t\treturn;\n\t\t}\n\t\tpending.resolve(response.result);\n\t}\n\n\tprivate handleExit(code: number | null, signal: string | null): void {\n\t\tif (this.closed) return;\n\t\tthis.closed = true;\n\t\tthis.initialized = false;\n\t\tconst detail = signal ? `signal ${signal}` : code === null ? \"spawn failure\" : `exit code ${code}`;\n\t\tconst stderr = this.transport.stderr.trim();\n\t\tthis.exitReason = `MCP server \"${this.options.name}\" stopped (${detail})${stderr ? `: ${stderr}` : \"\"}`;\n\t\tconst reason = new Error(this.exitReason);\n\t\tfor (const [id, pending] of this.pending) {\n\t\t\tclearTimeout(pending.timer);\n\t\t\tthis.pending.delete(id);\n\t\t\tpending.reject(reason);\n\t\t}\n\t}\n}\n"]}
@@ -15,10 +15,23 @@ import { formatJsonRpcError, isJsonRpcResponse, } from "./protocol.js";
15
15
  import { McpStdioTransport } from "./stdio-transport.js";
16
16
  /** Protocol revision this client implements. */
17
17
  export const MCP_PROTOCOL_VERSION = "2025-06-18";
18
+ /**
19
+ * Protocol revisions this client accepts from a server. The handshake fails
20
+ * closed on any other value instead of guessing at forward compatibility.
21
+ */
22
+ export const SUPPORTED_PROTOCOL_VERSIONS = ["2024-11-05", MCP_PROTOCOL_VERSION];
18
23
  /** Default per-request deadline. */
19
24
  export const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
20
25
  /** Default handshake deadline. Servers that install on first run need more room than a normal call. */
21
26
  export const DEFAULT_HANDSHAKE_TIMEOUT_MS = 60_000;
27
+ /** Page ceiling for `tools/list` so a looping cursor cannot stall a session forever. */
28
+ export const MAX_LIST_PAGES = 128;
29
+ /** Tool ceiling across all `tools/list` pages for one server. */
30
+ export const MAX_LIST_TOOLS = 4096;
31
+ /** Overall `tools/list` deadline; per-request timeouts do not bound the total. */
32
+ export const LIST_TOOLS_TIMEOUT_MS = 120_000;
33
+ /** In-flight request ceiling per server. */
34
+ export const MAX_PENDING_REQUESTS = 128;
22
35
  function isRecord(value) {
23
36
  return typeof value === "object" && value !== null && !Array.isArray(value);
24
37
  }
@@ -32,6 +45,7 @@ export class McpClient {
32
45
  closed = false;
33
46
  exitReason;
34
47
  info = {};
48
+ lastProtocolErrorReason;
35
49
  options;
36
50
  constructor(options) {
37
51
  this.options = options;
@@ -39,6 +53,9 @@ export class McpClient {
39
53
  this.handshakeTimeoutMs = options.handshakeTimeoutMs ?? DEFAULT_HANDSHAKE_TIMEOUT_MS;
40
54
  this.transport = new McpStdioTransport(options.transport, {
41
55
  onMessage: (message) => this.handleMessage(message),
56
+ onDecodeError: (reason) => {
57
+ this.lastProtocolErrorReason = reason;
58
+ },
42
59
  onExit: ({ code, signal }) => this.handleExit(code, signal),
43
60
  });
44
61
  }
@@ -51,10 +68,6 @@ export class McpClient {
51
68
  get ready() {
52
69
  return this.initialized && !this.closed;
53
70
  }
54
- /** Why the server is unusable, when it is. */
55
- get failure() {
56
- return this.exitReason;
57
- }
58
71
  /**
59
72
  * Spawn the server and complete the MCP handshake. Safe to await once; a
60
73
  * second call after a failure re-throws the recorded reason rather than
@@ -71,15 +84,26 @@ export class McpClient {
71
84
  capabilities: {},
72
85
  clientInfo: this.options.clientInfo ?? { name: "omk", version: "0.0.0" },
73
86
  }, this.handshakeTimeoutMs);
74
- if (isRecord(result) && isRecord(result.serverInfo)) {
87
+ this.assertInitializeResult(result);
88
+ this.initialized = true;
89
+ this.transport.send({ jsonrpc: "2.0", method: "notifications/initialized" });
90
+ }
91
+ // The handshake is a negotiation, not a formality: reject a response that
92
+ // does not carry a protocol version this client actually supports.
93
+ assertInitializeResult(result) {
94
+ if (!isRecord(result) || typeof result.protocolVersion !== "string") {
95
+ throw new Error(`MCP server "${this.options.name}" returned an invalid initialize result (missing protocolVersion)`);
96
+ }
97
+ if (!SUPPORTED_PROTOCOL_VERSIONS.includes(result.protocolVersion)) {
98
+ throw new Error(`MCP server "${this.options.name}" requested unsupported protocol version "${result.protocolVersion}"`);
99
+ }
100
+ if (isRecord(result.serverInfo)) {
75
101
  const { name, version } = result.serverInfo;
76
102
  this.info = {
77
103
  name: typeof name === "string" ? name : undefined,
78
104
  version: typeof version === "string" ? version : undefined,
79
105
  };
80
106
  }
81
- this.initialized = true;
82
- this.transport.send({ jsonrpc: "2.0", method: "notifications/initialized" });
83
107
  }
84
108
  /**
85
109
  * Protocol-level liveness probe. Rejects when the server is closed, dead,
@@ -90,28 +114,55 @@ export class McpClient {
90
114
  this.assertReady();
91
115
  await this.request("ping", {}, timeoutMs);
92
116
  }
93
- /** List the server's tools. Requires a completed handshake. */
117
+ /**
118
+ * List the server's tools. Requires a completed handshake.
119
+ *
120
+ * Pagination is bounded three ways — an overall deadline, a page count, and
121
+ * a tool count — and a repeated cursor is a protocol error, so a misbehaving
122
+ * server cannot keep the listing loop alive indefinitely. Duplicate tool
123
+ * names in one listing are rejected instead of silently overwritten.
124
+ */
94
125
  async listTools() {
95
126
  this.assertReady();
96
127
  const tools = [];
128
+ const seenNames = new Set();
129
+ const seenCursors = new Set();
130
+ const deadline = Date.now() + LIST_TOOLS_TIMEOUT_MS;
97
131
  let cursor;
98
- do {
99
- const result = await this.request("tools/list", cursor ? { cursor } : {});
100
- if (!isRecord(result) || !Array.isArray(result.tools))
101
- return tools;
102
- for (const raw of result.tools) {
132
+ const fail = (what) => {
133
+ throw new Error(`MCP server "${this.options.name}" ${what}`);
134
+ };
135
+ for (let page = 0; page < MAX_LIST_PAGES; page++) {
136
+ const remainingMs = deadline - Date.now();
137
+ if (remainingMs <= 0)
138
+ fail(`tools/list exceeded ${LIST_TOOLS_TIMEOUT_MS}ms`);
139
+ const result = await this.request("tools/list", cursor ? { cursor } : {}, Math.min(remainingMs, this.requestTimeoutMs));
140
+ const record = isRecord(result) ? result : fail("returned an invalid tools/list result");
141
+ const rawTools = Array.isArray(record.tools) ? record.tools : fail("returned an invalid tools/list result");
142
+ for (const raw of rawTools) {
103
143
  if (!isRecord(raw) || typeof raw.name !== "string" || raw.name.length === 0)
104
144
  continue;
145
+ if (seenNames.has(raw.name))
146
+ fail(`listed duplicate tool "${raw.name}"`);
147
+ seenNames.add(raw.name);
105
148
  tools.push({
106
149
  name: raw.name,
107
150
  description: typeof raw.description === "string" ? raw.description : undefined,
108
151
  title: typeof raw.title === "string" ? raw.title : undefined,
109
152
  inputSchema: isRecord(raw.inputSchema) ? raw.inputSchema : undefined,
110
153
  });
154
+ if (tools.length > MAX_LIST_TOOLS)
155
+ fail(`exceeded ${MAX_LIST_TOOLS} tools`);
111
156
  }
112
- cursor = typeof result.nextCursor === "string" && result.nextCursor.length > 0 ? result.nextCursor : undefined;
113
- } while (cursor);
114
- return tools;
157
+ const next = typeof record.nextCursor === "string" && record.nextCursor.length > 0 ? record.nextCursor : undefined;
158
+ if (!next)
159
+ return tools;
160
+ if (seenCursors.has(next))
161
+ fail("repeated tools/list cursor");
162
+ seenCursors.add(next);
163
+ cursor = next;
164
+ }
165
+ return fail(`exceeded ${MAX_LIST_PAGES} tools/list pages`);
115
166
  }
116
167
  /**
117
168
  * Invoke a tool. A protocol-level failure rejects; a tool-level failure
@@ -121,8 +172,11 @@ export class McpClient {
121
172
  async callTool(name, args, timeoutMs) {
122
173
  this.assertReady();
123
174
  const result = await this.request("tools/call", { name, arguments: args ?? {} }, timeoutMs);
124
- if (!isRecord(result))
125
- return { content: [], isError: false };
175
+ // A non-object result is a protocol violation, not an empty success —
176
+ // normalizing it would hide a broken server from the caller.
177
+ if (!isRecord(result)) {
178
+ throw new Error(`MCP server "${this.options.name}" returned an invalid tools/call result`);
179
+ }
126
180
  const content = Array.isArray(result.content) ? result.content : [];
127
181
  return {
128
182
  content,
@@ -137,6 +191,10 @@ export class McpClient {
137
191
  this.transport.close();
138
192
  this.handleExit(null, null);
139
193
  }
194
+ /** Why the server is unusable, when it is; includes the latest decode failure. */
195
+ get failure() {
196
+ return this.exitReason ?? this.lastProtocolErrorReason;
197
+ }
140
198
  assertReady() {
141
199
  if (this.closed)
142
200
  throw new Error(this.exitReason ?? `MCP server "${this.options.name}" is closed`);
@@ -147,6 +205,9 @@ export class McpClient {
147
205
  if (this.closed) {
148
206
  return Promise.reject(new Error(this.exitReason ?? `MCP server "${this.options.name}" is closed`));
149
207
  }
208
+ if (this.pending.size >= MAX_PENDING_REQUESTS) {
209
+ return Promise.reject(new Error(`MCP server "${this.options.name}" has ${MAX_PENDING_REQUESTS} pending requests (${method})`));
210
+ }
150
211
  const id = this.nextId++;
151
212
  const effectiveTimeout = timeoutMs ?? this.requestTimeoutMs;
152
213
  return new Promise((resolve, reject) => {
@@ -156,8 +217,7 @@ export class McpClient {
156
217
  }, effectiveTimeout);
157
218
  timer.unref?.();
158
219
  this.pending.set(id, { resolve, reject, timer, method });
159
- const sent = this.transport.send({ jsonrpc: "2.0", id, method, params });
160
- if (!sent) {
220
+ if (!this.transport.send({ jsonrpc: "2.0", id, method, params })) {
161
221
  this.pending.delete(id);
162
222
  clearTimeout(timer);
163
223
  reject(new Error(`MCP server "${this.options.name}" is not writable (${method})`));
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","sourceRoot":"","sources":["../../../src/core/mcp/client.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Minimal MCP client: initialize handshake, `tools/list`, `tools/call`.\n *\n * Deliberately dependency-free. The upstream SDK carries a full server\n * implementation, resource/prompt/sampling surfaces, and its own transport\n * stack; OMK needs a client for three methods, so the client is ~200 lines of\n * request correlation over a transport this repo already owns.\n *\n * Fail-closed contract:\n * - Every request has a deadline. A hung server rejects, it does not stall a turn.\n * - A transport exit rejects every in-flight request with the server's stderr tail.\n * - Calling before a completed handshake throws instead of guessing.\n */\n\nimport {\n\tformatJsonRpcError,\n\tisJsonRpcResponse,\n\ttype JsonRpcId,\n\ttype JsonRpcMessage,\n\ttype JsonRpcResponse,\n} from \"./protocol.ts\";\nimport { McpStdioTransport, type StdioTransportOptions } from \"./stdio-transport.ts\";\n\n/** Protocol revision this client implements. */\nexport const MCP_PROTOCOL_VERSION = \"2025-06-18\";\n/** Default per-request deadline. */\nexport const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;\n/** Default handshake deadline. Servers that install on first run need more room than a normal call. */\nexport const DEFAULT_HANDSHAKE_TIMEOUT_MS = 60_000;\n\nexport interface McpToolSchema {\n\treadonly name: string;\n\treadonly description?: string;\n\treadonly inputSchema?: Record<string, unknown>;\n\treadonly title?: string;\n}\n\nexport interface McpTextBlock {\n\treadonly type: \"text\";\n\treadonly text: string;\n}\n\nexport interface McpImageBlock {\n\treadonly type: \"image\";\n\treadonly data: string;\n\treadonly mimeType: string;\n}\n\nexport type McpContentBlock = McpTextBlock | McpImageBlock | { readonly type: string; readonly [key: string]: unknown };\n\nexport interface McpToolCallResult {\n\treadonly content: readonly McpContentBlock[];\n\treadonly isError: boolean;\n\treadonly structuredContent?: unknown;\n}\n\nexport interface McpServerInfo {\n\treadonly name?: string;\n\treadonly version?: string;\n}\n\nexport interface McpClientOptions {\n\t/** Server label used in error messages. */\n\treadonly name: string;\n\treadonly transport: StdioTransportOptions;\n\treadonly requestTimeoutMs?: number;\n\treadonly handshakeTimeoutMs?: number;\n\t/** Client identity reported during the handshake. */\n\treadonly clientInfo?: { readonly name: string; readonly version: string };\n}\n\ninterface PendingRequest {\n\treadonly resolve: (result: unknown) => void;\n\treadonly reject: (error: Error) => void;\n\treadonly timer: ReturnType<typeof setTimeout>;\n\treadonly method: string;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nexport class McpClient {\n\tprivate readonly transport: McpStdioTransport;\n\tprivate readonly pending = new Map<JsonRpcId, PendingRequest>();\n\tprivate readonly requestTimeoutMs: number;\n\tprivate readonly handshakeTimeoutMs: number;\n\tprivate nextId = 1;\n\tprivate initialized = false;\n\tprivate closed = false;\n\tprivate exitReason: string | undefined;\n\tprivate info: McpServerInfo = {};\n\tprivate readonly options: McpClientOptions;\n\n\tconstructor(options: McpClientOptions) {\n\t\tthis.options = options;\n\t\tthis.requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;\n\t\tthis.handshakeTimeoutMs = options.handshakeTimeoutMs ?? DEFAULT_HANDSHAKE_TIMEOUT_MS;\n\t\tthis.transport = new McpStdioTransport(options.transport, {\n\t\t\tonMessage: (message) => this.handleMessage(message),\n\t\t\tonExit: ({ code, signal }) => this.handleExit(code, signal),\n\t\t});\n\t}\n\n\tget name(): string {\n\t\treturn this.options.name;\n\t}\n\n\tget serverInfo(): McpServerInfo {\n\t\treturn this.info;\n\t}\n\n\tget ready(): boolean {\n\t\treturn this.initialized && !this.closed;\n\t}\n\n\t/** Why the server is unusable, when it is. */\n\tget failure(): string | undefined {\n\t\treturn this.exitReason;\n\t}\n\n\t/**\n\t * Spawn the server and complete the MCP handshake. Safe to await once; a\n\t * second call after a failure re-throws the recorded reason rather than\n\t * silently reusing a dead process.\n\t */\n\tasync connect(): Promise<void> {\n\t\tif (this.closed) throw new Error(this.exitReason ?? `MCP server \"${this.options.name}\" is closed`);\n\t\tif (this.initialized) return;\n\t\tthis.transport.start();\n\n\t\tconst result = await this.request(\n\t\t\t\"initialize\",\n\t\t\t{\n\t\t\t\tprotocolVersion: MCP_PROTOCOL_VERSION,\n\t\t\t\tcapabilities: {},\n\t\t\t\tclientInfo: this.options.clientInfo ?? { name: \"omk\", version: \"0.0.0\" },\n\t\t\t},\n\t\t\tthis.handshakeTimeoutMs,\n\t\t);\n\t\tif (isRecord(result) && isRecord(result.serverInfo)) {\n\t\t\tconst { name, version } = result.serverInfo;\n\t\t\tthis.info = {\n\t\t\t\tname: typeof name === \"string\" ? name : undefined,\n\t\t\t\tversion: typeof version === \"string\" ? version : undefined,\n\t\t\t};\n\t\t}\n\t\tthis.initialized = true;\n\t\tthis.transport.send({ jsonrpc: \"2.0\", method: \"notifications/initialized\" });\n\t}\n\n\t/**\n\t * Protocol-level liveness probe. Rejects when the server is closed, dead,\n\t * or fails to answer within `timeoutMs`, so callers can tell a silently\n\t * killed process apart from a genuinely connected one.\n\t */\n\tasync ping(timeoutMs?: number): Promise<void> {\n\t\tthis.assertReady();\n\t\tawait this.request(\"ping\", {}, timeoutMs);\n\t}\n\n\t/** List the server's tools. Requires a completed handshake. */\n\tasync listTools(): Promise<McpToolSchema[]> {\n\t\tthis.assertReady();\n\t\tconst tools: McpToolSchema[] = [];\n\t\tlet cursor: string | undefined;\n\t\tdo {\n\t\t\tconst result = await this.request(\"tools/list\", cursor ? { cursor } : {});\n\t\t\tif (!isRecord(result) || !Array.isArray(result.tools)) return tools;\n\t\t\tfor (const raw of result.tools) {\n\t\t\t\tif (!isRecord(raw) || typeof raw.name !== \"string\" || raw.name.length === 0) continue;\n\t\t\t\ttools.push({\n\t\t\t\t\tname: raw.name,\n\t\t\t\t\tdescription: typeof raw.description === \"string\" ? raw.description : undefined,\n\t\t\t\t\ttitle: typeof raw.title === \"string\" ? raw.title : undefined,\n\t\t\t\t\tinputSchema: isRecord(raw.inputSchema) ? raw.inputSchema : undefined,\n\t\t\t\t});\n\t\t\t}\n\t\t\tcursor = typeof result.nextCursor === \"string\" && result.nextCursor.length > 0 ? result.nextCursor : undefined;\n\t\t} while (cursor);\n\t\treturn tools;\n\t}\n\n\t/**\n\t * Invoke a tool. A protocol-level failure rejects; a tool-level failure\n\t * resolves with `isError: true`, matching MCP semantics so the model sees\n\t * the server's own error text instead of a harness exception.\n\t */\n\tasync callTool(name: string, args: unknown, timeoutMs?: number): Promise<McpToolCallResult> {\n\t\tthis.assertReady();\n\t\tconst result = await this.request(\"tools/call\", { name, arguments: args ?? {} }, timeoutMs);\n\t\tif (!isRecord(result)) return { content: [], isError: false };\n\t\tconst content = Array.isArray(result.content) ? (result.content as McpContentBlock[]) : [];\n\t\treturn {\n\t\t\tcontent,\n\t\t\tisError: result.isError === true,\n\t\t\tstructuredContent: result.structuredContent,\n\t\t};\n\t}\n\n\t/** Terminate the server and reject anything still in flight. */\n\tclose(): void {\n\t\tif (this.closed) return;\n\t\tthis.transport.close();\n\t\tthis.handleExit(null, null);\n\t}\n\n\tprivate assertReady(): void {\n\t\tif (this.closed) throw new Error(this.exitReason ?? `MCP server \"${this.options.name}\" is closed`);\n\t\tif (!this.initialized) throw new Error(`MCP server \"${this.options.name}\" is not initialized`);\n\t}\n\n\tprivate request(method: string, params: unknown, timeoutMs?: number): Promise<unknown> {\n\t\tif (this.closed) {\n\t\t\treturn Promise.reject(new Error(this.exitReason ?? `MCP server \"${this.options.name}\" is closed`));\n\t\t}\n\t\tconst id = this.nextId++;\n\t\tconst effectiveTimeout = timeoutMs ?? this.requestTimeoutMs;\n\t\treturn new Promise<unknown>((resolve, reject) => {\n\t\t\tconst timer = setTimeout(() => {\n\t\t\t\tthis.pending.delete(id);\n\t\t\t\treject(new Error(`MCP server \"${this.options.name}\" timed out after ${effectiveTimeout}ms on ${method}`));\n\t\t\t}, effectiveTimeout);\n\t\t\ttimer.unref?.();\n\t\t\tthis.pending.set(id, { resolve, reject, timer, method });\n\n\t\t\tconst sent = this.transport.send({ jsonrpc: \"2.0\", id, method, params });\n\t\t\tif (!sent) {\n\t\t\t\tthis.pending.delete(id);\n\t\t\t\tclearTimeout(timer);\n\t\t\t\treject(new Error(`MCP server \"${this.options.name}\" is not writable (${method})`));\n\t\t\t}\n\t\t});\n\t}\n\n\tprivate handleMessage(message: JsonRpcMessage): void {\n\t\tif (!isJsonRpcResponse(message)) return; // Server-initiated requests/notifications are not used yet.\n\t\tconst response: JsonRpcResponse = message;\n\t\tconst pending = this.pending.get(response.id);\n\t\tif (!pending) return;\n\t\tthis.pending.delete(response.id);\n\t\tclearTimeout(pending.timer);\n\t\tif (response.error) {\n\t\t\tpending.reject(new Error(`MCP server \"${this.options.name}\": ${formatJsonRpcError(response.error)}`));\n\t\t\treturn;\n\t\t}\n\t\tpending.resolve(response.result);\n\t}\n\n\tprivate handleExit(code: number | null, signal: string | null): void {\n\t\tif (this.closed) return;\n\t\tthis.closed = true;\n\t\tthis.initialized = false;\n\t\tconst detail = signal ? `signal ${signal}` : code === null ? \"spawn failure\" : `exit code ${code}`;\n\t\tconst stderr = this.transport.stderr.trim();\n\t\tthis.exitReason = `MCP server \"${this.options.name}\" stopped (${detail})${stderr ? `: ${stderr}` : \"\"}`;\n\t\tconst reason = new Error(this.exitReason);\n\t\tfor (const [id, pending] of this.pending) {\n\t\t\tclearTimeout(pending.timer);\n\t\t\tthis.pending.delete(id);\n\t\t\tpending.reject(reason);\n\t\t}\n\t}\n}\n"]}
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../../../src/core/mcp/client.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Minimal MCP client: initialize handshake, `tools/list`, `tools/call`.\n *\n * Deliberately dependency-free. The upstream SDK carries a full server\n * implementation, resource/prompt/sampling surfaces, and its own transport\n * stack; OMK needs a client for three methods, so the client is ~200 lines of\n * request correlation over a transport this repo already owns.\n *\n * Fail-closed contract:\n * - Every request has a deadline. A hung server rejects, it does not stall a turn.\n * - A transport exit rejects every in-flight request with the server's stderr tail.\n * - Calling before a completed handshake throws instead of guessing.\n */\n\nimport {\n\tformatJsonRpcError,\n\tisJsonRpcResponse,\n\ttype JsonRpcId,\n\ttype JsonRpcMessage,\n\ttype JsonRpcResponse,\n} from \"./protocol.ts\";\nimport { McpStdioTransport, type StdioTransportOptions } from \"./stdio-transport.ts\";\n\n/** Protocol revision this client implements. */\nexport const MCP_PROTOCOL_VERSION = \"2025-06-18\";\n/**\n * Protocol revisions this client accepts from a server. The handshake fails\n * closed on any other value instead of guessing at forward compatibility.\n */\nexport const SUPPORTED_PROTOCOL_VERSIONS: readonly string[] = [\"2024-11-05\", MCP_PROTOCOL_VERSION];\n/** Default per-request deadline. */\nexport const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;\n/** Default handshake deadline. Servers that install on first run need more room than a normal call. */\nexport const DEFAULT_HANDSHAKE_TIMEOUT_MS = 60_000;\n/** Page ceiling for `tools/list` so a looping cursor cannot stall a session forever. */\nexport const MAX_LIST_PAGES = 128;\n/** Tool ceiling across all `tools/list` pages for one server. */\nexport const MAX_LIST_TOOLS = 4096;\n/** Overall `tools/list` deadline; per-request timeouts do not bound the total. */\nexport const LIST_TOOLS_TIMEOUT_MS = 120_000;\n/** In-flight request ceiling per server. */\nexport const MAX_PENDING_REQUESTS = 128;\n\nexport interface McpToolSchema {\n\treadonly name: string;\n\treadonly description?: string;\n\treadonly inputSchema?: Record<string, unknown>;\n\treadonly title?: string;\n}\n\nexport interface McpTextBlock {\n\treadonly type: \"text\";\n\treadonly text: string;\n}\n\nexport type McpContentBlock =\n\t| McpTextBlock\n\t| { readonly type: \"image\"; readonly data: string; readonly mimeType: string }\n\t| { readonly type: string; readonly [key: string]: unknown };\n\nexport interface McpToolCallResult {\n\treadonly content: readonly McpContentBlock[];\n\treadonly isError: boolean;\n\treadonly structuredContent?: unknown;\n}\n\nexport interface McpServerInfo {\n\treadonly name?: string;\n\treadonly version?: string;\n}\n\nexport interface McpClientOptions {\n\t/** Server label used in error messages. */\n\treadonly name: string;\n\treadonly transport: StdioTransportOptions;\n\treadonly requestTimeoutMs?: number;\n\treadonly handshakeTimeoutMs?: number;\n\t/** Client identity reported during the handshake. */\n\treadonly clientInfo?: { readonly name: string; readonly version: string };\n}\n\ninterface PendingRequest {\n\treadonly resolve: (result: unknown) => void;\n\treadonly reject: (error: Error) => void;\n\treadonly timer: ReturnType<typeof setTimeout>;\n\treadonly method: string;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nexport class McpClient {\n\tprivate readonly transport: McpStdioTransport;\n\tprivate readonly pending = new Map<JsonRpcId, PendingRequest>();\n\tprivate readonly requestTimeoutMs: number;\n\tprivate readonly handshakeTimeoutMs: number;\n\tprivate nextId = 1;\n\tprivate initialized = false;\n\tprivate closed = false;\n\tprivate exitReason: string | undefined;\n\tprivate info: McpServerInfo = {};\n\tprivate lastProtocolErrorReason: string | undefined;\n\tprivate readonly options: McpClientOptions;\n\n\tconstructor(options: McpClientOptions) {\n\t\tthis.options = options;\n\t\tthis.requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;\n\t\tthis.handshakeTimeoutMs = options.handshakeTimeoutMs ?? DEFAULT_HANDSHAKE_TIMEOUT_MS;\n\t\tthis.transport = new McpStdioTransport(options.transport, {\n\t\t\tonMessage: (message) => this.handleMessage(message),\n\t\t\tonDecodeError: (reason) => {\n\t\t\t\tthis.lastProtocolErrorReason = reason;\n\t\t\t},\n\t\t\tonExit: ({ code, signal }) => this.handleExit(code, signal),\n\t\t});\n\t}\n\n\tget name(): string {\n\t\treturn this.options.name;\n\t}\n\n\tget serverInfo(): McpServerInfo {\n\t\treturn this.info;\n\t}\n\n\tget ready(): boolean {\n\t\treturn this.initialized && !this.closed;\n\t}\n\n\t/**\n\t * Spawn the server and complete the MCP handshake. Safe to await once; a\n\t * second call after a failure re-throws the recorded reason rather than\n\t * silently reusing a dead process.\n\t */\n\tasync connect(): Promise<void> {\n\t\tif (this.closed) throw new Error(this.exitReason ?? `MCP server \"${this.options.name}\" is closed`);\n\t\tif (this.initialized) return;\n\t\tthis.transport.start();\n\n\t\tconst result = await this.request(\n\t\t\t\"initialize\",\n\t\t\t{\n\t\t\t\tprotocolVersion: MCP_PROTOCOL_VERSION,\n\t\t\t\tcapabilities: {},\n\t\t\t\tclientInfo: this.options.clientInfo ?? { name: \"omk\", version: \"0.0.0\" },\n\t\t\t},\n\t\t\tthis.handshakeTimeoutMs,\n\t\t);\n\t\tthis.assertInitializeResult(result);\n\t\tthis.initialized = true;\n\t\tthis.transport.send({ jsonrpc: \"2.0\", method: \"notifications/initialized\" });\n\t}\n\n\t// The handshake is a negotiation, not a formality: reject a response that\n\t// does not carry a protocol version this client actually supports.\n\tprivate assertInitializeResult(result: unknown): void {\n\t\tif (!isRecord(result) || typeof result.protocolVersion !== \"string\") {\n\t\t\tthrow new Error(\n\t\t\t\t`MCP server \"${this.options.name}\" returned an invalid initialize result (missing protocolVersion)`,\n\t\t\t);\n\t\t}\n\t\tif (!SUPPORTED_PROTOCOL_VERSIONS.includes(result.protocolVersion)) {\n\t\t\tthrow new Error(\n\t\t\t\t`MCP server \"${this.options.name}\" requested unsupported protocol version \"${result.protocolVersion}\"`,\n\t\t\t);\n\t\t}\n\t\tif (isRecord(result.serverInfo)) {\n\t\t\tconst { name, version } = result.serverInfo;\n\t\t\tthis.info = {\n\t\t\t\tname: typeof name === \"string\" ? name : undefined,\n\t\t\t\tversion: typeof version === \"string\" ? version : undefined,\n\t\t\t};\n\t\t}\n\t}\n\n\t/**\n\t * Protocol-level liveness probe. Rejects when the server is closed, dead,\n\t * or fails to answer within `timeoutMs`, so callers can tell a silently\n\t * killed process apart from a genuinely connected one.\n\t */\n\tasync ping(timeoutMs?: number): Promise<void> {\n\t\tthis.assertReady();\n\t\tawait this.request(\"ping\", {}, timeoutMs);\n\t}\n\n\t/**\n\t * List the server's tools. Requires a completed handshake.\n\t *\n\t * Pagination is bounded three ways — an overall deadline, a page count, and\n\t * a tool count — and a repeated cursor is a protocol error, so a misbehaving\n\t * server cannot keep the listing loop alive indefinitely. Duplicate tool\n\t * names in one listing are rejected instead of silently overwritten.\n\t */\n\tasync listTools(): Promise<McpToolSchema[]> {\n\t\tthis.assertReady();\n\t\tconst tools: McpToolSchema[] = [];\n\t\tconst seenNames = new Set<string>();\n\t\tconst seenCursors = new Set<string>();\n\t\tconst deadline = Date.now() + LIST_TOOLS_TIMEOUT_MS;\n\t\tlet cursor: string | undefined;\n\t\tconst fail = (what: string): never => {\n\t\t\tthrow new Error(`MCP server \"${this.options.name}\" ${what}`);\n\t\t};\n\t\tfor (let page = 0; page < MAX_LIST_PAGES; page++) {\n\t\t\tconst remainingMs = deadline - Date.now();\n\t\t\tif (remainingMs <= 0) fail(`tools/list exceeded ${LIST_TOOLS_TIMEOUT_MS}ms`);\n\t\t\tconst result = await this.request(\n\t\t\t\t\"tools/list\",\n\t\t\t\tcursor ? { cursor } : {},\n\t\t\t\tMath.min(remainingMs, this.requestTimeoutMs),\n\t\t\t);\n\t\t\tconst record = isRecord(result) ? result : fail(\"returned an invalid tools/list result\");\n\t\t\tconst rawTools = Array.isArray(record.tools) ? record.tools : fail(\"returned an invalid tools/list result\");\n\t\t\tfor (const raw of rawTools) {\n\t\t\t\tif (!isRecord(raw) || typeof raw.name !== \"string\" || raw.name.length === 0) continue;\n\t\t\t\tif (seenNames.has(raw.name)) fail(`listed duplicate tool \"${raw.name}\"`);\n\t\t\t\tseenNames.add(raw.name);\n\t\t\t\ttools.push({\n\t\t\t\t\tname: raw.name,\n\t\t\t\t\tdescription: typeof raw.description === \"string\" ? raw.description : undefined,\n\t\t\t\t\ttitle: typeof raw.title === \"string\" ? raw.title : undefined,\n\t\t\t\t\tinputSchema: isRecord(raw.inputSchema) ? raw.inputSchema : undefined,\n\t\t\t\t});\n\t\t\t\tif (tools.length > MAX_LIST_TOOLS) fail(`exceeded ${MAX_LIST_TOOLS} tools`);\n\t\t\t}\n\t\t\tconst next =\n\t\t\t\ttypeof record.nextCursor === \"string\" && record.nextCursor.length > 0 ? record.nextCursor : undefined;\n\t\t\tif (!next) return tools;\n\t\t\tif (seenCursors.has(next)) fail(\"repeated tools/list cursor\");\n\t\t\tseenCursors.add(next);\n\t\t\tcursor = next;\n\t\t}\n\t\treturn fail(`exceeded ${MAX_LIST_PAGES} tools/list pages`);\n\t}\n\n\t/**\n\t * Invoke a tool. A protocol-level failure rejects; a tool-level failure\n\t * resolves with `isError: true`, matching MCP semantics so the model sees\n\t * the server's own error text instead of a harness exception.\n\t */\n\tasync callTool(name: string, args: unknown, timeoutMs?: number): Promise<McpToolCallResult> {\n\t\tthis.assertReady();\n\t\tconst result = await this.request(\"tools/call\", { name, arguments: args ?? {} }, timeoutMs);\n\t\t// A non-object result is a protocol violation, not an empty success —\n\t\t// normalizing it would hide a broken server from the caller.\n\t\tif (!isRecord(result)) {\n\t\t\tthrow new Error(`MCP server \"${this.options.name}\" returned an invalid tools/call result`);\n\t\t}\n\t\tconst content = Array.isArray(result.content) ? (result.content as McpContentBlock[]) : [];\n\t\treturn {\n\t\t\tcontent,\n\t\t\tisError: result.isError === true,\n\t\t\tstructuredContent: result.structuredContent,\n\t\t};\n\t}\n\n\t/** Terminate the server and reject anything still in flight. */\n\tclose(): void {\n\t\tif (this.closed) return;\n\t\tthis.transport.close();\n\t\tthis.handleExit(null, null);\n\t}\n\n\t/** Why the server is unusable, when it is; includes the latest decode failure. */\n\tget failure(): string | undefined {\n\t\treturn this.exitReason ?? this.lastProtocolErrorReason;\n\t}\n\n\tprivate assertReady(): void {\n\t\tif (this.closed) throw new Error(this.exitReason ?? `MCP server \"${this.options.name}\" is closed`);\n\t\tif (!this.initialized) throw new Error(`MCP server \"${this.options.name}\" is not initialized`);\n\t}\n\n\tprivate request(method: string, params: unknown, timeoutMs?: number): Promise<unknown> {\n\t\tif (this.closed) {\n\t\t\treturn Promise.reject(new Error(this.exitReason ?? `MCP server \"${this.options.name}\" is closed`));\n\t\t}\n\t\tif (this.pending.size >= MAX_PENDING_REQUESTS) {\n\t\t\treturn Promise.reject(\n\t\t\t\tnew Error(`MCP server \"${this.options.name}\" has ${MAX_PENDING_REQUESTS} pending requests (${method})`),\n\t\t\t);\n\t\t}\n\t\tconst id = this.nextId++;\n\t\tconst effectiveTimeout = timeoutMs ?? this.requestTimeoutMs;\n\t\treturn new Promise<unknown>((resolve, reject) => {\n\t\t\tconst timer = setTimeout(() => {\n\t\t\t\tthis.pending.delete(id);\n\t\t\t\treject(new Error(`MCP server \"${this.options.name}\" timed out after ${effectiveTimeout}ms on ${method}`));\n\t\t\t}, effectiveTimeout);\n\t\t\ttimer.unref?.();\n\t\t\tthis.pending.set(id, { resolve, reject, timer, method });\n\t\t\tif (!this.transport.send({ jsonrpc: \"2.0\", id, method, params })) {\n\t\t\t\tthis.pending.delete(id);\n\t\t\t\tclearTimeout(timer);\n\t\t\t\treject(new Error(`MCP server \"${this.options.name}\" is not writable (${method})`));\n\t\t\t}\n\t\t});\n\t}\n\n\tprivate handleMessage(message: JsonRpcMessage): void {\n\t\tif (!isJsonRpcResponse(message)) return; // Server-initiated requests/notifications are not used yet.\n\t\tconst response: JsonRpcResponse = message;\n\t\tconst pending = this.pending.get(response.id);\n\t\tif (!pending) return;\n\t\tthis.pending.delete(response.id);\n\t\tclearTimeout(pending.timer);\n\t\tif (response.error) {\n\t\t\tpending.reject(new Error(`MCP server \"${this.options.name}\": ${formatJsonRpcError(response.error)}`));\n\t\t\treturn;\n\t\t}\n\t\tpending.resolve(response.result);\n\t}\n\n\tprivate handleExit(code: number | null, signal: string | null): void {\n\t\tif (this.closed) return;\n\t\tthis.closed = true;\n\t\tthis.initialized = false;\n\t\tconst detail = signal ? `signal ${signal}` : code === null ? \"spawn failure\" : `exit code ${code}`;\n\t\tconst stderr = this.transport.stderr.trim();\n\t\tthis.exitReason = `MCP server \"${this.options.name}\" stopped (${detail})${stderr ? `: ${stderr}` : \"\"}`;\n\t\tconst reason = new Error(this.exitReason);\n\t\tfor (const [id, pending] of this.pending) {\n\t\t\tclearTimeout(pending.timer);\n\t\t\tthis.pending.delete(id);\n\t\t\tpending.reject(reason);\n\t\t}\n\t}\n}\n"]}
@@ -34,6 +34,8 @@ export interface McpServerStatus {
34
34
  /** Failure reason when `state` is `failed`. Never contains configured env values. */
35
35
  readonly error?: string;
36
36
  readonly serverVersion?: string;
37
+ /** Tool names excluded because their descriptions carried prompt-injection payloads. */
38
+ readonly quarantinedTools?: readonly string[];
37
39
  }
38
40
  export interface McpManagerOptions {
39
41
  readonly servers: readonly McpServerConfig[];
@@ -62,7 +64,14 @@ export declare class McpManager {
62
64
  listToolDefinitions(): Promise<ToolDefinition<TSchema, McpToolDetails>[]>;
63
65
  /** Connect a single server by name. Returns its status either way. */
64
66
  connect(name: string): Promise<McpServerStatus>;
65
- /** Terminate every connected server. Idempotent. */
67
+ /**
68
+ * Terminate every connected or connecting server. Idempotent.
69
+ *
70
+ * Bumping the generation invalidates any in-flight connect before its late
71
+ * completion can publish tools or flip the state back to `ready`, and the
72
+ * attempt's not-yet-published client is closed alongside the live one so no
73
+ * spawned process outlives the close.
74
+ */
66
75
  close(): void;
67
76
  /**
68
77
  * Verify that `ready` servers are actually alive with a protocol ping. A
@@ -1 +1 @@
1
- {"version":3,"file":"manager.d.ts","sourceRoot":"","sources":["../../../src/core/mcp/manager.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAC7D,OAAO,EAAE,SAAS,EAAE,KAAK,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC/D,OAAO,EAA2B,KAAK,cAAc,EAAE,MAAM,YAAY,CAAC;AAE1E,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,YAAY,GAAG,OAAO,GAAG,QAAQ,CAAC;AAExE,MAAM,WAAW,eAAe;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,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,+DAA+D;IAC/D,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IACnC,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;CACrC;AAED,MAAM,WAAW,eAAe;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,cAAc,CAAC;IAC/B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,qFAAqF;IACrF,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;CAChC;AAED,MAAM,WAAW,iBAAiB;IACjC,QAAQ,CAAC,OAAO,EAAE,SAAS,eAAe,EAAE,CAAC;IAC7C,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,UAAU,CAAC,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IAC1E,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,qFAAqF;IACrF,QAAQ,CAAC,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,gBAAgB,KAAK,SAAS,CAAC;CACjE;AAWD,qBAAa,UAAU;IACtB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAoC;IAC7D,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAoB;IAE5C,YAAY,OAAO,EAAE,iBAAiB,EAMrC;IAED,uDAAuD;IACvD,IAAI,WAAW,IAAI,MAAM,EAAE,CAE1B;IAED,6EAA6E;IAC7E,MAAM,IAAI,eAAe,EAAE,CAQ1B;IAED;;;;OAIG;IACG,mBAAmB,IAAI,OAAO,CAAC,cAAc,CAAC,OAAO,EAAE,cAAc,CAAC,EAAE,CAAC,CAY9E;IAED,sEAAsE;IAChE,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAapD;IAED,oDAAoD;IACpD,KAAK,IAAI,IAAI,CAOZ;IAED;;;;;;;;OAQG;IACG,WAAW,CAAC,OAAO,CAAC,EAAE;QAAE,aAAa,CAAC,EAAE,MAAM,CAAC;QAAC,eAAe,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC,CAa7G;YAEa,WAAW;IAazB,OAAO,CAAC,eAAe;YAiBT,cAAc;CAkC5B","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.d.ts","sourceRoot":"","sources":["../../../src/core/mcp/manager.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAE7D,OAAO,EAAE,SAAS,EAAE,KAAK,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC/D,OAAO,EAA2B,KAAK,cAAc,EAAE,MAAM,YAAY,CAAC;AAE1E,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,YAAY,GAAG,OAAO,GAAG,QAAQ,CAAC;AAExE,MAAM,WAAW,eAAe;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,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,+DAA+D;IAC/D,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IACnC,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;CACrC;AAED,MAAM,WAAW,eAAe;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,cAAc,CAAC;IAC/B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,qFAAqF;IACrF,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,wFAAwF;IACxF,QAAQ,CAAC,gBAAgB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC9C;AAED,MAAM,WAAW,iBAAiB;IACjC,QAAQ,CAAC,OAAO,EAAE,SAAS,eAAe,EAAE,CAAC;IAC7C,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,UAAU,CAAC,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IAC1E,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,qFAAqF;IACrF,QAAQ,CAAC,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,gBAAgB,KAAK,SAAS,CAAC;CACjE;AAiBD,qBAAa,UAAU;IACtB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAoC;IAC7D,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAoB;IAE5C,YAAY,OAAO,EAAE,iBAAiB,EAMrC;IAED,uDAAuD;IACvD,IAAI,WAAW,IAAI,MAAM,EAAE,CAE1B;IAED,6EAA6E;IAC7E,MAAM,IAAI,eAAe,EAAE,CAS1B;IAED;;;;OAIG;IACG,mBAAmB,IAAI,OAAO,CAAC,cAAc,CAAC,OAAO,EAAE,cAAc,CAAC,EAAE,CAAC,CAY9E;IAED,sEAAsE;IAChE,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAcpD;IAED;;;;;;;OAOG;IACH,KAAK,IAAI,IAAI,CAWZ;IAED;;;;;;;;OAQG;IACG,WAAW,CAAC,OAAO,CAAC,EAAE;QAAE,aAAa,CAAC,EAAE,MAAM,CAAC;QAAC,eAAe,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC,CAa7G;YAEa,WAAW;IAmBzB,OAAO,CAAC,eAAe;YAqBT,cAAc;CAiE5B","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"]}