Skip to content

Commit d98f86d

Browse files
committed
Add Radar and URL Scanner OAuth scopes
1 parent 8bc731b commit d98f86d

File tree

6 files changed

+68
-38
lines changed

6 files changed

+68
-38
lines changed

apps/radar/.dev.vars.example

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
CLOUDFLARE_CLIENT_ID=
22
CLOUDFLARE_CLIENT_SECRET=
3-
URL_SCANNER_API_TOKEN=
43
DEV_DISABLE_OAUTH=
54
DEV_CLOUDFLARE_API_TOKEN=
6-
DEV_CLOUDFLARE_EMAIL=
5+
DEV_CLOUDFLARE_EMAIL=

apps/radar/CONTRIBUTING.md

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ If you'd like to iterate and test your MCP server, you can do so in local develo
1111
```
1212
CLOUDFLARE_CLIENT_ID=your_development_cloudflare_client_id
1313
CLOUDFLARE_CLIENT_SECRET=your_development_cloudflare_client_secret
14-
URL_SCANNER_API_TOKEN=your_development_url_scanner_api_token
1514
```
1615

1716
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
2120
DEV_CLOUDFLARE_EMAIL=your_cloudflare_email
2221
# This is your global api token
2322
DEV_CLOUDFLARE_API_TOKEN=your_development_api_token
24-
URL_SCANNER_API_TOKEN=your_development_url_scanner_api_token
2523
```
2624

2725
2. Start the local development server:
@@ -40,7 +38,6 @@ Set secrets via Wrangler:
4038
```bash
4139
npx wrangler secret put CLOUDFLARE_CLIENT_ID -e <ENVIRONMENT>
4240
npx wrangler secret put CLOUDFLARE_CLIENT_SECRET -e <ENVIRONMENT>
43-
npx wrangler secret put URL_SCANNER_API_TOKEN -e <ENVIRONMENT>
4441
```
4542

4643
## Set up a KV namespace

apps/radar/src/context.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
1-
import type { RadarMCP } from './index'
1+
import type { RadarMCP, UserDetails } from './index'
22

33
export interface Env {
44
OAUTH_KV: KVNamespace
55
ENVIRONMENT: 'development' | 'staging' | 'production'
6-
ACCOUNT_ID: '6702657b6aa048cf3081ff3ff3c9c52f'
76
MCP_SERVER_NAME: string
87
MCP_SERVER_VERSION: string
98
CLOUDFLARE_CLIENT_ID: string
109
CLOUDFLARE_CLIENT_SECRET: string
11-
URL_SCANNER_API_TOKEN: string
1210
MCP_OBJECT: DurableObjectNamespace<RadarMCP>
11+
USER_DETAILS: DurableObjectNamespace<UserDetails>
1312
MCP_METRICS: AnalyticsEngineDataset
1413
DEV_DISABLE_OAUTH: string
1514
DEV_CLOUDFLARE_API_TOKEN: string

apps/radar/src/index.ts

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@ import {
66
handleTokenExchangeCallback,
77
} from '@repo/mcp-common/src/cloudflare-oauth-handler'
88
import { handleDevMode } from '@repo/mcp-common/src/dev-mode'
9+
import { getUserDetails, UserDetails } from '@repo/mcp-common/src/durable-objects/user_details'
910
import { getEnv } from '@repo/mcp-common/src/env'
1011
import { RequiredScopes } from '@repo/mcp-common/src/scopes'
1112
import { CloudflareMCPServer } from '@repo/mcp-common/src/server'
13+
import { registerAccountTools } from '@repo/mcp-common/src/tools/account'
1214
import { MetricsTracker } from '@repo/mcp-observability'
1315

1416
import { registerRadarTools } from './tools/radar'
@@ -19,6 +21,8 @@ import type { Env } from './context'
1921

2022
const env = getEnv<Env>()
2123

24+
export { UserDetails }
25+
2226
const metrics = new MetricsTracker(env.MCP_METRICS, {
2327
name: env.MCP_SERVER_NAME,
2428
version: env.MCP_SERVER_VERSION,
@@ -27,15 +31,13 @@ const metrics = new MetricsTracker(env.MCP_METRICS, {
2731
// Context from the auth process, encrypted & stored in the auth token
2832
// and provided to the DurableMCP as this.props
2933
type Props = AuthProps
30-
31-
type State = never
34+
type State = { activeAccountId: string | null }
3235

3336
export class RadarMCP extends McpAgent<Env, State, Props> {
3437
_server: CloudflareMCPServer | undefined
3538
set server(server: CloudflareMCPServer) {
3639
this._server = server
3740
}
38-
3941
get server(): CloudflareMCPServer {
4042
if (!this._server) {
4143
throw new Error('Tried to access server before it was initialized')
@@ -44,10 +46,7 @@ export class RadarMCP extends McpAgent<Env, State, Props> {
4446
return this._server
4547
}
4648

47-
constructor(
48-
public ctx: DurableObjectState,
49-
public env: Env
50-
) {
49+
constructor(ctx: DurableObjectState, env: Env) {
5150
super(ctx, env)
5251
}
5352

@@ -61,15 +60,38 @@ export class RadarMCP extends McpAgent<Env, State, Props> {
6160
},
6261
})
6362

63+
registerAccountTools(this)
6464
registerRadarTools(this)
6565
registerUrlScannerTools(this)
6666
}
67+
68+
async getActiveAccountId() {
69+
try {
70+
// Get UserDetails Durable Object based off the userId and retrieve the activeAccountId from it
71+
// we do this so we can persist activeAccountId across sessions
72+
const userDetails = getUserDetails(env, this.props.user.id)
73+
return await userDetails.getActiveAccountId()
74+
} catch (e) {
75+
this.server.recordError(e)
76+
return null
77+
}
78+
}
79+
80+
async setActiveAccountId(accountId: string) {
81+
try {
82+
const userDetails = getUserDetails(env, this.props.user.id)
83+
await userDetails.setActiveAccountId(accountId)
84+
} catch (e) {
85+
this.server.recordError(e)
86+
}
87+
}
6788
}
6889

6990
const RadarScopes = {
7091
...RequiredScopes,
71-
// TODO 'radar:read': 'Grants access to read Cloudflare Radar data.',
72-
// TODO 'url_scanner:write': 'Grants write level access to URL Scanner', // Remove URL_SCANNER_API_TOKEN env var
92+
'account:read': 'See your account info such as account details, analytics, and memberships.',
93+
'radar:read': 'Grants access to read Cloudflare Radar data.',
94+
'url_scanner:write': 'Grants write level access to URL Scanner',
7395
} as const
7496

7597
export default {

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

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,36 +16,42 @@ export function registerUrlScannerTools(agent: RadarMCP) {
1616
url: UrlParam,
1717
},
1818
async ({ url }) => {
19+
const accountId = await agent.getActiveAccountId()
20+
if (!accountId) {
21+
return {
22+
content: [
23+
{
24+
type: 'text',
25+
text: 'No currently active accountId. Try listing your accounts (accounts_list) and then setting an active account (set_active_account)',
26+
},
27+
],
28+
}
29+
}
30+
1931
try {
2032
const client = getCloudflareClient(agent.props.accessToken)
21-
const account_id = agent.env.ACCOUNT_ID
22-
const headers = {
23-
Authorization: `Bearer ${agent.env.URL_SCANNER_API_TOKEN}`,
24-
}
2533

26-
// TODO search if there are recent scans for the URL
27-
const scans = await client.urlScanner.scans.list(
28-
{
29-
account_id,
30-
size: 1,
31-
q: `page.url:${url}`,
32-
},
33-
{ headers }
34-
)
35-
console.log(scans)
34+
// Search if there are recent scans for the URL
35+
const scans = await client.urlScanner.scans.list({
36+
account_id: accountId,
37+
size: 1,
38+
q: `page.url:${url}`,
39+
})
3640

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

3943
if (!scanId) {
4044
// Submit scan
41-
// TODO investigate why this does not work
45+
// TODO theres an issue (reported) with this method in the cloudflare TS lib
4246
// const scan = await (client.urlScanner.scans.create({ account_id, url: "https://www.example.com" }, { headers })).withResponse()
4347

4448
const res = await fetch(
45-
`https://api.cloudflare.com/client/v4/accounts/${account_id}/urlscanner/v2/scan`,
49+
`https://api.cloudflare.com/client/v4/accounts/${accountId}/urlscanner/v2/scan`,
4650
{
4751
method: 'POST',
48-
headers,
52+
headers: {
53+
Authorization: `Bearer ${agent.props.accessToken}`,
54+
},
4955
body: JSON.stringify({ url }),
5056
}
5157
)
@@ -59,7 +65,7 @@ export function registerUrlScannerTools(agent: RadarMCP) {
5965
}
6066

