@agimon-ai/mcp-proxy 0.5.2 → 0.6.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 CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- const require_src = require('./src-DUR0uWiY.cjs');
2
+ const require_src = require('./src-BusJtVCU.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 new URL(`http://${runtime.host}:${runtime.port}${MCP_ENDPOINT_PATH}`);
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
- await startServer(new require_src.StdioHttpTransportHandler({ endpoint: await resolveStdioHttpEndpoint(config, options, resolvedConfigPath) }, createStdioSafeLogger()));
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-kgJ-iu3i.mjs";
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-DlmUab0b.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 new URL(`http://${runtime.host}:${runtime.port}${MCP_ENDPOINT_PATH}`);
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
- await startServer(new StdioHttpTransportHandler({ endpoint: await resolveStdioHttpEndpoint(config, options, resolvedConfigPath) }, createStdioSafeLogger()));
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
@@ -1,4 +1,4 @@
1
- const require_src = require('./src-DUR0uWiY.cjs');
1
+ const require_src = require('./src-BusJtVCU.cjs');
2
2
 
3
3
  exports.ConfigFetcherService = require_src.ConfigFetcherService;
4
4
  exports.DefinitionsCacheService = require_src.DefinitionsCacheService;
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-kgJ-iu3i.mjs";
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-DlmUab0b.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 };
@@ -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.5.1";
55
+ var version = "0.5.2";
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
- const message = error instanceof Error ? error.message : String(error);
1569
- return message.includes("unknown session") || message.includes("Session not found");
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 once.
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
- try {
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.client.close();
1624
- } catch (closeError) {
1625
- this.logger.warn(`Failed to close stale client for ${this.serverName}`, closeError);
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() {
@@ -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.5.1";
27
+ var version = "0.5.2";
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
- const message = error instanceof Error ? error.message : String(error);
1541
- return message.includes("unknown session") || message.includes("Session not found");
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 once.
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
- try {
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.client.close();
1596
- } catch (closeError) {
1597
- this.logger.warn(`Failed to close stale client for ${this.serverName}`, closeError);
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() {
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.5.2",
4
+ "version": "0.6.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/log-sink-mcp": "0.3.1",
32
- "@agimon-ai/foundation-process-registry": "0.3.1",
33
- "@agimon-ai/foundation-port-registry": "0.3.1"
31
+ "@agimon-ai/foundation-process-registry": "0.4.0",
32
+ "@agimon-ai/log-sink-mcp": "0.4.0",
33
+ "@agimon-ai/foundation-port-registry": "0.4.0"
34
34
  },
35
35
  "devDependencies": {
36
36
  "@types/js-yaml": "^4.0.9",