dynmcp 0.0.1 → 0.1.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 CHANGED
@@ -1,4 +1,4 @@
1
- # dynmcp
1
+ # Dynamic Discovery MCP
2
2
 
3
3
  A proxy MCP that exposes meta-tools so agents can discover and call upstream MCP tools on demand, without loading every tool schema into the context window.
4
4
 
@@ -8,27 +8,124 @@ Large MCPs routinely expose tens to hundreds of tools. When several are active a
8
8
 
9
9
  ## How It Works
10
10
 
11
- `dynmcp` sits in front of an upstream MCP and exposes exactly two tools:
11
+ `dynmcp` sits in front of one or more upstream MCPs and exposes exactly two tools:
12
12
 
13
13
  - **`discover_tool`** — its description contains a compact catalog of every upstream tool (name and one-line summary). Call it with a tool name to get that tool's full schema: description, parameters, types, and required fields.
14
14
  - **`use_tool`** — executes a tool by name, proxying the call to the upstream MCP and returning its output unchanged.
15
15
 
16
16
  The agent workflow: scan the catalog in `discover_tool`'s description to find relevant tools, call `discover_tool` to load the full schema of the one it needs, then call `use_tool` to execute it. Full schemas of tools the agent never needs never enter the context window.
17
17
 
18
- `dynmcp` runs locally, communicating with both the agent host and the upstream MCP over stdio.
19
-
20
18
  ## Usage
21
19
 
22
20
  Requires Node.js >= 20.
23
21
 
22
+ ### Single MCP (quick start)
23
+
24
24
  Prefix any MCP invocation with `dynmcp --`:
25
25
 
26
26
  ```bash
27
- # Before
27
+ # Before — tool schemas go straight into context
28
28
  npx -y chrome-devtools-mcp@latest
29
29
 
30
- # With dynmcp
30
+ # With dynmcp — only discover_tool and use_tool are exposed
31
31
  npx dynmcp@latest -- npx -y chrome-devtools-mcp@latest
32
32
  ```
33
33
 
