nuwax-mcp-stdio-proxy 1.0.0 → 1.1.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/index.d.ts +10 -5
- package/dist/index.js +33 -65
- package/dist/logger.d.ts +7 -0
- package/dist/logger.js +9 -0
- package/dist/transport.d.ts +26 -0
- package/dist/transport.js +75 -0
- package/dist/types.d.ts +28 -0
- package/dist/types.js +8 -0
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -2,17 +2,22 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* nuwax-mcp-stdio-proxy
|
|
4
4
|
*
|
|
5
|
-
* A pure TypeScript stdio MCP proxy that aggregates multiple
|
|
6
|
-
* into a single MCP server endpoint.
|
|
7
|
-
*
|
|
5
|
+
* A pure TypeScript stdio MCP proxy that aggregates multiple MCP servers
|
|
6
|
+
* into a single MCP server endpoint. Supports two upstream transport types:
|
|
7
|
+
*
|
|
8
|
+
* - **stdio**: Spawns child MCP server processes (StdioClientTransport)
|
|
9
|
+
* - **bridge**: Connects to persistent MCP Bridge servers via StreamableHTTP (StreamableHTTPClientTransport)
|
|
8
10
|
*
|
|
9
11
|
* Usage:
|
|
10
|
-
* nuwax-mcp-stdio-proxy --config '{"mcpServers":{
|
|
12
|
+
* nuwax-mcp-stdio-proxy --config '{"mcpServers":{
|
|
13
|
+
* "local": {"command":"npx","args":["-y","some-mcp"]},
|
|
14
|
+
* "remote": {"url":"http://127.0.0.1:8080/mcp/name"} // bridge to persistent server
|
|
15
|
+
* }}'
|
|
11
16
|
*
|
|
12
17
|
* Architecture:
|
|
13
18
|
* Agent Engine (stdin/stdout) ←→ This Proxy (StdioServerTransport)
|
|
14
19
|
* ├→ Child MCP Server A (StdioClientTransport)
|
|
15
|
-
* ├→
|
|
20
|
+
* ├→ Bridge MCP Server B (StreamableHTTPClientTransport)
|
|
16
21
|
* └→ ...
|
|
17
22
|
*/
|
|
18
23
|
export {};
|
package/dist/index.js
CHANGED
|
@@ -2,32 +2,31 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* nuwax-mcp-stdio-proxy
|
|
4
4
|
*
|
|
5
|
-
* A pure TypeScript stdio MCP proxy that aggregates multiple
|
|
6
|
-
* into a single MCP server endpoint.
|
|
7
|
-
*
|
|
5
|
+
* A pure TypeScript stdio MCP proxy that aggregates multiple MCP servers
|
|
6
|
+
* into a single MCP server endpoint. Supports two upstream transport types:
|
|
7
|
+
*
|
|
8
|
+
* - **stdio**: Spawns child MCP server processes (StdioClientTransport)
|
|
9
|
+
* - **bridge**: Connects to persistent MCP Bridge servers via StreamableHTTP (StreamableHTTPClientTransport)
|
|
8
10
|
*
|
|
9
11
|
* Usage:
|
|
10
|
-
* nuwax-mcp-stdio-proxy --config '{"mcpServers":{
|
|
12
|
+
* nuwax-mcp-stdio-proxy --config '{"mcpServers":{
|
|
13
|
+
* "local": {"command":"npx","args":["-y","some-mcp"]},
|
|
14
|
+
* "remote": {"url":"http://127.0.0.1:8080/mcp/name"} // bridge to persistent server
|
|
15
|
+
* }}'
|
|
11
16
|
*
|
|
12
17
|
* Architecture:
|
|
13
18
|
* Agent Engine (stdin/stdout) ←→ This Proxy (StdioServerTransport)
|
|
14
19
|
* ├→ Child MCP Server A (StdioClientTransport)
|
|
15
|
-
* ├→
|
|
20
|
+
* ├→ Bridge MCP Server B (StreamableHTTPClientTransport)
|
|
16
21
|
* └→ ...
|
|
17
22
|
*/
|
|
18
23
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
19
24
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
20
|
-
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
21
|
-
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
22
25
|
import { ListToolsRequestSchema, CallToolRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const logInfo = (msg) => log('INFO', msg);
|
|
28
|
-
const logWarn = (msg) => log('WARN', msg);
|
|
29
|
-
const logError = (msg) => log('ERROR', msg);
|
|
30
|
-
// ========== CLI Argument Parsing ==========
|
|
26
|
+
import { isHttpEntry } from './types.js';
|
|
27
|
+
import { logInfo, logWarn, logError } from './logger.js';
|
|
28
|
+
import { buildBaseEnv, connectStdio, connectBridge } from './transport.js';
|
|
29
|
+
// ========== CLI ==========
|
|
31
30
|
function parseConfig() {
|
|
32
31
|
const args = process.argv.slice(2);
|
|
33
32
|
const idx = args.indexOf('--config');
|
|
@@ -48,20 +47,6 @@ function parseConfig() {
|
|
|
48
47
|
process.exit(1);
|
|
49
48
|
}
|
|
50
49
|
}
|
|
51
|
-
// ========== Build Clean Environment for Child Processes ==========
|
|
52
|
-
function buildBaseEnv() {
|
|
53
|
-
const env = {};
|
|
54
|
-
// Copy current process.env, filtering out undefined values
|
|
55
|
-
for (const [key, value] of Object.entries(process.env)) {
|
|
56
|
-
if (value !== undefined) {
|
|
57
|
-
env[key] = value;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
// Remove ELECTRON_RUN_AS_NODE — child MCP servers should run normally,
|
|
61
|
-
// not as Electron Node.js instances
|
|
62
|
-
delete env.ELECTRON_RUN_AS_NODE;
|
|
63
|
-
return env;
|
|
64
|
-
}
|
|
65
50
|
// ========== Main ==========
|
|
66
51
|
async function main() {
|
|
67
52
|
const config = parseConfig();
|
|
@@ -70,38 +55,22 @@ async function main() {
|
|
|
70
55
|
logError('No MCP servers configured in mcpServers');
|
|
71
56
|
process.exit(1);
|
|
72
57
|
}
|
|
73
|
-
logInfo(`Starting proxy with ${entries.length}
|
|
74
|
-
// ---- Phase 1: Connect to all
|
|
75
|
-
// Build base env once — per-server env is merged on top
|
|
58
|
+
logInfo(`Starting proxy with ${entries.length} server(s): ${entries.map(([id]) => id).join(', ')}`);
|
|
59
|
+
// ---- Phase 1: Connect to all MCP servers (stdio + bridge) ----
|
|
76
60
|
const baseEnv = buildBaseEnv();
|
|
77
61
|
const clients = new Map();
|
|
62
|
+
const cleanups = new Map();
|
|
78
63
|
const toolToClient = new Map();
|
|
79
64
|
const toolToServer = new Map();
|
|
80
65
|
const toolsByName = new Map();
|
|
81
66
|
for (const [id, entry] of entries) {
|
|
82
|
-
let transport = null;
|
|
83
67
|
try {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
args: entry.args || [],
|
|
88
|
-
env: { ...baseEnv, ...(entry.env || {}) },
|
|
89
|
-
// stderr defaults to 'pipe' in the SDK; child errors captured via transport.stderr
|
|
90
|
-
});
|
|
91
|
-
// Attach stderr listener BEFORE connect to catch early child errors
|
|
92
|
-
// (SDK returns a PassThrough stream immediately for this purpose)
|
|
93
|
-
if (transport.stderr) {
|
|
94
|
-
transport.stderr.on('data', (chunk) => {
|
|
95
|
-
const text = chunk.toString().trim();
|
|
96
|
-
if (text) {
|
|
97
|
-
process.stderr.write(`[child:${id}] ${text}\n`);
|
|
98
|
-
}
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
const client = new Client({ name: `proxy-${id}`, version: '1.0.0' });
|
|
102
|
-
await client.connect(transport);
|
|
68
|
+
const { client, cleanup } = isHttpEntry(entry)
|
|
69
|
+
? await connectBridge(id, entry)
|
|
70
|
+
: await connectStdio(id, entry, baseEnv);
|
|
103
71
|
clients.set(id, client);
|
|
104
|
-
|
|
72
|
+
cleanups.set(id, cleanup);
|
|
73
|
+
// Discover tools (handle pagination)
|
|
105
74
|
const allServerTools = [];
|
|
106
75
|
let cursor;
|
|
107
76
|
do {
|
|
@@ -121,29 +90,20 @@ async function main() {
|
|
|
121
90
|
}
|
|
122
91
|
catch (e) {
|
|
123
92
|
logError(`Failed to connect to server "${id}": ${e}`);
|
|
124
|
-
// Clean up transport to prevent child process leak
|
|
125
|
-
if (transport) {
|
|
126
|
-
try {
|
|
127
|
-
await transport.close();
|
|
128
|
-
}
|
|
129
|
-
catch { /* ignore */ }
|
|
130
|
-
}
|
|
131
93
|
// Continue with remaining servers — partial startup is acceptable
|
|
132
94
|
}
|
|
133
95
|
}
|
|
134
96
|
if (clients.size === 0) {
|
|
135
|
-
logError('Failed to connect to any
|
|
97
|
+
logError('Failed to connect to any MCP server');
|
|
136
98
|
process.exit(1);
|
|
137
99
|
}
|
|
138
100
|
const aggregatedTools = Array.from(toolsByName.values());
|
|
139
101
|
logInfo(`Aggregated ${aggregatedTools.length} unique tool(s) from ${clients.size} server(s)`);
|
|
140
102
|
// ---- Phase 2: Create the aggregating MCP server ----
|
|
141
103
|
const server = new Server({ name: 'nuwax-mcp-stdio-proxy', version: '1.0.0' }, { capabilities: { tools: {} } });
|
|
142
|
-
// tools/list → return all aggregated tools
|
|
143
104
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
144
105
|
return { tools: aggregatedTools };
|
|
145
106
|
});
|
|
146
|
-
// tools/call → route to the correct child server
|
|
147
107
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
148
108
|
const { name, arguments: toolArgs } = request.params;
|
|
149
109
|
const client = toolToClient.get(name);
|
|
@@ -180,10 +140,18 @@ async function main() {
|
|
|
180
140
|
for (const [id, client] of clients) {
|
|
181
141
|
try {
|
|
182
142
|
await client.close();
|
|
183
|
-
logInfo(`Closed
|
|
143
|
+
logInfo(`Closed client "${id}"`);
|
|
144
|
+
}
|
|
145
|
+
catch (e) {
|
|
146
|
+
logError(`Failed to close client "${id}": ${e}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
for (const [id, cleanup] of cleanups) {
|
|
150
|
+
try {
|
|
151
|
+
await cleanup();
|
|
184
152
|
}
|
|
185
153
|
catch (e) {
|
|
186
|
-
logError(`Failed
|
|
154
|
+
logError(`Failed cleanup for "${id}": ${e}`);
|
|
187
155
|
}
|
|
188
156
|
}
|
|
189
157
|
try {
|
package/dist/logger.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logging utilities — stderr only, stdout is the MCP JSON-RPC channel
|
|
3
|
+
*/
|
|
4
|
+
export declare function log(level: string, msg: string): void;
|
|
5
|
+
export declare const logInfo: (msg: string) => void;
|
|
6
|
+
export declare const logWarn: (msg: string) => void;
|
|
7
|
+
export declare const logError: (msg: string) => void;
|
package/dist/logger.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logging utilities — stderr only, stdout is the MCP JSON-RPC channel
|
|
3
|
+
*/
|
|
4
|
+
export function log(level, msg) {
|
|
5
|
+
process.stderr.write(`[nuwax-mcp-proxy] ${level}: ${msg}\n`);
|
|
6
|
+
}
|
|
7
|
+
export const logInfo = (msg) => log('INFO', msg);
|
|
8
|
+
export const logWarn = (msg) => log('WARN', msg);
|
|
9
|
+
export const logError = (msg) => log('ERROR', msg);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transport layer — connect to upstream MCP servers via stdio or bridge (HTTP)
|
|
3
|
+
*/
|
|
4
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
5
|
+
import type { StdioServerEntry, BridgeServerEntry } from './types.js';
|
|
6
|
+
export interface ConnectedClient {
|
|
7
|
+
client: Client;
|
|
8
|
+
cleanup: () => Promise<void>;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Build a clean env for child processes (strips ELECTRON_RUN_AS_NODE)
|
|
12
|
+
*/
|
|
13
|
+
export declare function buildBaseEnv(): Record<string, string>;
|
|
14
|
+
/**
|
|
15
|
+
* Connect to a stdio MCP server (spawn child process)
|
|
16
|
+
*/
|
|
17
|
+
export declare function connectStdio(id: string, entry: StdioServerEntry, baseEnv: Record<string, string>): Promise<ConnectedClient>;
|
|
18
|
+
/**
|
|
19
|
+
* Connect to a bridge MCP server (StreamableHTTP → PersistentMcpBridge)
|
|
20
|
+
*
|
|
21
|
+
* Bridge connections target long-lived MCP servers managed by PersistentMcpBridge
|
|
22
|
+
* in the Electron main process, accessed via HTTP endpoints.
|
|
23
|
+
*/
|
|
24
|
+
export declare function connectBridge(id: string, entry: BridgeServerEntry): Promise<ConnectedClient>;
|
|
25
|
+
/** @deprecated Use connectBridge instead */
|
|
26
|
+
export declare const connectHttp: typeof connectBridge;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transport layer — connect to upstream MCP servers via stdio or bridge (HTTP)
|
|
3
|
+
*/
|
|
4
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
5
|
+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
6
|
+
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
7
|
+
import { logInfo } from './logger.js';
|
|
8
|
+
/**
|
|
9
|
+
* Build a clean env for child processes (strips ELECTRON_RUN_AS_NODE)
|
|
10
|
+
*/
|
|
11
|
+
export function buildBaseEnv() {
|
|
12
|
+
const env = {};
|
|
13
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
14
|
+
if (value !== undefined) {
|
|
15
|
+
env[key] = value;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
// Child MCP servers should run normally, not as Electron Node.js instances
|
|
19
|
+
delete env.ELECTRON_RUN_AS_NODE;
|
|
20
|
+
return env;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Connect to a stdio MCP server (spawn child process)
|
|
24
|
+
*/
|
|
25
|
+
export async function connectStdio(id, entry, baseEnv) {
|
|
26
|
+
logInfo(`Connecting to "${id}" (stdio): ${entry.command} ${(entry.args || []).join(' ')}`);
|
|
27
|
+
const transport = new StdioClientTransport({
|
|
28
|
+
command: entry.command,
|
|
29
|
+
args: entry.args || [],
|
|
30
|
+
env: { ...baseEnv, ...(entry.env || {}) },
|
|
31
|
+
});
|
|
32
|
+
// Attach stderr listener BEFORE connect to catch early child errors
|
|
33
|
+
if (transport.stderr) {
|
|
34
|
+
transport.stderr.on('data', (chunk) => {
|
|
35
|
+
const text = chunk.toString().trim();
|
|
36
|
+
if (text) {
|
|
37
|
+
process.stderr.write(`[child:${id}] ${text}\n`);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
const client = new Client({ name: `proxy-${id}`, version: '1.0.0' });
|
|
42
|
+
await client.connect(transport);
|
|
43
|
+
return {
|
|
44
|
+
client,
|
|
45
|
+
cleanup: async () => {
|
|
46
|
+
try {
|
|
47
|
+
await transport.close();
|
|
48
|
+
}
|
|
49
|
+
catch { /* ignore */ }
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Connect to a bridge MCP server (StreamableHTTP → PersistentMcpBridge)
|
|
55
|
+
*
|
|
56
|
+
* Bridge connections target long-lived MCP servers managed by PersistentMcpBridge
|
|
57
|
+
* in the Electron main process, accessed via HTTP endpoints.
|
|
58
|
+
*/
|
|
59
|
+
export async function connectBridge(id, entry) {
|
|
60
|
+
logInfo(`Connecting to "${id}" (bridge): ${entry.url}`);
|
|
61
|
+
const transport = new StreamableHTTPClientTransport(new URL(entry.url));
|
|
62
|
+
const client = new Client({ name: `proxy-${id}`, version: '1.0.0' });
|
|
63
|
+
await client.connect(transport);
|
|
64
|
+
return {
|
|
65
|
+
client,
|
|
66
|
+
cleanup: async () => {
|
|
67
|
+
try {
|
|
68
|
+
await transport.close();
|
|
69
|
+
}
|
|
70
|
+
catch { /* ignore */ }
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/** @deprecated Use connectBridge instead */
|
|
75
|
+
export const connectHttp = connectBridge;
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types for MCP server configuration
|
|
3
|
+
*/
|
|
4
|
+
/** stdio 类型: spawn 子进程 */
|
|
5
|
+
export interface StdioServerEntry {
|
|
6
|
+
command: string;
|
|
7
|
+
args?: string[];
|
|
8
|
+
env?: Record<string, string>;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Bridge 类型: 连接持久化 MCP Bridge (HTTP)
|
|
12
|
+
*
|
|
13
|
+
* 用于连接 PersistentMcpBridge 等长生命周期的 MCP server。
|
|
14
|
+
* Bridge server 通过 StreamableHTTPClientTransport 访问,
|
|
15
|
+
* 其生命周期独立于 proxy 进程。
|
|
16
|
+
*/
|
|
17
|
+
export interface BridgeServerEntry {
|
|
18
|
+
url: string;
|
|
19
|
+
}
|
|
20
|
+
/** @deprecated Use BridgeServerEntry instead */
|
|
21
|
+
export type HttpServerEntry = BridgeServerEntry;
|
|
22
|
+
export type McpServerEntry = StdioServerEntry | BridgeServerEntry;
|
|
23
|
+
export declare function isBridgeEntry(entry: McpServerEntry): entry is BridgeServerEntry;
|
|
24
|
+
/** @deprecated Use isBridgeEntry instead */
|
|
25
|
+
export declare const isHttpEntry: typeof isBridgeEntry;
|
|
26
|
+
export interface McpServersConfig {
|
|
27
|
+
mcpServers: Record<string, McpServerEntry>;
|
|
28
|
+
}
|
package/dist/types.js
ADDED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nuwax-mcp-stdio-proxy",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "TypeScript stdio MCP proxy — aggregates multiple
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "TypeScript stdio MCP proxy — aggregates multiple MCP servers (stdio + bridge) into one",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"nuwax-mcp-stdio-proxy": "./dist/index.js"
|