@agiflowai/one-mcp 0.3.12 → 0.3.14

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,192 +1,14 @@
1
1
  #!/usr/bin/env node
2
- import { a as version, d as DefinitionsCacheService, f as generateServerId, i as createServer, l as SkillService, m as ConfigFetcherService, n as SseTransportHandler, p as findConfigFile, r as StdioTransportHandler, s as SearchListToolsTool, t as HttpTransportHandler, u as McpClientManagerService } from "./http-ClRbCldm.mjs";
3
- import { writeFile } from "node:fs/promises";
4
- import { resolve } from "node:path";
2
+ import { _ as findConfigFile, a as SseTransportHandler, c as createServer, d as SearchListToolsTool, g as generateServerId, h as DefinitionsCacheService, i as StdioTransportHandler, l as version, m as McpClientManagerService, n as RuntimeStateService, o as HttpTransportHandler, p as SkillService, r as StdioHttpTransportHandler, s as TRANSPORT_MODE, t as StopServerService, v as ConfigFetcherService } from "./src-D7Yq1bTx.mjs";
3
+ import { access, writeFile } from "node:fs/promises";
4
+ import { constants } from "node:fs";
5
+ import { randomUUID } from "node:crypto";
6
+ import { join, resolve } from "node:path";
5
7
  import { Liquid } from "liquidjs";
8
+ import { spawn } from "node:child_process";
6
9
  import { Command } from "commander";
7
10
  import { log, print } from "@agiflowai/aicode-utils";
8
- import { spawn } from "node:child_process";
9
-
10
- //#region src/templates/mcp-config.yaml.liquid?raw
11
- var mcp_config_yaml_default = "# MCP Server Configuration\n# This file configures the MCP servers that one-mcp 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";
12
-
13
- //#endregion
14
- //#region src/templates/mcp-config.json?raw
15
- 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";
16
-
17
- //#endregion
18
- //#region src/commands/init.ts
19
- /**
20
- * Init Command
21
- *
22
- * DESIGN PATTERNS:
23
- * - Command pattern with Commander for CLI argument parsing
24
- * - Async/await pattern for asynchronous operations
25
- * - Error handling pattern with try-catch and proper exit codes
26
- *
27
- * CODING STANDARDS:
28
- * - Use async action handlers for asynchronous operations
29
- * - Provide clear option descriptions and default values
30
- * - Handle errors gracefully with process.exit()
31
- * - Log progress and errors to console
32
- * - Use Commander's .option() and .argument() for inputs
33
- *
34
- * AVOID:
35
- * - Synchronous blocking operations in action handlers
36
- * - Missing error handling (always use try-catch)
37
- * - Hardcoded values (use options or environment variables)
38
- * - Not exiting with appropriate exit codes on errors
39
- */
40
- /**
41
- * Initialize MCP configuration file
42
- */
43
- const initCommand = new Command("init").description("Initialize MCP configuration file").option("-o, --output <path>", "Output file path", "mcp-config.yaml").option("--json", "Generate JSON config instead of YAML", false).option("-f, --force", "Overwrite existing config file", false).option("--mcp-servers <json>", "JSON string of MCP servers to add to config (optional)").action(async (options) => {
44
- try {
45
- const outputPath = resolve(options.output);
46
- const isYaml = !options.json && (outputPath.endsWith(".yaml") || outputPath.endsWith(".yml"));
47
- let content;
48
- if (isYaml) {
49
- const liquid = new Liquid();
50
- let mcpServersData = null;
51
- if (options.mcpServers) try {
52
- const serversObj = JSON.parse(options.mcpServers);
53
- mcpServersData = Object.entries(serversObj).map(([name, config]) => ({
54
- name,
55
- command: config.command,
56
- args: config.args
57
- }));
58
- } catch (parseError) {
59
- log.error("Failed to parse --mcp-servers JSON:", parseError instanceof Error ? parseError.message : String(parseError));
60
- process.exit(1);
61
- }
62
- content = await liquid.parseAndRender(mcp_config_yaml_default, { mcpServers: mcpServersData });
63
- } else content = mcp_config_default;
64
- try {
65
- await writeFile(outputPath, content, {
66
- encoding: "utf-8",
67
- flag: options.force ? "w" : "wx"
68
- });
69
- } catch (error) {
70
- if (error && typeof error === "object" && "code" in error && error.code === "EEXIST") {
71
- log.error(`Config file already exists: ${outputPath}`);
72
- log.info("Use --force to overwrite");
73
- process.exit(1);
74
- }
75
- throw error;
76
- }
77
- log.info(`MCP configuration file created: ${outputPath}`);
78
- log.info("Next steps:");
79
- log.info("1. Edit the configuration file to add your MCP servers");
80
- log.info(`2. Run: one-mcp mcp-serve --config ${outputPath}`);
81
- } catch (error) {
82
- log.error("Error executing init:", error instanceof Error ? error.message : String(error));
83
- process.exit(1);
84
- }
85
- });
86
-
87
- //#endregion
88
- //#region src/types/index.ts
89
- /**
90
- * Transport mode constants
91
- */
92
- const TRANSPORT_MODE = {
93
- STDIO: "stdio",
94
- HTTP: "http",
95
- SSE: "sse"
96
- };
97
-
98
- //#endregion
99
- //#region src/commands/mcp-serve.ts
100
- /**
101
- * MCP Serve Command
102
- *
103
- * DESIGN PATTERNS:
104
- * - Command pattern with Commander for CLI argument parsing
105
- * - Transport abstraction pattern for flexible deployment (stdio, HTTP, SSE)
106
- * - Factory pattern for creating transport handlers
107
- * - Graceful shutdown pattern with signal handling
108
- *
109
- * CODING STANDARDS:
110
- * - Use async/await for asynchronous operations
111
- * - Implement proper error handling with try-catch blocks
112
- * - Handle process signals for graceful shutdown
113
- * - Provide clear CLI options and help messages
114
- *
115
- * AVOID:
116
- * - Hardcoded configuration values (use CLI options or environment variables)
117
- * - Missing error handling for transport startup
118
- * - Not cleaning up resources on shutdown
119
- */
120
- /**
121
- * Type guard to validate transport type
122
- * @param type - The transport type string to validate
123
- * @returns True if the type is a valid transport type
124
- */
125
- function isValidTransportType(type) {
126
- return type === "stdio" || type === "http" || type === "sse";
127
- }
128
- function isValidProxyMode(mode) {
129
- return mode === "meta" || mode === "flat" || mode === "search";
130
- }
131
- /**
132
- * Start MCP server with given transport handler
133
- * @param handler - The transport handler to start
134
- */
135
- async function startServer(handler) {
136
- await handler.start();
137
- const shutdown = async (signal) => {
138
- console.error(`\nReceived ${signal}, shutting down gracefully...`);
139
- try {
140
- await handler.stop();
141
- process.exit(0);
142
- } catch (error) {
143
- console.error("Error during shutdown:", error);
144
- process.exit(1);
145
- }
146
- };
147
- process.on("SIGINT", () => shutdown("SIGINT"));
148
- process.on("SIGTERM", () => shutdown("SIGTERM"));
149
- }
150
- /**
151
- * MCP Serve command
152
- */
153
- const mcpServeCommand = new Command("mcp-serve").description("Start MCP server with specified transport").option("-t, --type <type>", "Transport type: stdio, http, or sse", "stdio").option("-p, --port <port>", "Port to listen on (http/sse only)", (val) => parseInt(val, 10), 3e3).option("--host <host>", "Host to bind to (http/sse only)", "localhost").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 one-mcp 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) => {
154
- const transportType = options.type.toLowerCase();
155
- if (!isValidTransportType(transportType)) {
156
- console.error(`Unknown transport type: '${transportType}'. Valid options: stdio, http, sse`);
157
- process.exit(1);
158
- }
159
- if (!isValidProxyMode(options.proxyMode)) {
160
- console.error(`Unknown proxy mode: '${options.proxyMode}'. Valid options: meta, flat, search`);
161
- process.exit(1);
162
- }
163
- try {
164
- const serverOptions = {
165
- configFilePath: options.config || findConfigFile() || void 0,
166
- noCache: options.cache === false,
167
- serverId: options.id,
168
- definitionsCachePath: options.definitionsCache,
169
- clearDefinitionsCache: options.clearDefinitionsCache,
170
- proxyMode: options.proxyMode
171
- };
172
- if (transportType === "stdio") await startServer(new StdioTransportHandler(await createServer(serverOptions)));
173
- else if (transportType === "http") await startServer(new HttpTransportHandler(await createServer(serverOptions), {
174
- mode: TRANSPORT_MODE.HTTP,
175
- port: options.port || Number(process.env.MCP_PORT) || 3e3,
176
- host: options.host || process.env.MCP_HOST || "localhost"
177
- }));
178
- else if (transportType === "sse") await startServer(new SseTransportHandler(await createServer(serverOptions), {
179
- mode: TRANSPORT_MODE.SSE,
180
- port: options.port || Number(process.env.MCP_PORT) || 3e3,
181
- host: options.host || process.env.MCP_HOST || "localhost"
182
- }));
183
- } catch (error) {
184
- console.error(`Failed to start MCP server with transport '${transportType}' on ${options.host}:${options.port}:`, error);
185
- process.exit(1);
186
- }
187
- });
188
11
 
