@j0hanz/code-review-analyst-mcp 1.7.4 → 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 +15 -20
  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
  };
@@ -395,13 +394,11 @@ export class ToolExecutionRunner {
395
394
  const details = asObjectRecord(record.details);
396
395
  const { attempt } = details;
397
396
  const msg = `Network error. Retrying (attempt ${String(attempt)})...`;
398
- await reportProgressStepUpdate(this.reportProgress, this.config.name, this.progressContext, STEP_CALLING_MODEL, msg);
399
- await this.updateStatusMessage(msg);
397
+ await this.reportAndStatus(STEP_CALLING_MODEL, msg);
400
398
  }
401
399
  else if (record.event === 'gemini_queue_acquired') {
402
400
  const msg = 'Model queue acquired, generating response...';
403
- await reportProgressStepUpdate(this.reportProgress, this.config.name, this.progressContext, STEP_CALLING_MODEL, msg);
404
- await this.updateStatusMessage(msg);
401
+ await this.reportAndStatus(STEP_CALLING_MODEL, msg);
405
402
  }
406
403
  }
407
404
  setResponseSchemaOverride(responseSchema) {
@@ -469,8 +466,7 @@ export class ToolExecutionRunner {
469
466
  try {
470
467
  const raw = await generateStructuredJson(createGenerationRequest(this.config, { systemInstruction, prompt: retryPrompt }, this.responseSchema, this.onLog, this.signal));
471
468
  if (attempt === 0) {
472
- await this.updateStatusMessage('Verifying output structure...');
473
- 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...');
474
470
  }
475
471
  parsed = this.config.resultSchema.parse(raw);
476
472
  break;
@@ -498,6 +494,10 @@ export class ToolExecutionRunner {
498
494
  }
499
495
  return parsed;
500
496
  }
497
+ async reportAndStatus(step, message) {
498
+ await reportProgressStepUpdate(this.reportProgress, this.config.name, this.progressContext, step, message);
499
+ await this.updateStatusMessage(message);
500
+ }
501
501
  async run(input) {
502
502
  try {
503
503
  const inputRecord = parseToolInput(input, this.config.fullInputSchema);
@@ -505,23 +505,18 @@ export class ToolExecutionRunner {
505
505
  const ctx = {
506
506
  diffSlot: this.hasSnapshot ? this.diffSlotSnapshot : getDiff(),
507
507
  };
508
- await reportProgressStepUpdate(this.reportProgress, this.config.name, this.progressContext, STEP_STARTING, 'Initializing...');
509
- await this.updateStatusMessage('Initializing...');
510
- await reportProgressStepUpdate(this.reportProgress, this.config.name, this.progressContext, STEP_VALIDATING, 'Validating request parameters...');
511
- await this.updateStatusMessage('Validating request parameters...');
508
+ await this.reportAndStatus(STEP_STARTING, 'Initializing...');
509
+ await this.reportAndStatus(STEP_VALIDATING, 'Validating request parameters...');
512
510
  const validationError = await this.executeValidation(inputRecord, ctx);
513
511
  if (validationError) {
514
512
  return validationError;
515
513
  }
516
- await reportProgressStepUpdate(this.reportProgress, this.config.name, this.progressContext, STEP_BUILDING_PROMPT, 'Constructing analysis context...');
517
- await this.updateStatusMessage('Constructing analysis context...');
514
+ await this.reportAndStatus(STEP_BUILDING_PROMPT, 'Constructing analysis context...');
518
515
  const promptParts = this.config.buildPrompt(inputRecord, ctx);
519
516
  const { prompt, systemInstruction } = promptParts;
520
- await reportProgressStepUpdate(this.reportProgress, this.config.name, this.progressContext, STEP_CALLING_MODEL, 'Querying Gemini model...');
521
- await this.updateStatusMessage('Querying Gemini model...');
517
+ await this.reportAndStatus(STEP_CALLING_MODEL, 'Querying Gemini model...');
522
518
  const parsed = await this.executeModelCall(systemInstruction, prompt);
523
- await reportProgressStepUpdate(this.reportProgress, this.config.name, this.progressContext, STEP_FINALIZING, 'Processing results...');
524
- await this.updateStatusMessage('Processing results...');
519
+ await this.reportAndStatus(STEP_FINALIZING, 'Processing results...');
525
520
  const finalResult = (this.config.transformResult
526
521
  ? this.config.transformResult(inputRecord, parsed, ctx)
527
522
  : parsed);
@@ -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.4",
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",