@j0hanz/code-review-analyst-mcp 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.js CHANGED
@@ -2,6 +2,7 @@ import { InMemoryTaskStore } from '@modelcontextprotocol/sdk/experimental/tasks/
2
2
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
3
  import { readFileSync } from 'node:fs';
4
4
  import { findPackageJSON } from 'node:module';
5
+ import { initDiffStore } from './lib/diff-store.js';
5
6
  import { getErrorMessage } from './lib/errors.js';
6
7
  import { registerAllPrompts } from './prompts/index.js';
7
8
  import { registerAllResources } from './resources/index.js';
@@ -14,7 +15,7 @@ const VERSION_FIELD_ERROR = 'missing or invalid version field';
14
15
  const SERVER_CAPABILITIES = {
15
16
  logging: {},
16
17
  completions: {},
17
- resources: {},
18
+ resources: { subscribe: true },
18
19
  tools: {},
19
20
  tasks: {
20
21
  list: {},
@@ -80,6 +81,7 @@ function createMcpServer(taskStore) {
80
81
  });
81
82
  }
82
83
  function registerServerCapabilities(server) {
84
+ initDiffStore(server);
83
85
  registerAllTools(server);
84
86
  registerAllResources(server, SERVER_INSTRUCTIONS);
85
87
  registerAllPrompts(server, SERVER_INSTRUCTIONS);
@@ -1,4 +1,5 @@
1
1
  import { validateDiffBudget } from '../lib/diff-budget.js';
2
+ import { createNoDiffError, getDiff } from '../lib/diff-store.js';
2
3
  import { requireToolContract } from '../lib/tool-contracts.js';
3
4
  import { registerStructuredToolTask } from '../lib/tool-factory.js';
4
5
  import { AnalyzeComplexityInputSchema } from '../schemas/inputs.js';
@@ -10,18 +11,11 @@ Identify potential bottlenecks arising from loop nesting, recursive calls, and a
10
11
  Return strict JSON only.
11
12
  `;
12
13
  const TOOL_CONTRACT = requireToolContract('analyze_time_space_complexity');
13
- function formatOptionalLine(label, value) {
14
- return value === undefined ? '' : `\n${label}: ${value}`;
15
- }
16
- function buildAnalyzeComplexityPrompt(input) {
17
- const languageLine = formatOptionalLine('Language', input.language);
18
- return `${languageLine}\nDiff:\n${input.diff}`.trimStart();
19
- }
20
14
  export function registerAnalyzeComplexityTool(server) {
21
15
  registerStructuredToolTask(server, {
22
16
  name: 'analyze_time_space_complexity',
23
17
  title: 'Analyze Time & Space Complexity',
24
- description: 'Analyze Big-O time and space complexity of code changes and detect performance degradations.',
18
+ description: 'Analyze Big-O complexity of the cached diff changes. Call generate_diff first.',
25
19
  inputSchema: AnalyzeComplexityInputSchema,
26
20
  fullInputSchema: AnalyzeComplexityInputSchema,
27
21
  resultSchema: AnalyzeComplexityResultSchema,
@@ -32,14 +26,26 @@ export function registerAnalyzeComplexityTool(server) {
32
26
  ...(TOOL_CONTRACT.thinkingBudget !== undefined
33
27
  ? { thinkingBudget: TOOL_CONTRACT.thinkingBudget }
34
28
  : undefined),
35
- validateInput: (input) => validateDiffBudget(input.diff),
29
+ validateInput: () => {
30
+ const slot = getDiff();
31
+ if (!slot)
32
+ return createNoDiffError();
33
+ return validateDiffBudget(slot.diff);
34
+ },
36
35
  formatOutcome: (result) => result.isDegradation
37
36
  ? 'Performance degradation detected'
38
37
  : 'No degradation',
39
38
  formatOutput: (result) => `Complexity Analysis: Time=${result.timeComplexity}, Space=${result.spaceComplexity}. ${result.explanation}`,
40
- buildPrompt: (input) => ({
41
- systemInstruction: SYSTEM_INSTRUCTION,
42
- prompt: buildAnalyzeComplexityPrompt(input),
43
- }),
39
+ buildPrompt: (input) => {
40
+ const slot = getDiff();
41
+ const diff = slot?.diff ?? '';
42
+ const languageLine = input.language
43
+ ? `\nLanguage: ${input.language}`
44
+ : '';
45
+ return {
46
+ systemInstruction: SYSTEM_INSTRUCTION,
47
+ prompt: `${languageLine}\nDiff:\n${diff}`.trimStart(),
48
+ };
49
+ },
44
50
  });
45
51
  }
@@ -1,5 +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
4
  import { requireToolContract } from '../lib/tool-contracts.js';
4
5
  import { registerStructuredToolTask } from '../lib/tool-factory.js';
5
6
  import { AnalyzePrImpactInputSchema } from '../schemas/inputs.js';
@@ -14,25 +15,11 @@ const TOOL_CONTRACT = requireToolContract('analyze_pr_impact');
14
15
  function formatLanguageSegment(language) {
15
16
  return language ? `\nLanguage: ${language}` : '';
16
17
  }
17
- function buildAnalyzePrImpactPrompt(input) {
18
- const files = parseDiffFiles(input.diff);
19
- const { stats, summary: fileSummary } = computeDiffStatsAndSummaryFromFiles(files);
20
- const languageSegment = formatLanguageSegment(input.language);
21
- return `
22
- Repository: ${input.repository}${languageSegment}
23
- Change Stats: ${stats.files} files, +${stats.added} lines, -${stats.deleted} lines.
24
- Changed Files:
25
- ${fileSummary}
26
-
27
- Diff:
28
- ${input.diff}
29
- `;
30
- }
31
18
  export function registerAnalyzePrImpactTool(server) {
32
19
  registerStructuredToolTask(server, {
33
20
  name: 'analyze_pr_impact',
34
21
  title: 'Analyze PR Impact',
35
- description: 'Assess the impact and risk of a pull request diff.',
22
+ description: 'Assess the impact and risk of the cached diff. Call generate_diff first.',
36
23
  inputSchema: AnalyzePrImpactInputSchema,
37
24
  fullInputSchema: AnalyzePrImpactInputSchema,
38
25
  resultSchema: PrImpactResultSchema,
@@ -40,12 +27,32 @@ export function registerAnalyzePrImpactTool(server) {
40
27
  model: TOOL_CONTRACT.model,
41
28
  timeoutMs: TOOL_CONTRACT.timeoutMs,
42
29
  maxOutputTokens: TOOL_CONTRACT.maxOutputTokens,
43
- validateInput: (input) => validateDiffBudget(input.diff),
30
+ validateInput: () => {
31
+ const slot = getDiff();
32
+ if (!slot)
33
+ return createNoDiffError();
34
+ return validateDiffBudget(slot.diff);
35
+ },
44
36
  formatOutcome: (result) => `severity: ${result.severity}`,
45
37
  formatOutput: (result) => `Impact Analysis (${result.severity}): ${result.summary}`,
46
- buildPrompt: (input) => ({
47
- systemInstruction: SYSTEM_INSTRUCTION,
48
- prompt: buildAnalyzePrImpactPrompt(input),
49
- }),
38
+ buildPrompt: (input) => {
39
+ const slot = getDiff();
40
+ const diff = slot?.diff ?? '';
41
+ const files = parseDiffFiles(diff);
42
+ const { stats, summary: fileSummary } = computeDiffStatsAndSummaryFromFiles(files);
43
+ const languageSegment = formatLanguageSegment(input.language);
44
+ return {
45
+ systemInstruction: SYSTEM_INSTRUCTION,
46
+ prompt: `
47
+ Repository: ${input.repository}${languageSegment}
48
+ Change Stats: ${stats.files} files, +${stats.added} lines, -${stats.deleted} lines.
49
+ Changed Files:
50
+ ${fileSummary}
51
+
52
+ Diff:
53
+ ${diff}
54
+ `,
55
+ };
56
+ },
50
57
  });
51
58
  }
@@ -1,4 +1,5 @@
1
1
  import { validateDiffBudget } from '../lib/diff-budget.js';
2
+ import { createNoDiffError, getDiff } from '../lib/diff-store.js';
2
3
  import { requireToolContract } from '../lib/tool-contracts.js';
3
4
  import { registerStructuredToolTask } from '../lib/tool-factory.js';
4
5
  import { DetectApiBreakingInputSchema } from '../schemas/inputs.js';
@@ -10,18 +11,11 @@ Classify each breaking change with its affected element, nature of change, consu
10
11
  Return strict JSON only.
11
12
  `;
12
13
  const TOOL_CONTRACT = requireToolContract('detect_api_breaking_changes');
13
- function formatOptionalLine(label, value) {
14
- return value === undefined ? '' : `\n${label}: ${value}`;
15
- }
16
- function buildDetectApiBreakingPrompt(input) {
17
- const languageLine = formatOptionalLine('Language', input.language);
18
- return `${languageLine}\nDiff:\n${input.diff}`.trimStart();
19
- }
20
14
  export function registerDetectApiBreakingTool(server) {
21
15
  registerStructuredToolTask(server, {
22
16
  name: 'detect_api_breaking_changes',
23
17
  title: 'Detect API Breaking Changes',
24
- description: 'Detect breaking changes to public APIs, interfaces, and contracts in a unified diff.',
18
+ description: 'Detect breaking changes to public APIs in the cached diff. Call generate_diff first.',
25
19
  inputSchema: DetectApiBreakingInputSchema,
26
20
  fullInputSchema: DetectApiBreakingInputSchema,
27
21
  resultSchema: DetectApiBreakingResultSchema,
@@ -29,14 +23,26 @@ export function registerDetectApiBreakingTool(server) {
29
23
  model: TOOL_CONTRACT.model,
30
24
  timeoutMs: TOOL_CONTRACT.timeoutMs,
31
25
  maxOutputTokens: TOOL_CONTRACT.maxOutputTokens,
32
- validateInput: (input) => validateDiffBudget(input.diff),
26
+ validateInput: () => {
27
+ const slot = getDiff();
28
+ if (!slot)
29
+ return createNoDiffError();
30
+ return validateDiffBudget(slot.diff);
31
+ },
33
32
  formatOutcome: (result) => `${result.breakingChanges.length} breaking change(s) found`,
34
33
  formatOutput: (result) => result.hasBreakingChanges
35
34
  ? `API Breaking Changes: ${result.breakingChanges.length} found.`
36
35
  : 'No API breaking changes detected.',
37
- buildPrompt: (input) => ({
38
- systemInstruction: SYSTEM_INSTRUCTION,
39
- prompt: buildDetectApiBreakingPrompt(input),
40
- }),
36
+ buildPrompt: (input) => {
37
+ const slot = getDiff();
38
+ const diff = slot?.diff ?? '';
39
+ const languageLine = input.language
40
+ ? `\nLanguage: ${input.language}`
41
+ : '';
42
+ return {
43
+ systemInstruction: SYSTEM_INSTRUCTION,
44
+ prompt: `${languageLine}\nDiff:\n${diff}`.trimStart(),
45
+ };
46
+ },
41
47
  });
42
48
  }
@@ -0,0 +1,2 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerGenerateDiffTool(server: McpServer): void;
@@ -0,0 +1,71 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { z } from 'zod';
3
+ import { cleanDiff, isEmptyDiff, NOISY_EXCLUDE_PATHSPECS, } from '../lib/diff-cleaner.js';
4
+ import { computeDiffStatsFromFiles, parseDiffFiles, } from '../lib/diff-parser.js';
5
+ import { DIFF_RESOURCE_URI, storeDiff } from '../lib/diff-store.js';
6
+ import { createErrorToolResponse, createToolResponse, } from '../lib/tool-response.js';
7
+ const GIT_TIMEOUT_MS = 30_000;
8
+ const GIT_MAX_BUFFER = 10 * 1024 * 1024; // 10 MB
9
+ function buildGitArgs(mode) {
10
+ const args = ['diff', '--no-color', '--no-ext-diff'];
11
+ if (mode === 'staged') {
12
+ args.push('--cached');
13
+ }
14
+ // '--' separates flags from pathspecs. Everything after it is a
15
+ // pathspec, never interpreted as a flag — prevents flag injection.
16
+ args.push('--', ...NOISY_EXCLUDE_PATHSPECS);
17
+ return args;
18
+ }
19
+ function describeModeHint(mode) {
20
+ return mode === 'staged'
21
+ ? 'staged with git add'
22
+ : 'modified but not yet staged (git add)';
23
+ }
24
+ export function registerGenerateDiffTool(server) {
25
+ server.registerTool('generate_diff', {
26
+ 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.',
27
+ inputSchema: {
28
+ mode: z
29
+ .enum(['unstaged', 'staged'])
30
+ .describe('"unstaged": working-tree changes not yet staged. "staged": changes added to the index with git add.'),
31
+ },
32
+ }, (input) => {
33
+ const { mode } = input;
34
+ const args = buildGitArgs(mode);
35
+ // spawnSync with an explicit args array — no shell, no interpolation.
36
+ // 'git' is resolved via PATH which is controlled by the server environment.
37
+ // eslint-disable-next-line sonarjs/no-os-command-from-path
38
+ const result = spawnSync('git', args, {
39
+ cwd: process.cwd(),
40
+ encoding: 'utf8',
41
+ maxBuffer: GIT_MAX_BUFFER,
42
+ timeout: GIT_TIMEOUT_MS,
43
+ });
44
+ if (result.error) {
45
+ return createErrorToolResponse('E_GENERATE_DIFF', `Failed to run git: ${result.error.message}. Ensure git is installed and the working directory is a git repository.`, undefined, { retryable: false, kind: 'internal' });
46
+ }
47
+ if (result.status !== 0) {
48
+ const stderr = result.stderr.trim();
49
+ return createErrorToolResponse('E_GENERATE_DIFF', `git exited with code ${String(result.status)}: ${stderr || 'unknown error'}. Ensure the working directory is a git repository.`, undefined, { retryable: false, kind: 'internal' });
50
+ }
51
+ const cleaned = cleanDiff(result.stdout);
52
+ if (isEmptyDiff(cleaned)) {
53
+ return createErrorToolResponse('E_NO_CHANGES', `No ${mode} changes found in the current branch. Make sure you have changes that are ${describeModeHint(mode)}.`, undefined, { retryable: false, kind: 'validation' });
54
+ }
55
+ const parsedFiles = parseDiffFiles(cleaned);
56
+ const stats = computeDiffStatsFromFiles(parsedFiles);
57
+ const generatedAt = new Date().toISOString();
58
+ storeDiff({ diff: cleaned, stats, generatedAt, mode });
59
+ const summary = `Diff cached at ${DIFF_RESOURCE_URI} — ${stats.files} file(s), +${stats.added} -${stats.deleted}. All review tools are now ready.`;
60
+ return createToolResponse({
61
+ ok: true,
62
+ result: {
63
+ diffRef: DIFF_RESOURCE_URI,
64
+ stats,
65
+ generatedAt,
66
+ mode,
67
+ message: summary,
68
+ },
69
+ }, summary);
70
+ });
71
+ }
@@ -1,5 +1,6 @@
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
4
  import { requireToolContract } from '../lib/tool-contracts.js';
4
5
  import { registerStructuredToolTask } from '../lib/tool-factory.js';
5
6
  import { GenerateReviewSummaryInputSchema } from '../schemas/inputs.js';
@@ -13,36 +14,14 @@ You are a senior code reviewer. Summarize this PR with precision: risk level, ke
13
14
  Be specific — name the exact logic changed, not generic patterns.
14
15
  Return strict JSON only.
15
16
  `;
16
- const statsCache = new WeakMap();
17
- function getCachedStats(input) {
18
- const cached = statsCache.get(input);
19
- if (cached) {
20
- return cached;
21
- }
22
- const parsedFiles = parseDiffFiles(input.diff);
23
- const stats = computeDiffStatsFromFiles(parsedFiles);
24
- statsCache.set(input, stats);
25
- return stats;
26
- }
27
17
  function formatLanguageSegment(language) {
28
18
  return language ? `\nLanguage: ${language}` : '';
29
19
  }
30
- function buildReviewSummaryPrompt(input) {
31
- const stats = getCachedStats(input);
32
- const languageSegment = formatLanguageSegment(input.language);
33
- return `
34
- Repository: ${input.repository}${languageSegment}
35
- Stats: ${stats.files} files, +${stats.added}, -${stats.deleted}
36
-
37
- Diff:
38
- ${input.diff}
39
- `;
40
- }
41
20
  export function registerGenerateReviewSummaryTool(server) {
42
21
  registerStructuredToolTask(server, {
43
22
  name: 'generate_review_summary',
44
23
  title: 'Generate Review Summary',
45
- description: 'Summarize a pull request diff and assess high-level risk.',
24
+ description: 'Summarize the cached diff and assess high-level risk. Call generate_diff first.',
46
25
  inputSchema: GenerateReviewSummaryInputSchema,
47
26
  fullInputSchema: GenerateReviewSummaryInputSchema,
48
27
  resultSchema: ReviewSummaryModelSchema,
@@ -50,11 +29,18 @@ export function registerGenerateReviewSummaryTool(server) {
50
29
  model: TOOL_CONTRACT.model,
51
30
  timeoutMs: TOOL_CONTRACT.timeoutMs,
52
31
  maxOutputTokens: TOOL_CONTRACT.maxOutputTokens,
53
- validateInput: (input) => validateDiffBudget(input.diff),
32
+ validateInput: () => {
33
+ const slot = getDiff();
34
+ if (!slot)
35
+ return createNoDiffError();
36
+ return validateDiffBudget(slot.diff);
37
+ },
54
38
  formatOutcome: (result) => `risk: ${result.overallRisk}`,
55
39
  transformResult: (input, result) => {
56
- const stats = getCachedStats(input);
57
- statsCache.delete(input);
40
+ const slot = getDiff();
41
+ const diff = slot?.diff ?? '';
42
+ const parsedFiles = parseDiffFiles(diff);
43
+ const stats = computeDiffStatsFromFiles(parsedFiles);
58
44
  return {
59
45
  ...result,
60
46
  stats: {
@@ -65,9 +51,22 @@ export function registerGenerateReviewSummaryTool(server) {
65
51
  };
66
52
  },
67
53
  formatOutput: (result) => `Review Summary: ${result.summary}\nRecommendation: ${result.recommendation}`,
68
- buildPrompt: (input) => ({
69
- systemInstruction: SYSTEM_INSTRUCTION,
70
- prompt: buildReviewSummaryPrompt(input),
71
- }),
54
+ buildPrompt: (input) => {
55
+ const slot = getDiff();
56
+ const diff = slot?.diff ?? '';
57
+ const parsedFiles = parseDiffFiles(diff);
58
+ const stats = computeDiffStatsFromFiles(parsedFiles);
59
+ const languageSegment = formatLanguageSegment(input.language);
60
+ return {
61
+ systemInstruction: SYSTEM_INSTRUCTION,
62
+ prompt: `
63
+ Repository: ${input.repository}${languageSegment}
64
+ Stats: ${stats.files} files, +${stats.added}, -${stats.deleted}
65
+
66
+ Diff:
67
+ ${diff}
68
+ `,
69
+ };
70
+ },
72
71
  });
73
72
  }
@@ -1,5 +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
4
  import { requireToolContract } from '../lib/tool-contracts.js';
4
5
  import { registerStructuredToolTask } from '../lib/tool-factory.js';
5
6
  import { GenerateTestPlanInputSchema } from '../schemas/inputs.js';
@@ -14,26 +15,11 @@ const TOOL_CONTRACT = requireToolContract('generate_test_plan');
14
15
  function formatOptionalLine(label, value) {
15
16
  return value === undefined ? '' : `\n${label}: ${value}`;
16
17
  }
17
- function buildGenerateTestPlanPrompt(input) {
18
- const parsedFiles = parseDiffFiles(input.diff);
19
- const { stats, paths } = computeDiffStatsAndPathsFromFiles(parsedFiles);
20
- const languageLine = formatOptionalLine('Language', input.language);
21
- const frameworkLine = formatOptionalLine('Test Framework', input.testFramework);
22
- const maxCasesLine = formatOptionalLine('Max Test Cases', input.maxTestCases);
23
- return `
24
- Repository: ${input.repository}${languageLine}${frameworkLine}${maxCasesLine}
25
- Stats: ${stats.files} files, +${stats.added}, -${stats.deleted}
26
- Changed Files: ${paths.join(', ')}
27
-
28
- Diff:
29
- ${input.diff}
30
- `;
31
- }
32
18
  export function registerGenerateTestPlanTool(server) {
33
19
  registerStructuredToolTask(server, {
34
20
  name: 'generate_test_plan',
35
21
  title: 'Generate Test Plan',
36
- description: 'Create a test plan covering the changes in the diff.',
22
+ description: 'Create a test plan covering the cached diff changes. Call generate_diff first.',
37
23
  inputSchema: GenerateTestPlanInputSchema,
38
24
  fullInputSchema: GenerateTestPlanInputSchema,
39
25
  resultSchema: TestPlanResultSchema,
@@ -44,19 +30,37 @@ export function registerGenerateTestPlanTool(server) {
44
30
  ...(TOOL_CONTRACT.thinkingBudget !== undefined
45
31
  ? { thinkingBudget: TOOL_CONTRACT.thinkingBudget }
46
32
  : undefined),
47
- validateInput: (input) => validateDiffBudget(input.diff),
33
+ validateInput: () => {
34
+ const slot = getDiff();
35
+ if (!slot)
36
+ return createNoDiffError();
37
+ return validateDiffBudget(slot.diff);
38
+ },
48
39
  formatOutcome: (result) => `${result.testCases.length} test cases`,
49
40
  formatOutput: (result) => `Test Plan: ${result.summary}\n${result.testCases.length} cases proposed.`,
50
41
  transformResult: (input, result) => {
51
42
  const cappedTestCases = result.testCases.slice(0, input.maxTestCases ?? result.testCases.length);
43
+ return { ...result, testCases: cappedTestCases };
44
+ },
45
+ buildPrompt: (input) => {
46
+ const slot = getDiff();
47
+ const diff = slot?.diff ?? '';
48
+ const parsedFiles = parseDiffFiles(diff);
49
+ const { stats, paths } = computeDiffStatsAndPathsFromFiles(parsedFiles);
50
+ const languageLine = formatOptionalLine('Language', input.language);
51
+ const frameworkLine = formatOptionalLine('Test Framework', input.testFramework);
52
+ const maxCasesLine = formatOptionalLine('Max Test Cases', input.maxTestCases);
52
53
  return {
53
- ...result,
54
- testCases: cappedTestCases,
54
+ systemInstruction: SYSTEM_INSTRUCTION,
55
+ prompt: `
56
+ Repository: ${input.repository}${languageLine}${frameworkLine}${maxCasesLine}
57
+ Stats: ${stats.files} files, +${stats.added}, -${stats.deleted}
58
+ Changed Files: ${paths.join(', ')}
59
+
60
+ Diff:
61
+ ${diff}
62
+ `,
55
63
  };
56
64
  },
57
- buildPrompt: (input) => ({
58
- systemInstruction: SYSTEM_INSTRUCTION,
59
- prompt: buildGenerateTestPlanPrompt(input),
60
- }),
61
65
  });
62
66
  }
@@ -1,11 +1,13 @@
1
1
  import { registerAnalyzeComplexityTool } from './analyze-complexity.js';
2
2
  import { registerAnalyzePrImpactTool } from './analyze-pr-impact.js';
3
3
  import { registerDetectApiBreakingTool } from './detect-api-breaking.js';
4
+ import { registerGenerateDiffTool } from './generate-diff.js';
4
5
  import { registerGenerateReviewSummaryTool } from './generate-review-summary.js';
5
6
  import { registerGenerateTestPlanTool } from './generate-test-plan.js';
6
7
  import { registerInspectCodeQualityTool } from './inspect-code-quality.js';
7
8
  import { registerSuggestSearchReplaceTool } from './suggest-search-replace.js';
8
9
  const TOOL_REGISTRARS = [
10
+ registerGenerateDiffTool,
9
11
  registerAnalyzePrImpactTool,
10
12
  registerGenerateReviewSummaryTool,
11
13
  registerInspectCodeQualityTool,
@@ -1,6 +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
5
  import { requireToolContract } from '../lib/tool-contracts.js';
5
6
  import { registerStructuredToolTask } from '../lib/tool-factory.js';
6
7
  import { InspectCodeQualityInputSchema } from '../schemas/inputs.js';
@@ -49,31 +50,11 @@ ${sanitizeContent(file.content)}
49
50
  }
50
51
  return `${FILE_CONTEXT_HEADING}${fileBlocks.join('\n')}`;
51
52
  }
52
- function buildInspectPrompt(input) {
53
- const parsedFiles = parseDiffFiles(input.diff);
54
- const { summary: fileSummary } = computeDiffStatsAndSummaryFromFiles(parsedFiles);
55
- const fileContext = formatFileContext(input.files);
56
- const languageLine = formatOptionalLine('Language', input.language);
57
- const maxFindingsLine = formatOptionalLine('Max Findings', input.maxFindings);
58
- const noFilesNote = !input.files?.length
59
- ? '\nNote: No file context provided. Leave contextualInsights empty.'
60
- : '';
61
- return `
62
- Repository: ${input.repository}${languageLine}
63
- Focus Areas: ${input.focusAreas?.join(', ') ?? DEFAULT_FOCUS_AREAS}${maxFindingsLine}${noFilesNote}
64
- Changed Files:
65
- ${fileSummary}
66
-
67
- Diff:
68
- ${input.diff}
69
- ${fileContext}
70
- `;
71
- }
72
53
  export function registerInspectCodeQualityTool(server) {
73
54
  registerStructuredToolTask(server, {
74
55
  name: 'inspect_code_quality',
75
56
  title: 'Inspect Code Quality',
76
- description: 'Deep-dive code review with optional file context.',
57
+ description: 'Deep-dive code review with optional file context. Call generate_diff first.',
77
58
  inputSchema: InspectCodeQualityInputSchema,
78
59
  fullInputSchema: InspectCodeQualityInputSchema,
79
60
  resultSchema: CodeQualityOutputSchema,
@@ -89,13 +70,16 @@ export function registerInspectCodeQualityTool(server) {
89
70
  const fileCount = input.files?.length;
90
71
  return fileCount ? `+${fileCount} files` : '';
91
72
  },
92
- formatOutcome: (result) => `${result.findings.length} findings, risk: ${result.overallRisk}`,
93
73
  validateInput: (input) => {
94
- const diffError = validateDiffBudget(input.diff);
74
+ const slot = getDiff();
75
+ if (!slot)
76
+ return createNoDiffError();
77
+ const diffError = validateDiffBudget(slot.diff);
95
78
  if (diffError)
96
79
  return diffError;
97
- return validateContextBudget(input.diff, input.files);
80
+ return validateContextBudget(slot.diff, input.files);
98
81
  },
82
+ formatOutcome: (result) => `${result.findings.length} findings, risk: ${result.overallRisk}`,
99
83
  formatOutput: (result) => {
100
84
  const count = result.findings.length;
101
85
  const total = result.totalFindings ?? count;
@@ -109,9 +93,30 @@ export function registerInspectCodeQualityTool(server) {
109
93
  const cappedFindings = capFindings(result.findings, input.maxFindings);
110
94
  return { ...result, findings: cappedFindings, totalFindings };
111
95
  },
112
- buildPrompt: (input) => ({
113
- systemInstruction: SYSTEM_INSTRUCTION,
114
- prompt: buildInspectPrompt(input),
115
- }),
96
+ buildPrompt: (input) => {
97
+ const slot = getDiff();
98
+ const diff = slot?.diff ?? '';
99
+ const parsedFiles = parseDiffFiles(diff);
100
+ const { summary: fileSummary } = computeDiffStatsAndSummaryFromFiles(parsedFiles);
101
+ const fileContext = formatFileContext(input.files);
102
+ const languageLine = formatOptionalLine('Language', input.language);
103
+ const maxFindingsLine = formatOptionalLine('Max Findings', input.maxFindings);
104
+ const noFilesNote = !input.files?.length
105
+ ? '\nNote: No file context provided. Leave contextualInsights empty.'
106
+ : '';
107
+ return {
108
+ systemInstruction: SYSTEM_INSTRUCTION,
109
+ prompt: `
110
+ Repository: ${input.repository}${languageLine}
111
+ Focus Areas: ${input.focusAreas?.join(', ') ?? DEFAULT_FOCUS_AREAS}${maxFindingsLine}${noFilesNote}
112
+ Changed Files:
113
+ ${fileSummary}
114
+
115
+ Diff:
116
+ ${diff}
117
+ ${fileContext}
118
+ `,
119
+ };
120
+ },
116
121
  });
117
122
  }
@@ -1,5 +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
4
  import { requireToolContract } from '../lib/tool-contracts.js';
4
5
  import { registerStructuredToolTask } from '../lib/tool-factory.js';
5
6
  import { SuggestSearchReplaceInputSchema } from '../schemas/inputs.js';
@@ -14,23 +15,11 @@ const TOOL_CONTRACT = requireToolContract('suggest_search_replace');
14
15
  function formatPatchCount(count) {
15
16
  return `${count} ${count === 1 ? 'patch' : 'patches'}`;
16
17
  }
17
- function buildSuggestSearchReplacePrompt(input) {
18
- const files = parseDiffFiles(input.diff);
19
- const paths = extractChangedPathsFromFiles(files);
20
- return `
21
- Finding: ${input.findingTitle}
22
- Details: ${input.findingDetails}
23
- Changed Files: ${paths.join(', ')}
24
-
25
- Diff:
26
- ${input.diff}
27
- `;
28
- }
29
18
  export function registerSuggestSearchReplaceTool(server) {
30
19
  registerStructuredToolTask(server, {
31
20
  name: 'suggest_search_replace',
32
21
  title: 'Suggest Search & Replace',
33
- description: 'Generate search-and-replace blocks to fix a finding.',
22
+ description: 'Generate search-and-replace blocks to fix a finding from the cached diff. Call generate_diff first.',
34
23
  inputSchema: SuggestSearchReplaceInputSchema,
35
24
  fullInputSchema: SuggestSearchReplaceInputSchema,
36
25
  resultSchema: SearchReplaceResultSchema,
@@ -41,16 +30,34 @@ export function registerSuggestSearchReplaceTool(server) {
41
30
  ...(TOOL_CONTRACT.thinkingBudget !== undefined
42
31
  ? { thinkingBudget: TOOL_CONTRACT.thinkingBudget }
43
32
  : undefined),
44
- validateInput: (input) => validateDiffBudget(input.diff),
33
+ validateInput: () => {
34
+ const slot = getDiff();
35
+ if (!slot)
36
+ return createNoDiffError();
37
+ return validateDiffBudget(slot.diff);
38
+ },
45
39
  formatOutcome: (result) => formatPatchCount(result.blocks.length),
46
40
  formatOutput: (result) => {
47
41
  const count = result.blocks.length;
48
42
  const patches = formatPatchCount(count);
49
43
  return `${result.summary}\n${patches} • Checklist: ${result.validationChecklist.join(' | ')}`;
50
44
  },
51
- buildPrompt: (input) => ({
52
- systemInstruction: SYSTEM_INSTRUCTION,
53
- prompt: buildSuggestSearchReplacePrompt(input),
54
- }),
45
+ buildPrompt: (input) => {
46
+ const slot = getDiff();
47
+ const diff = slot?.diff ?? '';
48
+ const files = parseDiffFiles(diff);
49
+ const paths = extractChangedPathsFromFiles(files);
50
+ return {
51
+ systemInstruction: SYSTEM_INSTRUCTION,
52
+ prompt: `
53
+ Finding: ${input.findingTitle}
54
+ Details: ${input.findingDetails}
55
+ Changed Files: ${paths.join(', ')}
56
+
57
+ Diff:
58
+ ${diff}
59
+ `,
60
+ };
61
+ },
55
62
  });
56
63
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@j0hanz/code-review-analyst-mcp",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "mcpName": "io.github.j0hanz/code-review-analyst",
5
5
  "description": "Gemini-powered MCP server for code review analysis.",
6
6
  "type": "module",
@@ -32,7 +32,6 @@
32
32
  },
33
33
  "scripts": {
34
34
  "clean": "node scripts/tasks.mjs clean",
35
- "validate:instructions": "node scripts/tasks.mjs validate:instructions",
36
35
  "build": "node scripts/tasks.mjs build",
37
36
  "copy:assets": "node scripts/tasks.mjs copy:assets",
38
37
  "prepare": "npm run build",