@j0hanz/code-review-analyst-mcp 1.4.0 → 1.4.2
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/tool-factory.d.ts +36 -3
- package/dist/lib/tool-factory.js +46 -3
- package/dist/schemas/inputs.js +20 -18
- package/dist/tools/analyze-complexity.js +6 -7
- package/dist/tools/analyze-pr-impact.js +6 -7
- package/dist/tools/detect-api-breaking.js +6 -7
- package/dist/tools/generate-diff.js +5 -1
- package/dist/tools/generate-review-summary.js +18 -19
- package/dist/tools/generate-test-plan.js +6 -7
- package/dist/tools/inspect-code-quality.js +6 -7
- package/dist/tools/suggest-search-replace.js +6 -7
- package/package.json +1 -1
|
@@ -1,11 +1,39 @@
|
|
|
1
1
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import type { ZodRawShapeCompat } from '@modelcontextprotocol/sdk/server/zod-compat.js';
|
|
3
|
+
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
|
3
4
|
import { z } from 'zod';
|
|
5
|
+
import { type DiffSlot } from './diff-store.js';
|
|
4
6
|
import { createErrorToolResponse } from './tool-response.js';
|
|
5
7
|
export interface PromptParts {
|
|
6
8
|
systemInstruction: string;
|
|
7
9
|
prompt: string;
|
|
8
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* Immutable snapshot of server-side state captured once at the start of a
|
|
13
|
+
* tool execution, before `validateInput` runs. Threading it through both
|
|
14
|
+
* `validateInput` and `buildPrompt` eliminates the TOCTOU gap that would
|
|
15
|
+
* otherwise allow a concurrent `generate_diff` call to replace the cached
|
|
16
|
+
* diff between the budget check and prompt assembly.
|
|
17
|
+
*/
|
|
18
|
+
export interface ToolExecutionContext {
|
|
19
|
+
readonly diffSlot: DiffSlot | undefined;
|
|
20
|
+
}
|
|
21
|
+
type ProgressToken = string | number;
|
|
22
|
+
interface ProgressNotificationParams {
|
|
23
|
+
progressToken: ProgressToken;
|
|
24
|
+
progress: number;
|
|
25
|
+
total?: number;
|
|
26
|
+
message?: string;
|
|
27
|
+
}
|
|
28
|
+
interface ProgressExtra {
|
|
29
|
+
_meta?: {
|
|
30
|
+
progressToken?: unknown;
|
|
31
|
+
};
|
|
32
|
+
sendNotification: (notification: {
|
|
33
|
+
method: 'notifications/progress';
|
|
34
|
+
params: ProgressNotificationParams;
|
|
35
|
+
}) => Promise<void>;
|
|
36
|
+
}
|
|
9
37
|
export interface StructuredToolTaskConfig<TInput extends object = Record<string, unknown>, TResult extends object = Record<string, unknown>, TFinal extends TResult = TResult> {
|
|
10
38
|
/** Tool name registered with the MCP server (e.g. 'analyze_pr_impact'). */
|
|
11
39
|
name: string;
|
|
@@ -24,9 +52,9 @@ export interface StructuredToolTaskConfig<TInput extends object = Record<string,
|
|
|
24
52
|
/** Stable error code returned on failure (e.g. 'E_INSPECT_QUALITY'). */
|
|
25
53
|
errorCode: string;
|
|
26
54
|
/** Optional post-processing hook called after resultSchema.parse(). The return value replaces the parsed result. */
|
|
27
|
-
transformResult?: (input: TInput, result: TResult) => TFinal;
|
|
55
|
+
transformResult?: (input: TInput, result: TResult, ctx: ToolExecutionContext) => TFinal;
|
|
28
56
|
/** Optional validation hook for input parameters. */
|
|
29
|
-
validateInput?: (input: TInput) => Promise<ReturnType<typeof createErrorToolResponse> | undefined> | ReturnType<typeof createErrorToolResponse> | undefined;
|
|
57
|
+
validateInput?: (input: TInput, ctx: ToolExecutionContext) => Promise<ReturnType<typeof createErrorToolResponse> | undefined> | ReturnType<typeof createErrorToolResponse> | undefined;
|
|
30
58
|
/** Optional Gemini model to use (e.g. 'gemini-2.5-pro'). */
|
|
31
59
|
model?: string;
|
|
32
60
|
/** Optional thinking budget in tokens. */
|
|
@@ -44,6 +72,11 @@ export interface StructuredToolTaskConfig<TInput extends object = Record<string,
|
|
|
44
72
|
/** Optional short outcome suffix for the completion progress message (e.g., "3 findings"). */
|
|
45
73
|
formatOutcome?: (result: TFinal) => string;
|
|
46
74
|
/** Builds the system instruction and user prompt from parsed tool input. */
|
|
47
|
-
buildPrompt: (input: TInput) => PromptParts;
|
|
75
|
+
buildPrompt: (input: TInput, ctx: ToolExecutionContext) => PromptParts;
|
|
48
76
|
}
|
|
77
|
+
export declare function wrapToolHandler<TInput, TResult extends CallToolResult>(options: {
|
|
78
|
+
toolName: string;
|
|
79
|
+
progressContext?: (input: TInput) => string;
|
|
80
|
+
}, handler: (input: TInput, extra: ProgressExtra) => Promise<TResult> | TResult): (input: TInput, extra: ProgressExtra) => Promise<TResult>;
|
|
49
81
|
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;
|
|
82
|
+
export {};
|
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';
|
|
@@ -217,6 +218,43 @@ function createGeminiLogger(server, taskId) {
|
|
|
217
218
|
}
|
|
218
219
|
};
|
|
219
220
|
}
|
|
221
|
+
export function wrapToolHandler(options, handler) {
|
|
222
|
+
return async (input, extra) => {
|
|
223
|
+
const context = normalizeProgressContext(options.progressContext?.(input));
|
|
224
|
+
// Start progress (0/1)
|
|
225
|
+
await sendTaskProgress(extra, {
|
|
226
|
+
current: 0,
|
|
227
|
+
total: 1,
|
|
228
|
+
message: formatProgressStep(options.toolName, context, 'starting'),
|
|
229
|
+
});
|
|
230
|
+
try {
|
|
231
|
+
const result = await handler(input, extra);
|
|
232
|
+
// End progress (1/1)
|
|
233
|
+
const outcome = result.isError ? 'failed' : 'completed';
|
|
234
|
+
const success = !result.isError;
|
|
235
|
+
await sendTaskProgress(extra, {
|
|
236
|
+
current: 1,
|
|
237
|
+
total: 1,
|
|
238
|
+
message: formatProgressCompletion(options.toolName, context, outcome, success),
|
|
239
|
+
});
|
|
240
|
+
return result;
|
|
241
|
+
}
|
|
242
|
+
catch (error) {
|
|
243
|
+
// Progress is best-effort; must never mask the original error.
|
|
244
|
+
try {
|
|
245
|
+
await sendTaskProgress(extra, {
|
|
246
|
+
current: 1,
|
|
247
|
+
total: 1,
|
|
248
|
+
message: formatProgressCompletion(options.toolName, context, 'failed', false),
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
// Swallow progress delivery errors so the original error propagates.
|
|
253
|
+
}
|
|
254
|
+
throw error;
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
}
|
|
220
258
|
export function registerStructuredToolTask(server, config) {
|
|
221
259
|
const responseSchema = createGeminiResponseSchema({
|
|
222
260
|
geminiSchema: config.geminiSchema,
|
|
@@ -260,9 +298,14 @@ export function registerStructuredToolTask(server, config) {
|
|
|
260
298
|
const onLog = createGeminiLogger(server, task.taskId);
|
|
261
299
|
const inputRecord = parseToolInput(input, config.fullInputSchema);
|
|
262
300
|
progressContext = normalizeProgressContext(config.progressContext?.(inputRecord));
|
|
301
|
+
// Snapshot the diff slot ONCE before any async work so that
|
|
302
|
+
// validateInput and buildPrompt observe the same state. Without
|
|
303
|
+
// this, a concurrent generate_diff call between the two awaits
|
|
304
|
+
// could replace the slot and silently bypass the budget check.
|
|
305
|
+
const ctx = { diffSlot: getDiff() };
|
|
263
306
|
await reportProgressStepUpdate(reportProgress, config.name, progressContext, 0, 'starting');
|
|
264
307
|
if (config.validateInput) {
|
|
265
|
-
const validationError = await config.validateInput(inputRecord);
|
|
308
|
+
const validationError = await config.validateInput(inputRecord, ctx);
|
|
266
309
|
if (validationError) {
|
|
267
310
|
const validationMessage = validationError.structuredContent.error?.message ??
|
|
268
311
|
INPUT_VALIDATION_FAILED;
|
|
@@ -273,7 +316,7 @@ export function registerStructuredToolTask(server, config) {
|
|
|
273
316
|
}
|
|
274
317
|
}
|
|
275
318
|
await reportProgressStepUpdate(reportProgress, config.name, progressContext, 1, 'preparing');
|
|
276
|
-
const promptParts = config.buildPrompt(inputRecord);
|
|
319
|
+
const promptParts = config.buildPrompt(inputRecord, ctx);
|
|
277
320
|
const { prompt } = promptParts;
|
|
278
321
|
const { systemInstruction } = promptParts;
|
|
279
322
|
const modelLabel = friendlyModelName(config.model);
|
|
@@ -306,7 +349,7 @@ export function registerStructuredToolTask(server, config) {
|
|
|
306
349
|
throw new Error('Unexpected state: parsed result is undefined');
|
|
307
350
|
}
|
|
308
351
|
const finalResult = (config.transformResult
|
|
309
|
-
? config.transformResult(inputRecord, parsed)
|
|
352
|
+
? config.transformResult(inputRecord, parsed, ctx)
|
|
310
353
|
: parsed);
|
|
311
354
|
const textContent = config.formatOutput
|
|
312
355
|
? config.formatOutput(finalResult)
|
package/dist/schemas/inputs.js
CHANGED
|
@@ -20,57 +20,59 @@ function createBoundedString(min, max, description) {
|
|
|
20
20
|
function createOptionalBoundedString(min, max, description) {
|
|
21
21
|
return createBoundedString(min, max, description).optional();
|
|
22
22
|
}
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
const LANGUAGE_DESCRIPTION = 'Primary programming language (e.g. TypeScript, Python, Rust). Auto-infer from file extensions. Omit if multi-language.';
|
|
24
|
+
const REPOSITORY_DESCRIPTION = 'Repository identifier (owner/repo). Auto-infer from git remote or directory name.';
|
|
25
|
+
function createLanguageSchema() {
|
|
26
|
+
return createOptionalBoundedString(INPUT_LIMITS.language.min, INPUT_LIMITS.language.max, LANGUAGE_DESCRIPTION);
|
|
25
27
|
}
|
|
26
28
|
function createRepositorySchema() {
|
|
27
|
-
return createBoundedString(INPUT_LIMITS.repository.min, INPUT_LIMITS.repository.max,
|
|
29
|
+
return createBoundedString(INPUT_LIMITS.repository.min, INPUT_LIMITS.repository.max, REPOSITORY_DESCRIPTION);
|
|
28
30
|
}
|
|
29
31
|
function createOptionalBoundedInteger(min, max, description) {
|
|
30
32
|
return z.number().int().min(min).max(max).optional().describe(description);
|
|
31
33
|
}
|
|
32
34
|
export const FileContextSchema = z.strictObject({
|
|
33
|
-
path: createBoundedString(INPUT_LIMITS.fileContext.path.min, INPUT_LIMITS.fileContext.path.max, '
|
|
35
|
+
path: createBoundedString(INPUT_LIMITS.fileContext.path.min, INPUT_LIMITS.fileContext.path.max, 'Repo-relative path (e.g. src/utils/helpers.ts).'),
|
|
34
36
|
content: createBoundedString(INPUT_LIMITS.fileContext.content.min, INPUT_LIMITS.fileContext.content.max, 'Full file content.'),
|
|
35
37
|
});
|
|
36
38
|
export const AnalyzePrImpactInputSchema = z.strictObject({
|
|
37
39
|
repository: createRepositorySchema(),
|
|
38
|
-
language: createLanguageSchema(
|
|
40
|
+
language: createLanguageSchema(),
|
|
39
41
|
});
|
|
40
42
|
export const GenerateReviewSummaryInputSchema = z.strictObject({
|
|
41
43
|
repository: createRepositorySchema(),
|
|
42
|
-
language: createLanguageSchema(
|
|
44
|
+
language: createLanguageSchema(),
|
|
43
45
|
});
|
|
44
46
|
export const InspectCodeQualityInputSchema = z.strictObject({
|
|
45
47
|
repository: createRepositorySchema(),
|
|
46
|
-
language: createLanguageSchema(
|
|
48
|
+
language: createLanguageSchema(),
|
|
47
49
|
focusAreas: z
|
|
48
|
-
.array(createBoundedString(INPUT_LIMITS.focusArea.min, INPUT_LIMITS.focusArea.max, 'Focus
|
|
50
|
+
.array(createBoundedString(INPUT_LIMITS.focusArea.min, INPUT_LIMITS.focusArea.max, 'Focus tag (e.g. security, performance, bug, logic).'))
|
|
49
51
|
.min(1)
|
|
50
52
|
.max(INPUT_LIMITS.focusArea.maxItems)
|
|
51
53
|
.optional()
|
|
52
|
-
.describe('
|
|
53
|
-
maxFindings: createOptionalBoundedInteger(INPUT_LIMITS.maxFindings.min, INPUT_LIMITS.maxFindings.max, '
|
|
54
|
+
.describe('Review focus areas. Standard tags: security, performance, correctness, maintainability, concurrency. Omit for general review.'),
|
|
55
|
+
maxFindings: createOptionalBoundedInteger(INPUT_LIMITS.maxFindings.min, INPUT_LIMITS.maxFindings.max, 'Max findings (1-25). Default: 10.'),
|
|
54
56
|
files: z
|
|
55
57
|
.array(FileContextSchema)
|
|
56
58
|
.min(1)
|
|
57
59
|
.max(INPUT_LIMITS.fileContext.maxItems)
|
|
58
60
|
.optional()
|
|
59
|
-
.describe('Full
|
|
61
|
+
.describe('Full content of changed files. Highly recommended for accurate analysis. Omit if unavailable.'),
|
|
60
62
|
});
|
|
61
63
|
export const SuggestSearchReplaceInputSchema = z.strictObject({
|
|
62
|
-
findingTitle: createBoundedString(INPUT_LIMITS.findingTitle.min, INPUT_LIMITS.findingTitle.max, '
|
|
63
|
-
findingDetails: createBoundedString(INPUT_LIMITS.findingDetails.min, INPUT_LIMITS.findingDetails.max, '
|
|
64
|
+
findingTitle: createBoundedString(INPUT_LIMITS.findingTitle.min, INPUT_LIMITS.findingTitle.max, 'Exact finding title from inspect_code_quality.'),
|
|
65
|
+
findingDetails: createBoundedString(INPUT_LIMITS.findingDetails.min, INPUT_LIMITS.findingDetails.max, 'Exact finding explanation from inspect_code_quality.'),
|
|
64
66
|
});
|
|
65
67
|
export const GenerateTestPlanInputSchema = z.strictObject({
|
|
66
68
|
repository: createRepositorySchema(),
|
|
67
|
-
language: createLanguageSchema(
|
|
68
|
-
testFramework: createOptionalBoundedString(INPUT_LIMITS.testFramework.min, INPUT_LIMITS.testFramework.max, 'Test framework
|
|
69
|
-
maxTestCases: createOptionalBoundedInteger(INPUT_LIMITS.maxTestCases.min, INPUT_LIMITS.maxTestCases.max, '
|
|
69
|
+
language: createLanguageSchema(),
|
|
70
|
+
testFramework: createOptionalBoundedString(INPUT_LIMITS.testFramework.min, INPUT_LIMITS.testFramework.max, 'Test framework (jest, vitest, pytest, node:test, junit). Auto-infer from package.json/config.'),
|
|
71
|
+
maxTestCases: createOptionalBoundedInteger(INPUT_LIMITS.maxTestCases.min, INPUT_LIMITS.maxTestCases.max, 'Max test cases (1-30). Default: 15.'),
|
|
70
72
|
});
|
|
71
73
|
export const AnalyzeComplexityInputSchema = z.strictObject({
|
|
72
|
-
language: createLanguageSchema(
|
|
74
|
+
language: createLanguageSchema(),
|
|
73
75
|
});
|
|
74
76
|
export const DetectApiBreakingInputSchema = z.strictObject({
|
|
75
|
-
language: createLanguageSchema(
|
|
77
|
+
language: createLanguageSchema(),
|
|
76
78
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { validateDiffBudget } from '../lib/diff-budget.js';
|
|
2
|
-
import { createNoDiffError
|
|
2
|
+
import { createNoDiffError } from '../lib/diff-store.js';
|
|
3
3
|
import { requireToolContract } from '../lib/tool-contracts.js';
|
|
4
4
|
import { registerStructuredToolTask } from '../lib/tool-factory.js';
|
|
5
5
|
import { AnalyzeComplexityInputSchema } from '../schemas/inputs.js';
|
|
@@ -15,7 +15,7 @@ export function registerAnalyzeComplexityTool(server) {
|
|
|
15
15
|
registerStructuredToolTask(server, {
|
|
16
16
|
name: 'analyze_time_space_complexity',
|
|
17
17
|
title: 'Analyze Time & Space Complexity',
|
|
18
|
-
description: 'Analyze Big-O complexity
|
|
18
|
+
description: 'Analyze Big-O complexity. Prerequisite: generate_diff. Auto-infer language.',
|
|
19
19
|
inputSchema: AnalyzeComplexityInputSchema,
|
|
20
20
|
fullInputSchema: AnalyzeComplexityInputSchema,
|
|
21
21
|
resultSchema: AnalyzeComplexityResultSchema,
|
|
@@ -26,8 +26,8 @@ export function registerAnalyzeComplexityTool(server) {
|
|
|
26
26
|
...(TOOL_CONTRACT.thinkingBudget !== undefined
|
|
27
27
|
? { thinkingBudget: TOOL_CONTRACT.thinkingBudget }
|
|
28
28
|
: undefined),
|
|
29
|
-
validateInput: () => {
|
|
30
|
-
const slot =
|
|
29
|
+
validateInput: (_input, ctx) => {
|
|
30
|
+
const slot = ctx.diffSlot;
|
|
31
31
|
if (!slot)
|
|
32
32
|
return createNoDiffError();
|
|
33
33
|
return validateDiffBudget(slot.diff);
|
|
@@ -36,9 +36,8 @@ export function registerAnalyzeComplexityTool(server) {
|
|
|
36
36
|
? 'Performance degradation detected'
|
|
37
37
|
: 'No degradation',
|
|
38
38
|
formatOutput: (result) => `Complexity Analysis: Time=${result.timeComplexity}, Space=${result.spaceComplexity}. ${result.explanation}`,
|
|
39
|
-
buildPrompt: (input) => {
|
|
40
|
-
const
|
|
41
|
-
const diff = slot?.diff ?? '';
|
|
39
|
+
buildPrompt: (input, ctx) => {
|
|
40
|
+
const diff = ctx.diffSlot?.diff ?? '';
|
|
42
41
|
const languageLine = input.language
|
|
43
42
|
? `\nLanguage: ${input.language}`
|
|
44
43
|
: '';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { validateDiffBudget } from '../lib/diff-budget.js';
|
|
2
2
|
import { computeDiffStatsAndSummaryFromFiles, parseDiffFiles, } from '../lib/diff-parser.js';
|
|
3
|
-
import { createNoDiffError
|
|
3
|
+
import { createNoDiffError } from '../lib/diff-store.js';
|
|
4
4
|
import { requireToolContract } from '../lib/tool-contracts.js';
|
|
5
5
|
import { registerStructuredToolTask } from '../lib/tool-factory.js';
|
|
6
6
|
import { AnalyzePrImpactInputSchema } from '../schemas/inputs.js';
|
|
@@ -19,7 +19,7 @@ export function registerAnalyzePrImpactTool(server) {
|
|
|
19
19
|
registerStructuredToolTask(server, {
|
|
20
20
|
name: 'analyze_pr_impact',
|
|
21
21
|
title: 'Analyze PR Impact',
|
|
22
|
-
description: 'Assess
|
|
22
|
+
description: 'Assess impact and risk from cached diff. Prerequisite: generate_diff. Auto-infer repo/language.',
|
|
23
23
|
inputSchema: AnalyzePrImpactInputSchema,
|
|
24
24
|
fullInputSchema: AnalyzePrImpactInputSchema,
|
|
25
25
|
resultSchema: PrImpactResultSchema,
|
|
@@ -27,17 +27,16 @@ export function registerAnalyzePrImpactTool(server) {
|
|
|
27
27
|
model: TOOL_CONTRACT.model,
|
|
28
28
|
timeoutMs: TOOL_CONTRACT.timeoutMs,
|
|
29
29
|
maxOutputTokens: TOOL_CONTRACT.maxOutputTokens,
|
|
30
|
-
validateInput: () => {
|
|
31
|
-
const slot =
|
|
30
|
+
validateInput: (_input, ctx) => {
|
|
31
|
+
const slot = ctx.diffSlot;
|
|
32
32
|
if (!slot)
|
|
33
33
|
return createNoDiffError();
|
|
34
34
|
return validateDiffBudget(slot.diff);
|
|
35
35
|
},
|
|
36
36
|
formatOutcome: (result) => `severity: ${result.severity}`,
|
|
37
37
|
formatOutput: (result) => `Impact Analysis (${result.severity}): ${result.summary}`,
|
|
38
|
-
buildPrompt: (input) => {
|
|
39
|
-
const
|
|
40
|
-
const diff = slot?.diff ?? '';
|
|
38
|
+
buildPrompt: (input, ctx) => {
|
|
39
|
+
const diff = ctx.diffSlot?.diff ?? '';
|
|
41
40
|
const files = parseDiffFiles(diff);
|
|
42
41
|
const { stats, summary: fileSummary } = computeDiffStatsAndSummaryFromFiles(files);
|
|
43
42
|
const languageSegment = formatLanguageSegment(input.language);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { validateDiffBudget } from '../lib/diff-budget.js';
|
|
2
|
-
import { createNoDiffError
|
|
2
|
+
import { createNoDiffError } from '../lib/diff-store.js';
|
|
3
3
|
import { requireToolContract } from '../lib/tool-contracts.js';
|
|
4
4
|
import { registerStructuredToolTask } from '../lib/tool-factory.js';
|
|
5
5
|
import { DetectApiBreakingInputSchema } from '../schemas/inputs.js';
|
|
@@ -15,7 +15,7 @@ export function registerDetectApiBreakingTool(server) {
|
|
|
15
15
|
registerStructuredToolTask(server, {
|
|
16
16
|
name: 'detect_api_breaking_changes',
|
|
17
17
|
title: 'Detect API Breaking Changes',
|
|
18
|
-
description: 'Detect breaking changes
|
|
18
|
+
description: 'Detect breaking API changes. Prerequisite: generate_diff. Auto-infer language.',
|
|
19
19
|
inputSchema: DetectApiBreakingInputSchema,
|
|
20
20
|
fullInputSchema: DetectApiBreakingInputSchema,
|
|
21
21
|
resultSchema: DetectApiBreakingResultSchema,
|
|
@@ -23,8 +23,8 @@ export function registerDetectApiBreakingTool(server) {
|
|
|
23
23
|
model: TOOL_CONTRACT.model,
|
|
24
24
|
timeoutMs: TOOL_CONTRACT.timeoutMs,
|
|
25
25
|
maxOutputTokens: TOOL_CONTRACT.maxOutputTokens,
|
|
26
|
-
validateInput: () => {
|
|
27
|
-
const slot =
|
|
26
|
+
validateInput: (_input, ctx) => {
|
|
27
|
+
const slot = ctx.diffSlot;
|
|
28
28
|
if (!slot)
|
|
29
29
|
return createNoDiffError();
|
|
30
30
|
return validateDiffBudget(slot.diff);
|
|
@@ -33,9 +33,8 @@ export function registerDetectApiBreakingTool(server) {
|
|
|
33
33
|
formatOutput: (result) => result.hasBreakingChanges
|
|
34
34
|
? `API Breaking Changes: ${result.breakingChanges.length} found.`
|
|
35
35
|
: 'No API breaking changes detected.',
|
|
36
|
-
buildPrompt: (input) => {
|
|
37
|
-
const
|
|
38
|
-
const diff = slot?.diff ?? '';
|
|
36
|
+
buildPrompt: (input, ctx) => {
|
|
37
|
+
const diff = ctx.diffSlot?.diff ?? '';
|
|
39
38
|
const languageLine = input.language
|
|
40
39
|
? `\nLanguage: ${input.language}`
|
|
41
40
|
: '';
|
|
@@ -3,6 +3,7 @@ import { z } from 'zod';
|
|
|
3
3
|
import { cleanDiff, isEmptyDiff, NOISY_EXCLUDE_PATHSPECS, } from '../lib/diff-cleaner.js';
|
|
4
4
|
import { computeDiffStatsFromFiles, parseDiffFiles, } from '../lib/diff-parser.js';
|
|
5
5
|
import { DIFF_RESOURCE_URI, storeDiff } from '../lib/diff-store.js';
|
|
6
|
+
import { wrapToolHandler } from '../lib/tool-factory.js';
|
|
6
7
|
import { createErrorToolResponse, createToolResponse, } from '../lib/tool-response.js';
|
|
7
8
|
const GIT_TIMEOUT_MS = 30_000;
|
|
8
9
|
const GIT_MAX_BUFFER = 10 * 1024 * 1024; // 10 MB
|
|
@@ -29,6 +30,9 @@ export function registerGenerateDiffTool(server) {
|
|
|
29
30
|
.enum(['unstaged', 'staged'])
|
|
30
31
|
.describe('"unstaged": working-tree changes not yet staged. "staged": changes added to the index with git add.'),
|
|
31
32
|
},
|
|
33
|
+
}, wrapToolHandler({
|
|
34
|
+
toolName: 'generate_diff',
|
|
35
|
+
progressContext: (input) => input.mode,
|
|
32
36
|
}, (input) => {
|
|
33
37
|
const { mode } = input;
|
|
34
38
|
const args = buildGitArgs(mode);
|
|
@@ -67,5 +71,5 @@ export function registerGenerateDiffTool(server) {
|
|
|
67
71
|
message: summary,
|
|
68
72
|
},
|
|
69
73
|
}, summary);
|
|
70
|
-
});
|
|
74
|
+
}));
|
|
71
75
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { validateDiffBudget } from '../lib/diff-budget.js';
|
|
2
2
|
import { computeDiffStatsFromFiles, parseDiffFiles, } from '../lib/diff-parser.js';
|
|
3
|
-
import { createNoDiffError
|
|
3
|
+
import { createNoDiffError } from '../lib/diff-store.js';
|
|
4
4
|
import { requireToolContract } from '../lib/tool-contracts.js';
|
|
5
|
-
import { registerStructuredToolTask } from '../lib/tool-factory.js';
|
|
5
|
+
import { registerStructuredToolTask, } from '../lib/tool-factory.js';
|
|
6
6
|
import { GenerateReviewSummaryInputSchema } from '../schemas/inputs.js';
|
|
7
7
|
import { ReviewSummaryResultSchema } from '../schemas/outputs.js';
|
|
8
8
|
const ReviewSummaryModelSchema = ReviewSummaryResultSchema.omit({
|
|
@@ -17,11 +17,16 @@ Return strict JSON only.
|
|
|
17
17
|
function formatLanguageSegment(language) {
|
|
18
18
|
return language ? `\nLanguage: ${language}` : '';
|
|
19
19
|
}
|
|
20
|
+
function getDiffStats(ctx) {
|
|
21
|
+
const diff = ctx.diffSlot?.diff ?? '';
|
|
22
|
+
const { files, added, deleted } = computeDiffStatsFromFiles(parseDiffFiles(diff));
|
|
23
|
+
return { diff, files, added, deleted };
|
|
24
|
+
}
|
|
20
25
|
export function registerGenerateReviewSummaryTool(server) {
|
|
21
26
|
registerStructuredToolTask(server, {
|
|
22
27
|
name: 'generate_review_summary',
|
|
23
28
|
title: 'Generate Review Summary',
|
|
24
|
-
description: 'Summarize
|
|
29
|
+
description: 'Summarize diff and risk level. Prerequisite: generate_diff. Auto-infer repo/language.',
|
|
25
30
|
inputSchema: GenerateReviewSummaryInputSchema,
|
|
26
31
|
fullInputSchema: GenerateReviewSummaryInputSchema,
|
|
27
32
|
resultSchema: ReviewSummaryModelSchema,
|
|
@@ -29,39 +34,33 @@ export function registerGenerateReviewSummaryTool(server) {
|
|
|
29
34
|
model: TOOL_CONTRACT.model,
|
|
30
35
|
timeoutMs: TOOL_CONTRACT.timeoutMs,
|
|
31
36
|
maxOutputTokens: TOOL_CONTRACT.maxOutputTokens,
|
|
32
|
-
validateInput: () => {
|
|
33
|
-
const slot =
|
|
37
|
+
validateInput: (_input, ctx) => {
|
|
38
|
+
const slot = ctx.diffSlot;
|
|
34
39
|
if (!slot)
|
|
35
40
|
return createNoDiffError();
|
|
36
41
|
return validateDiffBudget(slot.diff);
|
|
37
42
|
},
|
|
38
43
|
formatOutcome: (result) => `risk: ${result.overallRisk}`,
|
|
39
|
-
transformResult: (
|
|
40
|
-
const
|
|
41
|
-
const diff = slot?.diff ?? '';
|
|
42
|
-
const parsedFiles = parseDiffFiles(diff);
|
|
43
|
-
const stats = computeDiffStatsFromFiles(parsedFiles);
|
|
44
|
+
transformResult: (_input, result, ctx) => {
|
|
45
|
+
const { files, added, deleted } = getDiffStats(ctx);
|
|
44
46
|
return {
|
|
45
47
|
...result,
|
|
46
48
|
stats: {
|
|
47
|
-
filesChanged:
|
|
48
|
-
linesAdded:
|
|
49
|
-
linesRemoved:
|
|
49
|
+
filesChanged: files,
|
|
50
|
+
linesAdded: added,
|
|
51
|
+
linesRemoved: deleted,
|
|
50
52
|
},
|
|
51
53
|
};
|
|
52
54
|
},
|
|
53
55
|
formatOutput: (result) => `Review Summary: ${result.summary}\nRecommendation: ${result.recommendation}`,
|
|
54
|
-
buildPrompt: (input) => {
|
|
55
|
-
const
|
|
56
|
-
const diff = slot?.diff ?? '';
|
|
57
|
-
const parsedFiles = parseDiffFiles(diff);
|
|
58
|
-
const stats = computeDiffStatsFromFiles(parsedFiles);
|
|
56
|
+
buildPrompt: (input, ctx) => {
|
|
57
|
+
const { diff, files, added, deleted } = getDiffStats(ctx);
|
|
59
58
|
const languageSegment = formatLanguageSegment(input.language);
|
|
60
59
|
return {
|
|
61
60
|
systemInstruction: SYSTEM_INSTRUCTION,
|
|
62
61
|
prompt: `
|
|
63
62
|
Repository: ${input.repository}${languageSegment}
|
|
64
|
-
Stats: ${
|
|
63
|
+
Stats: ${files} files, +${added}, -${deleted}
|
|
65
64
|
|
|
66
65
|
Diff:
|
|
67
66
|
${diff}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { validateDiffBudget } from '../lib/diff-budget.js';
|
|
2
2
|
import { computeDiffStatsAndPathsFromFiles, parseDiffFiles, } from '../lib/diff-parser.js';
|
|
3
|
-
import { createNoDiffError
|
|
3
|
+
import { createNoDiffError } from '../lib/diff-store.js';
|
|
4
4
|
import { requireToolContract } from '../lib/tool-contracts.js';
|
|
5
5
|
import { registerStructuredToolTask } from '../lib/tool-factory.js';
|
|
6
6
|
import { GenerateTestPlanInputSchema } from '../schemas/inputs.js';
|
|
@@ -19,7 +19,7 @@ export function registerGenerateTestPlanTool(server) {
|
|
|
19
19
|
registerStructuredToolTask(server, {
|
|
20
20
|
name: 'generate_test_plan',
|
|
21
21
|
title: 'Generate Test Plan',
|
|
22
|
-
description: '
|
|
22
|
+
description: 'Generate test cases. Prerequisite: generate_diff. Auto-infer repo/language/framework.',
|
|
23
23
|
inputSchema: GenerateTestPlanInputSchema,
|
|
24
24
|
fullInputSchema: GenerateTestPlanInputSchema,
|
|
25
25
|
resultSchema: TestPlanResultSchema,
|
|
@@ -30,8 +30,8 @@ export function registerGenerateTestPlanTool(server) {
|
|
|
30
30
|
...(TOOL_CONTRACT.thinkingBudget !== undefined
|
|
31
31
|
? { thinkingBudget: TOOL_CONTRACT.thinkingBudget }
|
|
32
32
|
: undefined),
|
|
33
|
-
validateInput: () => {
|
|
34
|
-
const slot =
|
|
33
|
+
validateInput: (_input, ctx) => {
|
|
34
|
+
const slot = ctx.diffSlot;
|
|
35
35
|
if (!slot)
|
|
36
36
|
return createNoDiffError();
|
|
37
37
|
return validateDiffBudget(slot.diff);
|
|
@@ -42,9 +42,8 @@ export function registerGenerateTestPlanTool(server) {
|
|
|
42
42
|
const cappedTestCases = result.testCases.slice(0, input.maxTestCases ?? result.testCases.length);
|
|
43
43
|
return { ...result, testCases: cappedTestCases };
|
|
44
44
|
},
|
|
45
|
-
buildPrompt: (input) => {
|
|
46
|
-
const
|
|
47
|
-
const diff = slot?.diff ?? '';
|
|
45
|
+
buildPrompt: (input, ctx) => {
|
|
46
|
+
const diff = ctx.diffSlot?.diff ?? '';
|
|
48
47
|
const parsedFiles = parseDiffFiles(diff);
|
|
49
48
|
const { stats, paths } = computeDiffStatsAndPathsFromFiles(parsedFiles);
|
|
50
49
|
const languageLine = formatOptionalLine('Language', input.language);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { validateContextBudget } from '../lib/context-budget.js';
|
|
2
2
|
import { validateDiffBudget } from '../lib/diff-budget.js';
|
|
3
3
|
import { computeDiffStatsAndSummaryFromFiles, parseDiffFiles, } from '../lib/diff-parser.js';
|
|
4
|
-
import { createNoDiffError
|
|
4
|
+
import { createNoDiffError } from '../lib/diff-store.js';
|
|
5
5
|
import { requireToolContract } from '../lib/tool-contracts.js';
|
|
6
6
|
import { registerStructuredToolTask } from '../lib/tool-factory.js';
|
|
7
7
|
import { InspectCodeQualityInputSchema } from '../schemas/inputs.js';
|
|
@@ -54,7 +54,7 @@ export function registerInspectCodeQualityTool(server) {
|
|
|
54
54
|
registerStructuredToolTask(server, {
|
|
55
55
|
name: 'inspect_code_quality',
|
|
56
56
|
title: 'Inspect Code Quality',
|
|
57
|
-
description: 'Deep
|
|
57
|
+
description: 'Deep code review. Prerequisite: generate_diff. Auto-infer repo/language/focus. Provide file content for best results.',
|
|
58
58
|
inputSchema: InspectCodeQualityInputSchema,
|
|
59
59
|
fullInputSchema: InspectCodeQualityInputSchema,
|
|
60
60
|
resultSchema: CodeQualityOutputSchema,
|
|
@@ -70,8 +70,8 @@ export function registerInspectCodeQualityTool(server) {
|
|
|
70
70
|
const fileCount = input.files?.length;
|
|
71
71
|
return fileCount ? `+${fileCount} files` : '';
|
|
72
72
|
},
|
|
73
|
-
validateInput: (input) => {
|
|
74
|
-
const slot =
|
|
73
|
+
validateInput: (input, ctx) => {
|
|
74
|
+
const slot = ctx.diffSlot;
|
|
75
75
|
if (!slot)
|
|
76
76
|
return createNoDiffError();
|
|
77
77
|
const diffError = validateDiffBudget(slot.diff);
|
|
@@ -93,9 +93,8 @@ export function registerInspectCodeQualityTool(server) {
|
|
|
93
93
|
const cappedFindings = capFindings(result.findings, input.maxFindings);
|
|
94
94
|
return { ...result, findings: cappedFindings, totalFindings };
|
|
95
95
|
},
|
|
96
|
-
buildPrompt: (input) => {
|
|
97
|
-
const
|
|
98
|
-
const diff = slot?.diff ?? '';
|
|
96
|
+
buildPrompt: (input, ctx) => {
|
|
97
|
+
const diff = ctx.diffSlot?.diff ?? '';
|
|
99
98
|
const parsedFiles = parseDiffFiles(diff);
|
|
100
99
|
const { summary: fileSummary } = computeDiffStatsAndSummaryFromFiles(parsedFiles);
|
|
101
100
|
const fileContext = formatFileContext(input.files);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { validateDiffBudget } from '../lib/diff-budget.js';
|
|
2
2
|
import { extractChangedPathsFromFiles, parseDiffFiles, } from '../lib/diff-parser.js';
|
|
3
|
-
import { createNoDiffError
|
|
3
|
+
import { createNoDiffError } from '../lib/diff-store.js';
|
|
4
4
|
import { requireToolContract } from '../lib/tool-contracts.js';
|
|
5
5
|
import { registerStructuredToolTask } from '../lib/tool-factory.js';
|
|
6
6
|
import { SuggestSearchReplaceInputSchema } from '../schemas/inputs.js';
|
|
@@ -19,7 +19,7 @@ export function registerSuggestSearchReplaceTool(server) {
|
|
|
19
19
|
registerStructuredToolTask(server, {
|
|
20
20
|
name: 'suggest_search_replace',
|
|
21
21
|
title: 'Suggest Search & Replace',
|
|
22
|
-
description: 'Generate
|
|
22
|
+
description: 'Generate fix patches. Prerequisite: inspect_code_quality findings. Pass verbatim finding title/details.',
|
|
23
23
|
inputSchema: SuggestSearchReplaceInputSchema,
|
|
24
24
|
fullInputSchema: SuggestSearchReplaceInputSchema,
|
|
25
25
|
resultSchema: SearchReplaceResultSchema,
|
|
@@ -30,8 +30,8 @@ export function registerSuggestSearchReplaceTool(server) {
|
|
|
30
30
|
...(TOOL_CONTRACT.thinkingBudget !== undefined
|
|
31
31
|
? { thinkingBudget: TOOL_CONTRACT.thinkingBudget }
|
|
32
32
|
: undefined),
|
|
33
|
-
validateInput: () => {
|
|
34
|
-
const slot =
|
|
33
|
+
validateInput: (_input, ctx) => {
|
|
34
|
+
const slot = ctx.diffSlot;
|
|
35
35
|
if (!slot)
|
|
36
36
|
return createNoDiffError();
|
|
37
37
|
return validateDiffBudget(slot.diff);
|
|
@@ -42,9 +42,8 @@ export function registerSuggestSearchReplaceTool(server) {
|
|
|
42
42
|
const patches = formatPatchCount(count);
|
|
43
43
|
return `${result.summary}\n${patches} • Checklist: ${result.validationChecklist.join(' | ')}`;
|
|
44
44
|
},
|
|
45
|
-
buildPrompt: (input) => {
|
|
46
|
-
const
|
|
47
|
-
const diff = slot?.diff ?? '';
|
|
45
|
+
buildPrompt: (input, ctx) => {
|
|
46
|
+
const diff = ctx.diffSlot?.diff ?? '';
|
|
48
47
|
const files = parseDiffFiles(diff);
|
|
49
48
|
const paths = extractChangedPathsFromFiles(files);
|
|
50
49
|
return {
|