@agimon-ai/mcp-proxy 0.4.11 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.cjs CHANGED
@@ -1,17 +1,27 @@
1
1
  #!/usr/bin/env node
2
- const require_src = require('./src-DwErnAUn.cjs');
3
- let node_crypto = require("node:crypto");
2
+ const require_src = require('./src-B5N-kt9Y.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
  /**
@@ -159,12 +161,6 @@ async function findExistingHealthyRuntime(workspaceRoot) {
159
161
  } catch {}
160
162
  return null;
161
163
  }
162
- function resolveSiblingRegistryPath(registryPath, fileName) {
163
- if (!registryPath) return;
164
- const resolved = node_path.default.resolve(registryPath);
165
- if (node_path.default.extname(resolved) === ".json") return node_path.default.join(node_path.default.dirname(resolved), fileName);
166
- return node_path.default.join(resolved, fileName);
167
- }
168
164
  function buildCliCandidates() {
169
165
  const __filename$1 = (0, node_url.fileURLToPath)(require("url").pathToFileURL(__filename).href);
170
166
  const __dirname$1 = node_path.default.dirname(__filename$1);
@@ -269,7 +265,7 @@ async function prestartHttpRuntime(options) {
269
265
  ...process.env,
270
266
  ...registryPath ? {
271
267
  PORT_REGISTRY_PATH: registryPath,
272
- PROCESS_REGISTRY_PATH: resolveSiblingRegistryPath(registryPath, "processes.json")
268
+ PROCESS_REGISTRY_PATH: (0, __agimon_ai_foundation_process_registry.resolveSiblingRegistryPath)(registryPath, "processes.json")
273
269
  } : {}
274
270
  };
275
271
  const child = spawnBackgroundRuntime([
@@ -387,16 +383,37 @@ async function findConfigFileAsync() {
387
383
  const configPath = (0, node_path.resolve)(projectPath, fileName);
388
384
  if (await pathExists(configPath)) return configPath;
389
385
  }
390
- const cwd = process.cwd();
391
- for (const fileName of CONFIG_FILE_NAMES) {
392
- const configPath = (0, node_path.join)(cwd, fileName);
393
- 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;
394
396
  }
395
397
  return null;
396
398
  } catch (error) {
397
399
  throw new Error(`Failed to discover MCP config file: ${toErrorMessage$9(error)}`);
398
400
  }
399
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
+ }
400
417
  async function resolveServerId(options, resolvedConfigPath) {
401
418
  const container = require_src.createProxyIoCContainer();
402
419
  if (options.id) return options.id;
@@ -418,12 +435,12 @@ function validateTransportType(type) {
418
435
  function validateProxyMode(mode) {
419
436
  if (!isValidProxyMode(mode)) throw new Error(`Unknown proxy mode: '${mode}'. Valid options: meta, flat, search`);
420
437
  }
421
- function createTransportConfig(options, mode) {
438
+ function createTransportConfig(options, mode, proxyDefaults) {
422
439
  const envPort = process.env.MCP_PORT ? Number(process.env.MCP_PORT) : void 0;
423
440
  return {
424
441
  mode,
425
- port: options.port ?? (Number.isFinite(envPort) ? envPort : void 0),
426
- 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
427
444
  };
428
445
  }
429
446
  function createStdioSafeLogger() {
@@ -472,9 +489,6 @@ function createRuntimeRecord(serverId, config, port, shutdownToken, configPath)
472
489
  function createPortRegistryService() {
473
490
  return new __agimon_ai_foundation_port_registry.PortRegistryService(process.env.PORT_REGISTRY_PATH);
474
491
  }
475
- function createProcessRegistryService() {
476
- return new __agimon_ai_foundation_process_registry.ProcessRegistryService(process.env.PROCESS_REGISTRY_PATH);
477
- }
478
492
  function getRegistryEnvironment() {
479
493
  return process.env.NODE_ENV ?? "development";
480
494
  }
@@ -521,41 +535,6 @@ async function createPortRegistryLease(serviceName, host, preferredPort, serverI
521
535
  }
522
536
  };
523
537
  }
524
- async function createProcessRegistryLease(serviceName, host, port, serverId, transport, configPath) {
525
- const processRegistry = createProcessRegistryService();
526
- const result = await processRegistry.registerProcess({
527
- repositoryPath: getRegistryRepositoryPath(),
528
- serviceName,
529
- serviceType: PROCESS_REGISTRY_SERVICE_TYPE,
530
- environment: getRegistryEnvironment(),
531
- pid: process.pid,
532
- host,
533
- port,
534
- command: process.argv[1],
535
- args: process.argv.slice(2),
536
- metadata: {
537
- transport,
538
- serverId,
539
- ...configPath ? { configPath } : {}
540
- }
541
- });
542
- if (!result.success || !result.record) throw new Error(result.error || `Failed to register process for ${serviceName}`);
543
- let released = false;
544
- return { release: async (options) => {
545
- if (released) return;
546
- released = true;
547
- const releaseResult = await processRegistry.releaseProcess({
548
- repositoryPath: getRegistryRepositoryPath(),
549
- serviceName,
550
- serviceType: PROCESS_REGISTRY_SERVICE_TYPE,
551
- pid: process.pid,
552
- environment: getRegistryEnvironment(),
553
- kill: options?.kill ?? false,
554
- releasePort: options?.releasePort ?? false
555
- });
556
- if (!releaseResult.success && releaseResult.error && !releaseResult.error.includes("No matching process entry")) throw new Error(releaseResult.error || `Failed to release process for ${serviceName}`);
557
- } };
558
- }
559
538
  async function releasePortLease(lease) {
560
539
  if (!lease) return;
561
540
  await lease.release();
@@ -589,13 +568,19 @@ async function stopOwnedHttpTransport(handler, runtimeStateService, serverId, pr
589
568
  throw new Error(`Failed to stop owned HTTP transport '${serverId}': ${toErrorMessage$9(error)}`);
590
569
  }
591
570
  } finally {
592
- await processLease?.release({
593
- kill: false,
594
- releasePort: false
595
- });
571
+ await processLease?.release({ kill: false });
596
572
  await removeRuntimeRecord(runtimeStateService, serverId);
597
573
  }
598
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
+ }
599
584
  async function cleanupFailedRuntimeStartup(handler, runtimeStateService, serverId, processLease) {
600
585
  try {
601
586
  try {
@@ -604,10 +589,7 @@ async function cleanupFailedRuntimeStartup(handler, runtimeStateService, serverI
604
589
  throw new Error(`Failed to stop HTTP transport during cleanup for '${serverId}': ${toErrorMessage$9(error)}`);
605
590
  }
606
591
  } finally {
607
- await processLease?.release({
608
- kill: false,
609
- releasePort: false
610
- });
592
+ await processLease?.release({ kill: false });
611
593
  await removeRuntimeRecord(runtimeStateService, serverId);
612
594
  }
613
595
  }
@@ -652,7 +634,21 @@ async function createAndStartHttpRuntime(serverOptions, config, resolvedConfigPa
652
634
  ...config,
653
635
  port: runtimePort
654
636
  };
655
- 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
+ });
656
652
  let releasePort = async () => {
657
653
  await releasePortLease(portLease ?? null);
658
654
  releasePort = async () => void 0;
@@ -676,10 +672,7 @@ async function createAndStartHttpRuntime(serverOptions, config, resolvedConfigPa
676
672
  handler = new require_src.HttpTransportHandler(() => require_src.createSessionServer(sharedServices), runtimeConfig, createHttpAdminOptions(runtimeRecord.serverId, shutdownToken, stopHandler));
677
673
  } catch (error) {
678
674
  await releasePort();
679
- await processLease.release({
680
- kill: false,
681
- releasePort: false
682
- });
675
+ await processLease.release({ kill: false });
683
676
  await sharedServices.dispose();
684
677
  throw new Error(`Failed to create HTTP runtime server: ${toErrorMessage$9(error)}`);
685
678
  }
@@ -687,19 +680,11 @@ async function createAndStartHttpRuntime(serverOptions, config, resolvedConfigPa
687
680
  await startServer(handler, async () => {
688
681
  await releasePort();
689
682
  await sharedServices.dispose();
690
- await processLease.release({
691
- kill: false,
692
- releasePort: false
693
- });
694
- await removeRuntimeRecord(runtimeStateService, runtimeRecord.serverId);
683
+ await cleanupHttpRuntime(runtimeStateService, runtimeRecord.serverId, processLease);
695
684
  });
696
685
  await writeRuntimeRecord(runtimeStateService, runtimeRecord);
697
686
  } catch (error) {
698
687
  await releasePort();
699
- await processLease.release({
700
- kill: false,
701
- releasePort: false
702
- });
703
688
  await sharedServices.dispose();
704
689
  await cleanupFailedRuntimeStartup(handler, runtimeStateService, runtimeRecord.serverId, processLease);
705
690
  throw new Error(`Failed to start HTTP runtime '${runtimeRecord.serverId}': ${toErrorMessage$9(error)}`);
@@ -779,21 +764,21 @@ async function startStdioHttpTransport(config, options, resolvedConfigPath) {
779
764
  throw new Error(`Failed to start stdio-http transport: ${toErrorMessage$9(error)}`);
780
765
  }
781
766
  }
782
- async function startTransport(transportType, options, resolvedConfigPath, serverOptions) {
767
+ async function startTransport(transportType, options, resolvedConfigPath, serverOptions, proxyDefaults) {
783
768
  try {
784
769
  if (transportType === TRANSPORT_TYPE_STDIO) {
785
770
  await startStdioTransport(serverOptions);
786
771
  return;
787
772
  }
788
773
  if (transportType === TRANSPORT_TYPE_HTTP) {
789
- await createAndStartHttpRuntime(serverOptions, createTransportConfig(options, require_src.TRANSPORT_MODE.HTTP), resolvedConfigPath);
774
+ await createAndStartHttpRuntime(serverOptions, createTransportConfig(options, require_src.TRANSPORT_MODE.HTTP, proxyDefaults), resolvedConfigPath);
790
775
  return;
791
776
  }
792
777
  if (transportType === TRANSPORT_TYPE_SSE) {
793
- await startSseTransport(serverOptions, createTransportConfig(options, require_src.TRANSPORT_MODE.SSE));
778
+ await startSseTransport(serverOptions, createTransportConfig(options, require_src.TRANSPORT_MODE.SSE, proxyDefaults));
794
779
  return;
795
780
  }
796
- await startStdioHttpTransport(createTransportConfig(options, require_src.TRANSPORT_MODE.HTTP), options, resolvedConfigPath);
781
+ await startStdioHttpTransport(createTransportConfig(options, require_src.TRANSPORT_MODE.HTTP, proxyDefaults), options, resolvedConfigPath);
797
782
  } catch (error) {
798
783
  throw new Error(`Failed to start transport '${transportType}': ${toErrorMessage$9(error)}`);
799
784
  }
@@ -801,18 +786,19 @@ async function startTransport(transportType, options, resolvedConfigPath, server
801
786
  /**
802
787
  * MCP Serve command
803
788
  */
804
- 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) => {
805
790
  try {
806
- const transportType = validateTransportType(options.type.toLowerCase());
807
- validateProxyMode(options.proxyMode);
808
791
  const resolvedConfigPath = options.config || await findConfigFileAsync() || void 0;
809
- 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);
810
796
  } catch (error) {
811
- const rawTransportType = options.type.toLowerCase();
797
+ const rawTransportType = (options.type ?? TRANSPORT_TYPE_STDIO).toLowerCase();
812
798
  const transportType = isValidTransportType(rawTransportType) ? rawTransportType : TRANSPORT_TYPE_STDIO;
813
799
  const envPort = process.env.MCP_PORT ? Number(process.env.MCP_PORT) : void 0;
814
800
  const requestedPort = options.port ?? (Number.isFinite(envPort) ? envPort : void 0);
815
- console.error(formatStartError(transportType, options.host, requestedPort, error));
801
+ console.error(formatStartError(transportType, options.host ?? DEFAULT_HOST, requestedPort, error));
816
802
  process.exit(1);
817
803
  }
