@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.
- package/dist/lib/diff-cleaner.d.ts +12 -0
- package/dist/lib/diff-cleaner.js +51 -0
- package/dist/lib/diff-store.d.ts +22 -0
- package/dist/lib/diff-store.js +28 -0
- package/dist/lib/tool-contracts.d.ts +26 -49
- package/dist/lib/tool-contracts.js +41 -54
- package/dist/lib/tool-factory.d.ts +14 -3
- package/dist/lib/tool-factory.js +9 -3
- package/dist/resources/index.js +19 -0
- package/dist/resources/instructions.js +10 -0
- package/dist/resources/server-config.js +1 -0
- package/dist/schemas/inputs.d.ts +0 -7
- package/dist/schemas/inputs.js +20 -32
- package/dist/server.js +3 -1
- package/dist/tools/analyze-complexity.js +18 -13
- package/dist/tools/analyze-pr-impact.js +26 -20
- package/dist/tools/detect-api-breaking.js +18 -13
- package/dist/tools/generate-diff.d.ts +2 -0
- package/dist/tools/generate-diff.js +71 -0
- package/dist/tools/generate-review-summary.js +32 -34
- package/dist/tools/generate-test-plan.js +26 -23
- package/dist/tools/index.js +2 -0
- package/dist/tools/inspect-code-quality.js +33 -29
- package/dist/tools/suggest-search-replace.js +24 -18
- package/package.json +1 -2
- package/dist/instructions.md +0 -7
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare const NOISY_EXCLUDE_PATHSPECS: readonly [":(exclude)package-lock.json", ":(exclude)yarn.lock", ":(exclude)pnpm-lock.yaml", ":(exclude)bun.lockb", ":(exclude)*.lock", ":(exclude)dist/", ":(exclude)build/", ":(exclude)out/", ":(exclude).next/", ":(exclude)coverage/", ":(exclude)*.min.js", ":(exclude)*.min.css", ":(exclude)*.map"];
|
|
2
|
+
/**
|
|
3
|
+
* Split raw unified diff into per-file sections and strip:
|
|
4
|
+
* - Binary file sections ("Binary files a/... and b/... differ")
|
|
5
|
+
* - GIT binary patch sections
|
|
6
|
+
* - Mode-only sections (permission changes with no content hunks)
|
|
7
|
+
*
|
|
8
|
+
* Does NOT modify content lines (+ / - / space) to preserve verbatim
|
|
9
|
+
* accuracy required by suggest_search_replace.
|
|
10
|
+
*/
|
|
11
|
+
export declare function cleanDiff(raw: string): string;
|
|
12
|
+
export declare function isEmptyDiff(diff: string): boolean;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export const NOISY_EXCLUDE_PATHSPECS = [
|
|
2
|
+
':(exclude)package-lock.json',
|
|
3
|
+
':(exclude)yarn.lock',
|
|
4
|
+
':(exclude)pnpm-lock.yaml',
|
|
5
|
+
':(exclude)bun.lockb',
|
|
6
|
+
':(exclude)*.lock',
|
|
7
|
+
':(exclude)dist/',
|
|
8
|
+
':(exclude)build/',
|
|
9
|
+
':(exclude)out/',
|
|
10
|
+
':(exclude).next/',
|
|
11
|
+
':(exclude)coverage/',
|
|
12
|
+
':(exclude)*.min.js',
|
|
13
|
+
':(exclude)*.min.css',
|
|
14
|
+
':(exclude)*.map',
|
|
15
|
+
];
|
|
16
|
+
// Regex patterns to identify noisy diff file sections.
|
|
17
|
+
const BINARY_FILE_LINE = /^Binary files .+ differ$/m;
|
|
18
|
+
const GIT_BINARY_PATCH = /^GIT binary patch/m;
|
|
19
|
+
const HAS_HUNK = /^@@/m;
|
|
20
|
+
const HAS_OLD_MODE = /^old mode /m;
|
|
21
|
+
/**
|
|
22
|
+
* Split raw unified diff into per-file sections and strip:
|
|
23
|
+
* - Binary file sections ("Binary files a/... and b/... differ")
|
|
24
|
+
* - GIT binary patch sections
|
|
25
|
+
* - Mode-only sections (permission changes with no content hunks)
|
|
26
|
+
*
|
|
27
|
+
* Does NOT modify content lines (+ / - / space) to preserve verbatim
|
|
28
|
+
* accuracy required by suggest_search_replace.
|
|
29
|
+
*/
|
|
30
|
+
export function cleanDiff(raw) {
|
|
31
|
+
if (!raw)
|
|
32
|
+
return '';
|
|
33
|
+
// Split on the start of each "diff --git" header, keeping the header.
|
|
34
|
+
const sections = raw.split(/(?=^diff --git )/m);
|
|
35
|
+
const cleaned = sections.filter((section) => {
|
|
36
|
+
if (!section.trim())
|
|
37
|
+
return false;
|
|
38
|
+
if (BINARY_FILE_LINE.test(section))
|
|
39
|
+
return false;
|
|
40
|
+
if (GIT_BINARY_PATCH.test(section))
|
|
41
|
+
return false;
|
|
42
|
+
// Drop mode-only sections that have no actual content hunks.
|
|
43
|
+
if (HAS_OLD_MODE.test(section) && !HAS_HUNK.test(section))
|
|
44
|
+
return false;
|
|
45
|
+
return true;
|
|
46
|
+
});
|
|
47
|
+
return cleaned.join('').trim();
|
|
48
|
+
}
|
|
49
|
+
export function isEmptyDiff(diff) {
|
|
50
|
+
return diff.trim().length === 0;
|
|
51
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { createErrorToolResponse } from './tool-response.js';
|
|
3
|
+
export declare const DIFF_RESOURCE_URI = "diff://current";
|
|
4
|
+
export interface DiffStats {
|
|
5
|
+
files: number;
|
|
6
|
+
added: number;
|
|
7
|
+
deleted: number;
|
|
8
|
+
}
|
|
9
|
+
export interface DiffSlot {
|
|
10
|
+
diff: string;
|
|
11
|
+
stats: DiffStats;
|
|
12
|
+
generatedAt: string;
|
|
13
|
+
mode: string;
|
|
14
|
+
}
|
|
15
|
+
/** Call once during server setup so the store can emit resource-updated notifications. */
|
|
16
|
+
export declare function initDiffStore(server: McpServer): void;
|
|
17
|
+
export declare function storeDiff(data: DiffSlot): void;
|
|
18
|
+
export declare function getDiff(): DiffSlot | undefined;
|
|
19
|
+
export declare function hasDiff(): boolean;
|
|
20
|
+
/** Test-only: directly set or clear the diff slot without emitting resource-updated. */
|
|
21
|
+
export declare function setDiffForTesting(data: DiffSlot | undefined): void;
|
|
22
|
+
export declare function createNoDiffError(): ReturnType<typeof createErrorToolResponse>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { createErrorToolResponse } from './tool-response.js';
|
|
2
|
+
export const DIFF_RESOURCE_URI = 'diff://current';
|
|
3
|
+
let slot;
|
|
4
|
+
let sendResourceUpdated;
|
|
5
|
+
/** Call once during server setup so the store can emit resource-updated notifications. */
|
|
6
|
+
export function initDiffStore(server) {
|
|
7
|
+
const inner = server.server;
|
|
8
|
+
sendResourceUpdated = inner.sendResourceUpdated.bind(inner);
|
|
9
|
+
}
|
|
10
|
+
export function storeDiff(data) {
|
|
11
|
+
slot = data;
|
|
12
|
+
void sendResourceUpdated?.({ uri: DIFF_RESOURCE_URI }).catch(() => {
|
|
13
|
+
// Notification is best-effort; never block the tool response.
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
export function getDiff() {
|
|
17
|
+
return slot;
|
|
18
|
+
}
|
|
19
|
+
export function hasDiff() {
|
|
20
|
+
return slot !== undefined;
|
|
21
|
+
}
|
|
22
|
+
/** Test-only: directly set or clear the diff slot without emitting resource-updated. */
|
|
23
|
+
export function setDiffForTesting(data) {
|
|
24
|
+
slot = data;
|
|
25
|
+
}
|
|
26
|
+
export function createNoDiffError() {
|
|
27
|
+
return createErrorToolResponse('E_NO_DIFF', 'No diff cached. You must call the generate_diff tool before using any review tool. Run generate_diff with mode="unstaged" or mode="staged" to capture the current branch changes, then retry this tool.', undefined, { retryable: false, kind: 'validation' });
|
|
28
|
+
}
|
|
@@ -8,9 +8,12 @@ export interface ToolParameterContract {
|
|
|
8
8
|
export interface ToolContract {
|
|
9
9
|
name: string;
|
|
10
10
|
purpose: string;
|
|
11
|
+
/** Set to 'none' for synchronous (non-Gemini) tools. */
|
|
11
12
|
model: string;
|
|
13
|
+
/** Set to 0 for synchronous (non-Gemini) tools. */
|
|
12
14
|
timeoutMs: number;
|
|
13
15
|
thinkingBudget?: number;
|
|
16
|
+
/** Set to 0 for synchronous (non-Gemini) tools. */
|
|
14
17
|
maxOutputTokens: number;
|
|
15
18
|
params: readonly ToolParameterContract[];
|
|
16
19
|
outputShape: string;
|
|
@@ -19,18 +22,28 @@ export interface ToolContract {
|
|
|
19
22
|
constraints?: readonly string[];
|
|
20
23
|
}
|
|
21
24
|
export declare const TOOL_CONTRACTS: readonly [{
|
|
25
|
+
readonly name: "generate_diff";
|
|
26
|
+
readonly purpose: "Generate a diff of current changes and cache it server-side. MUST be called before any other tool. Uses git to capture unstaged or staged changes in the current working directory.";
|
|
27
|
+
readonly model: "none";
|
|
28
|
+
readonly timeoutMs: 0;
|
|
29
|
+
readonly maxOutputTokens: 0;
|
|
30
|
+
readonly params: readonly [{
|
|
31
|
+
readonly name: "mode";
|
|
32
|
+
readonly type: "string";
|
|
33
|
+
readonly required: true;
|
|
34
|
+
readonly constraints: "'unstaged' | 'staged'";
|
|
35
|
+
readonly description: "'unstaged': working tree changes not yet staged. 'staged': changes added to the index (git add).";
|
|
36
|
+
}];
|
|
37
|
+
readonly outputShape: "{ok, result: {diffRef, stats{files, added, deleted}, generatedAt, mode, message}}";
|
|
38
|
+
readonly gotchas: readonly ["Must be called first — all other tools return E_NO_DIFF if no diff is cached.", "Noisy files (lock files, dist/, build/, minified assets) are excluded automatically.", "Empty diff (no changes) returns E_NO_CHANGES."];
|
|
39
|
+
readonly crossToolFlow: readonly ["Caches diff at diff://current — consumed automatically by all review tools."];
|
|
40
|
+
}, {
|
|
22
41
|
readonly name: "analyze_pr_impact";
|
|
23
42
|
readonly purpose: "Assess severity, categories, breaking changes, and rollback complexity.";
|
|
24
43
|
readonly model: "gemini-2.5-flash";
|
|
25
44
|
readonly timeoutMs: 90000;
|
|
26
45
|
readonly maxOutputTokens: 2048;
|
|
27
46
|
readonly params: readonly [{
|
|
28
|
-
readonly name: "diff";
|
|
29
|
-
readonly type: "string";
|
|
30
|
-
readonly required: true;
|
|
31
|
-
readonly constraints: "10-120K chars";
|
|
32
|
-
readonly description: "Unified diff text.";
|
|
33
|
-
}, {
|
|
34
47
|
readonly name: "repository";
|
|
35
48
|
readonly type: "string";
|
|
36
49
|
readonly required: true;
|
|
@@ -44,7 +57,7 @@ export declare const TOOL_CONTRACTS: readonly [{
|
|
|
44
57
|
readonly description: "Primary language hint.";
|
|
45
58
|
}];
|
|
46
59
|
readonly outputShape: "{severity, categories[], summary, breakingChanges[], affectedAreas[], rollbackComplexity}";
|
|
47
|
-
readonly gotchas: readonly ["
|
|
60
|
+
readonly gotchas: readonly ["Requires generate_diff to be called first.", "Flash triage tool optimized for speed."];
|
|
48
61
|
readonly crossToolFlow: readonly ["severity/categories feed triage and merge-gate decisions."];
|
|
49
62
|
}, {
|
|
50
63
|
readonly name: "generate_review_summary";
|
|
@@ -53,12 +66,6 @@ export declare const TOOL_CONTRACTS: readonly [{
|
|
|
53
66
|
readonly timeoutMs: 90000;
|
|
54
67
|
readonly maxOutputTokens: 2048;
|
|
55
68
|
readonly params: readonly [{
|
|
56
|
-
readonly name: "diff";
|
|
57
|
-
readonly type: "string";
|
|
58
|
-
readonly required: true;
|
|
59
|
-
readonly constraints: "10-120K chars";
|
|
60
|
-
readonly description: "Unified diff text.";
|
|
61
|
-
}, {
|
|
62
69
|
readonly name: "repository";
|
|
63
70
|
readonly type: "string";
|
|
64
71
|
readonly required: true;
|
|
@@ -72,7 +79,7 @@ export declare const TOOL_CONTRACTS: readonly [{
|
|
|
72
79
|
readonly description: "Primary language hint.";
|
|
73
80
|
}];
|
|
74
81
|
readonly outputShape: "{summary, overallRisk, keyChanges[], recommendation, stats{filesChanged, linesAdded, linesRemoved}}";
|
|
75
|
-
readonly gotchas: readonly ["
|
|
82
|
+
readonly gotchas: readonly ["Requires generate_diff to be called first.", "stats are computed locally from the diff."];
|
|
76
83
|
readonly crossToolFlow: readonly ["Use before deep review to decide whether Pro analysis is needed."];
|
|
77
84
|
}, {
|
|
78
85
|
readonly name: "inspect_code_quality";
|
|
@@ -82,12 +89,6 @@ export declare const TOOL_CONTRACTS: readonly [{
|
|
|
82
89
|
readonly thinkingBudget: 16384;
|
|
83
90
|
readonly maxOutputTokens: 8192;
|
|
84
91
|
readonly params: readonly [{
|
|
85
|
-
readonly name: "diff";
|
|
86
|
-
readonly type: "string";
|
|
87
|
-
readonly required: true;
|
|
88
|
-
readonly constraints: "10-120K chars";
|
|
89
|
-
readonly description: "Unified diff text.";
|
|
90
|
-
}, {
|
|
91
92
|
readonly name: "repository";
|
|
92
93
|
readonly type: "string";
|
|
93
94
|
readonly required: true;
|
|
@@ -119,7 +120,7 @@ export declare const TOOL_CONTRACTS: readonly [{
|
|
|
119
120
|
readonly description: "Optional full file content context.";
|
|
120
121
|
}];
|
|
121
122
|
readonly outputShape: "{summary, overallRisk, findings[], testsNeeded[], contextualInsights[], totalFindings}";
|
|
122
|
-
readonly gotchas: readonly ["Combined diff + file context is bounded by MAX_CONTEXT_CHARS.", "maxFindings caps output after generation."];
|
|
123
|
+
readonly gotchas: readonly ["Requires generate_diff to be called first.", "Combined diff + file context is bounded by MAX_CONTEXT_CHARS.", "maxFindings caps output after generation."];
|
|
123
124
|
readonly crossToolFlow: readonly ["findings[].title -> suggest_search_replace.findingTitle", "findings[].explanation -> suggest_search_replace.findingDetails"];
|
|
124
125
|
readonly constraints: readonly ["Context budget (diff + files) < 500K chars."];
|
|
125
126
|
}, {
|
|
@@ -130,12 +131,6 @@ export declare const TOOL_CONTRACTS: readonly [{
|
|
|
130
131
|
readonly thinkingBudget: 16384;
|
|
131
132
|
readonly maxOutputTokens: 4096;
|
|
132
133
|
readonly params: readonly [{
|
|
133
|
-
readonly name: "diff";
|
|
134
|
-
readonly type: "string";
|
|
135
|
-
readonly required: true;
|
|
136
|
-
readonly constraints: "10-120K chars";
|
|
137
|
-
readonly description: "Unified diff containing the target issue.";
|
|
138
|
-
}, {
|
|
139
134
|
readonly name: "findingTitle";
|
|
140
135
|
readonly type: "string";
|
|
141
136
|
readonly required: true;
|
|
@@ -149,7 +144,7 @@ export declare const TOOL_CONTRACTS: readonly [{
|
|
|
149
144
|
readonly description: "Detailed finding context.";
|
|
150
145
|
}];
|
|
151
146
|
readonly outputShape: "{summary, blocks[], validationChecklist[]}";
|
|
152
|
-
readonly gotchas: readonly ["One finding per call to avoid mixed patch intent.", "search must be exact whitespace-preserving match."];
|
|
147
|
+
readonly gotchas: readonly ["Requires generate_diff to be called first.", "One finding per call to avoid mixed patch intent.", "search must be exact whitespace-preserving match."];
|
|
153
148
|
readonly crossToolFlow: readonly ["Consumes findings from inspect_code_quality for targeted fixes."];
|
|
154
149
|
readonly constraints: readonly ["One finding per call; verbatim search match required."];
|
|
155
150
|
}, {
|
|
@@ -160,12 +155,6 @@ export declare const TOOL_CONTRACTS: readonly [{
|
|
|
160
155
|
readonly thinkingBudget: 8192;
|
|
161
156
|
readonly maxOutputTokens: 4096;
|
|
162
157
|
readonly params: readonly [{
|
|
163
|
-
readonly name: "diff";
|
|
164
|
-
readonly type: "string";
|
|
165
|
-
readonly required: true;
|
|
166
|
-
readonly constraints: "10-120K chars";
|
|
167
|
-
readonly description: "Unified diff text.";
|
|
168
|
-
}, {
|
|
169
158
|
readonly name: "repository";
|
|
170
159
|
readonly type: "string";
|
|
171
160
|
readonly required: true;
|
|
@@ -191,7 +180,7 @@ export declare const TOOL_CONTRACTS: readonly [{
|
|
|
191
180
|
readonly description: "Post-generation cap applied to test cases.";
|
|
192
181
|
}];
|
|
193
182
|
readonly outputShape: "{summary, testCases[], coverageSummary}";
|
|
194
|
-
readonly gotchas: readonly ["maxTestCases caps output after generation."];
|
|
183
|
+
readonly gotchas: readonly ["Requires generate_diff to be called first.", "maxTestCases caps output after generation."];
|
|
195
184
|
readonly crossToolFlow: readonly ["Pair with inspect_code_quality to validate high-risk paths."];
|
|
196
185
|
}, {
|
|
197
186
|
readonly name: "analyze_time_space_complexity";
|
|
@@ -201,12 +190,6 @@ export declare const TOOL_CONTRACTS: readonly [{
|
|
|
201
190
|
readonly thinkingBudget: 8192;
|
|
202
191
|
readonly maxOutputTokens: 2048;
|
|
203
192
|
readonly params: readonly [{
|
|
204
|
-
readonly name: "diff";
|
|
205
|
-
readonly type: "string";
|
|
206
|
-
readonly required: true;
|
|
207
|
-
readonly constraints: "10-120K chars";
|
|
208
|
-
readonly description: "Unified diff text.";
|
|
209
|
-
}, {
|
|
210
193
|
readonly name: "language";
|
|
211
194
|
readonly type: "string";
|
|
212
195
|
readonly required: false;
|
|
@@ -214,7 +197,7 @@ export declare const TOOL_CONTRACTS: readonly [{
|
|
|
214
197
|
readonly description: "Primary language hint.";
|
|
215
198
|
}];
|
|
216
199
|
readonly outputShape: "{timeComplexity, spaceComplexity, explanation, potentialBottlenecks[], isDegradation}";
|
|
217
|
-
readonly gotchas: readonly ["Analyzes only changed code visible in the diff."];
|
|
200
|
+
readonly gotchas: readonly ["Requires generate_diff to be called first.", "Analyzes only changed code visible in the diff."];
|
|
218
201
|
readonly crossToolFlow: readonly ["Use for algorithmic/performance-sensitive changes."];
|
|
219
202
|
}, {
|
|
220
203
|
readonly name: "detect_api_breaking_changes";
|
|
@@ -223,12 +206,6 @@ export declare const TOOL_CONTRACTS: readonly [{
|
|
|
223
206
|
readonly timeoutMs: 90000;
|
|
224
207
|
readonly maxOutputTokens: 4096;
|
|
225
208
|
readonly params: readonly [{
|
|
226
|
-
readonly name: "diff";
|
|
227
|
-
readonly type: "string";
|
|
228
|
-
readonly required: true;
|
|
229
|
-
readonly constraints: "10-120K chars";
|
|
230
|
-
readonly description: "Unified diff text.";
|
|
231
|
-
}, {
|
|
232
209
|
readonly name: "language";
|
|
233
210
|
readonly type: "string";
|
|
234
211
|
readonly required: false;
|
|
@@ -236,7 +213,7 @@ export declare const TOOL_CONTRACTS: readonly [{
|
|
|
236
213
|
readonly description: "Primary language hint.";
|
|
237
214
|
}];
|
|
238
215
|
readonly outputShape: "{hasBreakingChanges, breakingChanges[]}";
|
|
239
|
-
readonly gotchas: readonly ["Targets public API contracts over internal refactors."];
|
|
216
|
+
readonly gotchas: readonly ["Requires generate_diff to be called first.", "Targets public API contracts over internal refactors."];
|
|
240
217
|
readonly crossToolFlow: readonly ["Run before merge for API-surface-sensitive changes."];
|
|
241
218
|
}];
|
|
242
219
|
export declare function getToolContracts(): readonly ToolContract[];
|
|
@@ -1,6 +1,31 @@
|
|
|
1
1
|
import { DEFAULT_TIMEOUT_PRO_MS, FLASH_API_BREAKING_MAX_OUTPUT_TOKENS, FLASH_COMPLEXITY_MAX_OUTPUT_TOKENS, FLASH_MODEL, FLASH_TEST_PLAN_MAX_OUTPUT_TOKENS, FLASH_THINKING_BUDGET, FLASH_TRIAGE_MAX_OUTPUT_TOKENS, PRO_MODEL, PRO_PATCH_MAX_OUTPUT_TOKENS, PRO_REVIEW_MAX_OUTPUT_TOKENS, PRO_THINKING_BUDGET, } from './model-config.js';
|
|
2
2
|
const DEFAULT_TIMEOUT_FLASH_MS = 90_000;
|
|
3
3
|
export const TOOL_CONTRACTS = [
|
|
4
|
+
{
|
|
5
|
+
name: 'generate_diff',
|
|
6
|
+
purpose: 'Generate a diff of current changes and cache it server-side. MUST be called before any other tool. Uses git to capture unstaged or staged changes in the current working directory.',
|
|
7
|
+
model: 'none',
|
|
8
|
+
timeoutMs: 0,
|
|
9
|
+
maxOutputTokens: 0,
|
|
10
|
+
params: [
|
|
11
|
+
{
|
|
12
|
+
name: 'mode',
|
|
13
|
+
type: 'string',
|
|
14
|
+
required: true,
|
|
15
|
+
constraints: "'unstaged' | 'staged'",
|
|
16
|
+
description: "'unstaged': working tree changes not yet staged. 'staged': changes added to the index (git add).",
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
outputShape: '{ok, result: {diffRef, stats{files, added, deleted}, generatedAt, mode, message}}',
|
|
20
|
+
gotchas: [
|
|
21
|
+
'Must be called first — all other tools return E_NO_DIFF if no diff is cached.',
|
|
22
|
+
'Noisy files (lock files, dist/, build/, minified assets) are excluded automatically.',
|
|
23
|
+
'Empty diff (no changes) returns E_NO_CHANGES.',
|
|
24
|
+
],
|
|
25
|
+
crossToolFlow: [
|
|
26
|
+
'Caches diff at diff://current — consumed automatically by all review tools.',
|
|
27
|
+
],
|
|
28
|
+
},
|
|
4
29
|
{
|
|
5
30
|
name: 'analyze_pr_impact',
|
|
6
31
|
purpose: 'Assess severity, categories, breaking changes, and rollback complexity.',
|
|
@@ -8,13 +33,6 @@ export const TOOL_CONTRACTS = [
|
|
|
8
33
|
timeoutMs: DEFAULT_TIMEOUT_FLASH_MS,
|
|
9
34
|
maxOutputTokens: FLASH_TRIAGE_MAX_OUTPUT_TOKENS,
|
|
10
35
|
params: [
|
|
11
|
-
{
|
|
12
|
-
name: 'diff',
|
|
13
|
-
type: 'string',
|
|
14
|
-
required: true,
|
|
15
|
-
constraints: '10-120K chars',
|
|
16
|
-
description: 'Unified diff text.',
|
|
17
|
-
},
|
|
18
36
|
{
|
|
19
37
|
name: 'repository',
|
|
20
38
|
type: 'string',
|
|
@@ -32,8 +50,8 @@ export const TOOL_CONTRACTS = [
|
|
|
32
50
|
],
|
|
33
51
|
outputShape: '{severity, categories[], summary, breakingChanges[], affectedAreas[], rollbackComplexity}',
|
|
34
52
|
gotchas: [
|
|
53
|
+
'Requires generate_diff to be called first.',
|
|
35
54
|
'Flash triage tool optimized for speed.',
|
|
36
|
-
'Diff-only analysis (no full-file context).',
|
|
37
55
|
],
|
|
38
56
|
crossToolFlow: [
|
|
39
57
|
'severity/categories feed triage and merge-gate decisions.',
|
|
@@ -46,13 +64,6 @@ export const TOOL_CONTRACTS = [
|
|
|
46
64
|
timeoutMs: DEFAULT_TIMEOUT_FLASH_MS,
|
|
47
65
|
maxOutputTokens: FLASH_TRIAGE_MAX_OUTPUT_TOKENS,
|
|
48
66
|
params: [
|
|
49
|
-
{
|
|
50
|
-
name: 'diff',
|
|
51
|
-
type: 'string',
|
|
52
|
-
required: true,
|
|
53
|
-
constraints: '10-120K chars',
|
|
54
|
-
description: 'Unified diff text.',
|
|
55
|
-
},
|
|
56
67
|
{
|
|
57
68
|
name: 'repository',
|
|
58
69
|
type: 'string',
|
|
@@ -70,8 +81,8 @@ export const TOOL_CONTRACTS = [
|
|
|
70
81
|
],
|
|
71
82
|
outputShape: '{summary, overallRisk, keyChanges[], recommendation, stats{filesChanged, linesAdded, linesRemoved}}',
|
|
72
83
|
gotchas: [
|
|
84
|
+
'Requires generate_diff to be called first.',
|
|
73
85
|
'stats are computed locally from the diff.',
|
|
74
|
-
'Flash triage tool optimized for speed.',
|
|
75
86
|
],
|
|
76
87
|
crossToolFlow: [
|
|
77
88
|
'Use before deep review to decide whether Pro analysis is needed.',
|
|
@@ -85,13 +96,6 @@ export const TOOL_CONTRACTS = [
|
|
|
85
96
|
thinkingBudget: PRO_THINKING_BUDGET,
|
|
86
97
|
maxOutputTokens: PRO_REVIEW_MAX_OUTPUT_TOKENS,
|
|
87
98
|
params: [
|
|
88
|
-
{
|
|
89
|
-
name: 'diff',
|
|
90
|
-
type: 'string',
|
|
91
|
-
required: true,
|
|
92
|
-
constraints: '10-120K chars',
|
|
93
|
-
description: 'Unified diff text.',
|
|
94
|
-
},
|
|
95
99
|
{
|
|
96
100
|
name: 'repository',
|
|
97
101
|
type: 'string',
|
|
@@ -130,6 +134,7 @@ export const TOOL_CONTRACTS = [
|
|
|
130
134
|
],
|
|
131
135
|
outputShape: '{summary, overallRisk, findings[], testsNeeded[], contextualInsights[], totalFindings}',
|
|
132
136
|
gotchas: [
|
|
137
|
+
'Requires generate_diff to be called first.',
|
|
133
138
|
'Combined diff + file context is bounded by MAX_CONTEXT_CHARS.',
|
|
134
139
|
'maxFindings caps output after generation.',
|
|
135
140
|
],
|
|
@@ -147,13 +152,6 @@ export const TOOL_CONTRACTS = [
|
|
|
147
152
|
thinkingBudget: PRO_THINKING_BUDGET,
|
|
148
153
|
maxOutputTokens: PRO_PATCH_MAX_OUTPUT_TOKENS,
|
|
149
154
|
params: [
|
|
150
|
-
{
|
|
151
|
-
name: 'diff',
|
|
152
|
-
type: 'string',
|
|
153
|
-
required: true,
|
|
154
|
-
constraints: '10-120K chars',
|
|
155
|
-
description: 'Unified diff containing the target issue.',
|
|
156
|
-
},
|
|
157
155
|
{
|
|
158
156
|
name: 'findingTitle',
|
|
159
157
|
type: 'string',
|
|
@@ -171,6 +169,7 @@ export const TOOL_CONTRACTS = [
|
|
|
171
169
|
],
|
|
172
170
|
outputShape: '{summary, blocks[], validationChecklist[]}',
|
|
173
171
|
gotchas: [
|
|
172
|
+
'Requires generate_diff to be called first.',
|
|
174
173
|
'One finding per call to avoid mixed patch intent.',
|
|
175
174
|
'search must be exact whitespace-preserving match.',
|
|
176
175
|
],
|
|
@@ -187,13 +186,6 @@ export const TOOL_CONTRACTS = [
|
|
|
187
186
|
thinkingBudget: FLASH_THINKING_BUDGET,
|
|
188
187
|
maxOutputTokens: FLASH_TEST_PLAN_MAX_OUTPUT_TOKENS,
|
|
189
188
|
params: [
|
|
190
|
-
{
|
|
191
|
-
name: 'diff',
|
|
192
|
-
type: 'string',
|
|
193
|
-
required: true,
|
|
194
|
-
constraints: '10-120K chars',
|
|
195
|
-
description: 'Unified diff text.',
|
|
196
|
-
},
|
|
197
189
|
{
|
|
198
190
|
name: 'repository',
|
|
199
191
|
type: 'string',
|
|
@@ -224,7 +216,10 @@ export const TOOL_CONTRACTS = [
|
|
|
224
216
|
},
|
|
225
217
|
],
|
|
226
218
|
outputShape: '{summary, testCases[], coverageSummary}',
|
|
227
|
-
gotchas: [
|
|
219
|
+
gotchas: [
|
|
220
|
+
'Requires generate_diff to be called first.',
|
|
221
|
+
'maxTestCases caps output after generation.',
|
|
222
|
+
],
|
|
228
223
|
crossToolFlow: [
|
|
229
224
|
'Pair with inspect_code_quality to validate high-risk paths.',
|
|
230
225
|
],
|
|
@@ -237,13 +232,6 @@ export const TOOL_CONTRACTS = [
|
|
|
237
232
|
thinkingBudget: FLASH_THINKING_BUDGET,
|
|
238
233
|
maxOutputTokens: FLASH_COMPLEXITY_MAX_OUTPUT_TOKENS,
|
|
239
234
|
params: [
|
|
240
|
-
{
|
|
241
|
-
name: 'diff',
|
|
242
|
-
type: 'string',
|
|
243
|
-
required: true,
|
|
244
|
-
constraints: '10-120K chars',
|
|
245
|
-
description: 'Unified diff text.',
|
|
246
|
-
},
|
|
247
235
|
{
|
|
248
236
|
name: 'language',
|
|
249
237
|
type: 'string',
|
|
@@ -253,7 +241,10 @@ export const TOOL_CONTRACTS = [
|
|
|
253
241
|
},
|
|
254
242
|
],
|
|
255
243
|
outputShape: '{timeComplexity, spaceComplexity, explanation, potentialBottlenecks[], isDegradation}',
|
|
256
|
-
gotchas: [
|
|
244
|
+
gotchas: [
|
|
245
|
+
'Requires generate_diff to be called first.',
|
|
246
|
+
'Analyzes only changed code visible in the diff.',
|
|
247
|
+
],
|
|
257
248
|
crossToolFlow: ['Use for algorithmic/performance-sensitive changes.'],
|
|
258
249
|
},
|
|
259
250
|
{
|
|
@@ -263,13 +254,6 @@ export const TOOL_CONTRACTS = [
|
|
|
263
254
|
timeoutMs: DEFAULT_TIMEOUT_FLASH_MS,
|
|
264
255
|
maxOutputTokens: FLASH_API_BREAKING_MAX_OUTPUT_TOKENS,
|
|
265
256
|
params: [
|
|
266
|
-
{
|
|
267
|
-
name: 'diff',
|
|
268
|
-
type: 'string',
|
|
269
|
-
required: true,
|
|
270
|
-
constraints: '10-120K chars',
|
|
271
|
-
description: 'Unified diff text.',
|
|
272
|
-
},
|
|
273
257
|
{
|
|
274
258
|
name: 'language',
|
|
275
259
|
type: 'string',
|
|
@@ -279,7 +263,10 @@ export const TOOL_CONTRACTS = [
|
|
|
279
263
|
},
|
|
280
264
|
],
|
|
281
265
|
outputShape: '{hasBreakingChanges, breakingChanges[]}',
|
|
282
|
-
gotchas: [
|
|
266
|
+
gotchas: [
|
|
267
|
+
'Requires generate_diff to be called first.',
|
|
268
|
+
'Targets public API contracts over internal refactors.',
|
|
269
|
+
],
|
|
283
270
|
crossToolFlow: ['Run before merge for API-surface-sensitive changes.'],
|
|
284
271
|
},
|
|
285
272
|
];
|
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import type { ZodRawShapeCompat } from '@modelcontextprotocol/sdk/server/zod-compat.js';
|
|
3
3
|
import { z } from 'zod';
|
|
4
|
+
import { type DiffSlot } from './diff-store.js';
|
|
4
5
|
import { createErrorToolResponse } from './tool-response.js';
|
|
5
6
|
export interface PromptParts {
|
|
6
7
|
systemInstruction: string;
|
|
7
8
|
prompt: string;
|
|
8
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* Immutable snapshot of server-side state captured once at the start of a
|
|
12
|
+
* tool execution, before `validateInput` runs. Threading it through both
|
|
13
|
+
* `validateInput` and `buildPrompt` eliminates the TOCTOU gap that would
|
|
14
|
+
* otherwise allow a concurrent `generate_diff` call to replace the cached
|
|
15
|
+
* diff between the budget check and prompt assembly.
|
|
16
|
+
*/
|
|
17
|
+
export interface ToolExecutionContext {
|
|
18
|
+
readonly diffSlot: DiffSlot | undefined;
|
|
19
|
+
}
|
|
9
20
|
export interface StructuredToolTaskConfig<TInput extends object = Record<string, unknown>, TResult extends object = Record<string, unknown>, TFinal extends TResult = TResult> {
|
|
10
21
|
/** Tool name registered with the MCP server (e.g. 'analyze_pr_impact'). */
|
|
11
22
|
name: string;
|
|
@@ -24,9 +35,9 @@ export interface StructuredToolTaskConfig<TInput extends object = Record<string,
|
|
|
24
35
|
/** Stable error code returned on failure (e.g. 'E_INSPECT_QUALITY'). */
|
|
25
36
|
errorCode: string;
|
|
26
37
|
/** Optional post-processing hook called after resultSchema.parse(). The return value replaces the parsed result. */
|
|
27
|
-
transformResult?: (input: TInput, result: TResult) => TFinal;
|
|
38
|
+
transformResult?: (input: TInput, result: TResult, ctx: ToolExecutionContext) => TFinal;
|
|
28
39
|
/** Optional validation hook for input parameters. */
|
|
29
|
-
validateInput?: (input: TInput) => Promise<ReturnType<typeof createErrorToolResponse> | undefined> | ReturnType<typeof createErrorToolResponse> | undefined;
|
|
40
|
+
validateInput?: (input: TInput, ctx: ToolExecutionContext) => Promise<ReturnType<typeof createErrorToolResponse> | undefined> | ReturnType<typeof createErrorToolResponse> | undefined;
|
|
30
41
|
/** Optional Gemini model to use (e.g. 'gemini-2.5-pro'). */
|
|
31
42
|
model?: string;
|
|
32
43
|
/** Optional thinking budget in tokens. */
|
|
@@ -44,6 +55,6 @@ export interface StructuredToolTaskConfig<TInput extends object = Record<string,
|
|
|
44
55
|
/** Optional short outcome suffix for the completion progress message (e.g., "3 findings"). */
|
|
45
56
|
formatOutcome?: (result: TFinal) => string;
|
|
46
57
|
/** Builds the system instruction and user prompt from parsed tool input. */
|
|
47
|
-
buildPrompt: (input: TInput) => PromptParts;
|
|
58
|
+
buildPrompt: (input: TInput, ctx: ToolExecutionContext) => PromptParts;
|
|
48
59
|
}
|
|
49
60
|
export declare function registerStructuredToolTask<TInput extends object, TResult extends object = Record<string, unknown>, TFinal extends TResult = TResult>(server: McpServer, config: StructuredToolTaskConfig<TInput, TResult, TFinal>): void;
|
package/dist/lib/tool-factory.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { DefaultOutputSchema } from '../schemas/outputs.js';
|
|
3
|
+
import { getDiff } from './diff-store.js';
|
|
3
4
|
import { getErrorMessage, RETRYABLE_UPSTREAM_ERROR_PATTERN } from './errors.js';
|
|
4
5
|
import { stripJsonSchemaConstraints } from './gemini-schema.js';
|
|
5
6
|
import { generateStructuredJson, getCurrentRequestId } from './gemini.js';
|
|
@@ -260,9 +261,14 @@ export function registerStructuredToolTask(server, config) {
|
|
|
260
261
|
const onLog = createGeminiLogger(server, task.taskId);
|
|
261
262
|
const inputRecord = parseToolInput(input, config.fullInputSchema);
|
|
262
263
|
progressContext = normalizeProgressContext(config.progressContext?.(inputRecord));
|
|
264
|
+
// Snapshot the diff slot ONCE before any async work so that
|
|
265
|
+
// validateInput and buildPrompt observe the same state. Without
|
|
266
|
+
// this, a concurrent generate_diff call between the two awaits
|
|
267
|
+
// could replace the slot and silently bypass the budget check.
|
|
268
|
+
const ctx = { diffSlot: getDiff() };
|
|
263
269
|
await reportProgressStepUpdate(reportProgress, config.name, progressContext, 0, 'starting');
|
|
264
270
|
if (config.validateInput) {
|
|
265
|
-
const validationError = await config.validateInput(inputRecord);
|
|
271
|
+
const validationError = await config.validateInput(inputRecord, ctx);
|
|
266
272
|
if (validationError) {
|
|
267
273
|
const validationMessage = validationError.structuredContent.error?.message ??
|
|
268
274
|
INPUT_VALIDATION_FAILED;
|
|
@@ -273,7 +279,7 @@ export function registerStructuredToolTask(server, config) {
|
|
|
273
279
|
}
|
|
274
280
|
}
|
|
275
281
|
await reportProgressStepUpdate(reportProgress, config.name, progressContext, 1, 'preparing');
|
|
276
|
-
const promptParts = config.buildPrompt(inputRecord);
|
|
282
|
+
const promptParts = config.buildPrompt(inputRecord, ctx);
|
|
277
283
|
const { prompt } = promptParts;
|
|
278
284
|
const { systemInstruction } = promptParts;
|
|
279
285
|
const modelLabel = friendlyModelName(config.model);
|
|
@@ -306,7 +312,7 @@ export function registerStructuredToolTask(server, config) {
|
|
|
306
312
|
throw new Error('Unexpected state: parsed result is undefined');
|
|
307
313
|
}
|
|
308
314
|
const finalResult = (config.transformResult
|
|
309
|
-
? config.transformResult(inputRecord, parsed)
|
|
315
|
+
? config.transformResult(inputRecord, parsed, ctx)
|
|
310
316
|
: parsed);
|
|
311
317
|
const textContent = config.formatOutput
|
|
312
318
|
? config.formatOutput(finalResult)
|
package/dist/resources/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ResourceTemplate, } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { DIFF_RESOURCE_URI, getDiff } from '../lib/diff-store.js';
|
|
2
3
|
import { buildServerConfig } from './server-config.js';
|
|
3
4
|
import { buildToolCatalog } from './tool-catalog.js';
|
|
4
5
|
import { getToolInfo, getToolInfoNames } from './tool-info.js';
|
|
@@ -76,10 +77,28 @@ function registerToolInfoResources(server) {
|
|
|
76
77
|
return { contents: [createMarkdownContent(uri, text)] };
|
|
77
78
|
});
|
|
78
79
|
}
|
|
80
|
+
function registerDiffResource(server) {
|
|
81
|
+
server.registerResource('diff-current', new ResourceTemplate(DIFF_RESOURCE_URI, { list: undefined }), {
|
|
82
|
+
title: 'Current Diff',
|
|
83
|
+
description: 'The most recently generated diff, cached by generate_diff. Read by all review tools automatically.',
|
|
84
|
+
mimeType: 'text/x-patch',
|
|
85
|
+
annotations: {
|
|
86
|
+
audience: ['assistant'],
|
|
87
|
+
priority: 1.0,
|
|
88
|
+
},
|
|
89
|
+
}, (uri) => {
|
|
90
|
+
const slot = getDiff();
|
|
91
|
+
const text = slot
|
|
92
|
+
? `# Diff — ${slot.mode} — ${slot.generatedAt}\n# ${slot.stats.files} file(s), +${slot.stats.added} -${slot.stats.deleted}\n\n${slot.diff}`
|
|
93
|
+
: '# No diff cached. Call generate_diff first.';
|
|
94
|
+
return { contents: [{ uri: uri.href, mimeType: 'text/x-patch', text }] };
|
|
95
|
+
});
|
|
96
|
+
}
|
|
79
97
|
export function registerAllResources(server, instructions) {
|
|
80
98
|
for (const def of STATIC_RESOURCES) {
|
|
81
99
|
const override = def.id === 'server-instructions' ? instructions : undefined;
|
|
82
100
|
registerStaticResource(server, def, override);
|
|
83
101
|
}
|
|
84
102
|
registerToolInfoResources(server);
|
|
103
|
+
registerDiffResource(server);
|
|
85
104
|
}
|
|
@@ -10,6 +10,7 @@ const RESOURCE_LIST = [
|
|
|
10
10
|
'- `internal://workflows`: Recommended multi-step tool workflows.',
|
|
11
11
|
'- `internal://server-config`: Runtime limits and model configuration.',
|
|
12
12
|
'- `internal://tool-info/{toolName}`: Per-tool contract details.',
|
|
13
|
+
'- `diff://current`: Cached diff from the most recent generate_diff run.',
|
|
13
14
|
];
|
|
14
15
|
function formatParameterLine(parameter) {
|
|
15
16
|
const required = parameter.required ? 'required' : 'optional';
|
|
@@ -17,6 +18,15 @@ function formatParameterLine(parameter) {
|
|
|
17
18
|
}
|
|
18
19
|
function formatToolSection(contract) {
|
|
19
20
|
const parameterLines = contract.params.map((parameter) => formatParameterLine(parameter));
|
|
21
|
+
if (contract.model === 'none') {
|
|
22
|
+
// Synchronous built-in tool (no Gemini call)
|
|
23
|
+
return `### \`${contract.name}\`
|
|
24
|
+
- Purpose: ${contract.purpose}
|
|
25
|
+
- Model: \`none\` (synchronous built-in)
|
|
26
|
+
- Parameters:
|
|
27
|
+
${parameterLines.join('\n')}
|
|
28
|
+
- Output shape: \`${contract.outputShape}\``;
|
|
29
|
+
}
|
|
20
30
|
const thinkingLine = contract.thinkingBudget === undefined
|
|
21
31
|
? '- Thinking budget: disabled'
|
|
22
32
|
: `- Thinking budget: ${contract.thinkingBudget}`;
|
|
@@ -35,6 +35,7 @@ export function buildServerConfig() {
|
|
|
35
35
|
const defaultModel = getModelOverride();
|
|
36
36
|
const safetyThreshold = getSafetyThreshold();
|
|
37
37
|
const toolRows = getToolContracts()
|
|
38
|
+
.filter((contract) => contract.model !== 'none')
|
|
38
39
|
.map((contract) => {
|
|
39
40
|
return `| \`${contract.name}\` | \`${contract.model}\` | ${formatThinkingBudget(contract.thinkingBudget)} | ${formatTimeout(contract.timeoutMs)} | ${formatNumber(contract.maxOutputTokens)} |`;
|
|
40
41
|
})
|