DEV Community

Cover image for WebMCP: Give AI agents a real API for your Angular app
Mariano Álvarez 🇨🇷
Mariano Álvarez 🇨🇷

Posted on

WebMCP: Give AI agents a real API for your Angular app

Most browser agents use a website in the same way we do: they read what is on the screen, find a button, click it, and hope the page behaves as expected.

It works. Until the button changes, the layout moves, or a custom component makes the action difficult to understand. If you have written end-to-end tests, you already know how this story ends.

But what if the website could tell the agent what it can do?

That is the idea behind WebMCP. Instead of making an agent guess how to use our interface, we can expose small, structured tools such as search_products or add_product_to_cart.

Let's build that with Angular.

What WebMCP changes

WebMCP is a proposed web standard that allows a page to expose JavaScript functions and HTML forms as tools for AI agents.

A tool has four important parts:

  • A name.
  • A description of when to use it.
  • A JSON Schema describing its inputs.
  • A function that executes the action.

The name is a little confusing because WebMCP is not simply an MCP server running in the browser. It borrows concepts from MCP, but it is designed around things the web already understands: documents, origins, permissions, iframes, and the lifecycle of a tab.

In the current proposal, the API lives at document.modelContext.

The important difference is simple. Without WebMCP, the agent sees a button and tries to understand it. With WebMCP, the application says: “I have a tool that searches products. It accepts a query and returns these fields.”

Less guessing. Fewer fragile clicks.

The example: a small online store

Suppose a user asks an agent:

Find a waterproof backpack under $100 and add one to my cart.

The agent needs to do two things:

  1. Search the catalog.
  2. Add the selected product to the cart.

I would stop there. I would not expose a checkout tool in the first version. Adding an item to a cart is easy to reverse; charging a credit card is not.

Angular 22 has experimental WebMCP support, so we can register both tools through dependency injection instead of manually connecting them to a component.

Register the tools

The complete example has two tools. Instead of putting everything in one large block, let's build it in small pieces.

The example assumes that ProductCatalogService and CartService already exist and are also used by the visible Angular interface.

1. Add the imports

import {
  inject,
  provideExperimentalWebMcpTools,
} from '@angular/core';
import { CartService } from './cart.service';
import { ProductCatalogService } from './product-catalog.service';
Enter fullscreen mode Exit fullscreen mode

2. Create the product search tool

This is a read-only tool. It searches the existing catalog and returns only the fields the agent needs.

export function provideProductSearchTool() {
  return provideExperimentalWebMcpTools([
    {
      name: 'search_products',
      description:
        'Searches the product catalog using the customer query. ' +
        'Returns product ID, name, price, and stock status.',
      inputSchema: {
        type: 'object',
        properties: {
          query: {
            type: 'string',
            description: 'The search terms supplied by the customer.',
          },
          maxResults: {
            type: 'number',
            description: 'Maximum number of products to return.',
          },
        },
        required: ['query'],
        additionalProperties: false,
      },
      annotations: {
        readOnlyHint: true,
        untrustedContentHint: true,
      },
      execute: async ({ query, maxResults }, { signal }) => {
        if (typeof query !== 'string' || query.trim().length === 0) {
          throw new Error('The search query cannot be empty.');
        }

        if (
          maxResults !== undefined &&
          (!Number.isInteger(maxResults) || maxResults < 1 || maxResults > 10)
        ) {
          throw new Error('maxResults must be an integer from 1 to 10.');
        }

        const catalog = inject(ProductCatalogService);
        const products = await catalog.search({
          query: query.trim(),
          limit: maxResults ?? 5,
          signal,
        });

        return {
          content: [
            {
              type: 'text',
              text: JSON.stringify(
                products.map(({ id, name, price, inStock }) => ({
                  id,
                  name,
                  price,
                  inStock,
                })),
              ),
            },
          ],
        };
      },
    },
  ]);
}
Enter fullscreen mode Exit fullscreen mode

The readOnlyHint tells the agent that this tool does not change the application. untrustedContentHint is useful when names or descriptions can contain content supplied by sellers or other users.

