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