@agimon-ai/mcp-proxy 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.cjs ADDED
@@ -0,0 +1,1513 @@
1
+ #!/usr/bin/env node
2
+ const require_src = require('./src-6KF7hZTe.cjs');
3
+ let node_crypto = require("node:crypto");
4
+ let node_fs_promises = require("node:fs/promises");
5
+ let node_path = require("node:path");
6
+ let node_fs = require("node:fs");
7
+ let liquidjs = require("liquidjs");
8
+ let commander = require("commander");
9
+ let __agimon_ai_foundation_process_registry = require("@agimon-ai/foundation-process-registry");
10
+ let __agimon_ai_foundation_port_registry = require("@agimon-ai/foundation-port-registry");
11
+
12
+ //#region src/utils/output.ts
13
+ function writeLine(message = "") {
14
+ console.log(message);
15
+ }
16
+ function writeError(message, detail) {
17
+ if (detail) console.error(`${message} ${detail}`);
18
+ else console.error(message);
19
+ }
20
+ const log = {
21
+ info: (message) => writeLine(message),
22
+ error: (message, detail) => writeError(message, detail)
23
+ };
24
+ const print = {
25
+ info: (message) => writeLine(message),
26
+ warning: (message) => writeLine(`Warning: ${message}`),
27
+ error: (message) => writeError(message),
28
+ success: (message) => writeLine(message),
29
+ newline: () => writeLine(),
30
+ header: (message) => writeLine(message),
31
+ item: (message) => writeLine(`- ${message}`),
32
+ indent: (message) => writeLine(` ${message}`)
33
+ };
34
+
35
+ //#endregion
36
+ //#region src/templates/mcp-config.yaml.liquid?raw
37
+ 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";
38
+
39
+ //#endregion
40
+ //#region src/templates/mcp-config.json?raw
41
+ 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";
42
+
43
+ //#endregion
44
+ //#region src/commands/init.ts
45
+ /**
46
+ * Init Command
47
+ *
48
+ * DESIGN PATTERNS:
49
+ * - Command pattern with Commander for CLI argument parsing
50
+ * - Async/await pattern for asynchronous operations
51
+ * - Error handling pattern with try-catch and proper exit codes
52
+ *
53
+ * CODING STANDARDS:
54
+ * - Use async action handlers for asynchronous operations
55
+ * - Provide clear option descriptions and default values
56
+ * - Handle errors gracefully with process.exit()
57
+ * - Log progress and errors to console
58
+ * - Use Commander's .option() and .argument() for inputs
59
+ *
60
+ * AVOID:
61
+ * - Synchronous blocking operations in action handlers
62
+ * - Missing error handling (always use try-catch)
63
+ * - Hardcoded values (use options or environment variables)
64
+ * - Not exiting with appropriate exit codes on errors
65
+ */
66
+ /**
67
+ * Initialize MCP configuration file
68
+ */
69
+ const initCommand = new commander.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) => {
70
+ try {
71
+ const outputPath = (0, node_path.resolve)(options.output);
72
+ const isYaml = !options.json && (outputPath.endsWith(".yaml") || outputPath.endsWith(".yml"));
73
+ let content;
74
+ if (isYaml) {
75
+ const liquid = new liquidjs.Liquid();
76
+ let mcpServersData = null;
77
+ if (options.mcpServers) try {
78
+ const serversObj = JSON.parse(options.mcpServers);
79
+ mcpServersData = Object.entries(serversObj).map(([name, config]) => ({
80
+ name,
81
+ command: config.command,
82
+ args: config.args
83
+ }));
84
+ } catch (parseError) {
85
+ log.error("Failed to parse --mcp-servers JSON:", parseError instanceof Error ? parseError.message : String(parseError));
86
+ process.exit(1);
87
+ }
88
+ content = await liquid.parseAndRender(mcp_config_yaml_default, { mcpServers: mcpServersData });
89
+ } else content = mcp_config_default;
90
+ try {
91
+ await (0, node_fs_promises.writeFile)(outputPath, content, {
92
+ encoding: "utf-8",
93
+ flag: options.force ? "w" : "wx"
94
+ });
95
+ } catch (error) {
96
+ if (error && typeof error === "object" && "code" in error && error.code === "EEXIST") {
97
+ log.error(`Config file already exists: ${outputPath}`);
98
+ log.info("Use --force to overwrite");
99
+ process.exit(1);
100
+ }
101
+ throw error;
102
+ }
103
+ log.info(`MCP configuration file created: ${outputPath}`);
104
+ log.info("Next steps:");
105
+ log.info("1. Edit the configuration file to add your MCP servers");
106
+ log.info(`2. Run: mcp-proxy mcp-serve --config ${outputPath}`);
107
+ } catch (error) {
108
+ log.error("Error executing init:", error instanceof Error ? error.message : String(error));
109
+ process.exit(1);
110
+ }
111
+ });
112
+
113
+ //#endregion
114
+ //#region src/commands/mcp-serve.ts
115
+ /**
116
+ * MCP Serve Command
117
+ *
118
+ * DESIGN PATTERNS:
119
+ * - Command pattern with Commander for CLI argument parsing
120
+ * - Transport abstraction pattern for flexible deployment (stdio, HTTP, SSE)
121
+ * - Factory pattern for creating transport handlers
122
+ * - Graceful shutdown pattern with signal handling
123
+ *
124
+ * CODING STANDARDS:
125
+ * - Use async/await for asynchronous operations
126
+ * - Implement proper error handling with try-catch blocks
127
+ * - Handle process signals for graceful shutdown
128
+ * - Provide clear CLI options and help messages
129
+ *
130
+ * AVOID:
131
+ * - Hardcoded configuration values (use CLI options or environment variables)
132
+ * - Missing error handling for transport startup
133
+ * - Not cleaning up resources on shutdown
134
+ */
135
+ const CONFIG_FILE_NAMES = [
136
+ "mcp-config.yaml",
137
+ "mcp-config.yml",
138
+ "mcp-config.json"
139
+ ];
140
+ const MCP_ENDPOINT_PATH = "/mcp";
141
+ const DEFAULT_PORT = 3e3;
142
+ const DEFAULT_HOST = "localhost";
143
+ const TRANSPORT_TYPE_STDIO = "stdio";
144
+ const TRANSPORT_TYPE_HTTP = "http";
145
+ const TRANSPORT_TYPE_SSE = "sse";
146
+ const TRANSPORT_TYPE_STDIO_HTTP = "stdio-http";
147
+ const RUNTIME_TRANSPORT = TRANSPORT_TYPE_HTTP;
148
+ const STDIO_HTTP_PROXY_STOP_LABEL = "Failed stopping stdio-http proxy";
149
+ const INTERNAL_HTTP_STOP_LABEL = "Failed stopping internal HTTP transport";
150
+ const PORT_REGISTRY_SERVICE_HTTP = "mcp-proxy-http";
151
+ const PORT_REGISTRY_SERVICE_STDIO_HTTP = "mcp-proxy-stdio-http";
152
+ const PORT_REGISTRY_SERVICE_TYPE = "service";
153
+ const PORT_REGISTRY_REPOSITORY_PATH = process.cwd();
154
+ const PROCESS_REGISTRY_SERVICE_HTTP = "mcp-proxy-http";
155
+ const PROCESS_REGISTRY_SERVICE_STDIO_HTTP = "mcp-proxy-stdio-http";
156
+ const PROCESS_REGISTRY_SERVICE_TYPE = "service";
157
+ const PROCESS_REGISTRY_REPOSITORY_PATH = process.cwd();
158
+ function toErrorMessage$9(error) {
159
+ return error instanceof Error ? error.message : String(error);
160
+ }
161
+ function isValidTransportType(type) {
162
+ return type === TRANSPORT_TYPE_STDIO || type === TRANSPORT_TYPE_HTTP || type === TRANSPORT_TYPE_SSE || type === TRANSPORT_TYPE_STDIO_HTTP;
163
+ }
164
+ function isValidProxyMode(mode) {
165
+ return mode === "meta" || mode === "flat" || mode === "search";
166
+ }
167
+ function isAddressInUseError(error) {
168
+ if (error instanceof Error && error.message.includes("EADDRINUSE")) return true;
169
+ if (typeof error !== "object" || error === null) return false;
170
+ if ("code" in error && error.code === "EADDRINUSE") return true;
171
+ if ("message" in error && typeof error.message === "string" && error.message.includes("EADDRINUSE")) return true;
172
+ return false;
173
+ }
174
+ async function pathExists(filePath) {
175
+ try {
176
+ await (0, node_fs_promises.access)(filePath, node_fs.constants.F_OK);
177
+ return true;
178
+ } catch {
179
+ return false;
180
+ }
181
+ }
182
+ async function findConfigFileAsync() {
183
+ try {
184
+ const projectPath = process.env.PROJECT_PATH;
185
+ if (projectPath) for (const fileName of CONFIG_FILE_NAMES) {
186
+ const configPath = (0, node_path.resolve)(projectPath, fileName);
187
+ if (await pathExists(configPath)) return configPath;
188
+ }
189
+ const cwd = process.cwd();
190
+ for (const fileName of CONFIG_FILE_NAMES) {
191
+ const configPath = (0, node_path.join)(cwd, fileName);
192
+ if (await pathExists(configPath)) return configPath;
193
+ }
194
+ return null;
195
+ } catch (error) {
196
+ throw new Error(`Failed to discover MCP config file: ${toErrorMessage$9(error)}`);
197
+ }
198
+ }
199
+ async function resolveServerId(options, resolvedConfigPath) {
200
+ const container = require_src.createProxyIoCContainer();
201
+ if (options.id) return options.id;
202
+ if (resolvedConfigPath) try {
203
+ const config = await container.createConfigFetcherService({
204
+ configFilePath: resolvedConfigPath,
205
+ useCache: options.cache !== false
206
+ }).fetchConfiguration(options.cache === false);
207
+ if (config.id) return config.id;
208
+ } catch (error) {
209
+ throw new Error(`Failed to resolve server ID from config '${resolvedConfigPath}': ${toErrorMessage$9(error)}`);
210
+ }
211
+ return require_src.generateServerId();
212
+ }
213
+ function validateTransportType(type) {
214
+ 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}`);
215
+ return type;
216
+ }
217
+ function validateProxyMode(mode) {
218
+ if (!isValidProxyMode(mode)) throw new Error(`Unknown proxy mode: '${mode}'. Valid options: meta, flat, search`);
219
+ }
220
+ function createTransportConfig(options, mode) {
221
+ return {
222
+ mode,
223
+ port: options.port || Number(process.env.MCP_PORT) || DEFAULT_PORT,
224
+ host: options.host || process.env.MCP_HOST || DEFAULT_HOST
225
+ };
226
+ }
227
+ function createServerOptions(options, resolvedConfigPath, serverId) {
228
+ return {
229
+ configFilePath: resolvedConfigPath,
230
+ noCache: options.cache === false,
231
+ serverId,
232
+ definitionsCachePath: options.definitionsCache,
233
+ clearDefinitionsCache: options.clearDefinitionsCache,
234
+ proxyMode: options.proxyMode
235
+ };
236
+ }
237
+ function formatStartError(type, host, port, error) {
238
+ const startErrorMessage = toErrorMessage$9(error);
239
+ if (type === TRANSPORT_TYPE_STDIO) return `Failed to start MCP server with transport '${type}': ${startErrorMessage}`;
240
+ return `Failed to start MCP server with transport '${type}' on ${host}:${port}: ${startErrorMessage}`;
241
+ }
242
+ function createRuntimeRecord(serverId, config, port, shutdownToken, configPath) {
243
+ return {
244
+ serverId,
245
+ host: config.host ?? DEFAULT_HOST,
246
+ port,
247
+ transport: RUNTIME_TRANSPORT,
248
+ shutdownToken,
249
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
250
+ pid: process.pid,
251
+ configPath
252
+ };
253
+ }
254
+ function createPortRegistryService() {
255
+ return new __agimon_ai_foundation_port_registry.PortRegistryService(process.env.PORT_REGISTRY_PATH);
256
+ }
257
+ function createProcessRegistryService() {
258
+ return new __agimon_ai_foundation_process_registry.ProcessRegistryService(process.env.PROCESS_REGISTRY_PATH);
259
+ }
260
+ function getRegistryEnvironment() {
261
+ return process.env.NODE_ENV ?? "development";
262
+ }
263
+ async function createPortRegistryLease(serviceName, host, preferredPort, serverId, transport, configPath) {
264
+ const portRegistry = createPortRegistryService();
265
+ const result = await portRegistry.reservePort({
266
+ repositoryPath: PORT_REGISTRY_REPOSITORY_PATH,
267
+ serviceName,
268
+ serviceType: PORT_REGISTRY_SERVICE_TYPE,
269
+ environment: getRegistryEnvironment(),
270
+ pid: process.pid,
271
+ host,
272
+ preferredPort,
273
+ portRange: {
274
+ min: preferredPort,
275
+ max: preferredPort
276
+ },
277
+ force: true,
278
+ metadata: {
279
+ transport,
280
+ serverId,
281
+ ...configPath ? { configPath } : {}
282
+ }
283
+ });
284
+ if (!result.success || !result.record) throw new Error(result.error || `Failed to reserve port ${preferredPort} in port registry`);
285
+ let released = false;
286
+ return { release: async () => {
287
+ if (released) return;
288
+ released = true;
289
+ const releaseResult = await portRegistry.releasePort({
290
+ repositoryPath: PORT_REGISTRY_REPOSITORY_PATH,
291
+ serviceName,
292
+ serviceType: PORT_REGISTRY_SERVICE_TYPE,
293
+ pid: process.pid,
294
+ environment: getRegistryEnvironment(),
295
+ force: true
296
+ });
297
+ if (!releaseResult.success && releaseResult.error && !releaseResult.error.includes("No matching registry entry")) throw new Error(releaseResult.error || `Failed to release port for ${serviceName}`);
298
+ } };
299
+ }
300
+ async function createProcessRegistryLease(serviceName, host, port, serverId, transport, configPath) {
301
+ const processRegistry = createProcessRegistryService();
302
+ const result = await processRegistry.registerProcess({
303
+ repositoryPath: PROCESS_REGISTRY_REPOSITORY_PATH,
304
+ serviceName,
305
+ serviceType: PROCESS_REGISTRY_SERVICE_TYPE,
306
+ environment: getRegistryEnvironment(),
307
+ pid: process.pid,
308
+ host,
309
+ port,
310
+ command: process.argv[1],
311
+ args: process.argv.slice(2),
312
+ metadata: {
313
+ transport,
314
+ serverId,
315
+ ...configPath ? { configPath } : {}
316
+ }
317
+ });
318
+ if (!result.success || !result.record) throw new Error(result.error || `Failed to register process for ${serviceName}`);
319
+ let released = false;
320
+ return { release: async (options) => {
321
+ if (released) return;
322
+ released = true;
323
+ const releaseResult = await processRegistry.releaseProcess({
324
+ repositoryPath: PROCESS_REGISTRY_REPOSITORY_PATH,
325
+ serviceName,
326
+ serviceType: PROCESS_REGISTRY_SERVICE_TYPE,
327
+ pid: process.pid,
328
+ environment: getRegistryEnvironment(),
329
+ kill: options?.kill ?? false,
330
+ releasePort: options?.releasePort ?? false
331
+ });
332
+ if (!releaseResult.success && releaseResult.error && !releaseResult.error.includes("No matching process entry")) throw new Error(releaseResult.error || `Failed to release process for ${serviceName}`);
333
+ } };
334
+ }
335
+ async function releasePortLease(lease) {
336
+ if (!lease) return;
337
+ await lease.release();
338
+ }
339
+ function createHttpAdminOptions(serverId, shutdownToken, onShutdownRequested) {
340
+ return {
341
+ serverId,
342
+ shutdownToken,
343
+ onShutdownRequested
344
+ };
345
+ }
346
+ async function removeRuntimeRecord(runtimeStateService, serverId) {
347
+ try {
348
+ await runtimeStateService.remove(serverId);
349
+ } catch (error) {
350
+ throw new Error(`Failed to remove runtime state for '${serverId}': ${toErrorMessage$9(error)}`);
351
+ }
352
+ }
353
+ async function writeRuntimeRecord(runtimeStateService, record) {
354
+ try {
355
+ await runtimeStateService.write(record);
356
+ } catch (error) {
357
+ throw new Error(`Failed to persist runtime state for '${record.serverId}': ${toErrorMessage$9(error)}`);
358
+ }
359
+ }
360
+ async function stopOwnedHttpTransport(handler, runtimeStateService, serverId, processLease) {
361
+ try {
362
+ try {
363
+ await handler.stop();
364
+ } catch (error) {
365
+ throw new Error(`Failed to stop owned HTTP transport '${serverId}': ${toErrorMessage$9(error)}`);
366
+ }
367
+ } finally {
368
+ await processLease?.release({
369
+ kill: false,
370
+ releasePort: false
371
+ });
372
+ await removeRuntimeRecord(runtimeStateService, serverId);
373
+ }
374
+ }
375
+ async function cleanupFailedRuntimeStartup(handler, runtimeStateService, serverId, processLease) {
376
+ try {
377
+ try {
378
+ await handler.stop();
379
+ } catch (error) {
380
+ throw new Error(`Failed to stop HTTP transport during cleanup for '${serverId}': ${toErrorMessage$9(error)}`);
381
+ }
382
+ } finally {
383
+ await processLease?.release({
384
+ kill: false,
385
+ releasePort: false
386
+ });
387
+ await removeRuntimeRecord(runtimeStateService, serverId);
388
+ }
389
+ }
390
+ async function stopTransportWithContext(label, stopOperation) {
391
+ try {
392
+ await stopOperation();
393
+ } catch (error) {
394
+ throw new Error(`${label}: ${toErrorMessage$9(error)}`);
395
+ }
396
+ }
397
+ async function removeRuntimeRecordDuringStop(runtimeStateService, serverId) {
398
+ try {
399
+ await removeRuntimeRecord(runtimeStateService, serverId);
400
+ } catch (error) {
401
+ throw new Error(`Failed to remove runtime state during HTTP stop callback for '${serverId}': ${toErrorMessage$9(error)}`);
402
+ }
403
+ }
404
+ function createStdioHttpInternalTransport(sharedServices, config, adminOptions) {
405
+ try {
406
+ return new require_src.HttpTransportHandler(() => require_src.createSessionServer(sharedServices), config, adminOptions);
407
+ } catch (error) {
408
+ throw new Error(`Failed to create internal HTTP transport for stdio-http proxy: ${toErrorMessage$9(error)}`);
409
+ }
410
+ }
411
+ /**
412
+ * Start MCP server with given transport handler
413
+ * @param handler - The transport handler to start
414
+ * @param onStopped - Optional cleanup callback run after signal-based shutdown
415
+ */
416
+ async function startServer(handler, onStopped) {
417
+ try {
418
+ await handler.start();
419
+ } catch (error) {
420
+ throw new Error(`Failed to start transport handler: ${toErrorMessage$9(error)}`);
421
+ }
422
+ const shutdown = async (signal) => {
423
+ console.error(`\nReceived ${signal}, shutting down gracefully...`);
424
+ try {
425
+ await handler.stop();
426
+ if (onStopped) await onStopped();
427
+ process.exit(0);
428
+ } catch (error) {
429
+ console.error(`Failed to gracefully stop transport during ${signal}: ${toErrorMessage$9(error)}`);
430
+ process.exit(1);
431
+ }
432
+ };
433
+ process.on("SIGINT", async () => await shutdown("SIGINT"));
434
+ process.on("SIGTERM", async () => await shutdown("SIGTERM"));
435
+ }
436
+ async function createAndStartHttpRuntime(serverOptions, config, resolvedConfigPath) {
437
+ const sharedServices = await require_src.initializeSharedServices(serverOptions);
438
+ const runtimeStateService = new require_src.RuntimeStateService();
439
+ const shutdownToken = (0, node_crypto.randomUUID)();
440
+ const runtimeServerId = serverOptions.serverId ?? require_src.generateServerId();
441
+ const processLease = await createProcessRegistryLease(PROCESS_REGISTRY_SERVICE_HTTP, config.host ?? DEFAULT_HOST, config.port ?? DEFAULT_PORT, runtimeServerId, TRANSPORT_TYPE_HTTP, resolvedConfigPath);
442
+ const portLease = await createPortRegistryLease(PORT_REGISTRY_SERVICE_HTTP, config.host ?? DEFAULT_HOST, config.port ?? DEFAULT_PORT, runtimeServerId, TRANSPORT_TYPE_HTTP, resolvedConfigPath);
443
+ let releasePort = async () => {
444
+ await releasePortLease(portLease);
445
+ releasePort = async () => void 0;
446
+ };
447
+ const runtimeRecord = createRuntimeRecord(runtimeServerId, config, config.port ?? DEFAULT_PORT, shutdownToken, resolvedConfigPath);
448
+ let handler;
449
+ let isStopping = false;
450
+ const stopHandler = async () => {
451
+ if (isStopping) return;
452
+ isStopping = true;
453
+ try {
454
+ await stopOwnedHttpTransport(handler, runtimeStateService, runtimeRecord.serverId, processLease);
455
+ await releasePort();
456
+ await sharedServices.dispose();
457
+ process.exit(0);
458
+ } catch (error) {
459
+ throw new Error(`Failed to stop HTTP runtime '${runtimeRecord.serverId}' from admin shutdown: ${toErrorMessage$9(error)}`);
460
+ }
461
+ };
462
+ try {
463
+ handler = new require_src.HttpTransportHandler(() => require_src.createSessionServer(sharedServices), config, createHttpAdminOptions(runtimeRecord.serverId, shutdownToken, stopHandler));
464
+ } catch (error) {
465
+ await releasePort();
466
+ await processLease.release({
467
+ kill: false,
468
+ releasePort: false
469
+ });
470
+ await sharedServices.dispose();
471
+ throw new Error(`Failed to create HTTP runtime server: ${toErrorMessage$9(error)}`);
472
+ }
473
+ try {
474
+ await startServer(handler, async () => {
475
+ await releasePort();
476
+ await sharedServices.dispose();
477
+ await processLease.release({
478
+ kill: false,
479
+ releasePort: false
480
+ });
481
+ await removeRuntimeRecordDuringStop(runtimeStateService, runtimeRecord.serverId);
482
+ });
483
+ await writeRuntimeRecord(runtimeStateService, runtimeRecord);
484
+ } catch (error) {
485
+ await releasePort();
486
+ await processLease.release({
487
+ kill: false,
488
+ releasePort: false
489
+ });
490
+ await sharedServices.dispose();
491
+ await cleanupFailedRuntimeStartup(handler, runtimeStateService, runtimeRecord.serverId, processLease);
492
+ throw new Error(`Failed to start HTTP runtime '${runtimeRecord.serverId}': ${toErrorMessage$9(error)}`);
493
+ }
494
+ console.error(`Runtime state: http://${runtimeRecord.host}:${runtimeRecord.port} (${runtimeRecord.serverId})`);
495
+ }
496
+ async function stopInternalHttpTransport(stdioHttpHandler, httpHandler, ownsInternalHttpTransport) {
497
+ try {
498
+ const stopOperations = [stopTransportWithContext(STDIO_HTTP_PROXY_STOP_LABEL, async () => {
499
+ await stdioHttpHandler.stop();
500
+ })];
501
+ if (ownsInternalHttpTransport && httpHandler) stopOperations.push(stopTransportWithContext(INTERNAL_HTTP_STOP_LABEL, async () => {
502
+ await httpHandler.stop();
503
+ }));
504
+ await Promise.all(stopOperations);
505
+ } catch (error) {
506
+ throw new Error(`Failed to stop stdio-http transport: ${toErrorMessage$9(error)}`);
507
+ }
508
+ }
509
+ async function startStdioTransport(serverOptions) {
510
+ try {
511
+ await startServer(new require_src.StdioTransportHandler(await require_src.createServer(serverOptions)));
512
+ } catch (error) {
513
+ throw new Error(`Failed to start stdio transport: ${toErrorMessage$9(error)}`);
514
+ }
515
+ }
516
+ async function startSseTransport(serverOptions, config) {
517
+ try {
518
+ await startServer(new require_src.SseTransportHandler(await require_src.createServer(serverOptions), config));
519
+ } catch (error) {
520
+ throw new Error(`Failed to start SSE transport: ${toErrorMessage$9(error)}`);
521
+ }
522
+ }
523
+ async function startStdioHttpTransport(serverOptions, config, resolvedConfigPath) {
524
+ const shared = { services: null };
525
+ let processLease;
526
+ let internalPortLease = null;
527
+ try {
528
+ const stdioHttpHandler = new require_src.StdioHttpTransportHandler({ endpoint: new URL(`http://${config.host}:${config.port}${MCP_ENDPOINT_PATH}`) });
529
+ const runtimeStateService = new require_src.RuntimeStateService();
530
+ const serverId = serverOptions.serverId ?? require_src.generateServerId();
531
+ const shutdownToken = (0, node_crypto.randomUUID)();
532
+ processLease = await createProcessRegistryLease(PROCESS_REGISTRY_SERVICE_STDIO_HTTP, config.host ?? DEFAULT_HOST, config.port ?? DEFAULT_PORT, serverId, TRANSPORT_TYPE_STDIO_HTTP, resolvedConfigPath);
533
+ let httpHandler = null;
534
+ let ownsInternalHttpTransport = false;
535
+ let isStopping = false;
536
+ let releaseInternalPort = async () => {
537
+ await releasePortLease(internalPortLease);
538
+ internalPortLease = null;
539
+ releaseInternalPort = async () => void 0;
540
+ };
541
+ const stopOwnedRuntime = async () => {
542
+ if (isStopping) return;
543
+ isStopping = true;
544
+ try {
545
+ await Promise.all([stopInternalHttpTransport(stdioHttpHandler, httpHandler, ownsInternalHttpTransport), removeRuntimeRecord(runtimeStateService, serverId)]);
546
+ await releaseInternalPort();
547
+ if (shared.services) await shared.services.dispose();
548
+ ownsInternalHttpTransport = false;
549
+ process.exit(0);
550
+ } catch (error) {
551
+ console.error(`Unexpected error during admin shutdown: ${toErrorMessage$9(error)}`);
552
+ process.exit(1);
553
+ }
554
+ };
555
+ const adminOptions = createHttpAdminOptions(serverId, shutdownToken, stopOwnedRuntime);
556
+ await startServer({
557
+ async start() {
558
+ let initialProxyConnectError;
559
+ try {
560
+ await stdioHttpHandler.start();
561
+ return;
562
+ } catch (error) {
563
+ initialProxyConnectError = error;
564
+ }
565
+ if (!shared.services) shared.services = await require_src.initializeSharedServices(serverOptions);
566
+ try {
567
+ internalPortLease = await createPortRegistryLease(PORT_REGISTRY_SERVICE_STDIO_HTTP, config.host ?? DEFAULT_HOST, config.port ?? DEFAULT_PORT, serverId, TRANSPORT_TYPE_STDIO_HTTP, resolvedConfigPath);
568
+ httpHandler = createStdioHttpInternalTransport(shared.services, config, adminOptions);
569
+ await httpHandler.start();
570
+ ownsInternalHttpTransport = true;
571
+ } catch (error) {
572
+ if (!isAddressInUseError(error)) {
573
+ await releaseInternalPort();
574
+ throw new Error(`Failed to start internal HTTP transport for stdio-http proxy: ${toErrorMessage$9(error)}`);
575
+ }
576
+ }
577
+ try {
578
+ await stdioHttpHandler.start();
579
+ } catch (error) {
580
+ let rollbackStopErrorMessage = "";
581
+ if (ownsInternalHttpTransport && httpHandler) {
582
+ try {
583
+ await httpHandler.stop();
584
+ } catch (stopError) {
585
+ rollbackStopErrorMessage = toErrorMessage$9(stopError);
586
+ }
587
+ ownsInternalHttpTransport = false;
588
+ }
589
+ await releaseInternalPort();
590
+ const retryErrorMessage = toErrorMessage$9(error);
591
+ const initialErrorMessage = toErrorMessage$9(initialProxyConnectError);
592
+ const rollbackMessage = rollbackStopErrorMessage ? `; rollback stop failed: ${rollbackStopErrorMessage}` : "";
593
+ throw new Error(`Failed to start stdio-http proxy bridge: initial connect failed (${initialErrorMessage}); retry failed (${retryErrorMessage})${rollbackMessage}`);
594
+ }
595
+ if (ownsInternalHttpTransport) try {
596
+ await writeRuntimeRecord(runtimeStateService, createRuntimeRecord(serverId, config, config.port ?? DEFAULT_PORT, shutdownToken, resolvedConfigPath));
597
+ } catch (error) {
598
+ throw new Error(`Failed to persist runtime state for stdio-http server '${serverId}': ${toErrorMessage$9(error)}`);
599
+ }
600
+ },
601
+ async stop() {
602
+ try {
603
+ await Promise.all([stopInternalHttpTransport(stdioHttpHandler, httpHandler, ownsInternalHttpTransport), removeRuntimeRecord(runtimeStateService, serverId)]);
604
+ await releaseInternalPort();
605
+ await processLease?.release({
606
+ kill: false,
607
+ releasePort: false
608
+ });
609
+ ownsInternalHttpTransport = false;
610
+ if (shared.services) await shared.services.dispose();
611
+ } catch (error) {
612
+ ownsInternalHttpTransport = false;
613
+ throw new Error(`Failed during stdio-http shutdown for '${serverId}': ${toErrorMessage$9(error)}`);
614
+ }
615
+ }
616
+ });
617
+ } catch (error) {
618
+ try {
619
+ await releasePortLease(internalPortLease);
620
+ await processLease?.release({
621
+ kill: false,
622
+ releasePort: false
623
+ });
624
+ } catch {}
625
+ if (shared.services) await shared.services.dispose();
626
+ throw new Error(`Failed to start stdio-http transport: ${toErrorMessage$9(error)}`);
627
+ }
628
+ }
629
+ async function startTransport(transportType, options, resolvedConfigPath, serverOptions) {
630
+ try {
631
+ if (transportType === TRANSPORT_TYPE_STDIO) {
632
+ await startStdioTransport(serverOptions);
633
+ return;
634
+ }
635
+ if (transportType === TRANSPORT_TYPE_HTTP) {
636
+ await createAndStartHttpRuntime(serverOptions, createTransportConfig(options, require_src.TRANSPORT_MODE.HTTP), resolvedConfigPath);
637
+ return;
638
+ }
639
+ if (transportType === TRANSPORT_TYPE_SSE) {
640
+ await startSseTransport(serverOptions, createTransportConfig(options, require_src.TRANSPORT_MODE.SSE));
641
+ return;
642
+ }
643
+ await startStdioHttpTransport(serverOptions, createTransportConfig(options, require_src.TRANSPORT_MODE.HTTP), resolvedConfigPath);
644
+ } catch (error) {
645
+ throw new Error(`Failed to start transport '${transportType}': ${toErrorMessage$9(error)}`);
646
+ }
647
+ }
648
+ /**
649
+ * MCP Serve command
650
+ */
651
+ const mcpServeCommand = new commander.Command("mcp-serve").description("Start MCP server with specified transport").option("-t, --type <type>", `Transport type: ${TRANSPORT_TYPE_STDIO}, ${TRANSPORT_TYPE_HTTP}, ${TRANSPORT_TYPE_SSE}, or ${TRANSPORT_TYPE_STDIO_HTTP}`, TRANSPORT_TYPE_STDIO).option("-p, --port <port>", "Port to listen on (http/sse/stdio-http internal HTTP)", (val) => parseInt(val, 10), DEFAULT_PORT).option("--host <host>", "Host to bind to (http/sse/stdio-http internal HTTP)", DEFAULT_HOST).option("-c, --config <path>", "Path to MCP server configuration file").option("--no-cache", "Disable configuration caching, always reload from config file").option("--definitions-cache <path>", "Path to prefetched tool/prompt/skill definitions cache file").option("--clear-definitions-cache", "Delete definitions cache before startup", false).option("--proxy-mode <mode>", "How mcp-proxy exposes downstream tools: meta, flat, or search", "meta").option("--id <id>", "Unique server identifier (overrides config file id, auto-generated if not provided)").action(async (options) => {
652
+ try {
653
+ const transportType = validateTransportType(options.type.toLowerCase());
654
+ validateProxyMode(options.proxyMode);
655
+ const resolvedConfigPath = options.config || await findConfigFileAsync() || void 0;
656
+ await startTransport(transportType, options, resolvedConfigPath, createServerOptions(options, resolvedConfigPath, await resolveServerId(options, resolvedConfigPath)));
657
+ } catch (error) {
658
+ const rawTransportType = options.type.toLowerCase();
659
+ const transportType = isValidTransportType(rawTransportType) ? rawTransportType : TRANSPORT_TYPE_STDIO;
660
+ console.error(formatStartError(transportType, options.host, options.port, error));
661
+ process.exit(1);
662
+ }
663
+ });
664
+
665
+ //#endregion
666
+ //#region src/commands/bootstrap.ts
667
+ function toErrorMessage$8(error) {
668
+ return error instanceof Error ? error.message : String(error);
669
+ }
670
+ async function withConnectedCommandContext(options, run) {
671
+ const container = require_src.createProxyIoCContainer();
672
+ const configFilePath = options.config || require_src.findConfigFile();
673
+ if (!configFilePath) throw new Error("No config file found. Use --config or create mcp-config.yaml");
674
+ const config = await container.createConfigFetcherService({
675
+ configFilePath,
676
+ useCache: options.useCache
677
+ }).fetchConfiguration();
678
+ const clientManager = container.createClientManagerService();
679
+ await Promise.all(Object.entries(config.mcpServers).map(async ([serverName, serverConfig]) => {
680
+ try {
681
+ await clientManager.connectToServer(serverName, serverConfig);
682
+ if (!options.json) console.error(`✓ Connected to ${serverName}`);
683
+ } catch (error) {
684
+ if (!options.json) console.error(`✗ Failed to connect to ${serverName}: ${toErrorMessage$8(error)}`);
685
+ }
686
+ }));
687
+ if (clientManager.getAllClients().length === 0) throw new Error("No MCP servers connected");
688
+ try {
689
+ return await run({
690
+ container,
691
+ configFilePath,
692
+ config,
693
+ clientManager
694
+ });
695
+ } finally {
696
+ await clientManager.disconnectAll();
697
+ }
698
+ }
699
+
700
+ //#endregion
701
+ //#region src/commands/list-tools.ts
702
+ /**
703
+ * List Tools Command
704
+ *
705
+ * DESIGN PATTERNS:
706
+ * - Command pattern with Commander for CLI argument parsing
707
+ * - Async/await pattern for asynchronous operations
708
+ * - Error handling pattern with try-catch and proper exit codes
709
+ *
710
+ * CODING STANDARDS:
711
+ * - Use async action handlers for asynchronous operations
712
+ * - Provide clear option descriptions and default values
713
+ * - Handle errors gracefully with process.exit()
714
+ * - Log progress and errors to console
715
+ * - Use Commander's .option() and .argument() for inputs
716
+ *
717
+ * AVOID:
718
+ * - Synchronous blocking operations in action handlers
719
+ * - Missing error handling (always use try-catch)
720
+ * - Hardcoded values (use options or environment variables)
721
+ * - Not exiting with appropriate exit codes on errors
722
+ */
723
+ function toErrorMessage$7(error) {
724
+ return error instanceof Error ? error.message : String(error);
725
+ }
726
+ function printSearchResults(result) {
727
+ for (const server of result.servers) {
728
+ console.log(`\n${server.server}:`);
729
+ if (server.capabilities && server.capabilities.length > 0) console.log(` capabilities: ${server.capabilities.join(", ")}`);
730
+ if (server.summary) console.log(` summary: ${server.summary}`);
731
+ if (server.tools.length === 0) {
732
+ console.log(" no tools");
733
+ continue;
734
+ }
735
+ for (const tool of server.tools) {
736
+ const capabilitySummary = tool.capabilities && tool.capabilities.length > 0 ? ` [${tool.capabilities.join(", ")}]` : "";
737
+ console.log(` - ${tool.name}${capabilitySummary}`);
738
+ if (tool.description) console.log(` ${tool.description}`);
739
+ }
740
+ }
741
+ }
742
+ const searchToolsCommand = new commander.Command("search-tools").description("Search proxied MCP tools by capability or server").option("-c, --config <path>", "Path to MCP server configuration file").option("-s, --server <name>", "Filter by server name").option("--capability <name>", "Filter by capability tag, summary, tool name, or description").option("--definitions-cache <path>", "Path to definitions cache file").option("-j, --json", "Output as JSON", false).action(async (options) => {
743
+ try {
744
+ await withConnectedCommandContext(options, async ({ container, config, clientManager, configFilePath }) => {
745
+ clientManager.registerServerConfigs(config.mcpServers);
746
+ const cachePath = options.definitionsCache || require_src.DefinitionsCacheService.getDefaultCachePath(configFilePath);
747
+ let cacheData;
748
+ try {
749
+ cacheData = await require_src.DefinitionsCacheService.readFromFile(cachePath);
750
+ } catch {
751
+ cacheData = void 0;
752
+ }
753
+ const definitionsCacheService = container.createDefinitionsCacheService(clientManager, void 0, { cacheData });
754
+ const textBlock = (await container.createSearchListToolsTool(clientManager, definitionsCacheService).execute({
755
+ capability: options.capability,
756
+ serverName: options.server
757
+ })).content.find((content) => content.type === "text");
758
+ const parsed = textBlock?.type === "text" ? JSON.parse(textBlock.text) : { servers: [] };
759
+ if (options.json) console.log(JSON.stringify(parsed, null, 2));
760
+ else {
761
+ if (!parsed.servers || parsed.servers.length === 0) throw new Error("No tools matched the requested filters");
762
+ printSearchResults(parsed);
763
+ }
764
+ });
765
+ } catch (error) {
766
+ console.error(`Error executing search-tools: ${toErrorMessage$7(error)}`);
767
+ process.exit(1);
768
+ }
769
+ });
770
+
771
+ //#endregion
772
+ //#region src/commands/describe-tools.ts
773
+ /**
774
+ * Describe Tools Command
775
+ *
776
+ * DESIGN PATTERNS:
777
+ * - Command pattern with Commander for CLI argument parsing
778
+ * - Async/await pattern for asynchronous operations
779
+ * - Error handling pattern with try-catch and proper exit codes
780
+ *
781
+ * CODING STANDARDS:
782
+ * - Use async action handlers for asynchronous operations
783
+ * - Provide clear option descriptions and default values
784
+ * - Handle errors gracefully with process.exit()
785
+ * - Log progress and errors to console
786
+ * - Use Commander's .option() and .argument() for inputs
787
+ *
788
+ * AVOID:
789
+ * - Synchronous blocking operations in action handlers
790
+ * - Missing error handling (always use try-catch)
791
+ * - Hardcoded values (use options or environment variables)
792
+ * - Not exiting with appropriate exit codes on errors
793
+ */
794
+ function toErrorMessage$6(error) {
795
+ return error instanceof Error ? error.message : String(error);
796
+ }
797
+ /**
798
+ * Describe specific MCP tools
799
+ */
800
+ const describeToolsCommand = new commander.Command("describe-tools").description("Describe specific MCP tools").argument("<toolNames...>", "Tool names to describe").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 (toolNames, options) => {
801
+ try {
802
+ await withConnectedCommandContext(options, async ({ container, config, clientManager }) => {
803
+ const clients = options.server ? [clientManager.getClient(options.server)].filter((client) => client !== void 0) : clientManager.getAllClients();
804
+ if (options.server && clients.length === 0) throw new Error(`Server "${options.server}" not found`);
805
+ const cwd = process.env.PROJECT_PATH || process.cwd();
806
+ const skillPaths = config.skills?.paths || [];
807
+ const skillService = skillPaths.length > 0 ? container.createSkillService(cwd, skillPaths) : void 0;
808
+ const foundTools = [];
809
+ const foundSkills = [];
810
+ const notFoundTools = [...toolNames];
811
+ const toolResults = await Promise.all(clients.map(async (client) => {
812
+ try {
813
+ return {
814
+ client,
815
+ tools: await client.listTools(),
816
+ error: null
817
+ };
818
+ } catch (error) {
819
+ return {
820
+ client,
821
+ tools: [],
822
+ error
823
+ };
824
+ }
825
+ }));
826
+ for (const { client, tools, error } of toolResults) {
827
+ if (error) {
828
+ if (!options.json) console.error(`Failed to list tools from ${client.serverName}:`, error);
829
+ continue;
830
+ }
831
+ for (const toolName of toolNames) {
832
+ const tool = tools.find((entry) => entry.name === toolName);
833
+ if (tool) {
834
+ foundTools.push({
835
+ server: client.serverName,
836
+ name: tool.name,
837
+ description: tool.description,
838
+ inputSchema: tool.inputSchema
839
+ });
840
+ const idx = notFoundTools.indexOf(toolName);
841
+ if (idx > -1) notFoundTools.splice(idx, 1);
842
+ }
843
+ }
844
+ }
845
+ if (skillService && notFoundTools.length > 0) {
846
+ const skillResults = await Promise.all([...notFoundTools].map(async (toolName) => {
847
+ const skillName = toolName.startsWith("skill__") ? toolName.slice(7) : toolName;
848
+ return {
849
+ toolName,
850
+ skill: await skillService.getSkill(skillName)
851
+ };
852
+ }));
853
+ for (const { toolName, skill } of skillResults) if (skill) {
854
+ foundSkills.push({
855
+ name: skill.name,
856
+ location: skill.basePath,
857
+ instructions: skill.content
858
+ });
859
+ const idx = notFoundTools.indexOf(toolName);
860
+ if (idx > -1) notFoundTools.splice(idx, 1);
861
+ }
862
+ }
863
+ const nextSteps = [];
864
+ if (foundTools.length > 0) nextSteps.push("For MCP tools: Use the use_tool function with toolName and toolArgs based on the inputSchema above.");
865
+ if (foundSkills.length > 0) nextSteps.push(`For skill, just follow skill's description to continue.`);
866
+ if (options.json) {
867
+ const result = {};
868
+ if (foundTools.length > 0) result.tools = foundTools;
869
+ if (foundSkills.length > 0) result.skills = foundSkills;
870
+ if (nextSteps.length > 0) result.nextSteps = nextSteps;
871
+ if (notFoundTools.length > 0) result.notFound = notFoundTools;
872
+ console.log(JSON.stringify(result, null, 2));
873
+ } else {
874
+ if (foundTools.length > 0) {
875
+ console.log("\nFound tools:\n");
876
+ for (const tool of foundTools) {
877
+ console.log(`Server: ${tool.server}`);
878
+ console.log(`Tool: ${tool.name}`);
879
+ console.log(`Description: ${tool.description || "No description"}`);
880
+ console.log("Input Schema:");
881
+ console.log(JSON.stringify(tool.inputSchema, null, 2));
882
+ console.log("");
883
+ }
884
+ }
885
+ if (foundSkills.length > 0) {
886
+ console.log("\nFound skills:\n");
887
+ for (const skill of foundSkills) {
888
+ console.log(`Skill: ${skill.name}`);
889
+ console.log(`Location: ${skill.location}`);
890
+ console.log(`Instructions:\n${skill.instructions}`);
891
+ console.log("");
892
+ }
893
+ }
894
+ if (nextSteps.length > 0) {
895
+ console.log("\nNext steps:");
896
+ for (const step of nextSteps) console.log(` • ${step}`);
897
+ console.log("");
898
+ }
899
+ if (notFoundTools.length > 0) console.error(`\nTools/skills not found: ${notFoundTools.join(", ")}`);
900
+ if (foundTools.length === 0 && foundSkills.length === 0) {
901
+ console.error("No tools or skills found");
902
+ process.exit(1);
903
+ }
904
+ }
905
+ });
906
+ } catch (error) {
907
+ console.error(`Error executing describe-tools: ${toErrorMessage$6(error)}`);
908
+ process.exit(1);
909
+ }
910
+ });
911
+
912
+ //#endregion
913
+ //#region src/commands/use-tool.ts
914
+ /**
915
+ * Use Tool Command
916
+ *
917
+ * DESIGN PATTERNS:
918
+ * - Command pattern with Commander for CLI argument parsing
919
+ * - Async/await pattern for asynchronous operations
920
+ * - Error handling pattern with try-catch and proper exit codes
921
+ *
922
+ * CODING STANDARDS:
923
+ * - Use async action handlers for asynchronous operations
924
+ * - Provide clear option descriptions and default values
925
+ * - Handle errors gracefully with process.exit()
926
+ * - Log progress and errors to console
927
+ * - Use Commander'"'"'s .option() and .argument() for inputs
928
+ *
929
+ * AVOID:
930
+ * - Synchronous blocking operations in action handlers
931
+ * - Missing error handling (always use try-catch)
932
+ * - Hardcoded values (use options or environment variables)
933
+ * - Not exiting with appropriate exit codes on errors
934
+ */
935
+ function toErrorMessage$5(error) {
936
+ return error instanceof Error ? error.message : String(error);
937
+ }
938
+ /**
939
+ * Execute an MCP tool with arguments
940
+ */
941
+ const useToolCommand = new commander.Command("use-tool").description("Execute an MCP tool with arguments").argument("<toolName>", "Tool name to execute").option("-c, --config <path>", "Path to MCP server configuration file").option("-s, --server <name>", "Server name (required if tool exists on multiple servers)").option("-a, --args <json>", "Tool arguments as JSON string", "{}").option("-t, --timeout <ms>", "Request timeout in milliseconds for tool execution (default: 60000)", parseInt).option("-j, --json", "Output as JSON", false).action(async (toolName, options) => {
942
+ try {
943
+ let toolArgs = {};
944
+ try {
945
+ toolArgs = JSON.parse(options.args);
946
+ } catch {
947
+ console.error("Error: Invalid JSON in --args");
948
+ process.exit(1);
949
+ }
950
+ await withConnectedCommandContext(options, async ({ container, config, clientManager }) => {
951
+ const clients = clientManager.getAllClients();
952
+ if (options.server) {
953
+ const client$1 = clientManager.getClient(options.server);
954
+ if (!client$1) throw new Error(`Server "${options.server}" not found`);
955
+ if (!options.json) console.error(`Executing ${toolName} on ${options.server}...`);
956
+ const requestOptions$1 = options.timeout ? { timeout: options.timeout } : void 0;
957
+ const result$1 = await client$1.callTool(toolName, toolArgs, requestOptions$1);
958
+ if (options.json) console.log(JSON.stringify(result$1, null, 2));
959
+ else {
960
+ console.log("\nResult:");
961
+ if (result$1.content) for (const content of result$1.content) if (content.type === "text") console.log(content.text);
962
+ else console.log(JSON.stringify(content, null, 2));
963
+ if (result$1.isError) {
964
+ console.error("\n⚠️ Tool execution returned an error");
965
+ process.exit(1);
966
+ }
967
+ }
968
+ return;
969
+ }
970
+ const searchResults = await Promise.all(clients.map(async (client$1) => {
971
+ try {
972
+ const hasTool = (await client$1.listTools()).some((t) => t.name === toolName);
973
+ return {
974
+ serverName: client$1.serverName,
975
+ hasTool,
976
+ error: null
977
+ };
978
+ } catch (error) {
979
+ return {
980
+ serverName: client$1.serverName,
981
+ hasTool: false,
982
+ error
983
+ };
984
+ }
985
+ }));
986
+ const matchingServers = [];
987
+ for (const { serverName, hasTool, error } of searchResults) {
988
+ if (error) {
989
+ if (!options.json) console.error(`Failed to list tools from ${serverName}:`, error);
990
+ continue;
991
+ }
992
+ if (hasTool) matchingServers.push(serverName);
993
+ }
994
+ if (matchingServers.length === 0) {
995
+ const skillPaths = config.skills?.paths || [];
996
+ if (skillPaths.length > 0) try {
997
+ const cwd = process.env.PROJECT_PATH || process.cwd();
998
+ const skillService = container.createSkillService(cwd, skillPaths);
999
+ const skillName = toolName.startsWith("skill__") ? toolName.slice(7) : toolName;
1000
+ const skill = await skillService.getSkill(skillName);
1001
+ if (skill) {
1002
+ const result$1 = { content: [{
1003
+ type: "text",
1004
+ text: skill.content
1005
+ }] };
1006
+ if (options.json) console.log(JSON.stringify(result$1, null, 2));
1007
+ else {
1008
+ console.log("\nSkill content:");
1009
+ console.log(skill.content);
1010
+ }
1011
+ return;
1012
+ }
1013
+ } catch (error) {
1014
+ if (!options.json) console.error(`Failed to lookup skill "${toolName}":`, error);
1015
+ }
1016
+ throw new Error(`Tool or skill "${toolName}" not found on any connected server or configured skill paths`);
1017
+ }
1018
+ if (matchingServers.length > 1) throw new Error(`Tool "${toolName}" found on multiple servers: ${matchingServers.join(", ")}`);
1019
+ const targetServer = matchingServers[0];
1020
+ const client = clientManager.getClient(targetServer);
1021
+ if (!client) throw new Error(`Internal error: Server "${targetServer}" not connected`);
1022
+ if (!options.json) console.error(`Executing ${toolName} on ${targetServer}...`);
1023
+ const requestOptions = options.timeout ? { timeout: options.timeout } : void 0;
1024
+ const result = await client.callTool(toolName, toolArgs, requestOptions);
1025
+ if (options.json) console.log(JSON.stringify(result, null, 2));
1026
+ else {
1027
+ console.log("\nResult:");
1028
+ if (result.content) for (const content of result.content) if (content.type === "text") console.log(content.text);
1029
+ else console.log(JSON.stringify(content, null, 2));
1030
+ if (result.isError) {
1031
+ console.error("\n⚠️ Tool execution returned an error");
1032
+ process.exit(1);
1033
+ }
1034
+ }
1035
+ });
1036
+ } catch (error) {
1037
+ console.error(`Error executing use-tool: ${toErrorMessage$5(error)}`);
1038
+ process.exit(1);
1039
+ }
1040
+ });
1041
+
1042
+ //#endregion
1043
+ //#region src/commands/list-resources.ts
1044
+ /**
1045
+ * ListResources Command
1046
+ *
1047
+ * DESIGN PATTERNS:
1048
+ * - Command pattern with Commander for CLI argument parsing
1049
+ * - Async/await pattern for asynchronous operations
1050
+ * - Error handling pattern with try-catch and proper exit codes
1051
+ *
1052
+ * CODING STANDARDS:
1053
+ * - Use async action handlers for asynchronous operations
1054
+ * - Provide clear option descriptions and default values
1055
+ * - Handle errors gracefully with process.exit()
1056
+ * - Log progress and errors to console
1057
+ * - Use Commander's .option() and .argument() for inputs
1058
+ *
1059
+ * AVOID:
1060
+ * - Synchronous blocking operations in action handlers
1061
+ * - Missing error handling (always use try-catch)
1062
+ * - Hardcoded values (use options or environment variables)
1063
+ * - Not exiting with appropriate exit codes on errors
1064
+ */
1065
+ function toErrorMessage$4(error) {
1066
+ return error instanceof Error ? error.message : String(error);
1067
+ }
1068
+ /**
1069
+ * List all available resources from connected MCP servers
1070
+ */
1071
+ const listResourcesCommand = new commander.Command("list-resources").description("List all available resources 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) => {
1072
+ try {
1073
+ await withConnectedCommandContext(options, async ({ clientManager }) => {
1074
+ const clients = options.server ? [clientManager.getClient(options.server)].filter((c) => c !== void 0) : clientManager.getAllClients();
1075
+ if (options.server && clients.length === 0) throw new Error(`Server "${options.server}" not found`);
1076
+ const resourcesByServer = {};
1077
+ const resourceResults = await Promise.all(clients.map(async (client) => {
1078
+ try {
1079
+ const resources = await client.listResources();
1080
+ return {
1081
+ serverName: client.serverName,
1082
+ resources,
1083
+ error: null
1084
+ };
1085
+ } catch (error) {
1086
+ return {
1087
+ serverName: client.serverName,
1088
+ resources: [],
1089
+ error
1090
+ };
1091
+ }
1092
+ }));
1093
+ for (const { serverName, resources, error } of resourceResults) {
1094
+ if (error && !options.json) console.error(`Failed to list resources from ${serverName}: ${toErrorMessage$4(error)}`);
1095
+ resourcesByServer[serverName] = resources;
1096
+ }
1097
+ if (options.json) console.log(JSON.stringify(resourcesByServer, null, 2));
1098
+ else for (const [serverName, resources] of Object.entries(resourcesByServer)) {
1099
+ console.log(`\n${serverName}:`);
1100
+ if (resources.length === 0) console.log(" No resources available");
1101
+ else for (const resource of resources) {
1102
+ const label = resource.name ? `${resource.name} (${resource.uri})` : resource.uri;
1103
+ console.log(` - ${label}${resource.description ? `: ${resource.description}` : ""}`);
1104
+ }
1105
+ }
1106
+ });
1107
+ } catch (error) {
1108
+ console.error(`Error executing list-resources: ${toErrorMessage$4(error)}`);
1109
+ process.exit(1);
1110
+ }
1111
+ });
1112
+
1113
+ //#endregion
1114
+ //#region src/commands/read-resource.ts
1115
+ /**
1116
+ * ReadResource Command
1117
+ *
1118
+ * DESIGN PATTERNS:
1119
+ * - Command pattern with Commander for CLI argument parsing
1120
+ * - Async/await pattern for asynchronous operations
1121
+ * - Error handling pattern with try-catch and proper exit codes
1122
+ *
1123
+ * CODING STANDARDS:
1124
+ * - Use async action handlers for asynchronous operations
1125
+ * - Provide clear option descriptions and default values
1126
+ * - Handle errors gracefully with process.exit()
1127
+ * - Log progress and errors to console
1128
+ * - Use Commander's .option() and .argument() for inputs
1129
+ *
1130
+ * AVOID:
1131
+ * - Synchronous blocking operations in action handlers
1132
+ * - Missing error handling (always use try-catch)
1133
+ * - Hardcoded values (use options or environment variables)
1134
+ * - Not exiting with appropriate exit codes on errors
1135
+ */
1136
+ function toErrorMessage$3(error) {
1137
+ return error instanceof Error ? error.message : String(error);
1138
+ }
1139
+ /**
1140
+ * Read a resource by URI from a connected MCP server
1141
+ */
1142
+ const readResourceCommand = new commander.Command("read-resource").description("Read a resource by URI from a connected MCP server").argument("<uri>", "Resource URI to read").option("-c, --config <path>", "Path to MCP server configuration file").option("-s, --server <name>", "Server name (required if resource exists on multiple servers)").option("-j, --json", "Output as JSON", false).action(async (uri, options) => {
1143
+ try {
1144
+ await withConnectedCommandContext(options, async ({ clientManager }) => {
1145
+ const clients = clientManager.getAllClients();
1146
+ if (options.server) {
1147
+ const client$1 = clientManager.getClient(options.server);
1148
+ if (!client$1) throw new Error(`Server "${options.server}" not found`);
1149
+ if (!options.json) console.error(`Reading ${uri} from ${options.server}...`);
1150
+ const result$1 = await client$1.readResource(uri);
1151
+ if (options.json) console.log(JSON.stringify(result$1, null, 2));
1152
+ else for (const content of result$1.contents) if ("text" in content) console.log(content.text);
1153
+ else console.log(JSON.stringify(content, null, 2));
1154
+ return;
1155
+ }
1156
+ const searchResults = await Promise.all(clients.map(async (client$1) => {
1157
+ try {
1158
+ const hasResource = (await client$1.listResources()).some((r) => r.uri === uri);
1159
+ return {
1160
+ serverName: client$1.serverName,
1161
+ hasResource,
1162
+ error: null
1163
+ };
1164
+ } catch (error) {
1165
+ return {
1166
+ serverName: client$1.serverName,
1167
+ hasResource: false,
1168
+ error
1169
+ };
1170
+ }
1171
+ }));
1172
+ const matchingServers = [];
1173
+ for (const { serverName, hasResource, error } of searchResults) {
1174
+ if (error) {
1175
+ console.error(`Failed to list resources from ${serverName}: ${toErrorMessage$3(error)}`);
1176
+ continue;
1177
+ }
1178
+ if (hasResource) matchingServers.push(serverName);
1179
+ }
1180
+ if (matchingServers.length === 0) throw new Error(`Resource "${uri}" not found on any connected server`);
1181
+ if (matchingServers.length > 1) throw new Error(`Resource "${uri}" found on multiple servers: ${matchingServers.join(", ")}. Use --server to disambiguate`);
1182
+ const targetServer = matchingServers[0];
1183
+ const client = clientManager.getClient(targetServer);
1184
+ if (!client) throw new Error(`Internal error: Server "${targetServer}" not connected`);
1185
+ if (!options.json) console.error(`Reading ${uri} from ${targetServer}...`);
1186
+ const result = await client.readResource(uri);
1187
+ if (options.json) console.log(JSON.stringify(result, null, 2));
1188
+ else for (const content of result.contents) if ("text" in content) console.log(content.text);
1189
+ else console.log(JSON.stringify(content, null, 2));
1190
+ });
1191
+ } catch (error) {
1192
+ console.error(`Error executing read-resource: ${toErrorMessage$3(error)}`);
1193
+ process.exit(1);
1194
+ }
1195
+ });
1196
+
1197
+ //#endregion
1198
+ //#region src/commands/list-prompts.ts
1199
+ function toErrorMessage$2(error) {
1200
+ return error instanceof Error ? error.message : String(error);
1201
+ }
1202
+ const listPromptsCommand = new commander.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) => {
1203
+ try {
1204
+ await withConnectedCommandContext(options, async ({ clientManager }) => {
1205
+ const clients = options.server ? [clientManager.getClient(options.server)].filter((client) => client !== void 0) : clientManager.getAllClients();
1206
+ if (options.server && clients.length === 0) throw new Error(`Server "${options.server}" not found`);
1207
+ const promptsByServer = {};
1208
+ await Promise.all(clients.map(async (client) => {
1209
+ try {
1210
+ promptsByServer[client.serverName] = await client.listPrompts();
1211
+ } catch (error) {
1212
+ promptsByServer[client.serverName] = [];
1213
+ if (!options.json) console.error(`Failed to list prompts from ${client.serverName}: ${toErrorMessage$2(error)}`);
1214
+ }
1215
+ }));
1216
+ if (options.json) console.log(JSON.stringify(promptsByServer, null, 2));
1217
+ else for (const [serverName, prompts] of Object.entries(promptsByServer)) {
1218
+ console.log(`\n${serverName}:`);
1219
+ if (prompts.length === 0) {
1220
+ console.log(" No prompts available");
1221
+ continue;
1222
+ }
1223
+ for (const prompt of prompts) {
1224
+ console.log(` - ${prompt.name}: ${prompt.description || "No description"}`);
1225
+ if (prompt.arguments && prompt.arguments.length > 0) {
1226
+ const args = prompt.arguments.map((arg) => `${arg.name}${arg.required ? " (required)" : ""}`).join(", ");
1227
+ console.log(` args: ${args}`);
1228
+ }
1229
+ }
1230
+ }
1231
+ });
1232
+ } catch (error) {
1233
+ console.error(`Error executing list-prompts: ${toErrorMessage$2(error)}`);
1234
+ process.exit(1);
1235
+ }
1236
+ });
1237
+
1238
+ //#endregion
1239
+ //#region src/commands/get-prompt.ts
1240
+ function toErrorMessage$1(error) {
1241
+ return error instanceof Error ? error.message : String(error);
1242
+ }
1243
+ const getPromptCommand = new commander.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) => {
1244
+ try {
1245
+ let promptArgs = {};
1246
+ try {
1247
+ promptArgs = JSON.parse(options.args);
1248
+ } catch {
1249
+ throw new Error("Invalid JSON in --args");
1250
+ }
1251
+ await withConnectedCommandContext(options, async ({ clientManager }) => {
1252
+ const clients = clientManager.getAllClients();
1253
+ if (options.server) {
1254
+ const client$1 = clientManager.getClient(options.server);
1255
+ if (!client$1) throw new Error(`Server "${options.server}" not found`);
1256
+ const prompt$1 = await client$1.getPrompt(promptName, promptArgs);
1257
+ if (options.json) console.log(JSON.stringify(prompt$1, null, 2));
1258
+ else for (const message of prompt$1.messages) {
1259
+ const content = message.content;
1260
+ if (typeof content === "object" && content && "text" in content) console.log(content.text);
1261
+ else console.log(JSON.stringify(message, null, 2));
1262
+ }
1263
+ return;
1264
+ }
1265
+ const matchingServers = [];
1266
+ await Promise.all(clients.map(async (client$1) => {
1267
+ try {
1268
+ if ((await client$1.listPrompts()).some((prompt$1) => prompt$1.name === promptName)) matchingServers.push(client$1.serverName);
1269
+ } catch (error) {
1270
+ if (!options.json) console.error(`Failed to list prompts from ${client$1.serverName}: ${toErrorMessage$1(error)}`);
1271
+ }
1272
+ }));
1273
+ if (matchingServers.length === 0) throw new Error(`Prompt "${promptName}" not found on any connected server`);
1274
+ if (matchingServers.length > 1) throw new Error(`Prompt "${promptName}" found on multiple servers: ${matchingServers.join(", ")}. Use --server to disambiguate`);
1275
+ const client = clientManager.getClient(matchingServers[0]);
1276
+ if (!client) throw new Error(`Internal error: Server "${matchingServers[0]}" not connected`);
1277
+ const prompt = await client.getPrompt(promptName, promptArgs);
1278
+ if (options.json) console.log(JSON.stringify(prompt, null, 2));
1279
+ else for (const message of prompt.messages) {
1280
+ const content = message.content;
1281
+ if (typeof content === "object" && content && "text" in content) console.log(content.text);
1282
+ else console.log(JSON.stringify(message, null, 2));
1283
+ }
1284
+ });
1285
+ } catch (error) {
1286
+ console.error(`Error executing get-prompt: ${toErrorMessage$1(error)}`);
1287
+ process.exit(1);
1288
+ }
1289
+ });
1290
+
1291
+ //#endregion
1292
+ //#region src/commands/prefetch.ts
1293
+ /**
1294
+ * Prefetch Command
1295
+ *
1296
+ * DESIGN PATTERNS:
1297
+ * - Command pattern with Commander for CLI argument parsing
1298
+ * - Async/await pattern for asynchronous operations
1299
+ * - Error handling pattern with try-catch and proper exit codes
1300
+ *
1301
+ * CODING STANDARDS:
1302
+ * - Use async action handlers for asynchronous operations
1303
+ * - Provide clear option descriptions and default values
1304
+ * - Handle errors gracefully with process.exit()
1305
+ * - Log progress and errors to console
1306
+ * - Use Commander's .option() and .argument() for inputs
1307
+ *
1308
+ * AVOID:
1309
+ * - Synchronous blocking operations in action handlers
1310
+ * - Missing error handling (always use try-catch)
1311
+ * - Hardcoded values (use options or environment variables)
1312
+ * - Not exiting with appropriate exit codes on errors
1313
+ */
1314
+ /**
1315
+ * Pre-download packages used by MCP servers (npx, pnpx, uvx, uv)
1316
+ */
1317
+ const prefetchCommand = new commander.Command("prefetch").description("Pre-download packages used by MCP servers (npx, pnpx, uvx, uv)").option("-c, --config <path>", "Path to MCP server configuration file").option("-p, --parallel", "Run prefetch commands in parallel", false).option("-d, --dry-run", "Show what would be prefetched without executing", false).option("-f, --filter <type>", "Filter by package manager type: npx, pnpx, uvx, or uv").option("--definitions-out <path>", "Write discovered definitions to a JSON or YAML cache file").option("--skip-packages", "Skip package prefetch and only build definitions cache", false).option("--clear-definitions-cache", "Delete the definitions cache file before continuing", false).action(async (options) => {
1318
+ try {
1319
+ const container = require_src.createProxyIoCContainer();
1320
+ const configFilePath = options.config || require_src.findConfigFile();
1321
+ if (!configFilePath) {
1322
+ print.error("No MCP configuration file found.");
1323
+ print.info("Use --config <path> to specify a config file, or run \"mcp-proxy init\" to create one.");
1324
+ process.exit(1);
1325
+ }
1326
+ print.info(`Loading configuration from: ${configFilePath}`);
1327
+ const mcpConfig = await container.createConfigFetcherService({
1328
+ configFilePath,
1329
+ useCache: false
1330
+ }).fetchConfiguration(true);
1331
+ const serverId = mcpConfig.id || require_src.generateServerId();
1332
+ const configHash = require_src.DefinitionsCacheService.generateConfigHash(mcpConfig);
1333
+ const effectiveDefinitionsPath = options.definitionsOut || require_src.DefinitionsCacheService.getDefaultCachePath(configFilePath);
1334
+ const prefetchService = container.createPrefetchService({
1335
+ mcpConfig,
1336
+ filter: options.filter,
1337
+ parallel: options.parallel
1338
+ });
1339
+ const packages = prefetchService.extractPackages();
1340
+ const shouldPrefetchPackages = !options.skipPackages;
1341
+ const shouldWriteDefinitions = Boolean(options.definitionsOut);
1342
+ if (options.clearDefinitionsCache) if (options.dryRun) print.info(`Would clear definitions cache: ${effectiveDefinitionsPath}`);
1343
+ else {
1344
+ await require_src.DefinitionsCacheService.clearFile(effectiveDefinitionsPath);
1345
+ print.success(`Cleared definitions cache: ${effectiveDefinitionsPath}`);
1346
+ }
1347
+ if (shouldPrefetchPackages) if (packages.length === 0) {
1348
+ print.warning("No packages found to prefetch.");
1349
+ print.info("Prefetch supports: npx, pnpx, uvx, and uv run commands");
1350
+ } else {
1351
+ print.info(`Found ${packages.length} package(s) to prefetch:`);
1352
+ for (const pkg of packages) print.item(`${pkg.serverName}: ${pkg.packageManager} ${pkg.packageName}`);
1353
+ }
1354
+ if (!shouldPrefetchPackages && !shouldWriteDefinitions) {
1355
+ print.warning("Nothing to do. Use package prefetch or provide --definitions-out.");
1356
+ return;
1357
+ }
1358
+ if (options.dryRun) {
1359
+ if (shouldPrefetchPackages && packages.length > 0) {
1360
+ print.newline();
1361
+ print.header("Dry run mode - commands that would be executed:");
1362
+ for (const pkg of packages) print.indent(pkg.fullCommand.join(" "));
1363
+ }
1364
+ if (shouldWriteDefinitions) {
1365
+ print.newline();
1366
+ print.info(`Would write definitions cache to: ${effectiveDefinitionsPath}`);
1367
+ }
1368
+ return;
1369
+ }
1370
+ let packagePrefetchFailed = false;
1371
+ if (shouldPrefetchPackages && packages.length > 0) {
1372
+ print.newline();
1373
+ print.info("Prefetching packages...");
1374
+ const summary = await prefetchService.prefetch();
1375
+ print.newline();
1376
+ if (summary.failed === 0) print.success(`Package prefetch complete: ${summary.successful} succeeded, ${summary.failed} failed`);
1377
+ else print.warning(`Package prefetch complete: ${summary.successful} succeeded, ${summary.failed} failed`);
1378
+ if (summary.failed > 0) {
1379
+ packagePrefetchFailed = true;
1380
+ print.newline();
1381
+ print.error("Failed packages:");
1382
+ for (const result of summary.results.filter((r) => !r.success)) print.item(`${result.package.serverName} (${result.package.packageName}): ${result.output.trim()}`);
1383
+ }
1384
+ }
1385
+ if (shouldWriteDefinitions) {
1386
+ print.newline();
1387
+ print.info("Collecting definitions cache...");
1388
+ const clientManager = container.createClientManagerService();
1389
+ const skillPaths = mcpConfig.skills?.paths || [];
1390
+ const skillService = skillPaths.length > 0 ? container.createSkillService(process.cwd(), skillPaths) : void 0;
1391
+ const definitionsCacheService = container.createDefinitionsCacheService(clientManager, skillService);
1392
+ await Promise.all(Object.entries(mcpConfig.mcpServers).map(async ([serverName, serverConfig]) => {
1393
+ try {
1394
+ await clientManager.connectToServer(serverName, serverConfig);
1395
+ print.item(`Connected for definitions: ${serverName}`);
1396
+ } catch (error) {
1397
+ print.warning(`Failed to connect for definitions: ${serverName} (${error instanceof Error ? error.message : String(error)})`);
1398
+ }
1399
+ }));
1400
+ const definitionsCache = await definitionsCacheService.collectForCache({
1401
+ configPath: configFilePath,
1402
+ configHash,
1403
+ oneMcpVersion: require_src.version,
1404
+ serverId
1405
+ });
1406
+ await require_src.DefinitionsCacheService.writeToFile(effectiveDefinitionsPath, definitionsCache);
1407
+ print.success(`Definitions cache written: ${effectiveDefinitionsPath} (${Object.keys(definitionsCache.servers).length} servers, ${definitionsCache.skills.length} skills)`);
1408
+ if (definitionsCache.failures.length > 0) print.warning(`Definitions cache completed with ${definitionsCache.failures.length} server failure(s)`);
1409
+ await clientManager.disconnectAll();
1410
+ }
1411
+ if (packagePrefetchFailed) process.exit(1);
1412
+ } catch (error) {
1413
+ print.error(`Error executing prefetch: ${error instanceof Error ? error.message : String(error)}`);
1414
+ process.exit(1);
1415
+ }
1416
+ });
1417
+
1418
+ //#endregion
1419
+ //#region src/commands/stop.ts
1420
+ /**
1421
+ * Stop Command
1422
+ *
1423
+ * Stops a running HTTP mcp-proxy server using the authenticated admin endpoint
1424
+ * and the persisted runtime registry.
1425
+ */
1426
+ function toErrorMessage(error) {
1427
+ return error instanceof Error ? error.message : String(error);
1428
+ }
1429
+ function printStopResult(result) {
1430
+ console.log(`Stopped mcp-proxy server '${result.serverId}'.`);
1431
+ console.log(`Endpoint: http://${result.host}:${result.port}`);
1432
+ console.log(`Result: ${result.message}`);
1433
+ }
1434
+ /**
1435
+ * Stop a running HTTP mcp-proxy server.
1436
+ */
1437
+ const stopCommand = new commander.Command("stop").description("Stop a running HTTP mcp-proxy 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) => {
1438
+ try {
1439
+ if (options.config) console.error("Warning: --config is not used yet; runtime resolution uses the persisted registry.");
1440
+ const result = await require_src.createProxyIoCContainer().createStopServerService().stop({
1441
+ serverId: options.id,
1442
+ host: options.host,
1443
+ port: options.port,
1444
+ token: options.token,
1445
+ force: options.force,
1446
+ timeoutMs: options.timeout
1447
+ });
1448
+ if (options.json) {
1449
+ console.log(JSON.stringify(result, null, 2));
1450
+ return;
1451
+ }
1452
+ printStopResult(result);
1453
+ } catch (error) {
1454
+ const errorMessage = `Error executing stop: ${toErrorMessage(error)}`;
1455
+ if (options.json) console.log(JSON.stringify({
1456
+ ok: false,
1457
+ error: errorMessage
1458
+ }, null, 2));
1459
+ else console.error(errorMessage);
1460
+ process.exit(1);
1461
+ }
1462
+ });
1463
+
1464
+ //#endregion
1465
+ //#region src/cli.ts
1466
+ /**
1467
+ * MCP Server Entry Point
1468
+ *
1469
+ * DESIGN PATTERNS:
1470
+ * - CLI pattern with Commander for argument parsing
1471
+ * - Command pattern for organizing CLI commands
1472
+ * - Transport abstraction for multiple communication methods
1473
+ *
1474
+ * CODING STANDARDS:
1475
+ * - Use async/await for asynchronous operations
1476
+ * - Handle errors gracefully with try-catch
1477
+ * - Log important events for debugging
1478
+ * - Register all commands in main entry point
1479
+ *
1480
+ * AVOID:
1481
+ * - Hardcoding command logic in index.ts (use separate command files)
1482
+ * - Missing error handling for command execution
1483
+ */
1484
+ /**
1485
+ * Main entry point
1486
+ */
1487
+ async function main() {
1488
+ try {
1489
+ const program = new commander.Command();
1490
+ program.name("mcp-proxy").description("MCP proxy server package").version(require_src.version);
1491
+ program.addCommand(initCommand);
1492
+ program.addCommand(mcpServeCommand);
1493
+ program.addCommand(searchToolsCommand);
1494
+ program.addCommand(describeToolsCommand);
1495
+ program.addCommand(useToolCommand);
1496
+ program.addCommand(listResourcesCommand);
1497
+ program.addCommand(readResourceCommand);
1498
+ program.addCommand(listPromptsCommand);
1499
+ program.addCommand(getPromptCommand);
1500
+ program.addCommand(prefetchCommand);
1501
+ program.addCommand(stopCommand);
1502
+ await program.parseAsync(process.argv);
1503
+ } catch (error) {
1504
+ console.error(`CLI execution failed: ${error instanceof Error ? error.message : error}`);
1505
+ process.exit(1);
1506
+ }
1507
+ }
1508
+ main().catch((error) => {
1509
+ console.error(`Fatal error: ${error instanceof Error ? error.message : error}`);
1510
+ process.exit(1);
1511
+ });
1512
+
1513
+ //#endregion