@agimon-ai/mcp-proxy 0.5.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.cjs +225 -109
- package/dist/cli.mjs +225 -110
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +76 -62
- package/dist/index.d.mts +76 -62
- package/dist/index.mjs +1 -1
- package/dist/{src-B2m53VQ1.cjs → src-B5N-kt9Y.cjs} +2967 -2804
- package/dist/{src-DCIv5S_2.mjs → src-DQSfFKFP.mjs} +2970 -2806
- package/package.json +9 -6
package/dist/cli.cjs
CHANGED
|
@@ -1,17 +1,27 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
const require_src = require('./src-
|
|
3
|
-
let
|
|
2
|
+
const require_src = require('./src-B5N-kt9Y.cjs');
|
|
3
|
+
let node_fs = require("node:fs");
|
|
4
4
|
let node_fs_promises = require("node:fs/promises");
|
|
5
|
+
let js_yaml = require("js-yaml");
|
|
6
|
+
js_yaml = require_src.__toESM(js_yaml);
|
|
7
|
+
let node_crypto = require("node:crypto");
|
|
5
8
|
let node_path = require("node:path");
|
|
6
9
|
node_path = require_src.__toESM(node_path);
|
|
7
|
-
let node_fs = require("node:fs");
|
|
8
|
-
let liquidjs = require("liquidjs");
|
|
9
10
|
let node_child_process = require("node:child_process");
|
|
11
|
+
let liquidjs = require("liquidjs");
|
|
10
12
|
let commander = require("commander");
|
|
11
13
|
let __agimon_ai_foundation_port_registry = require("@agimon-ai/foundation-port-registry");
|
|
12
14
|
let __agimon_ai_foundation_process_registry = require("@agimon-ai/foundation-process-registry");
|
|
13
15
|
let node_url = require("node:url");
|
|
14
16
|
|
|
17
|
+
//#region src/templates/mcp-config.json?raw
|
|
18
|
+
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";
|
|
19
|
+
|
|
20
|
+
//#endregion
|
|
21
|
+
//#region src/templates/mcp-config.yaml.liquid?raw
|
|
22
|
+
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";
|
|
23
|
+
|
|
24
|
+
//#endregion
|
|
15
25
|
//#region src/utils/output.ts
|
|
16
26
|
function writeLine(message = "") {
|
|
17
27
|
console.log(message);
|
|
@@ -35,14 +45,6 @@ const print = {
|
|
|
35
45
|
indent: (message) => writeLine(` ${message}`)
|
|
36
46
|
};
|
|
37
47
|
|
|
38
|
-
//#endregion
|
|
39
|
-
//#region src/templates/mcp-config.yaml.liquid?raw
|
|
40
|
-
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";
|
|
41
|
-
|
|
42
|
-
//#endregion
|
|
43
|
-
//#region src/templates/mcp-config.json?raw
|
|
44
|
-
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";
|
|
45
|
-
|
|
46
48
|
//#endregion
|
|
47
49
|
//#region src/commands/init.ts
|
|
48
50
|
/**
|
|
@@ -138,11 +140,26 @@ function resolveWorkspaceRoot(startPath = process.env.PROJECT_PATH || process.cw
|
|
|
138
140
|
current = parent;
|
|
139
141
|
}
|
|
140
142
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
143
|
+
const PROCESS_REGISTRY_SERVICE_HTTP$1 = "mcp-proxy-http";
|
|
144
|
+
async function findExistingHealthyRuntime(workspaceRoot) {
|
|
145
|
+
const match = (await new __agimon_ai_foundation_process_registry.ProcessRegistryService(process.env.PROCESS_REGISTRY_PATH).listProcesses({
|
|
146
|
+
repositoryPath: workspaceRoot,
|
|
147
|
+
serviceName: PROCESS_REGISTRY_SERVICE_HTTP$1
|
|
148
|
+
}))[0];
|
|
149
|
+
if (!match?.host || !match?.port) return null;
|
|
150
|
+
try {
|
|
151
|
+
const healthUrl = `http://${match.host}:${match.port}/health`;
|
|
152
|
+
if ((await fetch(healthUrl)).ok) {
|
|
153
|
+
const metadata = match.metadata;
|
|
154
|
+
return {
|
|
155
|
+
host: match.host,
|
|
156
|
+
port: match.port,
|
|
157
|
+
serverId: metadata?.serverId ?? "unknown",
|
|
158
|
+
workspaceRoot
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
} catch {}
|
|
162
|
+
return null;
|
|
146
163
|
}
|
|
147
164
|
function buildCliCandidates() {
|
|
148
165
|
const __filename$1 = (0, node_url.fileURLToPath)(require("url").pathToFileURL(__filename).href);
|
|
@@ -217,16 +234,38 @@ function spawnBackgroundRuntime(args, env, cwd) {
|
|
|
217
234
|
child.unref();
|
|
218
235
|
return child;
|
|
219
236
|
}
|
|
237
|
+
async function stopExistingRuntime(runtimeStateService, serverId, host, port) {
|
|
238
|
+
const runtimes = await runtimeStateService.list();
|
|
239
|
+
const targetHost = host || DEFAULT_HOST$1;
|
|
240
|
+
const match = runtimes.find((r) => {
|
|
241
|
+
if (serverId && r.serverId === serverId) return true;
|
|
242
|
+
if (port !== void 0 && r.host === targetHost && r.port === port) return true;
|
|
243
|
+
return false;
|
|
244
|
+
});
|
|
245
|
+
if (!match) return;
|
|
246
|
+
const stopService = new require_src.StopServerService(runtimeStateService);
|
|
247
|
+
try {
|
|
248
|
+
await stopService.stop({
|
|
249
|
+
serverId: match.serverId,
|
|
250
|
+
force: true
|
|
251
|
+
});
|
|
252
|
+
} catch {
|
|
253
|
+
await runtimeStateService.remove(match.serverId);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
220
256
|
async function prestartHttpRuntime(options) {
|
|
221
257
|
const serverId = options.id || require_src.generateServerId();
|
|
222
258
|
const timeoutMs = parseTimeoutMs(options.timeoutMs);
|
|
223
259
|
const registryPath = options.registryPath || options.registryDir;
|
|
224
260
|
const workspaceRoot = resolveWorkspaceRoot();
|
|
261
|
+
const existing = await findExistingHealthyRuntime(workspaceRoot);
|
|
262
|
+
if (existing) return existing;
|
|
263
|
+
await stopExistingRuntime(new require_src.RuntimeStateService(), options.id, options.host, options.port);
|
|
225
264
|
const childEnv = {
|
|
226
265
|
...process.env,
|
|
227
266
|
...registryPath ? {
|
|
228
267
|
PORT_REGISTRY_PATH: registryPath,
|
|
229
|
-
PROCESS_REGISTRY_PATH: resolveSiblingRegistryPath(registryPath, "processes.json")
|
|
268
|
+
PROCESS_REGISTRY_PATH: (0, __agimon_ai_foundation_process_registry.resolveSiblingRegistryPath)(registryPath, "processes.json")
|
|
230
269
|
} : {}
|
|
231
270
|
};
|
|
232
271
|
const child = spawnBackgroundRuntime([
|
|
@@ -261,7 +300,7 @@ async function prestartHttpRuntime(options) {
|
|
|
261
300
|
workspaceRoot
|
|
262
301
|
};
|
|
263
302
|
} catch (error) {
|
|
264
|
-
throw new Error(`Failed to prestart HTTP runtime '${serverId}': ${error instanceof Error ? error.message : String(error)}
|
|
303
|
+
throw new Error(`Failed to prestart HTTP runtime '${serverId}': ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
265
304
|
}
|
|
266
305
|
}
|
|
267
306
|
const prestartHttpCommand = new commander.Command("prestart-http").description("Start an mcp-proxy HTTP runtime in the background and wait until it is healthy").option("--id <id>", "Server identifier to assign to the runtime").option("--host <host>", "Host to bind to", DEFAULT_HOST$1).option("-p, --port <port>", "Preferred HTTP port for the runtime", (value) => Number.parseInt(value, 10)).option("-c, --config <path>", "Path to MCP server configuration file").option("--no-cache", "Disable configuration caching, always reload from config file").option("--definitions-cache <path>", "Path to prefetched tool/prompt/skill definitions cache file").option("--clear-definitions-cache", "Delete definitions cache before startup", false).option("--proxy-mode <mode>", "How mcp-proxy exposes downstream tools: meta, flat, or search", "meta").option("--registry-path <path>", "Custom registry path or directory for service discovery").option("--registry-dir <path>", "Custom registry directory for service discovery").option("--timeout-ms <ms>", "How long to wait for the runtime to become healthy", String(DEFAULT_TIMEOUT_MS)).action(async (options) => {
|
|
@@ -272,7 +311,7 @@ const prestartHttpCommand = new commander.Command("prestart-http").description("
|
|
|
272
311
|
process.stdout.write(`runtimeUrl=http://${host}:${port}\n`);
|
|
273
312
|
process.stdout.write(`workspaceRoot=${workspaceRoot}\n`);
|
|
274
313
|
} catch (error) {
|
|
275
|
-
throw new Error(`Failed to prestart HTTP runtime '${options.id || "generated-server-id"}': ${error instanceof Error ? error.message : String(error)}
|
|
314
|
+
throw new Error(`Failed to prestart HTTP runtime '${options.id || "generated-server-id"}': ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
276
315
|
}
|
|
277
316
|
});
|
|
278
317
|
|
|
@@ -344,16 +383,37 @@ async function findConfigFileAsync() {
|
|
|
344
383
|
const configPath = (0, node_path.resolve)(projectPath, fileName);
|
|
345
384
|
if (await pathExists(configPath)) return configPath;
|
|
346
385
|
}
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
386
|
+
const MAX_PARENT_LEVELS = 3;
|
|
387
|
+
let searchDir = process.cwd();
|
|
388
|
+
for (let level = 0; level <= MAX_PARENT_LEVELS; level++) {
|
|
389
|
+
for (const fileName of CONFIG_FILE_NAMES) {
|
|
390
|
+
const configPath = (0, node_path.join)(searchDir, fileName);
|
|
391
|
+
if (await pathExists(configPath)) return configPath;
|
|
392
|
+
}
|
|
393
|
+
const parentDir = (0, node_path.dirname)(searchDir);
|
|
394
|
+
if (parentDir === searchDir) break;
|
|
395
|
+
searchDir = parentDir;
|
|
351
396
|
}
|
|
352
397
|
return null;
|
|
353
398
|
} catch (error) {
|
|
354
399
|
throw new Error(`Failed to discover MCP config file: ${toErrorMessage$9(error)}`);
|
|
355
400
|
}
|
|
356
401
|
}
|
|
402
|
+
function loadProxyDefaults(configPath) {
|
|
403
|
+
try {
|
|
404
|
+
const content = (0, node_fs.readFileSync)(configPath, "utf-8");
|
|
405
|
+
const proxy = (configPath.endsWith(".yaml") || configPath.endsWith(".yml") ? js_yaml.default.load(content) : JSON.parse(content))?.proxy;
|
|
406
|
+
if (!proxy || typeof proxy !== "object") return {};
|
|
407
|
+
const p = proxy;
|
|
408
|
+
return {
|
|
409
|
+
type: typeof p.type === "string" ? p.type : void 0,
|
|
410
|
+
port: typeof p.port === "number" && Number.isInteger(p.port) && p.port > 0 ? p.port : void 0,
|
|
411
|
+
host: typeof p.host === "string" ? p.host : void 0
|
|
412
|
+
};
|
|
413
|
+
} catch {
|
|
414
|
+
return {};
|
|
415
|
+
}
|
|
416
|
+
}
|
|
357
417
|
async function resolveServerId(options, resolvedConfigPath) {
|
|
358
418
|
const container = require_src.createProxyIoCContainer();
|
|
359
419
|
if (options.id) return options.id;
|
|
@@ -375,12 +435,12 @@ function validateTransportType(type) {
|
|
|
375
435
|
function validateProxyMode(mode) {
|
|
376
436
|
if (!isValidProxyMode(mode)) throw new Error(`Unknown proxy mode: '${mode}'. Valid options: meta, flat, search`);
|
|
377
437
|
}
|
|
378
|
-
function createTransportConfig(options, mode) {
|
|
438
|
+
function createTransportConfig(options, mode, proxyDefaults) {
|
|
379
439
|
const envPort = process.env.MCP_PORT ? Number(process.env.MCP_PORT) : void 0;
|
|
380
440
|
return {
|
|
381
441
|
mode,
|
|
382
|
-
port: options.port ?? (Number.isFinite(envPort) ? envPort : void 0),
|
|
383
|
-
host: options.host
|
|
442
|
+
port: options.port ?? (Number.isFinite(envPort) ? envPort : void 0) ?? proxyDefaults?.port,
|
|
443
|
+
host: options.host ?? process.env.MCP_HOST ?? proxyDefaults?.host ?? DEFAULT_HOST
|
|
384
444
|
};
|
|
385
445
|
}
|
|
386
446
|
function createStdioSafeLogger() {
|
|
@@ -429,9 +489,6 @@ function createRuntimeRecord(serverId, config, port, shutdownToken, configPath)
|
|
|
429
489
|
function createPortRegistryService() {
|
|
430
490
|
return new __agimon_ai_foundation_port_registry.PortRegistryService(process.env.PORT_REGISTRY_PATH);
|
|
431
491
|
}
|
|
432
|
-
function createProcessRegistryService() {
|
|
433
|
-
return new __agimon_ai_foundation_process_registry.ProcessRegistryService(process.env.PROCESS_REGISTRY_PATH);
|
|
434
|
-
}
|
|
435
492
|
function getRegistryEnvironment() {
|
|
436
493
|
return process.env.NODE_ENV ?? "development";
|
|
437
494
|
}
|
|
@@ -478,41 +535,6 @@ async function createPortRegistryLease(serviceName, host, preferredPort, serverI
|
|
|
478
535
|
}
|
|
479
536
|
};
|
|
480
537
|
}
|
|
481
|
-
async function createProcessRegistryLease(serviceName, host, port, serverId, transport, configPath) {
|
|
482
|
-
const processRegistry = createProcessRegistryService();
|
|
483
|
-
const result = await processRegistry.registerProcess({
|
|
484
|
-
repositoryPath: getRegistryRepositoryPath(),
|
|
485
|
-
serviceName,
|
|
486
|
-
serviceType: PROCESS_REGISTRY_SERVICE_TYPE,
|
|
487
|
-
environment: getRegistryEnvironment(),
|
|
488
|
-
pid: process.pid,
|
|
489
|
-
host,
|
|
490
|
-
port,
|
|
491
|
-
command: process.argv[1],
|
|
492
|
-
args: process.argv.slice(2),
|
|
493
|
-
metadata: {
|
|
494
|
-
transport,
|
|
495
|
-
serverId,
|
|
496
|
-
...configPath ? { configPath } : {}
|
|
497
|
-
}
|
|
498
|
-
});
|
|
499
|
-
if (!result.success || !result.record) throw new Error(result.error || `Failed to register process for ${serviceName}`);
|
|
500
|
-
let released = false;
|
|
501
|
-
return { release: async (options) => {
|
|
502
|
-
if (released) return;
|
|
503
|
-
released = true;
|
|
504
|
-
const releaseResult = await processRegistry.releaseProcess({
|
|
505
|
-
repositoryPath: getRegistryRepositoryPath(),
|
|
506
|
-
serviceName,
|
|
507
|
-
serviceType: PROCESS_REGISTRY_SERVICE_TYPE,
|
|
508
|
-
pid: process.pid,
|
|
509
|
-
environment: getRegistryEnvironment(),
|
|
510
|
-
kill: options?.kill ?? false,
|
|
511
|
-
releasePort: options?.releasePort ?? false
|
|
512
|
-
});
|
|
513
|
-
if (!releaseResult.success && releaseResult.error && !releaseResult.error.includes("No matching process entry")) throw new Error(releaseResult.error || `Failed to release process for ${serviceName}`);
|
|
514
|
-
} };
|
|
515
|
-
}
|
|
516
538
|
async function releasePortLease(lease) {
|
|
517
539
|
if (!lease) return;
|
|
518
540
|
await lease.release();
|
|
@@ -546,13 +568,19 @@ async function stopOwnedHttpTransport(handler, runtimeStateService, serverId, pr
|
|
|
546
568
|
throw new Error(`Failed to stop owned HTTP transport '${serverId}': ${toErrorMessage$9(error)}`);
|
|
547
569
|
}
|
|
548
570
|
} finally {
|
|
549
|
-
await processLease?.release({
|
|
550
|
-
kill: false,
|
|
551
|
-
releasePort: false
|
|
552
|
-
});
|
|
571
|
+
await processLease?.release({ kill: false });
|
|
553
572
|
await removeRuntimeRecord(runtimeStateService, serverId);
|
|
554
573
|
}
|
|
555
574
|
}
|
|
575
|
+
/**
|
|
576
|
+
* Run post-stop cleanup for an HTTP runtime (release port, dispose services, remove state).
|
|
577
|
+
* This is the subset of stopOwnedHttpTransport that runs AFTER handler.stop() has already
|
|
578
|
+
* been called by startServer()'s signal handler — avoids double-stopping the transport.
|
|
579
|
+
*/
|
|
580
|
+
async function cleanupHttpRuntime(runtimeStateService, serverId, processLease) {
|
|
581
|
+
await processLease?.release({ kill: false });
|
|
582
|
+
await removeRuntimeRecord(runtimeStateService, serverId);
|
|
583
|
+
}
|
|
556
584
|
async function cleanupFailedRuntimeStartup(handler, runtimeStateService, serverId, processLease) {
|
|
557
585
|
try {
|
|
558
586
|
try {
|
|
@@ -561,10 +589,7 @@ async function cleanupFailedRuntimeStartup(handler, runtimeStateService, serverI
|
|
|
561
589
|
throw new Error(`Failed to stop HTTP transport during cleanup for '${serverId}': ${toErrorMessage$9(error)}`);
|
|
562
590
|
}
|
|
563
591
|
} finally {
|
|
564
|
-
await processLease?.release({
|
|
565
|
-
kill: false,
|
|
566
|
-
releasePort: false
|
|
567
|
-
});
|
|
592
|
+
await processLease?.release({ kill: false });
|
|
568
593
|
await removeRuntimeRecord(runtimeStateService, serverId);
|
|
569
594
|
}
|
|
570
595
|
}
|
|
@@ -609,7 +634,21 @@ async function createAndStartHttpRuntime(serverOptions, config, resolvedConfigPa
|
|
|
609
634
|
...config,
|
|
610
635
|
port: runtimePort
|
|
611
636
|
};
|
|
612
|
-
const processLease = await
|
|
637
|
+
const processLease = await (0, __agimon_ai_foundation_process_registry.createProcessLease)({
|
|
638
|
+
repositoryPath: getRegistryRepositoryPath(),
|
|
639
|
+
serviceName: PROCESS_REGISTRY_SERVICE_HTTP,
|
|
640
|
+
serviceType: PROCESS_REGISTRY_SERVICE_TYPE,
|
|
641
|
+
environment: getRegistryEnvironment(),
|
|
642
|
+
host: runtimeConfig.host ?? DEFAULT_HOST,
|
|
643
|
+
port: runtimePort,
|
|
644
|
+
command: process.argv[1],
|
|
645
|
+
args: process.argv.slice(2),
|
|
646
|
+
metadata: {
|
|
647
|
+
transport: TRANSPORT_TYPE_HTTP,
|
|
648
|
+
serverId: runtimeServerId,
|
|
649
|
+
...resolvedConfigPath ? { configPath: resolvedConfigPath } : {}
|
|
650
|
+
}
|
|
651
|
+
});
|
|
613
652
|
let releasePort = async () => {
|
|
614
653
|
await releasePortLease(portLease ?? null);
|
|
615
654
|
releasePort = async () => void 0;
|
|
@@ -633,10 +672,7 @@ async function createAndStartHttpRuntime(serverOptions, config, resolvedConfigPa
|
|
|
633
672
|
handler = new require_src.HttpTransportHandler(() => require_src.createSessionServer(sharedServices), runtimeConfig, createHttpAdminOptions(runtimeRecord.serverId, shutdownToken, stopHandler));
|
|
634
673
|
} catch (error) {
|
|
635
674
|
await releasePort();
|
|
636
|
-
await processLease.release({
|
|
637
|
-
kill: false,
|
|
638
|
-
releasePort: false
|
|
639
|
-
});
|
|
675
|
+
await processLease.release({ kill: false });
|
|
640
676
|
await sharedServices.dispose();
|
|
641
677
|
throw new Error(`Failed to create HTTP runtime server: ${toErrorMessage$9(error)}`);
|
|
642
678
|
}
|
|
@@ -644,19 +680,11 @@ async function createAndStartHttpRuntime(serverOptions, config, resolvedConfigPa
|
|
|
644
680
|
await startServer(handler, async () => {
|
|
645
681
|
await releasePort();
|
|
646
682
|
await sharedServices.dispose();
|
|
647
|
-
await processLease
|
|
648
|
-
kill: false,
|
|
649
|
-
releasePort: false
|
|
650
|
-
});
|
|
651
|
-
await removeRuntimeRecord(runtimeStateService, runtimeRecord.serverId);
|
|
683
|
+
await cleanupHttpRuntime(runtimeStateService, runtimeRecord.serverId, processLease);
|
|
652
684
|
});
|
|
653
685
|
await writeRuntimeRecord(runtimeStateService, runtimeRecord);
|
|
654
686
|
} catch (error) {
|
|
655
687
|
await releasePort();
|
|
656
|
-
await processLease.release({
|
|
657
|
-
kill: false,
|
|
658
|
-
releasePort: false
|
|
659
|
-
});
|
|
660
688
|
await sharedServices.dispose();
|
|
661
689
|
await cleanupFailedRuntimeStartup(handler, runtimeStateService, runtimeRecord.serverId, processLease);
|
|
662
690
|
throw new Error(`Failed to start HTTP runtime '${runtimeRecord.serverId}': ${toErrorMessage$9(error)}`);
|
|
@@ -672,7 +700,24 @@ async function startStdioTransport(serverOptions) {
|
|
|
672
700
|
}
|
|
673
701
|
async function startSseTransport(serverOptions, config) {
|
|
674
702
|
try {
|
|
675
|
-
|
|
703
|
+
const requestedPort = config.port;
|
|
704
|
+
const portRange = requestedPort !== void 0 ? {
|
|
705
|
+
min: requestedPort,
|
|
706
|
+
max: requestedPort
|
|
707
|
+
} : __agimon_ai_foundation_port_registry.DEFAULT_PORT_RANGE;
|
|
708
|
+
const portLease = await createPortRegistryLease("mcp-proxy-sse", config.host ?? DEFAULT_HOST, requestedPort, serverOptions.serverId ?? require_src.generateServerId(), TRANSPORT_TYPE_SSE, void 0, portRange);
|
|
709
|
+
const resolvedConfig = {
|
|
710
|
+
...config,
|
|
711
|
+
port: portLease.port
|
|
712
|
+
};
|
|
713
|
+
const handler = new require_src.SseTransportHandler(await require_src.createServer(serverOptions), resolvedConfig);
|
|
714
|
+
const shutdown = async () => {
|
|
715
|
+
await handler.stop();
|
|
716
|
+
await portLease.release();
|
|
717
|
+
};
|
|
718
|
+
process.on("SIGINT", shutdown);
|
|
719
|
+
process.on("SIGTERM", shutdown);
|
|
720
|
+
await startServer(handler);
|
|
676
721
|
} catch (error) {
|
|
677
722
|
throw new Error(`Failed to start SSE transport: ${toErrorMessage$9(error)}`);
|
|
678
723
|
}
|
|
@@ -680,13 +725,28 @@ async function startSseTransport(serverOptions, config) {
|
|
|
680
725
|
async function resolveStdioHttpEndpoint(config, options, resolvedConfigPath) {
|
|
681
726
|
const repositoryPath = getRegistryRepositoryPath();
|
|
682
727
|
if (config.port !== void 0) return new URL(`http://${config.host ?? DEFAULT_HOST}:${config.port}${MCP_ENDPOINT_PATH}`);
|
|
683
|
-
const
|
|
728
|
+
const portRegistry = createPortRegistryService();
|
|
729
|
+
const result = await portRegistry.getPort({
|
|
684
730
|
repositoryPath,
|
|
685
731
|
serviceName: PORT_REGISTRY_SERVICE_HTTP,
|
|
686
732
|
serviceType: PORT_REGISTRY_SERVICE_TYPE,
|
|
687
733
|
environment: getRegistryEnvironment()
|
|
688
734
|
});
|
|
689
|
-
if (result.success && result.record)
|
|
735
|
+
if (result.success && result.record) {
|
|
736
|
+
const host = config.host ?? result.record.host;
|
|
737
|
+
const endpoint = new URL(`http://${host}:${result.record.port}${MCP_ENDPOINT_PATH}`);
|
|
738
|
+
try {
|
|
739
|
+
const healthUrl = `http://${host}:${result.record.port}/health`;
|
|
740
|
+
if ((await fetch(healthUrl)).ok) return endpoint;
|
|
741
|
+
} catch {}
|
|
742
|
+
await portRegistry.releasePort({
|
|
743
|
+
repositoryPath,
|
|
744
|
+
serviceName: PORT_REGISTRY_SERVICE_HTTP,
|
|
745
|
+
serviceType: PORT_REGISTRY_SERVICE_TYPE,
|
|
746
|
+
environment: getRegistryEnvironment(),
|
|
747
|
+
force: true
|
|
748
|
+
});
|
|
749
|
+
}
|
|
690
750
|
const runtime = await prestartHttpRuntime({
|
|
691
751
|
host: config.host ?? DEFAULT_HOST,
|
|
692
752
|
config: options.config || resolvedConfigPath,
|
|
@@ -704,21 +764,21 @@ async function startStdioHttpTransport(config, options, resolvedConfigPath) {
|
|
|
704
764
|
throw new Error(`Failed to start stdio-http transport: ${toErrorMessage$9(error)}`);
|
|
705
765
|
}
|
|
706
766
|
}
|
|
707
|
-
async function startTransport(transportType, options, resolvedConfigPath, serverOptions) {
|
|
767
|
+
async function startTransport(transportType, options, resolvedConfigPath, serverOptions, proxyDefaults) {
|
|
708
768
|
try {
|
|
709
769
|
if (transportType === TRANSPORT_TYPE_STDIO) {
|
|
710
770
|
await startStdioTransport(serverOptions);
|
|
711
771
|
return;
|
|
712
772
|
}
|
|
713
773
|
if (transportType === TRANSPORT_TYPE_HTTP) {
|
|
714
|
-
await createAndStartHttpRuntime(serverOptions, createTransportConfig(options, require_src.TRANSPORT_MODE.HTTP), resolvedConfigPath);
|
|
774
|
+
await createAndStartHttpRuntime(serverOptions, createTransportConfig(options, require_src.TRANSPORT_MODE.HTTP, proxyDefaults), resolvedConfigPath);
|
|
715
775
|
return;
|
|
716
776
|
}
|
|
717
777
|
if (transportType === TRANSPORT_TYPE_SSE) {
|
|
718
|
-
await startSseTransport(serverOptions, createTransportConfig(options, require_src.TRANSPORT_MODE.SSE));
|
|
778
|
+
await startSseTransport(serverOptions, createTransportConfig(options, require_src.TRANSPORT_MODE.SSE, proxyDefaults));
|
|
719
779
|
return;
|
|
720
780
|
}
|
|
721
|
-
await startStdioHttpTransport(createTransportConfig(options, require_src.TRANSPORT_MODE.HTTP), options, resolvedConfigPath);
|
|
781
|
+
await startStdioHttpTransport(createTransportConfig(options, require_src.TRANSPORT_MODE.HTTP, proxyDefaults), options, resolvedConfigPath);
|
|
722
782
|
} catch (error) {
|
|
723
783
|
throw new Error(`Failed to start transport '${transportType}': ${toErrorMessage$9(error)}`);
|
|
724
784
|
}
|
|
@@ -726,18 +786,19 @@ async function startTransport(transportType, options, resolvedConfigPath, server
|
|
|
726
786
|
/**
|
|
727
787
|
* MCP Serve command
|
|
728
788
|
*/
|
|
729
|
-
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}
|
|
789
|
+
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) => {
|
|
730
790
|
try {
|
|
731
|
-
const transportType = validateTransportType(options.type.toLowerCase());
|
|
732
|
-
validateProxyMode(options.proxyMode);
|
|
733
791
|
const resolvedConfigPath = options.config || await findConfigFileAsync() || void 0;
|
|
734
|
-
|
|
792
|
+
const proxyDefaults = resolvedConfigPath ? loadProxyDefaults(resolvedConfigPath) : {};
|
|
793
|
+
const transportType = validateTransportType((options.type ?? proxyDefaults.type ?? TRANSPORT_TYPE_STDIO).toLowerCase());
|
|
794
|
+
validateProxyMode(options.proxyMode);
|
|
795
|
+
await startTransport(transportType, options, resolvedConfigPath, createServerOptions(options, resolvedConfigPath, await resolveServerId(options, resolvedConfigPath)), proxyDefaults);
|
|
735
796
|
} catch (error) {
|
|
736
|
-
const rawTransportType = options.type.toLowerCase();
|
|
797
|
+
const rawTransportType = (options.type ?? TRANSPORT_TYPE_STDIO).toLowerCase();
|
|
737
798
|
const transportType = isValidTransportType(rawTransportType) ? rawTransportType : TRANSPORT_TYPE_STDIO;
|
|
738
799
|
const envPort = process.env.MCP_PORT ? Number(process.env.MCP_PORT) : void 0;
|
|
739
800
|
const requestedPort = options.port ?? (Number.isFinite(envPort) ? envPort : void 0);
|
|
740
|
-
console.error(formatStartError(transportType, options.host, requestedPort, error));
|
|
801
|
+
console.error(formatStartError(transportType, options.host ?? DEFAULT_HOST, requestedPort, error));
|
|
741
802
|
process.exit(1);
|
|
742
803
|
}
|
|
743
804
|
});
|
|
@@ -747,14 +808,58 @@ const mcpServeCommand = new commander.Command("mcp-serve").description("Start MC
|
|
|
747
808
|
function toErrorMessage$8(error) {
|
|
748
809
|
return error instanceof Error ? error.message : String(error);
|
|
749
810
|
}
|
|
750
|
-
async function
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
811
|
+
async function checkHealth(host, port) {
|
|
812
|
+
try {
|
|
813
|
+
return (await fetch(`http://${host}:${port}/health`, { signal: AbortSignal.timeout(3e3) })).ok;
|
|
814
|
+
} catch {
|
|
815
|
+
return false;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
/**
|
|
819
|
+
* Proxy mode: connect to a running HTTP server instead of downstream servers directly.
|
|
820
|
+
* Auto-starts the server if not running.
|
|
821
|
+
*/
|
|
822
|
+
async function withProxiedContext(container, config, configFilePath, options, run) {
|
|
823
|
+
const host = config.proxy?.host ?? "localhost";
|
|
824
|
+
const port = config.proxy?.port;
|
|
825
|
+
const endpoint = `http://${host}:${port}/mcp`;
|
|
826
|
+
if (!await checkHealth(host, port)) {
|
|
827
|
+
if (!options.json) console.error("Starting HTTP proxy server in background...");
|
|
828
|
+
await prestartHttpRuntime({
|
|
829
|
+
host,
|
|
830
|
+
port,
|
|
831
|
+
config: configFilePath,
|
|
832
|
+
cache: options.useCache !== false,
|
|
833
|
+
clearDefinitionsCache: false,
|
|
834
|
+
proxyMode: "flat"
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
const clientManager = container.createClientManagerService();
|
|
838
|
+
try {
|
|
839
|
+
await clientManager.connectToServer("proxy", {
|
|
840
|
+
name: "proxy",
|
|
841
|
+
transport: "http",
|
|
842
|
+
config: { url: endpoint }
|
|
843
|
+
});
|
|
844
|
+
if (!options.json) console.error(`✓ Connected to proxy at ${endpoint}`);
|
|
845
|
+
} catch (error) {
|
|
846
|
+
throw new Error(`Failed to connect to proxy server at ${endpoint}: ${toErrorMessage$8(error)}`);
|
|
847
|
+
}
|
|
848
|
+
try {
|
|
849
|
+
return await run({
|
|
850
|
+
container,
|
|
851
|
+
configFilePath,
|
|
852
|
+
config,
|
|
853
|
+
clientManager
|
|
854
|
+
});
|
|
855
|
+
} finally {
|
|
856
|
+
await clientManager.disconnectAll();
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
/**
|
|
860
|
+
* Direct mode: connect to all downstream MCP servers individually.
|
|
861
|
+
*/
|
|
862
|
+
async function withDirectContext(container, config, configFilePath, options, run) {
|
|
758
863
|
const clientManager = container.createClientManagerService();
|
|
759
864
|
await Promise.all(Object.entries(config.mcpServers).map(async ([serverName, serverConfig]) => {
|
|
760
865
|
try {
|
|
@@ -776,6 +881,17 @@ async function withConnectedCommandContext(options, run) {
|
|
|
776
881
|
await clientManager.disconnectAll();
|
|
777
882
|
}
|
|
778
883
|
}
|
|
884
|
+
async function withConnectedCommandContext(options, run) {
|
|
885
|
+
const container = require_src.createProxyIoCContainer();
|
|
886
|
+
const configFilePath = options.config || require_src.findConfigFile();
|
|
887
|
+
if (!configFilePath) throw new Error("No config file found. Use --config or create mcp-config.yaml");
|
|
888
|
+
const config = await container.createConfigFetcherService({
|
|
889
|
+
configFilePath,
|
|
890
|
+
useCache: options.useCache
|
|
891
|
+
}).fetchConfiguration();
|
|
892
|
+
if (config.proxy?.port) return await withProxiedContext(container, config, configFilePath, options, run);
|
|
893
|
+
return await withDirectContext(container, config, configFilePath, options, run);
|
|
894
|
+
}
|
|
779
895
|
|
|
780
896
|
//#endregion
|
|
781
897
|
//#region src/commands/list-tools.ts
|
|
@@ -1018,7 +1134,7 @@ function toErrorMessage$5(error) {
|
|
|
1018
1134
|
/**
|
|
1019
1135
|
* Execute an MCP tool with arguments
|
|
1020
1136
|
*/
|
|
1021
|
-
const useToolCommand = new commander.Command("use-tool").description("Execute an MCP tool with arguments").argument("<toolName>", "Tool name to execute").option("-c, --config <path>", "Path to MCP server configuration file").option("-s, --server <name>", "Server name (required if tool exists on multiple servers)").option("-a, --args <json>", "Tool arguments as JSON string", "{}").option("-t, --timeout <ms>", "Request timeout in milliseconds for tool execution (default: 60000)", parseInt).option("-j, --json", "Output as JSON", false).action(async (toolName, options) => {
|
|
1137
|
+
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) => {
|
|
1022
1138
|
try {
|
|
1023
1139
|
let toolArgs = {};
|
|
1024
1140
|
try {
|