@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.d.ts +166 -6
- package/dist/index.js +483 -55
- 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
|
-
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"
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
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
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
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.
|
|
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
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
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
|