From 2213017e27648f3295d2292663af850b4678b280 Mon Sep 17 00:00:00 2001 From: Jonathan Norris Date: Wed, 23 Jul 2025 16:22:57 -0400 Subject: [PATCH 1/3] feat: add result endpoints to MCP tools --- src/api/results.ts | 40 +++++++ src/mcp/server.ts | 6 + src/mcp/tools/commonSchemas.ts | 69 ++++++++++++ src/mcp/tools/resultsTools.ts | 194 +++++++++++++++++++++++++++++++++ src/mcp/types.ts | 29 ++++- 5 files changed, 336 insertions(+), 2 deletions(-) create mode 100644 src/api/results.ts create mode 100644 src/mcp/tools/resultsTools.ts 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..fea22e882 100644 --- a/src/mcp/types.ts +++ b/src/mcp/types.ts @@ -184,6 +184,31 @@ export const UpdateFeatureTargetingArgsSchema = }) export const GetFeatureAuditLogHistoryArgsSchema = z.object({ - feature_key: z.string(), - days_back: z.number().min(1).max(365).default(30).optional(), + featureKey: z.string(), + daysBack: 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 From 3dcd607c35c299df188749b70dd0fe5532bfb9dd Mon Sep 17 00:00:00 2001 From: Jonathan Norris Date: Wed, 23 Jul 2025 16:28:55 -0400 Subject: [PATCH 2/3] fix: yarn build --- src/mcp/tools/featureTools.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/mcp/tools/featureTools.ts b/src/mcp/tools/featureTools.ts index 1ca469dbc..2799d7ea4 100644 --- a/src/mcp/tools/featureTools.ts +++ b/src/mcp/tools/featureTools.ts @@ -1138,8 +1138,8 @@ export const featureToolHandlers: Record = { getFeatureAuditLogHistory( authToken, projectKey, - validatedArgs.feature_key, - validatedArgs.days_back || 30, + validatedArgs.featureKey, + validatedArgs.daysBack || 30, ), 'getFeatureAuditLogHistory', ) @@ -1148,7 +1148,7 @@ export const featureToolHandlers: Record = { generateFeatureDashboardLink( orgId, projectKey, - validatedArgs.feature_key, + validatedArgs.featureKey, 'audit-log', ), ) From 1347dae94df4e2947008120733318f9af3d5279a Mon Sep 17 00:00:00 2001 From: Jonathan Norris Date: Wed, 23 Jul 2025 16:33:13 -0400 Subject: [PATCH 3/3] fix: revert feature tools changes --- src/mcp/tools/featureTools.ts | 6 +++--- src/mcp/types.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/mcp/tools/featureTools.ts b/src/mcp/tools/featureTools.ts index 2799d7ea4..1ca469dbc 100644 --- a/src/mcp/tools/featureTools.ts +++ b/src/mcp/tools/featureTools.ts @@ -1138,8 +1138,8 @@ export const featureToolHandlers: Record = { getFeatureAuditLogHistory( authToken, projectKey, - validatedArgs.featureKey, - validatedArgs.daysBack || 30, + validatedArgs.feature_key, + validatedArgs.days_back || 30, ), 'getFeatureAuditLogHistory', ) @@ -1148,7 +1148,7 @@ export const featureToolHandlers: Record = { generateFeatureDashboardLink( orgId, projectKey, - validatedArgs.featureKey, + validatedArgs.feature_key, 'audit-log', ), ) diff --git a/src/mcp/types.ts b/src/mcp/types.ts index fea22e882..646dc7a05 100644 --- a/src/mcp/types.ts +++ b/src/mcp/types.ts @@ -184,8 +184,8 @@ export const UpdateFeatureTargetingArgsSchema = }) export const GetFeatureAuditLogHistoryArgsSchema = z.object({ - featureKey: z.string(), - daysBack: z.number().min(1).max(365).default(30).optional(), + feature_key: z.string(), + days_back: z.number().min(1).max(365).default(30).optional(), }) // Base evaluation query schema (matches API camelCase naming)