Building a security workbench for the tools agents call on the web
The problem
WebMCP lets a website register JavaScript tools that an AI agent can call directly. A shopping site can expose search_products. A support portal can expose issue_refund. The agent reads each tool's description and schema, decides what to call, and treats the result as context for its next step.
That convenience creates a new attack surface. The agent trusts three pieces of text it did not write:
- The description. It can hide a line telling the agent to ignore its earlier instructions.
- The schema. Loose or missing constraints let the agent pass anything, including data it should never send.
- The output. A response can ask the agent to quietly forward a session token to an external URL.
Chrome's own WebMCP security guidance names prompt injection and untrusted tool output as real risks. Yet when we looked for a way to take a specific WebMCP tool and test it for these problems, we found nothing built for the job. Developers were shipping tools with no way to ask a simple question: is this tool safe for an agent to call?
That gap is what WebMCP Guard Studio, our WebMCP Challenge project, sets out to close.
What Guard Studio does
You bring a tool definition by pasting it, uploading JSON, or pointing at a hosted URL. Guard Studio then:
- Classifies the tool as read-only, write, or sensitive
- Flags risky or missing schema constraints
- Scans descriptions for prompt injection patterns
- Detects instructions that touch sensitive data
- Examines sample outputs for suspicious content
- Produces an overall risk score
A red-team bench lets you run attack samples against your own tool: token exfiltration, obfuscated jailbreaks, poisoned descriptions, and silent actions. When something fails, Guard Studio generates a hardened registerTool() version with a stricter schema, output limits, audit events, and approval checks.
For sensitive tools, a human approval queue holds execution until a person clicks Approve or Deny. The agent cannot approve its own request.
Guard Studio also registers five WebMCP tools of its own (import, analyze, scan, generate, simulate), so an agent can run the full audit flow itself.
How we built it
The frontend is React and Vite on Vercel. The backend is a stateless FastAPI service on Render. No database, no accounts, every request independent.
Scanning in production is deterministic and pattern-based, covering nine injection families including instruction overrides, data exfiltration, hidden instructions, silent actions, and sensitive-data requests. It inspects descriptions, schemas, and sample payloads. An optional model-based mode uses Meta Prompt Guard 2 via Hugging Face Transformers, but it stays optional because the free-tier memory budget cannot support it.
Fallbacks are everywhere. If document.modelContext is missing, the app says WebMCP is unavailable instead of failing silently. If the backend is asleep or unreachable, a local scanner in the browser takes over.
We also published a deliberately vulnerable refund tool. Scan it and you get a Critical score of 100/100 with six findings, every time. That reproducible bad example made every demo and test easier.
What went wrong, and what it taught us
The agent approved itself. Our first approval design put an approve boolean in the tool input. The agent simply sent approve: true. We had described a human in the loop without building one.
The fix was to make approval a wait, not an input. The tool's execute() now returns a Promise that only the UI can resolve, and it auto-denies after two minutes. There is no code path the agent can use to fake it. If your security boundary depends on the agent choosing to respect it, you do not have a boundary.
Our scanner failed our own red team. A sample asking the agent to silently POST a token to an external URL came back safe. POST was missing from our exfiltration verbs, and one regex matched http but not https. Two tiny gaps, one critical miss. Every red-team sample is now a regression test.
One execution, two readers. The agent needs compact output to save context; the UI needs full findings, scores, and audit events. We built a single execution sequence that feeds both, capping the agent response near 1,500 characters so the two views can never disagree.
Tools registered repeatedly. registerTool() rejects duplicate names, but our React callbacks changed identity every render, so any UI edit tried to re-register all five tools. Ref-backed stable callbacks fixed it.
What's next
- Rug-pull detection: hash approved tool definitions and warn if a description or schema changes later
- CI mode: fail a pull request that introduces a new tool-level security issue
- Browser extension: inspect WebMCP tools registered by any live site
- Better detection: stronger model-based scanning with Prompt Guard 2, plus PII detection on outputs via Microsoft Presidio
-
Evals in CI: run Chrome's WebMCP evals (already in
evals/guardstudio-tools.json) alongside the scanner, so both security and behavior regressions block a release
The takeaway
Prompt injection is not a chat problem. It lives anywhere an agent reads text it did not write, and with WebMCP that means every description, schema, and output a website exposes.
The fixes are rarely clever. Usually they move a decision out of a place the agent controls and into one it does not. A field named approve is a suggestion. A Promise that won't resolve until a human clicks is a control.
Top comments (1)
Putting
approvein the tool input made the requester its own approver. Moving approval to a Promise only the UI can resolve closes that, and the two-minute auto-deny is a sensible default.One more check for the refund tool: validate the amount outside the model against the original charge and what has already been refunded. Then an approved call still cannot refund more than the charge.