@wrongstack/mcp 0.319.1 → 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) {
@@ -3611,7 +3664,8 @@ function wrapMCPTool(serverName, mcpTool, client, permission = "confirm", observ
3611
3664
  const signal = opts?.signal ?? ctx?.signal;
3612
3665
  const res = await live.callTool(mcpTool.name, input, signal ? { signal } : void 0);
3613
3666
  if (res.isError) {
3614
- throw new Error(stringify(res.content));
3667
+ const errText = stringify(res.content);
3668
+ throw new Error(errText || `MCP tool "${qualifiedName}" failed`);
3615
3669
  }
3616
3670
  ok = true;
3617
3671
  return stringify(res.content);
@@ -3660,6 +3714,7 @@ function applySlotTools(ctx, slot, tools, client) {
3660
3714
  },
3661
3715
  onFinish: ({ durationMs, ok }) => {
3662
3716
  slot.operations.inFlightCalls = Math.max(0, slot.operations.inFlightCalls - 1);
3717
+ slot.lastUsed = Date.now();
3663
3718
  pushBounded(slot.operations.callSamples, durationMs, MCP_OPERATION_LIMITS.LATENCY_SAMPLES);
3664
3719
  if (ok) {
3665
3720
  ctx.recordSuccess(slot);
@@ -3783,7 +3838,7 @@ async function attemptConnectSlot(ctx, slot) {
3783
3838
  client.addToolsChangedListener(ctx.onToolsChanged);
3784
3839
  ctx.addCatalogListeners(client);
3785
3840
  await client.connect();
3786
- 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) {
3787
3842
  client.removeExitListener(ctx.onChildExit);
3788
3843
  if (boundDisconnect) client.removeDisconnectListener(boundDisconnect);
3789
3844
  client.removeToolsChangedListener(ctx.onToolsChanged);
@@ -3886,8 +3941,28 @@ function resetDisconnectedSlotTools(slot, toolRegistry) {
3886
3941
  slot.resourceTemplates = void 0;
3887
3942
  slot.prompts = void 0;
3888
3943
  }
3889
- function markLazySlotDormant(slot, events, reason) {
3890
- 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;
3891
3966
  slot.state = "dormant";
3892
3967
  events.emit("mcp.server.disconnected", {
3893
3968
  name: slot.cfg.name,
@@ -3950,17 +4025,23 @@ function buildRegistryOperationalHealth(servers, disabledServers) {
3950
4025
 
3951
4026
  // src/registry-idle.ts
3952
4027
  async function sleepIdleSlot(ctx, slot) {
4028
+ if (slot.operations.inFlightCalls > 0) return;
3953
4029
  slot.reconnectPending = false;
3954
4030
  if (slot.reconnectTimer) {
3955
4031
  clearTimeout(slot.reconnectTimer);
3956
4032
  slot.reconnectTimer = void 0;
3957
4033
  }
3958
4034
  if (slot.client) {
3959
- slot.client.removeExitListener(ctx.onChildExit);
3960
- if (slot.onDisconnect) slot.client.removeDisconnectListener(slot.onDisconnect);
3961
- slot.client.removeToolsChangedListener(ctx.onToolsChanged);
3962
- ctx.removeCatalogListeners(slot.client);
3963
- 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
+ }
3964
4045
  slot.client = void 0;
3965
4046
  }
3966
4047
  slot.onDisconnect = void 0;
@@ -3974,7 +4055,7 @@ async function sweepIdleSlots(ctx) {
3974
4055
  if (ctx.idleTimeoutMs <= 0) return false;
3975
4056
  const now = Date.now();
3976
4057
  for (const slot of ctx.servers.values()) {
3977
- 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) {
3978
4059
  await sleepIdleSlot(ctx, slot);
3979
4060
  }
3980
4061
  }
@@ -4062,6 +4143,7 @@ function scheduleRegistryReconnect({
4062
4143
  slot.reconnectTimer = void 0;
4063
4144
  void attemptReconnect(slot);
4064
4145
  }, delay);
4146
+ slot.reconnectTimer.unref?.();
4065
4147
  }
4066
4148
 
4067
4149
  // src/registry.ts
@@ -4209,7 +4291,7 @@ var MCPRegistry = class _MCPRegistry {
4209
4291
  if (!slot) throw new Error(`MCP server "${name}" not registered`);
4210
4292
  slot.lastUsed = Date.now();
4211
4293
  if (slot.client && slot.state === "connected") return slot.client;
4212
- const waking = slot.state === "dormant";
4294
+ const waking = slot.state === "dormant" && !slot.connecting;
4213
4295
  if (waking) {
4214
4296
  slot.operations.wakeCount++;
4215
4297
  this.recordOperation(slot, "wake", "lazy-demand");
@@ -4285,22 +4367,21 @@ var MCPRegistry = class _MCPRegistry {
4285
4367
  }
4286
4368
  slot.state = "disconnected";
4287
4369
  if (slot.client) {
4288
- slot.client.removeExitListener(this.onChildExit);
4289
- if (slot.onDisconnect) slot.client.removeDisconnectListener(slot.onDisconnect);
4290
- slot.client.removeToolsChangedListener(this.onToolsChanged);
4291
- this.removeCatalogListeners(slot.client);
4292
- 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
+ }
4293
4380
  slot.client = void 0;
4294
4381
  }
4295
4382
  slot.onDisconnect = void 0;
4296
4383
  slot.connecting = void 0;
4297
- for (const t of slot.toolNames) this.toolRegistry.unregister(t);
4298
- slot.toolNames = [];
4299
- slot.lazyTools = [];
4300
- slot.serverMetadata = void 0;
4301
- slot.resources = void 0;
4302
- slot.resourceTemplates = void 0;
4303
- slot.prompts = void 0;
4384
+ resetDisconnectedSlotTools(slot, this.toolRegistry);
4304
4385
  slot.registeredLazy = false;
4305
4386
  this.recordOperation(slot, "stop", "manual");
4306
4387
  this.events.emit("mcp.server.disconnected", { name, reason: "stop" });
@@ -4344,8 +4425,17 @@ var MCPRegistry = class _MCPRegistry {
4344
4425
  }
4345
4426
  getCatalog(name) {
4346
4427
  const slot = this.servers.get(name);
4347
- if (!slot) return void 0;
4348
- 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;
4349
4439
  }
4350
4440
  async listResources(name, opts = {}) {
4351
4441
  const slot = this.requireSlot(name);
@@ -4502,9 +4592,16 @@ var MCPRegistry = class _MCPRegistry {
4502
4592
  clearInterval(this.idleTimer);
4503
4593
  this.idleTimer = void 0;
4504
4594
  }
4505
- for (const name of Array.from(this.servers.keys())) {
4506
- await this.stop(name);
4507
- }
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
+ );
4508
4605
  this.disabledServers.clear();
4509
4606
  }
4510
4607
  /**
@@ -4566,15 +4663,19 @@ var MCPRegistry = class _MCPRegistry {
4566
4663
  client.addPromptsChangedListener(this.onPromptsChanged);
4567
4664
  }
4568
4665
  removeCatalogListeners(client) {
4569
- client.removeResourcesChangedListener(this.onResourcesChanged);
4570
- client.removePromptsChangedListener(this.onPromptsChanged);
4666
+ client.removeResourcesChangedListener?.(this.onResourcesChanged);
4667
+ client.removePromptsChangedListener?.(this.onPromptsChanged);
4571
4668
  }
4572
4669
  onChildExit = (name, code, _signal) => {
4573
4670
  const slot = this.servers.get(name);
4574
4671
  if (!slot) return;
4575
4672
  if (slot.lazy) {
4576
4673
  this.recordFailure(slot, "transport", "process-exit-lazy");
4577
- 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
+ });
4578
4679
  return;
4579
4680
  }
4580
4681
  resetDisconnectedSlotTools(slot, this.toolRegistry);
@@ -4589,7 +4690,11 @@ var MCPRegistry = class _MCPRegistry {
4589
4690
  if (!slot) return;
4590
4691
  if (slot.lazy) {
4591
4692
  this.recordFailure(slot, "transport", "http-disconnect-lazy");
4592
- 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
+ });
4593
4698
  return;
4594
4699
  }
4595
4700
  resetDisconnectedSlotTools(slot, this.toolRegistry);
@@ -4952,11 +5057,14 @@ function serveStdio(server, opts = {}) {
4952
5057
  onEnd();
4953
5058
  return;
4954
5059
  }
4955
- let idx = buffer.indexOf("\n");
5060
+ let start = 0;
5061
+ let idx = buffer.indexOf("\n", start);
4956
5062
  while (idx !== -1) {
4957
- const line = buffer.slice(0, idx);
4958
- buffer = buffer.slice(idx + 1);
4959
- 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);
4960
5068
  if (!line.trim()) continue;
4961
5069
  const handler = server.handleMessage(line).then((res) => {
4962
5070
  if (res !== null) writeLine(res);
@@ -4974,6 +5082,7 @@ function serveStdio(server, opts = {}) {
4974
5082
  });
4975
5083
  inFlightHandlers.add(handler);
4976
5084
  }
5085
+ if (start > 0) buffer = buffer.slice(start);
4977
5086
  };
4978
5087
  let resolveDone;
4979
5088
  const done = new Promise((resolve) => {
@@ -4985,6 +5094,25 @@ function serveStdio(server, opts = {}) {
4985
5094
  if (closed) return;
4986
5095
  closed = true;
4987
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
+ }
4988
5116
  resolveDone();
4989
5117
  };
4990
5118
  stdin.on("data", onData);
@@ -5069,18 +5197,24 @@ async function handleHttpRequest(server, req, res, token, log, boundHost) {
5069
5197
  return send(415, JSON.stringify({ error: "content-type must be application/json" }));
5070
5198
  }
5071
5199
  let body = "";
5200
+ let aborted = false;
5072
5201
  req.on("data", (chunk) => {
5202
+ if (aborted) return;
5073
5203
  body += chunk.toString("utf8");
5074
5204
  if (body.length > HTTP_BODY_CAP) {
5205
+ aborted = true;
5075
5206
  send(413, JSON.stringify({ error: "payload too large" }));
5076
5207
  req.destroy();
5077
5208
  }
5078
5209
  });
5079
5210
  req.on("end", () => {
5211
+ if (aborted) return;
5080
5212
  void server.handleMessage(body).then((out) => {
5213
+ if (aborted) return;
5081
5214
  if (out === null) return send(202, "");
5082
5215
  return send(200, out);
5083
5216
  }).catch((err) => {
5217
+ if (aborted) return;
5084
5218
  log?.warn?.(`MCP http handler error: ${toErrorMessage2(err)}`);
5085
5219
  send(500, JSON.stringify({ error: "internal error" }));
5086
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.1",
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.1"
29
+ "@wrongstack/core": "0.320.1"
30
30
  },
31
31
  "devDependencies": {
32
32
  "@types/node": "^26.2.0",