Skip to content

Add Cloudflare Radar MCP server #72

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 27, 2025
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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -48,5 +48,6 @@ yarn-error.log*
.sentryclirc.lock/
tmp.json
tmp.ts
.idea

apps/sandbox-container/workdir
apps/sandbox-container/workdir
20 changes: 9 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,14 @@ Open Claude Desktop and navigate to Settings -> Developer -> Edit Config. This o

Replace the content with the following configuration. Once you restart Claude Desktop, a browser window will open showing your OAuth login page. Complete the authentication flow to grant Claude access to your MCP server. After you grant access, the tools will become available for you to use.

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

Expand Down Expand Up @@ -54,7 +51,8 @@ Some features may require a paid Cloudflare Workers plan. Ensure your Cloudflare

### Apps

- [workers-observability](apps/workers-observability/): The Workers Observability MCP server
- [workers-observability](apps/workers-observability): The Workers Observability MCP server
- [radar](apps/radar): The Cloudflare Radar MCP server

### Packages

Expand Down
3 changes: 3 additions & 0 deletions apps/radar/.dev.vars.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
CLOUDFLARE_CLIENT_ID=
CLOUDFLARE_CLIENT_SECRET=
URL_SCANNER_API_TOKEN=
5 changes: 5 additions & 0 deletions apps/radar/.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'],
}
117 changes: 117 additions & 0 deletions apps/radar/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# Cloudflare Radar 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 Radar API](https://developers.cloudflare.com/radar/) to provide global
Internet traffic insights, trends and other utilities.

## 🔨 Available Tools

Currently available tools:

| **Category** | **Tool** | **Description** |
| ---------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| **HTTP Requests** | `get_http_requests_data` | Fetches HTTP request data (timeseries, summaries, and grouped timeseries across dimensions like `deviceType`, `botClass`) |
| **Autonomous Systems** | `list_autonomous_systems` | Lists ASes; filter by location and sort by population size |
| | `get_as_details` | Retrieves detailed info for a specific ASN |
| **IP Addresses** | `get_ip_details` | Provides details about a specific IP address |
| **Traffic Anomalies** | `get_traffic_anomalies` | Lists traffic anomalies; filter by AS, location, start date, and end date |
| **Domains** | `get_domains_ranking` | Get top or trending domains |
| | `get_domain_rank_details` | Get domain rank details |
| **URL Scanner** | `scan_url` | Scans a URL via [Cloudflare’s URL Scanner](https://developers.cloudflare.com/radar/investigate/url-scanner/) |

This MCP server is still a work in progress, and we plan to add more tools in the future.

### Prompt Examples

- `What are the most used operating systems?`
- `What are the top 5 ASes in Portugal?`
- `Get information about ASN 13335.`
- `What are the details of IP address 1.1.1.1?`
- `List me traffic anomalies in Syria over the last year.`
- `Compare domain rankings in the US and UK.`
- `Give me rank details for google.com in March 2025.`
- `Scan https://example.com.`

## Access the remote MCP server from Claude Desktop

Open Claude Desktop and navigate to `Settings -> Developer -> Edit Config`.
This opens the configuration file that controls which MCP servers Claude can access.

Replace the content with the following configuration:

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

Once you restart Claude Desktop, a browser window will open showing your OAuth login page.
Complete the authentication flow to grant Claude access to your MCP server.
After you grant access, the tools will become available for you to use.

## Setup

#### Secrets

Set secrets via Wrangler:

```bash
npx wrangler secret put CLOUDFLARE_CLIENT_ID -e <ENVIRONMENT>
npx wrangler secret put CLOUDFLARE_CLIENT_SECRET -e <ENVIRONMENT>
npx wrangler secret put URL_SCANNER_API_TOKEN -e <ENVIRONMENT>
```

#### Set up a KV namespace

Create the KV namespace:

```bash
npx wrangler kv namespace create "OAUTH_KV"
```

Then, update the Wrangler file with the generated KV namespace ID.

#### Deploy & Test

Deploy the MCP server to make it available on your workers.dev domain:

```bash
npx wrangler deploy -e <ENVIRONMENT>
```

Test the remote server using [Inspector](https://modelcontextprotocol.io/docs/tools/inspector):

```bash
npx @modelcontextprotocol/inspector@latest
```

## Local Development

If you'd like to iterate and test your MCP server, you can do so in local development.
This will require you to create another OAuth App on Cloudflare:

1. Create a `.dev.vars` file in your project root with:

```
CLOUDFLARE_CLIENT_ID=your_development_cloudflare_client_id
CLOUDFLARE_CLIENT_SECRET=your_development_cloudflare_client_secret
URL_SCANNER_API_TOKEN=your_development_url_scanner_api_token
```

2. Start the local development server:

```bash
npx wrangler dev
```

3. To test locally, open Inspector, and connect to `http://localhost:8976/sse`.
Once you follow the prompts, you'll be able to "List Tools".

You can also connect to Claude Desktop.
33 changes: 33 additions & 0 deletions apps/radar/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"name": "cloudflare-radar-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.3",
"@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"
},
"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"
}
}
14 changes: 14 additions & 0 deletions apps/radar/src/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { RadarMCP } from './index'

export interface Env {
OAUTH_KV: KVNamespace
ENVIRONMENT: 'development' | 'staging' | 'production'
ACCOUNT_ID: '6702657b6aa048cf3081ff3ff3c9c52f'
MCP_SERVER_NAME: string
MCP_SERVER_VERSION: string
CLOUDFLARE_CLIENT_ID: string
CLOUDFLARE_CLIENT_SECRET: string
URL_SCANNER_API_TOKEN: string
MCP_OBJECT: DurableObjectNamespace<RadarMCP>
MCP_METRICS: AnalyticsEngineDataset
}
89 changes: 89 additions & 0 deletions apps/radar/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import OAuthProvider from '@cloudflare/workers-oauth-provider'
import { McpAgent } from 'agents/mcp'

import {
createAuthHandlers,
handleTokenExchangeCallback,
} from '@repo/mcp-common/src/cloudflare-oauth-handler'
import { getEnv } from '@repo/mcp-common/src/env'
import { CloudflareMCPServer } from '@repo/mcp-common/src/server'
import { MetricsTracker } from '@repo/mcp-observability'

import { registerRadarTools } from './tools/radar'
import { registerUrlScannerTools } from './tools/url-scanner'

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

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']
}

export type State = never

export class RadarMCP 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(
public ctx: DurableObjectState,
public 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,
},
})

registerRadarTools(this)
registerUrlScannerTools(this)
}
}

// TODO add radar:read and url_scanner:write scopes once they are available
// Also remove URL_SCANNER_API_TOKEN env var
const RadarScopes = {
'user:read': 'See your user info such as name, email address, and account memberships.',
offline_access: 'Grants refresh tokens for long-lived access.',
} as const

export default new OAuthProvider({
apiRoute: '/sse',
apiHandler: RadarMCP.mount('/sse'),
// @ts-ignore
defaultHandler: createAuthHandlers({ scopes: RadarScopes, 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