189
- //#endregion
190
12
  //#region src/services/PrefetchService/constants.ts
191
13
  /**
192
14
  * PrefetchService Constants
@@ -564,6 +386,475 @@ var PrefetchService = class {
564
386
  }
565
387
  };
566
388
 
389
+ //#endregion
390
+ //#region src/templates/mcp-config.yaml.liquid?raw
391
+ var mcp_config_yaml_default = "# MCP Server Configuration\n# This file configures the MCP servers that one-mcp 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";
392
+
393
+ //#endregion
394
+ //#region src/templates/mcp-config.json?raw
395
+ 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";
396
+
397
+ //#endregion
398
+ //#region src/commands/init.ts
399
+ /**
400
+ * Init Command
401
+ *
402
+ * DESIGN PATTERNS:
403
+ * - Command pattern with Commander for CLI argument parsing
404
+ * - Async/await pattern for asynchronous operations
405
+ * - Error handling pattern with try-catch and proper exit codes
406
+ *
407
+ * CODING STANDARDS:
408
+ * - Use async action handlers for asynchronous operations
409
+ * - Provide clear option descriptions and default values
410
+ * - Handle errors gracefully with process.exit()
411
+ * - Log progress and errors to console
412
+ * - Use Commander's .option() and .argument() for inputs
413
+ *
414
+ * AVOID:
415
+ * - Synchronous blocking operations in action handlers
416
+ * - Missing error handling (always use try-catch)
417
+ * - Hardcoded values (use options or environment variables)
418
+ * - Not exiting with appropriate exit codes on errors
419
+ */
420
+ /**
421
+ * Initialize MCP configuration file
422
+ */
423
+ const initCommand = new Command("init").description("Initialize MCP configuration file").option("-o, --output <path>", "Output file path", "mcp-config.yaml").option("--json", "Generate JSON config instead of YAML", false).option("-f, --force", "Overwrite existing config file", false).option("--mcp-servers <json>", "JSON string of MCP servers to add to config (optional)").action(async (options) => {
424
+ try {
425
+ const outputPath = resolve(options.output);
426
+ const isYaml = !options.json && (outputPath.endsWith(".yaml") || outputPath.endsWith(".yml"));
427
+ let content;
428
+ if (isYaml) {
429
+ const liquid = new Liquid();
430
+ let mcpServersData = null;
431
+ if (options.mcpServers) try {
432
+ const serversObj = JSON.parse(options.mcpServers);
433
+ mcpServersData = Object.entries(serversObj).map(([name, config]) => ({
434
+ name,
435
+ command: config.command,
436
+ args: config.args
437
+ }));
438
+ } catch (parseError) {
439
+ log.error("Failed to parse --mcp-servers JSON:", parseError instanceof Error ? parseError.message : String(parseError));
440
+ process.exit(1);
441
+ }
442
+ content = await liquid.parseAndRender(mcp_config_yaml_default, { mcpServers: mcpServersData });
443
+ } else content = mcp_config_default;
444
+ try {
445
+ await writeFile(outputPath, content, {
446
+ encoding: "utf-8",
447
+ flag: options.force ? "w" : "wx"
448
+ });
449
+ } catch (error) {
450
+ if (error && typeof error === "object" && "code" in error && error.code === "EEXIST") {
451
+ log.error(`Config file already exists: ${outputPath}`);
452
+ log.info("Use --force to overwrite");
453
+ process.exit(1);
454
+ }
455
+ throw error;
456
+ }
457
+ log.info(`MCP configuration file created: ${outputPath}`);
458
+ log.info("Next steps:");
459
+ log.info("1. Edit the configuration file to add your MCP servers");
460
+ log.info(`2. Run: one-mcp mcp-serve --config ${outputPath}`);
461
+ } catch (error) {
462
+ log.error("Error executing init:", error instanceof Error ? error.message : String(error));
463
+ process.exit(1);
464
+ }
465
+ });
466
+
467
+ //#endregion
468
+ //#region src/commands/mcp-serve.ts
469
+ /**
470
+ * MCP Serve Command
471
+ *
472
+ * DESIGN PATTERNS:
473
+ * - Command pattern with Commander for CLI argument parsing
474
+ * - Transport abstraction pattern for flexible deployment (stdio, HTTP, SSE)
475
+ * - Factory pattern for creating transport handlers
476
+ * - Graceful shutdown pattern with signal handling
477
+ *
478
+ * CODING STANDARDS:
479
+ * - Use async/await for asynchronous operations
480
+ * - Implement proper error handling with try-catch blocks
481
+ * - Handle process signals for graceful shutdown
482
+ * - Provide clear CLI options and help messages
483
+ *
484
+ * AVOID:
485
+ * - Hardcoded configuration values (use CLI options or environment variables)
486
+ * - Missing error handling for transport startup
487
+ * - Not cleaning up resources on shutdown
488
+ */
489
+ const CONFIG_FILE_NAMES = [
490
+ "mcp-config.yaml",
491
+ "mcp-config.yml",
492
+ "mcp-config.json"
493
+ ];
494
+ const MCP_ENDPOINT_PATH = "/mcp";
495
+ const DEFAULT_PORT = 3e3;
496
+ const DEFAULT_HOST = "localhost";
497
+ const TRANSPORT_TYPE_STDIO = "stdio";
498
+ const TRANSPORT_TYPE_HTTP = "http";
499
+ const TRANSPORT_TYPE_SSE = "sse";
500
+ const TRANSPORT_TYPE_STDIO_HTTP = "stdio-http";
501
+ const RUNTIME_TRANSPORT = TRANSPORT_TYPE_HTTP;
502
+ const STDIO_HTTP_PROXY_STOP_LABEL = "Failed stopping stdio-http proxy";
503
+ const INTERNAL_HTTP_STOP_LABEL = "Failed stopping internal HTTP transport";
504
+ function toErrorMessage$6(error) {
505
+ return error instanceof Error ? error.message : String(error);
506
+ }
507
+ function isValidTransportType(type) {
508
+ return type === TRANSPORT_TYPE_STDIO || type === TRANSPORT_TYPE_HTTP || type === TRANSPORT_TYPE_SSE || type === TRANSPORT_TYPE_STDIO_HTTP;
509
+ }
510
+ function isValidProxyMode(mode) {
511
+ return mode === "meta" || mode === "flat" || mode === "search";
512
+ }
513
+ function isAddressInUseError(error) {
514
+ if (error instanceof Error && error.message.includes("EADDRINUSE")) return true;
515
+ if (typeof error !== "object" || error === null) return false;
516
+ if ("code" in error && error.code === "EADDRINUSE") return true;
517
+ if ("message" in error && typeof error.message === "string" && error.message.includes("EADDRINUSE")) return true;
518
+ return false;
519
+ }
520
+ async function pathExists(filePath) {
521
+ try {
522
+ await access(filePath, constants.F_OK);
523
+ return true;
524
+ } catch {
525
+ return false;
526
+ }
527
+ }
528
+ async function findConfigFileAsync() {
529
+ try {
530
+ const projectPath = process.env.PROJECT_PATH;
531
+ if (projectPath) for (const fileName of CONFIG_FILE_NAMES) {
532
+ const configPath = resolve(projectPath, fileName);
533
+ if (await pathExists(configPath)) return configPath;
534
+ }
535
+ const cwd = process.cwd();
536
+ for (const fileName of CONFIG_FILE_NAMES) {
537
+ const configPath = join(cwd, fileName);
538
+ if (await pathExists(configPath)) return configPath;
539
+ }
540
+ return null;
541
+ } catch (error) {
542
+ throw new Error(`Failed to discover MCP config file: ${toErrorMessage$6(error)}`);
543
+ }
544
+ }
545
+ async function resolveServerId(options, resolvedConfigPath) {
546
+ if (options.id) return options.id;
547
+ if (resolvedConfigPath) try {
548
+ const config = await new ConfigFetcherService({
549
+ configFilePath: resolvedConfigPath,
550
+ useCache: options.cache !== false
551
+ }).fetchConfiguration(options.cache === false);
552
+ if (config.id) return config.id;
553
+ } catch (error) {
554
+ throw new Error(`Failed to resolve server ID from config '${resolvedConfigPath}': ${toErrorMessage$6(error)}`);
555
+ }
556
+ return generateServerId();
557
+ }
558
+ function validateTransportType(type) {
559
+ if (!isValidTransportType(type)) throw new Error(`Unknown transport type: '${type}'. Valid options: ${TRANSPORT_TYPE_STDIO}, ${TRANSPORT_TYPE_HTTP}, ${TRANSPORT_TYPE_SSE}, ${TRANSPORT_TYPE_STDIO_HTTP}`);
560
+ return type;
561
+ }
562
+ function validateProxyMode(mode) {
563
+ if (!isValidProxyMode(mode)) throw new Error(`Unknown proxy mode: '${mode}'. Valid options: meta, flat, search`);
564
+ }
565
+ function createTransportConfig(options, mode) {
566
+ return {
567
+ mode,
568
+ port: options.port || Number(process.env.MCP_PORT) || DEFAULT_PORT,
569
+ host: options.host || process.env.MCP_HOST || DEFAULT_HOST
570
+ };
571
+ }
572
+ function createServerOptions(options, resolvedConfigPath, serverId) {
573
+ return {
574
+ configFilePath: resolvedConfigPath,
575
+ noCache: options.cache === false,
576
+ serverId,
577
+ definitionsCachePath: options.definitionsCache,
578
+ clearDefinitionsCache: options.clearDefinitionsCache,
579
+ proxyMode: options.proxyMode
580
+ };
581
+ }
582
+ function formatStartError(type, host, port, error) {
583
+ const startErrorMessage = toErrorMessage$6(error);
584
+ if (type === TRANSPORT_TYPE_STDIO) return `Failed to start MCP server with transport '${type}': ${startErrorMessage}`;
585
+ return `Failed to start MCP server with transport '${type}' on ${host}:${port}: ${startErrorMessage}`;
586
+ }
587
+ function createRuntimeRecord(serverId, config, shutdownToken, configPath) {
588
+ return {
589
+ serverId,
590
+ host: config.host ?? DEFAULT_HOST,
591
+ port: config.port ?? DEFAULT_PORT,
592
+ transport: RUNTIME_TRANSPORT,
593
+ shutdownToken,
594
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
595
+ pid: process.pid,
596
+ configPath
597
+ };
598
+ }
599
+ function createHttpAdminOptions(serverId, shutdownToken, onShutdownRequested) {
600
+ return {
601
+ serverId,
602
+ shutdownToken,
603
+ onShutdownRequested
604
+ };
605
+ }
606
+ async function removeRuntimeRecord(runtimeStateService, serverId) {
607
+ try {
608
+ await runtimeStateService.remove(serverId);
609
+ } catch (error) {
610
+ throw new Error(`Failed to remove runtime state for '${serverId}': ${toErrorMessage$6(error)}`);
611
+ }
612
+ }
613
+ async function writeRuntimeRecord(runtimeStateService, record) {
614
+ try {
615
+ await runtimeStateService.write(record);
616
+ } catch (error) {
617
+ throw new Error(`Failed to persist runtime state for '${record.serverId}': ${toErrorMessage$6(error)}`);
618
+ }
619
+ }
620
+ async function stopOwnedHttpTransport(handler, runtimeStateService, serverId) {
621
+ try {
622
+ try {
623
+ await handler.stop();
624
+ } catch (error) {
625
+ throw new Error(`Failed to stop owned HTTP transport '${serverId}': ${toErrorMessage$6(error)}`);
626
+ }
627
+ } finally {
628
+ await removeRuntimeRecord(runtimeStateService, serverId);
629
+ }
630
+ }
631
+ async function cleanupFailedRuntimeStartup(handler, runtimeStateService, serverId) {
632
+ try {
633
+ try {
634
+ await handler.stop();
635
+ } catch (error) {
636
+ throw new Error(`Failed to stop HTTP transport during cleanup for '${serverId}': ${toErrorMessage$6(error)}`);
637
+ }
638
+ } finally {
639
+ await removeRuntimeRecord(runtimeStateService, serverId);
640
+ }
641
+ }
642
+ async function stopTransportWithContext(label, stopOperation) {
643
+ try {
644
+ await stopOperation();
645
+ } catch (error) {
646
+ throw new Error(`${label}: ${toErrorMessage$6(error)}`);
647
+ }
648
+ }
649
+ async function removeRuntimeRecordDuringStop(runtimeStateService, serverId) {
650
+ try {
651
+ await removeRuntimeRecord(runtimeStateService, serverId);
652
+ } catch (error) {
653
+ throw new Error(`Failed to remove runtime state during HTTP stop callback for '${serverId}': ${toErrorMessage$6(error)}`);
654
+ }
655
+ }
656
+ async function createStdioHttpInternalTransport(serverOptions, config, adminOptions) {
657
+ try {
658
+ return new HttpTransportHandler(await createServer(serverOptions), config, adminOptions);
659
+ } catch (error) {
660
+ throw new Error(`Failed to create internal HTTP transport for stdio-http proxy: ${toErrorMessage$6(error)}`);
661
+ }
662
+ }
663
+ /**
664
+ * Start MCP server with given transport handler
665
+ * @param handler - The transport handler to start
666
+ * @param onStopped - Optional cleanup callback run after signal-based shutdown
667
+ */
668
+ async function startServer(handler, onStopped) {
669
+ try {
670
+ await handler.start();
671
+ } catch (error) {
672
+ throw new Error(`Failed to start transport handler: ${toErrorMessage$6(error)}`);
673
+ }
674
+ const shutdown = async (signal) => {
675
+ console.error(`\nReceived ${signal}, shutting down gracefully...`);
676
+ try {
677
+ await handler.stop();
678
+ if (onStopped) await onStopped();
679
+ process.exit(0);
680
+ } catch (error) {
681
+ console.error(`Failed to gracefully stop transport during ${signal}: ${toErrorMessage$6(error)}`);
682
+ process.exit(1);
683
+ }
684
+ };
685
+ process.on("SIGINT", async () => await shutdown("SIGINT"));
686
+ process.on("SIGTERM", async () => await shutdown("SIGTERM"));
687
+ }
688
+ async function createAndStartHttpRuntime(serverOptions, config, resolvedConfigPath) {
689
+ const runtimeStateService = new RuntimeStateService();
690
+ const shutdownToken = randomUUID();
691
+ const runtimeRecord = createRuntimeRecord(serverOptions.serverId ?? generateServerId(), config, shutdownToken, resolvedConfigPath);
692
+ let handler;
693
+ let isStopping = false;
694
+ const stopHandler = async () => {
695
+ if (isStopping) return;
696
+ isStopping = true;
697
+ try {
698
+ await stopOwnedHttpTransport(handler, runtimeStateService, runtimeRecord.serverId);
699
+ process.exit(0);
700
+ } catch (error) {
701
+ throw new Error(`Failed to stop HTTP runtime '${runtimeRecord.serverId}' from admin shutdown: ${toErrorMessage$6(error)}`);
702
+ }
703
+ };
704
+ try {
705
+ handler = new HttpTransportHandler(await createServer(serverOptions), config, createHttpAdminOptions(runtimeRecord.serverId, shutdownToken, stopHandler));
706
+ } catch (error) {
707
+ throw new Error(`Failed to create HTTP runtime server: ${toErrorMessage$6(error)}`);
708
+ }
709
+ try {
710
+ await startServer(handler, async () => {
711
+ await removeRuntimeRecordDuringStop(runtimeStateService, runtimeRecord.serverId);
712
+ });
713
+ await writeRuntimeRecord(runtimeStateService, runtimeRecord);
714
+ } catch (error) {
715
+ await cleanupFailedRuntimeStartup(handler, runtimeStateService, runtimeRecord.serverId);
716
+ throw new Error(`Failed to start HTTP runtime '${runtimeRecord.serverId}': ${toErrorMessage$6(error)}`);
717
+ }
718
+ console.error(`Runtime state: http://${runtimeRecord.host}:${runtimeRecord.port} (${runtimeRecord.serverId})`);
719
+ }
720
+ async function stopInternalHttpTransport(stdioHttpHandler, httpHandler, ownsInternalHttpTransport) {
721
+ try {
722
+ const stopOperations = [stopTransportWithContext(STDIO_HTTP_PROXY_STOP_LABEL, async () => {
723
+ await stdioHttpHandler.stop();
724
+ })];
725
+ if (ownsInternalHttpTransport && httpHandler) stopOperations.push(stopTransportWithContext(INTERNAL_HTTP_STOP_LABEL, async () => {
726
+ await httpHandler.stop();
727
+ }));
728
+ await Promise.all(stopOperations);
729
+ } catch (error) {
730
+ throw new Error(`Failed to stop stdio-http transport: ${toErrorMessage$6(error)}`);
731
+ }
732
+ }
733
+ async function startStdioTransport(serverOptions) {
734
+ try {
735
+ await startServer(new StdioTransportHandler(await createServer(serverOptions)));
736
+ } catch (error) {
737
+ throw new Error(`Failed to start stdio transport: ${toErrorMessage$6(error)}`);
738
+ }
739
+ }
740
+ async function startSseTransport(serverOptions, config) {
741
+ try {
742
+ await startServer(new SseTransportHandler(await createServer(serverOptions), config));
743
+ } catch (error) {
744
+ throw new Error(`Failed to start SSE transport: ${toErrorMessage$6(error)}`);
745
+ }
746
+ }
747
+ async function startStdioHttpTransport(serverOptions, config, resolvedConfigPath) {
748
+ try {
749
+ const stdioHttpHandler = new StdioHttpTransportHandler({ endpoint: new URL(`http://${config.host}:${config.port}${MCP_ENDPOINT_PATH}`) });
750
+ const runtimeStateService = new RuntimeStateService();
751
+ const serverId = serverOptions.serverId ?? generateServerId();
752
+ const shutdownToken = randomUUID();
753
+ let httpHandler = null;
754
+ let ownsInternalHttpTransport = false;
755
+ let isStopping = false;
756
+ const stopOwnedRuntime = async () => {
757
+ if (isStopping) return;
758
+ isStopping = true;
759
+ try {
760
+ await Promise.all([stopInternalHttpTransport(stdioHttpHandler, httpHandler, ownsInternalHttpTransport), removeRuntimeRecord(runtimeStateService, serverId)]);
761
+ ownsInternalHttpTransport = false;
762
+ process.exit(0);
763
+ } catch (error) {
764
+ console.error(`Unexpected error during admin shutdown: ${toErrorMessage$6(error)}`);
765
+ process.exit(1);
766
+ }
767
+ };
768
+ const adminOptions = createHttpAdminOptions(serverId, shutdownToken, stopOwnedRuntime);
769
+ await startServer({
770
+ async start() {
771
+ let initialProxyConnectError;
772
+ try {
773
+ await stdioHttpHandler.start();
774
+ return;
775
+ } catch (error) {
776
+ initialProxyConnectError = error;
777
+ }
778
+ try {
779
+ httpHandler = await createStdioHttpInternalTransport(serverOptions, config, adminOptions);
780
+ await httpHandler.start();
781
+ ownsInternalHttpTransport = true;
782
+ } catch (error) {
783
+ if (!isAddressInUseError(error)) throw new Error(`Failed to start internal HTTP transport for stdio-http proxy: ${toErrorMessage$6(error)}`);
784
+ }
785
+ try {
786
+ await stdioHttpHandler.start();
787
+ } catch (error) {
788
+ let rollbackStopErrorMessage = "";
789
+ if (ownsInternalHttpTransport && httpHandler) {
790
+ try {
791
+ await httpHandler.stop();
792
+ } catch (stopError) {
793
+ rollbackStopErrorMessage = toErrorMessage$6(stopError);
794
+ }
795
+ ownsInternalHttpTransport = false;
796
+ }
797
+ const retryErrorMessage = toErrorMessage$6(error);
798
+ const initialErrorMessage = toErrorMessage$6(initialProxyConnectError);
799
+ const rollbackMessage = rollbackStopErrorMessage ? `; rollback stop failed: ${rollbackStopErrorMessage}` : "";
800
+ throw new Error(`Failed to start stdio-http proxy bridge: initial connect failed (${initialErrorMessage}); retry failed (${retryErrorMessage})${rollbackMessage}`);
801
+ }
802
+ if (ownsInternalHttpTransport) try {
803
+ await writeRuntimeRecord(runtimeStateService, createRuntimeRecord(serverId, config, shutdownToken, resolvedConfigPath));
804
+ } catch (error) {
805
+ throw new Error(`Failed to persist runtime state for stdio-http server '${serverId}': ${toErrorMessage$6(error)}`);
806
+ }
807
+ },
808
+ async stop() {
809
+ try {
810
+ await Promise.all([stopInternalHttpTransport(stdioHttpHandler, httpHandler, ownsInternalHttpTransport), removeRuntimeRecord(runtimeStateService, serverId)]);
811
+ ownsInternalHttpTransport = false;
812
+ } catch (error) {
813
+ ownsInternalHttpTransport = false;
814
+ throw new Error(`Failed during stdio-http shutdown for '${serverId}': ${toErrorMessage$6(error)}`);
815
+ }
816
+ }
817
+ });
818
+ } catch (error) {
819
+ throw new Error(`Failed to start stdio-http transport: ${toErrorMessage$6(error)}`);
820
+ }
821
+ }
822
+ async function startTransport(transportType, options, resolvedConfigPath, serverOptions) {
823
+ try {
824
+ if (transportType === TRANSPORT_TYPE_STDIO) {
825
+ await startStdioTransport(serverOptions);
826
+ return;
827
+ }
828
+ if (transportType === TRANSPORT_TYPE_HTTP) {
829
+ await createAndStartHttpRuntime(serverOptions, createTransportConfig(options, TRANSPORT_MODE.HTTP), resolvedConfigPath);
830
+ return;
831
+ }
832
+ if (transportType === TRANSPORT_TYPE_SSE) {
833
+ await startSseTransport(serverOptions, createTransportConfig(options, TRANSPORT_MODE.SSE));
834
+ return;
835
+ }
836
+ await startStdioHttpTransport(serverOptions, createTransportConfig(options, TRANSPORT_MODE.HTTP), resolvedConfigPath);
837
+ } catch (error) {
838
+ throw new Error(`Failed to start transport '${transportType}': ${toErrorMessage$6(error)}`);
839
+ }
840
+ }
841
+ /**
842
+ * MCP Serve command
843
+ */
844
+ 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 one-mcp 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) => {
845
+ try {
846
+ const transportType = validateTransportType(options.type.toLowerCase());
847
+ validateProxyMode(options.proxyMode);
848
+ const resolvedConfigPath = options.config || await findConfigFileAsync() || void 0;
849
+ await startTransport(transportType, options, resolvedConfigPath, createServerOptions(options, resolvedConfigPath, await resolveServerId(options, resolvedConfigPath)));
850
+ } catch (error) {
851
+ const rawTransportType = options.type.toLowerCase();
852
+ const transportType = isValidTransportType(rawTransportType) ? rawTransportType : TRANSPORT_TYPE_STDIO;
853
+ console.error(formatStartError(transportType, options.host, options.port, error));
854
+ process.exit(1);
855
+ }
856
+ });
857
+
567
858
  //#endregion
