Skip to main content
Glama
extension.ts25.6 kB
import * as vscode from 'vscode'; import * as fs from 'fs'; import * as fsPromises from 'fs/promises'; import * as path from 'path'; import * as os from 'os'; import { spawn, ChildProcess } from 'child_process'; const MCP_SERVER_NAME = 'octocode'; const MCP_COMMAND = 'npx'; const MCP_ARGS = ['octocode-mcp@latest']; // GitHub Authentication Constants const GITHUB_AUTH_PROVIDER_ID = 'github'; const GITHUB_SCOPES = ['repo', 'read:user']; let mcpProcess: ChildProcess | null = null; let outputChannel: vscode.OutputChannel; let statusBarItem: vscode.StatusBarItem; let isAuthenticated = false; type McpServerConfig = { command: string; type: 'stdio'; args: string[]; env?: Record<string, string>; }; type McpConfig = { mcpServers: Record<string, McpServerConfig>; }; // MCP Client definitions with platform-aware paths type McpClientDef = { name: string; getConfigPath: () => string; configKey: 'mcpServers' | 'servers'; }; function getPlatformConfigBase(): string { const platform = process.platform; const home = os.homedir(); if (platform === 'darwin') { return path.join(home, 'Library', 'Application Support'); } else if (platform === 'win32') { return process.env.APPDATA || path.join(home, 'AppData', 'Roaming'); } else { return path.join(home, '.config'); } } const MCP_CLIENTS: Record<string, McpClientDef> = { cline: { name: 'Cline', getConfigPath: () => path.join( getPlatformConfigBase(), 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev', 'settings', 'cline_mcp_settings.json' ), configKey: 'mcpServers', }, rooCode: { name: 'Roo Code', getConfigPath: () => path.join( getPlatformConfigBase(), 'Code', 'User', 'globalStorage', 'rooveterinaryinc.roo-cline', 'settings', 'mcp_settings.json' ), configKey: 'mcpServers', }, trae: { name: 'Trae', getConfigPath: () => path.join(getPlatformConfigBase(), 'Trae', 'mcp.json'), configKey: 'mcpServers', }, }; // Helper: Safe JSON Read async function safeReadJson<T>(filePath: string): Promise<T | null> { try { // Check access first to avoid throwing on non-existent files try { await fsPromises.access(filePath, fs.constants.R_OK); } catch { return null; } const content = await fsPromises.readFile(filePath, 'utf-8'); if (!content.trim()) { return null; } return JSON.parse(content) as T; } catch (error) { if (outputChannel) { outputChannel.appendLine( `Failed to read/parse JSON at ${filePath}: ${error}` ); } return null; } } // Detect editor type function getEditorInfo(): { name: string; scheme: string; mcpConfigPath: string | null; } { try { const appName = vscode.env.appName.toLowerCase(); if (appName.includes('cursor')) { // Windows uses %APPDATA%\Cursor, macOS/Linux use ~/.cursor const cursorConfigPath = process.platform === 'win32' ? path.join(getPlatformConfigBase(), 'Cursor', 'mcp.json') : path.join(os.homedir(), '.cursor', 'mcp.json'); return { name: 'Cursor', scheme: 'cursor', mcpConfigPath: cursorConfigPath, }; } if (appName.includes('windsurf')) { return { name: 'Windsurf', scheme: 'windsurf', mcpConfigPath: path.join( os.homedir(), '.codeium', 'windsurf', 'mcp_config.json' ), }; } if (appName.includes('antigravity')) { return { name: 'Antigravity', scheme: 'antigravity', mcpConfigPath: path.join( os.homedir(), '.gemini', 'antigravity', 'mcp_config.json' ), }; } if (appName.includes('trae')) { return { name: 'Trae', scheme: 'trae', mcpConfigPath: path.join(getPlatformConfigBase(), 'Trae', 'mcp.json'), }; } // VS Code fallback - use Claude Desktop config (platform-aware) return { name: 'VS Code', scheme: 'vscode', mcpConfigPath: path.join( getPlatformConfigBase(), 'Claude', 'claude_desktop_config.json' ), }; } catch { // Fallback if something fails in detection return { name: 'VS Code', scheme: 'vscode', mcpConfigPath: null, }; } } // ===== GitHub Authentication Functions ===== /** * Get the current GitHub token - prefers OAuth session, falls back to manual config */ async function getGitHubToken(): Promise<string | undefined> { // First try to get token from VS Code's GitHub authentication (OAuth) try { const session = await vscode.authentication.getSession( GITHUB_AUTH_PROVIDER_ID, GITHUB_SCOPES, { silent: true } // Don't prompt, just check if already authenticated ); if (session) { return session.accessToken; } } catch (err) { outputChannel.appendLine(`Error checking GitHub session: ${err}`); } // Fall back to manual token from settings const config = vscode.workspace.getConfiguration('octocode'); return config.get<string>('githubToken'); } /** * Login to GitHub using VS Code's built-in OAuth flow */ async function loginToGitHub(): Promise< vscode.AuthenticationSession | undefined > { try { outputChannel.appendLine('Initiating GitHub OAuth login...'); const session = await vscode.authentication.getSession( GITHUB_AUTH_PROVIDER_ID, GITHUB_SCOPES, { createIfNone: true } // This triggers the OAuth flow if not authenticated ); if (session) { outputChannel.appendLine( `Logged in to GitHub as ${session.account.label}` ); isAuthenticated = true; // Update all MCP configs with the new token await syncTokenToAllConfigs(session.accessToken); vscode.window.showInformationMessage( `Signed in to GitHub as ${session.account.label}. MCP configs updated!` ); return session; } } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error); outputChannel.appendLine(`GitHub login failed: ${errorMsg}`); vscode.window.showErrorMessage(`GitHub login failed: ${errorMsg}`); } return undefined; } /** * Logout from GitHub (clear token from MCP configs) */ async function logoutFromGitHub(): Promise<void> { try { outputChannel.appendLine('Clearing GitHub token from MCP configs...'); isAuthenticated = false; // Clear from MCP configs await syncTokenToAllConfigs(undefined); vscode.window.showInformationMessage( 'GitHub token cleared from MCP configs. To fully sign out, use VS Code Account menu (bottom left).' ); } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error); outputChannel.appendLine(`Error during logout: ${errorMsg}`); vscode.window.showErrorMessage(`Error clearing GitHub token: ${errorMsg}`); } } /** * Sync GitHub token to all known MCP config locations */ async function syncTokenToAllConfigs(token: string | undefined): Promise<void> { const editorInfo = getEditorInfo(); const configPaths: { name: string; path: string }[] = []; // Add current editor's config if (editorInfo.mcpConfigPath) { configPaths.push({ name: editorInfo.name, path: editorInfo.mcpConfigPath }); } // Add all MCP client configs for (const client of Object.values(MCP_CLIENTS)) { try { configPaths.push({ name: client.name, path: client.getConfigPath() }); } catch { // Ignore errors getting path } } // Update each config for (const { name, path: configPath } of configPaths) { try { await updateMcpConfigToken(configPath, token); outputChannel.appendLine( `Updated token in ${name} config: ${configPath}` ); } catch (err) { outputChannel.appendLine(`Failed to update ${name} config: ${err}`); } } } /** * Update GitHub token in a specific MCP config file */ async function updateMcpConfigToken( configPath: string, token: string | undefined ): Promise<void> { const existingConfig = await safeReadJson<McpConfig>(configPath); if (!existingConfig?.mcpServers?.[MCP_SERVER_NAME]) { // Server not configured in this file, skip return; } const serverConfig = existingConfig.mcpServers[MCP_SERVER_NAME]; if (token) { serverConfig.env = { ...serverConfig.env, GITHUB_TOKEN: token }; } else { // Remove token if (serverConfig.env) { delete serverConfig.env.GITHUB_TOKEN; if (Object.keys(serverConfig.env).length === 0) { delete serverConfig.env; } } } await fsPromises.writeFile( configPath, JSON.stringify(existingConfig, null, 2), 'utf-8' ); } /** * Check current GitHub auth status */ async function checkGitHubAuthStatus(): Promise<{ authenticated: boolean; accountName?: string; tokenSource: 'oauth' | 'manual' | 'none'; }> { // Check OAuth session first try { const session = await vscode.authentication.getSession( GITHUB_AUTH_PROVIDER_ID, GITHUB_SCOPES, { silent: true } ); if (session) { return { authenticated: true, accountName: session.account.label, tokenSource: 'oauth', }; } } catch { // Ignore } // Check manual token const config = vscode.workspace.getConfiguration('octocode'); const manualToken = config.get<string>('githubToken'); if (manualToken) { return { authenticated: true, tokenSource: 'manual', }; } return { authenticated: false, tokenSource: 'none', }; } // Install MCP server in editor's config async function installMcpServer( mcpConfigPath: string, showNotification = true, clientName = 'editor' ): Promise<boolean> { try { if (!mcpConfigPath) { throw new Error('Invalid configuration path provided'); } // Get GitHub token (OAuth or manual) const githubToken = await getGitHubToken(); let mcpConfig: McpConfig = { mcpServers: {} }; // Read existing config safely const existingConfig = await safeReadJson<McpConfig>(mcpConfigPath); if (existingConfig && typeof existingConfig === 'object') { // Preserve existing config structure mcpConfig = { ...existingConfig, mcpServers: existingConfig.mcpServers || {}, }; } // Check if already configured correctly const existingServer = mcpConfig.mcpServers[MCP_SERVER_NAME]; if ( existingServer && existingServer.command === MCP_COMMAND && JSON.stringify(existingServer.args) === JSON.stringify(MCP_ARGS) ) { // Check if env var needs update (e.g. token changed) const currentToken = existingServer.env?.GITHUB_TOKEN; if (currentToken === githubToken) { if (showNotification) { vscode.window.showInformationMessage( `Octocode MCP server is already configured for ${clientName}.` ); } return false; } } // Configure server const serverConfig: McpServerConfig = { command: MCP_COMMAND, type: 'stdio', args: MCP_ARGS, }; // Add GitHub token if configured if (githubToken) { serverConfig.env = { GITHUB_TOKEN: githubToken, }; } mcpConfig.mcpServers[MCP_SERVER_NAME] = serverConfig; // Ensure directory exists try { const dirPath = path.dirname(mcpConfigPath); await fsPromises.mkdir(dirPath, { recursive: true }); } catch (err) { throw new Error( `Failed to create directory ${path.dirname(mcpConfigPath)}: ${err}` ); } // Write config try { await fsPromises.writeFile( mcpConfigPath, JSON.stringify(mcpConfig, null, 2), 'utf-8' ); } catch (err) { throw new Error(`Failed to write config file ${mcpConfigPath}: ${err}`); } outputChannel.appendLine(`MCP server configured at: ${mcpConfigPath}`); if (showNotification) { vscode.window.showInformationMessage( `Octocode MCP server configured for ${clientName}! Restart to enable it.` ); } return true; } catch (err) { const errorMsg = err instanceof Error ? err.message : String(err); outputChannel.appendLine(`Failed to configure MCP server: ${errorMsg}`); if (showNotification) { vscode.window.showErrorMessage( `Failed to configure MCP server: ${errorMsg}` ); } return false; } } // Install MCP server for a specific client async function installForClient(clientKey: string): Promise<void> { try { const client = MCP_CLIENTS[clientKey]; if (!client) { vscode.window.showErrorMessage(`Unknown MCP client: ${clientKey}`); return; } const configPath = client.getConfigPath(); await installMcpServer(configPath, true, client.name); } catch (err) { const errorMsg = err instanceof Error ? err.message : String(err); vscode.window.showErrorMessage( `Error installing for ${clientKey}: ${errorMsg}` ); outputChannel.appendLine(`Error installing for ${clientKey}: ${errorMsg}`); } } // Start MCP server process async function startMcpServer(): Promise<void> { try { if (mcpProcess) { vscode.window.showWarningMessage('MCP server is already running.'); return; } // Get GitHub token (OAuth or manual) const githubToken = await getGitHubToken(); const env: Record<string, string | undefined> = { ...process.env }; if (githubToken) { env.GITHUB_TOKEN = githubToken; } outputChannel.appendLine('Starting Octocode MCP server...'); try { mcpProcess = spawn('npx', ['octocode-mcp@latest'], { env, stdio: ['pipe', 'pipe', 'pipe'], shell: process.platform === 'win32', }); } catch (spawnError) { outputChannel.appendLine(`Failed to spawn process: ${spawnError}`); vscode.window.showErrorMessage( `Failed to start MCP server process: ${spawnError}` ); return; } if (mcpProcess.stdout) { mcpProcess.stdout.on('data', (data: Buffer) => { outputChannel.appendLine(`[stdout] ${data.toString()}`); }); } if (mcpProcess.stderr) { mcpProcess.stderr.on('data', (data: Buffer) => { outputChannel.appendLine(`[stderr] ${data.toString()}`); }); } mcpProcess.on('close', (code: number | null) => { outputChannel.appendLine(`MCP server exited with code ${code}`); mcpProcess = null; updateStatusBar(false); }); mcpProcess.on('error', (err: Error) => { outputChannel.appendLine(`Failed to start MCP server: ${err.message}`); mcpProcess = null; updateStatusBar(false); vscode.window.showErrorMessage(`MCP Server error: ${err.message}`); }); updateStatusBar(true); vscode.window.showInformationMessage('Octocode MCP server started.'); } catch (err) { const errorMsg = err instanceof Error ? err.message : String(err); outputChannel.appendLine(`Unexpected error starting server: ${errorMsg}`); vscode.window.showErrorMessage( `Unexpected error starting server: ${errorMsg}` ); if (mcpProcess) { try { (mcpProcess as ChildProcess).kill(); } catch { // ignore kill error } mcpProcess = null; } } } // Stop MCP server process function stopMcpServer(): void { try { if (!mcpProcess) { vscode.window.showWarningMessage('MCP server is not running.'); return; } outputChannel.appendLine('Stopping Octocode MCP server...'); mcpProcess.kill(); mcpProcess = null; updateStatusBar(false); vscode.window.showInformationMessage('Octocode MCP server stopped.'); } catch (err) { const errorMsg = err instanceof Error ? err.message : String(err); outputChannel.appendLine(`Error stopping server: ${errorMsg}`); } } // Update status bar function updateStatusBar(running: boolean, authenticated?: boolean): void { try { const authIcon = authenticated ? '$(verified)' : ''; const authTooltip = authenticated ? ' (GitHub authenticated)' : ' (no GitHub auth)'; if (running) { statusBarItem.text = `$(zap) Octocode MCP: Running ${authIcon}`; statusBarItem.tooltip = `Octocode MCP server is running${authTooltip}. Click to stop.`; statusBarItem.command = 'octocode.stopServer'; statusBarItem.backgroundColor = undefined; } else { statusBarItem.text = `$(circle-slash) Octocode MCP: Off ${authIcon}`; statusBarItem.tooltip = `Octocode MCP server is stopped${authTooltip}. Click to start.`; statusBarItem.command = 'octocode.startServer'; statusBarItem.backgroundColor = new vscode.ThemeColor( 'statusBarItem.warningBackground' ); } statusBarItem.show(); } catch (err) { outputChannel.appendLine(`Error updating status bar: ${err}`); } } export async function activate( context: vscode.ExtensionContext ): Promise<void> { try { outputChannel = vscode.window.createOutputChannel('Octocode MCP'); // Wrap editor info detection let editorInfo; try { editorInfo = getEditorInfo(); } catch (e) { outputChannel.appendLine(`Error detecting editor: ${e}`); editorInfo = { name: 'VS Code', scheme: 'vscode', mcpConfigPath: null }; } outputChannel.appendLine( `Octocode MCP extension activated in ${editorInfo.name}` ); // Create status bar item statusBarItem = vscode.window.createStatusBarItem( vscode.StatusBarAlignment.Right, 100 ); context.subscriptions.push(statusBarItem); // Check initial auth status const initialAuthStatus = await checkGitHubAuthStatus(); isAuthenticated = initialAuthStatus.authenticated; updateStatusBar(false, isAuthenticated); if (initialAuthStatus.authenticated) { outputChannel.appendLine( `GitHub authenticated via ${initialAuthStatus.tokenSource}` + (initialAuthStatus.accountName ? ` as ${initialAuthStatus.accountName}` : '') ); } const config = vscode.workspace.getConfiguration('octocode'); // Register commands context.subscriptions.push( vscode.commands.registerCommand('octocode.startServer', () => { startMcpServer(); }) ); context.subscriptions.push( vscode.commands.registerCommand('octocode.stopServer', () => { stopMcpServer(); }) ); context.subscriptions.push( vscode.commands.registerCommand('octocode.showStatus', () => { if (mcpProcess) { vscode.window.showInformationMessage( "Octocode MCP server is running.\n\nTo use with AI assistants, the server should be configured in your editor's MCP settings." ); } else { vscode.window.showInformationMessage( "Octocode MCP server is not running.\n\nUse 'Octocode MCP: Start Server' to start it, or install it in your editor's MCP config for automatic startup." ); } }) ); // ===== GitHub Authentication Commands ===== context.subscriptions.push( vscode.commands.registerCommand('octocode.loginGitHub', async () => { await loginToGitHub(); const status = await checkGitHubAuthStatus(); updateStatusBar(mcpProcess !== null, status.authenticated); }) ); context.subscriptions.push( vscode.commands.registerCommand('octocode.logoutGitHub', async () => { await logoutFromGitHub(); const status = await checkGitHubAuthStatus(); updateStatusBar(mcpProcess !== null, status.authenticated); }) ); context.subscriptions.push( vscode.commands.registerCommand('octocode.showAuthStatus', async () => { const status = await checkGitHubAuthStatus(); if (status.authenticated) { const source = status.tokenSource === 'oauth' ? 'GitHub OAuth' : 'manual token'; const account = status.accountName ? ` as ${status.accountName}` : ''; vscode.window.showInformationMessage( `GitHub: Authenticated${account} (via ${source})` ); } else { const action = await vscode.window.showInformationMessage( 'GitHub: Not authenticated. Sign in to access private repositories.', 'Sign in to GitHub' ); if (action === 'Sign in to GitHub') { await loginToGitHub(); } } }) ); // Listen for GitHub auth session changes context.subscriptions.push( vscode.authentication.onDidChangeSessions(async e => { if (e.provider.id === GITHUB_AUTH_PROVIDER_ID) { outputChannel.appendLine('GitHub auth session changed'); // Check new auth state const session = await vscode.authentication.getSession( GITHUB_AUTH_PROVIDER_ID, GITHUB_SCOPES, { silent: true } ); if (session) { outputChannel.appendLine( `Session updated for ${session.account.label}` ); isAuthenticated = true; await syncTokenToAllConfigs(session.accessToken); } else { outputChannel.appendLine('Session cleared'); isAuthenticated = false; await syncTokenToAllConfigs(undefined); } updateStatusBar(mcpProcess !== null, isAuthenticated); } }) ); context.subscriptions.push( vscode.commands.registerCommand('octocode.installMcp', async () => { try { if (editorInfo.mcpConfigPath) { await installMcpServer( editorInfo.mcpConfigPath, true, editorInfo.name ); } else { vscode.window.showErrorMessage( 'MCP configuration not supported for this editor.' ); } } catch (err) { const msg = err instanceof Error ? err.message : String(err); vscode.window.showErrorMessage(`Failed to install MCP: ${msg}`); } }) ); // Register commands for specific MCP clients const registerInstallCommand = (command: string, clientKey: string) => { context.subscriptions.push( vscode.commands.registerCommand(command, async () => { await installForClient(clientKey); }) ); }; registerInstallCommand('octocode.installForCline', 'cline'); registerInstallCommand('octocode.installForRooCode', 'rooCode'); registerInstallCommand('octocode.installForTrae', 'trae'); // Install for all known MCP clients context.subscriptions.push( vscode.commands.registerCommand('octocode.installForAll', async () => { const results: string[] = []; for (const client of Object.values(MCP_CLIENTS)) { try { const configPath = client.getConfigPath(); const installed = await installMcpServer( configPath, false, client.name ); if (installed) { results.push(`✅ ${client.name}`); } else { results.push(`⏭️ ${client.name} (already configured)`); } } catch { results.push(`❌ ${client.name} (failed)`); } } vscode.window.showInformationMessage( `Octocode MCP installation complete:\n${results.join('\n')}` ); }) ); // Auto-install MCP server if configured try { const autoInstall = config.get<boolean>('autoInstallMcp', true); if (autoInstall && editorInfo.mcpConfigPath) { // Check if config file exists and has our server let needsInstall = true; const existingConfig = await safeReadJson<McpConfig>( editorInfo.mcpConfigPath ); if (existingConfig?.mcpServers?.[MCP_SERVER_NAME]) { needsInstall = false; } if (needsInstall) { const wasInstalled = await installMcpServer( editorInfo.mcpConfigPath, false ); if (wasInstalled) { vscode.window.showInformationMessage( `Octocode MCP server has been configured. Restart ${editorInfo.name} to enable it.` ); } } } } catch (autoInstallErr) { outputChannel.appendLine(`Auto-install failed: ${autoInstallErr}`); // Don't show error message to user to avoid annoyance on startup } // Cleanup on deactivation context.subscriptions.push({ dispose: () => { try { if (mcpProcess) { mcpProcess.kill(); mcpProcess = null; } outputChannel.dispose(); } catch { // ignore dispose error } }, }); outputChannel.appendLine('Octocode MCP extension ready.'); } catch (activationError) { console.error('Failed to activate Octocode MCP:', activationError); if (activationError instanceof Error) { vscode.window.showErrorMessage( `Octocode MCP failed to activate: ${activationError.message}` ); } } } export function deactivate(): void { try { if (mcpProcess) { mcpProcess.kill(); mcpProcess = null; } } catch { // ignore kill error } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/bgauryy/octocode-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server