BLOG
Browser Control for AI Agents: CDP, MCP, and Hosted Chromium
Browser control for AI agents explained: how CDP, MCP, and Playwright drive real Chromium, where local setups break, and how to run it in production.

Browser control is the layer that lets an agent or a script drive a real browser: navigate, click, type, read the DOM, and wait for network state. Most teams start with a local Chrome instance and Playwright, then discover that "control" is the easy part and *keeping* control across concurrency, retries, and long sessions is the hard part. This guide covers how browser control actually works under the hood, how the MCP and CDP paths differ, and what to check before you run it in production.
If you already know you want a hosted runtime, the remote browser for AI agents overview is the shorter path. If you want the mechanics first, keep reading.
What browser control actually means
"Control" is overloaded. In practice it means four distinct capabilities, and conflating them causes most integration bugs:
- Session lifecycle — creating a browser process, allocating a profile, and tearing it down cleanly.
- Transport — the protocol your code uses to send commands (CDP over WebSocket, or a higher-level wrapper like Playwright).
- Action primitives — navigate, click, fill, evaluate, screenshot, wait.
- Observation — DOM snapshots, accessibility trees, network events, console logs.
A local chromium.launch() gives you all four on one machine. A hosted runtime gives you the same four over a network boundary, which changes the failure modes: latency, disconnects, and session affinity become first-class concerns.
The primary transport for Chromium is the Chrome DevTools Protocol (CDP). Playwright, Puppeteer, and Selenium's CDP bridge all speak it. When you "control a web browser" from an agent, you are almost always sending CDP commands, whether or not you see them.
The three control paths developers actually use
1. Direct CDP over WebSocket
You connect to a ws:// endpoint and send JSON commands. Maximum control, minimum abstraction. You manage targets, sessions, and event routing yourself.
import WebSocket from "ws";
const ws = new WebSocket(process.env.CDP_WS_ENDPOINT!);
let id = 0;
const pending = new Map<number, (v: unknown) => void>();
ws.on("message", (raw) => {
const msg = JSON.parse(raw.toString());
if (msg.id && pending.has(msg.id)) {
pending.get(msg.id)!(msg.result);
pending.delete(msg.id);
}
});
function send(method: string, params: Record<string, unknown> = {}) {
return new Promise((resolve) => {
const msgId = ++id;
pending.set(msgId, resolve);
ws.send(JSON.stringify({ id: msgId, method, params }));
});
}
await new Promise((r) => ws.on("open", r));
await send("Page.enable");
await send("Page.navigate", { url: "https://example.com" });
const { result } = (await send("Runtime.evaluate", {
expression: "document.title",
returnByValue: true,
})) as { result: { value: string } };
console.log(result.value);This is what "browser control" looks like at the metal. It is also what you should reach for when a framework hides a capability you need.
2. Playwright over CDP
Playwright wraps CDP with locators, auto-waiting, and a test-friendly API. chromium.connectOverCDP() attaches to an existing browser instead of launching one — the standard pattern for remote control.
import { chromium } from "playwright";
const browser = await chromium.connectOverCDP(process.env.CDP_WS_ENDPOINT!);
const context = browser.contexts()[0] ?? (await browser.newContext());
const page = context.pages()[0] ?? (await context.newPage());
await page.goto("https://example.com", { waitUntil: "domcontentloaded" });
await page.getByRole("link", { name: "More information" }).click();
console.log(await page.title());
// Do not call browser.close() on a shared remote session unless you mean it.
await browser.close();Note the caveat: connectOverCDP is Chromium-only in Playwright, and closing the browser object can terminate the remote session depending on the provider. Check the Playwright CDP docs for the exact semantics.
3. MCP servers
The Model Context Protocol exposes browser actions as tools an LLM can call. Two families dominate:
- Chrome DevTools MCP — exposes CDP-level operations (performance traces, network, console) to a model.
- Playwright MCP — exposes Playwright-style actions (click by role, snapshot the accessibility tree) as tools.
MCP is a *tool interface*, not a runtime. It still needs a browser underneath. The question is whether that browser is a local Chrome on the developer's laptop or a hosted Chromium session reachable over CDP. For agent workflows that need to survive a laptop closing, the hosted path is the one that holds up.
Local control vs hosted control
| Dimension | Local Chrome + Playwright | Hosted Chromium (Remote Browser) |
|---|---|---|
| Setup | Install browsers per machine, pin versions | Connect to a CDP endpoint |
| Concurrency | Bounded by local RAM/CPU | Bounded by your plan; see /pricing |
| Session persistence | Dies with the process | Persistent profiles across sessions |
| Network identity | Your IP | Configurable proxy settings |
| Debugging | Local DevTools | Live viewer + CDP |
| Failure recovery | Manual restart | Session isolation, clean teardown |
| Cost model | Dev machine time | Metered browser-hours |
The trade-off is not "local is bad." Local is faster to iterate on and has zero network latency. Hosted wins when you need *repeatable* control: the same Chromium build, the same profile, the same proxy, every run, from any worker.
Where browser control breaks in production
Most "the agent got stuck" reports trace back to one of these:
- Session leaks. A crashed worker leaves a browser running. Without a lifecycle owner, you pay for ghost sessions.
- Profile collisions. Two agents share a cookie jar and log each other out. Persistent profiles must be scoped per task or per user.
- CDP disconnects. Long-running agents drop the WebSocket. You need reconnect logic or a runtime that keeps the session alive server-side.
- Version drift. Local Chrome auto-updates; your selectors break. Pinning the browser build is a control decision, not an afterthought.
- No observation. When an agent fails, you need the DOM, the network log, and a screenshot. If you cannot replay the session, you cannot fix it.
A hosted runtime addresses the first four structurally. The fifth is why a live viewer and session recordings matter as much as the control API itself.
Choosing a control layer: a checklist
Before committing to a stack, answer these:
- Does the agent need to survive the client? If yes, the browser must live server-side.
- Which protocol do your tools speak? Playwright, Puppeteer, and Selenium all work over CDP; MCP servers sit on top.
- Do you need persistent logins? Then profile management is a requirement, not a nice-to-have.
- What is your concurrency ceiling? Model it against browser-hours, not requests.
- How will you debug a failed run? Insist on live viewing and replay.
- What is your network posture? Proxy and stealth-related settings are configurable browser settings, not magic — verify what a provider actually supports.
If you are comparing runtimes, the remote browser online piece walks through what "online" actually buys you versus a local install.
Wiring an agent to hosted Chromium
The integration is deliberately boring: get a CDP endpoint, connect, drive. The interesting part is what surrounds it.
import { chromium } from "playwright";
async function runTask(endpoint: string, url: string) {
const browser = await chromium.connectOverCDP(endpoint);
const context = browser.contexts()[0];
const page = context.pages()[0] ?? (await context.newPage());
try {
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 30_000 });
await page.getByRole("button", { name: /accept/i }).click({ timeout: 5_000 });
const text = await page.locator("main").innerText();
return { ok: true, text };
} catch (err) {
await page.screenshot({ path: `/tmp/fail-${Date.now()}.png` });
throw err;
} finally {
// Detach without killing the remote session if you plan to resume it.
await browser.close();
}
}Two production notes:
- Timeouts are control. Every
goto,click, andwaitForneeds an explicit timeout. An agent with no timeout is an agent that hangs. - Detach vs destroy. Decide whether
browser.close()should end the session or just disconnect. Hosted runtimes usually expose both; pick deliberately.
For the full API surface — session creation, profile reuse, viewer URLs — see the documentation.
MCP, Cursor, and the "browser agent" trend
A lot of recent interest comes from IDE-integrated agents. Cursor, Claude Code, and Codex can call MCP tools, and browser MCP servers let them drive a page. That works well for short, interactive tasks on a developer's machine.
It works less well when:
- The task runs longer than the IDE session.
- Multiple agents need isolated browsers.
- You need an audit trail of what the agent did.
In those cases the MCP server should point at a hosted CDP endpoint rather than a local Chrome. The tool interface stays the same; the runtime underneath changes. That separation — tool layer vs runtime layer — is the single most useful mental model for browser control in 2026.
If your workflow is specifically about driving a browser from a remote client, the remote control browser guide covers the client-side patterns.
What "good" browser control looks like
A production-grade control layer has these properties:
- Deterministic sessions. Same Chromium build, same flags, same profile policy every time.
- Explicit lifecycle. Create, use, stop. No orphaned processes.
- Observable. Live view, screenshots, network logs, console output.
- Isolated. One task cannot read another task's cookies or storage.
- Metered. You can attribute cost to a task or tenant. See /pricing for how Remote Browser meters browser-hours.
- Protocol-native. CDP in, CDP out — no proprietary action language you cannot escape.
None of these are exotic. They are the baseline you should demand before an agent touches a real account.
Bottom line
Browser control is a solved problem at the protocol level and an unsolved one at the operational level. CDP gives you the commands; Playwright and MCP give you ergonomics; the runtime decides whether your agent finishes the task or dies with a disconnected socket. Start local to prototype, move to hosted Chromium when you need persistence, isolation, and a debug trail — and keep the control layer protocol-native so you can swap runtimes without rewriting the agent.