@j0hanz/code-review-analyst-mcp 1.4.0 → 1.4.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.
@@ -1,11 +1,39 @@
1
1
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import type { ZodRawShapeCompat } from '@modelcontextprotocol/sdk/server/zod-compat.js';
3
+ import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
3
4
  import { z } from 'zod';
5
+ import { type DiffSlot } from './diff-store.js';
4
6
  import { createErrorToolResponse } from './tool-response.js';
5
7
  export interface PromptParts {
6
8
  systemInstruction: string;
7
9
  prompt: string;
8
10
  }
11
+ /**
12
+ * Immutable snapshot of server-side state captured once at the start of a
13
+ * tool execution, before `validateInput` runs. Threading it through both
14
+ * `validateInput` and `buildPrompt` eliminates the TOCTOU gap that would
15
+ * otherwise allow a concurrent `generate_diff` call to replace the cached
16
+ * diff between the budget check and prompt assembly.
17
+ */
18
+ export interface ToolExecutionContext {
19
+ readonly diffSlot: DiffSlot | undefined;
20
+ }
21
+ type ProgressToken = string | number;
22
+ interface ProgressNotificationParams {
23
+ progressToken: ProgressToken;
24
+ progress: number;
25
+ total?: number;
26
+ message?: string;
27
+ }
28
+ interface ProgressExtra {
29
+ _meta?: {
30
+ progressToken?: unknown;
31
+ };
32
+ sendNotification: (notification: {
33
+ method: 'notifications/progress';
34
+ params: ProgressNotificationParams;
35
+ }) => Promise<void>;
36
+ }
9
37
  export interface StructuredToolTaskConfig<TInput extends object = Record<string, unknown>, TResult extends object = Record<string, unknown>, TFinal extends TResult = TResult> {
10
38
  /** Tool name registered with the MCP server (e.g. 'analyze_pr_impact'). */
11
39
  name: string;
@@ -24,9 +52,9 @@ export interface StructuredToolTaskConfig<TInput extends object = Record<string,
24
52
  /** Stable error code returned on failure (e.g. 'E_INSPECT_QUALITY'). */
25
53
  errorCode: string;
26
54
  /** Optional post-processing hook called after resultSchema.parse(). The return value replaces the parsed result. */
27
- transformResult?: (input: TInput, result: TResult) => TFinal;
55
+ transformResult?: (input: TInput, result: TResult, ctx: ToolExecutionContext) => TFinal;
28
56
  /** Optional validation hook for input parameters. */
29
- validateInput?: (input: TInput) => Promise<ReturnType<typeof createErrorToolResponse> | undefined> | ReturnType<typeof createErrorToolResponse> | undefined;
57
+ validateInput?: (input: TInput, ctx: ToolExecutionContext) => Promise<ReturnType<typeof createErrorToolResponse> | undefined> | ReturnType<typeof createErrorToolResponse> | undefined;
30
58
  /** Optional Gemini model to use (e.g. 'gemini-2.5-pro'). */
31
59
  model?: string;
32
60
  /** Optional thinking budget in tokens. */
@@ -44,6 +72,11 @@ export interface StructuredToolTaskConfig<TInput extends object = Record<string,
44
72
  /** Optional short outcome suffix for the completion progress message (e.g., "3 findings"). */
45
73
  formatOutcome?: (result: TFinal) => string;
46
74
  /** Builds the system instruction and user prompt from parsed tool input. */
47
- buildPrompt: (input: TInput) => PromptParts;
75
+ buildPrompt: (input: TInput, ctx: ToolExecutionContext) => PromptParts;
48
76
  }
77
+ export declare function wrapToolHandler<TInput, TResult extends CallToolResult>(options: {
78
+ toolName: string;
79
+ progressContext?: (input: TInput) => string;
80
+ }, handler: (input: TInput, extra: ProgressExtra) => Promise<TResult> | TResult): (input: TInput, extra: ProgressExtra) => Promise<TResult>;
49
81
  export declare function registerStructuredToolTask<TInput extends object, TResult extends object = Record<string, unknown>, TFinal extends TResult = TResult>(server: McpServer, config: StructuredToolTaskConfig<TInput, TResult, TFinal>): void;
82
+ export {};
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import { DefaultOutputSchema } from '../schemas/outputs.js';
3
+ import { getDiff } from './diff-store.js';
3
4
  import { getErrorMessage, RETRYABLE_UPSTREAM_ERROR_PATTERN } from './errors.js';
4
5
  import { stripJsonSchemaConstraints } from './gemini-schema.js';
