@wrongstack/mcp 1.0.29 → 1.0.33
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 +800 -408
- 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 -181
- 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
|
/**
|
|
@@ -2011,7 +2046,17 @@ async function readBodyCapped(res, maxBytes = MAX_MCP_HTTP_BODY_BYTES) {
|
|
|
2011
2046
|
const safeCap = typeof maxBytes === "number" && Number.isFinite(maxBytes) && maxBytes >= 0 ? maxBytes : MAX_MCP_HTTP_BODY_BYTES;
|
|
2012
2047
|
const body = res.body;
|
|
2013
2048
|
if (!body || typeof body.getReader !== "function") {
|
|
2014
|
-
|
|
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;
|
|
2015
2060
|
}
|
|
2016
2061
|
const reader = body.getReader();
|
|
2017
2062
|
const chunks = [];
|
|
@@ -2568,7 +2613,7 @@ function extractJsonRpcEnvelopes(text) {
|
|
|
2568
2613
|
} catch {
|
|
2569
2614
|
}
|
|
2570
2615
|
};
|
|
2571
|
-
for (const raw of text.split(
|
|
2616
|
+
for (const raw of text.split(/\r\n|\r|\n/)) {
|
|
2572
2617
|
const line = raw.replace(/\r$/, "");
|
|
2573
2618
|
if (line === "") {
|
|
2574
2619
|
flush();
|
|
@@ -2773,6 +2818,8 @@ var SSETransport = class extends BaseHTTPTransport {
|
|
|
2773
2818
|
_nextId = 1;
|
|
2774
2819
|
readerDone = false;
|
|
2775
2820
|
closed = false;
|
|
2821
|
+
toolCatalogVersion = 0;
|
|
2822
|
+
toolCatalogRevision = 0;
|
|
2776
2823
|
readLoopAbort;
|
|
2777
2824
|
reader;
|
|
2778
2825
|
/** POST target announced by the server's `endpoint` event. */
|
|
@@ -2791,10 +2838,12 @@ var SSETransport = class extends BaseHTTPTransport {
|
|
|
2791
2838
|
}
|
|
2792
2839
|
/** Refresh tool list when server sends notifications/tools/list_changed. */
|
|
2793
2840
|
async handleToolsListChanged() {
|
|
2841
|
+
const version = ++this.toolCatalogVersion;
|
|
2794
2842
|
try {
|
|
2795
2843
|
const tools = await listAllTools((params) => this.httpPost("tools/list", params));
|
|
2796
|
-
if (!tools) return;
|
|
2844
|
+
if (!tools || version !== this.toolCatalogVersion) return;
|
|
2797
2845
|
this.tools.splice(0, this.tools.length, ...tools);
|
|
2846
|
+
this.toolCatalogRevision++;
|
|
2798
2847
|
for (const cb of this.toolsChangedListeners) {
|
|
2799
2848
|
try {
|
|
2800
2849
|
cb([...this.tools]);
|
|
@@ -2805,14 +2854,21 @@ var SSETransport = class extends BaseHTTPTransport {
|
|
|
2805
2854
|
}
|
|
2806
2855
|
}
|
|
2807
2856
|
async connect() {
|
|
2857
|
+
this.toolCatalogVersion++;
|
|
2808
2858
|
this.readerDone = false;
|
|
2809
2859
|
this.closed = false;
|
|
2810
2860
|
this.endpointUrl = void 0;
|
|
2811
2861
|
this.state = "connecting";
|
|
2812
2862
|
this.serverMetadata = void 0;
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
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);
|
|
2816
2872
|
try {
|
|
2817
2873
|
const sseUrl = this.buildSSEUrl();
|
|
2818
2874
|
const fetchOpts = {
|
|
@@ -2821,6 +2877,12 @@ var SSETransport = class extends BaseHTTPTransport {
|
|
|
2821
2877
|
};
|
|
2822
2878
|
this.applyTlsAgent(fetchOpts);
|
|
2823
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
|
+
}
|
|
2824
2886
|
if (!response.ok) {
|
|
2825
2887
|
throw new ToolError4({
|
|
2826
2888
|
message: `SSE connect HTTP ${response.status}: ${response.statusText}`,
|
|
@@ -2839,15 +2901,18 @@ var SSETransport = class extends BaseHTTPTransport {
|
|
|
2839
2901
|
}
|
|
2840
2902
|
const textDecoder = new TextDecoder();
|
|
2841
2903
|
const sseReader = new SSEReader();
|
|
2842
|
-
|
|
2904
|
+
const readLoopAbort = new AbortController();
|
|
2905
|
+
this.readLoopAbort = readLoopAbort;
|
|
2843
2906
|
const streamReady = new Promise((resolve) => {
|
|
2844
2907
|
this.streamSignal = resolve;
|
|
2845
2908
|
});
|
|
2846
2909
|
sseReader.onEndpoint((endpoint) => {
|
|
2910
|
+
if (this.abortController !== controller || signal.aborted || this.closed) return;
|
|
2847
2911
|
this.acceptEndpoint(endpoint);
|
|
2848
2912
|
this.streamSignal?.();
|
|
2849
2913
|
});
|
|
2850
2914
|
sseReader.onMessage((msg) => {
|
|
2915
|
+
if (this.abortController !== controller || signal.aborted || this.closed) return;
|
|
2851
2916
|
this.streamSignal?.();
|
|
2852
2917
|
const id = msg.id;
|
|
2853
2918
|
if (msg.method && (typeof id === "number" || typeof id === "string")) {
|
|
@@ -2886,8 +2951,9 @@ var SSETransport = class extends BaseHTTPTransport {
|
|
|
2886
2951
|
cancel: () => reader.cancel(),
|
|
2887
2952
|
releaseLock: () => reader.releaseLock()
|
|
2888
2953
|
};
|
|
2889
|
-
void this.readSSEBody(reader, textDecoder, sseReader);
|
|
2954
|
+
void this.readSSEBody(reader, textDecoder, sseReader, controller, readLoopAbort.signal);
|
|
2890
2955
|
await this.waitForStream(streamReady, signal);
|
|
2956
|
+
assertCurrent();
|
|
2891
2957
|
const initRes = await this.httpPost("initialize", {
|
|
2892
2958
|
protocolVersion: MCP_CONSTANTS.PROTOCOL_VERSION,
|
|
2893
2959
|
// Client capabilities: elicitation when the host can ask its user.
|
|
@@ -2895,6 +2961,7 @@ var SSETransport = class extends BaseHTTPTransport {
|
|
|
2895
2961
|
capabilities: this.serverRequests.capabilities(),
|
|
2896
2962
|
clientInfo: MCP_CONSTANTS.CLIENT_INFO
|
|
2897
2963
|
});
|
|
2964
|
+
assertCurrent();
|
|
2898
2965
|
if (initRes.error) {
|
|
2899
2966
|
throw new ToolError4({
|
|
2900
2967
|
message: `initialize failed: ${initRes.error.message}`,
|
|
@@ -2909,17 +2976,24 @@ var SSETransport = class extends BaseHTTPTransport {
|
|
|
2909
2976
|
await this.httpPost("notifications/initialized", {});
|
|
2910
2977
|
} catch {
|
|
2911
2978
|
}
|
|
2979
|
+
assertCurrent();
|
|
2980
|
+
const revision = this.toolCatalogRevision;
|
|
2912
2981
|
const tools = await listAllTools((params) => this.httpPost("tools/list", params));
|
|
2913
|
-
|
|
2982
|
+
assertCurrent();
|
|
2983
|
+
if (revision === this.toolCatalogRevision) {
|
|
2984
|
+
this.tools.splice(0, this.tools.length, ...tools ?? []);
|
|
2985
|
+
}
|
|
2914
2986
|
this.state = "connected";
|
|
2915
2987
|
clearTimeout(startupTimer);
|
|
2916
2988
|
} catch (err) {
|
|
2917
2989
|
clearTimeout(startupTimer);
|
|
2918
|
-
this.
|
|
2919
|
-
|
|
2990
|
+
if (this.abortController === controller && !this.closed) {
|
|
2991
|
+
this.state = "failed";
|
|
2992
|
+
}
|
|
2993
|
+
controller.abort();
|
|
2920
2994
|
throw err;
|
|
2921
2995
|
} finally {
|
|
2922
|
-
this.streamSignal = void 0;
|
|
2996
|
+
if (this.abortController === controller) this.streamSignal = void 0;
|
|
2923
2997
|
}
|
|
2924
2998
|
}
|
|
2925
2999
|
/** Resolve once the stream announced its endpoint, emitted anything, ended, or the cap passed. */
|
|
@@ -2957,9 +3031,9 @@ var SSETransport = class extends BaseHTTPTransport {
|
|
|
2957
3031
|
} catch {
|
|
2958
3032
|
}
|
|
2959
3033
|
}
|
|
2960
|
-
async readSSEBody(reader, decoder, sseReader) {
|
|
3034
|
+
async readSSEBody(reader, decoder, sseReader, controller, signal) {
|
|
2961
3035
|
try {
|
|
2962
|
-
while (!this.readerDone) {
|
|
3036
|
+
while (!this.readerDone && !signal?.aborted && (!controller || this.abortController === controller) && !this.closed) {
|
|
2963
3037
|
const { done, value } = await reader.read();
|
|
2964
3038
|
if (done) break;
|
|
2965
3039
|
const chunk = decoder.decode(value, { stream: true });
|
|
@@ -2967,11 +3041,13 @@ var SSETransport = class extends BaseHTTPTransport {
|
|
|
2967
3041
|
}
|
|
2968
3042
|
} catch {
|
|
2969
3043
|
} finally {
|
|
2970
|
-
this.
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
this.state
|
|
2974
|
-
|
|
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
|
+
}
|
|
2975
3051
|
}
|
|
2976
3052
|
}
|
|
2977
3053
|
}
|
|
@@ -3117,6 +3193,7 @@ var SSETransport = class extends BaseHTTPTransport {
|
|
|
3117
3193
|
return { jsonrpc: "2.0", id: result.id, result: result.result, error: result.error };
|
|
3118
3194
|
}
|
|
3119
3195
|
async close() {
|
|
3196
|
+
this.toolCatalogVersion++;
|
|
3120
3197
|
this.releasePinnedDispatcher();
|
|
3121
3198
|
const alreadyClosed = this.closed;
|
|
3122
3199
|
this.closed = true;
|
|
@@ -3157,6 +3234,8 @@ function invalidResponse(method, url, err) {
|
|
|
3157
3234
|
var StreamableHTTPTransport = class _StreamableHTTPTransport extends BaseHTTPTransport {
|
|
3158
3235
|
_nextId = 1;
|
|
3159
3236
|
closed = false;
|
|
3237
|
+
toolCatalogVersion = 0;
|
|
3238
|
+
toolCatalogRevision = 0;
|
|
3160
3239
|
sessionId;
|
|
3161
3240
|
constructor(opts) {
|
|
3162
3241
|
super(opts, "StreamableHTTP");
|
|
@@ -3252,10 +3331,12 @@ var StreamableHTTPTransport = class _StreamableHTTPTransport extends BaseHTTPTra
|
|
|
3252
3331
|
}
|
|
3253
3332
|
}
|
|
3254
3333
|
async refreshTools() {
|
|
3334
|
+
const version = ++this.toolCatalogVersion;
|
|
3255
3335
|
try {
|
|
3256
3336
|
const tools = await listAllTools((params) => this.postRaw("tools/list", params));
|
|
3257
|
-
if (!tools) return;
|
|
3337
|
+
if (!tools || version !== this.toolCatalogVersion) return;
|
|
3258
3338
|
this.tools.splice(0, this.tools.length, ...tools);
|
|
3339
|
+
this.toolCatalogRevision++;
|
|
3259
3340
|
for (const listener of this.toolsChangedListeners) {
|
|
3260
3341
|
try {
|
|
3261
3342
|
listener([...tools]);
|
|
@@ -3266,12 +3347,19 @@ var StreamableHTTPTransport = class _StreamableHTTPTransport extends BaseHTTPTra
|
|
|
3266
3347
|
}
|
|
3267
3348
|
}
|
|
3268
3349
|
async connect() {
|
|
3350
|
+
this.toolCatalogVersion++;
|
|
3269
3351
|
this.closed = false;
|
|
3270
3352
|
this.state = "connecting";
|
|
3271
3353
|
this.serverMetadata = void 0;
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
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);
|
|
3275
3363
|
try {
|
|
3276
3364
|
const initializeId = this.genId();
|
|
3277
3365
|
const initFetchOpts = {
|
|
@@ -3297,6 +3385,7 @@ var StreamableHTTPTransport = class _StreamableHTTPTransport extends BaseHTTPTra
|
|
|
3297
3385
|
};
|
|
3298
3386
|
this.applyTlsAgent(initFetchOpts);
|
|
3299
3387
|
const initRes = await this.fetchWithAuthorization(this.url, initFetchOpts, signal);
|
|
3388
|
+
assertCurrent();
|
|
3300
3389
|
if (!initRes.ok) {
|
|
3301
3390
|
throw new Error(`initialize HTTP ${initRes.status}: ${initRes.statusText}`);
|
|
3302
3391
|
}
|
|
@@ -3308,6 +3397,7 @@ var StreamableHTTPTransport = class _StreamableHTTPTransport extends BaseHTTPTra
|
|
|
3308
3397
|
} else {
|
|
3309
3398
|
data = extractJsonRpcResults(await readBodyCapped(initRes))[0];
|
|
3310
3399
|
}
|
|
3400
|
+
assertCurrent();
|
|
3311
3401
|
if (!data) {
|
|
3312
3402
|
throw new Error("Could not parse initialize response");
|
|
3313
3403
|
}
|
|
@@ -3319,14 +3409,21 @@ var StreamableHTTPTransport = class _StreamableHTTPTransport extends BaseHTTPTra
|
|
|
3319
3409
|
this.protocolVersion = this.serverMetadata.protocolVersion;
|
|
3320
3410
|
this.sessionId = initRes.headers.get("mcp-session-id") ?? void 0;
|
|
3321
3411
|
await this.postRaw("notifications/initialized", {});
|
|
3412
|
+
assertCurrent();
|
|
3413
|
+
const revision = this.toolCatalogRevision;
|
|
3322
3414
|
const tools = await listAllTools((params) => this.postRaw("tools/list", params));
|
|
3323
|
-
|
|
3415
|
+
assertCurrent();
|
|
3416
|
+
if (revision === this.toolCatalogRevision) {
|
|
3417
|
+
this.tools.splice(0, this.tools.length, ...tools ?? []);
|
|
3418
|
+
}
|
|
3324
3419
|
this.state = "connected";
|
|
3325
3420
|
clearTimeout(startupTimer);
|
|
3326
3421
|
} catch (err) {
|
|
3327
3422
|
clearTimeout(startupTimer);
|
|
3328
|
-
this.
|
|
3329
|
-
|
|
3423
|
+
if (this.abortController === controller && !this.closed) {
|
|
3424
|
+
this.state = "failed";
|
|
3425
|
+
}
|
|
3426
|
+
controller.abort();
|
|
3330
3427
|
throw err;
|
|
3331
3428
|
}
|
|
3332
3429
|
}
|
|
@@ -3443,6 +3540,7 @@ var StreamableHTTPTransport = class _StreamableHTTPTransport extends BaseHTTPTra
|
|
|
3443
3540
|
return toToolCallResult(res);
|
|
3444
3541
|
}
|
|
3445
3542
|
async close() {
|
|
3543
|
+
this.toolCatalogVersion++;
|
|
3446
3544
|
this.releasePinnedDispatcher();
|
|
3447
3545
|
const alreadyClosed = this.closed;
|
|
3448
3546
|
this.closed = true;
|
|
@@ -3467,9 +3565,17 @@ var StreamableHTTPTransport = class _StreamableHTTPTransport extends BaseHTTPTra
|
|
|
3467
3565
|
}
|
|
3468
3566
|
};
|
|
3469
3567
|
function completeEventsEnd(text) {
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
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;
|
|
3473
3579
|
}
|
|
3474
3580
|
|
|
3475
3581
|
// src/client-http-connection.ts
|
|
@@ -3580,8 +3686,248 @@ async function connectStreamableHTTP(host) {
|
|
|
3580
3686
|
host.state = "connected";
|
|
3581
3687
|
}
|
|
3582
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
|
+
|
|
3583
3929
|
// src/client.ts
|
|
3584
|
-
var MCPClient = class
|
|
3930
|
+
var MCPClient = class {
|
|
3585
3931
|
constructor(opts) {
|
|
3586
3932
|
this.opts = opts;
|
|
3587
3933
|
this.serverRequests = new ServerRequestResponder(opts.elicitation);
|
|
@@ -3591,15 +3937,6 @@ var MCPClient = class _MCPClient {
|
|
|
3591
3937
|
this.requestCapability.bind(this),
|
|
3592
3938
|
this.requireResourceSubscriptions.bind(this)
|
|
3593
3939
|
);
|
|
3594
|
-
/**
|
|
3595
|
-
* Maximum bytes the rx buffer may accumulate before the connection is
|
|
3596
|
-
* forcefully closed. A well-behaved JSON-RPC server emits newline-delimited
|
|
3597
|
-
* messages that are individually much smaller than this; a server that never
|
|
3598
|
-
* sends a newline would grow the buffer without limit and OOM the process.
|
|
3599
|
-
* 16 MiB is generous for any legitimate single message while bounding the
|
|
3600
|
-
* worst-case memory to a predictable cap.
|
|
3601
|
-
*/
|
|
3602
|
-
static MAX_RX_BUFFER_BYTES = 16 * 1024 * 1024;
|
|
3603
3940
|
state = "idle";
|
|
3604
3941
|
child;
|
|
3605
3942
|
nextId = 1;
|
|
@@ -3630,7 +3967,7 @@ var MCPClient = class _MCPClient {
|
|
|
3630
3967
|
* JSON-RPC payload. StringDecoder withholds the partial sequence until the
|
|
3631
3968
|
* chunk that completes it (same remedy as `readFileHead` in core utils).
|
|
3632
3969
|
*/
|
|
3633
|
-
rxDecoder = new
|
|
3970
|
+
rxDecoder = new StringDecoder2("utf8");
|
|
3634
3971
|
_tools = [];
|
|
3635
3972
|
/** Server-declared handshake metadata. Populated for stdio in the first protocol slice. */
|
|
3636
3973
|
_serverMetadata;
|
|
@@ -3640,6 +3977,8 @@ var MCPClient = class _MCPClient {
|
|
|
3640
3977
|
_lastNotifySkipped = false;
|
|
3641
3978
|
closePromise;
|
|
3642
3979
|
connectPromise;
|
|
3980
|
+
toolCatalogVersion = 0;
|
|
3981
|
+
toolCatalogRevision = 0;
|
|
3643
3982
|
// HTTP transports
|
|
3644
3983
|
sseTransport;
|
|
3645
3984
|
httpTransport;
|
|
@@ -3702,6 +4041,7 @@ var MCPClient = class _MCPClient {
|
|
|
3702
4041
|
return this.connectPromise;
|
|
3703
4042
|
}
|
|
3704
4043
|
async connectInner() {
|
|
4044
|
+
this.toolCatalogVersion++;
|
|
3705
4045
|
this.state = "connecting";
|
|
3706
4046
|
this._serverMetadata = void 0;
|
|
3707
4047
|
try {
|
|
@@ -3722,106 +4062,7 @@ var MCPClient = class _MCPClient {
|
|
|
3722
4062
|
}
|
|
3723
4063
|
}
|
|
3724
4064
|
async connectStdio() {
|
|
3725
|
-
|
|
3726
|
-
this.state = "failed";
|
|
3727
|
-
throw new Error('MCP stdio transport requires "command"');
|
|
3728
|
-
}
|
|
3729
|
-
this.rxBuffer = "";
|
|
3730
|
-
this.rxBufferBytes = 0;
|
|
3731
|
-
this.rxDecoder = new StringDecoder("utf8");
|
|
3732
|
-
const extraEnv = { ...this.opts.env };
|
|
3733
|
-
if (this.opts.passthroughEnv) {
|
|
3734
|
-
for (const name of this.opts.passthroughEnv) {
|
|
3735
|
-
const val = process.env[name];
|
|
3736
|
-
if (val !== void 0) {
|
|
3737
|
-
extraEnv[name] = val;
|
|
3738
|
-
}
|
|
3739
|
-
}
|
|
3740
|
-
}
|
|
3741
|
-
const isWin = process.platform === "win32";
|
|
3742
|
-
const rawArgs = this.opts.args ?? [];
|
|
3743
|
-
const spawnEnv = buildChildEnv2({ extra: extraEnv });
|
|
3744
|
-
const stdio = ["pipe", "pipe", "pipe"];
|
|
3745
|
-
const child = isWin ? (() => {
|
|
3746
|
-
const shim = buildWin32CmdShimInvocation(this.opts.command, rawArgs);
|
|
3747
|
-
return spawn2(shim.command, shim.args, {
|
|
3748
|
-
env: spawnEnv,
|
|
3749
|
-
stdio,
|
|
3750
|
-
...this.opts.cwd ? { cwd: this.opts.cwd } : {},
|
|
3751
|
-
windowsVerbatimArguments: shim.windowsVerbatimArguments,
|
|
3752
|
-
// Without this every MCP server spawned from a console-less host
|
|
3753
|
-
// (WebUI server, scheduled runs) opens a visible console window.
|
|
3754
|
-
windowsHide: true
|
|
3755
|
-
});
|
|
3756
|
-
})() : spawn2(this.opts.command, rawArgs, {
|
|
3757
|
-
env: spawnEnv,
|
|
3758
|
-
stdio,
|
|
3759
|
-
windowsHide: true,
|
|
3760
|
-
...this.opts.cwd ? { cwd: this.opts.cwd } : {}
|
|
3761
|
-
});
|
|
3762
|
-
this.child = child;
|
|
3763
|
-
child.stdout?.on("data", (chunk) => this.onData(this.rxDecoder.write(chunk)));
|
|
3764
|
-
child.stdout?.on("end", () => {
|
|
3765
|
-
const tail = this.rxDecoder.end();
|
|
3766
|
-
if (tail) this.onData(tail);
|
|
3767
|
-
if (this.rxBuffer.trim()) {
|
|
3768
|
-
const line = this.rxBuffer.trim();
|
|
3769
|
-
this.rxBuffer = "";
|
|
3770
|
-
this.onLine(line);
|
|
3771
|
-
}
|
|
3772
|
-
});
|
|
3773
|
-
child.stderr?.on("data", () => {
|
|
3774
|
-
});
|
|
3775
|
-
child.stdin?.on("error", (err) => {
|
|
3776
|
-
this.failPending(`MCP "${this.opts.name}" stdin error: ${toErrorMessage(err)}`);
|
|
3777
|
-
});
|
|
3778
|
-
child.on("exit", (code, signal) => {
|
|
3779
|
-
this.state = "disconnected";
|
|
3780
|
-
this.failPending(
|
|
3781
|
-
`MCP "${this.opts.name}" child exited (code=${code ?? "null"} signal=${signal ?? "null"})`
|
|
3782
|
-
);
|
|
3783
|
-
for (const listener of this.exitListeners) {
|
|
3784
|
-
try {
|
|
3785
|
-
listener(this.opts.name, code, signal);
|
|
3786
|
-
} catch {
|
|
3787
|
-
}
|
|
3788
|
-
}
|
|
3789
|
-
});
|
|
3790
|
-
child.on("error", (err) => {
|
|
3791
|
-
this.state = "failed";
|
|
3792
|
-
this.failPending(`MCP "${this.opts.name}" child error: ${toErrorMessage(err)}`);
|
|
3793
|
-
});
|
|
3794
|
-
const initialize = await this.request(
|
|
3795
|
-
"initialize",
|
|
3796
|
-
{
|
|
3797
|
-
protocolVersion: MCP_CONSTANTS.PROTOCOL_VERSION,
|
|
3798
|
-
// Client capabilities: elicitation when the host can ask its user.
|
|
3799
|
-
// `tools` is a SERVER capability and never belonged here.
|
|
3800
|
-
capabilities: this.serverRequests.capabilities(),
|
|
3801
|
-
clientInfo: MCP_CONSTANTS.CLIENT_INFO
|
|
3802
|
-
},
|
|
3803
|
-
typeof this.opts.startupTimeoutMs === "number" && Number.isFinite(this.opts.startupTimeoutMs) && this.opts.startupTimeoutMs > 0 ? this.opts.startupTimeoutMs : 1e4
|
|
3804
|
-
);
|
|
3805
|
-
if (initialize.error) {
|
|
3806
|
-
this.state = "failed";
|
|
3807
|
-
throw new Error(`MCP initialize failed: ${initialize.error.message}`);
|
|
3808
|
-
}
|
|
3809
|
-
try {
|
|
3810
|
-
this._serverMetadata = parseServerMetadata(initialize.result);
|
|
3811
|
-
} catch (err) {
|
|
3812
|
-
this.state = "failed";
|
|
3813
|
-
throw new Error(`MCP initialize returned malformed server metadata: ${toErrorMessage(err)}`);
|
|
3814
|
-
}
|
|
3815
|
-
try {
|
|
3816
|
-
await this.notify("notifications/initialized", {});
|
|
3817
|
-
} catch (err) {
|
|
3818
|
-
console.warn(
|
|
3819
|
-
'[MCP] notify("notifications/initialized") failed for "' + this.opts.name + '": ' + toErrorMessage(err)
|
|
3820
|
-
);
|
|
3821
|
-
}
|
|
3822
|
-
this._tools = await listAllTools((params) => this.request("tools/list", params)) ?? [];
|
|
3823
|
-
this._toolsCache = this._tools;
|
|
3824
|
-
this.state = "connected";
|
|
4065
|
+
return connectStdio(this.clientStdioHost());
|
|
3825
4066
|
}
|
|
3826
4067
|
async connectSSE() {
|
|
3827
4068
|
return connectSSE(this.clientHttpConnectionHost());
|
|
@@ -3878,6 +4119,7 @@ var MCPClient = class _MCPClient {
|
|
|
3878
4119
|
return this.closePromise;
|
|
3879
4120
|
}
|
|
3880
4121
|
async closeInner() {
|
|
4122
|
+
this.toolCatalogVersion++;
|
|
3881
4123
|
if (this.child) {
|
|
3882
4124
|
const child = this.child;
|
|
3883
4125
|
const exitPromise = new Promise((resolve) => {
|
|
@@ -4055,132 +4297,13 @@ var MCPClient = class _MCPClient {
|
|
|
4055
4297
|
this.pending.clear();
|
|
4056
4298
|
}
|
|
4057
4299
|
async notify(method, params) {
|
|
4058
|
-
|
|
4059
|
-
this._lastNotifySkipped = true;
|
|
4060
|
-
console.warn(
|
|
4061
|
-
JSON.stringify({
|
|
4062
|
-
level: "warn",
|
|
4063
|
-
event: "mcp.notify_skipped_backpressure",
|
|
4064
|
-
server: this.opts.name,
|
|
4065
|
-
method,
|
|
4066
|
-
message: "stdin buffer backpressure (already waiting for drain)",
|
|
4067
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4068
|
-
})
|
|
4069
|
-
);
|
|
4070
|
-
return;
|
|
4071
|
-
}
|
|
4072
|
-
const stdin = this.child?.stdin;
|
|
4073
|
-
if (!stdin || stdin.destroyed === true || stdin.writable === false) {
|
|
4074
|
-
return;
|
|
4075
|
-
}
|
|
4076
|
-
const req = { jsonrpc: "2.0", method, params };
|
|
4077
|
-
const encoded = JSON.stringify(req) + "\n";
|
|
4078
|
-
try {
|
|
4079
|
-
const ok = stdin.write(encoded);
|
|
4080
|
-
if (!ok) {
|
|
4081
|
-
this._drainPending = true;
|
|
4082
|
-
await new Promise((resolve, reject) => {
|
|
4083
|
-
const timeout = setTimeout(() => {
|
|
4084
|
-
stdin.removeListener?.("drain", onDrain);
|
|
4085
|
-
stdin.removeListener?.("error", onError);
|
|
4086
|
-
this._drainPending = false;
|
|
4087
|
-
reject(new Error(`MCP notify("${method}") drain timeout`));
|
|
4088
|
-
}, 500);
|
|
4089
|
-
const onDrain = () => {
|
|
4090
|
-
clearTimeout(timeout);
|
|
4091
|
-
stdin.removeListener?.("drain", onDrain);
|
|
4092
|
-
stdin.removeListener?.("error", onError);
|
|
4093
|
-
this._drainPending = false;
|
|
4094
|
-
resolve();
|
|
4095
|
-
};
|
|
4096
|
-
const onError = (err) => {
|
|
4097
|
-
clearTimeout(timeout);
|
|
4098
|
-
stdin.removeListener?.("drain", onDrain);
|
|
4099
|
-
stdin.removeListener?.("error", onError);
|
|
4100
|
-
this._drainPending = false;
|
|
4101
|
-
reject(err);
|
|
4102
|
-
};
|
|
4103
|
-
stdin.once?.("drain", onDrain);
|
|
4104
|
-
stdin.once?.("error", onError);
|
|
4105
|
-
});
|
|
4106
|
-
}
|
|
4107
|
-
} catch (err) {
|
|
4108
|
-
throw new Error(`[MCP] notify("${method}") failed: ${toErrorMessage(err)}`);
|
|
4109
|
-
}
|
|
4300
|
+
return notifyStdio(this.clientStdioHost(), method, params);
|
|
4110
4301
|
}
|
|
4111
4302
|
onData(s) {
|
|
4112
|
-
this.
|
|
4113
|
-
if (this.rxBufferBytes > _MCPClient.MAX_RX_BUFFER_BYTES) {
|
|
4114
|
-
const truncated = this.rxBufferBytes;
|
|
4115
|
-
this.rxParts = [];
|
|
4116
|
-
this.rxBufferBytes = 0;
|
|
4117
|
-
this.failPending(
|
|
4118
|
-
`MCP "${this.opts.name}" rx buffer overflow (${truncated} bytes without a newline) \u2014 closing connection`
|
|
4119
|
-
);
|
|
4120
|
-
void this.close();
|
|
4121
|
-
return;
|
|
4122
|
-
}
|
|
4123
|
-
let start = 0;
|
|
4124
|
-
let idx = s.indexOf("\n");
|
|
4125
|
-
if (idx === -1) {
|
|
4126
|
-
this.rxParts.push(s);
|
|
4127
|
-
return;
|
|
4128
|
-
}
|
|
4129
|
-
while (idx !== -1) {
|
|
4130
|
-
const head = this.rxParts.length > 0 ? this.rxParts.join("") : "";
|
|
4131
|
-
this.rxParts = [];
|
|
4132
|
-
const line = (head + s.slice(start, idx)).trim();
|
|
4133
|
-
start = idx + 1;
|
|
4134
|
-
if (line) this.onLine(line);
|
|
4135
|
-
idx = s.indexOf("\n", start);
|
|
4136
|
-
}
|
|
4137
|
-
const tail = s.slice(start);
|
|
4138
|
-
this.rxBufferBytes = Buffer.byteLength(tail, "utf8");
|
|
4139
|
-
if (tail) this.rxParts.push(tail);
|
|
4303
|
+
receiveStdioData(this.clientStdioProtocolHost(), s);
|
|
4140
4304
|
}
|
|
4141
4305
|
onLine(line) {
|
|
4142
|
-
|
|
4143
|
-
try {
|
|
4144
|
-
msg = JSON.parse(line);
|
|
4145
|
-
} catch {
|
|
4146
|
-
return;
|
|
4147
|
-
}
|
|
4148
|
-
if (typeof msg !== "object" || msg === null) return;
|
|
4149
|
-
const envelope = msg;
|
|
4150
|
-
if (envelope["jsonrpc"] !== "2.0") return;
|
|
4151
|
-
if (typeof envelope["method"] === "string") {
|
|
4152
|
-
const id = envelope["id"];
|
|
4153
|
-
if (typeof id === "number" || typeof id === "string") {
|
|
4154
|
-
void this.handleServerRequest({
|
|
4155
|
-
jsonrpc: "2.0",
|
|
4156
|
-
id,
|
|
4157
|
-
method: envelope["method"],
|
|
4158
|
-
params: envelope["params"]
|
|
4159
|
-
});
|
|
4160
|
-
return;
|
|
4161
|
-
}
|
|
4162
|
-
if (Object.hasOwn(envelope, "id")) return;
|
|
4163
|
-
if (envelope["method"] === "notifications/cancelled") {
|
|
4164
|
-
this.serverRequests.cancel(envelope["params"]);
|
|
4165
|
-
} else if (envelope["method"] === "notifications/tools/list_changed") {
|
|
4166
|
-
void this.handleToolsListChanged();
|
|
4167
|
-
} else if (envelope["method"] === "notifications/resources/list_changed") {
|
|
4168
|
-
this.emitCapabilityChanged("resources");
|
|
4169
|
-
} else if (envelope["method"] === "notifications/prompts/list_changed") {
|
|
4170
|
-
this.emitCapabilityChanged("prompts");
|
|
4171
|
-
} else if (envelope["method"] === "notifications/resources/updated") {
|
|
4172
|
-
const uri = resourceUpdatedUri(envelope["params"]);
|
|
4173
|
-
if (uri) this.emitResourceUpdated(uri);
|
|
4174
|
-
}
|
|
4175
|
-
return;
|
|
4176
|
-
}
|
|
4177
|
-
if (!isJsonRpcResult(msg)) return;
|
|
4178
|
-
const response = msg;
|
|
4179
|
-
if (this.pending.has(response.id)) {
|
|
4180
|
-
const entry = this.pending.get(response.id);
|
|
4181
|
-
this.pending.delete(response.id);
|
|
4182
|
-
entry?.resolve(response);
|
|
4183
|
-
}
|
|
4306
|
+
receiveStdioLine(this.clientStdioProtocolHost(), line);
|
|
4184
4307
|
}
|
|
4185
4308
|
async handleServerRequest(request3) {
|
|
4186
4309
|
const response = await this.serverRequests.answer(request3);
|
|
@@ -4197,11 +4320,13 @@ var MCPClient = class _MCPClient {
|
|
|
4197
4320
|
* to a hard crash on a transient notification glitch.
|
|
4198
4321
|
*/
|
|
4199
4322
|
async handleToolsListChanged() {
|
|
4323
|
+
const version = ++this.toolCatalogVersion;
|
|
4200
4324
|
try {
|
|
4201
4325
|
const tools = await listAllTools((params) => this.request("tools/list", params));
|
|
4202
|
-
if (!tools) return;
|
|
4326
|
+
if (!tools || version !== this.toolCatalogVersion) return;
|
|
4203
4327
|
this._tools = tools;
|
|
4204
4328
|
this._toolsCache = tools;
|
|
4329
|
+
this.toolCatalogRevision++;
|
|
4205
4330
|
for (const listener of this.toolsChangedListeners) {
|
|
4206
4331
|
try {
|
|
4207
4332
|
listener(this.opts.name, [...tools]);
|
|
@@ -4255,9 +4380,7 @@ var MCPClient = class _MCPClient {
|
|
|
4255
4380
|
clientHttpConnectionHost() {
|
|
4256
4381
|
const self = this;
|
|
4257
4382
|
return {
|
|
4258
|
-
|
|
4259
|
-
return self.opts;
|
|
4260
|
-
},
|
|
4383
|
+
opts: this.opts,
|
|
4261
4384
|
get state() {
|
|
4262
4385
|
return self.state;
|
|
4263
4386
|
},
|
|
@@ -4301,6 +4424,109 @@ var MCPClient = class _MCPClient {
|
|
|
4301
4424
|
serverRequests: this.serverRequests
|
|
4302
4425
|
};
|
|
4303
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
|
+
}
|
|
4304
4530
|
};
|
|
4305
4531
|
|
|
4306
4532
|
// src/config-env.ts
|
|
@@ -4321,7 +4547,7 @@ function mcpEnvPlaceholders(cfg) {
|
|
|
4321
4547
|
function expandMcpEnvPlaceholders(cfg, env = process.env) {
|
|
4322
4548
|
const missing = /* @__PURE__ */ new Set();
|
|
4323
4549
|
const expand = (value) => value.replace(PLACEHOLDER, (_match, name, fallback) => {
|
|
4324
|
-
const resolved = env[name];
|
|
4550
|
+
const resolved = Object.hasOwn(env, name) ? env[name] : void 0;
|
|
4325
4551
|
if (resolved !== void 0 && resolved !== "") return resolved;
|
|
4326
4552
|
if (fallback !== void 0) return fallback;
|
|
4327
4553
|
missing.add(name);
|
|
@@ -4680,7 +4906,12 @@ async function elicitViaUserInput(request3, fallback, options = {}) {
|
|
|
4680
4906
|
}
|
|
4681
4907
|
const content = {};
|
|
4682
4908
|
form.fields.forEach((field, i) => {
|
|
4683
|
-
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
|
+
});
|
|
4684
4915
|
});
|
|
4685
4916
|
const checked = validateElicitationContent(form.fields, content);
|
|
4686
4917
|
if (checked.ok) return { action: "accept", content: checked.value };
|
|
@@ -5031,6 +5262,12 @@ function forgetRegistryState(registry, name) {
|
|
|
5031
5262
|
import { createHash as createHash2, randomBytes as randomBytes4 } from "node:crypto";
|
|
5032
5263
|
import * as fs2 from "node:fs/promises";
|
|
5033
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
|
+
}
|
|
5034
5271
|
function manifestConfigHash(cfg) {
|
|
5035
5272
|
const basis = JSON.stringify({
|
|
5036
5273
|
transport: cfg.transport,
|
|
@@ -5066,7 +5303,8 @@ async function readCapabilityManifest(cacheDir, name, configHash) {
|
|
|
5066
5303
|
try {
|
|
5067
5304
|
const raw = await fs2.readFile(manifestFile(cacheDir, name), "utf8");
|
|
5068
5305
|
const parsed = JSON.parse(raw);
|
|
5069
|
-
if (parsed.configHash !== configHash || !Array.isArray(parsed.tools)
|
|
5306
|
+
if (parsed.configHash !== configHash || !Array.isArray(parsed.tools) || !parsed.tools.every(isCachedTool))
|
|
5307
|
+
return null;
|
|
5070
5308
|
return {
|
|
5071
5309
|
tools: parsed.tools,
|
|
5072
5310
|
serverMetadata: parsed.serverMetadata === void 0 ? void 0 : parseServerMetadata(parsed.serverMetadata),
|
|
@@ -5283,6 +5521,13 @@ async function completeRegistryAuthorization(manager, cfg, name, callbackUrl, si
|
|
|
5283
5521
|
// src/registry-catalog.ts
|
|
5284
5522
|
var MAX_CATALOG_PAGES = 100;
|
|
5285
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
|
+
}
|
|
5286
5531
|
async function collectCatalogPages(load, select) {
|
|
5287
5532
|
const items = [];
|
|
5288
5533
|
const seenCursors = /* @__PURE__ */ new Set();
|
|
@@ -5315,10 +5560,105 @@ function registryCatalogSnapshot(slot) {
|
|
|
5315
5560
|
};
|
|
5316
5561
|
}
|
|
5317
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
|
+
|
|
5318
5657
|
// src/registry-connect-loop.ts
|
|
5319
5658
|
import { expectDefined } from "@wrongstack/core/utils";
|
|
5320
5659
|
|
|
5321
5660
|
// src/wrap-tool.ts
|
|
5661
|
+
import { createSandboxMcpGate } from "@wrongstack/core/sandbox";
|
|
5322
5662
|
import { ToolCapabilities } from "@wrongstack/core/security";
|
|
5323
5663
|
import { mcpQualifiedToolName } from "@wrongstack/core/utils";
|
|
5324
5664
|
|
|
@@ -5385,9 +5725,10 @@ function isMutatingTool(mcpTool) {
|
|
|
5385
5725
|
}
|
|
5386
5726
|
return false;
|
|
5387
5727
|
}
|
|
5388
|
-
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 });
|
|
5389
5730
|
const qualifiedName = mcpQualifiedToolName(serverName, mcpTool.name);
|
|
5390
|
-
|
|
5731
|
+
const tool = {
|
|
5391
5732
|
name: qualifiedName,
|
|
5392
5733
|
description: mcpTool.description ?? `${qualifiedName} (MCP tool)`,
|
|
5393
5734
|
usageHint: `Tool provided by MCP server "${serverName}". ${mcpTool.description ?? ""}`,
|
|
@@ -5426,6 +5767,7 @@ function wrapMCPTool(serverName, mcpTool, client, permission = "confirm", observ
|
|
|
5426
5767
|
}
|
|
5427
5768
|
}
|
|
5428
5769
|
};
|
|
5770
|
+
return sandboxGate(tool);
|
|
5429
5771
|
}
|
|
5430
5772
|
function describeUrlOutcomes(serverName, toolName, outcomes) {
|
|
5431
5773
|
const lines = outcomes.map(({ elicitation, action }) => {
|
|
@@ -5490,36 +5832,48 @@ function applySlotTools(ctx, slot, tools, client) {
|
|
|
5490
5832
|
const allowed = slot.cfg.allowedTools;
|
|
5491
5833
|
const filtered = tools.filter((t) => !allowed || allowed.includes(t.name));
|
|
5492
5834
|
const signature = JSON.stringify(
|
|
5493
|
-
filtered.map((t) => [
|
|
5835
|
+
filtered.map((t) => [
|
|
5836
|
+
t.name,
|
|
5837
|
+
t.description ?? null,
|
|
5838
|
+
t.inputSchema ?? null,
|
|
5839
|
+
t.outputSchema ?? null
|
|
5840
|
+
])
|
|
5494
5841
|
);
|
|
5495
5842
|
const lazyWrappersCurrent = slot.lazy && slot.toolSignature === signature && slot.lazyTools.length === filtered.length && (slot.registeredLazy || ctx.lazyMode);
|
|
5496
5843
|
if (lazyWrappersCurrent) return;
|
|
5497
5844
|
const clientArg = slot.lazy ? () => ctx.ensureConnected(slot.cfg.name) : expectDefined(client);
|
|
5498
5845
|
const wrapped = filtered.map(
|
|
5499
|
-
(t) => wrapMCPTool(
|
|
5500
|
-
|
|
5501
|
-
|
|
5502
|
-
|
|
5503
|
-
|
|
5504
|
-
|
|
5505
|
-
|
|
5506
|
-
)
|
|
5507
|
-
|
|
5508
|
-
|
|
5509
|
-
|
|
5510
|
-
|
|
5511
|
-
|
|
5512
|
-
|
|
5513
|
-
|
|
5514
|
-
|
|
5515
|
-
|
|
5516
|
-
|
|
5517
|
-
|
|
5518
|
-
|
|
5519
|
-
|
|
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
|
+
}
|
|
5520
5874
|
}
|
|
5521
5875
|
}
|
|
5522
|
-
|
|
5876
|
+
)
|
|
5523
5877
|
);
|
|
5524
5878
|
slot.lazyTools = wrapped;
|
|
5525
5879
|
slot.toolSignature = signature;
|
|
@@ -5544,51 +5898,62 @@ function applySlotTools(ctx, slot, tools, client) {
|
|
|
5544
5898
|
}
|
|
5545
5899
|
async function discoverSlotCapabilities(ctx, slot, client) {
|
|
5546
5900
|
const startedAt = Date.now();
|
|
5547
|
-
|
|
5548
|
-
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;
|
|
5549
5907
|
if (capabilities?.resources) {
|
|
5550
5908
|
try {
|
|
5551
|
-
|
|
5909
|
+
resources = await collectCatalogPages(
|
|
5552
5910
|
(cursor) => client.listResources(cursor ? { cursor } : {}),
|
|
5553
5911
|
(page) => page.resources
|
|
5554
5912
|
);
|
|
5555
5913
|
} catch (err) {
|
|
5556
|
-
slot
|
|
5914
|
+
if (!ownsConnectedSlot(ctx, slot, client)) return;
|
|
5557
5915
|
ctx.recordFailure(slot, "protocol", "resource-discovery-failed");
|
|
5558
5916
|
ctx.log.warn(`MCP server "${slot.cfg.name}" resource discovery failed`, err);
|
|
5559
5917
|
}
|
|
5918
|
+
if (!ownsConnectedSlot(ctx, slot, client)) return;
|
|
5560
5919
|
try {
|
|
5561
|
-
|
|
5920
|
+
resourceTemplates = await collectCatalogPages(
|
|
5562
5921
|
(cursor) => client.listResourceTemplates(cursor ? { cursor } : {}),
|
|
5563
5922
|
(page) => page.resourceTemplates
|
|
5564
5923
|
);
|
|
5565
5924
|
} catch (err) {
|
|
5566
|
-
slot
|
|
5925
|
+
if (!ownsConnectedSlot(ctx, slot, client)) return;
|
|
5567
5926
|
ctx.recordFailure(slot, "protocol", "resource-template-discovery-failed");
|
|
5568
5927
|
ctx.log.warn(`MCP server "${slot.cfg.name}" resource template discovery failed`, err);
|
|
5569
5928
|
}
|
|
5570
|
-
} else {
|
|
5571
|
-
slot.resources = void 0;
|
|
5572
|
-
slot.resourceTemplates = void 0;
|
|
5573
5929
|
}
|
|
5930
|
+
if (!ownsConnectedSlot(ctx, slot, client)) return;
|
|
5574
5931
|
if (capabilities?.prompts) {
|
|
5575
5932
|
try {
|
|
5576
|
-
|
|
5933
|
+
prompts = await collectCatalogPages(
|
|
5577
5934
|
(cursor) => client.listPrompts(cursor ? { cursor } : {}),
|
|
5578
5935
|
(page) => page.prompts
|
|
5579
5936
|
);
|
|
5580
5937
|
} catch (err) {
|
|
5581
|
-
slot
|
|
5938
|
+
if (!ownsConnectedSlot(ctx, slot, client)) return;
|
|
5582
5939
|
ctx.recordFailure(slot, "protocol", "prompt-discovery-failed");
|
|
5583
5940
|
ctx.log.warn(`MCP server "${slot.cfg.name}" prompt discovery failed`, err);
|
|
5584
5941
|
}
|
|
5585
|
-
} else {
|
|
5586
|
-
slot.prompts = void 0;
|
|
5587
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;
|
|
5588
5950
|
const durationMs = Date.now() - startedAt;
|
|
5589
5951
|
pushBounded(slot.operations.discoverySamples, durationMs, MCP_OPERATION_LIMITS.LATENCY_SAMPLES);
|
|
5590
5952
|
ctx.recordOperation(slot, "discover", "complete", void 0, durationMs, false);
|
|
5591
5953
|
}
|
|
5954
|
+
function ownsConnectedSlot(ctx, slot, client) {
|
|
5955
|
+
return ctx.servers.get(slot.cfg.name) === slot && slot.client === client && slot.state === "connected";
|
|
5956
|
+
}
|
|
5592
5957
|
async function persistSlotCapabilityManifest(cacheDir, slot) {
|
|
5593
5958
|
if (!slot.lazy || !cacheDir) return;
|
|
5594
5959
|
const previous = slot.manifestWrite ?? Promise.resolve();
|
|
@@ -5609,9 +5974,11 @@ async function persistSlotCapabilityManifest(cacheDir, slot) {
|
|
|
5609
5974
|
}
|
|
5610
5975
|
async function attemptConnectSlot(ctx, slot) {
|
|
5611
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;
|
|
5612
5979
|
let attempt = 0;
|
|
5613
5980
|
while (attempt < MAX_ATTEMPTS2) {
|
|
5614
|
-
if (
|
|
5981
|
+
if (!isCurrent()) {
|
|
5615
5982
|
return;
|
|
5616
5983
|
}
|
|
5617
5984
|
attempt++;
|
|
@@ -5645,7 +6012,7 @@ async function attemptConnectSlot(ctx, slot) {
|
|
|
5645
6012
|
client.addToolsChangedListener(ctx.onToolsChanged);
|
|
5646
6013
|
ctx.addCatalogListeners(client);
|
|
5647
6014
|
await client.connect();
|
|
5648
|
-
if (slot.state === "disconnected" || !
|
|
6015
|
+
if (slot.state === "disconnected" || !isCurrent()) {
|
|
5649
6016
|
client.removeExitListener(ctx.onChildExit);
|
|
5650
6017
|
if (boundDisconnect) client.removeDisconnectListener(boundDisconnect);
|
|
5651
6018
|
client.removeToolsChangedListener(ctx.onToolsChanged);
|
|
@@ -5673,8 +6040,10 @@ async function attemptConnectSlot(ctx, slot) {
|
|
|
5673
6040
|
const discovered = mc.listTools();
|
|
5674
6041
|
slot.discoveredTools = discovered;
|
|
5675
6042
|
await discoverSlotCapabilities(ctx, slot, mc);
|
|
6043
|
+
if (!ownsConnectedSlot(ctx, slot, mc)) return;
|
|
5676
6044
|
await persistSlotCapabilityManifest(ctx.cacheDir, slot);
|
|
5677
|
-
|
|
6045
|
+
if (!ownsConnectedSlot(ctx, slot, mc)) return;
|
|
6046
|
+
applySlotTools(ctx, slot, mc.listTools(), mc);
|
|
5678
6047
|
const durationMs = Date.now() - startedAt;
|
|
5679
6048
|
pushBounded(
|
|
5680
6049
|
slot.operations.connectionSamples,
|
|
@@ -5697,8 +6066,6 @@ async function attemptConnectSlot(ctx, slot) {
|
|
|
5697
6066
|
});
|
|
5698
6067
|
return;
|
|
5699
6068
|
} catch (err) {
|
|
5700
|
-
ctx.recordFailure(slot, "transport", "connect-attempt-failed", Date.now() - startedAt);
|
|
5701
|
-
ctx.log.warn(`MCP server "${slot.cfg.name}" connect attempt ${attempt} failed`, err);
|
|
5702
6069
|
if (client) {
|
|
5703
6070
|
client.removeExitListener(ctx.onChildExit);
|
|
5704
6071
|
if (boundDisconnect) client.removeDisconnectListener(boundDisconnect);
|
|
@@ -5707,6 +6074,9 @@ async function attemptConnectSlot(ctx, slot) {
|
|
|
5707
6074
|
await client.close().catch(() => {
|
|
5708
6075
|
});
|
|
5709
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);
|
|
5710
6080
|
if (attempt >= MAX_ATTEMPTS2) {
|
|
5711
6081
|
ctx.log.error(
|
|
5712
6082
|
`MCP server "${slot.cfg.name}" connect exhausted after ${MAX_ATTEMPTS2} attempts`,
|
|
@@ -5728,7 +6098,7 @@ async function attemptConnectSlot(ctx, slot) {
|
|
|
5728
6098
|
}
|
|
5729
6099
|
const delay = 500 * 2 ** attempt;
|
|
5730
6100
|
await new Promise((r) => setTimeout(r, delay));
|
|
5731
|
-
if (slot.state === "disconnected" ||
|
|
6101
|
+
if (slot.state === "disconnected" || !isCurrent()) {
|
|
5732
6102
|
return;
|
|
5733
6103
|
}
|
|
5734
6104
|
}
|
|
@@ -5860,7 +6230,7 @@ async function sleepIdleSlot(ctx, slot) {
|
|
|
5860
6230
|
ctx.log.warn(`MCP server "${slot.cfg.name}" error during idle sleep close`, err);
|
|
5861
6231
|
}
|
|
5862
6232
|
}
|
|
5863
|
-
if (slot.client || slot.state !== "dormant") return;
|
|
6233
|
+
if (ctx.servers.get(slot.cfg.name) !== slot || slot.client || slot.state !== "dormant") return;
|
|
5864
6234
|
slot.onDisconnect = void 0;
|
|
5865
6235
|
slot.operations.sleepCount++;
|
|
5866
6236
|
ctx.recordOperation(slot, "sleep", "idle-timeout");
|
|
@@ -6092,7 +6462,9 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
6092
6462
|
async startLazy(slot) {
|
|
6093
6463
|
const cacheDir = expectDefined2(this.cacheDir);
|
|
6094
6464
|
const hash = manifestConfigHash(slot.cfg);
|
|
6465
|
+
const generation = slot.startupGeneration;
|
|
6095
6466
|
const cached = await readCapabilityManifest(cacheDir, slot.cfg.name, hash);
|
|
6467
|
+
if (this.servers.get(slot.cfg.name) !== slot || slot.startupGeneration !== generation) return;
|
|
6096
6468
|
if (cached) {
|
|
6097
6469
|
slot.serverMetadata = cached.serverMetadata;
|
|
6098
6470
|
slot.resources = cached.resources;
|
|
@@ -6111,15 +6483,15 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
6111
6483
|
singleFlightConnect(slot) {
|
|
6112
6484
|
if (slot.client && slot.state === "connected") return Promise.resolve(slot.client);
|
|
6113
6485
|
if (slot.connecting) return slot.connecting;
|
|
6114
|
-
const
|
|
6115
|
-
|
|
6116
|
-
|
|
6117
|
-
|
|
6118
|
-
} finally {
|
|
6119
|
-
slot.connecting = void 0;
|
|
6120
|
-
}
|
|
6121
|
-
})();
|
|
6486
|
+
const generation = slot.startupGeneration;
|
|
6487
|
+
const promise = this.attemptConnect(slot).then(
|
|
6488
|
+
() => slot.startupGeneration === generation ? slot.client : void 0
|
|
6489
|
+
);
|
|
6122
6490
|
slot.connecting = promise;
|
|
6491
|
+
const clearFlight = () => {
|
|
6492
|
+
if (slot.connecting === promise) slot.connecting = void 0;
|
|
6493
|
+
};
|
|
6494
|
+
void promise.then(clearFlight, clearFlight);
|
|
6123
6495
|
return promise;
|
|
6124
6496
|
}
|
|
6125
6497
|
/**
|
|
@@ -6217,6 +6589,7 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
6217
6589
|
async stop(name) {
|
|
6218
6590
|
const slot = this.servers.get(name);
|
|
6219
6591
|
if (!slot) return;
|
|
6592
|
+
slot.startupGeneration = (slot.startupGeneration ?? 0) + 1;
|
|
6220
6593
|
slot.reconnectPending = false;
|
|
6221
6594
|
if (slot.reconnectTimer) {
|
|
6222
6595
|
clearTimeout(slot.reconnectTimer);
|
|
@@ -6268,7 +6641,7 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
6268
6641
|
}
|
|
6269
6642
|
if (slot.state === "dormant") return;
|
|
6270
6643
|
if (slot.operations.inFlightCalls > 0) {
|
|
6271
|
-
throw new Error(`MCP server "${name}" has
|
|
6644
|
+
throw new Error(`MCP server "${name}" has requests in flight \u2014 try again when they finish`);
|
|
6272
6645
|
}
|
|
6273
6646
|
await sleepIdleSlot(this.idleContext(), slot);
|
|
6274
6647
|
}
|
|
@@ -6323,76 +6696,73 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
6323
6696
|
return buildRegistryOperationalHealth(this.servers.values(), this.disabledServers.values());
|
|
6324
6697
|
}
|
|
6325
6698
|
getCatalog(name) {
|
|
6326
|
-
|
|
6327
|
-
if (slot) {
|
|
6328
|
-
return registryCatalogSnapshot(slot);
|
|
6329
|
-
}
|
|
6330
|
-
const disabled = this.disabledServers.get(name);
|
|
6331
|
-
if (disabled) {
|
|
6332
|
-
return {
|
|
6333
|
-
name: disabled.name,
|
|
6334
|
-
state: "idle"
|
|
6335
|
-
};
|
|
6336
|
-
}
|
|
6337
|
-
return void 0;
|
|
6699
|
+
return getCatalog.call(this.registryCatalogOperationsHost(), name);
|
|
6338
6700
|
}
|
|
6339
6701
|
async listResources(name, opts = {}) {
|
|
6340
|
-
|
|
6341
|
-
if (!opts.refresh && slot.resources) return cloneCatalogRecords(slot.resources);
|
|
6342
|
-
const client = await this.ensureConnected(name);
|
|
6343
|
-
if (!client.getServerMetadata()?.capabilities.resources) return [];
|
|
6344
|
-
slot.resources = await collectCatalogPages(
|
|
6345
|
-
(cursor) => client.listResources(cursor ? { cursor } : {}),
|
|
6346
|
-
(page) => page.resources
|
|
6347
|
-
);
|
|
6348
|
-
await this.persistCapabilityManifest(slot);
|
|
6349
|
-
return cloneCatalogRecords(slot.resources);
|
|
6702
|
+
return listResources.call(this.registryCatalogOperationsHost(), name, opts);
|
|
6350
6703
|
}
|
|
6351
6704
|
async listResourceTemplates(name, opts = {}) {
|
|
6352
|
-
|
|
6353
|
-
if (!opts.refresh && slot.resourceTemplates) return cloneCatalogRecords(slot.resourceTemplates);
|
|
6354
|
-
const client = await this.ensureConnected(name);
|
|
6355
|
-
if (!client.getServerMetadata()?.capabilities.resources) return [];
|
|
6356
|
-
slot.resourceTemplates = await collectCatalogPages(
|
|
6357
|
-
(cursor) => client.listResourceTemplates(cursor ? { cursor } : {}),
|
|
6358
|
-
(page) => page.resourceTemplates
|
|
6359
|
-
);
|
|
6360
|
-
await this.persistCapabilityManifest(slot);
|
|
6361
|
-
return cloneCatalogRecords(slot.resourceTemplates);
|
|
6705
|
+
return listResourceTemplates.call(this.registryCatalogOperationsHost(), name, opts);
|
|
6362
6706
|
}
|
|
6363
6707
|
async readResource(name, uri) {
|
|
6364
|
-
return (
|
|
6708
|
+
return readResource.call(this.registryCatalogOperationsHost(), name, uri);
|
|
6365
6709
|
}
|
|
6366
6710
|
async selectResourceForInsertion(name, uri, policy) {
|
|
6367
|
-
return
|
|
6711
|
+
return selectResourceForInsertion.call(
|
|
6712
|
+
this.registryCatalogOperationsHost(),
|
|
6713
|
+
name,
|
|
6714
|
+
uri,
|
|
6715
|
+
policy
|
|
6716
|
+
);
|
|
6368
6717
|
}
|
|
6369
6718
|
async subscribeResource(name, uri) {
|
|
6370
|
-
|
|
6719
|
+
return subscribeResource.call(this.registryCatalogOperationsHost(), name, uri);
|
|
6371
6720
|
}
|
|
6372
6721
|
async unsubscribeResource(name, uri) {
|
|
6373
|
-
|
|
6722
|
+
return unsubscribeResource.call(this.registryCatalogOperationsHost(), name, uri);
|
|
6374
6723
|
}
|
|
6375
6724
|
async listPrompts(name, opts = {}) {
|
|
6376
|
-
|
|
6377
|
-
if (!opts.refresh && slot.prompts) return cloneCatalogRecords(slot.prompts);
|
|
6378
|
-
const client = await this.ensureConnected(name);
|
|
6379
|
-
if (!client.getServerMetadata()?.capabilities.prompts) return [];
|
|
6380
|
-
slot.prompts = await collectCatalogPages(
|
|
6381
|
-
(cursor) => client.listPrompts(cursor ? { cursor } : {}),
|
|
6382
|
-
(page) => page.prompts
|
|
6383
|
-
);
|
|
6384
|
-
await this.persistCapabilityManifest(slot);
|
|
6385
|
-
return cloneCatalogRecords(slot.prompts);
|
|
6725
|
+
return listPrompts.call(this.registryCatalogOperationsHost(), name, opts);
|
|
6386
6726
|
}
|
|
6387
6727
|
async getPrompt(serverName, promptName, args) {
|
|
6388
|
-
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
|
+
}
|
|
6389
6759
|
}
|
|
6390
6760
|
async selectPromptForInsertion(serverName, promptName, args, policy) {
|
|
6391
|
-
return
|
|
6761
|
+
return selectPromptForInsertion.call(
|
|
6762
|
+
this.registryCatalogOperationsHost(),
|
|
6392
6763
|
serverName,
|
|
6393
6764
|
promptName,
|
|
6394
6765
|
args,
|
|
6395
|
-
await this.getPrompt(serverName, promptName, args),
|
|
6396
6766
|
policy
|
|
6397
6767
|
);
|
|
6398
6768
|
}
|
|
@@ -6535,6 +6905,8 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
6535
6905
|
onResourcesChanged = (name) => {
|
|
6536
6906
|
const slot = this.servers.get(name);
|
|
6537
6907
|
if (!slot) return;
|
|
6908
|
+
advanceCatalogVersion(slot, "resources");
|
|
6909
|
+
advanceCatalogVersion(slot, "resourceTemplates");
|
|
6538
6910
|
slot.resources = void 0;
|
|
6539
6911
|
slot.resourceTemplates = void 0;
|
|
6540
6912
|
void this.persistCapabilityManifest(slot);
|
|
@@ -6547,6 +6919,7 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
6547
6919
|
onPromptsChanged = (name) => {
|
|
6548
6920
|
const slot = this.servers.get(name);
|
|
6549
6921
|
if (!slot) return;
|
|
6922
|
+
advanceCatalogVersion(slot, "prompts");
|
|
6550
6923
|
slot.prompts = void 0;
|
|
6551
6924
|
void this.persistCapabilityManifest(slot);
|
|
6552
6925
|
this.log.info(`MCP server "${name}" prompt catalog invalidated`);
|
|
@@ -6640,12 +7013,20 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
6640
7013
|
async attemptConnect(slot) {
|
|
6641
7014
|
return attemptConnectSlot(this.connectContext(), slot);
|
|
6642
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
|
+
}
|
|
6643
7026
|
};
|
|
6644
7027
|
|
|
6645
|
-
// src/server.ts
|
|
6646
|
-
import {
|
|
6647
|
-
import { createServer as createServer2 } from "node:http";
|
|
6648
|
-
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";
|
|
6649
7030
|
var PARSE_ERROR = -32700;
|
|
6650
7031
|
var INVALID_REQUEST = -32600;
|
|
6651
7032
|
var METHOD_NOT_FOUND = -32601;
|
|
@@ -6711,7 +7092,7 @@ var MCPServer = class {
|
|
|
6711
7092
|
}
|
|
6712
7093
|
return JSON.stringify({ jsonrpc: "2.0", id: msg.id, result });
|
|
6713
7094
|
} catch (err) {
|
|
6714
|
-
const message =
|
|
7095
|
+
const message = toErrorMessage3(err);
|
|
6715
7096
|
this.logger?.warn?.(`MCP server: method "${msg.method}" threw: ${message}`);
|
|
6716
7097
|
const code = err instanceof InvalidToolArgumentsError ? INVALID_PARAMS : INTERNAL_ERROR;
|
|
6717
7098
|
return this.encodeError(requestId, code, message);
|
|
@@ -6932,6 +7313,14 @@ function stringifyItem(c) {
|
|
|
6932
7313
|
return String(c);
|
|
6933
7314
|
}
|
|
6934
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";
|
|
6935
7324
|
function serveStdio(server, opts = {}) {
|
|
6936
7325
|
const stdin = opts.stdin ?? process.stdin;
|
|
6937
7326
|
const stdout = opts.stdout ?? process.stdout;
|
|
@@ -6949,7 +7338,7 @@ function serveStdio(server, opts = {}) {
|
|
|
6949
7338
|
`, () => resolve());
|
|
6950
7339
|
})
|
|
6951
7340
|
).catch((err) => {
|
|
6952
|
-
const msg =
|
|
7341
|
+
const msg = toErrorMessage4(err);
|
|
6953
7342
|
console.error(
|
|
6954
7343
|
JSON.stringify({
|
|
6955
7344
|
level: "error",
|
|
@@ -7001,7 +7390,7 @@ function serveStdio(server, opts = {}) {
|
|
|
7001
7390
|
JSON.stringify({
|
|
7002
7391
|
level: "error",
|
|
7003
7392
|
event: "mcp_server.handle_message_failed",
|
|
7004
|
-
message:
|
|
7393
|
+
message: toErrorMessage4(err),
|
|
7005
7394
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
7006
7395
|
})
|
|
7007
7396
|
);
|
|
@@ -7036,7 +7425,7 @@ function serveStdio(server, opts = {}) {
|
|
|
7036
7425
|
JSON.stringify({
|
|
7037
7426
|
level: "error",
|
|
7038
7427
|
event: "mcp_server.handle_message_failed",
|
|
7039
|
-
message:
|
|
7428
|
+
message: toErrorMessage4(err),
|
|
7040
7429
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
7041
7430
|
})
|
|
7042
7431
|
);
|
|
@@ -7061,6 +7450,8 @@ function serveStdio(server, opts = {}) {
|
|
|
7061
7450
|
};
|
|
7062
7451
|
}
|
|
7063
7452
|
var HTTP_BODY_CAP = 4 * 1024 * 1024;
|
|
7453
|
+
|
|
7454
|
+
// src/server-http.ts
|
|
7064
7455
|
function isLoopbackHost(host) {
|
|
7065
7456
|
return host === "127.0.0.1" || host === "::1" || host === "localhost";
|
|
7066
7457
|
}
|
|
@@ -7097,7 +7488,7 @@ function serveHttp(server, opts = {}) {
|
|
|
7097
7488
|
});
|
|
7098
7489
|
}
|
|
7099
7490
|
async function handleHttpRequest(server, req, res, token, log, boundHost) {
|
|
7100
|
-
const send = (status,
|
|
7491
|
+
const send = (status, body, type = "application/json") => {
|
|
7101
7492
|
res.writeHead(status, {
|
|
7102
7493
|
"content-type": type,
|
|
7103
7494
|
// This endpoint executes tools. Nothing about it should be embedded,
|
|
@@ -7108,7 +7499,7 @@ async function handleHttpRequest(server, req, res, token, log, boundHost) {
|
|
|
7108
7499
|
// No CORS headers are ever emitted: a cross-origin reader must not be
|
|
7109
7500
|
// able to see a response even if it manages to send the request.
|
|
7110
7501
|
});
|
|
7111
|
-
res.end(
|
|
7502
|
+
res.end(body);
|
|
7112
7503
|
};
|
|
7113
7504
|
if (!originIsAcceptable(req, boundHost)) {
|
|
7114
7505
|
return send(403, JSON.stringify({ error: "cross-origin forbidden" }));
|
|
@@ -7128,13 +7519,13 @@ async function handleHttpRequest(server, req, res, token, log, boundHost) {
|
|
|
7128
7519
|
if (!isJsonContentType(req.headers["content-type"])) {
|
|
7129
7520
|
return send(415, JSON.stringify({ error: "content-type must be application/json" }));
|
|
7130
7521
|
}
|
|
7131
|
-
|
|
7522
|
+
const chunks = [];
|
|
7132
7523
|
let bodyBytes = 0;
|
|
7133
7524
|
let aborted = false;
|
|
7134
7525
|
req.on("data", (chunk) => {
|
|
7135
7526
|
if (aborted) return;
|
|
7136
7527
|
bodyBytes += chunk.byteLength;
|
|
7137
|
-
|
|
7528
|
+
chunks.push(chunk);
|
|
7138
7529
|
if (bodyBytes > HTTP_BODY_CAP) {
|
|
7139
7530
|
aborted = true;
|
|
7140
7531
|
send(413, JSON.stringify({ error: "payload too large" }));
|
|
@@ -7143,11 +7534,12 @@ async function handleHttpRequest(server, req, res, token, log, boundHost) {
|
|
|
7143
7534
|
});
|
|
7144
7535
|
req.on("end", () => {
|
|
7145
7536
|
if (aborted) return;
|
|
7537
|
+
const body = Buffer.concat(chunks, bodyBytes).toString("utf8");
|
|
7146
7538
|
void server.handleMessage(body).then((out) => {
|
|
7147
7539
|
if (out === null) return send(202, "");
|
|
7148
7540
|
return send(200, out);
|
|
7149
7541
|
}).catch((err) => {
|
|
7150
|
-
log?.warn?.(`MCP http handler error: ${
|
|
7542
|
+
log?.warn?.(`MCP http handler error: ${toErrorMessage5(err)}`);
|
|
7151
7543
|
send(500, JSON.stringify({ error: "internal error" }));
|
|
7152
7544
|
});
|
|
7153
7545
|
});
|
|
@@ -7451,7 +7843,7 @@ function emptyFile() {
|
|
|
7451
7843
|
return { version: TOKEN_STORE_VERSION, updatedAt: (/* @__PURE__ */ new Date(0)).toISOString(), entries: [] };
|
|
7452
7844
|
}
|
|
7453
7845
|
function validateStoreFile(value) {
|
|
7454
|
-
if (!
|
|
7846
|
+
if (!isRecord3(value) || value["version"] !== TOKEN_STORE_VERSION || !Array.isArray(value["entries"])) {
|
|
7455
7847
|
throw new Error("MCP token store has an unsupported or malformed structure");
|
|
7456
7848
|
}
|
|
7457
7849
|
if (value["entries"].length > MAX_ENTRIES)
|
|
@@ -7463,7 +7855,7 @@ function validateStoreFile(value) {
|
|
|
7463
7855
|
};
|
|
7464
7856
|
}
|
|
7465
7857
|
function validateEncryptedEntry(value) {
|
|
7466
|
-
if (!
|
|
7858
|
+
if (!isRecord3(value)) throw new Error("MCP token store entry must be an object");
|
|
7467
7859
|
const resource = canonicalMcpResource(boundedString(value["resource"], "resource", 4096));
|
|
7468
7860
|
const authorizationServer = validateMcpAuthorizationServerMetadata(value["authorizationServer"]);
|
|
7469
7861
|
const scopes = stringArray(value["scopes"], "scopes", 128);
|
|
@@ -7508,7 +7900,7 @@ function normalizeStoredAuthorization(value) {
|
|
|
7508
7900
|
updatedAt: boundedString(value.updatedAt, "updatedAt", 128)
|
|
7509
7901
|
};
|
|
7510
7902
|
}
|
|
7511
|
-
function
|
|
7903
|
+
function isRecord3(value) {
|
|
7512
7904
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7513
7905
|
}
|
|
7514
7906
|
function boundedString(value, field, maxLength) {
|