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