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

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.
@@ -4,17 +4,14 @@ export declare const FileContextSchema: z.ZodObject<{
4
4
  content: z.ZodString;
5
5
  }, z.core.$strict>;
6
6
  export declare const AnalyzePrImpactInputSchema: z.ZodObject<{
7
- diff: z.ZodString;
8
7
  repository: z.ZodString;
9
8
  language: z.ZodOptional<z.ZodString>;
10
9
  }, z.core.$strict>;
11
10
  export declare const GenerateReviewSummaryInputSchema: z.ZodObject<{
12
- diff: z.ZodString;
13
11
  repository: z.ZodString;
14
12
  language: z.ZodOptional<z.ZodString>;
15
13
  }, z.core.$strict>;
16
14
  export declare const InspectCodeQualityInputSchema: z.ZodObject<{
17
- diff: z.ZodString;
18
15
  repository: z.ZodString;
19
16
  language: z.ZodOptional<z.ZodString>;
20
17
  focusAreas: z.ZodOptional<z.ZodArray<z.ZodString>>;
@@ -25,22 +22,18 @@ export declare const InspectCodeQualityInputSchema: z.ZodObject<{
25
22
  }, z.core.$strict>>>;
26
23
  }, z.core.$strict>;
27
24
  export declare const SuggestSearchReplaceInputSchema: z.ZodObject<{
28
- diff: z.ZodString;
29
25
  findingTitle: z.ZodString;
30
26
  findingDetails: z.ZodString;
31
27
  }, z.core.$strict>;
32
28
  export declare const GenerateTestPlanInputSchema: z.ZodObject<{
33
- diff: z.ZodString;
34
29
  repository: z.ZodString;
35
30
  language: z.ZodOptional<z.ZodString>;
36
31
  testFramework: z.ZodOptional<z.ZodString>;
37
32
  maxTestCases: z.ZodOptional<z.ZodNumber>;
38
33
  }, z.core.$strict>;
39
34
  export declare const AnalyzeComplexityInputSchema: z.ZodObject<{
40
- diff: z.ZodString;
41
35
  language: z.ZodOptional<z.ZodString>;
42
36
  }, z.core.$strict>;
43
37
  export declare const DetectApiBreakingInputSchema: z.ZodObject<{
44
- diff: z.ZodString;
45
38
  language: z.ZodOptional<z.ZodString>;
46
39
  }, z.core.$strict>;
@@ -1,6 +1,5 @@
1
1
  import { z } from 'zod';
2
2
  const INPUT_LIMITS = {
3
- diff: { min: 10 },
4
3
  repository: { min: 1, max: 200 },
5
4
  language: { min: 2, max: 32 },
6
5
  fileContext: {
@@ -21,70 +20,59 @@ function createBoundedString(min, max, description) {
21
20
  function createOptionalBoundedString(min, max, description) {
22
21
  return createBoundedString(min, max, description).optional();
23
22
  }
24
- function createLanguageSchema(description) {
25
- return createOptionalBoundedString(INPUT_LIMITS.language.min, INPUT_LIMITS.language.max, description);
26
- }
27
- function createDiffSchema(description) {
28
- return z
29
- .string()
30
- .min(INPUT_LIMITS.diff.min)
31
- .describe(`${description} Budget is enforced at runtime via MAX_DIFF_CHARS (default 120,000 chars).`);
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);
32
27
  }
33
28
  function createRepositorySchema() {
34
- 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);
35
30
  }
36
31
  function createOptionalBoundedInteger(min, max, description) {
37
32
  return z.number().int().min(min).max(max).optional().describe(description);
38
33
  }
39
34
  export const FileContextSchema = z.strictObject({
40
- 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).'),
41
36
  content: createBoundedString(INPUT_LIMITS.fileContext.content.min, INPUT_LIMITS.fileContext.content.max, 'Full file content.'),
42
37
  });
