dynmcp 0.1.1 → 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/dist/index.js CHANGED
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import process5 from "process";
4
+ import process6 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.1.1",
10
+ version: "0.2.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",
@@ -99,13 +100,16 @@ import figlet from "figlet";
99
100
  import chalk from "chalk";
100
101
 
101
102
  // src/proxy/index.ts
102
- import process4 from "process";
103
+ import process5 from "process";
103
104
  import { StdioClientTransport as StdioClientTransport2 } from "@modelcontextprotocol/sdk/client/stdio.js";
104
105
 
105
106
  // src/config/schema.ts
106
107
  import { z } from "zod";
107
108
  var MCP_NAME_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
108
109
  var mcpName = z.string().regex(MCP_NAME_PATTERN);
110
+ var envModeSchema = z.enum(["enable", "dotenv", "process", "disable"]).describe(
111
+ '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.'
112
+ );
109
113
  var stdioTransport = z.object({
110
114
  transport: z.literal("stdio"),
111
115
  command: z.string(),
@@ -131,51 +135,227 @@ var transportConfig = z.discriminatedUnion("transport", [
131
135
  sseTransport
132
136
  ]);
133
137
  var mcpConfigSchema = z.object({
138
+ env: envModeSchema.optional(),
134
139
  mcp: z.record(mcpName, transportConfig).refine((obj) => Object.keys(obj).length > 0, { message: "At least one MCP must be configured" })
135
140
  });
136
141
 
137
142
  // src/config/loader.ts
138
- import { readFileSync } from "fs";
139
- import { existsSync } from "fs";
140
- import { resolve } from "path";
143
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
144
+ import { resolve as resolve2 } from "path";
145
+ import process2 from "process";
141
146
  import { parse as parseYaml } from "yaml";
147
+
148
+ // src/config/env-sources.ts
149
+ import { existsSync, readFileSync } from "fs";
150
+ import { resolve } from "path";
151
+ import process from "process";
152
+ import dotenv from "dotenv";
153
+ var DEFAULT_DOTENV_FILENAME = ".env";
154
+ function loadEnv(options) {
155
+ const { mode, envFilePath, cwd = process.cwd(), processEnv = process.env } = options;
156
+ if (envFilePath !== void 0 && (mode === "disable" || mode === "process")) {
157
+ throw new Error(
158
+ `--env flag is incompatible with env mode "${mode}". --env requires env mode "enable" or "dotenv".`
159
+ );
160
+ }
161
+ if (mode === "disable") {
162
+ return { variables: {}, interpolationEnabled: false };
163
+ }
164
+ const dotenvVars = mode === "process" ? {} : readDotenvFile(envFilePath, cwd);
165
+ const processVars = mode === "dotenv" ? {} : filterDefined(processEnv);
166
+ const variables = { ...processVars, ...dotenvVars };
167
+ return { variables, interpolationEnabled: true };
168
+ }
169
+ function readDotenvFile(envFilePath, cwd) {
170
+ const isExplicit = envFilePath !== void 0;
171
+ const resolvedPath = isExplicit ? resolve(envFilePath) : resolve(cwd, DEFAULT_DOTENV_FILENAME);
172
+ if (!existsSync(resolvedPath)) {
173
+ if (isExplicit) {
174
+ throw new Error(`.env file not found: ${resolvedPath}`);
175
+ }
176
+ return {};
177
+ }
178
+ let raw;
179
+ try {
180
+ raw = readFileSync(resolvedPath, "utf-8");
181
+ } catch (readError) {
182
+ const message = readError instanceof Error ? readError.message : String(readError);
183
+ throw new Error(`Failed to read .env file (${resolvedPath}): ${message}`);
184
+ }
185
+ try {
186
+ return dotenv.parse(raw);
187
+ } catch (parseError) {
188
+ const message = parseError instanceof Error ? parseError.message : String(parseError);
189
+ throw new Error(`Failed to parse .env file (${resolvedPath}): ${message}`);
190
+ }
191
+ }
192
+ function filterDefined(env) {
193
+ const result = {};
194
+ for (const [key, value] of Object.entries(env)) {
195
+ if (value !== void 0) {
196
+ result[key] = value;
197
+ }
198
+ }
199
+ return result;
200
+ }
201
+
202
+ // src/config/interpolate.ts
203
+ var TOP_LEVEL_PASSTHROUGH_KEYS = /* @__PURE__ */ new Set(["$schema", "env"]);
204
+ var MissingEnvVarsError = class extends Error {
205
+ constructor(missingVars) {
206
+ const list = missingVars.join(", ");
207
+ const plural = missingVars.length === 1 ? "" : "s";
208
+ super(`Missing required environment variable${plural}: ${list}`);
209
+ this.missingVars = missingVars;
210
+ this.name = "MissingEnvVarsError";
211
+ }
212
+ missingVars;
213
+ };
214
+ function interpolateConfig(config, env) {
215
+ if (config === null || typeof config !== "object" || Array.isArray(config)) {
216
+ return config;
217
+ }
218
+ const missing = [];
219
+ const result = {};
220
+ for (const [key, value] of Object.entries(config)) {
221
+ if (TOP_LEVEL_PASSTHROUGH_KEYS.has(key)) {
222
+ result[key] = value;
223
+ } else {
224
+ result[key] = walkNode(value, env, missing);
225
+ }
226
+ }
227
+ if (missing.length > 0) {
228
+ const unique = Array.from(new Set(missing)).sort();
229
+ throw new MissingEnvVarsError(unique);
230
+ }
231
+ return result;
232
+ }
233
+ function walkNode(node, env, missing) {
234
+ if (typeof node === "string") {
235
+ return interpolateString(node, env, missing);
236
+ }
237
+ if (Array.isArray(node)) {
238
+ return node.map((item) => walkNode(item, env, missing));
239
+ }
240
+ if (node !== null && typeof node === "object") {
241
+ const result = {};
242
+ for (const [key, value] of Object.entries(node)) {
243
+ result[key] = walkNode(value, env, missing);
244
+ }
245
+ return result;
246
+ }
247
+ return node;
248
+ }
249
+ function interpolateString(value, env, missing) {
250
+ let result = "";
251
+ let i = 0;
252
+ const len = value.length;
253
+ while (i < len) {
254
+ const ch = value[i];
255
+ if (ch === "$" && value[i + 1] === "$" && value[i + 2] === "{") {
256
+ const close = value.indexOf("}", i + 3);
257
+ if (close === -1) {
258
+ result += ch;
259
+ i += 1;
260
+ continue;
261
+ }
262
+ result += value.substring(i + 1, close + 1);
263
+ i = close + 1;
264
+ continue;
265
+ }
266
+ if (ch === "$" && value[i + 1] === "{") {
267
+ const close = value.indexOf("}", i + 2);
268
+ if (close === -1) {
269
+ result += value.substring(i);
270
+ break;
271
+ }
272
+ const expr = value.substring(i + 2, close);
273
+ const { name, defaultValue } = parseExpr(expr);
274
+ const resolved = env[name];
275
+ const hasValue = resolved !== void 0 && resolved !== "";
276
+ if (hasValue) {
277
+ result += resolved;
278
+ } else if (defaultValue !== void 0) {
279
+ result += defaultValue;
280
+ } else if (resolved !== void 0) {
281
+ result += "";
282
+ } else {
283
+ missing.push(name);
284
+ }
285
+ i = close + 1;
286
+ continue;
287
+ }
288
+ result += ch;
289
+ i += 1;
290
+ }
291
+ return result;
292
+ }
293
+ function parseExpr(expr) {
294
+ const sep = expr.indexOf(":-");
295
+ if (sep === -1) {
296
+ return { name: expr, defaultValue: void 0 };
297
+ }
298
+ return {
299
+ name: expr.substring(0, sep),
300
+ defaultValue: expr.substring(sep + 2)
301
+ };
302
+ }
303
+
304
+ // src/config/loader.ts
142
305
  var AUTO_DISCOVER_NAMES = ["mcp.json", ".mcp.json"];
306
+ var DEFAULT_ENV_MODE = "enable";
307
+ var VALID_ENV_MODES = ["enable", "dotenv", "process", "disable"];
143
308
  function resolveConfigPath(explicitPath) {
144
309
  if (explicitPath) {
145
- const resolved = resolve(explicitPath);
146
- if (!existsSync(resolved)) {
310
+ const resolved = resolve2(explicitPath);
311
+ if (!existsSync2(resolved)) {
147
312
  throw new Error(`Config file not found: ${resolved}`);
148
313
  }
149
314
  return resolved;
150
315
  }
151
- const cwd = process.cwd();
316
+ const cwd = process2.cwd();
152
317
  for (const name of AUTO_DISCOVER_NAMES) {
153
- const candidate = resolve(cwd, name);
154
- if (existsSync(candidate)) {
318
+ const candidate = resolve2(cwd, name);
319
+ if (existsSync2(candidate)) {
155
320
  return candidate;
156
321
  }
157
322
  }
158
- const searched = AUTO_DISCOVER_NAMES.map((n) => resolve(cwd, n)).join(", ");
323
+ const searched = AUTO_DISCOVER_NAMES.map((n) => resolve2(cwd, n)).join(", ");
159
324
  throw new Error(`No config file found. Searched: ${searched}`);
160
325
  }
161
- function loadConfig(explicitPath) {
162
- const configPath = resolveConfigPath(explicitPath);
163
- const raw = readFileSync(configPath, "utf-8");
326
+ function loadConfig(options = {}) {
327
+ const { configPath, envFilePath } = options;
328
+ const resolvedPath = resolveConfigPath(configPath);
329
+ const raw = readFileSync2(resolvedPath, "utf-8");
164
330
  let content;
165
331
  try {
166
- content = isYamlFile(configPath) ? parseYaml(raw) : JSON.parse(raw);
332
+ content = isYamlFile(resolvedPath) ? parseYaml(raw) : JSON.parse(raw);
167
333
  } catch (parseError) {
168
334
  const message = parseError instanceof Error ? parseError.message : String(parseError);
169
- throw new Error(`Failed to parse config file (${configPath}): ${message}`);
335
+ throw new Error(`Failed to parse config file (${resolvedPath}): ${message}`);
170
336
  }
171
- const result = mcpConfigSchema.safeParse(content);
337
+ const envMode = readEnvMode(content);
338
+ const loadedEnv = loadEnv({ mode: envMode, envFilePath });
339
+ const interpolated = loadedEnv.interpolationEnabled ? interpolateConfig(content, loadedEnv.variables) : content;
340
+ const result = mcpConfigSchema.safeParse(interpolated);
172
341
  if (!result.success) {
173
342
  const formatted = result.error.issues.map((issue) => ` - ${issue.path.join(".")}: ${issue.message}`).join("\n");
174
- throw new Error(`Invalid config file (${configPath}):
343
+ throw new Error(`Invalid config file (${resolvedPath}):
175
344
  ${formatted}`);
176
345
  }
177
346
  return result.data;
178
347
  }
348
+ function readEnvMode(content) {
349
+ if (content === null || typeof content !== "object" || Array.isArray(content)) {
350
+ return DEFAULT_ENV_MODE;
351
+ }
352
+ const value = content.env;
353
+ if (value === void 0) return DEFAULT_ENV_MODE;
354
+ if (typeof value === "string" && VALID_ENV_MODES.includes(value)) {
355
+ return value;
356
+ }
357
+ return DEFAULT_ENV_MODE;
358
+ }
179
359
  function isYamlFile(filePath) {
180
360
  return filePath.endsWith(".yml") || filePath.endsWith(".yaml");
181
361
  }
@@ -316,7 +496,7 @@ function buildAnnotationLines(tool) {
316
496
  }
317
497
 
318
498
  // src/proxy/upstream-client.ts
319
- import process2 from "process";
499
+ import process3 from "process";
320
500
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
321
501
  var UpstreamClient = class {
322
502
  transport;
@@ -325,7 +505,7 @@ var UpstreamClient = class {
325
505
  constructor({ name, transport, onTransportError }) {
326
506
  this.transport = transport;
327
507
  this.onTransportError = onTransportError ?? ((error) => {
328
- process2.stderr.write(`[${name}] Upstream MCP transport error: ${error.message}
508
+ process3.stderr.write(`[${name}] Upstream MCP transport error: ${error.message}
329
509
  `);
330
510
  });
331
511
  }
@@ -437,7 +617,7 @@ var Orchestrator = class {
437
617
  };
438
618
 
439
619
  // src/proxy/server.ts
440
- import process3 from "process";
620
+ import process4 from "process";
441
621
  import { FastMCP } from "fastmcp";
442
622
  import { z as z3 } from "zod";
443
623
  var ProxyServer = class {
@@ -475,7 +655,7 @@ var ProxyServer = class {
475
655
  return result;
476
656
  }
477
657
  });
478
- process3.stderr.write("Starting dynamic-discovery-mcp server over stdio\n");
658
+ process4.stderr.write("Starting dynamic-discovery-mcp server over stdio\n");
479
659
  await server.start({ transportType: "stdio" });
480
660
  }
481
661
  };
@@ -488,7 +668,7 @@ async function startProxy(command, args) {
488
668
  name: command,
489
669
  transport,
490
670
  onTransportError: (error) => {
491
- process4.stderr.write(`Upstream MCP transport error: ${error.message}
671
+ process5.stderr.write(`Upstream MCP transport error: ${error.message}
492
672
  `);
493
673
  shutdown(1);
494
674
  }
@@ -497,36 +677,36 @@ async function startProxy(command, args) {
497
677
  if (isShuttingDown) return;
498
678
  isShuttingDown = true;
499
679
  upstreamClient.disconnect().catch((error) => {
500
- process4.stderr.write(
680
+ process5.stderr.write(
501
681
  `dynmcp: error during disconnect: ${error instanceof Error ? error.message : String(error)}
502
682
  `
503
683
  );
504
- }).finally(() => process4.exit(exitCode));
684
+ }).finally(() => process5.exit(exitCode));
505
685
  };
506
686
  try {
507
687
  await upstreamClient.connect();
508
688
  } catch (error) {
509
- process4.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
689
+ process5.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
510
690
  `);
511
- process4.exit(1);
691
+ process5.exit(1);
512
692
  }
513
693
  let tools;
514
694
  try {
515
695
  tools = await upstreamClient.listTools();
516
696
  } catch (error) {
517
- process4.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
697
+ process5.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
518
698
  `);
519
- process4.exit(1);
699
+ process5.exit(1);
520
700
  }
521
701
  const catalog = ToolCatalog.fromFlat(tools);
522
702
  const proxyServer = new ProxyServer({
523
703
  catalog,
524
704
  callTool: (name, input) => upstreamClient.callTool(name, input)
525
705
  });
526
- process4.on("SIGINT", () => shutdown(0));
527
- process4.on("SIGTERM", () => shutdown(0));
528
- process4.stdin.on("end", () => shutdown(0));
529
- process4.stdin.on("close", () => shutdown(0));
706
+ process5.on("SIGINT", () => shutdown(0));
707
+ process5.on("SIGTERM", () => shutdown(0));
708
+ process5.stdin.on("end", () => shutdown(0));
709
+ process5.stdin.on("close", () => shutdown(0));
530
710
  try {
531
711
  await proxyServer.start();
532
712
  } catch (error) {
@@ -534,9 +714,9 @@ async function startProxy(command, args) {
534
714
  throw error;
535
715
  }
536
716
  }
537
- async function startProxyFromConfig(configPath) {
717
+ async function startProxyFromConfig(options = {}) {
538
718
  let isShuttingDown = false;
539
- const config = loadConfig(configPath);
719
+ const config = loadConfig(options);
540
720
  const mcps = /* @__PURE__ */ new Map();
541
721
  for (const [name, entry] of Object.entries(config.mcp)) {
542
722
  mcps.set(name, { transport: createTransport(entry) });
@@ -544,7 +724,7 @@ async function startProxyFromConfig(configPath) {
544
724
  const orchestrator = new Orchestrator({
545
725
  mcps,
546
726
  onTransportError: (mcpName2, error) => {
547
- process4.stderr.write(`Upstream MCP "${mcpName2}" transport error: ${error.message}
727
+ process5.stderr.write(`Upstream MCP "${mcpName2}" transport error: ${error.message}
548
728
  `);
549
729
  shutdown(1);
550
730
  }
@@ -553,27 +733,27 @@ async function startProxyFromConfig(configPath) {
553
733
  if (isShuttingDown) return;
554
734
  isShuttingDown = true;
555
735
  orchestrator.disconnectAll().catch((error) => {
556
- process4.stderr.write(
736
+ process5.stderr.write(
557
737
  `dynmcp: error during disconnect: ${error instanceof Error ? error.message : String(error)}
558
738
  `
559
739
  );
560
- }).finally(() => process4.exit(exitCode));
740
+ }).finally(() => process5.exit(exitCode));
561
741
  };
562
742
  try {
563
743
  await orchestrator.connect();
564
744
  } catch (error) {
565
- process4.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
745
+ process5.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
566
746
  `);
567
- process4.exit(1);
747
+ process5.exit(1);
568
748
  }
569
749
  const proxyServer = new ProxyServer({
570
750
  catalog: orchestrator.catalog,
571
751
  callTool: (name, input) => orchestrator.callTool(name, input)
572
752
  });
573
- process4.on("SIGINT", () => shutdown(0));
574
- process4.on("SIGTERM", () => shutdown(0));
575
- process4.stdin.on("end", () => shutdown(0));
576
- process4.stdin.on("close", () => shutdown(0));
753
+ process5.on("SIGINT", () => shutdown(0));
754
+ process5.on("SIGTERM", () => shutdown(0));
755
+ process5.stdin.on("end", () => shutdown(0));
756
+ process5.stdin.on("close", () => shutdown(0));
577
757
  try {
578
758
  await proxyServer.start();
579
759
  } catch (error) {
@@ -593,39 +773,43 @@ var cliBanner = chalk.bold.magentaBright(
593
773
  var cli = new Command(package_default.name).description(package_default.description).version(package_default.version).addHelpText("beforeAll", cliBanner).addHelpText(
594
774
  "after",
595
775
  "\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 = process5.argv.indexOf("--");
776
+ ).option("-c, --config <path>", "Path to config file (JSON or YAML)").option(
777
+ "-e, --env <path>",
778
+ "Path to a .env file for environment variable interpolation"
779
+ ).allowExcessArguments(true).passThroughOptions(true).action(async (_options, cmd) => {
780
+ const separatorIndex = process6.argv.indexOf("--");
598
781
  const configPath = cmd.opts().config;
782
+ const envFilePath = cmd.opts().env;
599
783
  if (separatorIndex !== -1) {
600
- const [command, ...args] = process5.argv.slice(separatorIndex + 1);
784
+ const [command, ...args] = process6.argv.slice(separatorIndex + 1);
601
785
  if (command === void 0) {
602
- process5.stderr.write(
786
+ process6.stderr.write(
603
787
  "dynmcp: no upstream command provided after --.\nUsage: dynmcp -- <command> [args...]\n"
604
788
  );
605
- process5.exit(1);
789
+ process6.exit(1);
606
790
  }
607
791
  try {
608
792
  await startProxy(command, args);
609
793
  } catch (error) {
610
- process5.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
794
+ process6.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
611
795
  `);
612
- process5.exit(1);
796
+ process6.exit(1);
613
797
  }
614
798
  return;
615
799
  }
616
800
  try {
617
- await startProxyFromConfig(configPath);
801
+ await startProxyFromConfig({ configPath, envFilePath });
618
802
  } catch (error) {
619
- process5.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
803
+ process6.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
620
804
  `);
621
- process5.exit(1);
805
+ process6.exit(1);
622
806
  }
623
807
  });
624
808
 
625
809
  // src/index.ts
626
- import process6 from "process";
810
+ import process7 from "process";
627
811
  async function main() {
628
- cli.parse(process6.argv);
812
+ cli.parse(process7.argv);
629
813
  }
630
814
  main();
631
815
  //# sourceMappingURL=index.js.map