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/README.md +59 -0
- package/dist/index.cjs +238 -54
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +241 -57
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/schema/mcp-config.json +10 -0
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
|
|
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.
|
|
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",
|
|
@@ -96,6 +96,7 @@ var package_default = {
|
|
|
96
96
|
boxen: "^8.0.1",
|
|
97
97
|
chalk: "^5.6.2",
|
|
98
98
|
commander: "^14.0.3",
|
|
99
|
+
dotenv: "^17.4.2",
|
|
99
100
|
enquirer: "^2.4.1",
|
|
100
101
|
fastmcp: "^4.0.1",
|
|
101
102
|
figlet: "^1.11.0",
|
|
@@ -121,13 +122,16 @@ var import_figlet = __toESM(require("figlet"), 1);
|
|
|
121
122
|
var import_chalk = __toESM(require("chalk"), 1);
|
|
122
123
|
|
|
123
124
|
// src/proxy/index.ts
|
|
124
|
-
var
|
|
125
|
+
var import_node_process5 = __toESM(require("process"), 1);
|
|
125
126
|
var import_stdio2 = require("@modelcontextprotocol/sdk/client/stdio.js");
|
|
126
127
|
|
|
127
128
|
// src/config/schema.ts
|
|
128
129
|
var import_zod = require("zod");
|
|
129
130
|
var MCP_NAME_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
|
|
130
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
|
+
);
|
|
131
135
|
var stdioTransport = import_zod.z.object({
|
|
132
136
|
transport: import_zod.z.literal("stdio"),
|
|
133
137
|
command: import_zod.z.string(),
|
|
@@ -153,51 +157,227 @@ var transportConfig = import_zod.z.discriminatedUnion("transport", [
|
|
|
153
157
|
sseTransport
|
|
154
158
|
]);
|
|
155
159
|
var mcpConfigSchema = import_zod.z.object({
|
|
160
|
+
env: envModeSchema.optional(),
|
|
156
161
|
mcp: import_zod.z.record(mcpName, transportConfig).refine((obj) => Object.keys(obj).length > 0, { message: "At least one MCP must be configured" })
|
|
157
162
|
});
|
|
158
163
|
|
|
159
164
|
// src/config/loader.ts
|
|
160
|
-
var import_node_fs = require("fs");
|
|
161
165
|
var import_node_fs2 = require("fs");
|
|
162
|
-
var
|
|
166
|
+
var import_node_path2 = require("path");
|
|
167
|
+
var import_node_process2 = __toESM(require("process"), 1);
|
|
163
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
|
|
164
327
|
var AUTO_DISCOVER_NAMES = ["mcp.json", ".mcp.json"];
|
|
328
|
+
var DEFAULT_ENV_MODE = "enable";
|
|
329
|
+
var VALID_ENV_MODES = ["enable", "dotenv", "process", "disable"];
|
|
165
330
|
function resolveConfigPath(explicitPath) {
|
|
166
331
|
if (explicitPath) {
|
|
167
|
-
const resolved = (0,
|
|
332
|
+
const resolved = (0, import_node_path2.resolve)(explicitPath);
|
|
168
333
|
if (!(0, import_node_fs2.existsSync)(resolved)) {
|
|
169
334
|
throw new Error(`Config file not found: ${resolved}`);
|
|
170
335
|
}
|
|
171
336
|
return resolved;
|
|
172
337
|
}
|
|
173
|
-
const cwd =
|
|
338
|
+
const cwd = import_node_process2.default.cwd();
|
|
174
339
|
for (const name of AUTO_DISCOVER_NAMES) {
|
|
175
|
-
const candidate = (0,
|
|
340
|
+
const candidate = (0, import_node_path2.resolve)(cwd, name);
|
|
176
341
|
if ((0, import_node_fs2.existsSync)(candidate)) {
|
|
177
342
|
return candidate;
|
|
178
343
|
}
|
|
179
344
|
}
|
|
180
|
-
const searched = AUTO_DISCOVER_NAMES.map((n) => (0,
|
|
345
|
+
const searched = AUTO_DISCOVER_NAMES.map((n) => (0, import_node_path2.resolve)(cwd, n)).join(", ");
|
|
181
346
|
throw new Error(`No config file found. Searched: ${searched}`);
|
|
182
347
|
}
|
|
183
|
-
function loadConfig(
|
|
184
|
-
const configPath =
|
|
185
|
-
const
|
|
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");
|
|
186
352
|
let content;
|
|
187
353
|
try {
|
|
188
|
-
content = isYamlFile(
|
|
354
|
+
content = isYamlFile(resolvedPath) ? (0, import_yaml.parse)(raw) : JSON.parse(raw);
|
|
189
355
|
} catch (parseError) {
|
|
190
356
|
const message = parseError instanceof Error ? parseError.message : String(parseError);
|
|
191
|
-
throw new Error(`Failed to parse config file (${
|
|
357
|
+
throw new Error(`Failed to parse config file (${resolvedPath}): ${message}`);
|
|
192
358
|
}
|
|
193
|
-
const
|
|
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);
|
|
194
363
|
if (!result.success) {
|
|
195
364
|
const formatted = result.error.issues.map((issue) => ` - ${issue.path.join(".")}: ${issue.message}`).join("\n");
|
|
196
|
-
throw new Error(`Invalid config file (${
|
|
365
|
+
throw new Error(`Invalid config file (${resolvedPath}):
|
|
197
366
|
${formatted}`);
|
|
198
367
|
}
|
|
199
368
|
return result.data;
|
|
200
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
|
+
}
|
|
201
381
|
function isYamlFile(filePath) {
|
|
202
382
|
return filePath.endsWith(".yml") || filePath.endsWith(".yaml");
|
|
203
383
|
}
|
|
@@ -338,7 +518,7 @@ function buildAnnotationLines(tool) {
|
|
|
338
518
|
}
|
|
339
519
|
|
|
340
520
|
// src/proxy/upstream-client.ts
|
|
341
|
-
var
|
|
521
|
+
var import_node_process3 = __toESM(require("process"), 1);
|
|
342
522
|
var import_client = require("@modelcontextprotocol/sdk/client/index.js");
|
|
343
523
|
var UpstreamClient = class {
|
|
344
524
|
transport;
|
|
@@ -347,7 +527,7 @@ var UpstreamClient = class {
|
|
|
347
527
|
constructor({ name, transport, onTransportError }) {
|
|
348
528
|
this.transport = transport;
|
|
349
529
|
this.onTransportError = onTransportError ?? ((error) => {
|
|
350
|
-
|
|
530
|
+
import_node_process3.default.stderr.write(`[${name}] Upstream MCP transport error: ${error.message}
|
|
351
531
|
`);
|
|
352
532
|
});
|
|
353
533
|
}
|
|
@@ -459,7 +639,7 @@ var Orchestrator = class {
|
|
|
459
639
|
};
|
|
460
640
|
|
|
461
641
|
// src/proxy/server.ts
|
|
462
|
-
var
|
|
642
|
+
var import_node_process4 = __toESM(require("process"), 1);
|
|
463
643
|
var import_fastmcp = require("fastmcp");
|
|
464
644
|
var import_zod3 = require("zod");
|
|
465
645
|
var ProxyServer = class {
|
|
@@ -497,7 +677,7 @@ var ProxyServer = class {
|
|
|
497
677
|
return result;
|
|
498
678
|
}
|
|
499
679
|
});
|
|
500
|
-
|
|
680
|
+
import_node_process4.default.stderr.write("Starting dynamic-discovery-mcp server over stdio\n");
|
|
501
681
|
await server.start({ transportType: "stdio" });
|
|
502
682
|
}
|
|
503
683
|
};
|
|
@@ -510,7 +690,7 @@ async function startProxy(command, args) {
|
|
|
510
690
|
name: command,
|
|
511
691
|
transport,
|
|
512
692
|
onTransportError: (error) => {
|
|
513
|
-
|
|
693
|
+
import_node_process5.default.stderr.write(`Upstream MCP transport error: ${error.message}
|
|
514
694
|
`);
|
|
515
695
|
shutdown(1);
|
|
516
696
|
}
|
|
@@ -519,36 +699,36 @@ async function startProxy(command, args) {
|
|
|
519
699
|
if (isShuttingDown) return;
|
|
520
700
|
isShuttingDown = true;
|
|
521
701
|
upstreamClient.disconnect().catch((error) => {
|
|
522
|
-
|
|
702
|
+
import_node_process5.default.stderr.write(
|
|
523
703
|
`dynmcp: error during disconnect: ${error instanceof Error ? error.message : String(error)}
|
|
524
704
|
`
|
|
525
705
|
);
|
|
526
|
-
}).finally(() =>
|
|
706
|
+
}).finally(() => import_node_process5.default.exit(exitCode));
|
|
527
707
|
};
|
|
528
708
|
try {
|
|
529
709
|
await upstreamClient.connect();
|
|
530
710
|
} catch (error) {
|
|
531
|
-
|
|
711
|
+
import_node_process5.default.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
|
|
532
712
|
`);
|
|
533
|
-
|
|
713
|
+
import_node_process5.default.exit(1);
|
|
534
714
|
}
|
|
535
715
|
let tools;
|
|
536
716
|
try {
|
|
537
717
|
tools = await upstreamClient.listTools();
|
|
538
718
|
} catch (error) {
|
|
539
|
-
|
|
719
|
+
import_node_process5.default.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
|
|
540
720
|
`);
|
|
541
|
-
|
|
721
|
+
import_node_process5.default.exit(1);
|
|
542
722
|
}
|
|
543
723
|
const catalog = ToolCatalog.fromFlat(tools);
|
|
544
724
|
const proxyServer = new ProxyServer({
|
|
545
725
|
catalog,
|
|
546
726
|
callTool: (name, input) => upstreamClient.callTool(name, input)
|
|
547
727
|
});
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
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));
|
|
552
732
|
try {
|
|
553
733
|
await proxyServer.start();
|
|
554
734
|
} catch (error) {
|
|
@@ -556,9 +736,9 @@ async function startProxy(command, args) {
|
|
|
556
736
|
throw error;
|
|
557
737
|
}
|
|
558
738
|
}
|
|
559
|
-
async function startProxyFromConfig(
|
|
739
|
+
async function startProxyFromConfig(options = {}) {
|
|
560
740
|
let isShuttingDown = false;
|
|
561
|
-
const config = loadConfig(
|
|
741
|
+
const config = loadConfig(options);
|
|
562
742
|
const mcps = /* @__PURE__ */ new Map();
|
|
563
743
|
for (const [name, entry] of Object.entries(config.mcp)) {
|
|
564
744
|
mcps.set(name, { transport: createTransport(entry) });
|
|
@@ -566,7 +746,7 @@ async function startProxyFromConfig(configPath) {
|
|
|
566
746
|
const orchestrator = new Orchestrator({
|
|
567
747
|
mcps,
|
|
568
748
|
onTransportError: (mcpName2, error) => {
|
|
569
|
-
|
|
749
|
+
import_node_process5.default.stderr.write(`Upstream MCP "${mcpName2}" transport error: ${error.message}
|
|
570
750
|
`);
|
|
571
751
|
shutdown(1);
|
|
572
752
|
}
|
|
@@ -575,27 +755,27 @@ async function startProxyFromConfig(configPath) {
|
|
|
575
755
|
if (isShuttingDown) return;
|
|
576
756
|
isShuttingDown = true;
|
|
577
757
|
orchestrator.disconnectAll().catch((error) => {
|
|
578
|
-
|
|
758
|
+
import_node_process5.default.stderr.write(
|
|
579
759
|
`dynmcp: error during disconnect: ${error instanceof Error ? error.message : String(error)}
|
|
580
760
|
`
|
|
581
761
|
);
|
|
582
|
-
}).finally(() =>
|
|
762
|
+
}).finally(() => import_node_process5.default.exit(exitCode));
|
|
583
763
|
};
|
|
584
764
|
try {
|
|
585
765
|
await orchestrator.connect();
|
|
586
766
|
} catch (error) {
|
|
587
|
-
|
|
767
|
+
import_node_process5.default.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
|
|
588
768
|
`);
|
|
589
|
-
|
|
769
|
+
import_node_process5.default.exit(1);
|
|
590
770
|
}
|
|
591
771
|
const proxyServer = new ProxyServer({
|
|
592
772
|
catalog: orchestrator.catalog,
|
|
593
773
|
callTool: (name, input) => orchestrator.callTool(name, input)
|
|
594
774
|
});
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
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));
|
|
599
779
|
try {
|
|
600
780
|
await proxyServer.start();
|
|
601
781
|
} catch (error) {
|
|
@@ -615,39 +795,43 @@ var cliBanner = import_chalk.default.bold.magentaBright(
|
|
|
615
795
|
var cli = new import_commander.Command(package_default.name).description(package_default.description).version(package_default.version).addHelpText("beforeAll", cliBanner).addHelpText(
|
|
616
796
|
"after",
|
|
617
797
|
"\nExamples:\n dynmcp -- npx -y chrome-devtools-mcp@latest\n dynmcp --config ./mcp.json\n"
|
|
618
|
-
).option("-c, --config <path>", "Path to config file (JSON or YAML)").
|
|
619
|
-
|
|
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("--");
|
|
620
803
|
const configPath = cmd.opts().config;
|
|
804
|
+
const envFilePath = cmd.opts().env;
|
|
621
805
|
if (separatorIndex !== -1) {
|
|
622
|
-
const [command, ...args] =
|
|
806
|
+
const [command, ...args] = import_node_process6.default.argv.slice(separatorIndex + 1);
|
|
623
807
|
if (command === void 0) {
|
|
624
|
-
|
|
808
|
+
import_node_process6.default.stderr.write(
|
|
625
809
|
"dynmcp: no upstream command provided after --.\nUsage: dynmcp -- <command> [args...]\n"
|
|
626
810
|
);
|
|
627
|
-
|
|
811
|
+
import_node_process6.default.exit(1);
|
|
628
812
|
}
|
|
629
813
|
try {
|
|
630
814
|
await startProxy(command, args);
|
|
631
815
|
} catch (error) {
|
|
632
|
-
|
|
816
|
+
import_node_process6.default.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
|
|
633
817
|
`);
|
|
634
|
-
|
|
818
|
+
import_node_process6.default.exit(1);
|
|
635
819
|
}
|
|
636
820
|
return;
|
|
637
821
|
}
|
|
638
822
|
try {
|
|
639
|
-
await startProxyFromConfig(configPath);
|
|
823
|
+
await startProxyFromConfig({ configPath, envFilePath });
|
|
640
824
|
} catch (error) {
|
|
641
|
-
|
|
825
|
+
import_node_process6.default.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
|
|
642
826
|
`);
|
|
643
|
-
|
|
827
|
+
import_node_process6.default.exit(1);
|
|
644
828
|
}
|
|
645
829
|
});
|
|
646
830
|
|
|
647
831
|
// src/index.ts
|
|
648
|
-
var
|
|
832
|
+
var import_node_process7 = __toESM(require("process"), 1);
|
|
649
833
|
async function main() {
|
|
650
|
-
cli.parse(
|
|
834
|
+
cli.parse(import_node_process7.default.argv);
|
|
651
835
|
}
|
|
652
836
|
main();
|
|
653
837
|
//# sourceMappingURL=index.cjs.map
|