@j0hanz/code-review-analyst-mcp 1.7.3 → 1.7.5

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.
Files changed (38) hide show
  1. package/dist/index.js +3 -2
  2. package/dist/lib/diff-store.js +3 -2
  3. package/dist/lib/diff.js +4 -9
  4. package/dist/lib/env-config.js +1 -4
  5. package/dist/lib/errors.js +1 -2
  6. package/dist/lib/format.d.ts +3 -0
  7. package/dist/lib/format.js +9 -0
  8. package/dist/lib/gemini-schema.js +3 -2
  9. package/dist/lib/gemini.js +6 -9
  10. package/dist/lib/markdown.d.ts +2 -0
  11. package/dist/lib/markdown.js +6 -0
  12. package/dist/lib/model-config.d.ts +2 -2
  13. package/dist/lib/model-config.js +2 -3
  14. package/dist/lib/tool-contracts.js +11 -11
  15. package/dist/lib/tool-factory.d.ts +1 -0
  16. package/dist/lib/tool-factory.js +67 -47
  17. package/dist/lib/tool-response.js +6 -5
  18. package/dist/lib/types.d.ts +2 -1
  19. package/dist/prompts/index.js +6 -3
  20. package/dist/resources/index.js +20 -8
  21. package/dist/resources/instructions.js +9 -8
  22. package/dist/resources/server-config.js +14 -13
  23. package/dist/resources/tool-catalog.js +12 -11
  24. package/dist/resources/tool-info.js +3 -4
  25. package/dist/resources/workflows.js +2 -3
  26. package/dist/schemas/inputs.js +12 -10
  27. package/dist/schemas/outputs.js +3 -2
  28. package/dist/server.js +6 -5
  29. package/dist/tools/analyze-complexity.js +2 -3
  30. package/dist/tools/analyze-pr-impact.js +1 -3
  31. package/dist/tools/detect-api-breaking.js +2 -3
  32. package/dist/tools/generate-diff.js +8 -5
  33. package/dist/tools/generate-review-summary.js +1 -3
  34. package/dist/tools/generate-test-plan.js +1 -3
  35. package/dist/tools/index.js +2 -2
  36. package/dist/tools/inspect-code-quality.js +1 -3
  37. package/dist/tools/suggest-search-replace.js +2 -1
  38. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -22,9 +22,10 @@ const CLI_OPTIONS = {
22
22
  },
23
23
  };
