🤖 WebMCP
3,943
stars
136
commits
Bikeshed
primary language
Sep 10, 2026
updated
WebMCP lets developers expose web application functionality—either JavaScript functions or HTML <form> elements—as "tools" with natural language descriptions and structured schemas, designed for AI agent ingestion. These tools can be invoked by AI agents, including those built into the browser, hosted in iframes, or running in extensions to actuate web content that was traditionally designed for human interaction.
TypeScript type definitions for WebMCP are available in the webmcp-types npm package.
See Implementation Status for browser support.
The web platform is the world's largest gateway to information and capabilities. Today, user experiences rely on visual layouts, mouse and touch interactions, and visual cues to communicate functionality and state, but as AI agents become prevalent, the potential for even greater user value is within reach. The motivation of WebMCP is to provide a lightweight way to adapt web content for use by AI agents.
AI platforms such as Copilot, ChatGPT, Claude, and Gemini are increasingly able to interact with external services to perform actions such as checking local weather, finding flight and hotel information, and providing driving directions. This is facilitated by "tools" that external services provide to extend the AI model’s capabilities, and give the AI domain-specific functionality that it cannot obtain on its own.
External tools integrate with each AI platform via bespoke backend integrations, such as Model Context Protocol or OpenAPI. A service registers its tools with an AI platform, and the platform communicates directly with the service's backend servers via an API. In this document, we call this style of tool a “backend integration”; users make use of the tools by chatting with an AI, and the AI platform communicates with the service on the user's behalf.
Backend integrations work well for server-side actions, but they pose significant challenges for interactive web applications:
WebMCP introduces a client-side alternative. It allows web developers to define tools directly in the browser page's script. This enables visually rich, cooperative interplay between a user, a web page, and an agent with shared context. Page UI and content remain available to the agent for actuation, but the agent can use WebMCP tools to achieve the user's goals more directly, reliably, and quickly, as the tools are in a format more suited to the agent.
graph TD
subgraph WB["<b><i>Web browser</i></b>"]
BA["Browser-integrated AI agent"]
subgraph RP["Running Page 'index.html'"]
WMCP["WebMCP tools"]
end
end
AI["<b><i>AI agent platform</i></b>"]
TP["<b><i>Third-party service<br>(example.com)</i></b>"]
%% Connections
TP -->|"1. Browser loads page over HTTP"| RP
AI <-->|"2. LLM in the cloud communicates with a browser AI agent to act on web content"| BA
BA <-->|"3. Browser agent uses WebMCP tools to actuate the current page"| WMCP
WMCP -->|"4. WebMCP tools update UI and make API calls"| TP
graph TD
AI["<b><i>AI agent platform</i></b>"]
subgraph WB["<b><i>Web Browser</i></b>"]
BIA["Browser-integrated AI agent"]
RP["Running Page <index.html>"]
end
subgraph TP["<b><i>Third-party service (example.com)</i></b>"]
MCP[("MCP Server")]
end
RP <-->|1. Browser loads page over HTTP| TP
BIA -->|2. User prompt sent to agent platform in the cloud.| AI
AI -->|"3. Agent platform uses pre-configured MCP server to interact directly with service and fulfill user request."| MCP
MCP -->|4a. MCP response routed back to agent platform.| AI
AI -->|5. Response rendered to user by browser agent. Web page has no direct visibility or control.| BIA
RP <-.->|4b. Service manually pushes updates to page.| TP
Many challenges faced by assistive technology also apply to AI agents that struggle to navigate existing human-first interfaces when agent-first "tools" are not available. Even when agents succeed, simple operations often require multiple steps and can be slow or unreliable.
Web pages that use WebMCP can be thought of as in-page Model Context Protocol (MCP) servers that implement tools exposing client-side logic and DOM interaction rather than server-side APIs. WebMCP enables collaborative workflows where users and agents work together within the same web interface, leveraging existing application logic while maintaining shared context and user control.
One of the scenarios we want to enable is making the web more accessible to general-purpose AI-based agents. In the absence of alternatives like MCP servers to accomplish their goals, these general-purpose agents often rely on observing the browser state through a combination of screenshots, and DOM and accessibility tree snapshots, and then interact with the page by simulating human user input. We believe that WebMCP will give these tools an alternative means to interact with the web that give the web developer more control over whether and how an AI-based agent interacts with their site.
The proposed API will not conflict with these existing automation techniques. If an agent or assistive tool finds that the task it is trying to accomplish is not achievable through the WebMCP tools that the page provides, then it can fall back to general-purpose browser automation to try and accomplish its task.
WebMCP enables cooperative workflows where the user collaborates with the agent rather than completely delegating their goal to it.
Jen wants to create a yard sale flyer on https://easely.example. She wants to filter templates and make visual edits. Instead of navigating menus, she interacts with her browser's agent:
await document.modelContext.registerTool({
name: "filter-templates",
description: "Filters the list of templates based on a natural language visual description.",
inputSchema: {
type: "object",
properties: {
description: { type: "string", description: "A visual description of templates to show." }
},
required: ["description"]
},
execute({ description }) {
filterTemplatesInUI(description);
}
});
filter-templates tool, and the UI instantly updates to show matching layouts.edit-design(instructions).edit-design. The graphic design page applies these edits as a batch of "uncommitted" changes in the UI, allowing Jen to review or adjust them.order-prints tool:
await document.modelContext.registerTool({
name: "order-prints",
description: "Orders the current design for printing and shipping to the user.",
inputSchema: {
type: "object",
properties: {
copies: { type: "number", description: "Number of copies between 1 and 1000." },
pageSize: { type: "string", enum: ["Letter", "Legal", "A4"], default: "Letter" }
},
required: ["copies"]
},
execute({ copies, pageSize }) {
initiatePrintCheckout(copies, pageSize);
}
});
Maya is shopping for dresses on http://wildebloom.example/shop.
await document.modelContext.registerTool({
name: "get-dresses",
description: "Returns an array of product listings containing id, description, price, and photo.",
inputSchema: {
type: "object",
properties: {
size: { type: "number", description: "Optional EU dress size to filter by." },
color: { type: "string", description: "Optional color to filter by." }
}
},
async execute({ size, color }) {
const response = await fetchDresses(size, color);
return response.json();
}
});
await document.modelContext.registerTool({
name: "show-dresses",
...
});
await document.modelContext.registerTool({
name: "filter-products",
...
});
get-dresses(6) (automatically translating Maya's size into EU units from her browser profile context) and receives a JSON array of detailed product listings:
{
"products": [
{
"id": 1021,
"description": "A short sleeve midi dress in organic cotton with a floral print...",
"price": "€180",
"image": "img_1021.png"
},
{
"id": 4320,
"description": "A straight-cut formal linen gown on plant-based dyes...",
"price": "€220",
"image": "img_4320.png"
},
{
"id": 684,
"description": ...
},
...
]
}
show-dresses([1021, 4320, 684, ...]). This updates the UI on the page to show only the requested dresses.filter-products([1021, 684]), instantly updating the site's UI with relevant dresses.John is a software developer performing a code review in Gerrit. The interface is complex, but the page registers helpful tools to inspect trybot statuses and retrieve logs, perfect for agents that are typically trained on everyday usage, and may otherwise do a poor job actuating such complicated interfaces.
await document.modelContext.registerTool({
name: "get-trybot-statuses",
description: "Returns the current status of all trybot runs for the active patch.",
execute() {
return activePatch.getStatuses();
}
});
await document.modelContext.registerTool({
name: "get-trybot-failure-snippet",
description: "If a bot failed, returns the tail log snippet describing the error.",
inputSchema: {
type: "object",
properties: {
botName: { type: "string", description: "The bot name to query." }
},
required: ["botName"]
},
execute({ botName }) {
return activePatch.getFailureSnippet(botName);
}
});
get-trybot-statuses and receives a JSON array representing the trybot statuses:
[
{ "botName": "mac-x64-rel", "status": "FAIL" },
{ "botName": "android-15-rel", "status": "FAIL" }
]
get-trybot-failure-snippet for each failing bot. After ingesting the logs, it reports back:
gfx::DisplayCompositor."display_compositor_android.cc. Please add a suggested edit to the build file adding it to the Android sources."add-suggested-edit(filename, patch) tool to apply the diff. The Gerrit UI instantly displays the suggested patch as a code-review diff for John to accept, modify, or reject.WebMCP introduces an imperative API on the web platform under document.modelContext. This interface allows pages to expose client-side actions that agents can discover and invoke in a secure, browser-mediated environment.
document.modelContextA Model Context Provider registers tools by calling the document.modelContext.registerTool() method.
const controller = new AbortController();
await document.modelContext.registerTool({
name: "add-todo",
description: "Add a new item to the user's active todo list",
inputSchema: {
type: "object",
properties: {
text: { type: "string", description: "The text content of the todo item" }
},
required: ["text"]
},
async execute({ text }) {
// Reuse existing client-side application logic and update UI.
await addTodoItemToCollection(text);
return {
content: [
{
type: "text",
text: `Added todo item: "${text}" successfully.`
}
]
};
}
}, { signal: controller.signal });
// To unregister the tool later, abort the signal.
// controller.abort();
document.modelContext.registerTool().inputSchema.execute callback with the provided arguments, and executes client-side logic on the page.For forms and standard HTML inputs, a declarative counterpart to the imperative API allows the browser to automatically synthesize tool definitions from <form> elements. This is detailed in the Declarative API Explainer. It will be soon folded into this explainer document.
We've gotten the following question a few times:
why isn't declarative WebMCP sufficient on its own—why must there be an imperative counterpart?
The reason WebMCP is not limited to only declarative form tools is for the same reason that websites cannot be built exclusively out of declarative forms. Some of the web's functionality is only possible with JavaScript, and for WebMCP to represent the web's full functionality to agents, it must be able to expose that JavaScript functionality through imperative tools, not just declarative ones.
While much of this explainer assumes integration with built-in browser agents, WebMCP also supports author-provided agents, such as agents embedded directly on a page or running in an iframe, that can collaborate with parent frames and nested contexts.
By default, WebMCP is enabled in top-level Windows and its same-origin iframes, but access can be delegated to cross-origin iframes using the Permissions Policy allow="tools":
<iframe src="https://chat-bot-provider.example/" allow="tools"></iframe>
Calls to document.modelContext.registerTool() will return a promise rejected with NotAllowedError DOMException when the permission is disabled, whether by the allow attribute or the Permissions-Policy: tools=() header. Handling of declarative tool registration errors, including when the permission is disabled is TBD; see Issue #182.
registerTool() and exposedTo`By default, tools registered by a document are only exposed to itself, same-origin documents in the same tree, and built-in browser agents (see this discussion). To support author-provided agents running in frames, developers can selectively share tools with specific secure origins via the exposedTo option:
await document.modelContext.registerTool({
name: "share-location",
description: "Returns the user's office location.",
execute() { return { office: "Building 4" }; }
}, { exposedTo: ["https://trusted-partner.example"] });
Any document in the tree matching these origins (and allowed to use tools permission) will:
toolchange event on its document.modelContext when the tool is registered or unregistered.getTools() and executeTool()Once tools are registered, in-page agents can discover and invoke them using getTools() and executeTool().
Calling document.modelContext.getTools() returns a promise that resolves with an array of RegisteredTool dictionary objects. Each object contains the tool's name, description, inputSchema, origin, and owner window. By default, getTools() only returns tools registered by documents same-origin with the caller in the frame tree. To retrieve cross-origin tools, you must explicitly list their origins in the fromOrigins option. This array only supports secure origins.
// Discover tools exposed by same-origin frames in the tree (default)
const tools = await document.modelContext.getTools();
for (const tool of tools) {
console.log(`Tool: ${tool.name} (from ${tool.origin})`);
console.log(`Description: ${tool.description}`);
console.log(`Parameters schema:`, tool.inputSchema);
}
// Discover additional tools provided by a cross-origin frame (in addition to
// same-origin ones):
const crossOriginTools = await document.modelContext.getTools({
fromOrigins: ["https://trusted-partner.example"]
});
An agent executes a discovered RegisteredTool by passing the tool dictionary and input arguments along to document.modelContext.executeTool(). The browser securely mediates the execution, ensuring the exposedTo and fromOrigins agree, and the tool runs in the tool owner's execution context:
const tools = await document.modelContext.getTools();
const addTodoTool = tools.find(t => t.name === "add-todo");
if (addTodoTool) {
try {
const result = await document.modelContext.executeTool(
addTodoTool,
{ text: "Buy groceries" }
);
console.log("Tool result:", result);
} catch (error) {
console.error("Tool execution failed:", error);
}
}
AbortSignalTool invocations can be cancelled mid-execution (e.g., if the user aborts an ongoing request, as they might with the "stop button" that's present in most agent UIs) by passing an AbortSignal:
const controller = new AbortController();
const executionPromise = document.modelContext.executeTool(
addTodoTool,
{ text: "Buy groceries" },
{ signal: controller.signal }
);
// If the user cancels the interaction:
stopButton.addEventListener('click', e => controller.abort());
The tool's execution callback receives this signal via its options.signal parameter, allowing it to abort underlying network requests or asynchronous tasks cleanly.
toolchange eventWhen tools are added, removed, or updated dynamically (such as when user interactions result in new tools being registered), document.modelContext fires a toolchange event:
document.modelContext.addEventListener("toolchange", async () => {
const currentTools = await document.modelContext.getTools();
updateAgentToolRegistry(currentTools);
});
We considered directly adopting the full Model Context Protocol (MCP) spec in the browser without creating a web-native API. However:
Instead, WebMCP derives direct inspiration and shares a common vocabulary with MCP (e.g., tools, schemas, parameters), but provides a form-fitting, client-safe solution designed natively for the web platform.
We considered declaring tools solely inside static manifest files (like the Web App Manifest). While useful for offline or background discovery:
Our current approach allows imperative script-based registration, with the potential for static declarations to be layered on in the future.
'toolcall')Another alternative was to handle tool execution exclusively via window-level events:
document.agent.addEventListener('toolcall', async (e) => {
if (e.name === 'add-todo') {
e.respondWith(handleAddTodo(e.arguments));
}
});
switch-case statement blocks in event handlers."toolcall" event is dispatched on the window before falling back to executing the registered imperative execute callback, allowing advanced interception.Interacting with AI agents crosses traditional trust boundaries. Security, privacy, permissions policy, and origin isolation are crucial aspects of this proposal.
For our current considerations, refer to the Security and Privacy Considerations section of the specification.
As the WebMCP proposal continues to evolve with community and stakeholder feedback, we are tracking several active design discussions and technical challenges:
Multimodal input/output: AI agents are increasingly multimodal, and we should consider how tools can consume binary media as inputs and how to return them as outputs (e.g., audio, streams, media blobs, etc.). See Issue #41, Issue #86, and Issue #81, and Prompt API: Multimodal inputs.
Cross-document tool response: How should WebMCP handle tool responses when a tool (a form submission, for example) causes the page to navigate to another document? See Issue #135.
Built-in agent exposure by default:
The exposedTo array only takes origins, but we're considering introducing a new keyword like native-agent, letting authors control a tool's exposure to a built-in agent. The running idea is that by default in the top-level document, a missing exposedTo array would expose tools to the built-in agent, and in iframes, a missing exposedTo array would not expose tools to the built-in agent
Transferable/streamable tool inputs and outputs: AI models inherently support streaming data. WebMCP should consider enabling streaming tool inputs and outputs (such as chunked generation or large data transfers) without blocking on a massive copy. See Issue #82. See also MCP discussion and MCP Apps streaming tool inputs.
Input and output schema validation: Investigating native validation of tool inputs and outputs against declared JSON schemas before invoking the page's JS execution callback, or letting the output reach the model. See Issue #92.
Skills Integration: Determining if the author should expose a higher-level "skill" to help the agent coordinate multiple related tools to fulfill a user journey. See Issue #161.
Output schema: Supporting structured outputSchema contracts (complementing inputSchema) to help LLMs reliably reason about the return values of tools. See Issue #9.
User prompting and elicitation: Exploring a way for a tool to prompt the user for confirmation when tools require explicit user authorization. This could be done by delegating to the agent and its harness, or by invoking native browser permission dialogue outside of the agent loop. See Issue #165 and Issue #50 for discussion about the ModelContextClient interface.
Tool progress reporting: For long-running tasks (e.g., batch processing or generating content), the agent may want a way to track a tool's progress. We are exploring how this intersects with the established MCP Progress specification.
Service workers integration: Extending WebMCP to background Service Workers to allow agents to discover and invoke tools on sites the user doesn't currently have open. This is detailed in the supplementary Service Workers Explainer, which proposes background discovery mechanisms, session identification, and JIT worker installation.
First published August 13, 2025
Brandon Walderman
<brwalder@microsoft.com>
Leo Lee<leo.lee@microsoft.com>
Andrew Nolan<annolan@microsoft.com>
David Bokan<bokan@google.com>
Khushal Sagar<khushalsagar@google.com>
Hannah Van Opstal<hvanopstal@google.com>
Since then, the specification draft has evolved significantly, primarily driven by Dominic Farolino.
Many thanks to Alex Nahas and Jason McGhee for sharing their valuable implementation experience.
Bikeshed
99.2%
🤖 WebMCP
3,943
stars
136
commits
Bikeshed
primary language
Sep 10, 2026
updated
WebMCP lets developers expose web application functionality—either JavaScript functions or HTML <form> elements—as "tools" with natural language descriptions and structured schemas, designed for AI agent ingestion. These tools can be invoked by AI agents, including those built into the browser, hosted in iframes, or running in extensions to actuate web content that was traditionally designed for human interaction.
TypeScript type definitions for WebMCP are available in the webmcp-types npm package.
See Implementation Status for browser support.
The web platform is the world's largest gateway to information and capabilities. Today, user experiences rely on visual layouts, mouse and touch interactions, and visual cues to communicate functionality and state, but as AI agents become prevalent, the potential for even greater user value is within reach. The motivation of WebMCP is to provide a lightweight way to adapt web content for use by AI agents.
AI platforms such as Copilot, ChatGPT, Claude, and Gemini are increasingly able to interact with external services to perform actions such as checking local weather, finding flight and hotel information, and providing driving directions. This is facilitated by "tools" that external services provide to extend the AI model’s capabilities, and give the AI domain-specific functionality that it cannot obtain on its own.
External tools integrate with each AI platform via bespoke backend integrations, such as Model Context Protocol or OpenAPI. A service registers its tools with an AI platform, and the platform communicates directly with the service's backend servers via an API. In this document, we call this style of tool a “backend integration”; users make use of the tools by chatting with an AI, and the AI platform communicates with the service on the user's behalf.
Backend integrations work well for server-side actions, but they pose significant challenges for interactive web applications:
WebMCP introduces a client-side alternative. It allows web developers to define tools directly in the browser page's script. This enables visually rich, cooperative interplay between a user, a web page, and an agent with shared context. Page UI and content remain available to the agent for actuation, but the agent can use WebMCP tools to achieve the user's goals more directly, reliably, and quickly, as the tools are in a format more suited to the agent.
graph TD
subgraph WB["<b><i>Web browser</i></b>"]
BA["Browser-integrated AI agent"]
subgraph RP["Running Page 'index.html'"]
WMCP["WebMCP tools"]
end
end
AI["<b><i>AI agent platform</i></b>"]
TP["<b><i>Third-party service<br>(example.com)</i></b>"]
%% Connections
TP -->|"1. Browser loads page over HTTP"| RP
AI <-->|"2. LLM in the cloud communicates with a browser AI agent to act on web content"| BA
BA <-->|"3. Browser agent uses WebMCP tools to actuate the current page"| WMCP
WMCP -->|"4. WebMCP tools update UI and make API calls"| TP
graph TD
AI["<b><i>AI agent platform</i></b>"]
subgraph WB["<b><i>Web Browser</i></b>"]
BIA["Browser-integrated AI agent"]
RP["Running Page <index.html>"]
end
subgraph TP["<b><i>Third-party service (example.com)</i></b>"]
MCP[("MCP Server")]
end
RP <-->|1. Browser loads page over HTTP| TP
BIA -->|2. User prompt sent to agent platform in the cloud.| AI
AI -->|"3. Agent platform uses pre-configured MCP server to interact directly with service and fulfill user request."| MCP
MCP -->|4a. MCP response routed back to agent platform.| AI
AI -->|5. Response rendered to user by browser agent. Web page has no direct visibility or control.| BIA
RP <-.->|4b. Service manually pushes updates to page.| TP
Many challenges faced by assistive technology also apply to AI agents that struggle to navigate existing human-first interfaces when agent-first "tools" are not available. Even when agents succeed, simple operations often require multiple steps and can be slow or unreliable.
Web pages that use WebMCP can be thought of as in-page Model Context Protocol (MCP) servers that implement tools exposing client-side logic and DOM interaction rather than server-side APIs. WebMCP enables collaborative workflows where users and agents work together within the same web interface, leveraging existing application logic while maintaining shared context and user control.
One of the scenarios we want to enable is making the web more accessible to general-purpose AI-based agents. In the absence of alternatives like MCP servers to accomplish their goals, these general-purpose agents often rely on observing the browser state through a combination of screenshots, and DOM and accessibility tree snapshots, and then interact with the page by simulating human user input. We believe that WebMCP will give these tools an alternative means to interact with the web that give the web developer more control over whether and how an AI-based agent interacts with their site.
The proposed API will not conflict with these existing automation techniques. If an agent or assistive tool finds that the task it is trying to accomplish is not achievable through the WebMCP tools that the page provides, then it can fall back to general-purpose browser automation to try and accomplish its task.
WebMCP enables cooperative workflows where the user collaborates with the agent rather than completely delegating their goal to it.
Jen wants to create a yard sale flyer on https://easely.example. She wants to filter templates and make visual edits. Instead of navigating menus, she interacts with her browser's agent:
await document.modelContext.registerTool({
name: "filter-templates",
description: "Filters the list of templates based on a natural language visual description.",
inputSchema: {
type: "object",
properties: {
description: { type: "string", description: "A visual description of templates to show." }
},
required: ["description"]
},
execute({ description }) {
filterTemplatesInUI(description);
}
});
filter-templates tool, and the UI instantly updates to show matching layouts.edit-design(instructions).edit-design. The graphic design page applies these edits as a batch of "uncommitted" changes in the UI, allowing Jen to review or adjust them.order-prints tool:
await document.modelContext.registerTool({
name: "order-prints",
description: "Orders the current design for printing and shipping to the user.",
inputSchema: {
type: "object",
properties: {
copies: { type: "number", description: "Number of copies between 1 and 1000." },
pageSize: { type: "string", enum: ["Letter", "Legal", "A4"], default: "Letter" }
},
required: ["copies"]
},
execute({ copies, pageSize }) {
initiatePrintCheckout(copies, pageSize);
}
});
Maya is shopping for dresses on http://wildebloom.example/shop.
await document.modelContext.registerTool({
name: "get-dresses",
description: "Returns an array of product listings containing id, description, price, and photo.",
inputSchema: {
type: "object",
properties: {
size: { type: "number", description: "Optional EU dress size to filter by." },
color: { type: "string", description: "Optional color to filter by." }
}
},
async execute({ size, color }) {
const response = await fetchDresses(size, color);
return response.json();
}
});
await document.modelContext.registerTool({
name: "show-dresses",
...
});
await document.modelContext.registerTool({
name: "filter-products",
...
});
get-dresses(6) (automatically translating Maya's size into EU units from her browser profile context) and receives a JSON array of detailed product listings:
{
"products": [
{
"id": 1021,
"description": "A short sleeve midi dress in organic cotton with a floral print...",
"price": "€180",
"image": "img_1021.png"
},
{
"id": 4320,
"description": "A straight-cut formal linen gown on plant-based dyes...",
"price": "€220",
"image": "img_4320.png"
},
{
"id": 684,
"description": ...
},
...
]
}
show-dresses([1021, 4320, 684, ...]). This updates the UI on the page to show only the requested dresses.filter-products([1021, 684]), instantly updating the site's UI with relevant dresses.John is a software developer performing a code review in Gerrit. The interface is complex, but the page registers helpful tools to inspect trybot statuses and retrieve logs, perfect for agents that are typically trained on everyday usage, and may otherwise do a poor job actuating such complicated interfaces.
await document.modelContext.registerTool({
name: "get-trybot-statuses",
description: "Returns the current status of all trybot runs for the active patch.",
execute() {
return activePatch.getStatuses();
}
});
await document.modelContext.registerTool({
name: "get-trybot-failure-snippet",
description: "If a bot failed, returns the tail log snippet describing the error.",
inputSchema: {
type: "object",
properties: {
botName: { type: "string", description: "The bot name to query." }
},
required: ["botName"]
},
execute({ botName }) {
return activePatch.getFailureSnippet(botName);
}
});
get-trybot-statuses and receives a JSON array representing the trybot statuses:
[
{ "botName": "mac-x64-rel", "status": "FAIL" },
{ "botName": "android-15-rel", "status": "FAIL" }
]
get-trybot-failure-snippet for each failing bot. After ingesting the logs, it reports back:
gfx::DisplayCompositor."display_compositor_android.cc. Please add a suggested edit to the build file adding it to the Android sources."add-suggested-edit(filename, patch) tool to apply the diff. The Gerrit UI instantly displays the suggested patch as a code-review diff for John to accept, modify, or reject.WebMCP introduces an imperative API on the web platform under document.modelContext. This interface allows pages to expose client-side actions that agents can discover and invoke in a secure, browser-mediated environment.
document.modelContextA Model Context Provider registers tools by calling the document.modelContext.registerTool() method.
const controller = new AbortController();
await document.modelContext.registerTool({
name: "add-todo",
description: "Add a new item to the user's active todo list",
inputSchema: {
type: "object",
properties: {
text: { type: "string", description: "The text content of the todo item" }
},
required: ["text"]
},
async execute({ text }) {
// Reuse existing client-side application logic and update UI.
await addTodoItemToCollection(text);
return {
content: [
{
type: "text",
text: `Added todo item: "${text}" successfully.`
}
]
};
}
}, { signal: controller.signal });
// To unregister the tool later, abort the signal.
// controller.abort();
document.modelContext.registerTool().inputSchema.execute callback with the provided arguments, and executes client-side logic on the page.For forms and standard HTML inputs, a declarative counterpart to the imperative API allows the browser to automatically synthesize tool definitions from <form> elements. This is detailed in the Declarative API Explainer. It will be soon folded into this explainer document.
We've gotten the following question a few times:
why isn't declarative WebMCP sufficient on its own—why must there be an imperative counterpart?
The reason WebMCP is not limited to only declarative form tools is for the same reason that websites cannot be built exclusively out of declarative forms. Some of the web's functionality is only possible with JavaScript, and for WebMCP to represent the web's full functionality to agents, it must be able to expose that JavaScript functionality through imperative tools, not just declarative ones.
While much of this explainer assumes integration with built-in browser agents, WebMCP also supports author-provided agents, such as agents embedded directly on a page or running in an iframe, that can collaborate with parent frames and nested contexts.
By default, WebMCP is enabled in top-level Windows and its same-origin iframes, but access can be delegated to cross-origin iframes using the Permissions Policy allow="tools":
<iframe src="https://chat-bot-provider.example/" allow="tools"></iframe>
Calls to document.modelContext.registerTool() will return a promise rejected with NotAllowedError DOMException when the permission is disabled, whether by the allow attribute or the Permissions-Policy: tools=() header. Handling of declarative tool registration errors, including when the permission is disabled is TBD; see Issue #182.
registerTool() and exposedTo`By default, tools registered by a document are only exposed to itself, same-origin documents in the same tree, and built-in browser agents (see this discussion). To support author-provided agents running in frames, developers can selectively share tools with specific secure origins via the exposedTo option:
await document.modelContext.registerTool({
name: "share-location",
description: "Returns the user's office location.",
execute() { return { office: "Building 4" }; }
}, { exposedTo: ["https://trusted-partner.example"] });
Any document in the tree matching these origins (and allowed to use tools permission) will:
toolchange event on its document.modelContext when the tool is registered or unregistered.getTools() and executeTool()Once tools are registered, in-page agents can discover and invoke them using getTools() and executeTool().
Calling document.modelContext.getTools() returns a promise that resolves with an array of RegisteredTool dictionary objects. Each object contains the tool's name, description, inputSchema, origin, and owner window. By default, getTools() only returns tools registered by documents same-origin with the caller in the frame tree. To retrieve cross-origin tools, you must explicitly list their origins in the fromOrigins option. This array only supports secure origins.
// Discover tools exposed by same-origin frames in the tree (default)
const tools = await document.modelContext.getTools();
for (const tool of tools) {
console.log(`Tool: ${tool.name} (from ${tool.origin})`);
console.log(`Description: ${tool.description}`);
console.log(`Parameters schema:`, tool.inputSchema);
}
// Discover additional tools provided by a cross-origin frame (in addition to
// same-origin ones):
const crossOriginTools = await document.modelContext.getTools({
fromOrigins: ["https://trusted-partner.example"]
});
An agent executes a discovered RegisteredTool by passing the tool dictionary and input arguments along to document.modelContext.executeTool(). The browser securely mediates the execution, ensuring the exposedTo and fromOrigins agree, and the tool runs in the tool owner's execution context:
const tools = await document.modelContext.getTools();
const addTodoTool = tools.find(t => t.name === "add-todo");
if (addTodoTool) {
try {
const result = await document.modelContext.executeTool(
addTodoTool,
{ text: "Buy groceries" }
);
console.log("Tool result:", result);
} catch (error) {
console.error("Tool execution failed:", error);
}
}
AbortSignalTool invocations can be cancelled mid-execution (e.g., if the user aborts an ongoing request, as they might with the "stop button" that's present in most agent UIs) by passing an AbortSignal:
const controller = new AbortController();
const executionPromise = document.modelContext.executeTool(
addTodoTool,
{ text: "Buy groceries" },
{ signal: controller.signal }
);
// If the user cancels the interaction:
stopButton.addEventListener('click', e => controller.abort());
The tool's execution callback receives this signal via its options.signal parameter, allowing it to abort underlying network requests or asynchronous tasks cleanly.
toolchange eventWhen tools are added, removed, or updated dynamically (such as when user interactions result in new tools being registered), document.modelContext fires a toolchange event:
document.modelContext.addEventListener("toolchange", async () => {
const currentTools = await document.modelContext.getTools();
updateAgentToolRegistry(currentTools);
});
We considered directly adopting the full Model Context Protocol (MCP) spec in the browser without creating a web-native API. However:
Instead, WebMCP derives direct inspiration and shares a common vocabulary with MCP (e.g., tools, schemas, parameters), but provides a form-fitting, client-safe solution designed natively for the web platform.
We considered declaring tools solely inside static manifest files (like the Web App Manifest). While useful for offline or background discovery:
Our current approach allows imperative script-based registration, with the potential for static declarations to be layered on in the future.
'toolcall')Another alternative was to handle tool execution exclusively via window-level events:
document.agent.addEventListener('toolcall', async (e) => {
if (e.name === 'add-todo') {
e.respondWith(handleAddTodo(e.arguments));
}
});
switch-case statement blocks in event handlers."toolcall" event is dispatched on the window before falling back to executing the registered imperative execute callback, allowing advanced interception.Interacting with AI agents crosses traditional trust boundaries. Security, privacy, permissions policy, and origin isolation are crucial aspects of this proposal.
For our current considerations, refer to the Security and Privacy Considerations section of the specification.
As the WebMCP proposal continues to evolve with community and stakeholder feedback, we are tracking several active design discussions and technical challenges:
Multimodal input/output: AI agents are increasingly multimodal, and we should consider how tools can consume binary media as inputs and how to return them as outputs (e.g., audio, streams, media blobs, etc.). See Issue #41, Issue #86, and Issue #81, and Prompt API: Multimodal inputs.
Cross-document tool response: How should WebMCP handle tool responses when a tool (a form submission, for example) causes the page to navigate to another document? See Issue #135.
Built-in agent exposure by default:
The exposedTo array only takes origins, but we're considering introducing a new keyword like native-agent, letting authors control a tool's exposure to a built-in agent. The running idea is that by default in the top-level document, a missing exposedTo array would expose tools to the built-in agent, and in iframes, a missing exposedTo array would not expose tools to the built-in agent
Transferable/streamable tool inputs and outputs: AI models inherently support streaming data. WebMCP should consider enabling streaming tool inputs and outputs (such as chunked generation or large data transfers) without blocking on a massive copy. See Issue #82. See also MCP discussion and MCP Apps streaming tool inputs.
Input and output schema validation: Investigating native validation of tool inputs and outputs against declared JSON schemas before invoking the page's JS execution callback, or letting the output reach the model. See Issue #92.
Skills Integration: Determining if the author should expose a higher-level "skill" to help the agent coordinate multiple related tools to fulfill a user journey. See Issue #161.
Output schema: Supporting structured outputSchema contracts (complementing inputSchema) to help LLMs reliably reason about the return values of tools. See Issue #9.
User prompting and elicitation: Exploring a way for a tool to prompt the user for confirmation when tools require explicit user authorization. This could be done by delegating to the agent and its harness, or by invoking native browser permission dialogue outside of the agent loop. See Issue #165 and Issue #50 for discussion about the ModelContextClient interface.
Tool progress reporting: For long-running tasks (e.g., batch processing or generating content), the agent may want a way to track a tool's progress. We are exploring how this intersects with the established MCP Progress specification.
Service workers integration: Extending WebMCP to background Service Workers to allow agents to discover and invoke tools on sites the user doesn't currently have open. This is detailed in the supplementary Service Workers Explainer, which proposes background discovery mechanisms, session identification, and JIT worker installation.
First published August 13, 2025
Brandon Walderman
<brwalder@microsoft.com>
Leo Lee<leo.lee@microsoft.com>
Andrew Nolan<annolan@microsoft.com>
David Bokan<bokan@google.com>
Khushal Sagar<khushalsagar@google.com>
Hannah Van Opstal<hvanopstal@google.com>
Since then, the specification draft has evolved significantly, primarily driven by Dominic Farolino.
Many thanks to Alex Nahas and Jason McGhee for sharing their valuable implementation experience.
Bikeshed
99.2%