@wrongstack/mcp 0.293.0 → 0.295.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -47,7 +47,7 @@ function parseMcpBearerChallenge(header, resource) {
47
47
  if (!header) return challenge;
48
48
  const bearer = /(?:^|,)\s*Bearer(?:\s+|$)/i.exec(header);
49
49
  if (!bearer) return challenge;
50
- const parameters = header.slice((bearer.index ?? 0) + bearer[0].length);
50
+ const parameters = header.slice(bearer.index + bearer[0].length);
51
51
  const resourceMetadata = challengeParameter(parameters, "resource_metadata");
52
52
  if (resourceMetadata) {
53
53
  const metadataUrl = validateMetadataUrl(resourceMetadata);
@@ -395,7 +395,7 @@ async function requestPinnedJson(rawUrl, options) {
395
395
  };
396
396
  const requestFn = url.protocol === "https:" ? https.request : http.request;
397
397
  const request3 = requestFn(requestOptions, (response) => {
398
- const status = response.statusCode ?? 0;
398
+ const status = response.statusCode;
399
399
  if (status === 404 || status === 410) {
400
400
  response.resume();
401
401
  finish(void 0, void 0);
@@ -739,8 +739,7 @@ function boundedServerName(value) {
739
739
 
740
740
  // src/client.ts
741
741
  import { spawn } from "node:child_process";
742
- import { buildChildEnv } from "@wrongstack/core";
743
- import { toErrorMessage } from "@wrongstack/core/utils";
742
+ import { buildChildEnv, toErrorMessage } from "@wrongstack/core/utils";
744
743
 
745
744
  // src/constants.ts
746
745
  var MCP_CONSTANTS = Object.freeze({
@@ -997,13 +996,15 @@ function normalizeMCPTools(value) {
997
996
  if (typeof t.name !== "string" || t.name.trim().length === 0) continue;
998
997
  const inputSchema = t.inputSchema && typeof t.inputSchema === "object" && !Array.isArray(t.inputSchema) ? t.inputSchema : { type: "object", properties: {} };
999
998
  if (!t.inputSchema || typeof t.inputSchema !== "object" || Array.isArray(t.inputSchema)) {
1000
- console.warn(JSON.stringify({
1001
- level: "warn",
1002
- event: "mcp.tool_schema_invalid",
1003
- tool: t.name,
1004
- message: "no/invalid inputSchema \u2014 defaulting to empty object",
1005
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
1006
- }));
999
+ console.warn(
1000
+ JSON.stringify({
1001
+ level: "warn",
1002
+ event: "mcp.tool_schema_invalid",
1003
+ tool: t.name,
1004
+ message: "no/invalid inputSchema \u2014 defaulting to empty object",
1005
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1006
+ })
1007
+ );
1007
1008
  }
1008
1009
  tools.push({
1009
1010
  name: t.name,
@@ -1014,96 +1015,8 @@ function normalizeMCPTools(value) {
1014
1015
  return tools;
1015
1016
  }
1016
1017
 
1017
- // src/transport-jsonrpc.ts
1018
- import { ToolError } from "@wrongstack/core";
1019
- function isJsonRpcResult(v) {
1020
- if (typeof v !== "object" || v === null) return false;
1021
- const r = v;
1022
- if (r["jsonrpc"] !== "2.0" || typeof r["id"] !== "number") return false;
1023
- if (Object.hasOwn(r, "method")) return false;
1024
- const hasResult = Object.hasOwn(r, "result");
1025
- const hasError = Object.hasOwn(r, "error");
1026
- if (hasResult === hasError) return false;
1027
- if (hasError) {
1028
- const error = r["error"];
1029
- return typeof error === "object" && error !== null && typeof error["code"] === "number" && typeof error["message"] === "string";
1030
- }
1031
- return true;
1032
- }
1033
- function isJsonRpcMethodEnvelope(v) {
1034
- if (typeof v !== "object" || v === null) return false;
1035
- const envelope = v;
1036
- if (envelope["jsonrpc"] !== "2.0" || typeof envelope["method"] !== "string") return false;
1037
- const id = envelope["id"];
1038
- return id === void 0 || typeof id === "number" || typeof id === "string";
1039
- }
1040
- function extractJsonRpcEnvelopes(text) {
1041
- const out = [];
1042
- let dataBuf = [];
1043
- const flush = () => {
1044
- if (dataBuf.length === 0) return;
1045
- const joined = dataBuf.join("\n").trim();
1046
- dataBuf = [];
1047
- if (!joined) return;
1048
- try {
1049
- const parsed = JSON.parse(joined);
1050
- if (isJsonRpcResult(parsed) || isJsonRpcMethodEnvelope(parsed)) out.push(parsed);
1051
- } catch {
1052
- }
1053
- };
1054
- for (const raw of text.split("\n")) {
1055
- const line = raw.replace(/\r$/, "");
1056
- if (line === "") {
1057
- flush();
1058
- continue;
1059
- }
1060
- if (line.startsWith(":")) continue;
1061
- if (line.startsWith("data:")) {
1062
- let v = line.slice(5);
1063
- if (v.startsWith(" ")) v = v.slice(1);
1064
- dataBuf.push(v);
1065
- continue;
1066
- }
1067
- if (line.startsWith("event:") || line.startsWith("id:") || line.startsWith("retry:")) {
1068
- continue;
1069
- }
1070
- const trimmed = line.trim();
1071
- if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
1072
- try {
1073
- const parsed = JSON.parse(trimmed);
1074
- if (isJsonRpcResult(parsed) || isJsonRpcMethodEnvelope(parsed)) out.push(parsed);
1075
- } catch {
1076
- }
1077
- }
1078
- }
1079
- flush();
1080
- return out;
1081
- }
1082
- function extractJsonRpcResults(text) {
1083
- return extractJsonRpcEnvelopes(text).filter(isJsonRpcResult);
1084
- }
1085
- function assertMatchingJsonRpcResult(data, expectedId, method) {
1086
- if (!isJsonRpcResult(data)) {
1087
- throw new ToolError({
1088
- message: "Invalid JSON-RPC response: not a JSON-RPC 2.0 envelope",
1089
- code: "TOOL_EXECUTION_FAILED",
1090
- toolName: "mcp_transport_jsonrpc",
1091
- context: { method, expectedId, reason: "not-jsonrpc-envelope" }
1092
- });
1093
- }
1094
- if (data.id !== expectedId) {
1095
- throw new ToolError({
1096
- message: `Invalid JSON-RPC response: id mismatch for ${method} (expected ${expectedId}, got ${data.id})`,
1097
- code: "TOOL_EXECUTION_FAILED",
1098
- toolName: "mcp_transport_jsonrpc",
1099
- context: { method, expectedId, actualId: data.id, reason: "id-mismatch" }
1100
- });
1101
- }
1102
- return data;
1103
- }
1104
-
1105
1018
  // src/sse-reader.ts
1106
- import { ToolError as ToolError2 } from "@wrongstack/core";
1019
+ import { ToolError } from "@wrongstack/core/types";
1107
1020
  var SSE_READER_MAX_BUFFER = 256 * 1024;
1108
1021
  var SSE_READER_MAX_DATA_LINES = 1024;
1109
1022
  var SSEReader = class {
@@ -1119,7 +1032,7 @@ var SSEReader = class {
1119
1032
  }
1120
1033
  feed(chunk) {
1121
1034
  if (chunk.length > SSE_READER_MAX_BUFFER) {
1122
- throw new ToolError2({
1035
+ throw new ToolError({
1123
1036
  message: `SSE: chunk size ${chunk.length} exceeds max buffer ${SSE_READER_MAX_BUFFER} \u2014 refusing to accumulate`,
1124
1037
  code: "TOOL_EXECUTION_FAILED",
1125
1038
  toolName: "mcp_transport_sse_reader",
@@ -1128,7 +1041,7 @@ var SSEReader = class {
1128
1041
  }
1129
1042
  this.buffer += chunk;
1130
1043
  if (this.buffer.length > SSE_READER_MAX_BUFFER) {
1131
- throw new ToolError2({
1044
+ throw new ToolError({
1132
1045
  message: `SSE: pending line exceeds ${SSE_READER_MAX_BUFFER} bytes \u2014 upstream is not framing events`,
1133
1046
  code: "TOOL_EXECUTION_FAILED",
1134
1047
  toolName: "mcp_transport_sse_reader",
@@ -1139,13 +1052,16 @@ var SSEReader = class {
1139
1052
  }
1140
1053
  });
1141
1054
  }
1142
- let idx = this.buffer.indexOf("\n");
1055
+ let start = 0;
1056
+ let idx = this.buffer.indexOf("\n", start);
1143
1057
  while (idx !== -1) {
1144
- const line = this.buffer.slice(0, idx).replace(/\r$/, "");
1145
- this.buffer = this.buffer.slice(idx + 1);
1146
- idx = this.buffer.indexOf("\n");
1147
- this.processLine(line);
1058
+ let end = idx;
1059
+ if (end > start && this.buffer.charCodeAt(end - 1) === 13) end--;
1060
+ this.processLine(this.buffer.slice(start, end));
1061
+ start = idx + 1;
1062
+ idx = this.buffer.indexOf("\n", start);
1148
1063
  }
1064
+ if (start > 0) this.buffer = this.buffer.slice(start);
1149
1065
  }
1150
1066
  processLine(line) {
1151
1067
  if (line === "") {
@@ -1160,7 +1076,7 @@ var SSEReader = class {
1160
1076
  if (field === "event") {
1161
1077
  } else if (field === "data") {
1162
1078
  if (this.dataLines.length >= SSE_READER_MAX_DATA_LINES) {
1163
- throw new ToolError2({
1079
+ throw new ToolError({
1164
1080
  message: `SSE: exceeded ${SSE_READER_MAX_DATA_LINES} data lines per event \u2014 upstream is not sending blank-line delimiters`,
1165
1081
  code: "TOOL_EXECUTION_FAILED",
1166
1082
  toolName: "mcp_transport_sse_reader",
@@ -1204,11 +1120,11 @@ var SSEReader = class {
1204
1120
 
1205
1121
  // src/transport-base.ts
1206
1122
  import * as https2 from "node:https";
1207
- import { ConfigError as ConfigError2 } from "@wrongstack/core";
1123
+ import { ConfigError as ConfigError2 } from "@wrongstack/core/types";
1208
1124
 
1209
1125
  // src/transport-security.ts
1210
1126
  import * as net2 from "node:net";
1211
- import { ConfigError } from "@wrongstack/core";
1127
+ import { ConfigError } from "@wrongstack/core/types";
1212
1128
  function isTlsUnsafeAllowed() {
1213
1129
  return process.env["WRONGSTACK_UNSAFE_MCP_TLS"] === "1";
1214
1130
  }
@@ -1450,9 +1366,133 @@ var BaseHTTPTransport = class {
1450
1366
  }
1451
1367
  };
1452
1368
 
1369
+ // src/transport-jsonrpc.ts
1370
+ import { ToolError as ToolError2 } from "@wrongstack/core/types";
1371
+ function isJsonRpcResult(v) {
1372
+ if (typeof v !== "object" || v === null) return false;
1373
+ const r = v;
1374
+ if (r["jsonrpc"] !== "2.0" || typeof r["id"] !== "number") return false;
1375
+ if (Object.hasOwn(r, "method")) return false;
1376
+ const hasResult = Object.hasOwn(r, "result");
1377
+ const hasError = Object.hasOwn(r, "error");
1378
+ if (hasResult === hasError) return false;
1379
+ if (hasError) {
1380
+ const error = r["error"];
1381
+ return typeof error === "object" && error !== null && typeof error["code"] === "number" && typeof error["message"] === "string";
1382
+ }
1383
+ return true;
1384
+ }
1385
+ function isJsonRpcMethodEnvelope(v) {
1386
+ if (typeof v !== "object" || v === null) return false;
1387
+ const envelope = v;
1388
+ if (envelope["jsonrpc"] !== "2.0" || typeof envelope["method"] !== "string") return false;
1389
+ const id = envelope["id"];
1390
+ return id === void 0 || typeof id === "number" || typeof id === "string";
1391
+ }
1392
+ function extractJsonRpcEnvelopes(text) {
1393
+ const out = [];
1394
+ let dataBuf = [];
1395
+ const flush = () => {
1396
+ if (dataBuf.length === 0) return;
1397
+ const joined = dataBuf.join("\n").trim();
1398
+ dataBuf = [];
1399
+ if (!joined) return;
1400
+ try {
1401
+ const parsed = JSON.parse(joined);
1402
+ if (isJsonRpcResult(parsed) || isJsonRpcMethodEnvelope(parsed)) out.push(parsed);
1403
+ } catch {
1404
+ }
1405
+ };
1406
+ for (const raw of text.split("\n")) {
1407
+ const line = raw.replace(/\r$/, "");
1408
+ if (line === "") {
1409
+ flush();
1410
+ continue;
1411
+ }
1412
+ if (line.startsWith(":")) continue;
1413
+ if (line.startsWith("data:")) {
1414
+ let v = line.slice(5);
1415
+ if (v.startsWith(" ")) v = v.slice(1);
1416
+ dataBuf.push(v);
1417
+ continue;
1418
+ }
1419
+ if (line.startsWith("event:") || line.startsWith("id:") || line.startsWith("retry:")) {
1420
+ continue;
1421
+ }
1422
+ const trimmed = line.trim();
1423
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
1424
+ try {
1425
+ const parsed = JSON.parse(trimmed);
1426
+ if (isJsonRpcResult(parsed) || isJsonRpcMethodEnvelope(parsed)) out.push(parsed);
1427
+ } catch {
1428
+ }
1429
+ }
1430
+ }
1431
+ flush();
1432
+ return out;
1433
+ }
1434
+ function extractJsonRpcResults(text) {
1435
+ return extractJsonRpcEnvelopes(text).filter(isJsonRpcResult);
1436
+ }
1437
+ function assertMatchingJsonRpcResult(data, expectedId, method) {
1438
+ if (!isJsonRpcResult(data)) {
1439
+ throw new ToolError2({
1440
+ message: "Invalid JSON-RPC response: not a JSON-RPC 2.0 envelope",
1441
+ code: "TOOL_EXECUTION_FAILED",
1442
+ toolName: "mcp_transport_jsonrpc",
1443
+ context: { method, expectedId, reason: "not-jsonrpc-envelope" }
1444
+ });
1445
+ }
1446
+ if (data.id !== expectedId) {
1447
+ throw new ToolError2({
1448
+ message: `Invalid JSON-RPC response: id mismatch for ${method} (expected ${expectedId}, got ${data.id})`,
1449
+ code: "TOOL_EXECUTION_FAILED",
1450
+ toolName: "mcp_transport_jsonrpc",
1451
+ context: { method, expectedId, actualId: data.id, reason: "id-mismatch" }
1452
+ });
1453
+ }
1454
+ return data;
1455
+ }
1456
+
1453
1457
  // src/transport-sse.ts
1454
1458
  import { randomBytes as randomBytes2 } from "node:crypto";
1455
- import { ToolError as ToolError3 } from "@wrongstack/core";
1459
+ import { ToolError as ToolError4 } from "@wrongstack/core/types";
1460
+
1461
+ // src/read-body.ts
1462
+ import { ToolError as ToolError3 } from "@wrongstack/core/types";
1463
+ var MAX_MCP_HTTP_BODY_BYTES = 16 * 1024 * 1024;
1464
+ async function readBodyCapped(res, maxBytes = MAX_MCP_HTTP_BODY_BYTES) {
1465
+ const body = res.body;
1466
+ if (!body || typeof body.getReader !== "function") {
1467
+ return await res.text();
1468
+ }
1469
+ const reader = body.getReader();
1470
+ const chunks = [];
1471
+ let total = 0;
1472
+ try {
1473
+ for (; ; ) {
1474
+ const { done, value } = await reader.read();
1475
+ if (done) break;
1476
+ if (!value) continue;
1477
+ total += value.byteLength;
1478
+ if (total > maxBytes) {
1479
+ await reader.cancel().catch(() => void 0);
1480
+ throw new ToolError3({
1481
+ message: `MCP response body exceeded ${maxBytes} bytes \u2014 refusing to buffer`,
1482
+ code: "TOOL_EXECUTION_FAILED",
1483
+ toolName: "mcp_transport",
1484
+ context: { maxBytes, received: total }
1485
+ });
1486
+ }
1487
+ chunks.push(value);
1488
+ }
1489
+ } finally {
1490
+ reader.releaseLock?.();
1491
+ }
1492
+ return Buffer.concat(chunks).toString("utf8");
1493
+ }
1494
+
1495
+ // src/transport-sse.ts
1456
1496
  var SSETransport = class extends BaseHTTPTransport {
1457
1497
  _nextId = 1;
1458
1498
  readerDone = false;
@@ -1499,7 +1539,7 @@ var SSETransport = class extends BaseHTTPTransport {
1499
1539
  this.applyTlsAgent(fetchOpts);
1500
1540
  const response = await this.fetchWithAuthorization(sseUrl, fetchOpts, signal);
1501
1541
  if (!response.ok) {
1502
- throw new ToolError3({
1542
+ throw new ToolError4({
1503
1543
  message: `SSE connect HTTP ${response.status}: ${response.statusText}`,
1504
1544
  code: "TOOL_EXECUTION_FAILED",
1505
1545
  toolName: "mcp_transport_sse_connect",
@@ -1507,7 +1547,7 @@ var SSETransport = class extends BaseHTTPTransport {
1507
1547
  });
1508
1548
  }
1509
1549
  if (!response.body) {
1510
- throw new ToolError3({
1550
+ throw new ToolError4({
1511
1551
  message: "SSE response has no body",
1512
1552
  code: "TOOL_EXECUTION_FAILED",
1513
1553
  toolName: "mcp_transport_sse_connect",
@@ -1540,7 +1580,7 @@ var SSETransport = class extends BaseHTTPTransport {
1540
1580
  clientInfo: MCP_CONSTANTS.CLIENT_INFO
1541
1581
  });
1542
1582
  if (initRes.error) {
1543
- throw new ToolError3({
1583
+ throw new ToolError4({
1544
1584
  message: `initialize failed: ${initRes.error.message}`,
1545
1585
  code: "TOOL_EXECUTION_FAILED",
1546
1586
  toolName: "mcp_transport_initialize",
@@ -1615,7 +1655,7 @@ var SSETransport = class extends BaseHTTPTransport {
1615
1655
  const body2 = await res.text();
1616
1656
  const cap = MCP_CONSTANTS.REQUEST_LOG_CAP;
1617
1657
  const snippet = body2.length > cap ? `${body2.slice(0, cap)}\u2026 [${body2.length} bytes total]` : body2;
1618
- throw new ToolError3({
1658
+ throw new ToolError4({
1619
1659
  message: `HTTP ${res.status}: ${snippet}`,
1620
1660
  code: "TOOL_EXECUTION_FAILED",
1621
1661
  toolName: method,
@@ -1624,9 +1664,9 @@ var SSETransport = class extends BaseHTTPTransport {
1624
1664
  }
1625
1665
  let data;
1626
1666
  try {
1627
- data = await res.json();
1667
+ data = JSON.parse(await readBodyCapped(res));
1628
1668
  } catch (err) {
1629
- throw new ToolError3({
1669
+ throw new ToolError4({
1630
1670
  message: `Invalid JSON-RPC response: ${err instanceof Error ? err.message : "parse failed"}`,
1631
1671
  code: "TOOL_EXECUTION_FAILED",
1632
1672
  toolName: method,
@@ -1651,7 +1691,7 @@ var SSETransport = class extends BaseHTTPTransport {
1651
1691
  }
1652
1692
  async callTool(name, input, opts) {
1653
1693
  if (this.state !== "connected") {
1654
- throw new ToolError3({
1694
+ throw new ToolError4({
1655
1695
  message: `SSE transport not connected (state=${this.state})`,
1656
1696
  code: "TOOL_EXECUTION_FAILED",
1657
1697
  toolName: name,
@@ -1688,7 +1728,7 @@ var SSETransport = class extends BaseHTTPTransport {
1688
1728
  try {
1689
1729
  const res = await this.fetchWithAuthorization(this.url, fetchOpts, timeoutSignal.signal);
1690
1730
  if (!res.ok) {
1691
- throw new ToolError3({
1731
+ throw new ToolError4({
1692
1732
  message: `HTTP ${res.status}: ${res.statusText}`,
1693
1733
  code: "TOOL_EXECUTION_FAILED",
1694
1734
  toolName: method,
@@ -1702,9 +1742,9 @@ var SSETransport = class extends BaseHTTPTransport {
1702
1742
  }
1703
1743
  let data;
1704
1744
  try {
1705
- data = await res.json();
1745
+ data = JSON.parse(await readBodyCapped(res));
1706
1746
  } catch (err) {
1707
- throw new ToolError3({
1747
+ throw new ToolError4({
1708
1748
  message: `Invalid JSON-RPC response: ${err instanceof Error ? err.message : "parse failed"}`,
1709
1749
  code: "TOOL_EXECUTION_FAILED",
1710
1750
  toolName: method,
@@ -1764,7 +1804,7 @@ var StreamableHTTPTransport = class extends BaseHTTPTransport {
1764
1804
  }
1765
1805
  }
1766
1806
  const responses = envelopes.filter(isJsonRpcResult);
1767
- return responses.find((envelope) => envelope.id === requestId) ?? responses.find((envelope) => envelope.id !== void 0) ?? responses[0];
1807
+ return responses.find((envelope) => envelope.id === requestId) ?? responses[0];
1768
1808
  }
1769
1809
  handleNotification(method) {
1770
1810
  if (method === "notifications/resources/list_changed") {
@@ -1883,9 +1923,9 @@ var StreamableHTTPTransport = class extends BaseHTTPTransport {
1883
1923
  }
1884
1924
  if (method.startsWith("notifications/")) {
1885
1925
  await res.text().catch(() => void 0);
1886
- return { jsonrpc: "2.0" };
1926
+ return { jsonrpc: "2.0", id };
1887
1927
  }
1888
- const match = this.consumeResponseText(await res.text(), id);
1928
+ const match = this.consumeResponseText(await readBodyCapped(res), id);
1889
1929
  if (match) {
1890
1930
  return assertMatchingJsonRpcResult(match, id, method);
1891
1931
  }
@@ -1932,7 +1972,7 @@ var StreamableHTTPTransport = class extends BaseHTTPTransport {
1932
1972
  await res.text().catch(() => void 0);
1933
1973
  return { jsonrpc: "2.0", id };
1934
1974
  }
1935
- const parsed = this.consumeResponseText(await res.text(), id);
1975
+ const parsed = this.consumeResponseText(await readBodyCapped(res), id);
1936
1976
  if (parsed) {
1937
1977
  return {
1938
1978
  jsonrpc: "2.0",
@@ -1979,17 +2019,32 @@ var StreamableHTTPTransport = class extends BaseHTTPTransport {
1979
2019
  };
1980
2020
 
1981
2021
  // src/client.ts
1982
- function isJsonRpcResponse(value) {
1983
- if (typeof value !== "object" || value === null) return false;
1984
- const response = value;
1985
- if (response["jsonrpc"] !== "2.0" || typeof response["id"] !== "number") return false;
1986
- if (Object.hasOwn(response, "method")) return false;
1987
- const hasResult = Object.hasOwn(response, "result");
1988
- const hasError = Object.hasOwn(response, "error");
1989
- if (hasResult === hasError) return false;
1990
- if (!hasError) return true;
1991
- const error = response["error"];
1992
- return typeof error === "object" && error !== null && typeof error["code"] === "number" && typeof error["message"] === "string";
2022
+ function forceKillTree(child) {
2023
+ if (child.pid === void 0) {
2024
+ try {
2025
+ child.kill("SIGKILL");
2026
+ } catch {
2027
+ }
2028
+ return;
2029
+ }
2030
+ if (process.platform === "win32") {
2031
+ const killer = spawn("taskkill", ["/pid", String(child.pid), "/T", "/F"], {
2032
+ stdio: "ignore",
2033
+ windowsHide: true
2034
+ });
2035
+ killer.once("error", () => {
2036
+ try {
2037
+ child.kill("SIGKILL");
2038
+ } catch {
2039
+ }
2040
+ });
2041
+ killer.unref();
2042
+ return;
2043
+ }
2044
+ try {
2045
+ child.kill("SIGKILL");
2046
+ } catch {
2047
+ }
1993
2048
  }
1994
2049
  var MCPClient = class _MCPClient {
1995
2050
  constructor(opts) {
@@ -2383,10 +2438,7 @@ var MCPClient = class _MCPClient {
2383
2438
  new Promise((resolve) => setTimeout(() => resolve("timeout"), GRACEFUL_MS))
2384
2439
  ]);
2385
2440
  if (gracefulRace === "timeout") {
2386
- try {
2387
- child.kill("SIGKILL");
2388
- } catch {
2389
- }
2441
+ forceKillTree(child);
2390
2442
  await Promise.race([
2391
2443
  exitPromise,
2392
2444
  new Promise((resolve) => setTimeout(resolve, FORCE_TIMEOUT_MS))
@@ -2615,11 +2667,12 @@ var MCPClient = class _MCPClient {
2615
2667
  }
2616
2668
  return;
2617
2669
  }
2618
- if (!isJsonRpcResponse(msg)) return;
2619
- if (this.pending.has(msg.id)) {
2620
- const entry = this.pending.get(msg.id);
2621
- this.pending.delete(msg.id);
2622
- entry?.resolve(msg);
2670
+ if (!isJsonRpcResult(msg)) return;
2671
+ const response = msg;
2672
+ if (this.pending.has(response.id)) {
2673
+ const entry = this.pending.get(response.id);
2674
+ this.pending.delete(response.id);
2675
+ entry?.resolve(response);
2623
2676
  }
2624
2677
  }
2625
2678
  handleServerRequest(request3) {
@@ -2853,7 +2906,7 @@ async function writeConfig(path2, cfg) {
2853
2906
  try {
2854
2907
  await fs.rename(tmp, path2);
2855
2908
  } catch (err) {
2856
- await fs.rm(tmp, { force: true }).catch(() => void 0);
2909
+ await fs.rm(tmp, { force: true });
2857
2910
  throw err;
2858
2911
  }
2859
2912
  }
@@ -2917,6 +2970,8 @@ function projectServer(name, cfg, registry) {
2917
2970
  if (cfg.description !== void 0) info.description = cfg.description;
2918
2971
  if (cfg.url !== void 0) info.url = cfg.url;
2919
2972
  if (cfg.command !== void 0) info.command = cfg.command;
2973
+ if (cfg.args !== void 0) info.args = cfg.args;
2974
+ if (cfg.env !== void 0) info.env = cfg.env;
2920
2975
  if (cfg.lazy !== void 0) info.lazy = cfg.lazy;
2921
2976
  return info;
2922
2977
  }
@@ -3091,7 +3146,7 @@ function forgetRegistryState(registry, name) {
3091
3146
  }
3092
3147
 
3093
3148
  // src/manifest-cache.ts
3094
- import { createHash as createHash2 } from "node:crypto";
3149
+ import { createHash as createHash2, randomBytes as randomBytes4 } from "node:crypto";
3095
3150
  import * as fs2 from "node:fs/promises";
3096
3151
  import * as path from "node:path";
3097
3152
  function manifestConfigHash(cfg) {
@@ -3139,9 +3194,14 @@ async function writeCapabilityManifest(cacheDir, name, configHash, manifest) {
3139
3194
  const file = manifestFile(cacheDir, name);
3140
3195
  await fs2.mkdir(path.dirname(file), { recursive: true });
3141
3196
  const body = { version: 2, configHash, ...manifest };
3142
- const tmp = `${file}.tmp`;
3143
- await fs2.writeFile(tmp, JSON.stringify(body, null, 2), "utf8");
3144
- await fs2.rename(tmp, file);
3197
+ const tmp = `${file}.${process.pid}.${randomBytes4(6).toString("hex")}.tmp`;
3198
+ try {
3199
+ await fs2.writeFile(tmp, JSON.stringify(body, null, 2), "utf8");
3200
+ await fs2.rename(tmp, file);
3201
+ } catch (err) {
3202
+ await fs2.rm(tmp, { force: true });
3203
+ throw err;
3204
+ }
3145
3205
  } catch {
3146
3206
  }
3147
3207
  }
@@ -3202,7 +3262,10 @@ function evaluateHealthThresholds(operations, thresholds) {
3202
3262
  if (!thresholds) return [];
3203
3263
  const checks = [];
3204
3264
  if (thresholds.connectionLatencyP95Ms !== void 0 && operations.connectionSamples.length > 0) {
3205
- const value = percentile([...operations.connectionSamples].sort((a, b) => a - b), 0.95);
3265
+ const value = percentile(
3266
+ [...operations.connectionSamples].sort((a, b) => a - b),
3267
+ 0.95
3268
+ );
3206
3269
  checks.push({
3207
3270
  name: "connection-latency-p95",
3208
3271
  passed: value <= thresholds.connectionLatencyP95Ms,
@@ -3211,7 +3274,10 @@ function evaluateHealthThresholds(operations, thresholds) {
3211
3274
  });
3212
3275
  }
3213
3276
  if (thresholds.discoveryLatencyP95Ms !== void 0 && operations.discoverySamples.length > 0) {
3214
- const value = percentile([...operations.discoverySamples].sort((a, b) => a - b), 0.95);
3277
+ const value = percentile(
3278
+ [...operations.discoverySamples].sort((a, b) => a - b),
3279
+ 0.95
3280
+ );
3215
3281
  checks.push({
3216
3282
  name: "discovery-latency-p95",
3217
3283
  passed: value <= thresholds.discoveryLatencyP95Ms,
@@ -3220,7 +3286,10 @@ function evaluateHealthThresholds(operations, thresholds) {
3220
3286
  });
3221
3287
  }
3222
3288
  if (thresholds.callLatencyP95Ms !== void 0 && operations.callSamples.length > 0) {
3223
- const value = percentile([...operations.callSamples].sort((a, b) => a - b), 0.95);
3289
+ const value = percentile(
3290
+ [...operations.callSamples].sort((a, b) => a - b),
3291
+ 0.95
3292
+ );
3224
3293
  checks.push({
3225
3294
  name: "call-latency-p95",
3226
3295
  passed: value <= thresholds.callLatencyP95Ms,
@@ -3268,10 +3337,10 @@ function percentile(sorted, ratio) {
3268
3337
  }
3269
3338
 
3270
3339
  // src/registry.ts
3271
- import { expectDefined } from "@wrongstack/core";
3340
+ import { expectDefined } from "@wrongstack/core/utils";
3272
3341
 
3273
3342
  // src/wrap-tool.ts
3274
- import { ToolCapabilities } from "@wrongstack/core";
3343
+ import { ToolCapabilities } from "@wrongstack/core/security";
3275
3344
  var MUTATING_RE = /create|update|delete|write|send|set|put|post|patch|remove|rename|move/i;
3276
3345
  function isMutatingTool(mcpTool) {
3277
3346
  if (MUTATING_RE.test(mcpTool.name)) return true;
@@ -3456,11 +3525,7 @@ var MCPRegistry = class _MCPRegistry {
3456
3525
  * cache yet, do a one-time cold discovery connect to learn + cache the tools.
3457
3526
  */
3458
3527
  async startLazy(slot) {
3459
- const cacheDir = this.cacheDir;
3460
- if (!cacheDir) {
3461
- await this.attemptConnect(slot);
3462
- return;
3463
- }
3528
+ const cacheDir = expectDefined(this.cacheDir);
3464
3529
  const hash = manifestConfigHash(slot.cfg);
3465
3530
  const cached = await readCapabilityManifest(cacheDir, slot.cfg.name, hash);
3466
3531
  if (cached) {
@@ -4298,8 +4363,7 @@ function catalogSnapshot(slot) {
4298
4363
 
4299
4364
  // src/server.ts
4300
4365
  import { createServer } from "node:http";
4301
- import { expectDefined as expectDefined2 } from "@wrongstack/core";
4302
- import { toErrorMessage as toErrorMessage2 } from "@wrongstack/core/utils";
4366
+ import { expectDefined as expectDefined2, toErrorMessage as toErrorMessage2 } from "@wrongstack/core/utils";
4303
4367
  var PARSE_ERROR = -32700;
4304
4368
  var INVALID_REQUEST = -32600;
4305
4369
  var METHOD_NOT_FOUND = -32601;
@@ -4624,8 +4688,7 @@ function serveHttp(server, opts = {}) {
4624
4688
  httpServer.once("error", reject);
4625
4689
  httpServer.listen(port, host, () => {
4626
4690
  httpServer.removeListener("error", reject);
4627
- const addr = httpServer.address();
4628
- const boundPort = typeof addr === "object" && addr ? addr.port : port;
4691
+ const boundPort = httpServer.address().port;
4629
4692
  const displayHost = host === "::1" ? "[::1]" : host;
4630
4693
  resolve({
4631
4694
  port: boundPort,
@@ -4657,18 +4720,14 @@ async function handleHttpRequest(server, req, res, token, log) {
4657
4720
  }
4658
4721
  }
4659
4722
  let body = "";
4660
- let tooLarge = false;
4661
4723
  req.on("data", (chunk) => {
4662
- if (tooLarge) return;
4663
4724
  body += chunk.toString("utf8");
4664
4725
  if (body.length > HTTP_BODY_CAP) {
4665
- tooLarge = true;
4666
4726
  send(413, JSON.stringify({ error: "payload too large" }));
4667
4727
  req.destroy();
4668
4728
  }
4669
4729
  });
4670
4730
  req.on("end", () => {
4671
- if (tooLarge) return;
4672
4731
  void server.handleMessage(body).then((out) => {
4673
4732
  if (out === null) return send(202, "");
4674
4733
  return send(200, out);