@agimon-ai/mcp-proxy 0.10.5 → 0.10.8
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/cli.cjs +145 -1753
- package/dist/cli.mjs +145 -1752
- package/dist/index.cjs +1 -27
- package/dist/index.mjs +1 -2
- package/dist/src-BRdnqbWv.mjs +59 -0
- package/dist/src-DjeyR393.cjs +59 -0
- package/package.json +5 -5
- package/dist/cli.d.cts +0 -1
- package/dist/cli.d.mts +0 -1
- package/dist/index.d.cts +0 -1368
- package/dist/index.d.mts +0 -1368
- package/dist/src-Bf27zWoI.cjs +0 -5185
- package/dist/src-Dv4pMmyW.mjs +0 -4995
package/dist/cli.cjs
CHANGED
|
@@ -1,1754 +1,146 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
const
|
|
3
|
-
|
|
4
|
-
let
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
let
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
force: true
|
|
148
|
-
});
|
|
149
|
-
} catch {
|
|
150
|
-
await runtimeStateService.remove(match.serverId);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
async function prestartHttpRuntime(options) {
|
|
154
|
-
const serverId = options.id || require_src.generateServerId();
|
|
155
|
-
const timeoutMs = parseTimeoutMs(options.timeoutMs);
|
|
156
|
-
const registryPath = options.registryPath || options.registryDir;
|
|
157
|
-
const workspaceRoot = resolveWorkspaceRoot();
|
|
158
|
-
const existing = await findExistingRuntime(workspaceRoot);
|
|
159
|
-
if (existing && await isRuntimeHealthy(existing.host, existing.port)) return {
|
|
160
|
-
host: existing.host,
|
|
161
|
-
port: existing.port,
|
|
162
|
-
serverId: existing.serverId,
|
|
163
|
-
workspaceRoot,
|
|
164
|
-
reusedExistingRuntime: true
|
|
165
|
-
};
|
|
166
|
-
const targetPort = options.port ?? existing?.port;
|
|
167
|
-
await stopExistingRuntime(new require_src.RuntimeStateService(), options.id, options.host, targetPort);
|
|
168
|
-
const childEnv = {
|
|
169
|
-
...process.env,
|
|
170
|
-
...registryPath ? {
|
|
171
|
-
PORT_REGISTRY_PATH: registryPath,
|
|
172
|
-
PROCESS_REGISTRY_PATH: (0, _agimon_ai_foundation_process_registry.resolveSiblingRegistryPath)(registryPath, "processes.json")
|
|
173
|
-
} : {}
|
|
174
|
-
};
|
|
175
|
-
const child = spawnBackgroundRuntime([
|
|
176
|
-
"mcp-serve",
|
|
177
|
-
"--type",
|
|
178
|
-
"http",
|
|
179
|
-
"--id",
|
|
180
|
-
serverId,
|
|
181
|
-
"--host",
|
|
182
|
-
options.host || DEFAULT_HOST$1,
|
|
183
|
-
...targetPort !== void 0 ? ["--port", String(targetPort)] : [],
|
|
184
|
-
...options.config ? ["--config", options.config] : [],
|
|
185
|
-
...options.cache === false ? ["--no-cache"] : [],
|
|
186
|
-
...options.definitionsCache ? ["--definitions-cache", options.definitionsCache] : [],
|
|
187
|
-
...options.clearDefinitionsCache ? ["--clear-definitions-cache"] : [],
|
|
188
|
-
"--proxy-mode",
|
|
189
|
-
options.proxyMode
|
|
190
|
-
], childEnv, workspaceRoot);
|
|
191
|
-
const childExit = new Promise((_, reject) => {
|
|
192
|
-
child.once("exit", (code, signal) => {
|
|
193
|
-
reject(/* @__PURE__ */ new Error(`Background runtime exited before becoming healthy (code=${code ?? "null"}, signal=${signal ?? "null"})`));
|
|
194
|
-
});
|
|
195
|
-
});
|
|
196
|
-
const runtimeFile = node_path.default.join(require_src.RuntimeStateService.getDefaultRuntimeDir(), `${serverId}.runtime.json`);
|
|
197
|
-
try {
|
|
198
|
-
await Promise.race([waitForFile(runtimeFile, timeoutMs), childExit]);
|
|
199
|
-
const { host, port } = await Promise.race([waitForHealthyRuntime(serverId, timeoutMs), childExit]);
|
|
200
|
-
return {
|
|
201
|
-
host,
|
|
202
|
-
port,
|
|
203
|
-
serverId,
|
|
204
|
-
workspaceRoot,
|
|
205
|
-
reusedExistingRuntime: false
|
|
206
|
-
};
|
|
207
|
-
} catch (error) {
|
|
208
|
-
throw new Error(`Failed to prestart HTTP runtime '${serverId}': ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
function writePrestartHttpResult(result) {
|
|
212
|
-
process.stdout.write(`mcp-proxy HTTP runtime ready at http://${result.host}:${result.port} (${result.serverId})\n`);
|
|
213
|
-
process.stdout.write(`runtimeId=${result.serverId}\n`);
|
|
214
|
-
process.stdout.write(`runtimeUrl=http://${result.host}:${result.port}\n`);
|
|
215
|
-
process.stdout.write(`workspaceRoot=${result.workspaceRoot}\n`);
|
|
216
|
-
}
|
|
217
|
-
//#endregion
|
|
218
|
-
//#region src/commands/bootstrap.ts
|
|
219
|
-
function toErrorMessage$9(error) {
|
|
220
|
-
return error instanceof Error ? error.message : String(error);
|
|
221
|
-
}
|
|
222
|
-
async function checkHealth(host, port) {
|
|
223
|
-
try {
|
|
224
|
-
return (await fetch(`http://${host}:${port}/health`, { signal: AbortSignal.timeout(3e3) })).ok;
|
|
225
|
-
} catch {
|
|
226
|
-
return false;
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
/**
|
|
230
|
-
* Proxy mode: connect to a running HTTP server instead of downstream servers directly.
|
|
231
|
-
* Auto-starts the server if not running.
|
|
232
|
-
*/
|
|
233
|
-
async function withProxiedContext(container, config, configFilePath, options, run) {
|
|
234
|
-
const host = config.proxy?.host ?? "localhost";
|
|
235
|
-
const port = config.proxy?.port;
|
|
236
|
-
const endpoint = `http://${host}:${port}/mcp`;
|
|
237
|
-
if (!await checkHealth(host, port)) {
|
|
238
|
-
if (!options.json) console.error("Starting HTTP proxy server in background...");
|
|
239
|
-
await prestartHttpRuntime({
|
|
240
|
-
host,
|
|
241
|
-
port,
|
|
242
|
-
config: configFilePath,
|
|
243
|
-
cache: options.useCache !== false,
|
|
244
|
-
clearDefinitionsCache: false,
|
|
245
|
-
proxyMode: "flat"
|
|
246
|
-
});
|
|
247
|
-
}
|
|
248
|
-
const clientManager = container.createClientManagerService();
|
|
249
|
-
try {
|
|
250
|
-
await clientManager.connectToServer("proxy", {
|
|
251
|
-
name: "proxy",
|
|
252
|
-
transport: "http",
|
|
253
|
-
config: { url: endpoint }
|
|
254
|
-
});
|
|
255
|
-
if (!options.json) console.error(`✓ Connected to proxy at ${endpoint}`);
|
|
256
|
-
} catch (error) {
|
|
257
|
-
throw new Error(`Failed to connect to proxy server at ${endpoint}: ${toErrorMessage$9(error)}`);
|
|
258
|
-
}
|
|
259
|
-
try {
|
|
260
|
-
return await run({
|
|
261
|
-
container,
|
|
262
|
-
configFilePath,
|
|
263
|
-
config,
|
|
264
|
-
clientManager
|
|
265
|
-
});
|
|
266
|
-
} finally {
|
|
267
|
-
await clientManager.disconnectAll();
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
/**
|
|
271
|
-
* Direct mode: connect to all downstream MCP servers individually.
|
|
272
|
-
*/
|
|
273
|
-
async function withDirectContext(container, config, configFilePath, options, run) {
|
|
274
|
-
const clientManager = container.createClientManagerService();
|
|
275
|
-
await Promise.all(Object.entries(config.mcpServers).map(async ([serverName, serverConfig]) => {
|
|
276
|
-
try {
|
|
277
|
-
await clientManager.connectToServer(serverName, serverConfig);
|
|
278
|
-
if (!options.json) console.error(`✓ Connected to ${serverName}`);
|
|
279
|
-
} catch (error) {
|
|
280
|
-
if (!options.json) console.error(`✗ Failed to connect to ${serverName}: ${toErrorMessage$9(error)}`);
|
|
281
|
-
}
|
|
282
|
-
}));
|
|
283
|
-
if (clientManager.getAllClients().length === 0) throw new Error("No MCP servers connected");
|
|
284
|
-
try {
|
|
285
|
-
return await run({
|
|
286
|
-
container,
|
|
287
|
-
configFilePath,
|
|
288
|
-
config,
|
|
289
|
-
clientManager
|
|
290
|
-
});
|
|
291
|
-
} finally {
|
|
292
|
-
await clientManager.disconnectAll();
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
async function withConnectedCommandContext(options, run) {
|
|
296
|
-
const container = require_src.createProxyIoCContainer();
|
|
297
|
-
const configFilePath = options.config || require_src.findConfigFile();
|
|
298
|
-
if (!configFilePath) throw new Error("No config file found. Use --config or create mcp-config.yaml");
|
|
299
|
-
const config = await container.createConfigFetcherService({
|
|
300
|
-
configFilePath,
|
|
301
|
-
useCache: options.useCache
|
|
302
|
-
}).fetchConfiguration();
|
|
303
|
-
if (config.proxy?.port) return await withProxiedContext(container, config, configFilePath, options, run);
|
|
304
|
-
return await withDirectContext(container, config, configFilePath, options, run);
|
|
305
|
-
}
|
|
306
|
-
//#endregion
|
|
307
|
-
//#region src/commands/describe-tools.ts
|
|
308
|
-
/**
|
|
309
|
-
* Describe Tools Command
|
|
310
|
-
*
|
|
311
|
-
* DESIGN PATTERNS:
|
|
312
|
-
* - Command pattern with Commander for CLI argument parsing
|
|
313
|
-
* - Async/await pattern for asynchronous operations
|
|
314
|
-
* - Error handling pattern with try-catch and proper exit codes
|
|
315
|
-
*
|
|
316
|
-
* CODING STANDARDS:
|
|
317
|
-
* - Use async action handlers for asynchronous operations
|
|
318
|
-
* - Provide clear option descriptions and default values
|
|
319
|
-
* - Handle errors gracefully with process.exit()
|
|
320
|
-
* - Log progress and errors to console
|
|
321
|
-
* - Use Commander's .option() and .argument() for inputs
|
|
322
|
-
*
|
|
323
|
-
* AVOID:
|
|
324
|
-
* - Synchronous blocking operations in action handlers
|
|
325
|
-
* - Missing error handling (always use try-catch)
|
|
326
|
-
* - Hardcoded values (use options or environment variables)
|
|
327
|
-
* - Not exiting with appropriate exit codes on errors
|
|
328
|
-
*/
|
|
329
|
-
function toErrorMessage$8(error) {
|
|
330
|
-
return error instanceof Error ? error.message : String(error);
|
|
331
|
-
}
|
|
332
|
-
/**
|
|
333
|
-
* Describe specific MCP tools
|
|
334
|
-
*/
|
|
335
|
-
const describeToolsCommand = new commander.Command("describe-tools").description("Describe specific MCP tools").argument("<toolNames...>", "Tool names to describe").option("-c, --config <path>", "Path to MCP server configuration file").option("-s, --server <name>", "Filter by server name").option("-j, --json", "Output as JSON", false).action(async (toolNames, options) => {
|
|
336
|
-
try {
|
|
337
|
-
await withConnectedCommandContext(options, async ({ container, config, clientManager }) => {
|
|
338
|
-
const clients = options.server ? [clientManager.getClient(options.server)].filter((client) => client !== void 0) : clientManager.getAllClients();
|
|
339
|
-
if (options.server && clients.length === 0) throw new Error(`Server "${options.server}" not found`);
|
|
340
|
-
const cwd = process.env.PROJECT_PATH || process.cwd();
|
|
341
|
-
const skillPaths = config.skills?.paths || [];
|
|
342
|
-
const skillService = skillPaths.length > 0 ? container.createSkillService(cwd, skillPaths) : void 0;
|
|
343
|
-
const foundTools = [];
|
|
344
|
-
const foundSkills = [];
|
|
345
|
-
const notFoundTools = [...toolNames];
|
|
346
|
-
const toolResults = await Promise.all(clients.map(async (client) => {
|
|
347
|
-
try {
|
|
348
|
-
return {
|
|
349
|
-
client,
|
|
350
|
-
tools: await client.listTools(),
|
|
351
|
-
error: null
|
|
352
|
-
};
|
|
353
|
-
} catch (error) {
|
|
354
|
-
return {
|
|
355
|
-
client,
|
|
356
|
-
tools: [],
|
|
357
|
-
error
|
|
358
|
-
};
|
|
359
|
-
}
|
|
360
|
-
}));
|
|
361
|
-
for (const { client, tools, error } of toolResults) {
|
|
362
|
-
if (error) {
|
|
363
|
-
if (!options.json) console.error(`Failed to list tools from ${client.serverName}:`, error);
|
|
364
|
-
continue;
|
|
365
|
-
}
|
|
366
|
-
for (const toolName of toolNames) {
|
|
367
|
-
const tool = tools.find((entry) => entry.name === toolName);
|
|
368
|
-
if (tool) {
|
|
369
|
-
foundTools.push({
|
|
370
|
-
server: client.serverName,
|
|
371
|
-
name: tool.name,
|
|
372
|
-
description: tool.description,
|
|
373
|
-
inputSchema: tool.inputSchema
|
|
374
|
-
});
|
|
375
|
-
const idx = notFoundTools.indexOf(toolName);
|
|
376
|
-
if (idx > -1) notFoundTools.splice(idx, 1);
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
if (skillService && notFoundTools.length > 0) {
|
|
381
|
-
const skillResults = await Promise.all([...notFoundTools].map(async (toolName) => {
|
|
382
|
-
const skillName = toolName.startsWith("skill__") ? toolName.slice(7) : toolName;
|
|
383
|
-
return {
|
|
384
|
-
toolName,
|
|
385
|
-
skill: await skillService.getSkill(skillName)
|
|
386
|
-
};
|
|
387
|
-
}));
|
|
388
|
-
for (const { toolName, skill } of skillResults) if (skill) {
|
|
389
|
-
foundSkills.push({
|
|
390
|
-
name: skill.name,
|
|
391
|
-
location: skill.basePath,
|
|
392
|
-
instructions: skill.content
|
|
393
|
-
});
|
|
394
|
-
const idx = notFoundTools.indexOf(toolName);
|
|
395
|
-
if (idx > -1) notFoundTools.splice(idx, 1);
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
const nextSteps = [];
|
|
399
|
-
if (foundTools.length > 0) nextSteps.push("For MCP tools: Use the use_tool function with toolName and toolArgs based on the inputSchema above.");
|
|
400
|
-
if (foundSkills.length > 0) nextSteps.push(`For skill, just follow skill's description to continue.`);
|
|
401
|
-
if (options.json) {
|
|
402
|
-
const result = {};
|
|
403
|
-
if (foundTools.length > 0) result.tools = foundTools;
|
|
404
|
-
if (foundSkills.length > 0) result.skills = foundSkills;
|
|
405
|
-
if (nextSteps.length > 0) result.nextSteps = nextSteps;
|
|
406
|
-
if (notFoundTools.length > 0) result.notFound = notFoundTools;
|
|
407
|
-
console.log(JSON.stringify(result, null, 2));
|
|
408
|
-
} else {
|
|
409
|
-
if (foundTools.length > 0) {
|
|
410
|
-
console.log("\nFound tools:\n");
|
|
411
|
-
for (const tool of foundTools) {
|
|
412
|
-
console.log(`Server: ${tool.server}`);
|
|
413
|
-
console.log(`Tool: ${tool.name}`);
|
|
414
|
-
console.log(`Description: ${tool.description || "No description"}`);
|
|
415
|
-
console.log("Input Schema:");
|
|
416
|
-
console.log(JSON.stringify(tool.inputSchema, null, 2));
|
|
417
|
-
console.log("");
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
if (foundSkills.length > 0) {
|
|
421
|
-
console.log("\nFound skills:\n");
|
|
422
|
-
for (const skill of foundSkills) {
|
|
423
|
-
console.log(`Skill: ${skill.name}`);
|
|
424
|
-
console.log(`Location: ${skill.location}`);
|
|
425
|
-
console.log(`Instructions:\n${skill.instructions}`);
|
|
426
|
-
console.log("");
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
if (nextSteps.length > 0) {
|
|
430
|
-
console.log("\nNext steps:");
|
|
431
|
-
for (const step of nextSteps) console.log(` • ${step}`);
|
|
432
|
-
console.log("");
|
|
433
|
-
}
|
|
434
|
-
if (notFoundTools.length > 0) console.error(`\nTools/skills not found: ${notFoundTools.join(", ")}`);
|
|
435
|
-
if (foundTools.length === 0 && foundSkills.length === 0) {
|
|
436
|
-
console.error("No tools or skills found");
|
|
437
|
-
process.exit(1);
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
});
|
|
441
|
-
} catch (error) {
|
|
442
|
-
console.error(`Error executing describe-tools: ${toErrorMessage$8(error)}`);
|
|
443
|
-
process.exit(1);
|
|
444
|
-
}
|
|
445
|
-
});
|
|
446
|
-
//#endregion
|
|
447
|
-
//#region src/commands/get-prompt.ts
|
|
448
|
-
function toErrorMessage$7(error) {
|
|
449
|
-
return error instanceof Error ? error.message : String(error);
|
|
450
|
-
}
|
|
451
|
-
const getPromptCommand = new commander.Command("get-prompt").description("Get a prompt by name from a connected MCP server").argument("<promptName>", "Prompt name to fetch").option("-c, --config <path>", "Path to MCP server configuration file").option("-s, --server <name>", "Server name (required if prompt exists on multiple servers)").option("-a, --args <json>", "Prompt arguments as JSON string", "{}").option("-j, --json", "Output as JSON", false).action(async (promptName, options) => {
|
|
452
|
-
try {
|
|
453
|
-
let promptArgs = {};
|
|
454
|
-
try {
|
|
455
|
-
promptArgs = JSON.parse(options.args);
|
|
456
|
-
} catch {
|
|
457
|
-
throw new Error("Invalid JSON in --args");
|
|
458
|
-
}
|
|
459
|
-
await withConnectedCommandContext(options, async ({ clientManager }) => {
|
|
460
|
-
const clients = clientManager.getAllClients();
|
|
461
|
-
if (options.server) {
|
|
462
|
-
const client = clientManager.getClient(options.server);
|
|
463
|
-
if (!client) throw new Error(`Server "${options.server}" not found`);
|
|
464
|
-
const prompt = await client.getPrompt(promptName, promptArgs);
|
|
465
|
-
if (options.json) console.log(JSON.stringify(prompt, null, 2));
|
|
466
|
-
else for (const message of prompt.messages) {
|
|
467
|
-
const content = message.content;
|
|
468
|
-
if (typeof content === "object" && content && "text" in content) console.log(content.text);
|
|
469
|
-
else console.log(JSON.stringify(message, null, 2));
|
|
470
|
-
}
|
|
471
|
-
return;
|
|
472
|
-
}
|
|
473
|
-
const matchingServers = [];
|
|
474
|
-
await Promise.all(clients.map(async (client) => {
|
|
475
|
-
try {
|
|
476
|
-
if ((await client.listPrompts()).some((prompt) => prompt.name === promptName)) matchingServers.push(client.serverName);
|
|
477
|
-
} catch (error) {
|
|
478
|
-
if (!options.json) console.error(`Failed to list prompts from ${client.serverName}: ${toErrorMessage$7(error)}`);
|
|
479
|
-
}
|
|
480
|
-
}));
|
|
481
|
-
if (matchingServers.length === 0) throw new Error(`Prompt "${promptName}" not found on any connected server`);
|
|
482
|
-
if (matchingServers.length > 1) throw new Error(`Prompt "${promptName}" found on multiple servers: ${matchingServers.join(", ")}. Use --server to disambiguate`);
|
|
483
|
-
const client = clientManager.getClient(matchingServers[0]);
|
|
484
|
-
if (!client) throw new Error(`Internal error: Server "${matchingServers[0]}" not connected`);
|
|
485
|
-
const prompt = await client.getPrompt(promptName, promptArgs);
|
|
486
|
-
if (options.json) console.log(JSON.stringify(prompt, null, 2));
|
|
487
|
-
else for (const message of prompt.messages) {
|
|
488
|
-
const content = message.content;
|
|
489
|
-
if (typeof content === "object" && content && "text" in content) console.log(content.text);
|
|
490
|
-
else console.log(JSON.stringify(message, null, 2));
|
|
491
|
-
}
|
|
492
|
-
});
|
|
493
|
-
} catch (error) {
|
|
494
|
-
console.error(`Error executing get-prompt: ${toErrorMessage$7(error)}`);
|
|
495
|
-
process.exit(1);
|
|
496
|
-
}
|
|
497
|
-
});
|
|
498
|
-
//#endregion
|
|
499
|
-
//#region src/templates/mcp-config.json?raw
|
|
500
|
-
var mcp_config_default = "{\n \"_comment\": \"MCP Server Configuration - Use ${VAR_NAME} syntax for environment variable interpolation\",\n \"_instructions\": \"config.instruction: Server's default instruction | instruction: User override (takes precedence)\",\n \"mcpServers\": {\n \"example-server\": {\n \"command\": \"node\",\n \"args\": [\"/path/to/mcp-server/build/index.js\"],\n \"env\": {\n \"LOG_LEVEL\": \"info\",\n \"_comment\": \"You can use environment variable interpolation:\",\n \"_example_DATABASE_URL\": \"${DATABASE_URL}\",\n \"_example_API_KEY\": \"${MY_API_KEY}\"\n },\n \"config\": {\n \"instruction\": \"Use this server for...\"\n },\n \"_instruction_override\": \"Optional user override - takes precedence over config.instruction\"\n }\n }\n}\n";
|
|
501
|
-
//#endregion
|
|
502
|
-
//#region src/templates/mcp-config.yaml.liquid?raw
|
|
503
|
-
var mcp_config_yaml_default = "# MCP Server Configuration\n# This file configures the MCP servers that mcp-proxy will connect to\n#\n# Environment Variable Interpolation:\n# Use ${VAR_NAME} syntax to reference environment variables\n# Example: ${HOME}, ${API_KEY}, ${DATABASE_URL}\n#\n# Instructions:\n# - config.instruction: Server's default instruction (from server documentation)\n# - instruction: User override (optional, takes precedence over config.instruction)\n# - config.toolBlacklist: Array of tool names to hide/block from this server\n# - config.omitToolDescription: Boolean to show only tool names without descriptions (saves tokens)\n\n# Remote Configuration Sources (OPTIONAL)\n# Fetch and merge configurations from remote URLs\n# Remote configs are merged with local configs based on merge strategy\n#\n# SECURITY: SSRF Protection is ENABLED by default\n# - Only HTTPS URLs are allowed (set security.enforceHttps: false to allow HTTP)\n# - Private IPs and localhost are blocked (set security.allowPrivateIPs: true for internal networks)\n# - Blocked ranges: 127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16\nremoteConfigs:\n # Example 1: Basic remote config with default security\n # - url: ${AGIFLOW_URL}/api/v1/mcp-configs\n # headers:\n # Authorization: Bearer ${AGIFLOW_API_KEY}\n # mergeStrategy: local-priority # Options: local-priority (default), remote-priority, merge-deep\n #\n # Example 2: Remote config with custom security settings (for internal networks)\n # - url: ${INTERNAL_URL}/mcp-configs\n # headers:\n # Authorization: Bearer ${INTERNAL_TOKEN}\n # security:\n # allowPrivateIPs: true # Allow internal IPs (default: false)\n # enforceHttps: false # Allow HTTP (default: true, HTTPS only)\n # mergeStrategy: local-priority\n #\n # Example 3: Remote config with additional validation (OPTIONAL)\n # - url: ${AGIFLOW_URL}/api/v1/mcp-configs\n # headers:\n # Authorization: Bearer ${AGIFLOW_API_KEY}\n # X-API-Key: ${AGIFLOW_API_KEY}\n # security:\n # enforceHttps: true # Require HTTPS (default: true)\n # allowPrivateIPs: false # Block private IPs (default: false)\n # validation: # OPTIONAL: Additional regex validation on top of security checks\n # url: ^https://.*\\.agiflow\\.io/.* # OPTIONAL: Regex pattern to validate URL format\n # headers: # OPTIONAL: Regex patterns to validate header values\n # Authorization: ^Bearer [A-Za-z0-9_-]+$\n # X-API-Key: ^[A-Za-z0-9_-]{32,}$\n # mergeStrategy: local-priority\n\nmcpServers:\n{%- if mcpServers %}{% for server in mcpServers %}\n {{ server.name }}:\n command: {{ server.command }}\n args:{% for arg in server.args %}\n - '{{ arg }}'{% endfor %}\n # env:\n # LOG_LEVEL: info\n # # API_KEY: ${MY_API_KEY}\n # config:\n # instruction: Use this server for...\n # # toolBlacklist:\n # # - tool_to_block\n # # omitToolDescription: true\n{% endfor %}\n # Example MCP server using SSE transport\n # remote-server:\n # url: https://example.com/mcp\n # type: sse\n # headers:\n # Authorization: Bearer ${API_KEY}\n # config:\n # instruction: This server provides tools for...\n{% else %}\n # Example MCP server using stdio transport\n example-server:\n command: node\n args:\n - /path/to/mcp-server/build/index.js\n env:\n # Environment variables for the MCP server\n LOG_LEVEL: info\n # You can use environment variable interpolation:\n # DATABASE_URL: ${DATABASE_URL}\n # API_KEY: ${MY_API_KEY}\n config:\n # Server's default instruction (from server documentation)\n instruction: Use this server for...\n # Optional: Block specific tools from being listed or executed\n # toolBlacklist:\n # - dangerous_tool_name\n # - another_blocked_tool\n # Optional: Omit tool descriptions to save tokens (default: false)\n # omitToolDescription: true\n # instruction: Optional user override - takes precedence over config.instruction\n\n # Example MCP server using SSE transport with environment variables\n # remote-server:\n # url: https://example.com/mcp\n # type: sse\n # headers:\n # # Use ${VAR_NAME} to interpolate environment variables\n # Authorization: Bearer ${API_KEY}\n # config:\n # instruction: This server provides tools for...\n # # Optional: Block specific tools from being listed or executed\n # # toolBlacklist:\n # # - tool_to_block\n # # Optional: Omit tool descriptions to save tokens (default: false)\n # # omitToolDescription: true\n # # instruction: Optional user override\n{% endif %}\n";
|
|
504
|
-
//#endregion
|
|
505
|
-
//#region src/utils/output.ts
|
|
506
|
-
function writeLine(message = "") {
|
|
507
|
-
console.log(message);
|
|
508
|
-
}
|
|
509
|
-
function writeError(message, detail) {
|
|
510
|
-
if (detail) console.error(`${message} ${detail}`);
|
|
511
|
-
else console.error(message);
|
|
512
|
-
}
|
|
513
|
-
const log = {
|
|
514
|
-
info: (message) => writeLine(message),
|
|
515
|
-
error: (message, detail) => writeError(message, detail)
|
|
516
|
-
};
|
|
517
|
-
const print = {
|
|
518
|
-
info: (message) => writeLine(message),
|
|
519
|
-
warning: (message) => writeLine(`Warning: ${message}`),
|
|
520
|
-
error: (message) => writeError(message),
|
|
521
|
-
success: (message) => writeLine(message),
|
|
522
|
-
newline: () => writeLine(),
|
|
523
|
-
header: (message) => writeLine(message),
|
|
524
|
-
item: (message) => writeLine(`- ${message}`),
|
|
525
|
-
indent: (message) => writeLine(` ${message}`)
|
|
526
|
-
};
|
|
527
|
-
//#endregion
|
|
528
|
-
//#region src/commands/init.ts
|
|
529
|
-
/**
|
|
530
|
-
* Init Command
|
|
531
|
-
*
|
|
532
|
-
* DESIGN PATTERNS:
|
|
533
|
-
* - Command pattern with Commander for CLI argument parsing
|
|
534
|
-
* - Async/await pattern for asynchronous operations
|
|
535
|
-
* - Error handling pattern with try-catch and proper exit codes
|
|
536
|
-
*
|
|
537
|
-
* CODING STANDARDS:
|
|
538
|
-
* - Use async action handlers for asynchronous operations
|
|
539
|
-
* - Provide clear option descriptions and default values
|
|
540
|
-
* - Handle errors gracefully with process.exit()
|
|
541
|
-
* - Log progress and errors to console
|
|
542
|
-
* - Use Commander's .option() and .argument() for inputs
|
|
543
|
-
*
|
|
544
|
-
* AVOID:
|
|
545
|
-
* - Synchronous blocking operations in action handlers
|
|
546
|
-
* - Missing error handling (always use try-catch)
|
|
547
|
-
* - Hardcoded values (use options or environment variables)
|
|
548
|
-
* - Not exiting with appropriate exit codes on errors
|
|
549
|
-
*/
|
|
550
|
-
/**
|
|
551
|
-
* Initialize MCP configuration file
|
|
552
|
-
*/
|
|
553
|
-
const initCommand = new commander.Command("init").description("Initialize MCP configuration file").option("-o, --output <path>", "Output file path", "mcp-config.yaml").option("--json", "Generate JSON config instead of YAML", false).option("-f, --force", "Overwrite existing config file", false).option("--mcp-servers <json>", "JSON string of MCP servers to add to config (optional)").action(async (options) => {
|
|
554
|
-
try {
|
|
555
|
-
const outputPath = (0, node_path.resolve)(options.output);
|
|
556
|
-
const isYaml = !options.json && (outputPath.endsWith(".yaml") || outputPath.endsWith(".yml"));
|
|
557
|
-
let content;
|
|
558
|
-
if (isYaml) {
|
|
559
|
-
const liquid = new liquidjs.Liquid();
|
|
560
|
-
let mcpServersData = null;
|
|
561
|
-
if (options.mcpServers) try {
|
|
562
|
-
const serversObj = JSON.parse(options.mcpServers);
|
|
563
|
-
mcpServersData = Object.entries(serversObj).map(([name, config]) => ({
|
|
564
|
-
name,
|
|
565
|
-
command: config.command,
|
|
566
|
-
args: config.args
|
|
567
|
-
}));
|
|
568
|
-
} catch (parseError) {
|
|
569
|
-
log.error("Failed to parse --mcp-servers JSON:", parseError instanceof Error ? parseError.message : String(parseError));
|
|
570
|
-
process.exit(1);
|
|
571
|
-
}
|
|
572
|
-
content = await liquid.parseAndRender(mcp_config_yaml_default, { mcpServers: mcpServersData });
|
|
573
|
-
} else content = mcp_config_default;
|
|
574
|
-
try {
|
|
575
|
-
await (0, node_fs_promises.writeFile)(outputPath, content, {
|
|
576
|
-
encoding: "utf-8",
|
|
577
|
-
flag: options.force ? "w" : "wx"
|
|
578
|
-
});
|
|
579
|
-
} catch (error) {
|
|
580
|
-
if (error && typeof error === "object" && "code" in error && error.code === "EEXIST") {
|
|
581
|
-
log.error(`Config file already exists: ${outputPath}`);
|
|
582
|
-
log.info("Use --force to overwrite");
|
|
583
|
-
process.exit(1);
|
|
584
|
-
}
|
|
585
|
-
throw error;
|
|
586
|
-
}
|
|
587
|
-
log.info(`MCP configuration file created: ${outputPath}`);
|
|
588
|
-
log.info("Next steps:");
|
|
589
|
-
log.info("1. Edit the configuration file to add your MCP servers");
|
|
590
|
-
log.info(`2. Run: mcp-proxy mcp-serve --config ${outputPath}`);
|
|
591
|
-
} catch (error) {
|
|
592
|
-
log.error("Error executing init:", error instanceof Error ? error.message : String(error));
|
|
593
|
-
process.exit(1);
|
|
594
|
-
}
|
|
595
|
-
});
|
|
596
|
-
//#endregion
|
|
597
|
-
//#region src/commands/list-prompts.ts
|
|
598
|
-
function toErrorMessage$6(error) {
|
|
599
|
-
return error instanceof Error ? error.message : String(error);
|
|
600
|
-
}
|
|
601
|
-
const listPromptsCommand = new commander.Command("list-prompts").description("List all available prompts from connected MCP servers").option("-c, --config <path>", "Path to MCP server configuration file").option("-s, --server <name>", "Filter by server name").option("-j, --json", "Output as JSON", false).action(async (options) => {
|
|
602
|
-
try {
|
|
603
|
-
await withConnectedCommandContext(options, async ({ clientManager }) => {
|
|
604
|
-
const clients = options.server ? [clientManager.getClient(options.server)].filter((client) => client !== void 0) : clientManager.getAllClients();
|
|
605
|
-
if (options.server && clients.length === 0) throw new Error(`Server "${options.server}" not found`);
|
|
606
|
-
const promptsByServer = {};
|
|
607
|
-
await Promise.all(clients.map(async (client) => {
|
|
608
|
-
try {
|
|
609
|
-
promptsByServer[client.serverName] = await client.listPrompts();
|
|
610
|
-
} catch (error) {
|
|
611
|
-
promptsByServer[client.serverName] = [];
|
|
612
|
-
if (!options.json) console.error(`Failed to list prompts from ${client.serverName}: ${toErrorMessage$6(error)}`);
|
|
613
|
-
}
|
|
614
|
-
}));
|
|
615
|
-
if (options.json) console.log(JSON.stringify(promptsByServer, null, 2));
|
|
616
|
-
else for (const [serverName, prompts] of Object.entries(promptsByServer)) {
|
|
617
|
-
console.log(`\n${serverName}:`);
|
|
618
|
-
if (prompts.length === 0) {
|
|
619
|
-
console.log(" No prompts available");
|
|
620
|
-
continue;
|
|
621
|
-
}
|
|
622
|
-
for (const prompt of prompts) {
|
|
623
|
-
console.log(` - ${prompt.name}: ${prompt.description || "No description"}`);
|
|
624
|
-
if (prompt.arguments && prompt.arguments.length > 0) {
|
|
625
|
-
const args = prompt.arguments.map((arg) => `${arg.name}${arg.required ? " (required)" : ""}`).join(", ");
|
|
626
|
-
console.log(` args: ${args}`);
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
});
|
|
631
|
-
} catch (error) {
|
|
632
|
-
console.error(`Error executing list-prompts: ${toErrorMessage$6(error)}`);
|
|
633
|
-
process.exit(1);
|
|
634
|
-
}
|
|
635
|
-
});
|
|
636
|
-
//#endregion
|
|
637
|
-
//#region src/commands/list-resources.ts
|
|
638
|
-
/**
|
|
639
|
-
* ListResources Command
|
|
640
|
-
*
|
|
641
|
-
* DESIGN PATTERNS:
|
|
642
|
-
* - Command pattern with Commander for CLI argument parsing
|
|
643
|
-
* - Async/await pattern for asynchronous operations
|
|
644
|
-
* - Error handling pattern with try-catch and proper exit codes
|
|
645
|
-
*
|
|
646
|
-
* CODING STANDARDS:
|
|
647
|
-
* - Use async action handlers for asynchronous operations
|
|
648
|
-
* - Provide clear option descriptions and default values
|
|
649
|
-
* - Handle errors gracefully with process.exit()
|
|
650
|
-
* - Log progress and errors to console
|
|
651
|
-
* - Use Commander's .option() and .argument() for inputs
|
|
652
|
-
*
|
|
653
|
-
* AVOID:
|
|
654
|
-
* - Synchronous blocking operations in action handlers
|
|
655
|
-
* - Missing error handling (always use try-catch)
|
|
656
|
-
* - Hardcoded values (use options or environment variables)
|
|
657
|
-
* - Not exiting with appropriate exit codes on errors
|
|
658
|
-
*/
|
|
659
|
-
function toErrorMessage$5(error) {
|
|
660
|
-
return error instanceof Error ? error.message : String(error);
|
|
661
|
-
}
|
|
662
|
-
/**
|
|
663
|
-
* List all available resources from connected MCP servers
|
|
664
|
-
*/
|
|
665
|
-
const listResourcesCommand = new commander.Command("list-resources").description("List all available resources from connected MCP servers").option("-c, --config <path>", "Path to MCP server configuration file").option("-s, --server <name>", "Filter by server name").option("-j, --json", "Output as JSON", false).action(async (options) => {
|
|
666
|
-
try {
|
|
667
|
-
await withConnectedCommandContext(options, async ({ clientManager }) => {
|
|
668
|
-
const clients = options.server ? [clientManager.getClient(options.server)].filter((c) => c !== void 0) : clientManager.getAllClients();
|
|
669
|
-
if (options.server && clients.length === 0) throw new Error(`Server "${options.server}" not found`);
|
|
670
|
-
const resourcesByServer = {};
|
|
671
|
-
const resourceResults = await Promise.all(clients.map(async (client) => {
|
|
672
|
-
try {
|
|
673
|
-
const resources = await client.listResources();
|
|
674
|
-
return {
|
|
675
|
-
serverName: client.serverName,
|
|
676
|
-
resources,
|
|
677
|
-
error: null
|
|
678
|
-
};
|
|
679
|
-
} catch (error) {
|
|
680
|
-
return {
|
|
681
|
-
serverName: client.serverName,
|
|
682
|
-
resources: [],
|
|
683
|
-
error
|
|
684
|
-
};
|
|
685
|
-
}
|
|
686
|
-
}));
|
|
687
|
-
for (const { serverName, resources, error } of resourceResults) {
|
|
688
|
-
if (error && !options.json) console.error(`Failed to list resources from ${serverName}: ${toErrorMessage$5(error)}`);
|
|
689
|
-
resourcesByServer[serverName] = resources;
|
|
690
|
-
}
|
|
691
|
-
if (options.json) console.log(JSON.stringify(resourcesByServer, null, 2));
|
|
692
|
-
else for (const [serverName, resources] of Object.entries(resourcesByServer)) {
|
|
693
|
-
console.log(`\n${serverName}:`);
|
|
694
|
-
if (resources.length === 0) console.log(" No resources available");
|
|
695
|
-
else for (const resource of resources) {
|
|
696
|
-
const label = resource.name ? `${resource.name} (${resource.uri})` : resource.uri;
|
|
697
|
-
console.log(` - ${label}${resource.description ? `: ${resource.description}` : ""}`);
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
});
|
|
701
|
-
} catch (error) {
|
|
702
|
-
console.error(`Error executing list-resources: ${toErrorMessage$5(error)}`);
|
|
703
|
-
process.exit(1);
|
|
704
|
-
}
|
|
705
|
-
});
|
|
706
|
-
//#endregion
|
|
707
|
-
//#region src/commands/list-tools.ts
|
|
708
|
-
/**
|
|
709
|
-
* List Tools Command
|
|
710
|
-
*
|
|
711
|
-
* DESIGN PATTERNS:
|
|
712
|
-
* - Command pattern with Commander for CLI argument parsing
|
|
713
|
-
* - Async/await pattern for asynchronous operations
|
|
714
|
-
* - Error handling pattern with try-catch and proper exit codes
|
|
715
|
-
*
|
|
716
|
-
* CODING STANDARDS:
|
|
717
|
-
* - Use async action handlers for asynchronous operations
|
|
718
|
-
* - Provide clear option descriptions and default values
|
|
719
|
-
* - Handle errors gracefully with process.exit()
|
|
720
|
-
* - Log progress and errors to console
|
|
721
|
-
* - Use Commander's .option() and .argument() for inputs
|
|
722
|
-
*
|
|
723
|
-
* AVOID:
|
|
724
|
-
* - Synchronous blocking operations in action handlers
|
|
725
|
-
* - Missing error handling (always use try-catch)
|
|
726
|
-
* - Hardcoded values (use options or environment variables)
|
|
727
|
-
* - Not exiting with appropriate exit codes on errors
|
|
728
|
-
*/
|
|
729
|
-
function toErrorMessage$4(error) {
|
|
730
|
-
return error instanceof Error ? error.message : String(error);
|
|
731
|
-
}
|
|
732
|
-
function printSearchResults(result) {
|
|
733
|
-
for (const server of result.servers) {
|
|
734
|
-
console.log(`\n${server.server}:`);
|
|
735
|
-
if (server.capabilities && server.capabilities.length > 0) console.log(` capabilities: ${server.capabilities.join(", ")}`);
|
|
736
|
-
if (server.summary) console.log(` summary: ${server.summary}`);
|
|
737
|
-
if (server.tools.length === 0) {
|
|
738
|
-
console.log(" no tools");
|
|
739
|
-
continue;
|
|
740
|
-
}
|
|
741
|
-
for (const tool of server.tools) {
|
|
742
|
-
const capabilitySummary = tool.capabilities && tool.capabilities.length > 0 ? ` [${tool.capabilities.join(", ")}]` : "";
|
|
743
|
-
console.log(` - ${tool.name}${capabilitySummary}`);
|
|
744
|
-
if (tool.description) console.log(` ${tool.description}`);
|
|
745
|
-
}
|
|
746
|
-
}
|
|
747
|
-
}
|
|
748
|
-
const searchToolsCommand = new commander.Command("search-tools").description("Search proxied MCP tools by capability or server").option("-c, --config <path>", "Path to MCP server configuration file").option("-s, --server <name>", "Filter by server name").option("--capability <name>", "Filter by capability tag, summary, tool name, or description").option("--definitions-cache <path>", "Path to definitions cache file").option("-j, --json", "Output as JSON", false).action(async (options) => {
|
|
749
|
-
try {
|
|
750
|
-
await withConnectedCommandContext(options, async ({ container, config, clientManager, configFilePath }) => {
|
|
751
|
-
clientManager.registerServerConfigs(config.mcpServers);
|
|
752
|
-
const cachePath = options.definitionsCache || require_src.DefinitionsCacheService.getDefaultCachePath(configFilePath);
|
|
753
|
-
let cacheData;
|
|
754
|
-
try {
|
|
755
|
-
cacheData = await require_src.DefinitionsCacheService.readFromFile(cachePath);
|
|
756
|
-
} catch {
|
|
757
|
-
cacheData = void 0;
|
|
758
|
-
}
|
|
759
|
-
const definitionsCacheService = container.createDefinitionsCacheService(clientManager, void 0, { cacheData });
|
|
760
|
-
const textBlock = (await container.createSearchListToolsTool(clientManager, definitionsCacheService).execute({
|
|
761
|
-
capability: options.capability,
|
|
762
|
-
serverName: options.server
|
|
763
|
-
})).content.find((content) => content.type === "text");
|
|
764
|
-
const parsed = textBlock?.type === "text" ? JSON.parse(textBlock.text) : { servers: [] };
|
|
765
|
-
if (options.json) console.log(JSON.stringify(parsed, null, 2));
|
|
766
|
-
else {
|
|
767
|
-
if (!parsed.servers || parsed.servers.length === 0) throw new Error("No tools matched the requested filters");
|
|
768
|
-
printSearchResults(parsed);
|
|
769
|
-
}
|
|
770
|
-
});
|
|
771
|
-
} catch (error) {
|
|
772
|
-
console.error(`Error executing search-tools: ${toErrorMessage$4(error)}`);
|
|
773
|
-
process.exit(1);
|
|
774
|
-
}
|
|
775
|
-
});
|
|
776
|
-
//#endregion
|
|
777
|
-
//#region src/commands/mcp-serve.ts
|
|
778
|
-
/**
|
|
779
|
-
* MCP Serve Command
|
|
780
|
-
*
|
|
781
|
-
* DESIGN PATTERNS:
|
|
782
|
-
* - Command pattern with Commander for CLI argument parsing
|
|
783
|
-
* - Transport abstraction pattern for flexible deployment (stdio, HTTP, SSE)
|
|
784
|
-
* - Factory pattern for creating transport handlers
|
|
785
|
-
* - Graceful shutdown pattern with signal handling
|
|
786
|
-
*
|
|
787
|
-
* CODING STANDARDS:
|
|
788
|
-
* - Use async/await for asynchronous operations
|
|
789
|
-
* - Implement proper error handling with try-catch blocks
|
|
790
|
-
* - Handle process signals for graceful shutdown
|
|
791
|
-
* - Provide clear CLI options and help messages
|
|
792
|
-
*
|
|
793
|
-
* AVOID:
|
|
794
|
-
* - Hardcoded configuration values (use CLI options or environment variables)
|
|
795
|
-
* - Missing error handling for transport startup
|
|
796
|
-
* - Not cleaning up resources on shutdown
|
|
797
|
-
*/
|
|
798
|
-
const CONFIG_FILE_NAMES = [
|
|
799
|
-
"mcp-config.yaml",
|
|
800
|
-
"mcp-config.yml",
|
|
801
|
-
"mcp-config.json"
|
|
802
|
-
];
|
|
803
|
-
const MCP_ENDPOINT_PATH = "/mcp";
|
|
804
|
-
const DEFAULT_HOST = "localhost";
|
|
805
|
-
const TRANSPORT_TYPE_STDIO = "stdio";
|
|
806
|
-
const TRANSPORT_TYPE_HTTP = "http";
|
|
807
|
-
const TRANSPORT_TYPE_SSE = "sse";
|
|
808
|
-
const TRANSPORT_TYPE_STDIO_HTTP = "stdio-http";
|
|
809
|
-
const RUNTIME_TRANSPORT = TRANSPORT_TYPE_HTTP;
|
|
810
|
-
const PORT_REGISTRY_SERVICE_HTTP = "mcp-proxy-http";
|
|
811
|
-
const PORT_REGISTRY_SERVICE_TYPE = "service";
|
|
812
|
-
function getWorkspaceRoot() {
|
|
813
|
-
return resolveWorkspaceRoot();
|
|
814
|
-
}
|
|
815
|
-
const PROCESS_REGISTRY_SERVICE_HTTP = "mcp-proxy-http";
|
|
816
|
-
const PROCESS_REGISTRY_SERVICE_TYPE = "service";
|
|
817
|
-
function getRegistryRepositoryPath() {
|
|
818
|
-
return getWorkspaceRoot();
|
|
819
|
-
}
|
|
820
|
-
function toErrorMessage$3(error) {
|
|
821
|
-
return error instanceof Error ? error.message : String(error);
|
|
822
|
-
}
|
|
823
|
-
function isValidTransportType(type) {
|
|
824
|
-
return type === TRANSPORT_TYPE_STDIO || type === TRANSPORT_TYPE_HTTP || type === TRANSPORT_TYPE_SSE || type === TRANSPORT_TYPE_STDIO_HTTP;
|
|
825
|
-
}
|
|
826
|
-
function isValidProxyMode(mode) {
|
|
827
|
-
return mode === "meta" || mode === "flat" || mode === "search";
|
|
828
|
-
}
|
|
829
|
-
async function pathExists(filePath) {
|
|
830
|
-
try {
|
|
831
|
-
await (0, node_fs_promises.access)(filePath, node_fs.constants.F_OK);
|
|
832
|
-
return true;
|
|
833
|
-
} catch {
|
|
834
|
-
return false;
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
async function findConfigFileAsync() {
|
|
838
|
-
try {
|
|
839
|
-
const projectPath = process.env.PROJECT_PATH;
|
|
840
|
-
if (projectPath) for (const fileName of CONFIG_FILE_NAMES) {
|
|
841
|
-
const configPath = (0, node_path.resolve)(projectPath, fileName);
|
|
842
|
-
if (await pathExists(configPath)) return configPath;
|
|
843
|
-
}
|
|
844
|
-
const MAX_PARENT_LEVELS = 3;
|
|
845
|
-
let searchDir = process.cwd();
|
|
846
|
-
for (let level = 0; level <= MAX_PARENT_LEVELS; level++) {
|
|
847
|
-
for (const fileName of CONFIG_FILE_NAMES) {
|
|
848
|
-
const configPath = (0, node_path.join)(searchDir, fileName);
|
|
849
|
-
if (await pathExists(configPath)) return configPath;
|
|
850
|
-
}
|
|
851
|
-
const parentDir = (0, node_path.dirname)(searchDir);
|
|
852
|
-
if (parentDir === searchDir) break;
|
|
853
|
-
searchDir = parentDir;
|
|
854
|
-
}
|
|
855
|
-
return null;
|
|
856
|
-
} catch (error) {
|
|
857
|
-
throw new Error(`Failed to discover MCP config file: ${toErrorMessage$3(error)}`);
|
|
858
|
-
}
|
|
859
|
-
}
|
|
860
|
-
function loadProxyDefaults(configPath) {
|
|
861
|
-
try {
|
|
862
|
-
const content = (0, node_fs.readFileSync)(configPath, "utf-8");
|
|
863
|
-
const proxy = (configPath.endsWith(".yaml") || configPath.endsWith(".yml") ? js_yaml.default.load(content) : JSON.parse(content))?.proxy;
|
|
864
|
-
if (!proxy || typeof proxy !== "object") return {};
|
|
865
|
-
const p = proxy;
|
|
866
|
-
return {
|
|
867
|
-
type: typeof p.type === "string" ? p.type : void 0,
|
|
868
|
-
port: typeof p.port === "number" && Number.isInteger(p.port) && p.port > 0 ? p.port : void 0,
|
|
869
|
-
host: typeof p.host === "string" ? p.host : void 0,
|
|
870
|
-
keepAlive: typeof p.keepAlive === "boolean" ? p.keepAlive : void 0
|
|
871
|
-
};
|
|
872
|
-
} catch {
|
|
873
|
-
return {};
|
|
874
|
-
}
|
|
875
|
-
}
|
|
876
|
-
async function resolveServerId(options, resolvedConfigPath) {
|
|
877
|
-
const container = require_src.createProxyIoCContainer();
|
|
878
|
-
if (options.id) return options.id;
|
|
879
|
-
if (resolvedConfigPath) try {
|
|
880
|
-
const config = await container.createConfigFetcherService({
|
|
881
|
-
configFilePath: resolvedConfigPath,
|
|
882
|
-
useCache: options.cache !== false
|
|
883
|
-
}).fetchConfiguration(options.cache === false);
|
|
884
|
-
if (config.id) return config.id;
|
|
885
|
-
} catch (error) {
|
|
886
|
-
throw new Error(`Failed to resolve server ID from config '${resolvedConfigPath}': ${toErrorMessage$3(error)}`);
|
|
887
|
-
}
|
|
888
|
-
return require_src.generateServerId();
|
|
889
|
-
}
|
|
890
|
-
function validateTransportType(type) {
|
|
891
|
-
if (!isValidTransportType(type)) throw new Error(`Unknown transport type: '${type}'. Valid options: ${TRANSPORT_TYPE_STDIO}, ${TRANSPORT_TYPE_HTTP}, ${TRANSPORT_TYPE_SSE}, ${TRANSPORT_TYPE_STDIO_HTTP}`);
|
|
892
|
-
return type;
|
|
893
|
-
}
|
|
894
|
-
function validateProxyMode(mode) {
|
|
895
|
-
if (!isValidProxyMode(mode)) throw new Error(`Unknown proxy mode: '${mode}'. Valid options: meta, flat, search`);
|
|
896
|
-
}
|
|
897
|
-
function createTransportConfig(options, mode, proxyDefaults) {
|
|
898
|
-
const envPort = process.env.MCP_PORT ? Number(process.env.MCP_PORT) : void 0;
|
|
899
|
-
return {
|
|
900
|
-
mode,
|
|
901
|
-
port: options.port ?? (Number.isFinite(envPort) ? envPort : void 0) ?? proxyDefaults?.port,
|
|
902
|
-
host: options.host ?? process.env.MCP_HOST ?? proxyDefaults?.host ?? DEFAULT_HOST
|
|
903
|
-
};
|
|
904
|
-
}
|
|
905
|
-
function createStdioSafeLogger() {
|
|
906
|
-
const logToStderr = (message, data) => {
|
|
907
|
-
if (data === void 0) {
|
|
908
|
-
console.error(message);
|
|
909
|
-
return;
|
|
910
|
-
}
|
|
911
|
-
console.error(message, data);
|
|
912
|
-
};
|
|
913
|
-
return {
|
|
914
|
-
trace: logToStderr,
|
|
915
|
-
debug: logToStderr,
|
|
916
|
-
info: logToStderr,
|
|
917
|
-
warn: logToStderr,
|
|
918
|
-
error: logToStderr
|
|
919
|
-
};
|
|
920
|
-
}
|
|
921
|
-
function createServerOptions(options, resolvedConfigPath, serverId) {
|
|
922
|
-
return {
|
|
923
|
-
configFilePath: resolvedConfigPath,
|
|
924
|
-
noCache: options.cache === false,
|
|
925
|
-
serverId,
|
|
926
|
-
definitionsCachePath: options.definitionsCache,
|
|
927
|
-
clearDefinitionsCache: options.clearDefinitionsCache,
|
|
928
|
-
proxyMode: options.proxyMode
|
|
929
|
-
};
|
|
930
|
-
}
|
|
931
|
-
function formatStartError(type, host, port, error) {
|
|
932
|
-
const startErrorMessage = toErrorMessage$3(error);
|
|
933
|
-
if (type === TRANSPORT_TYPE_STDIO) return `Failed to start MCP server with transport '${type}': ${startErrorMessage}`;
|
|
934
|
-
return `Failed to start MCP server with transport '${type}' on ${port === void 0 ? `${host} (dynamic port)` : `${host}:${port}`}: ${startErrorMessage}`;
|
|
935
|
-
}
|
|
936
|
-
function createRuntimeRecord(serverId, config, port, shutdownToken, configPath) {
|
|
937
|
-
return {
|
|
938
|
-
serverId,
|
|
939
|
-
host: config.host ?? DEFAULT_HOST,
|
|
940
|
-
port,
|
|
941
|
-
transport: RUNTIME_TRANSPORT,
|
|
942
|
-
shutdownToken,
|
|
943
|
-
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
944
|
-
pid: process.pid,
|
|
945
|
-
configPath
|
|
946
|
-
};
|
|
947
|
-
}
|
|
948
|
-
function createPortRegistryService() {
|
|
949
|
-
return new _agimon_ai_foundation_port_registry.PortRegistryService(process.env.PORT_REGISTRY_PATH);
|
|
950
|
-
}
|
|
951
|
-
function getRegistryEnvironment() {
|
|
952
|
-
return process.env.NODE_ENV ?? "development";
|
|
953
|
-
}
|
|
954
|
-
async function createPortRegistryLease(serviceName, host, preferredPort, serverId, transport, configPath, portRange = preferredPort !== void 0 ? {
|
|
955
|
-
min: preferredPort,
|
|
956
|
-
max: preferredPort
|
|
957
|
-
} : _agimon_ai_foundation_port_registry.DEFAULT_PORT_RANGE) {
|
|
958
|
-
const portRegistry = createPortRegistryService();
|
|
959
|
-
const result = await portRegistry.reservePort({
|
|
960
|
-
repositoryPath: getRegistryRepositoryPath(),
|
|
961
|
-
serviceName,
|
|
962
|
-
serviceType: PORT_REGISTRY_SERVICE_TYPE,
|
|
963
|
-
environment: getRegistryEnvironment(),
|
|
964
|
-
pid: process.pid,
|
|
965
|
-
host,
|
|
966
|
-
preferredPort,
|
|
967
|
-
portRange,
|
|
968
|
-
force: true,
|
|
969
|
-
metadata: {
|
|
970
|
-
transport,
|
|
971
|
-
serverId,
|
|
972
|
-
...configPath ? { configPath } : {}
|
|
973
|
-
}
|
|
974
|
-
});
|
|
975
|
-
if (!result.success || !result.record) {
|
|
976
|
-
const requestedPortLabel = preferredPort === void 0 ? "dynamic port" : `port ${preferredPort}`;
|
|
977
|
-
throw new Error(result.error || `Failed to reserve ${requestedPortLabel} in port registry`);
|
|
978
|
-
}
|
|
979
|
-
let released = false;
|
|
980
|
-
return {
|
|
981
|
-
port: result.record.port,
|
|
982
|
-
release: async () => {
|
|
983
|
-
if (released) return;
|
|
984
|
-
released = true;
|
|
985
|
-
const releaseResult = await portRegistry.releasePort({
|
|
986
|
-
repositoryPath: getRegistryRepositoryPath(),
|
|
987
|
-
serviceName,
|
|
988
|
-
serviceType: PORT_REGISTRY_SERVICE_TYPE,
|
|
989
|
-
pid: process.pid,
|
|
990
|
-
environment: getRegistryEnvironment(),
|
|
991
|
-
force: true
|
|
992
|
-
});
|
|
993
|
-
if (!releaseResult.success && releaseResult.error && !releaseResult.error.includes("No matching registry entry")) throw new Error(releaseResult.error || `Failed to release port for ${serviceName}`);
|
|
994
|
-
}
|
|
995
|
-
};
|
|
996
|
-
}
|
|
997
|
-
async function releasePortLease(lease) {
|
|
998
|
-
if (!lease) return;
|
|
999
|
-
await lease.release();
|
|
1000
|
-
}
|
|
1001
|
-
function createHttpAdminOptions(serverId, shutdownToken, onShutdownRequested) {
|
|
1002
|
-
return {
|
|
1003
|
-
serverId,
|
|
1004
|
-
shutdownToken,
|
|
1005
|
-
onShutdownRequested
|
|
1006
|
-
};
|
|
1007
|
-
}
|
|
1008
|
-
async function removeRuntimeRecord(runtimeStateService, serverId) {
|
|
1009
|
-
try {
|
|
1010
|
-
await runtimeStateService.remove(serverId);
|
|
1011
|
-
} catch (error) {
|
|
1012
|
-
throw new Error(`Failed to remove runtime state for '${serverId}': ${toErrorMessage$3(error)}`);
|
|
1013
|
-
}
|
|
1014
|
-
}
|
|
1015
|
-
async function writeRuntimeRecord(runtimeStateService, record) {
|
|
1016
|
-
try {
|
|
1017
|
-
await runtimeStateService.write(record);
|
|
1018
|
-
} catch (error) {
|
|
1019
|
-
throw new Error(`Failed to persist runtime state for '${record.serverId}': ${toErrorMessage$3(error)}`);
|
|
1020
|
-
}
|
|
1021
|
-
}
|
|
1022
|
-
async function stopOwnedHttpTransport(handler, runtimeStateService, serverId, processLease) {
|
|
1023
|
-
try {
|
|
1024
|
-
try {
|
|
1025
|
-
await handler.stop();
|
|
1026
|
-
} catch (error) {
|
|
1027
|
-
throw new Error(`Failed to stop owned HTTP transport '${serverId}': ${toErrorMessage$3(error)}`);
|
|
1028
|
-
}
|
|
1029
|
-
} finally {
|
|
1030
|
-
await processLease?.release({ kill: false });
|
|
1031
|
-
await removeRuntimeRecord(runtimeStateService, serverId);
|
|
1032
|
-
}
|
|
1033
|
-
}
|
|
1034
|
-
/**
|
|
1035
|
-
* Run post-stop cleanup for an HTTP runtime (release port, dispose services, remove state).
|
|
1036
|
-
* This is the subset of stopOwnedHttpTransport that runs AFTER handler.stop() has already
|
|
1037
|
-
* been called by startServer()'s signal handler — avoids double-stopping the transport.
|
|
1038
|
-
*/
|
|
1039
|
-
async function cleanupHttpRuntime(runtimeStateService, serverId, processLease) {
|
|
1040
|
-
await processLease?.release({ kill: false });
|
|
1041
|
-
await removeRuntimeRecord(runtimeStateService, serverId);
|
|
1042
|
-
}
|
|
1043
|
-
async function cleanupFailedRuntimeStartup(handler, runtimeStateService, serverId, processLease) {
|
|
1044
|
-
try {
|
|
1045
|
-
try {
|
|
1046
|
-
await handler.stop();
|
|
1047
|
-
} catch (error) {
|
|
1048
|
-
throw new Error(`Failed to stop HTTP transport during cleanup for '${serverId}': ${toErrorMessage$3(error)}`);
|
|
1049
|
-
}
|
|
1050
|
-
} finally {
|
|
1051
|
-
await processLease?.release({ kill: false });
|
|
1052
|
-
await removeRuntimeRecord(runtimeStateService, serverId);
|
|
1053
|
-
}
|
|
1054
|
-
}
|
|
1055
|
-
/**
|
|
1056
|
-
* Start MCP server with given transport handler
|
|
1057
|
-
* @param handler - The transport handler to start
|
|
1058
|
-
* @param onStopped - Optional cleanup callback run after signal-based shutdown
|
|
1059
|
-
*/
|
|
1060
|
-
async function startServer(handler, onStopped) {
|
|
1061
|
-
try {
|
|
1062
|
-
await handler.start();
|
|
1063
|
-
} catch (error) {
|
|
1064
|
-
throw new Error(`Failed to start transport handler: ${toErrorMessage$3(error)}`);
|
|
1065
|
-
}
|
|
1066
|
-
const shutdown = async (signal) => {
|
|
1067
|
-
console.error(`\nReceived ${signal}, shutting down gracefully...`);
|
|
1068
|
-
try {
|
|
1069
|
-
await handler.stop();
|
|
1070
|
-
if (onStopped) await onStopped();
|
|
1071
|
-
process.exit(0);
|
|
1072
|
-
} catch (error) {
|
|
1073
|
-
console.error(`Failed to gracefully stop transport during ${signal}: ${toErrorMessage$3(error)}`);
|
|
1074
|
-
process.exit(1);
|
|
1075
|
-
}
|
|
1076
|
-
};
|
|
1077
|
-
process.on("SIGINT", async () => await shutdown("SIGINT"));
|
|
1078
|
-
process.on("SIGTERM", async () => await shutdown("SIGTERM"));
|
|
1079
|
-
}
|
|
1080
|
-
async function createAndStartHttpRuntime(serverOptions, config, resolvedConfigPath) {
|
|
1081
|
-
const sharedServices = await require_src.initializeSharedServices(serverOptions);
|
|
1082
|
-
const runtimeStateService = new require_src.RuntimeStateService();
|
|
1083
|
-
const shutdownToken = (0, node_crypto.randomUUID)();
|
|
1084
|
-
const runtimeServerId = serverOptions.serverId ?? require_src.generateServerId();
|
|
1085
|
-
const requestedPort = config.port;
|
|
1086
|
-
const portRange = requestedPort !== void 0 ? {
|
|
1087
|
-
min: requestedPort,
|
|
1088
|
-
max: requestedPort
|
|
1089
|
-
} : _agimon_ai_foundation_port_registry.DEFAULT_PORT_RANGE;
|
|
1090
|
-
const portLease = await createPortRegistryLease(PORT_REGISTRY_SERVICE_HTTP, config.host ?? DEFAULT_HOST, requestedPort, runtimeServerId, TRANSPORT_TYPE_HTTP, resolvedConfigPath, portRange);
|
|
1091
|
-
const runtimePort = portLease.port;
|
|
1092
|
-
const runtimeConfig = {
|
|
1093
|
-
...config,
|
|
1094
|
-
port: runtimePort
|
|
1095
|
-
};
|
|
1096
|
-
const processLease = await (0, _agimon_ai_foundation_process_registry.createProcessLease)({
|
|
1097
|
-
repositoryPath: getRegistryRepositoryPath(),
|
|
1098
|
-
serviceName: PROCESS_REGISTRY_SERVICE_HTTP,
|
|
1099
|
-
serviceType: PROCESS_REGISTRY_SERVICE_TYPE,
|
|
1100
|
-
environment: getRegistryEnvironment(),
|
|
1101
|
-
host: runtimeConfig.host ?? DEFAULT_HOST,
|
|
1102
|
-
port: runtimePort,
|
|
1103
|
-
command: process.argv[1],
|
|
1104
|
-
args: process.argv.slice(2),
|
|
1105
|
-
metadata: {
|
|
1106
|
-
transport: TRANSPORT_TYPE_HTTP,
|
|
1107
|
-
serverId: runtimeServerId,
|
|
1108
|
-
...resolvedConfigPath ? { configPath: resolvedConfigPath } : {}
|
|
1109
|
-
}
|
|
1110
|
-
});
|
|
1111
|
-
let releasePort = async () => {
|
|
1112
|
-
await releasePortLease(portLease ?? null);
|
|
1113
|
-
releasePort = async () => void 0;
|
|
1114
|
-
};
|
|
1115
|
-
const runtimeRecord = createRuntimeRecord(runtimeServerId, runtimeConfig, runtimePort, shutdownToken, resolvedConfigPath);
|
|
1116
|
-
let handler;
|
|
1117
|
-
let isStopping = false;
|
|
1118
|
-
const stopHandler = async () => {
|
|
1119
|
-
if (isStopping) return;
|
|
1120
|
-
isStopping = true;
|
|
1121
|
-
try {
|
|
1122
|
-
await stopOwnedHttpTransport(handler, runtimeStateService, runtimeRecord.serverId, processLease);
|
|
1123
|
-
await releasePort();
|
|
1124
|
-
await sharedServices.dispose();
|
|
1125
|
-
process.exit(0);
|
|
1126
|
-
} catch (error) {
|
|
1127
|
-
throw new Error(`Failed to stop HTTP runtime '${runtimeRecord.serverId}' from admin shutdown: ${toErrorMessage$3(error)}`);
|
|
1128
|
-
}
|
|
1129
|
-
};
|
|
1130
|
-
try {
|
|
1131
|
-
handler = new require_src.HttpTransportHandler(() => require_src.createSessionServer(sharedServices), runtimeConfig, createHttpAdminOptions(runtimeRecord.serverId, shutdownToken, stopHandler));
|
|
1132
|
-
} catch (error) {
|
|
1133
|
-
await releasePort();
|
|
1134
|
-
await processLease.release({ kill: false });
|
|
1135
|
-
await sharedServices.dispose();
|
|
1136
|
-
throw new Error(`Failed to create HTTP runtime server: ${toErrorMessage$3(error)}`);
|
|
1137
|
-
}
|
|
1138
|
-
try {
|
|
1139
|
-
await startServer(handler, async () => {
|
|
1140
|
-
await releasePort();
|
|
1141
|
-
await sharedServices.dispose();
|
|
1142
|
-
await cleanupHttpRuntime(runtimeStateService, runtimeRecord.serverId, processLease);
|
|
1143
|
-
});
|
|
1144
|
-
await writeRuntimeRecord(runtimeStateService, runtimeRecord);
|
|
1145
|
-
} catch (error) {
|
|
1146
|
-
await releasePort();
|
|
1147
|
-
await sharedServices.dispose();
|
|
1148
|
-
await cleanupFailedRuntimeStartup(handler, runtimeStateService, runtimeRecord.serverId, processLease);
|
|
1149
|
-
throw new Error(`Failed to start HTTP runtime '${runtimeRecord.serverId}': ${toErrorMessage$3(error)}`);
|
|
1150
|
-
}
|
|
1151
|
-
console.error(`Runtime state: http://${runtimeRecord.host}:${runtimeRecord.port} (${runtimeRecord.serverId})`);
|
|
1152
|
-
}
|
|
1153
|
-
async function startStdioTransport(serverOptions) {
|
|
1154
|
-
let server;
|
|
1155
|
-
try {
|
|
1156
|
-
server = await require_src.createServer(serverOptions);
|
|
1157
|
-
await startServer(new require_src.StdioTransportHandler(server, createStdioSafeLogger()), async () => {
|
|
1158
|
-
await server?.dispose?.();
|
|
1159
|
-
});
|
|
1160
|
-
} catch (error) {
|
|
1161
|
-
await server?.dispose?.();
|
|
1162
|
-
throw new Error(`Failed to start stdio transport: ${toErrorMessage$3(error)}`);
|
|
1163
|
-
}
|
|
1164
|
-
}
|
|
1165
|
-
async function startSseTransport(serverOptions, config) {
|
|
1166
|
-
try {
|
|
1167
|
-
const requestedPort = config.port;
|
|
1168
|
-
const portRange = requestedPort !== void 0 ? {
|
|
1169
|
-
min: requestedPort,
|
|
1170
|
-
max: requestedPort
|
|
1171
|
-
} : _agimon_ai_foundation_port_registry.DEFAULT_PORT_RANGE;
|
|
1172
|
-
const portLease = await createPortRegistryLease("mcp-proxy-sse", config.host ?? DEFAULT_HOST, requestedPort, serverOptions.serverId ?? require_src.generateServerId(), TRANSPORT_TYPE_SSE, void 0, portRange);
|
|
1173
|
-
const resolvedConfig = {
|
|
1174
|
-
...config,
|
|
1175
|
-
port: portLease.port
|
|
1176
|
-
};
|
|
1177
|
-
const handler = new require_src.SseTransportHandler(await require_src.createServer(serverOptions), resolvedConfig);
|
|
1178
|
-
const shutdown = async () => {
|
|
1179
|
-
await handler.stop();
|
|
1180
|
-
await portLease.release();
|
|
1181
|
-
};
|
|
1182
|
-
process.on("SIGINT", shutdown);
|
|
1183
|
-
process.on("SIGTERM", shutdown);
|
|
1184
|
-
await startServer(handler);
|
|
1185
|
-
} catch (error) {
|
|
1186
|
-
throw new Error(`Failed to start SSE transport: ${toErrorMessage$3(error)}`);
|
|
1187
|
-
}
|
|
1188
|
-
}
|
|
1189
|
-
async function resolveStdioHttpEndpoint(config, options, resolvedConfigPath) {
|
|
1190
|
-
const repositoryPath = getRegistryRepositoryPath();
|
|
1191
|
-
if (config.port !== void 0) return { endpoint: new URL(`http://${config.host ?? DEFAULT_HOST}:${config.port}${MCP_ENDPOINT_PATH}`) };
|
|
1192
|
-
const portRegistry = createPortRegistryService();
|
|
1193
|
-
const result = await portRegistry.getPort({
|
|
1194
|
-
repositoryPath,
|
|
1195
|
-
serviceName: PORT_REGISTRY_SERVICE_HTTP,
|
|
1196
|
-
serviceType: PORT_REGISTRY_SERVICE_TYPE,
|
|
1197
|
-
environment: getRegistryEnvironment()
|
|
1198
|
-
});
|
|
1199
|
-
if (result.success && result.record) {
|
|
1200
|
-
const host = config.host ?? result.record.host;
|
|
1201
|
-
const endpoint = new URL(`http://${host}:${result.record.port}${MCP_ENDPOINT_PATH}`);
|
|
1202
|
-
try {
|
|
1203
|
-
const healthUrl = `http://${host}:${result.record.port}/health`;
|
|
1204
|
-
if ((await fetch(healthUrl)).ok) return { endpoint };
|
|
1205
|
-
} catch {}
|
|
1206
|
-
await portRegistry.releasePort({
|
|
1207
|
-
repositoryPath,
|
|
1208
|
-
serviceName: PORT_REGISTRY_SERVICE_HTTP,
|
|
1209
|
-
serviceType: PORT_REGISTRY_SERVICE_TYPE,
|
|
1210
|
-
environment: getRegistryEnvironment(),
|
|
1211
|
-
force: true
|
|
1212
|
-
});
|
|
1213
|
-
}
|
|
1214
|
-
const runtime = await prestartHttpRuntime({
|
|
1215
|
-
host: config.host ?? DEFAULT_HOST,
|
|
1216
|
-
config: options.config || resolvedConfigPath,
|
|
1217
|
-
cache: options.cache,
|
|
1218
|
-
definitionsCache: options.definitionsCache,
|
|
1219
|
-
clearDefinitionsCache: options.clearDefinitionsCache,
|
|
1220
|
-
proxyMode: options.proxyMode
|
|
1221
|
-
});
|
|
1222
|
-
return {
|
|
1223
|
-
endpoint: new URL(`http://${runtime.host}:${runtime.port}${MCP_ENDPOINT_PATH}`),
|
|
1224
|
-
ownedRuntimeServerId: runtime.reusedExistingRuntime ? void 0 : runtime.serverId
|
|
1225
|
-
};
|
|
1226
|
-
}
|
|
1227
|
-
async function rediscoverStdioHttpEndpoint(config, options, resolvedConfigPath) {
|
|
1228
|
-
const { endpoint } = await resolveStdioHttpEndpoint(config, options, resolvedConfigPath);
|
|
1229
|
-
return endpoint;
|
|
1230
|
-
}
|
|
1231
|
-
async function startStdioHttpTransport(config, options, resolvedConfigPath, proxyDefaults) {
|
|
1232
|
-
let ownedRuntimeServerId;
|
|
1233
|
-
const keepAlive = proxyDefaults?.keepAlive ?? false;
|
|
1234
|
-
try {
|
|
1235
|
-
const resolvedEndpoint = await resolveStdioHttpEndpoint(config, options, resolvedConfigPath);
|
|
1236
|
-
ownedRuntimeServerId = resolvedEndpoint.ownedRuntimeServerId;
|
|
1237
|
-
const { endpoint } = resolvedEndpoint;
|
|
1238
|
-
await startServer(new require_src.StdioHttpTransportHandler({
|
|
1239
|
-
endpoint,
|
|
1240
|
-
resolveEndpoint: async () => await rediscoverStdioHttpEndpoint(config, options, resolvedConfigPath)
|
|
1241
|
-
}, createStdioSafeLogger()), async () => {
|
|
1242
|
-
if (keepAlive || !ownedRuntimeServerId) return;
|
|
1243
|
-
await new require_src.StopServerService().stop({
|
|
1244
|
-
serverId: ownedRuntimeServerId,
|
|
1245
|
-
force: true
|
|
1246
|
-
});
|
|
1247
|
-
});
|
|
1248
|
-
} catch (error) {
|
|
1249
|
-
if (!keepAlive && ownedRuntimeServerId) {
|
|
1250
|
-
const stopServerService = new require_src.StopServerService();
|
|
1251
|
-
try {
|
|
1252
|
-
await stopServerService.stop({
|
|
1253
|
-
serverId: ownedRuntimeServerId,
|
|
1254
|
-
force: true
|
|
1255
|
-
});
|
|
1256
|
-
} catch (cleanupError) {
|
|
1257
|
-
throw new Error(`Failed to start stdio-http transport: ${toErrorMessage$3(error)}; also failed to stop owned HTTP runtime '${ownedRuntimeServerId}': ${toErrorMessage$3(cleanupError)}`);
|
|
1258
|
-
}
|
|
1259
|
-
}
|
|
1260
|
-
throw new Error(`Failed to start stdio-http transport: ${toErrorMessage$3(error)}`);
|
|
1261
|
-
}
|
|
1262
|
-
}
|
|
1263
|
-
async function startTransport(transportType, options, resolvedConfigPath, serverOptions, proxyDefaults) {
|
|
1264
|
-
try {
|
|
1265
|
-
if (transportType === TRANSPORT_TYPE_STDIO) {
|
|
1266
|
-
await startStdioTransport(serverOptions);
|
|
1267
|
-
return;
|
|
1268
|
-
}
|
|
1269
|
-
if (transportType === TRANSPORT_TYPE_HTTP) {
|
|
1270
|
-
await createAndStartHttpRuntime(serverOptions, createTransportConfig(options, require_src.TRANSPORT_MODE.HTTP, proxyDefaults), resolvedConfigPath);
|
|
1271
|
-
return;
|
|
1272
|
-
}
|
|
1273
|
-
if (transportType === TRANSPORT_TYPE_SSE) {
|
|
1274
|
-
await startSseTransport(serverOptions, createTransportConfig(options, require_src.TRANSPORT_MODE.SSE, proxyDefaults));
|
|
1275
|
-
return;
|
|
1276
|
-
}
|
|
1277
|
-
await startStdioHttpTransport(createTransportConfig(options, require_src.TRANSPORT_MODE.HTTP, proxyDefaults), options, resolvedConfigPath, proxyDefaults);
|
|
1278
|
-
} catch (error) {
|
|
1279
|
-
throw new Error(`Failed to start transport '${transportType}': ${toErrorMessage$3(error)}`);
|
|
1280
|
-
}
|
|
1281
|
-
}
|
|
1282
|
-
async function prestartHttpRuntimeCommand(options, resolvedConfigPath) {
|
|
1283
|
-
try {
|
|
1284
|
-
writePrestartHttpResult(await prestartHttpRuntime({
|
|
1285
|
-
id: options.id,
|
|
1286
|
-
host: options.host ?? DEFAULT_HOST,
|
|
1287
|
-
port: options.port,
|
|
1288
|
-
config: options.config || resolvedConfigPath,
|
|
1289
|
-
cache: options.cache,
|
|
1290
|
-
definitionsCache: options.definitionsCache,
|
|
1291
|
-
clearDefinitionsCache: options.clearDefinitionsCache,
|
|
1292
|
-
proxyMode: options.proxyMode,
|
|
1293
|
-
timeoutMs: options.timeoutMs
|
|
1294
|
-
}));
|
|
1295
|
-
} catch (error) {
|
|
1296
|
-
console.error(`Failed to prestart HTTP runtime '${options.id || "generated-server-id"}': ${toErrorMessage$3(error)}`);
|
|
1297
|
-
process.exit(1);
|
|
1298
|
-
}
|
|
1299
|
-
}
|
|
1300
|
-
/**
|
|
1301
|
-
* MCP Serve command
|
|
1302
|
-
*/
|
|
1303
|
-
const mcpServeCommand = new commander.Command("mcp-serve").description("Start MCP server with specified transport or prestart the HTTP runtime in the background").option("-t, --type <type>", `Transport type: ${TRANSPORT_TYPE_STDIO}, ${TRANSPORT_TYPE_HTTP}, ${TRANSPORT_TYPE_SSE}, or ${TRANSPORT_TYPE_STDIO_HTTP}`).option("-p, --port <port>", "Port to listen on (http/sse) or backend port for stdio-http", (val) => Number.parseInt(val, 10)).option("--host <host>", "Host to bind to (http/sse) or backend host for stdio-http").option("-c, --config <path>", "Path to MCP server configuration file").option("--no-cache", "Disable configuration caching, always reload from config file").option("--definitions-cache <path>", "Path to prefetched tool/prompt/skill definitions cache file").option("--clear-definitions-cache", "Delete definitions cache before startup", false).option("--proxy-mode <mode>", "How mcp-proxy exposes downstream tools: meta, flat, or search", "meta").option("--id <id>", "Unique server identifier (overrides config file id, auto-generated if not provided)").option("--prestart-http", "Prestart the HTTP runtime in the background and exit after it becomes healthy", false).option("--timeout-ms <ms>", "How long to wait for the HTTP runtime to become healthy", String(DEFAULT_HTTP_RUNTIME_TIMEOUT_MS)).action(async (options) => {
|
|
1304
|
-
try {
|
|
1305
|
-
if (options.prestartHttp) {
|
|
1306
|
-
await prestartHttpRuntimeCommand(options, options.config || await findConfigFileAsync() || void 0);
|
|
1307
|
-
return;
|
|
1308
|
-
}
|
|
1309
|
-
const resolvedConfigPath = options.config || await findConfigFileAsync() || void 0;
|
|
1310
|
-
const proxyDefaults = resolvedConfigPath ? loadProxyDefaults(resolvedConfigPath) : {};
|
|
1311
|
-
const transportType = validateTransportType((options.type ?? proxyDefaults.type ?? TRANSPORT_TYPE_STDIO).toLowerCase());
|
|
1312
|
-
validateProxyMode(options.proxyMode);
|
|
1313
|
-
await startTransport(transportType, options, resolvedConfigPath, createServerOptions(options, resolvedConfigPath, await resolveServerId(options, resolvedConfigPath)), proxyDefaults);
|
|
1314
|
-
} catch (error) {
|
|
1315
|
-
const rawTransportType = (options.type ?? TRANSPORT_TYPE_STDIO).toLowerCase();
|
|
1316
|
-
const transportType = isValidTransportType(rawTransportType) ? rawTransportType : TRANSPORT_TYPE_STDIO;
|
|
1317
|
-
const envPort = process.env.MCP_PORT ? Number(process.env.MCP_PORT) : void 0;
|
|
1318
|
-
const requestedPort = options.port ?? (Number.isFinite(envPort) ? envPort : void 0);
|
|
1319
|
-
console.error(formatStartError(transportType, options.host ?? DEFAULT_HOST, requestedPort, error));
|
|
1320
|
-
process.exit(1);
|
|
1321
|
-
}
|
|
1322
|
-
});
|
|
1323
|
-
//#endregion
|
|
1324
|
-
//#region src/commands/prefetch.ts
|
|
1325
|
-
/**
|
|
1326
|
-
* Prefetch Command
|
|
1327
|
-
*
|
|
1328
|
-
* DESIGN PATTERNS:
|
|
1329
|
-
* - Command pattern with Commander for CLI argument parsing
|
|
1330
|
-
* - Async/await pattern for asynchronous operations
|
|
1331
|
-
* - Error handling pattern with try-catch and proper exit codes
|
|
1332
|
-
*
|
|
1333
|
-
* CODING STANDARDS:
|
|
1334
|
-
* - Use async action handlers for asynchronous operations
|
|
1335
|
-
* - Provide clear option descriptions and default values
|
|
1336
|
-
* - Handle errors gracefully with process.exit()
|
|
1337
|
-
* - Log progress and errors to console
|
|
1338
|
-
* - Use Commander's .option() and .argument() for inputs
|
|
1339
|
-
*
|
|
1340
|
-
* AVOID:
|
|
1341
|
-
* - Synchronous blocking operations in action handlers
|
|
1342
|
-
* - Missing error handling (always use try-catch)
|
|
1343
|
-
* - Hardcoded values (use options or environment variables)
|
|
1344
|
-
* - Not exiting with appropriate exit codes on errors
|
|
1345
|
-
*/
|
|
1346
|
-
/**
|
|
1347
|
-
* Pre-download packages used by MCP servers (npx, pnpx, uvx, uv)
|
|
1348
|
-
*/
|
|
1349
|
-
const prefetchCommand = new commander.Command("prefetch").description("Pre-download packages used by MCP servers (npx, pnpx, uvx, uv)").option("-c, --config <path>", "Path to MCP server configuration file").option("-p, --parallel", "Run prefetch commands in parallel", false).option("-d, --dry-run", "Show what would be prefetched without executing", false).option("-f, --filter <type>", "Filter by package manager type: npx, pnpx, uvx, or uv").option("--definitions-out <path>", "Write discovered definitions to a JSON or YAML cache file").option("--skip-packages", "Skip package prefetch and only build definitions cache", false).option("--clear-definitions-cache", "Delete the definitions cache file before continuing", false).action(async (options) => {
|
|
1350
|
-
try {
|
|
1351
|
-
const container = require_src.createProxyIoCContainer();
|
|
1352
|
-
const configFilePath = options.config || require_src.findConfigFile();
|
|
1353
|
-
if (!configFilePath) {
|
|
1354
|
-
print.error("No MCP configuration file found.");
|
|
1355
|
-
print.info("Use --config <path> to specify a config file, or run \"mcp-proxy init\" to create one.");
|
|
1356
|
-
process.exit(1);
|
|
1357
|
-
}
|
|
1358
|
-
print.info(`Loading configuration from: ${configFilePath}`);
|
|
1359
|
-
const mcpConfig = await container.createConfigFetcherService({
|
|
1360
|
-
configFilePath,
|
|
1361
|
-
useCache: false
|
|
1362
|
-
}).fetchConfiguration(true);
|
|
1363
|
-
const serverId = mcpConfig.id || require_src.generateServerId();
|
|
1364
|
-
const configHash = require_src.DefinitionsCacheService.generateConfigHash(mcpConfig);
|
|
1365
|
-
const effectiveDefinitionsPath = options.definitionsOut || require_src.DefinitionsCacheService.getDefaultCachePath(configFilePath);
|
|
1366
|
-
const prefetchService = container.createPrefetchService({
|
|
1367
|
-
mcpConfig,
|
|
1368
|
-
filter: options.filter,
|
|
1369
|
-
parallel: options.parallel
|
|
1370
|
-
});
|
|
1371
|
-
const packages = prefetchService.extractPackages();
|
|
1372
|
-
const shouldPrefetchPackages = !options.skipPackages;
|
|
1373
|
-
const shouldWriteDefinitions = Boolean(options.definitionsOut);
|
|
1374
|
-
if (options.clearDefinitionsCache) if (options.dryRun) print.info(`Would clear definitions cache: ${effectiveDefinitionsPath}`);
|
|
1375
|
-
else {
|
|
1376
|
-
await require_src.DefinitionsCacheService.clearFile(effectiveDefinitionsPath);
|
|
1377
|
-
print.success(`Cleared definitions cache: ${effectiveDefinitionsPath}`);
|
|
1378
|
-
}
|
|
1379
|
-
if (shouldPrefetchPackages) if (packages.length === 0) {
|
|
1380
|
-
print.warning("No packages found to prefetch.");
|
|
1381
|
-
print.info("Prefetch supports: npx, pnpx, uvx, and uv run commands");
|
|
1382
|
-
} else {
|
|
1383
|
-
print.info(`Found ${packages.length} package(s) to prefetch:`);
|
|
1384
|
-
for (const pkg of packages) print.item(`${pkg.serverName}: ${pkg.packageManager} ${pkg.packageName}`);
|
|
1385
|
-
}
|
|
1386
|
-
if (!shouldPrefetchPackages && !shouldWriteDefinitions) {
|
|
1387
|
-
print.warning("Nothing to do. Use package prefetch or provide --definitions-out.");
|
|
1388
|
-
return;
|
|
1389
|
-
}
|
|
1390
|
-
if (options.dryRun) {
|
|
1391
|
-
if (shouldPrefetchPackages && packages.length > 0) {
|
|
1392
|
-
print.newline();
|
|
1393
|
-
print.header("Dry run mode - commands that would be executed:");
|
|
1394
|
-
for (const pkg of packages) print.indent(pkg.fullCommand.join(" "));
|
|
1395
|
-
}
|
|
1396
|
-
if (shouldWriteDefinitions) {
|
|
1397
|
-
print.newline();
|
|
1398
|
-
print.info(`Would write definitions cache to: ${effectiveDefinitionsPath}`);
|
|
1399
|
-
}
|
|
1400
|
-
return;
|
|
1401
|
-
}
|
|
1402
|
-
let packagePrefetchFailed = false;
|
|
1403
|
-
if (shouldPrefetchPackages && packages.length > 0) {
|
|
1404
|
-
print.newline();
|
|
1405
|
-
print.info("Prefetching packages...");
|
|
1406
|
-
const summary = await prefetchService.prefetch();
|
|
1407
|
-
print.newline();
|
|
1408
|
-
if (summary.failed === 0) print.success(`Package prefetch complete: ${summary.successful} succeeded, ${summary.failed} failed`);
|
|
1409
|
-
else print.warning(`Package prefetch complete: ${summary.successful} succeeded, ${summary.failed} failed`);
|
|
1410
|
-
if (summary.failed > 0) {
|
|
1411
|
-
packagePrefetchFailed = true;
|
|
1412
|
-
print.newline();
|
|
1413
|
-
print.error("Failed packages:");
|
|
1414
|
-
for (const result of summary.results.filter((r) => !r.success)) print.item(`${result.package.serverName} (${result.package.packageName}): ${result.output.trim()}`);
|
|
1415
|
-
}
|
|
1416
|
-
}
|
|
1417
|
-
if (shouldWriteDefinitions) {
|
|
1418
|
-
print.newline();
|
|
1419
|
-
print.info("Collecting definitions cache...");
|
|
1420
|
-
const clientManager = container.createClientManagerService();
|
|
1421
|
-
const skillPaths = mcpConfig.skills?.paths || [];
|
|
1422
|
-
const skillService = skillPaths.length > 0 ? container.createSkillService(process.cwd(), skillPaths) : void 0;
|
|
1423
|
-
const definitionsCacheService = container.createDefinitionsCacheService(clientManager, skillService);
|
|
1424
|
-
await Promise.all(Object.entries(mcpConfig.mcpServers).map(async ([serverName, serverConfig]) => {
|
|
1425
|
-
try {
|
|
1426
|
-
await clientManager.connectToServer(serverName, serverConfig);
|
|
1427
|
-
print.item(`Connected for definitions: ${serverName}`);
|
|
1428
|
-
} catch (error) {
|
|
1429
|
-
print.warning(`Failed to connect for definitions: ${serverName} (${error instanceof Error ? error.message : String(error)})`);
|
|
1430
|
-
}
|
|
1431
|
-
}));
|
|
1432
|
-
const definitionsCache = await definitionsCacheService.collectForCache({
|
|
1433
|
-
configPath: configFilePath,
|
|
1434
|
-
configHash,
|
|
1435
|
-
oneMcpVersion: require_src.version,
|
|
1436
|
-
serverId
|
|
1437
|
-
});
|
|
1438
|
-
await require_src.DefinitionsCacheService.writeToFile(effectiveDefinitionsPath, definitionsCache);
|
|
1439
|
-
print.success(`Definitions cache written: ${effectiveDefinitionsPath} (${Object.keys(definitionsCache.servers).length} servers, ${definitionsCache.skills.length} skills)`);
|
|
1440
|
-
if (definitionsCache.failures.length > 0) print.warning(`Definitions cache completed with ${definitionsCache.failures.length} server failure(s)`);
|
|
1441
|
-
await clientManager.disconnectAll();
|
|
1442
|
-
}
|
|
1443
|
-
if (packagePrefetchFailed) process.exit(1);
|
|
1444
|
-
} catch (error) {
|
|
1445
|
-
print.error(`Error executing prefetch: ${error instanceof Error ? error.message : String(error)}`);
|
|
1446
|
-
process.exit(1);
|
|
1447
|
-
}
|
|
1448
|
-
});
|
|
1449
|
-
//#endregion
|
|
1450
|
-
//#region src/commands/read-resource.ts
|
|
1451
|
-
/**
|
|
1452
|
-
* ReadResource Command
|
|
1453
|
-
*
|
|
1454
|
-
* DESIGN PATTERNS:
|
|
1455
|
-
* - Command pattern with Commander for CLI argument parsing
|
|
1456
|
-
* - Async/await pattern for asynchronous operations
|
|
1457
|
-
* - Error handling pattern with try-catch and proper exit codes
|
|
1458
|
-
*
|
|
1459
|
-
* CODING STANDARDS:
|
|
1460
|
-
* - Use async action handlers for asynchronous operations
|
|
1461
|
-
* - Provide clear option descriptions and default values
|
|
1462
|
-
* - Handle errors gracefully with process.exit()
|
|
1463
|
-
* - Log progress and errors to console
|
|
1464
|
-
* - Use Commander's .option() and .argument() for inputs
|
|
1465
|
-
*
|
|
1466
|
-
* AVOID:
|
|
1467
|
-
* - Synchronous blocking operations in action handlers
|
|
1468
|
-
* - Missing error handling (always use try-catch)
|
|
1469
|
-
* - Hardcoded values (use options or environment variables)
|
|
1470
|
-
* - Not exiting with appropriate exit codes on errors
|
|
1471
|
-
*/
|
|
1472
|
-
function toErrorMessage$2(error) {
|
|
1473
|
-
return error instanceof Error ? error.message : String(error);
|
|
1474
|
-
}
|
|
1475
|
-
/**
|
|
1476
|
-
* Read a resource by URI from a connected MCP server
|
|
1477
|
-
*/
|
|
1478
|
-
const readResourceCommand = new commander.Command("read-resource").description("Read a resource by URI from a connected MCP server").argument("<uri>", "Resource URI to read").option("-c, --config <path>", "Path to MCP server configuration file").option("-s, --server <name>", "Server name (required if resource exists on multiple servers)").option("-j, --json", "Output as JSON", false).action(async (uri, options) => {
|
|
1479
|
-
try {
|
|
1480
|
-
await withConnectedCommandContext(options, async ({ clientManager }) => {
|
|
1481
|
-
const clients = clientManager.getAllClients();
|
|
1482
|
-
if (options.server) {
|
|
1483
|
-
const client = clientManager.getClient(options.server);
|
|
1484
|
-
if (!client) throw new Error(`Server "${options.server}" not found`);
|
|
1485
|
-
if (!options.json) console.error(`Reading ${uri} from ${options.server}...`);
|
|
1486
|
-
const result = await client.readResource(uri);
|
|
1487
|
-
if (options.json) console.log(JSON.stringify(result, null, 2));
|
|
1488
|
-
else for (const content of result.contents) if ("text" in content) console.log(content.text);
|
|
1489
|
-
else console.log(JSON.stringify(content, null, 2));
|
|
1490
|
-
return;
|
|
1491
|
-
}
|
|
1492
|
-
const searchResults = await Promise.all(clients.map(async (client) => {
|
|
1493
|
-
try {
|
|
1494
|
-
const hasResource = (await client.listResources()).some((r) => r.uri === uri);
|
|
1495
|
-
return {
|
|
1496
|
-
serverName: client.serverName,
|
|
1497
|
-
hasResource,
|
|
1498
|
-
error: null
|
|
1499
|
-
};
|
|
1500
|
-
} catch (error) {
|
|
1501
|
-
return {
|
|
1502
|
-
serverName: client.serverName,
|
|
1503
|
-
hasResource: false,
|
|
1504
|
-
error
|
|
1505
|
-
};
|
|
1506
|
-
}
|
|
1507
|
-
}));
|
|
1508
|
-
const matchingServers = [];
|
|
1509
|
-
for (const { serverName, hasResource, error } of searchResults) {
|
|
1510
|
-
if (error) {
|
|
1511
|
-
console.error(`Failed to list resources from ${serverName}: ${toErrorMessage$2(error)}`);
|
|
1512
|
-
continue;
|
|
1513
|
-
}
|
|
1514
|
-
if (hasResource) matchingServers.push(serverName);
|
|
1515
|
-
}
|
|
1516
|
-
if (matchingServers.length === 0) throw new Error(`Resource "${uri}" not found on any connected server`);
|
|
1517
|
-
if (matchingServers.length > 1) throw new Error(`Resource "${uri}" found on multiple servers: ${matchingServers.join(", ")}. Use --server to disambiguate`);
|
|
1518
|
-
const targetServer = matchingServers[0];
|
|
1519
|
-
const client = clientManager.getClient(targetServer);
|
|
1520
|
-
if (!client) throw new Error(`Internal error: Server "${targetServer}" not connected`);
|
|
1521
|
-
if (!options.json) console.error(`Reading ${uri} from ${targetServer}...`);
|
|
1522
|
-
const result = await client.readResource(uri);
|
|
1523
|
-
if (options.json) console.log(JSON.stringify(result, null, 2));
|
|
1524
|
-
else for (const content of result.contents) if ("text" in content) console.log(content.text);
|
|
1525
|
-
else console.log(JSON.stringify(content, null, 2));
|
|
1526
|
-
});
|
|
1527
|
-
} catch (error) {
|
|
1528
|
-
console.error(`Error executing read-resource: ${toErrorMessage$2(error)}`);
|
|
1529
|
-
process.exit(1);
|
|
1530
|
-
}
|
|
1531
|
-
});
|
|
1532
|
-
//#endregion
|
|
1533
|
-
//#region src/commands/stop.ts
|
|
1534
|
-
/**
|
|
1535
|
-
* Stop Command
|
|
1536
|
-
*
|
|
1537
|
-
* Stops a running HTTP mcp-proxy server using the authenticated admin endpoint
|
|
1538
|
-
* and the persisted runtime registry.
|
|
1539
|
-
*/
|
|
1540
|
-
function toErrorMessage$1(error) {
|
|
1541
|
-
return error instanceof Error ? error.message : String(error);
|
|
1542
|
-
}
|
|
1543
|
-
function printStopResult(result) {
|
|
1544
|
-
console.log(`Stopped mcp-proxy server '${result.serverId}'.`);
|
|
1545
|
-
console.log(`Endpoint: http://${result.host}:${result.port}`);
|
|
1546
|
-
console.log(`Result: ${result.message}`);
|
|
1547
|
-
}
|
|
1548
|
-
/**
|
|
1549
|
-
* Stop a running HTTP mcp-proxy server.
|
|
1550
|
-
*/
|
|
1551
|
-
const stopCommand = new commander.Command("stop").description("Stop a running HTTP mcp-proxy server").option("--id <id>", "Target server ID from the runtime registry").option("--host <host>", "Target runtime host").option("--port <port>", "Target runtime port", (value) => Number.parseInt(value, 10)).option("-c, --config <path>", "Reserved for future config-based targeting support").option("--token <token>", "Override the persisted shutdown token").option("--force", "Skip server ID verification against the /health response", false).option("-j, --json", "Output as JSON", false).option("--timeout <ms>", "Maximum time to wait for shutdown completion", (value) => Number.parseInt(value, 10), 5e3).action(async (options) => {
|
|
1552
|
-
try {
|
|
1553
|
-
if (options.config) console.error("Warning: --config is not used yet; runtime resolution uses the persisted registry.");
|
|
1554
|
-
const result = await require_src.createProxyIoCContainer().createStopServerService().stop({
|
|
1555
|
-
serverId: options.id,
|
|
1556
|
-
host: options.host,
|
|
1557
|
-
port: options.port,
|
|
1558
|
-
token: options.token,
|
|
1559
|
-
force: options.force,
|
|
1560
|
-
timeoutMs: options.timeout
|
|
1561
|
-
});
|
|
1562
|
-
if (options.json) {
|
|
1563
|
-
console.log(JSON.stringify(result, null, 2));
|
|
1564
|
-
return;
|
|
1565
|
-
}
|
|
1566
|
-
printStopResult(result);
|
|
1567
|
-
} catch (error) {
|
|
1568
|
-
const errorMessage = `Error executing stop: ${toErrorMessage$1(error)}`;
|
|
1569
|
-
if (options.json) console.log(JSON.stringify({
|
|
1570
|
-
ok: false,
|
|
1571
|
-
error: errorMessage
|
|
1572
|
-
}, null, 2));
|
|
1573
|
-
else console.error(errorMessage);
|
|
1574
|
-
process.exit(1);
|
|
1575
|
-
}
|
|
1576
|
-
});
|
|
1577
|
-
//#endregion
|
|
1578
|
-
//#region src/commands/use-tool.ts
|
|
1579
|
-
/**
|
|
1580
|
-
* Use Tool Command
|
|
1581
|
-
*
|
|
1582
|
-
* DESIGN PATTERNS:
|
|
1583
|
-
* - Command pattern with Commander for CLI argument parsing
|
|
1584
|
-
* - Async/await pattern for asynchronous operations
|
|
1585
|
-
* - Error handling pattern with try-catch and proper exit codes
|
|
1586
|
-
*
|
|
1587
|
-
* CODING STANDARDS:
|
|
1588
|
-
* - Use async action handlers for asynchronous operations
|
|
1589
|
-
* - Provide clear option descriptions and default values
|
|
1590
|
-
* - Handle errors gracefully with process.exit()
|
|
1591
|
-
* - Log progress and errors to console
|
|
1592
|
-
* - Use Commander'"'"'s .option() and .argument() for inputs
|
|
1593
|
-
*
|
|
1594
|
-
* AVOID:
|
|
1595
|
-
* - Synchronous blocking operations in action handlers
|
|
1596
|
-
* - Missing error handling (always use try-catch)
|
|
1597
|
-
* - Hardcoded values (use options or environment variables)
|
|
1598
|
-
* - Not exiting with appropriate exit codes on errors
|
|
1599
|
-
*/
|
|
1600
|
-
function toErrorMessage(error) {
|
|
1601
|
-
return error instanceof Error ? error.message : String(error);
|
|
1602
|
-
}
|
|
1603
|
-
/**
|
|
1604
|
-
* Execute an MCP tool with arguments
|
|
1605
|
-
*/
|
|
1606
|
-
const useToolCommand = new commander.Command("use-tool").description("Execute an MCP tool with arguments").argument("<toolName>", "Tool name to execute").option("-c, --config <path>", "Path to MCP server configuration file").option("-s, --server <name>", "Server name (required if tool exists on multiple servers)").option("-a, --args <json>", "Tool arguments as JSON string", "{}").option("-t, --timeout <ms>", "Request timeout in milliseconds for tool execution (default: 60000)", Number.parseInt).option("-j, --json", "Output as JSON", false).action(async (toolName, options) => {
|
|
1607
|
-
try {
|
|
1608
|
-
let toolArgs = {};
|
|
1609
|
-
try {
|
|
1610
|
-
toolArgs = JSON.parse(options.args);
|
|
1611
|
-
} catch {
|
|
1612
|
-
console.error("Error: Invalid JSON in --args");
|
|
1613
|
-
process.exit(1);
|
|
1614
|
-
}
|
|
1615
|
-
await withConnectedCommandContext(options, async ({ container, config, clientManager }) => {
|
|
1616
|
-
const clients = clientManager.getAllClients();
|
|
1617
|
-
if (options.server) {
|
|
1618
|
-
const client = clientManager.getClient(options.server);
|
|
1619
|
-
if (!client) throw new Error(`Server "${options.server}" not found`);
|
|
1620
|
-
if (!options.json) console.error(`Executing ${toolName} on ${options.server}...`);
|
|
1621
|
-
const requestOptions = options.timeout ? { timeout: options.timeout } : void 0;
|
|
1622
|
-
const result = await client.callTool(toolName, toolArgs, requestOptions);
|
|
1623
|
-
if (options.json) console.log(JSON.stringify(result, null, 2));
|
|
1624
|
-
else {
|
|
1625
|
-
console.log("\nResult:");
|
|
1626
|
-
if (result.content) for (const content of result.content) if (content.type === "text") console.log(content.text);
|
|
1627
|
-
else console.log(JSON.stringify(content, null, 2));
|
|
1628
|
-
if (result.isError) {
|
|
1629
|
-
console.error("\n⚠️ Tool execution returned an error");
|
|
1630
|
-
process.exit(1);
|
|
1631
|
-
}
|
|
1632
|
-
}
|
|
1633
|
-
return;
|
|
1634
|
-
}
|
|
1635
|
-
const searchResults = await Promise.all(clients.map(async (client) => {
|
|
1636
|
-
try {
|
|
1637
|
-
const hasTool = (await client.listTools()).some((t) => t.name === toolName);
|
|
1638
|
-
return {
|
|
1639
|
-
serverName: client.serverName,
|
|
1640
|
-
hasTool,
|
|
1641
|
-
error: null
|
|
1642
|
-
};
|
|
1643
|
-
} catch (error) {
|
|
1644
|
-
return {
|
|
1645
|
-
serverName: client.serverName,
|
|
1646
|
-
hasTool: false,
|
|
1647
|
-
error
|
|
1648
|
-
};
|
|
1649
|
-
}
|
|
1650
|
-
}));
|
|
1651
|
-
const matchingServers = [];
|
|
1652
|
-
for (const { serverName, hasTool, error } of searchResults) {
|
|
1653
|
-
if (error) {
|
|
1654
|
-
if (!options.json) console.error(`Failed to list tools from ${serverName}:`, error);
|
|
1655
|
-
continue;
|
|
1656
|
-
}
|
|
1657
|
-
if (hasTool) matchingServers.push(serverName);
|
|
1658
|
-
}
|
|
1659
|
-
if (matchingServers.length === 0) {
|
|
1660
|
-
const skillPaths = config.skills?.paths || [];
|
|
1661
|
-
if (skillPaths.length > 0) try {
|
|
1662
|
-
const cwd = process.env.PROJECT_PATH || process.cwd();
|
|
1663
|
-
const skillService = container.createSkillService(cwd, skillPaths);
|
|
1664
|
-
const skillName = toolName.startsWith("skill__") ? toolName.slice(7) : toolName;
|
|
1665
|
-
const skill = await skillService.getSkill(skillName);
|
|
1666
|
-
if (skill) {
|
|
1667
|
-
const result = { content: [{
|
|
1668
|
-
type: "text",
|
|
1669
|
-
text: skill.content
|
|
1670
|
-
}] };
|
|
1671
|
-
if (options.json) console.log(JSON.stringify(result, null, 2));
|
|
1672
|
-
else {
|
|
1673
|
-
console.log("\nSkill content:");
|
|
1674
|
-
console.log(skill.content);
|
|
1675
|
-
}
|
|
1676
|
-
return;
|
|
1677
|
-
}
|
|
1678
|
-
} catch (error) {
|
|
1679
|
-
if (!options.json) console.error(`Failed to lookup skill "${toolName}":`, error);
|
|
1680
|
-
}
|
|
1681
|
-
throw new Error(`Tool or skill "${toolName}" not found on any connected server or configured skill paths`);
|
|
1682
|
-
}
|
|
1683
|
-
if (matchingServers.length > 1) throw new Error(`Tool "${toolName}" found on multiple servers: ${matchingServers.join(", ")}`);
|
|
1684
|
-
const targetServer = matchingServers[0];
|
|
1685
|
-
const client = clientManager.getClient(targetServer);
|
|
1686
|
-
if (!client) throw new Error(`Internal error: Server "${targetServer}" not connected`);
|
|
1687
|
-
if (!options.json) console.error(`Executing ${toolName} on ${targetServer}...`);
|
|
1688
|
-
const requestOptions = options.timeout ? { timeout: options.timeout } : void 0;
|
|
1689
|
-
const result = await client.callTool(toolName, toolArgs, requestOptions);
|
|
1690
|
-
if (options.json) console.log(JSON.stringify(result, null, 2));
|
|
1691
|
-
else {
|
|
1692
|
-
console.log("\nResult:");
|
|
1693
|
-
if (result.content) for (const content of result.content) if (content.type === "text") console.log(content.text);
|
|
1694
|
-
else console.log(JSON.stringify(content, null, 2));
|
|
1695
|
-
if (result.isError) {
|
|
1696
|
-
console.error("\n⚠️ Tool execution returned an error");
|
|
1697
|
-
process.exit(1);
|
|
1698
|
-
}
|
|
1699
|
-
}
|
|
1700
|
-
});
|
|
1701
|
-
} catch (error) {
|
|
1702
|
-
console.error(`Error executing use-tool: ${toErrorMessage(error)}`);
|
|
1703
|
-
process.exit(1);
|
|
1704
|
-
}
|
|
1705
|
-
});
|
|
1706
|
-
//#endregion
|
|
1707
|
-
//#region src/cli.ts
|
|
1708
|
-
/**
|
|
1709
|
-
* MCP Server Entry Point
|
|
1710
|
-
*
|
|
1711
|
-
* DESIGN PATTERNS:
|
|
1712
|
-
* - CLI pattern with Commander for argument parsing
|
|
1713
|
-
* - Command pattern for organizing CLI commands
|
|
1714
|
-
* - Transport abstraction for multiple communication methods
|
|
1715
|
-
*
|
|
1716
|
-
* CODING STANDARDS:
|
|
1717
|
-
* - Use async/await for asynchronous operations
|
|
1718
|
-
* - Handle errors gracefully with try-catch
|
|
1719
|
-
* - Log important events for debugging
|
|
1720
|
-
* - Register all commands in main entry point
|
|
1721
|
-
*
|
|
1722
|
-
* AVOID:
|
|
1723
|
-
* - Hardcoding command logic in index.ts (use separate command files)
|
|
1724
|
-
* - Missing error handling for command execution
|
|
1725
|
-
*/
|
|
1726
|
-
/**
|
|
1727
|
-
* Main entry point
|
|
1728
|
-
*/
|
|
1729
|
-
async function main() {
|
|
1730
|
-
try {
|
|
1731
|
-
const program = new commander.Command();
|
|
1732
|
-
program.name("mcp-proxy").description("MCP proxy server package").version(require_src.version);
|
|
1733
|
-
program.addCommand(initCommand);
|
|
1734
|
-
program.addCommand(mcpServeCommand);
|
|
1735
|
-
program.addCommand(searchToolsCommand);
|
|
1736
|
-
program.addCommand(describeToolsCommand);
|
|
1737
|
-
program.addCommand(useToolCommand);
|
|
1738
|
-
program.addCommand(listResourcesCommand);
|
|
1739
|
-
program.addCommand(readResourceCommand);
|
|
1740
|
-
program.addCommand(listPromptsCommand);
|
|
1741
|
-
program.addCommand(getPromptCommand);
|
|
1742
|
-
program.addCommand(prefetchCommand);
|
|
1743
|
-
program.addCommand(stopCommand);
|
|
1744
|
-
await program.parseAsync(process.argv);
|
|
1745
|
-
} catch (error) {
|
|
1746
|
-
console.error(`CLI execution failed: ${error instanceof Error ? error.message : error}`);
|
|
1747
|
-
process.exit(1);
|
|
1748
|
-
}
|
|
1749
|
-
}
|
|
1750
|
-
main().catch((error) => {
|
|
1751
|
-
console.error(`Fatal error: ${error instanceof Error ? error.message : error}`);
|
|
1752
|
-
process.exit(1);
|
|
1753
|
-
});
|
|
1754
|
-
//#endregion
|
|
2
|
+
const e=require(`./src-DjeyR393.cjs`);let t=require(`node:fs`),n=require(`node:fs/promises`),r=require(`js-yaml`);r=e.O(r);let i=require(`node:crypto`),a=require(`node:path`);a=e.O(a);let o=require(`@agimon-ai/foundation-process-registry`),s=require(`node:child_process`),c=require(`liquidjs`),l=require(`commander`),u=require(`node:url`),d=require(`@agimon-ai/foundation-port-registry`);const f=[`pnpm-workspace.yaml`,`nx.json`,`.git`],p=`localhost`;function m(e=process.env.PROJECT_PATH||process.cwd()){let n=a.default.resolve(e);for(;;){for(let e of f)if((0,t.existsSync)(a.default.join(n,e)))return n;let e=a.default.dirname(n);if(e===n)return process.cwd();n=e}}async function h(e){let t=(await new o.ProcessRegistryService(process.env.PROCESS_REGISTRY_PATH).listProcesses({repositoryPath:e,serviceName:`mcp-proxy-http`}))[0];if(!t?.host||!t?.port)return null;let n=t.metadata;return{host:t.host,port:t.port,serverId:n?.serverId??`unknown`}}async function g(e,t){try{return(await fetch(`http://${e}:${t}/health`)).ok}catch{return!1}}function _(){let e=(0,u.fileURLToPath)(require(`url`).pathToFileURL(__filename).href),n=a.default.dirname(e),r=[a.default.resolve(n,`cli.mjs`),a.default.resolve(n,`..`,`dist`,`cli.mjs`),a.default.resolve(n,`..`,`..`,`dist`,`cli.mjs`)],i=[a.default.resolve(n,`..`,`cli.ts`),a.default.resolve(n,`..`,`..`,`src`,`cli.ts`)];for(let e of r)if((0,t.existsSync)(e))return{command:process.execPath,args:[e]};for(let e of i)if((0,t.existsSync)(e))return{command:process.execPath,args:[`--import`,`tsx`,e]};throw Error(`Unable to locate mcp-proxy CLI entrypoint`)}function v(e){if(!e)return 12e4;let t=Number.parseInt(e,10);if(!Number.isInteger(t)||t<=0)throw Error(`Invalid timeout value: ${e}`);return t}async function y(e,t){let r=Date.now()+t;for(;Date.now()<r;){try{await(0,n.access)(e);return}catch{}await new Promise(e=>setTimeout(e,250))}throw Error(`Timed out waiting for runtime state file: ${e}`)}async function b(t,n){let r=new e.b,i=Date.now()+n;for(;Date.now()<i;){let e=await r.read(t);if(e){let t=`http://${e.host}:${e.port}/health`;try{let n=await fetch(t);if(n.ok){let t=await n.json().catch(()=>null);if(!t||t.transport===`http`)return{host:e.host,port:e.port}}}catch{}}await new Promise(e=>setTimeout(e,250))}throw Error(`Timed out waiting for HTTP runtime '${t}' to become healthy`)}function x(e,t,n){let{command:r,args:i}=_(),a=(0,s.spawn)(r,[...i,...e],{detached:!0,cwd:n,stdio:`ignore`,env:t});return a.unref(),a}async function ee(t,n,r,i){let a=await t.list(),o=r||p,s=a.find(e=>!!(n&&e.serverId===n||i!==void 0&&e.host===o&&e.port===i));if(!s)return;let c=new e.v(t);try{await c.stop({serverId:s.serverId,force:!0})}catch{await t.remove(s.serverId)}}async function S(t){let n=t.id||e.w(),r=v(t.timeoutMs),i=t.registryPath||t.registryDir,s=m(),c=await h(s);if(c&&await g(c.host,c.port))return{host:c.host,port:c.port,serverId:c.serverId,workspaceRoot:s,reusedExistingRuntime:!0};let l=t.port??c?.port;await ee(new e.b,t.id,t.host,l);let u={...process.env,...i?{PORT_REGISTRY_PATH:i,PROCESS_REGISTRY_PATH:(0,o.resolveSiblingRegistryPath)(i,`processes.json`)}:{}},d=x([`mcp-serve`,`--type`,`http`,`--id`,n,`--host`,t.host||p,...l===void 0?[]:[`--port`,String(l)],...t.config?[`--config`,t.config]:[],...t.cache===!1?[`--no-cache`]:[],...t.definitionsCache?[`--definitions-cache`,t.definitionsCache]:[],...t.clearDefinitionsCache?[`--clear-definitions-cache`]:[],`--proxy-mode`,t.proxyMode],u,s),f=new Promise((e,t)=>{d.once(`exit`,(e,n)=>{t(Error(`Background runtime exited before becoming healthy (code=${e??`null`}, signal=${n??`null`})`))})}),_=a.default.join(e.b.getDefaultRuntimeDir(),`${n}.runtime.json`);try{await Promise.race([y(_,r),f]);let{host:e,port:t}=await Promise.race([b(n,r),f]);return{host:e,port:t,serverId:n,workspaceRoot:s,reusedExistingRuntime:!1}}catch(e){throw Error(`Failed to prestart HTTP runtime '${n}': ${e instanceof Error?e.message:String(e)}`,{cause:e})}}function te(e){process.stdout.write(`mcp-proxy HTTP runtime ready at http://${e.host}:${e.port} (${e.serverId})\n`),process.stdout.write(`runtimeId=${e.serverId}\n`),process.stdout.write(`runtimeUrl=http://${e.host}:${e.port}\n`),process.stdout.write(`workspaceRoot=${e.workspaceRoot}\n`)}function C(e){return e instanceof Error?e.message:String(e)}async function ne(e,t){try{return(await fetch(`http://${e}:${t}/health`,{signal:AbortSignal.timeout(3e3)})).ok}catch{return!1}}async function re(e,t,n,r,i){let a=t.proxy?.host??`localhost`,o=t.proxy?.port,s=`http://${a}:${o}/mcp`;await ne(a,o)||(r.json||console.error(`Starting HTTP proxy server in background...`),await S({host:a,port:o,config:n,cache:r.useCache!==!1,clearDefinitionsCache:!1,proxyMode:`flat`}));let c=e.createClientManagerService();try{await c.connectToServer(`proxy`,{name:`proxy`,transport:`http`,config:{url:s}}),r.json||console.error(`✓ Connected to proxy at ${s}`)}catch(e){throw Error(`Failed to connect to proxy server at ${s}: ${C(e)}`)}try{return await i({container:e,configFilePath:n,config:t,clientManager:c})}finally{await c.disconnectAll()}}async function ie(e,t,n,r,i){let a=e.createClientManagerService();if(await Promise.all(Object.entries(t.mcpServers).map(async([e,t])=>{try{await a.connectToServer(e,t),r.json||console.error(`✓ Connected to ${e}`)}catch(t){r.json||console.error(`✗ Failed to connect to ${e}: ${C(t)}`)}})),a.getAllClients().length===0)throw Error(`No MCP servers connected`);try{return await i({container:e,configFilePath:n,config:t,clientManager:a})}finally{await a.disconnectAll()}}async function w(t,n){let r=e.o(),i=t.config||e.T();if(!i)throw Error(`No config file found. Use --config or create mcp-config.yaml`);let a=await r.createConfigFetcherService({configFilePath:i,useCache:t.useCache}).fetchConfiguration();return a.proxy?.port?await re(r,a,i,t,n):await ie(r,a,i,t,n)}function ae(e){return e instanceof Error?e.message:String(e)}const oe=new l.Command(`describe-tools`).description(`Describe specific MCP tools`).argument(`<toolNames...>`,`Tool names to describe`).option(`-c, --config <path>`,`Path to MCP server configuration file`).option(`-s, --server <name>`,`Filter by server name`).option(`-j, --json`,`Output as JSON`,!1).action(async(e,t)=>{try{await w(t,async({container:n,config:r,clientManager:i})=>{let a=t.server?[i.getClient(t.server)].filter(e=>e!==void 0):i.getAllClients();if(t.server&&a.length===0)throw Error(`Server "${t.server}" not found`);let o=process.env.PROJECT_PATH||process.cwd(),s=r.skills?.paths||[],c=s.length>0?n.createSkillService(o,s):void 0,l=[],u=[],d=[...e],f=await Promise.all(a.map(async e=>{try{return{client:e,tools:await e.listTools(),error:null}}catch(t){return{client:e,tools:[],error:t}}}));for(let{client:n,tools:r,error:i}of f){if(i){t.json||console.error(`Failed to list tools from ${n.serverName}:`,i);continue}for(let t of e){let e=r.find(e=>e.name===t);if(e){l.push({server:n.serverName,name:e.name,description:e.description,inputSchema:e.inputSchema});let r=d.indexOf(t);r>-1&&d.splice(r,1)}}}if(c&&d.length>0){let e=await Promise.all([...d].map(async e=>{let t=e.startsWith(`skill__`)?e.slice(7):e;return{toolName:e,skill:await c.getSkill(t)}}));for(let{toolName:t,skill:n}of e)if(n){u.push({name:n.name,location:n.basePath,instructions:n.content});let e=d.indexOf(t);e>-1&&d.splice(e,1)}}let p=[];if(l.length>0&&p.push(`For MCP tools: Use the use_tool function with toolName and toolArgs based on the inputSchema above.`),u.length>0&&p.push(`For skill, just follow skill's description to continue.`),t.json){let e={};l.length>0&&(e.tools=l),u.length>0&&(e.skills=u),p.length>0&&(e.nextSteps=p),d.length>0&&(e.notFound=d),console.log(JSON.stringify(e,null,2))}else{if(l.length>0){console.log(`
|
|
3
|
+
Found tools:
|
|
4
|
+
`);for(let e of l)console.log(`Server: ${e.server}`),console.log(`Tool: ${e.name}`),console.log(`Description: ${e.description||`No description`}`),console.log(`Input Schema:`),console.log(JSON.stringify(e.inputSchema,null,2)),console.log(``)}if(u.length>0){console.log(`
|
|
5
|
+
Found skills:
|
|
6
|
+
`);for(let e of u)console.log(`Skill: ${e.name}`),console.log(`Location: ${e.location}`),console.log(`Instructions:\n${e.instructions}`),console.log(``)}if(p.length>0){console.log(`
|
|
7
|
+
Next steps:`);for(let e of p)console.log(` • ${e}`);console.log(``)}d.length>0&&console.error(`\nTools/skills not found: ${d.join(`, `)}`),l.length===0&&u.length===0&&(console.error(`No tools or skills found`),process.exit(1))}})}catch(e){console.error(`Error executing describe-tools: ${ae(e)}`),process.exit(1)}});function T(e){return e instanceof Error?e.message:String(e)}const se=new l.Command(`get-prompt`).description(`Get a prompt by name from a connected MCP server`).argument(`<promptName>`,`Prompt name to fetch`).option(`-c, --config <path>`,`Path to MCP server configuration file`).option(`-s, --server <name>`,`Server name (required if prompt exists on multiple servers)`).option(`-a, --args <json>`,`Prompt arguments as JSON string`,`{}`).option(`-j, --json`,`Output as JSON`,!1).action(async(e,t)=>{try{let n={};try{n=JSON.parse(t.args)}catch{throw Error(`Invalid JSON in --args`)}await w(t,async({clientManager:r})=>{let i=r.getAllClients();if(t.server){let i=r.getClient(t.server);if(!i)throw Error(`Server "${t.server}" not found`);let a=await i.getPrompt(e,n);if(t.json)console.log(JSON.stringify(a,null,2));else for(let e of a.messages){let t=e.content;typeof t==`object`&&t&&`text`in t?console.log(t.text):console.log(JSON.stringify(e,null,2))}return}let a=[];if(await Promise.all(i.map(async n=>{try{(await n.listPrompts()).some(t=>t.name===e)&&a.push(n.serverName)}catch(e){t.json||console.error(`Failed to list prompts from ${n.serverName}: ${T(e)}`)}})),a.length===0)throw Error(`Prompt "${e}" not found on any connected server`);if(a.length>1)throw Error(`Prompt "${e}" found on multiple servers: ${a.join(`, `)}. Use --server to disambiguate`);let o=r.getClient(a[0]);if(!o)throw Error(`Internal error: Server "${a[0]}" not connected`);let s=await o.getPrompt(e,n);if(t.json)console.log(JSON.stringify(s,null,2));else for(let e of s.messages){let t=e.content;typeof t==`object`&&t&&`text`in t?console.log(t.text):console.log(JSON.stringify(e,null,2))}})}catch(e){console.error(`Error executing get-prompt: ${T(e)}`),process.exit(1)}});var ce=`{
|
|
8
|
+
"_comment": "MCP Server Configuration - Use \${VAR_NAME} syntax for environment variable interpolation",
|
|
9
|
+
"_instructions": "config.instruction: Server's default instruction | instruction: User override (takes precedence)",
|
|
10
|
+
"mcpServers": {
|
|
11
|
+
"example-server": {
|
|
12
|
+
"command": "node",
|
|
13
|
+
"args": ["/path/to/mcp-server/build/index.js"],
|
|
14
|
+
"env": {
|
|
15
|
+
"LOG_LEVEL": "info",
|
|
16
|
+
"_comment": "You can use environment variable interpolation:",
|
|
17
|
+
"_example_DATABASE_URL": "\${DATABASE_URL}",
|
|
18
|
+
"_example_API_KEY": "\${MY_API_KEY}"
|
|
19
|
+
},
|
|
20
|
+
"config": {
|
|
21
|
+
"instruction": "Use this server for..."
|
|
22
|
+
},
|
|
23
|
+
"_instruction_override": "Optional user override - takes precedence over config.instruction"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
`,le=`# MCP Server Configuration
|
|
28
|
+
# This file configures the MCP servers that mcp-proxy will connect to
|
|
29
|
+
#
|
|
30
|
+
# Environment Variable Interpolation:
|
|
31
|
+
# Use \${VAR_NAME} syntax to reference environment variables
|
|
32
|
+
# Example: \${HOME}, \${API_KEY}, \${DATABASE_URL}
|
|
33
|
+
#
|
|
34
|
+
# Instructions:
|
|
35
|
+
# - config.instruction: Server's default instruction (from server documentation)
|
|
36
|
+
# - instruction: User override (optional, takes precedence over config.instruction)
|
|
37
|
+
# - config.toolBlacklist: Array of tool names to hide/block from this server
|
|
38
|
+
# - config.omitToolDescription: Boolean to show only tool names without descriptions (saves tokens)
|
|
39
|
+
|
|
40
|
+
# Remote Configuration Sources (OPTIONAL)
|
|
41
|
+
# Fetch and merge configurations from remote URLs
|
|
42
|
+
# Remote configs are merged with local configs based on merge strategy
|
|
43
|
+
#
|
|
44
|
+
# SECURITY: SSRF Protection is ENABLED by default
|
|
45
|
+
# - Only HTTPS URLs are allowed (set security.enforceHttps: false to allow HTTP)
|
|
46
|
+
# - Private IPs and localhost are blocked (set security.allowPrivateIPs: true for internal networks)
|
|
47
|
+
# - Blocked ranges: 127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16
|
|
48
|
+
remoteConfigs:
|
|
49
|
+
# Example 1: Basic remote config with default security
|
|
50
|
+
# - url: \${AGIFLOW_URL}/api/v1/mcp-configs
|
|
51
|
+
# headers:
|
|
52
|
+
# Authorization: Bearer \${AGIFLOW_API_KEY}
|
|
53
|
+
# mergeStrategy: local-priority # Options: local-priority (default), remote-priority, merge-deep
|
|
54
|
+
#
|
|
55
|
+
# Example 2: Remote config with custom security settings (for internal networks)
|
|
56
|
+
# - url: \${INTERNAL_URL}/mcp-configs
|
|
57
|
+
# headers:
|
|
58
|
+
# Authorization: Bearer \${INTERNAL_TOKEN}
|
|
59
|
+
# security:
|
|
60
|
+
# allowPrivateIPs: true # Allow internal IPs (default: false)
|
|
61
|
+
# enforceHttps: false # Allow HTTP (default: true, HTTPS only)
|
|
62
|
+
# mergeStrategy: local-priority
|
|
63
|
+
#
|
|
64
|
+
# Example 3: Remote config with additional validation (OPTIONAL)
|
|
65
|
+
# - url: \${AGIFLOW_URL}/api/v1/mcp-configs
|
|
66
|
+
# headers:
|
|
67
|
+
# Authorization: Bearer \${AGIFLOW_API_KEY}
|
|
68
|
+
# X-API-Key: \${AGIFLOW_API_KEY}
|
|
69
|
+
# security:
|
|
70
|
+
# enforceHttps: true # Require HTTPS (default: true)
|
|
71
|
+
# allowPrivateIPs: false # Block private IPs (default: false)
|
|
72
|
+
# validation: # OPTIONAL: Additional regex validation on top of security checks
|
|
73
|
+
# url: ^https://.*\\.agiflow\\.io/.* # OPTIONAL: Regex pattern to validate URL format
|
|
74
|
+
# headers: # OPTIONAL: Regex patterns to validate header values
|
|
75
|
+
# Authorization: ^Bearer [A-Za-z0-9_-]+$
|
|
76
|
+
# X-API-Key: ^[A-Za-z0-9_-]{32,}$
|
|
77
|
+
# mergeStrategy: local-priority
|
|
78
|
+
|
|
79
|
+
mcpServers:
|
|
80
|
+
{%- if mcpServers %}{% for server in mcpServers %}
|
|
81
|
+
{{ server.name }}:
|
|
82
|
+
command: {{ server.command }}
|
|
83
|
+
args:{% for arg in server.args %}
|
|
84
|
+
- '{{ arg }}'{% endfor %}
|
|
85
|
+
# env:
|
|
86
|
+
# LOG_LEVEL: info
|
|
87
|
+
# # API_KEY: \${MY_API_KEY}
|
|
88
|
+
# config:
|
|
89
|
+
# instruction: Use this server for...
|
|
90
|
+
# # toolBlacklist:
|
|
91
|
+
# # - tool_to_block
|
|
92
|
+
# # omitToolDescription: true
|
|
93
|
+
{% endfor %}
|
|
94
|
+
# Example MCP server using SSE transport
|
|
95
|
+
# remote-server:
|
|
96
|
+
# url: https://example.com/mcp
|
|
97
|
+
# type: sse
|
|
98
|
+
# headers:
|
|
99
|
+
# Authorization: Bearer \${API_KEY}
|
|
100
|
+
# config:
|
|
101
|
+
# instruction: This server provides tools for...
|
|
102
|
+
{% else %}
|
|
103
|
+
# Example MCP server using stdio transport
|
|
104
|
+
example-server:
|
|
105
|
+
command: node
|
|
106
|
+
args:
|
|
107
|
+
- /path/to/mcp-server/build/index.js
|
|
108
|
+
env:
|
|
109
|
+
# Environment variables for the MCP server
|
|
110
|
+
LOG_LEVEL: info
|
|
111
|
+
# You can use environment variable interpolation:
|
|
112
|
+
# DATABASE_URL: \${DATABASE_URL}
|
|
113
|
+
# API_KEY: \${MY_API_KEY}
|
|
114
|
+
config:
|
|
115
|
+
# Server's default instruction (from server documentation)
|
|
116
|
+
instruction: Use this server for...
|
|
117
|
+
# Optional: Block specific tools from being listed or executed
|
|
118
|
+
# toolBlacklist:
|
|
119
|
+
# - dangerous_tool_name
|
|
120
|
+
# - another_blocked_tool
|
|
121
|
+
# Optional: Omit tool descriptions to save tokens (default: false)
|
|
122
|
+
# omitToolDescription: true
|
|
123
|
+
# instruction: Optional user override - takes precedence over config.instruction
|
|
124
|
+
|
|
125
|
+
# Example MCP server using SSE transport with environment variables
|
|
126
|
+
# remote-server:
|
|
127
|
+
# url: https://example.com/mcp
|
|
128
|
+
# type: sse
|
|
129
|
+
# headers:
|
|
130
|
+
# # Use \${VAR_NAME} to interpolate environment variables
|
|
131
|
+
# Authorization: Bearer \${API_KEY}
|
|
132
|
+
# config:
|
|
133
|
+
# instruction: This server provides tools for...
|
|
134
|
+
# # Optional: Block specific tools from being listed or executed
|
|
135
|
+
# # toolBlacklist:
|
|
136
|
+
# # - tool_to_block
|
|
137
|
+
# # Optional: Omit tool descriptions to save tokens (default: false)
|
|
138
|
+
# # omitToolDescription: true
|
|
139
|
+
# # instruction: Optional user override
|
|
140
|
+
{% endif %}
|
|
141
|
+
`;function E(e=``){console.log(e)}function D(e,t){console.error(t?`${e} ${t}`:e)}const O={info:e=>E(e),error:(e,t)=>D(e,t)},k={info:e=>E(e),warning:e=>E(`Warning: ${e}`),error:e=>D(e),success:e=>E(e),newline:()=>E(),header:e=>E(e),item:e=>E(`- ${e}`),indent:e=>E(` ${e}`)},ue=new l.Command(`init`).description(`Initialize MCP configuration file`).option(`-o, --output <path>`,`Output file path`,`mcp-config.yaml`).option(`--json`,`Generate JSON config instead of YAML`,!1).option(`-f, --force`,`Overwrite existing config file`,!1).option(`--mcp-servers <json>`,`JSON string of MCP servers to add to config (optional)`).action(async e=>{try{let t=(0,a.resolve)(e.output),r=!e.json&&(t.endsWith(`.yaml`)||t.endsWith(`.yml`)),i;if(r){let t=new c.Liquid,n=null;if(e.mcpServers)try{let t=JSON.parse(e.mcpServers);n=Object.entries(t).map(([e,t])=>({name:e,command:t.command,args:t.args}))}catch(e){O.error(`Failed to parse --mcp-servers JSON:`,e instanceof Error?e.message:String(e)),process.exit(1)}i=await t.parseAndRender(le,{mcpServers:n})}else i=ce;try{await(0,n.writeFile)(t,i,{encoding:`utf-8`,flag:e.force?`w`:`wx`})}catch(e){throw e&&typeof e==`object`&&`code`in e&&e.code===`EEXIST`&&(O.error(`Config file already exists: ${t}`),O.info(`Use --force to overwrite`),process.exit(1)),e}O.info(`MCP configuration file created: ${t}`),O.info(`Next steps:`),O.info(`1. Edit the configuration file to add your MCP servers`),O.info(`2. Run: mcp-proxy mcp-serve --config ${t}`)}catch(e){O.error(`Error executing init:`,e instanceof Error?e.message:String(e)),process.exit(1)}});function A(e){return e instanceof Error?e.message:String(e)}const de=new l.Command(`list-prompts`).description(`List all available prompts from connected MCP servers`).option(`-c, --config <path>`,`Path to MCP server configuration file`).option(`-s, --server <name>`,`Filter by server name`).option(`-j, --json`,`Output as JSON`,!1).action(async e=>{try{await w(e,async({clientManager:t})=>{let n=e.server?[t.getClient(e.server)].filter(e=>e!==void 0):t.getAllClients();if(e.server&&n.length===0)throw Error(`Server "${e.server}" not found`);let r={};if(await Promise.all(n.map(async t=>{try{r[t.serverName]=await t.listPrompts()}catch(n){r[t.serverName]=[],e.json||console.error(`Failed to list prompts from ${t.serverName}: ${A(n)}`)}})),e.json)console.log(JSON.stringify(r,null,2));else for(let[e,t]of Object.entries(r)){if(console.log(`\n${e}:`),t.length===0){console.log(` No prompts available`);continue}for(let e of t)if(console.log(` - ${e.name}: ${e.description||`No description`}`),e.arguments&&e.arguments.length>0){let t=e.arguments.map(e=>`${e.name}${e.required?` (required)`:``}`).join(`, `);console.log(` args: ${t}`)}}})}catch(e){console.error(`Error executing list-prompts: ${A(e)}`),process.exit(1)}});function j(e){return e instanceof Error?e.message:String(e)}const fe=new l.Command(`list-resources`).description(`List all available resources from connected MCP servers`).option(`-c, --config <path>`,`Path to MCP server configuration file`).option(`-s, --server <name>`,`Filter by server name`).option(`-j, --json`,`Output as JSON`,!1).action(async e=>{try{await w(e,async({clientManager:t})=>{let n=e.server?[t.getClient(e.server)].filter(e=>e!==void 0):t.getAllClients();if(e.server&&n.length===0)throw Error(`Server "${e.server}" not found`);let r={},i=await Promise.all(n.map(async e=>{try{let t=await e.listResources();return{serverName:e.serverName,resources:t,error:null}}catch(t){return{serverName:e.serverName,resources:[],error:t}}}));for(let{serverName:t,resources:n,error:a}of i)a&&!e.json&&console.error(`Failed to list resources from ${t}: ${j(a)}`),r[t]=n;if(e.json)console.log(JSON.stringify(r,null,2));else for(let[e,t]of Object.entries(r))if(console.log(`\n${e}:`),t.length===0)console.log(` No resources available`);else for(let e of t){let t=e.name?`${e.name} (${e.uri})`:e.uri;console.log(` - ${t}${e.description?`: ${e.description}`:``}`)}})}catch(e){console.error(`Error executing list-resources: ${j(e)}`),process.exit(1)}});function pe(e){return e instanceof Error?e.message:String(e)}function me(e){for(let t of e.servers){if(console.log(`\n${t.server}:`),t.capabilities&&t.capabilities.length>0&&console.log(` capabilities: ${t.capabilities.join(`, `)}`),t.summary&&console.log(` summary: ${t.summary}`),t.tools.length===0){console.log(` no tools`);continue}for(let e of t.tools){let t=e.capabilities&&e.capabilities.length>0?` [${e.capabilities.join(`, `)}]`:``;console.log(` - ${e.name}${t}`),e.description&&console.log(` ${e.description}`)}}}const he=new l.Command(`search-tools`).description(`Search proxied MCP tools by capability or server`).option(`-c, --config <path>`,`Path to MCP server configuration file`).option(`-s, --server <name>`,`Filter by server name`).option(`--capability <name>`,`Filter by capability tag, summary, tool name, or description`).option(`--definitions-cache <path>`,`Path to definitions cache file`).option(`-j, --json`,`Output as JSON`,!1).action(async t=>{try{await w(t,async({container:n,config:r,clientManager:i,configFilePath:a})=>{i.registerServerConfigs(r.mcpServers);let o=t.definitionsCache||e.C.getDefaultCachePath(a),s;try{s=await e.C.readFromFile(o)}catch{s=void 0}let c=n.createDefinitionsCacheService(i,void 0,{cacheData:s}),l=(await n.createSearchListToolsTool(i,c).execute({capability:t.capability,serverName:t.server})).content.find(e=>e.type===`text`),u=l?.type===`text`?JSON.parse(l.text):{servers:[]};if(t.json)console.log(JSON.stringify(u,null,2));else{if(!u.servers||u.servers.length===0)throw Error(`No tools matched the requested filters`);me(u)}})}catch(e){console.error(`Error executing search-tools: ${pe(e)}`),process.exit(1)}}),M=[`mcp-config.yaml`,`mcp-config.yml`,`mcp-config.json`],N=`/mcp`,P=`localhost`,F=`stdio`,I=`http`,L=`stdio-http`,R=`mcp-proxy-http`,z=`service`;function ge(){return m()}function B(){return ge()}function V(e){return e instanceof Error?e.message:String(e)}function H(e){return e===F||e===I||e===`sse`||e===L}function _e(e){return e===`meta`||e===`flat`||e===`search`}async function U(e){try{return await(0,n.access)(e,t.constants.F_OK),!0}catch{return!1}}async function W(){try{let e=process.env.PROJECT_PATH;if(e)for(let t of M){let n=(0,a.resolve)(e,t);if(await U(n))return n}let t=process.cwd();for(let e=0;e<=3;e++){for(let e of M){let n=(0,a.join)(t,e);if(await U(n))return n}let e=(0,a.dirname)(t);if(e===t)break;t=e}return null}catch(e){throw Error(`Failed to discover MCP config file: ${V(e)}`)}}function ve(e){try{let n=(0,t.readFileSync)(e,`utf-8`),i=(e.endsWith(`.yaml`)||e.endsWith(`.yml`)?r.default.load(n):JSON.parse(n))?.proxy;if(!i||typeof i!=`object`)return{};let a=i;return{type:typeof a.type==`string`?a.type:void 0,port:typeof a.port==`number`&&Number.isInteger(a.port)&&a.port>0?a.port:void 0,host:typeof a.host==`string`?a.host:void 0,keepAlive:typeof a.keepAlive==`boolean`?a.keepAlive:void 0}}catch{return{}}}async function ye(t,n){let r=e.o();if(t.id)return t.id;if(n)try{let e=await r.createConfigFetcherService({configFilePath:n,useCache:t.cache!==!1}).fetchConfiguration(t.cache===!1);if(e.id)return e.id}catch(e){throw Error(`Failed to resolve server ID from config '${n}': ${V(e)}`)}return e.w()}function be(e){if(!H(e))throw Error(`Unknown transport type: '${e}'. Valid options: ${F}, ${I}, sse, ${L}`);return e}function xe(e){if(!_e(e))throw Error(`Unknown proxy mode: '${e}'. Valid options: meta, flat, search`)}function G(e,t,n){let r=process.env.MCP_PORT?Number(process.env.MCP_PORT):void 0;return{mode:t,port:e.port??(Number.isFinite(r)?r:void 0)??n?.port,host:e.host??process.env.MCP_HOST??n?.host??P}}function K(){let e=(e,t)=>{if(t===void 0){console.error(e);return}console.error(e,t)};return{trace:e,debug:e,info:e,warn:e,error:e}}function Se(e,t,n){return{configFilePath:t,noCache:e.cache===!1,serverId:n,definitionsCachePath:e.definitionsCache,clearDefinitionsCache:e.clearDefinitionsCache,proxyMode:e.proxyMode}}function Ce(e,t,n,r){let i=V(r);return e===F?`Failed to start MCP server with transport '${e}': ${i}`:`Failed to start MCP server with transport '${e}' on ${n===void 0?`${t} (dynamic port)`:`${t}:${n}`}: ${i}`}function we(e,t,n,r,i){return{serverId:e,host:t.host??P,port:n,transport:`http`,shutdownToken:r,startedAt:new Date().toISOString(),pid:process.pid,configPath:i}}function q(){return new d.PortRegistryService(process.env.PORT_REGISTRY_PATH)}function J(){return process.env.NODE_ENV??`development`}async function Y(e,t,n,r,i,a,o=n===void 0?d.DEFAULT_PORT_RANGE:{min:n,max:n}){let s=q(),c=await s.reservePort({repositoryPath:B(),serviceName:e,serviceType:z,environment:J(),pid:process.pid,host:t,preferredPort:n,portRange:o,force:!0,metadata:{transport:i,serverId:r,...a?{configPath:a}:{}}});if(!c.success||!c.record){let e=n===void 0?`dynamic port`:`port ${n}`;throw Error(c.error||`Failed to reserve ${e} in port registry`)}let l=!1;return{port:c.record.port,release:async()=>{if(l)return;l=!0;let t=await s.releasePort({repositoryPath:B(),serviceName:e,serviceType:z,pid:process.pid,environment:J(),force:!0});if(!t.success&&t.error&&!t.error.includes(`No matching registry entry`))throw Error(t.error||`Failed to release port for ${e}`)}}}async function Te(e){e&&await e.release()}function Ee(e,t,n){return{serverId:e,shutdownToken:t,onShutdownRequested:n}}async function X(e,t){try{await e.remove(t)}catch(e){throw Error(`Failed to remove runtime state for '${t}': ${V(e)}`)}}async function De(e,t){try{await e.write(t)}catch(e){throw Error(`Failed to persist runtime state for '${t.serverId}': ${V(e)}`)}}async function Oe(e,t,n,r){try{try{await e.stop()}catch(e){throw Error(`Failed to stop owned HTTP transport '${n}': ${V(e)}`)}}finally{await r?.release({kill:!1}),await X(t,n)}}async function ke(e,t,n){await n?.release({kill:!1}),await X(e,t)}async function Ae(e,t,n,r){try{try{await e.stop()}catch(e){throw Error(`Failed to stop HTTP transport during cleanup for '${n}': ${V(e)}`)}}finally{await r?.release({kill:!1}),await X(t,n)}}async function Z(e,t){try{await e.start()}catch(e){throw Error(`Failed to start transport handler: ${V(e)}`)}let n=async n=>{console.error(`\nReceived ${n}, shutting down gracefully...`);try{await e.stop(),t&&await t(),process.exit(0)}catch(e){console.error(`Failed to gracefully stop transport during ${n}: ${V(e)}`),process.exit(1)}};process.on(`SIGINT`,async()=>await n(`SIGINT`)),process.on(`SIGTERM`,async()=>await n(`SIGTERM`))}async function je(t,n,r){let a=await e.u(t),s=new e.b,c=(0,i.randomUUID)(),l=t.serverId??e.w(),u=n.port,f=u===void 0?d.DEFAULT_PORT_RANGE:{min:u,max:u},p=await Y(R,n.host??P,u,l,I,r,f),m=p.port,h={...n,port:m},g=await(0,o.createProcessLease)({repositoryPath:B(),serviceName:`mcp-proxy-http`,serviceType:`service`,environment:J(),host:h.host??P,port:m,command:process.argv[1],args:process.argv.slice(2),metadata:{transport:I,serverId:l,...r?{configPath:r}:{}}}),_=async()=>{await Te(p??null),_=async()=>void 0},v=we(l,h,m,c,r),y,b=!1,x=async()=>{if(!b){b=!0;try{await Oe(y,s,v.serverId,g),await _(),await a.dispose(),process.exit(0)}catch(e){throw Error(`Failed to stop HTTP runtime '${v.serverId}' from admin shutdown: ${V(e)}`)}}};try{y=new e.m(()=>e.r(a),h,Ee(v.serverId,c,x))}catch(e){throw await _(),await g.release({kill:!1}),await a.dispose(),Error(`Failed to create HTTP runtime server: ${V(e)}`)}try{await Z(y,async()=>{await _(),await a.dispose(),await ke(s,v.serverId,g)}),await De(s,v)}catch(e){throw await _(),await a.dispose(),await Ae(y,s,v.serverId,g),Error(`Failed to start HTTP runtime '${v.serverId}': ${V(e)}`)}console.error(`Runtime state: http://${v.host}:${v.port} (${v.serverId})`)}async function Me(t){let n;try{n=await e.n(t),await Z(new e.f(n,K()),async()=>{await n?.dispose?.()})}catch(e){throw await n?.dispose?.(),Error(`Failed to start stdio transport: ${V(e)}`)}}async function Ne(t,n){try{let r=n.port,i=r===void 0?d.DEFAULT_PORT_RANGE:{min:r,max:r},a=await Y(`mcp-proxy-sse`,n.host??P,r,t.serverId??e.w(),`sse`,void 0,i),o={...n,port:a.port},s=new e.p(await e.n(t),o),c=async()=>{await s.stop(),await a.release()};process.on(`SIGINT`,c),process.on(`SIGTERM`,c),await Z(s)}catch(e){throw Error(`Failed to start SSE transport: ${V(e)}`)}}async function Q(e,t,n){let r=B();if(e.port!==void 0)return{endpoint:new URL(`http://${e.host??P}:${e.port}${N}`)};let i=q(),a=await i.getPort({repositoryPath:r,serviceName:R,serviceType:z,environment:J()});if(a.success&&a.record){let t=e.host??a.record.host,n=new URL(`http://${t}:${a.record.port}${N}`);try{let e=`http://${t}:${a.record.port}/health`;if((await fetch(e)).ok)return{endpoint:n}}catch{}await i.releasePort({repositoryPath:r,serviceName:R,serviceType:z,environment:J(),force:!0})}let o=await S({host:e.host??P,config:t.config||n,cache:t.cache,definitionsCache:t.definitionsCache,clearDefinitionsCache:t.clearDefinitionsCache,proxyMode:t.proxyMode});return{endpoint:new URL(`http://${o.host}:${o.port}${N}`),ownedRuntimeServerId:o.reusedExistingRuntime?void 0:o.serverId}}async function Pe(e,t,n){let{endpoint:r}=await Q(e,t,n);return r}async function Fe(t,n,r,i){let a,o=i?.keepAlive??!1;try{let i=await Q(t,n,r);a=i.ownedRuntimeServerId;let{endpoint:s}=i;await Z(new e.d({endpoint:s,resolveEndpoint:async()=>await Pe(t,n,r)},K()),async()=>{o||!a||await new e.v().stop({serverId:a,force:!0})})}catch(t){if(!o&&a){let n=new e.v;try{await n.stop({serverId:a,force:!0})}catch(e){throw Error(`Failed to start stdio-http transport: ${V(t)}; also failed to stop owned HTTP runtime '${a}': ${V(e)}`)}}throw Error(`Failed to start stdio-http transport: ${V(t)}`)}}async function Ie(t,n,r,i,a){try{if(t===F){await Me(i);return}if(t===I){await je(i,G(n,e.t.HTTP,a),r);return}if(t===`sse`){await Ne(i,G(n,e.t.SSE,a));return}await Fe(G(n,e.t.HTTP,a),n,r,a)}catch(e){throw Error(`Failed to start transport '${t}': ${V(e)}`)}}async function Le(e,t){try{te(await S({id:e.id,host:e.host??P,port:e.port,config:e.config||t,cache:e.cache,definitionsCache:e.definitionsCache,clearDefinitionsCache:e.clearDefinitionsCache,proxyMode:e.proxyMode,timeoutMs:e.timeoutMs}))}catch(t){console.error(`Failed to prestart HTTP runtime '${e.id||`generated-server-id`}': ${V(t)}`),process.exit(1)}}const Re=new l.Command(`mcp-serve`).description(`Start MCP server with specified transport or prestart the HTTP runtime in the background`).option(`-t, --type <type>`,`Transport type: ${F}, ${I}, sse, or ${L}`).option(`-p, --port <port>`,`Port to listen on (http/sse) or backend port for stdio-http`,e=>Number.parseInt(e,10)).option(`--host <host>`,`Host to bind to (http/sse) or backend host for stdio-http`).option(`-c, --config <path>`,`Path to MCP server configuration file`).option(`--no-cache`,`Disable configuration caching, always reload from config file`).option(`--definitions-cache <path>`,`Path to prefetched tool/prompt/skill definitions cache file`).option(`--clear-definitions-cache`,`Delete definitions cache before startup`,!1).option(`--proxy-mode <mode>`,`How mcp-proxy exposes downstream tools: meta, flat, or search`,`meta`).option(`--id <id>`,`Unique server identifier (overrides config file id, auto-generated if not provided)`).option(`--prestart-http`,`Prestart the HTTP runtime in the background and exit after it becomes healthy`,!1).option(`--timeout-ms <ms>`,`How long to wait for the HTTP runtime to become healthy`,`120000`).action(async e=>{try{if(e.prestartHttp){await Le(e,e.config||await W()||void 0);return}let t=e.config||await W()||void 0,n=t?ve(t):{},r=be((e.type??n.type??F).toLowerCase());xe(e.proxyMode),await Ie(r,e,t,Se(e,t,await ye(e,t)),n)}catch(t){let n=(e.type??F).toLowerCase(),r=H(n)?n:F,i=process.env.MCP_PORT?Number(process.env.MCP_PORT):void 0,a=e.port??(Number.isFinite(i)?i:void 0);console.error(Ce(r,e.host??P,a,t)),process.exit(1)}}),ze=new l.Command(`prefetch`).description(`Pre-download packages used by MCP servers (npx, pnpx, uvx, uv)`).option(`-c, --config <path>`,`Path to MCP server configuration file`).option(`-p, --parallel`,`Run prefetch commands in parallel`,!1).option(`-d, --dry-run`,`Show what would be prefetched without executing`,!1).option(`-f, --filter <type>`,`Filter by package manager type: npx, pnpx, uvx, or uv`).option(`--definitions-out <path>`,`Write discovered definitions to a JSON or YAML cache file`).option(`--skip-packages`,`Skip package prefetch and only build definitions cache`,!1).option(`--clear-definitions-cache`,`Delete the definitions cache file before continuing`,!1).action(async t=>{try{let n=e.o(),r=t.config||e.T();r||(k.error(`No MCP configuration file found.`),k.info(`Use --config <path> to specify a config file, or run "mcp-proxy init" to create one.`),process.exit(1)),k.info(`Loading configuration from: ${r}`);let i=await n.createConfigFetcherService({configFilePath:r,useCache:!1}).fetchConfiguration(!0),a=i.id||e.w(),o=e.C.generateConfigHash(i),s=t.definitionsOut||e.C.getDefaultCachePath(r),c=n.createPrefetchService({mcpConfig:i,filter:t.filter,parallel:t.parallel}),l=c.extractPackages(),u=!t.skipPackages,d=!!t.definitionsOut;if(t.clearDefinitionsCache&&(t.dryRun?k.info(`Would clear definitions cache: ${s}`):(await e.C.clearFile(s),k.success(`Cleared definitions cache: ${s}`))),u)if(l.length===0)k.warning(`No packages found to prefetch.`),k.info(`Prefetch supports: npx, pnpx, uvx, and uv run commands`);else{k.info(`Found ${l.length} package(s) to prefetch:`);for(let e of l)k.item(`${e.serverName}: ${e.packageManager} ${e.packageName}`)}if(!u&&!d){k.warning(`Nothing to do. Use package prefetch or provide --definitions-out.`);return}if(t.dryRun){if(u&&l.length>0){k.newline(),k.header(`Dry run mode - commands that would be executed:`);for(let e of l)k.indent(e.fullCommand.join(` `))}d&&(k.newline(),k.info(`Would write definitions cache to: ${s}`));return}let f=!1;if(u&&l.length>0){k.newline(),k.info(`Prefetching packages...`);let e=await c.prefetch();if(k.newline(),e.failed===0?k.success(`Package prefetch complete: ${e.successful} succeeded, ${e.failed} failed`):k.warning(`Package prefetch complete: ${e.successful} succeeded, ${e.failed} failed`),e.failed>0){f=!0,k.newline(),k.error(`Failed packages:`);for(let t of e.results.filter(e=>!e.success))k.item(`${t.package.serverName} (${t.package.packageName}): ${t.output.trim()}`)}}if(d){k.newline(),k.info(`Collecting definitions cache...`);let t=n.createClientManagerService(),c=i.skills?.paths||[],l=c.length>0?n.createSkillService(process.cwd(),c):void 0,u=n.createDefinitionsCacheService(t,l);await Promise.all(Object.entries(i.mcpServers).map(async([e,n])=>{try{await t.connectToServer(e,n),k.item(`Connected for definitions: ${e}`)}catch(t){k.warning(`Failed to connect for definitions: ${e} (${t instanceof Error?t.message:String(t)})`)}}));let d=await u.collectForCache({configPath:r,configHash:o,oneMcpVersion:e.D,serverId:a});await e.C.writeToFile(s,d),k.success(`Definitions cache written: ${s} (${Object.keys(d.servers).length} servers, ${d.skills.length} skills)`),d.failures.length>0&&k.warning(`Definitions cache completed with ${d.failures.length} server failure(s)`),await t.disconnectAll()}f&&process.exit(1)}catch(e){k.error(`Error executing prefetch: ${e instanceof Error?e.message:String(e)}`),process.exit(1)}});function $(e){return e instanceof Error?e.message:String(e)}const Be=new l.Command(`read-resource`).description(`Read a resource by URI from a connected MCP server`).argument(`<uri>`,`Resource URI to read`).option(`-c, --config <path>`,`Path to MCP server configuration file`).option(`-s, --server <name>`,`Server name (required if resource exists on multiple servers)`).option(`-j, --json`,`Output as JSON`,!1).action(async(e,t)=>{try{await w(t,async({clientManager:n})=>{let r=n.getAllClients();if(t.server){let r=n.getClient(t.server);if(!r)throw Error(`Server "${t.server}" not found`);t.json||console.error(`Reading ${e} from ${t.server}...`);let i=await r.readResource(e);if(t.json)console.log(JSON.stringify(i,null,2));else for(let e of i.contents)`text`in e?console.log(e.text):console.log(JSON.stringify(e,null,2));return}let i=await Promise.all(r.map(async t=>{try{let n=(await t.listResources()).some(t=>t.uri===e);return{serverName:t.serverName,hasResource:n,error:null}}catch(e){return{serverName:t.serverName,hasResource:!1,error:e}}})),a=[];for(let{serverName:e,hasResource:t,error:n}of i){if(n){console.error(`Failed to list resources from ${e}: ${$(n)}`);continue}t&&a.push(e)}if(a.length===0)throw Error(`Resource "${e}" not found on any connected server`);if(a.length>1)throw Error(`Resource "${e}" found on multiple servers: ${a.join(`, `)}. Use --server to disambiguate`);let o=a[0],s=n.getClient(o);if(!s)throw Error(`Internal error: Server "${o}" not connected`);t.json||console.error(`Reading ${e} from ${o}...`);let c=await s.readResource(e);if(t.json)console.log(JSON.stringify(c,null,2));else for(let e of c.contents)`text`in e?console.log(e.text):console.log(JSON.stringify(e,null,2))})}catch(e){console.error(`Error executing read-resource: ${$(e)}`),process.exit(1)}});function Ve(e){return e instanceof Error?e.message:String(e)}function He(e){console.log(`Stopped mcp-proxy server '${e.serverId}'.`),console.log(`Endpoint: http://${e.host}:${e.port}`),console.log(`Result: ${e.message}`)}const Ue=new l.Command(`stop`).description(`Stop a running HTTP mcp-proxy server`).option(`--id <id>`,`Target server ID from the runtime registry`).option(`--host <host>`,`Target runtime host`).option(`--port <port>`,`Target runtime port`,e=>Number.parseInt(e,10)).option(`-c, --config <path>`,`Reserved for future config-based targeting support`).option(`--token <token>`,`Override the persisted shutdown token`).option(`--force`,`Skip server ID verification against the /health response`,!1).option(`-j, --json`,`Output as JSON`,!1).option(`--timeout <ms>`,`Maximum time to wait for shutdown completion`,e=>Number.parseInt(e,10),5e3).action(async t=>{try{t.config&&console.error(`Warning: --config is not used yet; runtime resolution uses the persisted registry.`);let n=await e.o().createStopServerService().stop({serverId:t.id,host:t.host,port:t.port,token:t.token,force:t.force,timeoutMs:t.timeout});if(t.json){console.log(JSON.stringify(n,null,2));return}He(n)}catch(e){let n=`Error executing stop: ${Ve(e)}`;t.json?console.log(JSON.stringify({ok:!1,error:n},null,2)):console.error(n),process.exit(1)}});function We(e){return e instanceof Error?e.message:String(e)}const Ge=new l.Command(`use-tool`).description(`Execute an MCP tool with arguments`).argument(`<toolName>`,`Tool name to execute`).option(`-c, --config <path>`,`Path to MCP server configuration file`).option(`-s, --server <name>`,`Server name (required if tool exists on multiple servers)`).option(`-a, --args <json>`,`Tool arguments as JSON string`,`{}`).option(`-t, --timeout <ms>`,`Request timeout in milliseconds for tool execution (default: 60000)`,Number.parseInt).option(`-j, --json`,`Output as JSON`,!1).action(async(e,t)=>{try{let n={};try{n=JSON.parse(t.args)}catch{console.error(`Error: Invalid JSON in --args`),process.exit(1)}await w(t,async({container:r,config:i,clientManager:a})=>{let o=a.getAllClients();if(t.server){let r=a.getClient(t.server);if(!r)throw Error(`Server "${t.server}" not found`);t.json||console.error(`Executing ${e} on ${t.server}...`);let i=t.timeout?{timeout:t.timeout}:void 0,o=await r.callTool(e,n,i);if(t.json)console.log(JSON.stringify(o,null,2));else{if(console.log(`
|
|
142
|
+
Result:`),o.content)for(let e of o.content)e.type===`text`?console.log(e.text):console.log(JSON.stringify(e,null,2));o.isError&&(console.error(`
|
|
143
|
+
⚠️ Tool execution returned an error`),process.exit(1))}return}let s=await Promise.all(o.map(async t=>{try{let n=(await t.listTools()).some(t=>t.name===e);return{serverName:t.serverName,hasTool:n,error:null}}catch(e){return{serverName:t.serverName,hasTool:!1,error:e}}})),c=[];for(let{serverName:e,hasTool:n,error:r}of s){if(r){t.json||console.error(`Failed to list tools from ${e}:`,r);continue}n&&c.push(e)}if(c.length===0){let n=i.skills?.paths||[];if(n.length>0)try{let i=process.env.PROJECT_PATH||process.cwd(),a=r.createSkillService(i,n),o=e.startsWith(`skill__`)?e.slice(7):e,s=await a.getSkill(o);if(s){let e={content:[{type:`text`,text:s.content}]};t.json?console.log(JSON.stringify(e,null,2)):(console.log(`
|
|
144
|
+
Skill content:`),console.log(s.content));return}}catch(n){t.json||console.error(`Failed to lookup skill "${e}":`,n)}throw Error(`Tool or skill "${e}" not found on any connected server or configured skill paths`)}if(c.length>1)throw Error(`Tool "${e}" found on multiple servers: ${c.join(`, `)}`);let l=c[0],u=a.getClient(l);if(!u)throw Error(`Internal error: Server "${l}" not connected`);t.json||console.error(`Executing ${e} on ${l}...`);let d=t.timeout?{timeout:t.timeout}:void 0,f=await u.callTool(e,n,d);if(t.json)console.log(JSON.stringify(f,null,2));else{if(console.log(`
|
|
145
|
+
Result:`),f.content)for(let e of f.content)e.type===`text`?console.log(e.text):console.log(JSON.stringify(e,null,2));f.isError&&(console.error(`
|
|
146
|
+
⚠️ Tool execution returned an error`),process.exit(1))}})}catch(e){console.error(`Error executing use-tool: ${We(e)}`),process.exit(1)}});async function Ke(){try{let t=new l.Command;t.name(`mcp-proxy`).description(`MCP proxy server package`).version(e.D),t.addCommand(ue),t.addCommand(Re),t.addCommand(he),t.addCommand(oe),t.addCommand(Ge),t.addCommand(fe),t.addCommand(Be),t.addCommand(de),t.addCommand(se),t.addCommand(ze),t.addCommand(Ue),await t.parseAsync(process.argv)}catch(e){console.error(`CLI execution failed: ${e instanceof Error?e.message:e}`),process.exit(1)}}Ke().catch(e=>{console.error(`Fatal error: ${e instanceof Error?e.message:e}`),process.exit(1)});
|