@j0hanz/code-review-analyst-mcp 1.2.1 → 1.3.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.
Files changed (49) hide show
  1. package/dist/index.js +12 -3
  2. package/dist/instructions.md +4 -146
  3. package/dist/lib/context-budget.d.ts +2 -2
  4. package/dist/lib/context-budget.js +12 -6
  5. package/dist/lib/diff-budget.js +6 -2
  6. package/dist/lib/diff-parser.js +31 -36
  7. package/dist/lib/env-config.d.ts +1 -0
  8. package/dist/lib/env-config.js +9 -3
  9. package/dist/lib/errors.d.ts +1 -0
  10. package/dist/lib/errors.js +5 -5
  11. package/dist/lib/gemini-schema.js +2 -1
  12. package/dist/lib/gemini.js +135 -67
  13. package/dist/lib/model-config.d.ts +14 -2
  14. package/dist/lib/model-config.js +30 -6
  15. package/dist/lib/tool-contracts.d.ts +245 -0
  16. package/dist/lib/tool-contracts.js +302 -0
  17. package/dist/lib/tool-factory.d.ts +5 -1
  18. package/dist/lib/tool-factory.js +48 -54
  19. package/dist/lib/tool-response.js +10 -12
  20. package/dist/lib/types.d.ts +3 -3
  21. package/dist/prompts/index.js +47 -41
  22. package/dist/resources/index.d.ts +1 -1
  23. package/dist/resources/index.js +80 -18
  24. package/dist/resources/instructions.d.ts +1 -0
  25. package/dist/resources/instructions.js +59 -0
  26. package/dist/resources/server-config.d.ts +1 -0
  27. package/dist/resources/server-config.js +70 -0
  28. package/dist/resources/tool-catalog.d.ts +1 -0
  29. package/dist/resources/tool-catalog.js +39 -0
  30. package/dist/resources/tool-info.d.ts +5 -0
  31. package/dist/resources/tool-info.js +122 -0
  32. package/dist/resources/workflows.d.ts +1 -0
  33. package/dist/resources/workflows.js +72 -0
  34. package/dist/schemas/inputs.d.ts +8 -0
  35. package/dist/schemas/inputs.js +20 -18
  36. package/dist/schemas/outputs.d.ts +17 -1
  37. package/dist/schemas/outputs.js +84 -52
  38. package/dist/server.js +25 -26
  39. package/dist/tools/analyze-complexity.d.ts +2 -0
  40. package/dist/tools/analyze-complexity.js +45 -0
  41. package/dist/tools/analyze-pr-impact.js +30 -25
  42. package/dist/tools/detect-api-breaking.d.ts +2 -0
  43. package/dist/tools/detect-api-breaking.js +42 -0
  44. package/dist/tools/generate-review-summary.js +26 -20
  45. package/dist/tools/generate-test-plan.js +34 -28
  46. package/dist/tools/index.js +9 -2
  47. package/dist/tools/inspect-code-quality.js +46 -40
  48. package/dist/tools/suggest-search-replace.js +34 -27
  49. package/package.json +1 -1
