You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Chrome DevTools MCP --autoConnect: tool calls hang indefinitely through MCPProxy stdio transport #1317
Chrome DevTools MCP with --autoConnect (connecting to real Chrome via chrome://inspect/#remote-debugging toggle) fails through MCPProxy. MCP initialization succeeds instantly, but every subsequent tool call hangs until MCPProxy's 60s timeout kills it.
Environment
MCPProxy: v0.67.0 (Windows, stdio transport)
chrome-devtools-mcp: v1.9.0
Chrome: v153 (Windows 11)
Chrome toggle: chrome://inspect/#remote-debugging enabled, DevToolsActivePort present on port 5680
Behavior
What works
MCP init via --autoConnect completes in <1s through MCPProxy stdio
Direct WebSocket handshake to Chrome's CDP port succeeds (101 Switching Protocols)
chrome-devtools-mcp works perfectly with --browser-url pointing at a dedicated --user-data-dir Chrome profile
What fails
Any tool call (list_pages, navigate_page, new_page, take_snapshot) hangs indefinitely
Chrome shows a consent dialog (""Allow remote debugging?"") that the user must click
MCPProxy's tool timeout (60s) cancels the request before or shortly after the user clicks Allow
After timeout, MCPProxy kills and restarts the stdio process, destroying the consent grant
The next attempt requires a fresh consent click, creating an infinite loop
Also tested
Playwright MCP via --cdp-endpoint: Same behavior - tool calls hang waiting for consent
Playwright MCP via SSE (persistent server on port 8686): MCPProxy SSE transport connects, init succeeds, but tool calls still timeout because the CDP consent dialog blocks the Playwright server's CDP connection
Root Cause
Chrome 136+ toggle-based remote debugging requires an interactive consent click per CDP connection. MCPProxy's stdio transport model (kill process on timeout → restart → need fresh consent) is fundamentally incompatible with this flow.
Additional issue: arg stripping on some tools
MCPProxy v0.67.0 strips arguments for certain chrome-devtools tools (navigate_page, new_page) returning ""missing property 'url'"" / ""missing property 'pageId'"" even when args are correctly provided. Workaround: mcpproxy_code_execution with call_tool() passes args correctly.
Expected Behavior
MCPProxy should not kill the MCP process after a tool timeout - the process should stay alive so consent persists
Or: MCPProxy should support a ""persistent process"" mode for MCPs that need interactive consent
Or: MCPProxy should support longer per-tool timeouts (not just init timeout)
Workaround
Install chrome-devtools-mcp directly in OpenCode's opencode.json MCP config (bypassing MCPProxy), where the process lifecycle is managed differently and consent can persist.
Reproduction
Enable Chrome toggle at chrome://inspect/#remote-debugging
Add chrome-devtools-mcp with --autoConnect to MCPProxy
Call any tool (e.g. list_pages)
Observe: Chrome shows consent dialog, tool call hangs, MCPProxy times out at 60s
Click Allow in Chrome during or after timeout
Call tool again - hangs again because MCPProxy restarted the process (consent lost)
I checked current main because there are two lifetimes mixed together here: the tools/call request and the managed stdio process/connection generation.
On current main, core/client.go wraps the call in call_tool_timeout (default 2m) and returns a timeout error, while managed/calltool_cancel_test.go explicitly asserts that this timeout must leave the upstream Ready and must not evict it. So if the child PID/connection generation really changes after the 60s failure on v0.67.0, that looks like a second owner reacting to cancellation/health state rather than the intended per-call timeout path.
A useful regression fixture would be a stdio MCP that:
initializes and reports an instance_id;
accepts tools/call, emits waiting_for_human, then blocks on a test gate longer than the call budget;
lets the proxy call time out;
proves the same PID/instance_id and connection generation remain alive;
releases the gate and defines what happens to the late response before allowing another call.
That last part matters: preserving the process fixes the consent loop, but a timed-out browser mutation is still an ambiguous outcome. I would return a typed waiting_human/deadline_exceeded result with a call id, reject blind retry while that call is unresolved, and require either explicit cancellation or a fresh read-only reconciliation (list_pages) on the same generation. A longer timeout is then a workaround, not the correctness contract.
For diagnosis, logging {server, pid/instance_id, connection_generation, request_id, cancellation_source, resulting_state} at timeout should show which layer actually restarts v0.67.0. The argument-stripping symptom should probably stay separate unless the same trace proves it shares the dispatch path.
I maintain Hronaut, a local persistent browser workspace, so this human-consent/session-lifetime boundary is directly relevant to our work. This analysis is AI-assisted; I have not reproduced the Windows/Chrome flow.
You're right on both counts. The process kill wasn't the per-call timeout path (that one already leaves the connection alone, there's a test asserting exactly that) — it was a second, independent thing: the background health check's own concurrent ping timing out on the same single-flight stdio pipe, which then tripped a reconnect that killed the process out from under the still-running call. Fixed in #1320, along with a couple more automatic-reconnect paths that had the same problem once I went looking.
Arg-stripping was indeed a separate bug, also fixed in #1320 — nothing to do with the dispatch/health path, just a plain arg-resolution bug in call_tool_write.
Your regression fixture idea (block a call past its timeout, confirm the same process/connection survives) is basically what the new tests do. The last part of your comment — typed waiting_human/deadline_exceeded results, blocking retries on an unresolved call, structured timeout logging — is a good idea but a separate, bigger piece of work. Opened #1321 to track it so it doesn't get lost.
Activity
Hronom commentedon Sep 19, 2026
I checked current
mainbecause there are two lifetimes mixed together here: thetools/callrequest and the managed stdio process/connection generation.On current main,
core/client.gowraps the call incall_tool_timeout(default 2m) and returns a timeout error, whilemanaged/calltool_cancel_test.goexplicitly asserts that this timeout must leave the upstreamReadyand must not evict it. So if the child PID/connection generation really changes after the 60s failure on v0.67.0, that looks like a second owner reacting to cancellation/health state rather than the intended per-call timeout path.A useful regression fixture would be a stdio MCP that:
instance_id;tools/call, emitswaiting_for_human, then blocks on a test gate longer than the call budget;instance_idand connection generation remain alive;That last part matters: preserving the process fixes the consent loop, but a timed-out browser mutation is still an ambiguous outcome. I would return a typed
waiting_human/deadline_exceededresult with a call id, reject blind retry while that call is unresolved, and require either explicit cancellation or a fresh read-only reconciliation (list_pages) on the same generation. A longer timeout is then a workaround, not the correctness contract.For diagnosis, logging
{server, pid/instance_id, connection_generation, request_id, cancellation_source, resulting_state}at timeout should show which layer actually restarts v0.67.0. The argument-stripping symptom should probably stay separate unless the same trace proves it shares the dispatch path.I maintain Hronaut, a local persistent browser workspace, so this human-consent/session-lifetime boundary is directly relevant to our work. This analysis is AI-assisted; I have not reproduced the Windows/Chrome flow.
Dumbris commentedon Sep 20, 2026
Good catch, thanks for digging into this.
You're right on both counts. The process kill wasn't the per-call timeout path (that one already leaves the connection alone, there's a test asserting exactly that) — it was a second, independent thing: the background health check's own concurrent ping timing out on the same single-flight stdio pipe, which then tripped a reconnect that killed the process out from under the still-running call. Fixed in #1320, along with a couple more automatic-reconnect paths that had the same problem once I went looking.
Arg-stripping was indeed a separate bug, also fixed in #1320 — nothing to do with the dispatch/health path, just a plain arg-resolution bug in
call_tool_write.Your regression fixture idea (block a call past its timeout, confirm the same process/connection survives) is basically what the new tests do. The last part of your comment — typed
waiting_human/deadline_exceededresults, blocking retries on an unresolved call, structured timeout logging — is a good idea but a separate, bigger piece of work. Opened #1321 to track it so it doesn't get lost.fix: don't kill an in-flight stdio process on ambiguous health-check …
Merge upstream/main into feat/tool-response-save-to-file