Skip to main content
Glama

page-mcp

A CDN-delivered WebMCP tool provider.

Drop one <script> into a web UI and that page's own JS engine starts hosting a set of devtools tools on document.modelContext:

  • DOM inspection — a text outline of the page with stable element refs

  • Interactions — click, fill, type, press, select, hover, scroll, upload

  • Diagnostics — console/error capture, and a JS REPL

Any agent that can reach the page can discover and call them. There is no server, no LLM and no chat UI in here — this is the tool surface, not an agent.

<script src="https://cdn.jsdelivr.net/npm/page-mcp/dist/page-mcp.js"></script>
<!-- unpkg mirror: https://unpkg.com/page-mcp/dist/page-mcp.js -->

npm i page-mcp ships the same single file at node_modules/page-mcp/dist/page-mcp.js for self-hosting.

Related MCP server: webappmcp

How it is consumed

The premise is that the coding agent owns the browser: it launches Chrome (headless or not), points it at the app it is working on, and drives it over CDP. Under that premise the agent needs no bridge — the WebMCP tools are reachable through the channel it already has.

   coding agent (Claude Code / Cursor / DSH / …)
              │
              │  the browser channel it already has
              │  · CDP Runtime.evaluate
              │  · chrome-devtools-mcp (list_webmcp_tools / execute_webmcp_tool)
              │  · Playwright / Puppeteer
              ▼
      Chrome  ──  page + <script src="…/page-mcp.js">
                        │
                        ▼
              document.modelContext
              ├── dev_snapshot, dev_click, dev_fill, …
              └── <tools the app registers itself>

executeTool calling contract, including the failure shapes

executeTool(tool, "{}")              // ✓ the second argument is a JSON STRING
executeTool("name", "{}")            // ✗ TypeError: RegisteredTool must be an object
executeTool(tool, {})                // ✗ DOMException UnknownError: Failed to parse input
                                     //   arguments — a DOMException, NOT a TypeError, so
                                     //   harnesses that classify errors by constructor
                                     //   name mis-sort this one as a tool bug

The result is the tool's own raw string, not a CallToolResult wrapper.

Two details worth stating precisely:

  1. CDP Runtime.evaluate runs in the page's own realm, so the tools Permissions-Policy default allowlist ('self') is satisfied — page JS can call getTools() / executeTool(), and so can an agent evaluating in that realm.

  2. Do not depend on Chrome's native WebMCP. It sits behind a flag / origin trial. The bundle carries the polyfill and installs it only when document.modelContext is absent, so the same file works on stable Chrome with no flag, in headless, and in Firefox/Safari.

A relay or extension is only needed when the agent does not own the browser — i.e. the tools live in the developer's own everyday Chrome. That is a secondary case; @mcp-b/webmcp-local-relay covers it.


The app's own tools

The highest-value half. A page can register tools that expose its own domain state, which no external agent can infer from the DOM:

window.pageMcp.register({
  name: "vitrine_state",
  description: "Read the live state of the 3D viewer…",
  annotations: { readOnlyHint: true },
  inputSchema: { type: "object", properties: {} },
  run: () => JSON.stringify({ /* … */ }),
});

This matters most where the DOM is empty. In the bundled demo the app renders into a WebGL canvas: a snapshot shows three buttons, while vitrine_state reports the scene graph, renderer stats and materialize progress. Module-scoped bindings are unreachable from eval, so registering a tool is the only way to expose them.


Tools

Tool

What it does

dev_snapshot

Text outline of the page; stable refs; include_hidden

dev_read

Text, attrs, computed style, CSS path for one ref

dev_find

Search by word/phrase, get live refs — no full snapshot needed

dev_box

Geometry + hit-test state; the handoff for a CDP element screenshot

dev_geometry_audit

Prove clipped text / unclickable / broken-image defects without pixels

dev_click

Real pointer/mouse sequence, then native click

dev_fill

Set one field's value, or a whole form in one call (React/Vue-aware)

dev_type

Per-keystroke typing for autocomplete-style inputs

dev_press

Key or key combination

dev_select

Choose an option in a native <select>, or list the options first

dev_hover

Pointer over an element

dev_drag

Pointer drag, ref or CSS selector — reaches canvases that have no ref

dev_scroll

Scroll by delta or bring a ref into view

dev_upload

Attach files (url or base64) to a file input or dropzone

dev_wait

Wait for a selector, text or JS predicate

dev_console

Console + uncaught errors + unhandled rejections

dev_network

fetch / XHR / beacon, with the call site that made each request

dev_storage

localStorage / sessionStorage / cookie read-write, for setting up or resetting app state

dev_changes

Ordered DOM delta since a token — what your action actually did

dev_perf