@@ -0,0 +1,302 @@
1
+ import { DEFAULT_TIMEOUT_PRO_MS, FLASH_API_BREAKING_MAX_OUTPUT_TOKENS, FLASH_COMPLEXITY_MAX_OUTPUT_TOKENS, FLASH_MODEL, FLASH_TEST_PLAN_MAX_OUTPUT_TOKENS, FLASH_THINKING_BUDGET, FLASH_TRIAGE_MAX_OUTPUT_TOKENS, PRO_MODEL, PRO_PATCH_MAX_OUTPUT_TOKENS, PRO_REVIEW_MAX_OUTPUT_TOKENS, PRO_THINKING_BUDGET, } from './model-config.js';
2
+ const DEFAULT_TIMEOUT_FLASH_MS = 90_000;
3
+ export const TOOL_CONTRACTS = [
4
+ {
5
+ name: 'analyze_pr_impact',
6
+ purpose: 'Assess severity, categories, breaking changes, and rollback complexity.',
7
+ model: FLASH_MODEL,
8
+ timeoutMs: DEFAULT_TIMEOUT_FLASH_MS,
9
+ maxOutputTokens: FLASH_TRIAGE_MAX_OUTPUT_TOKENS,
10
+ params: [
11
+ {
12
+ name: 'diff',
13
+ type: 'string',
14
+ required: true,
15
+ constraints: '10-120K chars',
16
+ description: 'Unified diff text.',
17
+ },
18
+ {
19
+ name: 'repository',
20
+ type: 'string',
21
+ required: true,
22
+ constraints: '1-200 chars',
23
+ description: 'Repository identifier (org/repo).',
24
+ },
25
+ {
26
+ name: 'language',
27
+ type: 'string',
28
+ required: false,
29
+ constraints: '2-32 chars',
30
+ description: 'Primary language hint.',
31
+ },
32
+ ],
33
+ outputShape: '{severity, categories[], summary, breakingChanges[], affectedAreas[], rollbackComplexity}',
34
+ gotchas: [
35
+ 'Flash triage tool optimized for speed.',
36
+ 'Diff-only analysis (no full-file context).',
37
+ ],
38
+ crossToolFlow: [
39
+ 'severity/categories feed triage and merge-gate decisions.',
40
+ ],
41
+ },
42
+ {
43
+ name: 'generate_review_summary',
44
+ purpose: 'Produce PR summary, risk rating, and merge recommendation.',
45
+ model: FLASH_MODEL,
46
+ timeoutMs: DEFAULT_TIMEOUT_FLASH_MS,
47
+ maxOutputTokens: FLASH_TRIAGE_MAX_OUTPUT_TOKENS,
48
+ params: [
49
+ {
50
+ name: 'diff',
51
+ type: 'string',
52
+ required: true,
53
+ constraints: '10-120K chars',
54
+ description: 'Unified diff text.',
55
+ },
56
+ {
57
+ name: 'repository',
58
+ type: 'string',
59
+ required: true,
60
+ constraints: '1-200 chars',
61
+ description: 'Repository identifier (org/repo).',
62
+ },
63
+ {
64
+ name: 'language',
65
+ type: 'string',
66
+ required: false,
67
+ constraints: '2-32 chars',
68
+ description: 'Primary language hint.',
69
+ },
70
+ ],
71
+ outputShape: '{summary, overallRisk, keyChanges[], recommendation, stats{filesChanged, linesAdded, linesRemoved}}',
72
+ gotchas: [
73
+ 'stats are computed locally from the diff.',
74
+ 'Flash triage tool optimized for speed.',
75
+ ],
76
+ crossToolFlow: [
77
+ 'Use before deep review to decide whether Pro analysis is needed.',
78
+ ],
79
+ },
80
+ {
81
+ name: 'inspect_code_quality',
82
+ purpose: 'Deep code review with optional full-file context.',
83
+ model: PRO_MODEL,
84
+ timeoutMs: DEFAULT_TIMEOUT_PRO_MS,
85
+ thinkingBudget: PRO_THINKING_BUDGET,
86
+ maxOutputTokens: PRO_REVIEW_MAX_OUTPUT_TOKENS,
87
+ params: [
88
+ {
89
+ name: 'diff',
90
+ type: 'string',
91
+ required: true,
92
+ constraints: '10-120K chars',
93
+ description: 'Unified diff text.',
94
+ },
95
+ {
96
+ name: 'repository',
97
+ type: 'string',
98
+ required: true,
99
+ constraints: '1-200 chars',
100
+ description: 'Repository identifier (org/repo).',
101
+ },
102
+ {
103
+ name: 'language',
104
+ type: 'string',
105
+ required: false,
106
+ constraints: '2-32 chars',
107
+ description: 'Primary language hint.',
108
+ },
109
+ {
110
+ name: 'focusAreas',
111
+ type: 'string[]',
112
+ required: false,
113
+ constraints: '1-12 items, 2-80 chars each',
114
+ description: 'Focused inspection categories.',
115
+ },
116
+ {
117
+ name: 'maxFindings',
118
+ type: 'number',
119
+ required: false,
120
+ constraints: '1-25',
121
+ description: 'Post-generation cap applied to findings.',
122
+ },
123
+ {
124
+ name: 'files',
125
+ type: 'object[]',
126
+ required: false,
127
+ constraints: '1-20 files, 100K chars/file',
128
+ description: 'Optional full file content context.',
129
+ },
130
+ ],
131
+ outputShape: '{summary, overallRisk, findings[], testsNeeded[], contextualInsights[], totalFindings}',
132
+ gotchas: [
133
+ 'Combined diff + file context is bounded by MAX_CONTEXT_CHARS.',
134
+ 'maxFindings caps output after generation.',
135
+ ],
136
+ crossToolFlow: [
137
+ 'findings[].title -> suggest_search_replace.findingTitle',
138
+ 'findings[].explanation -> suggest_search_replace.findingDetails',
139
+ ],
140
+ constraints: ['Context budget (diff + files) < 500K chars.'],
141
+ },
142
+ {
143
+ name: 'suggest_search_replace',
144
+ purpose: 'Generate verbatim search/replace fix blocks for one finding.',
145
+ model: PRO_MODEL,
146
+ timeoutMs: DEFAULT_TIMEOUT_PRO_MS,
147
+ thinkingBudget: PRO_THINKING_BUDGET,
148
+ maxOutputTokens: PRO_PATCH_MAX_OUTPUT_TOKENS,
149
+ params: [
150
+ {
151
+ name: 'diff',
152
+ type: 'string',
153
+ required: true,
154
+ constraints: '10-120K chars',
155
+ description: 'Unified diff containing the target issue.',
156
+ },
157
+ {
158
+ name: 'findingTitle',
159
+ type: 'string',
160
+ required: true,
161
+ constraints: '3-160 chars',
162
+ description: 'Short finding title.',
163
+ },
164
+ {
165
+ name: 'findingDetails',
166
+ type: 'string',
167
+ required: true,
168
+ constraints: '10-3000 chars',
169
+ description: 'Detailed finding context.',
170
+ },
171
+ ],
172
+ outputShape: '{summary, blocks[], validationChecklist[]}',
173
+ gotchas: [
174
+ 'One finding per call to avoid mixed patch intent.',
175
+ 'search must be exact whitespace-preserving match.',
176
+ ],
177
+ crossToolFlow: [
178
+ 'Consumes findings from inspect_code_quality for targeted fixes.',
179
+ ],
180
+ constraints: ['One finding per call; verbatim search match required.'],
181
+ },
182
+ {
183
+ name: 'generate_test_plan',
184
+ purpose: 'Generate prioritized test cases and coverage guidance.',
185
+ model: FLASH_MODEL,
186
+ timeoutMs: DEFAULT_TIMEOUT_FLASH_MS,
187
+ thinkingBudget: FLASH_THINKING_BUDGET,
188
+ maxOutputTokens: FLASH_TEST_PLAN_MAX_OUTPUT_TOKENS,
189
+ params: [
190
+ {
191
+ name: 'diff',
192
+ type: 'string',
193
+ required: true,
194
+ constraints: '10-120K chars',
195
+ description: 'Unified diff text.',
196
+ },
197
+ {
198
+ name: 'repository',
199
+ type: 'string',
200
+ required: true,
201
+ constraints: '1-200 chars',
202
+ description: 'Repository identifier (org/repo).',
203
+ },
204
+ {
205
+ name: 'language',
206
+ type: 'string',
207
+ required: false,
208
+ constraints: '2-32 chars',
209
+ description: 'Primary language hint.',
210
+ },
211
+ {
212
+ name: 'testFramework',
213
+ type: 'string',
214
+ required: false,
215
+ constraints: '1-50 chars',
216
+ description: 'Framework hint (jest, vitest, pytest, node:test).',
217
+ },
218
+ {
219
+ name: 'maxTestCases',
220
+ type: 'number',
221
+ required: false,
222
+ constraints: '1-30',
223
+ description: 'Post-generation cap applied to test cases.',
224
+ },
225
+ ],
226
+ outputShape: '{summary, testCases[], coverageSummary}',
227
+ gotchas: ['maxTestCases caps output after generation.'],
228
+ crossToolFlow: [
229
+ 'Pair with inspect_code_quality to validate high-risk paths.',
230
+ ],
231
+ },
232
+ {
233
+ name: 'analyze_time_space_complexity',
234
+ purpose: 'Analyze Big-O complexity and detect degradations in changed code.',
235
+ model: FLASH_MODEL,
236
+ timeoutMs: DEFAULT_TIMEOUT_FLASH_MS,
237
+ thinkingBudget: FLASH_THINKING_BUDGET,
238
+ maxOutputTokens: FLASH_COMPLEXITY_MAX_OUTPUT_TOKENS,
239
+ params: [
240
+ {
241
+ name: 'diff',
242
+ type: 'string',
243
+ required: true,
244
+ constraints: '10-120K chars',
245
+ description: 'Unified diff text.',
246
+ },
247
+ {
248
+ name: 'language',
249
+ type: 'string',
250
+ required: false,
251
+ constraints: '2-32 chars',
252
+ description: 'Primary language hint.',
253
+ },
254
+ ],
255
+ outputShape: '{timeComplexity, spaceComplexity, explanation, potentialBottlenecks[], isDegradation}',
256
+ gotchas: ['Analyzes only changed code visible in the diff.'],
257
+ crossToolFlow: ['Use for algorithmic/performance-sensitive changes.'],
258
+ },
259
+ {
260
+ name: 'detect_api_breaking_changes',
261
+ purpose: 'Detect breaking API/interface changes in a diff.',
262
+ model: FLASH_MODEL,
263
+ timeoutMs: DEFAULT_TIMEOUT_FLASH_MS,
264
+ maxOutputTokens: FLASH_API_BREAKING_MAX_OUTPUT_TOKENS,
265
+ params: [
266
+ {
267
+ name: 'diff',
268
+ type: 'string',
269
+ required: true,
270
+ constraints: '10-120K chars',
271
+ description: 'Unified diff text.',
272
+ },
273
+ {
274
+ name: 'language',
275
+ type: 'string',
276
+ required: false,
277
+ constraints: '2-32 chars',
278
+ description: 'Primary language hint.',
279
+ },
280
+ ],
281
+ outputShape: '{hasBreakingChanges, breakingChanges[]}',
282
+ gotchas: ['Targets public API contracts over internal refactors.'],
283
+ crossToolFlow: ['Run before merge for API-surface-sensitive changes.'],
284
+ },
285
+ ];
286
+ const TOOL_CONTRACTS_BY_NAME = new Map(TOOL_CONTRACTS.map((contract) => [contract.name, contract]));
287
+ export function getToolContracts() {
288
+ return TOOL_CONTRACTS;
289
+ }
290
+ export function getToolContract(toolName) {
291
+ return TOOL_CONTRACTS_BY_NAME.get(toolName);
292
+ }
293
+ export function requireToolContract(toolName) {
294
+ const contract = getToolContract(toolName);
295
+ if (contract) {
296
+ return contract;
297
+ }
298
+ throw new Error(`Unknown tool contract: ${toolName}`);
299
+ }
300
+ export function getToolContractNames() {
301
+ return TOOL_CONTRACTS.map((contract) => contract.name);
302
+ }
@@ -31,8 +31,12 @@ export interface StructuredToolTaskConfig<TInput extends object = Record<string,
31
31
  model?: string;
