LumaBrowser is a drop-in Chrome DevTools Protocol target for Playwright. Its CDP server runs on port 9222 inside the browser, so chromium.connectOverCDP attaches unchanged — every locator, click, fill, goto, and screenshot keeps working. The Lumabyte.* domain adds LLM-powered selector fallback, and Playwright MCP attaches to the same endpoint via --cdp-endpoint http://127.0.0.1:9222.
Looking for the marketing intro? Start at Self-Healing Selectors — the hub page covers Selenium, Playwright, and Puppeteer side-by-side. This page is the Playwright-specific deep dive.
Playwright is a cross-browser automation library that can drive its own protocol natively or attach to an existing Chromium over CDP. LumaBrowser is the browser itself, exposing CDP on port 9222 and the Lumabyte.* domain for LLM-resolved selectors. chromium.connectOverCDP is the documented path — the same path every cloud-browser provider (Browserless, Browserbase, Steel.dev) uses — so the move is a one-line change.
| Capability | Playwright | LumaBrowser |
|---|---|---|
| Attach method | chromium.launch() or connectOverCDP() | chromium.connectOverCDP('http://127.0.0.1:9222') |
| Chromium binary | Downloaded via npx playwright install | None — LumaBrowser is the browser |
Locators, auto-wait, expect(...) | Full support | Full support — client-side, unchanged over CDP |
| LLM selector fallback | Not available | Lumabyte.find, Lumabyte.click via newCDPSession |
| DOM + AX tree + screenshot | Three separate CDP calls | One call via Lumabyte.domSnapshot |
| Firefox / WebKit | Native | Chromium only — connectOverCDP is Chromium-only by design |
Playwright MCP (@playwright/mcp) | Bundles its own browser | Attaches via --cdp-endpoint http://127.0.0.1:9222 |
Playwright codegen | Records against its own browser | Record elsewhere, execute against LumaBrowser |
| Also works with Puppeteer / Selenium | No | Yes — same CDP on 9222, separate WebDriver on 9515 |
| MCP server for AI agents | Via @playwright/mcp (separate) | Built-in local MCP server |
| Setup | npm i playwright && npx playwright install | npx lumabrowser start |
LumaBrowser’s cdp-driver extension is a standards-compliant CDP WebSocket server on port 9222 — the same port and discovery surface Chromium exposes when launched with --remote-debugging-port=9222. Playwright’s chromium.connectOverCDP is the documented path for driving a pre-existing Chromium over CDP. What you don’t have to change:
playwright or @playwright/test install. Works from JavaScript/TypeScript, Python, .NET, and Java bindings — they all speak connectOverCDP.browser.contexts()[0] resolves on connect. LumaBrowser advertises a default BrowserContext so Playwright’s “attach to the existing context” idiom works immediately.Network.setUserAgentOverride, Fetch.enable, Emulation.setDeviceMetricsOverride, DOMSnapshot.captureSnapshot — all pass through to Chromium.page.context().newCDPSession(page) still works. That’s how you dispatch custom CDP domains from Playwright — including LumaBrowser’s Lumabyte.* methods.Just change the connect:
// Launches a bundled Chromium binary.
const { chromium } = require('playwright');
const browser = await chromium.launch({
headless: false,
});
const context = await browser.newContext();
const page = await context.newPage();
await page.goto('https://example.com');
console.log(await page.title());
await browser.close();// LumaBrowser is already running. Attach.
const { chromium } = require('playwright');
const browser = await chromium.connectOverCDP(
'http://127.0.0.1:9222',
);
const context = browser.contexts()[0]
?? await browser.newContext();
const page = await context.newPage();
await page.goto('https://example.com');
console.log(await page.title());
await browser.close();That’s the entire migration. Change launch(...) to connectOverCDP(url), pick up the implicit default context, and point at port 9222. Everything below is what you gain by opting in.
Three concrete wins. Each one is opt-in: pay the cost only where you want the benefit.
Playwright’s locators are already smart (role-based, text-based, test-id-first) — but they still resolve against a concrete DOM. Change a data-testid and getByTestId('submit') breaks the same way any other selector does. LumaBrowser’s Lumabyte.* CDP domain adds a description-based resolver that retries via an LLM using a natural-language description of what the element is, not how it’s currently labeled.
You dispatch it via page.context().newCDPSession(page) — Playwright’s canonical escape hatch for raw CDP.
// Breaks the moment the test-id is renamed
// during a UI refresh.
const { chromium } = require('playwright');
const browser = await chromium.launch();
const page = await (await browser.newContext()).newPage();
await page.goto('https://example.shop/cart');
await page.getByTestId('signup-btn').click();// LLM fallback re-resolves when selectors miss.
const { chromium } = require('playwright');
const browser = await chromium.connectOverCDP(
'http://127.0.0.1:9222',
);
const context = browser.contexts()[0]
?? await browser.newContext();
const page = await context.newPage();
await page.goto('https://example.shop/cart');
const cdp = await context.newCDPSession(page);
await cdp.send('Lumabyte.configureFallback', {
enabled: true,
});
// Option A: keep your locator, add a description fallback.
await cdp.send('Lumabyte.click', {
description: 'the sign-up button',
selector: '[data-testid="signup-btn"]',
});
// Option B: skip selectors entirely.
await cdp.send('Lumabyte.click', {
description: 'the sign-up button',
});Playwright agent loops that need full page state typically make three separate round-trips: DOMSnapshot.captureSnapshot, Accessibility.getFullAXTree, and Page.captureScreenshot. LumaBrowser’s Lumabyte.domSnapshot returns all three in a single call, ordered and correlated server-side — and it does this over the same connectOverCDP session you already have.
This matters especially for Playwright via connectOverCDP: Playwright adds a Node.js driver hop between your client and the browser for every protocol call. Cutting 3 × RTT to 1 × RTT is meaningful when you’re paying two network hops per round-trip.
// Three CDP round-trips, each going client →
// Playwright driver → browser → back.
const cdp = await page.context().newCDPSession(page);
await cdp.send('DOM.enable');
await cdp.send('Accessibility.enable');
const snapshot = await cdp.send(
'DOMSnapshot.captureSnapshot',
{ computedStyles: [] }
);
const axTree = await cdp.send(
'Accessibility.getFullAXTree'
);
const { data: screenshot } = await cdp.send(
'Page.captureScreenshot',
{ format: 'png' }
);// One call, server-correlated payload.
const cdp = await page.context().newCDPSession(page);
const { snapshot, axTree, screenshot } =
await cdp.send('Lumabyte.domSnapshot', {
includeAxTree: true,
includeScreenshot: true,
});
// snapshot — DOMSnapshot.captureSnapshot
// axTree — Accessibility.getFullAXTree
// screenshot — base64 PNG of the viewport
// 1 request. Parse locally.Classic Playwright ships a ~170 MB Chromium per-platform binary, downloads it during playwright install, and launches it as a child process for every script run. CI caches drift, --user-data-dir trees pile up, the binary version lags Chrome stable. LumaBrowser’s CDP server runs inside the browser process itself — no binary download, no launcher lifecycle, no temp profile to reap.
And unlike attaching to a user’s regular Chrome (where your automation would fight for the same tabs the user is browsing in), LumaBrowser gives the client dedicated automation tabs: only tabs created with kind: 'cdp' surface as CDP targets, rendered in the tab strip with a purple accent and a CDP badge so you can watch the session drive them live.
// playwright install downloads a bundled
// Chromium and spawns it per run.
const { chromium } = require('playwright');
const browser = await chromium.launch({
headless: false,
args: ['--no-sandbox'],
});
// Plus: Chromium version drift, ~170MB
// per-platform binary download during
// `playwright install`, stale --user-data-dir
// trees, and an occasional "browser closed
// unexpectedly" from the launcher at 3am.// LumaBrowser is already running. Attach.
const { chromium } = require('playwright');
const browser = await chromium.connectOverCDP(
'http://127.0.0.1:9222',
);
// Dedicated "CDP" tabs, visible in the strip.
// No binary download — skip `playwright install`
// for the chromium target.
// No child process.
// No --user-data-dir to clean up.
// The user's regular tabs stay invisible to
// the CDP client, so Network Interceptor and
// AI Chat don't fight for the debugger lock.What a realistic flow looks like when you mix Playwright locators, native CDP, and Lumabyte.*.
const { chromium } = require('playwright');
// 1. Attach to the already-running browser.
const browser = await chromium.connectOverCDP('http://127.0.0.1:9222');
const context = browser.contexts()[0] ?? await browser.newContext();
const page = await context.newPage();
await page.goto('https://example.shop/products/coffee-grinder');
const cdp = await context.newCDPSession(page);
// 2. Opt in to LLM fallback for this session.
await cdp.send('Lumabyte.configureFallback', {
enabled: true,
onFindFail: true,
onClickIntercepted: true,
});
// 3. Mix Playwright's native locators with Lumabyte.click for anything
// where the DOM label is less stable than the intent.
await page.getByRole('button', { name: /sign up/i }).click(); // locator
await cdp.send('Lumabyte.click', { description: 'the add-to-cart button' });
await cdp.send('Lumabyte.click', { description: 'the cart icon in the header' });
// 4. Pull DOM + AX tree + screenshot in one round-trip.
const { snapshot, axTree, screenshot } = await cdp.send('Lumabyte.domSnapshot', {
includeAxTree: true,
includeScreenshot: true,
});
// 5. Playwright's own API still works on top of the same page.
console.log(await page.title());
await browser.close();Playwright’s locator API for the elements that are stable, description-based resolution for the ones that aren’t, and one round-trip for full page capture when the agent needs it. Same page, same session, no context switching between tools.
@playwright/mcp is Microsoft’s MCP server for Playwright — the idiomatic way to give an LLM agent (Claude Desktop, Cursor, OpenClaw, any MCP-compatible host) tool-driven control of a browser. It supports --cdp-endpoint for attaching to an existing browser, which is exactly what LumaBrowser exposes.
# Start LumaBrowser's CDP server (once per launch).
curl -X POST http://localhost:3000/api/cdp/start
# Point Playwright MCP at it. Your LLM agent now drives LumaBrowser.
npx @playwright/mcp --cdp-endpoint http://127.0.0.1:9222The win: the MCP server’s tool calls land in a browser with built-in LLM selector fallback. When the agent produces a locator that doesn’t quite match, the Lumabyte orchestrator catches it instead of the agent loop crashing. And the agent’s own LLM doesn’t need to regenerate the tool call from scratch.
The full row-by-row matrix — connectOverCDP bootstrap, browser contexts, locators / auto-wait / expect, navigation and input, page.route, screenshots, Playwright MCP via --cdp-endpoint, the additive Lumabyte.* methods, plus the documented limits (Firefox / WebKit, full tracing, codegen, launch()) — lives in the API reference: /apis → CDP Driver section.
Two ways, takes under a minute either way:
http://127.0.0.1:9222.Or start it programmatically: POST /api/cdp/start. Full CDP domain reference, Lumabyte.* method signatures, host/port settings, and MCP tool surface are in the CDP Driver section of the API docs.
Yes for anything that uses chromium.connectOverCDP. Replace chromium.launch(...) with chromium.connectOverCDP('http://127.0.0.1:9222') and every page, locator, click, fill, screenshot, and Network/Fetch call keeps working. LumaBrowser’s cdp-driver extension implements the Target domain, advertises a default BrowserContext on connect, and forwards every other CDP command straight through Electron’s webContents.debugger to Chromium.
expect assertions?Yes, unchanged. They’re client-side features of the Playwright driver. The server just sees the resulting CDP commands. If your script depends on getByRole, getByTestId, or expect(locator).toBeVisible(), all of that keeps working against LumaBrowser.
playwright codegen?codegen launches its own Chromium and cannot record against a connectOverCDP target — that’s a Playwright design choice. Record against your own Chromium the way you always have, then point the finished script at LumaBrowser for execution. The generated page.* calls work unchanged.
Yes. Launch @playwright/mcp with --cdp-endpoint http://127.0.0.1:9222 and it attaches to LumaBrowser like any other connectOverCDP client. This is the idiomatic 2026 pattern for LLM agents driving a browser via Playwright — LumaBrowser gives the MCP server a dedicated browser with built-in LLM selector fallback.
Same CDP server for Puppeteer (puppeteer.connect({ browserURL })); separate WebDriver server on port 9515 for Selenium. See Puppeteer, Selenium, or the cross-driver hub.
Install LumaBrowser, enable the cdp-driver extension, and swap chromium.launch() for chromium.connectOverCDP('http://127.0.0.1:9222'). Every Playwright locator, assertion, and Page API keeps working. The Lumabyte.* domain is there when you want it.