@agimon-ai/mcp-proxy 0.5.0 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.cjs CHANGED
@@ -1,17 +1,27 @@
1
1
  #!/usr/bin/env node
2
- const require_src = require('./src-B2m53VQ1.cjs');
3
- let node_crypto = require("node:crypto");
2
+ const require_src = require('./src-DUR0uWiY.cjs');
3
+ let node_fs = require("node:fs");
4
4
  let node_fs_promises = require("node:fs/promises");
5
+ let js_yaml = require("js-yaml");
6
+ js_yaml = require_src.__toESM(js_yaml);
7
+ let node_crypto = require("node:crypto");
5
8
  let node_path = require("node:path");
6
9
  node_path = require_src.__toESM(node_path);
7
- let node_fs = require("node:fs");
8
- let liquidjs = require("liquidjs");
9
10
  let node_child_process = require("node:child_process");
11
+ let liquidjs = require("liquidjs");
10
12
  let commander = require("commander");
11
13
  let __agimon_ai_foundation_port_registry = require("@agimon-ai/foundation-port-registry");
12
14
  let __agimon_ai_foundation_process_registry = require("@agimon-ai/foundation-process-registry");
13
15
  let node_url = require("node:url");
14
16
 
17
+ //#region src/templates/mcp-config.json?raw
18
+ 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";
19
+
20
+ //#endregion
21
+ //#region src/templates/mcp-config.yaml.liquid?raw
22
+ 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";
23
+
24
+ //#endregion
15
25
  //#region src/utils/output.ts
