← Blog

BLOG

Cursor Browser Agent: Run Real Chromium from the IDE

Build a Cursor browser agent that drives hosted Chromium over CDP. Setup, Playwright wiring, production trade-offs, and what to check before you ship.

9 min readRemote Browser
Ivory computer towers connected across terracotta platforms in a quiet desert landscape
The infrastructure behind your browserREMOTE BROWSER / ILLUSTRATION

A Cursor browser agent is an AI coding assistant inside the Cursor IDE that can drive a real browser — navigating pages, clicking, filling forms, and reading the DOM — instead of only reading source files. The practical way to build one is to give Cursor a browser it can reach over the Chrome DevTools Protocol (CDP): either a local Chrome instance or a hosted Chromium session. This guide covers both, why the hosted path usually wins for anything beyond a quick check, and how to wire it up with Playwright in TypeScript.

The search intent here is concrete: you want Cursor to stop guessing about what a web page does and start observing it. That means a browser process, a CDP endpoint, and a tool surface Cursor can call. Everything below is about making those three pieces reliable.

Why a Cursor browser agent needs a real browser

Cursor's model reads your repository. It does not, by default, see rendered output. That gap matters whenever the thing you are debugging is not in the source:

  • Client-rendered apps. The HTML your server returns is not the DOM the user sees. A React or Next.js page hydrates, fetches, and mutates after load.
  • Auth flows. Login redirects, cookie banners, and session state only exist at runtime.
  • Third-party embeds. Payment iframes, chat widgets, and analytics tags behave differently in a live browser than in a static read.
  • Visual regressions. Layout bugs are invisible to a model reading JSX.

A browser agent closes that gap by giving the model a control channel into a live page. The model issues commands — navigate, click, type, evaluate — and gets back structured results. The browser is the ground truth; the model reasons over it.

There are two ways to supply that browser, and the choice shapes everything downstream.

Local Chrome vs hosted Chromium for Cursor

CriterionLocal Chrome via CDPHosted Chromium (Remote Browser)
SetupLaunch Chrome with --remote-debugging-port, manage the processCreate a session via API, get a CDP endpoint back
IsolationShares your machine, profile, and cookiesIsolated session per run
ReproducibilityDepends on your local Chrome version and extensionsConsistent image, versioned per session
ParallelismLimited by your laptopBounded by your plan's concurrency
PersistenceManual profile handlingPersistent profiles supported
DebuggingYou watch your own screenLive viewer for remote sessions
CostFree, but consumes local resourcesMetered per browser-hour — see /pricing
Best forOne-off checks, quick visual verificationCI, multi-step tasks, anything you rerun

Local Chrome is the right starting point for a single interactive session. You launch it, point Cursor at http://localhost:9222, and iterate. The friction shows up the moment you want the same agent to run unattended, run twice in parallel, or run on a machine that is not yours.

Hosted Chromium removes the process management. You ask for a session, receive a WebSocket CDP endpoint, and connect the same way you would to local Chrome. The rest of this guide uses that model because it is the one that survives contact with production.

How the pieces fit together

A Cursor browser agent has four layers:

  1. The model — Cursor's assistant, deciding what to do next.
  2. The tool layer — the functions the model can call (navigate, click, extract, screenshot).
  3. The driver — Playwright or Puppeteer translating those calls into CDP commands.
  4. The browser — a Chromium instance, local or hosted.

Cursor does not ship a built-in browser tool, so you supply layers 2 through 4. The cleanest pattern is a small local script or MCP server that exposes browser actions as tools, with Playwright underneath. Cursor calls the tools; the tools call Playwright; Playwright speaks CDP to the browser.

If you want the runtime-level view of this stack — sessions, profiles, proxies, and live debugging — the remote browser for AI agents post covers it in more depth than this one.

Wiring Cursor to hosted Chromium with Playwright

The connection is a single call. Playwright's chromium.connectOverCDP() accepts a WebSocket endpoint and returns a browser handle you drive exactly like a locally launched one. The authoritative reference is the Playwright CDP documentation.

import { chromium, Browser, BrowserContext, Page } from "playwright";

interface SessionInfo {
  cdpUrl: string;
  sessionId: string;
}

