codex-review-mcp 1.0.0 → 1.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/LICENSE +22 -0
- package/README.md +156 -0
- package/dist/mcp-server.js +5 -3
- package/dist/review/collectDiff.js +84 -14
- package/dist/tools/performCodeReview.js +1 -1
- package/package.json +13 -4
package/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2025 codex-review-mcp contributors
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
22
|
+
|
package/README.md
ADDED
@@ -0,0 +1,156 @@
|
|
1
|
+
# Codex Review MCP Server
|
2
|
+
|
3
|
+
Get AI-powered code reviews without leaving your conversation. Keep coding with your AI assistant, and when you need a second opinion, just ask for a review - all in the same context window.
|
4
|
+
|
5
|
+
## Why This Exists
|
6
|
+
|
7
|
+
You're coding with an AI assistant. It makes changes. You want a quick review. Normally you'd copy/paste code into another chat, wait, copy feedback back. **That breaks your flow.**
|
8
|
+
|
9
|
+
With this MCP server, your AI assistant can call out for a code review **right in the middle of your conversation**. No context switching. No copy/paste. The review comes back in the same chat.
|
10
|
+
|
11
|
+
## Features
|
12
|
+
|
13
|
+
- 🎯 **Zero Configuration** - Just add your OpenAI API key
|
14
|
+
- 🔍 Automatically reviews your uncommitted changes
|
15
|
+
- 🤖 Powered by GPT-5 Codex for intelligent code analysis
|
16
|
+
- 📊 Returns actionable feedback in Markdown format
|
17
|
+
- ⚡ Works with any git repository
|
18
|
+
- 🔄 Stays in your conversation context
|
19
|
+
|
20
|
+
## Installation
|
21
|
+
|
22
|
+
### For Cursor Users
|
23
|
+
|
24
|
+
Add the following configuration to your Cursor MCP settings (`~/.cursor/mcp.json` or `%USERPROFILE%\.cursor\mcp.json` on Windows):
|
25
|
+
|
26
|
+
```json
|
27
|
+
{
|
28
|
+
"mcpServers": {
|
29
|
+
"codex-reviewer": {
|
30
|
+
"command": "npx",
|
31
|
+
"args": ["-y", "codex-review-mcp@latest"],
|
32
|
+
"env": {
|
33
|
+
"OPENAI_API_KEY": "your-openai-api-key-here",
|
34
|
+
"CODEX_MODEL": "gpt-5-codex",
|
35
|
+
"WORKSPACE_ROOT": "${workspaceFolder}"
|
36
|
+
}
|
37
|
+
}
|
38
|
+
}
|
39
|
+
}
|
40
|
+
```
|
41
|
+
|
42
|
+
**Important**: Replace `your-openai-api-key-here` with your actual OpenAI API key.
|
43
|
+
|
44
|
+
### For Claude Desktop Users
|
45
|
+
|
46
|
+
Add to your Claude Desktop MCP configuration:
|
47
|
+
|
48
|
+
```json
|
49
|
+
{
|
50
|
+
"mcpServers": {
|
51
|
+
"codex-reviewer": {
|
52
|
+
"command": "npx",
|
53
|
+
"args": ["-y", "codex-review-mcp@latest"],
|
54
|
+
"env": {
|
55
|
+
"OPENAI_API_KEY": "your-openai-api-key-here",
|
56
|
+
"CODEX_MODEL": "gpt-5-codex"
|
57
|
+
}
|
58
|
+
}
|
59
|
+
}
|
60
|
+
}
|
61
|
+
```
|
62
|
+
|
63
|
+
## Usage
|
64
|
+
|
65
|
+
Once configured, just ask your AI assistant:
|
66
|
+
|
67
|
+
> "Do a code review with codex-reviewer"
|
68
|
+
|
69
|
+
That's it! The MCP server automatically figures out what to review:
|
70
|
+
- **If you have uncommitted changes**: Reviews your working tree vs HEAD (what you just coded)
|
71
|
+
- **If your working tree is clean**: Reviews your current branch vs the default branch (main/master)
|
72
|
+
- **If you're on the default branch with no changes**: Reports "No changes to review"
|
73
|
+
|
74
|
+
**Example conversation:**
|
75
|
+
```
|
76
|
+
You: "Add a login form with email validation"
|
77
|
+
AI: [makes changes]
|
78
|
+
AI: "Let me get a code review on this..."
|
79
|
+
AI: [calls codex-reviewer MCP - automatically reviews uncommitted changes]
|
80
|
+
AI: "The review found a potential issue with the email regex..."
|
81
|
+
```
|
82
|
+
|
83
|
+
No need to specify what to review - it just works!
|
84
|
+
|
85
|
+
### Advanced Options (Optional)
|
86
|
+
|
87
|
+
Most users won't need these, but they're available if you want more control:
|
88
|
+
|
89
|
+
- `target` (default: `"head"`): What to review
|
90
|
+
- `"head"`: All uncommitted changes (default - what you just coded)
|
91
|
+
- `"staged"`: Only staged changes (`git add`ed files)
|
92
|
+
- `"range"`: Specific commit range (requires baseRef and headRef)
|
93
|
+
- `focus`: Ask the reviewer to focus on something specific (e.g., "security issues")
|
94
|
+
- `paths`: Review only specific files
|
95
|
+
- `baseRef` / `headRef`: For reviewing commit ranges
|
96
|
+
- `maxTokens`: Limit the length of the review
|
97
|
+
- `workspaceDir`: Override auto-detection of repository location
|
98
|
+
|
99
|
+
## How It Works
|
100
|
+
|
101
|
+
1. **Detects Repository**: Automatically finds your git repository root
|
102
|
+
2. **Collects Diff**: Runs git diff to get the changes
|
103
|
+
3. **Gathers Context**: Collects relevant context about your codebase
|
104
|
+
4. **AI Review**: Sends to GPT-5 Codex for intelligent analysis
|
105
|
+
5. **Returns Feedback**: Provides actionable Markdown feedback
|
106
|
+
|
107
|
+
## Requirements
|
108
|
+
|
109
|
+
- Node.js 18+ (for running the MCP server)
|
110
|
+
- Git repository
|
111
|
+
- OpenAI API key with access to GPT-5 Codex
|
112
|
+
- Cursor or Claude Desktop (or any MCP-compatible client)
|
113
|
+
|
114
|
+
## Environment Variables
|
115
|
+
|
116
|
+
- `OPENAI_API_KEY` (required): Your OpenAI API key
|
117
|
+
- `CODEX_MODEL` (optional, default: "gpt-5-codex"): The model to use for reviews
|
118
|
+
- `WORKSPACE_ROOT` (optional): Path to your workspace (auto-detected in most cases)
|
119
|
+
|
120
|
+
## Development
|
121
|
+
|
122
|
+
### Local Development Setup
|
123
|
+
|
124
|
+
```bash
|
125
|
+
# Clone the repository
|
126
|
+
git clone <your-repo-url>
|
127
|
+
cd codex-review-mcp
|
128
|
+
|
129
|
+
# Install dependencies
|
130
|
+
npm install
|
131
|
+
|
132
|
+
# Build
|
133
|
+
npm run build
|
134
|
+
|
135
|
+
# For local testing, update your MCP config to point to the local build:
|
136
|
+
{
|
137
|
+
"codex-reviewer": {
|
138
|
+
"command": "node",
|
139
|
+
"args": ["/absolute/path/to/codex-review-mcp/dist/mcp-server.js"],
|
140
|
+
"env": {
|
141
|
+
"OPENAI_API_KEY": "your-key",
|
142
|
+
"CODEX_MODEL": "gpt-5-codex",
|
143
|
+
"WORKSPACE_ROOT": "${workspaceFolder}"
|
144
|
+
}
|
145
|
+
}
|
146
|
+
}
|
147
|
+
```
|
148
|
+
|
149
|
+
## License
|
150
|
+
|
151
|
+
MIT
|
152
|
+
|
153
|
+
## Contributing
|
154
|
+
|
155
|
+
Contributions are welcome! Please open an issue or PR.
|
156
|
+
|
package/dist/mcp-server.js
CHANGED
@@ -8,12 +8,13 @@ server.registerTool('perform_code_review', {
|
|
8
8
|
title: 'Perform Code Review',
|
9
9
|
description: 'Review git diffs in the current repo and return actionable Markdown feedback.',
|
10
10
|
inputSchema: {
|
11
|
-
target: z.enum(['staged', 'head', 'range']).default('
|
11
|
+
target: z.enum(['auto', 'staged', 'head', 'range']).default('auto'),
|
12
12
|
baseRef: z.string().optional(),
|
13
13
|
headRef: z.string().optional(),
|
14
14
|
focus: z.string().optional(),
|
15
15
|
paths: z.array(z.string()).optional(),
|
16
16
|
maxTokens: z.number().optional(),
|
17
|
+
workspaceDir: z.string().optional().describe('Absolute path to the workspace/repository directory. If not provided, attempts to detect from environment or current working directory.'),
|
17
18
|
},
|
18
19
|
}, async (input, extra) => {
|
19
20
|
const reviewInput = {
|
@@ -23,18 +24,19 @@ server.registerTool('perform_code_review', {
|
|
23
24
|
focus: input.focus,
|
24
25
|
paths: input.paths,
|
25
26
|
maxTokens: input.maxTokens,
|
27
|
+
workspaceDir: input.workspaceDir,
|
26
28
|
};
|
27
29
|
const onProgress = async (message, progress, total) => {
|
28
30
|
// Attach to tool-call request via related request ID so clients can map progress
|
29
31
|
await server.server.notification({
|
30
32
|
method: 'notifications/progress',
|
31
33
|
params: {
|
32
|
-
progressToken: extra?._meta?.progressToken ?? extra
|
34
|
+
progressToken: extra?._meta?.progressToken ?? extra?.requestId,
|
33
35
|
progress,
|
34
36
|
total,
|
35
37
|
message,
|
36
38
|
},
|
37
|
-
}, { relatedRequestId: extra.requestId });
|
39
|
+
}, extra?.requestId ? { relatedRequestId: extra.requestId } : undefined);
|
38
40
|
};
|
39
41
|
const markdown = await performCodeReview(reviewInput, onProgress);
|
40
42
|
return { content: [{ type: 'text', text: markdown, mimeType: 'text/markdown' }] };
|
@@ -14,7 +14,7 @@ function filterPaths(paths) {
|
|
14
14
|
return undefined;
|
15
15
|
return paths.filter((p) => !DEFAULT_IGNORES.some((g) => minimatch(p, g)));
|
16
16
|
}
|
17
|
-
export async function collectDiff(input) {
|
17
|
+
export async function collectDiff(input, workspaceDir) {
|
18
18
|
async function findRepoRoot(startDir) {
|
19
19
|
let dir = startDir;
|
20
20
|
// Walk up to filesystem root (max ~25 hops as a safety guard)
|
@@ -33,27 +33,97 @@ export async function collectDiff(input) {
|
|
33
33
|
}
|
34
34
|
return null;
|
35
35
|
}
|
36
|
+
async function detectDefaultBranch(repoRoot) {
|
37
|
+
// Try to get default branch from remote
|
38
|
+
try {
|
39
|
+
const { stdout } = await exec('git', ['remote', 'show', 'origin'], { cwd: repoRoot, encoding: 'utf8' });
|
40
|
+
const match = stdout.match(/HEAD branch:\s*(\S+)/);
|
41
|
+
if (match?.[1])
|
42
|
+
return match[1];
|
43
|
+
}
|
44
|
+
catch {
|
45
|
+
// Remote not available or other error, continue to fallback
|
46
|
+
}
|
47
|
+
// Fallback: check if main or master exists locally
|
48
|
+
for (const branch of ['main', 'master']) {
|
49
|
+
try {
|
50
|
+
await exec('git', ['rev-parse', '--verify', branch], { cwd: repoRoot });
|
51
|
+
return branch;
|
52
|
+
}
|
53
|
+
catch {
|
54
|
+
// Branch doesn't exist, try next
|
55
|
+
}
|
56
|
+
}
|
57
|
+
// Last resort: use HEAD~1 as baseline
|
58
|
+
return 'HEAD~1';
|
59
|
+
}
|
60
|
+
async function hasUncommittedChanges(repoRoot) {
|
61
|
+
try {
|
62
|
+
const { stdout } = await exec('git', ['status', '--porcelain'], { cwd: repoRoot, encoding: 'utf8' });
|
63
|
+
return stdout.trim().length > 0;
|
64
|
+
}
|
65
|
+
catch {
|
66
|
+
return false;
|
67
|
+
}
|
68
|
+
}
|
69
|
+
async function getCurrentBranch(repoRoot) {
|
70
|
+
try {
|
71
|
+
const { stdout } = await exec('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: repoRoot, encoding: 'utf8' });
|
72
|
+
return stdout.trim();
|
73
|
+
}
|
74
|
+
catch {
|
75
|
+
return null;
|
76
|
+
}
|
77
|
+
}
|
78
|
+
// Priority order: explicit workspaceDir param > env vars > process.cwd()
|
79
|
+
const preferredStart = workspaceDir || process.env.CODEX_REPO_ROOT || process.env.WORKSPACE_ROOT || process.env.INIT_CWD || process.cwd();
|
80
|
+
const preferredRoot = await findRepoRoot(preferredStart);
|
81
|
+
// If workspaceDir was explicitly provided but no repo found, fail immediately
|
82
|
+
if (workspaceDir && !preferredRoot) {
|
83
|
+
throw new Error(`Could not locate a Git repository starting from workspaceDir "${workspaceDir}".`);
|
84
|
+
}
|
85
|
+
const repoRoot = preferredRoot || (await findRepoRoot(process.cwd())) || process.cwd();
|
36
86
|
const args = ['diff', '--unified=0'];
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
87
|
+
if (input.target === 'auto') {
|
88
|
+
// Auto mode: detect what to review
|
89
|
+
const hasChanges = await hasUncommittedChanges(repoRoot);
|
90
|
+
if (hasChanges) {
|
91
|
+
// Review uncommitted changes vs HEAD
|
42
92
|
args.push('HEAD');
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
93
|
+
}
|
94
|
+
else {
|
95
|
+
// No uncommitted changes, review branch vs default
|
96
|
+
const currentBranch = await getCurrentBranch(repoRoot);
|
97
|
+
const defaultBranch = await detectDefaultBranch(repoRoot);
|
98
|
+
if (currentBranch === defaultBranch) {
|
99
|
+
// On default branch with no changes - nothing to review
|
100
|
+
return '';
|
47
101
|
}
|
48
|
-
|
49
|
-
|
102
|
+
// Review current branch vs default branch
|
103
|
+
args.push(`${defaultBranch}...HEAD`);
|
104
|
+
}
|
105
|
+
}
|
106
|
+
else {
|
107
|
+
// Explicit target modes
|
108
|
+
switch (input.target) {
|
109
|
+
case 'staged':
|
110
|
+
args.splice(1, 0, '--staged');
|
111
|
+
break;
|
112
|
+
case 'head':
|
113
|
+
args.push('HEAD');
|
114
|
+
break;
|
115
|
+
case 'range':
|
116
|
+
if (!input.baseRef || !input.headRef) {
|
117
|
+
throw new Error('range target requires baseRef and headRef');
|
118
|
+
}
|
119
|
+
args.push(`${input.baseRef}...${input.headRef}`);
|
120
|
+
break;
|
121
|
+
}
|
50
122
|
}
|
51
123
|
const filtered = filterPaths(input.paths);
|
52
124
|
if (filtered && filtered.length)
|
53
125
|
args.push(...filtered);
|
54
126
|
try {
|
55
|
-
const preferredStart = process.env.CODEX_REPO_ROOT || process.env.WORKSPACE_ROOT || process.env.INIT_CWD || process.cwd();
|
56
|
-
const repoRoot = (await findRepoRoot(preferredStart)) || (await findRepoRoot(process.cwd())) || process.cwd();
|
57
127
|
const { stdout } = await exec('git', args, {
|
58
128
|
encoding: 'utf8',
|
59
129
|
maxBuffer: 10 * 1024 * 1024,
|
@@ -6,7 +6,7 @@ import { formatOutput } from '../review/formatOutput.js';
|
|
6
6
|
import { debugLog } from '../util/debug.js';
|
7
7
|
export async function performCodeReview(input, onProgress) {
|
8
8
|
await onProgress?.('Collecting diff…', 10, 100);
|
9
|
-
const diffText = await collectDiff(input);
|
9
|
+
const diffText = await collectDiff(input, input.workspaceDir);
|
10
10
|
if (!diffText.trim()) {
|
11
11
|
throw new Error('No diff found. Ensure you have changes or a valid git range.');
|
12
12
|
}
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "codex-review-mcp",
|
3
|
-
"version": "1.
|
3
|
+
"version": "1.1.0",
|
4
4
|
"main": "index.js",
|
5
5
|
"scripts": {
|
6
6
|
"build": "tsc",
|
@@ -27,10 +27,19 @@
|
|
27
27
|
"node": ">=18",
|
28
28
|
"npm": ">=9"
|
29
29
|
},
|
30
|
-
"keywords": [
|
30
|
+
"keywords": [
|
31
|
+
"mcp",
|
32
|
+
"model-context-protocol",
|
33
|
+
"code-review",
|
34
|
+
"ai",
|
35
|
+
"gpt",
|
36
|
+
"codex",
|
37
|
+
"cursor",
|
38
|
+
"claude"
|
39
|
+
],
|
31
40
|
"author": "",
|
32
|
-
"license": "
|
33
|
-
"description": "",
|
41
|
+
"license": "MIT",
|
42
|
+
"description": "MCP server for AI-powered code reviews using GPT-5 Codex",
|
34
43
|
"type": "module",
|
35
44
|
"bin": {
|
36
45
|
"codex-review-mcp": "./bin/codex-review-mcp"
|