818
804
  });
@@ -822,14 +808,58 @@ const mcpServeCommand = new commander.Command("mcp-serve").description("Start MC
822
808
  function toErrorMessage$8(error) {
823
809
  return error instanceof Error ? error.message : String(error);
824
810
  }
825
- async function withConnectedCommandContext(options, run) {
826
- const container = require_src.createProxyIoCContainer();
827
- const configFilePath = options.config || require_src.findConfigFile();
828
- if (!configFilePath) throw new Error("No config file found. Use --config or create mcp-config.yaml");
829
- const config = await container.createConfigFetcherService({
830
- configFilePath,
831
- useCache: options.useCache
832
- }).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) {
833
863
  const clientManager = container.createClientManagerService();
834
864
  await Promise.all(Object.entries(config.mcpServers).map(async ([serverName, serverConfig]) => {
835
865
  try {
@@ -851,6 +881,17 @@ async function withConnectedCommandContext(options, run) {
851
881
  await clientManager.disconnectAll();
852
882
  }
853
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
+ }
854
895
 
855
896
  //#endregion
856
897
  //#region src/commands/list-tools.ts
@@ -1093,7 +1134,7 @@ function toErrorMessage$5(error) {
1093
1134
  /**
1094
1135
  * Execute an MCP tool with arguments
1095
1136
  */
1096
- 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) => {
1097
1138
  try {
1098
1139
  let toolArgs = {};
1099
1140
  try {