34
- Everything after `--` is the command used to launch the upstream MCP.
34
+ Everything after `--` is the command used to launch the upstream MCP. Tool names are exposed as-is (no namespace prefix).
35
+
36
+ ### Multiple MCPs (config file)
37
+
38
+ To proxy several MCPs at once, create a config file:
39
+
40
+ ```bash
41
+ # Auto-discover mcp.json or .mcp.json in cwd
42
+ npx dynmcp@latest
43
+
44
+ # Or specify explicitly
45
+ npx dynmcp@latest --config ./my-config.json
46
+ ```
47
+
48
+ When using a config file, tool names are namespaced as `<mcp-name>/<tool-name>` to avoid collisions.
49
+
50
+ ## Config File
51
+
52
+ The config file declares upstream MCPs under a top-level `mcp` key. Three transport types are supported:
53
+
54
+ ```json
55
+ {
56
+ "$schema": "https://unpkg.com/dynmcp/schema/mcp-config.json",
57
+ "mcp": {
58
+ "chrome-devtools": {
59
+ "transport": "stdio",
60
+ "command": "npx",
61
+ "args": ["-y", "chrome-devtools-mcp@latest"]
62
+ },
63
+ "filesystem": {
64
+ "transport": "stdio",
65
+ "command": "npx",
66
+ "args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
67
+ },
68
+ "aws-knowledge": {
69
+ "transport": "streamable-http",
70
+ "url": "https://knowledge-mcp.global.api.aws"
71
+ },
72
+ "remote-sse": {
73
+ "transport": "sse",
74
+ "url": "https://example.com/sse",
75
+ "headers": {
76
+ "Authorization": "Bearer my-token"
77
+ }
78
+ }
79
+ }
80
+ }
81
+ ```
82
+
83
+ YAML is also supported (use `.yml` or `.yaml` extension).
84
+
85
+ ### Transport Types
86
+
87
+ | Transport | Fields | Description |
88
+ |---|---|---|
89
+ | `stdio` | `command`, `args?`, `env?` | Spawns the MCP as a child process |
90
+ | `streamable-http` | `url`, `headers?` | Connects to a remote MCP over HTTP |
91
+ | `sse` | `url`, `headers?` | Connects to a remote MCP over Server-Sent Events |
92
+
93
+ ### Config Discovery
94
+
95
+ When no `--` command is provided, `dynmcp` looks for a config file in this order:
96
+
97
+ 1. Path from `-c` / `--config` flag
98
+ 2. `mcp.json` in the current directory
99
+ 3. `.mcp.json` in the current directory
100
+
101
+ ### Naming Rules
102
+
103
+ MCP names (the keys in the config) must match `^[a-z0-9][a-z0-9-]*$`.
104
+
105
+ ## CLI Reference
106
+
107
+ ```
108
+ dynmcp [options] [-- <upstream-command> [upstream-args...]]
109
+ ```
110
+
111
+ | Flag | Short | Description |
112
+ |---|---|---|
113
+ | `--version` | `-v` | Print the package version and exit |
114
+ | `--help` | `-h` | Print usage information and exit |
115
+ | `--config <path>` | `-c` | Path to config file (JSON or YAML) |
116
+ | `--` | | Everything after is the upstream MCP command (single-MCP mode) |
117
+
118
+ ### Mode Resolution
119
+
120
+ 1. If `--` is present, single-MCP mode is used (config file is ignored).
121
+ 2. Otherwise, config file mode is used.
122
+
123
+ ## Development
124
+
125
+ ```bash
126
+ npm install
127
+ npm run build # Compile to dist/
128
+ npm run typecheck # Type-check without emitting
129
+ npm run check # Biome lint + format
130
+ npm test # Run tests
131
+ ```
package/dist/index.cjs CHANGED
@@ -29,7 +29,7 @@ var import_commander = require("commander");
29
29
  // package.json
