From 63b7eb11674e9d202c5d7f1b8eb22b0a8c454110 Mon Sep 17 00:00:00 2001 From: Jonathan Norris Date: Fri, 27 Jun 2025 11:36:47 -0400 Subject: [PATCH 01/44] chore: MCP planning document --- docs/DevCycle_MCP_Planning.md | 182 +++++++++++++ docs/MCP_PLANNING_DOCUMENT.md | 470 ++++++++++++++++++++++++++++++++++ 2 files changed, 652 insertions(+) create mode 100644 docs/DevCycle_MCP_Planning.md create mode 100644 docs/MCP_PLANNING_DOCUMENT.md diff --git a/docs/DevCycle_MCP_Planning.md b/docs/DevCycle_MCP_Planning.md new file mode 100644 index 000000000..f694f9c96 --- /dev/null +++ b/docs/DevCycle_MCP_Planning.md @@ -0,0 +1,182 @@ +# DevCycle CLI to MCP Server Planning Document + +## Executive Summary + +Transform the DevCycle CLI into a Model Context Protocol (MCP) server to enable AI coding assistants like Cursor and Claude Code to seamlessly interact with DevCycle's feature flag management capabilities. + +## What is MCP? + +MCP is an open standard by Anthropic that standardizes how AI applications connect to data sources and tools. It acts as a "USB-C port for AI applications." + +**Key Components:** +- **Hosts**: AI applications (Cursor, Claude Code) +- **Servers**: Services providing capabilities (our DevCycle MCP server) +- **Protocol**: JSON-RPC 2.0 communication via stdin/stdout or HTTP + +**Server Capabilities:** +1. **Tools**: Functions callable by LLM (with user approval) +2. **Resources**: File-like data readable by clients +3. **Prompts**: Pre-written templates for common tasks + +## DevCycle CLI Analysis + +**Current Architecture:** +- Built with oclif framework and TypeScript +- Uses DevCycle Management API with auth via client credentials/SSO +- Repository configuration with `.devcycle/config.yml` + +**Major Features:** +- Feature/Variable/Variation management +- Code analysis (usage detection, diff) +- Self-targeting overrides +- Type generation +- Git-aware operations + +## MCP Server Design + +### Core Tools + +#### 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 + +#### Variable Management +- `list_variables` - List all variables +- `get_variable` - Get variable details +- `create_variable` - Create new variable + +#### Code Analysis +- `scan_variable_usages` - Scan codebase for variable usage +- `diff_variable_usage` - Compare usage between code versions + +#### Project/Environment +- `list_projects` - List available projects +- `get_current_project` - Get current project +- `select_project` - Switch projects +- `list_environments` - List environments + +#### Overrides +- `list_overrides` - List current overrides +- `set_override` - Set self-targeting override +- `clear_overrides` - Clear overrides + +### Resources + +- `devcycle://config/repo` - Repository configuration +- `devcycle://project/features` - All project features +- `devcycle://project/variables` - All project variables +- `devcycle://analysis/usages` - Latest usage scan results + +### Prompts + +- `create_feature_flag` - Guide through feature creation +- `implement_feature_flag` - Generate implementation code +- `setup_targeting` - Configure targeting rules +- `cleanup_removed_flags` - Guide flag removal + +## Implementation Plan + +### Phase 1: Core Setup (Weeks 1-2) +- MCP server infrastructure +- stdio transport implementation +- Authentication integration +- Basic capability negotiation + +### Phase 2: Essential Tools (Weeks 3-4) +- Feature and variable management tools +- Project management tools +- Error handling and validation + +### Phase 3: Code Analysis (Weeks 5-6) +- Usage scanning tool +- Diff analysis tool +- File filtering and pattern matching + +### Phase 4: Advanced Features (Weeks 7-8) +- Targeting and override tools +- Resource implementations +- Environment management + +### Phase 5: Polish (Weeks 9-10) +- Prompt templates +- Comprehensive testing +- Documentation + +### Phase 6: HTTP Transport (Weeks 11-12) +- HTTP server with SSE +- OAuth 2.0 authentication +- Production deployment + +## Technical Implementation + +### Project Structure +``` +src/mcp/ +├── server.ts # Main MCP server +├── tools/ # Tool implementations +│ ├── features.ts +│ ├── variables.ts +│ └── analysis.ts +├── resources/ # Resource implementations +├── prompts/ # Prompt templates +└── transports/ # stdio/HTTP transports +``` + +### Dependencies +```json +{ + "@modelcontextprotocol/sdk": "latest", + "zod": "^3.24.2" +} +``` + +### Integration Examples + +**Cursor Configuration:** +```json +{ + "mcpServers": { + "devcycle": { + "command": "node", + "args": ["/path/to/devcycle-mcp/dist/index.js"] + } + } +} +``` + +**Usage Scenarios:** +1. "Create a feature flag for new checkout" → AI uses tools to create and implement +2. "What flags are in this PR?" → AI analyzes diff for flag usage +3. "Remove old_feature flag" → AI scans usage and guides cleanup + +## Benefits + +### For Developers +- Feature flag management in coding environment +- Context-aware AI assistance +- Automatic code analysis +- Built-in best practices + +### For DevCycle +- First feature flag platform with native AI integration +- Superior developer experience +- Market differentiation +- Ecosystem growth + +## Success Metrics +- Response time < 500ms +- High developer adoption +- Increased API usage +- Positive community feedback + +## Risk Mitigation +- Start with stdio transport for simplicity +- Reuse existing CLI codebase extensively +- Implement comprehensive testing +- Monitor MCP specification changes + +## Conclusion + +This initiative positions DevCycle at the forefront of AI-assisted development, providing a significant competitive advantage by seamlessly integrating feature flag management into modern AI-powered coding workflows. \ No newline at end of file diff --git a/docs/MCP_PLANNING_DOCUMENT.md b/docs/MCP_PLANNING_DOCUMENT.md new file mode 100644 index 000000000..46f73d753 --- /dev/null +++ b/docs/MCP_PLANNING_DOCUMENT.md @@ -0,0 +1,470 @@ +# 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 + +### 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 + +### 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` + +## 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 + +## 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 From 3b5cfa7f7a89bdbe08dba348876d6ceb76807a93 Mon Sep 17 00:00:00 2001 From: Jonathan Norris Date: Mon, 7 Jul 2025 11:48:55 -0400 Subject: [PATCH 02/44] chore: move mcp docs --- docs/{ => mcp}/DevCycle_MCP_Planning.md | 0 docs/{ => mcp}/MCP_PLANNING_DOCUMENT.md | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename docs/{ => mcp}/DevCycle_MCP_Planning.md (100%) rename docs/{ => mcp}/MCP_PLANNING_DOCUMENT.md (100%) diff --git a/docs/DevCycle_MCP_Planning.md b/docs/mcp/DevCycle_MCP_Planning.md similarity index 100% rename from docs/DevCycle_MCP_Planning.md rename to docs/mcp/DevCycle_MCP_Planning.md diff --git a/docs/MCP_PLANNING_DOCUMENT.md b/docs/mcp/MCP_PLANNING_DOCUMENT.md similarity index 100% rename from docs/MCP_PLANNING_DOCUMENT.md rename to docs/mcp/MCP_PLANNING_DOCUMENT.md From 6b75baff39a76720e56a5cc6c2359d88c1d63783 Mon Sep 17 00:00:00 2001 From: Jonathan Norris Date: Mon, 7 Jul 2025 12:01:43 -0400 Subject: [PATCH 03/44] chore: update MCP docs --- docs/mcp/DevCycle_MCP_Planning.md | 182 ------------- docs/mcp/MCP_PLANNING_DOCUMENT.md | 431 ++++++++++++++++++++++++++++++ 2 files changed, 431 insertions(+), 182 deletions(-) delete mode 100644 docs/mcp/DevCycle_MCP_Planning.md diff --git a/docs/mcp/DevCycle_MCP_Planning.md b/docs/mcp/DevCycle_MCP_Planning.md deleted file mode 100644 index f694f9c96..000000000 --- a/docs/mcp/DevCycle_MCP_Planning.md +++ /dev/null @@ -1,182 +0,0 @@ -# DevCycle CLI to MCP Server Planning Document - -## Executive Summary - -Transform the DevCycle CLI into a Model Context Protocol (MCP) server to enable AI coding assistants like Cursor and Claude Code to seamlessly interact with DevCycle's feature flag management capabilities. - -## What is MCP? - -MCP is an open standard by Anthropic that standardizes how AI applications connect to data sources and tools. It acts as a "USB-C port for AI applications." - -**Key Components:** -- **Hosts**: AI applications (Cursor, Claude Code) -- **Servers**: Services providing capabilities (our DevCycle MCP server) -- **Protocol**: JSON-RPC 2.0 communication via stdin/stdout or HTTP - -**Server Capabilities:** -1. **Tools**: Functions callable by LLM (with user approval) -2. **Resources**: File-like data readable by clients -3. **Prompts**: Pre-written templates for common tasks - -## DevCycle CLI Analysis - -**Current Architecture:** -- Built with oclif framework and TypeScript -- Uses DevCycle Management API with auth via client credentials/SSO -- Repository configuration with `.devcycle/config.yml` - -**Major Features:** -- Feature/Variable/Variation management -- Code analysis (usage detection, diff) -- Self-targeting overrides -- Type generation -- Git-aware operations - -## MCP Server Design - -### Core Tools - -#### 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 - -#### Variable Management -- `list_variables` - List all variables -- `get_variable` - Get variable details -- `create_variable` - Create new variable - -#### Code Analysis -- `scan_variable_usages` - Scan codebase for variable usage -- `diff_variable_usage` - Compare usage between code versions - -#### Project/Environment -- `list_projects` - List available projects -- `get_current_project` - Get current project -- `select_project` - Switch projects -- `list_environments` - List environments - -#### Overrides -- `list_overrides` - List current overrides -- `set_override` - Set self-targeting override -- `clear_overrides` - Clear overrides - -### Resources - -- `devcycle://config/repo` - Repository configuration -- `devcycle://project/features` - All project features -- `devcycle://project/variables` - All project variables -- `devcycle://analysis/usages` - Latest usage scan results - -### Prompts - -- `create_feature_flag` - Guide through feature creation -- `implement_feature_flag` - Generate implementation code -- `setup_targeting` - Configure targeting rules -- `cleanup_removed_flags` - Guide flag removal - -## Implementation Plan - -### Phase 1: Core Setup (Weeks 1-2) -- MCP server infrastructure -- stdio transport implementation -- Authentication integration -- Basic capability negotiation - -### Phase 2: Essential Tools (Weeks 3-4) -- Feature and variable management tools -- Project management tools -- Error handling and validation - -### Phase 3: Code Analysis (Weeks 5-6) -- Usage scanning tool -- Diff analysis tool -- File filtering and pattern matching - -### Phase 4: Advanced Features (Weeks 7-8) -- Targeting and override tools -- Resource implementations -- Environment management - -### Phase 5: Polish (Weeks 9-10) -- Prompt templates -- Comprehensive testing -- Documentation - -### Phase 6: HTTP Transport (Weeks 11-12) -- HTTP server with SSE -- OAuth 2.0 authentication -- Production deployment - -## Technical Implementation - -### Project Structure -``` -src/mcp/ -├── server.ts # Main MCP server -├── tools/ # Tool implementations -│ ├── features.ts -│ ├── variables.ts -│ └── analysis.ts -├── resources/ # Resource implementations -├── prompts/ # Prompt templates -└── transports/ # stdio/HTTP transports -``` - -### Dependencies -```json -{ - "@modelcontextprotocol/sdk": "latest", - "zod": "^3.24.2" -} -``` - -### Integration Examples - -**Cursor Configuration:** -```json -{ - "mcpServers": { - "devcycle": { - "command": "node", - "args": ["/path/to/devcycle-mcp/dist/index.js"] - } - } -} -``` - -**Usage Scenarios:** -1. "Create a feature flag for new checkout" → AI uses tools to create and implement -2. "What flags are in this PR?" → AI analyzes diff for flag usage -3. "Remove old_feature flag" → AI scans usage and guides cleanup - -## Benefits - -### For Developers -- Feature flag management in coding environment -- Context-aware AI assistance -- Automatic code analysis -- Built-in best practices - -### For DevCycle -- First feature flag platform with native AI integration -- Superior developer experience -- Market differentiation -- Ecosystem growth - -## Success Metrics -- Response time < 500ms -- High developer adoption -- Increased API usage -- Positive community feedback - -## Risk Mitigation -- Start with stdio transport for simplicity -- Reuse existing CLI codebase extensively -- Implement comprehensive testing -- Monitor MCP specification changes - -## Conclusion - -This initiative positions DevCycle at the forefront of AI-assisted development, providing a significant competitive advantage by seamlessly integrating feature flag management into modern AI-powered coding workflows. \ No newline at end of file diff --git a/docs/mcp/MCP_PLANNING_DOCUMENT.md b/docs/mcp/MCP_PLANNING_DOCUMENT.md index 46f73d753..a40165073 100644 --- a/docs/mcp/MCP_PLANNING_DOCUMENT.md +++ b/docs/mcp/MCP_PLANNING_DOCUMENT.md @@ -171,6 +171,169 @@ AI Coding Assistant (Cursor/Claude Code) - 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 @@ -209,6 +372,75 @@ AI Coding Assistant (Cursor/Claude Code) - 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 @@ -235,6 +467,161 @@ AI Coding Assistant (Cursor/Claude 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) @@ -461,6 +848,50 @@ AI: Uses cleanup_removed_flags prompt → Guides through removal - 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. From 07736b660596f9f3d2c94801177b1e085f2e3477 Mon Sep 17 00:00:00 2001 From: Jonathan Norris Date: Mon, 7 Jul 2025 12:06:04 -0400 Subject: [PATCH 04/44] chore: add abstractions --- docs/mcp/MCP_ABSTRACTIONS_SUMMARY.md | 146 +++++++++++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 docs/mcp/MCP_ABSTRACTIONS_SUMMARY.md diff --git a/docs/mcp/MCP_ABSTRACTIONS_SUMMARY.md b/docs/mcp/MCP_ABSTRACTIONS_SUMMARY.md new file mode 100644 index 000000000..fc800938d --- /dev/null +++ b/docs/mcp/MCP_ABSTRACTIONS_SUMMARY.md @@ -0,0 +1,146 @@ +# 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 \ No newline at end of file From af5555bdd1285416a78f1cfe0643b7ca4de1b993 Mon Sep 17 00:00:00 2001 From: Jonathan Norris Date: Mon, 7 Jul 2025 13:28:43 -0400 Subject: [PATCH 05/44] chore: update hackathon planning doc --- docs/mcp/HACKATHON_MVP_PLAN.md | 211 +++++++++++++++++++++++++++++++++ 1 file changed, 211 insertions(+) create mode 100644 docs/mcp/HACKATHON_MVP_PLAN.md diff --git a/docs/mcp/HACKATHON_MVP_PLAN.md b/docs/mcp/HACKATHON_MVP_PLAN.md new file mode 100644 index 000000000..3e8fad0f7 --- /dev/null +++ b/docs/mcp/HACKATHON_MVP_PLAN.md @@ -0,0 +1,211 @@ +# DevCycle MCP Hackathon Implementation Plan + +## Overview + +This document outlines the implementation plan for 4 core DevCycle MCP tools in priority order. + +## Target Tools + +1. `list_features` / `get_feature` / `list_variables` / `get_variable` +2. `get_sdk_keys` +3. `enable_targeting` / `disable_targeting` +4. `create_feature` (interactive) + +## Implementation Order + +### 1. List/Get Features and Variables + +#### `list_features` +**Implementation Time**: 1.5 hours +**Complexity**: Low +**Files to leverage**: `src/api/features.ts`, `src/commands/features/list.ts` + +```typescript +async listFeatures(args: { + search?: string, + page?: number, + per_page?: number +}) { + // Reuse fetchFeatures from src/api/features.ts + return await this.api.fetchFeatures(args) +} +``` + +#### `get_feature` +**Implementation Time**: 1 hour +**Complexity**: Low +**Files to leverage**: `src/api/features.ts`, `src/commands/features/get.ts` + +```typescript +async getFeature(args: { feature_key: string }) { + // Reuse fetchFeature from src/api/features.ts + return await this.api.fetchFeature(args.feature_key) +} +``` + +#### `list_variables` +**Implementation Time**: 1.5 hours +**Complexity**: Low +**Files to leverage**: `src/api/variables.ts`, `src/commands/variables/list.ts` + +```typescript +async listVariables(args: { + search?: string, + page?: number, + per_page?: number +}) { + // Reuse fetchVariables from src/api/variables.ts + return await this.api.fetchVariables(args) +} +``` + +#### `get_variable` +**Implementation Time**: 1 hour +**Complexity**: Low +**Files to leverage**: `src/api/variables.ts`, `src/commands/variables/get.ts` + +```typescript +async getVariable(args: { variable_key: string }) { + // Reuse fetchVariable from src/api/variables.ts + return await this.api.fetchVariable(args.variable_key) +} +``` + +### 2. Get SDK Keys + +#### `get_sdk_keys` +**Implementation Time**: 2 hours +**Complexity**: Medium +**Files to leverage**: `src/api/environments.ts`, `src/commands/keys/get.ts` + +```typescript +async getSdkKeys(args: { + environment_key: string, + key_type?: 'mobile' | 'server' | 'client' +}) { + // Reuse fetchEnvironment from src/api/environments.ts + // Extract SDK keys based on key_type filter + const env = await this.api.fetchEnvironment(args.environment_key) + return { + mobile: env.sdkKeys?.mobile, + server: env.sdkKeys?.server, + client: env.sdkKeys?.client + } +} +``` + +### 3. Enable/Disable Targeting + +#### `enable_targeting` +**Implementation Time**: 2 hours +**Complexity**: Medium +**Files to leverage**: `src/api/targeting.ts`, `src/commands/targeting/enable.ts` + +```typescript +async enableTargeting(args: { + feature_key: string, + environment_key: string +}) { + // Reuse updateTargeting from src/api/targeting.ts + return await this.api.updateTargeting( + args.feature_key, + args.environment_key, + { status: 'active' } + ) +} +``` + +#### `disable_targeting` +**Implementation Time**: 1.5 hours +**Complexity**: Medium +**Files to leverage**: `src/api/targeting.ts`, `src/commands/targeting/disable.ts` + +```typescript +async disableTargeting(args: { + feature_key: string, + environment_key: string +}) { + // Reuse updateTargeting from src/api/targeting.ts + return await this.api.updateTargeting( + args.feature_key, + args.environment_key, + { status: 'inactive' } + ) +} +``` + +### 4. Interactive Create Feature + +#### `create_feature` +**Implementation Time**: 3 hours +**Complexity**: High +**Files to leverage**: `src/commands/features/create.ts`, `src/ui/prompts/featurePrompts.ts` + +```typescript +async createFeature(args: { + key?: string, + name?: string, + description?: string, + type?: 'release' | 'experiment' | 'permission' | 'ops', + interactive?: boolean +}) { + // If interactive mode, use prompts from src/ui/prompts/featurePrompts.ts + // Otherwise use provided args directly + // Reuse createFeature logic from src/commands/features/create.ts + + if (args.interactive) { + // Use existing prompt functions but adapt for MCP + // May need to return prompt questions for AI to handle + return { + requires_input: true, + questions: [ + { field: 'key', prompt: 'Enter feature key:', required: true }, + { field: 'name', prompt: 'Enter feature name:', required: true }, + { field: 'description', prompt: 'Enter description:', required: false }, + { field: 'type', prompt: 'Select type:', options: ['release', 'experiment', 'permission', 'ops'] } + ] + } + } else { + // Direct creation with provided args + return await this.api.createFeature(args) + } +} +``` + +## Implementation Notes + +### Authentication Integration +- Reuse existing auth patterns from `src/auth/ApiAuth.ts` +- Use `TokenCache` for token management +- Maintain compatibility with existing CLI authentication + +### Error Handling +- Wrap API errors in MCP-compatible error responses +- Provide clear error messages for common issues +- Use existing validation from CLI commands + +### File Structure + +``` +src/mcp/ +├── index.ts # MCP server entry point +├── server.ts # Main server implementation +├── tools/ +│ ├── features.ts # Feature and variable tools +│ ├── environments.ts # SDK keys and targeting tools +│ └── index.ts +└── utils/ + ├── auth.ts # Authentication helpers + └── api.ts # API client wrapper +``` + +## Dependencies + +```json +{ + "dependencies": { + "@modelcontextprotocol/sdk": "latest", + "zod": "^3.23.0" + } +} +``` From 9bf2392683082c531d56046dfb9bac6f27c799a9 Mon Sep 17 00:00:00 2001 From: Jonathan Norris Date: Mon, 7 Jul 2025 15:37:23 -0400 Subject: [PATCH 06/44] chore: add cursor rules for project patterns and conventions --- .cursor/rules/api-client.mdc | 73 ++++++++++++++ .cursor/rules/cli-commands.mdc | 77 +++++++++++++++ .cursor/rules/deferToPrettier.mdc | 7 ++ .cursor/rules/gitCommitConventions.mdc | 33 +++++++ .cursor/rules/project-structure.mdc | 54 +++++++++++ .cursor/rules/testing-patterns.mdc | 102 ++++++++++++++++++++ .cursor/rules/typescript-patterns.mdc | 94 ++++++++++++++++++ .cursor/rules/ui-patterns.mdc | 127 +++++++++++++++++++++++++ 8 files changed, 567 insertions(+) create mode 100644 .cursor/rules/api-client.mdc create mode 100644 .cursor/rules/cli-commands.mdc create mode 100644 .cursor/rules/deferToPrettier.mdc create mode 100644 .cursor/rules/gitCommitConventions.mdc create mode 100644 .cursor/rules/project-structure.mdc create mode 100644 .cursor/rules/testing-patterns.mdc create mode 100644 .cursor/rules/typescript-patterns.mdc create mode 100644 .cursor/rules/ui-patterns.mdc diff --git a/.cursor/rules/api-client.mdc b/.cursor/rules/api-client.mdc new file mode 100644 index 000000000..a89247fac --- /dev/null +++ b/.cursor/rules/api-client.mdc @@ -0,0 +1,73 @@ +--- +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..18aa7224b --- /dev/null +++ b/.cursor/rules/cli-commands.mdc @@ -0,0 +1,77 @@ +--- +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..71d5bf860 --- /dev/null +++ b/.cursor/rules/deferToPrettier.mdc @@ -0,0 +1,7 @@ +--- +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. diff --git a/.cursor/rules/gitCommitConventions.mdc b/.cursor/rules/gitCommitConventions.mdc new file mode 100644 index 000000000..528142e1e --- /dev/null +++ b/.cursor/rules/gitCommitConventions.mdc @@ -0,0 +1,33 @@ +--- +description: +globs: +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/project-structure.mdc b/.cursor/rules/project-structure.mdc new file mode 100644 index 000000000..91495fe42 --- /dev/null +++ b/.cursor/rules/project-structure.mdc @@ -0,0 +1,54 @@ +--- +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 +- 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..44d044ae9 --- /dev/null +++ b/.cursor/rules/testing-patterns.mdc @@ -0,0 +1,102 @@ +--- +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..c8c865dbb --- /dev/null +++ b/.cursor/rules/typescript-patterns.mdc @@ -0,0 +1,94 @@ +--- +globs: *.ts,*.js,src/**/*.ts,src/**/*.js +--- + +# 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 + +## 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..b48a1f612 --- /dev/null +++ b/.cursor/rules/ui-patterns.mdc @@ -0,0 +1,127 @@ +--- +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 From f0505041882648109689c954dbcc8ebb9039f657 Mon Sep 17 00:00:00 2001 From: Jonathan Norris <jonathan@taplytics.com> Date: Mon, 7 Jul 2025 16:26:53 -0400 Subject: [PATCH 07/44] feat: initial MCP server tools working --- README.md | 3 +- package.json | 6 +- src/api/features.ts | 65 ++- src/api/zodClient.ts | 2 +- src/commands/wink.ts | 2 +- src/ui/tableOutput.ts | 2 +- src/ui/writer.ts | 32 +- src/utils/parsers/BaseParser.ts | 2 +- src/utils/parsers/custom.ts | 2 +- src/utils/refactor/RefactorEngine.ts | 2 +- yarn.lock | 672 ++++++++++++++++++++++++++- 11 files changed, 738 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index d14fa89e8..f63594ba6 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,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 @@ -126,7 +126,6 @@ USAGE * [`dvc environments`](docs/environments.md) - Create a new Environment for an existing Feature. * [`dvc features`](docs/features.md) - Create, view, or modify Features with the Management API. * [`dvc generate`](docs/generate.md) - Generate Devcycle related files. -* [`dvc help`](docs/help.md) - Display help for dvc. * [`dvc identity`](docs/identity.md) - View or manage your DevCycle Identity. * [`dvc keys`](docs/keys.md) - Retrieve SDK keys from the Management API. * [`dvc login`](docs/login.md) - Log in to DevCycle. diff --git a/package.json b/package.json index 1207c9703..e057cdb88 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": "./dist/mcp/index.js" }, "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", diff --git a/src/api/features.ts b/src/api/features.ts index 2c242b8d7..9443a347f 100644 --- a/src/api/features.ts +++ b/src/api/features.ts @@ -21,15 +21,64 @@ export const fetchFeatures = async ( staleness?: string } = {}, ): Promise<Feature[]> => { - const response = await apiClient.get(FEATURE_URL, { - headers: buildHeaders(token), - params: { - project: project_id, - }, - queries, - }) + console.error('=== fetchFeatures API START ===') + console.error('Project ID:', project_id) + console.error('Token length:', token?.length || 0) + console.error('Token prefix:', token?.substring(0, 20) + '...') + console.error('Queries:', JSON.stringify(queries, null, 2)) + + const url = FEATURE_URL + console.error('API URL template:', url) + console.error('Full URL will be:', url.replace(':project', project_id)) + + try { + console.error('Making API request...') + const response = await apiClient.get(FEATURE_URL, { + headers: buildHeaders(token), + params: { + project: project_id, + }, + queries, + }) + + console.error('API request successful') + console.error('Response type:', typeof response) + console.error('Response is array:', Array.isArray(response)) + console.error( + 'Response length:', + Array.isArray(response) ? response.length : 'N/A', + ) + if (Array.isArray(response) && response.length > 0) { + console.error( + 'First feature sample:', + JSON.stringify(response[0], null, 2), + ) + } + console.error('=== fetchFeatures API END (SUCCESS) ===') - return response + return response + } catch (error) { + console.error('=== fetchFeatures API ERROR ===') + console.error('Error type:', error?.constructor?.name) + console.error( + 'Error message:', + error instanceof Error ? error.message : 'Unknown', + ) + if (error instanceof AxiosError) { + console.error('Axios error status:', error.response?.status) + console.error('Axios error data:', error.response?.data) + console.error('Axios error headers:', error.response?.headers) + console.error('Request config:', { + url: error.config?.url, + method: error.config?.method, + headers: error.config?.headers, + params: error.config?.params, + }) + } + console.error('Full error:', error) + console.error('=== fetchFeatures API END (ERROR) ===') + throw error + } } export const fetchFeatureByKey = async ( diff --git a/src/api/zodClient.ts b/src/api/zodClient.ts index 46a8258dd..e49f8ce67 100644 --- a/src/api/zodClient.ts +++ b/src/api/zodClient.ts @@ -297,7 +297,7 @@ const UpdateAudienceDto = z .partial() const VariableValidationEntity = z.object({ schemaType: z.string(), - enumValues: z.array(z.string()).optional(), + enumValues: z.array(z.any()).optional(), regexPattern: z.string().optional(), jsonSchema: z.string().optional(), description: z.string(), diff --git a/src/commands/wink.ts b/src/commands/wink.ts index 67cff53c2..d7b955cf1 100644 --- a/src/commands/wink.ts +++ b/src/commands/wink.ts @@ -5,6 +5,6 @@ export default class Wink extends Command { static hidden = true async run(): Promise<void> { - console.log(togglebotWink) + console.error(togglebotWink) } } diff --git a/src/ui/tableOutput.ts b/src/ui/tableOutput.ts index 31e75d755..575520096 100644 --- a/src/ui/tableOutput.ts +++ b/src/ui/tableOutput.ts @@ -19,7 +19,7 @@ export default class TableOutput { } ux.table(data, columns, options) - console.log('\r') + console.error('\r') } public printOverrides<T extends Record<string, unknown>>( diff --git a/src/ui/writer.ts b/src/ui/writer.ts index 5e406290e..2d5828bbd 100644 --- a/src/ui/writer.ts +++ b/src/ui/writer.ts @@ -3,31 +3,31 @@ import { togglebot, togglebotSleep, togglebotWink } from './togglebot' export default class Writer { public successMessage(message: string): void { - if (!this.headless) console.log(chalk.green(`✅ ${message}`)) + if (!this.headless) console.error(chalk.green(`✅ ${message}`)) } public failureMessage(message: string): void { - if (!this.headless) console.log(chalk.red(`❌ ${message}`)) + if (!this.headless) console.error(chalk.red(`❌ ${message}`)) } public warningMessage(message: string): void { - if (!this.headless) console.log(chalk.yellow(`⚠️ ${message}`)) + if (!this.headless) console.error(chalk.yellow(`⚠️ ${message}`)) } public statusMessage(message: string): void { - if (!this.headless) console.log(chalk.dim(`🤖 ${message}`)) + if (!this.headless) console.error(chalk.dim(`🤖 ${message}`)) } public infoMessage(message: string): void { - if (!this.headless) console.log(`🤖 ${message}`) + if (!this.headless) console.error(`🤖 ${message}`) } public infoMessageWithCommand(message: string, command: string): void { - if (!this.headless) console.log(`🤖 ${message} ${chalk.bold(command)}`) + if (!this.headless) console.error(`🤖 ${message} ${chalk.bold(command)}`) } public title(message: string): void { - if (!this.headless) console.log(`🤖 ${chalk.bold(message)}`) + if (!this.headless) console.error(`🤖 ${chalk.bold(message)}`) } public printCurrentValues(values: unknown): void { @@ -41,40 +41,40 @@ export default class Writer { public list(list: string[]): void { if (!this.headless) - console.log(list.length ? `${list.join('\n\r')}` : '(Empty)') + console.error(list.length ? `${list.join('\n\r')}` : '(Empty)') } public blankLine(): void { - if (!this.headless) console.log('\n\r') + if (!this.headless) console.error('\n\r') } public divider(): void { if (!this.headless) - console.log('----------------------------------------') + console.error('----------------------------------------') } public showResults(results: unknown): void { if (this.headless) { - console.log(JSON.stringify(results)) + console.error(JSON.stringify(results)) } else { - console.log(JSON.stringify(results, null, 2)) + console.error(JSON.stringify(results, null, 2)) } } public showRawResults(results: string): void { - console.log(results) + console.error(results) } public showTogglebot(): void { - if (!this.headless) console.log(togglebot) + if (!this.headless) console.error(togglebot) } public showTogglebotWink(): void { - if (!this.headless) console.log(togglebotWink) + if (!this.headless) console.error(togglebotWink) } public showTogglebotSleep(): void { - if (!this.headless) console.log(togglebotSleep) + if (!this.headless) console.error(togglebotSleep) } public showError(message: string): void { diff --git a/src/utils/parsers/BaseParser.ts b/src/utils/parsers/BaseParser.ts index 8fec5d33d..85a51ec53 100644 --- a/src/utils/parsers/BaseParser.ts +++ b/src/utils/parsers/BaseParser.ts @@ -171,7 +171,7 @@ export abstract class BaseParser { } printRegexPattern(): void { - console.log( + console.error( `Pattern for ${this.identity} parser: \n\t${this.buildRegexPattern().source}`, ) } diff --git a/src/utils/parsers/custom.ts b/src/utils/parsers/custom.ts index 3f53c5b72..571c03f31 100644 --- a/src/utils/parsers/custom.ts +++ b/src/utils/parsers/custom.ts @@ -18,7 +18,7 @@ export class CustomParser extends BaseParser { printRegexPattern(): void { const patterns = this.customPatterns.join('\n\t') - console.log(`Pattern for ${this.identity} parser: \n\t${patterns}`) + console.error(`Pattern for ${this.identity} parser: \n\t${patterns}`) } override match(content: string): MatchResult | null { diff --git a/src/utils/refactor/RefactorEngine.ts b/src/utils/refactor/RefactorEngine.ts index 8d11c45f1..d1cc588e4 100644 --- a/src/utils/refactor/RefactorEngine.ts +++ b/src/utils/refactor/RefactorEngine.ts @@ -511,7 +511,7 @@ export abstract class RefactorEngine { try { const { code } = recast.print(this.ast) if (this.output === 'console') { - console.log(code) + console.error(code) } else if (this.fileRefactored) { fs.writeFileSync(this.filepath, code) } diff --git a/yarn.lock b/yarn.lock index caa9d824a..1a7b09eb8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -161,6 +161,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 +175,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" @@ -196,6 +210,17 @@ __metadata: languageName: node linkType: hard +"@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" @@ -395,6 +420,16 @@ __metadata: languageName: node linkType: hard +"@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 +443,9 @@ __metadata: version: 0.0.0-use.local resolution: "@devcycle/cli@workspace:." dependencies: - "@babel/parser": "npm:^7.27.0" + "@babel/parser": "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 +495,7 @@ __metadata: zod: "npm:^3.24.2" bin: dvc: ./bin/run + dvc-mcp: ./dist/mcp/index.js languageName: unknown linkType: soft @@ -784,6 +821,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 +2021,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 +2114,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 +2483,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 +2605,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 +2733,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 +2756,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 +3140,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 +3163,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 +3184,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 +3201,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 +3363,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 +3424,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 +3442,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 +3481,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 +3545,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 +3559,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 +3582,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 +3748,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 +3790,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 +3843,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 +4031,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 +4166,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 +4320,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 +4345,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 +4500,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 +4595,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 +4618,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 +4682,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 +4780,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 +4798,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 +4885,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 +4947,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 +5072,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 +5833,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 +5882,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 +5920,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 +5936,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 +6286,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 +6696,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 +6746,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 +7070,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 +7154,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 +7217,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 +7368,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 +7409,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 +7448,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 +7766,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 +7804,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 +7893,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 +7921,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 +7954,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 +8009,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 +8285,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 +8532,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 +8701,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 +8858,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 +8976,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 +9399,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 +9415,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" From a6c7644dce84332b0556133764a97f57a577508f Mon Sep 17 00:00:00 2001 From: Jonathan Norris <jonathan@taplytics.com> Date: Mon, 7 Jul 2025 16:27:05 -0400 Subject: [PATCH 08/44] feat: add new MCP files --- src/mcp/index.ts | 32 ++++ src/mcp/server.ts | 332 ++++++++++++++++++++++++++++++++++++++++++ src/mcp/utils/api.ts | 172 ++++++++++++++++++++++ src/mcp/utils/auth.ts | 157 ++++++++++++++++++++ 4 files changed, 693 insertions(+) create mode 100644 src/mcp/index.ts create mode 100644 src/mcp/server.ts create mode 100644 src/mcp/utils/api.ts create mode 100644 src/mcp/utils/auth.ts diff --git a/src/mcp/index.ts b/src/mcp/index.ts new file mode 100644 index 000000000..55671d126 --- /dev/null +++ b/src/mcp/index.ts @@ -0,0 +1,32 @@ +#!/usr/bin/env node + +import { Server } from '@modelcontextprotocol/sdk/server/index.js' +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' +import { DevCycleMCPServer } from './server' + +async function main() { + const server = new Server( + { + name: 'devcycle', + version: '0.0.1', + }, + { + 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:', error) + process.exit(1) +}) diff --git a/src/mcp/server.ts b/src/mcp/server.ts new file mode 100644 index 000000000..eefb975a2 --- /dev/null +++ b/src/mcp/server.ts @@ -0,0 +1,332 @@ +import { Server } from '@modelcontextprotocol/sdk/server/index.js' +import { + CallToolRequestSchema, + ListToolsRequestSchema, +} from '@modelcontextprotocol/sdk/types.js' +import { DevCycleAuth } from './utils/auth' +import { DevCycleApiClient } from './utils/api' +import Writer from '../ui/writer' + +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 setupToolHandlers() { + this.server.setRequestHandler(ListToolsRequestSchema, async () => { + return { + tools: [ + { + name: 'list_features', + description: 'List features in the current project', + inputSchema: { + type: 'object', + properties: { + search: { + type: 'string', + description: + 'Search query to filter features', + }, + page: { + type: 'number', + description: 'Page number (default: 1)', + }, + per_page: { + type: 'number', + description: + 'Number of items per page (default: 10)', + }, + }, + }, + }, + { + name: 'list_variables', + description: 'List variables in the current project', + inputSchema: { + type: 'object', + properties: { + search: { + type: 'string', + description: + 'Search query to filter variables', + }, + page: { + type: 'number', + description: 'Page number (default: 1)', + }, + per_page: { + type: 'number', + description: + 'Number of items per page (default: 10)', + }, + }, + }, + }, + { + name: 'get_sdk_keys', + description: 'Get SDK keys for an environment', + inputSchema: { + type: 'object', + properties: { + environment_key: { + type: 'string', + description: 'The key of the environment', + }, + key_type: { + type: 'string', + enum: ['mobile', 'server', 'client'], + description: + 'The type of SDK key to retrieve', + }, + }, + required: ['environment_key'], + }, + }, + { + name: 'enable_targeting', + description: + 'Enable targeting for a feature in an environment', + inputSchema: { + type: 'object', + properties: { + feature_key: { + type: 'string', + description: 'The key of the feature', + }, + environment_key: { + type: 'string', + description: 'The key of the environment', + }, + }, + required: ['feature_key', 'environment_key'], + }, + }, + { + name: 'disable_targeting', + description: + 'Disable targeting for a feature in an environment', + inputSchema: { + type: 'object', + properties: { + feature_key: { + type: 'string', + description: 'The key of the feature', + }, + environment_key: { + type: 'string', + description: 'The key of the environment', + }, + }, + required: ['feature_key', 'environment_key'], + }, + }, + { + name: 'create_feature', + description: + 'Create a new feature flag (supports interactive mode)', + inputSchema: { + type: 'object', + properties: { + key: { + type: 'string', + description: 'Unique feature key', + }, + name: { + type: 'string', + description: 'Human-readable feature name', + }, + description: { + type: 'string', + description: 'Feature description', + }, + type: { + type: 'string', + enum: [ + 'release', + 'experiment', + 'permission', + 'ops', + ], + description: 'Feature type', + }, + interactive: { + type: 'boolean', + description: + 'Use interactive mode to prompt for missing fields', + }, + }, + }, + }, + ], + } + }) + + this.server.setRequestHandler( + CallToolRequestSchema, + async (request: any) => { + const { name, arguments: args } = request.params + + try { + let result + switch (name) { + case 'list_features': + result = await this.listFeatures(args) + break + case 'list_variables': + result = await this.listVariables(args) + break + case 'get_sdk_keys': + result = await this.getSdkKeys(args) + break + case 'enable_targeting': + result = await this.enableTargeting(args) + break + case 'disable_targeting': + result = await this.disableTargeting(args) + break + case 'create_feature': + result = await this.createFeature(args) + break + default: + throw new Error(`Unknown tool: ${name}`) + } + + return result + } catch (error) { + console.error(`Error in tool handler ${name}:`, error) + + const errorMessage = + error instanceof Error ? error.message : 'Unknown error' + return { + content: [ + { + type: 'text', + text: `Error: ${errorMessage}`, + }, + ], + } + } + }, + ) + } + + private setupErrorHandling() { + this.server.onerror = (error: Error) => { + console.error('MCP Server Error:', error) + } + } + + // Tool implementations + private async listFeatures(args: any) { + try { + const features = await this.apiClient.listFeatures(args) + return { + content: [ + { + type: 'text', + text: JSON.stringify(features, null, 2), + }, + ], + } + } catch (error) { + console.error('Error listing features:', error) + throw error + } + } + + private async listVariables(args: any) { + const variables = await this.apiClient.listVariables(args) + return { + content: [ + { + type: 'text', + text: JSON.stringify(variables, null, 2), + }, + ], + } + } + + private async getSdkKeys(args: any) { + const keys = await this.apiClient.getSdkKeys( + args.environment_key, + args.key_type, + ) + return { + content: [ + { + type: 'text', + text: JSON.stringify(keys, null, 2), + }, + ], + } + } + + private async enableTargeting(args: any) { + const result = await this.apiClient.enableTargeting( + args.feature_key, + args.environment_key, + ) + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + } + } + + private async disableTargeting(args: any) { + const result = await this.apiClient.disableTargeting( + args.feature_key, + args.environment_key, + ) + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + } + } + + private async createFeature(args: any) { + const result = await this.apiClient.createFeature(args) + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + } + } +} diff --git a/src/mcp/utils/api.ts b/src/mcp/utils/api.ts new file mode 100644 index 000000000..2c8ae0b4a --- /dev/null +++ b/src/mcp/utils/api.ts @@ -0,0 +1,172 @@ +import { DevCycleAuth } from './auth' +import { + fetchFeatures, + fetchFeatureByKey, + createFeature, +} from '../../api/features' +import { fetchVariables, fetchVariableByKey } from '../../api/variables' +import { fetchEnvironmentByKey } from '../../api/environments' +import { enableTargeting, disableTargeting } from '../../api/targeting' + +export class DevCycleApiClient { + constructor(private auth: DevCycleAuth) {} + + async listFeatures(args: { + search?: string + page?: number + per_page?: number + }) { + try { + this.auth.requireAuth() + this.auth.requireProject() + + const authToken = this.auth.getAuthToken() + const projectKey = this.auth.getProjectKey() + + const query = { + search: args.search, + page: args.page, + perPage: args.per_page, + } + + const result = await fetchFeatures(authToken, projectKey, query) + return result + } catch (error) { + console.error('Error during listFeatures:', error) + throw error + } + } + + async getFeature(featureKey: string) { + this.auth.requireAuth() + this.auth.requireProject() + + return await fetchFeatureByKey( + this.auth.getAuthToken(), + this.auth.getProjectKey(), + featureKey, + ) + } + + async listVariables(args: { + search?: string + page?: number + per_page?: number + }) { + this.auth.requireAuth() + this.auth.requireProject() + + const query = { + search: args.search, + page: args.page, + perPage: args.per_page, + } + + return await fetchVariables( + this.auth.getAuthToken(), + this.auth.getProjectKey(), + query, + ) + } + + async getVariable(variableKey: string) { + this.auth.requireAuth() + this.auth.requireProject() + + return await fetchVariableByKey( + this.auth.getAuthToken(), + this.auth.getProjectKey(), + variableKey, + ) + } + + async getSdkKeys( + environmentKey: string, + keyType?: 'mobile' | 'server' | 'client', + ) { + this.auth.requireAuth() + this.auth.requireProject() + + const environment = await fetchEnvironmentByKey( + this.auth.getAuthToken(), + this.auth.getProjectKey(), + environmentKey, + ) + + const sdkKeys = environment.sdkKeys + + if (keyType) { + return { + [keyType]: sdkKeys[keyType], + } + } + + return { + mobile: sdkKeys.mobile, + server: sdkKeys.server, + client: sdkKeys.client, + } + } + + async enableTargeting(featureKey: string, environmentKey: string) { + this.auth.requireAuth() + this.auth.requireProject() + + return await enableTargeting( + this.auth.getAuthToken(), + this.auth.getProjectKey(), + featureKey, + environmentKey, + ) + } + + async disableTargeting(featureKey: string, environmentKey: string) { + this.auth.requireAuth() + this.auth.requireProject() + + return await disableTargeting( + this.auth.getAuthToken(), + this.auth.getProjectKey(), + featureKey, + environmentKey, + ) + } + + async createFeature(args: { + key?: string + name?: string + description?: string + type?: 'release' | 'experiment' | 'permission' | 'ops' + interactive?: boolean + }) { + this.auth.requireAuth() + this.auth.requireProject() + + if (args.interactive) { + // For interactive mode, we would need to implement prompts + // For now, return an error asking for explicit parameters + throw new Error( + 'Interactive mode not yet supported in MCP. Please provide explicit parameters: key, name, description, type', + ) + } + + if (!args.key || !args.name) { + throw new Error( + 'Feature key and name are required when not using interactive mode', + ) + } + + const featureData = { + key: args.key, + name: args.name, + description: args.description || '', + type: args.type || 'release', + } + + return await createFeature( + this.auth.getAuthToken(), + this.auth.getProjectKey(), + featureData, + ) + } +} diff --git a/src/mcp/utils/auth.ts b/src/mcp/utils/auth.ts new file mode 100644 index 000000000..e325dde48 --- /dev/null +++ b/src/mcp/utils/auth.ts @@ -0,0 +1,157 @@ +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) { + throw new Error( + 'No authentication found. Please set DEVCYCLE_CLIENT_ID and DEVCYCLE_CLIENT_SECRET environment variables, ' + + 'or run "dvc login sso" in the CLI first.', + ) + } + + if (!this._projectKey) { + throw new Error( + 'No project configured. Please set DEVCYCLE_PROJECT_KEY environment variable, ' + + 'or configure a project using "dvc projects select" in the CLI.', + ) + } + } catch (error) { + console.error( + 'Failed to initialize DevCycle authentication:', + error, + ) + throw new Error( + `Failed to initialize DevCycle authentication: ${error instanceof Error ? error.message : 'Unknown error'}`, + ) + } + } + + 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 configure DevCycle credentials.', + ) + } + } + + requireProject(): void { + if (!this._projectKey) { + throw new Error( + 'Project key required. Please configure a DevCycle project.', + ) + } + } +} From a2abdab37fc3af8b525e70036e8ee695f1afbc8a Mon Sep 17 00:00:00 2001 From: Jonathan Norris <jonathan@taplytics.com> Date: Mon, 7 Jul 2025 16:37:35 -0400 Subject: [PATCH 09/44] fix: revert console.error changes in non mcp code --- src/api/features.ts | 65 ++++------------------------ src/commands/wink.ts | 2 +- src/ui/tableOutput.ts | 2 +- src/ui/writer.ts | 32 +++++++------- src/utils/parsers/BaseParser.ts | 2 +- src/utils/parsers/custom.ts | 2 +- src/utils/refactor/RefactorEngine.ts | 2 +- 7 files changed, 29 insertions(+), 78 deletions(-) diff --git a/src/api/features.ts b/src/api/features.ts index 9443a347f..2c242b8d7 100644 --- a/src/api/features.ts +++ b/src/api/features.ts @@ -21,64 +21,15 @@ export const fetchFeatures = async ( staleness?: string } = {}, ): Promise<Feature[]> => { - console.error('=== fetchFeatures API START ===') - console.error('Project ID:', project_id) - console.error('Token length:', token?.length || 0) - console.error('Token prefix:', token?.substring(0, 20) + '...') - console.error('Queries:', JSON.stringify(queries, null, 2)) - - const url = FEATURE_URL - console.error('API URL template:', url) - console.error('Full URL will be:', url.replace(':project', project_id)) - - try { - console.error('Making API request...') - const response = await apiClient.get(FEATURE_URL, { - headers: buildHeaders(token), - params: { - project: project_id, - }, - queries, - }) - - console.error('API request successful') - console.error('Response type:', typeof response) - console.error('Response is array:', Array.isArray(response)) - console.error( - 'Response length:', - Array.isArray(response) ? response.length : 'N/A', - ) - if (Array.isArray(response) && response.length > 0) { - console.error( - 'First feature sample:', - JSON.stringify(response[0], null, 2), - ) - } - console.error('=== fetchFeatures API END (SUCCESS) ===') + const response = await apiClient.get(FEATURE_URL, { + headers: buildHeaders(token), + params: { + project: project_id, + }, + queries, + }) - return response - } catch (error) { - console.error('=== fetchFeatures API ERROR ===') - console.error('Error type:', error?.constructor?.name) - console.error( - 'Error message:', - error instanceof Error ? error.message : 'Unknown', - ) - if (error instanceof AxiosError) { - console.error('Axios error status:', error.response?.status) - console.error('Axios error data:', error.response?.data) - console.error('Axios error headers:', error.response?.headers) - console.error('Request config:', { - url: error.config?.url, - method: error.config?.method, - headers: error.config?.headers, - params: error.config?.params, - }) - } - console.error('Full error:', error) - console.error('=== fetchFeatures API END (ERROR) ===') - throw error - } + return response } export const fetchFeatureByKey = async ( diff --git a/src/commands/wink.ts b/src/commands/wink.ts index d7b955cf1..67cff53c2 100644 --- a/src/commands/wink.ts +++ b/src/commands/wink.ts @@ -5,6 +5,6 @@ export default class Wink extends Command { static hidden = true async run(): Promise<void> { - console.error(togglebotWink) + console.log(togglebotWink) } } diff --git a/src/ui/tableOutput.ts b/src/ui/tableOutput.ts index 575520096..31e75d755 100644 --- a/src/ui/tableOutput.ts +++ b/src/ui/tableOutput.ts @@ -19,7 +19,7 @@ export default class TableOutput { } ux.table(data, columns, options) - console.error('\r') + console.log('\r') } public printOverrides<T extends Record<string, unknown>>( diff --git a/src/ui/writer.ts b/src/ui/writer.ts index 2d5828bbd..5e406290e 100644 --- a/src/ui/writer.ts +++ b/src/ui/writer.ts @@ -3,31 +3,31 @@ import { togglebot, togglebotSleep, togglebotWink } from './togglebot' export default class Writer { public successMessage(message: string): void { - if (!this.headless) console.error(chalk.green(`✅ ${message}`)) + if (!this.headless) console.log(chalk.green(`✅ ${message}`)) } public failureMessage(message: string): void { - if (!this.headless) console.error(chalk.red(`❌ ${message}`)) + if (!this.headless) console.log(chalk.red(`❌ ${message}`)) } public warningMessage(message: string): void { - if (!this.headless) console.error(chalk.yellow(`⚠️ ${message}`)) + if (!this.headless) console.log(chalk.yellow(`⚠️ ${message}`)) } public statusMessage(message: string): void { - if (!this.headless) console.error(chalk.dim(`🤖 ${message}`)) + if (!this.headless) console.log(chalk.dim(`🤖 ${message}`)) } public infoMessage(message: string): void { - if (!this.headless) console.error(`🤖 ${message}`) + if (!this.headless) console.log(`🤖 ${message}`) } public infoMessageWithCommand(message: string, command: string): void { - if (!this.headless) console.error(`🤖 ${message} ${chalk.bold(command)}`) + if (!this.headless) console.log(`🤖 ${message} ${chalk.bold(command)}`) } public title(message: string): void { - if (!this.headless) console.error(`🤖 ${chalk.bold(message)}`) + if (!this.headless) console.log(`🤖 ${chalk.bold(message)}`) } public printCurrentValues(values: unknown): void { @@ -41,40 +41,40 @@ export default class Writer { public list(list: string[]): void { if (!this.headless) - console.error(list.length ? `${list.join('\n\r')}` : '(Empty)') + console.log(list.length ? `${list.join('\n\r')}` : '(Empty)') } public blankLine(): void { - if (!this.headless) console.error('\n\r') + if (!this.headless) console.log('\n\r') } public divider(): void { if (!this.headless) - console.error('----------------------------------------') + console.log('----------------------------------------') } public showResults(results: unknown): void { if (this.headless) { - console.error(JSON.stringify(results)) + console.log(JSON.stringify(results)) } else { - console.error(JSON.stringify(results, null, 2)) + console.log(JSON.stringify(results, null, 2)) } } public showRawResults(results: string): void { - console.error(results) + console.log(results) } public showTogglebot(): void { - if (!this.headless) console.error(togglebot) + if (!this.headless) console.log(togglebot) } public showTogglebotWink(): void { - if (!this.headless) console.error(togglebotWink) + if (!this.headless) console.log(togglebotWink) } public showTogglebotSleep(): void { - if (!this.headless) console.error(togglebotSleep) + if (!this.headless) console.log(togglebotSleep) } public showError(message: string): void { diff --git a/src/utils/parsers/BaseParser.ts b/src/utils/parsers/BaseParser.ts index 85a51ec53..8fec5d33d 100644 --- a/src/utils/parsers/BaseParser.ts +++ b/src/utils/parsers/BaseParser.ts @@ -171,7 +171,7 @@ export abstract class BaseParser { } printRegexPattern(): void { - console.error( + console.log( `Pattern for ${this.identity} parser: \n\t${this.buildRegexPattern().source}`, ) } diff --git a/src/utils/parsers/custom.ts b/src/utils/parsers/custom.ts index 571c03f31..3f53c5b72 100644 --- a/src/utils/parsers/custom.ts +++ b/src/utils/parsers/custom.ts @@ -18,7 +18,7 @@ export class CustomParser extends BaseParser { printRegexPattern(): void { const patterns = this.customPatterns.join('\n\t') - console.error(`Pattern for ${this.identity} parser: \n\t${patterns}`) + console.log(`Pattern for ${this.identity} parser: \n\t${patterns}`) } override match(content: string): MatchResult | null { diff --git a/src/utils/refactor/RefactorEngine.ts b/src/utils/refactor/RefactorEngine.ts index d1cc588e4..8d11c45f1 100644 --- a/src/utils/refactor/RefactorEngine.ts +++ b/src/utils/refactor/RefactorEngine.ts @@ -511,7 +511,7 @@ export abstract class RefactorEngine { try { const { code } = recast.print(this.ast) if (this.output === 'console') { - console.error(code) + console.log(code) } else if (this.fileRefactored) { fs.writeFileSync(this.filepath, code) } From d775ee04c61a54ca3b0375fcf3e64d30ac2963ac Mon Sep 17 00:00:00 2001 From: Jonathan Norris <jonathan@taplytics.com> Date: Mon, 7 Jul 2025 16:39:20 -0400 Subject: [PATCH 10/44] fix: revert readme changes --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index f63594ba6..13141436c 100644 --- a/README.md +++ b/README.md @@ -126,6 +126,7 @@ USAGE * [`dvc environments`](docs/environments.md) - Create a new Environment for an existing Feature. * [`dvc features`](docs/features.md) - Create, view, or modify Features with the Management API. * [`dvc generate`](docs/generate.md) - Generate Devcycle related files. +* [`dvc help`](docs/help.md) - Display help for dvc. * [`dvc identity`](docs/identity.md) - View or manage your DevCycle Identity. * [`dvc keys`](docs/keys.md) - Retrieve SDK keys from the Management API. * [`dvc login`](docs/login.md) - Log in to DevCycle. From d602e945492691a0477f62210e9609ddd683573a Mon Sep 17 00:00:00 2001 From: Jonathan Norris <jonathan@taplytics.com> Date: Mon, 7 Jul 2025 16:54:52 -0400 Subject: [PATCH 11/44] chore: cleanup api.ts, add logging --- README.md | 1 - src/mcp/utils/api.ts | 286 ++++++++++++++++++++++++++++--------------- 2 files changed, 188 insertions(+), 99 deletions(-) diff --git a/README.md b/README.md index 13141436c..f63594ba6 100644 --- a/README.md +++ b/README.md @@ -126,7 +126,6 @@ USAGE * [`dvc environments`](docs/environments.md) - Create a new Environment for an existing Feature. * [`dvc features`](docs/features.md) - Create, view, or modify Features with the Management API. * [`dvc generate`](docs/generate.md) - Generate Devcycle related files. -* [`dvc help`](docs/help.md) - Display help for dvc. * [`dvc identity`](docs/identity.md) - View or manage your DevCycle Identity. * [`dvc keys`](docs/keys.md) - Retrieve SDK keys from the Management API. * [`dvc login`](docs/login.md) - Log in to DevCycle. diff --git a/src/mcp/utils/api.ts b/src/mcp/utils/api.ts index 2c8ae0b4a..5bf42e1cc 100644 --- a/src/mcp/utils/api.ts +++ b/src/mcp/utils/api.ts @@ -1,13 +1,23 @@ import { DevCycleAuth } from './auth' -import { - fetchFeatures, - fetchFeatureByKey, - createFeature, -} from '../../api/features' -import { fetchVariables, fetchVariableByKey } from '../../api/variables' +import { fetchFeatures, createFeature } from '../../api/features' +import { fetchVariables } from '../../api/variables' import { fetchEnvironmentByKey } from '../../api/environments' import { enableTargeting, disableTargeting } from '../../api/targeting' +function getErrorMessage(error: unknown): string { + if (error instanceof Error) { + return error.message + } + 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) {} @@ -16,6 +26,8 @@ export class DevCycleApiClient { page?: number per_page?: number }) { + console.error('MCP listFeatures params:', JSON.stringify(args, null, 2)) + try { this.auth.requireAuth() this.auth.requireProject() @@ -30,106 +42,166 @@ export class DevCycleApiClient { } const result = await fetchFeatures(authToken, projectKey, query) + console.error( + 'MCP listFeatures result:', + JSON.stringify(result, null, 2), + ) return result } catch (error) { - console.error('Error during listFeatures:', error) - throw error + console.error( + 'MCP listFeatures error:', + JSON.stringify({ error: getErrorMessage(error) }, null, 2), + ) + console.error('Error during listFeatures:', getErrorMessage(error)) + throw ensureError(error) } } - async getFeature(featureKey: string) { - this.auth.requireAuth() - this.auth.requireProject() - - return await fetchFeatureByKey( - this.auth.getAuthToken(), - this.auth.getProjectKey(), - featureKey, - ) - } - async listVariables(args: { search?: string page?: number per_page?: number }) { - this.auth.requireAuth() - this.auth.requireProject() - - const query = { - search: args.search, - page: args.page, - perPage: args.per_page, - } - - return await fetchVariables( - this.auth.getAuthToken(), - this.auth.getProjectKey(), - query, + console.error( + 'MCP listVariables params:', + JSON.stringify(args, null, 2), ) - } - async getVariable(variableKey: string) { - this.auth.requireAuth() - this.auth.requireProject() + try { + this.auth.requireAuth() + this.auth.requireProject() - return await fetchVariableByKey( - this.auth.getAuthToken(), - this.auth.getProjectKey(), - variableKey, - ) + const query = { + search: args.search, + page: args.page, + perPage: args.per_page, + } + + const result = await fetchVariables( + this.auth.getAuthToken(), + this.auth.getProjectKey(), + query, + ) + console.error( + 'MCP listVariables result:', + JSON.stringify(result, null, 2), + ) + return result + } catch (error) { + console.error( + 'MCP listVariables error:', + JSON.stringify({ error: getErrorMessage(error) }, null, 2), + ) + throw ensureError(error) + } } async getSdkKeys( environmentKey: string, keyType?: 'mobile' | 'server' | 'client', ) { - this.auth.requireAuth() - this.auth.requireProject() - - const environment = await fetchEnvironmentByKey( - this.auth.getAuthToken(), - this.auth.getProjectKey(), - environmentKey, + console.error( + 'MCP getSdkKeys params:', + JSON.stringify({ environmentKey, keyType }, null, 2), ) - const sdkKeys = environment.sdkKeys + try { + this.auth.requireAuth() + this.auth.requireProject() + + const environment = await fetchEnvironmentByKey( + this.auth.getAuthToken(), + this.auth.getProjectKey(), + environmentKey, + ) - if (keyType) { - return { - [keyType]: sdkKeys[keyType], + const sdkKeys = environment.sdkKeys + + let result + if (keyType) { + result = { + [keyType]: sdkKeys[keyType], + } + } else { + result = { + mobile: sdkKeys.mobile, + server: sdkKeys.server, + client: sdkKeys.client, + } } - } - return { - mobile: sdkKeys.mobile, - server: sdkKeys.server, - client: sdkKeys.client, + console.error( + 'MCP getSdkKeys result:', + JSON.stringify(result, null, 2), + ) + return result + } catch (error) { + console.error( + 'MCP getSdkKeys error:', + JSON.stringify({ error: getErrorMessage(error) }, null, 2), + ) + throw ensureError(error) } } async enableTargeting(featureKey: string, environmentKey: string) { - this.auth.requireAuth() - this.auth.requireProject() - - return await enableTargeting( - this.auth.getAuthToken(), - this.auth.getProjectKey(), - featureKey, - environmentKey, + console.error( + 'MCP enableTargeting params:', + JSON.stringify({ featureKey, environmentKey }, null, 2), ) + + try { + this.auth.requireAuth() + this.auth.requireProject() + + const result = await enableTargeting( + this.auth.getAuthToken(), + this.auth.getProjectKey(), + featureKey, + environmentKey, + ) + console.error( + 'MCP enableTargeting result:', + JSON.stringify(result, null, 2), + ) + return result + } catch (error) { + console.error( + 'MCP enableTargeting error:', + JSON.stringify({ error: getErrorMessage(error) }, null, 2), + ) + throw ensureError(error) + } } async disableTargeting(featureKey: string, environmentKey: string) { - this.auth.requireAuth() - this.auth.requireProject() - - return await disableTargeting( - this.auth.getAuthToken(), - this.auth.getProjectKey(), - featureKey, - environmentKey, + console.error( + 'MCP disableTargeting params:', + JSON.stringify({ featureKey, environmentKey }, null, 2), ) + + try { + this.auth.requireAuth() + this.auth.requireProject() + + const result = await disableTargeting( + this.auth.getAuthToken(), + this.auth.getProjectKey(), + featureKey, + environmentKey, + ) + console.error( + 'MCP disableTargeting result:', + JSON.stringify(result, null, 2), + ) + return result + } catch (error) { + console.error( + 'MCP disableTargeting error:', + JSON.stringify({ error: getErrorMessage(error) }, null, 2), + ) + throw ensureError(error) + } } async createFeature(args: { @@ -139,34 +211,52 @@ export class DevCycleApiClient { type?: 'release' | 'experiment' | 'permission' | 'ops' interactive?: boolean }) { - this.auth.requireAuth() - this.auth.requireProject() + console.error( + 'MCP createFeature params:', + JSON.stringify(args, null, 2), + ) - if (args.interactive) { - // For interactive mode, we would need to implement prompts - // For now, return an error asking for explicit parameters - throw new Error( - 'Interactive mode not yet supported in MCP. Please provide explicit parameters: key, name, description, type', - ) - } + try { + this.auth.requireAuth() + this.auth.requireProject() - if (!args.key || !args.name) { - throw new Error( - 'Feature key and name are required when not using interactive mode', - ) - } + if (args.interactive) { + // For interactive mode, we would need to implement prompts + // For now, return an error asking for explicit parameters + throw new Error( + 'Interactive mode not yet supported in MCP. Please provide explicit parameters: key, name, description, type', + ) + } - const featureData = { - key: args.key, - name: args.name, - description: args.description || '', - type: args.type || 'release', - } + if (!args.key || !args.name) { + throw new Error( + 'Feature key and name are required when not using interactive mode', + ) + } - return await createFeature( - this.auth.getAuthToken(), - this.auth.getProjectKey(), - featureData, - ) + const featureData = { + key: args.key, + name: args.name, + description: args.description || '', + type: args.type || 'release', + } + + const result = await createFeature( + this.auth.getAuthToken(), + this.auth.getProjectKey(), + featureData, + ) + console.error( + 'MCP createFeature result:', + JSON.stringify(result, null, 2), + ) + return result + } catch (error) { + console.error( + 'MCP createFeature error:', + JSON.stringify({ error: getErrorMessage(error) }, null, 2), + ) + throw ensureError(error) + } } } From dfea0ee2a544ad267817243b3dc2c702c377ab51 Mon Sep 17 00:00:00 2001 From: Jonathan Norris <jonathan@taplytics.com> Date: Mon, 7 Jul 2025 21:15:10 -0400 Subject: [PATCH 12/44] feat: add zod types to tool calls --- src/mcp/server.ts | 85 ++++++++++++++++++++++++++++---------------- src/mcp/types.ts | 45 +++++++++++++++++++++++ src/mcp/utils/api.ts | 60 +++++++++++++------------------ 3 files changed, 123 insertions(+), 67 deletions(-) create mode 100644 src/mcp/types.ts diff --git a/src/mcp/server.ts b/src/mcp/server.ts index eefb975a2..45b539685 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -6,6 +6,20 @@ import { import { DevCycleAuth } from './utils/auth' import { DevCycleApiClient } from './utils/api' import Writer from '../ui/writer' +import { + ListFeaturesArgsSchema, + ListVariablesArgsSchema, + GetSdkKeysArgsSchema, + EnableTargetingArgsSchema, + DisableTargetingArgsSchema, + CreateFeatureArgsSchema, + type ListFeaturesArgs, + type ListVariablesArgs, + type GetSdkKeysArgs, + type EnableTargetingArgs, + type DisableTargetingArgs, + type CreateFeatureArgs, +} from './types' export class DevCycleMCPServer { private auth: DevCycleAuth @@ -190,30 +204,48 @@ export class DevCycleMCPServer { this.server.setRequestHandler( CallToolRequestSchema, - async (request: any) => { + async (request) => { const { name, arguments: args } = request.params try { let result switch (name) { - case 'list_features': - result = await this.listFeatures(args) + case 'list_features': { + const validatedArgs = + ListFeaturesArgsSchema.parse(args) + result = await this.listFeatures(validatedArgs) break - case 'list_variables': - result = await this.listVariables(args) + } + case 'list_variables': { + const validatedArgs = + ListVariablesArgsSchema.parse(args) + result = await this.listVariables(validatedArgs) break - case 'get_sdk_keys': - result = await this.getSdkKeys(args) + } + case 'get_sdk_keys': { + const validatedArgs = + GetSdkKeysArgsSchema.parse(args) + result = await this.getSdkKeys(validatedArgs) break - case 'enable_targeting': - result = await this.enableTargeting(args) + } + case 'enable_targeting': { + const validatedArgs = + EnableTargetingArgsSchema.parse(args) + result = await this.enableTargeting(validatedArgs) break - case 'disable_targeting': - result = await this.disableTargeting(args) + } + case 'disable_targeting': { + const validatedArgs = + DisableTargetingArgsSchema.parse(args) + result = await this.disableTargeting(validatedArgs) break - case 'create_feature': - result = await this.createFeature(args) + } + case 'create_feature': { + const validatedArgs = + CreateFeatureArgsSchema.parse(args) + result = await this.createFeature(validatedArgs) break + } default: throw new Error(`Unknown tool: ${name}`) } @@ -244,7 +276,7 @@ export class DevCycleMCPServer { } // Tool implementations - private async listFeatures(args: any) { + private async listFeatures(args: ListFeaturesArgs) { try { const features = await this.apiClient.listFeatures(args) return { @@ -261,7 +293,7 @@ export class DevCycleMCPServer { } } - private async listVariables(args: any) { + private async listVariables(args: ListVariablesArgs) { const variables = await this.apiClient.listVariables(args) return { content: [ @@ -273,11 +305,8 @@ export class DevCycleMCPServer { } } - private async getSdkKeys(args: any) { - const keys = await this.apiClient.getSdkKeys( - args.environment_key, - args.key_type, - ) + private async getSdkKeys(args: GetSdkKeysArgs) { + const keys = await this.apiClient.getSdkKeys(args) return { content: [ { @@ -288,11 +317,8 @@ export class DevCycleMCPServer { } } - private async enableTargeting(args: any) { - const result = await this.apiClient.enableTargeting( - args.feature_key, - args.environment_key, - ) + private async enableTargeting(args: EnableTargetingArgs) { + const result = await this.apiClient.enableTargeting(args) return { content: [ { @@ -303,11 +329,8 @@ export class DevCycleMCPServer { } } - private async disableTargeting(args: any) { - const result = await this.apiClient.disableTargeting( - args.feature_key, - args.environment_key, - ) + private async disableTargeting(args: DisableTargetingArgs) { + const result = await this.apiClient.disableTargeting(args) return { content: [ { @@ -318,7 +341,7 @@ export class DevCycleMCPServer { } } - private async createFeature(args: any) { + private async createFeature(args: CreateFeatureArgs) { const result = await this.apiClient.createFeature(args) return { content: [ diff --git a/src/mcp/types.ts b/src/mcp/types.ts new file mode 100644 index 000000000..3f395ae5e --- /dev/null +++ b/src/mcp/types.ts @@ -0,0 +1,45 @@ +import { z } from 'zod' + +// Zod schemas for MCP tool arguments +export const ListFeaturesArgsSchema = z.object({ + search: z.string().optional(), + page: z.number().optional(), + per_page: z.number().optional(), +}) + +export const ListVariablesArgsSchema = z.object({ + search: z.string().optional(), + page: z.number().optional(), + per_page: z.number().optional(), +}) + +export const GetSdkKeysArgsSchema = z.object({ + environment_key: z.string(), + key_type: z.enum(['mobile', 'server', 'client']).optional(), +}) + +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 = z.object({ + key: z.string().optional(), + name: z.string().optional(), + description: z.string().optional(), + type: z.enum(['release', 'experiment', 'permission', 'ops']).optional(), + interactive: z.boolean().optional(), +}) + +// Type inference helpers +export type ListFeaturesArgs = z.infer<typeof ListFeaturesArgsSchema> +export type ListVariablesArgs = z.infer<typeof ListVariablesArgsSchema> +export type GetSdkKeysArgs = z.infer<typeof GetSdkKeysArgsSchema> +export type EnableTargetingArgs = z.infer<typeof EnableTargetingArgsSchema> +export type DisableTargetingArgs = z.infer<typeof DisableTargetingArgsSchema> +export type CreateFeatureArgs = z.infer<typeof CreateFeatureArgsSchema> diff --git a/src/mcp/utils/api.ts b/src/mcp/utils/api.ts index 5bf42e1cc..4f792a799 100644 --- a/src/mcp/utils/api.ts +++ b/src/mcp/utils/api.ts @@ -3,6 +3,14 @@ import { fetchFeatures, createFeature } from '../../api/features' import { fetchVariables } from '../../api/variables' import { fetchEnvironmentByKey } from '../../api/environments' import { enableTargeting, disableTargeting } from '../../api/targeting' +import type { + ListFeaturesArgs, + ListVariablesArgs, + GetSdkKeysArgs, + EnableTargetingArgs, + DisableTargetingArgs, + CreateFeatureArgs, +} from '../types' function getErrorMessage(error: unknown): string { if (error instanceof Error) { @@ -21,11 +29,7 @@ function ensureError(error: unknown): Error { export class DevCycleApiClient { constructor(private auth: DevCycleAuth) {} - async listFeatures(args: { - search?: string - page?: number - per_page?: number - }) { + async listFeatures(args: ListFeaturesArgs) { console.error('MCP listFeatures params:', JSON.stringify(args, null, 2)) try { @@ -57,11 +61,7 @@ export class DevCycleApiClient { } } - async listVariables(args: { - search?: string - page?: number - per_page?: number - }) { + async listVariables(args: ListVariablesArgs) { console.error( 'MCP listVariables params:', JSON.stringify(args, null, 2), @@ -96,14 +96,8 @@ export class DevCycleApiClient { } } - async getSdkKeys( - environmentKey: string, - keyType?: 'mobile' | 'server' | 'client', - ) { - console.error( - 'MCP getSdkKeys params:', - JSON.stringify({ environmentKey, keyType }, null, 2), - ) + async getSdkKeys(args: GetSdkKeysArgs) { + console.error('MCP getSdkKeys params:', JSON.stringify(args, null, 2)) try { this.auth.requireAuth() @@ -112,15 +106,15 @@ export class DevCycleApiClient { const environment = await fetchEnvironmentByKey( this.auth.getAuthToken(), this.auth.getProjectKey(), - environmentKey, + args.environment_key, ) const sdkKeys = environment.sdkKeys let result - if (keyType) { + if (args.key_type) { result = { - [keyType]: sdkKeys[keyType], + [args.key_type]: sdkKeys[args.key_type], } } else { result = { @@ -144,10 +138,10 @@ export class DevCycleApiClient { } } - async enableTargeting(featureKey: string, environmentKey: string) { + async enableTargeting(args: EnableTargetingArgs) { console.error( 'MCP enableTargeting params:', - JSON.stringify({ featureKey, environmentKey }, null, 2), + JSON.stringify(args, null, 2), ) try { @@ -157,8 +151,8 @@ export class DevCycleApiClient { const result = await enableTargeting( this.auth.getAuthToken(), this.auth.getProjectKey(), - featureKey, - environmentKey, + args.feature_key, + args.environment_key, ) console.error( 'MCP enableTargeting result:', @@ -174,10 +168,10 @@ export class DevCycleApiClient { } } - async disableTargeting(featureKey: string, environmentKey: string) { + async disableTargeting(args: DisableTargetingArgs) { console.error( 'MCP disableTargeting params:', - JSON.stringify({ featureKey, environmentKey }, null, 2), + JSON.stringify(args, null, 2), ) try { @@ -187,8 +181,8 @@ export class DevCycleApiClient { const result = await disableTargeting( this.auth.getAuthToken(), this.auth.getProjectKey(), - featureKey, - environmentKey, + args.feature_key, + args.environment_key, ) console.error( 'MCP disableTargeting result:', @@ -204,13 +198,7 @@ export class DevCycleApiClient { } } - async createFeature(args: { - key?: string - name?: string - description?: string - type?: 'release' | 'experiment' | 'permission' | 'ops' - interactive?: boolean - }) { + async createFeature(args: CreateFeatureArgs) { console.error( 'MCP createFeature params:', JSON.stringify(args, null, 2), From 19d4cdd0fd9c4c2e4550dbd85504fdb7707ca69e Mon Sep 17 00:00:00 2001 From: Jonathan Norris <jonathan@taplytics.com> Date: Mon, 7 Jul 2025 21:42:46 -0400 Subject: [PATCH 13/44] chore: add list_environments tool call --- src/mcp/server.ts | 53 ++++++++++++++++++++++++++++++++++++++++++++ src/mcp/utils/api.ts | 31 +++++++++++++++++++++++++- 2 files changed, 83 insertions(+), 1 deletion(-) diff --git a/src/mcp/server.ts b/src/mcp/server.ts index 45b539685..ab0b30829 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -103,6 +103,43 @@ export class DevCycleMCPServer { }, }, }, + { + name: 'list_environments', + description: 'List environments in the current project', + inputSchema: { + type: 'object', + properties: { + search: { + type: 'string', + description: + 'Search query to filter environments', + }, + page: { + type: 'number', + description: 'Page number (default: 1)', + }, + per_page: { + type: 'number', + description: + 'Number of items per page (default: 100)', + }, + sort_by: { + type: 'string', + description: + 'Field to sort by (default: createdAt)', + }, + sort_order: { + type: 'string', + enum: ['asc', 'desc'], + description: 'Sort order (default: desc)', + }, + created_by: { + type: 'string', + description: 'Filter by creator user ID', + }, + }, + }, + }, { name: 'get_sdk_keys', description: 'Get SDK keys for an environment', @@ -222,6 +259,10 @@ export class DevCycleMCPServer { result = await this.listVariables(validatedArgs) break } + case 'list_environments': { + result = await this.listEnvironments() + break + } case 'get_sdk_keys': { const validatedArgs = GetSdkKeysArgsSchema.parse(args) @@ -305,6 +346,18 @@ export class DevCycleMCPServer { } } + private async listEnvironments() { + const environments = await this.apiClient.listEnvironments() + return { + content: [ + { + type: 'text', + text: JSON.stringify(environments, null, 2), + }, + ], + } + } + private async getSdkKeys(args: GetSdkKeysArgs) { const keys = await this.apiClient.getSdkKeys(args) return { diff --git a/src/mcp/utils/api.ts b/src/mcp/utils/api.ts index 4f792a799..f9dbd83fe 100644 --- a/src/mcp/utils/api.ts +++ b/src/mcp/utils/api.ts @@ -1,7 +1,10 @@ import { DevCycleAuth } from './auth' import { fetchFeatures, createFeature } from '../../api/features' import { fetchVariables } from '../../api/variables' -import { fetchEnvironmentByKey } from '../../api/environments' +import { + fetchEnvironmentByKey, + fetchEnvironments, +} from '../../api/environments' import { enableTargeting, disableTargeting } from '../../api/targeting' import type { ListFeaturesArgs, @@ -96,6 +99,32 @@ export class DevCycleApiClient { } } + async listEnvironments() { + console.error('MCP listEnvironments') + + try { + this.auth.requireAuth() + this.auth.requireProject() + + const authToken = this.auth.getAuthToken() + const projectKey = this.auth.getProjectKey() + + const result = await fetchEnvironments(authToken, projectKey) + + console.error( + 'MCP listEnvironments result:', + JSON.stringify(result, null, 2), + ) + return result + } catch (error) { + console.error( + 'MCP listEnvironments error:', + JSON.stringify({ error: getErrorMessage(error) }, null, 2), + ) + throw ensureError(error) + } + } + async getSdkKeys(args: GetSdkKeysArgs) { console.error('MCP getSdkKeys params:', JSON.stringify(args, null, 2)) From 7b89969c1d765047b8597e840b093f4fa3818677 Mon Sep 17 00:00:00 2001 From: Jonathan Norris <jonathan@taplytics.com> Date: Mon, 7 Jul 2025 21:48:09 -0400 Subject: [PATCH 14/44] chore: cleanup server.ts --- src/mcp/server.ts | 129 ++++++++++++++++------------------------------ 1 file changed, 45 insertions(+), 84 deletions(-) diff --git a/src/mcp/server.ts b/src/mcp/server.ts index ab0b30829..c75fbe5fb 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -250,41 +250,71 @@ export class DevCycleMCPServer { case 'list_features': { const validatedArgs = ListFeaturesArgsSchema.parse(args) - result = await this.listFeatures(validatedArgs) + result = await this.executeApiCall( + () => + this.apiClient.listFeatures(validatedArgs), + 'listing features', + ) break } case 'list_variables': { const validatedArgs = ListVariablesArgsSchema.parse(args) - result = await this.listVariables(validatedArgs) + result = await this.executeApiCall( + () => + this.apiClient.listVariables(validatedArgs), + 'listing variables', + ) break } case 'list_environments': { - result = await this.listEnvironments() + result = await this.executeApiCall( + () => this.apiClient.listEnvironments(), + 'listing environments', + ) break } case 'get_sdk_keys': { const validatedArgs = GetSdkKeysArgsSchema.parse(args) - result = await this.getSdkKeys(validatedArgs) + result = await this.executeApiCall( + () => this.apiClient.getSdkKeys(validatedArgs), + 'getting SDK keys', + ) break } case 'enable_targeting': { const validatedArgs = EnableTargetingArgsSchema.parse(args) - result = await this.enableTargeting(validatedArgs) + result = await this.executeApiCall( + () => + this.apiClient.enableTargeting( + validatedArgs, + ), + 'enabling targeting', + ) break } case 'disable_targeting': { const validatedArgs = DisableTargetingArgsSchema.parse(args) - result = await this.disableTargeting(validatedArgs) + result = await this.executeApiCall( + () => + this.apiClient.disableTargeting( + validatedArgs, + ), + 'disabling targeting', + ) break } case 'create_feature': { const validatedArgs = CreateFeatureArgsSchema.parse(args) - result = await this.createFeature(validatedArgs) + result = await this.executeApiCall( + () => + this.apiClient.createFeature(validatedArgs), + 'creating feature', + ) break } default: @@ -316,93 +346,24 @@ export class DevCycleMCPServer { } } - // Tool implementations - private async listFeatures(args: ListFeaturesArgs) { + // Generic helper method for tool responses + private async executeApiCall<T>( + apiCall: () => Promise<T>, + errorContext: string, + ) { try { - const features = await this.apiClient.listFeatures(args) + const result = await apiCall() return { content: [ { type: 'text', - text: JSON.stringify(features, null, 2), + text: JSON.stringify(result, null, 2), }, ], } } catch (error) { - console.error('Error listing features:', error) + console.error(`Error ${errorContext}:`, error) throw error } } - - private async listVariables(args: ListVariablesArgs) { - const variables = await this.apiClient.listVariables(args) - return { - content: [ - { - type: 'text', - text: JSON.stringify(variables, null, 2), - }, - ], - } - } - - private async listEnvironments() { - const environments = await this.apiClient.listEnvironments() - return { - content: [ - { - type: 'text', - text: JSON.stringify(environments, null, 2), - }, - ], - } - } - - private async getSdkKeys(args: GetSdkKeysArgs) { - const keys = await this.apiClient.getSdkKeys(args) - return { - content: [ - { - type: 'text', - text: JSON.stringify(keys, null, 2), - }, - ], - } - } - - private async enableTargeting(args: EnableTargetingArgs) { - const result = await this.apiClient.enableTargeting(args) - return { - content: [ - { - type: 'text', - text: JSON.stringify(result, null, 2), - }, - ], - } - } - - private async disableTargeting(args: DisableTargetingArgs) { - const result = await this.apiClient.disableTargeting(args) - return { - content: [ - { - type: 'text', - text: JSON.stringify(result, null, 2), - }, - ], - } - } - - private async createFeature(args: CreateFeatureArgs) { - const result = await this.apiClient.createFeature(args) - return { - content: [ - { - type: 'text', - text: JSON.stringify(result, null, 2), - }, - ], - } - } } From 4d8e2f5243c3028812dd800c2725f7955aafb096 Mon Sep 17 00:00:00 2001 From: Jonathan Norris <jonathan@taplytics.com> Date: Mon, 7 Jul 2025 21:49:21 -0400 Subject: [PATCH 15/44] chore: cleanup list environments --- src/mcp/utils/api.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/mcp/utils/api.ts b/src/mcp/utils/api.ts index f9dbd83fe..ace44ef10 100644 --- a/src/mcp/utils/api.ts +++ b/src/mcp/utils/api.ts @@ -106,10 +106,10 @@ export class DevCycleApiClient { this.auth.requireAuth() this.auth.requireProject() - const authToken = this.auth.getAuthToken() - const projectKey = this.auth.getProjectKey() - - const result = await fetchEnvironments(authToken, projectKey) + const result = await fetchEnvironments( + this.auth.getAuthToken(), + this.auth.getProjectKey(), + ) console.error( 'MCP listEnvironments result:', From fe4c6eefbc329525158eb0bb8cea2867eaa8b4d5 Mon Sep 17 00:00:00 2001 From: Jonathan Norris <jonathan@taplytics.com> Date: Mon, 7 Jul 2025 21:59:21 -0400 Subject: [PATCH 16/44] feat: implement get_current_project tool call --- src/mcp/server.ts | 15 +++++++++++++++ src/mcp/utils/api.ts | 27 +++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/src/mcp/server.ts b/src/mcp/server.ts index c75fbe5fb..0d0403142 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -235,6 +235,14 @@ export class DevCycleMCPServer { }, }, }, + { + name: 'get_current_project', + description: 'Get the currently selected project', + inputSchema: { + type: 'object', + properties: {}, + }, + }, ], } }) @@ -317,6 +325,13 @@ export class DevCycleMCPServer { ) break } + case 'get_current_project': { + result = await this.executeApiCall( + () => this.apiClient.getCurrentProject(), + 'getting current project', + ) + break + } default: throw new Error(`Unknown tool: ${name}`) } diff --git a/src/mcp/utils/api.ts b/src/mcp/utils/api.ts index ace44ef10..e186cbe90 100644 --- a/src/mcp/utils/api.ts +++ b/src/mcp/utils/api.ts @@ -5,6 +5,7 @@ import { fetchEnvironmentByKey, fetchEnvironments, } from '../../api/environments' +import { fetchProject } from '../../api/projects' import { enableTargeting, disableTargeting } from '../../api/targeting' import type { ListFeaturesArgs, @@ -276,4 +277,30 @@ export class DevCycleApiClient { throw ensureError(error) } } + + async getCurrentProject() { + console.error('MCP getCurrentProject') + + try { + this.auth.requireAuth() + this.auth.requireProject() + + const result = await fetchProject( + this.auth.getAuthToken(), + this.auth.getProjectKey(), + ) + + console.error( + 'MCP getCurrentProject result:', + JSON.stringify(result, null, 2), + ) + return result + } catch (error) { + console.error( + 'MCP getCurrentProject error:', + JSON.stringify({ error: getErrorMessage(error) }, null, 2), + ) + throw ensureError(error) + } + } } From fc91da8b4154429760189e556d908a3b4085d13b Mon Sep 17 00:00:00 2001 From: Jonathan Norris <jonathan@taplytics.com> Date: Tue, 8 Jul 2025 11:46:28 -0400 Subject: [PATCH 17/44] feat: add list_projects tool call --- docs/mcp/MCP_ABSTRACTIONS_SUMMARY.md | 28 ++++++++++++- src/api/projects.ts | 18 +++++++- src/mcp/server.ts | 61 +++++++++++++++++++++++++++- src/mcp/types.ts | 27 ++++++++++-- src/mcp/utils/api.ts | 32 ++++++++++++++- 5 files changed, 155 insertions(+), 11 deletions(-) diff --git a/docs/mcp/MCP_ABSTRACTIONS_SUMMARY.md b/docs/mcp/MCP_ABSTRACTIONS_SUMMARY.md index fc800938d..9574a32d3 100644 --- a/docs/mcp/MCP_ABSTRACTIONS_SUMMARY.md +++ b/docs/mcp/MCP_ABSTRACTIONS_SUMMARY.md @@ -5,6 +5,7 @@ This document provides a quick reference of all recommended MCP abstractions for ## 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 @@ -12,6 +13,7 @@ This document provides a quick reference of all recommended MCP abstractions for - `delete_feature` - Delete a feature ### 2. Variable Management + - `list_variables` - List all variables - `get_variable` - Get variable details - `create_variable` - Create new variable @@ -19,12 +21,14 @@ This document provides a quick reference of all recommended MCP abstractions for - `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 @@ -34,6 +38,7 @@ This document provides a quick reference of all recommended MCP abstractions for - `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 @@ -42,6 +47,7 @@ This document provides a quick reference of all recommended MCP abstractions for - `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 @@ -50,6 +56,7 @@ This document provides a quick reference of all recommended MCP abstractions for - `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 @@ -57,34 +64,41 @@ This document provides a quick reference of all recommended MCP abstractions for - `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 @@ -92,48 +106,58 @@ This document provides a quick reference of all recommended MCP abstractions for - `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 +### 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 @@ -143,4 +167,4 @@ This document provides a quick reference of all recommended MCP abstractions for 3. **Targeting & Overrides** - Essential for testing 4. **Resources** - Read-only data access 5. **Prompts** - AI-guided workflows -6. **Advanced Analytics** - Usage metrics and insights \ No newline at end of file +6. **Advanced Analytics** - Usage metrics and insights diff --git a/src/api/projects.ts b/src/api/projects.ts index 74a7b5c68..2569312c3 100644 --- a/src/api/projects.ts +++ b/src/api/projects.ts @@ -19,10 +19,24 @@ export class CreateProjectParams { export class GetProjectsParams { @IsString() @IsOptional() - sortBy: string + sortBy?: string @IsOptional() - sortOrder: 'asc' | 'desc' + sortOrder?: 'asc' | 'desc' + + @IsString() + @IsOptional() + search?: string + + @IsString() + @IsOptional() + createdBy?: string + + @IsOptional() + page?: number + + @IsOptional() + perPage?: number } const BASE_URL = '/v1/projects' diff --git a/src/mcp/server.ts b/src/mcp/server.ts index 0d0403142..511013a0a 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -19,6 +19,7 @@ import { type EnableTargetingArgs, type DisableTargetingArgs, type CreateFeatureArgs, + ListProjectsArgsSchema, } from './types' export class DevCycleMCPServer { @@ -75,7 +76,7 @@ export class DevCycleMCPServer { per_page: { type: 'number', description: - 'Number of items per page (default: 10)', + 'Number of items per page (default: 100, max: 1000)', }, }, }, @@ -98,7 +99,7 @@ export class DevCycleMCPServer { per_page: { type: 'number', description: - 'Number of items per page (default: 10)', + 'Number of items per page (default: 100, max: 1000)', }, }, }, @@ -140,6 +141,52 @@ export class DevCycleMCPServer { }, }, }, + { + name: 'list_projects', + description: + 'List all projects in the current organization', + inputSchema: { + type: 'object', + properties: { + sort_by: { + type: 'string', + enum: [ + 'createdAt', + 'updatedAt', + 'name', + 'key', + 'createdBy', + 'propertyKey', + ], + description: + 'Field to sort by (default: createdAt)', + }, + sort_order: { + type: 'string', + enum: ['asc', 'desc'], + description: 'Sort order (default: desc)', + }, + search: { + type: 'string', + description: + 'Search query to filter projects (minimum 3 characters)', + }, + created_by: { + type: 'string', + description: 'Filter by creator user ID', + }, + page: { + type: 'number', + description: 'Page number (default: 1)', + }, + per_page: { + type: 'number', + description: + 'Number of items per page (default: 100, max: 1000)', + }, + }, + }, + }, { name: 'get_sdk_keys', description: 'Get SDK keys for an environment', @@ -282,6 +329,16 @@ export class DevCycleMCPServer { ) break } + case 'list_projects': { + const validatedArgs = + ListProjectsArgsSchema.parse(args) + result = await this.executeApiCall( + () => + this.apiClient.listProjects(validatedArgs), + 'listing projects', + ) + break + } case 'get_sdk_keys': { const validatedArgs = GetSdkKeysArgsSchema.parse(args) diff --git a/src/mcp/types.ts b/src/mcp/types.ts index 3f395ae5e..a5f80fea0 100644 --- a/src/mcp/types.ts +++ b/src/mcp/types.ts @@ -3,14 +3,32 @@ import { z } from 'zod' // Zod schemas for MCP tool arguments export const ListFeaturesArgsSchema = z.object({ search: z.string().optional(), - page: z.number().optional(), - per_page: z.number().optional(), + page: z.number().min(1).optional(), + per_page: z.number().min(1).max(1000).default(100).optional(), }) export const ListVariablesArgsSchema = z.object({ search: z.string().optional(), - page: z.number().optional(), - per_page: z.number().optional(), + page: z.number().min(1).optional(), + per_page: z.number().min(1).max(1000).default(100).optional(), +}) + +export const ListProjectsArgsSchema = z.object({ + sort_by: z + .enum([ + 'createdAt', + 'updatedAt', + 'name', + 'key', + 'createdBy', + 'propertyKey', + ]) + .optional(), + sort_order: z.enum(['asc', 'desc']).optional(), + search: z.string().min(3).optional(), + created_by: z.string().optional(), + page: z.number().min(1).optional(), + per_page: z.number().min(1).max(1000).default(100).optional(), }) export const GetSdkKeysArgsSchema = z.object({ @@ -39,6 +57,7 @@ export const CreateFeatureArgsSchema = z.object({ // Type inference helpers export type ListFeaturesArgs = z.infer<typeof ListFeaturesArgsSchema> export type ListVariablesArgs = z.infer<typeof ListVariablesArgsSchema> +export type ListProjectsArgs = z.infer<typeof ListProjectsArgsSchema> export type GetSdkKeysArgs = z.infer<typeof GetSdkKeysArgsSchema> export type EnableTargetingArgs = z.infer<typeof EnableTargetingArgsSchema> export type DisableTargetingArgs = z.infer<typeof DisableTargetingArgsSchema> diff --git a/src/mcp/utils/api.ts b/src/mcp/utils/api.ts index e186cbe90..f0187f1ac 100644 --- a/src/mcp/utils/api.ts +++ b/src/mcp/utils/api.ts @@ -5,11 +5,12 @@ import { fetchEnvironmentByKey, fetchEnvironments, } from '../../api/environments' -import { fetchProject } from '../../api/projects' +import { fetchProject, fetchProjects } from '../../api/projects' import { enableTargeting, disableTargeting } from '../../api/targeting' import type { ListFeaturesArgs, ListVariablesArgs, + ListProjectsArgs, GetSdkKeysArgs, EnableTargetingArgs, DisableTargetingArgs, @@ -126,6 +127,35 @@ export class DevCycleApiClient { } } + async listProjects(args: ListProjectsArgs) { + console.error('MCP listProjects params:', JSON.stringify(args, null, 2)) + + try { + this.auth.requireAuth() + + const query: any = {} + if (args.sort_by) query.sortBy = args.sort_by + if (args.sort_order) query.sortOrder = args.sort_order + if (args.search) query.search = args.search + if (args.created_by) query.createdBy = args.created_by + if (args.page) query.page = args.page + if (args.per_page) query.perPage = args.per_page + + const result = await fetchProjects(this.auth.getAuthToken(), query) + console.error( + 'MCP listProjects result:', + JSON.stringify(result, null, 2), + ) + return result + } catch (error) { + console.error( + 'MCP listProjects error:', + JSON.stringify({ error: getErrorMessage(error) }, null, 2), + ) + throw ensureError(error) + } + } + async getSdkKeys(args: GetSdkKeysArgs) { console.error('MCP getSdkKeys params:', JSON.stringify(args, null, 2)) From 4e8fc73f4913be0e259c352ecc4f4f87dae59998 Mon Sep 17 00:00:00 2001 From: Jonathan Norris <jonathan@taplytics.com> Date: Tue, 8 Jul 2025 11:57:16 -0400 Subject: [PATCH 18/44] feat: add get_self_targeting_identity and update_self_targeting_identity tool calls --- src/mcp/server.ts | 48 ++++++++++++++++++++++++++++++++++++ src/mcp/types.ts | 7 ++++++ src/mcp/utils/api.ts | 58 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 113 insertions(+) diff --git a/src/mcp/server.ts b/src/mcp/server.ts index 511013a0a..6f0c9913f 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -13,12 +13,14 @@ import { EnableTargetingArgsSchema, DisableTargetingArgsSchema, CreateFeatureArgsSchema, + UpdateSelfTargetingIdentityArgsSchema, type ListFeaturesArgs, type ListVariablesArgs, type GetSdkKeysArgs, type EnableTargetingArgs, type DisableTargetingArgs, type CreateFeatureArgs, + type UpdateSelfTargetingIdentityArgs, ListProjectsArgsSchema, } from './types' @@ -290,6 +292,31 @@ export class DevCycleMCPServer { properties: {}, }, }, + { + name: 'get_self_targeting_identity', + description: + 'Get current DevCycle identity for self-targeting', + inputSchema: { + type: 'object', + properties: {}, + }, + }, + { + name: 'update_self_targeting_identity', + description: + 'Update DevCycle identity for self-targeting and overrides', + inputSchema: { + type: 'object', + properties: { + dvc_user_id: { + type: 'string', + description: + 'DevCycle User ID for self-targeting (use null or empty string to clear)', + }, + }, + required: ['dvc_user_id'], + }, + }, ], } }) @@ -389,6 +416,27 @@ export class DevCycleMCPServer { ) break } + case 'get_self_targeting_identity': { + result = await this.executeApiCall( + () => this.apiClient.getSelfTargetingIdentity(), + 'getting self-targeting identity', + ) + break + } + case 'update_self_targeting_identity': { + const validatedArgs = + UpdateSelfTargetingIdentityArgsSchema.parse( + args, + ) + result = await this.executeApiCall( + () => + this.apiClient.updateSelfTargetingIdentity( + validatedArgs, + ), + 'updating self-targeting identity', + ) + break + } default: throw new Error(`Unknown tool: ${name}`) } diff --git a/src/mcp/types.ts b/src/mcp/types.ts index a5f80fea0..d6dcb82a1 100644 --- a/src/mcp/types.ts +++ b/src/mcp/types.ts @@ -54,6 +54,10 @@ export const CreateFeatureArgsSchema = z.object({ interactive: z.boolean().optional(), }) +export const UpdateSelfTargetingIdentityArgsSchema = z.object({ + dvc_user_id: z.string().nullable(), +}) + // Type inference helpers export type ListFeaturesArgs = z.infer<typeof ListFeaturesArgsSchema> export type ListVariablesArgs = z.infer<typeof ListVariablesArgsSchema> @@ -62,3 +66,6 @@ export type GetSdkKeysArgs = z.infer<typeof GetSdkKeysArgsSchema> export type EnableTargetingArgs = z.infer<typeof EnableTargetingArgsSchema> export type DisableTargetingArgs = z.infer<typeof DisableTargetingArgsSchema> export type CreateFeatureArgs = z.infer<typeof CreateFeatureArgsSchema> +export type UpdateSelfTargetingIdentityArgs = z.infer< + typeof UpdateSelfTargetingIdentityArgsSchema +> diff --git a/src/mcp/utils/api.ts b/src/mcp/utils/api.ts index f0187f1ac..9f341fa81 100644 --- a/src/mcp/utils/api.ts +++ b/src/mcp/utils/api.ts @@ -7,6 +7,7 @@ import { } from '../../api/environments' import { fetchProject, fetchProjects } from '../../api/projects' import { enableTargeting, disableTargeting } from '../../api/targeting' +import { fetchUserProfile, updateUserProfile } from '../../api/userProfile' import type { ListFeaturesArgs, ListVariablesArgs, @@ -15,6 +16,7 @@ import type { EnableTargetingArgs, DisableTargetingArgs, CreateFeatureArgs, + UpdateSelfTargetingIdentityArgs, } from '../types' function getErrorMessage(error: unknown): string { @@ -333,4 +335,60 @@ export class DevCycleApiClient { throw ensureError(error) } } + + async getSelfTargetingIdentity() { + console.error('MCP getSelfTargetingIdentity') + + try { + this.auth.requireAuth() + this.auth.requireProject() + + const result = await fetchUserProfile( + this.auth.getAuthToken(), + this.auth.getProjectKey(), + ) + + console.error( + 'MCP getSelfTargetingIdentity result:', + JSON.stringify(result, null, 2), + ) + return result + } catch (error) { + console.error( + 'MCP getSelfTargetingIdentity error:', + JSON.stringify({ error: getErrorMessage(error) }, null, 2), + ) + throw ensureError(error) + } + } + + async updateSelfTargetingIdentity(args: UpdateSelfTargetingIdentityArgs) { + console.error( + 'MCP updateSelfTargetingIdentity params:', + JSON.stringify(args, null, 2), + ) + + try { + this.auth.requireAuth() + this.auth.requireProject() + + const result = await updateUserProfile( + this.auth.getAuthToken(), + this.auth.getProjectKey(), + { dvcUserId: args.dvc_user_id }, + ) + + console.error( + 'MCP updateSelfTargetingIdentity result:', + JSON.stringify(result, null, 2), + ) + return result + } catch (error) { + console.error( + 'MCP updateSelfTargetingIdentity error:', + JSON.stringify({ error: getErrorMessage(error) }, null, 2), + ) + throw ensureError(error) + } + } } From fb2383eba00251eba550d2652205e96b3c9e5a49 Mon Sep 17 00:00:00 2001 From: Jonathan Norris <jonathan@taplytics.com> Date: Tue, 8 Jul 2025 13:28:10 -0400 Subject: [PATCH 19/44] feat: add self_targeting identity and override tools --- src/mcp/server.ts | 95 ++++++++++++++++++++++++++++++++++ src/mcp/types.ts | 17 +++++++ src/mcp/utils/api.ts | 118 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 230 insertions(+) diff --git a/src/mcp/server.ts b/src/mcp/server.ts index 6f0c9913f..e388f9f26 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -14,6 +14,8 @@ import { DisableTargetingArgsSchema, CreateFeatureArgsSchema, UpdateSelfTargetingIdentityArgsSchema, + SetSelfTargetingOverrideArgsSchema, + ClearSelfTargetingOverridesArgsSchema, type ListFeaturesArgs, type ListVariablesArgs, type GetSdkKeysArgs, @@ -21,6 +23,8 @@ import { type DisableTargetingArgs, type CreateFeatureArgs, type UpdateSelfTargetingIdentityArgs, + type SetSelfTargetingOverrideArgs, + type ClearSelfTargetingOverridesArgs, ListProjectsArgsSchema, } from './types' @@ -317,6 +321,63 @@ export class DevCycleMCPServer { required: ['dvc_user_id'], }, }, + { + name: 'list_self_targeting_overrides', + description: + 'List all self-targeting overrides for the current project', + inputSchema: { + type: 'object', + properties: {}, + }, + }, + { + name: 'set_self_targeting_override', + description: + 'Set a self-targeting override for a feature variation', + inputSchema: { + type: 'object', + properties: { + feature_key: { + type: 'string', + description: 'The key of the feature', + }, + environment_key: { + type: 'string', + description: 'The key of the environment', + }, + variation_key: { + type: 'string', + description: + 'The key of the variation to serve', + }, + }, + required: [ + 'feature_key', + 'environment_key', + 'variation_key', + ], + }, + }, + { + name: 'clear_self_targeting_overrides', + description: + 'Clear self-targeting overrides (all or specific feature/environment)', + inputSchema: { + type: 'object', + properties: { + feature_key: { + type: 'string', + description: + 'The key of the feature (optional - if provided with environment_key, clears specific override)', + }, + environment_key: { + type: 'string', + description: + 'The key of the environment (optional - if provided with feature_key, clears specific override)', + }, + }, + }, + }, ], } }) @@ -437,6 +498,40 @@ export class DevCycleMCPServer { ) break } + case 'list_self_targeting_overrides': { + result = await this.executeApiCall( + () => + this.apiClient.listSelfTargetingOverrides(), + 'listing self-targeting overrides', + ) + break + } + case 'set_self_targeting_override': { + const validatedArgs = + SetSelfTargetingOverrideArgsSchema.parse(args) + result = await this.executeApiCall( + () => + this.apiClient.setSelfTargetingOverride( + validatedArgs, + ), + 'setting self-targeting override', + ) + break + } + case 'clear_self_targeting_overrides': { + const validatedArgs = + ClearSelfTargetingOverridesArgsSchema.parse( + args, + ) + result = await this.executeApiCall( + () => + this.apiClient.clearSelfTargetingOverrides( + validatedArgs, + ), + 'clearing self-targeting overrides', + ) + break + } default: throw new Error(`Unknown tool: ${name}`) } diff --git a/src/mcp/types.ts b/src/mcp/types.ts index d6dcb82a1..02195fb95 100644 --- a/src/mcp/types.ts +++ b/src/mcp/types.ts @@ -58,6 +58,17 @@ 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().optional(), + environment_key: z.string().optional(), +}) + // Type inference helpers export type ListFeaturesArgs = z.infer<typeof ListFeaturesArgsSchema> export type ListVariablesArgs = z.infer<typeof ListVariablesArgsSchema> @@ -69,3 +80,9 @@ export type CreateFeatureArgs = z.infer<typeof CreateFeatureArgsSchema> export type UpdateSelfTargetingIdentityArgs = z.infer< typeof UpdateSelfTargetingIdentityArgsSchema > +export type SetSelfTargetingOverrideArgs = z.infer< + typeof SetSelfTargetingOverrideArgsSchema +> +export type ClearSelfTargetingOverridesArgs = z.infer< + typeof ClearSelfTargetingOverridesArgsSchema +> diff --git a/src/mcp/utils/api.ts b/src/mcp/utils/api.ts index 9f341fa81..63ea49e02 100644 --- a/src/mcp/utils/api.ts +++ b/src/mcp/utils/api.ts @@ -8,6 +8,12 @@ import { import { fetchProject, fetchProjects } from '../../api/projects' import { enableTargeting, disableTargeting } from '../../api/targeting' import { fetchUserProfile, updateUserProfile } from '../../api/userProfile' +import { + fetchProjectOverridesForUser, + updateOverride, + deleteAllProjectOverrides, + deleteFeatureOverrides, +} from '../../api/overrides' import type { ListFeaturesArgs, ListVariablesArgs, @@ -17,6 +23,8 @@ import type { DisableTargetingArgs, CreateFeatureArgs, UpdateSelfTargetingIdentityArgs, + SetSelfTargetingOverrideArgs, + ClearSelfTargetingOverridesArgs, } from '../types' function getErrorMessage(error: unknown): string { @@ -391,4 +399,114 @@ export class DevCycleApiClient { throw ensureError(error) } } + + async listSelfTargetingOverrides() { + console.error('MCP listSelfTargetingOverrides') + + try { + this.auth.requireAuth() + this.auth.requireProject() + + const result = await fetchProjectOverridesForUser( + this.auth.getAuthToken(), + this.auth.getProjectKey(), + ) + + console.error( + 'MCP listSelfTargetingOverrides result:', + JSON.stringify(result, null, 2), + ) + return result + } catch (error) { + console.error( + 'MCP listSelfTargetingOverrides error:', + JSON.stringify({ error: getErrorMessage(error) }, null, 2), + ) + throw ensureError(error) + } + } + + async setSelfTargetingOverride(args: SetSelfTargetingOverrideArgs) { + console.error( + 'MCP setSelfTargetingOverride params:', + JSON.stringify(args, null, 2), + ) + + try { + this.auth.requireAuth() + this.auth.requireProject() + + const result = await updateOverride( + this.auth.getAuthToken(), + this.auth.getProjectKey(), + args.feature_key, + { + environment: args.environment_key, + variation: args.variation_key, + }, + ) + + console.error( + 'MCP setSelfTargetingOverride result:', + JSON.stringify(result, null, 2), + ) + return result + } catch (error) { + console.error( + 'MCP setSelfTargetingOverride error:', + JSON.stringify({ error: getErrorMessage(error) }, null, 2), + ) + throw ensureError(error) + } + } + + async clearSelfTargetingOverrides(args: ClearSelfTargetingOverridesArgs) { + console.error( + 'MCP clearSelfTargetingOverrides params:', + JSON.stringify(args, null, 2), + ) + + try { + this.auth.requireAuth() + this.auth.requireProject() + + if (args.feature_key && args.environment_key) { + // Clear specific feature/environment override + const result = await deleteFeatureOverrides( + this.auth.getAuthToken(), + this.auth.getProjectKey(), + args.feature_key, + args.environment_key, + ) + + console.error( + 'MCP clearSelfTargetingOverrides (specific) result:', + JSON.stringify(result, null, 2), + ) + return { + message: `Cleared override for feature '${args.feature_key}' in environment '${args.environment_key}'`, + } + } else { + // Clear all project overrides + const result = await deleteAllProjectOverrides( + this.auth.getAuthToken(), + this.auth.getProjectKey(), + ) + + console.error( + 'MCP clearSelfTargetingOverrides (all) result:', + JSON.stringify(result, null, 2), + ) + return { + message: 'Cleared all overrides for the project', + } + } + } catch (error) { + console.error( + 'MCP clearSelfTargetingOverrides error:', + JSON.stringify({ error: getErrorMessage(error) }, null, 2), + ) + throw ensureError(error) + } + } } From 4409a3ad82e6ab5b454647fc7b01c4768e4da764 Mon Sep 17 00:00:00 2001 From: Jonathan Norris <jonathan@taplytics.com> Date: Tue, 8 Jul 2025 13:56:57 -0400 Subject: [PATCH 20/44] chore: cleanup the tools interfaces, move to /tools folder --- src/mcp/server.ts | 567 +++------------------------- src/mcp/tools/environmentTools.ts | 70 ++++ src/mcp/tools/featureTools.ts | 120 ++++++ src/mcp/tools/projectTools.ts | 78 ++++ src/mcp/tools/selfTargetingTools.ts | 134 +++++++ src/mcp/tools/variableTools.ts | 36 ++ 6 files changed, 497 insertions(+), 508 deletions(-) create mode 100644 src/mcp/tools/environmentTools.ts create mode 100644 src/mcp/tools/featureTools.ts create mode 100644 src/mcp/tools/projectTools.ts create mode 100644 src/mcp/tools/selfTargetingTools.ts create mode 100644 src/mcp/tools/variableTools.ts diff --git a/src/mcp/server.ts b/src/mcp/server.ts index e388f9f26..dda1a3685 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -2,31 +2,55 @@ 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 { - ListFeaturesArgsSchema, - ListVariablesArgsSchema, - GetSdkKeysArgsSchema, - EnableTargetingArgsSchema, - DisableTargetingArgsSchema, - CreateFeatureArgsSchema, - UpdateSelfTargetingIdentityArgsSchema, - SetSelfTargetingOverrideArgsSchema, - ClearSelfTargetingOverridesArgsSchema, - type ListFeaturesArgs, - type ListVariablesArgs, - type GetSdkKeysArgs, - type EnableTargetingArgs, - type DisableTargetingArgs, - type CreateFeatureArgs, - type UpdateSelfTargetingIdentityArgs, - type SetSelfTargetingOverrideArgs, - type ClearSelfTargetingOverridesArgs, - ListProjectsArgsSchema, -} from './types' + 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' + +// Tool handler function type +export type ToolHandler = ( + args: unknown, + apiClient: DevCycleApiClient, +) => Promise<any> + +// Combine all tool definitions +const allToolDefinitions: Tool[] = [ + ...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 @@ -61,326 +85,9 @@ export class DevCycleMCPServer { } private setupToolHandlers() { - this.server.setRequestHandler(ListToolsRequestSchema, async () => { - return { - tools: [ - { - name: 'list_features', - description: 'List features in the current project', - inputSchema: { - type: 'object', - properties: { - search: { - type: 'string', - description: - 'Search query to filter features', - }, - page: { - type: 'number', - description: 'Page number (default: 1)', - }, - per_page: { - type: 'number', - description: - 'Number of items per page (default: 100, max: 1000)', - }, - }, - }, - }, - { - name: 'list_variables', - description: 'List variables in the current project', - inputSchema: { - type: 'object', - properties: { - search: { - type: 'string', - description: - 'Search query to filter variables', - }, - page: { - type: 'number', - description: 'Page number (default: 1)', - }, - per_page: { - type: 'number', - description: - 'Number of items per page (default: 100, max: 1000)', - }, - }, - }, - }, - { - name: 'list_environments', - description: 'List environments in the current project', - inputSchema: { - type: 'object', - properties: { - search: { - type: 'string', - description: - 'Search query to filter environments', - }, - page: { - type: 'number', - description: 'Page number (default: 1)', - }, - per_page: { - type: 'number', - description: - 'Number of items per page (default: 100)', - }, - sort_by: { - type: 'string', - description: - 'Field to sort by (default: createdAt)', - }, - sort_order: { - type: 'string', - enum: ['asc', 'desc'], - description: 'Sort order (default: desc)', - }, - created_by: { - type: 'string', - description: 'Filter by creator user ID', - }, - }, - }, - }, - { - name: 'list_projects', - description: - 'List all projects in the current organization', - inputSchema: { - type: 'object', - properties: { - sort_by: { - type: 'string', - enum: [ - 'createdAt', - 'updatedAt', - 'name', - 'key', - 'createdBy', - 'propertyKey', - ], - description: - 'Field to sort by (default: createdAt)', - }, - sort_order: { - type: 'string', - enum: ['asc', 'desc'], - description: 'Sort order (default: desc)', - }, - search: { - type: 'string', - description: - 'Search query to filter projects (minimum 3 characters)', - }, - created_by: { - type: 'string', - description: 'Filter by creator user ID', - }, - page: { - type: 'number', - description: 'Page number (default: 1)', - }, - per_page: { - type: 'number', - description: - 'Number of items per page (default: 100, max: 1000)', - }, - }, - }, - }, - { - name: 'get_sdk_keys', - description: 'Get SDK keys for an environment', - inputSchema: { - type: 'object', - properties: { - environment_key: { - type: 'string', - description: 'The key of the environment', - }, - key_type: { - type: 'string', - enum: ['mobile', 'server', 'client'], - description: - 'The type of SDK key to retrieve', - }, - }, - required: ['environment_key'], - }, - }, - { - name: 'enable_targeting', - description: - 'Enable targeting for a feature in an environment', - inputSchema: { - type: 'object', - properties: { - feature_key: { - type: 'string', - description: 'The key of the feature', - }, - environment_key: { - type: 'string', - description: 'The key of the environment', - }, - }, - required: ['feature_key', 'environment_key'], - }, - }, - { - name: 'disable_targeting', - description: - 'Disable targeting for a feature in an environment', - inputSchema: { - type: 'object', - properties: { - feature_key: { - type: 'string', - description: 'The key of the feature', - }, - environment_key: { - type: 'string', - description: 'The key of the environment', - }, - }, - required: ['feature_key', 'environment_key'], - }, - }, - { - name: 'create_feature', - description: - 'Create a new feature flag (supports interactive mode)', - inputSchema: { - type: 'object', - properties: { - key: { - type: 'string', - description: 'Unique feature key', - }, - name: { - type: 'string', - description: 'Human-readable feature name', - }, - description: { - type: 'string', - description: 'Feature description', - }, - type: { - type: 'string', - enum: [ - 'release', - 'experiment', - 'permission', - 'ops', - ], - description: 'Feature type', - }, - interactive: { - type: 'boolean', - description: - 'Use interactive mode to prompt for missing fields', - }, - }, - }, - }, - { - name: 'get_current_project', - description: 'Get the currently selected project', - inputSchema: { - type: 'object', - properties: {}, - }, - }, - { - name: 'get_self_targeting_identity', - description: - 'Get current DevCycle identity for self-targeting', - inputSchema: { - type: 'object', - properties: {}, - }, - }, - { - name: 'update_self_targeting_identity', - description: - 'Update DevCycle identity for self-targeting and overrides', - inputSchema: { - type: 'object', - properties: { - dvc_user_id: { - type: 'string', - description: - 'DevCycle User ID for self-targeting (use null or empty string to clear)', - }, - }, - required: ['dvc_user_id'], - }, - }, - { - name: 'list_self_targeting_overrides', - description: - 'List all self-targeting overrides for the current project', - inputSchema: { - type: 'object', - properties: {}, - }, - }, - { - name: 'set_self_targeting_override', - description: - 'Set a self-targeting override for a feature variation', - inputSchema: { - type: 'object', - properties: { - feature_key: { - type: 'string', - description: 'The key of the feature', - }, - environment_key: { - type: 'string', - description: 'The key of the environment', - }, - variation_key: { - type: 'string', - description: - 'The key of the variation to serve', - }, - }, - required: [ - 'feature_key', - 'environment_key', - 'variation_key', - ], - }, - }, - { - name: 'clear_self_targeting_overrides', - description: - 'Clear self-targeting overrides (all or specific feature/environment)', - inputSchema: { - type: 'object', - properties: { - feature_key: { - type: 'string', - description: - 'The key of the feature (optional - if provided with environment_key, clears specific override)', - }, - environment_key: { - type: 'string', - description: - 'The key of the environment (optional - if provided with feature_key, clears specific override)', - }, - }, - }, - }, - ], - } - }) + this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ + tools: allToolDefinitions, + })) this.server.setRequestHandler( CallToolRequestSchema, @@ -388,155 +95,20 @@ export class DevCycleMCPServer { const { name, arguments: args } = request.params try { - let result - switch (name) { - case 'list_features': { - const validatedArgs = - ListFeaturesArgsSchema.parse(args) - result = await this.executeApiCall( - () => - this.apiClient.listFeatures(validatedArgs), - 'listing features', - ) - break - } - case 'list_variables': { - const validatedArgs = - ListVariablesArgsSchema.parse(args) - result = await this.executeApiCall( - () => - this.apiClient.listVariables(validatedArgs), - 'listing variables', - ) - break - } - case 'list_environments': { - result = await this.executeApiCall( - () => this.apiClient.listEnvironments(), - 'listing environments', - ) - break - } - case 'list_projects': { - const validatedArgs = - ListProjectsArgsSchema.parse(args) - result = await this.executeApiCall( - () => - this.apiClient.listProjects(validatedArgs), - 'listing projects', - ) - break - } - case 'get_sdk_keys': { - const validatedArgs = - GetSdkKeysArgsSchema.parse(args) - result = await this.executeApiCall( - () => this.apiClient.getSdkKeys(validatedArgs), - 'getting SDK keys', - ) - break - } - case 'enable_targeting': { - const validatedArgs = - EnableTargetingArgsSchema.parse(args) - result = await this.executeApiCall( - () => - this.apiClient.enableTargeting( - validatedArgs, - ), - 'enabling targeting', - ) - break - } - case 'disable_targeting': { - const validatedArgs = - DisableTargetingArgsSchema.parse(args) - result = await this.executeApiCall( - () => - this.apiClient.disableTargeting( - validatedArgs, - ), - 'disabling targeting', - ) - break - } - case 'create_feature': { - const validatedArgs = - CreateFeatureArgsSchema.parse(args) - result = await this.executeApiCall( - () => - this.apiClient.createFeature(validatedArgs), - 'creating feature', - ) - break - } - case 'get_current_project': { - result = await this.executeApiCall( - () => this.apiClient.getCurrentProject(), - 'getting current project', - ) - break - } - case 'get_self_targeting_identity': { - result = await this.executeApiCall( - () => this.apiClient.getSelfTargetingIdentity(), - 'getting self-targeting identity', - ) - break - } - case 'update_self_targeting_identity': { - const validatedArgs = - UpdateSelfTargetingIdentityArgsSchema.parse( - args, - ) - result = await this.executeApiCall( - () => - this.apiClient.updateSelfTargetingIdentity( - validatedArgs, - ), - 'updating self-targeting identity', - ) - break - } - case 'list_self_targeting_overrides': { - result = await this.executeApiCall( - () => - this.apiClient.listSelfTargetingOverrides(), - 'listing self-targeting overrides', - ) - break - } - case 'set_self_targeting_override': { - const validatedArgs = - SetSelfTargetingOverrideArgsSchema.parse(args) - result = await this.executeApiCall( - () => - this.apiClient.setSelfTargetingOverride( - validatedArgs, - ), - 'setting self-targeting override', - ) - break - } - case 'clear_self_targeting_overrides': { - const validatedArgs = - ClearSelfTargetingOverridesArgsSchema.parse( - args, - ) - result = await this.executeApiCall( - () => - this.apiClient.clearSelfTargetingOverrides( - validatedArgs, - ), - 'clearing self-targeting overrides', - ) - break - } - default: - throw new Error(`Unknown tool: ${name}`) + const handler = allToolHandlers[name] + if (!handler) { + throw new Error(`Unknown tool: ${name}`) } - return result + const result = await handler(args, this.apiClient) + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + } } catch (error) { console.error(`Error in tool handler ${name}:`, error) @@ -560,25 +132,4 @@ export class DevCycleMCPServer { console.error('MCP Server Error:', error) } } - - // Generic helper method for tool responses - private async executeApiCall<T>( - apiCall: () => Promise<T>, - errorContext: string, - ) { - try { - const result = await apiCall() - return { - content: [ - { - type: 'text', - text: JSON.stringify(result, null, 2), - }, - ], - } - } catch (error) { - console.error(`Error ${errorContext}:`, error) - throw error - } - } } diff --git a/src/mcp/tools/environmentTools.ts b/src/mcp/tools/environmentTools.ts new file mode 100644 index 000000000..95a78d838 --- /dev/null +++ b/src/mcp/tools/environmentTools.ts @@ -0,0 +1,70 @@ +import { Tool } from '@modelcontextprotocol/sdk/types.js' +import { DevCycleApiClient } from '../utils/api' +import { GetSdkKeysArgsSchema } from '../types' +import { ToolHandler } from '../server' + +export const environmentToolDefinitions: Tool[] = [ + { + name: 'list_environments', + description: 'List environments in the current project', + inputSchema: { + type: 'object', + properties: { + search: { + type: 'string', + description: 'Search query to filter environments', + }, + page: { + type: 'number', + description: 'Page number (default: 1)', + }, + per_page: { + type: 'number', + description: 'Number of items per page (default: 100)', + }, + sort_by: { + type: 'string', + description: 'Field to sort by (default: createdAt)', + }, + sort_order: { + type: 'string', + enum: ['asc', 'desc'], + description: 'Sort order (default: desc)', + }, + created_by: { + type: 'string', + description: 'Filter by creator user ID', + }, + }, + }, + }, + { + name: 'get_sdk_keys', + description: 'Get SDK keys for an environment', + inputSchema: { + type: 'object', + properties: { + environment_key: { + type: 'string', + description: 'The key of the environment', + }, + key_type: { + type: 'string', + enum: ['mobile', 'server', 'client'], + description: 'The type of SDK key to retrieve', + }, + }, + required: ['environment_key'], + }, + }, +] + +export const environmentToolHandlers: Record<string, ToolHandler> = { + list_environments: async (args: unknown, apiClient: DevCycleApiClient) => { + return await apiClient.listEnvironments() + }, + get_sdk_keys: async (args: unknown, apiClient: DevCycleApiClient) => { + const validatedArgs = GetSdkKeysArgsSchema.parse(args) + return await apiClient.getSdkKeys(validatedArgs) + }, +} diff --git a/src/mcp/tools/featureTools.ts b/src/mcp/tools/featureTools.ts new file mode 100644 index 000000000..04379db9c --- /dev/null +++ b/src/mcp/tools/featureTools.ts @@ -0,0 +1,120 @@ +import { Tool } from '@modelcontextprotocol/sdk/types.js' +import { DevCycleApiClient } from '../utils/api' +import { + ListFeaturesArgsSchema, + CreateFeatureArgsSchema, + EnableTargetingArgsSchema, + DisableTargetingArgsSchema, +} from '../types' +import { ToolHandler } from '../server' + +export const featureToolDefinitions: Tool[] = [ + { + name: 'list_features', + description: 'List features in the current project', + inputSchema: { + type: 'object', + properties: { + search: { + type: 'string', + description: 'Search query to filter features', + }, + page: { + type: 'number', + description: 'Page number (default: 1)', + }, + per_page: { + type: 'number', + description: + 'Number of items per page (default: 100, max: 1000)', + }, + }, + }, + }, + { + name: 'create_feature', + description: 'Create a new feature flag (supports interactive mode)', + inputSchema: { + type: 'object', + properties: { + key: { + type: 'string', + description: 'Unique feature key', + }, + name: { + type: 'string', + description: 'Human-readable feature name', + }, + description: { + type: 'string', + description: 'Feature description', + }, + type: { + type: 'string', + enum: ['release', 'experiment', 'permission', 'ops'], + description: 'Feature type', + }, + interactive: { + type: 'boolean', + description: + 'Use interactive mode to prompt for missing fields', + }, + }, + }, + }, + { + name: 'enable_targeting', + description: 'Enable targeting for a feature in an environment', + inputSchema: { + type: 'object', + properties: { + feature_key: { + type: 'string', + description: 'The key of the feature', + }, + environment_key: { + type: 'string', + description: 'The key of the environment', + }, + }, + required: ['feature_key', 'environment_key'], + }, + }, + { + name: 'disable_targeting', + description: 'Disable targeting for a feature in an environment', + inputSchema: { + type: 'object', + properties: { + feature_key: { + type: 'string', + description: 'The key of the feature', + }, + environment_key: { + type: 'string', + description: 'The key of the environment', + }, + }, + required: ['feature_key', 'environment_key'], + }, + }, +] + +export const featureToolHandlers: Record<string, ToolHandler> = { + list_features: async (args: unknown, apiClient: DevCycleApiClient) => { + const validatedArgs = ListFeaturesArgsSchema.parse(args) + return await apiClient.listFeatures(validatedArgs) + }, + create_feature: async (args: unknown, apiClient: DevCycleApiClient) => { + const validatedArgs = CreateFeatureArgsSchema.parse(args) + return await apiClient.createFeature(validatedArgs) + }, + enable_targeting: async (args: unknown, apiClient: DevCycleApiClient) => { + const validatedArgs = EnableTargetingArgsSchema.parse(args) + return await apiClient.enableTargeting(validatedArgs) + }, + disable_targeting: async (args: unknown, apiClient: DevCycleApiClient) => { + const validatedArgs = DisableTargetingArgsSchema.parse(args) + return await apiClient.disableTargeting(validatedArgs) + }, +} diff --git a/src/mcp/tools/projectTools.ts b/src/mcp/tools/projectTools.ts new file mode 100644 index 000000000..24990b95b --- /dev/null +++ b/src/mcp/tools/projectTools.ts @@ -0,0 +1,78 @@ +import { Tool } from '@modelcontextprotocol/sdk/types.js' +import { DevCycleApiClient } from '../utils/api' +import { ListProjectsArgsSchema } from '../types' +import { ToolHandler } from '../server' + +export const projectToolDefinitions: Tool[] = [ + { + name: 'list_projects', + description: 'List all projects in the current organization', + inputSchema: { + type: 'object', + properties: { + sort_by: { + type: 'string', + enum: [ + 'createdAt', + 'updatedAt', + 'name', + 'key', + 'createdBy', + 'propertyKey', + ], + description: 'Field to sort by (default: createdAt)', + }, + sort_order: { + type: 'string', + enum: ['asc', 'desc'], + description: 'Sort order (default: desc)', + }, + search: { + type: 'string', + description: + 'Search query to filter projects (minimum 3 characters)', + }, + created_by: { + type: 'string', + description: 'Filter by creator user ID', + }, + page: { + type: 'number', + description: 'Page number (default: 1)', + }, + per_page: { + type: 'number', + description: + 'Number of items per page (default: 100, max: 1000)', + }, + }, + }, + }, + { + name: 'get_current_project', + description: 'Get the currently selected project', + inputSchema: { + type: 'object', + properties: { + random_string: { + type: 'string', + description: 'Dummy parameter for no-parameter tools', + }, + }, + required: ['random_string'], + }, + }, +] + +export const projectToolHandlers: Record<string, ToolHandler> = { + list_projects: async (args: unknown, apiClient: DevCycleApiClient) => { + const validatedArgs = ListProjectsArgsSchema.parse(args) + return await apiClient.listProjects(validatedArgs) + }, + get_current_project: async ( + args: unknown, + apiClient: DevCycleApiClient, + ) => { + return await apiClient.getCurrentProject() + }, +} diff --git a/src/mcp/tools/selfTargetingTools.ts b/src/mcp/tools/selfTargetingTools.ts new file mode 100644 index 000000000..4aca30817 --- /dev/null +++ b/src/mcp/tools/selfTargetingTools.ts @@ -0,0 +1,134 @@ +import { Tool } from '@modelcontextprotocol/sdk/types.js' +import { DevCycleApiClient } from '../utils/api' +import { + UpdateSelfTargetingIdentityArgsSchema, + SetSelfTargetingOverrideArgsSchema, + ClearSelfTargetingOverridesArgsSchema, +} from '../types' +import { ToolHandler } from '../server' + +export const selfTargetingToolDefinitions: Tool[] = [ + { + name: 'get_self_targeting_identity', + description: 'Get current DevCycle identity for self-targeting', + inputSchema: { + type: 'object', + properties: { + random_string: { + type: 'string', + description: 'Dummy parameter for no-parameter tools', + }, + }, + required: ['random_string'], + }, + }, + { + name: 'update_self_targeting_identity', + description: + 'Update DevCycle identity for self-targeting and overrides', + inputSchema: { + type: 'object', + properties: { + dvc_user_id: { + type: 'string', + description: + 'DevCycle User ID for self-targeting (use null or empty string to clear)', + }, + }, + required: ['dvc_user_id'], + }, + }, + { + name: 'list_self_targeting_overrides', + description: + 'List all self-targeting overrides for the current project', + inputSchema: { + type: 'object', + properties: { + random_string: { + type: 'string', + description: 'Dummy parameter for no-parameter tools', + }, + }, + required: ['random_string'], + }, + }, + { + name: 'set_self_targeting_override', + description: 'Set a self-targeting override for a feature variation', + inputSchema: { + type: 'object', + properties: { + feature_key: { + type: 'string', + description: 'The key of the feature', + }, + environment_key: { + type: 'string', + description: 'The key of the environment', + }, + variation_key: { + type: 'string', + description: 'The key of the variation to serve', + }, + }, + required: ['feature_key', 'environment_key', 'variation_key'], + }, + }, + { + name: 'clear_self_targeting_overrides', + description: + 'Clear self-targeting overrides (all or specific feature/environment)', + inputSchema: { + type: 'object', + properties: { + feature_key: { + type: 'string', + description: + 'The key of the feature (optional - if provided with environment_key, clears specific override)', + }, + environment_key: { + type: 'string', + description: + 'The key of the environment (optional - if provided with feature_key, clears specific override)', + }, + }, + }, + }, +] + +export const selfTargetingToolHandlers: Record<string, ToolHandler> = { + get_self_targeting_identity: async ( + args: unknown, + apiClient: DevCycleApiClient, + ) => { + return await apiClient.getSelfTargetingIdentity() + }, + update_self_targeting_identity: async ( + args: unknown, + apiClient: DevCycleApiClient, + ) => { + const validatedArgs = UpdateSelfTargetingIdentityArgsSchema.parse(args) + return await apiClient.updateSelfTargetingIdentity(validatedArgs) + }, + list_self_targeting_overrides: async ( + args: unknown, + apiClient: DevCycleApiClient, + ) => { + return await apiClient.listSelfTargetingOverrides() + }, + set_self_targeting_override: async ( + args: unknown, + apiClient: DevCycleApiClient, + ) => { + const validatedArgs = SetSelfTargetingOverrideArgsSchema.parse(args) + return await apiClient.setSelfTargetingOverride(validatedArgs) + }, + clear_self_targeting_overrides: async ( + args: unknown, + apiClient: DevCycleApiClient, + ) => { + const validatedArgs = ClearSelfTargetingOverridesArgsSchema.parse(args) + return await apiClient.clearSelfTargetingOverrides(validatedArgs) + }, +} diff --git a/src/mcp/tools/variableTools.ts b/src/mcp/tools/variableTools.ts new file mode 100644 index 000000000..914363fa6 --- /dev/null +++ b/src/mcp/tools/variableTools.ts @@ -0,0 +1,36 @@ +import { Tool } from '@modelcontextprotocol/sdk/types.js' +import { DevCycleApiClient } from '../utils/api' +import { ListVariablesArgsSchema } from '../types' +import { ToolHandler } from '../server' + +export const variableToolDefinitions: Tool[] = [ + { + name: 'list_variables', + description: 'List variables in the current project', + inputSchema: { + type: 'object', + properties: { + search: { + type: 'string', + description: 'Search query to filter variables', + }, + page: { + type: 'number', + description: 'Page number (default: 1)', + }, + per_page: { + type: 'number', + description: + 'Number of items per page (default: 100, max: 1000)', + }, + }, + }, + }, +] + +export const variableToolHandlers: Record<string, ToolHandler> = { + list_variables: async (args: unknown, apiClient: DevCycleApiClient) => { + const validatedArgs = ListVariablesArgsSchema.parse(args) + return await apiClient.listVariables(validatedArgs) + }, +} From b1216a4f02fb44c19cd1b0b96012c097885836f2 Mon Sep 17 00:00:00 2001 From: Jonathan Norris <jonathan@taplytics.com> Date: Tue, 8 Jul 2025 14:16:40 -0400 Subject: [PATCH 21/44] feat: split clear_feature_self_targeting_overrides and clear_all_self_targeting_overrides --- src/mcp/tools/projectTools.ts | 8 +-- src/mcp/tools/selfTargetingTools.ts | 46 +++++++++-------- src/mcp/utils/api.ts | 79 ++++++++++++++++++----------- 3 files changed, 74 insertions(+), 59 deletions(-) diff --git a/src/mcp/tools/projectTools.ts b/src/mcp/tools/projectTools.ts index 24990b95b..68e1e7a7e 100644 --- a/src/mcp/tools/projectTools.ts +++ b/src/mcp/tools/projectTools.ts @@ -53,13 +53,7 @@ export const projectToolDefinitions: Tool[] = [ description: 'Get the currently selected project', inputSchema: { type: 'object', - properties: { - random_string: { - type: 'string', - description: 'Dummy parameter for no-parameter tools', - }, - }, - required: ['random_string'], + properties: {}, }, }, ] diff --git a/src/mcp/tools/selfTargetingTools.ts b/src/mcp/tools/selfTargetingTools.ts index 4aca30817..55db15656 100644 --- a/src/mcp/tools/selfTargetingTools.ts +++ b/src/mcp/tools/selfTargetingTools.ts @@ -13,13 +13,7 @@ export const selfTargetingToolDefinitions: Tool[] = [ description: 'Get current DevCycle identity for self-targeting', inputSchema: { type: 'object', - properties: { - random_string: { - type: 'string', - description: 'Dummy parameter for no-parameter tools', - }, - }, - required: ['random_string'], + properties: {}, }, }, { @@ -44,13 +38,7 @@ export const selfTargetingToolDefinitions: Tool[] = [ 'List all self-targeting overrides for the current project', inputSchema: { type: 'object', - properties: { - random_string: { - type: 'string', - description: 'Dummy parameter for no-parameter tools', - }, - }, - required: ['random_string'], + properties: {}, }, }, { @@ -76,23 +64,31 @@ export const selfTargetingToolDefinitions: Tool[] = [ }, }, { - name: 'clear_self_targeting_overrides', + name: 'clear_feature_self_targeting_overrides', description: - 'Clear self-targeting overrides (all or specific feature/environment)', + 'Clear self-targeting overrides for a specific feature/environment', inputSchema: { type: 'object', properties: { feature_key: { type: 'string', - description: - 'The key of the feature (optional - if provided with environment_key, clears specific override)', + description: 'The key of the feature', }, environment_key: { type: 'string', - description: - 'The key of the environment (optional - if provided with feature_key, clears specific override)', + description: 'The key of the environment', }, }, + required: ['feature_key', 'environment_key'], + }, + }, + { + name: 'clear_all_self_targeting_overrides', + description: + 'Clear all self-targeting overrides for the current project', + inputSchema: { + type: 'object', + properties: {}, }, }, ] @@ -124,11 +120,17 @@ export const selfTargetingToolHandlers: Record<string, ToolHandler> = { const validatedArgs = SetSelfTargetingOverrideArgsSchema.parse(args) return await apiClient.setSelfTargetingOverride(validatedArgs) }, - clear_self_targeting_overrides: async ( + clear_feature_self_targeting_overrides: async ( args: unknown, apiClient: DevCycleApiClient, ) => { const validatedArgs = ClearSelfTargetingOverridesArgsSchema.parse(args) - return await apiClient.clearSelfTargetingOverrides(validatedArgs) + return await apiClient.clearFeatureSelfTargetingOverrides(validatedArgs) + }, + clear_all_self_targeting_overrides: async ( + args: unknown, + apiClient: DevCycleApiClient, + ) => { + return await apiClient.clearAllSelfTargetingOverrides() }, } diff --git a/src/mcp/utils/api.ts b/src/mcp/utils/api.ts index 63ea49e02..f5b75eb80 100644 --- a/src/mcp/utils/api.ts +++ b/src/mcp/utils/api.ts @@ -460,9 +460,11 @@ export class DevCycleApiClient { } } - async clearSelfTargetingOverrides(args: ClearSelfTargetingOverridesArgs) { + async clearFeatureSelfTargetingOverrides( + args: ClearSelfTargetingOverridesArgs, + ) { console.error( - 'MCP clearSelfTargetingOverrides params:', + 'MCP clearFeatureSelfTargetingOverrides params:', JSON.stringify(args, null, 2), ) @@ -470,40 +472,57 @@ export class DevCycleApiClient { this.auth.requireAuth() this.auth.requireProject() - if (args.feature_key && args.environment_key) { - // Clear specific feature/environment override - const result = await deleteFeatureOverrides( - this.auth.getAuthToken(), - this.auth.getProjectKey(), - args.feature_key, - args.environment_key, + if (!args.feature_key || !args.environment_key) { + throw new Error( + 'Both feature_key and environment_key are required to clear a specific override', ) + } - console.error( - 'MCP clearSelfTargetingOverrides (specific) result:', - JSON.stringify(result, null, 2), - ) - return { - message: `Cleared override for feature '${args.feature_key}' in environment '${args.environment_key}'`, - } - } else { - // Clear all project overrides - const result = await deleteAllProjectOverrides( - this.auth.getAuthToken(), - this.auth.getProjectKey(), - ) + const result = await deleteFeatureOverrides( + this.auth.getAuthToken(), + this.auth.getProjectKey(), + args.feature_key, + args.environment_key, + ) - console.error( - 'MCP clearSelfTargetingOverrides (all) result:', - JSON.stringify(result, null, 2), - ) - return { - message: 'Cleared all overrides for the project', - } + console.error( + 'MCP clearFeatureSelfTargetingOverrides result:', + JSON.stringify(result, null, 2), + ) + return { + message: `Cleared override for feature '${args.feature_key}' in environment '${args.environment_key}'`, + } + } catch (error) { + console.error( + 'MCP clearFeatureSelfTargetingOverrides error:', + JSON.stringify({ error: getErrorMessage(error) }, null, 2), + ) + throw ensureError(error) + } + } + + async clearAllSelfTargetingOverrides() { + console.error('MCP clearAllSelfTargetingOverrides') + + try { + this.auth.requireAuth() + this.auth.requireProject() + + const result = await deleteAllProjectOverrides( + this.auth.getAuthToken(), + this.auth.getProjectKey(), + ) + + console.error( + 'MCP clearAllSelfTargetingOverrides result:', + JSON.stringify(result, null, 2), + ) + return { + message: 'Cleared all overrides for the project', } } catch (error) { console.error( - 'MCP clearSelfTargetingOverrides error:', + 'MCP clearAllSelfTargetingOverrides error:', JSON.stringify({ error: getErrorMessage(error) }, null, 2), ) throw ensureError(error) From d4a1656b37d7e66d575a8586e2e659e0b7b5878e Mon Sep 17 00:00:00 2001 From: Jonathan Norris <jonathan@taplytics.com> Date: Tue, 8 Jul 2025 14:43:22 -0400 Subject: [PATCH 22/44] feat: refactor api calls, move them into /tools files --- src/mcp/tools/environmentTools.ts | 64 +++- src/mcp/tools/featureTools.ts | 78 ++++- src/mcp/tools/projectTools.ts | 39 ++- src/mcp/tools/selfTargetingTools.ts | 105 +++++- src/mcp/tools/variableTools.ts | 16 +- src/mcp/types.ts | 19 ++ src/mcp/utils/api.ts | 500 +++------------------------- 7 files changed, 332 insertions(+), 489 deletions(-) diff --git a/src/mcp/tools/environmentTools.ts b/src/mcp/tools/environmentTools.ts index 95a78d838..4279d21e2 100644 --- a/src/mcp/tools/environmentTools.ts +++ b/src/mcp/tools/environmentTools.ts @@ -1,6 +1,10 @@ import { Tool } from '@modelcontextprotocol/sdk/types.js' -import { DevCycleApiClient } from '../utils/api' -import { GetSdkKeysArgsSchema } from '../types' +import { + DevCycleApiClient, + fetchEnvironments, + fetchEnvironmentByKey, +} from '../utils/api' +import { GetSdkKeysArgsSchema, ListEnvironmentsArgsSchema } from '../types' import { ToolHandler } from '../server' export const environmentToolDefinitions: Tool[] = [ @@ -12,19 +16,33 @@ export const environmentToolDefinitions: Tool[] = [ properties: { search: { type: 'string', - description: 'Search query to filter environments', + description: + 'Search query to filter environments (minimum 3 characters)', + minLength: 3, }, page: { type: 'number', description: 'Page number (default: 1)', + minimum: 1, }, per_page: { type: 'number', - description: 'Number of items per page (default: 100)', + description: + 'Number of items per page (default: 100, max: 1000)', + minimum: 1, + maximum: 1000, }, sort_by: { type: 'string', description: 'Field to sort by (default: createdAt)', + enum: [ + 'createdAt', + 'updatedAt', + 'name', + 'key', + 'createdBy', + 'propertyKey', + ], }, sort_order: { type: 'string', @@ -61,10 +79,44 @@ export const environmentToolDefinitions: Tool[] = [ export const environmentToolHandlers: Record<string, ToolHandler> = { list_environments: async (args: unknown, apiClient: DevCycleApiClient) => { - return await apiClient.listEnvironments() + const validatedArgs = ListEnvironmentsArgsSchema.parse(args) + + return await apiClient.executeWithLogging( + 'listEnvironments', + validatedArgs, + async (authToken, projectKey) => { + return await fetchEnvironments(authToken, projectKey) + }, + ) }, get_sdk_keys: async (args: unknown, apiClient: DevCycleApiClient) => { const validatedArgs = GetSdkKeysArgsSchema.parse(args) - return await apiClient.getSdkKeys(validatedArgs) + + return await apiClient.executeWithLogging( + 'getSdkKeys', + validatedArgs, + async (authToken, projectKey) => { + const environment = await fetchEnvironmentByKey( + authToken, + projectKey, + validatedArgs.environment_key, + ) + + const sdkKeys = environment.sdkKeys + + if (validatedArgs.key_type) { + return { + [validatedArgs.key_type]: + sdkKeys[validatedArgs.key_type], + } + } else { + return { + mobile: sdkKeys.mobile, + server: sdkKeys.server, + client: sdkKeys.client, + } + } + }, + ) }, } diff --git a/src/mcp/tools/featureTools.ts b/src/mcp/tools/featureTools.ts index 04379db9c..fc548dd80 100644 --- a/src/mcp/tools/featureTools.ts +++ b/src/mcp/tools/featureTools.ts @@ -1,5 +1,11 @@ import { Tool } from '@modelcontextprotocol/sdk/types.js' -import { DevCycleApiClient } from '../utils/api' +import { + DevCycleApiClient, + fetchFeatures, + createFeature, + enableTargeting, + disableTargeting, +} from '../utils/api' import { ListFeaturesArgsSchema, CreateFeatureArgsSchema, @@ -103,18 +109,80 @@ export const featureToolDefinitions: Tool[] = [ export const featureToolHandlers: Record<string, ToolHandler> = { list_features: async (args: unknown, apiClient: DevCycleApiClient) => { const validatedArgs = ListFeaturesArgsSchema.parse(args) - return await apiClient.listFeatures(validatedArgs) + + return await apiClient.executeWithLogging( + 'listFeatures', + validatedArgs, + async (authToken, projectKey) => { + const query = { + search: validatedArgs.search, + page: validatedArgs.page, + perPage: validatedArgs.per_page, + } + return await fetchFeatures(authToken, projectKey, query) + }, + ) }, create_feature: async (args: unknown, apiClient: DevCycleApiClient) => { const validatedArgs = CreateFeatureArgsSchema.parse(args) - return await apiClient.createFeature(validatedArgs) + + return await apiClient.executeWithLogging( + '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', + ) + } + + const featureData = { + key: validatedArgs.key, + name: validatedArgs.name, + description: validatedArgs.description || '', + type: validatedArgs.type || 'release', + } + + return await createFeature(authToken, projectKey, featureData) + }, + ) }, enable_targeting: async (args: unknown, apiClient: DevCycleApiClient) => { const validatedArgs = EnableTargetingArgsSchema.parse(args) - return await apiClient.enableTargeting(validatedArgs) + + return await apiClient.executeWithLogging( + 'enableTargeting', + validatedArgs, + async (authToken, projectKey) => { + return await enableTargeting( + authToken, + projectKey, + validatedArgs.feature_key, + validatedArgs.environment_key, + ) + }, + ) }, disable_targeting: async (args: unknown, apiClient: DevCycleApiClient) => { const validatedArgs = DisableTargetingArgsSchema.parse(args) - return await apiClient.disableTargeting(validatedArgs) + + return await apiClient.executeWithLogging( + 'disableTargeting', + validatedArgs, + async (authToken, projectKey) => { + return await disableTargeting( + authToken, + projectKey, + validatedArgs.feature_key, + validatedArgs.environment_key, + ) + }, + ) }, } diff --git a/src/mcp/tools/projectTools.ts b/src/mcp/tools/projectTools.ts index 68e1e7a7e..da48f8229 100644 --- a/src/mcp/tools/projectTools.ts +++ b/src/mcp/tools/projectTools.ts @@ -1,5 +1,5 @@ import { Tool } from '@modelcontextprotocol/sdk/types.js' -import { DevCycleApiClient } from '../utils/api' +import { DevCycleApiClient, fetchProjects, fetchProject } from '../utils/api' import { ListProjectsArgsSchema } from '../types' import { ToolHandler } from '../server' @@ -53,7 +53,13 @@ export const projectToolDefinitions: Tool[] = [ description: 'Get the currently selected project', inputSchema: { type: 'object', - properties: {}, + properties: { + random_string: { + type: 'string', + description: 'Dummy parameter for no-parameter tools', + }, + }, + required: ['random_string'], }, }, ] @@ -61,12 +67,37 @@ export const projectToolDefinitions: Tool[] = [ export const projectToolHandlers: Record<string, ToolHandler> = { list_projects: async (args: unknown, apiClient: DevCycleApiClient) => { const validatedArgs = ListProjectsArgsSchema.parse(args) - return await apiClient.listProjects(validatedArgs) + + return await apiClient.executeWithLogging( + 'listProjects', + validatedArgs, + async (authToken, projectKey) => { + const query: any = {} + if (validatedArgs.sort_by) query.sortBy = validatedArgs.sort_by + if (validatedArgs.sort_order) + query.sortOrder = validatedArgs.sort_order + if (validatedArgs.search) query.search = validatedArgs.search + if (validatedArgs.created_by) + query.createdBy = validatedArgs.created_by + if (validatedArgs.page) query.page = validatedArgs.page + if (validatedArgs.per_page) + query.perPage = validatedArgs.per_page + + return await fetchProjects(authToken, query) + }, + false, + ) }, get_current_project: async ( args: unknown, apiClient: DevCycleApiClient, ) => { - return await apiClient.getCurrentProject() + return await apiClient.executeWithLogging( + 'getCurrentProject', + args, + async (authToken, projectKey) => { + return await fetchProject(authToken, projectKey) + }, + ) }, } diff --git a/src/mcp/tools/selfTargetingTools.ts b/src/mcp/tools/selfTargetingTools.ts index 55db15656..e5b64e2ac 100644 --- a/src/mcp/tools/selfTargetingTools.ts +++ b/src/mcp/tools/selfTargetingTools.ts @@ -1,5 +1,13 @@ import { Tool } from '@modelcontextprotocol/sdk/types.js' -import { DevCycleApiClient } from '../utils/api' +import { + DevCycleApiClient, + fetchUserProfile, + updateUserProfile, + fetchProjectOverridesForUser, + updateOverride, + deleteFeatureOverrides, + deleteAllProjectOverrides, +} from '../utils/api' import { UpdateSelfTargetingIdentityArgsSchema, SetSelfTargetingOverrideArgsSchema, @@ -13,7 +21,13 @@ export const selfTargetingToolDefinitions: Tool[] = [ description: 'Get current DevCycle identity for self-targeting', inputSchema: { type: 'object', - properties: {}, + properties: { + random_string: { + type: 'string', + description: 'Dummy parameter for no-parameter tools', + }, + }, + required: ['random_string'], }, }, { @@ -38,7 +52,13 @@ export const selfTargetingToolDefinitions: Tool[] = [ 'List all self-targeting overrides for the current project', inputSchema: { type: 'object', - properties: {}, + properties: { + random_string: { + type: 'string', + description: 'Dummy parameter for no-parameter tools', + }, + }, + required: ['random_string'], }, }, { @@ -88,7 +108,13 @@ export const selfTargetingToolDefinitions: Tool[] = [ 'Clear all self-targeting overrides for the current project', inputSchema: { type: 'object', - properties: {}, + properties: { + random_string: { + type: 'string', + description: 'Dummy parameter for no-parameter tools', + }, + }, + required: ['random_string'], }, }, ] @@ -98,39 +124,98 @@ export const selfTargetingToolHandlers: Record<string, ToolHandler> = { args: unknown, apiClient: DevCycleApiClient, ) => { - return await apiClient.getSelfTargetingIdentity() + return await apiClient.executeWithLogging( + 'getSelfTargetingIdentity', + args, + async (authToken, projectKey) => { + return await fetchUserProfile(authToken, projectKey) + }, + ) }, update_self_targeting_identity: async ( args: unknown, apiClient: DevCycleApiClient, ) => { const validatedArgs = UpdateSelfTargetingIdentityArgsSchema.parse(args) - return await apiClient.updateSelfTargetingIdentity(validatedArgs) + + return await apiClient.executeWithLogging( + 'updateSelfTargetingIdentity', + validatedArgs, + async (authToken, projectKey) => { + return await updateUserProfile(authToken, projectKey, { + dvcUserId: validatedArgs.dvc_user_id, + }) + }, + ) }, list_self_targeting_overrides: async ( args: unknown, apiClient: DevCycleApiClient, ) => { - return await apiClient.listSelfTargetingOverrides() + return await apiClient.executeWithLogging( + 'listSelfTargetingOverrides', + args, + async (authToken, projectKey) => { + return await fetchProjectOverridesForUser(authToken, projectKey) + }, + ) }, set_self_targeting_override: async ( args: unknown, apiClient: DevCycleApiClient, ) => { const validatedArgs = SetSelfTargetingOverrideArgsSchema.parse(args) - return await apiClient.setSelfTargetingOverride(validatedArgs) + + return await apiClient.executeWithLogging( + 'setSelfTargetingOverride', + validatedArgs, + async (authToken, projectKey) => { + return await updateOverride( + authToken, + projectKey, + validatedArgs.feature_key, + { + environment: validatedArgs.environment_key, + variation: validatedArgs.variation_key, + }, + ) + }, + ) }, clear_feature_self_targeting_overrides: async ( args: unknown, apiClient: DevCycleApiClient, ) => { const validatedArgs = ClearSelfTargetingOverridesArgsSchema.parse(args) - return await apiClient.clearFeatureSelfTargetingOverrides(validatedArgs) + + return await apiClient.executeWithLogging( + 'clearFeatureSelfTargetingOverrides', + validatedArgs, + async (authToken, projectKey) => { + await deleteFeatureOverrides( + authToken, + projectKey, + validatedArgs.feature_key!, + validatedArgs.environment_key!, + ) + + return { + message: `Cleared override for feature '${validatedArgs.feature_key}' in environment '${validatedArgs.environment_key}'`, + } + }, + ) }, clear_all_self_targeting_overrides: async ( args: unknown, apiClient: DevCycleApiClient, ) => { - return await apiClient.clearAllSelfTargetingOverrides() + return await apiClient.executeWithLogging( + 'clearAllSelfTargetingOverrides', + args, + async (authToken, projectKey) => { + await deleteAllProjectOverrides(authToken, projectKey) + return { message: 'Cleared all overrides for the project' } + }, + ) }, } diff --git a/src/mcp/tools/variableTools.ts b/src/mcp/tools/variableTools.ts index 914363fa6..4fbd78be8 100644 --- a/src/mcp/tools/variableTools.ts +++ b/src/mcp/tools/variableTools.ts @@ -1,5 +1,5 @@ import { Tool } from '@modelcontextprotocol/sdk/types.js' -import { DevCycleApiClient } from '../utils/api' +import { DevCycleApiClient, fetchVariables } from '../utils/api' import { ListVariablesArgsSchema } from '../types' import { ToolHandler } from '../server' @@ -31,6 +31,18 @@ export const variableToolDefinitions: Tool[] = [ export const variableToolHandlers: Record<string, ToolHandler> = { list_variables: async (args: unknown, apiClient: DevCycleApiClient) => { const validatedArgs = ListVariablesArgsSchema.parse(args) - return await apiClient.listVariables(validatedArgs) + + return await apiClient.executeWithLogging( + 'listVariables', + validatedArgs, + async (authToken, projectKey) => { + const query = { + search: validatedArgs.search, + page: validatedArgs.page, + perPage: validatedArgs.per_page, + } + return await fetchVariables(authToken, projectKey, query) + }, + ) }, } diff --git a/src/mcp/types.ts b/src/mcp/types.ts index 02195fb95..0f6c4a175 100644 --- a/src/mcp/types.ts +++ b/src/mcp/types.ts @@ -31,6 +31,24 @@ export const ListProjectsArgsSchema = z.object({ per_page: z.number().min(1).max(1000).default(100).optional(), }) +export const ListEnvironmentsArgsSchema = z.object({ + search: z.string().min(3).optional(), + page: z.number().min(1).optional(), + per_page: z.number().min(1).max(1000).default(100).optional(), + sort_by: z + .enum([ + 'createdAt', + 'updatedAt', + 'name', + 'key', + 'createdBy', + 'propertyKey', + ]) + .optional(), + sort_order: z.enum(['asc', 'desc']).optional(), + created_by: z.string().optional(), +}) + export const GetSdkKeysArgsSchema = z.object({ environment_key: z.string(), key_type: z.enum(['mobile', 'server', 'client']).optional(), @@ -73,6 +91,7 @@ export const ClearSelfTargetingOverridesArgsSchema = z.object({ export type ListFeaturesArgs = z.infer<typeof ListFeaturesArgsSchema> export type ListVariablesArgs = z.infer<typeof ListVariablesArgsSchema> export type ListProjectsArgs = z.infer<typeof ListProjectsArgsSchema> +export type ListEnvironmentsArgs = z.infer<typeof ListEnvironmentsArgsSchema> export type GetSdkKeysArgs = z.infer<typeof GetSdkKeysArgsSchema> export type EnableTargetingArgs = z.infer<typeof EnableTargetingArgsSchema> export type DisableTargetingArgs = z.infer<typeof DisableTargetingArgsSchema> diff --git a/src/mcp/utils/api.ts b/src/mcp/utils/api.ts index f5b75eb80..f9d60a6d4 100644 --- a/src/mcp/utils/api.ts +++ b/src/mcp/utils/api.ts @@ -44,488 +44,64 @@ function ensureError(error: unknown): Error { export class DevCycleApiClient { constructor(private auth: DevCycleAuth) {} - async listFeatures(args: ListFeaturesArgs) { - console.error('MCP listFeatures params:', JSON.stringify(args, null, 2)) - - try { - this.auth.requireAuth() - this.auth.requireProject() - - const authToken = this.auth.getAuthToken() - const projectKey = this.auth.getProjectKey() - - const query = { - search: args.search, - page: args.page, - perPage: args.per_page, - } - - const result = await fetchFeatures(authToken, projectKey, query) - console.error( - 'MCP listFeatures result:', - JSON.stringify(result, null, 2), - ) - return result - } catch (error) { - console.error( - 'MCP listFeatures error:', - JSON.stringify({ error: getErrorMessage(error) }, null, 2), - ) - console.error('Error during listFeatures:', getErrorMessage(error)) - throw ensureError(error) - } - } - - async listVariables(args: ListVariablesArgs) { - console.error( - 'MCP listVariables params:', - JSON.stringify(args, null, 2), - ) - - try { - this.auth.requireAuth() - this.auth.requireProject() - - const query = { - search: args.search, - page: args.page, - perPage: args.per_page, - } - - const result = await fetchVariables( - this.auth.getAuthToken(), - this.auth.getProjectKey(), - query, - ) - console.error( - 'MCP listVariables result:', - JSON.stringify(result, null, 2), - ) - return result - } catch (error) { - console.error( - 'MCP listVariables error:', - JSON.stringify({ error: getErrorMessage(error) }, null, 2), - ) - throw ensureError(error) - } - } - - async listEnvironments() { - console.error('MCP listEnvironments') - - try { - this.auth.requireAuth() - this.auth.requireProject() - - const result = await fetchEnvironments( - this.auth.getAuthToken(), - this.auth.getProjectKey(), - ) - - console.error( - 'MCP listEnvironments result:', - JSON.stringify(result, null, 2), - ) - return result - } catch (error) { - console.error( - 'MCP listEnvironments error:', - JSON.stringify({ error: getErrorMessage(error) }, null, 2), - ) - throw ensureError(error) - } - } - - async listProjects(args: ListProjectsArgs) { - console.error('MCP listProjects params:', JSON.stringify(args, null, 2)) - - try { - this.auth.requireAuth() - - const query: any = {} - if (args.sort_by) query.sortBy = args.sort_by - if (args.sort_order) query.sortOrder = args.sort_order - if (args.search) query.search = args.search - if (args.created_by) query.createdBy = args.created_by - if (args.page) query.page = args.page - if (args.per_page) query.perPage = args.per_page - - const result = await fetchProjects(this.auth.getAuthToken(), query) - console.error( - 'MCP listProjects result:', - JSON.stringify(result, null, 2), - ) - return result - } catch (error) { - console.error( - 'MCP listProjects error:', - JSON.stringify({ error: getErrorMessage(error) }, null, 2), - ) - throw ensureError(error) - } - } - - async getSdkKeys(args: GetSdkKeysArgs) { - console.error('MCP getSdkKeys params:', JSON.stringify(args, null, 2)) - - try { - this.auth.requireAuth() - this.auth.requireProject() - - const environment = await fetchEnvironmentByKey( - this.auth.getAuthToken(), - this.auth.getProjectKey(), - args.environment_key, - ) - - const sdkKeys = environment.sdkKeys - - let result - if (args.key_type) { - result = { - [args.key_type]: sdkKeys[args.key_type], - } - } else { - result = { - mobile: sdkKeys.mobile, - server: sdkKeys.server, - client: sdkKeys.client, - } - } - - console.error( - 'MCP getSdkKeys result:', - JSON.stringify(result, null, 2), - ) - return result - } catch (error) { - console.error( - 'MCP getSdkKeys error:', - JSON.stringify({ error: getErrorMessage(error) }, null, 2), - ) - throw ensureError(error) - } - } - - async enableTargeting(args: EnableTargetingArgs) { - console.error( - 'MCP enableTargeting params:', - JSON.stringify(args, null, 2), - ) - - try { - this.auth.requireAuth() - this.auth.requireProject() - - const result = await enableTargeting( - this.auth.getAuthToken(), - this.auth.getProjectKey(), - args.feature_key, - args.environment_key, - ) - console.error( - 'MCP enableTargeting result:', - JSON.stringify(result, null, 2), - ) - return result - } catch (error) { - console.error( - 'MCP enableTargeting error:', - JSON.stringify({ error: getErrorMessage(error) }, null, 2), - ) - throw ensureError(error) - } - } - - async disableTargeting(args: DisableTargetingArgs) { + /** + * 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 disableTargeting params:', - JSON.stringify(args, null, 2), + `MCP ${operationName} args:`, + args ? JSON.stringify(args, null, 2) : 'none', ) try { this.auth.requireAuth() - this.auth.requireProject() - - const result = await disableTargeting( - this.auth.getAuthToken(), - this.auth.getProjectKey(), - args.feature_key, - args.environment_key, - ) - console.error( - 'MCP disableTargeting result:', - JSON.stringify(result, null, 2), - ) - return result - } catch (error) { - console.error( - 'MCP disableTargeting error:', - JSON.stringify({ error: getErrorMessage(error) }, null, 2), - ) - throw ensureError(error) - } - } - - async createFeature(args: CreateFeatureArgs) { - console.error( - 'MCP createFeature params:', - JSON.stringify(args, null, 2), - ) - - try { - this.auth.requireAuth() - this.auth.requireProject() - - if (args.interactive) { - // For interactive mode, we would need to implement prompts - // For now, return an error asking for explicit parameters - throw new Error( - 'Interactive mode not yet supported in MCP. Please provide explicit parameters: key, name, description, type', - ) - } - - if (!args.key || !args.name) { - throw new Error( - 'Feature key and name are required when not using interactive mode', - ) - } - - const featureData = { - key: args.key, - name: args.name, - description: args.description || '', - type: args.type || 'release', + if (requiresProject) { + this.auth.requireProject() } - const result = await createFeature( - this.auth.getAuthToken(), - this.auth.getProjectKey(), - featureData, - ) - console.error( - 'MCP createFeature result:', - JSON.stringify(result, null, 2), - ) - return result - } catch (error) { - console.error( - 'MCP createFeature error:', - JSON.stringify({ error: getErrorMessage(error) }, null, 2), - ) - throw ensureError(error) - } - } - - async getCurrentProject() { - console.error('MCP getCurrentProject') - - try { - this.auth.requireAuth() - this.auth.requireProject() - - const result = await fetchProject( - this.auth.getAuthToken(), - this.auth.getProjectKey(), - ) - - console.error( - 'MCP getCurrentProject result:', - JSON.stringify(result, null, 2), - ) - return result - } catch (error) { - console.error( - 'MCP getCurrentProject error:', - JSON.stringify({ error: getErrorMessage(error) }, null, 2), - ) - throw ensureError(error) - } - } - - async getSelfTargetingIdentity() { - console.error('MCP getSelfTargetingIdentity') - - try { - this.auth.requireAuth() - this.auth.requireProject() - - const result = await fetchUserProfile( - this.auth.getAuthToken(), - this.auth.getProjectKey(), - ) - - console.error( - 'MCP getSelfTargetingIdentity result:', - JSON.stringify(result, null, 2), - ) - return result - } catch (error) { - console.error( - 'MCP getSelfTargetingIdentity error:', - JSON.stringify({ error: getErrorMessage(error) }, null, 2), - ) - throw ensureError(error) - } - } - - async updateSelfTargetingIdentity(args: UpdateSelfTargetingIdentityArgs) { - console.error( - 'MCP updateSelfTargetingIdentity params:', - JSON.stringify(args, null, 2), - ) - - try { - this.auth.requireAuth() - this.auth.requireProject() - - const result = await updateUserProfile( - this.auth.getAuthToken(), - this.auth.getProjectKey(), - { dvcUserId: args.dvc_user_id }, - ) - - console.error( - 'MCP updateSelfTargetingIdentity result:', - JSON.stringify(result, null, 2), - ) - return result - } catch (error) { - console.error( - 'MCP updateSelfTargetingIdentity error:', - JSON.stringify({ error: getErrorMessage(error) }, null, 2), - ) - throw ensureError(error) - } - } - - async listSelfTargetingOverrides() { - console.error('MCP listSelfTargetingOverrides') - - try { - this.auth.requireAuth() - this.auth.requireProject() - - const result = await fetchProjectOverridesForUser( - this.auth.getAuthToken(), - this.auth.getProjectKey(), - ) + const authToken = this.auth.getAuthToken() + const projectKey = requiresProject ? this.auth.getProjectKey() : '' + const result = await operation(authToken, projectKey) console.error( - 'MCP listSelfTargetingOverrides result:', + `MCP ${operationName} result:`, JSON.stringify(result, null, 2), ) return result } catch (error) { console.error( - 'MCP listSelfTargetingOverrides error:', + `MCP ${operationName} error:`, JSON.stringify({ error: getErrorMessage(error) }, null, 2), ) throw ensureError(error) } } - async setSelfTargetingOverride(args: SetSelfTargetingOverrideArgs) { - console.error( - 'MCP setSelfTargetingOverride params:', - JSON.stringify(args, null, 2), - ) - - try { - this.auth.requireAuth() - this.auth.requireProject() - - const result = await updateOverride( - this.auth.getAuthToken(), - this.auth.getProjectKey(), - args.feature_key, - { - environment: args.environment_key, - variation: args.variation_key, - }, - ) - - console.error( - 'MCP setSelfTargetingOverride result:', - JSON.stringify(result, null, 2), - ) - return result - } catch (error) { - console.error( - 'MCP setSelfTargetingOverride error:', - JSON.stringify({ error: getErrorMessage(error) }, null, 2), - ) - throw ensureError(error) - } + public getAuth(): DevCycleAuth { + return this.auth } +} - async clearFeatureSelfTargetingOverrides( - args: ClearSelfTargetingOverridesArgs, - ) { - console.error( - 'MCP clearFeatureSelfTargetingOverrides params:', - JSON.stringify(args, null, 2), - ) - - try { - this.auth.requireAuth() - this.auth.requireProject() - - if (!args.feature_key || !args.environment_key) { - throw new Error( - 'Both feature_key and environment_key are required to clear a specific override', - ) - } - - const result = await deleteFeatureOverrides( - this.auth.getAuthToken(), - this.auth.getProjectKey(), - args.feature_key, - args.environment_key, - ) - - console.error( - 'MCP clearFeatureSelfTargetingOverrides result:', - JSON.stringify(result, null, 2), - ) - return { - message: `Cleared override for feature '${args.feature_key}' in environment '${args.environment_key}'`, - } - } catch (error) { - console.error( - 'MCP clearFeatureSelfTargetingOverrides error:', - JSON.stringify({ error: getErrorMessage(error) }, null, 2), - ) - throw ensureError(error) - } - } - - async clearAllSelfTargetingOverrides() { - console.error('MCP clearAllSelfTargetingOverrides') - - try { - this.auth.requireAuth() - this.auth.requireProject() - - const result = await deleteAllProjectOverrides( - this.auth.getAuthToken(), - this.auth.getProjectKey(), - ) - - console.error( - 'MCP clearAllSelfTargetingOverrides result:', - JSON.stringify(result, null, 2), - ) - return { - message: 'Cleared all overrides for the project', - } - } catch (error) { - console.error( - 'MCP clearAllSelfTargetingOverrides error:', - JSON.stringify({ error: getErrorMessage(error) }, null, 2), - ) - throw ensureError(error) - } - } +// Export API functions for use in tool files +export { + fetchFeatures, + createFeature, + fetchVariables, + fetchEnvironmentByKey, + fetchEnvironments, + fetchProject, + fetchProjects, + enableTargeting, + disableTargeting, + fetchUserProfile, + updateUserProfile, + fetchProjectOverridesForUser, + updateOverride, + deleteAllProjectOverrides, + deleteFeatureOverrides, } From 2f833f259b879764b19f0b80017c2996861bda4c Mon Sep 17 00:00:00 2001 From: Jonathan Norris <jonathan@taplytics.com> Date: Tue, 8 Jul 2025 14:57:52 -0400 Subject: [PATCH 23/44] chore: cleanup --- .cursor/rules/mcp-patterns.mdc | 24 ++++++++++++++++++++ src/mcp/tools/projectTools.ts | 32 ++++++++++----------------- src/mcp/tools/selfTargetingTools.ts | 34 +++++++---------------------- src/mcp/types.ts | 4 ++-- 4 files changed, 45 insertions(+), 49 deletions(-) create mode 100644 .cursor/rules/mcp-patterns.mdc diff --git a/.cursor/rules/mcp-patterns.mdc b/.cursor/rules/mcp-patterns.mdc new file mode 100644 index 000000000..858d8a5e3 --- /dev/null +++ b/.cursor/rules/mcp-patterns.mdc @@ -0,0 +1,24 @@ +--- +description: MCP tools can have empty parameters - no dummy parameters needed +globs: ["src/mcp/tools/**/*.ts"] +alwaysApply: true +--- + +# 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/src/mcp/tools/projectTools.ts b/src/mcp/tools/projectTools.ts index da48f8229..69f1baee0 100644 --- a/src/mcp/tools/projectTools.ts +++ b/src/mcp/tools/projectTools.ts @@ -53,13 +53,7 @@ export const projectToolDefinitions: Tool[] = [ description: 'Get the currently selected project', inputSchema: { type: 'object', - properties: { - random_string: { - type: 'string', - description: 'Dummy parameter for no-parameter tools', - }, - }, - required: ['random_string'], + properties: {}, }, }, ] @@ -71,19 +65,15 @@ export const projectToolHandlers: Record<string, ToolHandler> = { return await apiClient.executeWithLogging( 'listProjects', validatedArgs, - async (authToken, projectKey) => { - const query: any = {} - if (validatedArgs.sort_by) query.sortBy = validatedArgs.sort_by - if (validatedArgs.sort_order) - query.sortOrder = validatedArgs.sort_order - if (validatedArgs.search) query.search = validatedArgs.search - if (validatedArgs.created_by) - query.createdBy = validatedArgs.created_by - if (validatedArgs.page) query.page = validatedArgs.page - if (validatedArgs.per_page) - query.perPage = validatedArgs.per_page - - return await fetchProjects(authToken, query) + async (authToken) => { + return await fetchProjects(authToken, { + sortBy: validatedArgs.sort_by, + sortOrder: validatedArgs.sort_order, + search: validatedArgs.search, + createdBy: validatedArgs.created_by, + page: validatedArgs.page, + perPage: validatedArgs.per_page, + }) }, false, ) @@ -94,7 +84,7 @@ export const projectToolHandlers: Record<string, ToolHandler> = { ) => { return await apiClient.executeWithLogging( 'getCurrentProject', - args, + null, async (authToken, projectKey) => { return await fetchProject(authToken, projectKey) }, diff --git a/src/mcp/tools/selfTargetingTools.ts b/src/mcp/tools/selfTargetingTools.ts index e5b64e2ac..bb4b33676 100644 --- a/src/mcp/tools/selfTargetingTools.ts +++ b/src/mcp/tools/selfTargetingTools.ts @@ -21,13 +21,7 @@ export const selfTargetingToolDefinitions: Tool[] = [ description: 'Get current DevCycle identity for self-targeting', inputSchema: { type: 'object', - properties: { - random_string: { - type: 'string', - description: 'Dummy parameter for no-parameter tools', - }, - }, - required: ['random_string'], + properties: {}, }, }, { @@ -52,13 +46,7 @@ export const selfTargetingToolDefinitions: Tool[] = [ 'List all self-targeting overrides for the current project', inputSchema: { type: 'object', - properties: { - random_string: { - type: 'string', - description: 'Dummy parameter for no-parameter tools', - }, - }, - required: ['random_string'], + properties: {}, }, }, { @@ -108,13 +96,7 @@ export const selfTargetingToolDefinitions: Tool[] = [ 'Clear all self-targeting overrides for the current project', inputSchema: { type: 'object', - properties: { - random_string: { - type: 'string', - description: 'Dummy parameter for no-parameter tools', - }, - }, - required: ['random_string'], + properties: {}, }, }, ] @@ -126,7 +108,7 @@ export const selfTargetingToolHandlers: Record<string, ToolHandler> = { ) => { return await apiClient.executeWithLogging( 'getSelfTargetingIdentity', - args, + null, async (authToken, projectKey) => { return await fetchUserProfile(authToken, projectKey) }, @@ -154,7 +136,7 @@ export const selfTargetingToolHandlers: Record<string, ToolHandler> = { ) => { return await apiClient.executeWithLogging( 'listSelfTargetingOverrides', - args, + null, async (authToken, projectKey) => { return await fetchProjectOverridesForUser(authToken, projectKey) }, @@ -195,8 +177,8 @@ export const selfTargetingToolHandlers: Record<string, ToolHandler> = { await deleteFeatureOverrides( authToken, projectKey, - validatedArgs.feature_key!, - validatedArgs.environment_key!, + validatedArgs.feature_key, + validatedArgs.environment_key, ) return { @@ -211,7 +193,7 @@ export const selfTargetingToolHandlers: Record<string, ToolHandler> = { ) => { return await apiClient.executeWithLogging( 'clearAllSelfTargetingOverrides', - args, + null, async (authToken, projectKey) => { await deleteAllProjectOverrides(authToken, projectKey) return { message: 'Cleared all overrides for the project' } diff --git a/src/mcp/types.ts b/src/mcp/types.ts index 0f6c4a175..46984935c 100644 --- a/src/mcp/types.ts +++ b/src/mcp/types.ts @@ -83,8 +83,8 @@ export const SetSelfTargetingOverrideArgsSchema = z.object({ }) export const ClearSelfTargetingOverridesArgsSchema = z.object({ - feature_key: z.string().optional(), - environment_key: z.string().optional(), + feature_key: z.string(), + environment_key: z.string(), }) // Type inference helpers From 8d68e1116c23cb22cb3b806f4c3420ea96529e33 Mon Sep 17 00:00:00 2001 From: Jonathan Norris <jonathan@taplytics.com> Date: Tue, 8 Jul 2025 15:11:56 -0400 Subject: [PATCH 24/44] chore: fix cursor rules --- .cursor/rules/api-client.mdc | 1 + .cursor/rules/cli-commands.mdc | 1 + .cursor/rules/deferToPrettier.mdc | 3 +++ .cursor/rules/gitCommitConventions.mdc | 4 ++-- .cursor/rules/mcp-patterns.mdc | 6 ++---- .cursor/rules/project-structure.mdc | 2 ++ .cursor/rules/testing-patterns.mdc | 1 + .cursor/rules/typescript-patterns.mdc | 5 +++-- .cursor/rules/ui-patterns.mdc | 1 + 9 files changed, 16 insertions(+), 8 deletions(-) diff --git a/.cursor/rules/api-client.mdc b/.cursor/rules/api-client.mdc index a89247fac..b3a75bb61 100644 --- a/.cursor/rules/api-client.mdc +++ b/.cursor/rules/api-client.mdc @@ -1,4 +1,5 @@ --- +description: API client patterns and HTTP client setup guidelines globs: src/api/**/*.ts --- diff --git a/.cursor/rules/cli-commands.mdc b/.cursor/rules/cli-commands.mdc index 18aa7224b..cfed350a1 100644 --- a/.cursor/rules/cli-commands.mdc +++ b/.cursor/rules/cli-commands.mdc @@ -1,4 +1,5 @@ --- +description: CLI command patterns and base command structure guidelines globs: src/commands/**/*.ts --- diff --git a/.cursor/rules/deferToPrettier.mdc b/.cursor/rules/deferToPrettier.mdc index 71d5bf860..7a1a20811 100644 --- a/.cursor/rules/deferToPrettier.mdc +++ b/.cursor/rules/deferToPrettier.mdc @@ -1,7 +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 index 528142e1e..08f920933 100644 --- a/.cursor/rules/gitCommitConventions.mdc +++ b/.cursor/rules/gitCommitConventions.mdc @@ -1,8 +1,8 @@ --- -description: -globs: +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. diff --git a/.cursor/rules/mcp-patterns.mdc b/.cursor/rules/mcp-patterns.mdc index 858d8a5e3..a9843c541 100644 --- a/.cursor/rules/mcp-patterns.mdc +++ b/.cursor/rules/mcp-patterns.mdc @@ -1,9 +1,7 @@ --- -description: MCP tools can have empty parameters - no dummy parameters needed -globs: ["src/mcp/tools/**/*.ts"] -alwaysApply: true +globs: src/mcp/tools/**/*.ts +alwaysApply: false --- - # MCP Tool Parameters MCP tools that don't need parameters should use empty properties: diff --git a/.cursor/rules/project-structure.mdc b/.cursor/rules/project-structure.mdc index 91495fe42..024cda843 100644 --- a/.cursor/rules/project-structure.mdc +++ b/.cursor/rules/project-structure.mdc @@ -1,4 +1,5 @@ --- +description: DevCycle CLI project structure overview and architecture guidelines alwaysApply: true --- @@ -50,5 +51,6 @@ All commands extend [src/commands/base.ts](mdc:src/commands/base.ts) which provi ## 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 index 44d044ae9..2d65001f6 100644 --- a/.cursor/rules/testing-patterns.mdc +++ b/.cursor/rules/testing-patterns.mdc @@ -1,4 +1,5 @@ --- +description: Testing patterns and framework setup guidelines globs: **/*.test.ts,**/*.spec.ts,test/**/*.ts --- diff --git a/.cursor/rules/typescript-patterns.mdc b/.cursor/rules/typescript-patterns.mdc index c8c865dbb..97c148f71 100644 --- a/.cursor/rules/typescript-patterns.mdc +++ b/.cursor/rules/typescript-patterns.mdc @@ -1,7 +1,8 @@ --- -globs: *.ts,*.js,src/**/*.ts,src/**/*.js +globs: *.ts,*.js +alwaysApply: false --- - + # TypeScript/JavaScript Patterns ## Import Organization diff --git a/.cursor/rules/ui-patterns.mdc b/.cursor/rules/ui-patterns.mdc index b48a1f612..1bee3ea4d 100644 --- a/.cursor/rules/ui-patterns.mdc +++ b/.cursor/rules/ui-patterns.mdc @@ -1,4 +1,5 @@ --- +description: UI patterns and user interaction guidelines for prompts and output formatting globs: src/ui/**/*.ts,src/utils/prompts.ts --- From f7760d0d385b77c0247be5ee181aaba858ce5ae6 Mon Sep 17 00:00:00 2001 From: Jonathan Norris <jonathan@taplytics.com> Date: Tue, 8 Jul 2025 15:31:15 -0400 Subject: [PATCH 25/44] feat: add create_variable / update_variable / delete_variable tools --- src/api/variables.ts | 17 ++ src/mcp/tools/environmentTools.ts | 4 +- src/mcp/tools/featureTools.ts | 10 +- src/mcp/tools/projectTools.ts | 3 +- src/mcp/tools/selfTargetingTools.ts | 7 +- src/mcp/tools/variableTools.ts | 235 +++++++++++++++++++++++++++- src/mcp/types.ts | 56 +++++++ src/mcp/utils/api.ts | 46 ------ 8 files changed, 316 insertions(+), 62 deletions(-) diff --git a/src/api/variables.ts b/src/api/variables.ts index 9350533de..2b019d4dd 100644 --- a/src/api/variables.ts +++ b/src/api/variables.ts @@ -49,6 +49,23 @@ 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, diff --git a/src/mcp/tools/environmentTools.ts b/src/mcp/tools/environmentTools.ts index 4279d21e2..e357916f9 100644 --- a/src/mcp/tools/environmentTools.ts +++ b/src/mcp/tools/environmentTools.ts @@ -1,9 +1,9 @@ import { Tool } from '@modelcontextprotocol/sdk/types.js' +import { DevCycleApiClient } from '../utils/api' import { - DevCycleApiClient, fetchEnvironments, fetchEnvironmentByKey, -} from '../utils/api' +} from '../../api/environments' import { GetSdkKeysArgsSchema, ListEnvironmentsArgsSchema } from '../types' import { ToolHandler } from '../server' diff --git a/src/mcp/tools/featureTools.ts b/src/mcp/tools/featureTools.ts index fc548dd80..61a9da956 100644 --- a/src/mcp/tools/featureTools.ts +++ b/src/mcp/tools/featureTools.ts @@ -1,11 +1,7 @@ import { Tool } from '@modelcontextprotocol/sdk/types.js' -import { - DevCycleApiClient, - fetchFeatures, - createFeature, - enableTargeting, - disableTargeting, -} from '../utils/api' +import { DevCycleApiClient } from '../utils/api' +import { fetchFeatures, createFeature } from '../../api/features' +import { enableTargeting, disableTargeting } from '../../api/targeting' import { ListFeaturesArgsSchema, CreateFeatureArgsSchema, diff --git a/src/mcp/tools/projectTools.ts b/src/mcp/tools/projectTools.ts index 69f1baee0..2139b36a7 100644 --- a/src/mcp/tools/projectTools.ts +++ b/src/mcp/tools/projectTools.ts @@ -1,5 +1,6 @@ import { Tool } from '@modelcontextprotocol/sdk/types.js' -import { DevCycleApiClient, fetchProjects, fetchProject } from '../utils/api' +import { DevCycleApiClient } from '../utils/api' +import { fetchProjects, fetchProject } from '../../api/projects' import { ListProjectsArgsSchema } from '../types' import { ToolHandler } from '../server' diff --git a/src/mcp/tools/selfTargetingTools.ts b/src/mcp/tools/selfTargetingTools.ts index bb4b33676..7f01ea9bd 100644 --- a/src/mcp/tools/selfTargetingTools.ts +++ b/src/mcp/tools/selfTargetingTools.ts @@ -1,13 +1,12 @@ import { Tool } from '@modelcontextprotocol/sdk/types.js' +import { DevCycleApiClient } from '../utils/api' +import { fetchUserProfile, updateUserProfile } from '../../api/userProfile' import { - DevCycleApiClient, - fetchUserProfile, - updateUserProfile, fetchProjectOverridesForUser, updateOverride, deleteFeatureOverrides, deleteAllProjectOverrides, -} from '../utils/api' +} from '../../api/overrides' import { UpdateSelfTargetingIdentityArgsSchema, SetSelfTargetingOverrideArgsSchema, diff --git a/src/mcp/tools/variableTools.ts b/src/mcp/tools/variableTools.ts index 4fbd78be8..c7e727a93 100644 --- a/src/mcp/tools/variableTools.ts +++ b/src/mcp/tools/variableTools.ts @@ -1,6 +1,17 @@ import { Tool } from '@modelcontextprotocol/sdk/types.js' -import { DevCycleApiClient, fetchVariables } from '../utils/api' -import { ListVariablesArgsSchema } from '../types' +import { DevCycleApiClient } from '../utils/api' +import { + fetchVariables, + createVariable, + updateVariable, + deleteVariable, +} from '../../api/variables' +import { + ListVariablesArgsSchema, + CreateVariableArgsSchema, + UpdateVariableArgsSchema, + DeleteVariableArgsSchema, +} from '../types' import { ToolHandler } from '../server' export const variableToolDefinitions: Tool[] = [ @@ -26,6 +37,166 @@ export const variableToolDefinitions: Tool[] = [ }, }, }, + { + name: 'create_variable', + description: 'Create a new variable', + inputSchema: { + type: 'object', + properties: { + name: { + type: 'string', + description: 'Variable name (1-100 characters)', + }, + description: { + type: 'string', + description: 'Variable description (max 1000 characters)', + }, + key: { + type: 'string', + description: + 'Unique variable key (1-100 characters, must match pattern ^[a-z0-9-_.]+$)', + }, + _feature: { + type: 'string', + description: + 'Feature key or ID to associate with this variable', + }, + type: { + type: 'string', + enum: ['String', 'Boolean', 'Number', 'JSON'], + description: 'Variable type', + }, + defaultValue: { + description: 'Default value for the variable', + }, + validationSchema: { + type: 'object', + description: 'Validation schema for variable values', + properties: { + schemaType: { + type: 'string', + description: 'Schema type', + }, + enumValues: { + type: 'array', + description: 'Allowed enum values', + }, + regexPattern: { + type: 'string', + description: 'Regex pattern for validation', + }, + jsonSchema: { + type: 'string', + description: 'JSON schema for validation', + }, + description: { + type: 'string', + description: 'Schema description', + }, + exampleValue: { + description: 'Example value for the schema', + }, + }, + }, + tags: { + type: 'array', + items: { + type: 'string', + }, + description: 'Tags to organize variables', + }, + }, + required: ['key', 'type'], + }, + }, + { + name: 'update_variable', + description: 'Update an existing variable', + inputSchema: { + type: 'object', + properties: { + key: { + type: 'string', + description: 'Current variable key', + }, + name: { + type: 'string', + description: 'Updated variable name (1-100 characters)', + }, + description: { + type: 'string', + description: + 'Updated variable description (max 1000 characters)', + }, + new_key: { + type: 'string', + description: + 'New variable key (1-100 characters, must match pattern ^[a-z0-9-_.]+$)', + }, + type: { + type: 'string', + enum: ['String', 'Boolean', 'Number', 'JSON'], + description: 'Variable type', + }, + validationSchema: { + type: 'object', + description: 'Validation schema for variable values', + properties: { + schemaType: { + type: 'string', + description: 'Schema type', + }, + enumValues: { + type: 'array', + description: 'Allowed enum values', + }, + regexPattern: { + type: 'string', + description: 'Regex pattern for validation', + }, + jsonSchema: { + type: 'string', + description: 'JSON schema for validation', + }, + description: { + type: 'string', + description: 'Schema description', + }, + exampleValue: { + description: 'Example value for the schema', + }, + }, + }, + persistent: { + type: 'boolean', + description: + 'Whether the variable is intended to be long-lived within a feature', + }, + tags: { + type: 'array', + items: { + type: 'string', + }, + description: 'Tags to organize variables', + }, + }, + required: ['key'], + }, + }, + { + name: 'delete_variable', + description: 'Delete a variable', + inputSchema: { + type: 'object', + properties: { + key: { + type: 'string', + description: 'Variable key to delete', + }, + }, + required: ['key'], + }, + }, ] export const variableToolHandlers: Record<string, ToolHandler> = { @@ -45,4 +216,64 @@ export const variableToolHandlers: Record<string, ToolHandler> = { }, ) }, + create_variable: async (args: unknown, apiClient: DevCycleApiClient) => { + const validatedArgs = CreateVariableArgsSchema.parse(args) + + return await apiClient.executeWithLogging( + 'createVariable', + validatedArgs, + async (authToken, projectKey) => { + const params = { + name: validatedArgs.name, + description: validatedArgs.description, + key: validatedArgs.key, + _feature: validatedArgs._feature, + type: validatedArgs.type, + defaultValue: validatedArgs.defaultValue, + validationSchema: validatedArgs.validationSchema, + tags: validatedArgs.tags, + } + return await createVariable(authToken, projectKey, params) + }, + ) + }, + update_variable: async (args: unknown, apiClient: DevCycleApiClient) => { + const validatedArgs = UpdateVariableArgsSchema.parse(args) + + return await apiClient.executeWithLogging( + 'updateVariable', + validatedArgs, + async (authToken, projectKey) => { + const params = { + name: validatedArgs.name, + description: validatedArgs.description, + key: validatedArgs.new_key, + type: validatedArgs.type, + validationSchema: validatedArgs.validationSchema, + persistent: validatedArgs.persistent, + tags: validatedArgs.tags, + } + return await updateVariable( + authToken, + projectKey, + validatedArgs.key, + params, + ) + }, + ) + }, + delete_variable: async (args: unknown, apiClient: DevCycleApiClient) => { + const validatedArgs = DeleteVariableArgsSchema.parse(args) + + return await apiClient.executeWithLogging( + 'deleteVariable', + validatedArgs, + async (authToken, projectKey) => { + await deleteVariable(authToken, projectKey, validatedArgs.key) + return { + message: `Variable '${validatedArgs.key}' deleted successfully`, + } + }, + ) + }, } diff --git a/src/mcp/types.ts b/src/mcp/types.ts index 46984935c..657942a7c 100644 --- a/src/mcp/types.ts +++ b/src/mcp/types.ts @@ -13,6 +13,59 @@ export const ListVariablesArgsSchema = z.object({ per_page: z.number().min(1).max(1000).default(100).optional(), }) +export const CreateVariableArgsSchema = z.object({ + name: z.string().min(1).max(100).optional(), + description: z.string().max(1000).optional(), + key: z + .string() + .min(1) + .max(100) + .regex(/^[a-z0-9-_.]+$/), + _feature: z.string().optional(), + type: z.enum(['String', 'Boolean', 'Number', 'JSON']), + defaultValue: z.any().optional(), + validationSchema: z + .object({ + schemaType: z.string(), + enumValues: z.array(z.any()).optional(), + regexPattern: z.string().optional(), + jsonSchema: z.string().optional(), + description: z.string(), + exampleValue: z.any(), + }) + .optional(), + tags: z.array(z.string()).optional(), +}) + +export const UpdateVariableArgsSchema = z.object({ + key: z.string(), + name: z.string().min(1).max(100).optional(), + description: z.string().max(1000).optional(), + new_key: z + .string() + .min(1) + .max(100) + .regex(/^[a-z0-9-_.]+$/) + .optional(), + type: z.enum(['String', 'Boolean', 'Number', 'JSON']).optional(), + validationSchema: z + .object({ + schemaType: z.string(), + enumValues: z.array(z.any()).optional(), + regexPattern: z.string().optional(), + jsonSchema: z.string().optional(), + description: z.string(), + exampleValue: z.any(), + }) + .optional(), + persistent: z.boolean().optional(), + tags: z.array(z.string()).optional(), +}) + +export const DeleteVariableArgsSchema = z.object({ + key: z.string(), +}) + export const ListProjectsArgsSchema = z.object({ sort_by: z .enum([ @@ -90,6 +143,9 @@ export const ClearSelfTargetingOverridesArgsSchema = z.object({ // Type inference helpers export type ListFeaturesArgs = z.infer<typeof ListFeaturesArgsSchema> export type ListVariablesArgs = z.infer<typeof ListVariablesArgsSchema> +export type CreateVariableArgs = z.infer<typeof CreateVariableArgsSchema> +export type UpdateVariableArgs = z.infer<typeof UpdateVariableArgsSchema> +export type DeleteVariableArgs = z.infer<typeof DeleteVariableArgsSchema> export type ListProjectsArgs = z.infer<typeof ListProjectsArgsSchema> export type ListEnvironmentsArgs = z.infer<typeof ListEnvironmentsArgsSchema> export type GetSdkKeysArgs = z.infer<typeof GetSdkKeysArgsSchema> diff --git a/src/mcp/utils/api.ts b/src/mcp/utils/api.ts index f9d60a6d4..f00a663e3 100644 --- a/src/mcp/utils/api.ts +++ b/src/mcp/utils/api.ts @@ -1,31 +1,4 @@ import { DevCycleAuth } from './auth' -import { fetchFeatures, createFeature } from '../../api/features' -import { fetchVariables } from '../../api/variables' -import { - fetchEnvironmentByKey, - fetchEnvironments, -} from '../../api/environments' -import { fetchProject, fetchProjects } from '../../api/projects' -import { enableTargeting, disableTargeting } from '../../api/targeting' -import { fetchUserProfile, updateUserProfile } from '../../api/userProfile' -import { - fetchProjectOverridesForUser, - updateOverride, - deleteAllProjectOverrides, - deleteFeatureOverrides, -} from '../../api/overrides' -import type { - ListFeaturesArgs, - ListVariablesArgs, - ListProjectsArgs, - GetSdkKeysArgs, - EnableTargetingArgs, - DisableTargetingArgs, - CreateFeatureArgs, - UpdateSelfTargetingIdentityArgs, - SetSelfTargetingOverrideArgs, - ClearSelfTargetingOverridesArgs, -} from '../types' function getErrorMessage(error: unknown): string { if (error instanceof Error) { @@ -86,22 +59,3 @@ export class DevCycleApiClient { return this.auth } } - -// Export API functions for use in tool files -export { - fetchFeatures, - createFeature, - fetchVariables, - fetchEnvironmentByKey, - fetchEnvironments, - fetchProject, - fetchProjects, - enableTargeting, - disableTargeting, - fetchUserProfile, - updateUserProfile, - fetchProjectOverridesForUser, - updateOverride, - deleteAllProjectOverrides, - deleteFeatureOverrides, -} From d7089f3e9f33bf5f45f87361247a77873afeaa6c Mon Sep 17 00:00:00 2001 From: Jonathan Norris <jonathan@taplytics.com> Date: Tue, 8 Jul 2025 16:29:14 -0400 Subject: [PATCH 26/44] feat: add fetch_feature_variations / create_feature_variation / update_feature_variation tool calls --- src/mcp/tools/featureTools.ts | 152 ++++++++++++++++++++++++++++++++++ src/mcp/types.ts | 35 ++++++++ 2 files changed, 187 insertions(+) diff --git a/src/mcp/tools/featureTools.ts b/src/mcp/tools/featureTools.ts index 61a9da956..d838064ea 100644 --- a/src/mcp/tools/featureTools.ts +++ b/src/mcp/tools/featureTools.ts @@ -1,12 +1,20 @@ import { Tool } from '@modelcontextprotocol/sdk/types.js' import { DevCycleApiClient } from '../utils/api' import { fetchFeatures, createFeature } from '../../api/features' +import { + fetchVariations, + createVariation, + updateVariation, +} from '../../api/variations' import { enableTargeting, disableTargeting } from '../../api/targeting' import { ListFeaturesArgsSchema, CreateFeatureArgsSchema, EnableTargetingArgsSchema, DisableTargetingArgsSchema, + ListVariationsArgsSchema, + CreateVariationArgsSchema, + UpdateVariationArgsSchema, } from '../types' import { ToolHandler } from '../server' @@ -64,6 +72,81 @@ export const featureToolDefinitions: Tool[] = [ }, }, }, + { + name: 'fetch_feature_variations', + description: 'Get a list of variations for a feature', + inputSchema: { + type: 'object', + properties: { + feature_key: { + type: 'string', + description: 'The key of the feature', + }, + }, + required: ['feature_key'], + }, + }, + { + name: 'create_feature_variation', + description: 'Create a new variation within a feature', + inputSchema: { + type: 'object', + properties: { + feature_key: { + type: 'string', + description: 'The key of the feature', + }, + key: { + type: 'string', + description: + 'Unique variation key (1-100 characters, must match pattern ^[a-z0-9-_.]+$)', + }, + name: { + type: 'string', + description: + 'Human-readable variation name (max 100 characters)', + }, + variables: { + type: 'object', + description: + 'Optional key-value map of variable keys to their values for this variation (supports string, number, boolean, and object values)', + }, + }, + required: ['feature_key', 'key', 'name'], + }, + }, + { + name: 'update_feature_variation', + description: 'Update an existing variation by key', + inputSchema: { + type: 'object', + properties: { + feature_key: { + type: 'string', + description: 'The key of the feature', + }, + variation_key: { + type: 'string', + description: 'The key of the variation to update', + }, + key: { + type: 'string', + description: + 'New variation key (1-100 characters, must match pattern ^[a-z0-9-_.]+$)', + }, + name: { + type: 'string', + description: 'New variation name (max 100 characters)', + }, + variables: { + type: 'object', + description: + 'Overrides the key-value map of variable keys to their values for this variation (supports string, number, boolean, and object values)', + }, + }, + required: ['feature_key', 'variation_key'], + }, + }, { name: 'enable_targeting', description: 'Enable targeting for a feature in an environment', @@ -149,6 +232,75 @@ export const featureToolHandlers: Record<string, ToolHandler> = { }, ) }, + fetch_feature_variations: async ( + args: unknown, + apiClient: DevCycleApiClient, + ) => { + const validatedArgs = ListVariationsArgsSchema.parse(args) + + return await apiClient.executeWithLogging( + 'fetchFeatureVariations', + validatedArgs, + async (authToken, projectKey) => { + return await fetchVariations( + authToken, + projectKey, + validatedArgs.feature_key, + ) + }, + ) + }, + create_feature_variation: async ( + args: unknown, + apiClient: DevCycleApiClient, + ) => { + const validatedArgs = CreateVariationArgsSchema.parse(args) + + return await apiClient.executeWithLogging( + 'createFeatureVariation', + validatedArgs, + async (authToken, projectKey) => { + const variationData = { + key: validatedArgs.key, + name: validatedArgs.name, + variables: validatedArgs.variables, + } + + return await createVariation( + authToken, + projectKey, + validatedArgs.feature_key, + variationData, + ) + }, + ) + }, + update_feature_variation: async ( + args: unknown, + apiClient: DevCycleApiClient, + ) => { + const validatedArgs = UpdateVariationArgsSchema.parse(args) + + return await apiClient.executeWithLogging( + 'updateFeatureVariation', + validatedArgs, + async (authToken, projectKey) => { + const variationData = { + key: validatedArgs.key, + name: validatedArgs.name, + variables: validatedArgs.variables, + } + + return await updateVariation( + authToken, + projectKey, + validatedArgs.feature_key, + validatedArgs.variation_key, + variationData, + ) + }, + ) + }, enable_targeting: async (args: unknown, apiClient: DevCycleApiClient) => { const validatedArgs = EnableTargetingArgsSchema.parse(args) diff --git a/src/mcp/types.ts b/src/mcp/types.ts index 657942a7c..a01027654 100644 --- a/src/mcp/types.ts +++ b/src/mcp/types.ts @@ -140,6 +140,38 @@ export const ClearSelfTargetingOverridesArgsSchema = z.object({ environment_key: z.string(), }) +export const ListVariationsArgsSchema = z.object({ + feature_key: z.string(), +}) + +const VariationVariablesSchema = z + .record( + z.union([z.string(), z.number(), z.boolean(), z.record(z.unknown())]), + ) + .optional() + +export const CreateVariationArgsSchema = z.object({ + feature_key: z.string(), + key: z + .string() + .max(100) + .regex(/^[a-z0-9-_.]+$/), + name: z.string().max(100), + variables: VariationVariablesSchema, +}) + +export const UpdateVariationArgsSchema = z.object({ + feature_key: z.string(), + variation_key: z.string(), + key: z + .string() + .max(100) + .regex(/^[a-z0-9-_.]+$/) + .optional(), + name: z.string().max(100).optional(), + variables: VariationVariablesSchema, +}) + // Type inference helpers export type ListFeaturesArgs = z.infer<typeof ListFeaturesArgsSchema> export type ListVariablesArgs = z.infer<typeof ListVariablesArgsSchema> @@ -152,6 +184,9 @@ export type GetSdkKeysArgs = z.infer<typeof GetSdkKeysArgsSchema> export type EnableTargetingArgs = z.infer<typeof EnableTargetingArgsSchema> export type DisableTargetingArgs = z.infer<typeof DisableTargetingArgsSchema> export type CreateFeatureArgs = z.infer<typeof CreateFeatureArgsSchema> +export type ListVariationsArgs = z.infer<typeof ListVariationsArgsSchema> +export type CreateVariationArgs = z.infer<typeof CreateVariationArgsSchema> +export type UpdateVariationArgs = z.infer<typeof UpdateVariationArgsSchema> export type UpdateSelfTargetingIdentityArgs = z.infer< typeof UpdateSelfTargetingIdentityArgsSchema > From d2aaa072bfa768b8d07a068c7d0da0861acf8f7c Mon Sep 17 00:00:00 2001 From: Jonathan Norris <jonathan@taplytics.com> Date: Tue, 8 Jul 2025 16:46:51 -0400 Subject: [PATCH 27/44] feat: add list_feature_targeting and update_feature_targeting tools --- src/mcp/tools/featureTools.ts | 230 +++++++++++++++++++++++++++++++++- src/mcp/types.ts | 54 ++++++++ 2 files changed, 283 insertions(+), 1 deletion(-) diff --git a/src/mcp/tools/featureTools.ts b/src/mcp/tools/featureTools.ts index d838064ea..77296ab88 100644 --- a/src/mcp/tools/featureTools.ts +++ b/src/mcp/tools/featureTools.ts @@ -6,7 +6,12 @@ import { createVariation, updateVariation, } from '../../api/variations' -import { enableTargeting, disableTargeting } from '../../api/targeting' +import { + enableTargeting, + disableTargeting, + fetchTargetingForFeature, + updateFeatureConfigForEnvironment, +} from '../../api/targeting' import { ListFeaturesArgsSchema, CreateFeatureArgsSchema, @@ -15,6 +20,8 @@ import { ListVariationsArgsSchema, CreateVariationArgsSchema, UpdateVariationArgsSchema, + ListFeatureTargetingArgsSchema, + UpdateFeatureTargetingArgsSchema, } from '../types' import { ToolHandler } from '../server' @@ -183,6 +190,185 @@ export const featureToolDefinitions: Tool[] = [ required: ['feature_key', 'environment_key'], }, }, + { + name: 'list_feature_targeting', + description: + 'List feature configurations (targeting rules) for a feature', + inputSchema: { + type: 'object', + properties: { + feature_key: { + type: 'string', + description: 'The key of the feature', + }, + environment_key: { + type: 'string', + description: + 'The key of the environment (optional - if not provided, returns all environments)', + }, + }, + required: ['feature_key'], + }, + }, + { + name: 'update_feature_targeting', + description: + 'Update feature configuration (targeting rules) for a feature in an environment', + inputSchema: { + type: 'object', + properties: { + feature_key: { + type: 'string', + description: 'The key of the feature', + }, + environment_key: { + type: 'string', + description: 'The key of the environment', + }, + 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: + 'Target ID (optional for new targets)', + }, + name: { + type: 'string', + description: 'Target name', + }, + audience: { + type: 'object', + description: + 'Audience definition for the target', + properties: { + name: { + type: 'string', + description: 'Audience name', + }, + filters: { + type: 'object', + description: 'Filter definition', + properties: { + filters: { + type: 'array', + description: + 'Array of filter conditions', + }, + operator: { + type: 'string', + enum: ['and', 'or'], + description: + 'Logical operator for combining filters', + }, + }, + required: ['filters', 'operator'], + }, + }, + required: ['filters'], + }, + 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: { + type: { + type: 'string', + enum: [ + 'schedule', + 'gradual', + 'stepped', + ], + description: 'Rollout type', + }, + startDate: { + type: 'string', + format: 'date-time', + description: 'Rollout start date', + }, + startPercentage: { + type: 'number', + minimum: 0, + maximum: 1, + description: + 'Starting percentage for rollout', + }, + stages: { + type: 'array', + description: 'Rollout stages', + 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'], + }, + }, ] export const featureToolHandlers: Record<string, ToolHandler> = { @@ -333,4 +519,46 @@ export const featureToolHandlers: Record<string, ToolHandler> = { }, ) }, + list_feature_targeting: async ( + args: unknown, + apiClient: DevCycleApiClient, + ) => { + const validatedArgs = ListFeatureTargetingArgsSchema.parse(args) + + return await apiClient.executeWithLogging( + 'listFeatureTargeting', + validatedArgs, + async (authToken, projectKey) => { + return await fetchTargetingForFeature( + authToken, + projectKey, + validatedArgs.feature_key, + validatedArgs.environment_key, + ) + }, + ) + }, + update_feature_targeting: async ( + args: unknown, + apiClient: DevCycleApiClient, + ) => { + const validatedArgs = UpdateFeatureTargetingArgsSchema.parse(args) + + return await apiClient.executeWithLogging( + 'updateFeatureTargeting', + validatedArgs, + async (authToken, projectKey) => { + return await updateFeatureConfigForEnvironment( + authToken, + projectKey, + validatedArgs.feature_key, + validatedArgs.environment_key, + { + status: validatedArgs.status, + targets: validatedArgs.targets, + }, + ) + }, + ) + }, } diff --git a/src/mcp/types.ts b/src/mcp/types.ts index a01027654..a5de87edb 100644 --- a/src/mcp/types.ts +++ b/src/mcp/types.ts @@ -172,6 +172,54 @@ export const UpdateVariationArgsSchema = z.object({ variables: VariationVariablesSchema, }) +export const ListFeatureTargetingArgsSchema = z.object({ + feature_key: z.string(), + environment_key: z.string().optional(), +}) + +export const UpdateFeatureTargetingArgsSchema = z.object({ + feature_key: z.string(), + environment_key: z.string(), + status: z.enum(['active', 'inactive', 'archived']).optional(), + targets: z + .array( + z.object({ + _id: z.string().optional(), + name: z.string().optional(), + audience: z.object({ + name: z.string().max(100).optional(), + filters: z.object({ + filters: z.array(z.any()), + operator: z.enum(['and', 'or']), + }), + }), + rollout: z + .object({ + startPercentage: z.number().gte(0).lte(1).optional(), + type: z.enum(['schedule', 'gradual', 'stepped']), + startDate: z.string().datetime(), + stages: z + .array( + z.object({ + percentage: z.number().gte(0).lte(1), + type: z.enum(['linear', 'discrete']), + date: z.string().datetime(), + }), + ) + .optional(), + }) + .optional(), + distribution: z.array( + z.object({ + percentage: z.number().gte(0).lte(1), + _variation: z.string(), + }), + ), + }), + ) + .optional(), +}) + // Type inference helpers export type ListFeaturesArgs = z.infer<typeof ListFeaturesArgsSchema> export type ListVariablesArgs = z.infer<typeof ListVariablesArgsSchema> @@ -196,3 +244,9 @@ export type SetSelfTargetingOverrideArgs = z.infer< export type ClearSelfTargetingOverridesArgs = z.infer< typeof ClearSelfTargetingOverridesArgsSchema > +export type ListFeatureTargetingArgs = z.infer< + typeof ListFeatureTargetingArgsSchema +> +export type UpdateFeatureTargetingArgs = z.infer< + typeof UpdateFeatureTargetingArgsSchema +> From b121dac7615e4a13a54a45715293f21671075631 Mon Sep 17 00:00:00 2001 From: Jonathan Norris <jonathan@taplytics.com> Date: Tue, 8 Jul 2025 21:49:02 -0400 Subject: [PATCH 28/44] feat: add update_feature tool call, start using zodClient.ts types --- src/mcp/tools/featureTools.ts | 244 ++++++++++++++++++++++++++++++++-- src/mcp/types.ts | 17 ++- 2 files changed, 245 insertions(+), 16 deletions(-) diff --git a/src/mcp/tools/featureTools.ts b/src/mcp/tools/featureTools.ts index 77296ab88..2ed63d775 100644 --- a/src/mcp/tools/featureTools.ts +++ b/src/mcp/tools/featureTools.ts @@ -1,6 +1,6 @@ import { Tool } from '@modelcontextprotocol/sdk/types.js' import { DevCycleApiClient } from '../utils/api' -import { fetchFeatures, createFeature } from '../../api/features' +import { fetchFeatures, createFeature, updateFeature } from '../../api/features' import { fetchVariations, createVariation, @@ -15,6 +15,7 @@ import { import { ListFeaturesArgsSchema, CreateFeatureArgsSchema, + UpdateFeatureArgsSchema, EnableTargetingArgsSchema, DisableTargetingArgsSchema, ListVariationsArgsSchema, @@ -56,21 +57,121 @@ export const featureToolDefinitions: Tool[] = [ properties: { key: { type: 'string', - description: 'Unique feature key', + description: + 'Unique feature key (max 100 characters, pattern: ^[a-z0-9-_.]+$)', }, name: { type: 'string', - description: 'Human-readable feature name', + description: + 'Human-readable feature name (max 100 characters)', }, description: { type: 'string', - description: 'Feature description', + description: 'Feature description (max 1000 characters)', }, type: { type: 'string', enum: ['release', 'experiment', 'permission', 'ops'], description: 'Feature type', }, + tags: { + type: 'array', + items: { + type: 'string', + }, + description: 'Tags to organize features', + }, + controlVariation: { + type: 'string', + description: + 'The key of the variation that is used as the control variation for Metrics', + }, + settings: { + type: 'object', + properties: { + publicName: { + type: 'string', + description: + 'Public name for the feature (max 100 characters)', + }, + publicDescription: { + type: 'string', + description: + 'Public description for the feature (max 1000 characters)', + }, + optInEnabled: { + type: 'boolean', + description: + 'Whether opt-in is enabled for the feature', + }, + }, + description: + 'Feature-level settings (all properties required if provided)', + required: [ + 'publicName', + 'publicDescription', + 'optInEnabled', + ], + }, + sdkVisibility: { + type: 'object', + properties: { + mobile: { + type: 'boolean', + description: + 'Whether the feature is visible to mobile SDKs', + }, + client: { + type: 'boolean', + description: + 'Whether the feature is visible to client SDKs', + }, + server: { + type: 'boolean', + description: + 'Whether the feature is visible to server SDKs', + }, + }, + description: + 'SDK Type Visibility Settings (all properties required if provided)', + required: ['mobile', 'client', 'server'], + }, + variables: { + type: 'array', + description: + 'Array of variables to create or reassociate with this feature', + items: { + type: 'object', + description: 'Variable creation or reassociation data', + }, + }, + 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: { + type: 'string', + description: 'Status for this environment', + }, + }, + }, + }, interactive: { type: 'boolean', description: @@ -79,6 +180,115 @@ export const featureToolDefinitions: Tool[] = [ }, }, }, + { + name: 'update_feature', + description: 'Update an existing feature flag', + inputSchema: { + type: 'object', + properties: { + key: { + type: 'string', + description: + 'The key of the feature to update(1-100 characters, must match pattern ^[a-z0-9-_.]+$)', + }, + name: { + type: 'string', + description: + 'Human-readable feature name (max 100 characters)', + }, + description: { + type: 'string', + description: 'Feature description (max 1000 characters)', + }, + type: { + type: 'string', + enum: ['release', 'experiment', 'permission', 'ops'], + description: 'Feature type', + }, + tags: { + type: 'array', + items: { + type: 'string', + }, + description: 'Tags to organize Features on the dashboard', + }, + controlVariation: { + type: 'string', + description: + 'The key of the variation that is used as the control variation for Metrics', + }, + settings: { + type: 'object', + properties: { + publicName: { + type: 'string', + description: + 'Public name for the feature (max 100 characters)', + }, + publicDescription: { + type: 'string', + description: + 'Public description for the feature (max 1000 characters)', + }, + optInEnabled: { + type: 'boolean', + description: + 'Whether opt-in is enabled for the feature', + }, + }, + description: + 'Feature-level settings (all properties required if provided)', + required: [ + 'publicName', + 'publicDescription', + 'optInEnabled', + ], + }, + sdkVisibility: { + type: 'object', + properties: { + mobile: { + type: 'boolean', + description: + 'Whether the feature is visible to mobile SDKs', + }, + client: { + type: 'boolean', + description: + 'Whether the feature is visible to client SDKs', + }, + server: { + type: 'boolean', + description: + 'Whether the feature is visible to server SDKs', + }, + }, + description: + 'SDK Type Visibility Settings (all properties required if provided)', + required: ['mobile', 'client', 'server'], + }, + variables: { + type: 'array', + description: + 'Array of variables to create or reassociate with this feature', + items: { + type: 'object', + description: 'Variable creation or reassociation data', + }, + }, + variations: { + type: 'array', + description: 'Array of variations for this feature', + items: { + type: 'object', + description: + 'Variation data with key, name, variables, and _id', + }, + }, + }, + required: ['key'], + }, + }, { name: 'fetch_feature_variations', description: 'Get a list of variations for a feature', @@ -407,17 +617,31 @@ export const featureToolHandlers: Record<string, ToolHandler> = { ) } - const featureData = { - key: validatedArgs.key, - name: validatedArgs.name, - description: validatedArgs.description || '', - type: validatedArgs.type || 'release', - } + // Remove the MCP-specific 'interactive' property and pass the rest to the API + const { interactive, ...featureData } = validatedArgs return await createFeature(authToken, projectKey, featureData) }, ) }, + update_feature: async (args: unknown, apiClient: DevCycleApiClient) => { + const validatedArgs = UpdateFeatureArgsSchema.parse(args) + + return await apiClient.executeWithLogging( + 'updateFeature', + validatedArgs, + async (authToken, projectKey) => { + const { key, ...updateData } = validatedArgs + + return await updateFeature( + authToken, + projectKey, + key, + updateData, + ) + }, + ) + }, fetch_feature_variations: async ( args: unknown, apiClient: DevCycleApiClient, diff --git a/src/mcp/types.ts b/src/mcp/types.ts index a5de87edb..2d889dbf3 100644 --- a/src/mcp/types.ts +++ b/src/mcp/types.ts @@ -1,4 +1,5 @@ import { z } from 'zod' +import { schemas } from '../api/zodClient' // Zod schemas for MCP tool arguments export const ListFeaturesArgsSchema = z.object({ @@ -117,12 +118,15 @@ export const DisableTargetingArgsSchema = z.object({ environment_key: z.string(), }) -export const CreateFeatureArgsSchema = z.object({ - key: z.string().optional(), - name: z.string().optional(), - description: z.string().optional(), - type: z.enum(['release', 'experiment', 'permission', 'ops']).optional(), - interactive: z.boolean().optional(), +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 UpdateSelfTargetingIdentityArgsSchema = z.object({ @@ -232,6 +236,7 @@ export type GetSdkKeysArgs = z.infer<typeof GetSdkKeysArgsSchema> export type EnableTargetingArgs = z.infer<typeof EnableTargetingArgsSchema> export type DisableTargetingArgs = z.infer<typeof DisableTargetingArgsSchema> export type CreateFeatureArgs = z.infer<typeof CreateFeatureArgsSchema> +export type UpdateFeatureArgs = z.infer<typeof UpdateFeatureArgsSchema> export type ListVariationsArgs = z.infer<typeof ListVariationsArgsSchema> export type CreateVariationArgs = z.infer<typeof CreateVariationArgsSchema> export type UpdateVariationArgs = z.infer<typeof UpdateVariationArgsSchema> From 4e63e485afd50e3df31954b15b79561803798e15 Mon Sep 17 00:00:00 2001 From: Jonathan Norris <jonathan@taplytics.com> Date: Tue, 8 Jul 2025 22:33:21 -0400 Subject: [PATCH 29/44] feat: cleanup types.ts, use zodClient.ts types as base --- src/mcp/tools/environmentTools.ts | 21 ++-- src/mcp/tools/featureTools.ts | 77 ++++++------ src/mcp/tools/projectTools.ts | 17 +-- src/mcp/tools/variableTools.ts | 66 +++------- src/mcp/types.ts | 193 ++++++------------------------ 5 files changed, 104 insertions(+), 270 deletions(-) diff --git a/src/mcp/tools/environmentTools.ts b/src/mcp/tools/environmentTools.ts index e357916f9..c2e618367 100644 --- a/src/mcp/tools/environmentTools.ts +++ b/src/mcp/tools/environmentTools.ts @@ -25,14 +25,14 @@ export const environmentToolDefinitions: Tool[] = [ description: 'Page number (default: 1)', minimum: 1, }, - per_page: { + perPage: { type: 'number', description: 'Number of items per page (default: 100, max: 1000)', minimum: 1, maximum: 1000, }, - sort_by: { + sortBy: { type: 'string', description: 'Field to sort by (default: createdAt)', enum: [ @@ -44,12 +44,12 @@ export const environmentToolDefinitions: Tool[] = [ 'propertyKey', ], }, - sort_order: { + sortOrder: { type: 'string', enum: ['asc', 'desc'], description: 'Sort order (default: desc)', }, - created_by: { + createdBy: { type: 'string', description: 'Filter by creator user ID', }, @@ -62,17 +62,17 @@ export const environmentToolDefinitions: Tool[] = [ inputSchema: { type: 'object', properties: { - environment_key: { + environmentKey: { type: 'string', description: 'The key of the environment', }, - key_type: { + keyType: { type: 'string', enum: ['mobile', 'server', 'client'], description: 'The type of SDK key to retrieve', }, }, - required: ['environment_key'], + required: ['environmentKey'], }, }, ] @@ -99,15 +99,14 @@ export const environmentToolHandlers: Record<string, ToolHandler> = { const environment = await fetchEnvironmentByKey( authToken, projectKey, - validatedArgs.environment_key, + validatedArgs.environmentKey, ) const sdkKeys = environment.sdkKeys - if (validatedArgs.key_type) { + if (validatedArgs.keyType) { return { - [validatedArgs.key_type]: - sdkKeys[validatedArgs.key_type], + [validatedArgs.keyType]: sdkKeys[validatedArgs.keyType], } } else { return { diff --git a/src/mcp/tools/featureTools.ts b/src/mcp/tools/featureTools.ts index 2ed63d775..debf9ddcf 100644 --- a/src/mcp/tools/featureTools.ts +++ b/src/mcp/tools/featureTools.ts @@ -316,7 +316,7 @@ export const featureToolDefinitions: Tool[] = [ key: { type: 'string', description: - 'Unique variation key (1-100 characters, must match pattern ^[a-z0-9-_.]+$)', + 'Unique variation key (max 100 characters, pattern: ^[a-z0-9-_.]+$)', }, name: { type: 'string', @@ -327,6 +327,7 @@ export const featureToolDefinitions: Tool[] = [ 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'], @@ -349,7 +350,7 @@ export const featureToolDefinitions: Tool[] = [ key: { type: 'string', description: - 'New variation key (1-100 characters, must match pattern ^[a-z0-9-_.]+$)', + 'New variation key (max 100 characters, pattern: ^[a-z0-9-_.]+$)', }, name: { type: 'string', @@ -358,7 +359,12 @@ export const featureToolDefinitions: Tool[] = [ variables: { type: 'object', description: - 'Overrides the key-value map of variable keys to their values for this variation (supports string, number, boolean, and object values)', + 'Key-value map of variable keys to their values for this variation (supports string, number, boolean, and object values)', + additionalProperties: true, + }, + _id: { + type: 'string', + description: 'Internal variation ID (optional)', }, }, required: ['feature_key', 'variation_key'], @@ -454,7 +460,7 @@ export const featureToolDefinitions: Tool[] = [ }, name: { type: 'string', - description: 'Target name', + description: 'Target name (optional)', }, audience: { type: 'object', @@ -463,16 +469,18 @@ export const featureToolDefinitions: Tool[] = [ properties: { name: { type: 'string', - description: 'Audience name', + description: + 'Audience name (max 100 characters, optional)', }, filters: { type: 'object', - description: 'Filter definition', + description: + 'Audience filters with logical operator', properties: { filters: { type: 'array', description: - 'Array of filter conditions', + 'Array of filter conditions (supports all, optIn, user, userCountry, userAppVersion, userPlatformVersion, userCustom, audienceMatch filters)', }, operator: { type: 'string', @@ -512,6 +520,13 @@ export const featureToolDefinitions: Tool[] = [ type: 'object', description: 'Rollout configuration (optional)', properties: { + startPercentage: { + type: 'number', + minimum: 0, + maximum: 1, + description: + 'Starting percentage for rollout (optional)', + }, type: { type: 'string', enum: [ @@ -526,16 +541,10 @@ export const featureToolDefinitions: Tool[] = [ format: 'date-time', description: 'Rollout start date', }, - startPercentage: { - type: 'number', - minimum: 0, - maximum: 1, - description: - 'Starting percentage for rollout', - }, stages: { type: 'array', - description: 'Rollout stages', + description: + 'Rollout stages (optional)', items: { type: 'object', properties: { @@ -589,12 +598,7 @@ export const featureToolHandlers: Record<string, ToolHandler> = { 'listFeatures', validatedArgs, async (authToken, projectKey) => { - const query = { - search: validatedArgs.search, - page: validatedArgs.page, - perPage: validatedArgs.per_page, - } - return await fetchFeatures(authToken, projectKey, query) + return await fetchFeatures(authToken, projectKey, validatedArgs) }, ) }, @@ -670,16 +674,12 @@ export const featureToolHandlers: Record<string, ToolHandler> = { 'createFeatureVariation', validatedArgs, async (authToken, projectKey) => { - const variationData = { - key: validatedArgs.key, - name: validatedArgs.name, - variables: validatedArgs.variables, - } + const { feature_key, ...variationData } = validatedArgs return await createVariation( authToken, projectKey, - validatedArgs.feature_key, + feature_key, variationData, ) }, @@ -695,17 +695,14 @@ export const featureToolHandlers: Record<string, ToolHandler> = { 'updateFeatureVariation', validatedArgs, async (authToken, projectKey) => { - const variationData = { - key: validatedArgs.key, - name: validatedArgs.name, - variables: validatedArgs.variables, - } + const { feature_key, variation_key, ...variationData } = + validatedArgs return await updateVariation( authToken, projectKey, - validatedArgs.feature_key, - validatedArgs.variation_key, + feature_key, + variation_key, variationData, ) }, @@ -772,15 +769,15 @@ export const featureToolHandlers: Record<string, ToolHandler> = { 'updateFeatureTargeting', validatedArgs, async (authToken, projectKey) => { + const { feature_key, environment_key, ...configData } = + validatedArgs + return await updateFeatureConfigForEnvironment( authToken, projectKey, - validatedArgs.feature_key, - validatedArgs.environment_key, - { - status: validatedArgs.status, - targets: validatedArgs.targets, - }, + feature_key, + environment_key, + configData, ) }, ) diff --git a/src/mcp/tools/projectTools.ts b/src/mcp/tools/projectTools.ts index 2139b36a7..c3522f405 100644 --- a/src/mcp/tools/projectTools.ts +++ b/src/mcp/tools/projectTools.ts @@ -11,7 +11,7 @@ export const projectToolDefinitions: Tool[] = [ inputSchema: { type: 'object', properties: { - sort_by: { + sortBy: { type: 'string', enum: [ 'createdAt', @@ -23,7 +23,7 @@ export const projectToolDefinitions: Tool[] = [ ], description: 'Field to sort by (default: createdAt)', }, - sort_order: { + sortOrder: { type: 'string', enum: ['asc', 'desc'], description: 'Sort order (default: desc)', @@ -33,7 +33,7 @@ export const projectToolDefinitions: Tool[] = [ description: 'Search query to filter projects (minimum 3 characters)', }, - created_by: { + createdBy: { type: 'string', description: 'Filter by creator user ID', }, @@ -41,7 +41,7 @@ export const projectToolDefinitions: Tool[] = [ type: 'number', description: 'Page number (default: 1)', }, - per_page: { + perPage: { type: 'number', description: 'Number of items per page (default: 100, max: 1000)', @@ -67,14 +67,7 @@ export const projectToolHandlers: Record<string, ToolHandler> = { 'listProjects', validatedArgs, async (authToken) => { - return await fetchProjects(authToken, { - sortBy: validatedArgs.sort_by, - sortOrder: validatedArgs.sort_order, - search: validatedArgs.search, - createdBy: validatedArgs.created_by, - page: validatedArgs.page, - perPage: validatedArgs.per_page, - }) + return await fetchProjects(authToken, validatedArgs) }, false, ) diff --git a/src/mcp/tools/variableTools.ts b/src/mcp/tools/variableTools.ts index c7e727a93..6602752d0 100644 --- a/src/mcp/tools/variableTools.ts +++ b/src/mcp/tools/variableTools.ts @@ -98,13 +98,6 @@ export const variableToolDefinitions: Tool[] = [ }, }, }, - tags: { - type: 'array', - items: { - type: 'string', - }, - description: 'Tags to organize variables', - }, }, required: ['key', 'type'], }, @@ -128,11 +121,6 @@ export const variableToolDefinitions: Tool[] = [ description: 'Updated variable description (max 1000 characters)', }, - new_key: { - type: 'string', - description: - 'New variable key (1-100 characters, must match pattern ^[a-z0-9-_.]+$)', - }, type: { type: 'string', enum: ['String', 'Boolean', 'Number', 'JSON'], @@ -167,18 +155,6 @@ export const variableToolDefinitions: Tool[] = [ }, }, }, - persistent: { - type: 'boolean', - description: - 'Whether the variable is intended to be long-lived within a feature', - }, - tags: { - type: 'array', - items: { - type: 'string', - }, - description: 'Tags to organize variables', - }, }, required: ['key'], }, @@ -207,12 +183,11 @@ export const variableToolHandlers: Record<string, ToolHandler> = { 'listVariables', validatedArgs, async (authToken, projectKey) => { - const query = { - search: validatedArgs.search, - page: validatedArgs.page, - perPage: validatedArgs.per_page, - } - return await fetchVariables(authToken, projectKey, query) + return await fetchVariables( + authToken, + projectKey, + validatedArgs, + ) }, ) }, @@ -223,17 +198,11 @@ export const variableToolHandlers: Record<string, ToolHandler> = { 'createVariable', validatedArgs, async (authToken, projectKey) => { - const params = { - name: validatedArgs.name, - description: validatedArgs.description, - key: validatedArgs.key, - _feature: validatedArgs._feature, - type: validatedArgs.type, - defaultValue: validatedArgs.defaultValue, - validationSchema: validatedArgs.validationSchema, - tags: validatedArgs.tags, - } - return await createVariable(authToken, projectKey, params) + return await createVariable( + authToken, + projectKey, + validatedArgs, + ) }, ) }, @@ -244,20 +213,13 @@ export const variableToolHandlers: Record<string, ToolHandler> = { 'updateVariable', validatedArgs, async (authToken, projectKey) => { - const params = { - name: validatedArgs.name, - description: validatedArgs.description, - key: validatedArgs.new_key, - type: validatedArgs.type, - validationSchema: validatedArgs.validationSchema, - persistent: validatedArgs.persistent, - tags: validatedArgs.tags, - } + const { key, ...updateData } = validatedArgs + return await updateVariable( authToken, projectKey, - validatedArgs.key, - params, + key, + updateData, ) }, ) diff --git a/src/mcp/types.ts b/src/mcp/types.ts index 2d889dbf3..05638549f 100644 --- a/src/mcp/types.ts +++ b/src/mcp/types.ts @@ -5,62 +5,22 @@ import { schemas } from '../api/zodClient' export const ListFeaturesArgsSchema = z.object({ search: z.string().optional(), page: z.number().min(1).optional(), - per_page: z.number().min(1).max(1000).default(100).optional(), + perPage: z.number().min(1).max(1000).default(100).optional(), }) export const ListVariablesArgsSchema = z.object({ search: z.string().optional(), page: z.number().min(1).optional(), - per_page: z.number().min(1).max(1000).default(100).optional(), + perPage: z.number().min(1).max(1000).default(100).optional(), }) -export const CreateVariableArgsSchema = z.object({ - name: z.string().min(1).max(100).optional(), - description: z.string().max(1000).optional(), - key: z - .string() - .min(1) - .max(100) - .regex(/^[a-z0-9-_.]+$/), - _feature: z.string().optional(), - type: z.enum(['String', 'Boolean', 'Number', 'JSON']), - defaultValue: z.any().optional(), - validationSchema: z - .object({ - schemaType: z.string(), - enumValues: z.array(z.any()).optional(), - regexPattern: z.string().optional(), - jsonSchema: z.string().optional(), - description: z.string(), - exampleValue: z.any(), - }) - .optional(), - tags: z.array(z.string()).optional(), -}) +export const CreateVariableArgsSchema = schemas.CreateVariableDto -export const UpdateVariableArgsSchema = z.object({ - key: z.string(), - name: z.string().min(1).max(100).optional(), - description: z.string().max(1000).optional(), - new_key: z +export const UpdateVariableArgsSchema = schemas.UpdateVariableDto.extend({ + key: z .string() - .min(1) .max(100) - .regex(/^[a-z0-9-_.]+$/) - .optional(), - type: z.enum(['String', 'Boolean', 'Number', 'JSON']).optional(), - validationSchema: z - .object({ - schemaType: z.string(), - enumValues: z.array(z.any()).optional(), - regexPattern: z.string().optional(), - jsonSchema: z.string().optional(), - description: z.string(), - exampleValue: z.any(), - }) - .optional(), - persistent: z.boolean().optional(), - tags: z.array(z.string()).optional(), + .regex(/^[a-z0-9-_.]+$/), // Make key required for identifying the variable }) export const DeleteVariableArgsSchema = z.object({ @@ -68,7 +28,7 @@ export const DeleteVariableArgsSchema = z.object({ }) export const ListProjectsArgsSchema = z.object({ - sort_by: z + sortBy: z .enum([ 'createdAt', 'updatedAt', @@ -78,18 +38,18 @@ export const ListProjectsArgsSchema = z.object({ 'propertyKey', ]) .optional(), - sort_order: z.enum(['asc', 'desc']).optional(), + sortOrder: z.enum(['asc', 'desc']).optional(), search: z.string().min(3).optional(), - created_by: z.string().optional(), + createdBy: z.string().optional(), page: z.number().min(1).optional(), - per_page: z.number().min(1).max(1000).default(100).optional(), + perPage: z.number().min(1).max(1000).default(100).optional(), }) export const ListEnvironmentsArgsSchema = z.object({ search: z.string().min(3).optional(), page: z.number().min(1).optional(), - per_page: z.number().min(1).max(1000).default(100).optional(), - sort_by: z + perPage: z.number().min(1).max(1000).default(100).optional(), + sortBy: z .enum([ 'createdAt', 'updatedAt', @@ -99,13 +59,13 @@ export const ListEnvironmentsArgsSchema = z.object({ 'propertyKey', ]) .optional(), - sort_order: z.enum(['asc', 'desc']).optional(), - created_by: z.string().optional(), + sortOrder: z.enum(['asc', 'desc']).optional(), + createdBy: z.string().optional(), }) export const GetSdkKeysArgsSchema = z.object({ - environment_key: z.string(), - key_type: z.enum(['mobile', 'server', 'client']).optional(), + environmentKey: z.string(), + keyType: z.enum(['mobile', 'server', 'client']).optional(), }) export const EnableTargetingArgsSchema = z.object({ @@ -148,110 +108,33 @@ export const ListVariationsArgsSchema = z.object({ feature_key: z.string(), }) -const VariationVariablesSchema = z - .record( - z.union([z.string(), z.number(), z.boolean(), z.record(z.unknown())]), - ) - .optional() - -export const CreateVariationArgsSchema = z.object({ - feature_key: z.string(), - key: z - .string() - .max(100) - .regex(/^[a-z0-9-_.]+$/), - name: z.string().max(100), - variables: VariationVariablesSchema, +export const CreateVariationArgsSchema = schemas.CreateVariationDto.extend({ + feature_key: z.string(), // MCP-specific: identifies which feature to add variation to }) -export const UpdateVariationArgsSchema = z.object({ - feature_key: z.string(), - variation_key: z.string(), - key: z - .string() - .max(100) - .regex(/^[a-z0-9-_.]+$/) - .optional(), - name: z.string().max(100).optional(), - variables: VariationVariablesSchema, -}) +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 = z.object({ - feature_key: z.string(), - environment_key: z.string(), - status: z.enum(['active', 'inactive', 'archived']).optional(), - targets: z - .array( - z.object({ - _id: z.string().optional(), - name: z.string().optional(), - audience: z.object({ - name: z.string().max(100).optional(), - filters: z.object({ - filters: z.array(z.any()), - operator: z.enum(['and', 'or']), - }), - }), - rollout: z - .object({ - startPercentage: z.number().gte(0).lte(1).optional(), - type: z.enum(['schedule', 'gradual', 'stepped']), - startDate: z.string().datetime(), - stages: z - .array( - z.object({ - percentage: z.number().gte(0).lte(1), - type: z.enum(['linear', 'discrete']), - date: z.string().datetime(), - }), - ) - .optional(), - }) - .optional(), - distribution: z.array( - z.object({ - percentage: z.number().gte(0).lte(1), - _variation: z.string(), - }), - ), - }), - ) - .optional(), -}) - -// Type inference helpers -export type ListFeaturesArgs = z.infer<typeof ListFeaturesArgsSchema> -export type ListVariablesArgs = z.infer<typeof ListVariablesArgsSchema> -export type CreateVariableArgs = z.infer<typeof CreateVariableArgsSchema> -export type UpdateVariableArgs = z.infer<typeof UpdateVariableArgsSchema> -export type DeleteVariableArgs = z.infer<typeof DeleteVariableArgsSchema> -export type ListProjectsArgs = z.infer<typeof ListProjectsArgsSchema> -export type ListEnvironmentsArgs = z.infer<typeof ListEnvironmentsArgsSchema> -export type GetSdkKeysArgs = z.infer<typeof GetSdkKeysArgsSchema> -export type EnableTargetingArgs = z.infer<typeof EnableTargetingArgsSchema> -export type DisableTargetingArgs = z.infer<typeof DisableTargetingArgsSchema> -export type CreateFeatureArgs = z.infer<typeof CreateFeatureArgsSchema> -export type UpdateFeatureArgs = z.infer<typeof UpdateFeatureArgsSchema> -export type ListVariationsArgs = z.infer<typeof ListVariationsArgsSchema> -export type CreateVariationArgs = z.infer<typeof CreateVariationArgsSchema> -export type UpdateVariationArgs = z.infer<typeof UpdateVariationArgsSchema> -export type UpdateSelfTargetingIdentityArgs = z.infer< - typeof UpdateSelfTargetingIdentityArgsSchema -> -export type SetSelfTargetingOverrideArgs = z.infer< - typeof SetSelfTargetingOverrideArgsSchema -> -export type ClearSelfTargetingOverridesArgs = z.infer< - typeof ClearSelfTargetingOverridesArgsSchema -> -export type ListFeatureTargetingArgs = z.infer< - typeof ListFeatureTargetingArgsSchema -> -export type UpdateFeatureTargetingArgs = z.infer< - typeof UpdateFeatureTargetingArgsSchema -> +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 + }) From d12ddaaf3ea1f12e5056afb1fcedf49e06c73de7 Mon Sep 17 00:00:00 2001 From: Jonathan Norris <jonathan@taplytics.com> Date: Tue, 8 Jul 2025 22:36:16 -0400 Subject: [PATCH 30/44] feat: implement delete_feature tool --- src/mcp/tools/featureTools.ts | 37 ++++++++++++++++++++++++++++++++++- src/mcp/types.ts | 4 ++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/src/mcp/tools/featureTools.ts b/src/mcp/tools/featureTools.ts index debf9ddcf..30fa99a93 100644 --- a/src/mcp/tools/featureTools.ts +++ b/src/mcp/tools/featureTools.ts @@ -1,6 +1,11 @@ import { Tool } from '@modelcontextprotocol/sdk/types.js' import { DevCycleApiClient } from '../utils/api' -import { fetchFeatures, createFeature, updateFeature } from '../../api/features' +import { + fetchFeatures, + createFeature, + updateFeature, + deleteFeature, +} from '../../api/features' import { fetchVariations, createVariation, @@ -16,6 +21,7 @@ import { ListFeaturesArgsSchema, CreateFeatureArgsSchema, UpdateFeatureArgsSchema, + DeleteFeatureArgsSchema, EnableTargetingArgsSchema, DisableTargetingArgsSchema, ListVariationsArgsSchema, @@ -289,6 +295,20 @@ export const featureToolDefinitions: Tool[] = [ required: ['key'], }, }, + { + name: 'delete_feature', + description: 'Delete an existing feature flag', + inputSchema: { + type: 'object', + properties: { + key: { + type: 'string', + description: 'The key of the feature to delete', + }, + }, + required: ['key'], + }, + }, { name: 'fetch_feature_variations', description: 'Get a list of variations for a feature', @@ -646,6 +666,21 @@ export const featureToolHandlers: Record<string, ToolHandler> = { }, ) }, + delete_feature: async (args: unknown, apiClient: DevCycleApiClient) => { + const validatedArgs = DeleteFeatureArgsSchema.parse(args) + + return await apiClient.executeWithLogging( + 'deleteFeature', + validatedArgs, + async (authToken, projectKey) => { + return await deleteFeature( + authToken, + projectKey, + validatedArgs.key, + ) + }, + ) + }, fetch_feature_variations: async ( args: unknown, apiClient: DevCycleApiClient, diff --git a/src/mcp/types.ts b/src/mcp/types.ts index 05638549f..565da21eb 100644 --- a/src/mcp/types.ts +++ b/src/mcp/types.ts @@ -27,6 +27,10 @@ export const DeleteVariableArgsSchema = z.object({ key: z.string(), }) +export const DeleteFeatureArgsSchema = z.object({ + key: z.string(), +}) + export const ListProjectsArgsSchema = z.object({ sortBy: z .enum([ From cbe4dfe8b0132375129fe7d9b8e4d686a6bb5222 Mon Sep 17 00:00:00 2001 From: Jonathan Norris <jonathan@taplytics.com> Date: Tue, 8 Jul 2025 22:42:10 -0400 Subject: [PATCH 31/44] feat: cleanup featureTools.ts --- src/mcp/tools/featureTools.ts | 407 ++++++++++++++-------------------- 1 file changed, 167 insertions(+), 240 deletions(-) diff --git a/src/mcp/tools/featureTools.ts b/src/mcp/tools/featureTools.ts index 30fa99a93..6377a40aa 100644 --- a/src/mcp/tools/featureTools.ts +++ b/src/mcp/tools/featureTools.ts @@ -32,27 +32,142 @@ import { } from '../types' import { ToolHandler } from '../server' +// Reusable schema components +const FEATURE_KEY_PROPERTY = { + type: 'string' as const, + description: 'The key of the feature', +} + +const ENVIRONMENT_KEY_PROPERTY = { + type: 'string' as const, + description: 'The key of the environment', +} + +const ENVIRONMENT_KEY_OPTIONAL_PROPERTY = { + type: 'string' as const, + description: + 'The key of the environment (optional - if not provided, returns all environments)', +} + +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 CONTROL_VARIATION_PROPERTY = { + type: 'string' as const, + description: + 'The key of the variation that is used as the control variation for Metrics', +} + +const FEATURE_SETTINGS_PROPERTY = { + type: 'object' as const, + 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', + }, + }, + description: 'Feature-level settings (all properties required if provided)', + required: ['publicName', 'publicDescription', 'optInEnabled'] as const, +} + +const SDK_VISIBILITY_PROPERTY = { + type: 'object' as const, + properties: { + mobile: { + type: 'boolean' as const, + description: 'Whether the feature is visible to mobile SDKs', + }, + client: { + type: 'boolean' as const, + description: 'Whether the feature is visible to client SDKs', + }, + server: { + type: 'boolean' as const, + description: 'Whether the feature is visible to server SDKs', + }, + }, + description: + 'SDK Type Visibility Settings (all properties required if provided)', + 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_KEY_PROPERTY = { + type: 'string' as const, + description: + 'Unique variation key (max 100 characters, pattern: ^[a-z0-9-_.]+$)', +} + +const VARIATION_NAME_PROPERTY = { + type: 'string' as const, + description: 'Human-readable variation name (max 100 characters)', +} + +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, +} + +const PAGINATION_PROPERTIES = { + search: { + type: 'string' as const, + description: 'Search query to filter features', + }, + page: { + type: 'number' as const, + description: 'Page number (default: 1)', + }, + per_page: { + type: 'number' as const, + description: 'Number of items per page (default: 100, max: 1000)', + }, +} + +const FEATURE_ENVIRONMENT_REQUIRED_PROPERTIES = { + feature_key: FEATURE_KEY_PROPERTY, + environment_key: ENVIRONMENT_KEY_PROPERTY, +} + export const featureToolDefinitions: Tool[] = [ { name: 'list_features', description: 'List features in the current project', inputSchema: { type: 'object', - properties: { - search: { - type: 'string', - description: 'Search query to filter features', - }, - page: { - type: 'number', - description: 'Page number (default: 1)', - }, - per_page: { - type: 'number', - description: - 'Number of items per page (default: 100, max: 1000)', - }, - }, + properties: PAGINATION_PROPERTIES, }, }, { @@ -66,20 +181,9 @@ export const featureToolDefinitions: Tool[] = [ description: 'Unique feature key (max 100 characters, pattern: ^[a-z0-9-_.]+$)', }, - name: { - type: 'string', - description: - 'Human-readable feature name (max 100 characters)', - }, - description: { - type: 'string', - description: 'Feature description (max 1000 characters)', - }, - type: { - type: 'string', - enum: ['release', 'experiment', 'permission', 'ops'], - description: 'Feature type', - }, + name: FEATURE_NAME_PROPERTY, + description: FEATURE_DESCRIPTION_PROPERTY, + type: FEATURE_TYPE_PROPERTY, tags: { type: 'array', items: { @@ -87,70 +191,10 @@ export const featureToolDefinitions: Tool[] = [ }, description: 'Tags to organize features', }, - controlVariation: { - type: 'string', - description: - 'The key of the variation that is used as the control variation for Metrics', - }, - settings: { - type: 'object', - properties: { - publicName: { - type: 'string', - description: - 'Public name for the feature (max 100 characters)', - }, - publicDescription: { - type: 'string', - description: - 'Public description for the feature (max 1000 characters)', - }, - optInEnabled: { - type: 'boolean', - description: - 'Whether opt-in is enabled for the feature', - }, - }, - description: - 'Feature-level settings (all properties required if provided)', - required: [ - 'publicName', - 'publicDescription', - 'optInEnabled', - ], - }, - sdkVisibility: { - type: 'object', - properties: { - mobile: { - type: 'boolean', - description: - 'Whether the feature is visible to mobile SDKs', - }, - client: { - type: 'boolean', - description: - 'Whether the feature is visible to client SDKs', - }, - server: { - type: 'boolean', - description: - 'Whether the feature is visible to server SDKs', - }, - }, - description: - 'SDK Type Visibility Settings (all properties required if provided)', - required: ['mobile', 'client', 'server'], - }, - variables: { - type: 'array', - description: - 'Array of variables to create or reassociate with this feature', - items: { - type: 'object', - description: 'Variable creation or reassociation data', - }, - }, + 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', @@ -197,20 +241,9 @@ export const featureToolDefinitions: Tool[] = [ description: 'The key of the feature to update(1-100 characters, must match pattern ^[a-z0-9-_.]+$)', }, - name: { - type: 'string', - description: - 'Human-readable feature name (max 100 characters)', - }, - description: { - type: 'string', - description: 'Feature description (max 1000 characters)', - }, - type: { - type: 'string', - enum: ['release', 'experiment', 'permission', 'ops'], - description: 'Feature type', - }, + name: FEATURE_NAME_PROPERTY, + description: FEATURE_DESCRIPTION_PROPERTY, + type: FEATURE_TYPE_PROPERTY, tags: { type: 'array', items: { @@ -218,70 +251,10 @@ export const featureToolDefinitions: Tool[] = [ }, description: 'Tags to organize Features on the dashboard', }, - controlVariation: { - type: 'string', - description: - 'The key of the variation that is used as the control variation for Metrics', - }, - settings: { - type: 'object', - properties: { - publicName: { - type: 'string', - description: - 'Public name for the feature (max 100 characters)', - }, - publicDescription: { - type: 'string', - description: - 'Public description for the feature (max 1000 characters)', - }, - optInEnabled: { - type: 'boolean', - description: - 'Whether opt-in is enabled for the feature', - }, - }, - description: - 'Feature-level settings (all properties required if provided)', - required: [ - 'publicName', - 'publicDescription', - 'optInEnabled', - ], - }, - sdkVisibility: { - type: 'object', - properties: { - mobile: { - type: 'boolean', - description: - 'Whether the feature is visible to mobile SDKs', - }, - client: { - type: 'boolean', - description: - 'Whether the feature is visible to client SDKs', - }, - server: { - type: 'boolean', - description: - 'Whether the feature is visible to server SDKs', - }, - }, - description: - 'SDK Type Visibility Settings (all properties required if provided)', - required: ['mobile', 'client', 'server'], - }, - variables: { - type: 'array', - description: - 'Array of variables to create or reassociate with this feature', - items: { - type: 'object', - description: 'Variable creation or reassociation data', - }, - }, + 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', @@ -315,10 +288,7 @@ export const featureToolDefinitions: Tool[] = [ inputSchema: { type: 'object', properties: { - feature_key: { - type: 'string', - description: 'The key of the feature', - }, + feature_key: FEATURE_KEY_PROPERTY, }, required: ['feature_key'], }, @@ -329,20 +299,9 @@ export const featureToolDefinitions: Tool[] = [ inputSchema: { type: 'object', properties: { - feature_key: { - type: 'string', - description: 'The key of the feature', - }, - key: { - type: 'string', - description: - 'Unique variation key (max 100 characters, pattern: ^[a-z0-9-_.]+$)', - }, - name: { - type: 'string', - description: - 'Human-readable variation name (max 100 characters)', - }, + feature_key: FEATURE_KEY_PROPERTY, + key: VARIATION_KEY_PROPERTY, + name: VARIATION_NAME_PROPERTY, variables: { type: 'object', description: @@ -359,10 +318,7 @@ export const featureToolDefinitions: Tool[] = [ inputSchema: { type: 'object', properties: { - feature_key: { - type: 'string', - description: 'The key of the feature', - }, + feature_key: FEATURE_KEY_PROPERTY, variation_key: { type: 'string', description: 'The key of the variation to update', @@ -376,12 +332,7 @@ export const featureToolDefinitions: Tool[] = [ type: 'string', description: 'New variation name (max 100 characters)', }, - variables: { - type: 'object', - description: - 'Key-value map of variable keys to their values for this variation (supports string, number, boolean, and object values)', - additionalProperties: true, - }, + variables: VARIATION_VARIABLES_PROPERTY, _id: { type: 'string', description: 'Internal variation ID (optional)', @@ -391,38 +342,20 @@ export const featureToolDefinitions: Tool[] = [ }, }, { - name: 'enable_targeting', + name: 'enable_feature_targeting', description: 'Enable targeting for a feature in an environment', inputSchema: { type: 'object', - properties: { - feature_key: { - type: 'string', - description: 'The key of the feature', - }, - environment_key: { - type: 'string', - description: 'The key of the environment', - }, - }, + properties: FEATURE_ENVIRONMENT_REQUIRED_PROPERTIES, required: ['feature_key', 'environment_key'], }, }, { - name: 'disable_targeting', + name: 'disable_feature_targeting', description: 'Disable targeting for a feature in an environment', inputSchema: { type: 'object', - properties: { - feature_key: { - type: 'string', - description: 'The key of the feature', - }, - environment_key: { - type: 'string', - description: 'The key of the environment', - }, - }, + properties: FEATURE_ENVIRONMENT_REQUIRED_PROPERTIES, required: ['feature_key', 'environment_key'], }, }, @@ -433,15 +366,8 @@ export const featureToolDefinitions: Tool[] = [ inputSchema: { type: 'object', properties: { - feature_key: { - type: 'string', - description: 'The key of the feature', - }, - environment_key: { - type: 'string', - description: - 'The key of the environment (optional - if not provided, returns all environments)', - }, + feature_key: FEATURE_KEY_PROPERTY, + environment_key: ENVIRONMENT_KEY_OPTIONAL_PROPERTY, }, required: ['feature_key'], }, @@ -453,14 +379,8 @@ export const featureToolDefinitions: Tool[] = [ inputSchema: { type: 'object', properties: { - feature_key: { - type: 'string', - description: 'The key of the feature', - }, - environment_key: { - type: 'string', - description: 'The key of the environment', - }, + feature_key: FEATURE_KEY_PROPERTY, + environment_key: ENVIRONMENT_KEY_PROPERTY, status: { type: 'string', enum: ['active', 'inactive', 'archived'], @@ -642,6 +562,7 @@ export const featureToolHandlers: Record<string, ToolHandler> = { } // 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 createFeature(authToken, projectKey, featureData) @@ -743,7 +664,10 @@ export const featureToolHandlers: Record<string, ToolHandler> = { }, ) }, - enable_targeting: async (args: unknown, apiClient: DevCycleApiClient) => { + enable_feature_targeting: async ( + args: unknown, + apiClient: DevCycleApiClient, + ) => { const validatedArgs = EnableTargetingArgsSchema.parse(args) return await apiClient.executeWithLogging( @@ -759,7 +683,10 @@ export const featureToolHandlers: Record<string, ToolHandler> = { }, ) }, - disable_targeting: async (args: unknown, apiClient: DevCycleApiClient) => { + disable_feature_targeting: async ( + args: unknown, + apiClient: DevCycleApiClient, + ) => { const validatedArgs = DisableTargetingArgsSchema.parse(args) return await apiClient.executeWithLogging( From 651c2162ba8378787f4cd9a99b481a907ded5077 Mon Sep 17 00:00:00 2001 From: Jonathan Norris <jonathan@taplytics.com> Date: Tue, 8 Jul 2025 23:31:16 -0400 Subject: [PATCH 32/44] feat: add get_feature_audit_log_history tool call --- src/api/features.ts | 83 +++++++++++++++++++++++++++++++++++ src/mcp/tools/featureTools.ts | 40 +++++++++++++++++ src/mcp/types.ts | 5 +++ 3 files changed, 128 insertions(+) diff --git a/src/api/features.ts b/src/api/features.ts index 2c242b8d7..8b7425b82 100644 --- a/src/api/features.ts +++ b/src/api/features.ts @@ -156,3 +156,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/mcp/tools/featureTools.ts b/src/mcp/tools/featureTools.ts index 6377a40aa..8b7664ddd 100644 --- a/src/mcp/tools/featureTools.ts +++ b/src/mcp/tools/featureTools.ts @@ -5,6 +5,7 @@ import { createFeature, updateFeature, deleteFeature, + getFeatureAuditLogHistory, } from '../../api/features' import { fetchVariations, @@ -29,6 +30,7 @@ import { UpdateVariationArgsSchema, ListFeatureTargetingArgsSchema, UpdateFeatureTargetingArgsSchema, + GetFeatureAuditLogHistoryArgsSchema, } from '../types' import { ToolHandler } from '../server' @@ -528,6 +530,25 @@ export const featureToolDefinitions: Tool[] = [ required: ['feature_key', 'environment_key'], }, }, + { + name: 'get_feature_audit_log_history', + description: + 'Get timeline of feature flag changes from DevCycle audit log', + 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'], + }, + }, ] export const featureToolHandlers: Record<string, ToolHandler> = { @@ -744,4 +765,23 @@ export const featureToolHandlers: Record<string, ToolHandler> = { }, ) }, + get_feature_audit_log_history: async ( + args: unknown, + apiClient: DevCycleApiClient, + ) => { + const validatedArgs = GetFeatureAuditLogHistoryArgsSchema.parse(args) + + return await apiClient.executeWithLogging( + 'getFeatureAuditLogHistory', + validatedArgs, + async (authToken, projectKey) => { + return await getFeatureAuditLogHistory( + authToken, + projectKey, + validatedArgs.feature_key, + validatedArgs.days_back || 30, + ) + }, + ) + }, } diff --git a/src/mcp/types.ts b/src/mcp/types.ts index 565da21eb..00d1c6e43 100644 --- a/src/mcp/types.ts +++ b/src/mcp/types.ts @@ -142,3 +142,8 @@ export const UpdateFeatureTargetingArgsSchema = 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(), +}) From 6da772f06e3e2719d1779c2411f386265870a19c Mon Sep 17 00:00:00 2001 From: Jonathan Norris <jonathan@taplytics.com> Date: Tue, 8 Jul 2025 23:46:29 -0400 Subject: [PATCH 33/44] feat: try updating tool descriptions --- src/mcp/tools/featureTools.ts | 18 ++++++++++++------ src/mcp/tools/selfTargetingTools.ts | 5 +++-- src/mcp/tools/variableTools.ts | 9 ++++++--- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/src/mcp/tools/featureTools.ts b/src/mcp/tools/featureTools.ts index 8b7664ddd..fbc26b4c0 100644 --- a/src/mcp/tools/featureTools.ts +++ b/src/mcp/tools/featureTools.ts @@ -18,6 +18,7 @@ import { fetchTargetingForFeature, updateFeatureConfigForEnvironment, } from '../../api/targeting' + import { ListFeaturesArgsSchema, CreateFeatureArgsSchema, @@ -174,7 +175,8 @@ export const featureToolDefinitions: Tool[] = [ }, { name: 'create_feature', - description: 'Create a new feature flag (supports interactive mode)', + description: + 'Create a new feature flag (supports interactive mode). ⚠️ IMPORTANT: If creating configurations for production environments, always confirm with the user before proceeding.', inputSchema: { type: 'object', properties: { @@ -234,7 +236,8 @@ export const featureToolDefinitions: Tool[] = [ }, { name: 'update_feature', - description: 'Update an existing feature flag', + 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.', inputSchema: { type: 'object', properties: { @@ -272,7 +275,8 @@ export const featureToolDefinitions: Tool[] = [ }, { name: 'delete_feature', - description: 'Delete an existing feature flag', + 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.', inputSchema: { type: 'object', properties: { @@ -345,7 +349,8 @@ export const featureToolDefinitions: Tool[] = [ }, { name: 'enable_feature_targeting', - description: 'Enable targeting for a feature in an environment', + 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").', inputSchema: { type: 'object', properties: FEATURE_ENVIRONMENT_REQUIRED_PROPERTIES, @@ -354,7 +359,8 @@ export const featureToolDefinitions: Tool[] = [ }, { name: 'disable_feature_targeting', - description: 'Disable targeting for a feature in an environment', + 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").', inputSchema: { type: 'object', properties: FEATURE_ENVIRONMENT_REQUIRED_PROPERTIES, @@ -377,7 +383,7 @@ export const featureToolDefinitions: Tool[] = [ { name: 'update_feature_targeting', description: - 'Update feature configuration (targeting rules) for a feature in an environment', + '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").', inputSchema: { type: 'object', properties: { diff --git a/src/mcp/tools/selfTargetingTools.ts b/src/mcp/tools/selfTargetingTools.ts index 7f01ea9bd..23d54a6a6 100644 --- a/src/mcp/tools/selfTargetingTools.ts +++ b/src/mcp/tools/selfTargetingTools.ts @@ -50,7 +50,8 @@ export const selfTargetingToolDefinitions: Tool[] = [ }, { name: 'set_self_targeting_override', - description: 'Set a self-targeting override for a feature variation', + 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").', inputSchema: { type: 'object', properties: { @@ -73,7 +74,7 @@ export const selfTargetingToolDefinitions: Tool[] = [ { name: 'clear_feature_self_targeting_overrides', description: - 'Clear self-targeting overrides for a specific feature/environment', + '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").', inputSchema: { type: 'object', properties: { diff --git a/src/mcp/tools/variableTools.ts b/src/mcp/tools/variableTools.ts index 6602752d0..9b5e77f49 100644 --- a/src/mcp/tools/variableTools.ts +++ b/src/mcp/tools/variableTools.ts @@ -39,7 +39,8 @@ export const variableToolDefinitions: Tool[] = [ }, { name: 'create_variable', - description: 'Create a new variable', + description: + 'Create a new variable. ⚠️ IMPORTANT: Variables can affect feature flags in production environments. Always confirm with the user before creating variables for features that are active in production.', inputSchema: { type: 'object', properties: { @@ -104,7 +105,8 @@ export const variableToolDefinitions: Tool[] = [ }, { name: 'update_variable', - description: 'Update an existing 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.', inputSchema: { type: 'object', properties: { @@ -161,7 +163,8 @@ export const variableToolDefinitions: Tool[] = [ }, { name: 'delete_variable', - description: 'Delete a 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.', inputSchema: { type: 'object', properties: { From e5ffa671e2db1ab71748dd93c4dd89420c8c9681 Mon Sep 17 00:00:00 2001 From: Jonathan Norris <jonathan@taplytics.com> Date: Wed, 9 Jul 2025 13:32:07 -0400 Subject: [PATCH 34/44] feat: add create_environment and update_environment tools --- src/mcp/tools/environmentTools.ts | 174 +++++++++++++++++++++--------- src/mcp/types.ts | 6 ++ 2 files changed, 132 insertions(+), 48 deletions(-) diff --git a/src/mcp/tools/environmentTools.ts b/src/mcp/tools/environmentTools.ts index c2e618367..a71fd4e8f 100644 --- a/src/mcp/tools/environmentTools.ts +++ b/src/mcp/tools/environmentTools.ts @@ -3,57 +3,88 @@ import { DevCycleApiClient } from '../utils/api' import { fetchEnvironments, fetchEnvironmentByKey, + createEnvironment, + updateEnvironment, } from '../../api/environments' -import { GetSdkKeysArgsSchema, ListEnvironmentsArgsSchema } from '../types' +import { + GetSdkKeysArgsSchema, + ListEnvironmentsArgsSchema, + CreateEnvironmentArgsSchema, + UpdateEnvironmentArgsSchema, +} from '../types' import { ToolHandler } from '../server' +// Reusable schema components +const ENVIRONMENT_KEY_PROPERTY = { + type: 'string' as const, + description: + "The key of the environment, must be unique and can't be changed after creation", +} + +const PAGINATION_PROPERTIES = { + search: { + type: 'string' as const, + description: + 'Search query to filter environments (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, + description: 'Field to sort by (default: createdAt)', + enum: [ + 'createdAt', + 'updatedAt', + 'name', + 'key', + 'createdBy', + 'propertyKey', + ] as const, + }, + sortOrder: { + type: 'string' as const, + enum: ['asc', 'desc'] as const, + description: 'Sort order (default: desc)', + }, + createdBy: { + type: 'string' as const, + description: 'Filter by creator user ID', + }, +} + +const ENVIRONMENT_COMMON_PROPERTIES = { + key: ENVIRONMENT_KEY_PROPERTY, + name: { + type: 'string' as const, + description: 'The name of the environment', + }, + description: { + type: 'string' as const, + description: 'The description of the environment', + }, + color: { + type: 'string' as const, + description: 'The color for the environment', + }, +} + export const environmentToolDefinitions: Tool[] = [ { name: 'list_environments', description: 'List environments in the current project', inputSchema: { type: 'object', - properties: { - search: { - type: 'string', - description: - 'Search query to filter environments (minimum 3 characters)', - minLength: 3, - }, - page: { - type: 'number', - description: 'Page number (default: 1)', - minimum: 1, - }, - perPage: { - type: 'number', - description: - 'Number of items per page (default: 100, max: 1000)', - minimum: 1, - maximum: 1000, - }, - sortBy: { - type: 'string', - description: 'Field to sort by (default: createdAt)', - enum: [ - 'createdAt', - 'updatedAt', - 'name', - 'key', - 'createdBy', - 'propertyKey', - ], - }, - sortOrder: { - type: 'string', - enum: ['asc', 'desc'], - description: 'Sort order (default: desc)', - }, - createdBy: { - type: 'string', - description: 'Filter by creator user ID', - }, - }, + properties: PAGINATION_PROPERTIES, }, }, { @@ -62,19 +93,34 @@ export const environmentToolDefinitions: Tool[] = [ inputSchema: { type: 'object', properties: { - environmentKey: { - type: 'string', - description: 'The key of the environment', - }, + environmentKey: ENVIRONMENT_KEY_PROPERTY, keyType: { - type: 'string', - enum: ['mobile', 'server', 'client'], + type: 'string' as const, + enum: ['mobile', 'server', 'client'] as const, description: 'The type of SDK key to retrieve', }, }, required: ['environmentKey'], }, }, + { + name: 'create_environment', + description: 'Create a new environment', + inputSchema: { + type: 'object', + properties: ENVIRONMENT_COMMON_PROPERTIES, + required: ['name', 'key'], + }, + }, + { + name: 'update_environment', + description: 'Update an existing environment', + inputSchema: { + type: 'object', + properties: ENVIRONMENT_COMMON_PROPERTIES, + required: ['key'], + }, + }, ] export const environmentToolHandlers: Record<string, ToolHandler> = { @@ -118,4 +164,36 @@ export const environmentToolHandlers: Record<string, ToolHandler> = { }, ) }, + create_environment: async (args: unknown, apiClient: DevCycleApiClient) => { + const validatedArgs = CreateEnvironmentArgsSchema.parse(args) + + return await apiClient.executeWithLogging( + 'createEnvironment', + validatedArgs, + async (authToken, projectKey) => { + return await createEnvironment( + authToken, + projectKey, + validatedArgs, + ) + }, + ) + }, + update_environment: async (args: unknown, apiClient: DevCycleApiClient) => { + const validatedArgs = UpdateEnvironmentArgsSchema.parse(args) + + return await apiClient.executeWithLogging( + 'updateEnvironment', + validatedArgs, + async (authToken, projectKey) => { + const { key, ...updateParams } = validatedArgs + return await updateEnvironment( + authToken, + projectKey, + key, + updateParams, + ) + }, + ) + }, } diff --git a/src/mcp/types.ts b/src/mcp/types.ts index 00d1c6e43..911230218 100644 --- a/src/mcp/types.ts +++ b/src/mcp/types.ts @@ -72,6 +72,12 @@ export const GetSdkKeysArgsSchema = z.object({ 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(), From 0cc17973dc64e68a01d3c7ba3580097c3f2df770 Mon Sep 17 00:00:00 2001 From: Jonathan Norris <jonathan@taplytics.com> Date: Wed, 9 Jul 2025 13:44:53 -0400 Subject: [PATCH 35/44] feat: add create_project tool --- .cursor/rules/typescript-patterns.mdc | 35 ++++++++++++++++++++++ src/api/projects.ts | 2 +- src/mcp/tools/projectTools.ts | 43 +++++++++++++++++++++++++-- src/mcp/types.ts | 10 +++++++ 4 files changed, 87 insertions(+), 3 deletions(-) diff --git a/.cursor/rules/typescript-patterns.mdc b/.cursor/rules/typescript-patterns.mdc index 97c148f71..de149c33e 100644 --- a/.cursor/rules/typescript-patterns.mdc +++ b/.cursor/rules/typescript-patterns.mdc @@ -48,6 +48,41 @@ export default class MyCommand extends Base { - 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 diff --git a/src/api/projects.ts b/src/api/projects.ts index 2569312c3..7913a2536 100644 --- a/src/api/projects.ts +++ b/src/api/projects.ts @@ -9,7 +9,7 @@ export class CreateProjectParams { @IsString() @IsOptional() - description: string + description?: string @IsNotEmpty() @IsString() diff --git a/src/mcp/tools/projectTools.ts b/src/mcp/tools/projectTools.ts index c3522f405..dc1048f9f 100644 --- a/src/mcp/tools/projectTools.ts +++ b/src/mcp/tools/projectTools.ts @@ -1,7 +1,12 @@ import { Tool } from '@modelcontextprotocol/sdk/types.js' import { DevCycleApiClient } from '../utils/api' -import { fetchProjects, fetchProject } from '../../api/projects' -import { ListProjectsArgsSchema } from '../types' +import { + fetchProjects, + fetchProject, + createProject, + CreateProjectParams, +} from '../../api/projects' +import { ListProjectsArgsSchema, CreateProjectArgsSchema } from '../types' import { ToolHandler } from '../server' export const projectToolDefinitions: Tool[] = [ @@ -57,6 +62,28 @@ export const projectToolDefinitions: Tool[] = [ properties: {}, }, }, + { + name: 'create_project', + description: 'Create a new project', + inputSchema: { + type: 'object', + properties: { + name: { + type: 'string', + description: 'Project name', + }, + description: { + type: 'string', + description: 'Project description', + }, + key: { + type: 'string', + description: 'Unique project key', + }, + }, + required: ['name', 'key'], + }, + }, ] export const projectToolHandlers: Record<string, ToolHandler> = { @@ -84,4 +111,16 @@ export const projectToolHandlers: Record<string, ToolHandler> = { }, ) }, + create_project: async (args: unknown, apiClient: DevCycleApiClient) => { + const validatedArgs = CreateProjectArgsSchema.parse(args) + + return await apiClient.executeWithLogging( + 'createProject', + validatedArgs, + async (authToken) => { + return await createProject(authToken, validatedArgs) + }, + false, + ) + }, } diff --git a/src/mcp/types.ts b/src/mcp/types.ts index 911230218..4df9a90e6 100644 --- a/src/mcp/types.ts +++ b/src/mcp/types.ts @@ -49,6 +49,16 @@ export const ListProjectsArgsSchema = z.object({ perPage: z.number().min(1).max(1000).default(100).optional(), }) +export const CreateProjectArgsSchema = z.object({ + key: z + .string() + .min(1) + .max(100) + .regex(/^[a-z0-9-_.]+$/), + name: z.string().min(1), + description: z.string().optional(), +}) + export const ListEnvironmentsArgsSchema = z.object({ search: z.string().min(3).optional(), page: z.number().min(1).optional(), From e741e066fc64e4acc10543949d39a4210edc2864 Mon Sep 17 00:00:00 2001 From: Jonathan Norris <jonathan@taplytics.com> Date: Wed, 9 Jul 2025 13:49:56 -0400 Subject: [PATCH 36/44] chore: revert zodClient.ts changes --- src/api/zodClient.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/api/zodClient.ts b/src/api/zodClient.ts index e49f8ce67..02cdf8535 100644 --- a/src/api/zodClient.ts +++ b/src/api/zodClient.ts @@ -296,12 +296,12 @@ const UpdateAudienceDto = z }) .partial() const VariableValidationEntity = z.object({ - schemaType: z.string(), - enumValues: z.array(z.any()).optional(), + schemaType: z.object({}).partial(), + enumValues: z.object({}).partial().optional(), regexPattern: z.string().optional(), jsonSchema: z.string().optional(), description: z.string(), - exampleValue: z.any(), + exampleValue: z.object({}).partial(), }) const CreateVariableDto = z.object({ name: z.string().max(100).optional(), From 70674bc3758409ace3e3db42f4abb981fe0b6ee2 Mon Sep 17 00:00:00 2001 From: Jonathan Norris <jonathan@taplytics.com> Date: Wed, 9 Jul 2025 14:20:32 -0400 Subject: [PATCH 37/44] feat: fix tests, and update MCP error logging --- README.md | 1 + package.json | 7 ++ src/api/apiClient.ts | 34 ++++++--- src/mcp/server.ts | 22 +++++- src/mcp/utils/api.ts | 14 +++- yarn.lock | 178 ++++++++++++++++++++++++++++++++++++++++++- 6 files changed, 241 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index f63594ba6..13141436c 100644 --- a/README.md +++ b/README.md @@ -126,6 +126,7 @@ USAGE * [`dvc environments`](docs/environments.md) - Create a new Environment for an existing Feature. * [`dvc features`](docs/features.md) - Create, view, or modify Features with the Management API. * [`dvc generate`](docs/generate.md) - Generate Devcycle related files. +* [`dvc help`](docs/help.md) - Display help for dvc. * [`dvc identity`](docs/identity.md) - View or manage your DevCycle Identity. * [`dvc keys`](docs/keys.md) - Retrieve SDK keys from the Management API. * [`dvc login`](docs/login.md) - Log in to DevCycle. diff --git a/package.json b/package.json index e057cdb88..d33244942 100644 --- a/package.json +++ b/package.json @@ -48,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..8e4afd1fc 100644 --- a/src/api/apiClient.ts +++ b/src/api/apiClient.ts @@ -13,12 +13,16 @@ export const setDVCReferrer = ( caller = 'cli', ): void => { axiosClient.defaults.headers.common['dvc-referrer'] = 'cli' + + // 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) { diff --git a/src/mcp/server.ts b/src/mcp/server.ts index dda1a3685..cbbd57113 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -112,13 +112,29 @@ export class DevCycleMCPServer { } catch (error) { console.error(`Error in tool handler ${name}:`, error) - const errorMessage = - error instanceof Error ? error.message : 'Unknown error' + // Safely extract error message, handling undefined/null cases + let errorMessage = 'Unknown error' + if (error instanceof Error && error.message) { + errorMessage = error.message + } else if (error && typeof error === 'string') { + errorMessage = error + } else if (error && typeof error === 'object') { + errorMessage = JSON.stringify(error) + } + + // Return error as JSON to maintain consistent response format + const errorResponse = { + error: true, + message: errorMessage, + tool: name, + timestamp: new Date().toISOString(), + } + return { content: [ { type: 'text', - text: `Error: ${errorMessage}`, + text: JSON.stringify(errorResponse, null, 2), }, ], } diff --git a/src/mcp/utils/api.ts b/src/mcp/utils/api.ts index f00a663e3..84073896a 100644 --- a/src/mcp/utils/api.ts +++ b/src/mcp/utils/api.ts @@ -1,9 +1,21 @@ import { DevCycleAuth } from './auth' function getErrorMessage(error: unknown): string { - if (error instanceof Error) { + 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) } diff --git a/yarn.lock b/yarn.lock index 1a7b09eb8..79e1e073b 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" @@ -189,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" @@ -199,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" @@ -210,7 +324,7 @@ __metadata: languageName: node linkType: hard -"@babel/parser@npm:^7.28.0": +"@babel/parser@npm:^7.27.2, @babel/parser@npm:^7.28.0": version: 7.28.0 resolution: "@babel/parser@npm:7.28.0" dependencies: @@ -395,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" @@ -410,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" @@ -420,7 +560,7 @@ __metadata: languageName: node linkType: hard -"@babel/types@npm:^7.28.0": +"@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: @@ -443,7 +583,14 @@ __metadata: version: 0.0.0-use.local resolution: "@devcycle/cli@workspace:." dependencies: + "@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" @@ -752,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" @@ -784,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" @@ -804,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" From 2ff5451600aa9a1ee7b7597451969b60370b795a Mon Sep 17 00:00:00 2001 From: Jonathan Norris <jonathan@taplytics.com> Date: Wed, 9 Jul 2025 14:35:19 -0400 Subject: [PATCH 38/44] feat: add update_feature_status tool --- src/api/features.ts | 23 +++++++++++++++- src/api/schemas.ts | 7 +++++ src/mcp/tools/featureTools.ts | 49 +++++++++++++++++++++++++++++++++++ src/mcp/types.ts | 8 ++++++ 4 files changed, 86 insertions(+), 1 deletion(-) diff --git a/src/api/features.ts b/src/api/features.ts index 8b7425b82..8c76876ba 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' @@ -83,6 +88,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, diff --git a/src/api/schemas.ts b/src/api/schemas.ts index 9e5be7e58..a8b8ac997 100644 --- a/src/api/schemas.ts +++ b/src/api/schemas.ts @@ -28,6 +28,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/mcp/tools/featureTools.ts b/src/mcp/tools/featureTools.ts index fbc26b4c0..69abf0887 100644 --- a/src/mcp/tools/featureTools.ts +++ b/src/mcp/tools/featureTools.ts @@ -4,6 +4,7 @@ import { fetchFeatures, createFeature, updateFeature, + updateFeatureStatus, deleteFeature, getFeatureAuditLogHistory, } from '../../api/features' @@ -23,6 +24,7 @@ import { ListFeaturesArgsSchema, CreateFeatureArgsSchema, UpdateFeatureArgsSchema, + UpdateFeatureStatusArgsSchema, DeleteFeatureArgsSchema, EnableTargetingArgsSchema, DisableTargetingArgsSchema, @@ -273,6 +275,32 @@ export const featureToolDefinitions: Tool[] = [ required: ['key'], }, }, + { + 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.', + inputSchema: { + type: 'object', + properties: { + key: { + type: 'string', + description: + 'The key of the feature to update status for (1-100 characters, must match pattern ^[a-z0-9-_.]+$)', + }, + status: { + type: 'string', + enum: ['active', 'complete', 'archived'], + description: 'The status to set the feature to', + }, + staticVariation: { + type: 'string', + description: + 'The variation key or ID to serve if the status is set to complete (optional)', + }, + }, + required: ['key', 'status'], + }, + }, { name: 'delete_feature', description: @@ -614,6 +642,27 @@ export const featureToolHandlers: Record<string, ToolHandler> = { }, ) }, + update_feature_status: async ( + args: unknown, + apiClient: DevCycleApiClient, + ) => { + const validatedArgs = UpdateFeatureStatusArgsSchema.parse(args) + + return await apiClient.executeWithLogging( + 'updateFeatureStatus', + validatedArgs, + async (authToken, projectKey) => { + const { key, ...statusData } = validatedArgs + + return await updateFeatureStatus( + authToken, + projectKey, + key, + statusData, + ) + }, + ) + }, delete_feature: async (args: unknown, apiClient: DevCycleApiClient) => { const validatedArgs = DeleteFeatureArgsSchema.parse(args) diff --git a/src/mcp/types.ts b/src/mcp/types.ts index 4df9a90e6..948390944 100644 --- a/src/mcp/types.ts +++ b/src/mcp/types.ts @@ -1,5 +1,6 @@ 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({ @@ -109,6 +110,13 @@ export const UpdateFeatureArgsSchema = schemas.UpdateFeatureDto.extend({ .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(), }) From 2851935742e20ee90e9cdbe65402c6b0f4d32bfd Mon Sep 17 00:00:00 2001 From: Jonathan Norris <jonathan@taplytics.com> Date: Wed, 9 Jul 2025 14:46:34 -0400 Subject: [PATCH 39/44] chore: update mcp docs --- README.md | 36 +++ docs/mcp.md | 510 +++++++++++++++++++++++++++++++++ docs/mcp/HACKATHON_MVP_PLAN.md | 211 -------------- 3 files changed, 546 insertions(+), 211 deletions(-) create mode 100644 docs/mcp.md delete mode 100644 docs/mcp/HACKATHON_MVP_PLAN.md diff --git a/README.md b/README.md index 13141436c..f38903d43 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,7 @@ 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) * [Repo Configuration](#repo-configuration) <!-- tocstop --> # Setup @@ -143,6 +145,40 @@ 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. + +## Quick Setup + +### For Cursor +Add to `.cursor/mcp_settings.json`: +```json +{ + "mcpServers": { + "devcycle": { + "command": "dvc-mcp" + } + } +} +``` + +### For Claude Desktop +Add to your Claude configuration file: +```json +{ + "mcpServers": { + "devcycle": { + "command": "dvc-mcp" + } + } +} +``` + +The MCP server uses the same authentication as the CLI. Simply run `dvc login sso` first, then your AI assistant can manage feature flags on your behalf. + +For detailed documentation, 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/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/HACKATHON_MVP_PLAN.md b/docs/mcp/HACKATHON_MVP_PLAN.md deleted file mode 100644 index 3e8fad0f7..000000000 --- a/docs/mcp/HACKATHON_MVP_PLAN.md +++ /dev/null @@ -1,211 +0,0 @@ -# DevCycle MCP Hackathon Implementation Plan - -## Overview - -This document outlines the implementation plan for 4 core DevCycle MCP tools in priority order. - -## Target Tools - -1. `list_features` / `get_feature` / `list_variables` / `get_variable` -2. `get_sdk_keys` -3. `enable_targeting` / `disable_targeting` -4. `create_feature` (interactive) - -## Implementation Order - -### 1. List/Get Features and Variables - -#### `list_features` -**Implementation Time**: 1.5 hours -**Complexity**: Low -**Files to leverage**: `src/api/features.ts`, `src/commands/features/list.ts` - -```typescript -async listFeatures(args: { - search?: string, - page?: number, - per_page?: number -}) { - // Reuse fetchFeatures from src/api/features.ts - return await this.api.fetchFeatures(args) -} -``` - -#### `get_feature` -**Implementation Time**: 1 hour -**Complexity**: Low -**Files to leverage**: `src/api/features.ts`, `src/commands/features/get.ts` - -```typescript -async getFeature(args: { feature_key: string }) { - // Reuse fetchFeature from src/api/features.ts - return await this.api.fetchFeature(args.feature_key) -} -``` - -#### `list_variables` -**Implementation Time**: 1.5 hours -**Complexity**: Low -**Files to leverage**: `src/api/variables.ts`, `src/commands/variables/list.ts` - -```typescript -async listVariables(args: { - search?: string, - page?: number, - per_page?: number -}) { - // Reuse fetchVariables from src/api/variables.ts - return await this.api.fetchVariables(args) -} -``` - -#### `get_variable` -**Implementation Time**: 1 hour -**Complexity**: Low -**Files to leverage**: `src/api/variables.ts`, `src/commands/variables/get.ts` - -```typescript -async getVariable(args: { variable_key: string }) { - // Reuse fetchVariable from src/api/variables.ts - return await this.api.fetchVariable(args.variable_key) -} -``` - -### 2. Get SDK Keys - -#### `get_sdk_keys` -**Implementation Time**: 2 hours -**Complexity**: Medium -**Files to leverage**: `src/api/environments.ts`, `src/commands/keys/get.ts` - -```typescript -async getSdkKeys(args: { - environment_key: string, - key_type?: 'mobile' | 'server' | 'client' -}) { - // Reuse fetchEnvironment from src/api/environments.ts - // Extract SDK keys based on key_type filter - const env = await this.api.fetchEnvironment(args.environment_key) - return { - mobile: env.sdkKeys?.mobile, - server: env.sdkKeys?.server, - client: env.sdkKeys?.client - } -} -``` - -### 3. Enable/Disable Targeting - -#### `enable_targeting` -**Implementation Time**: 2 hours -**Complexity**: Medium -**Files to leverage**: `src/api/targeting.ts`, `src/commands/targeting/enable.ts` - -```typescript -async enableTargeting(args: { - feature_key: string, - environment_key: string -}) { - // Reuse updateTargeting from src/api/targeting.ts - return await this.api.updateTargeting( - args.feature_key, - args.environment_key, - { status: 'active' } - ) -} -``` - -#### `disable_targeting` -**Implementation Time**: 1.5 hours -**Complexity**: Medium -**Files to leverage**: `src/api/targeting.ts`, `src/commands/targeting/disable.ts` - -```typescript -async disableTargeting(args: { - feature_key: string, - environment_key: string -}) { - // Reuse updateTargeting from src/api/targeting.ts - return await this.api.updateTargeting( - args.feature_key, - args.environment_key, - { status: 'inactive' } - ) -} -``` - -### 4. Interactive Create Feature - -#### `create_feature` -**Implementation Time**: 3 hours -**Complexity**: High -**Files to leverage**: `src/commands/features/create.ts`, `src/ui/prompts/featurePrompts.ts` - -```typescript -async createFeature(args: { - key?: string, - name?: string, - description?: string, - type?: 'release' | 'experiment' | 'permission' | 'ops', - interactive?: boolean -}) { - // If interactive mode, use prompts from src/ui/prompts/featurePrompts.ts - // Otherwise use provided args directly - // Reuse createFeature logic from src/commands/features/create.ts - - if (args.interactive) { - // Use existing prompt functions but adapt for MCP - // May need to return prompt questions for AI to handle - return { - requires_input: true, - questions: [ - { field: 'key', prompt: 'Enter feature key:', required: true }, - { field: 'name', prompt: 'Enter feature name:', required: true }, - { field: 'description', prompt: 'Enter description:', required: false }, - { field: 'type', prompt: 'Select type:', options: ['release', 'experiment', 'permission', 'ops'] } - ] - } - } else { - // Direct creation with provided args - return await this.api.createFeature(args) - } -} -``` - -## Implementation Notes - -### Authentication Integration -- Reuse existing auth patterns from `src/auth/ApiAuth.ts` -- Use `TokenCache` for token management -- Maintain compatibility with existing CLI authentication - -### Error Handling -- Wrap API errors in MCP-compatible error responses -- Provide clear error messages for common issues -- Use existing validation from CLI commands - -### File Structure - -``` -src/mcp/ -├── index.ts # MCP server entry point -├── server.ts # Main server implementation -├── tools/ -│ ├── features.ts # Feature and variable tools -│ ├── environments.ts # SDK keys and targeting tools -│ └── index.ts -└── utils/ - ├── auth.ts # Authentication helpers - └── api.ts # API client wrapper -``` - -## Dependencies - -```json -{ - "dependencies": { - "@modelcontextprotocol/sdk": "latest", - "zod": "^3.23.0" - } -} -``` From 9e83d1129ac12718bf99ef47f7469c5885669783 Mon Sep 17 00:00:00 2001 From: Jonathan Norris <jonathan@taplytics.com> Date: Wed, 9 Jul 2025 15:12:04 -0400 Subject: [PATCH 40/44] feat: add update_project tool and API call, switch projects.ts to use zod types --- src/api/projects.ts | 56 +++++++++++---------------------- src/api/schemas.ts | 9 ++++++ src/api/zodClient.ts | 19 +++++++++++ src/commands/projects/create.ts | 7 +++-- src/commands/projects/get.ts | 6 ++-- src/mcp/tools/projectTools.ts | 48 ++++++++++++++++++++++++++-- src/mcp/types.ts | 27 +++------------- 7 files changed, 104 insertions(+), 68 deletions(-) diff --git a/src/api/projects.ts b/src/api/projects.ts index 7913a2536..0e4877201 100644 --- a/src/api/projects.ts +++ b/src/api/projects.ts @@ -1,43 +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' - - @IsString() - @IsOptional() - search?: string - - @IsString() - @IsOptional() - createdBy?: string - - @IsOptional() - page?: number - - @IsOptional() - perPage?: number -} +import { + CreateProjectParams, + UpdateProjectParams, + GetProjectsParams, +} from './schemas' const BASE_URL = '/v1/projects' @@ -68,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 a8b8ac997..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 > diff --git a/src/api/zodClient.ts b/src/api/zodClient.ts index 02cdf8535..2711cded4 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 @@ -864,6 +882,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/tools/projectTools.ts b/src/mcp/tools/projectTools.ts index dc1048f9f..72a82c7f0 100644 --- a/src/mcp/tools/projectTools.ts +++ b/src/mcp/tools/projectTools.ts @@ -4,9 +4,13 @@ import { fetchProjects, fetchProject, createProject, - CreateProjectParams, + updateProject, } from '../../api/projects' -import { ListProjectsArgsSchema, CreateProjectArgsSchema } from '../types' +import { + ListProjectsArgsSchema, + CreateProjectArgsSchema, + UpdateProjectArgsSchema, +} from '../types' import { ToolHandler } from '../server' export const projectToolDefinitions: Tool[] = [ @@ -84,6 +88,33 @@ export const projectToolDefinitions: Tool[] = [ required: ['name', 'key'], }, }, + { + name: 'update_project', + description: 'Update an existing project', + inputSchema: { + type: 'object', + properties: { + key: { + type: 'string', + description: + 'Project key to identify the project to update', + }, + name: { + type: 'string', + description: 'Updated project name', + }, + description: { + type: 'string', + description: 'Updated project description', + }, + color: { + type: 'string', + description: 'Project color (hex format)', + }, + }, + required: ['key'], + }, + }, ] export const projectToolHandlers: Record<string, ToolHandler> = { @@ -123,4 +154,17 @@ export const projectToolHandlers: Record<string, ToolHandler> = { false, ) }, + update_project: async (args: unknown, apiClient: DevCycleApiClient) => { + const validatedArgs = UpdateProjectArgsSchema.parse(args) + const { key, ...updateParams } = validatedArgs + + return await apiClient.executeWithLogging( + 'updateProject', + validatedArgs, + async (authToken) => { + return await updateProject(authToken, key, updateParams) + }, + false, + ) + }, } diff --git a/src/mcp/types.ts b/src/mcp/types.ts index 948390944..422688a31 100644 --- a/src/mcp/types.ts +++ b/src/mcp/types.ts @@ -32,32 +32,15 @@ export const DeleteFeatureArgsSchema = z.object({ key: z.string(), }) -export const ListProjectsArgsSchema = z.object({ - sortBy: z - .enum([ - 'createdAt', - 'updatedAt', - 'name', - 'key', - 'createdBy', - 'propertyKey', - ]) - .optional(), - sortOrder: z.enum(['asc', 'desc']).optional(), - search: z.string().min(3).optional(), - createdBy: z.string().optional(), - page: z.number().min(1).optional(), - perPage: z.number().min(1).max(1000).default(100).optional(), -}) +export const ListProjectsArgsSchema = schemas.GetProjectsParams + +export const CreateProjectArgsSchema = schemas.CreateProjectDto -export const CreateProjectArgsSchema = z.object({ +export const UpdateProjectArgsSchema = schemas.UpdateProjectDto.extend({ key: z .string() - .min(1) .max(100) - .regex(/^[a-z0-9-_.]+$/), - name: z.string().min(1), - description: z.string().optional(), + .regex(/^[a-z0-9-_.]+$/), // Make key required for identifying the project }) export const ListEnvironmentsArgsSchema = z.object({ From e9507cc1ef7aab668b65af9258fbd2a67fbb5056 Mon Sep 17 00:00:00 2001 From: Jonathan Norris <jonathan@taplytics.com> Date: Thu, 17 Jul 2025 12:31:48 -0400 Subject: [PATCH 41/44] feat: improve MCP server installation (#461) * feat: improve MCP server installation * fix: update yarn.lock * fix: error handling * fix: switch to path.resolve() * chore: refactor and cleanup error handling code * chore: address PR comments --- README.md | 71 +++++++++++++++++- bin/mcp | 6 ++ bin/mcp.cmd | 3 + oclif.manifest.json | 2 +- package.json | 2 +- src/api/zodClient.ts | 6 +- src/mcp/index.ts | 91 ++++++++++++++++++++++- src/mcp/server.ts | 169 +++++++++++++++++++++++++++++++++++------- src/mcp/utils/auth.ts | 73 +++++++++++++++--- yarn.lock | 2 +- 10 files changed, 375 insertions(+), 50 deletions(-) create mode 100755 bin/mcp create mode 100644 bin/mcp.cmd diff --git a/README.md b/README.md index f38903d43..794b8cf35 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,8 @@ The CLI can be customized in several ways using command-line args or by creating * [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 @@ -149,7 +151,27 @@ USAGE 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. -## Quick Setup +## 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`: @@ -165,6 +187,10 @@ Add to `.cursor/mcp_settings.json`: ### 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": { @@ -175,9 +201,48 @@ Add to your Claude configuration file: } ``` -The MCP server uses the same authentication as the CLI. Simply run `dvc login sso` first, then your AI assistant can manage feature flags on your behalf. +### 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, see [docs/mcp.md](docs/mcp.md). +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/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 d33244942..b937944df 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "author": "support@devcycle.com", "bin": { "dvc": "./bin/run", - "dvc-mcp": "./dist/mcp/index.js" + "dvc-mcp": "./bin/mcp" }, "homepage": "https://github.com/DevCycleHQ/cli", "license": "MIT", diff --git a/src/api/zodClient.ts b/src/api/zodClient.ts index 2711cded4..d814b843d 100644 --- a/src/api/zodClient.ts +++ b/src/api/zodClient.ts @@ -314,12 +314,12 @@ const UpdateAudienceDto = z }) .partial() const VariableValidationEntity = z.object({ - schemaType: z.object({}).partial(), - enumValues: z.object({}).partial().optional(), + schemaType: z.string(), + enumValues: z.array(z.string()).optional(), regexPattern: z.string().optional(), jsonSchema: z.string().optional(), description: z.string(), - exampleValue: z.object({}).partial(), + exampleValue: z.any(), }) const CreateVariableDto = z.object({ name: z.string().max(100).optional(), diff --git a/src/mcp/index.ts b/src/mcp/index.ts index 55671d126..f21ac9a26 100644 --- a/src/mcp/index.ts +++ b/src/mcp/index.ts @@ -3,12 +3,53 @@ 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' + +// 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 server = new Server( { name: 'devcycle', - version: '0.0.1', + version: getVersion(), }, { capabilities: { @@ -27,6 +68,52 @@ async function main() { } main().catch((error) => { - console.error('Failed to start DevCycle MCP server:', 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 index cbbd57113..9f39a81ad 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -84,6 +84,146 @@ export class DevCycleMCPServer { } } + 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, @@ -110,34 +250,7 @@ export class DevCycleMCPServer { ], } } catch (error) { - console.error(`Error in tool handler ${name}:`, error) - - // Safely extract error message, handling undefined/null cases - let errorMessage = 'Unknown error' - if (error instanceof Error && error.message) { - errorMessage = error.message - } else if (error && typeof error === 'string') { - errorMessage = error - } else if (error && typeof error === 'object') { - errorMessage = JSON.stringify(error) - } - - // Return error as JSON to maintain consistent response format - const errorResponse = { - error: true, - message: errorMessage, - tool: name, - timestamp: new Date().toISOString(), - } - - return { - content: [ - { - type: 'text', - text: JSON.stringify(errorResponse, null, 2), - }, - ], - } + return this.handleToolError(error, name) } }, ) diff --git a/src/mcp/utils/auth.ts b/src/mcp/utils/auth.ts index e325dde48..bc0a50875 100644 --- a/src/mcp/utils/auth.ts +++ b/src/mcp/utils/auth.ts @@ -44,25 +44,69 @@ export class DevCycleAuth { this._authToken = await this.apiAuth.getToken(flags, this._orgId) if (!this._authToken) { - throw new Error( - 'No authentication found. Please set DEVCYCLE_CLIENT_ID and DEVCYCLE_CLIENT_SECRET environment variables, ' + - 'or run "dvc login sso" in the CLI first.', - ) + 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) { - throw new Error( - 'No project configured. Please set DEVCYCLE_PROJECT_KEY environment variable, ' + - 'or configure a project using "dvc projects select" in the CLI.', - ) + 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: ${error instanceof Error ? error.message : 'Unknown 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', ) } } @@ -142,7 +186,11 @@ export class DevCycleAuth { requireAuth(): void { if (!this.hasToken()) { throw new Error( - 'Authentication required. Please configure DevCycle credentials.', + '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"', ) } } @@ -150,7 +198,10 @@ export class DevCycleAuth { requireProject(): void { if (!this._projectKey) { throw new Error( - 'Project key required. Please configure a DevCycle project.', + '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/yarn.lock b/yarn.lock index 79e1e073b..7f09b7100 100644 --- a/yarn.lock +++ b/yarn.lock @@ -642,7 +642,7 @@ __metadata: zod: "npm:^3.24.2" bin: dvc: ./bin/run - dvc-mcp: ./dist/mcp/index.js + dvc-mcp: ./bin/mcp languageName: unknown linkType: soft From b376f7db29379466b95a5f4038990625ee137f9c Mon Sep 17 00:00:00 2001 From: Jonathan Norris <jonathan@taplytics.com> Date: Thu, 17 Jul 2025 16:38:38 -0400 Subject: [PATCH 42/44] feat: add dashboard links and output schemas to environmentTools.ts (#462) * feat: add dashboard links and output schemas to environmentTools.ts * feat: dashboard links working * feat: update projectTools with dashboard links * feat: update self targeting tools with dashboard links * feat: update variableTools * chore: cleanup ouput schemas * feat: update featureTools.ts with dashboard links and input/output schemas * fix: add oclif.manifest file back * chore: cleanup tool calls * chore: update feature list query params --- src/api/features.ts | 14 +- src/api/variables.ts | 12 +- src/mcp/tools/commonSchemas.ts | 348 +++++++++++++++++ src/mcp/tools/environmentTools.ts | 187 +++++++-- src/mcp/tools/featureTools.ts | 571 ++++++++++++++++++++-------- src/mcp/tools/projectTools.ts | 243 ++++++++---- src/mcp/tools/selfTargetingTools.ts | 175 +++++++-- src/mcp/tools/variableTools.ts | 370 +++++++++++------- src/mcp/types.ts | 41 +- src/mcp/utils/api.ts | 25 ++ 10 files changed, 1559 insertions(+), 427 deletions(-) create mode 100644 src/mcp/tools/commonSchemas.ts diff --git a/src/api/features.ts b/src/api/features.ts index 8c76876ba..ac5581ed0 100644 --- a/src/api/features.ts +++ b/src/api/features.ts @@ -19,11 +19,21 @@ export const fetchFeatures = async ( token: string, project_id: string, queries: { - feature?: string page?: number perPage?: number + sortBy?: + | 'createdAt' + | 'updatedAt' + | 'name' + | 'key' + | 'createdBy' + | 'propertyKey' + sortOrder?: 'asc' | 'desc' search?: string - staleness?: string + staleness?: 'all' | 'unused' | 'released' | 'unmodified' | 'notStale' + createdBy?: string + type?: 'release' | 'experiment' | 'permission' | 'ops' + status?: 'active' | 'complete' | 'archived' } = {}, ): Promise<Feature[]> => { const response = await apiClient.get(FEATURE_URL, { diff --git a/src/api/variables.ts b/src/api/variables.ts index 2b019d4dd..87bef5a7b 100644 --- a/src/api/variables.ts +++ b/src/api/variables.ts @@ -70,10 +70,20 @@ export const fetchVariables = async ( token: string, project_id: string, queries: { - feature?: string page?: number perPage?: number + sortBy?: + | 'createdAt' + | 'updatedAt' + | 'name' + | 'key' + | 'createdBy' + | 'propertyKey' + sortOrder?: 'asc' | 'desc' search?: string + feature?: string + type?: 'String' | 'Boolean' | 'Number' | 'JSON' + status?: 'active' | 'archived' } = {}, ) => { return await apiClient.get('/v1/projects/:project/variables', { diff --git a/src/mcp/tools/commonSchemas.ts b/src/mcp/tools/commonSchemas.ts new file mode 100644 index 000000000..59d5c1ac7 --- /dev/null +++ b/src/mcp/tools/commonSchemas.ts @@ -0,0 +1,348 @@ +/** + * Common schema definitions used across multiple MCP tool files + */ + +// ============================================================================= +// SHARED OUTPUT SCHEMA PROPERTIES +// ============================================================================= + +export const DASHBOARD_LINK_PROPERTY = { + type: 'string' as const, + format: 'uri' as const, + description: 'URL to view and manage resources in the DevCycle dashboard', +} + +export const MESSAGE_RESPONSE_SCHEMA = { + type: 'object' as const, + properties: { + message: { + type: 'string' as const, + }, + }, + required: ['message'], +} + +// ============================================================================= +// SHARED INPUT SCHEMA PROPERTIES +// ============================================================================= + +export const FEATURE_KEY_PROPERTY = { + type: 'string' as const, + description: + 'The key of the feature (unique, immutable, max 100 characters, pattern: ^[a-z0-9-_.]+$)', +} + +export const ENVIRONMENT_KEY_PROPERTY = { + type: 'string' as const, + description: + 'The key of the environment (unique, immutable, max 100 characters, pattern: ^[a-z0-9-_.]+$)', +} + +export const VARIATION_KEY_PROPERTY = { + type: 'string' as const, + description: + 'Unique variation key (unique, immutable, max 100 characters, pattern: ^[a-z0-9-_.]+$)', +} + +export const VARIABLE_KEY_PROPERTY = { + type: 'string' as const, + description: + 'The variable key (unique, immutable, max 100 characters, pattern: ^[a-z0-9-_.]+$)', +} + +export const PROJECT_KEY_PROPERTY = { + type: 'string' as const, + description: + 'The project key (unique, immutable, max 100 characters, pattern: ^[a-z0-9-_.]+$)', +} + +// Filter type definitions based on DevCycle API swagger schemas + +export const ALL_FILTER_SCHEMA = { + type: 'object' as const, + description: 'Filter that matches all users', + properties: { + type: { + type: 'string' as const, + description: 'Filter type of this audience', + enum: ['all'] as const, + }, + }, + required: ['type'] as const, +} + +export const USER_FILTER_SCHEMA = { + type: 'object' as const, + description: 'Filter by basic user properties', + properties: { + type: { + type: 'string' as const, + description: 'Filter type of this audience', + enum: ['user'] as const, + }, + subType: { + type: 'string' as const, + description: 'Sub type of this filter', + enum: ['user_id', 'email', 'platform', 'deviceModel'] as const, + }, + comparator: { + type: 'string' as const, + description: 'Comparator to use', + enum: [ + '=', + '!=', + 'exist', + '!exist', + 'contain', + '!contain', + 'startWith', + '!startWith', + 'endWith', + '!endWith', + ] as const, + }, + values: { + type: 'array' as const, + description: + 'Array of values (required for all filters except exist/!exist)', + items: { + type: 'string' as const, + }, + }, + }, + required: ['type', 'subType', 'comparator'] as const, +} + +export const USER_COUNTRY_FILTER_SCHEMA = { + type: 'object' as const, + description: 'Filter by user country', + properties: { + type: { + type: 'string' as const, + description: 'Filter type of this audience', + enum: ['user'] as const, + }, + subType: { + type: 'string' as const, + description: 'Sub type of this filter', + enum: ['country'] as const, + }, + comparator: { + type: 'string' as const, + description: 'Comparator to use', + enum: [ + '=', + '!=', + 'exist', + '!exist', + 'contain', + '!contain', + 'startWith', + '!startWith', + 'endWith', + '!endWith', + ] as const, + }, + values: { + type: 'array' as const, + description: 'Array of country codes (e.g., CA, US)', + items: { + type: 'string' as const, + }, + }, + }, + required: ['type', 'subType', 'comparator'] as const, +} + +export const USER_APP_VERSION_FILTER_SCHEMA = { + type: 'object' as const, + description: 'Filter by application version', + properties: { + type: { + type: 'string' as const, + description: 'Filter type of this audience', + enum: ['user'] as const, + }, + subType: { + type: 'string' as const, + description: 'Sub type of this filter', + enum: ['appVersion'] as const, + }, + comparator: { + type: 'string' as const, + description: 'Comparator to use', + enum: ['=', '!=', '>', '>=', '<', '<=', 'exist', '!exist'] as const, + }, + values: { + type: 'array' as const, + description: 'Array of version strings (e.g., 1.0.2)', + items: { + type: 'string' as const, + }, + }, + }, + required: ['type', 'subType', 'comparator'] as const, +} + +export const USER_PLATFORM_VERSION_FILTER_SCHEMA = { + type: 'object' as const, + description: 'Filter by platform version', + properties: { + type: { + type: 'string' as const, + description: 'Filter type of this audience', + enum: ['user'] as const, + }, + subType: { + type: 'string' as const, + description: 'Sub type of this filter', + enum: ['platformVersion'] as const, + }, + comparator: { + type: 'string' as const, + description: 'Comparator to use', + enum: ['=', '!=', '>', '>=', '<', '<=', 'exist', '!exist'] as const, + }, + values: { + type: 'array' as const, + description: 'Array of platform version strings', + items: { + type: 'string' as const, + }, + }, + }, + required: ['type', 'subType', 'comparator'] as const, +} + +export const USER_CUSTOM_FILTER_SCHEMA = { + type: 'object' as const, + description: 'Filter by custom user data properties', + properties: { + type: { + type: 'string' as const, + description: 'Filter type of this audience', + enum: ['user'] as const, + }, + subType: { + type: 'string' as const, + description: 'Sub type of this filter', + enum: ['customData'] as const, + }, + comparator: { + type: 'string' as const, + description: 'Comparator to use', + enum: [ + '=', + '!=', + '>', + '>=', + '<', + '<=', + 'exist', + '!exist', + 'contain', + '!contain', + 'startWith', + '!startWith', + 'endWith', + '!endWith', + ] as const, + }, + dataKey: { + type: 'string' as const, + description: 'Data Key used for custom data', + minLength: 1, + }, + dataKeyType: { + type: 'string' as const, + description: 'Data Key Type used for custom data', + enum: ['String', 'Boolean', 'Number'] as const, + }, + values: { + type: 'array' as const, + description: 'Array of values (type depends on dataKeyType)', + items: { + anyOf: [ + { type: 'string' as const }, + { type: 'number' as const }, + { type: 'boolean' as const }, + ], + }, + }, + }, + required: [ + 'type', + 'subType', + 'comparator', + 'dataKey', + 'dataKeyType', + ] as const, +} + +export const AUDIENCE_MATCH_FILTER_SCHEMA = { + type: 'object' as const, + description: 'Filter by audience membership', + properties: { + type: { + type: 'string' as const, + enum: ['audienceMatch'] as const, + }, + comparator: { + type: 'string' as const, + enum: ['=', '!='] as const, + }, + _audiences: { + type: 'array' as const, + description: 'Array of audience IDs to match against', + items: { + type: 'string' as const, + }, + }, + }, + required: ['type'] as const, +} + +// Target Audience schema based on DevCycle API swagger definition +export const TARGET_AUDIENCE_PROPERTY = { + type: 'object' as const, + description: 'Audience definition for the target', + properties: { + name: { + type: 'string' as const, + description: + 'Audience display name, must be set for project-level audiences.', + example: 'Android Users', + maxLength: 100, + minLength: 1, + }, + filters: { + type: 'object' as const, + description: + 'Audience filters, describing logic for segmenting users', + properties: { + filters: { + type: 'array' as const, + description: 'Array of filter conditions', + items: { + anyOf: [ + ALL_FILTER_SCHEMA, + USER_FILTER_SCHEMA, + USER_COUNTRY_FILTER_SCHEMA, + USER_APP_VERSION_FILTER_SCHEMA, + USER_PLATFORM_VERSION_FILTER_SCHEMA, + USER_CUSTOM_FILTER_SCHEMA, + AUDIENCE_MATCH_FILTER_SCHEMA, + ], + }, + }, + operator: { + type: 'string' as const, + description: 'Operator type for combining filters', + enum: ['and', 'or'] as const, + }, + }, + required: ['filters', 'operator'] as const, + }, + }, + required: ['filters'] as const, +} diff --git a/src/mcp/tools/environmentTools.ts b/src/mcp/tools/environmentTools.ts index a71fd4e8f..2f1e53d10 100644 --- a/src/mcp/tools/environmentTools.ts +++ b/src/mcp/tools/environmentTools.ts @@ -13,19 +13,64 @@ import { UpdateEnvironmentArgsSchema, } from '../types' import { ToolHandler } from '../server' +import { + DASHBOARD_LINK_PROPERTY, + ENVIRONMENT_KEY_PROPERTY, +} from './commonSchemas' + +// Helper function to generate environment dashboard links +const generateEnvironmentDashboardLink = ( + orgId: string, + projectKey: string, +): string => { + return `https://app.devcycle.com/o/${orgId}/settings/p/${projectKey}/environments` +} + +// ============================================================================= +// INPUT SCHEMAS +// ============================================================================= // Reusable schema components -const ENVIRONMENT_KEY_PROPERTY = { +const ENVIRONMENT_COLOR_PROPERTY = { + type: 'string' as const, + description: 'Color used to represent this environment in the UI', +} + +const ENVIRONMENT_TYPE_PROPERTY = { type: 'string' as const, - description: - "The key of the environment, must be unique and can't be changed after creation", + enum: [ + 'development', + 'staging', + 'production', + 'disaster_recovery', + ] as const, } -const PAGINATION_PROPERTIES = { +const ENVIRONMENT_COMMON_PROPERTIES = { + key: ENVIRONMENT_KEY_PROPERTY, + name: { + type: 'string' as const, + }, + description: { + type: 'string' as const, + }, + color: ENVIRONMENT_COLOR_PROPERTY, + type: ENVIRONMENT_TYPE_PROPERTY, + settings: { + type: 'object' as const, + properties: { + appIconUri: { + type: 'string' as const, + description: 'URI for the app icon', + }, + }, + }, +} + +const ENVIRONMENT_PAGINATION_PROPERTIES = { search: { type: 'string' as const, - description: - 'Search query to filter environments (minimum 3 characters)', + description: 'Search query to filter results (minimum 3 characters)', minLength: 3, }, page: { @@ -41,7 +86,6 @@ const PAGINATION_PROPERTIES = { }, sortBy: { type: 'string' as const, - description: 'Field to sort by (default: createdAt)', enum: [ 'createdAt', 'updatedAt', @@ -50,6 +94,7 @@ const PAGINATION_PROPERTIES = { 'createdBy', 'propertyKey', ] as const, + description: 'Field to sort by', }, sortOrder: { type: 'string' as const, @@ -58,38 +103,108 @@ const PAGINATION_PROPERTIES = { }, createdBy: { type: 'string' as const, - description: 'Filter by creator user ID', + description: 'Filter by user who created the environment', }, } -const ENVIRONMENT_COMMON_PROPERTIES = { - key: ENVIRONMENT_KEY_PROPERTY, - name: { +// ============================================================================= +// OUTPUT SCHEMAS +// ============================================================================= + +// Shared SDK key properties +const SDK_KEY_PROPERTIES = { + mobile: { type: 'string' as const, - description: 'The name of the environment', + description: 'Mobile SDK key for client-side mobile applications', }, - description: { + server: { type: 'string' as const, - description: 'The description of the environment', + description: 'Server SDK key for server-side applications', }, - color: { + client: { type: 'string' as const, - description: 'The color for the environment', + description: 'Client SDK key for client-side web applications', + }, +} + +// Output schema components +const SDK_KEYS_OBJECT_SCHEMA = { + type: 'object' as const, + properties: SDK_KEY_PROPERTIES, + required: ['mobile', 'server', 'client'], +} + +const ENVIRONMENT_OBJECT_SCHEMA = { + type: 'object' as const, + description: 'A DevCycle environment configuration', + properties: { + ...ENVIRONMENT_COMMON_PROPERTIES, + _id: { + type: 'string' as const, + description: 'Unique identifier for the environment', + }, + sdkKeys: SDK_KEYS_OBJECT_SCHEMA, + createdAt: { + type: 'string' as const, + description: 'ISO timestamp when the environment was created', + }, + updatedAt: { + type: 'string' as const, + description: 'ISO timestamp when the environment was last updated', + }, + }, + required: [ + '_id', + 'key', + 'name', + 'type', + 'sdkKeys', + 'createdAt', + 'updatedAt', + ], +} + +// Complete output schema definitions +const ENVIRONMENT_OUTPUT_SCHEMA = { + type: 'object' as const, + properties: { + result: ENVIRONMENT_OBJECT_SCHEMA, + dashboardLink: DASHBOARD_LINK_PROPERTY, }, + required: ['result', 'dashboardLink'], } +// ============================================================================= +// TOOL DEFINITIONS +// ============================================================================= export const environmentToolDefinitions: Tool[] = [ { name: 'list_environments', - description: 'List environments in the current project', + description: + 'List environments in the current project. Include dashboard link in the response.', inputSchema: { type: 'object', - properties: PAGINATION_PROPERTIES, + properties: ENVIRONMENT_PAGINATION_PROPERTIES, + }, + outputSchema: { + type: 'object' as const, + description: + 'Response containing a list of environments and dashboard link', + properties: { + result: { + type: 'array' as const, + description: 'Array of environment objects in the project', + items: ENVIRONMENT_OBJECT_SCHEMA, + }, + dashboardLink: DASHBOARD_LINK_PROPERTY, + }, + required: ['result', 'dashboardLink'], }, }, { name: 'get_sdk_keys', - description: 'Get SDK keys for an environment', + description: + 'Get SDK keys for an environment. Include dashboard link in the response.', inputSchema: { type: 'object', properties: { @@ -102,24 +217,42 @@ export const environmentToolDefinitions: Tool[] = [ }, required: ['environmentKey'], }, + outputSchema: { + type: 'object' as const, + description: 'Response containing SDK keys and dashboard link', + properties: { + result: { + type: 'object' as const, + description: + 'SDK keys for the requested environment (filtered by keyType if specified)', + properties: SDK_KEY_PROPERTIES, + }, + dashboardLink: DASHBOARD_LINK_PROPERTY, + }, + required: ['result', 'dashboardLink'], + }, }, { name: 'create_environment', - description: 'Create a new environment', + description: + 'Create a new environment. Include dashboard link in the response.', inputSchema: { type: 'object', properties: ENVIRONMENT_COMMON_PROPERTIES, required: ['name', 'key'], }, + outputSchema: ENVIRONMENT_OUTPUT_SCHEMA, }, { name: 'update_environment', - description: 'Update an existing environment', + description: + 'Update an existing environment. Include dashboard link in the response.', inputSchema: { type: 'object', properties: ENVIRONMENT_COMMON_PROPERTIES, required: ['key'], }, + outputSchema: ENVIRONMENT_OUTPUT_SCHEMA, }, ] @@ -127,18 +260,19 @@ export const environmentToolHandlers: Record<string, ToolHandler> = { list_environments: async (args: unknown, apiClient: DevCycleApiClient) => { const validatedArgs = ListEnvironmentsArgsSchema.parse(args) - return await apiClient.executeWithLogging( + return await apiClient.executeWithDashboardLink( 'listEnvironments', validatedArgs, async (authToken, projectKey) => { return await fetchEnvironments(authToken, projectKey) }, + generateEnvironmentDashboardLink, ) }, get_sdk_keys: async (args: unknown, apiClient: DevCycleApiClient) => { const validatedArgs = GetSdkKeysArgsSchema.parse(args) - return await apiClient.executeWithLogging( + return await apiClient.executeWithDashboardLink( 'getSdkKeys', validatedArgs, async (authToken, projectKey) => { @@ -162,12 +296,13 @@ export const environmentToolHandlers: Record<string, ToolHandler> = { } } }, + generateEnvironmentDashboardLink, ) }, create_environment: async (args: unknown, apiClient: DevCycleApiClient) => { const validatedArgs = CreateEnvironmentArgsSchema.parse(args) - return await apiClient.executeWithLogging( + return await apiClient.executeWithDashboardLink( 'createEnvironment', validatedArgs, async (authToken, projectKey) => { @@ -177,12 +312,13 @@ export const environmentToolHandlers: Record<string, ToolHandler> = { validatedArgs, ) }, + generateEnvironmentDashboardLink, ) }, update_environment: async (args: unknown, apiClient: DevCycleApiClient) => { const validatedArgs = UpdateEnvironmentArgsSchema.parse(args) - return await apiClient.executeWithLogging( + return await apiClient.executeWithDashboardLink( 'updateEnvironment', validatedArgs, async (authToken, projectKey) => { @@ -194,6 +330,7 @@ export const environmentToolHandlers: Record<string, ToolHandler> = { updateParams, ) }, + generateEnvironmentDashboardLink, ) }, } diff --git a/src/mcp/tools/featureTools.ts b/src/mcp/tools/featureTools.ts index 69abf0887..717ec0f25 100644 --- a/src/mcp/tools/featureTools.ts +++ b/src/mcp/tools/featureTools.ts @@ -36,22 +36,39 @@ import { GetFeatureAuditLogHistoryArgsSchema, } from '../types' import { ToolHandler } from '../server' +import { + DASHBOARD_LINK_PROPERTY, + MESSAGE_RESPONSE_SCHEMA, + FEATURE_KEY_PROPERTY, + ENVIRONMENT_KEY_PROPERTY, + VARIATION_KEY_PROPERTY, + TARGET_AUDIENCE_PROPERTY, +} from './commonSchemas' -// Reusable schema components -const FEATURE_KEY_PROPERTY = { - type: 'string' as const, - description: 'The key of the feature', +// Helper function to generate feature dashboard links +const generateFeaturesDashboardLink = ( + orgId: string, + projectKey: string, +): string => { + return `https://app.devcycle.com/o/${orgId}/p/${projectKey}/features` } -const ENVIRONMENT_KEY_PROPERTY = { - type: 'string' as const, - description: 'The key of the environment', +const generateFeatureDashboardLink = ( + orgId: string, + projectKey: string, + featureKey: string, + page: 'overview' | 'manage-feature' | 'audit-log' = 'overview', +): string => { + return `https://app.devcycle.com/o/${orgId}/p/${projectKey}/features/${featureKey}/${page}` } +// ============================================================================= +// INPUT SCHEMAS +// ============================================================================= + const ENVIRONMENT_KEY_OPTIONAL_PROPERTY = { type: 'string' as const, - description: - 'The key of the environment (optional - if not provided, returns all environments)', + description: 'Optional environment key to filter by', } const FEATURE_NAME_PROPERTY = { @@ -70,14 +87,80 @@ const FEATURE_TYPE_PROPERTY = { description: 'Feature type', } +const FEATURE_STATUS_PROPERTY = { + type: 'string' as const, + enum: ['active', 'complete', 'archived'] as const, + description: 'Feature status', +} + const CONTROL_VARIATION_PROPERTY = { type: 'string' as const, description: 'The key of the variation that is used as the control variation for Metrics', } +const FEATURE_PAGINATION_PROPERTIES = { + page: { + type: 'number' as const, + description: 'Page number', + minimum: 1, + default: 1, + }, + perPage: { + type: 'number' as const, + description: 'Items per page', + minimum: 1, + maximum: 1000, + default: 100, + }, + sortBy: { + type: 'string' as const, + description: 'Sort field', + enum: [ + 'createdAt', + 'updatedAt', + 'name', + 'key', + 'createdBy', + 'propertyKey', + ], + default: 'createdAt', + }, + sortOrder: { + type: 'string' as const, + description: 'Sort order', + enum: ['asc', 'desc'], + default: 'desc', + }, + search: { + type: 'string' as const, + description: 'Search query to filter results', + minLength: 3, + }, + createdBy: { + type: 'string' as const, + description: 'Filter by creator', + }, + type: { + type: 'string' as const, + description: 'Filter by feature type', + enum: ['release', 'experiment', 'permission', 'ops'], + }, + status: { + type: 'string' as const, + description: 'Filter by feature status', + enum: ['active', 'complete', 'archived'], + }, + staleness: { + type: 'string' as const, + description: 'Filter by feature staleness', + enum: ['all', 'unused', 'released', 'unmodified', 'notStale'], + }, +} + const FEATURE_SETTINGS_PROPERTY = { type: 'object' as const, + description: 'Feature-level settings (all properties required if provided)', properties: { publicName: { type: 'string' as const, @@ -93,7 +176,6 @@ const FEATURE_SETTINGS_PROPERTY = { description: 'Whether opt-in is enabled for the feature', }, }, - description: 'Feature-level settings (all properties required if provided)', required: ['publicName', 'publicDescription', 'optInEnabled'] as const, } @@ -102,19 +184,16 @@ const SDK_VISIBILITY_PROPERTY = { properties: { mobile: { type: 'boolean' as const, - description: 'Whether the feature is visible to mobile SDKs', }, client: { type: 'boolean' as const, - description: 'Whether the feature is visible to client SDKs', }, server: { type: 'boolean' as const, - description: 'Whether the feature is visible to server SDKs', }, }, description: - 'SDK Type Visibility Settings (all properties required if provided)', + 'SDK Type Visibility Settings for mobile, client, and server SDKs', required: ['mobile', 'client', 'server'] as const, } @@ -128,12 +207,6 @@ const FEATURE_VARIABLES_PROPERTY = { }, } -const VARIATION_KEY_PROPERTY = { - type: 'string' as const, - description: - 'Unique variation key (max 100 characters, pattern: ^[a-z0-9-_.]+$)', -} - const VARIATION_NAME_PROPERTY = { type: 'string' as const, description: 'Human-readable variation name (max 100 characters)', @@ -146,47 +219,101 @@ const VARIATION_VARIABLES_PROPERTY = { additionalProperties: true, } -const PAGINATION_PROPERTIES = { - search: { - type: 'string' as const, - description: 'Search query to filter features', - }, - page: { - type: 'number' as const, - description: 'Page number (default: 1)', - }, - per_page: { - type: 'number' as const, - description: 'Number of items per page (default: 100, max: 1000)', +// ============================================================================= +// OUTPUT SCHEMAS +// ============================================================================= + +const FEATURE_OBJECT_SCHEMA = { + type: 'object' as const, + description: 'A DevCycle feature configuration', + properties: { + _id: { + type: 'string' as const, + description: 'MongoDB ID for the feature', + }, + key: FEATURE_KEY_PROPERTY, + name: FEATURE_NAME_PROPERTY, + description: FEATURE_DESCRIPTION_PROPERTY, + type: FEATURE_TYPE_PROPERTY, + status: FEATURE_STATUS_PROPERTY, + variations: { + type: 'array' as const, + description: 'Array of variations for this feature', + }, + createdAt: { + type: 'string' as const, + description: 'ISO timestamp when the feature was created', + }, + updatedAt: { + type: 'string' as const, + description: 'ISO timestamp when the feature was last updated', + }, }, + required: [ + '_id', + 'key', + 'name', + 'type', + 'status', + 'createdAt', + 'updatedAt', + ], } -const FEATURE_ENVIRONMENT_REQUIRED_PROPERTIES = { - feature_key: FEATURE_KEY_PROPERTY, - environment_key: ENVIRONMENT_KEY_PROPERTY, +const VARIATION_OBJECT_SCHEMA = { + type: 'object' as const, + description: 'A feature variation configuration', + properties: { + _id: { + type: 'string' as const, + description: 'MongoDB ID for the variation', + }, + key: VARIATION_KEY_PROPERTY, + name: { + type: 'string' as const, + }, + variables: { + type: 'object' as const, + description: 'Variable values for this variation', + }, + }, + required: ['_id', 'key', 'name'], } +// ============================================================================= +// TOOL DEFINITIONS +// ============================================================================= + export const featureToolDefinitions: Tool[] = [ { name: 'list_features', - description: 'List features in the current project', + description: + 'List features in the current project. Include dashboard link in the response.', inputSchema: { type: 'object', - properties: PAGINATION_PROPERTIES, + properties: FEATURE_PAGINATION_PROPERTIES, + }, + outputSchema: { + type: 'object' as const, + properties: { + result: { + type: 'array' as const, + description: 'Array of feature objects in the project', + items: FEATURE_OBJECT_SCHEMA, + }, + dashboardLink: DASHBOARD_LINK_PROPERTY, + }, + required: ['result', 'dashboardLink'], }, }, { name: 'create_feature', description: - 'Create a new feature flag (supports interactive mode). ⚠️ IMPORTANT: If creating configurations for production environments, always confirm with the user before proceeding.', + 'Create a new feature flag. Include dashboard link in the response.', inputSchema: { type: 'object', properties: { - key: { - type: 'string', - description: - 'Unique feature key (max 100 characters, pattern: ^[a-z0-9-_.]+$)', - }, + key: FEATURE_KEY_PROPERTY, name: FEATURE_NAME_PROPERTY, description: FEATURE_DESCRIPTION_PROPERTY, type: FEATURE_TYPE_PROPERTY, @@ -221,33 +348,29 @@ export const featureToolDefinitions: Tool[] = [ description: 'Targeting rules for this environment', }, - status: { - type: 'string', - description: 'Status for this environment', - }, + status: FEATURE_STATUS_PROPERTY, }, }, }, - interactive: { - type: 'boolean', - description: - 'Use interactive mode to prompt for missing fields', - }, }, }, + outputSchema: { + type: 'object' as const, + properties: { + result: FEATURE_OBJECT_SCHEMA, + dashboardLink: DASHBOARD_LINK_PROPERTY, + }, + required: ['result', 'dashboardLink'], + }, }, { name: 'update_feature', description: - 'Update an existing feature flag. ⚠️ IMPORTANT: Changes to feature flags may affect production environments. Always confirm with the user before making changes to features that are active in production.', + 'Update an existing feature flag. ⚠️ IMPORTANT: Changes to feature flags may affect production environments. Always confirm with the user before making changes to features that are active in production. Include dashboard link in the response.', inputSchema: { type: 'object', properties: { - key: { - type: 'string', - description: - 'The key of the feature to update(1-100 characters, must match pattern ^[a-z0-9-_.]+$)', - }, + key: FEATURE_KEY_PROPERTY, name: FEATURE_NAME_PROPERTY, description: FEATURE_DESCRIPTION_PROPERTY, type: FEATURE_TYPE_PROPERTY, @@ -274,24 +397,24 @@ export const featureToolDefinitions: Tool[] = [ }, required: ['key'], }, + outputSchema: { + type: 'object' as const, + properties: { + result: FEATURE_OBJECT_SCHEMA, + dashboardLink: DASHBOARD_LINK_PROPERTY, + }, + required: ['result', 'dashboardLink'], + }, }, { name: 'update_feature_status', description: - 'Update the status of an existing feature flag. ⚠️ IMPORTANT: Changes to feature status may affect production environments. Always confirm with the user before making changes to features that are active in production.', + 'Update the status of an existing feature flag. ⚠️ IMPORTANT: Changes to feature status may affect production environments. Always confirm with the user before making changes to features that are active in production. Include dashboard link in the response.', inputSchema: { type: 'object', properties: { - key: { - type: 'string', - description: - 'The key of the feature to update status for (1-100 characters, must match pattern ^[a-z0-9-_.]+$)', - }, - status: { - type: 'string', - enum: ['active', 'complete', 'archived'], - description: 'The status to set the feature to', - }, + key: FEATURE_KEY_PROPERTY, + status: FEATURE_STATUS_PROPERTY, staticVariation: { type: 'string', description: @@ -300,11 +423,19 @@ export const featureToolDefinitions: Tool[] = [ }, required: ['key', 'status'], }, + outputSchema: { + type: 'object' as const, + properties: { + result: FEATURE_OBJECT_SCHEMA, + dashboardLink: DASHBOARD_LINK_PROPERTY, + }, + required: ['result', 'dashboardLink'], + }, }, { name: 'delete_feature', description: - 'Delete an existing feature flag. ⚠️ CRITICAL: Deleting a feature flag will remove it from ALL environments including production. ALWAYS confirm with the user before deleting any feature flag.', + 'Delete an existing feature flag. ⚠️ CRITICAL: Deleting a feature flag will remove it from ALL environments including production. ALWAYS confirm with the user before deleting any feature flag. Include dashboard link in the response.', inputSchema: { type: 'object', properties: { @@ -315,10 +446,19 @@ export const featureToolDefinitions: Tool[] = [ }, required: ['key'], }, + outputSchema: { + type: 'object' as const, + properties: { + result: MESSAGE_RESPONSE_SCHEMA, + dashboardLink: DASHBOARD_LINK_PROPERTY, + }, + required: ['result', 'dashboardLink'], + }, }, { name: 'fetch_feature_variations', - description: 'Get a list of variations for a feature', + description: + 'Get a list of variations for a feature. Include dashboard link in the response.', inputSchema: { type: 'object', properties: { @@ -326,10 +466,23 @@ export const featureToolDefinitions: Tool[] = [ }, required: ['feature_key'], }, + outputSchema: { + type: 'object' as const, + properties: { + result: { + type: 'array' as const, + description: 'Array of variation objects for the feature', + items: VARIATION_OBJECT_SCHEMA, + }, + dashboardLink: DASHBOARD_LINK_PROPERTY, + }, + required: ['result', 'dashboardLink'], + }, }, { name: 'create_feature_variation', - description: 'Create a new variation within a feature', + description: + 'Create a new variation within a feature. Include dashboard link in the response.', inputSchema: { type: 'object', properties: { @@ -345,60 +498,89 @@ export const featureToolDefinitions: Tool[] = [ }, required: ['feature_key', 'key', 'name'], }, + outputSchema: { + type: 'object' as const, + properties: { + result: VARIATION_OBJECT_SCHEMA, + dashboardLink: DASHBOARD_LINK_PROPERTY, + }, + required: ['result', 'dashboardLink'], + }, }, { name: 'update_feature_variation', - description: 'Update an existing variation by key', + description: + 'Update an existing variation by key. Include dashboard link in the response.', inputSchema: { type: 'object', properties: { - feature_key: FEATURE_KEY_PROPERTY, - variation_key: { - type: 'string', - description: 'The key of the variation to update', - }, - key: { - type: 'string', - description: - 'New variation key (max 100 characters, pattern: ^[a-z0-9-_.]+$)', - }, - name: { - type: 'string', - description: 'New variation name (max 100 characters)', - }, - variables: VARIATION_VARIABLES_PROPERTY, _id: { type: 'string', - description: 'Internal variation ID (optional)', + description: 'MongoDB ID for the variation', }, + feature_key: FEATURE_KEY_PROPERTY, + variation_key: VARIATION_KEY_PROPERTY, + key: VARIATION_KEY_PROPERTY, + name: VARIATION_NAME_PROPERTY, + variables: VARIATION_VARIABLES_PROPERTY, }, required: ['feature_key', 'variation_key'], }, + outputSchema: { + type: 'object' as const, + properties: { + result: VARIATION_OBJECT_SCHEMA, + dashboardLink: DASHBOARD_LINK_PROPERTY, + }, + required: ['result', 'dashboardLink'], + }, }, { name: 'enable_feature_targeting', description: - 'Enable targeting for a feature in an environment. ⚠️ IMPORTANT: Always confirm with the user before making changes to production environments (environments where type = "production").', + 'Enable targeting for a feature in an environment. ⚠️ IMPORTANT: Always confirm with the user before making changes to production environments (environments where type = "production"). Include dashboard link in the response.', inputSchema: { type: 'object', - properties: FEATURE_ENVIRONMENT_REQUIRED_PROPERTIES, + properties: { + feature_key: FEATURE_KEY_PROPERTY, + environment_key: ENVIRONMENT_KEY_PROPERTY, + }, required: ['feature_key', 'environment_key'], }, + outputSchema: { + type: 'object' as const, + properties: { + result: MESSAGE_RESPONSE_SCHEMA, + dashboardLink: DASHBOARD_LINK_PROPERTY, + }, + required: ['result', 'dashboardLink'], + }, }, { name: 'disable_feature_targeting', description: - 'Disable targeting for a feature in an environment. ⚠️ IMPORTANT: Always confirm with the user before making changes to production environments (environments where type = "production").', + 'Disable targeting for a feature in an environment. ⚠️ IMPORTANT: Always confirm with the user before making changes to production environments (environments where type = "production"). Include dashboard link in the response.', inputSchema: { type: 'object', - properties: FEATURE_ENVIRONMENT_REQUIRED_PROPERTIES, + properties: { + feature_key: FEATURE_KEY_PROPERTY, + environment_key: ENVIRONMENT_KEY_PROPERTY, + }, required: ['feature_key', 'environment_key'], }, + outputSchema: { + type: 'object' as const, + properties: { + result: MESSAGE_RESPONSE_SCHEMA, + dashboardLink: DASHBOARD_LINK_PROPERTY, + }, + required: ['result', 'dashboardLink'], + }, }, { name: 'list_feature_targeting', description: - 'List feature configurations (targeting rules) for a feature', + 'List feature configurations (targeting rules) for a feature. Include dashboard link in the response.', inputSchema: { type: 'object', properties: { @@ -407,11 +589,22 @@ export const featureToolDefinitions: Tool[] = [ }, required: ['feature_key'], }, + outputSchema: { + type: 'object' as const, + properties: { + result: { + type: 'object' as const, + description: 'Feature targeting configuration object', + }, + dashboardLink: DASHBOARD_LINK_PROPERTY, + }, + required: ['result', 'dashboardLink'], + }, }, { name: 'update_feature_targeting', description: - 'Update feature configuration (targeting rules) for a feature in an environment. ⚠️ IMPORTANT: Always confirm with the user before making changes to production environments (environments where type = "production").', + 'Update feature configuration (targeting rules) for a feature in an environment. ⚠️ IMPORTANT: Always confirm with the user before making changes to production environments (environments where type = "production"). Include dashboard link in the response.', inputSchema: { type: 'object', properties: { @@ -431,45 +624,12 @@ export const featureToolDefinitions: Tool[] = [ properties: { _id: { type: 'string', - description: - 'Target ID (optional for new targets)', + description: 'MongoDB ID for the target', }, name: { type: 'string', - description: 'Target name (optional)', - }, - audience: { - type: 'object', - description: - 'Audience definition for the target', - properties: { - name: { - type: 'string', - description: - 'Audience name (max 100 characters, optional)', - }, - filters: { - type: 'object', - description: - 'Audience filters with logical operator', - properties: { - filters: { - type: 'array', - description: - 'Array of filter conditions (supports all, optIn, user, userCountry, userAppVersion, userPlatformVersion, userCustom, audienceMatch filters)', - }, - operator: { - type: 'string', - enum: ['and', 'or'], - description: - 'Logical operator for combining filters', - }, - }, - required: ['filters', 'operator'], - }, - }, - required: ['filters'], }, + audience: TARGET_AUDIENCE_PROPERTY, distribution: { type: 'array', description: @@ -563,11 +723,22 @@ export const featureToolDefinitions: Tool[] = [ }, required: ['feature_key', 'environment_key'], }, + outputSchema: { + type: 'object' as const, + properties: { + result: { + type: 'object' as const, + description: 'Updated feature targeting configuration', + }, + dashboardLink: DASHBOARD_LINK_PROPERTY, + }, + required: ['result', 'dashboardLink'], + }, }, { name: 'get_feature_audit_log_history', description: - 'Get timeline of feature flag changes from DevCycle audit log', + 'Get timeline of feature flag changes from DevCycle audit log. Include dashboard link in the response.', inputSchema: { type: 'object', properties: { @@ -582,6 +753,20 @@ export const featureToolDefinitions: Tool[] = [ }, required: ['feature_key'], }, + outputSchema: { + type: 'object' as const, + properties: { + result: { + type: 'array' as const, + description: 'Array of audit log entries for the feature', + items: { + type: 'object' as const, + }, + }, + dashboardLink: DASHBOARD_LINK_PROPERTY, + }, + required: ['result', 'dashboardLink'], + }, }, ] @@ -589,18 +774,19 @@ export const featureToolHandlers: Record<string, ToolHandler> = { list_features: async (args: unknown, apiClient: DevCycleApiClient) => { const validatedArgs = ListFeaturesArgsSchema.parse(args) - return await apiClient.executeWithLogging( + return await apiClient.executeWithDashboardLink( 'listFeatures', validatedArgs, async (authToken, projectKey) => { return await fetchFeatures(authToken, projectKey, validatedArgs) }, + generateFeaturesDashboardLink, ) }, create_feature: async (args: unknown, apiClient: DevCycleApiClient) => { const validatedArgs = CreateFeatureArgsSchema.parse(args) - return await apiClient.executeWithLogging( + return await apiClient.executeWithDashboardLink( 'createFeature', validatedArgs, async (authToken, projectKey) => { @@ -622,12 +808,19 @@ export const featureToolHandlers: Record<string, ToolHandler> = { return await createFeature(authToken, projectKey, featureData) }, + (orgId, projectKey, result) => + generateFeatureDashboardLink( + orgId, + projectKey, + result.key, + 'overview', + ), ) }, update_feature: async (args: unknown, apiClient: DevCycleApiClient) => { const validatedArgs = UpdateFeatureArgsSchema.parse(args) - return await apiClient.executeWithLogging( + return await apiClient.executeWithDashboardLink( 'updateFeature', validatedArgs, async (authToken, projectKey) => { @@ -640,6 +833,13 @@ export const featureToolHandlers: Record<string, ToolHandler> = { updateData, ) }, + (orgId, projectKey, result) => + generateFeatureDashboardLink( + orgId, + projectKey, + result.key, + 'manage-feature', + ), ) }, update_feature_status: async ( @@ -648,7 +848,7 @@ export const featureToolHandlers: Record<string, ToolHandler> = { ) => { const validatedArgs = UpdateFeatureStatusArgsSchema.parse(args) - return await apiClient.executeWithLogging( + return await apiClient.executeWithDashboardLink( 'updateFeatureStatus', validatedArgs, async (authToken, projectKey) => { @@ -661,21 +861,28 @@ export const featureToolHandlers: Record<string, ToolHandler> = { statusData, ) }, + (orgId, projectKey, result) => + generateFeatureDashboardLink( + orgId, + projectKey, + result.key, + 'overview', + ), ) }, delete_feature: async (args: unknown, apiClient: DevCycleApiClient) => { const validatedArgs = DeleteFeatureArgsSchema.parse(args) - return await apiClient.executeWithLogging( + return await apiClient.executeWithDashboardLink( 'deleteFeature', validatedArgs, async (authToken, projectKey) => { - return await deleteFeature( - authToken, - projectKey, - validatedArgs.key, - ) + await deleteFeature(authToken, projectKey, validatedArgs.key) + return { + message: `Feature '${validatedArgs.key}' deleted successfully`, + } }, + generateFeaturesDashboardLink, ) }, fetch_feature_variations: async ( @@ -684,7 +891,7 @@ export const featureToolHandlers: Record<string, ToolHandler> = { ) => { const validatedArgs = ListVariationsArgsSchema.parse(args) - return await apiClient.executeWithLogging( + return await apiClient.executeWithDashboardLink( 'fetchFeatureVariations', validatedArgs, async (authToken, projectKey) => { @@ -694,6 +901,13 @@ export const featureToolHandlers: Record<string, ToolHandler> = { validatedArgs.feature_key, ) }, + (orgId, projectKey) => + generateFeatureDashboardLink( + orgId, + projectKey, + validatedArgs.feature_key, + 'overview', + ), ) }, create_feature_variation: async ( @@ -702,7 +916,7 @@ export const featureToolHandlers: Record<string, ToolHandler> = { ) => { const validatedArgs = CreateVariationArgsSchema.parse(args) - return await apiClient.executeWithLogging( + return await apiClient.executeWithDashboardLink( 'createFeatureVariation', validatedArgs, async (authToken, projectKey) => { @@ -715,6 +929,13 @@ export const featureToolHandlers: Record<string, ToolHandler> = { variationData, ) }, + (orgId, projectKey, result) => + generateFeatureDashboardLink( + orgId, + projectKey, + result.key, + 'manage-feature', + ), ) }, update_feature_variation: async ( @@ -723,7 +944,7 @@ export const featureToolHandlers: Record<string, ToolHandler> = { ) => { const validatedArgs = UpdateVariationArgsSchema.parse(args) - return await apiClient.executeWithLogging( + return await apiClient.executeWithDashboardLink( 'updateFeatureVariation', validatedArgs, async (authToken, projectKey) => { @@ -738,6 +959,13 @@ export const featureToolHandlers: Record<string, ToolHandler> = { variationData, ) }, + (orgId, projectKey, result) => + generateFeatureDashboardLink( + orgId, + projectKey, + result.key, + 'manage-feature', + ), ) }, enable_feature_targeting: async ( @@ -746,17 +974,27 @@ export const featureToolHandlers: Record<string, ToolHandler> = { ) => { const validatedArgs = EnableTargetingArgsSchema.parse(args) - return await apiClient.executeWithLogging( + return await apiClient.executeWithDashboardLink( 'enableTargeting', validatedArgs, async (authToken, projectKey) => { - return await enableTargeting( + await enableTargeting( authToken, projectKey, validatedArgs.feature_key, validatedArgs.environment_key, ) + return { + message: `Targeting enabled for feature '${validatedArgs.feature_key}' in environment '${validatedArgs.environment_key}'`, + } }, + (orgId, projectKey) => + generateFeatureDashboardLink( + orgId, + projectKey, + validatedArgs.feature_key, + 'manage-feature', + ), ) }, disable_feature_targeting: async ( @@ -765,17 +1003,27 @@ export const featureToolHandlers: Record<string, ToolHandler> = { ) => { const validatedArgs = DisableTargetingArgsSchema.parse(args) - return await apiClient.executeWithLogging( + return await apiClient.executeWithDashboardLink( 'disableTargeting', validatedArgs, async (authToken, projectKey) => { - return await disableTargeting( + await disableTargeting( authToken, projectKey, validatedArgs.feature_key, validatedArgs.environment_key, ) + return { + message: `Targeting disabled for feature '${validatedArgs.feature_key}' in environment '${validatedArgs.environment_key}'`, + } }, + (orgId, projectKey) => + generateFeatureDashboardLink( + orgId, + projectKey, + validatedArgs.feature_key, + 'manage-feature', + ), ) }, list_feature_targeting: async ( @@ -784,7 +1032,7 @@ export const featureToolHandlers: Record<string, ToolHandler> = { ) => { const validatedArgs = ListFeatureTargetingArgsSchema.parse(args) - return await apiClient.executeWithLogging( + return await apiClient.executeWithDashboardLink( 'listFeatureTargeting', validatedArgs, async (authToken, projectKey) => { @@ -795,6 +1043,13 @@ export const featureToolHandlers: Record<string, ToolHandler> = { validatedArgs.environment_key, ) }, + (orgId, projectKey) => + generateFeatureDashboardLink( + orgId, + projectKey, + validatedArgs.feature_key, + 'manage-feature', + ), ) }, update_feature_targeting: async ( @@ -803,7 +1058,7 @@ export const featureToolHandlers: Record<string, ToolHandler> = { ) => { const validatedArgs = UpdateFeatureTargetingArgsSchema.parse(args) - return await apiClient.executeWithLogging( + return await apiClient.executeWithDashboardLink( 'updateFeatureTargeting', validatedArgs, async (authToken, projectKey) => { @@ -818,6 +1073,13 @@ export const featureToolHandlers: Record<string, ToolHandler> = { configData, ) }, + (orgId, projectKey) => + generateFeatureDashboardLink( + orgId, + projectKey, + validatedArgs.feature_key, + 'manage-feature', + ), ) }, get_feature_audit_log_history: async ( @@ -826,7 +1088,7 @@ export const featureToolHandlers: Record<string, ToolHandler> = { ) => { const validatedArgs = GetFeatureAuditLogHistoryArgsSchema.parse(args) - return await apiClient.executeWithLogging( + return await apiClient.executeWithDashboardLink( 'getFeatureAuditLogHistory', validatedArgs, async (authToken, projectKey) => { @@ -837,6 +1099,13 @@ export const featureToolHandlers: Record<string, ToolHandler> = { validatedArgs.days_back || 30, ) }, + (orgId, projectKey) => + generateFeatureDashboardLink( + orgId, + projectKey, + validatedArgs.feature_key, + 'audit-log', + ), ) }, } diff --git a/src/mcp/tools/projectTools.ts b/src/mcp/tools/projectTools.ts index 72a82c7f0..da89d1632 100644 --- a/src/mcp/tools/projectTools.ts +++ b/src/mcp/tools/projectTools.ts @@ -12,108 +12,191 @@ import { UpdateProjectArgsSchema, } from '../types' import { ToolHandler } from '../server' +import { DASHBOARD_LINK_PROPERTY, PROJECT_KEY_PROPERTY } from './commonSchemas' + +// Helper functions to generate project dashboard links +const generateProjectDashboardLink = ( + orgId: string, + projectKey: string, +): string => { + return `https://app.devcycle.com/o/${orgId}/p/${projectKey}` +} + +const generateOrganizationSettingsLink = (orgId: string): string => { + return `https://app.devcycle.com/o/${orgId}/settings` +} + +const generateEditProjectLink = (orgId: string, projectKey: string): string => { + return `https://app.devcycle.com/o/${orgId}/settings/projects/${projectKey}/edit` +} + +// ============================================================================= +// INPUT SCHEMAS +// ============================================================================= + +const PROJECT_COMMON_PROPERTIES = { + name: { + type: 'string' as const, + description: 'Project name', + }, + description: { + type: 'string' as const, + description: 'Project description', + }, + key: PROJECT_KEY_PROPERTY, + color: { + type: 'string' as const, + description: 'Project color (hex format)', + }, +} + +const PROJECT_PAGINATION_PROPERTIES = { + page: { + type: 'number' as const, + description: 'Page number', + minimum: 1, + default: 1, + }, + perPage: { + type: 'number' as const, + description: 'Items per page', + minimum: 1, + maximum: 1000, + default: 100, + }, + sortBy: { + type: 'string' as const, + description: 'Sort field', + enum: [ + 'createdAt', + 'updatedAt', + 'name', + 'key', + 'createdBy', + 'propertyKey', + ], + default: 'createdAt', + }, + sortOrder: { + type: 'string' as const, + description: 'Sort order', + enum: ['asc', 'desc'], + default: 'desc', + }, + search: { + type: 'string' as const, + description: 'Search query to filter results', + }, + createdBy: { + type: 'string' as const, + description: 'Filter by creator', + }, +} + +// ============================================================================= +// OUTPUT SCHEMAS +// ============================================================================= + +const PROJECT_OBJECT_SCHEMA = { + type: 'object' as const, + description: 'A DevCycle project configuration', + properties: { + _id: { + type: 'string' as const, + description: 'Unique identifier for the project', + }, + key: PROJECT_KEY_PROPERTY, + name: { + type: 'string' as const, + description: 'Display name of the project', + }, + description: { + type: 'string' as const, + description: 'Optional description of the project', + }, + color: { + type: 'string' as const, + description: 'Color used to represent this project in the UI', + }, + createdAt: { + type: 'string' as const, + description: 'ISO timestamp when the project was created', + }, + updatedAt: { + type: 'string' as const, + description: 'ISO timestamp when the project was last updated', + }, + }, + required: ['_id', 'key', 'name', 'createdAt', 'updatedAt'], +} + +// Complete output schema definitions +const PROJECT_OUTPUT_SCHEMA = { + type: 'object' as const, + properties: { + result: PROJECT_OBJECT_SCHEMA, + dashboardLink: DASHBOARD_LINK_PROPERTY, + }, + required: ['result', 'dashboardLink'], +} + +// ============================================================================= +// TOOL DEFINITIONS +// ============================================================================= export const projectToolDefinitions: Tool[] = [ { name: 'list_projects', - description: 'List all projects in the current organization', + description: + 'List all projects in the current organization. Include dashboard link in the response.', inputSchema: { type: 'object', + properties: PROJECT_PAGINATION_PROPERTIES, + }, + outputSchema: { + type: 'object' as const, properties: { - sortBy: { - type: 'string', - enum: [ - 'createdAt', - 'updatedAt', - 'name', - 'key', - 'createdBy', - 'propertyKey', - ], - description: 'Field to sort by (default: createdAt)', - }, - sortOrder: { - type: 'string', - enum: ['asc', 'desc'], - description: 'Sort order (default: desc)', - }, - search: { - type: 'string', - description: - 'Search query to filter projects (minimum 3 characters)', - }, - createdBy: { - type: 'string', - description: 'Filter by creator user ID', - }, - page: { - type: 'number', - description: 'Page number (default: 1)', - }, - perPage: { - type: 'number', - description: - 'Number of items per page (default: 100, max: 1000)', + result: { + type: 'array' as const, + description: 'Array of project objects in the organization', + items: PROJECT_OBJECT_SCHEMA, }, + dashboardLink: DASHBOARD_LINK_PROPERTY, }, + required: ['result', 'dashboardLink'], }, }, { name: 'get_current_project', - description: 'Get the currently selected project', + description: + 'Get the currently selected project. Include dashboard link in the response.', inputSchema: { type: 'object', properties: {}, }, + outputSchema: PROJECT_OUTPUT_SCHEMA, }, { name: 'create_project', - description: 'Create a new project', + description: + 'Create a new project. Include dashboard link in the response.', inputSchema: { type: 'object', - properties: { - name: { - type: 'string', - description: 'Project name', - }, - description: { - type: 'string', - description: 'Project description', - }, - key: { - type: 'string', - description: 'Unique project key', - }, - }, + properties: PROJECT_COMMON_PROPERTIES, required: ['name', 'key'], }, + outputSchema: PROJECT_OUTPUT_SCHEMA, }, { name: 'update_project', - description: 'Update an existing project', + description: + 'Update an existing project. Include dashboard link in the response.', inputSchema: { type: 'object', - properties: { - key: { - type: 'string', - description: - 'Project key to identify the project to update', - }, - name: { - type: 'string', - description: 'Updated project name', - }, - description: { - type: 'string', - description: 'Updated project description', - }, - color: { - type: 'string', - description: 'Project color (hex format)', - }, - }, + properties: PROJECT_COMMON_PROPERTIES, required: ['key'], }, + outputSchema: PROJECT_OUTPUT_SCHEMA, }, ] @@ -121,50 +204,54 @@ export const projectToolHandlers: Record<string, ToolHandler> = { list_projects: async (args: unknown, apiClient: DevCycleApiClient) => { const validatedArgs = ListProjectsArgsSchema.parse(args) - return await apiClient.executeWithLogging( + return await apiClient.executeWithDashboardLink( 'listProjects', validatedArgs, async (authToken) => { + // projectKey not used for listing all projects return await fetchProjects(authToken, validatedArgs) }, - false, + generateOrganizationSettingsLink, ) }, get_current_project: async ( args: unknown, apiClient: DevCycleApiClient, ) => { - return await apiClient.executeWithLogging( + return await apiClient.executeWithDashboardLink( 'getCurrentProject', null, async (authToken, projectKey) => { return await fetchProject(authToken, projectKey) }, + generateProjectDashboardLink, ) }, create_project: async (args: unknown, apiClient: DevCycleApiClient) => { const validatedArgs = CreateProjectArgsSchema.parse(args) - return await apiClient.executeWithLogging( + return await apiClient.executeWithDashboardLink( 'createProject', validatedArgs, async (authToken) => { + // projectKey not used for creating projects return await createProject(authToken, validatedArgs) }, - false, + generateProjectDashboardLink, ) }, update_project: async (args: unknown, apiClient: DevCycleApiClient) => { const validatedArgs = UpdateProjectArgsSchema.parse(args) const { key, ...updateParams } = validatedArgs - return await apiClient.executeWithLogging( + return await apiClient.executeWithDashboardLink( 'updateProject', validatedArgs, async (authToken) => { + // projectKey not used - we use the key from validated args return await updateProject(authToken, key, updateParams) }, - false, + generateEditProjectLink, ) }, } diff --git a/src/mcp/tools/selfTargetingTools.ts b/src/mcp/tools/selfTargetingTools.ts index 23d54a6a6..fc6d4432e 100644 --- a/src/mcp/tools/selfTargetingTools.ts +++ b/src/mcp/tools/selfTargetingTools.ts @@ -13,91 +13,184 @@ import { ClearSelfTargetingOverridesArgsSchema, } from '../types' import { ToolHandler } from '../server' +import { + DASHBOARD_LINK_PROPERTY, + MESSAGE_RESPONSE_SCHEMA, + FEATURE_KEY_PROPERTY, + ENVIRONMENT_KEY_PROPERTY, + VARIATION_KEY_PROPERTY, +} from './commonSchemas' + +// Helper functions to generate dashboard links +const generateSelfTargetingDashboardLink = (orgId: string): string => { + return `https://app.devcycle.com/o/${orgId}/settings/profile-overrides` +} + +// ============================================================================= +// INPUT SCHEMAS +// ============================================================================= + +const DVC_USER_ID_PROPERTY = { + type: 'string' as const, + description: + 'DevCycle User ID for self-targeting (use null or empty string to clear)', +} + +const OVERRIDE_COMMON_PROPERTIES = { + feature_key: FEATURE_KEY_PROPERTY, + environment_key: ENVIRONMENT_KEY_PROPERTY, + variation_key: VARIATION_KEY_PROPERTY, +} + +// ============================================================================= +// OUTPUT SCHEMAS +// ============================================================================= + +const USER_PROFILE_OBJECT_SCHEMA = { + type: 'object' as const, + description: 'DevCycle user profile for self-targeting', + properties: { + dvcUserId: { + type: 'string' as const, + description: 'DevCycle User ID for self-targeting', + }, + }, +} + +const OVERRIDE_OBJECT_SCHEMA = { + type: 'object' as const, + description: 'A self-targeting override configuration', + properties: { + feature: { + type: 'string' as const, + description: 'Feature key', + }, + environment: { + type: 'string' as const, + description: 'Environment key', + }, + variation: { + type: 'string' as const, + description: 'Variation key', + }, + }, + required: ['feature', 'environment', 'variation'], +} + +// Complete output schema definitions +const SELF_TARGETING_IDENTITY_OUTPUT_SCHEMA = { + type: 'object' as const, + properties: { + result: USER_PROFILE_OBJECT_SCHEMA, + dashboardLink: DASHBOARD_LINK_PROPERTY, + }, + required: ['result', 'dashboardLink'], +} + +// ============================================================================= +// TOOL DEFINITIONS +// ============================================================================= export const selfTargetingToolDefinitions: Tool[] = [ { name: 'get_self_targeting_identity', - description: 'Get current DevCycle identity for self-targeting', + description: + 'Get current DevCycle identity for self-targeting. Include dashboard link in the response.', inputSchema: { type: 'object', properties: {}, }, + outputSchema: SELF_TARGETING_IDENTITY_OUTPUT_SCHEMA, }, { name: 'update_self_targeting_identity', description: - 'Update DevCycle identity for self-targeting and overrides', + 'Update DevCycle identity for self-targeting and overrides. Include dashboard link in the response.', inputSchema: { type: 'object', properties: { - dvc_user_id: { - type: 'string', - description: - 'DevCycle User ID for self-targeting (use null or empty string to clear)', - }, + dvc_user_id: DVC_USER_ID_PROPERTY, }, required: ['dvc_user_id'], }, + outputSchema: SELF_TARGETING_IDENTITY_OUTPUT_SCHEMA, }, { name: 'list_self_targeting_overrides', description: - 'List all self-targeting overrides for the current project', + 'List all self-targeting overrides for the current project. Include dashboard link in the response.', inputSchema: { type: 'object', properties: {}, }, + outputSchema: { + type: 'object' as const, + properties: { + result: { + type: 'array' as const, + description: 'Array of self-targeting override objects', + items: OVERRIDE_OBJECT_SCHEMA, + }, + dashboardLink: DASHBOARD_LINK_PROPERTY, + }, + required: ['result', 'dashboardLink'], + }, }, { name: 'set_self_targeting_override', description: - 'Set a self-targeting override for a feature variation. ⚠️ IMPORTANT: Always confirm with the user before setting overrides for production environments (environments where type = "production").', + 'Set a self-targeting override for a feature variation. ⚠️ IMPORTANT: Always confirm with the user before setting overrides for production environments (environments where type = "production"). Include dashboard link in the response.', inputSchema: { type: 'object', + properties: OVERRIDE_COMMON_PROPERTIES, + required: ['feature_key', 'environment_key', 'variation_key'], + }, + outputSchema: { + type: 'object' as const, properties: { - feature_key: { - type: 'string', - description: 'The key of the feature', - }, - environment_key: { - type: 'string', - description: 'The key of the environment', - }, - variation_key: { - type: 'string', - description: 'The key of the variation to serve', - }, + result: OVERRIDE_OBJECT_SCHEMA, + dashboardLink: DASHBOARD_LINK_PROPERTY, }, - required: ['feature_key', 'environment_key', 'variation_key'], + required: ['result', 'dashboardLink'], }, }, { name: 'clear_feature_self_targeting_overrides', description: - 'Clear self-targeting overrides for a specific feature/environment. ⚠️ IMPORTANT: Always confirm with the user before clearing overrides for production environments (environments where type = "production").', + 'Clear self-targeting overrides for a specific feature/environment. ⚠️ IMPORTANT: Always confirm with the user before clearing overrides for production environments (environments where type = "production"). Include dashboard link in the response.', inputSchema: { type: 'object', properties: { - feature_key: { - type: 'string', - description: 'The key of the feature', - }, - environment_key: { - type: 'string', - description: 'The key of the environment', - }, + feature_key: FEATURE_KEY_PROPERTY, + environment_key: ENVIRONMENT_KEY_PROPERTY, }, required: ['feature_key', 'environment_key'], }, + outputSchema: { + type: 'object' as const, + properties: { + result: MESSAGE_RESPONSE_SCHEMA, + dashboardLink: DASHBOARD_LINK_PROPERTY, + }, + required: ['result', 'dashboardLink'], + }, }, { name: 'clear_all_self_targeting_overrides', description: - 'Clear all self-targeting overrides for the current project', + 'Clear all self-targeting overrides for the current project. ⚠️ IMPORTANT: Always confirm with the user before clearing all overrides as it can clear production environments (environments where type = "production"). Include dashboard link in the response.', inputSchema: { type: 'object', properties: {}, }, + outputSchema: { + type: 'object' as const, + properties: { + result: MESSAGE_RESPONSE_SCHEMA, + dashboardLink: DASHBOARD_LINK_PROPERTY, + }, + required: ['result', 'dashboardLink'], + }, }, ] @@ -106,12 +199,13 @@ export const selfTargetingToolHandlers: Record<string, ToolHandler> = { args: unknown, apiClient: DevCycleApiClient, ) => { - return await apiClient.executeWithLogging( + return await apiClient.executeWithDashboardLink( 'getSelfTargetingIdentity', null, async (authToken, projectKey) => { return await fetchUserProfile(authToken, projectKey) }, + generateSelfTargetingDashboardLink, ) }, update_self_targeting_identity: async ( @@ -120,7 +214,7 @@ export const selfTargetingToolHandlers: Record<string, ToolHandler> = { ) => { const validatedArgs = UpdateSelfTargetingIdentityArgsSchema.parse(args) - return await apiClient.executeWithLogging( + return await apiClient.executeWithDashboardLink( 'updateSelfTargetingIdentity', validatedArgs, async (authToken, projectKey) => { @@ -128,18 +222,20 @@ export const selfTargetingToolHandlers: Record<string, ToolHandler> = { dvcUserId: validatedArgs.dvc_user_id, }) }, + generateSelfTargetingDashboardLink, ) }, list_self_targeting_overrides: async ( args: unknown, apiClient: DevCycleApiClient, ) => { - return await apiClient.executeWithLogging( + return await apiClient.executeWithDashboardLink( 'listSelfTargetingOverrides', null, async (authToken, projectKey) => { return await fetchProjectOverridesForUser(authToken, projectKey) }, + generateSelfTargetingDashboardLink, ) }, set_self_targeting_override: async ( @@ -148,7 +244,7 @@ export const selfTargetingToolHandlers: Record<string, ToolHandler> = { ) => { const validatedArgs = SetSelfTargetingOverrideArgsSchema.parse(args) - return await apiClient.executeWithLogging( + return await apiClient.executeWithDashboardLink( 'setSelfTargetingOverride', validatedArgs, async (authToken, projectKey) => { @@ -162,6 +258,7 @@ export const selfTargetingToolHandlers: Record<string, ToolHandler> = { }, ) }, + generateSelfTargetingDashboardLink, ) }, clear_feature_self_targeting_overrides: async ( @@ -170,7 +267,7 @@ export const selfTargetingToolHandlers: Record<string, ToolHandler> = { ) => { const validatedArgs = ClearSelfTargetingOverridesArgsSchema.parse(args) - return await apiClient.executeWithLogging( + return await apiClient.executeWithDashboardLink( 'clearFeatureSelfTargetingOverrides', validatedArgs, async (authToken, projectKey) => { @@ -185,19 +282,21 @@ export const selfTargetingToolHandlers: Record<string, ToolHandler> = { message: `Cleared override for feature '${validatedArgs.feature_key}' in environment '${validatedArgs.environment_key}'`, } }, + generateSelfTargetingDashboardLink, ) }, clear_all_self_targeting_overrides: async ( args: unknown, apiClient: DevCycleApiClient, ) => { - return await apiClient.executeWithLogging( + return await apiClient.executeWithDashboardLink( 'clearAllSelfTargetingOverrides', null, async (authToken, projectKey) => { await deleteAllProjectOverrides(authToken, projectKey) return { message: 'Cleared all overrides for the project' } }, + generateSelfTargetingDashboardLink, ) }, } diff --git a/src/mcp/tools/variableTools.ts b/src/mcp/tools/variableTools.ts index 9b5e77f49..f9b231632 100644 --- a/src/mcp/tools/variableTools.ts +++ b/src/mcp/tools/variableTools.ts @@ -13,168 +13,278 @@ import { DeleteVariableArgsSchema, } from '../types' import { ToolHandler } from '../server' +import { + DASHBOARD_LINK_PROPERTY, + MESSAGE_RESPONSE_SCHEMA, + VARIABLE_KEY_PROPERTY, +} from './commonSchemas' + +// Helper function to generate variable dashboard links +const generateVariablesDashboardLink = ( + orgId: string, + projectKey: string, +): string => { + return `https://app.devcycle.com/o/${orgId}/p/${projectKey}/variables` +} + +// ============================================================================= +// INPUT SCHEMAS +// ============================================================================= +const VARIABLE_PAGINATION_PROPERTIES = { + page: { + type: 'number' as const, + description: 'Page number', + minimum: 1, + default: 1, + }, + perPage: { + type: 'number' as const, + description: 'Items per page', + minimum: 1, + maximum: 1000, + default: 100, + }, + sortBy: { + type: 'string' as const, + description: 'Sort field', + enum: [ + 'createdAt', + 'updatedAt', + 'name', + 'key', + 'createdBy', + 'propertyKey', + ], + default: 'createdAt', + }, + sortOrder: { + type: 'string' as const, + description: 'Sort order', + enum: ['asc', 'desc'], + default: 'desc', + }, + search: { + type: 'string' as const, + description: 'Search query to filter variables', + minLength: 3, + }, + feature: { + type: 'string' as const, + description: 'Filter by feature', + }, + type: { + type: 'string' as const, + description: 'Filter by variable type', + enum: ['String', 'Boolean', 'Number', 'JSON'], + }, + status: { + type: 'string' as const, + description: 'Filter by variable status', + enum: ['active', 'archived'], + }, +} + +const VARIABLE_TYPE_PROPERTY = { + type: 'string' as const, + enum: ['String', 'Boolean', 'Number', 'JSON'] as const, + description: 'Variable type', +} + +const VALIDATION_SCHEMA_PROPERTY = { + type: 'object' as const, + description: 'Validation schema for variable values', + properties: { + schemaType: { + type: 'string' as const, + description: 'Schema type', + }, + enumValues: { + type: 'array' as const, + description: 'Allowed enum values', + }, + regexPattern: { + type: 'string' as const, + description: 'Regex pattern for validation', + }, + jsonSchema: { + type: 'string' as const, + description: 'JSON schema for validation', + }, + description: { + type: 'string' as const, + description: 'Schema description', + }, + exampleValue: { + description: 'Example value for the schema', + }, + }, +} + +const VARIABLE_COMMON_PROPERTIES = { + name: { + type: 'string' as const, + description: 'Variable name (1-100 characters)', + }, + description: { + type: 'string' as const, + description: 'Variable description (max 1000 characters)', + }, + key: { + type: 'string' as const, + description: + 'Unique variable key (1-100 characters, must match pattern ^[a-z0-9-_.]+$)', + }, + _feature: { + type: 'string' as const, + description: 'Feature key or ID to associate with this variable', + }, + type: VARIABLE_TYPE_PROPERTY, + defaultValue: { + description: 'Default value for the variable', + }, + validationSchema: VALIDATION_SCHEMA_PROPERTY, +} + +const UPDATE_VARIABLE_PROPERTIES = { + key: { + type: 'string' as const, + description: 'Current variable key', + }, + name: VARIABLE_COMMON_PROPERTIES.name, + description: VARIABLE_COMMON_PROPERTIES.description, + type: VARIABLE_COMMON_PROPERTIES.type, + validationSchema: VARIABLE_COMMON_PROPERTIES.validationSchema, +} + +// ============================================================================= +// OUTPUT SCHEMAS +// ============================================================================= + +const VARIABLE_OBJECT_SCHEMA = { + type: 'object' as const, + description: 'A DevCycle variable configuration', + properties: { + _id: { + type: 'string' as const, + description: 'Unique identifier for the variable', + }, + key: VARIABLE_KEY_PROPERTY, + name: { + type: 'string' as const, + description: 'Display name of the variable', + }, + description: { + type: 'string' as const, + description: 'Optional description of the variable', + }, + type: { + type: 'string' as const, + description: 'Variable type (String, Boolean, Number, JSON)', + }, + defaultValue: { + description: 'Default value for the variable', + }, + _feature: { + type: 'string' as const, + description: 'Associated feature ID', + }, + validationSchema: { + type: 'object' as const, + description: 'Validation schema for the variable', + }, + createdAt: { + type: 'string' as const, + description: 'ISO timestamp when the variable was created', + }, + updatedAt: { + type: 'string' as const, + description: 'ISO timestamp when the variable was last updated', + }, + }, + required: ['_id', 'key', 'name', 'type', 'createdAt', 'updatedAt'], +} + +// ============================================================================= +// TOOL DEFINITIONS +// ============================================================================= export const variableToolDefinitions: Tool[] = [ { name: 'list_variables', - description: 'List variables in the current project', + description: + 'List variables in the current project. Include dashboard link in the response.', inputSchema: { type: 'object', + properties: VARIABLE_PAGINATION_PROPERTIES, + }, + outputSchema: { + type: 'object' as const, properties: { - search: { - type: 'string', - description: 'Search query to filter variables', - }, - page: { - type: 'number', - description: 'Page number (default: 1)', - }, - per_page: { - type: 'number', - description: - 'Number of items per page (default: 100, max: 1000)', + result: { + type: 'array' as const, + description: 'Array of variable objects in the project', + items: VARIABLE_OBJECT_SCHEMA, }, + dashboardLink: DASHBOARD_LINK_PROPERTY, }, + required: ['result', 'dashboardLink'], }, }, { name: 'create_variable', description: - 'Create a new variable. ⚠️ IMPORTANT: Variables can affect feature flags in production environments. Always confirm with the user before creating variables for features that are active in production.', + 'Create a new variable. Include dashboard link in the response.', inputSchema: { type: 'object', + properties: VARIABLE_COMMON_PROPERTIES, + required: ['key', 'type'], + }, + outputSchema: { + type: 'object' as const, properties: { - name: { - type: 'string', - description: 'Variable name (1-100 characters)', - }, - description: { - type: 'string', - description: 'Variable description (max 1000 characters)', - }, - key: { - type: 'string', - description: - 'Unique variable key (1-100 characters, must match pattern ^[a-z0-9-_.]+$)', - }, - _feature: { - type: 'string', - description: - 'Feature key or ID to associate with this variable', - }, - type: { - type: 'string', - enum: ['String', 'Boolean', 'Number', 'JSON'], - description: 'Variable type', - }, - defaultValue: { - description: 'Default value for the variable', - }, - validationSchema: { - type: 'object', - description: 'Validation schema for variable values', - properties: { - schemaType: { - type: 'string', - description: 'Schema type', - }, - enumValues: { - type: 'array', - description: 'Allowed enum values', - }, - regexPattern: { - type: 'string', - description: 'Regex pattern for validation', - }, - jsonSchema: { - type: 'string', - description: 'JSON schema for validation', - }, - description: { - type: 'string', - description: 'Schema description', - }, - exampleValue: { - description: 'Example value for the schema', - }, - }, - }, + result: VARIABLE_OBJECT_SCHEMA, + dashboardLink: DASHBOARD_LINK_PROPERTY, }, - required: ['key', 'type'], + required: ['result', 'dashboardLink'], }, }, { name: 'update_variable', description: - 'Update an existing variable. ⚠️ IMPORTANT: Variable changes can affect feature flags in production environments. Always confirm with the user before updating variables for features that are active in production.', + 'Update an existing variable. ⚠️ IMPORTANT: Variable changes can affect feature flags in production environments. Always confirm with the user before updating variables for features that are active in production. Include dashboard link in the response.', inputSchema: { type: 'object', + properties: UPDATE_VARIABLE_PROPERTIES, + required: ['key'], + }, + outputSchema: { + type: 'object' as const, properties: { - key: { - type: 'string', - description: 'Current variable key', - }, - name: { - type: 'string', - description: 'Updated variable name (1-100 characters)', - }, - description: { - type: 'string', - description: - 'Updated variable description (max 1000 characters)', - }, - type: { - type: 'string', - enum: ['String', 'Boolean', 'Number', 'JSON'], - description: 'Variable type', - }, - validationSchema: { - type: 'object', - description: 'Validation schema for variable values', - properties: { - schemaType: { - type: 'string', - description: 'Schema type', - }, - enumValues: { - type: 'array', - description: 'Allowed enum values', - }, - regexPattern: { - type: 'string', - description: 'Regex pattern for validation', - }, - jsonSchema: { - type: 'string', - description: 'JSON schema for validation', - }, - description: { - type: 'string', - description: 'Schema description', - }, - exampleValue: { - description: 'Example value for the schema', - }, - }, - }, + result: VARIABLE_OBJECT_SCHEMA, + dashboardLink: DASHBOARD_LINK_PROPERTY, }, - required: ['key'], + required: ['result', 'dashboardLink'], }, }, { name: 'delete_variable', description: - 'Delete a variable. ⚠️ CRITICAL: Deleting a variable will remove it from ALL environments including production. ALWAYS confirm with the user before deleting any variable.', + 'Delete a variable. ⚠️ CRITICAL: Deleting a variable will remove it from ALL environments including production. ALWAYS confirm with the user before deleting any variable. Include dashboard link in the response.', inputSchema: { type: 'object', properties: { - key: { - type: 'string', - description: 'Variable key to delete', - }, + key: VARIABLE_KEY_PROPERTY, }, required: ['key'], }, + outputSchema: { + type: 'object' as const, + properties: { + result: MESSAGE_RESPONSE_SCHEMA, + dashboardLink: DASHBOARD_LINK_PROPERTY, + }, + required: ['result', 'dashboardLink'], + }, }, ] @@ -182,7 +292,7 @@ export const variableToolHandlers: Record<string, ToolHandler> = { list_variables: async (args: unknown, apiClient: DevCycleApiClient) => { const validatedArgs = ListVariablesArgsSchema.parse(args) - return await apiClient.executeWithLogging( + return await apiClient.executeWithDashboardLink( 'listVariables', validatedArgs, async (authToken, projectKey) => { @@ -192,12 +302,13 @@ export const variableToolHandlers: Record<string, ToolHandler> = { validatedArgs, ) }, + generateVariablesDashboardLink, ) }, create_variable: async (args: unknown, apiClient: DevCycleApiClient) => { const validatedArgs = CreateVariableArgsSchema.parse(args) - return await apiClient.executeWithLogging( + return await apiClient.executeWithDashboardLink( 'createVariable', validatedArgs, async (authToken, projectKey) => { @@ -207,12 +318,13 @@ export const variableToolHandlers: Record<string, ToolHandler> = { validatedArgs, ) }, + generateVariablesDashboardLink, ) }, update_variable: async (args: unknown, apiClient: DevCycleApiClient) => { const validatedArgs = UpdateVariableArgsSchema.parse(args) - return await apiClient.executeWithLogging( + return await apiClient.executeWithDashboardLink( 'updateVariable', validatedArgs, async (authToken, projectKey) => { @@ -225,12 +337,13 @@ export const variableToolHandlers: Record<string, ToolHandler> = { updateData, ) }, + generateVariablesDashboardLink, ) }, delete_variable: async (args: unknown, apiClient: DevCycleApiClient) => { const validatedArgs = DeleteVariableArgsSchema.parse(args) - return await apiClient.executeWithLogging( + return await apiClient.executeWithDashboardLink( 'deleteVariable', validatedArgs, async (authToken, projectKey) => { @@ -239,6 +352,7 @@ export const variableToolHandlers: Record<string, ToolHandler> = { message: `Variable '${validatedArgs.key}' deleted successfully`, } }, + generateVariablesDashboardLink, ) }, } diff --git a/src/mcp/types.ts b/src/mcp/types.ts index 422688a31..696beb876 100644 --- a/src/mcp/types.ts +++ b/src/mcp/types.ts @@ -4,15 +4,48 @@ import { UpdateFeatureStatusDto } from '../api/schemas' // Zod schemas for MCP tool arguments export const ListFeaturesArgsSchema = z.object({ - search: z.string().optional(), - page: z.number().min(1).optional(), + page: z.number().min(1).default(1).optional(), perPage: z.number().min(1).max(1000).default(100).optional(), + sortBy: z + .enum([ + 'createdAt', + 'updatedAt', + 'name', + 'key', + 'createdBy', + 'propertyKey', + ]) + .default('createdAt') + .optional(), + sortOrder: z.enum(['asc', 'desc']).default('desc').optional(), + search: z.string().min(3).optional(), + staleness: z + .enum(['all', 'unused', 'released', 'unmodified', 'notStale']) + .optional(), + createdBy: z.string().optional(), + type: z.enum(['release', 'experiment', 'permission', 'ops']).optional(), + status: z.enum(['active', 'complete', 'archived']).optional(), }) export const ListVariablesArgsSchema = z.object({ - search: z.string().optional(), - page: z.number().min(1).optional(), + page: z.number().min(1).default(1).optional(), perPage: z.number().min(1).max(1000).default(100).optional(), + sortBy: z + .enum([ + 'createdAt', + 'updatedAt', + 'name', + 'key', + 'createdBy', + 'propertyKey', + ]) + .default('createdAt') + .optional(), + sortOrder: z.enum(['asc', 'desc']).default('desc').optional(), + search: z.string().min(3).optional(), + feature: z.string().optional(), + type: z.enum(['String', 'Boolean', 'Number', 'JSON']).optional(), + status: z.enum(['active', 'archived']).optional(), }) export const CreateVariableArgsSchema = schemas.CreateVariableDto diff --git a/src/mcp/utils/api.ts b/src/mcp/utils/api.ts index 84073896a..6cf03b42e 100644 --- a/src/mcp/utils/api.ts +++ b/src/mcp/utils/api.ts @@ -67,6 +67,31 @@ export class DevCycleApiClient { } } + /** + * Helper method to execute API calls and include dashboard links in the response + */ + public async executeWithDashboardLink<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 } From f802290afd63424f38938eeb6cc1806aa35c6a1f Mon Sep 17 00:00:00 2001 From: Jonathan Norris <jonathan@taplytics.com> Date: Fri, 18 Jul 2025 15:11:58 -0400 Subject: [PATCH 43/44] fix: Zodios Validation Errors in MCP Tools (#465) * fix: Zodios Validation Errors in MCP Tools * chore: cleanup TS comments --- src/api/apiClient.ts | 12 ++- src/api/zodClient.ts | 42 +++++++- src/mcp/server.ts | 63 +++++++++++- src/mcp/tools/environmentTools.ts | 41 +++++--- src/mcp/tools/featureTools.ts | 153 ++++++++++++++++++---------- src/mcp/tools/projectTools.ts | 22 +++- src/mcp/tools/selfTargetingTools.ts | 61 +++++++---- src/mcp/tools/variableTools.ts | 35 ++++--- src/mcp/utils/api.ts | 60 +++++++++-- src/ui/targetingTree.ts | 4 + 10 files changed, 368 insertions(+), 125 deletions(-) diff --git a/src/api/apiClient.ts b/src/api/apiClient.ts index 8e4afd1fc..a77369f21 100644 --- a/src/api/apiClient.ts +++ b/src/api/apiClient.ts @@ -109,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/zodClient.ts b/src/api/zodClient.ts index d814b843d..4078fef54 100644 --- a/src/api/zodClient.ts +++ b/src/api/zodClient.ts @@ -202,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'), }) @@ -244,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'), }) @@ -564,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(), @@ -576,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(), diff --git a/src/mcp/server.ts b/src/mcp/server.ts index 9f39a81ad..d18062f77 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -28,20 +28,41 @@ import { 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[] = [ +const allToolDefinitions: Tool[] = processToolDefinitions([ ...featureToolDefinitions, ...environmentToolDefinitions, ...projectToolDefinitions, ...variableToolDefinitions, ...selfTargetingToolDefinitions, -] +]) // Combine all tool handlers const allToolHandlers: Record<string, ToolHandler> = { @@ -241,7 +262,36 @@ export class DevCycleMCPServer { } const result = await handler(args, this.apiClient) - return { + + // 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', @@ -249,6 +299,13 @@ export class DevCycleMCPServer { }, ], } + 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) } diff --git a/src/mcp/tools/environmentTools.ts b/src/mcp/tools/environmentTools.ts index 2f1e53d10..d588af994 100644 --- a/src/mcp/tools/environmentTools.ts +++ b/src/mcp/tools/environmentTools.ts @@ -1,5 +1,5 @@ import { Tool } from '@modelcontextprotocol/sdk/types.js' -import { DevCycleApiClient } from '../utils/api' +import { DevCycleApiClient, handleZodiosValidationErrors } from '../utils/api' import { fetchEnvironments, fetchEnvironmentByKey, @@ -264,7 +264,10 @@ export const environmentToolHandlers: Record<string, ToolHandler> = { 'listEnvironments', validatedArgs, async (authToken, projectKey) => { - return await fetchEnvironments(authToken, projectKey) + return await handleZodiosValidationErrors( + () => fetchEnvironments(authToken, projectKey), + 'listEnvironments', + ) }, generateEnvironmentDashboardLink, ) @@ -276,10 +279,14 @@ export const environmentToolHandlers: Record<string, ToolHandler> = { 'getSdkKeys', validatedArgs, async (authToken, projectKey) => { - const environment = await fetchEnvironmentByKey( - authToken, - projectKey, - validatedArgs.environmentKey, + const environment = await handleZodiosValidationErrors( + () => + fetchEnvironmentByKey( + authToken, + projectKey, + validatedArgs.environmentKey, + ), + 'fetchEnvironmentByKey', ) const sdkKeys = environment.sdkKeys @@ -306,10 +313,10 @@ export const environmentToolHandlers: Record<string, ToolHandler> = { 'createEnvironment', validatedArgs, async (authToken, projectKey) => { - return await createEnvironment( - authToken, - projectKey, - validatedArgs, + return await handleZodiosValidationErrors( + () => + createEnvironment(authToken, projectKey, validatedArgs), + 'createEnvironment', ) }, generateEnvironmentDashboardLink, @@ -323,11 +330,15 @@ export const environmentToolHandlers: Record<string, ToolHandler> = { validatedArgs, async (authToken, projectKey) => { const { key, ...updateParams } = validatedArgs - return await updateEnvironment( - authToken, - projectKey, - key, - updateParams, + return await handleZodiosValidationErrors( + () => + updateEnvironment( + authToken, + projectKey, + key, + updateParams, + ), + 'updateEnvironment', ) }, generateEnvironmentDashboardLink, diff --git a/src/mcp/tools/featureTools.ts b/src/mcp/tools/featureTools.ts index 717ec0f25..1ca469dbc 100644 --- a/src/mcp/tools/featureTools.ts +++ b/src/mcp/tools/featureTools.ts @@ -44,6 +44,7 @@ import { VARIATION_KEY_PROPERTY, TARGET_AUDIENCE_PROPERTY, } from './commonSchemas' +import { handleZodiosValidationErrors } from '../utils/api' // Helper function to generate feature dashboard links const generateFeaturesDashboardLink = ( @@ -778,7 +779,10 @@ export const featureToolHandlers: Record<string, ToolHandler> = { 'listFeatures', validatedArgs, async (authToken, projectKey) => { - return await fetchFeatures(authToken, projectKey, validatedArgs) + return await handleZodiosValidationErrors( + () => fetchFeatures(authToken, projectKey, validatedArgs), + 'listFeatures', + ) }, generateFeaturesDashboardLink, ) @@ -806,7 +810,10 @@ export const featureToolHandlers: Record<string, ToolHandler> = { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { interactive, ...featureData } = validatedArgs - return await createFeature(authToken, projectKey, featureData) + return await handleZodiosValidationErrors( + () => createFeature(authToken, projectKey, featureData), + 'createFeature', + ) }, (orgId, projectKey, result) => generateFeatureDashboardLink( @@ -826,11 +833,9 @@ export const featureToolHandlers: Record<string, ToolHandler> = { async (authToken, projectKey) => { const { key, ...updateData } = validatedArgs - return await updateFeature( - authToken, - projectKey, - key, - updateData, + return await handleZodiosValidationErrors( + () => updateFeature(authToken, projectKey, key, updateData), + 'updateFeature', ) }, (orgId, projectKey, result) => @@ -854,11 +859,15 @@ export const featureToolHandlers: Record<string, ToolHandler> = { async (authToken, projectKey) => { const { key, ...statusData } = validatedArgs - return await updateFeatureStatus( - authToken, - projectKey, - key, - statusData, + return await handleZodiosValidationErrors( + () => + updateFeatureStatus( + authToken, + projectKey, + key, + statusData, + ), + 'updateFeatureStatus', ) }, (orgId, projectKey, result) => @@ -877,7 +886,11 @@ export const featureToolHandlers: Record<string, ToolHandler> = { 'deleteFeature', validatedArgs, async (authToken, projectKey) => { - await deleteFeature(authToken, projectKey, validatedArgs.key) + await handleZodiosValidationErrors( + () => + deleteFeature(authToken, projectKey, validatedArgs.key), + 'deleteFeature', + ) return { message: `Feature '${validatedArgs.key}' deleted successfully`, } @@ -895,10 +908,14 @@ export const featureToolHandlers: Record<string, ToolHandler> = { 'fetchFeatureVariations', validatedArgs, async (authToken, projectKey) => { - return await fetchVariations( - authToken, - projectKey, - validatedArgs.feature_key, + return await handleZodiosValidationErrors( + () => + fetchVariations( + authToken, + projectKey, + validatedArgs.feature_key, + ), + 'fetchVariations', ) }, (orgId, projectKey) => @@ -922,11 +939,15 @@ export const featureToolHandlers: Record<string, ToolHandler> = { async (authToken, projectKey) => { const { feature_key, ...variationData } = validatedArgs - return await createVariation( - authToken, - projectKey, - feature_key, - variationData, + return await handleZodiosValidationErrors( + () => + createVariation( + authToken, + projectKey, + feature_key, + variationData, + ), + 'createVariation', ) }, (orgId, projectKey, result) => @@ -951,12 +972,16 @@ export const featureToolHandlers: Record<string, ToolHandler> = { const { feature_key, variation_key, ...variationData } = validatedArgs - return await updateVariation( - authToken, - projectKey, - feature_key, - variation_key, - variationData, + return await handleZodiosValidationErrors( + () => + updateVariation( + authToken, + projectKey, + feature_key, + variation_key, + variationData, + ), + 'updateVariation', ) }, (orgId, projectKey, result) => @@ -978,11 +1003,15 @@ export const featureToolHandlers: Record<string, ToolHandler> = { 'enableTargeting', validatedArgs, async (authToken, projectKey) => { - await enableTargeting( - authToken, - projectKey, - validatedArgs.feature_key, - validatedArgs.environment_key, + 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}'`, @@ -1007,11 +1036,15 @@ export const featureToolHandlers: Record<string, ToolHandler> = { 'disableTargeting', validatedArgs, async (authToken, projectKey) => { - await disableTargeting( - authToken, - projectKey, - validatedArgs.feature_key, - validatedArgs.environment_key, + 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}'`, @@ -1036,11 +1069,15 @@ export const featureToolHandlers: Record<string, ToolHandler> = { 'listFeatureTargeting', validatedArgs, async (authToken, projectKey) => { - return await fetchTargetingForFeature( - authToken, - projectKey, - validatedArgs.feature_key, - validatedArgs.environment_key, + return await handleZodiosValidationErrors( + () => + fetchTargetingForFeature( + authToken, + projectKey, + validatedArgs.feature_key, + validatedArgs.environment_key, + ), + 'fetchTargetingForFeature', ) }, (orgId, projectKey) => @@ -1065,12 +1102,16 @@ export const featureToolHandlers: Record<string, ToolHandler> = { const { feature_key, environment_key, ...configData } = validatedArgs - return await updateFeatureConfigForEnvironment( - authToken, - projectKey, - feature_key, - environment_key, - configData, + return await handleZodiosValidationErrors( + () => + updateFeatureConfigForEnvironment( + authToken, + projectKey, + feature_key, + environment_key, + configData, + ), + 'updateFeatureConfigForEnvironment', ) }, (orgId, projectKey) => @@ -1092,11 +1133,15 @@ export const featureToolHandlers: Record<string, ToolHandler> = { 'getFeatureAuditLogHistory', validatedArgs, async (authToken, projectKey) => { - return await getFeatureAuditLogHistory( - authToken, - projectKey, - validatedArgs.feature_key, - validatedArgs.days_back || 30, + return await handleZodiosValidationErrors( + () => + getFeatureAuditLogHistory( + authToken, + projectKey, + validatedArgs.feature_key, + validatedArgs.days_back || 30, + ), + 'getFeatureAuditLogHistory', ) }, (orgId, projectKey) => diff --git a/src/mcp/tools/projectTools.ts b/src/mcp/tools/projectTools.ts index da89d1632..40fb1e178 100644 --- a/src/mcp/tools/projectTools.ts +++ b/src/mcp/tools/projectTools.ts @@ -1,5 +1,5 @@ import { Tool } from '@modelcontextprotocol/sdk/types.js' -import { DevCycleApiClient } from '../utils/api' +import { DevCycleApiClient, handleZodiosValidationErrors } from '../utils/api' import { fetchProjects, fetchProject, @@ -209,7 +209,10 @@ export const projectToolHandlers: Record<string, ToolHandler> = { validatedArgs, async (authToken) => { // projectKey not used for listing all projects - return await fetchProjects(authToken, validatedArgs) + return await handleZodiosValidationErrors( + () => fetchProjects(authToken, validatedArgs), + 'fetchProjects', + ) }, generateOrganizationSettingsLink, ) @@ -222,7 +225,10 @@ export const projectToolHandlers: Record<string, ToolHandler> = { 'getCurrentProject', null, async (authToken, projectKey) => { - return await fetchProject(authToken, projectKey) + return await handleZodiosValidationErrors( + () => fetchProject(authToken, projectKey), + 'fetchProject', + ) }, generateProjectDashboardLink, ) @@ -235,7 +241,10 @@ export const projectToolHandlers: Record<string, ToolHandler> = { validatedArgs, async (authToken) => { // projectKey not used for creating projects - return await createProject(authToken, validatedArgs) + return await handleZodiosValidationErrors( + () => createProject(authToken, validatedArgs), + 'createProject', + ) }, generateProjectDashboardLink, ) @@ -249,7 +258,10 @@ export const projectToolHandlers: Record<string, ToolHandler> = { validatedArgs, async (authToken) => { // projectKey not used - we use the key from validated args - return await updateProject(authToken, key, updateParams) + return await handleZodiosValidationErrors( + () => updateProject(authToken, key, updateParams), + 'updateProject', + ) }, generateEditProjectLink, ) diff --git a/src/mcp/tools/selfTargetingTools.ts b/src/mcp/tools/selfTargetingTools.ts index fc6d4432e..e67056d74 100644 --- a/src/mcp/tools/selfTargetingTools.ts +++ b/src/mcp/tools/selfTargetingTools.ts @@ -1,5 +1,5 @@ import { Tool } from '@modelcontextprotocol/sdk/types.js' -import { DevCycleApiClient } from '../utils/api' +import { DevCycleApiClient, handleZodiosValidationErrors } from '../utils/api' import { fetchUserProfile, updateUserProfile } from '../../api/userProfile' import { fetchProjectOverridesForUser, @@ -203,7 +203,10 @@ export const selfTargetingToolHandlers: Record<string, ToolHandler> = { 'getSelfTargetingIdentity', null, async (authToken, projectKey) => { - return await fetchUserProfile(authToken, projectKey) + return await handleZodiosValidationErrors( + () => fetchUserProfile(authToken, projectKey), + 'fetchUserProfile', + ) }, generateSelfTargetingDashboardLink, ) @@ -218,9 +221,13 @@ export const selfTargetingToolHandlers: Record<string, ToolHandler> = { 'updateSelfTargetingIdentity', validatedArgs, async (authToken, projectKey) => { - return await updateUserProfile(authToken, projectKey, { - dvcUserId: validatedArgs.dvc_user_id, - }) + return await handleZodiosValidationErrors( + () => + updateUserProfile(authToken, projectKey, { + dvcUserId: validatedArgs.dvc_user_id, + }), + 'updateUserProfile', + ) }, generateSelfTargetingDashboardLink, ) @@ -233,7 +240,10 @@ export const selfTargetingToolHandlers: Record<string, ToolHandler> = { 'listSelfTargetingOverrides', null, async (authToken, projectKey) => { - return await fetchProjectOverridesForUser(authToken, projectKey) + return await handleZodiosValidationErrors( + () => fetchProjectOverridesForUser(authToken, projectKey), + 'fetchProjectOverridesForUser', + ) }, generateSelfTargetingDashboardLink, ) @@ -248,14 +258,18 @@ export const selfTargetingToolHandlers: Record<string, ToolHandler> = { 'setSelfTargetingOverride', validatedArgs, async (authToken, projectKey) => { - return await updateOverride( - authToken, - projectKey, - validatedArgs.feature_key, - { - environment: validatedArgs.environment_key, - variation: validatedArgs.variation_key, - }, + return await handleZodiosValidationErrors( + () => + updateOverride( + authToken, + projectKey, + validatedArgs.feature_key, + { + environment: validatedArgs.environment_key, + variation: validatedArgs.variation_key, + }, + ), + 'updateOverride', ) }, generateSelfTargetingDashboardLink, @@ -271,11 +285,15 @@ export const selfTargetingToolHandlers: Record<string, ToolHandler> = { 'clearFeatureSelfTargetingOverrides', validatedArgs, async (authToken, projectKey) => { - await deleteFeatureOverrides( - authToken, - projectKey, - validatedArgs.feature_key, - validatedArgs.environment_key, + await handleZodiosValidationErrors( + () => + deleteFeatureOverrides( + authToken, + projectKey, + validatedArgs.feature_key, + validatedArgs.environment_key, + ), + 'deleteFeatureOverrides', ) return { @@ -293,7 +311,10 @@ export const selfTargetingToolHandlers: Record<string, ToolHandler> = { 'clearAllSelfTargetingOverrides', null, async (authToken, projectKey) => { - await deleteAllProjectOverrides(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 index f9b231632..97cdaad58 100644 --- a/src/mcp/tools/variableTools.ts +++ b/src/mcp/tools/variableTools.ts @@ -1,5 +1,5 @@ import { Tool } from '@modelcontextprotocol/sdk/types.js' -import { DevCycleApiClient } from '../utils/api' +import { DevCycleApiClient, handleZodiosValidationErrors } from '../utils/api' import { fetchVariables, createVariable, @@ -296,10 +296,9 @@ export const variableToolHandlers: Record<string, ToolHandler> = { 'listVariables', validatedArgs, async (authToken, projectKey) => { - return await fetchVariables( - authToken, - projectKey, - validatedArgs, + return await handleZodiosValidationErrors( + () => fetchVariables(authToken, projectKey, validatedArgs), + 'fetchVariables', ) }, generateVariablesDashboardLink, @@ -312,10 +311,9 @@ export const variableToolHandlers: Record<string, ToolHandler> = { 'createVariable', validatedArgs, async (authToken, projectKey) => { - return await createVariable( - authToken, - projectKey, - validatedArgs, + return await handleZodiosValidationErrors( + () => createVariable(authToken, projectKey, validatedArgs), + 'createVariable', ) }, generateVariablesDashboardLink, @@ -330,11 +328,10 @@ export const variableToolHandlers: Record<string, ToolHandler> = { async (authToken, projectKey) => { const { key, ...updateData } = validatedArgs - return await updateVariable( - authToken, - projectKey, - key, - updateData, + return await handleZodiosValidationErrors( + () => + updateVariable(authToken, projectKey, key, updateData), + 'updateVariable', ) }, generateVariablesDashboardLink, @@ -347,7 +344,15 @@ export const variableToolHandlers: Record<string, ToolHandler> = { 'deleteVariable', validatedArgs, async (authToken, projectKey) => { - await deleteVariable(authToken, projectKey, validatedArgs.key) + await handleZodiosValidationErrors( + () => + deleteVariable( + authToken, + projectKey, + validatedArgs.key, + ), + 'deleteVariable', + ) return { message: `Variable '${validatedArgs.key}' deleted successfully`, } diff --git a/src/mcp/utils/api.ts b/src/mcp/utils/api.ts index 6cf03b42e..3a7745965 100644 --- a/src/mcp/utils/api.ts +++ b/src/mcp/utils/api.ts @@ -1,5 +1,57 @@ import { DevCycleAuth } from './auth' +/** + * 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 @@ -51,13 +103,7 @@ export class DevCycleApiClient { const authToken = this.auth.getAuthToken() const projectKey = requiresProject ? this.auth.getProjectKey() : '' - - const result = await operation(authToken, projectKey) - console.error( - `MCP ${operationName} result:`, - JSON.stringify(result, null, 2), - ) - return result + return await operation(authToken, projectKey) } catch (error) { console.error( `MCP ${operationName} error:`, 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 = ( From 07cc33830feac30c1b7a8c94694106833b4685c5 Mon Sep 17 00:00:00 2001 From: Jonathan Norris <jonathan@taplytics.com> Date: Fri, 18 Jul 2025 15:43:55 -0400 Subject: [PATCH 44/44] feat(FDN-547): Add API request headers for MCP server tracking (#464) * feat: add MCP API headers to requests * feat: add headers.ts file --- src/api/apiClient.ts | 2 +- src/mcp/index.ts | 9 ++++++++- src/mcp/utils/api.ts | 4 ++++ src/mcp/utils/headers.ts | 29 +++++++++++++++++++++++++++++ 4 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 src/mcp/utils/headers.ts diff --git a/src/api/apiClient.ts b/src/api/apiClient.ts index a77369f21..1dc611c65 100644 --- a/src/api/apiClient.ts +++ b/src/api/apiClient.ts @@ -12,7 +12,7 @@ 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 = { diff --git a/src/mcp/index.ts b/src/mcp/index.ts index f21ac9a26..03027efb7 100644 --- a/src/mcp/index.ts +++ b/src/mcp/index.ts @@ -5,6 +5,7 @@ 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 { @@ -46,10 +47,16 @@ if (args.includes('--help') || args.includes('-h')) { } 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: getVersion(), + version, }, { capabilities: { diff --git a/src/mcp/utils/api.ts b/src/mcp/utils/api.ts index 3a7745965..75d5888f7 100644 --- a/src/mcp/utils/api.ts +++ b/src/mcp/utils/api.ts @@ -1,4 +1,5 @@ import { DevCycleAuth } from './auth' +import { setMCPToolCommand } from './headers' /** * Utility function to handle Zodios validation errors by extracting response data @@ -101,6 +102,9 @@ export class DevCycleApiClient { 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) 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') +}