diff --git a/oclif.manifest.json b/oclif.manifest.json index ee18e43e6..790e81773 100644 --- a/oclif.manifest.json +++ b/oclif.manifest.json @@ -1,5 +1,5 @@ { - "version": "5.21.0", + "version": "5.21.1", "commands": { "authCommand": { "id": "authCommand", diff --git a/src/api/features.ts b/src/api/features.ts index 8c76876ba..ac5581ed0 100644 --- a/src/api/features.ts +++ b/src/api/features.ts @@ -19,11 +19,21 @@ export const fetchFeatures = async ( token: string, project_id: string, queries: { - feature?: string page?: number perPage?: number + sortBy?: + | 'createdAt' + | 'updatedAt' + | 'name' + | 'key' + | 'createdBy' + | 'propertyKey' + sortOrder?: 'asc' | 'desc' search?: string - staleness?: string + staleness?: 'all' | 'unused' | 'released' | 'unmodified' | 'notStale' + createdBy?: string + type?: 'release' | 'experiment' | 'permission' | 'ops' + status?: 'active' | 'complete' | 'archived' } = {}, ): Promise => { const response = await apiClient.get(FEATURE_URL, { diff --git a/src/api/variables.ts b/src/api/variables.ts index 2b019d4dd..87bef5a7b 100644 --- a/src/api/variables.ts +++ b/src/api/variables.ts @@ -70,10 +70,20 @@ export const fetchVariables = async ( token: string, project_id: string, queries: { - feature?: string page?: number perPage?: number + sortBy?: + | 'createdAt' + | 'updatedAt' + | 'name' + | 'key' + | 'createdBy' + | 'propertyKey' + sortOrder?: 'asc' | 'desc' search?: string + feature?: string + type?: 'String' | 'Boolean' | 'Number' | 'JSON' + status?: 'active' | 'archived' } = {}, ) => { return await apiClient.get('/v1/projects/:project/variables', { diff --git a/src/mcp/tools/commonSchemas.ts b/src/mcp/tools/commonSchemas.ts new file mode 100644 index 000000000..59d5c1ac7 --- /dev/null +++ b/src/mcp/tools/commonSchemas.ts @@ -0,0 +1,348 @@ +/** + * Common schema definitions used across multiple MCP tool files + */ + +// ============================================================================= +// SHARED OUTPUT SCHEMA PROPERTIES +// ============================================================================= + +export const DASHBOARD_LINK_PROPERTY = { + type: 'string' as const, + format: 'uri' as const, + description: 'URL to view and manage resources in the DevCycle dashboard', +} + +export const MESSAGE_RESPONSE_SCHEMA = { + type: 'object' as const, + properties: { + message: { + type: 'string' as const, + }, + }, + required: ['message'], +} + +// ============================================================================= +// SHARED INPUT SCHEMA PROPERTIES +// ============================================================================= + +export const FEATURE_KEY_PROPERTY = { + type: 'string' as const, + description: + 'The key of the feature (unique, immutable, max 100 characters, pattern: ^[a-z0-9-_.]+$)', +} + +export const ENVIRONMENT_KEY_PROPERTY = { + type: 'string' as const, + description: + 'The key of the environment (unique, immutable, max 100 characters, pattern: ^[a-z0-9-_.]+$)', +} + +export const VARIATION_KEY_PROPERTY = { + type: 'string' as const, + description: + 'Unique variation key (unique, immutable, max 100 characters, pattern: ^[a-z0-9-_.]+$)', +} + +export const VARIABLE_KEY_PROPERTY = { + type: 'string' as const, + description: + 'The variable key (unique, immutable, max 100 characters, pattern: ^[a-z0-9-_.]+$)', +} + +export const PROJECT_KEY_PROPERTY = { + type: 'string' as const, + description: + 'The project key (unique, immutable, max 100 characters, pattern: ^[a-z0-9-_.]+$)', +} + +// Filter type definitions based on DevCycle API swagger schemas + +export const ALL_FILTER_SCHEMA = { + type: 'object' as const, + description: 'Filter that matches all users', + properties: { + type: { + type: 'string' as const, + description: 'Filter type of this audience', + enum: ['all'] as const, + }, + }, + required: ['type'] as const, +} + +export const USER_FILTER_SCHEMA = { + type: 'object' as const, + description: 'Filter by basic user properties', + properties: { + type: { + type: 'string' as const, + description: 'Filter type of this audience', + enum: ['user'] as const, + }, + subType: { + type: 'string' as const, + description: 'Sub type of this filter', + enum: ['user_id', 'email', 'platform', 'deviceModel'] as const, + }, + comparator: { + type: 'string' as const, + description: 'Comparator to use', + enum: [ + '=', + '!=', + 'exist', + '!exist', + 'contain', + '!contain', + 'startWith', + '!startWith', + 'endWith', + '!endWith', + ] as const, + }, + values: { + type: 'array' as const, + description: + 'Array of values (required for all filters except exist/!exist)', + items: { + type: 'string' as const, + }, + }, + }, + required: ['type', 'subType', 'comparator'] as const, +} + +export const USER_COUNTRY_FILTER_SCHEMA = { + type: 'object' as const, + description: 'Filter by user country', + properties: { + type: { + type: 'string' as const, + description: 'Filter type of this audience', + enum: ['user'] as const, + }, + subType: { + type: 'string' as const, + description: 'Sub type of this filter', + enum: ['country'] as const, + }, + comparator: { + type: 'string' as const, + description: 'Comparator to use', + enum: [ + '=', + '!=', + 'exist', + '!exist', + 'contain', + '!contain', + 'startWith', + '!startWith', + 'endWith', + '!endWith', + ] as const, + }, + values: { + type: 'array' as const, + description: 'Array of country codes (e.g., CA, US)', + items: { + type: 'string' as const, + }, + }, + }, + required: ['type', 'subType', 'comparator'] as const, +} + +export const USER_APP_VERSION_FILTER_SCHEMA = { + type: 'object' as const, + description: 'Filter by application version', + properties: { + type: { + type: 'string' as const, + description: 'Filter type of this audience', + enum: ['user'] as const, + }, + subType: { + type: 'string' as const, + description: 'Sub type of this filter', + enum: ['appVersion'] as const, + }, + comparator: { + type: 'string' as const, + description: 'Comparator to use', + enum: ['=', '!=', '>', '>=', '<', '<=', 'exist', '!exist'] as const, + }, + values: { + type: 'array' as const, + description: 'Array of version strings (e.g., 1.0.2)', + items: { + type: 'string' as const, + }, + }, + }, + required: ['type', 'subType', 'comparator'] as const, +} + +export const USER_PLATFORM_VERSION_FILTER_SCHEMA = { + type: 'object' as const, + description: 'Filter by platform version', + properties: { + type: { + type: 'string' as const, + description: 'Filter type of this audience', + enum: ['user'] as const, + }, + subType: { + type: 'string' as const, + description: 'Sub type of this filter', + enum: ['platformVersion'] as const, + }, + comparator: { + type: 'string' as const, + description: 'Comparator to use', + enum: ['=', '!=', '>', '>=', '<', '<=', 'exist', '!exist'] as const, + }, + values: { + type: 'array' as const, + description: 'Array of platform version strings', + items: { + type: 'string' as const, + }, + }, + }, + required: ['type', 'subType', 'comparator'] as const, +} + +export const USER_CUSTOM_FILTER_SCHEMA = { + type: 'object' as const, + description: 'Filter by custom user data properties', + properties: { + type: { + type: 'string' as const, + description: 'Filter type of this audience', + enum: ['user'] as const, + }, + subType: { + type: 'string' as const, + description: 'Sub type of this filter', + enum: ['customData'] as const, + }, + comparator: { + type: 'string' as const, + description: 'Comparator to use', + enum: [ + '=', + '!=', + '>', + '>=', + '<', + '<=', + 'exist', + '!exist', + 'contain', + '!contain', + 'startWith', + '!startWith', + 'endWith', + '!endWith', + ] as const, + }, + dataKey: { + type: 'string' as const, + description: 'Data Key used for custom data', + minLength: 1, + }, + dataKeyType: { + type: 'string' as const, + description: 'Data Key Type used for custom data', + enum: ['String', 'Boolean', 'Number'] as const, + }, + values: { + type: 'array' as const, + description: 'Array of values (type depends on dataKeyType)', + items: { + anyOf: [ + { type: 'string' as const }, + { type: 'number' as const }, + { type: 'boolean' as const }, + ], + }, + }, + }, + required: [ + 'type', + 'subType', + 'comparator', + 'dataKey', + 'dataKeyType', + ] as const, +} + +export const AUDIENCE_MATCH_FILTER_SCHEMA = { + type: 'object' as const, + description: 'Filter by audience membership', + properties: { + type: { + type: 'string' as const, + enum: ['audienceMatch'] as const, + }, + comparator: { + type: 'string' as const, + enum: ['=', '!='] as const, + }, + _audiences: { + type: 'array' as const, + description: 'Array of audience IDs to match against', + items: { + type: 'string' as const, + }, + }, + }, + required: ['type'] as const, +} + +// Target Audience schema based on DevCycle API swagger definition +export const TARGET_AUDIENCE_PROPERTY = { + type: 'object' as const, + description: 'Audience definition for the target', + properties: { + name: { + type: 'string' as const, + description: + 'Audience display name, must be set for project-level audiences.', + example: 'Android Users', + maxLength: 100, + minLength: 1, + }, + filters: { + type: 'object' as const, + description: + 'Audience filters, describing logic for segmenting users', + properties: { + filters: { + type: 'array' as const, + description: 'Array of filter conditions', + items: { + anyOf: [ + ALL_FILTER_SCHEMA, + USER_FILTER_SCHEMA, + USER_COUNTRY_FILTER_SCHEMA, + USER_APP_VERSION_FILTER_SCHEMA, + USER_PLATFORM_VERSION_FILTER_SCHEMA, + USER_CUSTOM_FILTER_SCHEMA, + AUDIENCE_MATCH_FILTER_SCHEMA, + ], + }, + }, + operator: { + type: 'string' as const, + description: 'Operator type for combining filters', + enum: ['and', 'or'] as const, + }, + }, + required: ['filters', 'operator'] as const, + }, + }, + required: ['filters'] as const, +} diff --git a/src/mcp/tools/environmentTools.ts b/src/mcp/tools/environmentTools.ts index a71fd4e8f..2f1e53d10 100644 --- a/src/mcp/tools/environmentTools.ts +++ b/src/mcp/tools/environmentTools.ts @@ -13,19 +13,64 @@ import { UpdateEnvironmentArgsSchema, } from '../types' import { ToolHandler } from '../server' +import { + DASHBOARD_LINK_PROPERTY, + ENVIRONMENT_KEY_PROPERTY, +} from './commonSchemas' + +// Helper function to generate environment dashboard links +const generateEnvironmentDashboardLink = ( + orgId: string, + projectKey: string, +): string => { + return `https://app.devcycle.com/o/${orgId}/settings/p/${projectKey}/environments` +} + +// ============================================================================= +// INPUT SCHEMAS +// ============================================================================= // Reusable schema components -const ENVIRONMENT_KEY_PROPERTY = { +const ENVIRONMENT_COLOR_PROPERTY = { + type: 'string' as const, + description: 'Color used to represent this environment in the UI', +} + +const ENVIRONMENT_TYPE_PROPERTY = { type: 'string' as const, - description: - "The key of the environment, must be unique and can't be changed after creation", + enum: [ + 'development', + 'staging', + 'production', + 'disaster_recovery', + ] as const, } -const PAGINATION_PROPERTIES = { +const ENVIRONMENT_COMMON_PROPERTIES = { + key: ENVIRONMENT_KEY_PROPERTY, + name: { + type: 'string' as const, + }, + description: { + type: 'string' as const, + }, + color: ENVIRONMENT_COLOR_PROPERTY, + type: ENVIRONMENT_TYPE_PROPERTY, + settings: { + type: 'object' as const, + properties: { + appIconUri: { + type: 'string' as const, + description: 'URI for the app icon', + }, + }, + }, +} + +const ENVIRONMENT_PAGINATION_PROPERTIES = { search: { type: 'string' as const, - description: - 'Search query to filter environments (minimum 3 characters)', + description: 'Search query to filter results (minimum 3 characters)', minLength: 3, }, page: { @@ -41,7 +86,6 @@ const PAGINATION_PROPERTIES = { }, sortBy: { type: 'string' as const, - description: 'Field to sort by (default: createdAt)', enum: [ 'createdAt', 'updatedAt', @@ -50,6 +94,7 @@ const PAGINATION_PROPERTIES = { 'createdBy', 'propertyKey', ] as const, + description: 'Field to sort by', }, sortOrder: { type: 'string' as const, @@ -58,38 +103,108 @@ const PAGINATION_PROPERTIES = { }, createdBy: { type: 'string' as const, - description: 'Filter by creator user ID', + description: 'Filter by user who created the environment', }, } -const ENVIRONMENT_COMMON_PROPERTIES = { - key: ENVIRONMENT_KEY_PROPERTY, - name: { +// ============================================================================= +// OUTPUT SCHEMAS +// ============================================================================= + +// Shared SDK key properties +const SDK_KEY_PROPERTIES = { + mobile: { type: 'string' as const, - description: 'The name of the environment', + description: 'Mobile SDK key for client-side mobile applications', }, - description: { + server: { type: 'string' as const, - description: 'The description of the environment', + description: 'Server SDK key for server-side applications', }, - color: { + client: { type: 'string' as const, - description: 'The color for the environment', + description: 'Client SDK key for client-side web applications', + }, +} + +// Output schema components +const SDK_KEYS_OBJECT_SCHEMA = { + type: 'object' as const, + properties: SDK_KEY_PROPERTIES, + required: ['mobile', 'server', 'client'], +} + +const ENVIRONMENT_OBJECT_SCHEMA = { + type: 'object' as const, + description: 'A DevCycle environment configuration', + properties: { + ...ENVIRONMENT_COMMON_PROPERTIES, + _id: { + type: 'string' as const, + description: 'Unique identifier for the environment', + }, + sdkKeys: SDK_KEYS_OBJECT_SCHEMA, + createdAt: { + type: 'string' as const, + description: 'ISO timestamp when the environment was created', + }, + updatedAt: { + type: 'string' as const, + description: 'ISO timestamp when the environment was last updated', + }, + }, + required: [ + '_id', + 'key', + 'name', + 'type', + 'sdkKeys', + 'createdAt', + 'updatedAt', + ], +} + +// Complete output schema definitions +const ENVIRONMENT_OUTPUT_SCHEMA = { + type: 'object' as const, + properties: { + result: ENVIRONMENT_OBJECT_SCHEMA, + dashboardLink: DASHBOARD_LINK_PROPERTY, }, + required: ['result', 'dashboardLink'], } +// ============================================================================= +// TOOL DEFINITIONS +// ============================================================================= export const environmentToolDefinitions: Tool[] = [ { name: 'list_environments', - description: 'List environments in the current project', + description: + 'List environments in the current project. Include dashboard link in the response.', inputSchema: { type: 'object', - properties: PAGINATION_PROPERTIES, + properties: ENVIRONMENT_PAGINATION_PROPERTIES, + }, + outputSchema: { + type: 'object' as const, + description: + 'Response containing a list of environments and dashboard link', + properties: { + result: { + type: 'array' as const, + description: 'Array of environment objects in the project', + items: ENVIRONMENT_OBJECT_SCHEMA, + }, + dashboardLink: DASHBOARD_LINK_PROPERTY, + }, + required: ['result', 'dashboardLink'], }, }, { name: 'get_sdk_keys', - description: 'Get SDK keys for an environment', + description: + 'Get SDK keys for an environment. Include dashboard link in the response.', inputSchema: { type: 'object', properties: { @@ -102,24 +217,42 @@ export const environmentToolDefinitions: Tool[] = [ }, required: ['environmentKey'], }, + outputSchema: { + type: 'object' as const, + description: 'Response containing SDK keys and dashboard link', + properties: { + result: { + type: 'object' as const, + description: + 'SDK keys for the requested environment (filtered by keyType if specified)', + properties: SDK_KEY_PROPERTIES, + }, + dashboardLink: DASHBOARD_LINK_PROPERTY, + }, + required: ['result', 'dashboardLink'], + }, }, { name: 'create_environment', - description: 'Create a new environment', + description: + 'Create a new environment. Include dashboard link in the response.', inputSchema: { type: 'object', properties: ENVIRONMENT_COMMON_PROPERTIES, required: ['name', 'key'], }, + outputSchema: ENVIRONMENT_OUTPUT_SCHEMA, }, { name: 'update_environment', - description: 'Update an existing environment', + description: + 'Update an existing environment. Include dashboard link in the response.', inputSchema: { type: 'object', properties: ENVIRONMENT_COMMON_PROPERTIES, required: ['key'], }, + outputSchema: ENVIRONMENT_OUTPUT_SCHEMA, }, ] @@ -127,18 +260,19 @@ export const environmentToolHandlers: Record = { list_environments: async (args: unknown, apiClient: DevCycleApiClient) => { const validatedArgs = ListEnvironmentsArgsSchema.parse(args) - return await apiClient.executeWithLogging( + return await apiClient.executeWithDashboardLink( 'listEnvironments', validatedArgs, async (authToken, projectKey) => { return await fetchEnvironments(authToken, projectKey) }, + generateEnvironmentDashboardLink, ) }, get_sdk_keys: async (args: unknown, apiClient: DevCycleApiClient) => { const validatedArgs = GetSdkKeysArgsSchema.parse(args) - return await apiClient.executeWithLogging( + return await apiClient.executeWithDashboardLink( 'getSdkKeys', validatedArgs, async (authToken, projectKey) => { @@ -162,12 +296,13 @@ export const environmentToolHandlers: Record = { } } }, + generateEnvironmentDashboardLink, ) }, create_environment: async (args: unknown, apiClient: DevCycleApiClient) => { const validatedArgs = CreateEnvironmentArgsSchema.parse(args) - return await apiClient.executeWithLogging( + return await apiClient.executeWithDashboardLink( 'createEnvironment', validatedArgs, async (authToken, projectKey) => { @@ -177,12 +312,13 @@ export const environmentToolHandlers: Record = { validatedArgs, ) }, + generateEnvironmentDashboardLink, ) }, update_environment: async (args: unknown, apiClient: DevCycleApiClient) => { const validatedArgs = UpdateEnvironmentArgsSchema.parse(args) - return await apiClient.executeWithLogging( + return await apiClient.executeWithDashboardLink( 'updateEnvironment', validatedArgs, async (authToken, projectKey) => { @@ -194,6 +330,7 @@ export const environmentToolHandlers: Record = { updateParams, ) }, + generateEnvironmentDashboardLink, ) }, } diff --git a/src/mcp/tools/featureTools.ts b/src/mcp/tools/featureTools.ts index 69abf0887..717ec0f25 100644 --- a/src/mcp/tools/featureTools.ts +++ b/src/mcp/tools/featureTools.ts @@ -36,22 +36,39 @@ import { GetFeatureAuditLogHistoryArgsSchema, } from '../types' import { ToolHandler } from '../server' +import { + DASHBOARD_LINK_PROPERTY, + MESSAGE_RESPONSE_SCHEMA, + FEATURE_KEY_PROPERTY, + ENVIRONMENT_KEY_PROPERTY, + VARIATION_KEY_PROPERTY, + TARGET_AUDIENCE_PROPERTY, +} from './commonSchemas' -// Reusable schema components -const FEATURE_KEY_PROPERTY = { - type: 'string' as const, - description: 'The key of the feature', +// Helper function to generate feature dashboard links +const generateFeaturesDashboardLink = ( + orgId: string, + projectKey: string, +): string => { + return `https://app.devcycle.com/o/${orgId}/p/${projectKey}/features` } -const ENVIRONMENT_KEY_PROPERTY = { - type: 'string' as const, - description: 'The key of the environment', +const generateFeatureDashboardLink = ( + orgId: string, + projectKey: string, + featureKey: string, + page: 'overview' | 'manage-feature' | 'audit-log' = 'overview', +): string => { + return `https://app.devcycle.com/o/${orgId}/p/${projectKey}/features/${featureKey}/${page}` } +// ============================================================================= +// INPUT SCHEMAS +// ============================================================================= + const ENVIRONMENT_KEY_OPTIONAL_PROPERTY = { type: 'string' as const, - description: - 'The key of the environment (optional - if not provided, returns all environments)', + description: 'Optional environment key to filter by', } const FEATURE_NAME_PROPERTY = { @@ -70,14 +87,80 @@ const FEATURE_TYPE_PROPERTY = { description: 'Feature type', } +const FEATURE_STATUS_PROPERTY = { + type: 'string' as const, + enum: ['active', 'complete', 'archived'] as const, + description: 'Feature status', +} + const CONTROL_VARIATION_PROPERTY = { type: 'string' as const, description: 'The key of the variation that is used as the control variation for Metrics', } +const FEATURE_PAGINATION_PROPERTIES = { + page: { + type: 'number' as const, + description: 'Page number', + minimum: 1, + default: 1, + }, + perPage: { + type: 'number' as const, + description: 'Items per page', + minimum: 1, + maximum: 1000, + default: 100, + }, + sortBy: { + type: 'string' as const, + description: 'Sort field', + enum: [ + 'createdAt', + 'updatedAt', + 'name', + 'key', + 'createdBy', + 'propertyKey', + ], + default: 'createdAt', + }, + sortOrder: { + type: 'string' as const, + description: 'Sort order', + enum: ['asc', 'desc'], + default: 'desc', + }, + search: { + type: 'string' as const, + description: 'Search query to filter results', + minLength: 3, + }, + createdBy: { + type: 'string' as const, + description: 'Filter by creator', + }, + type: { + type: 'string' as const, + description: 'Filter by feature type', + enum: ['release', 'experiment', 'permission', 'ops'], + }, + status: { + type: 'string' as const, + description: 'Filter by feature status', + enum: ['active', 'complete', 'archived'], + }, + staleness: { + type: 'string' as const, + description: 'Filter by feature staleness', + enum: ['all', 'unused', 'released', 'unmodified', 'notStale'], + }, +} + const FEATURE_SETTINGS_PROPERTY = { type: 'object' as const, + description: 'Feature-level settings (all properties required if provided)', properties: { publicName: { type: 'string' as const, @@ -93,7 +176,6 @@ const FEATURE_SETTINGS_PROPERTY = { description: 'Whether opt-in is enabled for the feature', }, }, - description: 'Feature-level settings (all properties required if provided)', required: ['publicName', 'publicDescription', 'optInEnabled'] as const, } @@ -102,19 +184,16 @@ const SDK_VISIBILITY_PROPERTY = { properties: { mobile: { type: 'boolean' as const, - description: 'Whether the feature is visible to mobile SDKs', }, client: { type: 'boolean' as const, - description: 'Whether the feature is visible to client SDKs', }, server: { type: 'boolean' as const, - description: 'Whether the feature is visible to server SDKs', }, }, description: - 'SDK Type Visibility Settings (all properties required if provided)', + 'SDK Type Visibility Settings for mobile, client, and server SDKs', required: ['mobile', 'client', 'server'] as const, } @@ -128,12 +207,6 @@ const FEATURE_VARIABLES_PROPERTY = { }, } -const VARIATION_KEY_PROPERTY = { - type: 'string' as const, - description: - 'Unique variation key (max 100 characters, pattern: ^[a-z0-9-_.]+$)', -} - const VARIATION_NAME_PROPERTY = { type: 'string' as const, description: 'Human-readable variation name (max 100 characters)', @@ -146,47 +219,101 @@ const VARIATION_VARIABLES_PROPERTY = { additionalProperties: true, } -const PAGINATION_PROPERTIES = { - search: { - type: 'string' as const, - description: 'Search query to filter features', - }, - page: { - type: 'number' as const, - description: 'Page number (default: 1)', - }, - per_page: { - type: 'number' as const, - description: 'Number of items per page (default: 100, max: 1000)', +// ============================================================================= +// OUTPUT SCHEMAS +// ============================================================================= + +const FEATURE_OBJECT_SCHEMA = { + type: 'object' as const, + description: 'A DevCycle feature configuration', + properties: { + _id: { + type: 'string' as const, + description: 'MongoDB ID for the feature', + }, + key: FEATURE_KEY_PROPERTY, + name: FEATURE_NAME_PROPERTY, + description: FEATURE_DESCRIPTION_PROPERTY, + type: FEATURE_TYPE_PROPERTY, + status: FEATURE_STATUS_PROPERTY, + variations: { + type: 'array' as const, + description: 'Array of variations for this feature', + }, + createdAt: { + type: 'string' as const, + description: 'ISO timestamp when the feature was created', + }, + updatedAt: { + type: 'string' as const, + description: 'ISO timestamp when the feature was last updated', + }, }, + required: [ + '_id', + 'key', + 'name', + 'type', + 'status', + 'createdAt', + 'updatedAt', + ], } -const FEATURE_ENVIRONMENT_REQUIRED_PROPERTIES = { - feature_key: FEATURE_KEY_PROPERTY, - environment_key: ENVIRONMENT_KEY_PROPERTY, +const VARIATION_OBJECT_SCHEMA = { + type: 'object' as const, + description: 'A feature variation configuration', + properties: { + _id: { + type: 'string' as const, + description: 'MongoDB ID for the variation', + }, + key: VARIATION_KEY_PROPERTY, + name: { + type: 'string' as const, + }, + variables: { + type: 'object' as const, + description: 'Variable values for this variation', + }, + }, + required: ['_id', 'key', 'name'], } +// ============================================================================= +// TOOL DEFINITIONS +// ============================================================================= + export const featureToolDefinitions: Tool[] = [ { name: 'list_features', - description: 'List features in the current project', + description: + 'List features in the current project. Include dashboard link in the response.', inputSchema: { type: 'object', - properties: PAGINATION_PROPERTIES, + properties: FEATURE_PAGINATION_PROPERTIES, + }, + outputSchema: { + type: 'object' as const, + properties: { + result: { + type: 'array' as const, + description: 'Array of feature objects in the project', + items: FEATURE_OBJECT_SCHEMA, + }, + dashboardLink: DASHBOARD_LINK_PROPERTY, + }, + required: ['result', 'dashboardLink'], }, }, { name: 'create_feature', description: - 'Create a new feature flag (supports interactive mode). ⚠️ IMPORTANT: If creating configurations for production environments, always confirm with the user before proceeding.', + 'Create a new feature flag. Include dashboard link in the response.', inputSchema: { type: 'object', properties: { - key: { - type: 'string', - description: - 'Unique feature key (max 100 characters, pattern: ^[a-z0-9-_.]+$)', - }, + key: FEATURE_KEY_PROPERTY, name: FEATURE_NAME_PROPERTY, description: FEATURE_DESCRIPTION_PROPERTY, type: FEATURE_TYPE_PROPERTY, @@ -221,33 +348,29 @@ export const featureToolDefinitions: Tool[] = [ description: 'Targeting rules for this environment', }, - status: { - type: 'string', - description: 'Status for this environment', - }, + status: FEATURE_STATUS_PROPERTY, }, }, }, - interactive: { - type: 'boolean', - description: - 'Use interactive mode to prompt for missing fields', - }, }, }, + outputSchema: { + type: 'object' as const, + properties: { + result: FEATURE_OBJECT_SCHEMA, + dashboardLink: DASHBOARD_LINK_PROPERTY, + }, + required: ['result', 'dashboardLink'], + }, }, { name: 'update_feature', description: - 'Update an existing feature flag. ⚠️ IMPORTANT: Changes to feature flags may affect production environments. Always confirm with the user before making changes to features that are active in production.', + 'Update an existing feature flag. ⚠️ IMPORTANT: Changes to feature flags may affect production environments. Always confirm with the user before making changes to features that are active in production. Include dashboard link in the response.', inputSchema: { type: 'object', properties: { - key: { - type: 'string', - description: - 'The key of the feature to update(1-100 characters, must match pattern ^[a-z0-9-_.]+$)', - }, + key: FEATURE_KEY_PROPERTY, name: FEATURE_NAME_PROPERTY, description: FEATURE_DESCRIPTION_PROPERTY, type: FEATURE_TYPE_PROPERTY, @@ -274,24 +397,24 @@ export const featureToolDefinitions: Tool[] = [ }, required: ['key'], }, + outputSchema: { + type: 'object' as const, + properties: { + result: FEATURE_OBJECT_SCHEMA, + dashboardLink: DASHBOARD_LINK_PROPERTY, + }, + required: ['result', 'dashboardLink'], + }, }, { name: 'update_feature_status', description: - 'Update the status of an existing feature flag. ⚠️ IMPORTANT: Changes to feature status may affect production environments. Always confirm with the user before making changes to features that are active in production.', + 'Update the status of an existing feature flag. ⚠️ IMPORTANT: Changes to feature status may affect production environments. Always confirm with the user before making changes to features that are active in production. Include dashboard link in the response.', inputSchema: { type: 'object', properties: { - key: { - type: 'string', - description: - 'The key of the feature to update status for (1-100 characters, must match pattern ^[a-z0-9-_.]+$)', - }, - status: { - type: 'string', - enum: ['active', 'complete', 'archived'], - description: 'The status to set the feature to', - }, + key: FEATURE_KEY_PROPERTY, + status: FEATURE_STATUS_PROPERTY, staticVariation: { type: 'string', description: @@ -300,11 +423,19 @@ export const featureToolDefinitions: Tool[] = [ }, required: ['key', 'status'], }, + outputSchema: { + type: 'object' as const, + properties: { + result: FEATURE_OBJECT_SCHEMA, + dashboardLink: DASHBOARD_LINK_PROPERTY, + }, + required: ['result', 'dashboardLink'], + }, }, { name: 'delete_feature', description: - 'Delete an existing feature flag. ⚠️ CRITICAL: Deleting a feature flag will remove it from ALL environments including production. ALWAYS confirm with the user before deleting any feature flag.', + 'Delete an existing feature flag. ⚠️ CRITICAL: Deleting a feature flag will remove it from ALL environments including production. ALWAYS confirm with the user before deleting any feature flag. Include dashboard link in the response.', inputSchema: { type: 'object', properties: { @@ -315,10 +446,19 @@ export const featureToolDefinitions: Tool[] = [ }, required: ['key'], }, + outputSchema: { + type: 'object' as const, + properties: { + result: MESSAGE_RESPONSE_SCHEMA, + dashboardLink: DASHBOARD_LINK_PROPERTY, + }, + required: ['result', 'dashboardLink'], + }, }, { name: 'fetch_feature_variations', - description: 'Get a list of variations for a feature', + description: + 'Get a list of variations for a feature. Include dashboard link in the response.', inputSchema: { type: 'object', properties: { @@ -326,10 +466,23 @@ export const featureToolDefinitions: Tool[] = [ }, required: ['feature_key'], }, + outputSchema: { + type: 'object' as const, + properties: { + result: { + type: 'array' as const, + description: 'Array of variation objects for the feature', + items: VARIATION_OBJECT_SCHEMA, + }, + dashboardLink: DASHBOARD_LINK_PROPERTY, + }, + required: ['result', 'dashboardLink'], + }, }, { name: 'create_feature_variation', - description: 'Create a new variation within a feature', + description: + 'Create a new variation within a feature. Include dashboard link in the response.', inputSchema: { type: 'object', properties: { @@ -345,60 +498,89 @@ export const featureToolDefinitions: Tool[] = [ }, required: ['feature_key', 'key', 'name'], }, + outputSchema: { + type: 'object' as const, + properties: { + result: VARIATION_OBJECT_SCHEMA, + dashboardLink: DASHBOARD_LINK_PROPERTY, + }, + required: ['result', 'dashboardLink'], + }, }, { name: 'update_feature_variation', - description: 'Update an existing variation by key', + description: + 'Update an existing variation by key. Include dashboard link in the response.', inputSchema: { type: 'object', properties: { - feature_key: FEATURE_KEY_PROPERTY, - variation_key: { - type: 'string', - description: 'The key of the variation to update', - }, - key: { - type: 'string', - description: - 'New variation key (max 100 characters, pattern: ^[a-z0-9-_.]+$)', - }, - name: { - type: 'string', - description: 'New variation name (max 100 characters)', - }, - variables: VARIATION_VARIABLES_PROPERTY, _id: { type: 'string', - description: 'Internal variation ID (optional)', + description: 'MongoDB ID for the variation', }, + feature_key: FEATURE_KEY_PROPERTY, + variation_key: VARIATION_KEY_PROPERTY, + key: VARIATION_KEY_PROPERTY, + name: VARIATION_NAME_PROPERTY, + variables: VARIATION_VARIABLES_PROPERTY, }, required: ['feature_key', 'variation_key'], }, + outputSchema: { + type: 'object' as const, + properties: { + result: VARIATION_OBJECT_SCHEMA, + dashboardLink: DASHBOARD_LINK_PROPERTY, + }, + required: ['result', 'dashboardLink'], + }, }, { name: 'enable_feature_targeting', description: - 'Enable targeting for a feature in an environment. ⚠️ IMPORTANT: Always confirm with the user before making changes to production environments (environments where type = "production").', + 'Enable targeting for a feature in an environment. ⚠️ IMPORTANT: Always confirm with the user before making changes to production environments (environments where type = "production"). Include dashboard link in the response.', inputSchema: { type: 'object', - properties: FEATURE_ENVIRONMENT_REQUIRED_PROPERTIES, + properties: { + feature_key: FEATURE_KEY_PROPERTY, + environment_key: ENVIRONMENT_KEY_PROPERTY, + }, required: ['feature_key', 'environment_key'], }, + outputSchema: { + type: 'object' as const, + properties: { + result: MESSAGE_RESPONSE_SCHEMA, + dashboardLink: DASHBOARD_LINK_PROPERTY, + }, + required: ['result', 'dashboardLink'], + }, }, { name: 'disable_feature_targeting', description: - 'Disable targeting for a feature in an environment. ⚠️ IMPORTANT: Always confirm with the user before making changes to production environments (environments where type = "production").', + 'Disable targeting for a feature in an environment. ⚠️ IMPORTANT: Always confirm with the user before making changes to production environments (environments where type = "production"). Include dashboard link in the response.', inputSchema: { type: 'object', - properties: FEATURE_ENVIRONMENT_REQUIRED_PROPERTIES, + properties: { + feature_key: FEATURE_KEY_PROPERTY, + environment_key: ENVIRONMENT_KEY_PROPERTY, + }, required: ['feature_key', 'environment_key'], }, + outputSchema: { + type: 'object' as const, + properties: { + result: MESSAGE_RESPONSE_SCHEMA, + dashboardLink: DASHBOARD_LINK_PROPERTY, + }, + required: ['result', 'dashboardLink'], + }, }, { name: 'list_feature_targeting', description: - 'List feature configurations (targeting rules) for a feature', + 'List feature configurations (targeting rules) for a feature. Include dashboard link in the response.', inputSchema: { type: 'object', properties: { @@ -407,11 +589,22 @@ export const featureToolDefinitions: Tool[] = [ }, required: ['feature_key'], }, + outputSchema: { + type: 'object' as const, + properties: { + result: { + type: 'object' as const, + description: 'Feature targeting configuration object', + }, + dashboardLink: DASHBOARD_LINK_PROPERTY, + }, + required: ['result', 'dashboardLink'], + }, }, { name: 'update_feature_targeting', description: - 'Update feature configuration (targeting rules) for a feature in an environment. ⚠️ IMPORTANT: Always confirm with the user before making changes to production environments (environments where type = "production").', + 'Update feature configuration (targeting rules) for a feature in an environment. ⚠️ IMPORTANT: Always confirm with the user before making changes to production environments (environments where type = "production"). Include dashboard link in the response.', inputSchema: { type: 'object', properties: { @@ -431,45 +624,12 @@ export const featureToolDefinitions: Tool[] = [ properties: { _id: { type: 'string', - description: - 'Target ID (optional for new targets)', + description: 'MongoDB ID for the target', }, name: { type: 'string', - description: 'Target name (optional)', - }, - audience: { - type: 'object', - description: - 'Audience definition for the target', - properties: { - name: { - type: 'string', - description: - 'Audience name (max 100 characters, optional)', - }, - filters: { - type: 'object', - description: - 'Audience filters with logical operator', - properties: { - filters: { - type: 'array', - description: - 'Array of filter conditions (supports all, optIn, user, userCountry, userAppVersion, userPlatformVersion, userCustom, audienceMatch filters)', - }, - operator: { - type: 'string', - enum: ['and', 'or'], - description: - 'Logical operator for combining filters', - }, - }, - required: ['filters', 'operator'], - }, - }, - required: ['filters'], }, + audience: TARGET_AUDIENCE_PROPERTY, distribution: { type: 'array', description: @@ -563,11 +723,22 @@ export const featureToolDefinitions: Tool[] = [ }, required: ['feature_key', 'environment_key'], }, + outputSchema: { + type: 'object' as const, + properties: { + result: { + type: 'object' as const, + description: 'Updated feature targeting configuration', + }, + dashboardLink: DASHBOARD_LINK_PROPERTY, + }, + required: ['result', 'dashboardLink'], + }, }, { name: 'get_feature_audit_log_history', description: - 'Get timeline of feature flag changes from DevCycle audit log', + 'Get timeline of feature flag changes from DevCycle audit log. Include dashboard link in the response.', inputSchema: { type: 'object', properties: { @@ -582,6 +753,20 @@ export const featureToolDefinitions: Tool[] = [ }, required: ['feature_key'], }, + outputSchema: { + type: 'object' as const, + properties: { + result: { + type: 'array' as const, + description: 'Array of audit log entries for the feature', + items: { + type: 'object' as const, + }, + }, + dashboardLink: DASHBOARD_LINK_PROPERTY, + }, + required: ['result', 'dashboardLink'], + }, }, ] @@ -589,18 +774,19 @@ export const featureToolHandlers: Record = { list_features: async (args: unknown, apiClient: DevCycleApiClient) => { const validatedArgs = ListFeaturesArgsSchema.parse(args) - return await apiClient.executeWithLogging( + return await apiClient.executeWithDashboardLink( 'listFeatures', validatedArgs, async (authToken, projectKey) => { return await fetchFeatures(authToken, projectKey, validatedArgs) }, + generateFeaturesDashboardLink, ) }, create_feature: async (args: unknown, apiClient: DevCycleApiClient) => { const validatedArgs = CreateFeatureArgsSchema.parse(args) - return await apiClient.executeWithLogging( + return await apiClient.executeWithDashboardLink( 'createFeature', validatedArgs, async (authToken, projectKey) => { @@ -622,12 +808,19 @@ export const featureToolHandlers: Record = { return await createFeature(authToken, projectKey, featureData) }, + (orgId, projectKey, result) => + generateFeatureDashboardLink( + orgId, + projectKey, + result.key, + 'overview', + ), ) }, update_feature: async (args: unknown, apiClient: DevCycleApiClient) => { const validatedArgs = UpdateFeatureArgsSchema.parse(args) - return await apiClient.executeWithLogging( + return await apiClient.executeWithDashboardLink( 'updateFeature', validatedArgs, async (authToken, projectKey) => { @@ -640,6 +833,13 @@ export const featureToolHandlers: Record = { updateData, ) }, + (orgId, projectKey, result) => + generateFeatureDashboardLink( + orgId, + projectKey, + result.key, + 'manage-feature', + ), ) }, update_feature_status: async ( @@ -648,7 +848,7 @@ export const featureToolHandlers: Record = { ) => { const validatedArgs = UpdateFeatureStatusArgsSchema.parse(args) - return await apiClient.executeWithLogging( + return await apiClient.executeWithDashboardLink( 'updateFeatureStatus', validatedArgs, async (authToken, projectKey) => { @@ -661,21 +861,28 @@ export const featureToolHandlers: Record = { statusData, ) }, + (orgId, projectKey, result) => + generateFeatureDashboardLink( + orgId, + projectKey, + result.key, + 'overview', + ), ) }, delete_feature: async (args: unknown, apiClient: DevCycleApiClient) => { const validatedArgs = DeleteFeatureArgsSchema.parse(args) - return await apiClient.executeWithLogging( + return await apiClient.executeWithDashboardLink( 'deleteFeature', validatedArgs, async (authToken, projectKey) => { - return await deleteFeature( - authToken, - projectKey, - validatedArgs.key, - ) + await deleteFeature(authToken, projectKey, validatedArgs.key) + return { + message: `Feature '${validatedArgs.key}' deleted successfully`, + } }, + generateFeaturesDashboardLink, ) }, fetch_feature_variations: async ( @@ -684,7 +891,7 @@ export const featureToolHandlers: Record = { ) => { const validatedArgs = ListVariationsArgsSchema.parse(args) - return await apiClient.executeWithLogging( + return await apiClient.executeWithDashboardLink( 'fetchFeatureVariations', validatedArgs, async (authToken, projectKey) => { @@ -694,6 +901,13 @@ export const featureToolHandlers: Record = { validatedArgs.feature_key, ) }, + (orgId, projectKey) => + generateFeatureDashboardLink( + orgId, + projectKey, + validatedArgs.feature_key, + 'overview', + ), ) }, create_feature_variation: async ( @@ -702,7 +916,7 @@ export const featureToolHandlers: Record = { ) => { const validatedArgs = CreateVariationArgsSchema.parse(args) - return await apiClient.executeWithLogging( + return await apiClient.executeWithDashboardLink( 'createFeatureVariation', validatedArgs, async (authToken, projectKey) => { @@ -715,6 +929,13 @@ export const featureToolHandlers: Record = { variationData, ) }, + (orgId, projectKey, result) => + generateFeatureDashboardLink( + orgId, + projectKey, + result.key, + 'manage-feature', + ), ) }, update_feature_variation: async ( @@ -723,7 +944,7 @@ export const featureToolHandlers: Record = { ) => { const validatedArgs = UpdateVariationArgsSchema.parse(args) - return await apiClient.executeWithLogging( + return await apiClient.executeWithDashboardLink( 'updateFeatureVariation', validatedArgs, async (authToken, projectKey) => { @@ -738,6 +959,13 @@ export const featureToolHandlers: Record = { variationData, ) }, + (orgId, projectKey, result) => + generateFeatureDashboardLink( + orgId, + projectKey, + result.key, + 'manage-feature', + ), ) }, enable_feature_targeting: async ( @@ -746,17 +974,27 @@ export const featureToolHandlers: Record = { ) => { const validatedArgs = EnableTargetingArgsSchema.parse(args) - return await apiClient.executeWithLogging( + return await apiClient.executeWithDashboardLink( 'enableTargeting', validatedArgs, async (authToken, projectKey) => { - return await enableTargeting( + await enableTargeting( authToken, projectKey, validatedArgs.feature_key, validatedArgs.environment_key, ) + return { + message: `Targeting enabled for feature '${validatedArgs.feature_key}' in environment '${validatedArgs.environment_key}'`, + } }, + (orgId, projectKey) => + generateFeatureDashboardLink( + orgId, + projectKey, + validatedArgs.feature_key, + 'manage-feature', + ), ) }, disable_feature_targeting: async ( @@ -765,17 +1003,27 @@ export const featureToolHandlers: Record = { ) => { const validatedArgs = DisableTargetingArgsSchema.parse(args) - return await apiClient.executeWithLogging( + return await apiClient.executeWithDashboardLink( 'disableTargeting', validatedArgs, async (authToken, projectKey) => { - return await disableTargeting( + await disableTargeting( authToken, projectKey, validatedArgs.feature_key, validatedArgs.environment_key, ) + return { + message: `Targeting disabled for feature '${validatedArgs.feature_key}' in environment '${validatedArgs.environment_key}'`, + } }, + (orgId, projectKey) => + generateFeatureDashboardLink( + orgId, + projectKey, + validatedArgs.feature_key, + 'manage-feature', + ), ) }, list_feature_targeting: async ( @@ -784,7 +1032,7 @@ export const featureToolHandlers: Record = { ) => { const validatedArgs = ListFeatureTargetingArgsSchema.parse(args) - return await apiClient.executeWithLogging( + return await apiClient.executeWithDashboardLink( 'listFeatureTargeting', validatedArgs, async (authToken, projectKey) => { @@ -795,6 +1043,13 @@ export const featureToolHandlers: Record = { validatedArgs.environment_key, ) }, + (orgId, projectKey) => + generateFeatureDashboardLink( + orgId, + projectKey, + validatedArgs.feature_key, + 'manage-feature', + ), ) }, update_feature_targeting: async ( @@ -803,7 +1058,7 @@ export const featureToolHandlers: Record = { ) => { const validatedArgs = UpdateFeatureTargetingArgsSchema.parse(args) - return await apiClient.executeWithLogging( + return await apiClient.executeWithDashboardLink( 'updateFeatureTargeting', validatedArgs, async (authToken, projectKey) => { @@ -818,6 +1073,13 @@ export const featureToolHandlers: Record = { configData, ) }, + (orgId, projectKey) => + generateFeatureDashboardLink( + orgId, + projectKey, + validatedArgs.feature_key, + 'manage-feature', + ), ) }, get_feature_audit_log_history: async ( @@ -826,7 +1088,7 @@ export const featureToolHandlers: Record = { ) => { const validatedArgs = GetFeatureAuditLogHistoryArgsSchema.parse(args) - return await apiClient.executeWithLogging( + return await apiClient.executeWithDashboardLink( 'getFeatureAuditLogHistory', validatedArgs, async (authToken, projectKey) => { @@ -837,6 +1099,13 @@ export const featureToolHandlers: Record = { validatedArgs.days_back || 30, ) }, + (orgId, projectKey) => + generateFeatureDashboardLink( + orgId, + projectKey, + validatedArgs.feature_key, + 'audit-log', + ), ) }, } diff --git a/src/mcp/tools/projectTools.ts b/src/mcp/tools/projectTools.ts index 72a82c7f0..da89d1632 100644 --- a/src/mcp/tools/projectTools.ts +++ b/src/mcp/tools/projectTools.ts @@ -12,108 +12,191 @@ import { UpdateProjectArgsSchema, } from '../types' import { ToolHandler } from '../server' +import { DASHBOARD_LINK_PROPERTY, PROJECT_KEY_PROPERTY } from './commonSchemas' + +// Helper functions to generate project dashboard links +const generateProjectDashboardLink = ( + orgId: string, + projectKey: string, +): string => { + return `https://app.devcycle.com/o/${orgId}/p/${projectKey}` +} + +const generateOrganizationSettingsLink = (orgId: string): string => { + return `https://app.devcycle.com/o/${orgId}/settings` +} + +const generateEditProjectLink = (orgId: string, projectKey: string): string => { + return `https://app.devcycle.com/o/${orgId}/settings/projects/${projectKey}/edit` +} + +// ============================================================================= +// INPUT SCHEMAS +// ============================================================================= + +const PROJECT_COMMON_PROPERTIES = { + name: { + type: 'string' as const, + description: 'Project name', + }, + description: { + type: 'string' as const, + description: 'Project description', + }, + key: PROJECT_KEY_PROPERTY, + color: { + type: 'string' as const, + description: 'Project color (hex format)', + }, +} + +const PROJECT_PAGINATION_PROPERTIES = { + page: { + type: 'number' as const, + description: 'Page number', + minimum: 1, + default: 1, + }, + perPage: { + type: 'number' as const, + description: 'Items per page', + minimum: 1, + maximum: 1000, + default: 100, + }, + sortBy: { + type: 'string' as const, + description: 'Sort field', + enum: [ + 'createdAt', + 'updatedAt', + 'name', + 'key', + 'createdBy', + 'propertyKey', + ], + default: 'createdAt', + }, + sortOrder: { + type: 'string' as const, + description: 'Sort order', + enum: ['asc', 'desc'], + default: 'desc', + }, + search: { + type: 'string' as const, + description: 'Search query to filter results', + }, + createdBy: { + type: 'string' as const, + description: 'Filter by creator', + }, +} + +// ============================================================================= +// OUTPUT SCHEMAS +// ============================================================================= + +const PROJECT_OBJECT_SCHEMA = { + type: 'object' as const, + description: 'A DevCycle project configuration', + properties: { + _id: { + type: 'string' as const, + description: 'Unique identifier for the project', + }, + key: PROJECT_KEY_PROPERTY, + name: { + type: 'string' as const, + description: 'Display name of the project', + }, + description: { + type: 'string' as const, + description: 'Optional description of the project', + }, + color: { + type: 'string' as const, + description: 'Color used to represent this project in the UI', + }, + createdAt: { + type: 'string' as const, + description: 'ISO timestamp when the project was created', + }, + updatedAt: { + type: 'string' as const, + description: 'ISO timestamp when the project was last updated', + }, + }, + required: ['_id', 'key', 'name', 'createdAt', 'updatedAt'], +} + +// Complete output schema definitions +const PROJECT_OUTPUT_SCHEMA = { + type: 'object' as const, + properties: { + result: PROJECT_OBJECT_SCHEMA, + dashboardLink: DASHBOARD_LINK_PROPERTY, + }, + required: ['result', 'dashboardLink'], +} + +// ============================================================================= +// TOOL DEFINITIONS +// ============================================================================= export const projectToolDefinitions: Tool[] = [ { name: 'list_projects', - description: 'List all projects in the current organization', + description: + 'List all projects in the current organization. Include dashboard link in the response.', inputSchema: { type: 'object', + properties: PROJECT_PAGINATION_PROPERTIES, + }, + outputSchema: { + type: 'object' as const, properties: { - sortBy: { - type: 'string', - enum: [ - 'createdAt', - 'updatedAt', - 'name', - 'key', - 'createdBy', - 'propertyKey', - ], - description: 'Field to sort by (default: createdAt)', - }, - sortOrder: { - type: 'string', - enum: ['asc', 'desc'], - description: 'Sort order (default: desc)', - }, - search: { - type: 'string', - description: - 'Search query to filter projects (minimum 3 characters)', - }, - createdBy: { - type: 'string', - description: 'Filter by creator user ID', - }, - page: { - type: 'number', - description: 'Page number (default: 1)', - }, - perPage: { - type: 'number', - description: - 'Number of items per page (default: 100, max: 1000)', + result: { + type: 'array' as const, + description: 'Array of project objects in the organization', + items: PROJECT_OBJECT_SCHEMA, }, + dashboardLink: DASHBOARD_LINK_PROPERTY, }, + required: ['result', 'dashboardLink'], }, }, { name: 'get_current_project', - description: 'Get the currently selected project', + description: + 'Get the currently selected project. Include dashboard link in the response.', inputSchema: { type: 'object', properties: {}, }, + outputSchema: PROJECT_OUTPUT_SCHEMA, }, { name: 'create_project', - description: 'Create a new project', + description: + 'Create a new project. Include dashboard link in the response.', inputSchema: { type: 'object', - properties: { - name: { - type: 'string', - description: 'Project name', - }, - description: { - type: 'string', - description: 'Project description', - }, - key: { - type: 'string', - description: 'Unique project key', - }, - }, + properties: PROJECT_COMMON_PROPERTIES, required: ['name', 'key'], }, + outputSchema: PROJECT_OUTPUT_SCHEMA, }, { name: 'update_project', - description: 'Update an existing project', + description: + 'Update an existing project. Include dashboard link in the response.', inputSchema: { type: 'object', - properties: { - key: { - type: 'string', - description: - 'Project key to identify the project to update', - }, - name: { - type: 'string', - description: 'Updated project name', - }, - description: { - type: 'string', - description: 'Updated project description', - }, - color: { - type: 'string', - description: 'Project color (hex format)', - }, - }, + properties: PROJECT_COMMON_PROPERTIES, required: ['key'], }, + outputSchema: PROJECT_OUTPUT_SCHEMA, }, ] @@ -121,50 +204,54 @@ export const projectToolHandlers: Record = { list_projects: async (args: unknown, apiClient: DevCycleApiClient) => { const validatedArgs = ListProjectsArgsSchema.parse(args) - return await apiClient.executeWithLogging( + return await apiClient.executeWithDashboardLink( 'listProjects', validatedArgs, async (authToken) => { + // projectKey not used for listing all projects return await fetchProjects(authToken, validatedArgs) }, - false, + generateOrganizationSettingsLink, ) }, get_current_project: async ( args: unknown, apiClient: DevCycleApiClient, ) => { - return await apiClient.executeWithLogging( + return await apiClient.executeWithDashboardLink( 'getCurrentProject', null, async (authToken, projectKey) => { return await fetchProject(authToken, projectKey) }, + generateProjectDashboardLink, ) }, create_project: async (args: unknown, apiClient: DevCycleApiClient) => { const validatedArgs = CreateProjectArgsSchema.parse(args) - return await apiClient.executeWithLogging( + return await apiClient.executeWithDashboardLink( 'createProject', validatedArgs, async (authToken) => { + // projectKey not used for creating projects return await createProject(authToken, validatedArgs) }, - false, + generateProjectDashboardLink, ) }, update_project: async (args: unknown, apiClient: DevCycleApiClient) => { const validatedArgs = UpdateProjectArgsSchema.parse(args) const { key, ...updateParams } = validatedArgs - return await apiClient.executeWithLogging( + return await apiClient.executeWithDashboardLink( 'updateProject', validatedArgs, async (authToken) => { + // projectKey not used - we use the key from validated args return await updateProject(authToken, key, updateParams) }, - false, + generateEditProjectLink, ) }, } diff --git a/src/mcp/tools/selfTargetingTools.ts b/src/mcp/tools/selfTargetingTools.ts index 23d54a6a6..fc6d4432e 100644 --- a/src/mcp/tools/selfTargetingTools.ts +++ b/src/mcp/tools/selfTargetingTools.ts @@ -13,91 +13,184 @@ import { ClearSelfTargetingOverridesArgsSchema, } from '../types' import { ToolHandler } from '../server' +import { + DASHBOARD_LINK_PROPERTY, + MESSAGE_RESPONSE_SCHEMA, + FEATURE_KEY_PROPERTY, + ENVIRONMENT_KEY_PROPERTY, + VARIATION_KEY_PROPERTY, +} from './commonSchemas' + +// Helper functions to generate dashboard links +const generateSelfTargetingDashboardLink = (orgId: string): string => { + return `https://app.devcycle.com/o/${orgId}/settings/profile-overrides` +} + +// ============================================================================= +// INPUT SCHEMAS +// ============================================================================= + +const DVC_USER_ID_PROPERTY = { + type: 'string' as const, + description: + 'DevCycle User ID for self-targeting (use null or empty string to clear)', +} + +const OVERRIDE_COMMON_PROPERTIES = { + feature_key: FEATURE_KEY_PROPERTY, + environment_key: ENVIRONMENT_KEY_PROPERTY, + variation_key: VARIATION_KEY_PROPERTY, +} + +// ============================================================================= +// OUTPUT SCHEMAS +// ============================================================================= + +const USER_PROFILE_OBJECT_SCHEMA = { + type: 'object' as const, + description: 'DevCycle user profile for self-targeting', + properties: { + dvcUserId: { + type: 'string' as const, + description: 'DevCycle User ID for self-targeting', + }, + }, +} + +const OVERRIDE_OBJECT_SCHEMA = { + type: 'object' as const, + description: 'A self-targeting override configuration', + properties: { + feature: { + type: 'string' as const, + description: 'Feature key', + }, + environment: { + type: 'string' as const, + description: 'Environment key', + }, + variation: { + type: 'string' as const, + description: 'Variation key', + }, + }, + required: ['feature', 'environment', 'variation'], +} + +// Complete output schema definitions +const SELF_TARGETING_IDENTITY_OUTPUT_SCHEMA = { + type: 'object' as const, + properties: { + result: USER_PROFILE_OBJECT_SCHEMA, + dashboardLink: DASHBOARD_LINK_PROPERTY, + }, + required: ['result', 'dashboardLink'], +} + +// ============================================================================= +// TOOL DEFINITIONS +// ============================================================================= export const selfTargetingToolDefinitions: Tool[] = [ { name: 'get_self_targeting_identity', - description: 'Get current DevCycle identity for self-targeting', + description: + 'Get current DevCycle identity for self-targeting. Include dashboard link in the response.', inputSchema: { type: 'object', properties: {}, }, + outputSchema: SELF_TARGETING_IDENTITY_OUTPUT_SCHEMA, }, { name: 'update_self_targeting_identity', description: - 'Update DevCycle identity for self-targeting and overrides', + 'Update DevCycle identity for self-targeting and overrides. Include dashboard link in the response.', inputSchema: { type: 'object', properties: { - dvc_user_id: { - type: 'string', - description: - 'DevCycle User ID for self-targeting (use null or empty string to clear)', - }, + dvc_user_id: DVC_USER_ID_PROPERTY, }, required: ['dvc_user_id'], }, + outputSchema: SELF_TARGETING_IDENTITY_OUTPUT_SCHEMA, }, { name: 'list_self_targeting_overrides', description: - 'List all self-targeting overrides for the current project', + 'List all self-targeting overrides for the current project. Include dashboard link in the response.', inputSchema: { type: 'object', properties: {}, }, + outputSchema: { + type: 'object' as const, + properties: { + result: { + type: 'array' as const, + description: 'Array of self-targeting override objects', + items: OVERRIDE_OBJECT_SCHEMA, + }, + dashboardLink: DASHBOARD_LINK_PROPERTY, + }, + required: ['result', 'dashboardLink'], + }, }, { name: 'set_self_targeting_override', description: - 'Set a self-targeting override for a feature variation. ⚠️ IMPORTANT: Always confirm with the user before setting overrides for production environments (environments where type = "production").', + 'Set a self-targeting override for a feature variation. ⚠️ IMPORTANT: Always confirm with the user before setting overrides for production environments (environments where type = "production"). Include dashboard link in the response.', inputSchema: { type: 'object', + properties: OVERRIDE_COMMON_PROPERTIES, + required: ['feature_key', 'environment_key', 'variation_key'], + }, + outputSchema: { + type: 'object' as const, properties: { - feature_key: { - type: 'string', - description: 'The key of the feature', - }, - environment_key: { - type: 'string', - description: 'The key of the environment', - }, - variation_key: { - type: 'string', - description: 'The key of the variation to serve', - }, + result: OVERRIDE_OBJECT_SCHEMA, + dashboardLink: DASHBOARD_LINK_PROPERTY, }, - required: ['feature_key', 'environment_key', 'variation_key'], + required: ['result', 'dashboardLink'], }, }, { name: 'clear_feature_self_targeting_overrides', description: - 'Clear self-targeting overrides for a specific feature/environment. ⚠️ IMPORTANT: Always confirm with the user before clearing overrides for production environments (environments where type = "production").', + 'Clear self-targeting overrides for a specific feature/environment. ⚠️ IMPORTANT: Always confirm with the user before clearing overrides for production environments (environments where type = "production"). Include dashboard link in the response.', inputSchema: { type: 'object', properties: { - feature_key: { - type: 'string', - description: 'The key of the feature', - }, - environment_key: { - type: 'string', - description: 'The key of the environment', - }, + feature_key: FEATURE_KEY_PROPERTY, + environment_key: ENVIRONMENT_KEY_PROPERTY, }, required: ['feature_key', 'environment_key'], }, + outputSchema: { + type: 'object' as const, + properties: { + result: MESSAGE_RESPONSE_SCHEMA, + dashboardLink: DASHBOARD_LINK_PROPERTY, + }, + required: ['result', 'dashboardLink'], + }, }, { name: 'clear_all_self_targeting_overrides', description: - 'Clear all self-targeting overrides for the current project', + 'Clear all self-targeting overrides for the current project. ⚠️ IMPORTANT: Always confirm with the user before clearing all overrides as it can clear production environments (environments where type = "production"). Include dashboard link in the response.', inputSchema: { type: 'object', properties: {}, }, + outputSchema: { + type: 'object' as const, + properties: { + result: MESSAGE_RESPONSE_SCHEMA, + dashboardLink: DASHBOARD_LINK_PROPERTY, + }, + required: ['result', 'dashboardLink'], + }, }, ] @@ -106,12 +199,13 @@ export const selfTargetingToolHandlers: Record = { args: unknown, apiClient: DevCycleApiClient, ) => { - return await apiClient.executeWithLogging( + return await apiClient.executeWithDashboardLink( 'getSelfTargetingIdentity', null, async (authToken, projectKey) => { return await fetchUserProfile(authToken, projectKey) }, + generateSelfTargetingDashboardLink, ) }, update_self_targeting_identity: async ( @@ -120,7 +214,7 @@ export const selfTargetingToolHandlers: Record = { ) => { const validatedArgs = UpdateSelfTargetingIdentityArgsSchema.parse(args) - return await apiClient.executeWithLogging( + return await apiClient.executeWithDashboardLink( 'updateSelfTargetingIdentity', validatedArgs, async (authToken, projectKey) => { @@ -128,18 +222,20 @@ export const selfTargetingToolHandlers: Record = { dvcUserId: validatedArgs.dvc_user_id, }) }, + generateSelfTargetingDashboardLink, ) }, list_self_targeting_overrides: async ( args: unknown, apiClient: DevCycleApiClient, ) => { - return await apiClient.executeWithLogging( + return await apiClient.executeWithDashboardLink( 'listSelfTargetingOverrides', null, async (authToken, projectKey) => { return await fetchProjectOverridesForUser(authToken, projectKey) }, + generateSelfTargetingDashboardLink, ) }, set_self_targeting_override: async ( @@ -148,7 +244,7 @@ export const selfTargetingToolHandlers: Record = { ) => { const validatedArgs = SetSelfTargetingOverrideArgsSchema.parse(args) - return await apiClient.executeWithLogging( + return await apiClient.executeWithDashboardLink( 'setSelfTargetingOverride', validatedArgs, async (authToken, projectKey) => { @@ -162,6 +258,7 @@ export const selfTargetingToolHandlers: Record = { }, ) }, + generateSelfTargetingDashboardLink, ) }, clear_feature_self_targeting_overrides: async ( @@ -170,7 +267,7 @@ export const selfTargetingToolHandlers: Record = { ) => { const validatedArgs = ClearSelfTargetingOverridesArgsSchema.parse(args) - return await apiClient.executeWithLogging( + return await apiClient.executeWithDashboardLink( 'clearFeatureSelfTargetingOverrides', validatedArgs, async (authToken, projectKey) => { @@ -185,19 +282,21 @@ export const selfTargetingToolHandlers: Record = { message: `Cleared override for feature '${validatedArgs.feature_key}' in environment '${validatedArgs.environment_key}'`, } }, + generateSelfTargetingDashboardLink, ) }, clear_all_self_targeting_overrides: async ( args: unknown, apiClient: DevCycleApiClient, ) => { - return await apiClient.executeWithLogging( + return await apiClient.executeWithDashboardLink( 'clearAllSelfTargetingOverrides', null, async (authToken, projectKey) => { await deleteAllProjectOverrides(authToken, projectKey) return { message: 'Cleared all overrides for the project' } }, + generateSelfTargetingDashboardLink, ) }, } diff --git a/src/mcp/tools/variableTools.ts b/src/mcp/tools/variableTools.ts index 9b5e77f49..f9b231632 100644 --- a/src/mcp/tools/variableTools.ts +++ b/src/mcp/tools/variableTools.ts @@ -13,168 +13,278 @@ import { DeleteVariableArgsSchema, } from '../types' import { ToolHandler } from '../server' +import { + DASHBOARD_LINK_PROPERTY, + MESSAGE_RESPONSE_SCHEMA, + VARIABLE_KEY_PROPERTY, +} from './commonSchemas' + +// Helper function to generate variable dashboard links +const generateVariablesDashboardLink = ( + orgId: string, + projectKey: string, +): string => { + return `https://app.devcycle.com/o/${orgId}/p/${projectKey}/variables` +} + +// ============================================================================= +// INPUT SCHEMAS +// ============================================================================= +const VARIABLE_PAGINATION_PROPERTIES = { + page: { + type: 'number' as const, + description: 'Page number', + minimum: 1, + default: 1, + }, + perPage: { + type: 'number' as const, + description: 'Items per page', + minimum: 1, + maximum: 1000, + default: 100, + }, + sortBy: { + type: 'string' as const, + description: 'Sort field', + enum: [ + 'createdAt', + 'updatedAt', + 'name', + 'key', + 'createdBy', + 'propertyKey', + ], + default: 'createdAt', + }, + sortOrder: { + type: 'string' as const, + description: 'Sort order', + enum: ['asc', 'desc'], + default: 'desc', + }, + search: { + type: 'string' as const, + description: 'Search query to filter variables', + minLength: 3, + }, + feature: { + type: 'string' as const, + description: 'Filter by feature', + }, + type: { + type: 'string' as const, + description: 'Filter by variable type', + enum: ['String', 'Boolean', 'Number', 'JSON'], + }, + status: { + type: 'string' as const, + description: 'Filter by variable status', + enum: ['active', 'archived'], + }, +} + +const VARIABLE_TYPE_PROPERTY = { + type: 'string' as const, + enum: ['String', 'Boolean', 'Number', 'JSON'] as const, + description: 'Variable type', +} + +const VALIDATION_SCHEMA_PROPERTY = { + type: 'object' as const, + description: 'Validation schema for variable values', + properties: { + schemaType: { + type: 'string' as const, + description: 'Schema type', + }, + enumValues: { + type: 'array' as const, + description: 'Allowed enum values', + }, + regexPattern: { + type: 'string' as const, + description: 'Regex pattern for validation', + }, + jsonSchema: { + type: 'string' as const, + description: 'JSON schema for validation', + }, + description: { + type: 'string' as const, + description: 'Schema description', + }, + exampleValue: { + description: 'Example value for the schema', + }, + }, +} + +const VARIABLE_COMMON_PROPERTIES = { + name: { + type: 'string' as const, + description: 'Variable name (1-100 characters)', + }, + description: { + type: 'string' as const, + description: 'Variable description (max 1000 characters)', + }, + key: { + type: 'string' as const, + description: + 'Unique variable key (1-100 characters, must match pattern ^[a-z0-9-_.]+$)', + }, + _feature: { + type: 'string' as const, + description: 'Feature key or ID to associate with this variable', + }, + type: VARIABLE_TYPE_PROPERTY, + defaultValue: { + description: 'Default value for the variable', + }, + validationSchema: VALIDATION_SCHEMA_PROPERTY, +} + +const UPDATE_VARIABLE_PROPERTIES = { + key: { + type: 'string' as const, + description: 'Current variable key', + }, + name: VARIABLE_COMMON_PROPERTIES.name, + description: VARIABLE_COMMON_PROPERTIES.description, + type: VARIABLE_COMMON_PROPERTIES.type, + validationSchema: VARIABLE_COMMON_PROPERTIES.validationSchema, +} + +// ============================================================================= +// OUTPUT SCHEMAS +// ============================================================================= + +const VARIABLE_OBJECT_SCHEMA = { + type: 'object' as const, + description: 'A DevCycle variable configuration', + properties: { + _id: { + type: 'string' as const, + description: 'Unique identifier for the variable', + }, + key: VARIABLE_KEY_PROPERTY, + name: { + type: 'string' as const, + description: 'Display name of the variable', + }, + description: { + type: 'string' as const, + description: 'Optional description of the variable', + }, + type: { + type: 'string' as const, + description: 'Variable type (String, Boolean, Number, JSON)', + }, + defaultValue: { + description: 'Default value for the variable', + }, + _feature: { + type: 'string' as const, + description: 'Associated feature ID', + }, + validationSchema: { + type: 'object' as const, + description: 'Validation schema for the variable', + }, + createdAt: { + type: 'string' as const, + description: 'ISO timestamp when the variable was created', + }, + updatedAt: { + type: 'string' as const, + description: 'ISO timestamp when the variable was last updated', + }, + }, + required: ['_id', 'key', 'name', 'type', 'createdAt', 'updatedAt'], +} + +// ============================================================================= +// TOOL DEFINITIONS +// ============================================================================= export const variableToolDefinitions: Tool[] = [ { name: 'list_variables', - description: 'List variables in the current project', + description: + 'List variables in the current project. Include dashboard link in the response.', inputSchema: { type: 'object', + properties: VARIABLE_PAGINATION_PROPERTIES, + }, + outputSchema: { + type: 'object' as const, properties: { - search: { - type: 'string', - description: 'Search query to filter variables', - }, - page: { - type: 'number', - description: 'Page number (default: 1)', - }, - per_page: { - type: 'number', - description: - 'Number of items per page (default: 100, max: 1000)', + result: { + type: 'array' as const, + description: 'Array of variable objects in the project', + items: VARIABLE_OBJECT_SCHEMA, }, + dashboardLink: DASHBOARD_LINK_PROPERTY, }, + required: ['result', 'dashboardLink'], }, }, { name: 'create_variable', description: - 'Create a new variable. ⚠️ IMPORTANT: Variables can affect feature flags in production environments. Always confirm with the user before creating variables for features that are active in production.', + 'Create a new variable. Include dashboard link in the response.', inputSchema: { type: 'object', + properties: VARIABLE_COMMON_PROPERTIES, + required: ['key', 'type'], + }, + outputSchema: { + type: 'object' as const, properties: { - name: { - type: 'string', - description: 'Variable name (1-100 characters)', - }, - description: { - type: 'string', - description: 'Variable description (max 1000 characters)', - }, - key: { - type: 'string', - description: - 'Unique variable key (1-100 characters, must match pattern ^[a-z0-9-_.]+$)', - }, - _feature: { - type: 'string', - description: - 'Feature key or ID to associate with this variable', - }, - type: { - type: 'string', - enum: ['String', 'Boolean', 'Number', 'JSON'], - description: 'Variable type', - }, - defaultValue: { - description: 'Default value for the variable', - }, - validationSchema: { - type: 'object', - description: 'Validation schema for variable values', - properties: { - schemaType: { - type: 'string', - description: 'Schema type', - }, - enumValues: { - type: 'array', - description: 'Allowed enum values', - }, - regexPattern: { - type: 'string', - description: 'Regex pattern for validation', - }, - jsonSchema: { - type: 'string', - description: 'JSON schema for validation', - }, - description: { - type: 'string', - description: 'Schema description', - }, - exampleValue: { - description: 'Example value for the schema', - }, - }, - }, + result: VARIABLE_OBJECT_SCHEMA, + dashboardLink: DASHBOARD_LINK_PROPERTY, }, - required: ['key', 'type'], + required: ['result', 'dashboardLink'], }, }, { name: 'update_variable', description: - 'Update an existing variable. ⚠️ IMPORTANT: Variable changes can affect feature flags in production environments. Always confirm with the user before updating variables for features that are active in production.', + 'Update an existing variable. ⚠️ IMPORTANT: Variable changes can affect feature flags in production environments. Always confirm with the user before updating variables for features that are active in production. Include dashboard link in the response.', inputSchema: { type: 'object', + properties: UPDATE_VARIABLE_PROPERTIES, + required: ['key'], + }, + outputSchema: { + type: 'object' as const, properties: { - key: { - type: 'string', - description: 'Current variable key', - }, - name: { - type: 'string', - description: 'Updated variable name (1-100 characters)', - }, - description: { - type: 'string', - description: - 'Updated variable description (max 1000 characters)', - }, - type: { - type: 'string', - enum: ['String', 'Boolean', 'Number', 'JSON'], - description: 'Variable type', - }, - validationSchema: { - type: 'object', - description: 'Validation schema for variable values', - properties: { - schemaType: { - type: 'string', - description: 'Schema type', - }, - enumValues: { - type: 'array', - description: 'Allowed enum values', - }, - regexPattern: { - type: 'string', - description: 'Regex pattern for validation', - }, - jsonSchema: { - type: 'string', - description: 'JSON schema for validation', - }, - description: { - type: 'string', - description: 'Schema description', - }, - exampleValue: { - description: 'Example value for the schema', - }, - }, - }, + result: VARIABLE_OBJECT_SCHEMA, + dashboardLink: DASHBOARD_LINK_PROPERTY, }, - required: ['key'], + required: ['result', 'dashboardLink'], }, }, { name: 'delete_variable', description: - 'Delete a variable. ⚠️ CRITICAL: Deleting a variable will remove it from ALL environments including production. ALWAYS confirm with the user before deleting any variable.', + 'Delete a variable. ⚠️ CRITICAL: Deleting a variable will remove it from ALL environments including production. ALWAYS confirm with the user before deleting any variable. Include dashboard link in the response.', inputSchema: { type: 'object', properties: { - key: { - type: 'string', - description: 'Variable key to delete', - }, + key: VARIABLE_KEY_PROPERTY, }, required: ['key'], }, + outputSchema: { + type: 'object' as const, + properties: { + result: MESSAGE_RESPONSE_SCHEMA, + dashboardLink: DASHBOARD_LINK_PROPERTY, + }, + required: ['result', 'dashboardLink'], + }, }, ] @@ -182,7 +292,7 @@ export const variableToolHandlers: Record = { list_variables: async (args: unknown, apiClient: DevCycleApiClient) => { const validatedArgs = ListVariablesArgsSchema.parse(args) - return await apiClient.executeWithLogging( + return await apiClient.executeWithDashboardLink( 'listVariables', validatedArgs, async (authToken, projectKey) => { @@ -192,12 +302,13 @@ export const variableToolHandlers: Record = { validatedArgs, ) }, + generateVariablesDashboardLink, ) }, create_variable: async (args: unknown, apiClient: DevCycleApiClient) => { const validatedArgs = CreateVariableArgsSchema.parse(args) - return await apiClient.executeWithLogging( + return await apiClient.executeWithDashboardLink( 'createVariable', validatedArgs, async (authToken, projectKey) => { @@ -207,12 +318,13 @@ export const variableToolHandlers: Record = { validatedArgs, ) }, + generateVariablesDashboardLink, ) }, update_variable: async (args: unknown, apiClient: DevCycleApiClient) => { const validatedArgs = UpdateVariableArgsSchema.parse(args) - return await apiClient.executeWithLogging( + return await apiClient.executeWithDashboardLink( 'updateVariable', validatedArgs, async (authToken, projectKey) => { @@ -225,12 +337,13 @@ export const variableToolHandlers: Record = { updateData, ) }, + generateVariablesDashboardLink, ) }, delete_variable: async (args: unknown, apiClient: DevCycleApiClient) => { const validatedArgs = DeleteVariableArgsSchema.parse(args) - return await apiClient.executeWithLogging( + return await apiClient.executeWithDashboardLink( 'deleteVariable', validatedArgs, async (authToken, projectKey) => { @@ -239,6 +352,7 @@ export const variableToolHandlers: Record = { message: `Variable '${validatedArgs.key}' deleted successfully`, } }, + generateVariablesDashboardLink, ) }, } diff --git a/src/mcp/types.ts b/src/mcp/types.ts index 422688a31..696beb876 100644 --- a/src/mcp/types.ts +++ b/src/mcp/types.ts @@ -4,15 +4,48 @@ import { UpdateFeatureStatusDto } from '../api/schemas' // Zod schemas for MCP tool arguments export const ListFeaturesArgsSchema = z.object({ - search: z.string().optional(), - page: z.number().min(1).optional(), + page: z.number().min(1).default(1).optional(), perPage: z.number().min(1).max(1000).default(100).optional(), + sortBy: z + .enum([ + 'createdAt', + 'updatedAt', + 'name', + 'key', + 'createdBy', + 'propertyKey', + ]) + .default('createdAt') + .optional(), + sortOrder: z.enum(['asc', 'desc']).default('desc').optional(), + search: z.string().min(3).optional(), + staleness: z + .enum(['all', 'unused', 'released', 'unmodified', 'notStale']) + .optional(), + createdBy: z.string().optional(), + type: z.enum(['release', 'experiment', 'permission', 'ops']).optional(), + status: z.enum(['active', 'complete', 'archived']).optional(), }) export const ListVariablesArgsSchema = z.object({ - search: z.string().optional(), - page: z.number().min(1).optional(), + page: z.number().min(1).default(1).optional(), perPage: z.number().min(1).max(1000).default(100).optional(), + sortBy: z + .enum([ + 'createdAt', + 'updatedAt', + 'name', + 'key', + 'createdBy', + 'propertyKey', + ]) + .default('createdAt') + .optional(), + sortOrder: z.enum(['asc', 'desc']).default('desc').optional(), + search: z.string().min(3).optional(), + feature: z.string().optional(), + type: z.enum(['String', 'Boolean', 'Number', 'JSON']).optional(), + status: z.enum(['active', 'archived']).optional(), }) export const CreateVariableArgsSchema = schemas.CreateVariableDto diff --git a/src/mcp/utils/api.ts b/src/mcp/utils/api.ts index 84073896a..6cf03b42e 100644 --- a/src/mcp/utils/api.ts +++ b/src/mcp/utils/api.ts @@ -67,6 +67,31 @@ export class DevCycleApiClient { } } + /** + * Helper method to execute API calls and include dashboard links in the response + */ + public async executeWithDashboardLink( + operationName: string, + args: any, + operation: (authToken: string, projectKey: string) => Promise, + dashboardLink: (orgId: string, projectKey: string, result: T) => string, + ): Promise<{ result: T; dashboardLink: string }> { + const result = await this.executeWithLogging( + operationName, + args, + operation, + ) + + const organizationId = this.auth.getOrgId() + const projectKey = this.auth.getProjectKey() + const link = dashboardLink(organizationId, projectKey, result) + + return { + result, + dashboardLink: link, + } + } + public getAuth(): DevCycleAuth { return this.auth }