dynmcp 0.1.0 → 0.2.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
@@ -102,6 +102,64 @@ When no `--` command is provided, `dynmcp` looks for a config file in this order
102
102
 
103
103
  MCP names (the keys in the config) must match `^[a-z0-9][a-z0-9-]*$`.
104
104
 
105
+ ## Environment Variable Interpolation
106
+
107
+ Config files can reference environment variables in any string-typed leaf value using shell-style syntax. This is useful for keeping secrets (bearer tokens, API keys) and host-specific values (paths, ports) out of the config file itself.
108
+
109
+ ```json
110
+ {
111
+ "mcp": {
112
+ "remote": {
113
+ "transport": "streamable-http",
114
+ "url": "${MCP_URL:-https://example.com/mcp}",
115
+ "headers": {
116
+ "Authorization": "Bearer ${MCP_TOKEN}"
117
+ }
118
+ }
119
+ }
120
+ }
121
+ ```
122
+
123
+ ### Syntax
124
+
125
+ | Form | Behavior |
126
+ |---|---|
127
+ | `${VAR}` | Replaced with the value of `VAR`. Hard error at startup if `VAR` is undefined. |
128
+ | `${VAR:-default}` | Replaced with `VAR` if set and non-empty, otherwise the literal `default` (may contain spaces, colons, etc.). |
129
+ | `$${...}` | Escape — emits a literal `${...}` with no interpolation. |
130
+
131
+ Interpolation only applies to **leaf string values** inside the `mcp` map (and nested objects/arrays within it). Map keys, the top-level `$schema` field, and the top-level `env` field are never interpolated. Partial-string interpolation works — `"Bearer ${TOKEN}"` is valid.
132
+
133
+ If any referenced variables are missing without a default, `dynmcp` exits at startup with an error listing **all** of them at once (not one at a time).
134
+
135
+ ### Sources (`env` field)
136
+
137
+ A top-level `env` field controls where variables are read from:
138
+
139
+ | Value | Behavior |
140
+ |---|---|
141
+ | `"enable"` (default) | Loads `.env` file (if present) and merges with `process.env`. `.env` values take precedence over `process.env` for the same key. |
142
+ | `"dotenv"` | Loads from `.env` file only. `process.env` is ignored. |
143
+ | `"process"` | Reads from `process.env` only. No `.env` file is loaded. |
144
+ | `"disable"` | Disables interpolation entirely — `${VAR}` is left literal. |
145
+
146
+ ```json
147
+ {
148
+ "env": "process",
149
+ "mcp": { /* ... */ }
150
+ }
151
+ ```
152
+
153
+ ### `.env` File Discovery
154
+
155
+ By default, `dynmcp` looks for a file literally named `.env` in the current working directory. To use a different path, pass `--env` / `-e`:
156
+
157
+ ```bash
158
+ dynmcp --env ./secrets.env
159
+ ```
160
+
161
+ Combining `--env` with `env: "disable"` or `env: "process"` is rejected as incoherent (no `.env` would be loaded). If `--env` points to a file that does not exist, `dynmcp` exits with an error.
162
+
105
163
  ## CLI Reference
106
164
 
