codex-review-mcp 2.3.4 → 2.3.6
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/dist/mcp-server.js +17 -14
- package/dist/mcp-server.test.js +315 -0
- package/dist/tools/performCodeReview.js +10 -0
- package/dist/tools/performCodeReview.test.js +28 -0
- package/package.json +1 -1
package/dist/mcp-server.js
CHANGED
@@ -32,41 +32,44 @@ server.registerTool('perform_code_review', {
|
|
32
32
|
|
33
33
|
🎯 USAGE:
|
34
34
|
|
35
|
-
1. Get the code
|
35
|
+
1. Get the workspace directory and code to review:
|
36
|
+
const workspaceDir = await getWorkspaceDirectory();
|
36
37
|
const diff = await runCommand("git diff");
|
37
38
|
|
38
|
-
2.
|
39
|
-
const rules = await readFile(".cursor/rules/project.mdc");
|
40
|
-
|
41
|
-
3. Call with content:
|
39
|
+
2. Call with content AND workspaceDir for context gathering:
|
42
40
|
await perform_code_review({
|
43
41
|
content: diff, // REQUIRED
|
42
|
+
workspaceDir: workspaceDir, // ⭐ CRITICAL for .cursor/rules!
|
44
43
|
contentType: "diff", // "diff" or "code"
|
45
|
-
customContext: rules, // Optional: saves tokens!
|
46
44
|
focus: "security and performance" // Optional: specific focus
|
47
45
|
});
|
48
46
|
|
49
47
|
💡 KEY BENEFITS:
|
50
48
|
- Expert Reviews: GPT-5 Codex with project-specific context
|
51
|
-
- Codebase-Aware:
|
49
|
+
- Codebase-Aware: Auto-finds .cursor/rules, package.json, tsconfig.json
|
50
|
+
- Prioritized: .cursor/rules are ALWAYS checked first
|
52
51
|
- Efficient: Provide customContext to skip auto-gathering
|
53
52
|
|
53
|
+
⚠️ IMPORTANT:
|
54
|
+
- workspaceDir is CRITICAL for finding .cursor/rules and project files
|
55
|
+
- Without it, context gathering looks in the wrong directory!
|
56
|
+
|
54
57
|
📋 PARAMETERS:
|
55
58
|
- content: Code or diff to review (REQUIRED)
|
59
|
+
- workspaceDir: Project directory (CRITICAL for .cursor/rules context)
|
56
60
|
- contentType: "diff" | "code"
|
57
|
-
- customContext:
|
58
|
-
- skipContextGathering: Skip auto-gathering
|
59
|
-
- focus: Specific areas to review (e.g., "security", "performance")
|
60
|
-
- workspaceDir: Base directory for context gathering (defaults to cwd)`,
|
61
|
+
- customContext: Manual context (bypasses auto-gathering if provided)
|
62
|
+
- skipContextGathering: Skip auto-gathering (only use if no context needed)
|
63
|
+
- focus: Specific areas to review (e.g., "security", "performance")`,
|
61
64
|
inputSchema: {
|
62
65
|
// REQUIRED
|
63
66
|
content: z.string().describe('Code or diff content to review (REQUIRED). Agent should run git commands or read files to get this.'),
|
64
67
|
// CONTENT TYPE
|
65
68
|
contentType: z.enum(['diff', 'code']).optional().describe('Type of content: "diff" for git diffs, "code" for static code review'),
|
66
69
|
// CONTEXT OPTIONS
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
+
workspaceDir: z.string().optional().describe('⭐ CRITICAL: Project root directory for finding .cursor/rules and config files. Without this, context gathering looks in wrong directory!'),
|
71
|
+
customContext: z.string().optional().describe('Optional: Provide manual context to skip auto-gathering (.cursor/rules, CODE_REVIEW.md, etc)'),
|
72
|
+
skipContextGathering: z.boolean().optional().describe('Set to true to skip automatic context gathering (only if no context needed)'),
|
70
73
|
// REVIEW OPTIONS
|
71
74
|
focus: z.string().optional().describe('Specific areas to focus on (e.g., "security and performance")'),
|
72
75
|
maxTokens: z.number().optional().describe('Maximum tokens for review response'),
|
@@ -0,0 +1,315 @@
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
2
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
3
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
4
|
+
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';
|
5
|
+
import { z } from 'zod';
|
6
|
+
// Mock the performCodeReview function
|
7
|
+
vi.mock('./tools/performCodeReview.js', () => ({
|
8
|
+
performCodeReview: vi.fn()
|
9
|
+
}));
|
10
|
+
import { performCodeReview } from './tools/performCodeReview.js';
|
11
|
+
describe('MCP Server Integration Tests', () => {
|
12
|
+
let server;
|
13
|
+
let client;
|
14
|
+
let serverTransport;
|
15
|
+
let clientTransport;
|
16
|
+
beforeEach(async () => {
|
17
|
+
// Reset mocks
|
18
|
+
vi.clearAllMocks();
|
19
|
+
// Create linked in-memory transports
|
20
|
+
[clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
21
|
+
// Import and set up the server (we'll need to refactor mcp-server.ts slightly)
|
22
|
+
// For now, create a test server with the same configuration
|
23
|
+
server = new McpServer({ name: 'codex-review-mcp', version: '2.3.4' });
|
24
|
+
// Register the same tools as in mcp-server.ts
|
25
|
+
server.registerTool('get_version', {
|
26
|
+
title: 'Get Version',
|
27
|
+
description: 'Get the version of the codex-review-mcp server.',
|
28
|
+
inputSchema: {},
|
29
|
+
}, async () => {
|
30
|
+
return {
|
31
|
+
content: [{
|
32
|
+
type: 'text',
|
33
|
+
text: `codex-review-mcp version 2.3.4`,
|
34
|
+
mimeType: 'text/plain'
|
35
|
+
}],
|
36
|
+
_meta: { version: '2.3.4' }
|
37
|
+
};
|
38
|
+
});
|
39
|
+
server.registerTool('perform_code_review', {
|
40
|
+
title: 'Perform Code Review',
|
41
|
+
description: 'Review code/diffs using GPT-5 Codex',
|
42
|
+
inputSchema: {
|
43
|
+
content: z.string().describe('Code or diff content to review'),
|
44
|
+
contentType: z.enum(['diff', 'code']).optional(),
|
45
|
+
workspaceDir: z.string().optional(),
|
46
|
+
customContext: z.string().optional(),
|
47
|
+
skipContextGathering: z.boolean().optional(),
|
48
|
+
focus: z.string().optional(),
|
49
|
+
maxTokens: z.number().optional(),
|
50
|
+
},
|
51
|
+
}, async (input, extra) => {
|
52
|
+
const onProgress = async (message, progress, total) => {
|
53
|
+
// Only send progress notifications if we have a valid progress token
|
54
|
+
const progressToken = extra?._meta?.progressToken ?? extra?.requestId;
|
55
|
+
if (progressToken === undefined || progressToken === null) {
|
56
|
+
// Skip progress notifications if no token available
|
57
|
+
return;
|
58
|
+
}
|
59
|
+
await server.server.notification({
|
60
|
+
method: 'notifications/progress',
|
61
|
+
params: {
|
62
|
+
progressToken,
|
63
|
+
progress,
|
64
|
+
total,
|
65
|
+
message,
|
66
|
+
},
|
67
|
+
}, extra?.requestId ? { relatedRequestId: extra.requestId } : undefined);
|
68
|
+
};
|
69
|
+
try {
|
70
|
+
const markdown = await performCodeReview({
|
71
|
+
content: input.content,
|
72
|
+
contentType: input.contentType,
|
73
|
+
workspaceDir: input.workspaceDir,
|
74
|
+
customContext: input.customContext,
|
75
|
+
skipContextGathering: input.skipContextGathering,
|
76
|
+
focus: input.focus,
|
77
|
+
maxTokens: input.maxTokens,
|
78
|
+
}, onProgress);
|
79
|
+
return {
|
80
|
+
content: [{ type: 'text', text: markdown, mimeType: 'text/markdown' }],
|
81
|
+
_meta: { version: '2.3.4' }
|
82
|
+
};
|
83
|
+
}
|
84
|
+
catch (error) {
|
85
|
+
const errorMessage = error?.message || String(error);
|
86
|
+
return {
|
87
|
+
content: [{
|
88
|
+
type: 'text',
|
89
|
+
text: `❌ Code Review Failed\n\n**Error:** ${errorMessage}\n\n**Version:** 2.3.4\n\nCheck MCP server logs for details.`,
|
90
|
+
mimeType: 'text/markdown'
|
91
|
+
}],
|
92
|
+
isError: true,
|
93
|
+
_meta: { version: '2.3.4', error: errorMessage }
|
94
|
+
};
|
95
|
+
}
|
96
|
+
});
|
97
|
+
// Create client
|
98
|
+
client = new Client({
|
99
|
+
name: 'test-client',
|
100
|
+
version: '1.0.0'
|
101
|
+
}, {
|
102
|
+
capabilities: {}
|
103
|
+
});
|
104
|
+
// Connect both
|
105
|
+
await server.connect(serverTransport);
|
106
|
+
await client.connect(clientTransport);
|
107
|
+
});
|
108
|
+
afterEach(async () => {
|
109
|
+
await client.close();
|
110
|
+
await server.close();
|
111
|
+
});
|
112
|
+
describe('Server Capabilities', () => {
|
113
|
+
it('should list all registered tools', async () => {
|
114
|
+
const tools = await client.listTools();
|
115
|
+
expect(tools.tools).toHaveLength(2);
|
116
|
+
expect(tools.tools.map(t => t.name)).toContain('get_version');
|
117
|
+
expect(tools.tools.map(t => t.name)).toContain('perform_code_review');
|
118
|
+
});
|
119
|
+
it('should have correct tool schemas', async () => {
|
120
|
+
const tools = await client.listTools();
|
121
|
+
const getVersionTool = tools.tools.find(t => t.name === 'get_version');
|
122
|
+
expect(getVersionTool).toBeDefined();
|
123
|
+
expect(getVersionTool?.description).toContain('version');
|
124
|
+
const reviewTool = tools.tools.find(t => t.name === 'perform_code_review');
|
125
|
+
expect(reviewTool).toBeDefined();
|
126
|
+
expect(reviewTool?.description).toContain('Review code');
|
127
|
+
});
|
128
|
+
});
|
129
|
+
describe('get_version Tool', () => {
|
130
|
+
it('should return the server version', async () => {
|
131
|
+
const result = await client.callTool({
|
132
|
+
name: 'get_version',
|
133
|
+
arguments: {}
|
134
|
+
});
|
135
|
+
expect(result.content).toHaveLength(1);
|
136
|
+
expect(result.content[0]).toMatchObject({
|
137
|
+
type: 'text',
|
138
|
+
mimeType: 'text/plain'
|
139
|
+
});
|
140
|
+
const text = result.content[0].text;
|
141
|
+
expect(text).toContain('codex-review-mcp version');
|
142
|
+
expect(text).toContain('2.3.4');
|
143
|
+
});
|
144
|
+
it('should include version in metadata', async () => {
|
145
|
+
const result = await client.callTool({
|
146
|
+
name: 'get_version',
|
147
|
+
arguments: {}
|
148
|
+
});
|
149
|
+
expect(result._meta).toBeDefined();
|
150
|
+
expect(result._meta?.version).toBe('2.3.4');
|
151
|
+
});
|
152
|
+
});
|
153
|
+
describe('perform_code_review Tool', () => {
|
154
|
+
beforeEach(() => {
|
155
|
+
// Mock performCodeReview to return a simple response
|
156
|
+
vi.mocked(performCodeReview).mockResolvedValue('# Code Review\n\nLooks good!');
|
157
|
+
});
|
158
|
+
it('should call performCodeReview with correct parameters', async () => {
|
159
|
+
await client.callTool({
|
160
|
+
name: 'perform_code_review',
|
161
|
+
arguments: {
|
162
|
+
content: 'console.log("test");',
|
163
|
+
contentType: 'code',
|
164
|
+
focus: 'security'
|
165
|
+
}
|
166
|
+
});
|
167
|
+
expect(performCodeReview).toHaveBeenCalledWith(expect.objectContaining({
|
168
|
+
content: 'console.log("test");',
|
169
|
+
contentType: 'code',
|
170
|
+
focus: 'security'
|
171
|
+
}), expect.any(Function) // onProgress callback
|
172
|
+
);
|
173
|
+
});
|
174
|
+
it('should pass workspaceDir parameter for context gathering', async () => {
|
175
|
+
// CRITICAL TEST: Verifies workspaceDir flows from MCP layer to performCodeReview
|
176
|
+
const testWorkspaceDir = '/Users/test/Projects/learn-webapp';
|
177
|
+
await client.callTool({
|
178
|
+
name: 'perform_code_review',
|
179
|
+
arguments: {
|
180
|
+
content: 'console.log("test");',
|
181
|
+
workspaceDir: testWorkspaceDir
|
182
|
+
}
|
183
|
+
});
|
184
|
+
// Verify workspaceDir is passed to performCodeReview
|
185
|
+
expect(performCodeReview).toHaveBeenCalledWith(expect.objectContaining({
|
186
|
+
workspaceDir: testWorkspaceDir
|
187
|
+
}), expect.any(Function));
|
188
|
+
});
|
189
|
+
it('should handle missing workspaceDir (falls back to cwd)', async () => {
|
190
|
+
await client.callTool({
|
191
|
+
name: 'perform_code_review',
|
192
|
+
arguments: {
|
193
|
+
content: 'console.log("test");'
|
194
|
+
// workspaceDir NOT provided
|
195
|
+
}
|
196
|
+
});
|
197
|
+
// workspaceDir should be undefined, causing gatherContext to use process.cwd()
|
198
|
+
expect(performCodeReview).toHaveBeenCalledWith(expect.objectContaining({
|
199
|
+
workspaceDir: undefined
|
200
|
+
}), expect.any(Function));
|
201
|
+
});
|
202
|
+
it('should return markdown content', async () => {
|
203
|
+
const result = await client.callTool({
|
204
|
+
name: 'perform_code_review',
|
205
|
+
arguments: {
|
206
|
+
content: 'console.log("test");'
|
207
|
+
}
|
208
|
+
});
|
209
|
+
expect(result.content).toHaveLength(1);
|
210
|
+
expect(result.content[0]).toMatchObject({
|
211
|
+
type: 'text',
|
212
|
+
mimeType: 'text/markdown'
|
213
|
+
});
|
214
|
+
const text = result.content[0].text;
|
215
|
+
expect(text).toContain('Code Review');
|
216
|
+
});
|
217
|
+
it('should handle errors gracefully', async () => {
|
218
|
+
// Mock an error
|
219
|
+
vi.mocked(performCodeReview).mockRejectedValue(new Error('Test error'));
|
220
|
+
const result = await client.callTool({
|
221
|
+
name: 'perform_code_review',
|
222
|
+
arguments: {
|
223
|
+
content: 'console.log("test");'
|
224
|
+
}
|
225
|
+
});
|
226
|
+
expect(result.isError).toBe(true);
|
227
|
+
const text = result.content[0].text;
|
228
|
+
expect(text).toContain('Code Review Failed');
|
229
|
+
expect(text).toContain('Test error');
|
230
|
+
});
|
231
|
+
});
|
232
|
+
describe('Progress Notifications', () => {
|
233
|
+
it('should call onProgress callback with proper parameters', async () => {
|
234
|
+
let onProgressCalled = false;
|
235
|
+
let progressMessages = [];
|
236
|
+
// Mock performCodeReview to call onProgress
|
237
|
+
vi.mocked(performCodeReview).mockImplementation(async (_input, onProgress) => {
|
238
|
+
if (onProgress) {
|
239
|
+
onProgressCalled = true;
|
240
|
+
await onProgress('Starting...', 10, 100);
|
241
|
+
progressMessages.push('Starting...');
|
242
|
+
await onProgress('Processing...', 50, 100);
|
243
|
+
progressMessages.push('Processing...');
|
244
|
+
await onProgress('Done', 100, 100);
|
245
|
+
progressMessages.push('Done');
|
246
|
+
}
|
247
|
+
return '# Review Complete';
|
248
|
+
});
|
249
|
+
await client.callTool({
|
250
|
+
name: 'perform_code_review',
|
251
|
+
arguments: {
|
252
|
+
content: 'test code'
|
253
|
+
}
|
254
|
+
});
|
255
|
+
// Verify onProgress was called with proper sequence
|
256
|
+
expect(onProgressCalled).toBe(true);
|
257
|
+
expect(progressMessages).toEqual(['Starting...', 'Processing...', 'Done']);
|
258
|
+
});
|
259
|
+
it('should handle progress with value 0 correctly (verifies explicit nullish check)', async () => {
|
260
|
+
// This test verifies we use explicit nullish check (=== null/undefined)
|
261
|
+
// not falsy check (!progressToken) which would incorrectly filter 0
|
262
|
+
let progressValues = [];
|
263
|
+
vi.mocked(performCodeReview).mockImplementation(async (_input, onProgress) => {
|
264
|
+
if (onProgress) {
|
265
|
+
await onProgress('Start', 0, 100); // 0 is a valid progress value
|
266
|
+
progressValues.push(0);
|
267
|
+
await onProgress('Middle', 50, 100);
|
268
|
+
progressValues.push(50);
|
269
|
+
await onProgress('End', 100, 100);
|
270
|
+
progressValues.push(100);
|
271
|
+
}
|
272
|
+
return '# Review Complete';
|
273
|
+
});
|
274
|
+
await client.callTool({
|
275
|
+
name: 'perform_code_review',
|
276
|
+
arguments: {
|
277
|
+
content: 'test code'
|
278
|
+
}
|
279
|
+
});
|
280
|
+
// Should accept 0 as a valid progress value
|
281
|
+
expect(progressValues).toContain(0);
|
282
|
+
expect(progressValues).toContain(50);
|
283
|
+
expect(progressValues).toContain(100);
|
284
|
+
});
|
285
|
+
});
|
286
|
+
describe('Error Handling', () => {
|
287
|
+
it('should catch and report errors with stack trace', async () => {
|
288
|
+
const testError = new Error('Detailed test error');
|
289
|
+
vi.mocked(performCodeReview).mockRejectedValue(testError);
|
290
|
+
const result = await client.callTool({
|
291
|
+
name: 'perform_code_review',
|
292
|
+
arguments: {
|
293
|
+
content: 'test code'
|
294
|
+
}
|
295
|
+
});
|
296
|
+
expect(result.isError).toBe(true);
|
297
|
+
const text = result.content[0].text;
|
298
|
+
expect(text).toContain('❌ Code Review Failed');
|
299
|
+
expect(text).toContain('Detailed test error');
|
300
|
+
expect(result._meta?.error).toBe('Detailed test error');
|
301
|
+
});
|
302
|
+
it('should handle non-Error objects', async () => {
|
303
|
+
vi.mocked(performCodeReview).mockRejectedValue('String error');
|
304
|
+
const result = await client.callTool({
|
305
|
+
name: 'perform_code_review',
|
306
|
+
arguments: {
|
307
|
+
content: 'test code'
|
308
|
+
}
|
309
|
+
});
|
310
|
+
expect(result.isError).toBe(true);
|
311
|
+
const text = result.content[0].text;
|
312
|
+
expect(text).toContain('String error');
|
313
|
+
});
|
314
|
+
});
|
315
|
+
});
|
@@ -37,6 +37,16 @@ export async function performCodeReview(input, onProgress) {
|
|
37
37
|
: shouldSkip
|
38
38
|
? ''
|
39
39
|
: await gatherContext(input.workspaceDir);
|
40
|
+
// Debug: Log what context was gathered (to stderr for MCP logs)
|
41
|
+
console.error(`[codex-review-mcp] Context gathered: ${context.length} chars`);
|
42
|
+
const hasCursorRules = context.includes('.cursor/rules');
|
43
|
+
console.error(`[codex-review-mcp] Contains .cursor/rules: ${hasCursorRules}`);
|
44
|
+
if (hasCursorRules) {
|
45
|
+
const cursorFiles = (context.match(/<!-- \.cursor\/rules\/[^>]+ -->/g) || []);
|
46
|
+
console.error(`[codex-review-mcp] Cursor rules files: ${cursorFiles.join(', ')}`);
|
47
|
+
}
|
48
|
+
// Also use debugLog for persistent logging
|
49
|
+
await debugLog(`Context gathered: ${context.length} chars, .cursor/rules: ${hasCursorRules}`);
|
40
50
|
// Build expert prompt
|
41
51
|
await onProgress?.('Building expert prompt…', 50, 100);
|
42
52
|
const isStaticReview = input.contentType === 'code';
|
@@ -112,6 +112,34 @@ describe('performCodeReview', () => {
|
|
112
112
|
});
|
113
113
|
expect(gatherContextModule.gatherContext).toHaveBeenCalledWith(workspaceDir);
|
114
114
|
});
|
115
|
+
it('should use process.cwd() when workspaceDir is not provided', async () => {
|
116
|
+
const gatherSpy = vi.spyOn(gatherContextModule, 'gatherContext').mockResolvedValue('');
|
117
|
+
vi.spyOn(invokeAgentModule, 'invokeAgent').mockResolvedValue('# Review');
|
118
|
+
await performCodeReview({
|
119
|
+
content: mockDiff
|
120
|
+
// workspaceDir NOT provided
|
121
|
+
});
|
122
|
+
// Should be called with undefined, which makes gatherContext use process.cwd()
|
123
|
+
expect(gatherSpy).toHaveBeenCalledWith(undefined);
|
124
|
+
});
|
125
|
+
it('should find .cursor/rules when workspaceDir points to correct directory', async () => {
|
126
|
+
// This tests the critical path: workspaceDir -> gatherContext -> .cursor/rules
|
127
|
+
const workspaceDir = '/project/with/cursor/rules';
|
128
|
+
const gatherSpy = vi.spyOn(gatherContextModule, 'gatherContext')
|
129
|
+
.mockResolvedValue('<!-- .cursor/rules/project.mdc -->\n# Cursor Rules\nUse strict mode');
|
130
|
+
vi.spyOn(invokeAgentModule, 'invokeAgent').mockResolvedValue('# Review');
|
131
|
+
const buildPromptSpy = vi.spyOn(buildPromptModule, 'buildPrompt').mockReturnValue('prompt');
|
132
|
+
await performCodeReview({
|
133
|
+
content: mockDiff,
|
134
|
+
workspaceDir
|
135
|
+
});
|
136
|
+
// Verify gatherContext was called with the workspace dir
|
137
|
+
expect(gatherSpy).toHaveBeenCalledWith(workspaceDir);
|
138
|
+
// Verify the gathered context (with .cursor/rules) is passed to buildPrompt
|
139
|
+
expect(buildPromptSpy).toHaveBeenCalledWith(expect.objectContaining({
|
140
|
+
context: expect.stringContaining('.cursor/rules/project.mdc')
|
141
|
+
}));
|
142
|
+
});
|
115
143
|
});
|
116
144
|
describe('Content Types', () => {
|
117
145
|
it('should handle diff content type', async () => {
|