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 CHANGED
@@ -177,8 +177,7 @@ export class PersistentMcpBridge {
177
177
  }
178
178
  return t;
179
179
  },
180
- pingIntervalMs: entry.config.pingIntervalMs,
181
- pingTimeoutMs: entry.config.pingTimeoutMs,
180
+ pingIntervalMs: 0, // No heartbeat for stdio — child process close/error events handle detection
182
181
  });
183
182
  const client = new Client({ name: 'nuwax-persistent-bridge', version: '1.0.0' }, { capabilities: {} });
184
183
  // Handle transport close → auto restart
@@ -195,10 +194,6 @@ export class PersistentMcpBridge {
195
194
  // Connect client to transport (this starts the subprocess)
196
195
  await wrapper.start();
197
196
  await client.connect(wrapper);
198
- // Enable heartbeat monitoring AFTER the MCP initialize handshake
199
- // completes — sending pings before initialize causes "Server not
200
- // initialized" errors from the MCP server.
201
- wrapper.enableHeartbeat();
202
197
  entry.client = client;
203
198
  entry.transport = wrapper;
204
199
  // List tools (cached)
package/dist/detect.d.ts CHANGED
@@ -1,14 +1,20 @@
1
1
  /**
2
2
  * Protocol auto-detection — determine whether a URL serves Streamable HTTP or SSE
3
+ *
4
+ * Aligned with workspace/mcp-proxy (Rust) detection logic:
5
+ * - Only probe Streamable HTTP (POST initialize)
6
+ * - Default to SSE if probe fails
3
7
  */
4
8
  /**
5
9
  * Detect the MCP transport protocol of a remote URL.
6
10
  *
7
- * Strategy:
8
- * 1. Try Streamable HTTP first: send a JSON-RPC initialize POST.
9
- * If the server responds with 200 and JSON, it's streamable-http.
10
- * Then clean up the orphan session via DELETE.
11
- * 2. If that fails, try SSE: send a GET and check for text/event-stream content-type.
12
- * 3. Default to 'stream' (Streamable HTTP) if both probes fail.
11
+ * Strategy (matches Rust mcp-proxy):
12
+ * 1. Send a JSON-RPC initialize POST to probe for Streamable HTTP.
13
+ * 2. Check 4 criteria any match means streamable-http:
14
+ * a. Response has `mcp-session-id` header
15
+ * b. Content-Type is `text/event-stream` with 2xx status
16
+ * c. Response body is valid JSON-RPC 2.0
17
+ * d. Status is 406 Not Acceptable
18
+ * 3. If probe fails or no criteria match → default to SSE.
13
19
  */
14
20
  export declare function detectProtocol(url: string, headers?: Record<string, string>): Promise<'sse' | 'stream'>;
package/dist/detect.js CHANGED
@@ -1,20 +1,25 @@
1
1
  /**
2
2
  * Protocol auto-detection — determine whether a URL serves Streamable HTTP or SSE
3
+ *
4
+ * Aligned with workspace/mcp-proxy (Rust) detection logic:
5
+ * - Only probe Streamable HTTP (POST initialize)
6
+ * - Default to SSE if probe fails
3
7
  */
4
8
  import { logInfo, logWarn } from './logger.js';
5
9
  /**
6
10
  * Detect the MCP transport protocol of a remote URL.
7
11
  *
8
- * Strategy:
9
- * 1. Try Streamable HTTP first: send a JSON-RPC initialize POST.
10
- * If the server responds with 200 and JSON, it's streamable-http.
11
- * Then clean up the orphan session via DELETE.
12
- * 2. If that fails, try SSE: send a GET and check for text/event-stream content-type.
13
- * 3. Default to 'stream' (Streamable HTTP) if both probes fail.
12
+ * Strategy (matches Rust mcp-proxy):
13
+ * 1. Send a JSON-RPC initialize POST to probe for Streamable HTTP.
14
+ * 2. Check 4 criteria any match means streamable-http:
15
+ * a. Response has `mcp-session-id` header
16
+ * b. Content-Type is `text/event-stream` with 2xx status
17
+ * c. Response body is valid JSON-RPC 2.0
18
+ * d. Status is 406 Not Acceptable
19
+ * 3. If probe fails or no criteria match → default to SSE.
14
20
  */
15
21
  export async function detectProtocol(url, headers) {
16
22
  logInfo(`Auto-detecting protocol for ${url}...`);
17
- // 1. Try Streamable HTTP — POST a JSON-RPC initialize request
18
23
  try {
19
24
  const reqHeaders = {
20
25
  'Content-Type': 'application/json',
@@ -22,7 +27,7 @@ export async function detectProtocol(url, headers) {
22
27
  ...headers,
23
28
  };
24
29
  const controller = new AbortController();
25
- const timeout = setTimeout(() => controller.abort(), 10_000);
30
+ const timeout = setTimeout(() => controller.abort(), 5_000);
26
31
  const res = await fetch(url, {
27
32
  method: 'POST',
28
33
  headers: reqHeaders,
@@ -39,57 +44,61 @@ export async function detectProtocol(url, headers) {
39
44
  signal: controller.signal,
40
45
  });
41
46
  clearTimeout(timeout);
42
- const ct = res.headers.get('content-type') || '';
43
- if (res.ok && (ct.includes('application/json') || ct.includes('text/event-stream'))) {
44
- logInfo(`Detected streamable-http protocol for ${url}`);
45
- // Consume body to avoid socket hang
47
+ // Check 1: mcp-session-id header (definitive Streamable HTTP marker)
48
+ if (res.headers.get('mcp-session-id')) {
49
+ logInfo(`Detected streamable-http protocol for ${url} (mcp-session-id header)`);
50
+ cleanupSession(url, res.headers.get('mcp-session-id'), headers);
46
51
  await res.text().catch(() => { });
47
- // Clean up orphan session — fire-and-forget DELETE so the server
48
- // can discard the half-initialized session we created during probing.
49
- // Not awaited: cleanup is best-effort and must not block detection.
50
- const sessionId = res.headers.get('mcp-session-id');
51
- if (sessionId) {
52
- fetch(url, {
53
- method: 'DELETE',
54
- headers: { 'mcp-session-id': sessionId, ...headers },
55
- signal: AbortSignal.timeout(5_000),
56
- }).catch(() => { });
57
- }
58
52
  return 'stream';
59
53
  }
60
- await res.text().catch(() => { });
61
- }
62
- catch {
63
- // Streamable HTTP probe failed, try SSE
64
- }
65
- // 2. Try SSE — GET and check for event-stream
66
- try {
67
- const reqHeaders = {
68
- Accept: 'text/event-stream',
69
- ...headers,
70
- };
71
- const controller = new AbortController();
72
- const timeout = setTimeout(() => controller.abort(), 10_000);
73
- const res = await fetch(url, {
74
- method: 'GET',
75
- headers: reqHeaders,
76
- signal: controller.signal,
77
- });
78
54
  const ct = res.headers.get('content-type') || '';
79
- if (ct.includes('text/event-stream')) {
80
- // Detected SSE — abort the stream before clearing the timeout
81
- clearTimeout(timeout);
82
- logInfo(`Detected SSE protocol for ${url}`);
55
+ // Check 2: text/event-stream content-type with success status
56
+ if (ct.includes('text/event-stream') && res.ok) {
57
+ logInfo(`Detected streamable-http protocol for ${url} (event-stream response)`);
58
+ cleanupSession(url, res.headers.get('mcp-session-id'), headers);
59
+ // Abort the stream to free the connection
83
60
  controller.abort();
84
- return 'sse';
61
+ return 'stream';
62
+ }
63
+ // Read body for JSON-RPC check
64
+ let bodyText = '';
65
+ try {
66
+ bodyText = await res.text();
67
+ }
68
+ catch { /* ignore */ }
69
+ // Check 3: valid JSON-RPC 2.0 response
70
+ try {
71
+ const json = JSON.parse(bodyText);
72
+ if (json && json.jsonrpc === '2.0') {
73
+ logInfo(`Detected streamable-http protocol for ${url} (JSON-RPC 2.0 response)`);
74
+ cleanupSession(url, res.headers.get('mcp-session-id'), headers);
75
+ return 'stream';
76
+ }
77
+ }
78
+ catch { /* not JSON */ }
79
+ // Check 4: 406 Not Acceptable (may indicate Streamable HTTP)
80
+ if (res.status === 406) {
81
+ logInfo(`Detected streamable-http protocol for ${url} (406 Not Acceptable)`);
82
+ return 'stream';
85
83
  }
86
- clearTimeout(timeout);
87
- await res.text().catch(() => { });
88
84
  }
89
85
  catch {
90
- // SSE probe failed
86
+ // Probe failed (timeout, connection refused, etc.)
91
87
  }
92
- // 3. Default to streamable-http
93
- logWarn(`Could not auto-detect protocol for ${url}, defaulting to streamable-http`);
94
- return 'stream';
88
+ // Default to SSE (matches Rust mcp-proxy behavior)
89
+ logWarn(`Could not detect streamable-http for ${url}, defaulting to SSE`);
90
+ return 'sse';
91
+ }
92
+ /**
93
+ * Clean up orphan session — fire-and-forget DELETE so the server
94
+ * can discard the half-initialized session we created during probing.
95
+ */
96
+ function cleanupSession(url, sessionId, headers) {
97
+ if (!sessionId)
98
+ return;
99
+ fetch(url, {
100
+ method: 'DELETE',
101
+ headers: { 'mcp-session-id': sessionId, ...headers },
102
+ signal: AbortSignal.timeout(5_000),
103
+ }).catch(() => { });
95
104
  }