11/**
22 * nuwa-cli ↔ @nuwax-ai/mcp-proxy-ts Host Adapter 封装。
33 *
4- * - 默认合并 Electron 同款 DEFAULT(chrome-devtools persistent)
5- * - 把 ACP mcpServers 改写成「每 server 一个 proxy stdio 入口」
6- * - 管理 PersistentMcpBridge(persistent 标记的 stdio server)
4+ * 对齐 nuwaclaw:
5+ * - PersistentMcpBridge(chrome-devtools 等 persistent)= Hub/serve 级单例
6+ * 「启动后一直在,直到主动 stop」
7+ * - serve 启动时 warmup;session 结束不杀 Bridge
8+ * - claude/codex:ephemeral 仍下发原始 stdio;persistent 经 proxy 接 Bridge URL
9+ * (避免与 Bridge 各起一份 chrome-devtools)
10+ * - 其它引擎:整表改写成 proxy stdio(含 Bridge URL)
711 */
812
913import type { McpServer } from "@agentclientprotocol/sdk" ;
@@ -30,7 +34,7 @@ const mcpBridgeLogger: McpProxyLogger = {
3034 error : ( ...args ) => debugLog ( "mcp-bridge" , args . map ( String ) . join ( " " ) ) ,
3135} ;
3236
33- /** Hub 级 PersistentMcpBridge 单例(管理在 @nuwax-ai/agent-kit);无 persistent server 时不启动 。 */
37+ /** Hub 级 PersistentMcpBridge 单例(管理在 @nuwax-ai/agent-kit)。 */
3438const persistentBridge = createPersistentBridge ( {
3539 create : ( logger ) => new PersistentMcpBridge ( logger ) ,
3640 logger : mcpBridgeLogger ,
@@ -71,6 +75,28 @@ function persistentNamesFromEnv(): Set<string> {
7175 ) ;
7276}
7377
78+ /** 对 stdio 条目做 Windows npx→node 解析。 */
79+ function resolveStdioEntry ( entry : HostStdioServerEntry ) : HostStdioServerEntry {
80+ const resolved = resolveStdioNoWindow ( entry . command , entry . args ?? [ ] ) ;
81+ return { ...entry , command : resolved . command , args : resolved . args } ;
82+ }
83+
84+ /**
85+ * serve 启动时用于 warmup 的默认 persistent 集合(仅 DEFAULT,含 npx 解析)。
86+ * 动态 ACP / NUWACLI_MCP_PERSISTENT 追加在 rewrite 时再并入。
87+ */
88+ export function buildDefaultPersistentServers ( ) : Record <
89+ string ,
90+ HostStdioServerEntry
91+ > {
92+ const out : Record < string , HostStdioServerEntry > = { } ;
93+ for ( const [ name , entry ] of Object . entries ( DEFAULT_MCP_PROXY_SERVERS ) ) {
94+ if ( ! entry . persistent ) continue ;
95+ out [ name ] = { ...resolveStdioEntry ( entry ) , persistent : true } ;
96+ }
97+ return out ;
98+ }
99+
74100/**
75101 * Host map → ACP 数组形态(无 proxy 改写时的回退)。
76102 */
@@ -105,6 +131,42 @@ function hostMapToAcpServers(
105131 return out ;
106132}
107133
134+ function proxyCommandsToAcpServers (
135+ rewritten : Record <
136+ string ,
137+ { command : string ; args : string [ ] ; env ?: Record < string , string > }
138+ > ,
139+ ) : McpServer [ ] {
140+ return Object . entries ( rewritten ) . map ( ( [ name , entry ] ) => ( {
141+ name,
142+ command : entry . command ,
143+ args : entry . args ,
144+ env : entry . env
145+ ? Object . entries ( entry . env ) . map ( ( [ n , v ] ) => ( { name : n , value : v } ) )
146+ : [ ] ,
147+ } ) ) ;
148+ }
149+
150+ /** 从已 merge 的 map 抽出 persistent stdio 子集。 */
151+ function extractPersistentServers (
152+ merged : Record < string , HostMcpServerEntry > ,
153+ ) : Record < string , HostStdioServerEntry > {
154+ const persistentNames = persistentNamesFromEnv ( ) ;
155+ const persistent : Record < string , HostStdioServerEntry > = { } ;
156+ for ( const [ name , item ] of Object . entries ( merged ) ) {
157+ if ( isHostRemoteEntry ( item ) ) continue ;
158+ if ( persistentNames . has ( name ) || item . persistent ) {
159+ persistent [ name ] = {
160+ command : item . command ,
161+ args : item . args ,
162+ env : item . env ,
163+ persistent : true ,
164+ } ;
165+ }
166+ }
167+ return persistent ;
168+ }
169+
108170/**
109171 * 确保 PersistentMcpBridge 已托管指定的 persistent stdio servers。
110172 * 单例管理在 @nuwax-ai/agent-kit(createPersistentBridge)。
@@ -115,6 +177,30 @@ export async function ensurePersistentMcpBridge(
115177 return persistentBridge . ensureStarted ( servers ) ;
116178}
117179
180+ /**
181+ * serve / Gateway 启动时预热 Bridge(对齐 nuwaclaw warmup)。
182+ * 失败只打日志,不阻断 serve;后续 rewrite 仍会再 ensure。
183+ */
184+ export async function warmupPersistentMcpBridge ( ) : Promise < void > {
185+ const servers = buildDefaultPersistentServers ( ) ;
186+ if ( Object . keys ( servers ) . length === 0 ) return ;
187+ try {
188+ await ensurePersistentMcpBridge ( servers ) ;
189+ debugLog ( "mcp-proxy" , "PersistentMcpBridge warmed" , {
190+ servers : Object . keys ( servers ) ,
191+ } ) ;
192+ } catch ( err ) {
193+ debugLog ( "mcp-proxy" , "PersistentMcpBridge warmup failed" , {
194+ error : err instanceof Error ? err . message : String ( err ) ,
195+ } ) ;
196+ }
197+ }
198+
199+ /** status 用:Bridge 是否已启动(与「有无临时 mcp-proxy-ts 进程」无关)。 */
200+ export function isPersistentMcpBridgeRunning ( ) : boolean {
201+ return persistentBridge . isRunning ( ) ;
202+ }
203+
118204/** serve / hub 关闭时停止 bridge,避免子进程残留。 */
119205export async function stopPersistentMcpBridge ( ) : Promise < void > {
120206 await persistentBridge . stop ( ) ;
@@ -127,6 +213,7 @@ export async function stopPersistentMcpBridge(): Promise<void> {
127213 * - 始终以 DEFAULT(chrome-devtools persistent)为底,再叠加 ACP 动态 MCP
128214 * - 空列表仍注入默认服务(仅 chrome-devtools)
129215 * - NUWACLI_MCP_PERSISTENT 可追加其它长驻名
216+ * - Bridge 按 Hub 生命周期常驻;claude/codex 的 persistent 走 proxy→Bridge URL
130217 */
131218export async function rewriteMcpServersForEngine (
132219 servers : McpServer [ ] | undefined ,
@@ -157,8 +244,7 @@ export async function rewriteMcpServersForEngine(
157244 // npx warmup + npm update already do the same). Remote entries pass through.
158245 for ( const [ name , entry ] of Object . entries ( merged ) ) {
159246 if ( isHostRemoteEntry ( entry ) ) continue ;
160- const resolved = resolveStdioNoWindow ( entry . command , entry . args ?? [ ] ) ;
161- merged [ name ] = { ...entry , command : resolved . command , args : resolved . args } ;
247+ merged [ name ] = resolveStdioEntry ( entry ) ;
162248 }
163249 const passthroughResolved = ( passthrough as AcpMcpServer [ ] ) . map ( ( server ) => {
164250 if ( ! ( "command" in server ) ) return server ;
@@ -170,16 +256,53 @@ export async function rewriteMcpServersForEngine(
170256 return passthroughResolved as McpServer [ ] ;
171257 }
172258
173- // claude-code-acp-ts 与 nuwax-codex-acp 均原生支持 ACP stdio MCP(各自把
174- // mcpServers 转成内部 MCP 配置:claude-code-acp-ts acp-agent.js、codex
175- // codex_agent.rs build_session_config)。mcp-proxy-ts proxy 桥接会把 server
176- // 改写成 proxy 入口形态,engine 注册不上原始 server name(codex "unknown MCP
177- // server")或工具不加载(claude)。直接下发原始 stdio 入口(DEFAULT + ACP)。
259+ const persistent = extractPersistentServers ( merged ) ;
260+ // 无论引擎:先确保 Bridge 起来(Hub 级常驻;与 session 无关)。
261+ const runningBridge = await ensurePersistentMcpBridge ( persistent ) ;
262+
263+ const proxyScriptPath = resolveProxyEntry ( ) ;
264+
265+ // claude / codex:ephemeral 原生 stdio;persistent 经 proxy 接 Bridge,避免双开。
178266 if ( engine === "codex" || engine === "claude" ) {
179- return [ ...hostMapToAcpServers ( merged ) , ...( passthroughResolved as McpServer [ ] ) ] ;
267+ const ephemeral : Record < string , HostMcpServerEntry > = { } ;
268+ const persistentOnly : Record < string , HostMcpServerEntry > = { } ;
269+ for ( const [ name , entry ] of Object . entries ( merged ) ) {
270+ if ( persistent [ name ] ) persistentOnly [ name ] = entry ;
271+ else ephemeral [ name ] = entry ;
272+ }
273+
274+ const out : McpServer [ ] = [ ...hostMapToAcpServers ( ephemeral ) ] ;
275+
276+ if ( Object . keys ( persistentOnly ) . length > 0 ) {
277+ if ( proxyScriptPath && runningBridge ) {
278+ const rewritten = rewriteServersToProxyCommands ( persistentOnly , {
279+ proxyScriptPath,
280+ nodeBinPath : process . execPath ,
281+ configDir : mcpProxyConfigDir ( ) ,
282+ logDir : mcpProxyLogDir ( ) ,
283+ projectId,
284+ bridge : runningBridge ,
285+ } ) ;
286+ if ( rewritten ) out . push ( ...proxyCommandsToAcpServers ( rewritten ) ) ;
287+ else out . push ( ...hostMapToAcpServers ( persistentOnly ) ) ;
288+ } else {
289+ // 无 proxy 脚本或 Bridge 未起:退回原始 stdio(可能与 warmup 失败并存)
290+ out . push ( ...hostMapToAcpServers ( persistentOnly ) ) ;
291+ }
292+ }
293+
294+ debugLog (
295+ "mcp-proxy" ,
296+ `${ engine } : ephemeral stdio + persistent via bridge/proxy` ,
297+ {
298+ ephemeral : Object . keys ( ephemeral ) ,
299+ persistent : Object . keys ( persistentOnly ) ,
300+ bridgeRunning : persistentBridge . isRunning ( ) ,
301+ } ,
302+ ) ;
303+ return [ ...out , ...( passthroughResolved as McpServer [ ] ) ] ;
180304 }
181305
182- const proxyScriptPath = resolveProxyEntry ( ) ;
183306 if ( ! proxyScriptPath ) {
184307 debugLog (
185308 "mcp-proxy" ,
@@ -188,22 +311,6 @@ export async function rewriteMcpServersForEngine(
188311 return [ ...hostMapToAcpServers ( merged ) , ...( passthroughResolved as McpServer [ ] ) ] ;
189312 }
190313
191- const persistentNames = persistentNamesFromEnv ( ) ;
192- const persistent : Record < string , HostStdioServerEntry > = { } ;
193- for ( const [ name , item ] of Object . entries ( merged ) ) {
194- if ( isHostRemoteEntry ( item ) ) continue ;
195- if ( persistentNames . has ( name ) || item . persistent ) {
196- persistent [ name ] = {
197- command : item . command ,
198- args : item . args ,
199- env : item . env ,
200- persistent : true ,
201- } ;
202- }
203- }
204-
205- const runningBridge = await ensurePersistentMcpBridge ( persistent ) ;
206-
207314 const rewritten = rewriteServersToProxyCommands ( merged , {
208315 proxyScriptPath,
209316 nodeBinPath : process . execPath ,
@@ -217,15 +324,7 @@ export async function rewriteMcpServersForEngine(
217324 return [ ...hostMapToAcpServers ( merged ) , ...( passthroughResolved as McpServer [ ] ) ] ;
218325 }
219326
220- const out : McpServer [ ] = Object . entries ( rewritten ) . map ( ( [ name , entry ] ) => ( {
221- name,
222- command : entry . command ,
223- args : entry . args ,
224- // ACP McpServerStdio.env 必填(不可为 undefined)
225- env : entry . env
226- ? Object . entries ( entry . env ) . map ( ( [ n , v ] ) => ( { name : n , value : v } ) )
227- : [ ] ,
228- } ) ) ;
327+ const out = proxyCommandsToAcpServers ( rewritten ) ;
229328
230329 debugLog (
231330 "mcp-proxy" ,
0 commit comments