Repository navigation
feat(drivers): the browser computer serves Playwright MCP's browser_* tools, attached over CDP (FD-4955) - #615
Conversation
… tools, attached over CDP (FD-4955) The page driver installs Node 24.22.2 and Playwright MCP 0.0.84 from pinned downloads and serves its tools, with --caps vision, beside computer_*: a Playwright MCP it runs attached to the browser over a CDP address the browser's relay issues it, a session per caller, started again when it exits or the browser's data reset ends its address, ended with the driver, with none of the driver's environment and its file access confined to ~/Uploads. browser_evaluate and browser_run_code_unsafe are hidden by default, through ForwardedTool.hidden_by_default, which hides a forwarded tool as @tool(hidden_by_default=True) hides one of an env's own. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| session = await self._session(ToolContext.current().caller.session) | ||
| return await session.call_tool(tool, arguments) |
There was a problem hiding this comment.
If Playwright MCP exits during a tool call, call() can remain stuck on session.call_tool(). The repository documents that this MCP client can leave requests pending after a response stream breaks. This client has neither the gateway's transport-failure reporting nor a wait for child exit. The HTTP timeout alone does not bound the pending MCP request.
Watch for child exit or transport failure, fail the pending call, and close its session without retrying the action.
Knowledge Base Used: Fail Child Calls Promptly After a Gateway Transport Loss
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agent_env/drivers/envs/page/driver.py
Line: 454-455
Comment:
**Browser calls can stay stuck**
If Playwright MCP exits during a tool call, `call()` can remain stuck on `session.call_tool()`. The repository documents that this MCP client can leave requests pending after a response stream breaks. This client has neither the gateway's transport-failure reporting nor a wait for child exit. The HTTP timeout alone does not bound the pending MCP request.
Watch for child exit or transport failure, fail the pending call, and close its session without retrying the action.
**Knowledge Base Used:** [Fail Child Calls Promptly After a Gateway Transport Loss](https://app.greptile.com/-/switch-tenant?tenant=scale-ai&next=%2Fcustom-context%2Fknowledge-base%2Fscaleapi%2Fagentenv-framework%2F-%2Freverts%2Fincident-mitigation_165-20261013-child-call-hangs-c3b98d7.md)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| held = self._sessions.get(caller) | ||
| if held is None or held.url != url or held.owner.done(): | ||
| if held is not None: | ||
| held.close.set() | ||
| held = self._sessions[caller] = await _Session.open(url) | ||
| return held.session |
There was a problem hiding this comment.
Disconnected sessions keep running
Every new caller adds a live _Session to _sessions, but ending the caller's MCP session never removes or closes it. Its owner task keeps waiting on close, holding an HTTP client and a downstream MCP session. Reconnects keep adding resources until the child restarts or the browser resets.
Add cleanup when the caller disconnects, or close idle sessions after a bounded time.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agent_env/drivers/envs/page/driver.py
Line: 464-469
Comment:
**Disconnected sessions keep running**
Every new caller adds a live `_Session` to `_sessions`, but ending the caller's MCP session never removes or closes it. Its owner task keeps waiting on `close`, holding an HTTP client and a downstream MCP session. Reconnects keep adding resources until the child restarts or the browser resets.
Add cleanup when the caller disconnects, or close idle sessions after a bounded time.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| self._running_on, self._url = endpoint, f"http://127.0.0.1:{port}/mcp" | ||
| await _listening(port, self._process) |
There was a problem hiding this comment.
_running() records the child as running before _listening() succeeds. If startup times out while the child stays alive, later calls reuse the same unready URL because returncode is still None. They cannot recover by starting a fresh child.
Stop and clear the child when readiness fails or startup is cancelled. Save the successful startup state only after the readiness check passes.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agent_env/drivers/envs/page/driver.py
Line: 485-486
Comment:
**Failed startup cannot recover**
`_running()` records the child as running before `_listening()` succeeds. If startup times out while the child stays alive, later calls reuse the same unready URL because `returncode` is still `None`. They cannot recover by starting a fresh child.
Stop and clear the child when readiness fails or startup is cancelled. Save the successful startup state only after the readiness check passes.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.
Summary
M8.8 (FD-4955), migration step 1. The browser computer, chromium alone, now serves Playwright MCP's
browser_*tools at its/mcp, besidecomputer_*and the page tools. Playwright MCP is 0.0.84, with--caps visionas today'splaywright-mcpruns, attached to chromium over CDP.browser_evaluate(JavaScript in the page) andbrowser_run_code_unsafe(code in Playwright MCP's own process).What changes
The page driver (
drivers/envs/page/)@playwright/mcp,playwrightandplaywright-core, which have no other dependencies.install-playwright-mcp.shlays them out as npm would, since an install has no network and the composer locks no npm packages.forwarded_tools, fromplaywright-mcp-tools.json, 0.0.84's owntools/listwithvision. An integration test checks the manifest against the installed version.playwright-mcp. The address is under the driver's loopback origin (fix(browser): CDP addresses through a TLS-ending tunnel are https and wss (FD-5470) #608'sorigin), through the driver's/svc/chromium/route, so the relay's guard applies to it as to any CDP client, and its connections show in the record.--isolated, no--headless, no idle timeout.setpriv --pdeathsigends it with the driver, so a restarted driver finds none of its predecessor's.~/Uploads, its working folder with--ignore-roots, matchingcomputer_upload.The SDK (
agentenv_protocol)ForwardedTool.hidden_by_defaulthides a forwarded tool as@tool(hidden_by_default=True)hides one of the env's own:_metamarkertools="*"Not in this PR:
playwright-mcp's users is M8.27's bar.The 0.0.41 → 0.0.84 contract change is on FD-4975 for that measurement:
browser_installis gone, and eight tools are new.refis renamedtargetin seven tools.Decisions (FD-4955, in chat with Edgar)
--caps visionis on, as today.browser_file_uploadis confined to~/Uploads. The rest are visible.ForwardedTool.hidden_by_defaultin the SDK, rather than a startup tool-access rule, which "every tool" steps and restarts would undo.Test plan
Unit:
_metamarker.tst/unit/drivers,env,bundleandtask_step6,022 passed, apart from the known Mac-only Windows disk-recipe case.Integration, local Docker (Mac, arm64): the new browser computer test, beside gate 3, the computer tools, the TLS front and
browser_connect_cdp: 5 of 5. On the real composed computer:browser_snapshotandbrowser_typetype into the page, and its viewport is unchangedbrowser_clickopens a confirm thatbrowser_handle_dialoganswersbrowser_evaluateonce it's namedtools/listequals the manifestplaywright-mcpconnectionA real agent, local Docker: Claude Code (Sonnet 5.5) completed a synthetic-page task through
browser_*alone. It snapshotted, typedlantern, clicked Reveal and answeredLANTERN-7777. Its trajectory has onlybrowser_*calls.A real agent,
modal_vm:LANTERN-7777, and the check scored 1.0.browser_snapshot,browser_type,browser_clickandbrowser_find.amd64 on local Docker is covered by CI's slow tier, which runs
browser_computer_test.pyon Linux amd64.Chaos and e2e, local Docker (arm64), timed. Playwright MCP's tools (navigate, type, click, read) still work:
browser_closereset_env's fresh containerEach answered within about a second.
Known limit: two
browser_*agents on one browser at once interfere. Today'splaywright-mcpgives each MCP session an isolated, headless context of its own. Here every session shares the one headful browser and its default context:One agent works, and so does a CDP agent beside it. Keeping agents apart is contexts' work (M8.15, M8.16). It's recorded on M8.27 (FD-4975) for its no-regression bar.
Docs
The user guide's browser page should cover:
browser_*tools the browser computer serves (Playwright MCP 0.0.84,vision)~/Uploadsas their file root🤖 Generated with Claude Code
Fix pending browser calls after child failure before merging.
Fix with agent prompt
Summary
Adds Playwright MCP's
browser_*tools to the page driver, attached to the existing browser through its guarded CDP relay.earakely-scale explicitly identifies the changed tool names and shared browser context as intentional, with compatibility measurement deferred to FD-4975.
Diagram
%%{init: {'theme': 'neutral'}}%% flowchart LR Agent[Agent MCP session] --> Driver[Page driver] Driver --> Access[Tool access checks] Access --> Session[Caller-specific MCP session] Session --> Child[Playwright MCP] Child --> Relay[Guarded CDP relay] Relay --> Browser[Existing Chromium browser]Reviews (1) · Last reviewed commit: "feat(drivers): the browser computer serv..." · Reviewed by Greptile