32
32
  /** Optional thinking budget in tokens. */
33
33
  thinkingBudget?: number;
34
- /** Optional timeout in ms for the Gemini call. Defaults to 60,000 ms. Use DEFAULT_TIMEOUT_PRO_MS for Pro model calls. */
34
+ /** Optional timeout in ms for the Gemini call. Defaults to 90,000 ms. Use DEFAULT_TIMEOUT_PRO_MS for Pro model calls. */
35
35
  timeoutMs?: number;
36
+ /** Optional max output tokens for Gemini. */
37
+ maxOutputTokens?: number;
38
+ /** Optional opt-in to Gemini thought output. Defaults to false. */
39
+ includeThoughts?: boolean;
36
40
  /** Optional formatter for human-readable text output. */
37
41
  formatOutput?: (result: TFinal) => string;
38
42
  /** Optional context text used in progress messages. */
@@ -12,6 +12,7 @@ const CANCELLED_ERROR_PATTERN = /cancelled|canceled/i;
12
12
  const TIMEOUT_ERROR_PATTERN = /timed out|timeout/i;
13
13
  const BUDGET_ERROR_PATTERN = /exceeds limit|max allowed size|input too large/i;
14
14
  const BUSY_ERROR_PATTERN = /too many concurrent/i;
15
+ const MAX_SCHEMA_RETRIES = 1;
15
16
  function createGeminiResponseSchema(config) {
16
17
  const sourceSchema = config.geminiSchema ?? config.resultSchema;
17
18
  return stripJsonSchemaConstraints(z.toJSONSchema(sourceSchema));
@@ -35,6 +36,12 @@ function createGenerationRequest(config, promptParts, responseSchema, onLog, sig
35
36
  if (config.timeoutMs !== undefined) {
36
37
  request.timeoutMs = config.timeoutMs;
37
38
  }
39
+ if (config.maxOutputTokens !== undefined) {
40
+ request.maxOutputTokens = config.maxOutputTokens;
41
+ }
42
+ if (config.includeThoughts !== undefined) {
43
+ request.includeThoughts = config.includeThoughts;
44
+ }
38
45
  if (signal !== undefined) {
39
46
  request.signal = signal;
40
47
  }
@@ -65,13 +72,7 @@ function classifyErrorMeta(error, message) {
65
72
  retryable: false,
66
73
  };
67
74
  }
68
- if (RETRYABLE_UPSTREAM_ERROR_PATTERN.test(message)) {
69
- return {
70
- kind: 'upstream',
71
- retryable: true,
72
- };
73
- }
74
- if (BUSY_ERROR_PATTERN.test(message)) {
75
+ if (isRetryableUpstreamMessage(message)) {
75
76
  return {
76
77
  kind: 'upstream',
77
78
  retryable: true,
@@ -82,6 +83,10 @@ function classifyErrorMeta(error, message) {
82
83
  retryable: false,
83
84
  };
84
85
  }
86
+ function isRetryableUpstreamMessage(message) {
87
+ return (RETRYABLE_UPSTREAM_ERROR_PATTERN.test(message) ||
88
+ BUSY_ERROR_PATTERN.test(message));
89
+ }
85
90
  async function sendTaskProgress(extra, payload) {
86
91
  const progressToken = extra._meta?.progressToken;
87
92
  if (typeof progressToken !== 'string' && typeof progressToken !== 'number') {
@@ -118,11 +123,14 @@ function createProgressReporter(extra) {
118
123
  const total = payload.total !== undefined
119
124
  ? Math.max(payload.total, current)
120
125
  : undefined;
121
- await sendTaskProgress(extra, {
122
- current,
123
- ...(total !== undefined ? { total } : {}),
124
- ...(payload.message !== undefined ? { message: payload.message } : {}),
125
- });
126
+ const progressPayload = { current };
127
+ if (total !== undefined) {
128
+ progressPayload.total = total;
129
+ }
130
+ if (payload.message !== undefined) {
131
+ progressPayload.message = payload.message;
132
+ }
133
+ await sendTaskProgress(extra, progressPayload);
126
134
  lastCurrent = current;
127
135
  if (total !== undefined && total === current) {
128
136
  didSendTerminal = true;
@@ -140,7 +148,7 @@ function normalizeProgressContext(context) {
140
148
  return `${compact.slice(0, 77)}...`;
141
149
  }
142
150
  function formatProgressStep(toolName, context, metadata) {
143
- const prefix = metadata === 'start' ? '▸' : '';
151
+ const prefix = metadata === 'starting' ? '▸' : '';
144
152
  return `${prefix} ${toolName}: ${context} [${metadata}]`;
145
153
  }
146
154
  function friendlyModelName(model) {
@@ -153,9 +161,23 @@ function friendlyModelName(model) {
153
161
  return 'model';
154
162
  }
155
163
  function formatProgressCompletion(toolName, context, outcome, success = true) {
156
- const prefix = success ? '' : '';
164
+ const prefix = success ? '' : '';
157
165
  return `${prefix} ${toolName}: ${context} • ${outcome}`;
158
166
  }
167
+ async function reportProgressStepUpdate(reportProgress, toolName, context, current, metadata) {
168
+ await reportProgress({
169
+ current,
170
+ total: TASK_PROGRESS_TOTAL,
171
+ message: formatProgressStep(toolName, context, metadata),
172
+ });
173
+ }
174
+ async function reportProgressCompletionUpdate(reportProgress, toolName, context, outcome, success = true) {
175
+ await reportProgress({
176
+ current: TASK_PROGRESS_TOTAL,
177
+ total: TASK_PROGRESS_TOTAL,
178
+ message: formatProgressCompletion(toolName, context, outcome, success),
179
+ });
180
+ }
159
181
  function toLoggingLevel(level) {
160
182
  switch (level) {
161
183
  case 'debug':
@@ -207,6 +229,7 @@ export function registerStructuredToolTask(server, config) {
207
229
  outputSchema: DefaultOutputSchema,
208
230
  annotations: {
209
231
  readOnlyHint: true,
232
+ idempotentHint: true,
210
233
  openWorldHint: true,
211
234
  },
212
235
  }, {
@@ -237,52 +260,31 @@ export function registerStructuredToolTask(server, config) {
237
260
  const onLog = createGeminiLogger(server, task.taskId);
238
261
  const inputRecord = parseToolInput(input, config.fullInputSchema);
239
262
  progressContext = normalizeProgressContext(config.progressContext?.(inputRecord));
240
- await reportProgress({
241
- current: 0,
242
- total: TASK_PROGRESS_TOTAL,
243
- message: formatProgressStep(config.name, progressContext, 'starting'),
244
- });
263
+ await reportProgressStepUpdate(reportProgress, config.name, progressContext, 0, 'starting');
245
264
  if (config.validateInput) {
246
265
  const validationError = await config.validateInput(inputRecord);
247
266
  if (validationError) {
248
267
  const validationMessage = validationError.structuredContent.error?.message ??
249
268
  INPUT_VALIDATION_FAILED;
250
269
  await updateStatusMessage(validationMessage);
251
- await reportProgress({
252
- current: TASK_PROGRESS_TOTAL,
253
- total: TASK_PROGRESS_TOTAL,
254
- message: formatProgressCompletion(config.name, progressContext, 'rejected', false),
255
- });
270
+ await reportProgressCompletionUpdate(reportProgress, config.name, progressContext, 'rejected', false);
256
271
  await storeResultSafely('completed', validationError);
257
272
  return;
258
273
  }
259
274
  }
260
- await reportProgress({
261
- current: 1,
262
- total: TASK_PROGRESS_TOTAL,
263
- message: formatProgressStep(config.name, progressContext, 'preparing'),
264
- });
275
+ await reportProgressStepUpdate(reportProgress, config.name, progressContext, 1, 'preparing');
265
276
  const promptParts = config.buildPrompt(inputRecord);
266
- let { prompt } = promptParts;
277
+ const { prompt } = promptParts;
267
278
  const { systemInstruction } = promptParts;
268
279
  const modelLabel = friendlyModelName(config.model);
269
- await reportProgress({
270
- current: 2,
271
- total: TASK_PROGRESS_TOTAL,
272
- message: formatProgressStep(config.name, progressContext, `awaiting ${modelLabel}`),
273
- });
274
- const MAX_SCHEMA_RETRIES = 1;
275
- let raw;
280
+ await reportProgressStepUpdate(reportProgress, config.name, progressContext, 2, modelLabel);
276
281
  let parsed;
282
+ let retryPrompt = prompt;
277
283
  for (let attempt = 0; attempt <= MAX_SCHEMA_RETRIES; attempt += 1) {
278
284
  try {
279
- raw = await generateStructuredJson(createGenerationRequest(config, { systemInstruction, prompt }, responseSchema, onLog, extra.signal));
285
+ const raw = await generateStructuredJson(createGenerationRequest(config, { systemInstruction, prompt: retryPrompt }, responseSchema, onLog, extra.signal));
280
286
  if (attempt === 0) {
281
- await reportProgress({
282
- current: 3,
283
- total: TASK_PROGRESS_TOTAL,
284
- message: formatProgressStep(config.name, progressContext, 'processing response'),
285
- });
287
+ await reportProgressStepUpdate(reportProgress, config.name, progressContext, 3, 'processing response');
286
288
  }
287
289
  parsed = config.resultSchema.parse(raw);
288
290
  break;
@@ -297,7 +299,7 @@ export function registerStructuredToolTask(server, config) {
297
299
  event: 'schema_validation_failed',
298
300
  details: { attempt, error: errorMessage },
299
301
  });
300
- prompt += `\n\nCRITICAL: The previous response was invalid JSON schema. Error: ${errorMessage}. Please fix and return valid JSON matching the schema.`;
302
+ retryPrompt = `${prompt}\n\nCRITICAL: The previous response failed schema validation. Error: ${errorMessage}`;
301
303
  }
302
304
  }
303
305
  if (!parsed) {
@@ -310,11 +312,7 @@ export function registerStructuredToolTask(server, config) {
310
312
  ? config.formatOutput(finalResult)
311
313
  : undefined;
312
314
  const outcome = config.formatOutcome?.(finalResult) ?? 'completed';
313
- await reportProgress({
314
- current: TASK_PROGRESS_TOTAL,
315
- total: TASK_PROGRESS_TOTAL,
316
- message: formatProgressCompletion(config.name, progressContext, outcome),
317
- });
315
+ await reportProgressCompletionUpdate(reportProgress, config.name, progressContext, outcome);
318
316
  await storeResultSafely('completed', createToolResponse({
319
317
  ok: true,
320
318
  result: finalResult,
@@ -323,11 +321,7 @@ export function registerStructuredToolTask(server, config) {
323
321
  catch (error) {
324
322
  const errorMessage = getErrorMessage(error);
325
323
  const errorMeta = classifyErrorMeta(error, errorMessage);
326
- await reportProgress({
327
- current: TASK_PROGRESS_TOTAL,
328
- total: TASK_PROGRESS_TOTAL,
329
- message: formatProgressCompletion(config.name, progressContext, 'failed', false),
330
- });
324
+ await reportProgressCompletionUpdate(reportProgress, config.name, progressContext, 'failed', false);
331
325
  await updateStatusMessage(errorMessage);
332
326
  await storeResultSafely('failed', createErrorToolResponse(config.errorCode, errorMessage, undefined, errorMeta));
333
327
  }
@@ -1,3 +1,11 @@
1
+ function appendErrorMeta(error, meta) {
2
+ if (meta?.retryable !== undefined) {
3
+ error.retryable = meta.retryable;
4
+ }
5
+ if (meta?.kind !== undefined) {
6
+ error.kind = meta.kind;
7
+ }
8
+ }
1
9
  function toTextContent(structured, textContent) {
2
10
  const text = textContent ?? JSON.stringify(structured);
3
11
  return [{ type: 'text', text }];
@@ -13,12 +21,7 @@ function createErrorStructuredContent(code, message, result, meta) {
13
21
  code,
14
22
  message,
15
23
  };
16
- if (meta?.retryable !== undefined) {
17
- error.retryable = meta.retryable;
18
- }
19
- if (meta?.kind !== undefined) {
20
- error.kind = meta.kind;
21
- }
24
+ appendErrorMeta(error, meta);
22
25
  if (result === undefined) {
23
26
  return { ok: false, error };
24
27
  }
@@ -29,10 +32,5 @@ export function createToolResponse(structured, textContent) {
29
32
  }
30
33
  export function createErrorToolResponse(code, message, result, meta) {
31
34
  const structured = createErrorStructuredContent(code, message, result, meta);
32
- const base = buildToolResponse(structured);
33
- return {
34
- content: base.content,
35
- structuredContent: base.structuredContent,
36
- isError: true,
37
- };
35
+ return { ...buildToolResponse(structured), isError: true };
38
36
  }
@@ -1,11 +1,12 @@
1
1
  export type JsonObject = Record<string, unknown>;
2
2
  export type GeminiLogHandler = (level: string, data: unknown) => Promise<void>;
3
- interface GeminiRequestExecutionOptions {
3
+ export interface GeminiRequestExecutionOptions {
4
4
  maxRetries?: number;
5
5
  timeoutMs?: number;
6
6
  temperature?: number;
7
7
  maxOutputTokens?: number;
8
8
  thinkingBudget?: number;
9
+ includeThoughts?: boolean;
9
10
  signal?: AbortSignal;
10
11
  onLog?: GeminiLogHandler;
11
12
  }
@@ -15,6 +16,5 @@ export interface GeminiStructuredRequestOptions extends GeminiRequestExecutionOp
15
16
  export interface GeminiStructuredRequest extends GeminiStructuredRequestOptions {
16
17
  systemInstruction?: string;
17
18
  prompt: string;
18
- responseSchema: JsonObject;
19
+ responseSchema: Readonly<JsonObject>;
19
20
  }
20
- export {};