diff --git a/mcp-worker/README.md b/mcp-worker/README.md index d080e3c5..41595b59 100644 --- a/mcp-worker/README.md +++ b/mcp-worker/README.md @@ -133,3 +133,27 @@ When a user completes OAuth on the hosted MCP Worker, the worker emits a single - **Channel**: `${orgId}-mcp-install` - **Event name**: `mcp-install` + +## MCP Tool Token Counts + +Measure how many AI tokens our MCP tool descriptions and schemas consume. Run the measurement script (from repo root): + +```bash +yarn install && +yarn measure:mcp-tokens +``` + +What it does: + +- Uses `gpt-tokenizer` (OpenAI-style) and `@anthropic-ai/tokenizer` (Anthropic) to count tokens in each tool's `description` and `inputSchema`. + +Current token totals: + +```json +{ + "totals": { + "anthropic": 10428, + "openai": 10746 + } +} +``` diff --git a/mcp-worker/src/projectSelectionTools.ts b/mcp-worker/src/projectSelectionTools.ts index 5c233ca5..cc512872 100644 --- a/mcp-worker/src/projectSelectionTools.ts +++ b/mcp-worker/src/projectSelectionTools.ts @@ -17,7 +17,10 @@ export const SelectProjectArgsSchema = z.object({ .string() .optional() .describe( - 'The project key to select. If not provided, will list all available projects to choose from.', + [ + 'The project key to select.', + 'If not provided, will list all available projects to choose from.', + ].join('\n'), ), }) @@ -111,6 +114,7 @@ export function registerProjectSelectionTools( 'Select a project to use for subsequent MCP operations.', 'Call without parameters to list available projects.', 'Do not automatically select a project, ask the user which project they want to select.', + 'Returns the current project, its environments, and SDK keys.', 'Include dashboard link in the response.', ].join('\n'), annotations: { diff --git a/package.json b/package.json index 9875fbde..0c9ba064 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "deploy:worker:prod": "cd mcp-worker && yarn deploy:prod", "deploy:worker:dev": "cd mcp-worker && yarn deploy:dev", "dev:worker": "cd mcp-worker && yarn dev", + "measure:mcp-tokens": "ts-node scripts/measureMcpTokens.ts", "format": "prettier --write \"src/**/*.{ts,js,json}\" \"test/**/*.{ts,js,json}\" \"test-utils/**/*.{ts,js,json}\" \"*.{ts,js,json,md}\"", "format:check": "prettier --check \"src/**/*.{ts,js,json}\" \"test/**/*.{ts,js,json}\" \"test-utils/**/*.{ts,js,json}\" \"*.{ts,js,json,md}\"", "lint": "eslint . --config eslint.config.mjs", @@ -67,6 +68,7 @@ "zod": "~3.25.76" }, "devDependencies": { + "@anthropic-ai/tokenizer": "^0.0.4", "@babel/code-frame": "^7.27.1", "@babel/core": "^7.28.0", "@babel/generator": "^7.28.0", @@ -85,6 +87,7 @@ "chai": "^5.1.2", "eslint": "^9.18.0", "eslint-config-prettier": "^9.1.0", + "gpt-tokenizer": "^3.0.1", "mocha": "^10.8.2", "mocha-chai-jest-snapshot": "^1.1.6", "nock": "^13.5.6", diff --git a/scripts/measureMcpTokens.ts b/scripts/measureMcpTokens.ts new file mode 100644 index 00000000..c1c31f45 --- /dev/null +++ b/scripts/measureMcpTokens.ts @@ -0,0 +1,135 @@ +#!/usr/bin/env ts-node +import { registerAllToolsWithServer } from '../src/mcp/tools' +import type { DevCycleMCPServerInstance } from '../src/mcp/server' +import type { IDevCycleApiClient } from '../src/mcp/api/interface' + +type Collected = { + name: string + description: string + inputSchema?: unknown + outputSchema?: unknown +} + +const collected: Collected[] = [] + +const mockServer: DevCycleMCPServerInstance = { + registerToolWithErrorHandling(name, config) { + collected.push({ + name, + description: config.description, + inputSchema: config.inputSchema, + outputSchema: config.outputSchema, + }) + }, +} + +// We do not need a real client to collect tool metadata +const fakeClient = {} as unknown as IDevCycleApiClient + +registerAllToolsWithServer(mockServer, fakeClient) + +let openAiEncoderPromise: Promise<(input: string) => number[]> | undefined +async function countOpenAI(text: string): Promise { + try { + if (!openAiEncoderPromise) { + openAiEncoderPromise = import('gpt-tokenizer').then((m) => m.encode) + } + const encode = await openAiEncoderPromise + return encode(text).length + } catch { + return 0 + } +} +let anthropicCounterPromise: Promise<(input: string) => number> | undefined +async function countAnthropic(text: string): Promise { + try { + if (!anthropicCounterPromise) { + anthropicCounterPromise = import('@anthropic-ai/tokenizer').then( + (m) => m.countTokens, + ) + } + const countTokens = await anthropicCounterPromise + return countTokens(text) + } catch { + return 0 + } +} + +type ResultRow = { + name: string + anthropic: { + description: number + inputSchema: number + outputSchema: number + total: number + } + openai: { + description: number + inputSchema: number + outputSchema: number + total: number + } +} + +const rows: ResultRow[] = [] +let grandAnthropic = 0 +let grandOpenAI = 0 + +async function main() { + for (const t of collected) { + const d = t.description ?? '' + const i = t.inputSchema ? JSON.stringify(t.inputSchema) : '' + const o = t.outputSchema ? JSON.stringify(t.outputSchema) : '' + + const [aDesc, aIn, aOut] = await Promise.all([ + countAnthropic(d), + i ? countAnthropic(i) : Promise.resolve(0), + o ? countAnthropic(o) : Promise.resolve(0), + ]) + const aTotal = aDesc + aIn + aOut + + const [oDesc, oIn, oOut] = await Promise.all([ + countOpenAI(d), + i ? countOpenAI(i) : Promise.resolve(0), + o ? countOpenAI(o) : Promise.resolve(0), + ]) + const oTotal = oDesc + oIn + oOut + + grandAnthropic += aTotal + grandOpenAI += oTotal + + rows.push({ + name: t.name, + anthropic: { + description: aDesc, + inputSchema: aIn, + outputSchema: aOut, + total: aTotal, + }, + openai: { + description: oDesc, + inputSchema: oIn, + outputSchema: oOut, + total: oTotal, + }, + }) + } + + rows.sort((a, b) => a.name.localeCompare(b.name)) + + console.log( + JSON.stringify( + { + tools: rows, + totals: { anthropic: grandAnthropic, openai: grandOpenAI }, + }, + null, + 2, + ), + ) +} + +main().catch((err) => { + console.error(err) + process.exit(1) +}) diff --git a/src/api/zodClient.ts b/src/api/zodClient.ts index 0465bbf8..cc101846 100644 --- a/src/api/zodClient.ts +++ b/src/api/zodClient.ts @@ -371,8 +371,8 @@ const UpdateAudienceDto = z }) .partial() const VariableValidationEntity = z.object({ - schemaType: z.string(), - enumValues: z.array(z.string()).optional(), + schemaType: z.enum(['enum', 'regex', 'jsonSchema']), + enumValues: z.union([z.array(z.string()), z.array(z.number())]).optional(), regexPattern: z.string().optional(), jsonSchema: z.string().optional(), description: z.string(), diff --git a/src/mcp/tools/customPropertiesTools.ts b/src/mcp/tools/customPropertiesTools.ts index 0eadaa1e..c49fe094 100644 --- a/src/mcp/tools/customPropertiesTools.ts +++ b/src/mcp/tools/customPropertiesTools.ts @@ -147,6 +147,7 @@ export function registerCustomPropertiesTools( { description: [ 'Create a new custom property.', + 'Custom properties are used in feature targeting audiences as custom user-data definitions.', 'Include dashboard link in the response.', ].join('\n'), annotations: { diff --git a/src/mcp/tools/featureTools.ts b/src/mcp/tools/featureTools.ts index 49f9602b..81a55e17 100644 --- a/src/mcp/tools/featureTools.ts +++ b/src/mcp/tools/featureTools.ts @@ -457,7 +457,9 @@ export function registerFeatureTools( 'create_feature', { description: [ - 'Create a new feature flag. Include dashboard link in the response.', + 'Create a new DevCycle feature. Include dashboard link in the response.', + 'Features are the main logical container for variables and targeting rules, defining what values variables will be served to users across environments.', + 'Features can contin multiple variables, and many variations, defined by the targeting rules to determine how variable values are distributed to users.', 'If a user is creating a feature, you should follow these steps and ask users for input on these steps:', '1. create a variable and associate it with this feature. (default to creating a "boolean" variable with the same key as the feature)', '2. create variations for the feature. (default to creating an "on" and "off" variation)', @@ -499,7 +501,7 @@ export function registerFeatureTools( 'update_feature_status', { description: [ - 'Update the status of an existing feature flag.', + 'Update the status of an existing feature.', '⚠️ 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.', @@ -520,13 +522,13 @@ export function registerFeatureTools( '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.', + '⚠️ CRITICAL: Deleting a feature will remove it from ALL environments including production.', + 'ALWAYS confirm with the user before deleting any feature.', 'Include dashboard link in the response.', ].join('\n'), annotations: { - title: 'Delete Feature Flag', + title: 'Delete Feature', destructiveHint: true, }, inputSchema: DeleteFeatureArgsSchema.shape, @@ -657,7 +659,8 @@ export function registerFeatureTools( 'get_feature_audit_log_history', { description: [ - 'Get feature flag audit log history from DevCycle.', + 'Get feature audit log history from DevCycle.', + 'Returns audit log data for all changes made to a feature / variation / targeting rule ordered by date.', 'Include dashboard link in the response.', ].join('\n'), annotations: { diff --git a/src/mcp/tools/installTools.ts b/src/mcp/tools/installTools.ts index 5bb0b403..e0bf47cb 100644 --- a/src/mcp/tools/installTools.ts +++ b/src/mcp/tools/installTools.ts @@ -46,6 +46,7 @@ export function registerInstallTools( { description: [ 'Fetch DevCycle SDK installation instructions, and follow the instructions to install the DevCycle SDK.', + 'Also includes documentation and examples for using DevCycle SDK in your application.', "Choose the guide that matches the application's language/framework.", ].join('\n'), annotations: { diff --git a/src/mcp/tools/localProjectTools.ts b/src/mcp/tools/localProjectTools.ts index 7cf947ad..789d6e49 100644 --- a/src/mcp/tools/localProjectTools.ts +++ b/src/mcp/tools/localProjectTools.ts @@ -18,7 +18,10 @@ export const SelectProjectArgsSchema = z.object({ .string() .optional() .describe( - 'The project key to select. If not provided, will list all available projects to choose from.', + [ + 'The project key to select.', + 'If not provided, will list all available projects to choose from.', + ].join('\n'), ), }) @@ -113,7 +116,8 @@ export function registerLocalProjectTools( 'Select a project to use for subsequent MCP operations.', 'Call without parameters to list available projects.', 'Do not automatically select a project, ask the user which project they want to select.', - 'This will update your local DevCycle configuration (~/.config/devcycle/user.yml).', + 'This will update your local DevCycle configuration for the MCP and CLI (~/.config/devcycle/user.yml).', + 'Returns the current project, its environments, and SDK keys.', 'Include dashboard link in the response.', ].join('\n'), annotations: { diff --git a/src/mcp/tools/projectTools.ts b/src/mcp/tools/projectTools.ts index 37eabcea..aaa7ed0e 100644 --- a/src/mcp/tools/projectTools.ts +++ b/src/mcp/tools/projectTools.ts @@ -118,7 +118,7 @@ export function registerProjectTools( { description: [ 'List all projects in the current organization.', - 'Include dashboard link in the response.', + 'Can be called before "select_project"', ].join('\n'), annotations: { title: 'List Projects', @@ -139,6 +139,7 @@ export function registerProjectTools( description: [ 'Get the currently selected project.', 'Include dashboard link in the response.', + 'Returns the current project, its environments, and SDK keys.', ].join('\n'), annotations: { title: 'Get Current Project', diff --git a/src/mcp/tools/resultsTools.ts b/src/mcp/tools/resultsTools.ts index e41c40e1..d4606709 100644 --- a/src/mcp/tools/resultsTools.ts +++ b/src/mcp/tools/resultsTools.ts @@ -81,7 +81,8 @@ export function registerResultsTools( 'get_feature_total_evaluations', { description: [ - 'Get total variable evaluations per time period for a specific feature.', + 'Get total variable evaluations per time period for a feature.', + 'Useful for understanding if a feature is being used or not.', 'Include dashboard link in the response.', ].join('\n'), annotations: { @@ -105,6 +106,7 @@ export function registerResultsTools( { description: [ 'Get total variable evaluations per time period for the entire project.', + 'Useful for understanding the overall usage of variables in a project by environment or SDK type.', 'Include dashboard link in the response.', ].join('\n'), annotations: { diff --git a/src/mcp/tools/selfTargetingTools.ts b/src/mcp/tools/selfTargetingTools.ts index 6e21c56e..cf69ed08 100644 --- a/src/mcp/tools/selfTargetingTools.ts +++ b/src/mcp/tools/selfTargetingTools.ts @@ -179,7 +179,8 @@ export function registerSelfTargetingTools( 'get_self_targeting_identity', { description: [ - 'Get current DevCycle identity for self-targeting.', + 'Get current DevCycle identity for self-targeting yourself into a feature.', + 'Your applications user_id used to identify yourself with the DevCycle SDK needs to match for self-targeting to work.', 'Include dashboard link in the response.', ].join('\n'), annotations: { @@ -197,7 +198,8 @@ export function registerSelfTargetingTools( 'update_self_targeting_identity', { description: [ - 'Update DevCycle identity for self-targeting and overrides.', + 'Update DevCycle identity for self-targeting yourself into a feature.', + 'Your applications user_id used to identify yourself with the DevCycle SDK needs to match for self-targeting to work.', 'Include dashboard link in the response.', ].join('\n'), annotations: { diff --git a/src/mcp/tools/variableTools.ts b/src/mcp/tools/variableTools.ts index 4d41d316..0cb6f47d 100644 --- a/src/mcp/tools/variableTools.ts +++ b/src/mcp/tools/variableTools.ts @@ -141,6 +141,7 @@ export function registerVariableTools( { description: [ 'Create a new variable.', + 'DevCycle variables can also be referred to as "feature flags" or "flags".', 'Include dashboard link in the response.', ].join('\n'), annotations: { @@ -159,7 +160,7 @@ export function registerVariableTools( { description: [ 'Update an existing variable.', - '⚠️ IMPORTANT: Variable changes can affect feature flags in production environments.', + '⚠️ IMPORTANT: Variable changes can affect features in production environments.', 'Always confirm with the user before updating variables for features that are active in production.', 'Include dashboard link in the response.', ].join('\n'), diff --git a/src/mcp/types.ts b/src/mcp/types.ts index 34dada29..b2a41d5b 100644 --- a/src/mcp/types.ts +++ b/src/mcp/types.ts @@ -3,6 +3,32 @@ import { schemas } from '../api/zodClient' import { UpdateFeatureStatusDto } from '../api/schemas' // Zod schemas for MCP tool arguments +export const VariableValidationSchema = z.object({ + schemaType: z + .enum(['enum', 'regex', 'jsonSchema']) + .describe('Type of validation to apply'), + enumValues: z + .union([z.array(z.string()), z.array(z.number())]) + .optional() + .describe( + 'Required when schemaType="enum": allowable values as strings or numbers', + ), + regexPattern: z + .string() + .optional() + .describe( + 'Required when schemaType="regex": regular expression pattern to validate against', + ), + jsonSchema: z + .string() + .optional() + .describe( + 'Required when schemaType="jsonSchema": stringified JSON Schema to validate against', + ), + description: z.string(), + exampleValue: z.any().describe('Example value demonstrating a valid input'), +}) + export const ListFeaturesArgsSchema = z.object({ page: z .number() @@ -16,7 +42,7 @@ export const ListFeaturesArgsSchema = z.object({ .max(1000) .default(100) .optional() - .describe('Number of items per page (1-1000)'), + .describe('Number of items per page'), sortBy: z .enum([ 'createdAt', @@ -27,18 +53,13 @@ export const ListFeaturesArgsSchema = z.object({ 'propertyKey', ]) .default('createdAt') - .optional() - .describe('Field to sort features by'), - sortOrder: z - .enum(['asc', 'desc']) - .default('desc') - .optional() - .describe('Sort order (ascending or descending)'), + .optional(), + sortOrder: z.enum(['asc', 'desc']).default('desc').optional(), search: z .string() .min(3) .optional() - .describe('Search term to filter features by name or key'), + .describe('Search term to filter features by "name" or "key"'), staleness: z .enum(['all', 'unused', 'released', 'unmodified', 'notStale']) .optional() @@ -70,7 +91,7 @@ export const ListVariablesArgsSchema = z.object({ .max(1000) .default(100) .optional() - .describe('Number of items per page (1-1000)'), + .describe('Number of items per page'), sortBy: z .enum([ 'createdAt', @@ -81,18 +102,13 @@ export const ListVariablesArgsSchema = z.object({ 'propertyKey', ]) .default('createdAt') - .optional() - .describe('Field to sort variables by'), - sortOrder: z - .enum(['asc', 'desc']) - .default('desc') - .optional() - .describe('Sort order (ascending or descending)'), + .optional(), + sortOrder: z.enum(['asc', 'desc']).default('desc').optional(), search: z .string() .min(3) .optional() - .describe('Search query to filter variables (minimum 3 characters)'), + .describe('Search query to filter variables by "name" or "key"'), feature: z.string().optional().describe('Filter by feature key'), type: z .enum(['String', 'Boolean', 'Number', 'JSON']) @@ -105,27 +121,23 @@ export const ListVariablesArgsSchema = z.object({ }) export const CreateVariableArgsSchema = z.object({ - key: schemas.CreateVariableDto.shape.key.describe( - 'Unique variable key (1-100 characters, lowercase letters, numbers, dots, dashes, underscores only)', - ), - name: schemas.CreateVariableDto.shape.name.describe( - 'Variable name (1-100 characters)', - ), - description: schemas.CreateVariableDto.shape.description - .optional() - .describe('Variable description (max 1000 characters)'), - type: schemas.CreateVariableDto.shape.type.describe( - 'Variable type (String, Boolean, Number, JSON)', - ), + key: schemas.CreateVariableDto.shape.key.describe('Unique variable key'), + name: schemas.CreateVariableDto.shape.name, + description: schemas.CreateVariableDto.shape.description.optional(), + type: schemas.CreateVariableDto.shape.type, defaultValue: schemas.CreateVariableDto.shape.defaultValue .optional() - .describe('Default value for the variable'), + .describe( + 'Default value for the variable, the data type of the defaultValue must match the variable.type', + ), _feature: schemas.CreateVariableDto.shape._feature .optional() - .describe('Feature key or ID to associate with this variable'), - validationSchema: schemas.CreateVariableDto.shape.validationSchema - .optional() - .describe('Validation schema for variable values'), + .describe( + 'Feature key or ID to associate with this variable, only set if variable is associated with a feature', + ), + validationSchema: VariableValidationSchema.optional().describe( + 'Validation schema for variable values', + ), }) export const UpdateVariableArgsSchema = z.object({ @@ -133,29 +145,21 @@ export const UpdateVariableArgsSchema = z.object({ .string() .max(100) .regex(/^[a-z0-9-_.]+$/) - .describe('Current variable key to identify which variable to update'), - name: schemas.UpdateVariableDto.shape.name - .optional() - .describe('Variable name (1-100 characters)'), - description: schemas.UpdateVariableDto.shape.description - .optional() - .describe('Variable description (max 1000 characters)'), - type: schemas.UpdateVariableDto.shape.type - .optional() - .describe('Variable type (String, Boolean, Number, JSON)'), - validationSchema: schemas.UpdateVariableDto.shape.validationSchema - .optional() - .describe('Validation schema for variable values'), + .describe('key to identify variable'), + name: schemas.UpdateVariableDto.shape.name.optional(), + description: schemas.UpdateVariableDto.shape.description.optional(), + type: schemas.UpdateVariableDto.shape.type.optional(), + validationSchema: VariableValidationSchema.optional().describe( + 'Validation schema for variable values', + ), }) export const DeleteVariableArgsSchema = z.object({ - key: z - .string() - .describe('Variable key to identify which variable to delete'), + key: z.string().describe('key to identify variable to delete'), }) export const DeleteFeatureArgsSchema = z.object({ - key: z.string().describe('Feature key to identify which feature to delete'), + key: z.string().describe('key to identify feature to delete'), }) export const ListProjectsArgsSchema = z.object({ @@ -163,16 +167,12 @@ export const ListProjectsArgsSchema = z.object({ 'Page number for pagination', ), perPage: schemas.GetProjectsParams.shape.perPage.describe( - 'Number of items per page (1-1000)', - ), - sortBy: schemas.GetProjectsParams.shape.sortBy.describe( - 'Field to sort projects by', - ), - sortOrder: schemas.GetProjectsParams.shape.sortOrder.describe( - 'Sort order (ascending or descending)', + 'Number of items per page', ), + sortBy: schemas.GetProjectsParams.shape.sortBy, + sortOrder: schemas.GetProjectsParams.shape.sortOrder, search: schemas.GetProjectsParams.shape.search.describe( - 'Search term to filter projects by name or key', + 'Search term to filter projects by "name" or "key"', ), createdBy: schemas.GetProjectsParams.shape.createdBy.describe( 'Filter projects by creator user ID', @@ -180,21 +180,15 @@ export const ListProjectsArgsSchema = z.object({ }) export const CreateProjectArgsSchema = z.object({ - name: schemas.CreateProjectDto.shape.name.describe( - 'Project name (max 100 characters)', - ), + name: schemas.CreateProjectDto.shape.name, key: schemas.CreateProjectDto.shape.key.describe( 'Unique project key (lowercase letters, numbers, dots, dashes, underscores only)', ), - description: schemas.CreateProjectDto.shape.description.describe( - 'Project description (max 1000 characters)', - ), + description: schemas.CreateProjectDto.shape.description, color: schemas.CreateProjectDto.shape.color.describe( 'Project color in hex format (e.g., #FF0000)', ), - settings: schemas.CreateProjectDto.shape.settings.describe( - 'Project settings configuration', - ), + settings: schemas.CreateProjectDto.shape.settings, }) export const UpdateProjectArgsSchema = z.object({ @@ -202,19 +196,13 @@ export const UpdateProjectArgsSchema = z.object({ .string() .max(100) .regex(/^[a-z0-9-_.]+$/) - .describe('Project key to identify which project to update'), // Make key required for identifying the project - name: schemas.UpdateProjectDto.shape.name.describe( - 'Updated project name (max 100 characters)', - ), - description: schemas.UpdateProjectDto.shape.description.describe( - 'Updated project description (max 1000 characters)', - ), + .describe('key to identify project to update'), + name: schemas.UpdateProjectDto.shape.name, + description: schemas.UpdateProjectDto.shape.description, color: schemas.UpdateProjectDto.shape.color.describe( 'Updated project color in hex format (e.g., #FF0000)', ), - settings: schemas.UpdateProjectDto.shape.settings.describe( - 'Updated project settings configuration', - ), + settings: schemas.UpdateProjectDto.shape.settings, }) export const ListEnvironmentsArgsSchema = z.object({ @@ -222,7 +210,7 @@ export const ListEnvironmentsArgsSchema = z.object({ .string() .min(3) .optional() - .describe('Search term to filter environments by name or key'), + .describe('filter environments by "name" or "key"'), page: z.number().min(1).optional().describe('Page number for pagination'), perPage: z .number() @@ -230,7 +218,7 @@ export const ListEnvironmentsArgsSchema = z.object({ .max(1000) .default(100) .optional() - .describe('Number of items per page (1-1000)'), + .describe('Number of items per page'), sortBy: z .enum([ 'createdAt', @@ -240,86 +228,57 @@ export const ListEnvironmentsArgsSchema = z.object({ 'createdBy', 'propertyKey', ]) - .optional() - .describe('Field to sort environments by'), - sortOrder: z - .enum(['asc', 'desc']) - .optional() - .describe('Sort order (ascending or descending)'), - createdBy: z - .string() - .optional() - .describe('Filter environments by creator user ID'), + .optional(), + sortOrder: z.enum(['asc', 'desc']).optional(), + createdBy: z.string().optional().describe('Filter by creator user ID'), }) export const GetSdkKeysArgsSchema = z.object({ - environmentKey: z.string().describe('Environment key to get SDK keys for'), + environmentKey: z.string(), keyType: z .enum(['mobile', 'server', 'client']) .optional() - .describe('Specific type of SDK key to retrieve (optional)'), + .describe('type of SDK key to retrieve'), }) export const CreateEnvironmentArgsSchema = z.object({ - name: schemas.CreateEnvironmentDto.shape.name.describe( - 'Environment name (max 100 characters)', - ), + name: schemas.CreateEnvironmentDto.shape.name, key: schemas.CreateEnvironmentDto.shape.key.describe( 'Unique environment key (lowercase letters, numbers, dots, dashes, underscores only)', ), - description: schemas.CreateEnvironmentDto.shape.description.describe( - 'Environment description (max 1000 characters)', - ), + description: schemas.CreateEnvironmentDto.shape.description, color: schemas.CreateEnvironmentDto.shape.color.describe( 'Environment color in hex format (e.g., #FF0000)', ), - type: schemas.CreateEnvironmentDto.shape.type.describe( - 'Environment type (development, staging, production, or disaster_recovery)', - ), - settings: schemas.CreateEnvironmentDto.shape.settings.describe( - 'Environment settings configuration', - ), + type: schemas.CreateEnvironmentDto.shape.type, + settings: schemas.CreateEnvironmentDto.shape.settings, }) export const UpdateEnvironmentArgsSchema = z.object({ - key: z - .string() - .describe('Environment key to identify which environment to update'), // Make key required for identifying the environment - name: schemas.UpdateEnvironmentDto.shape.name.describe( - 'Updated environment name (max 100 characters)', - ), - description: schemas.UpdateEnvironmentDto.shape.description.describe( - 'Updated environment description (max 1000 characters)', - ), + key: z.string().describe('key to identify environment to update'), + name: schemas.UpdateEnvironmentDto.shape.name, + description: schemas.UpdateEnvironmentDto.shape.description, color: schemas.UpdateEnvironmentDto.shape.color.describe( - 'Updated environment color in hex format (e.g., #FF0000)', - ), - type: schemas.UpdateEnvironmentDto.shape.type.describe( - 'Updated environment type (development, staging, production, or disaster_recovery)', - ), - settings: schemas.UpdateEnvironmentDto.shape.settings.describe( - 'Updated environment settings configuration', + 'color in hex format (e.g., #FF0000)', ), + type: schemas.UpdateEnvironmentDto.shape.type, + settings: schemas.UpdateEnvironmentDto.shape.settings, }) export const SetFeatureTargetingArgsSchema = z.object({ feature_key: z.string().describe('Feature key to set targeting for'), - environment_key: z.string().describe('Environment key to set targeting in'), - enabled: z - .boolean() - .describe('Whether to enable (true) or disable (false) targeting'), + environment_key: z + .string() + .describe('Environment key to set targeting for'), + enabled: z.boolean().describe('enable or disable targeting'), }) export const CreateFeatureArgsSchema = z.object({ - name: schemas.CreateFeatureDto.shape.name.describe( - 'Feature name (max 100 characters)', - ), + name: schemas.CreateFeatureDto.shape.name, key: schemas.CreateFeatureDto.shape.key.describe( 'Unique feature key (lowercase letters, numbers, dots, dashes, underscores only)', ), - description: schemas.CreateFeatureDto.shape.description.describe( - 'Feature description (max 1000 characters)', - ), + description: schemas.CreateFeatureDto.shape.description, variables: schemas.CreateFeatureDto.shape.variables.describe( 'Array of variables to create or reassociate with this feature', ), @@ -338,16 +297,10 @@ export const CreateFeatureArgsSchema = z.object({ sdkVisibility: schemas.CreateFeatureDto.shape.sdkVisibility.describe( 'SDK Type Visibility Settings for mobile, client, and server SDKs', ), - type: schemas.CreateFeatureDto.shape.type.describe( - 'Feature type (release, experiment, permission, or ops)', - ), + type: schemas.CreateFeatureDto.shape.type, tags: schemas.CreateFeatureDto.shape.tags.describe( 'Tags to organize features', ), - interactive: z - .boolean() - .optional() - .describe('MCP-specific: prompt for missing fields'), }) export const UpdateFeatureArgsSchema = z.object({ @@ -355,13 +308,9 @@ export const UpdateFeatureArgsSchema = z.object({ .string() .max(100) .regex(/^[a-z0-9-_.]+$/) - .describe('Feature key to identify which feature to update'), - name: schemas.UpdateFeatureDto.shape.name.describe( - 'Updated feature name (max 100 characters)', - ), - description: schemas.UpdateFeatureDto.shape.description.describe( - 'Updated feature description (max 1000 characters)', - ), + .describe('key to identify feature to update'), + name: schemas.UpdateFeatureDto.shape.name, + description: schemas.UpdateFeatureDto.shape.description, variables: schemas.UpdateFeatureDto.shape.variables.describe( 'Updated array of variables for this feature', ), @@ -374,9 +323,7 @@ export const UpdateFeatureArgsSchema = z.object({ sdkVisibility: schemas.UpdateFeatureDto.shape.sdkVisibility.describe( 'Updated SDK Type Visibility Settings for mobile, client, and server SDKs', ), - type: schemas.UpdateFeatureDto.shape.type.describe( - 'Updated feature type (release, experiment, permission, or ops)', - ), + type: schemas.UpdateFeatureDto.shape.type, tags: schemas.UpdateFeatureDto.shape.tags.describe( 'Updated tags to organize features', ), @@ -390,12 +337,10 @@ export const UpdateFeatureStatusArgsSchema = z.object({ .string() .max(100) .regex(/^[a-z0-9-_.]+$/) - .describe('Feature key to identify which feature to update'), - status: UpdateFeatureStatusDto.shape.status.describe( - 'Updated feature status (active, complete, or archived)', - ), + .describe('key to identify feature to update'), + status: UpdateFeatureStatusDto.shape.status, staticVariation: UpdateFeatureStatusDto.shape.staticVariation.describe( - 'The variation key or ID to serve if the status is set to complete (optional)', + 'The variation key or ID to serve if the status is set to complete', ), }) @@ -426,32 +371,30 @@ export const ListVariationsArgsSchema = z.object({ feature_key: z.string().describe('Feature key to list variations for'), }) +const variablesDescription = + 'key-value map of variable keys to their values for this variation. { "variableKey1": "value1", "variableKey2": false }' + export const CreateVariationArgsSchema = z.object({ feature_key: z.string().describe('Feature key to create variation for'), key: schemas.CreateVariationDto.shape.key.describe( 'Unique variation key (lowercase letters, numbers, dots, dashes, underscores only)', ), - name: schemas.CreateVariationDto.shape.name.describe( - 'Variation name (max 100 characters)', - ), - variables: schemas.CreateVariationDto.shape.variables.describe( - 'Key-value map of variable keys to their values for this variation', - ), + name: schemas.CreateVariationDto.shape.name, + variables: + schemas.CreateVariationDto.shape.variables.describe( + variablesDescription, + ), }) export const UpdateVariationArgsSchema = z.object({ feature_key: z .string() .describe('Feature key that the variation belongs to'), - variation_key: z - .string() - .describe('Variation key to identify which variation to update'), + variation_key: z.string().describe('key to identify variation to update'), key: schemas.UpdateFeatureVariationDto.shape.key.describe( 'Updated variation key (lowercase letters, numbers, dots, dashes, underscores only)', ), - name: schemas.UpdateFeatureVariationDto.shape.name.describe( - 'Updated variation name (max 100 characters)', - ), + name: schemas.UpdateFeatureVariationDto.shape.name, variables: z .record( z.union([ @@ -462,9 +405,7 @@ export const UpdateVariationArgsSchema = z.object({ ]), ) .optional() - .describe( - 'Updated key-value map of variable keys to their values for this variation', - ), + .describe(`Updated ${variablesDescription}`), }) export const ListFeatureTargetingArgsSchema = z.object({ @@ -480,9 +421,7 @@ export const UpdateFeatureTargetingArgsSchema = z.object({ environment_key: z .string() .describe('Environment key to update targeting in'), - status: schemas.UpdateFeatureConfigDto.shape.status.describe( - 'Updated targeting status for the feature', - ), + status: schemas.UpdateFeatureConfigDto.shape.status, targets: schemas.UpdateFeatureConfigDto.shape.targets.describe( 'Updated array of targeting rules/targets for the feature', ), @@ -497,24 +436,19 @@ export const GetFeatureAuditLogHistoryArgsSchema = z.object({ .min(1) .default(1) .optional() - .describe('Page number for pagination (default: 1)'), + .describe('Page number for pagination'), perPage: z .number() .min(1) .max(1000) .default(100) .optional() - .describe('Number of items per page (default: 100, max: 1000)'), + .describe('Number of items per page'), sortBy: z .enum(['createdAt', 'updatedAt', 'action', 'user']) .default('createdAt') - .optional() - .describe('Field to sort audit entries by (default: createdAt)'), - sortOrder: z - .enum(['asc', 'desc']) - .default('desc') - .optional() - .describe('Sort order (default: desc)'), + .optional(), + sortOrder: z.enum(['asc', 'desc']).default('desc').optional(), startDate: z .string() .optional() @@ -529,7 +463,7 @@ export const GetFeatureAuditLogHistoryArgsSchema = z.object({ .describe('Environment key to filter audit entries by'), user: z.string().optional().describe('User ID to filter audit entries by'), action: z - .string() + .enum(['created', 'modified', 'deleted']) .optional() .describe('Action type to filter audit entries by'), }) @@ -549,26 +483,17 @@ export const AuditLogEntitySchema = z.object({ // Base evaluation query schema (matches API camelCase naming) const BaseEvaluationQuerySchema = z.object({ - startDate: z - .number() - .optional() - .describe('Start date as Unix timestamp (optional)'), - endDate: z - .number() - .optional() - .describe('End date as Unix timestamp (optional)'), - environment: z - .string() - .optional() - .describe('Environment key to filter by (optional)'), + startDate: z.number().optional().describe('Start date as Unix timestamp'), + endDate: z.number().optional().describe('End date as Unix timestamp'), + environment: z.string().optional().describe('Environment key to filter by'), period: z .enum(['day', 'hour', 'month']) .optional() - .describe('Time period for aggregation (optional)'), + .describe('Time period for aggregation'), sdkType: z .enum(['client', 'server', 'mobile', 'api']) .optional() - .describe('SDK type to filter by (optional)'), + .describe('SDK type to filter by'), }) // MCP argument schemas (using camelCase to match API) @@ -577,14 +502,8 @@ export const GetFeatureTotalEvaluationsArgsSchema = featureKey: z .string() .describe('Feature key to get evaluation data for'), - platform: z - .string() - .optional() - .describe('Platform to filter by (optional)'), - variable: z - .string() - .optional() - .describe('Variable key to filter by (optional)'), + platform: z.string().optional().describe('Platform to filter by'), + variable: z.string().optional().describe('Variable key to filter by'), }) export const GetProjectTotalEvaluationsArgsSchema = BaseEvaluationQuerySchema @@ -608,7 +527,7 @@ export const ListCustomPropertiesArgsSchema = z.object({ .max(1000) .default(100) .optional() - .describe('Number of items per page (1-1000)'), + .describe('Number of items per page'), sortBy: z .enum([ 'createdAt', @@ -619,34 +538,22 @@ export const ListCustomPropertiesArgsSchema = z.object({ 'propertyKey', ]) .default('createdAt') - .optional() - .describe('Field to sort custom properties by'), - sortOrder: z - .enum(['asc', 'desc']) - .default('desc') - .optional() - .describe('Sort order (ascending or descending)'), + .optional(), + sortOrder: z.enum(['asc', 'desc']).default('desc').optional(), search: z .string() .min(3) .optional() - .describe('Search term to filter custom properties by name or key'), - createdBy: z - .string() - .optional() - .describe('Filter custom properties by creator user ID'), + .describe('Search term to filter by "name" or "key"'), + createdBy: z.string().optional().describe('Filter by creator user ID'), }) export const UpsertCustomPropertyArgsSchema = z.object({ - name: schemas.CreateCustomPropertyDto.shape.name.describe( - 'Custom property name (max 100 characters)', - ), + name: schemas.CreateCustomPropertyDto.shape.name, key: schemas.CreateCustomPropertyDto.shape.key.describe( 'Unique custom property key (lowercase letters, numbers, dots, dashes, underscores only)', ), - type: schemas.CreateCustomPropertyDto.shape.type.describe( - 'Custom property type (String, Boolean, or Number)', - ), + type: schemas.CreateCustomPropertyDto.shape.type, propertyKey: schemas.CreateCustomPropertyDto.shape.propertyKey.describe( 'Property key to associate with the custom property', ), @@ -657,20 +564,12 @@ export const UpdateCustomPropertyArgsSchema = z.object({ .string() .max(100) .regex(/^[a-z0-9-_.]+$/) - .describe('Custom property key to identify which property to update'), // Make key required for identifying the custom property - name: schemas.UpdateCustomPropertyDto.shape.name.describe( - 'Updated custom property name (max 100 characters)', - ), - propertyKey: schemas.UpdateCustomPropertyDto.shape.propertyKey.describe( - 'Updated property key to associate with the custom property', - ), - type: schemas.UpdateCustomPropertyDto.shape.type.describe( - 'Updated custom property type (String, Boolean, or Number)', - ), + .describe('key to identify property to update'), + name: schemas.UpdateCustomPropertyDto.shape.name, + propertyKey: schemas.UpdateCustomPropertyDto.shape.propertyKey, + type: schemas.UpdateCustomPropertyDto.shape.type, }) export const DeleteCustomPropertyArgsSchema = z.object({ - key: z - .string() - .describe('Custom property key to identify which property to delete'), + key: z.string().describe('key to identify property to delete'), }) diff --git a/yarn.lock b/yarn.lock index 50e0d15d..beff2083 100644 --- a/yarn.lock +++ b/yarn.lock @@ -77,6 +77,16 @@ __metadata: languageName: node linkType: hard +"@anthropic-ai/tokenizer@npm:^0.0.4": + version: 0.0.4 + resolution: "@anthropic-ai/tokenizer@npm:0.0.4" + dependencies: + "@types/node": "npm:^18.11.18" + tiktoken: "npm:^1.0.10" + checksum: 10c0/fddaa82c26228b6385a0a064c145450564d0288c51d0346a70ce62a716627b26c227077aa909405313668fb5cbef91f3e396783b83decfa01d1c8777d5141220 + languageName: node + linkType: hard + "@apidevtools/json-schema-ref-parser@npm:9.0.6": version: 9.0.6 resolution: "@apidevtools/json-schema-ref-parser@npm:9.0.6" @@ -734,6 +744,7 @@ __metadata: version: 0.0.0-use.local resolution: "@devcycle/cli@workspace:." dependencies: + "@anthropic-ai/tokenizer": "npm:^0.0.4" "@babel/code-frame": "npm:^7.27.1" "@babel/core": "npm:^7.28.0" "@babel/generator": "npm:^7.28.0" @@ -769,6 +780,7 @@ __metadata: eslint-config-prettier: "npm:^9.1.0" estraverse: "npm:^5.3.0" fuzzy: "npm:^0.1.3" + gpt-tokenizer: "npm:^3.0.1" inquirer: "npm:^8.2.6" inquirer-autocomplete-prompt: "npm:^2.0.1" js-sha256: "npm:^0.11.0" @@ -2824,6 +2836,15 @@ __metadata: languageName: node linkType: hard +"@types/node@npm:^18.11.18": + version: 18.19.123 + resolution: "@types/node@npm:18.19.123" + dependencies: + undici-types: "npm:~5.26.4" + checksum: 10c0/8077177ee2019c4c8875784b367732813b07e533f24197d1fb3bb09e81335267de9da3e70326daaba7a6499df2410257f6099d82d15c9a903d1587a752563178 + languageName: node + linkType: hard + "@types/node@npm:^18.19.68": version: 18.19.118 resolution: "@types/node@npm:18.19.118" @@ -6115,6 +6136,13 @@ __metadata: languageName: node linkType: hard +"gpt-tokenizer@npm:^3.0.1": + version: 3.0.1 + resolution: "gpt-tokenizer@npm:3.0.1" + checksum: 10c0/e95c0825ccc13d27ff873b507c95eca1224f6c7aafdc825a49271959c1f8480368bad75728e32190d106489e25bae788c5932c7c8266ec58dc446ad0d5a26883 + languageName: node + linkType: hard + "graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.5, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.6, graceful-fs@npm:^4.2.9": version: 4.2.11 resolution: "graceful-fs@npm:4.2.11" @@ -10524,6 +10552,13 @@ __metadata: languageName: node linkType: hard +"tiktoken@npm:^1.0.10": + version: 1.0.22 + resolution: "tiktoken@npm:1.0.22" + checksum: 10c0/4805cf957d32ee53707ea8416256b50f6b4e865fce1d36ba507bfcbf4dd31bfa69b31dbd1303ba216ae44eb680f54c46c11defa5305445ecea946f2c048fa87d + languageName: node + linkType: hard + "tinybench@npm:^2.9.0": version: 2.9.0 resolution: "tinybench@npm:2.9.0"