Long tasks / LCP / CLS / heap + a live frame sample

dev_assert

Check several facts in one call; a failure is a real failure

dev_eval

JS REPL

Names are prefixed (data-prefix) so they cannot collide with another browser tool set in the same agent context. Every tool carries debugging: true, which a consuming agent can use to filter dev tooling out of an end-user surface.


Boundaries and known limits

  • style-src without 'unsafe-inline' kills inline styles. Measured on Chrome 151: <style> in a shadow root is blocked, el.style.x = … is blocked and a violation is logged, but CSSStyleSheet + adoptedStyleSheets works. The page indicator and the click highlight use the latter.

  • script-src without 'unsafe-eval' kills dev_eval. It fails with a readable message rather than a crash; the CSP-safe path is to have the app register named tools. dev_snapshot and the act tools are unaffected.

  • Unknown and mistyped arguments are rejected, not ignored. dev_scroll {delta_y: 200} once scrolled by 0 and reported success. Validation now refuses the call before it runs: invalid arguments — unknown property "delta_y" — accepted: ref, dx, dy, include_snapshot. A third-party wrapper that forwards extra fields will be refused outright rather than silently mis-executing. That is deliberate; opt out per tool with additionalProperties: true.

  • Some tools cannot exist in the page. dev_upload takes a URL or base64 because the page cannot read a path on the developer's disk. Uploading a host path is a job for the agent's CDP layer (DOM.setFileInputFiles); screenshots are likewise a CDP-side concern (canvas.toDataURL only works when the app preserved the drawing buffer).

  • debugging is dropped on Chrome 151 — both native and polyfilled

  • Annotations are not propagated by the runtime — so we restore them. On Chrome 151, native getTools() returns readOnlyHint: false even for a tool that registered true, and the bundled polyfill (@mcp-b/webmcp-polyfill@5.1.0) hard-drops debugging and consequentialHint entirely. Since we know what we registered, getTools() is wrapped to merge our annotations back in, so a consumer now sees debugging: true on both runtimes. Do not "fix" this by switching to the 6.0.0-beta polyfill or the CG's own polyfill: they preserve the keys but change executeTool to take an input object, while native Chrome and chrome-devtools-mcp pass a JSON string. That trades an annotation for an interop break. Measured: docs/research/polyfill-annotation-verification.md.

  • Secure context required. document.modelContext is [SecureContext]. https:// and localhost / 127.0.0.1 qualify; plain-HTTP on a LAN IP does not.

  • Dev-only by construction. dev_eval is arbitrary JS execution in the user's session. The page-corner indicator says so and the script tag can be omitted from production builds.


Development

npm install
npm run build          # dist/page-mcp.js — single-file IIFE, polyfill included
node harness/make-demo.mjs   # regenerate demo/index.html from the three.js app
npm run serve          # http://127.0.0.1:8940/demo/
npm run drive          # end-to-end suite, native + polyfill runtimes

harness/browser.mjs is the browser tool an agent owns — a CDP attach client, deliberately not a bridge to this package:

node harness/browser.mjs start http://127.0.0.1:8940/demo/
node harness/browser.mjs tools
node harness/browser.mjs call dev_snapshot
node harness/browser.mjs screenshot /tmp/shot.png

Verified

  • 56/56 end-to-end assertions pass in both runtimes (native flag, and polyfill with no flag): discovery via getTools(), executeTool() round trips, snapshot of a canvas page, disabled-control diagnosis, app-tool read and mutation, console capture, form pass, eval. Tool count: 24 per runtime.

  • The published demo works over public HTTPS with the polyfill (https://view.pc.randomhash.app/2026-10-03_webmcp-devtools-demo/).

  • Styling survives a response-header CSP of default-src 'self'; script-src 'self'; style-src 'self' with 0 violations.

Independently reproduced by other agents on different machines (Chrome 147 + Chrome 151; docs/03, docs/10) — docs/10 records 24/24 tools usable in a third environment. Docs — index · 00 positioning · 01 agent usability test · 02 measurements · 03 external verification · 04 drive rerun

Headless without software GL (--enable-unsafe-swiftshader --use-angle=swiftshader) will hide any app-registered tools: the app's module aborts on a failed WebGL context and its register() calls never run. harness/browser.mjs and drive.mjs pass the flags already.

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    D
    maintenance
    Enables AI assistants to interact with web applications through DOM inspection, user interaction simulation, and application state management.
    MIT
  • A
    license
    C
    quality
    A
    maintenance
    Enables AI agents to automate real Chrome via a structured Semantic Action Graph, without screenshots, CDP, or bot detection. It supports clicks, form filling, navigation, and state diffs on strict-CSP sites and modern SPAs.
    7
    5
    MIT