24
24
  function setStringEnv(name, value) {
25
- if (typeof value === 'string') {
26
- process.env[name] = value;
25
+ if (typeof value !== 'string') {
26
+ return;
27
27
  }
28
+ process.env[name] = value;
28
29
  }
29
30
  function applyCliEnvironmentOverrides(values) {
30
31
  for (const mapping of CLI_ENV_MAPPINGS) {
@@ -10,9 +10,10 @@ let sendResourceUpdated;
10
10
  function setDiffSlot(key, data) {
11
11
  if (data) {
12
12
  diffSlots.set(key, data);
13
- return;
14
13
  }
15
- diffSlots.delete(key);
14
+ else {
15
+ diffSlots.delete(key);
16
+ }
16
17
  }
17
18
  function notifyDiffUpdated() {
18
19
  void sendResourceUpdated?.({ uri: DIFF_RESOURCE_URI }).catch(() => {
package/dist/lib/diff.js CHANGED
@@ -48,15 +48,10 @@ const GIT_BINARY_PATCH = /^GIT binary patch/m;
48
48
  const HAS_HUNK = /^@@/m;
49
49
  const HAS_OLD_MODE = /^old mode /m;
50
50
  function shouldKeepSection(section) {
51
- if (!section.trim())
52
- return false;
53
- if (BINARY_FILE_LINE.test(section))
54
- return false;
55
- if (GIT_BINARY_PATCH.test(section))
56
- return false;
57
- if (HAS_OLD_MODE.test(section) && !HAS_HUNK.test(section))
58
- return false;
59
- return true;
51
+ return (Boolean(section.trim()) &&
52
+ !BINARY_FILE_LINE.test(section) &&
53
+ !GIT_BINARY_PATCH.test(section) &&
54
+ (!HAS_OLD_MODE.test(section) || HAS_HUNK.test(section)));
60
55
  }
61
56
  function processSection(raw, start, end, sections) {
62
57
  if (end > start) {
@@ -4,10 +4,7 @@ function parsePositiveInteger(value) {
4
4
  return undefined;
5
5
  }
6
6
  const parsed = Number.parseInt(normalized, 10);
7
- if (!Number.isSafeInteger(parsed) || parsed <= 0) {
8
- return undefined;
9
- }
10
- return parsed;
7
+ return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : undefined;
11
8
  }
12
9
  function resolveEnvInt(envVar, defaultValue) {
13
10
  const envValue = process.env[envVar] ?? '';
@@ -8,8 +8,7 @@ function getStringProperty(value, key) {
8
8
  if (!isObjectRecord(value) || !(key in value)) {
9
9
  return undefined;
10
10
  }
11
- const record = value;
12
- const property = record[key];
11
+ const property = value[key];
13
12
  return typeof property === 'string' ? property : undefined;
14
13
  }
15
14
  export function getErrorMessage(error) {
@@ -0,0 +1,3 @@
1
+ export declare function formatOptionalLine(label: string, value: string | number | undefined): string;
2
+ export declare function formatLanguageSegment(language: string | undefined): string;
3
+ export declare function formatCountLabel(count: number, singular: string, plural: string): string;
@@ -0,0 +1,9 @@
1
+ export function formatOptionalLine(label, value) {
2
+ return value === undefined ? '' : `\n${label}: ${value}`;
3
+ }
4
+ export function formatLanguageSegment(language) {
5
+ return formatOptionalLine('Language', language);
6
+ }
7
+ export function formatCountLabel(count, singular, plural) {
8
+ return `${count} ${count === 1 ? singular : plural}`;
9
+ }
@@ -1,4 +1,4 @@
1
- const CONSTRAINT_KEYS = new Set([
1
+ const CONSTRAINT_KEY_VALUES = [
2
2
  'minLength',
3
3
  'maxLength',
4
4
  'minimum',
@@ -10,7 +10,8 @@ const CONSTRAINT_KEYS = new Set([
10
10
  'multipleOf',
11
11
  'pattern',
12
12
  'format',
13
- ]);
13
+ ];
14
+ const CONSTRAINT_KEYS = new Set(CONSTRAINT_KEY_VALUES);
14
15
  const INTEGER_JSON_TYPE = 'integer';
15
16
  const NUMBER_JSON_TYPE = 'number';
16
17
  function isJsonRecord(value) {
@@ -17,11 +17,8 @@ const GEMINI_BATCH_MODE_ENV_VAR = 'GEMINI_BATCH_MODE';
17
17
  const GEMINI_API_KEY_ENV_VAR = 'GEMINI_API_KEY';
18
18
  const GOOGLE_API_KEY_ENV_VAR = 'GOOGLE_API_KEY';
19
19
  function getDefaultModel() {
20
- if (_defaultModel !== undefined)
21
- return _defaultModel;
22
- const value = process.env[GEMINI_MODEL_ENV_VAR] ?? DEFAULT_MODEL;
23
- _defaultModel = value;
24
- return value;
20
+ _defaultModel ??= process.env[GEMINI_MODEL_ENV_VAR] ?? DEFAULT_MODEL;
21
+ return _defaultModel;
25
22
  }
26
23
  const DEFAULT_MAX_RETRIES = 3;
27
24
  const DEFAULT_TIMEOUT_MS = 90_000;
@@ -208,10 +205,10 @@ function getSafetySettings(threshold) {
208
205
  if (cached) {
209
206
  return cached;
210
207
  }
211
- const settings = new Array();
212
- for (const category of SAFETY_CATEGORIES) {
213
- settings.push({ category, threshold });
214
- }
208
+ const settings = SAFETY_CATEGORIES.map((category) => ({
209
+ category,
210
+ threshold,
211
+ }));
215
212
  safetySettingsCache.set(threshold, settings);
216
213
  return settings;
217
214
  }
@@ -0,0 +1,2 @@
1
+ export declare function toBulletedList(lines: readonly string[]): string;
2
+ export declare function toInlineCode(value: string): string;
@@ -0,0 +1,6 @@
1
+ export function toBulletedList(lines) {
2
+ return lines.map((line) => `- ${line}`).join('\n');
3
+ }
4
+ export function toInlineCode(value) {
5
+ return `\`${value}\``;
6
+ }
@@ -6,9 +6,9 @@ export declare const DEFAULT_LANGUAGE = "detect";
6
6
  export declare const DEFAULT_FRAMEWORK = "detect";
7
7
  /** Extended timeout for deep analysis calls (ms). */
8
8
  export declare const DEFAULT_TIMEOUT_EXTENDED_MS = 120000;
9
- export declare const MODEL_TIMEOUT_MS: {
9
+ export declare const MODEL_TIMEOUT_MS: Readonly<{
10
10
  readonly extended: 120000;
11
- };
11
+ }>;
12
12
  /** Thinking level for Flash triage. */
13
13
  export declare const FLASH_TRIAGE_THINKING_LEVEL: "minimal";
14
14
  /** Thinking level for Flash analysis. */
@@ -6,10 +6,9 @@ export const DEFAULT_LANGUAGE = 'detect';
6
6
  export const DEFAULT_FRAMEWORK = 'detect';
7
7
  /** Extended timeout for deep analysis calls (ms). */
8
8
  export const DEFAULT_TIMEOUT_EXTENDED_MS = 120_000;
9
- export const MODEL_TIMEOUT_MS = {
9
+ export const MODEL_TIMEOUT_MS = Object.freeze({
10
10
  extended: DEFAULT_TIMEOUT_EXTENDED_MS,
11
- };
12
- Object.freeze(MODEL_TIMEOUT_MS);
11
+ });
13
12
  // ---------------------------------------------------------------------------
14
13
  // Budgets (Thinking & Output)
15
14
  // ---------------------------------------------------------------------------
@@ -10,17 +10,17 @@ export const INSPECTION_FOCUS_AREAS = [
10
10
  'concurrency',
11
11
  ];
12
12
  export function buildStructuredToolRuntimeOptions(contract) {
13
- const options = {};
14
- if (contract.thinkingLevel !== undefined) {
15
- options.thinkingLevel = contract.thinkingLevel;
16
- }
17
- if (contract.temperature !== undefined) {
18
- options.temperature = contract.temperature;
19
- }
20
- if (contract.deterministicJson !== undefined) {
21
- options.deterministicJson = contract.deterministicJson;
22
- }
23
- return options;
13
+ return {
14
+ ...(contract.thinkingLevel !== undefined
15
+ ? { thinkingLevel: contract.thinkingLevel }
16
+ : {}),
17
+ ...(contract.temperature !== undefined
18
+ ? { temperature: contract.temperature }
19
+ : {}),
20
+ ...(contract.deterministicJson !== undefined
21
+ ? { deterministicJson: contract.deterministicJson }
22
+ : {}),
23
+ };
24
24
  }
25
25
  export const TOOL_CONTRACTS = [
26
26
  {
@@ -130,6 +130,7 @@ export declare class ToolExecutionRunner<TInput extends object, TResult extends
130
130
  private storeResultSafely;
131
131
  private executeValidation;
132
132
  private executeModelCall;
133
+ private reportAndStatus;
133
134
  run(input: unknown): Promise<CallToolResult>;
134
135
  }
135
136
  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;
@@ -37,11 +37,10 @@ function buildToolAnnotations(annotations) {
37
37
  openWorldHint: true,
38
38
  };
39
39
  }
40
- const annotationOverrides = { ...annotations };
41
- delete annotationOverrides.destructiveHint;
40
+ const { destructiveHint, ...annotationOverrides } = annotations;
42
41
  return {
43
- readOnlyHint: !annotations.destructiveHint,
44
- idempotentHint: !annotations.destructiveHint,
42
+ readOnlyHint: !destructiveHint,
43
+ idempotentHint: !destructiveHint,
45
44
  openWorldHint: true,
46
45
  ...annotationOverrides,
47
46
  };
@@ -183,13 +182,17 @@ function createProgressReporter(extra) {
183
182
  };
184
183
  }
185
184
  const progressToken = rawToken;
186
- let lastCurrent = 0;
185
+ let lastCurrent = -1;
187
186
  let didSendTerminal = false;
188
187
  return async (payload) => {
189
188
  if (didSendTerminal) {
190
189
  return;
191
190
  }
192
- const current = Math.max(payload.current, lastCurrent);
191
+ let { current } = payload;
192
+ if (current <= lastCurrent && current < (payload.total ?? Infinity)) {
193
+ current = lastCurrent + 0.01;
194
+ }
195
+ current = Math.max(current, lastCurrent);
193
196
  const total = payload.total !== undefined
194
197
  ? Math.max(payload.total, current)
195
198
  : undefined;
@@ -265,18 +268,20 @@ async function sendSingleStepProgress(extra, toolName, context, current, state)
265
268
  async function reportProgressStepUpdate(reportProgress, toolName, context, current, metadata) {
266
269
  await reportProgress({
267
270
  current,
271
+ total: TASK_PROGRESS_TOTAL,
268
272
  message: formatProgressStep(toolName, context, metadata),
269
273
  });
270
274
  }
271
275
  async function reportProgressCompletionUpdate(reportProgress, toolName, context, outcome) {
272
276
  await reportProgress({
273
277
  current: TASK_PROGRESS_TOTAL,
278
+ total: TASK_PROGRESS_TOTAL,
274
279
  message: formatProgressCompletion(toolName, context, outcome),
275
280
  });
276
281
  }
277
282
  async function reportSchemaRetryProgressBestEffort(reportProgress, toolName, context, retryCount, maxRetries) {
278
283
  try {
279
- await reportProgressStepUpdate(reportProgress, toolName, context, STEP_VALIDATING_RESPONSE, `Schema repair in progress (attempt ${retryCount}/${maxRetries})...`);
284
+ await reportProgressStepUpdate(reportProgress, toolName, context, STEP_VALIDATING_RESPONSE + retryCount / (maxRetries + 1), `Schema repair in progress (attempt ${retryCount}/${maxRetries})...`);
280
285
  }
281
286
  catch {
282
287
  // Progress updates are best-effort and must not interrupt retries.
@@ -389,13 +394,11 @@ export class ToolExecutionRunner {
389
394
  const details = asObjectRecord(record.details);
390
395
  const { attempt } = details;
391
396
  const msg = `Network error. Retrying (attempt ${String(attempt)})...`;
392
- await reportProgressStepUpdate(this.reportProgress, this.config.name, this.progressContext, STEP_CALLING_MODEL, msg);
393
- await this.updateStatusMessage(msg);
397
+ await this.reportAndStatus(STEP_CALLING_MODEL, msg);
394
398
  }
395
399
  else if (record.event === 'gemini_queue_acquired') {
396
400
  const msg = 'Model queue acquired, generating response...';
397
- await reportProgressStepUpdate(this.reportProgress, this.config.name, this.progressContext, STEP_CALLING_MODEL, msg);
398
- await this.updateStatusMessage(msg);
401
+ await this.reportAndStatus(STEP_CALLING_MODEL, msg);
399
402
  }
400
403
  }
401
404
  setResponseSchemaOverride(responseSchema) {
@@ -463,8 +466,7 @@ export class ToolExecutionRunner {
463
466
  try {
464
467
  const raw = await generateStructuredJson(createGenerationRequest(this.config, { systemInstruction, prompt: retryPrompt }, this.responseSchema, this.onLog, this.signal));
465
468
  if (attempt === 0) {
466
- await this.updateStatusMessage('Verifying output structure...');
467
- await reportProgressStepUpdate(this.reportProgress, this.config.name, this.progressContext, STEP_VALIDATING_RESPONSE, 'Verifying output structure...');
469
+ await this.reportAndStatus(STEP_VALIDATING_RESPONSE, 'Verifying output structure...');
468
470
  }
469
471
  parsed = this.config.resultSchema.parse(raw);
470
472
  break;
@@ -492,6 +494,10 @@ export class ToolExecutionRunner {
492
494
  }
493
495
  return parsed;
494
496
  }
497
+ async reportAndStatus(step, message) {
498
+ await reportProgressStepUpdate(this.reportProgress, this.config.name, this.progressContext, step, message);
499
+ await this.updateStatusMessage(message);
500
+ }
495
501
  async run(input) {
496
502
  try {
497
503
  const inputRecord = parseToolInput(input, this.config.fullInputSchema);
@@ -499,23 +505,18 @@ export class ToolExecutionRunner {
499
505
  const ctx = {
500
506
  diffSlot: this.hasSnapshot ? this.diffSlotSnapshot : getDiff(),
501
507
  };
502
- await reportProgressStepUpdate(this.reportProgress, this.config.name, this.progressContext, STEP_STARTING, 'Initializing...');
503
- await this.updateStatusMessage('Initializing...');
504
- await reportProgressStepUpdate(this.reportProgress, this.config.name, this.progressContext, STEP_VALIDATING, 'Validating request parameters...');
505
- await this.updateStatusMessage('Validating request parameters...');
508
+ await this.reportAndStatus(STEP_STARTING, 'Initializing...');
509
+ await this.reportAndStatus(STEP_VALIDATING, 'Validating request parameters...');
506
510
  const validationError = await this.executeValidation(inputRecord, ctx);
507
511
  if (validationError) {
508
512
  return validationError;
509
513
  }
510
- await reportProgressStepUpdate(this.reportProgress, this.config.name, this.progressContext, STEP_BUILDING_PROMPT, 'Constructing analysis context...');
511
- await this.updateStatusMessage('Constructing analysis context...');
514
+ await this.reportAndStatus(STEP_BUILDING_PROMPT, 'Constructing analysis context...');
512
515
  const promptParts = this.config.buildPrompt(inputRecord, ctx);
513
516
  const { prompt, systemInstruction } = promptParts;
514
- await reportProgressStepUpdate(this.reportProgress, this.config.name, this.progressContext, STEP_CALLING_MODEL, 'Querying Gemini model...');
515
- await this.updateStatusMessage('Querying Gemini model...');
517
+ await this.reportAndStatus(STEP_CALLING_MODEL, 'Querying Gemini model...');
516
518
  const parsed = await this.executeModelCall(systemInstruction, prompt);
517
- await reportProgressStepUpdate(this.reportProgress, this.config.name, this.progressContext, STEP_FINALIZING, 'Processing results...');
518
- await this.updateStatusMessage('Processing results...');
519
+ await this.reportAndStatus(STEP_FINALIZING, 'Processing results...');
519
520
  const finalResult = (this.config.transformResult
520
521
  ? this.config.transformResult(inputRecord, parsed, ctx)
521
522
  : parsed);
@@ -557,35 +558,54 @@ export function registerStructuredToolTask(server, config) {
557
558
  geminiSchema: config.geminiSchema,
558
559
  resultSchema: config.resultSchema,
559
560
  });
560
- server.registerTool(config.name, {
561
+ server.experimental.tasks.registerToolTask(config.name, {
561
562
  title: config.title,
562
563
  description: config.description,
563
564
  inputSchema: config.inputSchema,
564
565
  outputSchema: DefaultOutputSchema,
565
566
  annotations: buildToolAnnotations(config.annotations),
566
- }, async (input, extra) => {
567
- const runner = new ToolExecutionRunner(config, {
568
- onLog: async (level, data) => {
569
- // Standard logging for tool calls
570
- try {
571
- await server.sendLoggingMessage({
572
- level: toLoggingLevel(level),
573
- logger: 'gemini',
574
- data: asObjectRecord(data),
575
- });
576
- }
577
- catch {
578
- // Fallback if logging fails
579
- }
580
- },
581
- reportProgress: createProgressReporter(extra),
582
- statusReporter: {
583
- updateStatus: async () => {
584
- // No-op for standard tool calls as they don't have a persistent task status
567
+ execution: {
568
+ taskSupport: 'optional',
569
+ },
570
+ }, {
571
+ createTask: async (input, extra) => {
572
+ const task = await extra.taskStore.createTask({ ttl: 300000 });
573
+ const extendedStore = extra.taskStore;
574
+ const runner = new ToolExecutionRunner(config, {
575
+ onLog: async (level, data) => {
576
+ try {
577
+ await server.sendLoggingMessage({
578
+ level: toLoggingLevel(level),
579
+ logger: 'gemini',
580
+ data: asObjectRecord(data),
581
+ });
582
+ }
583
+ catch {
584
+ // Fallback if logging fails
585
+ }
585
586
  },
586
- },
587
- });
588
- runner.setResponseSchemaOverride(responseSchema);
589
- return await runner.run(input);
587
+ reportProgress: createProgressReporter(extra),
588
+ statusReporter: {
589
+ updateStatus: async (message) => {
590
+ await extendedStore.updateTaskStatus(task.taskId, 'working', message);
591
+ },
592
+ storeResult: async (status, result) => {
593
+ await extra.taskStore.storeTaskResult(task.taskId, status, result);
594
+ },
595
+ },
596
+ });
597
+ runner.setResponseSchemaOverride(responseSchema);
598
+ // Run in background
599
+ runner.run(input).catch(async (error) => {
600
+ await extendedStore.updateTaskStatus(task.taskId, 'failed', getErrorMessage(error));
601
+ });
602
+ return { task };
603
+ },
604
+ getTask: async (input, extra) => {
605
+ return await extra.taskStore.getTask(extra.taskId);
606
+ },
607
+ getTaskResult: async (input, extra) => {
608
+ return (await extra.taskStore.getTaskResult(extra.taskId));
609
+ },
590
610
  });
591
611
  }
@@ -6,16 +6,17 @@ function appendErrorMeta(error, meta) {
6
6
  error.kind = meta.kind;
7
7
  }
8
8
  }
9
+ function createToolError(code, message, meta) {
10
+ const error = { code, message };
11
+ appendErrorMeta(error, meta);
12
+ return error;
13
+ }
9
14
  function toTextContent(structured, textContent) {
10
15
  const text = textContent ?? JSON.stringify(structured);
11
16
  return [{ type: 'text', text }];
12
17
  }
13
18
  function createErrorStructuredContent(code, message, result, meta) {
14
- const error = {
15
- code,
16
- message,
17
- };
18
- appendErrorMeta(error, meta);
19
+ const error = createToolError(code, message, meta);
19
20
  if (result === undefined) {
20
21
  return { ok: false, error };
21
22
  }
@@ -1,11 +1,12 @@
1
1
  export type JsonObject = Record<string, unknown>;
2
2
  export type GeminiLogHandler = (level: string, data: unknown) => Promise<void>;
3
+ export type GeminiThinkingLevel = 'minimal' | 'low' | 'medium' | 'high';
3
4
  export interface GeminiRequestExecutionOptions {
4
5
  maxRetries?: number;
5
6
  timeoutMs?: number;
6
7
  temperature?: number;
7
8
  maxOutputTokens?: number;
8
- thinkingLevel?: 'minimal' | 'low' | 'medium' | 'high';
9
+ thinkingLevel?: GeminiThinkingLevel;
9
10
  includeThoughts?: boolean;
10
11
  signal?: AbortSignal;
11
12
  onLog?: GeminiLogHandler;
@@ -1,5 +1,6 @@
1
1
  import { completable } from '@modelcontextprotocol/sdk/server/completable.js';
2
2
  import { z } from 'zod';
3
+ import { toInlineCode } from '../lib/markdown.js';
3
4
  import { getToolContract, getToolContractNames, INSPECTION_FOCUS_AREAS, } from '../lib/tool-contracts.js';
4
5
  export const PROMPT_DEFINITIONS = [
5
6
  {
@@ -34,7 +35,7 @@ function completeByPrefix(values, prefix) {
34
35
  function getToolGuide(tool) {
35
36
  const contract = getToolContract(tool);
36
37
  if (!contract) {
37
- return `Use \`${tool}\` to analyze your code changes.`;
38
+ return `Use ${toInlineCode(tool)} to analyze your code changes.`;
38
39
  }
39
40
  const { thinkingLevel } = contract;
40
41
  const modelLine = thinkingLevel !== undefined
@@ -66,12 +67,14 @@ function registerHelpPrompt(server, instructions) {
66
67
  }));
67
68
  }
68
69
  function buildReviewGuideText(tool, focusArea) {
70
+ const toolCode = toInlineCode(tool);
71
+ const suggestToolCode = toInlineCode('suggest_search_replace');
69
72
  return (`# Guide: ${tool} / ${focusArea}\n\n` +
70
- `## Tool: \`${tool}\`\n${getToolGuide(tool)}\n\n` +
73
+ `## Tool: ${toolCode}\n${getToolGuide(tool)}\n\n` +
71
74
  `## Focus: ${focusArea}\n${getFocusAreaGuide(focusArea)}\n\n` +
72
75
  `## Example Fix\n` +
73
76
  `Finding: "Uncaught promise rejection"\n` +
74
- `Call \`suggest_search_replace\`:\n` +
77
+ `Call ${suggestToolCode}:\n` +
75
78
  '```\n' +
76
79
  `search: " } catch {\\n }"\n` +
77
80
  `replace: " } catch (err) {\\n logger.error(err);\\n }"\n` +
@@ -13,6 +13,16 @@ function completeByPrefix(values, prefix) {
13
13
  function createMarkdownContent(uri, text) {
14
14
  return { uri: uri.href, mimeType: RESOURCE_MIME_TYPE, text };
15
15
  }
16
+ function formatUnknownToolMessage(name) {
17
+ return `Unknown tool: ${name}`;
18
+ }
19
+ function formatDiffResourceText() {
20
+ const slot = getDiff();
21
+ if (!slot) {
22
+ return '# No diff cached. Call generate_diff first.';
23
+ }
24
+ return `# Diff — ${slot.mode} — ${slot.generatedAt}\n# ${slot.stats.files} file(s), +${slot.stats.added} -${slot.stats.deleted}\n\n${slot.diff}`;
25
+ }
16
26
  export const STATIC_RESOURCES = [
17
27
  {
18
28
  id: 'server-instructions',
@@ -77,7 +87,7 @@ function registerToolInfoResources(server) {
77
87
  }, (uri, { toolName }) => {
78
88
  const name = typeof toolName === 'string' ? toolName : '';
79
89
  const info = getToolInfo(name);
80
- const text = info ?? `Unknown tool: ${name}`;
90
+ const text = info ?? formatUnknownToolMessage(name);
81
91
  return { contents: [createMarkdownContent(uri, text)] };
82
92
  });
83
93
  }
@@ -91,13 +101,15 @@ function registerDiffResource(server) {
91
101
  audience: RESOURCE_AUDIENCE,
92
102
  priority: 1.0,
93
103
  },
94
- }, (uri) => {
95
- const slot = getDiff();
96
- const text = slot
97
- ? `# Diff — ${slot.mode} — ${slot.generatedAt}\n# ${slot.stats.files} file(s), +${slot.stats.added} -${slot.stats.deleted}\n\n${slot.diff}`
98
- : '# No diff cached. Call generate_diff first.';
99
- return { contents: [{ uri: uri.href, mimeType: PATCH_MIME_TYPE, text }] };
100
- });
104
+ }, (uri) => ({
105
+ contents: [
106
+ {
107
+ uri: uri.href,
108
+ mimeType: PATCH_MIME_TYPE,
109
+ text: formatDiffResourceText(),
110
+ },
111
+ ],
112
+ }));
101
113
  }
102
114
  export function registerAllResources(server, instructions) {
103
115
  for (const def of STATIC_RESOURCES) {
@@ -1,12 +1,13 @@
1
+ import { toBulletedList, toInlineCode } from '../lib/markdown.js';
1
2
  import { getToolContracts } from '../lib/tool-contracts.js';
2
3
  import { PROMPT_DEFINITIONS } from '../prompts/index.js';
3
4
  import { DIFF_RESOURCE_DESCRIPTION, STATIC_RESOURCES } from './index.js';
4
5
  import { getSharedConstraints } from './tool-info.js';
5
- const PROMPT_LIST = PROMPT_DEFINITIONS.map((def) => `- \`${def.name}\`: ${def.description}`);
6
+ const PROMPT_LIST = PROMPT_DEFINITIONS.map((def) => `${toInlineCode(def.name)}: ${def.description}`);
6
7
  const RESOURCE_LIST = [
7
- ...STATIC_RESOURCES.map((def) => `- \`${def.uri}\`: ${def.description}`),
8
- '- `internal://tool-info/{toolName}`: Per-tool contract details.',
9
- `- \`diff://current\`: ${DIFF_RESOURCE_DESCRIPTION}`,
8
+ ...STATIC_RESOURCES.map((def) => `${toInlineCode(def.uri)}: ${def.description}`),
9
+ `${toInlineCode('internal://tool-info/{toolName}')}: Per-tool contract details.`,
10
+ `${toInlineCode('diff://current')}: ${DIFF_RESOURCE_DESCRIPTION}`,
10
11
  ];
11
12
  function formatParameterLine(parameter) {
12
13
  const req = parameter.required ? 'req' : 'opt';
@@ -41,7 +42,7 @@ export function buildServerInstructions() {
41
42
  .map((contract) => `\`${contract.name}\``)
42
43
  .join(', ');
43
44
  const toolSections = contracts.map((contract) => formatToolSection(contract));
44
- const constraintLines = getSharedConstraints().map((constraint) => `- ${constraint}`);
45
+ const constraintLines = toBulletedList(getSharedConstraints());
45
46
  return `# CODE REVIEW ANALYST MCP
46
47
 
47
48
  ## CORE
@@ -50,16 +51,16 @@ export function buildServerInstructions() {
50
51
  - Tools: ${toolNames}
51
52
 
52
53
  ## PROMPTS
53
- ${PROMPT_LIST.join('\n')}
54
+ ${toBulletedList(PROMPT_LIST)}
54
55
 
55
56
  ## RESOURCES
56
- ${RESOURCE_LIST.join('\n')}
57
+ ${toBulletedList(RESOURCE_LIST)}
57
58
 
58
59
  ## TOOLS
59
60
  ${toolSections.join('\n\n')}
60
61
 
61
62
  ## CONSTRAINTS
62
- ${constraintLines.join('\n')}
63
+ ${constraintLines}
63
64
 
64
65
  ## TASK LIFECYCLE
65
66
  - Progress steps (0–6): starting → validating input → building prompt → calling model → validating response → finalizing → done.
@@ -1,4 +1,5 @@
1
1
  import { createCachedEnvInt } from '../lib/env-config.js';
2
+ import { toInlineCode } from '../lib/markdown.js';
2
3
  import { FLASH_MODEL } from '../lib/model-config.js';
3
4
  import { getToolContracts } from '../lib/tool-contracts.js';
4
5
  const DEFAULT_MAX_DIFF_CHARS = 120_000;
@@ -41,7 +42,7 @@ export function buildServerConfig() {
41
42
  const toolRows = getToolContracts()
42
43
  .filter((contract) => contract.model !== 'none')
43
44
  .map((contract) => {
44
- return `| \`${contract.name}\` | \`${contract.model}\` | ${formatThinkingLevel(contract.thinkingLevel)} | ${formatTimeout(contract.timeoutMs)} | ${formatNumber(contract.maxOutputTokens)} |`;
45
+ return `| ${toInlineCode(contract.name)} | ${toInlineCode(contract.model)} | ${formatThinkingLevel(contract.thinkingLevel)} | ${formatTimeout(contract.timeoutMs)} | ${formatNumber(contract.maxOutputTokens)} |`;
45
46
  })
46
47
  .join('\n');
47
48
  return `# Server Configuration
@@ -50,15 +51,15 @@ export function buildServerConfig() {
50
51
 
51
52
  | Limit | Value | Env |
52
53
  |-------|-------|-----|
53
- | Diff limit | ${formatNumber(maxDiffChars)} chars | \`MAX_DIFF_CHARS\` |
54
- | Concurrency limit | ${maxConcurrent} | \`MAX_CONCURRENT_CALLS\` |
55
- | Batch concurrency limit | ${maxConcurrentBatch} | \`MAX_CONCURRENT_BATCH_CALLS\` |
56
- | Wait timeout | ${formatNumber(concurrentWaitMs)}ms | \`MAX_CONCURRENT_CALLS_WAIT_MS\` |
57
- | Batch mode | ${batchMode} | \`GEMINI_BATCH_MODE\` |
54
+ | Diff limit | ${formatNumber(maxDiffChars)} chars | ${toInlineCode('MAX_DIFF_CHARS')} |
55
+ | Concurrency limit | ${maxConcurrent} | ${toInlineCode('MAX_CONCURRENT_CALLS')} |
56
+ | Batch concurrency limit | ${maxConcurrentBatch} | ${toInlineCode('MAX_CONCURRENT_BATCH_CALLS')} |
57
+ | Wait timeout | ${formatNumber(concurrentWaitMs)}ms | ${toInlineCode('MAX_CONCURRENT_CALLS_WAIT_MS')} |
58
+ | Batch mode | ${batchMode} | ${toInlineCode('GEMINI_BATCH_MODE')} |
58
59
 
59
60
  ## Model Assignments
60
61
 
61
- Default model: \`${defaultModel}\` (override with \`GEMINI_MODEL\`)
62
+ Default model: ${toInlineCode(defaultModel)} (override with ${toInlineCode('GEMINI_MODEL')})
62
63
 
63
64
  | Tool | Model | Thinking Level | Timeout | Max Output Tokens |
64
65
  |------|-------|----------------|---------|-------------------|
@@ -66,17 +67,17 @@ ${toolRows}
66
67
 
67
68
  ## Safety
68
69
 
69
- - Harm block threshold: \`${safetyThreshold}\`
70
- - Override with \`GEMINI_HARM_BLOCK_THRESHOLD\` (BLOCK_NONE, BLOCK_ONLY_HIGH, BLOCK_MEDIUM_AND_ABOVE, BLOCK_LOW_AND_ABOVE)
70
+ - Harm block threshold: ${toInlineCode(safetyThreshold)}
71
+ - Override with ${toInlineCode('GEMINI_HARM_BLOCK_THRESHOLD')} (BLOCK_NONE, BLOCK_ONLY_HIGH, BLOCK_MEDIUM_AND_ABOVE, BLOCK_LOW_AND_ABOVE)
71
72
 
72
73
  ## API Keys
73
74
 
74
- - Set \`GEMINI_API_KEY\` or \`GOOGLE_API_KEY\` environment variable (required)
75
+ - Set ${toInlineCode('GEMINI_API_KEY')} or ${toInlineCode('GOOGLE_API_KEY')} environment variable (required)
75
76
 
76
77
  ## Batch Mode
77
78
 
78
- - \`GEMINI_BATCH_MODE\`: \`off\` (default) or \`inline\`
79
- - \`GEMINI_BATCH_POLL_INTERVAL_MS\`: poll cadence for batch status checks
80
- - \`GEMINI_BATCH_TIMEOUT_MS\`: max wait for batch completion
79
+ - ${toInlineCode('GEMINI_BATCH_MODE')}: ${toInlineCode('off')} (default) or ${toInlineCode('inline')}
80
+ - ${toInlineCode('GEMINI_BATCH_POLL_INTERVAL_MS')}: poll cadence for batch status checks
81
+ - ${toInlineCode('GEMINI_BATCH_TIMEOUT_MS')}: max wait for batch completion
81
82
  `;
82
83
  }
@@ -1,13 +1,14 @@
1
+ import { toInlineCode } from '../lib/markdown.js';
1
2
  import { buildCoreContextPack } from './tool-info.js';
2
3
  const TOOL_CATALOG_CONTENT = `# Tool Catalog Details
3
4
 
4
5
  ## Optional Parameters
5
6
 
6
- - \`language\`: Primary language hint (auto-detects). All tools except \`suggest_search_replace\`.
7
- - \`focusAreas\`: Focus tags (security, performance, etc.). \`inspect_code_quality\` only.
8
- - \`maxFindings\`: Output cap (1–25). \`inspect_code_quality\` only.
9
- - \`testFramework\`: Framework hint. \`generate_test_plan\` only.
10
- - \`maxTestCases\`: Output cap (1–30). \`generate_test_plan\` only.
7
+ - ${toInlineCode('language')}: Primary language hint (auto-detects). All tools except ${toInlineCode('suggest_search_replace')}.
8
+ - ${toInlineCode('focusAreas')}: Focus tags (security, performance, etc.). ${toInlineCode('inspect_code_quality')} only.
9
+ - ${toInlineCode('maxFindings')}: Output cap (1–25). ${toInlineCode('inspect_code_quality')} only.
10
+ - ${toInlineCode('testFramework')}: Framework hint. ${toInlineCode('generate_test_plan')} only.
11
+ - ${toInlineCode('maxTestCases')}: Output cap (1–30). ${toInlineCode('generate_test_plan')} only.
11
12
 
12
13
  ## Cross-Tool Data Flow
13
14
 
@@ -26,12 +27,12 @@ generate_review_summary ──→ overallRisk ──────┤
26
27
 
27
28
  ## When to Use Each Tool
28
29
 
29
- - **Triage**: \`analyze_pr_impact\`, \`generate_review_summary\`.
30
- - **Inspection**: \`inspect_code_quality\`.
31
- - **Fixes**: \`suggest_search_replace\` (one finding/call).
32
- - **Tests**: \`generate_test_plan\`.
33
- - **Complexity**: \`analyze_time_space_complexity\`.
34
- - **Breaking API**: \`detect_api_breaking_changes\`.
30
+ - **Triage**: ${toInlineCode('analyze_pr_impact')}, ${toInlineCode('generate_review_summary')}.
31
+ - **Inspection**: ${toInlineCode('inspect_code_quality')}.
32
+ - **Fixes**: ${toInlineCode('suggest_search_replace')} (one finding/call).
33
+ - **Tests**: ${toInlineCode('generate_test_plan')}.
34
+ - **Complexity**: ${toInlineCode('analyze_time_space_complexity')}.
35
+ - **Breaking API**: ${toInlineCode('detect_api_breaking_changes')}.
35
36
  `;
36
37
  export function buildToolCatalog() {
37
38
  return `${buildCoreContextPack()}\n\n${TOOL_CATALOG_CONTENT}`;
@@ -1,3 +1,4 @@
1
+ import { toBulletedList, toInlineCode } from '../lib/markdown.js';
1
2
  import { getToolContract, getToolContracts } from '../lib/tool-contracts.js';
2
3
  const GLOBAL_CONSTRAINTS = [
3
4
  'Diff budget: <= 120K chars.',
@@ -71,7 +72,7 @@ ${entry.crossToolFlow.map((f) => `- ${f}`).join('\n')}
71
72
  `;
72
73
  }
73
74
  function formatCompactToolRow(entry) {
74
- return `| \`${entry.name}\` | ${entry.model} | ${entry.timeout} | ${entry.maxOutputTokens} | ${entry.purpose} |`;
75
+ return `| ${toInlineCode(entry.name)} | ${entry.model} | ${entry.timeout} | ${entry.maxOutputTokens} | ${entry.purpose} |`;
75
76
  }
76
77
  export function buildCoreContextPack() {
77
78
  const rows = TOOL_NAMES.flatMap((toolName) => {
@@ -91,9 +92,7 @@ export function buildCoreContextPack() {
91
92
  ${rows.join('\n')}
92
93
 
93
94
  ## Shared Constraints
94
- ${getSharedConstraints()
95
- .map((constraint) => `- ${constraint}`)
96
- .join('\n')}
95
+ ${toBulletedList(getSharedConstraints())}
97
96
  `;
98
97
  }
99
98
  export function getSharedConstraints() {
@@ -1,3 +1,4 @@
1
+ import { toBulletedList } from '../lib/markdown.js';
1
2
  import { getToolContracts } from '../lib/tool-contracts.js';
2
3
  import { getSharedConstraints } from './tool-info.js';
3
4
  function buildWorkflowToolReference() {
@@ -47,9 +48,7 @@ export function buildWorkflowGuide() {
47
48
  > Use for algorithm or API changes. Diff-only input.
48
49
 
49
50
  ## Shared Constraints
50
- ${getSharedConstraints()
51
- .map((constraint) => `- ${constraint}`)
52
- .join('\n')}
51
+ ${toBulletedList(getSharedConstraints())}
53
52
 
54
53
  ## Tool Reference
55
54
 
@@ -26,17 +26,19 @@ function createRepositorySchema() {
26
26
  function createOptionalBoundedInteger(min, max, description) {
27
27
  return z.number().int().min(min).max(max).optional().describe(description);
28
28
  }
29
+ const RepositorySchema = createRepositorySchema();
30
+ const LanguageSchema = createLanguageSchema();
29
31
  export const AnalyzePrImpactInputSchema = z.strictObject({
30
- repository: createRepositorySchema(),
31
- language: createLanguageSchema(),
32
+ repository: RepositorySchema,
33
+ language: LanguageSchema,
32
34
  });
33
35
  export const GenerateReviewSummaryInputSchema = z.strictObject({
34
- repository: createRepositorySchema(),
35
- language: createLanguageSchema(),
36
+ repository: RepositorySchema,
37
+ language: LanguageSchema,
36
38
  });
37
39
  export const InspectCodeQualityInputSchema = z.strictObject({
38
- repository: createRepositorySchema(),
39
- language: createLanguageSchema(),
40
+ repository: RepositorySchema,
41
+ language: LanguageSchema,
40
42
  focusAreas: z
41
43
  .array(createBoundedString(INPUT_LIMITS.focusArea.min, INPUT_LIMITS.focusArea.max, 'Focus tag (e.g. security, logic).'))
42
44
  .min(1)
@@ -50,14 +52,14 @@ export const SuggestSearchReplaceInputSchema = z.strictObject({
50
52
  findingDetails: createBoundedString(INPUT_LIMITS.findingDetails.min, INPUT_LIMITS.findingDetails.max, 'Exact finding explanation from inspect_code_quality.'),
51
53
  });
52
54
  export const GenerateTestPlanInputSchema = z.strictObject({
53
- repository: createRepositorySchema(),
54
- language: createLanguageSchema(),
55
+ repository: RepositorySchema,
56
+ language: LanguageSchema,
55
57
  testFramework: createOptionalBoundedString(INPUT_LIMITS.testFramework.min, INPUT_LIMITS.testFramework.max, 'Test framework (jest, pytest, etc). Auto-infer.'),
56
58
  maxTestCases: createOptionalBoundedInteger(INPUT_LIMITS.maxTestCases.min, INPUT_LIMITS.maxTestCases.max, 'Max test cases (1-30). Default: 15.'),
57
59
  });
58
60
  export const AnalyzeComplexityInputSchema = z.strictObject({
59
- language: createLanguageSchema(),
61
+ language: LanguageSchema,
60
62
  });
61
63
  export const DetectApiBreakingInputSchema = z.strictObject({
62
- language: createLanguageSchema(),
64
+ language: LanguageSchema,
63
65
  });
@@ -28,6 +28,7 @@ const OUTPUT_LIMITS = {
28
28
  };
29
29
  const QUALITY_RISK_LEVELS = ['low', 'medium', 'high', 'critical'];
30
30
  const MERGE_RISK_LEVELS = ['low', 'medium', 'high'];
31
+ const REVIEW_SUMMARY_LIMITS = OUTPUT_LIMITS.reviewDiffResult.summary;
31
32
  const ERROR_KINDS = [
32
33
  'validation',
33
34
  'budget',
@@ -49,8 +50,8 @@ function createBoundedStringArray(itemMin, itemMax, minItems, maxItems, descript
49
50
  function createReviewSummarySchema(description) {
50
51
  return z
51
52
  .string()
52
- .min(OUTPUT_LIMITS.reviewDiffResult.summary.min)
53
- .max(OUTPUT_LIMITS.reviewDiffResult.summary.max)
53
+ .min(REVIEW_SUMMARY_LIMITS.min)
54
+ .max(REVIEW_SUMMARY_LIMITS.max)
54
55
  .describe(description);
55
56
  }
56
57
  const reviewFindingSeveritySchema = z
package/dist/server.js CHANGED
@@ -14,6 +14,11 @@ const UTF8_ENCODING = 'utf8';
14
14
  const PackageJsonSchema = z.object({
15
15
  version: z.string().min(1),
16
16
  });
17
+ const TASK_TOOL_CALL_CAPABILITY = {
18
+ tools: {
19
+ call: {},
20
+ },
21
+ };
17
22
  const SERVER_CAPABILITIES = {
18
23
  logging: {},
19
24
  completions: {},
@@ -23,11 +28,7 @@ const SERVER_CAPABILITIES = {
23
28
  tasks: {
24
29
  list: {},
25
30
  cancel: {},
26
- requests: {
27
- tools: {
28
- call: {},
29
- },
30
- },
31
+ requests: TASK_TOOL_CALL_CAPABILITY,
31
32
  },
32
33
  };
33
34
  function readUtf8File(path) {
@@ -1,3 +1,4 @@
1
+ import { formatLanguageSegment } from '../lib/format.js';
1
2
  import { buildStructuredToolRuntimeOptions, requireToolContract, } from '../lib/tool-contracts.js';
2
3
  import { registerStructuredToolTask } from '../lib/tool-factory.js';
3
4
  import { AnalyzeComplexityInputSchema } from '../schemas/inputs.js';
@@ -42,9 +43,7 @@ export function registerAnalyzeComplexityTool(server) {
42
43
  formatOutput: (result) => `Time=${result.timeComplexity}, Space=${result.spaceComplexity}. ${result.explanation}`,
43
44
  buildPrompt: (input, ctx) => {
44
45
  const diff = ctx.diffSlot?.diff ?? '';
45
- const languageLine = input.language
46
- ? `\nLanguage: ${input.language}`
47
- : '';
46
+ const languageLine = formatLanguageSegment(input.language);
48
47
  return {
49
48
  systemInstruction: SYSTEM_INSTRUCTION,
50
49
  prompt: `${languageLine}\nDiff:\n${diff}\n\nBased on the diff above, analyze the Big-O time and space complexity.`.trimStart(),
@@ -1,4 +1,5 @@
1
1
  import { computeDiffStatsAndSummaryFromFiles } from '../lib/diff.js';
2
+ import { formatLanguageSegment } from '../lib/format.js';
2
3
  import { buildStructuredToolRuntimeOptions, requireToolContract, } from '../lib/tool-contracts.js';
3
4
  import { registerStructuredToolTask } from '../lib/tool-factory.js';
4
5
  import { AnalyzePrImpactInputSchema } from '../schemas/inputs.js';
@@ -24,9 +25,6 @@ Analyze the unified diff to assess:
24
25
  </constraints>
25
26
  `;
26
27
  const TOOL_CONTRACT = requireToolContract('analyze_pr_impact');
27
- function formatLanguageSegment(language) {
28
- return language ? `\nLanguage: ${language}` : '';
29
- }
30
28
  export function registerAnalyzePrImpactTool(server) {
31
29
  registerStructuredToolTask(server, {
32
30
  name: 'analyze_pr_impact',
@@ -1,3 +1,4 @@
1
+ import { formatLanguageSegment } from '../lib/format.js';
1
2
  import { buildStructuredToolRuntimeOptions, requireToolContract, } from '../lib/tool-contracts.js';
2
3
  import { registerStructuredToolTask } from '../lib/tool-factory.js';
3
4
  import { DetectApiBreakingInputSchema } from '../schemas/inputs.js';
@@ -41,9 +42,7 @@ export function registerDetectApiBreakingTool(server) {
41
42
  : 'No breaking changes.',
42
43
  buildPrompt: (input, ctx) => {
43
44
  const diff = ctx.diffSlot?.diff ?? '';
44
- const languageLine = input.language
45
- ? `\nLanguage: ${input.language}`
46
- : '';
45
+ const languageLine = formatLanguageSegment(input.language);
47
46
  return {
48
47
  systemInstruction: SYSTEM_INSTRUCTION,
49
48
  prompt: `${languageLine}\nDiff:\n${diff}\n\nBased on the diff above, detect any breaking API changes.`.trimStart(),
@@ -38,6 +38,13 @@ function describeModeHint(mode) {
38
38
  ? 'staged with git add'
39
39
  : 'modified but not yet staged (git add)';
40
40
  }
41
+ function formatGitFailureMessage(err) {
42
+ if (typeof err.code === 'number') {
43
+ const stderr = err.stderr?.trim() ?? 'unknown error';
44
+ return `git exited with code ${String(err.code)}: ${stderr}. Ensure the working directory is a git repository.`;
45
+ }
46
+ return `Failed to run git: ${err.message}. Ensure git is installed and the working directory is a git repository.`;
47
+ }
41
48
  export function registerGenerateDiffTool(server) {
42
49
  server.registerTool('generate_diff', {
43
50
  title: 'Generate Diff',
@@ -90,11 +97,7 @@ export function registerGenerateDiffTool(server) {
90
97
  }
91
98
  catch (error) {
92
99
  const err = error;
93
- if (err.code && typeof err.code === 'number') {
94
- const stderr = err.stderr ? err.stderr.trim() : '';
95
- return createErrorToolResponse('E_GENERATE_DIFF', `git exited with code ${String(err.code)}: ${stderr || 'unknown error'}. Ensure the working directory is a git repository.`, undefined, { retryable: false, kind: 'internal' });
96
- }
97
- return createErrorToolResponse('E_GENERATE_DIFF', `Failed to run git: ${err.message}. Ensure git is installed and the working directory is a git repository.`, undefined, { retryable: false, kind: 'internal' });
100
+ return createErrorToolResponse('E_GENERATE_DIFF', formatGitFailureMessage(err), undefined, { retryable: false, kind: 'internal' });
98
101
  }
99
102
  }));
100
103
  }
@@ -1,3 +1,4 @@
1
+ import { formatLanguageSegment } from '../lib/format.js';
1
2
  import { buildStructuredToolRuntimeOptions, requireToolContract, } from '../lib/tool-contracts.js';
2
3
  import { registerStructuredToolTask, } from '../lib/tool-factory.js';
3
4
  import { GenerateReviewSummaryInputSchema } from '../schemas/inputs.js';
@@ -25,9 +26,6 @@ Summarize the pull request based on the diff:
25
26
  - Return valid JSON matching the schema.
26
27
  </constraints>
27
28
  `;
28
- function formatLanguageSegment(language) {
29
- return language ? `\nLanguage: ${language}` : '';
30
- }
31
29
  function getDiffStats(ctx) {
32
30
  const slot = ctx.diffSlot;
33
31
  if (!slot) {
@@ -1,4 +1,5 @@
1
1
  import { computeDiffStatsAndPathsFromFiles } from '../lib/diff.js';
2
+ import { formatOptionalLine } from '../lib/format.js';
2
3
  import { buildStructuredToolRuntimeOptions, requireToolContract, } from '../lib/tool-contracts.js';
3
4
  import { registerStructuredToolTask } from '../lib/tool-factory.js';
4
5
  import { GenerateTestPlanInputSchema } from '../schemas/inputs.js';
@@ -22,9 +23,6 @@ Generate a prioritized test plan for the provided diff:
22
23
  </constraints>
23
24
  `;
24
25
  const TOOL_CONTRACT = requireToolContract('generate_test_plan');
25
- function formatOptionalLine(label, value) {
26
- return value === undefined ? '' : `\n${label}: ${value}`;
27
- }
28
26
  export function registerGenerateTestPlanTool(server) {
29
27
  registerStructuredToolTask(server, {
30
28
  name: 'generate_test_plan',
@@ -17,7 +17,7 @@ const TOOL_REGISTRARS = [
17
17
  registerDetectApiBreakingTool,
18
18
  ];
19
19
  export function registerAllTools(server) {
20
- for (const registrar of TOOL_REGISTRARS) {
20
+ TOOL_REGISTRARS.forEach((registrar) => {
21
21
  registrar(server);
22
- }
22
+ });
23
23
  }
@@ -1,4 +1,5 @@
1
1
  import { computeDiffStatsAndSummaryFromFiles } from '../lib/diff.js';
2
+ import { formatOptionalLine } from '../lib/format.js';
2
3
  import { buildStructuredToolRuntimeOptions, requireToolContract, } from '../lib/tool-contracts.js';
3
4
  import { registerStructuredToolTask } from '../lib/tool-factory.js';
4
5
  import { InspectCodeQualityInputSchema } from '../schemas/inputs.js';
@@ -25,9 +26,6 @@ Perform a deep code review of the provided diff:
25
26
  </constraints>
26
27
  `;
27
28
  const TOOL_CONTRACT = requireToolContract('inspect_code_quality');
28
- function formatOptionalLine(label, value) {
29
- return value === undefined ? '' : `\n${label}: ${value}`;
30
- }
31
29
  function capFindings(findings, maxFindings) {
32
30
  return findings.slice(0, maxFindings ?? findings.length);
33
31
  }
@@ -1,4 +1,5 @@
1
1
  import { extractChangedPathsFromFiles } from '../lib/diff.js';
2
+ import { formatCountLabel } from '../lib/format.js';
2
3
  import { buildStructuredToolRuntimeOptions, requireToolContract, } from '../lib/tool-contracts.js';
3
4
  import { registerStructuredToolTask } from '../lib/tool-factory.js';
4
5
  import { SuggestSearchReplaceInputSchema } from '../schemas/inputs.js';
@@ -24,7 +25,7 @@ Generate minimal search-and-replace blocks to fix the described issue:
24
25
  `;
25
26
  const TOOL_CONTRACT = requireToolContract('suggest_search_replace');
26
27
  function formatPatchCount(count) {
27
- return `${count} ${count === 1 ? 'patch' : 'patches'}`;
28
+ return formatCountLabel(count, 'patch', 'patches');
28
29
  }
29
30
  export function registerSuggestSearchReplaceTool(server) {
30
31
  registerStructuredToolTask(server, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@j0hanz/code-review-analyst-mcp",
3
- "version": "1.7.3",
3
+ "version": "1.7.5",
4
4
  "mcpName": "io.github.j0hanz/code-review-analyst",
5
5
  "description": "Gemini-powered MCP server for code review analysis.",
6
6
  "type": "module",