nuwax-mcp-stdio-proxy 1.4.9 → 1.4.10

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/index.js CHANGED
@@ -23118,6 +23118,12 @@ var ResilientTransportWrapper = class {
23118
23118
  mcpClient = null;
23119
23119
  heartbeatTimer = null;
23120
23120
  consecutiveFailures = 0;
23121
+ /** Captured MCP initialize request for replay on reconnect */
23122
+ initializeMessage = null;
23123
+ /** Captured MCP notifications/initialized for replay on reconnect */
23124
+ initializedNotification = null;
23125
+ /** Pending re-initialize promise resolver */
23126
+ pendingReInit = null;
23121
23127
  state = "idle";
23122
23128
  /** Current retry attempt count (reset on successful connect) */
23123
23129
  retryAttempt = 0;
@@ -23184,9 +23190,27 @@ var ResilientTransportWrapper = class {
23184
23190
  this.state = "connected";
23185
23191
  this.consecutiveFailures = 0;
23186
23192
  this.consecutivePingTimeouts = 0;
23187
- this.retryAttempt = 0;
23188
23193
  this.heartbeatOkCount = 0;
23189
23194
  this.log.info(`[McpProxy] [ResilientTransport:${this.options.name}] \u2705 Connected via ${this.activeTransport.constructor.name}`);
23195
+ if (!initial && this.initializeMessage) {
23196
+ this.log.info(`[McpProxy] [ResilientTransport:${this.options.name}] \u{1F504} Re-initializing MCP session...`);
23197
+ try {
23198
+ await this.performReInitialize();
23199
+ this.log.info(`[McpProxy] [ResilientTransport:${this.options.name}] \u2705 MCP session re-initialized`);
23200
+ } catch (err) {
23201
+ this.retryAttempt++;
23202
+ const delay = this.getBackoffDelay();
23203
+ this.log.error(`[McpProxy] [ResilientTransport:${this.options.name}] \u274C Re-initialize failed: ${err}`);
23204
+ this.log.info(`[McpProxy] [ResilientTransport:${this.options.name}] \u{1F504} Retrying in ${delay}ms (attempt ${this.retryAttempt})...`);
23205
+ this.state = "reconnecting";
23206
+ this.cleanupTransport();
23207
+ setTimeout(() => {
23208
+ this.performConnect();
23209
+ }, delay);
23210
+ return;
23211
+ }
23212
+ }
23213
+ this.retryAttempt = 0;
23190
23214
  this.flushQueue();
23191
23215
  if (!initial) {
23192
23216
  this.startHeartbeat();
@@ -23202,6 +23226,43 @@ var ResilientTransportWrapper = class {
23202
23226
  }, delay);
23203
23227
  }
23204
23228
  }
