Skip to content

Detect MCP traffic in Gateway logs

Last reviewed: 4 days ago

Organizations may lack visibility into Model Context Protocol (MCP) traffic, which can allow employees to connect to remote MCP servers outside of IT oversight. These connections risk the exfiltration of sensitive internal data and credentials, tool injection attacks or software supply chain risks.

As an IT administrator, you want to identify shadow MCP traffic to prevent unauthorized data exfiltration while still supporting governed use cases. In this tutorial, you will use the Cloudflare GraphQL Analytics API to scan Gateway HTTP logs for MCP traffic patterns, create DLP profiles that detect MCP JSON-RPC methods, and classify traffic to differentiate between authorized traffic sent to MCP server portals and traffic sent to "shadow" remote MCP servers.

Prerequisites

  • A Cloudflare account with a Zero Trust organization
  • Gateway with HTTP filtering enabled and actively proxying user traffic
  • An API token with the following permissions:
    • Account-level Zero Trust: Read
    • Account-level DLP: Write
    • Account-level Gateway: Write
  • Your Cloudflare account ID (available in the Cloudflare dashboard under Account Home)
  • Familiarity with GraphQL Analytics API queries
  • A working knowledge of TypeScript and REST APIs

1. Review the Gateway HTTP dataset

The gatewayHttpRequestsAdaptiveGroups dataset in the GraphQL Analytics API provides aggregated Gateway HTTP log data. Use this dataset to query for MCP-related traffic patterns:

  • Dimensions: httpHost, httpRequestURI, action, users, dlpProfiles
  • Time range: Up to 30 days of historical data
  • Grouping: Aggregates results by dimension values
  • Filtering: Supports OR, AND, and like operators

2. Build the MCP detection query

MCP traffic can be identified by three signals:

  1. Domain patterns: Hostnames containing mcp (for example, mcp.datadog.com)
  2. URL paths: Standard MCP endpoints such as /mcp, /mcp/sse, and /sse
  3. DLP matches: JSON-RPC methods in request bodies (covered in a later step)

The following GraphQL query scans Gateway logs for the first two signals:

