dynmcp 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -0
- package/dist/index.cjs +1276 -196
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1299 -197
- package/dist/index.js.map +1 -1
- package/package.json +3 -1
- package/schema/mcp-config.json +10 -0
package/dist/index.cjs
CHANGED
|
@@ -23,13 +23,13 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
23
23
|
));
|
|
24
24
|
|
|
25
25
|
// src/cli.ts
|
|
26
|
-
var
|
|
26
|
+
var import_node_process7 = __toESM(require("process"), 1);
|
|
27
27
|
var import_commander = require("commander");
|
|
28
28
|
|
|
29
29
|
// package.json
|
|
30
30
|
var package_default = {
|
|
31
31
|
name: "dynmcp",
|
|
32
|
-
version: "0.
|
|
32
|
+
version: "0.3.0",
|
|
33
33
|
description: "Dynamic MCP context management tool for AI MCP-enabled agents and clients.",
|
|
34
34
|
author: "Brandon Burrus <brandon@burrus.io>",
|
|
35
35
|
license: "MIT",
|
|
@@ -96,6 +96,7 @@ var package_default = {
|
|
|
96
96
|
boxen: "^8.0.1",
|
|
97
97
|
chalk: "^5.6.2",
|
|
98
98
|
commander: "^14.0.3",
|
|
99
|
+
dotenv: "^17.4.2",
|
|
99
100
|
enquirer: "^2.4.1",
|
|
100
101
|
fastmcp: "^4.0.1",
|
|
101
102
|
figlet: "^1.11.0",
|
|
@@ -108,6 +109,7 @@ var package_default = {
|
|
|
108
109
|
"@commitlint/cli": "^21.0.1",
|
|
109
110
|
"@commitlint/config-conventional": "^21.0.1",
|
|
110
111
|
"@types/node": "^25.9.0",
|
|
112
|
+
"@vitest/coverage-v8": "^4.1.6",
|
|
111
113
|
husky: "^9.1.7",
|
|
112
114
|
tsup: "^8.5.1",
|
|
113
115
|
tsx: "^4.22.2",
|
|
@@ -121,13 +123,16 @@ var import_figlet = __toESM(require("figlet"), 1);
|
|
|
121
123
|
var import_chalk = __toESM(require("chalk"), 1);
|
|
122
124
|
|
|
123
125
|
// src/proxy/index.ts
|
|
124
|
-
var
|
|
125
|
-
var
|
|
126
|
+
var import_node_process6 = __toESM(require("process"), 1);
|
|
127
|
+
var import_stdio3 = require("@modelcontextprotocol/sdk/client/stdio.js");
|
|
126
128
|
|
|
127
129
|
// src/config/schema.ts
|
|
128
130
|
var import_zod = require("zod");
|
|
129
131
|
var MCP_NAME_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
|
|
130
132
|
var mcpName = import_zod.z.string().regex(MCP_NAME_PATTERN);
|
|
133
|
+
var envModeSchema = import_zod.z.enum(["enable", "dotenv", "process", "disable"]).describe(
|
|
134
|
+
'Controls environment variable interpolation in config values. "enable" (default) merges .env and process.env (.env wins). "dotenv" loads .env only. "process" uses process.env only. "disable" turns interpolation off.'
|
|
135
|
+
);
|
|
131
136
|
var stdioTransport = import_zod.z.object({
|
|
132
137
|
transport: import_zod.z.literal("stdio"),
|
|
133
138
|
command: import_zod.z.string(),
|
|
@@ -153,51 +158,227 @@ var transportConfig = import_zod.z.discriminatedUnion("transport", [
|
|
|
153
158
|
sseTransport
|
|
154
159
|
]);
|
|
155
160
|
var mcpConfigSchema = import_zod.z.object({
|
|
161
|
+
env: envModeSchema.optional(),
|
|
156
162
|
mcp: import_zod.z.record(mcpName, transportConfig).refine((obj) => Object.keys(obj).length > 0, { message: "At least one MCP must be configured" })
|
|
157
163
|
});
|
|
158
164
|
|
|
159
165
|
// src/config/loader.ts
|
|
160
|
-
var import_node_fs = require("fs");
|
|
161
166
|
var import_node_fs2 = require("fs");
|
|
162
|
-
var
|
|
167
|
+
var import_node_path2 = require("path");
|
|
168
|
+
var import_node_process2 = __toESM(require("process"), 1);
|
|
163
169
|
var import_yaml = require("yaml");
|
|
170
|
+
|
|
171
|
+
// src/config/env-sources.ts
|
|
172
|
+
var import_node_fs = require("fs");
|
|
173
|
+
var import_node_path = require("path");
|
|
174
|
+
var import_node_process = __toESM(require("process"), 1);
|
|
175
|
+
var import_dotenv = __toESM(require("dotenv"), 1);
|
|
176
|
+
var DEFAULT_DOTENV_FILENAME = ".env";
|
|
177
|
+
function loadEnv(options) {
|
|
178
|
+
const { mode, envFilePath, cwd = import_node_process.default.cwd(), processEnv = import_node_process.default.env } = options;
|
|
179
|
+
if (envFilePath !== void 0 && (mode === "disable" || mode === "process")) {
|
|
180
|
+
throw new Error(
|
|
181
|
+
`--env flag is incompatible with env mode "${mode}". --env requires env mode "enable" or "dotenv".`
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
if (mode === "disable") {
|
|
185
|
+
return { variables: {}, interpolationEnabled: false };
|
|
186
|
+
}
|
|
187
|
+
const dotenvVars = mode === "process" ? {} : readDotenvFile(envFilePath, cwd);
|
|
188
|
+
const processVars = mode === "dotenv" ? {} : filterDefined(processEnv);
|
|
189
|
+
const variables = { ...processVars, ...dotenvVars };
|
|
190
|
+
return { variables, interpolationEnabled: true };
|
|
191
|
+
}
|
|
192
|
+
function readDotenvFile(envFilePath, cwd) {
|
|
193
|
+
const isExplicit = envFilePath !== void 0;
|
|
194
|
+
const resolvedPath = isExplicit ? (0, import_node_path.resolve)(envFilePath) : (0, import_node_path.resolve)(cwd, DEFAULT_DOTENV_FILENAME);
|
|
195
|
+
if (!(0, import_node_fs.existsSync)(resolvedPath)) {
|
|
196
|
+
if (isExplicit) {
|
|
197
|
+
throw new Error(`.env file not found: ${resolvedPath}`);
|
|
198
|
+
}
|
|
199
|
+
return {};
|
|
200
|
+
}
|
|
201
|
+
let raw;
|
|
202
|
+
try {
|
|
203
|
+
raw = (0, import_node_fs.readFileSync)(resolvedPath, "utf-8");
|
|
204
|
+
} catch (readError) {
|
|
205
|
+
const message = readError instanceof Error ? readError.message : String(readError);
|
|
206
|
+
throw new Error(`Failed to read .env file (${resolvedPath}): ${message}`);
|
|
207
|
+
}
|
|
208
|
+
try {
|
|
209
|
+
return import_dotenv.default.parse(raw);
|
|
210
|
+
} catch (parseError) {
|
|
211
|
+
const message = parseError instanceof Error ? parseError.message : String(parseError);
|
|
212
|
+
throw new Error(`Failed to parse .env file (${resolvedPath}): ${message}`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
function filterDefined(env) {
|
|
216
|
+
const result = {};
|
|
217
|
+
for (const [key, value] of Object.entries(env)) {
|
|
218
|
+
if (value !== void 0) {
|
|
219
|
+
result[key] = value;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return result;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// src/config/interpolate.ts
|
|
226
|
+
var TOP_LEVEL_PASSTHROUGH_KEYS = /* @__PURE__ */ new Set(["$schema", "env"]);
|
|
227
|
+
var MissingEnvVarsError = class extends Error {
|
|
228
|
+
constructor(missingVars) {
|
|
229
|
+
const list = missingVars.join(", ");
|
|
230
|
+
const plural = missingVars.length === 1 ? "" : "s";
|
|
231
|
+
super(`Missing required environment variable${plural}: ${list}`);
|
|
232
|
+
this.missingVars = missingVars;
|
|
233
|
+
this.name = "MissingEnvVarsError";
|
|
234
|
+
}
|
|
235
|
+
missingVars;
|
|
236
|
+
};
|
|
237
|
+
function interpolateConfig(config, env) {
|
|
238
|
+
if (config === null || typeof config !== "object" || Array.isArray(config)) {
|
|
239
|
+
return config;
|
|
240
|
+
}
|
|
241
|
+
const missing = [];
|
|
242
|
+
const result = {};
|
|
243
|
+
for (const [key, value] of Object.entries(config)) {
|
|
244
|
+
if (TOP_LEVEL_PASSTHROUGH_KEYS.has(key)) {
|
|
245
|
+
result[key] = value;
|
|
246
|
+
} else {
|
|
247
|
+
result[key] = walkNode(value, env, missing);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
if (missing.length > 0) {
|
|
251
|
+
const unique = Array.from(new Set(missing)).sort();
|
|
252
|
+
throw new MissingEnvVarsError(unique);
|
|
253
|
+
}
|
|
254
|
+
return result;
|
|
255
|
+
}
|
|
256
|
+
function walkNode(node, env, missing) {
|
|
257
|
+
if (typeof node === "string") {
|
|
258
|
+
return interpolateString(node, env, missing);
|
|
259
|
+
}
|
|
260
|
+
if (Array.isArray(node)) {
|
|
261
|
+
return node.map((item) => walkNode(item, env, missing));
|
|
262
|
+
}
|
|
263
|
+
if (node !== null && typeof node === "object") {
|
|
264
|
+
const result = {};
|
|
265
|
+
for (const [key, value] of Object.entries(node)) {
|
|
266
|
+
result[key] = walkNode(value, env, missing);
|
|
267
|
+
}
|
|
268
|
+
return result;
|
|
269
|
+
}
|
|
270
|
+
return node;
|
|
271
|
+
}
|
|
272
|
+
function interpolateString(value, env, missing) {
|
|
273
|
+
let result = "";
|
|
274
|
+
let i = 0;
|
|
275
|
+
const len = value.length;
|
|
276
|
+
while (i < len) {
|
|
277
|
+
const ch = value[i];
|
|
278
|
+
if (ch === "$" && value[i + 1] === "$" && value[i + 2] === "{") {
|
|
279
|
+
const close = value.indexOf("}", i + 3);
|
|
280
|
+
if (close === -1) {
|
|
281
|
+
result += ch;
|
|
282
|
+
i += 1;
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
result += value.substring(i + 1, close + 1);
|
|
286
|
+
i = close + 1;
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
if (ch === "$" && value[i + 1] === "{") {
|
|
290
|
+
const close = value.indexOf("}", i + 2);
|
|
291
|
+
if (close === -1) {
|
|
292
|
+
result += value.substring(i);
|
|
293
|
+
break;
|
|
294
|
+
}
|
|
295
|
+
const expr = value.substring(i + 2, close);
|
|
296
|
+
const { name, defaultValue } = parseExpr(expr);
|
|
297
|
+
const resolved = env[name];
|
|
298
|
+
const hasValue = resolved !== void 0 && resolved !== "";
|
|
299
|
+
if (hasValue) {
|
|
300
|
+
result += resolved;
|
|
301
|
+
} else if (defaultValue !== void 0) {
|
|
302
|
+
result += defaultValue;
|
|
303
|
+
} else if (resolved !== void 0) {
|
|
304
|
+
result += "";
|
|
305
|
+
} else {
|
|
306
|
+
missing.push(name);
|
|
307
|
+
}
|
|
308
|
+
i = close + 1;
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
result += ch;
|
|
312
|
+
i += 1;
|
|
313
|
+
}
|
|
314
|
+
return result;
|
|
315
|
+
}
|
|
316
|
+
function parseExpr(expr) {
|
|
317
|
+
const sep = expr.indexOf(":-");
|
|
318
|
+
if (sep === -1) {
|
|
319
|
+
return { name: expr, defaultValue: void 0 };
|
|
320
|
+
}
|
|
321
|
+
return {
|
|
322
|
+
name: expr.substring(0, sep),
|
|
323
|
+
defaultValue: expr.substring(sep + 2)
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// src/config/loader.ts
|
|
164
328
|
var AUTO_DISCOVER_NAMES = ["mcp.json", ".mcp.json"];
|
|
329
|
+
var DEFAULT_ENV_MODE = "enable";
|
|
330
|
+
var VALID_ENV_MODES = ["enable", "dotenv", "process", "disable"];
|
|
165
331
|
function resolveConfigPath(explicitPath) {
|
|
166
332
|
if (explicitPath) {
|
|
167
|
-
const resolved = (0,
|
|
333
|
+
const resolved = (0, import_node_path2.resolve)(explicitPath);
|
|
168
334
|
if (!(0, import_node_fs2.existsSync)(resolved)) {
|
|
169
335
|
throw new Error(`Config file not found: ${resolved}`);
|
|
170
336
|
}
|
|
171
337
|
return resolved;
|
|
172
338
|
}
|
|
173
|
-
const cwd =
|
|
339
|
+
const cwd = import_node_process2.default.cwd();
|
|
174
340
|
for (const name of AUTO_DISCOVER_NAMES) {
|
|
175
|
-
const candidate = (0,
|
|
341
|
+
const candidate = (0, import_node_path2.resolve)(cwd, name);
|
|
176
342
|
if ((0, import_node_fs2.existsSync)(candidate)) {
|
|
177
343
|
return candidate;
|
|
178
344
|
}
|
|
179
345
|
}
|
|
180
|
-
const searched = AUTO_DISCOVER_NAMES.map((n) => (0,
|
|
346
|
+
const searched = AUTO_DISCOVER_NAMES.map((n) => (0, import_node_path2.resolve)(cwd, n)).join(", ");
|
|
181
347
|
throw new Error(`No config file found. Searched: ${searched}`);
|
|
182
348
|
}
|
|
183
|
-
function loadConfig(
|
|
184
|
-
const configPath =
|
|
185
|
-
const
|
|
349
|
+
function loadConfig(options = {}) {
|
|
350
|
+
const { configPath, envFilePath } = options;
|
|
351
|
+
const resolvedPath = resolveConfigPath(configPath);
|
|
352
|
+
const raw = (0, import_node_fs2.readFileSync)(resolvedPath, "utf-8");
|
|
186
353
|
let content;
|
|
187
354
|
try {
|
|
188
|
-
content = isYamlFile(
|
|
355
|
+
content = isYamlFile(resolvedPath) ? (0, import_yaml.parse)(raw) : JSON.parse(raw);
|
|
189
356
|
} catch (parseError) {
|
|
190
357
|
const message = parseError instanceof Error ? parseError.message : String(parseError);
|
|
191
|
-
throw new Error(`Failed to parse config file (${
|
|
358
|
+
throw new Error(`Failed to parse config file (${resolvedPath}): ${message}`);
|
|
192
359
|
}
|
|
193
|
-
const
|
|
360
|
+
const envMode = readEnvMode(content);
|
|
361
|
+
const loadedEnv = loadEnv({ mode: envMode, envFilePath });
|
|
362
|
+
const interpolated = loadedEnv.interpolationEnabled ? interpolateConfig(content, loadedEnv.variables) : content;
|
|
363
|
+
const result = mcpConfigSchema.safeParse(interpolated);
|
|
194
364
|
if (!result.success) {
|
|
195
365
|
const formatted = result.error.issues.map((issue) => ` - ${issue.path.join(".")}: ${issue.message}`).join("\n");
|
|
196
|
-
throw new Error(`Invalid config file (${
|
|
366
|
+
throw new Error(`Invalid config file (${resolvedPath}):
|
|
197
367
|
${formatted}`);
|
|
198
368
|
}
|
|
199
369
|
return result.data;
|
|
200
370
|
}
|
|
371
|
+
function readEnvMode(content) {
|
|
372
|
+
if (content === null || typeof content !== "object" || Array.isArray(content)) {
|
|
373
|
+
return DEFAULT_ENV_MODE;
|
|
374
|
+
}
|
|
375
|
+
const value = content.env;
|
|
376
|
+
if (value === void 0) return DEFAULT_ENV_MODE;
|
|
377
|
+
if (typeof value === "string" && VALID_ENV_MODES.includes(value)) {
|
|
378
|
+
return value;
|
|
379
|
+
}
|
|
380
|
+
return DEFAULT_ENV_MODE;
|
|
381
|
+
}
|
|
201
382
|
function isYamlFile(filePath) {
|
|
202
383
|
return filePath.endsWith(".yml") || filePath.endsWith(".yaml");
|
|
203
384
|
}
|
|
@@ -205,33 +386,39 @@ function isYamlFile(filePath) {
|
|
|
205
386
|
// src/config/json-schema.ts
|
|
206
387
|
var import_zod2 = require("zod");
|
|
207
388
|
|
|
208
|
-
// src/proxy/
|
|
209
|
-
var
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
function
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
)
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
389
|
+
// src/proxy/orchestrator.ts
|
|
390
|
+
var import_node_process4 = __toESM(require("process"), 1);
|
|
391
|
+
|
|
392
|
+
// src/proxy/capability-aggregator.ts
|
|
393
|
+
function aggregateCapabilities(upstreams) {
|
|
394
|
+
const aggregated = {
|
|
395
|
+
tools: { listChanged: true }
|
|
396
|
+
};
|
|
397
|
+
for (const caps of upstreams) {
|
|
398
|
+
if (caps === void 0) continue;
|
|
399
|
+
if (caps.resources !== void 0) {
|
|
400
|
+
aggregated.resources ??= {};
|
|
401
|
+
if (caps.resources.subscribe === true) {
|
|
402
|
+
aggregated.resources.subscribe = true;
|
|
403
|
+
}
|
|
404
|
+
if (caps.resources.listChanged === true) {
|
|
405
|
+
aggregated.resources.listChanged = true;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
if (caps.prompts !== void 0) {
|
|
409
|
+
aggregated.prompts ??= {};
|
|
410
|
+
if (caps.prompts.listChanged === true) {
|
|
411
|
+
aggregated.prompts.listChanged = true;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
if (caps.logging !== void 0) {
|
|
415
|
+
aggregated.logging ??= {};
|
|
416
|
+
}
|
|
417
|
+
if (caps.completions !== void 0) {
|
|
418
|
+
aggregated.completions ??= {};
|
|
233
419
|
}
|
|
234
420
|
}
|
|
421
|
+
return aggregated;
|
|
235
422
|
}
|
|
236
423
|
|
|
237
424
|
// src/proxy/tool-catalog.ts
|
|
@@ -337,30 +524,332 @@ function buildAnnotationLines(tool) {
|
|
|
337
524
|
return lines;
|
|
338
525
|
}
|
|
339
526
|
|
|
527
|
+
// src/proxy/notification-forwarder.ts
|
|
528
|
+
var NotificationForwarder = class {
|
|
529
|
+
constructor(registry, resourceRouter, promptRouter, toolsByMcp, setToolCatalog, namespaced) {
|
|
530
|
+
this.registry = registry;
|
|
531
|
+
this.resourceRouter = resourceRouter;
|
|
532
|
+
this.promptRouter = promptRouter;
|
|
533
|
+
this.toolsByMcp = toolsByMcp;
|
|
534
|
+
this.setToolCatalog = setToolCatalog;
|
|
535
|
+
this.namespaced = namespaced;
|
|
536
|
+
}
|
|
537
|
+
registry;
|
|
538
|
+
resourceRouter;
|
|
539
|
+
promptRouter;
|
|
540
|
+
toolsByMcp;
|
|
541
|
+
setToolCatalog;
|
|
542
|
+
namespaced;
|
|
543
|
+
hostHandlers = {};
|
|
544
|
+
setHostHandlers(handlers) {
|
|
545
|
+
this.hostHandlers = handlers;
|
|
546
|
+
}
|
|
547
|
+
async handleToolsListChanged(mcpName2) {
|
|
548
|
+
const client = this.registry.get(mcpName2);
|
|
549
|
+
if (client === void 0) return;
|
|
550
|
+
const tools = await client.listTools().catch(() => []);
|
|
551
|
+
this.toolsByMcp.set(mcpName2, tools);
|
|
552
|
+
const rebuilt = this.namespaced ? ToolCatalog.fromGrouped(this.toolsByMcp) : ToolCatalog.fromFlat([...this.toolsByMcp.values()][0] ?? []);
|
|
553
|
+
this.setToolCatalog(rebuilt);
|
|
554
|
+
await this.hostHandlers.onToolsListChanged?.();
|
|
555
|
+
}
|
|
556
|
+
async handleResourcesListChanged(mcpName2) {
|
|
557
|
+
const router = this.resourceRouter();
|
|
558
|
+
const client = this.registry.get(mcpName2);
|
|
559
|
+
if (router === null || client === void 0) return;
|
|
560
|
+
const [resources, templates] = await Promise.all([
|
|
561
|
+
client.listResources().catch(() => []),
|
|
562
|
+
client.listResourceTemplates().catch(() => [])
|
|
563
|
+
]);
|
|
564
|
+
router.setResources(mcpName2, resources);
|
|
565
|
+
router.setTemplates(mcpName2, templates);
|
|
566
|
+
await this.hostHandlers.onResourcesListChanged?.();
|
|
567
|
+
}
|
|
568
|
+
async handleResourceUpdated(params) {
|
|
569
|
+
await this.hostHandlers.onResourceUpdated?.(params);
|
|
570
|
+
}
|
|
571
|
+
async handlePromptsListChanged(mcpName2) {
|
|
572
|
+
const router = this.promptRouter();
|
|
573
|
+
const client = this.registry.get(mcpName2);
|
|
574
|
+
if (router === null || client === void 0) return;
|
|
575
|
+
const prompts = await client.listPrompts().catch(() => []);
|
|
576
|
+
router.setPrompts(mcpName2, prompts);
|
|
577
|
+
await this.hostHandlers.onPromptsListChanged?.();
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Rewrites the upstream's `logger` field with the originating MCP's name as a
|
|
581
|
+
* prefix so the host can attribute log lines, then forwards the message to the
|
|
582
|
+
* host's log message handler.
|
|
583
|
+
*/
|
|
584
|
+
async handleLogMessage(mcpName2, params) {
|
|
585
|
+
const handler = this.hostHandlers.onLogMessage;
|
|
586
|
+
if (handler === void 0) return;
|
|
587
|
+
if (!this.namespaced) {
|
|
588
|
+
await handler(params);
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
const prefixed = {
|
|
592
|
+
...params,
|
|
593
|
+
logger: params.logger === void 0 ? mcpName2 : `${mcpName2}/${params.logger}`
|
|
594
|
+
};
|
|
595
|
+
await handler(prefixed);
|
|
596
|
+
}
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
// src/proxy/prompt-router.ts
|
|
600
|
+
var PromptRouter = class {
|
|
601
|
+
mcpOrder;
|
|
602
|
+
perMcp;
|
|
603
|
+
nameOwners = /* @__PURE__ */ new Map();
|
|
604
|
+
detectedCollisions = [];
|
|
605
|
+
constructor(mcpOrder) {
|
|
606
|
+
this.mcpOrder = [...mcpOrder];
|
|
607
|
+
this.perMcp = new Map(this.mcpOrder.map((name) => [name, []]));
|
|
608
|
+
}
|
|
609
|
+
setPrompts(mcpName2, prompts) {
|
|
610
|
+
const entry = this.perMcp.get(mcpName2);
|
|
611
|
+
if (entry === void 0) {
|
|
612
|
+
throw new Error(`PromptRouter: unknown mcp "${mcpName2}"`);
|
|
613
|
+
}
|
|
614
|
+
this.perMcp.set(mcpName2, [...prompts]);
|
|
615
|
+
this.rebuild();
|
|
616
|
+
}
|
|
617
|
+
aggregatedPrompts() {
|
|
618
|
+
const result = [];
|
|
619
|
+
for (const mcpName2 of this.mcpOrder) {
|
|
620
|
+
const entry = this.perMcp.get(mcpName2);
|
|
621
|
+
if (entry !== void 0) {
|
|
622
|
+
result.push(...entry);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
return result;
|
|
626
|
+
}
|
|
627
|
+
ownerOf(promptName) {
|
|
628
|
+
return this.nameOwners.get(promptName);
|
|
629
|
+
}
|
|
630
|
+
collisions() {
|
|
631
|
+
return this.detectedCollisions;
|
|
632
|
+
}
|
|
633
|
+
rebuild() {
|
|
634
|
+
this.nameOwners = /* @__PURE__ */ new Map();
|
|
635
|
+
const collisions = [];
|
|
636
|
+
for (const mcpName2 of this.mcpOrder) {
|
|
637
|
+
const prompts = this.perMcp.get(mcpName2);
|
|
638
|
+
if (prompts === void 0) continue;
|
|
639
|
+
for (const prompt of prompts) {
|
|
640
|
+
const existing = this.nameOwners.get(prompt.name);
|
|
641
|
+
if (existing === void 0) {
|
|
642
|
+
this.nameOwners.set(prompt.name, mcpName2);
|
|
643
|
+
} else {
|
|
644
|
+
collisions.push({ name: prompt.name, chosen: existing, shadowed: mcpName2 });
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
this.detectedCollisions = collisions;
|
|
649
|
+
}
|
|
650
|
+
};
|
|
651
|
+
|
|
652
|
+
// src/proxy/resource-router.ts
|
|
653
|
+
var ResourceRouter = class {
|
|
654
|
+
mcpOrder;
|
|
655
|
+
perMcp;
|
|
656
|
+
uriOwners = /* @__PURE__ */ new Map();
|
|
657
|
+
templateOwners = [];
|
|
658
|
+
detectedCollisions = [];
|
|
659
|
+
constructor(mcpOrder) {
|
|
660
|
+
this.mcpOrder = [...mcpOrder];
|
|
661
|
+
this.perMcp = new Map(
|
|
662
|
+
this.mcpOrder.map((name) => [name, { resources: [], templates: [] }])
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
setResources(mcpName2, resources) {
|
|
666
|
+
const entry = this.perMcp.get(mcpName2);
|
|
667
|
+
if (entry === void 0) {
|
|
668
|
+
throw new Error(`ResourceRouter: unknown mcp "${mcpName2}"`);
|
|
669
|
+
}
|
|
670
|
+
entry.resources = [...resources];
|
|
671
|
+
this.rebuild();
|
|
672
|
+
}
|
|
673
|
+
setTemplates(mcpName2, templates) {
|
|
674
|
+
const entry = this.perMcp.get(mcpName2);
|
|
675
|
+
if (entry === void 0) {
|
|
676
|
+
throw new Error(`ResourceRouter: unknown mcp "${mcpName2}"`);
|
|
677
|
+
}
|
|
678
|
+
entry.templates = [...templates];
|
|
679
|
+
this.rebuild();
|
|
680
|
+
}
|
|
681
|
+
aggregatedResources() {
|
|
682
|
+
const result = [];
|
|
683
|
+
for (const mcpName2 of this.mcpOrder) {
|
|
684
|
+
const entry = this.perMcp.get(mcpName2);
|
|
685
|
+
if (entry !== void 0) {
|
|
686
|
+
result.push(...entry.resources);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
return result;
|
|
690
|
+
}
|
|
691
|
+
aggregatedTemplates() {
|
|
692
|
+
const result = [];
|
|
693
|
+
for (const mcpName2 of this.mcpOrder) {
|
|
694
|
+
const entry = this.perMcp.get(mcpName2);
|
|
695
|
+
if (entry !== void 0) {
|
|
696
|
+
result.push(...entry.templates);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
return result;
|
|
700
|
+
}
|
|
701
|
+
/**
|
|
702
|
+
* Returns the mcpName that owns the given URI, or undefined if no upstream advertises it.
|
|
703
|
+
* Concrete URI matches take precedence over template prefix matches; templates are tried
|
|
704
|
+
* in config-file order (first-wins).
|
|
705
|
+
*/
|
|
706
|
+
ownerOf(uri) {
|
|
707
|
+
const concrete = this.uriOwners.get(uri);
|
|
708
|
+
if (concrete !== void 0) {
|
|
709
|
+
return concrete;
|
|
710
|
+
}
|
|
711
|
+
for (const { prefix, mcpName: mcpName2 } of this.templateOwners) {
|
|
712
|
+
if (prefix.length > 0 && uri.startsWith(prefix)) {
|
|
713
|
+
return mcpName2;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
return void 0;
|
|
717
|
+
}
|
|
718
|
+
collisions() {
|
|
719
|
+
return this.detectedCollisions;
|
|
720
|
+
}
|
|
721
|
+
rebuild() {
|
|
722
|
+
this.uriOwners = /* @__PURE__ */ new Map();
|
|
723
|
+
this.templateOwners = [];
|
|
724
|
+
const collisions = [];
|
|
725
|
+
for (const mcpName2 of this.mcpOrder) {
|
|
726
|
+
const entry = this.perMcp.get(mcpName2);
|
|
727
|
+
if (entry === void 0) continue;
|
|
728
|
+
for (const resource of entry.resources) {
|
|
729
|
+
const existing = this.uriOwners.get(resource.uri);
|
|
730
|
+
if (existing === void 0) {
|
|
731
|
+
this.uriOwners.set(resource.uri, mcpName2);
|
|
732
|
+
} else {
|
|
733
|
+
collisions.push({ uri: resource.uri, chosen: existing, shadowed: mcpName2 });
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
for (const template of entry.templates) {
|
|
737
|
+
this.templateOwners.push({
|
|
738
|
+
prefix: literalPrefixOf(template.uriTemplate),
|
|
739
|
+
template,
|
|
740
|
+
mcpName: mcpName2
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
this.detectedCollisions = collisions;
|
|
745
|
+
}
|
|
746
|
+
};
|
|
747
|
+
function literalPrefixOf(uriTemplate) {
|
|
748
|
+
const idx = uriTemplate.indexOf("{");
|
|
749
|
+
return idx === -1 ? uriTemplate : uriTemplate.slice(0, idx);
|
|
750
|
+
}
|
|
751
|
+
|
|
340
752
|
// src/proxy/upstream-client.ts
|
|
341
|
-
var
|
|
753
|
+
var import_node_process3 = __toESM(require("process"), 1);
|
|
342
754
|
var import_client = require("@modelcontextprotocol/sdk/client/index.js");
|
|
755
|
+
var import_types = require("@modelcontextprotocol/sdk/types.js");
|
|
343
756
|
var UpstreamClient = class {
|
|
344
757
|
transport;
|
|
345
758
|
onTransportError;
|
|
759
|
+
notificationHandlers;
|
|
760
|
+
serverRequestHandlers;
|
|
346
761
|
client = null;
|
|
347
|
-
constructor({
|
|
762
|
+
constructor({
|
|
763
|
+
name,
|
|
764
|
+
transport,
|
|
765
|
+
onTransportError,
|
|
766
|
+
notifications,
|
|
767
|
+
serverRequests
|
|
768
|
+
}) {
|
|
348
769
|
this.transport = transport;
|
|
770
|
+
this.notificationHandlers = notifications ?? {};
|
|
771
|
+
this.serverRequestHandlers = serverRequests ?? {};
|
|
349
772
|
this.onTransportError = onTransportError ?? ((error) => {
|
|
350
|
-
|
|
773
|
+
import_node_process3.default.stderr.write(`[${name}] Upstream MCP transport error: ${error.message}
|
|
351
774
|
`);
|
|
352
775
|
});
|
|
353
776
|
}
|
|
354
777
|
async connect() {
|
|
355
778
|
this.transport.onerror = this.onTransportError;
|
|
356
|
-
this.client = new import_client.Client(
|
|
779
|
+
this.client = new import_client.Client(
|
|
780
|
+
{ name: "dynamic-discovery-mcp", version: "1.0.0" },
|
|
781
|
+
{
|
|
782
|
+
capabilities: {
|
|
783
|
+
// Declare every client-side capability the proxy may relay on behalf of the host.
|
|
784
|
+
// Actual reachability of each feature depends on what the host supports — if the
|
|
785
|
+
// host does not support sampling, for instance, the host call returns an error
|
|
786
|
+
// which we forward back to the upstream verbatim.
|
|
787
|
+
sampling: {},
|
|
788
|
+
elicitation: {},
|
|
789
|
+
roots: { listChanged: true }
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
);
|
|
793
|
+
this.registerServerRequestHandlers(this.client);
|
|
794
|
+
if (this.notificationHandlers.onToolsListChanged !== void 0) {
|
|
795
|
+
this.client.setNotificationHandler(import_types.ToolListChangedNotificationSchema, async () => {
|
|
796
|
+
await this.notificationHandlers.onToolsListChanged?.();
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
if (this.notificationHandlers.onResourcesListChanged !== void 0) {
|
|
800
|
+
this.client.setNotificationHandler(import_types.ResourceListChangedNotificationSchema, async () => {
|
|
801
|
+
await this.notificationHandlers.onResourcesListChanged?.();
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
if (this.notificationHandlers.onResourceUpdated !== void 0) {
|
|
805
|
+
this.client.setNotificationHandler(import_types.ResourceUpdatedNotificationSchema, async (notification) => {
|
|
806
|
+
await this.notificationHandlers.onResourceUpdated?.({ uri: notification.params.uri });
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
if (this.notificationHandlers.onPromptsListChanged !== void 0) {
|
|
810
|
+
this.client.setNotificationHandler(import_types.PromptListChangedNotificationSchema, async () => {
|
|
811
|
+
await this.notificationHandlers.onPromptsListChanged?.();
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
if (this.notificationHandlers.onLogMessage !== void 0) {
|
|
815
|
+
this.client.setNotificationHandler(import_types.LoggingMessageNotificationSchema, async (notification) => {
|
|
816
|
+
await this.notificationHandlers.onLogMessage?.(notification.params);
|
|
817
|
+
});
|
|
818
|
+
}
|
|
357
819
|
await this.client.connect(this.transport);
|
|
358
820
|
}
|
|
359
|
-
async
|
|
360
|
-
|
|
361
|
-
|
|
821
|
+
async setLoggingLevel(level, options) {
|
|
822
|
+
const client = this.requireClient();
|
|
823
|
+
await client.setLoggingLevel(level, options);
|
|
824
|
+
}
|
|
825
|
+
async listPrompts(options) {
|
|
826
|
+
const client = this.requireClient();
|
|
827
|
+
const result = await client.listPrompts(void 0, options);
|
|
828
|
+
return result.prompts;
|
|
829
|
+
}
|
|
830
|
+
async getPrompt(name, args, options) {
|
|
831
|
+
const client = this.requireClient();
|
|
832
|
+
const params = { name };
|
|
833
|
+
if (args !== void 0) {
|
|
834
|
+
params.arguments = args;
|
|
362
835
|
}
|
|
363
|
-
|
|
836
|
+
return client.getPrompt(params, options);
|
|
837
|
+
}
|
|
838
|
+
async complete(params, options) {
|
|
839
|
+
const client = this.requireClient();
|
|
840
|
+
return client.complete(params, options);
|
|
841
|
+
}
|
|
842
|
+
/**
|
|
843
|
+
* Returns the capabilities advertised by the upstream server during initialize.
|
|
844
|
+
* Returns `undefined` if the client is not connected, or if the SDK has not yet
|
|
845
|
+
* recorded the server's capabilities (e.g. during a partially-completed handshake).
|
|
846
|
+
*/
|
|
847
|
+
getCapabilities() {
|
|
848
|
+
return this.client?.getServerCapabilities();
|
|
849
|
+
}
|
|
850
|
+
async listTools(options) {
|
|
851
|
+
const client = this.requireClient();
|
|
852
|
+
const result = await client.listTools(void 0, options);
|
|
364
853
|
return result.tools.map((tool) => {
|
|
365
854
|
const upstreamTool = {
|
|
366
855
|
name: tool.name,
|
|
@@ -382,13 +871,33 @@ var UpstreamClient = class {
|
|
|
382
871
|
return upstreamTool;
|
|
383
872
|
});
|
|
384
873
|
}
|
|
385
|
-
async callTool(name, input) {
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
}
|
|
389
|
-
const result = await this.client.callTool({ name, arguments: input });
|
|
874
|
+
async callTool(name, input, options) {
|
|
875
|
+
const client = this.requireClient();
|
|
876
|
+
const result = await client.callTool({ name, arguments: input }, void 0, options);
|
|
390
877
|
return result;
|
|
391
878
|
}
|
|
879
|
+
async listResources(options) {
|
|
880
|
+
const client = this.requireClient();
|
|
881
|
+
const result = await client.listResources(void 0, options);
|
|
882
|
+
return result.resources;
|
|
883
|
+
}
|
|
884
|
+
async listResourceTemplates(options) {
|
|
885
|
+
const client = this.requireClient();
|
|
886
|
+
const result = await client.listResourceTemplates(void 0, options);
|
|
887
|
+
return result.resourceTemplates;
|
|
888
|
+
}
|
|
889
|
+
async readResource(uri, options) {
|
|
890
|
+
const client = this.requireClient();
|
|
891
|
+
return client.readResource({ uri }, options);
|
|
892
|
+
}
|
|
893
|
+
async subscribeResource(uri, options) {
|
|
894
|
+
const client = this.requireClient();
|
|
895
|
+
await client.subscribeResource({ uri }, options);
|
|
896
|
+
}
|
|
897
|
+
async unsubscribeResource(uri, options) {
|
|
898
|
+
const client = this.requireClient();
|
|
899
|
+
await client.unsubscribeResource({ uri }, options);
|
|
900
|
+
}
|
|
392
901
|
async disconnect() {
|
|
393
902
|
if (this.client === null) {
|
|
394
903
|
return;
|
|
@@ -396,37 +905,198 @@ var UpstreamClient = class {
|
|
|
396
905
|
await this.client.close();
|
|
397
906
|
this.client = null;
|
|
398
907
|
}
|
|
908
|
+
/**
|
|
909
|
+
* Sends `notifications/roots/list_changed` to the upstream, letting it know that
|
|
910
|
+
* the host's set of filesystem roots has changed.
|
|
911
|
+
*/
|
|
912
|
+
async sendRootsListChanged() {
|
|
913
|
+
const client = this.requireClient();
|
|
914
|
+
await client.sendRootsListChanged();
|
|
915
|
+
}
|
|
916
|
+
registerServerRequestHandlers(client) {
|
|
917
|
+
if (this.serverRequestHandlers.onCreateMessage !== void 0) {
|
|
918
|
+
client.setRequestHandler(
|
|
919
|
+
import_types.CreateMessageRequestSchema,
|
|
920
|
+
async (request, extra) => {
|
|
921
|
+
return this.serverRequestHandlers.onCreateMessage(request.params, {
|
|
922
|
+
signal: extra.signal
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
);
|
|
926
|
+
}
|
|
927
|
+
if (this.serverRequestHandlers.onElicitInput !== void 0) {
|
|
928
|
+
client.setRequestHandler(
|
|
929
|
+
import_types.ElicitRequestSchema,
|
|
930
|
+
async (request, extra) => {
|
|
931
|
+
return this.serverRequestHandlers.onElicitInput(request.params, {
|
|
932
|
+
signal: extra.signal
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
);
|
|
936
|
+
}
|
|
937
|
+
if (this.serverRequestHandlers.onListRoots !== void 0) {
|
|
938
|
+
client.setRequestHandler(
|
|
939
|
+
import_types.ListRootsRequestSchema,
|
|
940
|
+
async (request, extra) => {
|
|
941
|
+
return this.serverRequestHandlers.onListRoots(request.params, {
|
|
942
|
+
signal: extra.signal
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
requireClient() {
|
|
949
|
+
if (this.client === null) {
|
|
950
|
+
throw new Error("Client is not connected. Call connect() first.");
|
|
951
|
+
}
|
|
952
|
+
return this.client;
|
|
953
|
+
}
|
|
399
954
|
};
|
|
400
955
|
|
|
401
|
-
// src/proxy/
|
|
402
|
-
var
|
|
403
|
-
config;
|
|
956
|
+
// src/proxy/upstream-registry.ts
|
|
957
|
+
var UpstreamRegistry = class {
|
|
404
958
|
clients = /* @__PURE__ */ new Map();
|
|
405
|
-
|
|
406
|
-
constructor(config) {
|
|
407
|
-
this.config = config;
|
|
408
|
-
}
|
|
409
|
-
async connect() {
|
|
410
|
-
const groups = /* @__PURE__ */ new Map();
|
|
959
|
+
async connectAll(entries) {
|
|
411
960
|
try {
|
|
412
|
-
for (const [mcpName2,
|
|
961
|
+
for (const [mcpName2, config] of entries) {
|
|
413
962
|
const client = new UpstreamClient({
|
|
414
963
|
name: mcpName2,
|
|
415
|
-
transport,
|
|
416
|
-
onTransportError:
|
|
417
|
-
|
|
418
|
-
|
|
964
|
+
transport: config.transport,
|
|
965
|
+
onTransportError: config.onTransportError,
|
|
966
|
+
notifications: config.notifications,
|
|
967
|
+
serverRequests: config.serverRequests
|
|
419
968
|
});
|
|
420
969
|
await client.connect();
|
|
421
|
-
const tools = await client.listTools();
|
|
422
970
|
this.clients.set(mcpName2, client);
|
|
423
|
-
groups.set(mcpName2, tools);
|
|
424
971
|
}
|
|
425
972
|
} catch (error) {
|
|
426
973
|
await this.disconnectAll();
|
|
427
974
|
throw error;
|
|
428
975
|
}
|
|
429
|
-
|
|
976
|
+
}
|
|
977
|
+
get(mcpName2) {
|
|
978
|
+
return this.clients.get(mcpName2);
|
|
979
|
+
}
|
|
980
|
+
/**
|
|
981
|
+
* Returns the sole connected client. Used by single-MCP (`--`) mode where the
|
|
982
|
+
* Orchestrator guarantees there is exactly one upstream. Returns undefined when
|
|
983
|
+
* zero or more than one client is connected.
|
|
984
|
+
*/
|
|
985
|
+
sole() {
|
|
986
|
+
if (this.clients.size !== 1) return void 0;
|
|
987
|
+
return this.clients.values().next().value;
|
|
988
|
+
}
|
|
989
|
+
names() {
|
|
990
|
+
return [...this.clients.keys()];
|
|
991
|
+
}
|
|
992
|
+
entries() {
|
|
993
|
+
return this.clients.entries();
|
|
994
|
+
}
|
|
995
|
+
size() {
|
|
996
|
+
return this.clients.size;
|
|
997
|
+
}
|
|
998
|
+
async disconnectAll() {
|
|
999
|
+
const disconnections = [...this.clients.values()].map((client) => client.disconnect());
|
|
1000
|
+
await Promise.all(disconnections);
|
|
1001
|
+
this.clients.clear();
|
|
1002
|
+
}
|
|
1003
|
+
};
|
|
1004
|
+
|
|
1005
|
+
// src/proxy/orchestrator.ts
|
|
1006
|
+
var Orchestrator = class {
|
|
1007
|
+
config;
|
|
1008
|
+
registry = new UpstreamRegistry();
|
|
1009
|
+
toolsByMcp = /* @__PURE__ */ new Map();
|
|
1010
|
+
resourceRouter = null;
|
|
1011
|
+
promptRouter = null;
|
|
1012
|
+
toolCatalog = null;
|
|
1013
|
+
aggregatedCapabilities = null;
|
|
1014
|
+
serverRequestForwarders = {};
|
|
1015
|
+
forwarder;
|
|
1016
|
+
constructor(config) {
|
|
1017
|
+
if (!config.namespaced && config.mcps.size !== 1) {
|
|
1018
|
+
throw new Error(
|
|
1019
|
+
`Single-MCP (non-namespaced) mode requires exactly one upstream; got ${config.mcps.size}.`
|
|
1020
|
+
);
|
|
1021
|
+
}
|
|
1022
|
+
this.config = config;
|
|
1023
|
+
this.forwarder = new NotificationForwarder(
|
|
1024
|
+
this.registry,
|
|
1025
|
+
() => this.resourceRouter,
|
|
1026
|
+
() => this.promptRouter,
|
|
1027
|
+
this.toolsByMcp,
|
|
1028
|
+
(catalog) => {
|
|
1029
|
+
this.toolCatalog = catalog;
|
|
1030
|
+
},
|
|
1031
|
+
this.config.namespaced
|
|
1032
|
+
);
|
|
1033
|
+
}
|
|
1034
|
+
setNotificationHandlers(handlers) {
|
|
1035
|
+
this.forwarder.setHostHandlers(handlers);
|
|
1036
|
+
}
|
|
1037
|
+
setServerRequestForwarders(forwarders) {
|
|
1038
|
+
this.serverRequestForwarders = forwarders;
|
|
1039
|
+
}
|
|
1040
|
+
async connect() {
|
|
1041
|
+
const resourceRouter = new ResourceRouter([...this.config.mcps.keys()]);
|
|
1042
|
+
const promptRouter = new PromptRouter([...this.config.mcps.keys()]);
|
|
1043
|
+
const upstreamEntries = [
|
|
1044
|
+
...this.config.mcps
|
|
1045
|
+
].map(([mcpName2, { transport }]) => [
|
|
1046
|
+
mcpName2,
|
|
1047
|
+
{
|
|
1048
|
+
transport,
|
|
1049
|
+
onTransportError: (error) => {
|
|
1050
|
+
this.config.onTransportError?.(mcpName2, error);
|
|
1051
|
+
},
|
|
1052
|
+
notifications: {
|
|
1053
|
+
onToolsListChanged: () => this.forwarder.handleToolsListChanged(mcpName2),
|
|
1054
|
+
onResourcesListChanged: () => this.forwarder.handleResourcesListChanged(mcpName2),
|
|
1055
|
+
onResourceUpdated: (params) => this.forwarder.handleResourceUpdated(params),
|
|
1056
|
+
onPromptsListChanged: () => this.forwarder.handlePromptsListChanged(mcpName2),
|
|
1057
|
+
onLogMessage: (params) => this.forwarder.handleLogMessage(mcpName2, params)
|
|
1058
|
+
},
|
|
1059
|
+
serverRequests: {
|
|
1060
|
+
onCreateMessage: (params, opts) => this.forwardCreateMessage(params, opts),
|
|
1061
|
+
onElicitInput: (params, opts) => this.forwardElicitInput(params, opts),
|
|
1062
|
+
onListRoots: (params, opts) => this.forwardListRoots(params, opts)
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
]);
|
|
1066
|
+
await this.registry.connectAll(upstreamEntries);
|
|
1067
|
+
const capabilityList = [];
|
|
1068
|
+
this.toolsByMcp.clear();
|
|
1069
|
+
for (const [mcpName2, client] of this.registry.entries()) {
|
|
1070
|
+
const caps = client.getCapabilities();
|
|
1071
|
+
capabilityList.push(caps);
|
|
1072
|
+
const tools = await client.listTools();
|
|
1073
|
+
this.toolsByMcp.set(mcpName2, tools);
|
|
1074
|
+
if (caps?.resources !== void 0) {
|
|
1075
|
+
const [resources, templates] = await Promise.all([
|
|
1076
|
+
client.listResources().catch(() => []),
|
|
1077
|
+
client.listResourceTemplates().catch(() => [])
|
|
1078
|
+
]);
|
|
1079
|
+
resourceRouter.setResources(mcpName2, resources);
|
|
1080
|
+
resourceRouter.setTemplates(mcpName2, templates);
|
|
1081
|
+
}
|
|
1082
|
+
if (caps?.prompts !== void 0) {
|
|
1083
|
+
const prompts = await client.listPrompts().catch(() => []);
|
|
1084
|
+
promptRouter.setPrompts(mcpName2, prompts);
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
this.toolCatalog = this.config.namespaced ? ToolCatalog.fromGrouped(this.toolsByMcp) : ToolCatalog.fromFlat([...this.toolsByMcp.values()][0] ?? []);
|
|
1088
|
+
this.aggregatedCapabilities = aggregateCapabilities(capabilityList);
|
|
1089
|
+
this.resourceRouter = resourceRouter;
|
|
1090
|
+
this.promptRouter = promptRouter;
|
|
1091
|
+
logCollisions(resourceRouter, promptRouter);
|
|
1092
|
+
}
|
|
1093
|
+
async disconnectAll() {
|
|
1094
|
+
await this.registry.disconnectAll();
|
|
1095
|
+
this.toolsByMcp.clear();
|
|
1096
|
+
this.toolCatalog = null;
|
|
1097
|
+
this.aggregatedCapabilities = null;
|
|
1098
|
+
this.resourceRouter = null;
|
|
1099
|
+
this.promptRouter = null;
|
|
430
1100
|
}
|
|
431
1101
|
get catalog() {
|
|
432
1102
|
if (this.toolCatalog === null) {
|
|
@@ -434,168 +1104,577 @@ var Orchestrator = class {
|
|
|
434
1104
|
}
|
|
435
1105
|
return this.toolCatalog;
|
|
436
1106
|
}
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
1107
|
+
get capabilities() {
|
|
1108
|
+
if (this.aggregatedCapabilities === null) {
|
|
1109
|
+
throw new Error("Orchestrator is not connected. Call connect() first.");
|
|
1110
|
+
}
|
|
1111
|
+
return this.aggregatedCapabilities;
|
|
1112
|
+
}
|
|
1113
|
+
// === Forward-direction request routing ===
|
|
1114
|
+
async callTool(displayName, input, options) {
|
|
1115
|
+
if (this.config.namespaced) {
|
|
1116
|
+
const { mcpName: mcpName2, toolName } = splitNamespacedName(displayName, this.registry.names());
|
|
1117
|
+
return this.requireClient(mcpName2, "tool").callTool(toolName, input, options);
|
|
1118
|
+
}
|
|
1119
|
+
const sole = this.registry.sole();
|
|
1120
|
+
if (sole === void 0) {
|
|
1121
|
+
throw new Error("Orchestrator is not connected. Call connect() first.");
|
|
1122
|
+
}
|
|
1123
|
+
return sole.callTool(displayName, input, options);
|
|
1124
|
+
}
|
|
1125
|
+
listResources() {
|
|
1126
|
+
return this.requireResourceRouter().aggregatedResources();
|
|
1127
|
+
}
|
|
1128
|
+
listResourceTemplates() {
|
|
1129
|
+
return this.requireResourceRouter().aggregatedTemplates();
|
|
1130
|
+
}
|
|
1131
|
+
async readResource(uri, options) {
|
|
1132
|
+
return this.resolveResourceOwner(uri).readResource(uri, options);
|
|
1133
|
+
}
|
|
1134
|
+
async subscribeResource(uri, options) {
|
|
1135
|
+
await this.resolveResourceOwner(uri).subscribeResource(uri, options);
|
|
1136
|
+
}
|
|
1137
|
+
async unsubscribeResource(uri, options) {
|
|
1138
|
+
await this.resolveResourceOwner(uri).unsubscribeResource(uri, options);
|
|
1139
|
+
}
|
|
1140
|
+
listPrompts() {
|
|
1141
|
+
return this.requirePromptRouter().aggregatedPrompts();
|
|
1142
|
+
}
|
|
1143
|
+
async getPrompt(name, args, options) {
|
|
1144
|
+
return this.resolvePromptOwner(name).getPrompt(name, args, options);
|
|
1145
|
+
}
|
|
1146
|
+
async complete(params, options) {
|
|
1147
|
+
const client = this.resolveCompletionTarget(params.ref);
|
|
1148
|
+
return client.complete(params, options);
|
|
1149
|
+
}
|
|
1150
|
+
// === Broadcasts ===
|
|
1151
|
+
/**
|
|
1152
|
+
* Broadcasts a `logging/setLevel` request to every upstream advertising the logging
|
|
1153
|
+
* capability. Errors from individual upstreams are swallowed so a single misbehaving
|
|
1154
|
+
* upstream cannot break the broadcast for others; failures are written to stderr.
|
|
1155
|
+
*/
|
|
1156
|
+
async setLoggingLevel(level, options) {
|
|
1157
|
+
await this.broadcastAsync(
|
|
1158
|
+
(client) => client.getCapabilities()?.logging !== void 0 ? client.setLoggingLevel(level, options) : Promise.resolve(),
|
|
1159
|
+
"setLoggingLevel"
|
|
1160
|
+
);
|
|
1161
|
+
}
|
|
1162
|
+
/**
|
|
1163
|
+
* Broadcasts `notifications/roots/list_changed` to every connected upstream — the
|
|
1164
|
+
* proxy declares roots capability uniformly to all upstreams so every one of them
|
|
1165
|
+
* may have requested roots and needs to know the list changed.
|
|
1166
|
+
*/
|
|
1167
|
+
async broadcastRootsListChanged() {
|
|
1168
|
+
await this.broadcastAsync((client) => client.sendRootsListChanged(), "sendRootsListChanged");
|
|
1169
|
+
}
|
|
1170
|
+
// === Server-initiated request forwarders (upstream → host) ===
|
|
1171
|
+
async forwardCreateMessage(params, options) {
|
|
1172
|
+
const handler = this.serverRequestForwarders.onCreateMessage;
|
|
1173
|
+
if (handler === void 0) {
|
|
1174
|
+
throw new Error("Proxy does not support sampling: host has not registered a handler.");
|
|
1175
|
+
}
|
|
1176
|
+
return handler(params, options);
|
|
1177
|
+
}
|
|
1178
|
+
async forwardElicitInput(params, options) {
|
|
1179
|
+
const handler = this.serverRequestForwarders.onElicitInput;
|
|
1180
|
+
if (handler === void 0) {
|
|
1181
|
+
throw new Error("Proxy does not support elicitation: host has not registered a handler.");
|
|
1182
|
+
}
|
|
1183
|
+
return handler(params, options);
|
|
1184
|
+
}
|
|
1185
|
+
async forwardListRoots(params, options) {
|
|
1186
|
+
const handler = this.serverRequestForwarders.onListRoots;
|
|
1187
|
+
if (handler === void 0) {
|
|
1188
|
+
throw new Error("Proxy does not support roots: host has not registered a handler.");
|
|
1189
|
+
}
|
|
1190
|
+
return handler(params, options);
|
|
1191
|
+
}
|
|
1192
|
+
// === Internal helpers ===
|
|
1193
|
+
requireResourceRouter() {
|
|
1194
|
+
if (this.resourceRouter === null) {
|
|
1195
|
+
throw new Error("Orchestrator is not connected. Call connect() first.");
|
|
1196
|
+
}
|
|
1197
|
+
return this.resourceRouter;
|
|
1198
|
+
}
|
|
1199
|
+
requirePromptRouter() {
|
|
1200
|
+
if (this.promptRouter === null) {
|
|
1201
|
+
throw new Error("Orchestrator is not connected. Call connect() first.");
|
|
1202
|
+
}
|
|
1203
|
+
return this.promptRouter;
|
|
1204
|
+
}
|
|
1205
|
+
resolveResourceOwner(uri) {
|
|
1206
|
+
const owner = this.requireResourceRouter().ownerOf(uri);
|
|
1207
|
+
if (owner === void 0) {
|
|
1208
|
+
throw new Error(`Unknown resource URI: "${uri}". No upstream MCP advertises it.`);
|
|
1209
|
+
}
|
|
1210
|
+
return this.requireClient(owner, "resource");
|
|
1211
|
+
}
|
|
1212
|
+
resolvePromptOwner(name) {
|
|
1213
|
+
const owner = this.requirePromptRouter().ownerOf(name);
|
|
1214
|
+
if (owner === void 0) {
|
|
1215
|
+
throw new Error(`Unknown prompt: "${name}". No upstream MCP advertises it.`);
|
|
443
1216
|
}
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
1217
|
+
return this.requireClient(owner, "prompt");
|
|
1218
|
+
}
|
|
1219
|
+
resolveCompletionTarget(ref) {
|
|
1220
|
+
if (ref.type === "ref/prompt") {
|
|
1221
|
+
return this.resolvePromptOwner(ref.name);
|
|
1222
|
+
}
|
|
1223
|
+
if (ref.type === "ref/resource") {
|
|
1224
|
+
return this.resolveResourceOwner(ref.uri);
|
|
1225
|
+
}
|
|
1226
|
+
const unknownRef = ref;
|
|
1227
|
+
throw new Error(`Unsupported completion ref type: "${unknownRef.type}"`);
|
|
1228
|
+
}
|
|
1229
|
+
requireClient(mcpName2, role) {
|
|
1230
|
+
const client = this.registry.get(mcpName2);
|
|
447
1231
|
if (client === void 0) {
|
|
448
|
-
|
|
449
|
-
throw new Error(`Unknown MCP: "${mcpName2}". Available MCPs: ${available}`);
|
|
1232
|
+
throw new Error(`Internal error: ${role} owner "${mcpName2}" has no connected client.`);
|
|
450
1233
|
}
|
|
451
|
-
return client
|
|
1234
|
+
return client;
|
|
452
1235
|
}
|
|
453
|
-
async
|
|
454
|
-
const
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
1236
|
+
async broadcastAsync(action, label) {
|
|
1237
|
+
const targets = [];
|
|
1238
|
+
for (const [mcpName2, client] of this.registry.entries()) {
|
|
1239
|
+
targets.push(
|
|
1240
|
+
action(client).catch((error) => {
|
|
1241
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1242
|
+
import_node_process4.default.stderr.write(`dynmcp: ${label} failed for "${mcpName2}": ${message}
|
|
1243
|
+
`);
|
|
1244
|
+
})
|
|
1245
|
+
);
|
|
1246
|
+
}
|
|
1247
|
+
await Promise.all(targets);
|
|
458
1248
|
}
|
|
459
1249
|
};
|
|
1250
|
+
function splitNamespacedName(namespacedName, knownMcpNames) {
|
|
1251
|
+
const separatorIndex = namespacedName.indexOf("/");
|
|
1252
|
+
if (separatorIndex === -1) {
|
|
1253
|
+
throw new Error(
|
|
1254
|
+
`Invalid namespaced tool name: "${namespacedName}". Expected format: "mcpName/toolName".`
|
|
1255
|
+
);
|
|
1256
|
+
}
|
|
1257
|
+
const mcpName2 = namespacedName.slice(0, separatorIndex);
|
|
1258
|
+
const toolName = namespacedName.slice(separatorIndex + 1);
|
|
1259
|
+
if (!knownMcpNames.includes(mcpName2)) {
|
|
1260
|
+
const available = [...knownMcpNames].sort().join(", ");
|
|
1261
|
+
throw new Error(`Unknown MCP: "${mcpName2}". Available MCPs: ${available}`);
|
|
1262
|
+
}
|
|
1263
|
+
return { mcpName: mcpName2, toolName };
|
|
1264
|
+
}
|
|
1265
|
+
function logCollisions(resourceRouter, promptRouter) {
|
|
1266
|
+
for (const collision of resourceRouter.collisions()) {
|
|
1267
|
+
import_node_process4.default.stderr.write(
|
|
1268
|
+
`dynmcp: resource URI collision: "${collision.uri}" is provided by "${collision.chosen}" and "${collision.shadowed}"; routing to "${collision.chosen}".
|
|
1269
|
+
`
|
|
1270
|
+
);
|
|
1271
|
+
}
|
|
1272
|
+
for (const collision of promptRouter.collisions()) {
|
|
1273
|
+
import_node_process4.default.stderr.write(
|
|
1274
|
+
`dynmcp: prompt name collision: "${collision.name}" is provided by "${collision.chosen}" and "${collision.shadowed}"; routing to "${collision.chosen}".
|
|
1275
|
+
`
|
|
1276
|
+
);
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
460
1279
|
|
|
461
1280
|
// src/proxy/server.ts
|
|
462
|
-
var
|
|
463
|
-
var
|
|
1281
|
+
var import_node_process5 = __toESM(require("process"), 1);
|
|
1282
|
+
var import_server = require("@modelcontextprotocol/sdk/server/index.js");
|
|
1283
|
+
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
1284
|
+
var import_types2 = require("@modelcontextprotocol/sdk/types.js");
|
|
464
1285
|
var import_zod3 = require("zod");
|
|
1286
|
+
var DISCOVER_TOOL_NAME = "discover_tool";
|
|
1287
|
+
var USE_TOOL_NAME = "use_tool";
|
|
1288
|
+
var USE_TOOL_DESCRIPTION = "Use a tool that was previously discovered with the discover_tool tool.";
|
|
1289
|
+
var DISCOVER_TOOL_INPUT_SCHEMA = {
|
|
1290
|
+
type: "object",
|
|
1291
|
+
properties: {
|
|
1292
|
+
tool_name: { type: "string" }
|
|
1293
|
+
},
|
|
1294
|
+
required: ["tool_name"]
|
|
1295
|
+
};
|
|
1296
|
+
var USE_TOOL_INPUT_SCHEMA = {
|
|
1297
|
+
type: "object",
|
|
1298
|
+
properties: {
|
|
1299
|
+
tool_name: { type: "string" },
|
|
1300
|
+
tool_input: { type: "object", additionalProperties: true, default: {} }
|
|
1301
|
+
},
|
|
1302
|
+
required: ["tool_name"]
|
|
1303
|
+
};
|
|
1304
|
+
var DiscoverToolArgsSchema = import_zod3.z.object({ tool_name: import_zod3.z.string() });
|
|
1305
|
+
var UseToolArgsSchema = import_zod3.z.object({
|
|
1306
|
+
tool_name: import_zod3.z.string(),
|
|
1307
|
+
tool_input: import_zod3.z.record(import_zod3.z.string(), import_zod3.z.unknown()).default({})
|
|
1308
|
+
});
|
|
465
1309
|
var ProxyServer = class {
|
|
466
1310
|
catalog;
|
|
467
1311
|
callTool;
|
|
468
|
-
|
|
1312
|
+
capabilities;
|
|
1313
|
+
resources;
|
|
1314
|
+
prompts;
|
|
1315
|
+
complete;
|
|
1316
|
+
setLoggingLevelCallback;
|
|
1317
|
+
onRootsListChangedCallback;
|
|
1318
|
+
sdkServer = null;
|
|
1319
|
+
constructor({
|
|
1320
|
+
catalog,
|
|
1321
|
+
callTool,
|
|
1322
|
+
capabilities,
|
|
1323
|
+
resources,
|
|
1324
|
+
prompts,
|
|
1325
|
+
complete,
|
|
1326
|
+
setLoggingLevel,
|
|
1327
|
+
onRootsListChanged
|
|
1328
|
+
}) {
|
|
469
1329
|
this.catalog = catalog;
|
|
470
1330
|
this.callTool = callTool;
|
|
1331
|
+
this.capabilities = capabilities;
|
|
1332
|
+
this.resources = resources;
|
|
1333
|
+
this.prompts = prompts;
|
|
1334
|
+
this.complete = complete;
|
|
1335
|
+
this.setLoggingLevelCallback = setLoggingLevel;
|
|
1336
|
+
this.onRootsListChangedCallback = onRootsListChanged;
|
|
1337
|
+
}
|
|
1338
|
+
buildServer() {
|
|
1339
|
+
const server = new import_server.Server(
|
|
1340
|
+
{
|
|
1341
|
+
name: "dynamic-discovery-mcp",
|
|
1342
|
+
version: package_default.version
|
|
1343
|
+
},
|
|
1344
|
+
{
|
|
1345
|
+
capabilities: this.capabilities
|
|
1346
|
+
}
|
|
1347
|
+
);
|
|
1348
|
+
this.registerToolHandlers(server);
|
|
1349
|
+
if (this.capabilities.resources !== void 0 && this.resources !== void 0) {
|
|
1350
|
+
this.registerResourceHandlers(server, this.resources);
|
|
1351
|
+
}
|
|
1352
|
+
if (this.capabilities.prompts !== void 0 && this.prompts !== void 0) {
|
|
1353
|
+
this.registerPromptHandlers(server, this.prompts);
|
|
1354
|
+
}
|
|
1355
|
+
if (this.capabilities.completions !== void 0 && this.complete !== void 0) {
|
|
1356
|
+
this.registerCompletionHandler(server, this.complete);
|
|
1357
|
+
}
|
|
1358
|
+
if (this.capabilities.logging !== void 0 && this.setLoggingLevelCallback !== void 0) {
|
|
1359
|
+
this.registerLoggingHandler(server, this.setLoggingLevelCallback);
|
|
1360
|
+
}
|
|
1361
|
+
if (this.onRootsListChangedCallback !== void 0) {
|
|
1362
|
+
const callback = this.onRootsListChangedCallback;
|
|
1363
|
+
server.setNotificationHandler(import_types2.RootsListChangedNotificationSchema, async () => {
|
|
1364
|
+
await callback();
|
|
1365
|
+
});
|
|
1366
|
+
}
|
|
1367
|
+
this.sdkServer = server;
|
|
1368
|
+
return server;
|
|
1369
|
+
}
|
|
1370
|
+
/**
|
|
1371
|
+
* Forwards an upstream-initiated `sampling/createMessage` request to the host. The
|
|
1372
|
+
* upstream's abort signal is threaded through so cancellation by the upstream
|
|
1373
|
+
* propagates to the host.
|
|
1374
|
+
*/
|
|
1375
|
+
async forwardCreateMessage(params, options) {
|
|
1376
|
+
const server = this.requireSdkServer();
|
|
1377
|
+
return server.createMessage(params, options);
|
|
1378
|
+
}
|
|
1379
|
+
/**
|
|
1380
|
+
* Forwards an upstream-initiated `elicitation/create` request to the host.
|
|
1381
|
+
*/
|
|
1382
|
+
async forwardElicitInput(params, options) {
|
|
1383
|
+
const server = this.requireSdkServer();
|
|
1384
|
+
return server.elicitInput(params, options);
|
|
1385
|
+
}
|
|
1386
|
+
/**
|
|
1387
|
+
* Forwards an upstream-initiated `roots/list` request to the host.
|
|
1388
|
+
*/
|
|
1389
|
+
async forwardListRoots(params, options) {
|
|
1390
|
+
const server = this.requireSdkServer();
|
|
1391
|
+
return server.listRoots(params, options);
|
|
1392
|
+
}
|
|
1393
|
+
requireSdkServer() {
|
|
1394
|
+
if (this.sdkServer === null) {
|
|
1395
|
+
throw new Error("ProxyServer is not built. Call buildServer() before forwarding requests.");
|
|
1396
|
+
}
|
|
1397
|
+
return this.sdkServer;
|
|
471
1398
|
}
|
|
472
1399
|
async start() {
|
|
473
|
-
const server =
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
1400
|
+
const server = this.buildServer();
|
|
1401
|
+
const transport = new import_stdio.StdioServerTransport();
|
|
1402
|
+
import_node_process5.default.stderr.write("Starting dynamic-discovery-mcp server over stdio\n");
|
|
1403
|
+
await server.connect(transport);
|
|
1404
|
+
}
|
|
1405
|
+
/**
|
|
1406
|
+
* Notifies the host that the discover_tool description has changed because an upstream
|
|
1407
|
+
* emitted `notifications/tools/list_changed`. The host should re-fetch the tools list
|
|
1408
|
+
* to pick up the regenerated catalog. Silently no-ops if `buildServer()` has not been
|
|
1409
|
+
* called yet.
|
|
1410
|
+
*/
|
|
1411
|
+
async sendToolListChanged() {
|
|
1412
|
+
if (this.sdkServer !== null) {
|
|
1413
|
+
await this.sdkServer.sendToolListChanged();
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
/**
|
|
1417
|
+
* Notifies the host that the proxy's aggregated resource list has changed. Silently
|
|
1418
|
+
* no-ops if `buildServer()` has not been called yet. Errors propagate.
|
|
1419
|
+
*/
|
|
1420
|
+
async sendResourceListChanged() {
|
|
1421
|
+
if (this.sdkServer !== null) {
|
|
1422
|
+
await this.sdkServer.sendResourceListChanged();
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
/**
|
|
1426
|
+
* Notifies the host that a specific subscribed resource has changed. Silently no-ops
|
|
1427
|
+
* if `buildServer()` has not been called yet.
|
|
1428
|
+
*/
|
|
1429
|
+
async sendResourceUpdated(params) {
|
|
1430
|
+
if (this.sdkServer !== null) {
|
|
1431
|
+
await this.sdkServer.sendResourceUpdated(params);
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
/**
|
|
1435
|
+
* Notifies the host that the proxy's aggregated prompt list has changed. Silently
|
|
1436
|
+
* no-ops if `buildServer()` has not been called yet.
|
|
1437
|
+
*/
|
|
1438
|
+
async sendPromptListChanged() {
|
|
1439
|
+
if (this.sdkServer !== null) {
|
|
1440
|
+
await this.sdkServer.sendPromptListChanged();
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
registerToolHandlers(server) {
|
|
1444
|
+
server.setRequestHandler(import_types2.ListToolsRequestSchema, async () => ({
|
|
1445
|
+
tools: [
|
|
1446
|
+
{
|
|
1447
|
+
name: DISCOVER_TOOL_NAME,
|
|
1448
|
+
description: this.catalog().discoverToolDescription,
|
|
1449
|
+
inputSchema: DISCOVER_TOOL_INPUT_SCHEMA
|
|
1450
|
+
},
|
|
1451
|
+
{
|
|
1452
|
+
name: USE_TOOL_NAME,
|
|
1453
|
+
description: USE_TOOL_DESCRIPTION,
|
|
1454
|
+
inputSchema: USE_TOOL_INPUT_SCHEMA
|
|
1455
|
+
}
|
|
1456
|
+
]
|
|
1457
|
+
}));
|
|
1458
|
+
server.setRequestHandler(
|
|
1459
|
+
import_types2.CallToolRequestSchema,
|
|
1460
|
+
async (request, extra) => {
|
|
1461
|
+
const { name, arguments: rawArgs } = request.params;
|
|
1462
|
+
const catalog = this.catalog();
|
|
1463
|
+
if (name === DISCOVER_TOOL_NAME) {
|
|
1464
|
+
const args = DiscoverToolArgsSchema.parse(rawArgs ?? {});
|
|
1465
|
+
return {
|
|
1466
|
+
content: [{ type: "text", text: catalog.getToolDetails(args.tool_name) }]
|
|
1467
|
+
};
|
|
1468
|
+
}
|
|
1469
|
+
if (name === USE_TOOL_NAME) {
|
|
1470
|
+
const args = UseToolArgsSchema.parse(rawArgs ?? {});
|
|
1471
|
+
if (!catalog.tools.has(args.tool_name)) {
|
|
1472
|
+
return {
|
|
1473
|
+
content: [{ type: "text", text: catalog.getToolDetails(args.tool_name) }]
|
|
1474
|
+
};
|
|
1475
|
+
}
|
|
1476
|
+
return await this.callTool(args.tool_name, args.tool_input, { signal: extra.signal });
|
|
1477
|
+
}
|
|
1478
|
+
return {
|
|
1479
|
+
isError: true,
|
|
1480
|
+
content: [{ type: "text", text: `Unknown tool: "${name}"` }]
|
|
1481
|
+
};
|
|
483
1482
|
}
|
|
1483
|
+
);
|
|
1484
|
+
}
|
|
1485
|
+
registerResourceHandlers(server, callbacks) {
|
|
1486
|
+
server.setRequestHandler(
|
|
1487
|
+
import_types2.ListResourcesRequestSchema,
|
|
1488
|
+
async () => ({
|
|
1489
|
+
resources: callbacks.listResources()
|
|
1490
|
+
})
|
|
1491
|
+
);
|
|
1492
|
+
server.setRequestHandler(
|
|
1493
|
+
import_types2.ListResourceTemplatesRequestSchema,
|
|
1494
|
+
async () => ({
|
|
1495
|
+
resourceTemplates: callbacks.listResourceTemplates()
|
|
1496
|
+
})
|
|
1497
|
+
);
|
|
1498
|
+
server.setRequestHandler(
|
|
1499
|
+
import_types2.ReadResourceRequestSchema,
|
|
1500
|
+
async (request, extra) => {
|
|
1501
|
+
return callbacks.readResource(request.params.uri, { signal: extra.signal });
|
|
1502
|
+
}
|
|
1503
|
+
);
|
|
1504
|
+
server.setRequestHandler(import_types2.SubscribeRequestSchema, async (request, extra) => {
|
|
1505
|
+
await callbacks.subscribeResource(request.params.uri, { signal: extra.signal });
|
|
1506
|
+
return {};
|
|
484
1507
|
});
|
|
485
|
-
server.
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
1508
|
+
server.setRequestHandler(import_types2.UnsubscribeRequestSchema, async (request, extra) => {
|
|
1509
|
+
await callbacks.unsubscribeResource(request.params.uri, { signal: extra.signal });
|
|
1510
|
+
return {};
|
|
1511
|
+
});
|
|
1512
|
+
}
|
|
1513
|
+
registerPromptHandlers(server, callbacks) {
|
|
1514
|
+
server.setRequestHandler(
|
|
1515
|
+
import_types2.ListPromptsRequestSchema,
|
|
1516
|
+
async () => ({
|
|
1517
|
+
prompts: callbacks.listPrompts()
|
|
1518
|
+
})
|
|
1519
|
+
);
|
|
1520
|
+
server.setRequestHandler(
|
|
1521
|
+
import_types2.GetPromptRequestSchema,
|
|
1522
|
+
async (request, extra) => {
|
|
1523
|
+
return callbacks.getPrompt(request.params.name, request.params.arguments, {
|
|
1524
|
+
signal: extra.signal
|
|
1525
|
+
});
|
|
498
1526
|
}
|
|
1527
|
+
);
|
|
1528
|
+
}
|
|
1529
|
+
registerCompletionHandler(server, callback) {
|
|
1530
|
+
server.setRequestHandler(
|
|
1531
|
+
import_types2.CompleteRequestSchema,
|
|
1532
|
+
async (request, extra) => {
|
|
1533
|
+
return callback(request.params, { signal: extra.signal });
|
|
1534
|
+
}
|
|
1535
|
+
);
|
|
1536
|
+
}
|
|
1537
|
+
registerLoggingHandler(server, callback) {
|
|
1538
|
+
server.setRequestHandler(import_types2.SetLevelRequestSchema, async (request, extra) => {
|
|
1539
|
+
await callback(request.params.level, { signal: extra.signal });
|
|
1540
|
+
return {};
|
|
499
1541
|
});
|
|
500
|
-
|
|
501
|
-
|
|
1542
|
+
}
|
|
1543
|
+
/**
|
|
1544
|
+
* Forwards a log message from an upstream MCP to the host. Silently no-ops if
|
|
1545
|
+
* `buildServer()` has not been called yet.
|
|
1546
|
+
*/
|
|
1547
|
+
async sendLoggingMessage(params) {
|
|
1548
|
+
if (this.sdkServer !== null) {
|
|
1549
|
+
await this.sdkServer.sendLoggingMessage(params);
|
|
1550
|
+
}
|
|
502
1551
|
}
|
|
503
1552
|
};
|
|
504
1553
|
|
|
505
|
-
// src/proxy/
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
upstreamClient.disconnect().catch((error) => {
|
|
522
|
-
import_node_process3.default.stderr.write(
|
|
523
|
-
`dynmcp: error during disconnect: ${error instanceof Error ? error.message : String(error)}
|
|
524
|
-
`
|
|
1554
|
+
// src/proxy/transport-factory.ts
|
|
1555
|
+
var import_stdio2 = require("@modelcontextprotocol/sdk/client/stdio.js");
|
|
1556
|
+
var import_streamableHttp = require("@modelcontextprotocol/sdk/client/streamableHttp.js");
|
|
1557
|
+
var import_sse = require("@modelcontextprotocol/sdk/client/sse.js");
|
|
1558
|
+
function createTransport(config) {
|
|
1559
|
+
switch (config.transport) {
|
|
1560
|
+
case "stdio":
|
|
1561
|
+
return new import_stdio2.StdioClientTransport({
|
|
1562
|
+
command: config.command,
|
|
1563
|
+
args: config.args,
|
|
1564
|
+
env: config.env
|
|
1565
|
+
});
|
|
1566
|
+
case "streamable-http":
|
|
1567
|
+
return new import_streamableHttp.StreamableHTTPClientTransport(
|
|
1568
|
+
new URL(config.url),
|
|
1569
|
+
config.headers ? { requestInit: { headers: config.headers } } : void 0
|
|
525
1570
|
);
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
let tools;
|
|
536
|
-
try {
|
|
537
|
-
tools = await upstreamClient.listTools();
|
|
538
|
-
} catch (error) {
|
|
539
|
-
import_node_process3.default.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
|
|
540
|
-
`);
|
|
541
|
-
import_node_process3.default.exit(1);
|
|
1571
|
+
case "sse":
|
|
1572
|
+
return new import_sse.SSEClientTransport(
|
|
1573
|
+
new URL(config.url),
|
|
1574
|
+
config.headers ? { requestInit: { headers: config.headers } } : void 0
|
|
1575
|
+
);
|
|
1576
|
+
default: {
|
|
1577
|
+
const _exhaustive = config;
|
|
1578
|
+
return _exhaustive;
|
|
1579
|
+
}
|
|
542
1580
|
}
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
// src/proxy/index.ts
|
|
1584
|
+
var SINGLE_MCP_NAME = "__default__";
|
|
1585
|
+
async function startProxy(command, args) {
|
|
1586
|
+
const transport = new import_stdio3.StdioClientTransport({ command, args });
|
|
1587
|
+
const mcps = /* @__PURE__ */ new Map([[SINGLE_MCP_NAME, { transport }]]);
|
|
1588
|
+
const orchestrator = buildOrchestrator({
|
|
1589
|
+
mcps,
|
|
1590
|
+
namespaced: false,
|
|
1591
|
+
transportErrorPrefix: () => "Upstream MCP"
|
|
547
1592
|
});
|
|
548
|
-
|
|
549
|
-
import_node_process3.default.on("SIGTERM", () => shutdown(0));
|
|
550
|
-
import_node_process3.default.stdin.on("end", () => shutdown(0));
|
|
551
|
-
import_node_process3.default.stdin.on("close", () => shutdown(0));
|
|
552
|
-
try {
|
|
553
|
-
await proxyServer.start();
|
|
554
|
-
} catch (error) {
|
|
555
|
-
shutdown(1);
|
|
556
|
-
throw error;
|
|
557
|
-
}
|
|
1593
|
+
await runProxy(orchestrator);
|
|
558
1594
|
}
|
|
559
|
-
async function startProxyFromConfig(
|
|
560
|
-
|
|
561
|
-
const config = loadConfig(configPath);
|
|
1595
|
+
async function startProxyFromConfig(options = {}) {
|
|
1596
|
+
const config = loadConfig(options);
|
|
562
1597
|
const mcps = /* @__PURE__ */ new Map();
|
|
563
1598
|
for (const [name, entry] of Object.entries(config.mcp)) {
|
|
564
1599
|
mcps.set(name, { transport: createTransport(entry) });
|
|
565
1600
|
}
|
|
566
|
-
const orchestrator =
|
|
1601
|
+
const orchestrator = buildOrchestrator({
|
|
567
1602
|
mcps,
|
|
1603
|
+
namespaced: true,
|
|
1604
|
+
transportErrorPrefix: (mcpName2) => `Upstream MCP "${mcpName2}"`
|
|
1605
|
+
});
|
|
1606
|
+
await runProxy(orchestrator);
|
|
1607
|
+
}
|
|
1608
|
+
var activeShutdown = { shutdown: null };
|
|
1609
|
+
function buildOrchestrator(params) {
|
|
1610
|
+
return new Orchestrator({
|
|
1611
|
+
mcps: params.mcps,
|
|
1612
|
+
namespaced: params.namespaced,
|
|
568
1613
|
onTransportError: (mcpName2, error) => {
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
1614
|
+
import_node_process6.default.stderr.write(
|
|
1615
|
+
`${params.transportErrorPrefix(mcpName2)} transport error: ${error.message}
|
|
1616
|
+
`
|
|
1617
|
+
);
|
|
1618
|
+
activeShutdown.shutdown?.(1);
|
|
572
1619
|
}
|
|
573
1620
|
});
|
|
1621
|
+
}
|
|
1622
|
+
async function runProxy(orchestrator) {
|
|
1623
|
+
let isShuttingDown = false;
|
|
574
1624
|
const shutdown = (exitCode) => {
|
|
575
1625
|
if (isShuttingDown) return;
|
|
576
1626
|
isShuttingDown = true;
|
|
577
1627
|
orchestrator.disconnectAll().catch((error) => {
|
|
578
|
-
|
|
1628
|
+
import_node_process6.default.stderr.write(
|
|
579
1629
|
`dynmcp: error during disconnect: ${error instanceof Error ? error.message : String(error)}
|
|
580
1630
|
`
|
|
581
1631
|
);
|
|
582
|
-
}).finally(() =>
|
|
1632
|
+
}).finally(() => import_node_process6.default.exit(exitCode));
|
|
583
1633
|
};
|
|
1634
|
+
activeShutdown.shutdown = shutdown;
|
|
584
1635
|
try {
|
|
585
1636
|
await orchestrator.connect();
|
|
586
1637
|
} catch (error) {
|
|
587
|
-
|
|
1638
|
+
import_node_process6.default.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
|
|
588
1639
|
`);
|
|
589
|
-
|
|
1640
|
+
import_node_process6.default.exit(1);
|
|
1641
|
+
return;
|
|
590
1642
|
}
|
|
591
1643
|
const proxyServer = new ProxyServer({
|
|
592
|
-
catalog: orchestrator.catalog,
|
|
593
|
-
|
|
1644
|
+
catalog: () => orchestrator.catalog,
|
|
1645
|
+
capabilities: orchestrator.capabilities,
|
|
1646
|
+
callTool: (name, input, options) => orchestrator.callTool(name, input, options),
|
|
1647
|
+
resources: orchestrator.capabilities.resources !== void 0 ? {
|
|
1648
|
+
listResources: () => orchestrator.listResources(),
|
|
1649
|
+
listResourceTemplates: () => orchestrator.listResourceTemplates(),
|
|
1650
|
+
readResource: (uri, options) => orchestrator.readResource(uri, options),
|
|
1651
|
+
subscribeResource: (uri, options) => orchestrator.subscribeResource(uri, options),
|
|
1652
|
+
unsubscribeResource: (uri, options) => orchestrator.unsubscribeResource(uri, options)
|
|
1653
|
+
} : void 0,
|
|
1654
|
+
prompts: orchestrator.capabilities.prompts !== void 0 ? {
|
|
1655
|
+
listPrompts: () => orchestrator.listPrompts(),
|
|
1656
|
+
getPrompt: (name, args, options) => orchestrator.getPrompt(name, args, options)
|
|
1657
|
+
} : void 0,
|
|
1658
|
+
complete: orchestrator.capabilities.completions !== void 0 ? (params, options) => orchestrator.complete(params, options) : void 0,
|
|
1659
|
+
setLoggingLevel: orchestrator.capabilities.logging !== void 0 ? (level, options) => orchestrator.setLoggingLevel(level, options) : void 0,
|
|
1660
|
+
onRootsListChanged: () => orchestrator.broadcastRootsListChanged()
|
|
594
1661
|
});
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
1662
|
+
orchestrator.setNotificationHandlers({
|
|
1663
|
+
onToolsListChanged: () => proxyServer.sendToolListChanged(),
|
|
1664
|
+
onResourcesListChanged: () => proxyServer.sendResourceListChanged(),
|
|
1665
|
+
onResourceUpdated: (params) => proxyServer.sendResourceUpdated(params),
|
|
1666
|
+
onPromptsListChanged: () => proxyServer.sendPromptListChanged(),
|
|
1667
|
+
onLogMessage: (params) => proxyServer.sendLoggingMessage(params)
|
|
1668
|
+
});
|
|
1669
|
+
orchestrator.setServerRequestForwarders({
|
|
1670
|
+
onCreateMessage: (params, options) => proxyServer.forwardCreateMessage(params, options),
|
|
1671
|
+
onElicitInput: (params, options) => proxyServer.forwardElicitInput(params, options),
|
|
1672
|
+
onListRoots: (params, options) => proxyServer.forwardListRoots(params, options)
|
|
1673
|
+
});
|
|
1674
|
+
import_node_process6.default.on("SIGINT", () => shutdown(0));
|
|
1675
|
+
import_node_process6.default.on("SIGTERM", () => shutdown(0));
|
|
1676
|
+
import_node_process6.default.stdin.on("end", () => shutdown(0));
|
|
1677
|
+
import_node_process6.default.stdin.on("close", () => shutdown(0));
|
|
599
1678
|
try {
|
|
600
1679
|
await proxyServer.start();
|
|
601
1680
|
} catch (error) {
|
|
@@ -615,39 +1694,40 @@ var cliBanner = import_chalk.default.bold.magentaBright(
|
|
|
615
1694
|
var cli = new import_commander.Command(package_default.name).description(package_default.description).version(package_default.version).addHelpText("beforeAll", cliBanner).addHelpText(
|
|
616
1695
|
"after",
|
|
617
1696
|
"\nExamples:\n dynmcp -- npx -y chrome-devtools-mcp@latest\n dynmcp --config ./mcp.json\n"
|
|
618
|
-
).option("-c, --config <path>", "Path to config file (JSON or YAML)").allowExcessArguments(true).passThroughOptions(true).action(async (_options, cmd) => {
|
|
619
|
-
const separatorIndex =
|
|
1697
|
+
).option("-c, --config <path>", "Path to config file (JSON or YAML)").option("-e, --env <path>", "Path to a .env file for environment variable interpolation").allowExcessArguments(true).passThroughOptions(true).action(async (_options, cmd) => {
|
|
1698
|
+
const separatorIndex = import_node_process7.default.argv.indexOf("--");
|
|
620
1699
|
const configPath = cmd.opts().config;
|
|
1700
|
+
const envFilePath = cmd.opts().env;
|
|
621
1701
|
if (separatorIndex !== -1) {
|
|
622
|
-
const [command, ...args] =
|
|
1702
|
+
const [command, ...args] = import_node_process7.default.argv.slice(separatorIndex + 1);
|
|
623
1703
|
if (command === void 0) {
|
|
624
|
-
|
|
1704
|
+
import_node_process7.default.stderr.write(
|
|
625
1705
|
"dynmcp: no upstream command provided after --.\nUsage: dynmcp -- <command> [args...]\n"
|
|
626
1706
|
);
|
|
627
|
-
|
|
1707
|
+
import_node_process7.default.exit(1);
|
|
628
1708
|
}
|
|
629
1709
|
try {
|
|
630
1710
|
await startProxy(command, args);
|
|
631
1711
|
} catch (error) {
|
|
632
|
-
|
|
1712
|
+
import_node_process7.default.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
|
|
633
1713
|
`);
|
|
634
|
-
|
|
1714
|
+
import_node_process7.default.exit(1);
|
|
635
1715
|
}
|
|
636
1716
|
return;
|
|
637
1717
|
}
|
|
638
1718
|
try {
|
|
639
|
-
await startProxyFromConfig(configPath);
|
|
1719
|
+
await startProxyFromConfig({ configPath, envFilePath });
|
|
640
1720
|
} catch (error) {
|
|
641
|
-
|
|
1721
|
+
import_node_process7.default.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
|
|
642
1722
|
`);
|
|
643
|
-
|
|
1723
|
+
import_node_process7.default.exit(1);
|
|
644
1724
|
}
|
|
645
1725
|
});
|
|
646
1726
|
|
|
647
1727
|
// src/index.ts
|
|
648
|
-
var
|
|
1728
|
+
var import_node_process8 = __toESM(require("process"), 1);
|
|
649
1729
|
async function main() {
|
|
650
|
-
cli.parse(
|
|
1730
|
+
cli.parse(import_node_process8.default.argv);
|
|
651
1731
|
}
|
|
652
1732
|
main();
|
|
653
1733
|
//# sourceMappingURL=index.cjs.map
|