@wrongstack/mcp 1.0.10 → 1.0.12
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/authorization.d.ts +6 -0
- package/dist/client.d.ts +16 -0
- package/dist/index.js +443 -192
- package/dist/registry-connect-loop.d.ts +1 -0
- package/dist/registry-slots.d.ts +11 -1
- package/dist/registry-types.d.ts +6 -0
- package/dist/registry.d.ts +31 -1
- package/dist/sse-reader.d.ts +8 -0
- package/dist/token-store.d.ts +2 -0
- package/dist/tool-schema.d.ts +14 -0
- package/dist/transport-jsonrpc.d.ts +6 -0
- package/dist/transport-sse.d.ts +25 -3
- package/dist/transport-streamable.d.ts +1 -0
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -5,6 +5,14 @@ import * as http from "node:http";
|
|
|
5
5
|
import * as https from "node:https";
|
|
6
6
|
import * as net from "node:net";
|
|
7
7
|
import { isPrivateIPv4, isPrivateIPv6 } from "@wrongstack/core/utils";
|
|
8
|
+
var MCPOAuthHttpError = class extends Error {
|
|
9
|
+
constructor(message, status) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.status = status;
|
|
12
|
+
}
|
|
13
|
+
status;
|
|
14
|
+
name = "MCPOAuthHttpError";
|
|
15
|
+
};
|
|
8
16
|
function canonicalMcpResource(rawUrl) {
|
|
9
17
|
let url;
|
|
10
18
|
try {
|
|
@@ -299,7 +307,8 @@ async function exchangeMcpAuthorizationCode(options) {
|
|
|
299
307
|
timeoutMs: options.timeoutMs,
|
|
300
308
|
maxResponseBytes: options.maxResponseBytes,
|
|
301
309
|
lookup: options.lookup,
|
|
302
|
-
allowedLoopbackHostname: loopbackHostnameForResource(resource)
|
|
310
|
+
allowedLoopbackHostname: loopbackHostnameForResource(resource),
|
|
311
|
+
label: "token endpoint"
|
|
303
312
|
});
|
|
304
313
|
if (response === void 0) throw new Error("MCP OAuth token endpoint returned no response");
|
|
305
314
|
return parseTokenResponse(response, resource);
|
|
@@ -321,7 +330,8 @@ async function refreshMcpAccessToken(options) {
|
|
|
321
330
|
timeoutMs: options.timeoutMs,
|
|
322
331
|
maxResponseBytes: options.maxResponseBytes,
|
|
323
332
|
lookup: options.lookup,
|
|
324
|
-
allowedLoopbackHostname: loopbackHostnameForResource(resource)
|
|
333
|
+
allowedLoopbackHostname: loopbackHostnameForResource(resource),
|
|
334
|
+
label: "token endpoint"
|
|
325
335
|
});
|
|
326
336
|
if (response === void 0) throw new Error("MCP OAuth token endpoint returned no response");
|
|
327
337
|
const parsed = parseTokenResponse(response, resource);
|
|
@@ -412,7 +422,8 @@ async function discoverFirst(candidates, fetchJson, signal, parse, label) {
|
|
|
412
422
|
throw new Error(`MCP ${label} discovery failed (${failures.join("; ")})`);
|
|
413
423
|
}
|
|
414
424
|
async function requestPinnedJson(rawUrl, options) {
|
|
415
|
-
const
|
|
425
|
+
const label = options.label ?? "discovery";
|
|
426
|
+
const url = secureOAuthUrl(rawUrl, `${label} URL`);
|
|
416
427
|
const target = await resolvePinnedAddress(url, options);
|
|
417
428
|
const timeoutMs = options.timeoutMs ?? 1e4;
|
|
418
429
|
const maxBytes = options.maxResponseBytes ?? 64 * 1024;
|
|
@@ -456,12 +467,12 @@ async function requestPinnedJson(rawUrl, options) {
|
|
|
456
467
|
}
|
|
457
468
|
if (status >= 300 && status < 400) {
|
|
458
469
|
response.resume();
|
|
459
|
-
finish(new Error(
|
|
470
|
+
finish(new Error(`MCP OAuth ${label} redirects are not allowed`));
|
|
460
471
|
return;
|
|
461
472
|
}
|
|
462
473
|
if (status < 200 || status >= 300) {
|
|
463
474
|
response.resume();
|
|
464
|
-
finish(new
|
|
475
|
+
finish(new MCPOAuthHttpError(`MCP OAuth ${label} HTTP ${status}`, status));
|
|
465
476
|
return;
|
|
466
477
|
}
|
|
467
478
|
const contentType = response.headers["content-type"] ?? "";
|
|
@@ -817,6 +828,7 @@ function boundedServerName(value) {
|
|
|
817
828
|
|
|
818
829
|
// src/client.ts
|
|
819
830
|
import { spawn as spawn2 } from "node:child_process";
|
|
831
|
+
import { StringDecoder } from "node:string_decoder";
|
|
820
832
|
import { buildChildEnv as buildChildEnv2, buildWin32CmdShimInvocation, toErrorMessage } from "@wrongstack/core/utils";
|
|
821
833
|
|
|
822
834
|
// src/client-process.ts
|
|
@@ -1127,6 +1139,34 @@ function parseGetPromptResult(value) {
|
|
|
1127
1139
|
}
|
|
1128
1140
|
|
|
1129
1141
|
// src/tool-schema.ts
|
|
1142
|
+
var MAX_TOOL_PAGES = 100;
|
|
1143
|
+
var MAX_TOOLS = 1e4;
|
|
1144
|
+
async function listAllTools(requestPage) {
|
|
1145
|
+
const tools = [];
|
|
1146
|
+
const seenCursors = /* @__PURE__ */ new Set();
|
|
1147
|
+
let cursor;
|
|
1148
|
+
for (let page = 0; page < MAX_TOOL_PAGES; page++) {
|
|
1149
|
+
let response;
|
|
1150
|
+
try {
|
|
1151
|
+
response = await requestPage(cursor ? { cursor } : {});
|
|
1152
|
+
} catch (err) {
|
|
1153
|
+
if (page === 0) throw err;
|
|
1154
|
+
break;
|
|
1155
|
+
}
|
|
1156
|
+
if (response.error) {
|
|
1157
|
+
if (page === 0) return null;
|
|
1158
|
+
break;
|
|
1159
|
+
}
|
|
1160
|
+
const result = response.result;
|
|
1161
|
+
tools.push(...normalizeMCPTools(result?.tools));
|
|
1162
|
+
const next = result?.nextCursor;
|
|
1163
|
+
if (typeof next !== "string" || next.length === 0) break;
|
|
1164
|
+
if (seenCursors.has(next) || tools.length >= MAX_TOOLS) break;
|
|
1165
|
+
seenCursors.add(next);
|
|
1166
|
+
cursor = next;
|
|
1167
|
+
}
|
|
1168
|
+
return tools.slice(0, MAX_TOOLS);
|
|
1169
|
+
}
|
|
1130
1170
|
function normalizeMCPTools(value) {
|
|
1131
1171
|
if (!Array.isArray(value)) return [];
|
|
1132
1172
|
const tools = [];
|
|
@@ -1164,6 +1204,20 @@ var SSE_READER_MAX_DATA_LINES = 1024;
|
|
|
1164
1204
|
var SSEReader = class {
|
|
1165
1205
|
buffer = "";
|
|
1166
1206
|
dataLines = [];
|
|
1207
|
+
eventName = "";
|
|
1208
|
+
endpointListeners = [];
|
|
1209
|
+
/**
|
|
1210
|
+
* Legacy HTTP+SSE transport: the server's first event is
|
|
1211
|
+
* `event: endpoint` whose data is the (relative) URL to POST requests to.
|
|
1212
|
+
* Its payload is a URL, not JSON, so it is dispatched separately.
|
|
1213
|
+
*/
|
|
1214
|
+
onEndpoint(cb) {
|
|
1215
|
+
this.endpointListeners.push(cb);
|
|
1216
|
+
return () => {
|
|
1217
|
+
const idx = this.endpointListeners.indexOf(cb);
|
|
1218
|
+
if (idx >= 0) this.endpointListeners.splice(idx, 1);
|
|
1219
|
+
};
|
|
1220
|
+
}
|
|
1167
1221
|
listeners = [];
|
|
1168
1222
|
onMessage(cb) {
|
|
1169
1223
|
this.listeners.push(cb);
|
|
@@ -1216,6 +1270,7 @@ var SSEReader = class {
|
|
|
1216
1270
|
let value = colonIdx === -1 ? "" : line.slice(colonIdx + 1);
|
|
1217
1271
|
if (value.startsWith(" ")) value = value.slice(1);
|
|
1218
1272
|
if (field === "event") {
|
|
1273
|
+
this.eventName = value;
|
|
1219
1274
|
} else if (field === "data") {
|
|
1220
1275
|
if (this.dataLines.length >= SSE_READER_MAX_DATA_LINES) {
|
|
1221
1276
|
throw new ToolError({
|
|
@@ -1233,12 +1288,23 @@ var SSEReader = class {
|
|
|
1233
1288
|
}
|
|
1234
1289
|
}
|
|
1235
1290
|
flush() {
|
|
1291
|
+
const eventName = this.eventName;
|
|
1292
|
+
this.eventName = "";
|
|
1236
1293
|
if (this.dataLines.length === 0) {
|
|
1237
1294
|
return;
|
|
1238
1295
|
}
|
|
1239
1296
|
const data = this.dataLines.join("\n").trim();
|
|
1240
1297
|
this.dataLines = [];
|
|
1241
1298
|
if (!data) return;
|
|
1299
|
+
if (eventName === "endpoint") {
|
|
1300
|
+
for (const cb of this.endpointListeners) {
|
|
1301
|
+
try {
|
|
1302
|
+
cb(data);
|
|
1303
|
+
} catch {
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
return;
|
|
1307
|
+
}
|
|
1242
1308
|
try {
|
|
1243
1309
|
const parsed = JSON.parse(data);
|
|
1244
1310
|
this.dispatch(parsed);
|
|
@@ -1256,7 +1322,9 @@ var SSEReader = class {
|
|
|
1256
1322
|
reset() {
|
|
1257
1323
|
this.buffer = "";
|
|
1258
1324
|
this.dataLines = [];
|
|
1325
|
+
this.eventName = "";
|
|
1259
1326
|
this.listeners = [];
|
|
1327
|
+
this.endpointListeners = [];
|
|
1260
1328
|
}
|
|
1261
1329
|
};
|
|
1262
1330
|
|
|
@@ -1688,6 +1756,11 @@ var BaseHTTPTransport = class {
|
|
|
1688
1756
|
|
|
1689
1757
|
// src/transport-jsonrpc.ts
|
|
1690
1758
|
import { ToolError as ToolError2 } from "@wrongstack/core/types";
|
|
1759
|
+
function encodeJsonRpcMessage(id, method, params) {
|
|
1760
|
+
return JSON.stringify(
|
|
1761
|
+
method.startsWith("notifications/") ? { jsonrpc: "2.0", method, params } : { jsonrpc: "2.0", id, method, params }
|
|
1762
|
+
);
|
|
1763
|
+
}
|
|
1691
1764
|
function isJsonRpcResult(v) {
|
|
1692
1765
|
if (typeof v !== "object" || v === null) return false;
|
|
1693
1766
|
const r = v;
|
|
@@ -1813,11 +1886,20 @@ async function readBodyCapped(res, maxBytes = MAX_MCP_HTTP_BODY_BYTES) {
|
|
|
1813
1886
|
}
|
|
1814
1887
|
|
|
1815
1888
|
// src/transport-sse.ts
|
|
1889
|
+
var ENDPOINT_WAIT_MS = 1e3;
|
|
1890
|
+
var SESSION_FATAL_HTTP_STATUSES = /* @__PURE__ */ new Set([401, 403, 404, 410]);
|
|
1816
1891
|
var SSETransport = class extends BaseHTTPTransport {
|
|
1817
1892
|
_nextId = 1;
|
|
1818
1893
|
readerDone = false;
|
|
1894
|
+
closed = false;
|
|
1819
1895
|
readLoopAbort;
|
|
1820
1896
|
reader;
|
|
1897
|
+
/** POST target announced by the server's `endpoint` event. */
|
|
1898
|
+
endpointUrl;
|
|
1899
|
+
/** Wakes connect() once the endpoint is known (or will not arrive). */
|
|
1900
|
+
streamSignal;
|
|
1901
|
+
/** Requests whose response is expected over the event stream, keyed by id. */
|
|
1902
|
+
streamPending = /* @__PURE__ */ new Map();
|
|
1821
1903
|
constructor(opts) {
|
|
1822
1904
|
super(opts, "SSETransport");
|
|
1823
1905
|
}
|
|
@@ -1829,18 +1911,13 @@ var SSETransport = class extends BaseHTTPTransport {
|
|
|
1829
1911
|
/** Refresh tool list when server sends notifications/tools/list_changed. */
|
|
1830
1912
|
async handleToolsListChanged() {
|
|
1831
1913
|
try {
|
|
1832
|
-
const
|
|
1833
|
-
if (!
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
...
|
|
1838
|
-
|
|
1839
|
-
for (const cb of this.toolsChangedListeners) {
|
|
1840
|
-
try {
|
|
1841
|
-
cb([...this.tools]);
|
|
1842
|
-
} catch {
|
|
1843
|
-
}
|
|
1914
|
+
const tools = await listAllTools((params) => this.httpPost("tools/list", params));
|
|
1915
|
+
if (!tools) return;
|
|
1916
|
+
this.tools.splice(0, this.tools.length, ...tools);
|
|
1917
|
+
for (const cb of this.toolsChangedListeners) {
|
|
1918
|
+
try {
|
|
1919
|
+
cb([...this.tools]);
|
|
1920
|
+
} catch {
|
|
1844
1921
|
}
|
|
1845
1922
|
}
|
|
1846
1923
|
} catch {
|
|
@@ -1848,6 +1925,8 @@ var SSETransport = class extends BaseHTTPTransport {
|
|
|
1848
1925
|
}
|
|
1849
1926
|
async connect() {
|
|
1850
1927
|
this.readerDone = false;
|
|
1928
|
+
this.closed = false;
|
|
1929
|
+
this.endpointUrl = void 0;
|
|
1851
1930
|
this.state = "connecting";
|
|
1852
1931
|
this.serverMetadata = void 0;
|
|
1853
1932
|
this.abortController = new AbortController();
|
|
@@ -1856,7 +1935,7 @@ var SSETransport = class extends BaseHTTPTransport {
|
|
|
1856
1935
|
try {
|
|
1857
1936
|
const sseUrl = this.buildSSEUrl();
|
|
1858
1937
|
const fetchOpts = {
|
|
1859
|
-
headers: this.headers,
|
|
1938
|
+
headers: { Accept: "text/event-stream", ...this.headers },
|
|
1860
1939
|
signal
|
|
1861
1940
|
};
|
|
1862
1941
|
this.applyTlsAgent(fetchOpts);
|
|
@@ -1880,7 +1959,15 @@ var SSETransport = class extends BaseHTTPTransport {
|
|
|
1880
1959
|
const textDecoder = new TextDecoder();
|
|
1881
1960
|
const sseReader = new SSEReader();
|
|
1882
1961
|
this.readLoopAbort = new AbortController();
|
|
1962
|
+
const streamReady = new Promise((resolve) => {
|
|
1963
|
+
this.streamSignal = resolve;
|
|
1964
|
+
});
|
|
1965
|
+
sseReader.onEndpoint((endpoint) => {
|
|
1966
|
+
this.acceptEndpoint(endpoint);
|
|
1967
|
+
this.streamSignal?.();
|
|
1968
|
+
});
|
|
1883
1969
|
sseReader.onMessage((msg) => {
|
|
1970
|
+
this.streamSignal?.();
|
|
1884
1971
|
if (msg.method && !msg.id) {
|
|
1885
1972
|
if (msg.method === "notifications/tools/list_changed") {
|
|
1886
1973
|
void this.handleToolsListChanged();
|
|
@@ -1889,6 +1976,14 @@ var SSETransport = class extends BaseHTTPTransport {
|
|
|
1889
1976
|
} else if (msg.method === "notifications/prompts/list_changed") {
|
|
1890
1977
|
this.notifyPromptsChanged();
|
|
1891
1978
|
}
|
|
1979
|
+
return;
|
|
1980
|
+
}
|
|
1981
|
+
if (!msg.method && isJsonRpcResult(msg)) {
|
|
1982
|
+
const pending = this.streamPending.get(msg.id);
|
|
1983
|
+
if (pending) {
|
|
1984
|
+
this.streamPending.delete(msg.id);
|
|
1985
|
+
pending.resolve(msg);
|
|
1986
|
+
}
|
|
1892
1987
|
}
|
|
1893
1988
|
});
|
|
1894
1989
|
const reader = response.body.getReader();
|
|
@@ -1896,10 +1991,13 @@ var SSETransport = class extends BaseHTTPTransport {
|
|
|
1896
1991
|
cancel: () => reader.cancel(),
|
|
1897
1992
|
releaseLock: () => reader.releaseLock()
|
|
1898
1993
|
};
|
|
1899
|
-
this.readSSEBody(reader, textDecoder, sseReader);
|
|
1994
|
+
void this.readSSEBody(reader, textDecoder, sseReader);
|
|
1995
|
+
await this.waitForStream(streamReady, signal);
|
|
1900
1996
|
const initRes = await this.httpPost("initialize", {
|
|
1901
1997
|
protocolVersion: MCP_CONSTANTS.PROTOCOL_VERSION,
|
|
1902
|
-
capabilities
|
|
1998
|
+
// Client capabilities (roots/sampling/elicitation) — none are offered.
|
|
1999
|
+
// `tools` is a SERVER capability and never belonged here.
|
|
2000
|
+
capabilities: {},
|
|
1903
2001
|
clientInfo: MCP_CONSTANTS.CLIENT_INFO
|
|
1904
2002
|
});
|
|
1905
2003
|
if (initRes.error) {
|
|
@@ -1916,13 +2014,8 @@ var SSETransport = class extends BaseHTTPTransport {
|
|
|
1916
2014
|
await this.httpPost("notifications/initialized", {});
|
|
1917
2015
|
} catch {
|
|
1918
2016
|
}
|
|
1919
|
-
const
|
|
1920
|
-
|
|
1921
|
-
this.tools.splice(0, this.tools.length);
|
|
1922
|
-
} else {
|
|
1923
|
-
const result = toolsRes.result;
|
|
1924
|
-
this.tools.splice(0, this.tools.length, ...normalizeMCPTools(result?.tools));
|
|
1925
|
-
}
|
|
2017
|
+
const tools = await listAllTools((params) => this.httpPost("tools/list", params));
|
|
2018
|
+
this.tools.splice(0, this.tools.length, ...tools ?? []);
|
|
1926
2019
|
this.state = "connected";
|
|
1927
2020
|
clearTimeout(startupTimer);
|
|
1928
2021
|
} catch (err) {
|
|
@@ -1930,6 +2023,43 @@ var SSETransport = class extends BaseHTTPTransport {
|
|
|
1930
2023
|
this.state = "failed";
|
|
1931
2024
|
this.abortController.abort();
|
|
1932
2025
|
throw err;
|
|
2026
|
+
} finally {
|
|
2027
|
+
this.streamSignal = void 0;
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
/** Resolve once the stream announced its endpoint, emitted anything, ended, or the cap passed. */
|
|
2031
|
+
async waitForStream(ready, signal) {
|
|
2032
|
+
let timer;
|
|
2033
|
+
let onAbort;
|
|
2034
|
+
try {
|
|
2035
|
+
await Promise.race([
|
|
2036
|
+
ready,
|
|
2037
|
+
new Promise((resolve) => {
|
|
2038
|
+
timer = setTimeout(resolve, Math.min(ENDPOINT_WAIT_MS, this.timeout));
|
|
2039
|
+
timer.unref?.();
|
|
2040
|
+
}),
|
|
2041
|
+
new Promise((resolve) => {
|
|
2042
|
+
onAbort = resolve;
|
|
2043
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
2044
|
+
})
|
|
2045
|
+
]);
|
|
2046
|
+
} finally {
|
|
2047
|
+
clearTimeout(timer);
|
|
2048
|
+
if (onAbort) signal.removeEventListener("abort", onAbort);
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
/**
|
|
2052
|
+
* Adopt the server's POST endpoint. It must stay on the configured origin:
|
|
2053
|
+
* a cross-origin endpoint would move every request — Authorization header
|
|
2054
|
+
* included — to a host that never passed configuration review.
|
|
2055
|
+
*/
|
|
2056
|
+
acceptEndpoint(endpoint) {
|
|
2057
|
+
try {
|
|
2058
|
+
const next = new URL(endpoint, this.url);
|
|
2059
|
+
if (next.origin !== new URL(this.url).origin) return;
|
|
2060
|
+
validateTransportUrl(next.toString());
|
|
2061
|
+
this.endpointUrl = next.toString();
|
|
2062
|
+
} catch {
|
|
1933
2063
|
}
|
|
1934
2064
|
}
|
|
1935
2065
|
async readSSEBody(reader, decoder, sseReader) {
|
|
@@ -1942,12 +2072,21 @@ var SSETransport = class extends BaseHTTPTransport {
|
|
|
1942
2072
|
}
|
|
1943
2073
|
} catch {
|
|
1944
2074
|
} finally {
|
|
2075
|
+
this.streamSignal?.();
|
|
2076
|
+
this.rejectStreamPending("SSE stream closed");
|
|
1945
2077
|
if (!this.readerDone && this.state !== "disconnected" && this.state !== "failed") {
|
|
1946
2078
|
this.state = "disconnected";
|
|
1947
2079
|
this.notifyDisconnect();
|
|
1948
2080
|
}
|
|
1949
2081
|
}
|
|
1950
2082
|
}
|
|
2083
|
+
rejectStreamPending(reason) {
|
|
2084
|
+
if (this.streamPending.size === 0) return;
|
|
2085
|
+
const err = new Error(`MCP "${this.name}": ${reason}`);
|
|
2086
|
+
const pending = [...this.streamPending.values()];
|
|
2087
|
+
this.streamPending.clear();
|
|
2088
|
+
for (const entry of pending) entry.reject(err);
|
|
2089
|
+
}
|
|
1951
2090
|
buildSSEUrl() {
|
|
1952
2091
|
try {
|
|
1953
2092
|
const url = new URL(this.url);
|
|
@@ -1957,12 +2096,37 @@ var SSETransport = class extends BaseHTTPTransport {
|
|
|
1957
2096
|
return this.url;
|
|
1958
2097
|
}
|
|
1959
2098
|
}
|
|
1960
|
-
|
|
2099
|
+
/** Register interest in a stream-delivered response before the POST goes out. */
|
|
2100
|
+
awaitStreamResponse(id, signal) {
|
|
2101
|
+
const promise = new Promise((resolve, reject) => {
|
|
2102
|
+
const onAbort = () => reject(signal.reason instanceof Error ? signal.reason : new Error("MCP request aborted"));
|
|
2103
|
+
this.streamPending.set(id, {
|
|
2104
|
+
resolve: (result) => {
|
|
2105
|
+
signal.removeEventListener("abort", onAbort);
|
|
2106
|
+
resolve(result);
|
|
2107
|
+
},
|
|
2108
|
+
reject: (err) => {
|
|
2109
|
+
signal.removeEventListener("abort", onAbort);
|
|
2110
|
+
reject(err);
|
|
2111
|
+
}
|
|
2112
|
+
});
|
|
2113
|
+
if (signal.aborted) onAbort();
|
|
2114
|
+
else signal.addEventListener("abort", onAbort, { once: true });
|
|
2115
|
+
});
|
|
2116
|
+
promise.catch(() => void 0);
|
|
2117
|
+
return promise;
|
|
2118
|
+
}
|
|
2119
|
+
httpPost(method, params, opts) {
|
|
2120
|
+
return this.postJsonRpc(method, params, this.requestTimeout, opts);
|
|
2121
|
+
}
|
|
2122
|
+
async postJsonRpc(method, params, timeoutMs, opts) {
|
|
1961
2123
|
const id = this.genId();
|
|
1962
|
-
const
|
|
2124
|
+
const isNotification = method.startsWith("notifications/");
|
|
2125
|
+
const body = encodeJsonRpcMessage(id, method, params);
|
|
1963
2126
|
const external = opts?.signal;
|
|
1964
2127
|
const parent = external && this.abortController ? AbortSignal.any([this.abortController.signal, external]) : external ?? this.abortController?.signal;
|
|
1965
|
-
const timeoutSignal = createTimeoutSignal(parent,
|
|
2128
|
+
const timeoutSignal = createTimeoutSignal(parent, timeoutMs);
|
|
2129
|
+
const streamed = isNotification ? void 0 : this.awaitStreamResponse(id, timeoutSignal.signal);
|
|
1966
2130
|
const fetchOpts = {
|
|
1967
2131
|
method: "POST",
|
|
1968
2132
|
headers: {
|
|
@@ -1974,8 +2138,19 @@ var SSETransport = class extends BaseHTTPTransport {
|
|
|
1974
2138
|
};
|
|
1975
2139
|
this.applyTlsAgent(fetchOpts);
|
|
1976
2140
|
try {
|
|
1977
|
-
|
|
2141
|
+
let res;
|
|
2142
|
+
try {
|
|
2143
|
+
res = await this.fetchWithAuthorization(
|
|
2144
|
+
this.endpointUrl ?? this.url,
|
|
2145
|
+
fetchOpts,
|
|
2146
|
+
timeoutSignal.signal
|
|
2147
|
+
);
|
|
2148
|
+
} catch (err) {
|
|
2149
|
+
if (!timeoutSignal.signal.aborted) this.markDisconnected();
|
|
2150
|
+
throw err;
|
|
2151
|
+
}
|
|
1978
2152
|
if (!res.ok) {
|
|
2153
|
+
if (SESSION_FATAL_HTTP_STATUSES.has(res.status)) this.markDisconnected();
|
|
1979
2154
|
let snippet;
|
|
1980
2155
|
try {
|
|
1981
2156
|
snippet = await readBodyCapped(res, MCP_CONSTANTS.REQUEST_LOG_CAP);
|
|
@@ -1990,25 +2165,28 @@ var SSETransport = class extends BaseHTTPTransport {
|
|
|
1990
2165
|
context: { transport: "sse", url: this.url, status: res.status }
|
|
1991
2166
|
});
|
|
1992
2167
|
}
|
|
1993
|
-
if (
|
|
2168
|
+
if (isNotification || !streamed) {
|
|
1994
2169
|
await readBodyCapped(res).catch(() => void 0);
|
|
1995
2170
|
return { jsonrpc: "2.0", id };
|
|
1996
2171
|
}
|
|
2172
|
+
let text;
|
|
2173
|
+
try {
|
|
2174
|
+
text = await readBodyCapped(res);
|
|
2175
|
+
} catch (err) {
|
|
2176
|
+
throw invalidResponse(method, this.url, err);
|
|
2177
|
+
}
|
|
2178
|
+
if (res.status === 202 || text.trim() === "") {
|
|
2179
|
+
return await streamed;
|
|
2180
|
+
}
|
|
1997
2181
|
let data;
|
|
1998
2182
|
try {
|
|
1999
|
-
data = JSON.parse(
|
|
2183
|
+
data = JSON.parse(text);
|
|
2000
2184
|
} catch (err) {
|
|
2001
|
-
throw
|
|
2002
|
-
message: `Invalid JSON-RPC response: ${err instanceof Error ? err.message : "parse failed"}`,
|
|
2003
|
-
code: "TOOL_EXECUTION_FAILED",
|
|
2004
|
-
toolName: method,
|
|
2005
|
-
context: { transport: "sse", url: this.url, phase: "parse-json" },
|
|
2006
|
-
cause: err
|
|
2007
|
-
});
|
|
2185
|
+
throw invalidResponse(method, this.url, err);
|
|
2008
2186
|
}
|
|
2009
2187
|
return assertMatchingJsonRpcResult(data, id, method);
|
|
2010
2188
|
} catch (err) {
|
|
2011
|
-
if (external?.aborted && !
|
|
2189
|
+
if (external?.aborted && !isNotification) {
|
|
2012
2190
|
void this.httpPost("notifications/cancelled", {
|
|
2013
2191
|
requestId: id,
|
|
2014
2192
|
reason: "client aborted"
|
|
@@ -2016,9 +2194,13 @@ var SSETransport = class extends BaseHTTPTransport {
|
|
|
2016
2194
|
});
|
|
2017
2195
|
throw makeAbortError(method);
|
|
2018
2196
|
}
|
|
2019
|
-
this.markDisconnected();
|
|
2020
2197
|
throw err;
|
|
2021
2198
|
} finally {
|
|
2199
|
+
const pending = this.streamPending.get(id);
|
|
2200
|
+
if (pending) {
|
|
2201
|
+
this.streamPending.delete(id);
|
|
2202
|
+
pending.reject(new Error("MCP request settled"));
|
|
2203
|
+
}
|
|
2022
2204
|
timeoutSignal.dispose();
|
|
2023
2205
|
}
|
|
2024
2206
|
}
|
|
@@ -2043,73 +2225,16 @@ var SSETransport = class extends BaseHTTPTransport {
|
|
|
2043
2225
|
}
|
|
2044
2226
|
/** Generic JSON-RPC request — used by MCPClient.request() for SSE transports. */
|
|
2045
2227
|
async request(method, params, timeoutMs, opts) {
|
|
2046
|
-
const
|
|
2047
|
-
|
|
2048
|
-
const external = opts?.signal;
|
|
2049
|
-
const parent = external && this.abortController ? AbortSignal.any([this.abortController.signal, external]) : external ?? this.abortController?.signal;
|
|
2050
|
-
const timeoutSignal = createTimeoutSignal(parent, timeoutMs ?? this.requestTimeout);
|
|
2051
|
-
const fetchOpts = {
|
|
2052
|
-
method: "POST",
|
|
2053
|
-
headers: {
|
|
2054
|
-
"Content-Type": "application/json",
|
|
2055
|
-
...this.headers
|
|
2056
|
-
},
|
|
2057
|
-
body,
|
|
2058
|
-
signal: timeoutSignal.signal
|
|
2059
|
-
};
|
|
2060
|
-
this.applyTlsAgent(fetchOpts);
|
|
2061
|
-
try {
|
|
2062
|
-
const res = await this.fetchWithAuthorization(this.url, fetchOpts, timeoutSignal.signal);
|
|
2063
|
-
if (!res.ok) {
|
|
2064
|
-
throw new ToolError4({
|
|
2065
|
-
message: `HTTP ${res.status}: ${res.statusText}`,
|
|
2066
|
-
code: "TOOL_EXECUTION_FAILED",
|
|
2067
|
-
toolName: method,
|
|
2068
|
-
context: {
|
|
2069
|
-
transport: "sse",
|
|
2070
|
-
url: this.url,
|
|
2071
|
-
status: res.status,
|
|
2072
|
-
statusText: res.statusText
|
|
2073
|
-
}
|
|
2074
|
-
});
|
|
2075
|
-
}
|
|
2076
|
-
if (method.startsWith("notifications/")) {
|
|
2077
|
-
await readBodyCapped(res).catch(() => void 0);
|
|
2078
|
-
return { jsonrpc: "2.0", id };
|
|
2079
|
-
}
|
|
2080
|
-
let data;
|
|
2081
|
-
try {
|
|
2082
|
-
data = JSON.parse(await readBodyCapped(res));
|
|
2083
|
-
} catch (err) {
|
|
2084
|
-
throw new ToolError4({
|
|
2085
|
-
message: `Invalid JSON-RPC response: ${err instanceof Error ? err.message : "parse failed"}`,
|
|
2086
|
-
code: "TOOL_EXECUTION_FAILED",
|
|
2087
|
-
toolName: method,
|
|
2088
|
-
context: { transport: "sse", url: this.url, phase: "parse-json" },
|
|
2089
|
-
cause: err
|
|
2090
|
-
});
|
|
2091
|
-
}
|
|
2092
|
-
const result = assertMatchingJsonRpcResult(data, id, method);
|
|
2093
|
-
return { jsonrpc: "2.0", id, result: result.result, error: result.error };
|
|
2094
|
-
} catch (err) {
|
|
2095
|
-
if (external?.aborted && !method.startsWith("notifications/")) {
|
|
2096
|
-
void this.httpPost("notifications/cancelled", {
|
|
2097
|
-
requestId: id,
|
|
2098
|
-
reason: "client aborted"
|
|
2099
|
-
}).catch(() => {
|
|
2100
|
-
});
|
|
2101
|
-
throw makeAbortError(method);
|
|
2102
|
-
}
|
|
2103
|
-
this.markDisconnected();
|
|
2104
|
-
throw err;
|
|
2105
|
-
} finally {
|
|
2106
|
-
timeoutSignal.dispose();
|
|
2107
|
-
}
|
|
2228
|
+
const result = await this.postJsonRpc(method, params, timeoutMs ?? this.requestTimeout, opts);
|
|
2229
|
+
return { jsonrpc: "2.0", id: result.id, result: result.result, error: result.error };
|
|
2108
2230
|
}
|
|
2109
2231
|
async close() {
|
|
2110
2232
|
this.releasePinnedDispatcher();
|
|
2111
|
-
|
|
2233
|
+
const alreadyClosed = this.closed;
|
|
2234
|
+
this.closed = true;
|
|
2112
2235
|
this.readerDone = true;
|
|
2236
|
+
this.state = "disconnected";
|
|
2237
|
+
if (alreadyClosed) return;
|
|
2113
2238
|
this.readLoopAbort?.abort();
|
|
2114
2239
|
try {
|
|
2115
2240
|
this.reader?.cancel();
|
|
@@ -2120,8 +2245,8 @@ var SSETransport = class extends BaseHTTPTransport {
|
|
|
2120
2245
|
} catch {
|
|
2121
2246
|
}
|
|
2122
2247
|
this.abortController?.abort();
|
|
2248
|
+
this.rejectStreamPending("transport closed");
|
|
2123
2249
|
this.disconnectHandlers.splice(0, this.disconnectHandlers.length);
|
|
2124
|
-
this.state = "disconnected";
|
|
2125
2250
|
}
|
|
2126
2251
|
markDisconnected() {
|
|
2127
2252
|
if (this.state === "connected") {
|
|
@@ -2130,10 +2255,20 @@ var SSETransport = class extends BaseHTTPTransport {
|
|
|
2130
2255
|
}
|
|
2131
2256
|
}
|
|
2132
2257
|
};
|
|
2258
|
+
function invalidResponse(method, url, err) {
|
|
2259
|
+
return new ToolError4({
|
|
2260
|
+
message: `Invalid JSON-RPC response: ${err instanceof Error ? err.message : "parse failed"}`,
|
|
2261
|
+
code: "TOOL_EXECUTION_FAILED",
|
|
2262
|
+
toolName: method,
|
|
2263
|
+
context: { transport: "sse", url, phase: "parse-json" },
|
|
2264
|
+
cause: err
|
|
2265
|
+
});
|
|
2266
|
+
}
|
|
2133
2267
|
|
|
2134
2268
|
// src/transport-streamable.ts
|
|
2135
2269
|
var StreamableHTTPTransport = class _StreamableHTTPTransport extends BaseHTTPTransport {
|
|
2136
2270
|
_nextId = 1;
|
|
2271
|
+
closed = false;
|
|
2137
2272
|
sessionId;
|
|
2138
2273
|
constructor(opts) {
|
|
2139
2274
|
super(opts, "StreamableHTTP");
|
|
@@ -2164,11 +2299,8 @@ var StreamableHTTPTransport = class _StreamableHTTPTransport extends BaseHTTPTra
|
|
|
2164
2299
|
}
|
|
2165
2300
|
async refreshTools() {
|
|
2166
2301
|
try {
|
|
2167
|
-
const
|
|
2168
|
-
if (
|
|
2169
|
-
const tools = normalizeMCPTools(
|
|
2170
|
-
response.result?.tools
|
|
2171
|
-
);
|
|
2302
|
+
const tools = await listAllTools((params) => this.postRaw("tools/list", params));
|
|
2303
|
+
if (!tools) return;
|
|
2172
2304
|
this.tools.splice(0, this.tools.length, ...tools);
|
|
2173
2305
|
for (const listener of this.toolsChangedListeners) {
|
|
2174
2306
|
try {
|
|
@@ -2180,6 +2312,7 @@ var StreamableHTTPTransport = class _StreamableHTTPTransport extends BaseHTTPTra
|
|
|
2180
2312
|
}
|
|
2181
2313
|
}
|
|
2182
2314
|
async connect() {
|
|
2315
|
+
this.closed = false;
|
|
2183
2316
|
this.state = "connecting";
|
|
2184
2317
|
this.serverMetadata = void 0;
|
|
2185
2318
|
this.abortController = new AbortController();
|
|
@@ -2200,7 +2333,8 @@ var StreamableHTTPTransport = class _StreamableHTTPTransport extends BaseHTTPTra
|
|
|
2200
2333
|
method: "initialize",
|
|
2201
2334
|
params: {
|
|
2202
2335
|
protocolVersion: MCP_CONSTANTS.PROTOCOL_VERSION,
|
|
2203
|
-
capabilities
|
|
2336
|
+
// Client capabilities — none offered (`tools` is a server capability).
|
|
2337
|
+
capabilities: {},
|
|
2204
2338
|
clientInfo: MCP_CONSTANTS.CLIENT_INFO
|
|
2205
2339
|
}
|
|
2206
2340
|
}),
|
|
@@ -2230,13 +2364,8 @@ var StreamableHTTPTransport = class _StreamableHTTPTransport extends BaseHTTPTra
|
|
|
2230
2364
|
this.protocolVersion = this.serverMetadata.protocolVersion;
|
|
2231
2365
|
this.sessionId = initRes.headers.get("mcp-session-id") ?? void 0;
|
|
2232
2366
|
await this.postRaw("notifications/initialized", {});
|
|
2233
|
-
const
|
|
2234
|
-
|
|
2235
|
-
this.tools.splice(0, this.tools.length);
|
|
2236
|
-
} else {
|
|
2237
|
-
const result = toolsRes.result;
|
|
2238
|
-
this.tools.splice(0, this.tools.length, ...normalizeMCPTools(result?.tools));
|
|
2239
|
-
}
|
|
2367
|
+
const tools = await listAllTools((params) => this.postRaw("tools/list", params));
|
|
2368
|
+
this.tools.splice(0, this.tools.length, ...tools ?? []);
|
|
2240
2369
|
this.state = "connected";
|
|
2241
2370
|
clearTimeout(startupTimer);
|
|
2242
2371
|
} catch (err) {
|
|
@@ -2248,7 +2377,7 @@ var StreamableHTTPTransport = class _StreamableHTTPTransport extends BaseHTTPTra
|
|
|
2248
2377
|
}
|
|
2249
2378
|
async postRaw(method, params, opts) {
|
|
2250
2379
|
const id = this.genId();
|
|
2251
|
-
const body =
|
|
2380
|
+
const body = encodeJsonRpcMessage(id, method, params);
|
|
2252
2381
|
const external = opts?.signal;
|
|
2253
2382
|
const parent = external && this.abortController ? AbortSignal.any([this.abortController.signal, external]) : external ?? this.abortController?.signal;
|
|
2254
2383
|
const timeoutSignal = createTimeoutSignal(parent, this.requestTimeout);
|
|
@@ -2298,7 +2427,7 @@ var StreamableHTTPTransport = class _StreamableHTTPTransport extends BaseHTTPTra
|
|
|
2298
2427
|
/** Generic JSON-RPC request — used by MCPClient.request() for SSE/streamable-http transports. */
|
|
2299
2428
|
async request(method, params, timeoutMs, opts) {
|
|
2300
2429
|
const id = this.genId();
|
|
2301
|
-
const body =
|
|
2430
|
+
const body = encodeJsonRpcMessage(id, method, params);
|
|
2302
2431
|
const external = opts?.signal;
|
|
2303
2432
|
const parent = external && this.abortController ? AbortSignal.any([this.abortController.signal, external]) : external ?? this.abortController?.signal;
|
|
2304
2433
|
const timeoutSignal = createTimeoutSignal(parent, timeoutMs ?? this.requestTimeout);
|
|
@@ -2328,11 +2457,12 @@ var StreamableHTTPTransport = class _StreamableHTTPTransport extends BaseHTTPTra
|
|
|
2328
2457
|
}
|
|
2329
2458
|
const parsed = this.consumeResponseText(await readBodyCapped(res), id);
|
|
2330
2459
|
if (parsed) {
|
|
2460
|
+
const matched = assertMatchingJsonRpcResult(parsed, id, method);
|
|
2331
2461
|
return {
|
|
2332
2462
|
jsonrpc: "2.0",
|
|
2333
2463
|
id,
|
|
2334
|
-
result:
|
|
2335
|
-
error:
|
|
2464
|
+
result: matched.result,
|
|
2465
|
+
error: matched.error
|
|
2336
2466
|
};
|
|
2337
2467
|
}
|
|
2338
2468
|
throw new Error("Could not parse response as JSON-RPC");
|
|
@@ -2366,8 +2496,10 @@ var StreamableHTTPTransport = class _StreamableHTTPTransport extends BaseHTTPTra
|
|
|
2366
2496
|
}
|
|
2367
2497
|
async close() {
|
|
2368
2498
|
this.releasePinnedDispatcher();
|
|
2369
|
-
|
|
2499
|
+
const alreadyClosed = this.closed;
|
|
2500
|
+
this.closed = true;
|
|
2370
2501
|
this.state = "disconnected";
|
|
2502
|
+
if (alreadyClosed) return;
|
|
2371
2503
|
this.abortController?.abort();
|
|
2372
2504
|
this.disconnectHandlers.splice(0, this.disconnectHandlers.length);
|
|
2373
2505
|
}
|
|
@@ -2413,6 +2545,14 @@ var MCPClient = class _MCPClient {
|
|
|
2413
2545
|
pending = /* @__PURE__ */ new Map();
|
|
2414
2546
|
rxBuffer = "";
|
|
2415
2547
|
rxBufferBytes = 0;
|
|
2548
|
+
/**
|
|
2549
|
+
* Incremental UTF-8 decoder for the stdio rx path. A pipe read boundary can
|
|
2550
|
+
* land inside a multi-byte sequence; decoding each chunk with
|
|
2551
|
+
* `chunk.toString()` would replace it with U+FFFD and silently corrupt the
|
|
2552
|
+
* JSON-RPC payload. StringDecoder withholds the partial sequence until the
|
|
2553
|
+
* chunk that completes it (same remedy as `readFileHead` in core utils).
|
|
2554
|
+
*/
|
|
2555
|
+
rxDecoder = new StringDecoder("utf8");
|
|
2416
2556
|
_tools = [];
|
|
2417
2557
|
/** Server-declared handshake metadata. Populated for stdio in the first protocol slice. */
|
|
2418
2558
|
_serverMetadata;
|
|
@@ -2507,6 +2647,7 @@ var MCPClient = class _MCPClient {
|
|
|
2507
2647
|
}
|
|
2508
2648
|
this.rxBuffer = "";
|
|
2509
2649
|
this.rxBufferBytes = 0;
|
|
2650
|
+
this.rxDecoder = new StringDecoder("utf8");
|
|
2510
2651
|
const extraEnv = { ...this.opts.env };
|
|
2511
2652
|
if (this.opts.passthroughEnv) {
|
|
2512
2653
|
for (const name of this.opts.passthroughEnv) {
|
|
@@ -2525,15 +2666,23 @@ var MCPClient = class _MCPClient {
|
|
|
2525
2666
|
return spawn2(shim.command, shim.args, {
|
|
2526
2667
|
env: spawnEnv,
|
|
2527
2668
|
stdio,
|
|
2669
|
+
...this.opts.cwd ? { cwd: this.opts.cwd } : {},
|
|
2528
2670
|
windowsVerbatimArguments: shim.windowsVerbatimArguments,
|
|
2529
2671
|
// Without this every MCP server spawned from a console-less host
|
|
2530
2672
|
// (WebUI server, scheduled runs) opens a visible console window.
|
|
2531
2673
|
windowsHide: true
|
|
2532
2674
|
});
|
|
2533
|
-
})() : spawn2(this.opts.command, rawArgs, {
|
|
2675
|
+
})() : spawn2(this.opts.command, rawArgs, {
|
|
2676
|
+
env: spawnEnv,
|
|
2677
|
+
stdio,
|
|
2678
|
+
windowsHide: true,
|
|
2679
|
+
...this.opts.cwd ? { cwd: this.opts.cwd } : {}
|
|
2680
|
+
});
|
|
2534
2681
|
this.child = child;
|
|
2535
|
-
child.stdout?.on("data", (chunk) => this.onData(
|
|
2682
|
+
child.stdout?.on("data", (chunk) => this.onData(this.rxDecoder.write(chunk)));
|
|
2536
2683
|
child.stdout?.on("end", () => {
|
|
2684
|
+
const tail = this.rxDecoder.end();
|
|
2685
|
+
if (tail) this.onData(tail);
|
|
2537
2686
|
if (this.rxBuffer.trim()) {
|
|
2538
2687
|
const line = this.rxBuffer.trim();
|
|
2539
2688
|
this.rxBuffer = "";
|
|
@@ -2565,7 +2714,9 @@ var MCPClient = class _MCPClient {
|
|
|
2565
2714
|
"initialize",
|
|
2566
2715
|
{
|
|
2567
2716
|
protocolVersion: MCP_CONSTANTS.PROTOCOL_VERSION,
|
|
2568
|
-
capabilities
|
|
2717
|
+
// Client capabilities (roots/sampling/elicitation) — none offered.
|
|
2718
|
+
// `tools` is a SERVER capability and never belonged here.
|
|
2719
|
+
capabilities: {},
|
|
2569
2720
|
clientInfo: MCP_CONSTANTS.CLIENT_INFO
|
|
2570
2721
|
},
|
|
2571
2722
|
this.opts.startupTimeoutMs ?? 1e4
|
|
@@ -2587,13 +2738,7 @@ var MCPClient = class _MCPClient {
|
|
|
2587
2738
|
'[MCP] notify("notifications/initialized") failed for "' + this.opts.name + '": ' + toErrorMessage(err)
|
|
2588
2739
|
);
|
|
2589
2740
|
}
|
|
2590
|
-
|
|
2591
|
-
if (toolsRes.error) {
|
|
2592
|
-
this._tools = [];
|
|
2593
|
-
} else {
|
|
2594
|
-
const result = toolsRes.result;
|
|
2595
|
-
this._tools = normalizeMCPTools(result?.tools);
|
|
2596
|
-
}
|
|
2741
|
+
this._tools = await listAllTools((params) => this.request("tools/list", params)) ?? [];
|
|
2597
2742
|
this._toolsCache = this._tools;
|
|
2598
2743
|
this.state = "connected";
|
|
2599
2744
|
}
|
|
@@ -2843,8 +2988,7 @@ var MCPClient = class _MCPClient {
|
|
|
2843
2988
|
this.child = void 0;
|
|
2844
2989
|
}
|
|
2845
2990
|
this.failPending(`MCP "${this.opts.name}" closed`);
|
|
2846
|
-
this.sseTransport?.close();
|
|
2847
|
-
this.httpTransport?.close();
|
|
2991
|
+
await Promise.allSettled([this.sseTransport?.close(), this.httpTransport?.close()]);
|
|
2848
2992
|
this.state = "disconnected";
|
|
2849
2993
|
}
|
|
2850
2994
|
request(method, params, timeoutMs = this.opts.requestTimeoutMs ?? 6e4, opts) {
|
|
@@ -3085,11 +3229,13 @@ var MCPClient = class _MCPClient {
|
|
|
3085
3229
|
}
|
|
3086
3230
|
}
|
|
3087
3231
|
handleServerRequest(request3) {
|
|
3088
|
-
const
|
|
3089
|
-
const response = {
|
|
3232
|
+
const response = request3.method === "ping" ? { jsonrpc: "2.0", id: request3.id, result: {} } : {
|
|
3090
3233
|
jsonrpc: "2.0",
|
|
3091
3234
|
id: request3.id,
|
|
3092
|
-
error: {
|
|
3235
|
+
error: {
|
|
3236
|
+
code: -32601,
|
|
3237
|
+
message: request3.method === "sampling/createMessage" ? "Client sampling is disabled by policy" : `Method not found: ${request3.method}`
|
|
3238
|
+
}
|
|
3093
3239
|
};
|
|
3094
3240
|
try {
|
|
3095
3241
|
this.child?.stdin?.write(`${JSON.stringify(response)}
|
|
@@ -3105,10 +3251,8 @@ var MCPClient = class _MCPClient {
|
|
|
3105
3251
|
*/
|
|
3106
3252
|
async handleToolsListChanged() {
|
|
3107
3253
|
try {
|
|
3108
|
-
const
|
|
3109
|
-
|
|
3110
|
-
toolsRes.result?.tools
|
|
3111
|
-
);
|
|
3254
|
+
const tools = await listAllTools((params) => this.request("tools/list", params));
|
|
3255
|
+
if (!tools) return;
|
|
3112
3256
|
this._tools = tools;
|
|
3113
3257
|
this._toolsCache = tools;
|
|
3114
3258
|
for (const listener of this.toolsChangedListeners) {
|
|
@@ -3446,7 +3590,7 @@ async function updateMcp(input, deps) {
|
|
|
3446
3590
|
servers[input.name] = cfg;
|
|
3447
3591
|
await persist(deps.configPath, full, servers);
|
|
3448
3592
|
if (cfg.enabled !== false) {
|
|
3449
|
-
return startServer(input.name, cfg, deps, `Server "${input.name}" updated
|
|
3593
|
+
return startServer(input.name, cfg, deps, `Server "${input.name}" updated`);
|
|
3450
3594
|
}
|
|
3451
3595
|
await safeStop(input.name, deps);
|
|
3452
3596
|
trackDisabled(deps.registry, cfg);
|
|
@@ -3483,7 +3627,7 @@ async function enableMcp(name, deps) {
|
|
|
3483
3627
|
cfg.enabled = true;
|
|
3484
3628
|
servers[name] = cfg;
|
|
3485
3629
|
await persist(deps.configPath, full, servers);
|
|
3486
|
-
return startServer(name, cfg, deps, `Server "${name}" enabled
|
|
3630
|
+
return startServer(name, cfg, deps, `Server "${name}" enabled`);
|
|
3487
3631
|
}
|
|
3488
3632
|
async function disableMcp(name, deps) {
|
|
3489
3633
|
if (!name) return { ok: false, message: "Server name is required" };
|
|
@@ -3526,12 +3670,19 @@ async function restartMcp(name, deps) {
|
|
|
3526
3670
|
}
|
|
3527
3671
|
const cfg = servers[name];
|
|
3528
3672
|
if (!cfg) return { ok: false, message: `Server "${name}" is not in config.` };
|
|
3529
|
-
return startServer(name, { ...cfg, name }, deps, `Server "${name}" started
|
|
3673
|
+
return startServer(name, { ...cfg, name }, deps, `Server "${name}" started`);
|
|
3530
3674
|
}
|
|
3531
3675
|
async function discoverMcp(name, deps) {
|
|
3532
3676
|
if (!name) return { ok: false, message: "Server name is required" };
|
|
3533
3677
|
const result = await restartMcp(name, deps);
|
|
3534
3678
|
if (!result.ok) return result;
|
|
3679
|
+
if (liveState(name, deps.registry).state === "dormant") {
|
|
3680
|
+
try {
|
|
3681
|
+
await deps.registry.ensureConnected(name);
|
|
3682
|
+
} catch (err) {
|
|
3683
|
+
return { ok: false, message: `Failed to discover "${name}": ${errMessage(err)}` };
|
|
3684
|
+
}
|
|
3685
|
+
}
|
|
3535
3686
|
const { state, tools } = liveState(name, deps.registry);
|
|
3536
3687
|
return {
|
|
3537
3688
|
ok: true,
|
|
@@ -3540,15 +3691,13 @@ async function discoverMcp(name, deps) {
|
|
|
3540
3691
|
tools
|
|
3541
3692
|
};
|
|
3542
3693
|
}
|
|
3543
|
-
async function startServer(name, cfg, deps, okMessage
|
|
3694
|
+
async function startServer(name, cfg, deps, okMessage) {
|
|
3544
3695
|
try {
|
|
3545
3696
|
const alreadyRegistered = deps.registry.list().some((s) => s.name === name);
|
|
3546
|
-
if (alreadyRegistered
|
|
3547
|
-
await deps.registry.restart(name);
|
|
3548
|
-
} else if (alreadyRegistered) {
|
|
3549
|
-
await deps.registry.restart(name);
|
|
3697
|
+
if (alreadyRegistered) {
|
|
3698
|
+
await deps.registry.restart(name, { ...cfg, name, enabled: true });
|
|
3550
3699
|
} else {
|
|
3551
|
-
await deps.registry.start({ ...cfg, enabled: true });
|
|
3700
|
+
await deps.registry.start({ ...cfg, name, enabled: true });
|
|
3552
3701
|
}
|
|
3553
3702
|
const { state, tools } = liveState(name, deps.registry);
|
|
3554
3703
|
return {
|
|
@@ -3905,14 +4054,37 @@ function wrapMCPTool(serverName, mcpTool, client, permission = "confirm", observ
|
|
|
3905
4054
|
}
|
|
3906
4055
|
};
|
|
3907
4056
|
}
|
|
4057
|
+
function renderContentBlock(item) {
|
|
4058
|
+
const type = item["type"];
|
|
4059
|
+
if (type === "text") return typeof item["text"] === "string" ? item["text"] : "";
|
|
4060
|
+
if (type === "image" || type === "audio") {
|
|
4061
|
+
const mime = typeof item["mimeType"] === "string" ? item["mimeType"] : "unknown type";
|
|
4062
|
+
const data = typeof item["data"] === "string" ? item["data"] : "";
|
|
4063
|
+
const kb = Math.max(1, Math.round(data.length * 3 / 4 / 1024));
|
|
4064
|
+
return `[${type} content: ${mime}, ~${kb} KB \u2014 binary payload not inlined]`;
|
|
4065
|
+
}
|
|
4066
|
+
if (type === "resource" && item["resource"] && typeof item["resource"] === "object") {
|
|
4067
|
+
const resource = item["resource"];
|
|
4068
|
+
const uri = typeof resource["uri"] === "string" ? resource["uri"] : "unknown";
|
|
4069
|
+
if (typeof resource["text"] === "string") return `[resource ${uri}]
|
|
4070
|
+
${resource["text"]}`;
|
|
4071
|
+
if (typeof resource["blob"] === "string") {
|
|
4072
|
+
const mime = typeof resource["mimeType"] === "string" ? `${resource["mimeType"]}, ` : "";
|
|
4073
|
+
return `[resource ${uri}: ${mime}binary payload not inlined]`;
|
|
4074
|
+
}
|
|
4075
|
+
return JSON.stringify(item);
|
|
4076
|
+
}
|
|
4077
|
+
if (type === "resource_link" && typeof item["uri"] === "string") {
|
|
4078
|
+
return `[resource link: ${item["uri"]}]`;
|
|
4079
|
+
}
|
|
4080
|
+
return JSON.stringify(item);
|
|
4081
|
+
}
|
|
3908
4082
|
function stringify(c) {
|
|
3909
4083
|
if (typeof c === "string") return c;
|
|
3910
4084
|
if (Array.isArray(c)) {
|
|
3911
4085
|
return c.map((item) => {
|
|
3912
4086
|
if (item && typeof item === "object") {
|
|
3913
|
-
|
|
3914
|
-
if (t === "text") return item.text ?? "";
|
|
3915
|
-
return JSON.stringify(item);
|
|
4087
|
+
return renderContentBlock(item);
|
|
3916
4088
|
}
|
|
3917
4089
|
return String(item);
|
|
3918
4090
|
}).join("\n");
|
|
@@ -3928,9 +4100,14 @@ function stringify(c) {
|
|
|
3928
4100
|
|
|
3929
4101
|
// src/registry-connect-loop.ts
|
|
3930
4102
|
function applySlotTools(ctx, slot, tools, client) {
|
|
3931
|
-
|
|
4103
|
+
slot.discoveredTools = tools;
|
|
3932
4104
|
const allowed = slot.cfg.allowedTools;
|
|
3933
4105
|
const filtered = tools.filter((t) => !allowed || allowed.includes(t.name));
|
|
4106
|
+
const signature = JSON.stringify(
|
|
4107
|
+
filtered.map((t) => [t.name, t.description ?? null, t.inputSchema ?? null])
|
|
4108
|
+
);
|
|
4109
|
+
const lazyWrappersCurrent = slot.lazy && slot.toolSignature === signature && slot.lazyTools.length === filtered.length && (slot.registeredLazy || ctx.lazyMode);
|
|
4110
|
+
if (lazyWrappersCurrent) return;
|
|
3934
4111
|
const clientArg = slot.lazy ? () => ctx.ensureConnected(slot.cfg.name) : expectDefined(client);
|
|
3935
4112
|
const wrapped = filtered.map(
|
|
3936
4113
|
(t) => wrapMCPTool(slot.cfg.name, t, clientArg, slot.cfg.permission ?? "confirm", {
|
|
@@ -3956,9 +4133,16 @@ function applySlotTools(ctx, slot, tools, client) {
|
|
|
3956
4133
|
})
|
|
3957
4134
|
);
|
|
3958
4135
|
slot.lazyTools = wrapped;
|
|
3959
|
-
|
|
3960
|
-
|
|
4136
|
+
slot.toolSignature = signature;
|
|
4137
|
+
const wasActive = slot.toolNames.length > 0;
|
|
4138
|
+
if (ctx.lazyMode && !wasActive) return;
|
|
4139
|
+
for (const name of slot.toolNames) {
|
|
4140
|
+
try {
|
|
4141
|
+
ctx.toolRegistry.unregister(name);
|
|
4142
|
+
} catch {
|
|
4143
|
+
}
|
|
3961
4144
|
}
|
|
4145
|
+
slot.toolNames = [];
|
|
3962
4146
|
for (const tool of wrapped) {
|
|
3963
4147
|
try {
|
|
3964
4148
|
ctx.toolRegistry.register(tool, `mcp:${slot.cfg.name}`);
|
|
@@ -3967,7 +4151,7 @@ function applySlotTools(ctx, slot, tools, client) {
|
|
|
3967
4151
|
ctx.log.warn(`MCP tool "${tool.name}" not registered`, err);
|
|
3968
4152
|
}
|
|
3969
4153
|
}
|
|
3970
|
-
if (slot.lazy && wrapped.length > 0) slot.registeredLazy = true;
|
|
4154
|
+
if (slot.lazy && !ctx.lazyMode && wrapped.length > 0) slot.registeredLazy = true;
|
|
3971
4155
|
}
|
|
3972
4156
|
async function discoverSlotCapabilities(ctx, slot, client) {
|
|
3973
4157
|
const startedAt = Date.now();
|
|
@@ -4019,15 +4203,17 @@ async function discoverSlotCapabilities(ctx, slot, client) {
|
|
|
4019
4203
|
async function persistSlotCapabilityManifest(cacheDir, slot) {
|
|
4020
4204
|
if (!slot.lazy || !cacheDir) return;
|
|
4021
4205
|
const previous = slot.manifestWrite ?? Promise.resolve();
|
|
4022
|
-
const pending = previous.then(
|
|
4023
|
-
|
|
4024
|
-
|
|
4206
|
+
const pending = previous.then(() => {
|
|
4207
|
+
const tools = slot.discoveredTools ?? slot.client?.listTools();
|
|
4208
|
+
if (!tools) return;
|
|
4209
|
+
return writeCapabilityManifest(cacheDir, slot.cfg.name, manifestConfigHash(slot.cfg), {
|
|
4210
|
+
tools,
|
|
4025
4211
|
serverMetadata: slot.serverMetadata,
|
|
4026
4212
|
resources: slot.resources,
|
|
4027
4213
|
resourceTemplates: slot.resourceTemplates,
|
|
4028
4214
|
prompts: slot.prompts
|
|
4029
|
-
})
|
|
4030
|
-
);
|
|
4215
|
+
});
|
|
4216
|
+
});
|
|
4031
4217
|
slot.manifestWrite = pending;
|
|
4032
4218
|
await pending;
|
|
4033
4219
|
if (slot.manifestWrite === pending) slot.manifestWrite = void 0;
|
|
@@ -4036,7 +4222,7 @@ async function attemptConnectSlot(ctx, slot) {
|
|
|
4036
4222
|
const MAX_ATTEMPTS = MCP_CONSTANTS.RECONNECT.MAX_ATTEMPTS;
|
|
4037
4223
|
let attempt = 0;
|
|
4038
4224
|
while (attempt < MAX_ATTEMPTS) {
|
|
4039
|
-
if (ctx.servers.
|
|
4225
|
+
if (ctx.servers.get(slot.cfg.name) !== slot) {
|
|
4040
4226
|
return;
|
|
4041
4227
|
}
|
|
4042
4228
|
attempt++;
|
|
@@ -4056,6 +4242,7 @@ async function attemptConnectSlot(ctx, slot) {
|
|
|
4056
4242
|
headers: slot.cfg.headers,
|
|
4057
4243
|
startupTimeoutMs: slot.cfg.startupTimeoutMs,
|
|
4058
4244
|
requestTimeoutMs: slot.cfg.requestTimeoutMs,
|
|
4245
|
+
cwd: ctx.cwd,
|
|
4059
4246
|
allowPrivateNetworks: slot.cfg.allowPrivateNetworks,
|
|
4060
4247
|
passthroughEnv: slot.cfg.passthroughEnv,
|
|
4061
4248
|
authorizationProvider: ctx.authorizationProviderFactory?.(slot.cfg)
|
|
@@ -4095,6 +4282,7 @@ async function attemptConnectSlot(ctx, slot) {
|
|
|
4095
4282
|
slot.reconnectCycles = 0;
|
|
4096
4283
|
const mc = client;
|
|
4097
4284
|
const discovered = mc.listTools();
|
|
4285
|
+
slot.discoveredTools = discovered;
|
|
4098
4286
|
await discoverSlotCapabilities(ctx, slot, mc);
|
|
4099
4287
|
await persistSlotCapabilityManifest(ctx.cacheDir, slot);
|
|
4100
4288
|
applySlotTools(ctx, slot, discovered, mc);
|
|
@@ -4150,7 +4338,7 @@ async function attemptConnectSlot(ctx, slot) {
|
|
|
4150
4338
|
}
|
|
4151
4339
|
const delay = 500 * 2 ** attempt;
|
|
4152
4340
|
await new Promise((r) => setTimeout(r, delay));
|
|
4153
|
-
if (slot.state === "disconnected" || ctx.servers.
|
|
4341
|
+
if (slot.state === "disconnected" || ctx.servers.get(slot.cfg.name) !== slot) {
|
|
4154
4342
|
return;
|
|
4155
4343
|
}
|
|
4156
4344
|
}
|
|
@@ -4167,6 +4355,7 @@ function resetDisconnectedSlotTools(slot, toolRegistry) {
|
|
|
4167
4355
|
}
|
|
4168
4356
|
slot.toolNames = [];
|
|
4169
4357
|
slot.lazyTools = [];
|
|
4358
|
+
slot.toolSignature = void 0;
|
|
4170
4359
|
slot.serverMetadata = void 0;
|
|
4171
4360
|
slot.resources = void 0;
|
|
4172
4361
|
slot.resourceTemplates = void 0;
|
|
@@ -4387,6 +4576,7 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
4387
4576
|
log;
|
|
4388
4577
|
lazyMode;
|
|
4389
4578
|
cacheDir;
|
|
4579
|
+
cwd;
|
|
4390
4580
|
idleTimeoutMs;
|
|
4391
4581
|
authorizationProviderFactory;
|
|
4392
4582
|
authorizationManager;
|
|
@@ -4399,6 +4589,7 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
4399
4589
|
this.log = opts.log;
|
|
4400
4590
|
this.lazyMode = opts.lazyMode ?? false;
|
|
4401
4591
|
this.cacheDir = opts.cacheDir;
|
|
4592
|
+
this.cwd = opts.cwd;
|
|
4402
4593
|
this.idleTimeoutMs = opts.idleTimeoutMs ?? MCP_CONSTANTS.IDLE.DEFAULT_TIMEOUT_MS;
|
|
4403
4594
|
this.authorizationProviderFactory = opts.authorizationProviderFactory;
|
|
4404
4595
|
this.authorizationManager = opts.authorizationManager;
|
|
@@ -4558,7 +4749,6 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
4558
4749
|
}
|
|
4559
4750
|
}
|
|
4560
4751
|
this.log.info(`MCP server "${name}" activated (${slot.toolNames.length} tools)`);
|
|
4561
|
-
this.events.emit("mcp.server.connected", { name, toolCount: slot.toolNames.length });
|
|
4562
4752
|
}
|
|
4563
4753
|
/**
|
|
4564
4754
|
* Unregister all tools for a given server from the tool registry.
|
|
@@ -4578,9 +4768,27 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
4578
4768
|
}
|
|
4579
4769
|
slot.toolNames = [];
|
|
4580
4770
|
this.log.info(`MCP server "${name}" deactivated (${count} tools removed)`);
|
|
4581
|
-
this.events.emit("mcp.server.disconnected", { name, reason: "deactivate" });
|
|
4582
4771
|
return count;
|
|
4583
4772
|
}
|
|
4773
|
+
/**
|
|
4774
|
+
* The tools a server offers — bare names, descriptions and input schemas —
|
|
4775
|
+
* without activating, registering or waking it. In token-saving mode the
|
|
4776
|
+
* model reaches MCP only through `mcp_use`, which needs the bare tool name
|
|
4777
|
+
* and its input shape; before this there was no way to learn either short
|
|
4778
|
+
* of guessing and reading the error. Honors `allowedTools`. Returns
|
|
4779
|
+
* `undefined` for an unknown server, `[]` when nothing was discovered yet.
|
|
4780
|
+
*/
|
|
4781
|
+
describeTools(name) {
|
|
4782
|
+
const slot = this.servers.get(name);
|
|
4783
|
+
if (!slot) return void 0;
|
|
4784
|
+
const allowed = slot.cfg.allowedTools;
|
|
4785
|
+
const tools = slot.discoveredTools ?? slot.client?.listTools() ?? [];
|
|
4786
|
+
return tools.filter((tool) => !allowed || allowed.includes(tool.name)).map((tool) => ({
|
|
4787
|
+
name: tool.name,
|
|
4788
|
+
...tool.description !== void 0 ? { description: tool.description } : {},
|
|
4789
|
+
inputSchema: structuredClone(tool.inputSchema)
|
|
4790
|
+
}));
|
|
4791
|
+
}
|
|
4584
4792
|
/**
|
|
4585
4793
|
* Check whether a server's tools are currently registered.
|
|
4586
4794
|
*/
|
|
@@ -4617,12 +4825,52 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
4617
4825
|
this.recordOperation(slot, "stop", "manual");
|
|
4618
4826
|
this.events.emit("mcp.server.disconnected", { name, reason: "stop" });
|
|
4619
4827
|
}
|
|
4620
|
-
|
|
4828
|
+
/**
|
|
4829
|
+
* Stop and start a registered server. Pass `nextCfg` to apply an edited
|
|
4830
|
+
* configuration: without it the slot reconnects with the config it was
|
|
4831
|
+
* started with, so an update/enable routed through restart() silently kept
|
|
4832
|
+
* the old command, url, env, permission and lazy flag until the next boot.
|
|
4833
|
+
*/
|
|
4834
|
+
/**
|
|
4835
|
+
* Put a running server to sleep while keeping its configuration enabled.
|
|
4836
|
+
*
|
|
4837
|
+
* A lazy server goes `dormant`: the process stops but its tools stay
|
|
4838
|
+
* registered and the next call wakes it. Surfaces previously used stop() for
|
|
4839
|
+
* this, which unregistered a lazy server's tools — "sleep" silently became
|
|
4840
|
+
* "unreachable until restarted". Eager servers have no dormant state, so
|
|
4841
|
+
* for them sleep is a stop.
|
|
4842
|
+
*/
|
|
4843
|
+
async sleep(name) {
|
|
4844
|
+
const slot = this.requireSlot(name);
|
|
4845
|
+
if (!slot.lazy) {
|
|
4846
|
+
await this.stop(name);
|
|
4847
|
+
return;
|
|
4848
|
+
}
|
|
4849
|
+
if (slot.state === "dormant") return;
|
|
4850
|
+
if (slot.operations.inFlightCalls > 0) {
|
|
4851
|
+
throw new Error(`MCP server "${name}" has tool calls in flight \u2014 try again when they finish`);
|
|
4852
|
+
}
|
|
4853
|
+
await sleepIdleSlot(this.idleContext(), slot);
|
|
4854
|
+
}
|
|
4855
|
+
async restart(name, nextCfg) {
|
|
4621
4856
|
const slot = this.servers.get(name);
|
|
4622
4857
|
if (!slot) throw new Error(`MCP server "${name}" not registered`);
|
|
4858
|
+
if (nextCfg && nextCfg.name !== name) {
|
|
4859
|
+
throw new Error(`MCP restart config names "${nextCfg.name}", expected "${name}"`);
|
|
4860
|
+
}
|
|
4861
|
+
if (nextCfg?.enabled === false) {
|
|
4862
|
+
await this.stop(name);
|
|
4863
|
+
this.markDisabled(nextCfg);
|
|
4864
|
+
return;
|
|
4865
|
+
}
|
|
4623
4866
|
slot.operations.restartCount++;
|
|
4624
4867
|
this.recordOperation(slot, "restart", "manual");
|
|
4625
4868
|
await this.stop(name);
|
|
4869
|
+
if (nextCfg) {
|
|
4870
|
+
slot.cfg = nextCfg;
|
|
4871
|
+
slot.lazy = !!nextCfg.lazy && !!this.cacheDir;
|
|
4872
|
+
slot.discoveredTools = void 0;
|
|
4873
|
+
}
|
|
4626
4874
|
slot.attempts = 0;
|
|
4627
4875
|
slot.reconnectCycles = 0;
|
|
4628
4876
|
if (slot.lazy) {
|
|
@@ -4744,6 +4992,7 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
4744
4992
|
log: this.log,
|
|
4745
4993
|
lazyMode: this.lazyMode,
|
|
4746
4994
|
cacheDir: this.cacheDir,
|
|
4995
|
+
cwd: this.cwd,
|
|
4747
4996
|
authorizationProviderFactory: this.authorizationProviderFactory,
|
|
4748
4997
|
operationListeners: this.operationListeners,
|
|
4749
4998
|
ensureConnected: (name) => this.ensureConnected(name),
|
|
@@ -4855,21 +5104,9 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
4855
5104
|
onToolsChanged = (name, _tools) => {
|
|
4856
5105
|
const slot = this.servers.get(name);
|
|
4857
5106
|
if (!slot?.client) return;
|
|
4858
|
-
for (const t of slot.toolNames) {
|
|
4859
|
-
try {
|
|
4860
|
-
this.toolRegistry.unregister(t);
|
|
4861
|
-
} catch {
|
|
4862
|
-
}
|
|
4863
|
-
}
|
|
4864
|
-
slot.toolNames = [];
|
|
4865
|
-
slot.registeredLazy = false;
|
|
4866
5107
|
const discovered = slot.client.listTools();
|
|
4867
5108
|
this.applyTools(slot, discovered, slot.client);
|
|
4868
5109
|
void this.persistCapabilityManifest(slot);
|
|
4869
|
-
this.events.emit("mcp.server.connected", {
|
|
4870
|
-
name: slot.cfg.name,
|
|
4871
|
-
toolCount: slot.toolNames.length
|
|
4872
|
-
});
|
|
4873
5110
|
this.log.info(
|
|
4874
5111
|
`MCP server "${slot.cfg.name}" tools refreshed (${this.toolNamesForSlot(slot).length} active)`
|
|
4875
5112
|
);
|
|
@@ -4899,7 +5136,7 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
4899
5136
|
}
|
|
4900
5137
|
onChildExit = (name, code, _signal) => {
|
|
4901
5138
|
const slot = this.servers.get(name);
|
|
4902
|
-
if (
|
|
5139
|
+
if (slot?.state !== "connected") return;
|
|
4903
5140
|
if (slot.lazy) {
|
|
4904
5141
|
this.recordFailure(slot, "transport", "process-exit-lazy");
|
|
4905
5142
|
markLazySlotDormant(slot, this.events, `exit:${code ?? "unknown"}`, {
|
|
@@ -4918,7 +5155,7 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
4918
5155
|
/** Handles SSE / streamable-http disconnect — same recovery as stdio child exit. */
|
|
4919
5156
|
onTransportDisconnect = (name) => {
|
|
4920
5157
|
const slot = this.servers.get(name);
|
|
4921
|
-
if (
|
|
5158
|
+
if (slot?.state !== "connected") return;
|
|
4922
5159
|
if (slot.lazy) {
|
|
4923
5160
|
this.recordFailure(slot, "transport", "http-disconnect-lazy");
|
|
4924
5161
|
markLazySlotDormant(slot, this.events, "http-disconnect", {
|
|
@@ -4954,7 +5191,7 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
4954
5191
|
slot.reconnectCycles++;
|
|
4955
5192
|
slot.operations.reconnectCount++;
|
|
4956
5193
|
this.recordOperation(slot, "reconnect", "automatic");
|
|
4957
|
-
await this.
|
|
5194
|
+
await this.singleFlightConnect(slot);
|
|
4958
5195
|
}
|
|
4959
5196
|
recordSuccess(slot, resetFailures = true) {
|
|
4960
5197
|
recordRegistrySuccess(slot, resetFailures);
|
|
@@ -5639,6 +5876,8 @@ var MCPRefreshingAuthorizationProvider = class {
|
|
|
5639
5876
|
}
|
|
5640
5877
|
options;
|
|
5641
5878
|
refreshPromise;
|
|
5879
|
+
/** Refresh token the authorization server rejected; cleared by a new authorization. */
|
|
5880
|
+
rejectedRefreshToken;
|
|
5642
5881
|
resource;
|
|
5643
5882
|
refreshSkewMs;
|
|
5644
5883
|
async getAccessToken(context) {
|
|
@@ -5676,16 +5915,28 @@ var MCPRefreshingAuthorizationProvider = class {
|
|
|
5676
5915
|
}
|
|
5677
5916
|
async refreshInner(state) {
|
|
5678
5917
|
const refreshToken = state.tokenSet.refreshToken;
|
|
5679
|
-
if (!refreshToken) {
|
|
5680
|
-
this.emit("reauth_required", state);
|
|
5918
|
+
if (!refreshToken || refreshToken === this.rejectedRefreshToken) {
|
|
5919
|
+
if (!refreshToken) this.emit("reauth_required", state);
|
|
5681
5920
|
return void 0;
|
|
5682
5921
|
}
|
|
5683
|
-
|
|
5684
|
-
|
|
5685
|
-
|
|
5686
|
-
|
|
5687
|
-
|
|
5688
|
-
|
|
5922
|
+
let tokenSet;
|
|
5923
|
+
try {
|
|
5924
|
+
tokenSet = await refreshMcpAccessToken({
|
|
5925
|
+
authorizationServer: state.authorizationServer,
|
|
5926
|
+
clientId: state.clientId,
|
|
5927
|
+
resource: state.resource,
|
|
5928
|
+
refreshToken
|
|
5929
|
+
});
|
|
5930
|
+
} catch (err) {
|
|
5931
|
+
const current = await this.options.store.load(this.options.serverName, this.resource).catch(() => void 0);
|
|
5932
|
+
if (current && current.tokenSet.refreshToken !== refreshToken) return current;
|
|
5933
|
+
if (err instanceof MCPOAuthHttpError && err.status >= 400 && err.status < 500) {
|
|
5934
|
+
this.rejectedRefreshToken = refreshToken;
|
|
5935
|
+
this.emit("reauth_required", state);
|
|
5936
|
+
return void 0;
|
|
5937
|
+
}
|
|
5938
|
+
throw err;
|
|
5939
|
+
}
|
|
5689
5940
|
const next = normalizeStoredAuthorization({
|
|
5690
5941
|
...state,
|
|
5691
5942
|
tokenSet,
|