claude-codex-bridge 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,146 @@
1
+ # claude-codex-bridge
2
+
3
+ [![npm version](https://img.shields.io/npm/v/claude-codex-bridge?color=f97316)](https://www.npmjs.com/package/claude-codex-bridge)
4
+ [![npm downloads](https://img.shields.io/npm/dm/claude-codex-bridge?color=3b82f6)](https://www.npmjs.com/package/claude-codex-bridge)
5
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](./LICENSE)
6
+ [![Node.js](https://img.shields.io/badge/node-%3E%3D18-brightgreen)](https://nodejs.org)
7
+
8
+ Bidirectional [MCP](https://modelcontextprotocol.io/) server bridge between **Claude Code** and **OpenAI Codex CLI**.
9
+
10
+ Let Claude and Codex work as partners — each can ask the other for help, review code, explain logic, and plan performance improvements.
11
+
12
+ ```
13
+ ┌──────────────┐ MCP ┌──────────────┐
14
+ │ │ ◄──────────────────► │ │
15
+ │ Claude Code │ claude-codex │ Codex CLI │
16
+ │ │ ◄───── bridge ─────► │ │
17
+ └──────────────┘ └──────────────┘
18
+ ```
19
+
20
+ ## Prerequisites
21
+
22
+ - [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code) — installed and authenticated
23
+ - [Codex CLI](https://developers.openai.com/codex/cli/) — installed and authenticated
24
+ - [Node.js](https://nodejs.org) >= 18
25
+
26
+ ## Quick Start
27
+
28
+ No install needed — use directly via `npx`:
29
+
30
+ ```bash
31
+ npx -p claude-codex-bridge ccb-codex # Start the Codex server
32
+ npx -p claude-codex-bridge ccb-claude # Start the Claude server
33
+ ```
34
+
35
+ ## Setup
36
+
37
+ ### Automatic (recommended)
38
+
39
+ Run `/setup` inside Claude Code to automatically configure both directions:
40
+
41
+ ```
42
+ /setup # Set up both Claude Code and Codex
43
+ /setup claude # Only set up Claude Code → Codex
44
+ /setup codex # Only set up Codex → Claude
45
+ ```
46
+
47
+ Or, if you don't have the repo cloned, just ask Claude Code:
48
+
49
+ > Please help me set up claude-codex-bridge MCP for both Claude Code and Codex according to https://github.com/Dunqing/claude-codex-bridge/blob/main/.claude/skills/setup/SKILL.md
50
+
51
+ ### Manual
52
+
53
+ <details>
54
+ <summary><strong>Claude Code → Codex</strong> (let Claude call Codex)</summary>
55
+
56
+ Add to your Claude Code MCP config:
57
+
58
+ ```bash
59
+ claude mcp add codex -- npx -p claude-codex-bridge ccb-codex
60
+ ```
61
+
62
+ Or add to `.mcp.json` in your project:
63
+
64
+ ```json
65
+ {
66
+ "mcpServers": {
67
+ "codex": {
68
+ "type": "stdio",
69
+ "command": "npx",
70
+ "args": ["-p", "claude-codex-bridge", "ccb-codex"]
71
+ }
72
+ }
73
+ }
74
+ ```
75
+
76
+ </details>
77
+
78
+ <details>
79
+ <summary><strong>Codex → Claude</strong> (let Codex call Claude)</summary>
80
+
81
+ Add to `~/.codex/config.toml`:
82
+
83
+ ```toml
84
+ [mcp_servers.claude]
85
+ command = "npx"
86
+ args = ["-p", "claude-codex-bridge", "ccb-claude"]
87
+ tool_timeout_sec = 300
88
+ ```
89
+
90
+ </details>
91
+
92
+ ## Tools
93
+
94
+ ### `ccb-codex` — Claude calls Codex
95
+
96
+ | Tool | Description |
97
+ |------|-------------|
98
+ | `codex_query` | Ask Codex a question or give it a task |
99
+ | `codex_review_code` | Ask Codex to review code changes |
100
+ | `codex_review_plan` | Ask Codex to critique an implementation plan |
101
+ | `codex_explain_code` | Ask Codex to explain code / logic / architecture |
102
+ | `codex_plan_perf` | Ask Codex to plan performance improvements |
103
+ | `codex_implement` | Ask Codex to write or modify code |
104
+
105
+ ### `ccb-claude` — Codex calls Claude
106
+
107
+ | Tool | Description |
108
+ |------|-------------|
109
+ | `claude_query` | Ask Claude a question or give it a task |
110
+ | `claude_review_code` | Ask Claude to review code changes |
111
+ | `claude_review_plan` | Ask Claude to critique an implementation plan |
112
+ | `claude_explain_code` | Ask Claude to explain code / logic / architecture |
113
+ | `claude_plan_perf` | Ask Claude to plan performance improvements |
114
+ | `claude_implement` | Ask Claude to write or modify code |
115
+
116
+ ## Configuration
117
+
118
+ | Variable | Description | Default |
119
+ |----------|-------------|---------|
120
+ | `BRIDGE_TIMEOUT_MS` | Subprocess timeout in milliseconds | `300000` (5 min) |
121
+ | `BRIDGE_DEBUG` | Enable debug logging to stderr | — |
122
+ | `BRIDGE_DEPTH` | Current recursion depth (set automatically) | `0` |
123
+
124
+ ### Anti-Recursion Guard
125
+
126
+ The bridge automatically prevents infinite loops. If Claude calls Codex which tries to call Claude again, the second call is blocked (`BRIDGE_DEPTH >= 2`).
127
+
128
+ ## Development
129
+
130
+ ```bash
131
+ git clone https://github.com/Dunqing/claude-codex-bridge.git
132
+ cd claude-codex-bridge
133
+ pnpm install
134
+
135
+ pnpm build # Compile to dist/
136
+ pnpm test # Run tests
137
+ pnpm lint # Lint and type check
138
+
139
+ # Dev mode (no build needed)
140
+ pnpm dev:codex-server
141
+ pnpm dev:claude-server
142
+ ```
143
+
144
+ ## License
145
+
146
+ [MIT](./LICENSE)
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1,250 @@
1
+ #!/usr/bin/env node
2
+ import { i as logger, n as buildPlanPerfPrompt, r as execCommand, t as buildExplainCodePrompt } from "./prompt-builder-CQ2IGDTq.mjs";
3
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+ import { z } from "zod";
6
+
7
+ //#region src/lib/claude-output-parser.ts
8
+ /**
9
+ * Parses the JSON output from `claude -p --output-format json`.
10
+ * The output is a single JSON object with a `result` field containing
11
+ * the response content.
12
+ */
13
+ function parseClaudeOutput(jsonOutput) {
14
+ const result = {
15
+ resultText: "",
16
+ sessionId: null,
17
+ costUsd: null,
18
+ errors: []
19
+ };
20
+ const trimmed = jsonOutput.trim();
21
+ if (!trimmed) {
22
+ result.errors.push("Empty output from Claude CLI");
23
+ return result;
24
+ }
25
+ let parsed;
26
+ try {
27
+ parsed = JSON.parse(trimmed);
28
+ } catch {
29
+ logger.debug("Failed to parse Claude output as JSON, using raw text");
30
+ result.resultText = trimmed;
31
+ return result;
32
+ }
33
+ const resultField = parsed["result"];
34
+ if (typeof resultField === "string") result.resultText = resultField;
35
+ else if (resultField && typeof resultField === "object") {
36
+ const content = resultField["content"];
37
+ if (Array.isArray(content)) result.resultText = content.filter((c) => c["type"] === "text").map((c) => c["text"]).join("\n");
38
+ }
39
+ if (!result.resultText) {
40
+ const message = parsed["message"];
41
+ const text = parsed["text"];
42
+ const output = parsed["output"];
43
+ result.resultText = message ?? text ?? output ?? "";
44
+ }
45
+ if (!result.resultText && Object.keys(parsed).length > 0) result.resultText = JSON.stringify(parsed, null, 2);
46
+ result.sessionId = parsed["session_id"] ?? parsed["sessionId"] ?? null;
47
+ result.costUsd = parsed["cost_usd"] ?? parsed["costUsd"] ?? null;
48
+ const error = parsed["error"];
49
+ if (error) {
50
+ const msg = typeof error === "string" ? error : error["message"] ?? JSON.stringify(error);
51
+ result.errors.push(msg);
52
+ }
53
+ return result;
54
+ }
55
+
56
+ //#endregion
57
+ //#region src/claude-server.ts
58
+ const server = new McpServer({
59
+ name: "claude-bridge",
60
+ version: "0.1.0"
61
+ });
62
+ async function runClaude(prompt, options = {}) {
63
+ const args = [
64
+ "-p",
65
+ "--output-format",
66
+ "json"
67
+ ];
68
+ if (options.model) args.push("--model", options.model);
69
+ if (options.maxTurns) args.push("--max-turns", String(options.maxTurns));
70
+ if (options.allowedTools && options.allowedTools.length > 0) for (const tool of options.allowedTools) args.push("--allowedTools", tool);
71
+ args.push(prompt);
72
+ const result = await execCommand({
73
+ command: "claude",
74
+ args,
75
+ cwd: options.workingDirectory
76
+ });
77
+ if (result.timedOut) return {
78
+ resultText: "",
79
+ sessionId: null,
80
+ costUsd: null,
81
+ errors: ["Claude timed out. Increase BRIDGE_TIMEOUT_MS if needed."]
82
+ };
83
+ const parsed = parseClaudeOutput(result.stdout);
84
+ if (result.exitCode !== 0 && !parsed.resultText) {
85
+ const stderr = result.stderr.toLowerCase();
86
+ if (stderr.includes("api key") || stderr.includes("authentication") || stderr.includes("unauthorized")) parsed.errors.push("Claude API key issue. Ensure ANTHROPIC_API_KEY is set.");
87
+ else if (result.stderr.trim()) parsed.errors.push(result.stderr.trim());
88
+ }
89
+ return parsed;
90
+ }
91
+ function formatClaudeResponse(parsed) {
92
+ if (parsed.errors.length > 0 && !parsed.resultText) return {
93
+ content: [{
94
+ type: "text",
95
+ text: `Error: ${parsed.errors.join("; ")}`
96
+ }],
97
+ isError: true
98
+ };
99
+ return { content: [{
100
+ type: "text",
101
+ text: parsed.resultText
102
+ }] };
103
+ }
104
+ const READ_ONLY_TOOLS = [
105
+ "Read",
106
+ "Grep",
107
+ "Glob",
108
+ "Bash(git diff *)",
109
+ "Bash(git log *)",
110
+ "Bash(git show *)"
111
+ ];
112
+ server.registerTool("claude_query", {
113
+ title: "Ask Claude",
114
+ description: "Ask Claude Code a question or give it a task. Claude will use its full toolset (file reading, code search, web search, etc.) to answer.",
115
+ inputSchema: {
116
+ prompt: z.string().describe("The question or task for Claude"),
117
+ workingDirectory: z.string().optional().describe("Working directory (defaults to server cwd)"),
118
+ model: z.string().optional().describe("Model alias: sonnet, opus, or haiku"),
119
+ maxTurns: z.number().optional().default(10).describe("Maximum agentic turns (limits runtime)"),
120
+ allowedTools: z.array(z.string()).optional().describe("Restrict tools (e.g., [\"Read\", \"Grep\", \"Glob\"])")
121
+ }
122
+ }, async ({ prompt, workingDirectory, model, maxTurns, allowedTools }) => {
123
+ return formatClaudeResponse(await runClaude(prompt, {
124
+ workingDirectory,
125
+ model,
126
+ maxTurns,
127
+ allowedTools
128
+ }));
129
+ });
130
+ server.registerTool("claude_review_code", {
131
+ title: "Claude Code Review",
132
+ description: "Ask Claude to review code for quality, bugs, security issues, and best practices. Provide a git diff range, file paths, or code snippet.",
133
+ inputSchema: {
134
+ target: z.string().describe("What to review: git diff range, file paths, or code snippet"),
135
+ focusAreas: z.string().optional().describe("Focus on: bugs, performance, style, security, etc."),
136
+ context: z.string().optional().describe("Additional context about the changes"),
137
+ workingDirectory: z.string().optional(),
138
+ maxTurns: z.number().optional().default(5)
139
+ }
140
+ }, async ({ target, focusAreas, context, workingDirectory, maxTurns }) => {
141
+ let prompt = `Review the following code changes. Provide specific, actionable feedback with line references.\n\nTarget: ${target}`;
142
+ if (focusAreas) prompt += `\n\nFocus areas: ${focusAreas}`;
143
+ if (context) prompt += `\n\nContext: ${context}`;
144
+ return formatClaudeResponse(await runClaude(prompt, {
145
+ workingDirectory,
146
+ maxTurns,
147
+ allowedTools: READ_ONLY_TOOLS
148
+ }));
149
+ });
150
+ server.registerTool("claude_review_plan", {
151
+ title: "Claude Plan Review",
152
+ description: "Ask Claude to critique an implementation plan. Claude will examine the actual codebase to validate feasibility and consistency with existing patterns.",
153
+ inputSchema: {
154
+ plan: z.string().describe("The implementation plan to review"),
155
+ codebasePath: z.string().optional().describe("Path to relevant codebase for context"),
156
+ constraints: z.string().optional().describe("Known constraints"),
157
+ workingDirectory: z.string().optional(),
158
+ maxTurns: z.number().optional().default(8)
159
+ }
160
+ }, async ({ plan, codebasePath, constraints, workingDirectory, maxTurns }) => {
161
+ let prompt = `Critique this implementation plan. Evaluate feasibility against the actual codebase, check consistency with existing patterns, identify gaps and risks.\n\nPlan:\n${plan}`;
162
+ if (codebasePath) prompt += `\n\nRelevant codebase: ${codebasePath}`;
163
+ if (constraints) prompt += `\n\nConstraints: ${constraints}`;
164
+ return formatClaudeResponse(await runClaude(prompt, {
165
+ workingDirectory,
166
+ maxTurns,
167
+ allowedTools: READ_ONLY_TOOLS
168
+ }));
169
+ });
170
+ server.registerTool("claude_explain_code", {
171
+ title: "Claude Explain Code",
172
+ description: "Ask Claude to deeply explain code, logic, or architecture. Claude will read the actual source files to give grounded explanations.",
173
+ inputSchema: {
174
+ target: z.string().describe("What to explain: file path, function name, module, or code snippet"),
175
+ depth: z.enum([
176
+ "overview",
177
+ "detailed",
178
+ "trace"
179
+ ]).optional().default("detailed").describe("Depth: overview, detailed, or full execution trace"),
180
+ context: z.string().optional().describe("Additional context about the codebase"),
181
+ workingDirectory: z.string().optional(),
182
+ maxTurns: z.number().optional().default(8)
183
+ }
184
+ }, async ({ target, depth, context, workingDirectory, maxTurns }) => {
185
+ return formatClaudeResponse(await runClaude(buildExplainCodePrompt({
186
+ target,
187
+ depth,
188
+ context
189
+ }), {
190
+ workingDirectory,
191
+ maxTurns,
192
+ allowedTools: READ_ONLY_TOOLS
193
+ }));
194
+ });
195
+ server.registerTool("claude_plan_perf", {
196
+ title: "Claude Performance Plan",
197
+ description: "Ask Claude to analyze performance and create an improvement plan. Claude reads the actual code to identify bottlenecks and propose optimizations.",
198
+ inputSchema: {
199
+ target: z.string().describe("What to optimize: function, module, or pipeline path"),
200
+ metrics: z.array(z.enum([
201
+ "latency",
202
+ "throughput",
203
+ "memory",
204
+ "binary-size"
205
+ ])).optional().describe("Performance metrics to focus on"),
206
+ constraints: z.string().optional().describe("Constraints"),
207
+ context: z.string().optional().describe("Additional context about usage patterns"),
208
+ workingDirectory: z.string().optional(),
209
+ maxTurns: z.number().optional().default(10)
210
+ }
211
+ }, async ({ target, metrics, constraints, context, workingDirectory, maxTurns }) => {
212
+ return formatClaudeResponse(await runClaude(buildPlanPerfPrompt({
213
+ target,
214
+ metrics,
215
+ constraints,
216
+ context
217
+ }), {
218
+ workingDirectory,
219
+ maxTurns,
220
+ allowedTools: READ_ONLY_TOOLS
221
+ }));
222
+ });
223
+ server.registerTool("claude_implement", {
224
+ title: "Claude Implement",
225
+ description: "Ask Claude to implement a feature, fix a bug, or make code changes. WARNING: This modifies your codebase.",
226
+ inputSchema: {
227
+ task: z.string().describe("What to implement or fix"),
228
+ workingDirectory: z.string().optional(),
229
+ model: z.string().optional(),
230
+ maxTurns: z.number().optional().default(15)
231
+ }
232
+ }, async ({ task, workingDirectory, model, maxTurns }) => {
233
+ return formatClaudeResponse(await runClaude(task, {
234
+ workingDirectory,
235
+ model,
236
+ maxTurns
237
+ }));
238
+ });
239
+ async function main() {
240
+ const transport = new StdioServerTransport();
241
+ await server.connect(transport);
242
+ logger.info("claude-bridge MCP server started on stdio");
243
+ }
244
+ main().catch((err) => {
245
+ logger.error("Failed to start claude-bridge:", err);
246
+ process.exit(1);
247
+ });
248
+
249
+ //#endregion
250
+ export { };
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1,264 @@
1
+ #!/usr/bin/env node
2
+ import { i as logger, n as buildPlanPerfPrompt, r as execCommand, t as buildExplainCodePrompt } from "./prompt-builder-CQ2IGDTq.mjs";
3
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+ import { z } from "zod";
6
+
7
+ //#region src/lib/codex-output-parser.ts
8
+ /**
9
+ * Parses the JSONL output from `codex exec --json`.
10
+ * Each line is a JSON event with a `type` field.
11
+ * We extract the final agent message, file changes, commands, and usage.
12
+ */
13
+ function parseCodexOutput(jsonlOutput) {
14
+ const result = {
15
+ threadId: null,
16
+ agentMessage: "",
17
+ fileChanges: [],
18
+ commandsExecuted: [],
19
+ usage: null,
20
+ errors: []
21
+ };
22
+ const lines = jsonlOutput.split("\n").filter((l) => l.trim().length > 0);
23
+ for (const line of lines) {
24
+ let event;
25
+ try {
26
+ event = JSON.parse(line);
27
+ } catch {
28
+ logger.debug(`Skipping non-JSON line: ${line.slice(0, 100)}`);
29
+ continue;
30
+ }
31
+ const type = event["type"];
32
+ if (!type) continue;
33
+ switch (type) {
34
+ case "thread.started":
35
+ result.threadId = event["threadId"] ?? null;
36
+ break;
37
+ case "item.completed": {
38
+ const itemType = event["itemType"];
39
+ if (itemType === "agent_message" || itemType === "message") {
40
+ const text = event["text"] ?? event["content"] ?? "";
41
+ if (text) result.agentMessage = text;
42
+ } else if (itemType === "file_change") {
43
+ const path = event["path"] ?? "";
44
+ const kind = event["kind"] ?? "update";
45
+ if (path) result.fileChanges.push({
46
+ path,
47
+ kind
48
+ });
49
+ } else if (itemType === "command_execution") result.commandsExecuted.push({
50
+ command: event["command"] ?? "",
51
+ exitCode: event["exitCode"] ?? null,
52
+ output: event["output"] ?? ""
53
+ });
54
+ break;
55
+ }
56
+ case "turn.completed": {
57
+ const usage = event["usage"];
58
+ if (usage) result.usage = {
59
+ inputTokens: usage["input_tokens"] ?? usage["inputTokens"] ?? 0,
60
+ outputTokens: usage["output_tokens"] ?? usage["outputTokens"] ?? 0
61
+ };
62
+ break;
63
+ }
64
+ case "turn.failed":
65
+ case "error": {
66
+ const msg = event["error"] ?? event["message"] ?? JSON.stringify(event);
67
+ result.errors.push(msg);
68
+ break;
69
+ }
70
+ }
71
+ }
72
+ if (!result.agentMessage && jsonlOutput.trim()) result.agentMessage = jsonlOutput.trim();
73
+ return result;
74
+ }
75
+
76
+ //#endregion
77
+ //#region src/codex-server.ts
78
+ const server = new McpServer({
79
+ name: "codex-bridge",
80
+ version: "0.1.0"
81
+ });
82
+ async function runCodex(prompt, options = {}) {
83
+ const args = ["exec", "--json"];
84
+ if (options.model) args.push("--model", options.model);
85
+ if (options.sandbox) args.push("--sandbox", options.sandbox);
86
+ if (options.fullAuto) args.push("--full-auto");
87
+ args.push(prompt);
88
+ const result = await execCommand({
89
+ command: "codex",
90
+ args,
91
+ cwd: options.workingDirectory
92
+ });
93
+ if (result.timedOut) return {
94
+ threadId: null,
95
+ agentMessage: "",
96
+ fileChanges: [],
97
+ commandsExecuted: [],
98
+ usage: null,
99
+ errors: ["Codex timed out. Increase BRIDGE_TIMEOUT_MS if needed."]
100
+ };
101
+ const parsed = parseCodexOutput(result.stdout);
102
+ if (result.exitCode !== 0 && !parsed.agentMessage) {
103
+ const stderr = result.stderr.toLowerCase();
104
+ if (stderr.includes("api key") || stderr.includes("authentication") || stderr.includes("unauthorized")) parsed.errors.push("Codex API key issue. Ensure OPENAI_API_KEY is set.");
105
+ else if (result.stderr.trim()) parsed.errors.push(result.stderr.trim());
106
+ }
107
+ return parsed;
108
+ }
109
+ function formatCodexResponse(parsed) {
110
+ if (parsed.errors.length > 0 && !parsed.agentMessage) return {
111
+ content: [{
112
+ type: "text",
113
+ text: `Error: ${parsed.errors.join("; ")}`
114
+ }],
115
+ isError: true
116
+ };
117
+ let text = parsed.agentMessage;
118
+ if (parsed.fileChanges.length > 0) {
119
+ text += "\n\n**Files changed:**\n";
120
+ for (const fc of parsed.fileChanges) text += `- ${fc.kind}: ${fc.path}\n`;
121
+ }
122
+ if (parsed.commandsExecuted.length > 0) {
123
+ text += "\n\n**Commands executed:**\n";
124
+ for (const cmd of parsed.commandsExecuted) text += `- \`${cmd.command}\` (exit: ${cmd.exitCode})\n`;
125
+ }
126
+ return { content: [{
127
+ type: "text",
128
+ text
129
+ }] };
130
+ }
131
+ server.registerTool("codex_query", {
132
+ title: "Ask Codex",
133
+ description: "Ask OpenAI Codex a question or give it a task. Use for getting a second opinion, exploring unfamiliar code, or tasks that benefit from a different model's perspective.",
134
+ inputSchema: {
135
+ prompt: z.string().describe("The question or task for Codex"),
136
+ workingDirectory: z.string().optional().describe("Working directory (defaults to server cwd)"),
137
+ model: z.string().optional().describe("Override the Codex model (e.g., o4-mini, o3)"),
138
+ sandbox: z.enum([
139
+ "read-only",
140
+ "workspace-write",
141
+ "danger-full-access"
142
+ ]).optional().default("read-only").describe("Sandbox level controlling what Codex can modify")
143
+ }
144
+ }, async ({ prompt, workingDirectory, model, sandbox }) => {
145
+ return formatCodexResponse(await runCodex(prompt, {
146
+ workingDirectory,
147
+ model,
148
+ sandbox
149
+ }));
150
+ });
151
+ server.registerTool("codex_review_code", {
152
+ title: "Codex Code Review",
153
+ description: "Ask Codex to review code. Provide a git diff range, file paths, or a code snippet. Returns specific, actionable feedback.",
154
+ inputSchema: {
155
+ target: z.string().describe("What to review: git diff range (e.g., \"HEAD~3..HEAD\"), file paths, or code snippet"),
156
+ focusAreas: z.string().optional().describe("Focus on: bugs, performance, style, security, etc."),
157
+ context: z.string().optional().describe("Additional context about the codebase or changes"),
158
+ workingDirectory: z.string().optional()
159
+ }
160
+ }, async ({ target, focusAreas, context, workingDirectory }) => {
161
+ let prompt = `Review the following code changes. Provide specific, actionable feedback with line references.\n\nTarget: ${target}`;
162
+ if (focusAreas) prompt += `\n\nFocus areas: ${focusAreas}`;
163
+ if (context) prompt += `\n\nContext: ${context}`;
164
+ return formatCodexResponse(await runCodex(prompt, {
165
+ workingDirectory,
166
+ sandbox: "read-only"
167
+ }));
168
+ });
169
+ server.registerTool("codex_review_plan", {
170
+ title: "Codex Plan Review",
171
+ description: "Ask Codex to critique an implementation plan. Identifies gaps, risks, missing edge cases, and suggests improvements.",
172
+ inputSchema: {
173
+ plan: z.string().describe("The implementation plan or design to review"),
174
+ codebasePath: z.string().optional().describe("Path to relevant codebase for context"),
175
+ constraints: z.string().optional().describe("Known constraints: timeline, tech stack, compatibility"),
176
+ workingDirectory: z.string().optional()
177
+ }
178
+ }, async ({ plan, codebasePath, constraints, workingDirectory }) => {
179
+ let prompt = `Critique this implementation plan. Identify gaps, risks, missing edge cases, and suggest improvements.\n\nPlan:\n${plan}`;
180
+ if (codebasePath) prompt += `\n\nRelevant codebase: ${codebasePath}`;
181
+ if (constraints) prompt += `\n\nConstraints: ${constraints}`;
182
+ return formatCodexResponse(await runCodex(prompt, {
183
+ workingDirectory,
184
+ sandbox: "read-only"
185
+ }));
186
+ });
187
+ server.registerTool("codex_explain_code", {
188
+ title: "Codex Explain Code",
189
+ description: "Ask Codex to deeply explain code, logic, or architecture. Useful for understanding unfamiliar code, onboarding, or documenting complex systems.",
190
+ inputSchema: {
191
+ target: z.string().describe("What to explain: file path, function name, module, or code snippet"),
192
+ depth: z.enum([
193
+ "overview",
194
+ "detailed",
195
+ "trace"
196
+ ]).optional().default("detailed").describe("Depth of explanation: overview, detailed, or full execution trace"),
197
+ context: z.string().optional().describe("Additional context about the codebase"),
198
+ workingDirectory: z.string().optional()
199
+ }
200
+ }, async ({ target, depth, context, workingDirectory }) => {
201
+ return formatCodexResponse(await runCodex(buildExplainCodePrompt({
202
+ target,
203
+ depth,
204
+ context
205
+ }), {
206
+ workingDirectory,
207
+ sandbox: "read-only"
208
+ }));
209
+ });
210
+ server.registerTool("codex_plan_perf", {
211
+ title: "Codex Performance Plan",
212
+ description: "Ask Codex to analyze performance and create an improvement plan. Identifies bottlenecks, proposes ranked optimizations with expected impact.",
213
+ inputSchema: {
214
+ target: z.string().describe("What to optimize: function, module, or pipeline path"),
215
+ metrics: z.array(z.enum([
216
+ "latency",
217
+ "throughput",
218
+ "memory",
219
+ "binary-size"
220
+ ])).optional().describe("Performance metrics to focus on"),
221
+ constraints: z.string().optional().describe("Constraints: must not increase binary size, etc."),
222
+ context: z.string().optional().describe("Additional context about usage patterns"),
223
+ workingDirectory: z.string().optional()
224
+ }
225
+ }, async ({ target, metrics, constraints, context, workingDirectory }) => {
226
+ return formatCodexResponse(await runCodex(buildPlanPerfPrompt({
227
+ target,
228
+ metrics,
229
+ constraints,
230
+ context
231
+ }), {
232
+ workingDirectory,
233
+ sandbox: "read-only"
234
+ }));
235
+ });
236
+ server.registerTool("codex_implement", {
237
+ title: "Codex Implement",
238
+ description: "Ask Codex to implement a feature, fix a bug, or make code changes. WARNING: This modifies your codebase. Returns a summary of what was changed.",
239
+ inputSchema: {
240
+ task: z.string().describe("What to implement or fix"),
241
+ workingDirectory: z.string().optional(),
242
+ model: z.string().optional(),
243
+ sandbox: z.enum(["workspace-write", "danger-full-access"]).optional().default("workspace-write").describe("Sandbox level (must allow writes)")
244
+ }
245
+ }, async ({ task, workingDirectory, model, sandbox }) => {
246
+ return formatCodexResponse(await runCodex(task, {
247
+ workingDirectory,
248
+ model,
249
+ sandbox,
250
+ fullAuto: true
251
+ }));
252
+ });
253
+ async function main() {
254
+ const transport = new StdioServerTransport();
255
+ await server.connect(transport);
256
+ logger.info("codex-bridge MCP server started on stdio");
257
+ }
258
+ main().catch((err) => {
259
+ logger.error("Failed to start codex-bridge:", err);
260
+ process.exit(1);
261
+ });
262
+
263
+ //#endregion
264
+ export { };
@@ -0,0 +1,159 @@
1
+ import { spawn } from "node:child_process";
2
+
3
+ //#region src/lib/errors.ts
4
+ var BridgeError = class extends Error {
5
+ constructor(message, code, details) {
6
+ super(message);
7
+ this.code = code;
8
+ this.details = details;
9
+ this.name = "BridgeError";
10
+ }
11
+ };
12
+
13
+ //#endregion
14
+ //#region src/lib/logger.ts
15
+ const isDebug = !!process.env["BRIDGE_DEBUG"];
16
+ const logger = {
17
+ info(msg, ...args) {
18
+ console.error(`[INFO] ${msg}`, ...args);
19
+ },
20
+ warn(msg, ...args) {
21
+ console.error(`[WARN] ${msg}`, ...args);
22
+ },
23
+ error(msg, ...args) {
24
+ console.error(`[ERROR] ${msg}`, ...args);
25
+ },
26
+ debug(msg, ...args) {
27
+ if (isDebug) console.error(`[DEBUG] ${msg}`, ...args);
28
+ }
29
+ };
30
+
31
+ //#endregion
32
+ //#region src/lib/exec-runner.ts
33
+ const DEFAULT_TIMEOUT_MS = 3e5;
34
+ const MAX_BRIDGE_DEPTH = 2;
35
+ function getTimeoutMs() {
36
+ const env = process.env["BRIDGE_TIMEOUT_MS"];
37
+ if (env) {
38
+ const parsed = parseInt(env, 10);
39
+ if (!Number.isNaN(parsed) && parsed > 0) return parsed;
40
+ }
41
+ return DEFAULT_TIMEOUT_MS;
42
+ }
43
+ function checkRecursionDepth() {
44
+ const depth = parseInt(process.env["BRIDGE_DEPTH"] ?? "0", 10);
45
+ if (depth >= MAX_BRIDGE_DEPTH) throw new BridgeError(`Maximum bridge nesting depth reached (${depth} >= ${MAX_BRIDGE_DEPTH}). This prevents infinite recursion between Claude and Codex.`, "RECURSION_LIMIT");
46
+ }
47
+ function execCommand(options) {
48
+ checkRecursionDepth();
49
+ const timeoutMs = options.timeoutMs ?? getTimeoutMs();
50
+ const currentDepth = parseInt(process.env["BRIDGE_DEPTH"] ?? "0", 10);
51
+ const env = {
52
+ ...process.env,
53
+ ...options.env,
54
+ BRIDGE_DEPTH: String(currentDepth + 1)
55
+ };
56
+ return new Promise((resolve, reject) => {
57
+ logger.debug(`exec: ${options.command} ${options.args.join(" ")} (cwd: ${options.cwd ?? process.cwd()}, timeout: ${timeoutMs}ms)`);
58
+ let child;
59
+ try {
60
+ child = spawn(options.command, options.args, {
61
+ cwd: options.cwd ?? process.cwd(),
62
+ env,
63
+ stdio: [
64
+ "ignore",
65
+ "pipe",
66
+ "pipe"
67
+ ]
68
+ });
69
+ } catch (err) {
70
+ reject(new BridgeError(`Failed to spawn "${options.command}": ${err instanceof Error ? err.message : String(err)}`, "CLI_NOT_FOUND"));
71
+ return;
72
+ }
73
+ const stdoutChunks = [];
74
+ const stderrChunks = [];
75
+ let timedOut = false;
76
+ let settled = false;
77
+ child.stdout.on("data", (chunk) => stdoutChunks.push(chunk));
78
+ child.stderr.on("data", (chunk) => stderrChunks.push(chunk));
79
+ const timer = setTimeout(() => {
80
+ timedOut = true;
81
+ logger.warn(`Process timed out after ${timeoutMs}ms, sending SIGTERM`);
82
+ child.kill("SIGTERM");
83
+ setTimeout(() => {
84
+ if (!settled) {
85
+ logger.warn("Process did not exit after SIGTERM, sending SIGKILL");
86
+ child.kill("SIGKILL");
87
+ }
88
+ }, 5e3);
89
+ }, timeoutMs);
90
+ child.on("error", (err) => {
91
+ clearTimeout(timer);
92
+ settled = true;
93
+ if (err.code === "ENOENT") reject(new BridgeError(`"${options.command}" not found. Is it installed and on your PATH?`, "CLI_NOT_FOUND"));
94
+ else reject(new BridgeError(`Process error: ${err.message}`, "PROCESS_ERROR", err.code));
95
+ });
96
+ child.on("close", (code) => {
97
+ clearTimeout(timer);
98
+ settled = true;
99
+ resolve({
100
+ exitCode: code ?? 1,
101
+ stdout: Buffer.concat(stdoutChunks).toString("utf-8"),
102
+ stderr: Buffer.concat(stderrChunks).toString("utf-8"),
103
+ timedOut
104
+ });
105
+ });
106
+ });
107
+ }
108
+
109
+ //#endregion
110
+ //#region src/lib/prompt-builder.ts
111
+ /**
112
+ * Builds a prompt for deep code/logic explanation.
113
+ */
114
+ function buildExplainCodePrompt(options) {
115
+ const depth = options.depth ?? "detailed";
116
+ let prompt = `Explain the following code/module/function in depth.\n\nTarget: ${options.target}\n\n${{
117
+ overview: "Provide a high-level overview: what the code does, its role in the system, and key abstractions. Keep it concise.",
118
+ detailed: "Provide a detailed explanation: purpose, control flow, data flow, key design decisions, edge cases, and how it interacts with surrounding code.",
119
+ trace: "Provide a full execution trace: step through the code path, explain each branch, data transformation, and side effect. Include call chains and state mutations."
120
+ }[depth]}`;
121
+ if (options.context) prompt += `\n\nAdditional context: ${options.context}`;
122
+ prompt += `\n\nStructure your response with:
123
+ 1. **Purpose** - What this code does and why it exists
124
+ 2. **Key Components** - Main functions, types, data structures
125
+ 3. **Control Flow** - How execution proceeds
126
+ 4. **Data Flow** - How data is transformed and passed
127
+ 5. **Design Decisions** - Why it's structured this way
128
+ 6. **Dependencies** - What it depends on and what depends on it`;
129
+ return prompt;
130
+ }
131
+ /**
132
+ * Builds a prompt for planning performance improvements.
133
+ */
134
+ function buildPlanPerfPrompt(options) {
135
+ const metricsList = (options.metrics ?? ["latency", "memory"]).join(", ");
136
+ let prompt = `Analyze the performance of the following code and create a concrete improvement plan.
137
+
138
+ Target: ${options.target}
139
+
140
+ Focus metrics: ${metricsList}
141
+
142
+ Perform the following analysis:
143
+ 1. **Current State** - Read and understand the target code
144
+ 2. **Hot Path Analysis** - Identify the critical execution path and where time/memory is spent
145
+ 3. **Bottleneck Identification** - List specific bottlenecks with evidence (e.g., unnecessary allocations, redundant computations, cache misses, algorithmic complexity)
146
+ 4. **Optimization Plan** - For each bottleneck, propose a ranked optimization with:
147
+ - Description of the change
148
+ - Expected impact (quantified if possible)
149
+ - Implementation difficulty (low/medium/high)
150
+ - Any correctness risks or trade-offs
151
+ 5. **Implementation Order** - Recommend which optimizations to apply first (highest impact, lowest risk)
152
+ 6. **Measurement Plan** - How to verify each optimization works (benchmarks, profiling commands)`;
153
+ if (options.constraints) prompt += `\n\nConstraints: ${options.constraints}`;
154
+ if (options.context) prompt += `\n\nAdditional context: ${options.context}`;
155
+ return prompt;
156
+ }
157
+
158
+ //#endregion
159
+ export { logger as i, buildPlanPerfPrompt as n, execCommand as r, buildExplainCodePrompt as t };
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "claude-codex-bridge",
3
+ "version": "0.1.0",
4
+ "description": "Bidirectional MCP server bridge between Claude Code and Codex CLI",
5
+ "type": "module",
6
+ "bin": {
7
+ "ccb-codex": "./dist/codex-server.mjs",
8
+ "ccb-claude": "./dist/claude-server.mjs"
9
+ },
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "keywords": [
14
+ "mcp",
15
+ "claude",
16
+ "codex",
17
+ "bridge",
18
+ "ai"
19
+ ],
20
+ "license": "MIT",
21
+ "engines": {
22
+ "node": ">=18"
23
+ },
24
+ "dependencies": {
25
+ "@modelcontextprotocol/sdk": "^1.26.0",
26
+ "zod": "^4.3.6"
27
+ },
28
+ "devDependencies": {
29
+ "@types/node": "^25.2.3",
30
+ "bumpp": "^10.4.1",
31
+ "husky": "^9.1.7",
32
+ "oxfmt": "^0.28.0",
33
+ "oxlint": "^1.43.0",
34
+ "publint": "^0.3.17",
35
+ "tsdown": "^0.20.3",
36
+ "tsx": "^4.21.0",
37
+ "typescript": "^5.9.3",
38
+ "vitest": "^4.0.18"
39
+ },
40
+ "scripts": {
41
+ "build": "tsdown",
42
+ "dev:codex-server": "tsx src/codex-server.ts",
43
+ "dev:claude-server": "tsx src/claude-server.ts",
44
+ "test": "vitest run",
45
+ "test:watch": "vitest",
46
+ "lint": "oxlint src/ test/ --type-check",
47
+ "fmt": "oxfmt --write src/ test/",
48
+ "fmt:check": "oxfmt --check src/ test/",
49
+ "publint": "publint",
50
+ "release": "bumpp"
51
+ }
52
+ }