WebMCP Security in Angular: The Agent Calling Your Tools Can Be Hijacked

A WebMCP tool is a function in your shipped Angular app that an AI agent can call without a human clicking. The catch nobody designs for: that agent reads pages, so it can be steered by content the user never trusted, and your execute runs with the user's session. Angular does not validate agent inputs against your schema, so the tool surface is an authenticated endpoint with a caller you do not control.

The threat model in one sentence

A WebMCP tool is a capability you handed to whatever AI agent the user is running, and that agent reads pages. The whole risk follows from that. The agent can be carrying instructions it picked up somewhere the user never vetted, a review, a comment, a support thread, a PDF, and those instructions can tell it to call your findOrder or cancelSubscription tool. This is prompt injection reaching a real function in your app, and the tool cannot tell a request the user meant from one a hostile page planted.

Now add the part that makes it sharp. Your execute runs in the user's browser, inside the user's session, with the user's cookies and tokens. A hijacked agent calling your tool acts with the user's authority, on instructions the user never gave. That is the frame for every decision below: treat each exposed tool as a public, authenticated endpoint whose caller is hostile, because functionally that is what it is.

The mechanics of registering tools, bootstrap, route, service, or a Signal Form, are in the WebMCP setup guide. This is the security layer that sits on top of them, and it is the part I would not ship without.

The schema is documentation, not a trust boundary

The first instinct is to lean on the inputSchema. Do not. Angular is explicit that it does not validate that the inputs an agent provides actually match the declared JSON schema. The schema tells the agent how to call the tool; it stops nothing. A hijacked or simply confused agent can hand your execute a string where you declared a number, an id for a record this user cannot see, or a payload three fields longer than you expected.

So validation is your job, at the top of every execute, before the inputs touch anything. Parse, do not assume. Reject what you did not declare. Check that the id belongs to something this user is allowed to read. It is the same posture you would bring to a request body from an untrusted client, because the agent is exactly that.

Validate inside execute; the schema did notts
execute: (raw) => {
  // Angular does not enforce inputSchema. Validate before anything else.
  const orderId = String(raw?.orderId ?? '');
  if (!/^[A-Z0-9-]{6,20}$/.test(orderId)) {
    return { content: [{ type: 'text', text: 'Invalid order id.' }] };
  }

  const orders = inject(Orders);
  // Authorize for THIS user on the server, not just here in the browser.
  return { content: [{ type: 'text', text: orders.summaryForCurrentUser(orderId) }] };
},

Least privilege: read before write, confirm before harm

The cheapest security control is not exposing the tool at all. An agent that can look up an order is a different risk from one that can cancel it. Start read-only. A tool that reads and returns data has a bounded blast radius; a tool that mutates, charges, deletes, or grants access hands an automated, possibly-hijacked caller a lever on your business.

When a tool has to mutate, put a human in the loop. The agent can prepare the action, fill the form, name the order to cancel, but a person confirms before it commits. Exposing a Signal Form as a tool fits this cleanly: the agent populates fields and the user reviews and submits, instead of the agent calling a fire-and-forget execute.

Keep what you return tight, too. A tool that answers findUser and returns the whole record just made every field, email, phone, internal flags, reachable by the agent and whatever is steering it. Return the minimum the task needs, not the row.

What the tool doesDefault callGuardrail
Reads non-sensitive dataExposeStill validate inputs and scope to the user
Reads PII or internal dataExpose narrowlyReturn the minimum; authorize per record
Mutates state (update, cancel)Human in the loopAgent prepares, the user confirms before commit
Money, identity, destructive, access grantsDo not expose yetKeep it off the WebMCP surface while this is experimental

Authorize on the server, scope on the client

Two controls, two places, and they are not interchangeable. On the client, exposedTo scopes which origins may call a tool, the same instinct as CORS, and an AbortSignal tears a tool down when its context ends. Set exposedTo to the narrowest set you can justify. But it is a client-side fence, and client-side fences do not authorize.

