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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions mcp-worker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
```
6 changes: 5 additions & 1 deletion mcp-worker/src/projectSelectionTools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
),
})

Expand Down Expand Up @@ -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: {
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand All @@ -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",
Expand Down
135 changes: 135 additions & 0 deletions scripts/measureMcpTokens.ts
Original file line number Diff line number Diff line change
@@ -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<number> {
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<number> {
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) : ''
Comment on lines +80 to +82
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: the variable names could be more verbose. not blocking


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)
})
1 change: 1 addition & 0 deletions src/mcp/tools/customPropertiesTools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
17 changes: 10 additions & 7 deletions src/mcp/tools/featureTools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)',
Expand Down Expand Up @@ -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.',
Expand All @@ -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,
Expand Down Expand Up @@ -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: {
Expand Down
1 change: 1 addition & 0 deletions src/mcp/tools/installTools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
8 changes: 6 additions & 2 deletions src/mcp/tools/localProjectTools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
),
})

Expand Down Expand Up @@ -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: {
Expand Down
3 changes: 2 additions & 1 deletion src/mcp/tools/projectTools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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',
Expand Down
4 changes: 3 additions & 1 deletion src/mcp/tools/resultsTools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -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: {
Expand Down
6 changes: 4 additions & 2 deletions src/mcp/tools/selfTargetingTools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -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: {
Expand Down
3 changes: 2 additions & 1 deletion src/mcp/tools/variableTools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -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'),
Expand Down
Loading