// Your session-creation call returns a CDP WebSocket URL.
// Exact request shape is documented at /documentation.
async function createSession(): Promise<SessionInfo> {
  const res = await fetch(`${process.env.BROWSER_API_BASE}/sessions`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${process.env.BROWSER_API_KEY}`,
    },
    body: JSON.stringify({
      // Configurable browser settings: region, proxy, profile, viewport.
      profile: "cursor-agent-default",
    }),
  });
  if (!res.ok) throw new Error(`session create failed: ${res.status}`);
  return (await res.json()) as SessionInfo;
}

export async function withBrowser<T>(
  fn: (page: Page) => Promise<T>
): Promise<T> {
  const { cdpUrl, sessionId } = await createSession();
  let browser: Browser | undefined;

  try {
    browser = await chromium.connectOverCDP(cdpUrl);
    const context: BrowserContext = browser.contexts()[0];
    const page: Page = context.pages()[0] ?? (await context.newPage());
    return await fn(page);
  } finally {
    await browser?.close();
    // Explicit teardown so the session stops metering.
    await fetch(`${process.env.BROWSER_API_BASE}/sessions/${sessionId}`, {
      method: "DELETE",
      headers: { Authorization: `Bearer ${process.env.BROWSER_API_KEY}` },
    });
  }
}

// Example tool the model can invoke:
export async function inspectPage(url: string) {
  return withBrowser(async (page) => {
    await page.goto(url, { waitUntil: "networkidle" });
    return {
      title: await page.title(),
      headings: await page.$$eval("h1, h2", (els) =>
        els.map((e) => e.textContent?.trim()).filter(Boolean)
      ),
      url: page.url(),
    };
  });
}

Three details matter more than they look:

  • `browser.contexts()[0]` — when you connect over CDP to an existing browser, the context already exists. Do not call newContext(); reuse what is there.
  • `browser.close()` — this disconnects the client. It does not necessarily terminate the remote session. Send an explicit teardown call so you are not billed for an idle browser.
  • `waitUntil: "networkidle"` — a reasonable default for agent work, but not universal. SPAs that poll will never go idle; use domcontentloaded plus an explicit selector wait in those cases.

Exposing browser actions to Cursor

Cursor needs a tool surface, not a raw Playwright script. Two patterns work:

MCP server. Wrap the functions above in a Model Context Protocol server and register it in Cursor's settings. The model then sees navigate, click, extract_text, and screenshot as callable tools. This is the most ergonomic option and keeps the browser logic out of your prompt.

CLI wrapper. Expose a small command-line tool (agent-browser navigate <url>) that Cursor invokes through its terminal tool. Simpler to set up, harder to keep stateful across calls. The agent-browser CLI post covers that path.

Either way, keep the tool set small and the return values structured. A model that receives { title, headings, url } reasons better than one that receives a 40,000-token HTML dump. Extract, do not dump.

Production criteria for a Cursor browser agent

Once the agent works interactively, the questions change. These are the criteria that separate a demo from something you can leave running.

Session lifecycle and cost

Every hosted session is metered. A Cursor agent that opens a browser, gets distracted, and leaves it idle is burning browser-hours. Enforce teardown in a finally block, set a session timeout, and monitor for orphaned sessions. Current rates are on /pricing; the point here is that idle time is the most common cost leak.

Isolation between runs

If two Cursor tasks share a profile, they share cookies and local storage. That is sometimes what you want (a logged-in session reused across tasks) and sometimes catastrophic (one task's cart bleeding into another's). Decide per workflow: ephemeral sessions for scraping, persistent profiles for authenticated flows.

Determinism

A hosted Chromium image is consistent across runs. Your local Chrome is not — extensions, versions, and settings drift. If your agent's behavior depends on the browser, pin it. This is the single strongest argument for hosted sessions in any workflow you intend to rerun.

Observability

When a Cursor agent fails, you need to see what it saw. A live viewer lets you watch the session in real time; recordings let you replay it after the fact. Without either, debugging a failed agent run is guesswork. The remote control browser post covers the interactive side of this.

Network and proxy configuration

Agents that hit geo-restricted content or rate-limited endpoints need configurable proxy settings. Hosted sessions let you set this per session rather than per machine. Be precise about what you need — "configurable browser settings" covers proxy, region, and viewport; do not assume more than the docs promise.

Common failure modes

  • Connecting to a closed endpoint. Sessions expire. If you cache a CDP URL across runs, you will eventually connect to a dead socket. Create per run.
  • Assuming `browser.close()` kills the session. It does not. Always call the session teardown endpoint.
  • Ignoring the existing context. Calling newContext() after connectOverCDP() creates a second context and confuses state. Reuse contexts()[0].
  • Over-broad tool returns. Returning full page HTML to the model wastes tokens and degrades reasoning. Return the fields the task needs.
  • No timeout on navigation. A page that hangs will hang the agent. Set page.setDefaultNavigationTimeout() explicitly.

When local Chrome is still the right call

Hosted Chromium is not universally better. Use local Chrome when:

  • You are doing a one-off visual check and the browser is already open.
  • The task requires your existing logged-in profile and you do not want to replicate it.
  • You are prototyping the tool surface and have not yet decided on a runtime.

Use hosted Chromium when the agent needs to run unattended, run in parallel, run on a machine that is not your laptop, or run the same way twice. For most Cursor browser agent workflows that graduate past prototyping, that is the deciding factor.

Getting started

The shortest path:

  1. Read the documentation for the session API and CDP endpoint format.
  2. Create a session, connect with chromium.connectOverCDP(), and confirm you can read page.title().
  3. Wrap two or three actions as tools and register them with Cursor.
  4. Add teardown, timeouts, and structured returns before you add more tools.
  5. Check /pricing and set a session timeout that matches your budget.

If you want to compare the hosted runtime against running Chromium yourself, the remote browser online post walks through the trade-offs without the Cursor-specific layer. And if you are still deciding whether a browser agent is the right architecture at all, start with what is a browser agent before wiring anything up.

The core idea is simple: Cursor is good at reasoning over code, and a browser is where that code actually runs. Connect the two over CDP, keep the tool surface narrow, and treat the session as a metered resource. That is the whole pattern.