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.
1. Install the package
Section titled “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-objectThe package requires Node.js 20 or later. See Installation for the full library requirements.
2. Verify the CLI
Section titled “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 --helpYou 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
Section titled “3. Register the server in your MCP client”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:
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:
codex mcp add playwright-page-object -- npx playwright-page-object mcpOr 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?
Section titled “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-ycan leave the client waiting. - On Windows, a client that cannot execute the
npx.cmdshim 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
Section titled “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?
Section titled “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?
Section titled “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?
Section titled “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?
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.
- Read How to Use the Server for the workflows these three calls belong to.
- Read the tool reference for every parameter and error code.
- Use Troubleshooting if the first call is empty or includes a warning.
- Return to the MCP overview for the recommended workflow and the difference from browser automation.