#!/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);