@agimon-ai/mcp-proxy 0.5.0 → 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, 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-DCIv5S_2.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
  /**
@@ -137,11 +138,26 @@ function resolveWorkspaceRoot(startPath = process.env.PROJECT_PATH || process.cw
137
138
  current = parent;
138
139
  }
139
140
  }
140
- function resolveSiblingRegistryPath(registryPath, fileName) {
141
- if (!registryPath) return;
142
- const resolved = path.resolve(registryPath);
143
- if (path.extname(resolved) === ".json") return path.join(path.dirname(resolved), fileName);
144
- return path.join(resolved, fileName);
141
+ const PROCESS_REGISTRY_SERVICE_HTTP$1 = "mcp-proxy-http";
142
+ async function findExistingHealthyRuntime(workspaceRoot) {
143
+ const match = (await new ProcessRegistryService(process.env.PROCESS_REGISTRY_PATH).listProcesses({
144
+ repositoryPath: workspaceRoot,
145
+ serviceName: PROCESS_REGISTRY_SERVICE_HTTP$1
146
+ }))[0];
147
+ if (!match?.host || !match?.port) return null;
148
+ try {
149
+ const healthUrl = `http://${match.host}:${match.port}/health`;
150
+ if ((await fetch(healthUrl)).ok) {
151
+ const metadata = match.metadata;
152
+ return {
153
+ host: match.host,
154
+ port: match.port,
155
+ serverId: metadata?.serverId ?? "unknown",
156
+ workspaceRoot
157
+ };
158
+ }
159
+ } catch {}
160
+ return null;
145
161
  }
146
162
  function buildCliCandidates() {
147
163
  const __filename = fileURLToPath(import.meta.url);
@@ -216,11 +232,33 @@ function spawnBackgroundRuntime(args, env, cwd) {
216
232
  child.unref();
217
233
  return child;
218
234
  }
235
+ async function stopExistingRuntime(runtimeStateService, serverId, host, port) {
236
+ const runtimes = await runtimeStateService.list();
237
+ const targetHost = host || DEFAULT_HOST$1;
238
+ const match = runtimes.find((r) => {
239
+ if (serverId && r.serverId === serverId) return true;
240
+ if (port !== void 0 && r.host === targetHost && r.port === port) return true;
241
+ return false;
242
+ });
243
+ if (!match) return;
244
+ const stopService = new StopServerService(runtimeStateService);
245
+ try {
246
+ await stopService.stop({
247
+ serverId: match.serverId,
248
+ force: true
249
+ });
250
+ } catch {
251
+ await runtimeStateService.remove(match.serverId);
252
+ }
253
+ }
219
254
  async function prestartHttpRuntime(options) {
220
255
  const serverId = options.id || generateServerId();
221
256
  const timeoutMs = parseTimeoutMs(options.timeoutMs);
222
257
  const registryPath = options.registryPath || options.registryDir;
223
258
  const workspaceRoot = resolveWorkspaceRoot();
259
+ const existing = await findExistingHealthyRuntime(workspaceRoot);
260
+ if (existing) return existing;
261
+ await stopExistingRuntime(new RuntimeStateService(), options.id, options.host, options.port);
224
262
  const childEnv = {
225
263
  ...process.env,
226
264
  ...registryPath ? {
@@ -260,7 +298,7 @@ async function prestartHttpRuntime(options) {
260
298
  workspaceRoot
261
299
  };
262
300
  } catch (error) {
263
- throw new Error(`Failed to prestart HTTP runtime '${serverId}': ${error instanceof Error ? error.message : String(error)}`);
301
+ throw new Error(`Failed to prestart HTTP runtime '${serverId}': ${error instanceof Error ? error.message : String(error)}`, { cause: error });
264
302
  }
265
303
  }
266
304
  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$1).option("-p, --port <port>", "Preferred HTTP port for the runtime", (value) => Number.parseInt(value, 10)).option("-c, --config <path>", "Path to MCP server configuration file").option("--no-cache", "Disable configuration caching, always reload from config file").option("--definitions-cache <path>", "Path to prefetched tool/prompt/skill definitions cache file").option("--clear-definitions-cache", "Delete definitions cache before startup", false).option("--proxy-mode <mode>", "How mcp-proxy exposes downstream tools: meta, flat, or search", "meta").option("--registry-path <path>", "Custom registry path or directory for service discovery").option("--registry-dir <path>", "Custom registry directory for service discovery").option("--timeout-ms <ms>", "How long to wait for the runtime to become healthy", String(DEFAULT_TIMEOUT_MS)).action(async (options) => {
@@ -271,7 +309,7 @@ const prestartHttpCommand = new Command("prestart-http").description("Start an m
271
309
  process.stdout.write(`runtimeUrl=http://${host}:${port}\n`);
272
310
  process.stdout.write(`workspaceRoot=${workspaceRoot}\n`);
273
311
  } catch (error) {
274
- throw new Error(`Failed to prestart HTTP runtime '${options.id || "generated-server-id"}': ${error instanceof Error ? error.message : String(error)}`);
312
+ throw new Error(`Failed to prestart HTTP runtime '${options.id || "generated-server-id"}': ${error instanceof Error ? error.message : String(error)}`, { cause: error });
275
313
  }
276
314
  });
277
315
 
@@ -343,16 +381,37 @@ async function findConfigFileAsync() {
343
381
  const configPath = resolve(projectPath, fileName);
344
382
  if (await pathExists(configPath)) return configPath;
345
383
  }
346
- const cwd = process.cwd();
347
- for (const fileName of CONFIG_FILE_NAMES) {
348
- const configPath = join(cwd, fileName);
349
- 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;
350
394
  }
351
395
  return null;
352
396
  } catch (error) {
353
397
  throw new Error(`Failed to discover MCP config file: ${toErrorMessage$9(error)}`);
354
398
  }
355
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
+ }
356
415
  async function resolveServerId(options, resolvedConfigPath) {
357
416
  const container = createProxyIoCContainer();
358
417
  if (options.id) return options.id;
@@ -374,12 +433,12 @@ function validateTransportType(type) {
374
433
  function validateProxyMode(mode) {
375
434
  if (!isValidProxyMode(mode)) throw new Error(`Unknown proxy mode: '${mode}'. Valid options: meta, flat, search`);
376
435
  }
377
- function createTransportConfig(options, mode) {
436
+ function createTransportConfig(options, mode, proxyDefaults) {
378
437
  const envPort = process.env.MCP_PORT ? Number(process.env.MCP_PORT) : void 0;
379
438
  return {
380
439
  mode,
381
- port: options.port ?? (Number.isFinite(envPort) ? envPort : void 0),
382
- 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
383
442
  };
384
443
  }
385
444
  function createStdioSafeLogger() {
@@ -428,9 +487,6 @@ function createRuntimeRecord(serverId, config, port, shutdownToken, configPath)
428
487
  function createPortRegistryService() {
429
488
  return new PortRegistryService(process.env.PORT_REGISTRY_PATH);
430
489
  }
431
- function createProcessRegistryService() {
432
- return new ProcessRegistryService(process.env.PROCESS_REGISTRY_PATH);
433
- }
434
490
  function getRegistryEnvironment() {
435
491
  return process.env.NODE_ENV ?? "development";
436
492
  }
@@ -477,41 +533,6 @@ async function createPortRegistryLease(serviceName, host, preferredPort, serverI
477
533
  }
478
534
  };
479
535
  }
480
- async function createProcessRegistryLease(serviceName, host, port, serverId, transport, configPath) {
481
- const processRegistry = createProcessRegistryService();
482
- const result = await processRegistry.registerProcess({
483
- repositoryPath: getRegistryRepositoryPath(),
484
- serviceName,
485
- serviceType: PROCESS_REGISTRY_SERVICE_TYPE,
486
- environment: getRegistryEnvironment(),
487
- pid: process.pid,
488
- host,
489
- port,
490
- command: process.argv[1],
491
- args: process.argv.slice(2),
492
- metadata: {
493
- transport,
494
- serverId,
495
- ...configPath ? { configPath } : {}
496
- }
497
- });
498
- if (!result.success || !result.record) throw new Error(result.error || `Failed to register process for ${serviceName}`);
499
- let released = false;
500
- return { release: async (options) => {
501
- if (released) return;
502
- released = true;
503
- const releaseResult = await processRegistry.releaseProcess({
504
- repositoryPath: getRegistryRepositoryPath(),
505
- serviceName,
506
- serviceType: PROCESS_REGISTRY_SERVICE_TYPE,
507
- pid: process.pid,
508
- environment: getRegistryEnvironment(),
509
- kill: options?.kill ?? false,
510
- releasePort: options?.releasePort ?? false
511
- });
512
- if (!releaseResult.success && releaseResult.error && !releaseResult.error.includes("No matching process entry")) throw new Error(releaseResult.error || `Failed to release process for ${serviceName}`);
513
- } };
514
- }
515
536
  async function releasePortLease(lease) {
516
537
  if (!lease) return;
517
538
  await lease.release();
@@ -545,13 +566,19 @@ async function stopOwnedHttpTransport(handler, runtimeStateService, serverId, pr
545
566
  throw new Error(`Failed to stop owned HTTP transport '${serverId}': ${toErrorMessage$9(error)}`);
546
567
  }
547
568
  } finally {
548
- await processLease?.release({
549
- kill: false,
550
- releasePort: false
551
- });
569
+ await processLease?.release({ kill: false });
552
570
  await removeRuntimeRecord(runtimeStateService, serverId);
553
571
  }
554
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
+ }
555
582
  async function cleanupFailedRuntimeStartup(handler, runtimeStateService, serverId, processLease) {
556
583
  try {
557
584
  try {
@@ -560,10 +587,7 @@ async function cleanupFailedRuntimeStartup(handler, runtimeStateService, serverI
560
587
  throw new Error(`Failed to stop HTTP transport during cleanup for '${serverId}': ${toErrorMessage$9(error)}`);
561
588
  }
562
589
  } finally {
563
- await processLease?.release({
564
- kill: false,
565
- releasePort: false
566
- });
590
+ await processLease?.release({ kill: false });
567
591
  await removeRuntimeRecord(runtimeStateService, serverId);
568
592
  }
569
593
  }
@@ -608,7 +632,21 @@ async function createAndStartHttpRuntime(serverOptions, config, resolvedConfigPa
608
632
  ...config,
609
633
  port: runtimePort
610
634
  };
611
- 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
+ });
612
650
  let releasePort = async () => {
613
651
  await releasePortLease(portLease ?? null);
614
652
  releasePort = async () => void 0;
@@ -632,10 +670,7 @@ async function createAndStartHttpRuntime(serverOptions, config, resolvedConfigPa
632
670
  handler = new HttpTransportHandler(() => createSessionServer(sharedServices), runtimeConfig, createHttpAdminOptions(runtimeRecord.serverId, shutdownToken, stopHandler));
633
671
  } catch (error) {
634
672
  await releasePort();
635
- await processLease.release({
636
- kill: false,
637
- releasePort: false
638
- });
673
+ await processLease.release({ kill: false });
639
674
  await sharedServices.dispose();
640
675
  throw new Error(`Failed to create HTTP runtime server: ${toErrorMessage$9(error)}`);
641
676
  }
@@ -643,19 +678,11 @@ async function createAndStartHttpRuntime(serverOptions, config, resolvedConfigPa
643
678
  await startServer(handler, async () => {
644
679
  await releasePort();
645
680
  await sharedServices.dispose();
646
- await processLease.release({
647
- kill: false,
648
- releasePort: false
649
- });
650
- await removeRuntimeRecord(runtimeStateService, runtimeRecord.serverId);
681
+ await cleanupHttpRuntime(runtimeStateService, runtimeRecord.serverId, processLease);
651
682
  });
652
683
  await writeRuntimeRecord(runtimeStateService, runtimeRecord);
653
684
  } catch (error) {
654
685
  await releasePort();
655
- await processLease.release({
656
- kill: false,
657
- releasePort: false
658
- });
659
686
  await sharedServices.dispose();
660
687
  await cleanupFailedRuntimeStartup(handler, runtimeStateService, runtimeRecord.serverId, processLease);
661
688
  throw new Error(`Failed to start HTTP runtime '${runtimeRecord.serverId}': ${toErrorMessage$9(error)}`);
@@ -671,7 +698,24 @@ async function startStdioTransport(serverOptions) {
671
698
  }
672
699
  async function startSseTransport(serverOptions, config) {
673
700
  try {
674
- await startServer(new SseTransportHandler(await createServer(serverOptions), config));
701
+ const requestedPort = config.port;
702
+ const portRange = requestedPort !== void 0 ? {
703
+ min: requestedPort,
704
+ max: requestedPort
705
+ } : DEFAULT_PORT_RANGE;
706
+ const portLease = await createPortRegistryLease("mcp-proxy-sse", config.host ?? DEFAULT_HOST, requestedPort, serverOptions.serverId ?? generateServerId(), TRANSPORT_TYPE_SSE, void 0, portRange);
707
+ const resolvedConfig = {
708
+ ...config,
709
+ port: portLease.port
710
+ };
711
+ const handler = new SseTransportHandler(await createServer(serverOptions), resolvedConfig);
712
+ const shutdown = async () => {
713
+ await handler.stop();
714
+ await portLease.release();
715
+ };
716
+ process.on("SIGINT", shutdown);
717
+ process.on("SIGTERM", shutdown);
718
+ await startServer(handler);
675
719
  } catch (error) {
676
720
  throw new Error(`Failed to start SSE transport: ${toErrorMessage$9(error)}`);
677
721
  }
@@ -679,13 +723,28 @@ async function startSseTransport(serverOptions, config) {
679
723
  async function resolveStdioHttpEndpoint(config, options, resolvedConfigPath) {
680
724
  const repositoryPath = getRegistryRepositoryPath();
681
725
  if (config.port !== void 0) return new URL(`http://${config.host ?? DEFAULT_HOST}:${config.port}${MCP_ENDPOINT_PATH}`);
682
- const result = await createPortRegistryService().getPort({
726
+ const portRegistry = createPortRegistryService();
727
+ const result = await portRegistry.getPort({
683
728
  repositoryPath,
684
729
  serviceName: PORT_REGISTRY_SERVICE_HTTP,
685
730
  serviceType: PORT_REGISTRY_SERVICE_TYPE,
686
731
  environment: getRegistryEnvironment()
687
732
  });
688
- if (result.success && result.record) return new URL(`http://${config.host ?? result.record.host}:${result.record.port}${MCP_ENDPOINT_PATH}`);
733
+ if (result.success && result.record) {
734
+ const host = config.host ?? result.record.host;
735
+ const endpoint = new URL(`http://${host}:${result.record.port}${MCP_ENDPOINT_PATH}`);
736
+ try {
737
+ const healthUrl = `http://${host}:${result.record.port}/health`;
738
+ if ((await fetch(healthUrl)).ok) return endpoint;
739
+ } catch {}
740
+ await portRegistry.releasePort({
741
+ repositoryPath,
742
+ serviceName: PORT_REGISTRY_SERVICE_HTTP,
743
+ serviceType: PORT_REGISTRY_SERVICE_TYPE,
744
+ environment: getRegistryEnvironment(),
745
+ force: true
746
+ });
747
+ }
689
748
  const runtime = await prestartHttpRuntime({
690
749
  host: config.host ?? DEFAULT_HOST,
691
750
  config: options.config || resolvedConfigPath,
@@ -703,21 +762,21 @@ async function startStdioHttpTransport(config, options, resolvedConfigPath) {
703
762
  throw new Error(`Failed to start stdio-http transport: ${toErrorMessage$9(error)}`);
704
763
  }
705
764
  }
706
- async function startTransport(transportType, options, resolvedConfigPath, serverOptions) {
765
+ async function startTransport(transportType, options, resolvedConfigPath, serverOptions, proxyDefaults) {
707
766
  try {
708
767
  if (transportType === TRANSPORT_TYPE_STDIO) {
709
768
  await startStdioTransport(serverOptions);
710
769
  return;
711
770
  }
712
771
  if (transportType === TRANSPORT_TYPE_HTTP) {
713
- await createAndStartHttpRuntime(serverOptions, createTransportConfig(options, TRANSPORT_MODE.HTTP), resolvedConfigPath);
772
+ await createAndStartHttpRuntime(serverOptions, createTransportConfig(options, TRANSPORT_MODE.HTTP, proxyDefaults), resolvedConfigPath);
714
773
  return;
715
774
  }
716
775
  if (transportType === TRANSPORT_TYPE_SSE) {
717
- await startSseTransport(serverOptions, createTransportConfig(options, TRANSPORT_MODE.SSE));
776
+ await startSseTransport(serverOptions, createTransportConfig(options, TRANSPORT_MODE.SSE, proxyDefaults));
718
777
  return;
719
778
  }
720
- await startStdioHttpTransport(createTransportConfig(options, TRANSPORT_MODE.HTTP), options, resolvedConfigPath);
779
+ await startStdioHttpTransport(createTransportConfig(options, TRANSPORT_MODE.HTTP, proxyDefaults), options, resolvedConfigPath);
721
780
  } catch (error) {
722
781
  throw new Error(`Failed to start transport '${transportType}': ${toErrorMessage$9(error)}`);
723
782
  }
@@ -725,18 +784,19 @@ async function startTransport(transportType, options, resolvedConfigPath, server
725
784
  /**
726
785
  * MCP Serve command
727
786
  */
