Featured

Deploy OpenClaw in 60 seconds — 20% off logoDeploy OpenClaw in 60 seconds — 20% off

Launch OpenClaw on Hostinger in about 60 seconds and keep your agent live 24/7. Our referral link gives you 20% off, no coupon code needed.

Launch on Hostinger
Run your Hermes agent on Hostinger, fully managed logoRun your Hermes agent on Hostinger, fully managed

Launch Hermes on Hostinger in one click, fully managed, no VPS knowledge needed. Use code ZACAARON10 for 10% off.

Launch on Hostinger
Crawl and scrape any site into clean data, 10% off logoCrawl and scrape any site into clean data, 10% off

Firecrawl crawls and scrapes any site into clean markdown for your agent. Get 1,000 free credits, and new users get 10% off their first purchase.

Try Firecrawl free
6,000+ web scrapers for your AI agent, start free logo6,000+ web scrapers for your AI agent, start free

Apify gives your agent live web data: 6,000+ prebuilt scrapers and actors, MCP-ready. Sign up free with $5 in usage credits.

Try Apify free
One API to scrape, enrich, and extract the internet. logoOne API to scrape, enrich, and extract the internet.

Context.dev gives your agents a single API to scrape, enrich, and extract live web data — no proxies, no parsers, no maintenance.

Start building free
SetupClaw: done-for-you OpenClaw for founders & exec teams logoSetupClaw: done-for-you OpenClaw for founders & exec teams

White-glove OpenClaw for founders and exec teams (4–50+ employees): we install, harden, integrate your tools, and maintain it — secured from day one.

Get it set up for you
SEO data APIs for your agent, $1 free credit logoSEO data APIs for your agent, $1 free credit

DataForSEO gives your agent live access to SERP results, keyword data, backlinks, and on-page SEO data through one API. New accounts get a $1 credit, good for up to 20,000 keyword or backlink lookups.

Try DataForSEO free
Reach 48,000+ AI builders

A flat monthly placement in front of developers actively installing AI tools. No lock-in, cancel anytime.

Advertise here

Works with

Claude CodeClaude DesktopCursorVS CodeClineCodex CLIOpenClaw+ any MCP client

Install to Claude Code

This server doesn't publish a one-line install command. Follow the setup in the source repository.

Summary

A local stdio MCP server that exposes compact semantic observations from UI elements annotated with data-agent-* hints, designed to work alongside the official Playwright MCP for browser automation.

README.md

Semantic Hints MCP

A small local stdio MCP server that exposes compact semantic observations from a UI annotated with data-semtag-* hints.

It is a companion to the official Playwright MCP, not a replacement. It only reads semantics; all browser actions (click, type, navigate) stay in Playwright MCP. The intended split:

  1. semantic_snapshot → a compact map of hinted elements (~2–3× smaller than a

Playwright ARIA snapshot of the same page).

  1. Pick a semantic id, e.g. checkout.submit.
  2. Act via Playwright MCP using the selector [data-semtag-id='checkout.submit'].
  3. semantic_observe → the current value/state of one hinted element.

Browser / session architecture

The semantic-hints MCP uses the Playwright library directly. It does not call the Playwright MCP internally.

However, the semantic-hints MCP and the official Playwright MCP must observe/control the same browser state during an experiment. Avoid accidentally creating two unrelated browser sessions where:

  • Playwright MCP clicks in browser A
  • semantic-hints MCP observes browser B

That would invalidate the workflow.

Semantic-hints MCP must not silently launch a separate browser by default.

Preferred design: one shared Chromium over CDP

  1. Start Chromium with remote debugging enabled, e.g.:
   chromium --remote-debugging-port=9222

(or the equivalent using your locally installed Chrome/Chromium binary).

  1. Configure the official Playwright MCP to connect to that shared browser, if

supported by the installed version/config.

  1. The semantic-hints MCP connects to the same browser via Playwright:
   chromium.connectOverCDP("http://127.0.0.1:9222")
  1. It reuses the active page where possible. If no suitable page exists and a

