MCP Headers Make Agent Traffic Legible, Not Governed
Cloudflare's MCP traffic detection and the stateless MCP spec expose agent tool calls in ordinary HTTP headers. Detection is now cheap; enforcement is not.
Two changes landed within a week of each other, and together they move a governance problem into ordinary infrastructure.
On 12 August, Cloudflare added MCP protocol detection to Gateway policies: a new Is MCP selector, an AI security report dashboard, and an example rule that blocks detected MCP traffic which did not arrive through an approved portal. On 14 August, Cloudflare explained how the detection actually works — and the honest answer is that it leans on protocol changes made by the MCP 2026-07-28 specification, which removed the session handshake and pushed the protocol version, operation, and tool name into HTTP headers on every request.
That second detail is the story. Agent traffic used to be operationally shapeless — a tool call looked like any other HTTPS API request. The stateless rewrite made MCP traffic announce itself on the wire, so any TLS-inspecting proxy, load balancer, or WAF can now classify it without an AI-specific product, a known hostname list, or body parsing.
The repeated angle to avoid
The ten most recent posts here covered ECH and certificate evidence, deployment-level authentication, Shopify checkout fields, agent-plugin trust, agent context copies, Turnstile and lead quality, server-side agent conversion tracking, AI work receipts, prototype exits, and Copilot billing handovers. Older overlapping posts covered MCP authorization, agent harnesses, gateway budgets, and permission controls.
The weak version of this article is the old formula: MCP traffic needs zero-trust governance. That take treats detection and enforcement as one thing and reaches for a security product. The sharper thesis is that the protocol changed shape underneath the tooling. Stateless MCP turned tool calls into self-describing HTTP requests, which commoditised detection — and simultaneously exposed exactly where enforcement did not get easier, because each of the three usable control points sees a different slice of the same call.
Fresh evidence and background context
The source map separates this week's announcements from the protocol change that made them possible:
| Source | Freshness | What it contributes |
|---|---|---|
| Cloudflare: how it detects MCP traffic | 14 August 2026 | The detection heuristics, the three control points, shadow versus bypass framing, per-tool risk tiers, and portal enforcement |
| Cloudflare docs: MCP protocol detection and AI Security dashboard | 12 August 2026 | The experimental.is_mcp selector (beta), the example block rule combining Is MCP with Traffic Source, and the dashboard location |
| Cloudflare tutorial: detect MCP traffic in Gateway logs | Current primary documentation | The GraphQL log scan and DLP-pattern approach that works on historical traffic and older clients |
| MCP specification 2026-07-28 changelog | Protocol reference, July 2026 | Stateless core (no initialize handshake), required Mcp-Method/Mcp-Name headers on POSTs, x-mcp-header, trace-context conventions, cacheable list results |
| MCP specification: version negotiation | Protocol reference, July 2026 | Per-request version in _meta, carried on HTTP as MCP-Protocol-Version |
| Cloudflare: the next generation of MCP | Background, 6 August 2026 | Platform-side context for the stateless rewrite and authorisation model |
The new information surplus sits between these sources: Cloudflare's post tells you what its product does with the protocol's signals, the specification tells you which signals are mandatory versus optional, and neither source separately gives you the coverage map of what each enforcement layer actually sees. That map is what an operator needs before buying or building anything.
One tool call, three forms, three sets of evidence
The same MCP tool call exists in three forms as it moves through a system, and each form leaks different evidence:
inside the client: a model decision — tool name + arguments
on the network: an HTTP transaction carrying a JSON-RPC message
at the server: a handler invocation that reads data or changes state
A weather lookup illustrates the wire form. The hostname and path identify the destination. The Authorization header carries the credential. Under the current specification the request also carries MCP-Protocol-Version, and — this is the important change — Mcp-Method and Mcp-Name headers that state the operation and the tool being invoked. The JSON-RPC body repeats the method, carries a request id the client can match to a response, and holds the arguments.
The arguments are the sensitive payload: a search query, source code, customer data, or instructions for an action like opening a ticket or changing infrastructure. The tool name says what the agent intends to call; the arguments say what data leaves the building and what the action will do. Response bodies are equally sensitive — inspection of them shows what the tool returned to the agent.
The 2026-07-28 specification restructured this wire format deliberately:
- The
initialize/notifications/initializedhandshake is gone; the protocol is stateless, and every request carries its version and client capabilities in_meta, mirrored on HTTP byMCP-Protocol-Version. Mcp-MethodandMcp-Nameare now required on Streamable HTTP POST requests, per the specification changelog.- Tool parameters can contribute custom request headers via
x-mcp-header. - Trace context follows OpenTelemetry conventions (
traceparent,tracestate,baggage) inside_meta. - List results (
tools/list,resources/list, prompts) carryttlMsandcacheScopehints through a cacheable-result interface.
Read that list as an operability programme, not a feature list. Load balancers can route on Mcp-Method without parsing JSON. Rate limiters can separate tools/list from tools/call at the edge. Standard tracing tooling can correlate tool calls. CDN layers can cache tool catalogues. None of this requires an AI-specific middlebox — the protocol now cooperates with ordinary HTTP infrastructure.
Why a URL never proved a protocol
Cloudflare's first-pass approach, documented in its Gateway logs tutorial, searched HTTP logs for hostnames containing mcp and paths like /mcp or /sse, plus DLP patterns matching JSON-RPC methods such as initialize, tools/call, and resources/read in request bodies. The company's own assessment is blunt: those signals miss an MCP server living at an ordinary URL like https://tools.example.com/api, and they can false-positive on an unrelated service that happens to contain mcp in a name.
The header is the stronger signal, but its epistemics deserve precision. MCP-Protocol-Version presence proves MCP; its absence proves nothing:
| Signal | When present | When absent |
|---|---|---|
mcp in hostname or path |
Weak hint; common convention, not a requirement | Nothing — most servers need not use the convention |
| JSON-RPC method in request body | Strong indicator, but requires TLS decryption plus body inspection | Nothing — body inspection may be excluded by policy or cost |
MCP-Protocol-Version header |
Strong positive indicator of MCP on that request | Nothing — see the blind spots below |
Four blind spots keep header detection a floor rather than an inventory:
- Legacy sessions. Under session-based Streamable HTTP, the header only appears after initialisation, so the first request to a previously unknown endpoint can pass unclassified.
- Old protocol versions. Versions earlier than 2025-06-18 did not define the header at all.
- Local transports.
stdioservers never generate network traffic, so no gateway will ever see them. - Nonconforming or off-path clients. Custom transports, buggy clients, off-network devices, and anything configured to bypass inspection remain invisible by construction.
The stateless spec narrows the first gap structurally — with no handshake, a conforming client puts the version on every POST, including the first one — but it cannot fix the others. Cloudflare ships the selector as experimental.is_mcp, explicitly beta, which is the right level of humility for a heuristic. Treat any count it produces as a lower bound.
Three control points, three different coverages
Cloudflare's post frames enforcement around the three forms of a tool call: control it inside the client, on the network, or at the server. The useful part is not the taxonomy itself but what each layer cannot see:
| Control point | What it sees | What it misses | What it can enforce | It fails when |
|---|---|---|---|---|
| Client hook (pre-serialisation) | Tool name, arguments, destination — including local stdio servers |
Everything done by other clients and devices | Allowlists, confirmation prompts, argument redaction | Employees use unmanaged clients; controls must be reimplemented per client |
| Network gateway (TLS-inspecting) | Widest remote lens; user and device identity; protocol headers; bodies where DLP applies | stdio, off-network traffic, anything marked do-not-inspect |
Allow/block/isolate by destination, user, method, or detected MCP | Traffic never traverses the proxy, or encrypted flows skip decryption |
| Server middleware (pre-handler) | Richest context: authenticated caller, parsed arguments, resolved tool | Nothing about its own tools — but other servers get no protection | Per-tool authorisation, rate limits, audit, block-before-execution | The organisation relies on servers that never implemented it |
Two consequences follow. First, no single layer is complete, so a defensible posture composes them: client hooks catch local stdio, the network catches remote shadow traffic, and the server is the last point where a bad call can be denied before it executes. Second, telemetry from one layer is never an inventory — a dashboard of network MCP traffic tells you about managed paths only.
Shadow servers and portal bypass fail differently
Once traffic is detectable, Cloudflare splits the governance problem in two, and the split is the transferable insight:
- Shadow MCP is a connection to a server the organisation never approved — an employee found it in a repository, a product guide, or a colleague's message and pointed their client at it. This is a discovery problem.
- Portal bypass is a connection to an approved server that skips the managed path — reaching the upstream URL directly, past the identity check, curated tool catalogue, DLP, and audit trail. This is an enforcement problem.
The documented example rule targets the second: Is MCP is true and Traffic Source is not MCP portal → block. Shadow destinations get found by the detection dashboard, investigated, then either approved and placed behind the managed path or blocked outright.
The trap is assuming the network rule alone closes the bypass. Cloudflare is explicit that bypass control needs network policy plus an origin that rejects direct requests — an access policy, a source-IP restriction, or an enterprise authorisation mechanism the MCP server itself initiates. If the origin happily serves any authenticated bearer token, a determined client just ignores the portal. You do not need Cloudflare to apply the pattern: an origin allowlist in front of your own MCP servers does the same job at small scale.
What the headers buy you without an AI product
The vendor-independent takeaway is that the signals are standardised now. A small team running its own MCP servers — or its own proxy — can act on them directly:
- Inventory: log the three headers on every request. Any host serving traffic that carries them is in your agent estate, whatever the URL looks like. Presence is a floor, not a census.
- Rate limiting by operation: bucket on
Mcp-Methodsotools/listfloods cannot exhaust capacity thattools/callneeds (nginx exposes request headers as$http_variables, lowercased with dashes as underscores):
map $http_mcp_method $mcp_bucket {
default "mcp-unclassified";
"tools/list" "mcp-read";
"tools/call" "mcp-invoke";
}
log_format mcp '$remote_addr mcp_version=$http_mcp_protocol_version '
'mcp_method=$http_mcp_method mcp_tool="$http_mcp_name" '
'host=$host uri=$request_uri status=$status';
- Routing and quotas: a load balancer can pin
tools/calltraffic to the pool that autoscales, and leave read traffic on the steady pool, with no body parsing. - Tracing: the trace-context conventions mean your existing OpenTelemetry collector can follow a tool call without an MCP-specific integration.
- Caching: the cacheable-result hints mean a CDN can serve
tools/listresponses instead of your origin — prompt-cache-friendly and cheap. - Caution:
x-mcp-headerlets tool parameters become request headers. If you operate a server that forwards these, sanitise and allowlist before anything reaches auth, logging, or upstream systems — an agent-supplied parameter is untrusted input, and headers are a classic injection surface.
One more implementation consequence: any policy keyed on these headers silently passes nonconforming traffic into its default branch. Pair header signals with destination allowlists rather than relying on detection alone.
A tier pattern for enforcement at the server
The richest enforcement point is server middleware, because it runs after authentication and argument validation but before the handler executes. Cloudflare describes its internal pattern, WriteGuard, in enough detail to copy: every tool gets a risk tier and an enabled state; reads pass through, allowed writes gain agent attribution and an audit event, and critical actions are blocked before their handler runs.
| Tier | Examples | Default handling |
|---|---|---|
| Read-only | tools/list, resources/read, search and get-style tools |
Pass through, light logging |
| State-changing | Create ticket, update record, post comment | Allow, attach agent attribution, write audit event |
| Critical / irreversible | refund_order, publish_page, change_dns, export_customers |
Block pending explicit human-scoped authorisation |
The ordering rule matters more than the tiers: authorise, rate-limit, and log before invoking the handler. Post-execution logging explains what happened; it prevents nothing. And because the control lives at the server, a user cannot evade it by switching clients or disabling a local hook — the property neither the client nor the network layer can offer.
A rollout sequence that composes the layers:
- Inventory first, policy later. Scan logs with URL patterns, DLP body patterns, and header presence; accept the result as a lower bound.
- Classify what you found. Approve servers into a managed path; block the rest; record both decisions.
- Tier your tools at the origin before handlers run, starting with anything that writes or spends.
- Make approved origins reject direct connections — access policy, IP allowlist, or server-side authorisation assertion.
- Re-run the scan on a cadence. A new shadow host is an incident signal, not dashboard noise.
Failure modes worth reviewing
Detection passes nonconforming traffic
A legacy client's first request, a pre-2025-06-18 version, or a nonconforming implementation crosses the gateway unclassified and meets no MCP policy. Keep destination allowlists active regardless of detection confidence.
The stdio blind spot becomes the main path
A developer wires a local stdio server with broad file or credential access. No network control will ever see it; only client hooks or OS-level controls do. Inventory local servers separately from remote ones.
Policy is tested in one client and disappears in another
Client-side hooks are per-product. A rule validated in one harness may not exist in the CLI a teammate uses. Client controls are defence in depth, never the policy of record.
The origin trusts the portal alone
The network blocks portal bypass, but the origin still accepts any valid token from anywhere. Direct connections from off-network devices skip the proxy entirely. Enforcement has to terminate at the origin.
One host serves both MCP and ordinary API traffic
Header-keyed rules classify only MCP-marked requests, which is correct — but blanket origin restrictions (IP allowlists, access policies) will also hit the ordinary API clients on that host. Split hosts or scope origin rules by path and authentication method.
The dashboard exists but nobody owns triage
Detection produces a list of shadow servers with no assigned reviewer. Decide the owner, the cadence, and the escalation before enabling blocking rules, or the inventory decays into wallpaper.
The practical conclusion
The stateless MCP rewrite quietly solved a problem that security products had been papering over: agent traffic is now identifiable by the same infrastructure that already terminates your TLS. Use that. Log the headers, bucket your rate limits by Mcp-Method, cache your tool catalogue, and treat any host carrying MCP-Protocol-Version as part of your agent estate.
Then be precise about what did not change. A header is a positive indicator on the requests that carry it and silence about everything else — stdio, off-path devices, nonconforming clients. And visibility was never authority. Whether the control point is a client hook, a gateway, or middleware in front of the handler, each one has a blind spot the other two have to cover. Detection became a commodity this month; governance remains the part you build.