nuwax-mcp-stdio-proxy 1.4.3 → 1.4.5

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
@@ -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}
@@ -6809,12 +6812,13 @@ var logError = (msg) => log("ERROR", msg);
6809
6812
 
6810
6813
  // src/types.ts
6811
6814
  function isSseEntry(entry) {
6812
- if (!("url" in entry)) return false;
6813
- const e = entry;
6814
- return e.transport === "sse" || !e.transport && /\/sse(?:\?|$)/i.test(e.url);
6815
+ return "url" in entry && entry.transport === "sse";
6815
6816
  }
6816
6817
  function isStreamableEntry(entry) {
6817
- return "url" in entry && typeof entry.url === "string" && !isSseEntry(entry);
6818
+ return "url" in entry && typeof entry.url === "string" && entry.transport === "streamable-http";
6819
+ }
6820
+ function needsProtocolDetection(entry) {
6821
+ return "url" in entry && typeof entry.url === "string" && !isSseEntry(entry) && !isStreamableEntry(entry);
6818
6822
  }
6819
6823
 
6820
6824
  // node_modules/zod/v3/helpers/util.js
@@ -23763,7 +23767,7 @@ var StdioServerTransport = class {
23763
23767
 
23764
23768
  // src/constants.ts
23765
23769
  var PKG_NAME = "nuwax-mcp-stdio-proxy";
23766
- var PKG_VERSION = "1.4.3";
23770
+ var PKG_VERSION = "1.4.5";
23767
23771
 
23768
23772
  // src/shared.ts
23769
23773
  async function discoverTools(client) {
@@ -23838,6 +23842,81 @@ function filterTools(tools, filter) {
23838
23842
  return tools;
23839
23843
  }
23840
23844
 
23845
+ // src/detect.ts
23846
+ async function detectProtocol(url2, headers) {
23847
+ logInfo(`Auto-detecting protocol for ${url2}...`);
23848
+ try {
23849
+ const reqHeaders = {
23850
+ "Content-Type": "application/json",
23851
+ Accept: "application/json, text/event-stream",
23852
+ ...headers
23853
+ };
23854
+ const controller = new AbortController();
23855
+ const timeout = setTimeout(() => controller.abort(), 1e4);
23856
+ const res = await fetch(url2, {
23857
+ method: "POST",
23858
+ headers: reqHeaders,
23859
+ body: JSON.stringify({
23860
+ jsonrpc: "2.0",
23861
+ id: 1,
23862
+ method: "initialize",
23863
+ params: {
23864
+ protocolVersion: "2025-03-26",
23865
+ capabilities: {},
23866
+ clientInfo: { name: "nuwax-mcp-detect", version: "1.0.0" }
23867
+ }
23868
+ }),
23869
+ signal: controller.signal
23870
+ });
23871
+ clearTimeout(timeout);
23872
+ const ct = res.headers.get("content-type") || "";
23873
+ if (res.ok && (ct.includes("application/json") || ct.includes("text/event-stream"))) {
23874
+ logInfo(`Detected streamable-http protocol for ${url2}`);
23875
+ await res.text().catch(() => {
23876
+ });
23877
+ const sessionId = res.headers.get("mcp-session-id");
23878
+ if (sessionId) {
23879
+ fetch(url2, {
23880
+ method: "DELETE",
23881
+ headers: { "mcp-session-id": sessionId, ...headers },
23882
+ signal: AbortSignal.timeout(5e3)
23883
+ }).catch(() => {
23884
+ });
23885
+ }
23886
+ return "stream";
23887
+ }
23888
+ await res.text().catch(() => {
23889
+ });
23890
+ } catch {
23891
+ }
23892
+ try {
23893
+ const reqHeaders = {
23894
+ Accept: "text/event-stream",
23895
+ ...headers
23896
+ };
23897
+ const controller = new AbortController();
23898
+ const timeout = setTimeout(() => controller.abort(), 1e4);
23899
+ const res = await fetch(url2, {
23900
+ method: "GET",
23901
+ headers: reqHeaders,
23902
+ signal: controller.signal
23903
+ });
23904
+ const ct = res.headers.get("content-type") || "";
23905
+ if (ct.includes("text/event-stream")) {
23906
+ clearTimeout(timeout);
23907
+ logInfo(`Detected SSE protocol for ${url2}`);
23908
+ controller.abort();
23909
+ return "sse";
23910
+ }
23911
+ clearTimeout(timeout);
23912
+ await res.text().catch(() => {
23913
+ });
23914
+ } catch {
23915
+ }
23916
+ logWarn(`Could not auto-detect protocol for ${url2}, defaulting to streamable-http`);
23917
+ return "stream";
23918
+ }
23919
+
23841
23920
  // src/modes/stdio.ts
23842
23921
  async function runStdio(config2, allowTools, denyTools) {
23843
23922
  const entries = Object.entries(config2.mcpServers);
@@ -23861,6 +23940,13 @@ async function runStdio(config2, allowTools, denyTools) {
23861
23940
  connected = await connectSse(id, entry);
23862
23941
  } else if (isStreamableEntry(entry)) {
23863
23942
  connected = await connectStreamable(id, entry);
23943
+ } else if (needsProtocolDetection(entry)) {
23944
+ const detected = await detectProtocol(entry.url, buildRequestHeaders(entry));
23945
+ if (detected === "sse") {
23946
+ connected = await connectSse(id, { ...entry, transport: "sse" });
23947
+ } else {
23948
+ connected = await connectStreamable(id, entry);
23949
+ }
23864
23950
  } else {
23865
23951
  connected = await connectStdio(id, entry, baseEnv);
23866
23952
  }
@@ -23938,81 +24024,6 @@ async function runStdio(config2, allowTools, denyTools) {
23938
24024
  });
23939
24025
  }
23940
24026
 
23941
- // src/detect.ts
23942
- async function detectProtocol(url2, headers) {
23943
- logInfo(`Auto-detecting protocol for ${url2}...`);
23944
- try {
23945
- const reqHeaders = {
23946
- "Content-Type": "application/json",
23947
- Accept: "application/json, text/event-stream",
23948
- ...headers
23949
- };
23950
- const controller = new AbortController();
23951
- const timeout = setTimeout(() => controller.abort(), 1e4);
23952
- const res = await fetch(url2, {
23953
- method: "POST",
23954
- headers: reqHeaders,
23955
- body: JSON.stringify({
23956
- jsonrpc: "2.0",
23957
- id: 1,
23958
- method: "initialize",
23959
- params: {
23960
- protocolVersion: "2025-03-26",
23961
- capabilities: {},
23962
- clientInfo: { name: "nuwax-mcp-detect", version: "1.0.0" }
23963
- }
23964
- }),
23965
- signal: controller.signal
23966
- });
23967
- clearTimeout(timeout);
23968
- const ct = res.headers.get("content-type") || "";
23969
- if (res.ok && (ct.includes("application/json") || ct.includes("text/event-stream"))) {
23970
- logInfo(`Detected streamable-http protocol for ${url2}`);
23971
- await res.text().catch(() => {
23972
- });
23973
- const sessionId = res.headers.get("mcp-session-id");
23974
- if (sessionId) {
23975
- fetch(url2, {
23976
- method: "DELETE",
23977
- headers: { "mcp-session-id": sessionId, ...headers },
23978
- signal: AbortSignal.timeout(5e3)
23979
- }).catch(() => {
23980
- });
23981
- }
23982
- return "stream";
23983
- }
23984
- await res.text().catch(() => {
23985
- });
23986
- } catch {
23987
- }
23988
- try {
23989
- const reqHeaders = {
23990
- Accept: "text/event-stream",
23991
- ...headers
23992
- };
23993
- const controller = new AbortController();
23994
- const timeout = setTimeout(() => controller.abort(), 1e4);
23995
- const res = await fetch(url2, {
23996
- method: "GET",
23997
- headers: reqHeaders,
23998
- signal: controller.signal
23999
- });
24000
- const ct = res.headers.get("content-type") || "";
24001
- if (ct.includes("text/event-stream")) {
24002
- clearTimeout(timeout);
24003
- logInfo(`Detected SSE protocol for ${url2}`);
24004
- controller.abort();
24005
- return "sse";
24006
- }
24007
- clearTimeout(timeout);
24008
- await res.text().catch(() => {
24009
- });
24010
- } catch {
24011
- }
24012
- logWarn(`Could not auto-detect protocol for ${url2}, defaulting to streamable-http`);
24013
- return "stream";
24014
- }
24015
-
24016
24027
  // src/modes/convert.ts
24017
24028
  async function runConvert(args) {
24018
24029
  let targetUrl;
@@ -25774,6 +25785,7 @@ function parseCliArgs() {
25774
25785
  }
25775
25786
  function parseStdioArgs(args) {
25776
25787
  let configJson;
25788
+ let configFile;
25777
25789
  let allowTools;
25778
25790
  let denyTools;
25779
25791
  for (let i = 0; i < args.length; i++) {
@@ -25781,6 +25793,9 @@ function parseStdioArgs(args) {
25781
25793
  if (arg === "--config" && i + 1 < args.length) {
25782
25794
  i++;
25783
25795
  configJson = args[i];
25796
+ } else if (arg === "--config-file" && i + 1 < args.length) {
25797
+ i++;
25798
+ configFile = args[i];
25784
25799
  } else if (arg === "--allow-tools" && i + 1 < args.length) {
25785
25800
  i++;
25786
25801
  allowTools = args[i].split(",").map((s) => s.trim()).filter(Boolean);
@@ -25789,16 +25804,21 @@ function parseStdioArgs(args) {
25789
25804
  denyTools = args[i].split(",").map((s) => s.trim()).filter(Boolean);
25790
25805
  }
25791
25806
  }
25792
- if (!configJson) {
25793
- logError("Missing --config argument");
25807
+ if (!configJson && !configFile) {
25808
+ logError("Missing --config or --config-file argument");
25794
25809
  logError(`Usage: nuwax-mcp-stdio-proxy --config '{"mcpServers":{...}}'`);
25810
+ logError(" or: nuwax-mcp-stdio-proxy --config-file /path/to/config.json");
25811
+ process.exit(1);
25812
+ }
25813
+ if (configJson && configFile) {
25814
+ logError("Cannot use both --config and --config-file");
25795
25815
  process.exit(1);
25796
25816
  }
25797
25817
  if (allowTools && denyTools) {
25798
25818
  logError("Cannot use both --allow-tools and --deny-tools");
25799
25819
  process.exit(1);
25800
25820
  }
25801
- const config2 = parseConfigJson(configJson);
25821
+ const config2 = configFile ? parseConfigFile(configFile) : parseConfigJson(configJson);
25802
25822
  return { mode: "stdio", config: config2, allowTools, denyTools };
25803
25823
  }
25804
25824
  function parseConvertArgs(args) {
@@ -25852,6 +25872,7 @@ function parseConvertArgs(args) {
25852
25872
  function parseProxyArgs(args) {
25853
25873
  let port;
25854
25874
  let config2;
25875
+ let configFile;
25855
25876
  for (let i = 0; i < args.length; i++) {
25856
25877
  const arg = args[i];
25857
25878
  if (arg === "--port" && i + 1 < args.length) {
@@ -25865,6 +25886,9 @@ function parseProxyArgs(args) {
25865
25886
  } else if (arg === "--config" && i + 1 < args.length) {
25866
25887
  i++;
25867
25888
  config2 = parseConfigJson(args[i]);
25889
+ } else if (arg === "--config-file" && i + 1 < args.length) {
25890
+ i++;
25891
+ configFile = args[i];
25868
25892
  } else {
25869
25893
  logError(`Unknown argument: "${arg}"`);
25870
25894
  printProxyUsage();
@@ -25876,8 +25900,11 @@ function parseProxyArgs(args) {
25876
25900
  printProxyUsage();
25877
25901
  process.exit(1);
25878
25902
  }
25903
+ if (configFile) {
25904
+ config2 = parseConfigFile(configFile);
25905
+ }
25879
25906
  if (!config2) {
25880
- logError("--config is required for proxy mode");
25907
+ logError("--config or --config-file is required for proxy mode");
25881
25908
  printProxyUsage();
25882
25909
  process.exit(1);
25883
25910
  }
@@ -25895,13 +25922,29 @@ function parseConfigJson(json2) {
25895
25922
  process.exit(1);
25896
25923
  }
25897
25924
  }
25925
+ function parseConfigFile(filePath) {
25926
+ try {
25927
+ const content = fs2.readFileSync(filePath, "utf-8");
25928
+ const config2 = JSON.parse(content);
25929
+ if (!config2.mcpServers || typeof config2.mcpServers !== "object") {
25930
+ throw new Error('config must contain a "mcpServers" object');
25931
+ }
25932
+ return config2;
25933
+ } catch (e) {
25934
+ logError(`Failed to read or parse config file "${filePath}": ${e}`);
25935
+ process.exit(1);
25936
+ }
25937
+ }
25898
25938
  function printUsage() {
25899
25939
  logError("Usage:");
25900
25940
  logError(` nuwax-mcp-stdio-proxy --config '{"mcpServers":{...}}' [OPTIONS] (stdio aggregation)`);
25941
+ logError(" nuwax-mcp-stdio-proxy --config-file <FILE> [OPTIONS] (stdio aggregation from file)");
25901
25942
  logError(" nuwax-mcp-stdio-proxy convert [URL] [OPTIONS] (remote \u2192 stdio)");
25902
25943
  logError(" nuwax-mcp-stdio-proxy proxy --port <PORT> --config '...' (HTTP server)");
25903
25944
  logError("");
25904
25945
  logError("Options (stdio / convert):");
25946
+ logError(" --config <JSON> MCP config JSON string");
25947
+ logError(" --config-file <FILE> MCP config JSON file path");
25905
25948
  logError(" --allow-tools <TOOLS> Tool whitelist (comma-separated)");
25906
25949
  logError(" --deny-tools <TOOLS> Tool blacklist (comma-separated)");
25907
25950
  }
@@ -25920,6 +25963,7 @@ function printConvertUsage() {
25920
25963
  }
25921
25964
  function printProxyUsage() {
25922
25965
  logError(`Usage: nuwax-mcp-stdio-proxy proxy --port <PORT> --config '{"mcpServers":{...}}'`);
25966
+ logError(" or: nuwax-mcp-stdio-proxy proxy --port <PORT> --config-file <FILE>");
25923
25967
  }
25924
25968
  async function main() {
25925
25969
  const args = parseCliArgs();
@@ -4,11 +4,12 @@
4
4
  * Aggregates multiple MCP servers (stdio + streamable-http + SSE)
5
5
  * into a single stdio MCP endpoint.
6
6
  */
7
- import { isSseEntry, isStreamableEntry } from '../types.js';
7
+ import { isSseEntry, isStreamableEntry, needsProtocolDetection } from '../types.js';
8
8
  import { logInfo, logWarn, logError } from '../logger.js';
9
- import { buildBaseEnv, connectStdio, connectStreamable, connectSse } from '../transport.js';
9
+ import { buildBaseEnv, connectStdio, connectStreamable, connectSse, buildRequestHeaders } from '../transport.js';
10
10
  import { discoverTools, createToolProxyServer, setupGracefulShutdown } from '../shared.js';
11
11
  import { filterTools } from '../filter.js';
12
+ import { detectProtocol } from '../detect.js';
12
13
  export async function runStdio(config, allowTools, denyTools) {
13
14
  const entries = Object.entries(config.mcpServers);
14
15
  if (entries.length === 0) {
@@ -32,6 +33,16 @@ export async function runStdio(config, allowTools, denyTools) {
32
33
  else if (isStreamableEntry(entry)) {
33
34
  connected = await connectStreamable(id, entry);
34
35
  }
36
+ else if (needsProtocolDetection(entry)) {
37
+ // No explicit transport — probe the URL to determine protocol
38
+ const detected = await detectProtocol(entry.url, buildRequestHeaders(entry));
39
+ if (detected === 'sse') {
40
+ connected = await connectSse(id, { ...entry, transport: 'sse' });
41
+ }
42
+ else {
43
+ connected = await connectStreamable(id, entry);
44
+ }
45
+ }
35
46
  else {
36
47
  connected = await connectStdio(id, entry, baseEnv);
37
48
  }
package/dist/types.d.ts CHANGED
@@ -45,6 +45,8 @@ export interface SseServerEntry {
45
45
  export type McpServerEntry = StdioServerEntry | StreamableServerEntry | SseServerEntry;
46
46
  export declare function isSseEntry(entry: McpServerEntry): entry is SseServerEntry;
47
47
  export declare function isStreamableEntry(entry: McpServerEntry): entry is StreamableServerEntry;
48
+ /** Check if URL entry has no explicit transport → needs auto-detection */
49
+ export declare function needsProtocolDetection(entry: McpServerEntry): entry is StreamableServerEntry;
48
50
  /** @deprecated Use StreamableServerEntry instead */
49
51
  export type BridgeServerEntry = StreamableServerEntry;
50
52
  /** @deprecated Use StreamableServerEntry instead */
package/dist/types.js CHANGED
@@ -2,16 +2,19 @@
2
2
  * Types for MCP server configuration
3
3
  */
4
4
  export function isSseEntry(entry) {
5
- if (!('url' in entry))
6
- return false;
7
- const e = entry;
8
- // Explicit transport: 'sse' or URL path contains /sse (auto-detect)
9
- return e.transport === 'sse' || (!e.transport && /\/sse(?:\?|$)/i.test(e.url));
5
+ return 'url' in entry && entry.transport === 'sse';
10
6
  }
11
7
  export function isStreamableEntry(entry) {
12
8
  return ('url' in entry &&
13
9
  typeof entry.url === 'string' &&
14
- !isSseEntry(entry));
10
+ entry.transport === 'streamable-http');
11
+ }
12
+ /** Check if URL entry has no explicit transport → needs auto-detection */
13
+ export function needsProtocolDetection(entry) {
14
+ return ('url' in entry &&
15
+ typeof entry.url === 'string' &&
16
+ !isSseEntry(entry) &&
17
+ !isStreamableEntry(entry));
15
18
  }
16
19
  /** @deprecated Use isStreamableEntry instead */
17
20
  export const isBridgeEntry = isStreamableEntry;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuwax-mcp-stdio-proxy",
3
- "version": "1.4.3",
3
+ "version": "1.4.5",
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": {