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 +85 -7
- package/dist/resilient.d.ts +16 -0
- package/dist/resilient.js +98 -9
- package/package.json +1 -1
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
|
-
*
|
|
23370
|
+
* Detach handlers from and close the current active transport.
|
|
23303
23371
|
*/
|
|
23304
|
-
|
|
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.
|
|
24298
|
+
var PKG_VERSION = "1.4.10";
|
|
24221
24299
|
|
|
24222
24300
|
// src/shared.ts
|
|
24223
24301
|
async function discoverTools(client) {
|
package/dist/resilient.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
320
|
+
* Detach handlers from and close the current active transport.
|
|
243
321
|
*/
|
|
244
|
-
|
|
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