@agimon-ai/mcp-proxy 0.9.0 → 0.9.1

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 CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- const require_src = require("./src-DJJH7z8i.cjs");
2
+ const require_src = require("./src-ElP1ds81.cjs");
3
3
  let node_fs = require("node:fs");
4
4
  let node_fs_promises = require("node:fs/promises");
5
5
  let js_yaml = require("js-yaml");
@@ -10,107 +10,9 @@ node_path = require_src.__toESM(node_path);
10
10
  let node_child_process = require("node:child_process");
11
11
  let liquidjs = require("liquidjs");
12
12
  let commander = require("commander");
13
- let _agimon_ai_foundation_port_registry = require("@agimon-ai/foundation-port-registry");
14
- let _agimon_ai_foundation_process_registry = require("@agimon-ai/foundation-process-registry");
15
13
  let node_url = require("node:url");
16
- //#region src/templates/mcp-config.json?raw
17
- 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";
18
- //#endregion
19
- //#region src/templates/mcp-config.yaml.liquid?raw
20
- 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";
21
- //#endregion
22
- //#region src/utils/output.ts
23
- function writeLine(message = "") {
24
- console.log(message);
25
- }
26
- function writeError(message, detail) {
27
- if (detail) console.error(`${message} ${detail}`);
28
- else console.error(message);
29
- }
30
- const log = {
31
- info: (message) => writeLine(message),
32
- error: (message, detail) => writeError(message, detail)
33
- };
34
- const print = {
35
- info: (message) => writeLine(message),
36
- warning: (message) => writeLine(`Warning: ${message}`),
37
- error: (message) => writeError(message),
38
- success: (message) => writeLine(message),
39
- newline: () => writeLine(),
40
- header: (message) => writeLine(message),
41
- item: (message) => writeLine(`- ${message}`),
42
- indent: (message) => writeLine(` ${message}`)
43
- };
44
- //#endregion
45
- //#region src/commands/init.ts
46
- /**
47
- * Init Command
48
- *
49
- * DESIGN PATTERNS:
50
- * - Command pattern with Commander for CLI argument parsing
51
- * - Async/await pattern for asynchronous operations
52
- * - Error handling pattern with try-catch and proper exit codes
53
- *
54
- * CODING STANDARDS:
55
- * - Use async action handlers for asynchronous operations
56
- * - Provide clear option descriptions and default values
57
- * - Handle errors gracefully with process.exit()
58
- * - Log progress and errors to console
59
- * - Use Commander's .option() and .argument() for inputs
60
- *
61
- * AVOID:
62
- * - Synchronous blocking operations in action handlers
63
- * - Missing error handling (always use try-catch)
64
- * - Hardcoded values (use options or environment variables)
65
- * - Not exiting with appropriate exit codes on errors
66
- */
67
- /**
68
- * Initialize MCP configuration file
69
- */
70
- 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) => {
71
- try {
72
- const outputPath = (0, node_path.resolve)(options.output);
73
- const isYaml = !options.json && (outputPath.endsWith(".yaml") || outputPath.endsWith(".yml"));
74
- let content;
75
- if (isYaml) {
76
- const liquid = new liquidjs.Liquid();
77
- let mcpServersData = null;
78
- if (options.mcpServers) try {
79
- const serversObj = JSON.parse(options.mcpServers);
80
- mcpServersData = Object.entries(serversObj).map(([name, config]) => ({
81
- name,
82
- command: config.command,
83
- args: config.args
84
- }));
85
- } catch (parseError) {
86
- log.error("Failed to parse --mcp-servers JSON:", parseError instanceof Error ? parseError.message : String(parseError));
87
- process.exit(1);
88
- }
89
- content = await liquid.parseAndRender(mcp_config_yaml_default, { mcpServers: mcpServersData });
90
- } else content = mcp_config_default;
91
- try {
92
- await (0, node_fs_promises.writeFile)(outputPath, content, {
93
- encoding: "utf-8",
94
- flag: options.force ? "w" : "wx"
95
- });
96
- } catch (error) {
97
- if (error && typeof error === "object" && "code" in error && error.code === "EEXIST") {
98
- log.error(`Config file already exists: ${outputPath}`);
99
- log.info("Use --force to overwrite");
100
- process.exit(1);
101
- }
102
- throw error;
103
- }
104
- log.info(`MCP configuration file created: ${outputPath}`);
105
- log.info("Next steps:");
106
- log.info("1. Edit the configuration file to add your MCP servers");
107
- log.info(`2. Run: mcp-proxy mcp-serve --config ${outputPath}`);
108
- } catch (error) {
109
- log.error("Error executing init:", error instanceof Error ? error.message : String(error));
110
- process.exit(1);
111
- }
112
- });
113
- //#endregion
14
+ let _agimon_ai_foundation_process_registry = require("@agimon-ai/foundation-process-registry");
15
+ let _agimon_ai_foundation_port_registry = require("@agimon-ai/foundation-port-registry");
114
16
  //#region src/commands/prestart-http.ts
115
17
  /**
116
18
  * Prestart HTTP Command
@@ -318,1188 +220,1074 @@ const prestartHttpCommand = new commander.Command("prestart-http").description("
318
220
  }
319
221
  });
320
222
  //#endregion
321
- //#region src/commands/mcp-serve.ts
322
- /**
323
- * MCP Serve Command
324
- *
325
- * DESIGN PATTERNS:
326
- * - Command pattern with Commander for CLI argument parsing
327
- * - Transport abstraction pattern for flexible deployment (stdio, HTTP, SSE)
328
- * - Factory pattern for creating transport handlers
329
- * - Graceful shutdown pattern with signal handling
330
- *
331
- * CODING STANDARDS:
332
- * - Use async/await for asynchronous operations
333
- * - Implement proper error handling with try-catch blocks
334
- * - Handle process signals for graceful shutdown
335
- * - Provide clear CLI options and help messages
336
- *
337
- * AVOID:
338
- * - Hardcoded configuration values (use CLI options or environment variables)
339
- * - Missing error handling for transport startup
340
- * - Not cleaning up resources on shutdown
341
- */
342
- const CONFIG_FILE_NAMES = [
343
- "mcp-config.yaml",
344
- "mcp-config.yml",
345
- "mcp-config.json"
346
- ];
347
- const MCP_ENDPOINT_PATH = "/mcp";
348
- const DEFAULT_HOST = "localhost";
349
- const TRANSPORT_TYPE_STDIO = "stdio";
350
- const TRANSPORT_TYPE_HTTP = "http";
351
- const TRANSPORT_TYPE_SSE = "sse";
352
- const TRANSPORT_TYPE_STDIO_HTTP = "stdio-http";
353
- const RUNTIME_TRANSPORT = TRANSPORT_TYPE_HTTP;
354
- const PORT_REGISTRY_SERVICE_HTTP = "mcp-proxy-http";
355
- const PORT_REGISTRY_SERVICE_TYPE = "service";
356
- function getWorkspaceRoot() {
357
- return resolveWorkspaceRoot();
358
- }
359
- const PROCESS_REGISTRY_SERVICE_HTTP = "mcp-proxy-http";
360
- const PROCESS_REGISTRY_SERVICE_TYPE = "service";
361
- function getRegistryRepositoryPath() {
362
- return getWorkspaceRoot();
363
- }
223
+ //#region src/commands/bootstrap.ts
364
224
  function toErrorMessage$9(error) {
365
225
  return error instanceof Error ? error.message : String(error);
366
226
  }
