@wrongstack/mcp 0.295.1 → 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 -28
- package/dist/client.d.ts.map +1 -1
- package/dist/index.js +305 -250
- package/dist/index.js.map +4 -4
- 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/package.json +3 -4
package/dist/index.js
CHANGED
|
@@ -738,7 +738,7 @@ function boundedServerName(value) {
|
|
|
738
738
|
}
|
|
739
739
|
|
|
740
740
|
// src/client.ts
|
|
741
|
-
import { spawn } from "node:child_process";
|
|
741
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
742
742
|
import { buildChildEnv, toErrorMessage } from "@wrongstack/core/utils";
|
|
743
743
|
|
|
744
744
|
// src/constants.ts
|
|
@@ -2018,7 +2018,33 @@ var StreamableHTTPTransport = class extends BaseHTTPTransport {
|
|
|
2018
2018
|
}
|
|
2019
2019
|
};
|
|
2020
2020
|
|
|
2021
|
-
// src/client.ts
|
|
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
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
// src/client-process.ts
|
|
2047
|
+
import { spawn } from "node:child_process";
|
|
2022
2048
|
function forceKillTree(child) {
|
|
2023
2049
|
if (child.pid === void 0) {
|
|
2024
2050
|
try {
|
|
@@ -2046,6 +2072,8 @@ function forceKillTree(child) {
|
|
|
2046
2072
|
} catch {
|
|
2047
2073
|
}
|
|
2048
2074
|
}
|
|
2075
|
+
|
|
2076
|
+
// src/client.ts
|
|
2049
2077
|
var MCPClient = class _MCPClient {
|
|
2050
2078
|
constructor(opts) {
|
|
2051
2079
|
this.opts = opts;
|
|
@@ -2160,14 +2188,14 @@ var MCPClient = class _MCPClient {
|
|
|
2160
2188
|
const rawArgs = this.opts.args ?? [];
|
|
2161
2189
|
const spawnEnv = buildChildEnv({ extra: extraEnv });
|
|
2162
2190
|
const stdio = ["pipe", "pipe", "pipe"];
|
|
2163
|
-
const child = isWin ?
|
|
2191
|
+
const child = isWin ? spawn2([this.opts.command, ...rawArgs].map(quoteWindowsArg).join(" "), {
|
|
2164
2192
|
env: spawnEnv,
|
|
2165
2193
|
stdio,
|
|
2166
2194
|
shell: true,
|
|
2167
2195
|
// Without this every MCP server spawned from a console-less host
|
|
2168
2196
|
// (WebUI server, scheduled runs) opens a visible console window.
|
|
2169
2197
|
windowsHide: true
|
|
2170
|
-
}) :
|
|
2198
|
+
}) : spawn2(this.opts.command, rawArgs, { env: spawnEnv, stdio, windowsHide: true });
|
|
2171
2199
|
this.child = child;
|
|
2172
2200
|
child.stdout?.on("data", (chunk) => this.onData(chunk.toString()));
|
|
2173
2201
|
child.stderr?.on("data", () => {
|
|
@@ -2739,29 +2767,6 @@ var MCPClient = class _MCPClient {
|
|
|
2739
2767
|
}
|
|
2740
2768
|
}
|
|
2741
2769
|
};
|
|
2742
|
-
function quoteWindowsArg(arg) {
|
|
2743
|
-
if (!/[\s"]/.test(arg)) return arg;
|
|
2744
|
-
return `"${arg.replace(/"/g, '""')}"`;
|
|
2745
|
-
}
|
|
2746
|
-
var MAX_PROTOCOL_INPUT_CHARS = 8192;
|
|
2747
|
-
function validateProtocolString(value, label, allowEmpty = false) {
|
|
2748
|
-
if (typeof value !== "string" || !allowEmpty && value.length === 0) {
|
|
2749
|
-
throw new Error(`MCP ${label} must be ${allowEmpty ? "a string" : "a non-empty string"}`);
|
|
2750
|
-
}
|
|
2751
|
-
if (value.length > MAX_PROTOCOL_INPUT_CHARS) {
|
|
2752
|
-
throw new Error(`MCP ${label} exceeds ${MAX_PROTOCOL_INPUT_CHARS} characters`);
|
|
2753
|
-
}
|
|
2754
|
-
}
|
|
2755
|
-
function pageParams(cursor, label) {
|
|
2756
|
-
if (cursor === void 0) return {};
|
|
2757
|
-
validateProtocolString(cursor, label);
|
|
2758
|
-
return { cursor };
|
|
2759
|
-
}
|
|
2760
|
-
function parseEmptyResult(value) {
|
|
2761
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
2762
|
-
throw new Error("Malformed MCP empty result: expected object");
|
|
2763
|
-
}
|
|
2764
|
-
}
|
|
2765
2770
|
|
|
2766
2771
|
// src/content-selection.ts
|
|
2767
2772
|
var DEFAULT_MCP_INSERTION_MAX_BYTES = 256 * 1024;
|
|
@@ -3339,6 +3344,230 @@ function percentile(sorted, ratio) {
|
|
|
3339
3344
|
// src/registry.ts
|
|
3340
3345
|
import { expectDefined } from "@wrongstack/core/utils";
|
|
3341
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
|
+
|
|
3342
3571
|
// src/wrap-tool.ts
|
|
3343
3572
|
import { ToolCapabilities } from "@wrongstack/core/security";
|
|
3344
3573
|
var MUTATING_RE = /create|update|delete|write|send|set|put|post|patch|remove|rename|move/i;
|
|
@@ -3436,42 +3665,31 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
3436
3665
|
return slot;
|
|
3437
3666
|
}
|
|
3438
3667
|
async beginAuthorization(name, input) {
|
|
3439
|
-
const manager = this.requireAuthorizationManager();
|
|
3440
3668
|
const cfg = this.requireHttpServerConfig(name);
|
|
3441
|
-
return
|
|
3442
|
-
serverName: name,
|
|
3443
|
-
resource: cfg.url,
|
|
3444
|
-
...input
|
|
3445
|
-
});
|
|
3669
|
+
return beginRegistryAuthorization(this.authorizationManager, cfg, name, input);
|
|
3446
3670
|
}
|
|
3447
3671
|
async completeAuthorization(name, callbackUrl, signal) {
|
|
3448
|
-
const manager = this.requireAuthorizationManager();
|
|
3449
3672
|
const cfg = this.requireHttpServerConfig(name);
|
|
3450
|
-
return
|
|
3673
|
+
return completeRegistryAuthorization(
|
|
3674
|
+
this.authorizationManager,
|
|
3675
|
+
cfg,
|
|
3676
|
+
name,
|
|
3677
|
+
callbackUrl,
|
|
3678
|
+
signal
|
|
3679
|
+
);
|
|
3451
3680
|
}
|
|
3452
3681
|
async authorizationStatus(name) {
|
|
3453
|
-
const manager = this.
|
|
3682
|
+
const manager = requireAuthorizationManager(this.authorizationManager);
|
|
3454
3683
|
const cfg = this.requireHttpServerConfig(name);
|
|
3455
3684
|
return manager.status(name, cfg.url);
|
|
3456
3685
|
}
|
|
3457
3686
|
async disconnectAuthorization(name) {
|
|
3458
|
-
const manager = this.
|
|
3687
|
+
const manager = requireAuthorizationManager(this.authorizationManager);
|
|
3459
3688
|
const cfg = this.requireHttpServerConfig(name);
|
|
3460
3689
|
return manager.disconnect(name, cfg.url);
|
|
3461
3690
|
}
|
|
3462
|
-
requireAuthorizationManager() {
|
|
3463
|
-
if (!this.authorizationManager) {
|
|
3464
|
-
throw new Error("MCP authorization management is not configured for this host");
|
|
3465
|
-
}
|
|
3466
|
-
return this.authorizationManager;
|
|
3467
|
-
}
|
|
3468
3691
|
requireHttpServerConfig(name) {
|
|
3469
|
-
|
|
3470
|
-
if (!cfg) throw new Error(`MCP server "${name}" not registered`);
|
|
3471
|
-
if (cfg.transport === "stdio" || !cfg.url) {
|
|
3472
|
-
throw new Error(`MCP server "${name}" does not use an HTTP transport`);
|
|
3473
|
-
}
|
|
3474
|
-
return cfg;
|
|
3692
|
+
return requireHttpServerConfig(this.servers, this.disabledServers, name);
|
|
3475
3693
|
}
|
|
3476
3694
|
async start(cfg) {
|
|
3477
3695
|
if (cfg.enabled === false) {
|
|
@@ -3687,84 +3905,36 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
3687
3905
|
}
|
|
3688
3906
|
/** Detailed, defensively-copied operational snapshots for CLI/WebUI/HQ. */
|
|
3689
3907
|
operationalHealth() {
|
|
3690
|
-
|
|
3691
|
-
const op = slot.operations;
|
|
3692
|
-
const baseHealth = healthStateFor(slot.state, op, slot.cfg.enabled !== false);
|
|
3693
|
-
const checks = evaluateHealthThresholds(op, slot.cfg.health?.thresholds);
|
|
3694
|
-
return {
|
|
3695
|
-
name: slot.cfg.name,
|
|
3696
|
-
connectionState: slot.state,
|
|
3697
|
-
healthState: applyHealthThresholds(baseHealth, checks),
|
|
3698
|
-
lastSuccessAt: op.lastSuccessAt,
|
|
3699
|
-
lastFailureAt: op.lastFailureAt,
|
|
3700
|
-
lastFailureKind: op.lastFailureKind,
|
|
3701
|
-
lastReason: op.lastReason,
|
|
3702
|
-
consecutiveFailures: op.consecutiveFailures,
|
|
3703
|
-
failures: { ...op.failures },
|
|
3704
|
-
reconnectCount: op.reconnectCount,
|
|
3705
|
-
wakeCount: op.wakeCount,
|
|
3706
|
-
sleepCount: op.sleepCount,
|
|
3707
|
-
restartCount: op.restartCount,
|
|
3708
|
-
connectionLatency: summarizeLatency(op.connectionSamples),
|
|
3709
|
-
discoveryLatency: summarizeLatency(op.discoverySamples),
|
|
3710
|
-
callLatency: summarizeLatency(op.callSamples),
|
|
3711
|
-
inFlightCalls: op.inFlightCalls,
|
|
3712
|
-
peakInFlightCalls: op.peakInFlightCalls,
|
|
3713
|
-
recentEvents: op.recentEvents.map((event) => ({ ...event })),
|
|
3714
|
-
healthChecks: checks
|
|
3715
|
-
};
|
|
3716
|
-
});
|
|
3717
|
-
const disabled = Array.from(this.disabledServers.values()).map((cfg) => {
|
|
3718
|
-
const operations = createMCPServerOperationState();
|
|
3719
|
-
return {
|
|
3720
|
-
name: cfg.name,
|
|
3721
|
-
connectionState: "idle",
|
|
3722
|
-
healthState: "disabled",
|
|
3723
|
-
consecutiveFailures: 0,
|
|
3724
|
-
failures: { ...operations.failures },
|
|
3725
|
-
reconnectCount: 0,
|
|
3726
|
-
wakeCount: 0,
|
|
3727
|
-
sleepCount: 0,
|
|
3728
|
-
restartCount: 0,
|
|
3729
|
-
connectionLatency: summarizeLatency([]),
|
|
3730
|
-
discoveryLatency: summarizeLatency([]),
|
|
3731
|
-
callLatency: summarizeLatency([]),
|
|
3732
|
-
inFlightCalls: 0,
|
|
3733
|
-
peakInFlightCalls: 0,
|
|
3734
|
-
recentEvents: [],
|
|
3735
|
-
healthChecks: []
|
|
3736
|
-
};
|
|
3737
|
-
});
|
|
3738
|
-
return [...active, ...disabled];
|
|
3908
|
+
return buildRegistryOperationalHealth(this.servers.values(), this.disabledServers.values());
|
|
3739
3909
|
}
|
|
3740
3910
|
getCatalog(name) {
|
|
3741
3911
|
const slot = this.servers.get(name);
|
|
3742
3912
|
if (!slot) return void 0;
|
|
3743
|
-
return
|
|
3913
|
+
return registryCatalogSnapshot(slot);
|
|
3744
3914
|
}
|
|
3745
3915
|
async listResources(name, opts = {}) {
|
|
3746
3916
|
const slot = this.requireSlot(name);
|
|
3747
|
-
if (!opts.refresh && slot.resources) return
|
|
3917
|
+
if (!opts.refresh && slot.resources) return cloneCatalogRecords(slot.resources);
|
|
3748
3918
|
const client = await this.ensureConnected(name);
|
|
3749
3919
|
if (!client.getServerMetadata()?.capabilities.resources) return [];
|
|
3750
|
-
slot.resources = await
|
|
3920
|
+
slot.resources = await collectCatalogPages(
|
|
3751
3921
|
(cursor) => client.listResources(cursor ? { cursor } : {}),
|
|
3752
3922
|
(page) => page.resources
|
|
3753
3923
|
);
|
|
3754
3924
|
await this.persistCapabilityManifest(slot);
|
|
3755
|
-
return
|
|
3925
|
+
return cloneCatalogRecords(slot.resources);
|
|
3756
3926
|
}
|
|
3757
3927
|
async listResourceTemplates(name, opts = {}) {
|
|
3758
3928
|
const slot = this.requireSlot(name);
|
|
3759
|
-
if (!opts.refresh && slot.resourceTemplates) return
|
|
3929
|
+
if (!opts.refresh && slot.resourceTemplates) return cloneCatalogRecords(slot.resourceTemplates);
|
|
3760
3930
|
const client = await this.ensureConnected(name);
|
|
3761
3931
|
if (!client.getServerMetadata()?.capabilities.resources) return [];
|
|
3762
|
-
slot.resourceTemplates = await
|
|
3932
|
+
slot.resourceTemplates = await collectCatalogPages(
|
|
3763
3933
|
(cursor) => client.listResourceTemplates(cursor ? { cursor } : {}),
|
|
3764
3934
|
(page) => page.resourceTemplates
|
|
3765
3935
|
);
|
|
3766
3936
|
await this.persistCapabilityManifest(slot);
|
|
3767
|
-
return
|
|
3937
|
+
return cloneCatalogRecords(slot.resourceTemplates);
|
|
3768
3938
|
}
|
|
3769
3939
|
async readResource(name, uri) {
|
|
3770
3940
|
return (await this.ensureConnected(name)).readResource(uri);
|
|
@@ -3780,15 +3950,15 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
3780
3950
|
}
|
|
3781
3951
|
async listPrompts(name, opts = {}) {
|
|
3782
3952
|
const slot = this.requireSlot(name);
|
|
3783
|
-
if (!opts.refresh && slot.prompts) return
|
|
3953
|
+
if (!opts.refresh && slot.prompts) return cloneCatalogRecords(slot.prompts);
|
|
3784
3954
|
const client = await this.ensureConnected(name);
|
|
3785
3955
|
if (!client.getServerMetadata()?.capabilities.prompts) return [];
|
|
3786
|
-
slot.prompts = await
|
|
3956
|
+
slot.prompts = await collectCatalogPages(
|
|
3787
3957
|
(cursor) => client.listPrompts(cursor ? { cursor } : {}),
|
|
3788
3958
|
(page) => page.prompts
|
|
3789
3959
|
);
|
|
3790
3960
|
await this.persistCapabilityManifest(slot);
|
|
3791
|
-
return
|
|
3961
|
+
return cloneCatalogRecords(slot.prompts);
|
|
3792
3962
|
}
|
|
3793
3963
|
async getPrompt(serverName, promptName, args) {
|
|
3794
3964
|
return (await this.ensureConnected(serverName)).getPrompt(promptName, args);
|
|
@@ -3868,7 +4038,7 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
3868
4038
|
const capabilities = slot.serverMetadata?.capabilities;
|
|
3869
4039
|
if (capabilities?.resources) {
|
|
3870
4040
|
try {
|
|
3871
|
-
slot.resources = await
|
|
4041
|
+
slot.resources = await collectCatalogPages(
|
|
3872
4042
|
(cursor) => client.listResources(cursor ? { cursor } : {}),
|
|
3873
4043
|
(page) => page.resources
|
|
3874
4044
|
);
|
|
@@ -3878,7 +4048,7 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
3878
4048
|
this.log.warn(`MCP server "${slot.cfg.name}" resource discovery failed`, err);
|
|
3879
4049
|
}
|
|
3880
4050
|
try {
|
|
3881
|
-
slot.resourceTemplates = await
|
|
4051
|
+
slot.resourceTemplates = await collectCatalogPages(
|
|
3882
4052
|
(cursor) => client.listResourceTemplates(cursor ? { cursor } : {}),
|
|
3883
4053
|
(page) => page.resourceTemplates
|
|
3884
4054
|
);
|
|
@@ -3893,7 +4063,7 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
3893
4063
|
}
|
|
3894
4064
|
if (capabilities?.prompts) {
|
|
3895
4065
|
try {
|
|
3896
|
-
slot.prompts = await
|
|
4066
|
+
slot.prompts = await collectCatalogPages(
|
|
3897
4067
|
(cursor) => client.listPrompts(cursor ? { cursor } : {}),
|
|
3898
4068
|
(page) => page.prompts
|
|
3899
4069
|
);
|
|
@@ -4072,27 +4242,11 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
4072
4242
|
const slot = this.servers.get(name);
|
|
4073
4243
|
if (!slot) return;
|
|
4074
4244
|
if (slot.lazy) {
|
|
4075
|
-
slot.client = void 0;
|
|
4076
|
-
slot.state = "dormant";
|
|
4077
4245
|
this.recordFailure(slot, "transport", "process-exit-lazy");
|
|
4078
|
-
this.events
|
|
4079
|
-
name,
|
|
4080
|
-
reason: `exit:${code ?? "unknown"} (dormant)`
|
|
4081
|
-
});
|
|
4246
|
+
markLazySlotDormant(slot, this.events, `exit:${code ?? "unknown"}`);
|
|
4082
4247
|
return;
|
|
4083
4248
|
}
|
|
4084
|
-
|
|
4085
|
-
try {
|
|
4086
|
-
this.toolRegistry.unregister(t);
|
|
4087
|
-
} catch {
|
|
4088
|
-
}
|
|
4089
|
-
}
|
|
4090
|
-
slot.toolNames = [];
|
|
4091
|
-
slot.lazyTools = [];
|
|
4092
|
-
slot.serverMetadata = void 0;
|
|
4093
|
-
slot.resources = void 0;
|
|
4094
|
-
slot.resourceTemplates = void 0;
|
|
4095
|
-
slot.prompts = void 0;
|
|
4249
|
+
resetDisconnectedSlotTools(slot, this.toolRegistry);
|
|
4096
4250
|
slot.state = "disconnected";
|
|
4097
4251
|
this.recordFailure(slot, "transport", "process-exit");
|
|
4098
4252
|
this.events.emit("mcp.server.disconnected", { name, reason: `exit:${code ?? "unknown"}` });
|
|
@@ -4103,69 +4257,30 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
4103
4257
|
const slot = this.servers.get(name);
|
|
4104
4258
|
if (!slot) return;
|
|
4105
4259
|
if (slot.lazy) {
|
|
4106
|
-
slot.client = void 0;
|
|
4107
|
-
slot.state = "dormant";
|
|
4108
4260
|
this.recordFailure(slot, "transport", "http-disconnect-lazy");
|
|
4109
|
-
this.events
|
|
4261
|
+
markLazySlotDormant(slot, this.events, "http-disconnect");
|
|
4110
4262
|
return;
|
|
4111
4263
|
}
|
|
4112
|
-
|
|
4113
|
-
try {
|
|
4114
|
-
this.toolRegistry.unregister(t);
|
|
4115
|
-
} catch {
|
|
4116
|
-
}
|
|
4117
|
-
}
|
|
4118
|
-
slot.toolNames = [];
|
|
4119
|
-
slot.lazyTools = [];
|
|
4120
|
-
slot.serverMetadata = void 0;
|
|
4121
|
-
slot.resources = void 0;
|
|
4122
|
-
slot.resourceTemplates = void 0;
|
|
4123
|
-
slot.prompts = void 0;
|
|
4264
|
+
resetDisconnectedSlotTools(slot, this.toolRegistry);
|
|
4124
4265
|
slot.state = "disconnected";
|
|
4125
4266
|
this.recordFailure(slot, "transport", "http-disconnect");
|
|
4126
4267
|
this.events.emit("mcp.server.disconnected", { name, reason: "http-disconnect" });
|
|
4127
4268
|
this.scheduleReconnect(slot);
|
|
4128
4269
|
};
|
|
4129
|
-
/**
|
|
4130
|
-
* L2-B: maximum number of reconnect cycles before staying `failed`.
|
|
4131
|
-
* One cycle = one full `attemptConnect` (which itself may try up to 3
|
|
4132
|
-
* times). Caps total reconnect storm at ~5 cycles, then the slot
|
|
4133
|
-
* needs an explicit `restart()` to re-engage.
|
|
4134
|
-
*/
|
|
4135
4270
|
static MAX_RECONNECT_CYCLES = MCP_CONSTANTS.RECONNECT.MAX_CYCLES;
|
|
4136
|
-
/** Base delay between cycles, in ms. Real delay adds jitter. */
|
|
4137
4271
|
static BASE_RECONNECT_DELAY_MS = MCP_CONSTANTS.RECONNECT.BASE_DELAY_MS;
|
|
4138
|
-
/** Hard ceiling on the inter-cycle delay so the user doesn't wait minutes. */
|
|
4139
4272
|
static MAX_RECONNECT_DELAY_MS = 3e4;
|
|
4140
4273
|
scheduleReconnect(slot) {
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
this.
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
this.
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
});
|
|
4152
|
-
return;
|
|
4153
|
-
}
|
|
4154
|
-
slot.reconnectPending = true;
|
|
4155
|
-
if (slot.reconnectTimer) {
|
|
4156
|
-
clearTimeout(slot.reconnectTimer);
|
|
4157
|
-
slot.reconnectTimer = void 0;
|
|
4158
|
-
}
|
|
4159
|
-
const base = Math.min(
|
|
4160
|
-
_MCPRegistry.BASE_RECONNECT_DELAY_MS * 2 ** slot.reconnectCycles,
|
|
4161
|
-
_MCPRegistry.MAX_RECONNECT_DELAY_MS
|
|
4162
|
-
);
|
|
4163
|
-
const jitter = base * MCP_CONSTANTS.RECONNECT.JITTER_FACTOR * (Math.random() * 2 - 1);
|
|
4164
|
-
const delay = Math.max(100, Math.round(base + jitter));
|
|
4165
|
-
slot.reconnectTimer = setTimeout(() => {
|
|
4166
|
-
slot.reconnectTimer = void 0;
|
|
4167
|
-
void this.attemptReconnect(slot);
|
|
4168
|
-
}, 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
|
+
});
|
|
4169
4284
|
}
|
|
4170
4285
|
async attemptReconnect(slot) {
|
|
4171
4286
|
slot.reconnectPending = false;
|
|
@@ -4175,48 +4290,21 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
4175
4290
|
await this.attemptConnect(slot);
|
|
4176
4291
|
}
|
|
4177
4292
|
recordSuccess(slot, resetFailures = true) {
|
|
4178
|
-
|
|
4179
|
-
operations.lastSuccessAt = Date.now();
|
|
4180
|
-
if (resetFailures) operations.consecutiveFailures = 0;
|
|
4293
|
+
recordRegistrySuccess(slot, resetFailures);
|
|
4181
4294
|
}
|
|
4182
4295
|
recordFailure(slot, failureKind, reason, durationMs) {
|
|
4183
|
-
|
|
4184
|
-
const safeReason = safeOperationReason(reason);
|
|
4185
|
-
operations.lastFailureAt = Date.now();
|
|
4186
|
-
operations.lastFailureKind = failureKind;
|
|
4187
|
-
operations.lastReason = safeReason;
|
|
4188
|
-
operations.consecutiveFailures++;
|
|
4189
|
-
operations.failures[failureKind]++;
|
|
4190
|
-
this.recordOperation(slot, "failure", safeReason, failureKind, durationMs);
|
|
4296
|
+
recordRegistryFailure(slot, this.operationListeners, failureKind, reason, durationMs);
|
|
4191
4297
|
}
|
|
4192
4298
|
recordOperation(slot, kind, reason, failureKind, durationMs, retain = true) {
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
4196
|
-
const event = {
|
|
4197
|
-
serverName: slot.cfg.name,
|
|
4299
|
+
recordRegistryOperation(
|
|
4300
|
+
slot,
|
|
4301
|
+
this.operationListeners,
|
|
4198
4302
|
kind,
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
if (failureKind !== void 0) event.failureKind = failureKind;
|
|
4205
|
-
if (durationMs !== void 0) event.durationMs = Math.max(0, Math.round(durationMs));
|
|
4206
|
-
if (retain) {
|
|
4207
|
-
pushBounded(operations.recentEvents, event, MCP_OPERATION_LIMITS.RECENT_EVENTS);
|
|
4208
|
-
}
|
|
4209
|
-
for (const listener of this.operationListeners) {
|
|
4210
|
-
try {
|
|
4211
|
-
listener({ ...event });
|
|
4212
|
-
} catch {
|
|
4213
|
-
}
|
|
4214
|
-
}
|
|
4215
|
-
}
|
|
4216
|
-
/** Keeps private-method unit fixtures from needing to duplicate every slot field. */
|
|
4217
|
-
operationsFor(slot) {
|
|
4218
|
-
if (!slot.operations) slot.operations = createMCPServerOperationState();
|
|
4219
|
-
return slot.operations;
|
|
4303
|
+
reason,
|
|
4304
|
+
failureKind,
|
|
4305
|
+
durationMs,
|
|
4306
|
+
retain
|
|
4307
|
+
);
|
|
4220
4308
|
}
|
|
4221
4309
|
async attemptConnect(slot) {
|
|
4222
4310
|
const MAX_ATTEMPTS = MCP_CONSTANTS.RECONNECT.MAX_ATTEMPTS;
|
|
@@ -4327,39 +4415,6 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
4327
4415
|
}
|
|
4328
4416
|
}
|
|
4329
4417
|
};
|
|
4330
|
-
var MAX_CATALOG_PAGES = 100;
|
|
4331
|
-
var MAX_CATALOG_ITEMS = 1e4;
|
|
4332
|
-
async function collectPages(load, select) {
|
|
4333
|
-
const items = [];
|
|
4334
|
-
const seenCursors = /* @__PURE__ */ new Set();
|
|
4335
|
-
let cursor;
|
|
4336
|
-
for (let pageNumber = 0; pageNumber < MAX_CATALOG_PAGES; pageNumber++) {
|
|
4337
|
-
const page = await load(cursor);
|
|
4338
|
-
items.push(...select(page));
|
|
4339
|
-
if (items.length > MAX_CATALOG_ITEMS) {
|
|
4340
|
-
throw new Error(`MCP catalog exceeds ${MAX_CATALOG_ITEMS} items`);
|
|
4341
|
-
}
|
|
4342
|
-
const next = page.nextCursor;
|
|
4343
|
-
if (!next) return items;
|
|
4344
|
-
if (seenCursors.has(next)) throw new Error(`MCP catalog repeated cursor "${next}"`);
|
|
4345
|
-
seenCursors.add(next);
|
|
4346
|
-
cursor = next;
|
|
4347
|
-
}
|
|
4348
|
-
throw new Error(`MCP catalog exceeds ${MAX_CATALOG_PAGES} pages`);
|
|
4349
|
-
}
|
|
4350
|
-
function cloneRecords(records) {
|
|
4351
|
-
return structuredClone(records);
|
|
4352
|
-
}
|
|
4353
|
-
function catalogSnapshot(slot) {
|
|
4354
|
-
return {
|
|
4355
|
-
name: slot.cfg.name,
|
|
4356
|
-
state: slot.state,
|
|
4357
|
-
serverMetadata: slot.serverMetadata ? structuredClone(slot.serverMetadata) : void 0,
|
|
4358
|
-
resources: slot.resources ? cloneRecords(slot.resources) : void 0,
|
|
4359
|
-
resourceTemplates: slot.resourceTemplates ? cloneRecords(slot.resourceTemplates) : void 0,
|
|
4360
|
-
prompts: slot.prompts ? cloneRecords(slot.prompts) : void 0
|
|
4361
|
-
};
|
|
4362
|
-
}
|
|
4363
4418
|
|
|
4364
4419
|
// src/server.ts
|
|
4365
4420
|
import { createServer } from "node:http";
|