nuwax-mcp-stdio-proxy 1.4.6 → 1.4.7
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/README.md +54 -31
- package/dist/bridge.js +33 -20
- package/dist/index.js +414 -48
- package/dist/modes/convert.d.ts +2 -0
- package/dist/modes/convert.js +2 -2
- package/dist/resilient.d.ts +80 -0
- package/dist/resilient.js +297 -0
- package/dist/transport.d.ts +2 -0
- package/dist/transport.js +101 -22
- package/dist/types.d.ts +12 -0
- package/package.json +5 -3
package/dist/index.js
CHANGED
|
@@ -23019,7 +23019,272 @@ function getDefaultEnvironment() {
|
|
|
23019
23019
|
return env;
|
|
23020
23020
|
}
|
|
23021
23021
|
|
|
23022
|
+
// src/resilient.ts
|
|
23023
|
+
var DEFAULT_OPTIONS = {
|
|
23024
|
+
pingIntervalMs: 2e4,
|
|
23025
|
+
maxConsecutiveFailures: 3,
|
|
23026
|
+
pingTimeoutMs: 5e3,
|
|
23027
|
+
reconnectDelayMs: 3e3,
|
|
23028
|
+
maxQueueSize: 100,
|
|
23029
|
+
name: "remote"
|
|
23030
|
+
};
|
|
23031
|
+
var defaultLogger = {
|
|
23032
|
+
info: (...args) => logInfo(args.map(String).join(" ")),
|
|
23033
|
+
warn: (...args) => logWarn(args.map(String).join(" ")),
|
|
23034
|
+
error: (...args) => logError(args.map(String).join(" "))
|
|
23035
|
+
};
|
|
23036
|
+
var ResilientTransportWrapper = class {
|
|
23037
|
+
options;
|
|
23038
|
+
log;
|
|
23039
|
+
activeTransport = null;
|
|
23040
|
+
mcpClient = null;
|
|
23041
|
+
heartbeatTimer = null;
|
|
23042
|
+
consecutiveFailures = 0;
|
|
23043
|
+
state = "connecting";
|
|
23044
|
+
// Handlers required by the Transport interface
|
|
23045
|
+
onclose;
|
|
23046
|
+
onerror;
|
|
23047
|
+
onmessage;
|
|
23048
|
+
// Queue for messages sent while reconnecting
|
|
23049
|
+
messageQueue = [];
|
|
23050
|
+
// Pending ping requests awaiting a response
|
|
23051
|
+
pendingPings = /* @__PURE__ */ new Map();
|
|
23052
|
+
constructor(options) {
|
|
23053
|
+
this.log = options.logger ?? defaultLogger;
|
|
23054
|
+
this.options = { ...DEFAULT_OPTIONS, logger: this.log, ...options };
|
|
23055
|
+
}
|
|
23056
|
+
/**
|
|
23057
|
+
* Initializes the transport and connects to the backend
|
|
23058
|
+
*
|
|
23059
|
+
* This method is idempotent - if the transport is already connected or
|
|
23060
|
+
* connecting, subsequent calls will return immediately without creating
|
|
23061
|
+
* duplicate connections. This is important because MCP SDK's client.connect()
|
|
23062
|
+
* internally calls transport.start(), and we also call it explicitly in bridge.ts.
|
|
23063
|
+
*/
|
|
23064
|
+
async start() {
|
|
23065
|
+
if (this.state === "connected" || this.state === "connecting") {
|
|
23066
|
+
return;
|
|
23067
|
+
}
|
|
23068
|
+
this.state = "connecting";
|
|
23069
|
+
await this.performConnect(true);
|
|
23070
|
+
}
|
|
23071
|
+
/**
|
|
23072
|
+
* Enable heartbeat monitoring. Call this AFTER the MCP client has
|
|
23073
|
+
* completed its initialize handshake (client.connect()), otherwise
|
|
23074
|
+
* the server will reject ping requests with "Server not initialized".
|
|
23075
|
+
*/
|
|
23076
|
+
enableHeartbeat() {
|
|
23077
|
+
this.startHeartbeat();
|
|
23078
|
+
}
|
|
23079
|
+
async performConnect(initial = false) {
|
|
23080
|
+
try {
|
|
23081
|
+
this.activeTransport = await this.options.connectParams();
|
|
23082
|
+
this.bindInnerTransport(this.activeTransport);
|
|
23083
|
+
await this.activeTransport.start();
|
|
23084
|
+
this.state = "connected";
|
|
23085
|
+
this.consecutiveFailures = 0;
|
|
23086
|
+
this.log.info(`[McpProxy] [ResilientTransport:${this.options.name}] Connected via ${this.activeTransport.constructor.name}`);
|
|
23087
|
+
this.flushQueue();
|
|
23088
|
+
if (!initial) {
|
|
23089
|
+
this.startHeartbeat();
|
|
23090
|
+
}
|
|
23091
|
+
} catch (err) {
|
|
23092
|
+
this.log.error(`[McpProxy] [ResilientTransport:${this.options.name}] Connect failed: ${err}`);
|
|
23093
|
+
if (initial) {
|
|
23094
|
+
this.state = "closed";
|
|
23095
|
+
throw err;
|
|
23096
|
+
}
|
|
23097
|
+
this.triggerReconnect();
|
|
23098
|
+
}
|
|
23099
|
+
}
|
|
23100
|
+
bindInnerTransport(transport) {
|
|
23101
|
+
transport.onclose = () => {
|
|
23102
|
+
if (this.state !== "closed") {
|
|
23103
|
+
this.log.warn(`[McpProxy] [ResilientTransport:${this.options.name}] Inner transport closed unexpectedly. Reconnecting...`);
|
|
23104
|
+
this.triggerReconnect();
|
|
23105
|
+
}
|
|
23106
|
+
};
|
|
23107
|
+
transport.onerror = (error2) => {
|
|
23108
|
+
if (this.state !== "closed") {
|
|
23109
|
+
this.log.warn(`[McpProxy] [ResilientTransport:${this.options.name}] Inner transport error: ${error2.message}`);
|
|
23110
|
+
if (this.state === "connecting") {
|
|
23111
|
+
if (this.onerror) this.onerror(error2);
|
|
23112
|
+
return;
|
|
23113
|
+
}
|
|
23114
|
+
this.triggerReconnect();
|
|
23115
|
+
}
|
|
23116
|
+
};
|
|
23117
|
+
transport.onmessage = (message) => {
|
|
23118
|
+
if ("id" in message && typeof message.id === "string" && message.id.startsWith("respl-ping-")) {
|
|
23119
|
+
const resolve = this.pendingPings.get(message.id);
|
|
23120
|
+
if (resolve) {
|
|
23121
|
+
resolve(true);
|
|
23122
|
+
this.pendingPings.delete(message.id);
|
|
23123
|
+
}
|
|
23124
|
+
return;
|
|
23125
|
+
}
|
|
23126
|
+
if (this.onmessage) {
|
|
23127
|
+
this.onmessage(message);
|
|
23128
|
+
}
|
|
23129
|
+
};
|
|
23130
|
+
}
|
|
23131
|
+
startHeartbeat() {
|
|
23132
|
+
this.stopHeartbeat();
|
|
23133
|
+
if (this.options.pingIntervalMs <= 0) return;
|
|
23134
|
+
this.heartbeatTimer = setInterval(() => {
|
|
23135
|
+
this.checkHealth();
|
|
23136
|
+
}, this.options.pingIntervalMs);
|
|
23137
|
+
}
|
|
23138
|
+
stopHeartbeat() {
|
|
23139
|
+
if (this.heartbeatTimer) {
|
|
23140
|
+
clearInterval(this.heartbeatTimer);
|
|
23141
|
+
this.heartbeatTimer = null;
|
|
23142
|
+
}
|
|
23143
|
+
}
|
|
23144
|
+
/** Track consecutive ping timeouts (no response at all, not even an error) */
|
|
23145
|
+
consecutivePingTimeouts = 0;
|
|
23146
|
+
/** If server doesn't support ping, auto-disable heartbeat after maxConsecutiveFailures timeouts */
|
|
23147
|
+
pingDisabled = false;
|
|
23148
|
+
async checkHealth() {
|
|
23149
|
+
if (this.state !== "connected" || !this.activeTransport) return;
|
|
23150
|
+
if (this.pingDisabled) return;
|
|
23151
|
+
try {
|
|
23152
|
+
const pingId = `respl-ping-${Date.now()}`;
|
|
23153
|
+
const responsePromise = new Promise((resolve) => {
|
|
23154
|
+
this.pendingPings.set(pingId, resolve);
|
|
23155
|
+
setTimeout(() => {
|
|
23156
|
+
if (this.pendingPings.has(pingId)) {
|
|
23157
|
+
this.pendingPings.delete(pingId);
|
|
23158
|
+
resolve(false);
|
|
23159
|
+
}
|
|
23160
|
+
}, this.options.pingTimeoutMs);
|
|
23161
|
+
});
|
|
23162
|
+
let sendFailed = false;
|
|
23163
|
+
try {
|
|
23164
|
+
this.log.info(`[McpProxy] [ResilientTransport:${this.options.name}] \u{1F493} Sending heartbeat ping (id: ${pingId})`);
|
|
23165
|
+
await this.activeTransport.send({
|
|
23166
|
+
jsonrpc: "2.0",
|
|
23167
|
+
id: pingId,
|
|
23168
|
+
method: "ping"
|
|
23169
|
+
});
|
|
23170
|
+
} catch (sendErr) {
|
|
23171
|
+
sendFailed = true;
|
|
23172
|
+
throw sendErr;
|
|
23173
|
+
}
|
|
23174
|
+
const success2 = await responsePromise;
|
|
23175
|
+
if (!success2) {
|
|
23176
|
+
this.consecutivePingTimeouts++;
|
|
23177
|
+
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;
|
|
23182
|
+
}
|
|
23183
|
+
this.log.warn(`[McpProxy] [ResilientTransport:${this.options.name}] Ping timeout (${this.consecutivePingTimeouts}/${this.options.maxConsecutiveFailures})`);
|
|
23184
|
+
return;
|
|
23185
|
+
}
|
|
23186
|
+
this.log.info(`[McpProxy] [ResilientTransport:${this.options.name}] \u{1F496} Heartbeat successful (id: ${pingId})`);
|
|
23187
|
+
this.consecutiveFailures = 0;
|
|
23188
|
+
this.consecutivePingTimeouts = 0;
|
|
23189
|
+
} catch (err) {
|
|
23190
|
+
this.consecutiveFailures++;
|
|
23191
|
+
this.log.warn(`[McpProxy] [ResilientTransport:${this.options.name}] \u{1F494} Heartbeat error (attempt ${this.consecutiveFailures}/${this.options.maxConsecutiveFailures}): ${err}`);
|
|
23192
|
+
if (this.consecutiveFailures >= this.options.maxConsecutiveFailures) {
|
|
23193
|
+
this.log.error(`[McpProxy] [ResilientTransport:${this.options.name}] Max consecutive heartbeat failures reached. Force reconnecting.`);
|
|
23194
|
+
this.triggerReconnect();
|
|
23195
|
+
}
|
|
23196
|
+
}
|
|
23197
|
+
}
|
|
23198
|
+
triggerReconnect() {
|
|
23199
|
+
if (this.state === "reconnecting" || this.state === "closed") return;
|
|
23200
|
+
this.log.warn(`[McpProxy] [ResilientTransport:${this.options.name}] \u{1F504} Triggering reconnect (previous state: ${this.state})`);
|
|
23201
|
+
this.state = "reconnecting";
|
|
23202
|
+
this.stopHeartbeat();
|
|
23203
|
+
if (this.activeTransport) {
|
|
23204
|
+
this.activeTransport.onclose = void 0;
|
|
23205
|
+
this.activeTransport.onerror = void 0;
|
|
23206
|
+
try {
|
|
23207
|
+
this.activeTransport.close();
|
|
23208
|
+
} catch {
|
|
23209
|
+
}
|
|
23210
|
+
this.activeTransport = null;
|
|
23211
|
+
}
|
|
23212
|
+
setTimeout(() => {
|
|
23213
|
+
this.performConnect();
|
|
23214
|
+
}, this.options.reconnectDelayMs);
|
|
23215
|
+
}
|
|
23216
|
+
async flushQueue() {
|
|
23217
|
+
if (!this.activeTransport || this.state !== "connected") return;
|
|
23218
|
+
if (this.messageQueue.length > 0) {
|
|
23219
|
+
this.log.info(`[McpProxy] [ResilientTransport:${this.options.name}] Flushing ${this.messageQueue.length} queued requests...`);
|
|
23220
|
+
}
|
|
23221
|
+
const queueToFlush = [...this.messageQueue];
|
|
23222
|
+
this.messageQueue = [];
|
|
23223
|
+
for (let i = 0; i < queueToFlush.length; i++) {
|
|
23224
|
+
const msg = queueToFlush[i];
|
|
23225
|
+
try {
|
|
23226
|
+
await this.activeTransport.send(msg);
|
|
23227
|
+
} catch (e) {
|
|
23228
|
+
this.log.error(`[McpProxy] [ResilientTransport:${this.options.name}] Error flushing queue: ${e}`);
|
|
23229
|
+
const failedAndRemaining = queueToFlush.slice(i);
|
|
23230
|
+
this.messageQueue = [...failedAndRemaining, ...this.messageQueue];
|
|
23231
|
+
this.triggerReconnect();
|
|
23232
|
+
break;
|
|
23233
|
+
}
|
|
23234
|
+
}
|
|
23235
|
+
}
|
|
23236
|
+
// --- Transport Interface Methods ---
|
|
23237
|
+
async send(message) {
|
|
23238
|
+
if (this.state === "closed") {
|
|
23239
|
+
throw new Error("Transport is closed");
|
|
23240
|
+
}
|
|
23241
|
+
if (this.state === "reconnecting" || this.state === "connecting") {
|
|
23242
|
+
if (this.messageQueue.length >= this.options.maxQueueSize) {
|
|
23243
|
+
this.log.warn(`[McpProxy] [ResilientTransport:${this.options.name}] Message queue full, dropping oldest request.`);
|
|
23244
|
+
this.messageQueue.shift();
|
|
23245
|
+
}
|
|
23246
|
+
this.messageQueue.push(message);
|
|
23247
|
+
return;
|
|
23248
|
+
}
|
|
23249
|
+
if (!this.activeTransport) {
|
|
23250
|
+
throw new Error("No active transport to send message");
|
|
23251
|
+
}
|
|
23252
|
+
try {
|
|
23253
|
+
await this.activeTransport.send(message);
|
|
23254
|
+
} catch (err) {
|
|
23255
|
+
this.messageQueue.push(message);
|
|
23256
|
+
this.triggerReconnect();
|
|
23257
|
+
}
|
|
23258
|
+
}
|
|
23259
|
+
async close() {
|
|
23260
|
+
if (this.state === "closed") return;
|
|
23261
|
+
this.state = "closed";
|
|
23262
|
+
this.stopHeartbeat();
|
|
23263
|
+
this.messageQueue = [];
|
|
23264
|
+
if (this.activeTransport) {
|
|
23265
|
+
this.activeTransport.onclose = void 0;
|
|
23266
|
+
this.activeTransport.onerror = void 0;
|
|
23267
|
+
await this.activeTransport.close();
|
|
23268
|
+
this.activeTransport = null;
|
|
23269
|
+
}
|
|
23270
|
+
if (this.onclose) {
|
|
23271
|
+
this.onclose();
|
|
23272
|
+
}
|
|
23273
|
+
}
|
|
23274
|
+
};
|
|
23275
|
+
|
|
23022
23276
|
// src/transport.ts
|
|
23277
|
+
async function withTimeout(promise2, ms, message) {
|
|
23278
|
+
let timeoutId;
|
|
23279
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
23280
|
+
timeoutId = setTimeout(() => reject(new Error(message)), ms);
|
|
23281
|
+
});
|
|
23282
|
+
try {
|
|
23283
|
+
return await Promise.race([promise2, timeoutPromise]);
|
|
23284
|
+
} finally {
|
|
23285
|
+
clearTimeout(timeoutId);
|
|
23286
|
+
}
|
|
23287
|
+
}
|
|
23023
23288
|
function buildBaseEnv() {
|
|
23024
23289
|
const env = {};
|
|
23025
23290
|
for (const [key, value] of Object.entries(process.env)) {
|
|
@@ -23042,28 +23307,53 @@ function buildRequestHeaders(entry) {
|
|
|
23042
23307
|
}
|
|
23043
23308
|
async function connectStdio(id, entry, baseEnv) {
|
|
23044
23309
|
logInfo(`Connecting to "${id}" (stdio): ${entry.command} ${(entry.args || []).join(" ")}`);
|
|
23045
|
-
const
|
|
23046
|
-
|
|
23047
|
-
|
|
23048
|
-
|
|
23049
|
-
|
|
23050
|
-
|
|
23051
|
-
|
|
23052
|
-
|
|
23053
|
-
|
|
23054
|
-
if (
|
|
23055
|
-
|
|
23310
|
+
const wrapper = new ResilientTransportWrapper({
|
|
23311
|
+
name: id,
|
|
23312
|
+
connectParams: async () => {
|
|
23313
|
+
const t = new CustomStdioClientTransport({
|
|
23314
|
+
command: entry.command,
|
|
23315
|
+
args: entry.args || [],
|
|
23316
|
+
env: { ...baseEnv, ...entry.env || {} },
|
|
23317
|
+
stderr: "pipe"
|
|
23318
|
+
});
|
|
23319
|
+
if (t.stderr) {
|
|
23320
|
+
t.stderr.on("data", (chunk) => {
|
|
23321
|
+
const text = chunk.toString().trim();
|
|
23322
|
+
if (text) {
|
|
23323
|
+
process.stderr.write(`[child:${id}] ${text}
|
|
23056
23324
|
`);
|
|
23325
|
+
}
|
|
23326
|
+
});
|
|
23057
23327
|
}
|
|
23058
|
-
|
|
23059
|
-
|
|
23328
|
+
return t;
|
|
23329
|
+
},
|
|
23330
|
+
pingIntervalMs: entry.pingIntervalMs,
|
|
23331
|
+
pingTimeoutMs: entry.pingTimeoutMs
|
|
23332
|
+
});
|
|
23060
23333
|
const client = new Client({ name: `proxy-${id}`, version: "1.0.0" });
|
|
23061
|
-
|
|
23334
|
+
try {
|
|
23335
|
+
await withTimeout(
|
|
23336
|
+
(async () => {
|
|
23337
|
+
await wrapper.start();
|
|
23338
|
+
await client.connect(wrapper);
|
|
23339
|
+
wrapper.enableHeartbeat();
|
|
23340
|
+
})(),
|
|
23341
|
+
5e3,
|
|
23342
|
+
`Connection initialization timed out after 5s`
|
|
23343
|
+
);
|
|
23344
|
+
} catch (err) {
|
|
23345
|
+
try {
|
|
23346
|
+
await wrapper.close();
|
|
23347
|
+
} catch {
|
|
23348
|
+
}
|
|
23349
|
+
throw err;
|
|
23350
|
+
}
|
|
23062
23351
|
return {
|
|
23063
23352
|
client,
|
|
23353
|
+
transport: wrapper,
|
|
23064
23354
|
cleanup: async () => {
|
|
23065
23355
|
try {
|
|
23066
|
-
await
|
|
23356
|
+
await wrapper.close();
|
|
23067
23357
|
} catch {
|
|
23068
23358
|
}
|
|
23069
23359
|
}
|
|
@@ -23073,17 +23363,41 @@ async function connectStreamable(id, entry) {
|
|
|
23073
23363
|
logInfo(`Connecting to "${id}" (streamable-http): ${entry.url}`);
|
|
23074
23364
|
const headers = buildRequestHeaders(entry);
|
|
23075
23365
|
const url2 = new URL(entry.url);
|
|
23076
|
-
const
|
|
23077
|
-
|
|
23078
|
-
|
|
23079
|
-
|
|
23366
|
+
const wrapper = new ResilientTransportWrapper({
|
|
23367
|
+
name: id,
|
|
23368
|
+
connectParams: async () => {
|
|
23369
|
+
return new StreamableHTTPClientTransport(
|
|
23370
|
+
url2,
|
|
23371
|
+
headers ? { requestInit: { headers } } : void 0
|
|
23372
|
+
);
|
|
23373
|
+
},
|
|
23374
|
+
pingIntervalMs: entry.pingIntervalMs,
|
|
23375
|
+
pingTimeoutMs: entry.pingTimeoutMs
|
|
23376
|
+
});
|
|
23080
23377
|
const client = new Client({ name: `proxy-${id}`, version: "1.0.0" });
|
|
23081
|
-
|
|
23378
|
+
try {
|
|
23379
|
+
await withTimeout(
|
|
23380
|
+
(async () => {
|
|
23381
|
+
await wrapper.start();
|
|
23382
|
+
await client.connect(wrapper);
|
|
23383
|
+
wrapper.enableHeartbeat();
|
|
23384
|
+
})(),
|
|
23385
|
+
5e3,
|
|
23386
|
+
`Connection initialization timed out after 5s`
|
|
23387
|
+
);
|
|
23388
|
+
} catch (err) {
|
|
23389
|
+
try {
|
|
23390
|
+
await wrapper.close();
|
|
23391
|
+
} catch {
|
|
23392
|
+
}
|
|
23393
|
+
throw err;
|
|
23394
|
+
}
|
|
23082
23395
|
return {
|
|
23083
23396
|
client,
|
|
23397
|
+
transport: wrapper,
|
|
23084
23398
|
cleanup: async () => {
|
|
23085
23399
|
try {
|
|
23086
|
-
await
|
|
23400
|
+
await wrapper.close();
|
|
23087
23401
|
} catch {
|
|
23088
23402
|
}
|
|
23089
23403
|
}
|
|
@@ -23093,17 +23407,41 @@ async function connectSse(id, entry) {
|
|
|
23093
23407
|
logInfo(`Connecting to "${id}" (sse): ${entry.url}`);
|
|
23094
23408
|
const headers = buildRequestHeaders(entry);
|
|
23095
23409
|
const url2 = new URL(entry.url);
|
|
23096
|
-
const
|
|
23097
|
-
|
|
23098
|
-
|
|
23099
|
-
|
|
23410
|
+
const wrapper = new ResilientTransportWrapper({
|
|
23411
|
+
name: id,
|
|
23412
|
+
connectParams: async () => {
|
|
23413
|
+
return new SSEClientTransport(
|
|
23414
|
+
url2,
|
|
23415
|
+
headers ? { requestInit: { headers } } : void 0
|
|
23416
|
+
);
|
|
23417
|
+
},
|
|
23418
|
+
pingIntervalMs: entry.pingIntervalMs,
|
|
23419
|
+
pingTimeoutMs: entry.pingTimeoutMs
|
|
23420
|
+
});
|
|
23100
23421
|
const client = new Client({ name: `proxy-${id}`, version: "1.0.0" });
|
|
23101
|
-
|
|
23422
|
+
try {
|
|
23423
|
+
await withTimeout(
|
|
23424
|
+
(async () => {
|
|
23425
|
+
await wrapper.start();
|
|
23426
|
+
await client.connect(wrapper);
|
|
23427
|
+
wrapper.enableHeartbeat();
|
|
23428
|
+
})(),
|
|
23429
|
+
5e3,
|
|
23430
|
+
`Connection initialization timed out after 5s`
|
|
23431
|
+
);
|
|
23432
|
+
} catch (err) {
|
|
23433
|
+
try {
|
|
23434
|
+
await wrapper.close();
|
|
23435
|
+
} catch {
|
|
23436
|
+
}
|
|
23437
|
+
throw err;
|
|
23438
|
+
}
|
|
23102
23439
|
return {
|
|
23103
23440
|
client,
|
|
23441
|
+
transport: wrapper,
|
|
23104
23442
|
cleanup: async () => {
|
|
23105
23443
|
try {
|
|
23106
|
-
await
|
|
23444
|
+
await wrapper.close();
|
|
23107
23445
|
} catch {
|
|
23108
23446
|
}
|
|
23109
23447
|
}
|
|
@@ -23767,7 +24105,7 @@ var StdioServerTransport = class {
|
|
|
23767
24105
|
|
|
23768
24106
|
// src/constants.ts
|
|
23769
24107
|
var PKG_NAME = "nuwax-mcp-stdio-proxy";
|
|
23770
|
-
var PKG_VERSION = "1.4.
|
|
24108
|
+
var PKG_VERSION = "1.4.7";
|
|
23771
24109
|
|
|
23772
24110
|
// src/shared.ts
|
|
23773
24111
|
async function discoverTools(client) {
|
|
@@ -24074,11 +24412,11 @@ async function runConvert(args) {
|
|
|
24074
24412
|
const entryId = "remote";
|
|
24075
24413
|
let connected;
|
|
24076
24414
|
if (protocol === "sse") {
|
|
24077
|
-
const sseEntry = { url: targetUrl, transport: "sse" };
|
|
24415
|
+
const sseEntry = { url: targetUrl, transport: "sse", pingIntervalMs: args.pingIntervalMs, pingTimeoutMs: args.pingTimeoutMs };
|
|
24078
24416
|
if (targetHeaders) sseEntry.headers = targetHeaders;
|
|
24079
24417
|
connected = await connectSse(entryId, sseEntry);
|
|
24080
24418
|
} else {
|
|
24081
|
-
const streamEntry = { url: targetUrl };
|
|
24419
|
+
const streamEntry = { url: targetUrl, pingIntervalMs: args.pingIntervalMs, pingTimeoutMs: args.pingTimeoutMs };
|
|
24082
24420
|
if (targetHeaders) streamEntry.headers = targetHeaders;
|
|
24083
24421
|
connected = await connectStreamable(entryId, streamEntry);
|
|
24084
24422
|
}
|
|
@@ -25354,7 +25692,7 @@ var StreamableHTTPServerTransport = class {
|
|
|
25354
25692
|
};
|
|
25355
25693
|
|
|
25356
25694
|
// src/bridge.ts
|
|
25357
|
-
var LOG_TAG = "[PersistentMcpBridge]";
|
|
25695
|
+
var LOG_TAG = "[McpProxy] [PersistentMcpBridge]";
|
|
25358
25696
|
var BASE_RESTART_COOLDOWN_MS = 5e3;
|
|
25359
25697
|
var MAX_RESTART_ATTEMPTS = 5;
|
|
25360
25698
|
var MAX_BODY_SIZE = 10 * 1024 * 1024;
|
|
@@ -25473,36 +25811,46 @@ var PersistentMcpBridge = class {
|
|
|
25473
25811
|
async spawnAndConnect(id, entry) {
|
|
25474
25812
|
try {
|
|
25475
25813
|
this.log.info(`${LOG_TAG} Spawning server "${id}": ${entry.config.command} ${(entry.config.args || []).join(" ")}`);
|
|
25476
|
-
const
|
|
25477
|
-
|
|
25478
|
-
|
|
25479
|
-
|
|
25480
|
-
|
|
25814
|
+
const wrapper = new ResilientTransportWrapper({
|
|
25815
|
+
name: id,
|
|
25816
|
+
logger: this.log,
|
|
25817
|
+
connectParams: async () => {
|
|
25818
|
+
const t = new CustomStdioClientTransport({
|
|
25819
|
+
command: entry.config.command,
|
|
25820
|
+
args: entry.config.args || [],
|
|
25821
|
+
env: entry.config.env,
|
|
25822
|
+
stderr: "pipe"
|
|
25823
|
+
});
|
|
25824
|
+
if (t.stderr) {
|
|
25825
|
+
t.stderr.on("data", (chunk) => {
|
|
25826
|
+
const text = chunk.toString().trim();
|
|
25827
|
+
if (text) this.log.info(`${LOG_TAG} [${id}:stderr] ${text}`);
|
|
25828
|
+
});
|
|
25829
|
+
}
|
|
25830
|
+
return t;
|
|
25831
|
+
},
|
|
25832
|
+
pingIntervalMs: entry.config.pingIntervalMs,
|
|
25833
|
+
pingTimeoutMs: entry.config.pingTimeoutMs
|
|
25481
25834
|
});
|
|
25482
25835
|
const client = new Client(
|
|
25483
25836
|
{ name: "nuwax-persistent-bridge", version: "1.0.0" },
|
|
25484
25837
|
{ capabilities: {} }
|
|
25485
25838
|
);
|
|
25486
|
-
|
|
25839
|
+
wrapper.onclose = () => {
|
|
25487
25840
|
this.log.warn(`${LOG_TAG} Server "${id}" transport closed`);
|
|
25488
25841
|
entry.healthy = false;
|
|
25489
25842
|
if (this.running && !entry.restarting) {
|
|
25490
25843
|
this.scheduleRestart(id, entry);
|
|
25491
25844
|
}
|
|
25492
25845
|
};
|
|
25493
|
-
|
|
25846
|
+
wrapper.onerror = (err) => {
|
|
25494
25847
|
this.log.error(`${LOG_TAG} Server "${id}" transport error:`, err.message);
|
|
25495
25848
|
};
|
|
25496
|
-
await
|
|
25849
|
+
await wrapper.start();
|
|
25850
|
+
await client.connect(wrapper);
|
|
25851
|
+
wrapper.enableHeartbeat();
|
|
25497
25852
|
entry.client = client;
|
|
25498
|
-
entry.transport =
|
|
25499
|
-
const stderrStream = transport.stderr;
|
|
25500
|
-
if (stderrStream) {
|
|
25501
|
-
stderrStream.on("data", (chunk) => {
|
|
25502
|
-
const text = chunk.toString().trim();
|
|
25503
|
-
if (text) this.log.info(`${LOG_TAG} [${id}:stderr] ${text}`);
|
|
25504
|
-
});
|
|
25505
|
-
}
|
|
25853
|
+
entry.transport = wrapper;
|
|
25506
25854
|
const result = await client.listTools();
|
|
25507
25855
|
entry.tools = result.tools;
|
|
25508
25856
|
entry.healthy = true;
|
|
@@ -25840,6 +26188,8 @@ function parseConvertArgs(args) {
|
|
|
25840
26188
|
let protocol;
|
|
25841
26189
|
let allowTools;
|
|
25842
26190
|
let denyTools;
|
|
26191
|
+
let pingIntervalMs;
|
|
26192
|
+
let pingTimeoutMs;
|
|
25843
26193
|
for (let i = 0; i < args.length; i++) {
|
|
25844
26194
|
const arg = args[i];
|
|
25845
26195
|
if (arg === "--config" && i + 1 < args.length) {
|
|
@@ -25862,6 +26212,20 @@ function parseConvertArgs(args) {
|
|
|
25862
26212
|
} else if (arg === "--deny-tools" && i + 1 < args.length) {
|
|
25863
26213
|
i++;
|
|
25864
26214
|
denyTools = args[i].split(",").map((s) => s.trim()).filter(Boolean);
|
|
26215
|
+
} else if (arg === "--ping-interval" && i + 1 < args.length) {
|
|
26216
|
+
i++;
|
|
26217
|
+
pingIntervalMs = parseInt(args[i], 10);
|
|
26218
|
+
if (isNaN(pingIntervalMs)) {
|
|
26219
|
+
logError(`Invalid ping interval: "${args[i]}"`);
|
|
26220
|
+
process.exit(1);
|
|
26221
|
+
}
|
|
26222
|
+
} else if (arg === "--ping-timeout" && i + 1 < args.length) {
|
|
26223
|
+
i++;
|
|
26224
|
+
pingTimeoutMs = parseInt(args[i], 10);
|
|
26225
|
+
if (isNaN(pingTimeoutMs)) {
|
|
26226
|
+
logError(`Invalid ping timeout: "${args[i]}"`);
|
|
26227
|
+
process.exit(1);
|
|
26228
|
+
}
|
|
25865
26229
|
} else if (!arg.startsWith("-") && !url2) {
|
|
25866
26230
|
url2 = arg;
|
|
25867
26231
|
} else {
|
|
@@ -25879,7 +26243,7 @@ function parseConvertArgs(args) {
|
|
|
25879
26243
|
logError("Cannot use both --allow-tools and --deny-tools");
|
|
25880
26244
|
process.exit(1);
|
|
25881
26245
|
}
|
|
25882
|
-
return { mode: "convert", url: url2, config: config2, name, protocol, allowTools, denyTools };
|
|
26246
|
+
return { mode: "convert", url: url2, config: config2, name, protocol, allowTools, denyTools, pingIntervalMs, pingTimeoutMs };
|
|
25883
26247
|
}
|
|
25884
26248
|
function parseProxyArgs(args) {
|
|
25885
26249
|
let port;
|
|
@@ -25972,6 +26336,8 @@ function printConvertUsage() {
|
|
|
25972
26336
|
logError(" --protocol <sse|stream> Protocol type (auto-detect if omitted)");
|
|
25973
26337
|
logError(" --allow-tools <TOOLS> Tool whitelist (comma-separated)");
|
|
25974
26338
|
logError(" --deny-tools <TOOLS> Tool blacklist (comma-separated)");
|
|
26339
|
+
logError(" --ping-interval <MS> Heartbeat ping interval (default: 20000)");
|
|
26340
|
+
logError(" --ping-timeout <MS> Heartbeat ping timeout (default: 5000)");
|
|
25975
26341
|
}
|
|
25976
26342
|
function printProxyUsage() {
|
|
25977
26343
|
logError(`Usage: nuwax-mcp-stdio-proxy proxy --port <PORT> --config '{"mcpServers":{...}}'`);
|
package/dist/modes/convert.d.ts
CHANGED
package/dist/modes/convert.js
CHANGED
|
@@ -67,13 +67,13 @@ export async function runConvert(args) {
|
|
|
67
67
|
const entryId = 'remote';
|
|
68
68
|
let connected;
|
|
69
69
|
if (protocol === 'sse') {
|
|
70
|
-
const sseEntry = { url: targetUrl, transport: 'sse' };
|
|
70
|
+
const sseEntry = { url: targetUrl, transport: 'sse', pingIntervalMs: args.pingIntervalMs, pingTimeoutMs: args.pingTimeoutMs };
|
|
71
71
|
if (targetHeaders)
|
|
72
72
|
sseEntry.headers = targetHeaders;
|
|
73
73
|
connected = await connectSse(entryId, sseEntry);
|
|
74
74
|
}
|
|
75
75
|
else {
|
|
76
|
-
const streamEntry = { url: targetUrl };
|
|
76
|
+
const streamEntry = { url: targetUrl, pingIntervalMs: args.pingIntervalMs, pingTimeoutMs: args.pingTimeoutMs };
|
|
77
77
|
if (targetHeaders)
|
|
78
78
|
streamEntry.headers = targetHeaders;
|
|
79
79
|
connected = await connectStreamable(entryId, streamEntry);
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resilient Transport Wrapper for MCP
|
|
3
|
+
*
|
|
4
|
+
* Provides heartbeat monitoring, automatic reconnection, and request queueing
|
|
5
|
+
* for MCP transports (HTTP, SSE, Stdio).
|
|
6
|
+
*/
|
|
7
|
+
import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
|
|
8
|
+
import { JSONRPCMessage } from '@modelcontextprotocol/sdk/types.js';
|
|
9
|
+
/** Logger interface — compatible with console, electron-log, and BridgeLogger */
|
|
10
|
+
export interface ResilientLogger {
|
|
11
|
+
info: (...args: unknown[]) => void;
|
|
12
|
+
warn: (...args: unknown[]) => void;
|
|
13
|
+
error: (...args: unknown[]) => void;
|
|
14
|
+
}
|
|
15
|
+
export interface ResilientTransportOptions {
|
|
16
|
+
/** Connection factory function */
|
|
17
|
+
connectParams: () => Promise<Transport>;
|
|
18
|
+
/** Heartbeat check interval (ms). Default: 20000 */
|
|
19
|
+
pingIntervalMs?: number;
|
|
20
|
+
/** Max consecutive failures before reconnecting. Default: 3 */
|
|
21
|
+
maxConsecutiveFailures?: number;
|
|
22
|
+
/** Timeout for checking ping or listTools (ms). Default: 5000 */
|
|
23
|
+
pingTimeoutMs?: number;
|
|
24
|
+
/** Backoff delay before reconnect attempt (ms). Default: 3000 */
|
|
25
|
+
reconnectDelayMs?: number;
|
|
26
|
+
/** Max queued requests during reconnect. Default: 100 */
|
|
27
|
+
maxQueueSize?: number;
|
|
28
|
+
/** Server name/ID for logging */
|
|
29
|
+
name?: string;
|
|
30
|
+
/**
|
|
31
|
+
* Optional custom logger. When running inside the Electron main process
|
|
32
|
+
* (e.g. PersistentMcpBridge), pass electron-log so heartbeat/reconnect
|
|
33
|
+
* logs appear in main.log alongside other application logs.
|
|
34
|
+
* Defaults to the built-in stderr logger (logger.js).
|
|
35
|
+
*/
|
|
36
|
+
logger?: ResilientLogger;
|
|
37
|
+
}
|
|
38
|
+
export declare class ResilientTransportWrapper implements Transport {
|
|
39
|
+
private options;
|
|
40
|
+
private log;
|
|
41
|
+
private activeTransport;
|
|
42
|
+
private mcpClient;
|
|
43
|
+
private heartbeatTimer;
|
|
44
|
+
private consecutiveFailures;
|
|
45
|
+
private state;
|
|
46
|
+
onclose?: () => void;
|
|
47
|
+
onerror?: (error: Error) => void;
|
|
48
|
+
onmessage?: (message: JSONRPCMessage) => void;
|
|
49
|
+
private messageQueue;
|
|
50
|
+
private pendingPings;
|
|
51
|
+
constructor(options: ResilientTransportOptions);
|
|
52
|
+
/**
|
|
53
|
+
* Initializes the transport and connects to the backend
|
|
54
|
+
*
|
|
55
|
+
* This method is idempotent - if the transport is already connected or
|
|
56
|
+
* connecting, subsequent calls will return immediately without creating
|
|
57
|
+
* duplicate connections. This is important because MCP SDK's client.connect()
|
|
58
|
+
* internally calls transport.start(), and we also call it explicitly in bridge.ts.
|
|
59
|
+
*/
|
|
60
|
+
start(): Promise<void>;
|
|
61
|
+
/**
|
|
62
|
+
* Enable heartbeat monitoring. Call this AFTER the MCP client has
|
|
63
|
+
* completed its initialize handshake (client.connect()), otherwise
|
|
64
|
+
* the server will reject ping requests with "Server not initialized".
|
|
65
|
+
*/
|
|
66
|
+
enableHeartbeat(): void;
|
|
67
|
+
private performConnect;
|
|
68
|
+
private bindInnerTransport;
|
|
69
|
+
private startHeartbeat;
|
|
70
|
+
private stopHeartbeat;
|
|
71
|
+
/** Track consecutive ping timeouts (no response at all, not even an error) */
|
|
72
|
+
private consecutivePingTimeouts;
|
|
73
|
+
/** If server doesn't support ping, auto-disable heartbeat after maxConsecutiveFailures timeouts */
|
|
74
|
+
private pingDisabled;
|
|
75
|
+
private checkHealth;
|
|
76
|
+
private triggerReconnect;
|
|
77
|
+
private flushQueue;
|
|
78
|
+
send(message: JSONRPCMessage): Promise<void>;
|
|
79
|
+
close(): Promise<void>;
|
|
80
|
+
}
|