# WebMCP: Everything You Need to Know
WebMCP dropped on February 10, 2026. I heard the announcement, opened Chrome Canary, enabled the flag, and typed this in DevTools:
```js
console.log(navigator.modelContext);
// ModelContext {}[[Prototype]]: ModelContext
```
It was real. So I spent the next week going deep — building with it, reading the W3C spec, comparing it to Anthropic's MCP. This is everything I found.
## The Problem WebMCP Solves
Before I explain what WebMCP is, let me explain why it needed to exist.
Right now, when an AI agent wants to interact with a website — book a flight, fill a form, submit an order — it works like this:
```
1. Take a screenshot of the page
2. Send screenshot to a vision model
3. Model guesses where to click
4. Simulate the click
5. Take another screenshot
6. Repeat 10-20 times until done
```
This is called browser automation and it's how tools like Playwright, browser-use, and most agentic AI work today. It's slow, expensive (every screenshot burns tokens), and incredibly fragile — move a button 5 pixels and the agent breaks.
WebMCP is a completely different approach. Instead of the agent _guessing_ how to interact with your site by looking at screenshots, your website tells the agent exactly what it can do — through structured, callable JavaScript functions.
## What WebMCP Actually Is
WebMCP (Web Model Context Protocol) is a new browser-native JavaScript API — a W3C Draft Community Group specification published February 12, 2026, authored by engineers from Google (Khushal Sagar, Dominic Farolino) and Microsoft (Brandon Walderman).
The entire API surface lives on one property:
```js
navigator.modelContext;
```
That's the entry point to everything. When I ran this in Chrome Canary DevTools:
```js
console.log(Object.getOwnPropertyNames(ModelContext.prototype));
// ['clearContext', 'provideContext', 'registerTool', 'unregisterTool', 'constructor']
```
Five methods. That's literally the whole API. Four of them are the ones you'll use as a developer.
The spec's one-line abstract says it best:
> "The WebMCP API enables web applications to provide JavaScript-based tools to AI agents."
## The W3C Spec — What It Actually Says
I read the full spec at webmachinelearning.github.io/webmcp so you don't have to. Here's the complete IDL — the canonical definition of every interface:
```webidl
partial interface Navigator {
[SecureContext, SameObject] readonly attribute ModelContext modelContext;
};
interface ModelContext {
undefined provideContext(optional ModelContextOptions options = {});
undefined clearContext();
undefined registerTool(ModelContextTool tool);
undefined unregisterTool(DOMString name);
};
dictionary ModelContextTool {
required DOMString name;
required DOMString description;
object inputSchema;
required ToolExecuteCallback execute;
ToolAnnotations annotations;
};
dictionary ToolAnnotations {
boolean readOnlyHint;
};
callback ToolExecuteCallback = Promise<any> (object input, ModelContextClient client);
interface ModelContextClient {
Promise<any> requestUserInteraction(UserInteractionCallback callback);
};
```
A few things worth noting from the spec directly:
`[SecureContext]` — WebMCP only works on HTTPS. Plain HTTP sites won't have navigator.modelContext at all. This is intentional security design.
`[SameObject]` — navigator.modelContext always returns the same instance. It's not creating a new object each access.
The spec names Claude and ChatGPT explicitly — under "AI Platform" terminology, the spec lists "OpenAI's ChatGPT, Anthropic's Claude, or Google's Gemini" as intended consumers. That's unusual for a W3C document and tells you this is built specifically for the current generation of AI.
Many sections say "TODO" — the Security section, Accessibility section, and all the implementation step details are currently blank. This is a living document being actively written. The interface is defined; the internals are still being specified.
## The Core API — With Real Examples
### registerTool() — The Main Event
This is what you'll use 90% of the time. You register a named function with a description and JSON Schema. The agent sees this and knows it can call it.
```js
navigator.modelContext.registerTool({
name: "get_course_details",
description:
"Get details about a specific course including fees, duration, and available seats",
inputSchema: {
type: "object",
properties: {
courseId: {
type: "string",
description: "The course ID, e.g. 'web-dev' or 'data-science'",
},
},
required: ["courseId"],
},
annotations: {
readOnlyHint: true, // this tool only reads, never writes
},
execute: async ({ courseId }) => {
const course = await fetchCourse(courseId);
return {
content: [
{
type: "text",
text: JSON.stringify(course),
},
],
};
},
});
```
The spec says registerTool throws an error if:
- A tool with the same name is already registered
- The inputSchema is not valid JSON Schema
So in React, you need cleanup in your useEffect:
```js
useEffect(() => {
navigator.modelContext.registerTool({ name: "my_tool", ... });
return () => {
// unregisterTool takes the NAME STRING, not the tool object
navigator.modelContext.unregisterTool("my_tool");
};
}, []);
```
### provideContext() — Register Everything at Once
When you want to declare your full set of tools in one shot, replacing whatever was there before:
```js
navigator.modelContext.provideContext({
tools: [
{
name: "list_courses",
description: "Get all available courses",
inputSchema: { type: "object", properties: {} },
execute: async () => { /* ... */ },
},
{
name: "enroll_student",
description: "Enroll a student in a course",
inputSchema: { /* ... */ },
execute: async (params) => { /* ... */ },
},
],
});
```
The difference from registerTool: provideContext clears all existing tools first then registers the new list. Use it when you want a clean slate. Use registerTool when you're adding tools incrementally (like in individual React components).
### clearContext() — Remove Everything
```js
navigator.modelContext.clearContext();
// All tools are now gone
```
Useful when the user navigates to a different section of your app where the previous tools no longer make sense.
### unregisterTool(name) — Remove One Tool
```js
navigator.modelContext.unregisterTool("enroll_student");
```
Takes the name string. This is what you call in React's useEffect cleanup.
### requestUserInteraction() — The Hidden Gem
This is the most underreported feature of WebMCP and the one that makes it fundamentally different from headless automation.
The execute callback receives two arguments: (input, client). That client is a ModelContextClient — and it has one method: requestUserInteraction().
It lets your tool pause mid-execution and ask the human to confirm before continuing:
```js
navigator.modelContext.registerTool({
name: "delete_student",
description: "Permanently delete a student record",
inputSchema: {
type: "object",
properties: {
studentId: { type: "string" },
},
required: ["studentId"],
},
execute: async ({ studentId }, client) => {
// Pause and ask the human — agent cannot bypass this
const confirmed = await client.requestUserInteraction(async () => {
return confirm(
Permanently delete student ${studentId}? This cannot be undone.
);
});
if (!confirmed) {
return {
content: [{ type: "text", text: "Deletion cancelled by user." }],
};
}
await deleteStudent(studentId);
return {
content: [{ type: "text", text: Student ${studentId} deleted. }],
};
},
});
```
The agent calls the tool. Your execute runs. At the requestUserInteraction line, execution pauses and a native browser dialog appears. The user clicks OK or Cancel. Only then does the agent get a result. The agent has zero ability to bypass this — it's baked into the protocol.
This is how WebMCP implements "human-in-the-loop" at the API level, not as an afterthought.
## The readOnlyHint Annotation
The spec defines one annotation today: readOnlyHint. When true, it signals to the agent that calling this tool has no side effects — it only reads data.
```js
// Read-only tool — agent can call this freely
{
name: "get_attendance_report",
description: "Get attendance report for a batch",
inputSchema: { /* ... */ },
annotations: { readOnlyHint: true }, // ← safe to call anytime
execute: async ({ batchId }) => { /* returns data */ }
}
// Write tool — no readOnlyHint, agent should be careful
{
name: "mark_attendance",
description: "Mark attendance for students in a batch",
inputSchema: { /* ... */ },
// no annotations — agent knows this modifies state
execute: async (params, client) => {
const confirmed = await client.requestUserInteraction(/* confirm dialog */);
/* ... */
}
}
```
This helps agents make smarter decisions — they know they can call read tools liberally but should be cautious (or ask for confirmation) with write tools.
## Building a Real Example — Student Enrollment in Next.js
I built this for my project Skillora (an education management SaaS). Here's how I structured WebMCP tools in a Next.js app with a reusable hook.
### The Hook
```typescript
// hooks/useRegisterMCP.ts
"use client";
import { useEffect, useRef } from "react";
// Extend Navigator type
declare global {
interface Navigator {
modelContext?: {
registerTool: (tool: MCPTool) => void;
unregisterTool: (name: string) => void;
provideContext: (options: { tools: MCPTool[] }) => void;
clearContext: () => void;
};
}
}
interface MCPTool {
name: string;
description: string;
inputSchema: {
type: "object";
properties: Record<
string,
{
type: string;
description?: string;
enum?: string[];
}
>;
required?: string[];
};
annotations?: { readOnlyHint?: boolean };
execute: (input: Record<string, unknown>, client?: unknown) => unknown;
}
export function useRegisterMCP<T = Record<string, unknown>>(options: {
name: string;
description: string;
inputSchema: MCPTool["inputSchema"];
annotations?: MCPTool["annotations"];
execute: (args: T, client?: unknown) => unknown;
}) {
// Keep execute ref fresh — avoids re-registering on every render
const executeRef = useRef(options.execute);
executeRef.current = options.execute;
useEffect(() => {
if (!navigator.modelContext) {
console.warn(
"[WebMCP] navigator.modelContext not found. Use Chrome Canary with the flag enabled.",
);
return;
}
try {
navigator.modelContext.registerTool({
name: options.name,
description: options.description,
inputSchema: options.inputSchema,
annotations: options.annotations,
execute: (input, client) => executeRef.current(input as T, client),
});
console.log([WebMCP] ✅ Registered: ${options.name});
} catch (err) {
console.error([WebMCP] Failed to register "${options.name}":, err);
}
return () => {
try {
navigator.modelContext?.unregisterTool(options.name);
} catch (_) {
/* ignore cleanup errors */
}
};
}, [options.name]); // only re-register if the tool NAME changes
}
```
### Using It in a Component
```typescript
// app/enrollment/page.tsx
"use client";
import { useState } from "react";
import { useRegisterMCP } from "@/hooks/useRegisterMCP";
const COURSES = [
{ id: "web-dev", name: "Web Development", fee: 25000 },
{ id: "data-science", name: "Data Science", fee: 35000 },
{ id: "mobile-dev", name: "Mobile Dev", fee: 28000 },
];
export default function EnrollmentPage() {
const [form, setForm] = useState({ name: "", email: "", courseId: "" });
// Tool 1: Agent reads available courses
useRegisterMCP({
name: "list_courses",
description: "Get all available courses with IDs, names, and fees. Call this first before fill_enrollment_form.",
inputSchema: { type: "object", properties: {} },
annotations: { readOnlyHint: true },
execute: () => ({
content: [{ type: "text", text: JSON.stringify(COURSES) }]
})
});
// Tool 2: Agent fills form fields
useRegisterMCP({
name: "fill_enrollment_form",
description: "Fill the enrollment form with student details. Call list_courses first to get valid courseId values.",
inputSchema: {
type: "object",
properties: {
name: { type: "string", description: "Student full name" },
email: { type: "string", description: "Student email" },
courseId: { type: "string", description: "Course ID", enum: COURSES.map(c => c.id) },
},
required: ["name", "email", "courseId"]
},
execute: (params) => {
// Directly updates React state — form fields fill visibly in real time
setForm(prev => ({ ...prev, ...params }));
return {
content: [{ type: "text", text: "Form filled. Call submit_enrollment to complete." }]
};
}
});
// Tool 3: Agent submits — with human confirmation
useRegisterMCP({
name: "submit_enrollment",
description: "Validate and submit the enrollment. Returns errors if fields are missing.",
inputSchema: { type: "object", properties: {} },
execute: async (_input, client) => {
if (!form.name || !form.email || !form.courseId) {
return { content: [{ type: "text", text: "Error: name, email, and courseId are required." }] };
}
// Human must confirm before submission
const confirmed = await (client as { requestUserInteraction: (cb: () => Promise<boolean>) => Promise<boolean> })
.requestUserInteraction(async () => confirm(Enroll ${form.name}?));
if (!confirmed) {
return { content: [{ type: "text", text: "Cancelled by user." }] };
}
const result = await fetch("/api/enroll", {
method: "POST",
credentials: "same-origin", // browser session handles auth automatically
body: JSON.stringify(form),
});
const data = await result.json();
return { content: [{ type: "text", text: Enrolled! Student ID: ${data.studentId} }] };
}
});
return (
<form>
<input value={form.name} onChange={e => setForm(p => ({...p, name: e.target.value}))} placeholder="Name" />
<input value={form.email} onChange={e => setForm(p => ({...p, email: e.target.value}))} placeholder="Email" />
<select value={form.courseId} onChange={e => setForm(p => ({...p, courseId: e.target.value}))}>
{COURSES.map(c => <option key={c.id} value={c.id}>{c.name}</option>)}
</select>
<button type="submit">Enroll</button>
</form>
);
}
```
When an AI agent visits this page, it can now do the entire enrollment flow in three structured calls — no screenshots, no DOM manipulation, no guessing.
## WebMCP vs Anthropic's MCP — The Real Difference
This is where most people get confused. Despite the similar name, these are completely different protocols.
```
MCP Server (Anthropic) WebMCP (W3C)
───────────────────── ────────────
Runs on your backend Runs in the browser tab
No browser required Browser + user required
JSON-RPC over HTTP/SSE navigator.modelContext API
You build OAuth 2.1 auth Browser session auth (free)
Agent acts alone, headless Agent works alongside user
Works today, production-ready Chrome Canary only (Feb 2026)
Spec by: Anthropic Spec by: Google + Microsoft + W3C
```
The mental model I use:
> MCP is for agents working alone in the dark.
> WebMCP is for agents working alongside humans in the light.
### The Auth Difference Is The Biggest Deal
With a backend MCP server, you have to build OAuth 2.1 from scratch. Every service needs tokens, scopes, refresh logic — it's weeks of work.
With WebMCP, the user is already logged in. The browser session cookie is already there. You just call fetch("/api/enroll", { credentials: "same-origin" }) and auth is handled automatically.
### When to Use Which
Use MCP Server when:
- Background automation — nightly reports, bulk operations
- No UI needed — pure data and API operations
- Integration with Claude Desktop, ChatGPT, Cursor
- The operation is fully autonomous with no human confirmation needed
Use WebMCP when:
- User is actively in the browser and AI is assisting them
- You want to reuse your existing frontend auth session
- The operation should update the visible UI
- You need human confirmation at specific steps
Use both (ideal architecture):
```
Teacher in browser → WebMCP
AI helps fill attendance, process enrollments
Human confirms each action
UI updates in real time
Principal at night → MCP Server
Claude generates monthly reports
Sends bulk fee reminders
Runs headlessly, no browser needed
```
## How WebMCP Connects to Claude and ChatGPT Today
Since WebMCP tools live in the browser, how do you get Claude Desktop or ChatGPT to call them? This is where MCP-B comes in — a bridge library by Alex Nahas (the same person who co-authored the WebMCP spec).
```
Claude Desktop / ChatGPT
│ JSON-RPC over HTTP (standard MCP)
▼
MCP-B Native Server (runs on your machine, port 12306)
│ WebSocket
▼
MCP-B Chrome Extension (installed in Chrome)
│ postMessage
▼
Your Browser Tab
│
navigator.modelContext (your registered WebMCP tools)
│
execute() runs → React state updates → API calls made
```
### Setting It Up
Step 1 — Install the polyfill in your app:
```bash
npm install @mcp-b/global
```
```typescript
// layout.tsx — must be the FIRST import
import "@mcp-b/global";
```
Step 2 — Install and start the native server:
```bash
npm install -g @mcp-b/native-server
mcp-b-native-server
# Running on http://127.0.0.1:12306
```
Step 3 — Add to Claude Desktop config:
```json
{
"mcpServers": {
"skillora-browser": {
"type": "streamable-http",
"url": "http://127.0.0.1:12306/mcp"
}
}
}
```
Step 4 — Install the MCP-B Chrome extension from the Chrome Web Store.
Now your WebMCP tools are callable from Claude Desktop. You write one set of tools. Claude, ChatGPT, and any MCP-compatible client can call them.
## The Honest Reality Check
WebMCP is genuinely exciting but it's important to be clear-eyed about where it is today:
- Chrome Canary only — not stable Chrome, not Firefox, not Safari
- No cross-browser commitment yet — Mozilla and Apple haven't announced support
- The spec has TODO sections — security and accessibility considerations aren't written yet
- The Chrome built-in AI that will call these tools isn't publicly available yet
- Production timeline — stable Chrome support is probably late 2026 at earliest
But here's why I'm writing about it now rather than waiting: the fundamentals are sound, the spec is real W3C work with Google and Microsoft behind it, and the Chrome Canary implementation is working. If you're building web apps today, understanding WebMCP puts you 6-12 months ahead of when everyone else starts scrambling to add it.
## How to Get Started Right Now
1. Apply for the EPP:
Go to developer.chrome.com/blog/webmcp-epp and apply.
2. Download Chrome Canary:
google.com/chrome/canary — installs separately from regular Chrome.
3. Enable the flag:
In Chrome Canary, go to chrome://flags/#webmcp-for-testing and set it to Enabled. Relaunch.
4. Verify it's working:
```js
console.log(navigator.modelContext);
// Should print: ModelContext {}
```
5. Register your first tool:
```js
navigator.modelContext.registerTool({
name: "say_hello",
description: "Says hello to a person by name",
inputSchema: {
type: "object",
properties: {
name: { type: "string", description: "The person's name" },
},
required: ["name"],
},
execute: async ({ name }) => {
alert(Hello, ${name}!);
return { content: [{ type: "text", text: Said hello to ${name} }] };
},
});
```
Open the Model Context Tool Inspector extension, find say_hello, call it with { "name": "World" }, and watch the alert fire. Your first real WebMCP tool, running in a real browser.
## The Big Picture
WebMCP doesn't change what your website does. It adds a second audience for it.
Humans use your UI. Agents use your tools. Same website, same code, same authentication, same business logic. You're not building a separate AI integration — you're making your existing app speak a language that AI agents understand.
The web has done this before. RSS feeds let feed readers consume your content. JSON-LD lets search engines understand your data. Open Graph lets social networks preview your links. Every time the web added a machine-readable layer on top of the human-readable layer, developers who added it early got outsized benefits.
WebMCP is that next layer — for the age of AI agents.
It's early. The spec has TODOs. Chrome Canary is not production. But the direction is clear, the people building it are serious, and the problem it solves is real.
I'm building it into Skillora now. By the time it ships in stable Chrome, it'll already be there.
## Resources
- My Demo: webmcp.sobitprasad.com
- GitHub: github.com/sobitp59/webmcp-tuta
- W3C Spec: webmachinelearning.github.io/webmcp
- Chrome EPP: developer.chrome.com/blog/webmcp-epp
- Official Demo: andreinwald.github.io/webmcp-demo
- GoogleChromeLabs Tools: github.com/GoogleChromeLabs/webmcp-tools
- MCP-B (bridge to Claude/ChatGPT): github.com/WebMCP-org
- Awesome WebMCP list: github.com/webmcpnet/awesome-webmcp
- W3C Community Group: github.com/webmachinelearning/webmcp