URL is provided, it may open/navigate a page. If no page exists and no URL is provided, it returns a clear error asking for a URL or active page.

Configuration

| Variable | Default | Purpose | | ------------------------------ | ----------------------- | ----------------------------------------------------------------------- | | SEMANTIC_HINTS_CDP_URL | http://127.0.0.1:9222 | CDP endpoint of the shared Chromium to attach to. | | SEMANTIC_HINTS_TARGET_URL | (unset) | Optional default app URL, opened when a tool is called with no url. | | SEMANTIC_HINTS_LAUNCH_BROWSER| false | If true, may launch a private browser when the CDP connection fails. | | SEMANTIC_HINTS_HEADLESS | true | Headless mode for the standalone-launch fallback only. |

Behavior:

  • If SEMANTIC_HINTS_CDP_URL is reachable, connect over CDP (the normal mode).
  • If the CDP connection fails and SEMANTIC_HINTS_LAUNCH_BROWSER is false, **fail

clearly with setup instructions** — never silently launch.

  • If SEMANTIC_HINTS_LAUNCH_BROWSER is true, the server may launch its own browser,

but every tool response then carries a warning field making clear this is a standalone session that may not share state with the Playwright MCP.

The normal research workflow should use the shared CDP browser mode.

Tools

semantic_snapshot

Returns hinted elements grouped by their data-semtag-rolenavigation, action, option, input, select, toggle, slider, observable, region, collection — plus other, collections, url and screen.

Empty groups are omitted, and there is no counts block: the arrays state their own lengths, so repeating them would only spend tokens.

{
  "url": "http://localhost:8082/products", // optional: navigate first; else read current page
  "scope": "products.filters",             // optional: data-semtag-id or CSS selector subtree
  "includeHidden": false                    // optional: include hidden elements (default false)
}

Per element (fields included only when present): id, name, action, state, target, controls, options (selects only), value (roles that hold one), enabled, visible. Never returns HTML, class names, or DOM subtrees.

There is no role field — the group key is the role, stated once for the whole group instead of once per entry. The exception is other, where the key says nothing: entries there keep the raw role string so an off-vocabulary value is visible rather than silently erased. semantic_observe always includes role, having no group around it.

The ten roles and the seven data-semtag-* attributes are defined in semtag-injector-skill/semtag-inject/references/hint-design.md, the source of truth for this server, the hint-injection prompt, and the browser-agent prompt alike. Nothing is inferred from tags or ARIA, and nothing is repaired: an element with a data-semtag-action but no data-semtag-role is other, not an action. other is how a hinting gap surfaces, so it is a thing to go fix in the app.

enabled and visible are omitted unless false — an element carrying neither is enabled and visible. Since snapshots exclude hidden elements by default, a "visible": true on every entry would only restate the filter.

Folded collections

A repeated collection is most of a real snapshot, and nearly all of it is repetition: every card restates its role and target, every price cell restates its role and state. So a collection is folded into one columnar block — shared attributes stated once, each item reduced to a row of only what varies:

"collections": [{
  "id": "products.grid",
  "name": "Products",
  "idPattern": "products.grid.item.{key}[.{control}]",
  "item":         { "role": "navigation" },
  "itemControls": { "price": { "role": "observable", "state": "product.price" },
                    "remove": { "role": "action", "name": "Remove",
                                "action": "remove-product" } },
  "fields": ["key", "name", "target", "price.value", "remove"],
  "items": [
    ["alpha", "Alpha Phone",  "product.detail", "£10.00", true],
    ["beta",  "Beta Laptop",  "product.detail", "£20.00", true],
    ["gamma", "Gamma Bundle", "bundle.detail",  "£30.00", null]
  ]
}]
  • item / itemControls[c] hold the fields every item agrees on.

