WebMCP: A New Way for AI Agents to Interact with the Web
Unlike traditional MCPs, which enable agents to interact with server-side tools, WebMCP allows web applications to directly expose their interfaces to AI agents. Agents can then natively interact with navigation, forms, page filters, frontend logic, ...
WebMCP is a proposed web standard from Google (currently in early preview in Chrome 146), it's API is subject to change.
What are the differences with OpenAI Atlas or other similar solutions?
One of the key advantages of WebMCP is that it doesn't depend on static DOM snapshots or image analysis to "understand" the user interface of your website. Instead, you guide the agent with a series of frontend tools that you expose on your website.
How to use WebMCP?
- Google Chrome 146 canary or later
- "WebMCP for testing" flag enabled in Chrome (chrome://flags/#enable-webmcp-testing)
- Model Context Tool Inspector extension installed
- A Gemini API Key
WebMCP exposes two complementary APIs: an Imperative API for defining tools in JavaScript, and a Declarative API for annotating existing HTML forms.
The Imperative API
Use navigator.modelContext.registerTool() to expose any frontend action as a named tool. Here's a minimal blog example — a search tool that filters posts:
Example using Zod:
const inputSchema = z.object({ query: z.string() .describe("The search query — e.g. 'ai', 'typescript', 'team management'"), tag: z.string() .describe("Filter posts by tag/category — e.g. 'ai', 'storybook', 'turborepo'") .optional(), })
const outputSchema = z.array(z.object({ id: z.string(), title: z.string(), summary: z.string(), date: z.string(), url: z.string(),}))
navigator.modelContext.registerTool({ name: "searchPosts", description: "Search blog posts by keyword. Returns a list of matching posts with their title, date, and URL.", inputSchema: z.toJSONSchema(inputSchema), outputSchema: z.toJSONSchema(outputSchema), execute: async ({ query, tag }: z.infer<typeof inputSchema>): Promise<z.infer<typeof outputSchema>> => { const results = await filterPosts({ query, tag });
router.navigate({ to: "/blog", params: { query, tag } });
return results; }});Example using Effect Schema:
const InputSchema = S.Struct({ query: S.String() .annotations({ description: "The search query — e.g. 'ai', 'typescript', 'team management'" }), tag: S.optional(S.String()) .annotations({ description: "Filter posts by tag/category — e.g. 'ai', 'storybook', 'turborepo'" }), })
const OutputSchema = S.Array(S.Struct({ id: S.String(), title: S.String(), summary: S.String(), date: S.String(), url: S.String(),}))
navigator.modelContext.registerTool({ name: "searchPosts", description: "Search blog posts by keyword. Returns a list of matching posts with their title, date, and URL.", inputSchema: inputSchema.pipe(JSONSchema.make), outputSchema: outputSchema.pipe(JSONSchema.make), execute: async ({ query, tag }: typeof InputSchema.Type): Promise<typeof OutputSchema.Type> => { // ... }});In this example, we navigate to the filtered view before returning results.
To unregister a tool you can use:
navigator.modelContext.unregisterTool("searchPosts");The Declarative API
If you already have HTML forms, you can turn them into WebMCP tools with just a few attributes — no JavaScript required. The browser does the translation automatically.
<form toolname="filterByTag" tooldescription="Filter blog posts by tag. Navigates to the filtered post list." action="/blog" method="GET"> <label for="tag">Tag</label>
<select name="tag" id="tag" toolparamdescription="The tag to filter by — e.g. 'ai', 'storybook'" > <option value="ai">AI</option> <option value="storybook">Storybook</option> <option value="turborepo">Turborepo</option> <option value="teamManagement">Team Management</option> </select>
<button type="submit">Filter</button></form>The browser reads toolname, tooldescription, and the form fields to automatically generate a JSON Schema for the agent.
It then pre-populates the form when the agent calls the tool, and submits it — or waits for the user to confirm, depending on whether toolautosubmit is set.
If you need to intercept agent submissions — for validation or to return structured data — you can listen for the submit event and use e.respondWith():
document.querySelector("form").addEventListener("submit", (e) => { e.preventDefault();
if (e.agentInvoked) { const results = getFilteredPosts(new FormData(e.target));
e.respondWith(Promise.resolve(JSON.stringify(results))); }});The browser also provides CSS pseudo-classes (:tool-form-active, :tool-submit-active) so you can visually signal to the user that an agent is interacting with the page.
Recommendations
- Use clear, action-focused names that match what the tool actually does.
- Describe when to use the tool and what each parameter means, in plain language.
- Design tools around real user actions, not just data or backend operations.
Try it on this website!
- Open this website in Chrome Canary with the "WebMCP for testing" flag enabled
- Open the Model Context Tool Inspector extension
- Enter your Gemini API Key
- In the Interact with the page / User Prompt field, enter:
- "Show me the posts under the tag ai"
- "Open the LinkedIn profile link"