Use the Playwright Page Object MCP Server in a Monorepo
A single-package repository needs no configuration: the server takes the working directory as its root and discovers everything else. A monorepo has to answer one question first - which sources belong to this server - because page objects and the components they select often live in different packages.
Get that wrong and the symptom is misleading rather than loud: selectors read as dead because the components that render their test IDs were never scanned.
Which layout do I have?
Section titled “Which layout do I have?”| Layout | Approach |
|---|---|
| One app, its own e2e specs | --project-root <app> - one server |
| App plus a shared UI package | Root the server at the common ancestor, then narrow with --src-dir |
| Several independent apps | One server per app, each with its own --project-root |
| Apps with different test-ID attributes | One server per app with its own --attribute - or one server, overriding attribute per call |
Every flag below defines the server’s workspace, so changing any of them needs a restart. Editing page objects, components, or the Playwright config does not.
How do I scope one app?
Section titled “How do I scope one app?”Point --project-root at the package. Discovery then runs inside it: the Playwright config, the tsconfig, and the test-ID attribute all resolve as they would in a standalone repository.
{ "mcpServers": { "web-page-objects": { "command": "npx", "args": ["playwright-page-object", "mcp", "--project-root", "apps/web"] } }}Name the server after the app rather than leaving it as playwright-page-object. In a multi-app setup the name is the only thing that tells an agent which one to call, and every tool description is otherwise identical.
What if my components live in a shared package?
Section titled “What if my components live in a shared package?”This is the case that needs care. Suppose page objects sit in apps/web/e2e and the components they select come from packages/ui:
repo/ apps/web/e2e/page-objects/ <- @Selector("SubmitButton") packages/ui/src/ <- renders data-testid="SubmitButton"Rooting at apps/web scans the page objects but not the components. map_coverage then reports the selector under deadSelectors, because nothing it can see renders that ID.
--src-dir cannot fix this. It only ever narrows an existing root - a path resolving outside --project-root is refused at startup:
--src-dir is outside --project-root: ../../packages/uiThe fix is to re-root at the common ancestor and narrow from there:
{ "mcpServers": { "web-page-objects": { "command": "npx", "args": [ "playwright-page-object", "mcp", "--project-root", ".", "--src-dir", "apps/web/e2e", "--src-dir", "packages/ui/src", "--playwright-config", "apps/web/playwright.config.ts" ] } }}--src-dir is repeatable and accepts globs, so --src-dir "packages/*/src" covers several packages at once. For a positive pattern a .. segment is refused wherever it appears, including after a glob (the check runs against the pattern’s static base), since its only effect there is to escape the root.
An exclusion is exempt. A value starting with ! is skipped before either check runs, on the grounds that excluding something outside the root is a harmless no-op. That also means a misspelled exclusion is accepted in silence - --src-dir '!../other/**' neither errors nor excludes anything - so do not read a clean startup as proof that a negated pattern matched what you meant.
Pin --playwright-config when you re-root like this. At the monorepo root the server may find several configs and rank one it did not intend; naming the app’s config removes the ambiguity, and the test-ID attribute follows from it.
How do I know a package is missing?
Section titled “How do I know a package is missing?”The server tells you, but the two tools say it differently.
map_coverage raises ui-scope-incomplete, naming the modules:
N component tag(s) come from M module(s) outside the scanned sources (
@acme/ui). Test ids rendered inside them are invisible here, so an id may exist without appearing in this report and a selector for one reads as dead.
When those sources are resolvable inside the repository, the hint names the directory to pass to --project-root. A module that resolves into node_modules has no usable re-root target - its IDs can only be confirmed from the package itself.
get_testid_tree does not emit that diagnostic. It reports the same situation structurally: the boundary node is marked external-module, fidelity drops to "partial", and fidelityReason counts the holes. So do not wait for ui-scope-incomplete on a tree response - read fidelity instead, and see Limitations for every boundary reason.
Read scope on any map_coverage response to confirm what was actually scanned:
"scope": { "uiFilesScanned": 5, "pageObjectFilesScanned": 5, "externalComponentModules": ["react"], "externalComponentTags": 1}externalComponentModules naming one of your own packages means the scope is still too narrow. react appearing there is expected and harmless.
How do I serve several apps at once?
Section titled “How do I serve several apps at once?”Register one server per app. Each gets its own root, and anything that differs per app - the attribute, the config - goes on that server:
{ "mcpServers": { "web-page-objects": { "command": "npx", "args": ["playwright-page-object", "mcp", "--project-root", "apps/web"] }, "admin-page-objects": { "command": "npx", "args": [ "playwright-page-object", "mcp", "--project-root", "apps/admin", "--attribute", "data-tid" ] } }}Servers are fully independent: separate workspaces, separate caches, and separate coverage handles. A coverageId from one is not valid on another.
Can I just point one server at the monorepo root?
Section titled “Can I just point one server at the monorepo root?”Often, yes - it is the simplest setup and it works up to the file limit. The trade-offs:
--max-filesdefaults to 8000. Crossing it does not degrade quietly - the call is refused withmax_files_exceeded, and every later call raises again until you raise--max-filesor narrow the scan with--src-dir. There is no partial-but-usable scan to fall back on. The cap counts files parsed, which includes anything the resolver pulls in, not just the files you analyse.- One default attribute, not one attribute. The server resolves a single default, but
get_testid_treeandmap_coverageeach accept a per-callattributethat overrides it, andquery_coverageinherits whatevermap_coverageused through the handle. A root-scoped server can therefore serve apps that disagree ondata-testidversusdata-tid- the caller just has to pass the right one on every call, and a forgotten override silently reads the wrong attribute rather than failing. Per-app servers make the correct attribute the default, which is a reliability argument rather than a capability one. - Config discovery has more candidates. Pin
--playwright-configif the ranked choice is not the one you want. list_page_objectsreturns every app’s classes. That is useful for cross-app work and noisy for single-app work; thefilterargument is a plain substring of class name and file path.
A root-scoped server is a good default when packages share one attribute and one Playwright config. Split per app as soon as either diverges.
Which tsconfig does it use?
Section titled “Which tsconfig does it use?”Explicit --tsconfig wins. Failing that, <project-root>/tsconfig.json is checked before anything near the specs - only when the root has none does the server walk up from the Playwright testDir to find the nearest one.
That order matters most in exactly the setup this page recommends. Re-root at the repository root to reach a shared UI package and, if a tsconfig.json sits there, the e2e config beside your specs is not used - its paths, include, and compiler options are not in effect. Pass --tsconfig explicitly when you want the e2e one. See Configuration for the full order.
How do I verify the scope is right?
Section titled “How do I verify the scope is right?”After a config change, one call answers it:
list_page_objects- the classes you expect appear, and their paths are the ones you intended.map_coveragewithbuckets: []- readscopeandmeta.warnings. Noui-scope-incompletenaming your own packages means both sides are in scope.
If list_page_objects is empty, the page objects are outside the scan or the files do not import from playwright-page-object. See Troubleshooting and Limitations for what the tools can see at all.
See also
Section titled “See also”- Configuration - every CLI option, discovery order, and refresh behaviour
- Limitations - what static analysis cannot prove, including cross-package boundaries
- Troubleshooting - empty scans, wrong attributes, and partial trees
- Quick Start - registering the server with your MCP client