mcp-codex-cli
Version:
MCP server wrapper for OpenAI's Codex CLI
179 lines • 6.88 kB
JavaScript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { spawn } from "node:child_process";
import { z } from "zod";
// Default model for Codex CLI
const DEFAULT_MODEL = "gpt-5";
// Global flag to track --allow-npx option
let ALLOW_NPX = false;
// Function to determine the codex-cli command and its initial arguments
export async function decideCodexCliCommand(allowNpx = false) {
return new Promise((resolve, reject) => {
const isWindows = process.platform === "win32";
const whichCmd = isWindows ? "where" : "which";
const child = spawn(whichCmd, ["codex"]);
child.on("close", (code) => {
if (code === 0) {
resolve({ command: "codex", initialArgs: [] });
}
else if (allowNpx) {
// Use npx to run codex CLI when not found locally
resolve({
command: "npx",
initialArgs: ["@openai/codex"],
});
}
else {
reject(new Error("codex not found globally and --allow-npx option not specified."));
}
});
child.on("error", (err) => {
reject(err);
});
});
}
// Function to execute codex-cli command
export async function executeCodexCli(codexCliCommand, args) {
const { command, initialArgs } = codexCliCommand;
// Note: codex-not-found case is no longer used since we now use npx directly
const commandArgs = [...initialArgs, ...args];
return new Promise((resolve, reject) => {
const child = spawn(command, commandArgs, {
stdio: ["pipe", "pipe", "pipe"],
});
let stdout = "";
let stderr = "";
// Close stdin immediately since we're not sending any input
child.stdin.end();
child.stdout.on("data", (data) => {
stdout += data.toString();
});
child.stderr.on("data", (data) => {
stderr += data.toString();
});
child.on("close", (code) => {
if (code === 0) {
resolve(stdout);
}
else {
reject(new Error(`codex exited with code ${code}: ${stderr}`));
}
});
child.on("error", (err) => {
reject(err);
});
});
}
// Zod schema for chat tool parameters
export const ChatParametersSchema = z.object({
prompt: z.string().describe("The task description to execute."),
approvalLevel: z
.string()
.optional()
.describe("Approval level: auto-edit (read/write files, requires approval for commands), full-auto (fully autonomous). Default: auto-edit."),
model: z
.string()
.optional()
.describe(`The model to use. Default: "${DEFAULT_MODEL}". Other options: "gpt-4.1", "gpt-4.1-mini", "o1", "o1-mini", or use --oss for open source models.`),
workingDir: z
.string()
.optional()
.describe("The working directory for the task execution."),
});
// Tool execution functions
export async function chat(args) {
const parsedArgs = ChatParametersSchema.parse(args);
// Validate approvalLevel if provided
if (parsedArgs.approvalLevel &&
!["auto-edit", "full-auto"].includes(parsedArgs.approvalLevel)) {
throw new Error(`Invalid approvalLevel: ${parsedArgs.approvalLevel}. Must be "auto-edit" or "full-auto".`);
}
// Use the same allowNpx setting as startup
const codexCliCmd = await decideCodexCliCommand(ALLOW_NPX);
const cliArgs = [];
// Set model (default to gpt-5)
const model = parsedArgs.model || DEFAULT_MODEL;
cliArgs.push("-m", model);
// Set approval level (default to auto-edit for MCP compatibility)
const approvalLevel = parsedArgs.approvalLevel || "auto-edit";
switch (approvalLevel) {
case "auto-edit":
cliArgs.push("-a", "on-failure");
cliArgs.push("-s", "workspace-write");
break;
case "full-auto":
cliArgs.push("--full-auto");
break;
}
// Always use exec mode for non-interactive execution
cliArgs.push("exec");
// Add skip git repo check for untrusted directories (needed for npx execution)
cliArgs.push("--skip-git-repo-check");
// Add the prompt at the end
cliArgs.push(parsedArgs.prompt);
// Set working directory if specified
if (parsedArgs.workingDir) {
process.chdir(parsedArgs.workingDir);
}
const result = await executeCodexCli(codexCliCmd, cliArgs);
return result;
}
async function main() {
// Check for --allow-npx argument
ALLOW_NPX = process.argv.includes("--allow-npx");
// Check if codex-cli is available at startup
try {
await decideCodexCliCommand(ALLOW_NPX);
}
catch (error) {
console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
console.error("Please install codex-cli globally using: npm install -g @openai/codex");
process.exit(1);
}
const server = new McpServer({
name: "mcp-codex-cli",
version: "1.0.0",
});
// Register chat tool
server.registerTool("chat", {
description: "Chat with Codex CLI in non-interactive mode. Can perform code generation, refactoring, and various development tasks.",
inputSchema: {
prompt: z.string().describe("The task description to execute."),
approvalLevel: z
.string()
.optional()
.describe("Approval level: auto-edit (read/write files, requires approval for commands), full-auto (fully autonomous). Default: auto-edit."),
model: z
.string()
.optional()
.describe(`The model to use. Default: "${DEFAULT_MODEL}". Other options: "gpt-4.1", "gpt-4.1-mini", "o1", "o1-mini".`),
workingDir: z
.string()
.optional()
.describe("The working directory for the task execution."),
},
}, async (args) => {
const result = await chat(args);
return {
content: [
{
type: "text",
text: result,
},
],
};
});
// Connect the server to stdio transport
const transport = new StdioServerTransport();
await server.connect(transport);
}
// Only run main if this file is being executed directly
// For NPX execution, import.meta.main might be undefined, so we'll always run main
if (import.meta.main !== false) {
main().catch((error) => {
console.error("Fatal error:", error);
process.exit(1);
});
}
//# sourceMappingURL=index.js.map