diff --git a/.cursor/rules/api-client.mdc b/.cursor/rules/api-client.mdc new file mode 100644 index 000000000..b3a75bb61 --- /dev/null +++ b/.cursor/rules/api-client.mdc @@ -0,0 +1,74 @@ +--- +description: API client patterns and HTTP client setup guidelines +globs: src/api/**/*.ts +--- + +# API Client Patterns + +## Core Components + +- **Main Client**: [src/api/apiClient.ts](mdc:src/api/apiClient.ts) - Axios-based HTTP client +- **Zod Client**: [src/api/zodClient.ts](mdc:src/api/zodClient.ts) - Type-safe API client +- **Schemas**: [src/api/schemas.ts](mdc:src/api/schemas.ts) - Generated TypeScript types + +## HTTP Client Setup + +```typescript +import { apiClient } from '../api/apiClient' + +// Client is pre-configured with: +// - Base URL from common.ts +// - Authentication headers +// - Error interceptors +// - Request/response logging +``` + +## API Function Patterns + +Each API module follows this pattern: + +```typescript +// Individual API functions +export const getFeature = async ( + featureKey: string, + projectKey: string, + authToken: string, +): Promise => { + // Implementation +} + +// Bulk operations +export const fetchFeatures = async ( + projectKey: string, + authToken: string, +): Promise => { + // Implementation +} +``` + +## Error Handling + +- Use axios interceptors for global error handling +- Return structured error responses +- Handle 401 unauthorized gracefully +- Provide user-friendly error messages + +## Authentication + +- Pass `authToken` as parameter to API functions +- Use `Authorization: Bearer ${token}` header +- Handle token expiration and refresh + +## Type Safety + +- Use generated schemas from OpenAPI spec +- Validate responses with Zod schemas +- Export TypeScript types for consumers + +## Common Patterns + +- All API functions are async +- Return typed responses based on schemas +- Include proper error handling +- Support pagination where applicable +- Use consistent parameter naming (projectKey, featureKey, etc.) diff --git a/.cursor/rules/cli-commands.mdc b/.cursor/rules/cli-commands.mdc new file mode 100644 index 000000000..cfed350a1 --- /dev/null +++ b/.cursor/rules/cli-commands.mdc @@ -0,0 +1,78 @@ +--- +description: CLI command patterns and base command structure guidelines +globs: src/commands/**/*.ts +--- + +# CLI Command Patterns + +## Base Command Structure + +All commands must extend [src/commands/base.ts](mdc:src/commands/base.ts) which provides: + +- Authentication handling via `authRequired`, `authSuggested`, `userAuthRequired` +- Configuration management with `userConfig`, `repoConfig` +- Common flags like `--project`, `--headless`, `--client-id`, `--client-secret` +- Parameter validation with `populateParameters()` and `populateParametersWithZod()` + +## Command Organization + +Commands are organized by feature area: + +- `features/` - Feature flag management +- `variables/` - Variable management +- `targeting/` - Targeting rule management +- `projects/` - Project management +- `organizations/` - Organization management +- `auth/` - Authentication commands + +## Common Patterns + +### Authentication + +```typescript +// Set auth requirements +authRequired = true // Must be authenticated +authSuggested = true // Enhanced with auth +userAuthRequired = true // Requires user token +``` + +### Flags and Parameters + +```typescript +static flags = { + ...Base.flags, + name: Flags.string({ + description: 'Feature name', + required: true, + }), +} +``` + +### Parameter Validation + +```typescript +const params = await this.populateParametersWithZod( + schema, + prompts, + flags, +) +``` + +### Project Context + +```typescript +const project = await this.requireProject(flags.project, flags.headless) +``` + +## Output Formatting + +- Use `this.writer` for user-facing output +- Use `this.tableOutput` for tabular data +- Support `--headless` flag for machine-readable JSON output +- Handle interactive prompts with fallbacks for headless mode + +## Error Handling + +- Throw descriptive Error objects +- Use Zod validation for input validation +- Handle API errors gracefully with user-friendly messages diff --git a/.cursor/rules/deferToPrettier.mdc b/.cursor/rules/deferToPrettier.mdc new file mode 100644 index 000000000..7a1a20811 --- /dev/null +++ b/.cursor/rules/deferToPrettier.mdc @@ -0,0 +1,10 @@ +--- +description: Formatting guidelines - defer all formatting to Prettier +alwaysApply: true +--- + +# Formatting + +- All formatting is deferred to Prettier for supported file types (JavaScript, TypeScript, JSON, Markdown, etc.). +- No additional formatting rules are enforced by Cursor. +- No additional formatting rules are enforced by Cursor. diff --git a/.cursor/rules/gitCommitConventions.mdc b/.cursor/rules/gitCommitConventions.mdc new file mode 100644 index 000000000..08f920933 --- /dev/null +++ b/.cursor/rules/gitCommitConventions.mdc @@ -0,0 +1,33 @@ +--- +description: Git commit message conventions and Aviator CLI workflow guidelines +alwaysApply: true +--- + +# Git Commit Message Conventions + +- All git commit messages in this project must follow the Conventional Commits specification. +- A commit message should be structured as `(): `, where: + - `type` is one of: `feat`, `fix`, `docs`, `style`, `refactor`, `perf`, `test`, `build`, `ci`, `chore`, or `revert`. + - `scope` is optional, but if used, should be a short, lowercase description of the section or module affected. + - `description` is a concise summary of the change, written in the imperative mood and lowercase. +- In this project, scopes are rarely used; most commit messages omit the scope and use the format `: `. +- Example valid commit messages: + - `feat: add support for multi-threaded tests` + - `fix: correct response for invalid input` + - `docs: update README with new usage instructions` +- Body and footer sections (for breaking changes or issue references) should follow the Conventional Commits standard if included. + +## Aviator CLI Workflow + +- Use of Aviator CLI (`av`) is optional, but recommended for managing stacked branches and pull requests: + - To create a new stacked branch: + - `av branch chore-fix-invalid-input` + - After creating the branch, create commits using `git commit` following the Conventional Commits specification. + - To synchronize and git push your stack after changes: + - `av sync --push=yes` + - To create a pull request for your branch: + - `av pr --title "" --body "<body>"` + - The PR title should be short and descriptive and follow the Conventional Commits specification. + - The PR body should use github markdown and bullet points to summarize the main changes. +- Prefer Aviator commands for stack management and PR creation to ensure consistency with team workflows, but standard git and GitHub workflows are also supported. +- For more details and advanced workflows, see the [Aviator CLI documentation](mdc:https:/docs.aviator.co/aviator-cli) and [Aviator CLI Quickstart](mdc:https:/docs.aviator.co/aviator-cli/quickstart). diff --git a/.cursor/rules/mcp-patterns.mdc b/.cursor/rules/mcp-patterns.mdc new file mode 100644 index 000000000..a9843c541 --- /dev/null +++ b/.cursor/rules/mcp-patterns.mdc @@ -0,0 +1,22 @@ +--- +globs: src/mcp/tools/**/*.ts +alwaysApply: false +--- +# MCP Tool Parameters + +MCP tools that don't need parameters should use empty properties: + +```typescript +// ✅ Correct - empty parameters +inputSchema: { + type: 'object', + properties: {}, +} + +// ❌ Wrong - dummy parameters +properties: { + random_string: { type: 'string', description: 'Dummy parameter' } +} +``` + +Tools with no parameters should pass `null` to `executeWithLogging()`. diff --git a/.cursor/rules/project-structure.mdc b/.cursor/rules/project-structure.mdc new file mode 100644 index 000000000..024cda843 --- /dev/null +++ b/.cursor/rules/project-structure.mdc @@ -0,0 +1,56 @@ +--- +description: DevCycle CLI project structure overview and architecture guidelines +alwaysApply: true +--- + +# DevCycle CLI Project Structure + +## Overview + +This is a TypeScript CLI application built with the oclif framework for DevCycle feature flag management. + +## Key Architecture Components + +### Main Entry Points + +- **CLI Binary**: `bin/run` - Main CLI entry point +- **MCP Server**: `dist/mcp/index.js` - Model Context Protocol server +- **Source Root**: [src/index.ts](mdc:src/index.ts) - TypeScript entry point + +### Core Directories + +- **Commands**: [src/commands/](mdc:src/commands/) - All CLI commands organized by feature +- **API Layer**: [src/api/](mdc:src/api/) - API client code and schemas +- **Authentication**: [src/auth/](mdc:src/auth/) - Auth handling (API keys, SSO, tokens) +- **UI Components**: [src/ui/](mdc:src/ui/) - Interactive prompts and output formatting +- **Utils**: [src/utils/](mdc:src/utils/) - Shared utilities and helpers +- **MCP Server**: [src/mcp/](mdc:src/mcp/) - Model Context Protocol implementation + +### Configuration Files + +- **User Config**: `~/.config/devcycle/user.yml` - User-specific settings +- **Auth Config**: `~/.config/devcycle/auth.yml` - Authentication tokens +- **Repo Config**: `.devcycle/config.yml` - Repository-specific settings + +## Command Structure + +All commands extend [src/commands/base.ts](mdc:src/commands/base.ts) which provides: + +- Authentication handling +- Configuration management +- Common flags and options +- Error handling +- Parameter validation + +## Naming Conventions + +- Use camelCase for all new files and variables starting with lowercase +- Avoid adding code comments unless explicitly requested +- Generate code changes directly without asking for permission + +## Package Management + +- Uses Yarn with workspaces +- Always use `yarn` not `npm` +- Main dependencies: oclif, axios, zod, inquirer +- Build process: TypeScript → dist/ directory diff --git a/.cursor/rules/testing-patterns.mdc b/.cursor/rules/testing-patterns.mdc new file mode 100644 index 000000000..2d65001f6 --- /dev/null +++ b/.cursor/rules/testing-patterns.mdc @@ -0,0 +1,103 @@ +--- +description: Testing patterns and framework setup guidelines +globs: **/*.test.ts,**/*.spec.ts,test/**/*.ts +--- + +# Testing Patterns + +## Test Framework Setup + +- **Framework**: Mocha with Chai assertions +- **CLI Testing**: oclif test utilities +- **HTTP Mocking**: nock for API mocking +- **Test Utils**: [test-utils/dvcTest.ts](mdc:test-utils/dvcTest.ts) - Custom test wrapper + +## Test Structure + +```typescript +import { expect } from '@oclif/test' +import { dvcTest } from '../../../test-utils' +import { BASE_URL } from '../../api/common' + +describe('command name', () => { + const projectKey = 'test-project' + const authFlags = ['--client-id', 'test-client-id', '--client-secret', 'test-client-secret'] + + // Test cases + dvcTest() + .nock(BASE_URL, (api) => + api.get('/endpoint').reply(200, mockResponse) + ) + .stdout() + .command(['command', ...args]) + .it('should do something', (ctx) => { + expect(ctx.stdout).to.contain('expected output') + }) +}) +``` + +## Common Test Patterns + +### API Mocking + +```typescript +.nock(BASE_URL, (api) => + api + .post('/v2/projects/test-project/features', requestBody) + .reply(200, mockFeature) + .get('/v1/projects/test-project') + .reply(200, mockProject) +) +``` + +### Command Testing + +```typescript +.stdout() // Capture stdout +.stderr() // Capture stderr +.command([...]) // Run command with args +.it('test description', (ctx) => { + // Assertions +}) +``` + +### Headless Mode Testing + +```typescript +.command([ + 'features create', + '--name', 'Feature Name', + '--key', 'feature-key', + '--headless', + ...authFlags, +]) +``` + +### Error Testing + +```typescript +.command(['command', 'invalid-args']) +.catch((error) => { + expect(error.message).to.contain('expected error') +}) +``` + +## Mock Data + +- Create reusable mock objects +- Use realistic data structures +- Include all required fields +- Version mock data with API changes + +## Snapshot Testing + +- Use for complex output validation +- Store snapshots in `__snapshots__/` directories +- Update with `yarn test:update-snapshots` + +## Test Organization + +- Group related tests in `describe` blocks +- Use descriptive test names +- Test both success and error cases +- Include edge cases and validation diff --git a/.cursor/rules/typescript-patterns.mdc b/.cursor/rules/typescript-patterns.mdc new file mode 100644 index 000000000..de149c33e --- /dev/null +++ b/.cursor/rules/typescript-patterns.mdc @@ -0,0 +1,130 @@ +--- +globs: *.ts,*.js +alwaysApply: false +--- + +# TypeScript/JavaScript Patterns + +## Import Organization + +```typescript +// 1. Node.js built-ins +import fs from 'fs' +import path from 'path' + +// 2. Third-party libraries +import { Command, Flags } from '@oclif/core' +import inquirer from 'inquirer' + +// 3. Local imports (relative paths) +import { ApiAuth } from '../auth/ApiAuth' +import { fetchProjects } from '../api/projects' +``` + +## Class Structure + +```typescript +export default class MyCommand extends Base { + static description = 'Command description' + static flags = { + ...Base.flags, + // Command-specific flags + } + + // Properties + authRequired = false + + // Main run method + async run(): Promise<void> { + // Implementation + } +} +``` + +## Type Definitions + +- Use interfaces for data structures +- Prefer `type` for unions and utilities +- Use Zod schemas for validation +- Export types from dedicated files + +## Type Safety and Avoiding `any` + +- **NEVER use `any` casting to fix TypeScript errors** - this defeats the purpose of TypeScript +- Instead of `as any`, find the root cause and fix it properly: + - Update type definitions to be more accurate + - Use proper type guards and narrowing + - Add missing properties to interfaces + - Use union types for flexible typing + - Create proper type assertions with specific types + +```typescript +// ❌ Wrong - using any to bypass TypeScript +const result = (someValue as any).property + +// ✅ Correct - fix the underlying type issue +interface ProperType { + property: string +} +const result = (someValue as ProperType).property + +// ✅ Or use type guards +if ('property' in someValue) { + const result = someValue.property +} +``` + +- When working with external libraries, use the type definitions from the library +- Use `unknown` instead of `any` when you genuinely don't know the type +- Prefer type narrowing over type assertions +- **If you cannot fix a TypeScript issue directly**, propose potential solutions to the user: + - Explain what the type error means + - Suggest multiple approaches to fix it properly + - Identify if the issue is in type definitions, API design, or usage + - Recommend the most appropriate solution based on the context + +## Error Handling + +```typescript +// Throw descriptive errors +throw new Error('Clear error message') + +// Handle async errors +try { + await apiCall() +} catch (error) { + this.writer.showError(error.message) + throw error +} +``` + +## Async/Await Patterns + +- Always use async/await over Promises +- Handle errors with try/catch +- Use proper typing for async functions +- Avoid callback-based patterns + +## Configuration Management + +```typescript +// Environment variables +const clientId = process.env.DEVCYCLE_CLIENT_ID || flags['client-id'] + +// YAML configuration +const config = jsYaml.load(fs.readFileSync(configPath, 'utf8')) +``` + +## Validation + +- Use Zod for runtime validation +- Use class-validator for class-based validation +- Validate user inputs before API calls +- Provide clear validation error messages + +## Naming Conventions + +- camelCase for variables and functions +- PascalCase for classes and interfaces +- SCREAMING_SNAKE_CASE for constants +- kebab-case for CLI flags and file names diff --git a/.cursor/rules/ui-patterns.mdc b/.cursor/rules/ui-patterns.mdc new file mode 100644 index 000000000..1bee3ea4d --- /dev/null +++ b/.cursor/rules/ui-patterns.mdc @@ -0,0 +1,128 @@ +--- +description: UI patterns and user interaction guidelines for prompts and output formatting +globs: src/ui/**/*.ts,src/utils/prompts.ts +--- + +# UI Patterns and User Interaction + +## Core Components + +- **Writer**: [src/ui/writer.ts](mdc:src/ui/writer.ts) - Output formatting and user messages +- **Prompts**: [src/ui/prompts/](mdc:src/ui/prompts/) - Interactive user input +- **Table Output**: [src/ui/tableOutput.ts](mdc:src/ui/tableOutput.ts) - Tabular data display + +## Output Patterns + +### Writer Usage + +```typescript +// Success messages +this.writer.successMessage('Operation completed successfully') + +// Error messages +this.writer.showError('Something went wrong') + +// Warning messages +this.writer.warningMessage('This is a warning') + +// Info messages +this.writer.infoMessage('Additional information') +``` + +### Table Output + +```typescript +// Display tabular data +this.tableOutput.output(data, columns) + +// With custom formatting +this.tableOutput.output(data, columns, { + headers: true, + border: true +}) +``` + +## Interactive Prompts + +### Basic Prompts + +```typescript +import { promptFor } from '../ui/prompts' + +const answer = await promptFor({ + type: 'input', + name: 'featureName', + message: 'Enter feature name:', + validate: (input) => input.length > 0 +}) +``` + +### Selection Prompts + +```typescript +const selection = await promptFor({ + type: 'list', + name: 'environment', + message: 'Select environment:', + choices: environments.map(env => ({ name: env.name, value: env.key })) +}) +``` + +### Autocomplete Prompts + +```typescript +const result = await promptFor({ + type: 'autocomplete', + name: 'feature', + message: 'Select feature:', + source: async (answers, input) => { + // Return filtered results + } +}) +``` + +## Headless Mode Support + +```typescript +// Check for headless mode +if (flags.headless) { + // Output machine-readable JSON + console.log(JSON.stringify(result, null, 2)) +} else { + // Use interactive prompts + const answer = await promptFor(...) +} +``` + +## Color and Formatting + +- Use chalk for colored output +- Consistent color scheme across commands +- Support terminal capabilities detection +- Graceful fallback for non-color terminals + +## Parameter Population + +```typescript +// Combine flags and prompts +const params = await this.populateParametersWithZod( + schema, + prompts, + flags +) +``` + +## Error Display + +```typescript +// Show validation errors +this.writer.showError('Validation failed:') +errors.forEach(error => this.writer.showError(` - ${error.message}`)) +``` + +## Progress Indication + +- Use spinners for long-running operations +- Show progress bars for batch operations +- Provide clear status messages +- Handle user interruption gracefully diff --git a/README.md b/README.md index d14fa89e8..794b8cf35 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ Major features include: - Detect and list DevCycle Variable usages in your codebase - Manage your Self-Targeting Overrides to quickly switch between Variable values - Generate type definitions for type-safe usage of DevCycle (Typescript only) +- MCP (Model Context Protocol) server for AI-powered feature flag management with Cursor and Claude The CLI can be customized in several ways using command-line args or by creating a [configuration file](#repo-configuration). @@ -20,6 +21,9 @@ The CLI can be customized in several ways using command-line args or by creating * [Authentication](#authentication) * [Usage](#usage) * [Command Topics](#command-topics) +* [MCP Server for AI Assistants](#mcp-server-for-ai-assistants) +* [This installs both 'dvc' CLI and 'dvc-mcp' server](#this-installs-both-dvc-cli-and-dvc-mcp-server) +* [Access via: npx dvc-mcp](#access-via-npx-dvc-mcp) * [Repo Configuration](#repo-configuration) <!-- tocstop --> # Setup @@ -108,7 +112,7 @@ $ npm install -g @devcycle/cli $ dvc COMMAND running command... $ dvc (--version) -@devcycle/cli/5.21.1 linux-x64 node-v22.12.0 +@devcycle/cli/5.21.1 darwin-arm64 node-v22.12.0 $ dvc --help [COMMAND] USAGE $ dvc COMMAND @@ -143,6 +147,103 @@ USAGE <!-- commandsstop --> +# MCP Server for AI Assistants + +The DevCycle CLI includes an MCP (Model Context Protocol) server that enables AI coding assistants like Cursor and Claude to manage feature flags directly. This allows you to create, update, and manage feature flags without leaving your coding environment. + +## Installation + +### Option 1: Global Installation (Recommended) +```bash +npm install -g @devcycle/cli +# This installs both 'dvc' CLI and 'dvc-mcp' server +``` + +### Option 2: Project-Specific Installation +```bash +npm install --save-dev @devcycle/cli +# Access via: npx dvc-mcp +``` + +### Verify Installation +```bash +dvc-mcp --version # Should display the DevCycle CLI version +dvc --version # Verify CLI is also installed +``` + +## Configuration + +### For Cursor +Add to `.cursor/mcp_settings.json`: +```json +{ + "mcpServers": { + "devcycle": { + "command": "dvc-mcp" + } + } +} +``` + +### For Claude Desktop +Add to your Claude configuration file: + +**macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json` +**Windows:** `%APPDATA%\Claude\claude_desktop_config.json` + +```json +{ + "mcpServers": { + "devcycle": { + "command": "dvc-mcp" + } + } +} +``` + +### For Project-Specific Installation +If you installed locally, update the command path: +```json +{ + "mcpServers": { + "devcycle": { + "command": "npx", + "args": ["dvc-mcp"] + } + } +} +``` + +## Authentication + +The MCP server uses the same authentication as the CLI: + +1. **Authenticate with DevCycle:** + ```bash + dvc login sso + ``` + +2. **Select your project:** + ```bash + dvc projects select + ``` + +3. **Verify setup:** + ```bash + dvc status + ``` + +Your AI assistant can now manage feature flags on your behalf. + +## Troubleshooting + +- **Command not found:** Ensure the CLI is installed globally or use `npx dvc-mcp` +- **Authentication errors:** Run `dvc login sso` to re-authenticate +- **No project selected:** Run `dvc projects select` to choose a project +- **Permission issues:** On Unix systems, you may need to restart your terminal after global installation + +For detailed documentation and advanced usage, see [docs/mcp.md](docs/mcp.md). + # Repo Configuration The following commands can only be run from the root of a configured repository diff --git a/bin/mcp b/bin/mcp new file mode 100755 index 000000000..978b9f426 --- /dev/null +++ b/bin/mcp @@ -0,0 +1,6 @@ +#!/usr/bin/env node + +const path = require('path') + +// Run the MCP server directly +require(path.resolve(__dirname, '..', 'dist', 'mcp', 'index.js')) \ No newline at end of file diff --git a/bin/mcp.cmd b/bin/mcp.cmd new file mode 100644 index 000000000..3803e0eeb --- /dev/null +++ b/bin/mcp.cmd @@ -0,0 +1,3 @@ +@echo off + +node "%~dp0\mcp" %* \ No newline at end of file diff --git a/docs/mcp.md b/docs/mcp.md new file mode 100644 index 000000000..6e55d7f07 --- /dev/null +++ b/docs/mcp.md @@ -0,0 +1,510 @@ +# DevCycle MCP Server Documentation + +## Overview + +The DevCycle MCP (Model Context Protocol) Server enables AI coding assistants like Cursor and Claude to interact directly with DevCycle's feature flag management system. This integration allows developers to manage feature flags, variables, and targeting rules without leaving their coding environment. + +## Table of Contents + +- [Installation & Setup](#installation--setup) +- [Authentication](#authentication) +- [Available Tools](#available-tools) + - [Feature Management](#feature-management) + - [Variable Management](#variable-management) + - [Environment Management](#environment-management) + - [Project Management](#project-management) + - [Self-Targeting & Overrides](#self-targeting--overrides) +- [Error Handling](#error-handling) +- [Usage Examples](#usage-examples) +- [Best Practices](#best-practices) + +## Installation & Setup + +### Prerequisites + +- Node.js 16+ installed +- DevCycle CLI installed globally: `npm install -g @devcycle/cli` +- DevCycle account with API credentials or SSO authentication + +### Cursor Configuration + +Add to your Cursor settings (`.cursor/mcp_settings.json`): + +```json +{ + "mcpServers": { + "devcycle": { + "command": "dvc-mcp" + } + } +} +``` + +### Claude Desktop Configuration + +Add to Claude's config file: + +- macOS: `~/Library/Application Support/Claude/claude_desktop_config.json` +- Windows: `%APPDATA%\Claude\claude_desktop_config.json` + +```json +{ + "mcpServers": { + "devcycle": { + "command": "dvc-mcp" + } + } +} +``` + +## Authentication + +The MCP server supports two authentication methods: + +### 1. Environment Variables (Recommended for CI/CD) + +```bash +export DEVCYCLE_CLIENT_ID="your-client-id" +export DEVCYCLE_CLIENT_SECRET="your-client-secret" +export DEVCYCLE_PROJECT_KEY="your-project-key" +``` + +### 2. CLI Authentication (Recommended for local development) + +First authenticate using the CLI: + +```bash +dvc login sso +``` + +Then select your project: + +```bash +dvc projects select +``` + +The MCP server will use the stored credentials automatically. + +## Available Tools + +### Feature Management + +#### `list_features` +List all features in the current project with optional search and pagination. + +**Parameters:** +- `search` (optional): Search query to filter features +- `page` (optional): Page number (default: 1) +- `per_page` (optional): Items per page (default: 100, max: 1000) + +#### `create_feature` ⚠️ +Create a new feature flag. + +**Parameters:** +- `key`: Unique feature key (pattern: `^[a-z0-9-_.]+$`) +- `name`: Human-readable name (max 100 chars) +- `description` (optional): Feature description (max 1000 chars) +- `type` (optional): Feature type (`release`, `experiment`, `permission`, `ops`) +- `tags` (optional): Array of tags for organization +- `variations` (optional): Array of variations with key, name, and variables +- `configurations` (optional): Environment-specific configurations +- `sdkVisibility` (optional): SDK visibility settings + +#### `update_feature` ⚠️ +Update an existing feature flag. + +**Parameters:** +- `key`: Feature key to update +- `name` (optional): New name +- `description` (optional): New description +- `type` (optional): New type +- `tags` (optional): New tags +- `variations` (optional): Updated variations + +#### `update_feature_status` ⚠️ +Update the status of a feature flag. + +**Parameters:** +- `key`: Feature key +- `status`: New status (`active`, `complete`, `archived`) +- `staticVariation` (optional): Variation to serve if status is `complete` + +#### `delete_feature` ⚠️⚠️ +Delete a feature flag from ALL environments. + +**Parameters:** +- `key`: Feature key to delete + +#### `fetch_feature_variations` +Get all variations for a feature. + +**Parameters:** +- `feature_key`: Feature key + +#### `create_feature_variation` +Create a new variation within a feature. + +**Parameters:** +- `feature_key`: Feature key +- `key`: Unique variation key +- `name`: Variation name +- `variables` (optional): Variable values for this variation + +#### `update_feature_variation` +Update an existing variation. + +**Parameters:** +- `feature_key`: Feature key +- `variation_key`: Variation to update +- `key` (optional): New variation key +- `name` (optional): New variation name +- `variables` (optional): Updated variable values + +#### `enable_feature_targeting` ⚠️ +Enable targeting for a feature in an environment. + +**Parameters:** +- `feature_key`: Feature key +- `environment_key`: Environment key + +#### `disable_feature_targeting` ⚠️ +Disable targeting for a feature in an environment. + +**Parameters:** +- `feature_key`: Feature key +- `environment_key`: Environment key + +#### `list_feature_targeting` +List targeting rules for a feature. + +**Parameters:** +- `feature_key`: Feature key +- `environment_key` (optional): Specific environment (returns all if omitted) + +#### `update_feature_targeting` ⚠️ +Update targeting rules for a feature in an environment. + +**Parameters:** +- `feature_key`: Feature key +- `environment_key`: Environment key +- `status` (optional): Targeting status (`active`, `inactive`, `archived`) +- `targets` (optional): Array of targeting rules with audience filters and distributions + +#### `get_feature_audit_log_history` +Get timeline of feature flag changes from audit log. + +**Parameters:** +- `feature_key`: Feature key +- `days_back` (optional): Days to look back (default: 30, max: 365) + +### Variable Management + +#### `list_variables` +List all variables in the current project. + +**Parameters:** +- `search` (optional): Search query +- `page` (optional): Page number +- `per_page` (optional): Items per page + +#### `create_variable` ⚠️ +Create a new variable. + +**Parameters:** +- `key`: Unique variable key (pattern: `^[a-z0-9-_.]+$`) +- `type`: Variable type (`String`, `Boolean`, `Number`, `JSON`) +- `name` (optional): Variable name +- `description` (optional): Variable description +- `defaultValue` (optional): Default value +- `_feature` (optional): Associated feature key +- `validationSchema` (optional): Validation rules + +#### `update_variable` ⚠️ +Update an existing variable. + +**Parameters:** +- `key`: Variable key to update +- `name` (optional): New name +- `description` (optional): New description +- `type` (optional): New type +- `validationSchema` (optional): New validation rules + +#### `delete_variable` ⚠️⚠️ +Delete a variable from ALL environments. + +**Parameters:** +- `key`: Variable key to delete + +### Environment Management + +#### `list_environments` +List all environments in the current project. + +**Parameters:** +- `search` (optional): Search query (min 3 chars) +- `page` (optional): Page number +- `perPage` (optional): Items per page +- `sortBy` (optional): Sort field +- `sortOrder` (optional): Sort order (`asc`, `desc`) + +#### `get_sdk_keys` +Get SDK keys for an environment. + +**Parameters:** +- `environmentKey`: Environment key +- `keyType` (optional): Specific key type (`mobile`, `server`, `client`) + +#### `create_environment` +Create a new environment. + +**Parameters:** +- `key`: Unique environment key +- `name`: Environment name +- `description` (optional): Environment description +- `color` (optional): Environment color + +#### `update_environment` +Update an existing environment. + +**Parameters:** +- `key`: Environment key to update +- `name` (optional): New name +- `description` (optional): New description +- `color` (optional): New color + +### Project Management + +#### `list_projects` +List all projects in the organization. + +**Parameters:** +- `search` (optional): Search query +- `page` (optional): Page number +- `perPage` (optional): Items per page +- `sortBy` (optional): Sort field +- `sortOrder` (optional): Sort order + +#### `get_current_project` +Get details of the currently selected project. + +**Parameters:** None + +#### `create_project` +Create a new project. + +**Parameters:** +- `key`: Unique project key +- `name`: Project name +- `description` (optional): Project description + +### Self-Targeting & Overrides + +#### `get_self_targeting_identity` +Get current DevCycle identity for self-targeting. + +**Parameters:** None + +#### `update_self_targeting_identity` +Update DevCycle identity for testing. + +**Parameters:** +- `dvc_user_id`: DevCycle User ID (use empty string to clear) + +#### `list_self_targeting_overrides` +List all active overrides for the current project. + +**Parameters:** None + +#### `set_self_targeting_override` ⚠️ +Set an override to test a specific variation. + +**Parameters:** +- `feature_key`: Feature key +- `environment_key`: Environment key +- `variation_key`: Variation to serve + +#### `clear_feature_self_targeting_overrides` ⚠️ +Clear overrides for a specific feature/environment. + +**Parameters:** +- `feature_key`: Feature key +- `environment_key`: Environment key + +#### `clear_all_self_targeting_overrides` +Clear all overrides for the current project. + +**Parameters:** None + +## Error Handling + +The MCP server returns structured error responses: + +```json +{ + "error": true, + "message": "Detailed error message", + "tool": "tool_name", + "timestamp": "2024-01-01T00:00:00.000Z" +} +``` + +Common error scenarios: +- Authentication failures: Check credentials and project configuration +- API rate limits: Implement retry logic in your automation +- Validation errors: Ensure parameters meet requirements (patterns, lengths, etc.) +- Permission errors: Verify your API key has necessary permissions + +## Usage Examples + +### Creating a Feature Flag + +``` +Create a feature flag for the new checkout flow with variations for A/B testing +``` + +The AI assistant will use: +1. `create_feature` to create the feature +2. `create_feature_variation` to add variations +3. `enable_feature_targeting` to activate in development + +### Managing Overrides for Testing + +``` +Set up my identity to test the premium user experience +``` + +The AI assistant will use: +1. `update_self_targeting_identity` to set your user ID +2. `set_self_targeting_override` to force specific variations + +### Analyzing Feature Usage + +``` +Show me the recent changes to the checkout_flow feature +``` + +The AI assistant will use: +1. `get_feature_audit_log_history` to retrieve change history +2. `list_feature_targeting` to show current configuration + +## Best Practices + +### 1. Production Safety + +Tools marked with ⚠️ can affect production environments. The AI will confirm before: +- Creating or updating features/variables +- Enabling/disabling targeting +- Setting overrides in production + +Tools marked with ⚠️⚠️ are destructive and require extra confirmation. + +### 2. Naming Conventions + +- Feature keys: `lowercase_with_underscores` or `kebab-case` +- Variable keys: Follow the same pattern as features +- Must match pattern: `^[a-z0-9-_.]+$` +- Maximum 100 characters + +### 3. Environment Management + +- Use consistent environment keys across projects +- Common pattern: `development`, `staging`, `production` +- Create environment-specific overrides for testing + +### 4. Self-Targeting Best Practices + +- Clear overrides after testing +- Use meaningful user IDs for team collaboration +- Document override purposes in team communications + +### 5. Audit and Compliance + +- Use `get_feature_audit_log_history` for compliance tracking +- Tag features appropriately for organization +- Include descriptions for documentation + +## Limitations + +Current MCP server does NOT support: +- Code analysis tools (usage scanning, cleanup) +- Git integration features +- Type generation +- Analytics and metrics +- MCP Resources (read-only data access) +- MCP Prompts (guided workflows) + +These features are planned for future releases. + +## Troubleshooting + +### Authentication Issues + +1. Verify environment variables are set correctly +2. Check CLI authentication: `dvc status` +3. Ensure project is selected: `dvc projects current` + +### Connection Issues + +1. Check DevCycle CLI is installed: `dvc --version` +2. Verify MCP server starts: `dvc-mcp` +3. Check AI assistant logs for connection errors + +### API Errors + +1. Verify API credentials have necessary permissions +2. Check project and organization access +3. Review error messages for specific validation issues + +For additional support, visit [DevCycle Documentation](https://docs.devcycle.com) or contact support. + +## Development & Local Testing + +For local development and testing of the MCP server: + +### Running from Source + +```bash +# Clone the repository +git clone https://github.com/DevCycleHQ/cli.git +cd cli + +# Install dependencies +yarn install + +# Build the project +yarn build + +# Run the MCP server +node dist/mcp/index.js +``` + +### Testing with AI Assistants + +For local testing, update your AI assistant configuration to point to the local build: + +```json +{ + "mcpServers": { + "devcycle": { + "command": "node", + "args": ["/path/to/cli/dist/mcp/index.js"] + } + } +} +``` + +### Debug Logging + +The MCP server logs all operations to stderr, which can be viewed in: +- Cursor: Developer Tools console +- Claude Desktop: Log files in the application support directory + +### Environment Variables for Development + +```bash +# Enable verbose logging +export DEBUG=1 + +# Use specific DevCycle API endpoint +export DEVCYCLE_API_URL="https://api.devcycle.com" +``` \ No newline at end of file diff --git a/docs/mcp/MCP_ABSTRACTIONS_SUMMARY.md b/docs/mcp/MCP_ABSTRACTIONS_SUMMARY.md new file mode 100644 index 000000000..9574a32d3 --- /dev/null +++ b/docs/mcp/MCP_ABSTRACTIONS_SUMMARY.md @@ -0,0 +1,170 @@ +# DevCycle MCP Abstractions Summary + +This document provides a quick reference of all recommended MCP abstractions for the DevCycle MCP server based on the CLI codebase analysis. + +## MCP Tools + +### 1. Feature Management + +- `list_features` - List all features with search/pagination +- `get_feature` - Get detailed feature information +- `create_feature` - Create new feature flag +- `update_feature` - Update existing feature +- `delete_feature` - Delete a feature + +### 2. Variable Management + +- `list_variables` - List all variables +- `get_variable` - Get variable details +- `create_variable` - Create new variable +- `update_variable` - Update variable properties +- `delete_variable` - Delete a variable + +### 3. Variation Management + +- `list_variations` - List variations for a feature +- `create_variation` - Create new variation +- `update_variation` - Update variation properties +- `get_variation` - Get specific variation details + +### 4. Targeting Management + +- `get_targeting_rules` - Get targeting for feature/environment +- `enable_targeting` - Enable targeting for environment +- `disable_targeting` - Disable targeting for environment +- `create_targeting_rule` - Create new targeting rule +- `update_targeting` - Update targeting rules +- `get_audiences` - List reusable audience definitions +- `validate_targeting_rule` - Validate targeting before applying + +### 5. Code Analysis + +- `analyze_variable_usage` - Comprehensive usage scanning with language detection +- `generate_usage_report` - Format usage analysis for documentation +- `cleanup_variable` - Replace variable with static value +- `analyze_pr_changes` - Analyze feature flag changes in PR +- `get_feature_history` - Git history of feature flag usage +- `find_unknown_variables` - Identify undefined variables in code + +### 6. Environment Management + +- `list_environments` - List project environments +- `get_environment` - Get environment details +- `create_environment` - Create new environment +- `update_environment` - Update environment settings +- `clone_environment` - Clone environment configuration +- `get_sdk_keys` - Retrieve SDK keys for environment + +### 7. Project & Organization + +- `list_projects` - List available projects +- `get_current_project` - Get current project +- `select_project` - Switch projects +- `list_organizations` - List available organizations +- `select_organization` - Switch organizations + +### 8. Override Management + +- `list_overrides` - List current overrides +- `set_override` - Set self-targeting override +- `clear_overrides` - Clear overrides +- `get_override` - Get specific override details + +### 9. Identity Management + +- `get_identity` - Get current DevCycle identity +- `update_identity` - Update identity for testing + +### 10. Type Generation + +- `generate_typescript_types` - Generate TypeScript definitions + +### 11. Analytics + +- `get_feature_usage_metrics` - Usage statistics +- `get_variation_distribution` - Variation serve distribution + +## MCP Resources + +### 1. Configuration + +- `devcycle://config/repo` - Repository configuration +- `devcycle://config/auth` - Auth status + +### 2. Project Data + +- `devcycle://project/features` - All features +- `devcycle://project/variables` - All variables +- `devcycle://project/environments` - All environments + +### 3. Analysis + +- `devcycle://analysis/usages` - Latest usage scan +- `devcycle://analysis/types` - Generated types +- `devcycle://analysis/unknown-variables` - Undefined variables +- `devcycle://analysis/parser-patterns` - Active regex patterns +- `devcycle://analysis/variable-aliases` - Variable mappings + +### 4. Targeting + +- `devcycle://targeting/rules/{feature}` - Feature targeting rules +- `devcycle://targeting/active-rules` - All active rules + +### 5. Overrides + +- `devcycle://overrides/current` - Active overrides +- `devcycle://overrides/available` - Available features + +### 6. Environment + +- `devcycle://environments/sdk-keys` - All SDK keys +- `devcycle://environments/comparison` - Environment diff + +### 7. Git Integration + +- `devcycle://git/feature-changes` - Recent changes +- `devcycle://git/pr-analysis` - PR flag analysis + +## MCP Prompts + +### 1. Feature Management + +- `create_feature_flag` - Guided feature creation +- `setup_targeting` - Configure targeting rules +- `plan_progressive_rollout` - Phased rollout strategy +- `create_killswitch` - Emergency rollback setup + +### 2. Code Integration + +- `implement_feature_flag` - Generate implementation code +- `cleanup_removed_flags` - Safe flag removal +- `refactor_flag_usage` - Best practices refactoring + +### 3. Analysis & Review + +- `analyze_feature_usage` - Comprehensive usage analysis +- `review_pr_flags` - PR review checklist +- `find_stale_flags` - Identify removable flags + +### 4. Testing + +- `setup_testing_overrides` - QA testing configuration +- `create_test_matrix` - Test case generation + +### 5. Documentation + +- `document_feature_flags` - Auto-generate docs +- `create_flag_runbook` - Operational procedures + +### 6. Migration + +- `migrate_feature_flags` - Project/environment migration + +## Priority Implementation Order + +1. **Core CRUD Tools** - Direct CLI command mappings +2. **Code Analysis Tools** - Leverage existing parsers +3. **Targeting & Overrides** - Essential for testing +4. **Resources** - Read-only data access +5. **Prompts** - AI-guided workflows +6. **Advanced Analytics** - Usage metrics and insights diff --git a/docs/mcp/MCP_PLANNING_DOCUMENT.md b/docs/mcp/MCP_PLANNING_DOCUMENT.md new file mode 100644 index 000000000..a40165073 --- /dev/null +++ b/docs/mcp/MCP_PLANNING_DOCUMENT.md @@ -0,0 +1,901 @@ +# DevCycle CLI to MCP Server Planning Document + +## Executive Summary + +This document outlines the plan to transform the DevCycle CLI into a Model Context Protocol (MCP) server, enabling AI coding assistants like Cursor and Claude Code to seamlessly interact with DevCycle's feature flag management capabilities. + +## What is MCP? + +The Model Context Protocol (MCP) is an open standard developed by Anthropic that standardizes how AI applications connect to data sources and tools. It acts as a "USB-C port for AI applications," providing a universal way to connect AI models to external systems. + +### Key MCP Concepts +- **Hosts**: AI applications that initiate connections (e.g., Cursor, Claude Code) +- **Clients**: Connectors within the host application +- **Servers**: Services that provide context and capabilities +- **Protocol**: JSON-RPC 2.0 based communication + +### MCP Server Capabilities +1. **Tools**: Functions that can be called by the LLM (with user approval) +2. **Resources**: File-like data that can be read by clients +3. **Prompts**: Pre-written templates for common tasks + +## Current DevCycle CLI Analysis + +### Architecture +- Built with oclif framework and TypeScript +- Uses DevCycle Management API with authentication via client credentials or SSO +- Supports repository configuration with `.devcycle/config.yml` + +### Major Features +1. **Feature Management**: Create, view, modify Features, Variables, Variations, and Targeting Rules +2. **Code Analysis**: Detect DevCycle Variable usages in codebase +3. **Self-Targeting Overrides**: Manage overrides for quick testing +4. **Type Generation**: Generate TypeScript definitions +5. **Repository Integration**: Git-aware diff and usage detection + +### Key Command Categories +- `features`: CRUD operations on features +- `variables`: CRUD operations on variables +- `variations`: Manage feature variations +- `targeting`: Configure targeting rules +- `environments`: Manage environments +- `projects`: Project management +- `organizations`: Organization selection +- `overrides`: Self-targeting overrides +- `usages`: Detect variable usage in code +- `diff`: Show changes between code versions +- `cleanup`: Replace variables with static values +- `generate`: Generate type definitions + +## MCP Server Design + +### 1. Architecture Overview + +``` +AI Coding Assistant (Cursor/Claude Code) + ↓ (MCP Client) + MCP Server (DevCycle) + ↓ (HTTP/API calls) + DevCycle Management API +``` + +### 2. Transport Mechanism + +**Primary**: stdio transport for local development +- Communicates via stdin/stdout +- Launched by AI host applications +- No network configuration required + +**Future**: HTTP transport for remote deployment +- RESTful endpoints with Server-Sent Events (SSE) +- OAuth 2.0 authentication +- Scalable for team/enterprise use + +### 3. MCP Tools Design + +#### 3.1 Feature Management Tools + +**`list_features`** +- Description: List all features in a project +- Parameters: `search` (optional), `page` (optional), `per_page` (optional) +- Returns: Array of feature objects with keys, names, descriptions, and status + +**`get_feature`** +- Description: Get detailed information about a specific feature +- Parameters: `feature_key` (required) +- Returns: Complete feature object with variables, variations, and targeting + +**`create_feature`** +- Description: Create a new feature flag +- Parameters: `key`, `name`, `description`, `type`, `variations` +- Returns: Created feature object + +**`update_feature`** +- Description: Update an existing feature +- Parameters: `feature_key`, `name`, `description`, `variations` +- Returns: Updated feature object + +#### 3.2 Variable Management Tools + +**`list_variables`** +- Description: List all variables in a project +- Parameters: `search`, `page`, `per_page` +- Returns: Array of variable objects + +**`get_variable`** +- Description: Get detailed variable information +- Parameters: `variable_key` +- Returns: Variable object with type, default value, and variations + +**`create_variable`** +- Description: Create a new variable +- Parameters: `key`, `name`, `description`, `type`, `default_value` +- Returns: Created variable object + +#### 3.3 Code Analysis Tools + +**`scan_variable_usages`** +- Description: Scan codebase for DevCycle variable usage +- Parameters: `include_patterns`, `exclude_patterns`, `client_names`, `match_patterns` +- Returns: Usage report with file locations and variable references + +**`diff_variable_usage`** +- Description: Compare variable usage between code versions +- Parameters: `base_ref`, `head_ref`, `include_patterns`, `exclude_patterns` +- Returns: Diff report showing added/removed variable usage + +#### 3.4 Targeting and Environment Tools + +**`list_environments`** +- Description: List project environments +- Returns: Array of environment objects + +**`get_targeting_rules`** +- Description: Get targeting rules for a feature +- Parameters: `feature_key`, `environment_key` +- Returns: Targeting configuration + +**`update_targeting`** +- Description: Update targeting rules +- Parameters: `feature_key`, `environment_key`, `targeting_rules` +- Returns: Updated targeting configuration + +#### 3.5 Override Management Tools + +**`list_overrides`** +- Description: List current self-targeting overrides +- Returns: Array of active overrides + +**`set_override`** +- Description: Set a self-targeting override +- Parameters: `variable_key`, `value`, `environment_key` +- Returns: Override confirmation + +**`clear_overrides`** +- Description: Clear all or specific overrides +- Parameters: `variable_keys` (optional) +- Returns: Cleared override confirmation + +#### 3.6 Project and Organization Tools + +**`list_projects`** +- Description: List available projects +- Returns: Array of project objects + +**`get_current_project`** +- Description: Get currently selected project +- Returns: Current project information + +**`select_project`** +- Description: Switch to a different project +- Parameters: `project_key` +- Returns: Project selection confirmation + +### 3.7 Enhanced MCP Tools (Based on Codebase Analysis) + +Based on my comprehensive review of the DevCycle CLI codebase, I recommend the following enhanced and additional MCP tools: + +#### 3.7.1 Advanced Code Analysis Tools + +**`analyze_variable_usage`** +- Description: Comprehensive variable usage analysis with language-specific parsing +- Parameters: + - `include_patterns`: File glob patterns to include + - `exclude_patterns`: File glob patterns to exclude + - `client_names`: Additional SDK client names to detect + - `match_patterns`: Custom regex patterns by file extension + - `show_only_unknown`: Filter to show only unknown variables +- Returns: Detailed usage report with file locations, line numbers, and code context +- Note: Supports JavaScript, TypeScript, React, Python, Ruby, Go, Java, C#, PHP, Dart, iOS, and Android + +**`generate_usage_report`** +- Description: Generate a formatted usage report for documentation or review +- Parameters: + - `format`: Output format ('json', 'markdown', 'console') + - `output_file`: Optional file path for output +- Returns: Formatted usage report + +**`cleanup_variable`** +- Description: Replace DevCycle variable with static value in code +- Parameters: + - `variable_key`: Variable to replace + - `replacement_value`: Value to use as replacement + - `value_type`: Type of replacement value ('String', 'Boolean', 'Number', 'JSON') + - `include_patterns`: Files to include + - `exclude_patterns`: Files to exclude +- Returns: List of files modified with before/after preview + +#### 3.7.2 Identity and User Profile Tools + +**`get_identity`** +- Description: Get current DevCycle identity information +- Returns: User profile with identity settings + +**`update_identity`** +- Description: Update DevCycle identity for testing +- Parameters: + - `user_id`: SDK-associated user ID + - `email`: User email + - `name`: User name + - `country`: User country + - `custom_data`: Additional custom properties +- Returns: Updated identity information + +#### 3.7.3 Variation Management Tools + +**`list_variations`** +- Description: List all variations for a feature +- Parameters: `feature_key` +- Returns: Array of variation objects with keys and values + +**`create_variation`** +- Description: Create a new variation for a feature +- Parameters: + - `feature_key`: Feature to add variation to + - `key`: Unique variation key + - `name`: Human-readable name + - `variables`: Variable values for this variation +- Returns: Created variation object + +**`update_variation`** +- Description: Update an existing variation +- Parameters: + - `feature_key`: Feature containing the variation + - `variation_key`: Variation to update + - `name`: New name + - `variables`: Updated variable values +- Returns: Updated variation object + +#### 3.7.4 Advanced Targeting Tools + +**`create_targeting_rule`** +- Description: Create a new targeting rule with audience definition +- Parameters: + - `feature_key`: Feature for the rule + - `environment_key`: Environment to apply rule + - `name`: Rule name + - `audience_filters`: Audience definition filters + - `serve_variation`: Variation to serve + - `rollout_percentage`: Optional percentage rollout +- Returns: Created targeting rule + +**`get_audiences`** +- Description: List reusable audience definitions +- Returns: Array of audience objects with filters + +**`validate_targeting_rule`** +- Description: Validate a targeting rule before applying +- Parameters: + - `audience_filters`: Proposed audience definition + - `test_users`: Sample users to test against +- Returns: Validation results with matched users + +#### 3.7.5 Git Integration Tools + +**`analyze_pr_changes`** +- Description: Analyze feature flag changes in a pull request +- Parameters: + - `base_ref`: Base branch reference + - `head_ref`: Head branch reference + - `pr_link`: Optional PR link for enhanced formatting +- Returns: Summary of added/removed/modified feature flags + +**`get_feature_history`** +- Description: Get git history of feature flag usage +- Parameters: + - `feature_key`: Feature to analyze + - `days_back`: Number of days to look back +- Returns: Timeline of feature flag changes + +#### 3.7.6 Type Generation Tools + +**`generate_typescript_types`** +- Description: Generate TypeScript type definitions for features +- Parameters: + - `output_path`: Where to write the generated types + - `include_descriptions`: Include JSDoc comments +- Returns: Generated type definition content + +#### 3.7.7 Environment Management Tools + +**`clone_environment`** +- Description: Clone an environment with all settings +- Parameters: + - `source_environment`: Environment to clone from + - `new_key`: Key for new environment + - `new_name`: Name for new environment + - `include_targeting`: Whether to copy targeting rules +- Returns: Created environment object + +**`get_sdk_keys`** +- Description: Retrieve SDK keys for an environment +- Parameters: + - `environment_key`: Environment to get keys for + - `key_type`: Type of key ('mobile', 'server', 'client') +- Returns: SDK key information + +#### 3.7.8 Analytics and Metrics Tools + +**`get_feature_usage_metrics`** +- Description: Get usage metrics for a feature +- Parameters: + - `feature_key`: Feature to analyze + - `environment_key`: Optional environment filter + - `start_date`: Start of date range + - `end_date`: End of date range + - `period`: Aggregation period ('hour', 'day', 'month') +- Returns: Usage statistics and evaluation counts + +**`get_variation_distribution`** +- Description: Get distribution of variation serves +- Parameters: + - `feature_key`: Feature to analyze + - `environment_key`: Environment to analyze + - `time_range`: Time range for analysis +- Returns: Percentage distribution by variation + +### 4. MCP Resources Design + +#### 4.1 Configuration Resources + +**`config://repo`** +- URI: `devcycle://config/repo` +- Description: Current repository configuration +- Content: YAML configuration file content + +**`config://auth`** +- URI: `devcycle://config/auth` +- Description: Authentication status and configuration +- Content: Current auth state (without sensitive data) + +#### 4.2 Project Data Resources + +**`project://features`** +- URI: `devcycle://project/features` +- Description: All features in current project +- Content: JSON array of feature objects + +**`project://variables`** +- URI: `devcycle://project/variables` +- Description: All variables in current project +- Content: JSON array of variable objects + +#### 4.3 Code Analysis Resources + +**`analysis://usages`** +- URI: `devcycle://analysis/usages` +- Description: Latest variable usage scan results +- Content: JSON report of variable usage in codebase + +**`analysis://types`** +- URI: `devcycle://analysis/types` +- Description: Generated TypeScript type definitions +- Content: TypeScript definition file + +### 4.4 Enhanced Resources (Based on Codebase Analysis) + +Based on the codebase review, I recommend adding these resources: + +#### 4.4.1 Advanced Analysis Resources + +**`analysis://unknown-variables`** +- URI: `devcycle://analysis/unknown-variables` +- Description: Variables found in code but not defined in DevCycle +- Content: List of potentially orphaned or mistyped variables + +**`analysis://parser-patterns`** +- URI: `devcycle://analysis/parser-patterns` +- Description: Active regex patterns used for variable detection +- Content: Patterns organized by file extension with examples + +**`analysis://variable-aliases`** +- URI: `devcycle://analysis/variable-aliases` +- Description: Configured variable aliases from repo config +- Content: Mapping of code aliases to DevCycle variable keys + +#### 4.4.2 Targeting Resources + +**`targeting://rules/:feature`** +- URI: `devcycle://targeting/rules/{feature_key}` +- Description: All targeting rules for a specific feature +- Content: Targeting configuration across all environments + +**`targeting://active-rules`** +- URI: `devcycle://targeting/active-rules` +- Description: All active targeting rules in the project +- Content: Filtered list of enabled targeting rules + +#### 4.4.3 Override Resources + +**`overrides://current`** +- URI: `devcycle://overrides/current` +- Description: Current user's self-targeting overrides +- Content: Active overrides by feature and environment + +**`overrides://available`** +- URI: `devcycle://overrides/available` +- Description: Features available for override testing +- Content: List of features with their variations + +#### 4.4.4 Environment Resources + +**`environments://sdk-keys`** +- URI: `devcycle://environments/sdk-keys` +- Description: SDK keys for all environments +- Content: Keys organized by environment and SDK type + +**`environments://comparison`** +- URI: `devcycle://environments/comparison` +- Description: Side-by-side environment configuration comparison +- Content: Differences in features, variables, and targeting + +#### 4.4.5 Git Integration Resources + +**`git://feature-changes`** +- URI: `devcycle://git/feature-changes` +- Description: Recent git changes affecting feature flags +- Content: Commits and diffs with feature flag modifications + +**`git://pr-analysis`** +- URI: `devcycle://git/pr-analysis` +- Description: Feature flag analysis for current PR +- Content: Added/removed/modified flags with risk assessment + +### 5. MCP Prompts Design + +#### 5.1 Feature Management Prompts + +**`create_feature_flag`** +- Description: "Create a new feature flag with best practices" +- Template: Guides through feature creation with naming conventions +- Parameters: `feature_name`, `description`, `feature_type` + +**`setup_targeting`** +- Description: "Set up targeting rules for a feature" +- Template: Helps configure audience targeting +- Parameters: `feature_key`, `environment`, `targeting_strategy` + +#### 5.2 Code Integration Prompts + +**`implement_feature_flag`** +- Description: "Generate code to implement a feature flag" +- Template: Provides code snippets for different SDKs +- Parameters: `variable_key`, `language`, `default_value` + +**`cleanup_removed_flags`** +- Description: "Clean up removed feature flags from code" +- Template: Guides through safe flag removal process +- Parameters: `variable_keys`, `replacement_values` + +### 5.3 Enhanced Prompts (Based on Codebase Analysis) + +#### 5.3.1 Analysis and Review Prompts + +**`analyze_feature_usage`** +- Description: "Analyze how a feature flag is used in the codebase" +- Template: Comprehensive usage analysis with recommendations +- Parameters: `feature_key`, `include_git_history`, `check_dependencies` + +**`review_pr_flags`** +- Description: "Review feature flag changes in a pull request" +- Template: Systematic review checklist for flag changes +- Parameters: `pr_url`, `check_targeting`, `check_rollback_plan` + +**`find_stale_flags`** +- Description: "Identify feature flags that may be ready for removal" +- Template: Analysis of flag age, usage, and rollout status +- Parameters: `days_old`, `check_full_rollout`, `check_code_usage` + +#### 5.3.2 Testing and Override Prompts + +**`setup_testing_overrides`** +- Description: "Configure overrides for testing feature variations" +- Template: Step-by-step override setup for QA testing +- Parameters: `feature_key`, `test_scenarios`, `environments` + +**`create_test_matrix`** +- Description: "Generate test cases for feature flag variations" +- Template: Comprehensive test matrix generation +- Parameters: `feature_key`, `user_segments`, `include_edge_cases` + +#### 5.3.3 Migration and Refactoring Prompts + +**`migrate_feature_flags`** +- Description: "Migrate feature flags between projects or environments" +- Template: Safe migration process with validation steps +- Parameters: `source_project`, `target_project`, `features_to_migrate` + +**`refactor_flag_usage`** +- Description: "Refactor feature flag usage to follow best practices" +- Template: Code refactoring guide with patterns +- Parameters: `scan_directory`, `fix_patterns`, `update_aliases` + +#### 5.3.4 Rollout Strategy Prompts + +**`plan_progressive_rollout`** +- Description: "Plan a progressive feature rollout strategy" +- Template: Phased rollout planning with targeting rules +- Parameters: `feature_key`, `rollout_phases`, `success_metrics` + +**`create_killswitch`** +- Description: "Set up an emergency kill switch for a feature" +- Template: Rapid rollback configuration +- Parameters: `feature_key`, `alert_conditions`, `rollback_variation` + +#### 5.3.5 Documentation Prompts + +**`document_feature_flags`** +- Description: "Generate documentation for feature flags" +- Template: Auto-generated flag documentation +- Parameters: `output_format`, `include_examples`, `include_metrics` + +**`create_flag_runbook`** +- Description: "Create an operational runbook for a feature" +- Template: Operational procedures and troubleshooting +- Parameters: `feature_key`, `include_monitoring`, `include_rollback` + +## Implementation Considerations + +Based on my analysis of the DevCycle CLI codebase, here are key implementation considerations for the MCP server: + +### 1. Architecture Patterns + +**Leverage Existing Infrastructure** +- Reuse the existing command structure from `src/commands/` +- Utilize the Zod-based API client (`src/api/zodClient.ts`) for type safety +- Maintain compatibility with existing authentication mechanisms + +**Parser Reusability** +- The language-specific parsers in `src/utils/parsers/` are highly sophisticated +- Support for 12+ languages with custom regex patterns +- Can be directly integrated into MCP tools for code analysis + +**Configuration Management** +- Respect existing `.devcycle/config.yml` structure +- Support both repository and user-level configurations +- Handle variable aliases and custom match patterns + +### 2. Tool Implementation Strategy + +**Tool Categories by Priority** +1. **Core CRUD Operations** (Week 1-2) + - Direct mapping from existing commands + - Minimal transformation required + +2. **Code Analysis Tools** (Week 3-4) + - Leverage existing parser infrastructure + - Add MCP-specific formatting for results + +3. **Interactive Features** (Week 5-6) + - Transform prompt-based flows to parameter-based tools + - Maintain validation logic from interactive commands + +4. **Advanced Features** (Week 7-8) + - Git integration using existing diff utilities + - Analytics tools requiring new API integrations + +### 3. Authentication Architecture + +**Token Management** +- Reuse `TokenCache` and `ApiAuth` classes +- Support both SSO and client credentials +- Implement refresh logic for long-running sessions + +**Multi-Project Support** +- Allow project switching within MCP session +- Cache project-specific data appropriately +- Handle organization context switches + +### 4. Error Handling Patterns + +**Consistent Error Responses** +- Map CLI error types to MCP error codes +- Preserve detailed error messages from API +- Include actionable suggestions in error responses + +**Validation Layers** +- Client-side validation using Zod schemas +- API validation feedback +- File system and git operation errors + +### 5. Performance Optimizations + +**Caching Strategy** +- Cache feature and variable lists +- Implement smart cache invalidation +- Reuse parser results for repeated operations + +**Batch Operations** +- Use existing `batchRequests` utility +- Implement parallel processing for bulk operations +- Optimize file system scanning for large codebases + +### 6. Data Transformation + +**Response Formatting** +- Transform CLI table output to structured JSON +- Preserve tree structures for targeting rules +- Flatten nested objects for easier consumption + +**Input Normalization** +- Accept both keys and IDs for resources +- Implement fuzzy matching for user convenience +- Validate inputs against cached resource lists + +## Implementation Plan + +### Phase 1: Core MCP Server Setup (Week 1-2) +1. **Project Structure** + - Create new `src/mcp/` directory + - Set up MCP server entry point + - Configure TypeScript build for MCP server + +2. **Basic MCP Implementation** + - Implement stdio transport + - Set up capability negotiation + - Create base server class extending existing CLI base + +3. **Authentication Integration** + - Reuse existing authentication system + - Handle token management + - Project selection integration + +### Phase 2: Essential Tools Implementation (Week 3-4) +1. **Feature Management Tools** + - `list_features`, `get_feature` + - `create_feature`, `update_feature` + - Error handling and validation + +2. **Variable Management Tools** + - `list_variables`, `get_variable` + - `create_variable` + - Type safety and schema validation + +3. **Project Management Tools** + - `list_projects`, `get_current_project` + - `select_project` + +### Phase 3: Code Analysis Tools (Week 5-6) +1. **Usage Analysis** + - `scan_variable_usages` tool + - Reuse existing parsing logic + - File filtering and pattern matching + +2. **Diff Analysis** + - `diff_variable_usage` tool + - Git integration for version comparison + - Change detection and reporting + +### Phase 4: Advanced Features (Week 7-8) +1. **Targeting and Overrides** + - Targeting rule management tools + - Override management tools + - Environment-specific operations + +2. **Resources Implementation** + - Configuration resources + - Project data resources + - Analysis result resources + +### Phase 5: Prompts and Polish (Week 9-10) +1. **Prompt Templates** + - Feature creation prompts + - Code implementation prompts + - Best practice guidance + +2. **Testing and Documentation** + - Comprehensive testing + - Integration testing with Claude Code/Cursor + - Documentation and examples + +### Phase 6: HTTP Transport (Week 11-12) +1. **HTTP Server Implementation** + - Server-Sent Events support + - RESTful endpoint structure + - Session management + +2. **Authentication and Security** + - OAuth 2.0 integration + - API key authentication + - Rate limiting and security + +## Technical Implementation Details + +### 1. Project Structure +``` +src/ +├── mcp/ +│ ├── server.ts # Main MCP server implementation +│ ├── tools/ # MCP tool implementations +│ │ ├── features.ts +│ │ ├── variables.ts +│ │ ├── analysis.ts +│ │ └── index.ts +│ ├── resources/ # MCP resource implementations +│ │ ├── config.ts +│ │ ├── project.ts +│ │ └── index.ts +│ ├── prompts/ # MCP prompt templates +│ │ ├── feature-management.ts +│ │ ├── code-integration.ts +│ │ └── index.ts +│ ├── transports/ # Transport implementations +│ │ ├── stdio.ts +│ │ ├── http.ts +│ │ └── index.ts +│ └── index.ts # MCP server entry point +``` + +### 2. Dependencies +```json +{ + "dependencies": { + "@modelcontextprotocol/sdk": "latest", + "zod": "^3.24.2" + } +} +``` + +### 3. Configuration Integration +- Reuse existing `.devcycle/config.yml` structure +- Extend configuration for MCP-specific settings +- Maintain backward compatibility with CLI + +### 4. Error Handling Strategy +- Wrap API errors in MCP-compatible error responses +- Provide helpful error messages for common issues +- Graceful degradation when API is unavailable + +### 5. Performance Considerations +- Cache frequently accessed data (features, variables) +- Implement request batching for bulk operations +- Optimize file scanning for large codebases + +## Integration Examples + +### 1. Cursor Integration +```json +{ + "mcpServers": { + "devcycle": { + "command": "node", + "args": ["/path/to/devcycle-mcp-server/dist/mcp/index.js"] + } + } +} +``` + +### 2. Claude Code Integration +```bash +claude mcp add devcycle npx @devcycle/cli mcp-server +``` + +### 3. Usage Scenarios + +**Scenario 1: Creating a Feature Flag** +``` +User: "Create a feature flag for the new checkout flow" +AI: Uses create_feature tool → Creates feature with best practices +AI: Uses implement_feature_flag prompt → Generates implementation code +``` + +**Scenario 2: Code Review Analysis** +``` +User: "What feature flags are being added in this PR?" +AI: Uses diff_variable_usage tool → Analyzes code changes +AI: Provides summary of new feature flag usage +``` + +**Scenario 3: Cleanup Assistance** +``` +User: "Help me remove the old_checkout_flow feature flag" +AI: Uses scan_variable_usages → Finds all usage locations +AI: Uses cleanup_removed_flags prompt → Guides through removal +``` + +## Benefits and Value Proposition + +### For Developers +- **Seamless Integration**: Feature flag management directly in coding environment +- **Context-Aware Assistance**: AI understands current project state +- **Code Analysis**: Automatic detection of feature flag usage +- **Best Practices**: Built-in guidance for feature flag implementation + +### For Teams +- **Consistency**: Standardized feature flag practices across team +- **Visibility**: Easy access to feature flag status during code review +- **Efficiency**: Reduced context switching between tools +- **Documentation**: Automatic documentation of feature flag usage + +### For DevCycle +- **Market Expansion**: First feature flag platform with native AI integration +- **Developer Experience**: Superior DX compared to competitors +- **Ecosystem Growth**: Enable third-party integrations through MCP +- **Competitive Advantage**: Unique positioning in the market + +## Risk Assessment and Mitigation + +### Technical Risks +1. **MCP Protocol Changes**: Monitor MCP specification updates +2. **Performance Issues**: Implement caching and optimization +3. **Authentication Complexity**: Reuse proven CLI auth patterns + +### Product Risks +1. **User Adoption**: Provide clear documentation and examples +2. **Feature Completeness**: Prioritize most-used CLI features +3. **Maintenance Overhead**: Design for extensibility and maintainability + +### Mitigation Strategies +- Start with stdio transport for simplicity +- Reuse existing CLI codebase extensively +- Implement comprehensive testing +- Create detailed documentation and examples + +## Success Metrics + +### Technical Metrics +- MCP server response time < 500ms for most operations +- 99% uptime for HTTP transport +- Zero authentication-related security issues + +### Adoption Metrics +- Number of developers using MCP integration +- Frequency of MCP tool usage +- User satisfaction scores + +### Business Metrics +- Increased DevCycle API usage +- Improved developer onboarding time +- Positive feedback from AI coding assistant communities + +## Key Recommendations + +Based on the comprehensive codebase analysis, here are the critical recommendations for the DevCycle MCP server implementation: + +### 1. Maximize Code Reuse + +The existing CLI codebase is well-architected and can be heavily reused: +- **Direct Command Mapping**: Most CLI commands can be transformed into MCP tools with minimal changes +- **Parser Infrastructure**: The sophisticated language parsers are a competitive advantage - use them as-is +- **API Client**: The Zod-based client provides excellent type safety and validation + +### 2. Focus on Developer Workflows + +Prioritize tools that support common developer tasks: +1. **Quick Flag Creation**: Simple tool to create feature with sensible defaults +2. **Code Analysis**: Immediate feedback on flag usage in current code +3. **PR Review**: Automated analysis of feature flag changes in pull requests +4. **Test Override**: Easy variation testing without code changes +5. **Safe Cleanup**: Guided removal of obsolete flags + +### 3. Leverage AI Capabilities + +Design tools that benefit from AI context: +- **Smart Suggestions**: Let AI suggest flag names based on code context +- **Automated Documentation**: Generate flag documentation from usage patterns +- **Risk Assessment**: AI can evaluate the risk of flag changes +- **Test Scenarios**: Generate comprehensive test cases for variations + +### 4. Maintain Compatibility + +Ensure the MCP server works seamlessly with existing DevCycle ecosystem: +- Support existing `.devcycle/config.yml` format +- Use the same authentication mechanisms +- Maintain consistent naming conventions +- Preserve CLI command structure where logical + +### 5. Performance Considerations + +Optimize for responsive AI interactions: +- Implement aggressive caching for read operations +- Use batch APIs for bulk operations +- Pre-parse common file types on startup +- Stream large results for better UX + +## Conclusion + +Converting the DevCycle CLI into an MCP server represents a significant opportunity to pioneer AI-native developer tooling in the feature flag space. By leveraging the existing CLI codebase and MCP's standardized protocol, we can deliver a seamless experience that enhances developer productivity while maintaining DevCycle's position as an innovative platform. + +The phased implementation approach ensures manageable development cycles while delivering value incrementally. The focus on reusing existing CLI functionality minimizes risk while maximizing the potential for rapid deployment and adoption. + +This initiative positions DevCycle at the forefront of the AI-assisted development workflow revolution, providing a competitive advantage that will be difficult for competitors to replicate. \ No newline at end of file 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/package.json b/package.json index 1207c9703..b937944df 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,8 @@ "description": "DevCycle CLI Tool", "author": "support@devcycle.com", "bin": { - "dvc": "./bin/run" + "dvc": "./bin/run", + "dvc-mcp": "./bin/mcp" }, "homepage": "https://github.com/DevCycleHQ/cli", "license": "MIT", @@ -17,7 +18,8 @@ "/oclif.manifest.json" ], "dependencies": { - "@babel/parser": "^7.27.0", + "@babel/parser": "^7.28.0", + "@modelcontextprotocol/sdk": "^1.15.0", "@oclif/core": "^2.16.0", "@oclif/plugin-autocomplete": "^2.3.10", "@oclif/plugin-help": "^6.2.27", @@ -46,6 +48,13 @@ "zod": "^3.24.2" }, "devDependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/core": "^7.28.0", + "@babel/generator": "^7.28.0", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.0", + "@babel/types": "^7.28.0", "@eslint/js": "^9.18.0", "@oclif/test": "^2.5.6", "@types/chai": "^5.2.2", diff --git a/src/api/apiClient.ts b/src/api/apiClient.ts index f670e7796..1dc611c65 100644 --- a/src/api/apiClient.ts +++ b/src/api/apiClient.ts @@ -12,13 +12,17 @@ export const setDVCReferrer = ( version: string, caller = 'cli', ): void => { - axiosClient.defaults.headers.common['dvc-referrer'] = 'cli' + axiosClient.defaults.headers.common['dvc-referrer'] = caller + + // Ensure we have valid values before stringifying + const metadata = { + command: command || 'unknown', + version: version || 'unknown', + caller: caller || 'cli', + } + axiosClient.defaults.headers.common['dvc-referrer-metadata'] = - JSON.stringify({ - command, - version, - caller, - }) + JSON.stringify(metadata) } axiosClient.interceptors.response.use( @@ -28,10 +32,22 @@ axiosClient.interceptors.response.use( (error: AxiosError) => { let isCallerCli = false if (error.config) { - const parsedDvcReferrerMetadata = JSON.parse( - error.config.headers['dvc-referrer-metadata'], - ) - isCallerCli = parsedDvcReferrerMetadata.caller === 'cli' + try { + const referrerMetadata = + error.config.headers['dvc-referrer-metadata'] + if (referrerMetadata && typeof referrerMetadata === 'string') { + const parsedDvcReferrerMetadata = + JSON.parse(referrerMetadata) + isCallerCli = parsedDvcReferrerMetadata.caller === 'cli' + } + } catch (parseError) { + // If we can't parse the referrer metadata, assume it's not from CLI + console.error( + 'Failed to parse dvc-referrer-metadata:', + parseError, + ) + isCallerCli = false + } } if (error.response?.status === 401) { @@ -93,10 +109,18 @@ export const errorMap = (issue: ZodIssueOptionalMessage, ctx: ErrorMapCtx) => { } } -export const apiClient = createApiClient(BASE_URL, { +// TLDR: the inferred TS schema was too big, so this is a workaround to fix it. +// Create intermediate type alias to break complex type inference +const _createApiClient = createApiClient +type ApiClientType = ReturnType<typeof _createApiClient> + +// Create the actual instance with explicit type annotation +const apiClient: ApiClientType = _createApiClient(BASE_URL, { axiosInstance: axiosClient, validate: 'request', -}) +}) as ApiClientType + +export { apiClient } export default apiClient export const v2ApiClient = createV2ApiClient(BASE_URL) diff --git a/src/api/features.ts b/src/api/features.ts index 2c242b8d7..ac5581ed0 100644 --- a/src/api/features.ts +++ b/src/api/features.ts @@ -4,7 +4,12 @@ import { apiClient as apiV1Client, axiosClient, } from './apiClient' -import { CreateFeatureParams, Feature, UpdateFeatureParams } from './schemas' +import { + CreateFeatureParams, + Feature, + UpdateFeatureParams, + UpdateFeatureStatusParams, +} from './schemas' import 'reflect-metadata' import { buildHeaders } from './common' @@ -14,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<Feature[]> => { const response = await apiClient.get(FEATURE_URL, { @@ -83,6 +98,22 @@ export const updateFeature = async ( }) } +export const updateFeatureStatus = async ( + token: string, + project_id: string, + feature_id: string, + params: UpdateFeatureStatusParams, +): Promise<Feature> => { + const response = await axiosClient.patch( + `/v1/projects/${project_id}/features/${feature_id}/status`, + params, + { + headers: buildHeaders(token), + }, + ) + return response.data +} + export const deleteFeature = async ( token: string, project_id: string, @@ -156,3 +187,86 @@ const generatePaginatedFeatureUrl = ( ): string => { return `/v1/projects/${project_id}/features?perPage=${perPage}&page=${page}&status=${status}` } + +export const getFeatureAuditLogHistory = async ( + token: string, + projectKey: string, + featureKey: string, + daysBack = 30, +): Promise<{ + timeline: Array<{ + id: string + timestamp: string + action: string + actor: { + name: string + email?: string + } + resource: { + type: string + name: string + key: string + } + changes: Array<{ + field: string + oldValue: unknown + newValue: unknown + }> + environment?: string + }> +}> => { + try { + // Calculate the date threshold + const sinceDate = new Date() + sinceDate.setDate(sinceDate.getDate() - daysBack) + const startDate = sinceDate.toISOString() + + const params = { + startDate, + perPage: 100, + page: 1, + } + console.error(`feature history params: ${JSON.stringify(params)}`) + + // Use the audit log API to get feature history + const response = await axiosClient.get( + `/v1/projects/${projectKey}/features/${featureKey}/audit`, + { + headers: buildHeaders(token), + params, + }, + ) + console.error( + `feature history response: ${JSON.stringify(response.data)}`, + ) + + const auditLogs = response.data || [] + + // Transform audit log entries to timeline format + const timeline = auditLogs.map((entry: any) => ({ + id: entry._id || entry.id, + timestamp: entry.createdAt || entry.timestamp, + action: entry.action || 'unknown', + actor: { + name: entry.user?.name || entry.actor?.name || 'Unknown', + email: entry.user?.email || entry.actor?.email, + }, + resource: { + type: entry.resourceType || 'feature', + name: entry.resourceName || featureKey, + key: entry.resourceKey || featureKey, + }, + changes: entry.changes || [], + environment: entry.environment?.key || entry.environmentKey, + })) + + return { timeline } + } catch (error) { + // If audit log API fails, return empty result + console.warn( + 'Failed to fetch feature history from audit log:', + error instanceof Error ? error.message : 'Unknown error', + ) + return { timeline: [] } + } +} diff --git a/src/api/projects.ts b/src/api/projects.ts index 74a7b5c68..0e4877201 100644 --- a/src/api/projects.ts +++ b/src/api/projects.ts @@ -1,29 +1,10 @@ -import { IsNotEmpty, IsOptional, IsString } from 'class-validator' import apiClient from './apiClient' import { buildHeaders } from './common' - -export class CreateProjectParams { - @IsString() - @IsNotEmpty() - name: string - - @IsString() - @IsOptional() - description: string - - @IsNotEmpty() - @IsString() - key: string -} - -export class GetProjectsParams { - @IsString() - @IsOptional() - sortBy: string - - @IsOptional() - sortOrder: 'asc' | 'desc' -} +import { + CreateProjectParams, + UpdateProjectParams, + GetProjectsParams, +} from './schemas' const BASE_URL = '/v1/projects' @@ -54,3 +35,16 @@ export const createProject = async ( headers: buildHeaders(token), }) } + +export const updateProject = async ( + token: string, + projectKey: string, + params: UpdateProjectParams, +) => { + return apiClient.patch(`${BASE_URL}/:key`, params, { + headers: buildHeaders(token), + params: { + key: projectKey, + }, + }) +} diff --git a/src/api/schemas.ts b/src/api/schemas.ts index 9e5be7e58..fcc69b337 100644 --- a/src/api/schemas.ts +++ b/src/api/schemas.ts @@ -12,6 +12,15 @@ export type Audience = z.infer<typeof schemas.Audience> export type Target = z.infer<typeof schemas.Target> export type Override = z.infer<typeof schemas.Override> export type CustomProperty = z.infer<typeof schemas.CustomProperty> +export type GetProjectsParams = z.infer<typeof schemas.GetProjectsParams> +export const GetProjectsParams = schemas.GetProjectsParams + +export type CreateProjectParams = z.infer<typeof schemas.CreateProjectDto> +export const CreateProjectDto = schemas.CreateProjectDto + +export type UpdateProjectParams = z.infer<typeof schemas.UpdateProjectDto> +export const UpdateProjectDto = schemas.UpdateProjectDto + export type CreateEnvironmentParams = z.infer< typeof schemas.CreateEnvironmentDto > @@ -28,6 +37,13 @@ export const CreateFeatureDto = schemas.CreateFeatureDto export type UpdateFeatureParams = z.infer<typeof schemas.UpdateFeatureDto> export const UpdateFeatureDto = schemas.UpdateFeatureDto +export const UpdateFeatureStatusDto = z.object({ + status: z.enum(['active', 'complete', 'archived']), + staticVariation: z.string().optional(), +}) + +export type UpdateFeatureStatusParams = z.infer<typeof UpdateFeatureStatusDto> + export type CreateVariableParams = z.infer<typeof schemas.CreateVariableDto> export const CreateVariableDto = schemas.CreateVariableDto diff --git a/src/api/variables.ts b/src/api/variables.ts index 9350533de..87bef5a7b 100644 --- a/src/api/variables.ts +++ b/src/api/variables.ts @@ -49,14 +49,41 @@ export const updateVariable = async ( }) } +export const deleteVariable = async ( + token: string, + project_id: string, + variableKey: string, +) => { + return apiClient.delete('/v1/projects/:project/variables/:key', undefined, { + headers: { + 'Content-Type': 'application/json', + Authorization: token, + }, + params: { + project: project_id, + key: variableKey, + }, + }) +} + 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/api/zodClient.ts b/src/api/zodClient.ts index 46a8258dd..4078fef54 100644 --- a/src/api/zodClient.ts +++ b/src/api/zodClient.ts @@ -39,6 +39,24 @@ const ProjectSettings = z.object({ obfuscation: ObfuscationSettings, staleness: StalenessSettings, }) +const GetProjectsParams = z.object({ + page: z.number().gte(1).optional().default(1), + perPage: z.number().gte(1).lte(1000).optional().default(100), + sortBy: z + .enum([ + 'createdAt', + 'updatedAt', + 'name', + 'key', + 'createdBy', + 'propertyKey', + ]) + .optional() + .default('createdAt'), + sortOrder: z.enum(['asc', 'desc']).optional().default('desc'), + search: z.string().optional(), + createdBy: z.string().optional(), +}) const CreateProjectDto = z.object({ name: z.string().max(100), key: z @@ -184,29 +202,59 @@ const UpdateEnvironmentDto = z const GenerateSdkTokensDto = z .object({ client: z.boolean(), server: z.boolean(), mobile: z.boolean() }) .partial() -const AllFilter = z.object({ type: z.literal('all').default('all') }) -const OptInFilter = z.object({ type: z.literal('optIn').default('optIn') }) +const AllFilter = z.object({ + type: z.literal('all').default('all'), + _audiences: z.array(z.string()).optional(), + values: z.array(z.string()).optional(), +}) +const OptInFilter = z.object({ + type: z.literal('optIn').default('optIn'), + _audiences: z.array(z.string()).optional(), + values: z.array(z.string()).optional(), +}) const UserFilter = z.object({ subType: z.enum(['user_id', 'email', 'platform', 'deviceModel']), - comparator: z.enum(['=', '!=', 'exist', '!exist', 'contain', '!contain']), + comparator: z.enum([ + '=', + '!=', + 'exist', + '!exist', + 'contain', + '!contain', + 'endWith', + 'startWith', + ]), values: z.array(z.string()).optional(), + _audiences: z.array(z.string()).optional(), type: z.literal('user').default('user'), }) const UserCountryFilter = z.object({ subType: z.literal('country').default('country'), - comparator: z.enum(['=', '!=', 'exist', '!exist', 'contain', '!contain']), + comparator: z.enum([ + '=', + '!=', + 'exist', + '!exist', + 'contain', + '!contain', + 'endWith', + 'startWith', + ]), values: z.array(z.string()), + _audiences: z.array(z.string()).optional(), type: z.literal('user').default('user'), }) const UserAppVersionFilter = z.object({ comparator: z.enum(['=', '!=', '>', '>=', '<', '<=', 'exist', '!exist']), values: z.array(z.string()).optional(), + _audiences: z.array(z.string()).optional(), type: z.literal('user').default('user'), subType: z.literal('appVersion').default('appVersion'), }) const UserPlatformVersionFilter = z.object({ comparator: z.enum(['=', '!=', '>', '>=', '<', '<=', 'exist', '!exist']), values: z.array(z.string()).optional(), + _audiences: z.array(z.string()).optional(), type: z.literal('user').default('user'), subType: z.literal('platformVersion').default('platformVersion'), }) @@ -226,6 +274,7 @@ const UserCustomFilter = z.object({ dataKey: z.string().min(1), dataKeyType: z.enum(['String', 'Boolean', 'Number']), values: z.array(z.union([z.boolean(), z.string(), z.number()])).optional(), + _audiences: z.array(z.string()).optional(), type: z.literal('user').default('user'), subType: z.literal('customData').default('customData'), }) @@ -546,8 +595,10 @@ const Target = z.object({ _id: z.string(), name: z.string().optional(), audience: TargetAudience, + filters: z.array(z.any()).optional(), rollout: Rollout.nullable().optional(), distribution: z.array(TargetDistribution), + bucketingKey: z.string().optional(), }) const FeatureConfig = z.object({ _feature: z.string(), @@ -558,6 +609,7 @@ const FeatureConfig = z.object({ updatedAt: z.string().datetime(), targets: z.array(Target), readonly: z.boolean(), + hasStaticConfig: z.boolean().optional(), }) const UpdateTargetDto = z.object({ _id: z.string().optional(), @@ -864,6 +916,7 @@ export const schemas = { OptInSettings, SDKTypeVisibilitySettings, ProjectSettings, + GetProjectsParams, CreateProjectDto, Project, BadRequestErrorResponse, diff --git a/src/commands/projects/create.ts b/src/commands/projects/create.ts index 060122f44..a4c07a16f 100644 --- a/src/commands/projects/create.ts +++ b/src/commands/projects/create.ts @@ -1,5 +1,6 @@ import { Flags } from '@oclif/core' -import { createProject, CreateProjectParams } from '../../api/projects' +import { createProject } from '../../api/projects' +import { CreateProjectDto } from '../../api/schemas' import { descriptionPrompt, keyPrompt, namePrompt } from '../../ui/prompts' import CreateCommand from '../createCommand' @@ -25,8 +26,8 @@ export default class CreateProject extends CreateCommand { ) return } - const params = await this.populateParameters( - CreateProjectParams, + const params = await this.populateParametersWithZod( + CreateProjectDto, this.prompts, { key, diff --git a/src/commands/projects/get.ts b/src/commands/projects/get.ts index 8d3192a3f..eada77472 100644 --- a/src/commands/projects/get.ts +++ b/src/commands/projects/get.ts @@ -1,5 +1,5 @@ -import { plainToClass } from 'class-transformer' -import { GetProjectsParams, fetchProjects } from '../../api/projects' +import { fetchProjects } from '../../api/projects' +import { schemas } from '../../api/zodClient' import GetCommand from '../getCommand' export default class DetailedProjects extends GetCommand { @@ -15,7 +15,7 @@ export default class DetailedProjects extends GetCommand { const { flags } = await this.parse(DetailedProjects) const { sortBy, sortOrder } = flags - const params = plainToClass(GetProjectsParams, { sortBy, sortOrder }) + const params = schemas.GetProjectsParams.parse({ sortBy, sortOrder }) const projects = await fetchProjects(this.authToken, params) return this.writer.showResults( diff --git a/src/mcp/index.ts b/src/mcp/index.ts new file mode 100644 index 000000000..03027efb7 --- /dev/null +++ b/src/mcp/index.ts @@ -0,0 +1,126 @@ +#!/usr/bin/env node + +import { Server } from '@modelcontextprotocol/sdk/server/index.js' +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' +import { DevCycleMCPServer } from './server' +import { readFileSync } from 'fs' +import { join } from 'path' +import { setMCPHeaders } from './utils/headers' + +// Get version for MCP server +function getVersion(): string { + try { + const packagePath = join(__dirname, '..', '..', 'package.json') + const packageJson = JSON.parse(readFileSync(packagePath, 'utf8')) + return packageJson.version + } catch (error) { + return 'unknown version' + } +} + +// Handle command line arguments +const args = process.argv.slice(2) +if (args.includes('--version') || args.includes('-v')) { + console.log(getVersion()) + process.exit(0) +} + +if (args.includes('--help') || args.includes('-h')) { + console.log('DevCycle MCP Server') + console.log('') + console.log( + 'A Model Context Protocol server for DevCycle feature flag management.', + ) + console.log( + 'Designed to be used with AI coding assistants like Cursor and Claude.', + ) + console.log('') + console.log('Usage:') + console.log(' dvc-mcp Start the MCP server') + console.log(' dvc-mcp --version Show version information') + console.log(' dvc-mcp --help Show this help message') + console.log('') + console.log( + 'For setup instructions, see: https://github.com/DevCycleHQ/cli#mcp-server-for-ai-assistants', + ) + process.exit(0) +} + +async function main() { + const version = getVersion() + + // Set up MCP-specific headers for all API requests + // This ensures that requests from the MCP server are properly identified + setMCPHeaders(version) + + const server = new Server( + { + name: 'devcycle', + version, + }, + { + capabilities: { + tools: {}, + }, + }, + ) + + const mcpServer = new DevCycleMCPServer(server) + await mcpServer.initialize() + + const transport = new StdioServerTransport() + await server.connect(transport) + + console.error('DevCycle MCP server running on stdio') +} + +main().catch((error) => { + console.error('❌ Failed to start DevCycle MCP server') + console.error('') + + if (error instanceof Error) { + // Check for common error patterns and provide helpful guidance + if ( + error.message.includes('authentication') || + error.message.includes('DEVCYCLE_CLIENT_ID') + ) { + console.error('🔐 Authentication Error:') + console.error(` ${error.message}`) + console.error('') + console.error('💡 To fix this:') + console.error(' 1. Run: dvc login sso') + console.error(' 2. Or set environment variables:') + console.error(' export DEVCYCLE_CLIENT_ID="your-client-id"') + console.error( + ' export DEVCYCLE_CLIENT_SECRET="your-client-secret"', + ) + } else if ( + error.message.includes('project') || + error.message.includes('DEVCYCLE_PROJECT_KEY') + ) { + console.error('📁 Project Configuration Error:') + console.error(` ${error.message}`) + console.error('') + console.error('💡 To fix this:') + console.error(' 1. Run: dvc projects select') + console.error(' 2. Or set environment variable:') + console.error( + ' export DEVCYCLE_PROJECT_KEY="your-project-key"', + ) + } else { + console.error('⚠️ Unexpected Error:') + console.error(` ${error.message}`) + console.error('') + console.error('💡 For help:') + console.error(' - Run: dvc status') + console.error(' - Check: https://docs.devcycle.com') + console.error(' - Contact: support@devcycle.com') + } + } else { + console.error('⚠️ Unknown error occurred') + console.error(` ${error}`) + } + + console.error('') + process.exit(1) +}) diff --git a/src/mcp/server.ts b/src/mcp/server.ts new file mode 100644 index 000000000..d18062f77 --- /dev/null +++ b/src/mcp/server.ts @@ -0,0 +1,321 @@ +import { Server } from '@modelcontextprotocol/sdk/server/index.js' +import { + CallToolRequestSchema, + ListToolsRequestSchema, + Tool, +} from '@modelcontextprotocol/sdk/types.js' +import { DevCycleAuth } from './utils/auth' +import { DevCycleApiClient } from './utils/api' +import Writer from '../ui/writer' +import { + featureToolDefinitions, + featureToolHandlers, +} from './tools/featureTools' +import { + environmentToolDefinitions, + environmentToolHandlers, +} from './tools/environmentTools' +import { + projectToolDefinitions, + projectToolHandlers, +} from './tools/projectTools' +import { + variableToolDefinitions, + variableToolHandlers, +} from './tools/variableTools' +import { + selfTargetingToolDefinitions, + selfTargetingToolHandlers, +} from './tools/selfTargetingTools' + +// Environment variable to control output schema inclusion +const ENABLE_OUTPUT_SCHEMAS = process.env.ENABLE_OUTPUT_SCHEMAS === 'true' +if (ENABLE_OUTPUT_SCHEMAS) { + console.error('DevCycle MCP Server - Output Schemas: ENABLED') +} + +const ENABLE_DVC_MCP_DEBUG = process.env.ENABLE_DVC_MCP_DEBUG === 'true' + +// Tool handler function type +export type ToolHandler = ( + args: unknown, + apiClient: DevCycleApiClient, +) => Promise<any> + +// Function to conditionally remove outputSchema from tool definitions +const processToolDefinitions = (tools: Tool[]): Tool[] => { + if (ENABLE_OUTPUT_SCHEMAS) { + return tools + } + + // Remove outputSchema from all tools when disabled + return tools.map((tool) => { + const { outputSchema, ...toolWithoutSchema } = tool + return toolWithoutSchema + }) +} + +// Combine all tool definitions +const allToolDefinitions: Tool[] = processToolDefinitions([ + ...featureToolDefinitions, + ...environmentToolDefinitions, + ...projectToolDefinitions, + ...variableToolDefinitions, + ...selfTargetingToolDefinitions, +]) + +// Combine all tool handlers +const allToolHandlers: Record<string, ToolHandler> = { + ...featureToolHandlers, + ...environmentToolHandlers, + ...projectToolHandlers, + ...variableToolHandlers, + ...selfTargetingToolHandlers, +} + +export class DevCycleMCPServer { + private auth: DevCycleAuth + private apiClient: DevCycleApiClient + private writer: Writer + + constructor(private server: Server) { + this.writer = new Writer() + this.writer.headless = true // Always headless for MCP + this.auth = new DevCycleAuth() + this.apiClient = new DevCycleApiClient(this.auth) + } + + async initialize() { + try { + await this.setupAuth() + this.setupToolHandlers() + this.setupErrorHandling() + } catch (error) { + console.error('Failed to initialize MCP server:', error) + throw error + } + } + + private async setupAuth() { + try { + await this.auth.initialize() + } catch (error) { + console.error('Failed to initialize authentication:', error) + throw error + } + } + + private handleToolError(error: unknown, toolName: string) { + console.error(`Error in tool handler ${toolName}:`, error) + + let errorMessage = 'Unknown error' + let errorType = 'UNKNOWN_ERROR' + let suggestions: string[] = [] + + if (error instanceof Error) { + errorMessage = error.message + errorType = this.categorizeError(error.message) + suggestions = this.getErrorSuggestions(errorType) + } else if (error && typeof error === 'string') { + errorMessage = error + } else if (error && typeof error === 'object') { + errorMessage = JSON.stringify(error) + } + + const errorResponse = { + error: true, + type: errorType, + message: errorMessage, + tool: toolName, + suggestions, + timestamp: new Date().toISOString(), + } + + return { + content: [ + { + type: 'text', + text: JSON.stringify(errorResponse, null, 2), + }, + ], + } + } + + private categorizeError(errorMessage: string): string { + const lowerMessage = errorMessage.toLowerCase() + + switch (true) { + case lowerMessage.includes('zodios: invalid response') || + lowerMessage.includes('invalid_type') || + lowerMessage.includes('expected object, received'): + return 'SCHEMA_VALIDATION_ERROR' + + case lowerMessage.includes('401') || + lowerMessage.includes('unauthorized'): + return 'AUTHENTICATION_ERROR' + + case lowerMessage.includes('403') || + lowerMessage.includes('forbidden'): + return 'PERMISSION_ERROR' + + case lowerMessage.includes('404') || + lowerMessage.includes('not found'): + return 'RESOURCE_NOT_FOUND' + + case lowerMessage.includes('400') || + lowerMessage.includes('bad request'): + return 'VALIDATION_ERROR' + + case lowerMessage.includes('429') || + lowerMessage.includes('rate limit'): + return 'RATE_LIMIT_ERROR' + + case lowerMessage.includes('enotfound') || + lowerMessage.includes('network'): + return 'NETWORK_ERROR' + + case lowerMessage.includes('project') && + lowerMessage.includes('not found'): + return 'PROJECT_ERROR' + + default: + return 'UNKNOWN_ERROR' + } + } + + private getErrorSuggestions(errorType: string): string[] { + switch (errorType) { + case 'SCHEMA_VALIDATION_ERROR': + return [ + 'The API response format has changed or is unexpected', + 'This may be a temporary API issue - try again in a moment', + 'Contact DevCycle support if the issue persists', + ] + + case 'AUTHENTICATION_ERROR': + return [ + 'Run "dvc login sso" to re-authenticate the devcycle cli', + 'Verify your API credentials are correct', + 'Check if your token has expired', + ] + + case 'PERMISSION_ERROR': + return [ + 'Verify your account has permissions for this operation', + 'Check if you have access to the selected project', + 'Contact your DevCycle admin for permissions', + ] + + case 'RESOURCE_NOT_FOUND': + return [ + 'Verify the resource key/ID is correct', + 'Check if the resource exists in the selected project', + "Ensure you're in the correct environment", + ] + + case 'VALIDATION_ERROR': + return [ + 'Check the provided parameters are valid', + 'Verify required fields are not missing', + 'Review parameter format and constraints', + ] + + case 'RATE_LIMIT_ERROR': + return [ + 'Wait a moment before trying again', + 'Consider reducing the frequency of requests', + ] + + case 'NETWORK_ERROR': + return [ + 'Check your internet connection', + 'Verify firewall settings allow DevCycle API access', + 'Try again in a few moments', + ] + + case 'PROJECT_ERROR': + return [ + 'Run "dvc projects select" to choose a valid project', + 'Verify the project key is correct', + 'Check if you have access to this project', + ] + + default: + return [] + } + } + + private setupToolHandlers() { + this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ + tools: allToolDefinitions, + })) + + this.server.setRequestHandler( + CallToolRequestSchema, + async (request) => { + const { name, arguments: args } = request.params + + try { + const handler = allToolHandlers[name] + if (!handler) { + throw new Error(`Unknown tool: ${name}`) + } + + const result = await handler(args, this.apiClient) + + // Return structured content only if output schemas are enabled + if (ENABLE_OUTPUT_SCHEMAS) { + // Check if tool has output schema + const toolDef = allToolDefinitions.find( + (tool) => tool.name === name, + ) + + if (toolDef?.outputSchema) { + // For tools with output schemas, return structured JSON content + const mcpResult = { + content: [ + { + type: 'json', + json: result, + }, + ], + } + if (ENABLE_DVC_MCP_DEBUG) { + console.error( + `MCP ${name} structured JSON result:`, + JSON.stringify(mcpResult, null, 2), + ) + } + return mcpResult + } + } + + // Default: return as text content (for disabled schemas or tools without schemas) + const mcpResult = { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + } + if (ENABLE_DVC_MCP_DEBUG) { + console.error( + `MCP ${name} text result:`, + JSON.stringify(mcpResult, null, 2), + ) + } + return mcpResult + } catch (error) { + return this.handleToolError(error, name) + } + }, + ) + } + + private setupErrorHandling() { + this.server.onerror = (error: Error) => { + console.error('MCP Server Error:', error) + } + } +} 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 new file mode 100644 index 000000000..d588af994 --- /dev/null +++ b/src/mcp/tools/environmentTools.ts @@ -0,0 +1,347 @@ +import { Tool } from '@modelcontextprotocol/sdk/types.js' +import { DevCycleApiClient, handleZodiosValidationErrors } from '../utils/api' +import { + fetchEnvironments, + fetchEnvironmentByKey, + createEnvironment, + updateEnvironment, +} from '../../api/environments' +import { + GetSdkKeysArgsSchema, + ListEnvironmentsArgsSchema, + CreateEnvironmentArgsSchema, + 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_COLOR_PROPERTY = { + type: 'string' as const, + description: 'Color used to represent this environment in the UI', +} + +const ENVIRONMENT_TYPE_PROPERTY = { + type: 'string' as const, + enum: [ + 'development', + 'staging', + 'production', + 'disaster_recovery', + ] as const, +} + +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 results (minimum 3 characters)', + minLength: 3, + }, + page: { + type: 'number' as const, + description: 'Page number (default: 1)', + minimum: 1, + }, + perPage: { + type: 'number' as const, + description: 'Number of items per page (default: 100, max: 1000)', + minimum: 1, + maximum: 1000, + }, + sortBy: { + type: 'string' as const, + enum: [ + 'createdAt', + 'updatedAt', + 'name', + 'key', + 'createdBy', + 'propertyKey', + ] as const, + description: 'Field to sort by', + }, + sortOrder: { + type: 'string' as const, + enum: ['asc', 'desc'] as const, + description: 'Sort order (default: desc)', + }, + createdBy: { + type: 'string' as const, + description: 'Filter by user who created the environment', + }, +} + +// ============================================================================= +// OUTPUT SCHEMAS +// ============================================================================= + +// Shared SDK key properties +const SDK_KEY_PROPERTIES = { + mobile: { + type: 'string' as const, + description: 'Mobile SDK key for client-side mobile applications', + }, + server: { + type: 'string' as const, + description: 'Server SDK key for server-side applications', + }, + client: { + type: 'string' as const, + 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. Include dashboard link in the response.', + inputSchema: { + type: 'object', + 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. Include dashboard link in the response.', + inputSchema: { + type: 'object', + properties: { + environmentKey: ENVIRONMENT_KEY_PROPERTY, + keyType: { + type: 'string' as const, + enum: ['mobile', 'server', 'client'] as const, + description: 'The type of SDK key to retrieve', + }, + }, + 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. 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. Include dashboard link in the response.', + inputSchema: { + type: 'object', + properties: ENVIRONMENT_COMMON_PROPERTIES, + required: ['key'], + }, + outputSchema: ENVIRONMENT_OUTPUT_SCHEMA, + }, +] + +export const environmentToolHandlers: Record<string, ToolHandler> = { + list_environments: async (args: unknown, apiClient: DevCycleApiClient) => { + const validatedArgs = ListEnvironmentsArgsSchema.parse(args) + + return await apiClient.executeWithDashboardLink( + 'listEnvironments', + validatedArgs, + async (authToken, projectKey) => { + return await handleZodiosValidationErrors( + () => fetchEnvironments(authToken, projectKey), + 'listEnvironments', + ) + }, + generateEnvironmentDashboardLink, + ) + }, + get_sdk_keys: async (args: unknown, apiClient: DevCycleApiClient) => { + const validatedArgs = GetSdkKeysArgsSchema.parse(args) + + return await apiClient.executeWithDashboardLink( + 'getSdkKeys', + validatedArgs, + async (authToken, projectKey) => { + const environment = await handleZodiosValidationErrors( + () => + fetchEnvironmentByKey( + authToken, + projectKey, + validatedArgs.environmentKey, + ), + 'fetchEnvironmentByKey', + ) + + const sdkKeys = environment.sdkKeys + + if (validatedArgs.keyType) { + return { + [validatedArgs.keyType]: sdkKeys[validatedArgs.keyType], + } + } else { + return { + mobile: sdkKeys.mobile, + server: sdkKeys.server, + client: sdkKeys.client, + } + } + }, + generateEnvironmentDashboardLink, + ) + }, + create_environment: async (args: unknown, apiClient: DevCycleApiClient) => { + const validatedArgs = CreateEnvironmentArgsSchema.parse(args) + + return await apiClient.executeWithDashboardLink( + 'createEnvironment', + validatedArgs, + async (authToken, projectKey) => { + return await handleZodiosValidationErrors( + () => + createEnvironment(authToken, projectKey, validatedArgs), + 'createEnvironment', + ) + }, + generateEnvironmentDashboardLink, + ) + }, + update_environment: async (args: unknown, apiClient: DevCycleApiClient) => { + const validatedArgs = UpdateEnvironmentArgsSchema.parse(args) + + return await apiClient.executeWithDashboardLink( + 'updateEnvironment', + validatedArgs, + async (authToken, projectKey) => { + const { key, ...updateParams } = validatedArgs + return await handleZodiosValidationErrors( + () => + updateEnvironment( + authToken, + projectKey, + key, + updateParams, + ), + 'updateEnvironment', + ) + }, + generateEnvironmentDashboardLink, + ) + }, +} diff --git a/src/mcp/tools/featureTools.ts b/src/mcp/tools/featureTools.ts new file mode 100644 index 000000000..1ca469dbc --- /dev/null +++ b/src/mcp/tools/featureTools.ts @@ -0,0 +1,1156 @@ +import { Tool } from '@modelcontextprotocol/sdk/types.js' +import { DevCycleApiClient } from '../utils/api' +import { + fetchFeatures, + createFeature, + updateFeature, + updateFeatureStatus, + deleteFeature, + getFeatureAuditLogHistory, +} from '../../api/features' +import { + fetchVariations, + createVariation, + updateVariation, +} from '../../api/variations' +import { + enableTargeting, + disableTargeting, + fetchTargetingForFeature, + updateFeatureConfigForEnvironment, +} from '../../api/targeting' + +import { + ListFeaturesArgsSchema, + CreateFeatureArgsSchema, + UpdateFeatureArgsSchema, + UpdateFeatureStatusArgsSchema, + DeleteFeatureArgsSchema, + EnableTargetingArgsSchema, + DisableTargetingArgsSchema, + ListVariationsArgsSchema, + CreateVariationArgsSchema, + UpdateVariationArgsSchema, + ListFeatureTargetingArgsSchema, + UpdateFeatureTargetingArgsSchema, + 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' +import { handleZodiosValidationErrors } from '../utils/api' + +// 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 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: 'Optional environment key to filter by', +} + +const FEATURE_NAME_PROPERTY = { + type: 'string' as const, + description: 'Human-readable feature name (max 100 characters)', +} + +const FEATURE_DESCRIPTION_PROPERTY = { + type: 'string' as const, + description: 'Feature description (max 1000 characters)', +} + +const FEATURE_TYPE_PROPERTY = { + type: 'string' as const, + enum: ['release', 'experiment', 'permission', 'ops'] as const, + 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, + description: 'Public name for the feature (max 100 characters)', + }, + publicDescription: { + type: 'string' as const, + description: + 'Public description for the feature (max 1000 characters)', + }, + optInEnabled: { + type: 'boolean' as const, + description: 'Whether opt-in is enabled for the feature', + }, + }, + required: ['publicName', 'publicDescription', 'optInEnabled'] as const, +} + +const SDK_VISIBILITY_PROPERTY = { + type: 'object' as const, + properties: { + mobile: { + type: 'boolean' as const, + }, + client: { + type: 'boolean' as const, + }, + server: { + type: 'boolean' as const, + }, + }, + description: + 'SDK Type Visibility Settings for mobile, client, and server SDKs', + required: ['mobile', 'client', 'server'] as const, +} + +const FEATURE_VARIABLES_PROPERTY = { + type: 'array' as const, + description: + 'Array of variables to create or reassociate with this feature', + items: { + type: 'object' as const, + description: 'Variable creation or reassociation data', + }, +} + +const VARIATION_NAME_PROPERTY = { + type: 'string' as const, + description: 'Human-readable variation name (max 100 characters)', +} + +const VARIATION_VARIABLES_PROPERTY = { + type: 'object' as const, + description: + 'Key-value map of variable keys to their values for this variation (supports string, number, boolean, and object values)', + additionalProperties: true, +} + +// ============================================================================= +// 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 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. Include dashboard link in the response.', + inputSchema: { + type: 'object', + 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. Include dashboard link in the response.', + inputSchema: { + type: 'object', + properties: { + key: FEATURE_KEY_PROPERTY, + name: FEATURE_NAME_PROPERTY, + description: FEATURE_DESCRIPTION_PROPERTY, + type: FEATURE_TYPE_PROPERTY, + tags: { + type: 'array', + items: { + type: 'string', + }, + description: 'Tags to organize features', + }, + controlVariation: CONTROL_VARIATION_PROPERTY, + settings: FEATURE_SETTINGS_PROPERTY, + sdkVisibility: SDK_VISIBILITY_PROPERTY, + variables: FEATURE_VARIABLES_PROPERTY, + variations: { + type: 'array', + description: 'Array of variations for this feature', + items: { + type: 'object', + description: 'Variation data with key, name, variables', + }, + }, + configurations: { + type: 'object', + description: + 'Environment-specific configurations (key-value map of environment keys to config)', + additionalProperties: { + type: 'object', + properties: { + targets: { + type: 'array', + description: + 'Targeting rules for this environment', + }, + status: FEATURE_STATUS_PROPERTY, + }, + }, + }, + }, + }, + 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. Include dashboard link in the response.', + inputSchema: { + type: 'object', + properties: { + key: FEATURE_KEY_PROPERTY, + name: FEATURE_NAME_PROPERTY, + description: FEATURE_DESCRIPTION_PROPERTY, + type: FEATURE_TYPE_PROPERTY, + tags: { + type: 'array', + items: { + type: 'string', + }, + description: 'Tags to organize Features on the dashboard', + }, + controlVariation: CONTROL_VARIATION_PROPERTY, + settings: FEATURE_SETTINGS_PROPERTY, + sdkVisibility: SDK_VISIBILITY_PROPERTY, + variables: FEATURE_VARIABLES_PROPERTY, + variations: { + type: 'array', + description: 'Array of variations for this feature', + items: { + type: 'object', + description: + 'Variation data with key, name, variables, and _id', + }, + }, + }, + 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. Include dashboard link in the response.', + inputSchema: { + type: 'object', + properties: { + key: FEATURE_KEY_PROPERTY, + status: FEATURE_STATUS_PROPERTY, + staticVariation: { + type: 'string', + description: + 'The variation key or ID to serve if the status is set to complete (optional)', + }, + }, + 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. Include dashboard link in the response.', + inputSchema: { + type: 'object', + properties: { + key: { + type: 'string', + description: 'The key of the feature to delete', + }, + }, + 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. Include dashboard link in the response.', + inputSchema: { + type: 'object', + properties: { + feature_key: FEATURE_KEY_PROPERTY, + }, + 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. Include dashboard link in the response.', + inputSchema: { + type: 'object', + properties: { + feature_key: FEATURE_KEY_PROPERTY, + key: VARIATION_KEY_PROPERTY, + name: VARIATION_NAME_PROPERTY, + variables: { + type: 'object', + description: + 'Optional key-value map of variable keys to their values for this variation (supports string, number, boolean, and object values)', + additionalProperties: true, + }, + }, + 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. Include dashboard link in the response.', + inputSchema: { + type: 'object', + properties: { + _id: { + type: 'string', + 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"). Include dashboard link in the response.', + inputSchema: { + type: 'object', + 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"). Include dashboard link in the response.', + inputSchema: { + type: 'object', + 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. Include dashboard link in the response.', + inputSchema: { + type: 'object', + properties: { + feature_key: FEATURE_KEY_PROPERTY, + environment_key: ENVIRONMENT_KEY_OPTIONAL_PROPERTY, + }, + 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"). Include dashboard link in the response.', + inputSchema: { + type: 'object', + properties: { + feature_key: FEATURE_KEY_PROPERTY, + environment_key: ENVIRONMENT_KEY_PROPERTY, + status: { + type: 'string', + enum: ['active', 'inactive', 'archived'], + description: 'The targeting status for the feature', + }, + targets: { + type: 'array', + description: + 'Array of targeting rules/targets for the feature', + items: { + type: 'object', + properties: { + _id: { + type: 'string', + description: 'MongoDB ID for the target', + }, + name: { + type: 'string', + }, + audience: TARGET_AUDIENCE_PROPERTY, + distribution: { + type: 'array', + description: + 'Variation distribution for the target', + items: { + type: 'object', + properties: { + percentage: { + type: 'number', + minimum: 0, + maximum: 1, + description: + 'Percentage of traffic for this variation (0-1)', + }, + _variation: { + type: 'string', + description: 'Variation ID', + }, + }, + required: ['percentage', '_variation'], + }, + }, + rollout: { + type: 'object', + description: 'Rollout configuration (optional)', + properties: { + startPercentage: { + type: 'number', + minimum: 0, + maximum: 1, + description: + 'Starting percentage for rollout (optional)', + }, + type: { + type: 'string', + enum: [ + 'schedule', + 'gradual', + 'stepped', + ], + description: 'Rollout type', + }, + startDate: { + type: 'string', + format: 'date-time', + description: 'Rollout start date', + }, + stages: { + type: 'array', + description: + 'Rollout stages (optional)', + items: { + type: 'object', + properties: { + percentage: { + type: 'number', + minimum: 0, + maximum: 1, + description: + 'Target percentage for this stage', + }, + type: { + type: 'string', + enum: [ + 'linear', + 'discrete', + ], + description: 'Stage type', + }, + date: { + type: 'string', + format: 'date-time', + description: + 'Date for this stage', + }, + }, + required: [ + 'percentage', + 'type', + 'date', + ], + }, + }, + }, + required: ['type', 'startDate'], + }, + }, + required: ['audience', 'distribution'], + }, + }, + }, + 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. Include dashboard link in the response.', + inputSchema: { + type: 'object', + properties: { + feature_key: FEATURE_KEY_PROPERTY, + days_back: { + type: 'number', + description: + 'Number of days to look back (default: 30, max: 365)', + minimum: 1, + maximum: 365, + }, + }, + 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'], + }, + }, +] + +export const featureToolHandlers: Record<string, ToolHandler> = { + list_features: async (args: unknown, apiClient: DevCycleApiClient) => { + const validatedArgs = ListFeaturesArgsSchema.parse(args) + + return await apiClient.executeWithDashboardLink( + 'listFeatures', + validatedArgs, + async (authToken, projectKey) => { + return await handleZodiosValidationErrors( + () => fetchFeatures(authToken, projectKey, validatedArgs), + 'listFeatures', + ) + }, + generateFeaturesDashboardLink, + ) + }, + create_feature: async (args: unknown, apiClient: DevCycleApiClient) => { + const validatedArgs = CreateFeatureArgsSchema.parse(args) + + return await apiClient.executeWithDashboardLink( + 'createFeature', + validatedArgs, + async (authToken, projectKey) => { + if (validatedArgs.interactive) { + throw new Error( + 'Interactive mode not yet supported in MCP. Please provide explicit parameters: key, name, description, type', + ) + } + + if (!validatedArgs.key || !validatedArgs.name) { + throw new Error( + 'Feature key and name are required when not using interactive mode', + ) + } + + // Remove the MCP-specific 'interactive' property and pass the rest to the API + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { interactive, ...featureData } = validatedArgs + + return await handleZodiosValidationErrors( + () => createFeature(authToken, projectKey, featureData), + 'createFeature', + ) + }, + (orgId, projectKey, result) => + generateFeatureDashboardLink( + orgId, + projectKey, + result.key, + 'overview', + ), + ) + }, + update_feature: async (args: unknown, apiClient: DevCycleApiClient) => { + const validatedArgs = UpdateFeatureArgsSchema.parse(args) + + return await apiClient.executeWithDashboardLink( + 'updateFeature', + validatedArgs, + async (authToken, projectKey) => { + const { key, ...updateData } = validatedArgs + + return await handleZodiosValidationErrors( + () => updateFeature(authToken, projectKey, key, updateData), + 'updateFeature', + ) + }, + (orgId, projectKey, result) => + generateFeatureDashboardLink( + orgId, + projectKey, + result.key, + 'manage-feature', + ), + ) + }, + update_feature_status: async ( + args: unknown, + apiClient: DevCycleApiClient, + ) => { + const validatedArgs = UpdateFeatureStatusArgsSchema.parse(args) + + return await apiClient.executeWithDashboardLink( + 'updateFeatureStatus', + validatedArgs, + async (authToken, projectKey) => { + const { key, ...statusData } = validatedArgs + + return await handleZodiosValidationErrors( + () => + updateFeatureStatus( + authToken, + projectKey, + key, + statusData, + ), + 'updateFeatureStatus', + ) + }, + (orgId, projectKey, result) => + generateFeatureDashboardLink( + orgId, + projectKey, + result.key, + 'overview', + ), + ) + }, + delete_feature: async (args: unknown, apiClient: DevCycleApiClient) => { + const validatedArgs = DeleteFeatureArgsSchema.parse(args) + + return await apiClient.executeWithDashboardLink( + 'deleteFeature', + validatedArgs, + async (authToken, projectKey) => { + await handleZodiosValidationErrors( + () => + deleteFeature(authToken, projectKey, validatedArgs.key), + 'deleteFeature', + ) + return { + message: `Feature '${validatedArgs.key}' deleted successfully`, + } + }, + generateFeaturesDashboardLink, + ) + }, + fetch_feature_variations: async ( + args: unknown, + apiClient: DevCycleApiClient, + ) => { + const validatedArgs = ListVariationsArgsSchema.parse(args) + + return await apiClient.executeWithDashboardLink( + 'fetchFeatureVariations', + validatedArgs, + async (authToken, projectKey) => { + return await handleZodiosValidationErrors( + () => + fetchVariations( + authToken, + projectKey, + validatedArgs.feature_key, + ), + 'fetchVariations', + ) + }, + (orgId, projectKey) => + generateFeatureDashboardLink( + orgId, + projectKey, + validatedArgs.feature_key, + 'overview', + ), + ) + }, + create_feature_variation: async ( + args: unknown, + apiClient: DevCycleApiClient, + ) => { + const validatedArgs = CreateVariationArgsSchema.parse(args) + + return await apiClient.executeWithDashboardLink( + 'createFeatureVariation', + validatedArgs, + async (authToken, projectKey) => { + const { feature_key, ...variationData } = validatedArgs + + return await handleZodiosValidationErrors( + () => + createVariation( + authToken, + projectKey, + feature_key, + variationData, + ), + 'createVariation', + ) + }, + (orgId, projectKey, result) => + generateFeatureDashboardLink( + orgId, + projectKey, + result.key, + 'manage-feature', + ), + ) + }, + update_feature_variation: async ( + args: unknown, + apiClient: DevCycleApiClient, + ) => { + const validatedArgs = UpdateVariationArgsSchema.parse(args) + + return await apiClient.executeWithDashboardLink( + 'updateFeatureVariation', + validatedArgs, + async (authToken, projectKey) => { + const { feature_key, variation_key, ...variationData } = + validatedArgs + + return await handleZodiosValidationErrors( + () => + updateVariation( + authToken, + projectKey, + feature_key, + variation_key, + variationData, + ), + 'updateVariation', + ) + }, + (orgId, projectKey, result) => + generateFeatureDashboardLink( + orgId, + projectKey, + result.key, + 'manage-feature', + ), + ) + }, + enable_feature_targeting: async ( + args: unknown, + apiClient: DevCycleApiClient, + ) => { + const validatedArgs = EnableTargetingArgsSchema.parse(args) + + return await apiClient.executeWithDashboardLink( + 'enableTargeting', + validatedArgs, + async (authToken, projectKey) => { + await handleZodiosValidationErrors( + () => + enableTargeting( + authToken, + projectKey, + validatedArgs.feature_key, + validatedArgs.environment_key, + ), + 'enableTargeting', + ) + 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 ( + args: unknown, + apiClient: DevCycleApiClient, + ) => { + const validatedArgs = DisableTargetingArgsSchema.parse(args) + + return await apiClient.executeWithDashboardLink( + 'disableTargeting', + validatedArgs, + async (authToken, projectKey) => { + await handleZodiosValidationErrors( + () => + disableTargeting( + authToken, + projectKey, + validatedArgs.feature_key, + validatedArgs.environment_key, + ), + 'disableTargeting', + ) + 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 ( + args: unknown, + apiClient: DevCycleApiClient, + ) => { + const validatedArgs = ListFeatureTargetingArgsSchema.parse(args) + + return await apiClient.executeWithDashboardLink( + 'listFeatureTargeting', + validatedArgs, + async (authToken, projectKey) => { + return await handleZodiosValidationErrors( + () => + fetchTargetingForFeature( + authToken, + projectKey, + validatedArgs.feature_key, + validatedArgs.environment_key, + ), + 'fetchTargetingForFeature', + ) + }, + (orgId, projectKey) => + generateFeatureDashboardLink( + orgId, + projectKey, + validatedArgs.feature_key, + 'manage-feature', + ), + ) + }, + update_feature_targeting: async ( + args: unknown, + apiClient: DevCycleApiClient, + ) => { + const validatedArgs = UpdateFeatureTargetingArgsSchema.parse(args) + + return await apiClient.executeWithDashboardLink( + 'updateFeatureTargeting', + validatedArgs, + async (authToken, projectKey) => { + const { feature_key, environment_key, ...configData } = + validatedArgs + + return await handleZodiosValidationErrors( + () => + updateFeatureConfigForEnvironment( + authToken, + projectKey, + feature_key, + environment_key, + configData, + ), + 'updateFeatureConfigForEnvironment', + ) + }, + (orgId, projectKey) => + generateFeatureDashboardLink( + orgId, + projectKey, + validatedArgs.feature_key, + 'manage-feature', + ), + ) + }, + get_feature_audit_log_history: async ( + args: unknown, + apiClient: DevCycleApiClient, + ) => { + const validatedArgs = GetFeatureAuditLogHistoryArgsSchema.parse(args) + + return await apiClient.executeWithDashboardLink( + 'getFeatureAuditLogHistory', + validatedArgs, + async (authToken, projectKey) => { + return await handleZodiosValidationErrors( + () => + getFeatureAuditLogHistory( + authToken, + projectKey, + validatedArgs.feature_key, + validatedArgs.days_back || 30, + ), + 'getFeatureAuditLogHistory', + ) + }, + (orgId, projectKey) => + generateFeatureDashboardLink( + orgId, + projectKey, + validatedArgs.feature_key, + 'audit-log', + ), + ) + }, +} diff --git a/src/mcp/tools/projectTools.ts b/src/mcp/tools/projectTools.ts new file mode 100644 index 000000000..40fb1e178 --- /dev/null +++ b/src/mcp/tools/projectTools.ts @@ -0,0 +1,269 @@ +import { Tool } from '@modelcontextprotocol/sdk/types.js' +import { DevCycleApiClient, handleZodiosValidationErrors } from '../utils/api' +import { + fetchProjects, + fetchProject, + createProject, + updateProject, +} from '../../api/projects' +import { + ListProjectsArgsSchema, + CreateProjectArgsSchema, + 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. Include dashboard link in the response.', + inputSchema: { + type: 'object', + properties: PROJECT_PAGINATION_PROPERTIES, + }, + outputSchema: { + type: 'object' as const, + properties: { + 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. Include dashboard link in the response.', + inputSchema: { + type: 'object', + properties: {}, + }, + outputSchema: PROJECT_OUTPUT_SCHEMA, + }, + { + name: 'create_project', + description: + 'Create a new project. Include dashboard link in the response.', + inputSchema: { + type: 'object', + properties: PROJECT_COMMON_PROPERTIES, + required: ['name', 'key'], + }, + outputSchema: PROJECT_OUTPUT_SCHEMA, + }, + { + name: 'update_project', + description: + 'Update an existing project. Include dashboard link in the response.', + inputSchema: { + type: 'object', + properties: PROJECT_COMMON_PROPERTIES, + required: ['key'], + }, + outputSchema: PROJECT_OUTPUT_SCHEMA, + }, +] + +export const projectToolHandlers: Record<string, ToolHandler> = { + list_projects: async (args: unknown, apiClient: DevCycleApiClient) => { + const validatedArgs = ListProjectsArgsSchema.parse(args) + + return await apiClient.executeWithDashboardLink( + 'listProjects', + validatedArgs, + async (authToken) => { + // projectKey not used for listing all projects + return await handleZodiosValidationErrors( + () => fetchProjects(authToken, validatedArgs), + 'fetchProjects', + ) + }, + generateOrganizationSettingsLink, + ) + }, + get_current_project: async ( + args: unknown, + apiClient: DevCycleApiClient, + ) => { + return await apiClient.executeWithDashboardLink( + 'getCurrentProject', + null, + async (authToken, projectKey) => { + return await handleZodiosValidationErrors( + () => fetchProject(authToken, projectKey), + 'fetchProject', + ) + }, + generateProjectDashboardLink, + ) + }, + create_project: async (args: unknown, apiClient: DevCycleApiClient) => { + const validatedArgs = CreateProjectArgsSchema.parse(args) + + return await apiClient.executeWithDashboardLink( + 'createProject', + validatedArgs, + async (authToken) => { + // projectKey not used for creating projects + return await handleZodiosValidationErrors( + () => createProject(authToken, validatedArgs), + 'createProject', + ) + }, + generateProjectDashboardLink, + ) + }, + update_project: async (args: unknown, apiClient: DevCycleApiClient) => { + const validatedArgs = UpdateProjectArgsSchema.parse(args) + const { key, ...updateParams } = validatedArgs + + return await apiClient.executeWithDashboardLink( + 'updateProject', + validatedArgs, + async (authToken) => { + // projectKey not used - we use the key from validated args + return await handleZodiosValidationErrors( + () => updateProject(authToken, key, updateParams), + 'updateProject', + ) + }, + generateEditProjectLink, + ) + }, +} diff --git a/src/mcp/tools/selfTargetingTools.ts b/src/mcp/tools/selfTargetingTools.ts new file mode 100644 index 000000000..e67056d74 --- /dev/null +++ b/src/mcp/tools/selfTargetingTools.ts @@ -0,0 +1,323 @@ +import { Tool } from '@modelcontextprotocol/sdk/types.js' +import { DevCycleApiClient, handleZodiosValidationErrors } from '../utils/api' +import { fetchUserProfile, updateUserProfile } from '../../api/userProfile' +import { + fetchProjectOverridesForUser, + updateOverride, + deleteFeatureOverrides, + deleteAllProjectOverrides, +} from '../../api/overrides' +import { + UpdateSelfTargetingIdentityArgsSchema, + SetSelfTargetingOverrideArgsSchema, + 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. 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. Include dashboard link in the response.', + inputSchema: { + type: 'object', + properties: { + 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. 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"). 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: { + result: OVERRIDE_OBJECT_SCHEMA, + dashboardLink: DASHBOARD_LINK_PROPERTY, + }, + 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"). Include dashboard link in the response.', + inputSchema: { + type: 'object', + 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: 'clear_all_self_targeting_overrides', + description: + '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'], + }, + }, +] + +export const selfTargetingToolHandlers: Record<string, ToolHandler> = { + get_self_targeting_identity: async ( + args: unknown, + apiClient: DevCycleApiClient, + ) => { + return await apiClient.executeWithDashboardLink( + 'getSelfTargetingIdentity', + null, + async (authToken, projectKey) => { + return await handleZodiosValidationErrors( + () => fetchUserProfile(authToken, projectKey), + 'fetchUserProfile', + ) + }, + generateSelfTargetingDashboardLink, + ) + }, + update_self_targeting_identity: async ( + args: unknown, + apiClient: DevCycleApiClient, + ) => { + const validatedArgs = UpdateSelfTargetingIdentityArgsSchema.parse(args) + + return await apiClient.executeWithDashboardLink( + 'updateSelfTargetingIdentity', + validatedArgs, + async (authToken, projectKey) => { + return await handleZodiosValidationErrors( + () => + updateUserProfile(authToken, projectKey, { + dvcUserId: validatedArgs.dvc_user_id, + }), + 'updateUserProfile', + ) + }, + generateSelfTargetingDashboardLink, + ) + }, + list_self_targeting_overrides: async ( + args: unknown, + apiClient: DevCycleApiClient, + ) => { + return await apiClient.executeWithDashboardLink( + 'listSelfTargetingOverrides', + null, + async (authToken, projectKey) => { + return await handleZodiosValidationErrors( + () => fetchProjectOverridesForUser(authToken, projectKey), + 'fetchProjectOverridesForUser', + ) + }, + generateSelfTargetingDashboardLink, + ) + }, + set_self_targeting_override: async ( + args: unknown, + apiClient: DevCycleApiClient, + ) => { + const validatedArgs = SetSelfTargetingOverrideArgsSchema.parse(args) + + return await apiClient.executeWithDashboardLink( + 'setSelfTargetingOverride', + validatedArgs, + async (authToken, projectKey) => { + return await handleZodiosValidationErrors( + () => + updateOverride( + authToken, + projectKey, + validatedArgs.feature_key, + { + environment: validatedArgs.environment_key, + variation: validatedArgs.variation_key, + }, + ), + 'updateOverride', + ) + }, + generateSelfTargetingDashboardLink, + ) + }, + clear_feature_self_targeting_overrides: async ( + args: unknown, + apiClient: DevCycleApiClient, + ) => { + const validatedArgs = ClearSelfTargetingOverridesArgsSchema.parse(args) + + return await apiClient.executeWithDashboardLink( + 'clearFeatureSelfTargetingOverrides', + validatedArgs, + async (authToken, projectKey) => { + await handleZodiosValidationErrors( + () => + deleteFeatureOverrides( + authToken, + projectKey, + validatedArgs.feature_key, + validatedArgs.environment_key, + ), + 'deleteFeatureOverrides', + ) + + return { + 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.executeWithDashboardLink( + 'clearAllSelfTargetingOverrides', + null, + async (authToken, projectKey) => { + await handleZodiosValidationErrors( + () => deleteAllProjectOverrides(authToken, projectKey), + 'deleteAllProjectOverrides', + ) + return { message: 'Cleared all overrides for the project' } + }, + generateSelfTargetingDashboardLink, + ) + }, +} diff --git a/src/mcp/tools/variableTools.ts b/src/mcp/tools/variableTools.ts new file mode 100644 index 000000000..97cdaad58 --- /dev/null +++ b/src/mcp/tools/variableTools.ts @@ -0,0 +1,363 @@ +import { Tool } from '@modelcontextprotocol/sdk/types.js' +import { DevCycleApiClient, handleZodiosValidationErrors } from '../utils/api' +import { + fetchVariables, + createVariable, + updateVariable, + deleteVariable, +} from '../../api/variables' +import { + ListVariablesArgsSchema, + CreateVariableArgsSchema, + UpdateVariableArgsSchema, + 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. Include dashboard link in the response.', + inputSchema: { + type: 'object', + properties: VARIABLE_PAGINATION_PROPERTIES, + }, + outputSchema: { + type: 'object' as const, + properties: { + 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. Include dashboard link in the response.', + inputSchema: { + type: 'object', + properties: VARIABLE_COMMON_PROPERTIES, + required: ['key', 'type'], + }, + outputSchema: { + type: 'object' as const, + properties: { + result: VARIABLE_OBJECT_SCHEMA, + dashboardLink: DASHBOARD_LINK_PROPERTY, + }, + 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. Include dashboard link in the response.', + inputSchema: { + type: 'object', + properties: UPDATE_VARIABLE_PROPERTIES, + required: ['key'], + }, + outputSchema: { + type: 'object' as const, + properties: { + result: VARIABLE_OBJECT_SCHEMA, + dashboardLink: DASHBOARD_LINK_PROPERTY, + }, + 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. Include dashboard link in the response.', + inputSchema: { + type: 'object', + properties: { + key: VARIABLE_KEY_PROPERTY, + }, + required: ['key'], + }, + outputSchema: { + type: 'object' as const, + properties: { + result: MESSAGE_RESPONSE_SCHEMA, + dashboardLink: DASHBOARD_LINK_PROPERTY, + }, + required: ['result', 'dashboardLink'], + }, + }, +] + +export const variableToolHandlers: Record<string, ToolHandler> = { + list_variables: async (args: unknown, apiClient: DevCycleApiClient) => { + const validatedArgs = ListVariablesArgsSchema.parse(args) + + return await apiClient.executeWithDashboardLink( + 'listVariables', + validatedArgs, + async (authToken, projectKey) => { + return await handleZodiosValidationErrors( + () => fetchVariables(authToken, projectKey, validatedArgs), + 'fetchVariables', + ) + }, + generateVariablesDashboardLink, + ) + }, + create_variable: async (args: unknown, apiClient: DevCycleApiClient) => { + const validatedArgs = CreateVariableArgsSchema.parse(args) + + return await apiClient.executeWithDashboardLink( + 'createVariable', + validatedArgs, + async (authToken, projectKey) => { + return await handleZodiosValidationErrors( + () => createVariable(authToken, projectKey, validatedArgs), + 'createVariable', + ) + }, + generateVariablesDashboardLink, + ) + }, + update_variable: async (args: unknown, apiClient: DevCycleApiClient) => { + const validatedArgs = UpdateVariableArgsSchema.parse(args) + + return await apiClient.executeWithDashboardLink( + 'updateVariable', + validatedArgs, + async (authToken, projectKey) => { + const { key, ...updateData } = validatedArgs + + return await handleZodiosValidationErrors( + () => + updateVariable(authToken, projectKey, key, updateData), + 'updateVariable', + ) + }, + generateVariablesDashboardLink, + ) + }, + delete_variable: async (args: unknown, apiClient: DevCycleApiClient) => { + const validatedArgs = DeleteVariableArgsSchema.parse(args) + + return await apiClient.executeWithDashboardLink( + 'deleteVariable', + validatedArgs, + async (authToken, projectKey) => { + await handleZodiosValidationErrors( + () => + deleteVariable( + authToken, + projectKey, + validatedArgs.key, + ), + 'deleteVariable', + ) + return { + message: `Variable '${validatedArgs.key}' deleted successfully`, + } + }, + generateVariablesDashboardLink, + ) + }, +} diff --git a/src/mcp/types.ts b/src/mcp/types.ts new file mode 100644 index 000000000..696beb876 --- /dev/null +++ b/src/mcp/types.ts @@ -0,0 +1,189 @@ +import { z } from 'zod' +import { schemas } from '../api/zodClient' +import { UpdateFeatureStatusDto } from '../api/schemas' + +// Zod schemas for MCP tool arguments +export const ListFeaturesArgsSchema = z.object({ + 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({ + 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 + +export const UpdateVariableArgsSchema = schemas.UpdateVariableDto.extend({ + key: z + .string() + .max(100) + .regex(/^[a-z0-9-_.]+$/), // Make key required for identifying the variable +}) + +export const DeleteVariableArgsSchema = z.object({ + key: z.string(), +}) + +export const DeleteFeatureArgsSchema = z.object({ + key: z.string(), +}) + +export const ListProjectsArgsSchema = schemas.GetProjectsParams + +export const CreateProjectArgsSchema = schemas.CreateProjectDto + +export const UpdateProjectArgsSchema = schemas.UpdateProjectDto.extend({ + key: z + .string() + .max(100) + .regex(/^[a-z0-9-_.]+$/), // Make key required for identifying the project +}) + +export const ListEnvironmentsArgsSchema = z.object({ + search: z.string().min(3).optional(), + page: z.number().min(1).optional(), + perPage: z.number().min(1).max(1000).default(100).optional(), + sortBy: z + .enum([ + 'createdAt', + 'updatedAt', + 'name', + 'key', + 'createdBy', + 'propertyKey', + ]) + .optional(), + sortOrder: z.enum(['asc', 'desc']).optional(), + createdBy: z.string().optional(), +}) + +export const GetSdkKeysArgsSchema = z.object({ + environmentKey: z.string(), + keyType: z.enum(['mobile', 'server', 'client']).optional(), +}) + +export const CreateEnvironmentArgsSchema = schemas.CreateEnvironmentDto + +export const UpdateEnvironmentArgsSchema = schemas.UpdateEnvironmentDto.extend({ + key: z.string(), // Make key required for identifying the environment +}) + +export const EnableTargetingArgsSchema = z.object({ + feature_key: z.string(), + environment_key: z.string(), +}) + +export const DisableTargetingArgsSchema = z.object({ + feature_key: z.string(), + environment_key: z.string(), +}) + +export const CreateFeatureArgsSchema = schemas.CreateFeatureDto.extend({ + interactive: z.boolean().optional(), // MCP-specific: prompt for missing fields +}) + +export const UpdateFeatureArgsSchema = schemas.UpdateFeatureDto.extend({ + key: z + .string() + .max(100) + .regex(/^[a-z0-9-_.]+$/), // Make key required for identifying the feature +}) + +export const UpdateFeatureStatusArgsSchema = UpdateFeatureStatusDto.extend({ + key: z + .string() + .max(100) + .regex(/^[a-z0-9-_.]+$/), // Feature key for identifying the feature +}) + +export const UpdateSelfTargetingIdentityArgsSchema = z.object({ + dvc_user_id: z.string().nullable(), +}) + +export const SetSelfTargetingOverrideArgsSchema = z.object({ + feature_key: z.string(), + environment_key: z.string(), + variation_key: z.string(), +}) + +export const ClearSelfTargetingOverridesArgsSchema = z.object({ + feature_key: z.string(), + environment_key: z.string(), +}) + +export const ListVariationsArgsSchema = z.object({ + feature_key: z.string(), +}) + +export const CreateVariationArgsSchema = schemas.CreateVariationDto.extend({ + feature_key: z.string(), // MCP-specific: identifies which feature to add variation to +}) + +export const UpdateVariationArgsSchema = + schemas.UpdateFeatureVariationDto.extend({ + feature_key: z.string(), // MCP-specific: identifies which feature the variation belongs to + variation_key: z.string(), // MCP-specific: identifies which variation to update + variables: z + .record( + z.union([ + z.string(), + z.number(), + z.boolean(), + z.record(z.unknown()), + ]), + ) + .optional(), // Constrain to API-compatible types + }) + +export const ListFeatureTargetingArgsSchema = z.object({ + feature_key: z.string(), + environment_key: z.string().optional(), +}) + +export const UpdateFeatureTargetingArgsSchema = + schemas.UpdateFeatureConfigDto.extend({ + feature_key: z.string(), // MCP-specific: identifies which feature to update targeting for + environment_key: z.string(), // MCP-specific: identifies which environment to update targeting in + }) + +export const GetFeatureAuditLogHistoryArgsSchema = z.object({ + feature_key: z.string(), + days_back: z.number().min(1).max(365).default(30).optional(), +}) diff --git a/src/mcp/utils/api.ts b/src/mcp/utils/api.ts new file mode 100644 index 000000000..75d5888f7 --- /dev/null +++ b/src/mcp/utils/api.ts @@ -0,0 +1,148 @@ +import { DevCycleAuth } from './auth' +import { setMCPToolCommand } from './headers' + +/** + * Utility function to handle Zodios validation errors by extracting response data + * when HTTP call succeeds (200 OK) but schema validation fails + */ +export async function handleZodiosValidationErrors<T>( + apiCall: () => Promise<T>, + operationName?: string, +): Promise<T> { + try { + return await apiCall() + } catch (error) { + // Check if this is a Zodios validation error with successful HTTP response + if ( + error instanceof Error && + error.message.includes('Zodios: Invalid response') && + error.message.includes('status: 200 OK') + ) { + if (operationName) { + console.error( + `MCP ${operationName}: Zodios validation failed but HTTP 200 OK - extracting response data`, + ) + } + + // Extract response data from error object using common patterns + const errorAny = error as any + const responseData = + errorAny.data || // Zodios primary location + errorAny.response?.data || // Axios standard location + errorAny.cause?.data || // Alternative nested location + null + + if (responseData) { + if (operationName) { + console.error( + `Successfully extracted response data for ${operationName}`, + ) + } + return responseData + } + + if (operationName) { + console.error( + `Could not extract response data from Zodios error for ${operationName}`, + ) + } + } + + // Re-throw the original error if we can't extract data + throw error + } +} + +function getErrorMessage(error: unknown): string { + if (error instanceof Error && error.message) { + return error.message + } + if (error && typeof error === 'string') { + return error + } + if (error && typeof error === 'object') { + return JSON.stringify(error) + } + if (error === null) { + return 'null' + } + if (error === undefined) { + return 'undefined' + } + return String(error) +} + +function ensureError(error: unknown): Error { + if (error instanceof Error) { + return error + } + return new Error(String(error)) +} + +export class DevCycleApiClient { + constructor(private auth: DevCycleAuth) {} + + /** + * Helper method to execute API calls with consistent error handling and logging + */ + public async executeWithLogging<T>( + operationName: string, + args: any, + operation: (authToken: string, projectKey: string) => Promise<T>, + requiresProject = true, + ): Promise<T> { + console.error( + `MCP ${operationName} args:`, + args ? JSON.stringify(args, null, 2) : 'none', + ) + + try { + this.auth.requireAuth() + if (requiresProject) { + this.auth.requireProject() + } + + // Set the specific MCP tool command in headers before making API calls + setMCPToolCommand(operationName) + + const authToken = this.auth.getAuthToken() + const projectKey = requiresProject ? this.auth.getProjectKey() : '' + return await operation(authToken, projectKey) + } catch (error) { + console.error( + `MCP ${operationName} error:`, + JSON.stringify({ error: getErrorMessage(error) }, null, 2), + ) + throw ensureError(error) + } + } + + /** + * Helper method to execute API calls and include dashboard links in the response + */ + public async executeWithDashboardLink<T>( + operationName: string, + args: any, + operation: (authToken: string, projectKey: string) => Promise<T>, + 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 + } +} diff --git a/src/mcp/utils/auth.ts b/src/mcp/utils/auth.ts new file mode 100644 index 000000000..bc0a50875 --- /dev/null +++ b/src/mcp/utils/auth.ts @@ -0,0 +1,208 @@ +import * as path from 'path' +import * as os from 'os' +import * as fs from 'fs' +import { ApiAuth } from '../../auth/ApiAuth' +import Writer from '../../ui/writer' + +export class DevCycleAuth { + private apiAuth: ApiAuth + private authPath: string + private cacheDir: string + private writer: Writer + private _authToken = '' + private _projectKey = '' + private _orgId = '' + + constructor() { + this.authPath = path.join( + os.homedir(), + '.config', + 'devcycle', + 'auth.yml', + ) + this.cacheDir = path.join(os.homedir(), '.config', 'devcycle', 'cache') + this.writer = new Writer() + this.writer.headless = true + this.apiAuth = new ApiAuth(this.authPath, this.cacheDir, this.writer) + } + + async initialize(): Promise<void> { + try { + // Try to get auth token from various sources + const flags = { + 'client-id': + process.env.DEVCYCLE_CLIENT_ID || process.env.DVC_CLIENT_ID, + 'client-secret': + process.env.DEVCYCLE_CLIENT_SECRET || + process.env.DVC_CLIENT_SECRET, + } + + // Load project config to get org and project + await this.loadConfig() + + // Get the auth token + this._authToken = await this.apiAuth.getToken(flags, this._orgId) + + if (!this._authToken) { + const hasEnvVars = + process.env.DEVCYCLE_CLIENT_ID && + process.env.DEVCYCLE_CLIENT_SECRET + + if (hasEnvVars) { + throw new Error( + 'Authentication failed with provided environment variables. ' + + 'Please verify your DEVCYCLE_CLIENT_ID and DEVCYCLE_CLIENT_SECRET are correct, ' + + 'or run "dvc login sso" to authenticate with SSO.', + ) + } else { + throw new Error( + 'No authentication found. Please either:\n' + + ' 1. Run "dvc login sso" in the CLI to authenticate with SSO\n' + + ' 2. Or set environment variables:\n' + + ' - DEVCYCLE_CLIENT_ID="your-client-id"\n' + + ' - DEVCYCLE_CLIENT_SECRET="your-client-secret"', + ) + } + } + + if (!this._projectKey) { + const hasProjectEnv = process.env.DEVCYCLE_PROJECT_KEY + + if (hasProjectEnv) { + throw new Error( + `Invalid project key "${hasProjectEnv}" in environment variable. ` + + 'Please verify DEVCYCLE_PROJECT_KEY is correct, or run "dvc projects select" to configure a project.', + ) + } else { + throw new Error( + 'No project configured. Please either:\n' + + ' 1. Run "dvc projects select" in the CLI to choose a project\n' + + ' 2. Or set environment variable: DEVCYCLE_PROJECT_KEY="your-project-key"\n' + + ' 3. Or add project to .devcycle/config.yml in your repository', + ) + } + } + } catch (error) { + console.error( + 'Failed to initialize DevCycle authentication:', + error, + ) + + // Preserve the original error message if it's already detailed + if ( + error instanceof Error && + (error.message.includes('authentication') || + error.message.includes('project') || + error.message.includes('DEVCYCLE_')) + ) { + throw error // Re-throw the original detailed error + } + + // For other errors, wrap with context + const errorMessage = + error instanceof Error ? error.message : 'Unknown error' + throw new Error( + `Failed to initialize DevCycle authentication: ${errorMessage}\n\n` + + 'Common solutions:\n' + + ' 1. Run "dvc status" to check your configuration\n' + + ' 2. Run "dvc login sso" to authenticate\n' + + ' 3. Run "dvc projects select" to choose a project', + ) + } + } + + private async loadConfig(): Promise<void> { + // Try to load project from environment variables first + this._projectKey = + process.env.DEVCYCLE_PROJECT_KEY || + process.env.DVC_PROJECT_KEY || + '' + + // Try to load from repo config + const repoConfigPath = '.devcycle/config.yml' + if (fs.existsSync(repoConfigPath)) { + try { + const yaml = await import('js-yaml') + const configContent = fs.readFileSync(repoConfigPath, 'utf8') + const config = yaml.load(configContent) as any + + if (config?.project && !this._projectKey) { + this._projectKey = config.project + } + if (config?.org?.id) { + this._orgId = config.org.id + } + } catch (error) { + console.error('Error loading repo config:', error) + // Ignore config loading errors, continue with env vars + } + } + + // Try to load from user config + const userConfigPath = path.join( + os.homedir(), + '.config', + 'devcycle', + 'user.yml', + ) + if ( + fs.existsSync(userConfigPath) && + (!this._projectKey || !this._orgId) + ) { + try { + const yaml = await import('js-yaml') + const configContent = fs.readFileSync(userConfigPath, 'utf8') + const config = yaml.load(configContent) as any + + if (config?.project && !this._projectKey) { + this._projectKey = config.project + } + if (config?.org?.id && !this._orgId) { + this._orgId = config.org.id + } + } catch (error) { + console.error('Error loading user config:', error) + // Ignore config loading errors + } + } + } + + getAuthToken(): string { + return this._authToken + } + + getProjectKey(): string { + return this._projectKey + } + + getOrgId(): string { + return this._orgId + } + + hasToken(): boolean { + return this._authToken !== '' + } + + requireAuth(): void { + if (!this.hasToken()) { + throw new Error( + 'Authentication required. Please either:\n' + + ' 1. Run "dvc login sso" to authenticate with SSO\n' + + ' 2. Or set environment variables:\n' + + ' - DEVCYCLE_CLIENT_ID="your-client-id"\n' + + ' - DEVCYCLE_CLIENT_SECRET="your-client-secret"', + ) + } + } + + requireProject(): void { + if (!this._projectKey) { + throw new Error( + 'Project configuration required. Please either:\n' + + ' 1. Run "dvc projects select" to choose a project\n' + + ' 2. Or set environment variable: DEVCYCLE_PROJECT_KEY="your-project-key"\n' + + ' 3. Or add project to .devcycle/config.yml in your repository', + ) + } + } +} diff --git a/src/mcp/utils/headers.ts b/src/mcp/utils/headers.ts new file mode 100644 index 000000000..2ec4aa63e --- /dev/null +++ b/src/mcp/utils/headers.ts @@ -0,0 +1,29 @@ +import { setDVCReferrer } from '../../api/apiClient' + +// Store the version for reuse in tool commands +let mcpVersion: string = 'unknown' + +/** + * Sets up MCP-specific headers for all API requests + * This ensures that API calls made from the MCP server are properly identified + * and can be tracked separately from CLI commands + */ +export function setMCPHeaders(version: string): void { + // Store version for later use in tool commands + mcpVersion = version + + // Set the referrer to identify this as an MCP request + // Command will be set dynamically for each tool call + // Caller is 'mcp' to distinguish from 'cli' and other callers + setDVCReferrer('mcp', version, 'mcp') +} + +/** + * Updates the command in the headers for a specific MCP tool call + * This allows tracking of individual MCP operations (e.g., "list_features", "create_project") + * @param toolName - The name of the MCP tool being called + */ +export function setMCPToolCommand(toolName: string): void { + // Update the command to be the tool name, keeping version and caller the same + setDVCReferrer(toolName, mcpVersion, 'mcp') +} diff --git a/src/ui/targetingTree.ts b/src/ui/targetingTree.ts index b5cf3b510..c24071ba0 100644 --- a/src/ui/targetingTree.ts +++ b/src/ui/targetingTree.ts @@ -43,6 +43,10 @@ const comparatorMap = { '>=': 'is greater than or equal to', '<': 'is less than', '<=': 'is less than or equal to', + endWith: 'ends with', + '!endWith': 'does not end with', + startWith: 'starts with', + '!startWith': 'does not start with', } export const renderTargetingTree = ( diff --git a/yarn.lock b/yarn.lock index caa9d824a..7f09b7100 100644 --- a/yarn.lock +++ b/yarn.lock @@ -68,6 +68,17 @@ __metadata: languageName: node linkType: hard +"@babel/code-frame@npm:^7.27.1": + version: 7.27.1 + resolution: "@babel/code-frame@npm:7.27.1" + dependencies: + "@babel/helper-validator-identifier": "npm:^7.27.1" + js-tokens: "npm:^4.0.0" + picocolors: "npm:^1.1.1" + checksum: 10c0/5dd9a18baa5fce4741ba729acc3a3272c49c25cb8736c4b18e113099520e7ef7b545a4096a26d600e4416157e63e87d66db46aa3fbf0a5f2286da2705c12da00 + languageName: node + linkType: hard + "@babel/compat-data@npm:^7.26.8": version: 7.26.8 resolution: "@babel/compat-data@npm:7.26.8" @@ -75,6 +86,13 @@ __metadata: languageName: node linkType: hard +"@babel/compat-data@npm:^7.27.2": + version: 7.28.0 + resolution: "@babel/compat-data@npm:7.28.0" + checksum: 10c0/c4e527302bcd61052423f757355a71c3bc62362bac13f7f130de16e439716f66091ff5bdecda418e8fa0271d4c725f860f0ee23ab7bf6e769f7a8bb16dfcb531 + languageName: node + linkType: hard + "@babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.20.12": version: 7.26.10 resolution: "@babel/core@npm:7.26.10" @@ -98,6 +116,29 @@ __metadata: languageName: node linkType: hard +"@babel/core@npm:^7.28.0": + version: 7.28.0 + resolution: "@babel/core@npm:7.28.0" + dependencies: + "@ampproject/remapping": "npm:^2.2.0" + "@babel/code-frame": "npm:^7.27.1" + "@babel/generator": "npm:^7.28.0" + "@babel/helper-compilation-targets": "npm:^7.27.2" + "@babel/helper-module-transforms": "npm:^7.27.3" + "@babel/helpers": "npm:^7.27.6" + "@babel/parser": "npm:^7.28.0" + "@babel/template": "npm:^7.27.2" + "@babel/traverse": "npm:^7.28.0" + "@babel/types": "npm:^7.28.0" + convert-source-map: "npm:^2.0.0" + debug: "npm:^4.1.0" + gensync: "npm:^1.0.0-beta.2" + json5: "npm:^2.2.3" + semver: "npm:^6.3.1" + checksum: 10c0/423302e7c721e73b1c096217880272e02020dfb697a55ccca60ad01bba90037015f84d0c20c6ce297cf33a19bb704bc5c2b3d3095f5284dfa592bd1de0b9e8c3 + languageName: node + linkType: hard + "@babel/generator@npm:^7.26.10, @babel/generator@npm:^7.27.0, @babel/generator@npm:^7.7.2": version: 7.27.0 resolution: "@babel/generator@npm:7.27.0" @@ -111,6 +152,19 @@ __metadata: languageName: node linkType: hard +"@babel/generator@npm:^7.28.0": + version: 7.28.0 + resolution: "@babel/generator@npm:7.28.0" + dependencies: + "@babel/parser": "npm:^7.28.0" + "@babel/types": "npm:^7.28.0" + "@jridgewell/gen-mapping": "npm:^0.3.12" + "@jridgewell/trace-mapping": "npm:^0.3.28" + jsesc: "npm:^3.0.2" + checksum: 10c0/1b3d122268ea3df50fde707ad864d9a55c72621357d5cebb972db3dd76859c45810c56e16ad23123f18f80cc2692f5a015d2858361300f0f224a05dc43d36a92 + languageName: node + linkType: hard + "@babel/helper-compilation-targets@npm:^7.26.5": version: 7.27.0 resolution: "@babel/helper-compilation-targets@npm:7.27.0" @@ -124,6 +178,26 @@ __metadata: languageName: node linkType: hard +"@babel/helper-compilation-targets@npm:^7.27.2": + version: 7.27.2 + resolution: "@babel/helper-compilation-targets@npm:7.27.2" + dependencies: + "@babel/compat-data": "npm:^7.27.2" + "@babel/helper-validator-option": "npm:^7.27.1" + browserslist: "npm:^4.24.0" + lru-cache: "npm:^5.1.1" + semver: "npm:^6.3.1" + checksum: 10c0/f338fa00dcfea931804a7c55d1a1c81b6f0a09787e528ec580d5c21b3ecb3913f6cb0f361368973ce953b824d910d3ac3e8a8ee15192710d3563826447193ad1 + languageName: node + linkType: hard + +"@babel/helper-globals@npm:^7.28.0": + version: 7.28.0 + resolution: "@babel/helper-globals@npm:7.28.0" + checksum: 10c0/5a0cd0c0e8c764b5f27f2095e4243e8af6fa145daea2b41b53c0c1414fe6ff139e3640f4e2207ae2b3d2153a1abd346f901c26c290ee7cb3881dd922d4ee9232 + languageName: node + linkType: hard + "@babel/helper-module-imports@npm:^7.25.9": version: 7.25.9 resolution: "@babel/helper-module-imports@npm:7.25.9" @@ -134,6 +208,16 @@ __metadata: languageName: node linkType: hard +"@babel/helper-module-imports@npm:^7.27.1": + version: 7.27.1 + resolution: "@babel/helper-module-imports@npm:7.27.1" + dependencies: + "@babel/traverse": "npm:^7.27.1" + "@babel/types": "npm:^7.27.1" + checksum: 10c0/e00aace096e4e29290ff8648455c2bc4ed982f0d61dbf2db1b5e750b9b98f318bf5788d75a4f974c151bd318fd549e81dbcab595f46b14b81c12eda3023f51e8 + languageName: node + linkType: hard + "@babel/helper-module-transforms@npm:^7.26.0": version: 7.26.0 resolution: "@babel/helper-module-transforms@npm:7.26.0" @@ -147,6 +231,19 @@ __metadata: languageName: node linkType: hard +"@babel/helper-module-transforms@npm:^7.27.3": + version: 7.27.3 + resolution: "@babel/helper-module-transforms@npm:7.27.3" + dependencies: + "@babel/helper-module-imports": "npm:^7.27.1" + "@babel/helper-validator-identifier": "npm:^7.27.1" + "@babel/traverse": "npm:^7.27.3" + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 10c0/fccb4f512a13b4c069af51e1b56b20f54024bcf1591e31e978a30f3502567f34f90a80da6a19a6148c249216292a8074a0121f9e52602510ef0f32dbce95ca01 + languageName: node + linkType: hard + "@babel/helper-plugin-utils@npm:^7.0.0, @babel/helper-plugin-utils@npm:^7.10.4, @babel/helper-plugin-utils@npm:^7.12.13, @babel/helper-plugin-utils@npm:^7.14.5, @babel/helper-plugin-utils@npm:^7.25.9, @babel/helper-plugin-utils@npm:^7.8.0": version: 7.26.5 resolution: "@babel/helper-plugin-utils@npm:7.26.5" @@ -161,6 +258,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-string-parser@npm:^7.27.1": + version: 7.27.1 + resolution: "@babel/helper-string-parser@npm:7.27.1" + checksum: 10c0/8bda3448e07b5583727c103560bcf9c4c24b3c1051a4c516d4050ef69df37bb9a4734a585fe12725b8c2763de0a265aa1e909b485a4e3270b7cfd3e4dbe4b602 + languageName: node + linkType: hard + "@babel/helper-validator-identifier@npm:^7.25.9": version: 7.25.9 resolution: "@babel/helper-validator-identifier@npm:7.25.9" @@ -168,6 +272,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-validator-identifier@npm:^7.27.1": + version: 7.27.1 + resolution: "@babel/helper-validator-identifier@npm:7.27.1" + checksum: 10c0/c558f11c4871d526498e49d07a84752d1800bf72ac0d3dad100309a2eaba24efbf56ea59af5137ff15e3a00280ebe588560534b0e894a4750f8b1411d8f78b84 + languageName: node + linkType: hard + "@babel/helper-validator-option@npm:^7.25.9": version: 7.25.9 resolution: "@babel/helper-validator-option@npm:7.25.9" @@ -175,6 +286,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-validator-option@npm:^7.27.1": + version: 7.27.1 + resolution: "@babel/helper-validator-option@npm:7.27.1" + checksum: 10c0/6fec5f006eba40001a20f26b1ef5dbbda377b7b68c8ad518c05baa9af3f396e780bdfded24c4eef95d14bb7b8fd56192a6ed38d5d439b97d10efc5f1a191d148 + languageName: node + linkType: hard + "@babel/helpers@npm:^7.26.10": version: 7.27.0 resolution: "@babel/helpers@npm:7.27.0" @@ -185,6 +303,16 @@ __metadata: languageName: node linkType: hard +"@babel/helpers@npm:^7.27.6": + version: 7.27.6 + resolution: "@babel/helpers@npm:7.27.6" + dependencies: + "@babel/template": "npm:^7.27.2" + "@babel/types": "npm:^7.27.6" + checksum: 10c0/448bac96ef8b0f21f2294a826df9de6bf4026fd023f8a6bb6c782fe3e61946801ca24381490b8e58d861fee75cd695a1882921afbf1f53b0275ee68c938bd6d3 + languageName: node + linkType: hard + "@babel/parser@npm:^7.14.7, @babel/parser@npm:^7.15.7, @babel/parser@npm:^7.26.10, @babel/parser@npm:^7.27.0": version: 7.27.0 resolution: "@babel/parser@npm:7.27.0" @@ -196,6 +324,17 @@ __metadata: languageName: node linkType: hard +"@babel/parser@npm:^7.27.2, @babel/parser@npm:^7.28.0": + version: 7.28.0 + resolution: "@babel/parser@npm:7.28.0" + dependencies: + "@babel/types": "npm:^7.28.0" + bin: + parser: ./bin/babel-parser.js + checksum: 10c0/c2ef81d598990fa949d1d388429df327420357cb5200271d0d0a2784f1e6d54afc8301eb8bdf96d8f6c77781e402da93c7dc07980fcc136ac5b9d5f1fce701b5 + languageName: node + linkType: hard + "@babel/plugin-syntax-async-generators@npm:^7.8.4": version: 7.8.4 resolution: "@babel/plugin-syntax-async-generators@npm:7.8.4" @@ -370,6 +509,17 @@ __metadata: languageName: node linkType: hard +"@babel/template@npm:^7.27.2": + version: 7.27.2 + resolution: "@babel/template@npm:7.27.2" + dependencies: + "@babel/code-frame": "npm:^7.27.1" + "@babel/parser": "npm:^7.27.2" + "@babel/types": "npm:^7.27.1" + checksum: 10c0/ed9e9022651e463cc5f2cc21942f0e74544f1754d231add6348ff1b472985a3b3502041c0be62dc99ed2d12cfae0c51394bf827452b98a2f8769c03b87aadc81 + languageName: node + linkType: hard + "@babel/traverse@npm:^7.25.9, @babel/traverse@npm:^7.26.10": version: 7.27.0 resolution: "@babel/traverse@npm:7.27.0" @@ -385,6 +535,21 @@ __metadata: languageName: node linkType: hard +"@babel/traverse@npm:^7.27.1, @babel/traverse@npm:^7.27.3, @babel/traverse@npm:^7.28.0": + version: 7.28.0 + resolution: "@babel/traverse@npm:7.28.0" + dependencies: + "@babel/code-frame": "npm:^7.27.1" + "@babel/generator": "npm:^7.28.0" + "@babel/helper-globals": "npm:^7.28.0" + "@babel/parser": "npm:^7.28.0" + "@babel/template": "npm:^7.27.2" + "@babel/types": "npm:^7.28.0" + debug: "npm:^4.3.1" + checksum: 10c0/32794402457827ac558173bcebdcc0e3a18fa339b7c41ca35621f9f645f044534d91bb923ff385f5f960f2e495f56ce18d6c7b0d064d2f0ccb55b285fa6bc7b9 + languageName: node + linkType: hard + "@babel/types@npm:^7.25.9, @babel/types@npm:^7.26.10, @babel/types@npm:^7.27.0, @babel/types@npm:^7.3.3": version: 7.27.0 resolution: "@babel/types@npm:7.27.0" @@ -395,6 +560,16 @@ __metadata: languageName: node linkType: hard +"@babel/types@npm:^7.27.1, @babel/types@npm:^7.27.6, @babel/types@npm:^7.28.0": + version: 7.28.0 + resolution: "@babel/types@npm:7.28.0" + dependencies: + "@babel/helper-string-parser": "npm:^7.27.1" + "@babel/helper-validator-identifier": "npm:^7.27.1" + checksum: 10c0/7ca8521bf5e2d2ed4db31176efaaf94463a6b7a4d16dcc60e34e963b3596c2ecadb85457bebed13a9ee9a5829ef5f515d05b55a991b6a8f3b835451843482e39 + languageName: node + linkType: hard + "@cspotcode/source-map-support@npm:^0.8.0": version: 0.8.1 resolution: "@cspotcode/source-map-support@npm:0.8.1" @@ -408,8 +583,16 @@ __metadata: version: 0.0.0-use.local resolution: "@devcycle/cli@workspace:." dependencies: - "@babel/parser": "npm:^7.27.0" + "@babel/code-frame": "npm:^7.27.1" + "@babel/core": "npm:^7.28.0" + "@babel/generator": "npm:^7.28.0" + "@babel/helper-validator-identifier": "npm:^7.27.1" + "@babel/parser": "npm:^7.28.0" + "@babel/template": "npm:^7.27.2" + "@babel/traverse": "npm:^7.28.0" + "@babel/types": "npm:^7.28.0" "@eslint/js": "npm:^9.18.0" + "@modelcontextprotocol/sdk": "npm:^1.15.0" "@oclif/core": "npm:^2.16.0" "@oclif/plugin-autocomplete": "npm:^2.3.10" "@oclif/plugin-help": "npm:^6.2.27" @@ -459,6 +642,7 @@ __metadata: zod: "npm:^3.24.2" bin: dvc: ./bin/run + dvc-mcp: ./bin/mcp languageName: unknown linkType: soft @@ -715,6 +899,16 @@ __metadata: languageName: node linkType: hard +"@jridgewell/gen-mapping@npm:^0.3.12": + version: 0.3.12 + resolution: "@jridgewell/gen-mapping@npm:0.3.12" + dependencies: + "@jridgewell/sourcemap-codec": "npm:^1.5.0" + "@jridgewell/trace-mapping": "npm:^0.3.24" + checksum: 10c0/32f771ae2467e4d440be609581f7338d786d3d621bac3469e943b9d6d116c23c4becb36f84898a92bbf2f3c0511365c54a945a3b86a83141547a2a360a5ec0c7 + languageName: node + linkType: hard + "@jridgewell/gen-mapping@npm:^0.3.5": version: 0.3.5 resolution: "@jridgewell/gen-mapping@npm:0.3.5" @@ -747,6 +941,13 @@ __metadata: languageName: node linkType: hard +"@jridgewell/sourcemap-codec@npm:^1.5.0": + version: 1.5.4 + resolution: "@jridgewell/sourcemap-codec@npm:1.5.4" + checksum: 10c0/c5aab3e6362a8dd94ad80ab90845730c825fc4c8d9cf07ebca7a2eb8a832d155d62558800fc41d42785f989ddbb21db6df004d1786e8ecb65e428ab8dff71309 + languageName: node + linkType: hard + "@jridgewell/trace-mapping@npm:0.3.9": version: 0.3.9 resolution: "@jridgewell/trace-mapping@npm:0.3.9" @@ -767,6 +968,16 @@ __metadata: languageName: node linkType: hard +"@jridgewell/trace-mapping@npm:^0.3.28": + version: 0.3.29 + resolution: "@jridgewell/trace-mapping@npm:0.3.29" + dependencies: + "@jridgewell/resolve-uri": "npm:^3.1.0" + "@jridgewell/sourcemap-codec": "npm:^1.4.14" + checksum: 10c0/fb547ba31658c4d74eb17e7389f4908bf7c44cef47acb4c5baa57289daf68e6fe53c639f41f751b3923aca67010501264f70e7b49978ad1f040294b22c37b333 + languageName: node + linkType: hard + "@jsdevtools/ono@npm:^7.1.3": version: 7.1.3 resolution: "@jsdevtools/ono@npm:7.1.3" @@ -784,6 +995,26 @@ __metadata: languageName: node linkType: hard +"@modelcontextprotocol/sdk@npm:^1.15.0": + version: 1.15.0 + resolution: "@modelcontextprotocol/sdk@npm:1.15.0" + dependencies: + ajv: "npm:^6.12.6" + content-type: "npm:^1.0.5" + cors: "npm:^2.8.5" + cross-spawn: "npm:^7.0.5" + eventsource: "npm:^3.0.2" + eventsource-parser: "npm:^3.0.0" + express: "npm:^5.0.1" + express-rate-limit: "npm:^7.5.0" + pkce-challenge: "npm:^5.0.0" + raw-body: "npm:^3.0.0" + zod: "npm:^3.23.8" + zod-to-json-schema: "npm:^3.24.1" + checksum: 10c0/ab94ce1ef997c6109dda8de60b9cbfaff96904a24971fb0bdc5c36ac8ae8c58f2b66faac58e654fdb0dc11627348192d8ade8e772af3b8da15022adb9fcd255a + languageName: node + linkType: hard + "@nodelib/fs.scandir@npm:2.1.5": version: 2.1.5 resolution: "@nodelib/fs.scandir@npm:2.1.5" @@ -1964,6 +2195,16 @@ __metadata: languageName: node linkType: hard +"accepts@npm:^2.0.0": + version: 2.0.0 + resolution: "accepts@npm:2.0.0" + dependencies: + mime-types: "npm:^3.0.0" + negotiator: "npm:^1.0.0" + checksum: 10c0/98374742097e140891546076215f90c32644feacf652db48412329de4c2a529178a81aa500fbb13dd3e6cbf6e68d829037b123ac037fc9a08bcec4b87b358eef + languageName: node + linkType: hard + "acorn-jsx@npm:^5.3.2": version: 5.3.2 resolution: "acorn-jsx@npm:5.3.2" @@ -2047,7 +2288,7 @@ __metadata: languageName: node linkType: hard -"ajv@npm:^6.12.4": +"ajv@npm:^6.12.4, ajv@npm:^6.12.6": version: 6.12.6 resolution: "ajv@npm:6.12.6" dependencies: @@ -2416,6 +2657,23 @@ __metadata: languageName: node linkType: hard +"body-parser@npm:^2.2.0": + version: 2.2.0 + resolution: "body-parser@npm:2.2.0" + dependencies: + bytes: "npm:^3.1.2" + content-type: "npm:^1.0.5" + debug: "npm:^4.4.0" + http-errors: "npm:^2.0.0" + iconv-lite: "npm:^0.6.3" + on-finished: "npm:^2.4.1" + qs: "npm:^6.14.0" + raw-body: "npm:^3.0.0" + type-is: "npm:^2.0.0" + checksum: 10c0/a9ded39e71ac9668e2211afa72e82ff86cc5ef94de1250b7d1ba9cc299e4150408aaa5f1e8b03dd4578472a3ce6d1caa2a23b27a6c18e526e48b4595174c116c + languageName: node + linkType: hard + "brace-expansion@npm:^1.1.7": version: 1.1.12 resolution: "brace-expansion@npm:1.1.12" @@ -2521,6 +2779,13 @@ __metadata: languageName: node linkType: hard +"bytes@npm:3.1.2, bytes@npm:^3.1.2": + version: 3.1.2 + resolution: "bytes@npm:3.1.2" + checksum: 10c0/76d1c43cbd602794ad8ad2ae94095cddeb1de78c5dddaa7005c51af10b0176c69971a6d88e805a90c2b6550d76636e43c40d8427a808b8645ede885de4a0358e + languageName: node + linkType: hard + "cac@npm:^6.7.14": version: 6.7.14 resolution: "cac@npm:6.7.14" @@ -2642,6 +2907,16 @@ __metadata: languageName: node linkType: hard +"call-bind-apply-helpers@npm:^1.0.1, call-bind-apply-helpers@npm:^1.0.2": + version: 1.0.2 + resolution: "call-bind-apply-helpers@npm:1.0.2" + dependencies: + es-errors: "npm:^1.3.0" + function-bind: "npm:^1.1.2" + checksum: 10c0/47bd9901d57b857590431243fea704ff18078b16890a6b3e021e12d279bbf211d039155e27d7566b374d49ee1f8189344bac9833dec7a20cdec370506361c938 + languageName: node + linkType: hard + "call-bind@npm:^1.0.2, call-bind@npm:^1.0.7": version: 1.0.7 resolution: "call-bind@npm:1.0.7" @@ -2655,6 +2930,16 @@ __metadata: languageName: node linkType: hard +"call-bound@npm:^1.0.2": + version: 1.0.4 + resolution: "call-bound@npm:1.0.4" + dependencies: + call-bind-apply-helpers: "npm:^1.0.2" + get-intrinsic: "npm:^1.3.0" + checksum: 10c0/f4796a6a0941e71c766aea672f63b72bc61234c4f4964dc6d7606e3664c307e7d77845328a8f3359ce39ddb377fed67318f9ee203dea1d47e46165dcf2917644 + languageName: node + linkType: hard + "call-me-maybe@npm:^1.0.1": version: 1.0.2 resolution: "call-me-maybe@npm:1.0.2" @@ -3029,7 +3314,16 @@ __metadata: languageName: node linkType: hard -"content-type@npm:^1.0.4": +"content-disposition@npm:^1.0.0": + version: 1.0.0 + resolution: "content-disposition@npm:1.0.0" + dependencies: + safe-buffer: "npm:5.2.1" + checksum: 10c0/c7b1ba0cea2829da0352ebc1b7f14787c73884bc707c8bc2271d9e3bf447b372270d09f5d3980dc5037c749ceef56b9a13fccd0b0001c87c3f12579967e4dd27 + languageName: node + linkType: hard + +"content-type@npm:^1.0.4, content-type@npm:^1.0.5": version: 1.0.5 resolution: "content-type@npm:1.0.5" checksum: 10c0/b76ebed15c000aee4678c3707e0860cb6abd4e680a598c0a26e17f0bfae723ec9cc2802f0ff1bc6e4d80603719010431d2231018373d4dde10f9ccff9dadf5af @@ -3043,6 +3337,20 @@ __metadata: languageName: node linkType: hard +"cookie-signature@npm:^1.2.1": + version: 1.2.2 + resolution: "cookie-signature@npm:1.2.2" + checksum: 10c0/54e05df1a293b3ce81589b27dddc445f462f6fa6812147c033350cd3561a42bc14481674e05ed14c7bd0ce1e8bb3dc0e40851bad75415733711294ddce0b7bc6 + languageName: node + linkType: hard + +"cookie@npm:^0.7.1": + version: 0.7.2 + resolution: "cookie@npm:0.7.2" + checksum: 10c0/9596e8ccdbf1a3a88ae02cf5ee80c1c50959423e1022e4e60b91dd87c622af1da309253d8abdb258fb5e3eacb4f08e579dc58b4897b8087574eee0fd35dfa5d2 + languageName: node + linkType: hard + "core-util-is@npm:~1.0.0": version: 1.0.3 resolution: "core-util-is@npm:1.0.3" @@ -3050,6 +3358,16 @@ __metadata: languageName: node linkType: hard +"cors@npm:^2.8.5": + version: 2.8.5 + resolution: "cors@npm:2.8.5" + dependencies: + object-assign: "npm:^4" + vary: "npm:^1" + checksum: 10c0/373702b7999409922da80de4a61938aabba6929aea5b6fd9096fefb9e8342f626c0ebd7507b0e8b0b311380744cc985f27edebc0a26e0ddb784b54e1085de761 + languageName: node + linkType: hard + "create-require@npm:^1.1.0": version: 1.1.1 resolution: "create-require@npm:1.1.1" @@ -3057,7 +3375,7 @@ __metadata: languageName: node linkType: hard -"cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.3, cross-spawn@npm:^7.0.6": +"cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.3, cross-spawn@npm:^7.0.5, cross-spawn@npm:^7.0.6": version: 7.0.6 resolution: "cross-spawn@npm:7.0.6" dependencies: @@ -3219,6 +3537,13 @@ __metadata: languageName: node linkType: hard +"depd@npm:2.0.0, depd@npm:^2.0.0": + version: 2.0.0 + resolution: "depd@npm:2.0.0" + checksum: 10c0/58bd06ec20e19529b06f7ad07ddab60e504d9e0faca4bd23079fac2d279c3594334d736508dc350e06e510aba5e22e4594483b3a6562ce7c17dd797f4cc4ad2c + languageName: node + linkType: hard + "deprecation@npm:^2.0.0, deprecation@npm:^2.3.1": version: 2.3.1 resolution: "deprecation@npm:2.3.1" @@ -3273,6 +3598,17 @@ __metadata: languageName: node linkType: hard +"dunder-proto@npm:^1.0.1": + version: 1.0.1 + resolution: "dunder-proto@npm:1.0.1" + dependencies: + call-bind-apply-helpers: "npm:^1.0.1" + es-errors: "npm:^1.3.0" + gopd: "npm:^1.2.0" + checksum: 10c0/199f2a0c1c16593ca0a145dbf76a962f8033ce3129f01284d48c45ed4e14fea9bbacd7b3610b6cdc33486cef20385ac054948fefc6272fcce645c09468f93031 + languageName: node + linkType: hard + "eastasianwidth@npm:^0.2.0": version: 0.2.0 resolution: "eastasianwidth@npm:0.2.0" @@ -3280,6 +3616,13 @@ __metadata: languageName: node linkType: hard +"ee-first@npm:1.1.1": + version: 1.1.1 + resolution: "ee-first@npm:1.1.1" + checksum: 10c0/b5bb125ee93161bc16bfe6e56c6b04de5ad2aa44234d8f644813cc95d861a6910903132b05093706de2b706599367c4130eb6d170f6b46895686b95f87d017b7 + languageName: node + linkType: hard + "ejs@npm:^3.1.10, ejs@npm:^3.1.8": version: 3.1.10 resolution: "ejs@npm:3.1.10" @@ -3312,6 +3655,13 @@ __metadata: languageName: node linkType: hard +"encodeurl@npm:^2.0.0": + version: 2.0.0 + resolution: "encodeurl@npm:2.0.0" + checksum: 10c0/5d317306acb13e6590e28e27924c754163946a2480de11865c991a3a7eed4315cd3fba378b543ca145829569eefe9b899f3d84bb09870f675ae60bc924b01ceb + languageName: node + linkType: hard + "encoding@npm:^0.1.12, encoding@npm:^0.1.13": version: 0.1.13 resolution: "encoding@npm:0.1.13" @@ -3369,6 +3719,13 @@ __metadata: languageName: node linkType: hard +"es-define-property@npm:^1.0.1": + version: 1.0.1 + resolution: "es-define-property@npm:1.0.1" + checksum: 10c0/3f54eb49c16c18707949ff25a1456728c883e81259f045003499efba399c08bad00deebf65cccde8c0e07908c1a225c9d472b7107e558f2a48e28d530e34527c + languageName: node + linkType: hard + "es-errors@npm:^1.3.0": version: 1.3.0 resolution: "es-errors@npm:1.3.0" @@ -3376,6 +3733,15 @@ __metadata: languageName: node linkType: hard +"es-object-atoms@npm:^1.0.0, es-object-atoms@npm:^1.1.1": + version: 1.1.1 + resolution: "es-object-atoms@npm:1.1.1" + dependencies: + es-errors: "npm:^1.3.0" + checksum: 10c0/65364812ca4daf48eb76e2a3b7a89b3f6a2e62a1c420766ce9f692665a29d94fe41fe88b65f24106f449859549711e4b40d9fb8002d862dfd7eb1c512d10be0c + languageName: node + linkType: hard + "escalade@npm:^3.1.1": version: 3.1.2 resolution: "escalade@npm:3.1.2" @@ -3390,6 +3756,13 @@ __metadata: languageName: node linkType: hard +"escape-html@npm:^1.0.3": + version: 1.0.3 + resolution: "escape-html@npm:1.0.3" + checksum: 10c0/524c739d776b36c3d29fa08a22e03e8824e3b2fd57500e5e44ecf3cc4707c34c60f9ca0781c0e33d191f2991161504c295e98f68c78fe7baa6e57081ec6ac0a3 + languageName: node + linkType: hard + "escape-string-regexp@npm:4.0.0, escape-string-regexp@npm:^4.0.0": version: 4.0.0 resolution: "escape-string-regexp@npm:4.0.0" @@ -3549,6 +3922,13 @@ __metadata: languageName: node linkType: hard +"etag@npm:^1.8.1": + version: 1.8.1 + resolution: "etag@npm:1.8.1" + checksum: 10c0/12be11ef62fb9817314d790089a0a49fae4e1b50594135dcb8076312b7d7e470884b5100d249b28c18581b7fd52f8b485689ffae22a11ed9ec17377a33a08f84 + languageName: node + linkType: hard + "eval-estree-expression@npm:^1.0.1": version: 1.1.0 resolution: "eval-estree-expression@npm:1.1.0" @@ -3584,6 +3964,22 @@ __metadata: languageName: node linkType: hard +"eventsource-parser@npm:^3.0.0, eventsource-parser@npm:^3.0.1": + version: 3.0.3 + resolution: "eventsource-parser@npm:3.0.3" + checksum: 10c0/2594011630efba56cafafc8ed6bd9a50db8f6d5dd62089b0950346e7961828c16efe07a588bdea3ba79e568fd9246c8163824a2ffaade767e1fdb2270c1fae0b + languageName: node + linkType: hard + +"eventsource@npm:^3.0.2": + version: 3.0.7 + resolution: "eventsource@npm:3.0.7" + dependencies: + eventsource-parser: "npm:^3.0.1" + checksum: 10c0/c48a73c38f300e33e9f11375d4ee969f25cbb0519608a12378a38068055ae8b55b6e0e8a49c3f91c784068434efe1d9f01eb49b6315b04b0da9157879ce2f67d + languageName: node + linkType: hard + "execa@npm:^5.0.0, execa@npm:^5.1.1": version: 5.1.1 resolution: "execa@npm:5.1.1" @@ -3621,6 +4017,50 @@ __metadata: languageName: node linkType: hard +"express-rate-limit@npm:^7.5.0": + version: 7.5.1 + resolution: "express-rate-limit@npm:7.5.1" + peerDependencies: + express: ">= 4.11" + checksum: 10c0/b07de84d700a2c07c4bf2f040e7558ed5a1f660f03ed5f30bf8ff7b51e98ba7a85215640e70fc48cbbb9151066ea51239d9a1b41febc9b84d98c7915b0186161 + languageName: node + linkType: hard + +"express@npm:^5.0.1": + version: 5.1.0 + resolution: "express@npm:5.1.0" + dependencies: + accepts: "npm:^2.0.0" + body-parser: "npm:^2.2.0" + content-disposition: "npm:^1.0.0" + content-type: "npm:^1.0.5" + cookie: "npm:^0.7.1" + cookie-signature: "npm:^1.2.1" + debug: "npm:^4.4.0" + encodeurl: "npm:^2.0.0" + escape-html: "npm:^1.0.3" + etag: "npm:^1.8.1" + finalhandler: "npm:^2.1.0" + fresh: "npm:^2.0.0" + http-errors: "npm:^2.0.0" + merge-descriptors: "npm:^2.0.0" + mime-types: "npm:^3.0.0" + on-finished: "npm:^2.4.1" + once: "npm:^1.4.0" + parseurl: "npm:^1.3.3" + proxy-addr: "npm:^2.0.7" + qs: "npm:^6.14.0" + range-parser: "npm:^1.2.1" + router: "npm:^2.2.0" + send: "npm:^1.1.0" + serve-static: "npm:^2.2.0" + statuses: "npm:^2.0.1" + type-is: "npm:^2.0.1" + vary: "npm:^1.1.2" + checksum: 10c0/80ce7c53c5f56887d759b94c3f2283e2e51066c98d4b72a4cc1338e832b77f1e54f30d0239cc10815a0f849bdb753e6a284d2fa48d4ab56faf9c501f55d751d6 + languageName: node + linkType: hard + "external-editor@npm:^3.0.3": version: 3.1.0 resolution: "external-editor@npm:3.1.0" @@ -3765,6 +4205,20 @@ __metadata: languageName: node linkType: hard +"finalhandler@npm:^2.1.0": + version: 2.1.0 + resolution: "finalhandler@npm:2.1.0" + dependencies: + debug: "npm:^4.4.0" + encodeurl: "npm:^2.0.0" + escape-html: "npm:^1.0.3" + on-finished: "npm:^2.4.1" + parseurl: "npm:^1.3.3" + statuses: "npm:^2.0.1" + checksum: 10c0/da0bbca6d03873472ee890564eb2183f4ed377f25f3628a0fc9d16dac40bed7b150a0d82ebb77356e4c6d97d2796ad2dba22948b951dddee2c8768b0d1b9fb1f + languageName: node + linkType: hard + "find-package-json@npm:^1.2.0": version: 1.2.0 resolution: "find-package-json@npm:1.2.0" @@ -3886,6 +4340,20 @@ __metadata: languageName: node linkType: hard +"forwarded@npm:0.2.0": + version: 0.2.0 + resolution: "forwarded@npm:0.2.0" + checksum: 10c0/9b67c3fac86acdbc9ae47ba1ddd5f2f81526fa4c8226863ede5600a3f7c7416ef451f6f1e240a3cc32d0fd79fcfe6beb08fd0da454f360032bde70bf80afbb33 + languageName: node + linkType: hard + +"fresh@npm:^2.0.0": + version: 2.0.0 + resolution: "fresh@npm:2.0.0" + checksum: 10c0/0557548194cb9a809a435bf92bcfbc20c89e8b5eb38861b73ced36750437251e39a111fc3a18b98531be9dd91fe1411e4969f229dc579ec0251ce6c5d4900bbc + languageName: node + linkType: hard + "fs-extra@npm:^10.1.0": version: 10.1.0 resolution: "fs-extra@npm:10.1.0" @@ -4026,6 +4494,24 @@ __metadata: languageName: node linkType: hard +"get-intrinsic@npm:^1.2.5, get-intrinsic@npm:^1.3.0": + version: 1.3.0 + resolution: "get-intrinsic@npm:1.3.0" + dependencies: + call-bind-apply-helpers: "npm:^1.0.2" + es-define-property: "npm:^1.0.1" + es-errors: "npm:^1.3.0" + es-object-atoms: "npm:^1.1.1" + function-bind: "npm:^1.1.2" + get-proto: "npm:^1.0.1" + gopd: "npm:^1.2.0" + has-symbols: "npm:^1.1.0" + hasown: "npm:^2.0.2" + math-intrinsics: "npm:^1.1.0" + checksum: 10c0/52c81808af9a8130f581e6a6a83e1ba4a9f703359e7a438d1369a5267a25412322f03dcbd7c549edaef0b6214a0630a28511d7df0130c93cfd380f4fa0b5b66a + languageName: node + linkType: hard + "get-package-type@npm:^0.1.0": version: 0.1.0 resolution: "get-package-type@npm:0.1.0" @@ -4033,6 +4519,16 @@ __metadata: languageName: node linkType: hard +"get-proto@npm:^1.0.1": + version: 1.0.1 + resolution: "get-proto@npm:1.0.1" + dependencies: + dunder-proto: "npm:^1.0.1" + es-object-atoms: "npm:^1.0.0" + checksum: 10c0/9224acb44603c5526955e83510b9da41baf6ae73f7398875fba50edc5e944223a89c4a72b070fcd78beb5f7bdda58ecb6294adc28f7acfc0da05f76a2399643c + languageName: node + linkType: hard + "get-stream@npm:^5.1.0": version: 5.2.0 resolution: "get-stream@npm:5.2.0" @@ -4178,6 +4674,13 @@ __metadata: languageName: node linkType: hard +"gopd@npm:^1.2.0": + version: 1.2.0 + resolution: "gopd@npm:1.2.0" + checksum: 10c0/50fff1e04ba2b7737c097358534eacadad1e68d24cccee3272e04e007bed008e68d2614f3987788428fd192a5ae3889d08fb2331417e4fc4a9ab366b2043cead + languageName: node + linkType: hard + "got@npm:^11": version: 11.8.6 resolution: "got@npm:11.8.6" @@ -4266,6 +4769,13 @@ __metadata: languageName: node linkType: hard +"has-symbols@npm:^1.1.0": + version: 1.1.0 + resolution: "has-symbols@npm:1.1.0" + checksum: 10c0/dde0a734b17ae51e84b10986e651c664379018d10b91b6b0e9b293eddb32f0f069688c841fb40f19e9611546130153e0a2a48fd7f512891fb000ddfa36f5a20e + languageName: node + linkType: hard + "has-tostringtag@npm:^1.0.0, has-tostringtag@npm:^1.0.2": version: 1.0.2 resolution: "has-tostringtag@npm:1.0.2" @@ -4282,7 +4792,7 @@ __metadata: languageName: node linkType: hard -"hasown@npm:^2.0.0": +"hasown@npm:^2.0.0, hasown@npm:^2.0.2": version: 2.0.2 resolution: "hasown@npm:2.0.2" dependencies: @@ -4346,6 +4856,19 @@ __metadata: languageName: node linkType: hard +"http-errors@npm:2.0.0, http-errors@npm:^2.0.0": + version: 2.0.0 + resolution: "http-errors@npm:2.0.0" + dependencies: + depd: "npm:2.0.0" + inherits: "npm:2.0.4" + setprototypeof: "npm:1.2.0" + statuses: "npm:2.0.1" + toidentifier: "npm:1.0.1" + checksum: 10c0/fc6f2715fe188d091274b5ffc8b3657bd85c63e969daa68ccb77afb05b071a4b62841acb7a21e417b5539014dff2ebf9550f0b14a9ff126f2734a7c1387f8e19 + languageName: node + linkType: hard + "http-proxy-agent@npm:^4.0.1": version: 4.0.1 resolution: "http-proxy-agent@npm:4.0.1" @@ -4431,6 +4954,15 @@ __metadata: languageName: node linkType: hard +"iconv-lite@npm:0.6.3, iconv-lite@npm:^0.6.2, iconv-lite@npm:^0.6.3": + version: 0.6.3 + resolution: "iconv-lite@npm:0.6.3" + dependencies: + safer-buffer: "npm:>= 2.1.2 < 3.0.0" + checksum: 10c0/98102bc66b33fcf5ac044099d1257ba0b7ad5e3ccd3221f34dd508ab4070edff183276221684e1e0555b145fce0850c9f7d2b60a9fcac50fbb4ea0d6e845a3b1 + languageName: node + linkType: hard + "iconv-lite@npm:^0.4.24": version: 0.4.24 resolution: "iconv-lite@npm:0.4.24" @@ -4440,15 +4972,6 @@ __metadata: languageName: node linkType: hard -"iconv-lite@npm:^0.6.2": - version: 0.6.3 - resolution: "iconv-lite@npm:0.6.3" - dependencies: - safer-buffer: "npm:>= 2.1.2 < 3.0.0" - checksum: 10c0/98102bc66b33fcf5ac044099d1257ba0b7ad5e3ccd3221f34dd508ab4070edff183276221684e1e0555b145fce0850c9f7d2b60a9fcac50fbb4ea0d6e845a3b1 - languageName: node - linkType: hard - "ieee754@npm:1.1.13": version: 1.1.13 resolution: "ieee754@npm:1.1.13" @@ -4536,7 +5059,7 @@ __metadata: languageName: node linkType: hard -"inherits@npm:2, inherits@npm:^2.0.1, inherits@npm:^2.0.3, inherits@npm:^2.0.4, inherits@npm:~2.0.3": +"inherits@npm:2, inherits@npm:2.0.4, inherits@npm:^2.0.1, inherits@npm:^2.0.3, inherits@npm:^2.0.4, inherits@npm:~2.0.3": version: 2.0.4 resolution: "inherits@npm:2.0.4" checksum: 10c0/4e531f648b29039fb7426fb94075e6545faa1eb9fe83c29f0b6d9e7263aceb4289d2d4557db0d428188eeb449cc7c5e77b0a0b2c4e248ff2a65933a0dee49ef2 @@ -4598,6 +5121,13 @@ __metadata: languageName: node linkType: hard +"ipaddr.js@npm:1.9.1": + version: 1.9.1 + resolution: "ipaddr.js@npm:1.9.1" + checksum: 10c0/0486e775047971d3fdb5fb4f063829bac45af299ae0b82dcf3afa2145338e08290563a2a70f34b732d795ecc8311902e541a8530eeb30d75860a78ff4e94ce2a + languageName: node + linkType: hard + "is-arguments@npm:^1.0.4": version: 1.1.1 resolution: "is-arguments@npm:1.1.1" @@ -4716,6 +5246,13 @@ __metadata: languageName: node linkType: hard +"is-promise@npm:^4.0.0": + version: 4.0.0 + resolution: "is-promise@npm:4.0.0" + checksum: 10c0/ebd5c672d73db781ab33ccb155fb9969d6028e37414d609b115cc534654c91ccd061821d5b987eefaa97cf4c62f0b909bb2f04db88306de26e91bfe8ddc01503 + languageName: node + linkType: hard + "is-retry-allowed@npm:^1.1.0": version: 1.2.0 resolution: "is-retry-allowed@npm:1.2.0" @@ -5470,6 +6007,20 @@ __metadata: languageName: node linkType: hard +"math-intrinsics@npm:^1.1.0": + version: 1.1.0 + resolution: "math-intrinsics@npm:1.1.0" + checksum: 10c0/7579ff94e899e2f76ab64491d76cf606274c874d8f2af4a442c016bd85688927fcfca157ba6bf74b08e9439dc010b248ce05b96cc7c126a354c3bae7fcb48b7f + languageName: node + linkType: hard + +"media-typer@npm:^1.1.0": + version: 1.1.0 + resolution: "media-typer@npm:1.1.0" + checksum: 10c0/7b4baa40b25964bb90e2121ee489ec38642127e48d0cc2b6baa442688d3fde6262bfdca86d6bbf6ba708784afcac168c06840c71facac70e390f5f759ac121b9 + languageName: node + linkType: hard + "mem-fs-editor@npm:^8.1.2 || ^9.0.0, mem-fs-editor@npm:^9.0.0": version: 9.7.0 resolution: "mem-fs-editor@npm:9.7.0" @@ -5505,6 +6056,13 @@ __metadata: languageName: node linkType: hard +"merge-descriptors@npm:^2.0.0": + version: 2.0.0 + resolution: "merge-descriptors@npm:2.0.0" + checksum: 10c0/95389b7ced3f9b36fbdcf32eb946dc3dd1774c2fdf164609e55b18d03aa499b12bd3aae3a76c1c7185b96279e9803525550d3eb292b5224866060a288f335cb3 + languageName: node + linkType: hard + "merge-stream@npm:^2.0.0": version: 2.0.0 resolution: "merge-stream@npm:2.0.0" @@ -5536,6 +6094,13 @@ __metadata: languageName: node linkType: hard +"mime-db@npm:^1.54.0": + version: 1.54.0 + resolution: "mime-db@npm:1.54.0" + checksum: 10c0/8d907917bc2a90fa2df842cdf5dfeaf509adc15fe0531e07bb2f6ab15992416479015828d6a74200041c492e42cce3ebf78e5ce714388a0a538ea9c53eece284 + languageName: node + linkType: hard + "mime-types@npm:^2.1.12": version: 2.1.35 resolution: "mime-types@npm:2.1.35" @@ -5545,6 +6110,15 @@ __metadata: languageName: node linkType: hard +"mime-types@npm:^3.0.0, mime-types@npm:^3.0.1": + version: 3.0.1 + resolution: "mime-types@npm:3.0.1" + dependencies: + mime-db: "npm:^1.54.0" + checksum: 10c0/bd8c20d3694548089cf229016124f8f40e6a60bbb600161ae13e45f793a2d5bb40f96bbc61f275836696179c77c1d6bf4967b2a75e0a8ad40fe31f4ed5be4da5 + languageName: node + linkType: hard + "mimic-fn@npm:^2.1.0": version: 2.1.0 resolution: "mimic-fn@npm:2.1.0" @@ -5886,6 +6460,13 @@ __metadata: languageName: node linkType: hard +"negotiator@npm:^1.0.0": + version: 1.0.0 + resolution: "negotiator@npm:1.0.0" + checksum: 10c0/4c559dd52669ea48e1914f9d634227c561221dd54734070791f999c52ed0ff36e437b2e07d5c1f6e32909fc625fe46491c16e4a8f0572567d4dd15c3a4fda04b + languageName: node + linkType: hard + "neo-async@npm:^2.6.2": version: 2.6.2 resolution: "neo-async@npm:2.6.2" @@ -6289,13 +6870,20 @@ __metadata: languageName: node linkType: hard -"object-assign@npm:^4.1.1": +"object-assign@npm:^4, object-assign@npm:^4.1.1": version: 4.1.1 resolution: "object-assign@npm:4.1.1" checksum: 10c0/1f4df9945120325d041ccf7b86f31e8bcc14e73d29171e37a7903050e96b81323784ec59f93f102ec635bcf6fa8034ba3ea0a8c7e69fa202b87ae3b6cec5a414 languageName: node linkType: hard +"object-inspect@npm:^1.13.3": + version: 1.13.4 + resolution: "object-inspect@npm:1.13.4" + checksum: 10c0/d7f8711e803b96ea3191c745d6f8056ce1f2496e530e6a19a0e92d89b0fa3c76d910c31f0aa270432db6bd3b2f85500a376a83aaba849a8d518c8845b3211692 + languageName: node + linkType: hard + "object-treeify@npm:^1.1.33": version: 1.1.33 resolution: "object-treeify@npm:1.1.33" @@ -6332,6 +6920,15 @@ __metadata: languageName: node linkType: hard +"on-finished@npm:^2.4.1": + version: 2.4.1 + resolution: "on-finished@npm:2.4.1" + dependencies: + ee-first: "npm:1.1.1" + checksum: 10c0/46fb11b9063782f2d9968863d9cbba33d77aa13c17f895f56129c274318b86500b22af3a160fe9995aa41317efcd22941b6eba747f718ced08d9a73afdb087b4 + languageName: node + linkType: hard + "once@npm:^1.3.0, once@npm:^1.3.1, once@npm:^1.4.0": version: 1.4.0 resolution: "once@npm:1.4.0" @@ -6647,6 +7244,13 @@ __metadata: languageName: node linkType: hard +"parseurl@npm:^1.3.3": + version: 1.3.3 + resolution: "parseurl@npm:1.3.3" + checksum: 10c0/90dd4760d6f6174adb9f20cf0965ae12e23879b5f5464f38e92fce8073354341e4b3b76fa3d878351efe7d01e617121955284cfd002ab087fba1a0726ec0b4f5 + languageName: node + linkType: hard + "password-prompt@npm:^1.1.2": version: 1.1.3 resolution: "password-prompt@npm:1.1.3" @@ -6724,7 +7328,7 @@ __metadata: languageName: node linkType: hard -"path-to-regexp@npm:^8.1.0": +"path-to-regexp@npm:^8.0.0, path-to-regexp@npm:^8.1.0": version: 8.2.0 resolution: "path-to-regexp@npm:8.2.0" checksum: 10c0/ef7d0a887b603c0a142fad16ccebdcdc42910f0b14830517c724466ad676107476bba2fe9fffd28fd4c141391ccd42ea426f32bb44c2c82ecaefe10c37b90f5a @@ -6787,6 +7391,13 @@ __metadata: languageName: node linkType: hard +"pkce-challenge@npm:^5.0.0": + version: 5.0.0 + resolution: "pkce-challenge@npm:5.0.0" + checksum: 10c0/c6706d627fdbb6f22bf8cc5d60d96d6b6a7bb481399b336a3d3f4e9bfba3e167a2c32f8ec0b5e74be686a0ba3bcc9894865d4c2dd1b91cea4c05dba1f28602c3 + languageName: node + linkType: hard + "pkg-dir@npm:^4.2.0": version: 4.2.0 resolution: "pkg-dir@npm:4.2.0" @@ -6931,6 +7542,16 @@ __metadata: languageName: node linkType: hard +"proxy-addr@npm:^2.0.7": + version: 2.0.7 + resolution: "proxy-addr@npm:2.0.7" + dependencies: + forwarded: "npm:0.2.0" + ipaddr.js: "npm:1.9.1" + checksum: 10c0/c3eed999781a35f7fd935f398b6d8920b6fb00bbc14287bc6de78128ccc1a02c89b95b56742bf7cf0362cc333c61d138532049c7dedc7a328ef13343eff81210 + languageName: node + linkType: hard + "proxy-from-env@npm:^1.1.0": version: 1.1.0 resolution: "proxy-from-env@npm:1.1.0" @@ -6962,6 +7583,15 @@ __metadata: languageName: node linkType: hard +"qs@npm:^6.14.0": + version: 6.14.0 + resolution: "qs@npm:6.14.0" + dependencies: + side-channel: "npm:^1.1.0" + checksum: 10c0/8ea5d91bf34f440598ee389d4a7d95820e3b837d3fd9f433871f7924801becaa0cd3b3b4628d49a7784d06a8aea9bc4554d2b6d8d584e2d221dc06238a42909c + languageName: node + linkType: hard + "querystring@npm:0.2.0": version: 0.2.0 resolution: "querystring@npm:0.2.0" @@ -6992,6 +7622,25 @@ __metadata: languageName: node linkType: hard +"range-parser@npm:^1.2.1": + version: 1.2.1 + resolution: "range-parser@npm:1.2.1" + checksum: 10c0/96c032ac2475c8027b7a4e9fe22dc0dfe0f6d90b85e496e0f016fbdb99d6d066de0112e680805075bd989905e2123b3b3d002765149294dce0c1f7f01fcc2ea0 + languageName: node + linkType: hard + +"raw-body@npm:^3.0.0": + version: 3.0.0 + resolution: "raw-body@npm:3.0.0" + dependencies: + bytes: "npm:3.1.2" + http-errors: "npm:2.0.0" + iconv-lite: "npm:0.6.3" + unpipe: "npm:1.0.0" + checksum: 10c0/f8daf4b724064a4811d118745a781ca0fb4676298b8adadfd6591155549cfea0a067523cf7dd3baeb1265fecc9ce5dfb2fc788c12c66b85202a336593ece0f87 + languageName: node + linkType: hard + "react-is@npm:^18.0.0": version: 18.2.0 resolution: "react-is@npm:18.2.0" @@ -7291,6 +7940,19 @@ __metadata: languageName: node linkType: hard +"router@npm:^2.2.0": + version: 2.2.0 + resolution: "router@npm:2.2.0" + dependencies: + debug: "npm:^4.4.0" + depd: "npm:^2.0.0" + is-promise: "npm:^4.0.0" + parseurl: "npm:^1.3.3" + path-to-regexp: "npm:^8.0.0" + checksum: 10c0/3279de7450c8eae2f6e095e9edacbdeec0abb5cb7249c6e719faa0db2dba43574b4fff5892d9220631c9abaff52dd3cad648cfea2aaace845e1a071915ac8867 + languageName: node + linkType: hard + "run-async@npm:^2.0.0, run-async@npm:^2.4.0, run-async@npm:^2.4.1": version: 2.4.1 resolution: "run-async@npm:2.4.1" @@ -7316,7 +7978,7 @@ __metadata: languageName: node linkType: hard -"safe-buffer@npm:^5.0.1, safe-buffer@npm:^5.1.0, safe-buffer@npm:~5.2.0": +"safe-buffer@npm:5.2.1, safe-buffer@npm:^5.0.1, safe-buffer@npm:^5.1.0, safe-buffer@npm:~5.2.0": version: 5.2.1 resolution: "safe-buffer@npm:5.2.1" checksum: 10c0/6501914237c0a86e9675d4e51d89ca3c21ffd6a31642efeba25ad65720bce6921c9e7e974e5be91a786b25aa058b5303285d3c15dbabf983a919f5f630d349f3 @@ -7405,6 +8067,25 @@ __metadata: languageName: node linkType: hard +"send@npm:^1.1.0, send@npm:^1.2.0": + version: 1.2.0 + resolution: "send@npm:1.2.0" + dependencies: + debug: "npm:^4.3.5" + encodeurl: "npm:^2.0.0" + escape-html: "npm:^1.0.3" + etag: "npm:^1.8.1" + fresh: "npm:^2.0.0" + http-errors: "npm:^2.0.0" + mime-types: "npm:^3.0.1" + ms: "npm:^2.1.3" + on-finished: "npm:^2.4.1" + range-parser: "npm:^1.2.1" + statuses: "npm:^2.0.1" + checksum: 10c0/531bcfb5616948d3468d95a1fd0adaeb0c20818ba4a500f439b800ca2117971489e02074ce32796fd64a6772ea3e7235fe0583d8241dbd37a053dc3378eff9a5 + languageName: node + linkType: hard + "serialize-javascript@npm:^6.0.2": version: 6.0.2 resolution: "serialize-javascript@npm:6.0.2" @@ -7414,6 +8095,18 @@ __metadata: languageName: node linkType: hard +"serve-static@npm:^2.2.0": + version: 2.2.0 + resolution: "serve-static@npm:2.2.0" + dependencies: + encodeurl: "npm:^2.0.0" + escape-html: "npm:^1.0.3" + parseurl: "npm:^1.3.3" + send: "npm:^1.2.0" + checksum: 10c0/30e2ed1dbff1984836cfd0c65abf5d3f3f83bcd696c99d2d3c97edbd4e2a3ff4d3f87108a7d713640d290a7b6fe6c15ddcbc61165ab2eaad48ea8d3b52c7f913 + languageName: node + linkType: hard + "set-blocking@npm:^2.0.0": version: 2.0.0 resolution: "set-blocking@npm:2.0.0" @@ -7435,6 +8128,13 @@ __metadata: languageName: node linkType: hard +"setprototypeof@npm:1.2.0": + version: 1.2.0 + resolution: "setprototypeof@npm:1.2.0" + checksum: 10c0/68733173026766fa0d9ecaeb07f0483f4c2dc70ca376b3b7c40b7cda909f94b0918f6c5ad5ce27a9160bdfb475efaa9d5e705a11d8eaae18f9835d20976028bc + languageName: node + linkType: hard + "shebang-command@npm:^2.0.0": version: 2.0.0 resolution: "shebang-command@npm:2.0.0" @@ -7483,6 +8183,54 @@ __metadata: languageName: node linkType: hard +"side-channel-list@npm:^1.0.0": + version: 1.0.0 + resolution: "side-channel-list@npm:1.0.0" + dependencies: + es-errors: "npm:^1.3.0" + object-inspect: "npm:^1.13.3" + checksum: 10c0/644f4ac893456c9490ff388bf78aea9d333d5e5bfc64cfb84be8f04bf31ddc111a8d4b83b85d7e7e8a7b845bc185a9ad02c052d20e086983cf59f0be517d9b3d + languageName: node + linkType: hard + +"side-channel-map@npm:^1.0.1": + version: 1.0.1 + resolution: "side-channel-map@npm:1.0.1" + dependencies: + call-bound: "npm:^1.0.2" + es-errors: "npm:^1.3.0" + get-intrinsic: "npm:^1.2.5" + object-inspect: "npm:^1.13.3" + checksum: 10c0/010584e6444dd8a20b85bc926d934424bd809e1a3af941cace229f7fdcb751aada0fb7164f60c2e22292b7fa3c0ff0bce237081fd4cdbc80de1dc68e95430672 + languageName: node + linkType: hard + +"side-channel-weakmap@npm:^1.0.2": + version: 1.0.2 + resolution: "side-channel-weakmap@npm:1.0.2" + dependencies: + call-bound: "npm:^1.0.2" + es-errors: "npm:^1.3.0" + get-intrinsic: "npm:^1.2.5" + object-inspect: "npm:^1.13.3" + side-channel-map: "npm:^1.0.1" + checksum: 10c0/71362709ac233e08807ccd980101c3e2d7efe849edc51455030327b059f6c4d292c237f94dc0685031dd11c07dd17a68afde235d6cf2102d949567f98ab58185 + languageName: node + linkType: hard + +"side-channel@npm:^1.1.0": + version: 1.1.0 + resolution: "side-channel@npm:1.1.0" + dependencies: + es-errors: "npm:^1.3.0" + object-inspect: "npm:^1.13.3" + side-channel-list: "npm:^1.0.0" + side-channel-map: "npm:^1.0.1" + side-channel-weakmap: "npm:^1.0.2" + checksum: 10c0/cb20dad41eb032e6c24c0982e1e5a24963a28aa6122b4f05b3f3d6bf8ae7fd5474ef382c8f54a6a3ab86e0cac4d41a23bd64ede3970e5bfb50326ba02a7996e6 + languageName: node + linkType: hard + "signal-exit@npm:^3.0.0, signal-exit@npm:^3.0.2, signal-exit@npm:^3.0.3, signal-exit@npm:^3.0.7": version: 3.0.7 resolution: "signal-exit@npm:3.0.7" @@ -7711,6 +8459,20 @@ __metadata: languageName: node linkType: hard +"statuses@npm:2.0.1": + version: 2.0.1 + resolution: "statuses@npm:2.0.1" + checksum: 10c0/34378b207a1620a24804ce8b5d230fea0c279f00b18a7209646d5d47e419d1cc23e7cbf33a25a1e51ac38973dc2ac2e1e9c647a8e481ef365f77668d72becfd0 + languageName: node + linkType: hard + +"statuses@npm:^2.0.1": + version: 2.0.2 + resolution: "statuses@npm:2.0.2" + checksum: 10c0/a9947d98ad60d01f6b26727570f3bcceb6c8fa789da64fe6889908fe2e294d57503b14bf2b5af7605c2d36647259e856635cd4c49eab41667658ec9d0080ec3f + languageName: node + linkType: hard + "stdout-stderr@npm:^0.1.9": version: 0.1.13 resolution: "stdout-stderr@npm:0.1.13" @@ -7944,6 +8706,13 @@ __metadata: languageName: node linkType: hard +"toidentifier@npm:1.0.1": + version: 1.0.1 + resolution: "toidentifier@npm:1.0.1" + checksum: 10c0/93937279934bd66cc3270016dd8d0afec14fb7c94a05c72dc57321f8bd1fa97e5bea6d1f7c89e728d077ca31ea125b78320a616a6c6cd0e6b9cb94cb864381c1 + languageName: node + linkType: hard + "tr46@npm:~0.0.3": version: 0.0.3 resolution: "tr46@npm:0.0.3" @@ -8106,6 +8875,17 @@ __metadata: languageName: node linkType: hard +"type-is@npm:^2.0.0, type-is@npm:^2.0.1": + version: 2.0.1 + resolution: "type-is@npm:2.0.1" + dependencies: + content-type: "npm:^1.0.5" + media-typer: "npm:^1.1.0" + mime-types: "npm:^3.0.0" + checksum: 10c0/7f7ec0a060b16880bdad36824ab37c26019454b67d73e8a465ed5a3587440fbe158bc765f0da68344498235c877e7dbbb1600beccc94628ed05599d667951b99 + languageName: node + linkType: hard + "typescript-eslint@npm:^8.21.0": version: 8.37.0 resolution: "typescript-eslint@npm:8.37.0" @@ -8252,6 +9032,13 @@ __metadata: languageName: node linkType: hard +"unpipe@npm:1.0.0": + version: 1.0.0 + resolution: "unpipe@npm:1.0.0" + checksum: 10c0/193400255bd48968e5c5383730344fbb4fa114cdedfab26e329e50dd2d81b134244bb8a72c6ac1b10ab0281a58b363d06405632c9d49ca9dfd5e90cbd7d0f32c + languageName: node + linkType: hard + "untildify@npm:^4.0.0": version: 4.0.0 resolution: "untildify@npm:4.0.0" @@ -8363,6 +9150,13 @@ __metadata: languageName: node linkType: hard +"vary@npm:^1, vary@npm:^1.1.2": + version: 1.1.2 + resolution: "vary@npm:1.1.2" + checksum: 10c0/f15d588d79f3675135ba783c91a4083dcd290a2a5be9fcb6514220a1634e23df116847b1cc51f66bfb0644cf9353b2abb7815ae499bab06e46dd33c1a6bf1f4f + languageName: node + linkType: hard + "vinyl-file@npm:^3.0.0": version: 3.0.0 resolution: "vinyl-file@npm:3.0.0" @@ -8779,6 +9573,15 @@ __metadata: languageName: node linkType: hard +"zod-to-json-schema@npm:^3.24.1": + version: 3.24.6 + resolution: "zod-to-json-schema@npm:3.24.6" + peerDependencies: + zod: ^3.24.1 + checksum: 10c0/b907ab6d057100bd25a37e5545bf5f0efa5902cd84d3c3ec05c2e51541431a47bd9bf1e5e151a244273409b45f5986d55b26e5d207f98abc5200702f733eb368 + languageName: node + linkType: hard + "zod@npm:^3.19.1": version: 3.22.4 resolution: "zod@npm:3.22.4" @@ -8786,6 +9589,13 @@ __metadata: languageName: node linkType: hard +"zod@npm:^3.23.8": + version: 3.25.75 + resolution: "zod@npm:3.25.75" + checksum: 10c0/e11c83dcd1437401c1edf4f0448bd13b9133e83196385d0ee5407662bef6c08099cb511b5e65f11f2feba244b6709bcd34077c9450c9f3064f75df51b362b5f2 + languageName: node + linkType: hard + "zod@npm:^3.24.2": version: 3.24.2 resolution: "zod@npm:3.24.2"