30
30
  var package_default = {
31
31
  name: "dynmcp",
32
- version: "0.0.1",
32
+ version: "0.1.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",
@@ -97,6 +97,7 @@ var package_default = {
97
97
  fastmcp: "^4.0.1",
98
98
  figlet: "^1.11.0",
99
99
  figures: "^6.1.0",
100
+ yaml: "^2.9.0",
100
101
  zod: "^4.4.3"
101
102
  },
102
103
  devDependencies: {
@@ -118,21 +119,143 @@ var import_chalk = __toESM(require("chalk"), 1);
118
119
 
119
120
  // src/proxy/index.ts
120
121
  var import_node_process3 = __toESM(require("process"), 1);
122
+ var import_stdio2 = require("@modelcontextprotocol/sdk/client/stdio.js");
123
+
124
+ // src/config/schema.ts
125
+ var import_zod = require("zod");
126
+ var MCP_NAME_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
127
+ var mcpName = import_zod.z.string().regex(MCP_NAME_PATTERN);
128
+ var stdioTransport = import_zod.z.object({
129
+ transport: import_zod.z.literal("stdio"),
130
+ command: import_zod.z.string(),
131
+ args: import_zod.z.array(import_zod.z.string()).optional(),
132
+ env: import_zod.z.record(import_zod.z.string(), import_zod.z.string()).optional()
133
+ }).strict();
134
+ var httpUrl = import_zod.z.string().url().refine((u) => u.startsWith("http://") || u.startsWith("https://"), {
135
+ message: "URL must use http:// or https:// scheme"
136
+ });
137
+ var streamableHttpTransport = import_zod.z.object({
138
+ transport: import_zod.z.literal("streamable-http"),
139
+ url: httpUrl,
140
+ headers: import_zod.z.record(import_zod.z.string(), import_zod.z.string()).optional()
141
+ }).strict();
142
+ var sseTransport = import_zod.z.object({
143
+ transport: import_zod.z.literal("sse"),
144
+ url: httpUrl,
145
+ headers: import_zod.z.record(import_zod.z.string(), import_zod.z.string()).optional()
146
+ }).strict();
147
+ var transportConfig = import_zod.z.discriminatedUnion("transport", [
148
+ stdioTransport,
149
+ streamableHttpTransport,
150
+ sseTransport
151
+ ]);
152
+ var mcpConfigSchema = import_zod.z.object({
153
+ mcp: import_zod.z.record(mcpName, transportConfig).refine((obj) => Object.keys(obj).length > 0, { message: "At least one MCP must be configured" })
154
+ });
155
+
156
+ // src/config/loader.ts
157
+ var import_node_fs = require("fs");
158
+ var import_node_fs2 = require("fs");
159
+ var import_node_path = require("path");
160
+ var import_yaml = require("yaml");
161
+ var AUTO_DISCOVER_NAMES = ["mcp.json", ".mcp.json"];
162
+ function resolveConfigPath(explicitPath) {
163
+ if (explicitPath) {
164
+ const resolved = (0, import_node_path.resolve)(explicitPath);
165
+ if (!(0, import_node_fs2.existsSync)(resolved)) {
166
+ throw new Error(`Config file not found: ${resolved}`);
167
+ }
168
+ return resolved;
169
+ }
170
+ const cwd = process.cwd();
171
+ for (const name of AUTO_DISCOVER_NAMES) {
172
+ const candidate = (0, import_node_path.resolve)(cwd, name);
173
+ if ((0, import_node_fs2.existsSync)(candidate)) {
174
+ return candidate;
175
+ }
176
+ }
177
+ const searched = AUTO_DISCOVER_NAMES.map((n) => (0, import_node_path.resolve)(cwd, n)).join(", ");
178
+ throw new Error(`No config file found. Searched: ${searched}`);
179
+ }
180
+ function loadConfig(explicitPath) {
181
+ const configPath = resolveConfigPath(explicitPath);
182
+ const raw = (0, import_node_fs.readFileSync)(configPath, "utf-8");
183
+ let content;
184
+ try {
185
+ content = isYamlFile(configPath) ? (0, import_yaml.parse)(raw) : JSON.parse(raw);
186
+ } catch (parseError) {
187
+ const message = parseError instanceof Error ? parseError.message : String(parseError);
188
+ throw new Error(`Failed to parse config file (${configPath}): ${message}`);
189
+ }
190
+ const result = mcpConfigSchema.safeParse(content);
191
+ if (!result.success) {
192
+ const formatted = result.error.issues.map((issue) => ` - ${issue.path.join(".")}: ${issue.message}`).join("\n");
193
+ throw new Error(`Invalid config file (${configPath}):
194
+ ${formatted}`);
195
+ }
196
+ return result.data;
197
+ }
198
+ function isYamlFile(filePath) {
199
+ return filePath.endsWith(".yml") || filePath.endsWith(".yaml");
200
+ }
201
+
202
+ // src/proxy/transport-factory.ts
203
+ var import_stdio = require("@modelcontextprotocol/sdk/client/stdio.js");
204
+ var import_streamableHttp = require("@modelcontextprotocol/sdk/client/streamableHttp.js");
205
+ var import_sse = require("@modelcontextprotocol/sdk/client/sse.js");
206
+ function createTransport(config) {
207
+ switch (config.transport) {
208
+ case "stdio":
209
+ return new import_stdio.StdioClientTransport({
210
+ command: config.command,
211
+ args: config.args,
212
+ env: config.env
213
+ });
214
+ case "streamable-http":
215
+ return new import_streamableHttp.StreamableHTTPClientTransport(
216
+ new URL(config.url),
217
+ config.headers ? { requestInit: { headers: config.headers } } : void 0
218
+ );
219
+ case "sse":
220
+ return new import_sse.SSEClientTransport(
221
+ new URL(config.url),
222
+ config.headers ? { requestInit: { headers: config.headers } } : void 0
223
+ );
224
+ default: {
225
+ const _exhaustive = config;
226
+ return _exhaustive;
227
+ }
228
+ }
229
+ }
121
230
 
122
231
  // src/proxy/tool-catalog.ts
123
232
  var DISCOVER_TOOL_PREAMBLE = `Use this tool to look up the full schema of a tool before calling it with use_tool.
124
233
  Call discover_tool with a tool name from the list below to get its complete description,
125
234
  input parameters, and output schema. Always discover a tool before using it.`;
126
- var ToolCatalog = class {
235
+ var ToolCatalog = class _ToolCatalog {
127
236
  tools;
128
237
  discoverToolDescription;
129
- constructor(upstreamTools) {
238
+ constructor(tools, description) {
239
+ this.tools = tools;
240
+ this.discoverToolDescription = description;
241
+ }
242
+ static fromFlat(upstreamTools) {
130
243
  const toolMap = /* @__PURE__ */ new Map();
131
244
  for (const tool of upstreamTools) {
132
245
  toolMap.set(tool.name, tool);
133
246
  }
134
- this.tools = toolMap;
135
- this.discoverToolDescription = buildDiscoverToolDescription(upstreamTools);
247
+ const description = buildFlatDescription(upstreamTools);
248
+ return new _ToolCatalog(toolMap, description);
249
+ }
250
+ static fromGrouped(groups) {
251
+ const toolMap = /* @__PURE__ */ new Map();
252
+ for (const [mcpName2, tools] of groups) {
253
+ for (const tool of tools) {
254
+ toolMap.set(`${mcpName2}/${tool.name}`, tool);
255
+ }
256
+ }
257
+ const description = buildGroupedDescription(groups);
258
+ return new _ToolCatalog(toolMap, description);
136
259
  }
137
260
  getToolDetails(toolName) {
138
261
  const tool = this.tools.get(toolName);
@@ -140,10 +263,10 @@ var ToolCatalog = class {
140
263
  const sortedNames = [...this.tools.keys()].sort().join(", ");
141
264
  return `Unknown tool: "${toolName}". Available tools: ${sortedNames}`;
142
265
  }
143
- return buildToolDetailsString(tool);
266
+ return buildToolDetailsString(toolName, tool);
144
267
  }
145
268
  };
146
- function buildDiscoverToolDescription(tools) {
269
+ function buildFlatDescription(tools) {
147
270
  const sortedTools = [...tools].sort((a, b) => a.name.localeCompare(b.name));
148
271
  const toolLines = sortedTools.map((tool) => `- ${tool.name}: ${tool.description}`).join("\n");
149
272
  return `${DISCOVER_TOOL_PREAMBLE}
@@ -152,9 +275,24 @@ function buildDiscoverToolDescription(tools) {
152
275
  ${toolLines}
153
276
  </tools>`;
154
277
  }
155
- function buildToolDetailsString(tool) {
278
+ function buildGroupedDescription(groups) {
279
+ const sortedMcpNames = [...groups.keys()].sort();
280
+ const sections = sortedMcpNames.map((mcpName2) => {
281
+ const tools = groups.get(mcpName2);
282
+ const sortedTools = [...tools].sort((a, b) => a.name.localeCompare(b.name));
283
+ const toolLines = sortedTools.map((tool) => `- ${mcpName2}/${tool.name}: ${tool.description}`).join("\n");
284
+ return `${mcpName2}:
285
+ ${toolLines}`;
286
+ });
287
+ return `${DISCOVER_TOOL_PREAMBLE}
288
+
289
+ <tools>
290
+ ${sections.join("\n\n")}
291
+ </tools>`;
292
+ }
293
+ function buildToolDetailsString(displayName, tool) {
156
294
  const lines = [
157
- `Tool: ${tool.name}`,
295
+ `Tool: ${displayName}`,
158
296
  `Description: ${tool.description}`,
159
297
  "",
160
298
  "Input Schema:",
@@ -193,73 +331,21 @@ function buildAnnotationLines(tool) {
193
331
  return lines;
194
332
  }
195
333
 
196
- // src/proxy/server.ts
197
- var import_node_process = __toESM(require("process"), 1);
198
- var import_fastmcp = require("fastmcp");
199
- var import_zod = require("zod");
200
- var ProxyServer = class {
201
- catalog;
202
- upstreamClient;
203
- constructor({ catalog, upstreamClient }) {
204
- this.catalog = catalog;
205
- this.upstreamClient = upstreamClient;
206
- }
207
- async start() {
208
- const server = new import_fastmcp.FastMCP({
209
- name: "dynamic-discovery-mcp",
210
- version: package_default.version
211
- });
212
- server.addTool({
213
- name: "discover_tool",
214
- description: this.catalog.discoverToolDescription,
215
- parameters: import_zod.z.object({ tool_name: import_zod.z.string() }),
216
- execute: async ({ tool_name }) => {
217
- return this.catalog.getToolDetails(tool_name);
218
- }
219
- });
220
- server.addTool({
221
- name: "use_tool",
222
- description: "Use a tool that was previously discovered with the discover_tool tool.",
223
- parameters: import_zod.z.object({
224
- tool_name: import_zod.z.string(),
225
- tool_input: import_zod.z.record(import_zod.z.string(), import_zod.z.unknown()).default({})
226
- }),
227
- execute: async ({ tool_name, tool_input }) => {
228
- if (!this.catalog.tools.has(tool_name)) {
229
- return this.catalog.getToolDetails(tool_name);
230
- }
231
- const result = await this.upstreamClient.callTool(tool_name, tool_input);
232
- return result;
233
- }
234
- });
235
- import_node_process.default.stderr.write("Starting dynamic-discovery-mcp server over stdio\n");
236
- await server.start({ transportType: "stdio" });
237
- }
238
- };
239
-
240
334
  // src/proxy/upstream-client.ts
241
- var import_node_process2 = __toESM(require("process"), 1);
335
+ var import_node_process = __toESM(require("process"), 1);
242
336
  var import_client = require("@modelcontextprotocol/sdk/client/index.js");
243
- var import_stdio = require("@modelcontextprotocol/sdk/client/stdio.js");
244
337
  var UpstreamClient = class {
245
- command;
246
- args;
338
+ transport;
247
339
  onTransportError;
248
340
  client = null;
249
- transport = null;
250
- constructor({ command, args, onTransportError }) {
251
- this.command = command;
252
- this.args = args;
341
+ constructor({ name, transport, onTransportError }) {
342
+ this.transport = transport;
253
343
  this.onTransportError = onTransportError ?? ((error) => {
254
- import_node_process2.default.stderr.write(`Upstream MCP transport error: ${error.message}
344
+ import_node_process.default.stderr.write(`[${name}] Upstream MCP transport error: ${error.message}
255
345
  `);
256
346
  });
257
347
  }
258
348
  async connect() {
259
- this.transport = new import_stdio.StdioClientTransport({
260
- command: this.command,
261
- args: this.args
262
- });
263
349
  this.transport.onerror = this.onTransportError;
264
350
  this.client = new import_client.Client({ name: "dynamic-discovery-mcp", version: "1.0.0" });
265
351
  await this.client.connect(this.transport);
@@ -294,10 +380,8 @@ var UpstreamClient = class {
294
380
  if (this.client === null) {
295
381
  throw new Error("Client is not connected. Call connect() first.");
296
382
  }
297
- return this.client.callTool({
298
- name,
299
- arguments: input
300
- });
383
+ const result = await this.client.callTool({ name, arguments: input });
384
+ return result;
301
385
  }
302
386
  async disconnect() {
303
387
  if (this.client === null) {
@@ -305,13 +389,126 @@ var UpstreamClient = class {
305
389
  }
306
390
  await this.client.close();
307
391
  this.client = null;
308
- this.transport = null;
392
+ }
393
+ };
394
+
395
+ // src/proxy/orchestrator.ts
396
+ var Orchestrator = class {
397
+ config;
398
+ clients = /* @__PURE__ */ new Map();
399
+ toolCatalog = null;
400
+ constructor(config) {
401
+ this.config = config;
402
+ }
403
+ async connect() {
404
+ const groups = /* @__PURE__ */ new Map();
405
+ try {
406
+ for (const [mcpName2, { transport }] of this.config.mcps) {
407
+ const client = new UpstreamClient({
408
+ name: mcpName2,
409
+ transport,
410
+ onTransportError: (error) => {
411
+ this.config.onTransportError?.(mcpName2, error);
412
+ }
413
+ });
414
+ await client.connect();
415
+ const tools = await client.listTools();
416
+ this.clients.set(mcpName2, client);
417
+ groups.set(mcpName2, tools);
418
+ }
419
+ } catch (error) {
420
+ await this.disconnectAll();
421
+ throw error;
422
+ }
423
+ this.toolCatalog = ToolCatalog.fromGrouped(groups);
424
+ }
425
+ get catalog() {
426
+ if (this.toolCatalog === null) {
427
+ throw new Error("Orchestrator is not connected. Call connect() first.");
428
+ }
429
+ return this.toolCatalog;
430
+ }
431
+ async callTool(namespacedName, input) {
432
+ const separatorIndex = namespacedName.indexOf("/");
433
+ if (separatorIndex === -1) {
434
+ throw new Error(
435
+ `Invalid namespaced tool name: "${namespacedName}". Expected format: "mcpName/toolName".`
436
+ );
437
+ }
438
+ const mcpName2 = namespacedName.slice(0, separatorIndex);
439
+ const toolName = namespacedName.slice(separatorIndex + 1);
440
+ const client = this.clients.get(mcpName2);
441
+ if (client === void 0) {
442
+ const available = [...this.clients.keys()].sort().join(", ");
443
+ throw new Error(`Unknown MCP: "${mcpName2}". Available MCPs: ${available}`);
444
+ }
445
+ return client.callTool(toolName, input);
446
+ }
447
+ async disconnectAll() {
448
+ const disconnections = [...this.clients.values()].map((client) => client.disconnect());
449
+ await Promise.all(disconnections);
450
+ this.clients.clear();
451
+ this.toolCatalog = null;
452
+ }
453
+ };
454
+
455
+ // src/proxy/server.ts
456
+ var import_node_process2 = __toESM(require("process"), 1);
457
+ var import_fastmcp = require("fastmcp");
458
+ var import_zod2 = require("zod");
459
+ var ProxyServer = class {
460
+ catalog;
461
+ callTool;
462
+ constructor({ catalog, callTool }) {
463
+ this.catalog = catalog;
464
+ this.callTool = callTool;
465
+ }
466
+ async start() {
467
+ const server = new import_fastmcp.FastMCP({
468
+ name: "dynamic-discovery-mcp",
469
+ version: package_default.version
470
+ });
471
+ server.addTool({
472
+ name: "discover_tool",
473
+ description: this.catalog.discoverToolDescription,
474
+ parameters: import_zod2.z.object({ tool_name: import_zod2.z.string() }),
475
+ execute: async ({ tool_name }) => {
476
+ return this.catalog.getToolDetails(tool_name);
477
+ }
478
+ });
479
+ server.addTool({
480
+ name: "use_tool",
481
+ description: "Use a tool that was previously discovered with the discover_tool tool.",
482
+ parameters: import_zod2.z.object({
483
+ tool_name: import_zod2.z.string(),
484
+ tool_input: import_zod2.z.record(import_zod2.z.string(), import_zod2.z.unknown()).default({})
485
+ }),
486
+ execute: async ({ tool_name, tool_input }) => {
487
+ if (!this.catalog.tools.has(tool_name)) {
488
+ return this.catalog.getToolDetails(tool_name);
489
+ }
490
+ const result = await this.callTool(tool_name, tool_input);
491
+ return result;
492
+ }
493
+ });
494
+ import_node_process2.default.stderr.write("Starting dynamic-discovery-mcp server over stdio\n");
495
+ await server.start({ transportType: "stdio" });
309
496
  }
310
497
  };
311
498
 
312
499
  // src/proxy/index.ts
313
500
  async function startProxy(command, args) {
314
501
  let isShuttingDown = false;
502
+ const transport = new import_stdio2.StdioClientTransport({ command, args });
503
+ const upstreamClient = new UpstreamClient({
504
+ name: command,
505
+ transport,
506
+ onTransportError: (error) => {
507
+ import_node_process3.default.stderr.write(`Upstream MCP transport error: ${error.message}
508
+ `);
509
+ shutdown(1);
510
+ }
511
+ });
315
512
  const shutdown = (exitCode) => {
316
513
  if (isShuttingDown) return;
317
514
  isShuttingDown = true;
@@ -322,15 +519,6 @@ async function startProxy(command, args) {
322
519
  );
323
520
  }).finally(() => import_node_process3.default.exit(exitCode));
324
521
  };
325
- const upstreamClient = new UpstreamClient({
326
- command,
327
- args,
328
- onTransportError: (error) => {
329
- import_node_process3.default.stderr.write(`Upstream MCP transport error: ${error.message}
330
- `);
331
- shutdown(1);
332
- }
333
- });
334
522
  try {
335
523
  await upstreamClient.connect();
336
524
  } catch (error) {
@@ -346,13 +534,68 @@ async function startProxy(command, args) {
346
534
  `);
347
535
  import_node_process3.default.exit(1);
348
536
  }
349
- const catalog = new ToolCatalog(tools);
350
- const proxyServer = new ProxyServer({ catalog, upstreamClient });
537
+ const catalog = ToolCatalog.fromFlat(tools);
538
+ const proxyServer = new ProxyServer({
539
+ catalog,
540
+ callTool: (name, input) => upstreamClient.callTool(name, input)
541
+ });
542
+ import_node_process3.default.on("SIGINT", () => shutdown(0));
543
+ import_node_process3.default.on("SIGTERM", () => shutdown(0));
544
+ import_node_process3.default.stdin.on("end", () => shutdown(0));
545
+ import_node_process3.default.stdin.on("close", () => shutdown(0));
546
+ try {
547
+ await proxyServer.start();
548
+ } catch (error) {
549
+ shutdown(1);
550
+ throw error;
551
+ }
552
+ }
553
+ async function startProxyFromConfig(configPath) {
554
+ let isShuttingDown = false;
555
+ const config = loadConfig(configPath);
556
+ const mcps = /* @__PURE__ */ new Map();
557
+ for (const [name, entry] of Object.entries(config.mcp)) {
558
+ mcps.set(name, { transport: createTransport(entry) });
559
+ }
560
+ const orchestrator = new Orchestrator({
561
+ mcps,
562
+ onTransportError: (mcpName2, error) => {
563
+ import_node_process3.default.stderr.write(`Upstream MCP "${mcpName2}" transport error: ${error.message}
564
+ `);
565
+ shutdown(1);
566
+ }
567
+ });
568
+ const shutdown = (exitCode) => {
569
+ if (isShuttingDown) return;
570
+ isShuttingDown = true;
571
+ orchestrator.disconnectAll().catch((error) => {
572
+ import_node_process3.default.stderr.write(
573
+ `dynmcp: error during disconnect: ${error instanceof Error ? error.message : String(error)}
574
+ `
575
+ );
576
+ }).finally(() => import_node_process3.default.exit(exitCode));
577
+ };
578
+ try {
579
+ await orchestrator.connect();
580
+ } catch (error) {
581
+ import_node_process3.default.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
582
+ `);
583
+ import_node_process3.default.exit(1);
584
+ }
585
+ const proxyServer = new ProxyServer({
586
+ catalog: orchestrator.catalog,
587
+ callTool: (name, input) => orchestrator.callTool(name, input)
588
+ });
351
589
  import_node_process3.default.on("SIGINT", () => shutdown(0));
352
590
  import_node_process3.default.on("SIGTERM", () => shutdown(0));
353
591
  import_node_process3.default.stdin.on("end", () => shutdown(0));
354
592
  import_node_process3.default.stdin.on("close", () => shutdown(0));
355
- await proxyServer.start();
593
+ try {
594
+ await proxyServer.start();
595
+ } catch (error) {
596
+ shutdown(1);
597
+ throw error;
598
+ }
356
599
  }
357
600
 
358
601
  // src/cli.ts
@@ -363,23 +606,31 @@ var cliBanner = import_chalk.default.bold.magentaBright(
363
606
  verticalLayout: "fitted"
364
607
  })
365
608
  );
366
- var cli = new import_commander.Command(package_default.name).description(package_default.description).version(package_default.version).addHelpText("beforeAll", cliBanner).addHelpText("after", "\nExample:\n dynmcp -- npx -y chrome-devtools-mcp@latest\n").allowExcessArguments(true).passThroughOptions(true).action(async () => {
609
+ var cli = new import_commander.Command(package_default.name).description(package_default.description).version(package_default.version).addHelpText("beforeAll", cliBanner).addHelpText(
610
+ "after",
611
+ "\nExamples:\n dynmcp -- npx -y chrome-devtools-mcp@latest\n dynmcp --config ./mcp.json\n"
612
+ ).option("-c, --config <path>", "Path to config file (JSON or YAML)").allowExcessArguments(true).passThroughOptions(true).action(async (_options, cmd) => {
367
613
  const separatorIndex = import_node_process4.default.argv.indexOf("--");
368
- if (separatorIndex === -1) {
369
- import_node_process4.default.stderr.write(
370
- "dynmcp: no upstream command provided.\nUsage: dynmcp -- <command> [args...]\nExample: dynmcp -- npx -y chrome-devtools-mcp@latest\n"
371
- );
372
- import_node_process4.default.exit(1);
373
- }
374
- const [command, ...args] = import_node_process4.default.argv.slice(separatorIndex + 1);
375
- if (command === void 0) {
376
- import_node_process4.default.stderr.write(
377
- "dynmcp: no upstream command provided.\nUsage: dynmcp -- <command> [args...]\nExample: dynmcp -- npx -y chrome-devtools-mcp@latest\n"
378
- );
379
- import_node_process4.default.exit(1);
614
+ const configPath = cmd.opts().config;
615
+ if (separatorIndex !== -1) {
616
+ const [command, ...args] = import_node_process4.default.argv.slice(separatorIndex + 1);
617
+ if (command === void 0) {
618
+ import_node_process4.default.stderr.write(
619
+ "dynmcp: no upstream command provided after --.\nUsage: dynmcp -- <command> [args...]\n"
620
+ );
621
+ import_node_process4.default.exit(1);
622
+ }
623
+ try {
624
+ await startProxy(command, args);
625
+ } catch (error) {
626
+ import_node_process4.default.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
627
+ `);
628
+ import_node_process4.default.exit(1);
629
+ }
630
+ return;
380
631
  }
381
632
  try {
382
- await startProxy(command, args);
633
+ await startProxyFromConfig(configPath);
383
634
  } catch (error) {
384
635
  import_node_process4.default.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
385
636
  `);