nuwax-mcp-stdio-proxy 1.4.7 → 1.4.8

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/bridge.js CHANGED
@@ -177,8 +177,7 @@ export class PersistentMcpBridge {
177
177
  }
178
178
  return t;
179
179
  },
180
- pingIntervalMs: entry.config.pingIntervalMs,
181
- pingTimeoutMs: entry.config.pingTimeoutMs,
180
+ pingIntervalMs: 0, // No heartbeat for stdio — child process close/error events handle detection
182
181
  });
183
182
  const client = new Client({ name: 'nuwax-persistent-bridge', version: '1.0.0' }, { capabilities: {} });
184
183
  // Handle transport close → auto restart
@@ -195,10 +194,6 @@ export class PersistentMcpBridge {
195
194
  // Connect client to transport (this starts the subprocess)
196
195
  await wrapper.start();
197
196
  await client.connect(wrapper);
198
- // Enable heartbeat monitoring AFTER the MCP initialize handshake
199
- // completes — sending pings before initialize causes "Server not
200
- // initialized" errors from the MCP server.
201
- wrapper.enableHeartbeat();
202
197
  entry.client = client;
203
198
  entry.transport = wrapper;
204
199
  // List tools (cached)
package/dist/detect.d.ts CHANGED
@@ -1,14 +1,20 @@
1
1
  /**
2
2
  * Protocol auto-detection — determine whether a URL serves Streamable HTTP or SSE
3
+ *
4
+ * Aligned with workspace/mcp-proxy (Rust) detection logic:
5
+ * - Only probe Streamable HTTP (POST initialize)
6
+ * - Default to SSE if probe fails
3
7
  */
4
8
  /**
5
9
  * Detect the MCP transport protocol of a remote URL.
6
10
  *
7
- * Strategy:
8
- * 1. Try Streamable HTTP first: send a JSON-RPC initialize POST.
9
- * If the server responds with 200 and JSON, it's streamable-http.
10
- * Then clean up the orphan session via DELETE.
11
- * 2. If that fails, try SSE: send a GET and check for text/event-stream content-type.
12
- * 3. Default to 'stream' (Streamable HTTP) if both probes fail.
11
+ * Strategy (matches Rust mcp-proxy):
12
+ * 1. Send a JSON-RPC initialize POST to probe for Streamable HTTP.
13
+ * 2. Check 4 criteria any match means streamable-http:
14
+ * a. Response has `mcp-session-id` header
15
+ * b. Content-Type is `text/event-stream` with 2xx status
16
+ * c. Response body is valid JSON-RPC 2.0
17
+ * d. Status is 406 Not Acceptable
18
+ * 3. If probe fails or no criteria match → default to SSE.
13
19
  */
14
20
  export declare function detectProtocol(url: string, headers?: Record<string, string>): Promise<'sse' | 'stream'>;
package/dist/detect.js CHANGED
@@ -1,20 +1,25 @@
1
1
  /**
2
2
  * Protocol auto-detection — determine whether a URL serves Streamable HTTP or SSE
3
+ *
4
+ * Aligned with workspace/mcp-proxy (Rust) detection logic:
5
+ * - Only probe Streamable HTTP (POST initialize)
6
+ * - Default to SSE if probe fails
3
7
  */
4
8
  import { logInfo, logWarn } from './logger.js';
5
9
  /**
6
10
  * Detect the MCP transport protocol of a remote URL.
7
11
  *
8
- * Strategy:
9
- * 1. Try Streamable HTTP first: send a JSON-RPC initialize POST.
10
- * If the server responds with 200 and JSON, it's streamable-http.
11
- * Then clean up the orphan session via DELETE.
12
- * 2. If that fails, try SSE: send a GET and check for text/event-stream content-type.
13
- * 3. Default to 'stream' (Streamable HTTP) if both probes fail.
12
+ * Strategy (matches Rust mcp-proxy):
13
+ * 1. Send a JSON-RPC initialize POST to probe for Streamable HTTP.
14
+ * 2. Check 4 criteria any match means streamable-http:
15
+ * a. Response has `mcp-session-id` header
16
+ * b. Content-Type is `text/event-stream` with 2xx status
17
+ * c. Response body is valid JSON-RPC 2.0
18
+ * d. Status is 406 Not Acceptable
19
+ * 3. If probe fails or no criteria match → default to SSE.
14
20
  */
15
21
  export async function detectProtocol(url, headers) {
16
22
  logInfo(`Auto-detecting protocol for ${url}...`);
17
- // 1. Try Streamable HTTP — POST a JSON-RPC initialize request
18
23
  try {
19
24
  const reqHeaders = {
20
25
  'Content-Type': 'application/json',
@@ -22,7 +27,7 @@ export async function detectProtocol(url, headers) {
22
27
  ...headers,
23
28
  };
24
29
  const controller = new AbortController();
25
- const timeout = setTimeout(() => controller.abort(), 10_000);
30
+ const timeout = setTimeout(() => controller.abort(), 5_000);
26
31
  const res = await fetch(url, {
27
32
  method: 'POST',
28
33
  headers: reqHeaders,
@@ -39,57 +44,61 @@ export async function detectProtocol(url, headers) {
39
44
  signal: controller.signal,
40
45
  });
41
46
  clearTimeout(timeout);
42
- const ct = res.headers.get('content-type') || '';
43
- if (res.ok && (ct.includes('application/json') || ct.includes('text/event-stream'))) {
44
- logInfo(`Detected streamable-http protocol for ${url}`);
45
- // Consume body to avoid socket hang
47
+ // Check 1: mcp-session-id header (definitive Streamable HTTP marker)
48
+ if (res.headers.get('mcp-session-id')) {
49
+ logInfo(`Detected streamable-http protocol for ${url} (mcp-session-id header)`);
50
+ cleanupSession(url, res.headers.get('mcp-session-id'), headers);
46
51
  await res.text().catch(() => { });
47
- // Clean up orphan session — fire-and-forget DELETE so the server
48
- // can discard the half-initialized session we created during probing.
49
- // Not awaited: cleanup is best-effort and must not block detection.
50
- const sessionId = res.headers.get('mcp-session-id');
51
- if (sessionId) {
52
- fetch(url, {
53
- method: 'DELETE',
54
- headers: { 'mcp-session-id': sessionId, ...headers },
55
- signal: AbortSignal.timeout(5_000),
56
- }).catch(() => { });
57
- }
58
52
  return 'stream';
59
53
  }
60
- await res.text().catch(() => { });
61
- }
62
- catch {
63
- // Streamable HTTP probe failed, try SSE
64
- }
65
- // 2. Try SSE — GET and check for event-stream
66
- try {
67
- const reqHeaders = {
68
- Accept: 'text/event-stream',
69
- ...headers,
70
- };
71
- const controller = new AbortController();
72
- const timeout = setTimeout(() => controller.abort(), 10_000);
73
- const res = await fetch(url, {
74
- method: 'GET',
75
- headers: reqHeaders,
76
- signal: controller.signal,
77
- });
78
54
  const ct = res.headers.get('content-type') || '';
79
- if (ct.includes('text/event-stream')) {
80
- // Detected SSE — abort the stream before clearing the timeout
81
- clearTimeout(timeout);
82
- logInfo(`Detected SSE protocol for ${url}`);
55
+ // Check 2: text/event-stream content-type with success status
56
+ if (ct.includes('text/event-stream') && res.ok) {
57
+ logInfo(`Detected streamable-http protocol for ${url} (event-stream response)`);
58
+ cleanupSession(url, res.headers.get('mcp-session-id'), headers);
59
+ // Abort the stream to free the connection
83
60
  controller.abort();
84
- return 'sse';
61
+ return 'stream';
62
+ }
63
+ // Read body for JSON-RPC check
64
+ let bodyText = '';
65
+ try {
66
+ bodyText = await res.text();
67
+ }
68
+ catch { /* ignore */ }
69
+ // Check 3: valid JSON-RPC 2.0 response
70
+ try {
71
+ const json = JSON.parse(bodyText);
72
+ if (json && json.jsonrpc === '2.0') {
73
+ logInfo(`Detected streamable-http protocol for ${url} (JSON-RPC 2.0 response)`);
74
+ cleanupSession(url, res.headers.get('mcp-session-id'), headers);
75
+ return 'stream';
76
+ }
77
+ }
78
+ catch { /* not JSON */ }
79
+ // Check 4: 406 Not Acceptable (may indicate Streamable HTTP)
80
+ if (res.status === 406) {
81
+ logInfo(`Detected streamable-http protocol for ${url} (406 Not Acceptable)`);
82
+ return 'stream';
85
83
  }
86
- clearTimeout(timeout);
87
- await res.text().catch(() => { });
88
84
  }
89
85
  catch {
90
- // SSE probe failed
86
+ // Probe failed (timeout, connection refused, etc.)
91
87
  }
92
- // 3. Default to streamable-http
93
- logWarn(`Could not auto-detect protocol for ${url}, defaulting to streamable-http`);
94
- return 'stream';
88
+ // Default to SSE (matches Rust mcp-proxy behavior)
89
+ logWarn(`Could not detect streamable-http for ${url}, defaulting to SSE`);
90
+ return 'sse';
91
+ }
92
+ /**
93
+ * Clean up orphan session — fire-and-forget DELETE so the server
94
+ * can discard the half-initialized session we created during probing.
95
+ */
96
+ function cleanupSession(url, sessionId, headers) {
97
+ if (!sessionId)
98
+ return;
99
+ fetch(url, {
100
+ method: 'DELETE',
101
+ headers: { 'mcp-session-id': sessionId, ...headers },
102
+ signal: AbortSignal.timeout(5_000),
103
+ }).catch(() => { });
95
104
  }
package/dist/index.js CHANGED
@@ -23040,7 +23040,7 @@ var ResilientTransportWrapper = class {
23040
23040
  mcpClient = null;
23041
23041
  heartbeatTimer = null;
23042
23042
  consecutiveFailures = 0;
23043
- state = "connecting";
23043
+ state = "idle";
23044
23044
  // Handlers required by the Transport interface
23045
23045
  onclose;
23046
23046
  onerror;
@@ -23051,7 +23051,17 @@ var ResilientTransportWrapper = class {
23051
23051
  pendingPings = /* @__PURE__ */ new Map();
23052
23052
  constructor(options) {
23053
23053
  this.log = options.logger ?? defaultLogger;
23054
- this.options = { ...DEFAULT_OPTIONS, logger: this.log, ...options };
23054
+ this.options = {
23055
+ ...DEFAULT_OPTIONS,
23056
+ logger: this.log,
23057
+ connectParams: options.connectParams,
23058
+ ...options.name !== void 0 && { name: options.name },
23059
+ ...options.pingIntervalMs !== void 0 && { pingIntervalMs: options.pingIntervalMs },
23060
+ ...options.pingTimeoutMs !== void 0 && { pingTimeoutMs: options.pingTimeoutMs },
23061
+ ...options.maxConsecutiveFailures !== void 0 && { maxConsecutiveFailures: options.maxConsecutiveFailures },
23062
+ ...options.reconnectDelayMs !== void 0 && { reconnectDelayMs: options.reconnectDelayMs },
23063
+ ...options.maxQueueSize !== void 0 && { maxQueueSize: options.maxQueueSize }
23064
+ };
23055
23065
  }
23056
23066
  /**
23057
23067
  * Initializes the transport and connects to the backend
@@ -23071,7 +23081,7 @@ var ResilientTransportWrapper = class {
23071
23081
  /**
23072
23082
  * Enable heartbeat monitoring. Call this AFTER the MCP client has
23073
23083
  * completed its initialize handshake (client.connect()), otherwise
23074
- * the server will reject ping requests with "Server not initialized".
23084
+ * the server will reject requests with "Server not initialized".
23075
23085
  */
23076
23086
  enableHeartbeat() {
23077
23087
  this.startHeartbeat();
@@ -23083,6 +23093,7 @@ var ResilientTransportWrapper = class {
23083
23093
  await this.activeTransport.start();
23084
23094
  this.state = "connected";
23085
23095
  this.consecutiveFailures = 0;
23096
+ this.consecutivePingTimeouts = 0;
23086
23097
  this.log.info(`[McpProxy] [ResilientTransport:${this.options.name}] Connected via ${this.activeTransport.constructor.name}`);
23087
23098
  this.flushQueue();
23088
23099
  if (!initial) {
@@ -23143,52 +23154,46 @@ var ResilientTransportWrapper = class {
23143
23154
  }
23144
23155
  /** Track consecutive ping timeouts (no response at all, not even an error) */
23145
23156
  consecutivePingTimeouts = 0;
23146
- /** If server doesn't support ping, auto-disable heartbeat after maxConsecutiveFailures timeouts */
23147
- pingDisabled = false;
23148
23157
  async checkHealth() {
23149
23158
  if (this.state !== "connected" || !this.activeTransport) return;
23150
- if (this.pingDisabled) return;
23151
23159
  try {
23152
- const pingId = `respl-ping-${Date.now()}`;
23160
+ const healthId = `respl-ping-${Date.now()}`;
23153
23161
  const responsePromise = new Promise((resolve) => {
23154
- this.pendingPings.set(pingId, resolve);
23162
+ this.pendingPings.set(healthId, resolve);
23155
23163
  setTimeout(() => {
23156
- if (this.pendingPings.has(pingId)) {
23157
- this.pendingPings.delete(pingId);
23164
+ if (this.pendingPings.has(healthId)) {
23165
+ this.pendingPings.delete(healthId);
23158
23166
  resolve(false);
23159
23167
  }
23160
23168
  }, this.options.pingTimeoutMs);
23161
23169
  });
23162
- let sendFailed = false;
23163
23170
  try {
23164
- this.log.info(`[McpProxy] [ResilientTransport:${this.options.name}] \u{1F493} Sending heartbeat ping (id: ${pingId})`);
23171
+ this.log.info(`[McpProxy] [ResilientTransport:${this.options.name}] \u{1F493} Sending heartbeat (id: ${healthId})`);
23165
23172
  await this.activeTransport.send({
23166
23173
  jsonrpc: "2.0",
23167
- id: pingId,
23168
- method: "ping"
23174
+ id: healthId,
23175
+ method: "tools/list",
23176
+ params: {}
23169
23177
  });
23170
23178
  } catch (sendErr) {
23171
- sendFailed = true;
23172
23179
  throw sendErr;
23173
23180
  }
23174
23181
  const success2 = await responsePromise;
23175
23182
  if (!success2) {
23176
23183
  this.consecutivePingTimeouts++;
23184
+ this.log.warn(`[McpProxy] [ResilientTransport:${this.options.name}] Heartbeat timeout (${this.consecutivePingTimeouts}/${this.options.maxConsecutiveFailures})`);
23177
23185
  if (this.consecutivePingTimeouts >= this.options.maxConsecutiveFailures) {
23178
- this.log.warn(`[McpProxy] [ResilientTransport:${this.options.name}] Server does not respond to ping. Disabling heartbeat monitoring.`);
23179
- this.pingDisabled = true;
23180
- this.stopHeartbeat();
23181
- return;
23186
+ this.log.error(`[McpProxy] [ResilientTransport:${this.options.name}] Max consecutive heartbeat timeouts reached. Force reconnecting.`);
23187
+ this.triggerReconnect();
23182
23188
  }
23183
- this.log.warn(`[McpProxy] [ResilientTransport:${this.options.name}] Ping timeout (${this.consecutivePingTimeouts}/${this.options.maxConsecutiveFailures})`);
23184
23189
  return;
23185
23190
  }
23186
- this.log.info(`[McpProxy] [ResilientTransport:${this.options.name}] \u{1F496} Heartbeat successful (id: ${pingId})`);
23191
+ this.log.info(`[McpProxy] [ResilientTransport:${this.options.name}] \u{1F496} Heartbeat OK (id: ${healthId})`);
23187
23192
  this.consecutiveFailures = 0;
23188
23193
  this.consecutivePingTimeouts = 0;
23189
23194
  } catch (err) {
23190
23195
  this.consecutiveFailures++;
23191
- this.log.warn(`[McpProxy] [ResilientTransport:${this.options.name}] \u{1F494} Heartbeat error (attempt ${this.consecutiveFailures}/${this.options.maxConsecutiveFailures}): ${err}`);
23196
+ this.log.warn(`[McpProxy] [ResilientTransport:${this.options.name}] \u{1F494} Heartbeat error (${this.consecutiveFailures}/${this.options.maxConsecutiveFailures}): ${err}`);
23192
23197
  if (this.consecutiveFailures >= this.options.maxConsecutiveFailures) {
23193
23198
  this.log.error(`[McpProxy] [ResilientTransport:${this.options.name}] Max consecutive heartbeat failures reached. Force reconnecting.`);
23194
23199
  this.triggerReconnect();
@@ -23274,6 +23279,8 @@ var ResilientTransportWrapper = class {
23274
23279
  };
23275
23280
 
23276
23281
  // src/transport.ts
23282
+ var DEFAULT_STDIO_CONNECTION_TIMEOUT_MS = 6e4;
23283
+ var DEFAULT_HTTP_CONNECTION_TIMEOUT_MS = 3e4;
23277
23284
  async function withTimeout(promise2, ms, message) {
23278
23285
  let timeoutId;
23279
23286
  const timeoutPromise = new Promise((_, reject) => {
@@ -23327,19 +23334,19 @@ async function connectStdio(id, entry, baseEnv) {
23327
23334
  }
23328
23335
  return t;
23329
23336
  },
23330
- pingIntervalMs: entry.pingIntervalMs,
23331
- pingTimeoutMs: entry.pingTimeoutMs
23337
+ // No heartbeat for stdio — child process close/error events handle detection
23338
+ pingIntervalMs: 0
23332
23339
  });
23333
23340
  const client = new Client({ name: `proxy-${id}`, version: "1.0.0" });
23341
+ const timeoutMs = entry.connectionTimeoutMs ?? DEFAULT_STDIO_CONNECTION_TIMEOUT_MS;
23334
23342
  try {
23335
23343
  await withTimeout(
23336
23344
  (async () => {
23337
23345
  await wrapper.start();
23338
23346
  await client.connect(wrapper);
23339
- wrapper.enableHeartbeat();
23340
23347
  })(),
23341
- 5e3,
23342
- `Connection initialization timed out after 5s`
23348
+ timeoutMs,
23349
+ `Connection initialization timed out after ${timeoutMs / 1e3}s`
23343
23350
  );
23344
23351
  } catch (err) {
23345
23352
  try {
@@ -23375,6 +23382,7 @@ async function connectStreamable(id, entry) {
23375
23382
  pingTimeoutMs: entry.pingTimeoutMs
23376
23383
  });
23377
23384
  const client = new Client({ name: `proxy-${id}`, version: "1.0.0" });
23385
+ const timeoutMs = entry.connectionTimeoutMs ?? DEFAULT_HTTP_CONNECTION_TIMEOUT_MS;
23378
23386
  try {
23379
23387
  await withTimeout(
23380
23388
  (async () => {
@@ -23382,8 +23390,8 @@ async function connectStreamable(id, entry) {
23382
23390
  await client.connect(wrapper);
23383
23391
  wrapper.enableHeartbeat();
23384
23392
  })(),
23385
- 5e3,
23386
- `Connection initialization timed out after 5s`
23393
+ timeoutMs,
23394
+ `Connection initialization timed out after ${timeoutMs / 1e3}s`
23387
23395
  );
23388
23396
  } catch (err) {
23389
23397
  try {
@@ -23419,6 +23427,7 @@ async function connectSse(id, entry) {
23419
23427
  pingTimeoutMs: entry.pingTimeoutMs
23420
23428
  });
23421
23429
  const client = new Client({ name: `proxy-${id}`, version: "1.0.0" });
23430
+ const timeoutMs = entry.connectionTimeoutMs ?? DEFAULT_HTTP_CONNECTION_TIMEOUT_MS;
23422
23431
  try {
23423
23432
  await withTimeout(
23424
23433
  (async () => {
@@ -23426,8 +23435,8 @@ async function connectSse(id, entry) {
23426
23435
  await client.connect(wrapper);
23427
23436
  wrapper.enableHeartbeat();
23428
23437
  })(),
23429
- 5e3,
23430
- `Connection initialization timed out after 5s`
23438
+ timeoutMs,
23439
+ `Connection initialization timed out after ${timeoutMs / 1e3}s`
23431
23440
  );
23432
23441
  } catch (err) {
23433
23442
  try {
@@ -24105,7 +24114,7 @@ var StdioServerTransport = class {
24105
24114
 
24106
24115
  // src/constants.ts
24107
24116
  var PKG_NAME = "nuwax-mcp-stdio-proxy";
24108
- var PKG_VERSION = "1.4.7";
24117
+ var PKG_VERSION = "1.4.8";
24109
24118
 
24110
24119
  // src/shared.ts
24111
24120
  async function discoverTools(client) {
@@ -24190,7 +24199,7 @@ async function detectProtocol(url2, headers) {
24190
24199
  ...headers
24191
24200
  };
24192
24201
  const controller = new AbortController();
24193
- const timeout = setTimeout(() => controller.abort(), 1e4);
24202
+ const timeout = setTimeout(() => controller.abort(), 5e3);
24194
24203
  const res = await fetch(url2, {
24195
24204
  method: "POST",
24196
24205
  headers: reqHeaders,
@@ -24207,52 +24216,51 @@ async function detectProtocol(url2, headers) {
24207
24216
  signal: controller.signal
24208
24217
  });
24209
24218
  clearTimeout(timeout);
24210
- const ct = res.headers.get("content-type") || "";
24211
- if (res.ok && (ct.includes("application/json") || ct.includes("text/event-stream"))) {
24212
- logInfo(`Detected streamable-http protocol for ${url2}`);
24219
+ if (res.headers.get("mcp-session-id")) {
24220
+ logInfo(`Detected streamable-http protocol for ${url2} (mcp-session-id header)`);
24221
+ cleanupSession(url2, res.headers.get("mcp-session-id"), headers);
24213
24222
  await res.text().catch(() => {
24214
24223
  });
24215
- const sessionId = res.headers.get("mcp-session-id");
24216
- if (sessionId) {
24217
- fetch(url2, {
24218
- method: "DELETE",
24219
- headers: { "mcp-session-id": sessionId, ...headers },
24220
- signal: AbortSignal.timeout(5e3)
24221
- }).catch(() => {
24222
- });
24223
- }
24224
24224
  return "stream";
24225
24225
  }
24226
- await res.text().catch(() => {
24227
- });
24228
- } catch {
24229
- }
24230
- try {
24231
- const reqHeaders = {
24232
- Accept: "text/event-stream",
24233
- ...headers
24234
- };
24235
- const controller = new AbortController();
24236
- const timeout = setTimeout(() => controller.abort(), 1e4);
24237
- const res = await fetch(url2, {
24238
- method: "GET",
24239
- headers: reqHeaders,
24240
- signal: controller.signal
24241
- });
24242
24226
  const ct = res.headers.get("content-type") || "";
24243
- if (ct.includes("text/event-stream")) {
24244
- clearTimeout(timeout);
24245
- logInfo(`Detected SSE protocol for ${url2}`);
24227
+ if (ct.includes("text/event-stream") && res.ok) {
24228
+ logInfo(`Detected streamable-http protocol for ${url2} (event-stream response)`);
24229
+ cleanupSession(url2, res.headers.get("mcp-session-id"), headers);
24246
24230
  controller.abort();
24247
- return "sse";
24231
+ return "stream";
24232
+ }
24233
+ let bodyText = "";
24234
+ try {
24235
+ bodyText = await res.text();
24236
+ } catch {
24237
+ }
24238
+ try {
24239
+ const json2 = JSON.parse(bodyText);
24240
+ if (json2 && json2.jsonrpc === "2.0") {
24241
+ logInfo(`Detected streamable-http protocol for ${url2} (JSON-RPC 2.0 response)`);
24242
+ cleanupSession(url2, res.headers.get("mcp-session-id"), headers);
24243
+ return "stream";
24244
+ }
24245
+ } catch {
24246
+ }
24247
+ if (res.status === 406) {
24248
+ logInfo(`Detected streamable-http protocol for ${url2} (406 Not Acceptable)`);
24249
+ return "stream";
24248
24250
  }
24249
- clearTimeout(timeout);
24250
- await res.text().catch(() => {
24251
- });
24252
24251
  } catch {
24253
24252
  }
24254
- logWarn(`Could not auto-detect protocol for ${url2}, defaulting to streamable-http`);
24255
- return "stream";
24253
+ logWarn(`Could not detect streamable-http for ${url2}, defaulting to SSE`);
24254
+ return "sse";
24255
+ }
24256
+ function cleanupSession(url2, sessionId, headers) {
24257
+ if (!sessionId) return;
24258
+ fetch(url2, {
24259
+ method: "DELETE",
24260
+ headers: { "mcp-session-id": sessionId, ...headers },
24261
+ signal: AbortSignal.timeout(5e3)
24262
+ }).catch(() => {
24263
+ });
24256
24264
  }
24257
24265
 
24258
24266
  // src/modes/stdio.ts
@@ -24271,52 +24279,69 @@ async function runStdio(config2, allowTools, denyTools) {
24271
24279
  const toolToClient = /* @__PURE__ */ new Map();
24272
24280
  const toolToServer = /* @__PURE__ */ new Map();
24273
24281
  const toolsByName = /* @__PURE__ */ new Map();
24274
- for (const [id, entry] of entries) {
24275
- try {
24276
- let connected;
24277
- if (isSseEntry(entry)) {
24278
- connected = await connectSse(id, entry);
24279
- } else if (isStreamableEntry(entry)) {
24280
- connected = await connectStreamable(id, entry);
24281
- } else if (needsProtocolDetection(entry)) {
24282
- const detected = await detectProtocol(entry.url, buildRequestHeaders(entry));
24283
- if (detected === "sse") {
24284
- connected = await connectSse(id, { ...entry, transport: "sse" });
24285
- } else {
24282
+ const connectableEntries = entries.filter(([id, entry]) => {
24283
+ if (entry.persistent) {
24284
+ logWarn(`Skipping persistent server "${id}" (handled by PersistentMcpBridge)`);
24285
+ return false;
24286
+ }
24287
+ return true;
24288
+ });
24289
+ const results = await Promise.allSettled(
24290
+ connectableEntries.map(async ([id, entry]) => {
24291
+ try {
24292
+ let connected;
24293
+ if (isSseEntry(entry)) {
24294
+ connected = await connectSse(id, entry);
24295
+ } else if (isStreamableEntry(entry)) {
24286
24296
  connected = await connectStreamable(id, entry);
24297
+ } else if (needsProtocolDetection(entry)) {
24298
+ const detected = await detectProtocol(entry.url, buildRequestHeaders(entry));
24299
+ if (detected === "sse") {
24300
+ connected = await connectSse(id, { ...entry, transport: "sse" });
24301
+ } else {
24302
+ connected = await connectStreamable(id, entry);
24303
+ }
24304
+ } else {
24305
+ connected = await connectStdio(id, entry, baseEnv);
24287
24306
  }
24288
- } else {
24289
- connected = await connectStdio(id, entry, baseEnv);
24290
- }
24291
- const { client, cleanup } = connected;
24292
- clients.set(id, client);
24293
- cleanups.set(id, cleanup);
24294
- let serverTools = await discoverTools(client);
24295
- if (entry.allowTools || entry.denyTools) {
24296
- const perFilter = {};
24297
- if (entry.allowTools) perFilter.allowTools = new Set(entry.allowTools);
24298
- if (entry.denyTools) perFilter.denyTools = new Set(entry.denyTools);
24299
- const before = serverTools.length;
24300
- serverTools = filterTools(serverTools, perFilter);
24301
- if (serverTools.length !== before) {
24302
- logInfo(`Server "${id}": filtered ${before} \u2192 ${serverTools.length} tool(s)`);
24303
- }
24304
- }
24305
- logInfo(
24306
- `Server "${id}": ${serverTools.length} tool(s)${serverTools.length > 0 ? " \u2014 " + serverTools.map((t) => t.name).join(", ") : ""}`
24307
- );
24308
- for (const tool of serverTools) {
24309
- if (toolToClient.has(tool.name)) {
24310
- logWarn(
24311
- `Tool "${tool.name}" from "${id}" shadows existing tool from "${toolToServer.get(tool.name)}"`
24312
- );
24313
- }
24314
- toolToClient.set(tool.name, client);
24315
- toolToServer.set(tool.name, id);
24316
- toolsByName.set(tool.name, tool);
24307
+ return { id, entry, connected };
24308
+ } catch (e) {
24309
+ throw new Error(`Server "${id}": ${e}`);
24317
24310
  }
24318
- } catch (e) {
24319
- logError(`Failed to connect to server "${id}": ${e}`);
24311
+ })
24312
+ );
24313
+ for (const result of results) {
24314
+ if (result.status === "rejected") {
24315
+ logError(`Failed to connect: ${result.reason}`);
24316
+ continue;
24317
+ }
24318
+ const { id, entry, connected } = result.value;
24319
+ const { client, cleanup } = connected;
24320
+ clients.set(id, client);
24321
+ cleanups.set(id, cleanup);
24322
+ let serverTools = await discoverTools(client);
24323
+ if (entry.allowTools || entry.denyTools) {
24324
+ const perFilter = {};
24325
+ if (entry.allowTools) perFilter.allowTools = new Set(entry.allowTools);
24326
+ if (entry.denyTools) perFilter.denyTools = new Set(entry.denyTools);
24327
+ const before = serverTools.length;
24328
+ serverTools = filterTools(serverTools, perFilter);
24329
+ if (serverTools.length !== before) {
24330
+ logInfo(`Server "${id}": filtered ${before} \u2192 ${serverTools.length} tool(s)`);
24331
+ }
24332
+ }
24333
+ logInfo(
24334
+ `Server "${id}": ${serverTools.length} tool(s)${serverTools.length > 0 ? " \u2014 " + serverTools.map((t) => t.name).join(", ") : ""}`
24335
+ );
24336
+ for (const tool of serverTools) {
24337
+ if (toolToClient.has(tool.name)) {
24338
+ logWarn(
24339
+ `Tool "${tool.name}" from "${id}" shadows existing tool from "${toolToServer.get(tool.name)}"`
24340
+ );
24341
+ }
24342
+ toolToClient.set(tool.name, client);
24343
+ toolToServer.set(tool.name, id);
24344
+ toolsByName.set(tool.name, tool);
24320
24345
  }
24321
24346
  }
24322
24347
  if (clients.size === 0) {
@@ -25829,8 +25854,8 @@ var PersistentMcpBridge = class {
25829
25854
  }
25830
25855
  return t;
25831
25856
  },
25832
- pingIntervalMs: entry.config.pingIntervalMs,
25833
- pingTimeoutMs: entry.config.pingTimeoutMs
25857
+ pingIntervalMs: 0
25858
+ // No heartbeat for stdio — child process close/error events handle detection
25834
25859
  });
25835
25860
  const client = new Client(
25836
25861
  { name: "nuwax-persistent-bridge", version: "1.0.0" },
@@ -25848,7 +25873,6 @@ var PersistentMcpBridge = class {
25848
25873
  };
25849
25874
  await wrapper.start();
25850
25875
  await client.connect(wrapper);
25851
- wrapper.enableHeartbeat();
25852
25876
  entry.client = client;
25853
25877
  entry.transport = wrapper;
25854
25878
  const result = await client.listTools();
@@ -24,7 +24,16 @@ export async function runStdio(config, allowTools, denyTools) {
24
24
  const toolToClient = new Map();
25
25
  const toolToServer = new Map();
26
26
  const toolsByName = new Map();
27
- for (const [id, entry] of entries) {
27
+ // Filter out persistent entries (handled by PersistentMcpBridge, not this proxy)
28
+ const connectableEntries = entries.filter(([id, entry]) => {
29
+ if (entry.persistent) {
30
+ logWarn(`Skipping persistent server "${id}" (handled by PersistentMcpBridge)`);
31
+ return false;
32
+ }
33
+ return true;
34
+ });
35
+ // Connect to all servers in parallel
36
+ const results = await Promise.allSettled(connectableEntries.map(async ([id, entry]) => {
28
37
  try {
29
38
  let connected;
30
39
  if (isSseEntry(entry)) {
@@ -46,36 +55,43 @@ export async function runStdio(config, allowTools, denyTools) {
46
55
  else {
47
56
  connected = await connectStdio(id, entry, baseEnv);
48
57
  }
49
- const { client, cleanup } = connected;
50
- clients.set(id, client);
51
- cleanups.set(id, cleanup);
52
- let serverTools = await discoverTools(client);
53
- // Per-server tool filtering (allowTools/denyTools in config entry)
54
- if (entry.allowTools || entry.denyTools) {
55
- const perFilter = {};
56
- if (entry.allowTools)
57
- perFilter.allowTools = new Set(entry.allowTools);
58
- if (entry.denyTools)
59
- perFilter.denyTools = new Set(entry.denyTools);
60
- const before = serverTools.length;
61
- serverTools = filterTools(serverTools, perFilter);
62
- if (serverTools.length !== before) {
63
- logInfo(`Server "${id}": filtered ${before} → ${serverTools.length} tool(s)`);
64
- }
65
- }
66
- logInfo(`Server "${id}": ${serverTools.length} tool(s)${serverTools.length > 0 ? ' — ' + serverTools.map((t) => t.name).join(', ') : ''}`);
67
- for (const tool of serverTools) {
68
- if (toolToClient.has(tool.name)) {
69
- logWarn(`Tool "${tool.name}" from "${id}" shadows existing tool from "${toolToServer.get(tool.name)}"`);
70
- }
71
- toolToClient.set(tool.name, client);
72
- toolToServer.set(tool.name, id);
73
- toolsByName.set(tool.name, tool);
74
- }
58
+ return { id, entry, connected };
75
59
  }
76
60
  catch (e) {
77
- logError(`Failed to connect to server "${id}": ${e}`);
78
- // Continue with remaining servers — partial startup is acceptable
61
+ throw new Error(`Server "${id}": ${e}`);
62
+ }
63
+ }));
64
+ for (const result of results) {
65
+ if (result.status === 'rejected') {
66
+ logError(`Failed to connect: ${result.reason}`);
67
+ continue;
68
+ }
69
+ const { id, entry, connected } = result.value;
70
+ const { client, cleanup } = connected;
71
+ clients.set(id, client);
72
+ cleanups.set(id, cleanup);
73
+ let serverTools = await discoverTools(client);
74
+ // Per-server tool filtering (allowTools/denyTools in config entry)
75
+ if (entry.allowTools || entry.denyTools) {
76
+ const perFilter = {};
77
+ if (entry.allowTools)
78
+ perFilter.allowTools = new Set(entry.allowTools);
79
+ if (entry.denyTools)
80
+ perFilter.denyTools = new Set(entry.denyTools);
81
+ const before = serverTools.length;
82
+ serverTools = filterTools(serverTools, perFilter);
83
+ if (serverTools.length !== before) {
84
+ logInfo(`Server "${id}": filtered ${before} → ${serverTools.length} tool(s)`);
85
+ }
86
+ }
87
+ logInfo(`Server "${id}": ${serverTools.length} tool(s)${serverTools.length > 0 ? ' — ' + serverTools.map((t) => t.name).join(', ') : ''}`);
88
+ for (const tool of serverTools) {
89
+ if (toolToClient.has(tool.name)) {
90
+ logWarn(`Tool "${tool.name}" from "${id}" shadows existing tool from "${toolToServer.get(tool.name)}"`);
91
+ }
92
+ toolToClient.set(tool.name, client);
93
+ toolToServer.set(tool.name, id);
94
+ toolsByName.set(tool.name, tool);
79
95
  }
80
96
  }
81
97
  if (clients.size === 0) {
@@ -61,7 +61,7 @@ export declare class ResilientTransportWrapper implements Transport {
61
61
  /**
62
62
  * Enable heartbeat monitoring. Call this AFTER the MCP client has
63
63
  * completed its initialize handshake (client.connect()), otherwise
64
- * the server will reject ping requests with "Server not initialized".
64
+ * the server will reject requests with "Server not initialized".
65
65
  */
66
66
  enableHeartbeat(): void;
67
67
  private performConnect;
@@ -70,8 +70,6 @@ export declare class ResilientTransportWrapper implements Transport {
70
70
  private stopHeartbeat;
71
71
  /** Track consecutive ping timeouts (no response at all, not even an error) */
72
72
  private consecutivePingTimeouts;
73
- /** If server doesn't support ping, auto-disable heartbeat after maxConsecutiveFailures timeouts */
74
- private pingDisabled;
75
73
  private checkHealth;
76
74
  private triggerReconnect;
77
75
  private flushQueue;
package/dist/resilient.js CHANGED
@@ -26,7 +26,7 @@ export class ResilientTransportWrapper {
26
26
  mcpClient = null;
27
27
  heartbeatTimer = null;
28
28
  consecutiveFailures = 0;
29
- state = 'connecting';
29
+ state = 'idle';
30
30
  // Handlers required by the Transport interface
31
31
  onclose;
32
32
  onerror;
@@ -37,7 +37,21 @@ export class ResilientTransportWrapper {
37
37
  pendingPings = new Map();
38
38
  constructor(options) {
39
39
  this.log = options.logger ?? defaultLogger;
40
- this.options = { ...DEFAULT_OPTIONS, logger: this.log, ...options };
40
+ // Build options by starting from defaults and only overriding with
41
+ // explicitly provided values. Spreading { pingIntervalMs: undefined }
42
+ // would clobber the default 20000, causing setInterval(fn, undefined)
43
+ // → interval ~0ms (fires every tick).
44
+ this.options = {
45
+ ...DEFAULT_OPTIONS,
46
+ logger: this.log,
47
+ connectParams: options.connectParams,
48
+ ...(options.name !== undefined && { name: options.name }),
49
+ ...(options.pingIntervalMs !== undefined && { pingIntervalMs: options.pingIntervalMs }),
50
+ ...(options.pingTimeoutMs !== undefined && { pingTimeoutMs: options.pingTimeoutMs }),
51
+ ...(options.maxConsecutiveFailures !== undefined && { maxConsecutiveFailures: options.maxConsecutiveFailures }),
52
+ ...(options.reconnectDelayMs !== undefined && { reconnectDelayMs: options.reconnectDelayMs }),
53
+ ...(options.maxQueueSize !== undefined && { maxQueueSize: options.maxQueueSize }),
54
+ };
41
55
  }
42
56
  /**
43
57
  * Initializes the transport and connects to the backend
@@ -58,7 +72,7 @@ export class ResilientTransportWrapper {
58
72
  /**
59
73
  * Enable heartbeat monitoring. Call this AFTER the MCP client has
60
74
  * completed its initialize handshake (client.connect()), otherwise
61
- * the server will reject ping requests with "Server not initialized".
75
+ * the server will reject requests with "Server not initialized".
62
76
  */
63
77
  enableHeartbeat() {
64
78
  this.startHeartbeat();
@@ -71,6 +85,7 @@ export class ResilientTransportWrapper {
71
85
  await this.activeTransport.start();
72
86
  this.state = 'connected';
73
87
  this.consecutiveFailures = 0;
88
+ this.consecutivePingTimeouts = 0;
74
89
  this.log.info(`[McpProxy] [ResilientTransport:${this.options.name}] Connected via ${this.activeTransport.constructor.name}`);
75
90
  // Flush any queued messages
76
91
  this.flushQueue();
@@ -112,7 +127,7 @@ export class ResilientTransportWrapper {
112
127
  }
113
128
  };
114
129
  transport.onmessage = (message) => {
115
- // Intercept our own ping responses here (both success and error responses)
130
+ // Intercept our own heartbeat responses (tools/list used as health check)
116
131
  if ('id' in message && typeof message.id === 'string' && message.id.startsWith('respl-ping-')) {
117
132
  const resolve = this.pendingPings.get(message.id);
118
133
  if (resolve) {
@@ -143,64 +158,54 @@ export class ResilientTransportWrapper {
143
158
  }
144
159
  /** Track consecutive ping timeouts (no response at all, not even an error) */
145
160
  consecutivePingTimeouts = 0;
146
- /** If server doesn't support ping, auto-disable heartbeat after maxConsecutiveFailures timeouts */
147
- pingDisabled = false;
148
161
  async checkHealth() {
149
162
  if (this.state !== 'connected' || !this.activeTransport)
150
163
  return;
151
- if (this.pingDisabled)
152
- return;
153
164
  try {
154
- // Send raw ping JSON-RPC
155
- const pingId = `respl-ping-${Date.now()}`;
165
+ // Use tools/list as health check (all MCP servers must support it).
166
+ // Unlike ping, which is optional and many servers ignore, tools/list
167
+ // is a required MCP method — matching Rust mcp-proxy behavior.
168
+ const healthId = `respl-ping-${Date.now()}`;
156
169
  const responsePromise = new Promise((resolve) => {
157
- this.pendingPings.set(pingId, resolve);
170
+ this.pendingPings.set(healthId, resolve);
158
171
  setTimeout(() => {
159
- if (this.pendingPings.has(pingId)) {
160
- this.pendingPings.delete(pingId);
172
+ if (this.pendingPings.has(healthId)) {
173
+ this.pendingPings.delete(healthId);
161
174
  resolve(false); // Timeout
162
175
  }
163
176
  }, this.options.pingTimeoutMs);
164
177
  });
165
- // Try to send ping — if send() throws, the transport itself is broken
166
- let sendFailed = false;
178
+ // Try to send tools/list — if send() throws, the transport itself is broken
167
179
  try {
168
- this.log.info(`[McpProxy] [ResilientTransport:${this.options.name}] 💓 Sending heartbeat ping (id: ${pingId})`);
180
+ this.log.info(`[McpProxy] [ResilientTransport:${this.options.name}] 💓 Sending heartbeat (id: ${healthId})`);
169
181
  await this.activeTransport.send({
170
182
  jsonrpc: '2.0',
171
- id: pingId,
172
- method: 'ping',
183
+ id: healthId,
184
+ method: 'tools/list',
185
+ params: {},
173
186
  });
174
187
  }
175
188
  catch (sendErr) {
176
- sendFailed = true;
177
189
  throw sendErr; // Transport broken, treat as real failure
178
190
  }
179
191
  const success = await responsePromise;
180
192
  if (!success) {
181
- // Ping was sent successfully but timed out — server might not support ping
182
193
  this.consecutivePingTimeouts++;
194
+ this.log.warn(`[McpProxy] [ResilientTransport:${this.options.name}] Heartbeat timeout (${this.consecutivePingTimeouts}/${this.options.maxConsecutiveFailures})`);
183
195
  if (this.consecutivePingTimeouts >= this.options.maxConsecutiveFailures) {
184
- // Server consistently never responds to ping it probably doesn't support
185
- // the ping method. Auto-disable heartbeat instead of reconnecting, since
186
- // the transport is likely healthy (sends succeed, no errors from transport).
187
- this.log.warn(`[McpProxy] [ResilientTransport:${this.options.name}] Server does not respond to ping. Disabling heartbeat monitoring.`);
188
- this.pingDisabled = true;
189
- this.stopHeartbeat();
190
- return;
196
+ this.log.error(`[McpProxy] [ResilientTransport:${this.options.name}] Max consecutive heartbeat timeouts reached. Force reconnecting.`);
197
+ this.triggerReconnect();
191
198
  }
192
- // Still count as a failure for logging, but don't reconnect yet
193
- this.log.warn(`[McpProxy] [ResilientTransport:${this.options.name}] Ping timeout (${this.consecutivePingTimeouts}/${this.options.maxConsecutiveFailures})`);
194
199
  return;
195
200
  }
196
201
  // Got a response — server is alive
197
- this.log.info(`[McpProxy] [ResilientTransport:${this.options.name}] 💖 Heartbeat successful (id: ${pingId})`);
202
+ this.log.info(`[McpProxy] [ResilientTransport:${this.options.name}] 💖 Heartbeat OK (id: ${healthId})`);
198
203
  this.consecutiveFailures = 0;
199
204
  this.consecutivePingTimeouts = 0;
200
205
  }
201
206
  catch (err) {
202
207
  this.consecutiveFailures++;
203
- this.log.warn(`[McpProxy] [ResilientTransport:${this.options.name}] 💔 Heartbeat error (attempt ${this.consecutiveFailures}/${this.options.maxConsecutiveFailures}): ${err}`);
208
+ this.log.warn(`[McpProxy] [ResilientTransport:${this.options.name}] 💔 Heartbeat error (${this.consecutiveFailures}/${this.options.maxConsecutiveFailures}): ${err}`);
204
209
  if (this.consecutiveFailures >= this.options.maxConsecutiveFailures) {
205
210
  this.log.error(`[McpProxy] [ResilientTransport:${this.options.name}] Max consecutive heartbeat failures reached. Force reconnecting.`);
206
211
  this.triggerReconnect();
package/dist/transport.js CHANGED
@@ -7,6 +7,8 @@ import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
7
7
  import { CustomStdioClientTransport } from './customStdio.js';
8
8
  import { logInfo } from './logger.js';
9
9
  import { ResilientTransportWrapper } from './resilient.js';
10
+ const DEFAULT_STDIO_CONNECTION_TIMEOUT_MS = 60_000;
11
+ const DEFAULT_HTTP_CONNECTION_TIMEOUT_MS = 30_000;
10
12
  /**
11
13
  * Helper to wrap a promise with a timeout
12
14
  */
@@ -73,16 +75,16 @@ export async function connectStdio(id, entry, baseEnv) {
73
75
  }
74
76
  return t;
75
77
  },
76
- pingIntervalMs: entry.pingIntervalMs,
77
- pingTimeoutMs: entry.pingTimeoutMs,
78
+ // No heartbeat for stdio — child process close/error events handle detection
79
+ pingIntervalMs: 0,
78
80
  });
79
81
  const client = new Client({ name: `proxy-${id}`, version: '1.0.0' });
82
+ const timeoutMs = entry.connectionTimeoutMs ?? DEFAULT_STDIO_CONNECTION_TIMEOUT_MS;
80
83
  try {
81
84
  await withTimeout((async () => {
82
85
  await wrapper.start();
83
86
  await client.connect(wrapper);
84
- wrapper.enableHeartbeat(); // Start heartbeat AFTER MCP initialize completes
85
- })(), 5000, `Connection initialization timed out after 5s`);
87
+ })(), timeoutMs, `Connection initialization timed out after ${timeoutMs / 1000}s`);
86
88
  }
87
89
  catch (err) {
88
90
  try {
@@ -121,12 +123,13 @@ export async function connectStreamable(id, entry) {
121
123
  pingTimeoutMs: entry.pingTimeoutMs,
122
124
  });
123
125
  const client = new Client({ name: `proxy-${id}`, version: '1.0.0' });
126
+ const timeoutMs = entry.connectionTimeoutMs ?? DEFAULT_HTTP_CONNECTION_TIMEOUT_MS;
124
127
  try {
125
128
  await withTimeout((async () => {
126
129
  await wrapper.start();
127
130
  await client.connect(wrapper);
128
131
  wrapper.enableHeartbeat(); // Start heartbeat AFTER MCP initialize completes
129
- })(), 5000, `Connection initialization timed out after 5s`);
132
+ })(), timeoutMs, `Connection initialization timed out after ${timeoutMs / 1000}s`);
130
133
  }
131
134
  catch (err) {
132
135
  try {
@@ -165,12 +168,13 @@ export async function connectSse(id, entry) {
165
168
  pingTimeoutMs: entry.pingTimeoutMs,
166
169
  });
167
170
  const client = new Client({ name: `proxy-${id}`, version: '1.0.0' });
171
+ const timeoutMs = entry.connectionTimeoutMs ?? DEFAULT_HTTP_CONNECTION_TIMEOUT_MS;
168
172
  try {
169
173
  await withTimeout((async () => {
170
174
  await wrapper.start();
171
175
  await client.connect(wrapper);
172
176
  wrapper.enableHeartbeat(); // Start heartbeat AFTER MCP initialize completes
173
- })(), 5000, `Connection initialization timed out after 5s`);
177
+ })(), timeoutMs, `Connection initialization timed out after ${timeoutMs / 1000}s`);
174
178
  }
175
179
  catch (err) {
176
180
  try {
package/dist/types.d.ts CHANGED
@@ -10,10 +10,8 @@ export interface StdioServerEntry {
10
10
  allowTools?: string[];
11
11
  /** 工具黑名单(排除指定工具) */
12
12
  denyTools?: string[];
13
- /** Heartbeat ping interval configuration (ms) */
14
- pingIntervalMs?: number;
15
- /** Heartbeat ping timeout configuration (ms) */
16
- pingTimeoutMs?: number;
13
+ /** Connection initialization timeout (ms). Defaults to 60000 for stdio. */
14
+ connectionTimeoutMs?: number;
17
15
  }
18
16
  /**
19
17
  * Streamable HTTP 类型: 连接远程 MCP server (Streamable HTTP)
@@ -34,6 +32,8 @@ export interface StreamableServerEntry {
34
32
  pingIntervalMs?: number;
35
33
  /** Heartbeat ping timeout configuration (ms) */
36
34
  pingTimeoutMs?: number;
35
+ /** Connection initialization timeout (ms). Defaults to 30000. */
36
+ connectionTimeoutMs?: number;
37
37
  }
38
38
  /**
39
39
  * SSE 类型: 连接远程 MCP server (Server-Sent Events)
@@ -53,6 +53,8 @@ export interface SseServerEntry {
53
53
  pingIntervalMs?: number;
54
54
  /** Heartbeat ping timeout configuration (ms) */
55
55
  pingTimeoutMs?: number;
56
+ /** Connection initialization timeout (ms). Defaults to 30000. */
57
+ connectionTimeoutMs?: number;
56
58
  }
57
59
  export type McpServerEntry = StdioServerEntry | StreamableServerEntry | SseServerEntry;
58
60
  export declare function isSseEntry(entry: McpServerEntry): entry is SseServerEntry;
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "nuwax-mcp-stdio-proxy",
3
- "version": "1.4.7",
3
+ "version": "1.4.8",
4
4
  "description": "TypeScript MCP proxy — aggregates multiple MCP servers (stdio + streamable-http + SSE) with convert & proxy modes",
5
5
  "type": "module",
6
6
  "bin": {
7
- "nuwax-mcp-stdio-proxy": "./dist/index.js"
7
+ "nuwax-mcp-stdio-proxy": "dist/index.js"
8
8
  },
9
9
  "main": "./dist/lib.js",
10
10
  "types": "./dist/lib.d.ts",