semantic-dom-mcp
Summary: This server drives a real Chromium browser via Playwright to give coding agents verified Playwright locators, structured page/assertion data, and observed page behavior so they can write real (not guessed) Playwright tests.
Extract a page's semantic DOM (
extract_semantic_dom): navigate an allowlisted staging URL (desktop or mobile viewport) and get factual Semantic JSON of all interactive/test-relevant elements with ready-to-use, uniqueness-verified Playwright locators and live state.Extract after interactions (
extract_semantic_dom_after): run a short declared action list (fill/click/press/wait, max 20) in the main frame, then snapshot the resulting state — good for success/error toasts, validation messages, and opened dialogs — with deterministic waits viawait_selector_after.Diagnose frames (
list_frames): inspect a page's frame tree (frame_path, url, name, same-origin/reachability) to debug cross-origin iframe boundaries before extraction.Check authentication (
check_auth): navigate using the configured storageState and detect if the session bounced to a login page, revealing expired auth instead of mysterious results.Tune extractions with options for
wait_for,wait_selector,max_nodes,include_hidden, andinclude_click_targets(heuristic JS-click cards).Use these alongside the README's session tools (
session_open/session_act/session_extract/etc.), prompts (write_playwright_test), and resources (conventions://playwright) to build multi-step flows grounded in real locators, redirects, API paths, and state transitions.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@semantic-dom-mcp extract the checkout page and write a success-path test"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
semantic-dom-mcp
Local MCP server (stdio, Node.js + TypeScript) that drives a real Chromium browser via Playwright and gives a coding agent what it needs to write a Playwright test it did not have to guess: verified locators (counted by Playwright's own engine, scoped the way a QA engineer scopes them), structured assertion data (tables as rows and cells, dialogs as label/value pairs), the behavior the page showed (navigations, requests, console errors), and diffs between steps of a flow. Same page → same output → same conventions → same test style across the team.
How it keeps tokens low. An agent starts with an outline of the page (regions, tables, dialogs, alerts; a few hundred to a few thousand characters, smaller than the native accessibility tree), then extracts one region, and acts with the snapshot in the same call. Measured on a real seller dashboard, the outline was 82–95% smaller than Playwright's aria snapshot of the same page, and a whole authenticated add-to-cart flow ran in about 10k tokens. A full-page extraction is still 2–3x the size of the aria snapshot, because it carries what that tree does not: executable locators, uniqueness verdicts and state. Ask for the whole page only when you need it. Numbers: benchmark/RESULTS.md, FLOW-VALIDATION.md. Docs: How it works (deep dive) · Team guide (setup + connecting your agent) · Benchmark methodology · v0.5 flow validation · Roadmap · Changelog
Quickstart
No clone, no build — the package is on npm. One-time browser setup (installs the Chromium build matching the package's bundled Playwright):
npx -y -p semantic-dom-mcp playwright install chromiumThen add the server to your MCP client:
{
"mcpServers": {
"semantic-dom": {
"command": "npx",
"args": ["-y", "semantic-dom-mcp"],
"env": {
"QA_MCP_ALLOWED_HOSTS": "staging.yourapp.internal,staging.admin.internal",
"QA_MCP_STORAGE_STATE": "./.auth/staging.json"
}
}
}
}That's the whole setup. Verify by asking your agent to list its MCP tools — you should
see extract_semantic_dom. See docs/GUIDE.md for per-client config
locations (Claude Code, Claude Desktop, Cursor, Windsurf), authenticated staging, and
troubleshooting. To run from a clone instead (contributors), see Development below.
Related MCP server: Browser MCP Server
Workflow
Single page. Ask your agent: "extract the checkout page and write a success-path test."
The agent calls
extract_semantic_dom({ url }). The server navigates a real Chromium page, runs the extractor inside it, and returns Semantic JSON: every interactive node with a ready-to-paste Playwright locator, uniqueness verified by Playwright's own engine.The agent uses the
write_playwright_testprompt (scenario + the JSON), which injects the team conventions.The result is a Playwright test in team style, grounded in real locators, never guessed ones.
Multi-step flow (v0.8 way). Ask: "write a test for login → add to cart → checkout."
session_open({ url })opens a persistent page, thensession_extract({ mode: "outline" })maps it: regions with aselector, tables with row identity and cells, dialogs, alerts.session_extract({ scope, roles, visible_only })pulls only the region the step needs, with locators verified unique inside it.session_act({ actions, then_extract: { mode: "diff", scope } })runs declared actions (paste any returnedplaywrightexpression as the locator) and returns, in the same call, what the page did (observed: redirects, requests, console errors) and what changed (added nodes, removed nodes, state transitions). That is the wait list and the assertion list for the step.Repeat per step, then
session_verify_locatorswith the expressions in the written spec, thensession_close.
Every fact a test needs (locator, redirect, API path, state transition, cell value) comes from the page, not from the agent's memory.
MCP surface
Kind | Name | Purpose |
Tool |
| The page as a map: regions (each with a |
Tool |
| Extract a URL into Semantic JSON ( |
Tool |
| Count every Playwright expression from a written spec against the live page; matches, uniqueness, first match, summary. |
Tool |
| The team conventions as text, for clients that hide MCP prompts. |
Tool |
| Same, but first runs a short declared action list (fill/click/press/select/goto/wait, max 20) in the main frame and snapshots the resulting state, plus an |
Tool |
| Open a persistent page for a multi-step flow ( |
Tool |
| Run declared actions in an open session; |
Tool |
| Snapshot the session's current state ( |
Tool |
|
|
Tool |
| Release the session's browser context. |
Tool |
| Diagnostic: open sessions with URL, expiry and counts. |
Tool |
| Diagnostic: navigates with the configured storageState and reports whether the session bounced to a login-looking page (expired auth shows up as an answer, not a mystery). |
Tool |
| Diagnostic frame tree with same-origin/reachability classification. |
Prompt |
| Team-standard test-writing prompt ( |
Resource |
| The same team conventions as read-only text. |
Errors (navigation failure, denied host, missing selector, expired session) come back as
structured JSON in the tool result, so the agent can react instead of crashing. Every tool carries
MCP annotations (readOnlyHint, openWorldHint: false) so clients can auto-approve the read-only
ones.
Configuration (environment variables)
Variable | Meaning |
| Required. Comma-separated hostnames the server may navigate to. Navigation is denied by default. Supports |
| Optional path to a Playwright |
| Optional team name used in the |
| Idle time before a session is closed automatically (default |
| Max concurrently open sessions (default |
Security posture
Tool inputs are untrusted (they arrive via an LLM): strict schemas (
additionalProperties: false), http/https only, host allowlist enforced before any navigation.extract_semantic_domonly reads the DOM. It never clicks, submits, or mutates the page. The sanctioned exceptions areextract_semantic_dom_afterandsession_act, which execute only an explicit, bounded, schema-validated action list, never log fill values, and refuse to extract if the page leaves the allowlisted hosts (a session in that state is closed).Sessions add their own limits: an idle TTL, a cap on open sessions, one in-flight call per session, and a bounded snapshot history kept in memory only.
Behavior capture is observation only. The server never issues requests of its own. Request and response bodies are never read, query strings are stripped from recorded URLs (they may carry tokens), console text is capped, dialogs are dismissed and popups closed at once.
Sessions check the allowlist before any action, after every action, after a failed action and before every snapshot. A session found off the allowlist is closed on the spot.
No network egress beyond navigating the browser to allowlisted URLs. No telemetry. Page contents are never logged (stderr carries only high-level events) and are not stored beyond the current call or session.
Semantics worth knowing
Snapshot honesty: the JSON is a single moment. A disabled submit button is reported
is_disabled: truewith a note. The conventions instruct the model to write the interactions that change state, not to assume it stays disabled. In a session, the diff shows the transition itself (is_disabled: false → true), so the test asserts a fact rather than an assumption.Scoped locators (schema 1.4): a nameless or repeated control is located inside its container the way a QA engineer writes it by hand:
getByRole('row', { name: 'Charizard' }).getByRole('radio'),getByRole('listitem').filter({ hasText: 'Puthera' }).getByRole('button', { name: 'Pilih Pembeli Ini' }),getByTestId('customer-checkbox-flex').getByRole('checkbox'). The node carrieswithinso an action can target the same element; the expression is verified unique by Playwright like every other locator.Compact wire format (schema 1.3): results are compact JSON and a node field that carries no information is omitted:
nullfields,frame_path: [],in_shadow: false,kind: "element", emptyfallback_locators, andtext_contentequal toaccessible_name. An absent property is null (not applicable, never false); absent structure means the default (main document, light DOM, nothing worth listing).is_visibleis always present. Fallbacks appear only when the primary is ambiguous or brittle. Same facts, about a third of the tokens.Assertable state (since schema 1.2): every node reports
value(never for password fields),aria_expanded,aria_selected,aria_invalid,described_by(the text of the elementsaria-describedbypoints at, where validation messages live),validation_message(browser constraint validation) and, for<select>,options. Absent state isnull, neverfalse.Observed behavior:
observed.requestslists xhr/fetch/document requests as method + path + status; static assets are counted indropped, not listed.observed.navigationslists main-frame URL changes in order. Neither is ever guessed; if a redirect or API path is not inobserved, the conventions tell the agent not to wait on it.Diff identity: nodes pair across snapshots by the most stable fact available: test-id, else id, else placeholder, else tag + role + accessible name, plus frame path and a document-order index for non-unique nodes. A hidden menu item that becomes visible is therefore a
changedentry, with its primary locator switch (getByText→getByRole) listed as one of the changes. A renamed node with no stable attribute shows as removed + added; the diff says so in its notes.Visibility is Playwright's:
is_visiblepredictstoBeVisible(), so it uses Playwright's rule (notdisplay:none,visibilitynot hidden, width and height both > 0). Opacity andaria-hiddendo not hide an element for Playwright and do not here either.Secrets never leave the server: a value typed into a password field, or a
fillmarkedsecret: true, is scrubbed from every string in every result (values, text, console, dialogs, errors) when it is 8+ characters. The set is process-wide and lasts until the server exits, so mark only real secrets: a redacted string blanks that text everywhere, and a locator whose text was redacted is flagged not unique. Password and credential-autocomplete fields never report avalueat all.Hidden nodes are included and flagged
is_visible: false(tests often assert hidden-ness); passinclude_hidden: falseto drop them (the count dropped is noted, never silent).Open shadow DOM is traversed and flagged
in_shadow— locators pierce it natively, so no>>>/::shadowCSS is ever emitted. Closed shadow roots appear asshadow_boundarymarker nodes (detected via pre-navigationattachShadowinstrumentation; closed roots created by declarative shadow DOM parse before scripts run and cannot be detected).Same-origin iframes are extracted per-frame with
frame_pathset (chainframeLocator()in that order). Cross-origin iframes are recorded as opaquecross_origin_framenodes with URL/name only — their DOM is never touched.Notification & dialog surfaces (
role="alert",role="status", dialogs) are extracted like interactive nodes. When a toast library keeps the live region empty and renders the message in a sibling (a common pattern across UI libraries), the message text is pulled from the enclosing container and flagged. For UI that renders late after an interaction,wait_selector_afteronextract_semantic_dom_afterwaits deterministically instead of guessingsettle_ms. Since those ARIA roles take names from the author (not contents), their role locator isgetByRole('alert')— or with thearia-labelname when one exists. For UI that only appears after an interaction (login-success toast), useextract_semantic_dom_after.JS-click cards (product tiles with no anchor/role/test-id) are invisible to the factual rules by design — pass
include_click_targets: trueto include cursor-pointer boundary elements with content, flagged as heuristic and located by their heading text.Links carry
href(schema 1.1) so agents can discover which page to extract next without scraping. Framework-generated ids (rc_select_*, ReactuseId, Radix, MUI...) are detected and demoted to last-resort with a note — they change between builds and must never be primary.viewport: "mobile"(375×812, touch) snapshots responsive states; visibility flags reflect the active media queries.Truncation is loud:
max_nodes/ depth caps settruncated: trueplus a note. Non-unique locators carryis_unique: falseanddisambiguationguidance.
Development
git clone https://github.com/helmif/semantic-dom-mcp.git && cd semantic-dom-mcp
npm install
npx playwright install chromium
npm run dev # run the server over stdio via tsx
npm run typecheck # tsc --noEmit (strict)
npm test # Vitest suites against real fixture pages in headless Chromium
npm run build # compile to dist/ (clients can then use "command": "node", "args": ["<path>/dist/index.js"])Repo layout: src/index.ts (bootstrap) · src/server.ts (MCP surface) · src/browser.ts
(Playwright layer + single-shot orchestration) · src/session.ts (persistent sessions) ·
src/observe.ts (behavior capture) · src/diff.ts (snapshot diff) · src/extractor/ (in-page
engine + locator resolution) · src/types.ts (frozen contract, schema 1.3) · src/compact.ts (wire rules) · src/conventions.ts
(single source of team conventions).
Available Tools
13 toolscheck_authARead-onlyIdempotent
Diagnostic: navigates with the configured QA_MCP_STORAGE_STATE session and reports whether the page bounced to a login-looking path (session likely expired). Use when extractions unexpectedly return login forms instead of the requested page.
Inputs: url (required), wait_for.
extract_outlineARead-onlyIdempotent
The page as a MAP, a few thousand characters: landmark regions (header/nav/main/forms/tables/lists/dialogs) each with a selector you can pass as scope to extract_semantic_dom / session_extract, structured tables (headers, row identity for getByRole('row', { name }), cells) and open dialogs (label/value fields), plus current alert text. Start here on any page you have not seen; then extract one region.
Inputs: url (required), scope, viewport, wait_for, +1 more.
extract_semantic_domARead-onlyIdempotent
Navigate to a staging URL and return factual Semantic JSON of all interactive/test-relevant elements with Playwright-native locators and live state. Use this before writing any Playwright test so selectors are real, not guessed.
Inputs: url (required), roles, scope, viewport, +8 more.
extract_semantic_dom_afterA
Like extract_semantic_dom, but first performs a short DECLARED list of actions (fill/click/press/select/goto/wait) in the main frame, then returns Semantic JSON of the RESULTING state. Use it for post-interaction UI a plain snapshot cannot see: success/error toasts, validation messages, opened dialogs. Derive action locators from a prior extract_semantic_dom call. The page must remain on allowlisted hosts after the actions, or nothing is extracted. Uniqueness reflects capture time — accumulating UI (chat threads, lists) can multiply matches later. The result's observed block lists navigations, xhr/fetch requests (method, path, status), console errors, dialogs and popups seen while the actions ran — use them for waitForURL/waitForResponse.
Inputs: url (required), actions (required), roles, scope, +11 more.
get_conventionsARead-onlyIdempotent
The team's Playwright test-writing conventions as text (same content as the write_playwright_test prompt), for clients that do not surface MCP prompts.
Inputs: no named parameters.
list_framesARead-onlyIdempotent
Diagnostic: navigate to a URL and return its frame tree (frame_path, url, name, same_origin, reachable). Useful for debugging cross-origin iframe boundaries before extraction.
Inputs: url (required), wait_for.
session_actA
Perform a short DECLARED action list (fill/click/press/select/goto/wait, max 20) in an open session's main frame. Returns the page's resulting URL/title and observed: main-frame navigations, xhr/fetch requests (method, path, status — bodies and query strings never captured), console errors, dialogs (auto-dismissed) and popups (recorded, closed). These are the facts for waitForURL/waitForResponse. Derive action locators from a prior extraction (paste its playwright expression). Pass then_extract to get the diff (or a scoped extraction / outline) in the same call. If the actions leave the allowlisted hosts the session is closed and nothing further is extracted.
Inputs: actions (required), session_id (required), settle_ms, then_extract, +1 more.
session_closeAIdempotent
Close an open session and release its browser context. Always call this when the flow is done. Idempotent: closing an unknown or already-closed id succeeds with was_open: false.
Inputs: session_id (required).
session_extractARead-only
Snapshot the CURRENT state of an open session as Semantic JSON (same shape as extract_semantic_dom, plus snapshot_id). Pass diff_against: 'previous' (or a snapshot_id) to receive only what changed: added nodes (new toasts/dialogs/fields), removed nodes, changed properties (value, is_disabled, aria_invalid, described_by…) and the behavior observed in between — far smaller than a full re-extraction and exactly the assertion list for the step. Diff identity: frame + test-id, else id, else placeholder, else tag+role+accessible name (+ document-order index); a renamed node with no stable attribute shows as removed + added; primary_locator.playwright changes say which locator is valid in which state.
Inputs: session_id (required), mode, roles, scope, +7 more.
session_listARead-onlyIdempotent
Diagnostic: list open sessions (id, URL, expiry, counts) — recover a session id after losing context.
Inputs: no named parameters.
session_openA
Open a persistent browser session at a staging URL for a MULTI-STEP flow (login → cart → checkout). The page stays open across calls: use session_act to perform declared actions and session_extract to snapshot or diff, then session_close. Fresh context per session (storageState applied if configured). Sessions expire after an idle TTL and are capped in number; the allowlist is re-checked after every step.
Inputs: url (required), viewport, wait_for, wait_selector.
session_verify_locatorsBRead-onlyIdempotent
verify_locators against an open session's current page (after acting, without a fresh navigation).
Inputs: locators (required), session_id (required).
verify_locatorsARead-onlyIdempotent
Count every given Playwright expression against the live page (same engine that verified the extraction). Use after writing a test: paste the spec's getBy*/locator expressions and get matches, uniqueness, and the first matched element per expression, plus a summary. Also the drift check to run in CI against a page.
Inputs: url (required), locators (required), viewport, wait_for, +1 more.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 13 tools
The set has a clear two-tier structure (one-shot extraction/verification vs. persistent session tools), and descriptions carefully separate them. However, extract_semantic_dom_after overlaps conceptually with the session_open/session_act/session_extract flow, and extract_semantic_dom vs session_extract require reading descriptions carefully to pick correctly.
Almost everything is consistent snake_case verb_noun, and the session_* prefix gives a clean, predictable grouping. Minor deviation: extract_semantic_dom_after uses a temporal suffix rather than a distinct verb, which slightly breaks the pattern.
13 tools is well within the sweet spot and each one earns its place, covering extraction, diagnostics, verification, conventions, and full session lifecycle without filler.
The surface covers the full Playwright test-authoring workflow: page mapping, snapshot extraction, post-interaction observation, locator verification, auth/frame diagnostics, conventions, and end-to-end session management with open/act/extract/verify/close/list. No obvious dead ends.
Maintenance
Related MCP Connectors
Hosted browser for AI agents: screenshots, post-JS DOM, console, WCAG. No install, no API key.
Cloud browser, web extraction, screenshots, and remote CDP tools for AI agents.
Control a real browser from AI agents: navigate, click, type, screenshot, read the page.
Give AI agents a real cloud Chrome: open sessions, navigate, read pages, click, type and screenshot.
Related MCP Servers
- AlicenseAqualityCmaintenanceEnables AI assistants to inspect, debug, and test web pages using Playwright. Provides comprehensive DOM inspection, visibility debugging, layout validation, and element finding capabilities in real browser environments.3471 npm5MIT
- AlicenseAqualityDmaintenanceEnables AI agents to understand web page structure and content through structured data extraction and element discovery using Playwright, eliminating the need for screenshots.414 npmMIT
- AlicenseAqualityBmaintenanceGives AI agents a compact, semantic interface to the browser, returning structured page snapshots with stable element IDs instead of raw DOM. Enables agents to navigate, interact, and extract information from web pages efficiently.2633 npm19MIT
- AlicenseAqualityBmaintenanceEnables AI agents to drive Playwright-based browser automation for UI testing, returning JSON/HTML reports with screenshots without server-side LLM or test scripts. https://easy-ui-mcp.vercel.app/16261MIT