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/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.0",
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",
@@ -52,9 +52,12 @@ var package_default = {
52
52
  }
53
53
  },
54
54
  files: [
55
- "dist"
55
+ "dist",
56
+ "schema"
56
57
  ],
57
58
  scripts: {
59
+ "generate:schema": "tsx scripts/generate-schema.ts",
60
+ prebuild: "tsx scripts/generate-schema.ts",
58
61
  build: "tsup",
59
62
  dev: "tsx src/index.ts",
60
63
  typecheck: "tsc --noEmit",
@@ -71,6 +74,7 @@ var package_default = {
71
74
  boxen: "^8.0.1",
72
75
  chalk: "^5.6.2",
73
76
  commander: "^14.0.3",
77
+ dotenv: "^17.4.2",
74
78
  enquirer: "^2.4.1",
75
79
  fastmcp: "^4.0.1",
76
80
  figlet: "^1.11.0",
@@ -96,13 +100,16 @@ import figlet from "figlet";
96
100
  import chalk from "chalk";
97
101
 
98
102
  // src/proxy/index.ts
99
- import process4 from "process";
103
+ import process5 from "process";
100
104
  import { StdioClientTransport as StdioClientTransport2 } from "@modelcontextprotocol/sdk/client/stdio.js";
101
105
 
102
106
  // src/config/schema.ts
103
107
  import { z } from "zod";
104
108
  var MCP_NAME_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
105
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
+ );
106
113
  var stdioTransport = z.object({
107
114
  transport: z.literal("stdio"),
108
115
  command: z.string(),
@@ -128,55 +135,234 @@ var transportConfig = z.discriminatedUnion("transport", [
128
135
  sseTransport
129
136
  ]);
130
137
  var mcpConfigSchema = z.object({
138
+ env: envModeSchema.optional(),
131
139
  mcp: z.record(mcpName, transportConfig).refine((obj) => Object.keys(obj).length > 0, { message: "At least one MCP must be configured" })
132
140
  });
133
141
 
134
142
  // src/config/loader.ts
135
- import { readFileSync } from "fs";
136
- import { existsSync } from "fs";
137
- 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";
138
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
139
305
  var AUTO_DISCOVER_NAMES = ["mcp.json", ".mcp.json"];
306
+ var DEFAULT_ENV_MODE = "enable";
307
+ var VALID_ENV_MODES = ["enable", "dotenv", "process", "disable"];
140
308
  function resolveConfigPath(explicitPath) {
141
309
  if (explicitPath) {
142
- const resolved = resolve(explicitPath);
143
- if (!existsSync(resolved)) {
310
+ const resolved = resolve2(explicitPath);
311
+ if (!existsSync2(resolved)) {
144
312
  throw new Error(`Config file not found: ${resolved}`);
145
313
  }
146
314
  return resolved;
147
315
  }
148
- const cwd = process.cwd();
316
+ const cwd = process2.cwd();
149
317
  for (const name of AUTO_DISCOVER_NAMES) {
150
- const candidate = resolve(cwd, name);
151
- if (existsSync(candidate)) {
318
+ const candidate = resolve2(cwd, name);
319
+ if (existsSync2(candidate)) {
152
320
  return candidate;
153
321
  }
154
322
  }
155
- const searched = AUTO_DISCOVER_NAMES.map((n) => resolve(cwd, n)).join(", ");
323
+ const searched = AUTO_DISCOVER_NAMES.map((n) => resolve2(cwd, n)).join(", ");
156
324
  throw new Error(`No config file found. Searched: ${searched}`);
157
325
  }
158
- function loadConfig(explicitPath) {
159
- const configPath = resolveConfigPath(explicitPath);
160
- 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");
161
330
  let content;
162
331
  try {
163
- content = isYamlFile(configPath) ? parseYaml(raw) : JSON.parse(raw);
332
+ content = isYamlFile(resolvedPath) ? parseYaml(raw) : JSON.parse(raw);
164
333
  } catch (parseError) {
165
334
  const message = parseError instanceof Error ? parseError.message : String(parseError);
166
- throw new Error(`Failed to parse config file (${configPath}): ${message}`);
335
+ throw new Error(`Failed to parse config file (${resolvedPath}): ${message}`);
167
336
  }
168
- 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);
169
341
  if (!result.success) {
170
342
  const formatted = result.error.issues.map((issue) => ` - ${issue.path.join(".")}: ${issue.message}`).join("\n");
171
- throw new Error(`Invalid config file (${configPath}):
343
+ throw new Error(`Invalid config file (${resolvedPath}):
172
344
  ${formatted}`);
173
345
  }
174
346
  return result.data;
175
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
+ }
176
359
  function isYamlFile(filePath) {
177
360
  return filePath.endsWith(".yml") || filePath.endsWith(".yaml");
178
361
  }
179
362
 
363
+ // src/config/json-schema.ts
364
+ import { z as z2 } from "zod";
365
+
180
366
  // src/proxy/transport-factory.ts
181
367
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
182
368
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
@@ -310,7 +496,7 @@ function buildAnnotationLines(tool) {
310
496
  }
311
497
 
312
498
  // src/proxy/upstream-client.ts
313
- import process2 from "process";
499
+ import process3 from "process";
314
500
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
315
501
  var UpstreamClient = class {
316
502
  transport;
@@ -319,7 +505,7 @@ var UpstreamClient = class {
319
505
  constructor({ name, transport, onTransportError }) {
320
506
  this.transport = transport;
321
507
  this.onTransportError = onTransportError ?? ((error) => {
322
- process2.stderr.write(`[${name}] Upstream MCP transport error: ${error.message}
508
+ process3.stderr.write(`[${name}] Upstream MCP transport error: ${error.message}
323
509
  `);
324
510
  });
325
511
  }
@@ -431,9 +617,9 @@ var Orchestrator = class {
431
617
  };
432
618
 
433
619
  // src/proxy/server.ts
434
- import process3 from "process";
620
+ import process4 from "process";
435
621
  import { FastMCP } from "fastmcp";
436
- import { z as z2 } from "zod";
622
+ import { z as z3 } from "zod";
437
623
  var ProxyServer = class {
438
624
  catalog;
439
625
  callTool;
@@ -449,7 +635,7 @@ var ProxyServer = class {
449
635
  server.addTool({
450
636
  name: "discover_tool",
451
637
  description: this.catalog.discoverToolDescription,
452
- parameters: z2.object({ tool_name: z2.string() }),
638
+ parameters: z3.object({ tool_name: z3.string() }),
453
639
  execute: async ({ tool_name }) => {
454
640
  return this.catalog.getToolDetails(tool_name);
455
641
  }
@@ -457,9 +643,9 @@ var ProxyServer = class {
457
643
  server.addTool({
458
644
  name: "use_tool",
459
645
  description: "Use a tool that was previously discovered with the discover_tool tool.",
460
- parameters: z2.object({
461
- tool_name: z2.string(),
462
- tool_input: z2.record(z2.string(), z2.unknown()).default({})
646
+ parameters: z3.object({
647
+ tool_name: z3.string(),
648
+ tool_input: z3.record(z3.string(), z3.unknown()).default({})
463
649
  }),
464
650
  execute: async ({ tool_name, tool_input }) => {
465
651
  if (!this.catalog.tools.has(tool_name)) {
@@ -469,7 +655,7 @@ var ProxyServer = class {
469
655
  return result;
470
656
  }
471
657
  });
472
- process3.stderr.write("Starting dynamic-discovery-mcp server over stdio\n");
658
+ process4.stderr.write("Starting dynamic-discovery-mcp server over stdio\n");
473
659
  await server.start({ transportType: "stdio" });
474
660
  }
475
661
  };
@@ -482,7 +668,7 @@ async function startProxy(command, args) {
482
668
  name: command,
483
669
  transport,
484
670
  onTransportError: (error) => {
485
- process4.stderr.write(`Upstream MCP transport error: ${error.message}
671
+ process5.stderr.write(`Upstream MCP transport error: ${error.message}
486
672
  `);
487
673
  shutdown(1);
488
674
  }
@@ -491,36 +677,36 @@ async function startProxy(command, args) {
491
677
  if (isShuttingDown) return;
492
678
  isShuttingDown = true;
493
679
  upstreamClient.disconnect().catch((error) => {
494
- process4.stderr.write(
680
+ process5.stderr.write(
495
681
  `dynmcp: error during disconnect: ${error instanceof Error ? error.message : String(error)}
496
682
  `
497
683
  );
498
- }).finally(() => process4.exit(exitCode));
684
+ }).finally(() => process5.exit(exitCode));
499
685
  };
500
686
  try {
501
687
  await upstreamClient.connect();
502
688
  } catch (error) {
503
- process4.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
689
+ process5.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
504
690
  `);
505
- process4.exit(1);
691
+ process5.exit(1);
506
692
  }
507
693
  let tools;
508
694
  try {
509
695
  tools = await upstreamClient.listTools();
510
696
  } catch (error) {
511
- process4.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
697
+ process5.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
512
698
  `);
513
- process4.exit(1);
699
+ process5.exit(1);
514
700
  }
515
701
  const catalog = ToolCatalog.fromFlat(tools);
516
702
  const proxyServer = new ProxyServer({
517
703
  catalog,
518
704
  callTool: (name, input) => upstreamClient.callTool(name, input)
519
705
  });
520
- process4.on("SIGINT", () => shutdown(0));
521
- process4.on("SIGTERM", () => shutdown(0));
522
- process4.stdin.on("end", () => shutdown(0));
523
- 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));
524
710
  try {
525
711
  await proxyServer.start();
526
712
  } catch (error) {
@@ -528,9 +714,9 @@ async function startProxy(command, args) {
528
714
  throw error;
529
715
  }
530
716
  }
531
- async function startProxyFromConfig(configPath) {
717
+ async function startProxyFromConfig(options = {}) {
532
718
  let isShuttingDown = false;
533
- const config = loadConfig(configPath);
719
+ const config = loadConfig(options);
534
720
  const mcps = /* @__PURE__ */ new Map();
535
721
  for (const [name, entry] of Object.entries(config.mcp)) {
536
722
  mcps.set(name, { transport: createTransport(entry) });
@@ -538,7 +724,7 @@ async function startProxyFromConfig(configPath) {
538
724
  const orchestrator = new Orchestrator({
539
725
  mcps,
540
726
  onTransportError: (mcpName2, error) => {
541
- process4.stderr.write(`Upstream MCP "${mcpName2}" transport error: ${error.message}
727
+ process5.stderr.write(`Upstream MCP "${mcpName2}" transport error: ${error.message}
542
728
  `);
543
729
  shutdown(1);
544
730
  }
@@ -547,27 +733,27 @@ async function startProxyFromConfig(configPath) {
547
733
  if (isShuttingDown) return;
548
734
  isShuttingDown = true;
549
735
  orchestrator.disconnectAll().catch((error) => {
550
- process4.stderr.write(
736
+ process5.stderr.write(
551
737
  `dynmcp: error during disconnect: ${error instanceof Error ? error.message : String(error)}
552
738
  `
553
739
  );
554
- }).finally(() => process4.exit(exitCode));
740
+ }).finally(() => process5.exit(exitCode));
555
741
  };
556
742
  try {
557
743
  await orchestrator.connect();
558
744
  } catch (error) {
559
- process4.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
745
+ process5.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
560
746
  `);
561
- process4.exit(1);
747
+ process5.exit(1);
562
748
  }
563
749
  const proxyServer = new ProxyServer({
564
750
  catalog: orchestrator.catalog,
565
751
  callTool: (name, input) => orchestrator.callTool(name, input)
566
752
  });
567
- process4.on("SIGINT", () => shutdown(0));
568
- process4.on("SIGTERM", () => shutdown(0));
569
- process4.stdin.on("end", () => shutdown(0));
570
- 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));
571
757
  try {
572
758
  await proxyServer.start();
573
759
  } catch (error) {
@@ -587,39 +773,43 @@ var cliBanner = chalk.bold.magentaBright(
587
773
  var cli = new Command(package_default.name).description(package_default.description).version(package_default.version).addHelpText("beforeAll", cliBanner).addHelpText(
588
774
  "after",
589
775
  "\nExamples:\n dynmcp -- npx -y chrome-devtools-mcp@latest\n dynmcp --config ./mcp.json\n"
590
- ).option("-c, --config <path>", "Path to config file (JSON or YAML)").allowExcessArguments(true).passThroughOptions(true).action(async (_options, cmd) => {
591
- 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("--");
592
781
  const configPath = cmd.opts().config;
782
+ const envFilePath = cmd.opts().env;
593
783
  if (separatorIndex !== -1) {
594
- const [command, ...args] = process5.argv.slice(separatorIndex + 1);
784
+ const [command, ...args] = process6.argv.slice(separatorIndex + 1);
595
785
  if (command === void 0) {
596
- process5.stderr.write(
786
+ process6.stderr.write(
597
787
  "dynmcp: no upstream command provided after --.\nUsage: dynmcp -- <command> [args...]\n"
598
788
  );
599
- process5.exit(1);
789
+ process6.exit(1);
600
790
  }
601
791
  try {
602
792
  await startProxy(command, args);
603
793
  } catch (error) {
604
- process5.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
794
+ process6.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
605
795
  `);
606
- process5.exit(1);
796
+ process6.exit(1);
607
797
  }
608
798
  return;
609
799
  }
610
800
  try {
611
- await startProxyFromConfig(configPath);
801
+ await startProxyFromConfig({ configPath, envFilePath });
612
802
  } catch (error) {
613
- process5.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
803
+ process6.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
614
804
  `);
615
- process5.exit(1);
805
+ process6.exit(1);
616
806
  }
617
807
  });
618
808
 
619
809
  // src/index.ts
620
- import process6 from "process";
810
+ import process7 from "process";
621
811
  async function main() {
622
- cli.parse(process6.argv);
812
+ cli.parse(process7.argv);
623
813
  }
624
814
  main();
625
815
  //# sourceMappingURL=index.js.map