(itemControls, not controls, because the container may carry its own data-semtag-controls.)

  • fields is the column header: "key" first, then the item's own varying

fields by bare name, then "<control>.<field>". items rows align to it, and null marks an absent cell.

  • An element only some items have gets a bare "<control>" presence column

holding true / null — above, gamma has no Remove button. It is needed because a uniform optional control otherwise hoists away completely, leaving no cell that could be null. The item itself can be optional too, and then takes a column named "item".

  • name is dropped from a cell when it equals value: for a text observable

the accessible name is the displayed value.

  • The bucket an element would have occupied is not carried. On real pages it

only ever restated role ("navigation"/"navigation"), and role with action / target / state already says everything an agent acts on.

Folding costs no addressability. idPattern states how to rebuild any member's real data-semtag-idproducts.grid.item.beta.price — which semantic_observe and [data-semtag-id="…"] still resolve exactly as before.

Folded members and their container leave the six flat buckets, so nothing is stated twice.

Folding needs two things: at least two items using the <prefix>.item.<key> id convention, and a DOM ancestor declaring data-semtag-role="collection". Ancestry, not id equality — a container is often named a little differently from the prefix its rows use (admin.products.list holding admin.products.item.42), and that is a naming nicety, not a reason to give up the compression. Hence id (the container's own hint) and idPattern (built from the rows' prefix) can differ. A repeated group with no collection ancestor at all is a hinting gap in the app, not something the extractor guesses at: it stays flat, and the hint-review overlay flags it.

semantic_observe

{ "id": "cart.total" }

Resolves [data-semtag-id="<id>"] and returns its compact current state. Returns a clear error if zero or multiple elements match.

Setup

cd semantic-hints-mcp
npm install
npx playwright install chromium   # one-time browser download
npm run build

See Configuration above for the environment variables.

Register in Claude Code

From this folder, after npm run build:

claude mcp add semantic-hints \
  --env SEMANTIC_HINTS_CDP_URL=http://127.0.0.1:9222 \
  --env SEMANTIC_HINTS_TARGET_URL=http://localhost:8080 \
  -- node "$(pwd)/dist/index.js"

Or add it to .mcp.json / your Claude Code config manually:

{
  "mcpServers": {
    "semantic-hints": {
      "command": "node",
      "args": ["/absolute/path/to/semantic-hints-mcp/dist/index.js"],
      "env": {
        "SEMANTIC_HINTS_CDP_URL": "http://127.0.0.1:9222",
        "SEMANTIC_HINTS_TARGET_URL": "http://localhost:8080"
      }
    }
  }
}

(For development without a build step, use "command": "npx", "args": ["tsx", "/abs/path/src/index.ts"].)

Usage example

Expected Claude Code workflow:

  1. Start the app under test, e.g.
   npm run dev          # → http://localhost:8080 for the annotated WebTestBench app
  1. Start a shared Chromium with CDP enabled, e.g.
   chromium --remote-debugging-port=9222
  1. Register/start both MCP servers:
  • the official Playwright MCP, configured for the shared browser if possible
  • the semantic-hints MCP, configured with SEMANTIC_HINTS_CDP_URL=http://127.0.0.1:9222
  1. Agent calls:
   semantic_snapshot({ "url": "http://localhost:8080" })
  1. Agent interacts using the official Playwright MCP:
   browser_click({ "target": "[data-semtag-id='checkout.submit']" })
  1. Agent calls:
   semantic_observe({ "id": "cart.total" })

The click (step 5) and observe (step 6) must operate on the same browser page/session — which is exactly what the shared CDP browser guarantees.

Tests

npm test

Vitest loads test/fixture.html in headless Chromium and checks: grouped compact output, hidden-element handling, scope, observable/input value reads, missing & duplicate-ID errors, and that output contains no raw HTML/DOM.

See related servers & alternatives →

Related MCP servers

Browse all →

Related guides

Hand-picked reading to help you choose and use Browser & Scraping servers.