Modern Angular
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.
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 does | Default call | Guardrail |
|---|---|---|
| Reads non-sensitive data | Expose | Still validate inputs and scope to the user |
| Reads PII or internal data | Expose narrowly | Return the minimum; authorize per record |
| Mutates state (update, cancel) | Human in the loop | Agent prepares, the user confirms before commit |
| Money, identity, destructive, access grants | Do not expose yet | Keep 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
- https://next.angular.dev/ai/webmcp
- https://next.angular.dev/api/core/provideExperimentalWebMcpTools
- https://webmachinelearning.github.io/webmcp/
- https://angular.dev/ai/mcp
- https://angular.dev/guide/forms/signals/overview
- https://angular.dev/reference/releases
- https://owasp.org/www-project-top-10-for-large-language-model-applications/
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.
André Ramosavailable for remote roles, UTC−3Get in touch →