@wrongstack/mcp 0.265.1 → 0.268.0

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
@@ -1,9 +1,11 @@
1
1
  import { spawn } from 'child_process';
2
- import { expectDefined, buildChildEnv } from '@wrongstack/core';
3
- import { randomBytes } from 'crypto';
2
+ import { expectDefined, ToolCapabilities, buildChildEnv } from '@wrongstack/core';
3
+ import { createHash, randomBytes } from 'crypto';
4
4
  import * as https from 'https';
5
5
  import * as net from 'net';
6
6
  import { toErrorMessage } from '@wrongstack/core/utils';
7
+ import * as fs from 'fs/promises';
8
+ import * as path from 'path';
7
9
  import { createServer } from 'http';
8
10
 
9
11
  // src/client.ts
@@ -37,6 +39,13 @@ var MCP_CONSTANTS = Object.freeze({
37
39
  /** Ms after which the force disconnect is triggered. */
38
40
  FORCE_TIMEOUT_MS: 1200
39
41
  }),
42
+ /** Lazy-connect idle lifecycle. */
43
+ IDLE: Object.freeze({
44
+ /** Default ms a lazy server stays connected with no tool calls before auto-sleep. */
45
+ DEFAULT_TIMEOUT_MS: 3e5,
46
+ /** How often the idle sweep runs (kept well below the timeout). */
47
+ SWEEP_INTERVAL_MS: 3e4
48
+ }),
40
49
  /** JSON-RPC response timeout for outstanding requests. */
41
50
  RESPONSE_TIMEOUT_MS: 500,
42
51
  /** Max buffer size for the SSE reader. */
@@ -72,7 +81,7 @@ function normalizeMCPTools(value) {
72
81
  return tools;
73
82
  }
74
83
  function isTlsUnsafeAllowed() {
75
- return process.env["WRONGSTACK_UNSAFE_MCP_TLS"] === "1" || process.env["CI"] === "true";
84
+ return process.env["WRONGSTACK_UNSAFE_MCP_TLS"] === "1";
76
85
  }
