nuwax-mcp-stdio-proxy 1.3.1 → 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 +8 -5
- 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);
|
|
@@ -246,7 +249,7 @@ export class PersistentMcpBridge {
|
|
|
246
249
|
entry.transport = null;
|
|
247
250
|
}
|
|
248
251
|
// ==================== Internal: HTTP Server ====================
|
|
249
|
-
async startHttpServer() {
|
|
252
|
+
async startHttpServer(listenPort) {
|
|
250
253
|
return new Promise((resolve, reject) => {
|
|
251
254
|
const server = http.createServer((req, res) => {
|
|
252
255
|
this.handleHttpRequest(req, res).catch((e) => {
|
|
@@ -257,8 +260,8 @@ export class PersistentMcpBridge {
|
|
|
257
260
|
}
|
|
258
261
|
});
|
|
259
262
|
});
|
|
260
|
-
// Listen on random port
|
|
261
|
-
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', () => {
|
|
262
265
|
const addr = server.address();
|
|
263
266
|
if (addr && typeof addr === 'object') {
|
|
264
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 {};
|