nuwax-mcp-stdio-proxy 1.3.0 → 1.4.0
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.d.ts +6 -1
- package/dist/bridge.js +9 -23
- package/dist/constants.d.ts +7 -0
- package/dist/constants.js +7 -0
- package/dist/detect.d.ts +14 -0
- package/dist/detect.js +95 -0
- package/dist/filter.d.ts +16 -0
- package/dist/filter.js +19 -0
- package/dist/index.d.ts +12 -15
- package/dist/index.js +5147 -2620
- package/dist/lib.d.ts +8 -2
- package/dist/lib.js +5 -1
- package/dist/modes/convert.d.ts +16 -0
- package/dist/modes/convert.js +117 -0
- package/dist/modes/proxy.d.ts +12 -0
- package/dist/modes/proxy.js +37 -0
- package/dist/modes/stdio.d.ts +8 -0
- package/dist/modes/stdio.js +95 -0
- package/dist/shared.d.ts +39 -0
- package/dist/shared.js +83 -0
- package/dist/transport.d.ts +21 -8
- package/dist/transport.js +50 -9
- package/dist/types.d.ts +29 -11
- package/dist/types.js +11 -4
- package/package.json +2 -2
package/dist/bridge.d.ts
CHANGED
|
@@ -32,8 +32,13 @@ export declare class PersistentMcpBridge {
|
|
|
32
32
|
constructor(logger?: BridgeLogger);
|
|
33
33
|
/**
|
|
34
34
|
* Start bridge: spawn child processes for each persistent server and create HTTP server
|
|
35
|
+
*
|
|
36
|
+
* @param servers - Map of server ID to stdio config
|
|
37
|
+
* @param options - Optional settings (e.g. port to listen on)
|
|
35
38
|
*/
|
|
36
|
-
start(servers: Record<string, StdioServerEntry
|
|
39
|
+
start(servers: Record<string, StdioServerEntry>, options?: {
|
|
40
|
+
port?: number;
|
|
41
|
+
}): Promise<void>;
|
|
37
42
|
/**
|
|
38
43
|
* Stop bridge: close HTTP, kill all child processes
|
|
39
44
|
*/
|
package/dist/bridge.js
CHANGED
|
@@ -40,8 +40,11 @@ export class PersistentMcpBridge {
|
|
|
40
40
|
}
|
|
41
41
|
/**
|
|
42
42
|
* Start bridge: spawn child processes for each persistent server and create HTTP server
|
|
43
|
+
*
|
|
44
|
+
* @param servers - Map of server ID to stdio config
|
|
45
|
+
* @param options - Optional settings (e.g. port to listen on)
|
|
43
46
|
*/
|
|
44
|
-
async start(servers) {
|
|
47
|
+
async start(servers, options) {
|
|
45
48
|
if (this.running) {
|
|
46
49
|
this.log.warn(`${LOG_TAG} Already running, stopping first`);
|
|
47
50
|
await this.stop();
|
|
@@ -52,7 +55,7 @@ export class PersistentMcpBridge {
|
|
|
52
55
|
await this.startServer(id, config);
|
|
53
56
|
}
|
|
54
57
|
// 2. Start HTTP server
|
|
55
|
-
await this.startHttpServer();
|
|
58
|
+
await this.startHttpServer(options?.port);
|
|
56
59
|
this.running = true;
|
|
57
60
|
// 3. Start periodic session cleanup
|
|
58
61
|
this.sessionCleanupTimer = setInterval(() => this.cleanupStaleSessions(), SESSION_CLEANUP_INTERVAL_MS);
|
|
@@ -227,8 +230,6 @@ export class PersistentMcpBridge {
|
|
|
227
230
|
}
|
|
228
231
|
entry.restarting = false;
|
|
229
232
|
entry.healthy = false;
|
|
230
|
-
// Capture PID before closing (transport.close() may invalidate the getter)
|
|
231
|
-
const pid = entry.transport?.pid;
|
|
232
233
|
try {
|
|
233
234
|
if (entry.client)
|
|
234
235
|
await entry.client.close();
|
|
@@ -236,7 +237,7 @@ export class PersistentMcpBridge {
|
|
|
236
237
|
catch (e) {
|
|
237
238
|
this.log.warn(`${LOG_TAG} Error closing client for "${id}":`, e);
|
|
238
239
|
}
|
|
239
|
-
// transport.close()
|
|
240
|
+
// transport.close() handles graceful shutdown: stdin.end → SIGTERM → SIGKILL
|
|
240
241
|
try {
|
|
241
242
|
if (entry.transport)
|
|
242
243
|
await entry.transport.close();
|
|
@@ -244,26 +245,11 @@ export class PersistentMcpBridge {
|
|
|
244
245
|
catch (e) {
|
|
245
246
|
this.log.warn(`${LOG_TAG} Error closing transport for "${id}":`, e);
|
|
246
247
|
}
|
|
247
|
-
// Force kill via PID if still alive after transport.close()
|
|
248
|
-
if (pid) {
|
|
249
|
-
try {
|
|
250
|
-
process.kill(pid, 0); // test if alive
|
|
251
|
-
process.kill(pid, 'SIGTERM');
|
|
252
|
-
setTimeout(() => {
|
|
253
|
-
try {
|
|
254
|
-
process.kill(pid, 0);
|
|
255
|
-
process.kill(pid, 'SIGKILL');
|
|
256
|
-
}
|
|
257
|
-
catch { /* already dead */ }
|
|
258
|
-
}, 2000);
|
|
259
|
-
}
|
|
260
|
-
catch { /* already dead */ }
|
|
261
|
-
}
|
|
262
248
|
entry.client = null;
|
|
263
249
|
entry.transport = null;
|
|
264
250
|
}
|
|
265
251
|
// ==================== Internal: HTTP Server ====================
|
|
266
|
-
async startHttpServer() {
|
|
252
|
+
async startHttpServer(listenPort) {
|
|
267
253
|
return new Promise((resolve, reject) => {
|
|
268
254
|
const server = http.createServer((req, res) => {
|
|
269
255
|
this.handleHttpRequest(req, res).catch((e) => {
|
|
@@ -274,8 +260,8 @@ export class PersistentMcpBridge {
|
|
|
274
260
|
}
|
|
275
261
|
});
|
|
276
262
|
});
|
|
277
|
-
// Listen on random port
|
|
278
|
-
server.listen(0, '127.0.0.1', () => {
|
|
263
|
+
// Listen on specified port or random port (0)
|
|
264
|
+
server.listen(listenPort ?? 0, '127.0.0.1', () => {
|
|
279
265
|
const addr = server.address();
|
|
280
266
|
if (addr && typeof addr === 'object') {
|
|
281
267
|
this.port = addr.port;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package constants — injected at build time by esbuild define (see build.mjs)
|
|
3
|
+
*
|
|
4
|
+
* Falls back to static values when running via tsc in development.
|
|
5
|
+
*/
|
|
6
|
+
export const PKG_NAME = process.env.__MCP_PROXY_PKG_NAME__ || 'nuwax-mcp-stdio-proxy';
|
|
7
|
+
export const PKG_VERSION = process.env.__MCP_PROXY_PKG_VERSION__ || '0.0.0-dev';
|
package/dist/detect.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Protocol auto-detection — determine whether a URL serves Streamable HTTP or SSE
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Detect the MCP transport protocol of a remote URL.
|
|
6
|
+
*
|
|
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.
|
|
13
|
+
*/
|
|
14
|
+
export declare function detectProtocol(url: string, headers?: Record<string, string>): Promise<'sse' | 'stream'>;
|
package/dist/detect.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Protocol auto-detection — determine whether a URL serves Streamable HTTP or SSE
|
|
3
|
+
*/
|
|
4
|
+
import { logInfo, logWarn } from './logger.js';
|
|
5
|
+
/**
|
|
6
|
+
* Detect the MCP transport protocol of a remote URL.
|
|
7
|
+
*
|
|
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.
|
|
14
|
+
*/
|
|
15
|
+
export async function detectProtocol(url, headers) {
|
|
16
|
+
logInfo(`Auto-detecting protocol for ${url}...`);
|
|
17
|
+
// 1. Try Streamable HTTP — POST a JSON-RPC initialize request
|
|
18
|
+
try {
|
|
19
|
+
const reqHeaders = {
|
|
20
|
+
'Content-Type': 'application/json',
|
|
21
|
+
Accept: 'application/json, text/event-stream',
|
|
22
|
+
...headers,
|
|
23
|
+
};
|
|
24
|
+
const controller = new AbortController();
|
|
25
|
+
const timeout = setTimeout(() => controller.abort(), 10_000);
|
|
26
|
+
const res = await fetch(url, {
|
|
27
|
+
method: 'POST',
|
|
28
|
+
headers: reqHeaders,
|
|
29
|
+
body: JSON.stringify({
|
|
30
|
+
jsonrpc: '2.0',
|
|
31
|
+
id: 1,
|
|
32
|
+
method: 'initialize',
|
|
33
|
+
params: {
|
|
34
|
+
protocolVersion: '2025-03-26',
|
|
35
|
+
capabilities: {},
|
|
36
|
+
clientInfo: { name: 'nuwax-mcp-detect', version: '1.0.0' },
|
|
37
|
+
},
|
|
38
|
+
}),
|
|
39
|
+
signal: controller.signal,
|
|
40
|
+
});
|
|
41
|
+
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
|
|
46
|
+
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
|
+
return 'stream';
|
|
59
|
+
}
|
|
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
|
+
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}`);
|
|
83
|
+
controller.abort();
|
|
84
|
+
return 'sse';
|
|
85
|
+
}
|
|
86
|
+
clearTimeout(timeout);
|
|
87
|
+
await res.text().catch(() => { });
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
// SSE probe failed
|
|
91
|
+
}
|
|
92
|
+
// 3. Default to streamable-http
|
|
93
|
+
logWarn(`Could not auto-detect protocol for ${url}, defaulting to streamable-http`);
|
|
94
|
+
return 'stream';
|
|
95
|
+
}
|
package/dist/filter.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool filtering — whitelist/blacklist tools by name
|
|
3
|
+
*/
|
|
4
|
+
import type { Tool } from '@modelcontextprotocol/sdk/types.js';
|
|
5
|
+
export interface ToolFilter {
|
|
6
|
+
allowTools?: Set<string>;
|
|
7
|
+
denyTools?: Set<string>;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Filter tools by allow/deny lists.
|
|
11
|
+
* - If allowTools is set, only tools in the set are returned.
|
|
12
|
+
* - If denyTools is set, tools in the set are excluded.
|
|
13
|
+
* - If both are set, allowTools takes precedence.
|
|
14
|
+
* - If neither is set, all tools are returned unchanged.
|
|
15
|
+
*/
|
|
16
|
+
export declare function filterTools(tools: Tool[], filter: ToolFilter): Tool[];
|
package/dist/filter.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool filtering — whitelist/blacklist tools by name
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Filter tools by allow/deny lists.
|
|
6
|
+
* - If allowTools is set, only tools in the set are returned.
|
|
7
|
+
* - If denyTools is set, tools in the set are excluded.
|
|
8
|
+
* - If both are set, allowTools takes precedence.
|
|
9
|
+
* - If neither is set, all tools are returned unchanged.
|
|
10
|
+
*/
|
|
11
|
+
export function filterTools(tools, filter) {
|
|
12
|
+
if (filter.allowTools && filter.allowTools.size > 0) {
|
|
13
|
+
return tools.filter((t) => filter.allowTools.has(t.name));
|
|
14
|
+
}
|
|
15
|
+
if (filter.denyTools && filter.denyTools.size > 0) {
|
|
16
|
+
return tools.filter((t) => !filter.denyTools.has(t.name));
|
|
17
|
+
}
|
|
18
|
+
return tools;
|
|
19
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,22 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* nuwax-mcp-stdio-proxy
|
|
2
|
+
* nuwax-mcp-stdio-proxy — CLI entry point
|
|
3
3
|
*
|
|
4
|
-
* A pure TypeScript
|
|
5
|
-
* into a single MCP server endpoint. Supports two upstream transport types:
|
|
4
|
+
* A pure TypeScript MCP proxy with three operating modes:
|
|
6
5
|
*
|
|
7
|
-
*
|
|
8
|
-
* -
|
|
6
|
+
* 1. **Default (stdio aggregation)**: Aggregates multiple MCP servers
|
|
7
|
+
* (stdio + streamable-http + SSE) into a single stdio endpoint.
|
|
8
|
+
* Usage: nuwax-mcp-stdio-proxy --config '{"mcpServers":{...}}'
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* "local": {"command":"npx","args":["-y","some-mcp"]},
|
|
13
|
-
* "remote": {"url":"http://127.0.0.1:8080/mcp/name"} // bridge to persistent server
|
|
14
|
-
* }}'
|
|
10
|
+
* 2. **convert**: Connects to a single remote MCP service and exposes via stdio.
|
|
11
|
+
* Usage: nuwax-mcp-stdio-proxy convert [URL] [OPTIONS]
|
|
15
12
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
13
|
+
* 3. **proxy**: Starts PersistentMcpBridge as a Streamable HTTP server.
|
|
14
|
+
* Usage: nuwax-mcp-stdio-proxy proxy --port 18099 --config '{"mcpServers":{...}}'
|
|
15
|
+
*
|
|
16
|
+
* This file handles CLI argument parsing and routes to the appropriate mode.
|
|
17
|
+
* Mode implementations live in modes/*.ts.
|
|
21
18
|
*/
|
|
22
19
|
export {};
|