107
165
  ```
@@ -113,6 +171,7 @@ dynmcp [options] [-- <upstream-command> [upstream-args...]]
113
171
  | `--version` | `-v` | Print the package version and exit |
114
172
  | `--help` | `-h` | Print usage information and exit |
115
173
  | `--config <path>` | `-c` | Path to config file (JSON or YAML) |
174
+ | `--env <path>` | `-e` | Path to a custom `.env` file for variable interpolation |
116
175
  | `--` | | Everything after is the upstream MCP command (single-MCP mode) |
117
176
 
118
177
  ### Mode Resolution
package/dist/index.cjs CHANGED
@@ -23,13 +23,13 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
23
23
  ));
24
24
 
25
25
  // src/cli.ts
26
- var import_node_process4 = __toESM(require("process"), 1);
26
+ var import_node_process6 = __toESM(require("process"), 1);
27
27
  var import_commander = require("commander");
28
28
 
29
29
  // package.json
30
30
  var package_default = {
31
31
  name: "dynmcp",
32
- version: "0.1.0",
32
+ version: "0.2.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",
@@ -74,9 +74,12 @@ var package_default = {
74
74
  }
75
75
  },
76
76
  files: [
77
- "dist"
77
+ "dist",
78
+ "schema"
78
79
  ],
79
80
  scripts: {
81
+ "generate:schema": "tsx scripts/generate-schema.ts",
82
+ prebuild: "tsx scripts/generate-schema.ts",
80
83
  build: "tsup",
81
84
  dev: "tsx src/index.ts",
82
85
  typecheck: "tsc --noEmit",
@@ -93,6 +96,7 @@ var package_default = {
93
96
  boxen: "^8.0.1",
94
97
  chalk: "^5.6.2",
95
98
  commander: "^14.0.3",
99
+ dotenv: "^17.4.2",
96
100
  enquirer: "^2.4.1",
97
101
  fastmcp: "^4.0.1",
98
102
  figlet: "^1.11.0",
@@ -118,13 +122,16 @@ var import_figlet = __toESM(require("figlet"), 1);
118
122
  var import_chalk = __toESM(require("chalk"), 1);
119
123
 
120
124
  // src/proxy/index.ts
121
- var import_node_process3 = __toESM(require("process"), 1);
125
+ var import_node_process5 = __toESM(require("process"), 1);
122
126
  var import_stdio2 = require("@modelcontextprotocol/sdk/client/stdio.js");
123
127
 
124
128
  // src/config/schema.ts
125
129
  var import_zod = require("zod");
126
130
  var MCP_NAME_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
127
131
  var mcpName = import_zod.z.string().regex(MCP_NAME_PATTERN);
132
+ var envModeSchema = import_zod.z.enum(["enable", "dotenv", "process", "disable"]).describe(
133
+ '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.'
134
+ );
128
135
  var stdioTransport = import_zod.z.object({
129
136
  transport: import_zod.z.literal("stdio"),
130
137
  command: import_zod.z.string(),
@@ -150,55 +157,234 @@ var transportConfig = import_zod.z.discriminatedUnion("transport", [
150
157
  sseTransport
151
158
  ]);
152
159
  var mcpConfigSchema = import_zod.z.object({
160
+ env: envModeSchema.optional(),
153
161
  mcp: import_zod.z.record(mcpName, transportConfig).refine((obj) => Object.keys(obj).length > 0, { message: "At least one MCP must be configured" })
154
162
  });
155
163
 
156
164
  // src/config/loader.ts
157
- var import_node_fs = require("fs");
158
165
  var import_node_fs2 = require("fs");
159
- var import_node_path = require("path");
166
+ var import_node_path2 = require("path");
167
+ var import_node_process2 = __toESM(require("process"), 1);
160
168
  var import_yaml = require("yaml");
169
+
170
+ // src/config/env-sources.ts
171
+ var import_node_fs = require("fs");
172
+ var import_node_path = require("path");
173
+ var import_node_process = __toESM(require("process"), 1);
174
+ var import_dotenv = __toESM(require("dotenv"), 1);
175
+ var DEFAULT_DOTENV_FILENAME = ".env";
176
+ function loadEnv(options) {
177
+ const { mode, envFilePath, cwd = import_node_process.default.cwd(), processEnv = import_node_process.default.env } = options;
178
+ if (envFilePath !== void 0 && (mode === "disable" || mode === "process")) {
179
+ throw new Error(
180
+ `--env flag is incompatible with env mode "${mode}". --env requires env mode "enable" or "dotenv".`
181
+ );
182
+ }
183
+ if (mode === "disable") {
184
+ return { variables: {}, interpolationEnabled: false };
185
+ }
186
+ const dotenvVars = mode === "process" ? {} : readDotenvFile(envFilePath, cwd);
187
+ const processVars = mode === "dotenv" ? {} : filterDefined(processEnv);
188
+ const variables = { ...processVars, ...dotenvVars };
189
+ return { variables, interpolationEnabled: true };
190
+ }
191
+ function readDotenvFile(envFilePath, cwd) {
192
+ const isExplicit = envFilePath !== void 0;
193
+ const resolvedPath = isExplicit ? (0, import_node_path.resolve)(envFilePath) : (0, import_node_path.resolve)(cwd, DEFAULT_DOTENV_FILENAME);
194
+ if (!(0, import_node_fs.existsSync)(resolvedPath)) {
195
+ if (isExplicit) {
196
+ throw new Error(`.env file not found: ${resolvedPath}`);
197
+ }
198
+ return {};
199
+ }
200
+ let raw;
201
+ try {
202
+ raw = (0, import_node_fs.readFileSync)(resolvedPath, "utf-8");
203
+ } catch (readError) {
204
+ const message = readError instanceof Error ? readError.message : String(readError);
205
+ throw new Error(`Failed to read .env file (${resolvedPath}): ${message}`);
206
+ }
207
+ try {
208
+ return import_dotenv.default.parse(raw);
209
+ } catch (parseError) {
210
+ const message = parseError instanceof Error ? parseError.message : String(parseError);
211
+ throw new Error(`Failed to parse .env file (${resolvedPath}): ${message}`);
212
+ }
213
+ }
214
+ function filterDefined(env) {
215
+ const result = {};
216
+ for (const [key, value] of Object.entries(env)) {
217
+ if (value !== void 0) {
218
+ result[key] = value;
219
+ }
220
+ }
221
+ return result;
222
+ }
223
+
224
+ // src/config/interpolate.ts
225
+ var TOP_LEVEL_PASSTHROUGH_KEYS = /* @__PURE__ */ new Set(["$schema", "env"]);
226
+ var MissingEnvVarsError = class extends Error {
227
+ constructor(missingVars) {
228
+ const list = missingVars.join(", ");
229
+ const plural = missingVars.length === 1 ? "" : "s";
230
+ super(`Missing required environment variable${plural}: ${list}`);
231
+ this.missingVars = missingVars;
232
+ this.name = "MissingEnvVarsError";
233
+ }
234
+ missingVars;
235
+ };
236
+ function interpolateConfig(config, env) {
237
+ if (config === null || typeof config !== "object" || Array.isArray(config)) {
238
+ return config;
239
+ }
240
+ const missing = [];
241
+ const result = {};
242
+ for (const [key, value] of Object.entries(config)) {
243
+ if (TOP_LEVEL_PASSTHROUGH_KEYS.has(key)) {
244
+ result[key] = value;
245
+ } else {
246
+ result[key] = walkNode(value, env, missing);
247
+ }
248
+ }
249
+ if (missing.length > 0) {
250
+ const unique = Array.from(new Set(missing)).sort();
251
+ throw new MissingEnvVarsError(unique);
252
+ }
253
+ return result;
254
+ }
255
+ function walkNode(node, env, missing) {
256
+ if (typeof node === "string") {
257
+ return interpolateString(node, env, missing);
258
+ }
259
+ if (Array.isArray(node)) {
260
+ return node.map((item) => walkNode(item, env, missing));
261
+ }
262
+ if (node !== null && typeof node === "object") {
263
+ const result = {};
264
+ for (const [key, value] of Object.entries(node)) {
265
+ result[key] = walkNode(value, env, missing);
266
+ }
267
+ return result;
268
+ }
269
+ return node;
270
+ }
271
+ function interpolateString(value, env, missing) {
272
+ let result = "";
273
+ let i = 0;
274
+ const len = value.length;
275
+ while (i < len) {
276
+ const ch = value[i];
277
+ if (ch === "$" && value[i + 1] === "$" && value[i + 2] === "{") {
278
+ const close = value.indexOf("}", i + 3);
279
+ if (close === -1) {
280
+ result += ch;
281
+ i += 1;
282
+ continue;
283
+ }
284
+ result += value.substring(i + 1, close + 1);
285
+ i = close + 1;
286
+ continue;
287
+ }
288
+ if (ch === "$" && value[i + 1] === "{") {
289
+ const close = value.indexOf("}", i + 2);
290
+ if (close === -1) {
291
+ result += value.substring(i);
292
+ break;
293
+ }
294
+ const expr = value.substring(i + 2, close);
295
+ const { name, defaultValue } = parseExpr(expr);
296
+ const resolved = env[name];
297
+ const hasValue = resolved !== void 0 && resolved !== "";
298
+ if (hasValue) {
299
+ result += resolved;
300
+ } else if (defaultValue !== void 0) {
301
+ result += defaultValue;
302
+ } else if (resolved !== void 0) {
303
+ result += "";
304
+ } else {
305
+ missing.push(name);
306
+ }
307
+ i = close + 1;
308
+ continue;
309
+ }
310
+ result += ch;
311
+ i += 1;
312
+ }
313
+ return result;
314
+ }
315
+ function parseExpr(expr) {
316
+ const sep = expr.indexOf(":-");
317
+ if (sep === -1) {
318
+ return { name: expr, defaultValue: void 0 };
319
+ }
320
+ return {
321
+ name: expr.substring(0, sep),
322
+ defaultValue: expr.substring(sep + 2)
323
+ };
324
+ }
325
+
326
+ // src/config/loader.ts
161
327
  var AUTO_DISCOVER_NAMES = ["mcp.json", ".mcp.json"];
328
+ var DEFAULT_ENV_MODE = "enable";
329
+ var VALID_ENV_MODES = ["enable", "dotenv", "process", "disable"];
162
330
  function resolveConfigPath(explicitPath) {
163
331
  if (explicitPath) {
164
- const resolved = (0, import_node_path.resolve)(explicitPath);
332
+ const resolved = (0, import_node_path2.resolve)(explicitPath);
165
333
  if (!(0, import_node_fs2.existsSync)(resolved)) {
166
334
  throw new Error(`Config file not found: ${resolved}`);
167
335
  }
168
336
  return resolved;
169
337
  }
170
- const cwd = process.cwd();
338
+ const cwd = import_node_process2.default.cwd();
171
339
  for (const name of AUTO_DISCOVER_NAMES) {
172
- const candidate = (0, import_node_path.resolve)(cwd, name);
340
+ const candidate = (0, import_node_path2.resolve)(cwd, name);
173
341
  if ((0, import_node_fs2.existsSync)(candidate)) {
174
342
  return candidate;
175
343
  }
176
344
  }
177
- const searched = AUTO_DISCOVER_NAMES.map((n) => (0, import_node_path.resolve)(cwd, n)).join(", ");
345
+ const searched = AUTO_DISCOVER_NAMES.map((n) => (0, import_node_path2.resolve)(cwd, n)).join(", ");
178
346
  throw new Error(`No config file found. Searched: ${searched}`);
179
347
  }
180
- function loadConfig(explicitPath) {
181
- const configPath = resolveConfigPath(explicitPath);
182
- const raw = (0, import_node_fs.readFileSync)(configPath, "utf-8");
348
+ function loadConfig(options = {}) {
349
+ const { configPath, envFilePath } = options;
350
+ const resolvedPath = resolveConfigPath(configPath);
351
+ const raw = (0, import_node_fs2.readFileSync)(resolvedPath, "utf-8");
183
352
  let content;
184
353
  try {
185
- content = isYamlFile(configPath) ? (0, import_yaml.parse)(raw) : JSON.parse(raw);
354
+ content = isYamlFile(resolvedPath) ? (0, import_yaml.parse)(raw) : JSON.parse(raw);
186
355
  } catch (parseError) {
187
356
  const message = parseError instanceof Error ? parseError.message : String(parseError);
188
- throw new Error(`Failed to parse config file (${configPath}): ${message}`);
357
+ throw new Error(`Failed to parse config file (${resolvedPath}): ${message}`);
189
358
  }
190
- const result = mcpConfigSchema.safeParse(content);
359
+ const envMode = readEnvMode(content);
360
+ const loadedEnv = loadEnv({ mode: envMode, envFilePath });
361
+ const interpolated = loadedEnv.interpolationEnabled ? interpolateConfig(content, loadedEnv.variables) : content;
362
+ const result = mcpConfigSchema.safeParse(interpolated);
191
363
  if (!result.success) {
192
364
  const formatted = result.error.issues.map((issue) => ` - ${issue.path.join(".")}: ${issue.message}`).join("\n");
193
- throw new Error(`Invalid config file (${configPath}):
365
+ throw new Error(`Invalid config file (${resolvedPath}):
194
366
  ${formatted}`);
