Skip to content

Chrome DevTools MCP --autoConnect: tool calls hang indefinitely through MCPProxy stdio transport #1317

Closed

Description

@expiren

Summary

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

  1. MCPProxy should not kill the MCP process after a tool timeout - the process should stay alive so consent persists
  2. Or: MCPProxy should support a ""persistent process"" mode for MCPs that need interactive consent
  3. 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

  1. Enable Chrome toggle at chrome://inspect/#remote-debugging
  2. Add chrome-devtools-mcp with --autoConnect to MCPProxy
  3. Call any tool (e.g. list_pages)
  4. Observe: Chrome shows consent dialog, tool call hangs, MCPProxy times out at 60s
  5. Click Allow in Chrome during or after timeout
  6. Call tool again - hangs again because MCPProxy restarted the process (consent lost)

Activity

  1. Hronom commented on Sep 19, 2026

    @Hronom

    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:

    1. initializes and reports an instance_id;
    2. accepts tools/call, emits waiting_for_human, then blocks on a test gate longer than the call budget;
    3. lets the proxy call time out;
    4. proves the same PID/instance_id and connection generation remain alive;
    5. 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.

  2. Dumbris commented on Sep 20, 2026

    @Dumbris
    Member

    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_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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions