@j0hanz/code-review-analyst-mcp 1.7.1 → 1.7.3

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.
@@ -514,12 +514,27 @@ async function throwGeminiFailure(attemptsMade, lastError, onLog) {
514
514
  async function runWithRetries(request, model, timeoutMs, maxRetries, onLog) {
515
515
  let lastError;
516
516
  let attempt = 0;
517
+ let currentModel = model;
517
518
  for (; attempt <= maxRetries; attempt += 1) {
518
519
  try {
519
- return await executeAttempt(request, model, timeoutMs, attempt, onLog);
520
+ return await executeAttempt(request, currentModel, timeoutMs, attempt, onLog);
520
521
  }
521
522
  catch (error) {
522
523
  lastError = error;
524
+ if (getNumericErrorCode(error) === 404 &&
525
+ currentModel === 'gemini-3-flash-preview') {
526
+ currentModel = 'gemini-2.5-flash';
527
+ delete request.thinkingLevel;
528
+ await emitGeminiLog(onLog, 'warning', {
529
+ event: 'gemini_model_fallback',
530
+ details: {
531
+ from: 'gemini-3-flash-preview',
532
+ to: 'gemini-2.5-flash',
533
+ reason: 'Model not found (404)',
534
+ },
535
+ });
536
+ continue;
537
+ }
523
538
  if (!canRetryAttempt(attempt, maxRetries, error)) {
524
539
  attempt += 1; // Count this attempt before breaking
525
540
  break;
@@ -737,6 +752,43 @@ async function cancelBatchIfNeeded(request, batches, batchName, onLog, completed
737
752
  });
738
753
  }
739
754
  }
755
+ async function createBatchJobWithFallback(request, batches, model, onLog) {
756
+ let currentModel = model;
757
+ const createSignal = request.signal ?? NEVER_ABORT_SIGNAL;
758
+ for (let attempt = 0; attempt <= 1; attempt += 1) {
759
+ try {
760
+ const createPayload = {
761
+ model: currentModel,
762
+ src: [
763
+ {
764
+ contents: [{ role: 'user', parts: [{ text: request.prompt }] }],
765
+ config: buildGenerationConfig(request, createSignal),
766
+ },
767
+ ],
768
+ };
769
+ return await batches.create(createPayload);
770
+ }
771
+ catch (error) {
772
+ if (attempt === 0 &&
773
+ getNumericErrorCode(error) === 404 &&
774
+ currentModel === 'gemini-3-flash-preview') {
775
+ currentModel = 'gemini-2.5-flash';
776
+ delete request.thinkingLevel;
777
+ await emitGeminiLog(onLog, 'warning', {
778
+ event: 'gemini_model_fallback',
779
+ details: {
780
+ from: 'gemini-3-flash-preview',
781
+ to: 'gemini-2.5-flash',
782
+ reason: 'Model not found (404) during batch create',
783
+ },
784
+ });
785
+ continue;
786
+ }
787
+ throw error;
788
+ }
789
+ }
790
+ throw new Error('Unexpected state: batch creation loop exited without returning or throwing.');
791
+ }
740
792
  async function runInlineBatchWithPolling(request, model, onLog) {
741
793
  const client = getClient();
742
794
  const { batches } = client;
@@ -747,17 +799,7 @@ async function runInlineBatchWithPolling(request, model, onLog) {
747
799
  let completed = false;
748
800
  let timedOut = false;
749
801
  try {
750
- const createSignal = request.signal ?? NEVER_ABORT_SIGNAL;
751
- const createPayload = {
752
- model,
753
- src: [
754
- {
755
- contents: [{ role: 'user', parts: [{ text: request.prompt }] }],
756
- config: buildGenerationConfig(request, createSignal),
757
- },
758
- ],
759
- };
760
- const createdJob = await batches.create(createPayload);
802
+ const createdJob = await createBatchJobWithFallback(request, batches, model, onLog);
761
803
  const createdRecord = toRecord(createdJob);
762
804
  batchName =
763
805
  typeof createdRecord?.name === 'string' ? createdRecord.name : undefined;
@@ -123,6 +123,7 @@ export declare class ToolExecutionRunner<TInput extends object, TResult extends
123
123
  reportProgress: (payload: ProgressPayload) => Promise<void>;
124
124
  statusReporter: TaskStatusReporter;
125
125
  }, signal?: AbortSignal | undefined);
126
+ private handleInternalLog;
126
127
  setResponseSchemaOverride(responseSchema: Record<string, unknown>): void;
127
128
  setDiffSlotSnapshot(diffSlotSnapshot: DiffSlot | undefined): void;
128
129
  private updateStatusMessage;
@@ -241,7 +241,7 @@ function normalizeProgressContext(context) {
241
241
  return `${compact.slice(0, 77)}...`;
242
242
  }
243
243
  function formatProgressStep(toolName, context, metadata) {
244
- return `${toolName}: ${context} [${metadata}]`;
244
+ return `${toolName}: ${context} ${metadata}`;
245
245
  }
246
246
  function formatProgressCompletion(toolName, context, outcome) {
247
247
  return `🗒 ${toolName}: ${context} • ${outcome}`;
@@ -276,7 +276,7 @@ async function reportProgressCompletionUpdate(reportProgress, toolName, context,
276
276
  }
277
277
  async function reportSchemaRetryProgressBestEffort(reportProgress, toolName, context, retryCount, maxRetries) {
278
278
  try {
279
- await reportProgressStepUpdate(reportProgress, toolName, context, STEP_VALIDATING_RESPONSE, `schema repair ${retryCount}/${maxRetries}`);
279
+ await reportProgressStepUpdate(reportProgress, toolName, context, STEP_VALIDATING_RESPONSE, `Schema repair in progress (attempt ${retryCount}/${maxRetries})...`);
280
280
  }
281
281
  catch {
282
282
  // Progress updates are best-effort and must not interrupt retries.
@@ -370,10 +370,33 @@ export class ToolExecutionRunner {
370
370
  this.config = config;
371
371
  this.signal = signal;
372
372
  this.responseSchema = getCachedGeminiResponseSchema(config);
373
- this.onLog = dependencies.onLog;
374
373
  this.reportProgress = dependencies.reportProgress;
375
374
  this.statusReporter = dependencies.statusReporter;
376
375
  this.progressContext = DEFAULT_PROGRESS_CONTEXT;
376
+ this.onLog = async (level, data) => {
377
+ try {
378
+ await dependencies.onLog(level, data);
379
+ }
380
+ catch {
381
+ // Ignore logging failures
382
+ }
383
+ await this.handleInternalLog(data);
384
+ };
385
+ }
386
+ async handleInternalLog(data) {
387
+ const record = asObjectRecord(data);
388
+ if (record.event === 'gemini_retry') {
389
+ const details = asObjectRecord(record.details);
390
+ const { attempt } = details;
391
+ const msg = `Network error. Retrying (attempt ${String(attempt)})...`;
392
+ await reportProgressStepUpdate(this.reportProgress, this.config.name, this.progressContext, STEP_CALLING_MODEL, msg);
393
+ await this.updateStatusMessage(msg);
394
+ }
395
+ else if (record.event === 'gemini_queue_acquired') {
396
+ const msg = 'Model queue acquired, generating response...';
397
+ await reportProgressStepUpdate(this.reportProgress, this.config.name, this.progressContext, STEP_CALLING_MODEL, msg);
398
+ await this.updateStatusMessage(msg);
399
+ }
377
400
  }
378
401
  setResponseSchemaOverride(responseSchema) {
379
402
  this.responseSchema = responseSchema;
@@ -440,8 +463,8 @@ export class ToolExecutionRunner {
440
463
  try {
441
464
  const raw = await generateStructuredJson(createGenerationRequest(this.config, { systemInstruction, prompt: retryPrompt }, this.responseSchema, this.onLog, this.signal));
442
465
  if (attempt === 0) {
443
- await this.updateStatusMessage('validating response');
444
- await reportProgressStepUpdate(this.reportProgress, this.config.name, this.progressContext, STEP_VALIDATING_RESPONSE, 'validating response');
466
+ await this.updateStatusMessage('Verifying output structure...');
467
+ await reportProgressStepUpdate(this.reportProgress, this.config.name, this.progressContext, STEP_VALIDATING_RESPONSE, 'Verifying output structure...');
445
468
  }
446
469
  parsed = this.config.resultSchema.parse(raw);
447
470
  break;
@@ -476,23 +499,23 @@ export class ToolExecutionRunner {
476
499
  const ctx = {
477
500
  diffSlot: this.hasSnapshot ? this.diffSlotSnapshot : getDiff(),
478
501
  };
479
- await reportProgressStepUpdate(this.reportProgress, this.config.name, this.progressContext, STEP_STARTING, 'starting');
480
- await this.updateStatusMessage('starting');
481
- await reportProgressStepUpdate(this.reportProgress, this.config.name, this.progressContext, STEP_VALIDATING, 'validating input');
482
- await this.updateStatusMessage('validating input');
502
+ await reportProgressStepUpdate(this.reportProgress, this.config.name, this.progressContext, STEP_STARTING, 'Initializing...');
503
+ await this.updateStatusMessage('Initializing...');
504
+ await reportProgressStepUpdate(this.reportProgress, this.config.name, this.progressContext, STEP_VALIDATING, 'Validating request parameters...');
505
+ await this.updateStatusMessage('Validating request parameters...');
483
506
  const validationError = await this.executeValidation(inputRecord, ctx);
484
507
  if (validationError) {
485
508
  return validationError;
486
509
  }
487
- await reportProgressStepUpdate(this.reportProgress, this.config.name, this.progressContext, STEP_BUILDING_PROMPT, 'building prompt');
488
- await this.updateStatusMessage('building prompt');
510
+ await reportProgressStepUpdate(this.reportProgress, this.config.name, this.progressContext, STEP_BUILDING_PROMPT, 'Constructing analysis context...');
511
+ await this.updateStatusMessage('Constructing analysis context...');
489
512
  const promptParts = this.config.buildPrompt(inputRecord, ctx);
490
513
  const { prompt, systemInstruction } = promptParts;
491
- await reportProgressStepUpdate(this.reportProgress, this.config.name, this.progressContext, STEP_CALLING_MODEL, 'calling model');
492
- await this.updateStatusMessage('calling model');
514
+ await reportProgressStepUpdate(this.reportProgress, this.config.name, this.progressContext, STEP_CALLING_MODEL, 'Querying Gemini model...');
515
+ await this.updateStatusMessage('Querying Gemini model...');
493
516
  const parsed = await this.executeModelCall(systemInstruction, prompt);
494
- await reportProgressStepUpdate(this.reportProgress, this.config.name, this.progressContext, STEP_FINALIZING, 'finalizing');
495
- await this.updateStatusMessage('finalizing');
517
+ await reportProgressStepUpdate(this.reportProgress, this.config.name, this.progressContext, STEP_FINALIZING, 'Processing results...');
518
+ await this.updateStatusMessage('Processing results...');
496
519
  const finalResult = (this.config.transformResult
497
520
  ? this.config.transformResult(inputRecord, parsed, ctx)
498
521
  : parsed);
@@ -503,7 +526,7 @@ export class ToolExecutionRunner {
503
526
  const ageMs = Date.now() - new Date(ctx.diffSlot.generatedAt).getTime();
504
527
  if (ageMs > diffStaleWarningMs.get()) {
505
528
  const ageMinutes = Math.round(ageMs / 60_000);
506
- const warning = `\n\n⚠️ Warning: The analyzed diff is over ${ageMinutes} minutes old. If you have made recent changes, please run generate_diff again.`;
529
+ const warning = `\n\nWarning: The analyzed diff is over ${ageMinutes} minutes old. If you have made recent changes, please run generate_diff again.`;
507
530
  textContent = textContent ? textContent + warning : warning;
508
531
  }
509
532
  }
@@ -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 (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.',
20
+ security: 'Focus: Injection, Auth, Crypto, OWASP.',
21
+ correctness: 'Focus: Logic, Edge Cases, Types.',
22
+ performance: 'Focus: Complexity, Memory, Latency.',
23
+ regressions: 'Focus: Behavior Changes, Breaking APIs.',
24
+ tests: 'Focus: Coverage, Error Paths.',
25
+ maintainability: 'Focus: Complexity, Readability, Patterns.',
26
+ concurrency: 'Focus: Races, Deadlocks, Atomicity.',
27
27
  };
28
28
  function isFocusArea(value) {
29
29
  return INSPECTION_FOCUS_AREAS.includes(value);
@@ -3,11 +3,23 @@ import { registerStructuredToolTask } from '../lib/tool-factory.js';
3
3
  import { AnalyzeComplexityInputSchema } from '../schemas/inputs.js';
4
4
  import { AnalyzeComplexityResultSchema } from '../schemas/outputs.js';
5
5
  const SYSTEM_INSTRUCTION = `
6
+ <role>
6
7
  Algorithm Complexity Analyst.
7
- Analyze Big-O time/space complexity for changes.
8
- Detect performance degradation vs original.
9
- Identify bottlenecks: nested loops, unbounded recursion, heavy allocations.
10
- Return strict JSON.
8
+ You are an expert in Big-O analysis and performance optimization.
9
+ </role>
10
+
11
+ <task>
12
+ Analyze the time and space complexity of the code changes:
13
+ - Compare new complexity vs. original implementation.
14
+ - Detect performance degradation (regression).
15
+ - Identify bottlenecks (nested loops, recursion, allocations).
16
+ </task>
17
+
18
+ <constraints>
19
+ - Focus on the changed code paths.
20
+ - Flag degradation only if complexity class worsens (e.g., O(n) -> O(n^2)).
21
+ - Return valid JSON matching the schema.
22
+ </constraints>
11
23
  `;
12
24
  const TOOL_CONTRACT = requireToolContract('analyze_time_space_complexity');
13
25
  export function registerAnalyzeComplexityTool(server) {
@@ -23,6 +35,7 @@ export function registerAnalyzeComplexityTool(server) {
23
35
  maxOutputTokens: TOOL_CONTRACT.maxOutputTokens,
24
36
  ...buildStructuredToolRuntimeOptions(TOOL_CONTRACT),
25
37
  requiresDiff: true,
38
+ progressContext: (input) => input.language ?? 'auto-detect',
26
39
  formatOutcome: (result) => result.isDegradation
27
40
  ? 'Performance degradation detected'
28
41
  : 'No degradation',
@@ -4,10 +4,24 @@ import { registerStructuredToolTask } from '../lib/tool-factory.js';
4
4
  import { AnalyzePrImpactInputSchema } from '../schemas/inputs.js';
5
5
  import { PrImpactResultSchema } from '../schemas/outputs.js';
6
6
  const SYSTEM_INSTRUCTION = `
7
+ <role>
7
8
  Technical Change Analyst.
8
- Assess objective impact: severity, risk categories, breaking changes, rollback cost.
9
- Strictly diff-based; no inference.
10
- Return strict JSON.
9
+ You are a strict, objective auditor of code changes.
10
+ </role>
11
+
12
+ <task>
13
+ Analyze the unified diff to assess:
14
+ - Severity (low/medium/high/critical)
15
+ - Risk categories (security, stability, etc.)
16
+ - Breaking changes (API, contract, schema)
17
+ - Rollback complexity
18
+ </task>
19
+
20
+ <constraints>
21
+ - Base analysis ONLY on the provided diff. No external inference.
22
+ - Ignore formatting/style changes unless they affect logic.
23
+ - Return valid JSON matching the schema.
24
+ </constraints>
11
25
  `;
12
26
  const TOOL_CONTRACT = requireToolContract('analyze_pr_impact');
13
27
  function formatLanguageSegment(language) {
@@ -3,11 +3,22 @@ import { registerStructuredToolTask } from '../lib/tool-factory.js';
3
3
  import { DetectApiBreakingInputSchema } from '../schemas/inputs.js';
4
4
  import { DetectApiBreakingResultSchema } from '../schemas/outputs.js';
5
5
  const SYSTEM_INSTRUCTION = `
6
+ <role>
6
7
  API Compatibility Analyst.
7
- Detect breaking changes to public APIs/contracts/interfaces.
8
- Definition: Breaking change = requires consumer code modification.
9
- Output: element, nature, impact, mitigation.
10
- Return strict JSON.
8
+ You are a strict guardian of public interfaces and contracts.
9
+ </role>
10
+
11
+ <task>
12
+ Detect breaking changes in public APIs, interfaces, or schemas:
13
+ - Identify changes that require consumer code modification.
14
+ - classify the nature, impact, and mitigation for each break.
15
+ </task>
16
+
17
+ <constraints>
18
+ - Definition: Breaking change = backwards-incompatible modification.
19
+ - Ignore internal/private APIs unless exported.
20
+ - Return valid JSON matching the schema.
21
+ </constraints>
11
22
  `;
12
23
  const TOOL_CONTRACT = requireToolContract('detect_api_breaking_changes');
13
24
  export function registerDetectApiBreakingTool(server) {
@@ -23,6 +34,7 @@ export function registerDetectApiBreakingTool(server) {
23
34
  maxOutputTokens: TOOL_CONTRACT.maxOutputTokens,
24
35
  ...buildStructuredToolRuntimeOptions(TOOL_CONTRACT),
25
36
  requiresDiff: true,
37
+ progressContext: (input) => input.language ?? 'auto-detect',
26
38
  formatOutcome: (result) => `${result.breakingChanges.length} breaking change(s) found`,
27
39
  formatOutput: (result) => result.hasBreakingChanges
28
40
  ? `${result.breakingChanges.length} breaking changes found.`
@@ -7,10 +7,23 @@ const ReviewSummaryModelSchema = ReviewSummaryResultSchema.omit({
7
7
  });
8
8
  const TOOL_CONTRACT = requireToolContract('generate_review_summary');
9
9
  const SYSTEM_INSTRUCTION = `
10
+ <role>
10
11
  Senior Code Reviewer.
11
- Summarize PR: risk, key changes, merge recommendation (merge/squash/block).
12
- Focus: Logic/behavior changes. Ignore: formatting/style/typos.
13
- Return strict JSON.
12
+ You are a pragmatic engineer focused on stability and maintainability.
13
+ </role>
14
+
15
+ <task>
16
+ Summarize the pull request based on the diff:
17
+ - Assess overall risk (low/medium/high).
18
+ - Highlight key logic/behavior changes.
19
+ - Recommend action: merge, squash, or block.
20
+ </task>
21
+
22
+ <constraints>
23
+ - Focus on logic and behavior; ignore style, formatting, and typos.
24
+ - Be concise and actionable.
25
+ - Return valid JSON matching the schema.
26
+ </constraints>
14
27
  `;
15
28
  function formatLanguageSegment(language) {
16
29
  return language ? `\nLanguage: ${language}` : '';
@@ -4,11 +4,22 @@ import { registerStructuredToolTask } from '../lib/tool-factory.js';
4
4
  import { GenerateTestPlanInputSchema } from '../schemas/inputs.js';
5
5
  import { TestPlanResultSchema } from '../schemas/outputs.js';
6
6
  const SYSTEM_INSTRUCTION = `
7
+ <role>
7
8
  QA Automation Architect.
8
- Generate test plan for diff.
9
- Prioritize: negative cases, edge cases, branch coverage, integration points.
10
- Focus: observable behavior only.
11
- Return strict JSON.
9
+ You are an expert in test strategy and coverage analysis.
10
+ </role>
11
+
12
+ <task>
13
+ Generate a prioritized test plan for the provided diff:
14
+ - Focus on negative cases, edge cases, and boundary conditions.
15
+ - Target branch coverage and integration points.
16
+ </task>
17
+
18
+ <constraints>
19
+ - Focus on observable behavior changes.
20
+ - Ignore internal refactors that do not affect contract.
21
+ - Return valid JSON matching the schema.
22
+ </constraints>
12
23
  `;
13
24
  const TOOL_CONTRACT = requireToolContract('generate_test_plan');
14
25
  function formatOptionalLine(label, value) {
@@ -5,11 +5,24 @@ import { InspectCodeQualityInputSchema } from '../schemas/inputs.js';
5
5
  import { CodeQualityOutputSchema, CodeQualityResultSchema, } from '../schemas/outputs.js';
6
6
  const DEFAULT_FOCUS_AREAS = 'General';
7
7
  const SYSTEM_INSTRUCTION = `
8
- Principal Engineer Code Review.
9
- Source: Unified diff.
10
- Goal: Identify bugs, security, performance, maintainability.
11
- Constraint: Ignore style/formatting. Prioritize correctness/failure modes.
12
- Return strict JSON.
8
+ <role>
9
+ Principal Engineer.
10
+ You are an expert in code quality, security, and performance.
11
+ </role>
12
+
13
+ <task>
14
+ Perform a deep code review of the provided diff:
15
+ - Identify bugs, security vulnerabilities, and performance issues.
16
+ - Assess maintainability and clarity.
17
+ - Provide contextual insights if full file content is available.
18
+ </task>
19
+
20
+ <constraints>
21
+ - Ignore style/formatting/whitespace changes.
22
+ - Prioritize correctness and failure modes over opinionated patterns.
23
+ - Findings must be actionable and specific to the diff.
24
+ - Return valid JSON matching the schema.
25
+ </constraints>
13
26
  `;
14
27
  const TOOL_CONTRACT = requireToolContract('inspect_code_quality');
15
28
  function formatOptionalLine(label, value) {
@@ -32,6 +45,12 @@ export function registerInspectCodeQualityTool(server) {
32
45
  maxOutputTokens: TOOL_CONTRACT.maxOutputTokens,
33
46
  ...buildStructuredToolRuntimeOptions(TOOL_CONTRACT),
34
47
  requiresDiff: true,
48
+ progressContext: (input) => {
49
+ const focus = input.focusAreas
50
+ ? `[${input.focusAreas.join(',')}]`
51
+ : 'general';
52
+ return `${input.repository} ${focus}`;
53
+ },
35
54
  formatOutcome: (result) => `${result.findings.length} findings, risk: ${result.overallRisk}`,
36
55
  formatOutput: (result) => {
37
56
  const count = result.findings.length;
@@ -4,11 +4,23 @@ import { registerStructuredToolTask } from '../lib/tool-factory.js';
4
4
  import { SuggestSearchReplaceInputSchema } from '../schemas/inputs.js';
5
5
  import { SearchReplaceResultSchema } from '../schemas/outputs.js';
6
6
  const SYSTEM_INSTRUCTION = `
7
+ <role>
7
8
  Code Remediation Expert.
8
- Generate minimal search/replace blocks for described issue.
9
- Constraint: 'search' must be verbatim (exact whitespace/indentation).
10
- Constraint: No context drift. Omit patch if exact match uncertain.
11
- Return strict JSON.
9
+ You are a precise tool for generating automated code fixes.
10
+ </role>
11
+
12
+ <task>
13
+ Generate minimal search-and-replace blocks to fix the described issue:
14
+ - 'search' block must match the existing code EXACTLY (including whitespace).
15
+ - 'replace' block must be syntactically correct and follow local style.
16
+ </task>
17
+
18
+ <constraints>
19
+ - Verbatim match required: preserve all indentation and newlines in 'search'.
20
+ - Do not include surrounding code unless necessary for uniqueness.
21
+ - If exact match is ambiguous, return an empty block list.
22
+ - Return valid JSON matching the schema.
23
+ </constraints>
12
24
  `;
13
25
  const TOOL_CONTRACT = requireToolContract('suggest_search_replace');
14
26
  function formatPatchCount(count) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@j0hanz/code-review-analyst-mcp",
3
- "version": "1.7.1",
3
+ "version": "1.7.3",
4
4
  "mcpName": "io.github.j0hanz/code-review-analyst",
5
5
  "description": "Gemini-powered MCP server for code review analysis.",
6
6
  "type": "module",