Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ The following servers are included in this repository:
| [**DNS Analytics server**](/apps/dns-analytics) | Optimize DNS performance and debug issues based on current set up | `https://dns-analytics.mcp.cloudflare.com/sse` |
| [**Digital Experience Monitoring server**](/apps/dex-analysis) | Get quick insight on critical applications for your organization | `https://dex.mcp.cloudflare.com/sse` |
| [**Cloudflare One CASB server**](/apps/cloudflare-one-casb) | Quickly identify any security misconfigurations for SaaS applications to safeguard users & data | `https://casb.mcp.cloudflare.com/sse` |
| [**GraphQL server**](/apps/graphql/) | Get analytics data using Cloudflare’s GraphQL API | `https://graphql.mcp.cloudflare.com/sse` |

## Access the remote MCP server from any MCP client

Expand Down
5 changes: 5 additions & 0 deletions apps/graphql/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/** @type {import("eslint").Linter.Config} */
module.exports = {
root: true,
extends: ['@repo/eslint-config/default.cjs'],
}
45 changes: 45 additions & 0 deletions apps/graphql/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Cloudflare GraphQL MCP Server

This is a [Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction) server that supports remote MCP
connections, with Cloudflare OAuth built-in. It integrates tools powered by the [Cloudflare GraphQL API](https://developers.cloudflare.com/analytics/graphql-api/) to provide insights and utilities for your Cloudflare account.

## Available Tools

Currently available tools:

| **Category** | **Tool** | **Description** |
| --------------------------- | ------------------------- | ----------------------------------------------------------------------------------------------- |
| **GraphQL Schema Search** | `graphql_schema_search` | Search the Cloudflare GraphQL API schema for types, fields, and enum values matching a keyword |
| **GraphQL Schema Overview** | `graphql_schema_overview` | Fetch the high-level overview of the Cloudflare GraphQL API schema |
| **GraphQL Type Details** | `graphql_type_details` | Fetch detailed information about a specific GraphQL type |
| **GraphQL Complete Schema** | `graphql_complete_schema` | Fetch the complete Cloudflare GraphQL API schema (combines overview and important type details) |
| **GraphQL Query Execution** | `graphql_query` | Execute a GraphQL query against the Cloudflare API |
| **GraphQL API Explorer** | `graphql_api_explorer` | Generate a Cloudflare [GraphQL API Explorer](https://graphql.cloudflare.com/explorer) link |

### Prompt Examples

- `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`
- `Can you generate a link to the Cloudflare GraphQL API Explorer with a pre-populated query and variables?`
- `I need to monitor HTTP requests and responses for a specific domain. Can you help me with that using the Cloudflare GraphQL API?`

## Access the remote MCP server from Claude Desktop

If your MCP client has first class support for remote MCP servers, the client will provide a way to accept the server URL (`https://graphql.mcp.cloudflare.com/sse`) directly within its interface (for example in [Cloudflare AI Playground](https://playground.ai.cloudflare.com/)).

If your client does not yet support remote MCP servers, you will need to set up its respective configuration file using [mcp-remote](https://www.npmjs.com/package/mcp-remote) to specify which servers your client can access.

Replace the content with the following configuration:

```json
{
"mcpServers": {
"cloudflare": {
"command": "npx",
"args": ["mcp-remote", "https://graphql.mcp.cloudflare.com/sse"]
}
}
}
```

Once you've set up your configuration file, restart MCP client and a browser window will open showing your OAuth login page. Proceed through the authentication flow to grant the client access to your MCP server. After you grant access, the tools will become available for you to use.
34 changes: 34 additions & 0 deletions apps/graphql/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"name": "graphql-mcp-server",
"version": "0.0.1",
"private": true,
"scripts": {
"check:lint": "run-eslint-workers",
"check:types": "run-tsc",
"deploy": "run-wrangler-deploy",
"dev": "wrangler dev",
"start": "wrangler dev",
"types": "wrangler types --include-env=false",
"test": "vitest run"
},
"dependencies": {
"@cloudflare/workers-oauth-provider": "0.0.5",
"@hono/zod-validator": "0.4.3",
"@modelcontextprotocol/sdk": "1.10.2",
"@repo/mcp-common": "workspace:*",
"@repo/mcp-observability": "workspace:*",
"agents": "0.0.67",
"cloudflare": "4.2.0",
"hono": "4.7.6",
"zod": "3.24.2",
"lz-string": "1.5.0"
},
"devDependencies": {
"@cloudflare/vitest-pool-workers": "0.8.14",
"@types/node": "22.14.1",
"prettier": "3.5.3",
"typescript": "5.5.4",
"vitest": "3.0.9",
"wrangler": "4.10.0"
}
}
130 changes: 130 additions & 0 deletions apps/graphql/src/graphql.app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import OAuthProvider from '@cloudflare/workers-oauth-provider'
import { McpAgent } from 'agents/mcp'

import {
createAuthHandlers,
handleTokenExchangeCallback,
} from '@repo/mcp-common/src/cloudflare-oauth-handler'
import { handleDevMode } from '@repo/mcp-common/src/dev-mode'
import { getUserDetails, UserDetails } from '@repo/mcp-common/src/durable-objects/user_details.do'
import { getEnv } from '@repo/mcp-common/src/env'
import { RequiredScopes } from '@repo/mcp-common/src/scopes'
import { initSentryWithUser } from '@repo/mcp-common/src/sentry'
import { CloudflareMCPServer } from '@repo/mcp-common/src/server'
import { registerAccountTools } from '@repo/mcp-common/src/tools/account.tools'
import { registerZoneTools } from '@repo/mcp-common/src/tools/zone.tools'
import { MetricsTracker } from '@repo/mcp-observability'

import { registerGraphQLTools } from './tools/graphql.tools'

import type { AuthProps } from '@repo/mcp-common/src/cloudflare-oauth-handler'
import type { Env } from './graphql.context'

export { UserDetails }

const env = getEnv<Env>()

const metrics = new MetricsTracker(env.MCP_METRICS, {
name: env.MCP_SERVER_NAME,
version: env.MCP_SERVER_VERSION,
})

// Context from the auth process, encrypted & stored in the auth token
// and provided to the DurableMCP as this.props
type Props = AuthProps
type State = { activeAccountId: string | null }

export class GraphQLMCP extends McpAgent<Env, State, Props> {
_server: CloudflareMCPServer | undefined
set server(server: CloudflareMCPServer) {
this._server = server
}

get server(): CloudflareMCPServer {
if (!this._server) {
throw new Error('Tried to access server before it was initialized')
}

return this._server
}

constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env)
}

async init() {
this.server = new CloudflareMCPServer({
userId: this.props.user.id,
wae: this.env.MCP_METRICS,
serverInfo: {
name: this.env.MCP_SERVER_NAME,
version: this.env.MCP_SERVER_VERSION,
},
sentry: initSentryWithUser(env, this.ctx, this.props.user.id),
})

// Register account tools
registerAccountTools(this)

// Register zone tools
registerZoneTools(this)

// Register GraphQL tools
registerGraphQLTools(this)
}

async getActiveAccountId() {
try {
// Get UserDetails Durable Object based off the userId and retrieve the activeAccountId from it
// we do this so we can persist activeAccountId across sessions
const userDetails = getUserDetails(env, this.props.user.id)
return await userDetails.getActiveAccountId()
} catch (e) {
this.server.recordError(e)
return null
}
}

async setActiveAccountId(accountId: string) {
try {
const userDetails = getUserDetails(env, this.props.user.id)
await userDetails.setActiveAccountId(accountId)
} catch (e) {
this.server.recordError(e)
}
}
}

const GraphQLScopes = {
...RequiredScopes,
'account:read': 'See your account info such as account details, analytics, and memberships.',
'zone:read': 'See zone data such as settings, analytics, and DNS records.',
} as const

export default {
fetch: async (req: Request, env: Env, ctx: ExecutionContext) => {
if (env.ENVIRONMENT === 'development' && env.DEV_DISABLE_OAUTH === 'true') {
return await handleDevMode(GraphQLMCP, req, env, ctx)
}

return new OAuthProvider({
apiHandlers: {
'/mcp': GraphQLMCP.serve('/mcp'),
'/sse': GraphQLMCP.serveSSE('/sse'),
},
// @ts-ignore
defaultHandler: createAuthHandlers({ scopes: GraphQLScopes, metrics }),
authorizeEndpoint: '/oauth/authorize',
tokenEndpoint: '/token',
tokenExchangeCallback: (options) =>
handleTokenExchangeCallback(
options,
env.CLOUDFLARE_CLIENT_ID,
env.CLOUDFLARE_CLIENT_SECRET
),
// Cloudflare access token TTL
accessTokenTTL: 3600,
clientRegistrationEndpoint: '/register',
}).fetch(req, env, ctx)
},
}
20 changes: 20 additions & 0 deletions apps/graphql/src/graphql.context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { GraphQLMCP, UserDetails } from './graphql.app'

export interface Env {
OAUTH_KV: KVNamespace
ENVIRONMENT: 'development' | 'staging' | 'production'
MCP_SERVER_NAME: string
MCP_SERVER_VERSION: string
CLOUDFLARE_CLIENT_ID: string
CLOUDFLARE_CLIENT_SECRET: string
MCP_OBJECT: DurableObjectNamespace<GraphQLMCP>
USER_DETAILS: DurableObjectNamespace<UserDetails>
MCP_METRICS: AnalyticsEngineDataset
SENTRY_ACCESS_CLIENT_ID: string
SENTRY_ACCESS_CLIENT_SECRET: string
GIT_HASH: string
SENTRY_DSN: string
DEV_DISABLE_OAUTH: string
DEV_CLOUDFLARE_API_TOKEN: string
DEV_CLOUDFLARE_EMAIL: string
}
Loading