AI Demo Cloudflare AI security demo

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.

Two endpoints, and no API key

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 — alice.watson@company.com, password Savetheinternet!1. When the agent connects to the portal you will be sent through Cloudflare Access and then FlareID.

Using the admin account

admin@company.com is the system administrator, and it exists only for the /admin page on each app.

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.

  1. 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.
  2. Click on the three dots next to each server → Authenticate server.
  3. Sign in as nikita.chapman@company.com (password Savetheinternet!1). Cloudflare fetches the server's tools and the status becomes Ready.
  4. Repeat for crm, work, fin and wiki.
Do not authenticate as admin

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.

Setting up the clients

Two clients are documented below and you only need one. Both reach the same two endpoints, so every demo script works with either.

Client 1 — opencode

First: a desktop with the Cloudflare One client, signed in as Alice

opencode runs on your machine, and that machine has to be enrolled in the Cloudflare One client and authenticated as alice.watson@company.com. Not optional, and not just for tidiness — it is what makes the configuration below work at all.

The device session is the credential. The Access application in front of is configured to accept it, which is why the provider config has no API key in it: nothing in a file, nothing in an environment variable, nothing on disk. Without the client, every model call is turned away by Access before it reaches the gateway.

Signed in as the wrong person is worse than not signed in, because it looks like it works: the requests succeed and every one of them is attributed to whoever that is. The walkthrough's argument is that the agent acts with Alice's identity, so check the client's account before you present, not after.

The same session is what makes step 1 happen: the browser-side DLP policy and the desktop notification explaining the block both come from this client. On a machine without it, cloudflared can fetch a short-lived Access token instead, and opencode can run that for you — but you lose step 1, so for a demo it is not a real substitute.

One deploy-time caveat, because it fails the same way as a missing client. Accepting a device session needs an account-wide Cloudflare One Client Authentication duration to exist first, and wire-access.sh degrades rather than failing if it cannot set one — it prints "Without client-session auth: clients authenticate with 'cloudflared access login' instead". If you saw that, the flag is not on the application and no device session will authenticate, however correctly the client is signed in. Turn it on under Zero Trust → Access controls → Access settings and re-run.

Cloudflare OS needs none of this: it authenticates in the browser, so a signed-in Access session in any tab is enough.

Then two things to configure: a provider against AI Gateway's OpenAI-compatible endpoint, and an MCP server against the portal. Both are below, but the configuration shape changed between opencode 1.x and 2.x — so check which one you have before copying anything:

opencode --version
opencode 1.x1.18.x and earlier · top-level permission map

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.xagents with a permissions array

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
      }
    }
  },
  "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 2 — 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, in which case the tile is left out rather than linking somewhere that does not resolve.

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:

  1. 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 no workers-ai/ prefix here — that belongs to other clients' naming.
  2. 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.
  3. Connect the portal in one pass. Open a chat, click the + in the message composer (the control labelled "Files, connections and skills"), then Add a new connection. Choose MCP Server, paste the portal endpoint — with /mcp on the end — then pick All tools and confirm. One grant, every server behind the portal, including any added later.
Two MCP connectors are installed, and only one of them is quick

MCP Server is the one to use. It takes a pasted endpoint and will grant all of it at once, which is why the step above is a single pass.

Company AI Portal — the connector named after your portal — is the administrator-configured one, and its resource dialog grants one upstream server per connection: it builds exactly one resource URL per grant (<endpoint>#server=work) and refuses to build one without a single server chosen. Five servers means five passes. Both reach the same portal with the same Access login and the same per-user identity, so for a demo the first is simply faster.

What you give up is narrower than it sounds. The portal connector carries the vetted trust tier, the only one on which an approved write may auto-apply; a pasted endpoint is byo. readOnlyHint is honoured on both, so reads still run without an approval prompt either way, and nothing in this demo wants writes auto-applied.

And a whole-portal grant does not hand Alice more than she should have: the portal only lists the servers her Access session entitles her to, so Ledger never appears and step 8's "the finance tools are not even listed" still holds. Worth confirming on the day by noting what the picker shows.

Do the grants before the first prompt, not during it

Skip that and the agent meets the portal for the first time mid-demo. It has no connection, so it reasons about that at length, calls listConnectableResources, and asks you to set one up — interesting once, and a poor way to open a walkthrough whose argument depends on the tools being unremarkable.

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.

Tool names change when you go through the portal

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.