@j0hanz/code-review-analyst-mcp 0.1.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.
@@ -0,0 +1,132 @@
1
+ import { z } from 'zod';
2
+ const OUTPUT_LIMITS = {
3
+ reviewFinding: {
4
+ fileMax: 260,
5
+ lineMin: 1,
6
+ lineMax: 1_000_000,
7
+ title: { min: 3, max: 160 },
8
+ text: { min: 10, max: 2_000 },
9
+ },
10
+ reviewDiffResult: {
11
+ summary: { min: 10, max: 2_000 },
12
+ findingsMax: 30,
13
+ testsNeeded: { minItems: 0, maxItems: 12, itemMin: 5, itemMax: 300 },
14
+ },
15
+ riskScoreResult: {
16
+ score: { min: 0, max: 100 },
17
+ rationale: { minItems: 1, maxItems: 10, itemMin: 8, itemMax: 500 },
18
+ },
19
+ patchSuggestionResult: {
20
+ summary: { min: 10, max: 1_000 },
21
+ patch: { min: 10, max: 60_000 },
22
+ checklist: { minItems: 1, maxItems: 12, itemMin: 6, itemMax: 300 },
23
+ },
24
+ };
25
+ export const DefaultOutputSchema = z.strictObject({
26
+ ok: z.boolean().describe('Whether the tool completed successfully.'),
27
+ result: z.unknown().optional().describe('Successful result payload.'),
28
+ error: z
29
+ .strictObject({
30
+ code: z.string().describe('Stable error code for callers.'),
31
+ message: z.string().describe('Human readable error details.'),
32
+ })
33
+ .optional()
34
+ .describe('Error payload when ok is false.'),
35
+ });
36
+ export const ReviewFindingSchema = z.strictObject({
37
+ severity: z
38
+ .enum(['low', 'medium', 'high', 'critical'])
39
+ .describe('Severity for this issue.'),
40
+ file: z
41
+ .string()
42
+ .min(1)
43
+ .max(OUTPUT_LIMITS.reviewFinding.fileMax)
44
+ .describe('File path for the finding.'),
45
+ line: z
46
+ .number()
47
+ .int()
48
+ .min(OUTPUT_LIMITS.reviewFinding.lineMin)
49
+ .max(OUTPUT_LIMITS.reviewFinding.lineMax)
50
+ .nullable()
51
+ .describe('1-based line number when known, otherwise null.'),
52
+ title: z
53
+ .string()
54
+ .min(OUTPUT_LIMITS.reviewFinding.title.min)
55
+ .max(OUTPUT_LIMITS.reviewFinding.title.max)
56
+ .describe('Short finding title.'),
57
+ explanation: z
58
+ .string()
59
+ .min(OUTPUT_LIMITS.reviewFinding.text.min)
60
+ .max(OUTPUT_LIMITS.reviewFinding.text.max)
61
+ .describe('Why this issue matters.'),
62
+ recommendation: z
63
+ .string()
64
+ .min(OUTPUT_LIMITS.reviewFinding.text.min)
65
+ .max(OUTPUT_LIMITS.reviewFinding.text.max)
66
+ .describe('Concrete fix recommendation.'),
67
+ });
68
+ export const ReviewDiffResultSchema = z.strictObject({
69
+ summary: z
70
+ .string()
71
+ .min(OUTPUT_LIMITS.reviewDiffResult.summary.min)
72
+ .max(OUTPUT_LIMITS.reviewDiffResult.summary.max)
73
+ .describe('Short review summary.'),
74
+ overallRisk: z
75
+ .enum(['low', 'medium', 'high'])
76
+ .describe('Overall risk for merging this diff.'),
77
+ findings: z
78
+ .array(ReviewFindingSchema.describe('Single code review finding.'))
79
+ .min(0)
80
+ .max(OUTPUT_LIMITS.reviewDiffResult.findingsMax)
81
+ .describe('Ordered list of findings, highest severity first.'),
82
+ testsNeeded: z
83
+ .array(z
84
+ .string()
85
+ .min(OUTPUT_LIMITS.reviewDiffResult.testsNeeded.itemMin)
86
+ .max(OUTPUT_LIMITS.reviewDiffResult.testsNeeded.itemMax)
87
+ .describe('Test recommendation to reduce risk.'))
88
+ .min(OUTPUT_LIMITS.reviewDiffResult.testsNeeded.minItems)
89
+ .max(OUTPUT_LIMITS.reviewDiffResult.testsNeeded.maxItems)
90
+ .describe('Targeted tests to add before merge.'),
91
+ });
92
+ export const RiskScoreResultSchema = z.strictObject({
93
+ score: z
94
+ .number()
95
+ .int()
96
+ .min(OUTPUT_LIMITS.riskScoreResult.score.min)
97
+ .max(OUTPUT_LIMITS.riskScoreResult.score.max)
98
+ .describe('Deployment risk score, where 100 is highest risk.'),
99
+ bucket: z
100
+ .enum(['low', 'medium', 'high', 'critical'])
101
+ .describe('Risk bucket derived from score and criticality.'),
102
+ rationale: z
103
+ .array(z
104
+ .string()
105
+ .min(OUTPUT_LIMITS.riskScoreResult.rationale.itemMin)
106
+ .max(OUTPUT_LIMITS.riskScoreResult.rationale.itemMax)
107
+ .describe('Reason that influenced the final score.'))
108
+ .min(OUTPUT_LIMITS.riskScoreResult.rationale.minItems)
109
+ .max(OUTPUT_LIMITS.riskScoreResult.rationale.maxItems)
110
+ .describe('Evidence-based explanation for the score.'),
111
+ });
112
+ export const PatchSuggestionResultSchema = z.strictObject({
113
+ summary: z
114
+ .string()
115
+ .min(OUTPUT_LIMITS.patchSuggestionResult.summary.min)
116
+ .max(OUTPUT_LIMITS.patchSuggestionResult.summary.max)
117
+ .describe('Short patch strategy summary.'),
118
+ patch: z
119
+ .string()
120
+ .min(OUTPUT_LIMITS.patchSuggestionResult.patch.min)
121
+ .max(OUTPUT_LIMITS.patchSuggestionResult.patch.max)
122
+ .describe('Unified diff patch text.'),
123
+ validationChecklist: z
124
+ .array(z
125
+ .string()
126
+ .min(OUTPUT_LIMITS.patchSuggestionResult.checklist.itemMin)
127
+ .max(OUTPUT_LIMITS.patchSuggestionResult.checklist.itemMax)
128
+ .describe('Validation step after applying patch.'))
129
+ .min(OUTPUT_LIMITS.patchSuggestionResult.checklist.minItems)
130
+ .max(OUTPUT_LIMITS.patchSuggestionResult.checklist.maxItems)
131
+ .describe('Post-change validation actions.'),
132
+ });
@@ -0,0 +1,2 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function createServer(): McpServer;
package/dist/server.js ADDED
@@ -0,0 +1,85 @@
1
+ import { InMemoryTaskStore } from '@modelcontextprotocol/sdk/experimental/tasks/stores/in-memory.js';
2
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+ import { readFileSync } from 'node:fs';
4
+ import { findPackageJSON } from 'node:module';
5
+ import { dirname, join } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { getErrorMessage } from './lib/errors.js';
8
+ import { registerAllPrompts } from './prompts/index.js';
9
+ import { registerAllResources } from './resources/index.js';
10
+ import { registerAllTools } from './tools/index.js';
11
+ function isPackageJsonMetadata(value) {
12
+ return (typeof value === 'object' &&
13
+ value !== null &&
14
+ 'version' in value &&
15
+ typeof value.version === 'string' &&
16
+ value.version.trim().length > 0);
17
+ }
18
+ function parsePackageJson(packageJson, packageJsonPath) {
19
+ let parsed;
20
+ try {
21
+ parsed = JSON.parse(packageJson);
22
+ }
23
+ catch (error) {
24
+ throw new Error(`Invalid JSON in ${packageJsonPath}: ${getErrorMessage(error)}`);
25
+ }
26
+ if (!isPackageJsonMetadata(parsed)) {
27
+ throw new Error(`Invalid package.json at ${packageJsonPath}: missing or invalid version field`);
28
+ }
29
+ return parsed;
30
+ }
31
+ function extractVersion(packageJson, packageJsonPath) {
32
+ return parsePackageJson(packageJson, packageJsonPath).version;
33
+ }
34
+ function readPackageJson(packageJsonPath) {
35
+ try {
36
+ return readFileSync(packageJsonPath, 'utf8');
37
+ }
38
+ catch (error) {
39
+ throw new Error(`Unable to read ${packageJsonPath}: ${getErrorMessage(error)}`);
40
+ }
41
+ }
42
+ function loadVersion() {
43
+ const packageJsonPath = findPackageJSON(import.meta.url);
44
+ if (!packageJsonPath) {
45
+ throw new Error('Unable to locate package.json for code-review-analyst.');
46
+ }
47
+ return extractVersion(readPackageJson(packageJsonPath), packageJsonPath);
48
+ }
49
+ const SERVER_VERSION = loadVersion();
50
+ function loadInstructions() {
51
+ const currentDir = dirname(fileURLToPath(import.meta.url));
52
+ try {
53
+ return readFileSync(join(currentDir, 'instructions.md'), 'utf8');
54
+ }
55
+ catch (error) {
56
+ process.emitWarning(`Failed to load instructions.md: ${getErrorMessage(error)}`);
57
+ return '(Instructions failed to load)';
58
+ }
59
+ }
60
+ const SERVER_INSTRUCTIONS = loadInstructions();
61
+ const SERVER_TASK_STORE = new InMemoryTaskStore();
62
+ export function createServer() {
63
+ const server = new McpServer({
64
+ name: 'code-review-analyst',
65
+ version: SERVER_VERSION,
66
+ }, {
67
+ instructions: SERVER_INSTRUCTIONS,
68
+ taskStore: SERVER_TASK_STORE,
69
+ capabilities: {
70
+ tasks: {
71
+ list: {},
72
+ cancel: {},
73
+ requests: {
74
+ tools: {
75
+ call: {},
76
+ },
77
+ },
78
+ },
79
+ },
80
+ });
81
+ registerAllTools(server);
82
+ registerAllResources(server, SERVER_INSTRUCTIONS);
83
+ registerAllPrompts(server, SERVER_INSTRUCTIONS);
84
+ return server;
85
+ }
@@ -0,0 +1,2 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerAllTools(server: McpServer): void;
@@ -0,0 +1,13 @@
1
+ import { registerReviewDiffTool } from './review-diff.js';
2
+ import { registerRiskScoreTool } from './risk-score.js';
3
+ import { registerSuggestPatchTool } from './suggest-patch.js';
4
+ const TOOL_REGISTRARS = [
5
+ registerReviewDiffTool,
6
+ registerRiskScoreTool,
7
+ registerSuggestPatchTool,
8
+ ];
9
+ export function registerAllTools(server) {
10
+ for (const registerTool of TOOL_REGISTRARS) {
11
+ registerTool(server);
12
+ }
13
+ }
@@ -0,0 +1,2 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerReviewDiffTool(server: McpServer): void;
@@ -0,0 +1,41 @@
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
+ }
@@ -0,0 +1,2 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerRiskScoreTool(server: McpServer): void;
@@ -0,0 +1,33 @@
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
+ }
@@ -0,0 +1,2 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerSuggestPatchTool(server: McpServer): void;
@@ -0,0 +1,34 @@
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
+ }
package/package.json ADDED
@@ -0,0 +1,77 @@
1
+ {
2
+ "name": "@j0hanz/code-review-analyst-mcp",
3
+ "version": "0.1.0",
4
+ "mcpName": "io.github.j0hanz/code-review-analyst",
5
+ "description": "Gemini-powered MCP server for code review analysis.",
6
+ "type": "module",
7
+ "main": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "default": "./dist/index.js"
13
+ },
14
+ "./package.json": "./package.json"
15
+ },
16
+ "bin": {
17
+ "code-review-analyst-mcp": "dist/index.js"
18
+ },
19
+ "files": [
20
+ "dist",
21
+ "README.md"
22
+ ],
23
+ "author": "j0hanz",
24
+ "license": "MIT",
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "https://github.com/j0hanz/code-review-analyst-mcp.git"
28
+ },
29
+ "homepage": "https://github.com/j0hanz/code-review-analyst-mcp#readme",
30
+ "engines": {
31
+ "node": ">=24"
32
+ },
33
+ "scripts": {
34
+ "clean": "node scripts/tasks.mjs clean",
35
+ "validate:instructions": "node scripts/tasks.mjs validate:instructions",
36
+ "build": "node scripts/tasks.mjs build",
37
+ "copy:assets": "node scripts/tasks.mjs copy:assets",
38
+ "prepare": "npm run build",
39
+ "dev": "tsc --watch --preserveWatchOutput",
40
+ "dev:run": "node --env-file=.env --watch dist/index.js",
41
+ "start": "node dist/index.js",
42
+ "format": "prettier --write .",
43
+ "type-check": "node scripts/tasks.mjs type-check",
44
+ "type-check:diagnostics": "tsc --noEmit --extendedDiagnostics",
45
+ "type-check:trace": "node -e \"require('fs').rmSync('.ts-trace',{recursive:true,force:true})\" && tsc --noEmit --generateTrace .ts-trace",
46
+ "lint": "eslint .",
47
+ "lint:fix": "eslint . --fix",
48
+ "test": "node scripts/tasks.mjs test",
49
+ "test:fast": "node --test --import tsx/esm src/__tests__/**/*.test.ts tests/**/*.test.ts",
50
+ "test:coverage": "node scripts/tasks.mjs test --coverage",
51
+ "knip": "knip",
52
+ "knip:fix": "knip --fix",
53
+ "inspector": "npm run build && npx -y @modelcontextprotocol/inspector node dist/index.js",
54
+ "prepublishOnly": "npm run lint && npm run type-check && npm run build"
55
+ },
56
+ "dependencies": {
57
+ "@google/genai": "^1.41.0",
58
+ "@modelcontextprotocol/sdk": "^1.26.0",
59
+ "zod": "^4.3.6"
60
+ },
61
+ "devDependencies": {
62
+ "@trivago/prettier-plugin-sort-imports": "^6.0.2",
63
+ "@types/node": "^24",
64
+ "@typescript-eslint/eslint-plugin": "^8.56.0",
65
+ "@typescript-eslint/parser": "^8.56.0",
66
+ "eslint": "^9.39.2",
67
+ "eslint-config-prettier": "^10.1.8",
68
+ "eslint-plugin-de-morgan": "^2.0.0",
69
+ "eslint-plugin-depend": "^1.4.0",
70
+ "eslint-plugin-sonarjs": "^3.0.7",
71
+ "eslint-plugin-unused-imports": "^4.4.1",
72
+ "prettier": "^3.8.1",
73
+ "tsx": "^4.21.0",
74
+ "typescript": "^5.9.3",
75
+ "typescript-eslint": "^8.56.0"
76
+ }
77
+ }