How Cloudflare MCP Helped Me Write GraphQL Queries

Discover how the Cloudflare MCP enabled me to navigate Cloudflare's GraphQL documentation and generate valid queries to retrieve my website statistics.

Introduction and Context

In my work, I needed to create a Laravel tool to retrieve statistics for our websites hosted on Cloudflare. I started by searching for API documentation on Cloudflare's website and PHP-compatible libraries.

While testing Cloudflare's official PHP SDK, I quickly realized two things: Cloudflare doesn't seem committed to maintaining this SDK long-term, and some old REST endpoints have been replaced by GraphQL endpoints.

Why GraphQL Blocked Me

Having never used a GraphQL API before, I started searching for endpoints in the documentation as I would for a REST API.

That's where I got lost. Unlike well-structured REST documentation, it was difficult to understand field names, arguments, time periods, and especially the permissions and limits associated with each plan.

I installed a GraphQL client (Altair) to test queries directly.

That's when I discovered an interesting feature: introspection.

Simply put, it allows the client to retrieve the GraphQL schema from the server to discover available queries, types, fields, arguments, and mutations.

On paper, it was perfect. In practice, it was misleading: introspection doesn't know the actual permissions for each zone, nor the limits specific to free, pro, business, or enterprise plans. Altair would generate seemingly "valid" queries, but often with too many arguments or fields I didn't have access to.

Unlike good REST documentation, GraphQL often requires you to find out yourself:

  • the required permission level
  • the actually available data
  • the accepted periods
  • the filters usable according to your plan

Génération des champs dans Altair

This generates the following GraphQL query:

query ASingleDatasetExample($zoneTag: string, $start: Time, $end: Time) {
  viewer {
    zones(filter: { zoneTag: $zoneTag }) {
      firewallEventsAdaptiveByTimeGroups {
        avg {
          sampleInterval
        }
        confidence(level: "_____") {
          count {
            estimate
            isValid
            lower
            sampleSize
            upper
          }
          level
        }
        count
        dimensions {
          apiGatewayMatchedEndpoint
          apiGatewayMatchedHost
          attackSignatureCategories
          attackSignatureRefs
          botDetectionIds
          botDetectionTags
          botScore
          botScoreSrcName
          date
          datetime
          datetimeFifteenMinutes
          datetimeFiveMinutes
          datetimeHour
          datetimeMinute
          description
          firewallForAiAnyPiiCategory
          firewallForAiCustomTopicCategoriesScoresMin
          firewallForAiInjectionScore
          firewallForAiPiiCategories
          firewallForAiUnsafeTopicCategories
          fraudAttack
          fraudDetectionIds
          fraudDetectionTags
          fraudEmailRisk
          fraudEventType
          fraudUserId
          httpApplicationVersion
          ja3Hash
          ja4
          jsDetectionPassed
          verifiedBotCategory
          wafAttackScore
          wafAttackScoreClass
          wafMlAttackScore
          wafMlSqliAttackScore
          wafMlXssAttackScore
          wafPathTraversalAttackScore
          wafRceAttackScore
          wafSqliAttackScore
          wafXssAttackScore
          zoneVersion
        }
        sum {
          botDetectionIdArray
          botDetectionIdCountArray
          botDetectionTagArray
          botDetectionTagCountArray
          fraudDetectionIdArray
          fraudDetectionIdCountArray
          fraudDetectionTagArray
          fraudDetectionTagCountArray
        }
      }
    }
  }
}

Three-quarters of which are enterprise data!

We can test and remove step by step the fields and dimensions we don't have access to in order to get a working query.

query ASingleDatasetExample($zoneTag: string, $start: Time, $end: Time) {
  viewer {
    zones(filter: { zoneTag: $zoneTag }) {
      firewallEventsAdaptiveByTimeGroups(
        filter: { datetime_gt: $start, datetime_lt: $end }
        limit: 2
        orderBy: [datetime_DESC]
      ) {
        avg {
          sampleInterval
        }
        count
        dimensions {
          date
          datetime
          datetimeFifteenMinutes
          datetimeFiveMinutes
          datetimeHour
          datetimeMinute
          description
          firewallForAiAnyPiiCategory
          firewallForAiCustomTopicCategoriesScoresMin
          firewallForAiInjectionScore
          firewallForAiPiiCategories
          firewallForAiUnsafeTopicCategories
          httpApplicationVersion
          verifiedBotCategory
          zoneVersion
        }
      }
    }
  }
}

