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) # 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