@wrongstack/mcp 1.0.28 → 1.0.32
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-stdio-protocol.d.ts +24 -0
- package/dist/client-stdio.d.ts +32 -0
- package/dist/client.d.ts +5 -10
- package/dist/index.js +830 -427
- package/dist/registry-catalog-operations.d.ts +29 -0
- package/dist/registry-catalog.d.ts +2 -0
- package/dist/registry-slots.d.ts +4 -0
- package/dist/registry.d.ts +4 -1
- package/dist/server-dispatch.d.ts +160 -0
- package/dist/server-http.d.ts +60 -0
- package/dist/server-stdio.d.ts +19 -0
- package/dist/server.d.ts +6 -175
- package/dist/sse-reader.d.ts +2 -0
- package/dist/transport-sse.d.ts +2 -0
- package/dist/transport-streamable.d.ts +2 -0
- package/dist/wrap-tool.d.ts +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import * as dns from "node:dns/promises";
|
|
|
6
6
|
import * as http from "node:http";
|
|
7
7
|
import * as https from "node:https";
|
|
8
8
|
import * as net from "node:net";
|
|
9
|
-
import { isPrivateIPv4, isPrivateIPv6 } from "@wrongstack/core/utils";
|
|
9
|
+
import { isPrivateIPv4, isPrivateIPv6, toErrorMessage } from "@wrongstack/core/utils";
|
|
10
10
|
var MCPOAuthHttpError = class extends Error {
|
|
11
11
|
constructor(message, status) {
|
|
12
12
|
super(message);
|
|
@@ -216,10 +216,10 @@ function requiredString(value, field) {
|
|
|
216
216
|
return value;
|
|
217
217
|
}
|
|
218
218
|
function optionalString(value, field) {
|
|
219
|
-
return value === void 0 ? void 0 : requiredString(value, field);
|
|
219
|
+
return value === void 0 || value === null || value === "" ? void 0 : requiredString(value, field);
|
|
220
220
|
}
|
|
221
221
|
function optionalBoolean(value, field) {
|
|
222
|
-
if (value === void 0) return void 0;
|
|
222
|
+
if (value === void 0 || value === null) return void 0;
|
|
223
223
|
if (typeof value !== "boolean") {
|
|
224
224
|
throw new Error(`MCP authorization field "${field}" must be a boolean`);
|
|
225
225
|
}
|
|
@@ -232,7 +232,7 @@ function boundedStringArray(value, field, maxItems) {
|
|
|
232
232
|
return [...new Set(value.map((entry) => requiredString(entry, field)))];
|
|
233
233
|
}
|
|
234
234
|
function optionalStringArray(value, field, maxItems) {
|
|
235
|
-
return value === void 0 ? [] : boundedStringArray(value, field, maxItems);
|
|
235
|
+
return value === void 0 || value === null ? [] : boundedStringArray(value, field, maxItems);
|
|
236
236
|
}
|
|
237
237
|
function secureOAuthUrl(value, label2) {
|
|
238
238
|
let url;
|
|
@@ -263,7 +263,7 @@ async function discoverFirst(candidates, fetchJson, signal, parse, label2) {
|
|
|
263
263
|
return { url: candidate, value: parse(value) };
|
|
264
264
|
} catch (error) {
|
|
265
265
|
signal?.throwIfAborted();
|
|
266
|
-
failures.push(`${candidate}: ${
|
|
266
|
+
failures.push(`${candidate}: ${toErrorMessage(error)}`);
|
|
267
267
|
}
|
|
268
268
|
}
|
|
269
269
|
throw new Error(`MCP ${label2} discovery failed (${failures.join("; ")})`);
|
|
@@ -634,7 +634,7 @@ function parseTokenResponse(value, resource) {
|
|
|
634
634
|
}
|
|
635
635
|
const expiresIn = response["expires_in"];
|
|
636
636
|
let expiresAt;
|
|
637
|
-
if (expiresIn !== void 0) {
|
|
637
|
+
if (expiresIn !== void 0 && expiresIn !== null) {
|
|
638
638
|
if (typeof expiresIn !== "number" || !Number.isFinite(expiresIn) || expiresIn <= 0 || expiresIn > 31536e3) {
|
|
639
639
|
throw new Error("MCP OAuth expires_in must be between 1 second and 1 year");
|
|
640
640
|
}
|
|
@@ -1154,9 +1154,7 @@ function boundedServerName(value) {
|
|
|
1154
1154
|
}
|
|
1155
1155
|
|
|
1156
1156
|
// src/client.ts
|
|
1157
|
-
import {
|
|
1158
|
-
import { StringDecoder } from "node:string_decoder";
|
|
1159
|
-
import { buildChildEnv as buildChildEnv2, buildWin32CmdShimInvocation, toErrorMessage } from "@wrongstack/core/utils";
|
|
1157
|
+
import { StringDecoder as StringDecoder2 } from "node:string_decoder";
|
|
1160
1158
|
|
|
1161
1159
|
// src/client-protocol-helpers.ts
|
|
1162
1160
|
var MAX_PROTOCOL_INPUT_CHARS = 8192;
|
|
@@ -1479,7 +1477,7 @@ function resolveHttpBearerHeaders(options, env = process.env) {
|
|
|
1479
1477
|
if (!/^[A-Za-z_][A-Za-z0-9_]{0,127}$/.test(envName)) {
|
|
1480
1478
|
throw new Error(`MCP bearerTokenEnv "${envName}" is not a valid environment variable name`);
|
|
1481
1479
|
}
|
|
1482
|
-
const token = env[envName];
|
|
1480
|
+
const token = Object.hasOwn(env, envName) ? env[envName] : void 0;
|
|
1483
1481
|
if (!token) throw new Error(`MCP "${options.name}" requires environment variable ${envName}`);
|
|
1484
1482
|
if (token.length > 16384 || /[\r\n]/.test(token)) {
|
|
1485
1483
|
throw new Error(`MCP "${options.name}" bearer token is oversized or contains newlines`);
|
|
@@ -1528,7 +1526,9 @@ var SSE_READER_MAX_BUFFER = 256 * 1024;
|
|
|
1528
1526
|
var SSE_READER_MAX_DATA_LINES = 1024;
|
|
1529
1527
|
var SSEReader = class {
|
|
1530
1528
|
buffer = "";
|
|
1529
|
+
skipLeadingLF = false;
|
|
1531
1530
|
dataLines = [];
|
|
1531
|
+
dataLength = 0;
|
|
1532
1532
|
eventName = "";
|
|
1533
1533
|
endpointListeners = [];
|
|
1534
1534
|
/**
|
|
@@ -1553,15 +1553,21 @@ var SSEReader = class {
|
|
|
1553
1553
|
}
|
|
1554
1554
|
feed(chunk) {
|
|
1555
1555
|
const carried = this.buffer.length;
|
|
1556
|
+
if (this.skipLeadingLF && chunk.length > 0) {
|
|
1557
|
+
this.skipLeadingLF = false;
|
|
1558
|
+
if (chunk.startsWith("\n")) chunk = chunk.slice(1);
|
|
1559
|
+
}
|
|
1556
1560
|
this.buffer += chunk;
|
|
1557
1561
|
let start = 0;
|
|
1558
|
-
let idx = this.buffer.
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1562
|
+
for (let idx = 0; idx < this.buffer.length; idx++) {
|
|
1563
|
+
const code = this.buffer.charCodeAt(idx);
|
|
1564
|
+
if (code !== 10 && code !== 13) continue;
|
|
1565
|
+
this.processLine(this.buffer.slice(start, idx));
|
|
1566
|
+
if (code === 13) {
|
|
1567
|
+
if (this.buffer.charCodeAt(idx + 1) === 10) idx++;
|
|
1568
|
+
else if (idx + 1 === this.buffer.length) this.skipLeadingLF = true;
|
|
1569
|
+
}
|
|
1563
1570
|
start = idx + 1;
|
|
1564
|
-
idx = this.buffer.indexOf("\n", start);
|
|
1565
1571
|
}
|
|
1566
1572
|
if (start > 0) this.buffer = this.buffer.slice(start);
|
|
1567
1573
|
if (this.buffer.length <= SSE_READER_MAX_BUFFER) return;
|
|
@@ -1585,6 +1591,13 @@ var SSEReader = class {
|
|
|
1585
1591
|
});
|
|
1586
1592
|
}
|
|
1587
1593
|
processLine(line) {
|
|
1594
|
+
if (line.length > SSE_READER_MAX_BUFFER) {
|
|
1595
|
+
throw new ToolError({
|
|
1596
|
+
message: `SSE: completed line exceeds ${SSE_READER_MAX_BUFFER} characters`,
|
|
1597
|
+
code: "TOOL_EXECUTION_FAILED",
|
|
1598
|
+
toolName: "mcp_transport_sse_reader"
|
|
1599
|
+
});
|
|
1600
|
+
}
|
|
1588
1601
|
if (line === "") {
|
|
1589
1602
|
this.flush();
|
|
1590
1603
|
return;
|
|
@@ -1597,6 +1610,14 @@ var SSEReader = class {
|
|
|
1597
1610
|
if (field === "event") {
|
|
1598
1611
|
this.eventName = value;
|
|
1599
1612
|
} else if (field === "data") {
|
|
1613
|
+
const nextLength = this.dataLength + value.length + (this.dataLines.length > 0 ? 1 : 0);
|
|
1614
|
+
if (nextLength > SSE_READER_MAX_BUFFER) {
|
|
1615
|
+
throw new ToolError({
|
|
1616
|
+
message: `SSE: event data exceeds ${SSE_READER_MAX_BUFFER} characters`,
|
|
1617
|
+
code: "TOOL_EXECUTION_FAILED",
|
|
1618
|
+
toolName: "mcp_transport_sse_reader"
|
|
1619
|
+
});
|
|
1620
|
+
}
|
|
1600
1621
|
if (this.dataLines.length >= SSE_READER_MAX_DATA_LINES) {
|
|
1601
1622
|
throw new ToolError({
|
|
1602
1623
|
message: `SSE: exceeded ${SSE_READER_MAX_DATA_LINES} data lines per event \u2014 upstream is not sending blank-line delimiters`,
|
|
@@ -1609,12 +1630,14 @@ var SSEReader = class {
|
|
|
1609
1630
|
}
|
|
1610
1631
|
});
|
|
1611
1632
|
}
|
|
1633
|
+
this.dataLength = nextLength;
|
|
1612
1634
|
this.dataLines.push(value);
|
|
1613
1635
|
}
|
|
1614
1636
|
}
|
|
1615
1637
|
flush() {
|
|
1616
1638
|
const eventName = this.eventName;
|
|
1617
1639
|
this.eventName = "";
|
|
1640
|
+
this.dataLength = 0;
|
|
1618
1641
|
if (this.dataLines.length === 0) {
|
|
1619
1642
|
return;
|
|
1620
1643
|
}
|
|
@@ -1646,8 +1669,10 @@ var SSEReader = class {
|
|
|
1646
1669
|
}
|
|
1647
1670
|
reset() {
|
|
1648
1671
|
this.buffer = "";
|
|
1672
|
+
this.skipLeadingLF = false;
|
|
1649
1673
|
this.dataLines = [];
|
|
1650
1674
|
this.eventName = "";
|
|
1675
|
+
this.dataLength = 0;
|
|
1651
1676
|
this.listeners = [];
|
|
1652
1677
|
this.endpointListeners = [];
|
|
1653
1678
|
}
|
|
@@ -1896,9 +1921,14 @@ function checkValue(field, raw) {
|
|
|
1896
1921
|
function validateElicitationContent(fields, content) {
|
|
1897
1922
|
const out = {};
|
|
1898
1923
|
for (const field of fields) {
|
|
1899
|
-
const raw = content[field.name];
|
|
1924
|
+
const raw = Object.hasOwn(content, field.name) ? content[field.name] : void 0;
|
|
1900
1925
|
if (Array.isArray(raw) && raw.length === 0 && field.kind === "multi-enum" && field.minItems === 0) {
|
|
1901
|
-
out
|
|
1926
|
+
Object.defineProperty(out, field.name, {
|
|
1927
|
+
value: [],
|
|
1928
|
+
enumerable: true,
|
|
1929
|
+
writable: true,
|
|
1930
|
+
configurable: true
|
|
1931
|
+
});
|
|
1902
1932
|
continue;
|
|
1903
1933
|
}
|
|
1904
1934
|
if (raw === void 0 || raw === null || Array.isArray(raw) && raw.length === 0) {
|
|
@@ -1907,7 +1937,12 @@ function validateElicitationContent(fields, content) {
|
|
|
1907
1937
|
}
|
|
1908
1938
|
const checked = checkValue(field, raw);
|
|
1909
1939
|
if (!checked.ok) return checked;
|
|
1910
|
-
out
|
|
1940
|
+
Object.defineProperty(out, field.name, {
|
|
1941
|
+
value: checked.value,
|
|
1942
|
+
enumerable: true,
|
|
1943
|
+
writable: true,
|
|
1944
|
+
configurable: true
|
|
1945
|
+
});
|
|
1911
1946
|
}
|
|
1912
1947
|
return { ok: true, value: out };
|
|
1913
1948
|
}
|
|
@@ -1965,7 +2000,7 @@ var ServerRequestResponder = class {
|
|
|
1965
2000
|
return { jsonrpc: "2.0", id, result: { action: "cancel" } };
|
|
1966
2001
|
} finally {
|
|
1967
2002
|
clearTimeout(deadline);
|
|
1968
|
-
this.pending.delete(id);
|
|
2003
|
+
if (this.pending.get(id) === controller) this.pending.delete(id);
|
|
1969
2004
|
}
|
|
1970
2005
|
}
|
|
1971
2006
|
/**
|
|
@@ -2008,9 +2043,20 @@ var ServerRequestResponder = class {
|
|
|
2008
2043
|
import { ToolError as ToolError2 } from "@wrongstack/core/types";
|
|
2009
2044
|
var MAX_MCP_HTTP_BODY_BYTES = 16 * 1024 * 1024;
|
|
2010
2045
|
async function readBodyCapped(res, maxBytes = MAX_MCP_HTTP_BODY_BYTES) {
|
|
2046
|
+
const safeCap = typeof maxBytes === "number" && Number.isFinite(maxBytes) && maxBytes >= 0 ? maxBytes : MAX_MCP_HTTP_BODY_BYTES;
|
|
2011
2047
|
const body = res.body;
|
|
2012
2048
|
if (!body || typeof body.getReader !== "function") {
|
|
2013
|
-
|
|
2049
|
+
const text = await res.text();
|
|
2050
|
+
const received = Buffer.byteLength(text, "utf8");
|
|
2051
|
+
if (received > safeCap) {
|
|
2052
|
+
throw new ToolError2({
|
|
2053
|
+
message: `MCP response body exceeded ${safeCap} bytes \u2014 refusing to buffer`,
|
|
2054
|
+
code: "TOOL_EXECUTION_FAILED",
|
|
2055
|
+
toolName: "mcp_transport",
|
|
2056
|
+
context: { maxBytes: safeCap, received }
|
|
2057
|
+
});
|
|
2058
|
+
}
|
|
2059
|
+
return text;
|
|
2014
2060
|
}
|
|
2015
2061
|
const reader = body.getReader();
|
|
2016
2062
|
const chunks = [];
|
|
@@ -2021,13 +2067,13 @@ async function readBodyCapped(res, maxBytes = MAX_MCP_HTTP_BODY_BYTES) {
|
|
|
2021
2067
|
if (done) break;
|
|
2022
2068
|
if (!value) continue;
|
|
2023
2069
|
total += value.byteLength;
|
|
2024
|
-
if (total >
|
|
2070
|
+
if (total > safeCap) {
|
|
2025
2071
|
await reader.cancel().catch(() => void 0);
|
|
2026
2072
|
throw new ToolError2({
|
|
2027
|
-
message: `MCP response body exceeded ${
|
|
2073
|
+
message: `MCP response body exceeded ${safeCap} bytes \u2014 refusing to buffer`,
|
|
2028
2074
|
code: "TOOL_EXECUTION_FAILED",
|
|
2029
2075
|
toolName: "mcp_transport",
|
|
2030
|
-
context: { maxBytes, received: total }
|
|
2076
|
+
context: { maxBytes: safeCap, received: total }
|
|
2031
2077
|
});
|
|
2032
2078
|
}
|
|
2033
2079
|
chunks.push(value);
|
|
@@ -2232,15 +2278,16 @@ function createTimeoutSignal(parent, timeoutMs, hold) {
|
|
|
2232
2278
|
} else {
|
|
2233
2279
|
parent?.addEventListener("abort", onAbort, { once: true });
|
|
2234
2280
|
}
|
|
2281
|
+
const safeTimeout = typeof timeoutMs === "number" && Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : 6e4;
|
|
2235
2282
|
const onTimeout = () => {
|
|
2236
2283
|
if (hold?.()) {
|
|
2237
|
-
timer = setTimeout(onTimeout,
|
|
2284
|
+
timer = setTimeout(onTimeout, safeTimeout);
|
|
2238
2285
|
timer.unref?.();
|
|
2239
2286
|
return;
|
|
2240
2287
|
}
|
|
2241
|
-
ctrl.abort(new Error(`MCP HTTP request timed out after ${
|
|
2288
|
+
ctrl.abort(new Error(`MCP HTTP request timed out after ${safeTimeout}ms`));
|
|
2242
2289
|
};
|
|
2243
|
-
let timer = setTimeout(onTimeout,
|
|
2290
|
+
let timer = setTimeout(onTimeout, safeTimeout);
|
|
2244
2291
|
timer.unref?.();
|
|
2245
2292
|
return {
|
|
2246
2293
|
signal: ctrl.signal,
|
|
@@ -2283,8 +2330,8 @@ var BaseHTTPTransport = class {
|
|
|
2283
2330
|
this.headers = { ...opts.headers };
|
|
2284
2331
|
this.authorizationProvider = opts.authorizationProvider;
|
|
2285
2332
|
this.authorizationResource = canonicalMcpResource(opts.url);
|
|
2286
|
-
this.timeout = opts.startupTimeoutMs
|
|
2287
|
-
this.requestTimeout = opts.requestTimeoutMs
|
|
2333
|
+
this.timeout = typeof opts.startupTimeoutMs === "number" && Number.isFinite(opts.startupTimeoutMs) && opts.startupTimeoutMs > 0 ? opts.startupTimeoutMs : 1e4;
|
|
2334
|
+
this.requestTimeout = typeof opts.requestTimeoutMs === "number" && Number.isFinite(opts.requestTimeoutMs) && opts.requestTimeoutMs > 0 ? opts.requestTimeoutMs : 6e4;
|
|
2288
2335
|
if (opts.tls) {
|
|
2289
2336
|
if (opts.tls.rejectUnauthorized === false) {
|
|
2290
2337
|
const where = urlOriginForLog(this.url);
|
|
@@ -2566,7 +2613,7 @@ function extractJsonRpcEnvelopes(text) {
|
|
|
2566
2613
|
} catch {
|
|
2567
2614
|
}
|
|
2568
2615
|
};
|
|
2569
|
-
for (const raw of text.split(
|
|
2616
|
+
for (const raw of text.split(/\r\n|\r|\n/)) {
|
|
2570
2617
|
const line = raw.replace(/\r$/, "");
|
|
2571
2618
|
if (line === "") {
|
|
2572
2619
|
flush();
|
|
@@ -2771,6 +2818,8 @@ var SSETransport = class extends BaseHTTPTransport {
|
|
|
2771
2818
|
_nextId = 1;
|
|
2772
2819
|
readerDone = false;
|
|
2773
2820
|
closed = false;
|
|
2821
|
+
toolCatalogVersion = 0;
|
|
2822
|
+
toolCatalogRevision = 0;
|
|
2774
2823
|
readLoopAbort;
|
|
2775
2824
|
reader;
|
|
2776
2825
|
/** POST target announced by the server's `endpoint` event. */
|
|
@@ -2789,10 +2838,12 @@ var SSETransport = class extends BaseHTTPTransport {
|
|
|
2789
2838
|
}
|
|
2790
2839
|
/** Refresh tool list when server sends notifications/tools/list_changed. */
|
|
2791
2840
|
async handleToolsListChanged() {
|
|
2841
|
+
const version = ++this.toolCatalogVersion;
|
|
2792
2842
|
try {
|
|
2793
2843
|
const tools = await listAllTools((params) => this.httpPost("tools/list", params));
|
|
2794
|
-
if (!tools) return;
|
|
2844
|
+
if (!tools || version !== this.toolCatalogVersion) return;
|
|
2795
2845
|
this.tools.splice(0, this.tools.length, ...tools);
|
|
2846
|
+
this.toolCatalogRevision++;
|
|
2796
2847
|
for (const cb of this.toolsChangedListeners) {
|
|
2797
2848
|
try {
|
|
2798
2849
|
cb([...this.tools]);
|
|
@@ -2803,14 +2854,21 @@ var SSETransport = class extends BaseHTTPTransport {
|
|
|
2803
2854
|
}
|
|
2804
2855
|
}
|
|
2805
2856
|
async connect() {
|
|
2857
|
+
this.toolCatalogVersion++;
|
|
2806
2858
|
this.readerDone = false;
|
|
2807
2859
|
this.closed = false;
|
|
2808
2860
|
this.endpointUrl = void 0;
|
|
2809
2861
|
this.state = "connecting";
|
|
2810
2862
|
this.serverMetadata = void 0;
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
const
|
|
2863
|
+
const controller = new AbortController();
|
|
2864
|
+
this.abortController = controller;
|
|
2865
|
+
const signal = controller.signal;
|
|
2866
|
+
const assertCurrent = () => {
|
|
2867
|
+
if (this.abortController !== controller || signal.aborted || this.closed) {
|
|
2868
|
+
throw makeAbortError("connect");
|
|
2869
|
+
}
|
|
2870
|
+
};
|
|
2871
|
+
const startupTimer = setTimeout(() => controller.abort(), this.timeout);
|
|
2814
2872
|
try {
|
|
2815
2873
|
const sseUrl = this.buildSSEUrl();
|
|
2816
2874
|
const fetchOpts = {
|
|
@@ -2819,6 +2877,12 @@ var SSETransport = class extends BaseHTTPTransport {
|
|
|
2819
2877
|
};
|
|
2820
2878
|
this.applyTlsAgent(fetchOpts);
|
|
2821
2879
|
const response = await this.fetchWithAuthorization(sseUrl, fetchOpts, signal);
|
|
2880
|
+
try {
|
|
2881
|
+
assertCurrent();
|
|
2882
|
+
} catch (error) {
|
|
2883
|
+
await response.body?.cancel().catch(() => void 0);
|
|
2884
|
+
throw error;
|
|
2885
|
+
}
|
|
2822
2886
|
if (!response.ok) {
|
|
2823
2887
|
throw new ToolError4({
|
|
2824
2888
|
message: `SSE connect HTTP ${response.status}: ${response.statusText}`,
|
|
@@ -2837,15 +2901,18 @@ var SSETransport = class extends BaseHTTPTransport {
|
|
|
2837
2901
|
}
|
|
2838
2902
|
const textDecoder = new TextDecoder();
|
|
2839
2903
|
const sseReader = new SSEReader();
|
|
2840
|
-
|
|
2904
|
+
const readLoopAbort = new AbortController();
|
|
2905
|
+
this.readLoopAbort = readLoopAbort;
|
|
2841
2906
|
const streamReady = new Promise((resolve) => {
|
|
2842
2907
|
this.streamSignal = resolve;
|
|
2843
2908
|
});
|
|
2844
2909
|
sseReader.onEndpoint((endpoint) => {
|
|
2910
|
+
if (this.abortController !== controller || signal.aborted || this.closed) return;
|
|
2845
2911
|
this.acceptEndpoint(endpoint);
|
|
2846
2912
|
this.streamSignal?.();
|
|
2847
2913
|
});
|
|
2848
2914
|
sseReader.onMessage((msg) => {
|
|
2915
|
+
if (this.abortController !== controller || signal.aborted || this.closed) return;
|
|
2849
2916
|
this.streamSignal?.();
|
|
2850
2917
|
const id = msg.id;
|
|
2851
2918
|
if (msg.method && (typeof id === "number" || typeof id === "string")) {
|
|
@@ -2884,8 +2951,9 @@ var SSETransport = class extends BaseHTTPTransport {
|
|
|
2884
2951
|
cancel: () => reader.cancel(),
|
|
2885
2952
|
releaseLock: () => reader.releaseLock()
|
|
2886
2953
|
};
|
|
2887
|
-
void this.readSSEBody(reader, textDecoder, sseReader);
|
|
2954
|
+
void this.readSSEBody(reader, textDecoder, sseReader, controller, readLoopAbort.signal);
|
|
2888
2955
|
await this.waitForStream(streamReady, signal);
|
|
2956
|
+
assertCurrent();
|
|
2889
2957
|
const initRes = await this.httpPost("initialize", {
|
|
2890
2958
|
protocolVersion: MCP_CONSTANTS.PROTOCOL_VERSION,
|
|
2891
2959
|
// Client capabilities: elicitation when the host can ask its user.
|
|
@@ -2893,6 +2961,7 @@ var SSETransport = class extends BaseHTTPTransport {
|
|
|
2893
2961
|
capabilities: this.serverRequests.capabilities(),
|
|
2894
2962
|
clientInfo: MCP_CONSTANTS.CLIENT_INFO
|
|
2895
2963
|
});
|
|
2964
|
+
assertCurrent();
|
|
2896
2965
|
if (initRes.error) {
|
|
2897
2966
|
throw new ToolError4({
|
|
2898
2967
|
message: `initialize failed: ${initRes.error.message}`,
|
|
@@ -2907,17 +2976,24 @@ var SSETransport = class extends BaseHTTPTransport {
|
|
|
2907
2976
|
await this.httpPost("notifications/initialized", {});
|
|
2908
2977
|
} catch {
|
|
2909
2978
|
}
|
|
2979
|
+
assertCurrent();
|
|
2980
|
+
const revision = this.toolCatalogRevision;
|
|
2910
2981
|
const tools = await listAllTools((params) => this.httpPost("tools/list", params));
|
|
2911
|
-
|
|
2982
|
+
assertCurrent();
|
|
2983
|
+
if (revision === this.toolCatalogRevision) {
|
|
2984
|
+
this.tools.splice(0, this.tools.length, ...tools ?? []);
|
|
2985
|
+
}
|
|
2912
2986
|
this.state = "connected";
|
|
2913
2987
|
clearTimeout(startupTimer);
|
|
2914
2988
|
} catch (err) {
|
|
2915
2989
|
clearTimeout(startupTimer);
|
|
2916
|
-
this.
|
|
2917
|
-
|
|
2990
|
+
if (this.abortController === controller && !this.closed) {
|
|
2991
|
+
this.state = "failed";
|
|
2992
|
+
}
|
|
2993
|
+
controller.abort();
|
|
2918
2994
|
throw err;
|
|
2919
2995
|
} finally {
|
|
2920
|
-
this.streamSignal = void 0;
|
|
2996
|
+
if (this.abortController === controller) this.streamSignal = void 0;
|
|
2921
2997
|
}
|
|
2922
2998
|
}
|
|
2923
2999
|
/** Resolve once the stream announced its endpoint, emitted anything, ended, or the cap passed. */
|
|
@@ -2955,9 +3031,9 @@ var SSETransport = class extends BaseHTTPTransport {
|
|
|
2955
3031
|
} catch {
|
|
2956
3032
|
}
|
|
2957
3033
|
}
|
|
2958
|
-
async readSSEBody(reader, decoder, sseReader) {
|
|
3034
|
+
async readSSEBody(reader, decoder, sseReader, controller, signal) {
|
|
2959
3035
|
try {
|
|
2960
|
-
while (!this.readerDone) {
|
|
3036
|
+
while (!this.readerDone && !signal?.aborted && (!controller || this.abortController === controller) && !this.closed) {
|
|
2961
3037
|
const { done, value } = await reader.read();
|
|
2962
3038
|
if (done) break;
|
|
2963
3039
|
const chunk = decoder.decode(value, { stream: true });
|
|
@@ -2965,11 +3041,13 @@ var SSETransport = class extends BaseHTTPTransport {
|
|
|
2965
3041
|
}
|
|
2966
3042
|
} catch {
|
|
2967
3043
|
} finally {
|
|
2968
|
-
this.
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
this.state
|
|
2972
|
-
|
|
3044
|
+
if (!controller || this.abortController === controller) {
|
|
3045
|
+
this.streamSignal?.();
|
|
3046
|
+
this.rejectStreamPending("SSE stream closed");
|
|
3047
|
+
if (!this.readerDone && !signal?.aborted && !this.closed && this.state !== "disconnected" && this.state !== "failed") {
|
|
3048
|
+
this.state = "disconnected";
|
|
3049
|
+
this.notifyDisconnect();
|
|
3050
|
+
}
|
|
2973
3051
|
}
|
|
2974
3052
|
}
|
|
2975
3053
|
}
|
|
@@ -3115,6 +3193,7 @@ var SSETransport = class extends BaseHTTPTransport {
|
|
|
3115
3193
|
return { jsonrpc: "2.0", id: result.id, result: result.result, error: result.error };
|
|
3116
3194
|
}
|
|
3117
3195
|
async close() {
|
|
3196
|
+
this.toolCatalogVersion++;
|
|
3118
3197
|
this.releasePinnedDispatcher();
|
|
3119
3198
|
const alreadyClosed = this.closed;
|
|
3120
3199
|
this.closed = true;
|
|
@@ -3155,6 +3234,8 @@ function invalidResponse(method, url, err) {
|
|
|
3155
3234
|
var StreamableHTTPTransport = class _StreamableHTTPTransport extends BaseHTTPTransport {
|
|
3156
3235
|
_nextId = 1;
|
|
3157
3236
|
closed = false;
|
|
3237
|
+
toolCatalogVersion = 0;
|
|
3238
|
+
toolCatalogRevision = 0;
|
|
3158
3239
|
sessionId;
|
|
3159
3240
|
constructor(opts) {
|
|
3160
3241
|
super(opts, "StreamableHTTP");
|
|
@@ -3250,10 +3331,12 @@ var StreamableHTTPTransport = class _StreamableHTTPTransport extends BaseHTTPTra
|
|
|
3250
3331
|
}
|
|
3251
3332
|
}
|
|
3252
3333
|
async refreshTools() {
|
|
3334
|
+
const version = ++this.toolCatalogVersion;
|
|
3253
3335
|
try {
|
|
3254
3336
|
const tools = await listAllTools((params) => this.postRaw("tools/list", params));
|
|
3255
|
-
if (!tools) return;
|
|
3337
|
+
if (!tools || version !== this.toolCatalogVersion) return;
|
|
3256
3338
|
this.tools.splice(0, this.tools.length, ...tools);
|
|
3339
|
+
this.toolCatalogRevision++;
|
|
3257
3340
|
for (const listener of this.toolsChangedListeners) {
|
|
3258
3341
|
try {
|
|
3259
3342
|
listener([...tools]);
|
|
@@ -3264,12 +3347,19 @@ var StreamableHTTPTransport = class _StreamableHTTPTransport extends BaseHTTPTra
|
|
|
3264
3347
|
}
|
|
3265
3348
|
}
|
|
3266
3349
|
async connect() {
|
|
3350
|
+
this.toolCatalogVersion++;
|
|
3267
3351
|
this.closed = false;
|
|
3268
3352
|
this.state = "connecting";
|
|
3269
3353
|
this.serverMetadata = void 0;
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
const
|
|
3354
|
+
const controller = new AbortController();
|
|
3355
|
+
this.abortController = controller;
|
|
3356
|
+
const signal = controller.signal;
|
|
3357
|
+
const assertCurrent = () => {
|
|
3358
|
+
if (this.abortController !== controller || signal.aborted || this.closed) {
|
|
3359
|
+
throw makeAbortError("connect");
|
|
3360
|
+
}
|
|
3361
|
+
};
|
|
3362
|
+
const startupTimer = setTimeout(() => controller.abort(), this.timeout);
|
|
3273
3363
|
try {
|
|
3274
3364
|
const initializeId = this.genId();
|
|
3275
3365
|
const initFetchOpts = {
|
|
@@ -3295,6 +3385,7 @@ var StreamableHTTPTransport = class _StreamableHTTPTransport extends BaseHTTPTra
|
|
|
3295
3385
|
};
|
|
3296
3386
|
this.applyTlsAgent(initFetchOpts);
|
|
3297
3387
|
const initRes = await this.fetchWithAuthorization(this.url, initFetchOpts, signal);
|
|
3388
|
+
assertCurrent();
|
|
3298
3389
|
if (!initRes.ok) {
|
|
3299
3390
|
throw new Error(`initialize HTTP ${initRes.status}: ${initRes.statusText}`);
|
|
3300
3391
|
}
|
|
@@ -3306,6 +3397,7 @@ var StreamableHTTPTransport = class _StreamableHTTPTransport extends BaseHTTPTra
|
|
|
3306
3397
|
} else {
|
|
3307
3398
|
data = extractJsonRpcResults(await readBodyCapped(initRes))[0];
|
|
3308
3399
|
}
|
|
3400
|
+
assertCurrent();
|
|
3309
3401
|
if (!data) {
|
|
3310
3402
|
throw new Error("Could not parse initialize response");
|
|
3311
3403
|
}
|
|
@@ -3317,14 +3409,21 @@ var StreamableHTTPTransport = class _StreamableHTTPTransport extends BaseHTTPTra
|
|
|
3317
3409
|
this.protocolVersion = this.serverMetadata.protocolVersion;
|
|
3318
3410
|
this.sessionId = initRes.headers.get("mcp-session-id") ?? void 0;
|
|
3319
3411
|
await this.postRaw("notifications/initialized", {});
|
|
3412
|
+
assertCurrent();
|
|
3413
|
+
const revision = this.toolCatalogRevision;
|
|
3320
3414
|
const tools = await listAllTools((params) => this.postRaw("tools/list", params));
|
|
3321
|
-
|
|
3415
|
+
assertCurrent();
|
|
3416
|
+
if (revision === this.toolCatalogRevision) {
|
|
3417
|
+
this.tools.splice(0, this.tools.length, ...tools ?? []);
|
|
3418
|
+
}
|
|
3322
3419
|
this.state = "connected";
|
|
3323
3420
|
clearTimeout(startupTimer);
|
|
3324
3421
|
} catch (err) {
|
|
3325
3422
|
clearTimeout(startupTimer);
|
|
3326
|
-
this.
|
|
3327
|
-
|
|
3423
|
+
if (this.abortController === controller && !this.closed) {
|
|
3424
|
+
this.state = "failed";
|
|
3425
|
+
}
|
|
3426
|
+
controller.abort();
|
|
3328
3427
|
throw err;
|
|
3329
3428
|
}
|
|
3330
3429
|
}
|
|
@@ -3441,6 +3540,7 @@ var StreamableHTTPTransport = class _StreamableHTTPTransport extends BaseHTTPTra
|
|
|
3441
3540
|
return toToolCallResult(res);
|
|
3442
3541
|
}
|
|
3443
3542
|
async close() {
|
|
3543
|
+
this.toolCatalogVersion++;
|
|
3444
3544
|
this.releasePinnedDispatcher();
|
|
3445
3545
|
const alreadyClosed = this.closed;
|
|
3446
3546
|
this.closed = true;
|
|
@@ -3465,9 +3565,17 @@ var StreamableHTTPTransport = class _StreamableHTTPTransport extends BaseHTTPTra
|
|
|
3465
3565
|
}
|
|
3466
3566
|
};
|
|
3467
3567
|
function completeEventsEnd(text) {
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3568
|
+
let start = 0;
|
|
3569
|
+
let end = 0;
|
|
3570
|
+
for (let idx = 0; idx < text.length; idx++) {
|
|
3571
|
+
const code = text.charCodeAt(idx);
|
|
3572
|
+
if (code !== 10 && code !== 13) continue;
|
|
3573
|
+
const empty = idx === start;
|
|
3574
|
+
if (code === 13 && text.charCodeAt(idx + 1) === 10) idx++;
|
|
3575
|
+
start = idx + 1;
|
|
3576
|
+
if (empty) end = start;
|
|
3577
|
+
}
|
|
3578
|
+
return end;
|
|
3471
3579
|
}
|
|
3472
3580
|
|
|
3473
3581
|
// src/client-http-connection.ts
|
|
@@ -3578,8 +3686,248 @@ async function connectStreamableHTTP(host) {
|
|
|
3578
3686
|
host.state = "connected";
|
|
3579
3687
|
}
|
|
3580
3688
|
|
|
3689
|
+
// src/client-stdio.ts
|
|
3690
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
3691
|
+
import { StringDecoder } from "node:string_decoder";
|
|
3692
|
+
import { buildChildEnv as buildChildEnv2, buildWin32CmdShimInvocation, toErrorMessage as toErrorMessage2 } from "@wrongstack/core/utils";
|
|
3693
|
+
async function connectStdio(host) {
|
|
3694
|
+
if (!host.opts.command) {
|
|
3695
|
+
host.state = "failed";
|
|
3696
|
+
throw new Error('MCP stdio transport requires "command"');
|
|
3697
|
+
}
|
|
3698
|
+
host.rxBuffer = "";
|
|
3699
|
+
host.rxBufferBytes = 0;
|
|
3700
|
+
host.rxDecoder = new StringDecoder("utf8");
|
|
3701
|
+
const extraEnv = { ...host.opts.env };
|
|
3702
|
+
if (host.opts.passthroughEnv) {
|
|
3703
|
+
for (const name of host.opts.passthroughEnv) {
|
|
3704
|
+
const val = process.env[name];
|
|
3705
|
+
if (val !== void 0) {
|
|
3706
|
+
extraEnv[name] = val;
|
|
3707
|
+
}
|
|
3708
|
+
}
|
|
3709
|
+
}
|
|
3710
|
+
const isWin = process.platform === "win32";
|
|
3711
|
+
const rawArgs = host.opts.args ?? [];
|
|
3712
|
+
const spawnEnv = buildChildEnv2({ extra: extraEnv });
|
|
3713
|
+
const stdio = ["pipe", "pipe", "pipe"];
|
|
3714
|
+
const child = isWin ? (() => {
|
|
3715
|
+
const shim = buildWin32CmdShimInvocation(host.opts.command, rawArgs);
|
|
3716
|
+
return spawn2(shim.command, shim.args, {
|
|
3717
|
+
env: spawnEnv,
|
|
3718
|
+
stdio,
|
|
3719
|
+
...host.opts.cwd ? { cwd: host.opts.cwd } : {},
|
|
3720
|
+
windowsVerbatimArguments: shim.windowsVerbatimArguments,
|
|
3721
|
+
// Without this every MCP server spawned from a console-less host
|
|
3722
|
+
// (WebUI server, scheduled runs) opens a visible console window.
|
|
3723
|
+
windowsHide: true
|
|
3724
|
+
});
|
|
3725
|
+
})() : spawn2(host.opts.command, rawArgs, {
|
|
3726
|
+
env: spawnEnv,
|
|
3727
|
+
stdio,
|
|
3728
|
+
windowsHide: true,
|
|
3729
|
+
...host.opts.cwd ? { cwd: host.opts.cwd } : {}
|
|
3730
|
+
});
|
|
3731
|
+
host.child = child;
|
|
3732
|
+
child.stdout?.on("data", (chunk) => host.onData(host.rxDecoder.write(chunk)));
|
|
3733
|
+
child.stdout?.on("end", () => {
|
|
3734
|
+
const tail = host.rxDecoder.end();
|
|
3735
|
+
if (tail) host.onData(tail);
|
|
3736
|
+
if (host.rxBuffer.trim()) {
|
|
3737
|
+
const line = host.rxBuffer.trim();
|
|
3738
|
+
host.rxBuffer = "";
|
|
3739
|
+
host.onLine(line);
|
|
3740
|
+
}
|
|
3741
|
+
});
|
|
3742
|
+
child.stderr?.on("data", () => {
|
|
3743
|
+
});
|
|
3744
|
+
child.stdin?.on("error", (err) => {
|
|
3745
|
+
host.failPending(`MCP "${host.opts.name}" stdin error: ${toErrorMessage2(err)}`);
|
|
3746
|
+
});
|
|
3747
|
+
child.on("exit", (code, signal) => {
|
|
3748
|
+
host.state = "disconnected";
|
|
3749
|
+
host.failPending(
|
|
3750
|
+
`MCP "${host.opts.name}" child exited (code=${code ?? "null"} signal=${signal ?? "null"})`
|
|
3751
|
+
);
|
|
3752
|
+
for (const listener of host.exitListeners) {
|
|
3753
|
+
try {
|
|
3754
|
+
listener(host.opts.name, code, signal);
|
|
3755
|
+
} catch {
|
|
3756
|
+
}
|
|
3757
|
+
}
|
|
3758
|
+
});
|
|
3759
|
+
child.on("error", (err) => {
|
|
3760
|
+
host.state = "failed";
|
|
3761
|
+
host.failPending(`MCP "${host.opts.name}" child error: ${toErrorMessage2(err)}`);
|
|
3762
|
+
});
|
|
3763
|
+
const initialize = await host.request(
|
|
3764
|
+
"initialize",
|
|
3765
|
+
{
|
|
3766
|
+
protocolVersion: MCP_CONSTANTS.PROTOCOL_VERSION,
|
|
3767
|
+
// Client capabilities: elicitation when the host can ask its user.
|
|
3768
|
+
// `tools` is a SERVER capability and never belonged here.
|
|
3769
|
+
capabilities: host.serverRequests.capabilities(),
|
|
3770
|
+
clientInfo: MCP_CONSTANTS.CLIENT_INFO
|
|
3771
|
+
},
|
|
3772
|
+
typeof host.opts.startupTimeoutMs === "number" && Number.isFinite(host.opts.startupTimeoutMs) && host.opts.startupTimeoutMs > 0 ? host.opts.startupTimeoutMs : 1e4
|
|
3773
|
+
);
|
|
3774
|
+
if (initialize.error) {
|
|
3775
|
+
host.state = "failed";
|
|
3776
|
+
throw new Error(`MCP initialize failed: ${initialize.error.message}`);
|
|
3777
|
+
}
|
|
3778
|
+
try {
|
|
3779
|
+
host._serverMetadata = parseServerMetadata(initialize.result);
|
|
3780
|
+
} catch (err) {
|
|
3781
|
+
host.state = "failed";
|
|
3782
|
+
throw new Error(`MCP initialize returned malformed server metadata: ${toErrorMessage2(err)}`);
|
|
3783
|
+
}
|
|
3784
|
+
try {
|
|
3785
|
+
await host.notify("notifications/initialized", {});
|
|
3786
|
+
} catch (err) {
|
|
3787
|
+
console.warn(
|
|
3788
|
+
'[MCP] notify("notifications/initialized") failed for "' + host.opts.name + '": ' + toErrorMessage2(err)
|
|
3789
|
+
);
|
|
3790
|
+
}
|
|
3791
|
+
const revision = host.toolCatalogRevision;
|
|
3792
|
+
const tools = await listAllTools((params) => host.request("tools/list", params)) ?? [];
|
|
3793
|
+
if (revision === host.toolCatalogRevision) host._tools = tools;
|
|
3794
|
+
host._toolsCache = host._tools;
|
|
3795
|
+
host.state = "connected";
|
|
3796
|
+
}
|
|
3797
|
+
async function notifyStdio(host, method, params) {
|
|
3798
|
+
if (host._drainPending) {
|
|
3799
|
+
host._lastNotifySkipped = true;
|
|
3800
|
+
console.warn(
|
|
3801
|
+
JSON.stringify({
|
|
3802
|
+
level: "warn",
|
|
3803
|
+
event: "mcp.notify_skipped_backpressure",
|
|
3804
|
+
server: host.opts.name,
|
|
3805
|
+
method,
|
|
3806
|
+
message: "stdin buffer backpressure (already waiting for drain)",
|
|
3807
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3808
|
+
})
|
|
3809
|
+
);
|
|
3810
|
+
return;
|
|
3811
|
+
}
|
|
3812
|
+
const stdin = host.child?.stdin;
|
|
3813
|
+
if (!stdin || stdin.destroyed === true || stdin.writable === false) {
|
|
3814
|
+
return;
|
|
3815
|
+
}
|
|
3816
|
+
const req = { jsonrpc: "2.0", method, params };
|
|
3817
|
+
const encoded = JSON.stringify(req) + "\n";
|
|
3818
|
+
try {
|
|
3819
|
+
const ok = stdin.write(encoded);
|
|
3820
|
+
if (!ok) {
|
|
3821
|
+
host._drainPending = true;
|
|
3822
|
+
await new Promise((resolve, reject) => {
|
|
3823
|
+
const timeout = setTimeout(() => {
|
|
3824
|
+
stdin.removeListener?.("drain", onDrain);
|
|
3825
|
+
stdin.removeListener?.("error", onError);
|
|
3826
|
+
host._drainPending = false;
|
|
3827
|
+
reject(new Error(`MCP notify("${method}") drain timeout`));
|
|
3828
|
+
}, 500);
|
|
3829
|
+
const onDrain = () => {
|
|
3830
|
+
clearTimeout(timeout);
|
|
3831
|
+
stdin.removeListener?.("drain", onDrain);
|
|
3832
|
+
stdin.removeListener?.("error", onError);
|
|
3833
|
+
host._drainPending = false;
|
|
3834
|
+
resolve();
|
|
3835
|
+
};
|
|
3836
|
+
const onError = (err) => {
|
|
3837
|
+
clearTimeout(timeout);
|
|
3838
|
+
stdin.removeListener?.("drain", onDrain);
|
|
3839
|
+
stdin.removeListener?.("error", onError);
|
|
3840
|
+
host._drainPending = false;
|
|
3841
|
+
reject(err);
|
|
3842
|
+
};
|
|
3843
|
+
stdin.once?.("drain", onDrain);
|
|
3844
|
+
stdin.once?.("error", onError);
|
|
3845
|
+
});
|
|
3846
|
+
}
|
|
3847
|
+
} catch (err) {
|
|
3848
|
+
throw new Error(`[MCP] notify("${method}") failed: ${toErrorMessage2(err)}`);
|
|
3849
|
+
}
|
|
3850
|
+
}
|
|
3851
|
+
|
|
3852
|
+
// src/client-stdio-protocol.ts
|
|
3853
|
+
var MAX_RX_BUFFER_BYTES = 16 * 1024 * 1024;
|
|
3854
|
+
function receiveStdioData(host, s) {
|
|
3855
|
+
host.rxBufferBytes += Buffer.byteLength(s, "utf8");
|
|
3856
|
+
if (host.rxBufferBytes > MAX_RX_BUFFER_BYTES) {
|
|
3857
|
+
const truncated = host.rxBufferBytes;
|
|
3858
|
+
host.rxParts = [];
|
|
3859
|
+
host.rxBufferBytes = 0;
|
|
3860
|
+
host.failPending(
|
|
3861
|
+
`MCP "${host.opts.name}" rx buffer overflow (${truncated} bytes without a newline) \u2014 closing connection`
|
|
3862
|
+
);
|
|
3863
|
+
void host.close();
|
|
3864
|
+
return;
|
|
3865
|
+
}
|
|
3866
|
+
let start = 0;
|
|
3867
|
+
let idx = s.indexOf("\n");
|
|
3868
|
+
if (idx === -1) {
|
|
3869
|
+
host.rxParts.push(s);
|
|
3870
|
+
return;
|
|
3871
|
+
}
|
|
3872
|
+
while (idx !== -1) {
|
|
3873
|
+
const head = host.rxParts.length > 0 ? host.rxParts.join("") : "";
|
|
3874
|
+
host.rxParts = [];
|
|
3875
|
+
const line = (head + s.slice(start, idx)).trim();
|
|
3876
|
+
start = idx + 1;
|
|
3877
|
+
if (line) host.onLine(line);
|
|
3878
|
+
idx = s.indexOf("\n", start);
|
|
3879
|
+
}
|
|
3880
|
+
const tail = s.slice(start);
|
|
3881
|
+
host.rxBufferBytes = Buffer.byteLength(tail, "utf8");
|
|
3882
|
+
if (tail) host.rxParts.push(tail);
|
|
3883
|
+
}
|
|
3884
|
+
function receiveStdioLine(host, line) {
|
|
3885
|
+
let msg;
|
|
3886
|
+
try {
|
|
3887
|
+
msg = JSON.parse(line);
|
|
3888
|
+
} catch {
|
|
3889
|
+
return;
|
|
3890
|
+
}
|
|
3891
|
+
if (typeof msg !== "object" || msg === null) return;
|
|
3892
|
+
const envelope = msg;
|
|
3893
|
+
if (envelope["jsonrpc"] !== "2.0") return;
|
|
3894
|
+
if (typeof envelope["method"] === "string") {
|
|
3895
|
+
const id = envelope["id"];
|
|
3896
|
+
if (typeof id === "number" || typeof id === "string") {
|
|
3897
|
+
void host.handleServerRequest({
|
|
3898
|
+
jsonrpc: "2.0",
|
|
3899
|
+
id,
|
|
3900
|
+
method: envelope["method"],
|
|
3901
|
+
params: envelope["params"]
|
|
3902
|
+
});
|
|
3903
|
+
return;
|
|
3904
|
+
}
|
|
3905
|
+
if (Object.hasOwn(envelope, "id")) return;
|
|
3906
|
+
if (envelope["method"] === "notifications/cancelled") {
|
|
3907
|
+
host.serverRequests.cancel(envelope["params"]);
|
|
3908
|
+
} else if (envelope["method"] === "notifications/tools/list_changed") {
|
|
3909
|
+
void host.handleToolsListChanged();
|
|
3910
|
+
} else if (envelope["method"] === "notifications/resources/list_changed") {
|
|
3911
|
+
host.emitCapabilityChanged("resources");
|
|
3912
|
+
} else if (envelope["method"] === "notifications/prompts/list_changed") {
|
|
3913
|
+
host.emitCapabilityChanged("prompts");
|
|
3914
|
+
} else if (envelope["method"] === "notifications/resources/updated") {
|
|
3915
|
+
const uri = resourceUpdatedUri(envelope["params"]);
|
|
3916
|
+
if (uri) host.emitResourceUpdated(uri);
|
|
3917
|
+
}
|
|
3918
|
+
return;
|
|
3919
|
+
}
|
|
3920
|
+
if (!isJsonRpcResult(msg)) return;
|
|
3921
|
+
const response = msg;
|
|
3922
|
+
if (host.pending.has(response.id)) {
|
|
3923
|
+
const entry = host.pending.get(response.id);
|
|
3924
|
+
host.pending.delete(response.id);
|
|
3925
|
+
entry?.resolve(response);
|
|
3926
|
+
}
|
|
3927
|
+
}
|
|
3928
|
+
|
|
3581
3929
|
// src/client.ts
|
|
3582
|
-
var MCPClient = class
|
|
3930
|
+
var MCPClient = class {
|
|
3583
3931
|
constructor(opts) {
|
|
3584
3932
|
this.opts = opts;
|
|
3585
3933
|
this.serverRequests = new ServerRequestResponder(opts.elicitation);
|
|
@@ -3589,15 +3937,6 @@ var MCPClient = class _MCPClient {
|
|
|
3589
3937
|
this.requestCapability.bind(this),
|
|
3590
3938
|
this.requireResourceSubscriptions.bind(this)
|
|
3591
3939
|
);
|
|
3592
|
-
/**
|
|
3593
|
-
* Maximum bytes the rx buffer may accumulate before the connection is
|
|
3594
|
-
* forcefully closed. A well-behaved JSON-RPC server emits newline-delimited
|
|
3595
|
-
* messages that are individually much smaller than this; a server that never
|
|
3596
|
-
* sends a newline would grow the buffer without limit and OOM the process.
|
|
3597
|
-
* 16 MiB is generous for any legitimate single message while bounding the
|
|
3598
|
-
* worst-case memory to a predictable cap.
|
|
3599
|
-
*/
|
|
3600
|
-
static MAX_RX_BUFFER_BYTES = 16 * 1024 * 1024;
|
|
3601
3940
|
state = "idle";
|
|
3602
3941
|
child;
|
|
3603
3942
|
nextId = 1;
|
|
@@ -3628,7 +3967,7 @@ var MCPClient = class _MCPClient {
|
|
|
3628
3967
|
* JSON-RPC payload. StringDecoder withholds the partial sequence until the
|
|
3629
3968
|
* chunk that completes it (same remedy as `readFileHead` in core utils).
|
|
3630
3969
|
*/
|
|
3631
|
-
rxDecoder = new
|
|
3970
|
+
rxDecoder = new StringDecoder2("utf8");
|
|
3632
3971
|
_tools = [];
|
|
3633
3972
|
/** Server-declared handshake metadata. Populated for stdio in the first protocol slice. */
|
|
3634
3973
|
_serverMetadata;
|
|
@@ -3638,6 +3977,8 @@ var MCPClient = class _MCPClient {
|
|
|
3638
3977
|
_lastNotifySkipped = false;
|
|
3639
3978
|
closePromise;
|
|
3640
3979
|
connectPromise;
|
|
3980
|
+
toolCatalogVersion = 0;
|
|
3981
|
+
toolCatalogRevision = 0;
|
|
3641
3982
|
// HTTP transports
|
|
3642
3983
|
sseTransport;
|
|
3643
3984
|
httpTransport;
|
|
@@ -3700,6 +4041,7 @@ var MCPClient = class _MCPClient {
|
|
|
3700
4041
|
return this.connectPromise;
|
|
3701
4042
|
}
|
|
3702
4043
|
async connectInner() {
|
|
4044
|
+
this.toolCatalogVersion++;
|
|
3703
4045
|
this.state = "connecting";
|
|
3704
4046
|
this._serverMetadata = void 0;
|
|
3705
4047
|
try {
|
|
@@ -3720,106 +4062,7 @@ var MCPClient = class _MCPClient {
|
|
|
3720
4062
|
}
|
|
3721
4063
|
}
|
|
3722
4064
|
async connectStdio() {
|
|
3723
|
-
|
|
3724
|
-
this.state = "failed";
|
|
3725
|
-
throw new Error('MCP stdio transport requires "command"');
|
|
3726
|
-
}
|
|
3727
|
-
this.rxBuffer = "";
|
|
3728
|
-
this.rxBufferBytes = 0;
|
|
3729
|
-
this.rxDecoder = new StringDecoder("utf8");
|
|
3730
|
-
const extraEnv = { ...this.opts.env };
|
|
3731
|
-
if (this.opts.passthroughEnv) {
|
|
3732
|
-
for (const name of this.opts.passthroughEnv) {
|
|
3733
|
-
const val = process.env[name];
|
|
3734
|
-
if (val !== void 0) {
|
|
3735
|
-
extraEnv[name] = val;
|
|
3736
|
-
}
|
|
3737
|
-
}
|
|
3738
|
-
}
|
|
3739
|
-
const isWin = process.platform === "win32";
|
|
3740
|
-
const rawArgs = this.opts.args ?? [];
|
|
3741
|
-
const spawnEnv = buildChildEnv2({ extra: extraEnv });
|
|
3742
|
-
const stdio = ["pipe", "pipe", "pipe"];
|
|
3743
|
-
const child = isWin ? (() => {
|
|
3744
|
-
const shim = buildWin32CmdShimInvocation(this.opts.command, rawArgs);
|
|
3745
|
-
return spawn2(shim.command, shim.args, {
|
|
3746
|
-
env: spawnEnv,
|
|
3747
|
-
stdio,
|
|
3748
|
-
...this.opts.cwd ? { cwd: this.opts.cwd } : {},
|
|
3749
|
-
windowsVerbatimArguments: shim.windowsVerbatimArguments,
|
|
3750
|
-
// Without this every MCP server spawned from a console-less host
|
|
3751
|
-
// (WebUI server, scheduled runs) opens a visible console window.
|
|
3752
|
-
windowsHide: true
|
|
3753
|
-
});
|
|
3754
|
-
})() : spawn2(this.opts.command, rawArgs, {
|
|
3755
|
-
env: spawnEnv,
|
|
3756
|
-
stdio,
|
|
3757
|
-
windowsHide: true,
|
|
3758
|
-
...this.opts.cwd ? { cwd: this.opts.cwd } : {}
|
|
3759
|
-
});
|
|
3760
|
-
this.child = child;
|
|
3761
|
-
child.stdout?.on("data", (chunk) => this.onData(this.rxDecoder.write(chunk)));
|
|
3762
|
-
child.stdout?.on("end", () => {
|
|
3763
|
-
const tail = this.rxDecoder.end();
|
|
3764
|
-
if (tail) this.onData(tail);
|
|
3765
|
-
if (this.rxBuffer.trim()) {
|
|
3766
|
-
const line = this.rxBuffer.trim();
|
|
3767
|
-
this.rxBuffer = "";
|
|
3768
|
-
this.onLine(line);
|
|
3769
|
-
}
|
|
3770
|
-
});
|
|
3771
|
-
child.stderr?.on("data", () => {
|
|
3772
|
-
});
|
|
3773
|
-
child.stdin?.on("error", (err) => {
|
|
3774
|
-
this.failPending(`MCP "${this.opts.name}" stdin error: ${toErrorMessage(err)}`);
|
|
3775
|
-
});
|
|
3776
|
-
child.on("exit", (code, signal) => {
|
|
3777
|
-
this.state = "disconnected";
|
|
3778
|
-
this.failPending(
|
|
3779
|
-
`MCP "${this.opts.name}" child exited (code=${code ?? "null"} signal=${signal ?? "null"})`
|
|
3780
|
-
);
|
|
3781
|
-
for (const listener of this.exitListeners) {
|
|
3782
|
-
try {
|
|
3783
|
-
listener(this.opts.name, code, signal);
|
|
3784
|
-
} catch {
|
|
3785
|
-
}
|
|
3786
|
-
}
|
|
3787
|
-
});
|
|
3788
|
-
child.on("error", (err) => {
|
|
3789
|
-
this.state = "failed";
|
|
3790
|
-
this.failPending(`MCP "${this.opts.name}" child error: ${toErrorMessage(err)}`);
|
|
3791
|
-
});
|
|
3792
|
-
const initialize = await this.request(
|
|
3793
|
-
"initialize",
|
|
3794
|
-
{
|
|
3795
|
-
protocolVersion: MCP_CONSTANTS.PROTOCOL_VERSION,
|
|
3796
|
-
// Client capabilities: elicitation when the host can ask its user.
|
|
3797
|
-
// `tools` is a SERVER capability and never belonged here.
|
|
3798
|
-
capabilities: this.serverRequests.capabilities(),
|
|
3799
|
-
clientInfo: MCP_CONSTANTS.CLIENT_INFO
|
|
3800
|
-
},
|
|
3801
|
-
this.opts.startupTimeoutMs ?? 1e4
|
|
3802
|
-
);
|
|
3803
|
-
if (initialize.error) {
|
|
3804
|
-
this.state = "failed";
|
|
3805
|
-
throw new Error(`MCP initialize failed: ${initialize.error.message}`);
|
|
3806
|
-
}
|
|
3807
|
-
try {
|
|
3808
|
-
this._serverMetadata = parseServerMetadata(initialize.result);
|
|
3809
|
-
} catch (err) {
|
|
3810
|
-
this.state = "failed";
|
|
3811
|
-
throw new Error(`MCP initialize returned malformed server metadata: ${toErrorMessage(err)}`);
|
|
3812
|
-
}
|
|
3813
|
-
try {
|
|
3814
|
-
await this.notify("notifications/initialized", {});
|
|
3815
|
-
} catch (err) {
|
|
3816
|
-
console.warn(
|
|
3817
|
-
'[MCP] notify("notifications/initialized") failed for "' + this.opts.name + '": ' + toErrorMessage(err)
|
|
3818
|
-
);
|
|
3819
|
-
}
|
|
3820
|
-
this._tools = await listAllTools((params) => this.request("tools/list", params)) ?? [];
|
|
3821
|
-
this._toolsCache = this._tools;
|
|
3822
|
-
this.state = "connected";
|
|
4065
|
+
return connectStdio(this.clientStdioHost());
|
|
3823
4066
|
}
|
|
3824
4067
|
async connectSSE() {
|
|
3825
4068
|
return connectSSE(this.clientHttpConnectionHost());
|
|
@@ -3876,6 +4119,7 @@ var MCPClient = class _MCPClient {
|
|
|
3876
4119
|
return this.closePromise;
|
|
3877
4120
|
}
|
|
3878
4121
|
async closeInner() {
|
|
4122
|
+
this.toolCatalogVersion++;
|
|
3879
4123
|
if (this.child) {
|
|
3880
4124
|
const child = this.child;
|
|
3881
4125
|
const exitPromise = new Promise((resolve) => {
|
|
@@ -3923,9 +4167,13 @@ var MCPClient = class _MCPClient {
|
|
|
3923
4167
|
await Promise.allSettled([this.sseTransport?.close(), this.httpTransport?.close()]);
|
|
3924
4168
|
this.state = "disconnected";
|
|
3925
4169
|
}
|
|
3926
|
-
request(method, params, timeoutMs
|
|
3927
|
-
|
|
3928
|
-
|
|
4170
|
+
request(method, params, timeoutMs, opts) {
|
|
4171
|
+
const defaultTimeoutMs = typeof this.opts.requestTimeoutMs === "number" && Number.isFinite(this.opts.requestTimeoutMs) && this.opts.requestTimeoutMs > 0 ? this.opts.requestTimeoutMs : 6e4;
|
|
4172
|
+
const effectiveTimeoutMs = typeof timeoutMs === "number" && Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : defaultTimeoutMs;
|
|
4173
|
+
if (this.sseTransport)
|
|
4174
|
+
return this.sseTransport.request(method, params, effectiveTimeoutMs, opts);
|
|
4175
|
+
if (this.httpTransport)
|
|
4176
|
+
return this.httpTransport.request(method, params, effectiveTimeoutMs, opts);
|
|
3929
4177
|
const signal = opts?.signal;
|
|
3930
4178
|
if (signal?.aborted) {
|
|
3931
4179
|
const err = new Error(`MCP "${this.opts.name}" request "${method}" aborted before send`);
|
|
@@ -3955,13 +4203,15 @@ var MCPClient = class _MCPClient {
|
|
|
3955
4203
|
};
|
|
3956
4204
|
const onTimeout = () => {
|
|
3957
4205
|
if (this.serverRequests.awaitingUser) {
|
|
3958
|
-
entry.timer = setTimeout(onTimeout,
|
|
4206
|
+
entry.timer = setTimeout(onTimeout, effectiveTimeoutMs);
|
|
3959
4207
|
return;
|
|
3960
4208
|
}
|
|
3961
4209
|
this.pending.delete(id);
|
|
3962
4210
|
detach();
|
|
3963
4211
|
reject(
|
|
3964
|
-
new Error(
|
|
4212
|
+
new Error(
|
|
4213
|
+
`MCP "${this.opts.name}" request "${method}" timed out after ${effectiveTimeoutMs}ms`
|
|
4214
|
+
)
|
|
3965
4215
|
);
|
|
3966
4216
|
};
|
|
3967
4217
|
const entry = {
|
|
@@ -3975,7 +4225,7 @@ var MCPClient = class _MCPClient {
|
|
|
3975
4225
|
detach();
|
|
3976
4226
|
reject(err);
|
|
3977
4227
|
},
|
|
3978
|
-
timer: setTimeout(onTimeout,
|
|
4228
|
+
timer: setTimeout(onTimeout, effectiveTimeoutMs)
|
|
3979
4229
|
};
|
|
3980
4230
|
this.pending.set(id, entry);
|
|
3981
4231
|
const stdin = this.child?.stdin;
|
|
@@ -4047,132 +4297,13 @@ var MCPClient = class _MCPClient {
|
|
|
4047
4297
|
this.pending.clear();
|
|
4048
4298
|
}
|
|
4049
4299
|
async notify(method, params) {
|
|
4050
|
-
|
|
4051
|
-
this._lastNotifySkipped = true;
|
|
4052
|
-
console.warn(
|
|
4053
|
-
JSON.stringify({
|
|
4054
|
-
level: "warn",
|
|
4055
|
-
event: "mcp.notify_skipped_backpressure",
|
|
4056
|
-
server: this.opts.name,
|
|
4057
|
-
method,
|
|
4058
|
-
message: "stdin buffer backpressure (already waiting for drain)",
|
|
4059
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4060
|
-
})
|
|
4061
|
-
);
|
|
4062
|
-
return;
|
|
4063
|
-
}
|
|
4064
|
-
const stdin = this.child?.stdin;
|
|
4065
|
-
if (!stdin || stdin.destroyed === true || stdin.writable === false) {
|
|
4066
|
-
return;
|
|
4067
|
-
}
|
|
4068
|
-
const req = { jsonrpc: "2.0", method, params };
|
|
4069
|
-
const encoded = JSON.stringify(req) + "\n";
|
|
4070
|
-
try {
|
|
4071
|
-
const ok = stdin.write(encoded);
|
|
4072
|
-
if (!ok) {
|
|
4073
|
-
this._drainPending = true;
|
|
4074
|
-
await new Promise((resolve, reject) => {
|
|
4075
|
-
const timeout = setTimeout(() => {
|
|
4076
|
-
stdin.removeListener?.("drain", onDrain);
|
|
4077
|
-
stdin.removeListener?.("error", onError);
|
|
4078
|
-
this._drainPending = false;
|
|
4079
|
-
reject(new Error(`MCP notify("${method}") drain timeout`));
|
|
4080
|
-
}, 500);
|
|
4081
|
-
const onDrain = () => {
|
|
4082
|
-
clearTimeout(timeout);
|
|
4083
|
-
stdin.removeListener?.("drain", onDrain);
|
|
4084
|
-
stdin.removeListener?.("error", onError);
|
|
4085
|
-
this._drainPending = false;
|
|
4086
|
-
resolve();
|
|
4087
|
-
};
|
|
4088
|
-
const onError = (err) => {
|
|
4089
|
-
clearTimeout(timeout);
|
|
4090
|
-
stdin.removeListener?.("drain", onDrain);
|
|
4091
|
-
stdin.removeListener?.("error", onError);
|
|
4092
|
-
this._drainPending = false;
|
|
4093
|
-
reject(err);
|
|
4094
|
-
};
|
|
4095
|
-
stdin.once?.("drain", onDrain);
|
|
4096
|
-
stdin.once?.("error", onError);
|
|
4097
|
-
});
|
|
4098
|
-
}
|
|
4099
|
-
} catch (err) {
|
|
4100
|
-
throw new Error(`[MCP] notify("${method}") failed: ${toErrorMessage(err)}`);
|
|
4101
|
-
}
|
|
4300
|
+
return notifyStdio(this.clientStdioHost(), method, params);
|
|
4102
4301
|
}
|
|
4103
4302
|
onData(s) {
|
|
4104
|
-
this.
|
|
4105
|
-
if (this.rxBufferBytes > _MCPClient.MAX_RX_BUFFER_BYTES) {
|
|
4106
|
-
const truncated = this.rxBufferBytes;
|
|
4107
|
-
this.rxParts = [];
|
|
4108
|
-
this.rxBufferBytes = 0;
|
|
4109
|
-
this.failPending(
|
|
4110
|
-
`MCP "${this.opts.name}" rx buffer overflow (${truncated} bytes without a newline) \u2014 closing connection`
|
|
4111
|
-
);
|
|
4112
|
-
void this.close();
|
|
4113
|
-
return;
|
|
4114
|
-
}
|
|
4115
|
-
let start = 0;
|
|
4116
|
-
let idx = s.indexOf("\n");
|
|
4117
|
-
if (idx === -1) {
|
|
4118
|
-
this.rxParts.push(s);
|
|
4119
|
-
return;
|
|
4120
|
-
}
|
|
4121
|
-
while (idx !== -1) {
|
|
4122
|
-
const head = this.rxParts.length > 0 ? this.rxParts.join("") : "";
|
|
4123
|
-
this.rxParts = [];
|
|
4124
|
-
const line = (head + s.slice(start, idx)).trim();
|
|
4125
|
-
start = idx + 1;
|
|
4126
|
-
if (line) this.onLine(line);
|
|
4127
|
-
idx = s.indexOf("\n", start);
|
|
4128
|
-
}
|
|
4129
|
-
const tail = s.slice(start);
|
|
4130
|
-
this.rxBufferBytes = Buffer.byteLength(tail, "utf8");
|
|
4131
|
-
if (tail) this.rxParts.push(tail);
|
|
4303
|
+
receiveStdioData(this.clientStdioProtocolHost(), s);
|
|
4132
4304
|
}
|
|
4133
4305
|
onLine(line) {
|
|
4134
|
-
|
|
4135
|
-
try {
|
|
4136
|
-
msg = JSON.parse(line);
|
|
4137
|
-
} catch {
|
|
4138
|
-
return;
|
|
4139
|
-
}
|
|
4140
|
-
if (typeof msg !== "object" || msg === null) return;
|
|
4141
|
-
const envelope = msg;
|
|
4142
|
-
if (envelope["jsonrpc"] !== "2.0") return;
|
|
4143
|
-
if (typeof envelope["method"] === "string") {
|
|
4144
|
-
const id = envelope["id"];
|
|
4145
|
-
if (typeof id === "number" || typeof id === "string") {
|
|
4146
|
-
void this.handleServerRequest({
|
|
4147
|
-
jsonrpc: "2.0",
|
|
4148
|
-
id,
|
|
4149
|
-
method: envelope["method"],
|
|
4150
|
-
params: envelope["params"]
|
|
4151
|
-
});
|
|
4152
|
-
return;
|
|
4153
|
-
}
|
|
4154
|
-
if (Object.hasOwn(envelope, "id")) return;
|
|
4155
|
-
if (envelope["method"] === "notifications/cancelled") {
|
|
4156
|
-
this.serverRequests.cancel(envelope["params"]);
|
|
4157
|
-
} else if (envelope["method"] === "notifications/tools/list_changed") {
|
|
4158
|
-
void this.handleToolsListChanged();
|
|
4159
|
-
} else if (envelope["method"] === "notifications/resources/list_changed") {
|
|
4160
|
-
this.emitCapabilityChanged("resources");
|
|
4161
|
-
} else if (envelope["method"] === "notifications/prompts/list_changed") {
|
|
4162
|
-
this.emitCapabilityChanged("prompts");
|
|
4163
|
-
} else if (envelope["method"] === "notifications/resources/updated") {
|
|
4164
|
-
const uri = resourceUpdatedUri(envelope["params"]);
|
|
4165
|
-
if (uri) this.emitResourceUpdated(uri);
|
|
4166
|
-
}
|
|
4167
|
-
return;
|
|
4168
|
-
}
|
|
4169
|
-
if (!isJsonRpcResult(msg)) return;
|
|
4170
|
-
const response = msg;
|
|
4171
|
-
if (this.pending.has(response.id)) {
|
|
4172
|
-
const entry = this.pending.get(response.id);
|
|
4173
|
-
this.pending.delete(response.id);
|
|
4174
|
-
entry?.resolve(response);
|
|
4175
|
-
}
|
|
4306
|
+
receiveStdioLine(this.clientStdioProtocolHost(), line);
|
|
4176
4307
|
}
|
|
4177
4308
|
async handleServerRequest(request3) {
|
|
4178
4309
|
const response = await this.serverRequests.answer(request3);
|
|
@@ -4189,11 +4320,13 @@ var MCPClient = class _MCPClient {
|
|
|
4189
4320
|
* to a hard crash on a transient notification glitch.
|
|
4190
4321
|
*/
|
|
4191
4322
|
async handleToolsListChanged() {
|
|
4323
|
+
const version = ++this.toolCatalogVersion;
|
|
4192
4324
|
try {
|
|
4193
4325
|
const tools = await listAllTools((params) => this.request("tools/list", params));
|
|
4194
|
-
if (!tools) return;
|
|
4326
|
+
if (!tools || version !== this.toolCatalogVersion) return;
|
|
4195
4327
|
this._tools = tools;
|
|
4196
4328
|
this._toolsCache = tools;
|
|
4329
|
+
this.toolCatalogRevision++;
|
|
4197
4330
|
for (const listener of this.toolsChangedListeners) {
|
|
4198
4331
|
try {
|
|
4199
4332
|
listener(this.opts.name, [...tools]);
|
|
@@ -4247,9 +4380,7 @@ var MCPClient = class _MCPClient {
|
|
|
4247
4380
|
clientHttpConnectionHost() {
|
|
4248
4381
|
const self = this;
|
|
4249
4382
|
return {
|
|
4250
|
-
|
|
4251
|
-
return self.opts;
|
|
4252
|
-
},
|
|
4383
|
+
opts: this.opts,
|
|
4253
4384
|
get state() {
|
|
4254
4385
|
return self.state;
|
|
4255
4386
|
},
|
|
@@ -4293,6 +4424,109 @@ var MCPClient = class _MCPClient {
|
|
|
4293
4424
|
serverRequests: this.serverRequests
|
|
4294
4425
|
};
|
|
4295
4426
|
}
|
|
4427
|
+
clientStdioHost() {
|
|
4428
|
+
const self = this;
|
|
4429
|
+
return {
|
|
4430
|
+
opts: this.opts,
|
|
4431
|
+
get state() {
|
|
4432
|
+
return self.state;
|
|
4433
|
+
},
|
|
4434
|
+
set state(value) {
|
|
4435
|
+
self.state = value;
|
|
4436
|
+
},
|
|
4437
|
+
get rxBuffer() {
|
|
4438
|
+
return self.rxBuffer;
|
|
4439
|
+
},
|
|
4440
|
+
set rxBuffer(value) {
|
|
4441
|
+
self.rxBuffer = value;
|
|
4442
|
+
},
|
|
4443
|
+
get rxBufferBytes() {
|
|
4444
|
+
return self.rxBufferBytes;
|
|
4445
|
+
},
|
|
4446
|
+
set rxBufferBytes(value) {
|
|
4447
|
+
self.rxBufferBytes = value;
|
|
4448
|
+
},
|
|
4449
|
+
get rxDecoder() {
|
|
4450
|
+
return self.rxDecoder;
|
|
4451
|
+
},
|
|
4452
|
+
set rxDecoder(value) {
|
|
4453
|
+
self.rxDecoder = value;
|
|
4454
|
+
},
|
|
4455
|
+
get child() {
|
|
4456
|
+
return self.child;
|
|
4457
|
+
},
|
|
4458
|
+
set child(value) {
|
|
4459
|
+
self.child = value;
|
|
4460
|
+
},
|
|
4461
|
+
onData: (...args) => this.onData(...args),
|
|
4462
|
+
onLine: (...args) => this.onLine(...args),
|
|
4463
|
+
failPending: (...args) => this.failPending(...args),
|
|
4464
|
+
exitListeners: this.exitListeners,
|
|
4465
|
+
request: (...args) => this.request(...args),
|
|
4466
|
+
serverRequests: this.serverRequests,
|
|
4467
|
+
get _serverMetadata() {
|
|
4468
|
+
return self._serverMetadata;
|
|
4469
|
+
},
|
|
4470
|
+
set _serverMetadata(value) {
|
|
4471
|
+
self._serverMetadata = value;
|
|
4472
|
+
},
|
|
4473
|
+
notify: (...args) => this.notify(...args),
|
|
4474
|
+
get toolCatalogRevision() {
|
|
4475
|
+
return self.toolCatalogRevision;
|
|
4476
|
+
},
|
|
4477
|
+
get _tools() {
|
|
4478
|
+
return self._tools;
|
|
4479
|
+
},
|
|
4480
|
+
set _tools(value) {
|
|
4481
|
+
self._tools = value;
|
|
4482
|
+
},
|
|
4483
|
+
get _toolsCache() {
|
|
4484
|
+
return self._toolsCache;
|
|
4485
|
+
},
|
|
4486
|
+
set _toolsCache(value) {
|
|
4487
|
+
self._toolsCache = value;
|
|
4488
|
+
},
|
|
4489
|
+
get _drainPending() {
|
|
4490
|
+
return self._drainPending;
|
|
4491
|
+
},
|
|
4492
|
+
set _drainPending(value) {
|
|
4493
|
+
self._drainPending = value;
|
|
4494
|
+
},
|
|
4495
|
+
get _lastNotifySkipped() {
|
|
4496
|
+
return self._lastNotifySkipped;
|
|
4497
|
+
},
|
|
4498
|
+
set _lastNotifySkipped(value) {
|
|
4499
|
+
self._lastNotifySkipped = value;
|
|
4500
|
+
}
|
|
4501
|
+
};
|
|
4502
|
+
}
|
|
4503
|
+
clientStdioProtocolHost() {
|
|
4504
|
+
const self = this;
|
|
4505
|
+
return {
|
|
4506
|
+
get rxBufferBytes() {
|
|
4507
|
+
return self.rxBufferBytes;
|
|
4508
|
+
},
|
|
4509
|
+
set rxBufferBytes(value) {
|
|
4510
|
+
self.rxBufferBytes = value;
|
|
4511
|
+
},
|
|
4512
|
+
get rxParts() {
|
|
4513
|
+
return self.rxParts;
|
|
4514
|
+
},
|
|
4515
|
+
set rxParts(value) {
|
|
4516
|
+
self.rxParts = value;
|
|
4517
|
+
},
|
|
4518
|
+
failPending: (...args) => this.failPending(...args),
|
|
4519
|
+
opts: this.opts,
|
|
4520
|
+
close: (...args) => this.close(...args),
|
|
4521
|
+
onLine: (...args) => this.onLine(...args),
|
|
4522
|
+
handleServerRequest: (...args) => this.handleServerRequest(...args),
|
|
4523
|
+
serverRequests: this.serverRequests,
|
|
4524
|
+
handleToolsListChanged: (...args) => this.handleToolsListChanged(...args),
|
|
4525
|
+
emitCapabilityChanged: (...args) => this.emitCapabilityChanged(...args),
|
|
4526
|
+
emitResourceUpdated: (...args) => this.emitResourceUpdated(...args),
|
|
4527
|
+
pending: this.pending
|
|
4528
|
+
};
|
|
4529
|
+
}
|
|
4296
4530
|
};
|
|
4297
4531
|
|
|
4298
4532
|
// src/config-env.ts
|
|
@@ -4313,7 +4547,7 @@ function mcpEnvPlaceholders(cfg) {
|
|
|
4313
4547
|
function expandMcpEnvPlaceholders(cfg, env = process.env) {
|
|
4314
4548
|
const missing = /* @__PURE__ */ new Set();
|
|
4315
4549
|
const expand = (value) => value.replace(PLACEHOLDER, (_match, name, fallback) => {
|
|
4316
|
-
const resolved = env[name];
|
|
4550
|
+
const resolved = Object.hasOwn(env, name) ? env[name] : void 0;
|
|
4317
4551
|
if (resolved !== void 0 && resolved !== "") return resolved;
|
|
4318
4552
|
if (fallback !== void 0) return fallback;
|
|
4319
4553
|
missing.add(name);
|
|
@@ -4672,7 +4906,12 @@ async function elicitViaUserInput(request3, fallback, options = {}) {
|
|
|
4672
4906
|
}
|
|
4673
4907
|
const content = {};
|
|
4674
4908
|
form.fields.forEach((field, i) => {
|
|
4675
|
-
content
|
|
4909
|
+
Object.defineProperty(content, field.name, {
|
|
4910
|
+
value: answerValue(field, byId.get(questionId(i))),
|
|
4911
|
+
enumerable: true,
|
|
4912
|
+
writable: true,
|
|
4913
|
+
configurable: true
|
|
4914
|
+
});
|
|
4676
4915
|
});
|
|
4677
4916
|
const checked = validateElicitationContent(form.fields, content);
|
|
4678
4917
|
if (checked.ok) return { action: "accept", content: checked.value };
|
|
@@ -5023,6 +5262,12 @@ function forgetRegistryState(registry, name) {
|
|
|
5023
5262
|
import { createHash as createHash2, randomBytes as randomBytes4 } from "node:crypto";
|
|
5024
5263
|
import * as fs2 from "node:fs/promises";
|
|
5025
5264
|
import * as path from "node:path";
|
|
5265
|
+
function isRecord2(value) {
|
|
5266
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
5267
|
+
}
|
|
5268
|
+
function isCachedTool(value) {
|
|
5269
|
+
return isRecord2(value) && typeof value["name"] === "string" && value["name"].trim().length > 0 && (value["description"] === void 0 || typeof value["description"] === "string") && isRecord2(value["inputSchema"]) && (value["outputSchema"] === void 0 || isRecord2(value["outputSchema"]));
|
|
5270
|
+
}
|
|
5026
5271
|
function manifestConfigHash(cfg) {
|
|
5027
5272
|
const basis = JSON.stringify({
|
|
5028
5273
|
transport: cfg.transport,
|
|
@@ -5058,7 +5303,8 @@ async function readCapabilityManifest(cacheDir, name, configHash) {
|
|
|
5058
5303
|
try {
|
|
5059
5304
|
const raw = await fs2.readFile(manifestFile(cacheDir, name), "utf8");
|
|
5060
5305
|
const parsed = JSON.parse(raw);
|
|
5061
|
-
if (parsed.configHash !== configHash || !Array.isArray(parsed.tools)
|
|
5306
|
+
if (parsed.configHash !== configHash || !Array.isArray(parsed.tools) || !parsed.tools.every(isCachedTool))
|
|
5307
|
+
return null;
|
|
5062
5308
|
return {
|
|
5063
5309
|
tools: parsed.tools,
|
|
5064
5310
|
serverMetadata: parsed.serverMetadata === void 0 ? void 0 : parseServerMetadata(parsed.serverMetadata),
|
|
@@ -5275,6 +5521,13 @@ async function completeRegistryAuthorization(manager, cfg, name, callbackUrl, si
|
|
|
5275
5521
|
// src/registry-catalog.ts
|
|
5276
5522
|
var MAX_CATALOG_PAGES = 100;
|
|
5277
5523
|
var MAX_CATALOG_ITEMS = 1e4;
|
|
5524
|
+
function advanceCatalogVersion(slot, kind) {
|
|
5525
|
+
slot.catalogVersions ??= {};
|
|
5526
|
+
const versions = slot.catalogVersions;
|
|
5527
|
+
const version = (versions[kind] ?? 0) + 1;
|
|
5528
|
+
versions[kind] = version;
|
|
5529
|
+
return version;
|
|
5530
|
+
}
|
|
5278
5531
|
async function collectCatalogPages(load, select) {
|
|
5279
5532
|
const items = [];
|
|
5280
5533
|
const seenCursors = /* @__PURE__ */ new Set();
|
|
@@ -5307,10 +5560,105 @@ function registryCatalogSnapshot(slot) {
|
|
|
5307
5560
|
};
|
|
5308
5561
|
}
|
|
5309
5562
|
|
|
5563
|
+
// src/registry-catalog-operations.ts
|
|
5564
|
+
function getCatalog(name) {
|
|
5565
|
+
const slot = this.servers.get(name);
|
|
5566
|
+
if (slot) {
|
|
5567
|
+
return registryCatalogSnapshot(slot);
|
|
5568
|
+
}
|
|
5569
|
+
const disabled = this.disabledServers.get(name);
|
|
5570
|
+
if (disabled) {
|
|
5571
|
+
return {
|
|
5572
|
+
name: disabled.name,
|
|
5573
|
+
state: "idle"
|
|
5574
|
+
};
|
|
5575
|
+
}
|
|
5576
|
+
return void 0;
|
|
5577
|
+
}
|
|
5578
|
+
async function listResources(name, opts = {}) {
|
|
5579
|
+
const slot = this.requireSlot(name);
|
|
5580
|
+
if (!opts.refresh && slot.resources) return cloneCatalogRecords(slot.resources);
|
|
5581
|
+
const version = advanceCatalogVersion(slot, "resources");
|
|
5582
|
+
return this.withConnectedClient(name, async (client, assertCurrent) => {
|
|
5583
|
+
if (!client.getServerMetadata()?.capabilities.resources) return [];
|
|
5584
|
+
const resources = await collectCatalogPages(
|
|
5585
|
+
(cursor) => client.listResources(cursor ? { cursor } : {}),
|
|
5586
|
+
(page) => page.resources
|
|
5587
|
+
);
|
|
5588
|
+
assertCurrent();
|
|
5589
|
+
if (slot.catalogVersions?.resources === version) {
|
|
5590
|
+
slot.resources = resources;
|
|
5591
|
+
await this.persistCapabilityManifest(slot);
|
|
5592
|
+
}
|
|
5593
|
+
return cloneCatalogRecords(resources);
|
|
5594
|
+
});
|
|
5595
|
+
}
|
|
5596
|
+
async function listResourceTemplates(name, opts = {}) {
|
|
5597
|
+
const slot = this.requireSlot(name);
|
|
5598
|
+
if (!opts.refresh && slot.resourceTemplates) return cloneCatalogRecords(slot.resourceTemplates);
|
|
5599
|
+
const version = advanceCatalogVersion(slot, "resourceTemplates");
|
|
5600
|
+
return this.withConnectedClient(name, async (client, assertCurrent) => {
|
|
5601
|
+
if (!client.getServerMetadata()?.capabilities.resources) return [];
|
|
5602
|
+
const templates = await collectCatalogPages(
|
|
5603
|
+
(cursor) => client.listResourceTemplates(cursor ? { cursor } : {}),
|
|
5604
|
+
(page) => page.resourceTemplates
|
|
5605
|
+
);
|
|
5606
|
+
assertCurrent();
|
|
5607
|
+
if (slot.catalogVersions?.resourceTemplates === version) {
|
|
5608
|
+
slot.resourceTemplates = templates;
|
|
5609
|
+
await this.persistCapabilityManifest(slot);
|
|
5610
|
+
}
|
|
5611
|
+
return cloneCatalogRecords(templates);
|
|
5612
|
+
});
|
|
5613
|
+
}
|
|
5614
|
+
async function readResource(name, uri) {
|
|
5615
|
+
return this.withConnectedClient(name, (client) => client.readResource(uri));
|
|
5616
|
+
}
|
|
5617
|
+
async function selectResourceForInsertion(name, uri, policy) {
|
|
5618
|
+
return prepareResourceInsertion(name, uri, await this.readResource(name, uri), policy);
|
|
5619
|
+
}
|
|
5620
|
+
async function subscribeResource(name, uri) {
|
|
5621
|
+
await this.withConnectedClient(name, (client) => client.subscribeResource(uri));
|
|
5622
|
+
}
|
|
5623
|
+
async function unsubscribeResource(name, uri) {
|
|
5624
|
+
await this.withConnectedClient(name, (client) => client.unsubscribeResource(uri));
|
|
5625
|
+
}
|
|
5626
|
+
async function listPrompts(name, opts = {}) {
|
|
5627
|
+
const slot = this.requireSlot(name);
|
|
5628
|
+
if (!opts.refresh && slot.prompts) return cloneCatalogRecords(slot.prompts);
|
|
5629
|
+
const version = advanceCatalogVersion(slot, "prompts");
|
|
5630
|
+
return this.withConnectedClient(name, async (client, assertCurrent) => {
|
|
5631
|
+
if (!client.getServerMetadata()?.capabilities.prompts) return [];
|
|
5632
|
+
const prompts = await collectCatalogPages(
|
|
5633
|
+
(cursor) => client.listPrompts(cursor ? { cursor } : {}),
|
|
5634
|
+
(page) => page.prompts
|
|
5635
|
+
);
|
|
5636
|
+
assertCurrent();
|
|
5637
|
+
if (slot.catalogVersions?.prompts === version) {
|
|
5638
|
+
slot.prompts = prompts;
|
|
5639
|
+
await this.persistCapabilityManifest(slot);
|
|
5640
|
+
}
|
|
5641
|
+
return cloneCatalogRecords(prompts);
|
|
5642
|
+
});
|
|
5643
|
+
}
|
|
5644
|
+
async function getPrompt(serverName, promptName, args) {
|
|
5645
|
+
return this.withConnectedClient(serverName, (client) => client.getPrompt(promptName, args));
|
|
5646
|
+
}
|
|
5647
|
+
async function selectPromptForInsertion(serverName, promptName, args, policy) {
|
|
5648
|
+
return preparePromptInsertion(
|
|
5649
|
+
serverName,
|
|
5650
|
+
promptName,
|
|
5651
|
+
args,
|
|
5652
|
+
await this.getPrompt(serverName, promptName, args),
|
|
5653
|
+
policy
|
|
5654
|
+
);
|
|
5655
|
+
}
|
|
5656
|
+
|
|
5310
5657
|
// src/registry-connect-loop.ts
|
|
5311
5658
|
import { expectDefined } from "@wrongstack/core/utils";
|
|
5312
5659
|
|
|
5313
5660
|
// src/wrap-tool.ts
|
|
5661
|
+
import { createSandboxMcpGate } from "@wrongstack/core/sandbox";
|
|
5314
5662
|
import { ToolCapabilities } from "@wrongstack/core/security";
|
|
5315
5663
|
import { mcpQualifiedToolName } from "@wrongstack/core/utils";
|
|
5316
5664
|
|
|
@@ -5377,9 +5725,10 @@ function isMutatingTool(mcpTool) {
|
|
|
5377
5725
|
}
|
|
5378
5726
|
return false;
|
|
5379
5727
|
}
|
|
5380
|
-
function wrapMCPTool(serverName, mcpTool, client, permission = "confirm", observer) {
|
|
5728
|
+
function wrapMCPTool(serverName, mcpTool, client, permission = "confirm", sandboxTrust = false, observer) {
|
|
5729
|
+
const sandboxGate = createSandboxMcpGate({ trusted: sandboxTrust });
|
|
5381
5730
|
const qualifiedName = mcpQualifiedToolName(serverName, mcpTool.name);
|
|
5382
|
-
|
|
5731
|
+
const tool = {
|
|
5383
5732
|
name: qualifiedName,
|
|
5384
5733
|
description: mcpTool.description ?? `${qualifiedName} (MCP tool)`,
|
|
5385
5734
|
usageHint: `Tool provided by MCP server "${serverName}". ${mcpTool.description ?? ""}`,
|
|
@@ -5418,6 +5767,7 @@ function wrapMCPTool(serverName, mcpTool, client, permission = "confirm", observ
|
|
|
5418
5767
|
}
|
|
5419
5768
|
}
|
|
5420
5769
|
};
|
|
5770
|
+
return sandboxGate(tool);
|
|
5421
5771
|
}
|
|
5422
5772
|
function describeUrlOutcomes(serverName, toolName, outcomes) {
|
|
5423
5773
|
const lines = outcomes.map(({ elicitation, action }) => {
|
|
@@ -5482,36 +5832,48 @@ function applySlotTools(ctx, slot, tools, client) {
|
|
|
5482
5832
|
const allowed = slot.cfg.allowedTools;
|
|
5483
5833
|
const filtered = tools.filter((t) => !allowed || allowed.includes(t.name));
|
|
5484
5834
|
const signature = JSON.stringify(
|
|
5485
|
-
filtered.map((t) => [
|
|
5835
|
+
filtered.map((t) => [
|
|
5836
|
+
t.name,
|
|
5837
|
+
t.description ?? null,
|
|
5838
|
+
t.inputSchema ?? null,
|
|
5839
|
+
t.outputSchema ?? null
|
|
5840
|
+
])
|
|
5486
5841
|
);
|
|
5487
5842
|
const lazyWrappersCurrent = slot.lazy && slot.toolSignature === signature && slot.lazyTools.length === filtered.length && (slot.registeredLazy || ctx.lazyMode);
|
|
5488
5843
|
if (lazyWrappersCurrent) return;
|
|
5489
5844
|
const clientArg = slot.lazy ? () => ctx.ensureConnected(slot.cfg.name) : expectDefined(client);
|
|
5490
5845
|
const wrapped = filtered.map(
|
|
5491
|
-
(t) => wrapMCPTool(
|
|
5492
|
-
|
|
5493
|
-
|
|
5494
|
-
|
|
5495
|
-
|
|
5496
|
-
|
|
5497
|
-
|
|
5498
|
-
)
|
|
5499
|
-
|
|
5500
|
-
|
|
5501
|
-
|
|
5502
|
-
|
|
5503
|
-
|
|
5504
|
-
|
|
5505
|
-
|
|
5506
|
-
|
|
5507
|
-
|
|
5508
|
-
|
|
5509
|
-
|
|
5510
|
-
|
|
5511
|
-
|
|
5846
|
+
(t) => wrapMCPTool(
|
|
5847
|
+
slot.cfg.name,
|
|
5848
|
+
t,
|
|
5849
|
+
clientArg,
|
|
5850
|
+
slot.cfg.permission ?? "confirm",
|
|
5851
|
+
slot.cfg.sandboxTrust === true,
|
|
5852
|
+
{
|
|
5853
|
+
onStart: (caller) => {
|
|
5854
|
+
if (caller) slot.activeCallers = [...slot.activeCallers ?? [], caller];
|
|
5855
|
+
slot.operations.inFlightCalls++;
|
|
5856
|
+
slot.operations.peakInFlightCalls = Math.max(
|
|
5857
|
+
slot.operations.peakInFlightCalls,
|
|
5858
|
+
slot.operations.inFlightCalls
|
|
5859
|
+
);
|
|
5860
|
+
ctx.recordOperation(slot, "call", "started", void 0, void 0, false);
|
|
5861
|
+
},
|
|
5862
|
+
onFinish: ({ durationMs, ok }, caller) => {
|
|
5863
|
+
const at = caller ? slot.activeCallers?.lastIndexOf(caller) ?? -1 : -1;
|
|
5864
|
+
if (at >= 0) slot.activeCallers?.splice(at, 1);
|
|
5865
|
+
slot.operations.inFlightCalls = Math.max(0, slot.operations.inFlightCalls - 1);
|
|
5866
|
+
slot.lastUsed = Date.now();
|
|
5867
|
+
pushBounded(slot.operations.callSamples, durationMs, MCP_OPERATION_LIMITS.LATENCY_SAMPLES);
|
|
5868
|
+
if (ok) {
|
|
5869
|
+
ctx.recordSuccess(slot);
|
|
5870
|
+
ctx.recordOperation(slot, "call", "ok", void 0, durationMs, false);
|
|
5871
|
+
} else {
|
|
5872
|
+
ctx.recordFailure(slot, "tool", "tool-call-failed", durationMs);
|
|
5873
|
+
}
|
|
5512
5874
|
}
|
|
5513
5875
|
}
|
|
5514
|
-
|
|
5876
|
+
)
|
|
5515
5877
|
);
|
|
5516
5878
|
slot.lazyTools = wrapped;
|
|
5517
5879
|
slot.toolSignature = signature;
|
|
@@ -5536,51 +5898,62 @@ function applySlotTools(ctx, slot, tools, client) {
|
|
|
5536
5898
|
}
|
|
5537
5899
|
async function discoverSlotCapabilities(ctx, slot, client) {
|
|
5538
5900
|
const startedAt = Date.now();
|
|
5539
|
-
|
|
5540
|
-
const
|
|
5901
|
+
const versions = { ...slot.catalogVersions };
|
|
5902
|
+
const serverMetadata = client.getServerMetadata();
|
|
5903
|
+
const capabilities = serverMetadata?.capabilities;
|
|
5904
|
+
let resources;
|
|
5905
|
+
let resourceTemplates;
|
|
5906
|
+
let prompts;
|
|
5541
5907
|
if (capabilities?.resources) {
|
|
5542
5908
|
try {
|
|
5543
|
-
|
|
5909
|
+
resources = await collectCatalogPages(
|
|
5544
5910
|
(cursor) => client.listResources(cursor ? { cursor } : {}),
|
|
5545
5911
|
(page) => page.resources
|
|
5546
5912
|
);
|
|
5547
5913
|
} catch (err) {
|
|
5548
|
-
slot
|
|
5914
|
+
if (!ownsConnectedSlot(ctx, slot, client)) return;
|
|
5549
5915
|
ctx.recordFailure(slot, "protocol", "resource-discovery-failed");
|
|
5550
5916
|
ctx.log.warn(`MCP server "${slot.cfg.name}" resource discovery failed`, err);
|
|
5551
5917
|
}
|
|
5918
|
+
if (!ownsConnectedSlot(ctx, slot, client)) return;
|
|
5552
5919
|
try {
|
|
5553
|
-
|
|
5920
|
+
resourceTemplates = await collectCatalogPages(
|
|
5554
5921
|
(cursor) => client.listResourceTemplates(cursor ? { cursor } : {}),
|
|
5555
5922
|
(page) => page.resourceTemplates
|
|
5556
5923
|
);
|
|
5557
5924
|
} catch (err) {
|
|
5558
|
-
slot
|
|
5925
|
+
if (!ownsConnectedSlot(ctx, slot, client)) return;
|
|
5559
5926
|
ctx.recordFailure(slot, "protocol", "resource-template-discovery-failed");
|
|
5560
5927
|
ctx.log.warn(`MCP server "${slot.cfg.name}" resource template discovery failed`, err);
|
|
5561
5928
|
}
|
|
5562
|
-
} else {
|
|
5563
|
-
slot.resources = void 0;
|
|
5564
|
-
slot.resourceTemplates = void 0;
|
|
5565
5929
|
}
|
|
5930
|
+
if (!ownsConnectedSlot(ctx, slot, client)) return;
|
|
5566
5931
|
if (capabilities?.prompts) {
|
|
5567
5932
|
try {
|
|
5568
|
-
|
|
5933
|
+
prompts = await collectCatalogPages(
|
|
5569
5934
|
(cursor) => client.listPrompts(cursor ? { cursor } : {}),
|
|
5570
5935
|
(page) => page.prompts
|
|
5571
5936
|
);
|
|
5572
5937
|
} catch (err) {
|
|
5573
|
-
slot
|
|
5938
|
+
if (!ownsConnectedSlot(ctx, slot, client)) return;
|
|
5574
5939
|
ctx.recordFailure(slot, "protocol", "prompt-discovery-failed");
|
|
5575
5940
|
ctx.log.warn(`MCP server "${slot.cfg.name}" prompt discovery failed`, err);
|
|
5576
5941
|
}
|
|
5577
|
-
} else {
|
|
5578
|
-
slot.prompts = void 0;
|
|
5579
5942
|
}
|
|
5943
|
+
if (!ownsConnectedSlot(ctx, slot, client)) return;
|
|
5944
|
+
slot.serverMetadata = serverMetadata;
|
|
5945
|
+
if (slot.catalogVersions?.resources === versions.resources) slot.resources = resources;
|
|
5946
|
+
if (slot.catalogVersions?.resourceTemplates === versions.resourceTemplates) {
|
|
5947
|
+
slot.resourceTemplates = resourceTemplates;
|
|
5948
|
+
}
|
|
5949
|
+
if (slot.catalogVersions?.prompts === versions.prompts) slot.prompts = prompts;
|
|
5580
5950
|
const durationMs = Date.now() - startedAt;
|
|
5581
5951
|
pushBounded(slot.operations.discoverySamples, durationMs, MCP_OPERATION_LIMITS.LATENCY_SAMPLES);
|
|
5582
5952
|
ctx.recordOperation(slot, "discover", "complete", void 0, durationMs, false);
|
|
5583
5953
|
}
|
|
5954
|
+
function ownsConnectedSlot(ctx, slot, client) {
|
|
5955
|
+
return ctx.servers.get(slot.cfg.name) === slot && slot.client === client && slot.state === "connected";
|
|
5956
|
+
}
|
|
5584
5957
|
async function persistSlotCapabilityManifest(cacheDir, slot) {
|
|
5585
5958
|
if (!slot.lazy || !cacheDir) return;
|
|
5586
5959
|
const previous = slot.manifestWrite ?? Promise.resolve();
|
|
@@ -5601,9 +5974,11 @@ async function persistSlotCapabilityManifest(cacheDir, slot) {
|
|
|
5601
5974
|
}
|
|
5602
5975
|
async function attemptConnectSlot(ctx, slot) {
|
|
5603
5976
|
const MAX_ATTEMPTS2 = MCP_CONSTANTS.RECONNECT.MAX_ATTEMPTS;
|
|
5977
|
+
const generation = slot.startupGeneration;
|
|
5978
|
+
const isCurrent = () => ctx.servers.get(slot.cfg.name) === slot && slot.startupGeneration === generation;
|
|
5604
5979
|
let attempt = 0;
|
|
5605
5980
|
while (attempt < MAX_ATTEMPTS2) {
|
|
5606
|
-
if (
|
|
5981
|
+
if (!isCurrent()) {
|
|
5607
5982
|
return;
|
|
5608
5983
|
}
|
|
5609
5984
|
attempt++;
|
|
@@ -5637,7 +6012,7 @@ async function attemptConnectSlot(ctx, slot) {
|
|
|
5637
6012
|
client.addToolsChangedListener(ctx.onToolsChanged);
|
|
5638
6013
|
ctx.addCatalogListeners(client);
|
|
5639
6014
|
await client.connect();
|
|
5640
|
-
if (slot.state === "disconnected" || !
|
|
6015
|
+
if (slot.state === "disconnected" || !isCurrent()) {
|
|
5641
6016
|
client.removeExitListener(ctx.onChildExit);
|
|
5642
6017
|
if (boundDisconnect) client.removeDisconnectListener(boundDisconnect);
|
|
5643
6018
|
client.removeToolsChangedListener(ctx.onToolsChanged);
|
|
@@ -5665,8 +6040,10 @@ async function attemptConnectSlot(ctx, slot) {
|
|
|
5665
6040
|
const discovered = mc.listTools();
|
|
5666
6041
|
slot.discoveredTools = discovered;
|
|
5667
6042
|
await discoverSlotCapabilities(ctx, slot, mc);
|
|
6043
|
+
if (!ownsConnectedSlot(ctx, slot, mc)) return;
|
|
5668
6044
|
await persistSlotCapabilityManifest(ctx.cacheDir, slot);
|
|
5669
|
-
|
|
6045
|
+
if (!ownsConnectedSlot(ctx, slot, mc)) return;
|
|
6046
|
+
applySlotTools(ctx, slot, mc.listTools(), mc);
|
|
5670
6047
|
const durationMs = Date.now() - startedAt;
|
|
5671
6048
|
pushBounded(
|
|
5672
6049
|
slot.operations.connectionSamples,
|
|
@@ -5689,8 +6066,6 @@ async function attemptConnectSlot(ctx, slot) {
|
|
|
5689
6066
|
});
|
|
5690
6067
|
return;
|
|
5691
6068
|
} catch (err) {
|
|
5692
|
-
ctx.recordFailure(slot, "transport", "connect-attempt-failed", Date.now() - startedAt);
|
|
5693
|
-
ctx.log.warn(`MCP server "${slot.cfg.name}" connect attempt ${attempt} failed`, err);
|
|
5694
6069
|
if (client) {
|
|
5695
6070
|
client.removeExitListener(ctx.onChildExit);
|
|
5696
6071
|
if (boundDisconnect) client.removeDisconnectListener(boundDisconnect);
|
|
@@ -5699,6 +6074,9 @@ async function attemptConnectSlot(ctx, slot) {
|
|
|
5699
6074
|
await client.close().catch(() => {
|
|
5700
6075
|
});
|
|
5701
6076
|
}
|
|
6077
|
+
if (!isCurrent() || slot.state === "disconnected") return;
|
|
6078
|
+
ctx.recordFailure(slot, "transport", "connect-attempt-failed", Date.now() - startedAt);
|
|
6079
|
+
ctx.log.warn(`MCP server "${slot.cfg.name}" connect attempt ${attempt} failed`, err);
|
|
5702
6080
|
if (attempt >= MAX_ATTEMPTS2) {
|
|
5703
6081
|
ctx.log.error(
|
|
5704
6082
|
`MCP server "${slot.cfg.name}" connect exhausted after ${MAX_ATTEMPTS2} attempts`,
|
|
@@ -5720,7 +6098,7 @@ async function attemptConnectSlot(ctx, slot) {
|
|
|
5720
6098
|
}
|
|
5721
6099
|
const delay = 500 * 2 ** attempt;
|
|
5722
6100
|
await new Promise((r) => setTimeout(r, delay));
|
|
5723
|
-
if (slot.state === "disconnected" ||
|
|
6101
|
+
if (slot.state === "disconnected" || !isCurrent()) {
|
|
5724
6102
|
return;
|
|
5725
6103
|
}
|
|
5726
6104
|
}
|
|
@@ -5852,7 +6230,7 @@ async function sleepIdleSlot(ctx, slot) {
|
|
|
5852
6230
|
ctx.log.warn(`MCP server "${slot.cfg.name}" error during idle sleep close`, err);
|
|
5853
6231
|
}
|
|
5854
6232
|
}
|
|
5855
|
-
if (slot.client || slot.state !== "dormant") return;
|
|
6233
|
+
if (ctx.servers.get(slot.cfg.name) !== slot || slot.client || slot.state !== "dormant") return;
|
|
5856
6234
|
slot.onDisconnect = void 0;
|
|
5857
6235
|
slot.operations.sleepCount++;
|
|
5858
6236
|
ctx.recordOperation(slot, "sleep", "idle-timeout");
|
|
@@ -6084,7 +6462,9 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
6084
6462
|
async startLazy(slot) {
|
|
6085
6463
|
const cacheDir = expectDefined2(this.cacheDir);
|
|
6086
6464
|
const hash = manifestConfigHash(slot.cfg);
|
|
6465
|
+
const generation = slot.startupGeneration;
|
|
6087
6466
|
const cached = await readCapabilityManifest(cacheDir, slot.cfg.name, hash);
|
|
6467
|
+
if (this.servers.get(slot.cfg.name) !== slot || slot.startupGeneration !== generation) return;
|
|
6088
6468
|
if (cached) {
|
|
6089
6469
|
slot.serverMetadata = cached.serverMetadata;
|
|
6090
6470
|
slot.resources = cached.resources;
|
|
@@ -6103,15 +6483,15 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
6103
6483
|
singleFlightConnect(slot) {
|
|
6104
6484
|
if (slot.client && slot.state === "connected") return Promise.resolve(slot.client);
|
|
6105
6485
|
if (slot.connecting) return slot.connecting;
|
|
6106
|
-
const
|
|
6107
|
-
|
|
6108
|
-
|
|
6109
|
-
|
|
6110
|
-
} finally {
|
|
6111
|
-
slot.connecting = void 0;
|
|
6112
|
-
}
|
|
6113
|
-
})();
|
|
6486
|
+
const generation = slot.startupGeneration;
|
|
6487
|
+
const promise = this.attemptConnect(slot).then(
|
|
6488
|
+
() => slot.startupGeneration === generation ? slot.client : void 0
|
|
6489
|
+
);
|
|
6114
6490
|
slot.connecting = promise;
|
|
6491
|
+
const clearFlight = () => {
|
|
6492
|
+
if (slot.connecting === promise) slot.connecting = void 0;
|
|
6493
|
+
};
|
|
6494
|
+
void promise.then(clearFlight, clearFlight);
|
|
6115
6495
|
return promise;
|
|
6116
6496
|
}
|
|
6117
6497
|
/**
|
|
@@ -6209,6 +6589,7 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
6209
6589
|
async stop(name) {
|
|
6210
6590
|
const slot = this.servers.get(name);
|
|
6211
6591
|
if (!slot) return;
|
|
6592
|
+
slot.startupGeneration = (slot.startupGeneration ?? 0) + 1;
|
|
6212
6593
|
slot.reconnectPending = false;
|
|
6213
6594
|
if (slot.reconnectTimer) {
|
|
6214
6595
|
clearTimeout(slot.reconnectTimer);
|
|
@@ -6260,7 +6641,7 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
6260
6641
|
}
|
|
6261
6642
|
if (slot.state === "dormant") return;
|
|
6262
6643
|
if (slot.operations.inFlightCalls > 0) {
|
|
6263
|
-
throw new Error(`MCP server "${name}" has
|
|
6644
|
+
throw new Error(`MCP server "${name}" has requests in flight \u2014 try again when they finish`);
|
|
6264
6645
|
}
|
|
6265
6646
|
await sleepIdleSlot(this.idleContext(), slot);
|
|
6266
6647
|
}
|
|
@@ -6315,76 +6696,73 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
6315
6696
|
return buildRegistryOperationalHealth(this.servers.values(), this.disabledServers.values());
|
|
6316
6697
|
}
|
|
6317
6698
|
getCatalog(name) {
|
|
6318
|
-
|
|
6319
|
-
if (slot) {
|
|
6320
|
-
return registryCatalogSnapshot(slot);
|
|
6321
|
-
}
|
|
6322
|
-
const disabled = this.disabledServers.get(name);
|
|
6323
|
-
if (disabled) {
|
|
6324
|
-
return {
|
|
6325
|
-
name: disabled.name,
|
|
6326
|
-
state: "idle"
|
|
6327
|
-
};
|
|
6328
|
-
}
|
|
6329
|
-
return void 0;
|
|
6699
|
+
return getCatalog.call(this.registryCatalogOperationsHost(), name);
|
|
6330
6700
|
}
|
|
6331
6701
|
async listResources(name, opts = {}) {
|
|
6332
|
-
|
|
6333
|
-
if (!opts.refresh && slot.resources) return cloneCatalogRecords(slot.resources);
|
|
6334
|
-
const client = await this.ensureConnected(name);
|
|
6335
|
-
if (!client.getServerMetadata()?.capabilities.resources) return [];
|
|
6336
|
-
slot.resources = await collectCatalogPages(
|
|
6337
|
-
(cursor) => client.listResources(cursor ? { cursor } : {}),
|
|
6338
|
-
(page) => page.resources
|
|
6339
|
-
);
|
|
6340
|
-
await this.persistCapabilityManifest(slot);
|
|
6341
|
-
return cloneCatalogRecords(slot.resources);
|
|
6702
|
+
return listResources.call(this.registryCatalogOperationsHost(), name, opts);
|
|
6342
6703
|
}
|
|
6343
6704
|
async listResourceTemplates(name, opts = {}) {
|
|
6344
|
-
|
|
6345
|
-
if (!opts.refresh && slot.resourceTemplates) return cloneCatalogRecords(slot.resourceTemplates);
|
|
6346
|
-
const client = await this.ensureConnected(name);
|
|
6347
|
-
if (!client.getServerMetadata()?.capabilities.resources) return [];
|
|
6348
|
-
slot.resourceTemplates = await collectCatalogPages(
|
|
6349
|
-
(cursor) => client.listResourceTemplates(cursor ? { cursor } : {}),
|
|
6350
|
-
(page) => page.resourceTemplates
|
|
6351
|
-
);
|
|
6352
|
-
await this.persistCapabilityManifest(slot);
|
|
6353
|
-
return cloneCatalogRecords(slot.resourceTemplates);
|
|
6705
|
+
return listResourceTemplates.call(this.registryCatalogOperationsHost(), name, opts);
|
|
6354
6706
|
}
|
|
6355
6707
|
async readResource(name, uri) {
|
|
6356
|
-
return (
|
|
6708
|
+
return readResource.call(this.registryCatalogOperationsHost(), name, uri);
|
|
6357
6709
|
}
|
|
6358
6710
|
async selectResourceForInsertion(name, uri, policy) {
|
|
6359
|
-
return
|
|
6711
|
+
return selectResourceForInsertion.call(
|
|
6712
|
+
this.registryCatalogOperationsHost(),
|
|
6713
|
+
name,
|
|
6714
|
+
uri,
|
|
6715
|
+
policy
|
|
6716
|
+
);
|
|
6360
6717
|
}
|
|
6361
6718
|
async subscribeResource(name, uri) {
|
|
6362
|
-
|
|
6719
|
+
return subscribeResource.call(this.registryCatalogOperationsHost(), name, uri);
|
|
6363
6720
|
}
|
|
6364
6721
|
async unsubscribeResource(name, uri) {
|
|
6365
|
-
|
|
6722
|
+
return unsubscribeResource.call(this.registryCatalogOperationsHost(), name, uri);
|
|
6366
6723
|
}
|
|
6367
6724
|
async listPrompts(name, opts = {}) {
|
|
6368
|
-
|
|
6369
|
-
if (!opts.refresh && slot.prompts) return cloneCatalogRecords(slot.prompts);
|
|
6370
|
-
const client = await this.ensureConnected(name);
|
|
6371
|
-
if (!client.getServerMetadata()?.capabilities.prompts) return [];
|
|
6372
|
-
slot.prompts = await collectCatalogPages(
|
|
6373
|
-
(cursor) => client.listPrompts(cursor ? { cursor } : {}),
|
|
6374
|
-
(page) => page.prompts
|
|
6375
|
-
);
|
|
6376
|
-
await this.persistCapabilityManifest(slot);
|
|
6377
|
-
return cloneCatalogRecords(slot.prompts);
|
|
6725
|
+
return listPrompts.call(this.registryCatalogOperationsHost(), name, opts);
|
|
6378
6726
|
}
|
|
6379
6727
|
async getPrompt(serverName, promptName, args) {
|
|
6380
|
-
return
|
|
6728
|
+
return getPrompt.call(
|
|
6729
|
+
this.registryCatalogOperationsHost(),
|
|
6730
|
+
serverName,
|
|
6731
|
+
promptName,
|
|
6732
|
+
args
|
|
6733
|
+
);
|
|
6734
|
+
}
|
|
6735
|
+
/** Keep all remote requests awake and reject results from superseded clients. */
|
|
6736
|
+
async withConnectedClient(name, run) {
|
|
6737
|
+
const slot = this.requireSlot(name);
|
|
6738
|
+
const generation = slot.startupGeneration;
|
|
6739
|
+
slot.operations.inFlightCalls++;
|
|
6740
|
+
slot.operations.peakInFlightCalls = Math.max(
|
|
6741
|
+
slot.operations.peakInFlightCalls,
|
|
6742
|
+
slot.operations.inFlightCalls
|
|
6743
|
+
);
|
|
6744
|
+
try {
|
|
6745
|
+
const client = await this.ensureConnected(name);
|
|
6746
|
+
const assertCurrent = () => {
|
|
6747
|
+
if (this.servers.get(name) !== slot || slot.startupGeneration !== generation || slot.client !== client || slot.state !== "connected") {
|
|
6748
|
+
throw new Error(`MCP server "${name}" connection changed during request`);
|
|
6749
|
+
}
|
|
6750
|
+
};
|
|
6751
|
+
assertCurrent();
|
|
6752
|
+
const result = await run(client, assertCurrent);
|
|
6753
|
+
assertCurrent();
|
|
6754
|
+
return result;
|
|
6755
|
+
} finally {
|
|
6756
|
+
slot.operations.inFlightCalls = Math.max(0, slot.operations.inFlightCalls - 1);
|
|
6757
|
+
if (slot.startupGeneration === generation) slot.lastUsed = Date.now();
|
|
6758
|
+
}
|
|
6381
6759
|
}
|
|
6382
6760
|
async selectPromptForInsertion(serverName, promptName, args, policy) {
|
|
6383
|
-
return
|
|
6761
|
+
return selectPromptForInsertion.call(
|
|
6762
|
+
this.registryCatalogOperationsHost(),
|
|
6384
6763
|
serverName,
|
|
6385
6764
|
promptName,
|
|
6386
6765
|
args,
|
|
6387
|
-
await this.getPrompt(serverName, promptName, args),
|
|
6388
6766
|
policy
|
|
6389
6767
|
);
|
|
6390
6768
|
}
|
|
@@ -6527,6 +6905,8 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
6527
6905
|
onResourcesChanged = (name) => {
|
|
6528
6906
|
const slot = this.servers.get(name);
|
|
6529
6907
|
if (!slot) return;
|
|
6908
|
+
advanceCatalogVersion(slot, "resources");
|
|
6909
|
+
advanceCatalogVersion(slot, "resourceTemplates");
|
|
6530
6910
|
slot.resources = void 0;
|
|
6531
6911
|
slot.resourceTemplates = void 0;
|
|
6532
6912
|
void this.persistCapabilityManifest(slot);
|
|
@@ -6539,6 +6919,7 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
6539
6919
|
onPromptsChanged = (name) => {
|
|
6540
6920
|
const slot = this.servers.get(name);
|
|
6541
6921
|
if (!slot) return;
|
|
6922
|
+
advanceCatalogVersion(slot, "prompts");
|
|
6542
6923
|
slot.prompts = void 0;
|
|
6543
6924
|
void this.persistCapabilityManifest(slot);
|
|
6544
6925
|
this.log.info(`MCP server "${name}" prompt catalog invalidated`);
|
|
@@ -6632,12 +7013,20 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
6632
7013
|
async attemptConnect(slot) {
|
|
6633
7014
|
return attemptConnectSlot(this.connectContext(), slot);
|
|
6634
7015
|
}
|
|
7016
|
+
registryCatalogOperationsHost() {
|
|
7017
|
+
void this.servers;
|
|
7018
|
+
void this.disabledServers;
|
|
7019
|
+
void this.requireSlot;
|
|
7020
|
+
void this.withConnectedClient;
|
|
7021
|
+
void this.persistCapabilityManifest;
|
|
7022
|
+
void this.readResource;
|
|
7023
|
+
void this.getPrompt;
|
|
7024
|
+
return this;
|
|
7025
|
+
}
|
|
6635
7026
|
};
|
|
6636
7027
|
|
|
6637
|
-
// src/server.ts
|
|
6638
|
-
import {
|
|
6639
|
-
import { createServer as createServer2 } from "node:http";
|
|
6640
|
-
import { expectDefined as expectDefined3, toErrorMessage as toErrorMessage2, validateAgainstSchema as validateAgainstSchema2 } from "@wrongstack/core/utils";
|
|
7028
|
+
// src/server-dispatch.ts
|
|
7029
|
+
import { expectDefined as expectDefined3, toErrorMessage as toErrorMessage3, validateAgainstSchema as validateAgainstSchema2 } from "@wrongstack/core/utils";
|
|
6641
7030
|
var PARSE_ERROR = -32700;
|
|
6642
7031
|
var INVALID_REQUEST = -32600;
|
|
6643
7032
|
var METHOD_NOT_FOUND = -32601;
|
|
@@ -6649,6 +7038,7 @@ var MCPServer = class {
|
|
|
6649
7038
|
logger;
|
|
6650
7039
|
resources;
|
|
6651
7040
|
prompts;
|
|
7041
|
+
instructions;
|
|
6652
7042
|
inFlightRequests = /* @__PURE__ */ new Map();
|
|
6653
7043
|
constructor(opts) {
|
|
6654
7044
|
this.host = opts.host;
|
|
@@ -6657,6 +7047,7 @@ var MCPServer = class {
|
|
|
6657
7047
|
version: MCP_CONSTANTS.CLIENT_INFO.version
|
|
6658
7048
|
};
|
|
6659
7049
|
this.logger = opts.logger;
|
|
7050
|
+
this.instructions = opts.instructions?.trim() || void 0;
|
|
6660
7051
|
this.resources = structuredClone(opts.resources ?? []);
|
|
6661
7052
|
this.prompts = structuredClone(opts.prompts ?? []);
|
|
6662
7053
|
}
|
|
@@ -6701,7 +7092,7 @@ var MCPServer = class {
|
|
|
6701
7092
|
}
|
|
6702
7093
|
return JSON.stringify({ jsonrpc: "2.0", id: msg.id, result });
|
|
6703
7094
|
} catch (err) {
|
|
6704
|
-
const message =
|
|
7095
|
+
const message = toErrorMessage3(err);
|
|
6705
7096
|
this.logger?.warn?.(`MCP server: method "${msg.method}" threw: ${message}`);
|
|
6706
7097
|
const code = err instanceof InvalidToolArgumentsError ? INVALID_PARAMS : INTERNAL_ERROR;
|
|
6707
7098
|
return this.encodeError(requestId, code, message);
|
|
@@ -6723,7 +7114,8 @@ var MCPServer = class {
|
|
|
6723
7114
|
...this.resources.length > 0 ? { resources: { subscribe: false, listChanged: false } } : {},
|
|
6724
7115
|
...this.prompts.length > 0 ? { prompts: { listChanged: false } } : {}
|
|
6725
7116
|
},
|
|
6726
|
-
serverInfo: this.serverInfo
|
|
7117
|
+
serverInfo: this.serverInfo,
|
|
7118
|
+
...this.instructions ? { instructions: this.instructions } : {}
|
|
6727
7119
|
};
|
|
6728
7120
|
case "ping":
|
|
6729
7121
|
return {};
|
|
@@ -6777,7 +7169,7 @@ var MCPServer = class {
|
|
|
6777
7169
|
const input = paramsRecord(params);
|
|
6778
7170
|
const args = stringRecord(input["arguments"], "prompts/get arguments");
|
|
6779
7171
|
for (const argument of prompt.arguments ?? []) {
|
|
6780
|
-
if (argument.required && args
|
|
7172
|
+
if (argument.required && !Object.hasOwn(args, argument.name)) {
|
|
6781
7173
|
throw new Error(`Prompt "${name}" requires argument "${argument.name}"`);
|
|
6782
7174
|
}
|
|
6783
7175
|
}
|
|
@@ -6876,11 +7268,11 @@ function requiredParamString(params, field, method) {
|
|
|
6876
7268
|
return value;
|
|
6877
7269
|
}
|
|
6878
7270
|
function stringRecord(value, label2) {
|
|
6879
|
-
if (value === void 0) return
|
|
7271
|
+
if (value === void 0) return /* @__PURE__ */ Object.create(null);
|
|
6880
7272
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
6881
7273
|
throw new Error(`${label2} must be an object`);
|
|
6882
7274
|
}
|
|
6883
|
-
const result =
|
|
7275
|
+
const result = /* @__PURE__ */ Object.create(null);
|
|
6884
7276
|
for (const [key, item] of Object.entries(value)) {
|
|
6885
7277
|
if (typeof item !== "string") throw new Error(`${label2}.${key} must be a string`);
|
|
6886
7278
|
result[key] = item;
|
|
@@ -6889,7 +7281,7 @@ function stringRecord(value, label2) {
|
|
|
6889
7281
|
}
|
|
6890
7282
|
function renderPromptTemplate(template, args) {
|
|
6891
7283
|
return template.replace(/\{\{([A-Za-z_][A-Za-z0-9_.-]*)\}\}/g, (_match, name) => {
|
|
6892
|
-
const value = args[name];
|
|
7284
|
+
const value = Object.hasOwn(args, name) ? args[name] : void 0;
|
|
6893
7285
|
if (value === void 0) throw new Error(`Missing prompt template argument "${name}"`);
|
|
6894
7286
|
return value;
|
|
6895
7287
|
});
|
|
@@ -6921,6 +7313,14 @@ function stringifyItem(c) {
|
|
|
6921
7313
|
return String(c);
|
|
6922
7314
|
}
|
|
6923
7315
|
}
|
|
7316
|
+
|
|
7317
|
+
// src/server-http.ts
|
|
7318
|
+
import { timingSafeEqual as timingSafeEqual2 } from "node:crypto";
|
|
7319
|
+
import { createServer as createServer2 } from "node:http";
|
|
7320
|
+
import { toErrorMessage as toErrorMessage5 } from "@wrongstack/core/utils";
|
|
7321
|
+
|
|
7322
|
+
// src/server-stdio.ts
|
|
7323
|
+
import { toErrorMessage as toErrorMessage4 } from "@wrongstack/core/utils";
|
|
6924
7324
|
function serveStdio(server, opts = {}) {
|
|
6925
7325
|
const stdin = opts.stdin ?? process.stdin;
|
|
6926
7326
|
const stdout = opts.stdout ?? process.stdout;
|
|
@@ -6938,7 +7338,7 @@ function serveStdio(server, opts = {}) {
|
|
|
6938
7338
|
`, () => resolve());
|
|
6939
7339
|
})
|
|
6940
7340
|
).catch((err) => {
|
|
6941
|
-
const msg =
|
|
7341
|
+
const msg = toErrorMessage4(err);
|
|
6942
7342
|
console.error(
|
|
6943
7343
|
JSON.stringify({
|
|
6944
7344
|
level: "error",
|
|
@@ -6990,7 +7390,7 @@ function serveStdio(server, opts = {}) {
|
|
|
6990
7390
|
JSON.stringify({
|
|
6991
7391
|
level: "error",
|
|
6992
7392
|
event: "mcp_server.handle_message_failed",
|
|
6993
|
-
message:
|
|
7393
|
+
message: toErrorMessage4(err),
|
|
6994
7394
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
6995
7395
|
})
|
|
6996
7396
|
);
|
|
@@ -7025,7 +7425,7 @@ function serveStdio(server, opts = {}) {
|
|
|
7025
7425
|
JSON.stringify({
|
|
7026
7426
|
level: "error",
|
|
7027
7427
|
event: "mcp_server.handle_message_failed",
|
|
7028
|
-
message:
|
|
7428
|
+
message: toErrorMessage4(err),
|
|
7029
7429
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
7030
7430
|
})
|
|
7031
7431
|
);
|
|
@@ -7050,6 +7450,8 @@ function serveStdio(server, opts = {}) {
|
|
|
7050
7450
|
};
|
|
7051
7451
|
}
|
|
7052
7452
|
var HTTP_BODY_CAP = 4 * 1024 * 1024;
|
|
7453
|
+
|
|
7454
|
+
// src/server-http.ts
|
|
7053
7455
|
function isLoopbackHost(host) {
|
|
7054
7456
|
return host === "127.0.0.1" || host === "::1" || host === "localhost";
|
|
7055
7457
|
}
|
|
@@ -7086,7 +7488,7 @@ function serveHttp(server, opts = {}) {
|
|
|
7086
7488
|
});
|
|
7087
7489
|
}
|
|
7088
7490
|
async function handleHttpRequest(server, req, res, token, log, boundHost) {
|
|
7089
|
-
const send = (status,
|
|
7491
|
+
const send = (status, body, type = "application/json") => {
|
|
7090
7492
|
res.writeHead(status, {
|
|
7091
7493
|
"content-type": type,
|
|
7092
7494
|
// This endpoint executes tools. Nothing about it should be embedded,
|
|
@@ -7097,7 +7499,7 @@ async function handleHttpRequest(server, req, res, token, log, boundHost) {
|
|
|
7097
7499
|
// No CORS headers are ever emitted: a cross-origin reader must not be
|
|
7098
7500
|
// able to see a response even if it manages to send the request.
|
|
7099
7501
|
});
|
|
7100
|
-
res.end(
|
|
7502
|
+
res.end(body);
|
|
7101
7503
|
};
|
|
7102
7504
|
if (!originIsAcceptable(req, boundHost)) {
|
|
7103
7505
|
return send(403, JSON.stringify({ error: "cross-origin forbidden" }));
|
|
@@ -7117,13 +7519,13 @@ async function handleHttpRequest(server, req, res, token, log, boundHost) {
|
|
|
7117
7519
|
if (!isJsonContentType(req.headers["content-type"])) {
|
|
7118
7520
|
return send(415, JSON.stringify({ error: "content-type must be application/json" }));
|
|
7119
7521
|
}
|
|
7120
|
-
|
|
7522
|
+
const chunks = [];
|
|
7121
7523
|
let bodyBytes = 0;
|
|
7122
7524
|
let aborted = false;
|
|
7123
7525
|
req.on("data", (chunk) => {
|
|
7124
7526
|
if (aborted) return;
|
|
7125
7527
|
bodyBytes += chunk.byteLength;
|
|
7126
|
-
|
|
7528
|
+
chunks.push(chunk);
|
|
7127
7529
|
if (bodyBytes > HTTP_BODY_CAP) {
|
|
7128
7530
|
aborted = true;
|
|
7129
7531
|
send(413, JSON.stringify({ error: "payload too large" }));
|
|
@@ -7132,11 +7534,12 @@ async function handleHttpRequest(server, req, res, token, log, boundHost) {
|
|
|
7132
7534
|
});
|
|
7133
7535
|
req.on("end", () => {
|
|
7134
7536
|
if (aborted) return;
|
|
7537
|
+
const body = Buffer.concat(chunks, bodyBytes).toString("utf8");
|
|
7135
7538
|
void server.handleMessage(body).then((out) => {
|
|
7136
7539
|
if (out === null) return send(202, "");
|
|
7137
7540
|
return send(200, out);
|
|
7138
7541
|
}).catch((err) => {
|
|
7139
|
-
log?.warn?.(`MCP http handler error: ${
|
|
7542
|
+
log?.warn?.(`MCP http handler error: ${toErrorMessage5(err)}`);
|
|
7140
7543
|
send(500, JSON.stringify({ error: "internal error" }));
|
|
7141
7544
|
});
|
|
7142
7545
|
});
|
|
@@ -7440,7 +7843,7 @@ function emptyFile() {
|
|
|
7440
7843
|
return { version: TOKEN_STORE_VERSION, updatedAt: (/* @__PURE__ */ new Date(0)).toISOString(), entries: [] };
|
|
7441
7844
|
}
|
|
7442
7845
|
function validateStoreFile(value) {
|
|
7443
|
-
if (!
|
|
7846
|
+
if (!isRecord3(value) || value["version"] !== TOKEN_STORE_VERSION || !Array.isArray(value["entries"])) {
|
|
7444
7847
|
throw new Error("MCP token store has an unsupported or malformed structure");
|
|
7445
7848
|
}
|
|
7446
7849
|
if (value["entries"].length > MAX_ENTRIES)
|
|
@@ -7452,7 +7855,7 @@ function validateStoreFile(value) {
|
|
|
7452
7855
|
};
|
|
7453
7856
|
}
|
|
7454
7857
|
function validateEncryptedEntry(value) {
|
|
7455
|
-
if (!
|
|
7858
|
+
if (!isRecord3(value)) throw new Error("MCP token store entry must be an object");
|
|
7456
7859
|
const resource = canonicalMcpResource(boundedString(value["resource"], "resource", 4096));
|
|
7457
7860
|
const authorizationServer = validateMcpAuthorizationServerMetadata(value["authorizationServer"]);
|
|
7458
7861
|
const scopes = stringArray(value["scopes"], "scopes", 128);
|
|
@@ -7497,7 +7900,7 @@ function normalizeStoredAuthorization(value) {
|
|
|
7497
7900
|
updatedAt: boundedString(value.updatedAt, "updatedAt", 128)
|
|
7498
7901
|
};
|
|
7499
7902
|
}
|
|
7500
|
-
function
|
|
7903
|
+
function isRecord3(value) {
|
|
7501
7904
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7502
7905
|
}
|
|
7503
7906
|
function boundedString(value, field, maxLength) {
|