Skip to main content
Glama
README.md
# webmcp-guard

A WebMCP-aware drop-in replacement for [`@playwright/mcp`](https://www.npmjs.com/package/@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](https://github.com/webmachinelearning/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`](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](https://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`](docs/testing-and-validation.md).

## Install / config

**Not yet published to npm.** For now, build from source (see
[Install / build (from source)](#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):

```json
{
  "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:

```json
{
  "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:

```json
{
  "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.

## 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`](docs/trust-boundary-design.md) for why
this two-track design exists rather than relying on elicitation alone.

## Architecture

```mermaid
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

```mermaid
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

```bash
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](https://webmcp.com) directory plus a hand-built adversarial
test case, documented in full in
[`docs/testing-and-validation.md`](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/`](research/tools/).

## Versions pinned

- `@playwright/mcp@0.0.79`
- `@modelcontextprotocol/sdk@^1.30.0`

## Further reading

- [`docs/trust-boundary-design.md`](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`](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`](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`](LICENSE).