diff --git a/apps/radar/README.md b/apps/radar/README.md index 0241254f..d8bcc468 100644 --- a/apps/radar/README.md +++ b/apps/radar/README.md @@ -10,17 +10,23 @@ Internet traffic insights, trends and other utilities. Currently available tools: -| **Category** | **Tool** | **Description** | -| ---------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | -| **HTTP Requests** | `get_http_requests_data` | Fetches HTTP request data (timeseries, summaries, and grouped timeseries across dimensions like `deviceType`, `botClass`) | -| **L7 Attacks** | `get_l7_attack_data` | Fetches L7 attack data (timeseries, summaries, and grouped timeseries across dimensions like `mitigationProduct`, `ipVersion`) | -| **Autonomous Systems** | `list_autonomous_systems` | Lists ASes; filter by location and sort by population size | -| | `get_as_details` | Retrieves detailed info for a specific ASN | -| **IP Addresses** | `get_ip_details` | Provides details about a specific IP address | -| **Traffic Anomalies** | `get_traffic_anomalies` | Lists traffic anomalies; filter by AS, location, start date, and end date | -| **Domains** | `get_domains_ranking` | Get top or trending domains | -| | `get_domain_rank_details` | Get domain rank details | -| **URL Scanner** | `scan_url` | Scans a URL via [Cloudflare’s URL Scanner](https://developers.cloudflare.com/radar/investigate/url-scanner/) | +| **Category** | **Tool** | **Description** | +| ---------------------- | ------------------------------- | --------------------------------------------------------------------------------------------------------------------- | +| **Autonomous Systems** | `list_autonomous_systems` | Lists ASes; filter by location and sort by population size | +| | `get_as_details` | Retrieves detailed info for a specific ASN | +| **Domains** | `get_domains_ranking` | Gets top or trending domains | +| | `get_domain_rank_details` | Gets domain rank details | +| **DNS** | `get_dns_data` | Retrieves DNS query data to 1.1.1.1, including timeseries, summaries, and breakdowns by dimensions like `queryType`. | +| **Email Routing** | `get_email_routing_data` | Retrieves Email Routing data, including timeseries, and breakdowns by dimensions like `encrypted`. | +| **Email Security** | `get_email_security_data` | Retrieves Email Security data, including timeseries, and breakdowns by dimensions like `threatCategory`. | +| **HTTP** | `get_http_data` | Retrieves HTTP request data, including timeseries, and breakdowns by dimensions like `deviceType`. | +| **IP Addresses** | `get_ip_details` | Provides details about a specific IP address | +| **Internet Services** | `get_internet_services_ranking` | Gets top Internet services | +| **Internet Speed** | `get_internet_speed_data` | Retrieves summary of bandwidth, latency, jitter, and packet loss, from the previous 90 days of Cloudflare Speed Test. | +| **Layer 3 Attacks** | `get_l3_attack_data` | Retrieves L3 attack data, including timeseries, top attacks, and breakdowns by dimensions like `protocol`. | +| **Layer 7 Attacks** | `get_l7_attack_data` | Retrieves L7 attack data, including timeseries, top attacks, and breakdowns by dimensions like `mitigationProduct`. | +| **Traffic Anomalies** | `get_traffic_anomalies` | Lists traffic anomalies and outages; filter by AS, location, start date, and end date | +| **URL Scanner** | `scan_url` | Scans a URL via [Cloudflare’s URL Scanner](https://developers.cloudflare.com/radar/investigate/url-scanner/) | This MCP server is still a work in progress, and we plan to add more tools in the future. diff --git a/apps/radar/src/context.ts b/apps/radar/src/context.ts index 128c3532..881b4b18 100644 --- a/apps/radar/src/context.ts +++ b/apps/radar/src/context.ts @@ -24,7 +24,7 @@ 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. +such as line charts, pie charts, bar charts, stacked area charts, choropleth maps, treemaps, or other relevant chart types. ### Making comparisons diff --git a/apps/radar/src/tools/radar.ts b/apps/radar/src/tools/radar.ts index 9ad7e414..127cf6e6 100644 --- a/apps/radar/src/tools/radar.ts +++ b/apps/radar/src/tools/radar.ts @@ -6,7 +6,6 @@ import { AsnParam, AsOrderByParam, ContinentArrayParam, - DataFormatParam, DateEndArrayParam, DateEndParam, DateListParam, @@ -14,15 +13,23 @@ import { DateRangeParam, DateStartArrayParam, DateStartParam, + DnsDimensionParam, DomainParam, DomainRankingTypeParam, + EmailRoutingDimensionParam, + EmailSecurityDimensionParam, HttpDimensionParam, + InternetServicesCategoryParam, + InternetSpeedDimensionParam, + InternetSpeedOrderByParam, IpParam, + L3AttackDimensionParam, L7AttackDimensionParam, LocationArrayParam, LocationListParam, LocationParam, } from '../types/radar' +import { resolveAndInvoke } from '../utils' import type { RadarMCP } from '../index' @@ -186,6 +193,46 @@ export function registerRadarTools(agent: RadarMCP) { } ) + agent.server.tool( + 'get_internet_services_ranking', + 'Get top Internet services', + { + limit: PaginationLimitParam, + date: DateListParam.optional(), + serviceCategory: InternetServicesCategoryParam.optional(), + }, + async ({ limit, date, serviceCategory }) => { + try { + const client = getCloudflareClient(agent.props.accessToken) + const r = await client.radar.ranking.internetServices.top({ + limit, + date, + serviceCategory, + }) + + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + result: r, + }), + }, + ], + } + } catch (error) { + return { + content: [ + { + type: 'text', + text: `Error getting Internet services ranking: ${error instanceof Error && error.message}`, + }, + ], + } + } + } + ) + agent.server.tool( 'get_domains_ranking', 'Get top or trending domains', @@ -264,8 +311,8 @@ export function registerRadarTools(agent: RadarMCP) { ) agent.server.tool( - 'get_http_requests_data', - 'Retrieve HTTP requests traffic trends.', + 'get_http_data', + 'Retrieve HTTP traffic trends.', { dateRange: DateRangeArrayParam.optional(), dateStart: DateStartArrayParam.optional(), @@ -273,22 +320,59 @@ export function registerRadarTools(agent: RadarMCP) { asn: AsnArrayParam, continent: ContinentArrayParam, location: LocationArrayParam, - format: DataFormatParam, dimension: HttpDimensionParam, }, - async ({ dateStart, dateEnd, dateRange, asn, location, continent, format, dimension }) => { + async ({ dateStart, dateEnd, dateRange, asn, location, continent, dimension }) => { try { - if (format !== 'timeseries' && !dimension) { - throw new Error(`The '${format}' format requires a 'dimension' to group the data.`) + const client = getCloudflareClient(agent.props.accessToken) + const r = await resolveAndInvoke(client.radar.http, dimension, { + asn, + continent, + location, + dateRange, + dateStart, + dateEnd, + }) + + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + result: r, + }), + }, + ], } + } catch (error) { + return { + content: [ + { + type: 'text', + text: `Error getting HTTP data: ${error instanceof Error && error.message}`, + }, + ], + } + } + } + ) + agent.server.tool( + 'get_dns_queries_data', + 'Retrieve trends in DNS queries to the 1.1.1.1 resolver.', + { + dateRange: DateRangeArrayParam.optional(), + dateStart: DateStartArrayParam.optional(), + dateEnd: DateEndArrayParam.optional(), + asn: AsnArrayParam, + continent: ContinentArrayParam, + location: LocationArrayParam, + dimension: DnsDimensionParam, + }, + async ({ dateStart, dateEnd, dateRange, asn, location, continent, dimension }) => { + try { const client = getCloudflareClient(agent.props.accessToken) - const endpoint = (...args: any) => - format === 'timeseries' - ? client.radar.http[format](...args) - : client.radar.http[format][dimension!](...args) - - const r = await endpoint({ + const r = await resolveAndInvoke(client.radar.dns, dimension, { asn, continent, location, @@ -312,7 +396,7 @@ export function registerRadarTools(agent: RadarMCP) { content: [ { type: 'text', - text: `Error getting HTTP data: ${error instanceof Error && error.message}`, + text: `Error getting DNS data: ${error instanceof Error && error.message}`, }, ], } @@ -330,22 +414,59 @@ export function registerRadarTools(agent: RadarMCP) { asn: AsnArrayParam, continent: ContinentArrayParam, location: LocationArrayParam, - format: DataFormatParam, dimension: L7AttackDimensionParam, }, - async ({ dateStart, dateEnd, dateRange, asn, location, continent, format, dimension }) => { + async ({ dateStart, dateEnd, dateRange, asn, location, continent, dimension }) => { try { - if (format !== 'timeseries' && !dimension) { - throw new Error(`The '${format}' format requires a 'dimension' to group the data.`) + const client = getCloudflareClient(agent.props.accessToken) + const r = await resolveAndInvoke(client.radar.attacks.layer7, dimension, { + asn, + continent, + location, + dateRange, + dateStart, + dateEnd, + }) + + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + result: r, + }), + }, + ], + } + } catch (error) { + return { + content: [ + { + type: 'text', + text: `Error getting L7 attack data: ${error instanceof Error && error.message}`, + }, + ], } + } + } + ) + agent.server.tool( + 'get_l3_attack_data', + 'Retrieve application layer (L3) attack trends.', + { + dateRange: DateRangeArrayParam.optional(), + dateStart: DateStartArrayParam.optional(), + dateEnd: DateEndArrayParam.optional(), + asn: AsnArrayParam, + continent: ContinentArrayParam, + location: LocationArrayParam, + dimension: L3AttackDimensionParam, + }, + async ({ dateStart, dateEnd, dateRange, asn, location, continent, dimension }) => { + try { const client = getCloudflareClient(agent.props.accessToken) - const endpoint = (...args: any) => - format === 'timeseries' - ? client.radar.attacks.layer7[format](...args) - : client.radar.attacks.layer7[format][dimension!](...args) - - const r = await endpoint({ + const r = await resolveAndInvoke(client.radar.attacks.layer3, dimension, { asn, continent, location, @@ -369,7 +490,137 @@ export function registerRadarTools(agent: RadarMCP) { content: [ { type: 'text', - text: `Error getting L7 attack data: ${error instanceof Error && error.message}`, + text: `Error getting L3 attack data: ${error instanceof Error && error.message}`, + }, + ], + } + } + } + ) + + agent.server.tool( + 'get_email_routing_data', + 'Retrieve Email Routing trends.', + { + dateRange: DateRangeArrayParam.optional(), + dateStart: DateStartArrayParam.optional(), + dateEnd: DateEndArrayParam.optional(), + dimension: EmailRoutingDimensionParam, + }, + async ({ dateStart, dateEnd, dateRange, dimension }) => { + try { + const client = getCloudflareClient(agent.props.accessToken) + const r = await resolveAndInvoke(client.radar.email.routing, dimension, { + dateRange, + dateStart, + dateEnd, + }) + + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + result: r, + }), + }, + ], + } + } catch (error) { + return { + content: [ + { + type: 'text', + text: `Error getting Email Routing data: ${error instanceof Error && error.message}`, + }, + ], + } + } + } + ) + + agent.server.tool( + 'get_email_security_data', + 'Retrieve Email Security trends.', + { + dateRange: DateRangeArrayParam.optional(), + dateStart: DateStartArrayParam.optional(), + dateEnd: DateEndArrayParam.optional(), + dimension: EmailSecurityDimensionParam, + }, + async ({ dateStart, dateEnd, dateRange, dimension }) => { + try { + const client = getCloudflareClient(agent.props.accessToken) + const r = await resolveAndInvoke(client.radar.email.security, dimension, { + dateRange, + dateStart, + dateEnd, + }) + + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + result: r, + }), + }, + ], + } + } catch (error) { + return { + content: [ + { + type: 'text', + text: `Error getting Email Security data: ${error instanceof Error && error.message}`, + }, + ], + } + } + } + ) + + agent.server.tool( + 'get_internet_speed_data', + 'Retrieve summary of bandwidth, latency, jitter, and packet loss, from the previous 90 days of Cloudflare Speed Test.', + { + dateEnd: DateEndArrayParam.optional(), + asn: AsnArrayParam, + continent: ContinentArrayParam, + location: LocationArrayParam, + dimension: InternetSpeedDimensionParam, + orderBy: InternetSpeedOrderByParam.optional(), + }, + async ({ dateEnd, asn, location, continent, dimension, orderBy }) => { + if (orderBy && dimension === 'summary') { + throw new Error('Order by is only allowed for top locations and ASes') + } + + try { + const client = getCloudflareClient(agent.props.accessToken) + const r = await resolveAndInvoke(client.radar.quality.speed, dimension, { + asn, + continent, + location, + dateEnd, + }) + + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + result: r, + }), + }, + ], + } + } catch (error) { + return { + content: [ + { + type: 'text', + text: `Error getting Internet speed data: ${error instanceof Error && error.message}`, }, ], } diff --git a/apps/radar/src/types/radar.ts b/apps/radar/src/types/radar.ts index 39a1eac7..102f8d45 100644 --- a/apps/radar/src/types/radar.ts +++ b/apps/radar/src/types/radar.ts @@ -26,6 +26,26 @@ export const DomainRankingTypeParam: z.ZodType .enum(['POPULAR', 'TRENDING_RISE', 'TRENDING_STEADY']) .describe('The ranking type.') +export const InternetServicesCategoryParam = z + .array( + z.enum([ + 'Generative AI', + 'E-commerce', + 'Cryptocurrency Services', + 'Email', + 'Fast Fashion', + 'Financial Services', + 'News', + 'Social Media', + 'Weather', + 'Jobs', + 'Low cost E-commerce', + 'Messaging', + 'Metaverse & Gaming', + ]) + ) + .describe('Filters results by Internet service category.') + export const DateParam = z.string().date().describe('Filters results by date.') export const DateListParam = z.array(DateParam).describe('Filters results by date.') @@ -132,38 +152,158 @@ export const AsOrderByParam: z.ZodType = z .optional() .describe('Optional order by parameter: "ASN" or "POPULATION".') -export const DataFormatParam = z - .enum(['timeseries', 'summary', 'timeseriesGroups']) - .describe( - "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." - ) - export const HttpDimensionParam = z .enum([ - 'deviceType', - 'httpProtocol', - 'httpVersion', - 'botClass', - 'ipVersion', - 'tlsVersion', - 'os', - 'postQuantum', + 'timeseries', + 'summary/deviceType', + 'summary/httpProtocol', + 'summary/httpVersion', + 'summary/botClass', + 'summary/ipVersion', + 'summary/tlsVersion', + 'summary/os', + 'summary/postQuantum', + 'top/browser', // TODO replace with "summary/browser" and "summary/browserFamily" once available on the lib + 'top/browserFamily', + 'timeseriesGroups/deviceType', + 'timeseriesGroups/httpProtocol', + 'timeseriesGroups/httpVersion', + 'timeseriesGroups/botClass', + 'timeseriesGroups/ipVersion', + 'timeseriesGroups/tlsVersion', + 'timeseriesGroups/os', + 'timeseriesGroups/postQuantum', + 'timeseriesGroups/browser', + 'timeseriesGroups/browserFamily', + 'top/locations', + 'top/ases', ]) - .optional() - .describe( - "Dimension used to group HTTP data. Allowed only when the format is 'summary' or 'timeseriesGroups'." - ) + .describe('Dimension indicating the type and format of HTTP data to retrieve.') + +export const DnsDimensionParam = z + .enum([ + 'timeseries', + 'summary/ipVersion', + 'summary/cacheHit', + 'summary/dnssec', + 'summary/dnssecAware', + 'summary/matchingAnswer', + 'summary/protocol', + 'summary/queryType', + 'summary/responseCode', + 'summary/responseTTL', + 'timeseriesGroups/ipVersion', + 'timeseriesGroups/cacheHit', + 'timeseriesGroups/dnssecAware', + 'timeseriesGroups/matchingAnswer', + 'timeseriesGroups/protocol', + 'timeseriesGroups/queryType', + 'timeseriesGroups/responseCode', + 'timeseriesGroups/responseTTL', + 'top/locations', + 'top/ases', + ]) + .describe('Dimension indicating the type and format of DNS data to retrieve.') export const L7AttackDimensionParam = z .enum([ - 'httpMethod', - 'httpVersion', - 'ipVersion', - 'mitigationProduct', - 'managedRules', - // TODO: add 'vertical' and 'industry' once they are in the cloudflare API lib + 'timeseries', + 'summary/httpMethod', + 'summary/httpVersion', + 'summary/ipVersion', + 'summary/managedRules', + 'summary/mitigationProduct', + 'top/vertical', // TODO replace with "summary/vertical" and "summary/industry" once available on the lib + 'top/industry', + 'timeseriesGroups/httpMethod', + 'timeseriesGroups/httpVersion', + 'timeseriesGroups/ipVersion', + 'timeseriesGroups/managedRules', + 'timeseriesGroups/mitigationProduct', + 'timeseriesGroups/vertical', + 'timeseriesGroups/industry', + 'top/locations/origin', + 'top/locations/target', + 'top/ases/origin', + 'top/attacks', ]) - .optional() - .describe( - "Dimension used to group L7 attack data. Allowed only when the format is 'summary' or 'timeseriesGroups'." - ) + .describe('Dimension indicating the type and format of L7 attack data to retrieve.') + +export const L3AttackDimensionParam = z + .enum([ + 'timeseries', + 'summary/protocol', + 'summary/ipVersion', + 'summary/vector', + 'summary/bitrate', + 'summary/duration', + 'top/vertical', // TODO replace with "summary/vertical" and "summary/industry" once available on the lib + 'top/industry', + 'timeseriesGroups/protocol', + 'timeseriesGroups/ipVersion', + 'timeseriesGroups/vector', + 'timeseriesGroups/bitrate', + 'timeseriesGroups/duration', + 'timeseriesGroups/vertical', + 'timeseriesGroups/industry', + 'top/locations/origin', + 'top/locations/target', + 'top/attacks', + ]) + .describe('Dimension indicating the type and format of L3 attack data to retrieve.') + +export const EmailRoutingDimensionParam = z + .enum([ + 'summary/ipVersion', + 'summary/encrypted', + 'summary/arc', + 'summary/dkim', + 'summary/dmarc', + 'summary/spf', + 'timeseriesGroups/ipVersion', + 'timeseriesGroups/encrypted', + 'timeseriesGroups/arc', + 'timeseriesGroups/dkim', + 'timeseriesGroups/dmarc', + 'timeseriesGroups/spf', + ]) + .describe('Dimension indicating the type and format of Email Routing data to retrieve.') + +export const EmailSecurityDimensionParam = z + .enum([ + 'summary/spam', + 'summary/malicious', + 'summary/spoof', + 'summary/threatCategory', + 'summary/arc', + 'summary/dkim', + 'summary/dmarc', + 'summary/spf', + 'summary/tlsVersion', + 'timeseriesGroups/spam', + 'timeseriesGroups/malicious', + 'timeseriesGroups/spoof', + 'timeseriesGroups/threatCategory', + 'timeseriesGroups/arc', + 'timeseriesGroups/dkim', + 'timeseriesGroups/dmarc', + 'timeseriesGroups/spf', + 'timeseriesGroups/tlsVersion', + 'top/tlds', + ]) + .describe('Dimension indicating the type and format of Email Security data to retrieve.') + +export const InternetSpeedDimensionParam = z + .enum(['summary', 'top/locations', 'top/ases']) + .describe('Dimension indicating the type and format of Internet speed data to retrieve.') + +export const InternetSpeedOrderByParam = z + .enum([ + 'BANDWIDTH_DOWNLOAD', + 'BANDWIDTH_UPLOAD', + 'LATENCY_IDLE', + 'LATENCY_LOADED', + 'JITTER_IDLE', + 'JITTER_LOADED', + ]) + .describe('Specifies the metric to order the results by. Only allowed for top locations and ASes') diff --git a/apps/radar/src/utils.ts b/apps/radar/src/utils.ts new file mode 100644 index 00000000..d83f6ebc --- /dev/null +++ b/apps/radar/src/utils.ts @@ -0,0 +1,17 @@ +/** + * Resolves and invokes a method dynamically based on the provided slugs. + * + * This function traverses the object based on the `slugs` array, binds the method + * to its correct context, and invokes it with the provided parameters. + * + * @param {Object} client - The root object (e.g., `client.radar.http`) to resolve methods from. + * @param {string[]} path - The path to the desired method. + * @param {Object} params - The parameters to pass when invoking the resolved method. + * @returns {Promise} The result of the method invocation. + */ +export async function resolveAndInvoke(client: any, path: string, params: any): Promise { + const slugs = path.split('/') + const method = slugs.reduce((acc, key) => acc?.[key], client) + const parentContext = slugs.slice(0, -1).reduce((acc, key) => acc?.[key], client) + return await method.bind(parentContext)(params) +}