@wrongstack/mcp 0.275.1 → 0.276.3
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 +117 -45
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
522
|
-
`Invalid JSON-RPC response: ${err instanceof Error ? err.message : "parse failed"}`,
|
|
523
|
-
|
|
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
|
|
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
|
|
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
|
|
572
|
-
`Invalid JSON-RPC response: ${err instanceof Error ? err.message : "parse failed"}`,
|
|
573
|
-
|
|
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();
|