77
86
  function validateTransportUrl(rawUrl) {
78
87
  let url;
@@ -304,7 +313,7 @@ var BaseHTTPTransport = class {
304
313
  if (opts.tls.rejectUnauthorized === false) {
305
314
  if (!isTlsUnsafeAllowed()) {
306
315
  throw new Error(
307
- `[mcp:${transportName}] TLS verification disabled \u2014 set WRONGSTACK_UNSAFE_MCP_TLS=1 or CI=true to allow. Rejecting insecure configuration for ${this.url}.`
316
+ `[mcp:${transportName}] TLS verification disabled \u2014 set WRONGSTACK_UNSAFE_MCP_TLS=1 to allow. Rejecting insecure configuration for ${this.url}.`
308
317
  );
309
318
  }
310
319
  console.error(
@@ -1185,8 +1194,6 @@ function quoteWindowsArg(arg) {
1185
1194
  if (!/[\s"]/.test(arg)) return arg;
1186
1195
  return `"${arg.replace(/"/g, '""')}"`;
1187
1196
  }
1188
-
1189
- // src/wrap-tool.ts
1190
1197
  var MUTATING_RE = /create|update|delete|write|send|set|put|post|patch|remove|rename|move/i;
1191
1198
  function isMutatingTool(mcpTool) {
1192
1199
  if (MUTATING_RE.test(mcpTool.name)) return true;
@@ -1209,9 +1216,11 @@ function wrapMCPTool(serverName, mcpTool, client, permission = "confirm") {
1209
1216
  usageHint: `Tool provided by MCP server "${serverName}". ${mcpTool.description ?? ""}`,
1210
1217
  permission,
1211
1218
  mutating: isMutatingTool(mcpTool),
1219
+ capabilities: [ToolCapabilities.MCP_PROXY],
1212
1220
  inputSchema: mcpTool.inputSchema ?? { type: "object", properties: {} },
1213
1221
  async execute(input, _ctx, _opts) {
1214
- const res = await client.callTool(mcpTool.name, input);
1222
+ const live = typeof client === "function" ? await client() : client;
1223
+ const res = await live.callTool(mcpTool.name, input);
1215
1224
  if (res.isError) {
1216
1225
  throw new Error(stringify(res.content));
1217
1226
  }
@@ -1239,20 +1248,63 @@ function stringify(c) {
1239
1248
  }
1240
1249
  return String(c ?? "");
1241
1250
  }
1251
+ function manifestConfigHash(cfg) {
1252
+ const basis = JSON.stringify({
1253
+ transport: cfg.transport,
1254
+ command: cfg.command ?? null,
1255
+ args: cfg.args ?? null,
1256
+ url: cfg.url ?? null
1257
+ });
1258
+ return createHash("sha256").update(basis).digest("hex").slice(0, 16);
1259
+ }
1260
+ function manifestFile(cacheDir, name) {
1261
+ const safe = name.replace(/[^a-zA-Z0-9._-]/g, "_");
1262
+ return path.join(cacheDir, "mcp-tools", `${safe}.json`);
1263
+ }
1264
+ async function readManifest(cacheDir, name, configHash) {
1265
+ try {
1266
+ const raw = await fs.readFile(manifestFile(cacheDir, name), "utf8");
1267
+ const parsed = JSON.parse(raw);
1268
+ if (parsed.configHash !== configHash || !Array.isArray(parsed.tools)) return null;
1269
+ return parsed.tools;
1270
+ } catch {
1271
+ return null;
1272
+ }
1273
+ }
1274
+ async function writeManifest(cacheDir, name, configHash, tools) {
1275
+ try {
1276
+ const file = manifestFile(cacheDir, name);
1277
+ await fs.mkdir(path.dirname(file), { recursive: true });
1278
+ const body = { configHash, tools };
1279
+ const tmp = `${file}.tmp`;
1280
+ await fs.writeFile(tmp, JSON.stringify(body, null, 2), "utf8");
1281
+ await fs.rename(tmp, file);
1282
+ } catch {
1283
+ }
1284
+ }
1285
+
1286
+ // src/registry.ts
1242
1287
  var MCPRegistry = class _MCPRegistry {
1243
1288
  servers = /* @__PURE__ */ new Map();
1244
1289
  toolRegistry;
1245
1290
  events;
1246
1291
  log;
1247
1292
  lazyMode;
1293
+ cacheDir;
1294
+ idleTimeoutMs;
1295
+ /** Single shared idle sweep timer (started lazily; unref'd; cleared on stopAll). */
1296
+ idleTimer;
1248
1297
  constructor(opts) {
1249
1298
  this.toolRegistry = opts.toolRegistry;
1250
1299
  this.events = opts.events;
1251
1300
  this.log = opts.log;
1252
1301
  this.lazyMode = opts.lazyMode ?? false;
1302
+ this.cacheDir = opts.cacheDir;
1303
+ this.idleTimeoutMs = opts.idleTimeoutMs ?? MCP_CONSTANTS.IDLE.DEFAULT_TIMEOUT_MS;
1253
1304
  }
1254
1305
  async start(cfg) {
1255
1306
  if (cfg.enabled === false) return;
1307
+ const lazy = !!cfg.lazy && !!this.cacheDir;
1256
1308
  const slot = {
1257
1309
  cfg,
1258
1310
  state: "idle",
@@ -1260,11 +1312,70 @@ var MCPRegistry = class _MCPRegistry {
1260
1312
  lazyTools: [],
1261
1313
  attempts: 0,
1262
1314
  reconnectPending: false,
1263
- reconnectCycles: 0
1315
+ reconnectCycles: 0,
1316
+ lazy,
1317
+ lastUsed: Date.now(),
1318
+ registeredLazy: false
1264
1319
  };
1265
1320
  this.servers.set(cfg.name, slot);
1321
+ if (lazy) {
1322
+ await this.startLazy(slot);
1323
+ } else {
1324
+ await this.attemptConnect(slot);
1325
+ }
1326
+ }
1327
+ /**
1328
+ * Boot a lazy server WITHOUT spawning it. If a tool manifest is cached (from a
1329
+ * prior connect with matching config), register resolver-backed wrappers and
1330
+ * go `dormant` — the process spawns on the first tool call. If there is no
1331
+ * cache yet, do a one-time cold discovery connect to learn + cache the tools.
1332
+ */
1333
+ async startLazy(slot) {
1334
+ const cacheDir = this.cacheDir;
1335
+ if (!cacheDir) {
1336
+ await this.attemptConnect(slot);
1337
+ return;
1338
+ }
1339
+ const hash = manifestConfigHash(slot.cfg);
1340
+ const cached = await readManifest(cacheDir, slot.cfg.name, hash);
1341
+ if (cached && cached.length > 0) {
1342
+ this.applyTools(slot, cached);
1343
+ slot.state = "dormant";
1344
+ this.ensureIdleSweep();
1345
+ this.log.info(
1346
+ `MCP server "${slot.cfg.name}" registered lazily from cache (${cached.length} tools, dormant)`
1347
+ );
1348
+ return;
1349
+ }
1266
1350
  await this.attemptConnect(slot);
1267
1351
  }
1352
+ /**
1353
+ * Ensure a lazy server is connected, spawning it on demand. Single-flight:
1354
+ * concurrent first-calls share one connect. Resolver wrappers call this.
1355
+ */
1356
+ async ensureConnected(name) {
1357
+ const slot = this.servers.get(name);
1358
+ if (!slot) throw new Error(`MCP server "${name}" not registered`);
1359
+ slot.lastUsed = Date.now();
1360
+ if (slot.client && slot.state === "connected") return slot.client;
1361
+ if (slot.connecting) return slot.connecting;
1362
+ slot.connecting = (async () => {
1363
+ try {
1364
+ slot.attempts = 0;
1365
+ slot.reconnectCycles = 0;
1366
+ await this.attemptConnect(slot);
1367
+ if (!slot.client) {
1368
+ throw new Error(`MCP server "${name}" failed to connect on demand`);
1369
+ }
1370
+ slot.lastUsed = Date.now();
1371
+ this.ensureIdleSweep();
1372
+ return slot.client;
1373
+ } finally {
1374
+ slot.connecting = void 0;
1375
+ }
1376
+ })();
1377
+ return slot.connecting;
1378
+ }
1268
1379
  /**
1269
1380
  * Register all cached tools for a given server into the tool registry.
1270
1381
  * No-op if tools are already registered or the server is not connected.
@@ -1272,7 +1383,8 @@ var MCPRegistry = class _MCPRegistry {
1272
1383
  */
1273
1384
  activateServer(name) {
1274
1385
  const slot = this.servers.get(name);
1275
- if (!slot || !slot.client) return;
1386
+ if (!slot) return;
1387
+ if (!slot.client && !slot.lazy) return;
1276
1388
  if (slot.toolNames.length > 0) return;
1277
1389
  const cached = slot.lazyTools;
1278
1390
  if (cached.length === 0) return;
@@ -1327,9 +1439,11 @@ var MCPRegistry = class _MCPRegistry {
1327
1439
  slot.client = void 0;
1328
1440
  }
1329
1441
  slot.onDisconnect = void 0;
1442
+ slot.connecting = void 0;
1330
1443
  for (const t of slot.toolNames) this.toolRegistry.unregister(t);
1331
1444
  slot.toolNames = [];
1332
1445
  slot.lazyTools = [];
1446
+ slot.registeredLazy = false;
1333
1447
  slot.state = "disconnected";
1334
1448
  this.events.emit("mcp.server.disconnected", { name, reason: "stop" });
1335
1449
  }
@@ -1342,11 +1456,89 @@ var MCPRegistry = class _MCPRegistry {
1342
1456
  await this.attemptConnect(slot);
1343
1457
  }
1344
1458
  list() {
1345
- return Array.from(this.servers.values()).map((s) => ({
1346
- name: s.cfg.name,
1347
- state: s.state,
1348
- toolCount: Math.max(s.toolNames.length, s.lazyTools.length)
1349
- }));
1459
+ return Array.from(this.servers.values()).map((s) => {
1460
+ const tools = this.toolNamesForSlot(s);
1461
+ return {
1462
+ name: s.cfg.name,
1463
+ state: s.state,
1464
+ toolCount: tools.length,
1465
+ tools
1466
+ };
1467
+ });
1468
+ }
1469
+ /**
1470
+ * Resolve the live tool names for a slot — the registered names in normal
1471
+ * mode, or the cached lazy-tool names when running in lazy mode (where
1472
+ * tools are connected but intentionally not registered).
1473
+ */
1474
+ toolNamesForSlot(s) {
1475
+ return s.toolNames.length > 0 ? s.toolNames.slice() : (s.lazyTools ?? []).map((t) => t.name);
1476
+ }
1477
+ /**
1478
+ * Wrap + register (or cache) a server's tools. Lazy servers get resolver-backed
1479
+ * wrappers that spawn the process on first use; eager servers bind the live
1480
+ * client directly. Honours token-saving `lazyMode` (cache, don't register) and
1481
+ * a register-once guard for lazy resolver wrappers (so a wake/reconnect reuses
1482
+ * the existing registrations rather than churning the tool list).
1483
+ */
1484
+ applyTools(slot, tools, client) {
1485
+ if (slot.lazy && slot.registeredLazy && !this.lazyMode) return;
1486
+ const allowed = slot.cfg.allowedTools;
1487
+ const filtered = tools.filter((t) => !allowed || allowed.includes(t.name));
1488
+ const clientArg = slot.lazy ? () => this.ensureConnected(slot.cfg.name) : expectDefined(client);
1489
+ const wrapped = filtered.map(
1490
+ (t) => wrapMCPTool(slot.cfg.name, t, clientArg, slot.cfg.permission ?? "confirm")
1491
+ );
1492
+ if (this.lazyMode) {
1493
+ slot.lazyTools = wrapped;
1494
+ return;
1495
+ }
1496
+ for (const tool of wrapped) {
1497
+ try {
1498
+ this.toolRegistry.register(tool, `mcp:${slot.cfg.name}`);
1499
+ slot.toolNames.push(tool.name);
1500
+ } catch (err) {
1501
+ this.log.warn(`MCP tool "${tool.name}" not registered`, err);
1502
+ }
1503
+ }
1504
+ if (slot.lazy) slot.registeredLazy = true;
1505
+ }
1506
+ /** Start the shared idle sweep timer once (unref'd so it never holds the process). */
1507
+ ensureIdleSweep() {
1508
+ if (this.idleTimer || this.idleTimeoutMs <= 0) return;
1509
+ this.idleTimer = setInterval(() => {
1510
+ void this.sweepIdle();
1511
+ }, MCP_CONSTANTS.IDLE.SWEEP_INTERVAL_MS);
1512
+ this.idleTimer.unref?.();
1513
+ }
1514
+ /** Auto-sleep connected lazy servers that have been idle past the timeout. */
1515
+ async sweepIdle() {
1516
+ if (this.idleTimeoutMs <= 0) return;
1517
+ const now = Date.now();
1518
+ for (const slot of this.servers.values()) {
1519
+ if (slot.lazy && slot.state === "connected" && slot.client && now - slot.lastUsed > this.idleTimeoutMs) {
1520
+ await this.sleepIdle(slot);
1521
+ }
1522
+ }
1523
+ }
1524
+ /**
1525
+ * Soft stop: close the server process but KEEP its resolver wrappers and
1526
+ * cached manifest registered, so the next tool call transparently re-wakes it.
1527
+ * Distinct from {@link stop} (full teardown for disable/remove).
1528
+ */
1529
+ async sleepIdle(slot) {
1530
+ slot.reconnectPending = false;
1531
+ if (slot.client) {
1532
+ slot.client.removeExitListener(this.onChildExit);
1533
+ if (slot.onDisconnect) slot.client.removeDisconnectListener(slot.onDisconnect);
1534
+ slot.client.removeToolsChangedListener(this.onToolsChanged);
1535
+ await slot.client.close();
1536
+ slot.client = void 0;
1537
+ }
1538
+ slot.onDisconnect = void 0;
1539
+ slot.state = "dormant";
1540
+ this.log.info(`MCP server "${slot.cfg.name}" idle \u2014 sleeping (tools stay registered)`);
1541
+ this.events.emit("mcp.server.disconnected", { name: slot.cfg.name, reason: "idle-sleep" });
1350
1542
  }
1351
1543
  /**
1352
1544
  * Catalog of every server ever registered with this registry — includes
@@ -1355,14 +1547,22 @@ var MCPRegistry = class _MCPRegistry {
1355
1547
  * triggering connections.
1356
1548
  */
1357
1549
  describe() {
1358
- return Array.from(this.servers.values()).map((s) => ({
1359
- name: s.cfg.name,
1360
- state: s.state,
1361
- toolCount: Math.max(s.toolNames.length, s.lazyTools.length),
1362
- enabled: s.cfg.enabled !== false
1363
- }));
1550
+ return Array.from(this.servers.values()).map((s) => {
1551
+ const tools = this.toolNamesForSlot(s);
1552
+ return {
1553
+ name: s.cfg.name,
1554
+ state: s.state,
1555
+ toolCount: tools.length,
1556
+ enabled: s.cfg.enabled !== false,
1557
+ tools
1558
+ };
1559
+ });
1364
1560
  }
1365
1561
  async stopAll() {
1562
+ if (this.idleTimer) {
1563
+ clearInterval(this.idleTimer);
1564
+ this.idleTimer = void 0;
1565
+ }
1366
1566
  for (const name of Array.from(this.servers.keys())) {
1367
1567
  await this.stop(name);
1368
1568
  }
@@ -1394,34 +1594,32 @@ var MCPRegistry = class _MCPRegistry {
1394
1594
  }
1395
1595
  }
1396
1596
  slot.toolNames = [];
1397
- const allowed = slot.cfg.allowedTools;
1398
- const wrapped = slot.client.listTools().filter((t) => !allowed || allowed.includes(t.name)).map((t) => wrapMCPTool(slot.cfg.name, t, expectDefined(slot.client), slot.cfg.permission ?? "confirm"));
1399
- if (this.lazyMode) {
1400
- slot.lazyTools = wrapped;
1401
- this.log.info(
1402
- `MCP server "${slot.cfg.name}" tools refreshed in lazy mode (${wrapped.length} cached)`
1403
- );
1404
- } else {
1405
- for (const tool of wrapped) {
1406
- try {
1407
- this.toolRegistry.register(tool, `mcp:${slot.cfg.name}`);
1408
- slot.toolNames.push(tool.name);
1409
- } catch (err) {
1410
- this.log.warn(`MCP tool "${tool.name}" not re-registered after list_changed`, err);
1411
- }
1412
- }
1597
+ slot.registeredLazy = false;
1598
+ const discovered = slot.client.listTools();
1599
+ if (slot.lazy && this.cacheDir) {
1600
+ void writeManifest(this.cacheDir, slot.cfg.name, manifestConfigHash(slot.cfg), discovered);
1413
1601
  }
1602
+ this.applyTools(slot, discovered, slot.client);
1414
1603
  this.events.emit("mcp.server.connected", {
1415
1604
  name: slot.cfg.name,
1416
1605
  toolCount: slot.toolNames.length
1417
1606
  });
1418
1607
  this.log.info(
1419
- `MCP server "${slot.cfg.name}" tools refreshed (${slot.toolNames.length} active, ${wrapped.length} total)`
1608
+ `MCP server "${slot.cfg.name}" tools refreshed (${this.toolNamesForSlot(slot).length} active)`
1420
1609
  );
1421
1610
  };
1422
1611
  onChildExit = (name, code, _signal) => {
1423
1612
  const slot = this.servers.get(name);
1424
1613
  if (!slot) return;
1614
+ if (slot.lazy) {
1615
+ slot.client = void 0;
1616
+ slot.state = "dormant";
1617
+ this.events.emit("mcp.server.disconnected", {
1618
+ name,
1619
+ reason: `exit:${code ?? "unknown"} (dormant)`
1620
+ });
1621
+ return;
1622
+ }
1425
1623
  for (const t of slot.toolNames) {
1426
1624
  try {
1427
1625
  this.toolRegistry.unregister(t);
@@ -1438,6 +1636,12 @@ var MCPRegistry = class _MCPRegistry {
1438
1636
  onTransportDisconnect = (name) => {
1439
1637
  const slot = this.servers.get(name);
1440
1638
  if (!slot) return;
1639
+ if (slot.lazy) {
1640
+ slot.client = void 0;
1641
+ slot.state = "dormant";
1642
+ this.events.emit("mcp.server.disconnected", { name, reason: "http-disconnect (dormant)" });
1643
+ return;
1644
+ }
1441
1645
  for (const t of slot.toolNames) {
1442
1646
  try {
1443
1647
  this.toolRegistry.unregister(t);
@@ -1531,26 +1735,19 @@ var MCPRegistry = class _MCPRegistry {
1531
1735
  const isReconnect = attempt > 1;
1532
1736
  slot.state = "connected";
1533
1737
  slot.reconnectCycles = 0;
1534
- const allowed = slot.cfg.allowedTools;
1535
1738
  const mc = client;
1536
- const candidateTools = mc.listTools();
1537
- const toWrap = candidateTools.length > 0 ? candidateTools : mc.listTools();
1538
- const wrapped = toWrap.filter((t) => !allowed || allowed.includes(t.name)).map((t) => wrapMCPTool(slot.cfg.name, t, mc, slot.cfg.permission ?? "confirm"));
1539
- if (this.lazyMode) {
1540
- slot.lazyTools = wrapped;
1541
- this.log.info(
1542
- `MCP server "${slot.cfg.name}" connected in lazy mode (${wrapped.length} tools cached, not registered)`
1739
+ const discovered = mc.listTools();
1740
+ if (slot.lazy && this.cacheDir) {
1741
+ await writeManifest(
1742
+ this.cacheDir,
1743
+ slot.cfg.name,
1744
+ manifestConfigHash(slot.cfg),
1745
+ discovered
1543
1746
  );
1544
- } else {
1545
- for (const tool of wrapped) {
1546
- try {
1547
- this.toolRegistry.register(tool, `mcp:${slot.cfg.name}`);
1548
- slot.toolNames.push(tool.name);
1549
- } catch (err) {
1550
- this.log.warn(`MCP tool "${tool.name}" not registered`, err);
1551
- }
1552
- }
1553
1747
  }
1748
+ this.applyTools(slot, discovered, mc);
1749
+ slot.lastUsed = Date.now();
1750
+ if (slot.lazy) this.ensureIdleSweep();
1554
1751
  this.events.emit(isReconnect ? "mcp.server.reconnected" : "mcp.server.connected", {
1555
1752
  name: slot.cfg.name,
1556
1753
  toolCount: slot.toolNames.length
@@ -1584,6 +1781,237 @@ var MCPRegistry = class _MCPRegistry {
1584
1781
  }
1585
1782
  }
1586
1783
  };
1784
+ async function readConfig(path2) {
1785
+ try {
1786
+ return JSON.parse(await fs.readFile(path2, "utf8"));
1787
+ } catch {
1788
+ return {};
1789
+ }
1790
+ }
1791
+ async function writeConfig(path2, cfg) {
1792
+ const raw = JSON.stringify(cfg, null, 2);
1793
+ const tmp = `${path2}.tmp`;
1794
+ await fs.writeFile(tmp, raw, "utf8");
1795
+ await fs.rename(tmp, path2);
1796
+ }
1797
+ function isMcpServerRecord(value) {
1798
+ return !!value && typeof value === "object" && !Array.isArray(value);
1799
+ }
1800
+ async function readServers(configPath) {
1801
+ const full = await readConfig(configPath);
1802
+ const servers = isMcpServerRecord(full.mcpServers) ? { ...full.mcpServers } : {};
1803
+ return { full, servers };
1804
+ }
1805
+ async function persist(configPath, full, servers) {
1806
+ full.mcpServers = servers;
1807
+ await writeConfig(configPath, full);
1808
+ }
1809
+ function normalizeTransport(t) {
1810
+ if (t === "sse") return "sse";
1811
+ if (t === "http" || t === "streamable-http") return "streamable-http";
1812
+ return "stdio";
1813
+ }
1814
+ function buildConfig(input, base) {
1815
+ const cfg = {
1816
+ name: input.name,
1817
+ transport: input.transport ? normalizeTransport(String(input.transport)) : base?.transport ?? "stdio"
1818
+ };
1819
+ const description = input.description ?? base?.description;
1820
+ if (description !== void 0) cfg.description = description;
1821
+ const command = input.command ?? base?.command;
1822
+ if (command !== void 0) cfg.command = command;
1823
+ const args = input.args ?? base?.args;
1824
+ if (args !== void 0) cfg.args = args;
1825
+ const env = input.env ?? base?.env;
1826
+ if (env !== void 0) cfg.env = env;
1827
+ const url = input.url ?? base?.url;
1828
+ if (url !== void 0) cfg.url = url;
1829
+ const headers = input.headers ?? base?.headers;
1830
+ if (headers !== void 0) cfg.headers = headers;
1831
+ const allowedTools = input.allowedTools ?? base?.allowedTools;
1832
+ if (allowedTools !== void 0) cfg.allowedTools = allowedTools;
1833
+ const permission = input.permission ?? base?.permission;
1834
+ if (permission !== void 0) cfg.permission = permission;
1835
+ const enabled = input.enabled ?? base?.enabled;
1836
+ if (enabled !== void 0) cfg.enabled = enabled;
1837
+ const lazy = input.lazy ?? base?.lazy;
1838
+ if (lazy !== void 0) cfg.lazy = lazy;
1839
+ return cfg;
1840
+ }
1841
+ function projectServer(name, cfg, registry) {
1842
+ const live = registry.list().find((s) => s.name === name);
1843
+ const info = {
1844
+ name,
1845
+ transport: cfg.transport,
1846
+ enabled: cfg.enabled !== false,
1847
+ status: live ? live.state : "stopped",
1848
+ tools: live?.tools ?? []
1849
+ };
1850
+ if (cfg.description !== void 0) info.description = cfg.description;
1851
+ if (cfg.url !== void 0) info.url = cfg.url;
1852
+ if (cfg.command !== void 0) info.command = cfg.command;
1853
+ if (cfg.lazy !== void 0) info.lazy = cfg.lazy;
1854
+ return info;
1855
+ }
1856
+ function liveState(name, registry) {
1857
+ const live = registry.list().find((s) => s.name === name);
1858
+ return { state: live?.state ?? "stopped", tools: live?.tools ?? [] };
1859
+ }
1860
+ function errMessage(err) {
1861
+ return err instanceof Error ? err.message : String(err);
1862
+ }
1863
+ async function listMcp(deps) {
1864
+ const { servers } = await readServers(deps.configPath);
1865
+ return Object.entries(servers).map(
1866
+ ([name, cfg]) => projectServer(name, { ...cfg}, deps.registry)
1867
+ );
1868
+ }
1869
+ async function addMcp(input, deps) {
1870
+ if (!input.name) return { ok: false, message: "Server name is required" };
1871
+ const { full, servers } = await readServers(deps.configPath);
1872
+ if (servers[input.name]) {
1873
+ return { ok: false, message: `Server "${input.name}" already exists` };
1874
+ }
1875
+ const preset = deps.presets?.[input.name];
1876
+ const hasExplicitConfig = !!(input.transport || input.command || input.url);
1877
+ const cfg = hasExplicitConfig ? buildConfig(input, preset) : preset ? buildConfig({ ...input, name: input.name }, preset) : buildConfig(input);
1878
+ if (!hasExplicitConfig && !preset) {
1879
+ const known = Object.keys(deps.presets ?? {}).join(", ");
1880
+ return {
1881
+ ok: false,
1882
+ message: known ? `Unknown server "${input.name}". Available presets: ${known}` : `No configuration provided for "${input.name}"`
1883
+ };
1884
+ }
1885
+ cfg.enabled = input.enabled ?? false;
1886
+ servers[input.name] = cfg;
1887
+ await persist(deps.configPath, full, servers);
1888
+ if (cfg.enabled) {
1889
+ return startServer(input.name, cfg, deps, `Server "${input.name}" added`);
1890
+ }
1891
+ return {
1892
+ ok: true,
1893
+ message: `Server "${input.name}" added (disabled)`,
1894
+ server: projectServer(input.name, cfg, deps.registry)
1895
+ };
1896
+ }
1897
+ async function updateMcp(input, deps) {
1898
+ if (!input.name) return { ok: false, message: "Server name is required" };
1899
+ const { full, servers } = await readServers(deps.configPath);
1900
+ const existing = servers[input.name];
1901
+ if (!existing) return { ok: false, message: `Server "${input.name}" not found` };
1902
+ const cfg = buildConfig(input, { ...existing, name: input.name });
1903
+ servers[input.name] = cfg;
1904
+ await persist(deps.configPath, full, servers);
1905
+ if (cfg.enabled !== false) {
1906
+ return startServer(input.name, cfg, deps, `Server "${input.name}" updated`, { restart: true });
1907
+ }
1908
+ await safeStop(input.name, deps);
1909
+ return {
1910
+ ok: true,
1911
+ message: `Server "${input.name}" updated`,
1912
+ server: projectServer(input.name, cfg, deps.registry)
1913
+ };
1914
+ }
1915
+ async function removeMcp(name, deps) {
1916
+ if (!name) return { ok: false, message: "Server name is required" };
1917
+ const { full, servers } = await readServers(deps.configPath);
1918
+ if (!servers[name]) return { ok: false, message: `Server "${name}" not found` };
1919
+ await safeStop(name, deps);
1920
+ delete servers[name];
1921
+ await persist(deps.configPath, full, servers);
1922
+ return { ok: true, message: `Server "${name}" removed` };
1923
+ }
1924
+ async function enableMcp(name, deps) {
1925
+ if (!name) return { ok: false, message: "Server name is required" };
1926
+ const { full, servers } = await readServers(deps.configPath);
1927
+ const cfg = servers[name];
1928
+ if (!cfg) {
1929
+ return { ok: false, message: `Server "${name}" is not in config. Add it first.` };
1930
+ }
1931
+ cfg.enabled = true;
1932
+ servers[name] = cfg;
1933
+ await persist(deps.configPath, full, servers);
1934
+ return startServer(name, cfg, deps, `Server "${name}" enabled`, { restart: true });
1935
+ }
1936
+ async function disableMcp(name, deps) {
1937
+ if (!name) return { ok: false, message: "Server name is required" };
1938
+ const { full, servers } = await readServers(deps.configPath);
1939
+ const cfg = servers[name];
1940
+ if (!cfg) return { ok: false, message: `Server "${name}" is not in config.` };
1941
+ await safeStop(name, deps);
1942
+ cfg.enabled = false;
1943
+ servers[name] = cfg;
1944
+ await persist(deps.configPath, full, servers);
1945
+ return {
1946
+ ok: true,
1947
+ message: `Server "${name}" disabled`,
1948
+ server: projectServer(name, cfg, deps.registry)
1949
+ };
1950
+ }
1951
+ async function restartMcp(name, deps) {
1952
+ if (!name) return { ok: false, message: "Server name is required" };
1953
+ const registered = deps.registry.list().some((s) => s.name === name);
1954
+ if (registered) {
1955
+ try {
1956
+ await deps.registry.restart(name);
1957
+ const { state, tools } = liveState(name, deps.registry);
1958
+ return { ok: true, message: `Server "${name}" restarted`, state, tools };
1959
+ } catch (err) {
1960
+ return { ok: false, message: `Failed to restart "${name}": ${errMessage(err)}` };
1961
+ }
1962
+ }
1963
+ const { servers } = await readServers(deps.configPath);
1964
+ const cfg = servers[name];
1965
+ if (!cfg) return { ok: false, message: `Server "${name}" is not in config.` };
1966
+ return startServer(name, { ...cfg, name }, deps, `Server "${name}" started`, { restart: true });
1967
+ }
1968
+ async function discoverMcp(name, deps) {
1969
+ if (!name) return { ok: false, message: "Server name is required" };
1970
+ const result = await restartMcp(name, deps);
1971
+ if (!result.ok) return result;
1972
+ const { state, tools } = liveState(name, deps.registry);
1973
+ return {
1974
+ ok: true,
1975
+ message: `Discovered ${tools.length} tool${tools.length === 1 ? "" : "s"} from "${name}"`,
1976
+ state,
1977
+ tools
1978
+ };
1979
+ }
1980
+ async function startServer(name, cfg, deps, okMessage, opts) {
1981
+ try {
1982
+ const alreadyRegistered = deps.registry.list().some((s) => s.name === name);
1983
+ if (alreadyRegistered && opts?.restart) {
1984
+ await deps.registry.restart(name);
1985
+ } else if (alreadyRegistered) {
1986
+ await deps.registry.restart(name);
1987
+ } else {
1988
+ await deps.registry.start({ ...cfg, enabled: true });
1989
+ }
1990
+ const { state, tools } = liveState(name, deps.registry);
1991
+ return {
1992
+ ok: true,
1993
+ message: okMessage,
1994
+ server: projectServer(name, cfg, deps.registry),
1995
+ state,
1996
+ tools
1997
+ };
1998
+ } catch (err) {
1999
+ const message = errMessage(err);
2000
+ return {
2001
+ ok: true,
2002
+ // config persisted — surface a soft warning, not a hard failure
2003
+ message: `${okMessage} in config, but failed to start: ${message}`,
2004
+ server: projectServer(name, cfg, deps.registry),
2005
+ registryError: message
2006
+ };
2007
+ }
2008
+ }
2009
+ async function safeStop(name, deps) {
2010
+ try {
2011
+ await deps.registry.stop(name);
2012
+ } catch {
2013
+ }
2014
+ }
1587
2015
  var PARSE_ERROR = -32700;
1588
2016
  var INVALID_REQUEST = -32600;
1589
2017
  var METHOD_NOT_FOUND = -32601;
@@ -1829,6 +2257,6 @@ async function handleHttpRequest(server, req, res, token, log) {
1829
2257
  });
1830
2258
  }
1831
2259
 
1832
- export { MCPClient, MCPRegistry, MCPServer, MCP_CONSTANTS, SSEReader, SSETransport, StreamableHTTPTransport, serveHttp, serveStdio, toContentBlocks, wrapMCPTool };
2260
+ export { MCPClient, MCPRegistry, MCPServer, MCP_CONSTANTS, SSEReader, SSETransport, StreamableHTTPTransport, addMcp, disableMcp, discoverMcp, enableMcp, listMcp, manifestConfigHash, readManifest, removeMcp, restartMcp, serveHttp, serveStdio, toContentBlocks, updateMcp, wrapMCPTool, writeManifest };
1833
2261
  //# sourceMappingURL=index.js.map
1834
2262
  //# sourceMappingURL=index.js.map