Setting up the agent
The demo needs an AI client with two things wired up: a model reached through Cloudflare AI Gateway, and the five MCP servers reached through the MCP server portal. These are the two places the controls live.
- Model traffic goes to
, the AI Gateway's own custom domain, whichwire-protection.shputs behind Cloudflare Access. On a custom domain, AI Gateway accepts a valid Access JWT as the request credential, so the client sends no gateway token and no API key at all. - Tool traffic goes to the MCP portal at
, which fronts,,and.
Both are Access applications on the same identity provider, so one login covers the lot, and
every request — model and tool — is attributed to the person who made it. AI Gateway
records the authenticated user as cf.user_id, which means logs, analytics and spend
controls are per-user without the client passing an identity.
Sign in as the right person
Every demo is run as Delta Graham — delta.graham@company.com,
password Savetheinternet!1. When the agent connects to the portal you will be sent
through Cloudflare Access and then FlareID; log in as Delta, not as the admin account. The whole point
is that the agent is acting with a real, low-privilege identity.
First: authenticate each MCP server (one browser login each)
wire-protection.sh registers the five MCP servers with Access and creates an Access
application for each, but it cannot perform the upstream OAuth login those servers
require — that is an authorization-code flow with a browser in the middle, and no API mints
that token. Until it is done each server sits in Waiting with zero tools, and the
portal has nothing to offer your agent.
- In the Cloudflare dashboard, go to Zero Trust → Access controls → MCP Portals
→ MCP servers. All five (
hr,crm,work,wiki,fin) should be listed, each showing Waiting. - Select a server → Edit → Authenticate server.
- Sign in as
delta.graham@company.com(passwordSavetheinternet!1). Cloudflare then fetches the server's tools and the status becomes Ready. - Repeat for
crm,workandwiki. fin(Ledger) is the exception: it only admits the leadership team, so authenticate that one asnikita.crist@company.com. Delta cannot authenticate it, and that is the control the access-control script demonstrates — once it is Ready, her portal still will not list its tools.- Check the portal lists tools from all five when signed in as the CEO, and from four as Delta.
admin@company.com exists in FlareID but is not an employee in any of the
apps, and every MCP server maps the Access identity to an employee record before it will
issue a token. Authenticate as admin and the flow dies at the token exchange with a generic
"Failed to retrieve authentication tokens" — the useful message
("no matching active employee was found") is produced by the MCP server but never surfaced.
There is a second reason to use the demo persona. Whoever you authenticate as becomes that server's admin credential, which is what the portal falls back to if Require user auth is ever turned off. Using the lowest-privileged person in the company means that misconfiguration fails safe, instead of silently granting every portal user the session of whoever happened to set it up.
Check its error text in the dashboard. If it mentions Cloudflare Gateway, the tool sync was
blocked by the DLP policies this demo installs: a tool catalogue is inspected like any other
response, so a tool whose description happens to contain the vocabulary in a DLP profile
will block tools/list itself — and then no server can ever finish syncing.
Deploy with PROTECTION_MODE=log, authenticate and sync the servers, then re-run
with PROTECTION_MODE=block. Capabilities are cached once synced, so enforcement can
go straight back on. It is also worth keeping tool descriptions free of the exact terms your
profiles match — a description should say what a tool returns without reproducing the
sensitive language it returns.
Option A — opencode
Add a provider pointing at AI Gateway's OpenAI-compatible endpoint and an MCP entry pointing at the
portal. In ~/.config/opencode/opencode.json:
{
"$schema": "https://opencode.ai/config.json",
"provider": {
"cf-ai-demo": {
"npm": "@ai-sdk/openai-compatible",
"name": "Cloudflare AI Gateway (demo)",
"options": {
"baseURL": "https://aig.<ZONE>/compat"
},
"models": {
"workers-ai/@cf/google/gemma-4-26b-a4b-it": {
"name": "Google Gemma 4 (Workers AI)"
}
}
}
},
"mcp": {
"servers": {
"ai-demo": {
"type": "remote",
"url": "https://mcp.<ZONE>/mcp"
}
}
},
"default_agent": "employee",
"agents": {
"employee": {
"description": "An ordinary employee's assistant - company tools only",
"mode": "primary",
"steps": 10,
"system": "You are the assistant of an employee at this company. Answer using only the company tools available to you. You have no filesystem, no shell and no access to the public internet. If the tools cannot answer the question, say so plainly rather than guessing or searching.",
"permissions": [
{ "action": "shell", "resource": "*", "effect": "deny" },
{ "action": "edit", "resource": "*", "effect": "deny" },
{ "action": "read", "resource": "*", "effect": "deny" },
{ "action": "glob", "resource": "*", "effect": "deny" },
{ "action": "grep", "resource": "*", "effect": "deny" },
{ "action": "webfetch", "resource": "*", "effect": "deny" },
{ "action": "websearch", "resource": "*", "effect": "deny" },
{ "action": "subagent", "resource": "*", "effect": "deny" },
{ "action": "skill", "resource": "*", "effect": "deny" }
]
}
}
}
opencode is a coding agent: out of the box it has a shell, a file reader and a web search, and
it is running inside a project directory. Ask it What is Nikita Crist's home address?
with
those available and it will cheerfully grep your filesystem and search the web — six tool
calls, no MCP, and a demo that proves nothing. The agent above denies every built-in action, so
the company's MCP tools are the only way to answer anything.
The system prompt matters as much as the permissions: it tells the model these
are company systems and that "I can't find that" is an acceptable answer. Without it a model that
has just been denied a web search will keep trying other routes.
steps: 10 is a seatbelt. If a prompt ever sends the agent enumerating records one
at a time, it stops rather than grinding through forty tool calls in front of an audience.
opencode speaks Streamable HTTP MCP directly and runs the OAuth flow itself — it sees the
portal's 401, follows the resource_metadata hint, registers itself
dynamically and stores the token. So there is no mcp-remote and no subprocess.
The older {"type": "local", "command": ["npx", "-y", "mcp-remote@latest", ...]}
recipe works from a terminal but fails in the desktop app with
NotFound: ChildProcess.spawn, because a GUI application does not inherit your
shell's PATH and npx usually lives somewhere like
/opt/homebrew/bin. If you do want that form, give the absolute path to
npx.
Note the nesting: opencode 2.x expects servers under mcp.servers, and agent
permissions as the ordered permissions list shown above. On 1.x, servers sit
directly under mcp and tools are switched off with the older
tools/permission maps instead - the 2.x docs are explicit that those
legacy fields should not be used in new configuration. Equivalent CLI for the server, which
writes the right shape for your version:
opencode mcp add ai-demo --global --url https://mcp.<ZONE>/mcp
The machine is enrolled in the Cloudflare One client and already signed in,
and the Access application in front of is configured to accept that client
session (allow_authenticate_via_warp). So the device's existing session authorises
the request, and there is no credential in the config file, in an environment variable, or on
disk anywhere.
On a machine without the client, use cloudflared to fetch a short-lived Access
token instead — opencode can run it for you through the auth.command field of
a discovery file. Cloudflare documents that pattern under
AI Gateway → Integrations → coding agents.
cloudflare-ai-gateway is a real provider id in
models.dev, with a catalogue of the 47 third-party models AI
Gateway can proxy. Name your provider that and opencode matches it by id, merges that catalogue,
and ignores the models map you wrote: the picker fills up with Claude, GPT and Qwen
entries, and the app starts probing models you have no provider keys for. Any id that isn't in
models.dev - cf-ai-demo here - avoids it. Restart opencode after editing, since it
caches the catalogue.
Reasoning models spend their token budget on reasoning before they emit any text, and the
OpenAI-compatible shape does not surface that. Called directly with a small
max_tokens, @cf/google/gemma-4-26b-a4b-it returns 200 with
finish_reason: "length" and an empty content string. It looks like a broken
gateway and isn't.
If you see empty replies, raise the token budget, or switch the model id to
workers-ai/@cf/meta/llama-3.3-70b-instruct-fp8-fast, which answers immediately and
is a safe fallback for a live demo.
On first use the portal returns 401 and opencode opens a browser window for the Access
login. After that its token is stored. If the server list shows needs authentication,
run /mcps, select it and sign in.
Option B — Open WebUI
- Model. Settings → Connections → add an OpenAI-compatible connection. Open WebUI
runs server-side, so it has no Cloudflare One client session to borrow: point it at
https://gateway.ai.cloudflare.com/v1/<ACCOUNT_ID>/employee-gateway/compatwith a Cloudflare API token that has Workers AI access, or give it an Access service token againsthttps://aig.<ZONE>/compat. Use the same model id as above,workers-ai/@cf/google/gemma-4-26b-a4b-it. - Tools. Bridge the MCP portal to an OpenAPI tool server and add it under Settings → Tools:
uvx mcpo --port 8000 -- npx -y mcp-remote@latest https://mcp.<ZONE>/mcp
Recent Open WebUI builds can also add a streamable-HTTP MCP server directly; either path works, the portal does not care. - Access. Open WebUI itself sits behind its own Cloudflare Access application with SSO, so the person typing prompts is already authenticated before they reach it.
Running without the protection layer first
If the suite was deployed with DEPLOY_PROTECTION=false there is no portal and no AI
Gateway yet. Point the client at the MCP servers individually and at the model provider directly:
{
"mcp": {
"servers": {
"workweek": { "type": "remote", "url": "https://hr-mcp.<ZONE>/mcp" },
"pipeline": { "type": "remote", "url": "https://crm-mcp.<ZONE>/mcp" },
"relay": { "type": "remote", "url": "https://work-mcp.<ZONE>/mcp" },
"nexus": { "type": "remote", "url": "https://wiki-mcp.<ZONE>/mcp" },
"ledger": { "type": "remote", "url": "https://fin-mcp.<ZONE>/mcp" }
}
}
}
Each server runs its own OAuth 2.1 flow and delegates login to Cloudflare Access, so you will sign in
as Delta once per server — except ledger, which will refuse her, because its Access
application allows only the leadership team. Every demo script works in this mode
— that is the "before" half of each one.
The portal namespaces every tool with its server id, so list_employees becomes
hr_list_employees, get_pipeline_summary becomes
crm_get_pipeline_summary, and so on with work_, wiki_ and
fin_. The
demo scripts name the underlying tool; your transcript will show the prefixed one.
Check it works
Before running any script, ask the agent something harmless that proves both legs are live:
Who am I, and which tools do you have available?
You should see Delta Graham come back from the whoami tool, a list of tools from the four
apps she can reach — Ledger's will be absent, by design — and, if the protection layer is
deployed, a corresponding request in the AI Gateway log and in the MCP portal log.