@wrongstack/mcp 0.275.0 → 0.276.2

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
@@ -1,5 +1,5 @@
1
1
  import { spawn } from 'child_process';
2
- import { expectDefined, ToolCapabilities, buildChildEnv } from '@wrongstack/core';
2
+ import { expectDefined, ToolCapabilities, buildChildEnv, ToolError, ConfigError } from '@wrongstack/core';
3
3
  import { createHash, randomBytes } from 'crypto';
4
4
  import * as https from 'https';
5
5
  import * as net from 'net';
@@ -88,12 +88,18 @@ function validateTransportUrl(rawUrl) {
88
88
  try {
89
89
  url = new URL(rawUrl);
90
90
  } catch {
91
- throw new Error(`MCP transport: invalid URL "${rawUrl}"`);
91
+ throw new ConfigError({
92
+ message: `MCP transport: invalid URL "${rawUrl}"`,
93
+ code: "CONFIG_INVALID",
94
+ context: { field: "url", rawUrl }
95
+ });
92
96
  }
93
97
  if (url.protocol !== "http:" && url.protocol !== "https:") {
94
- throw new Error(
95
- `MCP transport: unsupported protocol "${url.protocol}" \u2014 only http/https allowed`
96
- );
98
+ throw new ConfigError({
99
+ message: `MCP transport: unsupported protocol "${url.protocol}" \u2014 only http/https allowed`,
100
+ code: "CONFIG_INVALID",
101
+ context: { field: "url", rawUrl, protocol: url.protocol }
102
+ });
97
103
  }
98
104
  const hostname = url.hostname;
99
105
  const host = hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname;
@@ -101,25 +107,31 @@ function validateTransportUrl(rawUrl) {
101
107
  if (ipVersion === 4) {
102
108
  const parts = host.split(".").map(Number);
103
109
  if (parts[0] === 169 && parts[1] === 254) {
104
- throw new Error(
105
- `MCP transport: blocked link-local/IMDS address "${hostname}" \u2014 likely not a valid MCP server`
106
- );
110
+ throw new ConfigError({
111
+ message: `MCP transport: blocked link-local/IMDS address "${hostname}" \u2014 likely not a valid MCP server`,
112
+ code: "CONFIG_INVALID",
113
+ context: { field: "url", rawUrl, hostname }
114
+ });
107
115
  }
108
116
  } else if (ipVersion === 6) {
109
117
  const lower = host.toLowerCase();
110
118
  const linkLocal = /^fe[89ab]/.test(lower);
111
119
  if (linkLocal || lower === "fd00:ec2::254") {
112
- throw new Error(
113
- `MCP transport: blocked link-local/IMDS address "${hostname}" \u2014 likely not a valid MCP server`
114
- );
120
+ throw new ConfigError({
121
+ message: `MCP transport: blocked link-local/IMDS address "${hostname}" \u2014 likely not a valid MCP server`,
122
+ code: "CONFIG_INVALID",
123
+ context: { field: "url", rawUrl, hostname }
124
+ });
115
125
  }
116
126
  }
117
127
  if (url.protocol === "http:") {
118
128
  const isLoopback = hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
119
129
  if (!isLoopback) {
120
- throw new Error(
121
- `MCP transport: http:// is only allowed for loopback addresses; use https:// for "${hostname}"`
122
- );
130
+ throw new ConfigError({
131
+ message: `MCP transport: http:// is only allowed for loopback addresses; use https:// for "${hostname}"`,
132
+ code: "CONFIG_INVALID",
133
+ context: { field: "url", rawUrl, hostname, protocol: url.protocol }
134
+ });
123
135
  }
124
136
  }
125
137
  }
@@ -138,15 +150,21 @@ var SSEReader = class {
138
150
  }
139
151
  feed(chunk) {
140
152
  if (chunk.length > SSE_READER_MAX_BUFFER) {
141
- throw new Error(
142
- `SSE: chunk size ${chunk.length} exceeds max buffer ${SSE_READER_MAX_BUFFER} \u2014 refusing to accumulate`
143
- );
153
+ throw new ToolError({
154
+ message: `SSE: chunk size ${chunk.length} exceeds max buffer ${SSE_READER_MAX_BUFFER} \u2014 refusing to accumulate`,
155
+ code: "TOOL_EXECUTION_FAILED",
156
+ toolName: "mcp_transport_sse_reader",
157
+ context: { phase: "feed", chunkLength: chunk.length, maxBuffer: SSE_READER_MAX_BUFFER }
158
+ });
144
159
  }
145
160
  this.buffer += chunk;
146
161
  if (this.buffer.length > SSE_READER_MAX_BUFFER) {
147
- throw new Error(
148
- `SSE: pending line exceeds ${SSE_READER_MAX_BUFFER} bytes \u2014 upstream is not framing events`
149
- );
162
+ throw new ToolError({
163
+ message: `SSE: pending line exceeds ${SSE_READER_MAX_BUFFER} bytes \u2014 upstream is not framing events`,
164
+ code: "TOOL_EXECUTION_FAILED",
165
+ toolName: "mcp_transport_sse_reader",
166
+ context: { phase: "feed", bufferLength: this.buffer.length, maxBuffer: SSE_READER_MAX_BUFFER }
167
+ });
150
168
  }
151
169
  let idx = this.buffer.indexOf("\n");
152
170
  while (idx !== -1) {
@@ -168,9 +186,12 @@ var SSEReader = class {
168
186
  if (value.startsWith(" ")) value = value.slice(1);
169
187
  if (field === "event") ; else if (field === "data") {
170
188
  if (this.dataLines.length >= SSE_READER_MAX_DATA_LINES) {
171
- throw new Error(
172
- `SSE: exceeded ${SSE_READER_MAX_DATA_LINES} data lines per event \u2014 upstream is not sending blank-line delimiters`
173
- );
189
+ throw new ToolError({
190
+ message: `SSE: exceeded ${SSE_READER_MAX_DATA_LINES} data lines per event \u2014 upstream is not sending blank-line delimiters`,
191
+ code: "TOOL_EXECUTION_FAILED",
192
+ toolName: "mcp_transport_sse_reader",
193
+ context: { phase: "processLine", dataLineCount: this.dataLines.length, maxDataLines: SSE_READER_MAX_DATA_LINES }
194
+ });
174
195
  }
175
196
  this.dataLines.push(value);
176
197
  }
@@ -259,15 +280,28 @@ function pickJsonRpcResult(text, id) {
259
280
  }
260
281
  function assertMatchingJsonRpcResult(data, expectedId, method) {
261
282
  if (!isJsonRpcResult(data)) {
262
- throw new Error("Invalid JSON-RPC response: not a JSON-RPC 2.0 envelope");
283
+ throw new ToolError({
284
+ message: "Invalid JSON-RPC response: not a JSON-RPC 2.0 envelope",
285
+ code: "TOOL_EXECUTION_FAILED",
286
+ toolName: "mcp_transport_jsonrpc",
287
+ context: { method, expectedId, reason: "not-jsonrpc-envelope" }
288
+ });
263
289
  }
264
290
  if (data.id !== void 0 && data.id !== expectedId) {
265
- throw new Error(
266
- `Invalid JSON-RPC response: id mismatch for ${method} (expected ${expectedId}, got ${data.id})`
267
- );
291
+ throw new ToolError({
292
+ message: `Invalid JSON-RPC response: id mismatch for ${method} (expected ${expectedId}, got ${data.id})`,
293
+ code: "TOOL_EXECUTION_FAILED",
294
+ toolName: "mcp_transport_jsonrpc",
295
+ context: { method, expectedId, actualId: data.id, reason: "id-mismatch" }
296
+ });
268
297
  }
269
298
  if (data.id === void 0 && !method.startsWith("notifications/")) {
270
- throw new Error(`Invalid JSON-RPC response: missing id for ${method}`);
299
+ throw new ToolError({
300
+ message: `Invalid JSON-RPC response: missing id for ${method}`,
301
+ code: "TOOL_EXECUTION_FAILED",
302
+ toolName: "mcp_transport_jsonrpc",
303
+ context: { method, expectedId, reason: "missing-id" }
304
+ });
271
305
  }
272
306
  return data;
273
307
  }
@@ -312,9 +346,11 @@ var BaseHTTPTransport = class {
312
346
  if (opts.tls) {
313
347
  if (opts.tls.rejectUnauthorized === false) {
314
348
  if (!isTlsUnsafeAllowed()) {
315
- throw new Error(
316
- `[mcp:${transportName}] TLS verification disabled \u2014 set WRONGSTACK_UNSAFE_MCP_TLS=1 to allow. Rejecting insecure configuration for ${this.url}.`
317
- );
349
+ throw new ConfigError({
350
+ message: `[mcp:${transportName}] TLS verification disabled \u2014 set WRONGSTACK_UNSAFE_MCP_TLS=1 to allow. Rejecting insecure configuration for ${this.url}.`,
351
+ code: "CONFIG_INVALID",
352
+ context: { field: "tls.rejectUnauthorized", transportName, url: this.url }
353
+ });
318
354
  }
319
355
  console.error(
320
356
  `[mcp:${transportName}] \u26A0\uFE0F TLS verification DISABLED for ${this.url}. Network attacks are possible \u2014 only use on localhost.`
@@ -415,10 +451,20 @@ var SSETransport = class extends BaseHTTPTransport {
415
451
  this.applyTlsAgent(fetchOpts);
416
452
  const response = await fetch(sseUrl, fetchOpts);
417
453
  if (!response.ok) {
418
- throw new Error(`SSE connect HTTP ${response.status}: ${response.statusText}`);
454
+ throw new ToolError({
455
+ message: `SSE connect HTTP ${response.status}: ${response.statusText}`,
456
+ code: "TOOL_EXECUTION_FAILED",
457
+ toolName: "mcp_transport_sse_connect",
458
+ context: { url: sseUrl, status: response.status, statusText: response.statusText }
459
+ });
419
460
  }
420
461
  if (!response.body) {
421
- throw new Error("SSE response has no body");
462
+ throw new ToolError({
463
+ message: "SSE response has no body",
464
+ code: "TOOL_EXECUTION_FAILED",
465
+ toolName: "mcp_transport_sse_connect",
466
+ context: { url: sseUrl, reason: "missing-body" }
467
+ });
422
468
  }
423
469
  const textDecoder = new TextDecoder();
424
470
  const sseReader = new SSEReader();
@@ -442,7 +488,12 @@ var SSETransport = class extends BaseHTTPTransport {
442
488
  clientInfo: MCP_CONSTANTS.CLIENT_INFO
443
489
  });
444
490
  if (initRes.error) {
445
- throw new Error(`initialize failed: ${initRes.error.message}`);
491
+ throw new ToolError({
492
+ message: `initialize failed: ${initRes.error.message}`,
493
+ code: "TOOL_EXECUTION_FAILED",
494
+ toolName: "mcp_transport_initialize",
495
+ context: { transport: "sse", url: this.url }
496
+ });
446
497
  }
447
498
  try {
448
499
  await this.httpPost("notifications/initialized", {});
@@ -512,16 +563,24 @@ var SSETransport = class extends BaseHTTPTransport {
512
563
  const body2 = await res.text();
513
564
  const cap = MCP_CONSTANTS.REQUEST_LOG_CAP;
514
565
  const snippet = body2.length > cap ? `${body2.slice(0, cap)}\u2026 [${body2.length} bytes total]` : body2;
515
- throw new Error(`HTTP ${res.status}: ${snippet}`);
566
+ throw new ToolError({
567
+ message: `HTTP ${res.status}: ${snippet}`,
568
+ code: "TOOL_EXECUTION_FAILED",
569
+ toolName: method,
570
+ context: { transport: "sse", url: this.url, status: res.status }
571
+ });
516
572
  }
517
573
  let data;
518
574
  try {
519
575
  data = await res.json();
520
576
  } catch (err) {
521
- throw new Error(
522
- `Invalid JSON-RPC response: ${err instanceof Error ? err.message : "parse failed"}`,
523
- { cause: err }
524
- );
577
+ throw new ToolError({
578
+ message: `Invalid JSON-RPC response: ${err instanceof Error ? err.message : "parse failed"}`,
579
+ code: "TOOL_EXECUTION_FAILED",
580
+ toolName: method,
581
+ context: { transport: "sse", url: this.url, phase: "parse-json" },
582
+ cause: err
583
+ });
525
584
  }
526
585
  return assertMatchingJsonRpcResult(data, id, method);
527
586
  } finally {
@@ -530,7 +589,12 @@ var SSETransport = class extends BaseHTTPTransport {
530
589
  }
531
590
  async callTool(name, input) {
532
591
  if (this.state !== "connected") {
533
- throw new Error(`SSE transport not connected (state=${this.state})`);
592
+ throw new ToolError({
593
+ message: `SSE transport not connected (state=${this.state})`,
594
+ code: "TOOL_EXECUTION_FAILED",
595
+ toolName: name,
596
+ context: { transport: "sse", state: this.state }
597
+ });
534
598
  }
535
599
  const res = await this.httpPost("tools/call", { name, arguments: input });
536
600
  if (res.error) {
@@ -562,16 +626,24 @@ var SSETransport = class extends BaseHTTPTransport {
562
626
  this.applyTlsAgent(fetchOpts);
563
627
  const res = await fetch(this.url, fetchOpts);
564
628
  if (!res.ok) {
565
- throw new Error(`HTTP ${res.status}: ${res.statusText}`);
629
+ throw new ToolError({
630
+ message: `HTTP ${res.status}: ${res.statusText}`,
631
+ code: "TOOL_EXECUTION_FAILED",
632
+ toolName: method,
633
+ context: { transport: "sse", url: this.url, status: res.status, statusText: res.statusText }
634
+ });
566
635
  }
567
636
  let data;
568
637
  try {
569
638
  data = await res.json();
570
639
  } catch (err) {
571
- throw new Error(
572
- `Invalid JSON-RPC response: ${err instanceof Error ? err.message : "parse failed"}`,
573
- { cause: err }
574
- );
640
+ throw new ToolError({
641
+ message: `Invalid JSON-RPC response: ${err instanceof Error ? err.message : "parse failed"}`,
642
+ code: "TOOL_EXECUTION_FAILED",
643
+ toolName: method,
644
+ context: { transport: "sse", url: this.url, phase: "parse-json" },
645
+ cause: err
646
+ });
575
647
  }
576
648
  const result = assertMatchingJsonRpcResult(data, id, method);
577
649
  timeoutSignal.dispose();