Skip to content

Commit 5b6c81d

Browse files
committed
Stronger validation of dex tool params
1 parent eb24e3b commit 5b6c81d

File tree

2 files changed

+37
-39
lines changed

2 files changed

+37
-39
lines changed

.changeset/dark-plants-drive.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'dex-analysis': minor
3+
---
4+
5+
Stronger validation of DEX tool params

apps/dex-analysis/src/tools/dex-analysis.tools.ts

Lines changed: 32 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export function registerDEXTools(agent: CloudflareDEXMCP) {
1515
name: 'dex_test_statistics',
1616
description: 'Analyze Cloudflare DEX Test Results by quartile given a Test ID',
1717
schema: {
18-
testId: z.string().describe('The DEX Test ID to analyze details of.'),
18+
testId: testIdParam.describe('The DEX Test ID to analyze details of.'),
1919
from: timeStartParam,
2020
to: timeEndParam,
2121
},
@@ -44,7 +44,7 @@ export function registerDEXTools(agent: CloudflareDEXMCP) {
4444
schema: { page: pageParam },
4545
callback: async ({ accountId, accessToken, page }) => {
4646
return await fetchCloudflareApi({
47-
endpoint: `/dex/tests/overview?page=${page}&per_page=50`,
47+
endpoint: `/dex/tests/overview?${new URLSearchParams({page: String(page), per_page: '50'})}`,
4848
accountId,
4949
apiToken: accessToken,
5050
options: {
@@ -61,17 +61,13 @@ export function registerDEXTools(agent: CloudflareDEXMCP) {
6161
name: 'dex_http_test_details',
6262
description: 'Retrieve detailed time series results for an HTTP DEX test by id.',
6363
schema: {
64-
testId: z.string().describe('The HTTP DEX Test ID to get details for.'),
65-
deviceId: z
66-
.string()
64+
testId: testIdParam.describe('The HTTP DEX Test ID to get details for.'),
65+
deviceId: deviceIdParam
6766
.optional()
6867
.describe(
6968
"Optionally limit results to specific device(s). Can't be used in conjunction with the colo parameter."
7069
),
71-
colo: z
72-
.string()
73-
.optional()
74-
.describe('Optionally limit results to a specific Cloudflare colo.'),
70+
colo: coloParam.optional(),
7571
from: timeStartParam,
7672
to: timeEndParam,
7773
interval: aggregationIntervalParam,
@@ -96,17 +92,13 @@ export function registerDEXTools(agent: CloudflareDEXMCP) {
9692
name: 'dex_traceroute_test_details',
9793
description: 'Retrieve detailed time series results for a Traceroute DEX test by id.',
9894
schema: {
99-
testId: z.string().describe('The traceroute DEX Test ID to get details for.'),
100-
deviceId: z
101-
.string()
95+
testId: testIdParam.describe('The traceroute DEX Test ID to get details for.'),
96+
deviceId: deviceIdParam
10297
.optional()
10398
.describe(
10499
"Optionally limit results to specific device(s). Can't be used in conjunction with the colo parameter."
105100
),
106-
colo: z
107-
.string()
108-
.optional()
109-
.describe('Optionally limit results to a specific Cloudflare colo.'),
101+
colo: coloParam.optional(),
110102
timeStart: timeStartParam,
111103
timeEnd: timeEndParam,
112104
interval: aggregationIntervalParam,
@@ -132,8 +124,8 @@ export function registerDEXTools(agent: CloudflareDEXMCP) {
132124
description:
133125
'Retrieve aggregate network path data for a Traceroute DEX test by id. Use the dex_traceroute_test_result_network_path tool to further explore individual test runs hop-by-hop.',
134126
schema: {
135-
testId: z.string().describe('The traceroute DEX Test ID to get network path details for.'),
136-
deviceId: z.string().describe('The ID of the device to get network path details for.'),
127+
testId: testIdParam.describe('The traceroute DEX Test ID to get network path details for.'),
128+
deviceId: deviceIdParam.describe('The ID of the device to get network path details for.'),
137129
from: timeStartParam,
138130
to: timeEndParam,
139131
interval: aggregationIntervalParam,
@@ -161,6 +153,7 @@ export function registerDEXTools(agent: CloudflareDEXMCP) {
161153
schema: {
162154
testResultId: z
163155
.string()
156+
.uuid()
164157
.describe('The traceroute DEX Test Result ID to get network path details for.'),
165158
},
166159
agent,
@@ -211,8 +204,8 @@ export function registerDEXTools(agent: CloudflareDEXMCP) {
211204
'Create a remote packet capture (PCAP) for a device. This is a resource intensive and privacy-sensitive operation on a real user device.' +
212205
'Always ask for confirmation from the user that the targeted email and device are correct before executing a capture',
213206
schema: {
214-
device_id: z.string().describe('The device ID to target.'),
215-
user_email: z.string().describe('The email of the user associated with the device.'),
207+
device_id: z.string().uuid().describe('The device ID to target.'),
208+
user_email: z.string().email().describe('The email of the user associated with the device.'),
216209
'max-file-size-mb': z
217210
.number()
218211
.min(1)
@@ -269,8 +262,8 @@ export function registerDEXTools(agent: CloudflareDEXMCP) {
269262
'Create a remote Warp Diagnostic (WARP-diag) for a device. This is a resource intensive and privacy-sensitive operation on a real user device.' +
270263
'Always ask for confirmation from the user that the targeted email and device are correct before executing a capture',
271264
schema: {
272-
device_id: z.string().describe('The device ID to target.'),
273-
user_email: z.string().describe('The email of the user associated with the device.'),
265+
device_id: z.string().uuid().describe('The device ID to target.'),
266+
user_email: z.string().email().describe('The email of the user associated with the device.'),
274267
'test-all-routes': z
275268
.boolean()
276269
.default(true)
@@ -317,7 +310,7 @@ export function registerDEXTools(agent: CloudflareDEXMCP) {
317310
agent,
318311
callback: async ({ accountId, accessToken, page }) => {
319312
return await fetchCloudflareApi({
320-
endpoint: `/dex/commands?page=${page}&per_page=50`,
313+
endpoint: `/dex/commands?${new URLSearchParams({ page: String(page), per_page: `50` })}`,
321314
accountId,
322315
apiToken: accessToken,
323316
options: {
@@ -343,10 +336,7 @@ export function registerDEXTools(agent: CloudflareDEXMCP) {
343336
.describe(
344337
'Number of minutes before current time to use as cutoff for device states to include.'
345338
),
346-
colo: z
347-
.string()
348-
.optional()
349-
.describe('Optionally filter results to a specific Cloudflare colo.'),
339+
colo: coloParam.optional(),
350340
},
351341
agent,
352342
callback: async ({ accountId, accessToken, ...params }) => {
@@ -372,11 +362,10 @@ export function registerDEXTools(agent: CloudflareDEXMCP) {
372362
from: timeStartParam,
373363
to: timeEndParam,
374364
interval: aggregationIntervalParam,
375-
colo: z
376-
.string()
365+
colo: coloParam
377366
.optional()
378367
.describe('Filter results to WARP devices connected to a specific colo.'),
379-
device_id: z.string().optional().describe('Filter results to a specific device.'),
368+
device_id: z.string().uuid().optional().describe('Filter results to a specific device.'),
380369
},
381370
agent,
382371
callback: async ({ accountId, accessToken, ...params }) => {
@@ -407,11 +396,8 @@ export function registerDEXTools(agent: CloudflareDEXMCP) {
407396
source: z
408397
.enum(['last_seen', 'hourly', 'raw'])
409398
.describe('Specifies the granularity of results.'),
410-
colo: z
411-
.string()
412-
.optional()
413-
.describe('Filter results to WARP devices connected to a specific colo.'),
414-
device_id: z.string().optional().describe('Filter results to a specific device.'),
399+
colo: coloParam.optional(),
400+
device_id: z.string().uuid().optional().describe('Filter results to a specific device.'),
415401
mode: z.string().optional().describe('Filter results to devices with a specific WARP mode.'),
416402
platform: z
417403
.string()
@@ -521,13 +507,13 @@ export function registerDEXTools(agent: CloudflareDEXMCP) {
521507
description:
522508
'Given a WARP diag remote capture id and device_id, returns a list of the files contained in the archive.',
523509
schema: {
524-
deviceId: z
525-
.string()
510+
deviceId: deviceIdParam
526511
.describe(
527512
'The device_id field of the successful WARP-diag remote capture response to list contents of.'
528513
),
529514
commandId: z
530515
.string()
516+
.uuid()
531517
.describe(
532518
'The id of the successful WARP-diag remote capture response to list contents of.'
533519
),
@@ -553,8 +539,8 @@ export function registerDEXTools(agent: CloudflareDEXMCP) {
553539
description:
554540
'Explore the contents of remote capture WARP diag archive filepaths returned by the dex_list_remote_warp_diag_contents tool for analysis.',
555541
schema: {
556-
commandId: z.string().describe('The id of the command results to explore'),
557-
deviceId: z.string().describe('The device_id field of command to explore'),
542+
commandId: z.string().uuid().describe('The id of the command results to explore'),
543+
deviceId: deviceIdParam.describe('The device_id field of command to explore'),
558544
filepath: z.string().describe('The file path from the archive to retrieve contents for.'),
559545
},
560546
llmContext:
@@ -578,6 +564,7 @@ export function registerDEXTools(agent: CloudflareDEXMCP) {
578564
schema: {
579565
command_id: z
580566
.string()
567+
.uuid()
581568
.describe('The command_id of the successful WARP-diag remote capture to analyze.'),
582569
},
583570
llmContext:
@@ -679,3 +666,9 @@ const aggregationIntervalParam = z
679666
.describe('The time interval to group results by.')
680667

681668
const pageParam = z.number().min(1).describe('The page of results to retrieve.')
669+
const coloParam = z
670+
.string()
671+
.regex(/^[A-Z]{3}$/, '3-letter colo codes only')
672+
.describe('Optionally filter results to a specific Cloudflare colo.')
673+
const deviceIdParam = z.string().uuid()
674+
const testIdParam = z.string().uuid()

0 commit comments

Comments
 (0)