Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
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.
35 changes: 35 additions & 0 deletions apps/graphql/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"name": "graphql",
"version": "0.0.0",
"private": true,
"scripts": {
"check:lint": "run-eslint-workers",
"check:types": "run-tsc",
"deploy": "wrangler deploy",
"dev": "wrangler dev",
"start": "wrangler dev",
"cf-typegen": "wrangler types",
"test": "vitest run"
},
"dependencies": {
"@cloudflare/workers-oauth-provider": "0.0.3",
"@hono/zod-validator": "0.4.3",
"@modelcontextprotocol/sdk": "1.9.0",
"@repo/mcp-common": "workspace:*",
"@repo/mcp-observability": "workspace:*",
"agents": "0.0.62",
"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",
"@cloudflare/workers-types": "4.20250410.0",
"@types/node": "^22.15.0",
"prettier": "3.5.3",
"typescript": "5.5.4",
"vitest": "3.0.9",
"wrangler": "4.10.0"
}
}
18 changes: 18 additions & 0 deletions apps/graphql/src/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { UserDetails } from '@repo/mcp-common/src/durable-objects/user_details'
import type { GraphQLMCP } from './index'

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
}
125 changes: 125 additions & 0 deletions apps/graphql/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import OAuthProvider from '@cloudflare/workers-oauth-provider'
import { McpAgent } from 'agents/mcp'

import {
createAuthHandlers,
handleTokenExchangeCallback,
} from '@repo/mcp-common/src/cloudflare-oauth-handler'
import { getUserDetails, UserDetails } from '@repo/mcp-common/src/durable-objects/user_details'
import { getEnv } from '@repo/mcp-common/src/env'
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'
import { registerZoneTools } from '@repo/mcp-common/src/tools/zone'
import { MetricsTracker } from '@repo/mcp-observability'

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

import type { AccountSchema, UserSchema } from '@repo/mcp-common/src/cloudflare-oauth-handler'
import type { Env } from './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
export type Props = {
accessToken: string
user: UserSchema['result']
accounts: AccountSchema['result']
}

export 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
}

initialState: State = {
activeAccountId: null,
}

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 = {
'account:read': 'See your account info such as account details, analytics, and memberships.',
'user:read': 'See your user info such as name, email address, and account memberships.',
'zone:read': 'See zone data such as settings, analytics, and DNS records.',
offline_access: 'Grants refresh tokens for long-lived access.',
} as const

export default new OAuthProvider({
apiRoute: '/sse',
// @ts-ignore
apiHandler: GraphQLMCP.mount('/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',
})
Loading