@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.mjs CHANGED
@@ -1,16 +1,25 @@
1
1
  #!/usr/bin/env node
2
- import { D as findConfigFile, E as generateServerId, T as DefinitionsCacheService, _ as StopServerService, d as version, f as StdioHttpTransportHandler, h as HttpTransportHandler, m as SseTransportHandler, n as createServer, o as createProxyIoCContainer, p as StdioTransportHandler, r as createSessionServer, t as TRANSPORT_MODE, u as initializeSharedServices, v as RuntimeStateService } from "./src-Cp1GdSlN.mjs";
3
- import { randomUUID } from "node:crypto";
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-DQSfFKFP.mjs";
3
+ import { constants, existsSync, readFileSync } from "node:fs";
4
4
  import { access, writeFile } from "node:fs/promises";
5
- import path, { join, resolve } from "node:path";
6
- import { constants, existsSync } from "node:fs";
7
- import { Liquid } from "liquidjs";
5
+ import yaml from "js-yaml";
6
+ import { randomUUID } from "node:crypto";
7
+ import path, { dirname, join, resolve } from "node:path";
8
8
  import { spawn } from "node:child_process";
9
+ import { Liquid } from "liquidjs";
9
10
  import { Command } from "commander";
10
11
  import { DEFAULT_PORT_RANGE, PortRegistryService } from "@agimon-ai/foundation-port-registry";
11
- import { ProcessRegistryService } from "@agimon-ai/foundation-process-registry";
12
+ import { ProcessRegistryService, createProcessLease, resolveSiblingRegistryPath } from "@agimon-ai/foundation-process-registry";
12
13
  import { fileURLToPath } from "node:url";
13
14
 
15
+ //#region src/templates/mcp-config.json?raw
16
+ 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";
17
+
18
+ //#endregion
19
+ //#region src/templates/mcp-config.yaml.liquid?raw
20
+ var mcp_config_yaml_default = "# MCP Server Configuration\n# This file configures the MCP servers that mcp-proxy will connect to\n#\n# Environment Variable Interpolation:\n# Use ${VAR_NAME} syntax to reference environment variables\n# Example: ${HOME}, ${API_KEY}, ${DATABASE_URL}\n#\n# Instructions:\n# - config.instruction: Server's default instruction (from server documentation)\n# - instruction: User override (optional, takes precedence over config.instruction)\n# - config.toolBlacklist: Array of tool names to hide/block from this server\n# - config.omitToolDescription: Boolean to show only tool names without descriptions (saves tokens)\n\n# Remote Configuration Sources (OPTIONAL)\n# Fetch and merge configurations from remote URLs\n# Remote configs are merged with local configs based on merge strategy\n#\n# SECURITY: SSRF Protection is ENABLED by default\n# - Only HTTPS URLs are allowed (set security.enforceHttps: false to allow HTTP)\n# - Private IPs and localhost are blocked (set security.allowPrivateIPs: true for internal networks)\n# - Blocked ranges: 127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16\nremoteConfigs:\n # Example 1: Basic remote config with default security\n # - url: ${AGIFLOW_URL}/api/v1/mcp-configs\n # headers:\n # Authorization: Bearer ${AGIFLOW_API_KEY}\n # mergeStrategy: local-priority # Options: local-priority (default), remote-priority, merge-deep\n #\n # Example 2: Remote config with custom security settings (for internal networks)\n # - url: ${INTERNAL_URL}/mcp-configs\n # headers:\n # Authorization: Bearer ${INTERNAL_TOKEN}\n # security:\n # allowPrivateIPs: true # Allow internal IPs (default: false)\n # enforceHttps: false # Allow HTTP (default: true, HTTPS only)\n # mergeStrategy: local-priority\n #\n # Example 3: Remote config with additional validation (OPTIONAL)\n # - url: ${AGIFLOW_URL}/api/v1/mcp-configs\n # headers:\n # Authorization: Bearer ${AGIFLOW_API_KEY}\n # X-API-Key: ${AGIFLOW_API_KEY}\n # security:\n # enforceHttps: true # Require HTTPS (default: true)\n # allowPrivateIPs: false # Block private IPs (default: false)\n # validation: # OPTIONAL: Additional regex validation on top of security checks\n # url: ^https://.*\\.agiflow\\.io/.* # OPTIONAL: Regex pattern to validate URL format\n # headers: # OPTIONAL: Regex patterns to validate header values\n # Authorization: ^Bearer [A-Za-z0-9_-]+$\n # X-API-Key: ^[A-Za-z0-9_-]{32,}$\n # mergeStrategy: local-priority\n\nmcpServers:\n{%- if mcpServers %}{% for server in mcpServers %}\n {{ server.name }}:\n command: {{ server.command }}\n args:{% for arg in server.args %}\n - '{{ arg }}'{% endfor %}\n # env:\n # LOG_LEVEL: info\n # # API_KEY: ${MY_API_KEY}\n # config:\n # instruction: Use this server for...\n # # toolBlacklist:\n # # - tool_to_block\n # # omitToolDescription: true\n{% endfor %}\n # Example MCP server using SSE transport\n # remote-server:\n # url: https://example.com/mcp\n # type: sse\n # headers:\n # Authorization: Bearer ${API_KEY}\n # config:\n # instruction: This server provides tools for...\n{% else %}\n # Example MCP server using stdio transport\n example-server:\n command: node\n args:\n - /path/to/mcp-server/build/index.js\n env:\n # Environment variables for the MCP server\n LOG_LEVEL: info\n # You can use environment variable interpolation:\n # DATABASE_URL: ${DATABASE_URL}\n # API_KEY: ${MY_API_KEY}\n config:\n # Server's default instruction (from server documentation)\n instruction: Use this server for...\n # Optional: Block specific tools from being listed or executed\n # toolBlacklist:\n # - dangerous_tool_name\n # - another_blocked_tool\n # Optional: Omit tool descriptions to save tokens (default: false)\n # omitToolDescription: true\n # instruction: Optional user override - takes precedence over config.instruction\n\n # Example MCP server using SSE transport with environment variables\n # remote-server:\n # url: https://example.com/mcp\n # type: sse\n # headers:\n # # Use ${VAR_NAME} to interpolate environment variables\n # Authorization: Bearer ${API_KEY}\n # config:\n # instruction: This server provides tools for...\n # # Optional: Block specific tools from being listed or executed\n # # toolBlacklist:\n # # - tool_to_block\n # # Optional: Omit tool descriptions to save tokens (default: false)\n # # omitToolDescription: true\n # # instruction: Optional user override\n{% endif %}\n";
21
+
22
+ //#endregion
14
23
  //#region src/utils/output.ts