16
26
  function writeLine(message = "") {
17
27
  console.log(message);
@@ -35,14 +45,6 @@ const print = {
35
45
  indent: (message) => writeLine(` ${message}`)
36
46
  };
37
47
 
38
- //#endregion
39
- //#region src/templates/mcp-config.yaml.liquid?raw
40
- 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";
41
-
42
- //#endregion
43
- //#region src/templates/mcp-config.json?raw
44
- 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";
45
-
46
48
  //#endregion
47
49
  //#region src/commands/init.ts
48
50
  /**
@@ -138,11 +140,26 @@ function resolveWorkspaceRoot(startPath = process.env.PROJECT_PATH || process.cw
138
140
  current = parent;
139
141
  }
140
142
  }
141
- function resolveSiblingRegistryPath(registryPath, fileName) {
142
- if (!registryPath) return;
143
- const resolved = node_path.default.resolve(registryPath);
144
- if (node_path.default.extname(resolved) === ".json") return node_path.default.join(node_path.default.dirname(resolved), fileName);
145
- return node_path.default.join(resolved, fileName);
143
+ const PROCESS_REGISTRY_SERVICE_HTTP$1 = "mcp-proxy-http";
144
+ async function findExistingHealthyRuntime(workspaceRoot) {
145
+ const match = (await new __agimon_ai_foundation_process_registry.ProcessRegistryService(process.env.PROCESS_REGISTRY_PATH).listProcesses({
146
+ repositoryPath: workspaceRoot,
147
+ serviceName: PROCESS_REGISTRY_SERVICE_HTTP$1
148
+ }))[0];
149
+ if (!match?.host || !match?.port) return null;
150
+ try {
151
+ const healthUrl = `http://${match.host}:${match.port}/health`;
152
+ if ((await fetch(healthUrl)).ok) {
153
+ const metadata = match.metadata;
154
+ return {
155
+ host: match.host,
156
+ port: match.port,
157
+ serverId: metadata?.serverId ?? "unknown",
158
+ workspaceRoot
159
+ };
160
+ }
161
+ } catch {}
162
+ return null;
146
163
  }
147
164
  function buildCliCandidates() {
148
165
  const __filename$1 = (0, node_url.fileURLToPath)(require("url").pathToFileURL(__filename).href);
@@ -217,16 +234,38 @@ function spawnBackgroundRuntime(args, env, cwd) {
217
234
  child.unref();
218
235
  return child;
219
236
  }
237
+ async function stopExistingRuntime(runtimeStateService, serverId, host, port) {
238
+ const runtimes = await runtimeStateService.list();
239
+ const targetHost = host || DEFAULT_HOST$1;
240
+ const match = runtimes.find((r) => {
241
+ if (serverId && r.serverId === serverId) return true;
242
+ if (port !== void 0 && r.host === targetHost && r.port === port) return true;
243
+ return false;
244
+ });
245
+ if (!match) return;
246
+ const stopService = new require_src.StopServerService(runtimeStateService);
247
+ try {
248
+ await stopService.stop({
249
+ serverId: match.serverId,
250
+ force: true
251
+ });
252
+ } catch {
253
+ await runtimeStateService.remove(match.serverId);
254
+ }
255
+ }
220
256
  async function prestartHttpRuntime(options) {
221
257
  const serverId = options.id || require_src.generateServerId();
222
258
  const timeoutMs = parseTimeoutMs(options.timeoutMs);
223
259
  const registryPath = options.registryPath || options.registryDir;
224
260
  const workspaceRoot = resolveWorkspaceRoot();
261
+ const existing = await findExistingHealthyRuntime(workspaceRoot);
262
+ if (existing) return existing;
263
+ await stopExistingRuntime(new require_src.RuntimeStateService(), options.id, options.host, options.port);
225
264
  const childEnv = {
226
265
  ...process.env,
227
266
  ...registryPath ? {
228
267
  PORT_REGISTRY_PATH: registryPath,
229
- PROCESS_REGISTRY_PATH: resolveSiblingRegistryPath(registryPath, "processes.json")
268
+ PROCESS_REGISTRY_PATH: (0, __agimon_ai_foundation_process_registry.resolveSiblingRegistryPath)(registryPath, "processes.json")
230
269
  } : {}
231
270
  };
232
271
  const child = spawnBackgroundRuntime([
@@ -261,7 +300,7 @@ async function prestartHttpRuntime(options) {
261
300
  workspaceRoot
262
301
  };
263
302
  } catch (error) {
264
- throw new Error(`Failed to prestart HTTP runtime '${serverId}': ${error instanceof Error ? error.message : String(error)}`);
303
+ throw new Error(`Failed to prestart HTTP runtime '${serverId}': ${error instanceof Error ? error.message : String(error)}`, { cause: error });
265
304
  }
266
305
  }
267
306
  const prestartHttpCommand = new commander.Command("prestart-http").description("Start an mcp-proxy HTTP runtime in the background and wait until it is healthy").option("--id <id>", "Server identifier to assign to the runtime").option("--host <host>", "Host to bind to", DEFAULT_HOST$1).option("-p, --port <port>", "Preferred HTTP port for the runtime", (value) => Number.parseInt(value, 10)).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("--registry-path <path>", "Custom registry path or directory for service discovery").option("--registry-dir <path>", "Custom registry directory for service discovery").option("--timeout-ms <ms>", "How long to wait for the runtime to become healthy", String(DEFAULT_TIMEOUT_MS)).action(async (options) => {
@@ -272,7 +311,7 @@ const prestartHttpCommand = new commander.Command("prestart-http").description("
272
311
  process.stdout.write(`runtimeUrl=http://${host}:${port}\n`);
273
312
  process.stdout.write(`workspaceRoot=${workspaceRoot}\n`);
274
313
  } catch (error) {
275
- throw new Error(`Failed to prestart HTTP runtime '${options.id || "generated-server-id"}': ${error instanceof Error ? error.message : String(error)}`);
314
+ throw new Error(`Failed to prestart HTTP runtime '${options.id || "generated-server-id"}': ${error instanceof Error ? error.message : String(error)}`, { cause: error });
276
315
  }
277
316
  });
278
317
 
@@ -344,16 +383,37 @@ async function findConfigFileAsync() {
344
383
  const configPath = (0, node_path.resolve)(projectPath, fileName);
345
384
  if (await pathExists(configPath)) return configPath;
346
385
  }
347
- const cwd = process.cwd();
348
- for (const fileName of CONFIG_FILE_NAMES) {
349
- const configPath = (0, node_path.join)(cwd, fileName);
350
- if (await pathExists(configPath)) return configPath;
386
+ const MAX_PARENT_LEVELS = 3;
387
+ let searchDir = process.cwd();
388
+ for (let level = 0; level <= MAX_PARENT_LEVELS; level++) {
389
+ for (const fileName of CONFIG_FILE_NAMES) {
390
+ const configPath = (0, node_path.join)(searchDir, fileName);
391
+ if (await pathExists(configPath)) return configPath;
392
+ }
393
+ const parentDir = (0, node_path.dirname)(searchDir);
394
+ if (parentDir === searchDir) break;
395
+ searchDir = parentDir;
351
396
  }
352
397
  return null;
353
398
  } catch (error) {
354
399
  throw new Error(`Failed to discover MCP config file: ${toErrorMessage$9(error)}`);
355
400
  }
356
401
  }
402
+ function loadProxyDefaults(configPath) {
403
+ try {
404
+ const content = (0, node_fs.readFileSync)(configPath, "utf-8");
405
+ const proxy = (configPath.endsWith(".yaml") || configPath.endsWith(".yml") ? js_yaml.default.load(content) : JSON.parse(content))?.proxy;
406
+ if (!proxy || typeof proxy !== "object") return {};
407
+ const p = proxy;
408
+ return {
409
+ type: typeof p.type === "string" ? p.type : void 0,
410
+ port: typeof p.port === "number" && Number.isInteger(p.port) && p.port > 0 ? p.port : void 0,
411
+ host: typeof p.host === "string" ? p.host : void 0
412
+ };
413
+ } catch {
414
+ return {};
415
+ }
416
+ }
357
417
  async function resolveServerId(options, resolvedConfigPath) {
358
418
  const container = require_src.createProxyIoCContainer();
359
419
  if (options.id) return options.id;
@@ -375,12 +435,12 @@ function validateTransportType(type) {
375
435
  function validateProxyMode(mode) {
376
436
  if (!isValidProxyMode(mode)) throw new Error(`Unknown proxy mode: '${mode}'. Valid options: meta, flat, search`);
377
437
  }
378
- function createTransportConfig(options, mode) {
438
+ function createTransportConfig(options, mode, proxyDefaults) {
379
439
  const envPort = process.env.MCP_PORT ? Number(process.env.MCP_PORT) : void 0;
380
440
  return {
381
441
  mode,
382
- port: options.port ?? (Number.isFinite(envPort) ? envPort : void 0),
383
- host: options.host || process.env.MCP_HOST || DEFAULT_HOST
442
+ port: options.port ?? (Number.isFinite(envPort) ? envPort : void 0) ?? proxyDefaults?.port,
443
+ host: options.host ?? process.env.MCP_HOST ?? proxyDefaults?.host ?? DEFAULT_HOST
384
444
  };
385
445
  }
386
446
  function createStdioSafeLogger() {
@@ -429,9 +489,6 @@ function createRuntimeRecord(serverId, config, port, shutdownToken, configPath)
429
489
  function createPortRegistryService() {
430
490
  return new __agimon_ai_foundation_port_registry.PortRegistryService(process.env.PORT_REGISTRY_PATH);
431
491
  }
432
- function createProcessRegistryService() {
433
- return new __agimon_ai_foundation_process_registry.ProcessRegistryService(process.env.PROCESS_REGISTRY_PATH);
434
- }
435
492
  function getRegistryEnvironment() {
436
493
  return process.env.NODE_ENV ?? "development";
437
494
  }
@@ -478,41 +535,6 @@ async function createPortRegistryLease(serviceName, host, preferredPort, serverI
478
535
  }
479
536
  };
480
537
  }
481
- async function createProcessRegistryLease(serviceName, host, port, serverId, transport, configPath) {
482
- const processRegistry = createProcessRegistryService();
483
- const result = await processRegistry.registerProcess({
484
- repositoryPath: getRegistryRepositoryPath(),
485
- serviceName,
486
- serviceType: PROCESS_REGISTRY_SERVICE_TYPE,
487
- environment: getRegistryEnvironment(),
488
- pid: process.pid,
489
- host,
490
- port,
491
- command: process.argv[1],
492
- args: process.argv.slice(2),
493
- metadata: {
494
- transport,
495
- serverId,
496
- ...configPath ? { configPath } : {}
497
- }
498
- });
499
- if (!result.success || !result.record) throw new Error(result.error || `Failed to register process for ${serviceName}`);
500
- let released = false;
501
- return { release: async (options) => {
502
- if (released) return;
503
- released = true;
504
- const releaseResult = await processRegistry.releaseProcess({
505
- repositoryPath: getRegistryRepositoryPath(),
506
- serviceName,
507
- serviceType: PROCESS_REGISTRY_SERVICE_TYPE,
508
- pid: process.pid,
509
- environment: getRegistryEnvironment(),
510
- kill: options?.kill ?? false,
511
- releasePort: options?.releasePort ?? false
512
- });
513
- if (!releaseResult.success && releaseResult.error && !releaseResult.error.includes("No matching process entry")) throw new Error(releaseResult.error || `Failed to release process for ${serviceName}`);
514
- } };
515
- }
516
538
  async function releasePortLease(lease) {
517
539
  if (!lease) return;
518
540
  await lease.release();
@@ -546,13 +568,19 @@ async function stopOwnedHttpTransport(handler, runtimeStateService, serverId, pr
546
568
  throw new Error(`Failed to stop owned HTTP transport '${serverId}': ${toErrorMessage$9(error)}`);
547
569
  }
548
570
  } finally {
549
- await processLease?.release({
550
- kill: false,
551
- releasePort: false
552
- });
571
+ await processLease?.release({ kill: false });
553
572
  await removeRuntimeRecord(runtimeStateService, serverId);
554
573
  }
555
574
  }
575
+ /**
576
+ * Run post-stop cleanup for an HTTP runtime (release port, dispose services, remove state).
577
+ * This is the subset of stopOwnedHttpTransport that runs AFTER handler.stop() has already
578
+ * been called by startServer()'s signal handler — avoids double-stopping the transport.
579
+ */
580
+ async function cleanupHttpRuntime(runtimeStateService, serverId, processLease) {
581
+ await processLease?.release({ kill: false });
582
+ await removeRuntimeRecord(runtimeStateService, serverId);
583
+ }
556
584
  async function cleanupFailedRuntimeStartup(handler, runtimeStateService, serverId, processLease) {
557
585
  try {
558
586
  try {
@@ -561,10 +589,7 @@ async function cleanupFailedRuntimeStartup(handler, runtimeStateService, serverI
561
589
  throw new Error(`Failed to stop HTTP transport during cleanup for '${serverId}': ${toErrorMessage$9(error)}`);
562
590
  }
563
591
  } finally {
564
- await processLease?.release({
565
- kill: false,
566
- releasePort: false
567
- });
592
+ await processLease?.release({ kill: false });
568
593
  await removeRuntimeRecord(runtimeStateService, serverId);
569
594
  }
570
595
  }
@@ -609,7 +634,21 @@ async function createAndStartHttpRuntime(serverOptions, config, resolvedConfigPa
609
634
  ...config,
610
635
  port: runtimePort
611
636
  };
612
- const processLease = await createProcessRegistryLease(PROCESS_REGISTRY_SERVICE_HTTP, runtimeConfig.host ?? DEFAULT_HOST, runtimePort, runtimeServerId, TRANSPORT_TYPE_HTTP, resolvedConfigPath);
637
+ const processLease = await (0, __agimon_ai_foundation_process_registry.createProcessLease)({
638
+ repositoryPath: getRegistryRepositoryPath(),
639
+ serviceName: PROCESS_REGISTRY_SERVICE_HTTP,
640
+ serviceType: PROCESS_REGISTRY_SERVICE_TYPE,
641
+ environment: getRegistryEnvironment(),
642
+ host: runtimeConfig.host ?? DEFAULT_HOST,
643
+ port: runtimePort,
644
+ command: process.argv[1],
645
+ args: process.argv.slice(2),
646
+ metadata: {
647
+ transport: TRANSPORT_TYPE_HTTP,
648
+ serverId: runtimeServerId,
649
+ ...resolvedConfigPath ? { configPath: resolvedConfigPath } : {}
650
+ }
651
+ });
613
652
  let releasePort = async () => {
614
653
  await releasePortLease(portLease ?? null);
615
654
  releasePort = async () => void 0;
@@ -633,10 +672,7 @@ async function createAndStartHttpRuntime(serverOptions, config, resolvedConfigPa
633
672
  handler = new require_src.HttpTransportHandler(() => require_src.createSessionServer(sharedServices), runtimeConfig, createHttpAdminOptions(runtimeRecord.serverId, shutdownToken, stopHandler));
634
673
  } catch (error) {
635
674
  await releasePort();
636
- await processLease.release({
637
- kill: false,
638
- releasePort: false
639
- });
675
+ await processLease.release({ kill: false });
640
676
  await sharedServices.dispose();
641
677
  throw new Error(`Failed to create HTTP runtime server: ${toErrorMessage$9(error)}`);
642
678
  }
@@ -644,19 +680,11 @@ async function createAndStartHttpRuntime(serverOptions, config, resolvedConfigPa
644
680
  await startServer(handler, async () => {
645
681
  await releasePort();
646
682
  await sharedServices.dispose();
647
- await processLease.release({
648
- kill: false,
649
- releasePort: false
650
- });
651
- await removeRuntimeRecord(runtimeStateService, runtimeRecord.serverId);
683
+ await cleanupHttpRuntime(runtimeStateService, runtimeRecord.serverId, processLease);
652
684
  });
653
685
  await writeRuntimeRecord(runtimeStateService, runtimeRecord);
654
686
  } catch (error) {
655
687
  await releasePort();
656
- await processLease.release({
657
- kill: false,
658
- releasePort: false
659
- });
660
688
  await sharedServices.dispose();
661
689
  await cleanupFailedRuntimeStartup(handler, runtimeStateService, runtimeRecord.serverId, processLease);
662
690
  throw new Error(`Failed to start HTTP runtime '${runtimeRecord.serverId}': ${toErrorMessage$9(error)}`);
@@ -672,7 +700,24 @@ async function startStdioTransport(serverOptions) {
672
700
  }
673
701
  async function startSseTransport(serverOptions, config) {
674
702
  try {
675
- await startServer(new require_src.SseTransportHandler(await require_src.createServer(serverOptions), config));
703
+ const requestedPort = config.port;
704
+ const portRange = requestedPort !== void 0 ? {
705
+ min: requestedPort,
706
+ max: requestedPort
707
+ } : __agimon_ai_foundation_port_registry.DEFAULT_PORT_RANGE;
708
+ const portLease = await createPortRegistryLease("mcp-proxy-sse", config.host ?? DEFAULT_HOST, requestedPort, serverOptions.serverId ?? require_src.generateServerId(), TRANSPORT_TYPE_SSE, void 0, portRange);
709
+ const resolvedConfig = {
710
+ ...config,
711
+ port: portLease.port
712
+ };
713
+ const handler = new require_src.SseTransportHandler(await require_src.createServer(serverOptions), resolvedConfig);
714
+ const shutdown = async () => {
715
+ await handler.stop();
716
+ await portLease.release();
717
+ };
718
+ process.on("SIGINT", shutdown);
719
+ process.on("SIGTERM", shutdown);
720
+ await startServer(handler);
676
721
  } catch (error) {
677
722
  throw new Error(`Failed to start SSE transport: ${toErrorMessage$9(error)}`);
678
723
  }
@@ -680,13 +725,28 @@ async function startSseTransport(serverOptions, config) {
680
725
  async function resolveStdioHttpEndpoint(config, options, resolvedConfigPath) {
681
726
  const repositoryPath = getRegistryRepositoryPath();
682
727
  if (config.port !== void 0) return new URL(`http://${config.host ?? DEFAULT_HOST}:${config.port}${MCP_ENDPOINT_PATH}`);
683
- const result = await createPortRegistryService().getPort({
728
+ const portRegistry = createPortRegistryService();
729
+ const result = await portRegistry.getPort({
684
730
  repositoryPath,
685
731
  serviceName: PORT_REGISTRY_SERVICE_HTTP,
686
732
  serviceType: PORT_REGISTRY_SERVICE_TYPE,
687
733
  environment: getRegistryEnvironment()
688
734
  });
689
- if (result.success && result.record) return new URL(`http://${config.host ?? result.record.host}:${result.record.port}${MCP_ENDPOINT_PATH}`);
735
+ if (result.success && result.record) {
736
+ const host = config.host ?? result.record.host;
737
+ const endpoint = new URL(`http://${host}:${result.record.port}${MCP_ENDPOINT_PATH}`);
738
+ try {
739
+ const healthUrl = `http://${host}:${result.record.port}/health`;
740
+ if ((await fetch(healthUrl)).ok) return endpoint;
741
+ } catch {}
742
+ await portRegistry.releasePort({
743
+ repositoryPath,
744
+ serviceName: PORT_REGISTRY_SERVICE_HTTP,
745
+ serviceType: PORT_REGISTRY_SERVICE_TYPE,
746
+ environment: getRegistryEnvironment(),
747
+ force: true
748
+ });
749
+ }
690
750
  const runtime = await prestartHttpRuntime({
691
751
  host: config.host ?? DEFAULT_HOST,
692
752
  config: options.config || resolvedConfigPath,
@@ -704,21 +764,21 @@ async function startStdioHttpTransport(config, options, resolvedConfigPath) {
704
764
  throw new Error(`Failed to start stdio-http transport: ${toErrorMessage$9(error)}`);
705
765
  }
706
766
  }
707
- async function startTransport(transportType, options, resolvedConfigPath, serverOptions) {
767
+ async function startTransport(transportType, options, resolvedConfigPath, serverOptions, proxyDefaults) {
708
768
  try {
709
769
  if (transportType === TRANSPORT_TYPE_STDIO) {
710
770
  await startStdioTransport(serverOptions);
711
771
  return;
712
772
  }
713
773
  if (transportType === TRANSPORT_TYPE_HTTP) {
714
- await createAndStartHttpRuntime(serverOptions, createTransportConfig(options, require_src.TRANSPORT_MODE.HTTP), resolvedConfigPath);
774
+ await createAndStartHttpRuntime(serverOptions, createTransportConfig(options, require_src.TRANSPORT_MODE.HTTP, proxyDefaults), resolvedConfigPath);
715
775
  return;
716
776
  }
717
777
  if (transportType === TRANSPORT_TYPE_SSE) {
718
- await startSseTransport(serverOptions, createTransportConfig(options, require_src.TRANSPORT_MODE.SSE));
778
+ await startSseTransport(serverOptions, createTransportConfig(options, require_src.TRANSPORT_MODE.SSE, proxyDefaults));
719
779
  return;
720
780
  }
721
- await startStdioHttpTransport(createTransportConfig(options, require_src.TRANSPORT_MODE.HTTP), options, resolvedConfigPath);
781
+ await startStdioHttpTransport(createTransportConfig(options, require_src.TRANSPORT_MODE.HTTP, proxyDefaults), options, resolvedConfigPath);
722
782
  } catch (error) {
723
783
  throw new Error(`Failed to start transport '${transportType}': ${toErrorMessage$9(error)}`);
724
784
  }
@@ -726,18 +786,19 @@ async function startTransport(transportType, options, resolvedConfigPath, server
726
786
  /**
727
787
  * MCP Serve command
728
788
  */
729
- const mcpServeCommand = new commander.Command("mcp-serve").description("Start MCP server with specified transport").option("-t, --type <type>", `Transport type: ${TRANSPORT_TYPE_STDIO}, ${TRANSPORT_TYPE_HTTP}, ${TRANSPORT_TYPE_SSE}, or ${TRANSPORT_TYPE_STDIO_HTTP}`, TRANSPORT_TYPE_STDIO).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", DEFAULT_HOST).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) => {
789
+ const mcpServeCommand = new commander.Command("mcp-serve").description("Start MCP server with specified transport").option("-t, --type <type>", `Transport type: ${TRANSPORT_TYPE_STDIO}, ${TRANSPORT_TYPE_HTTP}, ${TRANSPORT_TYPE_SSE}, or ${TRANSPORT_TYPE_STDIO_HTTP}`).option("-p, --port <port>", "Port to listen on (http/sse) or backend port for stdio-http", (val) => Number.parseInt(val, 10)).option("--host <host>", "Host to bind to (http/sse) or backend host for stdio-http").option("-c, --config <path>", "Path to MCP server configuration file").option("--no-cache", "Disable configuration caching, always reload from config file").option("--definitions-cache <path>", "Path to prefetched tool/prompt/skill definitions cache file").option("--clear-definitions-cache", "Delete definitions cache before startup", false).option("--proxy-mode <mode>", "How mcp-proxy exposes downstream tools: meta, flat, or search", "meta").option("--id <id>", "Unique server identifier (overrides config file id, auto-generated if not provided)").action(async (options) => {
730
790
  try {
731
- const transportType = validateTransportType(options.type.toLowerCase());
732
- validateProxyMode(options.proxyMode);
733
791
  const resolvedConfigPath = options.config || await findConfigFileAsync() || void 0;
734
- await startTransport(transportType, options, resolvedConfigPath, createServerOptions(options, resolvedConfigPath, await resolveServerId(options, resolvedConfigPath)));
792
+ const proxyDefaults = resolvedConfigPath ? loadProxyDefaults(resolvedConfigPath) : {};
793
+ const transportType = validateTransportType((options.type ?? proxyDefaults.type ?? TRANSPORT_TYPE_STDIO).toLowerCase());
794
+ validateProxyMode(options.proxyMode);
795
+ await startTransport(transportType, options, resolvedConfigPath, createServerOptions(options, resolvedConfigPath, await resolveServerId(options, resolvedConfigPath)), proxyDefaults);
735
796
  } catch (error) {
736
- const rawTransportType = options.type.toLowerCase();
797
+ const rawTransportType = (options.type ?? TRANSPORT_TYPE_STDIO).toLowerCase();
737
798
  const transportType = isValidTransportType(rawTransportType) ? rawTransportType : TRANSPORT_TYPE_STDIO;
738
799
  const envPort = process.env.MCP_PORT ? Number(process.env.MCP_PORT) : void 0;
739
800
  const requestedPort = options.port ?? (Number.isFinite(envPort) ? envPort : void 0);
740
- console.error(formatStartError(transportType, options.host, requestedPort, error));
801
+ console.error(formatStartError(transportType, options.host ?? DEFAULT_HOST, requestedPort, error));
741
802
  process.exit(1);
742
803
  }
743
804
  });
@@ -747,14 +808,58 @@ const mcpServeCommand = new commander.Command("mcp-serve").description("Start MC
747
808
  function toErrorMessage$8(error) {
748
809
  return error instanceof Error ? error.message : String(error);
749
810
  }
750
- async function withConnectedCommandContext(options, run) {
751
- const container = require_src.createProxyIoCContainer();
752
- const configFilePath = options.config || require_src.findConfigFile();
753
- if (!configFilePath) throw new Error("No config file found. Use --config or create mcp-config.yaml");
754
- const config = await container.createConfigFetcherService({
755
- configFilePath,
756
- useCache: options.useCache
757
- }).fetchConfiguration();
811
+ async function checkHealth(host, port) {
812
+ try {
813
+ return (await fetch(`http://${host}:${port}/health`, { signal: AbortSignal.timeout(3e3) })).ok;
814
+ } catch {
815
+ return false;
816
+ }
817
+ }
818
+ /**
819
+ * Proxy mode: connect to a running HTTP server instead of downstream servers directly.
820
+ * Auto-starts the server if not running.
821
+ */
822
+ async function withProxiedContext(container, config, configFilePath, options, run) {
823
+ const host = config.proxy?.host ?? "localhost";
824
+ const port = config.proxy?.port;
825
+ const endpoint = `http://${host}:${port}/mcp`;
826
+ if (!await checkHealth(host, port)) {
827
+ if (!options.json) console.error("Starting HTTP proxy server in background...");
828
+ await prestartHttpRuntime({
829
+ host,
830
+ port,
831
+ config: configFilePath,
832
+ cache: options.useCache !== false,
833
+ clearDefinitionsCache: false,
834
+ proxyMode: "flat"
835
+ });
836
+ }
837
+ const clientManager = container.createClientManagerService();
838
+ try {
839
+ await clientManager.connectToServer("proxy", {
840
+ name: "proxy",
841
+ transport: "http",
842
+ config: { url: endpoint }
843
+ });
844
+ if (!options.json) console.error(`✓ Connected to proxy at ${endpoint}`);
845
+ } catch (error) {
846
+ throw new Error(`Failed to connect to proxy server at ${endpoint}: ${toErrorMessage$8(error)}`);
847
+ }
848
+ try {
849
+ return await run({
850
+ container,
851
+ configFilePath,
852
+ config,
853
+ clientManager
854
+ });
855
+ } finally {
856
+ await clientManager.disconnectAll();
857
+ }
858
+ }
859
+ /**
860
+ * Direct mode: connect to all downstream MCP servers individually.
861
+ */
862
+ async function withDirectContext(container, config, configFilePath, options, run) {
758
863
  const clientManager = container.createClientManagerService();
759
864
  await Promise.all(Object.entries(config.mcpServers).map(async ([serverName, serverConfig]) => {
760
865
  try {
@@ -776,6 +881,17 @@ async function withConnectedCommandContext(options, run) {
776
881
  await clientManager.disconnectAll();
777
882
  }
778
883
  }
884
+ async function withConnectedCommandContext(options, run) {
885
+ const container = require_src.createProxyIoCContainer();
886
+ const configFilePath = options.config || require_src.findConfigFile();
887
+ if (!configFilePath) throw new Error("No config file found. Use --config or create mcp-config.yaml");
888
+ const config = await container.createConfigFetcherService({
889
+ configFilePath,
890
+ useCache: options.useCache
891
+ }).fetchConfiguration();
892
+ if (config.proxy?.port) return await withProxiedContext(container, config, configFilePath, options, run);
893
+ return await withDirectContext(container, config, configFilePath, options, run);
894
+ }
779
895
 
780
896
  //#endregion
781
897
  //#region src/commands/list-tools.ts
@@ -1018,7 +1134,7 @@ function toErrorMessage$5(error) {
1018
1134
  /**
1019
1135
  * Execute an MCP tool with arguments
1020
1136
  */
1021
- const useToolCommand = new commander.Command("use-tool").description("Execute an MCP tool with arguments").argument("<toolName>", "Tool name to execute").option("-c, --config <path>", "Path to MCP server configuration file").option("-s, --server <name>", "Server name (required if tool exists on multiple servers)").option("-a, --args <json>", "Tool arguments as JSON string", "{}").option("-t, --timeout <ms>", "Request timeout in milliseconds for tool execution (default: 60000)", parseInt).option("-j, --json", "Output as JSON", false).action(async (toolName, options) => {
1137
+ const useToolCommand = new commander.Command("use-tool").description("Execute an MCP tool with arguments").argument("<toolName>", "Tool name to execute").option("-c, --config <path>", "Path to MCP server configuration file").option("-s, --server <name>", "Server name (required if tool exists on multiple servers)").option("-a, --args <json>", "Tool arguments as JSON string", "{}").option("-t, --timeout <ms>", "Request timeout in milliseconds for tool execution (default: 60000)", Number.parseInt).option("-j, --json", "Output as JSON", false).action(async (toolName, options) => {
1022
1138
  try {
1023
1139
  let toolArgs = {};
1024
1140
  try {