The authorization that counts is on the server. Your execute runs with the user's session, so the call reaches your backend already authenticated as the user. That is exactly why you re-check, server-side, that this user may do this thing to this record, on every call, the same as any other request. The browser is not a trust boundary, and an agent inside the browser is less of one.

Watch the lifecycle while you are at it. A tool that outlives its route is exposed surface you forgot you had. Route-scoped cleanup is the mechanism for that, and the open duplicate-tool bug around it is a good reminder to actually test that tools unregister when you navigate away, instead of assuming they did.

What I would not ship yet

All of this rides on an experimental API over a draft spec. The entry point already moved from navigator to document.modelContext mid-cycle, and the docs warn APIs can change outside major versions. For security that doubles the caution: you are hardening a surface whose shape can shift under you, so anything you expose is something you re-audit on every Angular minor, not just the majors.

Where I land: spike read-only tools behind a flag, on internal or low-stakes routes, and treat the security work as the actual deliverable, the input validation, the server-side authorization, the human-in-the-loop pattern for anything that writes. Those survive the API renames. What I would not put on the public app is a tool that moves money, changes identity, or does anything destructive, while the caller is an agent that can be talked into anything and the spec underneath is still a draft. The idea is good. The caller is not trustworthy yet.

Reusable artifact

WebMCP security checklist for Angular

  • Treat every exposed tool as a public, authenticated endpoint with a hostile caller.
  • Validate every execute input at runtime; Angular does not enforce the JSON schema.
  • Start read-only; do not expose tools that move money, change identity, or destroy data yet.
  • Put a human in the loop for any mutation: the agent prepares, the user confirms.
  • Re-check authorization server-side on every call; the browser session is not a trust boundary.
  • Set exposedTo to the narrowest origins, and confirm route-scoped tools actually unregister.
  • Return the minimum data the task needs; a wide return is a data-exfiltration path.
  • Re-audit the exposed surface on every Angular minor, not just majors.

FAQ

What is the WebMCP-specific risk, beyond normal web security?

The caller is an AI agent that reads pages, so it can be carrying injected instructions from content the user never trusted. Your execute runs with the user's session, so a hijacked agent acts with the user's authority on instructions the user never gave.

Is the JSON schema enough to validate agent inputs?

No. Angular states it does "not provide any implicit validation that the inputs provided by an agent actually match the defined JSON schema." Validate arguments at the top of every execute and reject anything you did not declare.

Does exposedTo make a tool safe?

No. exposedTo scopes which origins may call a tool, like CORS, but it is a client-side control, not authorization. Re-check on the server that this user may perform this action on every call.

Can I expose a payment or delete tool with WebMCP?

Not yet. While the API is experimental and the caller can be hijacked by prompt injection, keep tools that move money, change identity, or destroy data off the surface, or gate them behind explicit human confirmation.

Sources checked

Modern Angular Playbook

This article is one play.

The Modern Angular Playbook collects the diagnostic, the adoption matrix, eleven plays, and the 30-day plan. Free, in English and Portuguese.

You get both PDFs by email, through the Dojo IA list.

Open playbook →

André Ramosavailable for remote roles, UTC−3Get in touch →

Related

Guide · 12 min

WebMCP in Angular: How to Expose Tools to the Browser's AI Agent (and When to Wait)

How Angular exposes WebMCP tools to a browser AI agent (bootstrap, routes, services, and Signal Forms), plus the security surface that opens and where I would still wait before shipping.

Read article →

Note · 6 min

WebMCP vs the Angular CLI MCP Server: Runtime Tools vs Dev-Time Help

Which Angular MCP you actually want: the dev-time CLI MCP server that helps an assistant write your code, or runtime WebMCP that lets a browser agent call your shipped app.

Read article →

Guide · 7 min

Turning Signal Forms into AI Agent Tools

How Angular turns a Signal Form into an agent-callable tool, what its schema inference needs from your model, and where I would use it before it leaves experimental status.

Read article →