playwright-page-object
MCP Server

Set Up the Playwright Page Object MCP Server

Install and connect the Playwright page-object MCP server to Claude Code, Cursor, VS Code, or Codex CLI, then verify its tools.

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.

1. Install the package

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:

npm install -D playwright-page-object

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

2. Verify the CLI

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:

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.

3. Register the server in your MCP client

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

Claude Code

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:

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

Cursor

.cursor/mcp.json, same shape:

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

VS Code

.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"]
		}
	}
}

Codex CLI

Add the server with the CLI:

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"]

What if the client cannot start npx?

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.

4. Confirm the five tools are available

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.

What should an agent call first?

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.

1. Which page objects already exist?

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.

2. What does one page object expose?

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?

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.

Next

Last updated on

On this page