@wrongstack/mcp 1.0.28 → 1.0.32

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/index.js CHANGED
@@ -6,7 +6,7 @@ import * as dns from "node:dns/promises";
6
6
  import * as http from "node:http";
7
7
  import * as https from "node:https";
8
8
  import * as net from "node:net";
9
- import { isPrivateIPv4, isPrivateIPv6 } from "@wrongstack/core/utils";
9
+ import { isPrivateIPv4, isPrivateIPv6, toErrorMessage } from "@wrongstack/core/utils";
10
10
  var MCPOAuthHttpError = class extends Error {
11
11
  constructor(message, status) {
12
12
  super(message);
@@ -216,10 +216,10 @@ function requiredString(value, field) {
216
216
  return value;
217
217
  }
218
218
  function optionalString(value, field) {
219
- return value === void 0 ? void 0 : requiredString(value, field);
219
+ return value === void 0 || value === null || value === "" ? void 0 : requiredString(value, field);
220
220
  }
221
221
  function optionalBoolean(value, field) {
222
- if (value === void 0) return void 0;
222
+ if (value === void 0 || value === null) return void 0;
223
223
  if (typeof value !== "boolean") {
224
224
  throw new Error(`MCP authorization field "${field}" must be a boolean`);
225
225
  }
@@ -232,7 +232,7 @@ function boundedStringArray(value, field, maxItems) {
232
232
  return [...new Set(value.map((entry) => requiredString(entry, field)))];
233
233
  }
234
234
  function optionalStringArray(value, field, maxItems) {
235
- return value === void 0 ? [] : boundedStringArray(value, field, maxItems);
235
+ return value === void 0 || value === null ? [] : boundedStringArray(value, field, maxItems);
236
236
  }
237
237
  function secureOAuthUrl(value, label2) {
238
238
  let url;
@@ -263,7 +263,7 @@ async function discoverFirst(candidates, fetchJson, signal, parse, label2) {
263
263
  return { url: candidate, value: parse(value) };
264
264
  } catch (error) {
265
265
  signal?.throwIfAborted();
266
- failures.push(`${candidate}: ${error instanceof Error ? error.message : String(error)}`);
266
+ failures.push(`${candidate}: ${toErrorMessage(error)}`);
267
267
  }
268
268
  }
269
269
  throw new Error(`MCP ${label2} discovery failed (${failures.join("; ")})`);
@@ -634,7 +634,7 @@ function parseTokenResponse(value, resource) {
634
634
  }
635
635
  const expiresIn = response["expires_in"];
636
636
  let expiresAt;
637
- if (expiresIn !== void 0) {
637
+ if (expiresIn !== void 0 && expiresIn !== null) {
638
638
  if (typeof expiresIn !== "number" || !Number.isFinite(expiresIn) || expiresIn <= 0 || expiresIn > 31536e3) {
639
639
  throw new Error("MCP OAuth expires_in must be between 1 second and 1 year");
640
640
  }
@@ -1154,9 +1154,7 @@ function boundedServerName(value) {
1154
1154
  }
1155
1155
 
1156
1156
  // src/client.ts
1157
- import { spawn as spawn2 } from "node:child_process";
1158
- import { StringDecoder } from "node:string_decoder";
1159
- import { buildChildEnv as buildChildEnv2, buildWin32CmdShimInvocation, toErrorMessage } from "@wrongstack/core/utils";
1157
+ import { StringDecoder as StringDecoder2 } from "node:string_decoder";
1160
1158
 
1161
1159
  // src/client-protocol-helpers.ts
1162
1160
  var MAX_PROTOCOL_INPUT_CHARS = 8192;
@@ -1479,7 +1477,7 @@ function resolveHttpBearerHeaders(options, env = process.env) {
1479
1477
  if (!/^[A-Za-z_][A-Za-z0-9_]{0,127}$/.test(envName)) {
1480
1478
  throw new Error(`MCP bearerTokenEnv "${envName}" is not a valid environment variable name`);
1481
1479
  }
1482
- const token = env[envName];
1480
+ const token = Object.hasOwn(env, envName) ? env[envName] : void 0;
1483
1481
  if (!token) throw new Error(`MCP "${options.name}" requires environment variable ${envName}`);
1484
1482
  if (token.length > 16384 || /[\r\n]/.test(token)) {
1485
1483
  throw new Error(`MCP "${options.name}" bearer token is oversized or contains newlines`);
@@ -1528,7 +1526,9 @@ var SSE_READER_MAX_BUFFER = 256 * 1024;
1528
1526
  var SSE_READER_MAX_DATA_LINES = 1024;
1529
1527
  var SSEReader = class {
1530
1528
  buffer = "";
1529
+ skipLeadingLF = false;
1531
1530
  dataLines = [];
1531
+ dataLength = 0;
1532
1532
  eventName = "";
1533
1533
  endpointListeners = [];
1534
1534
  /**
@@ -1553,15 +1553,21 @@ var SSEReader = class {
1553
1553
  }
1554
1554
  feed(chunk) {
1555
1555
  const carried = this.buffer.length;
1556
+ if (this.skipLeadingLF && chunk.length > 0) {
1557
+ this.skipLeadingLF = false;
1558
+ if (chunk.startsWith("\n")) chunk = chunk.slice(1);
1559
+ }
1556
1560
  this.buffer += chunk;
1557
1561
  let start = 0;
1558
- let idx = this.buffer.indexOf("\n", start);
1559
- while (idx !== -1) {
1560
- let end = idx;
1561
- if (end > start && this.buffer.charCodeAt(end - 1) === 13) end--;
1562
- this.processLine(this.buffer.slice(start, end));
1562
+ for (let idx = 0; idx < this.buffer.length; idx++) {
1563
+ const code = this.buffer.charCodeAt(idx);
1564
+ if (code !== 10 && code !== 13) continue;
1565
+ this.processLine(this.buffer.slice(start, idx));
1566
+ if (code === 13) {
1567
+ if (this.buffer.charCodeAt(idx + 1) === 10) idx++;
1568
+ else if (idx + 1 === this.buffer.length) this.skipLeadingLF = true;
1569
+ }
1563
1570
  start = idx + 1;
1564
- idx = this.buffer.indexOf("\n", start);
1565
1571
  }
1566
1572
  if (start > 0) this.buffer = this.buffer.slice(start);
1567
1573
  if (this.buffer.length <= SSE_READER_MAX_BUFFER) return;
@@ -1585,6 +1591,13 @@ var SSEReader = class {
1585
1591
  });
1586
1592
  }
1587
1593
  processLine(line) {
1594
+ if (line.length > SSE_READER_MAX_BUFFER) {
1595
+ throw new ToolError({
1596
+ message: `SSE: completed line exceeds ${SSE_READER_MAX_BUFFER} characters`,
1597
+ code: "TOOL_EXECUTION_FAILED",
1598
+ toolName: "mcp_transport_sse_reader"
1599
+ });
1600
+ }
1588
1601
  if (line === "") {
1589
1602
  this.flush();
1590
1603
  return;
@@ -1597,6 +1610,14 @@ var SSEReader = class {
1597
1610
  if (field === "event") {
1598
1611
  this.eventName = value;
1599
1612
  } else if (field === "data") {
1613
+ const nextLength = this.dataLength + value.length + (this.dataLines.length > 0 ? 1 : 0);
1614
+ if (nextLength > SSE_READER_MAX_BUFFER) {
1615
+ throw new ToolError({
1616
+ message: `SSE: event data exceeds ${SSE_READER_MAX_BUFFER} characters`,
1617
+ code: "TOOL_EXECUTION_FAILED",
1618
+ toolName: "mcp_transport_sse_reader"
1619
+ });
1620
+ }
1600
1621
  if (this.dataLines.length >= SSE_READER_MAX_DATA_LINES) {
1601
1622
  throw new ToolError({
1602
1623
  message: `SSE: exceeded ${SSE_READER_MAX_DATA_LINES} data lines per event \u2014 upstream is not sending blank-line delimiters`,
@@ -1609,12 +1630,14 @@ var SSEReader = class {
1609
1630
  }
1610
1631
  });
1611
1632
  }
1633
+ this.dataLength = nextLength;
1612
1634
  this.dataLines.push(value);
1613
1635
  }
1614
1636
  }
1615
1637
  flush() {
1616
1638
  const eventName = this.eventName;
1617
1639
  this.eventName = "";
1640
+ this.dataLength = 0;
1618
1641
  if (this.dataLines.length === 0) {
1619
1642
  return;
1620
1643
  }
@@ -1646,8 +1669,10 @@ var SSEReader = class {
1646
1669
  }
1647
1670
  reset() {
1648
1671
  this.buffer = "";
1672
+ this.skipLeadingLF = false;
1649
1673
  this.dataLines = [];
1650
1674
  this.eventName = "";
1675
+ this.dataLength = 0;
1651
1676
  this.listeners = [];
1652
1677
  this.endpointListeners = [];
1653
1678
  }
@@ -1896,9 +1921,14 @@ function checkValue(field, raw) {
1896
1921
  function validateElicitationContent(fields, content) {
1897
1922
  const out = {};
1898
1923
  for (const field of fields) {
1899
- const raw = content[field.name];
1924
+ const raw = Object.hasOwn(content, field.name) ? content[field.name] : void 0;
1900
1925
  if (Array.isArray(raw) && raw.length === 0 && field.kind === "multi-enum" && field.minItems === 0) {
1901
- out[field.name] = [];
1926
+ Object.defineProperty(out, field.name, {
1927
+ value: [],
1928
+ enumerable: true,
1929
+ writable: true,
1930
+ configurable: true
1931
+ });
1902
1932
  continue;
1903
1933
  }
1904
1934
  if (raw === void 0 || raw === null || Array.isArray(raw) && raw.length === 0) {
@@ -1907,7 +1937,12 @@ function validateElicitationContent(fields, content) {
1907
1937
  }
1908
1938
  const checked = checkValue(field, raw);
1909
1939
  if (!checked.ok) return checked;
1910
- out[field.name] = checked.value;
1940
+ Object.defineProperty(out, field.name, {
1941
+ value: checked.value,
1942
+ enumerable: true,
1943
+ writable: true,
1944
+ configurable: true
1945
+ });
1911
1946
  }
1912
1947
  return { ok: true, value: out };
1913
1948
  }
@@ -1965,7 +2000,7 @@ var ServerRequestResponder = class {
1965
2000
  return { jsonrpc: "2.0", id, result: { action: "cancel" } };
1966
2001
  } finally {
1967
2002
  clearTimeout(deadline);
1968
- this.pending.delete(id);
2003
+ if (this.pending.get(id) === controller) this.pending.delete(id);
1969
2004
  }
1970
2005
  }
1971
2006
  /**
@@ -2008,9 +2043,20 @@ var ServerRequestResponder = class {
2008
2043
  import { ToolError as ToolError2 } from "@wrongstack/core/types";
2009
2044
  var MAX_MCP_HTTP_BODY_BYTES = 16 * 1024 * 1024;
2010
2045
  async function readBodyCapped(res, maxBytes = MAX_MCP_HTTP_BODY_BYTES) {
2046
+ const safeCap = typeof maxBytes === "number" && Number.isFinite(maxBytes) && maxBytes >= 0 ? maxBytes : MAX_MCP_HTTP_BODY_BYTES;
2011
2047
  const body = res.body;
2012
2048
  if (!body || typeof body.getReader !== "function") {
2013
- return await res.text();
2049
+ const text = await res.text();
2050
+ const received = Buffer.byteLength(text, "utf8");
2051
+ if (received > safeCap) {
2052
+ throw new ToolError2({
2053
+ message: `MCP response body exceeded ${safeCap} bytes \u2014 refusing to buffer`,
2054
+ code: "TOOL_EXECUTION_FAILED",
2055
+ toolName: "mcp_transport",
2056
+ context: { maxBytes: safeCap, received }
2057
+ });
2058
+ }
2059
+ return text;
2014
2060
  }
2015
2061
  const reader = body.getReader();
2016
2062
  const chunks = [];
@@ -2021,13 +2067,13 @@ async function readBodyCapped(res, maxBytes = MAX_MCP_HTTP_BODY_BYTES) {
2021
2067
  if (done) break;
2022
2068
  if (!value) continue;
2023
2069
  total += value.byteLength;
2024
- if (total > maxBytes) {
2070
+ if (total > safeCap) {
2025
2071
  await reader.cancel().catch(() => void 0);
2026
2072
  throw new ToolError2({
2027
- message: `MCP response body exceeded ${maxBytes} bytes \u2014 refusing to buffer`,
2073
+ message: `MCP response body exceeded ${safeCap} bytes \u2014 refusing to buffer`,
2028
2074
  code: "TOOL_EXECUTION_FAILED",
2029
2075
  toolName: "mcp_transport",
2030
- context: { maxBytes, received: total }
2076
+ context: { maxBytes: safeCap, received: total }
2031
2077
  });
2032
2078
  }
2033
2079
  chunks.push(value);
@@ -2232,15 +2278,16 @@ function createTimeoutSignal(parent, timeoutMs, hold) {
2232
2278
  } else {
2233
2279
  parent?.addEventListener("abort", onAbort, { once: true });
2234
2280
  }
2281
+ const safeTimeout = typeof timeoutMs === "number" && Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : 6e4;
2235
2282
  const onTimeout = () => {
2236
2283
  if (hold?.()) {
2237
- timer = setTimeout(onTimeout, timeoutMs);
2284
+ timer = setTimeout(onTimeout, safeTimeout);
2238
2285
  timer.unref?.();
2239
2286
  return;
2240
2287
  }
2241
- ctrl.abort(new Error(`MCP HTTP request timed out after ${timeoutMs}ms`));
2288
+ ctrl.abort(new Error(`MCP HTTP request timed out after ${safeTimeout}ms`));
2242
2289
  };
2243
- let timer = setTimeout(onTimeout, timeoutMs);
2290
+ let timer = setTimeout(onTimeout, safeTimeout);
2244
2291
  timer.unref?.();
2245
2292
  return {
2246
2293
  signal: ctrl.signal,
@@ -2283,8 +2330,8 @@ var BaseHTTPTransport = class {
2283
2330
  this.headers = { ...opts.headers };
2284
2331
  this.authorizationProvider = opts.authorizationProvider;
2285
2332
  this.authorizationResource = canonicalMcpResource(opts.url);
2286
- this.timeout = opts.startupTimeoutMs ?? 1e4;
2287
- this.requestTimeout = opts.requestTimeoutMs ?? 6e4;
2333
+ this.timeout = typeof opts.startupTimeoutMs === "number" && Number.isFinite(opts.startupTimeoutMs) && opts.startupTimeoutMs > 0 ? opts.startupTimeoutMs : 1e4;
2334
+ this.requestTimeout = typeof opts.requestTimeoutMs === "number" && Number.isFinite(opts.requestTimeoutMs) && opts.requestTimeoutMs > 0 ? opts.requestTimeoutMs : 6e4;
2288
2335
  if (opts.tls) {
2289
2336
  if (opts.tls.rejectUnauthorized === false) {
2290
2337
  const where = urlOriginForLog(this.url);
@@ -2566,7 +2613,7 @@ function extractJsonRpcEnvelopes(text) {
2566
2613
  } catch {
2567
2614
  }
2568
2615
  };
2569
- for (const raw of text.split("\n")) {
2616
+ for (const raw of text.split(/\r\n|\r|\n/)) {
2570
2617
  const line = raw.replace(/\r$/, "");
2571
2618
  if (line === "") {
2572
2619
  flush();
@@ -2771,6 +2818,8 @@ var SSETransport = class extends BaseHTTPTransport {
2771
2818
  _nextId = 1;
2772
2819
  readerDone = false;
2773
2820
  closed = false;
2821
+ toolCatalogVersion = 0;
2822
+ toolCatalogRevision = 0;
2774
2823
  readLoopAbort;
2775
2824
  reader;
2776
2825
  /** POST target announced by the server's `endpoint` event. */
@@ -2789,10 +2838,12 @@ var SSETransport = class extends BaseHTTPTransport {
2789
2838
  }
2790
2839
  /** Refresh tool list when server sends notifications/tools/list_changed. */
2791
2840
  async handleToolsListChanged() {
2841
+ const version = ++this.toolCatalogVersion;
2792
2842
  try {
2793
2843
  const tools = await listAllTools((params) => this.httpPost("tools/list", params));
2794
- if (!tools) return;
2844
+ if (!tools || version !== this.toolCatalogVersion) return;
2795
2845
  this.tools.splice(0, this.tools.length, ...tools);
2846
+ this.toolCatalogRevision++;
2796
2847
  for (const cb of this.toolsChangedListeners) {
2797
2848
  try {
2798
2849
  cb([...this.tools]);
@@ -2803,14 +2854,21 @@ var SSETransport = class extends BaseHTTPTransport {
2803
2854
  }
2804
2855
  }
2805
2856
  async connect() {
2857
+ this.toolCatalogVersion++;
2806
2858
  this.readerDone = false;
2807
2859
  this.closed = false;
2808
2860
  this.endpointUrl = void 0;
2809
2861
  this.state = "connecting";
2810
2862
  this.serverMetadata = void 0;
2811
- this.abortController = new AbortController();
2812
- const signal = this.abortController.signal;
2813
- const startupTimer = setTimeout(() => this.abortController?.abort(), this.timeout);
2863
+ const controller = new AbortController();
2864
+ this.abortController = controller;
2865
+ const signal = controller.signal;
2866
+ const assertCurrent = () => {
2867
+ if (this.abortController !== controller || signal.aborted || this.closed) {
2868
+ throw makeAbortError("connect");
2869
+ }
2870
+ };
2871
+ const startupTimer = setTimeout(() => controller.abort(), this.timeout);
2814
2872
  try {
2815
2873
  const sseUrl = this.buildSSEUrl();
2816
2874
  const fetchOpts = {
@@ -2819,6 +2877,12 @@ var SSETransport = class extends BaseHTTPTransport {
2819
2877
  };
2820
2878
  this.applyTlsAgent(fetchOpts);
2821
2879
  const response = await this.fetchWithAuthorization(sseUrl, fetchOpts, signal);
2880
+ try {
2881
+ assertCurrent();
2882
+ } catch (error) {
2883
+ await response.body?.cancel().catch(() => void 0);
2884
+ throw error;
2885
+ }
2822
2886
  if (!response.ok) {
2823
2887
  throw new ToolError4({
2824
2888
  message: `SSE connect HTTP ${response.status}: ${response.statusText}`,
@@ -2837,15 +2901,18 @@ var SSETransport = class extends BaseHTTPTransport {
2837
2901
  }
2838
2902
  const textDecoder = new TextDecoder();
2839
2903
  const sseReader = new SSEReader();
2840
- this.readLoopAbort = new AbortController();
2904
+ const readLoopAbort = new AbortController();
2905
+ this.readLoopAbort = readLoopAbort;
2841
2906
  const streamReady = new Promise((resolve) => {
2842
2907
  this.streamSignal = resolve;
2843
2908
  });
2844
2909
  sseReader.onEndpoint((endpoint) => {
2910
+ if (this.abortController !== controller || signal.aborted || this.closed) return;
2845
2911
  this.acceptEndpoint(endpoint);
2846
2912
  this.streamSignal?.();
2847
2913
  });
2848
2914
  sseReader.onMessage((msg) => {
2915
+ if (this.abortController !== controller || signal.aborted || this.closed) return;
2849
2916
  this.streamSignal?.();
2850
2917
  const id = msg.id;
2851
2918
  if (msg.method && (typeof id === "number" || typeof id === "string")) {
@@ -2884,8 +2951,9 @@ var SSETransport = class extends BaseHTTPTransport {
2884
2951
  cancel: () => reader.cancel(),
2885
2952
  releaseLock: () => reader.releaseLock()
2886
2953
  };
2887
- void this.readSSEBody(reader, textDecoder, sseReader);
2954
+ void this.readSSEBody(reader, textDecoder, sseReader, controller, readLoopAbort.signal);
2888
2955
  await this.waitForStream(streamReady, signal);
2956
+ assertCurrent();
2889
2957
  const initRes = await this.httpPost("initialize", {
2890
2958
  protocolVersion: MCP_CONSTANTS.PROTOCOL_VERSION,
2891
2959
  // Client capabilities: elicitation when the host can ask its user.
@@ -2893,6 +2961,7 @@ var SSETransport = class extends BaseHTTPTransport {
2893
2961
  capabilities: this.serverRequests.capabilities(),
2894
2962
  clientInfo: MCP_CONSTANTS.CLIENT_INFO
2895
2963
  });
2964
+ assertCurrent();
2896
2965
  if (initRes.error) {
2897
2966
  throw new ToolError4({
2898
2967
  message: `initialize failed: ${initRes.error.message}`,
@@ -2907,17 +2976,24 @@ var SSETransport = class extends BaseHTTPTransport {
2907
2976
  await this.httpPost("notifications/initialized", {});
2908
2977
  } catch {
2909
2978
  }
2979
+ assertCurrent();
2980
+ const revision = this.toolCatalogRevision;
2910
2981
  const tools = await listAllTools((params) => this.httpPost("tools/list", params));
2911
- this.tools.splice(0, this.tools.length, ...tools ?? []);
2982
+ assertCurrent();
2983
+ if (revision === this.toolCatalogRevision) {
2984
+ this.tools.splice(0, this.tools.length, ...tools ?? []);
2985
+ }
2912
2986
  this.state = "connected";
2913
2987
  clearTimeout(startupTimer);
2914
2988
  } catch (err) {
2915
2989
  clearTimeout(startupTimer);
2916
- this.state = "failed";
2917
- this.abortController.abort();
2990
+ if (this.abortController === controller && !this.closed) {
2991
+ this.state = "failed";
2992
+ }
2993
+ controller.abort();
2918
2994
  throw err;
2919
2995
  } finally {
2920
- this.streamSignal = void 0;
2996
+ if (this.abortController === controller) this.streamSignal = void 0;
2921
2997
  }
2922
2998
  }
2923
2999
  /** Resolve once the stream announced its endpoint, emitted anything, ended, or the cap passed. */
@@ -2955,9 +3031,9 @@ var SSETransport = class extends BaseHTTPTransport {
2955
3031
  } catch {
2956
3032
  }
2957
3033
  }
2958
- async readSSEBody(reader, decoder, sseReader) {
3034
+ async readSSEBody(reader, decoder, sseReader, controller, signal) {
2959
3035
  try {
2960
- while (!this.readerDone) {
3036
+ while (!this.readerDone && !signal?.aborted && (!controller || this.abortController === controller) && !this.closed) {
2961
3037
  const { done, value } = await reader.read();
2962
3038
  if (done) break;
2963
3039
  const chunk = decoder.decode(value, { stream: true });
@@ -2965,11 +3041,13 @@ var SSETransport = class extends BaseHTTPTransport {
2965
3041
  }
2966
3042
  } catch {
2967
3043
  } finally {
2968
- this.streamSignal?.();
2969
- this.rejectStreamPending("SSE stream closed");
2970
- if (!this.readerDone && this.state !== "disconnected" && this.state !== "failed") {
2971
- this.state = "disconnected";
2972
- this.notifyDisconnect();
3044
+ if (!controller || this.abortController === controller) {
3045
+ this.streamSignal?.();
3046
+ this.rejectStreamPending("SSE stream closed");
3047
+ if (!this.readerDone && !signal?.aborted && !this.closed && this.state !== "disconnected" && this.state !== "failed") {
3048
+ this.state = "disconnected";
3049
+ this.notifyDisconnect();
3050
+ }
2973
3051
  }
2974
3052
  }
2975
3053
  }
@@ -3115,6 +3193,7 @@ var SSETransport = class extends BaseHTTPTransport {
3115
3193
  return { jsonrpc: "2.0", id: result.id, result: result.result, error: result.error };
3116
3194
  }
3117
3195
  async close() {
3196
+ this.toolCatalogVersion++;
3118
3197
  this.releasePinnedDispatcher();
3119
3198
  const alreadyClosed = this.closed;
3120
3199
  this.closed = true;
@@ -3155,6 +3234,8 @@ function invalidResponse(method, url, err) {
3155
3234
  var StreamableHTTPTransport = class _StreamableHTTPTransport extends BaseHTTPTransport {
3156
3235
  _nextId = 1;
3157
3236
  closed = false;
3237
+ toolCatalogVersion = 0;
3238
+ toolCatalogRevision = 0;
3158
3239
  sessionId;
3159
3240
  constructor(opts) {
3160
3241
  super(opts, "StreamableHTTP");
@@ -3250,10 +3331,12 @@ var StreamableHTTPTransport = class _StreamableHTTPTransport extends BaseHTTPTra
3250
3331
  }
3251
3332
  }
3252
3333
  async refreshTools() {
3334
+ const version = ++this.toolCatalogVersion;
3253
3335
  try {
3254
3336
  const tools = await listAllTools((params) => this.postRaw("tools/list", params));
3255
- if (!tools) return;
3337
+ if (!tools || version !== this.toolCatalogVersion) return;
3256
3338
  this.tools.splice(0, this.tools.length, ...tools);
3339
+ this.toolCatalogRevision++;
3257
3340
  for (const listener of this.toolsChangedListeners) {
3258
3341
  try {
3259
3342
  listener([...tools]);
@@ -3264,12 +3347,19 @@ var StreamableHTTPTransport = class _StreamableHTTPTransport extends BaseHTTPTra
3264
3347
  }
3265
3348
  }
3266
3349
  async connect() {
3350
+ this.toolCatalogVersion++;
3267
3351
  this.closed = false;
3268
3352
  this.state = "connecting";
3269
3353
  this.serverMetadata = void 0;
3270
- this.abortController = new AbortController();
3271
- const signal = this.abortController.signal;
3272
- const startupTimer = setTimeout(() => this.abortController?.abort(), this.timeout);
3354
+ const controller = new AbortController();
3355
+ this.abortController = controller;
3356
+ const signal = controller.signal;
3357
+ const assertCurrent = () => {
3358
+ if (this.abortController !== controller || signal.aborted || this.closed) {
3359
+ throw makeAbortError("connect");
3360
+ }
3361
+ };
3362
+ const startupTimer = setTimeout(() => controller.abort(), this.timeout);
3273
3363
  try {
3274
3364
  const initializeId = this.genId();
3275
3365
  const initFetchOpts = {
@@ -3295,6 +3385,7 @@ var StreamableHTTPTransport = class _StreamableHTTPTransport extends BaseHTTPTra
3295
3385
  };
3296
3386
  this.applyTlsAgent(initFetchOpts);
3297
3387
  const initRes = await this.fetchWithAuthorization(this.url, initFetchOpts, signal);
3388
+ assertCurrent();
3298
3389
  if (!initRes.ok) {
3299
3390
  throw new Error(`initialize HTTP ${initRes.status}: ${initRes.statusText}`);
3300
3391
  }
@@ -3306,6 +3397,7 @@ var StreamableHTTPTransport = class _StreamableHTTPTransport extends BaseHTTPTra
3306
3397
  } else {
3307
3398
  data = extractJsonRpcResults(await readBodyCapped(initRes))[0];
3308
3399
  }
3400
+ assertCurrent();
3309
3401
  if (!data) {
3310
3402
  throw new Error("Could not parse initialize response");
3311
3403
  }
@@ -3317,14 +3409,21 @@ var StreamableHTTPTransport = class _StreamableHTTPTransport extends BaseHTTPTra
3317
3409
  this.protocolVersion = this.serverMetadata.protocolVersion;
3318
3410
  this.sessionId = initRes.headers.get("mcp-session-id") ?? void 0;
3319
3411
  await this.postRaw("notifications/initialized", {});
3412
+ assertCurrent();
3413
+ const revision = this.toolCatalogRevision;
3320
3414
  const tools = await listAllTools((params) => this.postRaw("tools/list", params));
3321
- this.tools.splice(0, this.tools.length, ...tools ?? []);
3415
+ assertCurrent();
3416
+ if (revision === this.toolCatalogRevision) {
3417
+ this.tools.splice(0, this.tools.length, ...tools ?? []);
3418
+ }
3322
3419
  this.state = "connected";
3323
3420
  clearTimeout(startupTimer);
3324
3421
  } catch (err) {
3325
3422
  clearTimeout(startupTimer);
3326
- this.state = "failed";
3327
- this.abortController.abort();
3423
+ if (this.abortController === controller && !this.closed) {
3424
+ this.state = "failed";
3425
+ }
3426
+ controller.abort();
3328
3427
  throw err;
3329
3428
  }
3330
3429
  }
@@ -3441,6 +3540,7 @@ var StreamableHTTPTransport = class _StreamableHTTPTransport extends BaseHTTPTra
3441
3540
  return toToolCallResult(res);
3442
3541
  }
3443
3542
  async close() {
3543
+ this.toolCatalogVersion++;
3444
3544
  this.releasePinnedDispatcher();
3445
3545
  const alreadyClosed = this.closed;
3446
3546
  this.closed = true;
@@ -3465,9 +3565,17 @@ var StreamableHTTPTransport = class _StreamableHTTPTransport extends BaseHTTPTra
3465
3565
  }
3466
3566
  };
3467
3567
  function completeEventsEnd(text) {
3468
- const lf = text.lastIndexOf("\n\n");
3469
- const crlf = text.lastIndexOf("\r\n\r\n");
3470
- return Math.max(lf === -1 ? 0 : lf + 2, crlf === -1 ? 0 : crlf + 4);
3568
+ let start = 0;
3569
+ let end = 0;
3570
+ for (let idx = 0; idx < text.length; idx++) {
3571
+ const code = text.charCodeAt(idx);
3572
+ if (code !== 10 && code !== 13) continue;
3573
+ const empty = idx === start;
3574
+ if (code === 13 && text.charCodeAt(idx + 1) === 10) idx++;
3575
+ start = idx + 1;
3576
+ if (empty) end = start;
3577
+ }
3578
+ return end;
3471
3579
  }
3472
3580
 
3473
3581
  // src/client-http-connection.ts
@@ -3578,8 +3686,248 @@ async function connectStreamableHTTP(host) {
3578
3686
  host.state = "connected";
3579
3687
  }
3580
3688
 
3689
+ // src/client-stdio.ts
3690
+ import { spawn as spawn2 } from "node:child_process";
3691
+ import { StringDecoder } from "node:string_decoder";
3692
+ import { buildChildEnv as buildChildEnv2, buildWin32CmdShimInvocation, toErrorMessage as toErrorMessage2 } from "@wrongstack/core/utils";
3693
+ async function connectStdio(host) {
3694
+ if (!host.opts.command) {
3695
+ host.state = "failed";
3696
+ throw new Error('MCP stdio transport requires "command"');
3697
+ }
3698
+ host.rxBuffer = "";
3699
+ host.rxBufferBytes = 0;
3700
+ host.rxDecoder = new StringDecoder("utf8");
3701
+ const extraEnv = { ...host.opts.env };
3702
+ if (host.opts.passthroughEnv) {
3703
+ for (const name of host.opts.passthroughEnv) {
3704
+ const val = process.env[name];
3705
+ if (val !== void 0) {
3706
+ extraEnv[name] = val;
3707
+ }
3708
+ }
3709
+ }
3710
+ const isWin = process.platform === "win32";
3711
+ const rawArgs = host.opts.args ?? [];
3712
+ const spawnEnv = buildChildEnv2({ extra: extraEnv });
3713
+ const stdio = ["pipe", "pipe", "pipe"];
3714
+ const child = isWin ? (() => {
3715
+ const shim = buildWin32CmdShimInvocation(host.opts.command, rawArgs);
3716
+ return spawn2(shim.command, shim.args, {
3717
+ env: spawnEnv,
3718
+ stdio,
3719
+ ...host.opts.cwd ? { cwd: host.opts.cwd } : {},
3720
+ windowsVerbatimArguments: shim.windowsVerbatimArguments,
3721
+ // Without this every MCP server spawned from a console-less host
3722
+ // (WebUI server, scheduled runs) opens a visible console window.
3723
+ windowsHide: true
3724
+ });
3725
+ })() : spawn2(host.opts.command, rawArgs, {
3726
+ env: spawnEnv,
3727
+ stdio,
3728
+ windowsHide: true,
3729
+ ...host.opts.cwd ? { cwd: host.opts.cwd } : {}
3730
+ });
3731
+ host.child = child;
3732
+ child.stdout?.on("data", (chunk) => host.onData(host.rxDecoder.write(chunk)));
3733
+ child.stdout?.on("end", () => {
3734
+ const tail = host.rxDecoder.end();
3735
+ if (tail) host.onData(tail);
3736
+ if (host.rxBuffer.trim()) {
3737
+ const line = host.rxBuffer.trim();
3738
+ host.rxBuffer = "";
3739
+ host.onLine(line);
3740
+ }
3741
+ });
3742
+ child.stderr?.on("data", () => {
3743
+ });
3744
+ child.stdin?.on("error", (err) => {
3745
+ host.failPending(`MCP "${host.opts.name}" stdin error: ${toErrorMessage2(err)}`);
3746
+ });
3747
+ child.on("exit", (code, signal) => {
3748
+ host.state = "disconnected";
3749
+ host.failPending(
3750
+ `MCP "${host.opts.name}" child exited (code=${code ?? "null"} signal=${signal ?? "null"})`
3751
+ );
3752
+ for (const listener of host.exitListeners) {
3753
+ try {
3754
+ listener(host.opts.name, code, signal);
3755
+ } catch {
3756
+ }
3757
+ }
3758
+ });
3759
+ child.on("error", (err) => {
3760
+ host.state = "failed";
3761
+ host.failPending(`MCP "${host.opts.name}" child error: ${toErrorMessage2(err)}`);
3762
+ });
3763
+ const initialize = await host.request(
3764
+ "initialize",
3765
+ {
3766
+ protocolVersion: MCP_CONSTANTS.PROTOCOL_VERSION,
3767
+ // Client capabilities: elicitation when the host can ask its user.
3768
+ // `tools` is a SERVER capability and never belonged here.
3769
+ capabilities: host.serverRequests.capabilities(),
3770
+ clientInfo: MCP_CONSTANTS.CLIENT_INFO
3771
+ },
3772
+ typeof host.opts.startupTimeoutMs === "number" && Number.isFinite(host.opts.startupTimeoutMs) && host.opts.startupTimeoutMs > 0 ? host.opts.startupTimeoutMs : 1e4
3773
+ );
3774
+ if (initialize.error) {
3775
+ host.state = "failed";
3776
+ throw new Error(`MCP initialize failed: ${initialize.error.message}`);
3777
+ }
3778
+ try {
3779
+ host._serverMetadata = parseServerMetadata(initialize.result);
3780
+ } catch (err) {
3781
+ host.state = "failed";
3782
+ throw new Error(`MCP initialize returned malformed server metadata: ${toErrorMessage2(err)}`);
3783
+ }
3784
+ try {
3785
+ await host.notify("notifications/initialized", {});
3786
+ } catch (err) {
3787
+ console.warn(
3788
+ '[MCP] notify("notifications/initialized") failed for "' + host.opts.name + '": ' + toErrorMessage2(err)
3789
+ );
3790
+ }
3791
+ const revision = host.toolCatalogRevision;
3792
+ const tools = await listAllTools((params) => host.request("tools/list", params)) ?? [];
3793
+ if (revision === host.toolCatalogRevision) host._tools = tools;
3794
+ host._toolsCache = host._tools;
3795
+ host.state = "connected";
3796
+ }
3797
+ async function notifyStdio(host, method, params) {
3798
+ if (host._drainPending) {
3799
+ host._lastNotifySkipped = true;
3800
+ console.warn(
3801
+ JSON.stringify({
3802
+ level: "warn",
3803
+ event: "mcp.notify_skipped_backpressure",
3804
+ server: host.opts.name,
3805
+ method,
3806
+ message: "stdin buffer backpressure (already waiting for drain)",
3807
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
3808
+ })
3809
+ );
3810
+ return;
3811
+ }
3812
+ const stdin = host.child?.stdin;
3813
+ if (!stdin || stdin.destroyed === true || stdin.writable === false) {
3814
+ return;
3815
+ }
3816
+ const req = { jsonrpc: "2.0", method, params };
3817
+ const encoded = JSON.stringify(req) + "\n";
3818
+ try {
3819
+ const ok = stdin.write(encoded);
3820
+ if (!ok) {
3821
+ host._drainPending = true;
3822
+ await new Promise((resolve, reject) => {
3823
+ const timeout = setTimeout(() => {
3824
+ stdin.removeListener?.("drain", onDrain);
3825
+ stdin.removeListener?.("error", onError);
3826
+ host._drainPending = false;
3827
+ reject(new Error(`MCP notify("${method}") drain timeout`));
3828
+ }, 500);
3829
+ const onDrain = () => {
3830
+ clearTimeout(timeout);
3831
+ stdin.removeListener?.("drain", onDrain);
3832
+ stdin.removeListener?.("error", onError);
3833
+ host._drainPending = false;
3834
+ resolve();
3835
+ };
3836
+ const onError = (err) => {
3837
+ clearTimeout(timeout);
3838
+ stdin.removeListener?.("drain", onDrain);
3839
+ stdin.removeListener?.("error", onError);
3840
+ host._drainPending = false;
3841
+ reject(err);
3842
+ };
3843
+ stdin.once?.("drain", onDrain);
3844
+ stdin.once?.("error", onError);
3845
+ });
3846
+ }
3847
+ } catch (err) {
3848
+ throw new Error(`[MCP] notify("${method}") failed: ${toErrorMessage2(err)}`);
3849
+ }
3850
+ }
3851
+
3852
+ // src/client-stdio-protocol.ts
3853
+ var MAX_RX_BUFFER_BYTES = 16 * 1024 * 1024;
3854
+ function receiveStdioData(host, s) {
3855
+ host.rxBufferBytes += Buffer.byteLength(s, "utf8");
3856
+ if (host.rxBufferBytes > MAX_RX_BUFFER_BYTES) {
3857
+ const truncated = host.rxBufferBytes;
3858
+ host.rxParts = [];
3859
+ host.rxBufferBytes = 0;
3860
+ host.failPending(
3861
+ `MCP "${host.opts.name}" rx buffer overflow (${truncated} bytes without a newline) \u2014 closing connection`
3862
+ );
3863
+ void host.close();
3864
+ return;
3865
+ }
3866
+ let start = 0;
3867
+ let idx = s.indexOf("\n");
3868
+ if (idx === -1) {
3869
+ host.rxParts.push(s);
3870
+ return;
3871
+ }
3872
+ while (idx !== -1) {
3873
+ const head = host.rxParts.length > 0 ? host.rxParts.join("") : "";
3874
+ host.rxParts = [];
3875
+ const line = (head + s.slice(start, idx)).trim();
3876
+ start = idx + 1;
3877
+ if (line) host.onLine(line);
3878
+ idx = s.indexOf("\n", start);
3879
+ }
3880
+ const tail = s.slice(start);
3881
+ host.rxBufferBytes = Buffer.byteLength(tail, "utf8");
3882
+ if (tail) host.rxParts.push(tail);
3883
+ }
3884
+ function receiveStdioLine(host, line) {
3885
+ let msg;
3886
+ try {
3887
+ msg = JSON.parse(line);
3888
+ } catch {
3889
+ return;
3890
+ }
3891
+ if (typeof msg !== "object" || msg === null) return;
3892
+ const envelope = msg;
3893
+ if (envelope["jsonrpc"] !== "2.0") return;
3894
+ if (typeof envelope["method"] === "string") {
3895
+ const id = envelope["id"];
3896
+ if (typeof id === "number" || typeof id === "string") {
3897
+ void host.handleServerRequest({
3898
+ jsonrpc: "2.0",
3899
+ id,
3900
+ method: envelope["method"],
3901
+ params: envelope["params"]
3902
+ });
3903
+ return;
3904
+ }
3905
+ if (Object.hasOwn(envelope, "id")) return;
3906
+ if (envelope["method"] === "notifications/cancelled") {
3907
+ host.serverRequests.cancel(envelope["params"]);
3908
+ } else if (envelope["method"] === "notifications/tools/list_changed") {
3909
+ void host.handleToolsListChanged();
3910
+ } else if (envelope["method"] === "notifications/resources/list_changed") {
3911
+ host.emitCapabilityChanged("resources");
3912
+ } else if (envelope["method"] === "notifications/prompts/list_changed") {
3913
+ host.emitCapabilityChanged("prompts");
3914
+ } else if (envelope["method"] === "notifications/resources/updated") {
3915
+ const uri = resourceUpdatedUri(envelope["params"]);
3916
+ if (uri) host.emitResourceUpdated(uri);
3917
+ }
3918
+ return;
3919
+ }
3920
+ if (!isJsonRpcResult(msg)) return;
3921
+ const response = msg;
3922
+ if (host.pending.has(response.id)) {
3923
+ const entry = host.pending.get(response.id);
3924
+ host.pending.delete(response.id);
3925
+ entry?.resolve(response);
3926
+ }
3927
+ }
3928
+
3581
3929
  // src/client.ts
3582
- var MCPClient = class _MCPClient {
3930
+ var MCPClient = class {
3583
3931
  constructor(opts) {
3584
3932
  this.opts = opts;
3585
3933
  this.serverRequests = new ServerRequestResponder(opts.elicitation);
@@ -3589,15 +3937,6 @@ var MCPClient = class _MCPClient {
3589
3937
  this.requestCapability.bind(this),
3590
3938
  this.requireResourceSubscriptions.bind(this)
3591
3939
  );
3592
- /**
3593
- * Maximum bytes the rx buffer may accumulate before the connection is
3594
- * forcefully closed. A well-behaved JSON-RPC server emits newline-delimited
3595
- * messages that are individually much smaller than this; a server that never
3596
- * sends a newline would grow the buffer without limit and OOM the process.
3597
- * 16 MiB is generous for any legitimate single message while bounding the
3598
- * worst-case memory to a predictable cap.
3599
- */
3600
- static MAX_RX_BUFFER_BYTES = 16 * 1024 * 1024;
3601
3940
  state = "idle";
3602
3941
  child;
3603
3942
  nextId = 1;
@@ -3628,7 +3967,7 @@ var MCPClient = class _MCPClient {
3628
3967
  * JSON-RPC payload. StringDecoder withholds the partial sequence until the
3629
3968
  * chunk that completes it (same remedy as `readFileHead` in core utils).
3630
3969
  */
3631
- rxDecoder = new StringDecoder("utf8");
3970
+ rxDecoder = new StringDecoder2("utf8");
3632
3971
  _tools = [];
3633
3972
  /** Server-declared handshake metadata. Populated for stdio in the first protocol slice. */
3634
3973
  _serverMetadata;
@@ -3638,6 +3977,8 @@ var MCPClient = class _MCPClient {
3638
3977
  _lastNotifySkipped = false;
3639
3978
  closePromise;
3640
3979
  connectPromise;
3980
+ toolCatalogVersion = 0;
3981
+ toolCatalogRevision = 0;
3641
3982
  // HTTP transports
3642
3983
  sseTransport;
3643
3984
  httpTransport;
@@ -3700,6 +4041,7 @@ var MCPClient = class _MCPClient {
3700
4041
  return this.connectPromise;
3701
4042
  }
3702
4043
  async connectInner() {
4044
+ this.toolCatalogVersion++;
3703
4045
  this.state = "connecting";
3704
4046
  this._serverMetadata = void 0;
3705
4047
  try {
@@ -3720,106 +4062,7 @@ var MCPClient = class _MCPClient {
3720
4062
  }
3721
4063
  }
3722
4064
  async connectStdio() {
3723
- if (!this.opts.command) {
3724
- this.state = "failed";
3725
- throw new Error('MCP stdio transport requires "command"');
3726
- }
3727
- this.rxBuffer = "";
3728
- this.rxBufferBytes = 0;
3729
- this.rxDecoder = new StringDecoder("utf8");
3730
- const extraEnv = { ...this.opts.env };
3731
- if (this.opts.passthroughEnv) {
3732
- for (const name of this.opts.passthroughEnv) {
3733
- const val = process.env[name];
3734
- if (val !== void 0) {
3735
- extraEnv[name] = val;
3736
- }
3737
- }
3738
- }
3739
- const isWin = process.platform === "win32";
3740
- const rawArgs = this.opts.args ?? [];
3741
- const spawnEnv = buildChildEnv2({ extra: extraEnv });
3742
- const stdio = ["pipe", "pipe", "pipe"];
3743
- const child = isWin ? (() => {
3744
- const shim = buildWin32CmdShimInvocation(this.opts.command, rawArgs);
3745
- return spawn2(shim.command, shim.args, {
3746
- env: spawnEnv,
3747
- stdio,
3748
- ...this.opts.cwd ? { cwd: this.opts.cwd } : {},
3749
- windowsVerbatimArguments: shim.windowsVerbatimArguments,
3750
- // Without this every MCP server spawned from a console-less host
3751
- // (WebUI server, scheduled runs) opens a visible console window.
3752
- windowsHide: true
3753
- });
3754
- })() : spawn2(this.opts.command, rawArgs, {
3755
- env: spawnEnv,
3756
- stdio,
3757
- windowsHide: true,
3758
- ...this.opts.cwd ? { cwd: this.opts.cwd } : {}
3759
- });
3760
- this.child = child;
3761
- child.stdout?.on("data", (chunk) => this.onData(this.rxDecoder.write(chunk)));
3762
- child.stdout?.on("end", () => {
3763
- const tail = this.rxDecoder.end();
3764
- if (tail) this.onData(tail);
3765
- if (this.rxBuffer.trim()) {
3766
- const line = this.rxBuffer.trim();
3767
- this.rxBuffer = "";
3768
- this.onLine(line);
3769
- }
3770
- });
3771
- child.stderr?.on("data", () => {
3772
- });
3773
- child.stdin?.on("error", (err) => {
3774
- this.failPending(`MCP "${this.opts.name}" stdin error: ${toErrorMessage(err)}`);
3775
- });
3776
- child.on("exit", (code, signal) => {
3777
- this.state = "disconnected";
3778
- this.failPending(
3779
- `MCP "${this.opts.name}" child exited (code=${code ?? "null"} signal=${signal ?? "null"})`
3780
- );
3781
- for (const listener of this.exitListeners) {
3782
- try {
3783
- listener(this.opts.name, code, signal);
3784
- } catch {
3785
- }
3786
- }
3787
- });
3788
- child.on("error", (err) => {
3789
- this.state = "failed";
3790
- this.failPending(`MCP "${this.opts.name}" child error: ${toErrorMessage(err)}`);
3791
- });
3792
- const initialize = await this.request(
3793
- "initialize",
3794
- {
3795
- protocolVersion: MCP_CONSTANTS.PROTOCOL_VERSION,
3796
- // Client capabilities: elicitation when the host can ask its user.
3797
- // `tools` is a SERVER capability and never belonged here.
3798
- capabilities: this.serverRequests.capabilities(),
3799
- clientInfo: MCP_CONSTANTS.CLIENT_INFO
3800
- },
3801
- this.opts.startupTimeoutMs ?? 1e4
3802
- );
3803
- if (initialize.error) {
3804
- this.state = "failed";
3805
- throw new Error(`MCP initialize failed: ${initialize.error.message}`);
3806
- }
3807
- try {
3808
- this._serverMetadata = parseServerMetadata(initialize.result);
3809
- } catch (err) {
3810
- this.state = "failed";
3811
- throw new Error(`MCP initialize returned malformed server metadata: ${toErrorMessage(err)}`);
3812
- }
3813
- try {
3814
- await this.notify("notifications/initialized", {});
3815
- } catch (err) {
3816
- console.warn(
3817
- '[MCP] notify("notifications/initialized") failed for "' + this.opts.name + '": ' + toErrorMessage(err)
3818
- );
3819
- }
3820
- this._tools = await listAllTools((params) => this.request("tools/list", params)) ?? [];
3821
- this._toolsCache = this._tools;
3822
- this.state = "connected";
4065
+ return connectStdio(this.clientStdioHost());
3823
4066
  }
3824
4067
  async connectSSE() {
3825
4068
  return connectSSE(this.clientHttpConnectionHost());
@@ -3876,6 +4119,7 @@ var MCPClient = class _MCPClient {
3876
4119
  return this.closePromise;
3877
4120
  }
3878
4121
  async closeInner() {
4122
+ this.toolCatalogVersion++;
3879
4123
  if (this.child) {
3880
4124
  const child = this.child;
3881
4125
  const exitPromise = new Promise((resolve) => {
@@ -3923,9 +4167,13 @@ var MCPClient = class _MCPClient {
3923
4167
  await Promise.allSettled([this.sseTransport?.close(), this.httpTransport?.close()]);
3924
4168
  this.state = "disconnected";
3925
4169
  }
3926
- request(method, params, timeoutMs = this.opts.requestTimeoutMs ?? 6e4, opts) {
3927
- if (this.sseTransport) return this.sseTransport.request(method, params, timeoutMs, opts);
3928
- if (this.httpTransport) return this.httpTransport.request(method, params, timeoutMs, opts);
4170
+ request(method, params, timeoutMs, opts) {
4171
+ const defaultTimeoutMs = typeof this.opts.requestTimeoutMs === "number" && Number.isFinite(this.opts.requestTimeoutMs) && this.opts.requestTimeoutMs > 0 ? this.opts.requestTimeoutMs : 6e4;
4172
+ const effectiveTimeoutMs = typeof timeoutMs === "number" && Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : defaultTimeoutMs;
4173
+ if (this.sseTransport)
4174
+ return this.sseTransport.request(method, params, effectiveTimeoutMs, opts);
4175
+ if (this.httpTransport)
4176
+ return this.httpTransport.request(method, params, effectiveTimeoutMs, opts);
3929
4177
  const signal = opts?.signal;
3930
4178
  if (signal?.aborted) {
3931
4179
  const err = new Error(`MCP "${this.opts.name}" request "${method}" aborted before send`);
@@ -3955,13 +4203,15 @@ var MCPClient = class _MCPClient {
3955
4203
  };
3956
4204
  const onTimeout = () => {
3957
4205
  if (this.serverRequests.awaitingUser) {
3958
- entry.timer = setTimeout(onTimeout, timeoutMs);
4206
+ entry.timer = setTimeout(onTimeout, effectiveTimeoutMs);
3959
4207
  return;
3960
4208
  }
3961
4209
  this.pending.delete(id);
3962
4210
  detach();
3963
4211
  reject(
3964
- new Error(`MCP "${this.opts.name}" request "${method}" timed out after ${timeoutMs}ms`)
4212
+ new Error(
4213
+ `MCP "${this.opts.name}" request "${method}" timed out after ${effectiveTimeoutMs}ms`
4214
+ )
3965
4215
  );
3966
4216
  };
3967
4217
  const entry = {
@@ -3975,7 +4225,7 @@ var MCPClient = class _MCPClient {
3975
4225
  detach();
3976
4226
  reject(err);
3977
4227
  },
3978
- timer: setTimeout(onTimeout, timeoutMs)
4228
+ timer: setTimeout(onTimeout, effectiveTimeoutMs)
3979
4229
  };
3980
4230
  this.pending.set(id, entry);
3981
4231
  const stdin = this.child?.stdin;
@@ -4047,132 +4297,13 @@ var MCPClient = class _MCPClient {
4047
4297
  this.pending.clear();
4048
4298
  }
4049
4299
  async notify(method, params) {
4050
- if (this._drainPending) {
4051
- this._lastNotifySkipped = true;
4052
- console.warn(
4053
- JSON.stringify({
4054
- level: "warn",
4055
- event: "mcp.notify_skipped_backpressure",
4056
- server: this.opts.name,
4057
- method,
4058
- message: "stdin buffer backpressure (already waiting for drain)",
4059
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
4060
- })
4061
- );
4062
- return;
4063
- }
4064
- const stdin = this.child?.stdin;
4065
- if (!stdin || stdin.destroyed === true || stdin.writable === false) {
4066
- return;
4067
- }
4068
- const req = { jsonrpc: "2.0", method, params };
4069
- const encoded = JSON.stringify(req) + "\n";
4070
- try {
4071
- const ok = stdin.write(encoded);
4072
- if (!ok) {
4073
- this._drainPending = true;
4074
- await new Promise((resolve, reject) => {
4075
- const timeout = setTimeout(() => {
4076
- stdin.removeListener?.("drain", onDrain);
4077
- stdin.removeListener?.("error", onError);
4078
- this._drainPending = false;
4079
- reject(new Error(`MCP notify("${method}") drain timeout`));
4080
- }, 500);
4081
- const onDrain = () => {
4082
- clearTimeout(timeout);
4083
- stdin.removeListener?.("drain", onDrain);
4084
- stdin.removeListener?.("error", onError);
4085
- this._drainPending = false;
4086
- resolve();
4087
- };
4088
- const onError = (err) => {
4089
- clearTimeout(timeout);
4090
- stdin.removeListener?.("drain", onDrain);
4091
- stdin.removeListener?.("error", onError);
4092
- this._drainPending = false;
4093
- reject(err);
4094
- };
4095
- stdin.once?.("drain", onDrain);
4096
- stdin.once?.("error", onError);
4097
- });
4098
- }
4099
- } catch (err) {
4100
- throw new Error(`[MCP] notify("${method}") failed: ${toErrorMessage(err)}`);
4101
- }
4300
+ return notifyStdio(this.clientStdioHost(), method, params);
4102
4301
  }
4103
4302
  onData(s) {
4104
- this.rxBufferBytes += Buffer.byteLength(s, "utf8");
4105
- if (this.rxBufferBytes > _MCPClient.MAX_RX_BUFFER_BYTES) {
4106
- const truncated = this.rxBufferBytes;
4107
- this.rxParts = [];
4108
- this.rxBufferBytes = 0;
4109
- this.failPending(
4110
- `MCP "${this.opts.name}" rx buffer overflow (${truncated} bytes without a newline) \u2014 closing connection`
4111
- );
4112
- void this.close();
4113
- return;
4114
- }
4115
- let start = 0;
4116
- let idx = s.indexOf("\n");
4117
- if (idx === -1) {
4118
- this.rxParts.push(s);
4119
- return;
4120
- }
4121
- while (idx !== -1) {
4122
- const head = this.rxParts.length > 0 ? this.rxParts.join("") : "";
4123
- this.rxParts = [];
4124
- const line = (head + s.slice(start, idx)).trim();
4125
- start = idx + 1;
4126
- if (line) this.onLine(line);
4127
- idx = s.indexOf("\n", start);
4128
- }
4129
- const tail = s.slice(start);
4130
- this.rxBufferBytes = Buffer.byteLength(tail, "utf8");
4131
- if (tail) this.rxParts.push(tail);
4303
+ receiveStdioData(this.clientStdioProtocolHost(), s);
4132
4304
  }
4133
4305
  onLine(line) {
4134
- let msg;
4135
- try {
4136
- msg = JSON.parse(line);
4137
- } catch {
4138
- return;
4139
- }
4140
- if (typeof msg !== "object" || msg === null) return;
4141
- const envelope = msg;
4142
- if (envelope["jsonrpc"] !== "2.0") return;
4143
- if (typeof envelope["method"] === "string") {
4144
- const id = envelope["id"];
4145
- if (typeof id === "number" || typeof id === "string") {
4146
- void this.handleServerRequest({
4147
- jsonrpc: "2.0",
4148
- id,
4149
- method: envelope["method"],
4150
- params: envelope["params"]
4151
- });
4152
- return;
4153
- }
4154
- if (Object.hasOwn(envelope, "id")) return;
4155
- if (envelope["method"] === "notifications/cancelled") {
4156
- this.serverRequests.cancel(envelope["params"]);
4157
- } else if (envelope["method"] === "notifications/tools/list_changed") {
4158
- void this.handleToolsListChanged();
4159
- } else if (envelope["method"] === "notifications/resources/list_changed") {
4160
- this.emitCapabilityChanged("resources");
4161
- } else if (envelope["method"] === "notifications/prompts/list_changed") {
4162
- this.emitCapabilityChanged("prompts");
4163
- } else if (envelope["method"] === "notifications/resources/updated") {
4164
- const uri = resourceUpdatedUri(envelope["params"]);
4165
- if (uri) this.emitResourceUpdated(uri);
4166
- }
4167
- return;
4168
- }
4169
- if (!isJsonRpcResult(msg)) return;
4170
- const response = msg;
4171
- if (this.pending.has(response.id)) {
4172
- const entry = this.pending.get(response.id);
4173
- this.pending.delete(response.id);
4174
- entry?.resolve(response);
4175
- }
4306
+ receiveStdioLine(this.clientStdioProtocolHost(), line);
4176
4307
  }
4177
4308
  async handleServerRequest(request3) {
4178
4309
  const response = await this.serverRequests.answer(request3);
@@ -4189,11 +4320,13 @@ var MCPClient = class _MCPClient {
4189
4320
  * to a hard crash on a transient notification glitch.
4190
4321
  */
4191
4322
  async handleToolsListChanged() {
4323
+ const version = ++this.toolCatalogVersion;
4192
4324
  try {
4193
4325
  const tools = await listAllTools((params) => this.request("tools/list", params));
4194
- if (!tools) return;
4326
+ if (!tools || version !== this.toolCatalogVersion) return;
4195
4327
  this._tools = tools;
4196
4328
  this._toolsCache = tools;
4329
+ this.toolCatalogRevision++;
4197
4330
  for (const listener of this.toolsChangedListeners) {
4198
4331
  try {
4199
4332
  listener(this.opts.name, [...tools]);
@@ -4247,9 +4380,7 @@ var MCPClient = class _MCPClient {
4247
4380
  clientHttpConnectionHost() {
4248
4381
  const self = this;
4249
4382
  return {
4250
- get opts() {
4251
- return self.opts;
4252
- },
4383
+ opts: this.opts,
4253
4384
  get state() {
4254
4385
  return self.state;
4255
4386
  },
@@ -4293,6 +4424,109 @@ var MCPClient = class _MCPClient {
4293
4424
  serverRequests: this.serverRequests
4294
4425
  };
4295
4426
  }
4427
+ clientStdioHost() {
4428
+ const self = this;
4429
+ return {
4430
+ opts: this.opts,
4431
+ get state() {
4432
+ return self.state;
4433
+ },
4434
+ set state(value) {
4435
+ self.state = value;
4436
+ },
4437
+ get rxBuffer() {
4438
+ return self.rxBuffer;
4439
+ },
4440
+ set rxBuffer(value) {
4441
+ self.rxBuffer = value;
4442
+ },
4443
+ get rxBufferBytes() {
4444
+ return self.rxBufferBytes;
4445
+ },
4446
+ set rxBufferBytes(value) {
4447
+ self.rxBufferBytes = value;
4448
+ },
4449
+ get rxDecoder() {
4450
+ return self.rxDecoder;
4451
+ },
4452
+ set rxDecoder(value) {
4453
+ self.rxDecoder = value;
4454
+ },
4455
+ get child() {
4456
+ return self.child;
4457
+ },
4458
+ set child(value) {
4459
+ self.child = value;
4460
+ },
4461
+ onData: (...args) => this.onData(...args),
4462
+ onLine: (...args) => this.onLine(...args),
4463
+ failPending: (...args) => this.failPending(...args),
4464
+ exitListeners: this.exitListeners,
4465
+ request: (...args) => this.request(...args),
4466
+ serverRequests: this.serverRequests,
4467
+ get _serverMetadata() {
4468
+ return self._serverMetadata;
4469
+ },
4470
+ set _serverMetadata(value) {
4471
+ self._serverMetadata = value;
4472
+ },
4473
+ notify: (...args) => this.notify(...args),
4474
+ get toolCatalogRevision() {
4475
+ return self.toolCatalogRevision;
4476
+ },
4477
+ get _tools() {
4478
+ return self._tools;
4479
+ },
4480
+ set _tools(value) {
4481
+ self._tools = value;
4482
+ },
4483
+ get _toolsCache() {
4484
+ return self._toolsCache;
4485
+ },
4486
+ set _toolsCache(value) {
4487
+ self._toolsCache = value;
4488
+ },
4489
+ get _drainPending() {
4490
+ return self._drainPending;
4491
+ },
4492
+ set _drainPending(value) {
4493
+ self._drainPending = value;
4494
+ },
4495
+ get _lastNotifySkipped() {
4496
+ return self._lastNotifySkipped;
4497
+ },
4498
+ set _lastNotifySkipped(value) {
4499
+ self._lastNotifySkipped = value;
4500
+ }
4501
+ };
4502
+ }
4503
+ clientStdioProtocolHost() {
4504
+ const self = this;
4505
+ return {
4506
+ get rxBufferBytes() {
4507
+ return self.rxBufferBytes;
4508
+ },
4509
+ set rxBufferBytes(value) {
4510
+ self.rxBufferBytes = value;
4511
+ },
4512
+ get rxParts() {
4513
+ return self.rxParts;
4514
+ },
4515
+ set rxParts(value) {
4516
+ self.rxParts = value;
4517
+ },
4518
+ failPending: (...args) => this.failPending(...args),
4519
+ opts: this.opts,
4520
+ close: (...args) => this.close(...args),
4521
+ onLine: (...args) => this.onLine(...args),
4522
+ handleServerRequest: (...args) => this.handleServerRequest(...args),
4523
+ serverRequests: this.serverRequests,
4524
+ handleToolsListChanged: (...args) => this.handleToolsListChanged(...args),
4525
+ emitCapabilityChanged: (...args) => this.emitCapabilityChanged(...args),
4526
+ emitResourceUpdated: (...args) => this.emitResourceUpdated(...args),
4527
+ pending: this.pending
4528
+ };
4529
+ }
4296
4530
  };
4297
4531
 
4298
4532
  // src/config-env.ts
@@ -4313,7 +4547,7 @@ function mcpEnvPlaceholders(cfg) {
4313
4547
  function expandMcpEnvPlaceholders(cfg, env = process.env) {
4314
4548
  const missing = /* @__PURE__ */ new Set();
4315
4549
  const expand = (value) => value.replace(PLACEHOLDER, (_match, name, fallback) => {
4316
- const resolved = env[name];
4550
+ const resolved = Object.hasOwn(env, name) ? env[name] : void 0;
4317
4551
  if (resolved !== void 0 && resolved !== "") return resolved;
4318
4552
  if (fallback !== void 0) return fallback;
4319
4553
  missing.add(name);
@@ -4672,7 +4906,12 @@ async function elicitViaUserInput(request3, fallback, options = {}) {
4672
4906
  }
4673
4907
  const content = {};
4674
4908
  form.fields.forEach((field, i) => {
4675
- content[field.name] = answerValue(field, byId.get(questionId(i)));
4909
+ Object.defineProperty(content, field.name, {
4910
+ value: answerValue(field, byId.get(questionId(i))),
4911
+ enumerable: true,
4912
+ writable: true,
4913
+ configurable: true
4914
+ });
4676
4915
  });
4677
4916
  const checked = validateElicitationContent(form.fields, content);
4678
4917
  if (checked.ok) return { action: "accept", content: checked.value };
@@ -5023,6 +5262,12 @@ function forgetRegistryState(registry, name) {
5023
5262
  import { createHash as createHash2, randomBytes as randomBytes4 } from "node:crypto";
5024
5263
  import * as fs2 from "node:fs/promises";
5025
5264
  import * as path from "node:path";
5265
+ function isRecord2(value) {
5266
+ return value !== null && typeof value === "object" && !Array.isArray(value);
5267
+ }
5268
+ function isCachedTool(value) {
5269
+ return isRecord2(value) && typeof value["name"] === "string" && value["name"].trim().length > 0 && (value["description"] === void 0 || typeof value["description"] === "string") && isRecord2(value["inputSchema"]) && (value["outputSchema"] === void 0 || isRecord2(value["outputSchema"]));
5270
+ }
5026
5271
  function manifestConfigHash(cfg) {
5027
5272
  const basis = JSON.stringify({
5028
5273
  transport: cfg.transport,
@@ -5058,7 +5303,8 @@ async function readCapabilityManifest(cacheDir, name, configHash) {
5058
5303
  try {
5059
5304
  const raw = await fs2.readFile(manifestFile(cacheDir, name), "utf8");
5060
5305
  const parsed = JSON.parse(raw);
5061
- if (parsed.configHash !== configHash || !Array.isArray(parsed.tools)) return null;
5306
+ if (parsed.configHash !== configHash || !Array.isArray(parsed.tools) || !parsed.tools.every(isCachedTool))
5307
+ return null;
5062
5308
  return {
5063
5309
  tools: parsed.tools,
5064
5310
  serverMetadata: parsed.serverMetadata === void 0 ? void 0 : parseServerMetadata(parsed.serverMetadata),
@@ -5275,6 +5521,13 @@ async function completeRegistryAuthorization(manager, cfg, name, callbackUrl, si
5275
5521
  // src/registry-catalog.ts
5276
5522
  var MAX_CATALOG_PAGES = 100;
5277
5523
  var MAX_CATALOG_ITEMS = 1e4;
5524
+ function advanceCatalogVersion(slot, kind) {
5525
+ slot.catalogVersions ??= {};
5526
+ const versions = slot.catalogVersions;
5527
+ const version = (versions[kind] ?? 0) + 1;
5528
+ versions[kind] = version;
5529
+ return version;
5530
+ }
5278
5531
  async function collectCatalogPages(load, select) {
5279
5532
  const items = [];
5280
5533
  const seenCursors = /* @__PURE__ */ new Set();
@@ -5307,10 +5560,105 @@ function registryCatalogSnapshot(slot) {
5307
5560
  };
5308
5561
  }
5309
5562
 
5563
+ // src/registry-catalog-operations.ts
5564
+ function getCatalog(name) {
5565
+ const slot = this.servers.get(name);
5566
+ if (slot) {
5567
+ return registryCatalogSnapshot(slot);
5568
+ }
5569
+ const disabled = this.disabledServers.get(name);
5570
+ if (disabled) {
5571
+ return {
5572
+ name: disabled.name,
5573
+ state: "idle"
5574
+ };
5575
+ }
5576
+ return void 0;
5577
+ }
5578
+ async function listResources(name, opts = {}) {
5579
+ const slot = this.requireSlot(name);
5580
+ if (!opts.refresh && slot.resources) return cloneCatalogRecords(slot.resources);
5581
+ const version = advanceCatalogVersion(slot, "resources");
5582
+ return this.withConnectedClient(name, async (client, assertCurrent) => {
5583
+ if (!client.getServerMetadata()?.capabilities.resources) return [];
5584
+ const resources = await collectCatalogPages(
5585
+ (cursor) => client.listResources(cursor ? { cursor } : {}),
5586
+ (page) => page.resources
5587
+ );
5588
+ assertCurrent();
5589
+ if (slot.catalogVersions?.resources === version) {
5590
+ slot.resources = resources;
5591
+ await this.persistCapabilityManifest(slot);
5592
+ }
5593
+ return cloneCatalogRecords(resources);
5594
+ });
5595
+ }
5596
+ async function listResourceTemplates(name, opts = {}) {
5597
+ const slot = this.requireSlot(name);
5598
+ if (!opts.refresh && slot.resourceTemplates) return cloneCatalogRecords(slot.resourceTemplates);
5599
+ const version = advanceCatalogVersion(slot, "resourceTemplates");
5600
+ return this.withConnectedClient(name, async (client, assertCurrent) => {
5601
+ if (!client.getServerMetadata()?.capabilities.resources) return [];
5602
+ const templates = await collectCatalogPages(
5603
+ (cursor) => client.listResourceTemplates(cursor ? { cursor } : {}),
5604
+ (page) => page.resourceTemplates
5605
+ );
5606
+ assertCurrent();
5607
+ if (slot.catalogVersions?.resourceTemplates === version) {
5608
+ slot.resourceTemplates = templates;
5609
+ await this.persistCapabilityManifest(slot);
5610
+ }
5611
+ return cloneCatalogRecords(templates);
5612
+ });
5613
+ }
5614
+ async function readResource(name, uri) {
5615
+ return this.withConnectedClient(name, (client) => client.readResource(uri));
5616
+ }
5617
+ async function selectResourceForInsertion(name, uri, policy) {
5618
+ return prepareResourceInsertion(name, uri, await this.readResource(name, uri), policy);
5619
+ }
5620
+ async function subscribeResource(name, uri) {
5621
+ await this.withConnectedClient(name, (client) => client.subscribeResource(uri));
5622
+ }
5623
+ async function unsubscribeResource(name, uri) {
5624
+ await this.withConnectedClient(name, (client) => client.unsubscribeResource(uri));
5625
+ }
5626
+ async function listPrompts(name, opts = {}) {
5627
+ const slot = this.requireSlot(name);
5628
+ if (!opts.refresh && slot.prompts) return cloneCatalogRecords(slot.prompts);
5629
+ const version = advanceCatalogVersion(slot, "prompts");
5630
+ return this.withConnectedClient(name, async (client, assertCurrent) => {
5631
+ if (!client.getServerMetadata()?.capabilities.prompts) return [];
5632
+ const prompts = await collectCatalogPages(
5633
+ (cursor) => client.listPrompts(cursor ? { cursor } : {}),
5634
+ (page) => page.prompts
5635
+ );
5636
+ assertCurrent();
5637
+ if (slot.catalogVersions?.prompts === version) {
5638
+ slot.prompts = prompts;
5639
+ await this.persistCapabilityManifest(slot);
5640
+ }
5641
+ return cloneCatalogRecords(prompts);
5642
+ });
5643
+ }
5644
+ async function getPrompt(serverName, promptName, args) {
5645
+ return this.withConnectedClient(serverName, (client) => client.getPrompt(promptName, args));
5646
+ }
5647
+ async function selectPromptForInsertion(serverName, promptName, args, policy) {
5648
+ return preparePromptInsertion(
5649
+ serverName,
5650
+ promptName,
5651
+ args,
5652
+ await this.getPrompt(serverName, promptName, args),
5653
+ policy
5654
+ );
5655
+ }
5656
+
5310
5657
  // src/registry-connect-loop.ts
5311
5658
  import { expectDefined } from "@wrongstack/core/utils";
5312
5659
 
5313
5660
  // src/wrap-tool.ts
5661
+ import { createSandboxMcpGate } from "@wrongstack/core/sandbox";
5314
5662
  import { ToolCapabilities } from "@wrongstack/core/security";
5315
5663
  import { mcpQualifiedToolName } from "@wrongstack/core/utils";
5316
5664
 
@@ -5377,9 +5725,10 @@ function isMutatingTool(mcpTool) {
5377
5725
  }
5378
5726
  return false;
5379
5727
  }
5380
- function wrapMCPTool(serverName, mcpTool, client, permission = "confirm", observer) {
5728
+ function wrapMCPTool(serverName, mcpTool, client, permission = "confirm", sandboxTrust = false, observer) {
5729
+ const sandboxGate = createSandboxMcpGate({ trusted: sandboxTrust });
5381
5730
  const qualifiedName = mcpQualifiedToolName(serverName, mcpTool.name);
5382
- return {
5731
+ const tool = {
5383
5732
  name: qualifiedName,
5384
5733
  description: mcpTool.description ?? `${qualifiedName} (MCP tool)`,
5385
5734
  usageHint: `Tool provided by MCP server "${serverName}". ${mcpTool.description ?? ""}`,
@@ -5418,6 +5767,7 @@ function wrapMCPTool(serverName, mcpTool, client, permission = "confirm", observ
5418
5767
  }
5419
5768
  }
5420
5769
  };
5770
+ return sandboxGate(tool);
5421
5771
  }
5422
5772
  function describeUrlOutcomes(serverName, toolName, outcomes) {
5423
5773
  const lines = outcomes.map(({ elicitation, action }) => {
@@ -5482,36 +5832,48 @@ function applySlotTools(ctx, slot, tools, client) {
5482
5832
  const allowed = slot.cfg.allowedTools;
5483
5833
  const filtered = tools.filter((t) => !allowed || allowed.includes(t.name));
5484
5834
  const signature = JSON.stringify(
5485
- filtered.map((t) => [t.name, t.description ?? null, t.inputSchema ?? null])
5835
+ filtered.map((t) => [
5836
+ t.name,
5837
+ t.description ?? null,
5838
+ t.inputSchema ?? null,
5839
+ t.outputSchema ?? null
5840
+ ])
5486
5841
  );
5487
5842
  const lazyWrappersCurrent = slot.lazy && slot.toolSignature === signature && slot.lazyTools.length === filtered.length && (slot.registeredLazy || ctx.lazyMode);
5488
5843
  if (lazyWrappersCurrent) return;
5489
5844
  const clientArg = slot.lazy ? () => ctx.ensureConnected(slot.cfg.name) : expectDefined(client);
5490
5845
  const wrapped = filtered.map(
5491
- (t) => wrapMCPTool(slot.cfg.name, t, clientArg, slot.cfg.permission ?? "confirm", {
5492
- onStart: (caller) => {
5493
- if (caller) slot.activeCallers = [...slot.activeCallers ?? [], caller];
5494
- slot.operations.inFlightCalls++;
5495
- slot.operations.peakInFlightCalls = Math.max(
5496
- slot.operations.peakInFlightCalls,
5497
- slot.operations.inFlightCalls
5498
- );
5499
- ctx.recordOperation(slot, "call", "started", void 0, void 0, false);
5500
- },
5501
- onFinish: ({ durationMs, ok }, caller) => {
5502
- const at = caller ? slot.activeCallers?.lastIndexOf(caller) ?? -1 : -1;
5503
- if (at >= 0) slot.activeCallers?.splice(at, 1);
5504
- slot.operations.inFlightCalls = Math.max(0, slot.operations.inFlightCalls - 1);
5505
- slot.lastUsed = Date.now();
5506
- pushBounded(slot.operations.callSamples, durationMs, MCP_OPERATION_LIMITS.LATENCY_SAMPLES);
5507
- if (ok) {
5508
- ctx.recordSuccess(slot);
5509
- ctx.recordOperation(slot, "call", "ok", void 0, durationMs, false);
5510
- } else {
5511
- ctx.recordFailure(slot, "tool", "tool-call-failed", durationMs);
5846
+ (t) => wrapMCPTool(
5847
+ slot.cfg.name,
5848
+ t,
5849
+ clientArg,
5850
+ slot.cfg.permission ?? "confirm",
5851
+ slot.cfg.sandboxTrust === true,
5852
+ {
5853
+ onStart: (caller) => {
5854
+ if (caller) slot.activeCallers = [...slot.activeCallers ?? [], caller];
5855
+ slot.operations.inFlightCalls++;
5856
+ slot.operations.peakInFlightCalls = Math.max(
5857
+ slot.operations.peakInFlightCalls,
5858
+ slot.operations.inFlightCalls
5859
+ );
5860
+ ctx.recordOperation(slot, "call", "started", void 0, void 0, false);
5861
+ },
5862
+ onFinish: ({ durationMs, ok }, caller) => {
5863
+ const at = caller ? slot.activeCallers?.lastIndexOf(caller) ?? -1 : -1;
5864
+ if (at >= 0) slot.activeCallers?.splice(at, 1);
5865
+ slot.operations.inFlightCalls = Math.max(0, slot.operations.inFlightCalls - 1);
5866
+ slot.lastUsed = Date.now();
5867
+ pushBounded(slot.operations.callSamples, durationMs, MCP_OPERATION_LIMITS.LATENCY_SAMPLES);
5868
+ if (ok) {
5869
+ ctx.recordSuccess(slot);
5870
+ ctx.recordOperation(slot, "call", "ok", void 0, durationMs, false);
5871
+ } else {
5872
+ ctx.recordFailure(slot, "tool", "tool-call-failed", durationMs);
5873
+ }
5512
5874
  }
5513
5875
  }
5514
- })
5876
+ )
5515
5877
  );
5516
5878
  slot.lazyTools = wrapped;
5517
5879
  slot.toolSignature = signature;
@@ -5536,51 +5898,62 @@ function applySlotTools(ctx, slot, tools, client) {
5536
5898
  }
5537
5899
  async function discoverSlotCapabilities(ctx, slot, client) {
5538
5900
  const startedAt = Date.now();
5539
- slot.serverMetadata = client.getServerMetadata();
5540
- const capabilities = slot.serverMetadata?.capabilities;
5901
+ const versions = { ...slot.catalogVersions };
5902
+ const serverMetadata = client.getServerMetadata();
5903
+ const capabilities = serverMetadata?.capabilities;
5904
+ let resources;
5905
+ let resourceTemplates;
5906
+ let prompts;
5541
5907
  if (capabilities?.resources) {
5542
5908
  try {
5543
- slot.resources = await collectCatalogPages(
5909
+ resources = await collectCatalogPages(
5544
5910
  (cursor) => client.listResources(cursor ? { cursor } : {}),
5545
5911
  (page) => page.resources
5546
5912
  );
5547
5913
  } catch (err) {
5548
- slot.resources = void 0;
5914
+ if (!ownsConnectedSlot(ctx, slot, client)) return;
5549
5915
  ctx.recordFailure(slot, "protocol", "resource-discovery-failed");
5550
5916
  ctx.log.warn(`MCP server "${slot.cfg.name}" resource discovery failed`, err);
5551
5917
  }
5918
+ if (!ownsConnectedSlot(ctx, slot, client)) return;
5552
5919
  try {
5553
- slot.resourceTemplates = await collectCatalogPages(
5920
+ resourceTemplates = await collectCatalogPages(
5554
5921
  (cursor) => client.listResourceTemplates(cursor ? { cursor } : {}),
5555
5922
  (page) => page.resourceTemplates
5556
5923
  );
5557
5924
  } catch (err) {
5558
- slot.resourceTemplates = void 0;
5925
+ if (!ownsConnectedSlot(ctx, slot, client)) return;
5559
5926
  ctx.recordFailure(slot, "protocol", "resource-template-discovery-failed");
5560
5927
  ctx.log.warn(`MCP server "${slot.cfg.name}" resource template discovery failed`, err);
5561
5928
  }
5562
- } else {
5563
- slot.resources = void 0;
5564
- slot.resourceTemplates = void 0;
5565
5929
  }
5930
+ if (!ownsConnectedSlot(ctx, slot, client)) return;
5566
5931
  if (capabilities?.prompts) {
5567
5932
  try {
5568
- slot.prompts = await collectCatalogPages(
5933
+ prompts = await collectCatalogPages(
5569
5934
  (cursor) => client.listPrompts(cursor ? { cursor } : {}),
5570
5935
  (page) => page.prompts
5571
5936
  );
5572
5937
  } catch (err) {
5573
- slot.prompts = void 0;
5938
+ if (!ownsConnectedSlot(ctx, slot, client)) return;
5574
5939
  ctx.recordFailure(slot, "protocol", "prompt-discovery-failed");
5575
5940
  ctx.log.warn(`MCP server "${slot.cfg.name}" prompt discovery failed`, err);
5576
5941
  }
5577
- } else {
5578
- slot.prompts = void 0;
5579
5942
  }
5943
+ if (!ownsConnectedSlot(ctx, slot, client)) return;
5944
+ slot.serverMetadata = serverMetadata;
5945
+ if (slot.catalogVersions?.resources === versions.resources) slot.resources = resources;
5946
+ if (slot.catalogVersions?.resourceTemplates === versions.resourceTemplates) {
5947
+ slot.resourceTemplates = resourceTemplates;
5948
+ }
5949
+ if (slot.catalogVersions?.prompts === versions.prompts) slot.prompts = prompts;
5580
5950
  const durationMs = Date.now() - startedAt;
5581
5951
  pushBounded(slot.operations.discoverySamples, durationMs, MCP_OPERATION_LIMITS.LATENCY_SAMPLES);
5582
5952
  ctx.recordOperation(slot, "discover", "complete", void 0, durationMs, false);
5583
5953
  }
5954
+ function ownsConnectedSlot(ctx, slot, client) {
5955
+ return ctx.servers.get(slot.cfg.name) === slot && slot.client === client && slot.state === "connected";
5956
+ }
5584
5957
  async function persistSlotCapabilityManifest(cacheDir, slot) {
5585
5958
  if (!slot.lazy || !cacheDir) return;
5586
5959
  const previous = slot.manifestWrite ?? Promise.resolve();
@@ -5601,9 +5974,11 @@ async function persistSlotCapabilityManifest(cacheDir, slot) {
5601
5974
  }
5602
5975
  async function attemptConnectSlot(ctx, slot) {
5603
5976
  const MAX_ATTEMPTS2 = MCP_CONSTANTS.RECONNECT.MAX_ATTEMPTS;
5977
+ const generation = slot.startupGeneration;
5978
+ const isCurrent = () => ctx.servers.get(slot.cfg.name) === slot && slot.startupGeneration === generation;
5604
5979
  let attempt = 0;
5605
5980
  while (attempt < MAX_ATTEMPTS2) {
5606
- if (ctx.servers.get(slot.cfg.name) !== slot) {
5981
+ if (!isCurrent()) {
5607
5982
  return;
5608
5983
  }
5609
5984
  attempt++;
@@ -5637,7 +6012,7 @@ async function attemptConnectSlot(ctx, slot) {
5637
6012
  client.addToolsChangedListener(ctx.onToolsChanged);
5638
6013
  ctx.addCatalogListeners(client);
5639
6014
  await client.connect();
5640
- if (slot.state === "disconnected" || !ctx.servers.has(slot.cfg.name) || ctx.servers.get(slot.cfg.name) !== slot) {
6015
+ if (slot.state === "disconnected" || !isCurrent()) {
5641
6016
  client.removeExitListener(ctx.onChildExit);
5642
6017
  if (boundDisconnect) client.removeDisconnectListener(boundDisconnect);
5643
6018
  client.removeToolsChangedListener(ctx.onToolsChanged);
@@ -5665,8 +6040,10 @@ async function attemptConnectSlot(ctx, slot) {
5665
6040
  const discovered = mc.listTools();
5666
6041
  slot.discoveredTools = discovered;
5667
6042
  await discoverSlotCapabilities(ctx, slot, mc);
6043
+ if (!ownsConnectedSlot(ctx, slot, mc)) return;
5668
6044
  await persistSlotCapabilityManifest(ctx.cacheDir, slot);
5669
- applySlotTools(ctx, slot, discovered, mc);
6045
+ if (!ownsConnectedSlot(ctx, slot, mc)) return;
6046
+ applySlotTools(ctx, slot, mc.listTools(), mc);
5670
6047
  const durationMs = Date.now() - startedAt;
5671
6048
  pushBounded(
5672
6049
  slot.operations.connectionSamples,
@@ -5689,8 +6066,6 @@ async function attemptConnectSlot(ctx, slot) {
5689
6066
  });
5690
6067
  return;
5691
6068
  } catch (err) {
5692
- ctx.recordFailure(slot, "transport", "connect-attempt-failed", Date.now() - startedAt);
5693
- ctx.log.warn(`MCP server "${slot.cfg.name}" connect attempt ${attempt} failed`, err);
5694
6069
  if (client) {
5695
6070
  client.removeExitListener(ctx.onChildExit);
5696
6071
  if (boundDisconnect) client.removeDisconnectListener(boundDisconnect);
@@ -5699,6 +6074,9 @@ async function attemptConnectSlot(ctx, slot) {
5699
6074
  await client.close().catch(() => {
5700
6075
  });
5701
6076
  }
6077
+ if (!isCurrent() || slot.state === "disconnected") return;
6078
+ ctx.recordFailure(slot, "transport", "connect-attempt-failed", Date.now() - startedAt);
6079
+ ctx.log.warn(`MCP server "${slot.cfg.name}" connect attempt ${attempt} failed`, err);
5702
6080
  if (attempt >= MAX_ATTEMPTS2) {
5703
6081
  ctx.log.error(
5704
6082
  `MCP server "${slot.cfg.name}" connect exhausted after ${MAX_ATTEMPTS2} attempts`,
@@ -5720,7 +6098,7 @@ async function attemptConnectSlot(ctx, slot) {
5720
6098
  }
5721
6099
  const delay = 500 * 2 ** attempt;
5722
6100
  await new Promise((r) => setTimeout(r, delay));
5723
- if (slot.state === "disconnected" || ctx.servers.get(slot.cfg.name) !== slot) {
6101
+ if (slot.state === "disconnected" || !isCurrent()) {
5724
6102
  return;
5725
6103
  }
5726
6104
  }
@@ -5852,7 +6230,7 @@ async function sleepIdleSlot(ctx, slot) {
5852
6230
  ctx.log.warn(`MCP server "${slot.cfg.name}" error during idle sleep close`, err);
5853
6231
  }
5854
6232
  }
5855
- if (slot.client || slot.state !== "dormant") return;
6233
+ if (ctx.servers.get(slot.cfg.name) !== slot || slot.client || slot.state !== "dormant") return;
5856
6234
  slot.onDisconnect = void 0;
5857
6235
  slot.operations.sleepCount++;
5858
6236
  ctx.recordOperation(slot, "sleep", "idle-timeout");
@@ -6084,7 +6462,9 @@ var MCPRegistry = class _MCPRegistry {
6084
6462
  async startLazy(slot) {
6085
6463
  const cacheDir = expectDefined2(this.cacheDir);
6086
6464
  const hash = manifestConfigHash(slot.cfg);
6465
+ const generation = slot.startupGeneration;
6087
6466
  const cached = await readCapabilityManifest(cacheDir, slot.cfg.name, hash);
6467
+ if (this.servers.get(slot.cfg.name) !== slot || slot.startupGeneration !== generation) return;
6088
6468
  if (cached) {
6089
6469
  slot.serverMetadata = cached.serverMetadata;
6090
6470
  slot.resources = cached.resources;
@@ -6103,15 +6483,15 @@ var MCPRegistry = class _MCPRegistry {
6103
6483
  singleFlightConnect(slot) {
6104
6484
  if (slot.client && slot.state === "connected") return Promise.resolve(slot.client);
6105
6485
  if (slot.connecting) return slot.connecting;
6106
- const promise = (async () => {
6107
- try {
6108
- await this.attemptConnect(slot);
6109
- return slot.client;
6110
- } finally {
6111
- slot.connecting = void 0;
6112
- }
6113
- })();
6486
+ const generation = slot.startupGeneration;
6487
+ const promise = this.attemptConnect(slot).then(
6488
+ () => slot.startupGeneration === generation ? slot.client : void 0
6489
+ );
6114
6490
  slot.connecting = promise;
6491
+ const clearFlight = () => {
6492
+ if (slot.connecting === promise) slot.connecting = void 0;
6493
+ };
6494
+ void promise.then(clearFlight, clearFlight);
6115
6495
  return promise;
6116
6496
  }
6117
6497
  /**
@@ -6209,6 +6589,7 @@ var MCPRegistry = class _MCPRegistry {
6209
6589
  async stop(name) {
6210
6590
  const slot = this.servers.get(name);
6211
6591
  if (!slot) return;
6592
+ slot.startupGeneration = (slot.startupGeneration ?? 0) + 1;
6212
6593
  slot.reconnectPending = false;
6213
6594
  if (slot.reconnectTimer) {
6214
6595
  clearTimeout(slot.reconnectTimer);
@@ -6260,7 +6641,7 @@ var MCPRegistry = class _MCPRegistry {
6260
6641
  }
6261
6642
  if (slot.state === "dormant") return;
6262
6643
  if (slot.operations.inFlightCalls > 0) {
6263
- throw new Error(`MCP server "${name}" has tool calls in flight \u2014 try again when they finish`);
6644
+ throw new Error(`MCP server "${name}" has requests in flight \u2014 try again when they finish`);
6264
6645
  }
6265
6646
  await sleepIdleSlot(this.idleContext(), slot);
6266
6647
  }
@@ -6315,76 +6696,73 @@ var MCPRegistry = class _MCPRegistry {
6315
6696
  return buildRegistryOperationalHealth(this.servers.values(), this.disabledServers.values());
6316
6697
  }
6317
6698
  getCatalog(name) {
6318
- const slot = this.servers.get(name);
6319
- if (slot) {
6320
- return registryCatalogSnapshot(slot);
6321
- }
6322
- const disabled = this.disabledServers.get(name);
6323
- if (disabled) {
6324
- return {
6325
- name: disabled.name,
6326
- state: "idle"
6327
- };
6328
- }
6329
- return void 0;
6699
+ return getCatalog.call(this.registryCatalogOperationsHost(), name);
6330
6700
  }
6331
6701
  async listResources(name, opts = {}) {
6332
- const slot = this.requireSlot(name);
6333
- if (!opts.refresh && slot.resources) return cloneCatalogRecords(slot.resources);
6334
- const client = await this.ensureConnected(name);
6335
- if (!client.getServerMetadata()?.capabilities.resources) return [];
6336
- slot.resources = await collectCatalogPages(
6337
- (cursor) => client.listResources(cursor ? { cursor } : {}),
6338
- (page) => page.resources
6339
- );
6340
- await this.persistCapabilityManifest(slot);
6341
- return cloneCatalogRecords(slot.resources);
6702
+ return listResources.call(this.registryCatalogOperationsHost(), name, opts);
6342
6703
  }
6343
6704
  async listResourceTemplates(name, opts = {}) {
6344
- const slot = this.requireSlot(name);
6345
- if (!opts.refresh && slot.resourceTemplates) return cloneCatalogRecords(slot.resourceTemplates);
6346
- const client = await this.ensureConnected(name);
6347
- if (!client.getServerMetadata()?.capabilities.resources) return [];
6348
- slot.resourceTemplates = await collectCatalogPages(
6349
- (cursor) => client.listResourceTemplates(cursor ? { cursor } : {}),
6350
- (page) => page.resourceTemplates
6351
- );
6352
- await this.persistCapabilityManifest(slot);
6353
- return cloneCatalogRecords(slot.resourceTemplates);
6705
+ return listResourceTemplates.call(this.registryCatalogOperationsHost(), name, opts);
6354
6706
  }
6355
6707
  async readResource(name, uri) {
6356
- return (await this.ensureConnected(name)).readResource(uri);
6708
+ return readResource.call(this.registryCatalogOperationsHost(), name, uri);
6357
6709
  }
6358
6710
  async selectResourceForInsertion(name, uri, policy) {
6359
- return prepareResourceInsertion(name, uri, await this.readResource(name, uri), policy);
6711
+ return selectResourceForInsertion.call(
6712
+ this.registryCatalogOperationsHost(),
6713
+ name,
6714
+ uri,
6715
+ policy
6716
+ );
6360
6717
  }
6361
6718
  async subscribeResource(name, uri) {
6362
- await (await this.ensureConnected(name)).subscribeResource(uri);
6719
+ return subscribeResource.call(this.registryCatalogOperationsHost(), name, uri);
6363
6720
  }
6364
6721
  async unsubscribeResource(name, uri) {
6365
- await (await this.ensureConnected(name)).unsubscribeResource(uri);
6722
+ return unsubscribeResource.call(this.registryCatalogOperationsHost(), name, uri);
6366
6723
  }
6367
6724
  async listPrompts(name, opts = {}) {
6368
- const slot = this.requireSlot(name);
6369
- if (!opts.refresh && slot.prompts) return cloneCatalogRecords(slot.prompts);
6370
- const client = await this.ensureConnected(name);
6371
- if (!client.getServerMetadata()?.capabilities.prompts) return [];
6372
- slot.prompts = await collectCatalogPages(
6373
- (cursor) => client.listPrompts(cursor ? { cursor } : {}),
6374
- (page) => page.prompts
6375
- );
6376
- await this.persistCapabilityManifest(slot);
6377
- return cloneCatalogRecords(slot.prompts);
6725
+ return listPrompts.call(this.registryCatalogOperationsHost(), name, opts);
6378
6726
  }
6379
6727
  async getPrompt(serverName, promptName, args) {
6380
- return (await this.ensureConnected(serverName)).getPrompt(promptName, args);
6728
+ return getPrompt.call(
6729
+ this.registryCatalogOperationsHost(),
6730
+ serverName,
6731
+ promptName,
6732
+ args
6733
+ );
6734
+ }
6735
+ /** Keep all remote requests awake and reject results from superseded clients. */
6736
+ async withConnectedClient(name, run) {
6737
+ const slot = this.requireSlot(name);
6738
+ const generation = slot.startupGeneration;
6739
+ slot.operations.inFlightCalls++;
6740
+ slot.operations.peakInFlightCalls = Math.max(
6741
+ slot.operations.peakInFlightCalls,
6742
+ slot.operations.inFlightCalls
6743
+ );
6744
+ try {
6745
+ const client = await this.ensureConnected(name);
6746
+ const assertCurrent = () => {
6747
+ if (this.servers.get(name) !== slot || slot.startupGeneration !== generation || slot.client !== client || slot.state !== "connected") {
6748
+ throw new Error(`MCP server "${name}" connection changed during request`);
6749
+ }
6750
+ };
6751
+ assertCurrent();
6752
+ const result = await run(client, assertCurrent);
6753
+ assertCurrent();
6754
+ return result;
6755
+ } finally {
6756
+ slot.operations.inFlightCalls = Math.max(0, slot.operations.inFlightCalls - 1);
6757
+ if (slot.startupGeneration === generation) slot.lastUsed = Date.now();
6758
+ }
6381
6759
  }
6382
6760
  async selectPromptForInsertion(serverName, promptName, args, policy) {
6383
- return preparePromptInsertion(
6761
+ return selectPromptForInsertion.call(
6762
+ this.registryCatalogOperationsHost(),
6384
6763
  serverName,
6385
6764
  promptName,
6386
6765
  args,
6387
- await this.getPrompt(serverName, promptName, args),
6388
6766
  policy
6389
6767
  );
6390
6768
  }
@@ -6527,6 +6905,8 @@ var MCPRegistry = class _MCPRegistry {
6527
6905
  onResourcesChanged = (name) => {
6528
6906
  const slot = this.servers.get(name);
6529
6907
  if (!slot) return;
6908
+ advanceCatalogVersion(slot, "resources");
6909
+ advanceCatalogVersion(slot, "resourceTemplates");
6530
6910
  slot.resources = void 0;
6531
6911
  slot.resourceTemplates = void 0;
6532
6912
  void this.persistCapabilityManifest(slot);
@@ -6539,6 +6919,7 @@ var MCPRegistry = class _MCPRegistry {
6539
6919
  onPromptsChanged = (name) => {
6540
6920
  const slot = this.servers.get(name);
6541
6921
  if (!slot) return;
6922
+ advanceCatalogVersion(slot, "prompts");
6542
6923
  slot.prompts = void 0;
6543
6924
  void this.persistCapabilityManifest(slot);
6544
6925
  this.log.info(`MCP server "${name}" prompt catalog invalidated`);
@@ -6632,12 +7013,20 @@ var MCPRegistry = class _MCPRegistry {
6632
7013
  async attemptConnect(slot) {
6633
7014
  return attemptConnectSlot(this.connectContext(), slot);
6634
7015
  }
7016
+ registryCatalogOperationsHost() {
7017
+ void this.servers;
7018
+ void this.disabledServers;
7019
+ void this.requireSlot;
7020
+ void this.withConnectedClient;
7021
+ void this.persistCapabilityManifest;
7022
+ void this.readResource;
7023
+ void this.getPrompt;
7024
+ return this;
7025
+ }
6635
7026
  };
6636
7027
 
6637
- // src/server.ts
6638
- import { timingSafeEqual as timingSafeEqual2 } from "node:crypto";
6639
- import { createServer as createServer2 } from "node:http";
6640
- import { expectDefined as expectDefined3, toErrorMessage as toErrorMessage2, validateAgainstSchema as validateAgainstSchema2 } from "@wrongstack/core/utils";
7028
+ // src/server-dispatch.ts
7029
+ import { expectDefined as expectDefined3, toErrorMessage as toErrorMessage3, validateAgainstSchema as validateAgainstSchema2 } from "@wrongstack/core/utils";
6641
7030
  var PARSE_ERROR = -32700;
6642
7031
  var INVALID_REQUEST = -32600;
6643
7032
  var METHOD_NOT_FOUND = -32601;
@@ -6649,6 +7038,7 @@ var MCPServer = class {
6649
7038
  logger;
6650
7039
  resources;
6651
7040
  prompts;
7041
+ instructions;
6652
7042
  inFlightRequests = /* @__PURE__ */ new Map();
6653
7043
  constructor(opts) {
6654
7044
  this.host = opts.host;
@@ -6657,6 +7047,7 @@ var MCPServer = class {
6657
7047
  version: MCP_CONSTANTS.CLIENT_INFO.version
6658
7048
  };
6659
7049
  this.logger = opts.logger;
7050
+ this.instructions = opts.instructions?.trim() || void 0;
6660
7051
  this.resources = structuredClone(opts.resources ?? []);
6661
7052
  this.prompts = structuredClone(opts.prompts ?? []);
6662
7053
  }
@@ -6701,7 +7092,7 @@ var MCPServer = class {
6701
7092
  }
6702
7093
  return JSON.stringify({ jsonrpc: "2.0", id: msg.id, result });
6703
7094
  } catch (err) {
6704
- const message = toErrorMessage2(err);
7095
+ const message = toErrorMessage3(err);
6705
7096
  this.logger?.warn?.(`MCP server: method "${msg.method}" threw: ${message}`);
6706
7097
  const code = err instanceof InvalidToolArgumentsError ? INVALID_PARAMS : INTERNAL_ERROR;
6707
7098
  return this.encodeError(requestId, code, message);
@@ -6723,7 +7114,8 @@ var MCPServer = class {
6723
7114
  ...this.resources.length > 0 ? { resources: { subscribe: false, listChanged: false } } : {},
6724
7115
  ...this.prompts.length > 0 ? { prompts: { listChanged: false } } : {}
6725
7116
  },
6726
- serverInfo: this.serverInfo
7117
+ serverInfo: this.serverInfo,
7118
+ ...this.instructions ? { instructions: this.instructions } : {}
6727
7119
  };
6728
7120
  case "ping":
6729
7121
  return {};
@@ -6777,7 +7169,7 @@ var MCPServer = class {
6777
7169
  const input = paramsRecord(params);
6778
7170
  const args = stringRecord(input["arguments"], "prompts/get arguments");
6779
7171
  for (const argument of prompt.arguments ?? []) {
6780
- if (argument.required && args[argument.name] === void 0) {
7172
+ if (argument.required && !Object.hasOwn(args, argument.name)) {
6781
7173
  throw new Error(`Prompt "${name}" requires argument "${argument.name}"`);
6782
7174
  }
6783
7175
  }
@@ -6876,11 +7268,11 @@ function requiredParamString(params, field, method) {
6876
7268
  return value;
6877
7269
  }
6878
7270
  function stringRecord(value, label2) {
6879
- if (value === void 0) return {};
7271
+ if (value === void 0) return /* @__PURE__ */ Object.create(null);
6880
7272
  if (!value || typeof value !== "object" || Array.isArray(value)) {
6881
7273
  throw new Error(`${label2} must be an object`);
6882
7274
  }
6883
- const result = {};
7275
+ const result = /* @__PURE__ */ Object.create(null);
6884
7276
  for (const [key, item] of Object.entries(value)) {
6885
7277
  if (typeof item !== "string") throw new Error(`${label2}.${key} must be a string`);
6886
7278
  result[key] = item;
@@ -6889,7 +7281,7 @@ function stringRecord(value, label2) {
6889
7281
  }
6890
7282
  function renderPromptTemplate(template, args) {
6891
7283
  return template.replace(/\{\{([A-Za-z_][A-Za-z0-9_.-]*)\}\}/g, (_match, name) => {
6892
- const value = args[name];
7284
+ const value = Object.hasOwn(args, name) ? args[name] : void 0;
6893
7285
  if (value === void 0) throw new Error(`Missing prompt template argument "${name}"`);
6894
7286
  return value;
6895
7287
  });
@@ -6921,6 +7313,14 @@ function stringifyItem(c) {
6921
7313
  return String(c);
6922
7314
  }
6923
7315
  }
7316
+
7317
+ // src/server-http.ts
7318
+ import { timingSafeEqual as timingSafeEqual2 } from "node:crypto";
7319
+ import { createServer as createServer2 } from "node:http";
7320
+ import { toErrorMessage as toErrorMessage5 } from "@wrongstack/core/utils";
7321
+
7322
+ // src/server-stdio.ts
7323
+ import { toErrorMessage as toErrorMessage4 } from "@wrongstack/core/utils";
6924
7324
  function serveStdio(server, opts = {}) {
6925
7325
  const stdin = opts.stdin ?? process.stdin;
6926
7326
  const stdout = opts.stdout ?? process.stdout;
@@ -6938,7 +7338,7 @@ function serveStdio(server, opts = {}) {
6938
7338
  `, () => resolve());
6939
7339
  })
6940
7340
  ).catch((err) => {
6941
- const msg = toErrorMessage2(err);
7341
+ const msg = toErrorMessage4(err);
6942
7342
  console.error(
6943
7343
  JSON.stringify({
6944
7344
  level: "error",
@@ -6990,7 +7390,7 @@ function serveStdio(server, opts = {}) {
6990
7390
  JSON.stringify({
6991
7391
  level: "error",
6992
7392
  event: "mcp_server.handle_message_failed",
6993
- message: toErrorMessage2(err),
7393
+ message: toErrorMessage4(err),
6994
7394
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
6995
7395
  })
6996
7396
  );
@@ -7025,7 +7425,7 @@ function serveStdio(server, opts = {}) {
7025
7425
  JSON.stringify({
7026
7426
  level: "error",
7027
7427
  event: "mcp_server.handle_message_failed",
7028
- message: toErrorMessage2(err),
7428
+ message: toErrorMessage4(err),
7029
7429
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
7030
7430
  })
7031
7431
  );
@@ -7050,6 +7450,8 @@ function serveStdio(server, opts = {}) {
7050
7450
  };
7051
7451
  }
7052
7452
  var HTTP_BODY_CAP = 4 * 1024 * 1024;
7453
+
7454
+ // src/server-http.ts
7053
7455
  function isLoopbackHost(host) {
7054
7456
  return host === "127.0.0.1" || host === "::1" || host === "localhost";
7055
7457
  }
@@ -7086,7 +7488,7 @@ function serveHttp(server, opts = {}) {
7086
7488
  });
7087
7489
  }
7088
7490
  async function handleHttpRequest(server, req, res, token, log, boundHost) {
7089
- const send = (status, body2, type = "application/json") => {
7491
+ const send = (status, body, type = "application/json") => {
7090
7492
  res.writeHead(status, {
7091
7493
  "content-type": type,
7092
7494
  // This endpoint executes tools. Nothing about it should be embedded,
@@ -7097,7 +7499,7 @@ async function handleHttpRequest(server, req, res, token, log, boundHost) {
7097
7499
  // No CORS headers are ever emitted: a cross-origin reader must not be
7098
7500
  // able to see a response even if it manages to send the request.
7099
7501
  });
7100
- res.end(body2);
7502
+ res.end(body);
7101
7503
  };
7102
7504
  if (!originIsAcceptable(req, boundHost)) {
7103
7505
  return send(403, JSON.stringify({ error: "cross-origin forbidden" }));
@@ -7117,13 +7519,13 @@ async function handleHttpRequest(server, req, res, token, log, boundHost) {
7117
7519
  if (!isJsonContentType(req.headers["content-type"])) {
7118
7520
  return send(415, JSON.stringify({ error: "content-type must be application/json" }));
7119
7521
  }
7120
- let body = "";
7522
+ const chunks = [];
7121
7523
  let bodyBytes = 0;
7122
7524
  let aborted = false;
7123
7525
  req.on("data", (chunk) => {
7124
7526
  if (aborted) return;
7125
7527
  bodyBytes += chunk.byteLength;
7126
- body += chunk.toString("utf8");
7528
+ chunks.push(chunk);
7127
7529
  if (bodyBytes > HTTP_BODY_CAP) {
7128
7530
  aborted = true;
7129
7531
  send(413, JSON.stringify({ error: "payload too large" }));
@@ -7132,11 +7534,12 @@ async function handleHttpRequest(server, req, res, token, log, boundHost) {
7132
7534
  });
7133
7535
  req.on("end", () => {
7134
7536
  if (aborted) return;
7537
+ const body = Buffer.concat(chunks, bodyBytes).toString("utf8");
7135
7538
  void server.handleMessage(body).then((out) => {
7136
7539
  if (out === null) return send(202, "");
7137
7540
  return send(200, out);
7138
7541
  }).catch((err) => {
7139
- log?.warn?.(`MCP http handler error: ${toErrorMessage2(err)}`);
7542
+ log?.warn?.(`MCP http handler error: ${toErrorMessage5(err)}`);
7140
7543
  send(500, JSON.stringify({ error: "internal error" }));
7141
7544
  });
7142
7545
  });
@@ -7440,7 +7843,7 @@ function emptyFile() {
7440
7843
  return { version: TOKEN_STORE_VERSION, updatedAt: (/* @__PURE__ */ new Date(0)).toISOString(), entries: [] };
7441
7844
  }
7442
7845
  function validateStoreFile(value) {
7443
- if (!isRecord2(value) || value["version"] !== TOKEN_STORE_VERSION || !Array.isArray(value["entries"])) {
7846
+ if (!isRecord3(value) || value["version"] !== TOKEN_STORE_VERSION || !Array.isArray(value["entries"])) {
7444
7847
  throw new Error("MCP token store has an unsupported or malformed structure");
7445
7848
  }
7446
7849
  if (value["entries"].length > MAX_ENTRIES)
@@ -7452,7 +7855,7 @@ function validateStoreFile(value) {
7452
7855
  };
7453
7856
  }
7454
7857
  function validateEncryptedEntry(value) {
7455
- if (!isRecord2(value)) throw new Error("MCP token store entry must be an object");
7858
+ if (!isRecord3(value)) throw new Error("MCP token store entry must be an object");
7456
7859
  const resource = canonicalMcpResource(boundedString(value["resource"], "resource", 4096));
7457
7860
  const authorizationServer = validateMcpAuthorizationServerMetadata(value["authorizationServer"]);
7458
7861
  const scopes = stringArray(value["scopes"], "scopes", 128);
@@ -7497,7 +7900,7 @@ function normalizeStoredAuthorization(value) {
7497
7900
  updatedAt: boundedString(value.updatedAt, "updatedAt", 128)
7498
7901
  };
7499
7902
  }
7500
- function isRecord2(value) {
7903
+ function isRecord3(value) {
7501
7904
  return typeof value === "object" && value !== null && !Array.isArray(value);
7502
7905
  }
7503
7906
  function boundedString(value, field, maxLength) {