nuwax-mcp-stdio-proxy 1.4.6 → 1.4.7
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/README.md +54 -31
- package/dist/bridge.js +33 -20
- package/dist/index.js +414 -48
- package/dist/modes/convert.d.ts +2 -0
- package/dist/modes/convert.js +2 -2
- package/dist/resilient.d.ts +80 -0
- package/dist/resilient.js +297 -0
- package/dist/transport.d.ts +2 -0
- package/dist/transport.js +101 -22
- package/dist/types.d.ts +12 -0
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -1,37 +1,64 @@
|
|
|
1
1
|
# nuwax-mcp-stdio-proxy
|
|
2
2
|
|
|
3
|
-
TypeScript
|
|
3
|
+
一个纯 TypeScript 编写的 MCP (Model Context Protocol) 代理工具,为 MCP Server 提供高级聚合、协议转换以及生命周期管理功能。
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
它的设计初衷,是为了解决将 MCP Server 集成到大型应用或 Agent OS 平台时遇到的“启动时序竞争”及“应用退出后产生僵尸进程”等痛点问题。
|
|
6
|
+
|
|
7
|
+
## 环境要求
|
|
6
8
|
|
|
7
9
|
- **Node.js** >= 22.0.0
|
|
8
10
|
|
|
9
|
-
##
|
|
11
|
+
## 运行模式
|
|
12
|
+
|
|
13
|
+
该代理工具具备三种截然不同的工作模式:
|
|
14
|
+
|
|
15
|
+
### 1. Stdio 聚合模式 (默认)
|
|
16
|
+
|
|
17
|
+
将多个基于不同协议上游 MCP Server 聚合成单个面向下游的 `stdio` 节点。
|
|
10
18
|
|
|
11
19
|
```bash
|
|
12
20
|
nuwax-mcp-stdio-proxy --config '{"mcpServers":{...}}'
|
|
13
21
|
```
|
|
14
22
|
|
|
15
|
-
|
|
23
|
+
配置中可以混合配置 `stdio` (子进程) 和 `bridge` (HTTP 连接) 类型的上游服务器。代理会统一将它们聚合并向客户端暴露唯一的一个 `stdio` MCP 交互接口。
|
|
16
24
|
|
|
17
|
-
|
|
18
|
-
- **bridge**: `{ "url" }` — proxy connects via HTTP (Streamable HTTP) to a long-lived MCP bridge (e.g. Electron app’s PersistentMcpBridge).
|
|
25
|
+
### 2. 协议转换模式 (`convert`)
|
|
19
26
|
|
|
20
|
-
|
|
27
|
+
将单个远程的 SSE 或 Streamable HTTP MCP Server 代理转化为本地的 `stdio` MCP Server。适用于仅支持 `stdio` 接入的客户端应用。
|
|
21
28
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
| **bridge** | `{ "url": string }` | Connect to MCP over HTTP (e.g. `http://127.0.0.1:PORT/mcp/<serverId>`). |
|
|
29
|
+
```bash
|
|
30
|
+
nuwax-mcp-stdio-proxy convert http://example.com/mcp/sse --protocol sse
|
|
31
|
+
```
|
|
26
32
|
|
|
27
|
-
|
|
33
|
+
### 3. 持久化 HTTP 桥接模式 (`proxy`)
|
|
34
|
+
|
|
35
|
+
作为 Streamable HTTP Server 启动 `PersistentMcpBridge`。该模式会预先构建并管理标准的 `stdio` 子进程,进而将它们通过高速、可秒连的 HTTP 接口暴露给下游使用。
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
nuwax-mcp-stdio-proxy proxy --port 18099 --config '{"mcpServers":{...}}'
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## 配置文件格式
|
|
42
|
+
|
|
43
|
+
在使用默认聚合模式或 `proxy` 模式时,使用如下结构的 JSON 配置:
|
|
44
|
+
|
|
45
|
+
| 节点类型 | 配置结构 | 描述 |
|
|
46
|
+
| ---------- | -------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
|
|
47
|
+
| **stdio** | `{ "command": string, "args"?: string[], "env"?: Record<string, string> }` | 创建子进程;通过 stdio 进行 MCP 通信。 |
|
|
48
|
+
| **bridge** | `{ "url": string }` | 通过 HTTP 建立 MCP 连接 (例如 `http://127.0.0.1:PORT/mcp/<serverId>`)。 |
|
|
49
|
+
|
|
50
|
+
**配置示例:**
|
|
28
51
|
|
|
29
52
|
```json
|
|
30
53
|
{
|
|
31
54
|
"mcpServers": {
|
|
32
55
|
"filesystem": {
|
|
33
56
|
"command": "npx",
|
|
34
|
-
"args": [
|
|
57
|
+
"args": [
|
|
58
|
+
"-y",
|
|
59
|
+
"@modelcontextprotocol/server-filesystem",
|
|
60
|
+
"/path/to/allowed"
|
|
61
|
+
]
|
|
35
62
|
},
|
|
36
63
|
"chrome-devtools": {
|
|
37
64
|
"url": "http://127.0.0.1:57278/mcp/chrome-devtools"
|
|
@@ -40,31 +67,27 @@ Example:
|
|
|
40
67
|
}
|
|
41
68
|
```
|
|
42
69
|
|
|
43
|
-
|
|
44
|
-
- `chrome-devtools` → bridge (HTTP to a persistent bridge).
|
|
45
|
-
|
|
46
|
-
## Architecture
|
|
70
|
+
## 架构简图
|
|
47
71
|
|
|
48
72
|
```
|
|
49
|
-
Agent / ACP
|
|
73
|
+
Agent / ACP 引擎客户端 (stdin/stdout)
|
|
50
74
|
↕
|
|
51
|
-
nuwax-mcp-stdio-proxy (
|
|
52
|
-
├→ stdio
|
|
53
|
-
└→ bridge
|
|
75
|
+
nuwax-mcp-stdio-proxy (Stdio 模式)
|
|
76
|
+
├→ [stdio 上游] → Spawn 启动子进程 (StdioClientTransport)
|
|
77
|
+
└→ [bridge 上游] → StreamableHTTPClientTransport → 连接 PersistentMcpBridge HTTP
|
|
54
78
|
```
|
|
55
79
|
|
|
56
|
-
|
|
57
|
-
- **Upstream**: multiple backends — some are child processes (stdio), some are HTTP bridge endpoints. Tools from all upstreams are aggregated and exposed as one tool list.
|
|
80
|
+
通过引入 `proxy` 模式开启 `PersistentMcpBridge`,上层的应用可以剥离 MCP Server 的原本子进程生命周期,从而避免启动初期的时序竞争条件,同时能够更加可靠、整洁地在退出时销毁无用进程。
|
|
58
81
|
|
|
59
|
-
##
|
|
82
|
+
## 开发指令
|
|
60
83
|
|
|
61
|
-
|
|
|
62
|
-
|
|
63
|
-
| `npm run build`
|
|
64
|
-
| `npm run test`
|
|
65
|
-
| `npm run test:run`
|
|
66
|
-
| `npm run test:coverage` |
|
|
84
|
+
| 指令 | 描述 |
|
|
85
|
+
| ----------------------- | ------------------------------------------------------------ |
|
|
86
|
+
| `npm run build` | 编译 TypeScript 输出到 `dist/` 目录。 |
|
|
87
|
+
| `npm run test` | 运行测试 (Vitest)。 |
|
|
88
|
+
| `npm run test:run` | 单次运行所有的单元与集成测试 (无监控交互)。 |
|
|
89
|
+
| `npm run test:coverage` | 运行测试并生成详细的覆盖率统计报告 (存于 `coverage/` 目录)。 |
|
|
67
90
|
|
|
68
|
-
##
|
|
91
|
+
## 许可证
|
|
69
92
|
|
|
70
93
|
MIT
|
package/dist/bridge.js
CHANGED
|
@@ -20,7 +20,8 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
|
20
20
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
21
21
|
import { ListToolsRequestSchema, CallToolRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
22
22
|
import { CustomStdioClientTransport } from './customStdio.js';
|
|
23
|
-
|
|
23
|
+
import { ResilientTransportWrapper } from './resilient.js';
|
|
24
|
+
const LOG_TAG = '[McpProxy] [PersistentMcpBridge]';
|
|
24
25
|
const BASE_RESTART_COOLDOWN_MS = 5_000;
|
|
25
26
|
const MAX_RESTART_ATTEMPTS = 5;
|
|
26
27
|
const MAX_BODY_SIZE = 10 * 1024 * 1024; // 10 MB
|
|
@@ -156,38 +157,50 @@ export class PersistentMcpBridge {
|
|
|
156
157
|
async spawnAndConnect(id, entry) {
|
|
157
158
|
try {
|
|
158
159
|
this.log.info(`${LOG_TAG} Spawning server "${id}": ${entry.config.command} ${(entry.config.args || []).join(' ')}`);
|
|
159
|
-
// Create MCP Client +
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
160
|
+
// Create MCP Client + ResilientTransportWrapper
|
|
161
|
+
const wrapper = new ResilientTransportWrapper({
|
|
162
|
+
name: id,
|
|
163
|
+
logger: this.log,
|
|
164
|
+
connectParams: async () => {
|
|
165
|
+
const t = new CustomStdioClientTransport({
|
|
166
|
+
command: entry.config.command,
|
|
167
|
+
args: entry.config.args || [],
|
|
168
|
+
env: entry.config.env,
|
|
169
|
+
stderr: 'pipe',
|
|
170
|
+
});
|
|
171
|
+
if (t.stderr) {
|
|
172
|
+
t.stderr.on('data', (chunk) => {
|
|
173
|
+
const text = chunk.toString().trim();
|
|
174
|
+
if (text)
|
|
175
|
+
this.log.info(`${LOG_TAG} [${id}:stderr] ${text}`);
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
return t;
|
|
179
|
+
},
|
|
180
|
+
pingIntervalMs: entry.config.pingIntervalMs,
|
|
181
|
+
pingTimeoutMs: entry.config.pingTimeoutMs,
|
|
165
182
|
});
|
|
166
183
|
const client = new Client({ name: 'nuwax-persistent-bridge', version: '1.0.0' }, { capabilities: {} });
|
|
167
184
|
// Handle transport close → auto restart
|
|
168
|
-
|
|
185
|
+
wrapper.onclose = () => {
|
|
169
186
|
this.log.warn(`${LOG_TAG} Server "${id}" transport closed`);
|
|
170
187
|
entry.healthy = false;
|
|
171
188
|
if (this.running && !entry.restarting) {
|
|
172
189
|
this.scheduleRestart(id, entry);
|
|
173
190
|
}
|
|
174
191
|
};
|
|
175
|
-
|
|
192
|
+
wrapper.onerror = (err) => {
|
|
176
193
|
this.log.error(`${LOG_TAG} Server "${id}" transport error:`, err.message);
|
|
177
194
|
};
|
|
178
195
|
// Connect client to transport (this starts the subprocess)
|
|
179
|
-
await
|
|
196
|
+
await wrapper.start();
|
|
197
|
+
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();
|
|
180
202
|
entry.client = client;
|
|
181
|
-
entry.transport =
|
|
182
|
-
// Pipe stderr for debugging
|
|
183
|
-
const stderrStream = transport.stderr;
|
|
184
|
-
if (stderrStream) {
|
|
185
|
-
stderrStream.on('data', (chunk) => {
|
|
186
|
-
const text = chunk.toString().trim();
|
|
187
|
-
if (text)
|
|
188
|
-
this.log.info(`${LOG_TAG} [${id}:stderr] ${text}`);
|
|
189
|
-
});
|
|
190
|
-
}
|
|
203
|
+
entry.transport = wrapper;
|
|
191
204
|
// List tools (cached)
|
|
192
205
|
const result = await client.listTools();
|
|
193
206
|
entry.tools = result.tools;
|