195
367
  }
196
368
  return result.data;
197
369
  }
370
+ function readEnvMode(content) {
371
+ if (content === null || typeof content !== "object" || Array.isArray(content)) {
372
+ return DEFAULT_ENV_MODE;
373
+ }
374
+ const value = content.env;
375
+ if (value === void 0) return DEFAULT_ENV_MODE;
376
+ if (typeof value === "string" && VALID_ENV_MODES.includes(value)) {
377
+ return value;
378
+ }
379
+ return DEFAULT_ENV_MODE;
380
+ }
198
381
  function isYamlFile(filePath) {
199
382
  return filePath.endsWith(".yml") || filePath.endsWith(".yaml");
200
383
  }
201
384
 
385
+ // src/config/json-schema.ts
386
+ var import_zod2 = require("zod");
387
+
202
388
  // src/proxy/transport-factory.ts
203
389
  var import_stdio = require("@modelcontextprotocol/sdk/client/stdio.js");
204
390
  var import_streamableHttp = require("@modelcontextprotocol/sdk/client/streamableHttp.js");
@@ -332,7 +518,7 @@ function buildAnnotationLines(tool) {
332
518
  }
333
519
 
334
520
  // src/proxy/upstream-client.ts
335
- var import_node_process = __toESM(require("process"), 1);
521
+ var import_node_process3 = __toESM(require("process"), 1);
336
522
  var import_client = require("@modelcontextprotocol/sdk/client/index.js");
