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/bridge.js
CHANGED
|
@@ -177,8 +177,7 @@ export class PersistentMcpBridge {
|
|
|
177
177
|
}
|
|
178
178
|
return t;
|
|
179
179
|
},
|
|
180
|
-
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.
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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.
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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(),
|
|
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
|
-
|
|
43
|
-
if (res.
|
|
44
|
-
logInfo(`Detected streamable-http protocol for ${url}`);
|
|
45
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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 '
|
|
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
|
-
//
|
|
86
|
+
// Probe failed (timeout, connection refused, etc.)
|
|
91
87
|
}
|
|
92
|
-
//
|
|
93
|
-
logWarn(`Could not
|
|
94
|
-
return '
|
|
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
|
}
|