@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/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 ? spawn([this.opts.command, ...rawArgs].map(quoteWindowsArg).join(" "), {
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
- }) : spawn(this.opts.command, rawArgs, { env: spawnEnv, stdio, windowsHide: true });
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 manager.begin({
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 manager.complete({ serverName: name, resource: cfg.url, callbackUrl, signal });
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.requireAuthorizationManager();
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.requireAuthorizationManager();
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
- const cfg = this.servers.get(name)?.cfg ?? this.disabledServers.get(name);
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
- const active = Array.from(this.servers.values()).map((slot) => {
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 catalogSnapshot(slot);
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 cloneRecords(slot.resources);
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 collectPages(
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 cloneRecords(slot.resources);
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 cloneRecords(slot.resourceTemplates);
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 collectPages(
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 cloneRecords(slot.resourceTemplates);
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 cloneRecords(slot.prompts);
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 collectPages(
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 cloneRecords(slot.prompts);
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 collectPages(
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 collectPages(
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 collectPages(
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.emit("mcp.server.disconnected", {
4079
- name,
4080
- reason: `exit:${code ?? "unknown"} (dormant)`
4081
- });
4246
+ markLazySlotDormant(slot, this.events, `exit:${code ?? "unknown"}`);
4082
4247
  return;
4083
4248
  }
4084
- for (const t of slot.toolNames) {
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.emit("mcp.server.disconnected", { name, reason: "http-disconnect (dormant)" });
4261
+ markLazySlotDormant(slot, this.events, "http-disconnect");
4110
4262
  return;
4111
4263
  }
4112
- for (const t of slot.toolNames) {
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
- if (slot.reconnectPending) return;
4142
- if (slot.reconnectCycles >= _MCPRegistry.MAX_RECONNECT_CYCLES) {
4143
- slot.state = "failed";
4144
- this.recordFailure(slot, "transport", "reconnect-exhausted");
4145
- this.log.error(
4146
- `MCP server "${slot.cfg.name}" giving up after ${slot.reconnectCycles} reconnect cycles. Use \`/mcp restart ${slot.cfg.name}\` to retry.`
4147
- );
4148
- this.events.emit("mcp.server.disconnected", {
4149
- name: slot.cfg.name,
4150
- reason: `reconnect-exhausted:${slot.reconnectCycles}`
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
- const operations = this.operationsFor(slot);
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
- const operations = this.operationsFor(slot);
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
- const operations = this.operationsFor(slot);
4194
- const baseHealth = healthStateFor(slot.state, operations, slot.cfg.enabled !== false);
4195
- const checks = evaluateHealthThresholds(operations, slot.cfg.health?.thresholds);
4196
- const event = {
4197
- serverName: slot.cfg.name,
4299
+ recordRegistryOperation(
4300
+ slot,
4301
+ this.operationListeners,
4198
4302
  kind,
4199
- at: Date.now(),
4200
- connectionState: slot.state,
4201
- healthState: applyHealthThresholds(baseHealth, checks)
4202
- };
4203
- if (reason !== void 0) event.reason = safeOperationReason(reason);
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";