WebMCP in Angular: your app as a toolbox for AI agents
Your Angular application can now register tools that an AI agent discovers and executes without ever leaving the page. The feature is called WebMCP, it shipped as experimental in Angular v22, and it turns your app into a small MCP server running inside the browser.
Today, agents "use" your app the old way: they read the DOM, simulate clicks, and pray the selector didn't change. WebMCP proposes something healthier: you declare tools with a name, a description, and a JSON Schema, and the agent calls them directly. - Source: WebMCP Explainer
In this post we'll cover what WebMCP is, the APIs Angular v22 ships to use it, and the details that will save you hours of debugging.
What WebMCP is
WebMCP is a proposed web standard at the W3C Web Machine Learning Community Group. The latest draft is dated October 16, 2026, and it defines a new property: document.modelContext. - Source: WebMCP Draft Community Group Report
The idea in one sentence: the page exposes JavaScript tools with natural-language descriptions and JSON Schemas, and agents discover and invoke them. A page using WebMCP works like an MCP server that implements its tools in client-side script instead of on the backend.
Browser support is on its way:
- Chrome 149 has it in Origin Trial (since June 2026). For local development:
chrome://flags/#enable-webmcp-testing. - Edge 150 is also in Origin Trial.
- Firefox and Safari have open standards-position issues; nothing implemented yet.
- ChatGPT Desktop already invokes WebMCP tools, and Meta announced support in Ray-Ban Display.
- Source: WebMCP implementation status, Chrome Origin Trial
The platform API is straightforward. This, in plain JavaScript, registers a tool:
// browser-console.js
await document.modelContext.registerTool({
name: "add-todo",
description: "Add a new item to the user's todo list",
inputSchema: {
type: "object",
properties: { text: { type: "string" } },
required: ["text"]
},
async execute({ text }) {
// your app logic, the same your UI uses
}
});
registerTool, getTools and executeTool are the three methods of the modelContext object. - Source: Chrome WebMCP docs
The problem it solves
Picture asking an agent: "buy a size M t-shirt from this store". Without WebMCP, the agent opens the page, scrapes the HTML, guesses which <div> is the size filter, and simulates clicks. It works until you change a CSS class. It's like asking someone for directions by describing the buildings' colors: the first rainy day, they're lost.
With WebMCP, the store registers a get-dresses(size, color) tool and a filter-products(ids) tool. The agent sees them, calls them with typed arguments, and the UI updates. The user watches everything happen on screen and confirms the sensitive actions. Real human-in-the-loop, with context shared between agent, page and user. - Source: WebMCP Explainer
One important note: WebMCP doesn't replace backend MCP servers. It complements them. The backend remains your source of truth; WebMCP exposes the actions of the interface you already have.
The Angular v22 APIs
Angular v22 ships experimental support with two functions you import from @angular/core. Mind the names: they're long for a reason.
provideExperimentalWebMcpTools registers tools at the app or route level:
// main.ts
import {Service, inject, provideExperimentalWebMcpTools} from '@angular/core';
import {bootstrapApplication} from '@angular/platform-browser';
import {AppRoot} from './app-root';
@Service()
class Greeter {
sayHello(): string {
return 'Hello agent!';
}
}
bootstrapApplication(AppRoot, {
providers: [
provideExperimentalWebMcpTools([
{
name: 'greet',
description: 'Greets the agent.',
inputSchema: {type: 'object', properties: {}},
execute: () => {
const greeter = inject(Greeter);
return {content: [{type: 'text', text: greeter.sayHello()}]};
},
},
]),
],
});
Note that execute runs inside the injection context of the associated Injector: you can call inject() inside it, no ceremony needed. Tools register when the injector initializes and unregister when it's destroyed. - Source: provideExperimentalWebMcpTools
Argument types are inferred from the JSON Schema. With required: ['query'] the type stops being string | undefined, and with additionalProperties: false the args object stays closed:
// search-tool.ts
import {provideExperimentalWebMcpTools} from '@angular/core';
provideExperimentalWebMcpTools([
{
name: 'searchCatalog',
description: 'Searches the store catalog for products matching a query.',
inputSchema: {
type: 'object',
properties: {
query: {type: 'string', description: 'The search keywords.'},
maxResults: {type: 'number', description: 'Maximum number of results.'},
},
required: ['query'],
additionalProperties: false,
},
execute: ({query, maxResults}) => {
// query is string, maxResults is number | undefined
const limit = maxResults ?? 5;
return {
content: [{type: 'text', text: `Returning up to ${limit} results for "${query}".`}],
};
},
},
]);
- Source: Angular WebMCP guide
declareExperimentalWebMcpTool is the imperative variant, for registering a tool dynamically from any injection context. Combined with signals, you get tools that read your app's live state:
// counter.ts
import {Service, declareExperimentalWebMcpTool, signal} from '@angular/core';
@Service()
export class Counter {
readonly count = signal(0);
constructor() {
declareExperimentalWebMcpTool({
name: 'getCounter',
description: 'Reads the global counter.',
inputSchema: {type: 'object', properties: {}},
execute: () => ({
content: [{type: 'text', text: `The count is: ${this.count()}.`}],
}),
});
}
}
The agent asks for the counter and always gets the current value. No polling, no scraping. - Source: declareExperimentalWebMcpTool
Route-scoped tools, with automatic cleanup
My favorite pattern: register tools only where they make sense. A dashboard exposes exportDashboardReports; checkout exposes the cart tools. All through the route's providers:
// routes.ts
import {provideExperimentalWebMcpTools} from '@angular/core';
import {Routes} from '@angular/router';
export const routes: Routes = [
{
path: 'dashboard',
loadComponent: () => import('./dashboard').then((m) => m.Dashboard),
providers: [
provideExperimentalWebMcpTools([
{
name: 'exportDashboardReports',
description: 'Exports the current dashboard analytics.',
inputSchema: {type: 'object', properties: {}},
execute: () => ({
content: [{type: 'text', text: 'Dashboard export successfully triggered.'}],
}),
},
]),
],
},
];
But there's a catch: if you navigate away, those tools stay visible to the agent, unless you enable automatic injector cleanup:
// app.config.ts
import {provideRouter, withAutoCleanupInjectors} from '@angular/router';
export const appConfig: ApplicationConfig = {
providers: [provideRouter(routes, withAutoCleanupInjectors())],
};
- Source: Angular WebMCP guide
Without withAutoCleanupInjectors, the exportDashboardReports tool stays registered after the user left the dashboard. The agent would call it and the export no longer has context. Silent failure, guaranteed.
Signal Forms: free tools
This is where Angular's design shows it was thought through. With provideExperimentalWebMcpForms, every Signal Form registers automatically as a WebMCP tool, with the schema inferred from the model and the validators included. - Source: provideExperimentalWebMcpForms
// user-registration.ts
import {Component, signal} from '@angular/core';
import {form, required} from '@angular/forms/signals';
@Component({
selector: 'app-user-registration',
templateUrl: './user-registration.html',
})
export class UserRegistration {
private readonly model = signal({
firstName: '',
lastName: '',
age: 0,
hobbies: ['Web Development'],
});
readonly userForm = form(
this.model,
(f) => {
required(f.firstName, {message: 'First name is mandatory.'});
required(f.lastName, {message: 'Last name is mandatory.'});
},
{
// Registers a `registerUser` tool with parameters from the model
experimentalWebMcpTool: {
name: 'registerUser',
description: 'Registers a new user.',
},
submission: {
action: async (formValue) => {
await submitRegistration(formValue().value());
},
},
},
);
}
Angular infers the schema from the model's initial value: firstName and lastName as required strings, age as a number, hobbies as array<string>. And if the agent submits invalid data, it receives the validation errors and can self-correct. If you already migrated to Signal Forms, this is a two-for-one: write your form once and get UI and tool at the same time.
How to try it without waiting for an agent
Three ways, from fastest to most formal:
- Chrome DevTools: the Application panel has a WebMCP section where you see the available tools exactly as the agent sees them, with invocation counters, and you can run them manually without an agent. - Source: Chrome DevTools WebMCP pane
- The polyfill: for unit tests and unsupported browsers,
@mcp-b/webmcp-polyfillsimulatesdocument.modelContext. Your tools' tests run on any CI. - Your favorite agent: ChatGPT Desktop already invokes WebMCP tools, and Chrome agent extensions can query them via content scripts.
Security: the things nobody will forgive
Three things to know before you register your first tool:
First: Angular doesn't validate inputs. The agent can send anything; the schema is documentation, not a security guard. Validate inside execute:
// validation.ts
execute: ({query, maxResults}) => {
if (typeof query !== 'string') throw new Error(`Bad query: ${query}`);
if (typeof maxResults !== 'number' && maxResults !== undefined) {
throw new Error(`Bad maxResults: ${maxResults}`);
}
// ...
}
Second: names collide and things blow up. Registering two tools with the same name throws. declareExperimentalWebMcpTool inside a component runs every time the component instantiates; if that component can render more than once at a time, you have a time bomb. Prefer app or route providers, or root services. - Source: Angular WebMCP guide
Third: prompt injection lives here too. The spec dedicates a whole section to it: misleading descriptions that induce unwanted purchases, tool responses with injected instructions, and sites asking for excessive parameters to profile users. Mitigations are on the way (annotations like consequentialHint to require user confirmation on high-stakes actions), but today the responsibility is yours. - Source: WebMCP spec, Security section
WebMCP vs the CLI MCP server
Don't confuse the two layers. Angular already ships an MCP server for your coding agent (npx @angular/cli mcp): it compiles, runs tests, searches the docs and teaches best practices while you code. That's dev-time.
WebMCP is runtime: your deployed app exposing its features to the agents that visit it. One helps whoever writes the code; the other helps whoever uses your product. To go deeper on the dev-time layer, I already wrote about Angular Agent Skills and about building apps with Genkit and Gemini.
Production tips
- Watch your token budget. Every tool consumes agent context with its name, description and schema. Ten well-described tools beat thirty generic ones. - Source: Chrome WebMCP docs
- One tool, one responsibility.
searchProductsandaddToCartseparate, not a mega-toolshop(action: string)the agent has to guess how to use. - Keep the tool in sync with the visible UI. If the
filter-productstool filters the grid the user is looking at, the user sees what the agent does. If the tool runs in parallel with a UI that never changes, you breed distrust. - Register and unregister dynamically. Cart tools only while a cart exists. Less noise for the agent, smaller attack surface for you.
- Validate strictly in code, loosely in schema. The schema teaches the agent; the code decides what gets in.
Next time an agent visits your app, will it scrape your DOM or call your tools? That difference is yours to decide now. Have you played with WebMCP in Chrome 149? Tell me in the comments.