diff --git a/apps/ai-gateway/README.md b/apps/ai-gateway/README.md index 90c309cf..721a86a4 100644 --- a/apps/ai-gateway/README.md +++ b/apps/ai-gateway/README.md @@ -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/)). diff --git a/apps/auditlogs/README.md b/apps/auditlogs/README.md index aa4afc6a..31748907 100644 --- a/apps/auditlogs/README.md +++ b/apps/auditlogs/README.md @@ -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/)). diff --git a/apps/autorag/README.md b/apps/autorag/README.md index 47fdf1cb..cfcffbd5 100644 --- a/apps/autorag/README.md +++ b/apps/autorag/README.md @@ -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/)). diff --git a/apps/browser-rendering/README.md b/apps/browser-rendering/README.md index 33e7c74f..287b512e 100644 --- a/apps/browser-rendering/README.md +++ b/apps/browser-rendering/README.md @@ -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/)). diff --git a/apps/dns-analytics/README.md b/apps/dns-analytics/README.md index 386ae8e5..91687d1c 100644 --- a/apps/dns-analytics/README.md +++ b/apps/dns-analytics/README.md @@ -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/)). diff --git a/apps/logpush/README.md b/apps/logpush/README.md index 84cd185c..09b38224 100644 --- a/apps/logpush/README.md +++ b/apps/logpush/README.md @@ -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 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/)). diff --git a/apps/radar/.dev.vars.example b/apps/radar/.dev.vars.example index 7c6e82bf..860dc82f 100644 --- a/apps/radar/.dev.vars.example +++ b/apps/radar/.dev.vars.example @@ -1,6 +1,5 @@ CLOUDFLARE_CLIENT_ID= CLOUDFLARE_CLIENT_SECRET= -URL_SCANNER_API_TOKEN= DEV_DISABLE_OAUTH= DEV_CLOUDFLARE_API_TOKEN= -DEV_CLOUDFLARE_EMAIL= \ No newline at end of file +DEV_CLOUDFLARE_EMAIL= diff --git a/apps/radar/CONTRIBUTING.md b/apps/radar/CONTRIBUTING.md index 390a0164..20e6cf61 100644 --- a/apps/radar/CONTRIBUTING.md +++ b/apps/radar/CONTRIBUTING.md @@ -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: @@ -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: @@ -40,7 +38,6 @@ Set secrets via Wrangler: ```bash npx wrangler secret put CLOUDFLARE_CLIENT_ID -e npx wrangler secret put CLOUDFLARE_CLIENT_SECRET -e -npx wrangler secret put URL_SCANNER_API_TOKEN -e ``` ## Set up a KV namespace diff --git a/apps/radar/README.md b/apps/radar/README.md index 463c0292..0241254f 100644 --- a/apps/radar/README.md +++ b/apps/radar/README.md @@ -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/)). diff --git a/apps/radar/src/context.ts b/apps/radar/src/context.ts index e9572078..128c3532 100644 --- a/apps/radar/src/context.ts +++ b/apps/radar/src/context.ts @@ -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 + USER_DETAILS: DurableObjectNamespace 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. +` diff --git a/apps/radar/src/index.ts b/apps/radar/src/index.ts index 363833c8..765ce96c 100644 --- a/apps/radar/src/index.ts +++ b/apps/radar/src/index.ts @@ -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' @@ -19,6 +22,8 @@ import type { Env } from './context' const env = getEnv() +export { UserDetails } + const metrics = new MetricsTracker(env.MCP_METRICS, { name: env.MCP_SERVER_NAME, version: env.MCP_SERVER_VERSION, @@ -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 { _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') @@ -44,10 +47,7 @@ export class RadarMCP extends McpAgent { return this._server } - constructor( - public ctx: DurableObjectState, - public env: Env - ) { + constructor(ctx: DurableObjectState, env: Env) { super(ctx, env) } @@ -59,16 +59,42 @@ export class RadarMCP extends McpAgent { 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) => { diff --git a/apps/radar/src/tools/radar.ts b/apps/radar/src/tools/radar.ts index ab43bd62..9ad7e414 100644 --- a/apps/radar/src/tools/radar.ts +++ b/apps/radar/src/tools/radar.ts @@ -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, @@ -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(), @@ -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(), @@ -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(), diff --git a/apps/radar/src/tools/url-scanner.ts b/apps/radar/src/tools/url-scanner.ts index 0d30aba7..4d029a47 100644 --- a/apps/radar/src/tools/url-scanner.ts +++ b/apps/radar/src/tools/url-scanner.ts @@ -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, }) @@ -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 }), }, ], diff --git a/apps/radar/wrangler.jsonc b/apps/radar/wrangler.jsonc index 233b097f..32cdd477 100644 --- a/apps/radar/wrangler.jsonc +++ b/apps/radar/wrangler.jsonc @@ -33,7 +33,6 @@ ], "vars": { "ENVIRONMENT": "development", - "ACCOUNT_ID": "6702657b6aa048cf3081ff3ff3c9c52f", "MCP_SERVER_NAME": "Cloudflare Radar Remote MCP Server - Dev", "MCP_SERVER_VERSION": "1.0.0" }, @@ -58,6 +57,11 @@ { "class_name": "RadarMCP", "name": "MCP_OBJECT" + }, + { + "class_name": "UserDetails", + "name": "USER_DETAILS", + "script_name": "mcp-cloudflare-workers-observability-staging" } ] }, @@ -69,7 +73,6 @@ ], "vars": { "ENVIRONMENT": "staging", - "ACCOUNT_ID": "6702657b6aa048cf3081ff3ff3c9c52f", "MCP_SERVER_NAME": "Cloudflare Radar Remote MCP Server - Staging", "MCP_SERVER_VERSION": "1.0.0" }, @@ -89,6 +92,11 @@ { "class_name": "RadarMCP", "name": "MCP_OBJECT" + }, + { + "class_name": "UserDetails", + "name": "USER_DETAILS", + "script_name": "mcp-cloudflare-workers-observability-production" } ] }, @@ -100,7 +108,6 @@ ], "vars": { "ENVIRONMENT": "production", - "ACCOUNT_ID": "6702657b6aa048cf3081ff3ff3c9c52f", "MCP_SERVER_NAME": "Cloudflare Radar Remote MCP Server", "MCP_SERVER_VERSION": "1.0.0" }, diff --git a/apps/sandbox-container/README.md b/apps/sandbox-container/README.md index 61fbe84a..556b1eb1 100644 --- a/apps/sandbox-container/README.md +++ b/apps/sandbox-container/README.md @@ -24,7 +24,7 @@ This MCP server is still a work in progress, and we plan to add more tools in th - `Clone and explore this github repo: [repo link]. Setup and run the tests in your development environment` - `Analyze this data using Python` -## 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://bindings.mcp.cloudflare.com`) directly within its interface (for example in [Cloudflare AI Playground](https://playground.ai.cloudflare.com/)). diff --git a/apps/workers-bindings/README.md b/apps/workers-bindings/README.md index f42ee3ad..096886ef 100644 --- a/apps/workers-bindings/README.md +++ b/apps/workers-bindings/README.md @@ -62,7 +62,7 @@ This MCP server is still a work in progress, and we plan to add more tools in th - `Update the cache settings for Hyperdrive config 'YOUR_HYPERDRIVE_ID'.` (Replace YOUR_HYPERDRIVE_ID) - `Delete the Hyperdrive config 'OLD_HYPERDRIVE_ID'.` (Replace OLD_HYPERDRIVE_ID) -## 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://bindings.mcp.cloudflare.com`) directly within its interface (for example in [Cloudflare AI Playground](https://playground.ai.cloudflare.com/)). diff --git a/apps/workers-observability/README.md b/apps/workers-observability/README.md index effae9aa..1b1ae73f 100644 --- a/apps/workers-observability/README.md +++ b/apps/workers-observability/README.md @@ -26,7 +26,7 @@ This MCP server is still a work in progress, and we plan to add more tools in th - `How many requests were made to my worker 'my-app' broken down by HTTP status code?` - `Compare the error rates between my production and staging workers` -## 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://observability.mcp.cloudflare.com`) directly within its interface (for example in [Cloudflare AI Playground](https://playground.ai.cloudflare.com/)).