728
- 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) => {
729
788
  try {
730
- const transportType = validateTransportType(options.type.toLowerCase());
731
- validateProxyMode(options.proxyMode);
732
789
  const resolvedConfigPath = options.config || await findConfigFileAsync() || void 0;
733
- 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);
734
794
  } catch (error) {
735
- const rawTransportType = options.type.toLowerCase();
795
+ const rawTransportType = (options.type ?? TRANSPORT_TYPE_STDIO).toLowerCase();
736
796
  const transportType = isValidTransportType(rawTransportType) ? rawTransportType : TRANSPORT_TYPE_STDIO;
737
797
  const envPort = process.env.MCP_PORT ? Number(process.env.MCP_PORT) : void 0;
738
798
  const requestedPort = options.port ?? (Number.isFinite(envPort) ? envPort : void 0);
739
- console.error(formatStartError(transportType, options.host, requestedPort, error));
799
+ console.error(formatStartError(transportType, options.host ?? DEFAULT_HOST, requestedPort, error));
740
800
  process.exit(1);
741
801
  }
742
802
  });
@@ -746,14 +806,58 @@ const mcpServeCommand = new Command("mcp-serve").description("Start MCP server w
746
806
  function toErrorMessage$8(error) {
747
807
  return error instanceof Error ? error.message : String(error);
748
808
  }
749
- async function withConnectedCommandContext(options, run) {
750
- const container = createProxyIoCContainer();
751
- const configFilePath = options.config || findConfigFile();
752
- if (!configFilePath) throw new Error("No config file found. Use --config or create mcp-config.yaml");
753
- const config = await container.createConfigFetcherService({
754
- configFilePath,
755
- useCache: options.useCache
756
- }).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) {
757
861
  const clientManager = container.createClientManagerService();
758
862
  await Promise.all(Object.entries(config.mcpServers).map(async ([serverName, serverConfig]) => {
759
863
  try {
@@ -775,6 +879,17 @@ async function withConnectedCommandContext(options, run) {
775
879
  await clientManager.disconnectAll();
776
880
  }
777
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
+ }
778
893
 
779
894
  //#endregion
780
895
  //#region src/commands/list-tools.ts
@@ -1017,7 +1132,7 @@ function toErrorMessage$5(error) {
1017
1132
  /**
1018
1133
  * Execute an MCP tool with arguments
1019
1134
  */
1020
- 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) => {
1021
1136
  try {
1022
1137
  let toolArgs = {};
1023
1138
  try {
package/dist/index.cjs CHANGED
@@ -1,4 +1,4 @@
1
- const require_src = require('./src-B2m53VQ1.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;