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 CHANGED
@@ -1,37 +1,64 @@
1
1
  # nuwax-mcp-stdio-proxy
2
2
 
3
- TypeScript stdio MCP proxy — aggregates multiple MCP servers into a single stdio endpoint. Supports **stdio** (spawn child processes) and **bridge** (Streamable HTTP to a persistent MCP bridge).
3
+ 一个纯 TypeScript 编写的 MCP (Model Context Protocol) 代理工具,为 MCP Server 提供高级聚合、协议转换以及生命周期管理功能。
4
4
 
5
- ## Requirements
5
+ 它的设计初衷,是为了解决将 MCP Server 集成到大型应用或 Agent OS 平台时遇到的“启动时序竞争”及“应用退出后产生僵尸进程”等痛点问题。
6
+
7
+ ## 环境要求
6
8
 
7
9
  - **Node.js** >= 22.0.0
8
10
 
9
- ## Usage
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
- The proxy reads JSON from `--config` and runs as a stdio MCP server. Upstream servers can be:
23
+ 配置中可以混合配置 `stdio` (子进程) `bridge` (HTTP 连接) 类型的上游服务器。代理会统一将它们聚合并向客户端暴露唯一的一个 `stdio` MCP 交互接口。
16
24
 
17
- - **stdio**: `{ "command", "args?", "env?" }` — proxy spawns a child process and talks MCP over stdin/stdout.
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
- ## Config format
27
+ 将单个远程的 SSE 或 Streamable HTTP MCP Server 代理转化为本地的 `stdio` MCP Server。适用于仅支持 `stdio` 接入的客户端应用。
21
28
 
22
- | Entry type | Shape | Description |
23
- |------------|--------|-------------|
24
- | **stdio** | `{ "command": string, "args"?: string[], "env"?: Record<string, string> }` | Spawn subprocess; MCP over stdio. |
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
- Example:
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": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/allowed"]
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
- - `filesystem` → stdio (child process).
44
- - `chrome-devtools` → bridge (HTTP to a persistent bridge).
45
-
46
- ## Architecture
70
+ ## 架构简图
47
71
 
48
72
  ```
49
- Agent / ACP engine (stdin/stdout)
73
+ Agent / ACP 引擎客户端 (stdin/stdout)
50
74
 
51
- nuwax-mcp-stdio-proxy (StdioServerTransport)
52
- ├→ stdio upstreamchild process (StdioClientTransport)
53
- └→ bridge upstream → StreamableHTTPClientTransport → PersistentMcpBridge HTTP
75
+ nuwax-mcp-stdio-proxy (Stdio 模式)
76
+ ├→ [stdio 上游]Spawn 启动子进程 (StdioClientTransport)
77
+ └→ [bridge 上游] → StreamableHTTPClientTransport → 连接 PersistentMcpBridge HTTP
54
78
  ```
55
79
 
56
- - **Downstream**: one stdio MCP server; the agent talks to the proxy over stdin/stdout.
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
- ## Scripts
82
+ ## 开发指令
60
83
 
61
- | Command | Description |
62
- |---------|-------------|
63
- | `npm run build` | Compile TypeScript to `dist/`. |
64
- | `npm run test` | Run tests (Vitest). |
65
- | `npm run test:run` | Run tests once (no watch). |
66
- | `npm run test:coverage` | Run tests with 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
- ## License
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
- const LOG_TAG = '[PersistentMcpBridge]';
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 + CustomStdioClientTransport (handles spawn internally)
160
- const transport = new CustomStdioClientTransport({
161
- command: entry.config.command,
162
- args: entry.config.args || [],
163
- env: entry.config.env,
164
- stderr: 'pipe',
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
- transport.onclose = () => {
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
- transport.onerror = (err) => {
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 client.connect(transport);
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 = 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;