43
38
  export const AnalyzePrImpactInputSchema = z.strictObject({
44
- diff: createDiffSchema('Unified diff text for the PR or commit.'),
45
39
  repository: createRepositorySchema(),
46
- language: createLanguageSchema('Primary language to bias analysis.'),
40
+ language: createLanguageSchema(),
47
41
  });
48
42
  export const GenerateReviewSummaryInputSchema = z.strictObject({
49
- diff: createDiffSchema('Unified diff text for one PR or commit.'),
50
43
  repository: createRepositorySchema(),
51
- language: createLanguageSchema('Primary implementation language.'),
44
+ language: createLanguageSchema(),
52
45
  });
53
46
  export const InspectCodeQualityInputSchema = z.strictObject({
54
- diff: createDiffSchema('Unified diff text for in-depth analysis.'),
55
47
  repository: createRepositorySchema(),
56
- language: createLanguageSchema('Primary language.'),
48
+ language: createLanguageSchema(),
57
49
  focusAreas: z
58
- .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).'))
59
51
  .min(1)
60
52
  .max(INPUT_LIMITS.focusArea.maxItems)
61
53
  .optional()
62
- .describe('Specific areas to inspect: security, correctness, etc.'),
63
- 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.'),
64
56
  files: z
65
57
  .array(FileContextSchema)
66
58
  .min(1)
67
59
  .max(INPUT_LIMITS.fileContext.maxItems)
68
60
  .optional()
69
- .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.'),
70
62
  });
71
63
  export const SuggestSearchReplaceInputSchema = z.strictObject({
72
- diff: createDiffSchema('Unified diff that contains the issue to fix.'),
73
- findingTitle: createBoundedString(INPUT_LIMITS.findingTitle.min, INPUT_LIMITS.findingTitle.max, 'Short title of the finding to fix.'),
74
- 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.'),
75
66
  });
76
67
  export const GenerateTestPlanInputSchema = z.strictObject({
77
- diff: createDiffSchema('Unified diff to generate tests for.'),
78
68
  repository: createRepositorySchema(),
79
- language: createLanguageSchema('Primary language.'),
80
- testFramework: createOptionalBoundedString(INPUT_LIMITS.testFramework.min, INPUT_LIMITS.testFramework.max, 'Test framework to use, e.g. jest, vitest, pytest, node:test.'),
81
- 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.'),
82
72
  });
83
73
  export const AnalyzeComplexityInputSchema = z.strictObject({
84
- diff: createDiffSchema('Unified diff to analyze for time and space complexity.'),
85
- language: createLanguageSchema('Primary language to bias analysis.'),
74
+ language: createLanguageSchema(),
86
75
  });
87
76
  export const DetectApiBreakingInputSchema = z.strictObject({
88
- diff: createDiffSchema('Unified diff to scan for API breaking changes.'),
89
- language: createLanguageSchema('Primary language to bias analysis.'),
77
+ language: createLanguageSchema(),
90
78
  });
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 } 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. Prerequisite: generate_diff. Auto-infer language.',
25
19
  inputSchema: AnalyzeComplexityInputSchema,
26
20
  fullInputSchema: AnalyzeComplexityInputSchema,
27
21
  resultSchema: AnalyzeComplexityResultSchema,
@@ -32,14 +26,25 @@ 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: (_input, ctx) => {
30
+ const slot = ctx.diffSlot;
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, ctx) => {
40
+ const diff = ctx.diffSlot?.diff ?? '';
41
+ const languageLine = input.language
42
+ ? `\nLanguage: ${input.language}`
43
+ : '';
44
+ return {
45
+ systemInstruction: SYSTEM_INSTRUCTION,
46
+ prompt: `${languageLine}\nDiff:\n${diff}`.trimStart(),
47
+ };
48
+ },
44
49
  });
45
50
  }
@@ -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 } 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 impact and risk from cached diff. Prerequisite: generate_diff. Auto-infer repo/language.',
36
23
  inputSchema: AnalyzePrImpactInputSchema,
