WebMCP
How permdock/webmcp registers browser-exposed tools through document.modelContext only when the client snapshot allows them, and how WebMCP hints and Permissions-Policy fit in.
Status: planned
Phase: 2
Draft posture: build (pinned to the W3C Web Machine Learning CG draft as shipped in Chrome 150, document.modelContext; the pinned draft date is recorded on the WebMCP adapter page when Phase 2 starts and bumped with fixtures and a changeset, per ADR 0025)
Adapter phases: permdock/webmcp 2.
What it is
WebMCP is a browser API, incubated in the W3C WebML Community Group and documented by Chrome, that lets a web page expose tools to agents running in or alongside the browser, in the same shape as MCP tools:
document.modelContext.registerTool(...)registers a tool with a name, description, input schema and handler. The earliernavigator.modelContextentry point is deprecated in Chrome 150 in favour ofdocument.modelContext.- A
toolsPermissions-Policy directive controls whether a document (and which embedded frames) may register tools at all. - Tool annotations
readOnlyHintanduntrustedContentHinttell the agent whether a tool mutates state and whether its output may contain untrusted content that should not be treated as instructions. - Registration accepts an
AbortSignal; aborting it unregisters the tool, which is how a page removes tools when its state changes. - Puppeteer exposes
page.webmcpso tests can enumerate and call a page's tools deterministically. @mcp-b/webmcp-polyfillprovides the API in browsers that do not ship it yet.
Why it matters for PermDock
A page that registers a "delete post" tool for every visitor is handing agents a capability the current user may not have. The client already holds a PermDock snapshot for exactly this reason: it knows which permissions the user has, including ownership conditions, without a round trip. permdock/webmcp is the same idea as permission on registerTool in permdock/mcp, running on the client: register only the tools the snapshot allows, describe them from the permission metadata, and unregister them when the snapshot changes. See the webmcp adapter.
Two boundaries stay server-side. The snapshot decides what to register; the server still decides what to execute, because a page-level tool handler calls the same API route that protect guards. And tool arguments coming from an agent are untrusted input, validated at the boundary like MCP tool arguments.
How PermDock uses it
import { usePermDock } from 'permdock/react'
import { registerTools } from 'permdock/webmcp'
function PostTools() {
const permdock = usePermDock()
useEffect(() => {
const controller = new AbortController()
registerTools(document.modelContext, permissions.post, {
permdock,
signal: controller.signal,
})
return () => controller.abort()
}, [permdock])
return null
}What registerTools does:
- Iterates
listPermissions(permissions.post)and registers one tool per collection action the snapshot grants (can(permissions.post.create)), plus instance actions when adataresolver is supplied so ownership conditions can be checked per call. - Fills
name,descriptionandinputSchemafrom the permission's action metadata and the resource's Standard JSON Schema. - Sets
readOnlyHintfor actions marked read-only in the action metadata (read,list) and leaves it unset for mutations. - Sets
untrustedContentHintwhen the resource carries user-generated fields, or when configured explicitly, so the agent does not treat tool output as instructions. - Passes the caller's
AbortSignalthrough and additionally aborts and re-registers when the snapshot changes (usePermDock().statusmoving tostaleand back toready), so revoked permissions disappear from the tool list without a reload. - Does nothing when
document.modelContextis undefined and no polyfill is present;permdock doctorreports the missing Permissions-Policy header if the page is served withouttools.
Mapping table
| WebMCP concept | PermDock concept |
|---|---|
document.modelContext.registerTool | registerTools(document.modelContext, permissions.<resource>, options) |
navigator.modelContext (deprecated) | Not used; the polyfill's document.modelContext is targeted |
| Tool name and description | Permission key and action metadata (title, description) |
Tool inputSchema | Resource's Standard JSON Schema |
readOnlyHint | Action metadata readOnly, defaulted for read / list |
untrustedContentHint | Adapter option or resource metadata |
AbortSignal unregistration | Caller's signal plus automatic abort on snapshot change |
Permissions-Policy tools | Deployment requirement checked by permdock doctor |
| Which tools exist for this user | can(...) against the client snapshot |
| Whether a tool call succeeds | Server-side protect on the route the handler calls |
Puppeteer page.webmcp | Used in the webmcp example's tests to assert the registered tool list per role |
@mcp-b/webmcp-polyfill | Supported; the adapter only requires the document.modelContext shape |
Sources
- Chrome WebMCP documentation.
- WebMCP in Chrome 150:
navigator.modelContextdeprecation. - Product plan, "Standards and agent runtimes (Sept 2026)" and the
permdock/webmcpAPI sketch.
Open questions
- Whether instance-level tools should be registered per visible resource (one tool per post) or once with an
idargument checked at call time; the latter is smaller but the snapshot can only answer it when adataresolver is supplied. - How
approval-requiredsurfaces inside a page: the browser has no elicitation channel, so the handler likely returns a structured refusal with the token and the page renders its own approval UI. - Whether the adapter should refuse to register mutating tools when
usePermDock().statusispendingorserver-only, or register them and rely on the server check.
Shared Signals and CAEP
How the OpenID Shared Signals Framework 1.0 and CAEP 1.0 let permdock/ssf invalidate snapshots the moment an identity provider revokes a session instead of waiting for a TTL.
A2A
How permdock/a2a emits Agent Card security schemes and requirements per skill and filters the authenticated extended card by the caller's permissions.