@wrongstack/mcp 1.0.26 → 1.0.27

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
@@ -26,7 +26,15 @@ export declare class MCPClient {
26
26
  * (stdio child exit, `close()`) so callers don't hang forever.
27
27
  */
28
28
  private readonly pending;
29
- private rxBuffer;
29
+ /**
30
+ * The unterminated tail of stdout, as the chunks it arrived in; joined only
31
+ * when its newline arrives. Kept as one string (`+=` then `indexOf`), every
32
+ * chunk flattened and re-scanned everything buffered so far — O(n²) for a
33
+ * multi-MiB tools/call result.
34
+ */
35
+ private rxParts;
36
+ private get rxBuffer();
37
+ private set rxBuffer(value);
30
38
  private rxBufferBytes;
31
39
  /**
32
40
  * Incremental UTF-8 decoder for the stdio rx path. A pipe read boundary can
package/dist/index.js CHANGED
@@ -1543,27 +1543,8 @@ var SSEReader = class {
1543
1543
  };
1544
1544
  }
1545
1545
  feed(chunk) {
1546
- if (chunk.length > SSE_READER_MAX_BUFFER) {
1547
- throw new ToolError({
1548
- message: `SSE: chunk size ${chunk.length} exceeds max buffer ${SSE_READER_MAX_BUFFER} \u2014 refusing to accumulate`,
1549
- code: "TOOL_EXECUTION_FAILED",
1550
- toolName: "mcp_transport_sse_reader",
1551
- context: { phase: "feed", chunkLength: chunk.length, maxBuffer: SSE_READER_MAX_BUFFER }
1552
- });
1553
- }
1546
+ const carried = this.buffer.length;
1554
1547
  this.buffer += chunk;
1555
- if (this.buffer.length > SSE_READER_MAX_BUFFER) {
1556
- throw new ToolError({
1557
- message: `SSE: pending line exceeds ${SSE_READER_MAX_BUFFER} bytes \u2014 upstream is not framing events`,
1558
- code: "TOOL_EXECUTION_FAILED",
1559
- toolName: "mcp_transport_sse_reader",
1560
- context: {
1561
- phase: "feed",
1562
- bufferLength: this.buffer.length,
1563
- maxBuffer: SSE_READER_MAX_BUFFER
1564
- }
1565
- });
1566
- }
1567
1548
  let start = 0;
1568
1549
  let idx = this.buffer.indexOf("\n", start);
1569
1550
  while (idx !== -1) {
@@ -1574,6 +1555,25 @@ var SSEReader = class {
1574
1555
  idx = this.buffer.indexOf("\n", start);
1575
1556
  }
1576
1557
  if (start > 0) this.buffer = this.buffer.slice(start);
1558
+ if (this.buffer.length <= SSE_READER_MAX_BUFFER) return;
1559
+ if (start >= carried) {
1560
+ throw new ToolError({
1561
+ message: `SSE: chunk size ${chunk.length} exceeds max buffer ${SSE_READER_MAX_BUFFER} \u2014 refusing to accumulate`,
1562
+ code: "TOOL_EXECUTION_FAILED",
1563
+ toolName: "mcp_transport_sse_reader",
1564
+ context: { phase: "feed", chunkLength: chunk.length, maxBuffer: SSE_READER_MAX_BUFFER }
1565
+ });
1566
+ }
1567
+ throw new ToolError({
1568
+ message: `SSE: pending line exceeds ${SSE_READER_MAX_BUFFER} bytes \u2014 upstream is not framing events`,
1569
+ code: "TOOL_EXECUTION_FAILED",
1570
+ toolName: "mcp_transport_sse_reader",
1571
+ context: {
1572
+ phase: "feed",
1573
+ bufferLength: this.buffer.length,
1574
+ maxBuffer: SSE_READER_MAX_BUFFER
1575
+ }
1576
+ });
1577
1577
  }
1578
1578
  processLine(line) {
1579
1579
  if (line === "") {
@@ -1649,6 +1649,7 @@ import * as https2 from "node:https";
1649
1649
  import { ConfigError as ConfigError2 } from "@wrongstack/core/types";
1650
1650
 
1651
1651
  // src/elicitation.ts
1652
+ import { sanitizeTerminalText } from "@wrongstack/core/utils";
1652
1653
  var ELICITATION_MAX_WAIT_MS = 10 * 6e4;
1653
1654
  var MAX_FIELDS = 32;
1654
1655
  var MAX_MESSAGE_CHARS = 4e3;
@@ -1659,6 +1660,13 @@ function isRecord(v) {
1659
1660
  function optionalString3(v) {
1660
1661
  return typeof v === "string" && v.trim() ? v : void 0;
1661
1662
  }
1663
+ function displayText(v) {
1664
+ return sanitizeTerminalText(v);
1665
+ }
1666
+ function optionalDisplayString(v) {
1667
+ const text = optionalString3(v);
1668
+ return text === void 0 ? void 0 : optionalString3(displayText(text));
1669
+ }
1662
1670
  function optionalNumber(v) {
1663
1671
  return typeof v === "number" && Number.isFinite(v) ? v : void 0;
1664
1672
  }
@@ -1672,7 +1680,7 @@ function parseOptions(schema) {
1672
1680
  const names = Array.isArray(schema["enumNames"]) ? schema["enumNames"] : [];
1673
1681
  return values.map((value, i) => ({
1674
1682
  value,
1675
- label: typeof names[i] === "string" && names[i] ? names[i] : value
1683
+ label: displayText(typeof names[i] === "string" && names[i] ? names[i] : value)
1676
1684
  }));
1677
1685
  }
1678
1686
  const titled = Array.isArray(schema["oneOf"]) ? schema["oneOf"] : Array.isArray(schema["anyOf"]) ? schema["anyOf"] : void 0;
@@ -1682,7 +1690,7 @@ function parseOptions(schema) {
1682
1690
  if (!isRecord(entry) || typeof entry["const"] !== "string") return void 0;
1683
1691
  options.push({
1684
1692
  value: entry["const"],
1685
- label: optionalString3(entry["title"]) ?? entry["const"]
1693
+ label: optionalDisplayString(entry["title"]) ?? displayText(entry["const"])
1686
1694
  });
1687
1695
  }
1688
1696
  return options;
@@ -1691,8 +1699,8 @@ function parseField(name, schema, required) {
1691
1699
  if (!isRecord(schema)) return { ok: false, error: `property "${name}" is not a schema object` };
1692
1700
  const base = {
1693
1701
  name,
1694
- title: optionalString3(schema["title"]),
1695
- description: optionalString3(schema["description"]),
1702
+ title: optionalDisplayString(schema["title"]),
1703
+ description: optionalDisplayString(schema["description"]),
1696
1704
  required
1697
1705
  };
1698
1706
  const type = schema["type"];
@@ -1789,7 +1797,7 @@ function parseUrlElicitation(params) {
1789
1797
  ok: true,
1790
1798
  value: {
1791
1799
  mode: "url",
1792
- message: message.slice(0, MAX_MESSAGE_CHARS),
1800
+ message: displayText(message).slice(0, MAX_MESSAGE_CHARS),
1793
1801
  url: url.href,
1794
1802
  elicitationId
1795
1803
  }
@@ -1822,10 +1830,10 @@ function parseElicitationParams(params) {
1822
1830
  if (!parsed.ok) return parsed;
1823
1831
  fields.push(parsed.value);
1824
1832
  }
1825
- return { ok: true, value: { message: message.slice(0, MAX_MESSAGE_CHARS), fields } };
1833
+ return { ok: true, value: { message: displayText(message).slice(0, MAX_MESSAGE_CHARS), fields } };
1826
1834
  }
1827
1835
  function label(field) {
1828
- return field.title ?? field.name;
1836
+ return field.title ?? displayText(field.name);
1829
1837
  }
1830
1838
  function checkValue(field, raw) {
1831
1839
  const name = label(field);
@@ -2191,6 +2199,14 @@ if (!pinnedAgentsCleanupRegistered) {
2191
2199
  pinnedAgents.clear();
2192
2200
  });
2193
2201
  }
2202
+ function urlOriginForLog(raw) {
2203
+ try {
2204
+ const url = new URL(raw);
2205
+ return `${url.protocol}//${url.host}`;
2206
+ } catch {
2207
+ return "<invalid url>";
2208
+ }
2209
+ }
2194
2210
  function makeAbortError(method) {
2195
2211
  const err = new Error(`MCP request "${method}" aborted by client`);
2196
2212
  err.name = "AbortError";
@@ -2262,15 +2278,16 @@ var BaseHTTPTransport = class {
2262
2278
  this.requestTimeout = opts.requestTimeoutMs ?? 6e4;
2263
2279
  if (opts.tls) {
2264
2280
  if (opts.tls.rejectUnauthorized === false) {
2281
+ const where = urlOriginForLog(this.url);
2265
2282
  if (!isTlsUnsafeAllowed()) {
2266
2283
  throw new ConfigError2({
2267
- message: `[mcp:${transportName}] TLS verification disabled \u2014 set WRONGSTACK_UNSAFE_MCP_TLS=1 to allow. Rejecting insecure configuration for ${this.url}.`,
2284
+ message: `[mcp:${transportName}] TLS verification disabled \u2014 set WRONGSTACK_UNSAFE_MCP_TLS=1 to allow. Rejecting insecure configuration for ${where}.`,
2268
2285
  code: "CONFIG_INVALID",
2269
- context: { field: "tls.rejectUnauthorized", transportName, url: this.url }
2286
+ context: { field: "tls.rejectUnauthorized", transportName, url: where }
2270
2287
  });
2271
2288
  }
2272
2289
  console.error(
2273
- `[mcp:${transportName}] \u26A0\uFE0F TLS verification DISABLED for ${this.url}. Network attacks are possible \u2014 only use on localhost.`
2290
+ `[mcp:${transportName}] \u26A0\uFE0F TLS verification DISABLED for ${where}. Network attacks are possible \u2014 only use on localhost.`
2274
2291
  );
2275
2292
  }
2276
2293
  this.tlsAgent = new https2.Agent({
@@ -3581,7 +3598,19 @@ var MCPClient = class _MCPClient {
3581
3598
  * (stdio child exit, `close()`) so callers don't hang forever.
3582
3599
  */
3583
3600
  pending = /* @__PURE__ */ new Map();
3584
- rxBuffer = "";
3601
+ /**
3602
+ * The unterminated tail of stdout, as the chunks it arrived in; joined only
3603
+ * when its newline arrives. Kept as one string (`+=` then `indexOf`), every
3604
+ * chunk flattened and re-scanned everything buffered so far — O(n²) for a
3605
+ * multi-MiB tools/call result.
3606
+ */
3607
+ rxParts = [];
3608
+ get rxBuffer() {
3609
+ return this.rxParts.join("");
3610
+ }
3611
+ set rxBuffer(value) {
3612
+ this.rxParts = value ? [value] : [];
3613
+ }
3585
3614
  rxBufferBytes = 0;
3586
3615
  /**
3587
3616
  * Incremental UTF-8 decoder for the stdio rx path. A pipe read boundary can
@@ -4063,11 +4092,10 @@ var MCPClient = class _MCPClient {
4063
4092
  }
4064
4093
  }
4065
4094
  onData(s) {
4066
- this.rxBuffer += s;
4067
4095
  this.rxBufferBytes += Buffer.byteLength(s, "utf8");
4068
4096
  if (this.rxBufferBytes > _MCPClient.MAX_RX_BUFFER_BYTES) {
4069
4097
  const truncated = this.rxBufferBytes;
4070
- this.rxBuffer = "";
4098
+ this.rxParts = [];
4071
4099
  this.rxBufferBytes = 0;
4072
4100
  this.failPending(
4073
4101
  `MCP "${this.opts.name}" rx buffer overflow (${truncated} bytes without a newline) \u2014 closing connection`
@@ -4076,17 +4104,22 @@ var MCPClient = class _MCPClient {
4076
4104
  return;
4077
4105
  }
4078
4106
  let start = 0;
4079
- let idx = this.rxBuffer.indexOf("\n");
4107
+ let idx = s.indexOf("\n");
4108
+ if (idx === -1) {
4109
+ this.rxParts.push(s);
4110
+ return;
4111
+ }
4080
4112
  while (idx !== -1) {
4081
- const line = this.rxBuffer.slice(start, idx).trim();
4113
+ const head = this.rxParts.length > 0 ? this.rxParts.join("") : "";
4114
+ this.rxParts = [];
4115
+ const line = (head + s.slice(start, idx)).trim();
4082
4116
  start = idx + 1;
4083
4117
  if (line) this.onLine(line);
4084
- idx = this.rxBuffer.indexOf("\n", start);
4085
- }
4086
- if (start > 0) {
4087
- this.rxBufferBytes -= Buffer.byteLength(this.rxBuffer.slice(0, start), "utf8");
4088
- this.rxBuffer = this.rxBuffer.slice(start);
4118
+ idx = s.indexOf("\n", start);
4089
4119
  }
4120
+ const tail = s.slice(start);
4121
+ this.rxBufferBytes = Buffer.byteLength(tail, "utf8");
4122
+ if (tail) this.rxParts.push(tail);
4090
4123
  }
4091
4124
  onLine(line) {
4092
4125
  let msg;
@@ -4424,6 +4457,7 @@ function requireIdentity(value, label2) {
4424
4457
  // src/elicitation-form.ts
4425
4458
  import { randomUUID } from "node:crypto";
4426
4459
  import { hostname } from "node:os";
4460
+ import { sanitizeTerminalText as sanitizeTerminalText2 } from "@wrongstack/core/utils";
4427
4461
  var MAX_ATTEMPTS = 3;
4428
4462
  var CONFIRM_QUESTION = "confirm";
4429
4463
  var URL_QUESTION = "url";
@@ -4456,7 +4490,8 @@ function describe(field) {
4456
4490
  function toQuestion(field, index) {
4457
4491
  const base = {
4458
4492
  id: questionId(index),
4459
- prompt: field.title ?? field.name,
4493
+ // Titles arrive sanitized from the parser; a bare field name does not.
4494
+ prompt: field.title ?? sanitizeTerminalText2(field.name),
4460
4495
  description: describe(field),
4461
4496
  required: field.required
4462
4497
  };
@@ -4733,6 +4768,10 @@ function buildConfig(input, base) {
4733
4768
  if (allowedTools !== void 0) cfg.allowedTools = allowedTools;
4734
4769
  const permission = input.permission ?? base?.permission;
4735
4770
  if (permission !== void 0) cfg.permission = permission;
4771
+ const startupTimeoutMs = input.startupTimeoutMs ?? base?.startupTimeoutMs;
4772
+ if (startupTimeoutMs !== void 0) cfg.startupTimeoutMs = startupTimeoutMs;
4773
+ const requestTimeoutMs = input.requestTimeoutMs ?? base?.requestTimeoutMs;
4774
+ if (requestTimeoutMs !== void 0) cfg.requestTimeoutMs = requestTimeoutMs;
4736
4775
  const allowPrivateNetworks = input.allowPrivateNetworks ?? base?.allowPrivateNetworks;
4737
4776
  if (allowPrivateNetworks !== void 0) cfg.allowPrivateNetworks = allowPrivateNetworks;
4738
4777
  const enabled = input.enabled ?? base?.enabled;
@@ -5648,7 +5687,8 @@ async function attemptConnectSlot(ctx, slot) {
5648
5687
  slot.reconnectPending = false;
5649
5688
  ctx.events.emit("mcp.server.disconnected", {
5650
5689
  name: slot.cfg.name,
5651
- reason: err instanceof Error ? err.message : "unknown"
5690
+ reason: err instanceof Error ? err.message : "unknown",
5691
+ terminal: true
5652
5692
  });
5653
5693
  return;
5654
5694
  }
@@ -5869,7 +5909,8 @@ function scheduleRegistryReconnect({
5869
5909
  );
5870
5910
  events.emit("mcp.server.disconnected", {
5871
5911
  name: slot.cfg.name,
5872
- reason: `reconnect-exhausted:${slot.reconnectCycles}`
5912
+ reason: `reconnect-exhausted:${slot.reconnectCycles}`,
5913
+ terminal: true
5873
5914
  });
5874
5915
  return;
5875
5916
  }
package/dist/manage.d.ts CHANGED
@@ -17,6 +17,10 @@ export interface McpServerInput {
17
17
  bearerTokenEnv?: string | undefined;
18
18
  allowedTools?: string[] | undefined;
19
19
  permission?: Permission | undefined;
20
+ /** Connect/startup timeout in ms. Defaults to 10s when omitted. */
21
+ startupTimeoutMs?: number | undefined;
22
+ /** Per-request JSON-RPC timeout in ms. Defaults to 60s when omitted. */
23
+ requestTimeoutMs?: number | undefined;
20
24
  /** Lazy connect — spawn the process only on first tool call (see config). */
21
25
  lazy?: boolean | undefined;
22
26
  /** Env var names to forward from parent process at spawn time. */
@@ -51,6 +51,11 @@ export interface HttpTransportOptions {
51
51
  * Abort error whose `name` is `'AbortError'` so the core executor's
52
52
  * classifyToolError maps it to FATAL / not-retryable (user cancellation).
53
53
  */
54
+ /**
55
+ * `scheme://host:port` of a server URL, for log lines. The path and query can
56
+ * hold an expanded `${SECRET}` (config-env), and userinfo is a credential.
57
+ */
58
+ export declare function urlOriginForLog(raw: string): string;
54
59
  export declare function makeAbortError(method: string): Error;
55
60
  /** Advance a JSON-RPC number id without leaving JavaScript's safe-integer range. */
56
61
  export declare function nextJsonRpcId(id: number): number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wrongstack/mcp",
3
- "version": "1.0.26",
3
+ "version": "1.0.27",
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": "1.0.26",
29
+ "@wrongstack/core": "1.0.27",
30
30
  "undici": "^8.10.2"
31
31
  },
32
32
  "devDependencies": {