Skip to content

Commit ff47a35

Browse files
committed
add get_http_requests_data tool
1 parent ac8b912 commit ff47a35

File tree

4 files changed

+196
-37
lines changed

4 files changed

+196
-37
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ Some features may require a paid Cloudflare Workers plan. Ensure your Cloudflare
5555
### Apps
5656

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

5960
### Packages
6061

apps/radar/README.md

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,23 @@
1-
# Model Context Protocol (MCP) Server + Cloudflare Radar
1+
# Cloudflare Radar MCP Server 📡
22

3-
This is a [Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction) server that supports remote MCP connections, with Cloudflare OAuth built-in.
3+
This is a [Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction) server that supports remote MCP
4+
connections, with Cloudflare OAuth built-in.
45

5-
It features tools powered by the [Cloudflare Radar API](https://developers.cloudflare.com/radar/).
6+
It integrates tools powered by the [Cloudflare Radar API](https://developers.cloudflare.com/radar/) to provide global
7+
Internet traffic insights, trends and other utilities.
68

7-
## Tools
9+
## 🔨 Available Tools
810

911
Currently available tools:
1012

11-
- `list_autonomous_systems`: Lists autonomous systems (filterable by location and sortable by population size)
12-
- `get_as_details`: Retrieves details of an autonomous system by ASN
13-
- `get_ip_details`: Provides information about a specific IP address
14-
- `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/)
13+
| **Category** | **Tool** | **Description** |
14+
|------------------------|---------------------------|---------------------------------------------------------------------------------------------------------------------------|
15+
| **HTTP Requests** | `get_http_requests_data` | Fetches HTTP request data (timeseries, summaries, and grouped timeseries across dimensions like `deviceType`, `botClass`) |
16+
| **Autonomous Systems** | `list_autonomous_systems` | Lists ASes; filter by location and sort by population size |
17+
| | `get_as_details` | Retrieves detailed info for a specific ASN |
18+
| **IP Addresses** | `get_ip_details` | Provides details about a specific IP address |
19+
| **Traffic Anomalies** | `get_traffic_anomalies` | Lists traffic anomalies; filter by AS, location, start date, and end date |
20+
| **URL Scanner** | `scan_url` | Scans a URL via [Cloudflare’s URL Scanner](https://developers.cloudflare.com/radar/investigate/url-scanner/) |
1621

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

@@ -59,17 +64,17 @@ This will require you to create another OAuth App on Cloudflare:
5964

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

62-
```
63-
CLOUDFLARE_CLIENT_ID=your_development_cloudflare_client_id
64-
CLOUDFLARE_CLIENT_SECRET=your_development_cloudflare_client_secret
65-
URL_SCANNER_API_TOKEN=your_development_url_scanner_api_token
66-
```
67+
```
68+
CLOUDFLARE_CLIENT_ID=your_development_cloudflare_client_id
69+
CLOUDFLARE_CLIENT_SECRET=your_development_cloudflare_client_secret
70+
URL_SCANNER_API_TOKEN=your_development_url_scanner_api_token
71+
```
6772

6873
2. Start the local development server:
6974

70-
```bash
71-
npx wrangler dev
72-
```
75+
```bash
76+
npx wrangler dev
77+
```
7378

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

apps/radar/src/tools/radar.ts

Lines changed: 79 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,21 @@ import { getCloudflareClient } from '@repo/mcp-common/src/cloudflare-api'
22
import { PaginationLimitParam, PaginationOffsetParam } from '@repo/mcp-common/src/types/shared'
33

44
import {
5+
AsnArrayParam,
56
AsnParam,
67
AsOrderByParam,
8+
ContinentArrayParam,
9+
DataFormatParam,
10+
DateEndArrayParam,
711
DateEndParam,
12+
DateRangeArrayParam,
13+
DateRangeParam,
14+
DateStartArrayParam,
815
DateStartParam,
16+
HttpDimensionParam,
917
IpParam,
10-
SingleLocationParam,
18+
LocationArrayParam,
19+
LocationParam,
1120
} from '../types/radar'
1221

1322
import type { RadarMCP } from '../index'
@@ -19,7 +28,7 @@ export function registerRadarTools(agent: RadarMCP) {
1928
{
2029
limit: PaginationLimitParam,
2130
offset: PaginationOffsetParam,
22-
location: SingleLocationParam,
31+
location: LocationParam.optional(),
2332
orderBy: AsOrderByParam,
2433
},
2534
async ({ limit, offset, location, orderBy }) => {
@@ -130,21 +139,22 @@ export function registerRadarTools(agent: RadarMCP) {
130139
limit: PaginationLimitParam,
131140
offset: PaginationOffsetParam,
132141
asn: AsnParam.optional(),
133-
location: SingleLocationParam,
134-
// TODO maybe we don't need this param since we have the other two dateRange: DateRangeParam.optional(),
135-
dateStart: DateStartParam,
136-
dateEnd: DateEndParam,
142+
location: LocationParam.optional(),
143+
dateRange: DateRangeParam.optional(),
144+
dateStart: DateStartParam.optional(),
145+
dateEnd: DateEndParam.optional(),
137146
},
138-
async ({ limit, offset, asn, location, dateStart, dateEnd }) => {
147+
async ({ limit, offset, asn, location, dateStart, dateEnd, dateRange }) => {
139148
try {
140149
const client = getCloudflareClient(agent.props.accessToken)
141150
const r = await client.radar.trafficAnomalies.get({
142151
limit: limit ?? undefined,
143152
offset: offset ?? undefined,
144153
asn: asn ?? undefined,
145154
location: location ?? undefined,
146-
dateStart: dateStart,
147-
dateEnd: dateEnd,
155+
dateRange: dateRange ?? undefined,
156+
dateStart: dateStart ?? undefined,
157+
dateEnd: dateEnd ?? undefined,
148158
status: 'VERIFIED',
149159
})
150160

@@ -170,4 +180,64 @@ export function registerRadarTools(agent: RadarMCP) {
170180
}
171181
}
172182
)
183+
184+
agent.server.tool(
185+
'get_http_requests_data',
186+
'Retrieve HTTP request trends. Provide either a `dateRange`, or both `dateStart` and `dateEnd`, to define the time window. ' +
187+
'Use arrays to compare multiple filters — the array index determines which series each filter value belongs to.' +
188+
'For each filter series, you must provide a corresponding `dateRange`, or a `dateStart`/`dateEnd` pair.',
189+
{
190+
dateRange: DateRangeArrayParam.optional(),
191+
dateStart: DateStartArrayParam.optional(),
192+
dateEnd: DateEndArrayParam.optional(),
193+
asn: AsnArrayParam,
194+
continent: ContinentArrayParam,
195+
location: LocationArrayParam,
196+
format: DataFormatParam,
197+
dimension: HttpDimensionParam,
198+
},
199+
async ({ dateStart, dateEnd, dateRange, asn, location, continent, format, dimension }) => {
200+
try {
201+
if (!dimension && format !== 'timeseries') {
202+
throw new Error(
203+
`Missing 'dimension' parameter. The '${format}' format requires a dimension to group the data.`
204+
)
205+
}
206+
207+
const client = getCloudflareClient(agent.props.accessToken)
208+
const endpoint = (...args: any) => format === 'timeseries'
209+
? client.radar.http[format](...args)
210+
: client.radar.http[format][dimension!](...args)
211+
212+
const r = await endpoint({
213+
asn: asn ?? undefined,
214+
continent: continent ?? undefined,
215+
location: location ?? undefined,
216+
dateRange: dateRange,
217+
dateStart: dateStart,
218+
dateEnd: dateEnd,
219+
})
220+
221+
return {
222+
content: [
223+
{
224+
type: 'text',
225+
text: JSON.stringify({
226+
result: r,
227+
}),
228+
},
229+
],
230+
}
231+
} catch (error) {
232+
return {
233+
content: [
234+
{
235+
type: 'text',
236+
text: `Error getting HTTP data: ${error instanceof Error && error.message}`,
237+
},
238+
],
239+
}
240+
}
241+
}
242+
)
173243
}

apps/radar/src/types/radar.ts

Lines changed: 93 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import { z } from 'zod'
55

66
import type { ASNListParams } from 'cloudflare/resources/radar/entities/asns.mjs'
7-
import type { TrafficAnomalyGetParams } from 'cloudflare/resources/radar/traffic-anomalies.mjs'
7+
import type { HTTPTimeseriesParams } from 'cloudflare/resources/radar/http.mjs'
88

99
export const AsnParam = z
1010
.number()
@@ -13,37 +13,120 @@ export const AsnParam = z
1313

1414
export const IpParam = z.string().ip().describe('IPv4 or IPv6 address in standard notation.')
1515

16-
export const DateRangeParam: z.ZodType<TrafficAnomalyGetParams['dateRange']> = z
16+
export const DateRangeParam = z
1717
.string()
1818
.toLowerCase()
1919
.regex(
20-
/^((([1-9]|[1-9][0-9]|[1-2][0-9][0-9]|3[0-5][0-9]|36[0-4])[d](control)?)|(([1-9]|[1-4][0-9]|5[0-2])[w](control)?))$/,
20+
/^((([1-9]|[1-9][0-9]|[1-2][0-9][0-9]|3[0-5][0-9]|36[0-4])d(control)?)|(([1-9]|[1-4][0-9]|5[0-2])w(control)?))$/,
2121
'Invalid Date Range'
2222
)
2323
.describe(
24-
"Date range string in the format of 'Xd' or 'Xw', where X is a number of days (1–364) or weeks (1–52), optionally suffixed with 'control'."
24+
'Filters results by date range. ' +
25+
'For example, use `7d` and `7dcontrol` to compare this week with the previous week. ' +
26+
'Use this parameter or set specific start and end dates (`dateStart` and `dateEnd` parameters).'
2527
)
2628

27-
export const DateStartParam: z.ZodType<TrafficAnomalyGetParams['dateStart']> = z
29+
export const DateRangeArrayParam: z.ZodType<HTTPTimeseriesParams['dateRange']> = z
30+
.array(DateRangeParam)
31+
.describe(
32+
'Filters results by date range. ' +
33+
'For example, use `7d` and `7dcontrol` to compare this week with the previous week. ' +
34+
'Use this parameter or set specific start and end dates (`dateStart` and `dateEnd` parameters).'
35+
)
36+
37+
export const DateStartParam = z
2838
.string()
2939
.datetime()
30-
.describe('Start date in ISO 8601 format (e.g., 2023-04-01T00:00:00Z).')
40+
.describe(
41+
'Start date in ISO 8601 format (e.g., 2023-04-01T00:00:00Z). ' +
42+
'Either use this parameter together with `dateEnd`, or use `dateRange`.'
43+
)
3144

32-
export const DateEndParam: z.ZodType<TrafficAnomalyGetParams['dateEnd']> = z
45+
export const DateStartArrayParam: z.ZodType<HTTPTimeseriesParams['dateStart']> = z
46+
.array(DateStartParam)
47+
.describe(
48+
'Start of the date range. ' +
49+
'Either use this parameter together with `dateEnd` or use `dateRange`.'
50+
)
51+
52+
export const DateEndParam = z
3353
.string()
3454
.datetime()
35-
.describe('End date in ISO 8601 format (e.g., 2023-04-30T23:59:59Z).')
55+
.describe(
56+
'End date in ISO 8601 format (e.g., 2023-04-30T23:59:59Z). ' +
57+
'Either use this parameter together with `dateStart`, or use `dateRange`.'
58+
)
59+
60+
export const DateEndArrayParam: z.ZodType<HTTPTimeseriesParams['dateEnd']> = z
61+
.array(DateEndParam)
62+
.describe(
63+
'End of the date range. ' +
64+
'Either use this parameter together with `dateStart`, or use `dateRange`.'
65+
)
3666

37-
export const SingleLocationParam: z.ZodType<ASNListParams['location']> = z
67+
export const LocationParam: z.ZodType<ASNListParams['location']> = z
3868
.string()
3969
.regex(/^[a-zA-Z]{2}$/, {
4070
message:
4171
'Invalid location code. Must be a valid alpha-2 location code (two letters, case insensitive).',
4272
})
73+
.describe('Filters results by location. Specify a valid alpha-2 location code.')
74+
75+
export const LocationArrayParam: z.ZodType<HTTPTimeseriesParams['location']> = z
76+
.array(
77+
z.string().regex(/^(-?[a-zA-Z]{2})$/, {
78+
message: 'Each value must be a valid alpha-2 location code, optionally prefixed with `-`.',
79+
})
80+
)
4381
.optional()
44-
.describe('Optional alpha-2 country code (e.g., "US", "DE").')
82+
.describe(
83+
'Filters results by location. Provide an array of alpha-2 country codes (e.g., "US", "PT"). ' +
84+
'Prefix a code with `-` to exclude it (e.g., ["-US", "PT"] excludes the US and includes Portugal).'
85+
)
86+
87+
export const ContinentArrayParam: z.ZodType<HTTPTimeseriesParams['continent']> = z
88+
.array(
89+
z.string().regex(/^(-?[a-zA-Z]{2})$/, {
90+
message: 'Each value must be a valid alpha-2 continent code, optionally prefixed with `-`.',
91+
})
92+
)
93+
.optional()
94+
.describe(
95+
'Filters results by continent. Provide an array of alpha-2 continent codes (e.g., "EU", "NA"). ' +
96+
'Prefix a code with `-` to exclude it (e.g., ["-EU", "NA"] excludes Europe and includes North America).'
97+
)
98+
99+
export const AsnArrayParam: z.ZodType<HTTPTimeseriesParams['asn']> = z
100+
.array(z.string().refine((val) => val !== '0', { message: 'ASN cannot be 0' }))
101+
.optional()
102+
.describe(
103+
'Filters results by ASN. Provide an array of ASN strings. ' +
104+
'Prefix with `-` to exclude (e.g., ["-174", "3356"] excludes AS174 and includes AS3356). '
105+
)
45106

46107
export const AsOrderByParam: z.ZodType<ASNListParams['orderBy']> = z
47108
.enum(['ASN', 'POPULATION'])
48109
.optional()
49110
.describe('Optional order by parameter: "ASN" or "POPULATION".')
111+
112+
export const DataFormatParam = z
113+
.enum(['timeseries', 'summary', 'timeseriesGroups'])
114+
.describe(
115+
"Specifies the data format: 'summary' for aggregated results by dimension, 'timeseries' for a time-based view of HTTP requests, or 'timeseriesGroups' to group timeseries data by dimensions."
116+
)
117+
118+
export const HttpDimensionParam = z
119+
.enum([
120+
'deviceType',
121+
'httpProtocol',
122+
'httpVersion',
123+
'botClass',
124+
'ipVersion',
125+
'tlsVersion',
126+
'os',
127+
'postQuantum',
128+
])
129+
.optional()
130+
.describe(
131+
"Dimension used to group HTTP data. Allowed only when the format is 'summary' or 'timeseriesGroups'."
132+
)

0 commit comments

Comments
 (0)