Skip to main content
Glama

webmcp-guard

A WebMCP-aware drop-in replacement for @playwright/mcp.

Playwright MCP is the de facto browser-automation MCP server wired into Claude Code, Cursor, VS Code, and friends. It has no awareness of WebMCP — the emerging W3C proposal that lets a page register agent-callable tools directly (document.modelContext.registerTool, with navigator.modelContext as a legacy alias). Every agent using plain Playwright MCP on a WebMCP-enabled site falls back to click/type/scrape automation even when the page is explicitly offering a better, structured interface.

webmcp-guard sits in front of @playwright/mcp as a proxy: it passes its entire native tool surface through unchanged (zero behavior regression for sites that don't use WebMCP), detects WebMCP tools after every navigation, and — because those tools are JavaScript the page controls rather than code the server ships — exposes them behind a trust layer: session-scoped provenance tracking that flags when a previously-seen tool's schema changes, mandatory confirmation before any page-defined tool call runs, clear webmcp:* namespacing so they're never confused with native tools, and an audit log. See docs/trust-boundary-design.md for the full reasoning behind that design.

Validated against the real ecosystem, not just synthetic tests: all 168 live sites in the webmcp.com public directory, plus a hand-built adversarial test case. That process found and fixed a real cross-origin tool-invocation vulnerability in this project's own trust layer. Full methodology, evidence, and honest limitations in docs/testing-and-validation.md.

Install / config

Not yet published to npm. For now, build from source (see Install / build (from source) below) and point your MCP client config at the built CLI directly. Once published, install will be exactly npm install -g webmcp-guard / npx webmcp-guard, mirroring @playwright/mcp's own install path — the config shape below is already written for that end state, it just uses a local path today instead of a package name.

webmcp-guard is close-to-drop-in with @playwright/mcp: it only special-cases --browser and --headless/--headed (needed to build its own browser launch config), and forwards every other flag — including --allowed-origins, --blocked-origins, --allow-unrestricted-file-access, --isolated, --caps, etc. — verbatim to the underlying @playwright/mcp process it spawns. Any existing playwright-mcp config line keeps working unmodified.

Before — a typical @playwright/mcp entry in an MCP client config (mcpServers in Claude Code's or Cursor's config file):

{
  "mcpServers": {
    "playwright": {
      "command": "npx",
      "args": [
        "@playwright/mcp@latest",
        "--browser", "chrome",
        "--allowed-origins", "https://example.com",
        "--blocked-origins", "https://evil.com"
      ]
    }
  }
}

After, once published — swap the package name in args, everything else (all flags, their values, and their order) is unchanged:

{
  "mcpServers": {
    "playwright": {
      "command": "npx",
      "args": [
        "webmcp-guard@latest",
        "--browser", "chrome",
        "--allowed-origins", "https://example.com",
        "--blocked-origins", "https://evil.com"
      ]
    }
  }
}

Today, from a local build — same flags, command/args point at the built CLI instead:

{
  "mcpServers": {
    "playwright": {
      "command": "node",
      "args": [
        "/absolute/path/to/webmcp-guard/packages/webmcp-guard/dist/cli.js",
        "--browser", "chrome",
        "--allowed-origins", "https://example.com",
        "--blocked-origins", "https://evil.com"
      ]
    }
  }
}

Once published, the entire migration is a one-line package-name swap — no flags need to be added, removed, or reordered.

Related MCP server: mcp-browser

What webmcp:* tools are, and what you'll see

When webmcp-guard navigates to a page that registers WebMCP tools, it lists them to your harness as new, separate MCP tools named webmcp:<name> (e.g. webmcp:add_to_cart) — distinct from and never merged into Playwright's native tool list, so it's always obvious at a glance whether a tool is server code or something the page itself defined.

Because a webmcp:* tool's implementation is JavaScript the page controls, every call into one requires explicit confirmation before it runs — this is on by default and is not something a misconfigured client can silently skip past. What that confirmation looks like depends on your client:

  • Claude Code CLI (and any client that declares MCP elicitation support): you'll see an interactive prompt showing the tool name, the page's origin, and — if the tool's schema has changed since webmcp-guard last saw it from that origin — an explicit warning that its definition changed. You accept or decline; declining, cancelling, or letting the prompt time out (30s) all result in the call being denied, never silently allowed.

  • Clients without elicitation support (e.g. Claude Desktop, or a Cursor session hitting its current elicitation-rendering bug): webmcp-guard falls back to relying on your client's own generic tool-approval settings — Claude Code's settings.json ask/allow/deny rules, or Cursor's allowlist/auto-run approvals — keyed on the webmcp:* tool name. You should configure an ask (or equivalent) rule for webmcp:* tools in these clients yourself; webmcp-guard can't force this from the server side, though every fallback-path call is still recorded to its audit log so the reliance is visible after the fact.

See docs/trust-boundary-design.md for why this two-track design exists rather than relying on elicitation alone.

Architecture

flowchart TD
    Harness["Agent Harness\n(Claude Code / Cursor / etc.)"]
    Guard["webmcp-guard\npackages/webmcp-guard"]
    Trust["@webmcp-guard/trust-layer\nprovenance · confirmation gate\nnamespacing · audit log"]
    Adapter["@webmcp-guard/playwright\nspawns & speaks to @playwright/mcp"]
    Upstream["@playwright/mcp\n(spawned subprocess, pinned version)"]
    Browser["Real Chrome"]
    Page["Web page\n(may register document.modelContext tools)"]

    Harness -- "MCP over stdio" --> Guard
    Guard -- "native tool calls, pass-through" --> Adapter
    Guard -- "webmcp:* calls, gated" --> Trust
    Trust -- "evaluated call" --> Adapter
    Adapter -- "MCP over stdio\n(internal client)" --> Upstream
    Upstream --> Browser
    Browser --> Page

    style Trust fill:#2d5a2d,stroke:#4ade80,color:#fff

webmcp-guard is a proxy, not a fork: it runs @playwright/mcp as an internal MCP client via server composition (stdio), rather than vendoring its source. See Decision 1 in research/decision-log.md for why this replaced the original fork hypothesis.

  • packages/playwright (@webmcp-guard/playwright) — the adapter that spawns/speaks to @playwright/mcp, passes tool calls through, and runs the WebMCP detection probe via @playwright/mcp's own browser_evaluate tool. Kept Playwright-specific and adapter-agnostic-by-convention so a future non-Playwright backend could reuse the same shape.

  • packages/trust-layer (@webmcp-guard/trust-layer) — provenance store, schema-diffing, confirmation gating, namespacing, and the audit log. Deliberately has no Playwright-specific imports, since it's meant to generalize to other automation backends later.

  • packages/webmcp-guard (webmcp-guard) — the MCP server itself: connects to the adapter on startup, exposes every upstream Playwright tool unchanged plus any detected webmcp:* tools gated by the trust layer, and after every browser_navigate/tab-switch, runs the detection probe and logs/caches the result.

The trust gate, per webmcp:* call

flowchart TD
    Call["Agent calls webmcp:&lt;tool&gt;"] --> Live{"Tool actually present\non the CURRENTLY active page?"}
    Live -- No --> Deny1["Denied — stale tool\nfrom a previous origin\n(closes the cross-origin bug)"]
    Live -- Yes --> Verdict{"Provenance verdict"}
    Verdict -- "new-tool / unchanged" --> Gate
    Verdict -- "schema-changed" --> Warn["Confirmation prompt\nshows explicit warning"] --> Gate
    Gate{"Client declared\nMCP elicitation?"}
    Gate -- Yes --> Elicit["Structured confirmation\nprompt via elicitation"]
    Gate -- No --> Fallback["Relies on client's own\ngeneric ask/allow/deny gate"]
    Elicit --> Decision{"Confirmed?"}
    Decision -- "No / timeout / error" --> Deny2["Denied — fail closed,\nnever silent-allow"]
    Decision -- Yes --> Allow["Tool call forwarded\nto the page"]
    Fallback --> Allow
    Allow --> Audit["Recorded to audit log"]
    Deny1 --> Audit
    Deny2 --> Audit

Install / build (from source)

npm install
npm run build

Run

node packages/webmcp-guard/dist/cli.js --browser chrome

Or wire it into an MCP client config as shown above.

Testing

npm test   # builds all packages, runs the trust-layer's unit tests (18 tests)

Unit tests cover the trust layer in isolation (provenance/schema-diffing, confirmation gating, namespacing, audit log). The larger claim — that this actually works against the real WebMCP ecosystem — is backed by validation against all 168 live sites in the public webmcp.com directory plus a hand-built adversarial test case, documented in full in docs/testing-and-validation.md. That process is what found the cross-origin invocation bug shown in the diagram above — it was not caught by unit tests or the synthetic adversarial test alone, only by testing against real, unmodified production sites. Reproducible via the scripts in research/tools/.

Versions pinned

  • @playwright/mcp@0.0.79

  • @modelcontextprotocol/sdk@^1.30.0

Further reading

  • docs/trust-boundary-design.md — the full trust-boundary design document: why WebMCP tools are a different trust boundary, the provenance/schema-diff model, the confirmation-gating design, namespacing, the audit log, and what this project is explicitly not.

  • docs/testing-and-validation.md — the consolidated testing report: methodology, every bug found and fixed with evidence, the full 168-site directory crawl results, and honestly-stated limitations.

  • research/README.md — index of the primary-source research this project was built on (WebMCP spec state, MCP protocol details, prior art, upstream internals) and the raw validation data/tools.

  • research/decision-log.md — architecture decisions and the reasoning behind them (fork vs. proxy, detection property names, confirmation mechanism, provenance persistence, etc.).

License

Apache-2.0 — see LICENSE.

Related MCP Connectors

Related MCP Servers