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