DEV Community

Cover image for Angular WebMCP — Your App is Now an AI Tool 🔥🚀
Giorgio Galassi for Google Developer Group

Posted on • Originally published at Medium

Angular WebMCP — Your App is Now an AI Tool 🔥🚀

There's a pattern in Angular releases that I've come to appreciate: every major version picks one bet that's slightly ahead of its time. Signals were that bet in v16. Zoneless was that bet in v21. In v22, that bet is WebMCP, and it's a fundamentally different kind of feature — not a framework improvement, but an architectural shift in what an Angular app is.

Let me explain what I mean.


🧭 The Idea: Outside → In

Every Angular feature so far has been about what happens inside the app — how components detect changes, how services are injected, how forms manage state. WebMCP is different: it's about exposing your app's capabilities to the outside, specifically to AI agents running in the browser.

The mental model is simple. Today, an AI assistant browsing your app sees a DOM — pixels and HTML. It can read text and click buttons, but it has no understanding of what your app can do. WebMCP changes that. You declare a set of tools backed by your real Angular services, your real signals, your real DI graph, and any WebMCP-capable agent can discover and call them directly through a typed, described interface you control.

This is different from Agent Skills, which is your app calling out to AI. Two separate features, two directions: WebMCP is agents driving your app; Agent Skills is your app calling AI. Today we're only talking about WebMCP.


🔧 Browser Support

WebMCP is built on the W3C ModelContext API, a draft browser standard. Your tools register on navigator.modelContext and any agent that speaks this protocol can query that object and call them.

As of June 2026, Edge 147 ships it natively, Chrome 149 has an open Origin Trial, Firefox is committed for Q3 2026, and Safari for Q4. Mass adoption is realistically mid-2027, and Angular ships this as experimental — that flag is honest, but the integration is already surprisingly clean.

⚠️ Experimental: provideExperimentalWebMcpTools() is available in Angular 22 but carries no stability guarantees yet. The W3C spec is still evolving; expect API changes before general availability.


🛠️ Building Your First WebMCP Tool

Let's build something concrete. A dashboard shows a list of expenses and we want an AI assistant to ask "what expenses are currently visible?" and get real, live data back — not a DOM scrape or a static API response, but the actual signal state the component is rendering from.

Step 1: The service

// expense.service.ts
import { Service, signal } from '@angular/core';

export interface Expense {
  id: string;
  description: string;
  amount: number;
  currency: string;
  category: ExpenseCategory;
}

export const ExpenseCategory = {
  Accommodation: 'accommodation',
  Transport: 'transport',
  Meals: 'meals',
} as const;

export type ExpenseCategory = typeof ExpenseCategory[keyof typeof ExpenseCategory];

export const CATEGORY_VALUES = Object.values(ExpenseCategory).join(', ');

@Service()
export class ExpenseService {
  private expenses = signal<Expense[]>([
    { id: '1', description: 'Hotel',  amount: 250, currency: 'USD', category: ExpenseCategory.Accommodation },
    { id: '2', description: 'Taxi',   amount: 35,  currency: 'USD', category: ExpenseCategory.Transport },
    { id: '3', description: 'Dinner', amount: 80,  currency: 'USD', category: ExpenseCategory.Meals },
  ]);

  getVisible(): Expense[] {
    return this.expenses();
  }
}
Enter fullscreen mode Exit fullscreen mode

@Service() is Angular 22's new shorthand for @Injectable({ providedIn: 'root' }) — if you want the full picture on that, I covered it in Angular 22 — @Service and injectAsync: Dependency Injection Finally Grows Up. Notice that CATEGORY_VALUES lives right here next to the ExpenseCategory object — we'll use it to keep the tool description in sync automatically.

Step 2: Declaring the tool

Parameters are described using JSON Schema syntax — the same format Angular uses internally and the same format the agent receives.

// expense-mcp.tool.ts
import { declareExperimentalWebMcpTool, inject } from '@angular/core';
import { ExpenseService, ExpenseCategory, CATEGORY_VALUES } from './expense.service';

export const expenseListTool = declareExperimentalWebMcpTool({
  name: 'getVisibleExpenses',
  description: 'Returns the list of expenses currently visible in the dashboard.',
  inputSchema: {
    type: 'object',
    properties: {
      category: {
        type: 'string',
        description: `Filter by expense category. Allowed values: ${CATEGORY_VALUES}.`,
      },
    },
    additionalProperties: false,
  },
  execute: ({ category }: { category?: ExpenseCategory }) => {
    const svc = inject(ExpenseService);
    const all = svc.getVisible();
    const filtered = category ? all.filter(e => e.category === category) : all;
    return { content: [{ type: 'text', text: JSON.stringify(filtered) }] };
  }
});
Enter fullscreen mode Exit fullscreen mode

Four things to notice here.

The return value follows the MCP tool result format. { content: [{ type: 'text', text: '...' }] } is the wire format the agent expects, defined by the Model Context Protocol spec. You don't return a plain object or array directly — you serialise your data into that text field with JSON.stringify, and the agent parses it on its end. This is not Angular-specific; it's the protocol contract every MCP tool must honour.

name and description are instructions to the LLM, not documentation for humans. The agent reads them at runtime to decide whether and how to call the tool, so write them like an OpenAPI spec for an AI: precise, specific, unambiguous.

