@j0hanz/code-review-analyst-mcp 1.0.2 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +7 -6
- package/dist/instructions.md +4 -6
- package/dist/lib/diff-budget.js +15 -9
- package/dist/lib/errors.js +4 -1
- package/dist/lib/gemini-schema.js +5 -4
- package/dist/lib/gemini.js +78 -34
- package/dist/lib/tool-factory.d.ts +3 -3
- package/dist/lib/tool-factory.js +35 -18
- package/dist/lib/tool-response.js +7 -5
- package/dist/lib/types.d.ts +2 -2
- package/dist/prompts/index.js +70 -2
- package/dist/resources/index.js +6 -3
- package/dist/schemas/inputs.js +9 -31
- package/dist/schemas/outputs.js +12 -45
- package/dist/server.js +13 -9
- package/dist/tools/review-diff.js +8 -7
- package/dist/tools/risk-score.js +8 -7
- package/dist/tools/suggest-patch.js +8 -7
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -4,6 +4,11 @@ import { parseArgs } from 'node:util';
|
|
|
4
4
|
import { getErrorMessage } from './lib/errors.js';
|
|
5
5
|
import { createServer } from './server.js';
|
|
6
6
|
const SHUTDOWN_SIGNALS = ['SIGINT', 'SIGTERM'];
|
|
7
|
+
function setEnvFromArg(name, value) {
|
|
8
|
+
if (typeof value === 'string') {
|
|
9
|
+
process.env[name] = value;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
7
12
|
function parseCommandLineArgs() {
|
|
8
13
|
const { values } = parseArgs({
|
|
9
14
|
args: process.argv.slice(2),
|
|
@@ -18,12 +23,8 @@ function parseCommandLineArgs() {
|
|
|
18
23
|
},
|
|
19
24
|
strict: false,
|
|
20
25
|
});
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
if (typeof values['max-diff-chars'] === 'string') {
|
|
25
|
-
process.env.MAX_DIFF_CHARS = values['max-diff-chars'];
|
|
26
|
-
}
|
|
26
|
+
setEnvFromArg('GEMINI_MODEL', values.model);
|
|
27
|
+
setEnvFromArg('MAX_DIFF_CHARS', values['max-diff-chars']);
|
|
27
28
|
}
|
|
28
29
|
let shuttingDown = false;
|
|
29
30
|
async function shutdown(server) {
|
package/dist/instructions.md
CHANGED
|
@@ -67,7 +67,7 @@ These instructions are available as a resource (internal://instructions) or prom
|
|
|
67
67
|
|
|
68
68
|
- Purpose: Generate structured review findings, overall risk, and test recommendations from a unified diff.
|
|
69
69
|
- Input: `maxFindings` defaults to 10; `focusAreas` defaults to security/correctness/regressions/performance when omitted.
|
|
70
|
-
- Output: `ok/result/error` envelope; successful payload
|
|
70
|
+
- Output: `ok/result/error` envelope; successful payload includes `summary`, `overallRisk`, `findings[]`, and `testsNeeded[]`.
|
|
71
71
|
- Gotcha: Schema allows `diff` up to 400,000 chars, but runtime rejects payloads above `MAX_DIFF_CHARS` (default 120,000) with `E_INPUT_TOO_LARGE`.
|
|
72
72
|
- Side effects: Calls external Gemini API (`openWorldHint: true`); does not mutate local state (`readOnlyHint: true`).
|
|
73
73
|
|
|
@@ -75,7 +75,7 @@ These instructions are available as a resource (internal://instructions) or prom
|
|
|
75
75
|
|
|
76
76
|
- Purpose: Produce deployment risk score and rationale for release decisions.
|
|
77
77
|
- Input: `deploymentCriticality` defaults to `medium` when omitted.
|
|
78
|
-
- Output: `ok/result/error` envelope; successful payload includes `score`, `bucket`, and `rationale`.
|
|
78
|
+
- Output: `ok/result/error` envelope; successful payload includes `score`, `bucket`, and `rationale[]`.
|
|
79
79
|
- Gotcha: Uses the same runtime diff budget guard as other tools; oversized inputs fail before model execution.
|
|
80
80
|
- Side effects: External Gemini call only.
|
|
81
81
|
|
|
@@ -83,7 +83,7 @@ These instructions are available as a resource (internal://instructions) or prom
|
|
|
83
83
|
|
|
84
84
|
- Purpose: Generate a focused unified diff patch for one selected review finding.
|
|
85
85
|
- Input: `patchStyle` defaults to `balanced`; requires both `findingTitle` and `findingDetails`.
|
|
86
|
-
- Output: `ok/result/error` envelope; successful payload includes `summary`, `patch`, and `validationChecklist`.
|
|
86
|
+
- Output: `ok/result/error` envelope; successful payload includes `summary`, `patch`, and `validationChecklist[]`.
|
|
87
87
|
- Gotcha: Output is model-generated text and must be validated before application.
|
|
88
88
|
- Side effects: External Gemini call only.
|
|
89
89
|
|
|
@@ -104,7 +104,7 @@ These instructions are available as a resource (internal://instructions) or prom
|
|
|
104
104
|
- API credentials: Require `GEMINI_API_KEY` or `GOOGLE_API_KEY`.
|
|
105
105
|
- Model selection: Uses `GEMINI_MODEL` if set; defaults to `gemini-2.5-flash`.
|
|
106
106
|
- Diff size: Runtime limit defaults to 120,000 chars (`MAX_DIFF_CHARS` env override). Input schema max is 400,000 chars.
|
|
107
|
-
- Timeout/retries: Per-call timeout defaults to
|
|
107
|
+
- Timeout/retries: Per-call timeout defaults to 45,000 ms; retry count defaults to 1 with exponential backoff.
|
|
108
108
|
- Output tokens: `maxOutputTokens` defaults to 16,384 to prevent unbounded responses.
|
|
109
109
|
- Safety config: Gemini safety thresholds default to `BLOCK_NONE` for configured harm categories and can be overridden with `GEMINI_HARM_BLOCK_THRESHOLD` (`BLOCK_NONE`, `BLOCK_ONLY_HIGH`, `BLOCK_MEDIUM_AND_ABOVE`, `BLOCK_LOW_AND_ABOVE`).
|
|
110
110
|
- Resource scope: Only `internal://instructions` is registered as a resource; no dynamic resource templates are exposed.
|
|
@@ -122,5 +122,3 @@ These instructions are available as a resource (internal://instructions) or prom
|
|
|
122
122
|
- Gemini timeout message (`Gemini request timed out after ...ms.`): Request exceeded timeout budget. → Reduce prompt/diff size or increase `timeoutMs` in caller.
|
|
123
123
|
- Empty model body (`Gemini returned an empty response body.`): Provider returned no text payload. → Retry and inspect model/service status.
|
|
124
124
|
- JSON parse failure from model output (wrapped by tool error codes): Output was not valid JSON. → Retry with same schema; inspect logs for malformed response text.
|
|
125
|
-
|
|
126
|
-
---
|
package/dist/lib/diff-budget.js
CHANGED
|
@@ -2,22 +2,28 @@ import { createErrorToolResponse } from './tool-response.js';
|
|
|
2
2
|
const DEFAULT_MAX_DIFF_CHARS = 120_000;
|
|
3
3
|
const MAX_DIFF_CHARS_ENV_VAR = 'MAX_DIFF_CHARS';
|
|
4
4
|
const numberFormatter = new Intl.NumberFormat('en-US');
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
// Lazy-cached: first call happens after parseCommandLineArgs() sets MAX_DIFF_CHARS.
|
|
6
|
+
let _maxDiffChars;
|
|
7
|
+
function parsePositiveInteger(value) {
|
|
8
|
+
const parsed = Number.parseInt(value, 10);
|
|
9
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
return parsed;
|
|
11
13
|
}
|
|
12
14
|
function getMaxDiffChars() {
|
|
13
|
-
|
|
15
|
+
if (_maxDiffChars !== undefined)
|
|
16
|
+
return _maxDiffChars;
|
|
17
|
+
const value = parsePositiveInteger(process.env[MAX_DIFF_CHARS_ENV_VAR] ?? '') ??
|
|
18
|
+
DEFAULT_MAX_DIFF_CHARS;
|
|
19
|
+
_maxDiffChars = value;
|
|
20
|
+
return value;
|
|
14
21
|
}
|
|
15
22
|
export function exceedsDiffBudget(diff) {
|
|
16
23
|
return diff.length > getMaxDiffChars();
|
|
17
24
|
}
|
|
18
25
|
export function getDiffBudgetError(diffLength) {
|
|
19
|
-
|
|
20
|
-
return `diff exceeds max allowed size (${formatNumber(diffLength)} chars > ${formatNumber(maxDiffChars)} chars)`;
|
|
26
|
+
return `diff exceeds max allowed size (${numberFormatter.format(diffLength)} chars > ${numberFormatter.format(getMaxDiffChars())} chars)`;
|
|
21
27
|
}
|
|
22
28
|
export function validateDiffBudget(diff) {
|
|
23
29
|
if (!exceedsDiffBudget(diff)) {
|
package/dist/lib/errors.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { inspect } from 'node:util';
|
|
2
|
+
function isString(value) {
|
|
3
|
+
return typeof value === 'string';
|
|
4
|
+
}
|
|
2
5
|
function isErrorWithMessage(error) {
|
|
3
6
|
return (typeof error === 'object' &&
|
|
4
7
|
error !== null &&
|
|
@@ -9,7 +12,7 @@ export function getErrorMessage(error) {
|
|
|
9
12
|
if (isErrorWithMessage(error)) {
|
|
10
13
|
return error.message;
|
|
11
14
|
}
|
|
12
|
-
if (
|
|
15
|
+
if (isString(error)) {
|
|
13
16
|
return error;
|
|
14
17
|
}
|
|
15
18
|
return inspect(error, { depth: 3, breakLength: 120 });
|
|
@@ -15,6 +15,9 @@ const CONSTRAINT_KEYS = new Set([
|
|
|
15
15
|
'maxItems',
|
|
16
16
|
'multipleOf',
|
|
17
17
|
]);
|
|
18
|
+
function isJsonRecord(value) {
|
|
19
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
20
|
+
}
|
|
18
21
|
/**
|
|
19
22
|
* Recursively strips value-range constraints (`min*`, `max*`, `multipleOf`)
|
|
20
23
|
* from a JSON Schema object and converts `"type": "integer"` to
|
|
@@ -36,11 +39,9 @@ export function stripJsonSchemaConstraints(schema) {
|
|
|
36
39
|
continue;
|
|
37
40
|
}
|
|
38
41
|
if (Array.isArray(value)) {
|
|
39
|
-
result[key] = value.map((item) =>
|
|
40
|
-
? stripJsonSchemaConstraints(item)
|
|
41
|
-
: item);
|
|
42
|
+
result[key] = value.map((item) => isJsonRecord(item) ? stripJsonSchemaConstraints(item) : item);
|
|
42
43
|
}
|
|
43
|
-
else if (
|
|
44
|
+
else if (isJsonRecord(value)) {
|
|
44
45
|
result[key] = stripJsonSchemaConstraints(value);
|
|
45
46
|
}
|
|
46
47
|
else {
|
package/dist/lib/gemini.js
CHANGED
|
@@ -3,18 +3,33 @@ import { randomInt, randomUUID } from 'node:crypto';
|
|
|
3
3
|
import { EventEmitter } from 'node:events';
|
|
4
4
|
import { performance } from 'node:perf_hooks';
|
|
5
5
|
import { setTimeout as sleep } from 'node:timers/promises';
|
|
6
|
+
import { debuglog } from 'node:util';
|
|
6
7
|
import { GoogleGenAI, HarmBlockThreshold, HarmCategory } from '@google/genai';
|
|
7
8
|
import { getErrorMessage } from './errors.js';
|
|
9
|
+
// Lazy-cached: first call happens after parseCommandLineArgs() sets GEMINI_MODEL.
|
|
10
|
+
let _defaultModel;
|
|
8
11
|
function getDefaultModel() {
|
|
9
|
-
|
|
12
|
+
if (_defaultModel !== undefined)
|
|
13
|
+
return _defaultModel;
|
|
14
|
+
const value = process.env.GEMINI_MODEL ?? 'gemini-2.5-flash';
|
|
15
|
+
_defaultModel = value;
|
|
16
|
+
return value;
|
|
10
17
|
}
|
|
11
18
|
const DEFAULT_MAX_RETRIES = 1;
|
|
12
|
-
const DEFAULT_TIMEOUT_MS =
|
|
19
|
+
const DEFAULT_TIMEOUT_MS = 45_000;
|
|
13
20
|
const DEFAULT_MAX_OUTPUT_TOKENS = 16_384;
|
|
14
21
|
const RETRY_DELAY_BASE_MS = 300;
|
|
15
22
|
const RETRY_DELAY_MAX_MS = 5_000;
|
|
16
23
|
const RETRY_JITTER_RATIO = 0.2;
|
|
17
24
|
const DEFAULT_SAFETY_THRESHOLD = HarmBlockThreshold.BLOCK_NONE;
|
|
25
|
+
const RETRYABLE_NUMERIC_CODES = new Set([429, 500, 502, 503, 504]);
|
|
26
|
+
const RETRYABLE_TRANSIENT_CODES = new Set([
|
|
27
|
+
'RESOURCE_EXHAUSTED',
|
|
28
|
+
'UNAVAILABLE',
|
|
29
|
+
'DEADLINE_EXCEEDED',
|
|
30
|
+
'INTERNAL',
|
|
31
|
+
'ABORTED',
|
|
32
|
+
]);
|
|
18
33
|
const numberFormatter = new Intl.NumberFormat('en-US');
|
|
19
34
|
function formatNumber(value) {
|
|
20
35
|
return numberFormatter.format(value);
|
|
@@ -25,14 +40,32 @@ const SAFETY_THRESHOLD_BY_NAME = {
|
|
|
25
40
|
BLOCK_MEDIUM_AND_ABOVE: HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE,
|
|
26
41
|
BLOCK_LOW_AND_ABOVE: HarmBlockThreshold.BLOCK_LOW_AND_ABOVE,
|
|
27
42
|
};
|
|
43
|
+
function getSafetyThreshold() {
|
|
44
|
+
const threshold = process.env.GEMINI_HARM_BLOCK_THRESHOLD;
|
|
45
|
+
if (!threshold) {
|
|
46
|
+
return DEFAULT_SAFETY_THRESHOLD;
|
|
47
|
+
}
|
|
48
|
+
const normalizedThreshold = threshold.trim().toUpperCase();
|
|
49
|
+
if (normalizedThreshold in SAFETY_THRESHOLD_BY_NAME) {
|
|
50
|
+
return SAFETY_THRESHOLD_BY_NAME[normalizedThreshold];
|
|
51
|
+
}
|
|
52
|
+
return DEFAULT_SAFETY_THRESHOLD;
|
|
53
|
+
}
|
|
28
54
|
let cachedClient;
|
|
29
55
|
export const geminiEvents = new EventEmitter();
|
|
56
|
+
const debug = debuglog('gemini');
|
|
30
57
|
geminiEvents.on('log', (payload) => {
|
|
31
|
-
|
|
58
|
+
debug(JSON.stringify(payload));
|
|
32
59
|
});
|
|
33
60
|
const geminiContext = new AsyncLocalStorage({
|
|
34
61
|
name: 'gemini_request',
|
|
62
|
+
defaultValue: { requestId: 'unknown', model: 'unknown' },
|
|
35
63
|
});
|
|
64
|
+
// Shared fallback avoids a fresh object allocation per logEvent call when outside a run context.
|
|
65
|
+
const UNKNOWN_CONTEXT = {
|
|
66
|
+
requestId: 'unknown',
|
|
67
|
+
model: 'unknown',
|
|
68
|
+
};
|
|
36
69
|
function getApiKey() {
|
|
37
70
|
const apiKey = process.env.GEMINI_API_KEY ?? process.env.GOOGLE_API_KEY;
|
|
38
71
|
if (!apiKey) {
|
|
@@ -51,14 +84,22 @@ function nextRequestId() {
|
|
|
51
84
|
return randomUUID();
|
|
52
85
|
}
|
|
53
86
|
function logEvent(event, details) {
|
|
54
|
-
const context = geminiContext.getStore();
|
|
87
|
+
const context = geminiContext.getStore() ?? UNKNOWN_CONTEXT;
|
|
55
88
|
geminiEvents.emit('log', {
|
|
56
89
|
event,
|
|
57
|
-
requestId: context
|
|
58
|
-
model: context
|
|
90
|
+
requestId: context.requestId,
|
|
91
|
+
model: context.model,
|
|
59
92
|
...details,
|
|
60
93
|
});
|
|
61
94
|
}
|
|
95
|
+
async function safeCallOnLog(onLog, level, data) {
|
|
96
|
+
try {
|
|
97
|
+
await onLog?.(level, data);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// Log callbacks are best-effort; never fail the tool call.
|
|
101
|
+
}
|
|
102
|
+
}
|
|
62
103
|
function getNestedError(error) {
|
|
63
104
|
if (!error || typeof error !== 'object') {
|
|
64
105
|
return undefined;
|
|
@@ -101,19 +142,12 @@ function getTransientErrorCode(error) {
|
|
|
101
142
|
}
|
|
102
143
|
function shouldRetry(error) {
|
|
103
144
|
const numericCode = getNumericErrorCode(error);
|
|
104
|
-
if (numericCode
|
|
105
|
-
numericCode === 500 ||
|
|
106
|
-
numericCode === 502 ||
|
|
107
|
-
numericCode === 503 ||
|
|
108
|
-
numericCode === 504) {
|
|
145
|
+
if (numericCode !== undefined && RETRYABLE_NUMERIC_CODES.has(numericCode)) {
|
|
109
146
|
return true;
|
|
110
147
|
}
|
|
111
148
|
const transientCode = getTransientErrorCode(error);
|
|
112
|
-
if (transientCode
|
|
113
|
-
transientCode
|
|
114
|
-
transientCode === 'DEADLINE_EXCEEDED' ||
|
|
115
|
-
transientCode === 'INTERNAL' ||
|
|
116
|
-
transientCode === 'ABORTED') {
|
|
149
|
+
if (transientCode !== undefined &&
|
|
150
|
+
RETRYABLE_TRANSIENT_CODES.has(transientCode)) {
|
|
117
151
|
return true;
|
|
118
152
|
}
|
|
119
153
|
const message = getErrorMessage(error);
|
|
@@ -126,17 +160,6 @@ function getRetryDelayMs(attempt) {
|
|
|
126
160
|
const jitter = randomInt(0, jitterWindow);
|
|
127
161
|
return Math.min(RETRY_DELAY_MAX_MS, boundedDelay + jitter);
|
|
128
162
|
}
|
|
129
|
-
function getSafetyThreshold() {
|
|
130
|
-
const threshold = process.env.GEMINI_HARM_BLOCK_THRESHOLD;
|
|
131
|
-
if (!threshold) {
|
|
132
|
-
return DEFAULT_SAFETY_THRESHOLD;
|
|
133
|
-
}
|
|
134
|
-
const normalizedThreshold = threshold.trim().toUpperCase();
|
|
135
|
-
if (normalizedThreshold in SAFETY_THRESHOLD_BY_NAME) {
|
|
136
|
-
return SAFETY_THRESHOLD_BY_NAME[normalizedThreshold];
|
|
137
|
-
}
|
|
138
|
-
return DEFAULT_SAFETY_THRESHOLD;
|
|
139
|
-
}
|
|
140
163
|
function buildGenerationConfig(request, abortSignal) {
|
|
141
164
|
const safetyThreshold = getSafetyThreshold();
|
|
142
165
|
return {
|
|
@@ -144,9 +167,10 @@ function buildGenerationConfig(request, abortSignal) {
|
|
|
144
167
|
maxOutputTokens: request.maxOutputTokens ?? DEFAULT_MAX_OUTPUT_TOKENS,
|
|
145
168
|
responseMimeType: 'application/json',
|
|
146
169
|
responseSchema: request.responseSchema,
|
|
170
|
+
// Spread undefined instead of {} so no intermediate object is allocated when absent.
|
|
147
171
|
...(request.systemInstruction
|
|
148
172
|
? { systemInstruction: request.systemInstruction }
|
|
149
|
-
:
|
|
173
|
+
: undefined),
|
|
150
174
|
safetySettings: [
|
|
151
175
|
{
|
|
152
176
|
category: HarmCategory.HARM_CATEGORY_HATE_SPEECH,
|
|
@@ -168,15 +192,16 @@ function buildGenerationConfig(request, abortSignal) {
|
|
|
168
192
|
abortSignal,
|
|
169
193
|
};
|
|
170
194
|
}
|
|
195
|
+
function combineSignals(signal, requestSignal) {
|
|
196
|
+
return requestSignal ? AbortSignal.any([signal, requestSignal]) : signal;
|
|
197
|
+
}
|
|
171
198
|
async function generateContentWithTimeout(request, model, timeoutMs) {
|
|
172
199
|
const controller = new AbortController();
|
|
173
200
|
const timeout = setTimeout(() => {
|
|
174
201
|
controller.abort();
|
|
175
202
|
}, timeoutMs);
|
|
176
203
|
timeout.unref();
|
|
177
|
-
const signal = request.signal
|
|
178
|
-
? AbortSignal.any([controller.signal, request.signal])
|
|
179
|
-
: controller.signal;
|
|
204
|
+
const signal = combineSignals(controller.signal, request.signal);
|
|
180
205
|
try {
|
|
181
206
|
return await getClient().models.generateContent({
|
|
182
207
|
model,
|
|
@@ -189,7 +214,7 @@ async function generateContentWithTimeout(request, model, timeoutMs) {
|
|
|
189
214
|
throw new Error('Gemini request was cancelled.');
|
|
190
215
|
}
|
|
191
216
|
if (controller.signal.aborted) {
|
|
192
|
-
throw new Error(`Gemini request timed out after ${formatNumber(timeoutMs)}ms
|
|
217
|
+
throw new Error(`Gemini request timed out after ${formatNumber(timeoutMs)}ms.`, { cause: error });
|
|
193
218
|
}
|
|
194
219
|
throw error;
|
|
195
220
|
}
|
|
@@ -201,15 +226,23 @@ export async function generateStructuredJson(request) {
|
|
|
201
226
|
const model = request.model ?? getDefaultModel();
|
|
202
227
|
const timeoutMs = request.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
203
228
|
const maxRetries = request.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
229
|
+
const { onLog } = request;
|
|
204
230
|
return geminiContext.run({ requestId: nextRequestId(), model }, async () => {
|
|
205
231
|
let lastError;
|
|
206
232
|
for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
|
|
207
233
|
const startedAt = performance.now();
|
|
208
234
|
try {
|
|
209
235
|
const response = await generateContentWithTimeout(request, model, timeoutMs);
|
|
236
|
+
const latencyMs = Math.round(performance.now() - startedAt);
|
|
210
237
|
logEvent('gemini_call', {
|
|
211
238
|
attempt,
|
|
212
|
-
latencyMs
|
|
239
|
+
latencyMs,
|
|
240
|
+
usageMetadata: response.usageMetadata ?? null,
|
|
241
|
+
});
|
|
242
|
+
await safeCallOnLog(onLog, 'info', {
|
|
243
|
+
event: 'gemini_call',
|
|
244
|
+
attempt,
|
|
245
|
+
latencyMs,
|
|
213
246
|
usageMetadata: response.usageMetadata ?? null,
|
|
214
247
|
});
|
|
215
248
|
if (!response.text) {
|
|
@@ -236,6 +269,12 @@ export async function generateStructuredJson(request) {
|
|
|
236
269
|
delayMs,
|
|
237
270
|
reason: getErrorMessage(error),
|
|
238
271
|
});
|
|
272
|
+
await safeCallOnLog(onLog, 'warning', {
|
|
273
|
+
event: 'gemini_retry',
|
|
274
|
+
attempt,
|
|
275
|
+
delayMs,
|
|
276
|
+
reason: getErrorMessage(error),
|
|
277
|
+
});
|
|
239
278
|
await sleep(delayMs, undefined, { ref: false });
|
|
240
279
|
}
|
|
241
280
|
}
|
|
@@ -243,6 +282,11 @@ export async function generateStructuredJson(request) {
|
|
|
243
282
|
error: getErrorMessage(lastError),
|
|
244
283
|
attempts: maxRetries + 1,
|
|
245
284
|
});
|
|
246
|
-
|
|
285
|
+
await safeCallOnLog(onLog, 'error', {
|
|
286
|
+
event: 'gemini_failure',
|
|
287
|
+
error: getErrorMessage(lastError),
|
|
288
|
+
attempts: maxRetries + 1,
|
|
289
|
+
});
|
|
290
|
+
throw new Error(`Gemini request failed after ${maxRetries + 1} attempts: ${getErrorMessage(lastError)}`, { cause: lastError });
|
|
247
291
|
});
|
|
248
292
|
}
|
|
@@ -13,10 +13,10 @@ export interface StructuredToolTaskConfig<TInput extends object = Record<string,
|
|
|
13
13
|
title: string;
|
|
14
14
|
/** Short description of the tool's purpose. */
|
|
15
15
|
description: string;
|
|
16
|
-
/** Zod shape
|
|
16
|
+
/** Zod schema shape for the tool input. Used by the MCP SDK to strip unknown fields before the handler runs. */
|
|
17
17
|
inputSchema: ZodRawShapeCompat;
|
|
18
|
-
/**
|
|
19
|
-
fullInputSchema
|
|
18
|
+
/** Zod schema for validating the complete tool input, including all expected fields. This is used within the handler to validate the actual input shape after the MCP SDK has stripped unknown fields. */
|
|
19
|
+
fullInputSchema: z.ZodType<TInput>;
|
|
20
20
|
/** Zod schema for parsing and validating the Gemini structured response. */
|
|
21
21
|
resultSchema: z.ZodType;
|
|
22
22
|
/** Optional Zod schema used specifically for Gemini response validation. */
|
package/dist/lib/tool-factory.js
CHANGED
|
@@ -4,10 +4,18 @@ import { getErrorMessage } from './errors.js';
|
|
|
4
4
|
import { stripJsonSchemaConstraints } from './gemini-schema.js';
|
|
5
5
|
import { generateStructuredJson } from './gemini.js';
|
|
6
6
|
import { createErrorToolResponse, createToolResponse, } from './tool-response.js';
|
|
7
|
+
function createGeminiResponseSchema(config) {
|
|
8
|
+
const sourceSchema = config.geminiSchema ?? config.resultSchema;
|
|
9
|
+
return stripJsonSchemaConstraints(z.toJSONSchema(sourceSchema));
|
|
10
|
+
}
|
|
11
|
+
function parseToolInput(input, fullInputSchema) {
|
|
12
|
+
return fullInputSchema.parse(input);
|
|
13
|
+
}
|
|
7
14
|
export function registerStructuredToolTask(server, config) {
|
|
8
|
-
const responseSchema =
|
|
9
|
-
|
|
10
|
-
:
|
|
15
|
+
const responseSchema = createGeminiResponseSchema({
|
|
16
|
+
geminiSchema: config.geminiSchema,
|
|
17
|
+
resultSchema: config.resultSchema,
|
|
18
|
+
});
|
|
11
19
|
server.experimental.tasks.registerToolTask(config.name, {
|
|
12
20
|
title: config.title,
|
|
13
21
|
description: config.description,
|
|
@@ -39,21 +47,34 @@ export function registerStructuredToolTask(server, config) {
|
|
|
39
47
|
// Progress is best-effort; never fail the tool call.
|
|
40
48
|
}
|
|
41
49
|
};
|
|
50
|
+
const updateStatusMessage = async (message) => {
|
|
51
|
+
try {
|
|
52
|
+
await extra.taskStore.updateTaskStatus(task.taskId, 'working', message);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// statusMessage is best-effort; task may already be terminal.
|
|
56
|
+
}
|
|
57
|
+
};
|
|
42
58
|
try {
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
59
|
+
const onLog = async (level, data) => {
|
|
60
|
+
try {
|
|
61
|
+
await server.sendLoggingMessage({
|
|
62
|
+
level: level,
|
|
63
|
+
logger: 'gemini',
|
|
64
|
+
data,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// Logging is best-effort; never fail the tool call.
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
const inputRecord = parseToolInput(input, config.fullInputSchema);
|
|
46
72
|
if (config.validateInput) {
|
|
47
73
|
const validationError = await config.validateInput(inputRecord);
|
|
48
74
|
if (validationError) {
|
|
49
75
|
const validationMessage = validationError.structuredContent.error?.message ??
|
|
50
76
|
'Input validation failed';
|
|
51
|
-
|
|
52
|
-
await extra.taskStore.updateTaskStatus(task.taskId, 'working', validationMessage);
|
|
53
|
-
}
|
|
54
|
-
catch {
|
|
55
|
-
// statusMessage is best-effort; task may already be terminal.
|
|
56
|
-
}
|
|
77
|
+
await updateStatusMessage(validationMessage);
|
|
57
78
|
await extra.taskStore.storeTaskResult(task.taskId, 'failed', validationError);
|
|
58
79
|
return { task };
|
|
59
80
|
}
|
|
@@ -66,6 +87,7 @@ export function registerStructuredToolTask(server, config) {
|
|
|
66
87
|
prompt,
|
|
67
88
|
responseSchema,
|
|
68
89
|
signal: extra.signal,
|
|
90
|
+
onLog,
|
|
69
91
|
});
|
|
70
92
|
await sendProgress(3, 4);
|
|
71
93
|
const parsed = config.resultSchema.parse(raw);
|
|
@@ -77,12 +99,7 @@ export function registerStructuredToolTask(server, config) {
|
|
|
77
99
|
}
|
|
78
100
|
catch (error) {
|
|
79
101
|
const errorMessage = getErrorMessage(error);
|
|
80
|
-
|
|
81
|
-
await extra.taskStore.updateTaskStatus(task.taskId, 'working', errorMessage);
|
|
82
|
-
}
|
|
83
|
-
catch {
|
|
84
|
-
// statusMessage is best-effort; task may already be terminal.
|
|
85
|
-
}
|
|
102
|
+
await updateStatusMessage(errorMessage);
|
|
86
103
|
await extra.taskStore.storeTaskResult(task.taskId, 'failed', createErrorToolResponse(config.errorCode, errorMessage));
|
|
87
104
|
}
|
|
88
105
|
return { task };
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
function toTextContent(structured) {
|
|
2
2
|
return [{ type: 'text', text: JSON.stringify(structured) }];
|
|
3
3
|
}
|
|
4
|
+
function createErrorStructuredContent(code, message, result) {
|
|
5
|
+
if (result === undefined) {
|
|
6
|
+
return { ok: false, error: { code, message } };
|
|
7
|
+
}
|
|
8
|
+
return { ok: false, error: { code, message }, result };
|
|
9
|
+
}
|
|
4
10
|
export function createToolResponse(structured) {
|
|
5
11
|
return {
|
|
6
12
|
content: toTextContent(structured),
|
|
@@ -8,11 +14,7 @@ export function createToolResponse(structured) {
|
|
|
8
14
|
};
|
|
9
15
|
}
|
|
10
16
|
export function createErrorToolResponse(code, message, result) {
|
|
11
|
-
const structured =
|
|
12
|
-
ok: false,
|
|
13
|
-
error: { code, message },
|
|
14
|
-
...(result === undefined ? {} : { result }),
|
|
15
|
-
};
|
|
17
|
+
const structured = createErrorStructuredContent(code, message, result);
|
|
16
18
|
return {
|
|
17
19
|
content: toTextContent(structured),
|
|
18
20
|
structuredContent: structured,
|
package/dist/lib/types.d.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
export type JsonObject = Record<string, unknown>;
|
|
2
|
-
interface GeminiStructuredRequestOptions {
|
|
2
|
+
export interface GeminiStructuredRequestOptions {
|
|
3
3
|
model?: string;
|
|
4
4
|
maxRetries?: number;
|
|
5
5
|
timeoutMs?: number;
|
|
6
6
|
temperature?: number;
|
|
7
7
|
maxOutputTokens?: number;
|
|
8
8
|
signal?: AbortSignal;
|
|
9
|
+
onLog?: (level: string, data: unknown) => Promise<void>;
|
|
9
10
|
}
|
|
10
11
|
export interface GeminiStructuredRequest extends GeminiStructuredRequestOptions {
|
|
11
12
|
systemInstruction?: string;
|
|
12
13
|
prompt: string;
|
|
13
14
|
responseSchema: JsonObject;
|
|
14
15
|
}
|
|
15
|
-
export {};
|
package/dist/prompts/index.js
CHANGED
|
@@ -1,9 +1,51 @@
|
|
|
1
|
+
import { completable } from '@modelcontextprotocol/sdk/server/completable.js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
const HELP_PROMPT_NAME = 'get-help';
|
|
4
|
+
const HELP_PROMPT_DESCRIPTION = 'Return the server usage instructions.';
|
|
5
|
+
const REVIEW_GUIDE_PROMPT_NAME = 'review-guide';
|
|
6
|
+
const REVIEW_GUIDE_PROMPT_DESCRIPTION = 'Guided workflow instructions for a specific code review tool and focus area.';
|
|
7
|
+
const TOOLS = ['review_diff', 'risk_score', 'suggest_patch'];
|
|
8
|
+
const FOCUS_AREAS = [
|
|
9
|
+
'security',
|
|
10
|
+
'correctness',
|
|
11
|
+
'performance',
|
|
12
|
+
'regressions',
|
|
13
|
+
'tests',
|
|
14
|
+
];
|
|
15
|
+
const TOOL_GUIDES = {
|
|
16
|
+
review_diff: 'Call `review_diff` with `diff` (unified diff text) and `repository` (org/repo). ' +
|
|
17
|
+
'Optional: `focusAreas` array and `maxFindings` cap. ' +
|
|
18
|
+
'Returns structured findings, overallRisk, and test recommendations.',
|
|
19
|
+
risk_score: 'Call `risk_score` with `diff`. Optional: `deploymentCriticality` (low, medium, high). ' +
|
|
20
|
+
'Returns a 0–100 score, bucket, and rationale for release gating.',
|
|
21
|
+
suggest_patch: 'First call `review_diff` to get findings. Then call `suggest_patch` with `diff`, ' +
|
|
22
|
+
'`findingTitle`, and `findingDetails` from one finding. ' +
|
|
23
|
+
'Optional: `patchStyle` (minimal, balanced, defensive). One finding per call.',
|
|
24
|
+
};
|
|
25
|
+
const FOCUS_AREA_GUIDES = {
|
|
26
|
+
security: 'Audit for injection vulnerabilities, insecure data handling, broken authentication, ' +
|
|
27
|
+
'cryptographic failures, and OWASP Top 10 issues.',
|
|
28
|
+
correctness: 'Check for logic errors, edge case mishandling, incorrect algorithm implementations, ' +
|
|
29
|
+
'and API contract violations.',
|
|
30
|
+
performance: 'Identify algorithmic complexity issues, unnecessary allocations, blocking I/O, ' +
|
|
31
|
+
'and database query inefficiencies.',
|
|
32
|
+
regressions: 'Look for changes that could break existing behavior, removed guards, altered return types, ' +
|
|
33
|
+
'or contract changes in public APIs.',
|
|
34
|
+
tests: 'Assess test coverage gaps, missing edge case tests, flaky test patterns, ' +
|
|
35
|
+
'and untested error paths.',
|
|
36
|
+
};
|
|
37
|
+
function getToolGuide(tool) {
|
|
38
|
+
return TOOL_GUIDES[tool] ?? `Use \`${tool}\` to analyze your code changes.`;
|
|
39
|
+
}
|
|
40
|
+
function getFocusAreaGuide(focusArea) {
|
|
41
|
+
return FOCUS_AREA_GUIDES[focusArea] ?? `Focus on ${focusArea} concerns.`;
|
|
42
|
+
}
|
|
1
43
|
export function registerAllPrompts(server, instructions) {
|
|
2
|
-
server.registerPrompt(
|
|
44
|
+
server.registerPrompt(HELP_PROMPT_NAME, {
|
|
3
45
|
title: 'Get Help',
|
|
4
46
|
description: 'Return the server usage instructions.',
|
|
5
47
|
}, () => ({
|
|
6
|
-
description:
|
|
48
|
+
description: HELP_PROMPT_DESCRIPTION,
|
|
7
49
|
messages: [
|
|
8
50
|
{
|
|
9
51
|
role: 'user',
|
|
@@ -14,4 +56,30 @@ export function registerAllPrompts(server, instructions) {
|
|
|
14
56
|
},
|
|
15
57
|
],
|
|
16
58
|
}));
|
|
59
|
+
server.registerPrompt(REVIEW_GUIDE_PROMPT_NAME, {
|
|
60
|
+
title: 'Review Guide',
|
|
61
|
+
description: REVIEW_GUIDE_PROMPT_DESCRIPTION,
|
|
62
|
+
argsSchema: {
|
|
63
|
+
tool: completable(z
|
|
64
|
+
.string()
|
|
65
|
+
.describe('Which review tool to use: review_diff, risk_score, or suggest_patch'), (value) => TOOLS.filter((t) => t.startsWith(value))),
|
|
66
|
+
focusArea: completable(z
|
|
67
|
+
.string()
|
|
68
|
+
.describe('Focus area: security, correctness, performance, regressions, or tests'), (value) => FOCUS_AREAS.filter((f) => f.startsWith(value))),
|
|
69
|
+
},
|
|
70
|
+
}, ({ tool, focusArea }) => ({
|
|
71
|
+
description: `Code review guide: ${tool} / ${focusArea}`,
|
|
72
|
+
messages: [
|
|
73
|
+
{
|
|
74
|
+
role: 'user',
|
|
75
|
+
content: {
|
|
76
|
+
type: 'text',
|
|
77
|
+
text: `# Code Review Guide\n\n` +
|
|
78
|
+
`## Tool: \`${tool}\`\n${getToolGuide(tool)}\n\n` +
|
|
79
|
+
`## Focus Area: ${focusArea}\n${getFocusAreaGuide(focusArea)}\n\n` +
|
|
80
|
+
`> Tip: Run \`get-help\` for full server documentation.`,
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
}));
|
|
17
85
|
}
|
package/dist/resources/index.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
const RESOURCE_ID = 'server-instructions';
|
|
2
|
+
const RESOURCE_URI = 'internal://instructions';
|
|
3
|
+
const RESOURCE_MIME_TYPE = 'text/markdown';
|
|
1
4
|
export function registerAllResources(server, instructions) {
|
|
2
|
-
server.registerResource(
|
|
5
|
+
server.registerResource(RESOURCE_ID, RESOURCE_URI, {
|
|
3
6
|
title: 'Server Instructions',
|
|
4
7
|
description: 'Guidance for using the MCP tools effectively.',
|
|
5
|
-
mimeType:
|
|
8
|
+
mimeType: RESOURCE_MIME_TYPE,
|
|
6
9
|
annotations: {
|
|
7
10
|
audience: ['assistant'],
|
|
8
11
|
priority: 0.8,
|
|
@@ -11,7 +14,7 @@ export function registerAllResources(server, instructions) {
|
|
|
11
14
|
contents: [
|
|
12
15
|
{
|
|
13
16
|
uri: uri.href,
|
|
14
|
-
mimeType:
|
|
17
|
+
mimeType: RESOURCE_MIME_TYPE,
|
|
15
18
|
text: instructions,
|
|
16
19
|
},
|
|
17
20
|
],
|
package/dist/schemas/inputs.js
CHANGED
|
@@ -8,32 +8,18 @@ const INPUT_LIMITS = {
|
|
|
8
8
|
findingTitle: { min: 3, max: 160 },
|
|
9
9
|
findingDetails: { min: 10, max: 3_000 },
|
|
10
10
|
};
|
|
11
|
+
function createBoundedString(min, max, description) {
|
|
12
|
+
return z.string().min(min).max(max).describe(description);
|
|
13
|
+
}
|
|
11
14
|
function createDiffSchema(description) {
|
|
12
|
-
return
|
|
13
|
-
.string()
|
|
14
|
-
.min(INPUT_LIMITS.diff.min)
|
|
15
|
-
.max(INPUT_LIMITS.diff.max)
|
|
16
|
-
.describe(description);
|
|
15
|
+
return createBoundedString(INPUT_LIMITS.diff.min, INPUT_LIMITS.diff.max, description);
|
|
17
16
|
}
|
|
18
17
|
export const ReviewDiffInputSchema = z.strictObject({
|
|
19
18
|
diff: createDiffSchema('Unified diff text for one PR or commit.'),
|
|
20
|
-
repository:
|
|
21
|
-
|
|
22
|
-
.min(INPUT_LIMITS.repository.min)
|
|
23
|
-
.max(INPUT_LIMITS.repository.max)
|
|
24
|
-
.describe('Repository identifier, for example org/repo.'),
|
|
25
|
-
language: z
|
|
26
|
-
.string()
|
|
27
|
-
.min(INPUT_LIMITS.language.min)
|
|
28
|
-
.max(INPUT_LIMITS.language.max)
|
|
29
|
-
.optional()
|
|
30
|
-
.describe('Primary implementation language to bias review depth.'),
|
|
19
|
+
repository: createBoundedString(INPUT_LIMITS.repository.min, INPUT_LIMITS.repository.max, 'Repository identifier, for example org/repo.'),
|
|
20
|
+
language: createBoundedString(INPUT_LIMITS.language.min, INPUT_LIMITS.language.max, 'Primary implementation language to bias review depth.').optional(),
|
|
31
21
|
focusAreas: z
|
|
32
|
-
.array(
|
|
33
|
-
.string()
|
|
34
|
-
.min(INPUT_LIMITS.focusArea.min)
|
|
35
|
-
.max(INPUT_LIMITS.focusArea.max)
|
|
36
|
-
.describe('Specific area to inspect, for example security or tests.'))
|
|
22
|
+
.array(createBoundedString(INPUT_LIMITS.focusArea.min, INPUT_LIMITS.focusArea.max, 'Specific area to inspect, for example security or tests.'))
|
|
37
23
|
.min(1)
|
|
38
24
|
.max(INPUT_LIMITS.focusArea.maxItems)
|
|
39
25
|
.optional()
|
|
@@ -55,16 +41,8 @@ export const RiskScoreInputSchema = z.strictObject({
|
|
|
55
41
|
});
|
|
56
42
|
export const SuggestPatchInputSchema = z.strictObject({
|
|
57
43
|
diff: createDiffSchema('Unified diff text that contains the issue to patch.'),
|
|
58
|
-
findingTitle:
|
|
59
|
-
|
|
60
|
-
.min(INPUT_LIMITS.findingTitle.min)
|
|
61
|
-
.max(INPUT_LIMITS.findingTitle.max)
|
|
62
|
-
.describe('Short title of the finding that needs a patch.'),
|
|
63
|
-
findingDetails: z
|
|
64
|
-
.string()
|
|
65
|
-
.min(INPUT_LIMITS.findingDetails.min)
|
|
66
|
-
.max(INPUT_LIMITS.findingDetails.max)
|
|
67
|
-
.describe('Detailed explanation of the bug or risk.'),
|
|
44
|
+
findingTitle: createBoundedString(INPUT_LIMITS.findingTitle.min, INPUT_LIMITS.findingTitle.max, 'Short title of the finding that needs a patch.'),
|
|
45
|
+
findingDetails: createBoundedString(INPUT_LIMITS.findingDetails.min, INPUT_LIMITS.findingDetails.max, 'Detailed explanation of the bug or risk.'),
|
|
68
46
|
patchStyle: z
|
|
69
47
|
.enum(['minimal', 'balanced', 'defensive'])
|
|
70
48
|
.optional()
|
package/dist/schemas/outputs.js
CHANGED
|
@@ -22,6 +22,9 @@ const OUTPUT_LIMITS = {
|
|
|
22
22
|
checklist: { minItems: 1, maxItems: 12, itemMin: 6, itemMax: 300 },
|
|
23
23
|
},
|
|
24
24
|
};
|
|
25
|
+
function createBoundedString(min, max, description) {
|
|
26
|
+
return z.string().min(min).max(max).describe(description);
|
|
27
|
+
}
|
|
25
28
|
export const DefaultOutputSchema = z.strictObject({
|
|
26
29
|
ok: z.boolean().describe('Whether the tool completed successfully.'),
|
|
27
30
|
result: z.unknown().optional().describe('Successful result payload.'),
|
|
@@ -49,28 +52,12 @@ export const ReviewFindingSchema = z.strictObject({
|
|
|
49
52
|
.max(OUTPUT_LIMITS.reviewFinding.lineMax)
|
|
50
53
|
.nullable()
|
|
51
54
|
.describe('1-based line number when known, otherwise null.'),
|
|
52
|
-
title:
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
.max(OUTPUT_LIMITS.reviewFinding.title.max)
|
|
56
|
-
.describe('Short finding title.'),
|
|
57
|
-
explanation: z
|
|
58
|
-
.string()
|
|
59
|
-
.min(OUTPUT_LIMITS.reviewFinding.text.min)
|
|
60
|
-
.max(OUTPUT_LIMITS.reviewFinding.text.max)
|
|
61
|
-
.describe('Why this issue matters.'),
|
|
62
|
-
recommendation: z
|
|
63
|
-
.string()
|
|
64
|
-
.min(OUTPUT_LIMITS.reviewFinding.text.min)
|
|
65
|
-
.max(OUTPUT_LIMITS.reviewFinding.text.max)
|
|
66
|
-
.describe('Concrete fix recommendation.'),
|
|
55
|
+
title: createBoundedString(OUTPUT_LIMITS.reviewFinding.title.min, OUTPUT_LIMITS.reviewFinding.title.max, 'Short finding title.'),
|
|
56
|
+
explanation: createBoundedString(OUTPUT_LIMITS.reviewFinding.text.min, OUTPUT_LIMITS.reviewFinding.text.max, 'Why this issue matters.'),
|
|
57
|
+
recommendation: createBoundedString(OUTPUT_LIMITS.reviewFinding.text.min, OUTPUT_LIMITS.reviewFinding.text.max, 'Concrete fix recommendation.'),
|
|
67
58
|
});
|
|
68
59
|
export const ReviewDiffResultSchema = z.strictObject({
|
|
69
|
-
summary:
|
|
70
|
-
.string()
|
|
71
|
-
.min(OUTPUT_LIMITS.reviewDiffResult.summary.min)
|
|
72
|
-
.max(OUTPUT_LIMITS.reviewDiffResult.summary.max)
|
|
73
|
-
.describe('Short review summary.'),
|
|
60
|
+
summary: createBoundedString(OUTPUT_LIMITS.reviewDiffResult.summary.min, OUTPUT_LIMITS.reviewDiffResult.summary.max, 'Short review summary.'),
|
|
74
61
|
overallRisk: z
|
|
75
62
|
.enum(['low', 'medium', 'high'])
|
|
76
63
|
.describe('Overall risk for merging this diff.'),
|
|
@@ -80,11 +67,7 @@ export const ReviewDiffResultSchema = z.strictObject({
|
|
|
80
67
|
.max(OUTPUT_LIMITS.reviewDiffResult.findingsMax)
|
|
81
68
|
.describe('Ordered list of findings, highest severity first.'),
|
|
82
69
|
testsNeeded: z
|
|
83
|
-
.array(
|
|
84
|
-
.string()
|
|
85
|
-
.min(OUTPUT_LIMITS.reviewDiffResult.testsNeeded.itemMin)
|
|
86
|
-
.max(OUTPUT_LIMITS.reviewDiffResult.testsNeeded.itemMax)
|
|
87
|
-
.describe('Test recommendation to reduce risk.'))
|
|
70
|
+
.array(createBoundedString(OUTPUT_LIMITS.reviewDiffResult.testsNeeded.itemMin, OUTPUT_LIMITS.reviewDiffResult.testsNeeded.itemMax, 'Test recommendation to reduce risk.'))
|
|
88
71
|
.min(OUTPUT_LIMITS.reviewDiffResult.testsNeeded.minItems)
|
|
89
72
|
.max(OUTPUT_LIMITS.reviewDiffResult.testsNeeded.maxItems)
|
|
90
73
|
.describe('Targeted tests to add before merge.'),
|
|
@@ -100,32 +83,16 @@ export const RiskScoreResultSchema = z.strictObject({
|
|
|
100
83
|
.enum(['low', 'medium', 'high', 'critical'])
|
|
101
84
|
.describe('Risk bucket derived from score and criticality.'),
|
|
102
85
|
rationale: z
|
|
103
|
-
.array(
|
|
104
|
-
.string()
|
|
105
|
-
.min(OUTPUT_LIMITS.riskScoreResult.rationale.itemMin)
|
|
106
|
-
.max(OUTPUT_LIMITS.riskScoreResult.rationale.itemMax)
|
|
107
|
-
.describe('Reason that influenced the final score.'))
|
|
86
|
+
.array(createBoundedString(OUTPUT_LIMITS.riskScoreResult.rationale.itemMin, OUTPUT_LIMITS.riskScoreResult.rationale.itemMax, 'Reason that influenced the final score.'))
|
|
108
87
|
.min(OUTPUT_LIMITS.riskScoreResult.rationale.minItems)
|
|
109
88
|
.max(OUTPUT_LIMITS.riskScoreResult.rationale.maxItems)
|
|
110
89
|
.describe('Evidence-based explanation for the score.'),
|
|
111
90
|
});
|
|
112
91
|
export const PatchSuggestionResultSchema = z.strictObject({
|
|
113
|
-
summary:
|
|
114
|
-
|
|
115
|
-
.min(OUTPUT_LIMITS.patchSuggestionResult.summary.min)
|
|
116
|
-
.max(OUTPUT_LIMITS.patchSuggestionResult.summary.max)
|
|
117
|
-
.describe('Short patch strategy summary.'),
|
|
118
|
-
patch: z
|
|
119
|
-
.string()
|
|
120
|
-
.min(OUTPUT_LIMITS.patchSuggestionResult.patch.min)
|
|
121
|
-
.max(OUTPUT_LIMITS.patchSuggestionResult.patch.max)
|
|
122
|
-
.describe('Unified diff patch text.'),
|
|
92
|
+
summary: createBoundedString(OUTPUT_LIMITS.patchSuggestionResult.summary.min, OUTPUT_LIMITS.patchSuggestionResult.summary.max, 'Short patch strategy summary.'),
|
|
93
|
+
patch: createBoundedString(OUTPUT_LIMITS.patchSuggestionResult.patch.min, OUTPUT_LIMITS.patchSuggestionResult.patch.max, 'Unified diff patch text.'),
|
|
123
94
|
validationChecklist: z
|
|
124
|
-
.array(
|
|
125
|
-
.string()
|
|
126
|
-
.min(OUTPUT_LIMITS.patchSuggestionResult.checklist.itemMin)
|
|
127
|
-
.max(OUTPUT_LIMITS.patchSuggestionResult.checklist.itemMax)
|
|
128
|
-
.describe('Validation step after applying patch.'))
|
|
95
|
+
.array(createBoundedString(OUTPUT_LIMITS.patchSuggestionResult.checklist.itemMin, OUTPUT_LIMITS.patchSuggestionResult.checklist.itemMax, 'Validation step after applying patch.'))
|
|
129
96
|
.min(OUTPUT_LIMITS.patchSuggestionResult.checklist.minItems)
|
|
130
97
|
.max(OUTPUT_LIMITS.patchSuggestionResult.checklist.maxItems)
|
|
131
98
|
.describe('Post-change validation actions.'),
|
package/dist/server.js
CHANGED
|
@@ -8,6 +8,9 @@ import { getErrorMessage } from './lib/errors.js';
|
|
|
8
8
|
import { registerAllPrompts } from './prompts/index.js';
|
|
9
9
|
import { registerAllResources } from './resources/index.js';
|
|
10
10
|
import { registerAllTools } from './tools/index.js';
|
|
11
|
+
const SERVER_NAME = 'code-review-analyst';
|
|
12
|
+
const INSTRUCTIONS_FILENAME = 'instructions.md';
|
|
13
|
+
const INSTRUCTIONS_FALLBACK = '(Instructions failed to load)';
|
|
11
14
|
function isPackageJsonMetadata(value) {
|
|
12
15
|
return (typeof value === 'object' &&
|
|
13
16
|
value !== null &&
|
|
@@ -28,9 +31,6 @@ function parsePackageJson(packageJson, packageJsonPath) {
|
|
|
28
31
|
}
|
|
29
32
|
return parsed;
|
|
30
33
|
}
|
|
31
|
-
function extractVersion(packageJson, packageJsonPath) {
|
|
32
|
-
return parsePackageJson(packageJson, packageJsonPath).version;
|
|
33
|
-
}
|
|
34
34
|
function readPackageJson(packageJsonPath) {
|
|
35
35
|
try {
|
|
36
36
|
return readFileSync(packageJsonPath, 'utf8');
|
|
@@ -42,31 +42,35 @@ function readPackageJson(packageJsonPath) {
|
|
|
42
42
|
function loadVersion() {
|
|
43
43
|
const packageJsonPath = findPackageJSON(import.meta.url);
|
|
44
44
|
if (!packageJsonPath) {
|
|
45
|
-
throw new Error(
|
|
45
|
+
throw new Error(`Unable to locate package.json for ${SERVER_NAME}.`);
|
|
46
46
|
}
|
|
47
|
-
return
|
|
47
|
+
return parsePackageJson(readPackageJson(packageJsonPath), packageJsonPath)
|
|
48
|
+
.version;
|
|
48
49
|
}
|
|
49
50
|
const SERVER_VERSION = loadVersion();
|
|
50
51
|
function loadInstructions() {
|
|
51
52
|
const currentDir = dirname(fileURLToPath(import.meta.url));
|
|
53
|
+
const instructionsPath = join(currentDir, INSTRUCTIONS_FILENAME);
|
|
52
54
|
try {
|
|
53
|
-
return readFileSync(
|
|
55
|
+
return readFileSync(instructionsPath, 'utf8');
|
|
54
56
|
}
|
|
55
57
|
catch (error) {
|
|
56
|
-
process.emitWarning(`Failed to load
|
|
57
|
-
return
|
|
58
|
+
process.emitWarning(`Failed to load ${INSTRUCTIONS_FILENAME}: ${getErrorMessage(error)}`);
|
|
59
|
+
return INSTRUCTIONS_FALLBACK;
|
|
58
60
|
}
|
|
59
61
|
}
|
|
60
62
|
const SERVER_INSTRUCTIONS = loadInstructions();
|
|
61
63
|
const SERVER_TASK_STORE = new InMemoryTaskStore();
|
|
62
64
|
export function createServer() {
|
|
63
65
|
const server = new McpServer({
|
|
64
|
-
name:
|
|
66
|
+
name: SERVER_NAME,
|
|
65
67
|
version: SERVER_VERSION,
|
|
66
68
|
}, {
|
|
67
69
|
instructions: SERVER_INSTRUCTIONS,
|
|
68
70
|
taskStore: SERVER_TASK_STORE,
|
|
69
71
|
capabilities: {
|
|
72
|
+
logging: {},
|
|
73
|
+
completions: {},
|
|
70
74
|
tasks: {
|
|
71
75
|
list: {},
|
|
72
76
|
cancel: {},
|
|
@@ -4,16 +4,17 @@ import { ReviewDiffInputSchema } from '../schemas/inputs.js';
|
|
|
4
4
|
import { ReviewDiffResultSchema } from '../schemas/outputs.js';
|
|
5
5
|
const DEFAULT_MAX_FINDINGS = 10;
|
|
6
6
|
const DEFAULT_FOCUS_AREAS = 'security, correctness, regressions, performance';
|
|
7
|
+
// Hoisted: avoids array allocation + join on every request.
|
|
8
|
+
const SYSTEM_INSTRUCTION = 'You are a senior staff engineer performing pull request review.\nReturn strict JSON only with no markdown fences.';
|
|
9
|
+
function joinPromptLines(lines) {
|
|
10
|
+
return lines.join('\n');
|
|
11
|
+
}
|
|
7
12
|
function buildReviewPrompt(input) {
|
|
8
13
|
const focus = input.focusAreas?.length
|
|
9
14
|
? input.focusAreas.join(', ')
|
|
10
15
|
: DEFAULT_FOCUS_AREAS;
|
|
11
16
|
const maxFindings = input.maxFindings ?? DEFAULT_MAX_FINDINGS;
|
|
12
|
-
const
|
|
13
|
-
'You are a senior staff engineer performing pull request review.',
|
|
14
|
-
'Return strict JSON only with no markdown fences.',
|
|
15
|
-
].join('\n');
|
|
16
|
-
const prompt = [
|
|
17
|
+
const prompt = joinPromptLines([
|
|
17
18
|
`Repository: ${input.repository}`,
|
|
18
19
|
`Primary language: ${input.language ?? 'not specified'}`,
|
|
19
20
|
`Focus areas: ${focus}`,
|
|
@@ -23,8 +24,8 @@ function buildReviewPrompt(input) {
|
|
|
23
24
|
'',
|
|
24
25
|
'Unified diff:',
|
|
25
26
|
input.diff,
|
|
26
|
-
]
|
|
27
|
-
return { systemInstruction, prompt };
|
|
27
|
+
]);
|
|
28
|
+
return { systemInstruction: SYSTEM_INSTRUCTION, prompt };
|
|
28
29
|
}
|
|
29
30
|
export function registerReviewDiffTool(server) {
|
|
30
31
|
registerStructuredToolTask(server, {
|
package/dist/tools/risk-score.js
CHANGED
|
@@ -3,20 +3,21 @@ import { registerStructuredToolTask, } from '../lib/tool-factory.js';
|
|
|
3
3
|
import { RiskScoreInputSchema } from '../schemas/inputs.js';
|
|
4
4
|
import { RiskScoreResultSchema } from '../schemas/outputs.js';
|
|
5
5
|
const DEFAULT_DEPLOYMENT_CRITICALITY = 'medium';
|
|
6
|
+
// Hoisted: avoids array allocation + join on every request.
|
|
7
|
+
const SYSTEM_INSTRUCTION = 'You are assessing software deployment risk from a code diff.\nReturn strict JSON only, no markdown fences.';
|
|
8
|
+
function joinPromptLines(lines) {
|
|
9
|
+
return lines.join('\n');
|
|
10
|
+
}
|
|
6
11
|
function buildRiskPrompt(input) {
|
|
7
|
-
const
|
|
8
|
-
'You are assessing software deployment risk from a code diff.',
|
|
9
|
-
'Return strict JSON only, no markdown fences.',
|
|
10
|
-
].join('\n');
|
|
11
|
-
const prompt = [
|
|
12
|
+
const prompt = joinPromptLines([
|
|
12
13
|
`Deployment criticality: ${input.deploymentCriticality ?? DEFAULT_DEPLOYMENT_CRITICALITY}`,
|
|
13
14
|
'Score guidance: 0 is no risk, 100 is severe risk.',
|
|
14
15
|
'Rationale must be concise, concrete, and evidence-based.',
|
|
15
16
|
'',
|
|
16
17
|
'Unified diff:',
|
|
17
18
|
input.diff,
|
|
18
|
-
]
|
|
19
|
-
return { systemInstruction, prompt };
|
|
19
|
+
]);
|
|
20
|
+
return { systemInstruction: SYSTEM_INSTRUCTION, prompt };
|
|
20
21
|
}
|
|
21
22
|
export function registerRiskScoreTool(server) {
|
|
22
23
|
registerStructuredToolTask(server, {
|
|
@@ -3,12 +3,13 @@ import { registerStructuredToolTask, } from '../lib/tool-factory.js';
|
|
|
3
3
|
import { SuggestPatchInputSchema } from '../schemas/inputs.js';
|
|
4
4
|
import { PatchSuggestionResultSchema } from '../schemas/outputs.js';
|
|
5
5
|
const DEFAULT_PATCH_STYLE = 'balanced';
|
|
6
|
+
// Hoisted: avoids array allocation + join on every request.
|
|
7
|
+
const SYSTEM_INSTRUCTION = 'You are producing a corrective patch for a code review issue.\nReturn strict JSON only, no markdown fences.';
|
|
8
|
+
function joinPromptLines(lines) {
|
|
9
|
+
return lines.join('\n');
|
|
10
|
+
}
|
|
6
11
|
function buildPatchPrompt(input) {
|
|
7
|
-
const
|
|
8
|
-
'You are producing a corrective patch for a code review issue.',
|
|
9
|
-
'Return strict JSON only, no markdown fences.',
|
|
10
|
-
].join('\n');
|
|
11
|
-
const prompt = [
|
|
12
|
+
const prompt = joinPromptLines([
|
|
12
13
|
`Patch style: ${input.patchStyle ?? DEFAULT_PATCH_STYLE}`,
|
|
13
14
|
`Finding title: ${input.findingTitle}`,
|
|
14
15
|
`Finding details: ${input.findingDetails}`,
|
|
@@ -16,8 +17,8 @@ function buildPatchPrompt(input) {
|
|
|
16
17
|
'',
|
|
17
18
|
'Original unified diff:',
|
|
18
19
|
input.diff,
|
|
19
|
-
]
|
|
20
|
-
return { systemInstruction, prompt };
|
|
20
|
+
]);
|
|
21
|
+
return { systemInstruction: SYSTEM_INSTRUCTION, prompt };
|
|
21
22
|
}
|
|
22
23
|
export function registerSuggestPatchTool(server) {
|
|
23
24
|
registerStructuredToolTask(server, {
|