@agimon-ai/mcp-proxy 0.8.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.cjs +1208 -1202
- package/dist/cli.mjs +1208 -1202
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +1 -18
- package/dist/index.d.mts +4 -21
- package/dist/index.mjs +1 -1
- package/dist/{src-Dorvm5bM.mjs → src-Dv7rJN0P.mjs} +631 -678
- package/dist/{src-dZuRf3Wt.cjs → src-ElP1ds81.cjs} +630 -677
- package/package.json +5 -5
package/dist/cli.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
const require_src = require("./src-
|
|
2
|
+
const require_src = require("./src-ElP1ds81.cjs");
|
|
3
3
|
let node_fs = require("node:fs");
|
|
4
4
|
let node_fs_promises = require("node:fs/promises");
|
|
5
5
|
let js_yaml = require("js-yaml");
|
|
@@ -10,107 +10,9 @@ node_path = require_src.__toESM(node_path);
|
|
|
10
10
|
let node_child_process = require("node:child_process");
|
|
11
11
|
let liquidjs = require("liquidjs");
|
|
12
12
|
let commander = require("commander");
|
|
13
|
-
let _agimon_ai_foundation_port_registry = require("@agimon-ai/foundation-port-registry");
|
|
14
|
-
let _agimon_ai_foundation_process_registry = require("@agimon-ai/foundation-process-registry");
|
|
15
13
|
let node_url = require("node:url");
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
//#endregion
|
|
19
|
-
//#region src/templates/mcp-config.yaml.liquid?raw
|
|
20
|
-
var mcp_config_yaml_default = "# MCP Server Configuration\n# This file configures the MCP servers that mcp-proxy will connect to\n#\n# Environment Variable Interpolation:\n# Use ${VAR_NAME} syntax to reference environment variables\n# Example: ${HOME}, ${API_KEY}, ${DATABASE_URL}\n#\n# Instructions:\n# - config.instruction: Server's default instruction (from server documentation)\n# - instruction: User override (optional, takes precedence over config.instruction)\n# - config.toolBlacklist: Array of tool names to hide/block from this server\n# - config.omitToolDescription: Boolean to show only tool names without descriptions (saves tokens)\n\n# Remote Configuration Sources (OPTIONAL)\n# Fetch and merge configurations from remote URLs\n# Remote configs are merged with local configs based on merge strategy\n#\n# SECURITY: SSRF Protection is ENABLED by default\n# - Only HTTPS URLs are allowed (set security.enforceHttps: false to allow HTTP)\n# - Private IPs and localhost are blocked (set security.allowPrivateIPs: true for internal networks)\n# - Blocked ranges: 127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16\nremoteConfigs:\n # Example 1: Basic remote config with default security\n # - url: ${AGIFLOW_URL}/api/v1/mcp-configs\n # headers:\n # Authorization: Bearer ${AGIFLOW_API_KEY}\n # mergeStrategy: local-priority # Options: local-priority (default), remote-priority, merge-deep\n #\n # Example 2: Remote config with custom security settings (for internal networks)\n # - url: ${INTERNAL_URL}/mcp-configs\n # headers:\n # Authorization: Bearer ${INTERNAL_TOKEN}\n # security:\n # allowPrivateIPs: true # Allow internal IPs (default: false)\n # enforceHttps: false # Allow HTTP (default: true, HTTPS only)\n # mergeStrategy: local-priority\n #\n # Example 3: Remote config with additional validation (OPTIONAL)\n # - url: ${AGIFLOW_URL}/api/v1/mcp-configs\n # headers:\n # Authorization: Bearer ${AGIFLOW_API_KEY}\n # X-API-Key: ${AGIFLOW_API_KEY}\n # security:\n # enforceHttps: true # Require HTTPS (default: true)\n # allowPrivateIPs: false # Block private IPs (default: false)\n # validation: # OPTIONAL: Additional regex validation on top of security checks\n # url: ^https://.*\\.agiflow\\.io/.* # OPTIONAL: Regex pattern to validate URL format\n # headers: # OPTIONAL: Regex patterns to validate header values\n # Authorization: ^Bearer [A-Za-z0-9_-]+$\n # X-API-Key: ^[A-Za-z0-9_-]{32,}$\n # mergeStrategy: local-priority\n\nmcpServers:\n{%- if mcpServers %}{% for server in mcpServers %}\n {{ server.name }}:\n command: {{ server.command }}\n args:{% for arg in server.args %}\n - '{{ arg }}'{% endfor %}\n # env:\n # LOG_LEVEL: info\n # # API_KEY: ${MY_API_KEY}\n # config:\n # instruction: Use this server for...\n # # toolBlacklist:\n # # - tool_to_block\n # # omitToolDescription: true\n{% endfor %}\n # Example MCP server using SSE transport\n # remote-server:\n # url: https://example.com/mcp\n # type: sse\n # headers:\n # Authorization: Bearer ${API_KEY}\n # config:\n # instruction: This server provides tools for...\n{% else %}\n # Example MCP server using stdio transport\n example-server:\n command: node\n args:\n - /path/to/mcp-server/build/index.js\n env:\n # Environment variables for the MCP server\n LOG_LEVEL: info\n # You can use environment variable interpolation:\n # DATABASE_URL: ${DATABASE_URL}\n # API_KEY: ${MY_API_KEY}\n config:\n # Server's default instruction (from server documentation)\n instruction: Use this server for...\n # Optional: Block specific tools from being listed or executed\n # toolBlacklist:\n # - dangerous_tool_name\n # - another_blocked_tool\n # Optional: Omit tool descriptions to save tokens (default: false)\n # omitToolDescription: true\n # instruction: Optional user override - takes precedence over config.instruction\n\n # Example MCP server using SSE transport with environment variables\n # remote-server:\n # url: https://example.com/mcp\n # type: sse\n # headers:\n # # Use ${VAR_NAME} to interpolate environment variables\n # Authorization: Bearer ${API_KEY}\n # config:\n # instruction: This server provides tools for...\n # # Optional: Block specific tools from being listed or executed\n # # toolBlacklist:\n # # - tool_to_block\n # # Optional: Omit tool descriptions to save tokens (default: false)\n # # omitToolDescription: true\n # # instruction: Optional user override\n{% endif %}\n";
|
|
21
|
-
//#endregion
|
|
22
|
-
//#region src/utils/output.ts
|
|
23
|
-
function writeLine(message = "") {
|
|
24
|
-
console.log(message);
|
|
25
|
-
}
|
|
26
|
-
function writeError(message, detail) {
|
|
27
|
-
if (detail) console.error(`${message} ${detail}`);
|
|
28
|
-
else console.error(message);
|
|
29
|
-
}
|
|
30
|
-
const log = {
|
|
31
|
-
info: (message) => writeLine(message),
|
|
32
|
-
error: (message, detail) => writeError(message, detail)
|
|
33
|
-
};
|
|
34
|
-
const print = {
|
|
35
|
-
info: (message) => writeLine(message),
|
|
36
|
-
warning: (message) => writeLine(`Warning: ${message}`),
|
|
37
|
-
error: (message) => writeError(message),
|
|
38
|
-
success: (message) => writeLine(message),
|
|
39
|
-
newline: () => writeLine(),
|
|
40
|
-
header: (message) => writeLine(message),
|
|
41
|
-
item: (message) => writeLine(`- ${message}`),
|
|
42
|
-
indent: (message) => writeLine(` ${message}`)
|
|
43
|
-
};
|
|
44
|
-
//#endregion
|
|
45
|
-
//#region src/commands/init.ts
|
|
46
|
-
/**
|
|
47
|
-
* Init Command
|
|
48
|
-
*
|
|
49
|
-
* DESIGN PATTERNS:
|
|
50
|
-
* - Command pattern with Commander for CLI argument parsing
|
|
51
|
-
* - Async/await pattern for asynchronous operations
|
|
52
|
-
* - Error handling pattern with try-catch and proper exit codes
|
|
53
|
-
*
|
|
54
|
-
* CODING STANDARDS:
|
|
55
|
-
* - Use async action handlers for asynchronous operations
|
|
56
|
-
* - Provide clear option descriptions and default values
|
|
57
|
-
* - Handle errors gracefully with process.exit()
|
|
58
|
-
* - Log progress and errors to console
|
|
59
|
-
* - Use Commander's .option() and .argument() for inputs
|
|
60
|
-
*
|
|
61
|
-
* AVOID:
|
|
62
|
-
* - Synchronous blocking operations in action handlers
|
|
63
|
-
* - Missing error handling (always use try-catch)
|
|
64
|
-
* - Hardcoded values (use options or environment variables)
|
|
65
|
-
* - Not exiting with appropriate exit codes on errors
|
|
66
|
-
*/
|
|
67
|
-
/**
|
|
68
|
-
* Initialize MCP configuration file
|
|
69
|
-
*/
|
|
70
|
-
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) => {
|
|
71
|
-
try {
|
|
72
|
-
const outputPath = (0, node_path.resolve)(options.output);
|
|
73
|
-
const isYaml = !options.json && (outputPath.endsWith(".yaml") || outputPath.endsWith(".yml"));
|
|
74
|
-
let content;
|
|
75
|
-
if (isYaml) {
|
|
76
|
-
const liquid = new liquidjs.Liquid();
|
|
77
|
-
let mcpServersData = null;
|
|
78
|
-
if (options.mcpServers) try {
|
|
79
|
-
const serversObj = JSON.parse(options.mcpServers);
|
|
80
|
-
mcpServersData = Object.entries(serversObj).map(([name, config]) => ({
|
|
81
|
-
name,
|
|
82
|
-
command: config.command,
|
|
83
|
-
args: config.args
|
|
84
|
-
}));
|
|
85
|
-
} catch (parseError) {
|
|
86
|
-
log.error("Failed to parse --mcp-servers JSON:", parseError instanceof Error ? parseError.message : String(parseError));
|
|
87
|
-
process.exit(1);
|
|
88
|
-
}
|
|
89
|
-
content = await liquid.parseAndRender(mcp_config_yaml_default, { mcpServers: mcpServersData });
|
|
90
|
-
} else content = mcp_config_default;
|
|
91
|
-
try {
|
|
92
|
-
await (0, node_fs_promises.writeFile)(outputPath, content, {
|
|
93
|
-
encoding: "utf-8",
|
|
94
|
-
flag: options.force ? "w" : "wx"
|
|
95
|
-
});
|
|
96
|
-
} catch (error) {
|
|
97
|
-
if (error && typeof error === "object" && "code" in error && error.code === "EEXIST") {
|
|
98
|
-
log.error(`Config file already exists: ${outputPath}`);
|
|
99
|
-
log.info("Use --force to overwrite");
|
|
100
|
-
process.exit(1);
|
|
101
|
-
}
|
|
102
|
-
throw error;
|
|
103
|
-
}
|
|
104
|
-
log.info(`MCP configuration file created: ${outputPath}`);
|
|
105
|
-
log.info("Next steps:");
|
|
106
|
-
log.info("1. Edit the configuration file to add your MCP servers");
|
|
107
|
-
log.info(`2. Run: mcp-proxy mcp-serve --config ${outputPath}`);
|
|
108
|
-
} catch (error) {
|
|
109
|
-
log.error("Error executing init:", error instanceof Error ? error.message : String(error));
|
|
110
|
-
process.exit(1);
|
|
111
|
-
}
|
|
112
|
-
});
|
|
113
|
-
//#endregion
|
|
14
|
+
let _agimon_ai_foundation_process_registry = require("@agimon-ai/foundation-process-registry");
|
|
15
|
+
let _agimon_ai_foundation_port_registry = require("@agimon-ai/foundation-port-registry");
|
|
114
16
|
//#region src/commands/prestart-http.ts
|
|
115
17
|
/**
|
|
116
18
|
* Prestart HTTP Command
|
|
@@ -136,36 +38,35 @@ function resolveWorkspaceRoot(startPath = process.env.PROJECT_PATH || process.cw
|
|
|
136
38
|
}
|
|
137
39
|
}
|
|
138
40
|
const PROCESS_REGISTRY_SERVICE_HTTP$1 = "mcp-proxy-http";
|
|
139
|
-
async function
|
|
41
|
+
async function findExistingRuntime(workspaceRoot) {
|
|
140
42
|
const match = (await new _agimon_ai_foundation_process_registry.ProcessRegistryService(process.env.PROCESS_REGISTRY_PATH).listProcesses({
|
|
141
43
|
repositoryPath: workspaceRoot,
|
|
142
44
|
serviceName: PROCESS_REGISTRY_SERVICE_HTTP$1
|
|
143
45
|
}))[0];
|
|
144
46
|
if (!match?.host || !match?.port) return null;
|
|
47
|
+
const metadata = match.metadata;
|
|
48
|
+
return {
|
|
49
|
+
host: match.host,
|
|
50
|
+
port: match.port,
|
|
51
|
+
serverId: metadata?.serverId ?? "unknown"
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
async function isRuntimeHealthy(host, port) {
|
|
145
55
|
try {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
host: match.host,
|
|
151
|
-
port: match.port,
|
|
152
|
-
serverId: metadata?.serverId ?? "unknown",
|
|
153
|
-
workspaceRoot,
|
|
154
|
-
reusedExistingRuntime: true
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
} catch {}
|
|
158
|
-
return null;
|
|
56
|
+
return (await fetch(`http://${host}:${port}/health`)).ok;
|
|
57
|
+
} catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
159
60
|
}
|
|
160
61
|
function buildCliCandidates() {
|
|
161
|
-
const
|
|
162
|
-
const
|
|
62
|
+
const currentFile = (0, node_url.fileURLToPath)(require("url").pathToFileURL(__filename).href);
|
|
63
|
+
const currentDir = node_path.default.dirname(currentFile);
|
|
163
64
|
const distCandidates = [
|
|
164
|
-
node_path.default.resolve(
|
|
165
|
-
node_path.default.resolve(
|
|
166
|
-
node_path.default.resolve(
|
|
65
|
+
node_path.default.resolve(currentDir, "cli.mjs"),
|
|
66
|
+
node_path.default.resolve(currentDir, "..", "dist", "cli.mjs"),
|
|
67
|
+
node_path.default.resolve(currentDir, "..", "..", "dist", "cli.mjs")
|
|
167
68
|
];
|
|
168
|
-
const srcCandidates = [node_path.default.resolve(
|
|
69
|
+
const srcCandidates = [node_path.default.resolve(currentDir, "..", "cli.ts"), node_path.default.resolve(currentDir, "..", "..", "src", "cli.ts")];
|
|
169
70
|
for (const candidate of distCandidates) if ((0, node_fs.existsSync)(candidate)) return {
|
|
170
71
|
command: process.execPath,
|
|
171
72
|
args: [candidate]
|
|
@@ -254,9 +155,16 @@ async function prestartHttpRuntime(options) {
|
|
|
254
155
|
const timeoutMs = parseTimeoutMs(options.timeoutMs);
|
|
255
156
|
const registryPath = options.registryPath || options.registryDir;
|
|
256
157
|
const workspaceRoot = resolveWorkspaceRoot();
|
|
257
|
-
const existing = await
|
|
258
|
-
if (existing) return
|
|
259
|
-
|
|
158
|
+
const existing = await findExistingRuntime(workspaceRoot);
|
|
159
|
+
if (existing && await isRuntimeHealthy(existing.host, existing.port)) return {
|
|
160
|
+
host: existing.host,
|
|
161
|
+
port: existing.port,
|
|
162
|
+
serverId: existing.serverId,
|
|
163
|
+
workspaceRoot,
|
|
164
|
+
reusedExistingRuntime: true
|
|
165
|
+
};
|
|
166
|
+
const targetPort = options.port ?? existing?.port;
|
|
167
|
+
await stopExistingRuntime(new require_src.RuntimeStateService(), options.id, options.host, targetPort);
|
|
260
168
|
const childEnv = {
|
|
261
169
|
...process.env,
|
|
262
170
|
...registryPath ? {
|
|
@@ -272,7 +180,7 @@ async function prestartHttpRuntime(options) {
|
|
|
272
180
|
serverId,
|
|
273
181
|
"--host",
|
|
274
182
|
options.host || DEFAULT_HOST$1,
|
|
275
|
-
...
|
|
183
|
+
...targetPort !== void 0 ? ["--port", String(targetPort)] : [],
|
|
276
184
|
...options.config ? ["--config", options.config] : [],
|
|
277
185
|
...options.cache === false ? ["--no-cache"] : [],
|
|
278
186
|
...options.definitionsCache ? ["--definitions-cache", options.definitionsCache] : [],
|
|
@@ -312,1188 +220,1074 @@ const prestartHttpCommand = new commander.Command("prestart-http").description("
|
|
|
312
220
|
}
|
|
313
221
|
});
|
|
314
222
|
//#endregion
|
|
315
|
-
//#region src/commands/
|
|
316
|
-
/**
|
|
317
|
-
* MCP Serve Command
|
|
318
|
-
*
|
|
319
|
-
* DESIGN PATTERNS:
|
|
320
|
-
* - Command pattern with Commander for CLI argument parsing
|
|
321
|
-
* - Transport abstraction pattern for flexible deployment (stdio, HTTP, SSE)
|
|
322
|
-
* - Factory pattern for creating transport handlers
|
|
323
|
-
* - Graceful shutdown pattern with signal handling
|
|
324
|
-
*
|
|
325
|
-
* CODING STANDARDS:
|
|
326
|
-
* - Use async/await for asynchronous operations
|
|
327
|
-
* - Implement proper error handling with try-catch blocks
|
|
328
|
-
* - Handle process signals for graceful shutdown
|
|
329
|
-
* - Provide clear CLI options and help messages
|
|
330
|
-
*
|
|
331
|
-
* AVOID:
|
|
332
|
-
* - Hardcoded configuration values (use CLI options or environment variables)
|
|
333
|
-
* - Missing error handling for transport startup
|
|
334
|
-
* - Not cleaning up resources on shutdown
|
|
335
|
-
*/
|
|
336
|
-
const CONFIG_FILE_NAMES = [
|
|
337
|
-
"mcp-config.yaml",
|
|
338
|
-
"mcp-config.yml",
|
|
339
|
-
"mcp-config.json"
|
|
340
|
-
];
|
|
341
|
-
const MCP_ENDPOINT_PATH = "/mcp";
|
|
342
|
-
const DEFAULT_HOST = "localhost";
|
|
343
|
-
const TRANSPORT_TYPE_STDIO = "stdio";
|
|
344
|
-
const TRANSPORT_TYPE_HTTP = "http";
|
|
345
|
-
const TRANSPORT_TYPE_SSE = "sse";
|
|
346
|
-
const TRANSPORT_TYPE_STDIO_HTTP = "stdio-http";
|
|
347
|
-
const RUNTIME_TRANSPORT = TRANSPORT_TYPE_HTTP;
|
|
348
|
-
const PORT_REGISTRY_SERVICE_HTTP = "mcp-proxy-http";
|
|
349
|
-
const PORT_REGISTRY_SERVICE_TYPE = "service";
|
|
350
|
-
function getWorkspaceRoot() {
|
|
351
|
-
return resolveWorkspaceRoot();
|
|
352
|
-
}
|
|
353
|
-
const PROCESS_REGISTRY_SERVICE_HTTP = "mcp-proxy-http";
|
|
354
|
-
const PROCESS_REGISTRY_SERVICE_TYPE = "service";
|
|
355
|
-
function getRegistryRepositoryPath() {
|
|
356
|
-
return getWorkspaceRoot();
|
|
357
|
-
}
|
|
223
|
+
//#region src/commands/bootstrap.ts
|
|
358
224
|
function toErrorMessage$9(error) {
|
|
359
225
|
return error instanceof Error ? error.message : String(error);
|
|
360
226
|
}
|
|
361
|
-
function
|
|
362
|
-
return type === TRANSPORT_TYPE_STDIO || type === TRANSPORT_TYPE_HTTP || type === TRANSPORT_TYPE_SSE || type === TRANSPORT_TYPE_STDIO_HTTP;
|
|
363
|
-
}
|
|
364
|
-
function isValidProxyMode(mode) {
|
|
365
|
-
return mode === "meta" || mode === "flat" || mode === "search";
|
|
366
|
-
}
|
|
367
|
-
async function pathExists(filePath) {
|
|
227
|
+
async function checkHealth(host, port) {
|
|
368
228
|
try {
|
|
369
|
-
await (
|
|
370
|
-
return true;
|
|
229
|
+
return (await fetch(`http://${host}:${port}/health`, { signal: AbortSignal.timeout(3e3) })).ok;
|
|
371
230
|
} catch {
|
|
372
231
|
return false;
|
|
373
232
|
}
|
|
374
233
|
}
|
|
375
|
-
|
|
234
|
+
/**
|
|
235
|
+
* Proxy mode: connect to a running HTTP server instead of downstream servers directly.
|
|
236
|
+
* Auto-starts the server if not running.
|
|
237
|
+
*/
|
|
238
|
+
async function withProxiedContext(container, config, configFilePath, options, run) {
|
|
239
|
+
const host = config.proxy?.host ?? "localhost";
|
|
240
|
+
const port = config.proxy?.port;
|
|
241
|
+
const endpoint = `http://${host}:${port}/mcp`;
|
|
242
|
+
if (!await checkHealth(host, port)) {
|
|
243
|
+
if (!options.json) console.error("Starting HTTP proxy server in background...");
|
|
244
|
+
await prestartHttpRuntime({
|
|
245
|
+
host,
|
|
246
|
+
port,
|
|
247
|
+
config: configFilePath,
|
|
248
|
+
cache: options.useCache !== false,
|
|
249
|
+
clearDefinitionsCache: false,
|
|
250
|
+
proxyMode: "flat"
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
const clientManager = container.createClientManagerService();
|
|
376
254
|
try {
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
let searchDir = process.cwd();
|
|
384
|
-
for (let level = 0; level <= MAX_PARENT_LEVELS; level++) {
|
|
385
|
-
for (const fileName of CONFIG_FILE_NAMES) {
|
|
386
|
-
const configPath = (0, node_path.join)(searchDir, fileName);
|
|
387
|
-
if (await pathExists(configPath)) return configPath;
|
|
388
|
-
}
|
|
389
|
-
const parentDir = (0, node_path.dirname)(searchDir);
|
|
390
|
-
if (parentDir === searchDir) break;
|
|
391
|
-
searchDir = parentDir;
|
|
392
|
-
}
|
|
393
|
-
return null;
|
|
255
|
+
await clientManager.connectToServer("proxy", {
|
|
256
|
+
name: "proxy",
|
|
257
|
+
transport: "http",
|
|
258
|
+
config: { url: endpoint }
|
|
259
|
+
});
|
|
260
|
+
if (!options.json) console.error(`✓ Connected to proxy at ${endpoint}`);
|
|
394
261
|
} catch (error) {
|
|
395
|
-
throw new Error(`Failed to
|
|
262
|
+
throw new Error(`Failed to connect to proxy server at ${endpoint}: ${toErrorMessage$9(error)}`);
|
|
263
|
+
}
|
|
264
|
+
try {
|
|
265
|
+
return await run({
|
|
266
|
+
container,
|
|
267
|
+
configFilePath,
|
|
268
|
+
config,
|
|
269
|
+
clientManager
|
|
270
|
+
});
|
|
271
|
+
} finally {
|
|
272
|
+
await clientManager.disconnectAll();
|
|
396
273
|
}
|
|
397
274
|
}
|
|
398
|
-
|
|
275
|
+
/**
|
|
276
|
+
* Direct mode: connect to all downstream MCP servers individually.
|
|
277
|
+
*/
|
|
278
|
+
async function withDirectContext(container, config, configFilePath, options, run) {
|
|
279
|
+
const clientManager = container.createClientManagerService();
|
|
280
|
+
await Promise.all(Object.entries(config.mcpServers).map(async ([serverName, serverConfig]) => {
|
|
281
|
+
try {
|
|
282
|
+
await clientManager.connectToServer(serverName, serverConfig);
|
|
283
|
+
if (!options.json) console.error(`✓ Connected to ${serverName}`);
|
|
284
|
+
} catch (error) {
|
|
285
|
+
if (!options.json) console.error(`✗ Failed to connect to ${serverName}: ${toErrorMessage$9(error)}`);
|
|
286
|
+
}
|
|
287
|
+
}));
|
|
288
|
+
if (clientManager.getAllClients().length === 0) throw new Error("No MCP servers connected");
|
|
399
289
|
try {
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
keepAlive: typeof p.keepAlive === "boolean" ? p.keepAlive : void 0
|
|
409
|
-
};
|
|
410
|
-
} catch {
|
|
411
|
-
return {};
|
|
290
|
+
return await run({
|
|
291
|
+
container,
|
|
292
|
+
configFilePath,
|
|
293
|
+
config,
|
|
294
|
+
clientManager
|
|
295
|
+
});
|
|
296
|
+
} finally {
|
|
297
|
+
await clientManager.disconnectAll();
|
|
412
298
|
}
|
|
413
299
|
}
|
|
414
|
-
async function
|
|
300
|
+
async function withConnectedCommandContext(options, run) {
|
|
415
301
|
const container = require_src.createProxyIoCContainer();
|
|
416
|
-
|
|
417
|
-
if (
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
throw new Error(`Failed to resolve server ID from config '${resolvedConfigPath}': ${toErrorMessage$9(error)}`);
|
|
425
|
-
}
|
|
426
|
-
return require_src.generateServerId();
|
|
427
|
-
}
|
|
428
|
-
function validateTransportType(type) {
|
|
429
|
-
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}`);
|
|
430
|
-
return type;
|
|
431
|
-
}
|
|
432
|
-
function validateProxyMode(mode) {
|
|
433
|
-
if (!isValidProxyMode(mode)) throw new Error(`Unknown proxy mode: '${mode}'. Valid options: meta, flat, search`);
|
|
434
|
-
}
|
|
435
|
-
function createTransportConfig(options, mode, proxyDefaults) {
|
|
436
|
-
const envPort = process.env.MCP_PORT ? Number(process.env.MCP_PORT) : void 0;
|
|
437
|
-
return {
|
|
438
|
-
mode,
|
|
439
|
-
port: options.port ?? (Number.isFinite(envPort) ? envPort : void 0) ?? proxyDefaults?.port,
|
|
440
|
-
host: options.host ?? process.env.MCP_HOST ?? proxyDefaults?.host ?? DEFAULT_HOST
|
|
441
|
-
};
|
|
442
|
-
}
|
|
443
|
-
function createStdioSafeLogger() {
|
|
444
|
-
const logToStderr = (message, data) => {
|
|
445
|
-
if (data === void 0) {
|
|
446
|
-
console.error(message);
|
|
447
|
-
return;
|
|
448
|
-
}
|
|
449
|
-
console.error(message, data);
|
|
450
|
-
};
|
|
451
|
-
return {
|
|
452
|
-
trace: logToStderr,
|
|
453
|
-
debug: logToStderr,
|
|
454
|
-
info: logToStderr,
|
|
455
|
-
warn: logToStderr,
|
|
456
|
-
error: logToStderr
|
|
457
|
-
};
|
|
458
|
-
}
|
|
459
|
-
function createServerOptions(options, resolvedConfigPath, serverId) {
|
|
460
|
-
return {
|
|
461
|
-
configFilePath: resolvedConfigPath,
|
|
462
|
-
noCache: options.cache === false,
|
|
463
|
-
serverId,
|
|
464
|
-
definitionsCachePath: options.definitionsCache,
|
|
465
|
-
clearDefinitionsCache: options.clearDefinitionsCache,
|
|
466
|
-
proxyMode: options.proxyMode
|
|
467
|
-
};
|
|
468
|
-
}
|
|
469
|
-
function formatStartError(type, host, port, error) {
|
|
470
|
-
const startErrorMessage = toErrorMessage$9(error);
|
|
471
|
-
if (type === TRANSPORT_TYPE_STDIO) return `Failed to start MCP server with transport '${type}': ${startErrorMessage}`;
|
|
472
|
-
return `Failed to start MCP server with transport '${type}' on ${port === void 0 ? `${host} (dynamic port)` : `${host}:${port}`}: ${startErrorMessage}`;
|
|
473
|
-
}
|
|
474
|
-
function createRuntimeRecord(serverId, config, port, shutdownToken, configPath) {
|
|
475
|
-
return {
|
|
476
|
-
serverId,
|
|
477
|
-
host: config.host ?? DEFAULT_HOST,
|
|
478
|
-
port,
|
|
479
|
-
transport: RUNTIME_TRANSPORT,
|
|
480
|
-
shutdownToken,
|
|
481
|
-
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
482
|
-
pid: process.pid,
|
|
483
|
-
configPath
|
|
484
|
-
};
|
|
485
|
-
}
|
|
486
|
-
function createPortRegistryService() {
|
|
487
|
-
return new _agimon_ai_foundation_port_registry.PortRegistryService(process.env.PORT_REGISTRY_PATH);
|
|
488
|
-
}
|
|
489
|
-
function getRegistryEnvironment() {
|
|
490
|
-
return process.env.NODE_ENV ?? "development";
|
|
491
|
-
}
|
|
492
|
-
async function createPortRegistryLease(serviceName, host, preferredPort, serverId, transport, configPath, portRange = preferredPort !== void 0 ? {
|
|
493
|
-
min: preferredPort,
|
|
494
|
-
max: preferredPort
|
|
495
|
-
} : _agimon_ai_foundation_port_registry.DEFAULT_PORT_RANGE) {
|
|
496
|
-
const portRegistry = createPortRegistryService();
|
|
497
|
-
const result = await portRegistry.reservePort({
|
|
498
|
-
repositoryPath: getRegistryRepositoryPath(),
|
|
499
|
-
serviceName,
|
|
500
|
-
serviceType: PORT_REGISTRY_SERVICE_TYPE,
|
|
501
|
-
environment: getRegistryEnvironment(),
|
|
502
|
-
pid: process.pid,
|
|
503
|
-
host,
|
|
504
|
-
preferredPort,
|
|
505
|
-
portRange,
|
|
506
|
-
force: true,
|
|
507
|
-
metadata: {
|
|
508
|
-
transport,
|
|
509
|
-
serverId,
|
|
510
|
-
...configPath ? { configPath } : {}
|
|
511
|
-
}
|
|
512
|
-
});
|
|
513
|
-
if (!result.success || !result.record) {
|
|
514
|
-
const requestedPortLabel = preferredPort === void 0 ? "dynamic port" : `port ${preferredPort}`;
|
|
515
|
-
throw new Error(result.error || `Failed to reserve ${requestedPortLabel} in port registry`);
|
|
516
|
-
}
|
|
517
|
-
let released = false;
|
|
518
|
-
return {
|
|
519
|
-
port: result.record.port,
|
|
520
|
-
release: async () => {
|
|
521
|
-
if (released) return;
|
|
522
|
-
released = true;
|
|
523
|
-
const releaseResult = await portRegistry.releasePort({
|
|
524
|
-
repositoryPath: getRegistryRepositoryPath(),
|
|
525
|
-
serviceName,
|
|
526
|
-
serviceType: PORT_REGISTRY_SERVICE_TYPE,
|
|
527
|
-
pid: process.pid,
|
|
528
|
-
environment: getRegistryEnvironment(),
|
|
529
|
-
force: true
|
|
530
|
-
});
|
|
531
|
-
if (!releaseResult.success && releaseResult.error && !releaseResult.error.includes("No matching registry entry")) throw new Error(releaseResult.error || `Failed to release port for ${serviceName}`);
|
|
532
|
-
}
|
|
533
|
-
};
|
|
534
|
-
}
|
|
535
|
-
async function releasePortLease(lease) {
|
|
536
|
-
if (!lease) return;
|
|
537
|
-
await lease.release();
|
|
538
|
-
}
|
|
539
|
-
function createHttpAdminOptions(serverId, shutdownToken, onShutdownRequested) {
|
|
540
|
-
return {
|
|
541
|
-
serverId,
|
|
542
|
-
shutdownToken,
|
|
543
|
-
onShutdownRequested
|
|
544
|
-
};
|
|
545
|
-
}
|
|
546
|
-
async function removeRuntimeRecord(runtimeStateService, serverId) {
|
|
547
|
-
try {
|
|
548
|
-
await runtimeStateService.remove(serverId);
|
|
549
|
-
} catch (error) {
|
|
550
|
-
throw new Error(`Failed to remove runtime state for '${serverId}': ${toErrorMessage$9(error)}`);
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
async function writeRuntimeRecord(runtimeStateService, record) {
|
|
554
|
-
try {
|
|
555
|
-
await runtimeStateService.write(record);
|
|
556
|
-
} catch (error) {
|
|
557
|
-
throw new Error(`Failed to persist runtime state for '${record.serverId}': ${toErrorMessage$9(error)}`);
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
async function stopOwnedHttpTransport(handler, runtimeStateService, serverId, processLease) {
|
|
561
|
-
try {
|
|
562
|
-
try {
|
|
563
|
-
await handler.stop();
|
|
564
|
-
} catch (error) {
|
|
565
|
-
throw new Error(`Failed to stop owned HTTP transport '${serverId}': ${toErrorMessage$9(error)}`);
|
|
566
|
-
}
|
|
567
|
-
} finally {
|
|
568
|
-
await processLease?.release({ kill: false });
|
|
569
|
-
await removeRuntimeRecord(runtimeStateService, serverId);
|
|
570
|
-
}
|
|
302
|
+
const configFilePath = options.config || require_src.findConfigFile();
|
|
303
|
+
if (!configFilePath) throw new Error("No config file found. Use --config or create mcp-config.yaml");
|
|
304
|
+
const config = await container.createConfigFetcherService({
|
|
305
|
+
configFilePath,
|
|
306
|
+
useCache: options.useCache
|
|
307
|
+
}).fetchConfiguration();
|
|
308
|
+
if (config.proxy?.port) return await withProxiedContext(container, config, configFilePath, options, run);
|
|
309
|
+
return await withDirectContext(container, config, configFilePath, options, run);
|
|
571
310
|
}
|
|
311
|
+
//#endregion
|
|
312
|
+
//#region src/commands/describe-tools.ts
|
|
572
313
|
/**
|
|
573
|
-
*
|
|
574
|
-
*
|
|
575
|
-
*
|
|
314
|
+
* Describe Tools Command
|
|
315
|
+
*
|
|
316
|
+
* DESIGN PATTERNS:
|
|
317
|
+
* - Command pattern with Commander for CLI argument parsing
|
|
318
|
+
* - Async/await pattern for asynchronous operations
|
|
319
|
+
* - Error handling pattern with try-catch and proper exit codes
|
|
320
|
+
*
|
|
321
|
+
* CODING STANDARDS:
|
|
322
|
+
* - Use async action handlers for asynchronous operations
|
|
323
|
+
* - Provide clear option descriptions and default values
|
|
324
|
+
* - Handle errors gracefully with process.exit()
|
|
325
|
+
* - Log progress and errors to console
|
|
326
|
+
* - Use Commander's .option() and .argument() for inputs
|
|
327
|
+
*
|
|
328
|
+
* AVOID:
|
|
329
|
+
* - Synchronous blocking operations in action handlers
|
|
330
|
+
* - Missing error handling (always use try-catch)
|
|
331
|
+
* - Hardcoded values (use options or environment variables)
|
|
332
|
+
* - Not exiting with appropriate exit codes on errors
|
|
576
333
|
*/
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
await removeRuntimeRecord(runtimeStateService, serverId);
|
|
580
|
-
}
|
|
581
|
-
async function cleanupFailedRuntimeStartup(handler, runtimeStateService, serverId, processLease) {
|
|
582
|
-
try {
|
|
583
|
-
try {
|
|
584
|
-
await handler.stop();
|
|
585
|
-
} catch (error) {
|
|
586
|
-
throw new Error(`Failed to stop HTTP transport during cleanup for '${serverId}': ${toErrorMessage$9(error)}`);
|
|
587
|
-
}
|
|
588
|
-
} finally {
|
|
589
|
-
await processLease?.release({ kill: false });
|
|
590
|
-
await removeRuntimeRecord(runtimeStateService, serverId);
|
|
591
|
-
}
|
|
334
|
+
function toErrorMessage$8(error) {
|
|
335
|
+
return error instanceof Error ? error.message : String(error);
|
|
592
336
|
}
|
|
593
337
|
/**
|
|
594
|
-
*
|
|
595
|
-
* @param handler - The transport handler to start
|
|
596
|
-
* @param onStopped - Optional cleanup callback run after signal-based shutdown
|
|
338
|
+
* Describe specific MCP tools
|
|
597
339
|
*/
|
|
598
|
-
|
|
340
|
+
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) => {
|
|
599
341
|
try {
|
|
600
|
-
await
|
|
342
|
+
await withConnectedCommandContext(options, async ({ container, config, clientManager }) => {
|
|
343
|
+
const clients = options.server ? [clientManager.getClient(options.server)].filter((client) => client !== void 0) : clientManager.getAllClients();
|
|
344
|
+
if (options.server && clients.length === 0) throw new Error(`Server "${options.server}" not found`);
|
|
345
|
+
const cwd = process.env.PROJECT_PATH || process.cwd();
|
|
346
|
+
const skillPaths = config.skills?.paths || [];
|
|
347
|
+
const skillService = skillPaths.length > 0 ? container.createSkillService(cwd, skillPaths) : void 0;
|
|
348
|
+
const foundTools = [];
|
|
349
|
+
const foundSkills = [];
|
|
350
|
+
const notFoundTools = [...toolNames];
|
|
351
|
+
const toolResults = await Promise.all(clients.map(async (client) => {
|
|
352
|
+
try {
|
|
353
|
+
return {
|
|
354
|
+
client,
|
|
355
|
+
tools: await client.listTools(),
|
|
356
|
+
error: null
|
|
357
|
+
};
|
|
358
|
+
} catch (error) {
|
|
359
|
+
return {
|
|
360
|
+
client,
|
|
361
|
+
tools: [],
|
|
362
|
+
error
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
}));
|
|
366
|
+
for (const { client, tools, error } of toolResults) {
|
|
367
|
+
if (error) {
|
|
368
|
+
if (!options.json) console.error(`Failed to list tools from ${client.serverName}:`, error);
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
for (const toolName of toolNames) {
|
|
372
|
+
const tool = tools.find((entry) => entry.name === toolName);
|
|
373
|
+
if (tool) {
|
|
374
|
+
foundTools.push({
|
|
375
|
+
server: client.serverName,
|
|
376
|
+
name: tool.name,
|
|
377
|
+
description: tool.description,
|
|
378
|
+
inputSchema: tool.inputSchema
|
|
379
|
+
});
|
|
380
|
+
const idx = notFoundTools.indexOf(toolName);
|
|
381
|
+
if (idx > -1) notFoundTools.splice(idx, 1);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
if (skillService && notFoundTools.length > 0) {
|
|
386
|
+
const skillResults = await Promise.all([...notFoundTools].map(async (toolName) => {
|
|
387
|
+
const skillName = toolName.startsWith("skill__") ? toolName.slice(7) : toolName;
|
|
388
|
+
return {
|
|
389
|
+
toolName,
|
|
390
|
+
skill: await skillService.getSkill(skillName)
|
|
391
|
+
};
|
|
392
|
+
}));
|
|
393
|
+
for (const { toolName, skill } of skillResults) if (skill) {
|
|
394
|
+
foundSkills.push({
|
|
395
|
+
name: skill.name,
|
|
396
|
+
location: skill.basePath,
|
|
397
|
+
instructions: skill.content
|
|
398
|
+
});
|
|
399
|
+
const idx = notFoundTools.indexOf(toolName);
|
|
400
|
+
if (idx > -1) notFoundTools.splice(idx, 1);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
const nextSteps = [];
|
|
404
|
+
if (foundTools.length > 0) nextSteps.push("For MCP tools: Use the use_tool function with toolName and toolArgs based on the inputSchema above.");
|
|
405
|
+
if (foundSkills.length > 0) nextSteps.push(`For skill, just follow skill's description to continue.`);
|
|
406
|
+
if (options.json) {
|
|
407
|
+
const result = {};
|
|
408
|
+
if (foundTools.length > 0) result.tools = foundTools;
|
|
409
|
+
if (foundSkills.length > 0) result.skills = foundSkills;
|
|
410
|
+
if (nextSteps.length > 0) result.nextSteps = nextSteps;
|
|
411
|
+
if (notFoundTools.length > 0) result.notFound = notFoundTools;
|
|
412
|
+
console.log(JSON.stringify(result, null, 2));
|
|
413
|
+
} else {
|
|
414
|
+
if (foundTools.length > 0) {
|
|
415
|
+
console.log("\nFound tools:\n");
|
|
416
|
+
for (const tool of foundTools) {
|
|
417
|
+
console.log(`Server: ${tool.server}`);
|
|
418
|
+
console.log(`Tool: ${tool.name}`);
|
|
419
|
+
console.log(`Description: ${tool.description || "No description"}`);
|
|
420
|
+
console.log("Input Schema:");
|
|
421
|
+
console.log(JSON.stringify(tool.inputSchema, null, 2));
|
|
422
|
+
console.log("");
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
if (foundSkills.length > 0) {
|
|
426
|
+
console.log("\nFound skills:\n");
|
|
427
|
+
for (const skill of foundSkills) {
|
|
428
|
+
console.log(`Skill: ${skill.name}`);
|
|
429
|
+
console.log(`Location: ${skill.location}`);
|
|
430
|
+
console.log(`Instructions:\n${skill.instructions}`);
|
|
431
|
+
console.log("");
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
if (nextSteps.length > 0) {
|
|
435
|
+
console.log("\nNext steps:");
|
|
436
|
+
for (const step of nextSteps) console.log(` • ${step}`);
|
|
437
|
+
console.log("");
|
|
438
|
+
}
|
|
439
|
+
if (notFoundTools.length > 0) console.error(`\nTools/skills not found: ${notFoundTools.join(", ")}`);
|
|
440
|
+
if (foundTools.length === 0 && foundSkills.length === 0) {
|
|
441
|
+
console.error("No tools or skills found");
|
|
442
|
+
process.exit(1);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
});
|
|
601
446
|
} catch (error) {
|
|
602
|
-
|
|
447
|
+
console.error(`Error executing describe-tools: ${toErrorMessage$8(error)}`);
|
|
448
|
+
process.exit(1);
|
|
603
449
|
}
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
process.exit(0);
|
|
610
|
-
} catch (error) {
|
|
611
|
-
console.error(`Failed to gracefully stop transport during ${signal}: ${toErrorMessage$9(error)}`);
|
|
612
|
-
process.exit(1);
|
|
613
|
-
}
|
|
614
|
-
};
|
|
615
|
-
process.on("SIGINT", async () => await shutdown("SIGINT"));
|
|
616
|
-
process.on("SIGTERM", async () => await shutdown("SIGTERM"));
|
|
450
|
+
});
|
|
451
|
+
//#endregion
|
|
452
|
+
//#region src/commands/get-prompt.ts
|
|
453
|
+
function toErrorMessage$7(error) {
|
|
454
|
+
return error instanceof Error ? error.message : String(error);
|
|
617
455
|
}
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
const shutdownToken = (0, node_crypto.randomUUID)();
|
|
622
|
-
const runtimeServerId = serverOptions.serverId ?? require_src.generateServerId();
|
|
623
|
-
const requestedPort = config.port;
|
|
624
|
-
const portRange = requestedPort !== void 0 ? {
|
|
625
|
-
min: requestedPort,
|
|
626
|
-
max: requestedPort
|
|
627
|
-
} : _agimon_ai_foundation_port_registry.DEFAULT_PORT_RANGE;
|
|
628
|
-
const portLease = await createPortRegistryLease(PORT_REGISTRY_SERVICE_HTTP, config.host ?? DEFAULT_HOST, requestedPort, runtimeServerId, TRANSPORT_TYPE_HTTP, resolvedConfigPath, portRange);
|
|
629
|
-
const runtimePort = portLease.port;
|
|
630
|
-
const runtimeConfig = {
|
|
631
|
-
...config,
|
|
632
|
-
port: runtimePort
|
|
633
|
-
};
|
|
634
|
-
const processLease = await (0, _agimon_ai_foundation_process_registry.createProcessLease)({
|
|
635
|
-
repositoryPath: getRegistryRepositoryPath(),
|
|
636
|
-
serviceName: PROCESS_REGISTRY_SERVICE_HTTP,
|
|
637
|
-
serviceType: PROCESS_REGISTRY_SERVICE_TYPE,
|
|
638
|
-
environment: getRegistryEnvironment(),
|
|
639
|
-
host: runtimeConfig.host ?? DEFAULT_HOST,
|
|
640
|
-
port: runtimePort,
|
|
641
|
-
command: process.argv[1],
|
|
642
|
-
args: process.argv.slice(2),
|
|
643
|
-
metadata: {
|
|
644
|
-
transport: TRANSPORT_TYPE_HTTP,
|
|
645
|
-
serverId: runtimeServerId,
|
|
646
|
-
...resolvedConfigPath ? { configPath: resolvedConfigPath } : {}
|
|
647
|
-
}
|
|
648
|
-
});
|
|
649
|
-
let releasePort = async () => {
|
|
650
|
-
await releasePortLease(portLease ?? null);
|
|
651
|
-
releasePort = async () => void 0;
|
|
652
|
-
};
|
|
653
|
-
const runtimeRecord = createRuntimeRecord(runtimeServerId, runtimeConfig, runtimePort, shutdownToken, resolvedConfigPath);
|
|
654
|
-
let handler;
|
|
655
|
-
let isStopping = false;
|
|
656
|
-
const stopHandler = async () => {
|
|
657
|
-
if (isStopping) return;
|
|
658
|
-
isStopping = true;
|
|
456
|
+
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) => {
|
|
457
|
+
try {
|
|
458
|
+
let promptArgs = {};
|
|
659
459
|
try {
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
process.exit(0);
|
|
664
|
-
} catch (error) {
|
|
665
|
-
throw new Error(`Failed to stop HTTP runtime '${runtimeRecord.serverId}' from admin shutdown: ${toErrorMessage$9(error)}`);
|
|
460
|
+
promptArgs = JSON.parse(options.args);
|
|
461
|
+
} catch {
|
|
462
|
+
throw new Error("Invalid JSON in --args");
|
|
666
463
|
}
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
464
|
+
await withConnectedCommandContext(options, async ({ clientManager }) => {
|
|
465
|
+
const clients = clientManager.getAllClients();
|
|
466
|
+
if (options.server) {
|
|
467
|
+
const client = clientManager.getClient(options.server);
|
|
468
|
+
if (!client) throw new Error(`Server "${options.server}" not found`);
|
|
469
|
+
const prompt = await client.getPrompt(promptName, promptArgs);
|
|
470
|
+
if (options.json) console.log(JSON.stringify(prompt, null, 2));
|
|
471
|
+
else for (const message of prompt.messages) {
|
|
472
|
+
const content = message.content;
|
|
473
|
+
if (typeof content === "object" && content && "text" in content) console.log(content.text);
|
|
474
|
+
else console.log(JSON.stringify(message, null, 2));
|
|
475
|
+
}
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
const matchingServers = [];
|
|
479
|
+
await Promise.all(clients.map(async (client) => {
|
|
480
|
+
try {
|
|
481
|
+
if ((await client.listPrompts()).some((prompt) => prompt.name === promptName)) matchingServers.push(client.serverName);
|
|
482
|
+
} catch (error) {
|
|
483
|
+
if (!options.json) console.error(`Failed to list prompts from ${client.serverName}: ${toErrorMessage$7(error)}`);
|
|
484
|
+
}
|
|
485
|
+
}));
|
|
486
|
+
if (matchingServers.length === 0) throw new Error(`Prompt "${promptName}" not found on any connected server`);
|
|
487
|
+
if (matchingServers.length > 1) throw new Error(`Prompt "${promptName}" found on multiple servers: ${matchingServers.join(", ")}. Use --server to disambiguate`);
|
|
488
|
+
const client = clientManager.getClient(matchingServers[0]);
|
|
489
|
+
if (!client) throw new Error(`Internal error: Server "${matchingServers[0]}" not connected`);
|
|
490
|
+
const prompt = await client.getPrompt(promptName, promptArgs);
|
|
491
|
+
if (options.json) console.log(JSON.stringify(prompt, null, 2));
|
|
492
|
+
else for (const message of prompt.messages) {
|
|
493
|
+
const content = message.content;
|
|
494
|
+
if (typeof content === "object" && content && "text" in content) console.log(content.text);
|
|
495
|
+
else console.log(JSON.stringify(message, null, 2));
|
|
496
|
+
}
|
|
681
497
|
});
|
|
682
|
-
await writeRuntimeRecord(runtimeStateService, runtimeRecord);
|
|
683
498
|
} catch (error) {
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
await cleanupFailedRuntimeStartup(handler, runtimeStateService, runtimeRecord.serverId, processLease);
|
|
687
|
-
throw new Error(`Failed to start HTTP runtime '${runtimeRecord.serverId}': ${toErrorMessage$9(error)}`);
|
|
499
|
+
console.error(`Error executing get-prompt: ${toErrorMessage$7(error)}`);
|
|
500
|
+
process.exit(1);
|
|
688
501
|
}
|
|
689
|
-
|
|
502
|
+
});
|
|
503
|
+
//#endregion
|
|
504
|
+
//#region src/templates/mcp-config.json?raw
|
|
505
|
+
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";
|
|
506
|
+
//#endregion
|
|
507
|
+
//#region src/templates/mcp-config.yaml.liquid?raw
|
|
508
|
+
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";
|
|
509
|
+
//#endregion
|
|
510
|
+
//#region src/utils/output.ts
|
|
511
|
+
function writeLine(message = "") {
|
|
512
|
+
console.log(message);
|
|
690
513
|
}
|
|
691
|
-
|
|
514
|
+
function writeError(message, detail) {
|
|
515
|
+
if (detail) console.error(`${message} ${detail}`);
|
|
516
|
+
else console.error(message);
|
|
517
|
+
}
|
|
518
|
+
const log = {
|
|
519
|
+
info: (message) => writeLine(message),
|
|
520
|
+
error: (message, detail) => writeError(message, detail)
|
|
521
|
+
};
|
|
522
|
+
const print = {
|
|
523
|
+
info: (message) => writeLine(message),
|
|
524
|
+
warning: (message) => writeLine(`Warning: ${message}`),
|
|
525
|
+
error: (message) => writeError(message),
|
|
526
|
+
success: (message) => writeLine(message),
|
|
527
|
+
newline: () => writeLine(),
|
|
528
|
+
header: (message) => writeLine(message),
|
|
529
|
+
item: (message) => writeLine(`- ${message}`),
|
|
530
|
+
indent: (message) => writeLine(` ${message}`)
|
|
531
|
+
};
|
|
532
|
+
//#endregion
|
|
533
|
+
//#region src/commands/init.ts
|
|
534
|
+
/**
|
|
535
|
+
* Init Command
|
|
536
|
+
*
|
|
537
|
+
* DESIGN PATTERNS:
|
|
538
|
+
* - Command pattern with Commander for CLI argument parsing
|
|
539
|
+
* - Async/await pattern for asynchronous operations
|
|
540
|
+
* - Error handling pattern with try-catch and proper exit codes
|
|
541
|
+
*
|
|
542
|
+
* CODING STANDARDS:
|
|
543
|
+
* - Use async action handlers for asynchronous operations
|
|
544
|
+
* - Provide clear option descriptions and default values
|
|
545
|
+
* - Handle errors gracefully with process.exit()
|
|
546
|
+
* - Log progress and errors to console
|
|
547
|
+
* - Use Commander's .option() and .argument() for inputs
|
|
548
|
+
*
|
|
549
|
+
* AVOID:
|
|
550
|
+
* - Synchronous blocking operations in action handlers
|
|
551
|
+
* - Missing error handling (always use try-catch)
|
|
552
|
+
* - Hardcoded values (use options or environment variables)
|
|
553
|
+
* - Not exiting with appropriate exit codes on errors
|
|
554
|
+
*/
|
|
555
|
+
/**
|
|
556
|
+
* Initialize MCP configuration file
|
|
557
|
+
*/
|
|
558
|
+
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) => {
|
|
692
559
|
try {
|
|
693
|
-
|
|
560
|
+
const outputPath = (0, node_path.resolve)(options.output);
|
|
561
|
+
const isYaml = !options.json && (outputPath.endsWith(".yaml") || outputPath.endsWith(".yml"));
|
|
562
|
+
let content;
|
|
563
|
+
if (isYaml) {
|
|
564
|
+
const liquid = new liquidjs.Liquid();
|
|
565
|
+
let mcpServersData = null;
|
|
566
|
+
if (options.mcpServers) try {
|
|
567
|
+
const serversObj = JSON.parse(options.mcpServers);
|
|
568
|
+
mcpServersData = Object.entries(serversObj).map(([name, config]) => ({
|
|
569
|
+
name,
|
|
570
|
+
command: config.command,
|
|
571
|
+
args: config.args
|
|
572
|
+
}));
|
|
573
|
+
} catch (parseError) {
|
|
574
|
+
log.error("Failed to parse --mcp-servers JSON:", parseError instanceof Error ? parseError.message : String(parseError));
|
|
575
|
+
process.exit(1);
|
|
576
|
+
}
|
|
577
|
+
content = await liquid.parseAndRender(mcp_config_yaml_default, { mcpServers: mcpServersData });
|
|
578
|
+
} else content = mcp_config_default;
|
|
579
|
+
try {
|
|
580
|
+
await (0, node_fs_promises.writeFile)(outputPath, content, {
|
|
581
|
+
encoding: "utf-8",
|
|
582
|
+
flag: options.force ? "w" : "wx"
|
|
583
|
+
});
|
|
584
|
+
} catch (error) {
|
|
585
|
+
if (error && typeof error === "object" && "code" in error && error.code === "EEXIST") {
|
|
586
|
+
log.error(`Config file already exists: ${outputPath}`);
|
|
587
|
+
log.info("Use --force to overwrite");
|
|
588
|
+
process.exit(1);
|
|
589
|
+
}
|
|
590
|
+
throw error;
|
|
591
|
+
}
|
|
592
|
+
log.info(`MCP configuration file created: ${outputPath}`);
|
|
593
|
+
log.info("Next steps:");
|
|
594
|
+
log.info("1. Edit the configuration file to add your MCP servers");
|
|
595
|
+
log.info(`2. Run: mcp-proxy mcp-serve --config ${outputPath}`);
|
|
694
596
|
} catch (error) {
|
|
695
|
-
|
|
597
|
+
log.error("Error executing init:", error instanceof Error ? error.message : String(error));
|
|
598
|
+
process.exit(1);
|
|
696
599
|
}
|
|
600
|
+
});
|
|
601
|
+
//#endregion
|
|
602
|
+
//#region src/commands/list-prompts.ts
|
|
603
|
+
function toErrorMessage$6(error) {
|
|
604
|
+
return error instanceof Error ? error.message : String(error);
|
|
697
605
|
}
|
|
698
|
-
|
|
606
|
+
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) => {
|
|
699
607
|
try {
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
repositoryPath,
|
|
728
|
-
serviceName: PORT_REGISTRY_SERVICE_HTTP,
|
|
729
|
-
serviceType: PORT_REGISTRY_SERVICE_TYPE,
|
|
730
|
-
environment: getRegistryEnvironment()
|
|
731
|
-
});
|
|
732
|
-
if (result.success && result.record) {
|
|
733
|
-
const host = config.host ?? result.record.host;
|
|
734
|
-
const endpoint = new URL(`http://${host}:${result.record.port}${MCP_ENDPOINT_PATH}`);
|
|
735
|
-
try {
|
|
736
|
-
const healthUrl = `http://${host}:${result.record.port}/health`;
|
|
737
|
-
if ((await fetch(healthUrl)).ok) return { endpoint };
|
|
738
|
-
} catch {}
|
|
739
|
-
await portRegistry.releasePort({
|
|
740
|
-
repositoryPath,
|
|
741
|
-
serviceName: PORT_REGISTRY_SERVICE_HTTP,
|
|
742
|
-
serviceType: PORT_REGISTRY_SERVICE_TYPE,
|
|
743
|
-
environment: getRegistryEnvironment(),
|
|
744
|
-
force: true
|
|
608
|
+
await withConnectedCommandContext(options, async ({ clientManager }) => {
|
|
609
|
+
const clients = options.server ? [clientManager.getClient(options.server)].filter((client) => client !== void 0) : clientManager.getAllClients();
|
|
610
|
+
if (options.server && clients.length === 0) throw new Error(`Server "${options.server}" not found`);
|
|
611
|
+
const promptsByServer = {};
|
|
612
|
+
await Promise.all(clients.map(async (client) => {
|
|
613
|
+
try {
|
|
614
|
+
promptsByServer[client.serverName] = await client.listPrompts();
|
|
615
|
+
} catch (error) {
|
|
616
|
+
promptsByServer[client.serverName] = [];
|
|
617
|
+
if (!options.json) console.error(`Failed to list prompts from ${client.serverName}: ${toErrorMessage$6(error)}`);
|
|
618
|
+
}
|
|
619
|
+
}));
|
|
620
|
+
if (options.json) console.log(JSON.stringify(promptsByServer, null, 2));
|
|
621
|
+
else for (const [serverName, prompts] of Object.entries(promptsByServer)) {
|
|
622
|
+
console.log(`\n${serverName}:`);
|
|
623
|
+
if (prompts.length === 0) {
|
|
624
|
+
console.log(" No prompts available");
|
|
625
|
+
continue;
|
|
626
|
+
}
|
|
627
|
+
for (const prompt of prompts) {
|
|
628
|
+
console.log(` - ${prompt.name}: ${prompt.description || "No description"}`);
|
|
629
|
+
if (prompt.arguments && prompt.arguments.length > 0) {
|
|
630
|
+
const args = prompt.arguments.map((arg) => `${arg.name}${arg.required ? " (required)" : ""}`).join(", ");
|
|
631
|
+
console.log(` args: ${args}`);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
745
635
|
});
|
|
636
|
+
} catch (error) {
|
|
637
|
+
console.error(`Error executing list-prompts: ${toErrorMessage$6(error)}`);
|
|
638
|
+
process.exit(1);
|
|
746
639
|
}
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
640
|
+
});
|
|
641
|
+
//#endregion
|
|
642
|
+
//#region src/commands/list-resources.ts
|
|
643
|
+
/**
|
|
644
|
+
* ListResources Command
|
|
645
|
+
*
|
|
646
|
+
* DESIGN PATTERNS:
|
|
647
|
+
* - Command pattern with Commander for CLI argument parsing
|
|
648
|
+
* - Async/await pattern for asynchronous operations
|
|
649
|
+
* - Error handling pattern with try-catch and proper exit codes
|
|
650
|
+
*
|
|
651
|
+
* CODING STANDARDS:
|
|
652
|
+
* - Use async action handlers for asynchronous operations
|
|
653
|
+
* - Provide clear option descriptions and default values
|
|
654
|
+
* - Handle errors gracefully with process.exit()
|
|
655
|
+
* - Log progress and errors to console
|
|
656
|
+
* - Use Commander's .option() and .argument() for inputs
|
|
657
|
+
*
|
|
658
|
+
* AVOID:
|
|
659
|
+
* - Synchronous blocking operations in action handlers
|
|
660
|
+
* - Missing error handling (always use try-catch)
|
|
661
|
+
* - Hardcoded values (use options or environment variables)
|
|
662
|
+
* - Not exiting with appropriate exit codes on errors
|
|
663
|
+
*/
|
|
664
|
+
function toErrorMessage$5(error) {
|
|
665
|
+
return error instanceof Error ? error.message : String(error);
|
|
759
666
|
}
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
667
|
+
/**
|
|
668
|
+
* List all available resources from connected MCP servers
|
|
669
|
+
*/
|
|
670
|
+
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) => {
|
|
763
671
|
try {
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
672
|
+
await withConnectedCommandContext(options, async ({ clientManager }) => {
|
|
673
|
+
const clients = options.server ? [clientManager.getClient(options.server)].filter((c) => c !== void 0) : clientManager.getAllClients();
|
|
674
|
+
if (options.server && clients.length === 0) throw new Error(`Server "${options.server}" not found`);
|
|
675
|
+
const resourcesByServer = {};
|
|
676
|
+
const resourceResults = await Promise.all(clients.map(async (client) => {
|
|
677
|
+
try {
|
|
678
|
+
const resources = await client.listResources();
|
|
679
|
+
return {
|
|
680
|
+
serverName: client.serverName,
|
|
681
|
+
resources,
|
|
682
|
+
error: null
|
|
683
|
+
};
|
|
684
|
+
} catch (error) {
|
|
685
|
+
return {
|
|
686
|
+
serverName: client.serverName,
|
|
687
|
+
resources: [],
|
|
688
|
+
error
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
}));
|
|
692
|
+
for (const { serverName, resources, error } of resourceResults) {
|
|
693
|
+
if (error && !options.json) console.error(`Failed to list resources from ${serverName}: ${toErrorMessage$5(error)}`);
|
|
694
|
+
resourcesByServer[serverName] = resources;
|
|
695
|
+
}
|
|
696
|
+
if (options.json) console.log(JSON.stringify(resourcesByServer, null, 2));
|
|
697
|
+
else for (const [serverName, resources] of Object.entries(resourcesByServer)) {
|
|
698
|
+
console.log(`\n${serverName}:`);
|
|
699
|
+
if (resources.length === 0) console.log(" No resources available");
|
|
700
|
+
else for (const resource of resources) {
|
|
701
|
+
const label = resource.name ? `${resource.name} (${resource.uri})` : resource.uri;
|
|
702
|
+
console.log(` - ${label}${resource.description ? `: ${resource.description}` : ""}`);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
773
705
|
});
|
|
774
706
|
} catch (error) {
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
try {
|
|
778
|
-
await stopServerService.stop({
|
|
779
|
-
serverId: ownedRuntimeServerId,
|
|
780
|
-
force: true
|
|
781
|
-
});
|
|
782
|
-
} catch (cleanupError) {
|
|
783
|
-
throw new Error(`Failed to start stdio-http transport: ${toErrorMessage$9(error)}; also failed to stop owned HTTP runtime '${ownedRuntimeServerId}': ${toErrorMessage$9(cleanupError)}`);
|
|
784
|
-
}
|
|
785
|
-
}
|
|
786
|
-
throw new Error(`Failed to start stdio-http transport: ${toErrorMessage$9(error)}`);
|
|
707
|
+
console.error(`Error executing list-resources: ${toErrorMessage$5(error)}`);
|
|
708
|
+
process.exit(1);
|
|
787
709
|
}
|
|
710
|
+
});
|
|
711
|
+
//#endregion
|
|
712
|
+
//#region src/commands/list-tools.ts
|
|
713
|
+
/**
|
|
714
|
+
* List Tools Command
|
|
715
|
+
*
|
|
716
|
+
* DESIGN PATTERNS:
|
|
717
|
+
* - Command pattern with Commander for CLI argument parsing
|
|
718
|
+
* - Async/await pattern for asynchronous operations
|
|
719
|
+
* - Error handling pattern with try-catch and proper exit codes
|
|
720
|
+
*
|
|
721
|
+
* CODING STANDARDS:
|
|
722
|
+
* - Use async action handlers for asynchronous operations
|
|
723
|
+
* - Provide clear option descriptions and default values
|
|
724
|
+
* - Handle errors gracefully with process.exit()
|
|
725
|
+
* - Log progress and errors to console
|
|
726
|
+
* - Use Commander's .option() and .argument() for inputs
|
|
727
|
+
*
|
|
728
|
+
* AVOID:
|
|
729
|
+
* - Synchronous blocking operations in action handlers
|
|
730
|
+
* - Missing error handling (always use try-catch)
|
|
731
|
+
* - Hardcoded values (use options or environment variables)
|
|
732
|
+
* - Not exiting with appropriate exit codes on errors
|
|
733
|
+
*/
|
|
734
|
+
function toErrorMessage$4(error) {
|
|
735
|
+
return error instanceof Error ? error.message : String(error);
|
|
788
736
|
}
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
return;
|
|
737
|
+
function printSearchResults(result) {
|
|
738
|
+
for (const server of result.servers) {
|
|
739
|
+
console.log(`\n${server.server}:`);
|
|
740
|
+
if (server.capabilities && server.capabilities.length > 0) console.log(` capabilities: ${server.capabilities.join(", ")}`);
|
|
741
|
+
if (server.summary) console.log(` summary: ${server.summary}`);
|
|
742
|
+
if (server.tools.length === 0) {
|
|
743
|
+
console.log(" no tools");
|
|
744
|
+
continue;
|
|
798
745
|
}
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
746
|
+
for (const tool of server.tools) {
|
|
747
|
+
const capabilitySummary = tool.capabilities && tool.capabilities.length > 0 ? ` [${tool.capabilities.join(", ")}]` : "";
|
|
748
|
+
console.log(` - ${tool.name}${capabilitySummary}`);
|
|
749
|
+
if (tool.description) console.log(` ${tool.description}`);
|
|
802
750
|
}
|
|
803
|
-
await startStdioHttpTransport(createTransportConfig(options, require_src.TRANSPORT_MODE.HTTP, proxyDefaults), options, resolvedConfigPath, proxyDefaults);
|
|
804
|
-
} catch (error) {
|
|
805
|
-
throw new Error(`Failed to start transport '${transportType}': ${toErrorMessage$9(error)}`);
|
|
806
751
|
}
|
|
807
752
|
}
|
|
808
|
-
|
|
809
|
-
* MCP Serve command
|
|
810
|
-
*/
|
|
811
|
-
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}`).option("-p, --port <port>", "Port to listen on (http/sse) or backend port for stdio-http", (val) => Number.parseInt(val, 10)).option("--host <host>", "Host to bind to (http/sse) or backend host for stdio-http").option("-c, --config <path>", "Path to MCP server configuration file").option("--no-cache", "Disable configuration caching, always reload from config file").option("--definitions-cache <path>", "Path to prefetched tool/prompt/skill definitions cache file").option("--clear-definitions-cache", "Delete definitions cache before startup", false).option("--proxy-mode <mode>", "How mcp-proxy exposes downstream tools: meta, flat, or search", "meta").option("--id <id>", "Unique server identifier (overrides config file id, auto-generated if not provided)").action(async (options) => {
|
|
753
|
+
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) => {
|
|
812
754
|
try {
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
755
|
+
await withConnectedCommandContext(options, async ({ container, config, clientManager, configFilePath }) => {
|
|
756
|
+
clientManager.registerServerConfigs(config.mcpServers);
|
|
757
|
+
const cachePath = options.definitionsCache || require_src.DefinitionsCacheService.getDefaultCachePath(configFilePath);
|
|
758
|
+
let cacheData;
|
|
759
|
+
try {
|
|
760
|
+
cacheData = await require_src.DefinitionsCacheService.readFromFile(cachePath);
|
|
761
|
+
} catch {
|
|
762
|
+
cacheData = void 0;
|
|
763
|
+
}
|
|
764
|
+
const definitionsCacheService = container.createDefinitionsCacheService(clientManager, void 0, { cacheData });
|
|
765
|
+
const textBlock = (await container.createSearchListToolsTool(clientManager, definitionsCacheService).execute({
|
|
766
|
+
capability: options.capability,
|
|
767
|
+
serverName: options.server
|
|
768
|
+
})).content.find((content) => content.type === "text");
|
|
769
|
+
const parsed = textBlock?.type === "text" ? JSON.parse(textBlock.text) : { servers: [] };
|
|
770
|
+
if (options.json) console.log(JSON.stringify(parsed, null, 2));
|
|
771
|
+
else {
|
|
772
|
+
if (!parsed.servers || parsed.servers.length === 0) throw new Error("No tools matched the requested filters");
|
|
773
|
+
printSearchResults(parsed);
|
|
774
|
+
}
|
|
775
|
+
});
|
|
818
776
|
} catch (error) {
|
|
819
|
-
|
|
820
|
-
const transportType = isValidTransportType(rawTransportType) ? rawTransportType : TRANSPORT_TYPE_STDIO;
|
|
821
|
-
const envPort = process.env.MCP_PORT ? Number(process.env.MCP_PORT) : void 0;
|
|
822
|
-
const requestedPort = options.port ?? (Number.isFinite(envPort) ? envPort : void 0);
|
|
823
|
-
console.error(formatStartError(transportType, options.host ?? DEFAULT_HOST, requestedPort, error));
|
|
777
|
+
console.error(`Error executing search-tools: ${toErrorMessage$4(error)}`);
|
|
824
778
|
process.exit(1);
|
|
825
779
|
}
|
|
826
780
|
});
|
|
827
781
|
//#endregion
|
|
828
|
-
//#region src/commands/
|
|
829
|
-
|
|
782
|
+
//#region src/commands/mcp-serve.ts
|
|
783
|
+
/**
|
|
784
|
+
* MCP Serve Command
|
|
785
|
+
*
|
|
786
|
+
* DESIGN PATTERNS:
|
|
787
|
+
* - Command pattern with Commander for CLI argument parsing
|
|
788
|
+
* - Transport abstraction pattern for flexible deployment (stdio, HTTP, SSE)
|
|
789
|
+
* - Factory pattern for creating transport handlers
|
|
790
|
+
* - Graceful shutdown pattern with signal handling
|
|
791
|
+
*
|
|
792
|
+
* CODING STANDARDS:
|
|
793
|
+
* - Use async/await for asynchronous operations
|
|
794
|
+
* - Implement proper error handling with try-catch blocks
|
|
795
|
+
* - Handle process signals for graceful shutdown
|
|
796
|
+
* - Provide clear CLI options and help messages
|
|
797
|
+
*
|
|
798
|
+
* AVOID:
|
|
799
|
+
* - Hardcoded configuration values (use CLI options or environment variables)
|
|
800
|
+
* - Missing error handling for transport startup
|
|
801
|
+
* - Not cleaning up resources on shutdown
|
|
802
|
+
*/
|
|
803
|
+
const CONFIG_FILE_NAMES = [
|
|
804
|
+
"mcp-config.yaml",
|
|
805
|
+
"mcp-config.yml",
|
|
806
|
+
"mcp-config.json"
|
|
807
|
+
];
|
|
808
|
+
const MCP_ENDPOINT_PATH = "/mcp";
|
|
809
|
+
const DEFAULT_HOST = "localhost";
|
|
810
|
+
const TRANSPORT_TYPE_STDIO = "stdio";
|
|
811
|
+
const TRANSPORT_TYPE_HTTP = "http";
|
|
812
|
+
const TRANSPORT_TYPE_SSE = "sse";
|
|
813
|
+
const TRANSPORT_TYPE_STDIO_HTTP = "stdio-http";
|
|
814
|
+
const RUNTIME_TRANSPORT = TRANSPORT_TYPE_HTTP;
|
|
815
|
+
const PORT_REGISTRY_SERVICE_HTTP = "mcp-proxy-http";
|
|
816
|
+
const PORT_REGISTRY_SERVICE_TYPE = "service";
|
|
817
|
+
function getWorkspaceRoot() {
|
|
818
|
+
return resolveWorkspaceRoot();
|
|
819
|
+
}
|
|
820
|
+
const PROCESS_REGISTRY_SERVICE_HTTP = "mcp-proxy-http";
|
|
821
|
+
const PROCESS_REGISTRY_SERVICE_TYPE = "service";
|
|
822
|
+
function getRegistryRepositoryPath() {
|
|
823
|
+
return getWorkspaceRoot();
|
|
824
|
+
}
|
|
825
|
+
function toErrorMessage$3(error) {
|
|
830
826
|
return error instanceof Error ? error.message : String(error);
|
|
831
827
|
}
|
|
832
|
-
|
|
828
|
+
function isValidTransportType(type) {
|
|
829
|
+
return type === TRANSPORT_TYPE_STDIO || type === TRANSPORT_TYPE_HTTP || type === TRANSPORT_TYPE_SSE || type === TRANSPORT_TYPE_STDIO_HTTP;
|
|
830
|
+
}
|
|
831
|
+
function isValidProxyMode(mode) {
|
|
832
|
+
return mode === "meta" || mode === "flat" || mode === "search";
|
|
833
|
+
}
|
|
834
|
+
async function pathExists(filePath) {
|
|
833
835
|
try {
|
|
834
|
-
|
|
836
|
+
await (0, node_fs_promises.access)(filePath, node_fs.constants.F_OK);
|
|
837
|
+
return true;
|
|
835
838
|
} catch {
|
|
836
839
|
return false;
|
|
837
840
|
}
|
|
838
841
|
}
|
|
839
|
-
|
|
840
|
-
* Proxy mode: connect to a running HTTP server instead of downstream servers directly.
|
|
841
|
-
* Auto-starts the server if not running.
|
|
842
|
-
*/
|
|
843
|
-
async function withProxiedContext(container, config, configFilePath, options, run) {
|
|
844
|
-
const host = config.proxy?.host ?? "localhost";
|
|
845
|
-
const port = config.proxy?.port;
|
|
846
|
-
const endpoint = `http://${host}:${port}/mcp`;
|
|
847
|
-
if (!await checkHealth(host, port)) {
|
|
848
|
-
if (!options.json) console.error("Starting HTTP proxy server in background...");
|
|
849
|
-
await prestartHttpRuntime({
|
|
850
|
-
host,
|
|
851
|
-
port,
|
|
852
|
-
config: configFilePath,
|
|
853
|
-
cache: options.useCache !== false,
|
|
854
|
-
clearDefinitionsCache: false,
|
|
855
|
-
proxyMode: "flat"
|
|
856
|
-
});
|
|
857
|
-
}
|
|
858
|
-
const clientManager = container.createClientManagerService();
|
|
842
|
+
async function findConfigFileAsync() {
|
|
859
843
|
try {
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
}
|
|
865
|
-
|
|
844
|
+
const projectPath = process.env.PROJECT_PATH;
|
|
845
|
+
if (projectPath) for (const fileName of CONFIG_FILE_NAMES) {
|
|
846
|
+
const configPath = (0, node_path.resolve)(projectPath, fileName);
|
|
847
|
+
if (await pathExists(configPath)) return configPath;
|
|
848
|
+
}
|
|
849
|
+
const MAX_PARENT_LEVELS = 3;
|
|
850
|
+
let searchDir = process.cwd();
|
|
851
|
+
for (let level = 0; level <= MAX_PARENT_LEVELS; level++) {
|
|
852
|
+
for (const fileName of CONFIG_FILE_NAMES) {
|
|
853
|
+
const configPath = (0, node_path.join)(searchDir, fileName);
|
|
854
|
+
if (await pathExists(configPath)) return configPath;
|
|
855
|
+
}
|
|
856
|
+
const parentDir = (0, node_path.dirname)(searchDir);
|
|
857
|
+
if (parentDir === searchDir) break;
|
|
858
|
+
searchDir = parentDir;
|
|
859
|
+
}
|
|
860
|
+
return null;
|
|
866
861
|
} catch (error) {
|
|
867
|
-
throw new Error(`Failed to
|
|
862
|
+
throw new Error(`Failed to discover MCP config file: ${toErrorMessage$3(error)}`);
|
|
868
863
|
}
|
|
864
|
+
}
|
|
865
|
+
function loadProxyDefaults(configPath) {
|
|
869
866
|
try {
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
867
|
+
const content = (0, node_fs.readFileSync)(configPath, "utf-8");
|
|
868
|
+
const proxy = (configPath.endsWith(".yaml") || configPath.endsWith(".yml") ? js_yaml.default.load(content) : JSON.parse(content))?.proxy;
|
|
869
|
+
if (!proxy || typeof proxy !== "object") return {};
|
|
870
|
+
const p = proxy;
|
|
871
|
+
return {
|
|
872
|
+
type: typeof p.type === "string" ? p.type : void 0,
|
|
873
|
+
port: typeof p.port === "number" && Number.isInteger(p.port) && p.port > 0 ? p.port : void 0,
|
|
874
|
+
host: typeof p.host === "string" ? p.host : void 0,
|
|
875
|
+
keepAlive: typeof p.keepAlive === "boolean" ? p.keepAlive : void 0
|
|
876
|
+
};
|
|
877
|
+
} catch {
|
|
878
|
+
return {};
|
|
878
879
|
}
|
|
879
880
|
}
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
881
|
+
async function resolveServerId(options, resolvedConfigPath) {
|
|
882
|
+
const container = require_src.createProxyIoCContainer();
|
|
883
|
+
if (options.id) return options.id;
|
|
884
|
+
if (resolvedConfigPath) try {
|
|
885
|
+
const config = await container.createConfigFetcherService({
|
|
886
|
+
configFilePath: resolvedConfigPath,
|
|
887
|
+
useCache: options.cache !== false
|
|
888
|
+
}).fetchConfiguration(options.cache === false);
|
|
889
|
+
if (config.id) return config.id;
|
|
890
|
+
} catch (error) {
|
|
891
|
+
throw new Error(`Failed to resolve server ID from config '${resolvedConfigPath}': ${toErrorMessage$3(error)}`);
|
|
892
|
+
}
|
|
893
|
+
return require_src.generateServerId();
|
|
894
|
+
}
|
|
895
|
+
function validateTransportType(type) {
|
|
896
|
+
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}`);
|
|
897
|
+
return type;
|
|
898
|
+
}
|
|
899
|
+
function validateProxyMode(mode) {
|
|
900
|
+
if (!isValidProxyMode(mode)) throw new Error(`Unknown proxy mode: '${mode}'. Valid options: meta, flat, search`);
|
|
901
|
+
}
|
|
902
|
+
function createTransportConfig(options, mode, proxyDefaults) {
|
|
903
|
+
const envPort = process.env.MCP_PORT ? Number(process.env.MCP_PORT) : void 0;
|
|
904
|
+
return {
|
|
905
|
+
mode,
|
|
906
|
+
port: options.port ?? (Number.isFinite(envPort) ? envPort : void 0) ?? proxyDefaults?.port,
|
|
907
|
+
host: options.host ?? process.env.MCP_HOST ?? proxyDefaults?.host ?? DEFAULT_HOST
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
function createStdioSafeLogger() {
|
|
911
|
+
const logToStderr = (message, data) => {
|
|
912
|
+
if (data === void 0) {
|
|
913
|
+
console.error(message);
|
|
914
|
+
return;
|
|
891
915
|
}
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
916
|
+
console.error(message, data);
|
|
917
|
+
};
|
|
918
|
+
return {
|
|
919
|
+
trace: logToStderr,
|
|
920
|
+
debug: logToStderr,
|
|
921
|
+
info: logToStderr,
|
|
922
|
+
warn: logToStderr,
|
|
923
|
+
error: logToStderr
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
function createServerOptions(options, resolvedConfigPath, serverId) {
|
|
927
|
+
return {
|
|
928
|
+
configFilePath: resolvedConfigPath,
|
|
929
|
+
noCache: options.cache === false,
|
|
930
|
+
serverId,
|
|
931
|
+
definitionsCachePath: options.definitionsCache,
|
|
932
|
+
clearDefinitionsCache: options.clearDefinitionsCache,
|
|
933
|
+
proxyMode: options.proxyMode
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
function formatStartError(type, host, port, error) {
|
|
937
|
+
const startErrorMessage = toErrorMessage$3(error);
|
|
938
|
+
if (type === TRANSPORT_TYPE_STDIO) return `Failed to start MCP server with transport '${type}': ${startErrorMessage}`;
|
|
939
|
+
return `Failed to start MCP server with transport '${type}' on ${port === void 0 ? `${host} (dynamic port)` : `${host}:${port}`}: ${startErrorMessage}`;
|
|
940
|
+
}
|
|
941
|
+
function createRuntimeRecord(serverId, config, port, shutdownToken, configPath) {
|
|
942
|
+
return {
|
|
943
|
+
serverId,
|
|
944
|
+
host: config.host ?? DEFAULT_HOST,
|
|
945
|
+
port,
|
|
946
|
+
transport: RUNTIME_TRANSPORT,
|
|
947
|
+
shutdownToken,
|
|
948
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
949
|
+
pid: process.pid,
|
|
950
|
+
configPath
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
function createPortRegistryService() {
|
|
954
|
+
return new _agimon_ai_foundation_port_registry.PortRegistryService(process.env.PORT_REGISTRY_PATH);
|
|
955
|
+
}
|
|
956
|
+
function getRegistryEnvironment() {
|
|
957
|
+
return process.env.NODE_ENV ?? "development";
|
|
958
|
+
}
|
|
959
|
+
async function createPortRegistryLease(serviceName, host, preferredPort, serverId, transport, configPath, portRange = preferredPort !== void 0 ? {
|
|
960
|
+
min: preferredPort,
|
|
961
|
+
max: preferredPort
|
|
962
|
+
} : _agimon_ai_foundation_port_registry.DEFAULT_PORT_RANGE) {
|
|
963
|
+
const portRegistry = createPortRegistryService();
|
|
964
|
+
const result = await portRegistry.reservePort({
|
|
965
|
+
repositoryPath: getRegistryRepositoryPath(),
|
|
966
|
+
serviceName,
|
|
967
|
+
serviceType: PORT_REGISTRY_SERVICE_TYPE,
|
|
968
|
+
environment: getRegistryEnvironment(),
|
|
969
|
+
pid: process.pid,
|
|
970
|
+
host,
|
|
971
|
+
preferredPort,
|
|
972
|
+
portRange,
|
|
973
|
+
force: true,
|
|
974
|
+
metadata: {
|
|
975
|
+
transport,
|
|
976
|
+
serverId,
|
|
977
|
+
...configPath ? { configPath } : {}
|
|
978
|
+
}
|
|
979
|
+
});
|
|
980
|
+
if (!result.success || !result.record) {
|
|
981
|
+
const requestedPortLabel = preferredPort === void 0 ? "dynamic port" : `port ${preferredPort}`;
|
|
982
|
+
throw new Error(result.error || `Failed to reserve ${requestedPortLabel} in port registry`);
|
|
903
983
|
}
|
|
984
|
+
let released = false;
|
|
985
|
+
return {
|
|
986
|
+
port: result.record.port,
|
|
987
|
+
release: async () => {
|
|
988
|
+
if (released) return;
|
|
989
|
+
released = true;
|
|
990
|
+
const releaseResult = await portRegistry.releasePort({
|
|
991
|
+
repositoryPath: getRegistryRepositoryPath(),
|
|
992
|
+
serviceName,
|
|
993
|
+
serviceType: PORT_REGISTRY_SERVICE_TYPE,
|
|
994
|
+
pid: process.pid,
|
|
995
|
+
environment: getRegistryEnvironment(),
|
|
996
|
+
force: true
|
|
997
|
+
});
|
|
998
|
+
if (!releaseResult.success && releaseResult.error && !releaseResult.error.includes("No matching registry entry")) throw new Error(releaseResult.error || `Failed to release port for ${serviceName}`);
|
|
999
|
+
}
|
|
1000
|
+
};
|
|
904
1001
|
}
|
|
905
|
-
async function
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
if (!configFilePath) throw new Error("No config file found. Use --config or create mcp-config.yaml");
|
|
909
|
-
const config = await container.createConfigFetcherService({
|
|
910
|
-
configFilePath,
|
|
911
|
-
useCache: options.useCache
|
|
912
|
-
}).fetchConfiguration();
|
|
913
|
-
if (config.proxy?.port) return await withProxiedContext(container, config, configFilePath, options, run);
|
|
914
|
-
return await withDirectContext(container, config, configFilePath, options, run);
|
|
1002
|
+
async function releasePortLease(lease) {
|
|
1003
|
+
if (!lease) return;
|
|
1004
|
+
await lease.release();
|
|
915
1005
|
}
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
* - Command pattern with Commander for CLI argument parsing
|
|
923
|
-
* - Async/await pattern for asynchronous operations
|
|
924
|
-
* - Error handling pattern with try-catch and proper exit codes
|
|
925
|
-
*
|
|
926
|
-
* CODING STANDARDS:
|
|
927
|
-
* - Use async action handlers for asynchronous operations
|
|
928
|
-
* - Provide clear option descriptions and default values
|
|
929
|
-
* - Handle errors gracefully with process.exit()
|
|
930
|
-
* - Log progress and errors to console
|
|
931
|
-
* - Use Commander's .option() and .argument() for inputs
|
|
932
|
-
*
|
|
933
|
-
* AVOID:
|
|
934
|
-
* - Synchronous blocking operations in action handlers
|
|
935
|
-
* - Missing error handling (always use try-catch)
|
|
936
|
-
* - Hardcoded values (use options or environment variables)
|
|
937
|
-
* - Not exiting with appropriate exit codes on errors
|
|
938
|
-
*/
|
|
939
|
-
function toErrorMessage$7(error) {
|
|
940
|
-
return error instanceof Error ? error.message : String(error);
|
|
1006
|
+
function createHttpAdminOptions(serverId, shutdownToken, onShutdownRequested) {
|
|
1007
|
+
return {
|
|
1008
|
+
serverId,
|
|
1009
|
+
shutdownToken,
|
|
1010
|
+
onShutdownRequested
|
|
1011
|
+
};
|
|
941
1012
|
}
|
|
942
|
-
function
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
if (server.tools.length === 0) {
|
|
948
|
-
console.log(" no tools");
|
|
949
|
-
continue;
|
|
950
|
-
}
|
|
951
|
-
for (const tool of server.tools) {
|
|
952
|
-
const capabilitySummary = tool.capabilities && tool.capabilities.length > 0 ? ` [${tool.capabilities.join(", ")}]` : "";
|
|
953
|
-
console.log(` - ${tool.name}${capabilitySummary}`);
|
|
954
|
-
if (tool.description) console.log(` ${tool.description}`);
|
|
955
|
-
}
|
|
1013
|
+
async function removeRuntimeRecord(runtimeStateService, serverId) {
|
|
1014
|
+
try {
|
|
1015
|
+
await runtimeStateService.remove(serverId);
|
|
1016
|
+
} catch (error) {
|
|
1017
|
+
throw new Error(`Failed to remove runtime state for '${serverId}': ${toErrorMessage$3(error)}`);
|
|
956
1018
|
}
|
|
957
1019
|
}
|
|
958
|
-
|
|
1020
|
+
async function writeRuntimeRecord(runtimeStateService, record) {
|
|
959
1021
|
try {
|
|
960
|
-
await
|
|
961
|
-
clientManager.registerServerConfigs(config.mcpServers);
|
|
962
|
-
const cachePath = options.definitionsCache || require_src.DefinitionsCacheService.getDefaultCachePath(configFilePath);
|
|
963
|
-
let cacheData;
|
|
964
|
-
try {
|
|
965
|
-
cacheData = await require_src.DefinitionsCacheService.readFromFile(cachePath);
|
|
966
|
-
} catch {
|
|
967
|
-
cacheData = void 0;
|
|
968
|
-
}
|
|
969
|
-
const definitionsCacheService = container.createDefinitionsCacheService(clientManager, void 0, { cacheData });
|
|
970
|
-
const textBlock = (await container.createSearchListToolsTool(clientManager, definitionsCacheService).execute({
|
|
971
|
-
capability: options.capability,
|
|
972
|
-
serverName: options.server
|
|
973
|
-
})).content.find((content) => content.type === "text");
|
|
974
|
-
const parsed = textBlock?.type === "text" ? JSON.parse(textBlock.text) : { servers: [] };
|
|
975
|
-
if (options.json) console.log(JSON.stringify(parsed, null, 2));
|
|
976
|
-
else {
|
|
977
|
-
if (!parsed.servers || parsed.servers.length === 0) throw new Error("No tools matched the requested filters");
|
|
978
|
-
printSearchResults(parsed);
|
|
979
|
-
}
|
|
980
|
-
});
|
|
1022
|
+
await runtimeStateService.write(record);
|
|
981
1023
|
} catch (error) {
|
|
982
|
-
|
|
983
|
-
process.exit(1);
|
|
1024
|
+
throw new Error(`Failed to persist runtime state for '${record.serverId}': ${toErrorMessage$3(error)}`);
|
|
984
1025
|
}
|
|
985
|
-
});
|
|
986
|
-
//#endregion
|
|
987
|
-
//#region src/commands/describe-tools.ts
|
|
988
|
-
/**
|
|
989
|
-
* Describe Tools Command
|
|
990
|
-
*
|
|
991
|
-
* DESIGN PATTERNS:
|
|
992
|
-
* - Command pattern with Commander for CLI argument parsing
|
|
993
|
-
* - Async/await pattern for asynchronous operations
|
|
994
|
-
* - Error handling pattern with try-catch and proper exit codes
|
|
995
|
-
*
|
|
996
|
-
* CODING STANDARDS:
|
|
997
|
-
* - Use async action handlers for asynchronous operations
|
|
998
|
-
* - Provide clear option descriptions and default values
|
|
999
|
-
* - Handle errors gracefully with process.exit()
|
|
1000
|
-
* - Log progress and errors to console
|
|
1001
|
-
* - Use Commander's .option() and .argument() for inputs
|
|
1002
|
-
*
|
|
1003
|
-
* AVOID:
|
|
1004
|
-
* - Synchronous blocking operations in action handlers
|
|
1005
|
-
* - Missing error handling (always use try-catch)
|
|
1006
|
-
* - Hardcoded values (use options or environment variables)
|
|
1007
|
-
* - Not exiting with appropriate exit codes on errors
|
|
1008
|
-
*/
|
|
1009
|
-
function toErrorMessage$6(error) {
|
|
1010
|
-
return error instanceof Error ? error.message : String(error);
|
|
1011
1026
|
}
|
|
1012
|
-
|
|
1013
|
-
* Describe specific MCP tools
|
|
1014
|
-
*/
|
|
1015
|
-
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) => {
|
|
1027
|
+
async function stopOwnedHttpTransport(handler, runtimeStateService, serverId, processLease) {
|
|
1016
1028
|
try {
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
const notFoundTools = [...toolNames];
|
|
1026
|
-
const toolResults = await Promise.all(clients.map(async (client) => {
|
|
1027
|
-
try {
|
|
1028
|
-
return {
|
|
1029
|
-
client,
|
|
1030
|
-
tools: await client.listTools(),
|
|
1031
|
-
error: null
|
|
1032
|
-
};
|
|
1033
|
-
} catch (error) {
|
|
1034
|
-
return {
|
|
1035
|
-
client,
|
|
1036
|
-
tools: [],
|
|
1037
|
-
error
|
|
1038
|
-
};
|
|
1039
|
-
}
|
|
1040
|
-
}));
|
|
1041
|
-
for (const { client, tools, error } of toolResults) {
|
|
1042
|
-
if (error) {
|
|
1043
|
-
if (!options.json) console.error(`Failed to list tools from ${client.serverName}:`, error);
|
|
1044
|
-
continue;
|
|
1045
|
-
}
|
|
1046
|
-
for (const toolName of toolNames) {
|
|
1047
|
-
const tool = tools.find((entry) => entry.name === toolName);
|
|
1048
|
-
if (tool) {
|
|
1049
|
-
foundTools.push({
|
|
1050
|
-
server: client.serverName,
|
|
1051
|
-
name: tool.name,
|
|
1052
|
-
description: tool.description,
|
|
1053
|
-
inputSchema: tool.inputSchema
|
|
1054
|
-
});
|
|
1055
|
-
const idx = notFoundTools.indexOf(toolName);
|
|
1056
|
-
if (idx > -1) notFoundTools.splice(idx, 1);
|
|
1057
|
-
}
|
|
1058
|
-
}
|
|
1059
|
-
}
|
|
1060
|
-
if (skillService && notFoundTools.length > 0) {
|
|
1061
|
-
const skillResults = await Promise.all([...notFoundTools].map(async (toolName) => {
|
|
1062
|
-
const skillName = toolName.startsWith("skill__") ? toolName.slice(7) : toolName;
|
|
1063
|
-
return {
|
|
1064
|
-
toolName,
|
|
1065
|
-
skill: await skillService.getSkill(skillName)
|
|
1066
|
-
};
|
|
1067
|
-
}));
|
|
1068
|
-
for (const { toolName, skill } of skillResults) if (skill) {
|
|
1069
|
-
foundSkills.push({
|
|
1070
|
-
name: skill.name,
|
|
1071
|
-
location: skill.basePath,
|
|
1072
|
-
instructions: skill.content
|
|
1073
|
-
});
|
|
1074
|
-
const idx = notFoundTools.indexOf(toolName);
|
|
1075
|
-
if (idx > -1) notFoundTools.splice(idx, 1);
|
|
1076
|
-
}
|
|
1077
|
-
}
|
|
1078
|
-
const nextSteps = [];
|
|
1079
|
-
if (foundTools.length > 0) nextSteps.push("For MCP tools: Use the use_tool function with toolName and toolArgs based on the inputSchema above.");
|
|
1080
|
-
if (foundSkills.length > 0) nextSteps.push(`For skill, just follow skill's description to continue.`);
|
|
1081
|
-
if (options.json) {
|
|
1082
|
-
const result = {};
|
|
1083
|
-
if (foundTools.length > 0) result.tools = foundTools;
|
|
1084
|
-
if (foundSkills.length > 0) result.skills = foundSkills;
|
|
1085
|
-
if (nextSteps.length > 0) result.nextSteps = nextSteps;
|
|
1086
|
-
if (notFoundTools.length > 0) result.notFound = notFoundTools;
|
|
1087
|
-
console.log(JSON.stringify(result, null, 2));
|
|
1088
|
-
} else {
|
|
1089
|
-
if (foundTools.length > 0) {
|
|
1090
|
-
console.log("\nFound tools:\n");
|
|
1091
|
-
for (const tool of foundTools) {
|
|
1092
|
-
console.log(`Server: ${tool.server}`);
|
|
1093
|
-
console.log(`Tool: ${tool.name}`);
|
|
1094
|
-
console.log(`Description: ${tool.description || "No description"}`);
|
|
1095
|
-
console.log("Input Schema:");
|
|
1096
|
-
console.log(JSON.stringify(tool.inputSchema, null, 2));
|
|
1097
|
-
console.log("");
|
|
1098
|
-
}
|
|
1099
|
-
}
|
|
1100
|
-
if (foundSkills.length > 0) {
|
|
1101
|
-
console.log("\nFound skills:\n");
|
|
1102
|
-
for (const skill of foundSkills) {
|
|
1103
|
-
console.log(`Skill: ${skill.name}`);
|
|
1104
|
-
console.log(`Location: ${skill.location}`);
|
|
1105
|
-
console.log(`Instructions:\n${skill.instructions}`);
|
|
1106
|
-
console.log("");
|
|
1107
|
-
}
|
|
1108
|
-
}
|
|
1109
|
-
if (nextSteps.length > 0) {
|
|
1110
|
-
console.log("\nNext steps:");
|
|
1111
|
-
for (const step of nextSteps) console.log(` • ${step}`);
|
|
1112
|
-
console.log("");
|
|
1113
|
-
}
|
|
1114
|
-
if (notFoundTools.length > 0) console.error(`\nTools/skills not found: ${notFoundTools.join(", ")}`);
|
|
1115
|
-
if (foundTools.length === 0 && foundSkills.length === 0) {
|
|
1116
|
-
console.error("No tools or skills found");
|
|
1117
|
-
process.exit(1);
|
|
1118
|
-
}
|
|
1119
|
-
}
|
|
1120
|
-
});
|
|
1121
|
-
} catch (error) {
|
|
1122
|
-
console.error(`Error executing describe-tools: ${toErrorMessage$6(error)}`);
|
|
1123
|
-
process.exit(1);
|
|
1029
|
+
try {
|
|
1030
|
+
await handler.stop();
|
|
1031
|
+
} catch (error) {
|
|
1032
|
+
throw new Error(`Failed to stop owned HTTP transport '${serverId}': ${toErrorMessage$3(error)}`);
|
|
1033
|
+
}
|
|
1034
|
+
} finally {
|
|
1035
|
+
await processLease?.release({ kill: false });
|
|
1036
|
+
await removeRuntimeRecord(runtimeStateService, serverId);
|
|
1124
1037
|
}
|
|
1125
|
-
});
|
|
1126
|
-
//#endregion
|
|
1127
|
-
//#region src/commands/use-tool.ts
|
|
1128
|
-
/**
|
|
1129
|
-
* Use Tool Command
|
|
1130
|
-
*
|
|
1131
|
-
* DESIGN PATTERNS:
|
|
1132
|
-
* - Command pattern with Commander for CLI argument parsing
|
|
1133
|
-
* - Async/await pattern for asynchronous operations
|
|
1134
|
-
* - Error handling pattern with try-catch and proper exit codes
|
|
1135
|
-
*
|
|
1136
|
-
* CODING STANDARDS:
|
|
1137
|
-
* - Use async action handlers for asynchronous operations
|
|
1138
|
-
* - Provide clear option descriptions and default values
|
|
1139
|
-
* - Handle errors gracefully with process.exit()
|
|
1140
|
-
* - Log progress and errors to console
|
|
1141
|
-
* - Use Commander'"'"'s .option() and .argument() for inputs
|
|
1142
|
-
*
|
|
1143
|
-
* AVOID:
|
|
1144
|
-
* - Synchronous blocking operations in action handlers
|
|
1145
|
-
* - Missing error handling (always use try-catch)
|
|
1146
|
-
* - Hardcoded values (use options or environment variables)
|
|
1147
|
-
* - Not exiting with appropriate exit codes on errors
|
|
1148
|
-
*/
|
|
1149
|
-
function toErrorMessage$5(error) {
|
|
1150
|
-
return error instanceof Error ? error.message : String(error);
|
|
1151
1038
|
}
|
|
1152
1039
|
/**
|
|
1153
|
-
*
|
|
1040
|
+
* Run post-stop cleanup for an HTTP runtime (release port, dispose services, remove state).
|
|
1041
|
+
* This is the subset of stopOwnedHttpTransport that runs AFTER handler.stop() has already
|
|
1042
|
+
* been called by startServer()'s signal handler — avoids double-stopping the transport.
|
|
1154
1043
|
*/
|
|
1155
|
-
|
|
1044
|
+
async function cleanupHttpRuntime(runtimeStateService, serverId, processLease) {
|
|
1045
|
+
await processLease?.release({ kill: false });
|
|
1046
|
+
await removeRuntimeRecord(runtimeStateService, serverId);
|
|
1047
|
+
}
|
|
1048
|
+
async function cleanupFailedRuntimeStartup(handler, runtimeStateService, serverId, processLease) {
|
|
1156
1049
|
try {
|
|
1157
|
-
let toolArgs = {};
|
|
1158
1050
|
try {
|
|
1159
|
-
|
|
1160
|
-
} catch {
|
|
1161
|
-
|
|
1162
|
-
process.exit(1);
|
|
1051
|
+
await handler.stop();
|
|
1052
|
+
} catch (error) {
|
|
1053
|
+
throw new Error(`Failed to stop HTTP transport during cleanup for '${serverId}': ${toErrorMessage$3(error)}`);
|
|
1163
1054
|
}
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
const client = clientManager.getClient(options.server);
|
|
1168
|
-
if (!client) throw new Error(`Server "${options.server}" not found`);
|
|
1169
|
-
if (!options.json) console.error(`Executing ${toolName} on ${options.server}...`);
|
|
1170
|
-
const requestOptions = options.timeout ? { timeout: options.timeout } : void 0;
|
|
1171
|
-
const result = await client.callTool(toolName, toolArgs, requestOptions);
|
|
1172
|
-
if (options.json) console.log(JSON.stringify(result, null, 2));
|
|
1173
|
-
else {
|
|
1174
|
-
console.log("\nResult:");
|
|
1175
|
-
if (result.content) for (const content of result.content) if (content.type === "text") console.log(content.text);
|
|
1176
|
-
else console.log(JSON.stringify(content, null, 2));
|
|
1177
|
-
if (result.isError) {
|
|
1178
|
-
console.error("\n⚠️ Tool execution returned an error");
|
|
1179
|
-
process.exit(1);
|
|
1180
|
-
}
|
|
1181
|
-
}
|
|
1182
|
-
return;
|
|
1183
|
-
}
|
|
1184
|
-
const searchResults = await Promise.all(clients.map(async (client) => {
|
|
1185
|
-
try {
|
|
1186
|
-
const hasTool = (await client.listTools()).some((t) => t.name === toolName);
|
|
1187
|
-
return {
|
|
1188
|
-
serverName: client.serverName,
|
|
1189
|
-
hasTool,
|
|
1190
|
-
error: null
|
|
1191
|
-
};
|
|
1192
|
-
} catch (error) {
|
|
1193
|
-
return {
|
|
1194
|
-
serverName: client.serverName,
|
|
1195
|
-
hasTool: false,
|
|
1196
|
-
error
|
|
1197
|
-
};
|
|
1198
|
-
}
|
|
1199
|
-
}));
|
|
1200
|
-
const matchingServers = [];
|
|
1201
|
-
for (const { serverName, hasTool, error } of searchResults) {
|
|
1202
|
-
if (error) {
|
|
1203
|
-
if (!options.json) console.error(`Failed to list tools from ${serverName}:`, error);
|
|
1204
|
-
continue;
|
|
1205
|
-
}
|
|
1206
|
-
if (hasTool) matchingServers.push(serverName);
|
|
1207
|
-
}
|
|
1208
|
-
if (matchingServers.length === 0) {
|
|
1209
|
-
const skillPaths = config.skills?.paths || [];
|
|
1210
|
-
if (skillPaths.length > 0) try {
|
|
1211
|
-
const cwd = process.env.PROJECT_PATH || process.cwd();
|
|
1212
|
-
const skillService = container.createSkillService(cwd, skillPaths);
|
|
1213
|
-
const skillName = toolName.startsWith("skill__") ? toolName.slice(7) : toolName;
|
|
1214
|
-
const skill = await skillService.getSkill(skillName);
|
|
1215
|
-
if (skill) {
|
|
1216
|
-
const result = { content: [{
|
|
1217
|
-
type: "text",
|
|
1218
|
-
text: skill.content
|
|
1219
|
-
}] };
|
|
1220
|
-
if (options.json) console.log(JSON.stringify(result, null, 2));
|
|
1221
|
-
else {
|
|
1222
|
-
console.log("\nSkill content:");
|
|
1223
|
-
console.log(skill.content);
|
|
1224
|
-
}
|
|
1225
|
-
return;
|
|
1226
|
-
}
|
|
1227
|
-
} catch (error) {
|
|
1228
|
-
if (!options.json) console.error(`Failed to lookup skill "${toolName}":`, error);
|
|
1229
|
-
}
|
|
1230
|
-
throw new Error(`Tool or skill "${toolName}" not found on any connected server or configured skill paths`);
|
|
1231
|
-
}
|
|
1232
|
-
if (matchingServers.length > 1) throw new Error(`Tool "${toolName}" found on multiple servers: ${matchingServers.join(", ")}`);
|
|
1233
|
-
const targetServer = matchingServers[0];
|
|
1234
|
-
const client = clientManager.getClient(targetServer);
|
|
1235
|
-
if (!client) throw new Error(`Internal error: Server "${targetServer}" not connected`);
|
|
1236
|
-
if (!options.json) console.error(`Executing ${toolName} on ${targetServer}...`);
|
|
1237
|
-
const requestOptions = options.timeout ? { timeout: options.timeout } : void 0;
|
|
1238
|
-
const result = await client.callTool(toolName, toolArgs, requestOptions);
|
|
1239
|
-
if (options.json) console.log(JSON.stringify(result, null, 2));
|
|
1240
|
-
else {
|
|
1241
|
-
console.log("\nResult:");
|
|
1242
|
-
if (result.content) for (const content of result.content) if (content.type === "text") console.log(content.text);
|
|
1243
|
-
else console.log(JSON.stringify(content, null, 2));
|
|
1244
|
-
if (result.isError) {
|
|
1245
|
-
console.error("\n⚠️ Tool execution returned an error");
|
|
1246
|
-
process.exit(1);
|
|
1247
|
-
}
|
|
1248
|
-
}
|
|
1249
|
-
});
|
|
1250
|
-
} catch (error) {
|
|
1251
|
-
console.error(`Error executing use-tool: ${toErrorMessage$5(error)}`);
|
|
1252
|
-
process.exit(1);
|
|
1055
|
+
} finally {
|
|
1056
|
+
await processLease?.release({ kill: false });
|
|
1057
|
+
await removeRuntimeRecord(runtimeStateService, serverId);
|
|
1253
1058
|
}
|
|
1254
|
-
});
|
|
1255
|
-
//#endregion
|
|
1256
|
-
//#region src/commands/list-resources.ts
|
|
1257
|
-
/**
|
|
1258
|
-
* ListResources Command
|
|
1259
|
-
*
|
|
1260
|
-
* DESIGN PATTERNS:
|
|
1261
|
-
* - Command pattern with Commander for CLI argument parsing
|
|
1262
|
-
* - Async/await pattern for asynchronous operations
|
|
1263
|
-
* - Error handling pattern with try-catch and proper exit codes
|
|
1264
|
-
*
|
|
1265
|
-
* CODING STANDARDS:
|
|
1266
|
-
* - Use async action handlers for asynchronous operations
|
|
1267
|
-
* - Provide clear option descriptions and default values
|
|
1268
|
-
* - Handle errors gracefully with process.exit()
|
|
1269
|
-
* - Log progress and errors to console
|
|
1270
|
-
* - Use Commander's .option() and .argument() for inputs
|
|
1271
|
-
*
|
|
1272
|
-
* AVOID:
|
|
1273
|
-
* - Synchronous blocking operations in action handlers
|
|
1274
|
-
* - Missing error handling (always use try-catch)
|
|
1275
|
-
* - Hardcoded values (use options or environment variables)
|
|
1276
|
-
* - Not exiting with appropriate exit codes on errors
|
|
1277
|
-
*/
|
|
1278
|
-
function toErrorMessage$4(error) {
|
|
1279
|
-
return error instanceof Error ? error.message : String(error);
|
|
1280
1059
|
}
|
|
1281
1060
|
/**
|
|
1282
|
-
*
|
|
1061
|
+
* Start MCP server with given transport handler
|
|
1062
|
+
* @param handler - The transport handler to start
|
|
1063
|
+
* @param onStopped - Optional cleanup callback run after signal-based shutdown
|
|
1283
1064
|
*/
|
|
1284
|
-
|
|
1065
|
+
async function startServer(handler, onStopped) {
|
|
1285
1066
|
try {
|
|
1286
|
-
await
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1067
|
+
await handler.start();
|
|
1068
|
+
} catch (error) {
|
|
1069
|
+
throw new Error(`Failed to start transport handler: ${toErrorMessage$3(error)}`);
|
|
1070
|
+
}
|
|
1071
|
+
const shutdown = async (signal) => {
|
|
1072
|
+
console.error(`\nReceived ${signal}, shutting down gracefully...`);
|
|
1073
|
+
try {
|
|
1074
|
+
await handler.stop();
|
|
1075
|
+
if (onStopped) await onStopped();
|
|
1076
|
+
process.exit(0);
|
|
1077
|
+
} catch (error) {
|
|
1078
|
+
console.error(`Failed to gracefully stop transport during ${signal}: ${toErrorMessage$3(error)}`);
|
|
1079
|
+
process.exit(1);
|
|
1080
|
+
}
|
|
1081
|
+
};
|
|
1082
|
+
process.on("SIGINT", async () => await shutdown("SIGINT"));
|
|
1083
|
+
process.on("SIGTERM", async () => await shutdown("SIGTERM"));
|
|
1084
|
+
}
|
|
1085
|
+
async function createAndStartHttpRuntime(serverOptions, config, resolvedConfigPath) {
|
|
1086
|
+
const sharedServices = await require_src.initializeSharedServices(serverOptions);
|
|
1087
|
+
const runtimeStateService = new require_src.RuntimeStateService();
|
|
1088
|
+
const shutdownToken = (0, node_crypto.randomUUID)();
|
|
1089
|
+
const runtimeServerId = serverOptions.serverId ?? require_src.generateServerId();
|
|
1090
|
+
const requestedPort = config.port;
|
|
1091
|
+
const portRange = requestedPort !== void 0 ? {
|
|
1092
|
+
min: requestedPort,
|
|
1093
|
+
max: requestedPort
|
|
1094
|
+
} : _agimon_ai_foundation_port_registry.DEFAULT_PORT_RANGE;
|
|
1095
|
+
const portLease = await createPortRegistryLease(PORT_REGISTRY_SERVICE_HTTP, config.host ?? DEFAULT_HOST, requestedPort, runtimeServerId, TRANSPORT_TYPE_HTTP, resolvedConfigPath, portRange);
|
|
1096
|
+
const runtimePort = portLease.port;
|
|
1097
|
+
const runtimeConfig = {
|
|
1098
|
+
...config,
|
|
1099
|
+
port: runtimePort
|
|
1100
|
+
};
|
|
1101
|
+
const processLease = await (0, _agimon_ai_foundation_process_registry.createProcessLease)({
|
|
1102
|
+
repositoryPath: getRegistryRepositoryPath(),
|
|
1103
|
+
serviceName: PROCESS_REGISTRY_SERVICE_HTTP,
|
|
1104
|
+
serviceType: PROCESS_REGISTRY_SERVICE_TYPE,
|
|
1105
|
+
environment: getRegistryEnvironment(),
|
|
1106
|
+
host: runtimeConfig.host ?? DEFAULT_HOST,
|
|
1107
|
+
port: runtimePort,
|
|
1108
|
+
command: process.argv[1],
|
|
1109
|
+
args: process.argv.slice(2),
|
|
1110
|
+
metadata: {
|
|
1111
|
+
transport: TRANSPORT_TYPE_HTTP,
|
|
1112
|
+
serverId: runtimeServerId,
|
|
1113
|
+
...resolvedConfigPath ? { configPath: resolvedConfigPath } : {}
|
|
1114
|
+
}
|
|
1115
|
+
});
|
|
1116
|
+
let releasePort = async () => {
|
|
1117
|
+
await releasePortLease(portLease ?? null);
|
|
1118
|
+
releasePort = async () => void 0;
|
|
1119
|
+
};
|
|
1120
|
+
const runtimeRecord = createRuntimeRecord(runtimeServerId, runtimeConfig, runtimePort, shutdownToken, resolvedConfigPath);
|
|
1121
|
+
let handler;
|
|
1122
|
+
let isStopping = false;
|
|
1123
|
+
const stopHandler = async () => {
|
|
1124
|
+
if (isStopping) return;
|
|
1125
|
+
isStopping = true;
|
|
1126
|
+
try {
|
|
1127
|
+
await stopOwnedHttpTransport(handler, runtimeStateService, runtimeRecord.serverId, processLease);
|
|
1128
|
+
await releasePort();
|
|
1129
|
+
await sharedServices.dispose();
|
|
1130
|
+
process.exit(0);
|
|
1131
|
+
} catch (error) {
|
|
1132
|
+
throw new Error(`Failed to stop HTTP runtime '${runtimeRecord.serverId}' from admin shutdown: ${toErrorMessage$3(error)}`);
|
|
1133
|
+
}
|
|
1134
|
+
};
|
|
1135
|
+
try {
|
|
1136
|
+
handler = new require_src.HttpTransportHandler(() => require_src.createSessionServer(sharedServices), runtimeConfig, createHttpAdminOptions(runtimeRecord.serverId, shutdownToken, stopHandler));
|
|
1320
1137
|
} catch (error) {
|
|
1321
|
-
|
|
1322
|
-
|
|
1138
|
+
await releasePort();
|
|
1139
|
+
await processLease.release({ kill: false });
|
|
1140
|
+
await sharedServices.dispose();
|
|
1141
|
+
throw new Error(`Failed to create HTTP runtime server: ${toErrorMessage$3(error)}`);
|
|
1323
1142
|
}
|
|
1324
|
-
});
|
|
1325
|
-
//#endregion
|
|
1326
|
-
//#region src/commands/read-resource.ts
|
|
1327
|
-
/**
|
|
1328
|
-
* ReadResource Command
|
|
1329
|
-
*
|
|
1330
|
-
* DESIGN PATTERNS:
|
|
1331
|
-
* - Command pattern with Commander for CLI argument parsing
|
|
1332
|
-
* - Async/await pattern for asynchronous operations
|
|
1333
|
-
* - Error handling pattern with try-catch and proper exit codes
|
|
1334
|
-
*
|
|
1335
|
-
* CODING STANDARDS:
|
|
1336
|
-
* - Use async action handlers for asynchronous operations
|
|
1337
|
-
* - Provide clear option descriptions and default values
|
|
1338
|
-
* - Handle errors gracefully with process.exit()
|
|
1339
|
-
* - Log progress and errors to console
|
|
1340
|
-
* - Use Commander's .option() and .argument() for inputs
|
|
1341
|
-
*
|
|
1342
|
-
* AVOID:
|
|
1343
|
-
* - Synchronous blocking operations in action handlers
|
|
1344
|
-
* - Missing error handling (always use try-catch)
|
|
1345
|
-
* - Hardcoded values (use options or environment variables)
|
|
1346
|
-
* - Not exiting with appropriate exit codes on errors
|
|
1347
|
-
*/
|
|
1348
|
-
function toErrorMessage$3(error) {
|
|
1349
|
-
return error instanceof Error ? error.message : String(error);
|
|
1350
|
-
}
|
|
1351
|
-
/**
|
|
1352
|
-
* Read a resource by URI from a connected MCP server
|
|
1353
|
-
*/
|
|
1354
|
-
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) => {
|
|
1355
1143
|
try {
|
|
1356
|
-
await
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
if (!client) throw new Error(`Server "${options.server}" not found`);
|
|
1361
|
-
if (!options.json) console.error(`Reading ${uri} from ${options.server}...`);
|
|
1362
|
-
const result = await client.readResource(uri);
|
|
1363
|
-
if (options.json) console.log(JSON.stringify(result, null, 2));
|
|
1364
|
-
else for (const content of result.contents) if ("text" in content) console.log(content.text);
|
|
1365
|
-
else console.log(JSON.stringify(content, null, 2));
|
|
1366
|
-
return;
|
|
1367
|
-
}
|
|
1368
|
-
const searchResults = await Promise.all(clients.map(async (client) => {
|
|
1369
|
-
try {
|
|
1370
|
-
const hasResource = (await client.listResources()).some((r) => r.uri === uri);
|
|
1371
|
-
return {
|
|
1372
|
-
serverName: client.serverName,
|
|
1373
|
-
hasResource,
|
|
1374
|
-
error: null
|
|
1375
|
-
};
|
|
1376
|
-
} catch (error) {
|
|
1377
|
-
return {
|
|
1378
|
-
serverName: client.serverName,
|
|
1379
|
-
hasResource: false,
|
|
1380
|
-
error
|
|
1381
|
-
};
|
|
1382
|
-
}
|
|
1383
|
-
}));
|
|
1384
|
-
const matchingServers = [];
|
|
1385
|
-
for (const { serverName, hasResource, error } of searchResults) {
|
|
1386
|
-
if (error) {
|
|
1387
|
-
console.error(`Failed to list resources from ${serverName}: ${toErrorMessage$3(error)}`);
|
|
1388
|
-
continue;
|
|
1389
|
-
}
|
|
1390
|
-
if (hasResource) matchingServers.push(serverName);
|
|
1391
|
-
}
|
|
1392
|
-
if (matchingServers.length === 0) throw new Error(`Resource "${uri}" not found on any connected server`);
|
|
1393
|
-
if (matchingServers.length > 1) throw new Error(`Resource "${uri}" found on multiple servers: ${matchingServers.join(", ")}. Use --server to disambiguate`);
|
|
1394
|
-
const targetServer = matchingServers[0];
|
|
1395
|
-
const client = clientManager.getClient(targetServer);
|
|
1396
|
-
if (!client) throw new Error(`Internal error: Server "${targetServer}" not connected`);
|
|
1397
|
-
if (!options.json) console.error(`Reading ${uri} from ${targetServer}...`);
|
|
1398
|
-
const result = await client.readResource(uri);
|
|
1399
|
-
if (options.json) console.log(JSON.stringify(result, null, 2));
|
|
1400
|
-
else for (const content of result.contents) if ("text" in content) console.log(content.text);
|
|
1401
|
-
else console.log(JSON.stringify(content, null, 2));
|
|
1144
|
+
await startServer(handler, async () => {
|
|
1145
|
+
await releasePort();
|
|
1146
|
+
await sharedServices.dispose();
|
|
1147
|
+
await cleanupHttpRuntime(runtimeStateService, runtimeRecord.serverId, processLease);
|
|
1402
1148
|
});
|
|
1149
|
+
await writeRuntimeRecord(runtimeStateService, runtimeRecord);
|
|
1403
1150
|
} catch (error) {
|
|
1404
|
-
|
|
1405
|
-
|
|
1151
|
+
await releasePort();
|
|
1152
|
+
await sharedServices.dispose();
|
|
1153
|
+
await cleanupFailedRuntimeStartup(handler, runtimeStateService, runtimeRecord.serverId, processLease);
|
|
1154
|
+
throw new Error(`Failed to start HTTP runtime '${runtimeRecord.serverId}': ${toErrorMessage$3(error)}`);
|
|
1406
1155
|
}
|
|
1407
|
-
});
|
|
1408
|
-
//#endregion
|
|
1409
|
-
//#region src/commands/list-prompts.ts
|
|
1410
|
-
function toErrorMessage$2(error) {
|
|
1411
|
-
return error instanceof Error ? error.message : String(error);
|
|
1156
|
+
console.error(`Runtime state: http://${runtimeRecord.host}:${runtimeRecord.port} (${runtimeRecord.serverId})`);
|
|
1412
1157
|
}
|
|
1413
|
-
|
|
1158
|
+
async function startStdioTransport(serverOptions) {
|
|
1414
1159
|
try {
|
|
1415
|
-
await
|
|
1416
|
-
const clients = options.server ? [clientManager.getClient(options.server)].filter((client) => client !== void 0) : clientManager.getAllClients();
|
|
1417
|
-
if (options.server && clients.length === 0) throw new Error(`Server "${options.server}" not found`);
|
|
1418
|
-
const promptsByServer = {};
|
|
1419
|
-
await Promise.all(clients.map(async (client) => {
|
|
1420
|
-
try {
|
|
1421
|
-
promptsByServer[client.serverName] = await client.listPrompts();
|
|
1422
|
-
} catch (error) {
|
|
1423
|
-
promptsByServer[client.serverName] = [];
|
|
1424
|
-
if (!options.json) console.error(`Failed to list prompts from ${client.serverName}: ${toErrorMessage$2(error)}`);
|
|
1425
|
-
}
|
|
1426
|
-
}));
|
|
1427
|
-
if (options.json) console.log(JSON.stringify(promptsByServer, null, 2));
|
|
1428
|
-
else for (const [serverName, prompts] of Object.entries(promptsByServer)) {
|
|
1429
|
-
console.log(`\n${serverName}:`);
|
|
1430
|
-
if (prompts.length === 0) {
|
|
1431
|
-
console.log(" No prompts available");
|
|
1432
|
-
continue;
|
|
1433
|
-
}
|
|
1434
|
-
for (const prompt of prompts) {
|
|
1435
|
-
console.log(` - ${prompt.name}: ${prompt.description || "No description"}`);
|
|
1436
|
-
if (prompt.arguments && prompt.arguments.length > 0) {
|
|
1437
|
-
const args = prompt.arguments.map((arg) => `${arg.name}${arg.required ? " (required)" : ""}`).join(", ");
|
|
1438
|
-
console.log(` args: ${args}`);
|
|
1439
|
-
}
|
|
1440
|
-
}
|
|
1441
|
-
}
|
|
1442
|
-
});
|
|
1160
|
+
await startServer(new require_src.StdioTransportHandler(await require_src.createServer(serverOptions), createStdioSafeLogger()));
|
|
1443
1161
|
} catch (error) {
|
|
1444
|
-
|
|
1445
|
-
process.exit(1);
|
|
1162
|
+
throw new Error(`Failed to start stdio transport: ${toErrorMessage$3(error)}`);
|
|
1446
1163
|
}
|
|
1447
|
-
});
|
|
1448
|
-
//#endregion
|
|
1449
|
-
//#region src/commands/get-prompt.ts
|
|
1450
|
-
function toErrorMessage$1(error) {
|
|
1451
|
-
return error instanceof Error ? error.message : String(error);
|
|
1452
1164
|
}
|
|
1453
|
-
|
|
1165
|
+
async function startSseTransport(serverOptions, config) {
|
|
1454
1166
|
try {
|
|
1455
|
-
|
|
1167
|
+
const requestedPort = config.port;
|
|
1168
|
+
const portRange = requestedPort !== void 0 ? {
|
|
1169
|
+
min: requestedPort,
|
|
1170
|
+
max: requestedPort
|
|
1171
|
+
} : _agimon_ai_foundation_port_registry.DEFAULT_PORT_RANGE;
|
|
1172
|
+
const portLease = await createPortRegistryLease("mcp-proxy-sse", config.host ?? DEFAULT_HOST, requestedPort, serverOptions.serverId ?? require_src.generateServerId(), TRANSPORT_TYPE_SSE, void 0, portRange);
|
|
1173
|
+
const resolvedConfig = {
|
|
1174
|
+
...config,
|
|
1175
|
+
port: portLease.port
|
|
1176
|
+
};
|
|
1177
|
+
const handler = new require_src.SseTransportHandler(await require_src.createServer(serverOptions), resolvedConfig);
|
|
1178
|
+
const shutdown = async () => {
|
|
1179
|
+
await handler.stop();
|
|
1180
|
+
await portLease.release();
|
|
1181
|
+
};
|
|
1182
|
+
process.on("SIGINT", shutdown);
|
|
1183
|
+
process.on("SIGTERM", shutdown);
|
|
1184
|
+
await startServer(handler);
|
|
1185
|
+
} catch (error) {
|
|
1186
|
+
throw new Error(`Failed to start SSE transport: ${toErrorMessage$3(error)}`);
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
async function resolveStdioHttpEndpoint(config, options, resolvedConfigPath) {
|
|
1190
|
+
const repositoryPath = getRegistryRepositoryPath();
|
|
1191
|
+
if (config.port !== void 0) return { endpoint: new URL(`http://${config.host ?? DEFAULT_HOST}:${config.port}${MCP_ENDPOINT_PATH}`) };
|
|
1192
|
+
const portRegistry = createPortRegistryService();
|
|
1193
|
+
const result = await portRegistry.getPort({
|
|
1194
|
+
repositoryPath,
|
|
1195
|
+
serviceName: PORT_REGISTRY_SERVICE_HTTP,
|
|
1196
|
+
serviceType: PORT_REGISTRY_SERVICE_TYPE,
|
|
1197
|
+
environment: getRegistryEnvironment()
|
|
1198
|
+
});
|
|
1199
|
+
if (result.success && result.record) {
|
|
1200
|
+
const host = config.host ?? result.record.host;
|
|
1201
|
+
const endpoint = new URL(`http://${host}:${result.record.port}${MCP_ENDPOINT_PATH}`);
|
|
1456
1202
|
try {
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1203
|
+
const healthUrl = `http://${host}:${result.record.port}/health`;
|
|
1204
|
+
if ((await fetch(healthUrl)).ok) return { endpoint };
|
|
1205
|
+
} catch {}
|
|
1206
|
+
await portRegistry.releasePort({
|
|
1207
|
+
repositoryPath,
|
|
1208
|
+
serviceName: PORT_REGISTRY_SERVICE_HTTP,
|
|
1209
|
+
serviceType: PORT_REGISTRY_SERVICE_TYPE,
|
|
1210
|
+
environment: getRegistryEnvironment(),
|
|
1211
|
+
force: true
|
|
1212
|
+
});
|
|
1213
|
+
}
|
|
1214
|
+
const runtime = await prestartHttpRuntime({
|
|
1215
|
+
host: config.host ?? DEFAULT_HOST,
|
|
1216
|
+
config: options.config || resolvedConfigPath,
|
|
1217
|
+
cache: options.cache,
|
|
1218
|
+
definitionsCache: options.definitionsCache,
|
|
1219
|
+
clearDefinitionsCache: options.clearDefinitionsCache,
|
|
1220
|
+
proxyMode: options.proxyMode
|
|
1221
|
+
});
|
|
1222
|
+
return {
|
|
1223
|
+
endpoint: new URL(`http://${runtime.host}:${runtime.port}${MCP_ENDPOINT_PATH}`),
|
|
1224
|
+
ownedRuntimeServerId: runtime.reusedExistingRuntime ? void 0 : runtime.serverId
|
|
1225
|
+
};
|
|
1226
|
+
}
|
|
1227
|
+
async function startStdioHttpTransport(config, options, resolvedConfigPath, proxyDefaults) {
|
|
1228
|
+
let ownedRuntimeServerId;
|
|
1229
|
+
const keepAlive = proxyDefaults?.keepAlive ?? false;
|
|
1230
|
+
try {
|
|
1231
|
+
const resolvedEndpoint = await resolveStdioHttpEndpoint(config, options, resolvedConfigPath);
|
|
1232
|
+
ownedRuntimeServerId = resolvedEndpoint.ownedRuntimeServerId;
|
|
1233
|
+
const { endpoint } = resolvedEndpoint;
|
|
1234
|
+
await startServer(new require_src.StdioHttpTransportHandler({ endpoint }, createStdioSafeLogger()), async () => {
|
|
1235
|
+
if (keepAlive || !ownedRuntimeServerId) return;
|
|
1236
|
+
await new require_src.StopServerService().stop({
|
|
1237
|
+
serverId: ownedRuntimeServerId,
|
|
1238
|
+
force: true
|
|
1239
|
+
});
|
|
1240
|
+
});
|
|
1241
|
+
} catch (error) {
|
|
1242
|
+
if (!keepAlive && ownedRuntimeServerId) {
|
|
1243
|
+
const stopServerService = new require_src.StopServerService();
|
|
1244
|
+
try {
|
|
1245
|
+
await stopServerService.stop({
|
|
1246
|
+
serverId: ownedRuntimeServerId,
|
|
1247
|
+
force: true
|
|
1248
|
+
});
|
|
1249
|
+
} catch (cleanupError) {
|
|
1250
|
+
throw new Error(`Failed to start stdio-http transport: ${toErrorMessage$3(error)}; also failed to stop owned HTTP runtime '${ownedRuntimeServerId}': ${toErrorMessage$3(cleanupError)}`);
|
|
1493
1251
|
}
|
|
1494
|
-
}
|
|
1252
|
+
}
|
|
1253
|
+
throw new Error(`Failed to start stdio-http transport: ${toErrorMessage$3(error)}`);
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
async function startTransport(transportType, options, resolvedConfigPath, serverOptions, proxyDefaults) {
|
|
1257
|
+
try {
|
|
1258
|
+
if (transportType === TRANSPORT_TYPE_STDIO) {
|
|
1259
|
+
await startStdioTransport(serverOptions);
|
|
1260
|
+
return;
|
|
1261
|
+
}
|
|
1262
|
+
if (transportType === TRANSPORT_TYPE_HTTP) {
|
|
1263
|
+
await createAndStartHttpRuntime(serverOptions, createTransportConfig(options, require_src.TRANSPORT_MODE.HTTP, proxyDefaults), resolvedConfigPath);
|
|
1264
|
+
return;
|
|
1265
|
+
}
|
|
1266
|
+
if (transportType === TRANSPORT_TYPE_SSE) {
|
|
1267
|
+
await startSseTransport(serverOptions, createTransportConfig(options, require_src.TRANSPORT_MODE.SSE, proxyDefaults));
|
|
1268
|
+
return;
|
|
1269
|
+
}
|
|
1270
|
+
await startStdioHttpTransport(createTransportConfig(options, require_src.TRANSPORT_MODE.HTTP, proxyDefaults), options, resolvedConfigPath, proxyDefaults);
|
|
1271
|
+
} catch (error) {
|
|
1272
|
+
throw new Error(`Failed to start transport '${transportType}': ${toErrorMessage$3(error)}`);
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
/**
|
|
1276
|
+
* MCP Serve command
|
|
1277
|
+
*/
|
|
1278
|
+
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}`).option("-p, --port <port>", "Port to listen on (http/sse) or backend port for stdio-http", (val) => Number.parseInt(val, 10)).option("--host <host>", "Host to bind to (http/sse) or backend host for stdio-http").option("-c, --config <path>", "Path to MCP server configuration file").option("--no-cache", "Disable configuration caching, always reload from config file").option("--definitions-cache <path>", "Path to prefetched tool/prompt/skill definitions cache file").option("--clear-definitions-cache", "Delete definitions cache before startup", false).option("--proxy-mode <mode>", "How mcp-proxy exposes downstream tools: meta, flat, or search", "meta").option("--id <id>", "Unique server identifier (overrides config file id, auto-generated if not provided)").action(async (options) => {
|
|
1279
|
+
try {
|
|
1280
|
+
const resolvedConfigPath = options.config || await findConfigFileAsync() || void 0;
|
|
1281
|
+
const proxyDefaults = resolvedConfigPath ? loadProxyDefaults(resolvedConfigPath) : {};
|
|
1282
|
+
const transportType = validateTransportType((options.type ?? proxyDefaults.type ?? TRANSPORT_TYPE_STDIO).toLowerCase());
|
|
1283
|
+
validateProxyMode(options.proxyMode);
|
|
1284
|
+
await startTransport(transportType, options, resolvedConfigPath, createServerOptions(options, resolvedConfigPath, await resolveServerId(options, resolvedConfigPath)), proxyDefaults);
|
|
1495
1285
|
} catch (error) {
|
|
1496
|
-
|
|
1286
|
+
const rawTransportType = (options.type ?? TRANSPORT_TYPE_STDIO).toLowerCase();
|
|
1287
|
+
const transportType = isValidTransportType(rawTransportType) ? rawTransportType : TRANSPORT_TYPE_STDIO;
|
|
1288
|
+
const envPort = process.env.MCP_PORT ? Number(process.env.MCP_PORT) : void 0;
|
|
1289
|
+
const requestedPort = options.port ?? (Number.isFinite(envPort) ? envPort : void 0);
|
|
1290
|
+
console.error(formatStartError(transportType, options.host ?? DEFAULT_HOST, requestedPort, error));
|
|
1497
1291
|
process.exit(1);
|
|
1498
1292
|
}
|
|
1499
1293
|
});
|
|
@@ -1624,6 +1418,89 @@ const prefetchCommand = new commander.Command("prefetch").description("Pre-downl
|
|
|
1624
1418
|
}
|
|
1625
1419
|
});
|
|
1626
1420
|
//#endregion
|
|
1421
|
+
//#region src/commands/read-resource.ts
|
|
1422
|
+
/**
|
|
1423
|
+
* ReadResource Command
|
|
1424
|
+
*
|
|
1425
|
+
* DESIGN PATTERNS:
|
|
1426
|
+
* - Command pattern with Commander for CLI argument parsing
|
|
1427
|
+
* - Async/await pattern for asynchronous operations
|
|
1428
|
+
* - Error handling pattern with try-catch and proper exit codes
|
|
1429
|
+
*
|
|
1430
|
+
* CODING STANDARDS:
|
|
1431
|
+
* - Use async action handlers for asynchronous operations
|
|
1432
|
+
* - Provide clear option descriptions and default values
|
|
1433
|
+
* - Handle errors gracefully with process.exit()
|
|
1434
|
+
* - Log progress and errors to console
|
|
1435
|
+
* - Use Commander's .option() and .argument() for inputs
|
|
1436
|
+
*
|
|
1437
|
+
* AVOID:
|
|
1438
|
+
* - Synchronous blocking operations in action handlers
|
|
1439
|
+
* - Missing error handling (always use try-catch)
|
|
1440
|
+
* - Hardcoded values (use options or environment variables)
|
|
1441
|
+
* - Not exiting with appropriate exit codes on errors
|
|
1442
|
+
*/
|
|
1443
|
+
function toErrorMessage$2(error) {
|
|
1444
|
+
return error instanceof Error ? error.message : String(error);
|
|
1445
|
+
}
|
|
1446
|
+
/**
|
|
1447
|
+
* Read a resource by URI from a connected MCP server
|
|
1448
|
+
*/
|
|
1449
|
+
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) => {
|
|
1450
|
+
try {
|
|
1451
|
+
await withConnectedCommandContext(options, async ({ clientManager }) => {
|
|
1452
|
+
const clients = clientManager.getAllClients();
|
|
1453
|
+
if (options.server) {
|
|
1454
|
+
const client = clientManager.getClient(options.server);
|
|
1455
|
+
if (!client) throw new Error(`Server "${options.server}" not found`);
|
|
1456
|
+
if (!options.json) console.error(`Reading ${uri} from ${options.server}...`);
|
|
1457
|
+
const result = await client.readResource(uri);
|
|
1458
|
+
if (options.json) console.log(JSON.stringify(result, null, 2));
|
|
1459
|
+
else for (const content of result.contents) if ("text" in content) console.log(content.text);
|
|
1460
|
+
else console.log(JSON.stringify(content, null, 2));
|
|
1461
|
+
return;
|
|
1462
|
+
}
|
|
1463
|
+
const searchResults = await Promise.all(clients.map(async (client) => {
|
|
1464
|
+
try {
|
|
1465
|
+
const hasResource = (await client.listResources()).some((r) => r.uri === uri);
|
|
1466
|
+
return {
|
|
1467
|
+
serverName: client.serverName,
|
|
1468
|
+
hasResource,
|
|
1469
|
+
error: null
|
|
1470
|
+
};
|
|
1471
|
+
} catch (error) {
|
|
1472
|
+
return {
|
|
1473
|
+
serverName: client.serverName,
|
|
1474
|
+
hasResource: false,
|
|
1475
|
+
error
|
|
1476
|
+
};
|
|
1477
|
+
}
|
|
1478
|
+
}));
|
|
1479
|
+
const matchingServers = [];
|
|
1480
|
+
for (const { serverName, hasResource, error } of searchResults) {
|
|
1481
|
+
if (error) {
|
|
1482
|
+
console.error(`Failed to list resources from ${serverName}: ${toErrorMessage$2(error)}`);
|
|
1483
|
+
continue;
|
|
1484
|
+
}
|
|
1485
|
+
if (hasResource) matchingServers.push(serverName);
|
|
1486
|
+
}
|
|
1487
|
+
if (matchingServers.length === 0) throw new Error(`Resource "${uri}" not found on any connected server`);
|
|
1488
|
+
if (matchingServers.length > 1) throw new Error(`Resource "${uri}" found on multiple servers: ${matchingServers.join(", ")}. Use --server to disambiguate`);
|
|
1489
|
+
const targetServer = matchingServers[0];
|
|
1490
|
+
const client = clientManager.getClient(targetServer);
|
|
1491
|
+
if (!client) throw new Error(`Internal error: Server "${targetServer}" not connected`);
|
|
1492
|
+
if (!options.json) console.error(`Reading ${uri} from ${targetServer}...`);
|
|
1493
|
+
const result = await client.readResource(uri);
|
|
1494
|
+
if (options.json) console.log(JSON.stringify(result, null, 2));
|
|
1495
|
+
else for (const content of result.contents) if ("text" in content) console.log(content.text);
|
|
1496
|
+
else console.log(JSON.stringify(content, null, 2));
|
|
1497
|
+
});
|
|
1498
|
+
} catch (error) {
|
|
1499
|
+
console.error(`Error executing read-resource: ${toErrorMessage$2(error)}`);
|
|
1500
|
+
process.exit(1);
|
|
1501
|
+
}
|
|
1502
|
+
});
|
|
1503
|
+
//#endregion
|
|
1627
1504
|
//#region src/commands/stop.ts
|
|
1628
1505
|
/**
|
|
1629
1506
|
* Stop Command
|
|
@@ -1631,7 +1508,7 @@ const prefetchCommand = new commander.Command("prefetch").description("Pre-downl
|
|
|
1631
1508
|
* Stops a running HTTP mcp-proxy server using the authenticated admin endpoint
|
|
1632
1509
|
* and the persisted runtime registry.
|
|
1633
1510
|
*/
|
|
1634
|
-
function toErrorMessage(error) {
|
|
1511
|
+
function toErrorMessage$1(error) {
|
|
1635
1512
|
return error instanceof Error ? error.message : String(error);
|
|
1636
1513
|
}
|
|
1637
1514
|
function printStopResult(result) {
|
|
@@ -1659,7 +1536,7 @@ const stopCommand = new commander.Command("stop").description("Stop a running HT
|
|
|
1659
1536
|
}
|
|
1660
1537
|
printStopResult(result);
|
|
1661
1538
|
} catch (error) {
|
|
1662
|
-
const errorMessage = `Error executing stop: ${toErrorMessage(error)}`;
|
|
1539
|
+
const errorMessage = `Error executing stop: ${toErrorMessage$1(error)}`;
|
|
1663
1540
|
if (options.json) console.log(JSON.stringify({
|
|
1664
1541
|
ok: false,
|
|
1665
1542
|
error: errorMessage
|
|
@@ -1669,6 +1546,135 @@ const stopCommand = new commander.Command("stop").description("Stop a running HT
|
|
|
1669
1546
|
}
|
|
1670
1547
|
});
|
|
1671
1548
|
//#endregion
|
|
1549
|
+
//#region src/commands/use-tool.ts
|
|
1550
|
+
/**
|
|
1551
|
+
* Use Tool Command
|
|
1552
|
+
*
|
|
1553
|
+
* DESIGN PATTERNS:
|
|
1554
|
+
* - Command pattern with Commander for CLI argument parsing
|
|
1555
|
+
* - Async/await pattern for asynchronous operations
|
|
1556
|
+
* - Error handling pattern with try-catch and proper exit codes
|
|
1557
|
+
*
|
|
1558
|
+
* CODING STANDARDS:
|
|
1559
|
+
* - Use async action handlers for asynchronous operations
|
|
1560
|
+
* - Provide clear option descriptions and default values
|
|
1561
|
+
* - Handle errors gracefully with process.exit()
|
|
1562
|
+
* - Log progress and errors to console
|
|
1563
|
+
* - Use Commander'"'"'s .option() and .argument() for inputs
|
|
1564
|
+
*
|
|
1565
|
+
* AVOID:
|
|
1566
|
+
* - Synchronous blocking operations in action handlers
|
|
1567
|
+
* - Missing error handling (always use try-catch)
|
|
1568
|
+
* - Hardcoded values (use options or environment variables)
|
|
1569
|
+
* - Not exiting with appropriate exit codes on errors
|
|
1570
|
+
*/
|
|
1571
|
+
function toErrorMessage(error) {
|
|
1572
|
+
return error instanceof Error ? error.message : String(error);
|
|
1573
|
+
}
|
|
1574
|
+
/**
|
|
1575
|
+
* Execute an MCP tool with arguments
|
|
1576
|
+
*/
|
|
1577
|
+
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)", Number.parseInt).option("-j, --json", "Output as JSON", false).action(async (toolName, options) => {
|
|
1578
|
+
try {
|
|
1579
|
+
let toolArgs = {};
|
|
1580
|
+
try {
|
|
1581
|
+
toolArgs = JSON.parse(options.args);
|
|
1582
|
+
} catch {
|
|
1583
|
+
console.error("Error: Invalid JSON in --args");
|
|
1584
|
+
process.exit(1);
|
|
1585
|
+
}
|
|
1586
|
+
await withConnectedCommandContext(options, async ({ container, config, clientManager }) => {
|
|
1587
|
+
const clients = clientManager.getAllClients();
|
|
1588
|
+
if (options.server) {
|
|
1589
|
+
const client = clientManager.getClient(options.server);
|
|
1590
|
+
if (!client) throw new Error(`Server "${options.server}" not found`);
|
|
1591
|
+
if (!options.json) console.error(`Executing ${toolName} on ${options.server}...`);
|
|
1592
|
+
const requestOptions = options.timeout ? { timeout: options.timeout } : void 0;
|
|
1593
|
+
const result = await client.callTool(toolName, toolArgs, requestOptions);
|
|
1594
|
+
if (options.json) console.log(JSON.stringify(result, null, 2));
|
|
1595
|
+
else {
|
|
1596
|
+
console.log("\nResult:");
|
|
1597
|
+
if (result.content) for (const content of result.content) if (content.type === "text") console.log(content.text);
|
|
1598
|
+
else console.log(JSON.stringify(content, null, 2));
|
|
1599
|
+
if (result.isError) {
|
|
1600
|
+
console.error("\n⚠️ Tool execution returned an error");
|
|
1601
|
+
process.exit(1);
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
return;
|
|
1605
|
+
}
|
|
1606
|
+
const searchResults = await Promise.all(clients.map(async (client) => {
|
|
1607
|
+
try {
|
|
1608
|
+
const hasTool = (await client.listTools()).some((t) => t.name === toolName);
|
|
1609
|
+
return {
|
|
1610
|
+
serverName: client.serverName,
|
|
1611
|
+
hasTool,
|
|
1612
|
+
error: null
|
|
1613
|
+
};
|
|
1614
|
+
} catch (error) {
|
|
1615
|
+
return {
|
|
1616
|
+
serverName: client.serverName,
|
|
1617
|
+
hasTool: false,
|
|
1618
|
+
error
|
|
1619
|
+
};
|
|
1620
|
+
}
|
|
1621
|
+
}));
|
|
1622
|
+
const matchingServers = [];
|
|
1623
|
+
for (const { serverName, hasTool, error } of searchResults) {
|
|
1624
|
+
if (error) {
|
|
1625
|
+
if (!options.json) console.error(`Failed to list tools from ${serverName}:`, error);
|
|
1626
|
+
continue;
|
|
1627
|
+
}
|
|
1628
|
+
if (hasTool) matchingServers.push(serverName);
|
|
1629
|
+
}
|
|
1630
|
+
if (matchingServers.length === 0) {
|
|
1631
|
+
const skillPaths = config.skills?.paths || [];
|
|
1632
|
+
if (skillPaths.length > 0) try {
|
|
1633
|
+
const cwd = process.env.PROJECT_PATH || process.cwd();
|
|
1634
|
+
const skillService = container.createSkillService(cwd, skillPaths);
|
|
1635
|
+
const skillName = toolName.startsWith("skill__") ? toolName.slice(7) : toolName;
|
|
1636
|
+
const skill = await skillService.getSkill(skillName);
|
|
1637
|
+
if (skill) {
|
|
1638
|
+
const result = { content: [{
|
|
1639
|
+
type: "text",
|
|
1640
|
+
text: skill.content
|
|
1641
|
+
}] };
|
|
1642
|
+
if (options.json) console.log(JSON.stringify(result, null, 2));
|
|
1643
|
+
else {
|
|
1644
|
+
console.log("\nSkill content:");
|
|
1645
|
+
console.log(skill.content);
|
|
1646
|
+
}
|
|
1647
|
+
return;
|
|
1648
|
+
}
|
|
1649
|
+
} catch (error) {
|
|
1650
|
+
if (!options.json) console.error(`Failed to lookup skill "${toolName}":`, error);
|
|
1651
|
+
}
|
|
1652
|
+
throw new Error(`Tool or skill "${toolName}" not found on any connected server or configured skill paths`);
|
|
1653
|
+
}
|
|
1654
|
+
if (matchingServers.length > 1) throw new Error(`Tool "${toolName}" found on multiple servers: ${matchingServers.join(", ")}`);
|
|
1655
|
+
const targetServer = matchingServers[0];
|
|
1656
|
+
const client = clientManager.getClient(targetServer);
|
|
1657
|
+
if (!client) throw new Error(`Internal error: Server "${targetServer}" not connected`);
|
|
1658
|
+
if (!options.json) console.error(`Executing ${toolName} on ${targetServer}...`);
|
|
1659
|
+
const requestOptions = options.timeout ? { timeout: options.timeout } : void 0;
|
|
1660
|
+
const result = await client.callTool(toolName, toolArgs, requestOptions);
|
|
1661
|
+
if (options.json) console.log(JSON.stringify(result, null, 2));
|
|
1662
|
+
else {
|
|
1663
|
+
console.log("\nResult:");
|
|
1664
|
+
if (result.content) for (const content of result.content) if (content.type === "text") console.log(content.text);
|
|
1665
|
+
else console.log(JSON.stringify(content, null, 2));
|
|
1666
|
+
if (result.isError) {
|
|
1667
|
+
console.error("\n⚠️ Tool execution returned an error");
|
|
1668
|
+
process.exit(1);
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
});
|
|
1672
|
+
} catch (error) {
|
|
1673
|
+
console.error(`Error executing use-tool: ${toErrorMessage(error)}`);
|
|
1674
|
+
process.exit(1);
|
|
1675
|
+
}
|
|
1676
|
+
});
|
|
1677
|
+
//#endregion
|
|
1672
1678
|
//#region src/cli.ts
|
|
1673
1679
|
/**
|
|
1674
1680
|
* MCP Server Entry Point
|