Pre-baked Chrome for Browser MCP Tooling¶
Previously, shared/opencode.json enabled the Playwright and Chrome DevTools MCP servers, but the Shared Base Image shipped no browser. Both servers launched on demand and then failed with Could not find Google Chrome executable for channel 'stable' at /opt/google/chrome/chrome; neither ran headless, and neither disabled Chrome's sandbox, so they could not start as root in a display-less container. Every agent session saw two dead MCP servers that required manual provisioning. We decided to pre-bake a single headless Google Chrome into a dynamic image layer whenever Browser Tooling is requested, and to launch both MCP servers against that one binary with the flags the sandbox environment requires.
Considered Options¶
- Install Chrome on demand at agent run time: Leave the servers broken until an agent manually installs a browser. Rejected because it adds download latency and a network dependency to the first browser call, and leaves the MCP servers visibly failing on every fresh sandbox.
- Two separate browser builds (one per MCP server): Use Playwright's bundled Chromium for
playwrightand Puppeteer's Chrome forchrome-devtools. Rejected because both servers are only Chrome DevTools Protocol frontends that can share one binary, so two builds would duplicate hundreds of megabytes and two caches for no benefit. - Pre-baked shared Google Chrome stable: Install Google Chrome once via
npx playwright install --with-deps chromeinto a dynamic layer, resolving to/opt/google/chrome/chrome— the defaultchrome/stablechannel both servers look for — and launch both headless with the sandbox disabled. Accepted because it makes both MCP servers work out of the box from a single browser install, reuses Modal's layer cache, and follows the existing pre-baked-toolchain pattern (ADR 0001). - Explicit
--browserflag that is also implied by agent surfaces: Add a standalone--browserflag, and enable it implicitly when--opencodeor--agyis passed. Accepted because browser MCP servers work with existing--opencode/--agycommands out of the box, while Chrome remains independently provisionable for plain shells, test suites, and other tooling.
Consequences¶
shared/image.pygains abrowserparameter onbuild_sandbox_imageand abrowser_enabled(...)helper; the browser layer runsnpx -y playwright install --with-deps chrome.standard_sandbox/main.pyandvm_sandbox/main.pygain a--browserflag;--opencodeand--agyimply it throughbrowser_enabled, setENABLE_BROWSER=1, and listBrowserin the launch banner.shared/startup.shprints a Browser Tooling banner and installs the on-demandcheck-browserutility whenENABLE_BROWSER=1.shared/opencode.jsonlaunchesplaywrightwith--browser chrome --headless --no-sandboxandchrome-devtoolswith--headless --chrome-arg=--no-sandbox --chrome-arg=--disable-dev-shm-usage.- Sandboxes launched without Browser Tooling (
--browser,--opencode, or--agy) do not carry the Chrome layer. - Chrome tracks latest stable at build time; Modal's layer cache freezes the resolved version until the layer input changes.