@agimon-ai/mcp-proxy 0.4.0 → 0.4.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,13 +1,16 @@
1
1
  #!/usr/bin/env node
2
- const require_src = require('./src-6KF7hZTe.cjs');
2
+ const require_src = require('./src-Cm2AVRBA.cjs');
3
3
  let node_crypto = require("node:crypto");
4
4
  let node_fs_promises = require("node:fs/promises");
5
5
  let node_path = require("node:path");
6
+ node_path = require_src.__toESM(node_path);
6
7
  let node_fs = require("node:fs");
7
8
  let liquidjs = require("liquidjs");
9
+ let node_child_process = require("node:child_process");
8
10
  let commander = require("commander");
9
11
  let __agimon_ai_foundation_process_registry = require("@agimon-ai/foundation-process-registry");
10
12
  let __agimon_ai_foundation_port_registry = require("@agimon-ai/foundation-port-registry");
13
+ let node_url = require("node:url");
11
14
 
12
15
  //#region src/utils/output.ts
13
16
  function writeLine(message = "") {
@@ -139,7 +142,7 @@ const CONFIG_FILE_NAMES = [
139
142
  ];
140
143
  const MCP_ENDPOINT_PATH = "/mcp";
141
144
  const DEFAULT_PORT = 3e3;
142
- const DEFAULT_HOST = "localhost";
145
+ const DEFAULT_HOST$1 = "localhost";
143
146
  const TRANSPORT_TYPE_STDIO = "stdio";
144
147
  const TRANSPORT_TYPE_HTTP = "http";
145
148
  const TRANSPORT_TYPE_SSE = "sse";
@@ -218,10 +221,11 @@ function validateProxyMode(mode) {
218
221
  if (!isValidProxyMode(mode)) throw new Error(`Unknown proxy mode: '${mode}'. Valid options: meta, flat, search`);
219
222
  }
220
223
  function createTransportConfig(options, mode) {
224
+ const envPort = process.env.MCP_PORT ? Number(process.env.MCP_PORT) : void 0;
221
225
  return {
222
226
  mode,
223
- port: options.port || Number(process.env.MCP_PORT) || DEFAULT_PORT,
224
- host: options.host || process.env.MCP_HOST || DEFAULT_HOST
227
+ port: options.port ?? (Number.isFinite(envPort) ? envPort : void 0) ?? DEFAULT_PORT,
228
+ host: options.host || process.env.MCP_HOST || DEFAULT_HOST$1
225
229
  };
226
230
  }
227
231
  function createServerOptions(options, resolvedConfigPath, serverId) {
@@ -242,7 +246,7 @@ function formatStartError(type, host, port, error) {
242
246
  function createRuntimeRecord(serverId, config, port, shutdownToken, configPath) {
243
247
  return {
244
248
  serverId,
245
- host: config.host ?? DEFAULT_HOST,
249
+ host: config.host ?? DEFAULT_HOST$1,
246
250
  port,
247
251
  transport: RUNTIME_TRANSPORT,
248
252
  shutdownToken,
@@ -260,7 +264,10 @@ function createProcessRegistryService() {
260
264
  function getRegistryEnvironment() {
261
265
  return process.env.NODE_ENV ?? "development";
262
266
  }
263
- async function createPortRegistryLease(serviceName, host, preferredPort, serverId, transport, configPath) {
267
+ async function createPortRegistryLease(serviceName, host, preferredPort, serverId, transport, configPath, portRange = {
268
+ min: preferredPort,
269
+ max: preferredPort
270
+ }) {
264
271
  const portRegistry = createPortRegistryService();
265
272
  const result = await portRegistry.reservePort({
266
273
  repositoryPath: PORT_REGISTRY_REPOSITORY_PATH,
@@ -270,10 +277,7 @@ async function createPortRegistryLease(serviceName, host, preferredPort, serverI
270
277
  pid: process.pid,
271
278
  host,
272
279
  preferredPort,
273
- portRange: {
274
- min: preferredPort,
275
- max: preferredPort
276
- },
280
+ portRange,
277
281
  force: true,
278
282
  metadata: {
279
283
  transport,
@@ -283,19 +287,22 @@ async function createPortRegistryLease(serviceName, host, preferredPort, serverI
283
287
  });
284
288
  if (!result.success || !result.record) throw new Error(result.error || `Failed to reserve port ${preferredPort} in port registry`);
285
289
  let released = false;
286
- return { release: async () => {
287
- if (released) return;
288
- released = true;
289
- const releaseResult = await portRegistry.releasePort({
290
- repositoryPath: PORT_REGISTRY_REPOSITORY_PATH,
291
- serviceName,
292
- serviceType: PORT_REGISTRY_SERVICE_TYPE,
293
- pid: process.pid,
294
- environment: getRegistryEnvironment(),
295
- force: true
296
- });
297
- if (!releaseResult.success && releaseResult.error && !releaseResult.error.includes("No matching registry entry")) throw new Error(releaseResult.error || `Failed to release port for ${serviceName}`);
298
- } };
290
+ return {
291
+ port: result.record.port,
292
+ release: async () => {
293
+ if (released) return;
294
+ released = true;
295
+ const releaseResult = await portRegistry.releasePort({
296
+ repositoryPath: PORT_REGISTRY_REPOSITORY_PATH,
297
+ serviceName,
298
+ serviceType: PORT_REGISTRY_SERVICE_TYPE,
299
+ pid: process.pid,
300
+ environment: getRegistryEnvironment(),
301
+ force: true
302
+ });
303
+ if (!releaseResult.success && releaseResult.error && !releaseResult.error.includes("No matching registry entry")) throw new Error(releaseResult.error || `Failed to release port for ${serviceName}`);
304
+ }
305
+ };
299
306
  }
300
307
  async function createProcessRegistryLease(serviceName, host, port, serverId, transport, configPath) {
301
308
  const processRegistry = createProcessRegistryService();
@@ -438,13 +445,14 @@ async function createAndStartHttpRuntime(serverOptions, config, resolvedConfigPa
438
445
  const runtimeStateService = new require_src.RuntimeStateService();
439
446
  const shutdownToken = (0, node_crypto.randomUUID)();
440
447
  const runtimeServerId = serverOptions.serverId ?? require_src.generateServerId();
441
- const processLease = await createProcessRegistryLease(PROCESS_REGISTRY_SERVICE_HTTP, config.host ?? DEFAULT_HOST, config.port ?? DEFAULT_PORT, runtimeServerId, TRANSPORT_TYPE_HTTP, resolvedConfigPath);
442
- const portLease = await createPortRegistryLease(PORT_REGISTRY_SERVICE_HTTP, config.host ?? DEFAULT_HOST, config.port ?? DEFAULT_PORT, runtimeServerId, TRANSPORT_TYPE_HTTP, resolvedConfigPath);
448
+ const runtimePort = config.port ?? DEFAULT_PORT;
449
+ const processLease = await createProcessRegistryLease(PROCESS_REGISTRY_SERVICE_HTTP, config.host ?? DEFAULT_HOST$1, runtimePort, runtimeServerId, TRANSPORT_TYPE_HTTP, resolvedConfigPath);
450
+ const portLease = await createPortRegistryLease(PORT_REGISTRY_SERVICE_HTTP, config.host ?? DEFAULT_HOST$1, runtimePort, runtimeServerId, TRANSPORT_TYPE_HTTP, resolvedConfigPath);
443
451
  let releasePort = async () => {
444
- await releasePortLease(portLease);
452
+ await releasePortLease(portLease ?? null);
445
453
  releasePort = async () => void 0;
446
454
  };
447
- const runtimeRecord = createRuntimeRecord(runtimeServerId, config, config.port ?? DEFAULT_PORT, shutdownToken, resolvedConfigPath);
455
+ const runtimeRecord = createRuntimeRecord(runtimeServerId, config, runtimePort, shutdownToken, resolvedConfigPath);
448
456
  let handler;
449
457
  let isStopping = false;
450
458
  const stopHandler = async () => {
@@ -520,30 +528,37 @@ async function startSseTransport(serverOptions, config) {
520
528
  throw new Error(`Failed to start SSE transport: ${toErrorMessage$9(error)}`);
521
529
  }
522
530
  }
523
- async function startStdioHttpTransport(serverOptions, config, resolvedConfigPath) {
531
+ async function startStdioHttpTransport(serverOptions, config, resolvedConfigPath, options) {
524
532
  const shared = { services: null };
525
533
  let processLease;
526
- let internalPortLease = null;
534
+ let portLease;
527
535
  try {
528
- const stdioHttpHandler = new require_src.StdioHttpTransportHandler({ endpoint: new URL(`http://${config.host}:${config.port}${MCP_ENDPOINT_PATH}`) });
529
- const runtimeStateService = new require_src.RuntimeStateService();
530
536
  const serverId = serverOptions.serverId ?? require_src.generateServerId();
537
+ const envPort = process.env.MCP_PORT ? Number(process.env.MCP_PORT) : void 0;
538
+ const requestedPort = options.port ?? (Number.isFinite(envPort) ? envPort : void 0) ?? DEFAULT_PORT;
539
+ const portRange = options.port !== void 0 || Number.isFinite(envPort) ? {
540
+ min: requestedPort,
541
+ max: requestedPort
542
+ } : __agimon_ai_foundation_port_registry.DEFAULT_PORT_RANGE;
543
+ portLease = await createPortRegistryLease(PORT_REGISTRY_SERVICE_STDIO_HTTP, config.host ?? DEFAULT_HOST$1, requestedPort, serverId, TRANSPORT_TYPE_STDIO_HTTP, resolvedConfigPath, portRange);
544
+ const internalPort = portLease.port;
545
+ const transportConfig = {
546
+ ...config,
547
+ port: internalPort
548
+ };
549
+ const stdioHttpHandler = new require_src.StdioHttpTransportHandler({ endpoint: new URL(`http://${transportConfig.host}:${transportConfig.port}${MCP_ENDPOINT_PATH}`) });
550
+ const runtimeStateService = new require_src.RuntimeStateService();
531
551
  const shutdownToken = (0, node_crypto.randomUUID)();
532
- processLease = await createProcessRegistryLease(PROCESS_REGISTRY_SERVICE_STDIO_HTTP, config.host ?? DEFAULT_HOST, config.port ?? DEFAULT_PORT, serverId, TRANSPORT_TYPE_STDIO_HTTP, resolvedConfigPath);
552
+ processLease = await createProcessRegistryLease(PROCESS_REGISTRY_SERVICE_STDIO_HTTP, transportConfig.host ?? DEFAULT_HOST$1, internalPort, serverId, TRANSPORT_TYPE_STDIO_HTTP, resolvedConfigPath);
533
553
  let httpHandler = null;
534
554
  let ownsInternalHttpTransport = false;
535
555
  let isStopping = false;
536
- let releaseInternalPort = async () => {
537
- await releasePortLease(internalPortLease);
538
- internalPortLease = null;
539
- releaseInternalPort = async () => void 0;
540
- };
541
556
  const stopOwnedRuntime = async () => {
542
557
  if (isStopping) return;
543
558
  isStopping = true;
544
559
  try {
545
560
  await Promise.all([stopInternalHttpTransport(stdioHttpHandler, httpHandler, ownsInternalHttpTransport), removeRuntimeRecord(runtimeStateService, serverId)]);
546
- await releaseInternalPort();
561
+ await releasePortLease(portLease ?? null);
547
562
  if (shared.services) await shared.services.dispose();
548
563
  ownsInternalHttpTransport = false;
549
564
  process.exit(0);
@@ -564,15 +579,11 @@ async function startStdioHttpTransport(serverOptions, config, resolvedConfigPath
564
579
  }
565
580
  if (!shared.services) shared.services = await require_src.initializeSharedServices(serverOptions);
566
581
  try {
567
- internalPortLease = await createPortRegistryLease(PORT_REGISTRY_SERVICE_STDIO_HTTP, config.host ?? DEFAULT_HOST, config.port ?? DEFAULT_PORT, serverId, TRANSPORT_TYPE_STDIO_HTTP, resolvedConfigPath);
568
- httpHandler = createStdioHttpInternalTransport(shared.services, config, adminOptions);
582
+ httpHandler = createStdioHttpInternalTransport(shared.services, transportConfig, adminOptions);
569
583
  await httpHandler.start();
570
584
  ownsInternalHttpTransport = true;
571
585
  } catch (error) {
572
- if (!isAddressInUseError(error)) {
573
- await releaseInternalPort();
574
- throw new Error(`Failed to start internal HTTP transport for stdio-http proxy: ${toErrorMessage$9(error)}`);
575
- }
586
+ if (!isAddressInUseError(error)) throw new Error(`Failed to start internal HTTP transport for stdio-http proxy: ${toErrorMessage$9(error)}`);
576
587
  }
577
588
  try {
578
589
  await stdioHttpHandler.start();
@@ -586,14 +597,13 @@ async function startStdioHttpTransport(serverOptions, config, resolvedConfigPath
586
597
  }
587
598
  ownsInternalHttpTransport = false;
588
599
  }
589
- await releaseInternalPort();
590
600
  const retryErrorMessage = toErrorMessage$9(error);
591
601
  const initialErrorMessage = toErrorMessage$9(initialProxyConnectError);
592
602
  const rollbackMessage = rollbackStopErrorMessage ? `; rollback stop failed: ${rollbackStopErrorMessage}` : "";
593
603
  throw new Error(`Failed to start stdio-http proxy bridge: initial connect failed (${initialErrorMessage}); retry failed (${retryErrorMessage})${rollbackMessage}`);
594
604
  }
595
605
  if (ownsInternalHttpTransport) try {
596
- await writeRuntimeRecord(runtimeStateService, createRuntimeRecord(serverId, config, config.port ?? DEFAULT_PORT, shutdownToken, resolvedConfigPath));
606
+ await writeRuntimeRecord(runtimeStateService, createRuntimeRecord(serverId, transportConfig, internalPort, shutdownToken, resolvedConfigPath));
597
607
  } catch (error) {
598
608
  throw new Error(`Failed to persist runtime state for stdio-http server '${serverId}': ${toErrorMessage$9(error)}`);
599
609
  }
@@ -601,7 +611,7 @@ async function startStdioHttpTransport(serverOptions, config, resolvedConfigPath
601
611
  async stop() {
602
612
  try {
603
613
  await Promise.all([stopInternalHttpTransport(stdioHttpHandler, httpHandler, ownsInternalHttpTransport), removeRuntimeRecord(runtimeStateService, serverId)]);
604
- await releaseInternalPort();
614
+ await releasePortLease(portLease ?? null);
605
615
  await processLease?.release({
606
616
  kill: false,
607
617
  releasePort: false
@@ -616,7 +626,7 @@ async function startStdioHttpTransport(serverOptions, config, resolvedConfigPath
616
626
  });
617
627
  } catch (error) {
618
628
  try {
619
- await releasePortLease(internalPortLease);
629
+ await portLease?.release();
620
630
  await processLease?.release({
621
631
  kill: false,
622
632
  releasePort: false
@@ -640,7 +650,7 @@ async function startTransport(transportType, options, resolvedConfigPath, server
640
650
  await startSseTransport(serverOptions, createTransportConfig(options, require_src.TRANSPORT_MODE.SSE));
641
651
  return;
642
652
  }
643
- await startStdioHttpTransport(serverOptions, createTransportConfig(options, require_src.TRANSPORT_MODE.HTTP), resolvedConfigPath);
653
+ await startStdioHttpTransport(serverOptions, createTransportConfig(options, require_src.TRANSPORT_MODE.HTTP), resolvedConfigPath, options);
644
654
  } catch (error) {
645
655
  throw new Error(`Failed to start transport '${transportType}': ${toErrorMessage$9(error)}`);
646
656
  }
@@ -648,7 +658,7 @@ async function startTransport(transportType, options, resolvedConfigPath, server
648
658
  /**
649
659
  * MCP Serve command
650
660
  */
651
- 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/stdio-http internal HTTP)", (val) => parseInt(val, 10), DEFAULT_PORT).option("--host <host>", "Host to bind to (http/sse/stdio-http internal 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) => {
661
+ 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/stdio-http internal HTTP)", (val) => parseInt(val, 10)).option("--host <host>", "Host to bind to (http/sse/stdio-http internal HTTP)", DEFAULT_HOST$1).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) => {
652
662
  try {
653
663
  const transportType = validateTransportType(options.type.toLowerCase());
654
664
  validateProxyMode(options.proxyMode);
@@ -657,11 +667,161 @@ const mcpServeCommand = new commander.Command("mcp-serve").description("Start MC
657
667
  } catch (error) {
658
668
  const rawTransportType = options.type.toLowerCase();
659
669
  const transportType = isValidTransportType(rawTransportType) ? rawTransportType : TRANSPORT_TYPE_STDIO;
660
- console.error(formatStartError(transportType, options.host, options.port, error));
670
+ console.error(formatStartError(transportType, options.host, options.port ?? DEFAULT_PORT, error));
661
671
  process.exit(1);
662
672
  }
663
673
  });
664
674
 
675
+ //#endregion
676
+ //#region src/commands/prestart-http.ts
677
+ /**
678
+ * Prestart HTTP Command
679
+ *
680
+ * Starts an mcp-proxy HTTP runtime in the background, waits until it is healthy,
681
+ * and then exits so the runtime can be reused by other commands.
682
+ */
683
+ const WORKSPACE_MARKERS = [
684
+ "pnpm-workspace.yaml",
685
+ "nx.json",
686
+ ".git"
687
+ ];
688
+ const DEFAULT_HOST = "localhost";
689
+ const DEFAULT_TIMEOUT_MS = 12e4;
690
+ const POLL_INTERVAL_MS = 250;
691
+ function resolveWorkspaceRoot(startPath = process.cwd()) {
692
+ let current = node_path.default.resolve(startPath);
693
+ while (true) {
694
+ for (const marker of WORKSPACE_MARKERS) if ((0, node_fs.existsSync)(node_path.default.join(current, marker))) return current;
695
+ const parent = node_path.default.dirname(current);
696
+ if (parent === current) return process.cwd();
697
+ current = parent;
698
+ }
699
+ }
700
+ function resolveSiblingRegistryPath(registryPath, fileName) {
701
+ if (!registryPath) return;
702
+ const resolved = node_path.default.resolve(registryPath);
703
+ if (node_path.default.extname(resolved) === ".json") return node_path.default.join(node_path.default.dirname(resolved), fileName);
704
+ return node_path.default.join(resolved, fileName);
705
+ }
706
+ function buildCliCandidates() {
707
+ const __filename$1 = (0, node_url.fileURLToPath)(require("url").pathToFileURL(__filename).href);
708
+ const __dirname$1 = node_path.default.dirname(__filename$1);
709
+ const distCandidates = [
710
+ node_path.default.resolve(__dirname$1, "cli.mjs"),
711
+ node_path.default.resolve(__dirname$1, "..", "dist", "cli.mjs"),
712
+ node_path.default.resolve(__dirname$1, "..", "..", "dist", "cli.mjs")
713
+ ];
714
+ const srcCandidates = [node_path.default.resolve(__dirname$1, "..", "cli.ts"), node_path.default.resolve(__dirname$1, "..", "..", "src", "cli.ts")];
715
+ for (const candidate of distCandidates) if ((0, node_fs.existsSync)(candidate)) return {
716
+ command: process.execPath,
717
+ args: [candidate]
718
+ };
719
+ for (const candidate of srcCandidates) if ((0, node_fs.existsSync)(candidate)) return {
720
+ command: process.execPath,
721
+ args: [
722
+ "--import",
723
+ "tsx",
724
+ candidate
725
+ ]
726
+ };
727
+ throw new Error("Unable to locate mcp-proxy CLI entrypoint");
728
+ }
729
+ function parseTimeoutMs(value) {
730
+ if (!value) return DEFAULT_TIMEOUT_MS;
731
+ const parsed = Number.parseInt(value, 10);
732
+ if (!Number.isInteger(parsed) || parsed <= 0) throw new Error(`Invalid timeout value: ${value}`);
733
+ return parsed;
734
+ }
735
+ async function waitForFile(filePath, timeoutMs) {
736
+ const deadline = Date.now() + timeoutMs;
737
+ while (Date.now() < deadline) {
738
+ try {
739
+ await (0, node_fs_promises.access)(filePath);
740
+ return;
741
+ } catch {}
742
+ await new Promise((resolve$2) => setTimeout(resolve$2, POLL_INTERVAL_MS));
743
+ }
744
+ throw new Error(`Timed out waiting for runtime state file: ${filePath}`);
745
+ }
746
+ async function waitForHealthyRuntime(serverId, timeoutMs) {
747
+ const runtimeStateService = new require_src.RuntimeStateService();
748
+ const deadline = Date.now() + timeoutMs;
749
+ while (Date.now() < deadline) {
750
+ const record = await runtimeStateService.read(serverId);
751
+ if (record) {
752
+ const healthUrl = `http://${record.host}:${record.port}/health`;
753
+ try {
754
+ const response = await fetch(healthUrl);
755
+ if (response.ok) {
756
+ const payload = await response.json().catch(() => null);
757
+ if (!payload || payload.transport === "http") return {
758
+ host: record.host,
759
+ port: record.port
760
+ };
761
+ }
762
+ } catch {}
763
+ }
764
+ await new Promise((resolve$2) => setTimeout(resolve$2, POLL_INTERVAL_MS));
765
+ }
766
+ throw new Error(`Timed out waiting for HTTP runtime '${serverId}' to become healthy`);
767
+ }
768
+ function spawnBackgroundRuntime(args, env) {
769
+ const { command, args: baseArgs } = buildCliCandidates();
770
+ const child = (0, node_child_process.spawn)(command, [...baseArgs, ...args], {
771
+ detached: true,
772
+ stdio: "ignore",
773
+ env
774
+ });
775
+ child.unref();
776
+ return child;
777
+ }
778
+ 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).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) => {
779
+ const serverId = options.id || require_src.generateServerId();
780
+ const timeoutMs = parseTimeoutMs(options.timeoutMs);
781
+ const registryPath = options.registryPath || options.registryDir;
782
+ new require_src.RuntimeStateService();
783
+ const workspaceRoot = resolveWorkspaceRoot(process.cwd());
784
+ const childEnv = {
785
+ ...process.env,
786
+ ...registryPath ? {
787
+ PORT_REGISTRY_PATH: registryPath,
788
+ PROCESS_REGISTRY_PATH: resolveSiblingRegistryPath(registryPath, "processes.json")
789
+ } : {}
790
+ };
791
+ const child = spawnBackgroundRuntime([
792
+ "mcp-serve",
793
+ "--type",
794
+ "http",
795
+ "--id",
796
+ serverId,
797
+ "--host",
798
+ options.host || DEFAULT_HOST,
799
+ ...options.port !== void 0 ? ["--port", String(options.port)] : [],
800
+ ...options.config ? ["--config", options.config] : [],
801
+ ...options.cache === false ? ["--no-cache"] : [],
802
+ ...options.definitionsCache ? ["--definitions-cache", options.definitionsCache] : [],
803
+ ...options.clearDefinitionsCache ? ["--clear-definitions-cache"] : [],
804
+ "--proxy-mode",
805
+ options.proxyMode
806
+ ], childEnv);
807
+ const childExit = new Promise((_, reject) => {
808
+ child.once("exit", (code, signal) => {
809
+ reject(/* @__PURE__ */ new Error(`Background runtime exited before becoming healthy (code=${code ?? "null"}, signal=${signal ?? "null"})`));
810
+ });
811
+ });
812
+ const runtimeFile = node_path.default.join(require_src.RuntimeStateService.getDefaultRuntimeDir(), `${serverId}.runtime.json`);
813
+ try {
814
+ await Promise.race([waitForFile(runtimeFile, timeoutMs), childExit]);
815
+ const { host, port } = await Promise.race([waitForHealthyRuntime(serverId, timeoutMs), childExit]);
816
+ process.stdout.write(`mcp-proxy HTTP runtime ready at http://${host}:${port} (${serverId})\n`);
817
+ process.stdout.write(`runtimeId=${serverId}\n`);
818
+ process.stdout.write(`runtimeUrl=http://${host}:${port}\n`);
819
+ process.stdout.write(`workspaceRoot=${workspaceRoot}\n`);
820
+ } catch (error) {
821
+ throw new Error(`Failed to prestart HTTP runtime '${serverId}': ${error instanceof Error ? error.message : String(error)}`);
822
+ }
823
+ });
824
+
665
825
  //#endregion
666
826
  //#region src/commands/bootstrap.ts
667
827
  function toErrorMessage$8(error) {
@@ -1490,6 +1650,7 @@ async function main() {
1490
1650
  program.name("mcp-proxy").description("MCP proxy server package").version(require_src.version);
1491
1651
  program.addCommand(initCommand);
1492
1652
  program.addCommand(mcpServeCommand);
1653
+ program.addCommand(prestartHttpCommand);
1493
1654
  program.addCommand(searchToolsCommand);
1494
1655
  program.addCommand(describeToolsCommand);
1495
1656
  program.addCommand(useToolCommand);
package/dist/cli.mjs CHANGED
@@ -1,13 +1,15 @@
1
1
  #!/usr/bin/env node
2
- import { D as findConfigFile, E as generateServerId, T as DefinitionsCacheService, 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-DQ_MagA0.mjs";
2
+ import { D as findConfigFile, E as generateServerId, T as DefinitionsCacheService, 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-CNXwWpA2.mjs";
3
3
  import { randomUUID } from "node:crypto";
4
4
  import { access, writeFile } from "node:fs/promises";
5
- import { join, resolve } from "node:path";
6
- import { constants } from "node:fs";
5
+ import path, { join, resolve } from "node:path";
6
+ import { constants, existsSync } from "node:fs";
7
7
  import { Liquid } from "liquidjs";
8
+ import { spawn } from "node:child_process";
8
9
  import { Command } from "commander";
9
10
  import { ProcessRegistryService } from "@agimon-ai/foundation-process-registry";
10
- import { PortRegistryService } from "@agimon-ai/foundation-port-registry";
11
+ import { DEFAULT_PORT_RANGE, PortRegistryService } from "@agimon-ai/foundation-port-registry";
12
+ import { fileURLToPath } from "node:url";
11
13
 
12
14
  //#region src/utils/output.ts
13
15
  function writeLine(message = "") {
@@ -139,7 +141,7 @@ const CONFIG_FILE_NAMES = [
139
141
  ];
140
142
  const MCP_ENDPOINT_PATH = "/mcp";
141
143
  const DEFAULT_PORT = 3e3;
142
- const DEFAULT_HOST = "localhost";
144
+ const DEFAULT_HOST$1 = "localhost";
143
145
  const TRANSPORT_TYPE_STDIO = "stdio";
144
146
  const TRANSPORT_TYPE_HTTP = "http";
145
147
  const TRANSPORT_TYPE_SSE = "sse";
@@ -218,10 +220,11 @@ function validateProxyMode(mode) {
218
220
  if (!isValidProxyMode(mode)) throw new Error(`Unknown proxy mode: '${mode}'. Valid options: meta, flat, search`);
219
221
  }
220
222
  function createTransportConfig(options, mode) {
223
+ const envPort = process.env.MCP_PORT ? Number(process.env.MCP_PORT) : void 0;
221
224
  return {
222
225
  mode,
223
- port: options.port || Number(process.env.MCP_PORT) || DEFAULT_PORT,
224
- host: options.host || process.env.MCP_HOST || DEFAULT_HOST
226
+ port: options.port ?? (Number.isFinite(envPort) ? envPort : void 0) ?? DEFAULT_PORT,
227
+ host: options.host || process.env.MCP_HOST || DEFAULT_HOST$1
225
228
  };
226
229
  }
227
230
  function createServerOptions(options, resolvedConfigPath, serverId) {
@@ -242,7 +245,7 @@ function formatStartError(type, host, port, error) {
242
245
  function createRuntimeRecord(serverId, config, port, shutdownToken, configPath) {
243
246
  return {
244
247
  serverId,
245
- host: config.host ?? DEFAULT_HOST,
248
+ host: config.host ?? DEFAULT_HOST$1,
246
249
  port,
247
250
  transport: RUNTIME_TRANSPORT,
248
251
  shutdownToken,
@@ -260,7 +263,10 @@ function createProcessRegistryService() {
260
263
  function getRegistryEnvironment() {
261
264
  return process.env.NODE_ENV ?? "development";
262
265
  }
263
- async function createPortRegistryLease(serviceName, host, preferredPort, serverId, transport, configPath) {
266
+ async function createPortRegistryLease(serviceName, host, preferredPort, serverId, transport, configPath, portRange = {
267
+ min: preferredPort,
268
+ max: preferredPort
269
+ }) {
264
270
  const portRegistry = createPortRegistryService();
265
271
  const result = await portRegistry.reservePort({
266
272
  repositoryPath: PORT_REGISTRY_REPOSITORY_PATH,
@@ -270,10 +276,7 @@ async function createPortRegistryLease(serviceName, host, preferredPort, serverI
270
276
  pid: process.pid,
271
277
  host,
272
278
  preferredPort,
273
- portRange: {
274
- min: preferredPort,
275
- max: preferredPort
276
- },
279
+ portRange,
277
280
  force: true,
278
281
  metadata: {
279
282
  transport,
@@ -283,19 +286,22 @@ async function createPortRegistryLease(serviceName, host, preferredPort, serverI
283
286
  });
284
287
  if (!result.success || !result.record) throw new Error(result.error || `Failed to reserve port ${preferredPort} in port registry`);
285
288
  let released = false;
286
- return { release: async () => {
287
- if (released) return;
288
- released = true;
289
- const releaseResult = await portRegistry.releasePort({
290
- repositoryPath: PORT_REGISTRY_REPOSITORY_PATH,
291
- serviceName,
292
- serviceType: PORT_REGISTRY_SERVICE_TYPE,
293
- pid: process.pid,
294
- environment: getRegistryEnvironment(),
295
- force: true
296
- });
297
- if (!releaseResult.success && releaseResult.error && !releaseResult.error.includes("No matching registry entry")) throw new Error(releaseResult.error || `Failed to release port for ${serviceName}`);
298
- } };
289
+ return {
290
+ port: result.record.port,
291
+ release: async () => {
292
+ if (released) return;
293
+ released = true;
294
+ const releaseResult = await portRegistry.releasePort({
295
+ repositoryPath: PORT_REGISTRY_REPOSITORY_PATH,
296
+ serviceName,
297
+ serviceType: PORT_REGISTRY_SERVICE_TYPE,
298
+ pid: process.pid,
299
+ environment: getRegistryEnvironment(),
300
+ force: true
301
+ });
302
+ if (!releaseResult.success && releaseResult.error && !releaseResult.error.includes("No matching registry entry")) throw new Error(releaseResult.error || `Failed to release port for ${serviceName}`);
303
+ }
304
+ };
299
305
  }
300
306
  async function createProcessRegistryLease(serviceName, host, port, serverId, transport, configPath) {
301
307
  const processRegistry = createProcessRegistryService();
@@ -438,13 +444,14 @@ async function createAndStartHttpRuntime(serverOptions, config, resolvedConfigPa
438
444
  const runtimeStateService = new RuntimeStateService();
439
445
  const shutdownToken = randomUUID();
440
446
  const runtimeServerId = serverOptions.serverId ?? generateServerId();
441
- const processLease = await createProcessRegistryLease(PROCESS_REGISTRY_SERVICE_HTTP, config.host ?? DEFAULT_HOST, config.port ?? DEFAULT_PORT, runtimeServerId, TRANSPORT_TYPE_HTTP, resolvedConfigPath);
442
- const portLease = await createPortRegistryLease(PORT_REGISTRY_SERVICE_HTTP, config.host ?? DEFAULT_HOST, config.port ?? DEFAULT_PORT, runtimeServerId, TRANSPORT_TYPE_HTTP, resolvedConfigPath);
447
+ const runtimePort = config.port ?? DEFAULT_PORT;
448
+ const processLease = await createProcessRegistryLease(PROCESS_REGISTRY_SERVICE_HTTP, config.host ?? DEFAULT_HOST$1, runtimePort, runtimeServerId, TRANSPORT_TYPE_HTTP, resolvedConfigPath);
449
+ const portLease = await createPortRegistryLease(PORT_REGISTRY_SERVICE_HTTP, config.host ?? DEFAULT_HOST$1, runtimePort, runtimeServerId, TRANSPORT_TYPE_HTTP, resolvedConfigPath);
443
450
  let releasePort = async () => {
444
- await releasePortLease(portLease);
451
+ await releasePortLease(portLease ?? null);
445
452
  releasePort = async () => void 0;
446
453
  };
447
- const runtimeRecord = createRuntimeRecord(runtimeServerId, config, config.port ?? DEFAULT_PORT, shutdownToken, resolvedConfigPath);
454
+ const runtimeRecord = createRuntimeRecord(runtimeServerId, config, runtimePort, shutdownToken, resolvedConfigPath);
448
455
  let handler;
449
456
  let isStopping = false;
450
457
  const stopHandler = async () => {
@@ -520,30 +527,37 @@ async function startSseTransport(serverOptions, config) {
520
527
  throw new Error(`Failed to start SSE transport: ${toErrorMessage$9(error)}`);
521
528
  }
522
529
  }
523
- async function startStdioHttpTransport(serverOptions, config, resolvedConfigPath) {
530
+ async function startStdioHttpTransport(serverOptions, config, resolvedConfigPath, options) {
524
531
  const shared = { services: null };
525
532
  let processLease;
526
- let internalPortLease = null;
533
+ let portLease;
527
534
  try {
528
- const stdioHttpHandler = new StdioHttpTransportHandler({ endpoint: new URL(`http://${config.host}:${config.port}${MCP_ENDPOINT_PATH}`) });
529
- const runtimeStateService = new RuntimeStateService();
530
535
  const serverId = serverOptions.serverId ?? generateServerId();
536
+ const envPort = process.env.MCP_PORT ? Number(process.env.MCP_PORT) : void 0;
537
+ const requestedPort = options.port ?? (Number.isFinite(envPort) ? envPort : void 0) ?? DEFAULT_PORT;
538
+ const portRange = options.port !== void 0 || Number.isFinite(envPort) ? {
539
+ min: requestedPort,
540
+ max: requestedPort
541
+ } : DEFAULT_PORT_RANGE;
542
+ portLease = await createPortRegistryLease(PORT_REGISTRY_SERVICE_STDIO_HTTP, config.host ?? DEFAULT_HOST$1, requestedPort, serverId, TRANSPORT_TYPE_STDIO_HTTP, resolvedConfigPath, portRange);
543
+ const internalPort = portLease.port;
544
+ const transportConfig = {
545
+ ...config,
546
+ port: internalPort
547
+ };
548
+ const stdioHttpHandler = new StdioHttpTransportHandler({ endpoint: new URL(`http://${transportConfig.host}:${transportConfig.port}${MCP_ENDPOINT_PATH}`) });
549
+ const runtimeStateService = new RuntimeStateService();
531
550
  const shutdownToken = randomUUID();
532
- processLease = await createProcessRegistryLease(PROCESS_REGISTRY_SERVICE_STDIO_HTTP, config.host ?? DEFAULT_HOST, config.port ?? DEFAULT_PORT, serverId, TRANSPORT_TYPE_STDIO_HTTP, resolvedConfigPath);
551
+ processLease = await createProcessRegistryLease(PROCESS_REGISTRY_SERVICE_STDIO_HTTP, transportConfig.host ?? DEFAULT_HOST$1, internalPort, serverId, TRANSPORT_TYPE_STDIO_HTTP, resolvedConfigPath);
533
552
  let httpHandler = null;
534
553
  let ownsInternalHttpTransport = false;
535
554
  let isStopping = false;
536
- let releaseInternalPort = async () => {
537
- await releasePortLease(internalPortLease);
538
- internalPortLease = null;
539
- releaseInternalPort = async () => void 0;
540
- };
541
555
  const stopOwnedRuntime = async () => {
542
556
  if (isStopping) return;
543
557
  isStopping = true;
544
558
  try {
545
559
  await Promise.all([stopInternalHttpTransport(stdioHttpHandler, httpHandler, ownsInternalHttpTransport), removeRuntimeRecord(runtimeStateService, serverId)]);
546
- await releaseInternalPort();
560
+ await releasePortLease(portLease ?? null);
547
561
  if (shared.services) await shared.services.dispose();
548
562
  ownsInternalHttpTransport = false;
549
563
  process.exit(0);
@@ -564,15 +578,11 @@ async function startStdioHttpTransport(serverOptions, config, resolvedConfigPath
564
578
  }
565
579
  if (!shared.services) shared.services = await initializeSharedServices(serverOptions);
566
580
  try {
567
- internalPortLease = await createPortRegistryLease(PORT_REGISTRY_SERVICE_STDIO_HTTP, config.host ?? DEFAULT_HOST, config.port ?? DEFAULT_PORT, serverId, TRANSPORT_TYPE_STDIO_HTTP, resolvedConfigPath);
568
- httpHandler = createStdioHttpInternalTransport(shared.services, config, adminOptions);
581
+ httpHandler = createStdioHttpInternalTransport(shared.services, transportConfig, adminOptions);
569
582
  await httpHandler.start();
570
583
  ownsInternalHttpTransport = true;
571
584
  } catch (error) {
572
- if (!isAddressInUseError(error)) {
573
- await releaseInternalPort();
574
- throw new Error(`Failed to start internal HTTP transport for stdio-http proxy: ${toErrorMessage$9(error)}`);
575
- }
585
+ if (!isAddressInUseError(error)) throw new Error(`Failed to start internal HTTP transport for stdio-http proxy: ${toErrorMessage$9(error)}`);
576
586
  }
577
587
  try {
578
588
  await stdioHttpHandler.start();
@@ -586,14 +596,13 @@ async function startStdioHttpTransport(serverOptions, config, resolvedConfigPath
586
596
  }
587
597
  ownsInternalHttpTransport = false;
588
598
  }
589
- await releaseInternalPort();
590
599
  const retryErrorMessage = toErrorMessage$9(error);
591
600
  const initialErrorMessage = toErrorMessage$9(initialProxyConnectError);
592
601
  const rollbackMessage = rollbackStopErrorMessage ? `; rollback stop failed: ${rollbackStopErrorMessage}` : "";
593
602
  throw new Error(`Failed to start stdio-http proxy bridge: initial connect failed (${initialErrorMessage}); retry failed (${retryErrorMessage})${rollbackMessage}`);
594
603
  }
595
604
  if (ownsInternalHttpTransport) try {
596
- await writeRuntimeRecord(runtimeStateService, createRuntimeRecord(serverId, config, config.port ?? DEFAULT_PORT, shutdownToken, resolvedConfigPath));
605
+ await writeRuntimeRecord(runtimeStateService, createRuntimeRecord(serverId, transportConfig, internalPort, shutdownToken, resolvedConfigPath));
597
606
  } catch (error) {
598
607
  throw new Error(`Failed to persist runtime state for stdio-http server '${serverId}': ${toErrorMessage$9(error)}`);
599
608
  }
@@ -601,7 +610,7 @@ async function startStdioHttpTransport(serverOptions, config, resolvedConfigPath
601
610
  async stop() {
602
611
  try {
603
612
  await Promise.all([stopInternalHttpTransport(stdioHttpHandler, httpHandler, ownsInternalHttpTransport), removeRuntimeRecord(runtimeStateService, serverId)]);
604
- await releaseInternalPort();
613
+ await releasePortLease(portLease ?? null);
605
614
  await processLease?.release({
606
615
  kill: false,
607
616
  releasePort: false
@@ -616,7 +625,7 @@ async function startStdioHttpTransport(serverOptions, config, resolvedConfigPath
616
625
  });
617
626
  } catch (error) {
618
627
  try {
619
- await releasePortLease(internalPortLease);
628
+ await portLease?.release();
620
629
  await processLease?.release({
621
630
  kill: false,
622
631
  releasePort: false
@@ -640,7 +649,7 @@ async function startTransport(transportType, options, resolvedConfigPath, server
640
649
  await startSseTransport(serverOptions, createTransportConfig(options, TRANSPORT_MODE.SSE));
641
650
  return;
642
651
  }
643
- await startStdioHttpTransport(serverOptions, createTransportConfig(options, TRANSPORT_MODE.HTTP), resolvedConfigPath);
652
+ await startStdioHttpTransport(serverOptions, createTransportConfig(options, TRANSPORT_MODE.HTTP), resolvedConfigPath, options);
644
653
  } catch (error) {
645
654
  throw new Error(`Failed to start transport '${transportType}': ${toErrorMessage$9(error)}`);
646
655
  }
@@ -648,7 +657,7 @@ async function startTransport(transportType, options, resolvedConfigPath, server
648
657
  /**
649
658
  * MCP Serve command
650
659
  */
651
- 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/stdio-http internal HTTP)", (val) => parseInt(val, 10), DEFAULT_PORT).option("--host <host>", "Host to bind to (http/sse/stdio-http internal 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) => {
660
+ 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/stdio-http internal HTTP)", (val) => parseInt(val, 10)).option("--host <host>", "Host to bind to (http/sse/stdio-http internal HTTP)", DEFAULT_HOST$1).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) => {
652
661
  try {
653
662
  const transportType = validateTransportType(options.type.toLowerCase());
654
663
  validateProxyMode(options.proxyMode);
@@ -657,11 +666,161 @@ const mcpServeCommand = new Command("mcp-serve").description("Start MCP server w
657
666
  } catch (error) {
658
667
  const rawTransportType = options.type.toLowerCase();
659
668
  const transportType = isValidTransportType(rawTransportType) ? rawTransportType : TRANSPORT_TYPE_STDIO;
660
- console.error(formatStartError(transportType, options.host, options.port, error));
669
+ console.error(formatStartError(transportType, options.host, options.port ?? DEFAULT_PORT, error));
661
670
  process.exit(1);
662
671
  }
663
672
  });
664
673
 
674
+ //#endregion
675
+ //#region src/commands/prestart-http.ts
676
+ /**
677
+ * Prestart HTTP Command
678
+ *
679
+ * Starts an mcp-proxy HTTP runtime in the background, waits until it is healthy,
680
+ * and then exits so the runtime can be reused by other commands.
681
+ */
682
+ const WORKSPACE_MARKERS = [
683
+ "pnpm-workspace.yaml",
684
+ "nx.json",
685
+ ".git"
686
+ ];
687
+ const DEFAULT_HOST = "localhost";
688
+ const DEFAULT_TIMEOUT_MS = 12e4;
689
+ const POLL_INTERVAL_MS = 250;
690
+ function resolveWorkspaceRoot(startPath = process.cwd()) {
691
+ let current = path.resolve(startPath);
692
+ while (true) {
693
+ for (const marker of WORKSPACE_MARKERS) if (existsSync(path.join(current, marker))) return current;
694
+ const parent = path.dirname(current);
695
+ if (parent === current) return process.cwd();
696
+ current = parent;
697
+ }
698
+ }
699
+ function resolveSiblingRegistryPath(registryPath, fileName) {
700
+ if (!registryPath) return;
701
+ const resolved = path.resolve(registryPath);
702
+ if (path.extname(resolved) === ".json") return path.join(path.dirname(resolved), fileName);
703
+ return path.join(resolved, fileName);
704
+ }
705
+ function buildCliCandidates() {
706
+ const __filename = fileURLToPath(import.meta.url);
707
+ const __dirname = path.dirname(__filename);
708
+ const distCandidates = [
709
+ path.resolve(__dirname, "cli.mjs"),
710
+ path.resolve(__dirname, "..", "dist", "cli.mjs"),
711
+ path.resolve(__dirname, "..", "..", "dist", "cli.mjs")
712
+ ];
713
+ const srcCandidates = [path.resolve(__dirname, "..", "cli.ts"), path.resolve(__dirname, "..", "..", "src", "cli.ts")];
714
+ for (const candidate of distCandidates) if (existsSync(candidate)) return {
715
+ command: process.execPath,
716
+ args: [candidate]
717
+ };
718
+ for (const candidate of srcCandidates) if (existsSync(candidate)) return {
719
+ command: process.execPath,
720
+ args: [
721
+ "--import",
722
+ "tsx",
723
+ candidate
724
+ ]
725
+ };
726
+ throw new Error("Unable to locate mcp-proxy CLI entrypoint");
727
+ }
728
+ function parseTimeoutMs(value) {
729
+ if (!value) return DEFAULT_TIMEOUT_MS;
730
+ const parsed = Number.parseInt(value, 10);
731
+ if (!Number.isInteger(parsed) || parsed <= 0) throw new Error(`Invalid timeout value: ${value}`);
732
+ return parsed;
733
+ }
734
+ async function waitForFile(filePath, timeoutMs) {
735
+ const deadline = Date.now() + timeoutMs;
736
+ while (Date.now() < deadline) {
737
+ try {
738
+ await access(filePath);
739
+ return;
740
+ } catch {}
741
+ await new Promise((resolve$1) => setTimeout(resolve$1, POLL_INTERVAL_MS));
742
+ }
743
+ throw new Error(`Timed out waiting for runtime state file: ${filePath}`);
744
+ }
745
+ async function waitForHealthyRuntime(serverId, timeoutMs) {
746
+ const runtimeStateService = new RuntimeStateService();
747
+ const deadline = Date.now() + timeoutMs;
748
+ while (Date.now() < deadline) {
749
+ const record = await runtimeStateService.read(serverId);
750
+ if (record) {
751
+ const healthUrl = `http://${record.host}:${record.port}/health`;
752
+ try {
753
+ const response = await fetch(healthUrl);
754
+ if (response.ok) {
755
+ const payload = await response.json().catch(() => null);
756
+ if (!payload || payload.transport === "http") return {
757
+ host: record.host,
758
+ port: record.port
759
+ };
760
+ }
761
+ } catch {}
762
+ }
763
+ await new Promise((resolve$1) => setTimeout(resolve$1, POLL_INTERVAL_MS));
764
+ }
765
+ throw new Error(`Timed out waiting for HTTP runtime '${serverId}' to become healthy`);
766
+ }
767
+ function spawnBackgroundRuntime(args, env) {
768
+ const { command, args: baseArgs } = buildCliCandidates();
769
+ const child = spawn(command, [...baseArgs, ...args], {
770
+ detached: true,
771
+ stdio: "ignore",
772
+ env
773
+ });
774
+ child.unref();
775
+ return child;
776
+ }
777
+ const prestartHttpCommand = new 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).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) => {
778
+ const serverId = options.id || generateServerId();
779
+ const timeoutMs = parseTimeoutMs(options.timeoutMs);
780
+ const registryPath = options.registryPath || options.registryDir;
781
+ new RuntimeStateService();
782
+ const workspaceRoot = resolveWorkspaceRoot(process.cwd());
783
+ const childEnv = {
784
+ ...process.env,
785
+ ...registryPath ? {
786
+ PORT_REGISTRY_PATH: registryPath,
787
+ PROCESS_REGISTRY_PATH: resolveSiblingRegistryPath(registryPath, "processes.json")
788
+ } : {}
789
+ };
790
+ const child = spawnBackgroundRuntime([
791
+ "mcp-serve",
792
+ "--type",
793
+ "http",
794
+ "--id",
795
+ serverId,
796
+ "--host",
797
+ options.host || DEFAULT_HOST,
798
+ ...options.port !== void 0 ? ["--port", String(options.port)] : [],
799
+ ...options.config ? ["--config", options.config] : [],
800
+ ...options.cache === false ? ["--no-cache"] : [],
801
+ ...options.definitionsCache ? ["--definitions-cache", options.definitionsCache] : [],
802
+ ...options.clearDefinitionsCache ? ["--clear-definitions-cache"] : [],
803
+ "--proxy-mode",
804
+ options.proxyMode
805
+ ], childEnv);
806
+ const childExit = new Promise((_, reject) => {
807
+ child.once("exit", (code, signal) => {
808
+ reject(/* @__PURE__ */ new Error(`Background runtime exited before becoming healthy (code=${code ?? "null"}, signal=${signal ?? "null"})`));
809
+ });
810
+ });
811
+ const runtimeFile = path.join(RuntimeStateService.getDefaultRuntimeDir(), `${serverId}.runtime.json`);
812
+ try {
813
+ await Promise.race([waitForFile(runtimeFile, timeoutMs), childExit]);
814
+ const { host, port } = await Promise.race([waitForHealthyRuntime(serverId, timeoutMs), childExit]);
815
+ process.stdout.write(`mcp-proxy HTTP runtime ready at http://${host}:${port} (${serverId})\n`);
816
+ process.stdout.write(`runtimeId=${serverId}\n`);
817
+ process.stdout.write(`runtimeUrl=http://${host}:${port}\n`);
818
+ process.stdout.write(`workspaceRoot=${workspaceRoot}\n`);
819
+ } catch (error) {
820
+ throw new Error(`Failed to prestart HTTP runtime '${serverId}': ${error instanceof Error ? error.message : String(error)}`);
821
+ }
822
+ });
823
+
665
824
  //#endregion
666
825
  //#region src/commands/bootstrap.ts
667
826
  function toErrorMessage$8(error) {
@@ -1490,6 +1649,7 @@ async function main() {
1490
1649
  program.name("mcp-proxy").description("MCP proxy server package").version(version);
1491
1650
  program.addCommand(initCommand);
1492
1651
  program.addCommand(mcpServeCommand);
1652
+ program.addCommand(prestartHttpCommand);
1493
1653
  program.addCommand(searchToolsCommand);
1494
1654
  program.addCommand(describeToolsCommand);
1495
1655
  program.addCommand(useToolCommand);
package/dist/index.cjs CHANGED
@@ -1,4 +1,4 @@
1
- const require_src = require('./src-6KF7hZTe.cjs');
1
+ const require_src = require('./src-Cm2AVRBA.cjs');
2
2
 
3
3
  exports.ConfigFetcherService = require_src.ConfigFetcherService;
4
4
  exports.DefinitionsCacheService = require_src.DefinitionsCacheService;
package/dist/index.mjs CHANGED
@@ -1,3 +1,3 @@
1
- import { C as SearchListToolsTool, D as findConfigFile, E as generateServerId, S as UseToolTool, T as DefinitionsCacheService, _ as StopServerService, a as createProxyContainer, b as createProxyLogger, c as createStdioHttpTransportHandler, f as StdioHttpTransportHandler, g as SkillService, h as HttpTransportHandler, i as createHttpTransportHandler, l as createStdioTransportHandler, m as SseTransportHandler, n as createServer, p as StdioTransportHandler, r as createSessionServer, s as createSseTransportHandler, t as TRANSPORT_MODE, u as initializeSharedServices, v as RuntimeStateService, w as DescribeToolsTool, x as ConfigFetcherService, y as McpClientManagerService } from "./src-DQ_MagA0.mjs";
1
+ import { C as SearchListToolsTool, D as findConfigFile, E as generateServerId, S as UseToolTool, T as DefinitionsCacheService, _ as StopServerService, a as createProxyContainer, b as createProxyLogger, c as createStdioHttpTransportHandler, f as StdioHttpTransportHandler, g as SkillService, h as HttpTransportHandler, i as createHttpTransportHandler, l as createStdioTransportHandler, m as SseTransportHandler, n as createServer, p as StdioTransportHandler, r as createSessionServer, s as createSseTransportHandler, t as TRANSPORT_MODE, u as initializeSharedServices, v as RuntimeStateService, w as DescribeToolsTool, x as ConfigFetcherService, y as McpClientManagerService } from "./src-CNXwWpA2.mjs";
2
2
 
3
3
  export { ConfigFetcherService, DefinitionsCacheService, DescribeToolsTool, HttpTransportHandler, McpClientManagerService, RuntimeStateService, SearchListToolsTool, SkillService, SseTransportHandler, StdioHttpTransportHandler, StdioTransportHandler, StopServerService, TRANSPORT_MODE, UseToolTool, createHttpTransportHandler, createProxyContainer, createProxyLogger, createServer, createSessionServer, createSseTransportHandler, createStdioHttpTransportHandler, createStdioTransportHandler, findConfigFile, generateServerId, initializeSharedServices };
@@ -2758,9 +2758,9 @@ function isShutdownResponse(value) {
2758
2758
  * @param path - Request path to append
2759
2759
  * @returns Full runtime URL
2760
2760
  */
2761
- function buildRuntimeUrl(runtime, path) {
2761
+ function buildRuntimeUrl(runtime, path$1) {
2762
2762
  if (!ALLOWED_HOSTS.has(runtime.host)) throw new Error(`Refusing to connect to non-loopback host '${runtime.host}'. Only ${Array.from(ALLOWED_HOSTS).join(", ")} are allowed.`);
2763
- return `${HTTP_PROTOCOL}${runtime.host}${URL_PORT_SEPARATOR}${runtime.port}${path}`;
2763
+ return `${HTTP_PROTOCOL}${runtime.host}${URL_PORT_SEPARATOR}${runtime.port}${path$1}`;
2764
2764
  }
2765
2765
  function toErrorMessage$1(error) {
2766
2766
  return error instanceof Error ? error.message : String(error);
@@ -2938,13 +2938,13 @@ var SkillLoadError = class extends Error {
2938
2938
  * @returns true if path exists, false otherwise
2939
2939
  * @throws Error for unexpected filesystem errors (permission denied, etc.)
2940
2940
  */
2941
- async function pathExists(path) {
2941
+ async function pathExists(path$1) {
2942
2942
  try {
2943
- await access(path);
2943
+ await access(path$1);
2944
2944
  return true;
2945
2945
  } catch (error) {
2946
2946
  if (error instanceof Error && "code" in error && error.code === "ENOENT") return false;
2947
- throw new Error(`Failed to check path existence for "${path}": ${error instanceof Error ? error.message : "Unknown error"}`);
2947
+ throw new Error(`Failed to check path existence for "${path$1}": ${error instanceof Error ? error.message : "Unknown error"}`);
2948
2948
  }
2949
2949
  }
2950
2950
  /**
@@ -4313,7 +4313,7 @@ var StdioHttpTransportHandler = class {
4313
4313
 
4314
4314
  //#endregion
4315
4315
  //#region package.json
4316
- var version = "0.3.19";
4316
+ var version = "0.4.1";
4317
4317
 
4318
4318
  //#endregion
4319
4319
  //#region src/container/index.ts
@@ -4342,7 +4342,7 @@ var StdioHttpTransportHandler = class {
4342
4342
 
4343
4343
  //#endregion
4344
4344
  //#region package.json
4345
- var version = "0.3.19";
4345
+ var version = "0.4.1";
4346
4346
 
4347
4347
  //#endregion
4348
4348
  //#region src/container/index.ts
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@agimon-ai/mcp-proxy",
3
3
  "description": "MCP proxy server package",
4
- "version": "0.4.0",
4
+ "version": "0.4.2",
5
5
  "license": "AGPL-3.0",
6
6
  "keywords": [
7
7
  "mcp",
@@ -27,9 +27,9 @@
27
27
  "js-yaml": "^4.1.0",
28
28
  "liquidjs": "^10.21.0",
29
29
  "zod": "^3.24.1",
30
- "@agimon-ai/foundation-process-registry": "0.2.0",
31
- "@agimon-ai/foundation-port-registry": "0.2.4",
32
- "@agimon-ai/log-sink-mcp": "0.2.5"
30
+ "@agimon-ai/foundation-port-registry": "0.2.6",
31
+ "@agimon-ai/log-sink-mcp": "0.2.6",
32
+ "@agimon-ai/foundation-process-registry": "0.2.2"
33
33
  },
34
34
  "devDependencies": {
35
35
  "@types/express": "^5.0.0",