#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { spawn } from "child_process";
/**
* 会话存储接口
*/
interface SessionData {
threadId: string;
lastActivity: number;
}
/**
* Codex CLI 包装器
* 直接调用系统安装的 'codex' 命令以获得完整的权限控制
*/
class CodexClient {
/**
* 执行 Codex 命令
*/
async run(prompt: string, threadId?: string): Promise<{ text: string; newThreadId?: string }> {
return new Promise((resolve, reject) => {
const args = ['exec'];
if (threadId) {
// 恢复会话
// 注意:resume 子命令支持的选项有限,暂时只添加 --json
// 如果需要覆盖配置,可以使用 -c,例如: args.push('-c', 'sandbox_mode="workspace-write"')
args.push('resume', threadId, '--json');
} else {
// 新会话
// 尝试使用更激进的 danger-full-access 以强制开启写权限
args.push('--json', '--sandbox', 'danger-full-access');
}
// 跳过 Git 仓库检查,防止环境检测问题
args.push('--skip-git-repo-check');
// 使用 stdin 传递提示词,避免 Shell 参数解析问题
args.push('-');
// 在 Windows 上使用 codex.cmd,其他平台使用 codex
const command = process.platform === 'win32' ? 'codex.cmd' : 'codex';
console.error(`正在执行命令: ${command} ${args.join(' ')}`);
const child = spawn(command, args, {
env: { ...process.env, FORCE_COLOR: '0' }, // 禁用颜色以减少控制字符
cwd: process.cwd(), // 在当前目录执行
shell: true // 在 Shell 中运行以解析 PATH
});
// 写入提示词到 stdin
child.stdin.write(prompt);
child.stdin.end();
let stdout = '';
let stderr = '';
let detectedThreadId: string | undefined = undefined;
let finalResponse = '';
child.stdout.on('data', (data) => {
const chunk = data.toString();
// 调试日志:打印 stdout
console.error(`[Codex CLI stdout] ${chunk}`);
stdout += chunk;
// 实时解析 JSONL 行以提取 thread_id 和 响应文本
const lines = chunk.split('\n');
for (const line of lines) {
if (!line.trim()) continue;
try {
const event = JSON.parse(line);
// 提取 Thread ID
if (event.type === 'thread.started' && event.thread_id) {
detectedThreadId = event.thread_id;
}
// 提取 Agent Message (顶层事件)
if (event.type === 'agent_message' && event.text) {
finalResponse = event.text;
}
// 提取 Agent Message (嵌套在 item.completed 中)
if (event.type === 'item.completed' && event.item && event.item.type === 'agent_message' && event.item.text) {
finalResponse = event.item.text;
}
} catch (e) {
// 忽略非 JSON 行
}
}
});
child.stderr.on('data', (data) => {
stderr += data.toString();
// 将 Codex 的进度输出转发到 MCP 的 stderr 日志
console.error(`[Codex CLI] ${data.toString().trim()}`);
});
child.on('close', (code) => {
if (code !== 0) {
console.error(`Codex 进程退出,代码: ${code}`);
console.error(`Stderr: ${stderr}`);
// 即使退出代码非0,如果有响应也返回,否则报错
if (finalResponse) {
resolve({ text: finalResponse, newThreadId: detectedThreadId });
} else {
reject(new Error(`Codex CLI 失败 (Exit ${code}): ${stderr}`));
}
} else {
resolve({ text: finalResponse, newThreadId: detectedThreadId });
}
});
child.on('error', (err) => {
reject(err);
});
});
}
}
/**
* Codex MCP Server 类
*/
class CodexMCPServer {
private server: Server;
private client: CodexClient;
private sessions: Map<string, SessionData>;
constructor() {
this.client = new CodexClient();
this.sessions = new Map();
// 初始化 MCP Server
this.server = new Server(
{
name: "codex-mcp-server",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
this.setupHandlers();
}
/**
* 设置 MCP 请求处理器
*/
private setupHandlers(): void {
// 列出可用工具
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "review_project",
description:
"【核心工具】审查整个项目的代码。适用于跨文件夹修改、系统级重构或需要全面上下文的情况。Codex 将分析项目结构和代码,根据提供的变更描述进行审查和修复。",
inputSchema: {
type: "object",
properties: {
project_path: {
type: "string",
description: "当前项目的根目录路径(绝对路径)",
},
change_description: {
type: "string",
description: "Agent 具体修改了代码的什么内容(详细说明修改了哪些文件、逻辑以及目的)",
},
session_id: {
type: "string",
description: "用于维护上下文连续性的会话 ID。**重要提示**:Agent 应为每个独立的用户任务生成一个唯一的 Session ID,并在该任务的所有相关工具调用中复用它。这允许 Codex 记住之前的变更和对话历史。",
},
},
required: ["project_path", "change_description"],
},
},
{
name: "review_file",
description:
"【次要工具】审查单个文件的内容。适用于改动仅在单个或两个文件内的情况。Codex 将读取文件并根据变更描述提供审查建议。",
inputSchema: {
type: "object",
properties: {
file_path: {
type: "string",
description: "要审查的文件路径(相对或绝对路径)",
},
change_description: {
type: "string",
description: "Agent 具体修改了代码的什么内容(详细说明修改了逻辑以及目的)",
},
session_id: {
type: "string",
description: "用于维护上下文连续性的会话 ID。**重要提示**:Agent 应为每个独立的用户任务生成一个唯一的 Session ID,并在该任务的所有相关工具调用中复用它。这允许 Codex 记住之前的变更和对话历史。",
},
},
required: ["file_path", "change_description"],
},
},
{
name: "review_code_changes",
description:
"【通用工具】将代码或文本发送给 Codex 代理进行审查、修改和验证。Codex 将分析代码,执行必要的编辑或命令,并返回修复摘要。每次 Gemini 修改代码后,应该调用此工具获取 Codex 的审查意见。",
inputSchema: {
type: "object",
properties: {
content: {
type: "string",
description: "需要审查的代码或文本内容(Gemini 修改后的代码)",
},
session_id: {
type: "string",
description: "用于维护上下文连续性的会话 ID。**重要提示**:Agent 应为每个独立的用户任务生成一个唯一的 Session ID,并在该任务的所有相关工具调用中复用它。这允许 Codex 记住之前的变更和对话历史。",
},
context: {
type: "string",
description: "可选的上下文信息,例如文件路径、修改原因等",
},
},
required: ["content"],
},
},
{
name: "continue_review",
description:
"【会话工具】在现有会话中继续讨论。用于追问 Codex 更多细节或请求进一步的修改建议。",
inputSchema: {
type: "object",
properties: {
message: {
type: "string",
description: "要发送给 Codex 的消息",
},
session_id: {
type: "string",
description: "必需的会话 ID,用于继续之前的对话",
},
},
required: ["message", "session_id"],
},
},
],
};
});
// 调用工具
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
try {
switch (request.params.name) {
case "review_project": {
const { project_path, change_description, session_id } = request.params.arguments as {
project_path: string;
change_description: string;
session_id?: string;
};
const result = await this.reviewProject(project_path, change_description, session_id);
return {
content: [
{
type: "text",
text: result,
},
],
};
}
case "review_file": {
const { file_path, change_description, session_id } = request.params.arguments as {
file_path: string;
change_description: string;
session_id?: string;
};
const result = await this.reviewFile(file_path, change_description, session_id);
return {
content: [
{
type: "text",
text: result,
},
],
};
}
case "review_code_changes": {
const { content, session_id, context } = request.params.arguments as {
content: string;
session_id?: string;
context?: string;
};
const result = await this.reviewCodeChanges(content, session_id, context);
return {
content: [
{
type: "text",
text: result,
},
],
};
}
case "continue_review": {
const { message, session_id } = request.params.arguments as {
message: string;
session_id: string;
};
const result = await this.continueReview(message, session_id);
return {
content: [
{
type: "text",
text: result,
},
],
};
}
default:
throw new Error(`未知工具: ${request.params.name}`);
}
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : "未知错误";
console.error(`工具 ${request.params.name} 执行失败:`, error);
return {
content: [
{
type: "text",
text: `错误: ${errorMessage}`,
},
],
isError: true,
};
}
});
}
/**
* 审查代码变更的主要方法
*/
private async reviewCodeChanges(
content: string,
sessionId?: string,
context?: string
): Promise<string> {
try {
// 查找现有的 threadId
let threadId: string | undefined;
if (sessionId && this.sessions.has(sessionId)) {
threadId = this.sessions.get(sessionId)!.threadId;
console.error(`使用现有线程 ID: ${threadId}`);
}
// 构建提示词
let prompt = `你是一个智能代码代理,运行在项目根目录: ${process.cwd()}。
请根据用户的请求执行任务。你可以使用工具(如 ls, Get-Content, Set-Content 等)来探索代码库、读取文件和修改文件。
请直接执行必要的操作来完成请求,而不仅仅是提供建议。`;
if (context) {
prompt += `\n\n【上下文信息】\n${context}`;
}
prompt += `\n\n【用户请求】\n${content}`;
console.error("正在运行 Codex 审查 (CLI 模式)...");
// 运行 Codex CLI
const { text, newThreadId } = await this.client.run(prompt, threadId);
// 如果创建了新线程且提供了 sessionId,保存映射关系
if (sessionId && newThreadId) {
this.sessions.set(sessionId, {
threadId: newThreadId,
lastActivity: Date.now(),
});
console.error(`已保存新会话映射: ${sessionId} -> ${newThreadId}`);
} else if (sessionId && this.sessions.has(sessionId)) {
this.sessions.get(sessionId)!.lastActivity = Date.now();
}
const responseText = text || "Codex 完成了执行但未返回文本响应";
console.error(`Codex 响应长度: ${responseText.length} 字符`);
return responseText;
} catch (error) {
console.error("审查代码时出错:", error);
throw error;
}
}
/**
* 审查整个项目的方法
*/
private async reviewProject(
projectPath: string,
changeDescription: string,
sessionId?: string
): Promise<string> {
const prompt = `请审查位于以下路径的项目:${projectPath}\n\n我刚刚修改了代码,以下是变更详情:\n${changeDescription}\n\n请分析这些变更是否正确,是否存在潜在 Bug,并根据需要执行修复或提供建议。`;
const context = `项目路径: ${projectPath}\n变更描述: ${changeDescription}`;
// 复用 reviewCodeChanges 发送请求
// 注意:这里我们将 prompt 作为 content 发送
return await this.reviewCodeChanges(prompt, sessionId, context);
}
/**
* 审查文件的方法
*/
private async reviewFile(
filePath: string,
changeDescription: string,
sessionId?: string
): Promise<string> {
try {
const fs = await import("fs/promises");
const path = await import("path");
console.error(`正在读取文件: ${filePath}`);
const content = await fs.readFile(filePath, "utf-8");
const fileName = path.basename(filePath);
const context = `文件路径: ${filePath}\n文件名: ${fileName}\n变更描述: ${changeDescription}`;
return await this.reviewCodeChanges(content, sessionId, context);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "未知错误";
throw new Error(`无法读取文件 ${filePath}: ${errorMessage}`);
}
}
/**
* 在现有会话中继续讨论
*/
private async continueReview(
message: string,
sessionId: string
): Promise<string> {
try {
if (!this.sessions.has(sessionId)) {
throw new Error(`会话 ${sessionId} 不存在。请先使用 review_code_changes 创建会话。`);
}
const threadId = this.sessions.get(sessionId)!.threadId;
console.error(`在会话 ${sessionId} (${threadId}) 中继续讨论...`);
// 运行 Codex CLI
const { text } = await this.client.run(message, threadId);
this.sessions.get(sessionId)!.lastActivity = Date.now();
const responseText = text || "Codex 完成了处理但未返回文本响应";
console.error(`Codex 响应长度: ${responseText.length} 字符`);
return responseText;
} catch (error) {
console.error("继续审查时出错:", error);
throw error;
}
}
/**
* 启动服务器
*/
async start(): Promise<void> {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error("Codex MCP Server 已启动 (CLI Mode)");
}
}
// 主函数
async function main() {
try {
const server = new CodexMCPServer();
await server.start();
} catch (error) {
console.error("启动服务器失败:", error);
process.exit(1);
}
}
main();