open-multi-agent-kit 0.95.0 → 0.95.2

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 (281) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +1 -1
  3. package/dist/approvals/approval-receipt-store.d.ts +18 -0
  4. package/dist/approvals/approval-receipt-store.d.ts.map +1 -0
  5. package/dist/approvals/approval-receipt-store.js +169 -0
  6. package/dist/approvals/approval-receipt-store.js.map +1 -0
  7. package/dist/approvals/approval-receipt.d.ts +43 -0
  8. package/dist/approvals/approval-receipt.d.ts.map +1 -0
  9. package/dist/approvals/approval-receipt.js +208 -0
  10. package/dist/approvals/approval-receipt.js.map +1 -0
  11. package/dist/approvals/approval-verifier.d.ts +17 -0
  12. package/dist/approvals/approval-verifier.d.ts.map +1 -0
  13. package/dist/approvals/approval-verifier.js +23 -0
  14. package/dist/approvals/approval-verifier.js.map +1 -0
  15. package/dist/approvals/plannotator-approval-bridge.d.ts +58 -0
  16. package/dist/approvals/plannotator-approval-bridge.d.ts.map +1 -0
  17. package/dist/approvals/plannotator-approval-bridge.js +169 -0
  18. package/dist/approvals/plannotator-approval-bridge.js.map +1 -0
  19. package/dist/cli/args.d.ts.map +1 -1
  20. package/dist/cli/args.js +2 -0
  21. package/dist/cli/args.js.map +1 -1
  22. package/dist/commands/package-doctor-cli.d.ts +13 -0
  23. package/dist/commands/package-doctor-cli.d.ts.map +1 -0
  24. package/dist/commands/package-doctor-cli.js +80 -0
  25. package/dist/commands/package-doctor-cli.js.map +1 -0
  26. package/dist/commands/router-feedback-cli.d.ts +11 -0
  27. package/dist/commands/router-feedback-cli.d.ts.map +1 -0
  28. package/dist/commands/router-feedback-cli.js +99 -0
  29. package/dist/commands/router-feedback-cli.js.map +1 -0
  30. package/dist/commands/stats-cli.d.ts +20 -0
  31. package/dist/commands/stats-cli.d.ts.map +1 -0
  32. package/dist/commands/stats-cli.js +99 -0
  33. package/dist/commands/stats-cli.js.map +1 -0
  34. package/dist/core/agent-session-runtime.d.ts.map +1 -1
  35. package/dist/core/agent-session-runtime.js +4 -1
  36. package/dist/core/agent-session-runtime.js.map +1 -1
  37. package/dist/core/agent-session.d.ts +97 -29
  38. package/dist/core/agent-session.d.ts.map +1 -1
  39. package/dist/core/agent-session.js +583 -199
  40. package/dist/core/agent-session.js.map +1 -1
  41. package/dist/core/bash-executor.d.ts.map +1 -1
  42. package/dist/core/bash-executor.js +8 -2
  43. package/dist/core/bash-executor.js.map +1 -1
  44. package/dist/core/compaction/transaction.d.ts +9 -0
  45. package/dist/core/compaction/transaction.d.ts.map +1 -1
  46. package/dist/core/compaction/transaction.js +17 -2
  47. package/dist/core/compaction/transaction.js.map +1 -1
  48. package/dist/core/context-budget-governor-v2.d.ts +1 -1
  49. package/dist/core/context-budget-governor-v2.d.ts.map +1 -1
  50. package/dist/core/context-budget-governor-v2.js +1 -1
  51. package/dist/core/context-budget-governor-v2.js.map +1 -1
  52. package/dist/core/context-budget-system-prompt.d.ts.map +1 -1
  53. package/dist/core/context-budget-system-prompt.js +18 -2
  54. package/dist/core/context-budget-system-prompt.js.map +1 -1
  55. package/dist/core/context-budget-v2-cache-disk.d.ts +109 -0
  56. package/dist/core/context-budget-v2-cache-disk.d.ts.map +1 -0
  57. package/dist/core/context-budget-v2-cache-disk.js +343 -0
  58. package/dist/core/context-budget-v2-cache-disk.js.map +1 -0
  59. package/dist/core/context-budget-v2-cache-keys.d.ts +22 -0
  60. package/dist/core/context-budget-v2-cache-keys.d.ts.map +1 -1
  61. package/dist/core/context-budget-v2-cache-keys.js +4 -4
  62. package/dist/core/context-budget-v2-cache-keys.js.map +1 -1
  63. package/dist/core/context-budget-v2-cache.d.ts +1 -0
  64. package/dist/core/context-budget-v2-cache.d.ts.map +1 -1
  65. package/dist/core/context-budget-v2-cache.js +1 -0
  66. package/dist/core/context-budget-v2-cache.js.map +1 -1
  67. package/dist/core/context-budget-v2-plan-cache-keys.d.ts.map +1 -1
  68. package/dist/core/context-budget-v2-plan-cache-keys.js +39 -5
  69. package/dist/core/context-budget-v2-plan-cache-keys.js.map +1 -1
  70. package/dist/core/context-budget-v2-planner.d.ts.map +1 -1
  71. package/dist/core/context-budget-v2-planner.js +17 -14
  72. package/dist/core/context-budget-v2-planner.js.map +1 -1
  73. package/dist/core/context-budget-v2-types.d.ts +1 -0
  74. package/dist/core/context-budget-v2-types.d.ts.map +1 -1
  75. package/dist/core/context-budget-v2-types.js.map +1 -1
  76. package/dist/core/extensions/loader.d.ts.map +1 -1
  77. package/dist/core/extensions/loader.js +45 -22
  78. package/dist/core/extensions/loader.js.bak-sharedjiti-20260812-111246 +553 -0
  79. package/dist/core/extensions/loader.js.map +1 -1
  80. package/dist/core/extensions/types.d.ts +4 -0
  81. package/dist/core/extensions/types.d.ts.map +1 -1
  82. package/dist/core/extensions/types.js.map +1 -1
  83. package/dist/core/headroom.d.ts.map +1 -1
  84. package/dist/core/headroom.js +7 -2
  85. package/dist/core/headroom.js.map +1 -1
  86. package/dist/core/mcp/client.d.ts +105 -0
  87. package/dist/core/mcp/client.d.ts.map +1 -0
  88. package/dist/core/mcp/client.js +198 -0
  89. package/dist/core/mcp/client.js.map +1 -0
  90. package/dist/core/mcp/config.d.ts +23 -0
  91. package/dist/core/mcp/config.d.ts.map +1 -0
  92. package/dist/core/mcp/config.js +89 -0
  93. package/dist/core/mcp/config.js.map +1 -0
  94. package/dist/core/mcp/index.d.ts +15 -0
  95. package/dist/core/mcp/index.d.ts.map +1 -0
  96. package/dist/core/mcp/index.js +15 -0
  97. package/dist/core/mcp/index.js.map +1 -0
  98. package/dist/core/mcp/manager.d.ts +84 -0
  99. package/dist/core/mcp/manager.d.ts.map +1 -0
  100. package/dist/core/mcp/manager.js +173 -0
  101. package/dist/core/mcp/manager.js.map +1 -0
  102. package/dist/core/mcp/protocol.d.ts +59 -0
  103. package/dist/core/mcp/protocol.d.ts.map +1 -0
  104. package/dist/core/mcp/protocol.js +103 -0
  105. package/dist/core/mcp/protocol.js.map +1 -0
  106. package/dist/core/mcp/stdio-transport.d.ts +63 -0
  107. package/dist/core/mcp/stdio-transport.d.ts.map +1 -0
  108. package/dist/core/mcp/stdio-transport.js +132 -0
  109. package/dist/core/mcp/stdio-transport.js.map +1 -0
  110. package/dist/core/mcp/tools.d.ts +47 -0
  111. package/dist/core/mcp/tools.d.ts.map +1 -0
  112. package/dist/core/mcp/tools.js +102 -0
  113. package/dist/core/mcp/tools.js.map +1 -0
  114. package/dist/core/package-archive.d.ts +3 -0
  115. package/dist/core/package-archive.d.ts.map +1 -0
  116. package/dist/core/package-archive.js +161 -0
  117. package/dist/core/package-archive.js.map +1 -0
  118. package/dist/core/package-doctor-source-scan.d.ts +13 -0
  119. package/dist/core/package-doctor-source-scan.d.ts.map +1 -0
  120. package/dist/core/package-doctor-source-scan.js +124 -0
  121. package/dist/core/package-doctor-source-scan.js.map +1 -0
  122. package/dist/core/package-doctor.d.ts +41 -0
  123. package/dist/core/package-doctor.d.ts.map +1 -0
  124. package/dist/core/package-doctor.js +145 -0
  125. package/dist/core/package-doctor.js.map +1 -0
  126. package/dist/core/package-manager.d.ts +10 -1
  127. package/dist/core/package-manager.d.ts.map +1 -1
  128. package/dist/core/package-manager.js +175 -42
  129. package/dist/core/package-manager.js.map +1 -1
  130. package/dist/core/package-manifest.d.ts +26 -0
  131. package/dist/core/package-manifest.d.ts.map +1 -0
  132. package/dist/core/package-manifest.js +85 -0
  133. package/dist/core/package-manifest.js.map +1 -0
  134. package/dist/core/pi-compat.d.ts +5 -0
  135. package/dist/core/pi-compat.d.ts.map +1 -0
  136. package/dist/core/pi-compat.js +18 -0
  137. package/dist/core/pi-compat.js.map +1 -0
  138. package/dist/core/reasoning-router-bias.d.ts +10 -14
  139. package/dist/core/reasoning-router-bias.d.ts.map +1 -1
  140. package/dist/core/reasoning-router-bias.js +22 -15
  141. package/dist/core/reasoning-router-bias.js.map +1 -1
  142. package/dist/core/reasoning-router-v4.d.ts +9 -1
  143. package/dist/core/reasoning-router-v4.d.ts.map +1 -1
  144. package/dist/core/reasoning-router-v4.js +20 -5
  145. package/dist/core/reasoning-router-v4.js.map +1 -1
  146. package/dist/core/redaction.d.ts +2 -5
  147. package/dist/core/redaction.d.ts.map +1 -1
  148. package/dist/core/redaction.js +14 -1
  149. package/dist/core/redaction.js.map +1 -1
  150. package/dist/core/repository-learning-scope.d.ts +10 -0
  151. package/dist/core/repository-learning-scope.d.ts.map +1 -0
  152. package/dist/core/repository-learning-scope.js +42 -0
  153. package/dist/core/repository-learning-scope.js.map +1 -0
  154. package/dist/core/resource-loader.d.ts.map +1 -1
  155. package/dist/core/resource-loader.js +16 -1
  156. package/dist/core/resource-loader.js.map +1 -1
  157. package/dist/core/router-feedback-collector.d.ts +2 -1
  158. package/dist/core/router-feedback-collector.d.ts.map +1 -1
  159. package/dist/core/router-feedback-collector.js +2 -2
  160. package/dist/core/router-feedback-collector.js.map +1 -1
  161. package/dist/core/runtime-provenance.d.ts +54 -0
  162. package/dist/core/runtime-provenance.d.ts.map +1 -0
  163. package/dist/core/runtime-provenance.js +115 -0
  164. package/dist/core/runtime-provenance.js.map +1 -0
  165. package/dist/core/sandbox/backend.d.ts.map +1 -1
  166. package/dist/core/sandbox/backend.js +39 -21
  167. package/dist/core/sandbox/backend.js.map +1 -1
  168. package/dist/core/sandbox/default-policy.d.ts +4 -5
  169. package/dist/core/sandbox/default-policy.d.ts.map +1 -1
  170. package/dist/core/sandbox/default-policy.js +9 -11
  171. package/dist/core/sandbox/default-policy.js.map +1 -1
  172. package/dist/core/session-bash-runtime.d.ts +4 -4
  173. package/dist/core/session-bash-runtime.d.ts.map +1 -1
  174. package/dist/core/session-bash-runtime.js +6 -5
  175. package/dist/core/session-bash-runtime.js.map +1 -1
  176. package/dist/core/session-compaction-service.d.ts.map +1 -1
  177. package/dist/core/session-compaction-service.js +7 -7
  178. package/dist/core/session-compaction-service.js.map +1 -1
  179. package/dist/core/session-manager.d.ts +4 -1
  180. package/dist/core/session-manager.d.ts.map +1 -1
  181. package/dist/core/session-manager.js +47 -7
  182. package/dist/core/session-manager.js.map +1 -1
  183. package/dist/core/settings-manager.d.ts +7 -4
  184. package/dist/core/settings-manager.d.ts.map +1 -1
  185. package/dist/core/settings-manager.js +11 -3
  186. package/dist/core/settings-manager.js.map +1 -1
  187. package/dist/core/system-prompt.d.ts +9 -1
  188. package/dist/core/system-prompt.d.ts.map +1 -1
  189. package/dist/core/system-prompt.js +10 -4
  190. package/dist/core/system-prompt.js.map +1 -1
  191. package/dist/core/tools/bash.d.ts +2 -2
  192. package/dist/core/tools/bash.d.ts.map +1 -1
  193. package/dist/core/tools/bash.js +2 -1
  194. package/dist/core/tools/bash.js.map +1 -1
  195. package/dist/core/tools/tool-definition-wrapper.d.ts.map +1 -1
  196. package/dist/core/tools/tool-definition-wrapper.js +15 -0
  197. package/dist/core/tools/tool-definition-wrapper.js.map +1 -1
  198. package/dist/core/turn-metrics.d.ts +135 -0
  199. package/dist/core/turn-metrics.d.ts.map +1 -0
  200. package/dist/core/turn-metrics.js +261 -0
  201. package/dist/core/turn-metrics.js.map +1 -0
  202. package/dist/core/verified-bash-runtime.d.ts.map +1 -1
  203. package/dist/core/verified-bash-runtime.js +10 -4
  204. package/dist/core/verified-bash-runtime.js.map +1 -1
  205. package/dist/guardrails/replay-ledger-lock-owner.d.ts +7 -0
  206. package/dist/guardrails/replay-ledger-lock-owner.d.ts.map +1 -1
  207. package/dist/guardrails/replay-ledger-lock-owner.js +39 -0
  208. package/dist/guardrails/replay-ledger-lock-owner.js.map +1 -1
  209. package/dist/index.d.ts +6 -1
  210. package/dist/index.d.ts.map +1 -1
  211. package/dist/index.js +6 -1
  212. package/dist/index.js.map +1 -1
  213. package/dist/main.d.ts.map +1 -1
  214. package/dist/main.js +18 -0
  215. package/dist/main.js.map +1 -1
  216. package/dist/modes/interactive/components/control-panel-layout.d.ts.map +1 -1
  217. package/dist/modes/interactive/components/control-panel-layout.js +2 -2
  218. package/dist/modes/interactive/components/control-panel-layout.js.map +1 -1
  219. package/dist/modes/interactive/components/footer.d.ts +3 -0
  220. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  221. package/dist/modes/interactive/components/footer.js +40 -12
  222. package/dist/modes/interactive/components/footer.js.map +1 -1
  223. package/dist/modes/interactive/components/settings-selector.d.ts +2 -0
  224. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  225. package/dist/modes/interactive/components/settings-selector.js +10 -0
  226. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  227. package/dist/modes/interactive/components/status-sidebar.d.ts +14 -2
  228. package/dist/modes/interactive/components/status-sidebar.d.ts.map +1 -1
  229. package/dist/modes/interactive/components/status-sidebar.js +106 -19
  230. package/dist/modes/interactive/components/status-sidebar.js.map +1 -1
  231. package/dist/modes/interactive/interactive-mode.d.ts +10 -1
  232. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  233. package/dist/modes/interactive/interactive-mode.js +104 -17
  234. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  235. package/dist/modes/interactive/theme/dark.json +1 -1
  236. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  237. package/dist/modes/interactive/theme/theme.js +1 -0
  238. package/dist/modes/interactive/theme/theme.js.map +1 -1
  239. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  240. package/dist/modes/rpc/rpc-mode.js +1 -1
  241. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  242. package/docs/compaction.md +25 -3
  243. package/docs/containerization.md +14 -2
  244. package/docs/docs.json +8 -0
  245. package/docs/environment-variables.md +1 -1
  246. package/docs/extensions.md +75 -16
  247. package/docs/mcp.md +97 -0
  248. package/docs/metrics.md +87 -0
  249. package/docs/packages.md +25 -4
  250. package/docs/providers.md +1 -1
  251. package/docs/sdk.md +25 -4
  252. package/docs/settings.md +8 -7
  253. package/docs/usage.md +13 -3
  254. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  255. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  256. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  257. package/examples/extensions/gondolin/package-lock.json +2 -2
  258. package/examples/extensions/gondolin/package.json +1 -1
  259. package/examples/extensions/plannotator-approval-bridge.ts +83 -0
  260. package/examples/extensions/sandbox/package-lock.json +2 -2
  261. package/examples/extensions/sandbox/package.json +1 -1
  262. package/examples/extensions/subagent/README.md +21 -11
  263. package/examples/extensions/subagent/adaptive-agent-runtime.ts +9 -3
  264. package/examples/extensions/subagent/agents/planner.md +1 -3
  265. package/examples/extensions/subagent/agents/reviewer.md +1 -3
  266. package/examples/extensions/subagent/agents/scout.md +1 -3
  267. package/examples/extensions/subagent/agents/worker.md +0 -1
  268. package/examples/extensions/subagent/agents.test.ts +9 -0
  269. package/examples/extensions/subagent/agents.ts +11 -4
  270. package/examples/extensions/subagent/deadline-budget.test.ts +32 -0
  271. package/examples/extensions/subagent/deadline-budget.ts +25 -1
  272. package/examples/extensions/subagent/fixtures/fake-omk-json.mjs +10 -0
  273. package/examples/extensions/subagent/index.ts +234 -55
  274. package/examples/extensions/subagent/subagent-execution-policy.test.ts +27 -0
  275. package/examples/extensions/subagent/subagent-extension-smoke.test.ts +167 -2
  276. package/examples/extensions/subagent/workflow-graph.test.ts +80 -0
  277. package/examples/extensions/subagent/workflow-graph.ts +89 -0
  278. package/examples/extensions/with-deps/package-lock.json +2 -2
  279. package/examples/extensions/with-deps/package.json +1 -1
  280. package/npm-shrinkwrap.json +253 -439
  281. package/package.json +8 -8
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Minimal MCP client: initialize handshake, `tools/list`, `tools/call`.
3
+ *
4
+ * Deliberately dependency-free. The upstream SDK carries a full server
5
+ * implementation, resource/prompt/sampling surfaces, and its own transport
6
+ * stack; OMK needs a client for three methods, so the client is ~200 lines of
7
+ * request correlation over a transport this repo already owns.
8
+ *
9
+ * Fail-closed contract:
10
+ * - Every request has a deadline. A hung server rejects, it does not stall a turn.
11
+ * - A transport exit rejects every in-flight request with the server's stderr tail.
12
+ * - Calling before a completed handshake throws instead of guessing.
13
+ */
14
+ import { formatJsonRpcError, isJsonRpcResponse, } from "./protocol.js";
15
+ import { McpStdioTransport } from "./stdio-transport.js";
16
+ /** Protocol revision this client implements. */
17
+ export const MCP_PROTOCOL_VERSION = "2025-06-18";
18
+ /** Default per-request deadline. */
19
+ export const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
20
+ /** Default handshake deadline. Servers that install on first run need more room than a normal call. */
21
+ export const DEFAULT_HANDSHAKE_TIMEOUT_MS = 60_000;
22
+ function isRecord(value) {
23
+ return typeof value === "object" && value !== null && !Array.isArray(value);
24
+ }
25
+ export class McpClient {
26
+ transport;
27
+ pending = new Map();
28
+ requestTimeoutMs;
29
+ handshakeTimeoutMs;
30
+ nextId = 1;
31
+ initialized = false;
32
+ closed = false;
33
+ exitReason;
34
+ info = {};
35
+ options;
36
+ constructor(options) {
37
+ this.options = options;
38
+ this.requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
39
+ this.handshakeTimeoutMs = options.handshakeTimeoutMs ?? DEFAULT_HANDSHAKE_TIMEOUT_MS;
40
+ this.transport = new McpStdioTransport(options.transport, {
41
+ onMessage: (message) => this.handleMessage(message),
42
+ onExit: ({ code, signal }) => this.handleExit(code, signal),
43
+ });
44
+ }
45
+ get name() {
46
+ return this.options.name;
47
+ }
48
+ get serverInfo() {
49
+ return this.info;
50
+ }
51
+ get ready() {
52
+ return this.initialized && !this.closed;
53
+ }
54
+ /** Why the server is unusable, when it is. */
55
+ get failure() {
56
+ return this.exitReason;
57
+ }
58
+ /**
59
+ * Spawn the server and complete the MCP handshake. Safe to await once; a
60
+ * second call after a failure re-throws the recorded reason rather than
61
+ * silently reusing a dead process.
62
+ */
63
+ async connect() {
64
+ if (this.closed)
65
+ throw new Error(this.exitReason ?? `MCP server "${this.options.name}" is closed`);
66
+ if (this.initialized)
67
+ return;
68
+ this.transport.start();
69
+ const result = await this.request("initialize", {
70
+ protocolVersion: MCP_PROTOCOL_VERSION,
71
+ capabilities: {},
72
+ clientInfo: this.options.clientInfo ?? { name: "omk", version: "0.0.0" },
73
+ }, this.handshakeTimeoutMs);
74
+ if (isRecord(result) && isRecord(result.serverInfo)) {
75
+ const { name, version } = result.serverInfo;
76
+ this.info = {
77
+ name: typeof name === "string" ? name : undefined,
78
+ version: typeof version === "string" ? version : undefined,
79
+ };
80
+ }
81
+ this.initialized = true;
82
+ this.transport.send({ jsonrpc: "2.0", method: "notifications/initialized" });
83
+ }
84
+ /**
85
+ * Protocol-level liveness probe. Rejects when the server is closed, dead,
86
+ * or fails to answer within `timeoutMs`, so callers can tell a silently
87
+ * killed process apart from a genuinely connected one.
88
+ */
89
+ async ping(timeoutMs) {
90
+ this.assertReady();
91
+ await this.request("ping", {}, timeoutMs);
92
+ }
93
+ /** List the server's tools. Requires a completed handshake. */
94
+ async listTools() {
95
+ this.assertReady();
96
+ const tools = [];
97
+ 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) {
103
+ if (!isRecord(raw) || typeof raw.name !== "string" || raw.name.length === 0)
104
+ continue;
105
+ tools.push({
106
+ name: raw.name,
107
+ description: typeof raw.description === "string" ? raw.description : undefined,
108
+ title: typeof raw.title === "string" ? raw.title : undefined,
109
+ inputSchema: isRecord(raw.inputSchema) ? raw.inputSchema : undefined,
110
+ });
111
+ }
112
+ cursor = typeof result.nextCursor === "string" && result.nextCursor.length > 0 ? result.nextCursor : undefined;
113
+ } while (cursor);
114
+ return tools;
115
+ }
116
+ /**
117
+ * Invoke a tool. A protocol-level failure rejects; a tool-level failure
118
+ * resolves with `isError: true`, matching MCP semantics so the model sees
119
+ * the server's own error text instead of a harness exception.
120
+ */
121
+ async callTool(name, args, timeoutMs) {
122
+ this.assertReady();
123
+ const result = await this.request("tools/call", { name, arguments: args ?? {} }, timeoutMs);
124
+ if (!isRecord(result))
125
+ return { content: [], isError: false };
126
+ const content = Array.isArray(result.content) ? result.content : [];
127
+ return {
128
+ content,
129
+ isError: result.isError === true,
130
+ structuredContent: result.structuredContent,
131
+ };
132
+ }
133
+ /** Terminate the server and reject anything still in flight. */
134
+ close() {
135
+ if (this.closed)
136
+ return;
137
+ this.transport.close();
138
+ this.handleExit(null, null);
139
+ }
140
+ assertReady() {
141
+ if (this.closed)
142
+ throw new Error(this.exitReason ?? `MCP server "${this.options.name}" is closed`);
143
+ if (!this.initialized)
144
+ throw new Error(`MCP server "${this.options.name}" is not initialized`);
145
+ }
146
+ request(method, params, timeoutMs) {
147
+ if (this.closed) {
148
+ return Promise.reject(new Error(this.exitReason ?? `MCP server "${this.options.name}" is closed`));
149
+ }
150
+ const id = this.nextId++;
151
+ const effectiveTimeout = timeoutMs ?? this.requestTimeoutMs;
152
+ return new Promise((resolve, reject) => {
153
+ const timer = setTimeout(() => {
154
+ this.pending.delete(id);
155
+ reject(new Error(`MCP server "${this.options.name}" timed out after ${effectiveTimeout}ms on ${method}`));
156
+ }, effectiveTimeout);
157
+ timer.unref?.();
158
+ this.pending.set(id, { resolve, reject, timer, method });
159
+ const sent = this.transport.send({ jsonrpc: "2.0", id, method, params });
160
+ if (!sent) {
161
+ this.pending.delete(id);
162
+ clearTimeout(timer);
163
+ reject(new Error(`MCP server "${this.options.name}" is not writable (${method})`));
164
+ }
165
+ });
166
+ }
167
+ handleMessage(message) {
168
+ if (!isJsonRpcResponse(message))
169
+ return; // Server-initiated requests/notifications are not used yet.
170
+ const response = message;
171
+ const pending = this.pending.get(response.id);
172
+ if (!pending)
173
+ return;
174
+ this.pending.delete(response.id);
175
+ clearTimeout(pending.timer);
176
+ if (response.error) {
177
+ pending.reject(new Error(`MCP server "${this.options.name}": ${formatJsonRpcError(response.error)}`));
178
+ return;
179
+ }
180
+ pending.resolve(response.result);
181
+ }
182
+ handleExit(code, signal) {
183
+ if (this.closed)
184
+ return;
185
+ this.closed = true;
186
+ this.initialized = false;
187
+ const detail = signal ? `signal ${signal}` : code === null ? "spawn failure" : `exit code ${code}`;
188
+ const stderr = this.transport.stderr.trim();
189
+ this.exitReason = `MCP server "${this.options.name}" stopped (${detail})${stderr ? `: ${stderr}` : ""}`;
190
+ const reason = new Error(this.exitReason);
191
+ for (const [id, pending] of this.pending) {
192
+ clearTimeout(pending.timer);
193
+ this.pending.delete(id);
194
+ pending.reject(reason);
195
+ }
196
+ }
197
+ }
198
+ //# sourceMappingURL=client.js.map
@@ -0,0 +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"]}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Runtime MCP server configuration loader.
3
+ *
4
+ * `mcp-inventory.ts` deliberately strips env *values* so its output is safe to
5
+ * render; a client that has to spawn the server needs those values, so loading
6
+ * is split rather than weakening the inventory's redaction guarantee. The
7
+ * objects returned here are runtime-only — never log or render them directly.
8
+ *
9
+ * Source precedence matches the inventory exactly (later wins):
10
+ * 1. ~/.kimi/mcp.json
11
+ * 2. ~/.omk/mcp.json
12
+ * 3. <cwd>/.omk/mcp.json
13
+ */
14
+ import type { McpServerConfig } from "./manager.ts";
15
+ /** Config file paths consulted, in precedence order. */
16
+ export declare function mcpConfigPaths(cwd?: string, home?: string): string[];
17
+ /**
18
+ * Load spawnable stdio server configs. Entries without a `command` (HTTP/SSE
19
+ * servers) are skipped rather than failing the load, so an unsupported
20
+ * transport in the config cannot disable every other server.
21
+ */
22
+ export declare function loadMcpServerConfigs(cwd?: string, home?: string): McpServerConfig[];
23
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../../src/core/mcp/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAKH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAgDpD,wDAAwD;AACxD,wBAAgB,cAAc,CAAC,GAAG,GAAE,MAAsB,EAAE,IAAI,GAAE,MAAqB,GAAG,MAAM,EAAE,CAMjG;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,GAAE,MAAsB,EAAE,IAAI,GAAE,MAAqB,GAAG,eAAe,EAAE,CAUhH","sourcesContent":["/**\n * Runtime MCP server configuration loader.\n *\n * `mcp-inventory.ts` deliberately strips env *values* so its output is safe to\n * render; a client that has to spawn the server needs those values, so loading\n * is split rather than weakening the inventory's redaction guarantee. The\n * objects returned here are runtime-only — never log or render them directly.\n *\n * Source precedence matches the inventory exactly (later wins):\n * 1. ~/.kimi/mcp.json\n * 2. ~/.omk/mcp.json\n * 3. <cwd>/.omk/mcp.json\n */\n\nimport fs from \"node:fs\";\nimport os from \"node:os\";\nimport path from \"node:path\";\nimport type { McpServerConfig } from \"./manager.ts\";\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction readJson(filePath: string): unknown {\n\ttry {\n\t\treturn JSON.parse(fs.readFileSync(filePath, \"utf8\"));\n\t} catch {\n\t\t// A missing or malformed config file yields no servers, never a crash.\n\t\treturn undefined;\n\t}\n}\n\nfunction extractServers(raw: unknown): Record<string, unknown> {\n\tif (!isRecord(raw)) return {};\n\tconst candidate = raw.mcpServers ?? raw.servers ?? raw.mcp_servers;\n\treturn isRecord(candidate) ? candidate : {};\n}\n\nfunction toStringRecord(value: unknown): Record<string, string> | undefined {\n\tif (!isRecord(value)) return undefined;\n\tconst out: Record<string, string> = {};\n\tfor (const [key, item] of Object.entries(value)) {\n\t\tif (typeof item === \"string\") out[key] = item;\n\t}\n\treturn Object.keys(out).length > 0 ? out : undefined;\n}\n\nfunction toServerConfig(name: string, raw: unknown): McpServerConfig | undefined {\n\tif (!isRecord(raw)) return undefined;\n\tconst command = raw.command;\n\t// Only stdio servers are supported; an entry with a `url` is a different transport.\n\tif (typeof command !== \"string\" || command.length === 0) return undefined;\n\tconst args = Array.isArray(raw.args) ? raw.args.filter((arg): arg is string => typeof arg === \"string\") : undefined;\n\tconst startupTimeoutSec = typeof raw.startup_timeout_sec === \"number\" ? raw.startup_timeout_sec : undefined;\n\treturn {\n\t\tname,\n\t\tcommand,\n\t\targs,\n\t\tenv: toStringRecord(raw.env),\n\t\tcwd: typeof raw.cwd === \"string\" ? raw.cwd : undefined,\n\t\tdisabled: raw.disabled === true || raw.enabled === false,\n\t\thandshakeTimeoutMs: startupTimeoutSec !== undefined ? Math.max(1, startupTimeoutSec) * 1000 : undefined,\n\t};\n}\n\n/** Config file paths consulted, in precedence order. */\nexport function mcpConfigPaths(cwd: string = process.cwd(), home: string = os.homedir()): string[] {\n\treturn [\n\t\tpath.join(home, \".kimi\", \"mcp.json\"),\n\t\tpath.join(home, \".omk\", \"mcp.json\"),\n\t\tpath.join(cwd, \".omk\", \"mcp.json\"),\n\t];\n}\n\n/**\n * Load spawnable stdio server configs. Entries without a `command` (HTTP/SSE\n * servers) are skipped rather than failing the load, so an unsupported\n * transport in the config cannot disable every other server.\n */\nexport function loadMcpServerConfigs(cwd: string = process.cwd(), home: string = os.homedir()): McpServerConfig[] {\n\tconst merged = new Map<string, McpServerConfig>();\n\tfor (const filePath of mcpConfigPaths(cwd, home)) {\n\t\tconst servers = extractServers(readJson(filePath));\n\t\tfor (const name of Object.keys(servers).sort()) {\n\t\t\tconst config = toServerConfig(name, servers[name]);\n\t\t\tif (config) merged.set(name, config);\n\t\t}\n\t}\n\treturn [...merged.values()].sort((a, b) => a.name.localeCompare(b.name));\n}\n"]}
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Runtime MCP server configuration loader.
3
+ *
4
+ * `mcp-inventory.ts` deliberately strips env *values* so its output is safe to
5
+ * render; a client that has to spawn the server needs those values, so loading
6
+ * is split rather than weakening the inventory's redaction guarantee. The
7
+ * objects returned here are runtime-only — never log or render them directly.
8
+ *
9
+ * Source precedence matches the inventory exactly (later wins):
10
+ * 1. ~/.kimi/mcp.json
11
+ * 2. ~/.omk/mcp.json
12
+ * 3. <cwd>/.omk/mcp.json
13
+ */
14
+ import fs from "node:fs";
15
+ import os from "node:os";
16
+ import path from "node:path";
17
+ function isRecord(value) {
18
+ return typeof value === "object" && value !== null && !Array.isArray(value);
19
+ }
20
+ function readJson(filePath) {
21
+ try {
22
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
23
+ }
24
+ catch {
25
+ // A missing or malformed config file yields no servers, never a crash.
26
+ return undefined;
27
+ }
28
+ }
29
+ function extractServers(raw) {
30
+ if (!isRecord(raw))
31
+ return {};
32
+ const candidate = raw.mcpServers ?? raw.servers ?? raw.mcp_servers;
33
+ return isRecord(candidate) ? candidate : {};
34
+ }
35
+ function toStringRecord(value) {
36
+ if (!isRecord(value))
37
+ return undefined;
38
+ const out = {};
39
+ for (const [key, item] of Object.entries(value)) {
40
+ if (typeof item === "string")
41
+ out[key] = item;
42
+ }
43
+ return Object.keys(out).length > 0 ? out : undefined;
44
+ }
45
+ function toServerConfig(name, raw) {
46
+ if (!isRecord(raw))
47
+ return undefined;
48
+ const command = raw.command;
49
+ // Only stdio servers are supported; an entry with a `url` is a different transport.
50
+ if (typeof command !== "string" || command.length === 0)
51
+ return undefined;
52
+ const args = Array.isArray(raw.args) ? raw.args.filter((arg) => typeof arg === "string") : undefined;
53
+ const startupTimeoutSec = typeof raw.startup_timeout_sec === "number" ? raw.startup_timeout_sec : undefined;
54
+ return {
55
+ name,
56
+ command,
57
+ args,
58
+ env: toStringRecord(raw.env),
59
+ cwd: typeof raw.cwd === "string" ? raw.cwd : undefined,
60
+ disabled: raw.disabled === true || raw.enabled === false,
61
+ handshakeTimeoutMs: startupTimeoutSec !== undefined ? Math.max(1, startupTimeoutSec) * 1000 : undefined,
62
+ };
63
+ }
64
+ /** Config file paths consulted, in precedence order. */
65
+ export function mcpConfigPaths(cwd = process.cwd(), home = os.homedir()) {
66
+ return [
67
+ path.join(home, ".kimi", "mcp.json"),
68
+ path.join(home, ".omk", "mcp.json"),
69
+ path.join(cwd, ".omk", "mcp.json"),
70
+ ];
71
+ }
72
+ /**
73
+ * Load spawnable stdio server configs. Entries without a `command` (HTTP/SSE
74
+ * servers) are skipped rather than failing the load, so an unsupported
75
+ * transport in the config cannot disable every other server.
76
+ */
77
+ export function loadMcpServerConfigs(cwd = process.cwd(), home = os.homedir()) {
78
+ const merged = new Map();
79
+ for (const filePath of mcpConfigPaths(cwd, home)) {
80
+ const servers = extractServers(readJson(filePath));
81
+ for (const name of Object.keys(servers).sort()) {
82
+ const config = toServerConfig(name, servers[name]);
83
+ if (config)
84
+ merged.set(name, config);
85
+ }
86
+ }
87
+ return [...merged.values()].sort((a, b) => a.name.localeCompare(b.name));
88
+ }
89
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../../../src/core/mcp/config.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Runtime MCP server configuration loader.\n *\n * `mcp-inventory.ts` deliberately strips env *values* so its output is safe to\n * render; a client that has to spawn the server needs those values, so loading\n * is split rather than weakening the inventory's redaction guarantee. The\n * objects returned here are runtime-only — never log or render them directly.\n *\n * Source precedence matches the inventory exactly (later wins):\n * 1. ~/.kimi/mcp.json\n * 2. ~/.omk/mcp.json\n * 3. <cwd>/.omk/mcp.json\n */\n\nimport fs from \"node:fs\";\nimport os from \"node:os\";\nimport path from \"node:path\";\nimport type { McpServerConfig } from \"./manager.ts\";\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction readJson(filePath: string): unknown {\n\ttry {\n\t\treturn JSON.parse(fs.readFileSync(filePath, \"utf8\"));\n\t} catch {\n\t\t// A missing or malformed config file yields no servers, never a crash.\n\t\treturn undefined;\n\t}\n}\n\nfunction extractServers(raw: unknown): Record<string, unknown> {\n\tif (!isRecord(raw)) return {};\n\tconst candidate = raw.mcpServers ?? raw.servers ?? raw.mcp_servers;\n\treturn isRecord(candidate) ? candidate : {};\n}\n\nfunction toStringRecord(value: unknown): Record<string, string> | undefined {\n\tif (!isRecord(value)) return undefined;\n\tconst out: Record<string, string> = {};\n\tfor (const [key, item] of Object.entries(value)) {\n\t\tif (typeof item === \"string\") out[key] = item;\n\t}\n\treturn Object.keys(out).length > 0 ? out : undefined;\n}\n\nfunction toServerConfig(name: string, raw: unknown): McpServerConfig | undefined {\n\tif (!isRecord(raw)) return undefined;\n\tconst command = raw.command;\n\t// Only stdio servers are supported; an entry with a `url` is a different transport.\n\tif (typeof command !== \"string\" || command.length === 0) return undefined;\n\tconst args = Array.isArray(raw.args) ? raw.args.filter((arg): arg is string => typeof arg === \"string\") : undefined;\n\tconst startupTimeoutSec = typeof raw.startup_timeout_sec === \"number\" ? raw.startup_timeout_sec : undefined;\n\treturn {\n\t\tname,\n\t\tcommand,\n\t\targs,\n\t\tenv: toStringRecord(raw.env),\n\t\tcwd: typeof raw.cwd === \"string\" ? raw.cwd : undefined,\n\t\tdisabled: raw.disabled === true || raw.enabled === false,\n\t\thandshakeTimeoutMs: startupTimeoutSec !== undefined ? Math.max(1, startupTimeoutSec) * 1000 : undefined,\n\t};\n}\n\n/** Config file paths consulted, in precedence order. */\nexport function mcpConfigPaths(cwd: string = process.cwd(), home: string = os.homedir()): string[] {\n\treturn [\n\t\tpath.join(home, \".kimi\", \"mcp.json\"),\n\t\tpath.join(home, \".omk\", \"mcp.json\"),\n\t\tpath.join(cwd, \".omk\", \"mcp.json\"),\n\t];\n}\n\n/**\n * Load spawnable stdio server configs. Entries without a `command` (HTTP/SSE\n * servers) are skipped rather than failing the load, so an unsupported\n * transport in the config cannot disable every other server.\n */\nexport function loadMcpServerConfigs(cwd: string = process.cwd(), home: string = os.homedir()): McpServerConfig[] {\n\tconst merged = new Map<string, McpServerConfig>();\n\tfor (const filePath of mcpConfigPaths(cwd, home)) {\n\t\tconst servers = extractServers(readJson(filePath));\n\t\tfor (const name of Object.keys(servers).sort()) {\n\t\t\tconst config = toServerConfig(name, servers[name]);\n\t\t\tif (config) merged.set(name, config);\n\t\t}\n\t}\n\treturn [...merged.values()].sort((a, b) => a.name.localeCompare(b.name));\n}\n"]}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * MCP client surface for the OMK harness.
3
+ *
4
+ * Before this module, `mcp-inventory.ts` could read MCP configuration and the
5
+ * doctor could report on it, but the runtime had no way to speak the protocol:
6
+ * configured servers contributed zero tools to a session. This package closes
7
+ * that gap with a dependency-free stdio client.
8
+ */
9
+ export { DEFAULT_HANDSHAKE_TIMEOUT_MS, DEFAULT_REQUEST_TIMEOUT_MS, MCP_PROTOCOL_VERSION, McpClient, type McpClientOptions, type McpContentBlock, type McpServerInfo, type McpToolCallResult, type McpToolSchema, } from "./client.ts";
10
+ export { loadMcpServerConfigs, mcpConfigPaths } from "./config.ts";
11
+ export { McpManager, type McpManagerOptions, type McpServerConfig, type McpServerState, type McpServerStatus, } from "./manager.ts";
12
+ export { createLineDecoder, encodeMessage, formatJsonRpcError, isJsonRpcNotification, isJsonRpcRequest, isJsonRpcResponse, type JsonRpcMessage, MAX_MESSAGE_LINE_BYTES, } from "./protocol.ts";
13
+ export { DEFAULT_KILL_GRACE_MS, MAX_STDERR_TAIL_CHARS, McpStdioTransport, type StdioTransportOptions, } from "./stdio-transport.ts";
14
+ export { buildMcpToolName, createMcpToolDefinition, MAX_TOOL_NAME_LENGTH, MCP_TOOL_NAME_SEPARATOR, type McpToolDetails, mapMcpContent, sanitizeToolNameSegment, } from "./tools.ts";
15
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/mcp/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EACN,4BAA4B,EAC5B,0BAA0B,EAC1B,oBAAoB,EACpB,SAAS,EACT,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACpB,KAAK,aAAa,EAClB,KAAK,iBAAiB,EACtB,KAAK,aAAa,GAClB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,oBAAoB,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AACnE,OAAO,EACN,UAAU,EACV,KAAK,iBAAiB,EACtB,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,KAAK,eAAe,GACpB,MAAM,cAAc,CAAC;AACtB,OAAO,EACN,iBAAiB,EACjB,aAAa,EACb,kBAAkB,EAClB,qBAAqB,EACrB,gBAAgB,EAChB,iBAAiB,EACjB,KAAK,cAAc,EACnB,sBAAsB,GACtB,MAAM,eAAe,CAAC;AACvB,OAAO,EACN,qBAAqB,EACrB,qBAAqB,EACrB,iBAAiB,EACjB,KAAK,qBAAqB,GAC1B,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACN,gBAAgB,EAChB,uBAAuB,EACvB,oBAAoB,EACpB,uBAAuB,EACvB,KAAK,cAAc,EACnB,aAAa,EACb,uBAAuB,GACvB,MAAM,YAAY,CAAC","sourcesContent":["/**\n * MCP client surface for the OMK harness.\n *\n * Before this module, `mcp-inventory.ts` could read MCP configuration and the\n * doctor could report on it, but the runtime had no way to speak the protocol:\n * configured servers contributed zero tools to a session. This package closes\n * that gap with a dependency-free stdio client.\n */\n\nexport {\n\tDEFAULT_HANDSHAKE_TIMEOUT_MS,\n\tDEFAULT_REQUEST_TIMEOUT_MS,\n\tMCP_PROTOCOL_VERSION,\n\tMcpClient,\n\ttype McpClientOptions,\n\ttype McpContentBlock,\n\ttype McpServerInfo,\n\ttype McpToolCallResult,\n\ttype McpToolSchema,\n} from \"./client.ts\";\nexport { loadMcpServerConfigs, mcpConfigPaths } from \"./config.ts\";\nexport {\n\tMcpManager,\n\ttype McpManagerOptions,\n\ttype McpServerConfig,\n\ttype McpServerState,\n\ttype McpServerStatus,\n} from \"./manager.ts\";\nexport {\n\tcreateLineDecoder,\n\tencodeMessage,\n\tformatJsonRpcError,\n\tisJsonRpcNotification,\n\tisJsonRpcRequest,\n\tisJsonRpcResponse,\n\ttype JsonRpcMessage,\n\tMAX_MESSAGE_LINE_BYTES,\n} from \"./protocol.ts\";\nexport {\n\tDEFAULT_KILL_GRACE_MS,\n\tMAX_STDERR_TAIL_CHARS,\n\tMcpStdioTransport,\n\ttype StdioTransportOptions,\n} from \"./stdio-transport.ts\";\nexport {\n\tbuildMcpToolName,\n\tcreateMcpToolDefinition,\n\tMAX_TOOL_NAME_LENGTH,\n\tMCP_TOOL_NAME_SEPARATOR,\n\ttype McpToolDetails,\n\tmapMcpContent,\n\tsanitizeToolNameSegment,\n} from \"./tools.ts\";\n"]}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * MCP client surface for the OMK harness.
3
+ *
4
+ * Before this module, `mcp-inventory.ts` could read MCP configuration and the
5
+ * doctor could report on it, but the runtime had no way to speak the protocol:
6
+ * configured servers contributed zero tools to a session. This package closes
7
+ * that gap with a dependency-free stdio client.
8
+ */
9
+ export { DEFAULT_HANDSHAKE_TIMEOUT_MS, DEFAULT_REQUEST_TIMEOUT_MS, MCP_PROTOCOL_VERSION, McpClient, } from "./client.js";
10
+ export { loadMcpServerConfigs, mcpConfigPaths } from "./config.js";
11
+ export { McpManager, } from "./manager.js";
12
+ export { createLineDecoder, encodeMessage, formatJsonRpcError, isJsonRpcNotification, isJsonRpcRequest, isJsonRpcResponse, MAX_MESSAGE_LINE_BYTES, } from "./protocol.js";
13
+ export { DEFAULT_KILL_GRACE_MS, MAX_STDERR_TAIL_CHARS, McpStdioTransport, } from "./stdio-transport.js";
14
+ export { buildMcpToolName, createMcpToolDefinition, MAX_TOOL_NAME_LENGTH, MCP_TOOL_NAME_SEPARATOR, mapMcpContent, sanitizeToolNameSegment, } from "./tools.js";
15
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/core/mcp/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EACN,4BAA4B,EAC5B,0BAA0B,EAC1B,oBAAoB,EACpB,SAAS,GAMT,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,oBAAoB,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AACnE,OAAO,EACN,UAAU,GAKV,MAAM,cAAc,CAAC;AACtB,OAAO,EACN,iBAAiB,EACjB,aAAa,EACb,kBAAkB,EAClB,qBAAqB,EACrB,gBAAgB,EAChB,iBAAiB,EAEjB,sBAAsB,GACtB,MAAM,eAAe,CAAC;AACvB,OAAO,EACN,qBAAqB,EACrB,qBAAqB,EACrB,iBAAiB,GAEjB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACN,gBAAgB,EAChB,uBAAuB,EACvB,oBAAoB,EACpB,uBAAuB,EAEvB,aAAa,EACb,uBAAuB,GACvB,MAAM,YAAY,CAAC","sourcesContent":["/**\n * MCP client surface for the OMK harness.\n *\n * Before this module, `mcp-inventory.ts` could read MCP configuration and the\n * doctor could report on it, but the runtime had no way to speak the protocol:\n * configured servers contributed zero tools to a session. This package closes\n * that gap with a dependency-free stdio client.\n */\n\nexport {\n\tDEFAULT_HANDSHAKE_TIMEOUT_MS,\n\tDEFAULT_REQUEST_TIMEOUT_MS,\n\tMCP_PROTOCOL_VERSION,\n\tMcpClient,\n\ttype McpClientOptions,\n\ttype McpContentBlock,\n\ttype McpServerInfo,\n\ttype McpToolCallResult,\n\ttype McpToolSchema,\n} from \"./client.ts\";\nexport { loadMcpServerConfigs, mcpConfigPaths } from \"./config.ts\";\nexport {\n\tMcpManager,\n\ttype McpManagerOptions,\n\ttype McpServerConfig,\n\ttype McpServerState,\n\ttype McpServerStatus,\n} from \"./manager.ts\";\nexport {\n\tcreateLineDecoder,\n\tencodeMessage,\n\tformatJsonRpcError,\n\tisJsonRpcNotification,\n\tisJsonRpcRequest,\n\tisJsonRpcResponse,\n\ttype JsonRpcMessage,\n\tMAX_MESSAGE_LINE_BYTES,\n} from \"./protocol.ts\";\nexport {\n\tDEFAULT_KILL_GRACE_MS,\n\tMAX_STDERR_TAIL_CHARS,\n\tMcpStdioTransport,\n\ttype StdioTransportOptions,\n} from \"./stdio-transport.ts\";\nexport {\n\tbuildMcpToolName,\n\tcreateMcpToolDefinition,\n\tMAX_TOOL_NAME_LENGTH,\n\tMCP_TOOL_NAME_SEPARATOR,\n\ttype McpToolDetails,\n\tmapMcpContent,\n\tsanitizeToolNameSegment,\n} from \"./tools.ts\";\n"]}
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Lazy, crash-isolated MCP server manager.
3
+ *
4
+ * Design constraints that shaped this:
5
+ * - A workspace can configure 25 servers. Spawning them all at startup would
6
+ * trade a 0.6s cold start for a multi-second one, so connection is lazy and
7
+ * only happens when tools are actually requested.
8
+ * - One broken server must never take a session down. Every connect and every
9
+ * listing is isolated; a failure is recorded as server status and the other
10
+ * servers still contribute tools.
11
+ * - Connect work is deduplicated: concurrent `listToolDefinitions()` calls
12
+ * share one in-flight connect per server.
13
+ */
14
+ import type { TSchema } from "typebox";
15
+ import type { ToolDefinition } from "../extensions/types.ts";
16
+ import { McpClient, type McpClientOptions } from "./client.ts";
17
+ import { type McpToolDetails } from "./tools.ts";
18
+ export type McpServerState = "idle" | "connecting" | "ready" | "failed";
19
+ export interface McpServerConfig {
20
+ readonly name: string;
21
+ readonly command: string;
22
+ readonly args?: readonly string[];
23
+ readonly env?: Readonly<Record<string, string>>;
24
+ readonly cwd?: string;
25
+ /** Skip this server without removing it from configuration. */
26
+ readonly disabled?: boolean;
27
+ readonly requestTimeoutMs?: number;
28
+ readonly handshakeTimeoutMs?: number;
29
+ }
30
+ export interface McpServerStatus {
31
+ readonly name: string;
32
+ readonly state: McpServerState;
33
+ readonly toolCount: number;
34
+ /** Failure reason when `state` is `failed`. Never contains configured env values. */
35
+ readonly error?: string;
36
+ readonly serverVersion?: string;
37
+ }
38
+ export interface McpManagerOptions {
39
+ readonly servers: readonly McpServerConfig[];
40
+ readonly cwd?: string;
41
+ readonly clientInfo?: {
42
+ readonly name: string;
43
+ readonly version: string;
44
+ };
45
+ readonly callTimeoutMs?: number;
46
+ /** Injected for tests so the manager can be exercised without spawning processes. */
47
+ readonly createClient?: (options: McpClientOptions) => McpClient;
48
+ }
49
+ export declare class McpManager {
50
+ private readonly runtimes;
51
+ private readonly options;
52
+ constructor(options: McpManagerOptions);
53
+ /** Configured server names, in configuration order. */
54
+ get serverNames(): string[];
55
+ /** Current status per server. Safe to call before any connection attempt. */
56
+ status(): McpServerStatus[];
57
+ /**
58
+ * Connect every enabled server that has not been attempted yet, then return
59
+ * the union of their tools. Servers are connected concurrently and failures
60
+ * are isolated, so a partial result is the normal outcome, not an error.
61
+ */
62
+ listToolDefinitions(): Promise<ToolDefinition<TSchema, McpToolDetails>[]>;
63
+ /** Connect a single server by name. Returns its status either way. */
64
+ connect(name: string): Promise<McpServerStatus>;
65
+ /** Terminate every connected server. Idempotent. */
66
+ close(): void;
67
+ /**
68
+ * Verify that `ready` servers are actually alive with a protocol ping. A
69
+ * failed ping closes the client and marks the server `failed`, so a
70
+ * silently killed process stops masquerading as connected. Servers already
71
+ * `failed` are re-attempted only when `reconnectFailed` is set — callers
72
+ * should gate that behind a slow cadence so a permanently broken server is
73
+ * not respawned every probe. Idle servers stay idle: connection is lazy by
74
+ * design, and a status probe must not spawn processes.
75
+ */
76
+ checkHealth(options?: {
77
+ pingTimeoutMs?: number;
78
+ reconnectFailed?: boolean;
79
+ }): Promise<McpServerStatus[]>;
80
+ private pingRuntime;
81
+ private ensureConnected;
82
+ private connectRuntime;
83
+ }
84
+ //# sourceMappingURL=manager.d.ts.map
@@ -0,0 +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"]}