@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.d.ts +164 -3
- package/dist/index.js +479 -50
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
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
|
|
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
|
|
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
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
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
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
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
|
-
|
|
1398
|
-
const
|
|
1399
|
-
if (this.
|
|
1400
|
-
slot.
|
|
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.
|
|
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
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
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
|