Skip to content

feat(drivers): the browser computer serves Playwright MCP's browser_* tools, attached over CDP (FD-4955) - #615

Merged
earakely-scale merged 8 commits into
mainfrom
edgararakelyan/fd-4955-playwright-mcp-on-chromium
Nov 11, 2026
Merged

earakely-scale merged 8 commits into
mainfrom
edgararakelyan/fd-4955-playwright-mcp-on-chromium

Conversation

@earakely-scale

@earakely-scale earakely-scale commented Nov 11, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

M8.8 (FD-4955), migration step 1. The browser computer, chromium alone, now serves Playwright MCP's browser_* tools at its /mcp, beside computer_* and the page tools. Playwright MCP is 0.0.84, with --caps vision as today's playwright-mcp runs, attached to chromium over CDP.

  • A tool-based agent such as Claude Code drives the env's own browser, the one CDP agents like Browser Use drive. That gives the benchmarks their third arm on the same browser (Both-2, Both-3).
  • Tasks keep Playwright MCP's tool names.
  • Hidden by default, until a task's tool access names them: browser_evaluate (JavaScript in the page) and browser_run_code_unsafe (code in Playwright MCP's own process).

What changes

The page driver (drivers/envs/page/)

  • Install: it installs Node 24.22.2 and Playwright MCP 0.0.84 from pinned downloads.
    • Node's Linux tarball for each architecture, pinned to nodejs.org's SHASUMS256.
    • The three npm packages, checked against npm's integrity: @playwright/mcp, playwright and playwright-core, which have no other dependencies.
    • install-playwright-mcp.sh lays them out as npm would, since an install has no network and the composer locks no npm packages. It lays out whatever packages were downloaded, by the name each package.json gives, and fails the build when a package's dependency isn't there at the version it names.
  • Upgrading: python scripts/upgrade_playwright_mcp.py <version> pins a version:
    • the downloads in env.toml, following @playwright/mcp's dependencies (refusing a range, since the install takes exact versions)
    • the manifest, from that version's own tools/list, run on this machine's Node of the same version
    • it prints the tools that are new or gone. Run at 0.0.84, it writes what's committed.
  • Serving: it serves Playwright MCP's tools through forwarded_tools, from playwright-mcp-tools.json, 0.0.84's own tools/list with vision. An integration test checks the manifest against the installed version.
  • Code: Playwright MCP's process and sessions are in page/playwright_mcp.py, beside driver.py, which imports it as cua-driver imports hyprland.py.
  • Running Playwright MCP:
    • It attaches to the browser over a CDP address the browser issues to the user 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's origin), 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.
    • It never starts a browser of its own: no --isolated, no --headless, no idle timeout.
    • It starts on the first call. It starts again once it has exited, and again on a fresh address when the browser's data reset ends the last one.
    • setpriv --pdeathsig ends it with the driver, so a restarted driver finds none of its predecessor's.
  • Sessions: each caller of the driver's tools gets a session of its own on Playwright MCP, so each agent keeps its own snapshots and tab.
  • The action log (feat(browser): a browser's log of every agent action and tab event, across CDP, MCP tools and pixels (FD-4967) #613):
    • Playwright MCP's CDP commands are in the browser's log like any CDP client's, under the user playwright-mcp.
    • The driver's own check of Playwright MCP's address sends no tool-call header, so the log doesn't credit it to the agent's browser_* call.
    • Crediting Playwright MCP's commands to the browser_* call they serve is a follow-up.
  • Confinement:
    • It runs with none of the driver's environment, so the control token and the deploy's settings stay out of its process.
    • Its file access is the session user's ~/Uploads, its working folder with --ignore-roots, matching computer_upload.

The SDK (agentenv_protocol)

  • ForwardedTool.hidden_by_default hides a forwarded tool as @tool(hidden_by_default=True) hides one of the env's own:
    • listed with the _meta marker
    • hidden from every role until a tool-access step names it
    • never shown by tools="*"
    • fixed, so it survives a server restart
  • The field has a default, so existing callers are unaffected.

ForwardedTool.listed(tool, call) builds a forwarded tool from another server's tools/list entry: its name, description, schemas and annotations as listed. The five drivers that forward tools (page, cua-driver, agent-desktop, aside, codex-computer-use) use it, each forwarding the same tools as before.

mcp_port() is the port create_fastmcp_app serves on (MCP_PORT, else 18765), which the page driver's loopback origin reads rather than repeating the default.

Not in this PR:

  • Playwright MCP in a desktop computer's chromium
  • making chromium the default website browser (M8.27). No regression for 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_install is gone, and eight tools are new.
  • ref is renamed target in seven tools.
  • The context is shared, not isolated per session.

Decisions (FD-4955, in chat with Edgar)

  1. Playwright MCP runs in the page driver, whose tools it serves, rather than in the chromium app or an app of its own. Apps can't serve MCP tools to agents today.
  2. It reaches the browser through a relay-issued CDP address, rather than a debugging port that would skip the guard.
  3. The offline install is pinned downloads unpacked by a script, rather than a composer npm lock or NodeSource's apt repository.
  4. Versions: Playwright MCP 0.0.84 and Node 24 LTS. 0.0.85 has the same tools and arguments, and its changes are for extension mode only.
  5. --caps vision is on, as today.
  6. Hidden by default: the two code tools. browser_file_upload is confined to ~/Uploads. The rest are visible.
  7. ForwardedTool.hidden_by_default in the SDK, rather than a startup tool-access rule, which "every tool" steps and restarts would undo.

Test plan

  • Unit:

    • The SDK: a forwarded tool hidden by default stays hidden through an "every tool" rule until a rule names it, and is then listed with the _meta marker.
    • The page driver:
      • it serves the manifest's 31 tools, hiding exactly the two
      • the manifest lists exactly the 31 tools reviewed, so an upgrade that adds one fails until it's decided whether to hide it
      • the manifest is of the version env.toml downloads
      • it asks for its address under its loopback origin, without the agent's tool-call header
      • Playwright MCP's command attaches and never launches a browser
    • Against a stand-in Playwright MCP:
      • it starts on the first call and keeps a session per caller
      • it starts again on a new address, or once killed
      • it doesn't get the driver's control token
      • an exit at start is refused, saying why
    • Results: the protocol package 1,417 passed. tst/unit/drivers, env, bundle and task_step 6,022 passed, apart from the known Mac-only Windows disk-recipe case.
    • After merging main (feat(browser): a browser's log of every agent action and tab event, across CDP, MCP tools and pixels (FD-4967) #613): the protocol package and tst/unit/drivers, 1,588 passed, 1 skipped.
    • The SDK: ForwardedTool.listed and mcp_port. After the refactor: the protocol package, tst/unit/drivers and tst/unit/bundle, 2,822 passed, 12 skipped; tst/unit/env, 2,228 passed, apart from the Mac-only Windows disk-recipe case and a recorder timing case that passes alone.
  • The install script, in an Ubuntu 24.04 container (arm64), with no network: with the pinned downloads it lays out the three packages and cli.js --version answers 0.0.84; without playwright-core it fails, naming both packages that need it.

  • 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:

    • the tools are listed, the two code tools hidden
    • browser_snapshot and browser_type type into the page, and its viewport is unchanged
    • browser_click opens a confirm that browser_handle_dialog answers
    • tool access shows browser_evaluate once it's named
    • the installed Playwright MCP's own tools/list equals the manifest
    • after Playwright MCP is killed, and after the driver is killed, its tools answer again, with one Playwright MCP left
    • the record shows the playwright-mcp connection
  • A real agent, local Docker: Claude Code (Sonnet 5.5) completed a synthetic-page task through browser_* alone. It snapshotted, typed lantern, clicked Reveal and answered LANTERN-7777. Its trajectory has only browser_* calls.

  • A real agent, modal_vm:

    • The browser computer was composed on a Modal builder VM from this branch, and gate 3 made it ready there (amd64).
    • Claude Code ran on this Mac and drove it through Modal's tunnel. It answered LANTERN-7777, and the check scored 1.0.
    • Its trajectory has only browser_snapshot, browser_type, browser_click and browser_find.
  • amd64 on local Docker is covered by CI's slow tier, which runs browser_computer_test.py on Linux amd64.

  • Chaos and e2e, local Docker (arm64), timed. Playwright MCP's tools (navigate, type, click, read) still work:

    • beside a CDP agent connected at the same time
    • after browser_close
    • after the browser's data reset, on the fresh address the driver gets
    • after the app server is killed and Chrome relaunched
    • after Playwright MCP is killed
    • after reset_env's fresh container
    • one Playwright MCP is running at the end

    Each answered within about a second.

Known limit: two browser_* agents on one browser at once interfere. Today's playwright-mcp gives each MCP session an isolated, headless context of its own. Here every session shares the one headful browser and its default context:

  • On the same tab, their navigations interrupt each other.
  • On a tab each, only the newest is in front. A click on the other waits for the page to render, which a background tab never does, and times out.

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.

Fixed in #628: an intermittent browser_evaluate hang. In a new session, browser_evaluate sometimes got no answer from Playwright MCP within the 300 s bound. A stress loop reproduced it within 2 to 16 rounds.

The cause is in the browser's CDP relay, not here. It could deliver Runtime.executionContextCreated ahead of Page.getFrameTree's answer, so Playwright dropped the page's main world. #628 makes the relay keep the browser's order.

With #628 on this branch, 80 rounds in a row answered within 0.6 s each. Until it merges, this PR's browser computer test can rarely time out at browser_evaluate.

Docs

The user guide's browser page should cover:

  • the browser_* tools the browser computer serves (Playwright MCP 0.0.84, vision)
  • the two code tools hidden by default and how a task shows them
  • ~/Uploads as their file root

🤖 Generated with Claude Code

RetriggerView in GreptileConfidence Score: 5/5

The PR appears safe to merge; no new blocking issue was found.

Summary

The page driver serves Playwright MCP’s browser_* tools on the computer’s existing browser through guarded CDP access.

  • Browser computers now serve Playwright MCP tools on their shared Chromium.
  • Forwarding drivers can reuse the tool details listed by another server.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  Agent[Agent] --> Driver[Page driver MCP]
  Driver --> Access[Tool access check]
  Access --> MCP[Playwright MCP session]
  MCP --> Relay[Guarded CDP relay]
  Relay --> Browser[Existing Chromium browser]
Loading

Reviews (11) · Last reviewed commit: "fix(drivers): let the session user read ..." · Reviewed by Greptile

… 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>
@earakely-scale
earakely-scale requested a review from a team as a code owner November 11, 2026 05:42
… sessions close, a stuck start is stopped

A call races Playwright MCP's exit and its session's end, so one Playwright MCP stops answering fails then, without
being sent again, since it may have acted. A caller's session left idle for an hour is closed when another caller
comes, since a caller's own MCP session ends with no word to the driver. A start that never listens is stopped, and
its address and URL are recorded only once it does, so the next call starts afresh.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
earakely-scale and others added 2 commits November 11, 2026 06:09
A caller cancelled while its call waited left the call to Playwright MCP running; it's cancelled and awaited now, and
not sent again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A call is bounded by PLAYWRIGHT_MCP_CALL_TIMEOUT_S, and one that times out or fails in transport closes its session, so
the next call opens a fresh one rather than reusing a session already closing. Opening a session is bounded by
PLAYWRIGHT_MCP_START_S, and a Playwright MCP that can't open one in that time is stopped, so the next call starts it
afresh instead of every caller waiting on the one that hangs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
earakely-scale and others added 4 commits November 11, 2026 06:56
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The driver's own check of Playwright MCP's CDP address is sent without the
agent's tool-call header, so the browser's log doesn't credit it to the
browser_* call it's made during.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… script

- scripts/upgrade_playwright_mcp.py pins a version: the install's downloads in
  env.toml, every npm package Playwright MCP needs checked against npm's
  integrity, and the manifest from that version's tools/list. Run at 0.0.84 it
  writes what was committed.
- install-playwright-mcp.sh lays out whatever packages were downloaded, by the
  name each package.json gives, and fails when a dependency isn't there at the
  version it names.
- The unit test pins the manifest's tool names, so an upgrade that adds a tool
  fails until someone looks at whether to hide it, and checks the manifest is
  of the version env.toml downloads.
- Playwright MCP's process and sessions move to page/playwright_mcp.py.
- ForwardedTool.listed builds a forwarded tool from another server's tools/list
  entry; the five drivers that forward tools use it.
- mcp_port() is the port create_fastmcp_app serves on, which the page driver's
  CDP origin now reads instead of repeating its default.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The install moved each package's mktemp folder into node_modules, and mktemp
makes its folder 0700, so the session user's Playwright MCP couldn't load
its own cli.js and exited at start.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@earakely-scale
earakely-scale merged commit 4ceb0bc into main Nov 11, 2026
61 of 64 checks passed
@earakely-scale
earakely-scale deleted the edgararakelyan/fd-4955-playwright-mcp-on-chromium branch November 11, 2026 23:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant