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 +59 -0
- package/dist/index.cjs +250 -60
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +253 -63
- package/dist/index.js.map +1 -1
- package/package.json +6 -2
- package/schema/mcp-config.json +126 -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",
|
|
@@ -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
|
|
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
|
|
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,
|
|
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 =
|
|
338
|
+
const cwd = import_node_process2.default.cwd();
|
|
171
339
|
for (const name of AUTO_DISCOVER_NAMES) {
|
|
172
|
-
const candidate = (0,
|
|
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,
|
|
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(
|
|
181
|
-
const configPath =
|
|
182
|
-
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");
|
|
183
352
|
let content;
|
|
184
353
|
try {
|
|
185
|
-
content = isYamlFile(
|
|
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 (${
|
|
357
|
+
throw new Error(`Failed to parse config file (${resolvedPath}): ${message}`);
|
|
189
358
|
}
|
|
190
|
-
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);
|
|
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 (${
|
|
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
|
|
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
|
-
|
|
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
|
|
642
|
+
var import_node_process4 = __toESM(require("process"), 1);
|
|
457
643
|
var import_fastmcp = require("fastmcp");
|
|
458
|
-
var
|
|
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:
|
|
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:
|
|
483
|
-
tool_name:
|
|
484
|
-
tool_input:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(() =>
|
|
706
|
+
}).finally(() => import_node_process5.default.exit(exitCode));
|
|
521
707
|
};
|
|
522
708
|
try {
|
|
523
709
|
await upstreamClient.connect();
|
|
524
710
|
} catch (error) {
|
|
525
|
-
|
|
711
|
+
import_node_process5.default.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
|
|
526
712
|
`);
|
|
527
|
-
|
|
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
|
-
|
|
719
|
+
import_node_process5.default.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
|
|
534
720
|
`);
|
|
535
|
-
|
|
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
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
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(
|
|
739
|
+
async function startProxyFromConfig(options = {}) {
|
|
554
740
|
let isShuttingDown = false;
|
|
555
|
-
const config = loadConfig(
|
|
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
|
-
|
|
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
|
-
|
|
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(() =>
|
|
762
|
+
}).finally(() => import_node_process5.default.exit(exitCode));
|
|
577
763
|
};
|
|
578
764
|
try {
|
|
579
765
|
await orchestrator.connect();
|
|
580
766
|
} catch (error) {
|
|
581
|
-
|
|
767
|
+
import_node_process5.default.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
|
|
582
768
|
`);
|
|
583
|
-
|
|
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
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
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)").
|
|
613
|
-
|
|
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] =
|
|
806
|
+
const [command, ...args] = import_node_process6.default.argv.slice(separatorIndex + 1);
|
|
617
807
|
if (command === void 0) {
|
|
618
|
-
|
|
808
|
+
import_node_process6.default.stderr.write(
|
|
619
809
|
"dynmcp: no upstream command provided after --.\nUsage: dynmcp -- <command> [args...]\n"
|
|
620
810
|
);
|
|
621
|
-
|
|
811
|
+
import_node_process6.default.exit(1);
|
|
622
812
|
}
|
|
623
813
|
try {
|
|
624
814
|
await startProxy(command, args);
|
|
625
815
|
} catch (error) {
|
|
626
|
-
|
|
816
|
+
import_node_process6.default.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
|
|
627
817
|
`);
|
|
628
|
-
|
|
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
|
-
|
|
825
|
+
import_node_process6.default.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
|
|
636
826
|
`);
|
|
637
|
-
|
|
827
|
+
import_node_process6.default.exit(1);
|
|
638
828
|
}
|
|
639
829
|
});
|
|
640
830
|
|
|
641
831
|
// src/index.ts
|
|
642
|
-
var
|
|
832
|
+
var import_node_process7 = __toESM(require("process"), 1);
|
|
643
833
|
async function main() {
|
|
644
|
-
cli.parse(
|
|
834
|
+
cli.parse(import_node_process7.default.argv);
|
|
645
835
|
}
|
|
646
836
|
main();
|
|
647
837
|
//# sourceMappingURL=index.cjs.map
|