nuwax-mcp-stdio-proxy 1.4.4 → 1.4.6

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 CHANGED
@@ -49,17 +49,36 @@ export class PersistentMcpBridge {
49
49
  this.log.warn(`${LOG_TAG} Already running, stopping first`);
50
50
  await this.stop();
51
51
  }
52
- this.log.info(`${LOG_TAG} Starting with ${Object.keys(servers).length} persistent servers`);
53
- // 1. Spawn each persistent server + create MCP Client
54
- for (const [id, config] of Object.entries(servers)) {
55
- await this.startServer(id, config);
52
+ const serverCount = Object.keys(servers).length;
53
+ this.log.info(`${LOG_TAG} Starting with ${serverCount} persistent servers (parallel)`);
54
+ // 1. 并行启动 HTTP server 和所有 MCP server 子进程。
55
+ // 两者完全独立:HTTP server 在请求时才查询 this.servers,
56
+ // startServer 会在异步 spawnAndConnect 前先同步写入 this.servers,
57
+ // 所以 HTTP server 收到第一个请求时所有 entry 都已注册。
58
+ //
59
+ // 提前保存 serverPromises 引用:若 startHttpServer 抛出,在 catch 中先
60
+ // await Promise.allSettled(serverPromises) 等所有 spawnAndConnect 跑完
61
+ // (保证 entry.client / entry.transport 已赋值),再调用 stopServer,
62
+ // 才能可靠关闭子进程,防止孤儿进程残留。
63
+ const serverPromises = Object.entries(servers).map(([id, config]) => this.startServer(id, config));
64
+ try {
65
+ await Promise.all([
66
+ this.startHttpServer(options?.port),
67
+ ...serverPromises,
68
+ ]);
69
+ }
70
+ catch (e) {
71
+ this.log.error(`${LOG_TAG} 启动失败,清理已 spawn 的子进程:`, e);
72
+ // 等待所有 spawnAndConnect 完成,确保 entry.client/transport 已赋值后再清理
73
+ await Promise.allSettled(serverPromises);
74
+ await Promise.all(Array.from(this.servers.keys()).map((id) => this.stopServer(id)));
75
+ this.servers.clear();
76
+ throw e;
56
77
  }
57
- // 2. Start HTTP server
58
- await this.startHttpServer(options?.port);
59
78
  this.running = true;
60
- // 3. Start periodic session cleanup
79
+ // 2. Start periodic session cleanup
61
80
  this.sessionCleanupTimer = setInterval(() => this.cleanupStaleSessions(), SESSION_CLEANUP_INTERVAL_MS);
62
- this.log.info(`${LOG_TAG} Bridge ready on port ${this.port}`);
81
+ this.log.info(`${LOG_TAG} Bridge ready on port ${this.port} (${serverCount} servers)`);
63
82
  }
