@alfe.ai/mcp-bundler 0.0.1
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.cjs +460 -0
- package/dist/index.d.cts +241 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.ts +241 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +454 -0
- package/dist/index.js.map +1 -0
- package/package.json +29 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
//#region src/tool-naming.ts
|
|
2
|
+
/**
|
|
3
|
+
* Tool name sanitization and collision handling.
|
|
4
|
+
*
|
|
5
|
+
* OpenClaw constraint: tool names must match `[A-Za-z0-9_-]` and be ≤64 chars.
|
|
6
|
+
* Pattern mirrored from `openclaw/src/agents/pi-bundle-mcp-names.ts`.
|
|
7
|
+
*
|
|
8
|
+
* Strategy: prefix every tool with its server name (`{server}__{tool}`),
|
|
9
|
+
* sanitize disallowed chars to `_`, truncate, then suffix-disambiguate
|
|
10
|
+
* (`-2`, `-3`, ...) on collision.
|
|
11
|
+
*/
|
|
12
|
+
const DISALLOWED = /[^A-Za-z0-9_-]/g;
|
|
13
|
+
const MAX_LEN = 64;
|
|
14
|
+
const SEPARATOR = "__";
|
|
15
|
+
function sanitizeNameSegment(value) {
|
|
16
|
+
return value.replace(DISALLOWED, "_");
|
|
17
|
+
}
|
|
18
|
+
function buildNamespacedToolName(server, tool) {
|
|
19
|
+
const base = `${sanitizeNameSegment(server)}${SEPARATOR}${sanitizeNameSegment(tool)}`;
|
|
20
|
+
if (base.length <= MAX_LEN) return base;
|
|
21
|
+
const reservedForServer = sanitizeNameSegment(server).length + 2;
|
|
22
|
+
const toolBudget = Math.max(1, MAX_LEN - reservedForServer);
|
|
23
|
+
return `${sanitizeNameSegment(server)}${SEPARATOR}${sanitizeNameSegment(tool).slice(0, toolBudget)}`;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Disambiguate a candidate name against an existing set by appending `-2`, `-3`, etc.
|
|
27
|
+
* Mutates nothing; returns the chosen name. Caller is responsible for inserting it
|
|
28
|
+
* into the set.
|
|
29
|
+
*/
|
|
30
|
+
function disambiguateAgainst(candidate, taken) {
|
|
31
|
+
if (!taken.has(candidate)) return candidate;
|
|
32
|
+
for (let i = 2; i < 1e3; i += 1) {
|
|
33
|
+
const suffix = `-${i.toString()}`;
|
|
34
|
+
const room = MAX_LEN - suffix.length;
|
|
35
|
+
const next = `${candidate.length > room ? candidate.slice(0, room) : candidate}${suffix}`;
|
|
36
|
+
if (!taken.has(next)) return next;
|
|
37
|
+
}
|
|
38
|
+
return `${candidate.slice(0, MAX_LEN - 6)}-x${(taken.size % 1e3).toString().padStart(3, "0")}`;
|
|
39
|
+
}
|
|
40
|
+
//#endregion
|
|
41
|
+
//#region src/connection.ts
|
|
42
|
+
/** Env keys OpenClaw rejects from stdio MCP env blocks. Filter them out before spawning. */
|
|
43
|
+
const STDIO_ENV_DENYLIST = new Set([
|
|
44
|
+
"NODE_OPTIONS",
|
|
45
|
+
"PYTHONSTARTUP",
|
|
46
|
+
"PYTHONPATH",
|
|
47
|
+
"PERL5OPT",
|
|
48
|
+
"RUBYOPT",
|
|
49
|
+
"SHELLOPTS",
|
|
50
|
+
"PS4"
|
|
51
|
+
]);
|
|
52
|
+
function sanitizeStdioEnv(env) {
|
|
53
|
+
if (!env) return {};
|
|
54
|
+
const safe = {};
|
|
55
|
+
for (const [k, v] of Object.entries(env)) {
|
|
56
|
+
if (STDIO_ENV_DENYLIST.has(k)) continue;
|
|
57
|
+
safe[k] = v;
|
|
58
|
+
}
|
|
59
|
+
return safe;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* One Connection per MCP server. Owns lifecycle (lazy-spawn, close, refresh-lock).
|
|
63
|
+
* Refresh-lock pattern adapted from AIWerk `index.ts:219-250` — prevents
|
|
64
|
+
* reconnect + `notifications/tools/list_changed` race.
|
|
65
|
+
*/
|
|
66
|
+
var Connection = class {
|
|
67
|
+
name;
|
|
68
|
+
config;
|
|
69
|
+
deps;
|
|
70
|
+
logger;
|
|
71
|
+
client;
|
|
72
|
+
tools = [];
|
|
73
|
+
connectInFlight;
|
|
74
|
+
refreshInFlight = false;
|
|
75
|
+
refreshQueued = false;
|
|
76
|
+
lastUsedAt = Date.now();
|
|
77
|
+
constructor(params) {
|
|
78
|
+
this.name = params.name;
|
|
79
|
+
this.config = params.config;
|
|
80
|
+
this.deps = params.deps;
|
|
81
|
+
this.logger = params.logger;
|
|
82
|
+
}
|
|
83
|
+
/** Returns the most recent known tool list. May be empty if the server hasn't connected yet. */
|
|
84
|
+
snapshotTools() {
|
|
85
|
+
return this.tools;
|
|
86
|
+
}
|
|
87
|
+
/** Whether an MCP child process / remote connection has been established. */
|
|
88
|
+
isConnected() {
|
|
89
|
+
return this.client !== void 0;
|
|
90
|
+
}
|
|
91
|
+
/** Idle timestamp for reaping. */
|
|
92
|
+
idleSinceMs() {
|
|
93
|
+
return Date.now() - this.lastUsedAt;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Lazy connect + tool discovery. Safe to call concurrently; in-flight
|
|
97
|
+
* connects coalesce.
|
|
98
|
+
*/
|
|
99
|
+
async ensureConnected() {
|
|
100
|
+
if (this.client) return;
|
|
101
|
+
if (this.connectInFlight) return this.connectInFlight;
|
|
102
|
+
this.connectInFlight = this.connectAndDiscover().finally(() => {
|
|
103
|
+
this.connectInFlight = void 0;
|
|
104
|
+
});
|
|
105
|
+
return this.connectInFlight;
|
|
106
|
+
}
|
|
107
|
+
async connectAndDiscover() {
|
|
108
|
+
const safeConfig = "command" in this.config ? {
|
|
109
|
+
...this.config,
|
|
110
|
+
env: sanitizeStdioEnv(this.config.env)
|
|
111
|
+
} : this.config;
|
|
112
|
+
this.logger?.debug(`[mcp-bundler] connecting server "${this.name}"`);
|
|
113
|
+
const client = await this.deps.connect(safeConfig);
|
|
114
|
+
try {
|
|
115
|
+
const advertised = await client.listTools();
|
|
116
|
+
this.client = client;
|
|
117
|
+
this.tools = advertised.map((t) => ({
|
|
118
|
+
prefixed: buildNamespacedToolName(this.name, t.name),
|
|
119
|
+
server: this.name,
|
|
120
|
+
original: t.name,
|
|
121
|
+
label: (t.description ?? t.name).slice(0, 80),
|
|
122
|
+
description: t.description ?? "",
|
|
123
|
+
parameters: t.inputSchema
|
|
124
|
+
}));
|
|
125
|
+
this.lastUsedAt = Date.now();
|
|
126
|
+
this.logger?.info(`[mcp-bundler] server "${this.name}" connected, ${this.tools.length.toString()} tool(s)`);
|
|
127
|
+
} catch (err) {
|
|
128
|
+
await client.close().catch(() => void 0);
|
|
129
|
+
throw err;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Re-discover tools. Used on reconnect or `tools/list_changed` notification.
|
|
134
|
+
* Refresh-lock collapses concurrent refreshes; if one is in flight, the next
|
|
135
|
+
* is queued (max 1 queued, since N>1 queued provides no extra freshness).
|
|
136
|
+
*/
|
|
137
|
+
async refresh() {
|
|
138
|
+
if (!this.client) return this.ensureConnected();
|
|
139
|
+
if (this.refreshInFlight) {
|
|
140
|
+
this.refreshQueued = true;
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
this.refreshInFlight = true;
|
|
144
|
+
try {
|
|
145
|
+
this.tools = (await this.client.listTools()).map((t) => ({
|
|
146
|
+
prefixed: buildNamespacedToolName(this.name, t.name),
|
|
147
|
+
server: this.name,
|
|
148
|
+
original: t.name,
|
|
149
|
+
label: (t.description ?? t.name).slice(0, 80),
|
|
150
|
+
description: t.description ?? "",
|
|
151
|
+
parameters: t.inputSchema
|
|
152
|
+
}));
|
|
153
|
+
this.logger?.debug(`[mcp-bundler] server "${this.name}" refreshed, ${this.tools.length.toString()} tool(s)`);
|
|
154
|
+
} finally {
|
|
155
|
+
this.refreshInFlight = false;
|
|
156
|
+
if (this.refreshQueued) {
|
|
157
|
+
this.refreshQueued = false;
|
|
158
|
+
this.refresh().catch((err) => {
|
|
159
|
+
this.logger?.warn(`[mcp-bundler] queued refresh for "${this.name}" failed`, { err: err instanceof Error ? err.message : String(err) });
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
async callTool(originalName, args, signal) {
|
|
165
|
+
await this.ensureConnected();
|
|
166
|
+
if (!this.client) throw new Error(`server "${this.name}" failed to connect`);
|
|
167
|
+
this.lastUsedAt = Date.now();
|
|
168
|
+
return this.client.callTool(originalName, args, signal ? { signal } : void 0);
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Close the underlying transport. Idempotent. If a connect is in flight
|
|
172
|
+
* (warmup racing with reconcile-removal), wait for it to settle and then
|
|
173
|
+
* close the client it produced — otherwise the child process is orphaned.
|
|
174
|
+
*/
|
|
175
|
+
async close() {
|
|
176
|
+
if (this.connectInFlight) await this.connectInFlight.catch(() => void 0);
|
|
177
|
+
const c = this.client;
|
|
178
|
+
this.client = void 0;
|
|
179
|
+
this.tools = [];
|
|
180
|
+
if (c) await c.close().catch((err) => {
|
|
181
|
+
this.logger?.warn(`[mcp-bundler] close error for "${this.name}"`, { err: err instanceof Error ? err.message : String(err) });
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Stable hash of the config for diff detection in `reconcile`.
|
|
186
|
+
* Two configs with the same hash are equivalent (no restart needed).
|
|
187
|
+
*/
|
|
188
|
+
configFingerprint() {
|
|
189
|
+
return JSON.stringify(this.config);
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
/**
|
|
193
|
+
* Build the production `connect` factory using the official MCP SDK.
|
|
194
|
+
* Kept in a separate function so tests can substitute a mock without
|
|
195
|
+
* pulling the SDK into the test bundle.
|
|
196
|
+
*/
|
|
197
|
+
async function defaultConnect(server) {
|
|
198
|
+
const { Client } = await import("@modelcontextprotocol/sdk/client/index.js");
|
|
199
|
+
const client = new Client({
|
|
200
|
+
name: "alfe-mcp-bundler",
|
|
201
|
+
version: "0.0.0"
|
|
202
|
+
}, {});
|
|
203
|
+
if ("command" in server) {
|
|
204
|
+
const stdio = server;
|
|
205
|
+
const { StdioClientTransport } = await import("@modelcontextprotocol/sdk/client/stdio.js");
|
|
206
|
+
const transport = new StdioClientTransport({
|
|
207
|
+
command: stdio.command,
|
|
208
|
+
args: stdio.args ?? [],
|
|
209
|
+
env: { ...sanitizeStdioEnv(stdio.env) },
|
|
210
|
+
cwd: stdio.cwd
|
|
211
|
+
});
|
|
212
|
+
await client.connect(transport);
|
|
213
|
+
} else {
|
|
214
|
+
const remote = server;
|
|
215
|
+
if (remote.transport === "streamable-http") {
|
|
216
|
+
const { StreamableHTTPClientTransport } = await import("@modelcontextprotocol/sdk/client/streamableHttp.js");
|
|
217
|
+
const transport = new StreamableHTTPClientTransport(new URL(remote.url), { requestInit: { headers: remote.headers ?? {} } });
|
|
218
|
+
await client.connect(transport);
|
|
219
|
+
} else {
|
|
220
|
+
const { SSEClientTransport } = await import("@modelcontextprotocol/sdk/client/sse.js");
|
|
221
|
+
const transport = new SSEClientTransport(new URL(remote.url), { requestInit: { headers: remote.headers ?? {} } });
|
|
222
|
+
await client.connect(transport);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return {
|
|
226
|
+
async listTools() {
|
|
227
|
+
return (await client.listTools()).tools.map((t) => ({
|
|
228
|
+
name: t.name,
|
|
229
|
+
description: t.description,
|
|
230
|
+
inputSchema: t.inputSchema
|
|
231
|
+
}));
|
|
232
|
+
},
|
|
233
|
+
async callTool(name, args, opts) {
|
|
234
|
+
return await client.callTool({
|
|
235
|
+
name,
|
|
236
|
+
arguments: args
|
|
237
|
+
}, void 0, opts);
|
|
238
|
+
},
|
|
239
|
+
async close() {
|
|
240
|
+
await client.close();
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
//#endregion
|
|
245
|
+
//#region src/bundler.ts
|
|
246
|
+
const DEFAULT_IDLE_TTL_MS = 600 * 1e3;
|
|
247
|
+
const DEFAULT_IDLE_SWEEP_INTERVAL_MS = 60 * 1e3;
|
|
248
|
+
/**
|
|
249
|
+
* Provider-agnostic MCP server bundler. Holds N MCP server connections,
|
|
250
|
+
* exposes a unified namespaced tool catalog, and routes calls to the right
|
|
251
|
+
* server.
|
|
252
|
+
*
|
|
253
|
+
* Designed to be embedded in any host (OpenClaw plugin, AI proxy, Lambda).
|
|
254
|
+
* Public surface is intentionally synchronous where the host needs sync
|
|
255
|
+
* (snapshot, listTools), async only where I/O is unavoidable.
|
|
256
|
+
*/
|
|
257
|
+
var McpBundler = class {
|
|
258
|
+
logger;
|
|
259
|
+
connections = /* @__PURE__ */ new Map();
|
|
260
|
+
idleTtlMs;
|
|
261
|
+
idleSweepIntervalMs;
|
|
262
|
+
idleSweepTimer;
|
|
263
|
+
deps;
|
|
264
|
+
disposed = false;
|
|
265
|
+
reconcileLatch = Promise.resolve();
|
|
266
|
+
constructor(opts = {}, deps) {
|
|
267
|
+
this.logger = opts.logger;
|
|
268
|
+
this.idleTtlMs = opts.idleTtlMs ?? DEFAULT_IDLE_TTL_MS;
|
|
269
|
+
this.idleSweepIntervalMs = opts.idleSweepIntervalMs ?? DEFAULT_IDLE_SWEEP_INTERVAL_MS;
|
|
270
|
+
this.deps = deps ?? { connect: defaultConnect };
|
|
271
|
+
if (this.idleTtlMs > 0) this.startIdleSweep();
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Diff `desired` against current connections, spawn newcomers, dispose
|
|
275
|
+
* removals, hot-restart on config change. Pull-based — call whenever the
|
|
276
|
+
* host's config snapshot may have changed. Cheap if no diff.
|
|
277
|
+
*
|
|
278
|
+
* Lazy: newly-added servers are NOT eagerly connected; they connect on the
|
|
279
|
+
* first `callTool()` (or first `listTools()` after `forceDiscover()`).
|
|
280
|
+
* This avoids paying spawn cost for servers the agent never uses.
|
|
281
|
+
*/
|
|
282
|
+
async reconcile(desired) {
|
|
283
|
+
if (this.disposed) throw new Error("McpBundler: disposed");
|
|
284
|
+
const slot = this.reconcileLatch.then(async () => this.doReconcile(desired));
|
|
285
|
+
this.reconcileLatch = slot.catch(() => void 0);
|
|
286
|
+
return slot;
|
|
287
|
+
}
|
|
288
|
+
async doReconcile(desired) {
|
|
289
|
+
if (this.disposed) throw new Error("McpBundler: disposed");
|
|
290
|
+
const desiredNames = new Set(Object.keys(desired));
|
|
291
|
+
const currentNames = new Set(this.connections.keys());
|
|
292
|
+
const added = [];
|
|
293
|
+
const removed = [];
|
|
294
|
+
const changed = [];
|
|
295
|
+
const unchanged = [];
|
|
296
|
+
for (const name of currentNames) if (!desiredNames.has(name)) {
|
|
297
|
+
const conn = this.connections.get(name);
|
|
298
|
+
this.connections.delete(name);
|
|
299
|
+
if (conn) await conn.close();
|
|
300
|
+
removed.push(name);
|
|
301
|
+
}
|
|
302
|
+
for (const [name, config] of Object.entries(desired)) {
|
|
303
|
+
const existing = this.connections.get(name);
|
|
304
|
+
if (!existing) {
|
|
305
|
+
this.connections.set(name, new Connection({
|
|
306
|
+
name,
|
|
307
|
+
config,
|
|
308
|
+
deps: this.deps,
|
|
309
|
+
logger: this.logger
|
|
310
|
+
}));
|
|
311
|
+
added.push(name);
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
const nextFingerprint = JSON.stringify(config);
|
|
315
|
+
if (existing.configFingerprint() !== nextFingerprint) {
|
|
316
|
+
await existing.close();
|
|
317
|
+
this.connections.set(name, new Connection({
|
|
318
|
+
name,
|
|
319
|
+
config,
|
|
320
|
+
deps: this.deps,
|
|
321
|
+
logger: this.logger
|
|
322
|
+
}));
|
|
323
|
+
changed.push(name);
|
|
324
|
+
} else unchanged.push(name);
|
|
325
|
+
}
|
|
326
|
+
if (added.length || removed.length || changed.length) this.logger?.info("[mcp-bundler] reconciled", {
|
|
327
|
+
added: added.length,
|
|
328
|
+
removed: removed.length,
|
|
329
|
+
changed: changed.length,
|
|
330
|
+
unchanged: unchanged.length
|
|
331
|
+
});
|
|
332
|
+
return {
|
|
333
|
+
added,
|
|
334
|
+
removed,
|
|
335
|
+
changed,
|
|
336
|
+
unchanged
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Synchronous snapshot of all currently-known tools across connected servers.
|
|
341
|
+
* Servers that have not connected yet contribute nothing. Intended for use
|
|
342
|
+
* inside OpenClaw's plugin tool factory which must be sync.
|
|
343
|
+
*
|
|
344
|
+
* Tool names are namespaced and disambiguated (suffix `-2`, `-3` on collision)
|
|
345
|
+
* so cross-server name clashes never produce duplicate registrations.
|
|
346
|
+
*/
|
|
347
|
+
listTools() {
|
|
348
|
+
const seen = /* @__PURE__ */ new Set();
|
|
349
|
+
const out = [];
|
|
350
|
+
for (const conn of this.connections.values()) for (const tool of conn.snapshotTools()) {
|
|
351
|
+
const finalName = disambiguateAgainst(tool.prefixed, seen);
|
|
352
|
+
seen.add(finalName);
|
|
353
|
+
out.push(finalName === tool.prefixed ? tool : {
|
|
354
|
+
...tool,
|
|
355
|
+
prefixed: finalName
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
return out;
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Eagerly connect to every configured server and discover tools. Used by
|
|
362
|
+
* hosts that want a hot list rather than the lazy default. Errors are
|
|
363
|
+
* swallowed per-server (logged), so one bad server doesn't fail the batch.
|
|
364
|
+
*/
|
|
365
|
+
async warmup() {
|
|
366
|
+
if (this.disposed) return;
|
|
367
|
+
await Promise.allSettled(Array.from(this.connections.values()).map(async (conn) => {
|
|
368
|
+
try {
|
|
369
|
+
await conn.ensureConnected();
|
|
370
|
+
} catch (err) {
|
|
371
|
+
this.logger?.warn(`[mcp-bundler] warmup failed for "${conn.name}"`, { err: err instanceof Error ? err.message : String(err) });
|
|
372
|
+
}
|
|
373
|
+
}));
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Invoke a tool by its namespaced name. Routes to the originating server.
|
|
377
|
+
* Errors are returned as `{ isError: true, content: [...] }` so a failing
|
|
378
|
+
* tool doesn't crash the host.
|
|
379
|
+
*/
|
|
380
|
+
async callTool(prefixed, args, signal) {
|
|
381
|
+
if (this.disposed) return {
|
|
382
|
+
isError: true,
|
|
383
|
+
content: [{
|
|
384
|
+
type: "text",
|
|
385
|
+
text: "mcp-bundler disposed"
|
|
386
|
+
}]
|
|
387
|
+
};
|
|
388
|
+
const route = this.routeToolName(prefixed);
|
|
389
|
+
if (!route) return {
|
|
390
|
+
isError: true,
|
|
391
|
+
content: [{
|
|
392
|
+
type: "text",
|
|
393
|
+
text: `unknown tool: ${prefixed}`
|
|
394
|
+
}]
|
|
395
|
+
};
|
|
396
|
+
try {
|
|
397
|
+
return await route.connection.callTool(route.original, args, signal);
|
|
398
|
+
} catch (err) {
|
|
399
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
400
|
+
this.logger?.error(`[mcp-bundler] tool call failed for "${prefixed}"`, { err: msg });
|
|
401
|
+
return {
|
|
402
|
+
isError: true,
|
|
403
|
+
content: [{
|
|
404
|
+
type: "text",
|
|
405
|
+
text: `tool ${prefixed} failed: ${msg}`
|
|
406
|
+
}]
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Resolve a namespaced tool name back to its server connection and original
|
|
412
|
+
* tool name. Returns undefined if the tool is not currently advertised.
|
|
413
|
+
*/
|
|
414
|
+
routeToolName(prefixed) {
|
|
415
|
+
for (const conn of this.connections.values()) for (const tool of conn.snapshotTools()) if (tool.prefixed === prefixed) return {
|
|
416
|
+
connection: conn,
|
|
417
|
+
original: tool.original
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Tear down all connections and stop background tasks. Idempotent.
|
|
422
|
+
* Call from `registerRuntimeLifecycle({ cleanup })` in the host plugin.
|
|
423
|
+
*/
|
|
424
|
+
async dispose() {
|
|
425
|
+
if (this.disposed) return;
|
|
426
|
+
this.disposed = true;
|
|
427
|
+
if (this.idleSweepTimer) {
|
|
428
|
+
clearInterval(this.idleSweepTimer);
|
|
429
|
+
this.idleSweepTimer = void 0;
|
|
430
|
+
}
|
|
431
|
+
await Promise.allSettled(Array.from(this.connections.values()).map((c) => c.close()));
|
|
432
|
+
this.connections.clear();
|
|
433
|
+
}
|
|
434
|
+
startIdleSweep() {
|
|
435
|
+
this.idleSweepTimer = setInterval(() => {
|
|
436
|
+
this.sweepIdle().catch((err) => {
|
|
437
|
+
this.logger?.warn("[mcp-bundler] idle sweep error", { err: err instanceof Error ? err.message : String(err) });
|
|
438
|
+
});
|
|
439
|
+
}, this.idleSweepIntervalMs);
|
|
440
|
+
if (typeof this.idleSweepTimer === "object" && "unref" in this.idleSweepTimer) this.idleSweepTimer.unref();
|
|
441
|
+
}
|
|
442
|
+
async sweepIdle() {
|
|
443
|
+
if (this.idleTtlMs <= 0) return;
|
|
444
|
+
const targets = [];
|
|
445
|
+
for (const conn of this.connections.values()) if (conn.isConnected() && conn.idleSinceMs() > this.idleTtlMs) targets.push(conn);
|
|
446
|
+
if (targets.length === 0) return;
|
|
447
|
+
this.logger?.debug(`[mcp-bundler] reaping ${targets.length.toString()} idle server(s)`);
|
|
448
|
+
await Promise.allSettled(targets.map((c) => c.close()));
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
//#endregion
|
|
452
|
+
export { Connection, McpBundler, STDIO_ENV_DENYLIST, buildNamespacedToolName, defaultConnect, disambiguateAgainst, sanitizeNameSegment, sanitizeStdioEnv };
|
|
453
|
+
|
|
454
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/tool-naming.ts","../src/connection.ts","../src/bundler.ts"],"sourcesContent":["/**\n * Tool name sanitization and collision handling.\n *\n * OpenClaw constraint: tool names must match `[A-Za-z0-9_-]` and be ≤64 chars.\n * Pattern mirrored from `openclaw/src/agents/pi-bundle-mcp-names.ts`.\n *\n * Strategy: prefix every tool with its server name (`{server}__{tool}`),\n * sanitize disallowed chars to `_`, truncate, then suffix-disambiguate\n * (`-2`, `-3`, ...) on collision.\n */\n\nconst DISALLOWED = /[^A-Za-z0-9_-]/g;\nconst MAX_LEN = 64;\nconst SEPARATOR = '__';\n\nexport function sanitizeNameSegment(value: string): string {\n return value.replace(DISALLOWED, '_');\n}\n\nexport function buildNamespacedToolName(server: string, tool: string): string {\n const base = `${sanitizeNameSegment(server)}${SEPARATOR}${sanitizeNameSegment(tool)}`;\n if (base.length <= MAX_LEN) return base;\n // Truncate from the tool side first to keep the server prefix intact.\n const reservedForServer = sanitizeNameSegment(server).length + SEPARATOR.length;\n const toolBudget = Math.max(1, MAX_LEN - reservedForServer);\n return `${sanitizeNameSegment(server)}${SEPARATOR}${sanitizeNameSegment(tool).slice(0, toolBudget)}`;\n}\n\n/**\n * Disambiguate a candidate name against an existing set by appending `-2`, `-3`, etc.\n * Mutates nothing; returns the chosen name. Caller is responsible for inserting it\n * into the set.\n */\nexport function disambiguateAgainst(candidate: string, taken: ReadonlySet<string>): string {\n if (!taken.has(candidate)) return candidate;\n for (let i = 2; i < 1000; i += 1) {\n const suffix = `-${i.toString()}`;\n const room = MAX_LEN - suffix.length;\n const trimmed = candidate.length > room ? candidate.slice(0, room) : candidate;\n const next = `${trimmed}${suffix}`;\n if (!taken.has(next)) return next;\n }\n // Pathological: 998 collisions. Fall back to a deterministic-ish hash.\n return `${candidate.slice(0, MAX_LEN - 6)}-x${(taken.size % 1000).toString().padStart(3, '0')}`;\n}\n","import type { Logger, McpServerConfig, McpToolDescriptor, McpToolCallResult, StdioServerConfig } from './types.js';\nimport { buildNamespacedToolName } from './tool-naming.js';\n\n/** Env keys OpenClaw rejects from stdio MCP env blocks. Filter them out before spawning. */\nexport const STDIO_ENV_DENYLIST = new Set([\n 'NODE_OPTIONS',\n 'PYTHONSTARTUP',\n 'PYTHONPATH',\n 'PERL5OPT',\n 'RUBYOPT',\n 'SHELLOPTS',\n 'PS4',\n]);\n\nexport function sanitizeStdioEnv(env: Record<string, string> | undefined): Record<string, string> {\n if (!env) return {};\n const safe: Record<string, string> = {};\n for (const [k, v] of Object.entries(env)) {\n if (STDIO_ENV_DENYLIST.has(k)) continue;\n safe[k] = v;\n }\n return safe;\n}\n\nexport interface ConnectionDeps {\n /**\n * Factory for an MCP Client connected to the given config. Injected so tests\n * can mock without spawning real processes. In production this wraps\n * `@modelcontextprotocol/sdk/client`.\n */\n connect: (server: McpServerConfig) => Promise<McpClientHandle>;\n}\n\n/**\n * Minimal interface our connection layer needs from an MCP client. Mirrors\n * the @modelcontextprotocol/sdk Client surface but kept narrow so we can\n * mock cleanly in tests.\n */\nexport interface McpClientHandle {\n listTools(): Promise<{ name: string; description?: string; inputSchema: Record<string, unknown> }[]>;\n callTool(name: string, args: unknown, opts?: { signal?: AbortSignal }): Promise<McpToolCallResult>;\n close(): Promise<void>;\n}\n\n/**\n * One Connection per MCP server. Owns lifecycle (lazy-spawn, close, refresh-lock).\n * Refresh-lock pattern adapted from AIWerk `index.ts:219-250` — prevents\n * reconnect + `notifications/tools/list_changed` race.\n */\nexport class Connection {\n readonly name: string;\n readonly config: McpServerConfig;\n private readonly deps: ConnectionDeps;\n private readonly logger: Logger | undefined;\n\n private client: McpClientHandle | undefined;\n private tools: McpToolDescriptor[] = [];\n private connectInFlight: Promise<void> | undefined;\n private refreshInFlight = false;\n private refreshQueued = false;\n private lastUsedAt = Date.now();\n\n constructor(params: { name: string; config: McpServerConfig; deps: ConnectionDeps; logger?: Logger }) {\n this.name = params.name;\n this.config = params.config;\n this.deps = params.deps;\n this.logger = params.logger;\n }\n\n /** Returns the most recent known tool list. May be empty if the server hasn't connected yet. */\n snapshotTools(): McpToolDescriptor[] {\n return this.tools;\n }\n\n /** Whether an MCP child process / remote connection has been established. */\n isConnected(): boolean {\n return this.client !== undefined;\n }\n\n /** Idle timestamp for reaping. */\n idleSinceMs(): number {\n return Date.now() - this.lastUsedAt;\n }\n\n /**\n * Lazy connect + tool discovery. Safe to call concurrently; in-flight\n * connects coalesce.\n */\n async ensureConnected(): Promise<void> {\n if (this.client) return;\n if (this.connectInFlight) return this.connectInFlight;\n this.connectInFlight = this.connectAndDiscover().finally(() => {\n this.connectInFlight = undefined;\n });\n return this.connectInFlight;\n }\n\n private async connectAndDiscover(): Promise<void> {\n const safeConfig = 'command' in this.config\n ? ({ ...this.config, env: sanitizeStdioEnv(this.config.env) } satisfies StdioServerConfig)\n : this.config;\n this.logger?.debug(`[mcp-bundler] connecting server \"${this.name}\"`);\n const client = await this.deps.connect(safeConfig);\n try {\n const advertised = await client.listTools();\n this.client = client;\n this.tools = advertised.map((t) => ({\n prefixed: buildNamespacedToolName(this.name, t.name),\n server: this.name,\n original: t.name,\n label: (t.description ?? t.name).slice(0, 80),\n description: t.description ?? '',\n parameters: t.inputSchema,\n }));\n this.lastUsedAt = Date.now();\n this.logger?.info(`[mcp-bundler] server \"${this.name}\" connected, ${this.tools.length.toString()} tool(s)`);\n } catch (err) {\n await client.close().catch(() => undefined);\n throw err;\n }\n }\n\n /**\n * Re-discover tools. Used on reconnect or `tools/list_changed` notification.\n * Refresh-lock collapses concurrent refreshes; if one is in flight, the next\n * is queued (max 1 queued, since N>1 queued provides no extra freshness).\n */\n async refresh(): Promise<void> {\n if (!this.client) return this.ensureConnected();\n if (this.refreshInFlight) {\n this.refreshQueued = true;\n return;\n }\n this.refreshInFlight = true;\n try {\n const advertised = await this.client.listTools();\n this.tools = advertised.map((t) => ({\n prefixed: buildNamespacedToolName(this.name, t.name),\n server: this.name,\n original: t.name,\n label: (t.description ?? t.name).slice(0, 80),\n description: t.description ?? '',\n parameters: t.inputSchema,\n }));\n this.logger?.debug(`[mcp-bundler] server \"${this.name}\" refreshed, ${this.tools.length.toString()} tool(s)`);\n } finally {\n this.refreshInFlight = false;\n if (this.refreshQueued) {\n this.refreshQueued = false;\n // Trigger one more refresh; do not await so caller isn't blocked on cascading refreshes.\n void this.refresh().catch((err: unknown) => {\n this.logger?.warn(`[mcp-bundler] queued refresh for \"${this.name}\" failed`, { err: err instanceof Error ? err.message : String(err) });\n });\n }\n }\n }\n\n async callTool(originalName: string, args: unknown, signal?: AbortSignal): Promise<McpToolCallResult> {\n await this.ensureConnected();\n if (!this.client) throw new Error(`server \"${this.name}\" failed to connect`);\n this.lastUsedAt = Date.now();\n return this.client.callTool(originalName, args, signal ? { signal } : undefined);\n }\n\n /**\n * Close the underlying transport. Idempotent. If a connect is in flight\n * (warmup racing with reconcile-removal), wait for it to settle and then\n * close the client it produced — otherwise the child process is orphaned.\n */\n async close(): Promise<void> {\n if (this.connectInFlight) {\n await this.connectInFlight.catch(() => undefined);\n }\n const c = this.client;\n this.client = undefined;\n this.tools = [];\n if (c) await c.close().catch((err: unknown) => {\n this.logger?.warn(`[mcp-bundler] close error for \"${this.name}\"`, { err: err instanceof Error ? err.message : String(err) });\n });\n }\n\n /**\n * Stable hash of the config for diff detection in `reconcile`.\n * Two configs with the same hash are equivalent (no restart needed).\n */\n configFingerprint(): string {\n return JSON.stringify(this.config);\n }\n}\n\n/**\n * Build the production `connect` factory using the official MCP SDK.\n * Kept in a separate function so tests can substitute a mock without\n * pulling the SDK into the test bundle.\n */\nexport async function defaultConnect(server: McpServerConfig): Promise<McpClientHandle> {\n const { Client } = await import('@modelcontextprotocol/sdk/client/index.js');\n const client = new Client({ name: 'alfe-mcp-bundler', version: '0.0.0' }, {});\n\n if ('command' in server) {\n const stdio = server;\n const { StdioClientTransport } = await import('@modelcontextprotocol/sdk/client/stdio.js');\n const transport = new StdioClientTransport({\n command: stdio.command,\n args: stdio.args ?? [],\n env: { ...sanitizeStdioEnv(stdio.env) } as Record<string, string>,\n cwd: stdio.cwd,\n });\n await client.connect(transport);\n } else {\n const remote = server;\n if (remote.transport === 'streamable-http') {\n const { StreamableHTTPClientTransport } = await import('@modelcontextprotocol/sdk/client/streamableHttp.js');\n const transport = new StreamableHTTPClientTransport(new URL(remote.url), {\n requestInit: { headers: remote.headers ?? {} },\n });\n await client.connect(transport);\n } else {\n // SSE is deprecated in newer MCP SDK in favor of streamable-http, but\n // some servers still only support SSE — keep transport for back-compat.\n /* eslint-disable @typescript-eslint/no-deprecated */\n const { SSEClientTransport } = await import('@modelcontextprotocol/sdk/client/sse.js');\n const transport = new SSEClientTransport(new URL(remote.url), {\n requestInit: { headers: remote.headers ?? {} },\n });\n /* eslint-enable @typescript-eslint/no-deprecated */\n await client.connect(transport);\n }\n }\n\n return {\n async listTools() {\n const result = await client.listTools();\n return result.tools.map((t) => ({\n name: t.name,\n description: t.description,\n inputSchema: t.inputSchema as Record<string, unknown>,\n }));\n },\n async callTool(name, args, opts) {\n return (await client.callTool({ name, arguments: args as Record<string, unknown> | undefined }, undefined, opts)) as McpToolCallResult;\n },\n async close() {\n await client.close();\n },\n };\n}\n","import { Connection, defaultConnect, type ConnectionDeps } from './connection.js';\nimport { disambiguateAgainst } from './tool-naming.js';\nimport type {\n BundlerOptions,\n Logger,\n McpServerConfig,\n McpToolCallResult,\n McpToolDescriptor,\n ReconcileDiff,\n} from './types.js';\n\nconst DEFAULT_IDLE_TTL_MS = 10 * 60 * 1000;\nconst DEFAULT_IDLE_SWEEP_INTERVAL_MS = 60 * 1000;\n\n/**\n * Provider-agnostic MCP server bundler. Holds N MCP server connections,\n * exposes a unified namespaced tool catalog, and routes calls to the right\n * server.\n *\n * Designed to be embedded in any host (OpenClaw plugin, AI proxy, Lambda).\n * Public surface is intentionally synchronous where the host needs sync\n * (snapshot, listTools), async only where I/O is unavoidable.\n */\nexport class McpBundler {\n private readonly logger: Logger | undefined;\n private readonly connections = new Map<string, Connection>();\n private readonly idleTtlMs: number;\n private readonly idleSweepIntervalMs: number;\n private idleSweepTimer: ReturnType<typeof setInterval> | undefined;\n private readonly deps: ConnectionDeps;\n private disposed = false;\n // Serialize reconcile() so concurrent callers (multiple plugin tool factory\n // ticks within the same ms) don't interleave and orphan Connections, leaking\n // child processes. Acquired via a chain-of-promises latch.\n private reconcileLatch: Promise<unknown> = Promise.resolve();\n\n constructor(opts: BundlerOptions = {}, deps?: ConnectionDeps) {\n this.logger = opts.logger;\n this.idleTtlMs = opts.idleTtlMs ?? DEFAULT_IDLE_TTL_MS;\n this.idleSweepIntervalMs = opts.idleSweepIntervalMs ?? DEFAULT_IDLE_SWEEP_INTERVAL_MS;\n this.deps = deps ?? { connect: defaultConnect };\n if (this.idleTtlMs > 0) this.startIdleSweep();\n }\n\n /**\n * Diff `desired` against current connections, spawn newcomers, dispose\n * removals, hot-restart on config change. Pull-based — call whenever the\n * host's config snapshot may have changed. Cheap if no diff.\n *\n * Lazy: newly-added servers are NOT eagerly connected; they connect on the\n * first `callTool()` (or first `listTools()` after `forceDiscover()`).\n * This avoids paying spawn cost for servers the agent never uses.\n */\n async reconcile(desired: Record<string, McpServerConfig>): Promise<ReconcileDiff> {\n if (this.disposed) throw new Error('McpBundler: disposed');\n // Serialize reconciles. Caller awaits its slot; in-flight reconciles run\n // in declaration order. Errors don't poison the latch — `.catch` swallows\n // for chaining, the actual error rejects the awaited slot.\n const slot = this.reconcileLatch.then(async () => this.doReconcile(desired));\n this.reconcileLatch = slot.catch(() => undefined);\n return slot;\n }\n\n private async doReconcile(desired: Record<string, McpServerConfig>): Promise<ReconcileDiff> {\n if (this.disposed) throw new Error('McpBundler: disposed');\n const desiredNames = new Set(Object.keys(desired));\n const currentNames = new Set(this.connections.keys());\n\n const added: string[] = [];\n const removed: string[] = [];\n const changed: string[] = [];\n const unchanged: string[] = [];\n\n // Removals: dispose connections no longer in desired set.\n for (const name of currentNames) {\n if (!desiredNames.has(name)) {\n const conn = this.connections.get(name);\n this.connections.delete(name);\n if (conn) await conn.close();\n removed.push(name);\n }\n }\n\n // Additions and changes.\n for (const [name, config] of Object.entries(desired)) {\n const existing = this.connections.get(name);\n if (!existing) {\n this.connections.set(name, new Connection({ name, config, deps: this.deps, logger: this.logger }));\n added.push(name);\n continue;\n }\n const nextFingerprint = JSON.stringify(config);\n if (existing.configFingerprint() !== nextFingerprint) {\n // Config changed — close old, replace with fresh (lazy reconnect).\n await existing.close();\n this.connections.set(name, new Connection({ name, config, deps: this.deps, logger: this.logger }));\n changed.push(name);\n } else {\n unchanged.push(name);\n }\n }\n\n if (added.length || removed.length || changed.length) {\n this.logger?.info('[mcp-bundler] reconciled', {\n added: added.length,\n removed: removed.length,\n changed: changed.length,\n unchanged: unchanged.length,\n });\n }\n return { added, removed, changed, unchanged };\n }\n\n /**\n * Synchronous snapshot of all currently-known tools across connected servers.\n * Servers that have not connected yet contribute nothing. Intended for use\n * inside OpenClaw's plugin tool factory which must be sync.\n *\n * Tool names are namespaced and disambiguated (suffix `-2`, `-3` on collision)\n * so cross-server name clashes never produce duplicate registrations.\n */\n listTools(): McpToolDescriptor[] {\n const seen = new Set<string>();\n const out: McpToolDescriptor[] = [];\n for (const conn of this.connections.values()) {\n for (const tool of conn.snapshotTools()) {\n const finalName = disambiguateAgainst(tool.prefixed, seen);\n seen.add(finalName);\n out.push(finalName === tool.prefixed ? tool : { ...tool, prefixed: finalName });\n }\n }\n return out;\n }\n\n /**\n * Eagerly connect to every configured server and discover tools. Used by\n * hosts that want a hot list rather than the lazy default. Errors are\n * swallowed per-server (logged), so one bad server doesn't fail the batch.\n */\n async warmup(): Promise<void> {\n if (this.disposed) return;\n await Promise.allSettled(\n Array.from(this.connections.values()).map(async (conn) => {\n try {\n await conn.ensureConnected();\n } catch (err) {\n this.logger?.warn(`[mcp-bundler] warmup failed for \"${conn.name}\"`, {\n err: err instanceof Error ? err.message : String(err),\n });\n }\n }),\n );\n }\n\n /**\n * Invoke a tool by its namespaced name. Routes to the originating server.\n * Errors are returned as `{ isError: true, content: [...] }` so a failing\n * tool doesn't crash the host.\n */\n async callTool(prefixed: string, args: unknown, signal?: AbortSignal): Promise<McpToolCallResult> {\n if (this.disposed) {\n return { isError: true, content: [{ type: 'text', text: 'mcp-bundler disposed' }] };\n }\n const route = this.routeToolName(prefixed);\n if (!route) {\n return {\n isError: true,\n content: [{ type: 'text', text: `unknown tool: ${prefixed}` }],\n };\n }\n try {\n return await route.connection.callTool(route.original, args, signal);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n this.logger?.error(`[mcp-bundler] tool call failed for \"${prefixed}\"`, { err: msg });\n return {\n isError: true,\n content: [{ type: 'text', text: `tool ${prefixed} failed: ${msg}` }],\n };\n }\n }\n\n /**\n * Resolve a namespaced tool name back to its server connection and original\n * tool name. Returns undefined if the tool is not currently advertised.\n */\n private routeToolName(prefixed: string): { connection: Connection; original: string } | undefined {\n for (const conn of this.connections.values()) {\n for (const tool of conn.snapshotTools()) {\n if (tool.prefixed === prefixed) return { connection: conn, original: tool.original };\n }\n }\n return undefined;\n }\n\n /**\n * Tear down all connections and stop background tasks. Idempotent.\n * Call from `registerRuntimeLifecycle({ cleanup })` in the host plugin.\n */\n async dispose(): Promise<void> {\n if (this.disposed) return;\n this.disposed = true;\n if (this.idleSweepTimer) {\n clearInterval(this.idleSweepTimer);\n this.idleSweepTimer = undefined;\n }\n await Promise.allSettled(Array.from(this.connections.values()).map((c) => c.close()));\n this.connections.clear();\n }\n\n private startIdleSweep(): void {\n this.idleSweepTimer = setInterval(() => {\n void this.sweepIdle().catch((err: unknown) => {\n this.logger?.warn('[mcp-bundler] idle sweep error', {\n err: err instanceof Error ? err.message : String(err),\n });\n });\n }, this.idleSweepIntervalMs);\n // Don't keep the host process alive just for the sweep.\n if (typeof this.idleSweepTimer === 'object' && 'unref' in this.idleSweepTimer) {\n (this.idleSweepTimer as { unref: () => void }).unref();\n }\n }\n\n private async sweepIdle(): Promise<void> {\n if (this.idleTtlMs <= 0) return;\n const targets: Connection[] = [];\n for (const conn of this.connections.values()) {\n if (conn.isConnected() && conn.idleSinceMs() > this.idleTtlMs) {\n targets.push(conn);\n }\n }\n if (targets.length === 0) return;\n this.logger?.debug(`[mcp-bundler] reaping ${targets.length.toString()} idle server(s)`);\n await Promise.allSettled(targets.map((c) => c.close()));\n }\n}\n"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@alfe.ai/mcp-bundler",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Provider-agnostic MCP server bundler — connects to N MCP servers (stdio/SSE/streamable-http), aggregates their tools with namespacing, exposes a unified callable surface",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"require": "./dist/index.cjs",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@modelcontextprotocol/sdk": "^1.29.0"
|
|
20
|
+
},
|
|
21
|
+
"license": "UNLICENSED",
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsdown",
|
|
24
|
+
"dev": "tsdown --watch",
|
|
25
|
+
"test": "vitest run",
|
|
26
|
+
"typecheck": "tsc --noEmit",
|
|
27
|
+
"lint": "eslint ."
|
|
28
|
+
}
|
|
29
|
+
}
|