diff --git a/src/services/cyberfraudService.ts b/src/services/cyberfraudService.ts index 28104d1..3a9bc3c 100644 --- a/src/services/cyberfraudService.ts +++ b/src/services/cyberfraudService.ts @@ -9,8 +9,12 @@ import type { CyberfraudOverviewResponse, CyberfraudAccountInfoResponse, CyberfraudCustomRulesResponse, - TrafficDataInput, - TrafficDataResponse, + TrafficOvertimeInput, + TrafficOvertimeResponse, + TrafficMetricsInput, + TrafficMetricsResponse, + TrafficTopsInput, + TrafficTopsResponse, } from '../types/cyberfraud'; const API_BASE = `${HUMAN_API_BASE}/cyberfraud`; @@ -27,43 +31,40 @@ function buildAttackReportingUrl(endpoint: string, params: Record) return `${API_BASE}/attack-reporting${endpoint}?${queryParams.toString()}`; } -function buildTrafficDataQuery(params: { - from: number; - to: number; - appId?: string[]; - source?: string[]; - overtime?: string[]; - tops?: string[]; - traffic?: string[]; - pageType?: string[]; - count?: string[]; - withoutTotals?: boolean; - metricsEnrichment?: any; +function buildTrafficDashboardUrl(endpoint: string, from: number, to: number) { + const queryParams = new URLSearchParams(); + queryParams.append('from', from.toString()); + queryParams.append('to', to.toString()); + return `${API_BASE}/traffic${endpoint}?${queryParams.toString()}`; +} + +function buildTrafficTimeRange(startTime: string, endTime: string) { + const clamped = clampAttackReportingTimes(startTime, endTime); + return { + from: Math.floor(new Date(clamped.startTime).getTime() / 1000), + to: Math.floor(new Date(clamped.endTime).getTime() / 1000), + }; +} + +function buildTrafficDashboardBody(params: { + trafficSource: string[]; + filters?: Record; + searchQuery?: Record[]; + seriesFields?: string[]; + limit?: number; + includeNulls?: boolean; }) { - const query = new URLSearchParams(); - query.append('from', params.from.toString()); - query.append('to', params.to.toString()); - const sources = params.source && params.source.length > 0 ? params.source : ['web', 'mobile']; - sources.forEach((s) => query.append('source[]', s)); - if (params.appId) params.appId.forEach((id) => query.append('appId[]', id)); - const allOvertime = [ - 'legitimate', - 'blocked', - 'potentialBlock', - 'whitelist', - 'blacklist', - 'goodKnownBots', - 'captchaSolved', - ]; - const overtime = params.overtime && params.overtime.length > 0 ? params.overtime : allOvertime; - overtime.forEach((o) => query.append('overtime[]', o)); - if (params.tops) params.tops.forEach((t) => query.append('tops[]', t)); - if (params.traffic) params.traffic.forEach((t) => query.append('traffic[]', t)); - if (params.pageType) params.pageType.forEach((p) => query.append('pageType[]', p)); - if (params.count) params.count.forEach((c) => query.append('count[]', c)); - if (params.withoutTotals) query.append('withoutTotals', 'true'); - if (params.metricsEnrichment) query.append('metricsEnrichment', JSON.stringify(params.metricsEnrichment)); - return `${API_BASE}/traffic-data?${query.toString()}`; + const body: Record = { + trafficSource: params.trafficSource, + }; + + if (params.filters) body.filters = params.filters; + if (params.searchQuery) body.searchQuery = params.searchQuery; + if (params.seriesFields) body.seriesFields = params.seriesFields; + if (params.limit !== undefined) body.limit = params.limit; + if (params.includeNulls !== undefined) body.includeNulls = params.includeNulls; + + return body; } export class CyberfraudService { @@ -101,13 +102,36 @@ export class CyberfraudService { return (await res.json()) as CyberfraudCustomRulesResponse; } - async getTrafficData(params: TrafficDataInput): Promise { - const { startTime, endTime, ...rest } = params; - const clamped = clampAttackReportingTimes(startTime, endTime); - const from = Math.floor(new Date(clamped.startTime).getTime() / 1000); - const to = Math.floor(new Date(clamped.endTime).getTime() / 1000); - const url = buildTrafficDataQuery({ from, to, ...rest }); - const res = await this.http.request(url); - return (await res.json()) as TrafficDataResponse; + async getTrafficOvertime(params: TrafficOvertimeInput): Promise { + const { startTime, endTime, trafficSource, filters, searchQuery, seriesFields } = params; + const { from, to } = buildTrafficTimeRange(startTime, endTime); + const url = buildTrafficDashboardUrl('/overtime', from, to); + const body = buildTrafficDashboardBody({ trafficSource, filters, searchQuery, seriesFields }); + const res = await this.http.request(url, { method: 'POST', body }); + return (await res.json()) as TrafficOvertimeResponse; + } + + async getTrafficMetrics(params: TrafficMetricsInput): Promise { + const { startTime, endTime, trafficSource, filters, searchQuery } = params; + const { from, to } = buildTrafficTimeRange(startTime, endTime); + const url = buildTrafficDashboardUrl('/metrics', from, to); + const body = buildTrafficDashboardBody({ trafficSource, filters, searchQuery }); + const res = await this.http.request(url, { method: 'POST', body }); + return (await res.json()) as TrafficMetricsResponse; + } + + async getTrafficTops(params: TrafficTopsInput): Promise { + const { startTime, endTime, field, trafficSource, filters, searchQuery, limit, includeNulls } = params; + const { from, to } = buildTrafficTimeRange(startTime, endTime); + const url = buildTrafficDashboardUrl(`/tops/${encodeURIComponent(field)}`, from, to); + const body = buildTrafficDashboardBody({ + trafficSource, + filters, + searchQuery, + limit, + includeNulls, + }); + const res = await this.http.request(url, { method: 'POST', body }); + return (await res.json()) as TrafficTopsResponse; } } diff --git a/src/tools/getTrafficData.ts b/src/tools/getTrafficData.ts deleted file mode 100644 index 7f9c18a..0000000 --- a/src/tools/getTrafficData.ts +++ /dev/null @@ -1,88 +0,0 @@ -import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp'; -import { TrafficDataInputSchema, TrafficDataInput, TrafficDataOutputSchema } from '../types/cyberfraud'; -import { mcpToolHandler } from '../utils/mcpToolHandler'; -import { makeStructuredResponseSchema } from '../utils/makeStructuredResponseSchema'; -import type { CyberfraudService } from '../services/cyberfraudService'; -import { DATE_FORMAT_EXAMPLE_END, DATE_FORMAT_EXAMPLE_START } from '../utils/constants'; - -export function registerCyberfraudGetTrafficData(server: McpServer, cyberfraudService: CyberfraudService) { - server.registerTool( - 'human_get_traffic_data', - { - description: `Fetches comprehensive traffic analytics from HUMAN Security's Cyberfraud API. This powerful tool provides detailed insights into web and mobile traffic patterns, security metrics, and user behavior across your applications. - -🎯 QUICK DECISION GUIDE: -├── Need high-level totals without granular breakdowns? → Use "count" -├── Need trends/charts? → Use "overtime" -├── Need attack types? → Add "tops": ["incidents"] -├── Need URL path information? → Add "tops": ["path"] -├── Security focus only? → Add "traffic": ["blocked"] -└── Multi-platform analysis? → Include "source": ["web", "mobile"] - -❌ CRITICAL RULES: -• NEVER combine "count" + "overtime" (mutually exclusive) -• NEVER combine "overtime" + "tops" (creates misleading aggregation, see AGGREGATION WARNING below) -• "tops" completely transforms response structure -• "count" returns only high-level totals, no granularity -• Filters stack multiplicatively (can reduce data 87%+) - -🚨 AGGREGATION WARNING - IMPORTANT API BEHAVIOR: -When combining "overtime" + "tops" parameters, the API exhibits unexpected aggregation: -• First interval: Contains ALL historical data aggregated (total counts) -• Subsequent intervals: All show 0 counts (creating false impression of no recent activity) -• Result: Appears as time-series but is actually front-loaded aggregate data - -CORRECT USAGE: -• "startTime" and "endTime" should be in ISO 8601 format, e.g. "${DATE_FORMAT_EXAMPLE_START}". -• Path analysis: Use "tops": ["path"] for path breakdowns (no "count") -• Time-series: Use "overtime" without "tops" -• Attack types: Use "count" + "tops": ["incidents"] for totals - -✅ HIGH-VALUE PATTERNS: - -1. EXECUTIVE DASHBOARD: - {"count": ["legitimate", "blocked"]} - → Simple traffic health metrics without breakdowns - -2. PATH ANALYSIS: - {"tops": ["path"]} - → Most trafficked endpoints with accurate totals - -3. ATTACK TYPE ANALYSIS: - {"tops": ["incidents"]} - → Attack type breakdown with accurate totals - -4. SECURITY TIMELINE: - {"overtime": ["blocked"]} - → Pure attack volume trends over time - -5. FOCUSED ANALYSIS: - {"count": ["blocked"], "pageType": ["login"], "source": ["web"]} - → Login-specific web security - -⚠️ ENVIRONMENT NOTES: -• Mobile traffic may be minimal/absent in some environments -• Page type filters dramatically reduce scope -• Incident classification reveals valuable attack taxonomy -• Time-series uses ~20-minute intervals - -🔧 PARAMETER BEHAVIOR: -• "count": Aggregate totals across time range, should be used only when only total numbers are needed as the response will not include more granular breakdowns -• "overtime": Time-series with intervals (do NOT combine with "tops") -• "tops": Transforms aggregates to breakdowns (use with "count" only) -• "traffic": Security-only filter (excludes legitimate) -• "pageType": Journey-specific filter (very restrictive) -• "source": Platform filter (web/mobile) - -Response provides structured data optimized for security dashboards, threat analysis, and executive reporting with quantifiable metrics and actionable intelligence.`, - inputSchema: TrafficDataInputSchema.shape, - outputSchema: makeStructuredResponseSchema(TrafficDataOutputSchema).shape, - annotations: { - title: 'HUMAN Get Traffic Data', - readOnlyHint: true, - openWorldHint: true, - }, - }, - async (params: TrafficDataInput) => mcpToolHandler(async () => cyberfraudService.getTrafficData(params)), - ); -} diff --git a/src/tools/getTrafficMetrics.ts b/src/tools/getTrafficMetrics.ts new file mode 100644 index 0000000..e72dff5 --- /dev/null +++ b/src/tools/getTrafficMetrics.ts @@ -0,0 +1,72 @@ +import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp'; +import { TrafficMetricsInputSchema, TrafficMetricsInput, TrafficMetricsOutputSchema } from '../types/cyberfraud'; +import { mcpToolHandler } from '../utils/mcpToolHandler'; +import { makeStructuredResponseSchema } from '../utils/makeStructuredResponseSchema'; +import type { CyberfraudService } from '../services/cyberfraudService'; +import { DATE_FORMAT_EXAMPLE_END, DATE_FORMAT_EXAMPLE_START } from '../utils/constants'; + +export function registerCyberfraudGetTrafficMetrics(server: McpServer, cyberfraudService: CyberfraudService) { + server.registerTool( + 'human_get_traffic_metrics', + { + description: `Fetches aggregated traffic totals from HUMAN Security's Cyberfraud Traffic Dashboard API. Use this tool for KPIs, executive summaries, and high-level traffic health checks. + +🎯 QUICK DECISION GUIDE: +├── Need totals and KPIs? → Use this tool +├── Need trends over time? → Use human_get_traffic_overtime +├── Need top values for a dimension? → Use human_get_traffic_tops +├── Need security-only totals? → Add filters.trafficTags: ["blocked", "potentialBlock"] +├── Need login-specific totals? → Add filters.pageType: ["login_attempt"] +└── Need multi-platform totals? → Set trafficSource: ["web", "mobile"] + +❌ CRITICAL RULES: +• trafficSource is required (["web"], ["mobile"], or both) +• startTime/endTime must be valid ISO 8601 strings +• startTime cannot be after endTime or in the future +• Filters stack multiplicatively and can significantly reduce result scope +• app_id is derived from the API token — do not pass appIds + +✅ HIGH-VALUE PATTERNS: + +1. EXECUTIVE DASHBOARD: + {"startTime": "${DATE_FORMAT_EXAMPLE_START}", "endTime": "${DATE_FORMAT_EXAMPLE_END}", "trafficSource": ["web", "mobile"]} + → Total, blocked, legitimate, and known bot counts + +2. SECURITY KPIs: + {"startTime": "${DATE_FORMAT_EXAMPLE_START}", "endTime": "${DATE_FORMAT_EXAMPLE_END}", "trafficSource": ["web"], "filters": {"trafficTags": ["blocked", "blacklist", "potentialBlock"]}} + → Threat-focused totals for the period + +3. LOGIN SECURITY SUMMARY: + {"startTime": "${DATE_FORMAT_EXAMPLE_START}", "endTime": "${DATE_FORMAT_EXAMPLE_END}", "trafficSource": ["web"], "filters": {"pageType": ["login_attempt"]}} + → Login journey traffic totals + +⚠️ TECHNICAL INSIGHTS: +• Returns aggregated totals for the full requested time range +• content.results includes total, totalBlocked, legitimate, blocked, goodKnownBots, web, mobile, and more +• content.labels provides human-readable labels for metrics +• Best for single-number KPI reporting rather than charts + +🚀 OPTIMAL WORKFLOWS: + +1. EXECUTIVE REPORTING: + - Query broad trafficSource coverage + - Compare total vs totalBlocked vs legitimate + - Use labels for presentation-ready output + +2. SECURITY HEALTH CHECK: + - Filter to blocked/potentialBlock/blacklist tags + - Compare against unfiltered totals from a separate call + - Focus on login or checkout pageType when relevant + +Response provides aggregated traffic intelligence optimized for KPI dashboards, executive summaries, and quick health assessments.`, + inputSchema: TrafficMetricsInputSchema.shape, + outputSchema: makeStructuredResponseSchema(TrafficMetricsOutputSchema).shape, + annotations: { + title: 'HUMAN Get Traffic Metrics', + readOnlyHint: true, + openWorldHint: true, + }, + }, + async (params: TrafficMetricsInput) => mcpToolHandler(async () => cyberfraudService.getTrafficMetrics(params)), + ); +} diff --git a/src/tools/getTrafficOvertime.ts b/src/tools/getTrafficOvertime.ts new file mode 100644 index 0000000..236806c --- /dev/null +++ b/src/tools/getTrafficOvertime.ts @@ -0,0 +1,73 @@ +import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp'; +import { TrafficOvertimeInputSchema, TrafficOvertimeInput, TrafficOvertimeOutputSchema } from '../types/cyberfraud'; +import { mcpToolHandler } from '../utils/mcpToolHandler'; +import { makeStructuredResponseSchema } from '../utils/makeStructuredResponseSchema'; +import type { CyberfraudService } from '../services/cyberfraudService'; +import { DATE_FORMAT_EXAMPLE_END, DATE_FORMAT_EXAMPLE_START } from '../utils/constants'; + +export function registerCyberfraudGetTrafficOvertime(server: McpServer, cyberfraudService: CyberfraudService) { + server.registerTool( + 'human_get_traffic_overtime', + { + description: `Fetches minute-bucketed traffic overtime data from HUMAN Security's Cyberfraud Traffic Dashboard API. Use this tool for trend charts, attack timelines, and time-series security analysis. + +🎯 QUICK DECISION GUIDE: +├── Need trend charts over time? → Use this tool +├── Need high-level totals only? → Use human_get_traffic_metrics +├── Need top values for a dimension? → Use human_get_traffic_tops +├── Need attack type breakdown over time? → Add seriesFields: ["knownBot"] or filters.trafficTags +├── Need login-focused timeline? → Add filters.pageType: ["login_attempt"] +└── Need multi-platform view? → Set trafficSource: ["web", "mobile"] + +❌ CRITICAL RULES: +• trafficSource is required (["web"], ["mobile"], or both) +• startTime/endTime must be valid ISO 8601 strings +• startTime cannot be after endTime or in the future +• Filters stack multiplicatively and can significantly reduce result scope +• app_id is derived from the API token — do not pass appIds + +✅ HIGH-VALUE PATTERNS: + +1. SECURITY TIMELINE: + {"startTime": "${DATE_FORMAT_EXAMPLE_START}", "endTime": "${DATE_FORMAT_EXAMPLE_END}", "trafficSource": ["web", "mobile"], "filters": {"trafficTags": ["blocked", "potentialBlock"]}} + → Blocked traffic volume over time + +2. KNOWN BOT BREAKDOWN: + {"startTime": "${DATE_FORMAT_EXAMPLE_START}", "endTime": "${DATE_FORMAT_EXAMPLE_END}", "trafficSource": ["web"], "filters": {"trafficTags": ["goodKnownBots"]}, "seriesFields": ["knownBot"]} + → Known bot traffic split by bot name over time + +3. LOGIN ATTEMPT MONITORING: + {"startTime": "${DATE_FORMAT_EXAMPLE_START}", "endTime": "${DATE_FORMAT_EXAMPLE_END}", "trafficSource": ["web"], "filters": {"pageType": ["login_attempt"], "trafficTags": ["blocked"]}} + → Login attack timeline + +⚠️ TECHNICAL INSIGHTS: +• Returns one row per minute with traffic tag counts +• seriesFields adds extra per-value columns in each minute bucket +• Response uses { result, message, content.results[] } envelope +• Longer time ranges produce more minute buckets + +🚀 OPTIMAL WORKFLOWS: + +1. INCIDENT TIMELINE: + - Start with a focused 6-24 hour window + - Filter to blocked/potentialBlock trafficTags + - Expand window if more context is needed + +2. BOT TRAFFIC ANALYSIS: + - Filter to goodKnownBots + - Add seriesFields: ["knownBot"] + - Compare bot families over time + +Response provides minute-level traffic intelligence optimized for dashboards, SOC monitoring, and executive trend reporting.`, + inputSchema: TrafficOvertimeInputSchema.shape, + outputSchema: makeStructuredResponseSchema(TrafficOvertimeOutputSchema).shape, + annotations: { + title: 'HUMAN Get Traffic Overtime', + readOnlyHint: true, + openWorldHint: true, + }, + }, + async (params: TrafficOvertimeInput) => + mcpToolHandler(async () => cyberfraudService.getTrafficOvertime(params)), + ); +} diff --git a/src/tools/getTrafficTops.ts b/src/tools/getTrafficTops.ts new file mode 100644 index 0000000..26953a4 --- /dev/null +++ b/src/tools/getTrafficTops.ts @@ -0,0 +1,70 @@ +import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp'; +import { TrafficTopsInputSchema, TrafficTopsInput, TrafficTopsOutputSchema } from '../types/cyberfraud'; +import { mcpToolHandler } from '../utils/mcpToolHandler'; +import { makeStructuredResponseSchema } from '../utils/makeStructuredResponseSchema'; +import type { CyberfraudService } from '../services/cyberfraudService'; +import { DATE_FORMAT_EXAMPLE_END, DATE_FORMAT_EXAMPLE_START } from '../utils/constants'; + +export function registerCyberfraudGetTrafficTops(server: McpServer, cyberfraudService: CyberfraudService) { + server.registerTool( + 'human_get_traffic_tops', + { + description: `Fetches ranked top values for a selected traffic dimension from HUMAN Security's Cyberfraud Traffic Dashboard API. Use this tool for breakdowns by path, country, incident type, IP org, and more. + +🎯 QUICK DECISION GUIDE: +├── Need attack type breakdown? → field: "incidentTypes" +├── Need most targeted paths? → field: "path" +├── Need geo distribution? → field: "country" +├── Need top blocked IP orgs? → field: "socketIpOrgName" with blocked filters +├── Need custom rule usage? → field: "customRule" +└── Need high-level totals only? → Use human_get_traffic_metrics instead + +❌ CRITICAL RULES: +• field is required — choose one supported dimension +• trafficSource is required (["web"], ["mobile"], or both) +• startTime/endTime must be valid ISO 8601 strings +• limit defaults to 10 and must be between 1 and 100 +• app_id is derived from the API token — do not pass appIds + +✅ HIGH-VALUE PATTERNS: + +1. ATTACK TYPE BREAKDOWN: + {"startTime": "${DATE_FORMAT_EXAMPLE_START}", "endTime": "${DATE_FORMAT_EXAMPLE_END}", "trafficSource": ["web", "mobile"], "field": "incidentTypes", "filters": {"trafficTags": ["blocked"]}} + → Top incident types for blocked traffic + +2. PATH ANALYSIS: + {"startTime": "${DATE_FORMAT_EXAMPLE_START}", "endTime": "${DATE_FORMAT_EXAMPLE_END}", "trafficSource": ["web"], "field": "path", "limit": 20} + → Most trafficked endpoints + +3. TOP BLOCKED IP ORGS: + {"startTime": "${DATE_FORMAT_EXAMPLE_START}", "endTime": "${DATE_FORMAT_EXAMPLE_END}", "trafficSource": ["web", "mobile"], "field": "socketIpOrgName", "filters": {"trafficTags": ["blocked", "blacklist", "potentialBlock"]}, "limit": 10} + → Top organizations behind blocked traffic + +⚠️ TECHNICAL INSIGHTS: +• Returns content.results as [{ value, count }, ...] sorted by count descending +• includeNulls controls whether null field values appear in results +• Supported fields include socketIp, domain, path, country, customRule, knownBot, utmSource, and more +• Combine filters to focus on security-relevant subsets + +🚀 OPTIMAL WORKFLOWS: + +1. THREAT HUNTING: + - Start with field: "incidentTypes" on blocked traffic + - Follow up with field: "path" or "socketIpOrgName" for deeper context + +2. TARGET ANALYSIS: + - Use field: "path" for endpoint targeting + - Add pageType filters for journey-specific investigations + +Response provides ranked breakdown intelligence optimized for threat analysis, endpoint targeting review, and investigative reporting.`, + inputSchema: TrafficTopsInputSchema.shape, + outputSchema: makeStructuredResponseSchema(TrafficTopsOutputSchema).shape, + annotations: { + title: 'HUMAN Get Traffic Tops', + readOnlyHint: true, + openWorldHint: true, + }, + }, + async (params: TrafficTopsInput) => mcpToolHandler(async () => cyberfraudService.getTrafficTops(params)), + ); +} diff --git a/src/tools/index.ts b/src/tools/index.ts index 86b5013..a7c90b5 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -8,7 +8,9 @@ import { registerCodeDefenderGetScriptInventory } from './codeDefenderGetScriptI import { registerCodeDefenderGetHeaderInventory } from './codeDefenderGetHeaderInventory'; import type { CyberfraudService } from '../services/cyberfraudService'; import type { CodeDefenderService } from '../services/codeDefenderService'; -import { registerCyberfraudGetTrafficData } from './getTrafficData'; +import { registerCyberfraudGetTrafficOvertime } from './getTrafficOvertime'; +import { registerCyberfraudGetTrafficMetrics } from './getTrafficMetrics'; +import { registerCyberfraudGetTrafficTops } from './getTrafficTops'; export function registerTools( server: McpServer, @@ -23,7 +25,9 @@ export function registerTools( registerCyberfraudGetAttackReportingOverview(server, services.cyberfraudService); registerCyberfraudGetCustomRules(server, services.cyberfraudService); registerCyberfraudGetAccountInfo(server, services.cyberfraudService); - registerCyberfraudGetTrafficData(server, services.cyberfraudService); + registerCyberfraudGetTrafficOvertime(server, services.cyberfraudService); + registerCyberfraudGetTrafficMetrics(server, services.cyberfraudService); + registerCyberfraudGetTrafficTops(server, services.cyberfraudService); } // Register Code Defender tools if service is available diff --git a/src/types/cyberfraud/index.ts b/src/types/cyberfraud/index.ts index 0ec4528..37ea38f 100644 --- a/src/types/cyberfraud/index.ts +++ b/src/types/cyberfraud/index.ts @@ -1,4 +1,4 @@ export * from './attackReporting'; export * from './accountInfo'; export * from './customRules'; -export * from './trafficData'; +export * from './trafficDashboard'; diff --git a/src/types/cyberfraud/trafficDashboard.ts b/src/types/cyberfraud/trafficDashboard.ts new file mode 100644 index 0000000..1431d43 --- /dev/null +++ b/src/types/cyberfraud/trafficDashboard.ts @@ -0,0 +1,201 @@ +import { z } from 'zod'; +import { DATE_FORMAT_EXAMPLE_END, DATE_FORMAT_EXAMPLE_START } from '../../utils/constants'; + +export const TrafficSourceEnum = z.enum(['web', 'mobile'], { + description: + '🌐 PLATFORM FILTER: ["web"], ["mobile"], or both. NOTE: Mobile traffic may be minimal in some environments. Use both for complete coverage, individual for platform-specific analysis.', +}); + +export const TrafficTagEnum = z.enum( + [ + 'legitimate', + 'blocked', + 'potentialBlock', + 'goodKnownBots', + 'whitelist', + 'blacklist', + 'compromisedLogin', + 'failedLogin', + 'successfulLogin', + 'successfulCompromised', + 'legitimateCompromised', + 'blockedCompromised', + 'abusable_rules', + 'precheckPresented', + 'captchaSolved', + 'redirected', + ], + { + description: + '🎯 TRAFFIC TAG FILTER: Security classification filter applied inside filters.trafficTags. COMBINE WITH: pageType and other filters for focused analysis.', + }, +); + +export const PageTypeEnum = z.enum( + [ + 'login', + 'login_attempt', + 'checkout', + 'carding_attempt', + 'purchase', + 'purchase_request', + 'productsAndSearch', + 'research', + 'resource', + 'apiCall', + 'mobileUserAgents', + ], + { + description: + '🎯 PAGE JOURNEY FILTER: Focuses analysis on specific user journeys. ⚠️ SCOPE WARNING: Can significantly reduce result scope. USE SPARINGLY for targeted analysis.', + }, +); + +export const TrafficDashboardTopFieldEnum = z.enum( + [ + 'socketIp', + 'domain', + 'headerReferer', + 'incidentTypes', + 'socketIpOrgName', + 'path', + 'knownBot', + 'uaServer', + 'providerCloud', + 'providerProxy', + 'providerVpn', + 'providerClassification', + 'country', + 'customRule', + 'accessTokenName', + 'customParam1', + 'customParam2', + 'customParam3', + 'customParam4', + 'customParam5', + 'customParam6', + 'customParam7', + 'customParam8', + 'customParam9', + 'utmSource', + 'utmMedium', + 'utmCampaign', + 'utmTerm', + 'graphqlOperationName', + ], + { + description: + '🔍 TOP FIELD: Dimension to rank by request count. Use "incidentTypes" for attack taxonomy, "path" for endpoint targeting, "country" for geo analysis.', + }, +); + +export const TrafficDashboardSeriesFieldEnum = z.enum(['accessTokenName', 'customRule', 'knownBot'], { + description: + '📈 SERIES BREAKDOWN: Optional overtime series dimension. Adds per-value counts alongside core traffic tags in each minute bucket.', +}); + +export const TrafficDashboardFiltersSchema = z + .object({ + trafficTags: z.array(TrafficTagEnum).optional(), + pageType: z.array(PageTypeEnum).optional(), + browserFamily: z.array(z.string()).optional(), + osFamily: z.array(z.string()).optional(), + country: z.array(z.string()).optional(), + }) + .passthrough() + .optional() + .describe( + '🎯 FILTERS: Optional multi-dimensional filters. All filter fields stack multiplicatively and can reduce result scope significantly.', + ); + +export const TrafficDashboardSearchQuerySchema = z + .array(z.record(z.any())) + .optional() + .describe( + '🔍 ADVANCED SEARCH: Optional field/operator expression array for advanced filtering. Use when standard filters are insufficient.', + ); + +const trafficTimeRangeSchema = { + startTime: z + .string() + .describe( + `⏰ TIME RANGE START: ISO 8601 datetime string defining analysis period beginning. 🎯 FORMAT: "${DATE_FORMAT_EXAMPLE_START}". 💡 STRATEGY: Use shorter windows for granular timelines, longer periods for trend analysis.`, + ), + endTime: z + .string() + .describe( + `🏁 TIME RANGE END: ISO 8601 datetime string defining analysis period conclusion. 🎯 FORMAT: "${DATE_FORMAT_EXAMPLE_END}". ⚠️ CONSTRAINT: Must be after startTime and cannot be in the future.`, + ), +}; + +const trafficCommonBodySchema = { + trafficSource: z + .array(TrafficSourceEnum) + .min(1) + .describe( + '🌐 TRAFFIC SOURCE: Required platform scope. Use ["web"], ["mobile"], or ["web","mobile"] for complete coverage.', + ), + filters: TrafficDashboardFiltersSchema, + searchQuery: TrafficDashboardSearchQuerySchema, +}; + +export const TrafficOvertimeInputSchema = z.object({ + ...trafficTimeRangeSchema, + ...trafficCommonBodySchema, + seriesFields: z + .array(TrafficDashboardSeriesFieldEnum) + .optional() + .describe( + '📈 SERIES FIELDS: Optional breakdown dimensions for overtime charts. Options: accessTokenName, customRule, knownBot.', + ), +}); +export type TrafficOvertimeInput = z.infer; + +export const TrafficMetricsInputSchema = z.object({ + ...trafficTimeRangeSchema, + ...trafficCommonBodySchema, +}); +export type TrafficMetricsInput = z.infer; + +export const TrafficTopsInputSchema = z.object({ + ...trafficTimeRangeSchema, + ...trafficCommonBodySchema, + field: TrafficDashboardTopFieldEnum.describe( + '🔍 TOP FIELD: Required dimension to rank. Examples: "incidentTypes", "path", "country", "customRule".', + ), + limit: z + .number() + .int() + .min(1) + .max(100) + .optional() + .describe('📊 RESULT SIZE CONTROL: Maximum top values to return. Default: 10. Maximum: 100.'), + includeNulls: z + .boolean() + .optional() + .describe('🔧 NULL HANDLING: Include rows where the selected field is null. Default: false.'), +}); +export type TrafficTopsInput = z.infer; + +const TrafficDashboardResponseWrapperSchema = z + .object({ + result: z.boolean().optional().describe('✅ API SUCCESS: Indicates successful operation completion.'), + message: z.string().optional().describe('💬 STATUS MESSAGE: Human-readable response status.'), + content: z.record(z.any()).optional().describe('📦 MAIN PAYLOAD: Primary data when result=true.'), + }) + .passthrough(); + +export const TrafficOvertimeOutputSchema = TrafficDashboardResponseWrapperSchema.describe( + '📈 OVERTIME RESPONSE: Minute-bucketed traffic counts over the requested time range, optionally broken out by seriesFields.', +); +export type TrafficOvertimeResponse = z.infer; + +export const TrafficMetricsOutputSchema = TrafficDashboardResponseWrapperSchema.describe( + '📊 METRICS RESPONSE: Aggregated traffic totals and labels for the requested time range.', +); +export type TrafficMetricsResponse = z.infer; + +export const TrafficTopsOutputSchema = TrafficDashboardResponseWrapperSchema.describe( + '📋 TOPS RESPONSE: Ranked top values for the selected field, sorted by request count.', +); +export type TrafficTopsResponse = z.infer; diff --git a/src/types/cyberfraud/trafficData.ts b/src/types/cyberfraud/trafficData.ts deleted file mode 100644 index e98c146..0000000 --- a/src/types/cyberfraud/trafficData.ts +++ /dev/null @@ -1,201 +0,0 @@ -import { z } from 'zod'; -import { DATE_FORMAT_EXAMPLE_END, DATE_FORMAT_EXAMPLE_START } from '../../utils/constants'; - -export const TrafficDataSourceEnum = z.enum(['web', 'mobile'], { - description: - 'Platform filter: ["web"], ["mobile"], or ["web","mobile"]. NOTE: Mobile traffic may be minimal/absent in some environments (observed 99.999% web dominance). Use both for complete coverage, individual for platform-specific analysis.', -}); -export const TrafficDataOvertimeEnum = z.enum( - ['legitimate', 'blocked', 'potentialBlock', 'whitelist', 'blacklist', 'goodKnownBots', 'captchaSolved'], - { - description: - 'TIME-SERIES ANALYSIS: Returns intervals with ~20min timestamps for trend visualization. ❌ MUTUALLY EXCLUSIVE with "count" parameter. 🚨 CRITICAL: DO NOT combine with "tops" - causes misleading aggregation with all data front-loaded into first interval. ✅ BEST FOR: Attack timelines, trend charts, pattern detection. EXAMPLE: {"overtime": ["blocked"]} → Attack volume over time. COMBINE WITH: filters for focus, but NEVER with "tops".', - }, -); -export const TrafficDataTopsEnum = z.enum(['incidents', 'path'], { - description: - '🔄 RESPONSE TRANSFORMER: Completely changes response structure from aggregates to detailed breakdowns. 🚨 CRITICAL: DO NOT combine with "overtime" - causes misleading aggregation where all historical data appears in first interval with zeros after. ⚠️ CRITICAL INSIGHTS: "incidents" reveals attack classification (Bot Behavior, Spoof, Bad Reputation, etc.), "path" shows URL-specific targeting. WITHOUT tops: Aggregate totals. WITH tops: Individual breakdowns per category. ✅ SAFE USAGE: Combine with "count" only.', -}); -export const TrafficDataTrafficEnum = z.enum(['blocked', 'blacklist', 'potentialBlock'], { - description: - 'SECURITY-ONLY FILTER: Shows EXCLUSIVELY blocked/suspicious traffic. COMPLEMENTS count/overtime metrics, does NOT replace them. ✅ USE CASE: Pure security analysis, threat-focused reporting. EXCLUDES: All legitimate traffic. COMBINE WITH: Any count/overtime metrics for security-centric view.', -}); -export const TrafficDataPageTypeEnum = z.enum( - [ - 'login', - 'login_attempt', - 'checkout', - 'purchase', - 'purchase_request', - 'productsAndSearch', - 'research', - 'apiCall', - 'resource', - 'mobileUserAgents', - ], - { - description: - 'PAGE TYPE FILTER: Focuses analysis on specific user journeys. ⚠️ SCOPE WARNING: Very restrictive (observed 87% data reduction). USE CASES: Login security analysis, checkout protection, API endpoint monitoring. COMBINE WITH: Security metrics for targeted threat analysis.', - }, -); -export const TrafficDataCountEnum = z.enum( - [ - 'legitimate', - 'potentialBlock', - 'blocked', - 'whitelist', - 'blacklist', - 'goodKnownBots', - 'captchaSolved', - 'mobile', - 'web', - ], - { - description: - 'AGGREGATE ANALYSIS: Returns total counts across entire time range. ❌ MUTUALLY EXCLUSIVE with "overtime" parameter. ⚠️ LIMITATION: Will NOT return path breakdowns even with tops=["path"] - returns aggregate totals only. ✅ BEST FOR: Dashboards, KPIs, executive summaries. EXAMPLE: {"count": ["legitimate", "blocked"]} → Simple totals.', - }, -); - -export const TrafficDataMetricsEnrichmentSchema = z - .object({ - accountName: z - .string() - .optional() - .describe( - '🏢 ACCOUNT CONTEXT: Human-readable account name for dashboard labeling and report generation. 💡 USE CASES: Multi-tenant environments, executive reporting, audit trails. 📊 ENRICHMENT: Adds business context without affecting core metrics.', - ), - widgetTitle: z - .string() - .optional() - .describe( - '📊 DASHBOARD LABELING: Custom title for UI widgets and chart displays. 💡 USE CASES: Custom dashboards, executive summaries, operational monitoring. 🎯 EXAMPLES: "Login Security Overview", "Attack Timeline - Production".', - ), - uiContext: z - .string() - .optional() - .describe( - '🖥️ UI INTEGRATION: Context identifier for frontend integration and state management. 💡 USE CASES: Multi-dashboard applications, widget state tracking, user interface coordination. 📋 EXAMPLES: "main-dashboard", "security-ops-center", "executive-summary".', - ), - }) - .passthrough(); - -export const TrafficDataInputSchema = z.object({ - startTime: z - .string() - .describe( - `⏰ TIME RANGE START: ISO 8601 datetime string defining analysis period beginning. 🎯 FORMAT: "${DATE_FORMAT_EXAMPLE_START}". ⚠️ CONSTRAINT: Must be within API limits for data retention. 💡 STRATEGY: Use shorter windows for real-time monitoring, longer periods for trend analysis and pattern detection.`, - ), - endTime: z - .string() - .describe( - `🏁 TIME RANGE END: ISO 8601 datetime string defining analysis period conclusion. 🎯 FORMAT: "${DATE_FORMAT_EXAMPLE_END}". ⚠️ CONSTRAINT: Must be after startTime. 💡 STRATEGY: Use current time for live dashboards, specific timestamps for historical analysis and incident investigation.`, - ), - source: z - .array(TrafficDataSourceEnum) - .optional() - .describe( - '🌐 PLATFORM FILTER: ["web"], ["mobile"], or both. NOTE: Mobile traffic often minimal (<0.001% observed). Use for platform-specific security analysis or complete coverage.', - ), - overtime: z - .array(TrafficDataOvertimeEnum) - .optional() - .describe( - '📈 TIME-SERIES DATA: Returns ~20min interval data for trends. ❌ CANNOT combine with "count". 🚨 CRITICAL: DO NOT combine with "tops" - causes misleading aggregation with all data front-loaded into first interval. ✅ PERFECT FOR: Attack timelines, trend analysis, pattern detection. EXAMPLES: {"overtime": ["blocked"]} → Attack volume timeline.', - ), - tops: z - .array(TrafficDataTopsEnum) - .optional() - .describe( - '🔄 BREAKDOWN TRANSFORMER: Changes from totals to detailed categorization. 🚨 CRITICAL: DO NOT combine with "overtime" - causes misleading front-loaded aggregation. ⚠️ INSIGHTS: "incidents" reveals attack types (Bot Behavior, Spoof, Bad Reputation), "path" shows URL targeting. ✅ SAFE USAGE: Combine with "count" only for accurate breakdowns.', - ), - traffic: z - .array(TrafficDataTrafficEnum) - .optional() - .describe( - '🚨 SECURITY-ONLY FILTER: Shows ONLY threats/blocks. Excludes all legitimate traffic. PERFECT FOR: Pure security analysis, threat hunting, incident investigation. COMBINES WITH: any count/overtime metrics.', - ), - pageType: z - .array(TrafficDataPageTypeEnum) - .optional() - .describe( - '🎯 PAGE JOURNEY FILTER: Focus on specific user flows. ⚠️ MAJOR SCOPE REDUCTION: Can eliminate 87%+ of data. BEST FOR: Login security, checkout protection, API monitoring. USE SPARINGLY for targeted analysis.', - ), - count: z - .array(TrafficDataCountEnum) - .optional() - .describe( - '📊 AGGREGATE TOTALS: Returns summary counts across time range. ❌ CANNOT combine with "overtime". ✅ PERFECT FOR: Executive dashboards, KPI reporting, quick health checks. EXAMPLES: {"count": ["legitimate", "blocked"]} → Traffic health summary.', - ), - withoutTotals: z - .boolean() - .optional() - .describe( - 'Excludes summary totals from response. Use when only breakdown data is needed to reduce response size.', - ), - metricsEnrichment: TrafficDataMetricsEnrichmentSchema.optional().describe( - '🏷️ CONTEXTUAL METADATA: Enrichment object for dashboard integration and reporting context. ⚠️ NO IMPACT: Does not affect core data or query performance. 💡 USE CASES: Multi-tenant dashboards, executive reporting, UI state management. 📊 BENEFITS: Enhanced labeling, audit trails, and business context for analysis.', - ), -}); -export type TrafficDataInput = z.infer; - -const TrafficDataIntervalSchema = z - .object({ - timestamp: z - .number() - .optional() - .describe( - '⏱️ INTERVAL TIMESTAMP: Timestamp for the ~20-minute interval (milliseconds since epoch). Essential for time-series visualization and trend analysis.', - ), - count: z.number().optional().describe('Count for this interval.'), - }) - .passthrough(); - -const TrafficDataSeriesOvertimeSchema = z - .object({ - value: z.string().optional().describe('Label or value for this series.'), - intervals: z.array(TrafficDataIntervalSchema).optional().describe('List of intervals for this series.'), - }) - .passthrough(); - -const TrafficDataSeriesCountSchema = z - .object({ - value: z.string().optional().describe('Label or value for this series.'), - count: z.number().optional().describe('Count for this series.'), - }) - .passthrough(); - -const TrafficDataTotalsSchema = z - .object({ - total: z.number().optional().describe('Total count.'), - totalBlocked: z.number().optional().describe('Total blocked count.'), - }) - .passthrough(); - -const TrafficDataContentSchema = z - .object({ - legitimate: z.array(TrafficDataSeriesOvertimeSchema).optional(), - blocked: z.array(TrafficDataSeriesOvertimeSchema).optional(), - blacklist: z.array(TrafficDataSeriesOvertimeSchema).optional(), - goodKnownBots: z.array(TrafficDataSeriesOvertimeSchema).optional(), - potentialBlock: z.array(TrafficDataSeriesOvertimeSchema).optional(), - whitelist: z.array(TrafficDataSeriesOvertimeSchema).optional(), - captchaSolved: z.array(TrafficDataSeriesOvertimeSchema).optional(), - incidents: z.array(TrafficDataSeriesCountSchema).optional(), - path: z.array(TrafficDataSeriesCountSchema).optional(), - mobile: z.array(TrafficDataSeriesCountSchema).optional(), - web: z.array(TrafficDataSeriesCountSchema).optional(), - totals: TrafficDataTotalsSchema.optional(), - dataLags: z.array(z.unknown()).optional(), - }) - .passthrough() - .optional() - .describe('Main content data, keyed by metric type.'); - -export const TrafficDataOutputSchema = z - .object({ - result: z.boolean().optional().describe('Whether the request was successful.'), - message: z.string().optional().describe('Response message.'), - content: TrafficDataContentSchema, - }) - .passthrough(); -export type TrafficDataResponse = z.infer; diff --git a/src/utils/httpClient.ts b/src/utils/httpClient.ts index 17536bb..a86ed22 100644 --- a/src/utils/httpClient.ts +++ b/src/utils/httpClient.ts @@ -38,6 +38,9 @@ export class HttpClient { if (this.apiToken) { headers['Authorization'] = `Bearer ${this.apiToken}`; } + if (options.body !== undefined) { + headers['Content-Type'] = 'application/json'; + } headers[MCP_VERSION_HEADER] = MCP_VERSION; const res = await this.fetchImpl(url, { method: options.method || 'GET', diff --git a/test/services/cyberfraudService.test.ts b/test/services/cyberfraudService.test.ts index a236177..ee28128 100644 --- a/test/services/cyberfraudService.test.ts +++ b/test/services/cyberfraudService.test.ts @@ -105,4 +105,88 @@ describe('CyberfraudService', () => { const url = httpClient.request.firstCall.args[0]; expect(url).to.include('/overview/cid123'); }); + + it('getTrafficOvertime calls httpClient with POST and returns parsed response', async () => { + const fakeResponse = { result: true, content: { results: [] } }; + httpClient.request.resolves({ json: async () => fakeResponse, ok: true }); + const now = new Date(); + const startTime = new Date(now.getTime() - 24 * 60 * 60 * 1000).toISOString(); + const endTime = now.toISOString(); + const params = { + startTime, + endTime, + trafficSource: ['web', 'mobile'], + filters: { trafficTags: ['blocked'] }, + seriesFields: ['knownBot'], + }; + const result = await service.getTrafficOvertime(params as any); + expect(httpClient.request.calledOnce).to.be.true; + const [url, options] = httpClient.request.firstCall.args; + expect(url).to.include('/cyberfraud/traffic/overtime?'); + expect(url).to.include('from='); + expect(url).to.include('to='); + expect(options.method).to.equal('POST'); + expect(options.body).to.deep.equal({ + trafficSource: ['web', 'mobile'], + filters: { trafficTags: ['blocked'] }, + seriesFields: ['knownBot'], + }); + expect(result).to.equal(fakeResponse); + }); + + it('getTrafficMetrics calls httpClient with POST and returns parsed response', async () => { + const fakeResponse = { result: true, content: { results: { total: 100 } } }; + httpClient.request.resolves({ json: async () => fakeResponse, ok: true }); + const now = new Date(); + const startTime = new Date(now.getTime() - 24 * 60 * 60 * 1000).toISOString(); + const endTime = now.toISOString(); + const params = { + startTime, + endTime, + trafficSource: ['web'], + }; + const result = await service.getTrafficMetrics(params as any); + expect(httpClient.request.calledOnce).to.be.true; + const [url, options] = httpClient.request.firstCall.args; + expect(url).to.include('/cyberfraud/traffic/metrics?'); + expect(options.method).to.equal('POST'); + expect(options.body).to.deep.equal({ trafficSource: ['web'] }); + expect(result).to.equal(fakeResponse); + }); + + it('getTrafficTops calls httpClient with POST and encoded field in URL', async () => { + const fakeResponse = { result: true, content: { results: [{ value: 'US', count: 10 }] } }; + httpClient.request.resolves({ json: async () => fakeResponse, ok: true }); + const now = new Date(); + const startTime = new Date(now.getTime() - 24 * 60 * 60 * 1000).toISOString(); + const endTime = now.toISOString(); + const params = { + startTime, + endTime, + field: 'country', + trafficSource: ['web', 'mobile'], + limit: 5, + includeNulls: true, + }; + const result = await service.getTrafficTops(params as any); + expect(httpClient.request.calledOnce).to.be.true; + const [url, options] = httpClient.request.firstCall.args; + expect(url).to.include('/cyberfraud/traffic/tops/country?'); + expect(options.method).to.equal('POST'); + expect(options.body).to.deep.equal({ + trafficSource: ['web', 'mobile'], + limit: 5, + includeNulls: true, + }); + expect(result).to.equal(fakeResponse); + }); + + it('getTrafficOvertime propagates httpClient.request error', async () => { + httpClient.request.rejects(new Error('network fail')); + const now = new Date(); + const startTime = new Date(now.getTime() - 24 * 60 * 60 * 1000).toISOString(); + const endTime = now.toISOString(); + const params = { startTime, endTime, trafficSource: ['web'] }; + await expect(service.getTrafficOvertime(params as any)).to.be.rejectedWith('network fail'); + }); }); diff --git a/test/tools/getTrafficMetrics.test.ts b/test/tools/getTrafficMetrics.test.ts new file mode 100644 index 0000000..4884d18 --- /dev/null +++ b/test/tools/getTrafficMetrics.test.ts @@ -0,0 +1,19 @@ +import * as chai from 'chai'; +import sinon from 'sinon'; +import { registerCyberfraudGetTrafficMetrics } from '../../src/tools/getTrafficMetrics'; + +describe('registerCyberfraudGetTrafficMetrics', () => { + const { expect } = chai; + it('registers tool and handler calls service', async () => { + const server = { registerTool: sinon.stub() }; + const service = { getTrafficMetrics: sinon.stub().resolves('result') }; + registerCyberfraudGetTrafficMetrics(server as any, service as any); + expect(server.registerTool.calledOnce).to.be.true; + const [name, config, handler] = server.registerTool.firstCall.args; + expect(name).to.equal('human_get_traffic_metrics'); + expect(config).to.have.property('description'); + const params = { foo: 'bar' }; + await handler(params); + expect(service.getTrafficMetrics.calledWith(params)).to.be.true; + }); +}); diff --git a/test/tools/getTrafficOvertime.test.ts b/test/tools/getTrafficOvertime.test.ts new file mode 100644 index 0000000..6eb18f9 --- /dev/null +++ b/test/tools/getTrafficOvertime.test.ts @@ -0,0 +1,19 @@ +import * as chai from 'chai'; +import sinon from 'sinon'; +import { registerCyberfraudGetTrafficOvertime } from '../../src/tools/getTrafficOvertime'; + +describe('registerCyberfraudGetTrafficOvertime', () => { + const { expect } = chai; + it('registers tool and handler calls service', async () => { + const server = { registerTool: sinon.stub() }; + const service = { getTrafficOvertime: sinon.stub().resolves('result') }; + registerCyberfraudGetTrafficOvertime(server as any, service as any); + expect(server.registerTool.calledOnce).to.be.true; + const [name, config, handler] = server.registerTool.firstCall.args; + expect(name).to.equal('human_get_traffic_overtime'); + expect(config).to.have.property('description'); + const params = { foo: 'bar' }; + await handler(params); + expect(service.getTrafficOvertime.calledWith(params)).to.be.true; + }); +}); diff --git a/test/tools/getTrafficTops.test.ts b/test/tools/getTrafficTops.test.ts new file mode 100644 index 0000000..6130eb1 --- /dev/null +++ b/test/tools/getTrafficTops.test.ts @@ -0,0 +1,19 @@ +import * as chai from 'chai'; +import sinon from 'sinon'; +import { registerCyberfraudGetTrafficTops } from '../../src/tools/getTrafficTops'; + +describe('registerCyberfraudGetTrafficTops', () => { + const { expect } = chai; + it('registers tool and handler calls service', async () => { + const server = { registerTool: sinon.stub() }; + const service = { getTrafficTops: sinon.stub().resolves('result') }; + registerCyberfraudGetTrafficTops(server as any, service as any); + expect(server.registerTool.calledOnce).to.be.true; + const [name, config, handler] = server.registerTool.firstCall.args; + expect(name).to.equal('human_get_traffic_tops'); + expect(config).to.have.property('description'); + const params = { foo: 'bar' }; + await handler(params); + expect(service.getTrafficTops.calledWith(params)).to.be.true; + }); +}); diff --git a/test/utils/httpClient.test.ts b/test/utils/httpClient.test.ts index 32f6e42..708f20d 100644 --- a/test/utils/httpClient.test.ts +++ b/test/utils/httpClient.test.ts @@ -56,6 +56,7 @@ describe('HttpClient', () => { const args = fetchStub.firstCall.args[1]; expect(args.method).to.equal('POST'); expect(args.body).to.equal(JSON.stringify({ foo: 'bar' })); + expect(args.headers['Content-Type']).to.equal('application/json'); }); it('adds Authorization header if apiToken is set', async () => {