@dhrupo/codex-claude-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 +160 -0
- package/bin/codex-claude-mcp.mjs +8 -0
- package/bin/codex-claude.mjs +8 -0
- package/package.json +30 -0
- package/src/claude-cli.mjs +102 -0
- package/src/codex-claude-command.mjs +145 -0
- package/src/codex-cli.mjs +78 -0
- package/src/constants.mjs +3 -0
- package/src/schema.mjs +87 -0
- package/src/server.mjs +97 -0
- package/src/tools.mjs +79 -0
- package/tests/claude-cli.test.mjs +57 -0
- package/tests/codex-cli.test.mjs +52 -0
- package/tests/server.test.mjs +12 -0
- package/tests/tools.test.mjs +19 -0
package/README.md
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# codex-claude-bridge
|
|
2
|
+
|
|
3
|
+
Use Claude Code from inside Codex.
|
|
4
|
+
|
|
5
|
+
This package exposes Claude Code as MCP tools that Codex can call, and also ships a small `codex-claude` command for common flows.
|
|
6
|
+
|
|
7
|
+
## What You Get
|
|
8
|
+
|
|
9
|
+
- `codex-claude-mcp`: an MCP server that wraps the local `claude` CLI
|
|
10
|
+
- `codex-claude`: a helper command that drives Codex to call the Claude MCP tools
|
|
11
|
+
- Three MCP tools:
|
|
12
|
+
- `claude_ask`
|
|
13
|
+
- `claude_review`
|
|
14
|
+
- `claude_delegate`
|
|
15
|
+
|
|
16
|
+
## Requirements
|
|
17
|
+
|
|
18
|
+
- Node.js `>=18.18.0`
|
|
19
|
+
- `codex` installed and authenticated
|
|
20
|
+
- `claude` installed and authenticated
|
|
21
|
+
|
|
22
|
+
Check both CLIs first:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
codex --version
|
|
26
|
+
claude --version
|
|
27
|
+
claude auth status
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
Install the package:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npm install -g @dhrupo/codex-claude-bridge
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Register the MCP server in Codex:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
codex-claude install
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
That adds a global Codex MCP server named `claude-bridge`.
|
|
45
|
+
|
|
46
|
+
You can also register it manually:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
codex mcp add claude-bridge -- codex-claude-mcp
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Or without a global npm install:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
codex mcp add claude-bridge -- npx -y @dhrupo/codex-claude-bridge
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Confirm registration:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
codex mcp list
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Usage
|
|
65
|
+
|
|
66
|
+
### Run Through The Helper Command
|
|
67
|
+
|
|
68
|
+
Ask Claude a question through Codex:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
codex-claude ask "Summarize the current repository."
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Run a review:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
codex-claude review
|
|
78
|
+
codex-claude review "Review the current workspace for bugs and missing tests."
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Delegate an implementation task:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
codex-claude delegate "Fix the failing tests with the smallest safe patch."
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
If you are outside a git repository, add:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
--skip-git-repo-check
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Example:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
codex-claude ask --cwd /path/to/project --skip-git-repo-check "Reply with exactly OK."
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Run Through Codex Directly
|
|
100
|
+
|
|
101
|
+
Once installed, Codex can call these tools directly:
|
|
102
|
+
|
|
103
|
+
- `claude_ask`: read-only prompting
|
|
104
|
+
- `claude_review`: stricter review pass
|
|
105
|
+
- `claude_delegate`: writable implementation/delegation flow
|
|
106
|
+
|
|
107
|
+
Example `codex exec` prompt:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
codex exec --cd /path/to/project --skip-git-repo-check \
|
|
111
|
+
'Use the MCP tool `claude_ask` exactly once. Return only the tool result. Tool arguments: {"prompt":"Summarize this repo.","cwd":"/path/to/project","outputFormat":"text"}'
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## How It Works
|
|
115
|
+
|
|
116
|
+
- Codex discovers the `claude-bridge` MCP server
|
|
117
|
+
- Codex calls one of the bridge tools
|
|
118
|
+
- The bridge runs the local `claude --print` command
|
|
119
|
+
- The Claude response is returned back through MCP to Codex
|
|
120
|
+
|
|
121
|
+
This is an MCP bridge, not a native Codex slash-command plugin.
|
|
122
|
+
|
|
123
|
+
## Development
|
|
124
|
+
|
|
125
|
+
Install dependencies:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
npm install
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Run tests:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
npm test
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Start the MCP server directly:
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
npm start
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Troubleshooting
|
|
144
|
+
|
|
145
|
+
If the bridge works but Claude returns an auth or access error, test Claude directly first:
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
claude -p --output-format text "Reply with exactly OK."
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
If that fails, fix Claude access before testing through Codex.
|
|
152
|
+
|
|
153
|
+
If Codex is outside a git repo, use `--skip-git-repo-check`.
|
|
154
|
+
|
|
155
|
+
If needed, remove and re-add the MCP server:
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
codex mcp remove claude-bridge
|
|
159
|
+
codex mcp add claude-bridge -- codex-claude-mcp
|
|
160
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dhrupo/codex-claude-bridge",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Expose Claude Code as MCP tools for Codex.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"codex-claude": "./bin/codex-claude.mjs",
|
|
8
|
+
"codex-claude-mcp": "./bin/codex-claude-mcp.mjs"
|
|
9
|
+
},
|
|
10
|
+
"engines": {
|
|
11
|
+
"node": ">=18.18.0"
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"test": "node --test tests/*.test.mjs",
|
|
15
|
+
"start": "node ./bin/codex-claude-mcp.mjs",
|
|
16
|
+
"check": "node --test tests/*.test.mjs"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"codex",
|
|
20
|
+
"claude",
|
|
21
|
+
"mcp",
|
|
22
|
+
"anthropic",
|
|
23
|
+
"openai"
|
|
24
|
+
],
|
|
25
|
+
"license": "Apache-2.0",
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
28
|
+
"zod": "^4.1.12"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
function pushListOption(args, flag, values) {
|
|
4
|
+
if (!Array.isArray(values) || values.length === 0) {
|
|
5
|
+
return;
|
|
6
|
+
}
|
|
7
|
+
args.push(flag, values.join(" "));
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function buildClaudeArgs(input, defaults = {}) {
|
|
11
|
+
const args = ["--print"];
|
|
12
|
+
const outputFormat = input.outputFormat || defaults.outputFormat || "text";
|
|
13
|
+
const permissionMode = input.permissionMode || defaults.permissionMode;
|
|
14
|
+
|
|
15
|
+
args.push("--output-format", outputFormat);
|
|
16
|
+
|
|
17
|
+
if (input.model) {
|
|
18
|
+
args.push("--model", input.model);
|
|
19
|
+
}
|
|
20
|
+
if (permissionMode) {
|
|
21
|
+
args.push("--permission-mode", permissionMode);
|
|
22
|
+
}
|
|
23
|
+
if (typeof input.maxBudgetUsd === "number") {
|
|
24
|
+
args.push("--max-budget-usd", String(input.maxBudgetUsd));
|
|
25
|
+
}
|
|
26
|
+
if (input.appendSystemPrompt) {
|
|
27
|
+
args.push("--append-system-prompt", input.appendSystemPrompt);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
pushListOption(args, "--allowedTools", input.allowedTools);
|
|
31
|
+
pushListOption(args, "--disallowedTools", input.disallowedTools);
|
|
32
|
+
|
|
33
|
+
args.push(input.prompt);
|
|
34
|
+
return args;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function normalizeStdout(stdout, outputFormat) {
|
|
38
|
+
if (outputFormat === "json") {
|
|
39
|
+
try {
|
|
40
|
+
return JSON.stringify(JSON.parse(stdout), null, 2);
|
|
41
|
+
} catch {
|
|
42
|
+
return stdout.trim();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return stdout.trim();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function runClaude(input, options = {}) {
|
|
49
|
+
const command = options.command || "claude";
|
|
50
|
+
const cwd = input.cwd || options.cwd || process.cwd();
|
|
51
|
+
const args = buildClaudeArgs(input, options.defaults);
|
|
52
|
+
const outputFormat = input.outputFormat || options.defaults?.outputFormat || "text";
|
|
53
|
+
|
|
54
|
+
return new Promise((resolve, reject) => {
|
|
55
|
+
const child = spawn(command, args, {
|
|
56
|
+
cwd,
|
|
57
|
+
env: {
|
|
58
|
+
...process.env,
|
|
59
|
+
...options.env
|
|
60
|
+
},
|
|
61
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
let stdout = "";
|
|
65
|
+
let stderr = "";
|
|
66
|
+
|
|
67
|
+
child.stdout.on("data", (chunk) => {
|
|
68
|
+
stdout += String(chunk);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
child.stderr.on("data", (chunk) => {
|
|
72
|
+
stderr += String(chunk);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
child.on("error", (error) => {
|
|
76
|
+
reject(
|
|
77
|
+
new Error(`Failed to start Claude CLI. Ensure \`claude\` is installed and on PATH. ${error.message}`)
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
child.on("close", (code) => {
|
|
82
|
+
const trimmedStdout = stdout.trim();
|
|
83
|
+
const trimmedStderr = stderr.trim();
|
|
84
|
+
|
|
85
|
+
if (code !== 0) {
|
|
86
|
+
reject(
|
|
87
|
+
new Error(trimmedStderr || trimmedStdout || `Claude CLI exited with status ${code ?? "unknown"}.`)
|
|
88
|
+
);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
resolve({
|
|
93
|
+
command,
|
|
94
|
+
cwd,
|
|
95
|
+
args,
|
|
96
|
+
outputFormat,
|
|
97
|
+
stdout: normalizeStdout(trimmedStdout, outputFormat),
|
|
98
|
+
stderr: trimmedStderr
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
|
|
4
|
+
import { DEFAULT_MCP_SERVER_NAME } from "./constants.mjs";
|
|
5
|
+
import { buildCodexExecArgs, buildCodexMcpAddArgs, runCodex } from "./codex-cli.mjs";
|
|
6
|
+
|
|
7
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const serverScript = path.resolve(__dirname, "../bin/codex-claude-mcp.mjs");
|
|
9
|
+
|
|
10
|
+
function usage() {
|
|
11
|
+
return [
|
|
12
|
+
"Usage:",
|
|
13
|
+
" codex-claude install [--name <server-name>]",
|
|
14
|
+
" codex-claude ask [--cwd <dir>] [--model <model>] [--skip-git-repo-check] <prompt>",
|
|
15
|
+
" codex-claude review [--cwd <dir>] [--model <model>] [--skip-git-repo-check] [prompt]",
|
|
16
|
+
" codex-claude delegate [--cwd <dir>] [--model <model>] [--skip-git-repo-check] <prompt>"
|
|
17
|
+
].join("\n");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function parseCommand(argv) {
|
|
21
|
+
if (argv.length === 0 || argv[0] === "--help" || argv[0] === "-h") {
|
|
22
|
+
return { command: "help" };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const [command, ...rest] = argv;
|
|
26
|
+
const options = {
|
|
27
|
+
command,
|
|
28
|
+
cwd: undefined,
|
|
29
|
+
model: undefined,
|
|
30
|
+
skipGitRepoCheck: false,
|
|
31
|
+
serverName: DEFAULT_MCP_SERVER_NAME,
|
|
32
|
+
prompt: ""
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
for (let index = 0; index < rest.length; index += 1) {
|
|
36
|
+
const token = rest[index];
|
|
37
|
+
if (token === "--cwd") {
|
|
38
|
+
index += 1;
|
|
39
|
+
options.cwd = rest[index];
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (token === "--model") {
|
|
43
|
+
index += 1;
|
|
44
|
+
options.model = rest[index];
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (token === "--name") {
|
|
48
|
+
index += 1;
|
|
49
|
+
options.serverName = rest[index];
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (token === "--skip-git-repo-check") {
|
|
53
|
+
options.skipGitRepoCheck = true;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
options.prompt = rest.slice(index).join(" ");
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return options;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function buildCodexClaudePrompt(input) {
|
|
64
|
+
const toolNameByCommand = {
|
|
65
|
+
ask: "claude_ask",
|
|
66
|
+
review: "claude_review",
|
|
67
|
+
delegate: "claude_delegate"
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const toolName = toolNameByCommand[input.command];
|
|
71
|
+
const prompt =
|
|
72
|
+
input.prompt ||
|
|
73
|
+
(input.command === "review"
|
|
74
|
+
? "Review the current repository for bugs, regressions, and missing tests."
|
|
75
|
+
: "");
|
|
76
|
+
|
|
77
|
+
if (!toolName || !prompt) {
|
|
78
|
+
throw new Error(usage());
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const call = {
|
|
82
|
+
prompt,
|
|
83
|
+
cwd: input.cwd || process.cwd(),
|
|
84
|
+
outputFormat: "text"
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
return [
|
|
88
|
+
`Use the MCP tool \`${toolName}\` exactly once.`,
|
|
89
|
+
"Do not solve this task yourself.",
|
|
90
|
+
"Return only the tool result with no extra framing.",
|
|
91
|
+
"",
|
|
92
|
+
`Tool arguments: ${JSON.stringify(call)}`
|
|
93
|
+
].join("\n");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function installBridge(input) {
|
|
97
|
+
const args = buildCodexMcpAddArgs({
|
|
98
|
+
serverName: input.serverName,
|
|
99
|
+
serverScript
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
return runCodex(args, {
|
|
103
|
+
cwd: input.cwd || process.cwd()
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function runViaCodex(input) {
|
|
108
|
+
const args = buildCodexExecArgs({
|
|
109
|
+
cwd: input.cwd,
|
|
110
|
+
model: input.model,
|
|
111
|
+
skipGitRepoCheck: input.skipGitRepoCheck,
|
|
112
|
+
prompt: buildCodexClaudePrompt(input)
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
return runCodex(args, {
|
|
116
|
+
cwd: input.cwd || process.cwd()
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function runCodexClaudeCli(argv) {
|
|
121
|
+
const input = parseCommand(argv);
|
|
122
|
+
|
|
123
|
+
if (input.command === "help") {
|
|
124
|
+
console.log(usage());
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (input.command === "install") {
|
|
129
|
+
const result = await installBridge(input);
|
|
130
|
+
if (result.stdout) {
|
|
131
|
+
console.log(result.stdout);
|
|
132
|
+
}
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (input.command === "ask" || input.command === "review" || input.command === "delegate") {
|
|
137
|
+
const result = await runViaCodex(input);
|
|
138
|
+
if (result.stdout) {
|
|
139
|
+
console.log(result.stdout);
|
|
140
|
+
}
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
throw new Error(usage());
|
|
145
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
export function buildCodexExecArgs(input) {
|
|
4
|
+
const args = ["exec"];
|
|
5
|
+
|
|
6
|
+
if (input.cwd) {
|
|
7
|
+
args.push("--cd", input.cwd);
|
|
8
|
+
}
|
|
9
|
+
if (input.model) {
|
|
10
|
+
args.push("--model", input.model);
|
|
11
|
+
}
|
|
12
|
+
if (input.sandbox) {
|
|
13
|
+
args.push("--sandbox", input.sandbox);
|
|
14
|
+
}
|
|
15
|
+
if (input.skipGitRepoCheck) {
|
|
16
|
+
args.push("--skip-git-repo-check");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
args.push(input.prompt);
|
|
20
|
+
return args;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function buildCodexMcpAddArgs(input) {
|
|
24
|
+
return [
|
|
25
|
+
"mcp",
|
|
26
|
+
"add",
|
|
27
|
+
input.serverName,
|
|
28
|
+
"--",
|
|
29
|
+
"node",
|
|
30
|
+
input.serverScript
|
|
31
|
+
];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function runCodex(args, options = {}) {
|
|
35
|
+
const command = options.command || "codex";
|
|
36
|
+
const cwd = options.cwd || process.cwd();
|
|
37
|
+
|
|
38
|
+
return new Promise((resolve, reject) => {
|
|
39
|
+
const child = spawn(command, args, {
|
|
40
|
+
cwd,
|
|
41
|
+
env: {
|
|
42
|
+
...process.env,
|
|
43
|
+
...options.env
|
|
44
|
+
},
|
|
45
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
let stdout = "";
|
|
49
|
+
let stderr = "";
|
|
50
|
+
|
|
51
|
+
child.stdout.on("data", (chunk) => {
|
|
52
|
+
stdout += String(chunk);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
child.stderr.on("data", (chunk) => {
|
|
56
|
+
stderr += String(chunk);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
child.on("error", (error) => {
|
|
60
|
+
reject(new Error(`Failed to start Codex CLI. Ensure \`codex\` is installed and on PATH. ${error.message}`));
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
child.on("close", (code) => {
|
|
64
|
+
if (code !== 0) {
|
|
65
|
+
reject(new Error(stderr.trim() || stdout.trim() || `Codex CLI exited with status ${code ?? "unknown"}.`));
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
resolve({
|
|
70
|
+
command,
|
|
71
|
+
args,
|
|
72
|
+
cwd,
|
|
73
|
+
stdout: stdout.trim(),
|
|
74
|
+
stderr: stderr.trim()
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
}
|
package/src/schema.mjs
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
export const CLAUDE_COMMON_INPUT = {
|
|
2
|
+
cwd: {
|
|
3
|
+
type: "string",
|
|
4
|
+
description: "Working directory for the Claude Code run. Defaults to the current process directory."
|
|
5
|
+
},
|
|
6
|
+
model: {
|
|
7
|
+
type: "string",
|
|
8
|
+
description: "Optional Claude model or alias, for example `sonnet` or `claude-sonnet-4-6`."
|
|
9
|
+
},
|
|
10
|
+
permissionMode: {
|
|
11
|
+
type: "string",
|
|
12
|
+
enum: ["default", "acceptEdits", "auto", "dontAsk", "bypassPermissions", "plan"],
|
|
13
|
+
description: "Claude Code permission mode."
|
|
14
|
+
},
|
|
15
|
+
allowedTools: {
|
|
16
|
+
type: "array",
|
|
17
|
+
items: { type: "string" },
|
|
18
|
+
description: "Optional allow-list passed to `--allowedTools`."
|
|
19
|
+
},
|
|
20
|
+
disallowedTools: {
|
|
21
|
+
type: "array",
|
|
22
|
+
items: { type: "string" },
|
|
23
|
+
description: "Optional deny-list passed to `--disallowedTools`."
|
|
24
|
+
},
|
|
25
|
+
appendSystemPrompt: {
|
|
26
|
+
type: "string",
|
|
27
|
+
description: "Extra system guidance appended to Claude's default system prompt."
|
|
28
|
+
},
|
|
29
|
+
maxBudgetUsd: {
|
|
30
|
+
type: "number",
|
|
31
|
+
description: "Optional API budget cap for the run."
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const ASK_TOOL_SCHEMA = {
|
|
36
|
+
type: "object",
|
|
37
|
+
additionalProperties: false,
|
|
38
|
+
required: ["prompt"],
|
|
39
|
+
properties: {
|
|
40
|
+
prompt: {
|
|
41
|
+
type: "string",
|
|
42
|
+
description: "Task or question for Claude Code."
|
|
43
|
+
},
|
|
44
|
+
outputFormat: {
|
|
45
|
+
type: "string",
|
|
46
|
+
enum: ["text", "json"],
|
|
47
|
+
description: "Preferred result format. Defaults to `text`."
|
|
48
|
+
},
|
|
49
|
+
...CLAUDE_COMMON_INPUT
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const REVIEW_TOOL_SCHEMA = {
|
|
54
|
+
type: "object",
|
|
55
|
+
additionalProperties: false,
|
|
56
|
+
required: ["prompt"],
|
|
57
|
+
properties: {
|
|
58
|
+
prompt: {
|
|
59
|
+
type: "string",
|
|
60
|
+
description: "Review instructions. Example: `Review the current diff for bugs and regressions.`"
|
|
61
|
+
},
|
|
62
|
+
outputFormat: {
|
|
63
|
+
type: "string",
|
|
64
|
+
enum: ["text", "json"],
|
|
65
|
+
description: "Preferred result format. Defaults to `text`."
|
|
66
|
+
},
|
|
67
|
+
...CLAUDE_COMMON_INPUT
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export const DELEGATE_TOOL_SCHEMA = {
|
|
72
|
+
type: "object",
|
|
73
|
+
additionalProperties: false,
|
|
74
|
+
required: ["prompt"],
|
|
75
|
+
properties: {
|
|
76
|
+
prompt: {
|
|
77
|
+
type: "string",
|
|
78
|
+
description: "Implementation task for Claude Code."
|
|
79
|
+
},
|
|
80
|
+
outputFormat: {
|
|
81
|
+
type: "string",
|
|
82
|
+
enum: ["text", "json"],
|
|
83
|
+
description: "Preferred result format. Defaults to `text`."
|
|
84
|
+
},
|
|
85
|
+
...CLAUDE_COMMON_INPUT
|
|
86
|
+
}
|
|
87
|
+
};
|
package/src/server.mjs
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
|
|
5
|
+
import { PACKAGE_NAME, PACKAGE_VERSION } from "./constants.mjs";
|
|
6
|
+
import { ASK_TOOL_SCHEMA, DELEGATE_TOOL_SCHEMA, REVIEW_TOOL_SCHEMA } from "./schema.mjs";
|
|
7
|
+
import { handleClaudeAsk, handleClaudeDelegate, handleClaudeReview } from "./tools.mjs";
|
|
8
|
+
|
|
9
|
+
function jsonSchemaToZod(shape) {
|
|
10
|
+
const entries = Object.entries(shape.properties);
|
|
11
|
+
const built = {};
|
|
12
|
+
|
|
13
|
+
for (const [key, definition] of entries) {
|
|
14
|
+
let node;
|
|
15
|
+
if (definition.enum) {
|
|
16
|
+
node = z.enum(definition.enum);
|
|
17
|
+
} else if (definition.type === "string") {
|
|
18
|
+
node = z.string();
|
|
19
|
+
} else if (definition.type === "number") {
|
|
20
|
+
node = z.number();
|
|
21
|
+
} else if (definition.type === "array" && definition.items?.type === "string") {
|
|
22
|
+
node = z.array(z.string());
|
|
23
|
+
} else {
|
|
24
|
+
node = z.any();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!shape.required?.includes(key)) {
|
|
28
|
+
node = node.optional();
|
|
29
|
+
}
|
|
30
|
+
built[key] = node;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return z.object(built);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function createServer() {
|
|
37
|
+
const server = new McpServer({
|
|
38
|
+
name: PACKAGE_NAME,
|
|
39
|
+
version: PACKAGE_VERSION
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
server.registerTool(
|
|
43
|
+
"claude_ask",
|
|
44
|
+
{
|
|
45
|
+
title: "Claude Ask",
|
|
46
|
+
description: "Send a read-only prompt to Claude Code and return the result.",
|
|
47
|
+
inputSchema: jsonSchemaToZod(ASK_TOOL_SCHEMA),
|
|
48
|
+
annotations: {
|
|
49
|
+
readOnlyHint: true,
|
|
50
|
+
destructiveHint: false,
|
|
51
|
+
idempotentHint: true,
|
|
52
|
+
openWorldHint: false
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
handleClaudeAsk
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
server.registerTool(
|
|
59
|
+
"claude_review",
|
|
60
|
+
{
|
|
61
|
+
title: "Claude Review",
|
|
62
|
+
description: "Run Claude Code as a reviewer against the current workspace.",
|
|
63
|
+
inputSchema: jsonSchemaToZod(REVIEW_TOOL_SCHEMA),
|
|
64
|
+
annotations: {
|
|
65
|
+
readOnlyHint: true,
|
|
66
|
+
destructiveHint: false,
|
|
67
|
+
idempotentHint: true,
|
|
68
|
+
openWorldHint: false
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
handleClaudeReview
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
server.registerTool(
|
|
75
|
+
"claude_delegate",
|
|
76
|
+
{
|
|
77
|
+
title: "Claude Delegate",
|
|
78
|
+
description: "Delegate an implementation task to Claude Code, allowing file edits by default.",
|
|
79
|
+
inputSchema: jsonSchemaToZod(DELEGATE_TOOL_SCHEMA),
|
|
80
|
+
annotations: {
|
|
81
|
+
readOnlyHint: false,
|
|
82
|
+
destructiveHint: false,
|
|
83
|
+
idempotentHint: false,
|
|
84
|
+
openWorldHint: false
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
handleClaudeDelegate
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
return server;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export async function startServer() {
|
|
94
|
+
const server = await createServer();
|
|
95
|
+
const transport = new StdioServerTransport();
|
|
96
|
+
await server.connect(transport);
|
|
97
|
+
}
|
package/src/tools.mjs
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { runClaude } from "./claude-cli.mjs";
|
|
2
|
+
|
|
3
|
+
function formatResult(title, result) {
|
|
4
|
+
const lines = [
|
|
5
|
+
`# ${title}`,
|
|
6
|
+
"",
|
|
7
|
+
`Command: \`claude ${result.args.map((part) => (/\s/.test(part) ? JSON.stringify(part) : part)).join(" ")}\``,
|
|
8
|
+
`Working directory: \`${result.cwd}\``,
|
|
9
|
+
""
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
if (result.stdout) {
|
|
13
|
+
lines.push("Output:", result.outputFormat === "json" ? "```json" : "```text", result.stdout, "```");
|
|
14
|
+
} else {
|
|
15
|
+
lines.push("Output:", "```text", "", "```");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (result.stderr) {
|
|
19
|
+
lines.push("", "Stderr:", "```text", result.stderr, "```");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return lines.join("\n");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function toTextContent(text) {
|
|
26
|
+
return {
|
|
27
|
+
content: [
|
|
28
|
+
{
|
|
29
|
+
type: "text",
|
|
30
|
+
text
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function handleClaudeAsk(input) {
|
|
37
|
+
const result = await runClaude(input, {
|
|
38
|
+
defaults: {
|
|
39
|
+
outputFormat: input.outputFormat || "text",
|
|
40
|
+
permissionMode: input.permissionMode || "default"
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return toTextContent(formatResult("Claude Ask", result));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function handleClaudeReview(input) {
|
|
48
|
+
const reviewInput = {
|
|
49
|
+
...input,
|
|
50
|
+
appendSystemPrompt:
|
|
51
|
+
input.appendSystemPrompt ||
|
|
52
|
+
"Act as a strict code reviewer. Prioritize bugs, regressions, security issues, and missing tests."
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const result = await runClaude(reviewInput, {
|
|
56
|
+
defaults: {
|
|
57
|
+
outputFormat: input.outputFormat || "text",
|
|
58
|
+
permissionMode: input.permissionMode || "default"
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
return toTextContent(formatResult("Claude Review", result));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function handleClaudeDelegate(input) {
|
|
66
|
+
const delegateInput = {
|
|
67
|
+
...input,
|
|
68
|
+
permissionMode: input.permissionMode || "acceptEdits"
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const result = await runClaude(delegateInput, {
|
|
72
|
+
defaults: {
|
|
73
|
+
outputFormat: input.outputFormat || "text",
|
|
74
|
+
permissionMode: delegateInput.permissionMode
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
return toTextContent(formatResult("Claude Delegate", result));
|
|
79
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
|
|
4
|
+
import { buildClaudeArgs } from "../src/claude-cli.mjs";
|
|
5
|
+
|
|
6
|
+
test("buildClaudeArgs creates the expected Claude CLI invocation", () => {
|
|
7
|
+
const args = buildClaudeArgs({
|
|
8
|
+
prompt: "Review the current diff",
|
|
9
|
+
outputFormat: "json",
|
|
10
|
+
model: "sonnet",
|
|
11
|
+
permissionMode: "dontAsk",
|
|
12
|
+
allowedTools: ["Read", "Bash(git:*)"],
|
|
13
|
+
disallowedTools: ["Edit"],
|
|
14
|
+
appendSystemPrompt: "Be strict.",
|
|
15
|
+
maxBudgetUsd: 1.5
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
assert.deepEqual(args, [
|
|
19
|
+
"--print",
|
|
20
|
+
"--output-format",
|
|
21
|
+
"json",
|
|
22
|
+
"--model",
|
|
23
|
+
"sonnet",
|
|
24
|
+
"--permission-mode",
|
|
25
|
+
"dontAsk",
|
|
26
|
+
"--max-budget-usd",
|
|
27
|
+
"1.5",
|
|
28
|
+
"--append-system-prompt",
|
|
29
|
+
"Be strict.",
|
|
30
|
+
"--allowedTools",
|
|
31
|
+
"Read Bash(git:*)",
|
|
32
|
+
"--disallowedTools",
|
|
33
|
+
"Edit",
|
|
34
|
+
"Review the current diff"
|
|
35
|
+
]);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("buildClaudeArgs falls back to text output when unspecified", () => {
|
|
39
|
+
const args = buildClaudeArgs(
|
|
40
|
+
{
|
|
41
|
+
prompt: "Summarize the repo"
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
outputFormat: "text",
|
|
45
|
+
permissionMode: "default"
|
|
46
|
+
}
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
assert.deepEqual(args, [
|
|
50
|
+
"--print",
|
|
51
|
+
"--output-format",
|
|
52
|
+
"text",
|
|
53
|
+
"--permission-mode",
|
|
54
|
+
"default",
|
|
55
|
+
"Summarize the repo"
|
|
56
|
+
]);
|
|
57
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
|
|
4
|
+
import { buildCodexExecArgs, buildCodexMcpAddArgs } from "../src/codex-cli.mjs";
|
|
5
|
+
import { buildCodexClaudePrompt } from "../src/codex-claude-command.mjs";
|
|
6
|
+
|
|
7
|
+
test("buildCodexExecArgs creates the expected codex exec invocation", () => {
|
|
8
|
+
const args = buildCodexExecArgs({
|
|
9
|
+
cwd: "/repo",
|
|
10
|
+
model: "gpt-5.4",
|
|
11
|
+
skipGitRepoCheck: true,
|
|
12
|
+
prompt: "Use the tool."
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
assert.deepEqual(args, [
|
|
16
|
+
"exec",
|
|
17
|
+
"--cd",
|
|
18
|
+
"/repo",
|
|
19
|
+
"--model",
|
|
20
|
+
"gpt-5.4",
|
|
21
|
+
"--skip-git-repo-check",
|
|
22
|
+
"Use the tool."
|
|
23
|
+
]);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("buildCodexMcpAddArgs registers the local bridge server", () => {
|
|
27
|
+
const args = buildCodexMcpAddArgs({
|
|
28
|
+
serverName: "claude-bridge",
|
|
29
|
+
serverScript: "/tmp/codex-claude-mcp.mjs"
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
assert.deepEqual(args, [
|
|
33
|
+
"mcp",
|
|
34
|
+
"add",
|
|
35
|
+
"claude-bridge",
|
|
36
|
+
"--",
|
|
37
|
+
"node",
|
|
38
|
+
"/tmp/codex-claude-mcp.mjs"
|
|
39
|
+
]);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("buildCodexClaudePrompt targets the review tool with a default review request", () => {
|
|
43
|
+
const prompt = buildCodexClaudePrompt({
|
|
44
|
+
command: "review",
|
|
45
|
+
cwd: "/repo"
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
assert.match(prompt, /claude_review/);
|
|
49
|
+
assert.match(prompt, /Return only the tool result/);
|
|
50
|
+
assert.match(prompt, /Review the current repository for bugs, regressions, and missing tests/);
|
|
51
|
+
assert.match(prompt, /"cwd":"\/repo"/);
|
|
52
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
|
|
4
|
+
import { createServer } from "../src/server.mjs";
|
|
5
|
+
|
|
6
|
+
test("createServer registers the expected Claude bridge tools", async () => {
|
|
7
|
+
const server = await createServer();
|
|
8
|
+
assert.deepEqual(
|
|
9
|
+
Object.keys(server._registeredTools).sort(),
|
|
10
|
+
["claude_ask", "claude_delegate", "claude_review"]
|
|
11
|
+
);
|
|
12
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
|
|
4
|
+
import { handleClaudeAsk } from "../src/tools.mjs";
|
|
5
|
+
|
|
6
|
+
test("handleClaudeAsk renders a Codex-friendly text block", async () => {
|
|
7
|
+
const originalPath = process.env.PATH;
|
|
8
|
+
process.env.PATH = "/tmp";
|
|
9
|
+
|
|
10
|
+
const response = await handleClaudeAsk({
|
|
11
|
+
prompt: "Summarize",
|
|
12
|
+
outputFormat: "text"
|
|
13
|
+
}).catch((error) => error);
|
|
14
|
+
|
|
15
|
+
process.env.PATH = originalPath;
|
|
16
|
+
|
|
17
|
+
assert.equal(response instanceof Error, true);
|
|
18
|
+
assert.match(response.message, /Failed to start Claude CLI|Ensure `claude` is installed/);
|
|
19
|
+
});
|