# Task ID: 24 **Title:** Implement mcpctl install with LLM-Assisted Auto-Configuration **Status:** pending **Dependencies:** 22, 23 **Priority:** medium **Description:** Create the `mcpctl install ` command that uses a local LLM to automatically read MCP server documentation, generate envTemplate/setup guides/profiles, and walk users through configuration with validation. **Details:** Create src/cli/src/commands/install.ts: ```typescript import { Command } from 'commander'; import { RegistryClient } from '../registry/client'; import { LLMProvider } from '../llm/provider'; import { SetupWizard } from '../setup/wizard'; import { McpdClient } from '../api/mcpd-client'; export function createInstallCommand(): Command { const cmd = new Command('install') .description('Install and configure an MCP server') .argument('', 'Server name(s) from registry') .option('--non-interactive', 'Use env vars for credentials, no prompts') .option('--profile-name ', 'Name for the created profile') .option('--project ', 'Auto-add to this project') .option('--dry-run', 'Show configuration without applying') .option('--skip-llm', 'Only use registry metadata, no LLM analysis') .action(async (servers, options) => { await installAction(servers, options); }); return cmd; } ``` **Installation Flow:** ```typescript // install-action.ts export async function installAction( serverNames: string[], options: InstallOptions ): Promise { const registry = new RegistryClient(); const mcpd = new McpdClient(); const llm = await getLLMProvider(); // From Task 12 config for (const serverName of serverNames) { console.log(chalk.blue(`\nInstalling ${serverName}...`)); // Step 1: Fetch server metadata from registry const serverMeta = await registry.getServer(serverName); if (!serverMeta) { console.error(chalk.red(`Server '${serverName}' not found in registries`)); continue; } // Step 2: Check if envTemplate is complete let envTemplate = serverMeta.envTemplate; let setupGuide = serverMeta.setupGuide; let defaultProfiles: ProfileConfig[] = []; const needsLLMAnalysis = ( !options.skipLlm && (!envTemplate || envTemplate.length === 0 || hasIncompleteEnvVars(envTemplate)) ); // Step 3: LLM-assisted configuration generation if (needsLLMAnalysis && serverMeta.repositoryUrl) { console.log(chalk.dim('Analyzing server documentation with LLM...')); const readme = await fetchReadme(serverMeta.repositoryUrl); const llmResult = await analyzeWithLLM(llm, readme, serverMeta); // Merge LLM results with registry data envTemplate = mergeEnvTemplates(envTemplate, llmResult.envTemplate); setupGuide = llmResult.setupGuide || setupGuide; defaultProfiles = llmResult.profiles || []; } if (options.dryRun) { printDryRun(serverMeta, envTemplate, setupGuide, defaultProfiles); continue; } // Step 4: Register MCP server in mcpd const registeredServer = await mcpd.registerServer({ name: serverMeta.name, command: serverMeta.packages.npm ? `npx -y ${serverMeta.packages.npm}` : serverMeta.packages.docker ? `docker run ${serverMeta.packages.docker}` : throw new Error('No package source available'), envTemplate, transport: serverMeta.transport, }); // Step 5: Run setup wizard to collect credentials const wizard = new SetupWizard(envTemplate, { nonInteractive: options.nonInteractive }); const credentials = await wizard.run(); // Step 6: Create profile const profileName = options.profileName || `${serverMeta.name}-default`; const profile = await mcpd.createProfile({ name: profileName, serverId: registeredServer.id, config: credentials, }); // Step 7: Optionally add to project if (options.project) { await mcpd.addProfileToProject(options.project, profile.id); console.log(chalk.green(`Added to project '${options.project}'`)); } console.log(chalk.green(`✓ ${serverMeta.name} installed successfully`)); console.log(chalk.dim(` Profile: ${profileName}`)); } } ``` **LLM Analysis Implementation:** ```typescript // llm-analyzer.ts import { z } from 'zod'; const LLMAnalysisSchema = z.object({ envTemplate: z.array(z.object({ name: z.string(), description: z.string(), isSecret: z.boolean(), setupUrl: z.string().url().optional(), defaultValue: z.string().optional(), })), setupGuide: z.string().optional(), profiles: z.array(z.object({ name: z.string(), description: z.string(), permissions: z.array(z.string()), })).optional(), }); const ANALYSIS_PROMPT = ` Analyze this MCP server README and extract configuration information. README: {readme} Extract and return JSON with: 1. envTemplate: Array of required environment variables with: - name: The env var name (e.g., SLACK_BOT_TOKEN) - description: What this variable is for and where to get it - isSecret: true if this is a secret/token/password - setupUrl: URL to docs for obtaining this credential (if mentioned) 2. setupGuide: Step-by-step setup instructions in markdown 3. profiles: Suggested permission profiles (e.g., read-only, admin, limited) Return ONLY valid JSON matching this exact schema. No markdown formatting. `; export async function analyzeWithLLM( llm: LLMProvider, readme: string, serverMeta: RegistryServer ): Promise> { // Sanitize README to prevent prompt injection const sanitizedReadme = sanitizeForLLM(readme); const prompt = ANALYSIS_PROMPT.replace('{readme}', sanitizedReadme); const response = await llm.complete(prompt, { maxTokens: 2000, temperature: 0.1, // Low temperature for structured output }); // Extract JSON from response (handle markdown code blocks) const jsonStr = extractJSON(response); // Validate with Zod const parsed = LLMAnalysisSchema.safeParse(JSON.parse(jsonStr)); if (!parsed.success) { console.warn(chalk.yellow('LLM output validation failed, using registry data only')); return { envTemplate: [], setupGuide: undefined, profiles: [] }; } return parsed.data; } function sanitizeForLLM(text: string): string { // Remove potential prompt injection patterns return text .replace(/```[\s\S]*?```/g, (match) => match) // Keep code blocks .replace(/\{\{.*?\}\}/g, '') // Remove template syntax .replace(/\[INST\]/gi, '') // Remove common injection patterns .replace(/\[\/?SYSTEM\]/gi, '') .slice(0, 50000); // Limit length } ``` **GitHub README Fetching:** ```typescript // github.ts export async function fetchReadme(repoUrl: string): Promise { const { owner, repo } = parseGitHubUrl(repoUrl); // Try common README locations const paths = ['README.md', 'readme.md', 'README.rst', 'README']; for (const path of paths) { try { const response = await fetch( `https://raw.githubusercontent.com/${owner}/${repo}/main/${path}` ); if (response.ok) { return await response.text(); } // Try master branch const masterResponse = await fetch( `https://raw.githubusercontent.com/${owner}/${repo}/master/${path}` ); if (masterResponse.ok) { return await masterResponse.text(); } } catch { continue; } } throw new Error(`Could not fetch README from ${repoUrl}`); } ``` **Security Considerations:** - Sanitize LLM outputs before using (prevent prompt injection from malicious READMEs) - Validate generated envTemplate with Zod schema - Never auto-execute commands suggested by LLM without explicit user approval - Log LLM interactions for audit (without sensitive data) - Rate limit LLM calls to prevent abuse **Data Platform Auth Pattern Recognition:** LLM should understand complex auth patterns commonly found in data tools: - Service account JSON (GCP BigQuery, Vertex AI) - Connection strings (Snowflake, Databricks) - OAuth flows (dbt Cloud, Tableau) - IAM roles (AWS Redshift, Athena) - API keys with scopes (Fivetran, Airbyte) **Test Strategy:** TDD approach - write tests BEFORE implementation: 1. **Command parsing tests:** - Test single server argument - Test multiple servers (batch install) - Test all options parse correctly - Test --non-interactive and --dry-run flags 2. **Registry fetch tests:** - Test successful server lookup - Test server not found handling - Test registry error handling 3. **LLM prompt generation tests:** - Test prompt template populated correctly - Test README truncation at 50k chars - Test sanitization removes injection patterns - Test code blocks preserved in sanitization 4. **LLM response parsing tests:** - Test valid JSON extraction from plain response - Test JSON extraction from markdown code blocks - Test Zod validation accepts valid schema - Test Zod validation rejects invalid schema - Test graceful fallback on validation failure 5. **GitHub README fetch tests:** - Test main branch fetch - Test master branch fallback - Test different README filename handling - Test repository URL parsing (https, git@) - Test fetch failure handling 6. **envTemplate merge tests:** - Test LLM results merged with registry data - Test LLM results don't override existing registry data - Test deduplication by env var name 7. **Full install flow tests:** - Test complete flow with mocked dependencies - Test dry-run shows config without applying - Test skip-llm uses registry data only - Test non-interactive uses env vars - Test batch install processes all servers 8. **Security tests:** - Test prompt injection patterns sanitized - Test malformed LLM output rejected - Test no command auto-execution 9. **Data platform tests:** - Test recognition of service account JSON patterns - Test recognition of connection string patterns - Test OAuth flow detection Run: `pnpm --filter @mcpctl/cli test:run -- --coverage commands/install` ## Subtasks ### 24.1. Write TDD Test Suites for Install Command Parsing, GitHub README Fetching, and Core Types **Status:** pending **Dependencies:** None Create comprehensive Vitest test suites for the install command's CLI parsing, GitHub README fetching module with proxy support, and foundational types/Zod schemas BEFORE implementation, following the project's strict TDD approach. **Details:** Create src/cli/tests/unit/commands/install/ directory with test files. Write tests for: 1. **Command Parsing Tests** (install.test.ts): - Test single server argument parsing - Test multiple servers (batch install): `mcpctl install slack jira github` - Test all options parse correctly: --non-interactive, --profile-name, --project, --dry-run, --skip-llm - Test required argument validation (exits with code 2 if no server specified) - Test option combinations are mutually compatible 2. **GitHub README Fetching Tests** (github-fetcher.test.ts): - Test parseGitHubUrl() extracts owner/repo from various URL formats (https://github.com/owner/repo, git@github.com:owner/repo.git) - Test fetchReadme() tries multiple paths: README.md, readme.md, README.rst, README - Test branch fallback: main -> master - Test HTTP_PROXY/HTTPS_PROXY environment variable support using undici ProxyAgent - Test custom CA certificate support (NODE_EXTRA_CA_CERTS) - Test GitHub rate limit handling (403 with X-RateLimit-Remaining: 0) with exponential backoff - Test timeout handling (30s default) with AbortController - Create test fixtures: mock README responses in src/cli/tests/fixtures/readmes/ 3. **Type and Schema Tests** (types.test.ts): - Test InstallOptions Zod schema validates all fields - Test EnvTemplateEntry schema requires name, description, isSecret - Test LLMAnalysisResult schema validates envTemplate array, setupGuide string, profiles array - Test ProfileConfig schema validates name, description, permissions array 4. **Mock Infrastructure**: - Create MockRegistryClient in src/cli/tests/mocks/ that implements RegistryClient interface - Create MockLLMProvider that returns deterministic responses for testing - Create MockMcpdClient for testing server registration and profile creation - Use msw (Mock Service Worker) for GitHub API mocking All tests must fail initially (red phase) with 'module not found' or 'function not implemented' errors. ### 24.2. Write TDD Test Suites for LLM Analysis with Security Sanitization and Data Platform Auth Recognition **Status:** pending **Dependencies:** 24.1 Create comprehensive Vitest test suites for the LLM-based README analysis module, focusing on prompt injection prevention, output validation with Zod, and recognition of complex data platform authentication patterns (BigQuery service accounts, Snowflake connection strings, dbt OAuth). **Details:** Create src/cli/tests/unit/llm/analyzer.test.ts with comprehensive test coverage: 1. **Prompt Sanitization Tests** (SECURITY CRITICAL): - Test sanitizeForLLM() removes prompt injection patterns: [INST], [/SYSTEM], , <|endoftext|>, <> - Test removal of template syntax: {{variable}}, ${command} - Test preservation of legitimate code blocks (```typescript...```) - Test length truncation at 50000 characters - Test handling of Unicode edge cases and zero-width characters - Create malicious README fixtures in src/cli/tests/fixtures/readmes/malicious/ 2. **LLM Output Validation Tests**: - Test extractJSON() handles markdown code blocks: ```json...``` - Test extractJSON() handles raw JSON without code blocks - Test LLMAnalysisSchema Zod validation catches missing required fields - Test validation rejects envTemplate entries without isSecret field - Test graceful fallback returns empty result on validation failure (warn, don't crash) - Test malformed JSON handling (truncated, invalid syntax) 3. **Data Platform Auth Pattern Recognition Tests** (Principal Data Engineer focus): - Test BigQuery: recognizes GOOGLE_APPLICATION_CREDENTIALS, service account JSON patterns - Test Snowflake: recognizes SNOWFLAKE_ACCOUNT, SNOWFLAKE_USER, connection string format - Test dbt Cloud: recognizes DBT_API_KEY, DBT_ACCOUNT_ID, project selection patterns - Test Databricks: recognizes DATABRICKS_HOST, DATABRICKS_TOKEN, cluster configuration - Test AWS data services: recognizes AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, IAM role ARN - Test Fivetran/Airbyte: recognizes API key with scopes pattern - Create test fixtures: src/cli/tests/fixtures/readmes/data-platforms/ with realistic READMEs 4. **LLM Provider Integration Tests**: - Test analyzeWithLLM() uses injected LLMProvider (dependency injection) - Test temperature set to 0.1 for structured output - Test maxTokens set appropriately (2000) - Test timeout handling (60s default) - Test circuit breaker triggers on consecutive failures 5. **Profile Generation Tests**: - Test default profiles extracted: read-only, admin, limited access - Test permissions array parsing - Test profile descriptions are sanitized 6. **Structured Logging Tests** (SRE focus): - Test LLM interactions are logged with requestId, duration_ms, input_tokens - Test sensitive data (API keys, tokens) are NEVER logged - Test README content is not logged in full (truncate in logs) ### 24.3. Implement GitHub README Fetcher with Proxy Support and Rate Limit Handling **Status:** pending **Dependencies:** 24.1 Implement the GitHub README fetching module with enterprise networking support (HTTP/HTTPS proxy, custom CA certs), intelligent branch detection, rate limit handling with exponential backoff, and proper error handling for network failures. **Details:** Create src/cli/src/install/github-fetcher.ts: ```typescript import { ProxyAgent } from 'undici'; import { createHash } from 'crypto'; export interface GitHubFetcherConfig { proxyUrl?: string; // From HTTP_PROXY/HTTPS_PROXY caFile?: string; // Custom CA certificate path timeout?: number; // Default 30000ms maxRetries?: number; // Default 3 rateLimitWaitMs?: number; // Default 60000ms } export interface ParsedGitHubUrl { owner: string; repo: string; } export class GitHubReadmeFetcher { private config: Required; private agent?: ProxyAgent; constructor(config: Partial = {}) { ... } parseGitHubUrl(repoUrl: string): ParsedGitHubUrl { ... } async fetchReadme(repoUrl: string): Promise { ... } private async fetchWithRetry(url: string, attempt: number): Promise { ... } private handleRateLimit(response: Response): Promise { ... } } ``` **Implementation Requirements:** 1. **URL Parsing**: - Support HTTPS: `https://github.com/owner/repo` - Support HTTPS with .git: `https://github.com/owner/repo.git` - Support SSH: `git@github.com:owner/repo.git` - Extract owner and repo, strip .git suffix - Throw descriptive error for invalid URLs 2. **README Fetching**: - Try paths in order: README.md, readme.md, README.rst, README, Readme.md - Try branches in order: main, master, HEAD - Use raw.githubusercontent.com for content fetching - Return first successful fetch 3. **Proxy Support**: - Detect HTTP_PROXY, HTTPS_PROXY, NO_PROXY environment variables - Create undici ProxyAgent when proxy configured - Pass agent to fetch() dispatcher option - Support custom CA via NODE_EXTRA_CA_CERTS or config.caFile 4. **Rate Limit Handling**: - Check X-RateLimit-Remaining header - On 403 with rate limit exceeded, wait until X-RateLimit-Reset - Log rate limit events for SRE visibility - Implement exponential backoff: 1s, 2s, 4s (max 3 retries) 5. **Error Handling**: - Throw ReadmeNotFoundError if all paths fail - Throw NetworkError on connection failures - Throw RateLimitError if exhausted after retries - Include repository URL in all error messages 6. **SRE Metrics**: - Export metrics: github_fetch_duration_seconds, github_rate_limit_remaining gauge - Log structured events: { event: 'github_fetch', repo, branch, path, duration_ms } ### 24.4. Implement LLM-Based README Analyzer with Secure Prompt Construction and Zod Validation **Status:** pending **Dependencies:** 24.1, 24.2, 24.3 Implement the LLM analysis module that processes MCP server READMEs to extract environment templates, setup guides, and suggested profiles using the pluggable LLMProvider interface with robust input sanitization and output validation. **Details:** Create src/cli/src/install/llm-analyzer.ts: ```typescript import { z } from 'zod'; import type { LLMProvider } from '../llm/provider'; import type { RegistryServer } from '../registry/types'; export const EnvTemplateEntrySchema = z.object({ name: z.string().min(1), description: z.string().min(10), isSecret: z.boolean(), setupUrl: z.string().url().optional(), defaultValue: z.string().optional(), validation: z.enum(['required', 'optional', 'conditional']).optional(), }); export const ProfileConfigSchema = z.object({ name: z.string().min(1), description: z.string(), permissions: z.array(z.string()), }); export const LLMAnalysisSchema = z.object({ envTemplate: z.array(EnvTemplateEntrySchema), setupGuide: z.string().optional(), profiles: z.array(ProfileConfigSchema).optional(), }); export type LLMAnalysisResult = z.infer; export class LLMReadmeAnalyzer { constructor( private llmProvider: LLMProvider, private logger: StructuredLogger ) {} async analyze( readme: string, serverMeta: RegistryServer ): Promise { ... } sanitizeForLLM(text: string): string { ... } private buildPrompt(readme: string, serverMeta: RegistryServer): string { ... } private extractJSON(response: string): string { ... } private validateAndParse(jsonStr: string): LLMAnalysisResult { ... } } ``` **Implementation Requirements:** 1. **Input Sanitization** (SECURITY CRITICAL): - Remove prompt injection patterns: `[INST]`, `[/INST]`, `<>`, `<>`, ``, `<|endoftext|>`, `<|im_start|>`, `<|im_end|>` - Remove template syntax: `{{...}}`, `${...}`, `<%...%>` - Preserve code blocks (```...```) for context - Truncate to 50000 characters with warning log - Normalize Unicode (NFKC) to prevent homoglyph attacks - Log sanitization actions for audit 2. **Prompt Construction**: - Use structured prompt template requesting JSON output - Include serverMeta context (name, type, existing envTemplate if partial) - Request specific fields: envTemplate, setupGuide, profiles - Include examples of data platform auth patterns in prompt - Set temperature=0.1 for deterministic output - Set maxTokens=2000 3. **Response Processing**: - Extract JSON from markdown code blocks if present - Handle raw JSON without code blocks - Validate with Zod schema - Return empty result on validation failure (graceful degradation) - Log validation errors for debugging 4. **Data Platform Auth Recognition**: - Include prompt context about common patterns: - GCP: Service account JSON files, GOOGLE_APPLICATION_CREDENTIALS - AWS: Access keys, IAM roles, STS assume role - Azure: Service principals, managed identity - Snowflake: Account URL, OAuth, key-pair auth - Databricks: Personal access tokens, OAuth M2M - dbt Cloud: API tokens with account/project scoping 5. **Error Handling**: - Wrap LLM calls in try-catch - Return fallback result on LLM timeout - Circuit breaker integration via LLMProvider - Never propagate sensitive data in errors 6. **Structured Logging** (SRE): - Log: requestId, llmProvider, promptLength, responseLength, duration_ms - NEVER log: full README content, API keys, tokens - Log validation failures with field paths ### 24.5. Implement Install Command Handler with Full Installation Flow, SetupWizard, and mcpd Integration **Status:** pending **Dependencies:** 24.1, 24.2, 24.3, 24.4 Implement the main install command and action handler that orchestrates the full installation flow: registry lookup, LLM analysis (optional), server registration with mcpd, interactive credential collection via SetupWizard, profile creation, and optional project assignment. **Details:** Create src/cli/src/commands/install.ts and src/cli/src/install/install-action.ts: ```typescript // install.ts import { Command } from 'commander'; import { installAction } from '../install/install-action'; export function createInstallCommand(): Command { return new Command('install') .description('Install and configure an MCP server') .argument('', 'Server name(s) from registry') .option('--non-interactive', 'Use env vars for credentials, no prompts') .option('--profile-name ', 'Name for the created profile') .option('--project ', 'Auto-add to this project') .option('--dry-run', 'Show configuration without applying') .option('--skip-llm', 'Only use registry metadata, no LLM analysis') .action(installAction); } // install-action.ts export interface InstallOptions { nonInteractive?: boolean; profileName?: string; project?: string; dryRun?: boolean; skipLlm?: boolean; } export async function installAction( serverNames: string[], options: InstallOptions ): Promise { ... } ``` **Implementation Requirements:** 1. **Command Registration**: - Register in src/cli/src/commands/index.ts command registry - Follow existing Commander.js patterns from discover command (Task 23) - Set exit codes: 0 success, 1 partial success, 2 complete failure 2. **Installation Flow** (per server): - Step 1: Fetch server metadata from RegistryClient (from Task 22) - Step 2: Check if envTemplate is complete or needs LLM analysis - Step 3: If needed and --skip-llm not set, fetch README and analyze with LLM - Step 4: Merge LLM results with registry metadata (registry takes precedence for conflicts) - Step 5: If --dry-run, print configuration and exit - Step 6: Register MCP server with mcpd via McpdClient - Step 7: Run SetupWizard to collect credentials (or use env vars if --non-interactive) - Step 8: Create profile with collected credentials - Step 9: If --project specified, add profile to project 3. **Dependency Injection**: - Accept RegistryClient, LLMProvider, McpdClient via constructor or factory - Enable testing with mock implementations - Use getLLMProvider() factory from Task 12 configuration 4. **SetupWizard Integration** (from Task 10): - Pass envTemplate to SetupWizard - Handle nonInteractive mode (read from environment) - Validate credentials before storing - Support OAuth flows via browser for applicable servers 5. **Dry Run Mode**: - Print server metadata (name, command, transport) - Print envTemplate with descriptions - Print setupGuide if available - Print suggested profiles - Use chalk for formatted output - Exit without side effects 6. **Batch Install**: - Process servers sequentially (to avoid mcpd race conditions) - Continue on individual server failures (log warning) - Report summary at end: X installed, Y failed - Return appropriate exit code 7. **Error Handling**: - Catch RegistryNotFoundError and suggest 'mcpctl discover' - Catch McpdConnectionError and print mcpd health check URL - Catch SetupWizardCancelledError gracefully - Never expose credentials in error messages 8. **Structured Logging** (SRE): - Log: serverName, registrySource, llmAnalysisUsed, installDuration_ms, success - Emit metrics: install_total (counter), install_duration_seconds (histogram) 9. **Output Messages**: - Use chalk.blue for progress - Use chalk.green + checkmark for success - Use chalk.red for errors - Print profile name and usage instructions on success