3. Create the cart tool

The second tool changes the cart, but it does not place an order or charge the customer. That distinction should be clear in the description.

export function provideAddToCartTool() {
  return provideExperimentalWebMcpTools([
    {
      name: 'add_product_to_cart',
      description:
        'Adds an in-stock product to the visible shopping cart. ' +
        'This tool does not place an order or charge the customer.',
      inputSchema: {
        type: 'object',
        properties: {
          productId: {
            type: 'string',
            description: 'The product ID returned by search_products.',
          },
          quantity: {
            type: 'number',
            description: 'Number of units to add, from 1 to 10.',
          },
        },
        required: ['productId', 'quantity'],
        additionalProperties: false,
      },
      annotations: {
        readOnlyHint: false,
        consequentialHint: false,
        untrustedContentHint: false,
      },
      execute: async ({ productId, quantity }, { signal }) => {
        if (typeof productId !== 'string' || productId.length === 0) {
          throw new Error('A product ID is required.');
        }

        if (!Number.isInteger(quantity) || quantity < 1 || quantity > 10) {
          throw new Error('Quantity must be an integer from 1 to 10.');
        }

        const cart = inject(CartService);
        const item = await cart.add({ productId, quantity, signal });

        return {
          content: [
            {
              type: 'text',
              text: `Added ${item.quantity} × ${item.name} to the cart.`,
            },
          ],
        };
      },
    },
  ]);
}
Enter fullscreen mode Exit fullscreen mode

4. Register both providers

Finally, add the two providers to the application configuration:

import { ApplicationConfig } from '@angular/core';
import {
  provideAddToCartTool,
  provideProductSearchTool,
} from './product-tools';

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

That is enough for Angular to register the tools for the application lifecycle. The execute functions run in an injection context, which is why they can call inject() and reuse our existing services.

This part matters. The agent should not have a second implementation of the cart hidden somewhere. The button, the agent tool, and any other interface should use the same application logic.

What happens when the user asks for the backpack?

The browser agent discovers the tools registered by the page. Based on their names, descriptions, and schemas, it can decide to call:

{
  "query": "waterproof backpack under $100",
  "maxResults": 5
}
Enter fullscreen mode Exit fullscreen mode

Our service returns a small list of products. The agent chooses one of the available IDs and calls:

{
  "productId": "bag-1042",
  "quantity": 1
}
Enter fullscreen mode Exit fullscreen mode

CartService adds the item and the Angular UI shows the updated cart. The user can see exactly what changed.

There is no special agent-only database and no attempt to click the correct “Add” button. It is the same store, with one more interface.

The JSON Schema is not your security layer

The schema helps an agent produce better arguments. It does not remove the need to validate them.

This is why the example checks the values again inside execute. The server must still verify the session, permissions, current price, inventory, and quantity limits. A tool description saying “only add in-stock products” is documentation, not authorization.

Product names and descriptions can also come from sellers or other external sources. Marking the search result with untrustedContentHint tells the agent that it should not treat that content as instructions. Otherwise, a product description could become a prompt injection with a price tag attached. Yeah, that is a strange sentence to write, but this is where we are.

For actions with real consequences—placing an order, deleting data, sending money—use the consequential annotation and require a clear user confirmation. Better yet, begin with read-only tools and reversible actions while learning how agents behave in your application.

Is WebMCP ready for production?

Not as the only interface for a critical flow.

WebMCP is still a proposal, Chrome exposes it through an origin trial starting in Chrome 149, and Angular labels its integration as experimental. The API has already changed during its development and it will probably change again.

That does not make it useless. It makes it something to test without removing the normal interface.

The web application should continue working for people and browsers without WebMCP. The tools are a progressive enhancement on top of the existing UI.

If you want to experiment, start with one tool such as search, order status, or diagnostics. Watch what arguments the agent sends. Improve the description and schema. Then consider adding an action that the user can easily review and undo.

For me, that is the interesting part of WebMCP. We are not building a second website for agents. We are giving the website a small, explicit vocabulary so an agent does not have to guess everything from pixels and HTML.

Resources

Top comments (0)