5
6
  import { generateStructuredJson, getCurrentRequestId } from './gemini.js';
@@ -217,6 +218,43 @@ function createGeminiLogger(server, taskId) {
217
218
  }
218
219
  };
219
220
  }
221
+ export function wrapToolHandler(options, handler) {
222
+ return async (input, extra) => {
223
+ const context = normalizeProgressContext(options.progressContext?.(input));
224
+ // Start progress (0/1)
225
+ await sendTaskProgress(extra, {
226
+ current: 0,
227
+ total: 1,
228
+ message: formatProgressStep(options.toolName, context, 'starting'),
229
+ });
230
+ try {
231
+ const result = await handler(input, extra);
232
+ // End progress (1/1)
233
+ const outcome = result.isError ? 'failed' : 'completed';
234
+ const success = !result.isError;
235
+ await sendTaskProgress(extra, {
236
+ current: 1,
237
+ total: 1,
238
+ message: formatProgressCompletion(options.toolName, context, outcome, success),
239
+ });
240
+ return result;
241
+ }
242
+ catch (error) {
243
+ // Progress is best-effort; must never mask the original error.
244
+ try {
245
+ await sendTaskProgress(extra, {
246
+ current: 1,
247
+ total: 1,
248
+ message: formatProgressCompletion(options.toolName, context, 'failed', false),
249
+ });
250
+ }
251
+ catch {
252
+ // Swallow progress delivery errors so the original error propagates.
253
+ }
254
+ throw error;
255
+ }
256
+ };
257
+ }
220
258
  export function registerStructuredToolTask(server, config) {
221
259
  const responseSchema = createGeminiResponseSchema({
222
260
  geminiSchema: config.geminiSchema,
@@ -260,9 +298,14 @@ export function registerStructuredToolTask(server, config) {
260
298
  const onLog = createGeminiLogger(server, task.taskId);
261
299
  const inputRecord = parseToolInput(input, config.fullInputSchema);
262
300
  progressContext = normalizeProgressContext(config.progressContext?.(inputRecord));
301
+ // Snapshot the diff slot ONCE before any async work so that
302
+ // validateInput and buildPrompt observe the same state. Without
303
+ // this, a concurrent generate_diff call between the two awaits
304
+ // could replace the slot and silently bypass the budget check.
305
+ const ctx = { diffSlot: getDiff() };
263
306
  await reportProgressStepUpdate(reportProgress, config.name, progressContext, 0, 'starting');
264
307
  if (config.validateInput) {
265
- const validationError = await config.validateInput(inputRecord);
308
+ const validationError = await config.validateInput(inputRecord, ctx);
266
309
  if (validationError) {
267
310
  const validationMessage = validationError.structuredContent.error?.message ??
268
311
  INPUT_VALIDATION_FAILED;
@@ -273,7 +316,7 @@ export function registerStructuredToolTask(server, config) {
273
316
  }
274
317
  }
275
318
  await reportProgressStepUpdate(reportProgress, config.name, progressContext, 1, 'preparing');
276
- const promptParts = config.buildPrompt(inputRecord);
319
+ const promptParts = config.buildPrompt(inputRecord, ctx);
277
320
  const { prompt } = promptParts;
278
321
  const { systemInstruction } = promptParts;
279
322
  const modelLabel = friendlyModelName(config.model);
@@ -306,7 +349,7 @@ export function registerStructuredToolTask(server, config) {
306
349
  throw new Error('Unexpected state: parsed result is undefined');
307
350
  }
308
351
  const finalResult = (config.transformResult
309
- ? config.transformResult(inputRecord, parsed)
352
+ ? config.transformResult(inputRecord, parsed, ctx)
310
353
  : parsed);
311
354
  const textContent = config.formatOutput
312
355
  ? config.formatOutput(finalResult)
@@ -20,57 +20,59 @@ function createBoundedString(min, max, description) {
20
20
  function createOptionalBoundedString(min, max, description) {
21
21
  return createBoundedString(min, max, description).optional();
22
22
  }
23
- function createLanguageSchema(description) {
24
- return createOptionalBoundedString(INPUT_LIMITS.language.min, INPUT_LIMITS.language.max, description);
23
+ const LANGUAGE_DESCRIPTION = 'Primary programming language (e.g. TypeScript, Python, Rust). Auto-infer from file extensions. Omit if multi-language.';
24
+ const REPOSITORY_DESCRIPTION = 'Repository identifier (owner/repo). Auto-infer from git remote or directory name.';
25
+ function createLanguageSchema() {
26
+ return createOptionalBoundedString(INPUT_LIMITS.language.min, INPUT_LIMITS.language.max, LANGUAGE_DESCRIPTION);
25
27
  }
26
28
  function createRepositorySchema() {
27
- return createBoundedString(INPUT_LIMITS.repository.min, INPUT_LIMITS.repository.max, 'Repository identifier, e.g. org/repo.');
29
+ return createBoundedString(INPUT_LIMITS.repository.min, INPUT_LIMITS.repository.max, REPOSITORY_DESCRIPTION);
28
30
  }
29
31
  function createOptionalBoundedInteger(min, max, description) {
30
32
  return z.number().int().min(min).max(max).optional().describe(description);
31
33
  }
32
34
  export const FileContextSchema = z.strictObject({
33
- path: createBoundedString(INPUT_LIMITS.fileContext.path.min, INPUT_LIMITS.fileContext.path.max, 'File path relative to repo root.'),
35
+ path: createBoundedString(INPUT_LIMITS.fileContext.path.min, INPUT_LIMITS.fileContext.path.max, 'Repo-relative path (e.g. src/utils/helpers.ts).'),
34
36
  content: createBoundedString(INPUT_LIMITS.fileContext.content.min, INPUT_LIMITS.fileContext.content.max, 'Full file content.'),
35
37
  });
36
38
  export const AnalyzePrImpactInputSchema = z.strictObject({
37
39
  repository: createRepositorySchema(),
38
- language: createLanguageSchema('Primary language to bias analysis.'),
40
+ language: createLanguageSchema(),
39
41
  });
40
42
  export const GenerateReviewSummaryInputSchema = z.strictObject({
41
43
  repository: createRepositorySchema(),
42
- language: createLanguageSchema('Primary implementation language.'),
44
+ language: createLanguageSchema(),
43
45
  });
44
46
  export const InspectCodeQualityInputSchema = z.strictObject({
45
47
  repository: createRepositorySchema(),
46
- language: createLanguageSchema('Primary language.'),
48
+ language: createLanguageSchema(),
47
49
  focusAreas: z
48
- .array(createBoundedString(INPUT_LIMITS.focusArea.min, INPUT_LIMITS.focusArea.max, 'Focus area tag value.'))
50
+ .array(createBoundedString(INPUT_LIMITS.focusArea.min, INPUT_LIMITS.focusArea.max, 'Focus tag (e.g. security, performance, bug, logic).'))
49
51
  .min(1)
50
52
  .max(INPUT_LIMITS.focusArea.maxItems)
51
53
  .optional()
52
- .describe('Specific areas to inspect: security, correctness, etc.'),
53
- maxFindings: createOptionalBoundedInteger(INPUT_LIMITS.maxFindings.min, INPUT_LIMITS.maxFindings.max, 'Maximum number of findings to return.'),
54
+ .describe('Review focus areas. Standard tags: security, performance, correctness, maintainability, concurrency. Omit for general review.'),
55
+ maxFindings: createOptionalBoundedInteger(INPUT_LIMITS.maxFindings.min, INPUT_LIMITS.maxFindings.max, 'Max findings (1-25). Default: 10.'),
54
56
  files: z
55
57
  .array(FileContextSchema)
56
58
  .min(1)
57
59
  .max(INPUT_LIMITS.fileContext.maxItems)
58
60
  .optional()
59
- .describe('Full file contents for context-aware analysis. Provide the files changed in the diff for best results.'),
61
+ .describe('Full content of changed files. Highly recommended for accurate analysis. Omit if unavailable.'),
60
62
  });
61
63
  export const SuggestSearchReplaceInputSchema = z.strictObject({
62
- findingTitle: createBoundedString(INPUT_LIMITS.findingTitle.min, INPUT_LIMITS.findingTitle.max, 'Short title of the finding to fix.'),
63
- findingDetails: createBoundedString(INPUT_LIMITS.findingDetails.min, INPUT_LIMITS.findingDetails.max, 'Detailed explanation of the bug or risk.'),
64
+ findingTitle: createBoundedString(INPUT_LIMITS.findingTitle.min, INPUT_LIMITS.findingTitle.max, 'Exact finding title from inspect_code_quality.'),
65
+ findingDetails: createBoundedString(INPUT_LIMITS.findingDetails.min, INPUT_LIMITS.findingDetails.max, 'Exact finding explanation from inspect_code_quality.'),
64
66
  });
65
67
  export const GenerateTestPlanInputSchema = z.strictObject({
66
68
  repository: createRepositorySchema(),
67
- language: createLanguageSchema('Primary language.'),
68
- testFramework: createOptionalBoundedString(INPUT_LIMITS.testFramework.min, INPUT_LIMITS.testFramework.max, 'Test framework to use, e.g. jest, vitest, pytest, node:test.'),
69
- maxTestCases: createOptionalBoundedInteger(INPUT_LIMITS.maxTestCases.min, INPUT_LIMITS.maxTestCases.max, 'Maximum number of test cases to return.'),
69
+ language: createLanguageSchema(),
70
+ testFramework: createOptionalBoundedString(INPUT_LIMITS.testFramework.min, INPUT_LIMITS.testFramework.max, 'Test framework (jest, vitest, pytest, node:test, junit). Auto-infer from package.json/config.'),
71
+ maxTestCases: createOptionalBoundedInteger(INPUT_LIMITS.maxTestCases.min, INPUT_LIMITS.maxTestCases.max, 'Max test cases (1-30). Default: 15.'),
70
72
  });
71
73
  export const AnalyzeComplexityInputSchema = z.strictObject({
72
- language: createLanguageSchema('Primary language to bias analysis.'),
74
+ language: createLanguageSchema(),
73
75
  });
74
76
  export const DetectApiBreakingInputSchema = z.strictObject({
75
- language: createLanguageSchema('Primary language to bias analysis.'),
77
+ language: createLanguageSchema(),
76
78
  });
@@ -1,5 +1,5 @@
1
1
  import { validateDiffBudget } from '../lib/diff-budget.js';
2
- import { createNoDiffError, getDiff } from '../lib/diff-store.js';
2
+ import { createNoDiffError } from '../lib/diff-store.js';
3
3
  import { requireToolContract } from '../lib/tool-contracts.js';
4
4
  import { registerStructuredToolTask } from '../lib/tool-factory.js';
5
5
  import { AnalyzeComplexityInputSchema } from '../schemas/inputs.js';
@@ -15,7 +15,7 @@ export function registerAnalyzeComplexityTool(server) {
15
15
  registerStructuredToolTask(server, {
16
16
  name: 'analyze_time_space_complexity',
17
17
  title: 'Analyze Time & Space Complexity',
18
- description: 'Analyze Big-O complexity of the cached diff changes. Call generate_diff first.',
18
+ description: 'Analyze Big-O complexity. Prerequisite: generate_diff. Auto-infer language.',
19
19
  inputSchema: AnalyzeComplexityInputSchema,
20
20
  fullInputSchema: AnalyzeComplexityInputSchema,
21
21
  resultSchema: AnalyzeComplexityResultSchema,
@@ -26,8 +26,8 @@ export function registerAnalyzeComplexityTool(server) {
26
26
  ...(TOOL_CONTRACT.thinkingBudget !== undefined
27
27
  ? { thinkingBudget: TOOL_CONTRACT.thinkingBudget }
28
28
  : undefined),
29
- validateInput: () => {
30
- const slot = getDiff();
29
+ validateInput: (_input, ctx) => {
30
+ const slot = ctx.diffSlot;
31
31
  if (!slot)
32
32
  return createNoDiffError();
33
33
  return validateDiffBudget(slot.diff);
@@ -36,9 +36,8 @@ export function registerAnalyzeComplexityTool(server) {
36
36
  ? 'Performance degradation detected'
37
37
  : 'No degradation',
38
38
  formatOutput: (result) => `Complexity Analysis: Time=${result.timeComplexity}, Space=${result.spaceComplexity}. ${result.explanation}`,
39
- buildPrompt: (input) => {
40
- const slot = getDiff();
41
- const diff = slot?.diff ?? '';
39
+ buildPrompt: (input, ctx) => {
40
+ const diff = ctx.diffSlot?.diff ?? '';
42
41
  const languageLine = input.language
43
42
  ? `\nLanguage: ${input.language}`
44
43
  : '';
@@ -1,6 +1,6 @@
1
1
  import { validateDiffBudget } from '../lib/diff-budget.js';
2
2
  import { computeDiffStatsAndSummaryFromFiles, parseDiffFiles, } from '../lib/diff-parser.js';
3
- import { createNoDiffError, getDiff } from '../lib/diff-store.js';
3
+ import { createNoDiffError } from '../lib/diff-store.js';
4
4
  import { requireToolContract } from '../lib/tool-contracts.js';
5
5
  import { registerStructuredToolTask } from '../lib/tool-factory.js';
6
6
  import { AnalyzePrImpactInputSchema } from '../schemas/inputs.js';
@@ -19,7 +19,7 @@ export function registerAnalyzePrImpactTool(server) {
19
19
  registerStructuredToolTask(server, {
20
20
  name: 'analyze_pr_impact',
21
21
  title: 'Analyze PR Impact',
22
- description: 'Assess the impact and risk of the cached diff. Call generate_diff first.',
22
+ description: 'Assess impact and risk from cached diff. Prerequisite: generate_diff. Auto-infer repo/language.',
23
23
  inputSchema: AnalyzePrImpactInputSchema,
24
24
  fullInputSchema: AnalyzePrImpactInputSchema,
25
25
  resultSchema: PrImpactResultSchema,
@@ -27,17 +27,16 @@ export function registerAnalyzePrImpactTool(server) {
27
27
  model: TOOL_CONTRACT.model,
28
28
  timeoutMs: TOOL_CONTRACT.timeoutMs,
29
29
  maxOutputTokens: TOOL_CONTRACT.maxOutputTokens,
30
- validateInput: () => {
31
- const slot = getDiff();
30
+ validateInput: (_input, ctx) => {
31
+ const slot = ctx.diffSlot;
32
32
  if (!slot)
33
33
  return createNoDiffError();
34
34
  return validateDiffBudget(slot.diff);
35
35
  },
36
36
  formatOutcome: (result) => `severity: ${result.severity}`,
37
37
  formatOutput: (result) => `Impact Analysis (${result.severity}): ${result.summary}`,
38
- buildPrompt: (input) => {
39
- const slot = getDiff();
40
- const diff = slot?.diff ?? '';
38
+ buildPrompt: (input, ctx) => {
39
+ const diff = ctx.diffSlot?.diff ?? '';
41
40
  const files = parseDiffFiles(diff);
42
41
  const { stats, summary: fileSummary } = computeDiffStatsAndSummaryFromFiles(files);
43
42
  const languageSegment = formatLanguageSegment(input.language);
@@ -1,5 +1,5 @@
1
1
  import { validateDiffBudget } from '../lib/diff-budget.js';
2
- import { createNoDiffError, getDiff } from '../lib/diff-store.js';
2
+ import { createNoDiffError } from '../lib/diff-store.js';
3
3
  import { requireToolContract } from '../lib/tool-contracts.js';
4
4
  import { registerStructuredToolTask } from '../lib/tool-factory.js';
5
5
  import { DetectApiBreakingInputSchema } from '../schemas/inputs.js';
@@ -15,7 +15,7 @@ export function registerDetectApiBreakingTool(server) {
15
15
  registerStructuredToolTask(server, {
16
16
  name: 'detect_api_breaking_changes',
17
17
  title: 'Detect API Breaking Changes',
18
- description: 'Detect breaking changes to public APIs in the cached diff. Call generate_diff first.',
18
+ description: 'Detect breaking API changes. Prerequisite: generate_diff. Auto-infer language.',
19
19
  inputSchema: DetectApiBreakingInputSchema,
20
20
  fullInputSchema: DetectApiBreakingInputSchema,
21
21
  resultSchema: DetectApiBreakingResultSchema,
@@ -23,8 +23,8 @@ export function registerDetectApiBreakingTool(server) {
23
23
  model: TOOL_CONTRACT.model,
24
24
  timeoutMs: TOOL_CONTRACT.timeoutMs,
25
25
  maxOutputTokens: TOOL_CONTRACT.maxOutputTokens,
26
- validateInput: () => {
27
- const slot = getDiff();
26
+ validateInput: (_input, ctx) => {
27
+ const slot = ctx.diffSlot;
28
28
  if (!slot)
29
29
  return createNoDiffError();
30
30
  return validateDiffBudget(slot.diff);
@@ -33,9 +33,8 @@ export function registerDetectApiBreakingTool(server) {
33
33
  formatOutput: (result) => result.hasBreakingChanges
34
34
  ? `API Breaking Changes: ${result.breakingChanges.length} found.`
35
35
  : 'No API breaking changes detected.',
36
- buildPrompt: (input) => {
37
- const slot = getDiff();
38
- const diff = slot?.diff ?? '';
36
+ buildPrompt: (input, ctx) => {
37
+ const diff = ctx.diffSlot?.diff ?? '';
39
38
  const languageLine = input.language
40
39
  ? `\nLanguage: ${input.language}`
41
40
  : '';
@@ -3,6 +3,7 @@ import { z } from 'zod';
3
3
  import { cleanDiff, isEmptyDiff, NOISY_EXCLUDE_PATHSPECS, } from '../lib/diff-cleaner.js';
4
4
  import { computeDiffStatsFromFiles, parseDiffFiles, } from '../lib/diff-parser.js';
5
5
  import { DIFF_RESOURCE_URI, storeDiff } from '../lib/diff-store.js';
6
+ import { wrapToolHandler } from '../lib/tool-factory.js';
6
7
  import { createErrorToolResponse, createToolResponse, } from '../lib/tool-response.js';
7
8
  const GIT_TIMEOUT_MS = 30_000;
8
9
  const GIT_MAX_BUFFER = 10 * 1024 * 1024; // 10 MB
@@ -29,6 +30,9 @@ export function registerGenerateDiffTool(server) {
29
30
  .enum(['unstaged', 'staged'])
30
31
  .describe('"unstaged": working-tree changes not yet staged. "staged": changes added to the index with git add.'),
31
32
  },
33
+ }, wrapToolHandler({
34
+ toolName: 'generate_diff',
35
+ progressContext: (input) => input.mode,
32
36
  }, (input) => {
33
37
  const { mode } = input;
34
38
  const args = buildGitArgs(mode);
@@ -67,5 +71,5 @@ export function registerGenerateDiffTool(server) {
67
71
  message: summary,
68
72
  },
69
73
  }, summary);
70
- });
74
+ }));
71
75
  }
@@ -1,8 +1,8 @@
1
1
  import { validateDiffBudget } from '../lib/diff-budget.js';
2
2
  import { computeDiffStatsFromFiles, parseDiffFiles, } from '../lib/diff-parser.js';
3
- import { createNoDiffError, getDiff } from '../lib/diff-store.js';
3
+ import { createNoDiffError } from '../lib/diff-store.js';
4
4
  import { requireToolContract } from '../lib/tool-contracts.js';
5
- import { registerStructuredToolTask } from '../lib/tool-factory.js';
5
+ import { registerStructuredToolTask, } from '../lib/tool-factory.js';
6
6
  import { GenerateReviewSummaryInputSchema } from '../schemas/inputs.js';
7
7
  import { ReviewSummaryResultSchema } from '../schemas/outputs.js';
8
8
  const ReviewSummaryModelSchema = ReviewSummaryResultSchema.omit({
@@ -17,11 +17,16 @@ Return strict JSON only.
17
17
  function formatLanguageSegment(language) {
18
18
  return language ? `\nLanguage: ${language}` : '';
19
19
  }
20
+ function getDiffStats(ctx) {
21
+ const diff = ctx.diffSlot?.diff ?? '';
22
+ const { files, added, deleted } = computeDiffStatsFromFiles(parseDiffFiles(diff));
23
+ return { diff, files, added, deleted };
24
+ }
20
25
  export function registerGenerateReviewSummaryTool(server) {
21
26
  registerStructuredToolTask(server, {
22
27
  name: 'generate_review_summary',
23
28
  title: 'Generate Review Summary',
24
- description: 'Summarize the cached diff and assess high-level risk. Call generate_diff first.',
29
+ description: 'Summarize diff and risk level. Prerequisite: generate_diff. Auto-infer repo/language.',
25
30
  inputSchema: GenerateReviewSummaryInputSchema,
26
31
  fullInputSchema: GenerateReviewSummaryInputSchema,
27
32
  resultSchema: ReviewSummaryModelSchema,
@@ -29,39 +34,33 @@ export function registerGenerateReviewSummaryTool(server) {
29
34
  model: TOOL_CONTRACT.model,
30
35
  timeoutMs: TOOL_CONTRACT.timeoutMs,
31
36
  maxOutputTokens: TOOL_CONTRACT.maxOutputTokens,
32
- validateInput: () => {
33
- const slot = getDiff();
37
+ validateInput: (_input, ctx) => {
38
+ const slot = ctx.diffSlot;
34
39
  if (!slot)
35
40
  return createNoDiffError();
36
41
  return validateDiffBudget(slot.diff);
37
42
  },
38
43
  formatOutcome: (result) => `risk: ${result.overallRisk}`,
39
- transformResult: (input, result) => {
40
- const slot = getDiff();
41
- const diff = slot?.diff ?? '';
42
- const parsedFiles = parseDiffFiles(diff);
43
- const stats = computeDiffStatsFromFiles(parsedFiles);
44
+ transformResult: (_input, result, ctx) => {
45
+ const { files, added, deleted } = getDiffStats(ctx);
44
46
  return {
45
47
  ...result,
46
48
  stats: {
47
- filesChanged: stats.files,
48
- linesAdded: stats.added,
49
- linesRemoved: stats.deleted,
49
+ filesChanged: files,
50
+ linesAdded: added,
51
+ linesRemoved: deleted,
50
52
  },
51
53
  };
52
54
  },
53
55
  formatOutput: (result) => `Review Summary: ${result.summary}\nRecommendation: ${result.recommendation}`,
54
- buildPrompt: (input) => {
55
- const slot = getDiff();
56
- const diff = slot?.diff ?? '';
57
- const parsedFiles = parseDiffFiles(diff);
58
- const stats = computeDiffStatsFromFiles(parsedFiles);
56
+ buildPrompt: (input, ctx) => {
57
+ const { diff, files, added, deleted } = getDiffStats(ctx);
59
58
  const languageSegment = formatLanguageSegment(input.language);
60
59
  return {
61
60
  systemInstruction: SYSTEM_INSTRUCTION,
62
61
  prompt: `
63
62
  Repository: ${input.repository}${languageSegment}
64
- Stats: ${stats.files} files, +${stats.added}, -${stats.deleted}
63
+ Stats: ${files} files, +${added}, -${deleted}
65
64
 
66
65
  Diff:
67
66
  ${diff}
@@ -1,6 +1,6 @@
1
1
  import { validateDiffBudget } from '../lib/diff-budget.js';
2
2
  import { computeDiffStatsAndPathsFromFiles, parseDiffFiles, } from '../lib/diff-parser.js';
3
- import { createNoDiffError, getDiff } from '../lib/diff-store.js';
3
+ import { createNoDiffError } from '../lib/diff-store.js';
4
4
  import { requireToolContract } from '../lib/tool-contracts.js';
5
5
  import { registerStructuredToolTask } from '../lib/tool-factory.js';
6
6
  import { GenerateTestPlanInputSchema } from '../schemas/inputs.js';
@@ -19,7 +19,7 @@ export function registerGenerateTestPlanTool(server) {
19
19
  registerStructuredToolTask(server, {
20
20
  name: 'generate_test_plan',
21
21
  title: 'Generate Test Plan',
22
- description: 'Create a test plan covering the cached diff changes. Call generate_diff first.',
22
+ description: 'Generate test cases. Prerequisite: generate_diff. Auto-infer repo/language/framework.',
23
23
  inputSchema: GenerateTestPlanInputSchema,
24
24
  fullInputSchema: GenerateTestPlanInputSchema,
25
25
  resultSchema: TestPlanResultSchema,
@@ -30,8 +30,8 @@ export function registerGenerateTestPlanTool(server) {
30
30
  ...(TOOL_CONTRACT.thinkingBudget !== undefined
31
31
  ? { thinkingBudget: TOOL_CONTRACT.thinkingBudget }
32
32
  : undefined),
33
- validateInput: () => {
34
- const slot = getDiff();
33
+ validateInput: (_input, ctx) => {
34
+ const slot = ctx.diffSlot;
35
35
  if (!slot)
36
36
  return createNoDiffError();
37
37
  return validateDiffBudget(slot.diff);
@@ -42,9 +42,8 @@ export function registerGenerateTestPlanTool(server) {
42
42
  const cappedTestCases = result.testCases.slice(0, input.maxTestCases ?? result.testCases.length);
43
43
  return { ...result, testCases: cappedTestCases };
44
44
  },
45
- buildPrompt: (input) => {
46
- const slot = getDiff();
47
- const diff = slot?.diff ?? '';
45
+ buildPrompt: (input, ctx) => {
46
+ const diff = ctx.diffSlot?.diff ?? '';
48
47
  const parsedFiles = parseDiffFiles(diff);
49
48
  const { stats, paths } = computeDiffStatsAndPathsFromFiles(parsedFiles);
50
49
  const languageLine = formatOptionalLine('Language', input.language);
@@ -1,7 +1,7 @@
1
1
  import { validateContextBudget } from '../lib/context-budget.js';
2
2
  import { validateDiffBudget } from '../lib/diff-budget.js';
3
3
  import { computeDiffStatsAndSummaryFromFiles, parseDiffFiles, } from '../lib/diff-parser.js';
4
- import { createNoDiffError, getDiff } from '../lib/diff-store.js';
4
+ import { createNoDiffError } from '../lib/diff-store.js';
5
5
  import { requireToolContract } from '../lib/tool-contracts.js';
6
6
  import { registerStructuredToolTask } from '../lib/tool-factory.js';
7
7
  import { InspectCodeQualityInputSchema } from '../schemas/inputs.js';
@@ -54,7 +54,7 @@ export function registerInspectCodeQualityTool(server) {
54
54
  registerStructuredToolTask(server, {
55
55
  name: 'inspect_code_quality',
56
56
  title: 'Inspect Code Quality',
57
- description: 'Deep-dive code review with optional file context. Call generate_diff first.',
57
+ description: 'Deep code review. Prerequisite: generate_diff. Auto-infer repo/language/focus. Provide file content for best results.',
58
58
  inputSchema: InspectCodeQualityInputSchema,
59
59
  fullInputSchema: InspectCodeQualityInputSchema,
60
60
  resultSchema: CodeQualityOutputSchema,
@@ -70,8 +70,8 @@ export function registerInspectCodeQualityTool(server) {
70
70
  const fileCount = input.files?.length;
71
71
  return fileCount ? `+${fileCount} files` : '';
72
72
  },
73
- validateInput: (input) => {
74
- const slot = getDiff();
73
+ validateInput: (input, ctx) => {
74
+ const slot = ctx.diffSlot;
75
75
  if (!slot)
76
76
  return createNoDiffError();
77
77
  const diffError = validateDiffBudget(slot.diff);
@@ -93,9 +93,8 @@ export function registerInspectCodeQualityTool(server) {
93
93
  const cappedFindings = capFindings(result.findings, input.maxFindings);
94
94
  return { ...result, findings: cappedFindings, totalFindings };
95
95
  },
96
- buildPrompt: (input) => {
97
- const slot = getDiff();
98
- const diff = slot?.diff ?? '';
96
+ buildPrompt: (input, ctx) => {
97
+ const diff = ctx.diffSlot?.diff ?? '';
99
98
  const parsedFiles = parseDiffFiles(diff);
100
99
  const { summary: fileSummary } = computeDiffStatsAndSummaryFromFiles(parsedFiles);
101
100
  const fileContext = formatFileContext(input.files);
@@ -1,6 +1,6 @@
1
1
  import { validateDiffBudget } from '../lib/diff-budget.js';
2
2
  import { extractChangedPathsFromFiles, parseDiffFiles, } from '../lib/diff-parser.js';
3
- import { createNoDiffError, getDiff } from '../lib/diff-store.js';
3
+ import { createNoDiffError } from '../lib/diff-store.js';
4
4
  import { requireToolContract } from '../lib/tool-contracts.js';
5
5
  import { registerStructuredToolTask } from '../lib/tool-factory.js';
6
6
  import { SuggestSearchReplaceInputSchema } from '../schemas/inputs.js';
@@ -19,7 +19,7 @@ export function registerSuggestSearchReplaceTool(server) {
19
19
  registerStructuredToolTask(server, {
20
20
  name: 'suggest_search_replace',
21
21
  title: 'Suggest Search & Replace',
22
- description: 'Generate search-and-replace blocks to fix a finding from the cached diff. Call generate_diff first.',
22
+ description: 'Generate fix patches. Prerequisite: inspect_code_quality findings. Pass verbatim finding title/details.',
23
23
  inputSchema: SuggestSearchReplaceInputSchema,
24
24
  fullInputSchema: SuggestSearchReplaceInputSchema,
25
25
  resultSchema: SearchReplaceResultSchema,
@@ -30,8 +30,8 @@ export function registerSuggestSearchReplaceTool(server) {
30
30
  ...(TOOL_CONTRACT.thinkingBudget !== undefined
31
31
  ? { thinkingBudget: TOOL_CONTRACT.thinkingBudget }
32
32
  : undefined),
33
- validateInput: () => {
34
- const slot = getDiff();
33
+ validateInput: (_input, ctx) => {
34
+ const slot = ctx.diffSlot;
35
35
  if (!slot)
36
36
  return createNoDiffError();
37
37
  return validateDiffBudget(slot.diff);
@@ -42,9 +42,8 @@ export function registerSuggestSearchReplaceTool(server) {
42
42
  const patches = formatPatchCount(count);
43
43
  return `${result.summary}\n${patches} • Checklist: ${result.validationChecklist.join(' | ')}`;
44
44
  },
45
- buildPrompt: (input) => {
46
- const slot = getDiff();
47
- const diff = slot?.diff ?? '';
45
+ buildPrompt: (input, ctx) => {
46
+ const diff = ctx.diffSlot?.diff ?? '';
48
47
  const files = parseDiffFiles(diff);
49
48
  const paths = extractChangedPathsFromFiles(files);
50
49
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@j0hanz/code-review-analyst-mcp",
3
- "version": "1.4.0",
3
+ "version": "1.4.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",