JavaScript
const query = `
query MCPTrafficScan($accountTag: string, $since: string, $until: string) {
viewer {
accounts(filter: { accountTag: $accountTag }) {
gatewayHttpRequestsAdaptiveGroups(
filter: {
datetime_geq: $since
datetime_leq: $until
OR: [
{ httpHost_like: "%mcp%" }
{ httpRequestURI_like: "%/mcp%" }
{ httpRequestURI_like: "%/sse%" }
]
}
limit: 10000
) {
dimensions {
httpHost
action
users
}
count
}
}
}
}
`;
const variables = {
accountTag: "<YOUR_ACCOUNT_ID>",
since: "<START_DATE>", // ISO-8601 format, for example 2025-03-08T00:00:00Z
until: "<END_DATE>", // Up to 30 days after start date
};
const response = await fetch("https://api.cloudflare.com/client/v4/graphql", {
method: "POST",
headers: {
Authorization: `Bearer ${apiToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ query, variables }),
});
const data = await response.json();
const groups =
data.data?.viewer?.accounts?.[0]?.gatewayHttpRequestsAdaptiveGroups || [];

Replace <YOUR_ACCOUNT_ID> with your Cloudflare account ID. Replace <START_DATE> and <END_DATE> with ISO-8601 timestamps covering your desired time range (up to 30 days).

3. Process the query results

Each group in the response represents aggregated traffic for a specific httpHost and action combination. Parse the results to identify unblocked MCP connections:

JavaScript
const hits = groups.map((group) => ({
domain: group.dimensions.httpHost,
requestCount: group.count,
users: group.dimensions.users || [],
actions: {
allowed: group.dimensions.action === "allow" ? group.count : 0,
blocked: group.dimensions.action === "block" ? group.count : 0,
},
}));
const totalMCPRequests = hits.reduce((sum, h) => sum + h.requestCount, 0);
const unblockedHits = hits.filter((h) => h.actions.allowed > 0);
console.log(`Found ${totalMCPRequests} MCP requests`);
console.log(`${unblockedHits.length} destinations are unblocked`);

Key insights from the data:

  • Unblocked traffic (action = allow) - Active MCP connections that need investigation or blocking
  • Blocked traffic (action = block) - Your existing policies are working
  • User attribution - This indicates which employees are connecting to MCP servers

4. Create DLP profiles for MCP JSON-RPC detection

Gateway HTTP policies can match domains and URL paths, but they cannot inspect request bodies. DLP profiles scan POST body content for patterns, which is useful for shadow MCP detection, since MCP uses JSON-RPC over HTTP and has several detectable hallmarks.

Every MCP request contains a "method" field:

{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": { "name": "read_file", "arguments": { "path": "/etc/passwd" } }
}

An attacker could run an MCP server on a non-standard domain (for example, internal-tools.company.com/api/assistant) without triggering domain-based or path-based rules. You can use DLP scans of the POST body for "method": "tools/call" and other MCP-specific patterns to provide more robust protection of MCP traffic.

Review DLP constraints

Before building detection patterns, note the following DLP limitations:

  • Regex syntax — Rust regex (differs slightly from JavaScript and PCRE)
  • Scan depth — First 1,024 bytes of the request body only
  • POST only — DLP only scans POST requests
  • Performance — Regex patterns must be efficient to avoid catastrophic backtracking

Build MCP detection patterns

MCP indicators can be found in JSON-RPC method fields. The following regex patterns cover the core MCP protocol methods:

JavaScript
const DLP_REGEX_PATTERNS = [
{
name: "MCP Initialize Method",
regex: '"method"\\s{0,5}:\\s{0,5}"initialize"',
},
{
name: "MCP Tools Call",
regex: '"method"\\s{0,5}:\\s{0,5}"tools/call"',
},
{
name: "MCP Tools List",
regex: '"method"\\s{0,5}:\\s{0,5}"tools/list"',
},
{
name: "MCP Resources Read",
regex: '"method"\\s{0,5}:\\s{0,5}"resources/read"',
},
{
name: "MCP Resources List",
regex: '"method"\\s{0,5}:\\s{0,5}"resources/list"',
},
{
name: "MCP Prompts List",
regex: '"method"\\s{0,5}:\\s{0,5}"prompts/(list|get)"',
},
{
name: "MCP Sampling Create Message",
regex: '"method"\\s{0,5}:\\s{0,5}"sampling/createMessage"',
},
{
name: "MCP Protocol Version",
regex: '"protocolVersion"\\s{0,5}:\\s{0,5}"202[4-9]',
},
{
name: "MCP Notifications Initialized",
regex: '"method"\\s{0,5}:\\s{0,5}"notifications/initialized"',
},
{
name: "MCP Roots List",
regex: '"method"\\s{0,5}:\\s{0,5}"roots/list"',
},
];

Pattern explanation:

  • \\s{0,5} — Allows zero to five whitespace characters to handle both minified and pretty-printed JSON
  • "method" — Double quotes are literal because JSON requires them
  • "tools/call" — Matches the exact MCP method name
  • 202[4-9] — Matches MCP protocol versions 2024 through 2029

Create the DLP profile via API

Send a POST request to create a custom DLP profile containing all detection patterns:

JavaScript
const dlpProfile = {
name: "MCP-Shield: MCP JSON-RPC Detection",
description: "Detects MCP protocol JSON-RPC methods in HTTP request bodies.",
type: "custom",
entries: DLP_REGEX_PATTERNS.map((p) => ({
name: p.name,
enabled: true,
pattern: {
regex: p.regex,
validation: "luhn",
},
})),
};
const response = await fetch(
`https://api.cloudflare.com/client/v4/accounts/${accountId}/gateway/rules`,
{
method: "POST",
headers: {
Authorization: `Bearer ${apiToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify(dlpRule),
},
);
const data = await response.json();
if (data.success) {
console.log(`Created DLP profile: ${data.result.id}`);
}

Replace ${accountId} with your Cloudflare account ID and ${apiToken} with your API token.

Reference the DLP profile in a Gateway rule

After the DLP profile exists, create a Gateway HTTP policy that blocks requests matching the profile:

JavaScript
const dlpRule = {
name: "MCP-Shield: Block MCP JSON-RPC via DLP",
description: "Blocks requests with MCP JSON-RPC patterns detected by DLP",
precedence: 85,
enabled: true,
action: "block",
filters: ["http"],
traffic:
'any(http.request.body.scan.dlp.profiles[*] == "MCP-Shield: MCP JSON-RPC Detection")',
};

This rule triggers when the DLP profile matches any of the regex patterns in the request body.

5. Classify Portal traffic and shadow MCP traffic

Cloudflare MCP Server Portals provide governed infrastructure for approved MCP access within your organization, including:

  • Governed access — Centralized MCP infrastructure managed by your IT team
  • Audit trails — All MCP requests logged through Gateway with user attribution
  • Policy enforcement — Zero Trust policies apply automatically, including authentication and DLP
  • Approved tools — A curated set of MCP tools and resources vetted by security

When analyzing Gateway logs, it is helpful to differentiate between two types of MCP traffic:

Traffic typeCharacteristicsRisk levelAction
MCP Portal traffichttpHost matches your portal domain (for example, mcp.yourcompany.com or mcp-portal.pages.dev)AuthorizedMonitor
Shadow MCP traffichttpHost does not match any portal domain (for example, mcp.datadog.com, api.stripe.com/mcp)InvestigateBlock, redirect or review

Extend the query processing from Process the query results to classify traffic by comparing hostnames against your list of approved portal domains:

JavaScript
const portalDomains = [
"mcp.yourcompany.com",
"mcp-portal.pages.dev",
"approved-mcp.workers.dev",
];
const results = groups.map((group) => {
const isPortalTraffic = portalDomains.some((domain) =>
group.dimensions.httpHost.includes(domain),
);
return {
domain: group.dimensions.httpHost,
requestCount: group.count,
users: group.dimensions.users || [],
trafficType: isPortalTraffic ? "portal" : "shadow",
riskLevel: isPortalTraffic ? "low" : "high",
};
});
const portalTraffic = results.filter((r) => r.trafficType === "portal");
const shadowTraffic = results.filter((r) => r.trafficType === "shadow");
console.log("Portal traffic:", portalTraffic);
console.log("Shadow MCP traffic:", shadowTraffic);

Replace the portalDomains array with the actual domains of your approved MCP Server Portals.