There’s a familiar gap in most teams’ test suites: plenty of serious coverage, but a long tail of small flows that never quite make it in. You notice them during reviews. “We should have a check for that filter. “Someone should verify the title changes after save.” Then the sprint moves on. Nobody wants to rewrite the stack just to cover a few honest, boring paths, and nobody wants another flaky layer that guesses from pixels or leaves you arguing with screenshots in stand-up.
This is where Playwright MCP helps in a practical way. MCP (Model Context Protocol) is a light bridge between a client and the Playwright engine you already trust; in this article the client is a plain Node script. Instead of pixels, MCP works with page structure (roles, labels, attributes, and state) so interactions are stable and assertions stay in code. Through this article we’ll explore everything we need to know to get started with Playwright MCP and get hands on with the basics.
MCP (Model Context Protocol) is a simple way for a “client” to talk to a “tool server.”
What Is MCP, and What Is Playwright MCP?
MCP (Model Context Protocol) is a simple way for a “client” to talk to a “tool server.” The client can be anything that knows the protocol: a chat assistant, an editor plugin, or your own Node script. The tool server exposes a set of actions (“tools”) the client can call. Each call is a small JSON request with a name and arguments, and the server replies with structured results.
Playwright MCP is one specific tool server. It exposes Playwright’s browser actions as MCP tools. When you run npx @playwright/mcp@latest, you start a process that can do things like open a page, wait for something to appear, click, evaluate JavaScript, and take a screenshot. Your client does not talk to the browser directly; it calls the Playwright MCP server, and that server drives a real browser using Playwright under the hood.
You can think of it as a pipeline:
- Client (assistant or Node script)
- MCP request (for example, call a tool named browser_navigate with { url: "…" })
- Playwright MCP server (receives the request and runs Playwright)
- Real browser (Chromium, Chrome, Firefox, or WebKit)
- Structured response back to the client (success, error, returned text, or a file path to an artifact)
A few concrete examples help:
- To open a page, the client calls browser_navigate with a URL.
- To wait for visible text, it calls browser_wait_for with the text and a timeout.
- To run page code, it calls browser_evaluate with a small function string.
- To record evidence, it calls browser_take_screenshot with a filename.
The key idea is that Playwright MCP passes around structure, not pixels. The server can read the DOM and accessibility tree, so selectors and checks line up with how you already write tests. You still keep everything in the Playwright world for ownership: code, assertions, reports, and CI. MCP just gives you a clean, programmatic doorway into the same engine, whether the caller is an AI assistant or the small Node script you include in this article.
Getting Started (What You Need)
Prerequisites:
- Node.js LTS installed (v20 or newer).
- npm or pnpm available on your PATH.
- Permission to launch a local browser on your machine or CI runner.
- Internet access the first time you fetch the MCP server.
Create or open a Playwright project.
# New project
npm create playwright@latest
# Or add to an existing repo
npm i -D @playwright/test
npx playwright install
Add a predictable Playwright test environment.
Create playwright.config.ts (or edit it) so runs behave the same locally and in CI.
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
headless: true,
viewport: { width: 1366, height: 900 },
locale: 'en-US',
timezoneId: 'UTC',
trace: 'on-first-retry',
video: 'retain-on-failure',
screenshot: 'only-on-failure'
},
reporter: [['html', { open: 'never' }]]
});
Make sure you can launch the Playwright MCP server.
# Fetch and run the server on demand
npx @playwright/mcp@latest
If that command runs without errors, you’re ready to use playwright mcp from your Node script (or from any MCP-aware client). If it fails, check that Node is on PATH and try again after installing Playwright browsers with npx playwright install.
A Small, Honest Example You Can Run
We’re going to drive Playwright MCP from a plain Node script, no chat client, so the logic is explicit. The plan is bare-bones on purpose: create a tiny local page, click a button, and prove two things with code—the title changes and the URL ends with a hash. If those two checks pass, we’ve demonstrated the whole loop (navigate → interact → assert) without leaning on screenshots.
A good test starts with outcomes you can verify
First, give yourself something deterministic to test against.
A good test starts with outcomes you can verify. Here, a button click will update the title to clicked and the URL’s hash to #done. That’s two clear signals for assertions.
import { writeFileSync, mkdtempSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { pathToFileURL } from 'node:url';
// Temp folder + page that mutates title and hash on click
const dir = mkdtempSync(join(tmpdir(), 'mcp-demo-'));
const htmlPath = join(dir, 'test.html');
writeFileSync(
htmlPath,
`<!doctype html>
<meta charset="utf-8">
<title>ready</title>
<button id="go" onclick="document.title='clicked'; location.hash='done';">go</button>`
);
const fileUrl = pathToFileURL(htmlPath).href;
Two lines do most of the work here: the <button> sets document.title and location.hash. That’s our truth later.
Next, start the MCP server and connect a client.
We’ll talk to MCP over stdio using the official SDK and the same flags you’d use in CI.
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
const transport = new StdioClientTransport({
command: 'npx',
args: ['-y', '@playwright/mcp@latest', '--isolated', '--headless', '--browser=chrome'],
});
const client = new Client({ name: 'demo', version: '0.1' }, { capabilities: {} });
await client.connect(transport);
await client.listTools(); // optional sanity check
If you’re wondering about the flags, --isolated gives a clean browser context per run; --headless keeps the run fast (drop it locally if you want to watch); --browser=chrome is explicit. Swap for chromium, firefox, or webkit if you want a different engine.
Now do the actual work: navigate, wait, click.
We open the file:// URL, wait for visible text (no fixed sleeps), and then click the button from inside the page context.
await client.callTool({ name: 'browser_navigate', arguments: { url: fileUrl } });
await client.callTool({ name: 'browser_wait_for', arguments: { text: 'go', time: 10 } });
await client.callTool({
name: 'browser_evaluate',
arguments: { function: `() => document.getElementById('go')?.click()` },
});
This is where MCP’s “structured over pixel” approach shines. We’re not hoping the button is there; we wait until we see “go,” then interact.
Finally, assert the truth in code
After the click, we read the page title and URL back through MCP and make two straightforward checks. No image diffs, no guesswork.
const titleRes = await client.callTool({
name: 'browser_evaluate',
arguments: { function: `() => document.title` },
});
const urlRes = await client.callTool({
name: 'browser_evaluate',
arguments: { function: `() => location.href` },
});
const titleText = titleRes.content?.[0]?.text ?? '';
const urlText = urlRes.content?.[0]?.text ?? '';
if (titleText !== 'clicked') throw new Error(`Expected title 'clicked', got '${titleText}'`);
if (!/#done$/.test(urlText)) throw new Error(`Expected URL to end with '#done', got '${urlText}'`);
console.log('Assertions passed:', { titleText, urlText });
If you like to keep a breadcrumb for triage, you can still save a screenshot after assertions; just treat it as evidence, not the source of truth.
await client.callTool({
name: 'browser_take_screenshot',
arguments: { filename: 'after-click.png' },
});
And then, clean up:
await client.close();
transport.close();
That’s the loop. This is the complete code with multiple logging statements to interpret the output:
import { writeFileSync, mkdtempSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { pathToFileURL } from 'node:url';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
// 1) Create a tiny local HTML file
const dir = mkdtempSync(join(tmpdir(), 'mcp-demo-'));
const htmlPath = join(dir, 'test.html');
writeFileSync(
htmlPath,
`<!doctype html>
<meta charset="utf-8">
<title>ready</title>
<button id="go" onclick="document.title='clicked'; location.hash='done';">go</button>`
);
const fileUrl = pathToFileURL(htmlPath).href;
async function main() {
// 2) Spawn the Playwright MCP server (remove --headless to watch it)
const transport = new StdioClientTransport({
command: 'npx',
args: ['-y', '@playwright/mcp@latest', '--isolated', '--headless', '--browser=chrome'],
});
const client = new Client({ name: 'demo', version: '0.1' }, { capabilities: {} });
await client.connect(transport);
await client.listTools();
// 3) Open the local file (file://...)
await client.callTool({ name: 'browser_navigate', arguments: { url: fileUrl } });
await client.callTool({ name: 'browser_wait_for', arguments: { text: 'go', time: 10 } });
// 4) Click the button
await client.callTool({
name: 'browser_evaluate',
arguments: { function: `() => document.getElementById('go')?.click()` },
});
// 5) Verify + screenshot
const title = await client.callTool({
name: 'browser_evaluate',
arguments: { function: `() => document.title` },
});
console.log('Title:', title.content?.[0]?.text);
const url = await client.callTool({
name: 'browser_evaluate',
arguments: { function: `() => location.href` },
});
console.log('URL:', url.content?.[0]?.text);
const shot = await client.callTool({
name: 'browser_take_screenshot',
arguments: { filename: 'after-click.png' },
});
console.log('Screenshot saved as:', shot.content?.[0]?.text ?? 'after-click.png');
await client.close();
transport.close();
console.log('Done');
}
main().catch(err => { console.error(err); process.exitCode = 1; });
This is how the output of the code looks like when we run the file with the ‘node’ command:
Title: ### Result
"clicked"
### Ran Playwright code
```js
await page.evaluate('() => document.title');
```
### Page state
- Page URL: file:///var/folders/td/9kxhw75n0ws1d059l0lwzllc0000gn/T/mcp-demo-jUTOSD/test.html#done
- Page Title: clicked
- Page Snapshot:
```yaml
- button "go" [ref=e2]
```
URL: ### Result
"file:///var/folders/td/9kxhw75n0ws1d059l0lwzllc0000gn/T/mcp-demo-jUTOSD/test.html#done"
### Ran Playwright code
```js
await page.evaluate('() => location.href');
```
### Page state
- Page URL: file:///var/folders/td/9kxhw75n0ws1d059l0lwzllc0000gn/T/mcp-demo-jUTOSD/test.html#done
- Page Title: clicked
- Page Snapshot:
```yaml
- button "go" [ref=e2]
```
Screenshot saved as: ### Result
Took the viewport screenshot and saved it as /tmp/playwright-mcp-output/2025-08-04T07-53-28.154Z/after-click.png
### Ran Playwright code
```js
// Screenshot viewport and save it as /tmp/playwright-mcp-output/2025-08-04T07-53-28.154Z/after-click.png
await page.screenshot({
path: '/tmp/playwright-mcp-output/2025-08-04T07-53-28.154Z/after-click.png',
quality: 50,
scale: 'css',
type: 'jpeg'
});
```
Done
Those logs are formatted by the Playwright MCP server, not your code. Each tool call returns a markdown-style report with sections like “### Result” (the value), “### Ran Playwright code” (the snippet executed), and “### Page state” (URL/title plus a small accessibility snapshot). If you want plain values, either parse the “Result” block or have your browser_evaluate return booleans/strings and print just those.
You can paste the full script into one file and run it in CI or locally. More importantly, you’ve seen exactly how MCP fits: it’s just Playwright, routed through a protocol your tools can share.
You do not have to change how code moves through CI. You are just shortening the time from “we should test this” to “we have a reliable check.”
How This Translates to Your Day Job
You’ll probably use two paths in practice. For quick scaffolding or exploratory checks, let an assistant drive the browser and draft a Playwright spec that you review. When you want full control and local logs, use a small Node script that talks to Playwright MCP directly, like the example in this article.
In both cases, the hygiene is the same: use roles, labels, or data-test for selectors; wait for real state changes instead of sleeping; and assert things that hold up in code review such as title, URL, visible text, headings, or an HTTP 2xx. You do not have to change how code moves through CI. You are just shortening the time from “we should test this” to “we have a reliable check.”
Common Questions, Answered Plainly
People ask whether Playwright is faster than Selenium. In many setups, it is, thanks to its modern architecture and auto-waiting. But the honest answer is to measure in your CI with the same spec and see what the numbers say. They also ask what MCP “is” in testing terms. It’s a standardized way for an agent or program to use testing tools. For web, that means navigate, interact, and assert, then hand you artifacts and code you can keep. Is Playwright MCP open-source? Yes. Does it lock you in? No, the output you rely on is standard Playwright.

Where Autify Nexus Fits
Once you get a taste for how fast this can be, the next question is obvious: how do you make that speed available to people beyond the one engineer with a script? That’s what Autify Nexus is for. Nexus is built on Playwright and designed to be low-code when you want it, full-code when you need it. Non-specialists can describe flows in plain English and run them; engineers can drop straight into Playwright code at any time. Execution, artifacts, and collaboration live in one place. As broader cross-browser coverage lands, you keep the same Playwright foundation with a smoother, managed workflow on top.
A Quick Recap You Can Use Tomorrow
MCP is the bridge that lets a client, assistant or script, drive Playwright with structured, reliable actions. You created a tiny page, launched the MCP server, navigated, clicked, and proved behavior with two code-level assertions. That’s the essence. Keep using the same habits across your app: stable locators, explicit waits, assertions that reflect real risk, and artifacts only when they help. When it’s time to scale the practice to a team, bring those habits into Autify Nexus and keep moving fast without giving up full-code control.