Agent skill for implementing WebMCP on websites — expose app features to browser agents via document.modelContext.registerTool(). Install: npx skills add Blackie360/webmcp-skill -g
SaferSkills independently audited webmcp-skill (Agent Skill) and scored it 87/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 1 high-severity and 1 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 2 flagged
The text {match} is the classic direct prompt-injection phrasing. Placed in a skill body that the agent reads as trusted instructions, it tries to make the agent abandon its prior rules and follow whatever comes next — a full system-prompt override.
ignore/disregard/forget … previous instructions sentence.The text {match} tells the agent to skip the normal "ask the user first" gate. Used adversarially it removes the human-in-the-loop check before destructive or sensitive actions, turning a normally-gated agent into a fire-and-forget executor.
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Guide for exposing website features to browser agents via the WebMCP API.
| Server MCP | WebMCP | |
|---|---|---|
| Runtime | Node/Python server | Browser tab (JavaScript) |
| API | MCP SDK server.tool() | document.modelContext.registerTool() |
| Discovery | Client connects to server URL | Agent visits page; tools registered in-page |
| Auth | OAuth, API keys | Existing web session/cookies |
For server-side MCP, use an mcp-builder skill instead. This skill is browser-only.
modelContext, or agent-ready web toolsBefore registering tools, verify:
localhostdocument.domain is set or Origin-Agent-Cluster: ?0tools feature allowed (default self; cross-origin iframes need allow="tools")execute runs in an open browser context; no headless callsSimple HTML form submit?
└─ Yes → Consider declarative HTML annotations (spec still evolving; see examples/form-tool.html)
└─ No → Use imperative registerTool() (recommended for SPAs and complex logic)Prefer the imperative API until declarative form synthesis is stable in the spec.
Copy this checklist and track progress:
Task Progress:
- [ ] Step 1: Audit UI actions worth exposing
- [ ] Step 2: Map each action to one tool (reuse existing app functions)
- [ ] Step 3: Define name, title, description, inputSchema
- [ ] Step 4: Implement execute with structured return values
- [ ] Step 5: Set annotations (readOnlyHint, untrustedContentHint)
- [ ] Step 6: Register on mount; unregister on unmount via AbortSignal
- [ ] Step 7: Test with Chrome flag + Model Context Tool InspectorList user-facing actions agents should perform: search, filter, add to cart, submit form, navigate, etc. Skip actions that require opaque visual interaction unless wrapped in a dedicated tool.
Call the same functions your UI buttons use. Do not duplicate business logic in execute.
Name rules (from spec):
_, -, . onlyDescription: Plain language stating what the tool does and what side effects it has. Agents rely on this to choose tools and request user consent.
inputSchema: JSON Schema object. Keep parameters minimal — only what the action needs.
Return structured JSON agents can parse. Prefer { error: "..." } over thrown exceptions for expected failures.
execute: async ({ sku, quantity = 1 }) => {
if (!sku) return { error: 'sku is required' }
const result = await cartService.add(sku, quantity)
return { success: true, cartItemCount: result.count }
}annotations: {
readOnlyHint: true, // tool does not mutate state
untrustedContentHint: true // output includes user-generated content
}See references/security.md for when to set each.
const controller = new AbortController()
await document.modelContext.registerTool(
{
name: 'add_to_cart',
title: 'Add to cart',
description: 'Adds a product to the shopping cart by SKU. Does not checkout.',
inputSchema: {
type: 'object',
properties: {
sku: { type: 'string', description: 'Product SKU' },
quantity: { type: 'integer', minimum: 1, default: 1 }
},
required: ['sku']
},
annotations: { readOnlyHint: false },
execute: async ({ sku, quantity = 1 }) => {
const result = await cartService.add(sku, quantity)
return { success: true, cartItemCount: result.count }
}
},
{ signal: controller.signal }
)
// On page unload or component unmount:
controller.abort()If tools change at runtime, listen for registration updates:
document.modelContext.ontoolchange = () => {
// Refresh UI or internal tool list
}search_products and add_to_cart are separate toolsexecute triggers purchaseexecute before purchases, deletes, or sendsuntrustedContentHint: trueRegister after app initialization when DOM and state are ready.
useEffect(() => {
const controller = new AbortController()
void document.modelContext.registerTool({ /* ... */ }, { signal: controller.signal })
return () => controller.abort()
}, [dependencies])Register after hydration on SSR apps. Pass stable dependencies only.
Chrome documents experimental WebMCP support in Angular. Follow framework-specific guides when available; fall back to imperative registerTool in a service ngOnInit.
Parent must allow tools in the iframe:
<iframe src="https://embed.example.com" allow="tools"></iframe>Use exposedTo in registerTool options to expose tools to specific origins. See references/api-reference.md.
chrome://flags/#enable-webmcp-testing → Enabled → relaunchFull setup: references/browser-setup.md
| Error | Cause | Fix |
|---|---|---|
SecurityError | Not origin-isolated | Remove document.domain; check Origin-Agent-Cluster header |
NotAllowedError | tools policy blocked | Top-level page or add allow="tools" on iframe |
InvalidStateError | Duplicate tool name | Unregister first or use unique names |
| API undefined | Flag off or unsupported browser | Enable flag or join origin trial |
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.