@wrongstack/mcp 0.295.0 → 0.296.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client-process.d.ts +10 -0
- package/dist/client-process.d.ts.map +1 -0
- package/dist/client-protocol-helpers.d.ts +11 -0
- package/dist/client-protocol-helpers.d.ts.map +1 -0
- package/dist/client.d.ts +2 -19
- package/dist/client.d.ts.map +1 -1
- package/dist/index.js +517 -408
- package/dist/index.js.map +4 -4
- package/dist/manifest-cache.d.ts.map +1 -1
- package/dist/operations.d.ts.map +1 -1
- package/dist/read-body.d.ts +21 -0
- package/dist/read-body.d.ts.map +1 -0
- package/dist/registry-authorization.d.ts +14 -0
- package/dist/registry-authorization.d.ts.map +1 -0
- package/dist/registry-catalog.d.ts +20 -0
- package/dist/registry-catalog.d.ts.map +1 -0
- package/dist/registry-disconnect.d.ts +6 -0
- package/dist/registry-disconnect.d.ts.map +1 -0
- package/dist/registry-health.d.ts +5 -0
- package/dist/registry-health.d.ts.map +1 -0
- package/dist/registry-operations.d.ts +7 -0
- package/dist/registry-operations.d.ts.map +1 -0
- package/dist/registry-reconnect.d.ts +16 -0
- package/dist/registry-reconnect.d.ts.map +1 -0
- package/dist/registry-slots.d.ts +35 -0
- package/dist/registry-slots.d.ts.map +1 -0
- package/dist/registry-types.d.ts +26 -0
- package/dist/registry-types.d.ts.map +1 -0
- package/dist/registry.d.ts +5 -56
- package/dist/registry.d.ts.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/tool-schema.d.ts.map +1 -1
- package/dist/transport-jsonrpc.d.ts +2 -2
- package/dist/transport-jsonrpc.d.ts.map +1 -1
- package/dist/transport-sse.d.ts.map +1 -1
- package/dist/transport-streamable.d.ts.map +1 -1
- package/dist/transport.d.ts +2 -2
- package/dist/transport.d.ts.map +1 -1
- package/package.json +3 -4
package/dist/index.js
CHANGED
|
@@ -47,7 +47,7 @@ function parseMcpBearerChallenge(header, resource) {
|
|
|
47
47
|
if (!header) return challenge;
|
|
48
48
|
const bearer = /(?:^|,)\s*Bearer(?:\s+|$)/i.exec(header);
|
|
49
49
|
if (!bearer) return challenge;
|
|
50
|
-
const parameters = header.slice(
|
|
50
|
+
const parameters = header.slice(bearer.index + bearer[0].length);
|
|
51
51
|
const resourceMetadata = challengeParameter(parameters, "resource_metadata");
|
|
52
52
|
if (resourceMetadata) {
|
|
53
53
|
const metadataUrl = validateMetadataUrl(resourceMetadata);
|
|
@@ -395,7 +395,7 @@ async function requestPinnedJson(rawUrl, options) {
|
|
|
395
395
|
};
|
|
396
396
|
const requestFn = url.protocol === "https:" ? https.request : http.request;
|
|
397
397
|
const request3 = requestFn(requestOptions, (response) => {
|
|
398
|
-
const status = response.statusCode
|
|
398
|
+
const status = response.statusCode;
|
|
399
399
|
if (status === 404 || status === 410) {
|
|
400
400
|
response.resume();
|
|
401
401
|
finish(void 0, void 0);
|
|
@@ -738,9 +738,8 @@ function boundedServerName(value) {
|
|
|
738
738
|
}
|
|
739
739
|
|
|
740
740
|
// src/client.ts
|
|
741
|
-
import { spawn } from "node:child_process";
|
|
742
|
-
import { buildChildEnv } from "@wrongstack/core/utils";
|
|
743
|
-
import { toErrorMessage } from "@wrongstack/core/utils";
|
|
741
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
742
|
+
import { buildChildEnv, toErrorMessage } from "@wrongstack/core/utils";
|
|
744
743
|
|
|
745
744
|
// src/constants.ts
|
|
746
745
|
var MCP_CONSTANTS = Object.freeze({
|
|
@@ -997,13 +996,15 @@ function normalizeMCPTools(value) {
|
|
|
997
996
|
if (typeof t.name !== "string" || t.name.trim().length === 0) continue;
|
|
998
997
|
const inputSchema = t.inputSchema && typeof t.inputSchema === "object" && !Array.isArray(t.inputSchema) ? t.inputSchema : { type: "object", properties: {} };
|
|
999
998
|
if (!t.inputSchema || typeof t.inputSchema !== "object" || Array.isArray(t.inputSchema)) {
|
|
1000
|
-
console.warn(
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
999
|
+
console.warn(
|
|
1000
|
+
JSON.stringify({
|
|
1001
|
+
level: "warn",
|
|
1002
|
+
event: "mcp.tool_schema_invalid",
|
|
1003
|
+
tool: t.name,
|
|
1004
|
+
message: "no/invalid inputSchema \u2014 defaulting to empty object",
|
|
1005
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1006
|
+
})
|
|
1007
|
+
);
|
|
1007
1008
|
}
|
|
1008
1009
|
tools.push({
|
|
1009
1010
|
name: t.name,
|
|
@@ -1014,96 +1015,8 @@ function normalizeMCPTools(value) {
|
|
|
1014
1015
|
return tools;
|
|
1015
1016
|
}
|
|
1016
1017
|
|
|
1017
|
-
// src/transport-jsonrpc.ts
|
|
1018
|
-
import { ToolError } from "@wrongstack/core/types";
|
|
1019
|
-
function isJsonRpcResult(v) {
|
|
1020
|
-
if (typeof v !== "object" || v === null) return false;
|
|
1021
|
-
const r = v;
|
|
1022
|
-
if (r["jsonrpc"] !== "2.0" || typeof r["id"] !== "number") return false;
|
|
1023
|
-
if (Object.hasOwn(r, "method")) return false;
|
|
1024
|
-
const hasResult = Object.hasOwn(r, "result");
|
|
1025
|
-
const hasError = Object.hasOwn(r, "error");
|
|
1026
|
-
if (hasResult === hasError) return false;
|
|
1027
|
-
if (hasError) {
|
|
1028
|
-
const error = r["error"];
|
|
1029
|
-
return typeof error === "object" && error !== null && typeof error["code"] === "number" && typeof error["message"] === "string";
|
|
1030
|
-
}
|
|
1031
|
-
return true;
|
|
1032
|
-
}
|
|
1033
|
-
function isJsonRpcMethodEnvelope(v) {
|
|
1034
|
-
if (typeof v !== "object" || v === null) return false;
|
|
1035
|
-
const envelope = v;
|
|
1036
|
-
if (envelope["jsonrpc"] !== "2.0" || typeof envelope["method"] !== "string") return false;
|
|
1037
|
-
const id = envelope["id"];
|
|
1038
|
-
return id === void 0 || typeof id === "number" || typeof id === "string";
|
|
1039
|
-
}
|
|
1040
|
-
function extractJsonRpcEnvelopes(text) {
|
|
1041
|
-
const out = [];
|
|
1042
|
-
let dataBuf = [];
|
|
1043
|
-
const flush = () => {
|
|
1044
|
-
if (dataBuf.length === 0) return;
|
|
1045
|
-
const joined = dataBuf.join("\n").trim();
|
|
1046
|
-
dataBuf = [];
|
|
1047
|
-
if (!joined) return;
|
|
1048
|
-
try {
|
|
1049
|
-
const parsed = JSON.parse(joined);
|
|
1050
|
-
if (isJsonRpcResult(parsed) || isJsonRpcMethodEnvelope(parsed)) out.push(parsed);
|
|
1051
|
-
} catch {
|
|
1052
|
-
}
|
|
1053
|
-
};
|
|
1054
|
-
for (const raw of text.split("\n")) {
|
|
1055
|
-
const line = raw.replace(/\r$/, "");
|
|
1056
|
-
if (line === "") {
|
|
1057
|
-
flush();
|
|
1058
|
-
continue;
|
|
1059
|
-
}
|
|
1060
|
-
if (line.startsWith(":")) continue;
|
|
1061
|
-
if (line.startsWith("data:")) {
|
|
1062
|
-
let v = line.slice(5);
|
|
1063
|
-
if (v.startsWith(" ")) v = v.slice(1);
|
|
1064
|
-
dataBuf.push(v);
|
|
1065
|
-
continue;
|
|
1066
|
-
}
|
|
1067
|
-
if (line.startsWith("event:") || line.startsWith("id:") || line.startsWith("retry:")) {
|
|
1068
|
-
continue;
|
|
1069
|
-
}
|
|
1070
|
-
const trimmed = line.trim();
|
|
1071
|
-
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
1072
|
-
try {
|
|
1073
|
-
const parsed = JSON.parse(trimmed);
|
|
1074
|
-
if (isJsonRpcResult(parsed) || isJsonRpcMethodEnvelope(parsed)) out.push(parsed);
|
|
1075
|
-
} catch {
|
|
1076
|
-
}
|
|
1077
|
-
}
|
|
1078
|
-
}
|
|
1079
|
-
flush();
|
|
1080
|
-
return out;
|
|
1081
|
-
}
|
|
1082
|
-
function extractJsonRpcResults(text) {
|
|
1083
|
-
return extractJsonRpcEnvelopes(text).filter(isJsonRpcResult);
|
|
1084
|
-
}
|
|
1085
|
-
function assertMatchingJsonRpcResult(data, expectedId, method) {
|
|
1086
|
-
if (!isJsonRpcResult(data)) {
|
|
1087
|
-
throw new ToolError({
|
|
1088
|
-
message: "Invalid JSON-RPC response: not a JSON-RPC 2.0 envelope",
|
|
1089
|
-
code: "TOOL_EXECUTION_FAILED",
|
|
1090
|
-
toolName: "mcp_transport_jsonrpc",
|
|
1091
|
-
context: { method, expectedId, reason: "not-jsonrpc-envelope" }
|
|
1092
|
-
});
|
|
1093
|
-
}
|
|
1094
|
-
if (data.id !== expectedId) {
|
|
1095
|
-
throw new ToolError({
|
|
1096
|
-
message: `Invalid JSON-RPC response: id mismatch for ${method} (expected ${expectedId}, got ${data.id})`,
|
|
1097
|
-
code: "TOOL_EXECUTION_FAILED",
|
|
1098
|
-
toolName: "mcp_transport_jsonrpc",
|
|
1099
|
-
context: { method, expectedId, actualId: data.id, reason: "id-mismatch" }
|
|
1100
|
-
});
|
|
1101
|
-
}
|
|
1102
|
-
return data;
|
|
1103
|
-
}
|
|
1104
|
-
|
|
1105
1018
|
// src/sse-reader.ts
|
|
1106
|
-
import { ToolError
|
|
1019
|
+
import { ToolError } from "@wrongstack/core/types";
|
|
1107
1020
|
var SSE_READER_MAX_BUFFER = 256 * 1024;
|
|
1108
1021
|
var SSE_READER_MAX_DATA_LINES = 1024;
|
|
1109
1022
|
var SSEReader = class {
|
|
@@ -1119,7 +1032,7 @@ var SSEReader = class {
|
|
|
1119
1032
|
}
|
|
1120
1033
|
feed(chunk) {
|
|
1121
1034
|
if (chunk.length > SSE_READER_MAX_BUFFER) {
|
|
1122
|
-
throw new
|
|
1035
|
+
throw new ToolError({
|
|
1123
1036
|
message: `SSE: chunk size ${chunk.length} exceeds max buffer ${SSE_READER_MAX_BUFFER} \u2014 refusing to accumulate`,
|
|
1124
1037
|
code: "TOOL_EXECUTION_FAILED",
|
|
1125
1038
|
toolName: "mcp_transport_sse_reader",
|
|
@@ -1128,7 +1041,7 @@ var SSEReader = class {
|
|
|
1128
1041
|
}
|
|
1129
1042
|
this.buffer += chunk;
|
|
1130
1043
|
if (this.buffer.length > SSE_READER_MAX_BUFFER) {
|
|
1131
|
-
throw new
|
|
1044
|
+
throw new ToolError({
|
|
1132
1045
|
message: `SSE: pending line exceeds ${SSE_READER_MAX_BUFFER} bytes \u2014 upstream is not framing events`,
|
|
1133
1046
|
code: "TOOL_EXECUTION_FAILED",
|
|
1134
1047
|
toolName: "mcp_transport_sse_reader",
|
|
@@ -1163,7 +1076,7 @@ var SSEReader = class {
|
|
|
1163
1076
|
if (field === "event") {
|
|
1164
1077
|
} else if (field === "data") {
|
|
1165
1078
|
if (this.dataLines.length >= SSE_READER_MAX_DATA_LINES) {
|
|
1166
|
-
throw new
|
|
1079
|
+
throw new ToolError({
|
|
1167
1080
|
message: `SSE: exceeded ${SSE_READER_MAX_DATA_LINES} data lines per event \u2014 upstream is not sending blank-line delimiters`,
|
|
1168
1081
|
code: "TOOL_EXECUTION_FAILED",
|
|
1169
1082
|
toolName: "mcp_transport_sse_reader",
|
|
@@ -1453,9 +1366,133 @@ var BaseHTTPTransport = class {
|
|
|
1453
1366
|
}
|
|
1454
1367
|
};
|
|
1455
1368
|
|
|
1369
|
+
// src/transport-jsonrpc.ts
|
|
1370
|
+
import { ToolError as ToolError2 } from "@wrongstack/core/types";
|
|
1371
|
+
function isJsonRpcResult(v) {
|
|
1372
|
+
if (typeof v !== "object" || v === null) return false;
|
|
1373
|
+
const r = v;
|
|
1374
|
+
if (r["jsonrpc"] !== "2.0" || typeof r["id"] !== "number") return false;
|
|
1375
|
+
if (Object.hasOwn(r, "method")) return false;
|
|
1376
|
+
const hasResult = Object.hasOwn(r, "result");
|
|
1377
|
+
const hasError = Object.hasOwn(r, "error");
|
|
1378
|
+
if (hasResult === hasError) return false;
|
|
1379
|
+
if (hasError) {
|
|
1380
|
+
const error = r["error"];
|
|
1381
|
+
return typeof error === "object" && error !== null && typeof error["code"] === "number" && typeof error["message"] === "string";
|
|
1382
|
+
}
|
|
1383
|
+
return true;
|
|
1384
|
+
}
|
|
1385
|
+
function isJsonRpcMethodEnvelope(v) {
|
|
1386
|
+
if (typeof v !== "object" || v === null) return false;
|
|
1387
|
+
const envelope = v;
|
|
1388
|
+
if (envelope["jsonrpc"] !== "2.0" || typeof envelope["method"] !== "string") return false;
|
|
1389
|
+
const id = envelope["id"];
|
|
1390
|
+
return id === void 0 || typeof id === "number" || typeof id === "string";
|
|
1391
|
+
}
|
|
1392
|
+
function extractJsonRpcEnvelopes(text) {
|
|
1393
|
+
const out = [];
|
|
1394
|
+
let dataBuf = [];
|
|
1395
|
+
const flush = () => {
|
|
1396
|
+
if (dataBuf.length === 0) return;
|
|
1397
|
+
const joined = dataBuf.join("\n").trim();
|
|
1398
|
+
dataBuf = [];
|
|
1399
|
+
if (!joined) return;
|
|
1400
|
+
try {
|
|
1401
|
+
const parsed = JSON.parse(joined);
|
|
1402
|
+
if (isJsonRpcResult(parsed) || isJsonRpcMethodEnvelope(parsed)) out.push(parsed);
|
|
1403
|
+
} catch {
|
|
1404
|
+
}
|
|
1405
|
+
};
|
|
1406
|
+
for (const raw of text.split("\n")) {
|
|
1407
|
+
const line = raw.replace(/\r$/, "");
|
|
1408
|
+
if (line === "") {
|
|
1409
|
+
flush();
|
|
1410
|
+
continue;
|
|
1411
|
+
}
|
|
1412
|
+
if (line.startsWith(":")) continue;
|
|
1413
|
+
if (line.startsWith("data:")) {
|
|
1414
|
+
let v = line.slice(5);
|
|
1415
|
+
if (v.startsWith(" ")) v = v.slice(1);
|
|
1416
|
+
dataBuf.push(v);
|
|
1417
|
+
continue;
|
|
1418
|
+
}
|
|
1419
|
+
if (line.startsWith("event:") || line.startsWith("id:") || line.startsWith("retry:")) {
|
|
1420
|
+
continue;
|
|
1421
|
+
}
|
|
1422
|
+
const trimmed = line.trim();
|
|
1423
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
1424
|
+
try {
|
|
1425
|
+
const parsed = JSON.parse(trimmed);
|
|
1426
|
+
if (isJsonRpcResult(parsed) || isJsonRpcMethodEnvelope(parsed)) out.push(parsed);
|
|
1427
|
+
} catch {
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
flush();
|
|
1432
|
+
return out;
|
|
1433
|
+
}
|
|
1434
|
+
function extractJsonRpcResults(text) {
|
|
1435
|
+
return extractJsonRpcEnvelopes(text).filter(isJsonRpcResult);
|
|
1436
|
+
}
|
|
1437
|
+
function assertMatchingJsonRpcResult(data, expectedId, method) {
|
|
1438
|
+
if (!isJsonRpcResult(data)) {
|
|
1439
|
+
throw new ToolError2({
|
|
1440
|
+
message: "Invalid JSON-RPC response: not a JSON-RPC 2.0 envelope",
|
|
1441
|
+
code: "TOOL_EXECUTION_FAILED",
|
|
1442
|
+
toolName: "mcp_transport_jsonrpc",
|
|
1443
|
+
context: { method, expectedId, reason: "not-jsonrpc-envelope" }
|
|
1444
|
+
});
|
|
1445
|
+
}
|
|
1446
|
+
if (data.id !== expectedId) {
|
|
1447
|
+
throw new ToolError2({
|
|
1448
|
+
message: `Invalid JSON-RPC response: id mismatch for ${method} (expected ${expectedId}, got ${data.id})`,
|
|
1449
|
+
code: "TOOL_EXECUTION_FAILED",
|
|
1450
|
+
toolName: "mcp_transport_jsonrpc",
|
|
1451
|
+
context: { method, expectedId, actualId: data.id, reason: "id-mismatch" }
|
|
1452
|
+
});
|
|
1453
|
+
}
|
|
1454
|
+
return data;
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1456
1457
|
// src/transport-sse.ts
|
|
1457
1458
|
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
1459
|
+
import { ToolError as ToolError4 } from "@wrongstack/core/types";
|
|
1460
|
+
|
|
1461
|
+
// src/read-body.ts
|
|
1458
1462
|
import { ToolError as ToolError3 } from "@wrongstack/core/types";
|
|
1463
|
+
var MAX_MCP_HTTP_BODY_BYTES = 16 * 1024 * 1024;
|
|
1464
|
+
async function readBodyCapped(res, maxBytes = MAX_MCP_HTTP_BODY_BYTES) {
|
|
1465
|
+
const body = res.body;
|
|
1466
|
+
if (!body || typeof body.getReader !== "function") {
|
|
1467
|
+
return await res.text();
|
|
1468
|
+
}
|
|
1469
|
+
const reader = body.getReader();
|
|
1470
|
+
const chunks = [];
|
|
1471
|
+
let total = 0;
|
|
1472
|
+
try {
|
|
1473
|
+
for (; ; ) {
|
|
1474
|
+
const { done, value } = await reader.read();
|
|
1475
|
+
if (done) break;
|
|
1476
|
+
if (!value) continue;
|
|
1477
|
+
total += value.byteLength;
|
|
1478
|
+
if (total > maxBytes) {
|
|
1479
|
+
await reader.cancel().catch(() => void 0);
|
|
1480
|
+
throw new ToolError3({
|
|
1481
|
+
message: `MCP response body exceeded ${maxBytes} bytes \u2014 refusing to buffer`,
|
|
1482
|
+
code: "TOOL_EXECUTION_FAILED",
|
|
1483
|
+
toolName: "mcp_transport",
|
|
1484
|
+
context: { maxBytes, received: total }
|
|
1485
|
+
});
|
|
1486
|
+
}
|
|
1487
|
+
chunks.push(value);
|
|
1488
|
+
}
|
|
1489
|
+
} finally {
|
|
1490
|
+
reader.releaseLock?.();
|
|
1491
|
+
}
|
|
1492
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
// src/transport-sse.ts
|
|
1459
1496
|
var SSETransport = class extends BaseHTTPTransport {
|
|
1460
1497
|
_nextId = 1;
|
|
1461
1498
|
readerDone = false;
|
|
@@ -1502,7 +1539,7 @@ var SSETransport = class extends BaseHTTPTransport {
|
|
|
1502
1539
|
this.applyTlsAgent(fetchOpts);
|
|
1503
1540
|
const response = await this.fetchWithAuthorization(sseUrl, fetchOpts, signal);
|
|
1504
1541
|
if (!response.ok) {
|
|
1505
|
-
throw new
|
|
1542
|
+
throw new ToolError4({
|
|
1506
1543
|
message: `SSE connect HTTP ${response.status}: ${response.statusText}`,
|
|
1507
1544
|
code: "TOOL_EXECUTION_FAILED",
|
|
1508
1545
|
toolName: "mcp_transport_sse_connect",
|
|
@@ -1510,7 +1547,7 @@ var SSETransport = class extends BaseHTTPTransport {
|
|
|
1510
1547
|
});
|
|
1511
1548
|
}
|
|
1512
1549
|
if (!response.body) {
|
|
1513
|
-
throw new
|
|
1550
|
+
throw new ToolError4({
|
|
1514
1551
|
message: "SSE response has no body",
|
|
1515
1552
|
code: "TOOL_EXECUTION_FAILED",
|
|
1516
1553
|
toolName: "mcp_transport_sse_connect",
|
|
@@ -1543,7 +1580,7 @@ var SSETransport = class extends BaseHTTPTransport {
|
|
|
1543
1580
|
clientInfo: MCP_CONSTANTS.CLIENT_INFO
|
|
1544
1581
|
});
|
|
1545
1582
|
if (initRes.error) {
|
|
1546
|
-
throw new
|
|
1583
|
+
throw new ToolError4({
|
|
1547
1584
|
message: `initialize failed: ${initRes.error.message}`,
|
|
1548
1585
|
code: "TOOL_EXECUTION_FAILED",
|
|
1549
1586
|
toolName: "mcp_transport_initialize",
|
|
@@ -1618,7 +1655,7 @@ var SSETransport = class extends BaseHTTPTransport {
|
|
|
1618
1655
|
const body2 = await res.text();
|
|
1619
1656
|
const cap = MCP_CONSTANTS.REQUEST_LOG_CAP;
|
|
1620
1657
|
const snippet = body2.length > cap ? `${body2.slice(0, cap)}\u2026 [${body2.length} bytes total]` : body2;
|
|
1621
|
-
throw new
|
|
1658
|
+
throw new ToolError4({
|
|
1622
1659
|
message: `HTTP ${res.status}: ${snippet}`,
|
|
1623
1660
|
code: "TOOL_EXECUTION_FAILED",
|
|
1624
1661
|
toolName: method,
|
|
@@ -1627,9 +1664,9 @@ var SSETransport = class extends BaseHTTPTransport {
|
|
|
1627
1664
|
}
|
|
1628
1665
|
let data;
|
|
1629
1666
|
try {
|
|
1630
|
-
data = await res
|
|
1667
|
+
data = JSON.parse(await readBodyCapped(res));
|
|
1631
1668
|
} catch (err) {
|
|
1632
|
-
throw new
|
|
1669
|
+
throw new ToolError4({
|
|
1633
1670
|
message: `Invalid JSON-RPC response: ${err instanceof Error ? err.message : "parse failed"}`,
|
|
1634
1671
|
code: "TOOL_EXECUTION_FAILED",
|
|
1635
1672
|
toolName: method,
|
|
@@ -1654,7 +1691,7 @@ var SSETransport = class extends BaseHTTPTransport {
|
|
|
1654
1691
|
}
|
|
1655
1692
|
async callTool(name, input, opts) {
|
|
1656
1693
|
if (this.state !== "connected") {
|
|
1657
|
-
throw new
|
|
1694
|
+
throw new ToolError4({
|
|
1658
1695
|
message: `SSE transport not connected (state=${this.state})`,
|
|
1659
1696
|
code: "TOOL_EXECUTION_FAILED",
|
|
1660
1697
|
toolName: name,
|
|
@@ -1691,7 +1728,7 @@ var SSETransport = class extends BaseHTTPTransport {
|
|
|
1691
1728
|
try {
|
|
1692
1729
|
const res = await this.fetchWithAuthorization(this.url, fetchOpts, timeoutSignal.signal);
|
|
1693
1730
|
if (!res.ok) {
|
|
1694
|
-
throw new
|
|
1731
|
+
throw new ToolError4({
|
|
1695
1732
|
message: `HTTP ${res.status}: ${res.statusText}`,
|
|
1696
1733
|
code: "TOOL_EXECUTION_FAILED",
|
|
1697
1734
|
toolName: method,
|
|
@@ -1705,9 +1742,9 @@ var SSETransport = class extends BaseHTTPTransport {
|
|
|
1705
1742
|
}
|
|
1706
1743
|
let data;
|
|
1707
1744
|
try {
|
|
1708
|
-
data = await res
|
|
1745
|
+
data = JSON.parse(await readBodyCapped(res));
|
|
1709
1746
|
} catch (err) {
|
|
1710
|
-
throw new
|
|
1747
|
+
throw new ToolError4({
|
|
1711
1748
|
message: `Invalid JSON-RPC response: ${err instanceof Error ? err.message : "parse failed"}`,
|
|
1712
1749
|
code: "TOOL_EXECUTION_FAILED",
|
|
1713
1750
|
toolName: method,
|
|
@@ -1767,7 +1804,7 @@ var StreamableHTTPTransport = class extends BaseHTTPTransport {
|
|
|
1767
1804
|
}
|
|
1768
1805
|
}
|
|
1769
1806
|
const responses = envelopes.filter(isJsonRpcResult);
|
|
1770
|
-
return responses.find((envelope) => envelope.id === requestId) ?? responses
|
|
1807
|
+
return responses.find((envelope) => envelope.id === requestId) ?? responses[0];
|
|
1771
1808
|
}
|
|
1772
1809
|
handleNotification(method) {
|
|
1773
1810
|
if (method === "notifications/resources/list_changed") {
|
|
@@ -1886,9 +1923,9 @@ var StreamableHTTPTransport = class extends BaseHTTPTransport {
|
|
|
1886
1923
|
}
|
|
1887
1924
|
if (method.startsWith("notifications/")) {
|
|
1888
1925
|
await res.text().catch(() => void 0);
|
|
1889
|
-
return { jsonrpc: "2.0" };
|
|
1926
|
+
return { jsonrpc: "2.0", id };
|
|
1890
1927
|
}
|
|
1891
|
-
const match = this.consumeResponseText(await res
|
|
1928
|
+
const match = this.consumeResponseText(await readBodyCapped(res), id);
|
|
1892
1929
|
if (match) {
|
|
1893
1930
|
return assertMatchingJsonRpcResult(match, id, method);
|
|
1894
1931
|
}
|
|
@@ -1935,7 +1972,7 @@ var StreamableHTTPTransport = class extends BaseHTTPTransport {
|
|
|
1935
1972
|
await res.text().catch(() => void 0);
|
|
1936
1973
|
return { jsonrpc: "2.0", id };
|
|
1937
1974
|
}
|
|
1938
|
-
const parsed = this.consumeResponseText(await res
|
|
1975
|
+
const parsed = this.consumeResponseText(await readBodyCapped(res), id);
|
|
1939
1976
|
if (parsed) {
|
|
1940
1977
|
return {
|
|
1941
1978
|
jsonrpc: "2.0",
|
|
@@ -1981,19 +2018,62 @@ var StreamableHTTPTransport = class extends BaseHTTPTransport {
|
|
|
1981
2018
|
}
|
|
1982
2019
|
};
|
|
1983
2020
|
|
|
1984
|
-
// src/client.ts
|
|
1985
|
-
function
|
|
1986
|
-
if (
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
2021
|
+
// src/client-protocol-helpers.ts
|
|
2022
|
+
function quoteWindowsArg(arg) {
|
|
2023
|
+
if (!/[\s"]/.test(arg)) return arg;
|
|
2024
|
+
return `"${arg.replace(/"/g, '""')}"`;
|
|
2025
|
+
}
|
|
2026
|
+
var MAX_PROTOCOL_INPUT_CHARS = 8192;
|
|
2027
|
+
function validateProtocolString(value, label, allowEmpty = false) {
|
|
2028
|
+
if (typeof value !== "string" || !allowEmpty && value.length === 0) {
|
|
2029
|
+
throw new Error(`MCP ${label} must be ${allowEmpty ? "a string" : "a non-empty string"}`);
|
|
2030
|
+
}
|
|
2031
|
+
if (value.length > MAX_PROTOCOL_INPUT_CHARS) {
|
|
2032
|
+
throw new Error(`MCP ${label} exceeds ${MAX_PROTOCOL_INPUT_CHARS} characters`);
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
function pageParams(cursor, label) {
|
|
2036
|
+
if (cursor === void 0) return {};
|
|
2037
|
+
validateProtocolString(cursor, label);
|
|
2038
|
+
return { cursor };
|
|
2039
|
+
}
|
|
2040
|
+
function parseEmptyResult(value) {
|
|
2041
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
2042
|
+
throw new Error("Malformed MCP empty result: expected object");
|
|
2043
|
+
}
|
|
1996
2044
|
}
|
|
2045
|
+
|
|
2046
|
+
// src/client-process.ts
|
|
2047
|
+
import { spawn } from "node:child_process";
|
|
2048
|
+
function forceKillTree(child) {
|
|
2049
|
+
if (child.pid === void 0) {
|
|
2050
|
+
try {
|
|
2051
|
+
child.kill("SIGKILL");
|
|
2052
|
+
} catch {
|
|
2053
|
+
}
|
|
2054
|
+
return;
|
|
2055
|
+
}
|
|
2056
|
+
if (process.platform === "win32") {
|
|
2057
|
+
const killer = spawn("taskkill", ["/pid", String(child.pid), "/T", "/F"], {
|
|
2058
|
+
stdio: "ignore",
|
|
2059
|
+
windowsHide: true
|
|
2060
|
+
});
|
|
2061
|
+
killer.once("error", () => {
|
|
2062
|
+
try {
|
|
2063
|
+
child.kill("SIGKILL");
|
|
2064
|
+
} catch {
|
|
2065
|
+
}
|
|
2066
|
+
});
|
|
2067
|
+
killer.unref();
|
|
2068
|
+
return;
|
|
2069
|
+
}
|
|
2070
|
+
try {
|
|
2071
|
+
child.kill("SIGKILL");
|
|
2072
|
+
} catch {
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
// src/client.ts
|
|
1997
2077
|
var MCPClient = class _MCPClient {
|
|
1998
2078
|
constructor(opts) {
|
|
1999
2079
|
this.opts = opts;
|
|
@@ -2108,14 +2188,14 @@ var MCPClient = class _MCPClient {
|
|
|
2108
2188
|
const rawArgs = this.opts.args ?? [];
|
|
2109
2189
|
const spawnEnv = buildChildEnv({ extra: extraEnv });
|
|
2110
2190
|
const stdio = ["pipe", "pipe", "pipe"];
|
|
2111
|
-
const child = isWin ?
|
|
2191
|
+
const child = isWin ? spawn2([this.opts.command, ...rawArgs].map(quoteWindowsArg).join(" "), {
|
|
2112
2192
|
env: spawnEnv,
|
|
2113
2193
|
stdio,
|
|
2114
2194
|
shell: true,
|
|
2115
2195
|
// Without this every MCP server spawned from a console-less host
|
|
2116
2196
|
// (WebUI server, scheduled runs) opens a visible console window.
|
|
2117
2197
|
windowsHide: true
|
|
2118
|
-
}) :
|
|
2198
|
+
}) : spawn2(this.opts.command, rawArgs, { env: spawnEnv, stdio, windowsHide: true });
|
|
2119
2199
|
this.child = child;
|
|
2120
2200
|
child.stdout?.on("data", (chunk) => this.onData(chunk.toString()));
|
|
2121
2201
|
child.stderr?.on("data", () => {
|
|
@@ -2386,10 +2466,7 @@ var MCPClient = class _MCPClient {
|
|
|
2386
2466
|
new Promise((resolve) => setTimeout(() => resolve("timeout"), GRACEFUL_MS))
|
|
2387
2467
|
]);
|
|
2388
2468
|
if (gracefulRace === "timeout") {
|
|
2389
|
-
|
|
2390
|
-
child.kill("SIGKILL");
|
|
2391
|
-
} catch {
|
|
2392
|
-
}
|
|
2469
|
+
forceKillTree(child);
|
|
2393
2470
|
await Promise.race([
|
|
2394
2471
|
exitPromise,
|
|
2395
2472
|
new Promise((resolve) => setTimeout(resolve, FORCE_TIMEOUT_MS))
|
|
@@ -2618,11 +2695,12 @@ var MCPClient = class _MCPClient {
|
|
|
2618
2695
|
}
|
|
2619
2696
|
return;
|
|
2620
2697
|
}
|
|
2621
|
-
if (!
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
this.pending.
|
|
2625
|
-
|
|
2698
|
+
if (!isJsonRpcResult(msg)) return;
|
|
2699
|
+
const response = msg;
|
|
2700
|
+
if (this.pending.has(response.id)) {
|
|
2701
|
+
const entry = this.pending.get(response.id);
|
|
2702
|
+
this.pending.delete(response.id);
|
|
2703
|
+
entry?.resolve(response);
|
|
2626
2704
|
}
|
|
2627
2705
|
}
|
|
2628
2706
|
handleServerRequest(request3) {
|
|
@@ -2689,29 +2767,6 @@ var MCPClient = class _MCPClient {
|
|
|
2689
2767
|
}
|
|
2690
2768
|
}
|
|
2691
2769
|
};
|
|
2692
|
-
function quoteWindowsArg(arg) {
|
|
2693
|
-
if (!/[\s"]/.test(arg)) return arg;
|
|
2694
|
-
return `"${arg.replace(/"/g, '""')}"`;
|
|
2695
|
-
}
|
|
2696
|
-
var MAX_PROTOCOL_INPUT_CHARS = 8192;
|
|
2697
|
-
function validateProtocolString(value, label, allowEmpty = false) {
|
|
2698
|
-
if (typeof value !== "string" || !allowEmpty && value.length === 0) {
|
|
2699
|
-
throw new Error(`MCP ${label} must be ${allowEmpty ? "a string" : "a non-empty string"}`);
|
|
2700
|
-
}
|
|
2701
|
-
if (value.length > MAX_PROTOCOL_INPUT_CHARS) {
|
|
2702
|
-
throw new Error(`MCP ${label} exceeds ${MAX_PROTOCOL_INPUT_CHARS} characters`);
|
|
2703
|
-
}
|
|
2704
|
-
}
|
|
2705
|
-
function pageParams(cursor, label) {
|
|
2706
|
-
if (cursor === void 0) return {};
|
|
2707
|
-
validateProtocolString(cursor, label);
|
|
2708
|
-
return { cursor };
|
|
2709
|
-
}
|
|
2710
|
-
function parseEmptyResult(value) {
|
|
2711
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
2712
|
-
throw new Error("Malformed MCP empty result: expected object");
|
|
2713
|
-
}
|
|
2714
|
-
}
|
|
2715
2770
|
|
|
2716
2771
|
// src/content-selection.ts
|
|
2717
2772
|
var DEFAULT_MCP_INSERTION_MAX_BYTES = 256 * 1024;
|
|
@@ -2856,7 +2911,7 @@ async function writeConfig(path2, cfg) {
|
|
|
2856
2911
|
try {
|
|
2857
2912
|
await fs.rename(tmp, path2);
|
|
2858
2913
|
} catch (err) {
|
|
2859
|
-
await fs.rm(tmp, { force: true })
|
|
2914
|
+
await fs.rm(tmp, { force: true });
|
|
2860
2915
|
throw err;
|
|
2861
2916
|
}
|
|
2862
2917
|
}
|
|
@@ -3096,7 +3151,7 @@ function forgetRegistryState(registry, name) {
|
|
|
3096
3151
|
}
|
|
3097
3152
|
|
|
3098
3153
|
// src/manifest-cache.ts
|
|
3099
|
-
import { createHash as createHash2 } from "node:crypto";
|
|
3154
|
+
import { createHash as createHash2, randomBytes as randomBytes4 } from "node:crypto";
|
|
3100
3155
|
import * as fs2 from "node:fs/promises";
|
|
3101
3156
|
import * as path from "node:path";
|
|
3102
3157
|
function manifestConfigHash(cfg) {
|
|
@@ -3144,9 +3199,14 @@ async function writeCapabilityManifest(cacheDir, name, configHash, manifest) {
|
|
|
3144
3199
|
const file = manifestFile(cacheDir, name);
|
|
3145
3200
|
await fs2.mkdir(path.dirname(file), { recursive: true });
|
|
3146
3201
|
const body = { version: 2, configHash, ...manifest };
|
|
3147
|
-
const tmp = `${file}.tmp`;
|
|
3148
|
-
|
|
3149
|
-
|
|
3202
|
+
const tmp = `${file}.${process.pid}.${randomBytes4(6).toString("hex")}.tmp`;
|
|
3203
|
+
try {
|
|
3204
|
+
await fs2.writeFile(tmp, JSON.stringify(body, null, 2), "utf8");
|
|
3205
|
+
await fs2.rename(tmp, file);
|
|
3206
|
+
} catch (err) {
|
|
3207
|
+
await fs2.rm(tmp, { force: true });
|
|
3208
|
+
throw err;
|
|
3209
|
+
}
|
|
3150
3210
|
} catch {
|
|
3151
3211
|
}
|
|
3152
3212
|
}
|
|
@@ -3207,7 +3267,10 @@ function evaluateHealthThresholds(operations, thresholds) {
|
|
|
3207
3267
|
if (!thresholds) return [];
|
|
3208
3268
|
const checks = [];
|
|
3209
3269
|
if (thresholds.connectionLatencyP95Ms !== void 0 && operations.connectionSamples.length > 0) {
|
|
3210
|
-
const value = percentile(
|
|
3270
|
+
const value = percentile(
|
|
3271
|
+
[...operations.connectionSamples].sort((a, b) => a - b),
|
|
3272
|
+
0.95
|
|
3273
|
+
);
|
|
3211
3274
|
checks.push({
|
|
3212
3275
|
name: "connection-latency-p95",
|
|
3213
3276
|
passed: value <= thresholds.connectionLatencyP95Ms,
|
|
@@ -3216,7 +3279,10 @@ function evaluateHealthThresholds(operations, thresholds) {
|
|
|
3216
3279
|
});
|
|
3217
3280
|
}
|
|
3218
3281
|
if (thresholds.discoveryLatencyP95Ms !== void 0 && operations.discoverySamples.length > 0) {
|
|
3219
|
-
const value = percentile(
|
|
3282
|
+
const value = percentile(
|
|
3283
|
+
[...operations.discoverySamples].sort((a, b) => a - b),
|
|
3284
|
+
0.95
|
|
3285
|
+
);
|
|
3220
3286
|
checks.push({
|
|
3221
3287
|
name: "discovery-latency-p95",
|
|
3222
3288
|
passed: value <= thresholds.discoveryLatencyP95Ms,
|
|
@@ -3225,7 +3291,10 @@ function evaluateHealthThresholds(operations, thresholds) {
|
|
|
3225
3291
|
});
|
|
3226
3292
|
}
|
|
3227
3293
|
if (thresholds.callLatencyP95Ms !== void 0 && operations.callSamples.length > 0) {
|
|
3228
|
-
const value = percentile(
|
|
3294
|
+
const value = percentile(
|
|
3295
|
+
[...operations.callSamples].sort((a, b) => a - b),
|
|
3296
|
+
0.95
|
|
3297
|
+
);
|
|
3229
3298
|
checks.push({
|
|
3230
3299
|
name: "call-latency-p95",
|
|
3231
3300
|
passed: value <= thresholds.callLatencyP95Ms,
|
|
@@ -3275,6 +3344,230 @@ function percentile(sorted, ratio) {
|
|
|
3275
3344
|
// src/registry.ts
|
|
3276
3345
|
import { expectDefined } from "@wrongstack/core/utils";
|
|
3277
3346
|
|
|
3347
|
+
// src/registry-catalog.ts
|
|
3348
|
+
var MAX_CATALOG_PAGES = 100;
|
|
3349
|
+
var MAX_CATALOG_ITEMS = 1e4;
|
|
3350
|
+
async function collectCatalogPages(load, select) {
|
|
3351
|
+
const items = [];
|
|
3352
|
+
const seenCursors = /* @__PURE__ */ new Set();
|
|
3353
|
+
let cursor;
|
|
3354
|
+
for (let pageNumber = 0; pageNumber < MAX_CATALOG_PAGES; pageNumber++) {
|
|
3355
|
+
const page = await load(cursor);
|
|
3356
|
+
items.push(...select(page));
|
|
3357
|
+
if (items.length > MAX_CATALOG_ITEMS) {
|
|
3358
|
+
throw new Error(`MCP catalog exceeds ${MAX_CATALOG_ITEMS} items`);
|
|
3359
|
+
}
|
|
3360
|
+
const next = page.nextCursor;
|
|
3361
|
+
if (!next) return items;
|
|
3362
|
+
if (seenCursors.has(next)) throw new Error(`MCP catalog repeated cursor "${next}"`);
|
|
3363
|
+
seenCursors.add(next);
|
|
3364
|
+
cursor = next;
|
|
3365
|
+
}
|
|
3366
|
+
throw new Error(`MCP catalog exceeds ${MAX_CATALOG_PAGES} pages`);
|
|
3367
|
+
}
|
|
3368
|
+
function cloneCatalogRecords(records) {
|
|
3369
|
+
return structuredClone(records);
|
|
3370
|
+
}
|
|
3371
|
+
function registryCatalogSnapshot(slot) {
|
|
3372
|
+
return {
|
|
3373
|
+
name: slot.cfg.name,
|
|
3374
|
+
state: slot.state,
|
|
3375
|
+
serverMetadata: slot.serverMetadata ? structuredClone(slot.serverMetadata) : void 0,
|
|
3376
|
+
resources: slot.resources ? cloneCatalogRecords(slot.resources) : void 0,
|
|
3377
|
+
resourceTemplates: slot.resourceTemplates ? cloneCatalogRecords(slot.resourceTemplates) : void 0,
|
|
3378
|
+
prompts: slot.prompts ? cloneCatalogRecords(slot.prompts) : void 0
|
|
3379
|
+
};
|
|
3380
|
+
}
|
|
3381
|
+
|
|
3382
|
+
// src/registry-health.ts
|
|
3383
|
+
function buildRegistryOperationalHealth(servers, disabledServers) {
|
|
3384
|
+
const active = Array.from(servers).map((slot) => {
|
|
3385
|
+
const op = slot.operations;
|
|
3386
|
+
const baseHealth = healthStateFor(slot.state, op, slot.cfg.enabled !== false);
|
|
3387
|
+
const checks = evaluateHealthThresholds(op, slot.cfg.health?.thresholds);
|
|
3388
|
+
return {
|
|
3389
|
+
name: slot.cfg.name,
|
|
3390
|
+
connectionState: slot.state,
|
|
3391
|
+
healthState: applyHealthThresholds(baseHealth, checks),
|
|
3392
|
+
lastSuccessAt: op.lastSuccessAt,
|
|
3393
|
+
lastFailureAt: op.lastFailureAt,
|
|
3394
|
+
lastFailureKind: op.lastFailureKind,
|
|
3395
|
+
lastReason: op.lastReason,
|
|
3396
|
+
consecutiveFailures: op.consecutiveFailures,
|
|
3397
|
+
failures: { ...op.failures },
|
|
3398
|
+
reconnectCount: op.reconnectCount,
|
|
3399
|
+
wakeCount: op.wakeCount,
|
|
3400
|
+
sleepCount: op.sleepCount,
|
|
3401
|
+
restartCount: op.restartCount,
|
|
3402
|
+
connectionLatency: summarizeLatency(op.connectionSamples),
|
|
3403
|
+
discoveryLatency: summarizeLatency(op.discoverySamples),
|
|
3404
|
+
callLatency: summarizeLatency(op.callSamples),
|
|
3405
|
+
inFlightCalls: op.inFlightCalls,
|
|
3406
|
+
peakInFlightCalls: op.peakInFlightCalls,
|
|
3407
|
+
recentEvents: op.recentEvents.map((event) => ({ ...event })),
|
|
3408
|
+
healthChecks: checks
|
|
3409
|
+
};
|
|
3410
|
+
});
|
|
3411
|
+
const disabled = Array.from(disabledServers).map((cfg) => {
|
|
3412
|
+
const operations = createMCPServerOperationState();
|
|
3413
|
+
return {
|
|
3414
|
+
name: cfg.name,
|
|
3415
|
+
connectionState: "idle",
|
|
3416
|
+
healthState: "disabled",
|
|
3417
|
+
consecutiveFailures: 0,
|
|
3418
|
+
failures: { ...operations.failures },
|
|
3419
|
+
reconnectCount: 0,
|
|
3420
|
+
wakeCount: 0,
|
|
3421
|
+
sleepCount: 0,
|
|
3422
|
+
restartCount: 0,
|
|
3423
|
+
connectionLatency: summarizeLatency([]),
|
|
3424
|
+
discoveryLatency: summarizeLatency([]),
|
|
3425
|
+
callLatency: summarizeLatency([]),
|
|
3426
|
+
inFlightCalls: 0,
|
|
3427
|
+
peakInFlightCalls: 0,
|
|
3428
|
+
recentEvents: [],
|
|
3429
|
+
healthChecks: []
|
|
3430
|
+
};
|
|
3431
|
+
});
|
|
3432
|
+
return [...active, ...disabled];
|
|
3433
|
+
}
|
|
3434
|
+
|
|
3435
|
+
// src/registry-authorization.ts
|
|
3436
|
+
function requireAuthorizationManager(manager) {
|
|
3437
|
+
if (!manager) {
|
|
3438
|
+
throw new Error("MCP authorization management is not configured for this host");
|
|
3439
|
+
}
|
|
3440
|
+
return manager;
|
|
3441
|
+
}
|
|
3442
|
+
function requireHttpServerConfig(servers, disabledServers, name) {
|
|
3443
|
+
const cfg = servers.get(name)?.cfg ?? disabledServers.get(name);
|
|
3444
|
+
if (!cfg) throw new Error(`MCP server "${name}" not registered`);
|
|
3445
|
+
if (cfg.transport === "stdio" || !cfg.url) {
|
|
3446
|
+
throw new Error(`MCP server "${name}" does not use an HTTP transport`);
|
|
3447
|
+
}
|
|
3448
|
+
return cfg;
|
|
3449
|
+
}
|
|
3450
|
+
async function beginRegistryAuthorization(manager, cfg, name, input) {
|
|
3451
|
+
return requireAuthorizationManager(manager).begin({
|
|
3452
|
+
serverName: name,
|
|
3453
|
+
resource: cfg.url,
|
|
3454
|
+
...input
|
|
3455
|
+
});
|
|
3456
|
+
}
|
|
3457
|
+
async function completeRegistryAuthorization(manager, cfg, name, callbackUrl, signal) {
|
|
3458
|
+
return requireAuthorizationManager(manager).complete({
|
|
3459
|
+
serverName: name,
|
|
3460
|
+
resource: cfg.url,
|
|
3461
|
+
callbackUrl,
|
|
3462
|
+
signal
|
|
3463
|
+
});
|
|
3464
|
+
}
|
|
3465
|
+
|
|
3466
|
+
// src/registry-operations.ts
|
|
3467
|
+
function operationsForSlot(slot) {
|
|
3468
|
+
if (!slot.operations) slot.operations = createMCPServerOperationState();
|
|
3469
|
+
return slot.operations;
|
|
3470
|
+
}
|
|
3471
|
+
function recordRegistrySuccess(slot, resetFailures = true) {
|
|
3472
|
+
const operations = operationsForSlot(slot);
|
|
3473
|
+
operations.lastSuccessAt = Date.now();
|
|
3474
|
+
if (resetFailures) operations.consecutiveFailures = 0;
|
|
3475
|
+
}
|
|
3476
|
+
function recordRegistryFailure(slot, listeners, failureKind, reason, durationMs) {
|
|
3477
|
+
const operations = operationsForSlot(slot);
|
|
3478
|
+
const safeReason = safeOperationReason(reason);
|
|
3479
|
+
operations.lastFailureAt = Date.now();
|
|
3480
|
+
operations.lastFailureKind = failureKind;
|
|
3481
|
+
operations.lastReason = safeReason;
|
|
3482
|
+
operations.consecutiveFailures++;
|
|
3483
|
+
operations.failures[failureKind]++;
|
|
3484
|
+
recordRegistryOperation(slot, listeners, "failure", safeReason, failureKind, durationMs);
|
|
3485
|
+
}
|
|
3486
|
+
function recordRegistryOperation(slot, listeners, kind, reason, failureKind, durationMs, retain = true) {
|
|
3487
|
+
const operations = operationsForSlot(slot);
|
|
3488
|
+
const baseHealth = healthStateFor(slot.state, operations, slot.cfg.enabled !== false);
|
|
3489
|
+
const checks = evaluateHealthThresholds(operations, slot.cfg.health?.thresholds);
|
|
3490
|
+
const event = {
|
|
3491
|
+
serverName: slot.cfg.name,
|
|
3492
|
+
kind,
|
|
3493
|
+
at: Date.now(),
|
|
3494
|
+
connectionState: slot.state,
|
|
3495
|
+
healthState: applyHealthThresholds(baseHealth, checks)
|
|
3496
|
+
};
|
|
3497
|
+
if (reason !== void 0) event.reason = safeOperationReason(reason);
|
|
3498
|
+
if (failureKind !== void 0) event.failureKind = failureKind;
|
|
3499
|
+
if (durationMs !== void 0) event.durationMs = Math.max(0, Math.round(durationMs));
|
|
3500
|
+
if (retain) pushBounded(operations.recentEvents, event, MCP_OPERATION_LIMITS.RECENT_EVENTS);
|
|
3501
|
+
for (const listener of listeners) {
|
|
3502
|
+
try {
|
|
3503
|
+
listener({ ...event });
|
|
3504
|
+
} catch {
|
|
3505
|
+
}
|
|
3506
|
+
}
|
|
3507
|
+
}
|
|
3508
|
+
|
|
3509
|
+
// src/registry-disconnect.ts
|
|
3510
|
+
function resetDisconnectedSlotTools(slot, toolRegistry) {
|
|
3511
|
+
for (const t of slot.toolNames) {
|
|
3512
|
+
try {
|
|
3513
|
+
toolRegistry.unregister(t);
|
|
3514
|
+
} catch {
|
|
3515
|
+
}
|
|
3516
|
+
}
|
|
3517
|
+
slot.toolNames = [];
|
|
3518
|
+
slot.lazyTools = [];
|
|
3519
|
+
slot.serverMetadata = void 0;
|
|
3520
|
+
slot.resources = void 0;
|
|
3521
|
+
slot.resourceTemplates = void 0;
|
|
3522
|
+
slot.prompts = void 0;
|
|
3523
|
+
}
|
|
3524
|
+
function markLazySlotDormant(slot, events, reason) {
|
|
3525
|
+
slot.client = void 0;
|
|
3526
|
+
slot.state = "dormant";
|
|
3527
|
+
events.emit("mcp.server.disconnected", {
|
|
3528
|
+
name: slot.cfg.name,
|
|
3529
|
+
reason: `${reason} (dormant)`
|
|
3530
|
+
});
|
|
3531
|
+
}
|
|
3532
|
+
|
|
3533
|
+
// src/registry-reconnect.ts
|
|
3534
|
+
function scheduleRegistryReconnect({
|
|
3535
|
+
slot,
|
|
3536
|
+
events,
|
|
3537
|
+
log,
|
|
3538
|
+
maxReconnectCycles,
|
|
3539
|
+
baseReconnectDelayMs,
|
|
3540
|
+
maxReconnectDelayMs,
|
|
3541
|
+
recordReconnectExhausted,
|
|
3542
|
+
attemptReconnect
|
|
3543
|
+
}) {
|
|
3544
|
+
if (slot.reconnectPending) return;
|
|
3545
|
+
if (slot.reconnectCycles >= maxReconnectCycles) {
|
|
3546
|
+
slot.state = "failed";
|
|
3547
|
+
recordReconnectExhausted(slot);
|
|
3548
|
+
log.error(
|
|
3549
|
+
`MCP server "${slot.cfg.name}" giving up after ${slot.reconnectCycles} reconnect cycles. Use \`/mcp restart ${slot.cfg.name}\` to retry.`
|
|
3550
|
+
);
|
|
3551
|
+
events.emit("mcp.server.disconnected", {
|
|
3552
|
+
name: slot.cfg.name,
|
|
3553
|
+
reason: `reconnect-exhausted:${slot.reconnectCycles}`
|
|
3554
|
+
});
|
|
3555
|
+
return;
|
|
3556
|
+
}
|
|
3557
|
+
slot.reconnectPending = true;
|
|
3558
|
+
if (slot.reconnectTimer) {
|
|
3559
|
+
clearTimeout(slot.reconnectTimer);
|
|
3560
|
+
slot.reconnectTimer = void 0;
|
|
3561
|
+
}
|
|
3562
|
+
const base = Math.min(baseReconnectDelayMs * 2 ** slot.reconnectCycles, maxReconnectDelayMs);
|
|
3563
|
+
const jitter = base * MCP_CONSTANTS.RECONNECT.JITTER_FACTOR * (Math.random() * 2 - 1);
|
|
3564
|
+
const delay = Math.max(100, Math.round(base + jitter));
|
|
3565
|
+
slot.reconnectTimer = setTimeout(() => {
|
|
3566
|
+
slot.reconnectTimer = void 0;
|
|
3567
|
+
void attemptReconnect(slot);
|
|
3568
|
+
}, delay);
|
|
3569
|
+
}
|
|
3570
|
+
|
|
3278
3571
|
// src/wrap-tool.ts
|
|
3279
3572
|
import { ToolCapabilities } from "@wrongstack/core/security";
|
|
3280
3573
|
var MUTATING_RE = /create|update|delete|write|send|set|put|post|patch|remove|rename|move/i;
|
|
@@ -3372,42 +3665,31 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
3372
3665
|
return slot;
|
|
3373
3666
|
}
|
|
3374
3667
|
async beginAuthorization(name, input) {
|
|
3375
|
-
const manager = this.requireAuthorizationManager();
|
|
3376
3668
|
const cfg = this.requireHttpServerConfig(name);
|
|
3377
|
-
return
|
|
3378
|
-
serverName: name,
|
|
3379
|
-
resource: cfg.url,
|
|
3380
|
-
...input
|
|
3381
|
-
});
|
|
3669
|
+
return beginRegistryAuthorization(this.authorizationManager, cfg, name, input);
|
|
3382
3670
|
}
|
|
3383
3671
|
async completeAuthorization(name, callbackUrl, signal) {
|
|
3384
|
-
const manager = this.requireAuthorizationManager();
|
|
3385
3672
|
const cfg = this.requireHttpServerConfig(name);
|
|
3386
|
-
return
|
|
3673
|
+
return completeRegistryAuthorization(
|
|
3674
|
+
this.authorizationManager,
|
|
3675
|
+
cfg,
|
|
3676
|
+
name,
|
|
3677
|
+
callbackUrl,
|
|
3678
|
+
signal
|
|
3679
|
+
);
|
|
3387
3680
|
}
|
|
3388
3681
|
async authorizationStatus(name) {
|
|
3389
|
-
const manager = this.
|
|
3682
|
+
const manager = requireAuthorizationManager(this.authorizationManager);
|
|
3390
3683
|
const cfg = this.requireHttpServerConfig(name);
|
|
3391
3684
|
return manager.status(name, cfg.url);
|
|
3392
3685
|
}
|
|
3393
3686
|
async disconnectAuthorization(name) {
|
|
3394
|
-
const manager = this.
|
|
3687
|
+
const manager = requireAuthorizationManager(this.authorizationManager);
|
|
3395
3688
|
const cfg = this.requireHttpServerConfig(name);
|
|
3396
3689
|
return manager.disconnect(name, cfg.url);
|
|
3397
3690
|
}
|
|
3398
|
-
requireAuthorizationManager() {
|
|
3399
|
-
if (!this.authorizationManager) {
|
|
3400
|
-
throw new Error("MCP authorization management is not configured for this host");
|
|
3401
|
-
}
|
|
3402
|
-
return this.authorizationManager;
|
|
3403
|
-
}
|
|
3404
3691
|
requireHttpServerConfig(name) {
|
|
3405
|
-
|
|
3406
|
-
if (!cfg) throw new Error(`MCP server "${name}" not registered`);
|
|
3407
|
-
if (cfg.transport === "stdio" || !cfg.url) {
|
|
3408
|
-
throw new Error(`MCP server "${name}" does not use an HTTP transport`);
|
|
3409
|
-
}
|
|
3410
|
-
return cfg;
|
|
3692
|
+
return requireHttpServerConfig(this.servers, this.disabledServers, name);
|
|
3411
3693
|
}
|
|
3412
3694
|
async start(cfg) {
|
|
3413
3695
|
if (cfg.enabled === false) {
|
|
@@ -3461,11 +3743,7 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
3461
3743
|
* cache yet, do a one-time cold discovery connect to learn + cache the tools.
|
|
3462
3744
|
*/
|
|
3463
3745
|
async startLazy(slot) {
|
|
3464
|
-
const cacheDir = this.cacheDir;
|
|
3465
|
-
if (!cacheDir) {
|
|
3466
|
-
await this.attemptConnect(slot);
|
|
3467
|
-
return;
|
|
3468
|
-
}
|
|
3746
|
+
const cacheDir = expectDefined(this.cacheDir);
|
|
3469
3747
|
const hash = manifestConfigHash(slot.cfg);
|
|
3470
3748
|
const cached = await readCapabilityManifest(cacheDir, slot.cfg.name, hash);
|
|
3471
3749
|
if (cached) {
|
|
@@ -3627,84 +3905,36 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
3627
3905
|
}
|
|
3628
3906
|
/** Detailed, defensively-copied operational snapshots for CLI/WebUI/HQ. */
|
|
3629
3907
|
operationalHealth() {
|
|
3630
|
-
|
|
3631
|
-
const op = slot.operations;
|
|
3632
|
-
const baseHealth = healthStateFor(slot.state, op, slot.cfg.enabled !== false);
|
|
3633
|
-
const checks = evaluateHealthThresholds(op, slot.cfg.health?.thresholds);
|
|
3634
|
-
return {
|
|
3635
|
-
name: slot.cfg.name,
|
|
3636
|
-
connectionState: slot.state,
|
|
3637
|
-
healthState: applyHealthThresholds(baseHealth, checks),
|
|
3638
|
-
lastSuccessAt: op.lastSuccessAt,
|
|
3639
|
-
lastFailureAt: op.lastFailureAt,
|
|
3640
|
-
lastFailureKind: op.lastFailureKind,
|
|
3641
|
-
lastReason: op.lastReason,
|
|
3642
|
-
consecutiveFailures: op.consecutiveFailures,
|
|
3643
|
-
failures: { ...op.failures },
|
|
3644
|
-
reconnectCount: op.reconnectCount,
|
|
3645
|
-
wakeCount: op.wakeCount,
|
|
3646
|
-
sleepCount: op.sleepCount,
|
|
3647
|
-
restartCount: op.restartCount,
|
|
3648
|
-
connectionLatency: summarizeLatency(op.connectionSamples),
|
|
3649
|
-
discoveryLatency: summarizeLatency(op.discoverySamples),
|
|
3650
|
-
callLatency: summarizeLatency(op.callSamples),
|
|
3651
|
-
inFlightCalls: op.inFlightCalls,
|
|
3652
|
-
peakInFlightCalls: op.peakInFlightCalls,
|
|
3653
|
-
recentEvents: op.recentEvents.map((event) => ({ ...event })),
|
|
3654
|
-
healthChecks: checks
|
|
3655
|
-
};
|
|
3656
|
-
});
|
|
3657
|
-
const disabled = Array.from(this.disabledServers.values()).map((cfg) => {
|
|
3658
|
-
const operations = createMCPServerOperationState();
|
|
3659
|
-
return {
|
|
3660
|
-
name: cfg.name,
|
|
3661
|
-
connectionState: "idle",
|
|
3662
|
-
healthState: "disabled",
|
|
3663
|
-
consecutiveFailures: 0,
|
|
3664
|
-
failures: { ...operations.failures },
|
|
3665
|
-
reconnectCount: 0,
|
|
3666
|
-
wakeCount: 0,
|
|
3667
|
-
sleepCount: 0,
|
|
3668
|
-
restartCount: 0,
|
|
3669
|
-
connectionLatency: summarizeLatency([]),
|
|
3670
|
-
discoveryLatency: summarizeLatency([]),
|
|
3671
|
-
callLatency: summarizeLatency([]),
|
|
3672
|
-
inFlightCalls: 0,
|
|
3673
|
-
peakInFlightCalls: 0,
|
|
3674
|
-
recentEvents: [],
|
|
3675
|
-
healthChecks: []
|
|
3676
|
-
};
|
|
3677
|
-
});
|
|
3678
|
-
return [...active, ...disabled];
|
|
3908
|
+
return buildRegistryOperationalHealth(this.servers.values(), this.disabledServers.values());
|
|
3679
3909
|
}
|
|
3680
3910
|
getCatalog(name) {
|
|
3681
3911
|
const slot = this.servers.get(name);
|
|
3682
3912
|
if (!slot) return void 0;
|
|
3683
|
-
return
|
|
3913
|
+
return registryCatalogSnapshot(slot);
|
|
3684
3914
|
}
|
|
3685
3915
|
async listResources(name, opts = {}) {
|
|
3686
3916
|
const slot = this.requireSlot(name);
|
|
3687
|
-
if (!opts.refresh && slot.resources) return
|
|
3917
|
+
if (!opts.refresh && slot.resources) return cloneCatalogRecords(slot.resources);
|
|
3688
3918
|
const client = await this.ensureConnected(name);
|
|
3689
3919
|
if (!client.getServerMetadata()?.capabilities.resources) return [];
|
|
3690
|
-
slot.resources = await
|
|
3920
|
+
slot.resources = await collectCatalogPages(
|
|
3691
3921
|
(cursor) => client.listResources(cursor ? { cursor } : {}),
|
|
3692
3922
|
(page) => page.resources
|
|
3693
3923
|
);
|
|
3694
3924
|
await this.persistCapabilityManifest(slot);
|
|
3695
|
-
return
|
|
3925
|
+
return cloneCatalogRecords(slot.resources);
|
|
3696
3926
|
}
|
|
3697
3927
|
async listResourceTemplates(name, opts = {}) {
|
|
3698
3928
|
const slot = this.requireSlot(name);
|
|
3699
|
-
if (!opts.refresh && slot.resourceTemplates) return
|
|
3929
|
+
if (!opts.refresh && slot.resourceTemplates) return cloneCatalogRecords(slot.resourceTemplates);
|
|
3700
3930
|
const client = await this.ensureConnected(name);
|
|
3701
3931
|
if (!client.getServerMetadata()?.capabilities.resources) return [];
|
|
3702
|
-
slot.resourceTemplates = await
|
|
3932
|
+
slot.resourceTemplates = await collectCatalogPages(
|
|
3703
3933
|
(cursor) => client.listResourceTemplates(cursor ? { cursor } : {}),
|
|
3704
3934
|
(page) => page.resourceTemplates
|
|
3705
3935
|
);
|
|
3706
3936
|
await this.persistCapabilityManifest(slot);
|
|
3707
|
-
return
|
|
3937
|
+
return cloneCatalogRecords(slot.resourceTemplates);
|
|
3708
3938
|
}
|
|
3709
3939
|
async readResource(name, uri) {
|
|
3710
3940
|
return (await this.ensureConnected(name)).readResource(uri);
|
|
@@ -3720,15 +3950,15 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
3720
3950
|
}
|
|
3721
3951
|
async listPrompts(name, opts = {}) {
|
|
3722
3952
|
const slot = this.requireSlot(name);
|
|
3723
|
-
if (!opts.refresh && slot.prompts) return
|
|
3953
|
+
if (!opts.refresh && slot.prompts) return cloneCatalogRecords(slot.prompts);
|
|
3724
3954
|
const client = await this.ensureConnected(name);
|
|
3725
3955
|
if (!client.getServerMetadata()?.capabilities.prompts) return [];
|
|
3726
|
-
slot.prompts = await
|
|
3956
|
+
slot.prompts = await collectCatalogPages(
|
|
3727
3957
|
(cursor) => client.listPrompts(cursor ? { cursor } : {}),
|
|
3728
3958
|
(page) => page.prompts
|
|
3729
3959
|
);
|
|
3730
3960
|
await this.persistCapabilityManifest(slot);
|
|
3731
|
-
return
|
|
3961
|
+
return cloneCatalogRecords(slot.prompts);
|
|
3732
3962
|
}
|
|
3733
3963
|
async getPrompt(serverName, promptName, args) {
|
|
3734
3964
|
return (await this.ensureConnected(serverName)).getPrompt(promptName, args);
|
|
@@ -3808,7 +4038,7 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
3808
4038
|
const capabilities = slot.serverMetadata?.capabilities;
|
|
3809
4039
|
if (capabilities?.resources) {
|
|
3810
4040
|
try {
|
|
3811
|
-
slot.resources = await
|
|
4041
|
+
slot.resources = await collectCatalogPages(
|
|
3812
4042
|
(cursor) => client.listResources(cursor ? { cursor } : {}),
|
|
3813
4043
|
(page) => page.resources
|
|
3814
4044
|
);
|
|
@@ -3818,7 +4048,7 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
3818
4048
|
this.log.warn(`MCP server "${slot.cfg.name}" resource discovery failed`, err);
|
|
3819
4049
|
}
|
|
3820
4050
|
try {
|
|
3821
|
-
slot.resourceTemplates = await
|
|
4051
|
+
slot.resourceTemplates = await collectCatalogPages(
|
|
3822
4052
|
(cursor) => client.listResourceTemplates(cursor ? { cursor } : {}),
|
|
3823
4053
|
(page) => page.resourceTemplates
|
|
3824
4054
|
);
|
|
@@ -3833,7 +4063,7 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
3833
4063
|
}
|
|
3834
4064
|
if (capabilities?.prompts) {
|
|
3835
4065
|
try {
|
|
3836
|
-
slot.prompts = await
|
|
4066
|
+
slot.prompts = await collectCatalogPages(
|
|
3837
4067
|
(cursor) => client.listPrompts(cursor ? { cursor } : {}),
|
|
3838
4068
|
(page) => page.prompts
|
|
3839
4069
|
);
|
|
@@ -4012,27 +4242,11 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
4012
4242
|
const slot = this.servers.get(name);
|
|
4013
4243
|
if (!slot) return;
|
|
4014
4244
|
if (slot.lazy) {
|
|
4015
|
-
slot.client = void 0;
|
|
4016
|
-
slot.state = "dormant";
|
|
4017
4245
|
this.recordFailure(slot, "transport", "process-exit-lazy");
|
|
4018
|
-
this.events
|
|
4019
|
-
name,
|
|
4020
|
-
reason: `exit:${code ?? "unknown"} (dormant)`
|
|
4021
|
-
});
|
|
4246
|
+
markLazySlotDormant(slot, this.events, `exit:${code ?? "unknown"}`);
|
|
4022
4247
|
return;
|
|
4023
4248
|
}
|
|
4024
|
-
|
|
4025
|
-
try {
|
|
4026
|
-
this.toolRegistry.unregister(t);
|
|
4027
|
-
} catch {
|
|
4028
|
-
}
|
|
4029
|
-
}
|
|
4030
|
-
slot.toolNames = [];
|
|
4031
|
-
slot.lazyTools = [];
|
|
4032
|
-
slot.serverMetadata = void 0;
|
|
4033
|
-
slot.resources = void 0;
|
|
4034
|
-
slot.resourceTemplates = void 0;
|
|
4035
|
-
slot.prompts = void 0;
|
|
4249
|
+
resetDisconnectedSlotTools(slot, this.toolRegistry);
|
|
4036
4250
|
slot.state = "disconnected";
|
|
4037
4251
|
this.recordFailure(slot, "transport", "process-exit");
|
|
4038
4252
|
this.events.emit("mcp.server.disconnected", { name, reason: `exit:${code ?? "unknown"}` });
|
|
@@ -4043,69 +4257,30 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
4043
4257
|
const slot = this.servers.get(name);
|
|
4044
4258
|
if (!slot) return;
|
|
4045
4259
|
if (slot.lazy) {
|
|
4046
|
-
slot.client = void 0;
|
|
4047
|
-
slot.state = "dormant";
|
|
4048
4260
|
this.recordFailure(slot, "transport", "http-disconnect-lazy");
|
|
4049
|
-
this.events
|
|
4261
|
+
markLazySlotDormant(slot, this.events, "http-disconnect");
|
|
4050
4262
|
return;
|
|
4051
4263
|
}
|
|
4052
|
-
|
|
4053
|
-
try {
|
|
4054
|
-
this.toolRegistry.unregister(t);
|
|
4055
|
-
} catch {
|
|
4056
|
-
}
|
|
4057
|
-
}
|
|
4058
|
-
slot.toolNames = [];
|
|
4059
|
-
slot.lazyTools = [];
|
|
4060
|
-
slot.serverMetadata = void 0;
|
|
4061
|
-
slot.resources = void 0;
|
|
4062
|
-
slot.resourceTemplates = void 0;
|
|
4063
|
-
slot.prompts = void 0;
|
|
4264
|
+
resetDisconnectedSlotTools(slot, this.toolRegistry);
|
|
4064
4265
|
slot.state = "disconnected";
|
|
4065
4266
|
this.recordFailure(slot, "transport", "http-disconnect");
|
|
4066
4267
|
this.events.emit("mcp.server.disconnected", { name, reason: "http-disconnect" });
|
|
4067
4268
|
this.scheduleReconnect(slot);
|
|
4068
4269
|
};
|
|
4069
|
-
/**
|
|
4070
|
-
* L2-B: maximum number of reconnect cycles before staying `failed`.
|
|
4071
|
-
* One cycle = one full `attemptConnect` (which itself may try up to 3
|
|
4072
|
-
* times). Caps total reconnect storm at ~5 cycles, then the slot
|
|
4073
|
-
* needs an explicit `restart()` to re-engage.
|
|
4074
|
-
*/
|
|
4075
4270
|
static MAX_RECONNECT_CYCLES = MCP_CONSTANTS.RECONNECT.MAX_CYCLES;
|
|
4076
|
-
/** Base delay between cycles, in ms. Real delay adds jitter. */
|
|
4077
4271
|
static BASE_RECONNECT_DELAY_MS = MCP_CONSTANTS.RECONNECT.BASE_DELAY_MS;
|
|
4078
|
-
/** Hard ceiling on the inter-cycle delay so the user doesn't wait minutes. */
|
|
4079
4272
|
static MAX_RECONNECT_DELAY_MS = 3e4;
|
|
4080
4273
|
scheduleReconnect(slot) {
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
this.
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
this.
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
});
|
|
4092
|
-
return;
|
|
4093
|
-
}
|
|
4094
|
-
slot.reconnectPending = true;
|
|
4095
|
-
if (slot.reconnectTimer) {
|
|
4096
|
-
clearTimeout(slot.reconnectTimer);
|
|
4097
|
-
slot.reconnectTimer = void 0;
|
|
4098
|
-
}
|
|
4099
|
-
const base = Math.min(
|
|
4100
|
-
_MCPRegistry.BASE_RECONNECT_DELAY_MS * 2 ** slot.reconnectCycles,
|
|
4101
|
-
_MCPRegistry.MAX_RECONNECT_DELAY_MS
|
|
4102
|
-
);
|
|
4103
|
-
const jitter = base * MCP_CONSTANTS.RECONNECT.JITTER_FACTOR * (Math.random() * 2 - 1);
|
|
4104
|
-
const delay = Math.max(100, Math.round(base + jitter));
|
|
4105
|
-
slot.reconnectTimer = setTimeout(() => {
|
|
4106
|
-
slot.reconnectTimer = void 0;
|
|
4107
|
-
void this.attemptReconnect(slot);
|
|
4108
|
-
}, delay);
|
|
4274
|
+
scheduleRegistryReconnect({
|
|
4275
|
+
slot,
|
|
4276
|
+
events: this.events,
|
|
4277
|
+
log: this.log,
|
|
4278
|
+
maxReconnectCycles: _MCPRegistry.MAX_RECONNECT_CYCLES,
|
|
4279
|
+
baseReconnectDelayMs: _MCPRegistry.BASE_RECONNECT_DELAY_MS,
|
|
4280
|
+
maxReconnectDelayMs: _MCPRegistry.MAX_RECONNECT_DELAY_MS,
|
|
4281
|
+
recordReconnectExhausted: (target) => this.recordFailure(target, "transport", "reconnect-exhausted"),
|
|
4282
|
+
attemptReconnect: (target) => this.attemptReconnect(target)
|
|
4283
|
+
});
|
|
4109
4284
|
}
|
|
4110
4285
|
async attemptReconnect(slot) {
|
|
4111
4286
|
slot.reconnectPending = false;
|
|
@@ -4115,48 +4290,21 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
4115
4290
|
await this.attemptConnect(slot);
|
|
4116
4291
|
}
|
|
4117
4292
|
recordSuccess(slot, resetFailures = true) {
|
|
4118
|
-
|
|
4119
|
-
operations.lastSuccessAt = Date.now();
|
|
4120
|
-
if (resetFailures) operations.consecutiveFailures = 0;
|
|
4293
|
+
recordRegistrySuccess(slot, resetFailures);
|
|
4121
4294
|
}
|
|
4122
4295
|
recordFailure(slot, failureKind, reason, durationMs) {
|
|
4123
|
-
|
|
4124
|
-
const safeReason = safeOperationReason(reason);
|
|
4125
|
-
operations.lastFailureAt = Date.now();
|
|
4126
|
-
operations.lastFailureKind = failureKind;
|
|
4127
|
-
operations.lastReason = safeReason;
|
|
4128
|
-
operations.consecutiveFailures++;
|
|
4129
|
-
operations.failures[failureKind]++;
|
|
4130
|
-
this.recordOperation(slot, "failure", safeReason, failureKind, durationMs);
|
|
4296
|
+
recordRegistryFailure(slot, this.operationListeners, failureKind, reason, durationMs);
|
|
4131
4297
|
}
|
|
4132
4298
|
recordOperation(slot, kind, reason, failureKind, durationMs, retain = true) {
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
const event = {
|
|
4137
|
-
serverName: slot.cfg.name,
|
|
4299
|
+
recordRegistryOperation(
|
|
4300
|
+
slot,
|
|
4301
|
+
this.operationListeners,
|
|
4138
4302
|
kind,
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
if (failureKind !== void 0) event.failureKind = failureKind;
|
|
4145
|
-
if (durationMs !== void 0) event.durationMs = Math.max(0, Math.round(durationMs));
|
|
4146
|
-
if (retain) {
|
|
4147
|
-
pushBounded(operations.recentEvents, event, MCP_OPERATION_LIMITS.RECENT_EVENTS);
|
|
4148
|
-
}
|
|
4149
|
-
for (const listener of this.operationListeners) {
|
|
4150
|
-
try {
|
|
4151
|
-
listener({ ...event });
|
|
4152
|
-
} catch {
|
|
4153
|
-
}
|
|
4154
|
-
}
|
|
4155
|
-
}
|
|
4156
|
-
/** Keeps private-method unit fixtures from needing to duplicate every slot field. */
|
|
4157
|
-
operationsFor(slot) {
|
|
4158
|
-
if (!slot.operations) slot.operations = createMCPServerOperationState();
|
|
4159
|
-
return slot.operations;
|
|
4303
|
+
reason,
|
|
4304
|
+
failureKind,
|
|
4305
|
+
durationMs,
|
|
4306
|
+
retain
|
|
4307
|
+
);
|
|
4160
4308
|
}
|
|
4161
4309
|
async attemptConnect(slot) {
|
|
4162
4310
|
const MAX_ATTEMPTS = MCP_CONSTANTS.RECONNECT.MAX_ATTEMPTS;
|
|
@@ -4267,44 +4415,10 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
4267
4415
|
}
|
|
4268
4416
|
}
|
|
4269
4417
|
};
|
|
4270
|
-
var MAX_CATALOG_PAGES = 100;
|
|
4271
|
-
var MAX_CATALOG_ITEMS = 1e4;
|
|
4272
|
-
async function collectPages(load, select) {
|
|
4273
|
-
const items = [];
|
|
4274
|
-
const seenCursors = /* @__PURE__ */ new Set();
|
|
4275
|
-
let cursor;
|
|
4276
|
-
for (let pageNumber = 0; pageNumber < MAX_CATALOG_PAGES; pageNumber++) {
|
|
4277
|
-
const page = await load(cursor);
|
|
4278
|
-
items.push(...select(page));
|
|
4279
|
-
if (items.length > MAX_CATALOG_ITEMS) {
|
|
4280
|
-
throw new Error(`MCP catalog exceeds ${MAX_CATALOG_ITEMS} items`);
|
|
4281
|
-
}
|
|
4282
|
-
const next = page.nextCursor;
|
|
4283
|
-
if (!next) return items;
|
|
4284
|
-
if (seenCursors.has(next)) throw new Error(`MCP catalog repeated cursor "${next}"`);
|
|
4285
|
-
seenCursors.add(next);
|
|
4286
|
-
cursor = next;
|
|
4287
|
-
}
|
|
4288
|
-
throw new Error(`MCP catalog exceeds ${MAX_CATALOG_PAGES} pages`);
|
|
4289
|
-
}
|
|
4290
|
-
function cloneRecords(records) {
|
|
4291
|
-
return structuredClone(records);
|
|
4292
|
-
}
|
|
4293
|
-
function catalogSnapshot(slot) {
|
|
4294
|
-
return {
|
|
4295
|
-
name: slot.cfg.name,
|
|
4296
|
-
state: slot.state,
|
|
4297
|
-
serverMetadata: slot.serverMetadata ? structuredClone(slot.serverMetadata) : void 0,
|
|
4298
|
-
resources: slot.resources ? cloneRecords(slot.resources) : void 0,
|
|
4299
|
-
resourceTemplates: slot.resourceTemplates ? cloneRecords(slot.resourceTemplates) : void 0,
|
|
4300
|
-
prompts: slot.prompts ? cloneRecords(slot.prompts) : void 0
|
|
4301
|
-
};
|
|
4302
|
-
}
|
|
4303
4418
|
|
|
4304
4419
|
// src/server.ts
|
|
4305
4420
|
import { createServer } from "node:http";
|
|
4306
|
-
import { expectDefined as expectDefined2 } from "@wrongstack/core/utils";
|
|
4307
|
-
import { toErrorMessage as toErrorMessage2 } from "@wrongstack/core/utils";
|
|
4421
|
+
import { expectDefined as expectDefined2, toErrorMessage as toErrorMessage2 } from "@wrongstack/core/utils";
|
|
4308
4422
|
var PARSE_ERROR = -32700;
|
|
4309
4423
|
var INVALID_REQUEST = -32600;
|
|
4310
4424
|
var METHOD_NOT_FOUND = -32601;
|
|
@@ -4629,8 +4743,7 @@ function serveHttp(server, opts = {}) {
|
|
|
4629
4743
|
httpServer.once("error", reject);
|
|
4630
4744
|
httpServer.listen(port, host, () => {
|
|
4631
4745
|
httpServer.removeListener("error", reject);
|
|
4632
|
-
const
|
|
4633
|
-
const boundPort = typeof addr === "object" && addr ? addr.port : port;
|
|
4746
|
+
const boundPort = httpServer.address().port;
|
|
4634
4747
|
const displayHost = host === "::1" ? "[::1]" : host;
|
|
4635
4748
|
resolve({
|
|
4636
4749
|
port: boundPort,
|
|
@@ -4662,18 +4775,14 @@ async function handleHttpRequest(server, req, res, token, log) {
|
|
|
4662
4775
|
}
|
|
4663
4776
|
}
|
|
4664
4777
|
let body = "";
|
|
4665
|
-
let tooLarge = false;
|
|
4666
4778
|
req.on("data", (chunk) => {
|
|
4667
|
-
if (tooLarge) return;
|
|
4668
4779
|
body += chunk.toString("utf8");
|
|
4669
4780
|
if (body.length > HTTP_BODY_CAP) {
|
|
4670
|
-
tooLarge = true;
|
|
4671
4781
|
send(413, JSON.stringify({ error: "payload too large" }));
|
|
4672
4782
|
req.destroy();
|
|
4673
4783
|
}
|
|
4674
4784
|
});
|
|
4675
4785
|
req.on("end", () => {
|
|
4676
|
-
if (tooLarge) return;
|
|
4677
4786
|
void server.handleMessage(body).then((out) => {
|
|
4678
4787
|
if (out === null) return send(202, "");
|
|
4679
4788
|
return send(200, out);
|