diff --git a/src/api/results.ts b/src/api/results.ts new file mode 100644 index 000000000..e539fabcd --- /dev/null +++ b/src/api/results.ts @@ -0,0 +1,40 @@ +import apiClient from './apiClient' +import { buildHeaders } from './common' +import { + FeatureTotalEvaluationsQuerySchema, + ProjectTotalEvaluationsQuerySchema, +} from '../mcp/types' +import { z } from 'zod' + +export const fetchFeatureTotalEvaluations = async ( + token: string, + project_id: string, + feature_key: string, + queries: z.infer = {}, +) => { + return apiClient.get( + '/v1/projects/:project/features/:feature/results/total-evaluations', + { + headers: buildHeaders(token), + params: { + project: project_id, + feature: feature_key, + }, + queries, + }, + ) +} + +export const fetchProjectTotalEvaluations = async ( + token: string, + project_id: string, + queries: z.infer = {}, +) => { + return apiClient.get('/v1/projects/:project/results/total-evaluations', { + headers: buildHeaders(token), + params: { + project: project_id, + }, + queries, + }) +} diff --git a/src/mcp/server.ts b/src/mcp/server.ts index d18062f77..15a8982fe 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -27,6 +27,10 @@ import { selfTargetingToolDefinitions, selfTargetingToolHandlers, } from './tools/selfTargetingTools' +import { + resultsToolDefinitions, + resultsToolHandlers, +} from './tools/resultsTools' // Environment variable to control output schema inclusion const ENABLE_OUTPUT_SCHEMAS = process.env.ENABLE_OUTPUT_SCHEMAS === 'true' @@ -62,6 +66,7 @@ const allToolDefinitions: Tool[] = processToolDefinitions([ ...projectToolDefinitions, ...variableToolDefinitions, ...selfTargetingToolDefinitions, + ...resultsToolDefinitions, ]) // Combine all tool handlers @@ -71,6 +76,7 @@ const allToolHandlers: Record = { ...projectToolHandlers, ...variableToolHandlers, ...selfTargetingToolHandlers, + ...resultsToolHandlers, } export class DevCycleMCPServer { diff --git a/src/mcp/tools/commonSchemas.ts b/src/mcp/tools/commonSchemas.ts index 59d5c1ac7..af52ac923 100644 --- a/src/mcp/tools/commonSchemas.ts +++ b/src/mcp/tools/commonSchemas.ts @@ -346,3 +346,72 @@ export const TARGET_AUDIENCE_PROPERTY = { }, required: ['filters'] as const, } + +// ============================================================================= +// RESULTS AND ANALYTICS PROPERTIES +// ============================================================================= + +export const EVALUATION_QUERY_PROPERTIES = { + startDate: { + type: 'number' as const, + description: 'Start date as Unix timestamp (milliseconds since epoch)', + }, + endDate: { + type: 'number' as const, + description: 'End date as Unix timestamp (milliseconds since epoch)', + }, + platform: { + type: 'string' as const, + description: 'Platform filter for evaluation results', + }, + variable: { + type: 'string' as const, + description: 'Variable key filter for evaluation results', + }, + environment: { + type: 'string' as const, + description: 'Environment key to filter results', + }, + period: { + type: 'string' as const, + enum: ['day', 'hour', 'month'] as const, + description: 'Time aggregation period for results', + }, + sdkType: { + type: 'string' as const, + enum: ['client', 'server', 'mobile', 'api'] as const, + description: 'Filter by SDK type', + }, +} + +export const EVALUATION_DATA_POINT_SCHEMA = { + type: 'object' as const, + properties: { + date: { + type: 'string' as const, + format: 'date-time' as const, + description: 'ISO timestamp for this data point', + }, + values: { + type: 'object' as const, + description: 'Evaluation values for this time period', + }, + }, + required: ['date', 'values'] as const, +} + +export const PROJECT_DATA_POINT_SCHEMA = { + type: 'object' as const, + properties: { + date: { + type: 'string' as const, + format: 'date-time' as const, + description: 'ISO timestamp for this data point', + }, + value: { + type: 'number' as const, + description: 'Total evaluations in this time period', + }, + }, + required: ['date', 'value'] as const, +} diff --git a/src/mcp/tools/resultsTools.ts b/src/mcp/tools/resultsTools.ts new file mode 100644 index 000000000..e0c44c861 --- /dev/null +++ b/src/mcp/tools/resultsTools.ts @@ -0,0 +1,194 @@ +import { Tool } from '@modelcontextprotocol/sdk/types.js' +import { DevCycleApiClient, handleZodiosValidationErrors } from '../utils/api' +import { + fetchFeatureTotalEvaluations, + fetchProjectTotalEvaluations, +} from '../../api/results' +import { + GetFeatureTotalEvaluationsArgsSchema, + GetProjectTotalEvaluationsArgsSchema, + FeatureTotalEvaluationsQuerySchema, + ProjectTotalEvaluationsQuerySchema, +} from '../types' +import { ToolHandler } from '../server' +import { + DASHBOARD_LINK_PROPERTY, + FEATURE_KEY_PROPERTY, + EVALUATION_QUERY_PROPERTIES, + EVALUATION_DATA_POINT_SCHEMA, + PROJECT_DATA_POINT_SCHEMA, +} from './commonSchemas' + +// Helper functions to generate dashboard links +const generateFeatureAnalyticsDashboardLink = ( + orgId: string, + projectKey: string, + featureKey: string, +): string => { + return `https://app.devcycle.com/o/${orgId}/p/${projectKey}/features/${featureKey}/analytics` +} + +const generateProjectAnalyticsDashboardLink = ( + orgId: string, + projectKey: string, +): string => { + return `https://app.devcycle.com/o/${orgId}/p/${projectKey}/analytics` +} + +// ============================================================================= +// INPUT SCHEMAS +// ============================================================================= + +const FEATURE_EVALUATION_QUERY_PROPERTIES = { + featureKey: FEATURE_KEY_PROPERTY, + ...EVALUATION_QUERY_PROPERTIES, +} + +const PROJECT_EVALUATION_QUERY_PROPERTIES = EVALUATION_QUERY_PROPERTIES + +// ============================================================================= +// OUTPUT SCHEMAS +// ============================================================================= + +const FEATURE_EVALUATIONS_OUTPUT_SCHEMA = { + type: 'object' as const, + properties: { + result: { + type: 'object' as const, + description: 'Feature evaluation data aggregated by time period', + properties: { + evaluations: { + type: 'array' as const, + description: 'Array of evaluation data points', + items: EVALUATION_DATA_POINT_SCHEMA, + }, + cached: { + type: 'boolean' as const, + description: 'Whether this result came from cache', + }, + updatedAt: { + type: 'string' as const, + format: 'date-time' as const, + description: 'When the data was last updated', + }, + }, + required: ['evaluations', 'cached', 'updatedAt'], + }, + dashboardLink: DASHBOARD_LINK_PROPERTY, + }, + required: ['result', 'dashboardLink'], +} + +const PROJECT_EVALUATIONS_OUTPUT_SCHEMA = { + type: 'object' as const, + properties: { + result: { + type: 'object' as const, + description: 'Project evaluation data aggregated by time period', + properties: { + evaluations: { + type: 'array' as const, + description: 'Array of evaluation data points', + items: PROJECT_DATA_POINT_SCHEMA, + }, + cached: { + type: 'boolean' as const, + description: 'Whether this result came from cache', + }, + updatedAt: { + type: 'string' as const, + format: 'date-time' as const, + description: 'When the data was last updated', + }, + }, + required: ['evaluations', 'cached', 'updatedAt'], + }, + dashboardLink: DASHBOARD_LINK_PROPERTY, + }, + required: ['result', 'dashboardLink'], +} + +// ============================================================================= +// TOOL DEFINITIONS +// ============================================================================= + +export const resultsToolDefinitions: Tool[] = [ + { + name: 'get_feature_total_evaluations', + description: + 'Get total variable evaluations per time period for a specific feature. Include dashboard link in the response.', + inputSchema: { + type: 'object', + properties: FEATURE_EVALUATION_QUERY_PROPERTIES, + required: ['featureKey'], + }, + outputSchema: FEATURE_EVALUATIONS_OUTPUT_SCHEMA, + }, + { + name: 'get_project_total_evaluations', + description: + 'Get total variable evaluations per time period for the entire project. Include dashboard link in the response.', + inputSchema: { + type: 'object', + properties: PROJECT_EVALUATION_QUERY_PROPERTIES, + }, + outputSchema: PROJECT_EVALUATIONS_OUTPUT_SCHEMA, + }, +] + +export const resultsToolHandlers: Record = { + get_feature_total_evaluations: async ( + args: unknown, + apiClient: DevCycleApiClient, + ) => { + const validatedArgs = GetFeatureTotalEvaluationsArgsSchema.parse(args) + + return await apiClient.executeWithDashboardLink( + 'getFeatureTotalEvaluations', + validatedArgs, + async (authToken, projectKey) => { + const { featureKey, ...apiQueries } = validatedArgs + + return await handleZodiosValidationErrors( + () => + fetchFeatureTotalEvaluations( + authToken, + projectKey, + featureKey, + apiQueries, + ), + 'fetchFeatureTotalEvaluations', + ) + }, + (orgId, projectKey) => + generateFeatureAnalyticsDashboardLink( + orgId, + projectKey, + validatedArgs.featureKey, + ), + ) + }, + get_project_total_evaluations: async ( + args: unknown, + apiClient: DevCycleApiClient, + ) => { + const validatedArgs = GetProjectTotalEvaluationsArgsSchema.parse(args) + + return await apiClient.executeWithDashboardLink( + 'getProjectTotalEvaluations', + validatedArgs, + async (authToken, projectKey) => { + return await handleZodiosValidationErrors( + () => + fetchProjectTotalEvaluations( + authToken, + projectKey, + validatedArgs, + ), + 'fetchProjectTotalEvaluations', + ) + }, + generateProjectAnalyticsDashboardLink, + ) + }, +} diff --git a/src/mcp/types.ts b/src/mcp/types.ts index 696beb876..646dc7a05 100644 --- a/src/mcp/types.ts +++ b/src/mcp/types.ts @@ -187,3 +187,28 @@ export const GetFeatureAuditLogHistoryArgsSchema = z.object({ feature_key: z.string(), days_back: z.number().min(1).max(365).default(30).optional(), }) + +// Base evaluation query schema (matches API camelCase naming) +const BaseEvaluationQuerySchema = z.object({ + startDate: z.number().optional(), + endDate: z.number().optional(), + environment: z.string().optional(), + period: z.enum(['day', 'hour', 'month']).optional(), + sdkType: z.enum(['client', 'server', 'mobile', 'api']).optional(), +}) + +// MCP argument schemas (using camelCase to match API) +export const GetFeatureTotalEvaluationsArgsSchema = + BaseEvaluationQuerySchema.extend({ + featureKey: z.string(), + platform: z.string().optional(), + variable: z.string().optional(), + }) + +export const GetProjectTotalEvaluationsArgsSchema = BaseEvaluationQuerySchema + +// API query schemas (same as MCP args since we use camelCase throughout) +export const FeatureTotalEvaluationsQuerySchema = + GetFeatureTotalEvaluationsArgsSchema.omit({ featureKey: true }) +export const ProjectTotalEvaluationsQuerySchema = + GetProjectTotalEvaluationsArgsSchema