Skip to content

Commit 813894d

Browse files
committed
add scan_url tool
1 parent 532dcf6 commit 813894d

File tree

7 files changed

+63
-12
lines changed

7 files changed

+63
-12
lines changed

apps/radar/.dev.vars.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
CLOUDFLARE_CLIENT_ID=
22
CLOUDFLARE_CLIENT_SECRET=
3+
URL_SCANNER_API_TOKEN=

apps/radar/README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Currently available tools:
1212
- `get_as_details`: Retrieves details of an autonomous system by ASN
1313
- `get_ip_details`: Provides information about a specific IP address
1414
- `get_traffic_anomalies`: Lists traffic anomalies (filterable by AS, location, start date, and end date)
15+
- `scan_url`: Scan a URL using [URL Scanner](https://developers.cloudflare.com/radar/investigate/url-scanner/)
1516

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

@@ -24,6 +25,7 @@ Set secrets via Wrangler (ask in the `Cloudflare's Own MCP Servers` internal cha
2425
```bash
2526
npx wrangler secret put CLOUDFLARE_CLIENT_ID -e <ENVIRONMENT>
2627
npx wrangler secret put CLOUDFLARE_CLIENT_SECRET -e <ENVIRONMENT>
28+
npx wrangler secret put URL_SCANNER_API_TOKEN -e <ENVIRONMENT>
2729
```
2830

2931
#### Set up a KV namespace
@@ -60,6 +62,7 @@ This will require you to create another OAuth App on Cloudflare:
6062
```
6163
CLOUDFLARE_CLIENT_ID=your_development_cloudflare_client_id
6264
CLOUDFLARE_CLIENT_SECRET=your_development_cloudflare_client_secret
65+
URL_SCANNER_API_TOKEN=your_development_url_scanner_api_token
6366
```
6467

6568
2. Start the local development server:
@@ -69,4 +72,4 @@ This will require you to create another OAuth App on Cloudflare:
6972
```
7073

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

apps/radar/src/index.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@ import {
99
} from '@repo/mcp-common/src/cloudflare-oauth-handler'
1010

1111
import { registerRadarTools } from './tools/radar'
12-
13-
// TODO import { registerUrlScannerTools } from "./tools/url-scanner";
12+
import { registerUrlScannerTools } from './tools/url-scanner'
1413

1514
// Context from the auth process, encrypted & stored in the auth token
1615
// and provided to the DurableMCP as this.props
@@ -35,7 +34,7 @@ export class RadarMCP extends McpAgent<Env, State, Props> {
3534

3635
async init() {
3736
registerRadarTools(this)
38-
// TODO registerUrlScannerTools(this)
37+
registerUrlScannerTools(this)
3938
}
4039
}
4140

apps/radar/src/tools/url-scanner.ts

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ import { UrlParam } from '../types/url-scanner'
44

55
import type { RadarMCP } from '../index'
66

7+
const MAX_WAIT_SECONDS = 30
8+
const INTERVAL_SECONDS = 2
9+
10+
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
11+
712
export function registerUrlScannerTools(agent: RadarMCP) {
813
agent.server.tool(
914
'scan_url',
@@ -13,18 +18,54 @@ export function registerUrlScannerTools(agent: RadarMCP) {
1318
},
1419
async ({ url }) => {
1520
try {
16-
const accountId = '' // TODO agent.getActiveAccountId()
1721
const client = getCloudflareClient(agent.props.accessToken)
18-
const scanId = await client.urlScanner.scans.create({ account_id: accountId, url })
22+
const account_id = agent.env.ACCOUNT_ID
23+
const headers = {
24+
Authorization: `Bearer ${agent.env.URL_SCANNER_API_TOKEN}`,
25+
}
26+
27+
// TODO investigate why this does not work
28+
// const scan = await (client.urlScanner.scans.create({ account_id, url: "https://www.example.com" }, { headers })).withResponse()
29+
30+
const res = await fetch(
31+
`https://api.cloudflare.com/client/v4/accounts/${account_id}/urlscanner/v2/scan`,
32+
{
33+
method: 'POST',
34+
headers,
35+
body: JSON.stringify({ url }),
36+
}
37+
)
38+
if (!res.ok) {
39+
throw new Error('Failed to submit scan')
40+
}
1941

20-
// TODO get scan...
42+
const scan: any = await res.json()
43+
const scanId = scan?.uuid
44+
45+
let r = null
46+
let elapsed = 0
47+
while (elapsed < MAX_WAIT_SECONDS) {
48+
try {
49+
r = await client.urlScanner.scans.get(scanId, { account_id }, { headers })
50+
} catch {
51+
// Wait
52+
}
53+
if (r) break
54+
55+
await sleep(INTERVAL_SECONDS * 1000)
56+
elapsed += INTERVAL_SECONDS
57+
}
58+
59+
if (!r) {
60+
throw new Error('Scan timed out or still not ready')
61+
}
2162

2263
return {
2364
content: [
2465
{
2566
type: 'text',
2667
text: JSON.stringify({
27-
result: scanId,
68+
result: r,
2869
}),
2970
},
3071
],

apps/radar/src/types/radar.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ export const DateRangeParam: z.ZodType<TrafficAnomalyGetParams['dateRange']> = z
1818
'Invalid Date Range'
1919
)
2020

21+
//TODO use ".describe("...")"
22+
2123
export const DateStartParam: z.ZodType<TrafficAnomalyGetParams['dateStart']> = z.string().datetime()
2224
export const DateEndParam: z.ZodType<TrafficAnomalyGetParams['dateEnd']> = z.string().datetime()
2325

apps/radar/worker-configuration.d.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1-
// Generated by Wrangler by running `wrangler types` (hash: 76516c74a96893c385d9411bff0cd3a6)
1+
// Generated by Wrangler by running `wrangler types` (hash: 4c15ae95f0b70565da83b0950cd1543a)
22
// Runtime types generated with [email protected] 2025-03-10 nodejs_compat
33
declare namespace Cloudflare {
44
interface Env {
55
OAUTH_KV: KVNamespace;
66
ENVIRONMENT: "development" | "staging" | "production";
7+
ACCOUNT_ID: "6702657b6aa048cf3081ff3ff3c9c52f";
78
CLOUDFLARE_CLIENT_ID: string;
89
CLOUDFLARE_CLIENT_SECRET: string;
10+
URL_SCANNER_API_TOKEN: string;
911
MCP_OBJECT: DurableObjectNamespace<import("./src/index").RadarMCP>;
1012
}
1113
}

apps/radar/wrangler.jsonc

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@
3232
}
3333
],
3434
"vars": {
35-
"ENVIRONMENT": "development"
35+
"ENVIRONMENT": "development",
36+
"ACCOUNT_ID": "6702657b6aa048cf3081ff3ff3c9c52f"
3637
},
3738
"dev": {
3839
"port": 8976
@@ -59,7 +60,8 @@
5960
}
6061
],
6162
"vars": {
62-
"ENVIRONMENT": "staging"
63+
"ENVIRONMENT": "staging",
64+
"ACCOUNT_ID": "6702657b6aa048cf3081ff3ff3c9c52f"
6365
}
6466
},
6567
"production": {
@@ -81,7 +83,8 @@
8183
}
8284
],
8385
"vars": {
84-
"ENVIRONMENT": "production"
86+
"ENVIRONMENT": "production",
87+
"ACCOUNT_ID": "6702657b6aa048cf3081ff3ff3c9c52f"
8588
}
8689
}
8790
}

0 commit comments

Comments
 (0)