367
- function isValidTransportType(type) {
368
- return type === TRANSPORT_TYPE_STDIO || type === TRANSPORT_TYPE_HTTP || type === TRANSPORT_TYPE_SSE || type === TRANSPORT_TYPE_STDIO_HTTP;
369
- }
370
- function isValidProxyMode(mode) {
371
- return mode === "meta" || mode === "flat" || mode === "search";
372
- }
373
- async function pathExists(filePath) {
227
+ async function checkHealth(host, port) {
374
228
  try {
375
- await (0, node_fs_promises.access)(filePath, node_fs.constants.F_OK);
376
- return true;
229
+ return (await fetch(`http://${host}:${port}/health`, { signal: AbortSignal.timeout(3e3) })).ok;
377
230
  } catch {
378
231
  return false;
379
232
  }
380
233
  }
381
- async function findConfigFileAsync() {
234
+ /**
235
+ * Proxy mode: connect to a running HTTP server instead of downstream servers directly.
236
+ * Auto-starts the server if not running.
237
+ */
238
+ async function withProxiedContext(container, config, configFilePath, options, run) {
239
+ const host = config.proxy?.host ?? "localhost";
240
+ const port = config.proxy?.port;
241
+ const endpoint = `http://${host}:${port}/mcp`;
242
+ if (!await checkHealth(host, port)) {
243
+ if (!options.json) console.error("Starting HTTP proxy server in background...");
244
+ await prestartHttpRuntime({
245
+ host,
246
+ port,
247
+ config: configFilePath,
248
+ cache: options.useCache !== false,
249
+ clearDefinitionsCache: false,
250
+ proxyMode: "flat"
251
+ });
252
+ }
253
+ const clientManager = container.createClientManagerService();
382
254
  try {
383
- const projectPath = process.env.PROJECT_PATH;
384
- if (projectPath) for (const fileName of CONFIG_FILE_NAMES) {
385
- const configPath = (0, node_path.resolve)(projectPath, fileName);
386
- if (await pathExists(configPath)) return configPath;
387
- }
388
- const MAX_PARENT_LEVELS = 3;
389
- let searchDir = process.cwd();
390
- for (let level = 0; level <= MAX_PARENT_LEVELS; level++) {
391
- for (const fileName of CONFIG_FILE_NAMES) {
392
- const configPath = (0, node_path.join)(searchDir, fileName);
393
- if (await pathExists(configPath)) return configPath;
394
- }
395
- const parentDir = (0, node_path.dirname)(searchDir);
396
- if (parentDir === searchDir) break;
397
- searchDir = parentDir;
398
- }
399
- return null;
255
+ await clientManager.connectToServer("proxy", {
256
+ name: "proxy",
257
+ transport: "http",
258
+ config: { url: endpoint }
259
+ });
260
+ if (!options.json) console.error(`✓ Connected to proxy at ${endpoint}`);
400
261
  } catch (error) {
401
- throw new Error(`Failed to discover MCP config file: ${toErrorMessage$9(error)}`);
262
+ throw new Error(`Failed to connect to proxy server at ${endpoint}: ${toErrorMessage$9(error)}`);
402
263
  }
403
- }
404
- function loadProxyDefaults(configPath) {
405
264
  try {
406
- const content = (0, node_fs.readFileSync)(configPath, "utf-8");
407
- const proxy = (configPath.endsWith(".yaml") || configPath.endsWith(".yml") ? js_yaml.default.load(content) : JSON.parse(content))?.proxy;
408
- if (!proxy || typeof proxy !== "object") return {};
409
- const p = proxy;
410
- return {
411
- type: typeof p.type === "string" ? p.type : void 0,
412
- port: typeof p.port === "number" && Number.isInteger(p.port) && p.port > 0 ? p.port : void 0,
413
- host: typeof p.host === "string" ? p.host : void 0,
414
- keepAlive: typeof p.keepAlive === "boolean" ? p.keepAlive : void 0
415
- };
416
- } catch {
417
- return {};
265
+ return await run({
266
+ container,
267
+ configFilePath,
268
+ config,
269
+ clientManager
270
+ });
271
+ } finally {
272
+ await clientManager.disconnectAll();
418
273
  }
419
274
  }
420
- async function resolveServerId(options, resolvedConfigPath) {
421
- const container = require_src.createProxyIoCContainer();
422
- if (options.id) return options.id;
423
- if (resolvedConfigPath) try {
424
- const config = await container.createConfigFetcherService({
425
- configFilePath: resolvedConfigPath,
426
- useCache: options.cache !== false
427
- }).fetchConfiguration(options.cache === false);
428
- if (config.id) return config.id;
429
- } catch (error) {
430
- throw new Error(`Failed to resolve server ID from config '${resolvedConfigPath}': ${toErrorMessage$9(error)}`);
275
+ /**
276
+ * Direct mode: connect to all downstream MCP servers individually.
277
+ */
278
+ async function withDirectContext(container, config, configFilePath, options, run) {
279
+ const clientManager = container.createClientManagerService();
280
+ await Promise.all(Object.entries(config.mcpServers).map(async ([serverName, serverConfig]) => {
281
+ try {
282
+ await clientManager.connectToServer(serverName, serverConfig);
283
+ if (!options.json) console.error(`✓ Connected to ${serverName}`);
284
+ } catch (error) {
285
+ if (!options.json) console.error(`✗ Failed to connect to ${serverName}: ${toErrorMessage$9(error)}`);
286
+ }
287
+ }));
288
+ if (clientManager.getAllClients().length === 0) throw new Error("No MCP servers connected");
289
+ try {
290
+ return await run({
291
+ container,
292
+ configFilePath,
293
+ config,
294
+ clientManager
295
+ });
296
+ } finally {
297
+ await clientManager.disconnectAll();
431
298
  }
432
- return require_src.generateServerId();
433
299
  }
434
- function validateTransportType(type) {
435
- 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}`);
436
- return type;
437
- }
438
- function validateProxyMode(mode) {
439
- if (!isValidProxyMode(mode)) throw new Error(`Unknown proxy mode: '${mode}'. Valid options: meta, flat, search`);
440
- }
441
- function createTransportConfig(options, mode, proxyDefaults) {
442
- const envPort = process.env.MCP_PORT ? Number(process.env.MCP_PORT) : void 0;
443
- return {
444
- mode,
445
- port: options.port ?? (Number.isFinite(envPort) ? envPort : void 0) ?? proxyDefaults?.port,
446
- host: options.host ?? process.env.MCP_HOST ?? proxyDefaults?.host ?? DEFAULT_HOST
447
- };
448
- }
449
- function createStdioSafeLogger() {
450
- const logToStderr = (message, data) => {
451
- if (data === void 0) {
452
- console.error(message);
453
- return;
454
- }
455
- console.error(message, data);
456
- };
457
- return {
458
- trace: logToStderr,
459
- debug: logToStderr,
460
- info: logToStderr,
461
- warn: logToStderr,
462
- error: logToStderr
463
- };
464
- }
465
- function createServerOptions(options, resolvedConfigPath, serverId) {
466
- return {
467
- configFilePath: resolvedConfigPath,
468
- noCache: options.cache === false,
469
- serverId,
470
- definitionsCachePath: options.definitionsCache,
471
- clearDefinitionsCache: options.clearDefinitionsCache,
472
- proxyMode: options.proxyMode
473
- };
474
- }
475
- function formatStartError(type, host, port, error) {
476
- const startErrorMessage = toErrorMessage$9(error);
477
- if (type === TRANSPORT_TYPE_STDIO) return `Failed to start MCP server with transport '${type}': ${startErrorMessage}`;
478
- return `Failed to start MCP server with transport '${type}' on ${port === void 0 ? `${host} (dynamic port)` : `${host}:${port}`}: ${startErrorMessage}`;
479
- }
480
- function createRuntimeRecord(serverId, config, port, shutdownToken, configPath) {
481
- return {
482
- serverId,
483
- host: config.host ?? DEFAULT_HOST,
484
- port,
485
- transport: RUNTIME_TRANSPORT,
486
- shutdownToken,
487
- startedAt: (/* @__PURE__ */ new Date()).toISOString(),
488
- pid: process.pid,
489
- configPath
490
- };
491
- }
492
- function createPortRegistryService() {
493
- return new _agimon_ai_foundation_port_registry.PortRegistryService(process.env.PORT_REGISTRY_PATH);
494
- }
495
- function getRegistryEnvironment() {
496
- return process.env.NODE_ENV ?? "development";
497
- }
498
- async function createPortRegistryLease(serviceName, host, preferredPort, serverId, transport, configPath, portRange = preferredPort !== void 0 ? {
499
- min: preferredPort,
500
- max: preferredPort
501
- } : _agimon_ai_foundation_port_registry.DEFAULT_PORT_RANGE) {
502
- const portRegistry = createPortRegistryService();
503
- const result = await portRegistry.reservePort({
504
- repositoryPath: getRegistryRepositoryPath(),
505
- serviceName,
506
- serviceType: PORT_REGISTRY_SERVICE_TYPE,
507
- environment: getRegistryEnvironment(),
508
- pid: process.pid,
509
- host,
510
- preferredPort,
511
- portRange,
512
- force: true,
513
- metadata: {
514
- transport,
515
- serverId,
516
- ...configPath ? { configPath } : {}
517
- }
518
- });
519
- if (!result.success || !result.record) {
520
- const requestedPortLabel = preferredPort === void 0 ? "dynamic port" : `port ${preferredPort}`;
521
- throw new Error(result.error || `Failed to reserve ${requestedPortLabel} in port registry`);
522
- }
523
- let released = false;
524
- return {
525
- port: result.record.port,
526
- release: async () => {
527
- if (released) return;
528
- released = true;
529
- const releaseResult = await portRegistry.releasePort({
530
- repositoryPath: getRegistryRepositoryPath(),
531
- serviceName,
532
- serviceType: PORT_REGISTRY_SERVICE_TYPE,
533
- pid: process.pid,
534
- environment: getRegistryEnvironment(),
535
- force: true
536
- });
537
- if (!releaseResult.success && releaseResult.error && !releaseResult.error.includes("No matching registry entry")) throw new Error(releaseResult.error || `Failed to release port for ${serviceName}`);
538
- }
539
- };
540
- }
541
- async function releasePortLease(lease) {
542
- if (!lease) return;
543
- await lease.release();
544
- }
545
- function createHttpAdminOptions(serverId, shutdownToken, onShutdownRequested) {
546
- return {
547
- serverId,
548
- shutdownToken,
549
- onShutdownRequested
550
- };
551
- }
552
- async function removeRuntimeRecord(runtimeStateService, serverId) {
553
- try {
554
- await runtimeStateService.remove(serverId);
555
- } catch (error) {
556
- throw new Error(`Failed to remove runtime state for '${serverId}': ${toErrorMessage$9(error)}`);
557
- }
558
- }
559
- async function writeRuntimeRecord(runtimeStateService, record) {
560
- try {
561
- await runtimeStateService.write(record);
562
- } catch (error) {
563
- throw new Error(`Failed to persist runtime state for '${record.serverId}': ${toErrorMessage$9(error)}`);
564
- }
565
- }
566
- async function stopOwnedHttpTransport(handler, runtimeStateService, serverId, processLease) {
567
- try {
568
- try {
569
- await handler.stop();
570
- } catch (error) {
571
- throw new Error(`Failed to stop owned HTTP transport '${serverId}': ${toErrorMessage$9(error)}`);
572
- }
573
- } finally {
574
- await processLease?.release({ kill: false });
575
- await removeRuntimeRecord(runtimeStateService, serverId);
576
- }
300
+ async function withConnectedCommandContext(options, run) {
301
+ const container = require_src.createProxyIoCContainer();
302
+ const configFilePath = options.config || require_src.findConfigFile();
303
+ if (!configFilePath) throw new Error("No config file found. Use --config or create mcp-config.yaml");
304
+ const config = await container.createConfigFetcherService({
305
+ configFilePath,
306
+ useCache: options.useCache
307
+ }).fetchConfiguration();
308
+ if (config.proxy?.port) return await withProxiedContext(container, config, configFilePath, options, run);
309
+ return await withDirectContext(container, config, configFilePath, options, run);
577
310
  }
311
+ //#endregion
312
+ //#region src/commands/describe-tools.ts
578
313
  /**
579
- * Run post-stop cleanup for an HTTP runtime (release port, dispose services, remove state).
580
- * This is the subset of stopOwnedHttpTransport that runs AFTER handler.stop() has already
581
- * been called by startServer()'s signal handler — avoids double-stopping the transport.
314
+ * Describe Tools Command
315
+ *
316
+ * DESIGN PATTERNS:
317
+ * - Command pattern with Commander for CLI argument parsing
318
+ * - Async/await pattern for asynchronous operations
319
+ * - Error handling pattern with try-catch and proper exit codes
320
+ *
321
+ * CODING STANDARDS:
322
+ * - Use async action handlers for asynchronous operations
323
+ * - Provide clear option descriptions and default values
324
+ * - Handle errors gracefully with process.exit()
325
+ * - Log progress and errors to console
326
+ * - Use Commander's .option() and .argument() for inputs
327
+ *
328
+ * AVOID:
329
+ * - Synchronous blocking operations in action handlers
330
+ * - Missing error handling (always use try-catch)
331
+ * - Hardcoded values (use options or environment variables)
332
+ * - Not exiting with appropriate exit codes on errors
582
333
  */
583
- async function cleanupHttpRuntime(runtimeStateService, serverId, processLease) {
584
- await processLease?.release({ kill: false });
585
- await removeRuntimeRecord(runtimeStateService, serverId);
586
- }
587
- async function cleanupFailedRuntimeStartup(handler, runtimeStateService, serverId, processLease) {
588
- try {
589
- try {
590
- await handler.stop();
591
- } catch (error) {
592
- throw new Error(`Failed to stop HTTP transport during cleanup for '${serverId}': ${toErrorMessage$9(error)}`);
593
- }
594
- } finally {
595
- await processLease?.release({ kill: false });
596
- await removeRuntimeRecord(runtimeStateService, serverId);
597
- }
334
+ function toErrorMessage$8(error) {
335
+ return error instanceof Error ? error.message : String(error);
598
336
  }
599
337
  /**
600
- * Start MCP server with given transport handler
601
- * @param handler - The transport handler to start
602
- * @param onStopped - Optional cleanup callback run after signal-based shutdown
338
+ * Describe specific MCP tools
603
339
  */
604
- async function startServer(handler, onStopped) {
340
+ 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) => {
605
341
  try {
606
- await handler.start();
342
+ await withConnectedCommandContext(options, async ({ container, config, clientManager }) => {
343
+ const clients = options.server ? [clientManager.getClient(options.server)].filter((client) => client !== void 0) : clientManager.getAllClients();
344
+ if (options.server && clients.length === 0) throw new Error(`Server "${options.server}" not found`);
345
+ const cwd = process.env.PROJECT_PATH || process.cwd();
346
+ const skillPaths = config.skills?.paths || [];
347
+ const skillService = skillPaths.length > 0 ? container.createSkillService(cwd, skillPaths) : void 0;
348
+ const foundTools = [];
349
+ const foundSkills = [];
350
+ const notFoundTools = [...toolNames];
351
+ const toolResults = await Promise.all(clients.map(async (client) => {
352
+ try {
353
+ return {
354
+ client,
355
+ tools: await client.listTools(),
356
+ error: null
357
+ };
358
+ } catch (error) {
359
+ return {
360
+ client,
361
+ tools: [],
362
+ error
363
+ };
364
+ }
365
+ }));
366
+ for (const { client, tools, error } of toolResults) {
367
+ if (error) {
368
+ if (!options.json) console.error(`Failed to list tools from ${client.serverName}:`, error);
369
+ continue;
370
+ }
371
+ for (const toolName of toolNames) {
372
+ const tool = tools.find((entry) => entry.name === toolName);
373
+ if (tool) {
374
+ foundTools.push({
375
+ server: client.serverName,
376
+ name: tool.name,
377
+ description: tool.description,
378
+ inputSchema: tool.inputSchema
379
+ });
380
+ const idx = notFoundTools.indexOf(toolName);
381
+ if (idx > -1) notFoundTools.splice(idx, 1);
382
+ }
383
+ }
384
+ }
385
+ if (skillService && notFoundTools.length > 0) {
386
+ const skillResults = await Promise.all([...notFoundTools].map(async (toolName) => {
387
+ const skillName = toolName.startsWith("skill__") ? toolName.slice(7) : toolName;
388
+ return {
389
+ toolName,
390
+ skill: await skillService.getSkill(skillName)
391
+ };
392
+ }));
393
+ for (const { toolName, skill } of skillResults) if (skill) {
394
+ foundSkills.push({
395
+ name: skill.name,
396
+ location: skill.basePath,
397
+ instructions: skill.content
398
+ });
399
+ const idx = notFoundTools.indexOf(toolName);
400
+ if (idx > -1) notFoundTools.splice(idx, 1);
401
+ }
402
+ }
403
+ const nextSteps = [];
404
+ if (foundTools.length > 0) nextSteps.push("For MCP tools: Use the use_tool function with toolName and toolArgs based on the inputSchema above.");
405
+ if (foundSkills.length > 0) nextSteps.push(`For skill, just follow skill's description to continue.`);
406
+ if (options.json) {
407
+ const result = {};
408
+ if (foundTools.length > 0) result.tools = foundTools;
409
+ if (foundSkills.length > 0) result.skills = foundSkills;
410
+ if (nextSteps.length > 0) result.nextSteps = nextSteps;
411
+ if (notFoundTools.length > 0) result.notFound = notFoundTools;
412
+ console.log(JSON.stringify(result, null, 2));
413
+ } else {
414
+ if (foundTools.length > 0) {
415
+ console.log("\nFound tools:\n");
416
+ for (const tool of foundTools) {
417
+ console.log(`Server: ${tool.server}`);
418
+ console.log(`Tool: ${tool.name}`);
419
+ console.log(`Description: ${tool.description || "No description"}`);
420
+ console.log("Input Schema:");
421
+ console.log(JSON.stringify(tool.inputSchema, null, 2));
422
+ console.log("");
423
+ }
424
+ }
425
+ if (foundSkills.length > 0) {
426
+ console.log("\nFound skills:\n");
427
+ for (const skill of foundSkills) {
428
+ console.log(`Skill: ${skill.name}`);
429
+ console.log(`Location: ${skill.location}`);
430
+ console.log(`Instructions:\n${skill.instructions}`);
431
+ console.log("");
432
+ }
433
+ }
434
+ if (nextSteps.length > 0) {
435
+ console.log("\nNext steps:");
436
+ for (const step of nextSteps) console.log(` • ${step}`);
437
+ console.log("");
438
+ }
439
+ if (notFoundTools.length > 0) console.error(`\nTools/skills not found: ${notFoundTools.join(", ")}`);
440
+ if (foundTools.length === 0 && foundSkills.length === 0) {
441
+ console.error("No tools or skills found");
442
+ process.exit(1);
443
+ }
444
+ }
445
+ });
607
446
  } catch (error) {
608
- throw new Error(`Failed to start transport handler: ${toErrorMessage$9(error)}`);
447
+ console.error(`Error executing describe-tools: ${toErrorMessage$8(error)}`);
448
+ process.exit(1);
609
449
  }
610
- const shutdown = async (signal) => {
611
- console.error(`\nReceived ${signal}, shutting down gracefully...`);
612
- try {
613
- await handler.stop();
614
- if (onStopped) await onStopped();
615
- process.exit(0);
616
- } catch (error) {
617
- console.error(`Failed to gracefully stop transport during ${signal}: ${toErrorMessage$9(error)}`);
618
- process.exit(1);
619
- }
620
- };
621
- process.on("SIGINT", async () => await shutdown("SIGINT"));
622
- process.on("SIGTERM", async () => await shutdown("SIGTERM"));
450
+ });
451
+ //#endregion
452
+ //#region src/commands/get-prompt.ts
453
+ function toErrorMessage$7(error) {
454
+ return error instanceof Error ? error.message : String(error);
623
455
  }