337
523
  var UpstreamClient = class {
338
524
  transport;
@@ -341,7 +527,7 @@ var UpstreamClient = class {
341
527
  constructor({ name, transport, onTransportError }) {
342
528
  this.transport = transport;
343
529
  this.onTransportError = onTransportError ?? ((error) => {
344
- import_node_process.default.stderr.write(`[${name}] Upstream MCP transport error: ${error.message}
530
+ import_node_process3.default.stderr.write(`[${name}] Upstream MCP transport error: ${error.message}
345
531
  `);
346
532
  });
347
533
  }
@@ -453,9 +639,9 @@ var Orchestrator = class {
453
639
  };
454
640
 
455
641
  // src/proxy/server.ts
456
- var import_node_process2 = __toESM(require("process"), 1);
642
+ var import_node_process4 = __toESM(require("process"), 1);
457
643
  var import_fastmcp = require("fastmcp");
458
- var import_zod2 = require("zod");
644
+ var import_zod3 = require("zod");
459
645
  var ProxyServer = class {
460
646
  catalog;
461
647
  callTool;
@@ -471,7 +657,7 @@ var ProxyServer = class {
471
657
  server.addTool({
472
658
  name: "discover_tool",
473
659
  description: this.catalog.discoverToolDescription,
474
- parameters: import_zod2.z.object({ tool_name: import_zod2.z.string() }),
660
+ parameters: import_zod3.z.object({ tool_name: import_zod3.z.string() }),
475
661
  execute: async ({ tool_name }) => {
476
662
  return this.catalog.getToolDetails(tool_name);
477
663
  }
@@ -479,9 +665,9 @@ var ProxyServer = class {
479
665
  server.addTool({
480
666
  name: "use_tool",
481
667
  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({})
668
+ parameters: import_zod3.z.object({
669
+ tool_name: import_zod3.z.string(),
670
+ tool_input: import_zod3.z.record(import_zod3.z.string(), import_zod3.z.unknown()).default({})
485
671
  }),
486
672
  execute: async ({ tool_name, tool_input }) => {
487
673
  if (!this.catalog.tools.has(tool_name)) {
@@ -491,7 +677,7 @@ var ProxyServer = class {
491
677
  return result;
492
678
  }
493
679
  });
494
- import_node_process2.default.stderr.write("Starting dynamic-discovery-mcp server over stdio\n");
680
+ import_node_process4.default.stderr.write("Starting dynamic-discovery-mcp server over stdio\n");
495
681
  await server.start({ transportType: "stdio" });
496
682
  }
497
683
  };
@@ -504,7 +690,7 @@ async function startProxy(command, args) {
504
690
  name: command,
505
691
  transport,
506
692
  onTransportError: (error) => {
507
- import_node_process3.default.stderr.write(`Upstream MCP transport error: ${error.message}
693
+ import_node_process5.default.stderr.write(`Upstream MCP transport error: ${error.message}
508
694
  `);
509
695
  shutdown(1);
510
696
  }
@@ -513,36 +699,36 @@ async function startProxy(command, args) {
513
699
  if (isShuttingDown) return;
514
700
  isShuttingDown = true;
515
701
  upstreamClient.disconnect().catch((error) => {
516
- import_node_process3.default.stderr.write(
702
+ import_node_process5.default.stderr.write(
517
703
  `dynmcp: error during disconnect: ${error instanceof Error ? error.message : String(error)}
518
704
  `
519
705
  );
520
- }).finally(() => import_node_process3.default.exit(exitCode));
706
+ }).finally(() => import_node_process5.default.exit(exitCode));
521
707
  };
522
708
  try {
523
709
  await upstreamClient.connect();
524
710
  } catch (error) {
525
- import_node_process3.default.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
711
+ import_node_process5.default.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
526
712
  `);
527
- import_node_process3.default.exit(1);
713
+ import_node_process5.default.exit(1);
528
714
  }
529
715
  let tools;
530
716
  try {
531
717
  tools = await upstreamClient.listTools();
532
718
  } catch (error) {
533
- import_node_process3.default.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
719
+ import_node_process5.default.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
534
720
  `);
535
- import_node_process3.default.exit(1);
721
+ import_node_process5.default.exit(1);
536
722
  }
537
723
  const catalog = ToolCatalog.fromFlat(tools);
538
724
  const proxyServer = new ProxyServer({
539
725
  catalog,
540
726
  callTool: (name, input) => upstreamClient.callTool(name, input)
541
727
  });
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));
728
+ import_node_process5.default.on("SIGINT", () => shutdown(0));
729
+ import_node_process5.default.on("SIGTERM", () => shutdown(0));
730
+ import_node_process5.default.stdin.on("end", () => shutdown(0));
731
+ import_node_process5.default.stdin.on("close", () => shutdown(0));
546
732
  try {
547
733
  await proxyServer.start();
548
734
  } catch (error) {
@@ -550,9 +736,9 @@ async function startProxy(command, args) {
550
736
  throw error;
551
737
  }
552
738
  }
553
- async function startProxyFromConfig(configPath) {
739
+ async function startProxyFromConfig(options = {}) {
554
740
  let isShuttingDown = false;
555
- const config = loadConfig(configPath);
741
+ const config = loadConfig(options);
556
742
  const mcps = /* @__PURE__ */ new Map();
557
743
  for (const [name, entry] of Object.entries(config.mcp)) {
558
744
  mcps.set(name, { transport: createTransport(entry) });
@@ -560,7 +746,7 @@ async function startProxyFromConfig(configPath) {
560
746
  const orchestrator = new Orchestrator({
561
747
  mcps,
562
748
  onTransportError: (mcpName2, error) => {
563
- import_node_process3.default.stderr.write(`Upstream MCP "${mcpName2}" transport error: ${error.message}
749
+ import_node_process5.default.stderr.write(`Upstream MCP "${mcpName2}" transport error: ${error.message}
564
750
  `);
565
751
  shutdown(1);
566
752
  }
@@ -569,27 +755,27 @@ async function startProxyFromConfig(configPath) {
569
755
  if (isShuttingDown) return;
570
756
  isShuttingDown = true;
571
757
  orchestrator.disconnectAll().catch((error) => {
572
- import_node_process3.default.stderr.write(
758
+ import_node_process5.default.stderr.write(
573
759
  `dynmcp: error during disconnect: ${error instanceof Error ? error.message : String(error)}
574
760
  `
575
761
  );
576
- }).finally(() => import_node_process3.default.exit(exitCode));
762
+ }).finally(() => import_node_process5.default.exit(exitCode));
577
763
  };
578
764
  try {
579
765
  await orchestrator.connect();
580
766
  } catch (error) {
581
- import_node_process3.default.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
767
+ import_node_process5.default.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
582
768
  `);
583
- import_node_process3.default.exit(1);
769
+ import_node_process5.default.exit(1);
584
770
  }
585
771
  const proxyServer = new ProxyServer({
586
772
  catalog: orchestrator.catalog,
587
773
  callTool: (name, input) => orchestrator.callTool(name, input)
588
774
  });
589
- import_node_process3.default.on("SIGINT", () => shutdown(0));
590
- import_node_process3.default.on("SIGTERM", () => shutdown(0));
591
- import_node_process3.default.stdin.on("end", () => shutdown(0));
592
- import_node_process3.default.stdin.on("close", () => shutdown(0));
775
+ import_node_process5.default.on("SIGINT", () => shutdown(0));
776
+ import_node_process5.default.on("SIGTERM", () => shutdown(0));
777
+ import_node_process5.default.stdin.on("end", () => shutdown(0));
778
+ import_node_process5.default.stdin.on("close", () => shutdown(0));
593
779
  try {
594
780
  await proxyServer.start();
595
781
  } catch (error) {
@@ -609,39 +795,43 @@ var cliBanner = import_chalk.default.bold.magentaBright(
609
795
  var cli = new import_commander.Command(package_default.name).description(package_default.description).version(package_default.version).addHelpText("beforeAll", cliBanner).addHelpText(
610
796
  "after",
611
797
  "\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) => {
613
- const separatorIndex = import_node_process4.default.argv.indexOf("--");
798
+ ).option("-c, --config <path>", "Path to config file (JSON or YAML)").option(
799
+ "-e, --env <path>",
800
+ "Path to a .env file for environment variable interpolation"
801
+ ).allowExcessArguments(true).passThroughOptions(true).action(async (_options, cmd) => {
802
+ const separatorIndex = import_node_process6.default.argv.indexOf("--");
614
803
  const configPath = cmd.opts().config;
804
+ const envFilePath = cmd.opts().env;
615
805
  if (separatorIndex !== -1) {
616
- const [command, ...args] = import_node_process4.default.argv.slice(separatorIndex + 1);
806
+ const [command, ...args] = import_node_process6.default.argv.slice(separatorIndex + 1);
617
807
  if (command === void 0) {
618
- import_node_process4.default.stderr.write(
808
+ import_node_process6.default.stderr.write(
619
809
  "dynmcp: no upstream command provided after --.\nUsage: dynmcp -- <command> [args...]\n"
620
810
  );
621
- import_node_process4.default.exit(1);
811
+ import_node_process6.default.exit(1);
622
812
  }
623
813
  try {
624
814
  await startProxy(command, args);
625
815
  } catch (error) {
626
- import_node_process4.default.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
816
+ import_node_process6.default.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
627
817
  `);
628
- import_node_process4.default.exit(1);
818
+ import_node_process6.default.exit(1);
629
819
  }
630
820
  return;
631
821
  }
632
822
  try {
633
- await startProxyFromConfig(configPath);
823
+ await startProxyFromConfig({ configPath, envFilePath });
634
824
  } catch (error) {
635
- import_node_process4.default.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
825
+ import_node_process6.default.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
636
826
  `);
637
- import_node_process4.default.exit(1);
827
+ import_node_process6.default.exit(1);
638
828
  }
639
829
  });
640
830
 
641
831
  // src/index.ts
642
- var import_node_process5 = __toESM(require("process"), 1);
832
+ var import_node_process7 = __toESM(require("process"), 1);
643
833
  async function main() {
644
- cli.parse(import_node_process5.default.argv);
834
+ cli.parse(import_node_process7.default.argv);
645
835
  }
646
836
  main();
647
837
  //# sourceMappingURL=index.cjs.map