568
859
  //#region src/commands/list-tools.ts
569
860
  /**
@@ -587,7 +878,7 @@ var PrefetchService = class {
587
878
  * - Hardcoded values (use options or environment variables)
588
879
  * - Not exiting with appropriate exit codes on errors
589
880
  */
590
- function toErrorMessage$4(error) {
881
+ function toErrorMessage$5(error) {
591
882
  return error instanceof Error ? error.message : String(error);
592
883
  }
593
884
  function printSearchResults(result) {
@@ -618,7 +909,7 @@ const searchToolsCommand = new Command("search-tools").description("Search proxi
618
909
  await clientManager.connectToServer(serverName, serverConfig);
619
910
  if (!options.json) console.error(`✓ Connected to ${serverName}`);
620
911
  } catch (error) {
621
- if (!options.json) console.error(`✗ Failed to connect to ${serverName}: ${toErrorMessage$4(error)}`);
912
+ if (!options.json) console.error(`✗ Failed to connect to ${serverName}: ${toErrorMessage$5(error)}`);
622
913
  }
623
914
  });
624
915
  await Promise.all(connectionPromises);
@@ -642,7 +933,7 @@ const searchToolsCommand = new Command("search-tools").description("Search proxi
642
933
  }
643
934
  await clientManager.disconnectAll();
