Skip to content

[Bug]: [MCP] Streamable HTTP heartbeat unconditionally kills sessions, causing extension mode to reopen connect.html on every tool call #41182

@R-omk

Description

@R-omk

Version

1.61.0-alpha-1778188671000

Steps to reproduce

  1. Start MCP with --extension and --port (activates Streamable HTTP transport):
    npx -y @playwright/mcp@latest \
      --extension \
      --port 8068 \
      --executable-path /usr/bin/google-chrome
  2. Connect any MCP client that speaks Streamable HTTP.
  3. Call a single tool, e.g. browser_navigate(url: "https://github.com").
  4. Wait for the tool to succeed.
  5. Wait 5–10 seconds without making another call.
  6. Call any tool again, e.g. browser_snapshot.

Expected behavior

  • The second tool call reuses the existing session.
  • connect.html is not reopened.
  • The browser tab from step 3 remains controllable.

Actual behavior

  • A new chrome-extension://.../connect.html tab appears in the browser.
  • The client is asked to approve the connection again.
  • The previous tab context is lost.

Logs show that the session kills itself after ~5 seconds:

2026-06-07T17:14:51.335Z pw:mcp:test create http session
2026-06-07T17:15:11.301Z pw:mcp:relay CDP relay server started
...
2026-06-07T17:15:16.956Z pw:mcp:test delete http session   ← 5s later
2026-06-06-07T17:15:16.957Z pw:mcp:test close browser
2026-06-07T17:15:16.959Z pw:mcp:relay closing extension connection

Then the next POST creates a brand new session with a new relay UUID and a new connect.html tab.

Additional context

Why Streamable HTTP is affected

The MCP server starts a heartbeat (startHeartbeat) for every HTTP session. The heartbeat calls server.ping() every 3 seconds and expects a response within 5 seconds. If no response arrives, it calls server.close(), which tears down the entire session including the BrowserBackend and the CDPRelayServer.

This design works for transports with a persistent bidirectional connection (SSE, STDIO, WebSocket), but it is fundamentally incompatible with Streamable HTTP, which is request–response based. Once the HTTP response is sent back to the client, the server has no open channel to deliver a ping. The ping always times out, so the session always dies after ~5 seconds of idleness.

Code locations:

packages/playwright-core/src/tools/utils/mcp/http.ts line 146:

await mcpServer.connect(serverBackendFactory, transport, true); // runHeartbeat = true

For comparison, the SSE transport already disables heartbeat correctly (line 117):

await mcpServer.connect(serverBackendFactory, transport, false); // runHeartbeat = false

Why extension mode makes it worse

In --extension mode, each new session creates a new CDPRelayServer, which spawns a fresh Chrome process pointing at chrome-extension://.../connect.html. When the heartbeat kills the session, the next tool call repeats this entire process, so the user sees a flood of connect-page tabs and must approve each one manually.

Verified fix

Changing truefalse in handleStreamable eliminates the problem entirely. The same one-line change was tested locally in playwright-core/lib/coreBundle.js:64874:

-        await connect(serverBackendFactory, transport, true);
+        await connect(serverBackendFactory, transport, false);

After the patch:

  • The session stays alive between tool calls.
  • connect.html is opened only once.
  • browser_navigate, browser_snapshot, browser_click, and browser_tabs all operate on the same persistent tab.

Environment

@playwright/mcp version: 0.0.75 (latest)
playwright-core version: 1.61.0-alpha-1778188671000
Browser: Chrome/Brave/Edge with Playwright Extension (mmlmfjhmonkocbjadbfplnigmagldckm)
OS: Linux

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions