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