Shield 3: Secure MCP
Path: agent → tools. The stretch one.
Your Worker serves its own MCP server at /mcp. Right now anyone with the URL can list and
call every tool. Authentication tells you which workload connected; it doesn't decide which
tools it should get. Shield 3 does both, and (as a stretch) checks what is sent through the portal.
agent ── service token ──► Access ──► MCP server portal ──► your /mcp
│ only approved tools
└─ Gateway: logs + DLP on tool calls
Steps
Everything is in Zero Trust in your own team account, and every name below is the same in every
team's account. Your demo hostname (TEAM_HOSTNAME) and portal hostname (TEAM_PORTAL_HOSTNAME) are
on your team card. In the first command, DEMO_URL is your demo URL, set once per terminal as in
Step 1.
1. Prove the door is open
curl -s -X POST "$DEMO_URL/mcp" \
-H 'content-type: application/json' -H 'accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
You get your tool list back with no credentials. That's the before.
2. Give the agent an identity
- Access controls → Service credentials → Service tokens → Create.
Name
agent-token. Copy the Client ID and Secret now, you won't see the secret again. - Access controls → Policies → Add a policy. Name
agent service auth, action Service Auth, include Service Token =agent-token.
3. Register your server and build a portal
- Access controls → MCP Portals → MCP servers tab → Add an MCP server:
name
agent tools, Server IDagent, URLhttps://TEAM_HOSTNAME/mcp, authentication None, policyagent service auth. Select Save and connect server and wait for Ready. - Create the portal (Add MCP server portal): name
agent portal, under Custom domain pick your zone and the portal hostname from your team card, which becomeshttps://TEAM_PORTAL_HOSTNAME/mcp(a different name from your demo hostname). Addagent toolsand the same policyagent service auth. Turn Require user auth off (a service token can't do an interactive login) and leave Code Mode Off.
Tools are now namespaced by Server ID, e.g. agent_ask_agent.
4. Call it through the portal
The starter does not connect to the portal itself; you test the portal with a call that carries
the service token. Without a token the portal should refuse you. With it, you get your tool back.
Replace TEAM_PORTAL_HOSTNAME, <CLIENT_ID> and <CLIENT_SECRET> first.
curl -s -X POST https://TEAM_PORTAL_HOSTNAME/mcp \
-H 'content-type: application/json' -H 'accept: application/json, text/event-stream' \
-H 'CF-Access-Client-Id: <CLIENT_ID>' \
-H 'CF-Access-Client-Secret: <CLIENT_SECRET>' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
You should see agent_ask_agent (the server ID is now part of the name). Run it again without
the two headers to see the refusal. The full walk-through, with both shells, is in
Step 6.
5. Switch the tool off
Portal → Edit → under Servers pick your server → Tools → turn off ask_agent →
Save. Wait about 20 seconds and run the token call again: agent_ask_agent is gone. The
starter has only one tool; to see one allowed and one refused, ask your coding agent to add a
second tool next to ask_agent in src/mcp.ts, redeploy, select Sync capabilities on the
server, then switch off only one.
6. About the direct path
Your own https://TEAM_HOSTNAME/mcp still answers anyone, because the portal sits in front of
the server rather than replacing it. Closing it means making Access the server's own sign-in (or
checking the Cf-Access-Jwt-Assertion header in your /mcp handler), which is more than this
shield covers, and an Access application placed on /mcp could also stop the portal from
reaching your server. For the demo, show the portal path, and say plainly that the direct path is
still open.
7. Turn on Gateway and DLP (stretch)
- Edit the portal → Route traffic through Cloudflare Gateway on. Tool calls now show in Insights → Logs → HTTP request logs.
- Traffic policies → Firewall policies → HTTP → Add a policy, name
agent block cards: HostinTEAM_HOSTNAMEand DLP ProfileinFinancial Information→ Block. The host is your server's own address, not the portal address. - Through the portal, ask the tool a question that contains the synthetic card number, e.g. ask
ask_agent:my card is 4111-1111-1111-1111, what should I buy?You should see a blocked error instead of an answer. Use a standard DLP profile: the AI prompt profiles do not apply to MCP traffic.
Gateway HTTP policies apply to your whole account. A policy without a Host condition would
also inspect every other request your account sends through Gateway.
Before / after
| Test | Before | After |
|---|---|---|
tools/list with no credentials | Full tool list | Refused by Access |
| Portal call with the service token | n/a | Works (lists agent_ask_agent) |
After switching ask_agent off | Listed | Not listed |
| Card number in a tool call (stretch) | Answered | Blocked by Gateway DLP |
What to capture (for your demo, if you want one)
- The unauthenticated
curlrefused. - The token call listing
agent_ask_agent, and the same call after you switch the tool off. - (Stretch) The HTTP request log entry for the DLP block.
Troubleshooting
The server stays in Waiting or Error
- URL must end in
/mcpand speak Streamable HTTP (the starter's does). - Authentication None at the server; Access does the auth.
- Select Sync capabilities after fixing.
The agent connects but sees no tools
- Attach
agent service authto both the server and the portal. - User auth required and Code Mode off.
- Tool names changed: they are now
agent_<tool>. - Access, custom domains and Zero Trust setup depend on your role, which should be Administrator. If a menu is missing or a save is refused, ask a host.
A disabled tool still works
Did you select Save? Wait 20 seconds for sessions to pick up the change, then retry.