execute runs inside Angular's injection context, which means inject() works normally and your tool has full access to your DI graph.

CATEGORY_VALUES is derived from the object, not hardcoded. Add a value to ExpenseCategory and the agent description updates automatically, with no second place to maintain.

Step 3: Registering in the app

// app.config.ts
import { provideExperimentalWebMcpTools } from '@angular/core';
import { expenseListTool } from './expense-mcp.tool';

export const appConfig = {
  providers: [
    provideExperimentalWebMcpTools([expenseListTool]),
    // ... rest of your providers
  ]
};
Enter fullscreen mode Exit fullscreen mode

One line in your app config and you're done. From here, any WebMCP-capable agent visiting your app will find getVisibleExpenses in document.modelContext.tools and can call it against the live session.


🔍 What the Agent Actually Sees

When a WebMCP-capable agent inspects the page, this is what it finds on document.modelContext:

{
  "tools": [
    {
      "name": "getVisibleExpenses",
      "description": "Returns the list of expenses currently visible in the dashboard.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "category": {
            "type": "string",
            "description": "Filter by expense category. Allowed values: accommodation, transport, meals."
          }
        },
        "additionalProperties": false
      }
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

The user says "show me only transport expenses." The agent reads the schema, resolves category: "transport", calls the tool. Angular runs the handler against the live signal state. The agent gets back exactly the filtered rows the dashboard is currently showing — no DOM scraping, no brittle selectors, no static mock data.


⚛️ Signal Forms as Agent Tools

If your app is form-heavy, Angular 22 also ships provideExperimentalWebMcpForms(), which automatically surfaces all your Signal Forms as agent-callable tools without you having to declare each one manually.

// app.config.ts
import { provideExperimentalWebMcpForms } from '@angular/forms/signals';

export const appConfig = {
  providers: [
    provideExperimentalWebMcpForms(),
  ]
};
Enter fullscreen mode Exit fullscreen mode

⚠️ Experimental: provideExperimentalWebMcpForms() is double-experimental — both WebMCP itself and the forms integration are in preview. Treat it as a proof of concept for now.

Each Signal Form becomes a tool the agent can fill and submit. For internal tooling or admin dashboards this is already genuinely useful, and it shows where the Angular team is heading: if Signal Forms are the way to model user intent, they should also be the way to model agent intent.


💡 Beyond the Happy Path

The docs highlight forms as the easy entry point, but the more interesting territory is everything else — read queries, write actions, navigation triggers.

Let's be honest: a read + write pair is where agents become genuinely powerful. Expose getExpenseReports() alongside flagExpense(id, reason) and an agent can query, reason over the data, and act in a single turn. Add permission-awareness by injecting your AuthService inside the execute handler and the agent automatically gets a properly scoped view, seeing only what the current user is authorised to see. You could also expose derived data tools like getSummaryByCategory() — pre-computed aggregates cost fewer tokens and produce faster responses than handing the agent a raw list to process itself.

The general principle worth internalising: anywhere you'd write a hardcoded string describing your domain model, ask whether you can derive it from TypeScript instead. Your types are the single source of truth and the agent's schema should follow from them, not diverge from them.


🔒 One Thing Worth Getting Right Early: Trust

Because execute handlers run inside Angular's injection context with full access to your DI graph, a WebMCP tool is as powerful as the service method it wraps — which means a write tool is a real mutation, not a preview. Before shipping any tool that modifies state, there are three things worth understanding.

Prompt injection in your descriptions. The description and parameter description fields are read by the LLM as trusted context. A malicious site can embed instructions in its own tool descriptions that manipulate the agent's behaviour on other sites — and if your own tool returns user-generated content, that content is another vector. The W3C security considerations doc calls this out explicitly under "Metadata / Description Attacks" and "Output Injection Attacks": never echo unsanitised user content directly from an execute handler.

Angular does not validate inputs for you. The agent is supposed to match the inputSchema you define, but Angular makes no guarantee it does — the execute callback receives whatever the agent sends. The Angular docs are explicit on this: "Consider explicitly validating arguments to the execute function before using them." Treat every input as untrusted, the same way you would a form submission or a query parameter.

The session is the attack surface. The W3C spec's threat model notes that agents inherit the user's authentication context — session cookies, logged-in state, everything. The real risk isn't an anonymous external caller; same-origin constraints handle that. The risk is a legitimate, trusted agent that has been manipulated through prompt injection and then calls your write tools with the user's full permissions. Design accordingly: scope write tools tightly, and never expose an action you wouldn't want triggered automatically on behalf of a logged-in user.


⚠️ The Honest Caveat

The W3C spec is still early and has been changing frequently. Mid-2027 is the realistic mass-adoption target, when both browser default support and enough publisher adoption exist to make it meaningful at scale. For now it's great for internal tools and early experimentation, but it's not ready to ship to anonymous production users.

That said, the direction is set. Your app has always had one interface — the DOM, for humans. WebMCP adds a second one, for agents. How you design that second surface is a new skill worth starting to develop now, before the spec lands and everyone is catching up at once.


If you found this helpful, follow me here and on LinkedIn for more deep dives into Angular, signals, AI, and modern frontend development.

See you in the next one! 🤙🏻
— G.

Top comments (0)