LumaBrowser vs Playwright

LumaBrowser vs Playwright: connectOverCDP with LLM selector fallback

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.

LumaBrowser vs Playwright at a glance

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.

CapabilityPlaywrightLumaBrowser
Attach methodchromium.launch() or connectOverCDP()chromium.connectOverCDP('http://127.0.0.1:9222')
Chromium binaryDownloaded via npx playwright installNone — LumaBrowser is the browser
Locators, auto-wait, expect(...)Full supportFull support — client-side, unchanged over CDP
LLM selector fallbackNot availableLumabyte.find, Lumabyte.click via newCDPSession
DOM + AX tree + screenshotThree separate CDP callsOne call via Lumabyte.domSnapshot
Firefox / WebKitNativeChromium only — connectOverCDP is Chromium-only by design
Playwright MCP (@playwright/mcp)Bundles its own browserAttaches via --cdp-endpoint http://127.0.0.1:9222
Playwright codegenRecords against its own browserRecord elsewhere, execute against LumaBrowser
Also works with Puppeteer / SeleniumNoYes — same CDP on 9222, separate WebDriver on 9515
MCP server for AI agentsVia @playwright/mcp (separate)Built-in local MCP server
Setupnpm i playwright && npx playwright installnpx lumabrowser start
The drop-in promise

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:

  • No new library. Keep your existing playwright or @playwright/test install. Works from JavaScript/TypeScript, Python, .NET, and Java bindings — they all speak connectOverCDP.
  • Locators, auto-wait, and the Page API are unchanged. Those are client-side features. The driver just sees the resulting CDP commands, which forward through to Chromium as normal.
  • browser.contexts()[0] resolves on connect. LumaBrowser advertises a default BrowserContext so Playwright’s “attach to the existing context” idiom works immediately.
  • Native CDP is untouched. 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:

Playwright (classic)
// 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
// 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.

What’s actually different

Three concrete wins. Each one is opt-in: pay the cost only where you want the benefit.

Win 1: Resilient locators that survive a redesign

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.

Playwright (classic)
// 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();
LumaBrowser
// 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',
});
Win 2: One round-trip to read the whole page

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.

Playwright (classic)
// 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' }
);
LumaBrowser
// 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.
Win 3: No bundled Chromium, dedicated automation tabs

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 (classic)
// 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
// 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.
Full example: a semantic end-to-end flow

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 — the 2026 pattern

@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:9222

The 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.

Compatibility matrix

What works today when Playwright attaches via connectOverCDP, and where the deliberate limits are.

FeatureStatusNotes
chromium.connectOverCDP(url)SupportedDrop-in. Uses the standard Chrome bootstrap + /devtools/browser/{uuid} socket.
browser.contexts()[0] on connectSupportedLumaBrowser advertises a default BrowserContext so first-page-on-existing-context works immediately.
browser.newContext() for isolationSupportedBacked by Target.createBrowserContext. Persistent partition per context; cookies and storage isolated.
Locators, auto-wait, expect assertionsSupportedClient-side features — unaffected by the server implementation.
page.goto, click, fill, type, pressSupportedForwarded via Page, DOM, Input domains.
page.screenshot, page.pdfSupportedVia Page.captureScreenshot, Page.printToPDF.
page.route, page.waitForResponseSupportedVia Fetch / Network domain forwarding.
context.newCDPSession(page) + raw CDPSupportedHow you reach the Lumabyte.* domain from Playwright.
Playwright MCP via --cdp-endpointSupportedAttaches as a regular connectOverCDP client.
Lumabyte.find (description-first resolver)LumaBrowser onlyDeterministic-match → LLM → validation pipeline. Shared with the Selenium & Puppeteer paths.
Lumabyte.click (semantic click)LumaBrowser onlyResolves, scrolls into view, dispatches synthetic input via Input.dispatchMouseEvent.
Lumabyte.domSnapshot (one-shot capture)LumaBrowser onlyDOM + optional AX tree + optional screenshot in one CDP call.
Lumabyte.configureFallback / Lumabyte.getInfoLumaBrowser onlyPer-session LLM config — CDP’s analog of a Playwright test option.
Firefox / WebKit browser typesChromium onlyThis is a Playwright connectOverCDP limitation, not a LumaBrowser one. Documented by Microsoft.
Playwright tracing (context.tracing.start)PartialWorks for most events. Trace Viewer is tuned for the native Playwright protocol and some entries may render differently.
playwright codegenLaunch-mode onlycodegen launches its own Chromium; it can’t record against a connectOverCDP target. Record against your own Chromium, execute against LumaBrowser.
chromium.launch() / launchPersistentContext()Not applicableLumaBrowser is the browser — you connectOverCDP instead of launching.
How to enable it

Two ways, takes under a minute either way:

1. From the UI

  1. Open Settings → CDP Driver.
  2. (Optional) Check “Start automatically when LumaBrowser launches.”
  3. Click Save & Start.
  4. Point your Playwright client at http://127.0.0.1:9222.

2. Via the REST API

# Start the CDP server
curl -X POST http://localhost:3000/api/cdp/start

# Check status
curl http://localhost:3000/api/cdp/status

# Bind to all interfaces on a custom port
curl -X POST http://localhost:3000/api/cdp/settings \
  -H "Content-Type: application/json" \
  -d '{"host": "0.0.0.0", "port": 9333}'

For the full CDP domain reference, Lumabyte.* method signatures, and MCP tool surface, see the CDP Driver section of the API docs.

FAQ

Does my existing Playwright script just work?

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.

Can I still use Playwright’s auto-wait, locators, and 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.

What about 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.

Why not implement Playwright’s native protocol?

The native Playwright protocol is proprietary, generated from protocol.yml at build time, and locked to client/driver minor versions. Every Playwright release would be a breaking change for a custom implementation. connectOverCDP is the official, documented path for attaching Playwright to an existing browser, it’s exactly what every cloud-browser provider (Browserless, Browserbase, Steel.dev, Hyperbrowser) targets, and it’s versioned like CDP rather than like Playwright. Pure CDP gives you 80% of Playwright’s practical value with none of the version lock.

Does Playwright MCP work?

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, so flaky selectors don’t crash the agent loop.

Which LLM runs the Lumabyte.find fallback?

The fallback routes through whichever model you configure in the shared core-scope selector-resolver slot. Configure it once and every description-based call from the Selenium, Puppeteer, or Playwright path benefits — same orchestrator, same pipeline. If the slot is unconfigured, the resolver falls back to your global active LLM. You can point it at Anthropic, any OpenAI-compatible endpoint, or a local model via LM Studio or Ollama.

What doesn’t work?

Three documented limits. (1) Firefox and WebKit: connectOverCDP is Chromium-only by Playwright’s own design. LumaBrowser is Chromium, so this matches. (2) Full Playwright tracing: the Trace Viewer is tuned for the native protocol path and some entries may render differently. (3) playwright.chromium.launch(): LumaBrowser is the browser, so you connect instead of launching. Swap launch() for connectOverCDP() and the rest of the script stays the same.

What about Puppeteer or Selenium?

Same CDP server for Puppeteer (puppeteer.connect({ browserURL })), same Lumabyte LLM fallback. Selenium is a separate WebDriver server on port 9515, with the same LLM fallback exposed as the ai-description locator strategy. See the Selenium vs Puppeteer vs Playwright comparison for a three-column side-by-side.

Ready to try it

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.