Skip to content

Set Up the Playwright Page Object MCP Server

Install playwright-page-object, verify its CLI, and register one stdio command in your MCP client. The default command discovers the Playwright config, TypeScript config, page objects, and test-ID attribute for most repositories. Add flags only when your app lives outside that detected scope.

Install the package in the repository so npx uses the version pinned by your lockfile and does not need to download anything when the MCP client starts:

Terminal window
npm install -D playwright-page-object

The package requires Node.js 20 or later. See Installation for the full library requirements.

Run the help command before wiring the server into a client. The analysis engine loads only when the server starts, so this check does not scan your project:

Terminal window
npx playwright-page-object mcp --help

You should see the available flags. If this command fails, fix that error first; terminal output is usually easier to read than an MCP client log.

The server uses the MCP stdio transport. Choose your client below and add the matching configuration.

For a project-scoped configuration that the team can share, create .mcp.json at the repository root:

{
"mcpServers": {
"playwright-page-object": {
"command": "npx",
"args": ["playwright-page-object", "mcp"]
}
}
}

Or add it with one command:

Terminal window
claude mcp add --transport stdio --scope project playwright-page-object -- npx playwright-page-object mcp

.cursor/mcp.json, same shape:

{
"mcpServers": {
"playwright-page-object": {
"command": "npx",
"args": ["playwright-page-object", "mcp"]
}
}
}

.vscode/mcp.json. Note the different top-level key and the explicit type:

{
"servers": {
"playwright-page-object": {
"type": "stdio",
"command": "npx",
"args": ["playwright-page-object", "mcp"]
}
}
}

Add the server with the CLI:

Terminal window
codex mcp add playwright-page-object -- npx playwright-page-object mcp

Or edit ~/.codex/config.toml. For a project-scoped server in a trusted repository, use .codex/config.toml instead.

[mcp_servers.playwright-page-object]
command = "npx"
args = ["playwright-page-object", "mcp"]

Two common spawn problems happen before the MCP handshake:

  • If you intentionally skip local installation, use npx -y playwright-page-object mcp. A stdio process has no terminal for the npm install prompt, so omitting -y can leave the client waiting.
  • On Windows, a client that cannot execute the npx.cmd shim may report that the command was not found. Use "command": "cmd" with "args": ["/c", "npx", "playwright-page-object", "mcp"] for that client.

If your repository is not the client’s working directory, or the app you want analyzed is one package of a monorepo, add --project-root:

{
"mcpServers": {
"playwright-page-object": {
"command": "npx",
"args": ["playwright-page-object", "mcp", "--project-root", "apps/web"]
}
}
}

See Configuration for the rest of the flags.

Reload the MCP configuration or start a new client session, then open the client’s tool list. You should see list_page_objects, get_page_object_tree, get_testid_tree, map_coverage, and query_coverage. If none appear, run the same command in a terminal and follow the startup troubleshooting.

Start with the page-object index, inspect the relevant class, and then check the component’s test IDs. The examples below are abbreviated responses from this repository’s checkout example. On the wire, each response is a single compact JSON text block.

An agent calls list_page_objects before it globs anything, and before it creates a page object that may already exist:

{
"ok": true,
"data": [
{
"name": "CheckoutPage",
"file": "e2e/page-objects/CheckoutPage.ts",
"kind": "rootPageObject",
"root": { "kind": "testId", "testId": "CheckoutPage" },
"fixtures": ["checkoutPage"],
"members": 6,
"methods": 3
},
{
"name": "CartItemControl",
"file": "e2e/page-objects/CartItemControl.ts",
"kind": "nestedPageObject",
"members": 1,
"methods": 1
}
],
"meta": {
"root": "/repo/example",
"attribute": "data-testid",
"attributeSource": "default",
"playwrightConfig": "playwright.config.ts",
"scanned": 18,
"total": 6
}
}

The server found six classes in one call, so the agent does not have to open each file. meta.attribute and meta.attributeSource show which test-ID attribute the analysis used.

get_page_object_tree with {"class": "CheckoutPage"} returns the selector tree as indented text — outline is the default, and costs a fraction of the tokens JSON would:

CheckoutPage (rootPageObject) @testId "CheckoutPage" e2e/page-objects/CheckoutPage.ts fixture: checkoutPage
PromoCode -> PageObject @testId "PromoCodeInput"
PromoCodeInput -> Locator @testId "PromoCodeInput"
ApplyPromoButton -> ButtonControl @role "button" {"name":"Apply"}
ButtonControl (nestedPageObject) e2e/page-objects/controls/ButtonControl.ts
CartItems -> ListPageObject<CartItemControl> @testIdPattern /CartItem_/
CartItemControl (nestedPageObject) e2e/page-objects/CartItemControl.ts
RemoveButton -> ButtonControl @role "button" {"name":"Remove"}
ButtonControl (see above)
methods: expectVisible()
CartItemsAsPlainList -> ListPageObject<PageObject> @testIdPattern /CartItem_/
CartItemRows -> Locator @testIdPattern /CartItem_/
methods: applyPromoCode(code: string), expectCartEmpty(), expectCartHasItemCount(n: number)

The response shows both checkoutPage.CartItems.first().RemoveButton and the existing applyPromoCode(code) method. The agent can reuse them instead of rebuilding either path.

The same response’s meta.apiHints supplies what the tree cannot: that CartItems is a ListPageObject, so it has .first(); that RemoveButton is a page object, so its raw locator is .$; and that fixture: checkoutPage means the class is taken as a test argument rather than constructed. Together they are enough to write the body:

test("removes the first cart item", async ({ checkoutPage }) => {
await checkoutPage.applyPromoCode("SAVE20");
await checkoutPage.CartItems.first().RemoveButton.$.click();
await checkoutPage.expectCartHasItemCount(2);
});

The agent did not need to open the page-object source files or guess a method name.

3. Which test IDs does static analysis find?

Section titled “3. Which test IDs does static analysis find?”

get_testid_tree with no arguments walks from the auto-detected app entry:

- Header src/App.tsx:31
SignIn button src/components/Header.tsx:5
- CheckoutPage src/App.tsx:32
CheckoutPage main src/components/CheckoutPage.tsx:18
PromoSection section :19
PromoCodeInput input :21
ApplyPromoButton button :28
PromoApplied span :36 (conditional)
CartSection section :39
EmptyCart p :42 (conditional)
CartItemsList div :44 (conditional)
- CartItemComponent :46 (conditional, repeated)
CartItem_* div src/components/CartItem.tsx:10 (dynamic `CartItem_${item.id}`, conditional, repeated)
CartItemName span :11 (conditional, repeated)
CartItemPrice span :12 (conditional, repeated)
Remove button :13 (conditional, repeated)

A leading - marks a node with no test ID. CartItem_* is a template-literal ID reported as a pattern with its source expression, which explains why the rows use @ListSelector("CartItem_"). This response has meta.fidelity: "full", so the tree has no unresolved component boundary under its root within the scanned source.