Register your first WebMCP tool with navigator.modelContext
Kashish Hora
Co-founder of MCPcat
The Quick Answer
WebMCP lets you register a tool on your website using navigator.modelContext.registerTool(). This makes the tool discoverable by AI agents visiting the page:
navigator.modelContext.registerTool({
name: "search-flights",
description: "Search for available flights between two airports on a given date",
inputSchema: {
type: "object",
properties: {
origin: { type: "string", description: "Departure airport code (e.g. SFO)" },
destination: { type: "string", description: "Arrival airport code (e.g. JFK)" },
date: { type: "string", description: "Travel date in YYYY-MM-DD format" }
},
required: ["origin", "destination", "date"]
},
execute: async ({ origin, destination, date }) => {
const flights = await fetchFlights(origin, destination, date);
return { results: flights };
}
});WebMCP is a browser-native API that lets websites expose structured tools to AI agents. Tools run client-side, inherit the user's browser session, and require no server or build step.
Prerequisites
- A website served over HTTPS (or
localhostfor development) - A recent Chromium-based browser build with
chrome://flags/#enable-webmcp-testingenabled (availability varies by channel) - Basic JavaScript and JSON Schema knowledge
What Is WebMCP
WebMCP is a browser API being developed through the W3C Web Machine Learning Community Group. It gives websites a way to declare structured tools that AI agents can discover and call.
Unlike server-side MCP where you build a separate server process, WebMCP tools run directly in the browser tab. They have access to the DOM, the user's authenticated session, and any client-side state your app already manages. There's no separate server to deploy or API keys to manage.
WebMCP is currently in early preview in Chrome and may require experimental flags depending on your browser channel. For other browsers, polyfills like the one included in webmcp-react make the API available today. If you're familiar with MCP transport protocols like stdio or StreamableHTTP, WebMCP sits alongside them as a browser-native alternative.
Registering a WebMCP Tool
A tool needs four things: a name, a description, an optional inputSchema, and an execute callback. If inputSchema is omitted, the tool accepts no input. The name must be unique on the page, and the description should tell an agent what the tool does and when to use it.
navigator.modelContext.registerTool({
name: "search-flights",
description: "Search for available flights between two airports on a given date",
inputSchema: {
type: "object",
properties: {
origin: { type: "string", description: "Departure airport code (e.g. SFO)" },
destination: { type: "string", description: "Arrival airport code (e.g. JFK)" },
date: { type: "string", description: "Travel date in YYYY-MM-DD format" }
},
required: ["origin", "destination", "date"]
},
annotations: { readOnlyHint: true },
execute: async ({ origin, destination, date }) => {
const flights = await fetchFlights(origin, destination, date);
return { results: flights };
}
});The execute callback receives two arguments: the input object (matching your schema) and a client object for requesting user interaction (covered below). Whatever your callback returns gets sent back to the agent. This can be any serializable value — a string, object, array, etc. If you're bridging tools to desktop AI clients via an MCP server (as shown in the webmcp-react guide), return the MCP content format ({ content: [{ type: "text", text: "..." }] }) so the bridge can forward responses without transformation.
Setting annotations: { readOnlyHint: true } tells agents this tool only reads data and won't change anything. This helps agents decide whether it's safe to call without asking the user first.
If the browser doesn't support WebMCP yet, wrap your registration in a feature check:
if ("modelContext" in navigator) {
navigator.modelContext.registerTool({ /* ... */ });
}registerTool throws an InvalidStateError if the name or description is empty, or if a tool with the same name is already registered.
Unregistering a Tool
Call unregisterTool with the tool's name to remove it:
navigator.modelContext.unregisterTool("search-flights");This matters in single-page apps where tools should come and go with the UI. If the user navigates away from the flights page, a search-flights tool no longer makes sense. Stale tools persist until the page fully unloads, so clean them up on route changes or when the relevant UI is torn down.
unregisterTool throws an InvalidStateError if no tool with that name exists. If you're not sure whether the tool is registered, wrap it in a try/catch.
Defining Input Schemas
The inputSchema follows standard JSON Schema. The description field on each property is especially important — agents rely on it to understand what values to pass and in what format.
const inputSchema = {
type: "object",
properties: {
origin: {
type: "string",
description: "IATA airport code for departure (e.g. SFO, LAX, ORD)"
},
destination: {
type: "string",
description: "IATA airport code for arrival (e.g. JFK, LHR, NRT)"
},
date: {
type: "string",
description: "Travel date in YYYY-MM-DD format"
},
passengers: {
type: "integer",
description: "Number of passengers (1-9)",
minimum: 1,
maximum: 9
},
cabinClass: {
type: "string",
enum: ["economy", "business", "first"],
description: "Preferred cabin class"
}
},
required: ["origin", "destination", "date"]
};A few rules of thumb: keep schemas focused on one task, make property descriptions specific enough that an agent doesn't have to guess, and use required to distinguish between mandatory and optional fields. If a tool is getting too many properties, it's usually better to split it into multiple tools. For more on designing tool schemas, see adding custom tools to an MCP server in TypeScript. To write automated tests that validate inputs against schemas like these, see validation tests for tool inputs.
Confirming Actions with requestUserInteraction
For tools that change state — booking a flight, deleting an account, placing an order — use requestUserInteraction to pause execution and get the user's confirmation. The second argument to your execute callback is a client object that provides this method.
navigator.modelContext.registerTool({
name: "book-flight",
description: "Book a specific flight for the user",
// inputSchema with flightId (string) and passengers (integer)
inputSchema: { /* ... */ },
execute: async ({ flightId, passengers }, client) => {
const flight = await getFlightDetails(flightId);
const confirmed = await client.requestUserInteraction(async () => {
return confirm(
`Book ${passengers} seat(s) on ${flight.airline} ${flight.number} ` +
`for $${flight.price * passengers}?`
);
});
if (!confirmed) throw new Error("Booking cancelled by user");
const booking = await createBooking(flightId, passengers);
return { confirmationCode: booking.code };
}
});The callback you pass to requestUserInteraction runs in your page context. In this example it uses a simple confirm() dialog, but you could show a custom modal, a toast, or any other UI. The agent's execution pauses until the callback resolves. You can call requestUserInteraction multiple times during a single tool execution if you need multiple confirmations.
Testing Your WebMCP Tool
Chromium builds expose navigator.modelContextTesting as an experimental testing interface when the WebMCP testing flag is enabled. You can use it from the browser console to verify your tools are registered and working.
List all registered tools:
navigator.modelContextTesting.listTools();
// [{name: "search-flights", description: "Search for...", inputSchema: "{...}"}, ...]Each entry in the returned array has name, description, and inputSchema (as a JSON string). If a tool you registered doesn't appear here, it either threw during registration or was unregistered by another script.
Execute a tool by passing its name and a JSON string of the input:
await navigator.modelContextTesting.executeTool(
"search-flights",
JSON.stringify({ origin: "SFO", destination: "JFK", date: "2026-04-15" })
);Note that executeTool takes a JSON string, not an object. The result is also returned as a stringified value. In Chromium implementations, if the tool throws or the input doesn't match the schema, the promise rejects with a DOMException.
To connect your tools to desktop AI clients like Claude Code or Cursor, you need a bridge between the browser and the client. The webmcp-react Chrome extension and MCP server handle this.
Common Issues
InvalidStateError: A tool with this name already exists
This happens when registerTool is called twice with the same name. In development, hot module replacement (HMR) re-runs your registration code without unloading the page, so the tool from the previous module version is still registered. The same issue occurs in SPAs that re-run initialization logic on navigation.
// Safe re-registration pattern
function registerToolSafe(tool) {
try {
navigator.modelContext.unregisterTool(tool.name);
} catch (e) {
// Tool wasn't registered yet — that's fine
}
navigator.modelContext.registerTool(tool);
}navigator.modelContext is undefined
The API requires a secure context (HTTPS or localhost). Availability can vary by browser build and frame context, so check that:
- You're on
https://orhttp://localhost - The Chrome flag
chrome://flags/#enable-webmcp-testingis set to "Enabled" - If you're running inside an iframe, test in a top-level page as well (some builds may restrict iframe access)
- Your code runs client-side, not during server-side rendering
Use feature detection (if ("modelContext" in navigator)) to avoid errors on browsers that don't support the API yet.
Tool registered but agents can't find it
navigator.modelContext makes tools available within the browser page, but desktop AI clients like Claude Code and Cursor can't see them directly. You need a bridge — typically a browser extension that reads the registered tools and forwards them to a local MCP server.
The webmcp-react library includes both a Chrome extension and an MCP server (npx webmcp-server) that handle this. See the companion guide for setup instructions.
Next Steps
This guide covered the raw navigator.modelContext API. If you're building with React, webmcp-react wraps this API in hooks that handle polyfilling, component lifecycle, Zod schema conversion, and bridging to desktop AI clients automatically.
Related Guides
Add WebMCP tools to a React app with webmcp-react
How to use the webmcp-react library to register WebMCP tools in React with Zod schemas and connect them to desktop clients.
Adding custom tools to an MCP server in TypeScript
Step-by-step guide to adding custom tools to your TypeScript MCP server with proper typing and error handling.
Comparing stdio vs. SSE vs. StreamableHTTP
Compare MCP transport protocols to choose between stdio, SSE, and StreamableHTTP for your use case.