15
24
  function writeLine(message = "") {
16
25
  console.log(message);
@@ -34,14 +43,6 @@ const print = {
34
43
  indent: (message) => writeLine(` ${message}`)
35
44
  };
36
45
 
37
- //#endregion
38
- //#region src/templates/mcp-config.yaml.liquid?raw
39
- 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";
40
-
41
- //#endregion
42
- //#region src/templates/mcp-config.json?raw
43
- 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";
44
-
45
46
  //#endregion
46
47
  //#region src/commands/init.ts
47
48
  /**
@@ -158,12 +159,6 @@ async function findExistingHealthyRuntime(workspaceRoot) {
158
159
  } catch {}
159
160
  return null;
160
161
  }
161
- function resolveSiblingRegistryPath(registryPath, fileName) {
162
- if (!registryPath) return;
163
- const resolved = path.resolve(registryPath);
164
- if (path.extname(resolved) === ".json") return path.join(path.dirname(resolved), fileName);
165
- return path.join(resolved, fileName);
166
- }
167
162
  function buildCliCandidates() {
168
163
  const __filename = fileURLToPath(import.meta.url);
169
164
  const __dirname = path.dirname(__filename);
@@ -386,16 +381,37 @@ async function findConfigFileAsync() {
386
381
  const configPath = resolve(projectPath, fileName);
387
382
  if (await pathExists(configPath)) return configPath;
388
383
  }
389
- const cwd = process.cwd();
390
- for (const fileName of CONFIG_FILE_NAMES) {
391
- const configPath = join(cwd, fileName);
392
- if (await pathExists(configPath)) return configPath;
384
+ const MAX_PARENT_LEVELS = 3;
385
+ let searchDir = process.cwd();
386
+ for (let level = 0; level <= MAX_PARENT_LEVELS; level++) {
387
+ for (const fileName of CONFIG_FILE_NAMES) {
388
+ const configPath = join(searchDir, fileName);
389
+ if (await pathExists(configPath)) return configPath;
390
+ }
391
+ const parentDir = dirname(searchDir);
392
+ if (parentDir === searchDir) break;
393
+ searchDir = parentDir;
393
394
  }
394
395
  return null;
395
396
  } catch (error) {
396
397
  throw new Error(`Failed to discover MCP config file: ${toErrorMessage$9(error)}`);
397
398
  }
398
399
  }
400
+ function loadProxyDefaults(configPath) {
401
+ try {
402
+ const content = readFileSync(configPath, "utf-8");
403
+ const proxy = (configPath.endsWith(".yaml") || configPath.endsWith(".yml") ? yaml.load(content) : JSON.parse(content))?.proxy;
404
+ if (!proxy || typeof proxy !== "object") return {};
405
+ const p = proxy;
406
+ return {
407
+ type: typeof p.type === "string" ? p.type : void 0,
408
+ port: typeof p.port === "number" && Number.isInteger(p.port) && p.port > 0 ? p.port : void 0,
409
+ host: typeof p.host === "string" ? p.host : void 0
410
+ };
411
+ } catch {
412
+ return {};
413
+ }
414
+ }
399
415
  async function resolveServerId(options, resolvedConfigPath) {
400
416
  const container = createProxyIoCContainer();
401
417
  if (options.id) return options.id;
@@ -417,12 +433,12 @@ function validateTransportType(type) {
417
433
  function validateProxyMode(mode) {
418
434
  if (!isValidProxyMode(mode)) throw new Error(`Unknown proxy mode: '${mode}'. Valid options: meta, flat, search`);
419
435
  }
420
- function createTransportConfig(options, mode) {
436
+ function createTransportConfig(options, mode, proxyDefaults) {
421
437
  const envPort = process.env.MCP_PORT ? Number(process.env.MCP_PORT) : void 0;
422
438
  return {
423
439
  mode,
424
- port: options.port ?? (Number.isFinite(envPort) ? envPort : void 0),
425
- host: options.host || process.env.MCP_HOST || DEFAULT_HOST
440
+ port: options.port ?? (Number.isFinite(envPort) ? envPort : void 0) ?? proxyDefaults?.port,
441
+ host: options.host ?? process.env.MCP_HOST ?? proxyDefaults?.host ?? DEFAULT_HOST
426
442
  };
427
443
  }
428
444
  function createStdioSafeLogger() {
@@ -471,9 +487,6 @@ function createRuntimeRecord(serverId, config, port, shutdownToken, configPath)
471
487
  function createPortRegistryService() {
472
488
  return new PortRegistryService(process.env.PORT_REGISTRY_PATH);
473
489
  }
474
- function createProcessRegistryService() {
475
- return new ProcessRegistryService(process.env.PROCESS_REGISTRY_PATH);
476
- }
477
490
  function getRegistryEnvironment() {
478
491
  return process.env.NODE_ENV ?? "development";
479
492
  }
@@ -520,41 +533,6 @@ async function createPortRegistryLease(serviceName, host, preferredPort, serverI
520
533
  }
521
534
  };
522
535
  }
523
- async function createProcessRegistryLease(serviceName, host, port, serverId, transport, configPath) {
524
- const processRegistry = createProcessRegistryService();
525
- const result = await processRegistry.registerProcess({
526
- repositoryPath: getRegistryRepositoryPath(),
527
- serviceName,
528
- serviceType: PROCESS_REGISTRY_SERVICE_TYPE,
529
- environment: getRegistryEnvironment(),
530
- pid: process.pid,
531
- host,
532
- port,
533
- command: process.argv[1],
534
- args: process.argv.slice(2),
535
- metadata: {
536
- transport,
537
- serverId,
538
- ...configPath ? { configPath } : {}
539
- }
540
- });
541
- if (!result.success || !result.record) throw new Error(result.error || `Failed to register process for ${serviceName}`);
542
- let released = false;
543
- return { release: async (options) => {
544
- if (released) return;
545
- released = true;
546
- const releaseResult = await processRegistry.releaseProcess({
547
- repositoryPath: getRegistryRepositoryPath(),
548
- serviceName,
549
- serviceType: PROCESS_REGISTRY_SERVICE_TYPE,
550
- pid: process.pid,
551
- environment: getRegistryEnvironment(),
552
- kill: options?.kill ?? false,
553
- releasePort: options?.releasePort ?? false
554
- });
555
- if (!releaseResult.success && releaseResult.error && !releaseResult.error.includes("No matching process entry")) throw new Error(releaseResult.error || `Failed to release process for ${serviceName}`);
556
- } };
557
- }
558
536
  async function releasePortLease(lease) {
559
537
  if (!lease) return;
560
538
  await lease.release();
@@ -588,13 +566,19 @@ async function stopOwnedHttpTransport(handler, runtimeStateService, serverId, pr
588
566
  throw new Error(`Failed to stop owned HTTP transport '${serverId}': ${toErrorMessage$9(error)}`);
589
567
  }
590
568
  } finally {
591
- await processLease?.release({
592
- kill: false,
593
- releasePort: false
594
- });
569
+ await processLease?.release({ kill: false });
595
570
  await removeRuntimeRecord(runtimeStateService, serverId);
596
571
  }
597
572
  }
573
+ /**
574
+ * Run post-stop cleanup for an HTTP runtime (release port, dispose services, remove state).
575
+ * This is the subset of stopOwnedHttpTransport that runs AFTER handler.stop() has already
576
+ * been called by startServer()'s signal handler — avoids double-stopping the transport.
577
+ */
578
+ async function cleanupHttpRuntime(runtimeStateService, serverId, processLease) {
579
+ await processLease?.release({ kill: false });
580
+ await removeRuntimeRecord(runtimeStateService, serverId);
581
+ }
598
582
  async function cleanupFailedRuntimeStartup(handler, runtimeStateService, serverId, processLease) {
599
583
  try {
600
584
  try {
@@ -603,10 +587,7 @@ async function cleanupFailedRuntimeStartup(handler, runtimeStateService, serverI
603
587
  throw new Error(`Failed to stop HTTP transport during cleanup for '${serverId}': ${toErrorMessage$9(error)}`);
604
588
  }
605
589
  } finally {
606
- await processLease?.release({
607
- kill: false,
608
- releasePort: false
609
- });
590
+ await processLease?.release({ kill: false });
610
591
  await removeRuntimeRecord(runtimeStateService, serverId);
611
592
  }
612
593
  }
@@ -651,7 +632,21 @@ async function createAndStartHttpRuntime(serverOptions, config, resolvedConfigPa
651
632
  ...config,
652
633
  port: runtimePort
653
634
  };
654
- const processLease = await createProcessRegistryLease(PROCESS_REGISTRY_SERVICE_HTTP, runtimeConfig.host ?? DEFAULT_HOST, runtimePort, runtimeServerId, TRANSPORT_TYPE_HTTP, resolvedConfigPath);
635
+ const processLease = await createProcessLease({
636
+ repositoryPath: getRegistryRepositoryPath(),
637
+ serviceName: PROCESS_REGISTRY_SERVICE_HTTP,
638
+ serviceType: PROCESS_REGISTRY_SERVICE_TYPE,
639
+ environment: getRegistryEnvironment(),
640
+ host: runtimeConfig.host ?? DEFAULT_HOST,
641
+ port: runtimePort,
642
+ command: process.argv[1],
643
+ args: process.argv.slice(2),
644
+ metadata: {
645
+ transport: TRANSPORT_TYPE_HTTP,
646
+ serverId: runtimeServerId,
647
+ ...resolvedConfigPath ? { configPath: resolvedConfigPath } : {}
648
+ }
649
+ });
655
650
  let releasePort = async () => {
656
651
  await releasePortLease(portLease ?? null);
657
652
  releasePort = async () => void 0;
@@ -675,10 +670,7 @@ async function createAndStartHttpRuntime(serverOptions, config, resolvedConfigPa
675
670
  handler = new HttpTransportHandler(() => createSessionServer(sharedServices), runtimeConfig, createHttpAdminOptions(runtimeRecord.serverId, shutdownToken, stopHandler));
676
671
  } catch (error) {
677
672
  await releasePort();
678
- await processLease.release({
679
- kill: false,
680
- releasePort: false
681
- });
673
+ await processLease.release({ kill: false });
682
674
  await sharedServices.dispose();
683
675
  throw new Error(`Failed to create HTTP runtime server: ${toErrorMessage$9(error)}`);
684
676
  }
@@ -686,19 +678,11 @@ async function createAndStartHttpRuntime(serverOptions, config, resolvedConfigPa
686
678
  await startServer(handler, async () => {
687
679
  await releasePort();
688
680
  await sharedServices.dispose();
689
- await processLease.release({
690
- kill: false,
691
- releasePort: false
692
- });
693
- await removeRuntimeRecord(runtimeStateService, runtimeRecord.serverId);
681
+ await cleanupHttpRuntime(runtimeStateService, runtimeRecord.serverId, processLease);
694
682
  });
695
683
  await writeRuntimeRecord(runtimeStateService, runtimeRecord);
696
684
  } catch (error) {
697
685
  await releasePort();
698
- await processLease.release({
699
- kill: false,
700
- releasePort: false
701
- });
702
686
  await sharedServices.dispose();
703
687
  await cleanupFailedRuntimeStartup(handler, runtimeStateService, runtimeRecord.serverId, processLease);
704
688
  throw new Error(`Failed to start HTTP runtime '${runtimeRecord.serverId}': ${toErrorMessage$9(error)}`);
@@ -778,21 +762,21 @@ async function startStdioHttpTransport(config, options, resolvedConfigPath) {
778
762
  throw new Error(`Failed to start stdio-http transport: ${toErrorMessage$9(error)}`);
779
763
  }
780
764
  }
781
- async function startTransport(transportType, options, resolvedConfigPath, serverOptions) {
765
+ async function startTransport(transportType, options, resolvedConfigPath, serverOptions, proxyDefaults) {
782
766
  try {
783
767
  if (transportType === TRANSPORT_TYPE_STDIO) {
784
768
  await startStdioTransport(serverOptions);
785
769
  return;
786
770
  }
787
771
  if (transportType === TRANSPORT_TYPE_HTTP) {
788
- await createAndStartHttpRuntime(serverOptions, createTransportConfig(options, TRANSPORT_MODE.HTTP), resolvedConfigPath);
772
+ await createAndStartHttpRuntime(serverOptions, createTransportConfig(options, TRANSPORT_MODE.HTTP, proxyDefaults), resolvedConfigPath);
789
773
  return;
790
774
  }
791
775
  if (transportType === TRANSPORT_TYPE_SSE) {
792
- await startSseTransport(serverOptions, createTransportConfig(options, TRANSPORT_MODE.SSE));
776
+ await startSseTransport(serverOptions, createTransportConfig(options, TRANSPORT_MODE.SSE, proxyDefaults));
793
777
  return;
794
778
  }
795
- await startStdioHttpTransport(createTransportConfig(options, TRANSPORT_MODE.HTTP), options, resolvedConfigPath);
779
+ await startStdioHttpTransport(createTransportConfig(options, TRANSPORT_MODE.HTTP, proxyDefaults), options, resolvedConfigPath);
796
780
  } catch (error) {
797
781
  throw new Error(`Failed to start transport '${transportType}': ${toErrorMessage$9(error)}`);
798
782
  }
@@ -800,18 +784,19 @@ async function startTransport(transportType, options, resolvedConfigPath, server
800
784
  /**
801
785
  * MCP Serve command
802
786
  */
803
- 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}`, 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) => {
787
+ 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) => {
804
788
  try {
805
- const transportType = validateTransportType(options.type.toLowerCase());
806
- validateProxyMode(options.proxyMode);
807
789
  const resolvedConfigPath = options.config || await findConfigFileAsync() || void 0;
808
- await startTransport(transportType, options, resolvedConfigPath, createServerOptions(options, resolvedConfigPath, await resolveServerId(options, resolvedConfigPath)));
790
+ const proxyDefaults = resolvedConfigPath ? loadProxyDefaults(resolvedConfigPath) : {};
791
+ const transportType = validateTransportType((options.type ?? proxyDefaults.type ?? TRANSPORT_TYPE_STDIO).toLowerCase());
792
+ validateProxyMode(options.proxyMode);
793
+ await startTransport(transportType, options, resolvedConfigPath, createServerOptions(options, resolvedConfigPath, await resolveServerId(options, resolvedConfigPath)), proxyDefaults);
809
794
  } catch (error) {
810
- const rawTransportType = options.type.toLowerCase();
795
+ const rawTransportType = (options.type ?? TRANSPORT_TYPE_STDIO).toLowerCase();
811
796
  const transportType = isValidTransportType(rawTransportType) ? rawTransportType : TRANSPORT_TYPE_STDIO;
812
797
  const envPort = process.env.MCP_PORT ? Number(process.env.MCP_PORT) : void 0;
813
798
  const requestedPort = options.port ?? (Number.isFinite(envPort) ? envPort : void 0);
814
- console.error(formatStartError(transportType, options.host, requestedPort, error));
799
+ console.error(formatStartError(transportType, options.host ?? DEFAULT_HOST, requestedPort, error));
815
800
  process.exit(1);
816
801
  }
817
802
  });
@@ -821,14 +806,58 @@ const mcpServeCommand = new Command("mcp-serve").description("Start MCP server w
821
806
  function toErrorMessage$8(error) {
822
807
  return error instanceof Error ? error.message : String(error);
823
808
  }
824
- async function withConnectedCommandContext(options, run) {
825
- const container = createProxyIoCContainer();
826
- const configFilePath = options.config || findConfigFile();
827
- if (!configFilePath) throw new Error("No config file found. Use --config or create mcp-config.yaml");
828
- const config = await container.createConfigFetcherService({
829
- configFilePath,
830
- useCache: options.useCache
831
- }).fetchConfiguration();
809
+ async function checkHealth(host, port) {
810
+ try {
811
+ return (await fetch(`http://${host}:${port}/health`, { signal: AbortSignal.timeout(3e3) })).ok;
812
+ } catch {
813
+ return false;
814
+ }
815
+ }
816
+ /**
817
+ * Proxy mode: connect to a running HTTP server instead of downstream servers directly.
818
+ * Auto-starts the server if not running.
819
+ */
820
+ async function withProxiedContext(container, config, configFilePath, options, run) {
821
+ const host = config.proxy?.host ?? "localhost";
822
+ const port = config.proxy?.port;
823
+ const endpoint = `http://${host}:${port}/mcp`;
824
+ if (!await checkHealth(host, port)) {
825
+ if (!options.json) console.error("Starting HTTP proxy server in background...");
826
+ await prestartHttpRuntime({
827
+ host,
828
+ port,
829
+ config: configFilePath,
830
+ cache: options.useCache !== false,
831
+ clearDefinitionsCache: false,
832
+ proxyMode: "flat"
833
+ });
834
+ }
835
+ const clientManager = container.createClientManagerService();
836
+ try {
837
+ await clientManager.connectToServer("proxy", {
838
+ name: "proxy",
839
+ transport: "http",
840
+ config: { url: endpoint }
841
+ });
842
+ if (!options.json) console.error(`✓ Connected to proxy at ${endpoint}`);
843
+ } catch (error) {
844
+ throw new Error(`Failed to connect to proxy server at ${endpoint}: ${toErrorMessage$8(error)}`);
845
+ }
846
+ try {
847
+ return await run({
848
+ container,
849
+ configFilePath,
850
+ config,
851
+ clientManager
852
+ });
853
+ } finally {
854
+ await clientManager.disconnectAll();
855
+ }
856
+ }
857
+ /**
858
+ * Direct mode: connect to all downstream MCP servers individually.
859
+ */
860
+ async function withDirectContext(container, config, configFilePath, options, run) {
832
861
  const clientManager = container.createClientManagerService();
833
862
  await Promise.all(Object.entries(config.mcpServers).map(async ([serverName, serverConfig]) => {
834
863
  try {
@@ -850,6 +879,17 @@ async function withConnectedCommandContext(options, run) {
850
879
  await clientManager.disconnectAll();
851
880
  }
852
881
  }
882
+ async function withConnectedCommandContext(options, run) {
883
+ const container = createProxyIoCContainer();
884
+ const configFilePath = options.config || findConfigFile();
885
+ if (!configFilePath) throw new Error("No config file found. Use --config or create mcp-config.yaml");
886
+ const config = await container.createConfigFetcherService({
887
+ configFilePath,
888
+ useCache: options.useCache
889
+ }).fetchConfiguration();
890
+ if (config.proxy?.port) return await withProxiedContext(container, config, configFilePath, options, run);
891
+ return await withDirectContext(container, config, configFilePath, options, run);
892
+ }
853
893
 
854
894
  //#endregion
855
895
  //#region src/commands/list-tools.ts
@@ -1092,7 +1132,7 @@ function toErrorMessage$5(error) {
1092
1132
  /**
1093
1133
  * Execute an MCP tool with arguments
1094
1134
  */
1095
- 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)", parseInt).option("-j, --json", "Output as JSON", false).action(async (toolName, options) => {
1135
+ 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) => {
1096
1136
  try {
1097
1137
  let toolArgs = {};
1098
1138
  try {
package/dist/index.cjs CHANGED
@@ -1,4 +1,4 @@
1
- const require_src = require('./src-DwErnAUn.cjs');
1
+ const require_src = require('./src-B5N-kt9Y.cjs');
2
2
 
3
3
  exports.ConfigFetcherService = require_src.ConfigFetcherService;
4
4
  exports.DefinitionsCacheService = require_src.DefinitionsCacheService;
package/dist/index.d.cts CHANGED
@@ -260,6 +260,11 @@ interface PromptConfig {
260
260
  */
261
261
  interface RemoteMcpConfiguration {
262
262
  id?: string;
263
+ proxy?: {
264
+ type?: string;
265
+ port?: number;
266
+ host?: string;
267
+ };
263
268
  mcpServers: Record<string, McpServerConfig>;
264
269
  skills?: SkillsConfig;
265
270
  }
@@ -515,6 +520,62 @@ declare class ConfigFetcherService {
515
520
  isCacheValid(): boolean;
516
521
  }
517
522
  //#endregion
523
+ //#region src/services/McpClientManagerService.d.ts
524
+ /**
525
+ * Service for managing MCP client connections to remote servers
526
+ */
527
+ declare class McpClientManagerService {
528
+ private clients;
529
+ private serverConfigs;
530
+ private connectionPromises;
531
+ private logger;
532
+ constructor(logger?: LoggerLike);
533
+ /**
534
+ * Kill all stdio MCP server child processes.
535
+ * Sends SIGTERM first, then SIGKILL after 1s if the process hasn't exited.
536
+ * Must be called by the owner (e.g. transport/command layer) during shutdown.
537
+ * Awaiting the returned promise ensures force-kill timers complete before process.exit().
538
+ */
539
+ cleanupChildProcesses(): Promise<void>;
540
+ /**
541
+ * Connect to an MCP server based on its configuration with timeout
542
+ * Uses the timeout from server config, falling back to default (30s)
543
+ */
544
+ connectToServer(serverName: string, config: McpServerConfig): Promise<void>;
545
+ registerServerConfigs(configs: Record<string, McpServerConfig>): void;
546
+ getKnownServerNames(): string[];
547
+ getServerRequestTimeout(serverName: string): number | undefined;
548
+ ensureConnected(serverName: string): Promise<McpClientConnection>;
549
+ private createConnection;
550
+ /**
551
+ * Perform the actual connection to MCP server
552
+ */
553
+ private performConnection;
554
+ private connectStdioClient;
555
+ private connectHttpClient;
556
+ private connectSseClient;
557
+ /**
558
+ * Get a connected client by server name
559
+ */
560
+ getClient(serverName: string): McpClientConnection | undefined;
561
+ /**
562
+ * Get all connected clients
563
+ */
564
+ getAllClients(): McpClientConnection[];
565
+ /**
566
+ * Disconnect from a specific server
567
+ */
568
+ disconnectServer(serverName: string): Promise<void>;
569
+ /**
570
+ * Disconnect from all servers
571
+ */
572
+ disconnectAll(): Promise<void>;
573
+ /**
574
+ * Check if a server is connected
575
+ */
576
+ isConnected(serverName: string): boolean;
577
+ }
578
+ //#endregion
518
579
  //#region src/services/SkillService.d.ts
519
580
  /**
520
581
  * Service for loading and managing skills from configured skill directories.
@@ -643,60 +704,6 @@ declare class SkillService {
643
704
  private loadSkillFile;
644
705
  }
645
706
  //#endregion
646
- //#region src/services/McpClientManagerService.d.ts
647
- /**
648
- * Service for managing MCP client connections to remote servers
649
- */
650
- declare class McpClientManagerService {
651
- private clients;
652
- private serverConfigs;
653
- private connectionPromises;
654
- private logger;
655
- constructor(logger?: LoggerLike);
656
- /**
657
- * Synchronously kill all stdio MCP server child processes.
658
- * Must be called by the owner (e.g. transport/command layer) during shutdown.
659
- */
660
- cleanupChildProcesses(): void;
661
- /**
662
- * Connect to an MCP server based on its configuration with timeout
663
- * Uses the timeout from server config, falling back to default (30s)
664
- */
665
- connectToServer(serverName: string, config: McpServerConfig): Promise<void>;
666
- registerServerConfigs(configs: Record<string, McpServerConfig>): void;
667
- getKnownServerNames(): string[];
668
- getServerRequestTimeout(serverName: string): number | undefined;
669
- ensureConnected(serverName: string): Promise<McpClientConnection>;
670
- private createConnection;
671
- /**
672
- * Perform the actual connection to MCP server
673
- */
674
- private performConnection;
675
- private connectStdioClient;
676
- private connectHttpClient;
677
- private connectSseClient;
678
- /**
679
- * Get a connected client by server name
680
- */
681
- getClient(serverName: string): McpClientConnection | undefined;
682
- /**
683
- * Get all connected clients
684
- */
685
- getAllClients(): McpClientConnection[];
686
- /**
687
- * Disconnect from a specific server
688
- */
689
- disconnectServer(serverName: string): Promise<void>;
690
- /**
691
- * Disconnect from all servers
692
- */
693
- disconnectAll(): Promise<void>;
694
- /**
695
- * Check if a server is connected
696
- */
697
- isConnected(serverName: string): boolean;
698
- }
699
- //#endregion
700
707
  //#region src/services/DefinitionsCacheService.d.ts
701
708
  interface DefinitionsCacheServiceOptions {
702
709
  cacheData?: DefinitionsCacheFile;
@@ -1115,10 +1122,14 @@ declare class UseToolTool implements Tool<UseToolToolInput> {
1115
1122
  /**
1116
1123
  * HTTP transport handler using Streamable HTTP (protocol version 2025-03-26)
1117
1124
  * Provides stateful session management with resumability support
1125
+ *
1126
+ * Uses a hybrid server architecture:
1127
+ * - Raw Node.js createServer for /mcp routes (MCP SDK needs native req/res)
1128
+ * - Hono for REST routes (/health, /admin/shutdown)
1118
1129
  */
1119
1130
  declare class HttpTransportHandler implements HttpTransportHandler$1 {
1120
1131
  private serverFactory;
1121
- private app;
1132
+ private honoApp;
1122
1133
  private server;
1123
1134
  private sessionManager;
1124
1135
  private config;
@@ -1126,10 +1137,14 @@ declare class HttpTransportHandler implements HttpTransportHandler$1 {
1126
1137
  private adminRateLimiter;
1127
1138
  private logger;
1128
1139
  constructor(serverFactory: () => Server | Promise<Server>, config: TransportConfig, adminOptions?: HttpTransportAdminOptions, logger?: LoggerLike);
1129
- private setupMiddleware;
1130
- private setupRoutes;
1140
+ private setupHonoRoutes;
1131
1141
  private isAuthorizedShutdownRequest;
1132
1142
  private handleAdminShutdownRequest;
1143
+ /**
1144
+ * Handle MCP protocol requests (POST/GET/DELETE /mcp)
1145
+ * Uses raw Node.js req/res as required by MCP SDK
1146
+ */
1147
+ private handleMcpRequest;
1133
1148
  private handlePostRequest;
1134
1149
  private handleGetRequest;
1135
1150
  private handleDeleteRequest;
@@ -1147,14 +1162,13 @@ declare class HttpTransportHandler implements HttpTransportHandler$1 {
1147
1162
  */
1148
1163
  declare class SseTransportHandler implements HttpTransportHandler$1 {
1149
1164
  private serverFactory;
1150
- private app;
1165
+ private honoApp;
1151
1166
  private server;
1152
1167
  private sessionManager;
1153
1168
  private config;
1154
1169
  private logger;
1155
1170
  constructor(serverFactory: Server | (() => Server), config: TransportConfig, logger?: LoggerLike);
1156
- private setupMiddleware;
1157
- private setupRoutes;
1171
+ private setupHonoRoutes;
1158
1172
  private handleSseConnection;
1159
1173
  private handlePostMessage;
1160
1174
  start(): Promise<void>;