webmcp-guard
by SARTHAK2511
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:<tool>"] --> 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).
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues