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/index.js CHANGED
@@ -3222,8 +3222,8 @@ var require_utils = __commonJS({
3222
3222
  }
3223
3223
  return ind;
3224
3224
  }
3225
- function removeDotSegments(path2) {
3226
- let input = path2;
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 [path2, query] = wsComponent.resourceName.split("?");
3426
- wsComponent.path = path2 && path2 !== "/" ? path2 : void 0;
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, fs3, exportName) {
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, fs3[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 fs2 from "fs";
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
- process.stderr.write(`[nuwax-mcp-proxy] ${level}: ${msg}
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: path2, errorMaps, issueData } = params;
7184
- const fullPath = [...path2, ...issueData.path || []];
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, path2, key) {
7376
+ constructor(parent, value, path3, key) {
7300
7377
  this._cachedPath = [];
7301
7378
  this.parent = parent;
7302
7379
  this.data = value;
7303
- this._path = path2;
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, path2) {
10948
- if (!path2)
11024
+ function getElementAtPath(obj, path3) {
11025
+ if (!path3)
10949
11026
  return obj;
10950
- return path2.reduce((acc, key) => acc?.[key], obj);
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(path2, issues) {
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(path2);
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 fs from "fs";
22814
- import * as path from "path";
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 = path.join(dir, command + ext);
22852
- if (fs.existsSync(fullPath)) {
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: 3e3,
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 = "connecting";
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 = { ...DEFAULT_OPTIONS, logger: this.log, ...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 ping requests with "Server not initialized".
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.log.info(`[McpProxy] [ResilientTransport:${this.options.name}] Connected via ${this.activeTransport.constructor.name}`);
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.log.error(`[McpProxy] [ResilientTransport:${this.options.name}] Connect failed: ${err}`);
23093
- if (initial) {
23094
- this.state = "closed";
23095
- throw err;
23096
- }
23097
- this.triggerReconnect();
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
- /** If server doesn't support ping, auto-disable heartbeat after maxConsecutiveFailures timeouts */
23147
- pingDisabled = false;
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 pingId = `respl-ping-${Date.now()}`;
23256
+ const healthId = `respl-ping-${Date.now()}`;
23153
23257
  const responsePromise = new Promise((resolve) => {
23154
- this.pendingPings.set(pingId, resolve);
23258
+ this.pendingPings.set(healthId, resolve);
23155
23259
  setTimeout(() => {
23156
- if (this.pendingPings.has(pingId)) {
23157
- this.pendingPings.delete(pingId);
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: pingId,
23168
- method: "ping"
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.warn(`[McpProxy] [ResilientTransport:${this.options.name}] Server does not respond to ping. Disabling heartbeat monitoring.`);
23179
- this.pingDisabled = true;
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.log.info(`[McpProxy] [ResilientTransport:${this.options.name}] \u{1F496} Heartbeat successful (id: ${pingId})`);
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 (attempt ${this.consecutiveFailures}/${this.options.maxConsecutiveFailures}): ${err}`);
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. Force reconnecting.`);
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
- }, this.options.reconnectDelayMs);
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
- pingIntervalMs: entry.pingIntervalMs,
23331
- pingTimeoutMs: entry.pingTimeoutMs
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
- 5e3,
23342
- `Connection initialization timed out after 5s`
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
- 5e3,
23386
- `Connection initialization timed out after 5s`
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
- 5e3,
23430
- `Connection initialization timed out after 5s`
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.7";
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(), 1e4);
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
- const ct = res.headers.get("content-type") || "";
24211
- if (res.ok && (ct.includes("application/json") || ct.includes("text/event-stream"))) {
24212
- logInfo(`Detected streamable-http protocol for ${url2}`);
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
- clearTimeout(timeout);
24245
- logInfo(`Detected SSE protocol for ${url2}`);
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 "sse";
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 auto-detect protocol for ${url2}, defaulting to streamable-http`);
24255
- return "stream";
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
- for (const [id, entry] of entries) {
24275
- try {
24276
- let connected;
24277
- if (isSseEntry(entry)) {
24278
- connected = await connectSse(id, entry);
24279
- } else if (isStreamableEntry(entry)) {
24280
- connected = await connectStreamable(id, entry);
24281
- } else if (needsProtocolDetection(entry)) {
24282
- const detected = await detectProtocol(entry.url, buildRequestHeaders(entry));
24283
- if (detected === "sse") {
24284
- connected = await connectSse(id, { ...entry, transport: "sse" });
24285
- } else {
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
- } else {
24289
- connected = await connectStdio(id, entry, baseEnv);
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
- } catch (e) {
24319
- logError(`Failed to connect to server "${id}": ${e}`);
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: entry.config.pingIntervalMs,
25833
- pingTimeoutMs: entry.config.pingTimeoutMs
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 = fs2.readFileSync(filePath, "utf-8");
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');