nuwax-mcp-stdio-proxy 1.4.8 → 1.4.9
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 +143 -40
- package/dist/logger.d.ts +7 -1
- package/dist/logger.js +97 -2
- package/dist/resilient.d.ts +18 -1
- package/dist/resilient.js +48 -17
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -3222,8 +3222,8 @@ var require_utils = __commonJS({
|
|
|
3222
3222
|
}
|
|
3223
3223
|
return ind;
|
|
3224
3224
|
}
|
|
3225
|
-
function removeDotSegments(
|
|
3226
|
-
let input =
|
|
3225
|
+
function removeDotSegments(path3) {
|
|
3226
|
+
let input = path3;
|
|
3227
3227
|
const output = [];
|
|
3228
3228
|
let nextSlash = -1;
|
|
3229
3229
|
let len = 0;
|
|
@@ -3422,8 +3422,8 @@ var require_schemes = __commonJS({
|
|
|
3422
3422
|
wsComponent.secure = void 0;
|
|
3423
3423
|
}
|
|
3424
3424
|
if (wsComponent.resourceName) {
|
|
3425
|
-
const [
|
|
3426
|
-
wsComponent.path =
|
|
3425
|
+
const [path3, query] = wsComponent.resourceName.split("?");
|
|
3426
|
+
wsComponent.path = path3 && path3 !== "/" ? path3 : void 0;
|
|
3427
3427
|
wsComponent.query = query;
|
|
3428
3428
|
wsComponent.resourceName = void 0;
|
|
3429
3429
|
}
|
|
@@ -6785,12 +6785,12 @@ var require_dist = __commonJS({
|
|
|
6785
6785
|
throw new Error(`Unknown format "${name}"`);
|
|
6786
6786
|
return f;
|
|
6787
6787
|
};
|
|
6788
|
-
function addFormats(ajv, list,
|
|
6788
|
+
function addFormats(ajv, list, fs4, exportName) {
|
|
6789
6789
|
var _a2;
|
|
6790
6790
|
var _b;
|
|
6791
6791
|
(_a2 = (_b = ajv.opts.code).formats) !== null && _a2 !== void 0 ? _a2 : _b.formats = (0, codegen_1._)`require("ajv-formats/dist/formats").${exportName}`;
|
|
6792
6792
|
for (const f of list)
|
|
6793
|
-
ajv.addFormat(f,
|
|
6793
|
+
ajv.addFormat(f, fs4[f]);
|
|
6794
6794
|
}
|
|
6795
6795
|
module.exports = exports = formatsPlugin;
|
|
6796
6796
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -6799,12 +6799,89 @@ var require_dist = __commonJS({
|
|
|
6799
6799
|
});
|
|
6800
6800
|
|
|
6801
6801
|
// src/index.ts
|
|
6802
|
-
import * as
|
|
6802
|
+
import * as fs3 from "fs";
|
|
6803
6803
|
|
|
6804
6804
|
// src/logger.ts
|
|
6805
|
+
import * as fs from "fs";
|
|
6806
|
+
import * as path from "path";
|
|
6807
|
+
var logFilePath = process.env.MCP_PROXY_LOG_FILE;
|
|
6808
|
+
var MAX_LOG_FILES = 7;
|
|
6809
|
+
var logStream = null;
|
|
6810
|
+
var logDir = "";
|
|
6811
|
+
var logBaseName = "";
|
|
6812
|
+
var logExt = "";
|
|
6813
|
+
var currentDateStr = "";
|
|
6814
|
+
function dateStr() {
|
|
6815
|
+
const d = /* @__PURE__ */ new Date();
|
|
6816
|
+
const pad2 = (n) => String(n).padStart(2, "0");
|
|
6817
|
+
return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
|
|
6818
|
+
}
|
|
6819
|
+
function openLogFile() {
|
|
6820
|
+
if (!logFilePath) return;
|
|
6821
|
+
const today = dateStr();
|
|
6822
|
+
if (today === currentDateStr && logStream) return;
|
|
6823
|
+
if (logStream) {
|
|
6824
|
+
try {
|
|
6825
|
+
logStream.end();
|
|
6826
|
+
} catch {
|
|
6827
|
+
}
|
|
6828
|
+
}
|
|
6829
|
+
currentDateStr = today;
|
|
6830
|
+
const dated = path.join(logDir, `${logBaseName}-${today}${logExt}`);
|
|
6831
|
+
try {
|
|
6832
|
+
logStream = fs.createWriteStream(dated, { flags: "a" });
|
|
6833
|
+
} catch {
|
|
6834
|
+
logStream = null;
|
|
6835
|
+
}
|
|
6836
|
+
cleanupOldLogs();
|
|
6837
|
+
}
|
|
6838
|
+
function cleanupOldLogs() {
|
|
6839
|
+
if (!logDir || !logBaseName) return;
|
|
6840
|
+
try {
|
|
6841
|
+
const prefix = `${logBaseName}-`;
|
|
6842
|
+
const files = fs.readdirSync(logDir).filter((f) => f.startsWith(prefix) && f.endsWith(logExt)).sort().reverse();
|
|
6843
|
+
for (let i = MAX_LOG_FILES; i < files.length; i++) {
|
|
6844
|
+
try {
|
|
6845
|
+
fs.unlinkSync(path.join(logDir, files[i]));
|
|
6846
|
+
} catch {
|
|
6847
|
+
}
|
|
6848
|
+
}
|
|
6849
|
+
} catch {
|
|
6850
|
+
}
|
|
6851
|
+
}
|
|
6852
|
+
if (logFilePath) {
|
|
6853
|
+
try {
|
|
6854
|
+
logDir = path.dirname(logFilePath);
|
|
6855
|
+
if (logDir && !fs.existsSync(logDir)) {
|
|
6856
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
6857
|
+
}
|
|
6858
|
+
const fullName = path.basename(logFilePath);
|
|
6859
|
+
const dotIdx = fullName.lastIndexOf(".");
|
|
6860
|
+
if (dotIdx > 0) {
|
|
6861
|
+
logBaseName = fullName.substring(0, dotIdx);
|
|
6862
|
+
logExt = fullName.substring(dotIdx);
|
|
6863
|
+
} else {
|
|
6864
|
+
logBaseName = fullName;
|
|
6865
|
+
logExt = ".log";
|
|
6866
|
+
}
|
|
6867
|
+
openLogFile();
|
|
6868
|
+
} catch {
|
|
6869
|
+
}
|
|
6870
|
+
}
|
|
6871
|
+
function timestamp() {
|
|
6872
|
+
const d = /* @__PURE__ */ new Date();
|
|
6873
|
+
const pad2 = (n) => String(n).padStart(2, "0");
|
|
6874
|
+
const pad3 = (n) => String(n).padStart(3, "0");
|
|
6875
|
+
return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}.${pad3(d.getMilliseconds())}`;
|
|
6876
|
+
}
|
|
6805
6877
|
function log(level, msg) {
|
|
6806
|
-
|
|
6807
|
-
|
|
6878
|
+
const line = `[${timestamp()}] [${level.toLowerCase()}] [nuwax-mcp-proxy] ${msg}
|
|
6879
|
+
`;
|
|
6880
|
+
process.stderr.write(line);
|
|
6881
|
+
if (logFilePath) {
|
|
6882
|
+
openLogFile();
|
|
6883
|
+
logStream?.write(line);
|
|
6884
|
+
}
|
|
6808
6885
|
}
|
|
6809
6886
|
var logInfo = (msg) => log("INFO", msg);
|
|
6810
6887
|
var logWarn = (msg) => log("WARN", msg);
|
|
@@ -7180,8 +7257,8 @@ function getErrorMap() {
|
|
|
7180
7257
|
|
|
7181
7258
|
// node_modules/zod/v3/helpers/parseUtil.js
|
|
7182
7259
|
var makeIssue = (params) => {
|
|
7183
|
-
const { data, path:
|
|
7184
|
-
const fullPath = [...
|
|
7260
|
+
const { data, path: path3, errorMaps, issueData } = params;
|
|
7261
|
+
const fullPath = [...path3, ...issueData.path || []];
|
|
7185
7262
|
const fullIssue = {
|
|
7186
7263
|
...issueData,
|
|
7187
7264
|
path: fullPath
|
|
@@ -7296,11 +7373,11 @@ var errorUtil;
|
|
|
7296
7373
|
|
|
7297
7374
|
// node_modules/zod/v3/types.js
|
|
7298
7375
|
var ParseInputLazyPath = class {
|
|
7299
|
-
constructor(parent, value,
|
|
7376
|
+
constructor(parent, value, path3, key) {
|
|
7300
7377
|
this._cachedPath = [];
|
|
7301
7378
|
this.parent = parent;
|
|
7302
7379
|
this.data = value;
|
|
7303
|
-
this._path =
|
|
7380
|
+
this._path = path3;
|
|
7304
7381
|
this._key = key;
|
|
7305
7382
|
}
|
|
7306
7383
|
get path() {
|
|
@@ -10944,10 +11021,10 @@ function mergeDefs(...defs) {
|
|
|
10944
11021
|
function cloneDef(schema) {
|
|
10945
11022
|
return mergeDefs(schema._zod.def);
|
|
10946
11023
|
}
|
|
10947
|
-
function getElementAtPath(obj,
|
|
10948
|
-
if (!
|
|
11024
|
+
function getElementAtPath(obj, path3) {
|
|
11025
|
+
if (!path3)
|
|
10949
11026
|
return obj;
|
|
10950
|
-
return
|
|
11027
|
+
return path3.reduce((acc, key) => acc?.[key], obj);
|
|
10951
11028
|
}
|
|
10952
11029
|
function promiseAllObject(promisesObj) {
|
|
10953
11030
|
const keys = Object.keys(promisesObj);
|
|
@@ -11330,11 +11407,11 @@ function aborted(x, startIndex = 0) {
|
|
|
11330
11407
|
}
|
|
11331
11408
|
return false;
|
|
11332
11409
|
}
|
|
11333
|
-
function prefixIssues(
|
|
11410
|
+
function prefixIssues(path3, issues) {
|
|
11334
11411
|
return issues.map((iss) => {
|
|
11335
11412
|
var _a2;
|
|
11336
11413
|
(_a2 = iss).path ?? (_a2.path = []);
|
|
11337
|
-
iss.path.unshift(
|
|
11414
|
+
iss.path.unshift(path3);
|
|
11338
11415
|
return iss;
|
|
11339
11416
|
});
|
|
11340
11417
|
}
|
|
@@ -22810,8 +22887,8 @@ function serializeMessage(message) {
|
|
|
22810
22887
|
}
|
|
22811
22888
|
|
|
22812
22889
|
// src/customStdio.ts
|
|
22813
|
-
import * as
|
|
22814
|
-
import * as
|
|
22890
|
+
import * as fs2 from "fs";
|
|
22891
|
+
import * as path2 from "path";
|
|
22815
22892
|
function logDebug(msg) {
|
|
22816
22893
|
process.stderr.write(`[customStdio] ${msg}
|
|
22817
22894
|
`);
|
|
@@ -22848,8 +22925,8 @@ var CustomStdioClientTransport = class {
|
|
|
22848
22925
|
const pathDirs = (mergedEnv.PATH || "").split(";");
|
|
22849
22926
|
for (const dir of pathDirs) {
|
|
22850
22927
|
for (const ext of cmdExtensions) {
|
|
22851
|
-
const fullPath =
|
|
22852
|
-
if (
|
|
22928
|
+
const fullPath = path2.join(dir, command + ext);
|
|
22929
|
+
if (fs2.existsSync(fullPath)) {
|
|
22853
22930
|
command = fullPath;
|
|
22854
22931
|
logDebug(`Resolved "${this._serverParams.command}" to "${command}"`);
|
|
22855
22932
|
break;
|
|
@@ -23024,7 +23101,8 @@ var DEFAULT_OPTIONS = {
|
|
|
23024
23101
|
pingIntervalMs: 2e4,
|
|
23025
23102
|
maxConsecutiveFailures: 3,
|
|
23026
23103
|
pingTimeoutMs: 5e3,
|
|
23027
|
-
reconnectDelayMs:
|
|
23104
|
+
reconnectDelayMs: 1e3,
|
|
23105
|
+
maxReconnectDelayMs: 6e4,
|
|
23028
23106
|
maxQueueSize: 100,
|
|
23029
23107
|
name: "remote"
|
|
23030
23108
|
};
|
|
@@ -23041,6 +23119,8 @@ var ResilientTransportWrapper = class {
|
|
|
23041
23119
|
heartbeatTimer = null;
|
|
23042
23120
|
consecutiveFailures = 0;
|
|
23043
23121
|
state = "idle";
|
|
23122
|
+
/** Current retry attempt count (reset on successful connect) */
|
|
23123
|
+
retryAttempt = 0;
|
|
23044
23124
|
// Handlers required by the Transport interface
|
|
23045
23125
|
onclose;
|
|
23046
23126
|
onerror;
|
|
@@ -23060,9 +23140,18 @@ var ResilientTransportWrapper = class {
|
|
|
23060
23140
|
...options.pingTimeoutMs !== void 0 && { pingTimeoutMs: options.pingTimeoutMs },
|
|
23061
23141
|
...options.maxConsecutiveFailures !== void 0 && { maxConsecutiveFailures: options.maxConsecutiveFailures },
|
|
23062
23142
|
...options.reconnectDelayMs !== void 0 && { reconnectDelayMs: options.reconnectDelayMs },
|
|
23143
|
+
...options.maxReconnectDelayMs !== void 0 && { maxReconnectDelayMs: options.maxReconnectDelayMs },
|
|
23063
23144
|
...options.maxQueueSize !== void 0 && { maxQueueSize: options.maxQueueSize }
|
|
23064
23145
|
};
|
|
23065
23146
|
}
|
|
23147
|
+
/**
|
|
23148
|
+
* Calculate backoff delay using capped exponential backoff.
|
|
23149
|
+
* 1s → 2s → 4s → 8s → 16s → 32s → 60s (capped)
|
|
23150
|
+
*/
|
|
23151
|
+
getBackoffDelay() {
|
|
23152
|
+
const delay = this.options.reconnectDelayMs * Math.pow(2, this.retryAttempt);
|
|
23153
|
+
return Math.min(delay, this.options.maxReconnectDelayMs);
|
|
23154
|
+
}
|
|
23066
23155
|
/**
|
|
23067
23156
|
* Initializes the transport and connects to the backend
|
|
23068
23157
|
*
|
|
@@ -23072,7 +23161,7 @@ var ResilientTransportWrapper = class {
|
|
|
23072
23161
|
* internally calls transport.start(), and we also call it explicitly in bridge.ts.
|
|
23073
23162
|
*/
|
|
23074
23163
|
async start() {
|
|
23075
|
-
if (this.state === "connected" || this.state === "connecting") {
|
|
23164
|
+
if (this.state === "connected" || this.state === "connecting" || this.state === "reconnecting") {
|
|
23076
23165
|
return;
|
|
23077
23166
|
}
|
|
23078
23167
|
this.state = "connecting";
|
|
@@ -23087,6 +23176,7 @@ var ResilientTransportWrapper = class {
|
|
|
23087
23176
|
this.startHeartbeat();
|
|
23088
23177
|
}
|
|
23089
23178
|
async performConnect(initial = false) {
|
|
23179
|
+
if (this.state === "closed") return;
|
|
23090
23180
|
try {
|
|
23091
23181
|
this.activeTransport = await this.options.connectParams();
|
|
23092
23182
|
this.bindInnerTransport(this.activeTransport);
|
|
@@ -23094,18 +23184,22 @@ var ResilientTransportWrapper = class {
|
|
|
23094
23184
|
this.state = "connected";
|
|
23095
23185
|
this.consecutiveFailures = 0;
|
|
23096
23186
|
this.consecutivePingTimeouts = 0;
|
|
23097
|
-
this.
|
|
23187
|
+
this.retryAttempt = 0;
|
|
23188
|
+
this.heartbeatOkCount = 0;
|
|
23189
|
+
this.log.info(`[McpProxy] [ResilientTransport:${this.options.name}] \u2705 Connected via ${this.activeTransport.constructor.name}`);
|
|
23098
23190
|
this.flushQueue();
|
|
23099
23191
|
if (!initial) {
|
|
23100
23192
|
this.startHeartbeat();
|
|
23101
23193
|
}
|
|
23102
23194
|
} catch (err) {
|
|
23103
|
-
this.
|
|
23104
|
-
|
|
23105
|
-
|
|
23106
|
-
|
|
23107
|
-
|
|
23108
|
-
|
|
23195
|
+
const delay = this.getBackoffDelay();
|
|
23196
|
+
this.retryAttempt++;
|
|
23197
|
+
this.log.error(`[McpProxy] [ResilientTransport:${this.options.name}] \u274C Connect failed (attempt ${this.retryAttempt}): ${err}`);
|
|
23198
|
+
this.log.info(`[McpProxy] [ResilientTransport:${this.options.name}] \u{1F504} Retrying in ${delay}ms...`);
|
|
23199
|
+
this.state = "reconnecting";
|
|
23200
|
+
setTimeout(() => {
|
|
23201
|
+
this.performConnect();
|
|
23202
|
+
}, delay);
|
|
23109
23203
|
}
|
|
23110
23204
|
}
|
|
23111
23205
|
bindInnerTransport(transport) {
|
|
@@ -23154,6 +23248,8 @@ var ResilientTransportWrapper = class {
|
|
|
23154
23248
|
}
|
|
23155
23249
|
/** Track consecutive ping timeouts (no response at all, not even an error) */
|
|
23156
23250
|
consecutivePingTimeouts = 0;
|
|
23251
|
+
/** Successful heartbeat counter (for reducing log volume) */
|
|
23252
|
+
heartbeatOkCount = 0;
|
|
23157
23253
|
async checkHealth() {
|
|
23158
23254
|
if (this.state !== "connected" || !this.activeTransport) return;
|
|
23159
23255
|
try {
|
|
@@ -23168,7 +23264,6 @@ var ResilientTransportWrapper = class {
|
|
|
23168
23264
|
}, this.options.pingTimeoutMs);
|
|
23169
23265
|
});
|
|
23170
23266
|
try {
|
|
23171
|
-
this.log.info(`[McpProxy] [ResilientTransport:${this.options.name}] \u{1F493} Sending heartbeat (id: ${healthId})`);
|
|
23172
23267
|
await this.activeTransport.send({
|
|
23173
23268
|
jsonrpc: "2.0",
|
|
23174
23269
|
id: healthId,
|
|
@@ -23181,28 +23276,33 @@ var ResilientTransportWrapper = class {
|
|
|
23181
23276
|
const success2 = await responsePromise;
|
|
23182
23277
|
if (!success2) {
|
|
23183
23278
|
this.consecutivePingTimeouts++;
|
|
23184
|
-
this.log.warn(`[McpProxy] [ResilientTransport:${this.options.name}] Heartbeat timeout (${this.consecutivePingTimeouts}/${this.options.maxConsecutiveFailures})`);
|
|
23279
|
+
this.log.warn(`[McpProxy] [ResilientTransport:${this.options.name}] \u23F1\uFE0F Heartbeat timeout (${this.consecutivePingTimeouts}/${this.options.maxConsecutiveFailures})`);
|
|
23185
23280
|
if (this.consecutivePingTimeouts >= this.options.maxConsecutiveFailures) {
|
|
23186
|
-
this.log.error(`[McpProxy] [ResilientTransport:${this.options.name}] Max consecutive heartbeat timeouts reached.
|
|
23281
|
+
this.log.error(`[McpProxy] [ResilientTransport:${this.options.name}] Max consecutive heartbeat timeouts reached. Closing and retrying...`);
|
|
23187
23282
|
this.triggerReconnect();
|
|
23188
23283
|
}
|
|
23189
23284
|
return;
|
|
23190
23285
|
}
|
|
23191
|
-
this.
|
|
23286
|
+
this.heartbeatOkCount++;
|
|
23287
|
+
if (this.heartbeatOkCount % 5 === 1 || this.consecutiveFailures > 0 || this.consecutivePingTimeouts > 0) {
|
|
23288
|
+
this.log.info(`[McpProxy] [ResilientTransport:${this.options.name}] \u{1F496} Heartbeat OK (count: ${this.heartbeatOkCount})`);
|
|
23289
|
+
}
|
|
23192
23290
|
this.consecutiveFailures = 0;
|
|
23193
23291
|
this.consecutivePingTimeouts = 0;
|
|
23194
23292
|
} catch (err) {
|
|
23195
23293
|
this.consecutiveFailures++;
|
|
23196
23294
|
this.log.warn(`[McpProxy] [ResilientTransport:${this.options.name}] \u{1F494} Heartbeat error (${this.consecutiveFailures}/${this.options.maxConsecutiveFailures}): ${err}`);
|
|
23197
23295
|
if (this.consecutiveFailures >= this.options.maxConsecutiveFailures) {
|
|
23198
|
-
this.log.error(`[McpProxy] [ResilientTransport:${this.options.name}] Max consecutive heartbeat failures reached.
|
|
23296
|
+
this.log.error(`[McpProxy] [ResilientTransport:${this.options.name}] Max consecutive heartbeat failures reached. Closing and retrying...`);
|
|
23199
23297
|
this.triggerReconnect();
|
|
23200
23298
|
}
|
|
23201
23299
|
}
|
|
23202
23300
|
}
|
|
23301
|
+
/**
|
|
23302
|
+
* Close the current transport and schedule a reconnect with exponential backoff.
|
|
23303
|
+
*/
|
|
23203
23304
|
triggerReconnect() {
|
|
23204
23305
|
if (this.state === "reconnecting" || this.state === "closed") return;
|
|
23205
|
-
this.log.warn(`[McpProxy] [ResilientTransport:${this.options.name}] \u{1F504} Triggering reconnect (previous state: ${this.state})`);
|
|
23206
23306
|
this.state = "reconnecting";
|
|
23207
23307
|
this.stopHeartbeat();
|
|
23208
23308
|
if (this.activeTransport) {
|
|
@@ -23214,9 +23314,12 @@ var ResilientTransportWrapper = class {
|
|
|
23214
23314
|
}
|
|
23215
23315
|
this.activeTransport = null;
|
|
23216
23316
|
}
|
|
23317
|
+
const delay = this.getBackoffDelay();
|
|
23318
|
+
this.retryAttempt++;
|
|
23319
|
+
this.log.warn(`[McpProxy] [ResilientTransport:${this.options.name}] \u{1F504} Closed. Retrying in ${delay}ms (attempt ${this.retryAttempt})...`);
|
|
23217
23320
|
setTimeout(() => {
|
|
23218
23321
|
this.performConnect();
|
|
23219
|
-
},
|
|
23322
|
+
}, delay);
|
|
23220
23323
|
}
|
|
23221
23324
|
async flushQueue() {
|
|
23222
23325
|
if (!this.activeTransport || this.state !== "connected") return;
|
|
@@ -24114,7 +24217,7 @@ var StdioServerTransport = class {
|
|
|
24114
24217
|
|
|
24115
24218
|
// src/constants.ts
|
|
24116
24219
|
var PKG_NAME = "nuwax-mcp-stdio-proxy";
|
|
24117
|
-
var PKG_VERSION = "1.4.
|
|
24220
|
+
var PKG_VERSION = "1.4.9";
|
|
24118
24221
|
|
|
24119
24222
|
// src/shared.ts
|
|
24120
24223
|
async function discoverTools(client) {
|
|
@@ -26324,7 +26427,7 @@ function parseConfigJson(json2) {
|
|
|
26324
26427
|
}
|
|
26325
26428
|
function parseConfigFile(filePath) {
|
|
26326
26429
|
try {
|
|
26327
|
-
const content =
|
|
26430
|
+
const content = fs3.readFileSync(filePath, "utf-8");
|
|
26328
26431
|
const config2 = JSON.parse(content);
|
|
26329
26432
|
if (!config2.mcpServers || typeof config2.mcpServers !== "object") {
|
|
26330
26433
|
throw new Error('config must contain a "mcpServers" object');
|
package/dist/logger.d.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Logging utilities — stderr only, stdout is the MCP JSON-RPC channel
|
|
2
|
+
* Logging utilities — stderr only, stdout is the MCP JSON-RPC channel.
|
|
3
|
+
*
|
|
4
|
+
* When the environment variable MCP_PROXY_LOG_FILE is set, log lines are
|
|
5
|
+
* also appended to that file so the Electron host can tail them into main.log.
|
|
6
|
+
*
|
|
7
|
+
* Log rotation: file is named by date (e.g. mcp-proxy-2026-03-09.log).
|
|
8
|
+
* A new file is created each day. Old files beyond MAX_LOG_FILES are deleted.
|
|
3
9
|
*/
|
|
4
10
|
export declare function log(level: string, msg: string): void;
|
|
5
11
|
export declare const logInfo: (msg: string) => void;
|
package/dist/logger.js
CHANGED
|
@@ -1,8 +1,103 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Logging utilities — stderr only, stdout is the MCP JSON-RPC channel
|
|
2
|
+
* Logging utilities — stderr only, stdout is the MCP JSON-RPC channel.
|
|
3
|
+
*
|
|
4
|
+
* When the environment variable MCP_PROXY_LOG_FILE is set, log lines are
|
|
5
|
+
* also appended to that file so the Electron host can tail them into main.log.
|
|
6
|
+
*
|
|
7
|
+
* Log rotation: file is named by date (e.g. mcp-proxy-2026-03-09.log).
|
|
8
|
+
* A new file is created each day. Old files beyond MAX_LOG_FILES are deleted.
|
|
3
9
|
*/
|
|
10
|
+
import * as fs from 'fs';
|
|
11
|
+
import * as path from 'path';
|
|
12
|
+
const logFilePath = process.env.MCP_PROXY_LOG_FILE;
|
|
13
|
+
const MAX_LOG_FILES = 7;
|
|
14
|
+
let logStream = null;
|
|
15
|
+
let logDir = '';
|
|
16
|
+
let logBaseName = '';
|
|
17
|
+
let logExt = '';
|
|
18
|
+
let currentDateStr = '';
|
|
19
|
+
function dateStr() {
|
|
20
|
+
const d = new Date();
|
|
21
|
+
const pad2 = (n) => String(n).padStart(2, '0');
|
|
22
|
+
return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
|
|
23
|
+
}
|
|
24
|
+
function openLogFile() {
|
|
25
|
+
if (!logFilePath)
|
|
26
|
+
return;
|
|
27
|
+
const today = dateStr();
|
|
28
|
+
if (today === currentDateStr && logStream)
|
|
29
|
+
return;
|
|
30
|
+
// Close previous stream
|
|
31
|
+
if (logStream) {
|
|
32
|
+
try {
|
|
33
|
+
logStream.end();
|
|
34
|
+
}
|
|
35
|
+
catch { /* ignore */ }
|
|
36
|
+
}
|
|
37
|
+
currentDateStr = today;
|
|
38
|
+
const dated = path.join(logDir, `${logBaseName}-${today}${logExt}`);
|
|
39
|
+
try {
|
|
40
|
+
logStream = fs.createWriteStream(dated, { flags: 'a' });
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
logStream = null;
|
|
44
|
+
}
|
|
45
|
+
// Cleanup old log files
|
|
46
|
+
cleanupOldLogs();
|
|
47
|
+
}
|
|
48
|
+
function cleanupOldLogs() {
|
|
49
|
+
if (!logDir || !logBaseName)
|
|
50
|
+
return;
|
|
51
|
+
try {
|
|
52
|
+
const prefix = `${logBaseName}-`;
|
|
53
|
+
const files = fs.readdirSync(logDir)
|
|
54
|
+
.filter(f => f.startsWith(prefix) && f.endsWith(logExt))
|
|
55
|
+
.sort()
|
|
56
|
+
.reverse();
|
|
57
|
+
for (let i = MAX_LOG_FILES; i < files.length; i++) {
|
|
58
|
+
try {
|
|
59
|
+
fs.unlinkSync(path.join(logDir, files[i]));
|
|
60
|
+
}
|
|
61
|
+
catch { /* ignore */ }
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
catch { /* ignore */ }
|
|
65
|
+
}
|
|
66
|
+
if (logFilePath) {
|
|
67
|
+
try {
|
|
68
|
+
logDir = path.dirname(logFilePath);
|
|
69
|
+
if (logDir && !fs.existsSync(logDir)) {
|
|
70
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
71
|
+
}
|
|
72
|
+
const fullName = path.basename(logFilePath);
|
|
73
|
+
const dotIdx = fullName.lastIndexOf('.');
|
|
74
|
+
if (dotIdx > 0) {
|
|
75
|
+
logBaseName = fullName.substring(0, dotIdx);
|
|
76
|
+
logExt = fullName.substring(dotIdx);
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
logBaseName = fullName;
|
|
80
|
+
logExt = '.log';
|
|
81
|
+
}
|
|
82
|
+
openLogFile();
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// If file creation fails, continue without file logging
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function timestamp() {
|
|
89
|
+
const d = new Date();
|
|
90
|
+
const pad2 = (n) => String(n).padStart(2, '0');
|
|
91
|
+
const pad3 = (n) => String(n).padStart(3, '0');
|
|
92
|
+
return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}.${pad3(d.getMilliseconds())}`;
|
|
93
|
+
}
|
|
4
94
|
export function log(level, msg) {
|
|
5
|
-
|
|
95
|
+
const line = `[${timestamp()}] [${level.toLowerCase()}] [nuwax-mcp-proxy] ${msg}\n`;
|
|
96
|
+
process.stderr.write(line);
|
|
97
|
+
if (logFilePath) {
|
|
98
|
+
openLogFile(); // Rotate if date changed
|
|
99
|
+
logStream?.write(line);
|
|
100
|
+
}
|
|
6
101
|
}
|
|
7
102
|
export const logInfo = (msg) => log('INFO', msg);
|
|
8
103
|
export const logWarn = (msg) => log('WARN', msg);
|
package/dist/resilient.d.ts
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Provides heartbeat monitoring, automatic reconnection, and request queueing
|
|
5
5
|
* for MCP transports (HTTP, SSE, Stdio).
|
|
6
|
+
*
|
|
7
|
+
* Retry strategy: exponential backoff 1s → 2s → 4s → ... → 60s (capped),
|
|
8
|
+
* unlimited retries. Matches the Rust mcp-proxy CappedExponentialBackoff.
|
|
6
9
|
*/
|
|
7
10
|
import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
|
|
8
11
|
import { JSONRPCMessage } from '@modelcontextprotocol/sdk/types.js';
|
|
@@ -21,8 +24,10 @@ export interface ResilientTransportOptions {
|
|
|
21
24
|
maxConsecutiveFailures?: number;
|
|
22
25
|
/** Timeout for checking ping or listTools (ms). Default: 5000 */
|
|
23
26
|
pingTimeoutMs?: number;
|
|
24
|
-
/**
|
|
27
|
+
/** Base backoff delay for reconnect (ms). Default: 1000 */
|
|
25
28
|
reconnectDelayMs?: number;
|
|
29
|
+
/** Max backoff delay cap (ms). Default: 60000 */
|
|
30
|
+
maxReconnectDelayMs?: number;
|
|
26
31
|
/** Max queued requests during reconnect. Default: 100 */
|
|
27
32
|
maxQueueSize?: number;
|
|
28
33
|
/** Server name/ID for logging */
|
|
@@ -43,12 +48,19 @@ export declare class ResilientTransportWrapper implements Transport {
|
|
|
43
48
|
private heartbeatTimer;
|
|
44
49
|
private consecutiveFailures;
|
|
45
50
|
private state;
|
|
51
|
+
/** Current retry attempt count (reset on successful connect) */
|
|
52
|
+
private retryAttempt;
|
|
46
53
|
onclose?: () => void;
|
|
47
54
|
onerror?: (error: Error) => void;
|
|
48
55
|
onmessage?: (message: JSONRPCMessage) => void;
|
|
49
56
|
private messageQueue;
|
|
50
57
|
private pendingPings;
|
|
51
58
|
constructor(options: ResilientTransportOptions);
|
|
59
|
+
/**
|
|
60
|
+
* Calculate backoff delay using capped exponential backoff.
|
|
61
|
+
* 1s → 2s → 4s → 8s → 16s → 32s → 60s (capped)
|
|
62
|
+
*/
|
|
63
|
+
private getBackoffDelay;
|
|
52
64
|
/**
|
|
53
65
|
* Initializes the transport and connects to the backend
|
|
54
66
|
*
|
|
@@ -70,7 +82,12 @@ export declare class ResilientTransportWrapper implements Transport {
|
|
|
70
82
|
private stopHeartbeat;
|
|
71
83
|
/** Track consecutive ping timeouts (no response at all, not even an error) */
|
|
72
84
|
private consecutivePingTimeouts;
|
|
85
|
+
/** Successful heartbeat counter (for reducing log volume) */
|
|
86
|
+
private heartbeatOkCount;
|
|
73
87
|
private checkHealth;
|
|
88
|
+
/**
|
|
89
|
+
* Close the current transport and schedule a reconnect with exponential backoff.
|
|
90
|
+
*/
|
|
74
91
|
private triggerReconnect;
|
|
75
92
|
private flushQueue;
|
|
76
93
|
send(message: JSONRPCMessage): Promise<void>;
|
package/dist/resilient.js
CHANGED
|
@@ -3,13 +3,17 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Provides heartbeat monitoring, automatic reconnection, and request queueing
|
|
5
5
|
* for MCP transports (HTTP, SSE, Stdio).
|
|
6
|
+
*
|
|
7
|
+
* Retry strategy: exponential backoff 1s → 2s → 4s → ... → 60s (capped),
|
|
8
|
+
* unlimited retries. Matches the Rust mcp-proxy CappedExponentialBackoff.
|
|
6
9
|
*/
|
|
7
10
|
import { logInfo, logWarn, logError } from './logger.js';
|
|
8
11
|
const DEFAULT_OPTIONS = {
|
|
9
12
|
pingIntervalMs: 20000,
|
|
10
13
|
maxConsecutiveFailures: 3,
|
|
11
14
|
pingTimeoutMs: 5000,
|
|
12
|
-
reconnectDelayMs:
|
|
15
|
+
reconnectDelayMs: 1000,
|
|
16
|
+
maxReconnectDelayMs: 60000,
|
|
13
17
|
maxQueueSize: 100,
|
|
14
18
|
name: 'remote',
|
|
15
19
|
};
|
|
@@ -27,6 +31,8 @@ export class ResilientTransportWrapper {
|
|
|
27
31
|
heartbeatTimer = null;
|
|
28
32
|
consecutiveFailures = 0;
|
|
29
33
|
state = 'idle';
|
|
34
|
+
/** Current retry attempt count (reset on successful connect) */
|
|
35
|
+
retryAttempt = 0;
|
|
30
36
|
// Handlers required by the Transport interface
|
|
31
37
|
onclose;
|
|
32
38
|
onerror;
|
|
@@ -50,9 +56,18 @@ export class ResilientTransportWrapper {
|
|
|
50
56
|
...(options.pingTimeoutMs !== undefined && { pingTimeoutMs: options.pingTimeoutMs }),
|
|
51
57
|
...(options.maxConsecutiveFailures !== undefined && { maxConsecutiveFailures: options.maxConsecutiveFailures }),
|
|
52
58
|
...(options.reconnectDelayMs !== undefined && { reconnectDelayMs: options.reconnectDelayMs }),
|
|
59
|
+
...(options.maxReconnectDelayMs !== undefined && { maxReconnectDelayMs: options.maxReconnectDelayMs }),
|
|
53
60
|
...(options.maxQueueSize !== undefined && { maxQueueSize: options.maxQueueSize }),
|
|
54
61
|
};
|
|
55
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* Calculate backoff delay using capped exponential backoff.
|
|
65
|
+
* 1s → 2s → 4s → 8s → 16s → 32s → 60s (capped)
|
|
66
|
+
*/
|
|
67
|
+
getBackoffDelay() {
|
|
68
|
+
const delay = this.options.reconnectDelayMs * Math.pow(2, this.retryAttempt);
|
|
69
|
+
return Math.min(delay, this.options.maxReconnectDelayMs);
|
|
70
|
+
}
|
|
56
71
|
/**
|
|
57
72
|
* Initializes the transport and connects to the backend
|
|
58
73
|
*
|
|
@@ -62,8 +77,8 @@ export class ResilientTransportWrapper {
|
|
|
62
77
|
* internally calls transport.start(), and we also call it explicitly in bridge.ts.
|
|
63
78
|
*/
|
|
64
79
|
async start() {
|
|
65
|
-
// Idempotent check: if already connected or
|
|
66
|
-
if (this.state === 'connected' || this.state === 'connecting') {
|
|
80
|
+
// Idempotent check: if already connected, connecting, or retrying, return early
|
|
81
|
+
if (this.state === 'connected' || this.state === 'connecting' || this.state === 'reconnecting') {
|
|
67
82
|
return;
|
|
68
83
|
}
|
|
69
84
|
this.state = 'connecting';
|
|
@@ -78,6 +93,8 @@ export class ResilientTransportWrapper {
|
|
|
78
93
|
this.startHeartbeat();
|
|
79
94
|
}
|
|
80
95
|
async performConnect(initial = false) {
|
|
96
|
+
if (this.state === 'closed')
|
|
97
|
+
return;
|
|
81
98
|
try {
|
|
82
99
|
this.activeTransport = await this.options.connectParams();
|
|
83
100
|
// Inherit the handlers from this wrapper
|
|
@@ -86,7 +103,9 @@ export class ResilientTransportWrapper {
|
|
|
86
103
|
this.state = 'connected';
|
|
87
104
|
this.consecutiveFailures = 0;
|
|
88
105
|
this.consecutivePingTimeouts = 0;
|
|
89
|
-
this.
|
|
106
|
+
this.retryAttempt = 0;
|
|
107
|
+
this.heartbeatOkCount = 0;
|
|
108
|
+
this.log.info(`[McpProxy] [ResilientTransport:${this.options.name}] ✅ Connected via ${this.activeTransport.constructor.name}`);
|
|
90
109
|
// Flush any queued messages
|
|
91
110
|
this.flushQueue();
|
|
92
111
|
// Only start heartbeat on reconnects — initial connections need
|
|
@@ -98,12 +117,14 @@ export class ResilientTransportWrapper {
|
|
|
98
117
|
}
|
|
99
118
|
}
|
|
100
119
|
catch (err) {
|
|
101
|
-
this.
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
120
|
+
const delay = this.getBackoffDelay();
|
|
121
|
+
this.retryAttempt++;
|
|
122
|
+
this.log.error(`[McpProxy] [ResilientTransport:${this.options.name}] ❌ Connect failed (attempt ${this.retryAttempt}): ${err}`);
|
|
123
|
+
this.log.info(`[McpProxy] [ResilientTransport:${this.options.name}] 🔄 Retrying in ${delay}ms...`);
|
|
124
|
+
this.state = 'reconnecting';
|
|
125
|
+
setTimeout(() => {
|
|
126
|
+
this.performConnect();
|
|
127
|
+
}, delay);
|
|
107
128
|
}
|
|
108
129
|
}
|
|
109
130
|
bindInnerTransport(transport) {
|
|
@@ -158,6 +179,8 @@ export class ResilientTransportWrapper {
|
|
|
158
179
|
}
|
|
159
180
|
/** Track consecutive ping timeouts (no response at all, not even an error) */
|
|
160
181
|
consecutivePingTimeouts = 0;
|
|
182
|
+
/** Successful heartbeat counter (for reducing log volume) */
|
|
183
|
+
heartbeatOkCount = 0;
|
|
161
184
|
async checkHealth() {
|
|
162
185
|
if (this.state !== 'connected' || !this.activeTransport)
|
|
163
186
|
return;
|
|
@@ -177,7 +200,6 @@ export class ResilientTransportWrapper {
|
|
|
177
200
|
});
|
|
178
201
|
// Try to send tools/list — if send() throws, the transport itself is broken
|
|
179
202
|
try {
|
|
180
|
-
this.log.info(`[McpProxy] [ResilientTransport:${this.options.name}] 💓 Sending heartbeat (id: ${healthId})`);
|
|
181
203
|
await this.activeTransport.send({
|
|
182
204
|
jsonrpc: '2.0',
|
|
183
205
|
id: healthId,
|
|
@@ -191,15 +213,19 @@ export class ResilientTransportWrapper {
|
|
|
191
213
|
const success = await responsePromise;
|
|
192
214
|
if (!success) {
|
|
193
215
|
this.consecutivePingTimeouts++;
|
|
194
|
-
this.log.warn(`[McpProxy] [ResilientTransport:${this.options.name}] Heartbeat timeout (${this.consecutivePingTimeouts}/${this.options.maxConsecutiveFailures})`);
|
|
216
|
+
this.log.warn(`[McpProxy] [ResilientTransport:${this.options.name}] ⏱️ Heartbeat timeout (${this.consecutivePingTimeouts}/${this.options.maxConsecutiveFailures})`);
|
|
195
217
|
if (this.consecutivePingTimeouts >= this.options.maxConsecutiveFailures) {
|
|
196
|
-
this.log.error(`[McpProxy] [ResilientTransport:${this.options.name}] Max consecutive heartbeat timeouts reached.
|
|
218
|
+
this.log.error(`[McpProxy] [ResilientTransport:${this.options.name}] Max consecutive heartbeat timeouts reached. Closing and retrying...`);
|
|
197
219
|
this.triggerReconnect();
|
|
198
220
|
}
|
|
199
221
|
return;
|
|
200
222
|
}
|
|
201
223
|
// Got a response — server is alive
|
|
202
|
-
this.
|
|
224
|
+
this.heartbeatOkCount++;
|
|
225
|
+
// Only log every 5th success to reduce log volume (~100s interval)
|
|
226
|
+
if (this.heartbeatOkCount % 5 === 1 || this.consecutiveFailures > 0 || this.consecutivePingTimeouts > 0) {
|
|
227
|
+
this.log.info(`[McpProxy] [ResilientTransport:${this.options.name}] 💖 Heartbeat OK (count: ${this.heartbeatOkCount})`);
|
|
228
|
+
}
|
|
203
229
|
this.consecutiveFailures = 0;
|
|
204
230
|
this.consecutivePingTimeouts = 0;
|
|
205
231
|
}
|
|
@@ -207,15 +233,17 @@ export class ResilientTransportWrapper {
|
|
|
207
233
|
this.consecutiveFailures++;
|
|
208
234
|
this.log.warn(`[McpProxy] [ResilientTransport:${this.options.name}] 💔 Heartbeat error (${this.consecutiveFailures}/${this.options.maxConsecutiveFailures}): ${err}`);
|
|
209
235
|
if (this.consecutiveFailures >= this.options.maxConsecutiveFailures) {
|
|
210
|
-
this.log.error(`[McpProxy] [ResilientTransport:${this.options.name}] Max consecutive heartbeat failures reached.
|
|
236
|
+
this.log.error(`[McpProxy] [ResilientTransport:${this.options.name}] Max consecutive heartbeat failures reached. Closing and retrying...`);
|
|
211
237
|
this.triggerReconnect();
|
|
212
238
|
}
|
|
213
239
|
}
|
|
214
240
|
}
|
|
241
|
+
/**
|
|
242
|
+
* Close the current transport and schedule a reconnect with exponential backoff.
|
|
243
|
+
*/
|
|
215
244
|
triggerReconnect() {
|
|
216
245
|
if (this.state === 'reconnecting' || this.state === 'closed')
|
|
217
246
|
return;
|
|
218
|
-
this.log.warn(`[McpProxy] [ResilientTransport:${this.options.name}] 🔄 Triggering reconnect (previous state: ${this.state})`);
|
|
219
247
|
this.state = 'reconnecting';
|
|
220
248
|
this.stopHeartbeat();
|
|
221
249
|
// Clean up old transport
|
|
@@ -229,9 +257,12 @@ export class ResilientTransportWrapper {
|
|
|
229
257
|
catch { /* ignore */ }
|
|
230
258
|
this.activeTransport = null;
|
|
231
259
|
}
|
|
260
|
+
const delay = this.getBackoffDelay();
|
|
261
|
+
this.retryAttempt++;
|
|
262
|
+
this.log.warn(`[McpProxy] [ResilientTransport:${this.options.name}] 🔄 Closed. Retrying in ${delay}ms (attempt ${this.retryAttempt})...`);
|
|
232
263
|
setTimeout(() => {
|
|
233
264
|
this.performConnect();
|
|
234
|
-
},
|
|
265
|
+
}, delay);
|
|
235
266
|
}
|
|
236
267
|
async flushQueue() {
|
|
237
268
|
if (!this.activeTransport || this.state !== 'connected')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nuwax-mcp-stdio-proxy",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.9",
|
|
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": {
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
"test:run": "npm run build:tsc && vitest run",
|
|
19
19
|
"test:coverage": "npm run build:tsc && vitest run --coverage",
|
|
20
20
|
"prepublishOnly": "npm run build",
|
|
21
|
-
"publish:non-latest": "npm publish --tag beta"
|
|
21
|
+
"publish:non-latest": "npm publish --tag beta",
|
|
22
|
+
"publish:prerelease": "npm publish --tag prerelease"
|
|
22
23
|
},
|
|
23
24
|
"dependencies": {
|
|
24
25
|
"@modelcontextprotocol/sdk": "^1.27.1"
|