37
24
  fullInputSchema: AnalyzePrImpactInputSchema,
38
25
  resultSchema: PrImpactResultSchema,
@@ -40,12 +27,31 @@ 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: (_input, ctx) => {
31
+ const slot = ctx.diffSlot;
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, ctx) => {
39
+ const diff = ctx.diffSlot?.diff ?? '';
40
+ const files = parseDiffFiles(diff);
41
+ const { stats, summary: fileSummary } = computeDiffStatsAndSummaryFromFiles(files);
42
+ const languageSegment = formatLanguageSegment(input.language);
43
+ return {
44
+ systemInstruction: SYSTEM_INSTRUCTION,
45
+ prompt: `
46
+ Repository: ${input.repository}${languageSegment}
47
+ Change Stats: ${stats.files} files, +${stats.added} lines, -${stats.deleted} lines.
48
+ Changed Files:
49
+ ${fileSummary}
50
+
51
+ Diff:
52
+ ${diff}
53
+ `,
54
+ };
55
+ },
50
56
  });
51
57
  }
@@ -1,4 +1,5 @@
1
1
  import { validateDiffBudget } from '../lib/diff-budget.js';
2
+ import { createNoDiffError } 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 API changes. Prerequisite: generate_diff. Auto-infer language.',
25
19
  inputSchema: DetectApiBreakingInputSchema,
26
20
  fullInputSchema: DetectApiBreakingInputSchema,
27
21
  resultSchema: DetectApiBreakingResultSchema,
@@ -29,14 +23,25 @@ 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: (_input, ctx) => {
27
+ const slot = ctx.diffSlot;
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, ctx) => {
37
+ const diff = ctx.diffSlot?.diff ?? '';
38
+ const languageLine = input.language
39
+ ? `\nLanguage: ${input.language}`
40
+ : '';
41
+ return {
42
+ systemInstruction: SYSTEM_INSTRUCTION,
43
+ prompt: `${languageLine}\nDiff:\n${diff}`.trimStart(),
44
+ };
45
+ },
41
46
  });
42
47
  }
@@ -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,7 +1,8 @@
1
1
  import { validateDiffBudget } from '../lib/diff-budget.js';
2
2
  import { computeDiffStatsFromFiles, parseDiffFiles, } from '../lib/diff-parser.js';
3
+ import { createNoDiffError } from '../lib/diff-store.js';
3
4
  import { requireToolContract } from '../lib/tool-contracts.js';
4
- import { registerStructuredToolTask } from '../lib/tool-factory.js';
5
+ import { registerStructuredToolTask, } from '../lib/tool-factory.js';
5
6
  import { GenerateReviewSummaryInputSchema } from '../schemas/inputs.js';
6
7
  import { ReviewSummaryResultSchema } from '../schemas/outputs.js';
