Skip to content

Update Radar OAuth scopes and add base instructions #132

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 5 commits into from
May 2, 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
2 changes: 1 addition & 1 deletion apps/ai-gateway/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ This MCP server is still a work in progress, and we plan to add more tools in th
- `Show logs for gateway 'gateway-001' between January 1, 2023, and January 31, 2023.`
- `Fetch the latest errors from gateway-001 and debug what might have happened wrongly`

## Access the remote MCP server from from any MCP Client
## Access the remote MCP server from any MCP Client

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

Expand Down
2 changes: 1 addition & 1 deletion apps/auditlogs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Currently available tools:
- `Were there any suspicious changes made to my Cloudflare account yesterday around lunch time?`
- `When was the last activity that updated a DNS record?`

## Access the remote MCP server from from any MCP Client
## Access the remote MCP server from any MCP Client

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

Expand Down
2 changes: 1 addition & 1 deletion apps/autorag/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ This MCP server is still a work in progress, and we plan to add more tools in th
- `Search for documents in AutoRAG with ID 'rag123' using the query 'cloudflare security'.`
- `Perform an AI search in AutoRAG with ID 'rag456' for 'best practices for vector stores'.`

## Access the remote MCP server from from any MCP Client
## Access the remote MCP server from any MCP Client

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

Expand Down
2 changes: 1 addition & 1 deletion apps/browser-rendering/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ This MCP server is still a work in progress, and we plan to add more tools in th
- `Convert https://example.com to Markdown.`
- `Take a screenshot of https://example.com.`

## Access the remote MCP server from from any MCP Client
## Access the remote MCP server from any MCP Client

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

Expand Down
2 changes: 1 addition & 1 deletion apps/dns-analytics/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ This MCP server is still a work in progress, and we plan to add more tools in th
- `Read Cloudflare's documentation on managing DNS records and tell me how to optimize my DNS settings.`
- `Show me DNS Report for https://example.com in the last X days.`

## Access the remote MCP server from from any MCP Client
## Access the remote MCP server from any MCP Client

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

Expand Down
2 changes: 1 addition & 1 deletion apps/logpush/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ This MCP server is still a work in progress, and we plan to add more tools in th
- `Do any of my Logpush jobs in my <insert name> account have errors?`
- `Can you list all the enabled job failures from today?`

## Access the remote MCP server from from any MCP Client
## Access the remote MCP server from any MCP Client

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

Expand Down
3 changes: 1 addition & 2 deletions apps/radar/.dev.vars.example
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
CLOUDFLARE_CLIENT_ID=
CLOUDFLARE_CLIENT_SECRET=
URL_SCANNER_API_TOKEN=
DEV_DISABLE_OAUTH=
DEV_CLOUDFLARE_API_TOKEN=
DEV_CLOUDFLARE_EMAIL=
DEV_CLOUDFLARE_EMAIL=
3 changes: 0 additions & 3 deletions apps/radar/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ If you'd like to iterate and test your MCP server, you can do so in local develo
```
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
```

If you're an external contributor, you can provide a development API token:
Expand All @@ -21,7 +20,6 @@ If you'd like to iterate and test your MCP server, you can do so in local develo
DEV_CLOUDFLARE_EMAIL=your_cloudflare_email
# This is your global api token
DEV_CLOUDFLARE_API_TOKEN=your_development_api_token
URL_SCANNER_API_TOKEN=your_development_url_scanner_api_token
```

2. Start the local development server:
Expand All @@ -40,7 +38,6 @@ 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
Expand Down
2 changes: 1 addition & 1 deletion apps/radar/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ This MCP server is still a work in progress, and we plan to add more tools in th
- `Show me HTTP traffic trends from Portugal.`
- `Show me application layer attack trends from the last 7 days.`

## Access the remote MCP server from from any MCP Client
## Access the remote MCP server from any MCP Client

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

Expand Down
30 changes: 27 additions & 3 deletions apps/radar/src/context.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,41 @@
import type { RadarMCP } from './index'
import type { RadarMCP, UserDetails } 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>
USER_DETAILS: DurableObjectNamespace<UserDetails>
MCP_METRICS: AnalyticsEngineDataset
DEV_DISABLE_OAUTH: string
DEV_CLOUDFLARE_API_TOKEN: string
DEV_CLOUDFLARE_EMAIL: string
}

export const BASE_INSTRUCTIONS = /* markdown */ `
# Cloudflare Radar MCP Server

