Playwriter
remorses/playwriter · 3.5k stars · TypeScript · MIT
MCP server Chrome extension & CLI to let agents control your browser. Runs Playwright snippets in a stateful sandbox. Available as CLI or MCP
Install
npx -y skills add https://playwriter.devThese repos do not share one command. When an entry shows a command, it was copied as published. Check the repo's README before you run it.
Files
Other browser MCPs spawn a fresh Chrome — no logins, no extensions, instantly flagged by bot detectors, double the memory. Playwriter connects to your running browser instead. One Chrome extension, full Playwright API, everything you're already logged into.
Installation
- Install Extension from Chrome Web Store
- Click extension icon on a tab → turns green when connected
- Install the CLI and start automating the browser:
npm i -g playwriter
playwriter -s 1 -e 'state.page = await context.newPage(); await state.page.goto("https://example.com")'
- Install the skill so your agent knows how to use Playwriter:
npx -y skills add https://playwriter.dev
Quick Start
playwriter browser start # starts Chrome for Testing/Chromium with bundled Playwriter extension
playwriter session new # creates stateful sandbox, outputs session id (e.g. 1)
playwriter -s 1 -e 'state.page = await context.newPage(); await state.page.goto("https://example.com")'
playwriter -s 1 -e 'console.log(await snapshot({ page: state.page }))'
playwriter -s 1 -e 'await state.page.locator("aria-ref=e5").click()'
Tip: Always use single quotes for
-eto prevent bash from interpreting$, backticks, and\in your JS code. Use double quotes for strings inside the JS.
CLI Usage
Each session has isolated state. Browser tabs are shared across sessions.
# Browser management
playwriter browser start # auto-finds Chrome for Testing or Chromium, with recording flags enabled
playwriter browser start /path/to/browser-binary
# Session management
playwriter session new # creates stateful sandbox, outputs id (e.g. 1)
playwriter session new --tab-group agent1 --tab-group-color blue
playwriter session update 1 --tab-group research
playwriter session list # show sessions + state keys + group
playwriter session reset <id> # fix connection issues
# Execute (always use -s)
playwriter -s 1 -e 'state.page = await context.newPage(); await state.page.goto("https://example.com")'
playwriter -s 1 -e 'await state.page.click("button")'
playwriter -s 1 -e 'console.log(await state.page.title())'
There is no default page. Create your own tab and store it in state. To use a tab the user already opened, find it by URL:
playwriter -s 1 -e 'state.page = context.pages().findLast((p) => p.url().includes("github.com"))'
Tab groups
Local extension sessions use a Chrome tab group named playwriter by default. Remote-control sessions put the tabs they open into remote. Use the shortest clear single-word name with no spaces, such as docs, shop, test, or scrape.
# Park a long scrape in its own group. The user can Move group to new window
# (or another screen). New tabs from this session follow that group.
playwriter session new --tab-group scrape --tab-group-color grey
# Split concurrent agents so many open tabs stay readable
playwriter session new --tab-group agent1 --tab-group-color blue
playwriter session new --tab-group agent2 --tab-group-color pink
# Name a group the user can collapse when they don't care about it
playwriter session new --tab-group done --tab-group-color grey
# Rename or recolor later
playwriter session update 1 --tab-group research
playwriter session update 1 --tab-group-color red
# Remote tabs support the same title and color options
playwriter session new --remote <id> --tab-group support
playwriter session update 1 --tab-group review --tab-group-color cyan
--tab-group-color accepts: grey, blue, red, yellow, green, pink, purple, cyan, orange. Without it, color is derived from the name. The default local playwriter group stays green.
Node programs should use connectViaExtension() instead of posting /cli/session/new themselves:
import { connectViaExtension } from 'playwriter'
await using connection = await connectViaExtension({
tabGroup: 'email-check',
tabGroupColor: 'grey',
})
const page = await connection.browser.contexts()[0].newPage()
await page.goto('https://example.com')
// connection closes automatically when the scope ends, also on throw
tabGroupColor is typed as Chrome's tab group colors. await using calls close() at scope end: it closes the tabs this connection opened (never other sessions' tabs), disconnects CDP, and deletes the session. Without await using, call await connection.close() in finally.
Pass closeTabsOnEnd: false to keep the opened tabs after close(). A later process can reuse them:
// process A
await using a = await connectViaExtension({ tabGroup: 'job', closeTabsOnEnd: false })
await (await a.browser.contexts()[0].newPage()).goto('https://example.com')
// process B, later
await using b = await connectViaExtension({ tabGroup: 'job' })
const page = b.browser.contexts()[0].pages().findLast((p) => p.url().includes('example.com'))
Tabs that b did not open are never closed by b.close().
If the extension runs in more than one browser or profile, pass selectBrowser. It gets every connected browser and returns the one to use:
await using connection = await connectViaExtension({
selectBrowser: (browsers) => {
// [{ id, browser: 'Brave', email: 'me@work.com', activeTabs, playwriterVersion }]
return browsers.find((b) => b.email === 'me@work.com')
},
})
id is stable, so you can save it and match on it later. listBrowsers() returns the same list without connecting.
Multiline:
playwriter -s 1 -e $'
const title = await state.page.title();
console.log({ title, url: state.page.url() });
'
Examples
Variables in scope: context, state (persists between calls), cloud (local CLI sessions), require, importModule, native import(), and Node.js globals. Relative imports resolve from the session working directory. There is no default page; examples below assume state.page was set as shown above.
Persist data in state:
playwriter -e "state.users = await state.page.$$eval('.user', els => els.map(e => e.textContent))"
playwriter -e "console.log(state.users)"
Intercept network requests:
playwriter -e "state.requests = []; state.page.on('response', r => { if (r.url().includes('/api/')) state.requests.push(r.url()) })"
playwriter -e "await Promise.all([state.page.waitForResponse(r => r.url().includes('/api/')), state.page.click('button')])"
playwriter -e "console.log(state.requests)"
Set breakpoints and debug:
playwriter -e "state.cdp = await getCDPSession({ page: state.page }); state.dbg = createDebugger({ cdp: state.cdp }); await state.dbg.enable()"
playwriter -e "state.scripts = await state.dbg.listScripts({ search: 'app' }); console.log(state.scripts.map(s => s.url))"
playwriter -e "await state.dbg.setBreakpoint({ file: state.scripts[0].url, line: 42 })"
Live edit page code:
playwriter -e "state.cdp = await getCDPSession({ page: state.page }); state.editor = createEditor({ cdp: state.cdp }); await state.editor.enable()"
playwriter -e "await state.editor.edit({ url: 'https://example.com/app.js', oldString: 'const DEBUG = false', newString: 'const DEBUG = true' })"
Screenshot with labels:
playwriter -e "await screenshotWithAccessibilityLabels({ page: state.page })"
Live stream a tab to X Live / Twitch (RTMP, runs 24/7):
playwriter -s 1 -e "state.page = await context.newPage(); await state.page.goto('https://example.com')"
playwriter stream start -s 1 --rtmp rtmp://va.pscp.tv:80/x/<stream-key>
playwriter stream status -s 1
playwriter stream stop -s 1
MCP Setup
Using the CLI with the skill (step 4 above) is the recommended approach. For direct MCP server configuration, see MCP.md.
Visual Labels
Vimium-style labels for AI agents to identify elements:
await screenshotWithAccessibilityLabels({ page: state.page })
// Returns screenshot + accessibility snapshot with aria-ref selectors
await state.page.locator('aria-ref=e5').click()
Color-coded: yellow=links, orange=buttons, coral=inputs, pink=checkboxes, peach=sliders, salmon=menus, amber=tabs.
Comparison
vs Playwright MCP
Facts
- Kind
- MCP server
- Repo
- remorses/playwriter
- Group
- Uncategorized
- Stars
- 3.5k
- License
- MIT
- Language
- TypeScript
- Last push
- 2026-10-07
- Forks
- 189
- Homepage
- playwriter.dev
- Topics
- mcp, playwright
- 1Everythingmodelcontextprotocol/serversThis MCP server attempts to exercise all the features of the MCP protocol. It is not intended to be a useful server, but rather a test server for builders of MCP clients. It implements prompts, tools, resources, sampling, and more to showcase MCP capabilities.85.8k
- 2Fetchmodelcontextprotocol/serversA Model Context Protocol server that provides web content fetching capabilities. This server enables LLMs to retrieve and process content from web pages, converting HTML to markdown for easier consumption.85.8k
- 3Gitmodelcontextprotocol/serversA Model Context Protocol server for Git repository interaction and automation. This server provides tools to read, search, and manipulate Git repositories via Large Language Models.85.8k
- 4Memorymodelcontextprotocol/serversA basic implementation of persistent memory using a local knowledge graph. This lets Claude remember information about the user across chats.85.8k
- 5Sequential Thinkingmodelcontextprotocol/serversAn MCP server implementation that provides a tool for dynamic and reflective problem-solving through a structured thinking process.85.8k
- 6Timemodelcontextprotocol/serversA Model Context Protocol server that provides time and timezone conversion capabilities. This server enables LLMs to get current time information and perform timezone conversions using IANA timezone names, with automatic system timezone detection.85.8k