@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 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
- if (typeof values.model === 'string') {
22
- process.env.GEMINI_MODEL = values.model;
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) {
@@ -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 follows `ReviewDiffResultSchema` and includes `summary`, `overallRisk`, `findings`, and `testsNeeded`.
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 30,000 ms; retry count defaults to 1 with exponential backoff.
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
- ---
@@ -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
- function formatNumber(value) {
6
- return numberFormatter.format(value);
7
- }
8
- function getPositiveIntEnv(name) {
9
- const parsed = Number.parseInt(process.env[name] ?? '', 10);
10
- return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
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
- return getPositiveIntEnv(MAX_DIFF_CHARS_ENV_VAR) ?? DEFAULT_MAX_DIFF_CHARS;
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
- const maxDiffChars = getMaxDiffChars();
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)) {
@@ -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 (typeof error === 'string') {
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) => typeof item === 'object' && item !== null && !Array.isArray(item)
40
- ? stripJsonSchemaConstraints(item)
41
- : item);
42
+ result[key] = value.map((item) => isJsonRecord(item) ? stripJsonSchemaConstraints(item) : item);
42
43
  }
43
- else if (typeof value === 'object' && value !== null) {
44
+ else if (isJsonRecord(value)) {
44
45
  result[key] = stripJsonSchemaConstraints(value);
45
46
  }
46
47
  else {
@@ -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
- return process.env.GEMINI_MODEL ?? 'gemini-2.5-flash';
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 = 30_000;
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
- console.error(JSON.stringify(payload));
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?.requestId ?? null,
58
- model: context?.model ?? null,
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 === 429 ||
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 === 'RESOURCE_EXHAUSTED' ||
113
- transientCode === 'UNAVAILABLE' ||
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: Math.round(performance.now() - startedAt),
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
- throw new Error(`Gemini request failed after ${maxRetries + 1} attempts: ${getErrorMessage(lastError)}`);
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 object (e.g. `MySchema.shape`) used as the MCP input schema. */
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
- /** Full Zod schema for runtime input re-validation (rejects unknown fields). */
19
- fullInputSchema?: z.ZodType;
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. */
@@ -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 = config.geminiSchema
9
- ? stripJsonSchemaConstraints(z.toJSONSchema(config.geminiSchema))
10
- : stripJsonSchemaConstraints(z.toJSONSchema(config.resultSchema));
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 inputRecord = config.fullInputSchema
44
- ? config.fullInputSchema.parse(input)
45
- : input;
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
- try {
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
- try {
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,
@@ -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 {};
@@ -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('get-help', {
44
+ server.registerPrompt(HELP_PROMPT_NAME, {
3
45
  title: 'Get Help',
4
46
  description: 'Return the server usage instructions.',
5
47
  }, () => ({
6
- description: 'Server usage instructions',
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
  }
@@ -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('server-instructions', 'internal://instructions', {
5
+ server.registerResource(RESOURCE_ID, RESOURCE_URI, {
3
6
  title: 'Server Instructions',
4
7
  description: 'Guidance for using the MCP tools effectively.',
5
- mimeType: 'text/markdown',
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: 'text/markdown',
17
+ mimeType: RESOURCE_MIME_TYPE,
15
18
  text: instructions,
16
19
  },
17
20
  ],
@@ -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 z
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: z
21
- .string()
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(z
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: z
59
- .string()
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()
@@ -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: z
53
- .string()
54
- .min(OUTPUT_LIMITS.reviewFinding.title.min)
55
- .max(OUTPUT_LIMITS.reviewFinding.title.max)
56
- .describe('Short finding title.'),
57
- explanation: z
58
- .string()
59
- .min(OUTPUT_LIMITS.reviewFinding.text.min)
60
- .max(OUTPUT_LIMITS.reviewFinding.text.max)
61
- .describe('Why this issue matters.'),
62
- recommendation: z
63
- .string()
64
- .min(OUTPUT_LIMITS.reviewFinding.text.min)
65
- .max(OUTPUT_LIMITS.reviewFinding.text.max)
66
- .describe('Concrete fix recommendation.'),
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: z
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(z
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(z
104
- .string()
105
- .min(OUTPUT_LIMITS.riskScoreResult.rationale.itemMin)
106
- .max(OUTPUT_LIMITS.riskScoreResult.rationale.itemMax)
107
- .describe('Reason that influenced the final score.'))
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: z
114
- .string()
115
- .min(OUTPUT_LIMITS.patchSuggestionResult.summary.min)
116
- .max(OUTPUT_LIMITS.patchSuggestionResult.summary.max)
117
- .describe('Short patch strategy summary.'),
118
- patch: z
119
- .string()
120
- .min(OUTPUT_LIMITS.patchSuggestionResult.patch.min)
121
- .max(OUTPUT_LIMITS.patchSuggestionResult.patch.max)
122
- .describe('Unified diff patch text.'),
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(z
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('Unable to locate package.json for code-review-analyst.');
45
+ throw new Error(`Unable to locate package.json for ${SERVER_NAME}.`);
46
46
  }
47
- return extractVersion(readPackageJson(packageJsonPath), packageJsonPath);
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(join(currentDir, 'instructions.md'), 'utf8');
55
+ return readFileSync(instructionsPath, 'utf8');
54
56
  }
55
57
  catch (error) {
56
- process.emitWarning(`Failed to load instructions.md: ${getErrorMessage(error)}`);
57
- return '(Instructions failed to load)';
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: 'code-review-analyst',
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 systemInstruction = [
13
- 'You are a senior staff engineer performing pull request review.',
14
- 'Return strict JSON only with no markdown fences.',
15
- ].join('\n');
16
- const prompt = [
17
+ 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
- ].join('\n');
27
- return { systemInstruction, prompt };
27
+ ]);
28
+ return { systemInstruction: SYSTEM_INSTRUCTION, prompt };
28
29
  }
29
30
  export function registerReviewDiffTool(server) {
30
31
  registerStructuredToolTask(server, {
@@ -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 systemInstruction = [
8
- 'You are assessing software deployment risk from a code diff.',
9
- 'Return strict JSON only, no markdown fences.',
10
- ].join('\n');
11
- const prompt = [
12
+ 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
- ].join('\n');
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 systemInstruction = [
8
- 'You are producing a corrective patch for a code review issue.',
9
- 'Return strict JSON only, no markdown fences.',
10
- ].join('\n');
11
- const prompt = [
12
+ 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
- ].join('\n');
20
- return { systemInstruction, prompt };
20
+ ]);
21
+ return { systemInstruction: SYSTEM_INSTRUCTION, prompt };
21
22
  }
22
23
  export function registerSuggestPatchTool(server) {
23
24
  registerStructuredToolTask(server, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@j0hanz/code-review-analyst-mcp",
3
- "version": "1.0.2",
3
+ "version": "1.1.0",
4
4
  "mcpName": "io.github.j0hanz/code-review-analyst",
5
5
  "description": "Gemini-powered MCP server for code review analysis.",
6
6
  "type": "module",