Skip to main content
Glama
index.ts21.8 kB
#!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema, Tool, } from "@modelcontextprotocol/sdk/types.js"; import { exec } from "child_process"; import { promisify } from "util"; import * as fs from "fs/promises"; import * as path from "path"; const execAsync = promisify(exec); interface CodeReviewRequest { filePath?: string; directory?: string; code?: string; reviewers?: string[]; context?: string; } interface CLIAvailability { codex: boolean; gemini: boolean; checked: boolean; } class CodeReviewMCPServer { private server: Server; private cliAvailability: CLIAvailability = { codex: false, gemini: false, checked: false, }; constructor() { this.server = new Server( { name: "review-mcp", version: "1.0.0", }, { capabilities: { tools: {}, }, } ); this.setupHandlers(); this.detectCLIs(); } private async detectCLIs() { console.error("Detecting available CLI tools..."); // Check Codex CLI (codex-cli or openai) try { const { stdout } = await execAsync("codex --version", { timeout: 5000 }); if (stdout.includes("codex-cli")) { this.cliAvailability.codex = true; console.error("✓ Codex CLI detected"); } } catch (error) { // Try OpenAI CLI as fallback try { await execAsync("openai --version", { timeout: 5000 }); this.cliAvailability.codex = true; console.error("✓ OpenAI CLI detected"); } catch (error2) { this.cliAvailability.codex = false; console.error("✗ Codex/OpenAI CLI not found"); } } // Check Gemini CLI try { await execAsync("gemini --version 2>/dev/null || gemini --help", { timeout: 5000 }); this.cliAvailability.gemini = true; console.error("✓ Gemini CLI detected"); } catch (error) { this.cliAvailability.gemini = false; console.error("✗ Gemini CLI not found"); } this.cliAvailability.checked = true; if (!this.cliAvailability.codex && !this.cliAvailability.gemini) { console.error("⚠️ Warning: No review CLIs detected. Install OpenAI CLI and/or Gemini CLI to enable reviews."); } } private setupHandlers() { this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: this.getTools(), })); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { switch (name) { case "check_cli_status": return await this.handleCheckCLIStatus(); case "review_code": return await this.handleReviewCode(args as CodeReviewRequest); case "review_file": return await this.handleReviewFile(args as CodeReviewRequest); case "review_directory": return await this.handleReviewDirectory(args as CodeReviewRequest); default: throw new Error(`Unknown tool: ${name}`); } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { content: [ { type: "text", text: `Error: ${errorMessage}`, }, ], }; } }); } private async handleCheckCLIStatus() { // Ensure detection has completed if (!this.cliAvailability.checked) { await this.detectCLIs(); } let status = "# CLI Status Check\n\n"; if (this.cliAvailability.codex) { status += "✓ **OpenAI CLI (Codex)**: Available\n"; } else { status += "✗ **OpenAI CLI (Codex)**: Not found\n"; status += " - Install: `npm install -g openai`\n"; status += " - Set API key: `export OPENAI_API_KEY='your-key'`\n"; } status += "\n"; if (this.cliAvailability.gemini) { status += "✓ **Gemini CLI**: Available\n"; } else { status += "✗ **Gemini CLI**: Not found\n"; status += " - See setup instructions in README.md\n"; status += " - Set API key: `export GOOGLE_API_KEY='your-key'`\n"; } status += "\n"; if (this.cliAvailability.codex && this.cliAvailability.gemini) { status += "**Status**: Both review CLIs are available. All features enabled.\n"; } else if (this.cliAvailability.codex || this.cliAvailability.gemini) { status += "**Status**: One review CLI is available. Partial functionality enabled.\n"; } else { status += "**Status**: No review CLIs available. Please install at least one to use reviews.\n"; } return { content: [ { type: "text", text: status, }, ], }; } private getTools(): Tool[] { return [ { name: "check_cli_status", description: "Check which code review CLIs (Codex/OpenAI and Gemini) are installed and available. Use this before requesting reviews to see what's available.", inputSchema: { type: "object", properties: {}, }, }, { name: "review_code", description: "Request a code review from Codex and Gemini CLIs. Provide code directly as a string. Returns feedback from both reviewers for Claude to consider.", inputSchema: { type: "object", properties: { code: { type: "string", description: "The code to review", }, context: { type: "string", description: "Additional context about the code (optional)", }, reviewers: { type: "array", items: { type: "string", enum: ["codex", "gemini", "both"], }, description: "Which reviewers to use (default: both)", }, }, required: ["code"], }, }, { name: "review_file", description: "Request a code review of a specific file from Codex and Gemini CLIs. Returns feedback from both reviewers for Claude to consider.", inputSchema: { type: "object", properties: { filePath: { type: "string", description: "Path to the file to review", }, context: { type: "string", description: "Additional context about the code (optional)", }, reviewers: { type: "array", items: { type: "string", enum: ["codex", "gemini", "both"], }, description: "Which reviewers to use (default: both)", }, }, required: ["filePath"], }, }, { name: "review_directory", description: "Request a code review of all files in a directory from Codex and Gemini CLIs. Returns feedback from both reviewers for Claude to consider.", inputSchema: { type: "object", properties: { directory: { type: "string", description: "Path to the directory to review", }, context: { type: "string", description: "Additional context about the code (optional)", }, reviewers: { type: "array", items: { type: "string", enum: ["codex", "gemini", "both"], }, description: "Which reviewers to use (default: both)", }, }, required: ["directory"], }, }, ]; } private async handleReviewCode(args: CodeReviewRequest) { const { code, context, reviewers = ["both"] } = args; if (!code) { throw new Error("Code is required"); } const reviews = await this.performReview(code, context, reviewers); return { content: [ { type: "text", text: this.formatReviews(reviews), }, ], }; } private async handleReviewFile(args: CodeReviewRequest) { const { filePath, context, reviewers = ["both"] } = args; if (!filePath) { throw new Error("File path is required"); } const code = await fs.readFile(filePath, "utf-8"); const reviews = await this.performReview( code, `File: ${filePath}\n${context || ""}`, reviewers ); return { content: [ { type: "text", text: this.formatReviews(reviews), }, ], }; } private async handleReviewDirectory(args: CodeReviewRequest) { const { directory, context, reviewers = ["both"] } = args; if (!directory) { throw new Error("Directory path is required"); } const files = await this.getCodeFiles(directory); const allReviews: Array<{ file: string; reviews: Record<string, string> }> = []; for (const file of files) { const code = await fs.readFile(file, "utf-8"); const reviews = await this.performReview( code, `File: ${file}\n${context || ""}`, reviewers ); allReviews.push({ file, reviews }); } return { content: [ { type: "text", text: this.formatDirectoryReviews(allReviews), }, ], }; } private async getCodeFiles(directory: string): Promise<string[]> { const files: string[] = []; const entries = await fs.readdir(directory, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(directory, entry.name); if (entry.isDirectory()) { if (!entry.name.startsWith(".") && entry.name !== "node_modules") { files.push(...(await this.getCodeFiles(fullPath))); } } else if (this.isCodeFile(entry.name)) { files.push(fullPath); } } return files; } private isCodeFile(filename: string): boolean { const codeExtensions = [ ".js", ".ts", ".jsx", ".tsx", ".py", ".rb", ".go", ".java", ".c", ".cpp", ".cs", ".php", ".swift", ".kt", ".rs", ]; return codeExtensions.some((ext) => filename.endsWith(ext)); } private async performReview( code: string, context: string | undefined, reviewers: string[] ): Promise<Record<string, string>> { const reviews: Record<string, string> = {}; const useCodex = (reviewers.includes("codex") || reviewers.includes("both")) && this.cliAvailability.codex; const useGemini = (reviewers.includes("gemini") || reviewers.includes("both")) && this.cliAvailability.gemini; // If no CLIs are available, provide a helpful message if (!this.cliAvailability.codex && !this.cliAvailability.gemini) { reviews.info = "⚠️ No review CLIs are currently available. Please install OpenAI CLI and/or Gemini CLI to enable code reviews.\n\n" + "Check status with the check_cli_status tool."; return reviews; } const reviewPrompt = this.buildReviewPrompt(code, context); const reviewPromises: Promise<void>[] = []; if (useCodex) { reviewPromises.push( this.getCodexReview(reviewPrompt).then((review) => { reviews.codex = review; }) ); } else if (reviewers.includes("codex")) { reviews.codex = "⚠️ Codex review requested but OpenAI CLI is not available."; } if (useGemini) { reviewPromises.push( this.getGeminiReview(reviewPrompt).then((review) => { reviews.gemini = review; }) ); } else if (reviewers.includes("gemini")) { reviews.gemini = "⚠️ Gemini review requested but Gemini CLI is not available."; } await Promise.all(reviewPromises); return reviews; } private buildReviewPrompt(code: string, context?: string): string { let prompt = "You are a senior software engineer. "; prompt += "Code review the changes and implementation. "; prompt += "Don't change anything, just review.\n\n"; if (context) { prompt += `Context: ${context}\n\n`; } prompt += `Code:\n\`\`\`\n${code}\n\`\`\`\n\n`; prompt += "Be thorough, direct, and specific. Prioritize by severity."; return prompt; } private validateReview(review: string, source: string): string | null { if (!review || review.trim().length < 20) { console.error(`${source}: Review too short or empty`); return null; } const lowerReview = review.toLowerCase(); // Check if it's actually trying to rewrite code instead of reviewing const codeRewriteIndicators = [ 'here is the fixed code', 'here\'s the corrected', 'replace with:', 'should be:\n```', 'corrected code:', 'updated code:' ]; for (const indicator of codeRewriteIndicators) { if (lowerReview.includes(indicator)) { return `⚠️ ${source} attempted to rewrite the code instead of reviewing. Ignoring this response.\n\nNote: The reviewer was instructed to only provide feedback, not rewrite code.`; } } // Check if it's an error message or unhelpful response const invalidIndicators = [ 'i cannot', 'i\'m unable to', 'i don\'t have access', 'as an ai', 'i cannot assist with', 'error:', 'command not found', 'no such file' ]; for (const indicator of invalidIndicators) { if (lowerReview.includes(indicator)) { console.error(`${source}: Invalid or error response detected`); return null; } } // Check if response is relevant to code review const reviewKeywords = [ 'bug', 'issue', 'error', 'security', 'vulnerability', 'performance', 'recommend', 'suggest', 'improve', 'concern', 'risk', 'problem', 'should', 'could', 'better', 'consider', 'best practice' ]; const hasReviewContent = reviewKeywords.some(keyword => lowerReview.includes(keyword) ); if (!hasReviewContent) { console.error(`${source}: Response doesn't appear to be a code review`); return `⚠️ ${source} provided a response that doesn't appear to be a code review. The response may be off-topic or unclear.`; } return review; } private async getCodexReview(prompt: string): Promise<string> { try { // Try Codex CLI first - use single quotes and escape any single quotes in prompt const escapedPrompt = prompt.replace(/'/g, "'\\''"); const { stdout, stderr } = await execAsync( `codex exec --skip-git-repo-check '${escapedPrompt}'`, { timeout: 300000 } // 5 minutes ); if (stderr && !stdout) { throw new Error(stderr); } // Parse Codex CLI output // The actual review content appears after "tokens used" section // Codex duplicates the output at the end for convenience const lines = stdout.split('\n'); // Find "tokens used" line let tokensUsedIndex = -1; for (let i = 0; i < lines.length; i++) { if (lines[i].trim().startsWith('tokens used')) { tokensUsedIndex = i; break; } } if (tokensUsedIndex > 0) { // Get everything after "tokens used" and the line with the number let reviewStart = tokensUsedIndex + 1; // Skip the actual number line if present if (lines[reviewStart] && lines[reviewStart].match(/^\d+/)) { reviewStart++; } const reviewLines = []; for (let i = reviewStart; i < lines.length; i++) { const line = lines[i]; // Skip empty lines at start if (reviewLines.length === 0 && !line.trim()) continue; reviewLines.push(line); } const review = reviewLines.join('\n').trim(); if (review) { // Validate the review before returning const validatedReview = this.validateReview(review, 'Codex'); if (validatedReview) return validatedReview; } } // Fallback: look for content after "codex" label for (let i = 0; i < lines.length; i++) { if (lines[i].trim() === 'codex') { const reviewLines = []; for (let j = i + 1; j < lines.length; j++) { if (lines[j].trim().startsWith('tokens used')) break; reviewLines.push(lines[j]); } const review = reviewLines.join('\n').trim(); if (review) { const validatedReview = this.validateReview(review, 'Codex'); if (validatedReview) return validatedReview; } } } return "⚠️ No valid review content received from Codex"; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); // Try OpenAI CLI as fallback if (errorMessage.includes("command not found") || errorMessage.includes("not found")) { try { const { stdout, stderr } = await execAsync( `openai api chat.completions.create -m gpt-4 -g user "${prompt.replace(/"/g, '\\"')}"`, { timeout: 300000 } // 5 minutes ); if (stdout) return stdout.trim(); } catch (e) { return "⚠️ Codex CLI not found. Please install either:\n" + "- Codex CLI: brew install codex-cli\n" + "- OpenAI CLI: npm install -g openai"; } } return `⚠️ Codex review failed: ${errorMessage}`; } } private async getGeminiReview(prompt: string): Promise<string> { try { // Try Google AI CLI - use single quotes and escape any single quotes in prompt const escapedPrompt = prompt.replace(/'/g, "'\\''"); const { stdout, stderr } = await execAsync( `gemini '${escapedPrompt}'`, { timeout: 300000 } // 5 minutes ); if (stderr && !stdout) { throw new Error(stderr); } const review = stdout.trim(); if (!review) { return "⚠️ No review content received from Gemini"; } // Validate the review before returning const validatedReview = this.validateReview(review, 'Gemini'); return validatedReview || "⚠️ No valid review content received from Gemini"; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); if (errorMessage.includes("command not found") || errorMessage.includes("not found")) { return "⚠️ Gemini CLI not found. Please install Google AI CLI or set up gemini command.\n" + "You can use: https://github.com/google/generative-ai-python or similar tools."; } return `⚠️ Gemini review failed: ${errorMessage}`; } } private formatReviews(reviews: Record<string, string>): string { let output = "# Code Review Feedback\n\n"; // If there's an info message (e.g., no CLIs available), show it prominently if (reviews.info) { output += reviews.info; output += "\n\n"; return output; } output += "Claude, please consider this feedback from other AI reviewers:\n\n"; let hasValidReviews = false; if (reviews.codex) { output += "## Codex Review\n\n"; output += reviews.codex; output += "\n\n"; // Check if it's a valid review (not a warning) if (!reviews.codex.startsWith('⚠️')) { hasValidReviews = true; } } if (reviews.gemini) { output += "## Gemini Review\n\n"; output += reviews.gemini; output += "\n\n"; // Check if it's a valid review (not a warning) if (!reviews.gemini.startsWith('⚠️')) { hasValidReviews = true; } } output += "---\n\n"; if (hasValidReviews) { output += "**Instructions for Claude:**\n"; output += "Please analyze the above feedback and provide your own assessment. "; output += "You can agree, disagree, or add additional insights based on your review of the code. "; output += "If any reviewer attempted to rewrite code or provided off-topic responses, "; output += "note this and conduct your own independent review focusing on the actual issues."; } else { output += "**Note:** No valid external reviews were received. "; output += "Please conduct your own thorough code review as a senior engineer would."; } return output; } private formatDirectoryReviews( allReviews: Array<{ file: string; reviews: Record<string, string> }> ): string { let output = "# Directory Code Review Feedback\n\n"; output += "Claude, please consider this feedback from other AI reviewers:\n\n"; for (const { file, reviews } of allReviews) { output += `## File: ${file}\n\n`; if (reviews.codex) { output += "### Codex Review\n\n"; output += reviews.codex; output += "\n\n"; } if (reviews.gemini) { output += "### Gemini Review\n\n"; output += reviews.gemini; output += "\n\n"; } output += "---\n\n"; } output += "Please analyze this feedback and provide your own assessment of the codebase."; return output; } async run() { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error("Review MCP Server running on stdio"); } } const server = new CodeReviewMCPServer(); server.run().catch(console.error);

Implementation Reference

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/je4550/review-mcp'

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