@agimon-ai/mcp-proxy 0.5.2 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.cjs +38 -10
- package/dist/cli.mjs +38 -10
- package/dist/index.cjs +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{src-kgJ-iu3i.mjs → src-96J-GtMf.mjs} +70 -15
- package/dist/{src-DUR0uWiY.cjs → src-B6WquglB.cjs} +70 -15
- package/package.json +4 -4
package/dist/cli.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
const require_src = require('./src-
|
|
2
|
+
const require_src = require('./src-B6WquglB.cjs');
|
|
3
3
|
let node_fs = require("node:fs");
|
|
4
4
|
let node_fs_promises = require("node:fs/promises");
|
|
5
5
|
let js_yaml = require("js-yaml");
|
|
@@ -155,7 +155,8 @@ async function findExistingHealthyRuntime(workspaceRoot) {
|
|
|
155
155
|
host: match.host,
|
|
156
156
|
port: match.port,
|
|
157
157
|
serverId: metadata?.serverId ?? "unknown",
|
|
158
|
-
workspaceRoot
|
|
158
|
+
workspaceRoot,
|
|
159
|
+
reusedExistingRuntime: true
|
|
159
160
|
};
|
|
160
161
|
}
|
|
161
162
|
} catch {}
|
|
@@ -297,7 +298,8 @@ async function prestartHttpRuntime(options) {
|
|
|
297
298
|
host,
|
|
298
299
|
port,
|
|
299
300
|
serverId,
|
|
300
|
-
workspaceRoot
|
|
301
|
+
workspaceRoot,
|
|
302
|
+
reusedExistingRuntime: false
|
|
301
303
|
};
|
|
302
304
|
} catch (error) {
|
|
303
305
|
throw new Error(`Failed to prestart HTTP runtime '${serverId}': ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
@@ -408,7 +410,8 @@ function loadProxyDefaults(configPath) {
|
|
|
408
410
|
return {
|
|
409
411
|
type: typeof p.type === "string" ? p.type : void 0,
|
|
410
412
|
port: typeof p.port === "number" && Number.isInteger(p.port) && p.port > 0 ? p.port : void 0,
|
|
411
|
-
host: typeof p.host === "string" ? p.host : void 0
|
|
413
|
+
host: typeof p.host === "string" ? p.host : void 0,
|
|
414
|
+
keepAlive: typeof p.keepAlive === "boolean" ? p.keepAlive : void 0
|
|
412
415
|
};
|
|
413
416
|
} catch {
|
|
414
417
|
return {};
|
|
@@ -724,7 +727,7 @@ async function startSseTransport(serverOptions, config) {
|
|
|
724
727
|
}
|
|
725
728
|
async function resolveStdioHttpEndpoint(config, options, resolvedConfigPath) {
|
|
726
729
|
const repositoryPath = getRegistryRepositoryPath();
|
|
727
|
-
if (config.port !== void 0) return new URL(`http://${config.host ?? DEFAULT_HOST}:${config.port}${MCP_ENDPOINT_PATH}`);
|
|
730
|
+
if (config.port !== void 0) return { endpoint: new URL(`http://${config.host ?? DEFAULT_HOST}:${config.port}${MCP_ENDPOINT_PATH}`) };
|
|
728
731
|
const portRegistry = createPortRegistryService();
|
|
729
732
|
const result = await portRegistry.getPort({
|
|
730
733
|
repositoryPath,
|
|
@@ -737,7 +740,7 @@ async function resolveStdioHttpEndpoint(config, options, resolvedConfigPath) {
|
|
|
737
740
|
const endpoint = new URL(`http://${host}:${result.record.port}${MCP_ENDPOINT_PATH}`);
|
|
738
741
|
try {
|
|
739
742
|
const healthUrl = `http://${host}:${result.record.port}/health`;
|
|
740
|
-
if ((await fetch(healthUrl)).ok) return endpoint;
|
|
743
|
+
if ((await fetch(healthUrl)).ok) return { endpoint };
|
|
741
744
|
} catch {}
|
|
742
745
|
await portRegistry.releasePort({
|
|
743
746
|
repositoryPath,
|
|
@@ -755,12 +758,37 @@ async function resolveStdioHttpEndpoint(config, options, resolvedConfigPath) {
|
|
|
755
758
|
clearDefinitionsCache: options.clearDefinitionsCache,
|
|
756
759
|
proxyMode: options.proxyMode
|
|
757
760
|
});
|
|
758
|
-
return
|
|
761
|
+
return {
|
|
762
|
+
endpoint: new URL(`http://${runtime.host}:${runtime.port}${MCP_ENDPOINT_PATH}`),
|
|
763
|
+
ownedRuntimeServerId: runtime.reusedExistingRuntime ? void 0 : runtime.serverId
|
|
764
|
+
};
|
|
759
765
|
}
|
|
760
|
-
async function startStdioHttpTransport(config, options, resolvedConfigPath) {
|
|
766
|
+
async function startStdioHttpTransport(config, options, resolvedConfigPath, proxyDefaults) {
|
|
767
|
+
let ownedRuntimeServerId;
|
|
768
|
+
const keepAlive = proxyDefaults?.keepAlive ?? false;
|
|
761
769
|
try {
|
|
762
|
-
|
|
770
|
+
const resolvedEndpoint = await resolveStdioHttpEndpoint(config, options, resolvedConfigPath);
|
|
771
|
+
ownedRuntimeServerId = resolvedEndpoint.ownedRuntimeServerId;
|
|
772
|
+
const { endpoint } = resolvedEndpoint;
|
|
773
|
+
await startServer(new require_src.StdioHttpTransportHandler({ endpoint }, createStdioSafeLogger()), async () => {
|
|
774
|
+
if (keepAlive || !ownedRuntimeServerId) return;
|
|
775
|
+
await new require_src.StopServerService().stop({
|
|
776
|
+
serverId: ownedRuntimeServerId,
|
|
777
|
+
force: true
|
|
778
|
+
});
|
|
779
|
+
});
|
|
763
780
|
} catch (error) {
|
|
781
|
+
if (!keepAlive && ownedRuntimeServerId) {
|
|
782
|
+
const stopServerService = new require_src.StopServerService();
|
|
783
|
+
try {
|
|
784
|
+
await stopServerService.stop({
|
|
785
|
+
serverId: ownedRuntimeServerId,
|
|
786
|
+
force: true
|
|
787
|
+
});
|
|
788
|
+
} catch (cleanupError) {
|
|
789
|
+
throw new Error(`Failed to start stdio-http transport: ${toErrorMessage$9(error)}; also failed to stop owned HTTP runtime '${ownedRuntimeServerId}': ${toErrorMessage$9(cleanupError)}`);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
764
792
|
throw new Error(`Failed to start stdio-http transport: ${toErrorMessage$9(error)}`);
|
|
765
793
|
}
|
|
766
794
|
}
|
|
@@ -778,7 +806,7 @@ async function startTransport(transportType, options, resolvedConfigPath, server
|
|
|
778
806
|
await startSseTransport(serverOptions, createTransportConfig(options, require_src.TRANSPORT_MODE.SSE, proxyDefaults));
|
|
779
807
|
return;
|
|
780
808
|
}
|
|
781
|
-
await startStdioHttpTransport(createTransportConfig(options, require_src.TRANSPORT_MODE.HTTP, proxyDefaults), options, resolvedConfigPath);
|
|
809
|
+
await startStdioHttpTransport(createTransportConfig(options, require_src.TRANSPORT_MODE.HTTP, proxyDefaults), options, resolvedConfigPath, proxyDefaults);
|
|
782
810
|
} catch (error) {
|
|
783
811
|
throw new Error(`Failed to start transport '${transportType}': ${toErrorMessage$9(error)}`);
|
|
784
812
|
}
|
package/dist/cli.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
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-
|
|
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-96J-GtMf.mjs";
|
|
3
3
|
import { constants, existsSync, readFileSync } from "node:fs";
|
|
4
4
|
import { access, writeFile } from "node:fs/promises";
|
|
5
5
|
import yaml from "js-yaml";
|
|
@@ -153,7 +153,8 @@ async function findExistingHealthyRuntime(workspaceRoot) {
|
|
|
153
153
|
host: match.host,
|
|
154
154
|
port: match.port,
|
|
155
155
|
serverId: metadata?.serverId ?? "unknown",
|
|
156
|
-
workspaceRoot
|
|
156
|
+
workspaceRoot,
|
|
157
|
+
reusedExistingRuntime: true
|
|
157
158
|
};
|
|
158
159
|
}
|
|
159
160
|
} catch {}
|
|
@@ -295,7 +296,8 @@ async function prestartHttpRuntime(options) {
|
|
|
295
296
|
host,
|
|
296
297
|
port,
|
|
297
298
|
serverId,
|
|
298
|
-
workspaceRoot
|
|
299
|
+
workspaceRoot,
|
|
300
|
+
reusedExistingRuntime: false
|
|
299
301
|
};
|
|
300
302
|
} catch (error) {
|
|
301
303
|
throw new Error(`Failed to prestart HTTP runtime '${serverId}': ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
@@ -406,7 +408,8 @@ function loadProxyDefaults(configPath) {
|
|
|
406
408
|
return {
|
|
407
409
|
type: typeof p.type === "string" ? p.type : void 0,
|
|
408
410
|
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
|
|
411
|
+
host: typeof p.host === "string" ? p.host : void 0,
|
|
412
|
+
keepAlive: typeof p.keepAlive === "boolean" ? p.keepAlive : void 0
|
|
410
413
|
};
|
|
411
414
|
} catch {
|
|
412
415
|
return {};
|
|
@@ -722,7 +725,7 @@ async function startSseTransport(serverOptions, config) {
|
|
|
722
725
|
}
|
|
723
726
|
async function resolveStdioHttpEndpoint(config, options, resolvedConfigPath) {
|
|
724
727
|
const repositoryPath = getRegistryRepositoryPath();
|
|
725
|
-
if (config.port !== void 0) return new URL(`http://${config.host ?? DEFAULT_HOST}:${config.port}${MCP_ENDPOINT_PATH}`);
|
|
728
|
+
if (config.port !== void 0) return { endpoint: new URL(`http://${config.host ?? DEFAULT_HOST}:${config.port}${MCP_ENDPOINT_PATH}`) };
|
|
726
729
|
const portRegistry = createPortRegistryService();
|
|
727
730
|
const result = await portRegistry.getPort({
|
|
728
731
|
repositoryPath,
|
|
@@ -735,7 +738,7 @@ async function resolveStdioHttpEndpoint(config, options, resolvedConfigPath) {
|
|
|
735
738
|
const endpoint = new URL(`http://${host}:${result.record.port}${MCP_ENDPOINT_PATH}`);
|
|
736
739
|
try {
|
|
737
740
|
const healthUrl = `http://${host}:${result.record.port}/health`;
|
|
738
|
-
if ((await fetch(healthUrl)).ok) return endpoint;
|
|
741
|
+
if ((await fetch(healthUrl)).ok) return { endpoint };
|
|
739
742
|
} catch {}
|
|
740
743
|
await portRegistry.releasePort({
|
|
741
744
|
repositoryPath,
|
|
@@ -753,12 +756,37 @@ async function resolveStdioHttpEndpoint(config, options, resolvedConfigPath) {
|
|
|
753
756
|
clearDefinitionsCache: options.clearDefinitionsCache,
|
|
754
757
|
proxyMode: options.proxyMode
|
|
755
758
|
});
|
|
756
|
-
return
|
|
759
|
+
return {
|
|
760
|
+
endpoint: new URL(`http://${runtime.host}:${runtime.port}${MCP_ENDPOINT_PATH}`),
|
|
761
|
+
ownedRuntimeServerId: runtime.reusedExistingRuntime ? void 0 : runtime.serverId
|
|
762
|
+
};
|
|
757
763
|
}
|
|
758
|
-
async function startStdioHttpTransport(config, options, resolvedConfigPath) {
|
|
764
|
+
async function startStdioHttpTransport(config, options, resolvedConfigPath, proxyDefaults) {
|
|
765
|
+
let ownedRuntimeServerId;
|
|
766
|
+
const keepAlive = proxyDefaults?.keepAlive ?? false;
|
|
759
767
|
try {
|
|
760
|
-
|
|
768
|
+
const resolvedEndpoint = await resolveStdioHttpEndpoint(config, options, resolvedConfigPath);
|
|
769
|
+
ownedRuntimeServerId = resolvedEndpoint.ownedRuntimeServerId;
|
|
770
|
+
const { endpoint } = resolvedEndpoint;
|
|
771
|
+
await startServer(new StdioHttpTransportHandler({ endpoint }, createStdioSafeLogger()), async () => {
|
|
772
|
+
if (keepAlive || !ownedRuntimeServerId) return;
|
|
773
|
+
await new StopServerService().stop({
|
|
774
|
+
serverId: ownedRuntimeServerId,
|
|
775
|
+
force: true
|
|
776
|
+
});
|
|
777
|
+
});
|
|
761
778
|
} catch (error) {
|
|
779
|
+
if (!keepAlive && ownedRuntimeServerId) {
|
|
780
|
+
const stopServerService = new StopServerService();
|
|
781
|
+
try {
|
|
782
|
+
await stopServerService.stop({
|
|
783
|
+
serverId: ownedRuntimeServerId,
|
|
784
|
+
force: true
|
|
785
|
+
});
|
|
786
|
+
} catch (cleanupError) {
|
|
787
|
+
throw new Error(`Failed to start stdio-http transport: ${toErrorMessage$9(error)}; also failed to stop owned HTTP runtime '${ownedRuntimeServerId}': ${toErrorMessage$9(cleanupError)}`);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
762
790
|
throw new Error(`Failed to start stdio-http transport: ${toErrorMessage$9(error)}`);
|
|
763
791
|
}
|
|
764
792
|
}
|
|
@@ -776,7 +804,7 @@ async function startTransport(transportType, options, resolvedConfigPath, server
|
|
|
776
804
|
await startSseTransport(serverOptions, createTransportConfig(options, TRANSPORT_MODE.SSE, proxyDefaults));
|
|
777
805
|
return;
|
|
778
806
|
}
|
|
779
|
-
await startStdioHttpTransport(createTransportConfig(options, TRANSPORT_MODE.HTTP, proxyDefaults), options, resolvedConfigPath);
|
|
807
|
+
await startStdioHttpTransport(createTransportConfig(options, TRANSPORT_MODE.HTTP, proxyDefaults), options, resolvedConfigPath, proxyDefaults);
|
|
780
808
|
} catch (error) {
|
|
781
809
|
throw new Error(`Failed to start transport '${transportType}': ${toErrorMessage$9(error)}`);
|
|
782
810
|
}
|
package/dist/index.cjs
CHANGED
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { C as DefinitionsCacheService, E as ConfigFetcherService, S as createProxyLogger, T as findConfigFile, _ as DescribeToolsTool, a as createProxyContainer, b as RuntimeStateService, c as createStdioHttpTransportHandler, d as StdioHttpTransportHandler, f as StdioTransportHandler, g as SearchListToolsTool, h as UseToolTool, i as createHttpTransportHandler, l as createStdioTransportHandler, m as HttpTransportHandler, n as createServer, p as SseTransportHandler, r as createSessionServer, s as createSseTransportHandler, t as TRANSPORT_MODE, u as initializeSharedServices, v as SkillService, w as generateServerId, x as McpClientManagerService, y as StopServerService } from "./src-
|
|
1
|
+
import { C as DefinitionsCacheService, E as ConfigFetcherService, S as createProxyLogger, T as findConfigFile, _ as DescribeToolsTool, a as createProxyContainer, b as RuntimeStateService, c as createStdioHttpTransportHandler, d as StdioHttpTransportHandler, f as StdioTransportHandler, g as SearchListToolsTool, h as UseToolTool, i as createHttpTransportHandler, l as createStdioTransportHandler, m as HttpTransportHandler, n as createServer, p as SseTransportHandler, r as createSessionServer, s as createSseTransportHandler, t as TRANSPORT_MODE, u as initializeSharedServices, v as SkillService, w as generateServerId, x as McpClientManagerService, y as StopServerService } from "./src-96J-GtMf.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 };
|
|
@@ -24,7 +24,7 @@ import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
|
24
24
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
25
25
|
|
|
26
26
|
//#region package.json
|
|
27
|
-
var version = "0.
|
|
27
|
+
var version = "0.6.0";
|
|
28
28
|
|
|
29
29
|
//#endregion
|
|
30
30
|
//#region src/utils/mcpConfigSchema.ts
|
|
@@ -263,7 +263,8 @@ const ProxyConfigSchema = z.object({
|
|
|
263
263
|
"stdio-http"
|
|
264
264
|
]).optional(),
|
|
265
265
|
port: z.number().int().positive().optional(),
|
|
266
|
-
host: z.string().optional()
|
|
266
|
+
host: z.string().optional(),
|
|
267
|
+
keepAlive: z.boolean().optional()
|
|
267
268
|
}).optional();
|
|
268
269
|
/**
|
|
269
270
|
* Full Claude Code MCP configuration schema
|
|
@@ -1537,8 +1538,40 @@ const DEFAULT_CONNECTION_TIMEOUT_MS = 3e4;
|
|
|
1537
1538
|
* (e.g., downstream server restarted and no longer recognizes the session ID).
|
|
1538
1539
|
*/
|
|
1539
1540
|
function isSessionError(error) {
|
|
1540
|
-
|
|
1541
|
-
|
|
1541
|
+
return getErrorChain(error).some(({ message, code }) => {
|
|
1542
|
+
const normalizedMessage = message.toLowerCase();
|
|
1543
|
+
const normalizedCode = code?.toLowerCase();
|
|
1544
|
+
return normalizedMessage.includes("unknown session") || normalizedMessage.includes("session not found") || normalizedMessage.includes("transport closed") || normalizedMessage.includes("connection closed") || normalizedMessage.includes("socket hang up") || normalizedMessage.includes("fetch failed") || normalizedCode === "econnreset";
|
|
1545
|
+
});
|
|
1546
|
+
}
|
|
1547
|
+
function getErrorChain(error) {
|
|
1548
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1549
|
+
const chain = [];
|
|
1550
|
+
let current = error;
|
|
1551
|
+
while (current && !visited.has(current)) {
|
|
1552
|
+
visited.add(current);
|
|
1553
|
+
if (current instanceof Error) {
|
|
1554
|
+
const currentWithCode = current;
|
|
1555
|
+
chain.push({
|
|
1556
|
+
message: current.message,
|
|
1557
|
+
code: currentWithCode.code
|
|
1558
|
+
});
|
|
1559
|
+
current = currentWithCode.cause;
|
|
1560
|
+
continue;
|
|
1561
|
+
}
|
|
1562
|
+
if (typeof current === "object") {
|
|
1563
|
+
const currentRecord = current;
|
|
1564
|
+
chain.push({
|
|
1565
|
+
message: typeof currentRecord.message === "string" ? currentRecord.message : String(current),
|
|
1566
|
+
code: typeof currentRecord.code === "string" ? currentRecord.code : void 0
|
|
1567
|
+
});
|
|
1568
|
+
current = currentRecord.cause;
|
|
1569
|
+
continue;
|
|
1570
|
+
}
|
|
1571
|
+
chain.push({ message: String(current) });
|
|
1572
|
+
break;
|
|
1573
|
+
}
|
|
1574
|
+
return chain;
|
|
1542
1575
|
}
|
|
1543
1576
|
/**
|
|
1544
1577
|
* MCP Client wrapper for managing individual server connections
|
|
@@ -1556,6 +1589,7 @@ var McpClient = class {
|
|
|
1556
1589
|
childProcess;
|
|
1557
1590
|
connected = false;
|
|
1558
1591
|
reconnectFn;
|
|
1592
|
+
reconnectPromise;
|
|
1559
1593
|
constructor(serverName, transport, client, logger, config) {
|
|
1560
1594
|
this.serverName = serverName;
|
|
1561
1595
|
this.serverInstruction = config.instruction;
|
|
@@ -1580,26 +1614,47 @@ var McpClient = class {
|
|
|
1580
1614
|
setReconnectFn(fn) {
|
|
1581
1615
|
this.reconnectFn = fn;
|
|
1582
1616
|
}
|
|
1617
|
+
async reconnectClient() {
|
|
1618
|
+
if (!this.reconnectFn) throw new Error(`No reconnect function configured for ${this.serverName}`);
|
|
1619
|
+
const reconnectFn = this.reconnectFn;
|
|
1620
|
+
if (!this.reconnectPromise) this.reconnectPromise = (async () => {
|
|
1621
|
+
try {
|
|
1622
|
+
await this.client.close();
|
|
1623
|
+
} catch (closeError) {
|
|
1624
|
+
this.logger.warn(`Failed to close stale client for ${this.serverName}`, closeError);
|
|
1625
|
+
}
|
|
1626
|
+
const result = await reconnectFn();
|
|
1627
|
+
this.client = result.client;
|
|
1628
|
+
if (result.childProcess) this.childProcess = result.childProcess;
|
|
1629
|
+
})();
|
|
1630
|
+
try {
|
|
1631
|
+
await this.reconnectPromise;
|
|
1632
|
+
} finally {
|
|
1633
|
+
if (this.reconnectPromise) this.reconnectPromise = void 0;
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1583
1636
|
/**
|
|
1584
1637
|
* Wraps an operation with automatic retry on session errors.
|
|
1585
1638
|
* If the operation fails with a session error (e.g., downstream server restarted),
|
|
1586
|
-
* reconnects and retries
|
|
1639
|
+
* reconnects and retries. A second recovery attempt is allowed to absorb
|
|
1640
|
+
* transient socket resets while the restarted backend is coming back up.
|
|
1587
1641
|
*/
|
|
1588
1642
|
async withSessionRetry(operation) {
|
|
1589
|
-
|
|
1643
|
+
let recoveryAttempts = 0;
|
|
1644
|
+
while (true) try {
|
|
1590
1645
|
return await operation();
|
|
1591
1646
|
} catch (error) {
|
|
1592
|
-
if (!this.reconnectFn || !isSessionError(error)) throw error;
|
|
1647
|
+
if (!this.reconnectFn || !isSessionError(error) || recoveryAttempts >= 2) throw error;
|
|
1648
|
+
recoveryAttempts += 1;
|
|
1593
1649
|
this.logger.warn(`Session error for ${this.serverName}, reconnecting: ${error instanceof Error ? error.message : String(error)}`);
|
|
1594
|
-
try {
|
|
1595
|
-
await this.
|
|
1596
|
-
|
|
1597
|
-
|
|
1650
|
+
while (true) try {
|
|
1651
|
+
await this.reconnectClient();
|
|
1652
|
+
break;
|
|
1653
|
+
} catch (reconnectError) {
|
|
1654
|
+
if (!isSessionError(reconnectError) || recoveryAttempts >= 2) throw reconnectError;
|
|
1655
|
+
this.logger.warn(`Reconnect attempt ${String(recoveryAttempts)} for ${this.serverName} failed, retrying: ${reconnectError instanceof Error ? reconnectError.message : String(reconnectError)}`);
|
|
1656
|
+
recoveryAttempts += 1;
|
|
1598
1657
|
}
|
|
1599
|
-
const result = await this.reconnectFn();
|
|
1600
|
-
this.client = result.client;
|
|
1601
|
-
if (result.childProcess) this.childProcess = result.childProcess;
|
|
1602
|
-
return await operation();
|
|
1603
1658
|
}
|
|
1604
1659
|
}
|
|
1605
1660
|
async listTools() {
|
|
@@ -52,7 +52,7 @@ let __modelcontextprotocol_sdk_server_sse_js = require("@modelcontextprotocol/sd
|
|
|
52
52
|
let __modelcontextprotocol_sdk_server_stdio_js = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
53
53
|
|
|
54
54
|
//#region package.json
|
|
55
|
-
var version = "0.
|
|
55
|
+
var version = "0.6.0";
|
|
56
56
|
|
|
57
57
|
//#endregion
|
|
58
58
|
//#region src/utils/mcpConfigSchema.ts
|
|
@@ -291,7 +291,8 @@ const ProxyConfigSchema = zod.z.object({
|
|
|
291
291
|
"stdio-http"
|
|
292
292
|
]).optional(),
|
|
293
293
|
port: zod.z.number().int().positive().optional(),
|
|
294
|
-
host: zod.z.string().optional()
|
|
294
|
+
host: zod.z.string().optional(),
|
|
295
|
+
keepAlive: zod.z.boolean().optional()
|
|
295
296
|
}).optional();
|
|
296
297
|
/**
|
|
297
298
|
* Full Claude Code MCP configuration schema
|
|
@@ -1565,8 +1566,40 @@ const DEFAULT_CONNECTION_TIMEOUT_MS = 3e4;
|
|
|
1565
1566
|
* (e.g., downstream server restarted and no longer recognizes the session ID).
|
|
1566
1567
|
*/
|
|
1567
1568
|
function isSessionError(error) {
|
|
1568
|
-
|
|
1569
|
-
|
|
1569
|
+
return getErrorChain(error).some(({ message, code }) => {
|
|
1570
|
+
const normalizedMessage = message.toLowerCase();
|
|
1571
|
+
const normalizedCode = code?.toLowerCase();
|
|
1572
|
+
return normalizedMessage.includes("unknown session") || normalizedMessage.includes("session not found") || normalizedMessage.includes("transport closed") || normalizedMessage.includes("connection closed") || normalizedMessage.includes("socket hang up") || normalizedMessage.includes("fetch failed") || normalizedCode === "econnreset";
|
|
1573
|
+
});
|
|
1574
|
+
}
|
|
1575
|
+
function getErrorChain(error) {
|
|
1576
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1577
|
+
const chain = [];
|
|
1578
|
+
let current = error;
|
|
1579
|
+
while (current && !visited.has(current)) {
|
|
1580
|
+
visited.add(current);
|
|
1581
|
+
if (current instanceof Error) {
|
|
1582
|
+
const currentWithCode = current;
|
|
1583
|
+
chain.push({
|
|
1584
|
+
message: current.message,
|
|
1585
|
+
code: currentWithCode.code
|
|
1586
|
+
});
|
|
1587
|
+
current = currentWithCode.cause;
|
|
1588
|
+
continue;
|
|
1589
|
+
}
|
|
1590
|
+
if (typeof current === "object") {
|
|
1591
|
+
const currentRecord = current;
|
|
1592
|
+
chain.push({
|
|
1593
|
+
message: typeof currentRecord.message === "string" ? currentRecord.message : String(current),
|
|
1594
|
+
code: typeof currentRecord.code === "string" ? currentRecord.code : void 0
|
|
1595
|
+
});
|
|
1596
|
+
current = currentRecord.cause;
|
|
1597
|
+
continue;
|
|
1598
|
+
}
|
|
1599
|
+
chain.push({ message: String(current) });
|
|
1600
|
+
break;
|
|
1601
|
+
}
|
|
1602
|
+
return chain;
|
|
1570
1603
|
}
|
|
1571
1604
|
/**
|
|
1572
1605
|
* MCP Client wrapper for managing individual server connections
|
|
@@ -1584,6 +1617,7 @@ var McpClient = class {
|
|
|
1584
1617
|
childProcess;
|
|
1585
1618
|
connected = false;
|
|
1586
1619
|
reconnectFn;
|
|
1620
|
+
reconnectPromise;
|
|
1587
1621
|
constructor(serverName, transport, client, logger, config) {
|
|
1588
1622
|
this.serverName = serverName;
|
|
1589
1623
|
this.serverInstruction = config.instruction;
|
|
@@ -1608,26 +1642,47 @@ var McpClient = class {
|
|
|
1608
1642
|
setReconnectFn(fn) {
|
|
1609
1643
|
this.reconnectFn = fn;
|
|
1610
1644
|
}
|
|
1645
|
+
async reconnectClient() {
|
|
1646
|
+
if (!this.reconnectFn) throw new Error(`No reconnect function configured for ${this.serverName}`);
|
|
1647
|
+
const reconnectFn = this.reconnectFn;
|
|
1648
|
+
if (!this.reconnectPromise) this.reconnectPromise = (async () => {
|
|
1649
|
+
try {
|
|
1650
|
+
await this.client.close();
|
|
1651
|
+
} catch (closeError) {
|
|
1652
|
+
this.logger.warn(`Failed to close stale client for ${this.serverName}`, closeError);
|
|
1653
|
+
}
|
|
1654
|
+
const result = await reconnectFn();
|
|
1655
|
+
this.client = result.client;
|
|
1656
|
+
if (result.childProcess) this.childProcess = result.childProcess;
|
|
1657
|
+
})();
|
|
1658
|
+
try {
|
|
1659
|
+
await this.reconnectPromise;
|
|
1660
|
+
} finally {
|
|
1661
|
+
if (this.reconnectPromise) this.reconnectPromise = void 0;
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1611
1664
|
/**
|
|
1612
1665
|
* Wraps an operation with automatic retry on session errors.
|
|
1613
1666
|
* If the operation fails with a session error (e.g., downstream server restarted),
|
|
1614
|
-
* reconnects and retries
|
|
1667
|
+
* reconnects and retries. A second recovery attempt is allowed to absorb
|
|
1668
|
+
* transient socket resets while the restarted backend is coming back up.
|
|
1615
1669
|
*/
|
|
1616
1670
|
async withSessionRetry(operation) {
|
|
1617
|
-
|
|
1671
|
+
let recoveryAttempts = 0;
|
|
1672
|
+
while (true) try {
|
|
1618
1673
|
return await operation();
|
|
1619
1674
|
} catch (error) {
|
|
1620
|
-
if (!this.reconnectFn || !isSessionError(error)) throw error;
|
|
1675
|
+
if (!this.reconnectFn || !isSessionError(error) || recoveryAttempts >= 2) throw error;
|
|
1676
|
+
recoveryAttempts += 1;
|
|
1621
1677
|
this.logger.warn(`Session error for ${this.serverName}, reconnecting: ${error instanceof Error ? error.message : String(error)}`);
|
|
1622
|
-
try {
|
|
1623
|
-
await this.
|
|
1624
|
-
|
|
1625
|
-
|
|
1678
|
+
while (true) try {
|
|
1679
|
+
await this.reconnectClient();
|
|
1680
|
+
break;
|
|
1681
|
+
} catch (reconnectError) {
|
|
1682
|
+
if (!isSessionError(reconnectError) || recoveryAttempts >= 2) throw reconnectError;
|
|
1683
|
+
this.logger.warn(`Reconnect attempt ${String(recoveryAttempts)} for ${this.serverName} failed, retrying: ${reconnectError instanceof Error ? reconnectError.message : String(reconnectError)}`);
|
|
1684
|
+
recoveryAttempts += 1;
|
|
1626
1685
|
}
|
|
1627
|
-
const result = await this.reconnectFn();
|
|
1628
|
-
this.client = result.client;
|
|
1629
|
-
if (result.childProcess) this.childProcess = result.childProcess;
|
|
1630
|
-
return await operation();
|
|
1631
1686
|
}
|
|
1632
1687
|
}
|
|
1633
1688
|
async listTools() {
|
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
|
+
"version": "0.7.0",
|
|
5
5
|
"license": "AGPL-3.0",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"mcp",
|
|
@@ -28,9 +28,9 @@
|
|
|
28
28
|
"js-yaml": "^4.1.0",
|
|
29
29
|
"liquidjs": "^10.21.0",
|
|
30
30
|
"zod": "^3.24.1",
|
|
31
|
-
"@agimon-ai/
|
|
32
|
-
"@agimon-ai/
|
|
33
|
-
"@agimon-ai/foundation-port-registry": "0.
|
|
31
|
+
"@agimon-ai/foundation-process-registry": "0.5.0",
|
|
32
|
+
"@agimon-ai/log-sink-mcp": "0.5.0",
|
|
33
|
+
"@agimon-ai/foundation-port-registry": "0.5.0"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@types/js-yaml": "^4.0.9",
|