@wrongstack/mcp 0.319.0 → 0.320.1

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.
package/dist/client.d.ts CHANGED
@@ -63,6 +63,7 @@ export declare class MCPClient {
63
63
  private _toolsCache?;
64
64
  private _drainPending;
65
65
  private _lastNotifySkipped;
66
+ private closePromise?;
66
67
  private sseTransport?;
67
68
  private httpTransport?;
68
69
  /** Notified when the stdio child process exits so the registry can attempt reconnect. */
@@ -106,6 +107,7 @@ export declare class MCPClient {
106
107
  listPrompts(opts?: MCPPageOptions): Promise<MCPListPromptsResult>;
107
108
  getPrompt(name: string, args?: Record<string, string> | undefined, opts?: MCPRequestOptions): Promise<MCPGetPromptResult>;
108
109
  close(): Promise<void>;
110
+ private closeInner;
109
111
  private request;
110
112
  private requestCapability;
111
113
  private requireResourceSubscriptions;
package/dist/index.js CHANGED
@@ -1335,6 +1335,7 @@ function createTimeoutSignal(parent, timeoutMs) {
1335
1335
  () => ctrl.abort(new Error(`MCP HTTP request timed out after ${timeoutMs}ms`)),
1336
1336
  timeoutMs
1337
1337
  );
1338
+ timer.unref?.();
1338
1339
  return {
1339
1340
  signal: ctrl.signal,
1340
1341
  dispose: () => {
@@ -1775,7 +1776,8 @@ var SSETransport = class extends BaseHTTPTransport {
1775
1776
  sseReader.feed(chunk);
1776
1777
  }
1777
1778
  } catch {
1778
- if (this.state !== "disconnected" && this.state !== "failed") {
1779
+ } finally {
1780
+ if (!this.readerDone && this.state !== "disconnected" && this.state !== "failed") {
1779
1781
  this.state = "disconnected";
1780
1782
  this.notifyDisconnect();
1781
1783
  }
@@ -1809,9 +1811,13 @@ var SSETransport = class extends BaseHTTPTransport {
1809
1811
  try {
1810
1812
  const res = await this.fetchWithAuthorization(this.url, fetchOpts, timeoutSignal.signal);
1811
1813
  if (!res.ok) {
1812
- const body2 = await res.text();
1813
- const cap = MCP_CONSTANTS.REQUEST_LOG_CAP;
1814
- const snippet = body2.length > cap ? `${body2.slice(0, cap)}\u2026 [${body2.length} bytes total]` : body2;
1814
+ let snippet;
1815
+ try {
1816
+ snippet = await readBodyCapped(res, MCP_CONSTANTS.REQUEST_LOG_CAP);
1817
+ } catch (err) {
1818
+ const received = err instanceof ToolError4 && typeof err.context?.["received"] === "number" ? err.context["received"] : void 0;
1819
+ snippet = typeof received === "number" ? `\u2026 [${received}+ bytes total]` : "\u2026 [error body unreadable]";
1820
+ }
1815
1821
  throw new ToolError4({
1816
1822
  message: `HTTP ${res.status}: ${snippet}`,
1817
1823
  code: "TOOL_EXECUTION_FAILED",
@@ -1819,6 +1825,10 @@ var SSETransport = class extends BaseHTTPTransport {
1819
1825
  context: { transport: "sse", url: this.url, status: res.status }
1820
1826
  });
1821
1827
  }
1828
+ if (method.startsWith("notifications/")) {
1829
+ await readBodyCapped(res).catch(() => void 0);
1830
+ return { jsonrpc: "2.0", id };
1831
+ }
1822
1832
  let data;
1823
1833
  try {
1824
1834
  data = JSON.parse(await readBodyCapped(res));
@@ -1897,6 +1907,10 @@ var SSETransport = class extends BaseHTTPTransport {
1897
1907
  }
1898
1908
  });
1899
1909
  }
1910
+ if (method.startsWith("notifications/")) {
1911
+ await readBodyCapped(res).catch(() => void 0);
1912
+ return { jsonrpc: "2.0", id };
1913
+ }
1900
1914
  let data;
1901
1915
  try {
1902
1916
  data = JSON.parse(await readBodyCapped(res));
@@ -2023,10 +2037,10 @@ var StreamableHTTPTransport = class extends BaseHTTPTransport {
2023
2037
  const contentType = initRes.headers.get("content-type") ?? "";
2024
2038
  let data;
2025
2039
  if (contentType.includes("application/json")) {
2026
- const parsed = await initRes.json();
2040
+ const parsed = JSON.parse(await readBodyCapped(initRes));
2027
2041
  if (isJsonRpcResult(parsed)) data = parsed;
2028
2042
  } else {
2029
- data = extractJsonRpcResults(await initRes.text())[0];
2043
+ data = extractJsonRpcResults(await readBodyCapped(initRes))[0];
2030
2044
  }
2031
2045
  if (!data) {
2032
2046
  throw new Error("Could not parse initialize response");
@@ -2079,7 +2093,7 @@ var StreamableHTTPTransport = class extends BaseHTTPTransport {
2079
2093
  throw new Error(`HTTP ${res.status}: ${res.statusText}`);
2080
2094
  }
2081
2095
  if (method.startsWith("notifications/")) {
2082
- await res.text().catch(() => void 0);
2096
+ await readBodyCapped(res).catch(() => void 0);
2083
2097
  return { jsonrpc: "2.0", id };
2084
2098
  }
2085
2099
  const match = this.consumeResponseText(await readBodyCapped(res), id);
@@ -2126,7 +2140,7 @@ var StreamableHTTPTransport = class extends BaseHTTPTransport {
2126
2140
  throw new Error(`HTTP ${res.status}: ${res.statusText}`);
2127
2141
  }
2128
2142
  if (method.startsWith("notifications/")) {
2129
- await res.text().catch(() => void 0);
2143
+ await readBodyCapped(res).catch(() => void 0);
2130
2144
  return { jsonrpc: "2.0", id };
2131
2145
  }
2132
2146
  const parsed = this.consumeResponseText(await readBodyCapped(res), id);
@@ -2207,6 +2221,7 @@ var MCPClient = class _MCPClient {
2207
2221
  _toolsCache;
2208
2222
  _drainPending = false;
2209
2223
  _lastNotifySkipped = false;
2224
+ closePromise;
2210
2225
  // HTTP transports
2211
2226
  sseTransport;
2212
2227
  httpTransport;
@@ -2260,15 +2275,21 @@ var MCPClient = class _MCPClient {
2260
2275
  async connect() {
2261
2276
  this.state = "connecting";
2262
2277
  this._serverMetadata = void 0;
2263
- if (this.opts.transport === "stdio") {
2264
- await this.connectStdio();
2265
- } else if (this.opts.transport === "sse") {
2266
- await this.connectSSE();
2267
- } else if (this.opts.transport === "streamable-http") {
2268
- await this.connectStreamableHTTP();
2269
- } else {
2278
+ try {
2279
+ if (this.opts.transport === "stdio") {
2280
+ await this.connectStdio();
2281
+ } else if (this.opts.transport === "sse") {
2282
+ await this.connectSSE();
2283
+ } else if (this.opts.transport === "streamable-http") {
2284
+ await this.connectStreamableHTTP();
2285
+ } else {
2286
+ throw new Error(`Unknown transport "${this.opts.transport}"`);
2287
+ }
2288
+ } catch (err) {
2289
+ await this.close().catch(() => {
2290
+ });
2270
2291
  this.state = "failed";
2271
- throw new Error(`Unknown transport "${this.opts.transport}"`);
2292
+ throw err;
2272
2293
  }
2273
2294
  }
2274
2295
  async connectStdio() {
@@ -2303,6 +2324,13 @@ var MCPClient = class _MCPClient {
2303
2324
  })() : spawn2(this.opts.command, rawArgs, { env: spawnEnv, stdio, windowsHide: true });
2304
2325
  this.child = child;
2305
2326
  child.stdout?.on("data", (chunk) => this.onData(chunk.toString()));
2327
+ child.stdout?.on("end", () => {
2328
+ if (this.rxBuffer.trim()) {
2329
+ const line = this.rxBuffer.trim();
2330
+ this.rxBuffer = "";
2331
+ this.onLine(line);
2332
+ }
2333
+ });
2306
2334
  child.stderr?.on("data", () => {
2307
2335
  });
2308
2336
  child.stdin?.on("error", (err) => {
@@ -2554,6 +2582,13 @@ var MCPClient = class _MCPClient {
2554
2582
  );
2555
2583
  }
2556
2584
  async close() {
2585
+ if (this.closePromise) return this.closePromise;
2586
+ this.closePromise = this.closeInner().finally(() => {
2587
+ this.closePromise = void 0;
2588
+ });
2589
+ return this.closePromise;
2590
+ }
2591
+ async closeInner() {
2557
2592
  if (this.child) {
2558
2593
  const child = this.child;
2559
2594
  const exitPromise = new Promise((resolve) => {
@@ -2570,16 +2605,26 @@ var MCPClient = class _MCPClient {
2570
2605
  }
2571
2606
  const GRACEFUL_MS = 800;
2572
2607
  const FORCE_TIMEOUT_MS = 1200;
2608
+ let gracefulTimer;
2573
2609
  const gracefulRace = await Promise.race([
2574
2610
  exitPromise.then(() => "exited"),
2575
- new Promise((resolve) => setTimeout(() => resolve("timeout"), GRACEFUL_MS))
2611
+ new Promise((resolve) => {
2612
+ gracefulTimer = setTimeout(() => resolve("timeout"), GRACEFUL_MS);
2613
+ gracefulTimer.unref?.();
2614
+ })
2576
2615
  ]);
2616
+ if (gracefulTimer) clearTimeout(gracefulTimer);
2577
2617
  if (gracefulRace === "timeout") {
2578
2618
  forceKillTree(child);
2619
+ let forceTimer;
2579
2620
  await Promise.race([
2580
2621
  exitPromise,
2581
- new Promise((resolve) => setTimeout(resolve, FORCE_TIMEOUT_MS))
2622
+ new Promise((resolve) => {
2623
+ forceTimer = setTimeout(resolve, FORCE_TIMEOUT_MS);
2624
+ forceTimer.unref?.();
2625
+ })
2582
2626
  ]);
2627
+ if (forceTimer) clearTimeout(forceTimer);
2583
2628
  }
2584
2629
  child.stdout?.removeAllListeners();
2585
2630
  child.stderr?.removeAllListeners();
@@ -2709,49 +2754,53 @@ var MCPClient = class _MCPClient {
2709
2754
  this.pending.clear();
2710
2755
  }
2711
2756
  async notify(method, params) {
2757
+ if (this._drainPending) {
2758
+ this._lastNotifySkipped = true;
2759
+ console.warn(
2760
+ JSON.stringify({
2761
+ level: "warn",
2762
+ event: "mcp.notify_skipped_backpressure",
2763
+ server: this.opts.name,
2764
+ method,
2765
+ message: "stdin buffer backpressure (already waiting for drain)",
2766
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2767
+ })
2768
+ );
2769
+ return;
2770
+ }
2771
+ const stdin = this.child?.stdin;
2772
+ if (!stdin || stdin.destroyed === true || stdin.writable === false) {
2773
+ return;
2774
+ }
2712
2775
  const req = { jsonrpc: "2.0", method, params };
2713
2776
  const encoded = JSON.stringify(req) + "\n";
2714
2777
  try {
2715
- const ok = this.child?.stdin?.write(encoded);
2778
+ const ok = stdin.write(encoded);
2716
2779
  if (!ok) {
2717
- if (this._drainPending) {
2718
- this._lastNotifySkipped = true;
2719
- console.warn(
2720
- JSON.stringify({
2721
- level: "warn",
2722
- event: "mcp.notify_skipped_backpressure",
2723
- server: this.opts.name,
2724
- method,
2725
- message: "stdin buffer backpressure (already waiting for drain)",
2726
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
2727
- })
2728
- );
2729
- return;
2730
- }
2731
2780
  this._drainPending = true;
2732
2781
  await new Promise((resolve, reject) => {
2733
2782
  const timeout = setTimeout(() => {
2734
- this.child?.stdin?.removeListener?.("drain", onDrain);
2735
- this.child?.stdin?.removeListener?.("error", onError);
2783
+ stdin.removeListener?.("drain", onDrain);
2784
+ stdin.removeListener?.("error", onError);
2736
2785
  this._drainPending = false;
2737
2786
  reject(new Error(`MCP notify("${method}") drain timeout`));
2738
2787
  }, 500);
2739
2788
  const onDrain = () => {
2740
2789
  clearTimeout(timeout);
2741
- this.child?.stdin?.removeListener?.("drain", onDrain);
2742
- this.child?.stdin?.removeListener?.("error", onError);
2790
+ stdin.removeListener?.("drain", onDrain);
2791
+ stdin.removeListener?.("error", onError);
2743
2792
  this._drainPending = false;
2744
2793
  resolve();
2745
2794
  };
2746
2795
  const onError = (err) => {
2747
2796
  clearTimeout(timeout);
2748
- this.child?.stdin?.removeListener?.("drain", onDrain);
2749
- this.child?.stdin?.removeListener?.("error", onError);
2797
+ stdin.removeListener?.("drain", onDrain);
2798
+ stdin.removeListener?.("error", onError);
2750
2799
  this._drainPending = false;
2751
2800
  reject(err);
2752
2801
  };
2753
- this.child?.stdin?.once("drain", onDrain);
2754
- this.child?.stdin?.once("error", onError);
2802
+ stdin.once?.("drain", onDrain);
2803
+ stdin.once?.("error", onError);
2755
2804
  });
2756
2805
  }
2757
2806
  } catch (err) {
@@ -2769,12 +2818,16 @@ var MCPClient = class _MCPClient {
2769
2818
  void this.close();
2770
2819
  return;
2771
2820
  }
2821
+ let start = 0;
2772
2822
  let idx = this.rxBuffer.indexOf("\n");
2773
2823
  while (idx !== -1) {
2774
- const line = this.rxBuffer.slice(0, idx).trim();
2775
- this.rxBuffer = this.rxBuffer.slice(idx + 1);
2824
+ const line = this.rxBuffer.slice(start, idx).trim();
2825
+ start = idx + 1;
2776
2826
  if (line) this.onLine(line);
2777
- idx = this.rxBuffer.indexOf("\n");
2827
+ idx = this.rxBuffer.indexOf("\n", start);
2828
+ }
2829
+ if (start > 0) {
2830
+ this.rxBuffer = this.rxBuffer.slice(start);
2778
2831
  }
2779
2832
  }
2780
2833
  onLine(line) {
@@ -3040,11 +3093,7 @@ async function persist(configPath, full, servers) {
3040
3093
  full.mcpServers = servers;
3041
3094
  await writeConfig(configPath, full);
3042
3095
  }
3043
- var UNSAFE_SERVER_NAMES = /* @__PURE__ */ new Set([
3044
- "__proto__",
3045
- "constructor",
3046
- "prototype"
3047
- ]);
3096
+ var UNSAFE_SERVER_NAMES = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
3048
3097
  function unsafeServerNameResult(name) {
3049
3098
  if (!UNSAFE_SERVER_NAMES.has(name)) return void 0;
3050
3099
  return { ok: false, message: `Invalid server name "${name}"` };
@@ -3615,7 +3664,8 @@ function wrapMCPTool(serverName, mcpTool, client, permission = "confirm", observ
3615
3664
  const signal = opts?.signal ?? ctx?.signal;
3616
3665
  const res = await live.callTool(mcpTool.name, input, signal ? { signal } : void 0);
3617
3666
  if (res.isError) {
3618
- throw new Error(stringify(res.content));
3667
+ const errText = stringify(res.content);
3668
+ throw new Error(errText || `MCP tool "${qualifiedName}" failed`);
3619
3669
  }
3620
3670
  ok = true;
3621
3671
  return stringify(res.content);
@@ -3664,6 +3714,7 @@ function applySlotTools(ctx, slot, tools, client) {
3664
3714
  },
3665
3715
  onFinish: ({ durationMs, ok }) => {
3666
3716
  slot.operations.inFlightCalls = Math.max(0, slot.operations.inFlightCalls - 1);
3717
+ slot.lastUsed = Date.now();
3667
3718
  pushBounded(slot.operations.callSamples, durationMs, MCP_OPERATION_LIMITS.LATENCY_SAMPLES);
3668
3719
  if (ok) {
3669
3720
  ctx.recordSuccess(slot);
@@ -3787,7 +3838,7 @@ async function attemptConnectSlot(ctx, slot) {
3787
3838
  client.addToolsChangedListener(ctx.onToolsChanged);
3788
3839
  ctx.addCatalogListeners(client);
3789
3840
  await client.connect();
3790
- if (slot.state === "disconnected" || ctx.servers.has(slot.cfg.name) && ctx.servers.get(slot.cfg.name) !== slot) {
3841
+ if (slot.state === "disconnected" || !ctx.servers.has(slot.cfg.name) || ctx.servers.get(slot.cfg.name) !== slot) {
3791
3842
  client.removeExitListener(ctx.onChildExit);
3792
3843
  if (boundDisconnect) client.removeDisconnectListener(boundDisconnect);
3793
3844
  client.removeToolsChangedListener(ctx.onToolsChanged);
@@ -3890,8 +3941,28 @@ function resetDisconnectedSlotTools(slot, toolRegistry) {
3890
3941
  slot.resourceTemplates = void 0;
3891
3942
  slot.prompts = void 0;
3892
3943
  }
3893
- function markLazySlotDormant(slot, events, reason) {
3894
- slot.client = void 0;
3944
+ function markLazySlotDormant(slot, events, reason, options) {
3945
+ slot.reconnectPending = false;
3946
+ if (slot.reconnectTimer) {
3947
+ clearTimeout(slot.reconnectTimer);
3948
+ slot.reconnectTimer = void 0;
3949
+ }
3950
+ if (slot.client) {
3951
+ if (options?.onChildExit) {
3952
+ slot.client.removeExitListener?.(options.onChildExit);
3953
+ }
3954
+ if (slot.onDisconnect) {
3955
+ slot.client.removeDisconnectListener?.(slot.onDisconnect);
3956
+ }
3957
+ if (options?.onToolsChanged) {
3958
+ slot.client.removeToolsChangedListener?.(options.onToolsChanged);
3959
+ }
3960
+ options?.removeCatalogListeners?.(slot.client);
3961
+ slot.client.close?.().catch(() => {
3962
+ });
3963
+ slot.client = void 0;
3964
+ }
3965
+ slot.onDisconnect = void 0;
3895
3966
  slot.state = "dormant";
3896
3967
  events.emit("mcp.server.disconnected", {
3897
3968
  name: slot.cfg.name,
@@ -3954,17 +4025,23 @@ function buildRegistryOperationalHealth(servers, disabledServers) {
3954
4025
 
3955
4026
  // src/registry-idle.ts
3956
4027
  async function sleepIdleSlot(ctx, slot) {
4028
+ if (slot.operations.inFlightCalls > 0) return;
3957
4029
  slot.reconnectPending = false;
3958
4030
  if (slot.reconnectTimer) {
3959
4031
  clearTimeout(slot.reconnectTimer);
3960
4032
  slot.reconnectTimer = void 0;
3961
4033
  }
3962
4034
  if (slot.client) {
3963
- slot.client.removeExitListener(ctx.onChildExit);
3964
- if (slot.onDisconnect) slot.client.removeDisconnectListener(slot.onDisconnect);
3965
- slot.client.removeToolsChangedListener(ctx.onToolsChanged);
3966
- ctx.removeCatalogListeners(slot.client);
3967
- await slot.client.close();
4035
+ const client = slot.client;
4036
+ client.removeExitListener?.(ctx.onChildExit);
4037
+ if (slot.onDisconnect) client.removeDisconnectListener?.(slot.onDisconnect);
4038
+ client.removeToolsChangedListener?.(ctx.onToolsChanged);
4039
+ ctx.removeCatalogListeners(client);
4040
+ try {
4041
+ await client.close?.();
4042
+ } catch (err) {
4043
+ ctx.log.warn(`MCP server "${slot.cfg.name}" error during idle sleep close`, err);
4044
+ }
3968
4045
  slot.client = void 0;
3969
4046
  }
3970
4047
  slot.onDisconnect = void 0;
@@ -3978,7 +4055,7 @@ async function sweepIdleSlots(ctx) {
3978
4055
  if (ctx.idleTimeoutMs <= 0) return false;
3979
4056
  const now = Date.now();
3980
4057
  for (const slot of ctx.servers.values()) {
3981
- if (slot.lazy && slot.state === "connected" && slot.client && now - slot.lastUsed > ctx.idleTimeoutMs) {
4058
+ if (slot.lazy && slot.state === "connected" && slot.client && slot.operations.inFlightCalls === 0 && now - slot.lastUsed > ctx.idleTimeoutMs) {
3982
4059
  await sleepIdleSlot(ctx, slot);
3983
4060
  }
3984
4061
  }
@@ -4066,6 +4143,7 @@ function scheduleRegistryReconnect({
4066
4143
  slot.reconnectTimer = void 0;
4067
4144
  void attemptReconnect(slot);
4068
4145
  }, delay);
4146
+ slot.reconnectTimer.unref?.();
4069
4147
  }
4070
4148
 
4071
4149
  // src/registry.ts
@@ -4213,7 +4291,7 @@ var MCPRegistry = class _MCPRegistry {
4213
4291
  if (!slot) throw new Error(`MCP server "${name}" not registered`);
4214
4292
  slot.lastUsed = Date.now();
4215
4293
  if (slot.client && slot.state === "connected") return slot.client;
4216
- const waking = slot.state === "dormant";
4294
+ const waking = slot.state === "dormant" && !slot.connecting;
4217
4295
  if (waking) {
4218
4296
  slot.operations.wakeCount++;
4219
4297
  this.recordOperation(slot, "wake", "lazy-demand");
@@ -4289,22 +4367,21 @@ var MCPRegistry = class _MCPRegistry {
4289
4367
  }
4290
4368
  slot.state = "disconnected";
4291
4369
  if (slot.client) {
4292
- slot.client.removeExitListener(this.onChildExit);
4293
- if (slot.onDisconnect) slot.client.removeDisconnectListener(slot.onDisconnect);
4294
- slot.client.removeToolsChangedListener(this.onToolsChanged);
4295
- this.removeCatalogListeners(slot.client);
4296
- await slot.client.close();
4370
+ const client = slot.client;
4371
+ client.removeExitListener?.(this.onChildExit);
4372
+ if (slot.onDisconnect) client.removeDisconnectListener?.(slot.onDisconnect);
4373
+ client.removeToolsChangedListener?.(this.onToolsChanged);
4374
+ this.removeCatalogListeners(client);
4375
+ try {
4376
+ await client.close?.();
4377
+ } catch (err) {
4378
+ this.log.warn(`MCP server "${name}" error during stop close`, err);
4379
+ }
4297
4380
  slot.client = void 0;
4298
4381
  }
4299
4382
  slot.onDisconnect = void 0;
4300
4383
  slot.connecting = void 0;
4301
- for (const t of slot.toolNames) this.toolRegistry.unregister(t);
4302
- slot.toolNames = [];
4303
- slot.lazyTools = [];
4304
- slot.serverMetadata = void 0;
4305
- slot.resources = void 0;
4306
- slot.resourceTemplates = void 0;
4307
- slot.prompts = void 0;
4384
+ resetDisconnectedSlotTools(slot, this.toolRegistry);
4308
4385
  slot.registeredLazy = false;
4309
4386
  this.recordOperation(slot, "stop", "manual");
4310
4387
  this.events.emit("mcp.server.disconnected", { name, reason: "stop" });
@@ -4348,8 +4425,17 @@ var MCPRegistry = class _MCPRegistry {
4348
4425
  }
4349
4426
  getCatalog(name) {
4350
4427
  const slot = this.servers.get(name);
4351
- if (!slot) return void 0;
4352
- return registryCatalogSnapshot(slot);
4428
+ if (slot) {
4429
+ return registryCatalogSnapshot(slot);
4430
+ }
4431
+ const disabled = this.disabledServers.get(name);
4432
+ if (disabled) {
4433
+ return {
4434
+ name: disabled.name,
4435
+ state: "idle"
4436
+ };
4437
+ }
4438
+ return void 0;
4353
4439
  }
4354
4440
  async listResources(name, opts = {}) {
4355
4441
  const slot = this.requireSlot(name);
@@ -4506,9 +4592,16 @@ var MCPRegistry = class _MCPRegistry {
4506
4592
  clearInterval(this.idleTimer);
4507
4593
  this.idleTimer = void 0;
4508
4594
  }
4509
- for (const name of Array.from(this.servers.keys())) {
4510
- await this.stop(name);
4511
- }
4595
+ const names = Array.from(this.servers.keys());
4596
+ await Promise.all(
4597
+ names.map(async (name) => {
4598
+ try {
4599
+ await this.stop(name);
4600
+ } catch (err) {
4601
+ this.log.warn(`MCP server "${name}" failed to stop during stopAll`, err);
4602
+ }
4603
+ })
4604
+ );
4512
4605
  this.disabledServers.clear();
4513
4606
  }
4514
4607
  /**
@@ -4570,15 +4663,19 @@ var MCPRegistry = class _MCPRegistry {
4570
4663
  client.addPromptsChangedListener(this.onPromptsChanged);
4571
4664
  }
4572
4665
  removeCatalogListeners(client) {
4573
- client.removeResourcesChangedListener(this.onResourcesChanged);
4574
- client.removePromptsChangedListener(this.onPromptsChanged);
4666
+ client.removeResourcesChangedListener?.(this.onResourcesChanged);
4667
+ client.removePromptsChangedListener?.(this.onPromptsChanged);
4575
4668
  }
4576
4669
  onChildExit = (name, code, _signal) => {
4577
4670
  const slot = this.servers.get(name);
4578
4671
  if (!slot) return;
4579
4672
  if (slot.lazy) {
4580
4673
  this.recordFailure(slot, "transport", "process-exit-lazy");
4581
- markLazySlotDormant(slot, this.events, `exit:${code ?? "unknown"}`);
4674
+ markLazySlotDormant(slot, this.events, `exit:${code ?? "unknown"}`, {
4675
+ onChildExit: this.onChildExit,
4676
+ onToolsChanged: this.onToolsChanged,
4677
+ removeCatalogListeners: (c) => this.removeCatalogListeners(c)
4678
+ });
4582
4679
  return;
4583
4680
  }
4584
4681
  resetDisconnectedSlotTools(slot, this.toolRegistry);
@@ -4593,7 +4690,11 @@ var MCPRegistry = class _MCPRegistry {
4593
4690
  if (!slot) return;
4594
4691
  if (slot.lazy) {
4595
4692
  this.recordFailure(slot, "transport", "http-disconnect-lazy");
4596
- markLazySlotDormant(slot, this.events, "http-disconnect");
4693
+ markLazySlotDormant(slot, this.events, "http-disconnect", {
4694
+ onChildExit: this.onChildExit,
4695
+ onToolsChanged: this.onToolsChanged,
4696
+ removeCatalogListeners: (c) => this.removeCatalogListeners(c)
4697
+ });
4597
4698
  return;
4598
4699
  }
4599
4700
  resetDisconnectedSlotTools(slot, this.toolRegistry);
@@ -4956,11 +5057,14 @@ function serveStdio(server, opts = {}) {
4956
5057
  onEnd();
4957
5058
  return;
4958
5059
  }
4959
- let idx = buffer.indexOf("\n");
5060
+ let start = 0;
5061
+ let idx = buffer.indexOf("\n", start);
4960
5062
  while (idx !== -1) {
4961
- const line = buffer.slice(0, idx);
4962
- buffer = buffer.slice(idx + 1);
4963
- idx = buffer.indexOf("\n");
5063
+ let end = idx;
5064
+ if (end > start && buffer.charCodeAt(end - 1) === 13) end--;
5065
+ const line = buffer.slice(start, end);
5066
+ start = idx + 1;
5067
+ idx = buffer.indexOf("\n", start);
4964
5068
  if (!line.trim()) continue;
4965
5069
  const handler = server.handleMessage(line).then((res) => {
4966
5070
  if (res !== null) writeLine(res);
@@ -4978,6 +5082,7 @@ function serveStdio(server, opts = {}) {
4978
5082
  });
4979
5083
  inFlightHandlers.add(handler);
4980
5084
  }
5085
+ if (start > 0) buffer = buffer.slice(start);
4981
5086
  };
4982
5087
  let resolveDone;
4983
5088
  const done = new Promise((resolve) => {
@@ -4989,6 +5094,25 @@ function serveStdio(server, opts = {}) {
4989
5094
  if (closed) return;
4990
5095
  closed = true;
4991
5096
  stdin.off("data", onData);
5097
+ if (!bufferTooLarge && buffer.trim()) {
5098
+ const line = buffer.trim();
5099
+ buffer = "";
5100
+ const handler = server.handleMessage(line).then((res) => {
5101
+ if (res !== null) writeLine(res);
5102
+ }).catch((err) => {
5103
+ console.error(
5104
+ JSON.stringify({
5105
+ level: "error",
5106
+ event: "mcp_server.handle_message_failed",
5107
+ message: toErrorMessage2(err),
5108
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
5109
+ })
5110
+ );
5111
+ }).finally(() => {
5112
+ inFlightHandlers.delete(handler);
5113
+ });
5114
+ inFlightHandlers.add(handler);
5115
+ }
4992
5116
  resolveDone();
4993
5117
  };
4994
5118
  stdin.on("data", onData);
@@ -5073,18 +5197,24 @@ async function handleHttpRequest(server, req, res, token, log, boundHost) {
5073
5197
  return send(415, JSON.stringify({ error: "content-type must be application/json" }));
5074
5198
  }
5075
5199
  let body = "";
5200
+ let aborted = false;
5076
5201
  req.on("data", (chunk) => {
5202
+ if (aborted) return;
5077
5203
  body += chunk.toString("utf8");
5078
5204
  if (body.length > HTTP_BODY_CAP) {
5205
+ aborted = true;
5079
5206
  send(413, JSON.stringify({ error: "payload too large" }));
5080
5207
  req.destroy();
5081
5208
  }
5082
5209
  });
5083
5210
  req.on("end", () => {
5211
+ if (aborted) return;
5084
5212
  void server.handleMessage(body).then((out) => {
5213
+ if (aborted) return;
5085
5214
  if (out === null) return send(202, "");
5086
5215
  return send(200, out);
5087
5216
  }).catch((err) => {
5217
+ if (aborted) return;
5088
5218
  log?.warn?.(`MCP http handler error: ${toErrorMessage2(err)}`);
5089
5219
  send(500, JSON.stringify({ error: "internal error" }));
5090
5220
  });
@@ -2,5 +2,12 @@ import type { EventBus } from '@wrongstack/core/kernel';
2
2
  import type { ToolRegistry } from '@wrongstack/core/registry';
3
3
  import type { ServerSlot } from './registry-slots.js';
4
4
  export declare function resetDisconnectedSlotTools(slot: ServerSlot, toolRegistry: ToolRegistry): void;
5
- export declare function markLazySlotDormant(slot: ServerSlot, events: EventBus, reason: string): void;
5
+ export interface MarkLazySlotDormantOptions {
6
+ onChildExit?: (name: string, code: number | null, signal: string | null) => void;
7
+ onToolsChanged?: (name: string, tools: {
8
+ name: string;
9
+ }[]) => void;
10
+ removeCatalogListeners?: (client: import('./client.js').MCPClient) => void;
11
+ }
12
+ export declare function markLazySlotDormant(slot: ServerSlot, events: EventBus, reason: string, options?: MarkLazySlotDormantOptions): void;
6
13
  //# sourceMappingURL=registry-disconnect.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wrongstack/mcp",
3
- "version": "0.319.0",
3
+ "version": "0.320.1",
4
4
  "license": "MIT",
5
5
  "description": "WrongStack Model Context Protocol client and registry: stdio, SSE, and streamable HTTP transports.",
6
6
  "repository": {
@@ -26,7 +26,7 @@
26
26
  "!dist/**/*.map"
27
27
  ],
28
28
  "dependencies": {
29
- "@wrongstack/core": "0.319.0"
29
+ "@wrongstack/core": "0.320.1"
30
30
  },
31
31
  "devDependencies": {
32
32
  "@types/node": "^26.2.0",