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.
- LLM / model traffic goes to
, the AI Gateway's own custom domain which is 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. - MCP / tool traffic goes to the MCP portal at
, which fronts all the deployed MCP servers.
Access secures both traffic paths and therefore every request is attributed to the person who made it.
Sign in as the demo user
Every demo is run as Alice Watson —
, password Savetheinternet!1. When the agent connects to the portal you will be sent
through Cloudflare Access and then FlareID.
is the system administrator, and it exists only for the
/admin page on each app. It's also the admin for the IDP if you want to make changes there.
First: authenticate each MCP server (one browser login each)
The script registers five MCP servers with Access and creates an Access application for each, but it cannot perform the upstream OAuth login those servers require. Until it is done each server sits in Waiting and the portal has nothing to offer your agent. Note authenticate as the CEO Nikita Chapman so you can auth all five servers. (Alice has no access to the financial Ledger app)
- 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. - Click on the three dots next to each server → Authenticate server.
- Sign in as
(passwordSavetheinternet!1). Cloudflare fetches the server's tools and the status becomes Ready. - Repeat for
crm,work,finandwiki.
Setting up the clients
Cloudflare OS is deployed, but in my opinion not ready for demo. So you will need to install a local client, instructions below.
- Goose — use this one. Desktop app, native tool calls, and the only client of the three that shows the audience why something was blocked: it surfaces the gateway's own message instead of a bare status code. One OAuth covers every server behind the portal without extra requests.
- opencode — also a desktop app with native tool
calls, and what the walkthrough was originally written against. Workable, but it doesn't show useful
errors, you will see
Provider request failed with HTTP 424and nothing else, so every block becomes a trip to the dashboard to examine the log to understand what happened. It's not very user friendly. - Cloudflare OS — runs in the browser, nothing to install. Its agent works by writing code against its bindings, so expect two steps where the others take one, and a grant per server in each chat. Not the best user experience, but it is totally web based and requires no desktop to demo.
Enrolling a device in this demo's Zero Trust organisation needs configuration.
wire-access.mjs deliberately turns on install CA to system certificate store,
because Gateway cannot inspect HTTPS unless the device trusts its certificate — so the
Cloudflare One client puts this tenant's root certificate into your trust store, and from
then on the demo account can decrypt that device's traffic. Windows it deploys automatically,
on MacOS you need to go to Keychain Access and trust the certificates.
Cloudflare OS is the exception and needs none of this — it runs in a browser and authenticates through Access, so there is no device to enrol and no certificate to install. If you cannot make a VM, that is the client to use.
Client 1 — Goose (preferred)
Everything in this section - the Cloudflare One client, the Goose download, the provider, the portal extension, disabling the bundled extensions, and the keyring setting below - is done by one script. This lab's zone and team name are already baked into it, so it takes no arguments. Run it from an elevated PowerShell:
irm https://demo./goose-setup.ps1 -OutFile goose-setup.ps1
.\goose-setup.ps1
Two things it deliberately leaves to you, because neither can be scripted: signing in to the Cloudflare One client as Alice, and authorising the servers at the MCP portal the first time the agent reaches for a tool.
The steps below are what the script automates. Follow them by hand on macOS, or on Windows if you would rather see what it is doing.
Verified on Goose Desktop 1.53.0. Two things to add, in this order: a custom provider pointing at the AI Gateway, and a Streamable HTTP extension pointing at the portal.
- Add the provider. At first launch choose Connect to a provider →
+ Add a custom provider → Configure manually. Leave the
provider type as OpenAI Compatible, name it anything (Cloudflare works),
and set the API URL to
https:///compat. Leave the API base path empty. - Paste in one model under available models:
@cf/google/gemma-4-26b-a4b-it. - Add the portal. Extensions →
+ Add custom extension, type Streamable HTTP, name it
Company AI Portal, endpoint
https:///mcp. Add extension. - Disable every other extension so the portal is the only one left enabled under Default Extensions. This matters as much here as the deny list does in opencode: Goose ships with developer tooling, and an agent that can read your filesystem will answer from it instead of from the company's systems.
- Send any prompt. You are redirected to the portal once, where you authorise the servers — and then never asked again. Note that Alice does not get to auth the MCP server for the finance app, because the Access policy limits her.
On Windows the portal extension fails with “Failed to load extension” —
or, in the CLI, Auth required, when send discover request — even though the browser
says Authorization Success. The authorization did succeed; Goose then discarded it, because
Windows Credential Manager caps a credential at 2560 characters and Cloudflare's OAuth
tokens are longer than that. macOS Keychain has no such limit, which is the whole of the difference.
Goose logs the real cause as a warning that never reaches the screen:
Failed to save credentials: Failed to access keyring: Attribute 'password encoded as UTF-16'
is longer than platform limit of 2560 chars
So store credentials in a file instead. One line in
%APPDATA%\Block\goose\config\config.yaml — the same file the extension lives in,
and it covers the desktop app and the CLI together:
GOOSE_DISABLE_KEYRING: true
Or for a single terminal: $env:GOOSE_DISABLE_KEYRING = "1".
The token then sits unencrypted in secrets.yaml next to that config. For a
throwaway demo tenant that is a fair trade; do not carry the setting to a machine whose credentials you
care about.
This is the practical difference from Cloudflare OS, and worth knowing before you choose. Goose completes a single OAuth flow against the portal and can then call every server behind it, for the life of the token — no per-server grant, no per-chat grant, nothing to redo before a second run. Cloudflare OS instead issues a capability per server, scoped to the chat it was granted in.
Both are defensible, and the per-capability model is arguably the better security story.
Client 2 — opencode
Read this before choosing opencode. When the gateway refuses a call, opencode reports the whole of it as:
Provider request failed with HTTP 424
No reason, no policy name, nothing to read out. The gateway does send one — "Request content blocked due to DLP policy violations" — and opencode discards it. Goose prints it:
Request failed with status 424 Failed Dependency at
https://aig.<your-zone>/compat/v1/chat/completions:
Request content blocked due to DLP policy violations.
Six of the walkthrough's eight steps end in something being refused with an unclear UX. On opencode every one of those becomes you switching to the AI Gateway dashboard to explain what just happened, which works but isn't an ideal demo of the user experience.
Then two things to configure: a provider against AI Gateway's OpenAI-compatible endpoint, and an MCP server against the portal. There are two configuration shapes below, and do not pick between them by version number — that is the trap.
Verified on opencode Desktop 2.0.22: the 1.x configuration works and the 2.x one does not. The desktop app's version number is its own, and it does not tell you which config schema it reads — a "2.0.x" desktop build still wants the 1.x shape.
opencode 1.x — and the Desktop apptop-level permission map · verified on Desktop 2.0.22 and on the 1.18.x CLI
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./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./mcp",
"codemode": false
}
}
},
"permission": {
"execute": "deny",
"bash": "deny",
"edit": "deny",
"read": "deny",
"glob": "deny",
"grep": "deny",
"webfetch": "deny",
"websearch": "deny",
"subagent": "deny",
"skill": "deny"
}
}
opencode 2.x CLI onlyagents with a permissions array · rejected by Desktop 2.0.22
In ~/.config/opencode/opencode.json. Only for the 2.x CLI —
the Desktop app rejects this shape, including Desktop 2.0.22, so use the card above for it.
{
"$schema": "https://opencode.ai/config.json",
"provider": {
"cf-ai-demo": {
"npm": "@ai-sdk/openai-compatible",
"name": "Cloudflare AI Gateway (demo)",
"options": {
"baseURL": "https://aig./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./mcp",
"codemode": false
}
}
},
"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. Call the tool that matches the question directly - do not search for tools and do not write code. If the tools cannot answer the question, say so plainly rather than guessing.",
"permissions": [
{ "action": "execute", "resource": "*", "effect": "deny" },
{ "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" }
]
}
}
}
Client 3 — Cloudflare OS
./deploy.sh deploys
Cloudflare OS
at , and the five apps carry a Company AI tile in their launcher
pointing at it. Set DEPLOY_CLOUDFLARE_OS="false" to skip it.
Worth knowing why you might prefer it: every model call it makes routes through the AI Gateway named in its config, so the DLP profiles and guardrails apply to every agent turn rather than only the leg a desktop client chooses to send, and it authenticates users through Access, so the agent runs as the signed-in person.
It needs three things done once per person, in the browser, and no API can do any of them — a model and a connector both belong to a user account and are configured over that user's own session:
- Add the model. Settings → Choose your model → Add new model →
Other Cloudflare Workers AI…, then the bare id
@cf/google/gemma-4-26b-a4b-it. No key is requested: gateway mode authenticates on the Workers AI binding. The id takes noworkers-ai/prefix here — that belongs to other clients' naming. - Connect the portal account. On
/gatekeepers, add the portal. It runs an OAuth flow against Access. This connects the account and grants no servers — see the next step, which is not on that page. - Grant the servers you need, one at a time. Open a chat, click the
+ in the message composer (the control labelled "Files, connections and skills"),
then Add a new connection. Choose Company AI Portal, keep the
account already connected, pick a Server, choose All tools, and
confirm. Repeat per server. The picker lists five similar invented product names, so pick by what
the step needs rather than by what sounds right:
- WorkBox MCP — inbox and calendar. Step 4 needs this one.
- Pipeline MCP — the CRM. Step 8 needs this one.
- WorkWeek MCP — HR. Only for the per-app scripts.
- Nexus MCP — the wiki. Only for the per-app scripts, and the one most easily picked by mistake when you wanted the calendar.
- Ledger MCP — finance. Do not grant it; Alice is not entitled to it and its absence is the point of step 8.
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./mcp" },
"pipeline": { "type": "remote", "url": "https://crm-mcp./mcp" },
"relay": { "type": "remote", "url": "https://work-mcp./mcp" },
"nexus": { "type": "remote", "url": "https://wiki-mcp./mcp" },
"ledger": { "type": "remote", "url": "https://fin-mcp./mcp" }
}
}
}
Each server runs its own OAuth 2.1 flow and delegates login to Cloudflare Access, so you will sign in
as Alice 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 Alice Watson 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.