codex-review-mcp 1.0.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.
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import '../dist/mcp-server.js';
3
+
4
+
@@ -0,0 +1,43 @@
1
+ import 'dotenv/config';
2
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
+ import { z } from 'zod';
5
+ import { performCodeReview } from './tools/performCodeReview.js';
6
+ const server = new McpServer({ name: 'codex-review-mcp', version: '0.1.0' });
7
+ server.registerTool('perform_code_review', {
8
+ title: 'Perform Code Review',
9
+ description: 'Review git diffs in the current repo and return actionable Markdown feedback.',
10
+ inputSchema: {
11
+ target: z.enum(['staged', 'head', 'range']).default('head'),
12
+ baseRef: z.string().optional(),
13
+ headRef: z.string().optional(),
14
+ focus: z.string().optional(),
15
+ paths: z.array(z.string()).optional(),
16
+ maxTokens: z.number().optional(),
17
+ },
18
+ }, async (input, extra) => {
19
+ const reviewInput = {
20
+ target: input.target,
21
+ baseRef: input.baseRef,
22
+ headRef: input.headRef,
23
+ focus: input.focus,
24
+ paths: input.paths,
25
+ maxTokens: input.maxTokens,
26
+ };
27
+ const onProgress = async (message, progress, total) => {
28
+ // Attach to tool-call request via related request ID so clients can map progress
29
+ await server.server.notification({
30
+ method: 'notifications/progress',
31
+ params: {
32
+ progressToken: extra?._meta?.progressToken ?? extra.requestId,
33
+ progress,
34
+ total,
35
+ message,
36
+ },
37
+ }, { relatedRequestId: extra.requestId });
38
+ };
39
+ const markdown = await performCodeReview(reviewInput, onProgress);
40
+ return { content: [{ type: 'text', text: markdown, mimeType: 'text/markdown' }] };
41
+ });
42
+ const transport = new StdioServerTransport();
43
+ await server.connect(transport);
@@ -0,0 +1,13 @@
1
+ export function buildPrompt({ diffText, context, focus }) {
2
+ const focusLine = focus ? `Focus areas: ${focus}.` : '';
3
+ return [
4
+ 'You are an expert AI code reviewer. Be concise, specific, and actionable.',
5
+ 'Prefer minimal diffs and direct fixes. Respect project guidelines when provided.',
6
+ focusLine,
7
+ '\n---\nScope and Diff (unified=0):\n',
8
+ diffText,
9
+ context ? '\n---\nProject context and guidelines:\n' + context : '',
10
+ '\n---\nOutput strictly as Markdown with the following sections:\n',
11
+ '1) Title + scope summary\n2) Quick Summary (3–6 bullets)\n3) Issues table: severity | file:lines | category | explanation | suggested fix\n4) Inline suggested edits for top issues\n5) Positive notes\n6) Next steps',
12
+ ].join('\n');
13
+ }
@@ -0,0 +1,75 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+ import { promises as fs } from 'node:fs';
4
+ import { dirname, join } from 'node:path';
5
+ import { minimatch } from 'minimatch';
6
+ const exec = promisify(execFile);
7
+ const DEFAULT_IGNORES = [
8
+ '**/dist/**',
9
+ '**/build/**',
10
+ '**/*.lock',
11
+ ];
12
+ function filterPaths(paths) {
13
+ if (!paths || paths.length === 0)
14
+ return undefined;
15
+ return paths.filter((p) => !DEFAULT_IGNORES.some((g) => minimatch(p, g)));
16
+ }
17
+ export async function collectDiff(input) {
18
+ async function findRepoRoot(startDir) {
19
+ let dir = startDir;
20
+ // Walk up to filesystem root (max ~25 hops as a safety guard)
21
+ for (let i = 0; i < 25; i++) {
22
+ try {
23
+ // Accept both .git directory and file (worktree) as signal of repo root
24
+ await fs.stat(join(dir, '.git'));
25
+ return dir;
26
+ }
27
+ catch {
28
+ const parent = dirname(dir);
29
+ if (parent === dir)
30
+ break;
31
+ dir = parent;
32
+ }
33
+ }
34
+ return null;
35
+ }
36
+ const args = ['diff', '--unified=0'];
37
+ switch (input.target) {
38
+ case 'staged':
39
+ args.splice(1, 0, '--staged');
40
+ break;
41
+ case 'head':
42
+ args.push('HEAD');
43
+ break;
44
+ case 'range':
45
+ if (!input.baseRef || !input.headRef) {
46
+ throw new Error('range target requires baseRef and headRef');
47
+ }
48
+ args.push(`${input.baseRef}...${input.headRef}`);
49
+ break;
50
+ }
51
+ const filtered = filterPaths(input.paths);
52
+ if (filtered && filtered.length)
53
+ args.push(...filtered);
54
+ 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
+ const { stdout } = await exec('git', args, {
58
+ encoding: 'utf8',
59
+ maxBuffer: 10 * 1024 * 1024,
60
+ cwd: repoRoot,
61
+ });
62
+ // Drop obvious binary diffs
63
+ const text = stdout
64
+ .split('\n')
65
+ .filter((line) => !line.startsWith('Binary files '))
66
+ .join('\n');
67
+ // Enforce size cap ~300k chars
68
+ const cap = 300_000;
69
+ return text.length > cap ? text.slice(0, cap) + '\n<!-- diff truncated -->\n' : text;
70
+ }
71
+ catch (err) {
72
+ const msg = err?.stderr || err?.message || String(err);
73
+ throw new Error(`Failed to collect git diff: ${msg}`);
74
+ }
75
+ }
@@ -0,0 +1,10 @@
1
+ export function formatOutput(agentMarkdown) {
2
+ // For MVP, trust agent output if non-empty; otherwise return a minimal stub.
3
+ if (agentMarkdown && agentMarkdown.trim().length > 0)
4
+ return agentMarkdown;
5
+ return [
6
+ '# Code Review',
7
+ '',
8
+ 'No issues detected or empty output from agent.',
9
+ ].join('\n');
10
+ }
@@ -0,0 +1,42 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ const CANDIDATE_FILES = [
4
+ '.cursor/rules/project.mdc',
5
+ '.cursor/rules/**/*.md',
6
+ 'CODE_REVIEW.md',
7
+ 'CONTRIBUTING.md',
8
+ 'SECURITY.md',
9
+ '.eslintrc',
10
+ '.eslintrc.cjs',
11
+ '.eslintrc.js',
12
+ '.eslintrc.json',
13
+ 'package.json',
14
+ 'tsconfig.json',
15
+ ];
16
+ async function readIfExists(path) {
17
+ try {
18
+ const data = await fs.readFile(path, 'utf8');
19
+ return data;
20
+ }
21
+ catch {
22
+ return null;
23
+ }
24
+ }
25
+ export async function gatherContext() {
26
+ const chunks = [];
27
+ for (const rel of CANDIDATE_FILES) {
28
+ // naive glob: only handle literal paths here to keep MVP simple
29
+ if (rel.includes('*'))
30
+ continue;
31
+ const data = await readIfExists(join(process.cwd(), rel));
32
+ if (!data)
33
+ continue;
34
+ // bound each file contribution to ~8k chars
35
+ const capped = data.slice(0, 8000);
36
+ chunks.push(`\n<!-- ${rel} -->\n${capped}`);
37
+ // global size guard ~50k chars
38
+ if (chunks.join('').length > 50_000)
39
+ break;
40
+ }
41
+ return chunks.join('\n');
42
+ }
@@ -0,0 +1,14 @@
1
+ import { Agent, run } from '@openai/agents';
2
+ import { debugLog } from '../util/debug.js';
3
+ export async function invokeAgent({ prompt, maxTokens }) {
4
+ const model = process.env.CODEX_MODEL || 'gpt-5-codex';
5
+ const agent = new Agent({
6
+ name: 'Code Reviewer',
7
+ instructions: 'You are a precise code-review agent. Follow the output contract exactly. Use minimal verbosity.',
8
+ model,
9
+ });
10
+ const result = await run(agent, prompt);
11
+ const out = result.finalOutput ?? '';
12
+ await debugLog(`Agent model=${model} outputLen=${out.length}`);
13
+ return out;
14
+ }
@@ -0,0 +1,24 @@
1
+ import { collectDiff } from '../review/collectDiff.js';
2
+ import { gatherContext } from '../review/gatherContext.js';
3
+ import { buildPrompt } from '../review/buildPrompt.js';
4
+ import { invokeAgent } from '../review/invokeAgent.js';
5
+ import { formatOutput } from '../review/formatOutput.js';
6
+ import { debugLog } from '../util/debug.js';
7
+ export async function performCodeReview(input, onProgress) {
8
+ await onProgress?.('Collecting diff…', 10, 100);
9
+ const diffText = await collectDiff(input);
10
+ if (!diffText.trim()) {
11
+ throw new Error('No diff found. Ensure you have changes or a valid git range.');
12
+ }
13
+ await onProgress?.('Gathering context…', 30, 100);
14
+ const context = await gatherContext();
15
+ await onProgress?.('Building prompt…', 45, 100);
16
+ const prompt = buildPrompt({ diffText, context, focus: input.focus });
17
+ await onProgress?.('Calling GPT-5 Codex…', 65, 100);
18
+ const agentMd = await invokeAgent({ prompt, maxTokens: input.maxTokens });
19
+ await debugLog(`Review produced ${agentMd.length} chars`);
20
+ await onProgress?.('Formatting output…', 90, 100);
21
+ const out = formatOutput(agentMd);
22
+ await onProgress?.('Done', 100, 100);
23
+ return out;
24
+ }
@@ -0,0 +1,15 @@
1
+ import { appendFile } from 'node:fs/promises';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ const LOG_PATH = join(homedir(), '.codex-review-mcp-debug.log');
5
+ export async function debugLog(message) {
6
+ if (process.env.CODEX_REVIEW_MCP_DEBUG !== '1')
7
+ return;
8
+ const line = `[${new Date().toISOString()}] ${message}\n`;
9
+ try {
10
+ await appendFile(LOG_PATH, line, 'utf8');
11
+ }
12
+ catch {
13
+ // ignore logging failures in MVP
14
+ }
15
+ }
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "codex-review-mcp",
3
+ "version": "1.0.0",
4
+ "main": "index.js",
5
+ "scripts": {
6
+ "build": "tsc",
7
+ "dev": "tsc -w",
8
+ "prepare": "npm run build",
9
+ "mcp": "node dist/mcp-server.js",
10
+ "agents:sample": "node agents-sample.mjs"
11
+ },
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/pmerwin/codex-review-mcp.git"
15
+ },
16
+ "homepage": "https://github.com/pmerwin/codex-review-mcp#readme",
17
+ "bugs": {
18
+ "url": "https://github.com/pmerwin/codex-review-mcp/issues"
19
+ },
20
+ "files": [
21
+ "dist/**",
22
+ "bin/**",
23
+ "README.md",
24
+ "LICENSE"
25
+ ],
26
+ "engines": {
27
+ "node": ">=18",
28
+ "npm": ">=9"
29
+ },
30
+ "keywords": [],
31
+ "author": "",
32
+ "license": "ISC",
33
+ "description": "",
34
+ "type": "module",
35
+ "bin": {
36
+ "codex-review-mcp": "./bin/codex-review-mcp"
37
+ },
38
+ "dependencies": {
39
+ "@modelcontextprotocol/sdk": "^1.19.1",
40
+ "@openai/agents": "^0.1.9",
41
+ "dotenv": "^17.2.3",
42
+ "minimatch": "^10.0.3",
43
+ "openai": "^6.2.0",
44
+ "zod": "^3.25.76"
45
+ },
46
+ "devDependencies": {
47
+ "@types/node": "^24.7.0",
48
+ "typescript": "^5.9.3"
49
+ }
50
+ }