23229
+ /**
23230
+ * Replay the MCP initialize handshake on a reconnected transport.
23231
+ * Sends the captured `initialize` request with a unique internal ID,
23232
+ * waits for the response, then sends `notifications/initialized`.
23233
+ */
23234
+ async performReInitialize() {
23235
+ if (!this.activeTransport || !this.initializeMessage) {
23236
+ throw new Error("No transport or no captured initialize message");
23237
+ }
23238
+ const initId = `respl-init-${Date.now()}`;
23239
+ const initRequest = {
23240
+ ...this.initializeMessage,
23241
+ id: initId
23242
+ };
23243
+ const initPromise = new Promise((resolve) => {
23244
+ this.pendingReInit = resolve;
23245
+ setTimeout(() => {
23246
+ if (this.pendingReInit) {
23247
+ this.pendingReInit = null;
23248
+ resolve(false);
23249
+ }
23250
+ }, this.options.pingTimeoutMs);
23251
+ });
23252
+ try {
23253
+ await this.activeTransport.send(initRequest);
23254
+ } catch (err) {
23255
+ this.pendingReInit = null;
23256
+ throw err;
23257
+ }
23258
+ const success2 = await initPromise;
23259
+ if (!success2) {
23260
+ throw new Error("Re-initialize timed out");
23261
+ }
23262
+ if (this.initializedNotification) {
23263
+ await this.activeTransport.send(this.initializedNotification);
23264
+ }
23265
+ }
23205
23266
  bindInnerTransport(transport) {
23206
23267
  transport.onclose = () => {
23207
23268
  if (this.state !== "closed") {
@@ -23220,6 +23281,13 @@ var ResilientTransportWrapper = class {
23220
23281
  }
23221
23282
  };
23222
23283
  transport.onmessage = (message) => {
23284
+ if ("id" in message && typeof message.id === "string" && message.id.startsWith("respl-init-")) {
23285
+ if (this.pendingReInit) {
23286
+ this.pendingReInit(true);
23287
+ this.pendingReInit = null;
23288
+ }
23289
+ return;
23290
+ }
23223
23291
  if ("id" in message && typeof message.id === "string" && message.id.startsWith("respl-ping-")) {
23224
23292
  const resolve = this.pendingPings.get(message.id);
23225
23293
  if (resolve) {
@@ -23299,12 +23367,9 @@ var ResilientTransportWrapper = class {
23299
23367
  }
23300
23368
  }
23301
23369
  /**
23302
- * Close the current transport and schedule a reconnect with exponential backoff.
23370
+ * Detach handlers from and close the current active transport.
23303
23371
  */
23304
- triggerReconnect() {
23305
- if (this.state === "reconnecting" || this.state === "closed") return;
23306
- this.state = "reconnecting";
23307
- this.stopHeartbeat();
23372
+ cleanupTransport() {
23308
23373
  if (this.activeTransport) {
23309
23374
  this.activeTransport.onclose = void 0;
23310
23375
  this.activeTransport.onerror = void 0;
@@ -23314,6 +23379,15 @@ var ResilientTransportWrapper = class {
23314
23379
  }
23315
23380
  this.activeTransport = null;
23316
23381
  }
23382
+ }
23383
+ /**
23384
+ * Close the current transport and schedule a reconnect with exponential backoff.
23385
+ */
23386
+ triggerReconnect() {
23387
+ if (this.state === "reconnecting" || this.state === "closed") return;
23388
+ this.state = "reconnecting";
23389
+ this.stopHeartbeat();
23390
+ this.cleanupTransport();
23317
23391
  const delay = this.getBackoffDelay();
23318
23392
  this.retryAttempt++;
23319
23393
  this.log.warn(`[McpProxy] [ResilientTransport:${this.options.name}] \u{1F504} Closed. Retrying in ${delay}ms (attempt ${this.retryAttempt})...`);
@@ -23354,6 +23428,10 @@ var ResilientTransportWrapper = class {
23354
23428
  this.messageQueue.push(message);
23355
23429
  return;
23356
23430
  }
23431
+ if ("method" in message) {
23432
+ if (message.method === "initialize") this.initializeMessage = message;
23433
+ else if (message.method === "notifications/initialized") this.initializedNotification = message;
23434
+ }
23357
23435
  if (!this.activeTransport) {
23358
23436
  throw new Error("No active transport to send message");
23359
23437
  }
@@ -24217,7 +24295,7 @@ var StdioServerTransport = class {
24217
24295
 
24218
24296
  // src/constants.ts
24219
24297
  var PKG_NAME = "nuwax-mcp-stdio-proxy";
24220
- var PKG_VERSION = "1.4.9";
24298
+ var PKG_VERSION = "1.4.10";
24221
24299
 
24222
24300
  // src/shared.ts
24223
24301
  async function discoverTools(client) {
@@ -47,6 +47,12 @@ export declare class ResilientTransportWrapper implements Transport {
47
47
  private mcpClient;
48
48
  private heartbeatTimer;
49
49
  private consecutiveFailures;
50
+ /** Captured MCP initialize request for replay on reconnect */
51
+ private initializeMessage;
52
+ /** Captured MCP notifications/initialized for replay on reconnect */
53
+ private initializedNotification;
54
+ /** Pending re-initialize promise resolver */
55
+ private pendingReInit;
50
56
  private state;
51
57
  /** Current retry attempt count (reset on successful connect) */
52
58
  private retryAttempt;
@@ -77,6 +83,12 @@ export declare class ResilientTransportWrapper implements Transport {
77
83
  */
78
84
  enableHeartbeat(): void;
79
85
  private performConnect;
86
+ /**
87
+ * Replay the MCP initialize handshake on a reconnected transport.
88
+ * Sends the captured `initialize` request with a unique internal ID,
89
+ * waits for the response, then sends `notifications/initialized`.
90
+ */
91
+ private performReInitialize;
80
92
  private bindInnerTransport;
81
93
  private startHeartbeat;
82
94
  private stopHeartbeat;
@@ -85,6 +97,10 @@ export declare class ResilientTransportWrapper implements Transport {
85
97
  /** Successful heartbeat counter (for reducing log volume) */
86
98
  private heartbeatOkCount;
87
99
  private checkHealth;
100
+ /**
101
+ * Detach handlers from and close the current active transport.
102
+ */
103
+ private cleanupTransport;
88
104
  /**
89
105
  * Close the current transport and schedule a reconnect with exponential backoff.
90
106
  */
package/dist/resilient.js CHANGED
@@ -30,6 +30,12 @@ export class ResilientTransportWrapper {
30
30
  mcpClient = null;
31
31
  heartbeatTimer = null;
32
32
  consecutiveFailures = 0;
33
+ /** Captured MCP initialize request for replay on reconnect */
34
+ initializeMessage = null;
35
+ /** Captured MCP notifications/initialized for replay on reconnect */
36
+ initializedNotification = null;
37
+ /** Pending re-initialize promise resolver */
38
+ pendingReInit = null;
33
39
  state = 'idle';
34
40
  /** Current retry attempt count (reset on successful connect) */
35
41
  retryAttempt = 0;
@@ -103,9 +109,33 @@ export class ResilientTransportWrapper {
103
109
  this.state = 'connected';
104
110
  this.consecutiveFailures = 0;
105
111
  this.consecutivePingTimeouts = 0;
106
- this.retryAttempt = 0;
107
112
  this.heartbeatOkCount = 0;
108
113
  this.log.info(`[McpProxy] [ResilientTransport:${this.options.name}] ✅ Connected via ${this.activeTransport.constructor.name}`);
114
+ // On reconnect, replay MCP initialize handshake before doing anything else.
115
+ // The server requires initialize + notifications/initialized before accepting
116
+ // any other requests (tools/list, etc).
117
+ if (!initial && this.initializeMessage) {
118
+ this.log.info(`[McpProxy] [ResilientTransport:${this.options.name}] 🔄 Re-initializing MCP session...`);
119
+ try {
120
+ await this.performReInitialize();
121
+ this.log.info(`[McpProxy] [ResilientTransport:${this.options.name}] ✅ MCP session re-initialized`);
122
+ }
123
+ catch (err) {
124
+ // Re-initialize failed — treat as a connect failure, preserve backoff
125
+ this.retryAttempt++;
126
+ const delay = this.getBackoffDelay();
127
+ this.log.error(`[McpProxy] [ResilientTransport:${this.options.name}] ❌ Re-initialize failed: ${err}`);
128
+ this.log.info(`[McpProxy] [ResilientTransport:${this.options.name}] 🔄 Retrying in ${delay}ms (attempt ${this.retryAttempt})...`);
129
+ this.state = 'reconnecting';
130
+ this.cleanupTransport();
131
+ setTimeout(() => {
132
+ this.performConnect();
133
+ }, delay);
134
+ return;
135
+ }
136
+ }
137
+ // Reset retry count only after successful connect + re-initialize
138
+ this.retryAttempt = 0;
109
139
  // Flush any queued messages
110
140
  this.flushQueue();
111
141
  // Only start heartbeat on reconnects — initial connections need
@@ -127,6 +157,46 @@ export class ResilientTransportWrapper {
127
157
  }, delay);
128
158
  }
129
159
  }
160
+ /**
161
+ * Replay the MCP initialize handshake on a reconnected transport.
162
+ * Sends the captured `initialize` request with a unique internal ID,
163
+ * waits for the response, then sends `notifications/initialized`.
164
+ */
165
+ async performReInitialize() {
166
+ if (!this.activeTransport || !this.initializeMessage) {
167
+ throw new Error('No transport or no captured initialize message');
168
+ }
169
+ const initId = `respl-init-${Date.now()}`;
170
+ // Build the re-initialize request using the original params but with our internal ID
171
+ const initRequest = {
172
+ ...this.initializeMessage,
173
+ id: initId,
174
+ };
175
+ const initPromise = new Promise((resolve) => {
176
+ this.pendingReInit = resolve;
177
+ setTimeout(() => {
178
+ if (this.pendingReInit) {
179
+ this.pendingReInit = null;
180
+ resolve(false); // Timeout
181
+ }
182
+ }, this.options.pingTimeoutMs);
183
+ });
184
+ try {
185
+ await this.activeTransport.send(initRequest);
186
+ }
187
+ catch (err) {
188
+ this.pendingReInit = null;
189
+ throw err;
190
+ }
191
+ const success = await initPromise;
192
+ if (!success) {
193
+ throw new Error('Re-initialize timed out');
194
+ }
195
+ // Send notifications/initialized if we captured it
196
+ if (this.initializedNotification) {
197
+ await this.activeTransport.send(this.initializedNotification);
198
+ }
199
+ }
130
200
  bindInnerTransport(transport) {
131
201
  transport.onclose = () => {
132
202
  // If the inner transport closes but we are not intentionally closed
@@ -148,6 +218,14 @@ export class ResilientTransportWrapper {
148
218
  }
149
219
  };
150
220
  transport.onmessage = (message) => {
221
+ // Intercept re-initialize responses (don't forward to Client)
222
+ if ('id' in message && typeof message.id === 'string' && message.id.startsWith('respl-init-')) {
223
+ if (this.pendingReInit) {
224
+ this.pendingReInit(true);
225
+ this.pendingReInit = null;
226
+ }
227
+ return;
228
+ }
151
229
  // Intercept our own heartbeat responses (tools/list used as health check)
152
230
  if ('id' in message && typeof message.id === 'string' && message.id.startsWith('respl-ping-')) {
153
231
  const resolve = this.pendingPings.get(message.id);
@@ -239,16 +317,10 @@ export class ResilientTransportWrapper {
239
317
  }
240
318
  }
241
319
  /**
242
- * Close the current transport and schedule a reconnect with exponential backoff.
320
+ * Detach handlers from and close the current active transport.
243
321
  */
244
- triggerReconnect() {
245
- if (this.state === 'reconnecting' || this.state === 'closed')
246
- return;
247
- this.state = 'reconnecting';
248
- this.stopHeartbeat();
249
- // Clean up old transport
322
+ cleanupTransport() {
250
323
  if (this.activeTransport) {
251
- // Avoid triggering our own onclose
252
324
  this.activeTransport.onclose = undefined;
253
325
  this.activeTransport.onerror = undefined;
254
326
  try {
@@ -257,6 +329,16 @@ export class ResilientTransportWrapper {
257
329
  catch { /* ignore */ }
258
330
  this.activeTransport = null;
259
331
  }
332
+ }
333
+ /**
334
+ * Close the current transport and schedule a reconnect with exponential backoff.
335
+ */
336
+ triggerReconnect() {
337
+ if (this.state === 'reconnecting' || this.state === 'closed')
338
+ return;
339
+ this.state = 'reconnecting';
340
+ this.stopHeartbeat();
341
+ this.cleanupTransport();
260
342
  const delay = this.getBackoffDelay();
261
343
  this.retryAttempt++;
262
344
  this.log.warn(`[McpProxy] [ResilientTransport:${this.options.name}] 🔄 Closed. Retrying in ${delay}ms (attempt ${this.retryAttempt})...`);
@@ -301,6 +383,13 @@ export class ResilientTransportWrapper {
301
383
  this.messageQueue.push(message);
302
384
  return;
303
385
  }
386
+ // Capture initialize handshake messages for replay on reconnect
387
+ if ('method' in message) {
388
+ if (message.method === 'initialize')
389
+ this.initializeMessage = message;
390
+ else if (message.method === 'notifications/initialized')
391
+ this.initializedNotification = message;
392
+ }
304
393
  if (!this.activeTransport) {
305
394
  throw new Error('No active transport to send message');
306
395
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuwax-mcp-stdio-proxy",
3
- "version": "1.4.9",
3
+ "version": "1.4.10",
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": {