UNPKG

claude-git-hooks

Version:

Git hooks with Claude CLI for code analysis and automatic commit messages

343 lines (299 loc) 12.8 kB
/** * File: mcp-setup.js * Purpose: Automated GitHub MCP setup for Claude CLI * * Features: * - Auto-detect existing configuration * - Read token from Claude Desktop config * - Interactive token prompt if needed * - Configure Claude CLI MCP * - Set environment variables * - Verify configuration */ import { execSync } from 'child_process'; import fs from 'fs'; import path from 'path'; import os from 'os'; import logger from './logger.js'; import { promptConfirmation, promptEditField, showSuccess, showError, showInfo, showWarning } from './interactive-ui.js'; import { getClaudeCommand } from './claude-client.js'; import { approveGitHubMcpPermissions, executeMcpCommand } from './github-client.js'; /** * Check if GitHub MCP is already configured in Claude CLI * Why: Avoid duplicate configuration * * @returns {boolean} - True if configured */ export const isGitHubMCPConfigured = () => { try { const { command, args } = getClaudeCommand(); const fullCommand = `${command} ${args.join(' ')} mcp list`; const mcpList = execSync(fullCommand, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }); return mcpList.toLowerCase().includes('github'); } catch (error) { logger.debug('mcp-setup - isGitHubMCPConfigured', 'Failed to check MCP list', { error: error.message }); return false; } }; /** * Find GitHub token in Claude Desktop config * Why: Reuse existing token instead of asking user * * @returns {Object|null} - { token, configPath } or null if not found */ export const findGitHubTokenInDesktopConfig = () => { const possibleConfigPaths = [ // Linux/macOS standard path path.join(os.homedir(), '.config', 'Claude', 'claude_desktop_config.json'), // Windows native path path.join(os.homedir(), 'AppData', 'Roaming', 'Claude', 'claude_desktop_config.json'), // WSL accessing Windows path - try multiple user names '/mnt/c/Users/' + os.userInfo().username + '/AppData/Roaming/Claude/claude_desktop_config.json', ]; // Add additional Windows paths by checking if we're in WSL if (process.platform === 'linux' && fs.existsSync('/mnt/c')) { // We're in WSL, try to find all Windows user directories try { const usersDir = '/mnt/c/Users'; if (fs.existsSync(usersDir)) { const users = fs.readdirSync(usersDir); for (const user of users) { const configPath = `/mnt/c/Users/${user}/AppData/Roaming/Claude/claude_desktop_config.json`; if (!possibleConfigPaths.includes(configPath)) { possibleConfigPaths.push(configPath); } } } } catch (error) { logger.debug('mcp-setup - findGitHubTokenInDesktopConfig', 'Failed to scan /mnt/c/Users', { error: error.message }); } } for (const configPath of possibleConfigPaths) { try { if (fs.existsSync(configPath)) { const config = JSON.parse(fs.readFileSync(configPath, 'utf8')); const token = config.mcpServers?.github?.env?.GITHUB_PERSONAL_ACCESS_TOKEN; if (token) { logger.debug('mcp-setup - findGitHubTokenInDesktopConfig', 'Found token', { configPath }); return { token, configPath }; } } } catch (error) { logger.debug('mcp-setup - findGitHubTokenInDesktopConfig', 'Failed to read config', { configPath, error: error.message }); } } return null; }; /** * Add GitHub MCP to Claude CLI * Why: Configure MCP server for PR creation * * @param {boolean} replace - Replace existing configuration * @returns {Promise<boolean>} - True if successful */ export const addGitHubMCP = async (replace = false) => { try { const { command, args } = getClaudeCommand(); // Remove existing if replacing if (replace) { try { const removeCmd = `${command} ${args.join(' ')} mcp remove github`; execSync(removeCmd, { stdio: 'ignore' }); logger.debug('mcp-setup - addGitHubMCP', 'Removed existing GitHub MCP'); } catch (error) { // Ignore errors, may not have remove command logger.debug('mcp-setup - addGitHubMCP', 'Failed to remove (may not exist)', { error: error.message }); } } // Add GitHub MCP // Use -- to separate claude options from npx arguments const mcpCommand = `${command} ${args.join(' ')} mcp add github npx -- -y @modelcontextprotocol/server-github`; logger.debug('mcp-setup - addGitHubMCP', 'Running MCP add command', { mcpCommand }); execSync(mcpCommand, { stdio: 'inherit' }); logger.debug('mcp-setup - addGitHubMCP', 'Successfully added GitHub MCP'); return true; } catch (error) { logger.error('mcp-setup - addGitHubMCP', 'Failed to add GitHub MCP', error); return false; } }; /** * Set environment variable in shell RC files * Why: Persist GitHub token across sessions * * @param {string} token - GitHub Personal Access Token * @returns {boolean} - True if set in at least one file */ export const setEnvironmentVariable = (token) => { const envVarLine = `export GITHUB_PERSONAL_ACCESS_TOKEN="${token}"`; const shellRcFiles = [ path.join(os.homedir(), '.bashrc'), path.join(os.homedir(), '.bash_profile'), path.join(os.homedir(), '.zshrc') ]; let envVarSet = false; for (const rcFile of shellRcFiles) { try { if (fs.existsSync(rcFile)) { const content = fs.readFileSync(rcFile, 'utf8'); // Check if already present if (content.includes('GITHUB_PERSONAL_ACCESS_TOKEN')) { logger.debug('mcp-setup - setEnvironmentVariable', 'Already present', { rcFile }); showInfo(`Environment variable already in ${path.basename(rcFile)}`); envVarSet = true; continue; } // Add to file fs.appendFileSync(rcFile, `\n# GitHub MCP token for Claude CLI\n${envVarLine}\n`); showSuccess(`Added environment variable to ${path.basename(rcFile)}`); logger.debug('mcp-setup - setEnvironmentVariable', 'Added to file', { rcFile }); envVarSet = true; } } catch (error) { logger.debug('mcp-setup - setEnvironmentVariable', 'Failed to update file', { rcFile, error: error.message }); } } if (!envVarSet) { showWarning('Could not find shell RC file (.bashrc, .bash_profile, .zshrc)'); console.log(''); console.log('Please add this line to your shell configuration manually:'); console.log(` ${envVarLine}`); } // Set for current session process.env.GITHUB_PERSONAL_ACCESS_TOKEN = token; return envVarSet; }; /** * Setup GitHub MCP for Claude CLI * Why: Automate MCP configuration to enable create-pr functionality * * Interactive setup process: * 1. Check if GitHub MCP already configured * 2. Try to read GitHub token from Claude Desktop config * 3. If not found, prompt user for token * 4. Run: claude mcp add github npx -y @modelcontextprotocol/server-github * 5. Configure environment variable for token * 6. Verify configuration * * @returns {Promise<void>} */ export const setupGitHubMCP = async () => { try { console.log(''); showInfo('GitHub MCP Setup for Claude CLI'); console.log(''); // Step 1: Check if already configured console.log('🔍 Checking current MCP configuration...'); let mcpList = ''; try { const { command, args } = getClaudeCommand(); const fullCommand = `${command} ${args.join(' ')} mcp list`; mcpList = execSync(fullCommand, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }); } catch (error) { showError('Failed to run "claude mcp list". Is Claude CLI installed?'); console.error('Make sure Claude CLI is installed: npm install -g @anthropic-ai/claude-cli'); console.error('Error:', error.message); process.exit(1); } const hasGitHub = isGitHubMCPConfigured(); if (hasGitHub) { showSuccess('GitHub MCP is already configured!'); console.log(''); console.log(mcpList); const reconfigure = await promptConfirmation('Do you want to reconfigure it?', false); if (!reconfigure) { showInfo('Setup cancelled. Your existing configuration is unchanged.'); return; } } // Step 2: Try to read token from Claude Desktop config console.log(''); console.log('🔑 Looking for GitHub token...'); let githubToken = null; const tokenInfo = findGitHubTokenInDesktopConfig(); if (tokenInfo) { githubToken = tokenInfo.token; showSuccess('Found GitHub token in Claude Desktop config'); console.log(` Location: ${tokenInfo.configPath}`); } // Step 3: If not found, prompt user if (!githubToken) { showWarning('No GitHub token found in Claude Desktop config'); console.log(''); console.log('You need a GitHub Personal Access Token with these permissions:'); console.log(' - repo (Full control of private repositories)'); console.log(' - read:org (Read org and team membership)'); console.log(''); console.log('Create one at: https://github.com/settings/tokens/new'); console.log(''); githubToken = await promptEditField('GitHub Personal Access Token', ''); if (!githubToken || githubToken.trim() === '') { showError('GitHub token is required. Setup cancelled.'); process.exit(1); } } // Step 4: Add GitHub MCP to Claude CLI console.log(''); console.log('⚙️ Configuring GitHub MCP for Claude CLI...'); const added = await addGitHubMCP(hasGitHub); if (!added) { showError('Failed to add GitHub MCP'); process.exit(1); } showSuccess('GitHub MCP added to Claude CLI'); // Step 5: Configure environment variable console.log(''); console.log('🔧 Setting up environment variable...'); setEnvironmentVariable(githubToken); // Step 6: Approve MCP permissions console.log(''); console.log('🔐 Approving MCP permissions...'); const permResult = await approveGitHubMcpPermissions(); if (permResult.success) { showSuccess('MCP permissions approved'); } else { showWarning('Some permissions may need manual approval'); console.log(' Run: claude mcp approve github --all'); } // Step 7: Verify configuration console.log(''); console.log('✅ Verifying configuration...'); try { const { command, args } = getClaudeCommand(); const fullCommand = `${command} ${args.join(' ')} mcp list`; const verifyList = execSync(fullCommand, { encoding: 'utf8' }); if (verifyList.toLowerCase().includes('github')) { showSuccess('GitHub MCP is configured and ready!'); console.log(''); console.log(verifyList); } else { showWarning('Configuration completed but GitHub MCP not showing in list'); console.log('You may need to restart your terminal'); } } catch (error) { showWarning('Could not verify configuration'); logger.debug('mcp-setup - setupGitHubMCP', 'Verification failed', { error: error.message }); } // Final instructions console.log(''); showSuccess('Setup complete!'); console.log(''); console.log('Next steps:'); console.log(' 1. Restart your terminal (or run: source ~/.bashrc)'); console.log(' 2. Verify with: claude mcp list'); console.log(' 3. Try creating a PR: claude-hooks create-pr develop'); console.log(''); } catch (error) { showError('Error during MCP setup: ' + error.message); console.error(error); throw error; } };