Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions src/api/results.ts
Original file line number Diff line number Diff line change
@@ -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<typeof FeatureTotalEvaluationsQuerySchema> = {},
) => {
return apiClient.get(
'/v1/projects/:project/features/:feature/results/total-evaluations',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should rate limit this endpoint - or start caching the results from snowflake. If MCP servers are calling this with any frequency they'll get rate-limited.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yea we can keep an eye on that through DD, and add a rate limit if needed to the API

{
headers: buildHeaders(token),
params: {
project: project_id,
feature: feature_key,
},
queries,
},
)
}

export const fetchProjectTotalEvaluations = async (
token: string,
project_id: string,
queries: z.infer<typeof ProjectTotalEvaluationsQuerySchema> = {},
) => {
return apiClient.get('/v1/projects/:project/results/total-evaluations', {
headers: buildHeaders(token),
params: {
project: project_id,
},
queries,
})
}
6 changes: 6 additions & 0 deletions src/mcp/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -62,6 +66,7 @@ const allToolDefinitions: Tool[] = processToolDefinitions([
...projectToolDefinitions,
...variableToolDefinitions,
...selfTargetingToolDefinitions,
...resultsToolDefinitions,
])

// Combine all tool handlers
Expand All @@ -71,6 +76,7 @@ const allToolHandlers: Record<string, ToolHandler> = {
...projectToolHandlers,
...variableToolHandlers,
...selfTargetingToolHandlers,
...resultsToolHandlers,
}

export class DevCycleMCPServer {
Expand Down
69 changes: 69 additions & 0 deletions src/mcp/tools/commonSchemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
194 changes: 194 additions & 0 deletions src/mcp/tools/resultsTools.ts
Original file line number Diff line number Diff line change
@@ -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<string, ToolHandler> = {
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,
)
},
}
25 changes: 25 additions & 0 deletions src/mcp/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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