@wrongstack/mcp 0.286.0 → 0.287.0

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
@@ -1014,66 +1014,96 @@ function normalizeMCPTools(value) {
1014
1014
  return tools;
1015
1015
  }
1016
1016
 
1017
- // src/transport.ts
1018
- import { randomBytes as randomBytes2 } from "node:crypto";
1019
- import * as https2 from "node:https";
1020
- import * as net2 from "node:net";
1021
- import { ConfigError, ToolError } from "@wrongstack/core";
1022
- function isTlsUnsafeAllowed() {
1023
- return process.env["WRONGSTACK_UNSAFE_MCP_TLS"] === "1";
1024
- }
1025
- function validateTransportUrl(rawUrl) {
1026
- let url;
1027
- try {
1028
- url = new URL(rawUrl);
1029
- } catch {
1030
- throw new ConfigError({
1031
- message: `MCP transport: invalid URL "${rawUrl}"`,
1032
- code: "CONFIG_INVALID",
1033
- context: { field: "url", rawUrl }
1034
- });
1035
- }
1036
- if (url.protocol !== "http:" && url.protocol !== "https:") {
1037
- throw new ConfigError({
1038
- message: `MCP transport: unsupported protocol "${url.protocol}" \u2014 only http/https allowed`,
1039
- code: "CONFIG_INVALID",
1040
- context: { field: "url", rawUrl, protocol: url.protocol }
1041
- });
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";
1042
1030
  }
1043
- const hostname = url.hostname;
1044
- const host = hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname;
1045
- const ipVersion = net2.isIP(host);
1046
- if (ipVersion === 4) {
1047
- const parts = host.split(".").map(Number);
1048
- if (parts[0] === 169 && parts[1] === 254) {
1049
- throw new ConfigError({
1050
- message: `MCP transport: blocked link-local/IMDS address "${hostname}" \u2014 likely not a valid MCP server`,
1051
- code: "CONFIG_INVALID",
1052
- context: { field: "url", rawUrl, hostname }
1053
- });
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 {
1054
1052
  }
1055
- } else if (ipVersion === 6) {
1056
- const lower = host.toLowerCase();
1057
- const linkLocal = /^fe[89ab]/.test(lower);
1058
- if (linkLocal || lower === "fd00:ec2::254") {
1059
- throw new ConfigError({
1060
- message: `MCP transport: blocked link-local/IMDS address "${hostname}" \u2014 likely not a valid MCP server`,
1061
- code: "CONFIG_INVALID",
1062
- context: { field: "url", rawUrl, hostname }
1063
- });
1053
+ };
1054
+ for (const raw of text.split("\n")) {
1055
+ const line = raw.replace(/\r$/, "");
1056
+ if (line === "") {
1057
+ flush();
1058
+ continue;
1064
1059
  }
1065
- }
1066
- if (url.protocol === "http:") {
1067
- const isLoopback = hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
1068
- if (!isLoopback) {
1069
- throw new ConfigError({
1070
- message: `MCP transport: http:// is only allowed for loopback addresses; use https:// for "${hostname}"`,
1071
- code: "CONFIG_INVALID",
1072
- context: { field: "url", rawUrl, hostname, protocol: url.protocol }
1073
- });
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
+ }
1074
1077
  }
1075
1078
  }
1079
+ flush();
1080
+ return out;
1076
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
+ // src/sse-reader.ts
1106
+ import { ToolError as ToolError2 } from "@wrongstack/core";
1077
1107
  var SSE_READER_MAX_BUFFER = 256 * 1024;
1078
1108
  var SSE_READER_MAX_DATA_LINES = 1024;
1079
1109
  var SSEReader = class {
@@ -1089,7 +1119,7 @@ var SSEReader = class {
1089
1119
  }
1090
1120
  feed(chunk) {
1091
1121
  if (chunk.length > SSE_READER_MAX_BUFFER) {
1092
- throw new ToolError({
1122
+ throw new ToolError2({
1093
1123
  message: `SSE: chunk size ${chunk.length} exceeds max buffer ${SSE_READER_MAX_BUFFER} \u2014 refusing to accumulate`,
1094
1124
  code: "TOOL_EXECUTION_FAILED",
1095
1125
  toolName: "mcp_transport_sse_reader",
@@ -1098,7 +1128,7 @@ var SSEReader = class {
1098
1128
  }
1099
1129
  this.buffer += chunk;
1100
1130
  if (this.buffer.length > SSE_READER_MAX_BUFFER) {
1101
- throw new ToolError({
1131
+ throw new ToolError2({
1102
1132
  message: `SSE: pending line exceeds ${SSE_READER_MAX_BUFFER} bytes \u2014 upstream is not framing events`,
1103
1133
  code: "TOOL_EXECUTION_FAILED",
1104
1134
  toolName: "mcp_transport_sse_reader",
@@ -1130,7 +1160,7 @@ var SSEReader = class {
1130
1160
  if (field === "event") {
1131
1161
  } else if (field === "data") {
1132
1162
  if (this.dataLines.length >= SSE_READER_MAX_DATA_LINES) {
1133
- throw new ToolError({
1163
+ throw new ToolError2({
1134
1164
  message: `SSE: exceeded ${SSE_READER_MAX_DATA_LINES} data lines per event \u2014 upstream is not sending blank-line delimiters`,
1135
1165
  code: "TOOL_EXECUTION_FAILED",
1136
1166
  toolName: "mcp_transport_sse_reader",
@@ -1171,91 +1201,71 @@ var SSEReader = class {
1171
1201
  this.listeners = [];
1172
1202
  }
1173
1203
  };
1174
- function isJsonRpcResult(v) {
1175
- if (typeof v !== "object" || v === null) return false;
1176
- const r = v;
1177
- if (r["jsonrpc"] !== "2.0" || typeof r["id"] !== "number") return false;
1178
- if (Object.hasOwn(r, "method")) return false;
1179
- const hasResult = Object.hasOwn(r, "result");
1180
- const hasError = Object.hasOwn(r, "error");
1181
- if (hasResult === hasError) return false;
1182
- if (hasError) {
1183
- const error = r["error"];
1184
- return typeof error === "object" && error !== null && typeof error["code"] === "number" && typeof error["message"] === "string";
1185
- }
1186
- return true;
1187
- }
1188
- function isJsonRpcMethodEnvelope(v) {
1189
- if (typeof v !== "object" || v === null) return false;
1190
- const envelope = v;
1191
- if (envelope["jsonrpc"] !== "2.0" || typeof envelope["method"] !== "string") return false;
1192
- const id = envelope["id"];
1193
- return id === void 0 || typeof id === "number" || typeof id === "string";
1194
- }
1195
- function extractJsonRpcEnvelopes(text) {
1196
- const out = [];
1197
- let dataBuf = [];
1198
- const flush = () => {
1199
- if (dataBuf.length === 0) return;
1200
- const joined = dataBuf.join("\n").trim();
1201
- dataBuf = [];
1202
- if (!joined) return;
1203
- try {
1204
- const parsed = JSON.parse(joined);
1205
- if (isJsonRpcResult(parsed) || isJsonRpcMethodEnvelope(parsed)) out.push(parsed);
1206
- } catch {
1207
- }
1208
- };
1209
- for (const raw of text.split("\n")) {
1210
- const line = raw.replace(/\r$/, "");
1211
- if (line === "") {
1212
- flush();
1213
- continue;
1214
- }
1215
- if (line.startsWith(":")) continue;
1216
- if (line.startsWith("data:")) {
1217
- let v = line.slice(5);
1218
- if (v.startsWith(" ")) v = v.slice(1);
1219
- dataBuf.push(v);
1220
- continue;
1221
- }
1222
- if (line.startsWith("event:") || line.startsWith("id:") || line.startsWith("retry:")) {
1223
- continue;
1224
- }
1225
- const trimmed = line.trim();
1226
- if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
1227
- try {
1228
- const parsed = JSON.parse(trimmed);
1229
- if (isJsonRpcResult(parsed) || isJsonRpcMethodEnvelope(parsed)) out.push(parsed);
1230
- } catch {
1231
- }
1232
- }
1233
- }
1234
- flush();
1235
- return out;
1236
- }
1237
- function extractJsonRpcResults(text) {
1238
- return extractJsonRpcEnvelopes(text).filter(isJsonRpcResult);
1204
+
1205
+ // src/transport-base.ts
1206
+ import * as https2 from "node:https";
1207
+ import { ConfigError as ConfigError2 } from "@wrongstack/core";
1208
+
1209
+ // src/transport-security.ts
1210
+ import * as net2 from "node:net";
1211
+ import { ConfigError } from "@wrongstack/core";
1212
+ function isTlsUnsafeAllowed() {
1213
+ return process.env["WRONGSTACK_UNSAFE_MCP_TLS"] === "1";
1239
1214
  }
1240
- function assertMatchingJsonRpcResult(data, expectedId, method) {
1241
- if (!isJsonRpcResult(data)) {
1242
- throw new ToolError({
1243
- message: "Invalid JSON-RPC response: not a JSON-RPC 2.0 envelope",
1244
- code: "TOOL_EXECUTION_FAILED",
1245
- toolName: "mcp_transport_jsonrpc",
1246
- context: { method, expectedId, reason: "not-jsonrpc-envelope" }
1215
+ function validateTransportUrl(rawUrl) {
1216
+ let url;
1217
+ try {
1218
+ url = new URL(rawUrl);
1219
+ } catch {
1220
+ throw new ConfigError({
1221
+ message: `MCP transport: invalid URL "${rawUrl}"`,
1222
+ code: "CONFIG_INVALID",
1223
+ context: { field: "url", rawUrl }
1247
1224
  });
1248
1225
  }
1249
- if (data.id !== expectedId) {
1250
- throw new ToolError({
1251
- message: `Invalid JSON-RPC response: id mismatch for ${method} (expected ${expectedId}, got ${data.id})`,
1252
- code: "TOOL_EXECUTION_FAILED",
1253
- toolName: "mcp_transport_jsonrpc",
1254
- context: { method, expectedId, actualId: data.id, reason: "id-mismatch" }
1226
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
1227
+ throw new ConfigError({
1228
+ message: `MCP transport: unsupported protocol "${url.protocol}" \u2014 only http/https allowed`,
1229
+ code: "CONFIG_INVALID",
1230
+ context: { field: "url", rawUrl, protocol: url.protocol }
1255
1231
  });
1256
1232
  }
1257
- return data;
1233
+ const hostname = url.hostname;
1234
+ const host = hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname;
1235
+ const ipVersion = net2.isIP(host);
1236
+ if (ipVersion === 4) {
1237
+ const parts = host.split(".").map(Number);
1238
+ if (parts[0] === 169 && parts[1] === 254) {
1239
+ throw new ConfigError({
1240
+ message: `MCP transport: blocked link-local/IMDS address "${hostname}" \u2014 likely not a valid MCP server`,
1241
+ code: "CONFIG_INVALID",
1242
+ context: { field: "url", rawUrl, hostname }
1243
+ });
1244
+ }
1245
+ } else if (ipVersion === 6) {
1246
+ const lower = host.toLowerCase();
1247
+ const linkLocal = /^fe[89ab]/.test(lower);
1248
+ if (linkLocal || lower === "fd00:ec2::254") {
1249
+ throw new ConfigError({
1250
+ message: `MCP transport: blocked link-local/IMDS address "${hostname}" \u2014 likely not a valid MCP server`,
1251
+ code: "CONFIG_INVALID",
1252
+ context: { field: "url", rawUrl, hostname }
1253
+ });
1254
+ }
1255
+ }
1256
+ if (url.protocol === "http:") {
1257
+ const isLoopback = hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
1258
+ if (!isLoopback) {
1259
+ throw new ConfigError({
1260
+ message: `MCP transport: http:// is only allowed for loopback addresses; use https:// for "${hostname}"`,
1261
+ code: "CONFIG_INVALID",
1262
+ context: { field: "url", rawUrl, hostname, protocol: url.protocol }
1263
+ });
1264
+ }
1265
+ }
1258
1266
  }
1267
+
1268
+ // src/transport-base.ts
1259
1269
  function makeAbortError(method) {
1260
1270
  const err = new Error(`MCP request "${method}" aborted by client`);
1261
1271
  err.name = "AbortError";
@@ -1312,7 +1322,7 @@ var BaseHTTPTransport = class {
1312
1322
  if (opts.tls) {
1313
1323
  if (opts.tls.rejectUnauthorized === false) {
1314
1324
  if (!isTlsUnsafeAllowed()) {
1315
- throw new ConfigError({
1325
+ throw new ConfigError2({
1316
1326
  message: `[mcp:${transportName}] TLS verification disabled \u2014 set WRONGSTACK_UNSAFE_MCP_TLS=1 to allow. Rejecting insecure configuration for ${this.url}.`,
1317
1327
  code: "CONFIG_INVALID",
1318
1328
  context: { field: "tls.rejectUnauthorized", transportName, url: this.url }
@@ -1439,6 +1449,10 @@ var BaseHTTPTransport = class {
1439
1449
  }
1440
1450
  }
1441
1451
  };
1452
+
1453
+ // src/transport-sse.ts
1454
+ import { randomBytes as randomBytes2 } from "node:crypto";
1455
+ import { ToolError as ToolError3 } from "@wrongstack/core";
1442
1456
  var SSETransport = class extends BaseHTTPTransport {
1443
1457
  _nextId = 1;
1444
1458
  readerDone = false;
@@ -1485,7 +1499,7 @@ var SSETransport = class extends BaseHTTPTransport {
1485
1499
  this.applyTlsAgent(fetchOpts);
1486
1500
  const response = await this.fetchWithAuthorization(sseUrl, fetchOpts, signal);
1487
1501
  if (!response.ok) {
1488
- throw new ToolError({
1502
+ throw new ToolError3({
1489
1503
  message: `SSE connect HTTP ${response.status}: ${response.statusText}`,
1490
1504
  code: "TOOL_EXECUTION_FAILED",
1491
1505
  toolName: "mcp_transport_sse_connect",
@@ -1493,7 +1507,7 @@ var SSETransport = class extends BaseHTTPTransport {
1493
1507
  });
1494
1508
  }
1495
1509
  if (!response.body) {
1496
- throw new ToolError({
1510
+ throw new ToolError3({
1497
1511
  message: "SSE response has no body",
1498
1512
  code: "TOOL_EXECUTION_FAILED",
1499
1513
  toolName: "mcp_transport_sse_connect",
@@ -1526,7 +1540,7 @@ var SSETransport = class extends BaseHTTPTransport {
1526
1540
  clientInfo: MCP_CONSTANTS.CLIENT_INFO
1527
1541
  });
1528
1542
  if (initRes.error) {
1529
- throw new ToolError({
1543
+ throw new ToolError3({
1530
1544
  message: `initialize failed: ${initRes.error.message}`,
1531
1545
  code: "TOOL_EXECUTION_FAILED",
1532
1546
  toolName: "mcp_transport_initialize",
@@ -1601,7 +1615,7 @@ var SSETransport = class extends BaseHTTPTransport {
1601
1615
  const body2 = await res.text();
1602
1616
  const cap = MCP_CONSTANTS.REQUEST_LOG_CAP;
1603
1617
  const snippet = body2.length > cap ? `${body2.slice(0, cap)}\u2026 [${body2.length} bytes total]` : body2;
1604
- throw new ToolError({
1618
+ throw new ToolError3({
1605
1619
  message: `HTTP ${res.status}: ${snippet}`,
1606
1620
  code: "TOOL_EXECUTION_FAILED",
1607
1621
  toolName: method,
@@ -1612,7 +1626,7 @@ var SSETransport = class extends BaseHTTPTransport {
1612
1626
  try {
1613
1627
  data = await res.json();
1614
1628
  } catch (err) {
1615
- throw new ToolError({
1629
+ throw new ToolError3({
1616
1630
  message: `Invalid JSON-RPC response: ${err instanceof Error ? err.message : "parse failed"}`,
1617
1631
  code: "TOOL_EXECUTION_FAILED",
1618
1632
  toolName: method,
@@ -1637,7 +1651,7 @@ var SSETransport = class extends BaseHTTPTransport {
1637
1651
  }
1638
1652
  async callTool(name, input, opts) {
1639
1653
  if (this.state !== "connected") {
1640
- throw new ToolError({
1654
+ throw new ToolError3({
1641
1655
  message: `SSE transport not connected (state=${this.state})`,
1642
1656
  code: "TOOL_EXECUTION_FAILED",
1643
1657
  toolName: name,
@@ -1674,7 +1688,7 @@ var SSETransport = class extends BaseHTTPTransport {
1674
1688
  try {
1675
1689
  const res = await this.fetchWithAuthorization(this.url, fetchOpts, timeoutSignal.signal);
1676
1690
  if (!res.ok) {
1677
- throw new ToolError({
1691
+ throw new ToolError3({
1678
1692
  message: `HTTP ${res.status}: ${res.statusText}`,
1679
1693
  code: "TOOL_EXECUTION_FAILED",
1680
1694
  toolName: method,
@@ -1690,7 +1704,7 @@ var SSETransport = class extends BaseHTTPTransport {
1690
1704
  try {
1691
1705
  data = await res.json();
1692
1706
  } catch (err) {
1693
- throw new ToolError({
1707
+ throw new ToolError3({
1694
1708
  message: `Invalid JSON-RPC response: ${err instanceof Error ? err.message : "parse failed"}`,
1695
1709
  code: "TOOL_EXECUTION_FAILED",
1696
1710
  toolName: method,
@@ -1731,6 +1745,8 @@ var SSETransport = class extends BaseHTTPTransport {
1731
1745
  this.state = "disconnected";
1732
1746
  }
1733
1747
  };
1748
+
1749
+ // src/transport-streamable.ts
1734
1750
  var StreamableHTTPTransport = class extends BaseHTTPTransport {
1735
1751
  _nextId = 1;
1736
1752
  sessionId;