@j0hanz/code-review-analyst-mcp 1.6.0 → 1.6.2

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.
@@ -33,6 +33,14 @@ function shouldKeepSection(section) {
33
33
  }
34
34
  return true;
35
35
  }
36
+ function processSection(raw, start, end, sections) {
37
+ if (end > start) {
38
+ const section = raw.slice(start, end);
39
+ if (shouldKeepSection(section)) {
40
+ sections.push(section);
41
+ }
42
+ }
43
+ }
36
44
  /**
37
45
  * Split raw unified diff into per-file sections and strip:
38
46
  * - Binary file sections ("Binary files a/... and b/... differ")
@@ -46,25 +54,21 @@ export function cleanDiff(raw) {
46
54
  if (!raw)
47
55
  return '';
48
56
  const sections = [];
49
- const delimiter = /^diff --git /gm;
50
57
  let lastIndex = 0;
51
- let match;
52
- while ((match = delimiter.exec(raw)) !== null) {
53
- if (match.index > lastIndex) {
54
- const section = raw.slice(lastIndex, match.index);
55
- if (shouldKeepSection(section)) {
56
- sections.push(section);
57
- }
58
- }
59
- lastIndex = match.index;
58
+ let nextIndex = raw.startsWith('diff --git ')
59
+ ? 0
60
+ : raw.indexOf('\ndiff --git ');
61
+ if (nextIndex === -1) {
62
+ processSection(raw, 0, raw.length, sections);
63
+ return sections.join('').trim();
60
64
  }
61
- // Process the last section
62
- if (lastIndex < raw.length) {
63
- const section = raw.slice(lastIndex);
64
- if (shouldKeepSection(section)) {
65
- sections.push(section);
66
- }
65
+ while (nextIndex !== -1) {
66
+ const matchIndex = nextIndex === 0 ? 0 : nextIndex + 1; // +1 to skip \n
67
+ processSection(raw, lastIndex, matchIndex, sections);
68
+ lastIndex = matchIndex;
69
+ nextIndex = raw.indexOf('\ndiff --git ', lastIndex);
67
70
  }
71
+ processSection(raw, lastIndex, raw.length, sections);
68
72
  return sections.join('').trim();
69
73
  }
70
74
  export function isEmptyDiff(diff) {
@@ -1,5 +1,8 @@
1
+ import { createCachedEnvInt } from './env-config.js';
1
2
  import { createErrorToolResponse } from './tool-response.js';
2
3
  export const DIFF_RESOURCE_URI = 'diff://current';
4
+ const diffCacheTtlMs = createCachedEnvInt('DIFF_CACHE_TTL_MS', 60 * 60 * 1_000 // 1 hour default
5
+ );
3
6
  const diffSlots = new Map();
4
7
  let sendResourceUpdated;
5
8
  function setDiffSlot(key, data) {
@@ -29,7 +32,17 @@ export function storeDiff(data, key = process.cwd()) {
29
32
  notifyDiffUpdated();
30
33
  }
31
34
  export function getDiff(key = process.cwd()) {
32
- return diffSlots.get(key);
35
+ const slot = diffSlots.get(key);
36
+ if (!slot) {
37
+ return undefined;
38
+ }
39
+ const age = Date.now() - new Date(slot.generatedAt).getTime();
40
+ if (age > diffCacheTtlMs.get()) {
41
+ diffSlots.delete(key);
42
+ notifyDiffUpdated();
43
+ return undefined;
44
+ }
45
+ return slot;
33
46
  }
34
47
  export function hasDiff(key = process.cwd()) {
35
48
  return diffSlots.has(key);
@@ -8,7 +8,14 @@ import { stripJsonSchemaConstraints } from './gemini-schema.js';
8
8
  import { generateStructuredJson, getCurrentRequestId } from './gemini.js';
9
9
  import { createErrorToolResponse, createToolResponse, } from './tool-response.js';
10
10
  const DEFAULT_TASK_TTL_MS = 30 * 60 * 1_000;
11
- const TASK_PROGRESS_TOTAL = 4;
11
+ // Named progress step indices for 7-step progress (0–6).
12
+ const STEP_STARTING = 0;
13
+ const STEP_VALIDATING = 1;
14
+ const STEP_BUILDING_PROMPT = 2;
15
+ const STEP_CALLING_MODEL = 3;
16
+ const STEP_VALIDATING_RESPONSE = 4;
17
+ const STEP_FINALIZING = 5;
18
+ const TASK_PROGRESS_TOTAL = STEP_FINALIZING + 1;
12
19
  const INPUT_VALIDATION_FAILED = 'Input validation failed';
13
20
  const DEFAULT_PROGRESS_CONTEXT = 'request';
14
21
  const CANCELLED_ERROR_PATTERN = /cancelled|canceled/i;
@@ -240,7 +247,7 @@ async function reportProgressCompletionUpdate(reportProgress, toolName, context,
240
247
  }
241
248
  async function reportSchemaRetryProgressBestEffort(reportProgress, toolName, context, retryCount, maxRetries) {
242
249
  try {
243
- await reportProgressStepUpdate(reportProgress, toolName, context, 3, `repairing schema retry ${retryCount}/${maxRetries}`);
250
+ await reportProgressStepUpdate(reportProgress, toolName, context, STEP_VALIDATING_RESPONSE, `schema repair ${retryCount}/${maxRetries}`);
244
251
  }
245
252
  catch {
246
253
  // Progress updates are best-effort and must not interrupt retries.
@@ -281,7 +288,15 @@ function createGeminiLogger(server, taskId) {
281
288
  });
282
289
  }
283
290
  catch {
284
- // Logging is best-effort; never fail the tool call.
291
+ try {
292
+ const timestamp = new Date().toISOString();
293
+ const payload = JSON.stringify(asObjectRecord(data));
294
+ console.error(`[${timestamp}] [gemini:${level}] ${taskId} - ${payload}`);
295
+ }
296
+ catch {
297
+ // Safe fallback if JSON stringify fails
298
+ console.error(`[gemini:${level}] ${taskId} - (logging failed)`);
299
+ }
285
300
  }
286
301
  };
287
302
  }
@@ -387,8 +402,19 @@ export class ToolTaskRunner {
387
402
  async executeValidation(inputRecord, ctx) {
388
403
  const validationError = await validateRequest(this.config, inputRecord, ctx);
389
404
  if (validationError) {
390
- const validationMessage = validationError.structuredContent.error?.message ??
391
- INPUT_VALIDATION_FAILED;
405
+ let validationMessage = INPUT_VALIDATION_FAILED;
406
+ try {
407
+ const text = validationError.content[0]?.text;
408
+ if (text) {
409
+ const parsed = JSON.parse(text);
410
+ if (parsed.error?.message) {
411
+ validationMessage = parsed.error.message;
412
+ }
413
+ }
414
+ }
415
+ catch {
416
+ // fallback to default
417
+ }
392
418
  await this.updateStatusMessage(validationMessage);
393
419
  await reportProgressCompletionUpdate(this.reportProgress, this.config.name, this.progressContext, 'rejected');
394
420
  await this.storeResultSafely('completed', validationError);
@@ -404,7 +430,8 @@ export class ToolTaskRunner {
404
430
  try {
405
431
  const raw = await generateStructuredJson(createGenerationRequest(this.config, { systemInstruction, prompt: retryPrompt }, this.responseSchema, this.onLog, this.extra.signal));
406
432
  if (attempt === 0) {
407
- await reportProgressStepUpdate(this.reportProgress, this.config.name, this.progressContext, 3, 'validating response');
433
+ await this.updateStatusMessage('validating response');
434
+ await reportProgressStepUpdate(this.reportProgress, this.config.name, this.progressContext, STEP_VALIDATING_RESPONSE, 'validating response');
408
435
  }
409
436
  parsed = this.config.resultSchema.parse(raw);
410
437
  break;
@@ -441,16 +468,23 @@ export class ToolTaskRunner {
441
468
  const ctx = {
442
469
  diffSlot: this.hasSnapshot ? this.diffSlotSnapshot : getDiff(),
443
470
  };
444
- await reportProgressStepUpdate(this.reportProgress, this.config.name, this.progressContext, 0, 'starting');
471
+ await reportProgressStepUpdate(this.reportProgress, this.config.name, this.progressContext, STEP_STARTING, 'starting');
472
+ await this.updateStatusMessage('starting');
473
+ await reportProgressStepUpdate(this.reportProgress, this.config.name, this.progressContext, STEP_VALIDATING, 'validating input');
474
+ await this.updateStatusMessage('validating input');
445
475
  if (!(await this.executeValidation(inputRecord, ctx))) {
446
476
  return;
447
477
  }
448
- await reportProgressStepUpdate(this.reportProgress, this.config.name, this.progressContext, 1, 'building prompt');
478
+ await reportProgressStepUpdate(this.reportProgress, this.config.name, this.progressContext, STEP_BUILDING_PROMPT, 'building prompt');
479
+ await this.updateStatusMessage('building prompt');
449
480
  const promptParts = this.config.buildPrompt(inputRecord, ctx);
450
481
  const { prompt, systemInstruction } = promptParts;
451
482
  const modelLabel = friendlyModelName(this.config.model);
452
- await reportProgressStepUpdate(this.reportProgress, this.config.name, this.progressContext, 2, modelLabel);
483
+ await reportProgressStepUpdate(this.reportProgress, this.config.name, this.progressContext, STEP_CALLING_MODEL, modelLabel);
484
+ await this.updateStatusMessage(modelLabel);
453
485
  const parsed = await this.executeModelCall(systemInstruction, prompt);
486
+ await reportProgressStepUpdate(this.reportProgress, this.config.name, this.progressContext, STEP_FINALIZING, 'finalizing');
487
+ await this.updateStatusMessage('finalizing');
454
488
  const finalResult = (this.config.transformResult
455
489
  ? this.config.transformResult(inputRecord, parsed, ctx)
456
490
  : parsed);
@@ -459,6 +493,7 @@ export class ToolTaskRunner {
459
493
  : undefined;
460
494
  const outcome = this.config.formatOutcome?.(finalResult) ?? 'completed';
461
495
  await reportProgressCompletionUpdate(this.reportProgress, this.config.name, this.progressContext, outcome);
496
+ await this.updateStatusMessage(`completed: ${outcome}`);
462
497
  await this.storeResultSafely('completed', createToolResponse({
463
498
  ok: true,
464
499
  result: finalResult,
@@ -24,7 +24,9 @@ interface ToolResponse<TStructuredContent extends ToolStructuredContent> {
24
24
  content: ToolTextContent[];
25
25
  structuredContent: TStructuredContent;
26
26
  }
27
- interface ErrorToolResponse extends ToolResponse<ToolStructuredContent> {
27
+ interface ErrorToolResponse {
28
+ [key: string]: unknown;
29
+ content: ToolTextContent[];
28
30
  isError: true;
29
31
  }
30
32
  export declare function createToolResponse<TStructuredContent extends ToolStructuredContent>(structured: TStructuredContent, textContent?: string): ToolResponse<TStructuredContent>;
@@ -29,5 +29,8 @@ export function createToolResponse(structured, textContent) {
29
29
  }
30
30
  export function createErrorToolResponse(code, message, result, meta) {
31
31
  const structured = createErrorStructuredContent(code, message, result, meta);
32
- return { ...createToolResponse(structured), isError: true };
32
+ return {
33
+ content: toTextContent(structured),
34
+ isError: true,
35
+ };
33
36
  }
@@ -17,13 +17,13 @@ const TOOLS = getToolContractNames();
17
17
  const TOOL_DESCRIPTION_TEXT = 'Select tool for review guide.';
18
18
  const FOCUS_DESCRIPTION_TEXT = 'Select focus area.';
19
19
  const FOCUS_AREA_GUIDES = {
20
- security: 'Focus: Injection, auth, crypto, OWASP.',
21
- correctness: 'Focus: Logic, edge cases, algorithms, contracts.',
22
- performance: 'Focus: Complexity, allocations, I/O, queries.',
23
- regressions: 'Focus: Behavior changes, guards, types, breaks.',
24
- tests: 'Focus: Coverage, edge cases, flakes, error paths.',
25
- maintainability: 'Focus: Complexity, readability, structure, patterns.',
26
- concurrency: 'Focus: Race conditions, deadlocks, shared state.',
20
+ security: 'Focus: Injection (SQL/XSS), auth, crypto, OWASP Top 10.',
21
+ correctness: 'Focus: Logic errors, edge cases, algorithm validity, type safety.',
22
+ performance: 'Focus: Big-O complexity, memory allocations, I/O latency, N+1 queries.',
23
+ regressions: 'Focus: Behavior changes, missing guards, breaking API changes.',
24
+ tests: 'Focus: Missing coverage, flaky tests, error paths.',
25
+ maintainability: 'Focus: Code complexity, readability, DRY violations, patterns.',
26
+ concurrency: 'Focus: Race conditions, deadlocks, lack of atomicity.',
27
27
  };
28
28
  function isFocusArea(value) {
29
29
  return INSPECTION_FOCUS_AREAS.includes(value);
@@ -60,6 +60,11 @@ ${toolSections.join('\n\n')}
60
60
 
61
61
  ## CONSTRAINTS
62
62
  ${constraintLines.join('\n')}
63
+
64
+ ## TASK LIFECYCLE
65
+ - Progress steps (0–6): starting → validating input → building prompt → calling model → validating response → finalizing → done.
66
+ - Status messages update at each phase for task introspection.
67
+ - Schema repair: on validation failure, retries with error feedback (configurable via \`GEMINI_SCHEMA_RETRIES\`).
63
68
  - Task terminal states: \`completed\` and \`failed\`; cancellations are surfaced as \`failed\` with \`error.kind=cancelled\`.
64
69
  `;
65
70
  }
@@ -121,7 +121,7 @@ export declare const CodeQualityOutputSchema: z.ZodObject<{
121
121
  }, z.core.$strict>>;
122
122
  testsNeeded: z.ZodArray<z.ZodString>;
123
123
  contextualInsights: z.ZodArray<z.ZodString>;
124
- }, z.core.$strip>;
124
+ }, z.core.$strict>;
125
125
  export declare const SearchReplaceBlockSchema: z.ZodObject<{
126
126
  file: z.ZodString;
127
127
  search: z.ZodString;
@@ -151,7 +151,7 @@ export const ReviewSummaryResultSchema = z.strictObject({
151
151
  export const CodeQualityResultSchema = z.strictObject({
152
152
  ...CODE_QUALITY_SHARED_FIELDS,
153
153
  });
154
- export const CodeQualityOutputSchema = z.object({
154
+ export const CodeQualityOutputSchema = z.strictObject({
155
155
  ...CODE_QUALITY_SHARED_FIELDS,
156
156
  totalFindings: z
157
157
  .number()
@@ -6,7 +6,7 @@ const SYSTEM_INSTRUCTION = `
6
6
  Algorithm Complexity Analyst.
7
7
  Analyze Big-O time/space complexity for changes.
8
8
  Detect performance degradation vs original.
9
- Identify bottlenecks (loops, recursion, aux structures).
9
+ Identify bottlenecks: nested loops, unbounded recursion, heavy allocations.
10
10
  Return strict JSON.
11
11
  `;
12
12
  const TOOL_CONTRACT = requireToolContract('analyze_time_space_complexity');
@@ -35,7 +35,7 @@ export function registerAnalyzeComplexityTool(server) {
35
35
  : '';
36
36
  return {
37
37
  systemInstruction: SYSTEM_INSTRUCTION,
38
- prompt: `${languageLine}\\nDiff:\\n${diff}\\n\\nBased on the diff above, analyze the Big-O time and space complexity.`.trimStart(),
38
+ prompt: `${languageLine}\nDiff:\n${diff}\n\nBased on the diff above, analyze the Big-O time and space complexity.`.trimStart(),
39
39
  };
40
40
  },
41
41
  });
@@ -5,7 +5,7 @@ import { AnalyzePrImpactInputSchema } from '../schemas/inputs.js';
5
5
  import { PrImpactResultSchema } from '../schemas/outputs.js';
6
6
  const SYSTEM_INSTRUCTION = `
7
7
  Technical Change Analyst.
8
- Analyze diff for objective impact: severity, categories, breaking changes, affected areas, rollback complexity.
8
+ Assess objective impact: severity, risk categories, breaking changes, rollback cost.
9
9
  Strictly diff-based; no inference.
10
10
  Return strict JSON.
11
11
  `;
@@ -27,6 +27,7 @@ export function registerAnalyzePrImpactTool(server) {
27
27
  maxOutputTokens: TOOL_CONTRACT.maxOutputTokens,
28
28
  ...buildStructuredToolRuntimeOptions(TOOL_CONTRACT),
29
29
  requiresDiff: true,
30
+ progressContext: (input) => input.repository,
30
31
  formatOutcome: (result) => `severity: ${result.severity}`,
31
32
  formatOutput: (result) => `[${result.severity}] ${result.summary}`,
32
33
  buildPrompt: (input, ctx) => {
@@ -5,8 +5,8 @@ import { DetectApiBreakingResultSchema } from '../schemas/outputs.js';
5
5
  const SYSTEM_INSTRUCTION = `
6
6
  API Compatibility Analyst.
7
7
  Detect breaking changes to public APIs/contracts/interfaces.
8
- Breaking = consumer update required.
9
- Classify: element, nature, impact, mitigation.
8
+ Definition: Breaking change = requires consumer code modification.
9
+ Output: element, nature, impact, mitigation.
10
10
  Return strict JSON.
11
11
  `;
12
12
  const TOOL_CONTRACT = requireToolContract('detect_api_breaking_changes');
@@ -35,7 +35,7 @@ export function registerDetectApiBreakingTool(server) {
35
35
  : '';
36
36
  return {
37
37
  systemInstruction: SYSTEM_INSTRUCTION,
38
- prompt: `${languageLine}\\nDiff:\\n${diff}\\n\\nBased on the diff above, detect any breaking API changes.`.trimStart(),
38
+ prompt: `${languageLine}\nDiff:\n${diff}\n\nBased on the diff above, detect any breaking API changes.`.trimStart(),
39
39
  };
40
40
  },
41
41
  });
@@ -6,6 +6,7 @@ import { computeDiffStatsFromFiles, parseDiffFiles, } from '../lib/diff-parser.j
6
6
  import { DIFF_RESOURCE_URI, storeDiff } from '../lib/diff-store.js';
7
7
  import { wrapToolHandler } from '../lib/tool-factory.js';
8
8
  import { createErrorToolResponse, createToolResponse, } from '../lib/tool-response.js';
9
+ import { DefaultOutputSchema } from '../schemas/outputs.js';
9
10
  const GIT_TIMEOUT_MS = 30_000;
10
11
  const GIT_MAX_BUFFER = 10 * 1024 * 1024; // 10 MB
11
12
  const execFileAsync = promisify(execFile);
@@ -35,11 +36,12 @@ export function registerGenerateDiffTool(server) {
35
36
  server.registerTool('generate_diff', {
36
37
  title: 'Generate Diff',
37
38
  description: 'Generate a diff of the current branch working changes and cache it for all review tools. You MUST call this tool before calling any other review tool. Use "unstaged" for working-tree changes not yet staged, or "staged" for changes already added with git add.',
38
- inputSchema: {
39
+ inputSchema: z.strictObject({
39
40
  mode: z
40
41
  .enum(['unstaged', 'staged'])
41
42
  .describe('"unstaged": working-tree changes not yet staged. "staged": changes added to the index with git add.'),
42
- },
43
+ }),
44
+ outputSchema: DefaultOutputSchema,
43
45
  annotations: {
44
46
  readOnlyHint: false,
45
47
  idempotentHint: true,
@@ -9,7 +9,7 @@ const TOOL_CONTRACT = requireToolContract('generate_review_summary');
9
9
  const SYSTEM_INSTRUCTION = `
10
10
  Senior Code Reviewer.
11
11
  Summarize PR: risk, key changes, merge recommendation (merge/squash/block).
12
- Specific logic changes only.
12
+ Focus: Logic/behavior changes. Ignore: formatting/style/typos.
13
13
  Return strict JSON.
14
14
  `;
15
15
  function formatLanguageSegment(language) {
@@ -41,6 +41,7 @@ export function registerGenerateReviewSummaryTool(server) {
41
41
  maxOutputTokens: TOOL_CONTRACT.maxOutputTokens,
42
42
  ...buildStructuredToolRuntimeOptions(TOOL_CONTRACT),
43
43
  requiresDiff: true,
44
+ progressContext: (input) => input.repository,
44
45
  formatOutcome: (result) => `risk: ${result.overallRisk}`,
45
46
  transformResult: (_input, result, ctx) => {
46
47
  const { files, added, deleted } = getDiffStats(ctx);
@@ -5,9 +5,9 @@ import { GenerateTestPlanInputSchema } from '../schemas/inputs.js';
5
5
  import { TestPlanResultSchema } from '../schemas/outputs.js';
6
6
  const SYSTEM_INSTRUCTION = `
7
7
  QA Automation Architect.
8
- Test plan for diff.
9
- Prioritize: negative, edge, branches, integration.
10
- Target specific visible behavior.
8
+ Generate test plan for diff.
9
+ Prioritize: negative cases, edge cases, branch coverage, integration points.
10
+ Focus: observable behavior only.
11
11
  Return strict JSON.
12
12
  `;
13
13
  const TOOL_CONTRACT = requireToolContract('generate_test_plan');
@@ -28,6 +28,7 @@ export function registerGenerateTestPlanTool(server) {
28
28
  maxOutputTokens: TOOL_CONTRACT.maxOutputTokens,
29
29
  ...buildStructuredToolRuntimeOptions(TOOL_CONTRACT),
30
30
  requiresDiff: true,
31
+ progressContext: (input) => input.repository,
31
32
  formatOutcome: (result) => `${result.testCases.length} test cases`,
32
33
  formatOutput: (result) => `${result.summary}\n${result.testCases.length} test cases.`,
33
34
  transformResult: (input, result) => {
@@ -18,7 +18,7 @@ const SYSTEM_INSTRUCTION = `
18
18
  Principal Engineer Code Review.
19
19
  Source: Unified diff (primary), File excerpts (supplementary context).
20
20
  Goal: Identify bugs, security, performance, maintainability.
21
- Ignore style. Prioritize correctness/failure modes.
21
+ Constraint: Ignore style/formatting. Prioritize correctness/failure modes.
22
22
  Return strict JSON.
23
23
  `;
24
24
  const TOOL_CONTRACT = requireToolContract('inspect_code_quality');
@@ -6,8 +6,8 @@ import { SearchReplaceResultSchema } from '../schemas/outputs.js';
6
6
  const SYSTEM_INSTRUCTION = `
7
7
  Code Remediation Expert.
8
8
  Generate minimal search/replace blocks for described issue.
9
- CRITICAL: 'search' must be verbatim (exact whitespace/indentation).
10
- No out-of-scope changes. Omit if imprecise.
9
+ Constraint: 'search' must be verbatim (exact whitespace/indentation).
10
+ Constraint: No context drift. Omit patch if exact match uncertain.
11
11
  Return strict JSON.
12
12
  `;
13
13
  const TOOL_CONTRACT = requireToolContract('suggest_search_replace');
@@ -28,6 +28,7 @@ export function registerSuggestSearchReplaceTool(server) {
28
28
  maxOutputTokens: TOOL_CONTRACT.maxOutputTokens,
29
29
  ...buildStructuredToolRuntimeOptions(TOOL_CONTRACT),
30
30
  requiresDiff: true,
31
+ progressContext: (input) => input.findingTitle,
31
32
  formatOutcome: (result) => formatPatchCount(result.blocks.length),
32
33
  formatOutput: (result) => {
33
34
  const count = result.blocks.length;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@j0hanz/code-review-analyst-mcp",
3
- "version": "1.6.0",
3
+ "version": "1.6.2",
4
4
  "mcpName": "io.github.j0hanz/code-review-analyst",
5
5
  "description": "Gemini-powered MCP server for code review analysis.",
6
6
  "type": "module",