@agimon-ai/mcp-proxy 0.4.1 → 0.4.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 +212 -51
- package/dist/cli.mjs +214 -54
- package/dist/index.cjs +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{src-DQ_MagA0.mjs → src-CNXwWpA2.mjs} +6 -6
- package/dist/{src-6KF7hZTe.cjs → src-Cm2AVRBA.cjs} +1 -1
- package/package.json +4 -4
package/dist/cli.cjs
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
const require_src = require('./src-
|
|
2
|
+
const require_src = require('./src-Cm2AVRBA.cjs');
|
|
3
3
|
let node_crypto = require("node:crypto");
|
|
4
4
|
let node_fs_promises = require("node:fs/promises");
|
|
5
5
|
let node_path = require("node:path");
|
|
6
|
+
node_path = require_src.__toESM(node_path);
|
|
6
7
|
let node_fs = require("node:fs");
|
|
7
8
|
let liquidjs = require("liquidjs");
|
|
9
|
+
let node_child_process = require("node:child_process");
|
|
8
10
|
let commander = require("commander");
|
|
9
11
|
let __agimon_ai_foundation_process_registry = require("@agimon-ai/foundation-process-registry");
|
|
10
12
|
let __agimon_ai_foundation_port_registry = require("@agimon-ai/foundation-port-registry");
|
|
13
|
+
let node_url = require("node:url");
|
|
11
14
|
|
|
12
15
|
//#region src/utils/output.ts
|
|
13
16
|
function writeLine(message = "") {
|
|
@@ -139,7 +142,7 @@ const CONFIG_FILE_NAMES = [
|
|
|
139
142
|
];
|
|
140
143
|
const MCP_ENDPOINT_PATH = "/mcp";
|
|
141
144
|
const DEFAULT_PORT = 3e3;
|
|
142
|
-
const DEFAULT_HOST = "localhost";
|
|
145
|
+
const DEFAULT_HOST$1 = "localhost";
|
|
143
146
|
const TRANSPORT_TYPE_STDIO = "stdio";
|
|
144
147
|
const TRANSPORT_TYPE_HTTP = "http";
|
|
145
148
|
const TRANSPORT_TYPE_SSE = "sse";
|
|
@@ -218,10 +221,11 @@ function validateProxyMode(mode) {
|
|
|
218
221
|
if (!isValidProxyMode(mode)) throw new Error(`Unknown proxy mode: '${mode}'. Valid options: meta, flat, search`);
|
|
219
222
|
}
|
|
220
223
|
function createTransportConfig(options, mode) {
|
|
224
|
+
const envPort = process.env.MCP_PORT ? Number(process.env.MCP_PORT) : void 0;
|
|
221
225
|
return {
|
|
222
226
|
mode,
|
|
223
|
-
port: options.port
|
|
224
|
-
host: options.host || process.env.MCP_HOST || DEFAULT_HOST
|
|
227
|
+
port: options.port ?? (Number.isFinite(envPort) ? envPort : void 0) ?? DEFAULT_PORT,
|
|
228
|
+
host: options.host || process.env.MCP_HOST || DEFAULT_HOST$1
|
|
225
229
|
};
|
|
226
230
|
}
|
|
227
231
|
function createServerOptions(options, resolvedConfigPath, serverId) {
|
|
@@ -242,7 +246,7 @@ function formatStartError(type, host, port, error) {
|
|
|
242
246
|
function createRuntimeRecord(serverId, config, port, shutdownToken, configPath) {
|
|
243
247
|
return {
|
|
244
248
|
serverId,
|
|
245
|
-
host: config.host ?? DEFAULT_HOST,
|
|
249
|
+
host: config.host ?? DEFAULT_HOST$1,
|
|
246
250
|
port,
|
|
247
251
|
transport: RUNTIME_TRANSPORT,
|
|
248
252
|
shutdownToken,
|
|
@@ -260,7 +264,10 @@ function createProcessRegistryService() {
|
|
|
260
264
|
function getRegistryEnvironment() {
|
|
261
265
|
return process.env.NODE_ENV ?? "development";
|
|
262
266
|
}
|
|
263
|
-
async function createPortRegistryLease(serviceName, host, preferredPort, serverId, transport, configPath
|
|
267
|
+
async function createPortRegistryLease(serviceName, host, preferredPort, serverId, transport, configPath, portRange = {
|
|
268
|
+
min: preferredPort,
|
|
269
|
+
max: preferredPort
|
|
270
|
+
}) {
|
|
264
271
|
const portRegistry = createPortRegistryService();
|
|
265
272
|
const result = await portRegistry.reservePort({
|
|
266
273
|
repositoryPath: PORT_REGISTRY_REPOSITORY_PATH,
|
|
@@ -270,10 +277,7 @@ async function createPortRegistryLease(serviceName, host, preferredPort, serverI
|
|
|
270
277
|
pid: process.pid,
|
|
271
278
|
host,
|
|
272
279
|
preferredPort,
|
|
273
|
-
portRange
|
|
274
|
-
min: preferredPort,
|
|
275
|
-
max: preferredPort
|
|
276
|
-
},
|
|
280
|
+
portRange,
|
|
277
281
|
force: true,
|
|
278
282
|
metadata: {
|
|
279
283
|
transport,
|
|
@@ -283,19 +287,22 @@ async function createPortRegistryLease(serviceName, host, preferredPort, serverI
|
|
|
283
287
|
});
|
|
284
288
|
if (!result.success || !result.record) throw new Error(result.error || `Failed to reserve port ${preferredPort} in port registry`);
|
|
285
289
|
let released = false;
|
|
286
|
-
return {
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
290
|
+
return {
|
|
291
|
+
port: result.record.port,
|
|
292
|
+
release: async () => {
|
|
293
|
+
if (released) return;
|
|
294
|
+
released = true;
|
|
295
|
+
const releaseResult = await portRegistry.releasePort({
|
|
296
|
+
repositoryPath: PORT_REGISTRY_REPOSITORY_PATH,
|
|
297
|
+
serviceName,
|
|
298
|
+
serviceType: PORT_REGISTRY_SERVICE_TYPE,
|
|
299
|
+
pid: process.pid,
|
|
300
|
+
environment: getRegistryEnvironment(),
|
|
301
|
+
force: true
|
|
302
|
+
});
|
|
303
|
+
if (!releaseResult.success && releaseResult.error && !releaseResult.error.includes("No matching registry entry")) throw new Error(releaseResult.error || `Failed to release port for ${serviceName}`);
|
|
304
|
+
}
|
|
305
|
+
};
|
|
299
306
|
}
|
|
300
307
|
async function createProcessRegistryLease(serviceName, host, port, serverId, transport, configPath) {
|
|
301
308
|
const processRegistry = createProcessRegistryService();
|
|
@@ -438,13 +445,14 @@ async function createAndStartHttpRuntime(serverOptions, config, resolvedConfigPa
|
|
|
438
445
|
const runtimeStateService = new require_src.RuntimeStateService();
|
|
439
446
|
const shutdownToken = (0, node_crypto.randomUUID)();
|
|
440
447
|
const runtimeServerId = serverOptions.serverId ?? require_src.generateServerId();
|
|
441
|
-
const
|
|
442
|
-
const
|
|
448
|
+
const runtimePort = config.port ?? DEFAULT_PORT;
|
|
449
|
+
const processLease = await createProcessRegistryLease(PROCESS_REGISTRY_SERVICE_HTTP, config.host ?? DEFAULT_HOST$1, runtimePort, runtimeServerId, TRANSPORT_TYPE_HTTP, resolvedConfigPath);
|
|
450
|
+
const portLease = await createPortRegistryLease(PORT_REGISTRY_SERVICE_HTTP, config.host ?? DEFAULT_HOST$1, runtimePort, runtimeServerId, TRANSPORT_TYPE_HTTP, resolvedConfigPath);
|
|
443
451
|
let releasePort = async () => {
|
|
444
|
-
await releasePortLease(portLease);
|
|
452
|
+
await releasePortLease(portLease ?? null);
|
|
445
453
|
releasePort = async () => void 0;
|
|
446
454
|
};
|
|
447
|
-
const runtimeRecord = createRuntimeRecord(runtimeServerId, config,
|
|
455
|
+
const runtimeRecord = createRuntimeRecord(runtimeServerId, config, runtimePort, shutdownToken, resolvedConfigPath);
|
|
448
456
|
let handler;
|
|
449
457
|
let isStopping = false;
|
|
450
458
|
const stopHandler = async () => {
|
|
@@ -520,30 +528,37 @@ async function startSseTransport(serverOptions, config) {
|
|
|
520
528
|
throw new Error(`Failed to start SSE transport: ${toErrorMessage$9(error)}`);
|
|
521
529
|
}
|
|
522
530
|
}
|
|
523
|
-
async function startStdioHttpTransport(serverOptions, config, resolvedConfigPath) {
|
|
531
|
+
async function startStdioHttpTransport(serverOptions, config, resolvedConfigPath, options) {
|
|
524
532
|
const shared = { services: null };
|
|
525
533
|
let processLease;
|
|
526
|
-
let
|
|
534
|
+
let portLease;
|
|
527
535
|
try {
|
|
528
|
-
const stdioHttpHandler = new require_src.StdioHttpTransportHandler({ endpoint: new URL(`http://${config.host}:${config.port}${MCP_ENDPOINT_PATH}`) });
|
|
529
|
-
const runtimeStateService = new require_src.RuntimeStateService();
|
|
530
536
|
const serverId = serverOptions.serverId ?? require_src.generateServerId();
|
|
537
|
+
const envPort = process.env.MCP_PORT ? Number(process.env.MCP_PORT) : void 0;
|
|
538
|
+
const requestedPort = options.port ?? (Number.isFinite(envPort) ? envPort : void 0) ?? DEFAULT_PORT;
|
|
539
|
+
const portRange = options.port !== void 0 || Number.isFinite(envPort) ? {
|
|
540
|
+
min: requestedPort,
|
|
541
|
+
max: requestedPort
|
|
542
|
+
} : __agimon_ai_foundation_port_registry.DEFAULT_PORT_RANGE;
|
|
543
|
+
portLease = await createPortRegistryLease(PORT_REGISTRY_SERVICE_STDIO_HTTP, config.host ?? DEFAULT_HOST$1, requestedPort, serverId, TRANSPORT_TYPE_STDIO_HTTP, resolvedConfigPath, portRange);
|
|
544
|
+
const internalPort = portLease.port;
|
|
545
|
+
const transportConfig = {
|
|
546
|
+
...config,
|
|
547
|
+
port: internalPort
|
|
548
|
+
};
|
|
549
|
+
const stdioHttpHandler = new require_src.StdioHttpTransportHandler({ endpoint: new URL(`http://${transportConfig.host}:${transportConfig.port}${MCP_ENDPOINT_PATH}`) });
|
|
550
|
+
const runtimeStateService = new require_src.RuntimeStateService();
|
|
531
551
|
const shutdownToken = (0, node_crypto.randomUUID)();
|
|
532
|
-
processLease = await createProcessRegistryLease(PROCESS_REGISTRY_SERVICE_STDIO_HTTP,
|
|
552
|
+
processLease = await createProcessRegistryLease(PROCESS_REGISTRY_SERVICE_STDIO_HTTP, transportConfig.host ?? DEFAULT_HOST$1, internalPort, serverId, TRANSPORT_TYPE_STDIO_HTTP, resolvedConfigPath);
|
|
533
553
|
let httpHandler = null;
|
|
534
554
|
let ownsInternalHttpTransport = false;
|
|
535
555
|
let isStopping = false;
|
|
536
|
-
let releaseInternalPort = async () => {
|
|
537
|
-
await releasePortLease(internalPortLease);
|
|
538
|
-
internalPortLease = null;
|
|
539
|
-
releaseInternalPort = async () => void 0;
|
|
540
|
-
};
|
|
541
556
|
const stopOwnedRuntime = async () => {
|
|
542
557
|
if (isStopping) return;
|
|
543
558
|
isStopping = true;
|
|
544
559
|
try {
|
|
545
560
|
await Promise.all([stopInternalHttpTransport(stdioHttpHandler, httpHandler, ownsInternalHttpTransport), removeRuntimeRecord(runtimeStateService, serverId)]);
|
|
546
|
-
await
|
|
561
|
+
await releasePortLease(portLease ?? null);
|
|
547
562
|
if (shared.services) await shared.services.dispose();
|
|
548
563
|
ownsInternalHttpTransport = false;
|
|
549
564
|
process.exit(0);
|
|
@@ -564,15 +579,11 @@ async function startStdioHttpTransport(serverOptions, config, resolvedConfigPath
|
|
|
564
579
|
}
|
|
565
580
|
if (!shared.services) shared.services = await require_src.initializeSharedServices(serverOptions);
|
|
566
581
|
try {
|
|
567
|
-
|
|
568
|
-
httpHandler = createStdioHttpInternalTransport(shared.services, config, adminOptions);
|
|
582
|
+
httpHandler = createStdioHttpInternalTransport(shared.services, transportConfig, adminOptions);
|
|
569
583
|
await httpHandler.start();
|
|
570
584
|
ownsInternalHttpTransport = true;
|
|
571
585
|
} catch (error) {
|
|
572
|
-
if (!isAddressInUseError(error)) {
|
|
573
|
-
await releaseInternalPort();
|
|
574
|
-
throw new Error(`Failed to start internal HTTP transport for stdio-http proxy: ${toErrorMessage$9(error)}`);
|
|
575
|
-
}
|
|
586
|
+
if (!isAddressInUseError(error)) throw new Error(`Failed to start internal HTTP transport for stdio-http proxy: ${toErrorMessage$9(error)}`);
|
|
576
587
|
}
|
|
577
588
|
try {
|
|
578
589
|
await stdioHttpHandler.start();
|
|
@@ -586,14 +597,13 @@ async function startStdioHttpTransport(serverOptions, config, resolvedConfigPath
|
|
|
586
597
|
}
|
|
587
598
|
ownsInternalHttpTransport = false;
|
|
588
599
|
}
|
|
589
|
-
await releaseInternalPort();
|
|
590
600
|
const retryErrorMessage = toErrorMessage$9(error);
|
|
591
601
|
const initialErrorMessage = toErrorMessage$9(initialProxyConnectError);
|
|
592
602
|
const rollbackMessage = rollbackStopErrorMessage ? `; rollback stop failed: ${rollbackStopErrorMessage}` : "";
|
|
593
603
|
throw new Error(`Failed to start stdio-http proxy bridge: initial connect failed (${initialErrorMessage}); retry failed (${retryErrorMessage})${rollbackMessage}`);
|
|
594
604
|
}
|
|
595
605
|
if (ownsInternalHttpTransport) try {
|
|
596
|
-
await writeRuntimeRecord(runtimeStateService, createRuntimeRecord(serverId,
|
|
606
|
+
await writeRuntimeRecord(runtimeStateService, createRuntimeRecord(serverId, transportConfig, internalPort, shutdownToken, resolvedConfigPath));
|
|
597
607
|
} catch (error) {
|
|
598
608
|
throw new Error(`Failed to persist runtime state for stdio-http server '${serverId}': ${toErrorMessage$9(error)}`);
|
|
599
609
|
}
|
|
@@ -601,7 +611,7 @@ async function startStdioHttpTransport(serverOptions, config, resolvedConfigPath
|
|
|
601
611
|
async stop() {
|
|
602
612
|
try {
|
|
603
613
|
await Promise.all([stopInternalHttpTransport(stdioHttpHandler, httpHandler, ownsInternalHttpTransport), removeRuntimeRecord(runtimeStateService, serverId)]);
|
|
604
|
-
await
|
|
614
|
+
await releasePortLease(portLease ?? null);
|
|
605
615
|
await processLease?.release({
|
|
606
616
|
kill: false,
|
|
607
617
|
releasePort: false
|
|
@@ -616,7 +626,7 @@ async function startStdioHttpTransport(serverOptions, config, resolvedConfigPath
|
|
|
616
626
|
});
|
|
617
627
|
} catch (error) {
|
|
618
628
|
try {
|
|
619
|
-
await
|
|
629
|
+
await portLease?.release();
|
|
620
630
|
await processLease?.release({
|
|
621
631
|
kill: false,
|
|
622
632
|
releasePort: false
|
|
@@ -640,7 +650,7 @@ async function startTransport(transportType, options, resolvedConfigPath, server
|
|
|
640
650
|
await startSseTransport(serverOptions, createTransportConfig(options, require_src.TRANSPORT_MODE.SSE));
|
|
641
651
|
return;
|
|
642
652
|
}
|
|
643
|
-
await startStdioHttpTransport(serverOptions, createTransportConfig(options, require_src.TRANSPORT_MODE.HTTP), resolvedConfigPath);
|
|
653
|
+
await startStdioHttpTransport(serverOptions, createTransportConfig(options, require_src.TRANSPORT_MODE.HTTP), resolvedConfigPath, options);
|
|
644
654
|
} catch (error) {
|
|
645
655
|
throw new Error(`Failed to start transport '${transportType}': ${toErrorMessage$9(error)}`);
|
|
646
656
|
}
|
|
@@ -648,7 +658,7 @@ async function startTransport(transportType, options, resolvedConfigPath, server
|
|
|
648
658
|
/**
|
|
649
659
|
* MCP Serve command
|
|
650
660
|
*/
|
|
651
|
-
const mcpServeCommand = new commander.Command("mcp-serve").description("Start MCP server with specified transport").option("-t, --type <type>", `Transport type: ${TRANSPORT_TYPE_STDIO}, ${TRANSPORT_TYPE_HTTP}, ${TRANSPORT_TYPE_SSE}, or ${TRANSPORT_TYPE_STDIO_HTTP}`, TRANSPORT_TYPE_STDIO).option("-p, --port <port>", "Port to listen on (http/sse/stdio-http internal HTTP)", (val) => parseInt(val, 10)
|
|
661
|
+
const mcpServeCommand = new commander.Command("mcp-serve").description("Start MCP server with specified transport").option("-t, --type <type>", `Transport type: ${TRANSPORT_TYPE_STDIO}, ${TRANSPORT_TYPE_HTTP}, ${TRANSPORT_TYPE_SSE}, or ${TRANSPORT_TYPE_STDIO_HTTP}`, TRANSPORT_TYPE_STDIO).option("-p, --port <port>", "Port to listen on (http/sse/stdio-http internal HTTP)", (val) => parseInt(val, 10)).option("--host <host>", "Host to bind to (http/sse/stdio-http internal HTTP)", DEFAULT_HOST$1).option("-c, --config <path>", "Path to MCP server configuration file").option("--no-cache", "Disable configuration caching, always reload from config file").option("--definitions-cache <path>", "Path to prefetched tool/prompt/skill definitions cache file").option("--clear-definitions-cache", "Delete definitions cache before startup", false).option("--proxy-mode <mode>", "How mcp-proxy exposes downstream tools: meta, flat, or search", "meta").option("--id <id>", "Unique server identifier (overrides config file id, auto-generated if not provided)").action(async (options) => {
|
|
652
662
|
try {
|
|
653
663
|
const transportType = validateTransportType(options.type.toLowerCase());
|
|
654
664
|
validateProxyMode(options.proxyMode);
|
|
@@ -657,11 +667,161 @@ const mcpServeCommand = new commander.Command("mcp-serve").description("Start MC
|
|
|
657
667
|
} catch (error) {
|
|
658
668
|
const rawTransportType = options.type.toLowerCase();
|
|
659
669
|
const transportType = isValidTransportType(rawTransportType) ? rawTransportType : TRANSPORT_TYPE_STDIO;
|
|
660
|
-
console.error(formatStartError(transportType, options.host, options.port, error));
|
|
670
|
+
console.error(formatStartError(transportType, options.host, options.port ?? DEFAULT_PORT, error));
|
|
661
671
|
process.exit(1);
|
|
662
672
|
}
|
|
663
673
|
});
|
|
664
674
|
|
|
675
|
+
//#endregion
|
|
676
|
+
//#region src/commands/prestart-http.ts
|
|
677
|
+
/**
|
|
678
|
+
* Prestart HTTP Command
|
|
679
|
+
*
|
|
680
|
+
* Starts an mcp-proxy HTTP runtime in the background, waits until it is healthy,
|
|
681
|
+
* and then exits so the runtime can be reused by other commands.
|
|
682
|
+
*/
|
|
683
|
+
const WORKSPACE_MARKERS = [
|
|
684
|
+
"pnpm-workspace.yaml",
|
|
685
|
+
"nx.json",
|
|
686
|
+
".git"
|
|
687
|
+
];
|
|
688
|
+
const DEFAULT_HOST = "localhost";
|
|
689
|
+
const DEFAULT_TIMEOUT_MS = 12e4;
|
|
690
|
+
const POLL_INTERVAL_MS = 250;
|
|
691
|
+
function resolveWorkspaceRoot(startPath = process.cwd()) {
|
|
692
|
+
let current = node_path.default.resolve(startPath);
|
|
693
|
+
while (true) {
|
|
694
|
+
for (const marker of WORKSPACE_MARKERS) if ((0, node_fs.existsSync)(node_path.default.join(current, marker))) return current;
|
|
695
|
+
const parent = node_path.default.dirname(current);
|
|
696
|
+
if (parent === current) return process.cwd();
|
|
697
|
+
current = parent;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
function resolveSiblingRegistryPath(registryPath, fileName) {
|
|
701
|
+
if (!registryPath) return;
|
|
702
|
+
const resolved = node_path.default.resolve(registryPath);
|
|
703
|
+
if (node_path.default.extname(resolved) === ".json") return node_path.default.join(node_path.default.dirname(resolved), fileName);
|
|
704
|
+
return node_path.default.join(resolved, fileName);
|
|
705
|
+
}
|
|
706
|
+
function buildCliCandidates() {
|
|
707
|
+
const __filename$1 = (0, node_url.fileURLToPath)(require("url").pathToFileURL(__filename).href);
|
|
708
|
+
const __dirname$1 = node_path.default.dirname(__filename$1);
|
|
709
|
+
const distCandidates = [
|
|
710
|
+
node_path.default.resolve(__dirname$1, "cli.mjs"),
|
|
711
|
+
node_path.default.resolve(__dirname$1, "..", "dist", "cli.mjs"),
|
|
712
|
+
node_path.default.resolve(__dirname$1, "..", "..", "dist", "cli.mjs")
|
|
713
|
+
];
|
|
714
|
+
const srcCandidates = [node_path.default.resolve(__dirname$1, "..", "cli.ts"), node_path.default.resolve(__dirname$1, "..", "..", "src", "cli.ts")];
|
|
715
|
+
for (const candidate of distCandidates) if ((0, node_fs.existsSync)(candidate)) return {
|
|
716
|
+
command: process.execPath,
|
|
717
|
+
args: [candidate]
|
|
718
|
+
};
|
|
719
|
+
for (const candidate of srcCandidates) if ((0, node_fs.existsSync)(candidate)) return {
|
|
720
|
+
command: process.execPath,
|
|
721
|
+
args: [
|
|
722
|
+
"--import",
|
|
723
|
+
"tsx",
|
|
724
|
+
candidate
|
|
725
|
+
]
|
|
726
|
+
};
|
|
727
|
+
throw new Error("Unable to locate mcp-proxy CLI entrypoint");
|
|
728
|
+
}
|
|
729
|
+
function parseTimeoutMs(value) {
|
|
730
|
+
if (!value) return DEFAULT_TIMEOUT_MS;
|
|
731
|
+
const parsed = Number.parseInt(value, 10);
|
|
732
|
+
if (!Number.isInteger(parsed) || parsed <= 0) throw new Error(`Invalid timeout value: ${value}`);
|
|
733
|
+
return parsed;
|
|
734
|
+
}
|
|
735
|
+
async function waitForFile(filePath, timeoutMs) {
|
|
736
|
+
const deadline = Date.now() + timeoutMs;
|
|
737
|
+
while (Date.now() < deadline) {
|
|
738
|
+
try {
|
|
739
|
+
await (0, node_fs_promises.access)(filePath);
|
|
740
|
+
return;
|
|
741
|
+
} catch {}
|
|
742
|
+
await new Promise((resolve$2) => setTimeout(resolve$2, POLL_INTERVAL_MS));
|
|
743
|
+
}
|
|
744
|
+
throw new Error(`Timed out waiting for runtime state file: ${filePath}`);
|
|
745
|
+
}
|
|
746
|
+
async function waitForHealthyRuntime(serverId, timeoutMs) {
|
|
747
|
+
const runtimeStateService = new require_src.RuntimeStateService();
|
|
748
|
+
const deadline = Date.now() + timeoutMs;
|
|
749
|
+
while (Date.now() < deadline) {
|
|
750
|
+
const record = await runtimeStateService.read(serverId);
|
|
751
|
+
if (record) {
|
|
752
|
+
const healthUrl = `http://${record.host}:${record.port}/health`;
|
|
753
|
+
try {
|
|
754
|
+
const response = await fetch(healthUrl);
|
|
755
|
+
if (response.ok) {
|
|
756
|
+
const payload = await response.json().catch(() => null);
|
|
757
|
+
if (!payload || payload.transport === "http") return {
|
|
758
|
+
host: record.host,
|
|
759
|
+
port: record.port
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
} catch {}
|
|
763
|
+
}
|
|
764
|
+
await new Promise((resolve$2) => setTimeout(resolve$2, POLL_INTERVAL_MS));
|
|
765
|
+
}
|
|
766
|
+
throw new Error(`Timed out waiting for HTTP runtime '${serverId}' to become healthy`);
|
|
767
|
+
}
|
|
768
|
+
function spawnBackgroundRuntime(args, env) {
|
|
769
|
+
const { command, args: baseArgs } = buildCliCandidates();
|
|
770
|
+
const child = (0, node_child_process.spawn)(command, [...baseArgs, ...args], {
|
|
771
|
+
detached: true,
|
|
772
|
+
stdio: "ignore",
|
|
773
|
+
env
|
|
774
|
+
});
|
|
775
|
+
child.unref();
|
|
776
|
+
return child;
|
|
777
|
+
}
|
|
778
|
+
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).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) => {
|
|
779
|
+
const serverId = options.id || require_src.generateServerId();
|
|
780
|
+
const timeoutMs = parseTimeoutMs(options.timeoutMs);
|
|
781
|
+
const registryPath = options.registryPath || options.registryDir;
|
|
782
|
+
new require_src.RuntimeStateService();
|
|
783
|
+
const workspaceRoot = resolveWorkspaceRoot(process.cwd());
|
|
784
|
+
const childEnv = {
|
|
785
|
+
...process.env,
|
|
786
|
+
...registryPath ? {
|
|
787
|
+
PORT_REGISTRY_PATH: registryPath,
|
|
788
|
+
PROCESS_REGISTRY_PATH: resolveSiblingRegistryPath(registryPath, "processes.json")
|
|
789
|
+
} : {}
|
|
790
|
+
};
|
|
791
|
+
const child = spawnBackgroundRuntime([
|
|
792
|
+
"mcp-serve",
|
|
793
|
+
"--type",
|
|
794
|
+
"http",
|
|
795
|
+
"--id",
|
|
796
|
+
serverId,
|
|
797
|
+
"--host",
|
|
798
|
+
options.host || DEFAULT_HOST,
|
|
799
|
+
...options.port !== void 0 ? ["--port", String(options.port)] : [],
|
|
800
|
+
...options.config ? ["--config", options.config] : [],
|
|
801
|
+
...options.cache === false ? ["--no-cache"] : [],
|
|
802
|
+
...options.definitionsCache ? ["--definitions-cache", options.definitionsCache] : [],
|
|
803
|
+
...options.clearDefinitionsCache ? ["--clear-definitions-cache"] : [],
|
|
804
|
+
"--proxy-mode",
|
|
805
|
+
options.proxyMode
|
|
806
|
+
], childEnv);
|
|
807
|
+
const childExit = new Promise((_, reject) => {
|
|
808
|
+
child.once("exit", (code, signal) => {
|
|
809
|
+
reject(/* @__PURE__ */ new Error(`Background runtime exited before becoming healthy (code=${code ?? "null"}, signal=${signal ?? "null"})`));
|
|
810
|
+
});
|
|
811
|
+
});
|
|
812
|
+
const runtimeFile = node_path.default.join(require_src.RuntimeStateService.getDefaultRuntimeDir(), `${serverId}.runtime.json`);
|
|
813
|
+
try {
|
|
814
|
+
await Promise.race([waitForFile(runtimeFile, timeoutMs), childExit]);
|
|
815
|
+
const { host, port } = await Promise.race([waitForHealthyRuntime(serverId, timeoutMs), childExit]);
|
|
816
|
+
process.stdout.write(`mcp-proxy HTTP runtime ready at http://${host}:${port} (${serverId})\n`);
|
|
817
|
+
process.stdout.write(`runtimeId=${serverId}\n`);
|
|
818
|
+
process.stdout.write(`runtimeUrl=http://${host}:${port}\n`);
|
|
819
|
+
process.stdout.write(`workspaceRoot=${workspaceRoot}\n`);
|
|
820
|
+
} catch (error) {
|
|
821
|
+
throw new Error(`Failed to prestart HTTP runtime '${serverId}': ${error instanceof Error ? error.message : String(error)}`);
|
|
822
|
+
}
|
|
823
|
+
});
|
|
824
|
+
|
|
665
825
|
//#endregion
|
|
666
826
|
//#region src/commands/bootstrap.ts
|
|
667
827
|
function toErrorMessage$8(error) {
|
|
@@ -1490,6 +1650,7 @@ async function main() {
|
|
|
1490
1650
|
program.name("mcp-proxy").description("MCP proxy server package").version(require_src.version);
|
|
1491
1651
|
program.addCommand(initCommand);
|
|
1492
1652
|
program.addCommand(mcpServeCommand);
|
|
1653
|
+
program.addCommand(prestartHttpCommand);
|
|
1493
1654
|
program.addCommand(searchToolsCommand);
|
|
1494
1655
|
program.addCommand(describeToolsCommand);
|
|
1495
1656
|
program.addCommand(useToolCommand);
|
package/dist/cli.mjs
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { D as findConfigFile, E as generateServerId, T as DefinitionsCacheService, d as version, f as StdioHttpTransportHandler, h as HttpTransportHandler, m as SseTransportHandler, n as createServer, o as createProxyIoCContainer, p as StdioTransportHandler, r as createSessionServer, t as TRANSPORT_MODE, u as initializeSharedServices, v as RuntimeStateService } from "./src-
|
|
2
|
+
import { D as findConfigFile, E as generateServerId, T as DefinitionsCacheService, d as version, f as StdioHttpTransportHandler, h as HttpTransportHandler, m as SseTransportHandler, n as createServer, o as createProxyIoCContainer, p as StdioTransportHandler, r as createSessionServer, t as TRANSPORT_MODE, u as initializeSharedServices, v as RuntimeStateService } from "./src-CNXwWpA2.mjs";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
4
|
import { access, writeFile } from "node:fs/promises";
|
|
5
|
-
import { join, resolve } from "node:path";
|
|
6
|
-
import { constants } from "node:fs";
|
|
5
|
+
import path, { join, resolve } from "node:path";
|
|
6
|
+
import { constants, existsSync } from "node:fs";
|
|
7
7
|
import { Liquid } from "liquidjs";
|
|
8
|
+
import { spawn } from "node:child_process";
|
|
8
9
|
import { Command } from "commander";
|
|
9
10
|
import { ProcessRegistryService } from "@agimon-ai/foundation-process-registry";
|
|
10
|
-
import { PortRegistryService } from "@agimon-ai/foundation-port-registry";
|
|
11
|
+
import { DEFAULT_PORT_RANGE, PortRegistryService } from "@agimon-ai/foundation-port-registry";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
11
13
|
|
|
12
14
|
//#region src/utils/output.ts
|
|
13
15
|
function writeLine(message = "") {
|
|
@@ -139,7 +141,7 @@ const CONFIG_FILE_NAMES = [
|
|
|
139
141
|
];
|
|
140
142
|
const MCP_ENDPOINT_PATH = "/mcp";
|
|
141
143
|
const DEFAULT_PORT = 3e3;
|
|
142
|
-
const DEFAULT_HOST = "localhost";
|
|
144
|
+
const DEFAULT_HOST$1 = "localhost";
|
|
143
145
|
const TRANSPORT_TYPE_STDIO = "stdio";
|
|
144
146
|
const TRANSPORT_TYPE_HTTP = "http";
|
|
145
147
|
const TRANSPORT_TYPE_SSE = "sse";
|
|
@@ -218,10 +220,11 @@ function validateProxyMode(mode) {
|
|
|
218
220
|
if (!isValidProxyMode(mode)) throw new Error(`Unknown proxy mode: '${mode}'. Valid options: meta, flat, search`);
|
|
219
221
|
}
|
|
220
222
|
function createTransportConfig(options, mode) {
|
|
223
|
+
const envPort = process.env.MCP_PORT ? Number(process.env.MCP_PORT) : void 0;
|
|
221
224
|
return {
|
|
222
225
|
mode,
|
|
223
|
-
port: options.port
|
|
224
|
-
host: options.host || process.env.MCP_HOST || DEFAULT_HOST
|
|
226
|
+
port: options.port ?? (Number.isFinite(envPort) ? envPort : void 0) ?? DEFAULT_PORT,
|
|
227
|
+
host: options.host || process.env.MCP_HOST || DEFAULT_HOST$1
|
|
225
228
|
};
|
|
226
229
|
}
|
|
227
230
|
function createServerOptions(options, resolvedConfigPath, serverId) {
|
|
@@ -242,7 +245,7 @@ function formatStartError(type, host, port, error) {
|
|
|
242
245
|
function createRuntimeRecord(serverId, config, port, shutdownToken, configPath) {
|
|
243
246
|
return {
|
|
244
247
|
serverId,
|
|
245
|
-
host: config.host ?? DEFAULT_HOST,
|
|
248
|
+
host: config.host ?? DEFAULT_HOST$1,
|
|
246
249
|
port,
|
|
247
250
|
transport: RUNTIME_TRANSPORT,
|
|
248
251
|
shutdownToken,
|
|
@@ -260,7 +263,10 @@ function createProcessRegistryService() {
|
|
|
260
263
|
function getRegistryEnvironment() {
|
|
261
264
|
return process.env.NODE_ENV ?? "development";
|
|
262
265
|
}
|
|
263
|
-
async function createPortRegistryLease(serviceName, host, preferredPort, serverId, transport, configPath
|
|
266
|
+
async function createPortRegistryLease(serviceName, host, preferredPort, serverId, transport, configPath, portRange = {
|
|
267
|
+
min: preferredPort,
|
|
268
|
+
max: preferredPort
|
|
269
|
+
}) {
|
|
264
270
|
const portRegistry = createPortRegistryService();
|
|
265
271
|
const result = await portRegistry.reservePort({
|
|
266
272
|
repositoryPath: PORT_REGISTRY_REPOSITORY_PATH,
|
|
@@ -270,10 +276,7 @@ async function createPortRegistryLease(serviceName, host, preferredPort, serverI
|
|
|
270
276
|
pid: process.pid,
|
|
271
277
|
host,
|
|
272
278
|
preferredPort,
|
|
273
|
-
portRange
|
|
274
|
-
min: preferredPort,
|
|
275
|
-
max: preferredPort
|
|
276
|
-
},
|
|
279
|
+
portRange,
|
|
277
280
|
force: true,
|
|
278
281
|
metadata: {
|
|
279
282
|
transport,
|
|
@@ -283,19 +286,22 @@ async function createPortRegistryLease(serviceName, host, preferredPort, serverI
|
|
|
283
286
|
});
|
|
284
287
|
if (!result.success || !result.record) throw new Error(result.error || `Failed to reserve port ${preferredPort} in port registry`);
|
|
285
288
|
let released = false;
|
|
286
|
-
return {
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
289
|
+
return {
|
|
290
|
+
port: result.record.port,
|
|
291
|
+
release: async () => {
|
|
292
|
+
if (released) return;
|
|
293
|
+
released = true;
|
|
294
|
+
const releaseResult = await portRegistry.releasePort({
|
|
295
|
+
repositoryPath: PORT_REGISTRY_REPOSITORY_PATH,
|
|
296
|
+
serviceName,
|
|
297
|
+
serviceType: PORT_REGISTRY_SERVICE_TYPE,
|
|
298
|
+
pid: process.pid,
|
|
299
|
+
environment: getRegistryEnvironment(),
|
|
300
|
+
force: true
|
|
301
|
+
});
|
|
302
|
+
if (!releaseResult.success && releaseResult.error && !releaseResult.error.includes("No matching registry entry")) throw new Error(releaseResult.error || `Failed to release port for ${serviceName}`);
|
|
303
|
+
}
|
|
304
|
+
};
|
|
299
305
|
}
|
|
300
306
|
async function createProcessRegistryLease(serviceName, host, port, serverId, transport, configPath) {
|
|
301
307
|
const processRegistry = createProcessRegistryService();
|
|
@@ -438,13 +444,14 @@ async function createAndStartHttpRuntime(serverOptions, config, resolvedConfigPa
|
|
|
438
444
|
const runtimeStateService = new RuntimeStateService();
|
|
439
445
|
const shutdownToken = randomUUID();
|
|
440
446
|
const runtimeServerId = serverOptions.serverId ?? generateServerId();
|
|
441
|
-
const
|
|
442
|
-
const
|
|
447
|
+
const runtimePort = config.port ?? DEFAULT_PORT;
|
|
448
|
+
const processLease = await createProcessRegistryLease(PROCESS_REGISTRY_SERVICE_HTTP, config.host ?? DEFAULT_HOST$1, runtimePort, runtimeServerId, TRANSPORT_TYPE_HTTP, resolvedConfigPath);
|
|
449
|
+
const portLease = await createPortRegistryLease(PORT_REGISTRY_SERVICE_HTTP, config.host ?? DEFAULT_HOST$1, runtimePort, runtimeServerId, TRANSPORT_TYPE_HTTP, resolvedConfigPath);
|
|
443
450
|
let releasePort = async () => {
|
|
444
|
-
await releasePortLease(portLease);
|
|
451
|
+
await releasePortLease(portLease ?? null);
|
|
445
452
|
releasePort = async () => void 0;
|
|
446
453
|
};
|
|
447
|
-
const runtimeRecord = createRuntimeRecord(runtimeServerId, config,
|
|
454
|
+
const runtimeRecord = createRuntimeRecord(runtimeServerId, config, runtimePort, shutdownToken, resolvedConfigPath);
|
|
448
455
|
let handler;
|
|
449
456
|
let isStopping = false;
|
|
450
457
|
const stopHandler = async () => {
|
|
@@ -520,30 +527,37 @@ async function startSseTransport(serverOptions, config) {
|
|
|
520
527
|
throw new Error(`Failed to start SSE transport: ${toErrorMessage$9(error)}`);
|
|
521
528
|
}
|
|
522
529
|
}
|
|
523
|
-
async function startStdioHttpTransport(serverOptions, config, resolvedConfigPath) {
|
|
530
|
+
async function startStdioHttpTransport(serverOptions, config, resolvedConfigPath, options) {
|
|
524
531
|
const shared = { services: null };
|
|
525
532
|
let processLease;
|
|
526
|
-
let
|
|
533
|
+
let portLease;
|
|
527
534
|
try {
|
|
528
|
-
const stdioHttpHandler = new StdioHttpTransportHandler({ endpoint: new URL(`http://${config.host}:${config.port}${MCP_ENDPOINT_PATH}`) });
|
|
529
|
-
const runtimeStateService = new RuntimeStateService();
|
|
530
535
|
const serverId = serverOptions.serverId ?? generateServerId();
|
|
536
|
+
const envPort = process.env.MCP_PORT ? Number(process.env.MCP_PORT) : void 0;
|
|
537
|
+
const requestedPort = options.port ?? (Number.isFinite(envPort) ? envPort : void 0) ?? DEFAULT_PORT;
|
|
538
|
+
const portRange = options.port !== void 0 || Number.isFinite(envPort) ? {
|
|
539
|
+
min: requestedPort,
|
|
540
|
+
max: requestedPort
|
|
541
|
+
} : DEFAULT_PORT_RANGE;
|
|
542
|
+
portLease = await createPortRegistryLease(PORT_REGISTRY_SERVICE_STDIO_HTTP, config.host ?? DEFAULT_HOST$1, requestedPort, serverId, TRANSPORT_TYPE_STDIO_HTTP, resolvedConfigPath, portRange);
|
|
543
|
+
const internalPort = portLease.port;
|
|
544
|
+
const transportConfig = {
|
|
545
|
+
...config,
|
|
546
|
+
port: internalPort
|
|
547
|
+
};
|
|
548
|
+
const stdioHttpHandler = new StdioHttpTransportHandler({ endpoint: new URL(`http://${transportConfig.host}:${transportConfig.port}${MCP_ENDPOINT_PATH}`) });
|
|
549
|
+
const runtimeStateService = new RuntimeStateService();
|
|
531
550
|
const shutdownToken = randomUUID();
|
|
532
|
-
processLease = await createProcessRegistryLease(PROCESS_REGISTRY_SERVICE_STDIO_HTTP,
|
|
551
|
+
processLease = await createProcessRegistryLease(PROCESS_REGISTRY_SERVICE_STDIO_HTTP, transportConfig.host ?? DEFAULT_HOST$1, internalPort, serverId, TRANSPORT_TYPE_STDIO_HTTP, resolvedConfigPath);
|
|
533
552
|
let httpHandler = null;
|
|
534
553
|
let ownsInternalHttpTransport = false;
|
|
535
554
|
let isStopping = false;
|
|
536
|
-
let releaseInternalPort = async () => {
|
|
537
|
-
await releasePortLease(internalPortLease);
|
|
538
|
-
internalPortLease = null;
|
|
539
|
-
releaseInternalPort = async () => void 0;
|
|
540
|
-
};
|
|
541
555
|
const stopOwnedRuntime = async () => {
|
|
542
556
|
if (isStopping) return;
|
|
543
557
|
isStopping = true;
|
|
544
558
|
try {
|
|
545
559
|
await Promise.all([stopInternalHttpTransport(stdioHttpHandler, httpHandler, ownsInternalHttpTransport), removeRuntimeRecord(runtimeStateService, serverId)]);
|
|
546
|
-
await
|
|
560
|
+
await releasePortLease(portLease ?? null);
|
|
547
561
|
if (shared.services) await shared.services.dispose();
|
|
548
562
|
ownsInternalHttpTransport = false;
|
|
549
563
|
process.exit(0);
|
|
@@ -564,15 +578,11 @@ async function startStdioHttpTransport(serverOptions, config, resolvedConfigPath
|
|
|
564
578
|
}
|
|
565
579
|
if (!shared.services) shared.services = await initializeSharedServices(serverOptions);
|
|
566
580
|
try {
|
|
567
|
-
|
|
568
|
-
httpHandler = createStdioHttpInternalTransport(shared.services, config, adminOptions);
|
|
581
|
+
httpHandler = createStdioHttpInternalTransport(shared.services, transportConfig, adminOptions);
|
|
569
582
|
await httpHandler.start();
|
|
570
583
|
ownsInternalHttpTransport = true;
|
|
571
584
|
} catch (error) {
|
|
572
|
-
if (!isAddressInUseError(error)) {
|
|
573
|
-
await releaseInternalPort();
|
|
574
|
-
throw new Error(`Failed to start internal HTTP transport for stdio-http proxy: ${toErrorMessage$9(error)}`);
|
|
575
|
-
}
|
|
585
|
+
if (!isAddressInUseError(error)) throw new Error(`Failed to start internal HTTP transport for stdio-http proxy: ${toErrorMessage$9(error)}`);
|
|
576
586
|
}
|
|
577
587
|
try {
|
|
578
588
|
await stdioHttpHandler.start();
|
|
@@ -586,14 +596,13 @@ async function startStdioHttpTransport(serverOptions, config, resolvedConfigPath
|
|
|
586
596
|
}
|
|
587
597
|
ownsInternalHttpTransport = false;
|
|
588
598
|
}
|
|
589
|
-
await releaseInternalPort();
|
|
590
599
|
const retryErrorMessage = toErrorMessage$9(error);
|
|
591
600
|
const initialErrorMessage = toErrorMessage$9(initialProxyConnectError);
|
|
592
601
|
const rollbackMessage = rollbackStopErrorMessage ? `; rollback stop failed: ${rollbackStopErrorMessage}` : "";
|
|
593
602
|
throw new Error(`Failed to start stdio-http proxy bridge: initial connect failed (${initialErrorMessage}); retry failed (${retryErrorMessage})${rollbackMessage}`);
|
|
594
603
|
}
|
|
595
604
|
if (ownsInternalHttpTransport) try {
|
|
596
|
-
await writeRuntimeRecord(runtimeStateService, createRuntimeRecord(serverId,
|
|
605
|
+
await writeRuntimeRecord(runtimeStateService, createRuntimeRecord(serverId, transportConfig, internalPort, shutdownToken, resolvedConfigPath));
|
|
597
606
|
} catch (error) {
|
|
598
607
|
throw new Error(`Failed to persist runtime state for stdio-http server '${serverId}': ${toErrorMessage$9(error)}`);
|
|
599
608
|
}
|
|
@@ -601,7 +610,7 @@ async function startStdioHttpTransport(serverOptions, config, resolvedConfigPath
|
|
|
601
610
|
async stop() {
|
|
602
611
|
try {
|
|
603
612
|
await Promise.all([stopInternalHttpTransport(stdioHttpHandler, httpHandler, ownsInternalHttpTransport), removeRuntimeRecord(runtimeStateService, serverId)]);
|
|
604
|
-
await
|
|
613
|
+
await releasePortLease(portLease ?? null);
|
|
605
614
|
await processLease?.release({
|
|
606
615
|
kill: false,
|
|
607
616
|
releasePort: false
|
|
@@ -616,7 +625,7 @@ async function startStdioHttpTransport(serverOptions, config, resolvedConfigPath
|
|
|
616
625
|
});
|
|
617
626
|
} catch (error) {
|
|
618
627
|
try {
|
|
619
|
-
await
|
|
628
|
+
await portLease?.release();
|
|
620
629
|
await processLease?.release({
|
|
621
630
|
kill: false,
|
|
622
631
|
releasePort: false
|
|
@@ -640,7 +649,7 @@ async function startTransport(transportType, options, resolvedConfigPath, server
|
|
|
640
649
|
await startSseTransport(serverOptions, createTransportConfig(options, TRANSPORT_MODE.SSE));
|
|
641
650
|
return;
|
|
642
651
|
}
|
|
643
|
-
await startStdioHttpTransport(serverOptions, createTransportConfig(options, TRANSPORT_MODE.HTTP), resolvedConfigPath);
|
|
652
|
+
await startStdioHttpTransport(serverOptions, createTransportConfig(options, TRANSPORT_MODE.HTTP), resolvedConfigPath, options);
|
|
644
653
|
} catch (error) {
|
|
645
654
|
throw new Error(`Failed to start transport '${transportType}': ${toErrorMessage$9(error)}`);
|
|
646
655
|
}
|
|
@@ -648,7 +657,7 @@ async function startTransport(transportType, options, resolvedConfigPath, server
|
|
|
648
657
|
/**
|
|
649
658
|
* MCP Serve command
|
|
650
659
|
*/
|
|
651
|
-
const mcpServeCommand = new Command("mcp-serve").description("Start MCP server with specified transport").option("-t, --type <type>", `Transport type: ${TRANSPORT_TYPE_STDIO}, ${TRANSPORT_TYPE_HTTP}, ${TRANSPORT_TYPE_SSE}, or ${TRANSPORT_TYPE_STDIO_HTTP}`, TRANSPORT_TYPE_STDIO).option("-p, --port <port>", "Port to listen on (http/sse/stdio-http internal HTTP)", (val) => parseInt(val, 10)
|
|
660
|
+
const mcpServeCommand = new Command("mcp-serve").description("Start MCP server with specified transport").option("-t, --type <type>", `Transport type: ${TRANSPORT_TYPE_STDIO}, ${TRANSPORT_TYPE_HTTP}, ${TRANSPORT_TYPE_SSE}, or ${TRANSPORT_TYPE_STDIO_HTTP}`, TRANSPORT_TYPE_STDIO).option("-p, --port <port>", "Port to listen on (http/sse/stdio-http internal HTTP)", (val) => parseInt(val, 10)).option("--host <host>", "Host to bind to (http/sse/stdio-http internal HTTP)", DEFAULT_HOST$1).option("-c, --config <path>", "Path to MCP server configuration file").option("--no-cache", "Disable configuration caching, always reload from config file").option("--definitions-cache <path>", "Path to prefetched tool/prompt/skill definitions cache file").option("--clear-definitions-cache", "Delete definitions cache before startup", false).option("--proxy-mode <mode>", "How mcp-proxy exposes downstream tools: meta, flat, or search", "meta").option("--id <id>", "Unique server identifier (overrides config file id, auto-generated if not provided)").action(async (options) => {
|
|
652
661
|
try {
|
|
653
662
|
const transportType = validateTransportType(options.type.toLowerCase());
|
|
654
663
|
validateProxyMode(options.proxyMode);
|
|
@@ -657,11 +666,161 @@ const mcpServeCommand = new Command("mcp-serve").description("Start MCP server w
|
|
|
657
666
|
} catch (error) {
|
|
658
667
|
const rawTransportType = options.type.toLowerCase();
|
|
659
668
|
const transportType = isValidTransportType(rawTransportType) ? rawTransportType : TRANSPORT_TYPE_STDIO;
|
|
660
|
-
console.error(formatStartError(transportType, options.host, options.port, error));
|
|
669
|
+
console.error(formatStartError(transportType, options.host, options.port ?? DEFAULT_PORT, error));
|
|
661
670
|
process.exit(1);
|
|
662
671
|
}
|
|
663
672
|
});
|
|
664
673
|
|
|
674
|
+
//#endregion
|
|
675
|
+
//#region src/commands/prestart-http.ts
|
|
676
|
+
/**
|
|
677
|
+
* Prestart HTTP Command
|
|
678
|
+
*
|
|
679
|
+
* Starts an mcp-proxy HTTP runtime in the background, waits until it is healthy,
|
|
680
|
+
* and then exits so the runtime can be reused by other commands.
|
|
681
|
+
*/
|
|
682
|
+
const WORKSPACE_MARKERS = [
|
|
683
|
+
"pnpm-workspace.yaml",
|
|
684
|
+
"nx.json",
|
|
685
|
+
".git"
|
|
686
|
+
];
|
|
687
|
+
const DEFAULT_HOST = "localhost";
|
|
688
|
+
const DEFAULT_TIMEOUT_MS = 12e4;
|
|
689
|
+
const POLL_INTERVAL_MS = 250;
|
|
690
|
+
function resolveWorkspaceRoot(startPath = process.cwd()) {
|
|
691
|
+
let current = path.resolve(startPath);
|
|
692
|
+
while (true) {
|
|
693
|
+
for (const marker of WORKSPACE_MARKERS) if (existsSync(path.join(current, marker))) return current;
|
|
694
|
+
const parent = path.dirname(current);
|
|
695
|
+
if (parent === current) return process.cwd();
|
|
696
|
+
current = parent;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
function resolveSiblingRegistryPath(registryPath, fileName) {
|
|
700
|
+
if (!registryPath) return;
|
|
701
|
+
const resolved = path.resolve(registryPath);
|
|
702
|
+
if (path.extname(resolved) === ".json") return path.join(path.dirname(resolved), fileName);
|
|
703
|
+
return path.join(resolved, fileName);
|
|
704
|
+
}
|
|
705
|
+
function buildCliCandidates() {
|
|
706
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
707
|
+
const __dirname = path.dirname(__filename);
|
|
708
|
+
const distCandidates = [
|
|
709
|
+
path.resolve(__dirname, "cli.mjs"),
|
|
710
|
+
path.resolve(__dirname, "..", "dist", "cli.mjs"),
|
|
711
|
+
path.resolve(__dirname, "..", "..", "dist", "cli.mjs")
|
|
712
|
+
];
|
|
713
|
+
const srcCandidates = [path.resolve(__dirname, "..", "cli.ts"), path.resolve(__dirname, "..", "..", "src", "cli.ts")];
|
|
714
|
+
for (const candidate of distCandidates) if (existsSync(candidate)) return {
|
|
715
|
+
command: process.execPath,
|
|
716
|
+
args: [candidate]
|
|
717
|
+
};
|
|
718
|
+
for (const candidate of srcCandidates) if (existsSync(candidate)) return {
|
|
719
|
+
command: process.execPath,
|
|
720
|
+
args: [
|
|
721
|
+
"--import",
|
|
722
|
+
"tsx",
|
|
723
|
+
candidate
|
|
724
|
+
]
|
|
725
|
+
};
|
|
726
|
+
throw new Error("Unable to locate mcp-proxy CLI entrypoint");
|
|
727
|
+
}
|
|
728
|
+
function parseTimeoutMs(value) {
|
|
729
|
+
if (!value) return DEFAULT_TIMEOUT_MS;
|
|
730
|
+
const parsed = Number.parseInt(value, 10);
|
|
731
|
+
if (!Number.isInteger(parsed) || parsed <= 0) throw new Error(`Invalid timeout value: ${value}`);
|
|
732
|
+
return parsed;
|
|
733
|
+
}
|
|
734
|
+
async function waitForFile(filePath, timeoutMs) {
|
|
735
|
+
const deadline = Date.now() + timeoutMs;
|
|
736
|
+
while (Date.now() < deadline) {
|
|
737
|
+
try {
|
|
738
|
+
await access(filePath);
|
|
739
|
+
return;
|
|
740
|
+
} catch {}
|
|
741
|
+
await new Promise((resolve$1) => setTimeout(resolve$1, POLL_INTERVAL_MS));
|
|
742
|
+
}
|
|
743
|
+
throw new Error(`Timed out waiting for runtime state file: ${filePath}`);
|
|
744
|
+
}
|
|
745
|
+
async function waitForHealthyRuntime(serverId, timeoutMs) {
|
|
746
|
+
const runtimeStateService = new RuntimeStateService();
|
|
747
|
+
const deadline = Date.now() + timeoutMs;
|
|
748
|
+
while (Date.now() < deadline) {
|
|
749
|
+
const record = await runtimeStateService.read(serverId);
|
|
750
|
+
if (record) {
|
|
751
|
+
const healthUrl = `http://${record.host}:${record.port}/health`;
|
|
752
|
+
try {
|
|
753
|
+
const response = await fetch(healthUrl);
|
|
754
|
+
if (response.ok) {
|
|
755
|
+
const payload = await response.json().catch(() => null);
|
|
756
|
+
if (!payload || payload.transport === "http") return {
|
|
757
|
+
host: record.host,
|
|
758
|
+
port: record.port
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
} catch {}
|
|
762
|
+
}
|
|
763
|
+
await new Promise((resolve$1) => setTimeout(resolve$1, POLL_INTERVAL_MS));
|
|
764
|
+
}
|
|
765
|
+
throw new Error(`Timed out waiting for HTTP runtime '${serverId}' to become healthy`);
|
|
766
|
+
}
|
|
767
|
+
function spawnBackgroundRuntime(args, env) {
|
|
768
|
+
const { command, args: baseArgs } = buildCliCandidates();
|
|
769
|
+
const child = spawn(command, [...baseArgs, ...args], {
|
|
770
|
+
detached: true,
|
|
771
|
+
stdio: "ignore",
|
|
772
|
+
env
|
|
773
|
+
});
|
|
774
|
+
child.unref();
|
|
775
|
+
return child;
|
|
776
|
+
}
|
|
777
|
+
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).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) => {
|
|
778
|
+
const serverId = options.id || generateServerId();
|
|
779
|
+
const timeoutMs = parseTimeoutMs(options.timeoutMs);
|
|
780
|
+
const registryPath = options.registryPath || options.registryDir;
|
|
781
|
+
new RuntimeStateService();
|
|
782
|
+
const workspaceRoot = resolveWorkspaceRoot(process.cwd());
|
|
783
|
+
const childEnv = {
|
|
784
|
+
...process.env,
|
|
785
|
+
...registryPath ? {
|
|
786
|
+
PORT_REGISTRY_PATH: registryPath,
|
|
787
|
+
PROCESS_REGISTRY_PATH: resolveSiblingRegistryPath(registryPath, "processes.json")
|
|
788
|
+
} : {}
|
|
789
|
+
};
|
|
790
|
+
const child = spawnBackgroundRuntime([
|
|
791
|
+
"mcp-serve",
|
|
792
|
+
"--type",
|
|
793
|
+
"http",
|
|
794
|
+
"--id",
|
|
795
|
+
serverId,
|
|
796
|
+
"--host",
|
|
797
|
+
options.host || DEFAULT_HOST,
|
|
798
|
+
...options.port !== void 0 ? ["--port", String(options.port)] : [],
|
|
799
|
+
...options.config ? ["--config", options.config] : [],
|
|
800
|
+
...options.cache === false ? ["--no-cache"] : [],
|
|
801
|
+
...options.definitionsCache ? ["--definitions-cache", options.definitionsCache] : [],
|
|
802
|
+
...options.clearDefinitionsCache ? ["--clear-definitions-cache"] : [],
|
|
803
|
+
"--proxy-mode",
|
|
804
|
+
options.proxyMode
|
|
805
|
+
], childEnv);
|
|
806
|
+
const childExit = new Promise((_, reject) => {
|
|
807
|
+
child.once("exit", (code, signal) => {
|
|
808
|
+
reject(/* @__PURE__ */ new Error(`Background runtime exited before becoming healthy (code=${code ?? "null"}, signal=${signal ?? "null"})`));
|
|
809
|
+
});
|
|
810
|
+
});
|
|
811
|
+
const runtimeFile = path.join(RuntimeStateService.getDefaultRuntimeDir(), `${serverId}.runtime.json`);
|
|
812
|
+
try {
|
|
813
|
+
await Promise.race([waitForFile(runtimeFile, timeoutMs), childExit]);
|
|
814
|
+
const { host, port } = await Promise.race([waitForHealthyRuntime(serverId, timeoutMs), childExit]);
|
|
815
|
+
process.stdout.write(`mcp-proxy HTTP runtime ready at http://${host}:${port} (${serverId})\n`);
|
|
816
|
+
process.stdout.write(`runtimeId=${serverId}\n`);
|
|
817
|
+
process.stdout.write(`runtimeUrl=http://${host}:${port}\n`);
|
|
818
|
+
process.stdout.write(`workspaceRoot=${workspaceRoot}\n`);
|
|
819
|
+
} catch (error) {
|
|
820
|
+
throw new Error(`Failed to prestart HTTP runtime '${serverId}': ${error instanceof Error ? error.message : String(error)}`);
|
|
821
|
+
}
|
|
822
|
+
});
|
|
823
|
+
|
|
665
824
|
//#endregion
|
|
666
825
|
//#region src/commands/bootstrap.ts
|
|
667
826
|
function toErrorMessage$8(error) {
|
|
@@ -1490,6 +1649,7 @@ async function main() {
|
|
|
1490
1649
|
program.name("mcp-proxy").description("MCP proxy server package").version(version);
|
|
1491
1650
|
program.addCommand(initCommand);
|
|
1492
1651
|
program.addCommand(mcpServeCommand);
|
|
1652
|
+
program.addCommand(prestartHttpCommand);
|
|
1493
1653
|
program.addCommand(searchToolsCommand);
|
|
1494
1654
|
program.addCommand(describeToolsCommand);
|
|
1495
1655
|
program.addCommand(useToolCommand);
|
package/dist/index.cjs
CHANGED
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { C as SearchListToolsTool, D as findConfigFile, E as generateServerId, S as UseToolTool, T as DefinitionsCacheService, _ as StopServerService, a as createProxyContainer, b as createProxyLogger, c as createStdioHttpTransportHandler, f as StdioHttpTransportHandler, g as SkillService, h as HttpTransportHandler, i as createHttpTransportHandler, l as createStdioTransportHandler, m as SseTransportHandler, n as createServer, p as StdioTransportHandler, r as createSessionServer, s as createSseTransportHandler, t as TRANSPORT_MODE, u as initializeSharedServices, v as RuntimeStateService, w as DescribeToolsTool, x as ConfigFetcherService, y as McpClientManagerService } from "./src-
|
|
1
|
+
import { C as SearchListToolsTool, D as findConfigFile, E as generateServerId, S as UseToolTool, T as DefinitionsCacheService, _ as StopServerService, a as createProxyContainer, b as createProxyLogger, c as createStdioHttpTransportHandler, f as StdioHttpTransportHandler, g as SkillService, h as HttpTransportHandler, i as createHttpTransportHandler, l as createStdioTransportHandler, m as SseTransportHandler, n as createServer, p as StdioTransportHandler, r as createSessionServer, s as createSseTransportHandler, t as TRANSPORT_MODE, u as initializeSharedServices, v as RuntimeStateService, w as DescribeToolsTool, x as ConfigFetcherService, y as McpClientManagerService } from "./src-CNXwWpA2.mjs";
|
|
2
2
|
|
|
3
3
|
export { ConfigFetcherService, DefinitionsCacheService, DescribeToolsTool, HttpTransportHandler, McpClientManagerService, RuntimeStateService, SearchListToolsTool, SkillService, SseTransportHandler, StdioHttpTransportHandler, StdioTransportHandler, StopServerService, TRANSPORT_MODE, UseToolTool, createHttpTransportHandler, createProxyContainer, createProxyLogger, createServer, createSessionServer, createSseTransportHandler, createStdioHttpTransportHandler, createStdioTransportHandler, findConfigFile, generateServerId, initializeSharedServices };
|
|
@@ -2758,9 +2758,9 @@ function isShutdownResponse(value) {
|
|
|
2758
2758
|
* @param path - Request path to append
|
|
2759
2759
|
* @returns Full runtime URL
|
|
2760
2760
|
*/
|
|
2761
|
-
function buildRuntimeUrl(runtime, path) {
|
|
2761
|
+
function buildRuntimeUrl(runtime, path$1) {
|
|
2762
2762
|
if (!ALLOWED_HOSTS.has(runtime.host)) throw new Error(`Refusing to connect to non-loopback host '${runtime.host}'. Only ${Array.from(ALLOWED_HOSTS).join(", ")} are allowed.`);
|
|
2763
|
-
return `${HTTP_PROTOCOL}${runtime.host}${URL_PORT_SEPARATOR}${runtime.port}${path}`;
|
|
2763
|
+
return `${HTTP_PROTOCOL}${runtime.host}${URL_PORT_SEPARATOR}${runtime.port}${path$1}`;
|
|
2764
2764
|
}
|
|
2765
2765
|
function toErrorMessage$1(error) {
|
|
2766
2766
|
return error instanceof Error ? error.message : String(error);
|
|
@@ -2938,13 +2938,13 @@ var SkillLoadError = class extends Error {
|
|
|
2938
2938
|
* @returns true if path exists, false otherwise
|
|
2939
2939
|
* @throws Error for unexpected filesystem errors (permission denied, etc.)
|
|
2940
2940
|
*/
|
|
2941
|
-
async function pathExists(path) {
|
|
2941
|
+
async function pathExists(path$1) {
|
|
2942
2942
|
try {
|
|
2943
|
-
await access(path);
|
|
2943
|
+
await access(path$1);
|
|
2944
2944
|
return true;
|
|
2945
2945
|
} catch (error) {
|
|
2946
2946
|
if (error instanceof Error && "code" in error && error.code === "ENOENT") return false;
|
|
2947
|
-
throw new Error(`Failed to check path existence for "${path}": ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
2947
|
+
throw new Error(`Failed to check path existence for "${path$1}": ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
2948
2948
|
}
|
|
2949
2949
|
}
|
|
2950
2950
|
/**
|
|
@@ -4313,7 +4313,7 @@ var StdioHttpTransportHandler = class {
|
|
|
4313
4313
|
|
|
4314
4314
|
//#endregion
|
|
4315
4315
|
//#region package.json
|
|
4316
|
-
var version = "0.
|
|
4316
|
+
var version = "0.4.1";
|
|
4317
4317
|
|
|
4318
4318
|
//#endregion
|
|
4319
4319
|
//#region src/container/index.ts
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agimon-ai/mcp-proxy",
|
|
3
3
|
"description": "MCP proxy server package",
|
|
4
|
-
"version": "0.4.
|
|
4
|
+
"version": "0.4.2",
|
|
5
5
|
"license": "AGPL-3.0",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"mcp",
|
|
@@ -27,9 +27,9 @@
|
|
|
27
27
|
"js-yaml": "^4.1.0",
|
|
28
28
|
"liquidjs": "^10.21.0",
|
|
29
29
|
"zod": "^3.24.1",
|
|
30
|
-
"@agimon-ai/foundation-
|
|
31
|
-
"@agimon-ai/
|
|
32
|
-
"@agimon-ai/
|
|
30
|
+
"@agimon-ai/foundation-port-registry": "0.2.6",
|
|
31
|
+
"@agimon-ai/log-sink-mcp": "0.2.6",
|
|
32
|
+
"@agimon-ai/foundation-process-registry": "0.2.2"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"@types/express": "^5.0.0",
|