624
- async function createAndStartHttpRuntime(serverOptions, config, resolvedConfigPath) {
625
- const sharedServices = await require_src.initializeSharedServices(serverOptions);
626
- const runtimeStateService = new require_src.RuntimeStateService();
627
- const shutdownToken = (0, node_crypto.randomUUID)();
628
- const runtimeServerId = serverOptions.serverId ?? require_src.generateServerId();
629
- const requestedPort = config.port;
630
- const portRange = requestedPort !== void 0 ? {
631
- min: requestedPort,
632
- max: requestedPort
633
- } : _agimon_ai_foundation_port_registry.DEFAULT_PORT_RANGE;
634
- const portLease = await createPortRegistryLease(PORT_REGISTRY_SERVICE_HTTP, config.host ?? DEFAULT_HOST, requestedPort, runtimeServerId, TRANSPORT_TYPE_HTTP, resolvedConfigPath, portRange);
635
- const runtimePort = portLease.port;
636
- const runtimeConfig = {
637
- ...config,
638
- port: runtimePort
639
- };
640
- const processLease = await (0, _agimon_ai_foundation_process_registry.createProcessLease)({
641
- repositoryPath: getRegistryRepositoryPath(),
642
- serviceName: PROCESS_REGISTRY_SERVICE_HTTP,
643
- serviceType: PROCESS_REGISTRY_SERVICE_TYPE,
644
- environment: getRegistryEnvironment(),
645
- host: runtimeConfig.host ?? DEFAULT_HOST,
646
- port: runtimePort,
647
- command: process.argv[1],
648
- args: process.argv.slice(2),
649
- metadata: {
650
- transport: TRANSPORT_TYPE_HTTP,
651
- serverId: runtimeServerId,
652
- ...resolvedConfigPath ? { configPath: resolvedConfigPath } : {}
653
- }
654
- });
655
- let releasePort = async () => {
656
- await releasePortLease(portLease ?? null);
657
- releasePort = async () => void 0;
658
- };
659
- const runtimeRecord = createRuntimeRecord(runtimeServerId, runtimeConfig, runtimePort, shutdownToken, resolvedConfigPath);
660
- let handler;
661
- let isStopping = false;
662
- const stopHandler = async () => {
663
- if (isStopping) return;
664
- isStopping = true;
456
+ 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) => {
457
+ try {
458
+ let promptArgs = {};
665
459
  try {
666
- await stopOwnedHttpTransport(handler, runtimeStateService, runtimeRecord.serverId, processLease);
667
- await releasePort();
668
- await sharedServices.dispose();
669
- process.exit(0);
670
- } catch (error) {
671
- throw new Error(`Failed to stop HTTP runtime '${runtimeRecord.serverId}' from admin shutdown: ${toErrorMessage$9(error)}`);
460
+ promptArgs = JSON.parse(options.args);
461
+ } catch {
462
+ throw new Error("Invalid JSON in --args");
672
463
  }
673
- };
674
- try {
675
- handler = new require_src.HttpTransportHandler(() => require_src.createSessionServer(sharedServices), runtimeConfig, createHttpAdminOptions(runtimeRecord.serverId, shutdownToken, stopHandler));
676
- } catch (error) {
677
- await releasePort();
678
- await processLease.release({ kill: false });
679
- await sharedServices.dispose();
680
- throw new Error(`Failed to create HTTP runtime server: ${toErrorMessage$9(error)}`);
681
- }
682
- try {
683
- await startServer(handler, async () => {
684
- await releasePort();
685
- await sharedServices.dispose();
686
- await cleanupHttpRuntime(runtimeStateService, runtimeRecord.serverId, processLease);
464
+ await withConnectedCommandContext(options, async ({ clientManager }) => {
465
+ const clients = clientManager.getAllClients();
466
+ if (options.server) {
467
+ const client = clientManager.getClient(options.server);
468
+ if (!client) throw new Error(`Server "${options.server}" not found`);
469
+ const prompt = await client.getPrompt(promptName, promptArgs);
470
+ if (options.json) console.log(JSON.stringify(prompt, null, 2));
471
+ else for (const message of prompt.messages) {
472
+ const content = message.content;
473
+ if (typeof content === "object" && content && "text" in content) console.log(content.text);
474
+ else console.log(JSON.stringify(message, null, 2));
475
+ }
476
+ return;
477
+ }
478
+ const matchingServers = [];
479
+ await Promise.all(clients.map(async (client) => {
480
+ try {
481
+ if ((await client.listPrompts()).some((prompt) => prompt.name === promptName)) matchingServers.push(client.serverName);
482
+ } catch (error) {
483
+ if (!options.json) console.error(`Failed to list prompts from ${client.serverName}: ${toErrorMessage$7(error)}`);
484
+ }
485
+ }));
486
+ if (matchingServers.length === 0) throw new Error(`Prompt "${promptName}" not found on any connected server`);
487
+ if (matchingServers.length > 1) throw new Error(`Prompt "${promptName}" found on multiple servers: ${matchingServers.join(", ")}. Use --server to disambiguate`);
488
+ const client = clientManager.getClient(matchingServers[0]);
489
+ if (!client) throw new Error(`Internal error: Server "${matchingServers[0]}" not connected`);
490
+ const prompt = await client.getPrompt(promptName, promptArgs);
491
+ if (options.json) console.log(JSON.stringify(prompt, null, 2));
492
+ else for (const message of prompt.messages) {
493
+ const content = message.content;
494
+ if (typeof content === "object" && content && "text" in content) console.log(content.text);
495
+ else console.log(JSON.stringify(message, null, 2));
496
+ }
687
497
  });
688
- await writeRuntimeRecord(runtimeStateService, runtimeRecord);
689
498
  } catch (error) {
690
- await releasePort();
691
- await sharedServices.dispose();
692
- await cleanupFailedRuntimeStartup(handler, runtimeStateService, runtimeRecord.serverId, processLease);
693
- throw new Error(`Failed to start HTTP runtime '${runtimeRecord.serverId}': ${toErrorMessage$9(error)}`);
499
+ console.error(`Error executing get-prompt: ${toErrorMessage$7(error)}`);
500
+ process.exit(1);
694
501
  }
695
- console.error(`Runtime state: http://${runtimeRecord.host}:${runtimeRecord.port} (${runtimeRecord.serverId})`);
502
+ });
503
+ //#endregion
504
+ //#region src/templates/mcp-config.json?raw
505
+ 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";
506
+ //#endregion
507
+ //#region src/templates/mcp-config.yaml.liquid?raw
508
+ 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";
509
+ //#endregion
510
+ //#region src/utils/output.ts
511
+ function writeLine(message = "") {
512
+ console.log(message);
696
513
  }
697
- async function startStdioTransport(serverOptions) {
514
+ function writeError(message, detail) {
515
+ if (detail) console.error(`${message} ${detail}`);
516
+ else console.error(message);
517
+ }
518
+ const log = {
519
+ info: (message) => writeLine(message),
520
+ error: (message, detail) => writeError(message, detail)
521
+ };
522
+ const print = {
523
+ info: (message) => writeLine(message),
524
+ warning: (message) => writeLine(`Warning: ${message}`),
525
+ error: (message) => writeError(message),
526
+ success: (message) => writeLine(message),
527
+ newline: () => writeLine(),
528
+ header: (message) => writeLine(message),
529
+ item: (message) => writeLine(`- ${message}`),
530
+ indent: (message) => writeLine(` ${message}`)
531
+ };
532
+ //#endregion
533
+ //#region src/commands/init.ts
534
+ /**
535
+ * Init Command
536
+ *
537
+ * DESIGN PATTERNS:
538
+ * - Command pattern with Commander for CLI argument parsing
539
+ * - Async/await pattern for asynchronous operations
540
+ * - Error handling pattern with try-catch and proper exit codes
541
+ *
542
+ * CODING STANDARDS:
543
+ * - Use async action handlers for asynchronous operations
544
+ * - Provide clear option descriptions and default values
545
+ * - Handle errors gracefully with process.exit()
546
+ * - Log progress and errors to console
547
+ * - Use Commander's .option() and .argument() for inputs
548
+ *
549
+ * AVOID:
550
+ * - Synchronous blocking operations in action handlers
551
+ * - Missing error handling (always use try-catch)
552
+ * - Hardcoded values (use options or environment variables)
553
+ * - Not exiting with appropriate exit codes on errors
554
+ */
555
+ /**
556
+ * Initialize MCP configuration file
557
+ */
558
+ 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) => {
698
559
  try {
699
- await startServer(new require_src.StdioTransportHandler(await require_src.createServer(serverOptions), createStdioSafeLogger()));
560
+ const outputPath = (0, node_path.resolve)(options.output);
561
+ const isYaml = !options.json && (outputPath.endsWith(".yaml") || outputPath.endsWith(".yml"));
562
+ let content;
563
+ if (isYaml) {
564
+ const liquid = new liquidjs.Liquid();
565
+ let mcpServersData = null;
566
+ if (options.mcpServers) try {
567
+ const serversObj = JSON.parse(options.mcpServers);
568
+ mcpServersData = Object.entries(serversObj).map(([name, config]) => ({
569
+ name,
570
+ command: config.command,
571
+ args: config.args
572
+ }));
573
+ } catch (parseError) {
574
+ log.error("Failed to parse --mcp-servers JSON:", parseError instanceof Error ? parseError.message : String(parseError));
575
+ process.exit(1);
576
+ }
577
+ content = await liquid.parseAndRender(mcp_config_yaml_default, { mcpServers: mcpServersData });
578
+ } else content = mcp_config_default;
579
+ try {
580
+ await (0, node_fs_promises.writeFile)(outputPath, content, {
581
+ encoding: "utf-8",
582
+ flag: options.force ? "w" : "wx"
583
+ });
584
+ } catch (error) {
585
+ if (error && typeof error === "object" && "code" in error && error.code === "EEXIST") {
586
+ log.error(`Config file already exists: ${outputPath}`);
587
+ log.info("Use --force to overwrite");
588
+ process.exit(1);
589
+ }
590
+ throw error;
591
+ }
592
+ log.info(`MCP configuration file created: ${outputPath}`);
593
+ log.info("Next steps:");
594
+ log.info("1. Edit the configuration file to add your MCP servers");
595
+ log.info(`2. Run: mcp-proxy mcp-serve --config ${outputPath}`);
700
596
  } catch (error) {
701
- throw new Error(`Failed to start stdio transport: ${toErrorMessage$9(error)}`);
597
+ log.error("Error executing init:", error instanceof Error ? error.message : String(error));
598
+ process.exit(1);
702
599
  }
600
+ });
601
+ //#endregion
602
+ //#region src/commands/list-prompts.ts
603
+ function toErrorMessage$6(error) {
604
+ return error instanceof Error ? error.message : String(error);
703
605
  }
704
- async function startSseTransport(serverOptions, config) {
606
+ 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) => {
705
607
  try {
706
- const requestedPort = config.port;
707
- const portRange = requestedPort !== void 0 ? {
708
- min: requestedPort,
709
- max: requestedPort
710
- } : _agimon_ai_foundation_port_registry.DEFAULT_PORT_RANGE;
711
- const portLease = await createPortRegistryLease("mcp-proxy-sse", config.host ?? DEFAULT_HOST, requestedPort, serverOptions.serverId ?? require_src.generateServerId(), TRANSPORT_TYPE_SSE, void 0, portRange);
712
- const resolvedConfig = {
713
- ...config,
714
- port: portLease.port
715
- };
716
- const handler = new require_src.SseTransportHandler(await require_src.createServer(serverOptions), resolvedConfig);
717
- const shutdown = async () => {
718
- await handler.stop();
719
- await portLease.release();
720
- };
721
- process.on("SIGINT", shutdown);
722
- process.on("SIGTERM", shutdown);
723
- await startServer(handler);
724
- } catch (error) {
725
- throw new Error(`Failed to start SSE transport: ${toErrorMessage$9(error)}`);
726
- }
727
- }
728
- async function resolveStdioHttpEndpoint(config, options, resolvedConfigPath) {
729
- const repositoryPath = getRegistryRepositoryPath();
730
- if (config.port !== void 0) return { endpoint: new URL(`http://${config.host ?? DEFAULT_HOST}:${config.port}${MCP_ENDPOINT_PATH}`) };
731
- const portRegistry = createPortRegistryService();
732
- const result = await portRegistry.getPort({
733
- repositoryPath,
734
- serviceName: PORT_REGISTRY_SERVICE_HTTP,
735
- serviceType: PORT_REGISTRY_SERVICE_TYPE,
736
- environment: getRegistryEnvironment()
737
- });
738
- if (result.success && result.record) {
739
- const host = config.host ?? result.record.host;
740
- const endpoint = new URL(`http://${host}:${result.record.port}${MCP_ENDPOINT_PATH}`);
741
- try {
742
- const healthUrl = `http://${host}:${result.record.port}/health`;
743
- if ((await fetch(healthUrl)).ok) return { endpoint };
744
- } catch {}
745
- await portRegistry.releasePort({
746
- repositoryPath,
747
- serviceName: PORT_REGISTRY_SERVICE_HTTP,
748
- serviceType: PORT_REGISTRY_SERVICE_TYPE,
749
- environment: getRegistryEnvironment(),
750
- force: true
608
+ await withConnectedCommandContext(options, async ({ clientManager }) => {
609
+ const clients = options.server ? [clientManager.getClient(options.server)].filter((client) => client !== void 0) : clientManager.getAllClients();
610
+ if (options.server && clients.length === 0) throw new Error(`Server "${options.server}" not found`);
611
+ const promptsByServer = {};
612
+ await Promise.all(clients.map(async (client) => {
613
+ try {
614
+ promptsByServer[client.serverName] = await client.listPrompts();
615
+ } catch (error) {
616
+ promptsByServer[client.serverName] = [];
617
+ if (!options.json) console.error(`Failed to list prompts from ${client.serverName}: ${toErrorMessage$6(error)}`);
618
+ }
619
+ }));
620
+ if (options.json) console.log(JSON.stringify(promptsByServer, null, 2));
621
+ else for (const [serverName, prompts] of Object.entries(promptsByServer)) {
622
+ console.log(`\n${serverName}:`);
623
+ if (prompts.length === 0) {
624
+ console.log(" No prompts available");
625
+ continue;
626
+ }
627
+ for (const prompt of prompts) {
628
+ console.log(` - ${prompt.name}: ${prompt.description || "No description"}`);
629
+ if (prompt.arguments && prompt.arguments.length > 0) {
630
+ const args = prompt.arguments.map((arg) => `${arg.name}${arg.required ? " (required)" : ""}`).join(", ");
631
+ console.log(` args: ${args}`);
632
+ }
633
+ }
634
+ }
751
635
  });
636
+ } catch (error) {
637
+ console.error(`Error executing list-prompts: ${toErrorMessage$6(error)}`);
638
+ process.exit(1);
752
639
  }
753
- const runtime = await prestartHttpRuntime({
754
- host: config.host ?? DEFAULT_HOST,
755
- config: options.config || resolvedConfigPath,
756
- cache: options.cache,
757
- definitionsCache: options.definitionsCache,
758
- clearDefinitionsCache: options.clearDefinitionsCache,
759
- proxyMode: options.proxyMode
760
- });
761
- return {
762
- endpoint: new URL(`http://${runtime.host}:${runtime.port}${MCP_ENDPOINT_PATH}`),
763
- ownedRuntimeServerId: runtime.reusedExistingRuntime ? void 0 : runtime.serverId
764
- };
640
+ });
641
+ //#endregion
642
+ //#region src/commands/list-resources.ts
643
+ /**
644
+ * ListResources Command
645
+ *
646
+ * DESIGN PATTERNS:
647
+ * - Command pattern with Commander for CLI argument parsing
648
+ * - Async/await pattern for asynchronous operations
649
+ * - Error handling pattern with try-catch and proper exit codes
650
+ *
651
+ * CODING STANDARDS:
652
+ * - Use async action handlers for asynchronous operations
653
+ * - Provide clear option descriptions and default values
654
+ * - Handle errors gracefully with process.exit()
655
+ * - Log progress and errors to console
656
+ * - Use Commander's .option() and .argument() for inputs
657
+ *
658
+ * AVOID:
659
+ * - Synchronous blocking operations in action handlers
660
+ * - Missing error handling (always use try-catch)
661
+ * - Hardcoded values (use options or environment variables)
662
+ * - Not exiting with appropriate exit codes on errors
663
+ */
664
+ function toErrorMessage$5(error) {
665
+ return error instanceof Error ? error.message : String(error);
765
666
  }
766
- async function startStdioHttpTransport(config, options, resolvedConfigPath, proxyDefaults) {
767
- let ownedRuntimeServerId;
768
- const keepAlive = proxyDefaults?.keepAlive ?? false;
667
+ /**
668
+ * List all available resources from connected MCP servers
669
+ */
670
+ 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) => {
769
671
  try {
770
- const resolvedEndpoint = await resolveStdioHttpEndpoint(config, options, resolvedConfigPath);
771
- ownedRuntimeServerId = resolvedEndpoint.ownedRuntimeServerId;
772
- const { endpoint } = resolvedEndpoint;
773
- await startServer(new require_src.StdioHttpTransportHandler({ endpoint }, createStdioSafeLogger()), async () => {
774
- if (keepAlive || !ownedRuntimeServerId) return;
775
- await new require_src.StopServerService().stop({
776
- serverId: ownedRuntimeServerId,
777
- force: true
778
- });
672
+ await withConnectedCommandContext(options, async ({ clientManager }) => {
673
+ const clients = options.server ? [clientManager.getClient(options.server)].filter((c) => c !== void 0) : clientManager.getAllClients();
674
+ if (options.server && clients.length === 0) throw new Error(`Server "${options.server}" not found`);
675
+ const resourcesByServer = {};
676
+ const resourceResults = await Promise.all(clients.map(async (client) => {
677
+ try {
678
+ const resources = await client.listResources();
679
+ return {
680
+ serverName: client.serverName,
681
+ resources,
682
+ error: null
683
+ };
684
+ } catch (error) {
685
+ return {
686
+ serverName: client.serverName,
687
+ resources: [],
688
+ error
689
+ };
690
+ }
691
+ }));
692
+ for (const { serverName, resources, error } of resourceResults) {
693
+ if (error && !options.json) console.error(`Failed to list resources from ${serverName}: ${toErrorMessage$5(error)}`);
694
+ resourcesByServer[serverName] = resources;
695
+ }
696
+ if (options.json) console.log(JSON.stringify(resourcesByServer, null, 2));
697
+ else for (const [serverName, resources] of Object.entries(resourcesByServer)) {
698
+ console.log(`\n${serverName}:`);
699
+ if (resources.length === 0) console.log(" No resources available");
700
+ else for (const resource of resources) {
701
+ const label = resource.name ? `${resource.name} (${resource.uri})` : resource.uri;
702
+ console.log(` - ${label}${resource.description ? `: ${resource.description}` : ""}`);
703
+ }
704
+ }
779
705
  });
780
706
  } catch (error) {
781
- if (!keepAlive && ownedRuntimeServerId) {
782
- const stopServerService = new require_src.StopServerService();
783
- try {
784
- await stopServerService.stop({
785
- serverId: ownedRuntimeServerId,
786
- force: true
787
- });
788
- } catch (cleanupError) {
789
- throw new Error(`Failed to start stdio-http transport: ${toErrorMessage$9(error)}; also failed to stop owned HTTP runtime '${ownedRuntimeServerId}': ${toErrorMessage$9(cleanupError)}`);
790
- }
791
- }
792
- throw new Error(`Failed to start stdio-http transport: ${toErrorMessage$9(error)}`);
707
+ console.error(`Error executing list-resources: ${toErrorMessage$5(error)}`);
708
+ process.exit(1);
793
709
  }
710
+ });
711
+ //#endregion
712
+ //#region src/commands/list-tools.ts
713
+ /**
714
+ * List Tools Command
715
+ *
716
+ * DESIGN PATTERNS:
717
+ * - Command pattern with Commander for CLI argument parsing
718
+ * - Async/await pattern for asynchronous operations
719
+ * - Error handling pattern with try-catch and proper exit codes
720
+ *
721
+ * CODING STANDARDS:
722
+ * - Use async action handlers for asynchronous operations
723
+ * - Provide clear option descriptions and default values
724
+ * - Handle errors gracefully with process.exit()
725
+ * - Log progress and errors to console
726
+ * - Use Commander's .option() and .argument() for inputs
727
+ *
728
+ * AVOID:
729
+ * - Synchronous blocking operations in action handlers
730
+ * - Missing error handling (always use try-catch)
731
+ * - Hardcoded values (use options or environment variables)
732
+ * - Not exiting with appropriate exit codes on errors
733
+ */
734
+ function toErrorMessage$4(error) {
735
+ return error instanceof Error ? error.message : String(error);
794
736
  }
795
- async function startTransport(transportType, options, resolvedConfigPath, serverOptions, proxyDefaults) {
796
- try {
797
- if (transportType === TRANSPORT_TYPE_STDIO) {
798
- await startStdioTransport(serverOptions);
799
- return;
800
- }
801
- if (transportType === TRANSPORT_TYPE_HTTP) {
802
- await createAndStartHttpRuntime(serverOptions, createTransportConfig(options, require_src.TRANSPORT_MODE.HTTP, proxyDefaults), resolvedConfigPath);
803
- return;
737
+ function printSearchResults(result) {
738
+ for (const server of result.servers) {
739
+ console.log(`\n${server.server}:`);
740
+ if (server.capabilities && server.capabilities.length > 0) console.log(` capabilities: ${server.capabilities.join(", ")}`);
741
+ if (server.summary) console.log(` summary: ${server.summary}`);
742
+ if (server.tools.length === 0) {
743
+ console.log(" no tools");
744
+ continue;
804
745
  }
805
- if (transportType === TRANSPORT_TYPE_SSE) {
806
- await startSseTransport(serverOptions, createTransportConfig(options, require_src.TRANSPORT_MODE.SSE, proxyDefaults));
807
- return;
746
+ for (const tool of server.tools) {
747
+ const capabilitySummary = tool.capabilities && tool.capabilities.length > 0 ? ` [${tool.capabilities.join(", ")}]` : "";
748
+ console.log(` - ${tool.name}${capabilitySummary}`);
749
+ if (tool.description) console.log(` ${tool.description}`);
808
750
  }
809
- await startStdioHttpTransport(createTransportConfig(options, require_src.TRANSPORT_MODE.HTTP, proxyDefaults), options, resolvedConfigPath, proxyDefaults);
810
- } catch (error) {
811
- throw new Error(`Failed to start transport '${transportType}': ${toErrorMessage$9(error)}`);
812
751
  }
813
752
  }
814
- /**
815
- * MCP Serve command
816
- */
817
- const mcpServeCommand = new commander.Command("mcp-serve").description("Start MCP server with specified transport").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)").action(async (options) => {
753
+ 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) => {
818
754
  try {
819
- const resolvedConfigPath = options.config || await findConfigFileAsync() || void 0;
820
- const proxyDefaults = resolvedConfigPath ? loadProxyDefaults(resolvedConfigPath) : {};
821
- const transportType = validateTransportType((options.type ?? proxyDefaults.type ?? TRANSPORT_TYPE_STDIO).toLowerCase());
822
- validateProxyMode(options.proxyMode);
823
- await startTransport(transportType, options, resolvedConfigPath, createServerOptions(options, resolvedConfigPath, await resolveServerId(options, resolvedConfigPath)), proxyDefaults);
755
+ await withConnectedCommandContext(options, async ({ container, config, clientManager, configFilePath }) => {
756
+ clientManager.registerServerConfigs(config.mcpServers);
757
+ const cachePath = options.definitionsCache || require_src.DefinitionsCacheService.getDefaultCachePath(configFilePath);
758
+ let cacheData;
759
+ try {
760
+ cacheData = await require_src.DefinitionsCacheService.readFromFile(cachePath);
761
+ } catch {
762
+ cacheData = void 0;
763
+ }
764
+ const definitionsCacheService = container.createDefinitionsCacheService(clientManager, void 0, { cacheData });
765
+ const textBlock = (await container.createSearchListToolsTool(clientManager, definitionsCacheService).execute({
766
+ capability: options.capability,
767
+ serverName: options.server
768
+ })).content.find((content) => content.type === "text");
769
+ const parsed = textBlock?.type === "text" ? JSON.parse(textBlock.text) : { servers: [] };
770
+ if (options.json) console.log(JSON.stringify(parsed, null, 2));
771
+ else {
772
+ if (!parsed.servers || parsed.servers.length === 0) throw new Error("No tools matched the requested filters");
773
+ printSearchResults(parsed);
774
+ }
775
+ });
824
776
  } catch (error) {
825
- const rawTransportType = (options.type ?? TRANSPORT_TYPE_STDIO).toLowerCase();
826
- const transportType = isValidTransportType(rawTransportType) ? rawTransportType : TRANSPORT_TYPE_STDIO;
827
- const envPort = process.env.MCP_PORT ? Number(process.env.MCP_PORT) : void 0;
828
- const requestedPort = options.port ?? (Number.isFinite(envPort) ? envPort : void 0);
829
- console.error(formatStartError(transportType, options.host ?? DEFAULT_HOST, requestedPort, error));
777
+ console.error(`Error executing search-tools: ${toErrorMessage$4(error)}`);
830
778
  process.exit(1);
831
779
  }
832
780
  });
833
781
  //#endregion
834
- //#region src/commands/bootstrap.ts
835
- function toErrorMessage$8(error) {
782
+ //#region src/commands/mcp-serve.ts
783
+ /**
784
+ * MCP Serve Command
785
+ *
786
+ * DESIGN PATTERNS:
787
+ * - Command pattern with Commander for CLI argument parsing
788
+ * - Transport abstraction pattern for flexible deployment (stdio, HTTP, SSE)
789
+ * - Factory pattern for creating transport handlers
790
+ * - Graceful shutdown pattern with signal handling
791
+ *
792
+ * CODING STANDARDS:
793
+ * - Use async/await for asynchronous operations
794
+ * - Implement proper error handling with try-catch blocks
795
+ * - Handle process signals for graceful shutdown
796
+ * - Provide clear CLI options and help messages
797
+ *
798
+ * AVOID:
799
+ * - Hardcoded configuration values (use CLI options or environment variables)
800
+ * - Missing error handling for transport startup
801
+ * - Not cleaning up resources on shutdown
802
+ */
803
+ const CONFIG_FILE_NAMES = [
804
+ "mcp-config.yaml",
805
+ "mcp-config.yml",
806
+ "mcp-config.json"
807
+ ];
808
+ const MCP_ENDPOINT_PATH = "/mcp";
809
+ const DEFAULT_HOST = "localhost";
810
+ const TRANSPORT_TYPE_STDIO = "stdio";
811
+ const TRANSPORT_TYPE_HTTP = "http";
812
+ const TRANSPORT_TYPE_SSE = "sse";
813
+ const TRANSPORT_TYPE_STDIO_HTTP = "stdio-http";
814
+ const RUNTIME_TRANSPORT = TRANSPORT_TYPE_HTTP;
815
+ const PORT_REGISTRY_SERVICE_HTTP = "mcp-proxy-http";
816
+ const PORT_REGISTRY_SERVICE_TYPE = "service";
817
+ function getWorkspaceRoot() {
818
+ return resolveWorkspaceRoot();
819
+ }
820
+ const PROCESS_REGISTRY_SERVICE_HTTP = "mcp-proxy-http";
821
+ const PROCESS_REGISTRY_SERVICE_TYPE = "service";
822
+ function getRegistryRepositoryPath() {
823
+ return getWorkspaceRoot();
824
+ }
825
+ function toErrorMessage$3(error) {
836
826
  return error instanceof Error ? error.message : String(error);
837
827
  }
838
- async function checkHealth(host, port) {
828
+ function isValidTransportType(type) {
829
+ return type === TRANSPORT_TYPE_STDIO || type === TRANSPORT_TYPE_HTTP || type === TRANSPORT_TYPE_SSE || type === TRANSPORT_TYPE_STDIO_HTTP;
830
+ }
831
+ function isValidProxyMode(mode) {
832
+ return mode === "meta" || mode === "flat" || mode === "search";
833
+ }
834
+ async function pathExists(filePath) {
839
835
  try {
840
- return (await fetch(`http://${host}:${port}/health`, { signal: AbortSignal.timeout(3e3) })).ok;
836
+ await (0, node_fs_promises.access)(filePath, node_fs.constants.F_OK);
837
+ return true;
841
838
  } catch {
842
839
  return false;
843
840
  }
844
841
  }
845
- /**
846
- * Proxy mode: connect to a running HTTP server instead of downstream servers directly.
847
- * Auto-starts the server if not running.
848
- */
849
- async function withProxiedContext(container, config, configFilePath, options, run) {
850
- const host = config.proxy?.host ?? "localhost";
851
- const port = config.proxy?.port;
852
- const endpoint = `http://${host}:${port}/mcp`;
853
- if (!await checkHealth(host, port)) {
854
- if (!options.json) console.error("Starting HTTP proxy server in background...");
855
- await prestartHttpRuntime({
856
- host,
857
- port,
858
- config: configFilePath,
859
- cache: options.useCache !== false,
860
- clearDefinitionsCache: false,
861
- proxyMode: "flat"
862
- });
863
- }
864
- const clientManager = container.createClientManagerService();
842
+ async function findConfigFileAsync() {
865
843
  try {
866
- await clientManager.connectToServer("proxy", {
867
- name: "proxy",
868
- transport: "http",
869
- config: { url: endpoint }
870
- });
871
- if (!options.json) console.error(`✓ Connected to proxy at ${endpoint}`);
844
+ const projectPath = process.env.PROJECT_PATH;
845
+ if (projectPath) for (const fileName of CONFIG_FILE_NAMES) {
846
+ const configPath = (0, node_path.resolve)(projectPath, fileName);
847
+ if (await pathExists(configPath)) return configPath;
848
+ }
849
+ const MAX_PARENT_LEVELS = 3;
850
+ let searchDir = process.cwd();
851
+ for (let level = 0; level <= MAX_PARENT_LEVELS; level++) {
852
+ for (const fileName of CONFIG_FILE_NAMES) {
853
+ const configPath = (0, node_path.join)(searchDir, fileName);
854
+ if (await pathExists(configPath)) return configPath;
855
+ }
856
+ const parentDir = (0, node_path.dirname)(searchDir);
857
+ if (parentDir === searchDir) break;
858
+ searchDir = parentDir;
859
+ }
860
+ return null;
872
861
  } catch (error) {
873
- throw new Error(`Failed to connect to proxy server at ${endpoint}: ${toErrorMessage$8(error)}`);
862
+ throw new Error(`Failed to discover MCP config file: ${toErrorMessage$3(error)}`);
874
863
  }
864
+ }
865
+ function loadProxyDefaults(configPath) {
875
866
  try {
876
- return await run({
877
- container,
878
- configFilePath,
879
- config,
880
- clientManager
881
- });
882
- } finally {
883
- await clientManager.disconnectAll();
867
+ const content = (0, node_fs.readFileSync)(configPath, "utf-8");
868
+ const proxy = (configPath.endsWith(".yaml") || configPath.endsWith(".yml") ? js_yaml.default.load(content) : JSON.parse(content))?.proxy;
869
+ if (!proxy || typeof proxy !== "object") return {};
870
+ const p = proxy;
871
+ return {
872
+ type: typeof p.type === "string" ? p.type : void 0,
873
+ port: typeof p.port === "number" && Number.isInteger(p.port) && p.port > 0 ? p.port : void 0,
874
+ host: typeof p.host === "string" ? p.host : void 0,
875
+ keepAlive: typeof p.keepAlive === "boolean" ? p.keepAlive : void 0
876
+ };
877
+ } catch {
878
+ return {};
884
879
  }
885
880
  }
886
- /**
887
- * Direct mode: connect to all downstream MCP servers individually.
888
- */
889
- async function withDirectContext(container, config, configFilePath, options, run) {
890
- const clientManager = container.createClientManagerService();
891
- await Promise.all(Object.entries(config.mcpServers).map(async ([serverName, serverConfig]) => {
892
- try {
893
- await clientManager.connectToServer(serverName, serverConfig);
894
- if (!options.json) console.error(`✓ Connected to ${serverName}`);
895
- } catch (error) {
896
- if (!options.json) console.error(`✗ Failed to connect to ${serverName}: ${toErrorMessage$8(error)}`);
881
+ async function resolveServerId(options, resolvedConfigPath) {
882
+ const container = require_src.createProxyIoCContainer();
883
+ if (options.id) return options.id;
884
+ if (resolvedConfigPath) try {
885
+ const config = await container.createConfigFetcherService({
886
+ configFilePath: resolvedConfigPath,
887
+ useCache: options.cache !== false
888
+ }).fetchConfiguration(options.cache === false);
889
+ if (config.id) return config.id;
890
+ } catch (error) {
891
+ throw new Error(`Failed to resolve server ID from config '${resolvedConfigPath}': ${toErrorMessage$3(error)}`);
892
+ }
893
+ return require_src.generateServerId();
894
+ }
895
+ function validateTransportType(type) {
896
+ 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}`);
897
+ return type;
898
+ }
899
+ function validateProxyMode(mode) {
900
+ if (!isValidProxyMode(mode)) throw new Error(`Unknown proxy mode: '${mode}'. Valid options: meta, flat, search`);
901
+ }
902
+ function createTransportConfig(options, mode, proxyDefaults) {
903
+ const envPort = process.env.MCP_PORT ? Number(process.env.MCP_PORT) : void 0;
904
+ return {
905
+ mode,
906
+ port: options.port ?? (Number.isFinite(envPort) ? envPort : void 0) ?? proxyDefaults?.port,
907
+ host: options.host ?? process.env.MCP_HOST ?? proxyDefaults?.host ?? DEFAULT_HOST
908
+ };
909
+ }
910
+ function createStdioSafeLogger() {
911
+ const logToStderr = (message, data) => {
912
+ if (data === void 0) {
913
+ console.error(message);
914
+ return;
897
915
  }
898
- }));
899
- if (clientManager.getAllClients().length === 0) throw new Error("No MCP servers connected");
900
- try {
901
- return await run({
902
- container,
903
- configFilePath,
904
- config,
905
- clientManager
906
- });
907
- } finally {
908
- await clientManager.disconnectAll();
916
+ console.error(message, data);
917
+ };
918
+ return {
919
+ trace: logToStderr,
920
+ debug: logToStderr,
921
+ info: logToStderr,
922
+ warn: logToStderr,
923
+ error: logToStderr
924
+ };
925
+ }
926
+ function createServerOptions(options, resolvedConfigPath, serverId) {
927
+ return {
928
+ configFilePath: resolvedConfigPath,
929
+ noCache: options.cache === false,
930
+ serverId,
931
+ definitionsCachePath: options.definitionsCache,
932
+ clearDefinitionsCache: options.clearDefinitionsCache,
933
+ proxyMode: options.proxyMode
934
+ };
935
+ }
936
+ function formatStartError(type, host, port, error) {
937
+ const startErrorMessage = toErrorMessage$3(error);
938
+ if (type === TRANSPORT_TYPE_STDIO) return `Failed to start MCP server with transport '${type}': ${startErrorMessage}`;
939
+ return `Failed to start MCP server with transport '${type}' on ${port === void 0 ? `${host} (dynamic port)` : `${host}:${port}`}: ${startErrorMessage}`;
940
+ }
941
+ function createRuntimeRecord(serverId, config, port, shutdownToken, configPath) {
942
+ return {
943
+ serverId,
944
+ host: config.host ?? DEFAULT_HOST,
945
+ port,
946
+ transport: RUNTIME_TRANSPORT,
947
+ shutdownToken,
948
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
949
+ pid: process.pid,
950
+ configPath
951
+ };
952
+ }
953
+ function createPortRegistryService() {
954
+ return new _agimon_ai_foundation_port_registry.PortRegistryService(process.env.PORT_REGISTRY_PATH);
955
+ }
956
+ function getRegistryEnvironment() {
957
+ return process.env.NODE_ENV ?? "development";
958
+ }
959
+ async function createPortRegistryLease(serviceName, host, preferredPort, serverId, transport, configPath, portRange = preferredPort !== void 0 ? {
960
+ min: preferredPort,
961
+ max: preferredPort
962
+ } : _agimon_ai_foundation_port_registry.DEFAULT_PORT_RANGE) {
963
+ const portRegistry = createPortRegistryService();
964
+ const result = await portRegistry.reservePort({
965
+ repositoryPath: getRegistryRepositoryPath(),
966
+ serviceName,
967
+ serviceType: PORT_REGISTRY_SERVICE_TYPE,
968
+ environment: getRegistryEnvironment(),
969
+ pid: process.pid,
970
+ host,
971
+ preferredPort,
972
+ portRange,
973
+ force: true,
974
+ metadata: {
975
+ transport,
976
+ serverId,
977
+ ...configPath ? { configPath } : {}
978
+ }
979
+ });
980
+ if (!result.success || !result.record) {
981
+ const requestedPortLabel = preferredPort === void 0 ? "dynamic port" : `port ${preferredPort}`;
982
+ throw new Error(result.error || `Failed to reserve ${requestedPortLabel} in port registry`);
909
983
  }
984
+ let released = false;
985
+ return {
986
+ port: result.record.port,
987
+ release: async () => {
988
+ if (released) return;
989
+ released = true;
990
+ const releaseResult = await portRegistry.releasePort({
991
+ repositoryPath: getRegistryRepositoryPath(),
992
+ serviceName,
993
+ serviceType: PORT_REGISTRY_SERVICE_TYPE,
994
+ pid: process.pid,
995
+ environment: getRegistryEnvironment(),
996
+ force: true
997
+ });
998
+ if (!releaseResult.success && releaseResult.error && !releaseResult.error.includes("No matching registry entry")) throw new Error(releaseResult.error || `Failed to release port for ${serviceName}`);
999
+ }
1000
+ };
910
1001
  }
911
- async function withConnectedCommandContext(options, run) {
912
- const container = require_src.createProxyIoCContainer();
913
- const configFilePath = options.config || require_src.findConfigFile();
914
- if (!configFilePath) throw new Error("No config file found. Use --config or create mcp-config.yaml");
915
- const config = await container.createConfigFetcherService({
916
- configFilePath,
917
- useCache: options.useCache
918
- }).fetchConfiguration();
919
- if (config.proxy?.port) return await withProxiedContext(container, config, configFilePath, options, run);
920
- return await withDirectContext(container, config, configFilePath, options, run);
1002
+ async function releasePortLease(lease) {
1003
+ if (!lease) return;
1004
+ await lease.release();
921
1005
  }
922
- //#endregion
923
- //#region src/commands/list-tools.ts
924
- /**
925
- * List Tools Command
926
- *
927
- * DESIGN PATTERNS:
928
- * - Command pattern with Commander for CLI argument parsing
929
- * - Async/await pattern for asynchronous operations
930
- * - Error handling pattern with try-catch and proper exit codes
931
- *
932
- * CODING STANDARDS:
933
- * - Use async action handlers for asynchronous operations
934
- * - Provide clear option descriptions and default values
935
- * - Handle errors gracefully with process.exit()
936
- * - Log progress and errors to console
937
- * - Use Commander's .option() and .argument() for inputs
938
- *
939
- * AVOID:
940
- * - Synchronous blocking operations in action handlers
941
- * - Missing error handling (always use try-catch)
942
- * - Hardcoded values (use options or environment variables)
943
- * - Not exiting with appropriate exit codes on errors
944
- */
945
- function toErrorMessage$7(error) {
946
- return error instanceof Error ? error.message : String(error);
1006
+ function createHttpAdminOptions(serverId, shutdownToken, onShutdownRequested) {
1007
+ return {
1008
+ serverId,
1009
+ shutdownToken,
1010
+ onShutdownRequested
1011
+ };
947
1012
  }
948
- function printSearchResults(result) {
949
- for (const server of result.servers) {
950
- console.log(`\n${server.server}:`);
951
- if (server.capabilities && server.capabilities.length > 0) console.log(` capabilities: ${server.capabilities.join(", ")}`);
952
- if (server.summary) console.log(` summary: ${server.summary}`);
953
- if (server.tools.length === 0) {
954
- console.log(" no tools");
955
- continue;
956
- }
957
- for (const tool of server.tools) {
958
- const capabilitySummary = tool.capabilities && tool.capabilities.length > 0 ? ` [${tool.capabilities.join(", ")}]` : "";
959
- console.log(` - ${tool.name}${capabilitySummary}`);
960
- if (tool.description) console.log(` ${tool.description}`);
961
- }
1013
+ async function removeRuntimeRecord(runtimeStateService, serverId) {
1014
+ try {
1015
+ await runtimeStateService.remove(serverId);
1016
+ } catch (error) {
1017
+ throw new Error(`Failed to remove runtime state for '${serverId}': ${toErrorMessage$3(error)}`);
962
1018
  }
963
1019
  }
964
- 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) => {
1020
+ async function writeRuntimeRecord(runtimeStateService, record) {
965
1021
  try {
966
- await withConnectedCommandContext(options, async ({ container, config, clientManager, configFilePath }) => {
967
- clientManager.registerServerConfigs(config.mcpServers);
968
- const cachePath = options.definitionsCache || require_src.DefinitionsCacheService.getDefaultCachePath(configFilePath);
969
- let cacheData;
970
- try {
971
- cacheData = await require_src.DefinitionsCacheService.readFromFile(cachePath);
972
- } catch {
973
- cacheData = void 0;
974
- }
975
- const definitionsCacheService = container.createDefinitionsCacheService(clientManager, void 0, { cacheData });
976
- const textBlock = (await container.createSearchListToolsTool(clientManager, definitionsCacheService).execute({
977
- capability: options.capability,
978
- serverName: options.server
979
- })).content.find((content) => content.type === "text");
980
- const parsed = textBlock?.type === "text" ? JSON.parse(textBlock.text) : { servers: [] };
981
- if (options.json) console.log(JSON.stringify(parsed, null, 2));
982
- else {
983
- if (!parsed.servers || parsed.servers.length === 0) throw new Error("No tools matched the requested filters");
984
- printSearchResults(parsed);
985
- }
986
- });
1022
+ await runtimeStateService.write(record);
987
1023
  } catch (error) {
988
- console.error(`Error executing search-tools: ${toErrorMessage$7(error)}`);
989
- process.exit(1);
1024
+ throw new Error(`Failed to persist runtime state for '${record.serverId}': ${toErrorMessage$3(error)}`);
990
1025
  }
991
- });
992
- //#endregion
993
- //#region src/commands/describe-tools.ts
994
- /**
995
- * Describe Tools Command
996
- *
997
- * DESIGN PATTERNS:
998
- * - Command pattern with Commander for CLI argument parsing
999
- * - Async/await pattern for asynchronous operations
1000
- * - Error handling pattern with try-catch and proper exit codes
1001
- *
1002
- * CODING STANDARDS:
1003
- * - Use async action handlers for asynchronous operations
1004
- * - Provide clear option descriptions and default values
1005
- * - Handle errors gracefully with process.exit()
1006
- * - Log progress and errors to console
1007
- * - Use Commander's .option() and .argument() for inputs
1008
- *
1009
- * AVOID:
1010
- * - Synchronous blocking operations in action handlers
1011
- * - Missing error handling (always use try-catch)
1012
- * - Hardcoded values (use options or environment variables)
1013
- * - Not exiting with appropriate exit codes on errors
1014
- */
1015
- function toErrorMessage$6(error) {
1016
- return error instanceof Error ? error.message : String(error);
1017
1026
  }
1018
- /**
1019
- * Describe specific MCP tools
1020
- */
1021
- 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) => {
1027
+ async function stopOwnedHttpTransport(handler, runtimeStateService, serverId, processLease) {
1022
1028
  try {
1023
- await withConnectedCommandContext(options, async ({ container, config, clientManager }) => {
1024
- const clients = options.server ? [clientManager.getClient(options.server)].filter((client) => client !== void 0) : clientManager.getAllClients();
1025
- if (options.server && clients.length === 0) throw new Error(`Server "${options.server}" not found`);
1026
- const cwd = process.env.PROJECT_PATH || process.cwd();
1027
- const skillPaths = config.skills?.paths || [];
1028
- const skillService = skillPaths.length > 0 ? container.createSkillService(cwd, skillPaths) : void 0;
1029
- const foundTools = [];
1030
- const foundSkills = [];
1031
- const notFoundTools = [...toolNames];
1032
- const toolResults = await Promise.all(clients.map(async (client) => {
1033
- try {
1034
- return {
1035
- client,
1036
- tools: await client.listTools(),
1037
- error: null
1038
- };
1039
- } catch (error) {
1040
- return {
1041
- client,
1042
- tools: [],
1043
- error
1044
- };
1045
- }
1046
- }));
1047
- for (const { client, tools, error } of toolResults) {
1048
- if (error) {
1049
- if (!options.json) console.error(`Failed to list tools from ${client.serverName}:`, error);
1050
- continue;
1051
- }
1052
- for (const toolName of toolNames) {
1053
- const tool = tools.find((entry) => entry.name === toolName);
1054
- if (tool) {
1055
- foundTools.push({
1056
- server: client.serverName,
1057
- name: tool.name,
1058
- description: tool.description,
1059
- inputSchema: tool.inputSchema
1060
- });
1061
- const idx = notFoundTools.indexOf(toolName);
1062
- if (idx > -1) notFoundTools.splice(idx, 1);
1063
- }
1064
- }
1065
- }
1066
- if (skillService && notFoundTools.length > 0) {
1067
- const skillResults = await Promise.all([...notFoundTools].map(async (toolName) => {
1068
- const skillName = toolName.startsWith("skill__") ? toolName.slice(7) : toolName;
1069
- return {
1070
- toolName,
1071
- skill: await skillService.getSkill(skillName)
1072
- };
1073
- }));
1074
- for (const { toolName, skill } of skillResults) if (skill) {
1075
- foundSkills.push({
1076
- name: skill.name,
1077
- location: skill.basePath,
1078
- instructions: skill.content
1079
- });
1080
- const idx = notFoundTools.indexOf(toolName);
1081
- if (idx > -1) notFoundTools.splice(idx, 1);
1082
- }
1083
- }
1084
- const nextSteps = [];
1085
- if (foundTools.length > 0) nextSteps.push("For MCP tools: Use the use_tool function with toolName and toolArgs based on the inputSchema above.");
1086
- if (foundSkills.length > 0) nextSteps.push(`For skill, just follow skill's description to continue.`);
1087
- if (options.json) {
1088
- const result = {};
1089
- if (foundTools.length > 0) result.tools = foundTools;
1090
- if (foundSkills.length > 0) result.skills = foundSkills;
1091
- if (nextSteps.length > 0) result.nextSteps = nextSteps;
1092
- if (notFoundTools.length > 0) result.notFound = notFoundTools;
1093
- console.log(JSON.stringify(result, null, 2));
1094
- } else {
1095
- if (foundTools.length > 0) {
1096
- console.log("\nFound tools:\n");
1097
- for (const tool of foundTools) {
1098
- console.log(`Server: ${tool.server}`);
1099
- console.log(`Tool: ${tool.name}`);
1100
- console.log(`Description: ${tool.description || "No description"}`);
1101
- console.log("Input Schema:");
1102
- console.log(JSON.stringify(tool.inputSchema, null, 2));
1103
- console.log("");
1104
- }
1105
- }
1106
- if (foundSkills.length > 0) {
1107
- console.log("\nFound skills:\n");
1108
- for (const skill of foundSkills) {
1109
- console.log(`Skill: ${skill.name}`);
1110
- console.log(`Location: ${skill.location}`);
1111
- console.log(`Instructions:\n${skill.instructions}`);
1112
- console.log("");
1113
- }
1114
- }
1115
- if (nextSteps.length > 0) {
1116
- console.log("\nNext steps:");
1117
- for (const step of nextSteps) console.log(` • ${step}`);
1118
- console.log("");
1119
- }
1120
- if (notFoundTools.length > 0) console.error(`\nTools/skills not found: ${notFoundTools.join(", ")}`);
1121
- if (foundTools.length === 0 && foundSkills.length === 0) {
1122
- console.error("No tools or skills found");
1123
- process.exit(1);
1124
- }
1125
- }
1126
- });
1127
- } catch (error) {
1128
- console.error(`Error executing describe-tools: ${toErrorMessage$6(error)}`);
1129
- process.exit(1);
1029
+ try {
1030
+ await handler.stop();
1031
+ } catch (error) {
1032
+ throw new Error(`Failed to stop owned HTTP transport '${serverId}': ${toErrorMessage$3(error)}`);
1033
+ }
1034
+ } finally {
1035
+ await processLease?.release({ kill: false });
1036
+ await removeRuntimeRecord(runtimeStateService, serverId);
1130
1037
  }
1131
- });
1132
- //#endregion
1133
- //#region src/commands/use-tool.ts
1134
- /**
1135
- * Use Tool Command
1136
- *
1137
- * DESIGN PATTERNS:
1138
- * - Command pattern with Commander for CLI argument parsing
1139
- * - Async/await pattern for asynchronous operations
1140
- * - Error handling pattern with try-catch and proper exit codes
1141
- *
1142
- * CODING STANDARDS:
1143
- * - Use async action handlers for asynchronous operations
1144
- * - Provide clear option descriptions and default values
1145
- * - Handle errors gracefully with process.exit()
1146
- * - Log progress and errors to console
1147
- * - Use Commander'"'"'s .option() and .argument() for inputs
1148
- *
1149
- * AVOID:
1150
- * - Synchronous blocking operations in action handlers
1151
- * - Missing error handling (always use try-catch)
1152
- * - Hardcoded values (use options or environment variables)
1153
- * - Not exiting with appropriate exit codes on errors
1154
- */
1155
- function toErrorMessage$5(error) {
1156
- return error instanceof Error ? error.message : String(error);
1157
1038
  }
1158
1039
  /**
1159
- * Execute an MCP tool with arguments
1040
+ * Run post-stop cleanup for an HTTP runtime (release port, dispose services, remove state).
1041
+ * This is the subset of stopOwnedHttpTransport that runs AFTER handler.stop() has already
1042
+ * been called by startServer()'s signal handler — avoids double-stopping the transport.
1160
1043
  */
1161
- 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) => {
1044
+ async function cleanupHttpRuntime(runtimeStateService, serverId, processLease) {
1045
+ await processLease?.release({ kill: false });
1046
+ await removeRuntimeRecord(runtimeStateService, serverId);
1047
+ }
1048
+ async function cleanupFailedRuntimeStartup(handler, runtimeStateService, serverId, processLease) {
1162
1049
  try {
1163
- let toolArgs = {};
1164
1050
  try {
1165
- toolArgs = JSON.parse(options.args);
1166
- } catch {
1167
- console.error("Error: Invalid JSON in --args");
1168
- process.exit(1);
1051
+ await handler.stop();
1052
+ } catch (error) {
1053
+ throw new Error(`Failed to stop HTTP transport during cleanup for '${serverId}': ${toErrorMessage$3(error)}`);
1169
1054
  }
1170
- await withConnectedCommandContext(options, async ({ container, config, clientManager }) => {
1171
- const clients = clientManager.getAllClients();
1172
- if (options.server) {
1173
- const client = clientManager.getClient(options.server);
1174
- if (!client) throw new Error(`Server "${options.server}" not found`);
1175
- if (!options.json) console.error(`Executing ${toolName} on ${options.server}...`);
1176
- const requestOptions = options.timeout ? { timeout: options.timeout } : void 0;
1177
- const result = await client.callTool(toolName, toolArgs, requestOptions);
1178
- if (options.json) console.log(JSON.stringify(result, null, 2));
1179
- else {
1180
- console.log("\nResult:");
1181
- if (result.content) for (const content of result.content) if (content.type === "text") console.log(content.text);
1182
- else console.log(JSON.stringify(content, null, 2));
1183
- if (result.isError) {
1184
- console.error("\n⚠️ Tool execution returned an error");
1185
- process.exit(1);
1186
- }
1187
- }
1188
- return;
1189
- }
1190
- const searchResults = await Promise.all(clients.map(async (client) => {
1191
- try {
1192
- const hasTool = (await client.listTools()).some((t) => t.name === toolName);
1193
- return {
1194
- serverName: client.serverName,
1195
- hasTool,
1196
- error: null
1197
- };
1198
- } catch (error) {
1199
- return {
1200
- serverName: client.serverName,
1201
- hasTool: false,
1202
- error
1203
- };
1204
- }
1205
- }));
1206
- const matchingServers = [];
1207
- for (const { serverName, hasTool, error } of searchResults) {
1208
- if (error) {
1209
- if (!options.json) console.error(`Failed to list tools from ${serverName}:`, error);
1210
- continue;
1211
- }
1212
- if (hasTool) matchingServers.push(serverName);
1213
- }
1214
- if (matchingServers.length === 0) {
1215
- const skillPaths = config.skills?.paths || [];
1216
- if (skillPaths.length > 0) try {
1217
- const cwd = process.env.PROJECT_PATH || process.cwd();
1218
- const skillService = container.createSkillService(cwd, skillPaths);
1219
- const skillName = toolName.startsWith("skill__") ? toolName.slice(7) : toolName;
1220
- const skill = await skillService.getSkill(skillName);
1221
- if (skill) {
1222
- const result = { content: [{
1223
- type: "text",
1224
- text: skill.content
1225
- }] };
1226
- if (options.json) console.log(JSON.stringify(result, null, 2));
1227
- else {
1228
- console.log("\nSkill content:");
1229
- console.log(skill.content);
1230
- }
1231
- return;
1232
- }
1233
- } catch (error) {
1234
- if (!options.json) console.error(`Failed to lookup skill "${toolName}":`, error);
1235
- }
1236
- throw new Error(`Tool or skill "${toolName}" not found on any connected server or configured skill paths`);
1237
- }
1238
- if (matchingServers.length > 1) throw new Error(`Tool "${toolName}" found on multiple servers: ${matchingServers.join(", ")}`);
1239
- const targetServer = matchingServers[0];
1240
- const client = clientManager.getClient(targetServer);
1241
- if (!client) throw new Error(`Internal error: Server "${targetServer}" not connected`);
1242
- if (!options.json) console.error(`Executing ${toolName} on ${targetServer}...`);
1243
- const requestOptions = options.timeout ? { timeout: options.timeout } : void 0;
1244
- const result = await client.callTool(toolName, toolArgs, requestOptions);
1245
- if (options.json) console.log(JSON.stringify(result, null, 2));
1246
- else {
1247
- console.log("\nResult:");
1248
- if (result.content) for (const content of result.content) if (content.type === "text") console.log(content.text);
1249
- else console.log(JSON.stringify(content, null, 2));
1250
- if (result.isError) {
1251
- console.error("\n⚠️ Tool execution returned an error");
1252
- process.exit(1);
1253
- }
1254
- }
1255
- });
1256
- } catch (error) {
1257
- console.error(`Error executing use-tool: ${toErrorMessage$5(error)}`);
1258
- process.exit(1);
1055
+ } finally {
1056
+ await processLease?.release({ kill: false });
1057
+ await removeRuntimeRecord(runtimeStateService, serverId);
1259
1058
  }
1260
- });
1261
- //#endregion
1262
- //#region src/commands/list-resources.ts
1263
- /**
1264
- * ListResources Command
1265
- *
1266
- * DESIGN PATTERNS:
1267
- * - Command pattern with Commander for CLI argument parsing
1268
- * - Async/await pattern for asynchronous operations
1269
- * - Error handling pattern with try-catch and proper exit codes
1270
- *
1271
- * CODING STANDARDS:
1272
- * - Use async action handlers for asynchronous operations
1273
- * - Provide clear option descriptions and default values
1274
- * - Handle errors gracefully with process.exit()
1275
- * - Log progress and errors to console
1276
- * - Use Commander's .option() and .argument() for inputs
1277
- *
1278
- * AVOID:
1279
- * - Synchronous blocking operations in action handlers
1280
- * - Missing error handling (always use try-catch)
1281
- * - Hardcoded values (use options or environment variables)
1282
- * - Not exiting with appropriate exit codes on errors
1283
- */
1284
- function toErrorMessage$4(error) {
1285
- return error instanceof Error ? error.message : String(error);
1286
1059
  }
1287
1060
  /**
1288
- * List all available resources from connected MCP servers
1061
+ * Start MCP server with given transport handler
1062
+ * @param handler - The transport handler to start
1063
+ * @param onStopped - Optional cleanup callback run after signal-based shutdown
1289
1064
  */
1290
- 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) => {
1065
+ async function startServer(handler, onStopped) {
1291
1066
  try {
1292
- await withConnectedCommandContext(options, async ({ clientManager }) => {
1293
- const clients = options.server ? [clientManager.getClient(options.server)].filter((c) => c !== void 0) : clientManager.getAllClients();
1294
- if (options.server && clients.length === 0) throw new Error(`Server "${options.server}" not found`);
1295
- const resourcesByServer = {};
1296
- const resourceResults = await Promise.all(clients.map(async (client) => {
1297
- try {
1298
- const resources = await client.listResources();
1299
- return {
1300
- serverName: client.serverName,
1301
- resources,
1302
- error: null
1303
- };
1304
- } catch (error) {
1305
- return {
1306
- serverName: client.serverName,
1307
- resources: [],
1308
- error
1309
- };
1310
- }
1311
- }));
1312
- for (const { serverName, resources, error } of resourceResults) {
1313
- if (error && !options.json) console.error(`Failed to list resources from ${serverName}: ${toErrorMessage$4(error)}`);
1314
- resourcesByServer[serverName] = resources;
1315
- }
1316
- if (options.json) console.log(JSON.stringify(resourcesByServer, null, 2));
1317
- else for (const [serverName, resources] of Object.entries(resourcesByServer)) {
1318
- console.log(`\n${serverName}:`);
1319
- if (resources.length === 0) console.log(" No resources available");
1320
- else for (const resource of resources) {
1321
- const label = resource.name ? `${resource.name} (${resource.uri})` : resource.uri;
1322
- console.log(` - ${label}${resource.description ? `: ${resource.description}` : ""}`);
1323
- }
1324
- }
1325
- });
1067
+ await handler.start();
1068
+ } catch (error) {
1069
+ throw new Error(`Failed to start transport handler: ${toErrorMessage$3(error)}`);
1070
+ }
1071
+ const shutdown = async (signal) => {
1072
+ console.error(`\nReceived ${signal}, shutting down gracefully...`);
1073
+ try {
1074
+ await handler.stop();
1075
+ if (onStopped) await onStopped();
1076
+ process.exit(0);
1077
+ } catch (error) {
1078
+ console.error(`Failed to gracefully stop transport during ${signal}: ${toErrorMessage$3(error)}`);
1079
+ process.exit(1);
1080
+ }
1081
+ };
1082
+ process.on("SIGINT", async () => await shutdown("SIGINT"));
1083
+ process.on("SIGTERM", async () => await shutdown("SIGTERM"));
1084
+ }
1085
+ async function createAndStartHttpRuntime(serverOptions, config, resolvedConfigPath) {
1086
+ const sharedServices = await require_src.initializeSharedServices(serverOptions);
1087
+ const runtimeStateService = new require_src.RuntimeStateService();
1088
+ const shutdownToken = (0, node_crypto.randomUUID)();
1089
+ const runtimeServerId = serverOptions.serverId ?? require_src.generateServerId();
1090
+ const requestedPort = config.port;
1091
+ const portRange = requestedPort !== void 0 ? {
1092
+ min: requestedPort,
1093
+ max: requestedPort
1094
+ } : _agimon_ai_foundation_port_registry.DEFAULT_PORT_RANGE;
1095
+ const portLease = await createPortRegistryLease(PORT_REGISTRY_SERVICE_HTTP, config.host ?? DEFAULT_HOST, requestedPort, runtimeServerId, TRANSPORT_TYPE_HTTP, resolvedConfigPath, portRange);
1096
+ const runtimePort = portLease.port;
1097
+ const runtimeConfig = {
1098
+ ...config,
1099
+ port: runtimePort
1100
+ };
1101
+ const processLease = await (0, _agimon_ai_foundation_process_registry.createProcessLease)({
1102
+ repositoryPath: getRegistryRepositoryPath(),
1103
+ serviceName: PROCESS_REGISTRY_SERVICE_HTTP,
1104
+ serviceType: PROCESS_REGISTRY_SERVICE_TYPE,
1105
+ environment: getRegistryEnvironment(),
1106
+ host: runtimeConfig.host ?? DEFAULT_HOST,
1107
+ port: runtimePort,
1108
+ command: process.argv[1],
1109
+ args: process.argv.slice(2),
1110
+ metadata: {
1111
+ transport: TRANSPORT_TYPE_HTTP,
1112
+ serverId: runtimeServerId,
1113
+ ...resolvedConfigPath ? { configPath: resolvedConfigPath } : {}
1114
+ }
1115
+ });
1116
+ let releasePort = async () => {
1117
+ await releasePortLease(portLease ?? null);
1118
+ releasePort = async () => void 0;
1119
+ };
1120
+ const runtimeRecord = createRuntimeRecord(runtimeServerId, runtimeConfig, runtimePort, shutdownToken, resolvedConfigPath);
1121
+ let handler;
1122
+ let isStopping = false;
1123
+ const stopHandler = async () => {
1124
+ if (isStopping) return;
1125
+ isStopping = true;
1126
+ try {
1127
+ await stopOwnedHttpTransport(handler, runtimeStateService, runtimeRecord.serverId, processLease);
1128
+ await releasePort();
1129
+ await sharedServices.dispose();
1130
+ process.exit(0);
1131
+ } catch (error) {
1132
+ throw new Error(`Failed to stop HTTP runtime '${runtimeRecord.serverId}' from admin shutdown: ${toErrorMessage$3(error)}`);
1133
+ }
1134
+ };
1135
+ try {
1136
+ handler = new require_src.HttpTransportHandler(() => require_src.createSessionServer(sharedServices), runtimeConfig, createHttpAdminOptions(runtimeRecord.serverId, shutdownToken, stopHandler));
1326
1137
  } catch (error) {
1327
- console.error(`Error executing list-resources: ${toErrorMessage$4(error)}`);
1328
- process.exit(1);
1138
+ await releasePort();
1139
+ await processLease.release({ kill: false });
1140
+ await sharedServices.dispose();
1141
+ throw new Error(`Failed to create HTTP runtime server: ${toErrorMessage$3(error)}`);
1329
1142
  }
1330
- });
1331
- //#endregion
1332
- //#region src/commands/read-resource.ts
1333
- /**
1334
- * ReadResource Command
1335
- *
1336
- * DESIGN PATTERNS:
1337
- * - Command pattern with Commander for CLI argument parsing
1338
- * - Async/await pattern for asynchronous operations
1339
- * - Error handling pattern with try-catch and proper exit codes
1340
- *
1341
- * CODING STANDARDS:
1342
- * - Use async action handlers for asynchronous operations
1343
- * - Provide clear option descriptions and default values
1344
- * - Handle errors gracefully with process.exit()
1345
- * - Log progress and errors to console
1346
- * - Use Commander's .option() and .argument() for inputs
1347
- *
1348
- * AVOID:
1349
- * - Synchronous blocking operations in action handlers
1350
- * - Missing error handling (always use try-catch)
1351
- * - Hardcoded values (use options or environment variables)
1352
- * - Not exiting with appropriate exit codes on errors
1353
- */
1354
- function toErrorMessage$3(error) {
1355
- return error instanceof Error ? error.message : String(error);
1356
- }
1357
- /**
1358
- * Read a resource by URI from a connected MCP server
1359
- */
1360
- 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) => {
1361
1143
  try {
1362
- await withConnectedCommandContext(options, async ({ clientManager }) => {
1363
- const clients = clientManager.getAllClients();
1364
- if (options.server) {
1365
- const client = clientManager.getClient(options.server);
1366
- if (!client) throw new Error(`Server "${options.server}" not found`);
1367
- if (!options.json) console.error(`Reading ${uri} from ${options.server}...`);
1368
- const result = await client.readResource(uri);
1369
- if (options.json) console.log(JSON.stringify(result, null, 2));
1370
- else for (const content of result.contents) if ("text" in content) console.log(content.text);
1371
- else console.log(JSON.stringify(content, null, 2));
1372
- return;
1373
- }
1374
- const searchResults = await Promise.all(clients.map(async (client) => {
1375
- try {
1376
- const hasResource = (await client.listResources()).some((r) => r.uri === uri);
1377
- return {
1378
- serverName: client.serverName,
1379
- hasResource,
1380
- error: null
1381
- };
1382
- } catch (error) {
1383
- return {
1384
- serverName: client.serverName,
1385
- hasResource: false,
1386
- error
1387
- };
1388
- }
1389
- }));
1390
- const matchingServers = [];
1391
- for (const { serverName, hasResource, error } of searchResults) {
1392
- if (error) {
1393
- console.error(`Failed to list resources from ${serverName}: ${toErrorMessage$3(error)}`);
1394
- continue;
1395
- }
1396
- if (hasResource) matchingServers.push(serverName);
1397
- }
1398
- if (matchingServers.length === 0) throw new Error(`Resource "${uri}" not found on any connected server`);
1399
- if (matchingServers.length > 1) throw new Error(`Resource "${uri}" found on multiple servers: ${matchingServers.join(", ")}. Use --server to disambiguate`);
1400
- const targetServer = matchingServers[0];
1401
- const client = clientManager.getClient(targetServer);
1402
- if (!client) throw new Error(`Internal error: Server "${targetServer}" not connected`);
1403
- if (!options.json) console.error(`Reading ${uri} from ${targetServer}...`);
1404
- const result = await client.readResource(uri);
1405
- if (options.json) console.log(JSON.stringify(result, null, 2));
1406
- else for (const content of result.contents) if ("text" in content) console.log(content.text);
1407
- else console.log(JSON.stringify(content, null, 2));
1144
+ await startServer(handler, async () => {
1145
+ await releasePort();
1146
+ await sharedServices.dispose();
1147
+ await cleanupHttpRuntime(runtimeStateService, runtimeRecord.serverId, processLease);
1408
1148
  });
1149
+ await writeRuntimeRecord(runtimeStateService, runtimeRecord);
1409
1150
  } catch (error) {
1410
- console.error(`Error executing read-resource: ${toErrorMessage$3(error)}`);
1411
- process.exit(1);
1151
+ await releasePort();
1152
+ await sharedServices.dispose();
1153
+ await cleanupFailedRuntimeStartup(handler, runtimeStateService, runtimeRecord.serverId, processLease);
1154
+ throw new Error(`Failed to start HTTP runtime '${runtimeRecord.serverId}': ${toErrorMessage$3(error)}`);
1412
1155
  }
1413
- });
1414
- //#endregion
1415
- //#region src/commands/list-prompts.ts
1416
- function toErrorMessage$2(error) {
1417
- return error instanceof Error ? error.message : String(error);
1156
+ console.error(`Runtime state: http://${runtimeRecord.host}:${runtimeRecord.port} (${runtimeRecord.serverId})`);
1418
1157
  }
1419
- 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) => {
1158
+ async function startStdioTransport(serverOptions) {
1420
1159
  try {
1421
- await withConnectedCommandContext(options, async ({ clientManager }) => {
1422
- const clients = options.server ? [clientManager.getClient(options.server)].filter((client) => client !== void 0) : clientManager.getAllClients();
1423
- if (options.server && clients.length === 0) throw new Error(`Server "${options.server}" not found`);
1424
- const promptsByServer = {};
1425
- await Promise.all(clients.map(async (client) => {
1426
- try {
1427
- promptsByServer[client.serverName] = await client.listPrompts();
1428
- } catch (error) {
1429
- promptsByServer[client.serverName] = [];
1430
- if (!options.json) console.error(`Failed to list prompts from ${client.serverName}: ${toErrorMessage$2(error)}`);
1431
- }
1432
- }));
1433
- if (options.json) console.log(JSON.stringify(promptsByServer, null, 2));
1434
- else for (const [serverName, prompts] of Object.entries(promptsByServer)) {
1435
- console.log(`\n${serverName}:`);
1436
- if (prompts.length === 0) {
1437
- console.log(" No prompts available");
1438
- continue;
1439
- }
1440
- for (const prompt of prompts) {
1441
- console.log(` - ${prompt.name}: ${prompt.description || "No description"}`);
1442
- if (prompt.arguments && prompt.arguments.length > 0) {
1443
- const args = prompt.arguments.map((arg) => `${arg.name}${arg.required ? " (required)" : ""}`).join(", ");
1444
- console.log(` args: ${args}`);
1445
- }
1446
- }
1447
- }
1448
- });
1160
+ await startServer(new require_src.StdioTransportHandler(await require_src.createServer(serverOptions), createStdioSafeLogger()));
1449
1161
  } catch (error) {
1450
- console.error(`Error executing list-prompts: ${toErrorMessage$2(error)}`);
1451
- process.exit(1);
1162
+ throw new Error(`Failed to start stdio transport: ${toErrorMessage$3(error)}`);
1452
1163
  }
1453
- });
1454
- //#endregion
1455
- //#region src/commands/get-prompt.ts
1456
- function toErrorMessage$1(error) {
1457
- return error instanceof Error ? error.message : String(error);
1458
1164
  }
1459
- 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) => {
1165
+ async function startSseTransport(serverOptions, config) {
1460
1166
  try {
1461
- let promptArgs = {};
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}`);
1462
1202
  try {
1463
- promptArgs = JSON.parse(options.args);
1464
- } catch {
1465
- throw new Error("Invalid JSON in --args");
1466
- }
1467
- await withConnectedCommandContext(options, async ({ clientManager }) => {
1468
- const clients = clientManager.getAllClients();
1469
- if (options.server) {
1470
- const client = clientManager.getClient(options.server);
1471
- if (!client) throw new Error(`Server "${options.server}" not found`);
1472
- const prompt = await client.getPrompt(promptName, promptArgs);
1473
- if (options.json) console.log(JSON.stringify(prompt, null, 2));
1474
- else for (const message of prompt.messages) {
1475
- const content = message.content;
1476
- if (typeof content === "object" && content && "text" in content) console.log(content.text);
1477
- else console.log(JSON.stringify(message, null, 2));
1478
- }
1479
- return;
1480
- }
1481
- const matchingServers = [];
1482
- await Promise.all(clients.map(async (client) => {
1483
- try {
1484
- if ((await client.listPrompts()).some((prompt) => prompt.name === promptName)) matchingServers.push(client.serverName);
1485
- } catch (error) {
1486
- if (!options.json) console.error(`Failed to list prompts from ${client.serverName}: ${toErrorMessage$1(error)}`);
1487
- }
1488
- }));
1489
- if (matchingServers.length === 0) throw new Error(`Prompt "${promptName}" not found on any connected server`);
1490
- if (matchingServers.length > 1) throw new Error(`Prompt "${promptName}" found on multiple servers: ${matchingServers.join(", ")}. Use --server to disambiguate`);
1491
- const client = clientManager.getClient(matchingServers[0]);
1492
- if (!client) throw new Error(`Internal error: Server "${matchingServers[0]}" not connected`);
1493
- const prompt = await client.getPrompt(promptName, promptArgs);
1494
- if (options.json) console.log(JSON.stringify(prompt, null, 2));
1495
- else for (const message of prompt.messages) {
1496
- const content = message.content;
1497
- if (typeof content === "object" && content && "text" in content) console.log(content.text);
1498
- else console.log(JSON.stringify(message, null, 2));
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 startStdioHttpTransport(config, options, resolvedConfigPath, proxyDefaults) {
1228
+ let ownedRuntimeServerId;
1229
+ const keepAlive = proxyDefaults?.keepAlive ?? false;
1230
+ try {
1231
+ const resolvedEndpoint = await resolveStdioHttpEndpoint(config, options, resolvedConfigPath);
1232
+ ownedRuntimeServerId = resolvedEndpoint.ownedRuntimeServerId;
1233
+ const { endpoint } = resolvedEndpoint;
1234
+ await startServer(new require_src.StdioHttpTransportHandler({ endpoint }, createStdioSafeLogger()), async () => {
1235
+ if (keepAlive || !ownedRuntimeServerId) return;
1236
+ await new require_src.StopServerService().stop({
1237
+ serverId: ownedRuntimeServerId,
1238
+ force: true
1239
+ });
1240
+ });
1241
+ } catch (error) {
1242
+ if (!keepAlive && ownedRuntimeServerId) {
1243
+ const stopServerService = new require_src.StopServerService();
1244
+ try {
1245
+ await stopServerService.stop({
1246
+ serverId: ownedRuntimeServerId,
1247
+ force: true
1248
+ });
1249
+ } catch (cleanupError) {
1250
+ throw new Error(`Failed to start stdio-http transport: ${toErrorMessage$3(error)}; also failed to stop owned HTTP runtime '${ownedRuntimeServerId}': ${toErrorMessage$3(cleanupError)}`);
1499
1251
  }
1500
- });
1252
+ }
1253
+ throw new Error(`Failed to start stdio-http transport: ${toErrorMessage$3(error)}`);
1254
+ }
1255
+ }
1256
+ async function startTransport(transportType, options, resolvedConfigPath, serverOptions, proxyDefaults) {
1257
+ try {
1258
+ if (transportType === TRANSPORT_TYPE_STDIO) {
1259
+ await startStdioTransport(serverOptions);
1260
+ return;
1261
+ }
1262
+ if (transportType === TRANSPORT_TYPE_HTTP) {
1263
+ await createAndStartHttpRuntime(serverOptions, createTransportConfig(options, require_src.TRANSPORT_MODE.HTTP, proxyDefaults), resolvedConfigPath);
1264
+ return;
1265
+ }
1266
+ if (transportType === TRANSPORT_TYPE_SSE) {
1267
+ await startSseTransport(serverOptions, createTransportConfig(options, require_src.TRANSPORT_MODE.SSE, proxyDefaults));
1268
+ return;
1269
+ }
1270
+ await startStdioHttpTransport(createTransportConfig(options, require_src.TRANSPORT_MODE.HTTP, proxyDefaults), options, resolvedConfigPath, proxyDefaults);
1271
+ } catch (error) {
1272
+ throw new Error(`Failed to start transport '${transportType}': ${toErrorMessage$3(error)}`);
1273
+ }
1274
+ }
1275
+ /**
1276
+ * MCP Serve command
1277
+ */
1278
+ const mcpServeCommand = new commander.Command("mcp-serve").description("Start MCP server with specified transport").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)").action(async (options) => {
1279
+ try {
1280
+ const resolvedConfigPath = options.config || await findConfigFileAsync() || void 0;
1281
+ const proxyDefaults = resolvedConfigPath ? loadProxyDefaults(resolvedConfigPath) : {};
1282
+ const transportType = validateTransportType((options.type ?? proxyDefaults.type ?? TRANSPORT_TYPE_STDIO).toLowerCase());
1283
+ validateProxyMode(options.proxyMode);
1284
+ await startTransport(transportType, options, resolvedConfigPath, createServerOptions(options, resolvedConfigPath, await resolveServerId(options, resolvedConfigPath)), proxyDefaults);
1501
1285
  } catch (error) {
1502
- console.error(`Error executing get-prompt: ${toErrorMessage$1(error)}`);
1286
+ const rawTransportType = (options.type ?? TRANSPORT_TYPE_STDIO).toLowerCase();
1287
+ const transportType = isValidTransportType(rawTransportType) ? rawTransportType : TRANSPORT_TYPE_STDIO;
1288
+ const envPort = process.env.MCP_PORT ? Number(process.env.MCP_PORT) : void 0;
1289
+ const requestedPort = options.port ?? (Number.isFinite(envPort) ? envPort : void 0);
1290
+ console.error(formatStartError(transportType, options.host ?? DEFAULT_HOST, requestedPort, error));
1503
1291
  process.exit(1);
1504
1292
  }
1505
1293
  });
@@ -1630,6 +1418,89 @@ const prefetchCommand = new commander.Command("prefetch").description("Pre-downl
1630
1418
  }
1631
1419
  });
1632
1420
  //#endregion
1421
+ //#region src/commands/read-resource.ts
1422
+ /**
1423
+ * ReadResource Command
1424
+ *
1425
+ * DESIGN PATTERNS:
1426
+ * - Command pattern with Commander for CLI argument parsing
1427
+ * - Async/await pattern for asynchronous operations
1428
+ * - Error handling pattern with try-catch and proper exit codes
1429
+ *
1430
+ * CODING STANDARDS:
1431
+ * - Use async action handlers for asynchronous operations
1432
+ * - Provide clear option descriptions and default values
1433
+ * - Handle errors gracefully with process.exit()
1434
+ * - Log progress and errors to console
1435
+ * - Use Commander's .option() and .argument() for inputs
1436
+ *
1437
+ * AVOID:
1438
+ * - Synchronous blocking operations in action handlers
1439
+ * - Missing error handling (always use try-catch)
1440
+ * - Hardcoded values (use options or environment variables)
1441
+ * - Not exiting with appropriate exit codes on errors
1442
+ */
1443
+ function toErrorMessage$2(error) {
1444
+ return error instanceof Error ? error.message : String(error);
1445
+ }
1446
+ /**
1447
+ * Read a resource by URI from a connected MCP server
1448
+ */
1449
+ 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) => {
1450
+ try {
1451
+ await withConnectedCommandContext(options, async ({ clientManager }) => {
1452
+ const clients = clientManager.getAllClients();
1453
+ if (options.server) {
1454
+ const client = clientManager.getClient(options.server);
1455
+ if (!client) throw new Error(`Server "${options.server}" not found`);
1456
+ if (!options.json) console.error(`Reading ${uri} from ${options.server}...`);
1457
+ const result = await client.readResource(uri);
1458
+ if (options.json) console.log(JSON.stringify(result, null, 2));
1459
+ else for (const content of result.contents) if ("text" in content) console.log(content.text);
1460
+ else console.log(JSON.stringify(content, null, 2));
1461
+ return;
1462
+ }
1463
+ const searchResults = await Promise.all(clients.map(async (client) => {
1464
+ try {
1465
+ const hasResource = (await client.listResources()).some((r) => r.uri === uri);
1466
+ return {
1467
+ serverName: client.serverName,
1468
+ hasResource,
1469
+ error: null
1470
+ };
1471
+ } catch (error) {
1472
+ return {
1473
+ serverName: client.serverName,
1474
+ hasResource: false,
1475
+ error
1476
+ };
1477
+ }
1478
+ }));
1479
+ const matchingServers = [];
1480
+ for (const { serverName, hasResource, error } of searchResults) {
1481
+ if (error) {
1482
+ console.error(`Failed to list resources from ${serverName}: ${toErrorMessage$2(error)}`);
1483
+ continue;
1484
+ }
1485
+ if (hasResource) matchingServers.push(serverName);
1486
+ }
1487
+ if (matchingServers.length === 0) throw new Error(`Resource "${uri}" not found on any connected server`);
1488
+ if (matchingServers.length > 1) throw new Error(`Resource "${uri}" found on multiple servers: ${matchingServers.join(", ")}. Use --server to disambiguate`);
1489
+ const targetServer = matchingServers[0];
1490
+ const client = clientManager.getClient(targetServer);
1491
+ if (!client) throw new Error(`Internal error: Server "${targetServer}" not connected`);
1492
+ if (!options.json) console.error(`Reading ${uri} from ${targetServer}...`);
1493
+ const result = await client.readResource(uri);
1494
+ if (options.json) console.log(JSON.stringify(result, null, 2));
1495
+ else for (const content of result.contents) if ("text" in content) console.log(content.text);
1496
+ else console.log(JSON.stringify(content, null, 2));
1497
+ });
1498
+ } catch (error) {
1499
+ console.error(`Error executing read-resource: ${toErrorMessage$2(error)}`);
1500
+ process.exit(1);
1501
+ }
1502
+ });
1503
+ //#endregion
1633
1504
  //#region src/commands/stop.ts
1634
1505
  /**
1635
1506
  * Stop Command
@@ -1637,7 +1508,7 @@ const prefetchCommand = new commander.Command("prefetch").description("Pre-downl
1637
1508
  * Stops a running HTTP mcp-proxy server using the authenticated admin endpoint
1638
1509
  * and the persisted runtime registry.
1639
1510
  */
1640
- function toErrorMessage(error) {
1511
+ function toErrorMessage$1(error) {
1641
1512
  return error instanceof Error ? error.message : String(error);
1642
1513
  }
1643
1514
  function printStopResult(result) {
@@ -1665,7 +1536,7 @@ const stopCommand = new commander.Command("stop").description("Stop a running HT
1665
1536
  }
1666
1537
  printStopResult(result);
1667
1538
  } catch (error) {
1668
- const errorMessage = `Error executing stop: ${toErrorMessage(error)}`;
1539
+ const errorMessage = `Error executing stop: ${toErrorMessage$1(error)}`;
1669
1540
  if (options.json) console.log(JSON.stringify({
1670
1541
  ok: false,
1671
1542
  error: errorMessage
@@ -1675,6 +1546,135 @@ const stopCommand = new commander.Command("stop").description("Stop a running HT
1675
1546
  }
1676
1547
  });
1677
1548
  //#endregion
1549
+ //#region src/commands/use-tool.ts
1550
+ /**
1551
+ * Use Tool Command
1552
+ *
1553
+ * DESIGN PATTERNS:
1554
+ * - Command pattern with Commander for CLI argument parsing
1555
+ * - Async/await pattern for asynchronous operations
1556
+ * - Error handling pattern with try-catch and proper exit codes
1557
+ *
1558
+ * CODING STANDARDS:
1559
+ * - Use async action handlers for asynchronous operations
1560
+ * - Provide clear option descriptions and default values
1561
+ * - Handle errors gracefully with process.exit()
1562
+ * - Log progress and errors to console
1563
+ * - Use Commander'"'"'s .option() and .argument() for inputs
1564
+ *
1565
+ * AVOID:
1566
+ * - Synchronous blocking operations in action handlers
1567
+ * - Missing error handling (always use try-catch)
1568
+ * - Hardcoded values (use options or environment variables)
1569
+ * - Not exiting with appropriate exit codes on errors
1570
+ */
1571
+ function toErrorMessage(error) {
1572
+ return error instanceof Error ? error.message : String(error);
1573
+ }
1574
+ /**
1575
+ * Execute an MCP tool with arguments
1576
+ */
1577
+ 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) => {
1578
+ try {
1579
+ let toolArgs = {};
1580
+ try {
1581
+ toolArgs = JSON.parse(options.args);
1582
+ } catch {
1583
+ console.error("Error: Invalid JSON in --args");
1584
+ process.exit(1);
1585
+ }
1586
+ await withConnectedCommandContext(options, async ({ container, config, clientManager }) => {
1587
+ const clients = clientManager.getAllClients();
1588
+ if (options.server) {
1589
+ const client = clientManager.getClient(options.server);
1590
+ if (!client) throw new Error(`Server "${options.server}" not found`);
1591
+ if (!options.json) console.error(`Executing ${toolName} on ${options.server}...`);
1592
+ const requestOptions = options.timeout ? { timeout: options.timeout } : void 0;
1593
+ const result = await client.callTool(toolName, toolArgs, requestOptions);
1594
+ if (options.json) console.log(JSON.stringify(result, null, 2));
1595
+ else {
1596
+ console.log("\nResult:");
1597
+ if (result.content) for (const content of result.content) if (content.type === "text") console.log(content.text);
1598
+ else console.log(JSON.stringify(content, null, 2));
1599
+ if (result.isError) {
1600
+ console.error("\n⚠️ Tool execution returned an error");
1601
+ process.exit(1);
1602
+ }
1603
+ }
1604
+ return;
1605
+ }
1606
+ const searchResults = await Promise.all(clients.map(async (client) => {
1607
+ try {
1608
+ const hasTool = (await client.listTools()).some((t) => t.name === toolName);
1609
+ return {
1610
+ serverName: client.serverName,
1611
+ hasTool,
1612
+ error: null
1613
+ };
1614
+ } catch (error) {
1615
+ return {
1616
+ serverName: client.serverName,
1617
+ hasTool: false,
1618
+ error
1619
+ };
1620
+ }
1621
+ }));
1622
+ const matchingServers = [];
1623
+ for (const { serverName, hasTool, error } of searchResults) {
1624
+ if (error) {
1625
+ if (!options.json) console.error(`Failed to list tools from ${serverName}:`, error);
1626
+ continue;
1627
+ }
1628
+ if (hasTool) matchingServers.push(serverName);
1629
+ }
1630
+ if (matchingServers.length === 0) {
1631
+ const skillPaths = config.skills?.paths || [];
1632
+ if (skillPaths.length > 0) try {
1633
+ const cwd = process.env.PROJECT_PATH || process.cwd();
1634
+ const skillService = container.createSkillService(cwd, skillPaths);
1635
+ const skillName = toolName.startsWith("skill__") ? toolName.slice(7) : toolName;
1636
+ const skill = await skillService.getSkill(skillName);
1637
+ if (skill) {
1638
+ const result = { content: [{
1639
+ type: "text",
1640
+ text: skill.content
1641
+ }] };
1642
+ if (options.json) console.log(JSON.stringify(result, null, 2));
1643
+ else {
1644
+ console.log("\nSkill content:");
1645
+ console.log(skill.content);
1646
+ }
1647
+ return;
1648
+ }
1649
+ } catch (error) {
1650
+ if (!options.json) console.error(`Failed to lookup skill "${toolName}":`, error);
1651
+ }
1652
+ throw new Error(`Tool or skill "${toolName}" not found on any connected server or configured skill paths`);
1653
+ }
1654
+ if (matchingServers.length > 1) throw new Error(`Tool "${toolName}" found on multiple servers: ${matchingServers.join(", ")}`);
1655
+ const targetServer = matchingServers[0];
1656
+ const client = clientManager.getClient(targetServer);
1657
+ if (!client) throw new Error(`Internal error: Server "${targetServer}" not connected`);
1658
+ if (!options.json) console.error(`Executing ${toolName} on ${targetServer}...`);
1659
+ const requestOptions = options.timeout ? { timeout: options.timeout } : void 0;
1660
+ const result = await client.callTool(toolName, toolArgs, requestOptions);
1661
+ if (options.json) console.log(JSON.stringify(result, null, 2));
1662
+ else {
1663
+ console.log("\nResult:");
1664
+ if (result.content) for (const content of result.content) if (content.type === "text") console.log(content.text);
1665
+ else console.log(JSON.stringify(content, null, 2));
1666
+ if (result.isError) {
1667
+ console.error("\n⚠️ Tool execution returned an error");
1668
+ process.exit(1);
1669
+ }
1670
+ }
1671
+ });
1672
+ } catch (error) {
1673
+ console.error(`Error executing use-tool: ${toErrorMessage(error)}`);
1674
+ process.exit(1);
1675
+ }
1676
+ });
1677
+ //#endregion
1678
1678
  //#region src/cli.ts
1679
1679
  /**
1680
1680
  * MCP Server Entry Point