644
935
  } catch (error) {
645
- console.error(`Error executing search-tools: ${toErrorMessage$4(error)}`);
936
+ console.error(`Error executing search-tools: ${toErrorMessage$5(error)}`);
646
937
  process.exit(1);
647
938
  }
648
939
  });
@@ -1001,7 +1292,7 @@ const useToolCommand = new Command("use-tool").description("Execute an MCP tool
1001
1292
  * - Hardcoded values (use options or environment variables)
1002
1293
  * - Not exiting with appropriate exit codes on errors
1003
1294
  */
1004
- function toErrorMessage$3(error) {
1295
+ function toErrorMessage$4(error) {
1005
1296
  return error instanceof Error ? error.message : String(error);
1006
1297
  }
1007
1298
  /**
@@ -1018,7 +1309,7 @@ const listResourcesCommand = new Command("list-resources").description("List all
1018
1309
  await clientManager.connectToServer(serverName, serverConfig);
1019
1310
  if (!options.json) console.error(`✓ Connected to ${serverName}`);
1020
1311
  } catch (error) {
1021
- if (!options.json) console.error(`✗ Failed to connect to ${serverName}: ${toErrorMessage$3(error)}`);
1312
+ if (!options.json) console.error(`✗ Failed to connect to ${serverName}: ${toErrorMessage$4(error)}`);
1022
1313
  }
1023
1314
  }));
1024
1315
  const clients = options.server ? [clientManager.getClient(options.server)].filter((c) => c !== void 0) : clientManager.getAllClients();
@@ -1041,7 +1332,7 @@ const listResourcesCommand = new Command("list-resources").description("List all
1041
1332
  }
1042
1333
  }));
1043
1334
  for (const { serverName, resources, error } of resourceResults) {
1044
- if (error && !options.json) console.error(`Failed to list resources from ${serverName}: ${toErrorMessage$3(error)}`);
1335
+ if (error && !options.json) console.error(`Failed to list resources from ${serverName}: ${toErrorMessage$4(error)}`);
1045
1336
  resourcesByServer[serverName] = resources;
1046
1337
  }
1047
1338
  if (options.json) console.log(JSON.stringify(resourcesByServer, null, 2));
@@ -1055,7 +1346,7 @@ const listResourcesCommand = new Command("list-resources").description("List all
1055
1346
  }
1056
1347
  await clientManager.disconnectAll();
1057
1348
  } catch (error) {
1058
- console.error(`Error executing list-resources: ${toErrorMessage$3(error)}`);
1349
+ console.error(`Error executing list-resources: ${toErrorMessage$4(error)}`);
1059
1350
  process.exit(1);
1060
1351
  }
1061
1352
  });
@@ -1083,7 +1374,7 @@ const listResourcesCommand = new Command("list-resources").description("List all
1083
1374
  * - Hardcoded values (use options or environment variables)
1084
1375
  * - Not exiting with appropriate exit codes on errors
1085
1376
  */
1086
- function toErrorMessage$2(error) {
1377
+ function toErrorMessage$3(error) {
1087
1378
  return error instanceof Error ? error.message : String(error);
1088
1379
  }
1089
1380
  /**
@@ -1100,7 +1391,7 @@ const readResourceCommand = new Command("read-resource").description("Read a res
1100
1391
  await clientManager.connectToServer(serverName, serverConfig);
1101
1392
  if (!options.json) console.error(`✓ Connected to ${serverName}`);
1102
1393
  } catch (error) {
1103
- console.error(`✗ Failed to connect to ${serverName}: ${toErrorMessage$2(error)}`);
1394
+ console.error(`✗ Failed to connect to ${serverName}: ${toErrorMessage$3(error)}`);
1104
1395
  }
1105
1396
  }));
1106
1397
  const clients = clientManager.getAllClients();
@@ -1135,7 +1426,7 @@ const readResourceCommand = new Command("read-resource").description("Read a res
1135
1426
  const matchingServers = [];
1136
1427
  for (const { serverName, hasResource, error } of searchResults) {
1137
1428
  if (error) {
1138
- console.error(`Failed to list resources from ${serverName}: ${toErrorMessage$2(error)}`);
1429
+ console.error(`Failed to list resources from ${serverName}: ${toErrorMessage$3(error)}`);
1139
1430
  continue;
1140
1431
  }
1141
1432
  if (hasResource) matchingServers.push(serverName);
@@ -1152,14 +1443,14 @@ const readResourceCommand = new Command("read-resource").description("Read a res
1152
1443
  else console.log(JSON.stringify(content, null, 2));
1153
1444
  await clientManager.disconnectAll();
1154
1445
  } catch (error) {
1155
- console.error(`Error executing read-resource: ${toErrorMessage$2(error)}`);
1446
+ console.error(`Error executing read-resource: ${toErrorMessage$3(error)}`);
1156
1447
  process.exit(1);
1157
1448
  }
1158
1449
  });
1159
1450
 
1160
1451
  //#endregion
1161
1452
  //#region src/commands/list-prompts.ts
1162
- function toErrorMessage$1(error) {
1453
+ function toErrorMessage$2(error) {
1163
1454
  return error instanceof Error ? error.message : String(error);
1164
1455
  }
1165
1456
  const listPromptsCommand = new Command("list-prompts").description("List all available prompts from connected MCP servers").option("-c, --config <path>", "Path to MCP server configuration file").option("-s, --server <name>", "Filter by server name").option("-j, --json", "Output as JSON", false).action(async (options) => {
@@ -1173,7 +1464,7 @@ const listPromptsCommand = new Command("list-prompts").description("List all ava
1173
1464
  await clientManager.connectToServer(serverName, serverConfig);
1174
1465
  if (!options.json) console.error(`✓ Connected to ${serverName}`);
1175
1466
  } catch (error) {
1176
- if (!options.json) console.error(`✗ Failed to connect to ${serverName}: ${toErrorMessage$1(error)}`);
1467
+ if (!options.json) console.error(`✗ Failed to connect to ${serverName}: ${toErrorMessage$2(error)}`);
1177
1468
  }
1178
1469
  }));
1179
1470
  const clients = options.server ? [clientManager.getClient(options.server)].filter((client) => client !== void 0) : clientManager.getAllClients();
@@ -1184,7 +1475,7 @@ const listPromptsCommand = new Command("list-prompts").description("List all ava
1184
1475
  promptsByServer[client.serverName] = await client.listPrompts();
1185
1476
  } catch (error) {
1186
1477
  promptsByServer[client.serverName] = [];
1187
- if (!options.json) console.error(`Failed to list prompts from ${client.serverName}: ${toErrorMessage$1(error)}`);
1478
+ if (!options.json) console.error(`Failed to list prompts from ${client.serverName}: ${toErrorMessage$2(error)}`);
1188
1479
  }
1189
1480
  }));
1190
1481
  if (options.json) console.log(JSON.stringify(promptsByServer, null, 2));
@@ -1204,14 +1495,14 @@ const listPromptsCommand = new Command("list-prompts").description("List all ava
1204
1495
  }
1205
1496
  await clientManager.disconnectAll();
1206
1497
  } catch (error) {
1207
- console.error(`Error executing list-prompts: ${toErrorMessage$1(error)}`);
1498
+ console.error(`Error executing list-prompts: ${toErrorMessage$2(error)}`);
1208
1499
  process.exit(1);
1209
1500
  }
1210
1501
  });
1211
1502
 
1212
1503
  //#endregion
1213
1504
  //#region src/commands/get-prompt.ts
1214
- function toErrorMessage(error) {
1505
+ function toErrorMessage$1(error) {
1215
1506
  return error instanceof Error ? error.message : String(error);
1216
1507
  }
1217
1508
  const getPromptCommand = new Command("get-prompt").description("Get a prompt by name from a connected MCP server").argument("<promptName>", "Prompt name to fetch").option("-c, --config <path>", "Path to MCP server configuration file").option("-s, --server <name>", "Server name (required if prompt exists on multiple servers)").option("-a, --args <json>", "Prompt arguments as JSON string", "{}").option("-j, --json", "Output as JSON", false).action(async (promptName, options) => {
@@ -1231,7 +1522,7 @@ const getPromptCommand = new Command("get-prompt").description("Get a prompt by
1231
1522
  await clientManager.connectToServer(serverName, serverConfig);
1232
1523
  if (!options.json) console.error(`✓ Connected to ${serverName}`);
1233
1524
  } catch (error) {
1234
- if (!options.json) console.error(`✗ Failed to connect to ${serverName}: ${toErrorMessage(error)}`);
1525
+ if (!options.json) console.error(`✗ Failed to connect to ${serverName}: ${toErrorMessage$1(error)}`);
1235
1526
  }
1236
1527
  }));
1237
1528
  const clients = clientManager.getAllClients();
@@ -1254,7 +1545,7 @@ const getPromptCommand = new Command("get-prompt").description("Get a prompt by
1254
1545
  try {
1255
1546
  if ((await client$1.listPrompts()).some((prompt$1) => prompt$1.name === promptName)) matchingServers.push(client$1.serverName);
1256
1547
  } catch (error) {
1257
- if (!options.json) console.error(`Failed to list prompts from ${client$1.serverName}: ${toErrorMessage(error)}`);
1548
+ if (!options.json) console.error(`Failed to list prompts from ${client$1.serverName}: ${toErrorMessage$1(error)}`);
1258
1549
  }
1259
1550
  }));
1260
1551
  if (matchingServers.length === 0) throw new Error(`Prompt "${promptName}" not found on any connected server`);
@@ -1270,7 +1561,7 @@ const getPromptCommand = new Command("get-prompt").description("Get a prompt by
1270
1561
  }
1271
1562
  await clientManager.disconnectAll();
1272
1563
  } catch (error) {
1273
- console.error(`Error executing get-prompt: ${toErrorMessage(error)}`);
1564
+ console.error(`Error executing get-prompt: ${toErrorMessage$1(error)}`);
1274
1565
  process.exit(1);
1275
1566
  }
1276
1567
  });
@@ -1400,6 +1691,52 @@ const prefetchCommand = new Command("prefetch").description("Pre-download packag
1400
1691
  }
1401
1692
  });
1402
1693
 
1694
+ //#endregion
1695
+ //#region src/commands/stop.ts
1696
+ /**
1697
+ * Stop Command
1698
+ *
1699
+ * Stops a running HTTP one-mcp server using the authenticated admin endpoint
1700
+ * and the persisted runtime registry.
1701
+ */
1702
+ function toErrorMessage(error) {
1703
+ return error instanceof Error ? error.message : String(error);
1704
+ }
1705
+ function printStopResult(result) {
1706
+ console.log(`Stopped one-mcp server '${result.serverId}'.`);
1707
+ console.log(`Endpoint: http://${result.host}:${result.port}`);
1708
+ console.log(`Result: ${result.message}`);
1709
+ }
1710
+ /**
1711
+ * Stop a running HTTP one-mcp server.
1712
+ */
1713
+ const stopCommand = new Command("stop").description("Stop a running HTTP one-mcp server").option("--id <id>", "Target server ID from the runtime registry").option("--host <host>", "Target runtime host").option("--port <port>", "Target runtime port", (value) => Number.parseInt(value, 10)).option("-c, --config <path>", "Reserved for future config-based targeting support").option("--token <token>", "Override the persisted shutdown token").option("--force", "Skip server ID verification against the /health response", false).option("-j, --json", "Output as JSON", false).option("--timeout <ms>", "Maximum time to wait for shutdown completion", (value) => Number.parseInt(value, 10), 5e3).action(async (options) => {
1714
+ try {
1715
+ if (options.config) console.error("Warning: --config is not used yet; runtime resolution uses the persisted registry.");
1716
+ const result = await new StopServerService().stop({
1717
+ serverId: options.id,
1718
+ host: options.host,
1719
+ port: options.port,
1720
+ token: options.token,
1721
+ force: options.force,
1722
+ timeoutMs: options.timeout
1723
+ });
1724
+ if (options.json) {
1725
+ console.log(JSON.stringify(result, null, 2));
1726
+ return;
1727
+ }
1728
+ printStopResult(result);
1729
+ } catch (error) {
1730
+ const errorMessage = `Error executing stop: ${toErrorMessage(error)}`;
1731
+ if (options.json) console.log(JSON.stringify({
1732
+ ok: false,
1733
+ error: errorMessage
1734
+ }, null, 2));
1735
+ else console.error(errorMessage);
1736
+ process.exit(1);
1737
+ }
1738
+ });
1739
+
1403
1740
  //#endregion
1404
1741
  //#region src/cli.ts
1405
1742
  /**
@@ -1437,6 +1774,7 @@ async function main() {
1437
1774
  program.addCommand(listPromptsCommand);
1438
1775
  program.addCommand(getPromptCommand);
1439
1776
  program.addCommand(prefetchCommand);
1777
+ program.addCommand(stopCommand);
1440
1778
  await program.parseAsync(process.argv);
1441
1779
  } catch (error) {
1442
1780
  console.error(`CLI execution failed: ${error instanceof Error ? error.message : error}`);