dynmcp 0.0.1 → 0.1.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 +104 -7
- package/dist/index.cjs +348 -97
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +365 -114
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Dynamic Discovery MCP
|
|
2
2
|
|
|
3
3
|
A proxy MCP that exposes meta-tools so agents can discover and call upstream MCP tools on demand, without loading every tool schema into the context window.
|
|
4
4
|
|
|
@@ -8,27 +8,124 @@ Large MCPs routinely expose tens to hundreds of tools. When several are active a
|
|
|
8
8
|
|
|
9
9
|
## How It Works
|
|
10
10
|
|
|
11
|
-
`dynmcp` sits in front of
|
|
11
|
+
`dynmcp` sits in front of one or more upstream MCPs and exposes exactly two tools:
|
|
12
12
|
|
|
13
13
|
- **`discover_tool`** — its description contains a compact catalog of every upstream tool (name and one-line summary). Call it with a tool name to get that tool's full schema: description, parameters, types, and required fields.
|
|
14
14
|
- **`use_tool`** — executes a tool by name, proxying the call to the upstream MCP and returning its output unchanged.
|
|
15
15
|
|
|
16
16
|
The agent workflow: scan the catalog in `discover_tool`'s description to find relevant tools, call `discover_tool` to load the full schema of the one it needs, then call `use_tool` to execute it. Full schemas of tools the agent never needs never enter the context window.
|
|
17
17
|
|
|
18
|
-
`dynmcp` runs locally, communicating with both the agent host and the upstream MCP over stdio.
|
|
19
|
-
|
|
20
18
|
## Usage
|
|
21
19
|
|
|
22
20
|
Requires Node.js >= 20.
|
|
23
21
|
|
|
22
|
+
### Single MCP (quick start)
|
|
23
|
+
|
|
24
24
|
Prefix any MCP invocation with `dynmcp --`:
|
|
25
25
|
|
|
26
26
|
```bash
|
|
27
|
-
# Before
|
|
27
|
+
# Before — tool schemas go straight into context
|
|
28
28
|
npx -y chrome-devtools-mcp@latest
|
|
29
29
|
|
|
30
|
-
# With dynmcp
|
|
30
|
+
# With dynmcp — only discover_tool and use_tool are exposed
|
|
31
31
|
npx dynmcp@latest -- npx -y chrome-devtools-mcp@latest
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
-
Everything after `--` is the command used to launch the upstream MCP.
|
|
34
|
+
Everything after `--` is the command used to launch the upstream MCP. Tool names are exposed as-is (no namespace prefix).
|
|
35
|
+
|
|
36
|
+
### Multiple MCPs (config file)
|
|
37
|
+
|
|
38
|
+
To proxy several MCPs at once, create a config file:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# Auto-discover mcp.json or .mcp.json in cwd
|
|
42
|
+
npx dynmcp@latest
|
|
43
|
+
|
|
44
|
+
# Or specify explicitly
|
|
45
|
+
npx dynmcp@latest --config ./my-config.json
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
When using a config file, tool names are namespaced as `<mcp-name>/<tool-name>` to avoid collisions.
|
|
49
|
+
|
|
50
|
+
## Config File
|
|
51
|
+
|
|
52
|
+
The config file declares upstream MCPs under a top-level `mcp` key. Three transport types are supported:
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
{
|
|
56
|
+
"$schema": "https://unpkg.com/dynmcp/schema/mcp-config.json",
|
|
57
|
+
"mcp": {
|
|
58
|
+
"chrome-devtools": {
|
|
59
|
+
"transport": "stdio",
|
|
60
|
+
"command": "npx",
|
|
61
|
+
"args": ["-y", "chrome-devtools-mcp@latest"]
|
|
62
|
+
},
|
|
63
|
+
"filesystem": {
|
|
64
|
+
"transport": "stdio",
|
|
65
|
+
"command": "npx",
|
|
66
|
+
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
|
|
67
|
+
},
|
|
68
|
+
"aws-knowledge": {
|
|
69
|
+
"transport": "streamable-http",
|
|
70
|
+
"url": "https://knowledge-mcp.global.api.aws"
|
|
71
|
+
},
|
|
72
|
+
"remote-sse": {
|
|
73
|
+
"transport": "sse",
|
|
74
|
+
"url": "https://example.com/sse",
|
|
75
|
+
"headers": {
|
|
76
|
+
"Authorization": "Bearer my-token"
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
YAML is also supported (use `.yml` or `.yaml` extension).
|
|
84
|
+
|
|
85
|
+
### Transport Types
|
|
86
|
+
|
|
87
|
+
| Transport | Fields | Description |
|
|
88
|
+
|---|---|---|
|
|
89
|
+
| `stdio` | `command`, `args?`, `env?` | Spawns the MCP as a child process |
|
|
90
|
+
| `streamable-http` | `url`, `headers?` | Connects to a remote MCP over HTTP |
|
|
91
|
+
| `sse` | `url`, `headers?` | Connects to a remote MCP over Server-Sent Events |
|
|
92
|
+
|
|
93
|
+
### Config Discovery
|
|
94
|
+
|
|
95
|
+
When no `--` command is provided, `dynmcp` looks for a config file in this order:
|
|
96
|
+
|
|
97
|
+
1. Path from `-c` / `--config` flag
|
|
98
|
+
2. `mcp.json` in the current directory
|
|
99
|
+
3. `.mcp.json` in the current directory
|
|
100
|
+
|
|
101
|
+
### Naming Rules
|
|
102
|
+
|
|
103
|
+
MCP names (the keys in the config) must match `^[a-z0-9][a-z0-9-]*$`.
|
|
104
|
+
|
|
105
|
+
## CLI Reference
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
dynmcp [options] [-- <upstream-command> [upstream-args...]]
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
| Flag | Short | Description |
|
|
112
|
+
|---|---|---|
|
|
113
|
+
| `--version` | `-v` | Print the package version and exit |
|
|
114
|
+
| `--help` | `-h` | Print usage information and exit |
|
|
115
|
+
| `--config <path>` | `-c` | Path to config file (JSON or YAML) |
|
|
116
|
+
| `--` | | Everything after is the upstream MCP command (single-MCP mode) |
|
|
117
|
+
|
|
118
|
+
### Mode Resolution
|
|
119
|
+
|
|
120
|
+
1. If `--` is present, single-MCP mode is used (config file is ignored).
|
|
121
|
+
2. Otherwise, config file mode is used.
|
|
122
|
+
|
|
123
|
+
## Development
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
npm install
|
|
127
|
+
npm run build # Compile to dist/
|
|
128
|
+
npm run typecheck # Type-check without emitting
|
|
129
|
+
npm run check # Biome lint + format
|
|
130
|
+
npm test # Run tests
|
|
131
|
+
```
|
package/dist/index.cjs
CHANGED
|
@@ -29,7 +29,7 @@ var import_commander = require("commander");
|
|
|
29
29
|
// package.json
|
|
30
30
|
var package_default = {
|
|
31
31
|
name: "dynmcp",
|
|
32
|
-
version: "0.0
|
|
32
|
+
version: "0.1.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",
|
|
@@ -97,6 +97,7 @@ var package_default = {
|
|
|
97
97
|
fastmcp: "^4.0.1",
|
|
98
98
|
figlet: "^1.11.0",
|
|
99
99
|
figures: "^6.1.0",
|
|
100
|
+
yaml: "^2.9.0",
|
|
100
101
|
zod: "^4.4.3"
|
|
101
102
|
},
|
|
102
103
|
devDependencies: {
|
|
@@ -118,21 +119,143 @@ var import_chalk = __toESM(require("chalk"), 1);
|
|
|
118
119
|
|
|
119
120
|
// src/proxy/index.ts
|
|
120
121
|
var import_node_process3 = __toESM(require("process"), 1);
|
|
122
|
+
var import_stdio2 = require("@modelcontextprotocol/sdk/client/stdio.js");
|
|
123
|
+
|
|
124
|
+
// src/config/schema.ts
|
|
125
|
+
var import_zod = require("zod");
|
|
126
|
+
var MCP_NAME_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
|
|
127
|
+
var mcpName = import_zod.z.string().regex(MCP_NAME_PATTERN);
|
|
128
|
+
var stdioTransport = import_zod.z.object({
|
|
129
|
+
transport: import_zod.z.literal("stdio"),
|
|
130
|
+
command: import_zod.z.string(),
|
|
131
|
+
args: import_zod.z.array(import_zod.z.string()).optional(),
|
|
132
|
+
env: import_zod.z.record(import_zod.z.string(), import_zod.z.string()).optional()
|
|
133
|
+
}).strict();
|
|
134
|
+
var httpUrl = import_zod.z.string().url().refine((u) => u.startsWith("http://") || u.startsWith("https://"), {
|
|
135
|
+
message: "URL must use http:// or https:// scheme"
|
|
136
|
+
});
|
|
137
|
+
var streamableHttpTransport = import_zod.z.object({
|
|
138
|
+
transport: import_zod.z.literal("streamable-http"),
|
|
139
|
+
url: httpUrl,
|
|
140
|
+
headers: import_zod.z.record(import_zod.z.string(), import_zod.z.string()).optional()
|
|
141
|
+
}).strict();
|
|
142
|
+
var sseTransport = import_zod.z.object({
|
|
143
|
+
transport: import_zod.z.literal("sse"),
|
|
144
|
+
url: httpUrl,
|
|
145
|
+
headers: import_zod.z.record(import_zod.z.string(), import_zod.z.string()).optional()
|
|
146
|
+
}).strict();
|
|
147
|
+
var transportConfig = import_zod.z.discriminatedUnion("transport", [
|
|
148
|
+
stdioTransport,
|
|
149
|
+
streamableHttpTransport,
|
|
150
|
+
sseTransport
|
|
151
|
+
]);
|
|
152
|
+
var mcpConfigSchema = import_zod.z.object({
|
|
153
|
+
mcp: import_zod.z.record(mcpName, transportConfig).refine((obj) => Object.keys(obj).length > 0, { message: "At least one MCP must be configured" })
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// src/config/loader.ts
|
|
157
|
+
var import_node_fs = require("fs");
|
|
158
|
+
var import_node_fs2 = require("fs");
|
|
159
|
+
var import_node_path = require("path");
|
|
160
|
+
var import_yaml = require("yaml");
|
|
161
|
+
var AUTO_DISCOVER_NAMES = ["mcp.json", ".mcp.json"];
|
|
162
|
+
function resolveConfigPath(explicitPath) {
|
|
163
|
+
if (explicitPath) {
|
|
164
|
+
const resolved = (0, import_node_path.resolve)(explicitPath);
|
|
165
|
+
if (!(0, import_node_fs2.existsSync)(resolved)) {
|
|
166
|
+
throw new Error(`Config file not found: ${resolved}`);
|
|
167
|
+
}
|
|
168
|
+
return resolved;
|
|
169
|
+
}
|
|
170
|
+
const cwd = process.cwd();
|
|
171
|
+
for (const name of AUTO_DISCOVER_NAMES) {
|
|
172
|
+
const candidate = (0, import_node_path.resolve)(cwd, name);
|
|
173
|
+
if ((0, import_node_fs2.existsSync)(candidate)) {
|
|
174
|
+
return candidate;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const searched = AUTO_DISCOVER_NAMES.map((n) => (0, import_node_path.resolve)(cwd, n)).join(", ");
|
|
178
|
+
throw new Error(`No config file found. Searched: ${searched}`);
|
|
179
|
+
}
|
|
180
|
+
function loadConfig(explicitPath) {
|
|
181
|
+
const configPath = resolveConfigPath(explicitPath);
|
|
182
|
+
const raw = (0, import_node_fs.readFileSync)(configPath, "utf-8");
|
|
183
|
+
let content;
|
|
184
|
+
try {
|
|
185
|
+
content = isYamlFile(configPath) ? (0, import_yaml.parse)(raw) : JSON.parse(raw);
|
|
186
|
+
} catch (parseError) {
|
|
187
|
+
const message = parseError instanceof Error ? parseError.message : String(parseError);
|
|
188
|
+
throw new Error(`Failed to parse config file (${configPath}): ${message}`);
|
|
189
|
+
}
|
|
190
|
+
const result = mcpConfigSchema.safeParse(content);
|
|
191
|
+
if (!result.success) {
|
|
192
|
+
const formatted = result.error.issues.map((issue) => ` - ${issue.path.join(".")}: ${issue.message}`).join("\n");
|
|
193
|
+
throw new Error(`Invalid config file (${configPath}):
|
|
194
|
+
${formatted}`);
|
|
195
|
+
}
|
|
196
|
+
return result.data;
|
|
197
|
+
}
|
|
198
|
+
function isYamlFile(filePath) {
|
|
199
|
+
return filePath.endsWith(".yml") || filePath.endsWith(".yaml");
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// src/proxy/transport-factory.ts
|
|
203
|
+
var import_stdio = require("@modelcontextprotocol/sdk/client/stdio.js");
|
|
204
|
+
var import_streamableHttp = require("@modelcontextprotocol/sdk/client/streamableHttp.js");
|
|
205
|
+
var import_sse = require("@modelcontextprotocol/sdk/client/sse.js");
|
|
206
|
+
function createTransport(config) {
|
|
207
|
+
switch (config.transport) {
|
|
208
|
+
case "stdio":
|
|
209
|
+
return new import_stdio.StdioClientTransport({
|
|
210
|
+
command: config.command,
|
|
211
|
+
args: config.args,
|
|
212
|
+
env: config.env
|
|
213
|
+
});
|
|
214
|
+
case "streamable-http":
|
|
215
|
+
return new import_streamableHttp.StreamableHTTPClientTransport(
|
|
216
|
+
new URL(config.url),
|
|
217
|
+
config.headers ? { requestInit: { headers: config.headers } } : void 0
|
|
218
|
+
);
|
|
219
|
+
case "sse":
|
|
220
|
+
return new import_sse.SSEClientTransport(
|
|
221
|
+
new URL(config.url),
|
|
222
|
+
config.headers ? { requestInit: { headers: config.headers } } : void 0
|
|
223
|
+
);
|
|
224
|
+
default: {
|
|
225
|
+
const _exhaustive = config;
|
|
226
|
+
return _exhaustive;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
121
230
|
|
|
122
231
|
// src/proxy/tool-catalog.ts
|
|
123
232
|
var DISCOVER_TOOL_PREAMBLE = `Use this tool to look up the full schema of a tool before calling it with use_tool.
|
|
124
233
|
Call discover_tool with a tool name from the list below to get its complete description,
|
|
125
234
|
input parameters, and output schema. Always discover a tool before using it.`;
|
|
126
|
-
var ToolCatalog = class {
|
|
235
|
+
var ToolCatalog = class _ToolCatalog {
|
|
127
236
|
tools;
|
|
128
237
|
discoverToolDescription;
|
|
129
|
-
constructor(
|
|
238
|
+
constructor(tools, description) {
|
|
239
|
+
this.tools = tools;
|
|
240
|
+
this.discoverToolDescription = description;
|
|
241
|
+
}
|
|
242
|
+
static fromFlat(upstreamTools) {
|
|
130
243
|
const toolMap = /* @__PURE__ */ new Map();
|
|
131
244
|
for (const tool of upstreamTools) {
|
|
132
245
|
toolMap.set(tool.name, tool);
|
|
133
246
|
}
|
|
134
|
-
|
|
135
|
-
|
|
247
|
+
const description = buildFlatDescription(upstreamTools);
|
|
248
|
+
return new _ToolCatalog(toolMap, description);
|
|
249
|
+
}
|
|
250
|
+
static fromGrouped(groups) {
|
|
251
|
+
const toolMap = /* @__PURE__ */ new Map();
|
|
252
|
+
for (const [mcpName2, tools] of groups) {
|
|
253
|
+
for (const tool of tools) {
|
|
254
|
+
toolMap.set(`${mcpName2}/${tool.name}`, tool);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
const description = buildGroupedDescription(groups);
|
|
258
|
+
return new _ToolCatalog(toolMap, description);
|
|
136
259
|
}
|
|
137
260
|
getToolDetails(toolName) {
|
|
138
261
|
const tool = this.tools.get(toolName);
|
|
@@ -140,10 +263,10 @@ var ToolCatalog = class {
|
|
|
140
263
|
const sortedNames = [...this.tools.keys()].sort().join(", ");
|
|
141
264
|
return `Unknown tool: "${toolName}". Available tools: ${sortedNames}`;
|
|
142
265
|
}
|
|
143
|
-
return buildToolDetailsString(tool);
|
|
266
|
+
return buildToolDetailsString(toolName, tool);
|
|
144
267
|
}
|
|
145
268
|
};
|
|
146
|
-
function
|
|
269
|
+
function buildFlatDescription(tools) {
|
|
147
270
|
const sortedTools = [...tools].sort((a, b) => a.name.localeCompare(b.name));
|
|
148
271
|
const toolLines = sortedTools.map((tool) => `- ${tool.name}: ${tool.description}`).join("\n");
|
|
149
272
|
return `${DISCOVER_TOOL_PREAMBLE}
|
|
@@ -152,9 +275,24 @@ function buildDiscoverToolDescription(tools) {
|
|
|
152
275
|
${toolLines}
|
|
153
276
|
</tools>`;
|
|
154
277
|
}
|
|
155
|
-
function
|
|
278
|
+
function buildGroupedDescription(groups) {
|
|
279
|
+
const sortedMcpNames = [...groups.keys()].sort();
|
|
280
|
+
const sections = sortedMcpNames.map((mcpName2) => {
|
|
281
|
+
const tools = groups.get(mcpName2);
|
|
282
|
+
const sortedTools = [...tools].sort((a, b) => a.name.localeCompare(b.name));
|
|
283
|
+
const toolLines = sortedTools.map((tool) => `- ${mcpName2}/${tool.name}: ${tool.description}`).join("\n");
|
|
284
|
+
return `${mcpName2}:
|
|
285
|
+
${toolLines}`;
|
|
286
|
+
});
|
|
287
|
+
return `${DISCOVER_TOOL_PREAMBLE}
|
|
288
|
+
|
|
289
|
+
<tools>
|
|
290
|
+
${sections.join("\n\n")}
|
|
291
|
+
</tools>`;
|
|
292
|
+
}
|
|
293
|
+
function buildToolDetailsString(displayName, tool) {
|
|
156
294
|
const lines = [
|
|
157
|
-
`Tool: ${
|
|
295
|
+
`Tool: ${displayName}`,
|
|
158
296
|
`Description: ${tool.description}`,
|
|
159
297
|
"",
|
|
160
298
|
"Input Schema:",
|
|
@@ -193,73 +331,21 @@ function buildAnnotationLines(tool) {
|
|
|
193
331
|
return lines;
|
|
194
332
|
}
|
|
195
333
|
|
|
196
|
-
// src/proxy/server.ts
|
|
197
|
-
var import_node_process = __toESM(require("process"), 1);
|
|
198
|
-
var import_fastmcp = require("fastmcp");
|
|
199
|
-
var import_zod = require("zod");
|
|
200
|
-
var ProxyServer = class {
|
|
201
|
-
catalog;
|
|
202
|
-
upstreamClient;
|
|
203
|
-
constructor({ catalog, upstreamClient }) {
|
|
204
|
-
this.catalog = catalog;
|
|
205
|
-
this.upstreamClient = upstreamClient;
|
|
206
|
-
}
|
|
207
|
-
async start() {
|
|
208
|
-
const server = new import_fastmcp.FastMCP({
|
|
209
|
-
name: "dynamic-discovery-mcp",
|
|
210
|
-
version: package_default.version
|
|
211
|
-
});
|
|
212
|
-
server.addTool({
|
|
213
|
-
name: "discover_tool",
|
|
214
|
-
description: this.catalog.discoverToolDescription,
|
|
215
|
-
parameters: import_zod.z.object({ tool_name: import_zod.z.string() }),
|
|
216
|
-
execute: async ({ tool_name }) => {
|
|
217
|
-
return this.catalog.getToolDetails(tool_name);
|
|
218
|
-
}
|
|
219
|
-
});
|
|
220
|
-
server.addTool({
|
|
221
|
-
name: "use_tool",
|
|
222
|
-
description: "Use a tool that was previously discovered with the discover_tool tool.",
|
|
223
|
-
parameters: import_zod.z.object({
|
|
224
|
-
tool_name: import_zod.z.string(),
|
|
225
|
-
tool_input: import_zod.z.record(import_zod.z.string(), import_zod.z.unknown()).default({})
|
|
226
|
-
}),
|
|
227
|
-
execute: async ({ tool_name, tool_input }) => {
|
|
228
|
-
if (!this.catalog.tools.has(tool_name)) {
|
|
229
|
-
return this.catalog.getToolDetails(tool_name);
|
|
230
|
-
}
|
|
231
|
-
const result = await this.upstreamClient.callTool(tool_name, tool_input);
|
|
232
|
-
return result;
|
|
233
|
-
}
|
|
234
|
-
});
|
|
235
|
-
import_node_process.default.stderr.write("Starting dynamic-discovery-mcp server over stdio\n");
|
|
236
|
-
await server.start({ transportType: "stdio" });
|
|
237
|
-
}
|
|
238
|
-
};
|
|
239
|
-
|
|
240
334
|
// src/proxy/upstream-client.ts
|
|
241
|
-
var
|
|
335
|
+
var import_node_process = __toESM(require("process"), 1);
|
|
242
336
|
var import_client = require("@modelcontextprotocol/sdk/client/index.js");
|
|
243
|
-
var import_stdio = require("@modelcontextprotocol/sdk/client/stdio.js");
|
|
244
337
|
var UpstreamClient = class {
|
|
245
|
-
|
|
246
|
-
args;
|
|
338
|
+
transport;
|
|
247
339
|
onTransportError;
|
|
248
340
|
client = null;
|
|
249
|
-
transport
|
|
250
|
-
|
|
251
|
-
this.command = command;
|
|
252
|
-
this.args = args;
|
|
341
|
+
constructor({ name, transport, onTransportError }) {
|
|
342
|
+
this.transport = transport;
|
|
253
343
|
this.onTransportError = onTransportError ?? ((error) => {
|
|
254
|
-
|
|
344
|
+
import_node_process.default.stderr.write(`[${name}] Upstream MCP transport error: ${error.message}
|
|
255
345
|
`);
|
|
256
346
|
});
|
|
257
347
|
}
|
|
258
348
|
async connect() {
|
|
259
|
-
this.transport = new import_stdio.StdioClientTransport({
|
|
260
|
-
command: this.command,
|
|
261
|
-
args: this.args
|
|
262
|
-
});
|
|
263
349
|
this.transport.onerror = this.onTransportError;
|
|
264
350
|
this.client = new import_client.Client({ name: "dynamic-discovery-mcp", version: "1.0.0" });
|
|
265
351
|
await this.client.connect(this.transport);
|
|
@@ -294,10 +380,8 @@ var UpstreamClient = class {
|
|
|
294
380
|
if (this.client === null) {
|
|
295
381
|
throw new Error("Client is not connected. Call connect() first.");
|
|
296
382
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
arguments: input
|
|
300
|
-
});
|
|
383
|
+
const result = await this.client.callTool({ name, arguments: input });
|
|
384
|
+
return result;
|
|
301
385
|
}
|
|
302
386
|
async disconnect() {
|
|
303
387
|
if (this.client === null) {
|
|
@@ -305,13 +389,126 @@ var UpstreamClient = class {
|
|
|
305
389
|
}
|
|
306
390
|
await this.client.close();
|
|
307
391
|
this.client = null;
|
|
308
|
-
|
|
392
|
+
}
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
// src/proxy/orchestrator.ts
|
|
396
|
+
var Orchestrator = class {
|
|
397
|
+
config;
|
|
398
|
+
clients = /* @__PURE__ */ new Map();
|
|
399
|
+
toolCatalog = null;
|
|
400
|
+
constructor(config) {
|
|
401
|
+
this.config = config;
|
|
402
|
+
}
|
|
403
|
+
async connect() {
|
|
404
|
+
const groups = /* @__PURE__ */ new Map();
|
|
405
|
+
try {
|
|
406
|
+
for (const [mcpName2, { transport }] of this.config.mcps) {
|
|
407
|
+
const client = new UpstreamClient({
|
|
408
|
+
name: mcpName2,
|
|
409
|
+
transport,
|
|
410
|
+
onTransportError: (error) => {
|
|
411
|
+
this.config.onTransportError?.(mcpName2, error);
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
await client.connect();
|
|
415
|
+
const tools = await client.listTools();
|
|
416
|
+
this.clients.set(mcpName2, client);
|
|
417
|
+
groups.set(mcpName2, tools);
|
|
418
|
+
}
|
|
419
|
+
} catch (error) {
|
|
420
|
+
await this.disconnectAll();
|
|
421
|
+
throw error;
|
|
422
|
+
}
|
|
423
|
+
this.toolCatalog = ToolCatalog.fromGrouped(groups);
|
|
424
|
+
}
|
|
425
|
+
get catalog() {
|
|
426
|
+
if (this.toolCatalog === null) {
|
|
427
|
+
throw new Error("Orchestrator is not connected. Call connect() first.");
|
|
428
|
+
}
|
|
429
|
+
return this.toolCatalog;
|
|
430
|
+
}
|
|
431
|
+
async callTool(namespacedName, input) {
|
|
432
|
+
const separatorIndex = namespacedName.indexOf("/");
|
|
433
|
+
if (separatorIndex === -1) {
|
|
434
|
+
throw new Error(
|
|
435
|
+
`Invalid namespaced tool name: "${namespacedName}". Expected format: "mcpName/toolName".`
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
const mcpName2 = namespacedName.slice(0, separatorIndex);
|
|
439
|
+
const toolName = namespacedName.slice(separatorIndex + 1);
|
|
440
|
+
const client = this.clients.get(mcpName2);
|
|
441
|
+
if (client === void 0) {
|
|
442
|
+
const available = [...this.clients.keys()].sort().join(", ");
|
|
443
|
+
throw new Error(`Unknown MCP: "${mcpName2}". Available MCPs: ${available}`);
|
|
444
|
+
}
|
|
445
|
+
return client.callTool(toolName, input);
|
|
446
|
+
}
|
|
447
|
+
async disconnectAll() {
|
|
448
|
+
const disconnections = [...this.clients.values()].map((client) => client.disconnect());
|
|
449
|
+
await Promise.all(disconnections);
|
|
450
|
+
this.clients.clear();
|
|
451
|
+
this.toolCatalog = null;
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
// src/proxy/server.ts
|
|
456
|
+
var import_node_process2 = __toESM(require("process"), 1);
|
|
457
|
+
var import_fastmcp = require("fastmcp");
|
|
458
|
+
var import_zod2 = require("zod");
|
|
459
|
+
var ProxyServer = class {
|
|
460
|
+
catalog;
|
|
461
|
+
callTool;
|
|
462
|
+
constructor({ catalog, callTool }) {
|
|
463
|
+
this.catalog = catalog;
|
|
464
|
+
this.callTool = callTool;
|
|
465
|
+
}
|
|
466
|
+
async start() {
|
|
467
|
+
const server = new import_fastmcp.FastMCP({
|
|
468
|
+
name: "dynamic-discovery-mcp",
|
|
469
|
+
version: package_default.version
|
|
470
|
+
});
|
|
471
|
+
server.addTool({
|
|
472
|
+
name: "discover_tool",
|
|
473
|
+
description: this.catalog.discoverToolDescription,
|
|
474
|
+
parameters: import_zod2.z.object({ tool_name: import_zod2.z.string() }),
|
|
475
|
+
execute: async ({ tool_name }) => {
|
|
476
|
+
return this.catalog.getToolDetails(tool_name);
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
server.addTool({
|
|
480
|
+
name: "use_tool",
|
|
481
|
+
description: "Use a tool that was previously discovered with the discover_tool tool.",
|
|
482
|
+
parameters: import_zod2.z.object({
|
|
483
|
+
tool_name: import_zod2.z.string(),
|
|
484
|
+
tool_input: import_zod2.z.record(import_zod2.z.string(), import_zod2.z.unknown()).default({})
|
|
485
|
+
}),
|
|
486
|
+
execute: async ({ tool_name, tool_input }) => {
|
|
487
|
+
if (!this.catalog.tools.has(tool_name)) {
|
|
488
|
+
return this.catalog.getToolDetails(tool_name);
|
|
489
|
+
}
|
|
490
|
+
const result = await this.callTool(tool_name, tool_input);
|
|
491
|
+
return result;
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
import_node_process2.default.stderr.write("Starting dynamic-discovery-mcp server over stdio\n");
|
|
495
|
+
await server.start({ transportType: "stdio" });
|
|
309
496
|
}
|
|
310
497
|
};
|
|
311
498
|
|
|
312
499
|
// src/proxy/index.ts
|
|
313
500
|
async function startProxy(command, args) {
|
|
314
501
|
let isShuttingDown = false;
|
|
502
|
+
const transport = new import_stdio2.StdioClientTransport({ command, args });
|
|
503
|
+
const upstreamClient = new UpstreamClient({
|
|
504
|
+
name: command,
|
|
505
|
+
transport,
|
|
506
|
+
onTransportError: (error) => {
|
|
507
|
+
import_node_process3.default.stderr.write(`Upstream MCP transport error: ${error.message}
|
|
508
|
+
`);
|
|
509
|
+
shutdown(1);
|
|
510
|
+
}
|
|
511
|
+
});
|
|
315
512
|
const shutdown = (exitCode) => {
|
|
316
513
|
if (isShuttingDown) return;
|
|
317
514
|
isShuttingDown = true;
|
|
@@ -322,15 +519,6 @@ async function startProxy(command, args) {
|
|
|
322
519
|
);
|
|
323
520
|
}).finally(() => import_node_process3.default.exit(exitCode));
|
|
324
521
|
};
|
|
325
|
-
const upstreamClient = new UpstreamClient({
|
|
326
|
-
command,
|
|
327
|
-
args,
|
|
328
|
-
onTransportError: (error) => {
|
|
329
|
-
import_node_process3.default.stderr.write(`Upstream MCP transport error: ${error.message}
|
|
330
|
-
`);
|
|
331
|
-
shutdown(1);
|
|
332
|
-
}
|
|
333
|
-
});
|
|
334
522
|
try {
|
|
335
523
|
await upstreamClient.connect();
|
|
336
524
|
} catch (error) {
|
|
@@ -346,13 +534,68 @@ async function startProxy(command, args) {
|
|
|
346
534
|
`);
|
|
347
535
|
import_node_process3.default.exit(1);
|
|
348
536
|
}
|
|
349
|
-
const catalog =
|
|
350
|
-
const proxyServer = new ProxyServer({
|
|
537
|
+
const catalog = ToolCatalog.fromFlat(tools);
|
|
538
|
+
const proxyServer = new ProxyServer({
|
|
539
|
+
catalog,
|
|
540
|
+
callTool: (name, input) => upstreamClient.callTool(name, input)
|
|
541
|
+
});
|
|
542
|
+
import_node_process3.default.on("SIGINT", () => shutdown(0));
|
|
543
|
+
import_node_process3.default.on("SIGTERM", () => shutdown(0));
|
|
544
|
+
import_node_process3.default.stdin.on("end", () => shutdown(0));
|
|
545
|
+
import_node_process3.default.stdin.on("close", () => shutdown(0));
|
|
546
|
+
try {
|
|
547
|
+
await proxyServer.start();
|
|
548
|
+
} catch (error) {
|
|
549
|
+
shutdown(1);
|
|
550
|
+
throw error;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
async function startProxyFromConfig(configPath) {
|
|
554
|
+
let isShuttingDown = false;
|
|
555
|
+
const config = loadConfig(configPath);
|
|
556
|
+
const mcps = /* @__PURE__ */ new Map();
|
|
557
|
+
for (const [name, entry] of Object.entries(config.mcp)) {
|
|
558
|
+
mcps.set(name, { transport: createTransport(entry) });
|
|
559
|
+
}
|
|
560
|
+
const orchestrator = new Orchestrator({
|
|
561
|
+
mcps,
|
|
562
|
+
onTransportError: (mcpName2, error) => {
|
|
563
|
+
import_node_process3.default.stderr.write(`Upstream MCP "${mcpName2}" transport error: ${error.message}
|
|
564
|
+
`);
|
|
565
|
+
shutdown(1);
|
|
566
|
+
}
|
|
567
|
+
});
|
|
568
|
+
const shutdown = (exitCode) => {
|
|
569
|
+
if (isShuttingDown) return;
|
|
570
|
+
isShuttingDown = true;
|
|
571
|
+
orchestrator.disconnectAll().catch((error) => {
|
|
572
|
+
import_node_process3.default.stderr.write(
|
|
573
|
+
`dynmcp: error during disconnect: ${error instanceof Error ? error.message : String(error)}
|
|
574
|
+
`
|
|
575
|
+
);
|
|
576
|
+
}).finally(() => import_node_process3.default.exit(exitCode));
|
|
577
|
+
};
|
|
578
|
+
try {
|
|
579
|
+
await orchestrator.connect();
|
|
580
|
+
} catch (error) {
|
|
581
|
+
import_node_process3.default.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
|
|
582
|
+
`);
|
|
583
|
+
import_node_process3.default.exit(1);
|
|
584
|
+
}
|
|
585
|
+
const proxyServer = new ProxyServer({
|
|
586
|
+
catalog: orchestrator.catalog,
|
|
587
|
+
callTool: (name, input) => orchestrator.callTool(name, input)
|
|
588
|
+
});
|
|
351
589
|
import_node_process3.default.on("SIGINT", () => shutdown(0));
|
|
352
590
|
import_node_process3.default.on("SIGTERM", () => shutdown(0));
|
|
353
591
|
import_node_process3.default.stdin.on("end", () => shutdown(0));
|
|
354
592
|
import_node_process3.default.stdin.on("close", () => shutdown(0));
|
|
355
|
-
|
|
593
|
+
try {
|
|
594
|
+
await proxyServer.start();
|
|
595
|
+
} catch (error) {
|
|
596
|
+
shutdown(1);
|
|
597
|
+
throw error;
|
|
598
|
+
}
|
|
356
599
|
}
|
|
357
600
|
|
|
358
601
|
// src/cli.ts
|
|
@@ -363,23 +606,31 @@ var cliBanner = import_chalk.default.bold.magentaBright(
|
|
|
363
606
|
verticalLayout: "fitted"
|
|
364
607
|
})
|
|
365
608
|
);
|
|
366
|
-
var cli = new import_commander.Command(package_default.name).description(package_default.description).version(package_default.version).addHelpText("beforeAll", cliBanner).addHelpText(
|
|
609
|
+
var cli = new import_commander.Command(package_default.name).description(package_default.description).version(package_default.version).addHelpText("beforeAll", cliBanner).addHelpText(
|
|
610
|
+
"after",
|
|
611
|
+
"\nExamples:\n dynmcp -- npx -y chrome-devtools-mcp@latest\n dynmcp --config ./mcp.json\n"
|
|
612
|
+
).option("-c, --config <path>", "Path to config file (JSON or YAML)").allowExcessArguments(true).passThroughOptions(true).action(async (_options, cmd) => {
|
|
367
613
|
const separatorIndex = import_node_process4.default.argv.indexOf("--");
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
)
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
614
|
+
const configPath = cmd.opts().config;
|
|
615
|
+
if (separatorIndex !== -1) {
|
|
616
|
+
const [command, ...args] = import_node_process4.default.argv.slice(separatorIndex + 1);
|
|
617
|
+
if (command === void 0) {
|
|
618
|
+
import_node_process4.default.stderr.write(
|
|
619
|
+
"dynmcp: no upstream command provided after --.\nUsage: dynmcp -- <command> [args...]\n"
|
|
620
|
+
);
|
|
621
|
+
import_node_process4.default.exit(1);
|
|
622
|
+
}
|
|
623
|
+
try {
|
|
624
|
+
await startProxy(command, args);
|
|
625
|
+
} catch (error) {
|
|
626
|
+
import_node_process4.default.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
|
|
627
|
+
`);
|
|
628
|
+
import_node_process4.default.exit(1);
|
|
629
|
+
}
|
|
630
|
+
return;
|
|
380
631
|
}
|
|
381
632
|
try {
|
|
382
|
-
await
|
|
633
|
+
await startProxyFromConfig(configPath);
|
|
383
634
|
} catch (error) {
|
|
384
635
|
import_node_process4.default.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
|
|
385
636
|
`);
|