Discovering the Cloudflare MCP

While searching for limits and constraints in the documentation, I came across the Cloudflare MCP page for GraphQL. The README contained very helpful prompt examples, such as:

  • Show me HTTP traffic for the last 7 days for example.com
  • Show me which GraphQL datatype I need to use to query firewall events

I immediately liked the idea: instead of spending hours guessing the right fields and combinations, I could let an AI tool explore the schema and propose queries actually compatible with my context.

Attempting with the Playground

When discovering the MCP, I also saw that Cloudflare offered a Playground in chat form. I connected the GraphQL MCP there and started writing the prompts from the README.

But disaster struck: the Playground was too unstable in my case. When I tried to retrieve statistics for one of my domains, I sometimes got security errors or unusable responses.

Even when I explicitly asked to use the Cloudflare MCP, the result often went in circles: attempting to go through HTTP, misinterpreting the context, or queries that didn't complete.

I quickly abandoned this approach to move to Claude Code.

Moving to Claude Code

To add the GraphQL MCP in Claude Code, you can connect it with:

claude mcp add --transport sse cloudflare-graphql https://graphql.mcp.cloudflare.com/sse

Then, authenticate in the MCP settings.

:::info If you use it over several days, regularly check that the connection is still valid.

:::

Once connected, I was able to retry retrieving the data I was originally looking for.

And this time, it worked: I got the right queries, tested against my plan, with the right fields and parameters. Most useful, I finally had queries ready to integrate into my application.

What I Learned

The real benefit of the MCP is not just "generating a query." The benefit is especially reducing time wasted interpreting overly abstract GraphQL documentation.

In my case, it helped me:

  • identify the right fields
  • understand the constraints related to my plan
  • test parameters before integration
  • avoid starting with too ambitious or invalid queries

Tips on Cloudflare's GraphQL API

If you implement this API, keep in mind its limits. According to Cloudflare documentation, the GraphQL Analytics API is designed for aggregated data, and default limits include notably 300 requests over a 5-minute window, and a maximum of 10 zones for a zone-scoped query.

If you need to retrieve data for multiple zones, the cleanest approach is to work in batches of 10 zones at a time. This greatly reduces the total number of requests and avoids unnecessarily hitting the limits.

Query Example

Once the logic is understood, the interesting part becomes integration in the code. Here's an example of a GraphQL query structure I was able to stabilize after testing with the MCP:

query FirewallByTimeBatch(
  $zoneTags: [string!]
  $start: Time
  $end: Time
) {
  viewer {
    zones(filter: { zoneTag_in: $zoneTags }) {
      zoneTag
      firewallEventsAdaptiveByTimeGroups(
        filter: {
          datetime_geq: $start
          datetime_lt: $end
          action: "block"
        }
        limit: 1000
        orderBy: [datetimeHour_ASC]
      ) {
        dimensions {
          datetimeHour
        }
        count
      }
    }
  }
}

# variables
{
  "zoneTags": [
    "abc123",
    "abc456"
  ],
  "start": "2026-05-11T02:07:05Z",
  "end": "2026-05-11T17:07:05Z"
}

The goal wasn't to have the most elegant query possible, but the right query for my actual need.

Practical Conclusion

Cloudflare's GraphQL API remains powerful, but the documentation clearly requires more effort than a well-marked REST API. If you're not comfortable with GraphQL, I advise against spending too much time guessing fields and constraints manually.

Instead, use Cloudflare's MCP in your development environment. Let the AI explore the schema, test permissions according to your subscription, then generate queries you can integrate into your project.

In my opinion, this is the fastest way to go from confusing documentation to actually usable queries.