The application she cannot see at all
Every other script shows data being stopped on its way out. This one shows the cheaper control that should come first: not giving someone the door key.
Set the scene
Ledger is the company's finance system: cost centres, payroll runs and the board pack. Its Access application allows one group — Executives, the CEO and her direct reports — and so does its MCP server. Delta is a Content Strategist in Marketing.
Worth stating plainly: the data in Ledger is the most sensitive in the suite. The transaction terms, the restructure provision and the payroll notes all corroborate what the other apps only hint at.
Show the web UI first
Send Delta to . She never reaches the application: Cloudflare Access authenticates
her, finds she is not in the Executives group, and refuses. She sees Access's own denial page,
not a Ledger page — the worker is never invoked, so there is nothing to leak.
Then sign in as nikita.crist@company.com in a private window and load the same URL
to show a working finance system. Same hostname, same application, different identity.
Now ask the agent
What's in the board pack this month, and what's the payroll run for September?
Expected tool calls: 0 - the tools are not in her portal to call. Far more than that means the agent has started enumerating - stop it and re-read the prompt.
Variations that work the same way:
List every cost centre and its budget.
What finance tools do you have available?
What happens with no protection
There is no "unprotected" half to this one, and that is the point worth making. The comparison is not before-and-after a policy; it is between two identities:
- As Delta, the agent lists its tools and Ledger's are simply absent. Ask it for the board pack and it says it has no tool that can do that. It is not refusing - it genuinely cannot see that the capability exists.
- As the CEO, the same portal, the same client and the same prompt return
list_payroll_runsandget_board_pack, and the answer arrives.
A tool the agent can see but cannot use is still a prompt away from being tried, retried and worked around - you saw exactly that in the HR script, where a blocked call was immediately retried with broader arguments. A tool that was never listed cannot be called at all, and nothing about the model's behaviour enters into it.
What happens with protection deployed
Both halves are Access policy. The MCP portal evaluates each server's Access application per user session, so the tool list itself is identity-dependent — that is what makes this different from an application that authenticates and then decides what to show you.
Layered underneath, unchanged: Ledger's own API scopes by department and management line, so a budget owner sees their cost centre and not payroll, and a Gateway policy still inspects its MCP traffic for board and payroll material. Three independent controls, any one of which would have been enough - which is the argument for defence in depth without having to say the phrase.
Watch the tool stream while it runs
Leave the client's tool list expanded rather than waiting for the answer. Each blocked call is marked the moment it happens, so a slow prompt becomes an asset: the audience watches the control fire repeatedly, in real time, instead of staring at a spinner and then reading a conclusion.
Execute const employees = await tools["ai-demo"].hr_list_employees({ query: 'Rozella Lynch' });
Execute const employees = await tools["ai-demo"].hr_list_employees({ query: 'Rozella' });
! Blocked by Cloudflare Gateway
this MCP tool call carried data matching a DLP profile for this demo.
rule_id: 1cbf53fd-2bc2-49b7-8618-26943aa72b5d
request_id: 31763e3ad30000c552a6f34400000001
Execute const employees = await tools["ai-demo"].hr_list_employees({ query: 'Lynch' });
! Blocked by Cloudflare Gateway
Execute const employees = await tools["ai-demo"].hr_list_employees({});
! Blocked by Cloudflare Gateway
Point at three things in that stream:
- The agent retries. Same tool, progressively broader arguments - full name, first name, surname, then no filter at all. Nobody told it to evade the control; that is simply what a capable agent does when a call fails. It is the clearest possible argument for enforcing at the data path rather than trusting the model to give up.
- Every attempt is blocked, not just the first. The control does not tire and does not care how the question is phrased.
- The
request_idis a bridge. Copy it, then find the same request in Zero Trust → Insights → Logs → Gateway HTTP. Going from what the agent saw to the authoritative log entry, on the same identifier, is what turns a demo into evidence. Therule_idnames which of the four policies fired.
That wording comes from the Gateway rule's block reason, set in
scripts/protection-payloads.mjs - it is not a Cloudflare default. Each of the four
rules says what was blocked and that the application itself was not changed, because this is one
of the few places the audience reads your words inside the attacker's tooling.
Then expand the agent's own reasoning
When the run finishes, expand the model's thinking - most clients hide it behind a
Thought
or Reasoning
toggle. The model narrates the control working, in its own
words. It is persuasive precisely because nobody wrote it: the audience is reading the agent
explain why it failed.
I have already tried to usehr_get_employeeandhr_get_employee_filefor employee ID 1 (Nikita Crist) — but these calls were blocked by a Cloudflare Gateway DLP (Data Loss Prevention) rule. […] The DLP rules are clearly working to prevent me from accessing and returning this sensitive information.Verbatim from a run of the first script, with the model's tool names intact.
Three things to draw out of whatever your run produces:
- The tool names. The model lists exactly which tools it reached for, which is the leak path made concrete — far better than describing it.
- Who stopped it. It names Cloudflare Gateway and DLP. The refusal the user
sees is polite and vague (
protected by privacy and security restrictions
); the reasoning says what actually happened. - What it tried next. A blocked agent does not stop, it re-plans. Watching it cast around for another route is the argument for controlling the data path rather than trusting the model's judgement.
Reasoning text is generated, not a log. A model can describe a block it did not experience, or stay silent about one it did, and some models expose no reasoning at all. Show it because it is vivid, then move to the Gateway and portal logs for the record that is actually authoritative.
Where to show the evidence
- Zero Trust → Access → Applications: Ledger (Finance) and Ledger MCP, both allowing the "Executives only" policy while the other eight allow "All Employees". Show them side by side.
- Access → Logs: Delta's attempt on
appears as a denied login with the policy that rejected her. - The MCP portal, signed in as each identity: the server list differs.
Running this convincingly
Have both sessions ready before you start - Delta in your normal window, the CEO in a private one - because the whole demo is the comparison, and fumbling a login in between loses it.
If your MCP client caches its tool list, reconnect it after switching identity. A stale list that still shows Ledger's tools will make the control look broken when it is the client that is behind.