nuwax-mcp-stdio-proxy 1.4.7 → 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/bridge.js +1 -6
- package/dist/detect.d.ts +12 -6
- package/dist/detect.js +61 -52
- package/dist/index.js +278 -151
- package/dist/logger.d.ts +7 -1
- package/dist/logger.js +97 -2
- package/dist/modes/stdio.js +45 -29
- package/dist/resilient.d.ts +19 -4
- package/dist/resilient.js +80 -44
- package/dist/transport.js +10 -6
- package/dist/types.d.ts +6 -4
- package/package.json +4 -3
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
|
};
|
|
@@ -23040,7 +23118,9 @@ var ResilientTransportWrapper = class {
|
|
|
23040
23118
|
mcpClient = null;
|
|
23041
23119
|
heartbeatTimer = null;
|
|
23042
23120
|
consecutiveFailures = 0;
|
|
23043
|
-
state = "
|
|
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;
|
|
@@ -23051,7 +23131,26 @@ var ResilientTransportWrapper = class {
|
|
|
23051
23131
|
pendingPings = /* @__PURE__ */ new Map();
|
|
23052
23132
|
constructor(options) {
|
|
23053
23133
|
this.log = options.logger ?? defaultLogger;
|
|
23054
|
-
this.options = {
|
|
23134
|
+
this.options = {
|
|
23135
|
+
...DEFAULT_OPTIONS,
|
|
23136
|
+
logger: this.log,
|
|
23137
|
+
connectParams: options.connectParams,
|
|
23138
|
+
...options.name !== void 0 && { name: options.name },
|
|
23139
|
+
...options.pingIntervalMs !== void 0 && { pingIntervalMs: options.pingIntervalMs },
|
|
23140
|
+
...options.pingTimeoutMs !== void 0 && { pingTimeoutMs: options.pingTimeoutMs },
|
|
23141
|
+
...options.maxConsecutiveFailures !== void 0 && { maxConsecutiveFailures: options.maxConsecutiveFailures },
|
|
23142
|
+
...options.reconnectDelayMs !== void 0 && { reconnectDelayMs: options.reconnectDelayMs },
|
|
23143
|
+
...options.maxReconnectDelayMs !== void 0 && { maxReconnectDelayMs: options.maxReconnectDelayMs },
|
|
23144
|
+
...options.maxQueueSize !== void 0 && { maxQueueSize: options.maxQueueSize }
|
|
23145
|
+
};
|
|
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);
|
|
23055
23154
|
}
|
|
23056
23155
|
/**
|
|
23057
23156
|
* Initializes the transport and connects to the backend
|
|
@@ -23062,7 +23161,7 @@ var ResilientTransportWrapper = class {
|
|
|
23062
23161
|
* internally calls transport.start(), and we also call it explicitly in bridge.ts.
|
|
23063
23162
|
*/
|
|
23064
23163
|
async start() {
|
|
23065
|
-
if (this.state === "connected" || this.state === "connecting") {
|
|
23164
|
+
if (this.state === "connected" || this.state === "connecting" || this.state === "reconnecting") {
|
|
23066
23165
|
return;
|
|
23067
23166
|
}
|
|
23068
23167
|
this.state = "connecting";
|
|
@@ -23071,30 +23170,36 @@ var ResilientTransportWrapper = class {
|
|
|
23071
23170
|
/**
|
|
23072
23171
|
* Enable heartbeat monitoring. Call this AFTER the MCP client has
|
|
23073
23172
|
* completed its initialize handshake (client.connect()), otherwise
|
|
23074
|
-
* the server will reject
|
|
23173
|
+
* the server will reject requests with "Server not initialized".
|
|
23075
23174
|
*/
|
|
23076
23175
|
enableHeartbeat() {
|
|
23077
23176
|
this.startHeartbeat();
|
|
23078
23177
|
}
|
|
23079
23178
|
async performConnect(initial = false) {
|
|
23179
|
+
if (this.state === "closed") return;
|
|
23080
23180
|
try {
|
|
23081
23181
|
this.activeTransport = await this.options.connectParams();
|
|
23082
23182
|
this.bindInnerTransport(this.activeTransport);
|
|
23083
23183
|
await this.activeTransport.start();
|
|
23084
23184
|
this.state = "connected";
|
|
23085
23185
|
this.consecutiveFailures = 0;
|
|
23086
|
-
this.
|
|
23186
|
+
this.consecutivePingTimeouts = 0;
|
|
23187
|
+
this.retryAttempt = 0;
|
|
23188
|
+
this.heartbeatOkCount = 0;
|
|
23189
|
+
this.log.info(`[McpProxy] [ResilientTransport:${this.options.name}] \u2705 Connected via ${this.activeTransport.constructor.name}`);
|
|
23087
23190
|
this.flushQueue();
|
|
23088
23191
|
if (!initial) {
|
|
23089
23192
|
this.startHeartbeat();
|
|
23090
23193
|
}
|
|
23091
23194
|
} catch (err) {
|
|
23092
|
-
this.
|
|
23093
|
-
|
|
23094
|
-
|
|
23095
|
-
|
|
23096
|
-
|
|
23097
|
-
|
|
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);
|
|
23098
23203
|
}
|
|
23099
23204
|
}
|
|
23100
23205
|
bindInnerTransport(transport) {
|
|
@@ -23143,61 +23248,61 @@ var ResilientTransportWrapper = class {
|
|
|
23143
23248
|
}
|
|
23144
23249
|
/** Track consecutive ping timeouts (no response at all, not even an error) */
|
|
23145
23250
|
consecutivePingTimeouts = 0;
|
|
23146
|
-
/**
|
|
23147
|
-
|
|
23251
|
+
/** Successful heartbeat counter (for reducing log volume) */
|
|
23252
|
+
heartbeatOkCount = 0;
|
|
23148
23253
|
async checkHealth() {
|
|
23149
23254
|
if (this.state !== "connected" || !this.activeTransport) return;
|
|
23150
|
-
if (this.pingDisabled) return;
|
|
23151
23255
|
try {
|
|
23152
|
-
const
|
|
23256
|
+
const healthId = `respl-ping-${Date.now()}`;
|
|
23153
23257
|
const responsePromise = new Promise((resolve) => {
|
|
23154
|
-
this.pendingPings.set(
|
|
23258
|
+
this.pendingPings.set(healthId, resolve);
|
|
23155
23259
|
setTimeout(() => {
|
|
23156
|
-
if (this.pendingPings.has(
|
|
23157
|
-
this.pendingPings.delete(
|
|
23260
|
+
if (this.pendingPings.has(healthId)) {
|
|
23261
|
+
this.pendingPings.delete(healthId);
|
|
23158
23262
|
resolve(false);
|
|
23159
23263
|
}
|
|
23160
23264
|
}, this.options.pingTimeoutMs);
|
|
23161
23265
|
});
|
|
23162
|
-
let sendFailed = false;
|
|
23163
23266
|
try {
|
|
23164
|
-
this.log.info(`[McpProxy] [ResilientTransport:${this.options.name}] \u{1F493} Sending heartbeat ping (id: ${pingId})`);
|
|
23165
23267
|
await this.activeTransport.send({
|
|
23166
23268
|
jsonrpc: "2.0",
|
|
23167
|
-
id:
|
|
23168
|
-
method: "
|
|
23269
|
+
id: healthId,
|
|
23270
|
+
method: "tools/list",
|
|
23271
|
+
params: {}
|
|
23169
23272
|
});
|
|
23170
23273
|
} catch (sendErr) {
|
|
23171
|
-
sendFailed = true;
|
|
23172
23274
|
throw sendErr;
|
|
23173
23275
|
}
|
|
23174
23276
|
const success2 = await responsePromise;
|
|
23175
23277
|
if (!success2) {
|
|
23176
23278
|
this.consecutivePingTimeouts++;
|
|
23279
|
+
this.log.warn(`[McpProxy] [ResilientTransport:${this.options.name}] \u23F1\uFE0F Heartbeat timeout (${this.consecutivePingTimeouts}/${this.options.maxConsecutiveFailures})`);
|
|
23177
23280
|
if (this.consecutivePingTimeouts >= this.options.maxConsecutiveFailures) {
|
|
23178
|
-
this.log.
|
|
23179
|
-
this.
|
|
23180
|
-
this.stopHeartbeat();
|
|
23181
|
-
return;
|
|
23281
|
+
this.log.error(`[McpProxy] [ResilientTransport:${this.options.name}] Max consecutive heartbeat timeouts reached. Closing and retrying...`);
|
|
23282
|
+
this.triggerReconnect();
|
|
23182
23283
|
}
|
|
23183
|
-
this.log.warn(`[McpProxy] [ResilientTransport:${this.options.name}] Ping timeout (${this.consecutivePingTimeouts}/${this.options.maxConsecutiveFailures})`);
|
|
23184
23284
|
return;
|
|
23185
23285
|
}
|
|
23186
|
-
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
|
+
}
|
|
23187
23290
|
this.consecutiveFailures = 0;
|
|
23188
23291
|
this.consecutivePingTimeouts = 0;
|
|
23189
23292
|
} catch (err) {
|
|
23190
23293
|
this.consecutiveFailures++;
|
|
23191
|
-
this.log.warn(`[McpProxy] [ResilientTransport:${this.options.name}] \u{1F494} Heartbeat error (
|
|
23294
|
+
this.log.warn(`[McpProxy] [ResilientTransport:${this.options.name}] \u{1F494} Heartbeat error (${this.consecutiveFailures}/${this.options.maxConsecutiveFailures}): ${err}`);
|
|
23192
23295
|
if (this.consecutiveFailures >= this.options.maxConsecutiveFailures) {
|
|
23193
|
-
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...`);
|
|
23194
23297
|
this.triggerReconnect();
|
|
23195
23298
|
}
|
|
23196
23299
|
}
|
|
23197
23300
|
}
|
|
23301
|
+
/**
|
|
23302
|
+
* Close the current transport and schedule a reconnect with exponential backoff.
|
|
23303
|
+
*/
|
|
23198
23304
|
triggerReconnect() {
|
|
23199
23305
|
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
23306
|
this.state = "reconnecting";
|
|
23202
23307
|
this.stopHeartbeat();
|
|
23203
23308
|
if (this.activeTransport) {
|
|
@@ -23209,9 +23314,12 @@ var ResilientTransportWrapper = class {
|
|
|
23209
23314
|
}
|
|
23210
23315
|
this.activeTransport = null;
|
|
23211
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})...`);
|
|
23212
23320
|
setTimeout(() => {
|
|
23213
23321
|
this.performConnect();
|
|
23214
|
-
},
|
|
23322
|
+
}, delay);
|
|
23215
23323
|
}
|
|
23216
23324
|
async flushQueue() {
|
|
23217
23325
|
if (!this.activeTransport || this.state !== "connected") return;
|
|
@@ -23274,6 +23382,8 @@ var ResilientTransportWrapper = class {
|
|
|
23274
23382
|
};
|
|
23275
23383
|
|
|
23276
23384
|
// src/transport.ts
|
|
23385
|
+
var DEFAULT_STDIO_CONNECTION_TIMEOUT_MS = 6e4;
|
|
23386
|
+
var DEFAULT_HTTP_CONNECTION_TIMEOUT_MS = 3e4;
|
|
23277
23387
|
async function withTimeout(promise2, ms, message) {
|
|
23278
23388
|
let timeoutId;
|
|
23279
23389
|
const timeoutPromise = new Promise((_, reject) => {
|
|
@@ -23327,19 +23437,19 @@ async function connectStdio(id, entry, baseEnv) {
|
|
|
23327
23437
|
}
|
|
23328
23438
|
return t;
|
|
23329
23439
|
},
|
|
23330
|
-
|
|
23331
|
-
|
|
23440
|
+
// No heartbeat for stdio — child process close/error events handle detection
|
|
23441
|
+
pingIntervalMs: 0
|
|
23332
23442
|
});
|
|
23333
23443
|
const client = new Client({ name: `proxy-${id}`, version: "1.0.0" });
|
|
23444
|
+
const timeoutMs = entry.connectionTimeoutMs ?? DEFAULT_STDIO_CONNECTION_TIMEOUT_MS;
|
|
23334
23445
|
try {
|
|
23335
23446
|
await withTimeout(
|
|
23336
23447
|
(async () => {
|
|
23337
23448
|
await wrapper.start();
|
|
23338
23449
|
await client.connect(wrapper);
|
|
23339
|
-
wrapper.enableHeartbeat();
|
|
23340
23450
|
})(),
|
|
23341
|
-
|
|
23342
|
-
`Connection initialization timed out after
|
|
23451
|
+
timeoutMs,
|
|
23452
|
+
`Connection initialization timed out after ${timeoutMs / 1e3}s`
|
|
23343
23453
|
);
|
|
23344
23454
|
} catch (err) {
|
|
23345
23455
|
try {
|
|
@@ -23375,6 +23485,7 @@ async function connectStreamable(id, entry) {
|
|
|
23375
23485
|
pingTimeoutMs: entry.pingTimeoutMs
|
|
23376
23486
|
});
|
|
23377
23487
|
const client = new Client({ name: `proxy-${id}`, version: "1.0.0" });
|
|
23488
|
+
const timeoutMs = entry.connectionTimeoutMs ?? DEFAULT_HTTP_CONNECTION_TIMEOUT_MS;
|
|
23378
23489
|
try {
|
|
23379
23490
|
await withTimeout(
|
|
23380
23491
|
(async () => {
|
|
@@ -23382,8 +23493,8 @@ async function connectStreamable(id, entry) {
|
|
|
23382
23493
|
await client.connect(wrapper);
|
|
23383
23494
|
wrapper.enableHeartbeat();
|
|
23384
23495
|
})(),
|
|
23385
|
-
|
|
23386
|
-
`Connection initialization timed out after
|
|
23496
|
+
timeoutMs,
|
|
23497
|
+
`Connection initialization timed out after ${timeoutMs / 1e3}s`
|
|
23387
23498
|
);
|
|
23388
23499
|
} catch (err) {
|
|
23389
23500
|
try {
|
|
@@ -23419,6 +23530,7 @@ async function connectSse(id, entry) {
|
|
|
23419
23530
|
pingTimeoutMs: entry.pingTimeoutMs
|
|
23420
23531
|
});
|
|
23421
23532
|
const client = new Client({ name: `proxy-${id}`, version: "1.0.0" });
|
|
23533
|
+
const timeoutMs = entry.connectionTimeoutMs ?? DEFAULT_HTTP_CONNECTION_TIMEOUT_MS;
|
|
23422
23534
|
try {
|
|
23423
23535
|
await withTimeout(
|
|
23424
23536
|
(async () => {
|
|
@@ -23426,8 +23538,8 @@ async function connectSse(id, entry) {
|
|
|
23426
23538
|
await client.connect(wrapper);
|
|
23427
23539
|
wrapper.enableHeartbeat();
|
|
23428
23540
|
})(),
|
|
23429
|
-
|
|
23430
|
-
`Connection initialization timed out after
|
|
23541
|
+
timeoutMs,
|
|
23542
|
+
`Connection initialization timed out after ${timeoutMs / 1e3}s`
|
|
23431
23543
|
);
|
|
23432
23544
|
} catch (err) {
|
|
23433
23545
|
try {
|
|
@@ -24105,7 +24217,7 @@ var StdioServerTransport = class {
|
|
|
24105
24217
|
|
|
24106
24218
|
// src/constants.ts
|
|
24107
24219
|
var PKG_NAME = "nuwax-mcp-stdio-proxy";
|
|
24108
|
-
var PKG_VERSION = "1.4.
|
|
24220
|
+
var PKG_VERSION = "1.4.9";
|
|
24109
24221
|
|
|
24110
24222
|
// src/shared.ts
|
|
24111
24223
|
async function discoverTools(client) {
|
|
@@ -24190,7 +24302,7 @@ async function detectProtocol(url2, headers) {
|
|
|
24190
24302
|
...headers
|
|
24191
24303
|
};
|
|
24192
24304
|
const controller = new AbortController();
|
|
24193
|
-
const timeout = setTimeout(() => controller.abort(),
|
|
24305
|
+
const timeout = setTimeout(() => controller.abort(), 5e3);
|
|
24194
24306
|
const res = await fetch(url2, {
|
|
24195
24307
|
method: "POST",
|
|
24196
24308
|
headers: reqHeaders,
|
|
@@ -24207,52 +24319,51 @@ async function detectProtocol(url2, headers) {
|
|
|
24207
24319
|
signal: controller.signal
|
|
24208
24320
|
});
|
|
24209
24321
|
clearTimeout(timeout);
|
|
24210
|
-
|
|
24211
|
-
|
|
24212
|
-
|
|
24322
|
+
if (res.headers.get("mcp-session-id")) {
|
|
24323
|
+
logInfo(`Detected streamable-http protocol for ${url2} (mcp-session-id header)`);
|
|
24324
|
+
cleanupSession(url2, res.headers.get("mcp-session-id"), headers);
|
|
24213
24325
|
await res.text().catch(() => {
|
|
24214
24326
|
});
|
|
24215
|
-
const sessionId = res.headers.get("mcp-session-id");
|
|
24216
|
-
if (sessionId) {
|
|
24217
|
-
fetch(url2, {
|
|
24218
|
-
method: "DELETE",
|
|
24219
|
-
headers: { "mcp-session-id": sessionId, ...headers },
|
|
24220
|
-
signal: AbortSignal.timeout(5e3)
|
|
24221
|
-
}).catch(() => {
|
|
24222
|
-
});
|
|
24223
|
-
}
|
|
24224
24327
|
return "stream";
|
|
24225
24328
|
}
|
|
24226
|
-
await res.text().catch(() => {
|
|
24227
|
-
});
|
|
24228
|
-
} catch {
|
|
24229
|
-
}
|
|
24230
|
-
try {
|
|
24231
|
-
const reqHeaders = {
|
|
24232
|
-
Accept: "text/event-stream",
|
|
24233
|
-
...headers
|
|
24234
|
-
};
|
|
24235
|
-
const controller = new AbortController();
|
|
24236
|
-
const timeout = setTimeout(() => controller.abort(), 1e4);
|
|
24237
|
-
const res = await fetch(url2, {
|
|
24238
|
-
method: "GET",
|
|
24239
|
-
headers: reqHeaders,
|
|
24240
|
-
signal: controller.signal
|
|
24241
|
-
});
|
|
24242
24329
|
const ct = res.headers.get("content-type") || "";
|
|
24243
|
-
if (ct.includes("text/event-stream")) {
|
|
24244
|
-
|
|
24245
|
-
|
|
24330
|
+
if (ct.includes("text/event-stream") && res.ok) {
|
|
24331
|
+
logInfo(`Detected streamable-http protocol for ${url2} (event-stream response)`);
|
|
24332
|
+
cleanupSession(url2, res.headers.get("mcp-session-id"), headers);
|
|
24246
24333
|
controller.abort();
|
|
24247
|
-
return "
|
|
24334
|
+
return "stream";
|
|
24335
|
+
}
|
|
24336
|
+
let bodyText = "";
|
|
24337
|
+
try {
|
|
24338
|
+
bodyText = await res.text();
|
|
24339
|
+
} catch {
|
|
24340
|
+
}
|
|
24341
|
+
try {
|
|
24342
|
+
const json2 = JSON.parse(bodyText);
|
|
24343
|
+
if (json2 && json2.jsonrpc === "2.0") {
|
|
24344
|
+
logInfo(`Detected streamable-http protocol for ${url2} (JSON-RPC 2.0 response)`);
|
|
24345
|
+
cleanupSession(url2, res.headers.get("mcp-session-id"), headers);
|
|
24346
|
+
return "stream";
|
|
24347
|
+
}
|
|
24348
|
+
} catch {
|
|
24349
|
+
}
|
|
24350
|
+
if (res.status === 406) {
|
|
24351
|
+
logInfo(`Detected streamable-http protocol for ${url2} (406 Not Acceptable)`);
|
|
24352
|
+
return "stream";
|
|
24248
24353
|
}
|
|
24249
|
-
clearTimeout(timeout);
|
|
24250
|
-
await res.text().catch(() => {
|
|
24251
|
-
});
|
|
24252
24354
|
} catch {
|
|
24253
24355
|
}
|
|
24254
|
-
logWarn(`Could not
|
|
24255
|
-
return "
|
|
24356
|
+
logWarn(`Could not detect streamable-http for ${url2}, defaulting to SSE`);
|
|
24357
|
+
return "sse";
|
|
24358
|
+
}
|
|
24359
|
+
function cleanupSession(url2, sessionId, headers) {
|
|
24360
|
+
if (!sessionId) return;
|
|
24361
|
+
fetch(url2, {
|
|
24362
|
+
method: "DELETE",
|
|
24363
|
+
headers: { "mcp-session-id": sessionId, ...headers },
|
|
24364
|
+
signal: AbortSignal.timeout(5e3)
|
|
24365
|
+
}).catch(() => {
|
|
24366
|
+
});
|
|
24256
24367
|
}
|
|
24257
24368
|
|
|
24258
24369
|
// src/modes/stdio.ts
|
|
@@ -24271,52 +24382,69 @@ async function runStdio(config2, allowTools, denyTools) {
|
|
|
24271
24382
|
const toolToClient = /* @__PURE__ */ new Map();
|
|
24272
24383
|
const toolToServer = /* @__PURE__ */ new Map();
|
|
24273
24384
|
const toolsByName = /* @__PURE__ */ new Map();
|
|
24274
|
-
|
|
24275
|
-
|
|
24276
|
-
|
|
24277
|
-
|
|
24278
|
-
|
|
24279
|
-
|
|
24280
|
-
|
|
24281
|
-
|
|
24282
|
-
|
|
24283
|
-
|
|
24284
|
-
|
|
24285
|
-
|
|
24385
|
+
const connectableEntries = entries.filter(([id, entry]) => {
|
|
24386
|
+
if (entry.persistent) {
|
|
24387
|
+
logWarn(`Skipping persistent server "${id}" (handled by PersistentMcpBridge)`);
|
|
24388
|
+
return false;
|
|
24389
|
+
}
|
|
24390
|
+
return true;
|
|
24391
|
+
});
|
|
24392
|
+
const results = await Promise.allSettled(
|
|
24393
|
+
connectableEntries.map(async ([id, entry]) => {
|
|
24394
|
+
try {
|
|
24395
|
+
let connected;
|
|
24396
|
+
if (isSseEntry(entry)) {
|
|
24397
|
+
connected = await connectSse(id, entry);
|
|
24398
|
+
} else if (isStreamableEntry(entry)) {
|
|
24286
24399
|
connected = await connectStreamable(id, entry);
|
|
24400
|
+
} else if (needsProtocolDetection(entry)) {
|
|
24401
|
+
const detected = await detectProtocol(entry.url, buildRequestHeaders(entry));
|
|
24402
|
+
if (detected === "sse") {
|
|
24403
|
+
connected = await connectSse(id, { ...entry, transport: "sse" });
|
|
24404
|
+
} else {
|
|
24405
|
+
connected = await connectStreamable(id, entry);
|
|
24406
|
+
}
|
|
24407
|
+
} else {
|
|
24408
|
+
connected = await connectStdio(id, entry, baseEnv);
|
|
24287
24409
|
}
|
|
24288
|
-
|
|
24289
|
-
|
|
24290
|
-
|
|
24291
|
-
const { client, cleanup } = connected;
|
|
24292
|
-
clients.set(id, client);
|
|
24293
|
-
cleanups.set(id, cleanup);
|
|
24294
|
-
let serverTools = await discoverTools(client);
|
|
24295
|
-
if (entry.allowTools || entry.denyTools) {
|
|
24296
|
-
const perFilter = {};
|
|
24297
|
-
if (entry.allowTools) perFilter.allowTools = new Set(entry.allowTools);
|
|
24298
|
-
if (entry.denyTools) perFilter.denyTools = new Set(entry.denyTools);
|
|
24299
|
-
const before = serverTools.length;
|
|
24300
|
-
serverTools = filterTools(serverTools, perFilter);
|
|
24301
|
-
if (serverTools.length !== before) {
|
|
24302
|
-
logInfo(`Server "${id}": filtered ${before} \u2192 ${serverTools.length} tool(s)`);
|
|
24303
|
-
}
|
|
24304
|
-
}
|
|
24305
|
-
logInfo(
|
|
24306
|
-
`Server "${id}": ${serverTools.length} tool(s)${serverTools.length > 0 ? " \u2014 " + serverTools.map((t) => t.name).join(", ") : ""}`
|
|
24307
|
-
);
|
|
24308
|
-
for (const tool of serverTools) {
|
|
24309
|
-
if (toolToClient.has(tool.name)) {
|
|
24310
|
-
logWarn(
|
|
24311
|
-
`Tool "${tool.name}" from "${id}" shadows existing tool from "${toolToServer.get(tool.name)}"`
|
|
24312
|
-
);
|
|
24313
|
-
}
|
|
24314
|
-
toolToClient.set(tool.name, client);
|
|
24315
|
-
toolToServer.set(tool.name, id);
|
|
24316
|
-
toolsByName.set(tool.name, tool);
|
|
24410
|
+
return { id, entry, connected };
|
|
24411
|
+
} catch (e) {
|
|
24412
|
+
throw new Error(`Server "${id}": ${e}`);
|
|
24317
24413
|
}
|
|
24318
|
-
}
|
|
24319
|
-
|
|
24414
|
+
})
|
|
24415
|
+
);
|
|
24416
|
+
for (const result of results) {
|
|
24417
|
+
if (result.status === "rejected") {
|
|
24418
|
+
logError(`Failed to connect: ${result.reason}`);
|
|
24419
|
+
continue;
|
|
24420
|
+
}
|
|
24421
|
+
const { id, entry, connected } = result.value;
|
|
24422
|
+
const { client, cleanup } = connected;
|
|
24423
|
+
clients.set(id, client);
|
|
24424
|
+
cleanups.set(id, cleanup);
|
|
24425
|
+
let serverTools = await discoverTools(client);
|
|
24426
|
+
if (entry.allowTools || entry.denyTools) {
|
|
24427
|
+
const perFilter = {};
|
|
24428
|
+
if (entry.allowTools) perFilter.allowTools = new Set(entry.allowTools);
|
|
24429
|
+
if (entry.denyTools) perFilter.denyTools = new Set(entry.denyTools);
|
|
24430
|
+
const before = serverTools.length;
|
|
24431
|
+
serverTools = filterTools(serverTools, perFilter);
|
|
24432
|
+
if (serverTools.length !== before) {
|
|
24433
|
+
logInfo(`Server "${id}": filtered ${before} \u2192 ${serverTools.length} tool(s)`);
|
|
24434
|
+
}
|
|
24435
|
+
}
|
|
24436
|
+
logInfo(
|
|
24437
|
+
`Server "${id}": ${serverTools.length} tool(s)${serverTools.length > 0 ? " \u2014 " + serverTools.map((t) => t.name).join(", ") : ""}`
|
|
24438
|
+
);
|
|
24439
|
+
for (const tool of serverTools) {
|
|
24440
|
+
if (toolToClient.has(tool.name)) {
|
|
24441
|
+
logWarn(
|
|
24442
|
+
`Tool "${tool.name}" from "${id}" shadows existing tool from "${toolToServer.get(tool.name)}"`
|
|
24443
|
+
);
|
|
24444
|
+
}
|
|
24445
|
+
toolToClient.set(tool.name, client);
|
|
24446
|
+
toolToServer.set(tool.name, id);
|
|
24447
|
+
toolsByName.set(tool.name, tool);
|
|
24320
24448
|
}
|
|
24321
24449
|
}
|
|
24322
24450
|
if (clients.size === 0) {
|
|
@@ -25829,8 +25957,8 @@ var PersistentMcpBridge = class {
|
|
|
25829
25957
|
}
|
|
25830
25958
|
return t;
|
|
25831
25959
|
},
|
|
25832
|
-
pingIntervalMs:
|
|
25833
|
-
|
|
25960
|
+
pingIntervalMs: 0
|
|
25961
|
+
// No heartbeat for stdio — child process close/error events handle detection
|
|
25834
25962
|
});
|
|
25835
25963
|
const client = new Client(
|
|
25836
25964
|
{ name: "nuwax-persistent-bridge", version: "1.0.0" },
|
|
@@ -25848,7 +25976,6 @@ var PersistentMcpBridge = class {
|
|
|
25848
25976
|
};
|
|
25849
25977
|
await wrapper.start();
|
|
25850
25978
|
await client.connect(wrapper);
|
|
25851
|
-
wrapper.enableHeartbeat();
|
|
25852
25979
|
entry.client = client;
|
|
25853
25980
|
entry.transport = wrapper;
|
|
25854
25981
|
const result = await client.listTools();
|
|
@@ -26300,7 +26427,7 @@ function parseConfigJson(json2) {
|
|
|
26300
26427
|
}
|
|
26301
26428
|
function parseConfigFile(filePath) {
|
|
26302
26429
|
try {
|
|
26303
|
-
const content =
|
|
26430
|
+
const content = fs3.readFileSync(filePath, "utf-8");
|
|
26304
26431
|
const config2 = JSON.parse(content);
|
|
26305
26432
|
if (!config2.mcpServers || typeof config2.mcpServers !== "object") {
|
|
26306
26433
|
throw new Error('config must contain a "mcpServers" object');
|