6167
const r = await pollUntilReady({
62-
taskFn: () => client.urlScanner.scans.get(scanId, { account_id }, { headers }),
68+
taskFn: () => client.urlScanner.scans.get(scanId, { account_id: accountId }),
6369
intervalSeconds: INTERVAL_SECONDS,
6470
maxWaitSeconds: MAX_WAIT_SECONDS,
6571
})

apps/radar/wrangler.jsonc

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@
3333
],
3434
"vars": {
3535
"ENVIRONMENT": "development",
36-
"ACCOUNT_ID": "6702657b6aa048cf3081ff3ff3c9c52f",
3736
"MCP_SERVER_NAME": "Cloudflare Radar Remote MCP Server - Dev",
3837
"MCP_SERVER_VERSION": "1.0.0"
3938
},
@@ -58,6 +57,11 @@
5857
{
5958
"class_name": "RadarMCP",
6059
"name": "MCP_OBJECT"
60+
},
61+
{
62+
"class_name": "UserDetails",
63+
"name": "USER_DETAILS",
64+
"script_name": "mcp-cloudflare-workers-observability-staging"
6165
}
6266
]
6367
},
@@ -69,7 +73,6 @@
6973
],
7074
"vars": {
7175
"ENVIRONMENT": "staging",
72-
"ACCOUNT_ID": "6702657b6aa048cf3081ff3ff3c9c52f",
7376
"MCP_SERVER_NAME": "Cloudflare Radar Remote MCP Server - Staging",
7477
"MCP_SERVER_VERSION": "1.0.0"
7578
},
@@ -89,6 +92,11 @@
8992
{
9093
"class_name": "RadarMCP",
9194
"name": "MCP_OBJECT"
95+
},
96+
{
97+
"class_name": "UserDetails",
98+
"name": "USER_DETAILS",
99+
"script_name": "mcp-cloudflare-workers-observability-production"
92100
}
93101
]
94102
},
@@ -100,7 +108,6 @@
100108
],
101109
"vars": {
102110
"ENVIRONMENT": "production",
103-
"ACCOUNT_ID": "6702657b6aa048cf3081ff3ff3c9c52f",
104111
"MCP_SERVER_NAME": "Cloudflare Radar Remote MCP Server",
105112
"MCP_SERVER_VERSION": "1.0.0"
106113
},

0 commit comments

Comments
 (0)