@j0hanz/code-review-analyst-mcp 1.2.1 → 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/index.js +12 -3
- package/dist/lib/context-budget.d.ts +2 -2
- package/dist/lib/context-budget.js +12 -6
- package/dist/lib/diff-budget.js +6 -2
- package/dist/lib/diff-cleaner.d.ts +12 -0
- package/dist/lib/diff-cleaner.js +51 -0
- package/dist/lib/diff-parser.js +31 -36
- package/dist/lib/diff-store.d.ts +22 -0
- package/dist/lib/diff-store.js +28 -0
- package/dist/lib/env-config.d.ts +1 -0
- package/dist/lib/env-config.js +9 -3
- package/dist/lib/errors.d.ts +1 -0
- package/dist/lib/errors.js +5 -5
- package/dist/lib/gemini-schema.js +2 -1
- package/dist/lib/gemini.js +135 -67
- package/dist/lib/model-config.d.ts +14 -2
- package/dist/lib/model-config.js +30 -6
- package/dist/lib/tool-contracts.d.ts +222 -0
- package/dist/lib/tool-contracts.js +289 -0
- package/dist/lib/tool-factory.d.ts +5 -1
- package/dist/lib/tool-factory.js +48 -54
- package/dist/lib/tool-response.js +10 -12
- package/dist/lib/types.d.ts +3 -3
- package/dist/prompts/index.js +47 -41
- package/dist/resources/index.d.ts +1 -1
- package/dist/resources/index.js +99 -18
- package/dist/resources/instructions.d.ts +1 -0
- package/dist/resources/instructions.js +69 -0
- package/dist/resources/server-config.d.ts +1 -0
- package/dist/resources/server-config.js +71 -0
- package/dist/resources/tool-catalog.d.ts +1 -0
- package/dist/resources/tool-catalog.js +39 -0
- package/dist/resources/tool-info.d.ts +5 -0
- package/dist/resources/tool-info.js +122 -0
- package/dist/resources/workflows.d.ts +1 -0
- package/dist/resources/workflows.js +72 -0
- package/dist/schemas/inputs.d.ts +6 -5
- package/dist/schemas/inputs.js +17 -29
- package/dist/schemas/outputs.d.ts +17 -1
- package/dist/schemas/outputs.js +84 -52
- package/dist/server.js +28 -27
- package/dist/tools/analyze-complexity.d.ts +2 -0
- package/dist/tools/analyze-complexity.js +51 -0
- package/dist/tools/analyze-pr-impact.js +32 -20
- package/dist/tools/detect-api-breaking.d.ts +2 -0
- package/dist/tools/detect-api-breaking.js +48 -0
- package/dist/tools/generate-diff.d.ts +2 -0
- package/dist/tools/generate-diff.js +71 -0
- package/dist/tools/generate-review-summary.js +34 -29
- package/dist/tools/generate-test-plan.js +38 -28
- package/dist/tools/index.js +11 -2
- package/dist/tools/inspect-code-quality.js +47 -36
- package/dist/tools/suggest-search-replace.js +34 -20
- package/package.json +1 -2
- package/dist/instructions.md +0 -149
package/dist/schemas/inputs.d.ts
CHANGED
|
@@ -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,14 +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>;
|
|
34
|
+
export declare const AnalyzeComplexityInputSchema: z.ZodObject<{
|
|
35
|
+
language: z.ZodOptional<z.ZodString>;
|
|
36
|
+
}, z.core.$strict>;
|
|
37
|
+
export declare const DetectApiBreakingInputSchema: z.ZodObject<{
|
|
38
|
+
language: z.ZodOptional<z.ZodString>;
|
|
39
|
+
}, z.core.$strict>;
|
package/dist/schemas/inputs.js
CHANGED
|
@@ -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: {
|
|
@@ -24,29 +23,26 @@ function createOptionalBoundedString(min, max, description) {
|
|
|
24
23
|
function createLanguageSchema(description) {
|
|
25
24
|
return createOptionalBoundedString(INPUT_LIMITS.language.min, INPUT_LIMITS.language.max, description);
|
|
26
25
|
}
|
|
27
|
-
function
|
|
28
|
-
return
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
26
|
+
function createRepositorySchema() {
|
|
27
|
+
return createBoundedString(INPUT_LIMITS.repository.min, INPUT_LIMITS.repository.max, 'Repository identifier, e.g. org/repo.');
|
|
28
|
+
}
|
|
29
|
+
function createOptionalBoundedInteger(min, max, description) {
|
|
30
|
+
return z.number().int().min(min).max(max).optional().describe(description);
|
|
32
31
|
}
|
|
33
32
|
export const FileContextSchema = z.strictObject({
|
|
34
33
|
path: createBoundedString(INPUT_LIMITS.fileContext.path.min, INPUT_LIMITS.fileContext.path.max, 'File path relative to repo root.'),
|
|
35
34
|
content: createBoundedString(INPUT_LIMITS.fileContext.content.min, INPUT_LIMITS.fileContext.content.max, 'Full file content.'),
|
|
36
35
|
});
|
|
37
36
|
export const AnalyzePrImpactInputSchema = z.strictObject({
|
|
38
|
-
|
|
39
|
-
repository: createBoundedString(INPUT_LIMITS.repository.min, INPUT_LIMITS.repository.max, 'Repository identifier, e.g. org/repo.'),
|
|
37
|
+
repository: createRepositorySchema(),
|
|
40
38
|
language: createLanguageSchema('Primary language to bias analysis.'),
|
|
41
39
|
});
|
|
42
40
|
export const GenerateReviewSummaryInputSchema = z.strictObject({
|
|
43
|
-
|
|
44
|
-
repository: createBoundedString(INPUT_LIMITS.repository.min, INPUT_LIMITS.repository.max, 'Repository identifier, e.g. org/repo.'),
|
|
41
|
+
repository: createRepositorySchema(),
|
|
45
42
|
language: createLanguageSchema('Primary implementation language.'),
|
|
46
43
|
});
|
|
47
44
|
export const InspectCodeQualityInputSchema = z.strictObject({
|
|
48
|
-
|
|
49
|
-
repository: createBoundedString(INPUT_LIMITS.repository.min, INPUT_LIMITS.repository.max, 'Repository identifier, e.g. org/repo.'),
|
|
45
|
+
repository: createRepositorySchema(),
|
|
50
46
|
language: createLanguageSchema('Primary language.'),
|
|
51
47
|
focusAreas: z
|
|
52
48
|
.array(createBoundedString(INPUT_LIMITS.focusArea.min, INPUT_LIMITS.focusArea.max, 'Focus area tag value.'))
|
|
@@ -54,13 +50,7 @@ export const InspectCodeQualityInputSchema = z.strictObject({
|
|
|
54
50
|
.max(INPUT_LIMITS.focusArea.maxItems)
|
|
55
51
|
.optional()
|
|
56
52
|
.describe('Specific areas to inspect: security, correctness, etc.'),
|
|
57
|
-
maxFindings:
|
|
58
|
-
.number()
|
|
59
|
-
.int()
|
|
60
|
-
.min(1)
|
|
61
|
-
.max(25)
|
|
62
|
-
.optional()
|
|
63
|
-
.describe('Maximum number of findings to return.'),
|
|
53
|
+
maxFindings: createOptionalBoundedInteger(INPUT_LIMITS.maxFindings.min, INPUT_LIMITS.maxFindings.max, 'Maximum number of findings to return.'),
|
|
64
54
|
files: z
|
|
65
55
|
.array(FileContextSchema)
|
|
66
56
|
.min(1)
|
|
@@ -69,20 +59,18 @@ export const InspectCodeQualityInputSchema = z.strictObject({
|
|
|
69
59
|
.describe('Full file contents for context-aware analysis. Provide the files changed in the diff for best results.'),
|
|
70
60
|
});
|
|
71
61
|
export const SuggestSearchReplaceInputSchema = z.strictObject({
|
|
72
|
-
diff: createDiffSchema('Unified diff that contains the issue to fix.'),
|
|
73
62
|
findingTitle: createBoundedString(INPUT_LIMITS.findingTitle.min, INPUT_LIMITS.findingTitle.max, 'Short title of the finding to fix.'),
|
|
74
63
|
findingDetails: createBoundedString(INPUT_LIMITS.findingDetails.min, INPUT_LIMITS.findingDetails.max, 'Detailed explanation of the bug or risk.'),
|
|
75
64
|
});
|
|
76
65
|
export const GenerateTestPlanInputSchema = z.strictObject({
|
|
77
|
-
|
|
78
|
-
repository: createBoundedString(INPUT_LIMITS.repository.min, INPUT_LIMITS.repository.max, 'Repository identifier, e.g. org/repo.'),
|
|
66
|
+
repository: createRepositorySchema(),
|
|
79
67
|
language: createLanguageSchema('Primary language.'),
|
|
80
68
|
testFramework: createOptionalBoundedString(INPUT_LIMITS.testFramework.min, INPUT_LIMITS.testFramework.max, 'Test framework to use, e.g. jest, vitest, pytest, node:test.'),
|
|
81
|
-
maxTestCases:
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
69
|
+
maxTestCases: createOptionalBoundedInteger(INPUT_LIMITS.maxTestCases.min, INPUT_LIMITS.maxTestCases.max, 'Maximum number of test cases to return.'),
|
|
70
|
+
});
|
|
71
|
+
export const AnalyzeComplexityInputSchema = z.strictObject({
|
|
72
|
+
language: createLanguageSchema('Primary language to bias analysis.'),
|
|
73
|
+
});
|
|
74
|
+
export const DetectApiBreakingInputSchema = z.strictObject({
|
|
75
|
+
language: createLanguageSchema('Primary language to bias analysis.'),
|
|
88
76
|
});
|
|
@@ -98,6 +98,7 @@ export declare const CodeQualityResultSchema: z.ZodObject<{
|
|
|
98
98
|
contextualInsights: z.ZodArray<z.ZodString>;
|
|
99
99
|
}, z.core.$strict>;
|
|
100
100
|
export declare const CodeQualityOutputSchema: z.ZodObject<{
|
|
101
|
+
totalFindings: z.ZodOptional<z.ZodNumber>;
|
|
101
102
|
summary: z.ZodString;
|
|
102
103
|
overallRisk: z.ZodEnum<{
|
|
103
104
|
low: "low";
|
|
@@ -120,7 +121,6 @@ export declare const CodeQualityOutputSchema: z.ZodObject<{
|
|
|
120
121
|
}, z.core.$strict>>;
|
|
121
122
|
testsNeeded: z.ZodArray<z.ZodString>;
|
|
122
123
|
contextualInsights: z.ZodArray<z.ZodString>;
|
|
123
|
-
totalFindings: z.ZodOptional<z.ZodNumber>;
|
|
124
124
|
}, z.core.$strip>;
|
|
125
125
|
export declare const SearchReplaceBlockSchema: z.ZodObject<{
|
|
126
126
|
file: z.ZodString;
|
|
@@ -180,3 +180,19 @@ export declare const TestPlanResultSchema: z.ZodObject<{
|
|
|
180
180
|
}, z.core.$strict>>;
|
|
181
181
|
coverageSummary: z.ZodString;
|
|
182
182
|
}, z.core.$strict>;
|
|
183
|
+
export declare const AnalyzeComplexityResultSchema: z.ZodObject<{
|
|
184
|
+
timeComplexity: z.ZodString;
|
|
185
|
+
spaceComplexity: z.ZodString;
|
|
186
|
+
explanation: z.ZodString;
|
|
187
|
+
potentialBottlenecks: z.ZodArray<z.ZodString>;
|
|
188
|
+
isDegradation: z.ZodBoolean;
|
|
189
|
+
}, z.core.$strict>;
|
|
190
|
+
export declare const DetectApiBreakingResultSchema: z.ZodObject<{
|
|
191
|
+
hasBreakingChanges: z.ZodBoolean;
|
|
192
|
+
breakingChanges: z.ZodArray<z.ZodObject<{
|
|
193
|
+
element: z.ZodString;
|
|
194
|
+
natureOfChange: z.ZodString;
|
|
195
|
+
consumerImpact: z.ZodString;
|
|
196
|
+
suggestedMitigation: z.ZodString;
|
|
197
|
+
}, z.core.$strict>>;
|
|
198
|
+
}, z.core.$strict>;
|
package/dist/schemas/outputs.js
CHANGED
|
@@ -12,16 +12,30 @@ const OUTPUT_LIMITS = {
|
|
|
12
12
|
findingsMax: 50,
|
|
13
13
|
testsNeeded: { minItems: 0, maxItems: 20, itemMin: 1, itemMax: 300 },
|
|
14
14
|
},
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
complexity: {
|
|
16
|
+
timeComplexity: { min: 1, max: 200 },
|
|
17
|
+
spaceComplexity: { min: 1, max: 200 },
|
|
18
|
+
explanation: { min: 1, max: 2_000 },
|
|
19
|
+
bottleneck: { min: 1, max: 500, maxItems: 10 },
|
|
18
20
|
},
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
apiBreaking: {
|
|
22
|
+
element: { min: 1, max: 300 },
|
|
23
|
+
natureOfChange: { min: 1, max: 500 },
|
|
24
|
+
consumerImpact: { min: 1, max: 500 },
|
|
25
|
+
suggestedMitigation: { min: 1, max: 500 },
|
|
26
|
+
maxItems: 20,
|
|
23
27
|
},
|
|
24
28
|
};
|
|
29
|
+
const QUALITY_RISK_LEVELS = ['low', 'medium', 'high', 'critical'];
|
|
30
|
+
const MERGE_RISK_LEVELS = ['low', 'medium', 'high'];
|
|
31
|
+
const ERROR_KINDS = [
|
|
32
|
+
'validation',
|
|
33
|
+
'budget',
|
|
34
|
+
'upstream',
|
|
35
|
+
'timeout',
|
|
36
|
+
'cancelled',
|
|
37
|
+
'internal',
|
|
38
|
+
];
|
|
25
39
|
function createBoundedString(min, max, description) {
|
|
26
40
|
return z.string().min(min).max(max).describe(description);
|
|
27
41
|
}
|
|
@@ -32,6 +46,22 @@ function createBoundedStringArray(itemMin, itemMax, minItems, maxItems, descript
|
|
|
32
46
|
.max(maxItems)
|
|
33
47
|
.describe(description);
|
|
34
48
|
}
|
|
49
|
+
function createReviewSummarySchema(description) {
|
|
50
|
+
return z
|
|
51
|
+
.string()
|
|
52
|
+
.min(OUTPUT_LIMITS.reviewDiffResult.summary.min)
|
|
53
|
+
.max(OUTPUT_LIMITS.reviewDiffResult.summary.max)
|
|
54
|
+
.describe(description);
|
|
55
|
+
}
|
|
56
|
+
const reviewFindingSeveritySchema = z
|
|
57
|
+
.enum(QUALITY_RISK_LEVELS)
|
|
58
|
+
.describe('Severity for this issue.');
|
|
59
|
+
const qualityRiskSchema = z
|
|
60
|
+
.enum(QUALITY_RISK_LEVELS)
|
|
61
|
+
.describe('Overall risk with full context.');
|
|
62
|
+
const mergeRiskSchema = z
|
|
63
|
+
.enum(MERGE_RISK_LEVELS)
|
|
64
|
+
.describe('High-level merge risk.');
|
|
35
65
|
export const DefaultOutputSchema = z.strictObject({
|
|
36
66
|
ok: z.boolean().describe('Whether the tool completed successfully.'),
|
|
37
67
|
result: z.unknown().optional().describe('Successful result payload.'),
|
|
@@ -44,14 +74,7 @@ export const DefaultOutputSchema = z.strictObject({
|
|
|
44
74
|
.optional()
|
|
45
75
|
.describe('Whether the client should retry this request.'),
|
|
46
76
|
kind: z
|
|
47
|
-
.enum(
|
|
48
|
-
'validation',
|
|
49
|
-
'budget',
|
|
50
|
-
'upstream',
|
|
51
|
-
'timeout',
|
|
52
|
-
'cancelled',
|
|
53
|
-
'internal',
|
|
54
|
-
])
|
|
77
|
+
.enum(ERROR_KINDS)
|
|
55
78
|
.optional()
|
|
56
79
|
.describe('Machine-readable error category.'),
|
|
57
80
|
})
|
|
@@ -59,9 +82,7 @@ export const DefaultOutputSchema = z.strictObject({
|
|
|
59
82
|
.describe('Error payload when ok is false.'),
|
|
60
83
|
});
|
|
61
84
|
export const ReviewFindingSchema = z.strictObject({
|
|
62
|
-
severity:
|
|
63
|
-
.enum(['low', 'medium', 'high', 'critical'])
|
|
64
|
-
.describe('Severity for this issue.'),
|
|
85
|
+
severity: reviewFindingSeveritySchema,
|
|
65
86
|
file: z
|
|
66
87
|
.string()
|
|
67
88
|
.min(1)
|
|
@@ -75,13 +96,22 @@ export const ReviewFindingSchema = z.strictObject({
|
|
|
75
96
|
.nullable()
|
|
76
97
|
.describe('1-based line number when known, otherwise null.'),
|
|
77
98
|
title: createBoundedString(OUTPUT_LIMITS.reviewFinding.title.min, OUTPUT_LIMITS.reviewFinding.title.max, 'Short finding title.'),
|
|
78
|
-
explanation: createBoundedString(OUTPUT_LIMITS.reviewFinding.text.min, OUTPUT_LIMITS.reviewFinding.text.max, '
|
|
79
|
-
recommendation: createBoundedString(OUTPUT_LIMITS.reviewFinding.text.min, OUTPUT_LIMITS.reviewFinding.text.max, 'Concrete fix
|
|
99
|
+
explanation: createBoundedString(OUTPUT_LIMITS.reviewFinding.text.min, OUTPUT_LIMITS.reviewFinding.text.max, 'What the issue is and its runtime, security, or correctness impact.'),
|
|
100
|
+
recommendation: createBoundedString(OUTPUT_LIMITS.reviewFinding.text.min, OUTPUT_LIMITS.reviewFinding.text.max, 'Concrete fix - name the exact code, function, or pattern to change.'),
|
|
80
101
|
});
|
|
102
|
+
const CODE_QUALITY_SHARED_FIELDS = {
|
|
103
|
+
summary: createReviewSummarySchema('Deep-dive review summary.'),
|
|
104
|
+
overallRisk: qualityRiskSchema,
|
|
105
|
+
findings: z
|
|
106
|
+
.array(ReviewFindingSchema)
|
|
107
|
+
.min(0)
|
|
108
|
+
.max(30)
|
|
109
|
+
.describe('Findings ordered by severity, highest first.'),
|
|
110
|
+
testsNeeded: createBoundedStringArray(1, 300, 0, 12, 'Test cases needed to validate this change.'),
|
|
111
|
+
contextualInsights: createBoundedStringArray(1, 500, 0, 5, 'Cross-file insights only discoverable from the full file context. Omit when no file context was provided.'),
|
|
112
|
+
};
|
|
81
113
|
export const PrImpactResultSchema = z.strictObject({
|
|
82
|
-
severity: z
|
|
83
|
-
.enum(['low', 'medium', 'high', 'critical'])
|
|
84
|
-
.describe('Overall impact severity.'),
|
|
114
|
+
severity: z.enum(QUALITY_RISK_LEVELS).describe('Overall impact severity.'),
|
|
85
115
|
categories: z
|
|
86
116
|
.array(z.enum([
|
|
87
117
|
'breaking_change',
|
|
@@ -106,10 +136,8 @@ export const PrImpactResultSchema = z.strictObject({
|
|
|
106
136
|
.describe('Estimated difficulty to revert this change.'),
|
|
107
137
|
});
|
|
108
138
|
export const ReviewSummaryResultSchema = z.strictObject({
|
|
109
|
-
summary:
|
|
110
|
-
overallRisk:
|
|
111
|
-
.enum(['low', 'medium', 'high'])
|
|
112
|
-
.describe('High-level merge risk.'),
|
|
139
|
+
summary: createReviewSummarySchema('Human-readable PR summary.'),
|
|
140
|
+
overallRisk: mergeRiskSchema,
|
|
113
141
|
keyChanges: createBoundedStringArray(1, 300, 1, 15, 'Most important changes, ordered by significance.'),
|
|
114
142
|
recommendation: z
|
|
115
143
|
.string()
|
|
@@ -129,30 +157,10 @@ export const ReviewSummaryResultSchema = z.strictObject({
|
|
|
129
157
|
.describe('Change statistics (computed from diff before Gemini call).'),
|
|
130
158
|
});
|
|
131
159
|
export const CodeQualityResultSchema = z.strictObject({
|
|
132
|
-
|
|
133
|
-
overallRisk: z
|
|
134
|
-
.enum(['low', 'medium', 'high', 'critical'])
|
|
135
|
-
.describe('Overall risk with full context.'),
|
|
136
|
-
findings: z
|
|
137
|
-
.array(ReviewFindingSchema)
|
|
138
|
-
.min(0)
|
|
139
|
-
.max(30)
|
|
140
|
-
.describe('Findings ordered by severity, highest first.'),
|
|
141
|
-
testsNeeded: createBoundedStringArray(1, 300, 0, 12, 'Test cases needed to validate this change.'),
|
|
142
|
-
contextualInsights: createBoundedStringArray(1, 500, 0, 5, 'Insights only possible with full file context that diff alone cannot reveal.'),
|
|
160
|
+
...CODE_QUALITY_SHARED_FIELDS,
|
|
143
161
|
});
|
|
144
162
|
export const CodeQualityOutputSchema = z.object({
|
|
145
|
-
|
|
146
|
-
overallRisk: z
|
|
147
|
-
.enum(['low', 'medium', 'high', 'critical'])
|
|
148
|
-
.describe('Overall risk with full context.'),
|
|
149
|
-
findings: z
|
|
150
|
-
.array(ReviewFindingSchema)
|
|
151
|
-
.min(0)
|
|
152
|
-
.max(30)
|
|
153
|
-
.describe('Findings ordered by severity, highest first.'),
|
|
154
|
-
testsNeeded: createBoundedStringArray(1, 300, 0, 12, 'Test cases needed to validate this change.'),
|
|
155
|
-
contextualInsights: createBoundedStringArray(1, 500, 0, 5, 'Insights only possible with full file context that diff alone cannot reveal.'),
|
|
163
|
+
...CODE_QUALITY_SHARED_FIELDS,
|
|
156
164
|
totalFindings: z
|
|
157
165
|
.number()
|
|
158
166
|
.int()
|
|
@@ -166,17 +174,17 @@ export const SearchReplaceBlockSchema = z.strictObject({
|
|
|
166
174
|
.string()
|
|
167
175
|
.min(1)
|
|
168
176
|
.max(5000)
|
|
169
|
-
.describe('
|
|
177
|
+
.describe('Verbatim source text to find - character-exact including all whitespace and indentation.'),
|
|
170
178
|
replace: z
|
|
171
179
|
.string()
|
|
172
180
|
.min(0)
|
|
173
181
|
.max(5000)
|
|
174
|
-
.describe('Replacement text
|
|
182
|
+
.describe('Replacement text. Use empty string to delete.'),
|
|
175
183
|
explanation: z
|
|
176
184
|
.string()
|
|
177
185
|
.min(1)
|
|
178
186
|
.max(500)
|
|
179
|
-
.describe('Why this
|
|
187
|
+
.describe('Why this patch fixes the finding.'),
|
|
180
188
|
});
|
|
181
189
|
export const SearchReplaceResultSchema = z.strictObject({
|
|
182
190
|
summary: z.string().min(1).max(1000).describe('What the fix accomplishes.'),
|
|
@@ -227,3 +235,27 @@ export const TestPlanResultSchema = z.strictObject({
|
|
|
227
235
|
.max(500)
|
|
228
236
|
.describe('Summary of coverage gaps this plan addresses.'),
|
|
229
237
|
});
|
|
238
|
+
export const AnalyzeComplexityResultSchema = z.strictObject({
|
|
239
|
+
timeComplexity: createBoundedString(OUTPUT_LIMITS.complexity.timeComplexity.min, OUTPUT_LIMITS.complexity.timeComplexity.max, 'Big-O time complexity of the changed code (e.g., O(n log n)).'),
|
|
240
|
+
spaceComplexity: createBoundedString(OUTPUT_LIMITS.complexity.spaceComplexity.min, OUTPUT_LIMITS.complexity.spaceComplexity.max, 'Big-O space complexity of the changed code (e.g., O(n)).'),
|
|
241
|
+
explanation: createBoundedString(OUTPUT_LIMITS.complexity.explanation.min, OUTPUT_LIMITS.complexity.explanation.max, 'Detailed explanation of the complexity analysis, including key factors such as loop nesting and recursive calls.'),
|
|
242
|
+
potentialBottlenecks: createBoundedStringArray(OUTPUT_LIMITS.complexity.bottleneck.min, OUTPUT_LIMITS.complexity.bottleneck.max, 0, OUTPUT_LIMITS.complexity.bottleneck.maxItems, 'Potential performance bottlenecks identified in the change.'),
|
|
243
|
+
isDegradation: z
|
|
244
|
+
.boolean()
|
|
245
|
+
.describe('True if the change introduces a performance degradation compared to the original code.'),
|
|
246
|
+
});
|
|
247
|
+
export const DetectApiBreakingResultSchema = z.strictObject({
|
|
248
|
+
hasBreakingChanges: z
|
|
249
|
+
.boolean()
|
|
250
|
+
.describe('True if any breaking changes were detected.'),
|
|
251
|
+
breakingChanges: z
|
|
252
|
+
.array(z.strictObject({
|
|
253
|
+
element: createBoundedString(OUTPUT_LIMITS.apiBreaking.element.min, OUTPUT_LIMITS.apiBreaking.element.max, 'Name of the API element that changed (e.g., function signature, interface field, exported constant).'),
|
|
254
|
+
natureOfChange: createBoundedString(OUTPUT_LIMITS.apiBreaking.natureOfChange.min, OUTPUT_LIMITS.apiBreaking.natureOfChange.max, 'What changed and why it is breaking (e.g., removed parameter, changed return type, renamed export).'),
|
|
255
|
+
consumerImpact: createBoundedString(OUTPUT_LIMITS.apiBreaking.consumerImpact.min, OUTPUT_LIMITS.apiBreaking.consumerImpact.max, 'How existing consumers will be affected by this change.'),
|
|
256
|
+
suggestedMitigation: createBoundedString(OUTPUT_LIMITS.apiBreaking.suggestedMitigation.min, OUTPUT_LIMITS.apiBreaking.suggestedMitigation.max, 'Recommended mitigation strategy for consumers impacted by this breaking change.'),
|
|
257
|
+
}))
|
|
258
|
+
.min(0)
|
|
259
|
+
.max(OUTPUT_LIMITS.apiBreaking.maxItems)
|
|
260
|
+
.describe('List of breaking changes detected. Empty when hasBreakingChanges is false.'),
|
|
261
|
+
});
|
package/dist/server.js
CHANGED
|
@@ -2,22 +2,20 @@ 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 {
|
|
6
|
-
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { initDiffStore } from './lib/diff-store.js';
|
|
7
6
|
import { getErrorMessage } from './lib/errors.js';
|
|
8
7
|
import { registerAllPrompts } from './prompts/index.js';
|
|
9
8
|
import { registerAllResources } from './resources/index.js';
|
|
9
|
+
import { buildServerInstructions } from './resources/instructions.js';
|
|
10
10
|
import { registerAllTools } from './tools/index.js';
|
|
11
11
|
const SERVER_NAME = 'code-review-analyst';
|
|
12
|
-
const INSTRUCTIONS_FILENAME = 'instructions.md';
|
|
13
|
-
const INSTRUCTIONS_FALLBACK = '(Instructions failed to load)';
|
|
14
12
|
const UTF8_ENCODING = 'utf8';
|
|
15
|
-
const
|
|
16
|
-
const
|
|
13
|
+
const PACKAGE_VERSION_FIELD = 'version';
|
|
14
|
+
const VERSION_FIELD_ERROR = 'missing or invalid version field';
|
|
17
15
|
const SERVER_CAPABILITIES = {
|
|
18
16
|
logging: {},
|
|
19
17
|
completions: {},
|
|
20
|
-
resources: {},
|
|
18
|
+
resources: { subscribe: true },
|
|
21
19
|
tools: {},
|
|
22
20
|
tasks: {
|
|
23
21
|
list: {},
|
|
@@ -32,22 +30,27 @@ const SERVER_CAPABILITIES = {
|
|
|
32
30
|
function isPackageJsonMetadata(value) {
|
|
33
31
|
return (typeof value === 'object' &&
|
|
34
32
|
value !== null &&
|
|
35
|
-
|
|
33
|
+
PACKAGE_VERSION_FIELD in value &&
|
|
36
34
|
typeof value.version === 'string' &&
|
|
37
35
|
value.version.trim().length > 0);
|
|
38
36
|
}
|
|
39
37
|
function parsePackageJson(packageJson, packageJsonPath) {
|
|
40
|
-
|
|
38
|
+
const parsed = parseJsonText(packageJson, packageJsonPath);
|
|
39
|
+
ensurePackageJsonMetadata(parsed, packageJsonPath);
|
|
40
|
+
return parsed;
|
|
41
|
+
}
|
|
42
|
+
function parseJsonText(jsonText, sourcePath) {
|
|
41
43
|
try {
|
|
42
|
-
|
|
44
|
+
return JSON.parse(jsonText);
|
|
43
45
|
}
|
|
44
46
|
catch (error) {
|
|
45
|
-
throw new Error(`Invalid JSON in ${
|
|
47
|
+
throw new Error(`Invalid JSON in ${sourcePath}: ${getErrorMessage(error)}`);
|
|
46
48
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
+
}
|
|
50
|
+
function ensurePackageJsonMetadata(metadata, packageJsonPath) {
|
|
51
|
+
if (!isPackageJsonMetadata(metadata)) {
|
|
52
|
+
throw new Error(`Invalid package.json at ${packageJsonPath}: ${VERSION_FIELD_ERROR}`);
|
|
49
53
|
}
|
|
50
|
-
return parsed;
|
|
51
54
|
}
|
|
52
55
|
function readUtf8File(path) {
|
|
53
56
|
try {
|
|
@@ -66,19 +69,9 @@ function loadVersion() {
|
|
|
66
69
|
return parsePackageJson(packageJsonText, packageJsonPath).version;
|
|
67
70
|
}
|
|
68
71
|
const SERVER_VERSION = loadVersion();
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
catch (error) {
|
|
74
|
-
process.emitWarning(`Failed to load ${INSTRUCTIONS_FILENAME}: ${getErrorMessage(error)}`);
|
|
75
|
-
return INSTRUCTIONS_FALLBACK;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
const SERVER_INSTRUCTIONS = loadInstructions();
|
|
79
|
-
export function createServer() {
|
|
80
|
-
const taskStore = new InMemoryTaskStore();
|
|
81
|
-
const server = new McpServer({
|
|
72
|
+
const SERVER_INSTRUCTIONS = buildServerInstructions();
|
|
73
|
+
function createMcpServer(taskStore) {
|
|
74
|
+
return new McpServer({
|
|
82
75
|
name: SERVER_NAME,
|
|
83
76
|
version: SERVER_VERSION,
|
|
84
77
|
}, {
|
|
@@ -86,9 +79,17 @@ export function createServer() {
|
|
|
86
79
|
taskStore,
|
|
87
80
|
capabilities: SERVER_CAPABILITIES,
|
|
88
81
|
});
|
|
82
|
+
}
|
|
83
|
+
function registerServerCapabilities(server) {
|
|
84
|
+
initDiffStore(server);
|
|
89
85
|
registerAllTools(server);
|
|
90
86
|
registerAllResources(server, SERVER_INSTRUCTIONS);
|
|
91
87
|
registerAllPrompts(server, SERVER_INSTRUCTIONS);
|
|
88
|
+
}
|
|
89
|
+
export function createServer() {
|
|
90
|
+
const taskStore = new InMemoryTaskStore();
|
|
91
|
+
const server = createMcpServer(taskStore);
|
|
92
|
+
registerServerCapabilities(server);
|
|
92
93
|
const shutdown = async () => {
|
|
93
94
|
await server.close();
|
|
94
95
|
taskStore.cleanup();
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { validateDiffBudget } from '../lib/diff-budget.js';
|
|
2
|
+
import { createNoDiffError, getDiff } from '../lib/diff-store.js';
|
|
3
|
+
import { requireToolContract } from '../lib/tool-contracts.js';
|
|
4
|
+
import { registerStructuredToolTask } from '../lib/tool-factory.js';
|
|
5
|
+
import { AnalyzeComplexityInputSchema } from '../schemas/inputs.js';
|
|
6
|
+
import { AnalyzeComplexityResultSchema } from '../schemas/outputs.js';
|
|
7
|
+
const SYSTEM_INSTRUCTION = `
|
|
8
|
+
You are an algorithm complexity analyst. Analyze the diff to identify Big-O time and space complexity for added or modified functions and algorithms.
|
|
9
|
+
Detect if the change introduces a performance degradation compared to the original code.
|
|
10
|
+
Identify potential bottlenecks arising from loop nesting, recursive calls, and auxiliary data structure usage.
|
|
11
|
+
Return strict JSON only.
|
|
12
|
+
`;
|
|
13
|
+
const TOOL_CONTRACT = requireToolContract('analyze_time_space_complexity');
|
|
14
|
+
export function registerAnalyzeComplexityTool(server) {
|
|
15
|
+
registerStructuredToolTask(server, {
|
|
16
|
+
name: 'analyze_time_space_complexity',
|
|
17
|
+
title: 'Analyze Time & Space Complexity',
|
|
18
|
+
description: 'Analyze Big-O complexity of the cached diff changes. Call generate_diff first.',
|
|
19
|
+
inputSchema: AnalyzeComplexityInputSchema,
|
|
20
|
+
fullInputSchema: AnalyzeComplexityInputSchema,
|
|
21
|
+
resultSchema: AnalyzeComplexityResultSchema,
|
|
22
|
+
errorCode: 'E_ANALYZE_COMPLEXITY',
|
|
23
|
+
model: TOOL_CONTRACT.model,
|
|
24
|
+
timeoutMs: TOOL_CONTRACT.timeoutMs,
|
|
25
|
+
maxOutputTokens: TOOL_CONTRACT.maxOutputTokens,
|
|
26
|
+
...(TOOL_CONTRACT.thinkingBudget !== undefined
|
|
27
|
+
? { thinkingBudget: TOOL_CONTRACT.thinkingBudget }
|
|
28
|
+
: undefined),
|
|
29
|
+
validateInput: () => {
|
|
30
|
+
const slot = getDiff();
|
|
31
|
+
if (!slot)
|
|
32
|
+
return createNoDiffError();
|
|
33
|
+
return validateDiffBudget(slot.diff);
|
|
34
|
+
},
|
|
35
|
+
formatOutcome: (result) => result.isDegradation
|
|
36
|
+
? 'Performance degradation detected'
|
|
37
|
+
: 'No degradation',
|
|
38
|
+
formatOutput: (result) => `Complexity Analysis: Time=${result.timeComplexity}, Space=${result.spaceComplexity}. ${result.explanation}`,
|
|
39
|
+
buildPrompt: (input) => {
|
|
40
|
+
const slot = getDiff();
|
|
41
|
+
const diff = slot?.diff ?? '';
|
|
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
|
+
},
|
|
50
|
+
});
|
|
51
|
+
}
|
|
@@ -1,46 +1,58 @@
|
|
|
1
1
|
import { validateDiffBudget } from '../lib/diff-budget.js';
|
|
2
2
|
import { computeDiffStatsAndSummaryFromFiles, parseDiffFiles, } from '../lib/diff-parser.js';
|
|
3
|
-
import {
|
|
3
|
+
import { createNoDiffError, getDiff } from '../lib/diff-store.js';
|
|
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';
|
|
6
7
|
import { PrImpactResultSchema } from '../schemas/outputs.js';
|
|
7
8
|
const SYSTEM_INSTRUCTION = `
|
|
8
|
-
You are a technical change analyst.
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
Focus on breaking changes, API modifications, and rollback complexity.
|
|
9
|
+
You are a technical change analyst. Analyze the diff for objective, evidence-based impact assessment.
|
|
10
|
+
Classify severity, categories, breaking changes, affected areas, and rollback complexity strictly from diff evidence.
|
|
11
|
+
Never infer behavior not visible in the diff.
|
|
12
12
|
Return strict JSON only.
|
|
13
13
|
`;
|
|
14
|
+
const TOOL_CONTRACT = requireToolContract('analyze_pr_impact');
|
|
15
|
+
function formatLanguageSegment(language) {
|
|
16
|
+
return language ? `\nLanguage: ${language}` : '';
|
|
17
|
+
}
|
|
14
18
|
export function registerAnalyzePrImpactTool(server) {
|
|
15
19
|
registerStructuredToolTask(server, {
|
|
16
20
|
name: 'analyze_pr_impact',
|
|
17
21
|
title: 'Analyze PR Impact',
|
|
18
|
-
description: 'Assess the impact and risk of
|
|
22
|
+
description: 'Assess the impact and risk of the cached diff. Call generate_diff first.',
|
|
19
23
|
inputSchema: AnalyzePrImpactInputSchema,
|
|
20
24
|
fullInputSchema: AnalyzePrImpactInputSchema,
|
|
21
25
|
resultSchema: PrImpactResultSchema,
|
|
22
26
|
errorCode: 'E_ANALYZE_IMPACT',
|
|
23
|
-
model:
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
model: TOOL_CONTRACT.model,
|
|
28
|
+
timeoutMs: TOOL_CONTRACT.timeoutMs,
|
|
29
|
+
maxOutputTokens: TOOL_CONTRACT.maxOutputTokens,
|
|
30
|
+
validateInput: () => {
|
|
31
|
+
const slot = getDiff();
|
|
32
|
+
if (!slot)
|
|
33
|
+
return createNoDiffError();
|
|
34
|
+
return validateDiffBudget(slot.diff);
|
|
28
35
|
},
|
|
36
|
+
formatOutcome: (result) => `severity: ${result.severity}`,
|
|
37
|
+
formatOutput: (result) => `Impact Analysis (${result.severity}): ${result.summary}`,
|
|
29
38
|
buildPrompt: (input) => {
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
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}
|
|
36
48
|
Change Stats: ${stats.files} files, +${stats.added} lines, -${stats.deleted} lines.
|
|
37
49
|
Changed Files:
|
|
38
50
|
${fileSummary}
|
|
39
51
|
|
|
40
52
|
Diff:
|
|
41
|
-
${
|
|
42
|
-
|
|
43
|
-
|
|
53
|
+
${diff}
|
|
54
|
+
`,
|
|
55
|
+
};
|
|
44
56
|
},
|
|
45
57
|
});
|
|
46
58
|
}
|