@agimon-ai/mcp-proxy 0.5.0 → 0.5.2
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-DUR0uWiY.cjs} +2967 -2804
- package/dist/{src-DCIv5S_2.mjs → src-kgJ-iu3i.mjs} +2970 -2806
- package/package.json +9 -6
package/dist/cli.mjs
CHANGED
|
@@ -1,16 +1,25 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { C as DefinitionsCacheService, D as version, T as findConfigFile, b as RuntimeStateService, d as StdioHttpTransportHandler, f as StdioTransportHandler, m as HttpTransportHandler, n as createServer, o as createProxyIoCContainer, p as SseTransportHandler, r as createSessionServer, t as TRANSPORT_MODE, u as initializeSharedServices, w as generateServerId, y as StopServerService } from "./src-kgJ-iu3i.mjs";
|
|
3
|
+
import { constants, existsSync, readFileSync } from "node:fs";
|
|
4
4
|
import { access, writeFile } from "node:fs/promises";
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
5
|
+
import yaml from "js-yaml";
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
7
|
+
import path, { dirname, join, resolve } from "node:path";
|
|
8
8
|
import { spawn } from "node:child_process";
|
|
9
|
+
import { Liquid } from "liquidjs";
|
|
9
10
|
import { Command } from "commander";
|
|
10
11
|
import { DEFAULT_PORT_RANGE, PortRegistryService } from "@agimon-ai/foundation-port-registry";
|
|
11
|
-
import { ProcessRegistryService } from "@agimon-ai/foundation-process-registry";
|
|
12
|
+
import { ProcessRegistryService, createProcessLease, resolveSiblingRegistryPath } from "@agimon-ai/foundation-process-registry";
|
|
12
13
|
import { fileURLToPath } from "node:url";
|
|
13
14
|
|
|
15
|
+
//#region src/templates/mcp-config.json?raw
|
|
16
|
+
var mcp_config_default = "{\n \"_comment\": \"MCP Server Configuration - Use ${VAR_NAME} syntax for environment variable interpolation\",\n \"_instructions\": \"config.instruction: Server's default instruction | instruction: User override (takes precedence)\",\n \"mcpServers\": {\n \"example-server\": {\n \"command\": \"node\",\n \"args\": [\"/path/to/mcp-server/build/index.js\"],\n \"env\": {\n \"LOG_LEVEL\": \"info\",\n \"_comment\": \"You can use environment variable interpolation:\",\n \"_example_DATABASE_URL\": \"${DATABASE_URL}\",\n \"_example_API_KEY\": \"${MY_API_KEY}\"\n },\n \"config\": {\n \"instruction\": \"Use this server for...\"\n },\n \"_instruction_override\": \"Optional user override - takes precedence over config.instruction\"\n }\n }\n}\n";
|
|
17
|
+
|
|
18
|
+
//#endregion
|
|
19
|
+
//#region src/templates/mcp-config.yaml.liquid?raw
|
|
20
|
+
var mcp_config_yaml_default = "# MCP Server Configuration\n# This file configures the MCP servers that mcp-proxy will connect to\n#\n# Environment Variable Interpolation:\n# Use ${VAR_NAME} syntax to reference environment variables\n# Example: ${HOME}, ${API_KEY}, ${DATABASE_URL}\n#\n# Instructions:\n# - config.instruction: Server's default instruction (from server documentation)\n# - instruction: User override (optional, takes precedence over config.instruction)\n# - config.toolBlacklist: Array of tool names to hide/block from this server\n# - config.omitToolDescription: Boolean to show only tool names without descriptions (saves tokens)\n\n# Remote Configuration Sources (OPTIONAL)\n# Fetch and merge configurations from remote URLs\n# Remote configs are merged with local configs based on merge strategy\n#\n# SECURITY: SSRF Protection is ENABLED by default\n# - Only HTTPS URLs are allowed (set security.enforceHttps: false to allow HTTP)\n# - Private IPs and localhost are blocked (set security.allowPrivateIPs: true for internal networks)\n# - Blocked ranges: 127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16\nremoteConfigs:\n # Example 1: Basic remote config with default security\n # - url: ${AGIFLOW_URL}/api/v1/mcp-configs\n # headers:\n # Authorization: Bearer ${AGIFLOW_API_KEY}\n # mergeStrategy: local-priority # Options: local-priority (default), remote-priority, merge-deep\n #\n # Example 2: Remote config with custom security settings (for internal networks)\n # - url: ${INTERNAL_URL}/mcp-configs\n # headers:\n # Authorization: Bearer ${INTERNAL_TOKEN}\n # security:\n # allowPrivateIPs: true # Allow internal IPs (default: false)\n # enforceHttps: false # Allow HTTP (default: true, HTTPS only)\n # mergeStrategy: local-priority\n #\n # Example 3: Remote config with additional validation (OPTIONAL)\n # - url: ${AGIFLOW_URL}/api/v1/mcp-configs\n # headers:\n # Authorization: Bearer ${AGIFLOW_API_KEY}\n # X-API-Key: ${AGIFLOW_API_KEY}\n # security:\n # enforceHttps: true # Require HTTPS (default: true)\n # allowPrivateIPs: false # Block private IPs (default: false)\n # validation: # OPTIONAL: Additional regex validation on top of security checks\n # url: ^https://.*\\.agiflow\\.io/.* # OPTIONAL: Regex pattern to validate URL format\n # headers: # OPTIONAL: Regex patterns to validate header values\n # Authorization: ^Bearer [A-Za-z0-9_-]+$\n # X-API-Key: ^[A-Za-z0-9_-]{32,}$\n # mergeStrategy: local-priority\n\nmcpServers:\n{%- if mcpServers %}{% for server in mcpServers %}\n {{ server.name }}:\n command: {{ server.command }}\n args:{% for arg in server.args %}\n - '{{ arg }}'{% endfor %}\n # env:\n # LOG_LEVEL: info\n # # API_KEY: ${MY_API_KEY}\n # config:\n # instruction: Use this server for...\n # # toolBlacklist:\n # # - tool_to_block\n # # omitToolDescription: true\n{% endfor %}\n # Example MCP server using SSE transport\n # remote-server:\n # url: https://example.com/mcp\n # type: sse\n # headers:\n # Authorization: Bearer ${API_KEY}\n # config:\n # instruction: This server provides tools for...\n{% else %}\n # Example MCP server using stdio transport\n example-server:\n command: node\n args:\n - /path/to/mcp-server/build/index.js\n env:\n # Environment variables for the MCP server\n LOG_LEVEL: info\n # You can use environment variable interpolation:\n # DATABASE_URL: ${DATABASE_URL}\n # API_KEY: ${MY_API_KEY}\n config:\n # Server's default instruction (from server documentation)\n instruction: Use this server for...\n # Optional: Block specific tools from being listed or executed\n # toolBlacklist:\n # - dangerous_tool_name\n # - another_blocked_tool\n # Optional: Omit tool descriptions to save tokens (default: false)\n # omitToolDescription: true\n # instruction: Optional user override - takes precedence over config.instruction\n\n # Example MCP server using SSE transport with environment variables\n # remote-server:\n # url: https://example.com/mcp\n # type: sse\n # headers:\n # # Use ${VAR_NAME} to interpolate environment variables\n # Authorization: Bearer ${API_KEY}\n # config:\n # instruction: This server provides tools for...\n # # Optional: Block specific tools from being listed or executed\n # # toolBlacklist:\n # # - tool_to_block\n # # Optional: Omit tool descriptions to save tokens (default: false)\n # # omitToolDescription: true\n # # instruction: Optional user override\n{% endif %}\n";
|
|
21
|
+
|
|
22
|
+
//#endregion
|
|
14
23
|
//#region src/utils/output.ts
|
|
15
24
|
function writeLine(message = "") {
|
|
16
25
|
console.log(message);
|
|
@@ -34,14 +43,6 @@ const print = {
|
|
|
34
43
|
indent: (message) => writeLine(` ${message}`)
|
|
35
44
|
};
|
|
36
45
|
|
|
37
|
-
//#endregion
|
|
38
|
-
//#region src/templates/mcp-config.yaml.liquid?raw
|
|
39
|
-
var mcp_config_yaml_default = "# MCP Server Configuration\n# This file configures the MCP servers that mcp-proxy will connect to\n#\n# Environment Variable Interpolation:\n# Use ${VAR_NAME} syntax to reference environment variables\n# Example: ${HOME}, ${API_KEY}, ${DATABASE_URL}\n#\n# Instructions:\n# - config.instruction: Server's default instruction (from server documentation)\n# - instruction: User override (optional, takes precedence over config.instruction)\n# - config.toolBlacklist: Array of tool names to hide/block from this server\n# - config.omitToolDescription: Boolean to show only tool names without descriptions (saves tokens)\n\n# Remote Configuration Sources (OPTIONAL)\n# Fetch and merge configurations from remote URLs\n# Remote configs are merged with local configs based on merge strategy\n#\n# SECURITY: SSRF Protection is ENABLED by default\n# - Only HTTPS URLs are allowed (set security.enforceHttps: false to allow HTTP)\n# - Private IPs and localhost are blocked (set security.allowPrivateIPs: true for internal networks)\n# - Blocked ranges: 127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16\nremoteConfigs:\n # Example 1: Basic remote config with default security\n # - url: ${AGIFLOW_URL}/api/v1/mcp-configs\n # headers:\n # Authorization: Bearer ${AGIFLOW_API_KEY}\n # mergeStrategy: local-priority # Options: local-priority (default), remote-priority, merge-deep\n #\n # Example 2: Remote config with custom security settings (for internal networks)\n # - url: ${INTERNAL_URL}/mcp-configs\n # headers:\n # Authorization: Bearer ${INTERNAL_TOKEN}\n # security:\n # allowPrivateIPs: true # Allow internal IPs (default: false)\n # enforceHttps: false # Allow HTTP (default: true, HTTPS only)\n # mergeStrategy: local-priority\n #\n # Example 3: Remote config with additional validation (OPTIONAL)\n # - url: ${AGIFLOW_URL}/api/v1/mcp-configs\n # headers:\n # Authorization: Bearer ${AGIFLOW_API_KEY}\n # X-API-Key: ${AGIFLOW_API_KEY}\n # security:\n # enforceHttps: true # Require HTTPS (default: true)\n # allowPrivateIPs: false # Block private IPs (default: false)\n # validation: # OPTIONAL: Additional regex validation on top of security checks\n # url: ^https://.*\\.agiflow\\.io/.* # OPTIONAL: Regex pattern to validate URL format\n # headers: # OPTIONAL: Regex patterns to validate header values\n # Authorization: ^Bearer [A-Za-z0-9_-]+$\n # X-API-Key: ^[A-Za-z0-9_-]{32,}$\n # mergeStrategy: local-priority\n\nmcpServers:\n{%- if mcpServers %}{% for server in mcpServers %}\n {{ server.name }}:\n command: {{ server.command }}\n args:{% for arg in server.args %}\n - '{{ arg }}'{% endfor %}\n # env:\n # LOG_LEVEL: info\n # # API_KEY: ${MY_API_KEY}\n # config:\n # instruction: Use this server for...\n # # toolBlacklist:\n # # - tool_to_block\n # # omitToolDescription: true\n{% endfor %}\n # Example MCP server using SSE transport\n # remote-server:\n # url: https://example.com/mcp\n # type: sse\n # headers:\n # Authorization: Bearer ${API_KEY}\n # config:\n # instruction: This server provides tools for...\n{% else %}\n # Example MCP server using stdio transport\n example-server:\n command: node\n args:\n - /path/to/mcp-server/build/index.js\n env:\n # Environment variables for the MCP server\n LOG_LEVEL: info\n # You can use environment variable interpolation:\n # DATABASE_URL: ${DATABASE_URL}\n # API_KEY: ${MY_API_KEY}\n config:\n # Server's default instruction (from server documentation)\n instruction: Use this server for...\n # Optional: Block specific tools from being listed or executed\n # toolBlacklist:\n # - dangerous_tool_name\n # - another_blocked_tool\n # Optional: Omit tool descriptions to save tokens (default: false)\n # omitToolDescription: true\n # instruction: Optional user override - takes precedence over config.instruction\n\n # Example MCP server using SSE transport with environment variables\n # remote-server:\n # url: https://example.com/mcp\n # type: sse\n # headers:\n # # Use ${VAR_NAME} to interpolate environment variables\n # Authorization: Bearer ${API_KEY}\n # config:\n # instruction: This server provides tools for...\n # # Optional: Block specific tools from being listed or executed\n # # toolBlacklist:\n # # - tool_to_block\n # # Optional: Omit tool descriptions to save tokens (default: false)\n # # omitToolDescription: true\n # # instruction: Optional user override\n{% endif %}\n";
|
|
40
|
-
|
|
41
|
-
//#endregion
|
|
42
|
-
//#region src/templates/mcp-config.json?raw
|
|
43
|
-
var mcp_config_default = "{\n \"_comment\": \"MCP Server Configuration - Use ${VAR_NAME} syntax for environment variable interpolation\",\n \"_instructions\": \"config.instruction: Server's default instruction | instruction: User override (takes precedence)\",\n \"mcpServers\": {\n \"example-server\": {\n \"command\": \"node\",\n \"args\": [\"/path/to/mcp-server/build/index.js\"],\n \"env\": {\n \"LOG_LEVEL\": \"info\",\n \"_comment\": \"You can use environment variable interpolation:\",\n \"_example_DATABASE_URL\": \"${DATABASE_URL}\",\n \"_example_API_KEY\": \"${MY_API_KEY}\"\n },\n \"config\": {\n \"instruction\": \"Use this server for...\"\n },\n \"_instruction_override\": \"Optional user override - takes precedence over config.instruction\"\n }\n }\n}\n";
|
|
44
|
-
|
|
45
46
|
//#endregion
|
|
46
47
|
//#region src/commands/init.ts
|
|
47
48
|
/**
|
|
@@ -137,11 +138,26 @@ function resolveWorkspaceRoot(startPath = process.env.PROJECT_PATH || process.cw
|
|
|
137
138
|
current = parent;
|
|
138
139
|
}
|
|
139
140
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
141
|
+
const PROCESS_REGISTRY_SERVICE_HTTP$1 = "mcp-proxy-http";
|
|
142
|
+
async function findExistingHealthyRuntime(workspaceRoot) {
|
|
143
|
+
const match = (await new ProcessRegistryService(process.env.PROCESS_REGISTRY_PATH).listProcesses({
|
|
144
|
+
repositoryPath: workspaceRoot,
|
|
145
|
+
serviceName: PROCESS_REGISTRY_SERVICE_HTTP$1
|
|
146
|
+
}))[0];
|
|
147
|
+
if (!match?.host || !match?.port) return null;
|
|
148
|
+
try {
|
|
149
|
+
const healthUrl = `http://${match.host}:${match.port}/health`;
|
|
150
|
+
if ((await fetch(healthUrl)).ok) {
|
|
151
|
+
const metadata = match.metadata;
|
|
152
|
+
return {
|
|
153
|
+
host: match.host,
|
|
154
|
+
port: match.port,
|
|
155
|
+
serverId: metadata?.serverId ?? "unknown",
|
|
156
|
+
workspaceRoot
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
} catch {}
|
|
160
|
+
return null;
|
|
145
161
|
}
|
|
146
162
|
function buildCliCandidates() {
|
|
147
163
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -216,11 +232,33 @@ function spawnBackgroundRuntime(args, env, cwd) {
|
|
|
216
232
|
child.unref();
|
|
217
233
|
return child;
|
|
218
234
|
}
|
|
235
|
+
async function stopExistingRuntime(runtimeStateService, serverId, host, port) {
|
|
236
|
+
const runtimes = await runtimeStateService.list();
|
|
237
|
+
const targetHost = host || DEFAULT_HOST$1;
|
|
238
|
+
const match = runtimes.find((r) => {
|
|
239
|
+
if (serverId && r.serverId === serverId) return true;
|
|
240
|
+
if (port !== void 0 && r.host === targetHost && r.port === port) return true;
|
|
241
|
+
return false;
|
|
242
|
+
});
|
|
243
|
+
if (!match) return;
|
|
244
|
+
const stopService = new StopServerService(runtimeStateService);
|
|
245
|
+
try {
|
|
246
|
+
await stopService.stop({
|
|
247
|
+
serverId: match.serverId,
|
|
248
|
+
force: true
|
|
249
|
+
});
|
|
250
|
+
} catch {
|
|
251
|
+
await runtimeStateService.remove(match.serverId);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
219
254
|
async function prestartHttpRuntime(options) {
|
|
220
255
|
const serverId = options.id || generateServerId();
|
|
221
256
|
const timeoutMs = parseTimeoutMs(options.timeoutMs);
|
|
222
257
|
const registryPath = options.registryPath || options.registryDir;
|
|
223
258
|
const workspaceRoot = resolveWorkspaceRoot();
|
|
259
|
+
const existing = await findExistingHealthyRuntime(workspaceRoot);
|
|
260
|
+
if (existing) return existing;
|
|
261
|
+
await stopExistingRuntime(new RuntimeStateService(), options.id, options.host, options.port);
|
|
224
262
|
const childEnv = {
|
|
225
263
|
...process.env,
|
|
226
264
|
...registryPath ? {
|
|
@@ -260,7 +298,7 @@ async function prestartHttpRuntime(options) {
|
|
|
260
298
|
workspaceRoot
|
|
261
299
|
};
|
|
262
300
|
} catch (error) {
|
|
263
|
-
throw new Error(`Failed to prestart HTTP runtime '${serverId}': ${error instanceof Error ? error.message : String(error)}
|
|
301
|
+
throw new Error(`Failed to prestart HTTP runtime '${serverId}': ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
264
302
|
}
|
|
265
303
|
}
|
|
266
304
|
const prestartHttpCommand = new Command("prestart-http").description("Start an mcp-proxy HTTP runtime in the background and wait until it is healthy").option("--id <id>", "Server identifier to assign to the runtime").option("--host <host>", "Host to bind to", DEFAULT_HOST$1).option("-p, --port <port>", "Preferred HTTP port for the runtime", (value) => Number.parseInt(value, 10)).option("-c, --config <path>", "Path to MCP server configuration file").option("--no-cache", "Disable configuration caching, always reload from config file").option("--definitions-cache <path>", "Path to prefetched tool/prompt/skill definitions cache file").option("--clear-definitions-cache", "Delete definitions cache before startup", false).option("--proxy-mode <mode>", "How mcp-proxy exposes downstream tools: meta, flat, or search", "meta").option("--registry-path <path>", "Custom registry path or directory for service discovery").option("--registry-dir <path>", "Custom registry directory for service discovery").option("--timeout-ms <ms>", "How long to wait for the runtime to become healthy", String(DEFAULT_TIMEOUT_MS)).action(async (options) => {
|
|
@@ -271,7 +309,7 @@ const prestartHttpCommand = new Command("prestart-http").description("Start an m
|
|
|
271
309
|
process.stdout.write(`runtimeUrl=http://${host}:${port}\n`);
|
|
272
310
|
process.stdout.write(`workspaceRoot=${workspaceRoot}\n`);
|
|
273
311
|
} catch (error) {
|
|
274
|
-
throw new Error(`Failed to prestart HTTP runtime '${options.id || "generated-server-id"}': ${error instanceof Error ? error.message : String(error)}
|
|
312
|
+
throw new Error(`Failed to prestart HTTP runtime '${options.id || "generated-server-id"}': ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
275
313
|
}
|
|
276
314
|
});
|
|
277
315
|
|
|
@@ -343,16 +381,37 @@ async function findConfigFileAsync() {
|
|
|
343
381
|
const configPath = resolve(projectPath, fileName);
|
|
344
382
|
if (await pathExists(configPath)) return configPath;
|
|
345
383
|
}
|
|
346
|
-
const
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
384
|
+
const MAX_PARENT_LEVELS = 3;
|
|
385
|
+
let searchDir = process.cwd();
|
|
386
|
+
for (let level = 0; level <= MAX_PARENT_LEVELS; level++) {
|
|
387
|
+
for (const fileName of CONFIG_FILE_NAMES) {
|
|
388
|
+
const configPath = join(searchDir, fileName);
|
|
389
|
+
if (await pathExists(configPath)) return configPath;
|
|
390
|
+
}
|
|
391
|
+
const parentDir = dirname(searchDir);
|
|
392
|
+
if (parentDir === searchDir) break;
|
|
393
|
+
searchDir = parentDir;
|
|
350
394
|
}
|
|
351
395
|
return null;
|
|
352
396
|
} catch (error) {
|
|
353
397
|
throw new Error(`Failed to discover MCP config file: ${toErrorMessage$9(error)}`);
|
|
354
398
|
}
|
|
355
399
|
}
|
|
400
|
+
function loadProxyDefaults(configPath) {
|
|
401
|
+
try {
|
|
402
|
+
const content = readFileSync(configPath, "utf-8");
|
|
403
|
+
const proxy = (configPath.endsWith(".yaml") || configPath.endsWith(".yml") ? yaml.load(content) : JSON.parse(content))?.proxy;
|
|
404
|
+
if (!proxy || typeof proxy !== "object") return {};
|
|
405
|
+
const p = proxy;
|
|
406
|
+
return {
|
|
407
|
+
type: typeof p.type === "string" ? p.type : void 0,
|
|
408
|
+
port: typeof p.port === "number" && Number.isInteger(p.port) && p.port > 0 ? p.port : void 0,
|
|
409
|
+
host: typeof p.host === "string" ? p.host : void 0
|
|
410
|
+
};
|
|
411
|
+
} catch {
|
|
412
|
+
return {};
|
|
413
|
+
}
|
|
414
|
+
}
|
|
356
415
|
async function resolveServerId(options, resolvedConfigPath) {
|
|
357
416
|
const container = createProxyIoCContainer();
|
|
358
417
|
if (options.id) return options.id;
|
|
@@ -374,12 +433,12 @@ function validateTransportType(type) {
|
|
|
374
433
|
function validateProxyMode(mode) {
|
|
375
434
|
if (!isValidProxyMode(mode)) throw new Error(`Unknown proxy mode: '${mode}'. Valid options: meta, flat, search`);
|
|
376
435
|
}
|
|
377
|
-
function createTransportConfig(options, mode) {
|
|
436
|
+
function createTransportConfig(options, mode, proxyDefaults) {
|
|
378
437
|
const envPort = process.env.MCP_PORT ? Number(process.env.MCP_PORT) : void 0;
|
|
379
438
|
return {
|
|
380
439
|
mode,
|
|
381
|
-
port: options.port ?? (Number.isFinite(envPort) ? envPort : void 0),
|
|
382
|
-
host: options.host
|
|
440
|
+
port: options.port ?? (Number.isFinite(envPort) ? envPort : void 0) ?? proxyDefaults?.port,
|
|
441
|
+
host: options.host ?? process.env.MCP_HOST ?? proxyDefaults?.host ?? DEFAULT_HOST
|
|
383
442
|
};
|
|
384
443
|
}
|
|
385
444
|
function createStdioSafeLogger() {
|
|
@@ -428,9 +487,6 @@ function createRuntimeRecord(serverId, config, port, shutdownToken, configPath)
|
|
|
428
487
|
function createPortRegistryService() {
|
|
429
488
|
return new PortRegistryService(process.env.PORT_REGISTRY_PATH);
|
|
430
489
|
}
|
|
431
|
-
function createProcessRegistryService() {
|
|
432
|
-
return new ProcessRegistryService(process.env.PROCESS_REGISTRY_PATH);
|
|
433
|
-
}
|
|
434
490
|
function getRegistryEnvironment() {
|
|
435
491
|
return process.env.NODE_ENV ?? "development";
|
|
436
492
|
}
|
|
@@ -477,41 +533,6 @@ async function createPortRegistryLease(serviceName, host, preferredPort, serverI
|
|
|
477
533
|
}
|
|
478
534
|
};
|
|
479
535
|
}
|
|
480
|
-
async function createProcessRegistryLease(serviceName, host, port, serverId, transport, configPath) {
|
|
481
|
-
const processRegistry = createProcessRegistryService();
|
|
482
|
-
const result = await processRegistry.registerProcess({
|
|
483
|
-
repositoryPath: getRegistryRepositoryPath(),
|
|
484
|
-
serviceName,
|
|
485
|
-
serviceType: PROCESS_REGISTRY_SERVICE_TYPE,
|
|
486
|
-
environment: getRegistryEnvironment(),
|
|
487
|
-
pid: process.pid,
|
|
488
|
-
host,
|
|
489
|
-
port,
|
|
490
|
-
command: process.argv[1],
|
|
491
|
-
args: process.argv.slice(2),
|
|
492
|
-
metadata: {
|
|
493
|
-
transport,
|
|
494
|
-
serverId,
|
|
495
|
-
...configPath ? { configPath } : {}
|
|
496
|
-
}
|
|
497
|
-
});
|
|
498
|
-
if (!result.success || !result.record) throw new Error(result.error || `Failed to register process for ${serviceName}`);
|
|
499
|
-
let released = false;
|
|
500
|
-
return { release: async (options) => {
|
|
501
|
-
if (released) return;
|
|
502
|
-
released = true;
|
|
503
|
-
const releaseResult = await processRegistry.releaseProcess({
|
|
504
|
-
repositoryPath: getRegistryRepositoryPath(),
|
|
505
|
-
serviceName,
|
|
506
|
-
serviceType: PROCESS_REGISTRY_SERVICE_TYPE,
|
|
507
|
-
pid: process.pid,
|
|
508
|
-
environment: getRegistryEnvironment(),
|
|
509
|
-
kill: options?.kill ?? false,
|
|
510
|
-
releasePort: options?.releasePort ?? false
|
|
511
|
-
});
|
|
512
|
-
if (!releaseResult.success && releaseResult.error && !releaseResult.error.includes("No matching process entry")) throw new Error(releaseResult.error || `Failed to release process for ${serviceName}`);
|
|
513
|
-
} };
|
|
514
|
-
}
|
|
515
536
|
async function releasePortLease(lease) {
|
|
516
537
|
if (!lease) return;
|
|
517
538
|
await lease.release();
|
|
@@ -545,13 +566,19 @@ async function stopOwnedHttpTransport(handler, runtimeStateService, serverId, pr
|
|
|
545
566
|
throw new Error(`Failed to stop owned HTTP transport '${serverId}': ${toErrorMessage$9(error)}`);
|
|
546
567
|
}
|
|
547
568
|
} finally {
|
|
548
|
-
await processLease?.release({
|
|
549
|
-
kill: false,
|
|
550
|
-
releasePort: false
|
|
551
|
-
});
|
|
569
|
+
await processLease?.release({ kill: false });
|
|
552
570
|
await removeRuntimeRecord(runtimeStateService, serverId);
|
|
553
571
|
}
|
|
554
572
|
}
|
|
573
|
+
/**
|
|
574
|
+
* Run post-stop cleanup for an HTTP runtime (release port, dispose services, remove state).
|
|
575
|
+
* This is the subset of stopOwnedHttpTransport that runs AFTER handler.stop() has already
|
|
576
|
+
* been called by startServer()'s signal handler — avoids double-stopping the transport.
|
|
577
|
+
*/
|
|
578
|
+
async function cleanupHttpRuntime(runtimeStateService, serverId, processLease) {
|
|
579
|
+
await processLease?.release({ kill: false });
|
|
580
|
+
await removeRuntimeRecord(runtimeStateService, serverId);
|
|
581
|
+
}
|
|
555
582
|
async function cleanupFailedRuntimeStartup(handler, runtimeStateService, serverId, processLease) {
|
|
556
583
|
try {
|
|
557
584
|
try {
|
|
@@ -560,10 +587,7 @@ async function cleanupFailedRuntimeStartup(handler, runtimeStateService, serverI
|
|
|
560
587
|
throw new Error(`Failed to stop HTTP transport during cleanup for '${serverId}': ${toErrorMessage$9(error)}`);
|
|
561
588
|
}
|
|
562
589
|
} finally {
|
|
563
|
-
await processLease?.release({
|
|
564
|
-
kill: false,
|
|
565
|
-
releasePort: false
|
|
566
|
-
});
|
|
590
|
+
await processLease?.release({ kill: false });
|
|
567
591
|
await removeRuntimeRecord(runtimeStateService, serverId);
|
|
568
592
|
}
|
|
569
593
|
}
|
|
@@ -608,7 +632,21 @@ async function createAndStartHttpRuntime(serverOptions, config, resolvedConfigPa
|
|
|
608
632
|
...config,
|
|
609
633
|
port: runtimePort
|
|
610
634
|
};
|
|
611
|
-
const processLease = await
|
|
635
|
+
const processLease = await createProcessLease({
|
|
636
|
+
repositoryPath: getRegistryRepositoryPath(),
|
|
637
|
+
serviceName: PROCESS_REGISTRY_SERVICE_HTTP,
|
|
638
|
+
serviceType: PROCESS_REGISTRY_SERVICE_TYPE,
|
|
639
|
+
environment: getRegistryEnvironment(),
|
|
640
|
+
host: runtimeConfig.host ?? DEFAULT_HOST,
|
|
641
|
+
port: runtimePort,
|
|
642
|
+
command: process.argv[1],
|
|
643
|
+
args: process.argv.slice(2),
|
|
644
|
+
metadata: {
|
|
645
|
+
transport: TRANSPORT_TYPE_HTTP,
|
|
646
|
+
serverId: runtimeServerId,
|
|
647
|
+
...resolvedConfigPath ? { configPath: resolvedConfigPath } : {}
|
|
648
|
+
}
|
|
649
|
+
});
|
|
612
650
|
let releasePort = async () => {
|
|
613
651
|
await releasePortLease(portLease ?? null);
|
|
614
652
|
releasePort = async () => void 0;
|
|
@@ -632,10 +670,7 @@ async function createAndStartHttpRuntime(serverOptions, config, resolvedConfigPa
|
|
|
632
670
|
handler = new HttpTransportHandler(() => createSessionServer(sharedServices), runtimeConfig, createHttpAdminOptions(runtimeRecord.serverId, shutdownToken, stopHandler));
|
|
633
671
|
} catch (error) {
|
|
634
672
|
await releasePort();
|
|
635
|
-
await processLease.release({
|
|
636
|
-
kill: false,
|
|
637
|
-
releasePort: false
|
|
638
|
-
});
|
|
673
|
+
await processLease.release({ kill: false });
|
|
639
674
|
await sharedServices.dispose();
|
|
640
675
|
throw new Error(`Failed to create HTTP runtime server: ${toErrorMessage$9(error)}`);
|
|
641
676
|
}
|
|
@@ -643,19 +678,11 @@ async function createAndStartHttpRuntime(serverOptions, config, resolvedConfigPa
|
|
|
643
678
|
await startServer(handler, async () => {
|
|
644
679
|
await releasePort();
|
|
645
680
|
await sharedServices.dispose();
|
|
646
|
-
await processLease
|
|
647
|
-
kill: false,
|
|
648
|
-
releasePort: false
|
|
649
|
-
});
|
|
650
|
-
await removeRuntimeRecord(runtimeStateService, runtimeRecord.serverId);
|
|
681
|
+
await cleanupHttpRuntime(runtimeStateService, runtimeRecord.serverId, processLease);
|
|
651
682
|
});
|
|
652
683
|
await writeRuntimeRecord(runtimeStateService, runtimeRecord);
|
|
653
684
|
} catch (error) {
|
|
654
685
|
await releasePort();
|
|
655
|
-
await processLease.release({
|
|
656
|
-
kill: false,
|
|
657
|
-
releasePort: false
|
|
658
|
-
});
|
|
659
686
|
await sharedServices.dispose();
|
|
660
687
|
await cleanupFailedRuntimeStartup(handler, runtimeStateService, runtimeRecord.serverId, processLease);
|
|
661
688
|
throw new Error(`Failed to start HTTP runtime '${runtimeRecord.serverId}': ${toErrorMessage$9(error)}`);
|
|
@@ -671,7 +698,24 @@ async function startStdioTransport(serverOptions) {
|
|
|
671
698
|
}
|
|
672
699
|
async function startSseTransport(serverOptions, config) {
|
|
673
700
|
try {
|
|
674
|
-
|
|
701
|
+
const requestedPort = config.port;
|
|
702
|
+
const portRange = requestedPort !== void 0 ? {
|
|
703
|
+
min: requestedPort,
|
|
704
|
+
max: requestedPort
|
|
705
|
+
} : DEFAULT_PORT_RANGE;
|
|
706
|
+
const portLease = await createPortRegistryLease("mcp-proxy-sse", config.host ?? DEFAULT_HOST, requestedPort, serverOptions.serverId ?? generateServerId(), TRANSPORT_TYPE_SSE, void 0, portRange);
|
|
707
|
+
const resolvedConfig = {
|
|
708
|
+
...config,
|
|
709
|
+
port: portLease.port
|
|
710
|
+
};
|
|
711
|
+
const handler = new SseTransportHandler(await createServer(serverOptions), resolvedConfig);
|
|
712
|
+
const shutdown = async () => {
|
|
713
|
+
await handler.stop();
|
|
714
|
+
await portLease.release();
|
|
715
|
+
};
|
|
716
|
+
process.on("SIGINT", shutdown);
|
|
717
|
+
process.on("SIGTERM", shutdown);
|
|
718
|
+
await startServer(handler);
|
|
675
719
|
} catch (error) {
|
|
676
720
|
throw new Error(`Failed to start SSE transport: ${toErrorMessage$9(error)}`);
|
|
677
721
|
}
|
|
@@ -679,13 +723,28 @@ async function startSseTransport(serverOptions, config) {
|
|
|
679
723
|
async function resolveStdioHttpEndpoint(config, options, resolvedConfigPath) {
|
|
680
724
|
const repositoryPath = getRegistryRepositoryPath();
|
|
681
725
|
if (config.port !== void 0) return new URL(`http://${config.host ?? DEFAULT_HOST}:${config.port}${MCP_ENDPOINT_PATH}`);
|
|
682
|
-
const
|
|
726
|
+
const portRegistry = createPortRegistryService();
|
|
727
|
+
const result = await portRegistry.getPort({
|
|
683
728
|
repositoryPath,
|
|
684
729
|
serviceName: PORT_REGISTRY_SERVICE_HTTP,
|
|
685
730
|
serviceType: PORT_REGISTRY_SERVICE_TYPE,
|
|
686
731
|
environment: getRegistryEnvironment()
|
|
687
732
|
});
|
|
688
|
-
if (result.success && result.record)
|
|
733
|
+
if (result.success && result.record) {
|
|
734
|
+
const host = config.host ?? result.record.host;
|
|
735
|
+
const endpoint = new URL(`http://${host}:${result.record.port}${MCP_ENDPOINT_PATH}`);
|
|
736
|
+
try {
|
|
737
|
+
const healthUrl = `http://${host}:${result.record.port}/health`;
|
|
738
|
+
if ((await fetch(healthUrl)).ok) return endpoint;
|
|
739
|
+
} catch {}
|
|
740
|
+
await portRegistry.releasePort({
|
|
741
|
+
repositoryPath,
|
|
742
|
+
serviceName: PORT_REGISTRY_SERVICE_HTTP,
|
|
743
|
+
serviceType: PORT_REGISTRY_SERVICE_TYPE,
|
|
744
|
+
environment: getRegistryEnvironment(),
|
|
745
|
+
force: true
|
|
746
|
+
});
|
|
747
|
+
}
|
|
689
748
|
const runtime = await prestartHttpRuntime({
|
|
690
749
|
host: config.host ?? DEFAULT_HOST,
|
|
691
750
|
config: options.config || resolvedConfigPath,
|
|
@@ -703,21 +762,21 @@ async function startStdioHttpTransport(config, options, resolvedConfigPath) {
|
|
|
703
762
|
throw new Error(`Failed to start stdio-http transport: ${toErrorMessage$9(error)}`);
|
|
704
763
|
}
|
|
705
764
|
}
|
|
706
|
-
async function startTransport(transportType, options, resolvedConfigPath, serverOptions) {
|
|
765
|
+
async function startTransport(transportType, options, resolvedConfigPath, serverOptions, proxyDefaults) {
|
|
707
766
|
try {
|
|
708
767
|
if (transportType === TRANSPORT_TYPE_STDIO) {
|
|
709
768
|
await startStdioTransport(serverOptions);
|
|
710
769
|
return;
|
|
711
770
|
}
|
|
712
771
|
if (transportType === TRANSPORT_TYPE_HTTP) {
|
|
713
|
-
await createAndStartHttpRuntime(serverOptions, createTransportConfig(options, TRANSPORT_MODE.HTTP), resolvedConfigPath);
|
|
772
|
+
await createAndStartHttpRuntime(serverOptions, createTransportConfig(options, TRANSPORT_MODE.HTTP, proxyDefaults), resolvedConfigPath);
|
|
714
773
|
return;
|
|
715
774
|
}
|
|
716
775
|
if (transportType === TRANSPORT_TYPE_SSE) {
|
|
717
|
-
await startSseTransport(serverOptions, createTransportConfig(options, TRANSPORT_MODE.SSE));
|
|
776
|
+
await startSseTransport(serverOptions, createTransportConfig(options, TRANSPORT_MODE.SSE, proxyDefaults));
|
|
718
777
|
return;
|
|
719
778
|
}
|
|
720
|
-
await startStdioHttpTransport(createTransportConfig(options, TRANSPORT_MODE.HTTP), options, resolvedConfigPath);
|
|
779
|
+
await startStdioHttpTransport(createTransportConfig(options, TRANSPORT_MODE.HTTP, proxyDefaults), options, resolvedConfigPath);
|
|
721
780
|
} catch (error) {
|
|
722
781
|
throw new Error(`Failed to start transport '${transportType}': ${toErrorMessage$9(error)}`);
|
|
723
782
|
}
|
|
@@ -725,18 +784,19 @@ async function startTransport(transportType, options, resolvedConfigPath, server
|
|
|
725
784
|
/**
|
|
726
785
|
* MCP Serve command
|
|
727
786
|
*/
|
|
728
|
-
const mcpServeCommand = new Command("mcp-serve").description("Start MCP server with specified transport").option("-t, --type <type>", `Transport type: ${TRANSPORT_TYPE_STDIO}, ${TRANSPORT_TYPE_HTTP}, ${TRANSPORT_TYPE_SSE}, or ${TRANSPORT_TYPE_STDIO_HTTP}
|
|
787
|
+
const mcpServeCommand = new Command("mcp-serve").description("Start MCP server with specified transport").option("-t, --type <type>", `Transport type: ${TRANSPORT_TYPE_STDIO}, ${TRANSPORT_TYPE_HTTP}, ${TRANSPORT_TYPE_SSE}, or ${TRANSPORT_TYPE_STDIO_HTTP}`).option("-p, --port <port>", "Port to listen on (http/sse) or backend port for stdio-http", (val) => Number.parseInt(val, 10)).option("--host <host>", "Host to bind to (http/sse) or backend host for stdio-http").option("-c, --config <path>", "Path to MCP server configuration file").option("--no-cache", "Disable configuration caching, always reload from config file").option("--definitions-cache <path>", "Path to prefetched tool/prompt/skill definitions cache file").option("--clear-definitions-cache", "Delete definitions cache before startup", false).option("--proxy-mode <mode>", "How mcp-proxy exposes downstream tools: meta, flat, or search", "meta").option("--id <id>", "Unique server identifier (overrides config file id, auto-generated if not provided)").action(async (options) => {
|
|
729
788
|
try {
|
|
730
|
-
const transportType = validateTransportType(options.type.toLowerCase());
|
|
731
|
-
validateProxyMode(options.proxyMode);
|
|
732
789
|
const resolvedConfigPath = options.config || await findConfigFileAsync() || void 0;
|
|
733
|
-
|
|
790
|
+
const proxyDefaults = resolvedConfigPath ? loadProxyDefaults(resolvedConfigPath) : {};
|
|
791
|
+
const transportType = validateTransportType((options.type ?? proxyDefaults.type ?? TRANSPORT_TYPE_STDIO).toLowerCase());
|
|
792
|
+
validateProxyMode(options.proxyMode);
|
|
793
|
+
await startTransport(transportType, options, resolvedConfigPath, createServerOptions(options, resolvedConfigPath, await resolveServerId(options, resolvedConfigPath)), proxyDefaults);
|
|
734
794
|
} catch (error) {
|
|
735
|
-
const rawTransportType = options.type.toLowerCase();
|
|
795
|
+
const rawTransportType = (options.type ?? TRANSPORT_TYPE_STDIO).toLowerCase();
|
|
736
796
|
const transportType = isValidTransportType(rawTransportType) ? rawTransportType : TRANSPORT_TYPE_STDIO;
|
|
737
797
|
const envPort = process.env.MCP_PORT ? Number(process.env.MCP_PORT) : void 0;
|
|
738
798
|
const requestedPort = options.port ?? (Number.isFinite(envPort) ? envPort : void 0);
|
|
739
|
-
console.error(formatStartError(transportType, options.host, requestedPort, error));
|
|
799
|
+
console.error(formatStartError(transportType, options.host ?? DEFAULT_HOST, requestedPort, error));
|
|
740
800
|
process.exit(1);
|
|
741
801
|
}
|
|
742
802
|
});
|
|
@@ -746,14 +806,58 @@ const mcpServeCommand = new Command("mcp-serve").description("Start MCP server w
|
|
|
746
806
|
function toErrorMessage$8(error) {
|
|
747
807
|
return error instanceof Error ? error.message : String(error);
|
|
748
808
|
}
|
|
749
|
-
async function
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
809
|
+
async function checkHealth(host, port) {
|
|
810
|
+
try {
|
|
811
|
+
return (await fetch(`http://${host}:${port}/health`, { signal: AbortSignal.timeout(3e3) })).ok;
|
|
812
|
+
} catch {
|
|
813
|
+
return false;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
/**
|
|
817
|
+
* Proxy mode: connect to a running HTTP server instead of downstream servers directly.
|
|
818
|
+
* Auto-starts the server if not running.
|
|
819
|
+
*/
|
|
820
|
+
async function withProxiedContext(container, config, configFilePath, options, run) {
|
|
821
|
+
const host = config.proxy?.host ?? "localhost";
|
|
822
|
+
const port = config.proxy?.port;
|
|
823
|
+
const endpoint = `http://${host}:${port}/mcp`;
|
|
824
|
+
if (!await checkHealth(host, port)) {
|
|
825
|
+
if (!options.json) console.error("Starting HTTP proxy server in background...");
|
|
826
|
+
await prestartHttpRuntime({
|
|
827
|
+
host,
|
|
828
|
+
port,
|
|
829
|
+
config: configFilePath,
|
|
830
|
+
cache: options.useCache !== false,
|
|
831
|
+
clearDefinitionsCache: false,
|
|
832
|
+
proxyMode: "flat"
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
const clientManager = container.createClientManagerService();
|
|
836
|
+
try {
|
|
837
|
+
await clientManager.connectToServer("proxy", {
|
|
838
|
+
name: "proxy",
|
|
839
|
+
transport: "http",
|
|
840
|
+
config: { url: endpoint }
|
|
841
|
+
});
|
|
842
|
+
if (!options.json) console.error(`✓ Connected to proxy at ${endpoint}`);
|
|
843
|
+
} catch (error) {
|
|
844
|
+
throw new Error(`Failed to connect to proxy server at ${endpoint}: ${toErrorMessage$8(error)}`);
|
|
845
|
+
}
|
|
846
|
+
try {
|
|
847
|
+
return await run({
|
|
848
|
+
container,
|
|
849
|
+
configFilePath,
|
|
850
|
+
config,
|
|
851
|
+
clientManager
|
|
852
|
+
});
|
|
853
|
+
} finally {
|
|
854
|
+
await clientManager.disconnectAll();
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
/**
|
|
858
|
+
* Direct mode: connect to all downstream MCP servers individually.
|
|
859
|
+
*/
|
|
860
|
+
async function withDirectContext(container, config, configFilePath, options, run) {
|
|
757
861
|
const clientManager = container.createClientManagerService();
|
|
758
862
|
await Promise.all(Object.entries(config.mcpServers).map(async ([serverName, serverConfig]) => {
|
|
759
863
|
try {
|
|
@@ -775,6 +879,17 @@ async function withConnectedCommandContext(options, run) {
|
|
|
775
879
|
await clientManager.disconnectAll();
|
|
776
880
|
}
|
|
777
881
|
}
|
|
882
|
+
async function withConnectedCommandContext(options, run) {
|
|
883
|
+
const container = createProxyIoCContainer();
|
|
884
|
+
const configFilePath = options.config || findConfigFile();
|
|
885
|
+
if (!configFilePath) throw new Error("No config file found. Use --config or create mcp-config.yaml");
|
|
886
|
+
const config = await container.createConfigFetcherService({
|
|
887
|
+
configFilePath,
|
|
888
|
+
useCache: options.useCache
|
|
889
|
+
}).fetchConfiguration();
|
|
890
|
+
if (config.proxy?.port) return await withProxiedContext(container, config, configFilePath, options, run);
|
|
891
|
+
return await withDirectContext(container, config, configFilePath, options, run);
|
|
892
|
+
}
|
|
778
893
|
|
|
779
894
|
//#endregion
|
|
780
895
|
//#region src/commands/list-tools.ts
|
|
@@ -1017,7 +1132,7 @@ function toErrorMessage$5(error) {
|
|
|
1017
1132
|
/**
|
|
1018
1133
|
* Execute an MCP tool with arguments
|
|
1019
1134
|
*/
|
|
1020
|
-
const useToolCommand = new Command("use-tool").description("Execute an MCP tool with arguments").argument("<toolName>", "Tool name to execute").option("-c, --config <path>", "Path to MCP server configuration file").option("-s, --server <name>", "Server name (required if tool exists on multiple servers)").option("-a, --args <json>", "Tool arguments as JSON string", "{}").option("-t, --timeout <ms>", "Request timeout in milliseconds for tool execution (default: 60000)", parseInt).option("-j, --json", "Output as JSON", false).action(async (toolName, options) => {
|
|
1135
|
+
const useToolCommand = new Command("use-tool").description("Execute an MCP tool with arguments").argument("<toolName>", "Tool name to execute").option("-c, --config <path>", "Path to MCP server configuration file").option("-s, --server <name>", "Server name (required if tool exists on multiple servers)").option("-a, --args <json>", "Tool arguments as JSON string", "{}").option("-t, --timeout <ms>", "Request timeout in milliseconds for tool execution (default: 60000)", Number.parseInt).option("-j, --json", "Output as JSON", false).action(async (toolName, options) => {
|
|
1021
1136
|
try {
|
|
1022
1137
|
let toolArgs = {};
|
|
1023
1138
|
try {
|
package/dist/index.cjs
CHANGED