PermDock
Standards

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.

Draft posture: build (pinned to the W3C Web Machine Learning CG draft as shipped in Chrome 150, document.modelContext; pin recorded on the WebMCP adapter, per watch list)

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 earlier navigator.modelContext entry point is deprecated in Chrome 150 in favour of document.modelContext.
  • A tools Permissions-Policy directive controls whether a document (and which embedded frames) may register tools at all.
  • Tool annotations readOnlyHint and untrustedContentHint tell 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.webmcp so tests can enumerate and call a page's tools deterministically.
  • @mcp-b/webmcp-polyfill provides 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 each instance action the snapshot holds a grant for; its condition is checked against the validated input at call time.
  • Fills name, description and inputSchema from the permission's action metadata and the resource's Standard JSON Schema.
  • Sets readOnlyHint: true for actions marked read-only in the action metadata (read, list) and false for mutations.
  • Sets untrustedContentHint when the resource carries user-generated fields, or when configured explicitly, so the agent does not treat tool output as instructions.
  • Passes the caller's AbortSignal through and additionally aborts and re-registers when the snapshot changes (usePermDock().status moving to stale and back to ready), so revoked permissions disappear from the tool list without a reload.
  • Registers an instance action once, taking the row as validated arguments, never one tool per visible row. The client check is a filter; the server route the handler calls decides on the real row.
  • Skips any permission whose status is pending or server-only, so a tool is registered only when the snapshot can answer for it.
  • Returns approval-required as a structured tool result carrying the token; the browser has no elicitation channel, so the page renders its own approval UI.
  • Does nothing when document.modelContext is undefined and no polyfill is present; permdock doctor reports the missing Permissions-Policy header if the page is served without tools.

Mapping table

WebMCP conceptPermDock concept
document.modelContext.registerToolregisterTools(document.modelContext, permissions.<resource>, options)
navigator.modelContext (deprecated)Not used; the polyfill's document.modelContext is targeted
Tool name and descriptionPermission key and action metadata (title, description)
Tool inputSchemaResource's Standard JSON Schema
readOnlyHintAction metadata readOnly, defaulted for read / list
untrustedContentHintAdapter option or resource metadata
AbortSignal unregistrationCaller's signal plus automatic abort on snapshot change
Permissions-Policy toolsDeployment requirement checked by permdock doctor
Which tools exist for this usercan(...) against the client snapshot
Whether a tool call succeedsServer-side protect on the route the handler calls
Puppeteer page.webmcpUsed in the webmcp example's tests to assert the registered tool list per role
@mcp-b/webmcp-polyfillSupported; the adapter only requires the document.modelContext shape

Sources

Last updated on

On this page