7
8
  const ReviewSummaryModelSchema = ReviewSummaryResultSchema.omit({
@@ -13,36 +14,19 @@ 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
- `;
20
+ function getDiffStats(ctx) {
21
+ const diff = ctx.diffSlot?.diff ?? '';
22
+ const { files, added, deleted } = computeDiffStatsFromFiles(parseDiffFiles(diff));
23
+ return { diff, files, added, deleted };
40
24
  }
41
25
  export function registerGenerateReviewSummaryTool(server) {
42
26
  registerStructuredToolTask(server, {
43
27
  name: 'generate_review_summary',
44
28
  title: 'Generate Review Summary',
45
- description: 'Summarize a pull request diff and assess high-level risk.',
29
+ description: 'Summarize diff and risk level. Prerequisite: generate_diff. Auto-infer repo/language.',
46
30
  inputSchema: GenerateReviewSummaryInputSchema,
47
31
  fullInputSchema: GenerateReviewSummaryInputSchema,
48
32
  resultSchema: ReviewSummaryModelSchema,
@@ -50,24 +34,38 @@ export function registerGenerateReviewSummaryTool(server) {
50
34
  model: TOOL_CONTRACT.model,
51
35
  timeoutMs: TOOL_CONTRACT.timeoutMs,
52
36
  maxOutputTokens: TOOL_CONTRACT.maxOutputTokens,
53
- validateInput: (input) => validateDiffBudget(input.diff),
37
+ validateInput: (_input, ctx) => {
38
+ const slot = ctx.diffSlot;
39
+ if (!slot)
40
+ return createNoDiffError();
41
+ return validateDiffBudget(slot.diff);
42
+ },
54
43
  formatOutcome: (result) => `risk: ${result.overallRisk}`,
55
- transformResult: (input, result) => {
56
- const stats = getCachedStats(input);
57
- statsCache.delete(input);
44
+ transformResult: (_input, result, ctx) => {
45
+ const { files, added, deleted } = getDiffStats(ctx);
58
46
  return {
59
47
  ...result,
60
48
  stats: {
61
- filesChanged: stats.files,
62
- linesAdded: stats.added,
63
- linesRemoved: stats.deleted,
49
+ filesChanged: files,
50
+ linesAdded: added,
51
+ linesRemoved: deleted,
64
52
  },
65
53
  };
66
54
  },
67
55
  formatOutput: (result) => `Review Summary: ${result.summary}\nRecommendation: ${result.recommendation}`,
68
- buildPrompt: (input) => ({
69
- systemInstruction: SYSTEM_INSTRUCTION,
70
- prompt: buildReviewSummaryPrompt(input),
71
- }),
56
+ buildPrompt: (input, ctx) => {
57
+ const { diff, files, added, deleted } = getDiffStats(ctx);
58
+ const languageSegment = formatLanguageSegment(input.language);
59
+ return {
60
+ systemInstruction: SYSTEM_INSTRUCTION,
61
+ prompt: `
62
+ Repository: ${input.repository}${languageSegment}
63
+ Stats: ${files} files, +${added}, -${deleted}
64
+
65
+ Diff:
66
+ ${diff}
67
+ `,
68
+ };
69
+ },
72
70
  });
73
71
  }
@@ -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 } 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: 'Generate test cases. Prerequisite: generate_diff. Auto-infer repo/language/framework.',
37
23
  inputSchema: GenerateTestPlanInputSchema,
38
24
  fullInputSchema: GenerateTestPlanInputSchema,
39
25
  resultSchema: TestPlanResultSchema,
@@ -44,19 +30,36 @@ 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: (_input, ctx) => {
34
+ const slot = ctx.diffSlot;
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, ctx) => {
46
+ const diff = ctx.diffSlot?.diff ?? '';
47
+ const parsedFiles = parseDiffFiles(diff);
48
+ const { stats, paths } = computeDiffStatsAndPathsFromFiles(parsedFiles);
49
+ const languageLine = formatOptionalLine('Language', input.language);
50
+ const frameworkLine = formatOptionalLine('Test Framework', input.testFramework);
51
+ const maxCasesLine = formatOptionalLine('Max Test Cases', input.maxTestCases);
52
52
  return {
53
- ...result,
54
- testCases: cappedTestCases,
53
+ systemInstruction: SYSTEM_INSTRUCTION,
54
+ prompt: `
55
+ Repository: ${input.repository}${languageLine}${frameworkLine}${maxCasesLine}
56
+ Stats: ${stats.files} files, +${stats.added}, -${stats.deleted}
57
+ Changed Files: ${paths.join(', ')}
58
+
59
+ Diff:
60
+ ${diff}
61
+ `,
55
62
  };
56
63
  },
57
- buildPrompt: (input) => ({
58
- systemInstruction: SYSTEM_INSTRUCTION,
59
- prompt: buildGenerateTestPlanPrompt(input),
60
- }),
61
64
  });
62
65
  }
@@ -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,