claude-git-hooks
Version:
Git hooks with Claude CLI for code analysis and automatic commit messages
343 lines (299 loc) • 12.8 kB
JavaScript
/**
* 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;
}
};