This server integrates tools powered by the Cloudflare Radar API to provide insights into global Internet traffic,
trends, and other related utilities.

An active account is **only required** for URL Scanner-related tools (e.g., \`scan_url\`).

For tools related to Internet trends and insights, analyze the results and, when appropriate, generate visualizations
such as XY charts, pie charts, bar charts, or other relevant chart types.

### Making comparisons

Many tools support **array-based filters** to enable comparisons across multiple criteria.
In such cases, the array index corresponds to a distinct data series.
For each data series, provide a corresponding \`dateRange\`, or alternatively a \`dateStart\` and \`dateEnd\` pair.
Example: To compare HTTP traffic between Portugal and Spain over the last 7 days:
- \`dateRange: ["7d", "7d"]\`
- \`location: ["PT", "ES"]\`

This applies to date filters and other filters that support comparison across multiple values.
If a tool does **not** support array-based filters, you can achieve the same comparison by making multiple separate
calls to the tool.
`
46 changes: 36 additions & 10 deletions apps/radar/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@ import {
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'
import { getEnv } from '@repo/mcp-common/src/env'
import { RequiredScopes } from '@repo/mcp-common/src/scopes'
import { CloudflareMCPServer } from '@repo/mcp-common/src/server'
import { registerAccountTools } from '@repo/mcp-common/src/tools/account'
import { MetricsTracker } from '@repo/mcp-observability'

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

Expand All @@ -19,6 +22,8 @@ import type { Env } from './context'

const env = getEnv<Env>()

export { UserDetails }

const metrics = new MetricsTracker(env.MCP_METRICS, {
name: env.MCP_SERVER_NAME,
version: env.MCP_SERVER_VERSION,
Expand All @@ -27,15 +32,13 @@ const metrics = new MetricsTracker(env.MCP_METRICS, {
// Context from the auth process, encrypted & stored in the auth token
// and provided to the DurableMCP as this.props
type Props = AuthProps

type State = never
type State = { activeAccountId: string | null }

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')
Expand All @@ -44,10 +47,7 @@ export class RadarMCP extends McpAgent<Env, State, Props> {
return this._server
}

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

Expand All @@ -59,16 +59,42 @@ export class RadarMCP extends McpAgent<Env, State, Props> {
name: this.env.MCP_SERVER_NAME,
version: this.env.MCP_SERVER_VERSION,
},
options: { instructions: BASE_INSTRUCTIONS },
})

registerAccountTools(this)
registerRadarTools(this)
registerUrlScannerTools(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)
}
}
}

// TODO add radar:read and url_scanner:write scopes once they are available
// Also remove URL_SCANNER_API_TOKEN env var
const RadarScopes = { ...RequiredScopes } as const
const RadarScopes = {
...RequiredScopes,
'account:read': 'See your account info such as account details, analytics, and memberships.',
'radar:read': 'Grants access to read Cloudflare Radar data.',
'url_scanner:write': 'Grants write level access to URL Scanner',
} as const

export default {
fetch: async (req: Request, env: Env, ctx: ExecutionContext) => {
Expand Down
16 changes: 4 additions & 12 deletions apps/radar/src/tools/radar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ export function registerRadarTools(agent: RadarMCP) {

agent.server.tool(
'get_traffic_anomalies',
'Get traffic anomalies',
'Get traffic anomalies and outages',
{
limit: PaginationLimitParam,
offset: PaginationOffsetParam,
Expand Down Expand Up @@ -188,9 +188,7 @@ export function registerRadarTools(agent: RadarMCP) {

agent.server.tool(
'get_domains_ranking',
'Get top or trending domains' +
'Use arrays to compare multiple filters — the array index determines which series each filter value belongs to.' +
'For each filter series, you must provide a corresponding `date`.',
'Get top or trending domains',
{
limit: PaginationLimitParam,
date: DateListParam.optional(),
Expand Down Expand Up @@ -267,10 +265,7 @@ export function registerRadarTools(agent: RadarMCP) {

agent.server.tool(
'get_http_requests_data',
'Retrieve HTTP requests traffic trends. ' +
'Use arrays to compare multiple filters — the array index determines which series each filter value belongs to.' +
'For each filter series, you must provide a corresponding `dateRange`, or a `dateStart`/`dateEnd` pair. ' +
'Analyze the results and generate visualizations when appropriate.',
'Retrieve HTTP requests traffic trends.',
{
dateRange: DateRangeArrayParam.optional(),
dateStart: DateStartArrayParam.optional(),
Expand Down Expand Up @@ -327,10 +322,7 @@ export function registerRadarTools(agent: RadarMCP) {

agent.server.tool(
'get_l7_attack_data',
'Retrieve application layer (L7) attack trends. ' +
'Use arrays to compare multiple filters — the array index determines which series each filter value belongs to.' +
'For each filter series, you must provide a corresponding `dateRange`, or a `dateStart`/`dateEnd` pair. ' +
'Analyze the results and generate visualizations when appropriate.',
'Retrieve application layer (L7) attack trends.',
{
dateRange: DateRangeArrayParam.optional(),
dateStart: DateStartArrayParam.optional(),
Expand Down
62 changes: 42 additions & 20 deletions apps/radar/src/tools/url-scanner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,33 +16,55 @@ export function registerUrlScannerTools(agent: RadarMCP) {
url: UrlParam,
},
async ({ url }) => {
const accountId = await agent.getActiveAccountId()
if (!accountId) {
return {
content: [
{
type: 'text',
text: 'No currently active accountId. Try listing your accounts (accounts_list) and then setting an active account (set_active_account)',
},
],
}
}

try {
const client = getCloudflareClient(agent.props.accessToken)
const account_id = agent.env.ACCOUNT_ID
const headers = {
Authorization: `Bearer ${agent.env.URL_SCANNER_API_TOKEN}`,
}

// TODO investigate why this does not work
// const scan = await (client.urlScanner.scans.create({ account_id, url: "https://www.example.com" }, { headers })).withResponse()
// Search if there are recent scans for the URL
const scans = await client.urlScanner.scans.list({
account_id: accountId,
q: `page.url:"${url}"`,
})

let scanId = scans.results.length > 0 ? scans.results[0]._id : null

if (!scanId) {
// Submit scan
// TODO theres an issue (reported) with this method in the cloudflare TS lib
// const scan = await (client.urlScanner.scans.create({ account_id, url: "https://www.example.com" }, { headers })).withResponse()

const res = await fetch(
`https://api.cloudflare.com/client/v4/accounts/${account_id}/urlscanner/v2/scan`,
{
method: 'POST',
headers,
body: JSON.stringify({ url }),
const res = await fetch(
`https://api.cloudflare.com/client/v4/accounts/${accountId}/urlscanner/v2/scan`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${agent.props.accessToken}`,
},
body: JSON.stringify({ url }),
}
)

if (!res.ok) {
throw new Error('Failed to submit scan')
}
)
if (!res.ok) {
throw new Error('Failed to submit scan')
}

const scan = CreateScanResult.parse(await res.json())
const scanId = scan?.uuid
const scan = CreateScanResult.parse(await res.json())
scanId = scan?.uuid
}

const r = await pollUntilReady({
taskFn: () => client.urlScanner.scans.get(scanId, { account_id }, { headers }),
taskFn: () => client.urlScanner.scans.get(scanId, { account_id: accountId }),
intervalSeconds: INTERVAL_SECONDS,
maxWaitSeconds: MAX_WAIT_SECONDS,
})
Expand All @@ -52,7 +74,7 @@ export function registerUrlScannerTools(agent: RadarMCP) {
{
type: 'text',
text: JSON.stringify({
result: r, // TODO select what is more relevant, or add a param to allow the agent to select a set of metrics
result: { verdicts: r.verdicts, stats: r.stats, page: r.page }, // TODO select what is more relevant, or add a param to allow the agent to select a set of metrics
}),
},
],
Expand Down
Loading
Loading