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.
@@ -32,41 +32,44 @@ server.registerTool('perform_code_review', {
32
32
 
33
33
  🎯 USAGE:
34
34
 
35
- 1. Get the code/diff to review:
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. Optionally gather project context:
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: Checks YOUR patterns, enforces YOUR rules
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: Project rules/guidelines (saves tokens if provided)
58
- - skipContextGathering: Skip auto-gathering if you don't need context
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
- customContext: z.string().optional().describe('⭐ RECOMMENDED: Project context (.cursor/rules, CODE_REVIEW.md, etc). Saves tokens and time!'),
68
- skipContextGathering: z.boolean().optional().describe('Set to true to skip automatic context gathering (useful if no context needed)'),
69
- workspaceDir: z.string().optional().describe('Base directory for context gathering (defaults to current directory)'),
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 () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-review-mcp",
3
- "version": "2.3.4",
3
+ "version": "2.3.6",
4
4
  "main": "index.js",
5
5
  "scripts": {
6
6
  "build": "tsc",