64
83
  /**
65
84
  * Stop bridge: close HTTP, kill all child processes
@@ -72,15 +91,16 @@ export class PersistentMcpBridge {
72
91
  clearInterval(this.sessionCleanupTimer);
73
92
  this.sessionCleanupTimer = null;
74
93
  }
75
- // Close all HTTP sessions
76
- for (const [key, session] of this.httpSessions) {
94
+ // 并行关闭所有 HTTP sessions(各 session transport 独立,无顺序依赖)。
95
+ // 内层 async 函数自行 catch 错误后不再 throw,Promise.all 足够,不需要 allSettled。
96
+ await Promise.all(Array.from(this.httpSessions.entries()).map(async ([key, session]) => {
77
97
  try {
78
98
  await session.transport.close();
79
99
  }
80
100
  catch (e) {
81
101
  this.log.warn(`${LOG_TAG} Error closing HTTP session ${key}:`, e);
82
102
  }
83
- }
103
+ }));
84
104
  this.httpSessions.clear();
85
105
  // Close HTTP server
86
106
  if (this.httpServer) {
@@ -90,10 +110,8 @@ export class PersistentMcpBridge {
90
110
  this.httpServer = null;
91
111
  this.port = 0;
92
112
  }
93
- // Stop all persistent servers
94
- for (const [id] of this.servers) {
95
- await this.stopServer(id);
96
- }
113
+ // 并行关闭所有 MCP server 子进程(各自独立,无顺序依赖)
114
+ await Promise.all(Array.from(this.servers.keys()).map((id) => this.stopServer(id)));
97
115
  this.servers.clear();
98
116
  this.log.info(`${LOG_TAG} Stopped`);
99
117
  }
package/dist/index.js CHANGED
@@ -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, fs2, exportName) {
6788
+ function addFormats(ajv, list, fs3, 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, fs2[f]);
6793
+ ajv.addFormat(f, fs3[f]);
6794
6794
  }
6795
6795
  module.exports = exports = formatsPlugin;
6796
6796
  Object.defineProperty(exports, "__esModule", { value: true });
@@ -6798,6 +6798,9 @@ var require_dist = __commonJS({
6798
6798
  }
6799
6799
  });
6800
6800
 
6801
+ // src/index.ts
6802
+ import * as fs2 from "fs";
6803
+
6801
6804
  // src/logger.ts
6802
6805
  function log(level, msg) {
6803
6806
  process.stderr.write(`[nuwax-mcp-proxy] ${level}: ${msg}
@@ -23764,7 +23767,7 @@ var StdioServerTransport = class {
23764
23767
 
23765
23768
  // src/constants.ts
23766
23769
  var PKG_NAME = "nuwax-mcp-stdio-proxy";
23767
- var PKG_VERSION = "1.4.4";
23770
+ var PKG_VERSION = "1.4.6";
23768
23771
 
23769
23772
  // src/shared.ts
23770
23773
  async function discoverTools(client) {
@@ -25379,14 +25382,24 @@ var PersistentMcpBridge = class {
25379
25382
  this.log.warn(`${LOG_TAG} Already running, stopping first`);
25380
25383
  await this.stop();
25381
25384
  }
25382
- this.log.info(`${LOG_TAG} Starting with ${Object.keys(servers).length} persistent servers`);
25383
- for (const [id, config2] of Object.entries(servers)) {
25384
- await this.startServer(id, config2);
25385
+ const serverCount = Object.keys(servers).length;
25386
+ this.log.info(`${LOG_TAG} Starting with ${serverCount} persistent servers (parallel)`);
25387
+ const serverPromises = Object.entries(servers).map(([id, config2]) => this.startServer(id, config2));
25388
+ try {
25389
+ await Promise.all([
25390
+ this.startHttpServer(options?.port),
25391
+ ...serverPromises
25392
+ ]);
25393
+ } catch (e) {
25394
+ this.log.error(`${LOG_TAG} \u542F\u52A8\u5931\u8D25\uFF0C\u6E05\u7406\u5DF2 spawn \u7684\u5B50\u8FDB\u7A0B:`, e);
25395
+ await Promise.allSettled(serverPromises);
25396
+ await Promise.all(Array.from(this.servers.keys()).map((id) => this.stopServer(id)));
25397
+ this.servers.clear();
25398
+ throw e;
25385
25399
  }
25386
- await this.startHttpServer(options?.port);
25387
25400
  this.running = true;
25388
25401
  this.sessionCleanupTimer = setInterval(() => this.cleanupStaleSessions(), SESSION_CLEANUP_INTERVAL_MS);
25389
- this.log.info(`${LOG_TAG} Bridge ready on port ${this.port}`);
25402
+ this.log.info(`${LOG_TAG} Bridge ready on port ${this.port} (${serverCount} servers)`);
25390
25403
  }
25391
25404
  /**
25392
25405
  * Stop bridge: close HTTP, kill all child processes
@@ -25398,13 +25411,15 @@ var PersistentMcpBridge = class {
25398
25411
  clearInterval(this.sessionCleanupTimer);
25399
25412
  this.sessionCleanupTimer = null;
25400
25413
  }
25401
- for (const [key, session] of this.httpSessions) {
25402
- try {
25403
- await session.transport.close();
25404
- } catch (e) {
25405
- this.log.warn(`${LOG_TAG} Error closing HTTP session ${key}:`, e);
25406
- }
25407
- }
25414
+ await Promise.all(
25415
+ Array.from(this.httpSessions.entries()).map(async ([key, session]) => {
25416
+ try {
25417
+ await session.transport.close();
25418
+ } catch (e) {
25419
+ this.log.warn(`${LOG_TAG} Error closing HTTP session ${key}:`, e);
25420
+ }
25421
+ })
25422
+ );
25408
25423
  this.httpSessions.clear();
25409
25424
  if (this.httpServer) {
25410
25425
  await new Promise((resolve) => {
@@ -25413,9 +25428,9 @@ var PersistentMcpBridge = class {
25413
25428
  this.httpServer = null;
25414
25429
  this.port = 0;
25415
25430
  }
25416
- for (const [id] of this.servers) {
25417
- await this.stopServer(id);
25418
- }
25431
+ await Promise.all(
25432
+ Array.from(this.servers.keys()).map((id) => this.stopServer(id))
25433
+ );
25419
25434
  this.servers.clear();
25420
25435
  this.log.info(`${LOG_TAG} Stopped`);
25421
25436
  }
@@ -25782,6 +25797,7 @@ function parseCliArgs() {
25782
25797
  }
25783
25798
  function parseStdioArgs(args) {
25784
25799
  let configJson;
25800
+ let configFile;
25785
25801
  let allowTools;
25786
25802
  let denyTools;
25787
25803
  for (let i = 0; i < args.length; i++) {
@@ -25789,6 +25805,9 @@ function parseStdioArgs(args) {
25789
25805
  if (arg === "--config" && i + 1 < args.length) {
25790
25806
  i++;
25791
25807
  configJson = args[i];
25808
+ } else if (arg === "--config-file" && i + 1 < args.length) {
25809
+ i++;
25810
+ configFile = args[i];
25792
25811
  } else if (arg === "--allow-tools" && i + 1 < args.length) {
25793
25812
  i++;
25794
25813
  allowTools = args[i].split(",").map((s) => s.trim()).filter(Boolean);
@@ -25797,16 +25816,21 @@ function parseStdioArgs(args) {
25797
25816
  denyTools = args[i].split(",").map((s) => s.trim()).filter(Boolean);
25798
25817
  }
25799
25818
  }
25800
- if (!configJson) {
25801
- logError("Missing --config argument");
25819
+ if (!configJson && !configFile) {
25820
+ logError("Missing --config or --config-file argument");
25802
25821
  logError(`Usage: nuwax-mcp-stdio-proxy --config '{"mcpServers":{...}}'`);
25822
+ logError(" or: nuwax-mcp-stdio-proxy --config-file /path/to/config.json");
25823
+ process.exit(1);
25824
+ }
25825
+ if (configJson && configFile) {
25826
+ logError("Cannot use both --config and --config-file");
25803
25827
  process.exit(1);
25804
25828
  }
25805
25829
  if (allowTools && denyTools) {
25806
25830
  logError("Cannot use both --allow-tools and --deny-tools");
25807
25831
  process.exit(1);
25808
25832
  }
25809
- const config2 = parseConfigJson(configJson);
25833
+ const config2 = configFile ? parseConfigFile(configFile) : parseConfigJson(configJson);
25810
25834
  return { mode: "stdio", config: config2, allowTools, denyTools };
25811
25835
  }
25812
25836
  function parseConvertArgs(args) {
@@ -25860,6 +25884,7 @@ function parseConvertArgs(args) {
25860
25884
  function parseProxyArgs(args) {
25861
25885
  let port;
25862
25886
  let config2;
25887
+ let configFile;
25863
25888
  for (let i = 0; i < args.length; i++) {
25864
25889
  const arg = args[i];
25865
25890
  if (arg === "--port" && i + 1 < args.length) {
@@ -25873,6 +25898,9 @@ function parseProxyArgs(args) {
25873
25898
  } else if (arg === "--config" && i + 1 < args.length) {
25874
25899
  i++;
25875
25900
  config2 = parseConfigJson(args[i]);
25901
+ } else if (arg === "--config-file" && i + 1 < args.length) {
25902
+ i++;
25903
+ configFile = args[i];
25876
25904
  } else {
25877
25905
  logError(`Unknown argument: "${arg}"`);
25878
25906
  printProxyUsage();
@@ -25884,8 +25912,11 @@ function parseProxyArgs(args) {
25884
25912
  printProxyUsage();
25885
25913
  process.exit(1);
25886
25914
  }
25915
+ if (configFile) {
25916
+ config2 = parseConfigFile(configFile);
25917
+ }
25887
25918
  if (!config2) {
25888
- logError("--config is required for proxy mode");
25919
+ logError("--config or --config-file is required for proxy mode");
25889
25920
  printProxyUsage();
25890
25921
  process.exit(1);
25891
25922
  }
@@ -25903,13 +25934,29 @@ function parseConfigJson(json2) {
25903
25934
  process.exit(1);
25904
25935
  }
25905
25936
  }
25937
+ function parseConfigFile(filePath) {
25938
+ try {
25939
+ const content = fs2.readFileSync(filePath, "utf-8");
25940
+ const config2 = JSON.parse(content);
25941
+ if (!config2.mcpServers || typeof config2.mcpServers !== "object") {
25942
+ throw new Error('config must contain a "mcpServers" object');
25943
+ }
25944
+ return config2;
25945
+ } catch (e) {
25946
+ logError(`Failed to read or parse config file "${filePath}": ${e}`);
25947
+ process.exit(1);
25948
+ }
25949
+ }
25906
25950
  function printUsage() {
25907
25951
  logError("Usage:");
25908
25952
  logError(` nuwax-mcp-stdio-proxy --config '{"mcpServers":{...}}' [OPTIONS] (stdio aggregation)`);
25953
+ logError(" nuwax-mcp-stdio-proxy --config-file <FILE> [OPTIONS] (stdio aggregation from file)");
25909
25954
  logError(" nuwax-mcp-stdio-proxy convert [URL] [OPTIONS] (remote \u2192 stdio)");
25910
25955
  logError(" nuwax-mcp-stdio-proxy proxy --port <PORT> --config '...' (HTTP server)");
25911
25956
  logError("");
25912
25957
  logError("Options (stdio / convert):");
25958
+ logError(" --config <JSON> MCP config JSON string");
25959
+ logError(" --config-file <FILE> MCP config JSON file path");
25913
25960
  logError(" --allow-tools <TOOLS> Tool whitelist (comma-separated)");
25914
25961
  logError(" --deny-tools <TOOLS> Tool blacklist (comma-separated)");
25915
25962
  }
@@ -25928,6 +25975,7 @@ function printConvertUsage() {
25928
25975
  }
25929
25976
  function printProxyUsage() {
25930
25977
  logError(`Usage: nuwax-mcp-stdio-proxy proxy --port <PORT> --config '{"mcpServers":{...}}'`);
25978
+ logError(" or: nuwax-mcp-stdio-proxy proxy --port <PORT> --config-file <FILE>");
25931
25979
  }
25932
25980
  async function main() {
25933
25981
  const args = parseCliArgs();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuwax-mcp-stdio-proxy",
3
- "version": "1.4.4",
3
+ "version": "1.4.6",
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": {