WebMCP
permdock/webmcp registers browser-exposed WebMCP tools only for actions the current snapshot allows, with hints from action metadata and automatic unregistration when permissions change.
Status: planned Phase: 2
permdock/webmcp is the client-side twin of permdock/mcp. A page calls registerTools with a permission group and the snapshot-backed PermDock from usePermDock(); the adapter registers one WebMCP tool per allowed action on document.modelContext, sets tool hints from action metadata, and unregisters tools whenever the snapshot changes.
Purpose
WebMCP lets a web page expose tools to in-browser agents through document.modelContext.registerTool() (Chrome docs; navigator.modelContext is deprecated in Chrome 150). Pages are gated by the tools Permissions-Policy, and each tool can declare readOnlyHint and untrustedContentHint. The question a page must answer before registering anything is "which tools may this user trigger?", and that is exactly what the client snapshot holds. Registering tools the user cannot use invites a denied call; registering nothing wastes the capability. permdock/webmcp registers the allowed subset and keeps it in sync.
API
import { registerTools } from 'permdock/webmcp'
import { usePermDock } from 'permdock/react'
function PostTools() {
const permdock = usePermDock()
useEffect(() => {
const controller = new AbortController()
registerTools(document.modelContext, permissions.post, {
permdock,
signal: controller.signal,
handlers: {
read: async ({ id }) => api.posts.get(id),
update: async ({ id, ...patch }) => api.posts.update(id, patch),
create: async (input) => api.posts.create(input),
},
})
return () => controller.abort()
}, [permdock])
return null
}registerTools(modelContext, permissionGroup, options)walks the group (permissions.postis a resource node; a nested group such aspermissions.billingis also accepted) and registers a tool per action for whichpermdock.can(permission)(collection) or a grant exists (instance actions) in the snapshot.- Tool names are derived from the permission key (
post.updatebecomespost_update);titleanddescriptioncome from action metadata when actions are declared as a record. readOnlyHintistruefor actions tagged read-only in metadata (default forreadandlist);untrustedContentHintistruefor tools that return user-generated content, also from metadata.- The resource schema becomes the tool
inputSchema(via Standard JSON Schema) for instance actions; collection actions use the schema declared in metadata or accept no input. signalunregisters everything on abort; the adapter also aborts and re-registers internally when the snapshot changes (role change, tenant switch throughuseTenant().switchTo,invalidate, SSF event).tenant(optional) registers tools againstpermdock.tenant(id)instead of the active tenant, for a page that shows another organisation's workspace. Tool descriptions include the tenant'smeta.titlefrom the snapshot when one is present so a browser agent can tell two workspaces apart; the tenant id itself is never a tool argument the agent may set (tenancy). A snapshot withsimulated: trueregisters no tools at all.
Request lifecycle
- Mount: the component reads the snapshot-backed
PermDockand callsregisterTools. - Registration: for each action in the group, the adapter evaluates the snapshot and registers only allowed tools, carrying hints and JSON Schema input.
- Agent call: the browser agent invokes a tool. The adapter validates the input against the resource schema (boundary mode), re-checks
permdock.decide(permission, input)against the current snapshot, then calls the app handler. - Server enforcement: the handler calls the app's API, where a server adapter (
permdock/next,permdock/hono) makes the authoritative decision. The client check is a filter for good UX, never the security boundary. - Snapshot change:
usePermDock()re-renders with a new snapshot; the adapter aborts the previous registrations and repeats step 2. - Unmount or navigation: the
AbortSignalfires and all tools are unregistered.
What it validates
- Tool input against the resource's Standard Schema before the handler runs (
validate: 'boundary'): agent-supplied arguments are untrusted. - The
toolsPermissions-Policy:registerToolsis a no-op with a development warning whendocument.modelContextis absent (policy denied, unsupported browser, or the polyfill not loaded). - Snapshot freshness: tools are registered from the snapshot's
status; aserver-onlypermission (closure grant) is not registered until the decision endpoint has answered. - Nothing about the user's identity: the snapshot is issued by the server and is the only source of grants.
- Tenant arguments: if the resource schema carries the
scopes.tenant.keyfield (orgId), the adapter fills it from the active tenant and rejects a differing value supplied by the agent before the handler runs (tenant-mismatch), so a browser agent cannot address another tenant's rows through a tool it holds legitimately.
How denials surface
- Not registered: the default. A tool the user cannot use never appears in the page's tool list, so an agent cannot attempt it.
- Denied at call time (snapshot changed between listing and calling): the handler is not invoked; the adapter returns a tool error result whose text carries the Decision reason and
alternatives, mirroring the MCP refusal shape. approval-required: the adapter does not run the handler; it returns a result asking the agent to obtain user confirmation in the page UI. The app may provide anonApprovalRequiredcallback to open its own confirmation dialog and resolve the call with the approval token.- Server denials: the API answers RFC 9457
application/problem+json; the adapter forwards the problemtitleanddetailto the agent as a tool error.
Example app
apps/examples/webmcp: a Vite React app with post tools, a role switcher that changes the snapshot, the tools Permissions-Policy set in the dev server headers, @mcp-b/webmcp-polyfill for browsers without native support, and Puppeteer tests using page.webmcp to assert the registered tool list per role and a denied call after a role downgrade.
Related standards
- WebMCP:
document.modelContext, Permissions-Policytools, hints,AbortSignal, Puppeteerpage.webmcp, polyfill. - Standard Schema: input schemas via Standard JSON Schema.
- Snapshots: what the client knows and when it changes.
- MCP adapter: the server-side counterpart.
Open questions
- Naming:
post_updateversuspost.updateas the tool name, depending on WebMCP name constraints. - Whether instance actions with
whereconditions should register with a description that states the condition ("only your own posts") or register only when the condition can be evaluated client-side. - How approvals should be presented in a page with no host UI for confirmation; the polyfill and native implementations may differ.
- Whether
registerToolsshould accept multiple groups in one call to share a single abort scope. - Whether a multi-tenant page should register one tool set per tenant with a suffix (
post_update__acme) or, as documented, one set for the active tenant that re-registers on switch.
OpenAI Agents SDK
permdock/openai turns PermDock decisions into OpenAI Agents SDK needsApproval predicates, guards tool lists per caller, resolves interruptions against a pluggable ApprovalStore, and binds the replay-safe token to the serialised RunState.
A2A
permdock/a2a emits A2A Agent Cards whose skills carry security requirements derived from permissions and filters the authenticated extended card by the caller's grants.