@j0hanz/code-review-analyst-mcp 1.0.3 → 1.2.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/README.md +203 -193
- package/dist/index.js +22 -18
- package/dist/instructions.md +83 -60
- package/dist/lib/context-budget.d.ts +8 -0
- package/dist/lib/context-budget.js +30 -0
- package/dist/lib/diff-budget.d.ts +3 -1
- package/dist/lib/diff-budget.js +16 -13
- package/dist/lib/diff-parser.d.ts +34 -0
- package/dist/lib/diff-parser.js +114 -0
- package/dist/lib/env-config.d.ts +5 -0
- package/dist/lib/env-config.js +24 -0
- package/dist/lib/errors.d.ts +1 -0
- package/dist/lib/errors.js +13 -7
- package/dist/lib/gemini-schema.d.ts +3 -1
- package/dist/lib/gemini-schema.js +21 -19
- package/dist/lib/gemini.d.ts +1 -0
- package/dist/lib/gemini.js +264 -115
- package/dist/lib/model-config.d.ts +17 -0
- package/dist/lib/model-config.js +19 -0
- package/dist/lib/tool-factory.d.ts +21 -9
- package/dist/lib/tool-factory.js +277 -63
- package/dist/lib/tool-response.d.ts +9 -2
- package/dist/lib/tool-response.js +28 -11
- package/dist/lib/types.d.ts +7 -2
- package/dist/prompts/index.js +91 -3
- package/dist/resources/index.js +14 -10
- package/dist/schemas/inputs.d.ts +27 -15
- package/dist/schemas/inputs.js +60 -44
- package/dist/schemas/outputs.d.ts +130 -7
- package/dist/schemas/outputs.js +171 -74
- package/dist/server.d.ts +5 -1
- package/dist/server.js +39 -27
- package/dist/tools/analyze-pr-impact.d.ts +2 -0
- package/dist/tools/analyze-pr-impact.js +46 -0
- package/dist/tools/generate-review-summary.d.ts +2 -0
- package/dist/tools/generate-review-summary.js +67 -0
- package/dist/tools/generate-test-plan.d.ts +2 -0
- package/dist/tools/generate-test-plan.js +56 -0
- package/dist/tools/index.js +10 -6
- package/dist/tools/inspect-code-quality.d.ts +4 -0
- package/dist/tools/inspect-code-quality.js +107 -0
- package/dist/tools/suggest-search-replace.d.ts +2 -0
- package/dist/tools/suggest-search-replace.js +46 -0
- package/package.json +3 -2
- package/dist/tools/review-diff.d.ts +0 -2
- package/dist/tools/review-diff.js +0 -41
- package/dist/tools/risk-score.d.ts +0 -2
- package/dist/tools/risk-score.js +0 -33
- package/dist/tools/suggest-patch.d.ts +0 -2
- package/dist/tools/suggest-patch.js +0 -34
package/dist/server.js
CHANGED
|
@@ -8,6 +8,27 @@ import { getErrorMessage } from './lib/errors.js';
|
|
|
8
8
|
import { registerAllPrompts } from './prompts/index.js';
|
|
9
9
|
import { registerAllResources } from './resources/index.js';
|
|
10
10
|
import { registerAllTools } from './tools/index.js';
|
|
11
|
+
const SERVER_NAME = 'code-review-analyst';
|
|
12
|
+
const INSTRUCTIONS_FILENAME = 'instructions.md';
|
|
13
|
+
const INSTRUCTIONS_FALLBACK = '(Instructions failed to load)';
|
|
14
|
+
const UTF8_ENCODING = 'utf8';
|
|
15
|
+
const CURRENT_DIR = dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const INSTRUCTIONS_PATH = join(CURRENT_DIR, INSTRUCTIONS_FILENAME);
|
|
17
|
+
const SERVER_CAPABILITIES = {
|
|
18
|
+
logging: {},
|
|
19
|
+
completions: {},
|
|
20
|
+
resources: {},
|
|
21
|
+
tools: {},
|
|
22
|
+
tasks: {
|
|
23
|
+
list: {},
|
|
24
|
+
cancel: {},
|
|
25
|
+
requests: {
|
|
26
|
+
tools: {
|
|
27
|
+
call: {},
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
};
|
|
11
32
|
function isPackageJsonMetadata(value) {
|
|
12
33
|
return (typeof value === 'object' &&
|
|
13
34
|
value !== null &&
|
|
@@ -28,58 +49,49 @@ function parsePackageJson(packageJson, packageJsonPath) {
|
|
|
28
49
|
}
|
|
29
50
|
return parsed;
|
|
30
51
|
}
|
|
31
|
-
function
|
|
32
|
-
return parsePackageJson(packageJson, packageJsonPath).version;
|
|
33
|
-
}
|
|
34
|
-
function readPackageJson(packageJsonPath) {
|
|
52
|
+
function readUtf8File(path) {
|
|
35
53
|
try {
|
|
36
|
-
return readFileSync(
|
|
54
|
+
return readFileSync(path, UTF8_ENCODING);
|
|
37
55
|
}
|
|
38
56
|
catch (error) {
|
|
39
|
-
throw new Error(`Unable to read ${
|
|
57
|
+
throw new Error(`Unable to read ${path}: ${getErrorMessage(error)}`);
|
|
40
58
|
}
|
|
41
59
|
}
|
|
42
60
|
function loadVersion() {
|
|
43
61
|
const packageJsonPath = findPackageJSON(import.meta.url);
|
|
44
62
|
if (!packageJsonPath) {
|
|
45
|
-
throw new Error(
|
|
63
|
+
throw new Error(`Unable to locate package.json for ${SERVER_NAME}.`);
|
|
46
64
|
}
|
|
47
|
-
|
|
65
|
+
const packageJsonText = readUtf8File(packageJsonPath);
|
|
66
|
+
return parsePackageJson(packageJsonText, packageJsonPath).version;
|
|
48
67
|
}
|
|
49
68
|
const SERVER_VERSION = loadVersion();
|
|
50
69
|
function loadInstructions() {
|
|
51
|
-
const currentDir = dirname(fileURLToPath(import.meta.url));
|
|
52
70
|
try {
|
|
53
|
-
return
|
|
71
|
+
return readUtf8File(INSTRUCTIONS_PATH);
|
|
54
72
|
}
|
|
55
73
|
catch (error) {
|
|
56
|
-
process.emitWarning(`Failed to load
|
|
57
|
-
return
|
|
74
|
+
process.emitWarning(`Failed to load ${INSTRUCTIONS_FILENAME}: ${getErrorMessage(error)}`);
|
|
75
|
+
return INSTRUCTIONS_FALLBACK;
|
|
58
76
|
}
|
|
59
77
|
}
|
|
60
78
|
const SERVER_INSTRUCTIONS = loadInstructions();
|
|
61
|
-
const SERVER_TASK_STORE = new InMemoryTaskStore();
|
|
62
79
|
export function createServer() {
|
|
80
|
+
const taskStore = new InMemoryTaskStore();
|
|
63
81
|
const server = new McpServer({
|
|
64
|
-
name:
|
|
82
|
+
name: SERVER_NAME,
|
|
65
83
|
version: SERVER_VERSION,
|
|
66
84
|
}, {
|
|
67
85
|
instructions: SERVER_INSTRUCTIONS,
|
|
68
|
-
taskStore
|
|
69
|
-
capabilities:
|
|
70
|
-
tasks: {
|
|
71
|
-
list: {},
|
|
72
|
-
cancel: {},
|
|
73
|
-
requests: {
|
|
74
|
-
tools: {
|
|
75
|
-
call: {},
|
|
76
|
-
},
|
|
77
|
-
},
|
|
78
|
-
},
|
|
79
|
-
},
|
|
86
|
+
taskStore,
|
|
87
|
+
capabilities: SERVER_CAPABILITIES,
|
|
80
88
|
});
|
|
81
89
|
registerAllTools(server);
|
|
82
90
|
registerAllResources(server, SERVER_INSTRUCTIONS);
|
|
83
91
|
registerAllPrompts(server, SERVER_INSTRUCTIONS);
|
|
84
|
-
|
|
92
|
+
const shutdown = async () => {
|
|
93
|
+
await server.close();
|
|
94
|
+
taskStore.cleanup();
|
|
95
|
+
};
|
|
96
|
+
return { server, shutdown };
|
|
85
97
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { validateDiffBudget } from '../lib/diff-budget.js';
|
|
2
|
+
import { computeDiffStatsAndSummaryFromFiles, parseDiffFiles, } from '../lib/diff-parser.js';
|
|
3
|
+
import { DEFAULT_LANGUAGE, FLASH_MODEL } from '../lib/model-config.js';
|
|
4
|
+
import { registerStructuredToolTask } from '../lib/tool-factory.js';
|
|
5
|
+
import { AnalyzePrImpactInputSchema } from '../schemas/inputs.js';
|
|
6
|
+
import { PrImpactResultSchema } from '../schemas/outputs.js';
|
|
7
|
+
const SYSTEM_INSTRUCTION = `
|
|
8
|
+
You are a technical change analyst.
|
|
9
|
+
Your goal is to identify observable facts about what changed and their downstream effects.
|
|
10
|
+
Analyze the provided Unified Diff and determine its impact severity and categories.
|
|
11
|
+
Focus on breaking changes, API modifications, and rollback complexity.
|
|
12
|
+
Return strict JSON only.
|
|
13
|
+
`;
|
|
14
|
+
export function registerAnalyzePrImpactTool(server) {
|
|
15
|
+
registerStructuredToolTask(server, {
|
|
16
|
+
name: 'analyze_pr_impact',
|
|
17
|
+
title: 'Analyze PR Impact',
|
|
18
|
+
description: 'Assess the impact and risk of a pull request diff.',
|
|
19
|
+
inputSchema: AnalyzePrImpactInputSchema,
|
|
20
|
+
fullInputSchema: AnalyzePrImpactInputSchema,
|
|
21
|
+
resultSchema: PrImpactResultSchema,
|
|
22
|
+
errorCode: 'E_ANALYZE_IMPACT',
|
|
23
|
+
model: FLASH_MODEL,
|
|
24
|
+
validateInput: (input) => validateDiffBudget(input.diff),
|
|
25
|
+
progressContext: (input) => `repo: ${input.repository}, lang: ${input.language ?? DEFAULT_LANGUAGE}`,
|
|
26
|
+
formatOutput: (result) => {
|
|
27
|
+
return `Impact Analysis (${result.severity}): ${result.summary}`;
|
|
28
|
+
},
|
|
29
|
+
buildPrompt: (input) => {
|
|
30
|
+
const files = parseDiffFiles(input.diff);
|
|
31
|
+
const insights = computeDiffStatsAndSummaryFromFiles(files);
|
|
32
|
+
const { stats, summary: fileSummary } = insights;
|
|
33
|
+
const prompt = `
|
|
34
|
+
Repository: ${input.repository}
|
|
35
|
+
Language: ${input.language ?? DEFAULT_LANGUAGE}
|
|
36
|
+
Change Stats: ${stats.files} files, +${stats.added} lines, -${stats.deleted} lines.
|
|
37
|
+
Changed Files:
|
|
38
|
+
${fileSummary}
|
|
39
|
+
|
|
40
|
+
Diff:
|
|
41
|
+
${input.diff}
|
|
42
|
+
`;
|
|
43
|
+
return { systemInstruction: SYSTEM_INSTRUCTION, prompt };
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { validateDiffBudget } from '../lib/diff-budget.js';
|
|
2
|
+
import { computeDiffStatsFromFiles, parseDiffFiles, } from '../lib/diff-parser.js';
|
|
3
|
+
import { DEFAULT_LANGUAGE, FLASH_MODEL } from '../lib/model-config.js';
|
|
4
|
+
import { registerStructuredToolTask } from '../lib/tool-factory.js';
|
|
5
|
+
import { GenerateReviewSummaryInputSchema } from '../schemas/inputs.js';
|
|
6
|
+
import { ReviewSummaryResultSchema } from '../schemas/outputs.js';
|
|
7
|
+
const ReviewSummaryModelSchema = ReviewSummaryResultSchema.omit({
|
|
8
|
+
stats: true,
|
|
9
|
+
});
|
|
10
|
+
const SYSTEM_INSTRUCTION = `
|
|
11
|
+
You are a senior code reviewer.
|
|
12
|
+
Summarize the changes in this pull request and provide a high-level risk assessment.
|
|
13
|
+
Identify key changes and provide a merge recommendation.
|
|
14
|
+
Return strict JSON only.
|
|
15
|
+
`;
|
|
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
|
+
export function registerGenerateReviewSummaryTool(server) {
|
|
28
|
+
registerStructuredToolTask(server, {
|
|
29
|
+
name: 'generate_review_summary',
|
|
30
|
+
title: 'Generate Review Summary',
|
|
31
|
+
description: 'Summarize a pull request diff and assess high-level risk.',
|
|
32
|
+
inputSchema: GenerateReviewSummaryInputSchema,
|
|
33
|
+
fullInputSchema: GenerateReviewSummaryInputSchema,
|
|
34
|
+
resultSchema: ReviewSummaryModelSchema,
|
|
35
|
+
errorCode: 'E_REVIEW_SUMMARY',
|
|
36
|
+
model: FLASH_MODEL,
|
|
37
|
+
validateInput: (input) => validateDiffBudget(input.diff),
|
|
38
|
+
progressContext: (input) => `repo: ${input.repository}, lang: ${input.language ?? DEFAULT_LANGUAGE}`,
|
|
39
|
+
transformResult: (input, result) => {
|
|
40
|
+
const stats = getCachedStats(input);
|
|
41
|
+
statsCache.delete(input);
|
|
42
|
+
return {
|
|
43
|
+
...result,
|
|
44
|
+
stats: {
|
|
45
|
+
filesChanged: stats.files,
|
|
46
|
+
linesAdded: stats.added,
|
|
47
|
+
linesRemoved: stats.deleted,
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
},
|
|
51
|
+
formatOutput: (result) => {
|
|
52
|
+
return `Review Summary: ${result.summary}\nRecommendation: ${result.recommendation}`;
|
|
53
|
+
},
|
|
54
|
+
buildPrompt: (input) => {
|
|
55
|
+
const stats = getCachedStats(input);
|
|
56
|
+
const prompt = `
|
|
57
|
+
Repository: ${input.repository}
|
|
58
|
+
Language: ${input.language ?? DEFAULT_LANGUAGE}
|
|
59
|
+
Stats: ${stats.files} files, +${stats.added}, -${stats.deleted}
|
|
60
|
+
|
|
61
|
+
Diff:
|
|
62
|
+
${input.diff}
|
|
63
|
+
`;
|
|
64
|
+
return { systemInstruction: SYSTEM_INSTRUCTION, prompt };
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { validateDiffBudget } from '../lib/diff-budget.js';
|
|
2
|
+
import { computeDiffStatsAndPathsFromFiles, parseDiffFiles, } from '../lib/diff-parser.js';
|
|
3
|
+
import { DEFAULT_LANGUAGE, DEFAULT_FRAMEWORK as DEFAULT_TEST_FRAMEWORK, FLASH_MODEL, FLASH_THINKING_BUDGET, } from '../lib/model-config.js';
|
|
4
|
+
import { registerStructuredToolTask } from '../lib/tool-factory.js';
|
|
5
|
+
import { GenerateTestPlanInputSchema } from '../schemas/inputs.js';
|
|
6
|
+
import { TestPlanResultSchema } from '../schemas/outputs.js';
|
|
7
|
+
const DEFAULT_MAX_TEST_CASES = 'auto';
|
|
8
|
+
const SYSTEM_INSTRUCTION = `
|
|
9
|
+
You are a QA automation architect.
|
|
10
|
+
Analyze the diff and generate a comprehensive test plan.
|
|
11
|
+
Focus on edge cases, logical branches, and integration points affected by the changes.
|
|
12
|
+
Ensure test cases are actionable and verify specific behaviors.
|
|
13
|
+
Return strict JSON only.
|
|
14
|
+
`;
|
|
15
|
+
export function registerGenerateTestPlanTool(server) {
|
|
16
|
+
registerStructuredToolTask(server, {
|
|
17
|
+
name: 'generate_test_plan',
|
|
18
|
+
title: 'Generate Test Plan',
|
|
19
|
+
description: 'Create a test plan covering the changes in the diff.',
|
|
20
|
+
inputSchema: GenerateTestPlanInputSchema,
|
|
21
|
+
fullInputSchema: GenerateTestPlanInputSchema,
|
|
22
|
+
resultSchema: TestPlanResultSchema,
|
|
23
|
+
errorCode: 'E_GENERATE_TEST_PLAN',
|
|
24
|
+
model: FLASH_MODEL,
|
|
25
|
+
thinkingBudget: FLASH_THINKING_BUDGET,
|
|
26
|
+
validateInput: (input) => validateDiffBudget(input.diff),
|
|
27
|
+
progressContext: (input) => `repo: ${input.repository}, framework: ${input.testFramework ?? DEFAULT_TEST_FRAMEWORK}, max-cases: ${input.maxTestCases ?? DEFAULT_MAX_TEST_CASES}`,
|
|
28
|
+
formatOutput: (result) => {
|
|
29
|
+
return `Test Plan: ${result.summary}\n${result.testCases.length} cases proposed.`;
|
|
30
|
+
},
|
|
31
|
+
transformResult: (input, result) => {
|
|
32
|
+
const cappedTestCases = result.testCases.slice(0, input.maxTestCases ?? result.testCases.length);
|
|
33
|
+
return {
|
|
34
|
+
...result,
|
|
35
|
+
testCases: cappedTestCases,
|
|
36
|
+
};
|
|
37
|
+
},
|
|
38
|
+
buildPrompt: (input) => {
|
|
39
|
+
const parsedFiles = parseDiffFiles(input.diff);
|
|
40
|
+
const insights = computeDiffStatsAndPathsFromFiles(parsedFiles);
|
|
41
|
+
const { stats, paths } = insights;
|
|
42
|
+
const prompt = `
|
|
43
|
+
Repository: ${input.repository}
|
|
44
|
+
Language: ${input.language ?? DEFAULT_LANGUAGE}
|
|
45
|
+
Test Framework: ${input.testFramework ?? DEFAULT_TEST_FRAMEWORK}
|
|
46
|
+
Max Test Cases: ${input.maxTestCases ?? DEFAULT_MAX_TEST_CASES}
|
|
47
|
+
Stats: ${stats.files} files, +${stats.added}, -${stats.deleted}
|
|
48
|
+
Changed Files: ${paths.join(', ')}
|
|
49
|
+
|
|
50
|
+
Diff:
|
|
51
|
+
${input.diff}
|
|
52
|
+
`;
|
|
53
|
+
return { systemInstruction: SYSTEM_INSTRUCTION, prompt };
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
}
|
package/dist/tools/index.js
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { registerAnalyzePrImpactTool } from './analyze-pr-impact.js';
|
|
2
|
+
import { registerGenerateReviewSummaryTool } from './generate-review-summary.js';
|
|
3
|
+
import { registerGenerateTestPlanTool } from './generate-test-plan.js';
|
|
4
|
+
import { registerInspectCodeQualityTool } from './inspect-code-quality.js';
|
|
5
|
+
import { registerSuggestSearchReplaceTool } from './suggest-search-replace.js';
|
|
4
6
|
const TOOL_REGISTRARS = [
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
registerAnalyzePrImpactTool,
|
|
8
|
+
registerGenerateReviewSummaryTool,
|
|
9
|
+
registerInspectCodeQualityTool,
|
|
10
|
+
registerSuggestSearchReplaceTool,
|
|
11
|
+
registerGenerateTestPlanTool,
|
|
8
12
|
];
|
|
9
13
|
export function registerAllTools(server) {
|
|
10
14
|
for (const registerTool of TOOL_REGISTRARS) {
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
export declare function sanitizePath(path: string): string;
|
|
3
|
+
export declare function sanitizeContent(content: string): string;
|
|
4
|
+
export declare function registerInspectCodeQualityTool(server: McpServer): void;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { validateContextBudget } from '../lib/context-budget.js';
|
|
2
|
+
import { validateDiffBudget } from '../lib/diff-budget.js';
|
|
3
|
+
import { computeDiffStatsAndSummaryFromFiles, parseDiffFiles, } from '../lib/diff-parser.js';
|
|
4
|
+
import { DEFAULT_LANGUAGE, DEFAULT_TIMEOUT_PRO_MS, PRO_MODEL, PRO_THINKING_BUDGET, } from '../lib/model-config.js';
|
|
5
|
+
import { registerStructuredToolTask } from '../lib/tool-factory.js';
|
|
6
|
+
import { InspectCodeQualityInputSchema } from '../schemas/inputs.js';
|
|
7
|
+
import { CodeQualityOutputSchema, CodeQualityResultSchema, } from '../schemas/outputs.js';
|
|
8
|
+
const DEFAULT_FOCUS_AREAS = 'General';
|
|
9
|
+
const DEFAULT_MAX_FINDINGS = 'auto';
|
|
10
|
+
const FILE_CONTEXT_HEADING = '\nFull File Context:\n';
|
|
11
|
+
const PATH_ESCAPE_REPLACEMENTS = {
|
|
12
|
+
'"': '\\"',
|
|
13
|
+
'\n': ' ',
|
|
14
|
+
'\r': ' ',
|
|
15
|
+
};
|
|
16
|
+
const PATH_ESCAPE_PATTERN = /["\n\r]/g;
|
|
17
|
+
const SYSTEM_INSTRUCTION = `
|
|
18
|
+
You are a principal software engineer performing a deep code review.
|
|
19
|
+
Analyze the diff and provided file context to identify bugs, security issues, and quality problems.
|
|
20
|
+
Consider interactions between changed code and surrounding code.
|
|
21
|
+
Prioritize correctness and maintainability.
|
|
22
|
+
Return strict JSON only.
|
|
23
|
+
`;
|
|
24
|
+
export function sanitizePath(path) {
|
|
25
|
+
return path.replace(PATH_ESCAPE_PATTERN, (match) => {
|
|
26
|
+
return PATH_ESCAPE_REPLACEMENTS[match];
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
export function sanitizeContent(content) {
|
|
30
|
+
return content
|
|
31
|
+
.replaceAll('<<END_FILE>>', '<END_FILE_ESCAPED>')
|
|
32
|
+
.replaceAll('<<FILE', '<FILE');
|
|
33
|
+
}
|
|
34
|
+
function formatFileContext(files) {
|
|
35
|
+
if (!files || files.length === 0) {
|
|
36
|
+
return '';
|
|
37
|
+
}
|
|
38
|
+
let fileBlocks = '';
|
|
39
|
+
for (let index = 0; index < files.length; index += 1) {
|
|
40
|
+
const file = files[index];
|
|
41
|
+
if (!file) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
fileBlocks += `
|
|
45
|
+
<<FILE path="${sanitizePath(file.path)}">>
|
|
46
|
+
${sanitizeContent(file.content)}
|
|
47
|
+
<<END_FILE>>
|
|
48
|
+
`;
|
|
49
|
+
if (index < files.length - 1) {
|
|
50
|
+
fileBlocks += '\n';
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return `${FILE_CONTEXT_HEADING}${fileBlocks}`;
|
|
54
|
+
}
|
|
55
|
+
export function registerInspectCodeQualityTool(server) {
|
|
56
|
+
registerStructuredToolTask(server, {
|
|
57
|
+
name: 'inspect_code_quality',
|
|
58
|
+
title: 'Inspect Code Quality',
|
|
59
|
+
description: 'Deep-dive code review with optional file context.',
|
|
60
|
+
inputSchema: InspectCodeQualityInputSchema,
|
|
61
|
+
fullInputSchema: InspectCodeQualityInputSchema,
|
|
62
|
+
resultSchema: CodeQualityOutputSchema,
|
|
63
|
+
geminiSchema: CodeQualityResultSchema,
|
|
64
|
+
errorCode: 'E_INSPECT_QUALITY',
|
|
65
|
+
model: PRO_MODEL,
|
|
66
|
+
thinkingBudget: PRO_THINKING_BUDGET,
|
|
67
|
+
timeoutMs: DEFAULT_TIMEOUT_PRO_MS,
|
|
68
|
+
progressContext: (input) => `repo: ${input.repository}, focus: ${input.focusAreas?.length ?? 0}, file-context: ${input.files?.length ?? 0}, max-findings: ${input.maxFindings ?? DEFAULT_MAX_FINDINGS}`,
|
|
69
|
+
validateInput: (input) => {
|
|
70
|
+
const diffError = validateDiffBudget(input.diff);
|
|
71
|
+
if (diffError)
|
|
72
|
+
return diffError;
|
|
73
|
+
return validateContextBudget(input.diff, input.files);
|
|
74
|
+
},
|
|
75
|
+
formatOutput: (result) => {
|
|
76
|
+
const count = result.findings.length;
|
|
77
|
+
const total = result.totalFindings ?? count;
|
|
78
|
+
const findingsSuffix = count < total
|
|
79
|
+
? `${count} of ${total} findings reported.`
|
|
80
|
+
: `${count} findings reported.`;
|
|
81
|
+
return `Code Quality Inspection: ${result.summary}\n${findingsSuffix}`;
|
|
82
|
+
},
|
|
83
|
+
transformResult: (input, result) => {
|
|
84
|
+
const totalFindings = result.findings.length;
|
|
85
|
+
const cappedFindings = result.findings.slice(0, input.maxFindings ?? totalFindings);
|
|
86
|
+
return { ...result, findings: cappedFindings, totalFindings };
|
|
87
|
+
},
|
|
88
|
+
buildPrompt: (input) => {
|
|
89
|
+
const files = parseDiffFiles(input.diff);
|
|
90
|
+
const { summary: fileSummary } = computeDiffStatsAndSummaryFromFiles(files);
|
|
91
|
+
const fileContext = formatFileContext(input.files);
|
|
92
|
+
const prompt = `
|
|
93
|
+
Repository: ${input.repository}
|
|
94
|
+
Language: ${input.language ?? DEFAULT_LANGUAGE}
|
|
95
|
+
Focus Areas: ${input.focusAreas?.join(', ') ?? DEFAULT_FOCUS_AREAS}
|
|
96
|
+
Max Findings: ${input.maxFindings ?? DEFAULT_MAX_FINDINGS}
|
|
97
|
+
Changed Files:
|
|
98
|
+
${fileSummary}
|
|
99
|
+
|
|
100
|
+
Diff:
|
|
101
|
+
${input.diff}
|
|
102
|
+
${fileContext}
|
|
103
|
+
`;
|
|
104
|
+
return { systemInstruction: SYSTEM_INSTRUCTION, prompt };
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { validateDiffBudget } from '../lib/diff-budget.js';
|
|
2
|
+
import { extractChangedPathsFromFiles, parseDiffFiles, } from '../lib/diff-parser.js';
|
|
3
|
+
import { DEFAULT_TIMEOUT_PRO_MS, PRO_MODEL, PRO_THINKING_BUDGET, } from '../lib/model-config.js';
|
|
4
|
+
import { registerStructuredToolTask } from '../lib/tool-factory.js';
|
|
5
|
+
import { SuggestSearchReplaceInputSchema } from '../schemas/inputs.js';
|
|
6
|
+
import { SearchReplaceResultSchema } from '../schemas/outputs.js';
|
|
7
|
+
const SYSTEM_INSTRUCTION = `
|
|
8
|
+
You are a code remediation expert.
|
|
9
|
+
Generate precise search-and-replace blocks to fix the described issue.
|
|
10
|
+
The 'search' block must match exact verbatim text from the file (including whitespace).
|
|
11
|
+
Do not invent code that isn't present.
|
|
12
|
+
Scope your suggestions to the changed files if possible.
|
|
13
|
+
Return strict JSON only.
|
|
14
|
+
`;
|
|
15
|
+
export function registerSuggestSearchReplaceTool(server) {
|
|
16
|
+
registerStructuredToolTask(server, {
|
|
17
|
+
name: 'suggest_search_replace',
|
|
18
|
+
title: 'Suggest Search & Replace',
|
|
19
|
+
description: 'Generate search-and-replace blocks to fix a finding.',
|
|
20
|
+
inputSchema: SuggestSearchReplaceInputSchema,
|
|
21
|
+
fullInputSchema: SuggestSearchReplaceInputSchema,
|
|
22
|
+
resultSchema: SearchReplaceResultSchema,
|
|
23
|
+
errorCode: 'E_SUGGEST_SEARCH_REPLACE',
|
|
24
|
+
model: PRO_MODEL,
|
|
25
|
+
thinkingBudget: PRO_THINKING_BUDGET,
|
|
26
|
+
timeoutMs: DEFAULT_TIMEOUT_PRO_MS,
|
|
27
|
+
validateInput: (input) => validateDiffBudget(input.diff),
|
|
28
|
+
progressContext: (input) => `finding: ${input.findingTitle}, details: ${input.findingDetails.length} chars`,
|
|
29
|
+
formatOutput: (result) => {
|
|
30
|
+
return `Search/Replace Suggestion: ${result.summary}`;
|
|
31
|
+
},
|
|
32
|
+
buildPrompt: (input) => {
|
|
33
|
+
const files = parseDiffFiles(input.diff);
|
|
34
|
+
const paths = extractChangedPathsFromFiles(files);
|
|
35
|
+
const prompt = `
|
|
36
|
+
Finding: ${input.findingTitle}
|
|
37
|
+
Details: ${input.findingDetails}
|
|
38
|
+
Changed Files: ${paths.join(', ')}
|
|
39
|
+
|
|
40
|
+
Diff:
|
|
41
|
+
${input.diff}
|
|
42
|
+
`;
|
|
43
|
+
return { systemInstruction: SYSTEM_INSTRUCTION, prompt };
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@j0hanz/code-review-analyst-mcp",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.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",
|
|
@@ -55,8 +55,9 @@
|
|
|
55
55
|
"prepublishOnly": "npm run lint && npm run type-check && npm run build"
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
|
-
"@google/genai": "^1.
|
|
58
|
+
"@google/genai": "^1.42.0",
|
|
59
59
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
60
|
+
"parse-diff": "^0.11.1",
|
|
60
61
|
"zod": "^4.3.6"
|
|
61
62
|
},
|
|
62
63
|
"devDependencies": {
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import { validateDiffBudget } from '../lib/diff-budget.js';
|
|
2
|
-
import { registerStructuredToolTask, } from '../lib/tool-factory.js';
|
|
3
|
-
import { ReviewDiffInputSchema } from '../schemas/inputs.js';
|
|
4
|
-
import { ReviewDiffResultSchema } from '../schemas/outputs.js';
|
|
5
|
-
const DEFAULT_MAX_FINDINGS = 10;
|
|
6
|
-
const DEFAULT_FOCUS_AREAS = 'security, correctness, regressions, performance';
|
|
7
|
-
function buildReviewPrompt(input) {
|
|
8
|
-
const focus = input.focusAreas?.length
|
|
9
|
-
? input.focusAreas.join(', ')
|
|
10
|
-
: DEFAULT_FOCUS_AREAS;
|
|
11
|
-
const maxFindings = input.maxFindings ?? DEFAULT_MAX_FINDINGS;
|
|
12
|
-
const systemInstruction = [
|
|
13
|
-
'You are a senior staff engineer performing pull request review.',
|
|
14
|
-
'Return strict JSON only with no markdown fences.',
|
|
15
|
-
].join('\n');
|
|
16
|
-
const prompt = [
|
|
17
|
-
`Repository: ${input.repository}`,
|
|
18
|
-
`Primary language: ${input.language ?? 'not specified'}`,
|
|
19
|
-
`Focus areas: ${focus}`,
|
|
20
|
-
`Limit findings to ${maxFindings}.`,
|
|
21
|
-
'Prioritize concrete, high-confidence defects and risky behavior changes.',
|
|
22
|
-
'Include testsNeeded as short action items.',
|
|
23
|
-
'',
|
|
24
|
-
'Unified diff:',
|
|
25
|
-
input.diff,
|
|
26
|
-
].join('\n');
|
|
27
|
-
return { systemInstruction, prompt };
|
|
28
|
-
}
|
|
29
|
-
export function registerReviewDiffTool(server) {
|
|
30
|
-
registerStructuredToolTask(server, {
|
|
31
|
-
name: 'review_diff',
|
|
32
|
-
title: 'Review Diff',
|
|
33
|
-
description: 'Analyze a code diff and return structured findings, risk level, and test recommendations.',
|
|
34
|
-
inputSchema: ReviewDiffInputSchema.shape,
|
|
35
|
-
fullInputSchema: ReviewDiffInputSchema,
|
|
36
|
-
resultSchema: ReviewDiffResultSchema,
|
|
37
|
-
validateInput: (input) => validateDiffBudget(input.diff),
|
|
38
|
-
errorCode: 'E_REVIEW_DIFF',
|
|
39
|
-
buildPrompt: buildReviewPrompt,
|
|
40
|
-
});
|
|
41
|
-
}
|
package/dist/tools/risk-score.js
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { validateDiffBudget } from '../lib/diff-budget.js';
|
|
2
|
-
import { registerStructuredToolTask, } from '../lib/tool-factory.js';
|
|
3
|
-
import { RiskScoreInputSchema } from '../schemas/inputs.js';
|
|
4
|
-
import { RiskScoreResultSchema } from '../schemas/outputs.js';
|
|
5
|
-
const DEFAULT_DEPLOYMENT_CRITICALITY = 'medium';
|
|
6
|
-
function buildRiskPrompt(input) {
|
|
7
|
-
const systemInstruction = [
|
|
8
|
-
'You are assessing software deployment risk from a code diff.',
|
|
9
|
-
'Return strict JSON only, no markdown fences.',
|
|
10
|
-
].join('\n');
|
|
11
|
-
const prompt = [
|
|
12
|
-
`Deployment criticality: ${input.deploymentCriticality ?? DEFAULT_DEPLOYMENT_CRITICALITY}`,
|
|
13
|
-
'Score guidance: 0 is no risk, 100 is severe risk.',
|
|
14
|
-
'Rationale must be concise, concrete, and evidence-based.',
|
|
15
|
-
'',
|
|
16
|
-
'Unified diff:',
|
|
17
|
-
input.diff,
|
|
18
|
-
].join('\n');
|
|
19
|
-
return { systemInstruction, prompt };
|
|
20
|
-
}
|
|
21
|
-
export function registerRiskScoreTool(server) {
|
|
22
|
-
registerStructuredToolTask(server, {
|
|
23
|
-
name: 'risk_score',
|
|
24
|
-
title: 'Risk Score',
|
|
25
|
-
description: 'Score a diff from 0-100 and explain the key risk drivers for release decisions.',
|
|
26
|
-
inputSchema: RiskScoreInputSchema.shape,
|
|
27
|
-
fullInputSchema: RiskScoreInputSchema,
|
|
28
|
-
resultSchema: RiskScoreResultSchema,
|
|
29
|
-
validateInput: (input) => validateDiffBudget(input.diff),
|
|
30
|
-
errorCode: 'E_RISK_SCORE',
|
|
31
|
-
buildPrompt: buildRiskPrompt,
|
|
32
|
-
});
|
|
33
|
-
}
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { validateDiffBudget } from '../lib/diff-budget.js';
|
|
2
|
-
import { registerStructuredToolTask, } from '../lib/tool-factory.js';
|
|
3
|
-
import { SuggestPatchInputSchema } from '../schemas/inputs.js';
|
|
4
|
-
import { PatchSuggestionResultSchema } from '../schemas/outputs.js';
|
|
5
|
-
const DEFAULT_PATCH_STYLE = 'balanced';
|
|
6
|
-
function buildPatchPrompt(input) {
|
|
7
|
-
const systemInstruction = [
|
|
8
|
-
'You are producing a corrective patch for a code review issue.',
|
|
9
|
-
'Return strict JSON only, no markdown fences.',
|
|
10
|
-
].join('\n');
|
|
11
|
-
const prompt = [
|
|
12
|
-
`Patch style: ${input.patchStyle ?? DEFAULT_PATCH_STYLE}`,
|
|
13
|
-
`Finding title: ${input.findingTitle}`,
|
|
14
|
-
`Finding details: ${input.findingDetails}`,
|
|
15
|
-
'Patch output must be a valid unified diff snippet and avoid unrelated changes.',
|
|
16
|
-
'',
|
|
17
|
-
'Original unified diff:',
|
|
18
|
-
input.diff,
|
|
19
|
-
].join('\n');
|
|
20
|
-
return { systemInstruction, prompt };
|
|
21
|
-
}
|
|
22
|
-
export function registerSuggestPatchTool(server) {
|
|
23
|
-
registerStructuredToolTask(server, {
|
|
24
|
-
name: 'suggest_patch',
|
|
25
|
-
title: 'Suggest Patch',
|
|
26
|
-
description: 'Generate a focused unified diff patch to address one selected review finding.',
|
|
27
|
-
inputSchema: SuggestPatchInputSchema.shape,
|
|
28
|
-
fullInputSchema: SuggestPatchInputSchema,
|
|
29
|
-
resultSchema: PatchSuggestionResultSchema,
|
|
30
|
-
validateInput: (input) => validateDiffBudget(input.diff),
|
|
31
|
-
errorCode: 'E_SUGGEST_PATCH',
|
|
32
|
-
buildPrompt: buildPatchPrompt,
|
|
33
|
-
});
|
|
34
|
-
}
|