@alfe.ai/mcp-bundler 0.4.0 → 0.4.2
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/README.md +26 -0
- package/dist/index.cjs +590 -144
- package/dist/index.d.cts +37 -18
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +37 -18
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +591 -146
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { dirname, isAbsolute, join } from "node:path";
|
|
2
|
+
import { closeSync, constants, fchmodSync, fstatSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, watch, writeFileSync } from "node:fs";
|
|
3
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
3
4
|
import { homedir } from "node:os";
|
|
4
5
|
//#region src/tool-naming.ts
|
|
5
6
|
/**
|
|
@@ -19,11 +20,15 @@ function sanitizeNameSegment(value) {
|
|
|
19
20
|
return value.replace(DISALLOWED, "_");
|
|
20
21
|
}
|
|
21
22
|
function buildNamespacedToolName(server, tool) {
|
|
22
|
-
const
|
|
23
|
+
const sanitizedServer = sanitizeNameSegment(server);
|
|
24
|
+
const sanitizedTool = sanitizeNameSegment(tool);
|
|
25
|
+
const serverBudget = MAX_LEN - 2 - 1;
|
|
26
|
+
const boundedServer = sanitizedServer.slice(0, serverBudget);
|
|
27
|
+
const base = `${boundedServer}${SEPARATOR}${sanitizedTool}`;
|
|
23
28
|
if (base.length <= MAX_LEN) return base;
|
|
24
|
-
const reservedForServer =
|
|
29
|
+
const reservedForServer = boundedServer.length + 2;
|
|
25
30
|
const toolBudget = Math.max(1, MAX_LEN - reservedForServer);
|
|
26
|
-
return `${
|
|
31
|
+
return `${boundedServer}${SEPARATOR}${sanitizedTool.slice(0, toolBudget)}`;
|
|
27
32
|
}
|
|
28
33
|
/**
|
|
29
34
|
* Disambiguate a candidate name against an existing set by appending `-2`, `-3`, etc.
|
|
@@ -32,16 +37,231 @@ function buildNamespacedToolName(server, tool) {
|
|
|
32
37
|
*/
|
|
33
38
|
function disambiguateAgainst(candidate, taken) {
|
|
34
39
|
if (!taken.has(candidate)) return candidate;
|
|
35
|
-
for (let i = 2; i
|
|
40
|
+
for (let i = 2; i <= taken.size + 2; i += 1) {
|
|
36
41
|
const suffix = `-${i.toString()}`;
|
|
37
42
|
const room = MAX_LEN - suffix.length;
|
|
38
43
|
const next = `${candidate.length > room ? candidate.slice(0, room) : candidate}${suffix}`;
|
|
39
44
|
if (!taken.has(next)) return next;
|
|
40
45
|
}
|
|
41
|
-
|
|
46
|
+
throw new Error("unable to disambiguate MCP tool name");
|
|
47
|
+
}
|
|
48
|
+
//#endregion
|
|
49
|
+
//#region src/validation.ts
|
|
50
|
+
const MAX_SERVERS = 256;
|
|
51
|
+
const MAX_TOOLS_PER_SERVER = 512;
|
|
52
|
+
const MAX_SCHEMA_BYTES = 512 * 1024;
|
|
53
|
+
const MAX_ARGUMENT_BYTES = 2 * 1024 * 1024;
|
|
54
|
+
const MAX_RESULT_BYTES = 5 * 1024 * 1024;
|
|
55
|
+
const MAX_JSON_DEPTH = 64;
|
|
56
|
+
const MAX_JSON_NODES = 5e4;
|
|
57
|
+
const MAX_DESCRIPTION_LENGTH = 2e4;
|
|
58
|
+
const SERVER_ID = /^[A-Za-z0-9][A-Za-z0-9._:@#-]{0,127}$/;
|
|
59
|
+
const TOOL_NAME = /^[A-Za-z0-9_.:-]{1,128}$/;
|
|
60
|
+
const ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
61
|
+
const HEADER_NAME = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
|
|
62
|
+
const UNSAFE_KEYS = new Set([
|
|
63
|
+
"__proto__",
|
|
64
|
+
"prototype",
|
|
65
|
+
"constructor"
|
|
66
|
+
]);
|
|
67
|
+
function validateServerId(id, label = "server id") {
|
|
68
|
+
if (typeof id !== "string" || !SERVER_ID.test(id) || UNSAFE_KEYS.has(id)) throw new Error(`${label} must match ${SERVER_ID.source}`);
|
|
69
|
+
return id;
|
|
70
|
+
}
|
|
71
|
+
function validateServerCount(count) {
|
|
72
|
+
if (count > MAX_SERVERS) throw new Error(`MCP store exceeds the ${MAX_SERVERS.toString()} server limit`);
|
|
73
|
+
}
|
|
74
|
+
function validateServerConfig(value, label = "server config") {
|
|
75
|
+
const config = requireRecord(value, label);
|
|
76
|
+
const hasCommand = Object.hasOwn(config, "command");
|
|
77
|
+
if (hasCommand === Object.hasOwn(config, "url")) throw new Error(`${label} must contain exactly one of command or url`);
|
|
78
|
+
return hasCommand ? validateStdioConfig(config, label) : validateRemoteConfig(config, label);
|
|
79
|
+
}
|
|
80
|
+
function normalizeChildCatalog(server, advertised) {
|
|
81
|
+
validateServerId(server);
|
|
82
|
+
if (!Array.isArray(advertised)) throw new Error(`server "${server}" returned a non-array tool catalog`);
|
|
83
|
+
if (advertised.length > MAX_TOOLS_PER_SERVER) throw new Error(`server "${server}" advertised ${advertised.length.toString()} tools; limit is ${MAX_TOOLS_PER_SERVER.toString()}`);
|
|
84
|
+
const originals = /* @__PURE__ */ new Set();
|
|
85
|
+
return advertised.map((raw, index) => {
|
|
86
|
+
const tool = requireRecord(raw, `server "${server}" tool ${index.toString()}`);
|
|
87
|
+
const name = tool.name;
|
|
88
|
+
if (typeof name !== "string" || !TOOL_NAME.test(name)) throw new Error(`server "${server}" advertised an invalid tool name at index ${index.toString()}`);
|
|
89
|
+
if (originals.has(name)) throw new Error(`server "${server}" advertised duplicate tool "${name}"`);
|
|
90
|
+
originals.add(name);
|
|
91
|
+
const description = tool.description === void 0 ? "" : requireBoundedString(tool.description, `server "${server}" tool "${name}" description`, MAX_DESCRIPTION_LENGTH);
|
|
92
|
+
const parameters = cloneBoundedJson(tool.inputSchema, `server "${server}" tool "${name}" input schema`, MAX_SCHEMA_BYTES);
|
|
93
|
+
if (!isRecord$1(parameters)) throw new Error(`server "${server}" tool "${name}" input schema must be an object`);
|
|
94
|
+
return {
|
|
95
|
+
prefixed: buildNamespacedToolName(server, name),
|
|
96
|
+
server,
|
|
97
|
+
original: name,
|
|
98
|
+
label: (description || name).slice(0, 80),
|
|
99
|
+
description,
|
|
100
|
+
parameters
|
|
101
|
+
};
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
function validateToolArguments(args) {
|
|
105
|
+
if (args === void 0) return void 0;
|
|
106
|
+
const cloned = cloneBoundedJson(args, "MCP tool arguments", MAX_ARGUMENT_BYTES);
|
|
107
|
+
if (!isRecord$1(cloned)) throw new Error("MCP tool arguments must be an object");
|
|
108
|
+
return cloned;
|
|
109
|
+
}
|
|
110
|
+
function normalizeToolResult(result) {
|
|
111
|
+
const cloned = cloneBoundedJson(result, "MCP tool result", MAX_RESULT_BYTES);
|
|
112
|
+
if (!isRecord$1(cloned) || !Array.isArray(cloned.content)) throw new Error("MCP tool result must be an object with a content array");
|
|
113
|
+
for (const [index, item] of cloned.content.entries()) if (!isRecord$1(item)) throw new Error(`MCP tool result content[${index.toString()}] must be an object`);
|
|
114
|
+
if (cloned.isError !== void 0 && typeof cloned.isError !== "boolean") throw new Error("MCP tool result isError must be a boolean when present");
|
|
115
|
+
if (cloned.structuredContent !== void 0 && !isRecord$1(cloned.structuredContent)) throw new Error("MCP tool result structuredContent must be an object when present");
|
|
116
|
+
return cloned;
|
|
117
|
+
}
|
|
118
|
+
function redactErrorResult(result, config) {
|
|
119
|
+
if (!result.isError) return result;
|
|
120
|
+
return {
|
|
121
|
+
...result,
|
|
122
|
+
content: result.content.map((item) => item.type === "text" && typeof item.text === "string" ? {
|
|
123
|
+
...item,
|
|
124
|
+
text: redactSensitiveText(item.text, config)
|
|
125
|
+
} : item)
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function cloneToolDescriptor(tool) {
|
|
129
|
+
return {
|
|
130
|
+
...tool,
|
|
131
|
+
parameters: cloneBoundedJson(tool.parameters, `tool "${tool.prefixed}" schema`, MAX_SCHEMA_BYTES)
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function redactSensitiveText(text, config) {
|
|
135
|
+
let output = String(text).slice(0, MAX_DESCRIPTION_LENGTH).replace(/\b(Bearer|Basic)\s+[A-Za-z0-9._~+/=-]+/gi, "$1 [REDACTED]").replace(/\b(api[_-]?key|access[_-]?token|refresh[_-]?token|secret|password)\s*[=:]\s*[^\s,;]+/gi, "$1=[REDACTED]").replace(/\balfe_[A-Za-z0-9_-]{8,}/g, "[REDACTED]");
|
|
136
|
+
for (const secret of configSecrets(config)) {
|
|
137
|
+
if (secret.length < 4) continue;
|
|
138
|
+
output = output.split(secret).join("[REDACTED]");
|
|
139
|
+
}
|
|
140
|
+
return output;
|
|
141
|
+
}
|
|
142
|
+
function validateStdioConfig(config, label) {
|
|
143
|
+
const command = requireBoundedString(config.command, `${label}.command`, 4096);
|
|
144
|
+
rejectControlCharacters(command, `${label}.command`);
|
|
145
|
+
const result = { command };
|
|
146
|
+
if (config.args !== void 0) {
|
|
147
|
+
if (!Array.isArray(config.args) || config.args.length > 256) throw new Error(`${label}.args must be an array with at most 256 entries`);
|
|
148
|
+
result.args = config.args.map((arg, index) => {
|
|
149
|
+
const checked = requireBoundedString(arg, `${label}.args[${index.toString()}]`, 32 * 1024);
|
|
150
|
+
rejectNul(checked, `${label}.args[${index.toString()}]`);
|
|
151
|
+
return checked;
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
if (config.env !== void 0) result.env = validateStringMap(config.env, `${label}.env`, ENV_NAME, 256, 64 * 1024);
|
|
155
|
+
if (config.cwd !== void 0) {
|
|
156
|
+
const cwd = requireBoundedString(config.cwd, `${label}.cwd`, 4096);
|
|
157
|
+
rejectNul(cwd, `${label}.cwd`);
|
|
158
|
+
if (!isAbsolute(cwd)) throw new Error(`${label}.cwd must be an absolute path`);
|
|
159
|
+
result.cwd = cwd;
|
|
160
|
+
}
|
|
161
|
+
if (config.connectionTimeoutMs !== void 0) result.connectionTimeoutMs = validateTimeout(config.connectionTimeoutMs, `${label}.connectionTimeoutMs`);
|
|
162
|
+
return result;
|
|
163
|
+
}
|
|
164
|
+
function validateRemoteConfig(config, label) {
|
|
165
|
+
const rawUrl = requireBoundedString(config.url, `${label}.url`, 4096);
|
|
166
|
+
let parsed;
|
|
167
|
+
try {
|
|
168
|
+
parsed = new URL(rawUrl);
|
|
169
|
+
} catch {
|
|
170
|
+
throw new Error(`${label}.url must be an absolute URL`);
|
|
171
|
+
}
|
|
172
|
+
if (!["http:", "https:"].includes(parsed.protocol)) throw new Error(`${label}.url must use http or https`);
|
|
173
|
+
if (parsed.protocol === "http:" && !isLoopbackHost(parsed.hostname)) throw new Error(`${label}.url must use https unless it targets loopback`);
|
|
174
|
+
if (parsed.username || parsed.password) throw new Error(`${label}.url must not contain credentials`);
|
|
175
|
+
if (parsed.hash) throw new Error(`${label}.url must not contain a fragment`);
|
|
176
|
+
const result = { url: parsed.toString() };
|
|
177
|
+
if (config.transport !== void 0) {
|
|
178
|
+
if (config.transport !== "sse" && config.transport !== "streamable-http") throw new Error(`${label}.transport must be sse or streamable-http`);
|
|
179
|
+
result.transport = config.transport;
|
|
180
|
+
}
|
|
181
|
+
if (config.headers !== void 0) result.headers = validateStringMap(config.headers, `${label}.headers`, HEADER_NAME, 64, 32 * 1024, true);
|
|
182
|
+
if (config.connectionTimeoutMs !== void 0) result.connectionTimeoutMs = validateTimeout(config.connectionTimeoutMs, `${label}.connectionTimeoutMs`);
|
|
183
|
+
return result;
|
|
184
|
+
}
|
|
185
|
+
function validateStringMap(value, label, keyPattern, maxEntries, maxValueLength, rejectControls = false) {
|
|
186
|
+
const record = requireRecord(value, label);
|
|
187
|
+
const entries = Object.entries(record);
|
|
188
|
+
if (entries.length > maxEntries) throw new Error(`${label} has too many entries`);
|
|
189
|
+
const result = {};
|
|
190
|
+
for (const [key, raw] of entries) {
|
|
191
|
+
if (!keyPattern.test(key) || UNSAFE_KEYS.has(key)) throw new Error(`${label} contains invalid key "${key}"`);
|
|
192
|
+
const checked = requireBoundedString(raw, `${label}.${key}`, maxValueLength);
|
|
193
|
+
rejectNul(checked, `${label}.${key}`);
|
|
194
|
+
if (rejectControls) rejectControlCharacters(checked, `${label}.${key}`);
|
|
195
|
+
result[key] = checked;
|
|
196
|
+
}
|
|
197
|
+
return result;
|
|
198
|
+
}
|
|
199
|
+
function validateTimeout(value, label) {
|
|
200
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0 || value > 3e5) throw new Error(`${label} must be an integer from 0 to 300000`);
|
|
201
|
+
return value;
|
|
202
|
+
}
|
|
203
|
+
function cloneBoundedJson(value, label, maxBytes) {
|
|
204
|
+
let nodes = 0;
|
|
205
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
206
|
+
const visit = (item, depth) => {
|
|
207
|
+
nodes += 1;
|
|
208
|
+
if (nodes > MAX_JSON_NODES) throw new Error(`${label} exceeds the JSON node limit`);
|
|
209
|
+
if (depth > MAX_JSON_DEPTH) throw new Error(`${label} exceeds the JSON depth limit`);
|
|
210
|
+
if (item === null || typeof item === "boolean" || typeof item === "number") {
|
|
211
|
+
if (typeof item === "number" && !Number.isFinite(item)) throw new Error(`${label} contains a non-finite number`);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if (typeof item === "string") {
|
|
215
|
+
if (Buffer.byteLength(item, "utf8") > maxBytes) throw new Error(`${label} contains an oversized string`);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (typeof item !== "object") throw new Error(`${label} contains a non-JSON value`);
|
|
219
|
+
if (seen.has(item)) throw new Error(`${label} contains a cycle`);
|
|
220
|
+
seen.add(item);
|
|
221
|
+
if (!Array.isArray(item) && Object.getPrototypeOf(item) !== Object.prototype && Object.getPrototypeOf(item) !== null) throw new Error(`${label} contains a non-plain object`);
|
|
222
|
+
for (const [key, child] of Object.entries(item)) {
|
|
223
|
+
if (UNSAFE_KEYS.has(key)) throw new Error(`${label} contains unsafe key "${key}"`);
|
|
224
|
+
visit(child, depth + 1);
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
visit(value, 0);
|
|
228
|
+
const serialized = JSON.stringify(value);
|
|
229
|
+
if (Buffer.byteLength(serialized, "utf8") > maxBytes) throw new Error(`${label} exceeds the byte limit`);
|
|
230
|
+
return JSON.parse(serialized);
|
|
231
|
+
}
|
|
232
|
+
function requireRecord(value, label) {
|
|
233
|
+
if (!isRecord$1(value)) throw new Error(`${label} must be an object`);
|
|
234
|
+
for (const key of Object.keys(value)) if (UNSAFE_KEYS.has(key)) throw new Error(`${label} contains unsafe key "${key}"`);
|
|
235
|
+
return value;
|
|
236
|
+
}
|
|
237
|
+
function isRecord$1(value) {
|
|
238
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
239
|
+
const prototype = Object.getPrototypeOf(value);
|
|
240
|
+
return prototype === Object.prototype || prototype === null;
|
|
241
|
+
}
|
|
242
|
+
function requireBoundedString(value, label, maxLength) {
|
|
243
|
+
if (typeof value !== "string" || value.length === 0 || value.length > maxLength) throw new Error(`${label} must be a non-empty string of at most ${maxLength.toString()} characters`);
|
|
244
|
+
return value;
|
|
245
|
+
}
|
|
246
|
+
function rejectNul(value, label) {
|
|
247
|
+
if (value.includes("\0")) throw new Error(`${label} must not contain NUL`);
|
|
248
|
+
}
|
|
249
|
+
function rejectControlCharacters(value, label) {
|
|
250
|
+
for (const char of value) {
|
|
251
|
+
const code = char.charCodeAt(0);
|
|
252
|
+
if (code <= 31 || code === 127) throw new Error(`${label} must not contain control characters`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
function configSecrets(config) {
|
|
256
|
+
if (!config) return [];
|
|
257
|
+
return "command" in config ? Object.values(config.env ?? {}) : [...Object.values(config.headers ?? {}), new URL(config.url).password].filter(Boolean);
|
|
258
|
+
}
|
|
259
|
+
function isLoopbackHost(hostname) {
|
|
260
|
+
return hostname === "localhost" || hostname === "[::1]" || /^127(?:\.[0-9]{1,3}){3}$/.test(hostname);
|
|
42
261
|
}
|
|
43
262
|
//#endregion
|
|
44
263
|
//#region src/connection.ts
|
|
264
|
+
const MAX_STDERR_LINE_LENGTH = 16 * 1024;
|
|
45
265
|
/** Env keys OpenClaw rejects from stdio MCP env blocks. Filter them out before spawning. */
|
|
46
266
|
const STDIO_ENV_DENYLIST = new Set([
|
|
47
267
|
"NODE_OPTIONS",
|
|
@@ -93,8 +313,8 @@ var Connection = class Connection {
|
|
|
93
313
|
/** Whether any connect has ever been attempted — drives the eager retry sweep. */
|
|
94
314
|
connectAttempted = false;
|
|
95
315
|
constructor(params) {
|
|
96
|
-
this.name = params.name;
|
|
97
|
-
this.config = params.config;
|
|
316
|
+
this.name = validateServerId(params.name);
|
|
317
|
+
this.config = validateServerConfig(params.config);
|
|
98
318
|
this.deps = params.deps;
|
|
99
319
|
this.logger = params.logger;
|
|
100
320
|
this.onUnexpectedClose = params.onUnexpectedClose;
|
|
@@ -103,7 +323,7 @@ var Connection = class Connection {
|
|
|
103
323
|
}
|
|
104
324
|
/** Returns the most recent known tool list. May be empty if the server hasn't connected yet. */
|
|
105
325
|
snapshotTools() {
|
|
106
|
-
return this.tools;
|
|
326
|
+
return this.tools.map(cloneToolDescriptor);
|
|
107
327
|
}
|
|
108
328
|
/** Whether an MCP child process / remote connection has been established. */
|
|
109
329
|
isConnected() {
|
|
@@ -176,14 +396,7 @@ var Connection = class Connection {
|
|
|
176
396
|
client.onClose?.(() => {
|
|
177
397
|
this.handleUnexpectedClose(client);
|
|
178
398
|
});
|
|
179
|
-
this.tools = advertised
|
|
180
|
-
prefixed: buildNamespacedToolName(this.name, t.name),
|
|
181
|
-
server: this.name,
|
|
182
|
-
original: t.name,
|
|
183
|
-
label: (t.description ?? t.name).slice(0, 80),
|
|
184
|
-
description: t.description ?? "",
|
|
185
|
-
parameters: t.inputSchema
|
|
186
|
-
}));
|
|
399
|
+
this.tools = normalizeChildCatalog(this.name, advertised);
|
|
187
400
|
this.lastUsedAt = Date.now();
|
|
188
401
|
this.consecutiveFailures = 0;
|
|
189
402
|
this.lastError = void 0;
|
|
@@ -192,7 +405,7 @@ var Connection = class Connection {
|
|
|
192
405
|
} catch (err) {
|
|
193
406
|
attempt.then(({ client }) => client.close()).catch(() => void 0);
|
|
194
407
|
this.consecutiveFailures += 1;
|
|
195
|
-
this.lastError = err instanceof Error ? err.message : String(err);
|
|
408
|
+
this.lastError = redactSensitiveText(err instanceof Error ? err.message : String(err), this.config);
|
|
196
409
|
const backoff = Math.min(Connection.RECONNECT_BACKOFF_BASE_MS * 2 ** (this.consecutiveFailures - 1), Connection.RECONNECT_BACKOFF_MAX_MS);
|
|
197
410
|
this.reconnectBlockedUntilMs = Date.now() + backoff;
|
|
198
411
|
throw err;
|
|
@@ -225,6 +438,7 @@ var Connection = class Connection {
|
|
|
225
438
|
this.logger?.warn(`[mcp-bundler] server "${this.name}" connection closed unexpectedly; will re-spawn on next use`);
|
|
226
439
|
this.client = void 0;
|
|
227
440
|
this.tools = [];
|
|
441
|
+
handle.close().catch(() => void 0);
|
|
228
442
|
try {
|
|
229
443
|
this.onUnexpectedClose?.();
|
|
230
444
|
} catch {}
|
|
@@ -242,14 +456,10 @@ var Connection = class Connection {
|
|
|
242
456
|
}
|
|
243
457
|
this.refreshInFlight = true;
|
|
244
458
|
try {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
label: (t.description ?? t.name).slice(0, 80),
|
|
250
|
-
description: t.description ?? "",
|
|
251
|
-
parameters: t.inputSchema
|
|
252
|
-
}));
|
|
459
|
+
const handle = this.client;
|
|
460
|
+
const advertised = await handle.listTools();
|
|
461
|
+
if (this.client !== handle) return;
|
|
462
|
+
this.tools = normalizeChildCatalog(this.name, advertised);
|
|
253
463
|
this.logger?.debug(`[mcp-bundler] server "${this.name}" refreshed, ${this.tools.length.toString()} tool(s)`);
|
|
254
464
|
} finally {
|
|
255
465
|
this.refreshInFlight = false;
|
|
@@ -265,7 +475,8 @@ var Connection = class Connection {
|
|
|
265
475
|
await this.ensureConnected();
|
|
266
476
|
if (!this.client) throw new Error(`server "${this.name}" failed to connect`);
|
|
267
477
|
this.lastUsedAt = Date.now();
|
|
268
|
-
|
|
478
|
+
const checkedArgs = validateToolArguments(args);
|
|
479
|
+
return normalizeToolResult(await this.client.callTool(originalName, checkedArgs, signal ? { signal } : void 0));
|
|
269
480
|
}
|
|
270
481
|
/**
|
|
271
482
|
* Close the underlying transport. Idempotent. If a connect is in flight
|
|
@@ -314,16 +525,26 @@ async function defaultConnect(server, ctx) {
|
|
|
314
525
|
});
|
|
315
526
|
if (onStderrLine) {
|
|
316
527
|
let carry = "";
|
|
528
|
+
const emitLine = (raw) => {
|
|
529
|
+
if (raw.trim() === "") return;
|
|
530
|
+
const line = raw.length > MAX_STDERR_LINE_LENGTH ? `${raw.slice(0, MAX_STDERR_LINE_LENGTH)}…[truncated]` : raw;
|
|
531
|
+
try {
|
|
532
|
+
onStderrLine(line);
|
|
533
|
+
} catch {}
|
|
534
|
+
};
|
|
317
535
|
transport.stderr?.on("data", (chunk) => {
|
|
318
536
|
const parts = (carry + chunk.toString()).split("\n");
|
|
319
537
|
carry = parts.pop() ?? "";
|
|
320
|
-
for (const line of parts)
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
} catch {}
|
|
538
|
+
for (const line of parts) emitLine(line);
|
|
539
|
+
if (carry.length > MAX_STDERR_LINE_LENGTH) {
|
|
540
|
+
emitLine(carry);
|
|
541
|
+
carry = "";
|
|
325
542
|
}
|
|
326
543
|
});
|
|
544
|
+
transport.stderr?.on("end", () => {
|
|
545
|
+
emitLine(carry);
|
|
546
|
+
carry = "";
|
|
547
|
+
});
|
|
327
548
|
}
|
|
328
549
|
await client.connect(transport);
|
|
329
550
|
} else {
|
|
@@ -346,7 +567,7 @@ async function defaultConnect(server, ctx) {
|
|
|
346
567
|
closeHandler?.();
|
|
347
568
|
};
|
|
348
569
|
client.onclose = fireClose;
|
|
349
|
-
client.onerror =
|
|
570
|
+
client.onerror = () => void 0;
|
|
350
571
|
return {
|
|
351
572
|
async listTools() {
|
|
352
573
|
return (await client.listTools()).tools.map((t) => ({
|
|
@@ -409,10 +630,10 @@ var McpBundler = class {
|
|
|
409
630
|
reconcileLatch = Promise.resolve();
|
|
410
631
|
constructor(opts = {}, deps) {
|
|
411
632
|
this.logger = opts.logger;
|
|
412
|
-
this.idleTtlMs = opts.idleTtlMs ?? DEFAULT_IDLE_TTL_MS;
|
|
413
|
-
this.idleSweepIntervalMs = opts.idleSweepIntervalMs ?? DEFAULT_IDLE_SWEEP_INTERVAL_MS;
|
|
414
|
-
this.retrySweepIntervalMs = opts.retrySweepIntervalMs ?? DEFAULT_RETRY_SWEEP_INTERVAL_MS;
|
|
415
|
-
this.connectTimeoutMs = opts.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS;
|
|
633
|
+
this.idleTtlMs = checkedDuration(opts.idleTtlMs ?? DEFAULT_IDLE_TTL_MS, "idleTtlMs");
|
|
634
|
+
this.idleSweepIntervalMs = checkedDuration(opts.idleSweepIntervalMs ?? DEFAULT_IDLE_SWEEP_INTERVAL_MS, "idleSweepIntervalMs");
|
|
635
|
+
this.retrySweepIntervalMs = checkedDuration(opts.retrySweepIntervalMs ?? DEFAULT_RETRY_SWEEP_INTERVAL_MS, "retrySweepIntervalMs");
|
|
636
|
+
this.connectTimeoutMs = checkedDuration(opts.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS, "connectTimeoutMs", 3e5);
|
|
416
637
|
this.retryNeverConnected = opts.retryNeverConnected ?? false;
|
|
417
638
|
this.deps = deps ?? { connect: defaultConnect };
|
|
418
639
|
this.onToolError = opts.onToolError;
|
|
@@ -435,7 +656,7 @@ var McpBundler = class {
|
|
|
435
656
|
crash(name);
|
|
436
657
|
} } : {},
|
|
437
658
|
...stderr ? { onStderrLine: (line) => {
|
|
438
|
-
stderr(name, line);
|
|
659
|
+
stderr(name, redactSensitiveText(line, config));
|
|
439
660
|
} } : {}
|
|
440
661
|
});
|
|
441
662
|
}
|
|
@@ -462,7 +683,11 @@ var McpBundler = class {
|
|
|
462
683
|
}
|
|
463
684
|
async doReconcile(desired) {
|
|
464
685
|
if (this.disposed) throw new Error("McpBundler: disposed");
|
|
465
|
-
const
|
|
686
|
+
const desiredEntries = Object.entries(desired);
|
|
687
|
+
validateServerCount(desiredEntries.length);
|
|
688
|
+
const checkedDesired = {};
|
|
689
|
+
for (const [name, config] of desiredEntries) checkedDesired[validateServerId(name)] = validateServerConfig(config, `server "${name}" config`);
|
|
690
|
+
const desiredNames = new Set(Object.keys(checkedDesired));
|
|
466
691
|
const currentNames = new Set(this.connections.keys());
|
|
467
692
|
const added = [];
|
|
468
693
|
const removed = [];
|
|
@@ -474,7 +699,7 @@ var McpBundler = class {
|
|
|
474
699
|
if (conn) await conn.close();
|
|
475
700
|
removed.push(name);
|
|
476
701
|
}
|
|
477
|
-
for (const [name, config] of Object.entries(
|
|
702
|
+
for (const [name, config] of Object.entries(checkedDesired)) {
|
|
478
703
|
const existing = this.connections.get(name);
|
|
479
704
|
if (!existing) {
|
|
480
705
|
this.connections.set(name, this.buildConnection(name, config));
|
|
@@ -510,17 +735,7 @@ var McpBundler = class {
|
|
|
510
735
|
* so cross-server name clashes never produce duplicate registrations.
|
|
511
736
|
*/
|
|
512
737
|
listTools() {
|
|
513
|
-
|
|
514
|
-
const out = [];
|
|
515
|
-
for (const conn of this.connections.values()) for (const tool of conn.snapshotTools()) {
|
|
516
|
-
const finalName = disambiguateAgainst(tool.prefixed, seen);
|
|
517
|
-
seen.add(finalName);
|
|
518
|
-
out.push(finalName === tool.prefixed ? tool : {
|
|
519
|
-
...tool,
|
|
520
|
-
prefixed: finalName
|
|
521
|
-
});
|
|
522
|
-
}
|
|
523
|
-
return out;
|
|
738
|
+
return this.resolvedCatalog().map(({ descriptor }) => cloneToolDescriptor(descriptor));
|
|
524
739
|
}
|
|
525
740
|
/**
|
|
526
741
|
* Eagerly connect to every configured server and discover tools. Used by
|
|
@@ -602,10 +817,11 @@ var McpBundler = class {
|
|
|
602
817
|
if (this.disposed) return void 0;
|
|
603
818
|
const conn = this.connections.get(name);
|
|
604
819
|
if (!conn) return void 0;
|
|
820
|
+
const checkedTimeout = timeoutMs === void 0 ? void 0 : checkedDuration(timeoutMs, "warmServer timeoutMs", 3e5);
|
|
605
821
|
const connect = conn.ensureConnected();
|
|
606
822
|
connect.catch(() => void 0);
|
|
607
823
|
try {
|
|
608
|
-
if (
|
|
824
|
+
if (checkedTimeout !== void 0 && checkedTimeout > 0) await this.raceTimeout(connect, checkedTimeout, name);
|
|
609
825
|
else await connect;
|
|
610
826
|
} catch (err) {
|
|
611
827
|
const status = this.statusOf(conn);
|
|
@@ -647,7 +863,7 @@ var McpBundler = class {
|
|
|
647
863
|
isError: true,
|
|
648
864
|
content: [{
|
|
649
865
|
type: "text",
|
|
650
|
-
text: `unknown tool: ${prefixed}`
|
|
866
|
+
text: `unknown MCP tool: ${prefixed.slice(0, 128)}`
|
|
651
867
|
}]
|
|
652
868
|
};
|
|
653
869
|
try {
|
|
@@ -657,11 +873,11 @@ var McpBundler = class {
|
|
|
657
873
|
tool: route.original,
|
|
658
874
|
prefixed,
|
|
659
875
|
kind: "result-error",
|
|
660
|
-
message: extractErrorText(result)
|
|
876
|
+
message: redactSensitiveText(extractErrorText(result), route.connection.config)
|
|
661
877
|
});
|
|
662
|
-
return result;
|
|
878
|
+
return redactErrorResult(result, route.connection.config);
|
|
663
879
|
} catch (err) {
|
|
664
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
880
|
+
const msg = redactSensitiveText(err instanceof Error ? err.message : String(err), route.connection.config);
|
|
665
881
|
this.logger?.error(`[mcp-bundler] tool call failed for "${prefixed}"`, { err: msg });
|
|
666
882
|
this.reportToolError({
|
|
667
883
|
server: route.connection.name,
|
|
@@ -684,10 +900,32 @@ var McpBundler = class {
|
|
|
684
900
|
* tool name. Returns undefined if the tool is not currently advertised.
|
|
685
901
|
*/
|
|
686
902
|
routeToolName(prefixed) {
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
903
|
+
const match = this.resolvedCatalog().find(({ descriptor }) => descriptor.prefixed === prefixed);
|
|
904
|
+
return match ? {
|
|
905
|
+
connection: match.connection,
|
|
906
|
+
original: match.descriptor.original
|
|
907
|
+
} : void 0;
|
|
908
|
+
}
|
|
909
|
+
/**
|
|
910
|
+
* Build one collision-resolved catalog used by BOTH registration and
|
|
911
|
+
* routing. Keeping these views together prevents an advertised `-2` name
|
|
912
|
+
* from becoming uncallable when two child names sanitize to the same key.
|
|
913
|
+
*/
|
|
914
|
+
resolvedCatalog() {
|
|
915
|
+
const seen = /* @__PURE__ */ new Set();
|
|
916
|
+
const resolved = [];
|
|
917
|
+
for (const conn of this.connections.values()) for (const tool of conn.snapshotTools()) {
|
|
918
|
+
const finalName = disambiguateAgainst(tool.prefixed, seen);
|
|
919
|
+
seen.add(finalName);
|
|
920
|
+
resolved.push({
|
|
921
|
+
connection: conn,
|
|
922
|
+
descriptor: finalName === tool.prefixed ? tool : {
|
|
923
|
+
...tool,
|
|
924
|
+
prefixed: finalName
|
|
925
|
+
}
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
return resolved;
|
|
691
929
|
}
|
|
692
930
|
/**
|
|
693
931
|
* Tear down all connections and stop background tasks. Idempotent.
|
|
@@ -746,6 +984,10 @@ var McpBundler = class {
|
|
|
746
984
|
await Promise.allSettled(targets.map((c) => c.close()));
|
|
747
985
|
}
|
|
748
986
|
};
|
|
987
|
+
function checkedDuration(value, label, max = 864e5) {
|
|
988
|
+
if (!Number.isSafeInteger(value) || value < 0 || value > max) throw new Error(`McpBundler ${label} must be an integer from 0 to ${max.toString()}`);
|
|
989
|
+
return value;
|
|
990
|
+
}
|
|
749
991
|
//#endregion
|
|
750
992
|
//#region src/store.ts
|
|
751
993
|
const DEFAULT_STORE_PATH = join(join(homedir(), ".alfe", "mcp"), "servers.json");
|
|
@@ -753,6 +995,7 @@ const DEFAULT_STORE_PATH = join(join(homedir(), ".alfe", "mcp"), "servers.json")
|
|
|
753
995
|
const LOCK_WAIT_MS = 5e3;
|
|
754
996
|
const LOCK_RETRY_INTERVAL_MS = 25;
|
|
755
997
|
const LOCK_STALE_MS = 1e4;
|
|
998
|
+
const MAX_STORE_BYTES = 1024 * 1024;
|
|
756
999
|
/**
|
|
757
1000
|
* On-disk source of truth for the bundler's configured servers.
|
|
758
1001
|
*
|
|
@@ -771,22 +1014,36 @@ var Store = class {
|
|
|
771
1014
|
rewatchTimer;
|
|
772
1015
|
constructor(opts = {}) {
|
|
773
1016
|
this.storePath = opts.path ?? DEFAULT_STORE_PATH;
|
|
1017
|
+
if (!isAbsolute(this.storePath)) throw new Error("Store path must be absolute");
|
|
774
1018
|
this.logger = opts.logger;
|
|
775
1019
|
}
|
|
776
1020
|
get path() {
|
|
777
1021
|
return this.storePath;
|
|
778
1022
|
}
|
|
779
1023
|
read() {
|
|
780
|
-
|
|
1024
|
+
let fd = -1;
|
|
781
1025
|
try {
|
|
782
|
-
|
|
1026
|
+
try {
|
|
1027
|
+
fd = openSync(this.storePath, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
1028
|
+
} catch (err) {
|
|
1029
|
+
if (err.code === "ENOENT") return cloneEmpty();
|
|
1030
|
+
throw err;
|
|
1031
|
+
}
|
|
1032
|
+
const stat = fstatSync(fd);
|
|
1033
|
+
if (!stat.isFile()) throw new Error("store path is not a regular file");
|
|
1034
|
+
if (typeof process.getuid === "function" && stat.uid !== process.getuid()) throw new Error("store file is not owned by the current user");
|
|
1035
|
+
if ((stat.mode & 63) !== 0) fchmodSync(fd, 384);
|
|
1036
|
+
if (stat.size > MAX_STORE_BYTES) throw new Error(`store exceeds the ${MAX_STORE_BYTES.toString()} byte limit`);
|
|
1037
|
+
const raw = readFileSync(fd, "utf8");
|
|
783
1038
|
return normalize(JSON.parse(raw));
|
|
784
1039
|
} catch (err) {
|
|
785
|
-
this.logger?.warn("[mcp-bundler/store]
|
|
1040
|
+
this.logger?.warn("[mcp-bundler/store] rejected unreadable or invalid store", {
|
|
786
1041
|
err: errMsg$1(err),
|
|
787
1042
|
path: this.storePath
|
|
788
1043
|
});
|
|
789
|
-
|
|
1044
|
+
throw new Error(`Store.read: rejected ${this.storePath}: ${errMsg$1(err)}`, { cause: err });
|
|
1045
|
+
} finally {
|
|
1046
|
+
if (fd >= 0) closeSync(fd);
|
|
790
1047
|
}
|
|
791
1048
|
}
|
|
792
1049
|
/**
|
|
@@ -799,8 +1056,8 @@ var Store = class {
|
|
|
799
1056
|
* The lock guards the read-then-rename window so two processes
|
|
800
1057
|
* (e.g. two `alfe mcp add` shells, or the CLI racing the daemon)
|
|
801
1058
|
* can't drop each other's writes. The lock file is at
|
|
802
|
-
* `<storePath>.lock`;
|
|
803
|
-
*
|
|
1059
|
+
* `<storePath>.lock`; locks whose recorded writer process is no longer
|
|
1060
|
+
* alive are reclaimed so a crashed writer doesn't wedge the store.
|
|
804
1061
|
*
|
|
805
1062
|
* Pure-function shape (instead of a `read()` then `write(next)`
|
|
806
1063
|
* pair) intentionally — it keeps the read-modify-write contract
|
|
@@ -808,18 +1065,27 @@ var Store = class {
|
|
|
808
1065
|
* other's partial state.
|
|
809
1066
|
*/
|
|
810
1067
|
update(fn) {
|
|
811
|
-
mkdirSync(dirname(this.storePath), {
|
|
1068
|
+
mkdirSync(dirname(this.storePath), {
|
|
1069
|
+
recursive: true,
|
|
1070
|
+
mode: 448
|
|
1071
|
+
});
|
|
812
1072
|
const release = this.acquireLock();
|
|
813
1073
|
try {
|
|
814
|
-
const next = fn(this.read());
|
|
815
|
-
const
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
});
|
|
1074
|
+
const next = normalize(fn(this.read()));
|
|
1075
|
+
const payload = JSON.stringify(next, null, 2);
|
|
1076
|
+
if (Buffer.byteLength(payload, "utf8") > MAX_STORE_BYTES) throw new Error(`Store.update: result exceeds the ${MAX_STORE_BYTES.toString()} byte limit`);
|
|
1077
|
+
const tempPath = `${this.storePath}.${String(process.pid)}.${randomUUID()}.tmp`;
|
|
1078
|
+
let tempFd = -1;
|
|
820
1079
|
try {
|
|
1080
|
+
tempFd = openSync(tempPath, "wx", 384);
|
|
1081
|
+
writeFileSync(tempFd, payload, { encoding: "utf8" });
|
|
1082
|
+
fsyncSync(tempFd);
|
|
1083
|
+
closeSync(tempFd);
|
|
1084
|
+
tempFd = -1;
|
|
821
1085
|
renameSync(tempPath, this.storePath);
|
|
1086
|
+
this.fsyncParentDirectory();
|
|
822
1087
|
} catch (err) {
|
|
1088
|
+
if (tempFd >= 0) closeSync(tempFd);
|
|
823
1089
|
try {
|
|
824
1090
|
unlinkSync(tempPath);
|
|
825
1091
|
} catch {}
|
|
@@ -832,11 +1098,11 @@ var Store = class {
|
|
|
832
1098
|
}
|
|
833
1099
|
/**
|
|
834
1100
|
* Acquire an inter-process file lock by atomically creating a
|
|
835
|
-
* sentinel via `openSync(lockPath, 'wx')`.
|
|
836
|
-
* backoff up to `LOCK_WAIT_MS`.
|
|
837
|
-
*
|
|
838
|
-
*
|
|
839
|
-
*
|
|
1101
|
+
* sentinel via `openSync(lockPath, 'wx')`. Waits with bounded
|
|
1102
|
+
* backoff up to `LOCK_WAIT_MS`. A structured lock is reclaimed only when
|
|
1103
|
+
* its writer process is no longer alive; age alone cannot steal a lock
|
|
1104
|
+
* from a slow but valid writer. Old malformed/legacy locks retain a
|
|
1105
|
+
* conservative age fallback.
|
|
840
1106
|
*
|
|
841
1107
|
* Returns the release function. Single-process callers are
|
|
842
1108
|
* unaffected — re-entering the same process spins briefly while
|
|
@@ -846,20 +1112,33 @@ var Store = class {
|
|
|
846
1112
|
const lockPath = `${this.storePath}.lock`;
|
|
847
1113
|
const deadline = Date.now() + LOCK_WAIT_MS;
|
|
848
1114
|
let fd = -1;
|
|
1115
|
+
let heldDevice = -1;
|
|
1116
|
+
let heldInode = -1;
|
|
849
1117
|
for (;;) try {
|
|
850
1118
|
fd = openSync(lockPath, "wx", 384);
|
|
1119
|
+
const token = randomUUID();
|
|
1120
|
+
writeFileSync(fd, JSON.stringify({
|
|
1121
|
+
pid: process.pid,
|
|
1122
|
+
token,
|
|
1123
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1124
|
+
}), "utf8");
|
|
1125
|
+
fsyncSync(fd);
|
|
1126
|
+
const stat = fstatSync(fd);
|
|
1127
|
+
heldDevice = stat.dev;
|
|
1128
|
+
heldInode = stat.ino;
|
|
851
1129
|
break;
|
|
852
1130
|
} catch (err) {
|
|
853
1131
|
if (err.code !== "EEXIST") throw err;
|
|
854
|
-
|
|
1132
|
+
const stale = this.inspectStaleLock(lockPath);
|
|
1133
|
+
if (stale) {
|
|
855
1134
|
try {
|
|
856
|
-
|
|
1135
|
+
const current = lstatSync(lockPath);
|
|
1136
|
+
if (current.dev === stale.device && current.ino === stale.inode) unlinkSync(lockPath);
|
|
857
1137
|
} catch {}
|
|
858
1138
|
continue;
|
|
859
1139
|
}
|
|
860
1140
|
if (Date.now() >= deadline) throw new Error(`Store.update: timed out waiting for ${lockPath} (held by another writer or stale lock)`);
|
|
861
|
-
|
|
862
|
-
while (Date.now() < sleepUntil);
|
|
1141
|
+
sleepSync(LOCK_RETRY_INTERVAL_MS);
|
|
863
1142
|
}
|
|
864
1143
|
const held = fd;
|
|
865
1144
|
return () => {
|
|
@@ -867,16 +1146,56 @@ var Store = class {
|
|
|
867
1146
|
closeSync(held);
|
|
868
1147
|
} catch {}
|
|
869
1148
|
try {
|
|
870
|
-
|
|
1149
|
+
const current = lstatSync(lockPath);
|
|
1150
|
+
if (current.dev === heldDevice && current.ino === heldInode) unlinkSync(lockPath);
|
|
871
1151
|
} catch {}
|
|
872
1152
|
};
|
|
873
1153
|
}
|
|
874
|
-
|
|
1154
|
+
inspectStaleLock(lockPath) {
|
|
1155
|
+
let fd = -1;
|
|
1156
|
+
let modifiedAt;
|
|
1157
|
+
let device;
|
|
1158
|
+
let inode;
|
|
875
1159
|
try {
|
|
876
|
-
|
|
877
|
-
|
|
1160
|
+
fd = openSync(lockPath, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
1161
|
+
const stat = fstatSync(fd);
|
|
1162
|
+
modifiedAt = stat.mtimeMs;
|
|
1163
|
+
device = stat.dev;
|
|
1164
|
+
inode = stat.ino;
|
|
1165
|
+
if (!stat.isFile() || stat.size > 4096) return Date.now() - stat.mtimeMs > LOCK_STALE_MS ? {
|
|
1166
|
+
device,
|
|
1167
|
+
inode
|
|
1168
|
+
} : void 0;
|
|
1169
|
+
const parsed = JSON.parse(readFileSync(fd, "utf8"));
|
|
1170
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return Date.now() - stat.mtimeMs > LOCK_STALE_MS ? {
|
|
1171
|
+
device,
|
|
1172
|
+
inode
|
|
1173
|
+
} : void 0;
|
|
1174
|
+
const pid = parsed.pid;
|
|
1175
|
+
if (!Number.isSafeInteger(pid) || pid <= 0) return Date.now() - stat.mtimeMs > LOCK_STALE_MS ? {
|
|
1176
|
+
device,
|
|
1177
|
+
inode
|
|
1178
|
+
} : void 0;
|
|
1179
|
+
return !processIsAlive(pid) ? {
|
|
1180
|
+
device,
|
|
1181
|
+
inode
|
|
1182
|
+
} : void 0;
|
|
878
1183
|
} catch {
|
|
879
|
-
return
|
|
1184
|
+
return modifiedAt !== void 0 && device !== void 0 && inode !== void 0 && Date.now() - modifiedAt > LOCK_STALE_MS ? {
|
|
1185
|
+
device,
|
|
1186
|
+
inode
|
|
1187
|
+
} : void 0;
|
|
1188
|
+
} finally {
|
|
1189
|
+
if (fd >= 0) closeSync(fd);
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
fsyncParentDirectory() {
|
|
1193
|
+
let fd = -1;
|
|
1194
|
+
try {
|
|
1195
|
+
fd = openSync(dirname(this.storePath), constants.O_RDONLY);
|
|
1196
|
+
fsyncSync(fd);
|
|
1197
|
+
} catch {} finally {
|
|
1198
|
+
if (fd >= 0) closeSync(fd);
|
|
880
1199
|
}
|
|
881
1200
|
}
|
|
882
1201
|
/**
|
|
@@ -900,7 +1219,10 @@ var Store = class {
|
|
|
900
1219
|
}
|
|
901
1220
|
ensureWatcher() {
|
|
902
1221
|
if (this.watcher) return;
|
|
903
|
-
mkdirSync(dirname(this.storePath), {
|
|
1222
|
+
mkdirSync(dirname(this.storePath), {
|
|
1223
|
+
recursive: true,
|
|
1224
|
+
mode: 448
|
|
1225
|
+
});
|
|
904
1226
|
const dir = dirname(this.storePath);
|
|
905
1227
|
const basename = this.storePath.slice(dir.length + 1);
|
|
906
1228
|
let pending;
|
|
@@ -949,43 +1271,60 @@ var Store = class {
|
|
|
949
1271
|
function defaultStorePath() {
|
|
950
1272
|
return DEFAULT_STORE_PATH;
|
|
951
1273
|
}
|
|
1274
|
+
/**
|
|
1275
|
+
* One-way launch-identity fingerprint for cache invalidation across IPC.
|
|
1276
|
+
* Store entries contain credentials in env/headers, so consumers that only
|
|
1277
|
+
* need equality receive this digest rather than the executable configuration.
|
|
1278
|
+
*/
|
|
1279
|
+
function serverLaunchFingerprint(entry) {
|
|
1280
|
+
const identity = {
|
|
1281
|
+
config: toServerConfig(entry),
|
|
1282
|
+
version: entry.version ?? null
|
|
1283
|
+
};
|
|
1284
|
+
return `sha256:${createHash("sha256").update(stableJson(identity)).digest("hex")}`;
|
|
1285
|
+
}
|
|
952
1286
|
/** Pull the runtime config (transport + transport-specific fields) out of a stored entry. */
|
|
953
1287
|
function toServerConfig(entry) {
|
|
954
1288
|
if (entry.transport === "stdio") {
|
|
955
|
-
const { command, args, env, cwd } = entry;
|
|
1289
|
+
const { command, args, env, cwd, connectionTimeoutMs } = entry;
|
|
956
1290
|
const cfg = { command };
|
|
957
|
-
if (args) cfg.args = args;
|
|
958
|
-
if (env) cfg.env = env;
|
|
1291
|
+
if (args) cfg.args = [...args];
|
|
1292
|
+
if (env) cfg.env = { ...env };
|
|
959
1293
|
if (cwd) cfg.cwd = cwd;
|
|
960
|
-
|
|
1294
|
+
if (connectionTimeoutMs !== void 0) cfg.connectionTimeoutMs = connectionTimeoutMs;
|
|
1295
|
+
return validateServerConfig(cfg);
|
|
961
1296
|
}
|
|
962
1297
|
const { url, transport, headers, connectionTimeoutMs } = entry;
|
|
963
1298
|
const cfg = {
|
|
964
1299
|
url,
|
|
965
1300
|
transport
|
|
966
1301
|
};
|
|
967
|
-
if (headers) cfg.headers = headers;
|
|
1302
|
+
if (headers) cfg.headers = { ...headers };
|
|
968
1303
|
if (connectionTimeoutMs !== void 0) cfg.connectionTimeoutMs = connectionTimeoutMs;
|
|
969
|
-
return cfg;
|
|
1304
|
+
return validateServerConfig(cfg);
|
|
970
1305
|
}
|
|
971
1306
|
/** Build a stored entry from a runtime config + ownership metadata. */
|
|
972
1307
|
function toStoredEntry(config, meta) {
|
|
1308
|
+
validateOwner(meta.owner);
|
|
1309
|
+
const checked = validateServerConfig(config);
|
|
973
1310
|
const addedAt = meta.addedAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
974
|
-
|
|
1311
|
+
validateTimestamp(addedAt, "addedAt");
|
|
1312
|
+
if (meta.version !== void 0) validateVersion(meta.version);
|
|
1313
|
+
if ("command" in checked) return {
|
|
975
1314
|
transport: "stdio",
|
|
976
1315
|
owner: meta.owner,
|
|
977
1316
|
addedAt,
|
|
978
1317
|
...meta.version !== void 0 ? { version: meta.version } : {},
|
|
979
|
-
...
|
|
1318
|
+
...checked
|
|
980
1319
|
};
|
|
981
|
-
const transport = meta.transport ??
|
|
1320
|
+
const transport = meta.transport ?? checked.transport ?? "sse";
|
|
982
1321
|
if (transport === "stdio") throw new Error("toStoredEntry: transport=stdio specified but config is remote-shaped");
|
|
983
1322
|
return {
|
|
984
|
-
transport,
|
|
985
1323
|
owner: meta.owner,
|
|
986
1324
|
addedAt,
|
|
987
1325
|
...meta.version !== void 0 ? { version: meta.version } : {},
|
|
988
|
-
...
|
|
1326
|
+
...checked,
|
|
1327
|
+
transport
|
|
989
1328
|
};
|
|
990
1329
|
}
|
|
991
1330
|
function cloneEmpty() {
|
|
@@ -996,17 +1335,90 @@ function cloneEmpty() {
|
|
|
996
1335
|
};
|
|
997
1336
|
}
|
|
998
1337
|
function normalize(raw) {
|
|
999
|
-
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
1338
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new Error("store root must be an object");
|
|
1000
1339
|
const r = raw;
|
|
1340
|
+
const rawServers = r.servers;
|
|
1341
|
+
if (!rawServers || typeof rawServers !== "object" || Array.isArray(rawServers)) throw new Error("store.servers must be an object");
|
|
1342
|
+
const serverEntries = Object.entries(rawServers);
|
|
1343
|
+
validateServerCount(serverEntries.length);
|
|
1344
|
+
const servers = {};
|
|
1345
|
+
for (const [id, rawEntry] of serverEntries) {
|
|
1346
|
+
validateServerId(id);
|
|
1347
|
+
if (!rawEntry || typeof rawEntry !== "object" || Array.isArray(rawEntry)) throw new Error(`store server "${id}" must be an object`);
|
|
1348
|
+
const entry = rawEntry;
|
|
1349
|
+
validateOwner(entry.owner);
|
|
1350
|
+
validateTimestamp(entry.addedAt, `store server "${id}" addedAt`);
|
|
1351
|
+
if (entry.version !== void 0) validateVersion(entry.version);
|
|
1352
|
+
if (![
|
|
1353
|
+
"stdio",
|
|
1354
|
+
"sse",
|
|
1355
|
+
"streamable-http"
|
|
1356
|
+
].includes(String(entry.transport))) throw new Error(`store server "${id}" has an invalid transport`);
|
|
1357
|
+
servers[id] = toStoredEntry(entry.transport === "stdio" ? validateServerConfig(entry, `store server "${id}"`) : validateServerConfig({
|
|
1358
|
+
...entry,
|
|
1359
|
+
transport: entry.transport
|
|
1360
|
+
}, `store server "${id}"`), {
|
|
1361
|
+
owner: entry.owner,
|
|
1362
|
+
transport: entry.transport,
|
|
1363
|
+
addedAt: entry.addedAt,
|
|
1364
|
+
version: entry.version
|
|
1365
|
+
});
|
|
1366
|
+
}
|
|
1367
|
+
const rawConfig = r.config ?? {};
|
|
1368
|
+
if (!rawConfig || typeof rawConfig !== "object" || Array.isArray(rawConfig)) throw new Error("store.config must be an object");
|
|
1369
|
+
const config = {};
|
|
1370
|
+
const ttl = rawConfig.sessionIdleTtlMs;
|
|
1371
|
+
if (ttl !== void 0) {
|
|
1372
|
+
if (typeof ttl !== "number" || !Number.isSafeInteger(ttl) || ttl < 0 || ttl > 864e5) throw new Error("store.config.sessionIdleTtlMs must be an integer from 0 to 86400000");
|
|
1373
|
+
config.sessionIdleTtlMs = ttl;
|
|
1374
|
+
}
|
|
1375
|
+
const rawOwned = r._ownedOpenclawKeys;
|
|
1376
|
+
if (rawOwned !== void 0 && !Array.isArray(rawOwned)) throw new Error("store._ownedOpenclawKeys must be an array");
|
|
1377
|
+
const owned = [...new Set((rawOwned ?? []).map((value) => validateServerId(value, "store._ownedOpenclawKeys entry")))];
|
|
1378
|
+
validateServerCount(owned.length);
|
|
1001
1379
|
return {
|
|
1002
|
-
servers
|
|
1003
|
-
config
|
|
1004
|
-
_ownedOpenclawKeys:
|
|
1380
|
+
servers,
|
|
1381
|
+
config,
|
|
1382
|
+
_ownedOpenclawKeys: owned
|
|
1005
1383
|
};
|
|
1006
1384
|
}
|
|
1385
|
+
function validateOwner(owner) {
|
|
1386
|
+
if (owner === "cli" || owner === "manual") return;
|
|
1387
|
+
if (typeof owner === "string" && owner.startsWith("integration:")) {
|
|
1388
|
+
validateServerId(owner.slice(12), "integration owner id");
|
|
1389
|
+
return;
|
|
1390
|
+
}
|
|
1391
|
+
throw new Error("server owner must be cli, manual, or integration:<id>");
|
|
1392
|
+
}
|
|
1393
|
+
function validateTimestamp(value, label) {
|
|
1394
|
+
if (typeof value !== "string" || !Number.isFinite(Date.parse(value))) throw new Error(`${label} must be an ISO timestamp`);
|
|
1395
|
+
}
|
|
1396
|
+
function validateVersion(value) {
|
|
1397
|
+
if (typeof value !== "string" || !/^[0-9A-Za-z][0-9A-Za-z.+-]{0,127}$/.test(value)) throw new Error("server version is invalid");
|
|
1398
|
+
}
|
|
1399
|
+
function processIsAlive(pid) {
|
|
1400
|
+
try {
|
|
1401
|
+
process.kill(pid, 0);
|
|
1402
|
+
return true;
|
|
1403
|
+
} catch (err) {
|
|
1404
|
+
return err.code !== "ESRCH";
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
const sleepBuffer = new Int32Array(new SharedArrayBuffer(4));
|
|
1408
|
+
function sleepSync(ms) {
|
|
1409
|
+
Atomics.wait(sleepBuffer, 0, 0, ms);
|
|
1410
|
+
}
|
|
1007
1411
|
function errMsg$1(err) {
|
|
1008
1412
|
return err instanceof Error ? err.message : String(err);
|
|
1009
1413
|
}
|
|
1414
|
+
function stableJson(value) {
|
|
1415
|
+
if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
|
|
1416
|
+
if (value !== null && typeof value === "object") {
|
|
1417
|
+
const record = value;
|
|
1418
|
+
return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${stableJson(record[key])}`).join(",")}}`;
|
|
1419
|
+
}
|
|
1420
|
+
return value === void 0 ? "null" : JSON.stringify(value);
|
|
1421
|
+
}
|
|
1010
1422
|
//#endregion
|
|
1011
1423
|
//#region src/manager.ts
|
|
1012
1424
|
/**
|
|
@@ -1026,11 +1438,13 @@ function errMsg$1(err) {
|
|
|
1026
1438
|
*/
|
|
1027
1439
|
var Manager = class {
|
|
1028
1440
|
store;
|
|
1441
|
+
ownsStore;
|
|
1029
1442
|
logger;
|
|
1030
1443
|
bundler;
|
|
1031
1444
|
changeListeners = /* @__PURE__ */ new Set();
|
|
1032
1445
|
storeUnsubscribe;
|
|
1033
1446
|
constructor(opts = {}) {
|
|
1447
|
+
this.ownsStore = opts.store === void 0;
|
|
1034
1448
|
this.store = opts.store ?? new Store({ logger: opts.logger });
|
|
1035
1449
|
this.logger = opts.logger;
|
|
1036
1450
|
}
|
|
@@ -1041,14 +1455,14 @@ var Manager = class {
|
|
|
1041
1455
|
/**
|
|
1042
1456
|
* Register or overwrite a server entry. Mutation lands in the store
|
|
1043
1457
|
* synchronously; if a bundler has been attached via `loadIntoBundler`,
|
|
1044
|
-
* it is re-reconciled BEFORE `onChange` listeners fire
|
|
1045
|
-
*
|
|
1046
|
-
*
|
|
1458
|
+
* it is re-reconciled BEFORE `onChange` listeners fire. A reconcile error is
|
|
1459
|
+
* logged and does not roll back the durable store mutation, but listeners do
|
|
1460
|
+
* not fire against stale live state. The ordering is load-bearing: the daemon's `onChange`
|
|
1047
1461
|
* handler warms the bundler's current connections, so the just-added
|
|
1048
1462
|
* server must already have its Connection object or it is never warmed.
|
|
1049
1463
|
*/
|
|
1050
1464
|
async addServer(config, opts) {
|
|
1051
|
-
|
|
1465
|
+
validateServerId(opts.id, "Manager.addServer id");
|
|
1052
1466
|
const owner = opts.owner ?? "manual";
|
|
1053
1467
|
this.store.update((cur) => {
|
|
1054
1468
|
const previousAddedAt = lookupAddedAt(cur.servers, opts.id);
|
|
@@ -1066,8 +1480,10 @@ var Manager = class {
|
|
|
1066
1480
|
}
|
|
1067
1481
|
};
|
|
1068
1482
|
});
|
|
1069
|
-
|
|
1070
|
-
|
|
1483
|
+
if (opts.strictReconcile) {
|
|
1484
|
+
await this.reconcileBundler();
|
|
1485
|
+
this.fireChange();
|
|
1486
|
+
} else if (await this.reconcileBundlerLogged()) this.fireChange();
|
|
1071
1487
|
}
|
|
1072
1488
|
/**
|
|
1073
1489
|
* Remove a single server entry. No-op if the id isn't in the store.
|
|
@@ -1076,16 +1492,24 @@ var Manager = class {
|
|
|
1076
1492
|
* accidentally clobbering integration- or cli-owned entries.
|
|
1077
1493
|
*/
|
|
1078
1494
|
async removeServer(id, opts = {}) {
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1495
|
+
validateServerId(id, "Manager.removeServer id");
|
|
1496
|
+
const outcome = { removed: false };
|
|
1497
|
+
this.store.update((cur) => {
|
|
1498
|
+
const existing = lookupEntry(cur.servers, id);
|
|
1499
|
+
if (!existing) return cur;
|
|
1500
|
+
if (opts.expectedOwner && existing.owner !== opts.expectedOwner) throw new Error(`Manager.removeServer: server "${id}" is owned by "${existing.owner}", not "${opts.expectedOwner}"`);
|
|
1501
|
+
outcome.removed = true;
|
|
1502
|
+
return {
|
|
1503
|
+
...cur,
|
|
1504
|
+
servers: Object.fromEntries(Object.entries(cur.servers).filter(([key]) => key !== id))
|
|
1505
|
+
};
|
|
1506
|
+
});
|
|
1507
|
+
if (!outcome.removed && !opts.strictReconcile) return false;
|
|
1508
|
+
if (opts.strictReconcile) {
|
|
1509
|
+
await this.reconcileBundler();
|
|
1510
|
+
this.fireChange();
|
|
1511
|
+
} else if (await this.reconcileBundlerLogged()) this.fireChange();
|
|
1512
|
+
return outcome.removed;
|
|
1089
1513
|
}
|
|
1090
1514
|
/** Drop every entry whose owner matches — used by integration uninstall. */
|
|
1091
1515
|
async removeServersByOwner(owner) {
|
|
@@ -1101,8 +1525,7 @@ var Manager = class {
|
|
|
1101
1525
|
};
|
|
1102
1526
|
});
|
|
1103
1527
|
if (removed.length > 0) {
|
|
1104
|
-
await this.reconcileBundlerLogged();
|
|
1105
|
-
this.fireChange();
|
|
1528
|
+
if (await this.reconcileBundlerLogged()) this.fireChange();
|
|
1106
1529
|
}
|
|
1107
1530
|
return removed;
|
|
1108
1531
|
}
|
|
@@ -1135,8 +1558,14 @@ var Manager = class {
|
|
|
1135
1558
|
*/
|
|
1136
1559
|
async warmServer(id, timeoutMs) {
|
|
1137
1560
|
if (!this.bundler) return null;
|
|
1138
|
-
|
|
1139
|
-
|
|
1561
|
+
try {
|
|
1562
|
+
validateServerId(id, "Manager.warmServer id");
|
|
1563
|
+
await this.reconcileBundler();
|
|
1564
|
+
return await this.bundler.warmServer(id, timeoutMs) ?? null;
|
|
1565
|
+
} catch (err) {
|
|
1566
|
+
this.logger?.warn("[mcp-bundler/manager] warm reconcile failed", { err: errMsg(err) });
|
|
1567
|
+
return null;
|
|
1568
|
+
}
|
|
1140
1569
|
}
|
|
1141
1570
|
/**
|
|
1142
1571
|
* Push the current store contents into a bundler instance (which owns
|
|
@@ -1161,31 +1590,33 @@ var Manager = class {
|
|
|
1161
1590
|
}
|
|
1162
1591
|
/**
|
|
1163
1592
|
* Detach from the bundler and stop watching the store. Safe to call
|
|
1164
|
-
* multiple times.
|
|
1165
|
-
*
|
|
1593
|
+
* multiple times. An injected Store remains owned by its caller; a Store
|
|
1594
|
+
* constructed by this manager is disposed here.
|
|
1166
1595
|
*/
|
|
1167
1596
|
async dispose() {
|
|
1168
1597
|
if (this.storeUnsubscribe) {
|
|
1169
1598
|
this.storeUnsubscribe();
|
|
1170
1599
|
this.storeUnsubscribe = void 0;
|
|
1171
1600
|
}
|
|
1172
|
-
this.store.dispose();
|
|
1601
|
+
if (this.ownsStore) this.store.dispose();
|
|
1173
1602
|
this.changeListeners.clear();
|
|
1174
1603
|
this.bundler = void 0;
|
|
1175
1604
|
return Promise.resolve();
|
|
1176
1605
|
}
|
|
1177
1606
|
/**
|
|
1178
1607
|
* Awaited by every mutator so `onChange` listeners observe a bundler
|
|
1179
|
-
* that already contains the mutation. Reconcile failures are logged
|
|
1180
|
-
*
|
|
1181
|
-
*
|
|
1608
|
+
* that already contains the mutation. Reconcile failures are logged and
|
|
1609
|
+
* return false so callers preserve the durable mutation without notifying
|
|
1610
|
+
* listeners against stale derived state.
|
|
1182
1611
|
*/
|
|
1183
1612
|
async reconcileBundlerLogged() {
|
|
1184
|
-
if (!this.bundler) return;
|
|
1613
|
+
if (!this.bundler) return true;
|
|
1185
1614
|
try {
|
|
1186
1615
|
await this.reconcileBundler();
|
|
1616
|
+
return true;
|
|
1187
1617
|
} catch (err) {
|
|
1188
1618
|
this.logger?.warn("[mcp-bundler/manager] bundler reconcile failed", { err: errMsg(err) });
|
|
1619
|
+
return false;
|
|
1189
1620
|
}
|
|
1190
1621
|
}
|
|
1191
1622
|
async reconcileBundler() {
|
|
@@ -1233,15 +1664,17 @@ function lookupAddedAt(servers, id) {
|
|
|
1233
1664
|
*/
|
|
1234
1665
|
function checkPatternA(tools, options) {
|
|
1235
1666
|
const selectorNames = typeof options.selector === "string" ? [options.selector] : options.selector;
|
|
1667
|
+
if (selectorNames.length === 0 || selectorNames.some((name) => typeof name !== "string" || name.length === 0)) throw new Error("Pattern A selector must contain at least one non-empty property name");
|
|
1236
1668
|
const exempt = new Set(options.exempt ?? []);
|
|
1237
1669
|
const violations = [];
|
|
1238
1670
|
for (const tool of tools) {
|
|
1239
1671
|
if (exempt.has(tool.name)) continue;
|
|
1240
|
-
const schema = tool.parameters;
|
|
1241
|
-
const
|
|
1242
|
-
const
|
|
1243
|
-
const
|
|
1244
|
-
|
|
1672
|
+
const schema = isRecord(tool.parameters) ? tool.parameters : {};
|
|
1673
|
+
const rawProperties = schema.properties;
|
|
1674
|
+
const properties = isRecord(rawProperties) ? rawProperties : {};
|
|
1675
|
+
const required = Array.isArray(schema.required) ? schema.required.filter((value) => typeof value === "string") : [];
|
|
1676
|
+
const present = selectorNames.filter((name) => Object.hasOwn(properties, name));
|
|
1677
|
+
if (present.length === 0) {
|
|
1245
1678
|
violations.push({
|
|
1246
1679
|
tool: tool.name,
|
|
1247
1680
|
reason: "missing-selector-property",
|
|
@@ -1249,20 +1682,29 @@ function checkPatternA(tools, options) {
|
|
|
1249
1682
|
});
|
|
1250
1683
|
continue;
|
|
1251
1684
|
}
|
|
1252
|
-
|
|
1685
|
+
const requiredSelectors = present.filter((name) => required.includes(name));
|
|
1686
|
+
if (requiredSelectors.length === 0) {
|
|
1253
1687
|
violations.push({
|
|
1254
1688
|
tool: tool.name,
|
|
1255
1689
|
reason: "selector-not-required",
|
|
1256
|
-
detail: `selector
|
|
1690
|
+
detail: `selector [${present.join(", ")}] present in properties but missing from inputSchema.required`
|
|
1257
1691
|
});
|
|
1258
1692
|
continue;
|
|
1259
1693
|
}
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1694
|
+
if (!requiredSelectors.find((name) => {
|
|
1695
|
+
const property = properties[name];
|
|
1696
|
+
return isRecord(property) && property.type === "string";
|
|
1697
|
+
})) {
|
|
1698
|
+
const foundTypes = requiredSelectors.map((name) => {
|
|
1699
|
+
const property = properties[name];
|
|
1700
|
+
return `${name}=${JSON.stringify(isRecord(property) ? property.type : void 0)}`;
|
|
1701
|
+
});
|
|
1702
|
+
violations.push({
|
|
1703
|
+
tool: tool.name,
|
|
1704
|
+
reason: "selector-property-not-string",
|
|
1705
|
+
detail: `at least one required selector must be JSON Schema type=string (found ${foundTypes.join(", ")})`
|
|
1706
|
+
});
|
|
1707
|
+
}
|
|
1266
1708
|
}
|
|
1267
1709
|
return violations;
|
|
1268
1710
|
}
|
|
@@ -1273,7 +1715,7 @@ function checkPatternA(tools, options) {
|
|
|
1273
1715
|
function assertPatternA(tools, options) {
|
|
1274
1716
|
const violations = checkPatternA(tools, options);
|
|
1275
1717
|
if (violations.length === 0) return;
|
|
1276
|
-
const lines = violations.map((
|
|
1718
|
+
const lines = violations.map((violation) => ` - [${violation.reason}] ${violation.tool}: ${violation.detail}`);
|
|
1277
1719
|
throw new Error(`Pattern A validation failed for ${String(violations.length)} tool(s):\n${lines.join("\n")}`);
|
|
1278
1720
|
}
|
|
1279
1721
|
/**
|
|
@@ -1288,7 +1730,10 @@ function fromMcpDescriptor(descriptor) {
|
|
|
1288
1730
|
parameters: descriptor.parameters
|
|
1289
1731
|
};
|
|
1290
1732
|
}
|
|
1733
|
+
function isRecord(value) {
|
|
1734
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1735
|
+
}
|
|
1291
1736
|
//#endregion
|
|
1292
|
-
export { Connection, Manager, McpBundler, STDIO_ENV_DENYLIST, Store, assertPatternA, buildNamespacedToolName, checkPatternA, defaultConnect, defaultStorePath, disambiguateAgainst, fromMcpDescriptor, sanitizeNameSegment, sanitizeStdioEnv, toServerConfig, toStoredEntry };
|
|
1737
|
+
export { Connection, Manager, McpBundler, STDIO_ENV_DENYLIST, Store, assertPatternA, buildNamespacedToolName, checkPatternA, defaultConnect, defaultStorePath, disambiguateAgainst, fromMcpDescriptor, sanitizeNameSegment, sanitizeStdioEnv, serverLaunchFingerprint, toServerConfig, toStoredEntry };
|
|
1293
1738
|
|
|
1294
1739
|
//# sourceMappingURL=index.js.map
|