Version
1.61.0-alpha-1778188671000
Steps to reproduce
- Start MCP with
--extension and --port (activates Streamable HTTP transport):
npx -y @playwright/mcp@latest \
--extension \
--port 8068 \
--executable-path /usr/bin/google-chrome
- Connect any MCP client that speaks Streamable HTTP.
- Call a single tool, e.g.
browser_navigate(url: "https://github.com").
- Wait for the tool to succeed.
- Wait 5–10 seconds without making another call.
- 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 true → false 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
Version
1.61.0-alpha-1778188671000
Steps to reproduce
--extensionand--port(activates Streamable HTTP transport):browser_navigate(url: "https://github.com").browser_snapshot.Expected behavior
connect.htmlis not reopened.Actual behavior
chrome-extension://.../connect.htmltab appears in the browser.Logs show that the session kills itself after ~5 seconds:
Then the next POST creates a brand new session with a new relay UUID and a new
connect.htmltab.Additional context
Why Streamable HTTP is affected
The MCP server starts a heartbeat (
startHeartbeat) for every HTTP session. The heartbeat callsserver.ping()every 3 seconds and expects a response within 5 seconds. If no response arrives, it callsserver.close(), which tears down the entire session including theBrowserBackendand theCDPRelayServer.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.tsline 146:For comparison, the SSE transport already disables heartbeat correctly (line 117):
Why extension mode makes it worse
In
--extensionmode, each new session creates a newCDPRelayServer, which spawns a fresh Chrome process pointing atchrome-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
true→falseinhandleStreamableeliminates the problem entirely. The same one-line change was tested locally inplaywright-core/lib/coreBundle.js:64874:After the patch:
connect.htmlis opened only once.browser_navigate,browser_snapshot,browser_click, andbrowser_tabsall operate on the same persistent tab.Environment