使用说明
WebMCP Demo — oRPC Task Manager
Demo showing how a Next.js app with oRPC backend can expose its API routes as WebMCP tools — enabling AI agents to call them directly from the browser.
Includes a generic oRPC -> WebMCP bridge package (packages/orpc-webmcp) that auto-registers oRPC procedures as WebMCP tools.
What is WebMCP?
A browser-native JS API (document.modelContext) that lets web pages expose "tools" to AI agents.
Think: MCP but client-side, in the browser, with human-in-the-loop.
The specification is a Draft Community Group Report of the W3C Web Machine Learning Community Group, latest publication 28 July 2026. A Community Group Draft Report is not a W3C Standard and is not on the W3C Standards Track — it can and does change. This demo has been broken twice by it already.
API surface used here
// Registration is async and returns nothing. There is no handle.
await document.modelContext.registerTool(tool, { signal })
// Unregistration is aborting the signal you registered with.
controller.abort()
// Producer-side discovery and execution.
const tools = await document.modelContext.getTools()
await document.modelContext.executeTool(tools[0], '{"id":"1"}')
// Tool-list changes.
document.modelContext.addEventListener('toolchange', handler)
navigator.modelContext still resolves to the same object, but it is a deprecated
back-compat alias — the getter moved from Navigator to Document in the 27 May 2026
draft (webmachinelearning/webmcp#173,
PR #184) and Chrome deprecated it
in Chrome 150. Reading it logs a deprecation warning. Nothing in this repo touches it.
unregisterTool(), callTool(), listTools() and registerToolsChangedCallback() are
either removed from the spec or non-standard MCP-B extensions. This demo uses none of them.
Browser support
| Browser | Status |
|---|---|
| Chrome 150+ | Native document.modelContext, behind a flag (see below) |
| Edge 147 | Native |
| Chrome 149 | Origin trial, on the old navigator.modelContext location |
| Firefox, Safari | No shipping commitment |
Everywhere else, the app loads @mcp-b/webmcp-polyfill,
which installs document.modelContext itself. On Chrome 150+ the polyfill detects native
support and stands down; on Chrome 149 it mirrors the native navigator.modelContext onto
document.modelContext so page code only ever needs the one location.
Architecture
┌──────────────────────────────────────────────┐
│ Browser │
│ │
│ ┌──────────────┐ ┌───────────────────┐ │
│ │ AI Agent │◄──►│ document │ │
│ │ (or the Tool │ │ .modelContext │ │
│ │ Inspector) │ └────────┬──────────┘ │
│ └──────────────┘ │ │
│ execute() calls │
│ │ │
│ ┌─────────▼──────────┐ │
│ │ oRPC client │ │
│ │ (type-safe calls) │ │
│ └─────────┬──────────┘ │
│ │ │
└───────────────────────────────┼──────────────┘
│ HTTP
┌───────────────────────────────┼──────────────┐
│ Next.js Server │ │
│ ┌─────────▼──────────┐ │
│ │ oRPC router │ │
│ │ (procedures) │ │
│ └─────────┬──────────┘ │
│ │ │
│ ┌─────────▼──────────┐ │
│ │ In-memory store │ │
│ └────────────────────┘ │
└──────────────────────────────────────────────┘
Stack
| Layer | Tech |
|---|---|
| Monorepo | Turborepo + Bun |
| Frontend | Next.js 16, React 19, Tailwind 4, shadcn/ui |
| Backend API | oRPC 1.14 |
| Schema | Zod 4 |
| WebMCP runtime | @mcp-b/webmcp-polyfill 4 + @mcp-b/webmcp-types 4 |
| WebMCP bridge | packages/orpc-webmcp (included) |
Why the polyfill and not @mcp-b/global
Upstream ships three packages. @mcp-b/webmcp-types is types only, @mcp-b/webmcp-polyfill
is the runtime polyfill only, and @mcp-b/global adds the MCPB extras — an MCP bridge
transport, prompts and resources, testing helpers — on top of the polyfill.
This demo registers tools and nothing else, and the MCP-B browser extension the bridge
transport talks to is no longer available. So it takes the polyfill plus the types and skips
@mcp-b/global, which would otherwise pull in @mcp-b/transports and @mcp-b/webmcp-ts-sdk
and open a transport with no counterpart.
Getting Started
Prerequisites
- Bun 1.3+
- A Chromium-based browser
Install & Run
bun install
bun dev
App runs at http://localhost:3000. Tools register on load — the browser console prints
[WebMCP] Registered 6/6 tools from router.
Using WebMCP
Any browser (polyfill)
The polyfill installs document.modelContext and, alongside it,
navigator.modelContextTesting. From DevTools:
// List what the page exposes
await document.modelContext.getTools()
// Execute one. Arguments are a JSON *string*, not an object.
const tools = await document.modelContext.getTools()
await document.modelContext.executeTool(
tools.find((t) => t.name === 'tasks.create'),
JSON.stringify({ title: 'Buy groceries', description: 'Milk, eggs, bread' }),
)
Mutating tools call router.refresh() through the bridge's onMutate option, so the task
list on the page updates as the tool runs.
Chrome 150+ (native)
- Open
chrome://flags/#enable-webmcp-testing, set WebMCP for testing to Enabled, relaunch. - Run the app and open
http://localhost:3000. - The native API takes over and the polyfill stands down. The same
document.modelContext.getTools()/executeTool()calls work from DevTools.
Chrome also exposes navigator.modelContextTesting.listTools() and
.executeTool(name, argsJson) under this flag. Note the different signature: the testing API
executes by tool name, the producer API by the descriptor object from getTools().
Model Context Tool Inspector (visual)
A Chrome extension that lists the tools on a page and lets you fill in parameters and execute them without writing any console code.
Exposed Tools
The app registers these oRPC procedures as WebMCP tools:
| Tool | Type | Description |
|---|---|---|
tasks.list |
query | List all tasks, optional status filter |
tasks.get |
query | Get single task by ID |
tasks.create |
mutation | Create a new task |
tasks.update |
mutation | Update task title/description/status |
tasks.delete |
mutation | Delete a task |
tasks.search |
query | Search tasks by keyword |
exposeRouter()registers every procedure it finds, mutations included, and thereadOnlyHintannotation is a hint to the agent, not a gate on execution. That is fine here — the store is in-memory and resets when the server restarts — but it is not a pattern to copy onto a real backend. Opt-in exposure and a human-confirmation path are being designed as part of turning the bridge into a published package.
Project Structure
apps/web/ # Next.js app
app/
page.tsx # Task list UI
actions/tasks.ts # Server actions wrapping oRPC
rpc/[[...rest]]/route.ts # oRPC API route
lib/
orpc/ # oRPC router, schemas, client, store
components/
providers.tsx # Loads the polyfill, calls useWebMCP()
... # Task UI components
packages/orpc-webmcp/ # Generic oRPC-to-WebMCP bridge
src/
expose.ts # exposeRouter() — core bridge logic
react.ts # React hook (useWebMCP)
types.ts # Type definitions
How the bridge unregisters
registerTool() returns Promise<void> — no handle. One exposeRouter() call creates one
AbortController and passes its signal to every registration; unregister() aborts it, which
takes every tool in the batch back out at once.
exposeRouter() returns synchronously so useWebMCP's cleanup always has a controller to
abort, even if the component unmounts before the registrations resolve. Aborting mid-flight
removes tools that already landed and short-circuits the ones that have not. The returned
handle also carries a ready promise that settles with the per-tool outcome; it never
rejects, so ignoring it cannot produce an unhandled rejection.