@j0hanz/code-review-analyst-mcp 1.6.4 → 1.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -13
- package/dist/lib/diff-store.d.ts +1 -0
- package/dist/lib/diff-store.js +2 -0
- package/dist/lib/gemini.js +47 -17
- package/dist/lib/model-config.d.ts +13 -15
- package/dist/lib/model-config.js +16 -25
- package/dist/lib/tool-contracts.d.ts +9 -9
- package/dist/lib/tool-contracts.js +9 -9
- package/dist/lib/tool-factory.d.ts +20 -12
- package/dist/lib/tool-factory.js +124 -123
- package/dist/resources/index.js +1 -1
- package/dist/resources/instructions.js +1 -1
- package/dist/resources/tool-catalog.js +2 -2
- package/dist/tools/analyze-complexity.js +0 -1
- package/dist/tools/analyze-pr-impact.js +0 -1
- package/dist/tools/detect-api-breaking.js +0 -1
- package/dist/tools/generate-diff.js +10 -3
- package/dist/tools/generate-review-summary.js +0 -1
- package/dist/tools/generate-test-plan.js +0 -1
- package/dist/tools/inspect-code-quality.js +0 -1
- package/dist/tools/suggest-search-replace.js +0 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,13 +12,13 @@ Gemini-powered MCP server for pull request analysis with structured outputs for
|
|
|
12
12
|
|
|
13
13
|
## Overview
|
|
14
14
|
|
|
15
|
-
This server accepts unified diffs and returns structured JSON results — findings with severity, impact categories, merge risk, test plans, and verbatim search/replace fixes. It uses Gemini Thinking models (Flash for fast tools,
|
|
15
|
+
This server accepts unified diffs and returns structured JSON results — findings with severity, impact categories, merge risk, test plans, and verbatim search/replace fixes. It uses Gemini Thinking models (Flash for fast tools, Flash for deep analysis) and runs over **stdio transport**.
|
|
16
16
|
|
|
17
17
|
## Key Features
|
|
18
18
|
|
|
19
19
|
- **Impact Analysis** — Objective severity scoring, breaking change detection, and rollback complexity assessment.
|
|
20
20
|
- **Review Summary** — Concise PR digest with merge recommendation and change statistics.
|
|
21
|
-
- **Deep Code Inspection** —
|
|
21
|
+
- **Deep Code Inspection** — Flash model with high thinking level for context-aware analysis using full file contents.
|
|
22
22
|
- **Search & Replace Fixes** — Verbatim, copy-paste-ready code fixes tied to specific findings.
|
|
23
23
|
- **Test Plan Generation** — Systematic test case generation with priority ranking and pseudocode.
|
|
24
24
|
- **Async Task Support** — All tools support MCP task lifecycle with progress notifications.
|
|
@@ -279,13 +279,25 @@ docker build -t code-review-analyst-mcp .
|
|
|
279
279
|
|
|
280
280
|
## Tools
|
|
281
281
|
|
|
282
|
+
> [!IMPORTANT]
|
|
283
|
+
> Call `generate_diff` first (`mode: "unstaged"` or `"staged"`). All review tools read the cached server-side diff (`diff://current`) and do not accept a direct `diff` parameter.
|
|
284
|
+
|
|
285
|
+
### `generate_diff`
|
|
286
|
+
|
|
287
|
+
Generate and cache the current branch diff for downstream review tools.
|
|
288
|
+
|
|
289
|
+
| Parameter | Type | Required | Description |
|
|
290
|
+
| --------- | -------- | -------- | -------------------------------------------------- |
|
|
291
|
+
| `mode` | `string` | Yes | `unstaged` (working tree) or `staged` (git index). |
|
|
292
|
+
|
|
293
|
+
**Returns:** `diffRef`, `stats` (files, added, deleted), `generatedAt`, `mode`, `message`.
|
|
294
|
+
|
|
282
295
|
### `analyze_pr_impact`
|
|
283
296
|
|
|
284
|
-
Assess the impact and risk of
|
|
297
|
+
Assess the impact and risk of cached pull request changes using the Flash model.
|
|
285
298
|
|
|
286
299
|
| Parameter | Type | Required | Description |
|
|
287
300
|
| ------------ | -------- | -------- | ---------------------------------------- |
|
|
288
|
-
| `diff` | `string` | Yes | Unified diff text. |
|
|
289
301
|
| `repository` | `string` | Yes | Repository identifier (e.g. `org/repo`). |
|
|
290
302
|
| `language` | `string` | No | Primary language hint. |
|
|
291
303
|
|
|
@@ -297,7 +309,6 @@ Summarize a pull request diff and assess high-level risk using the Flash model.
|
|
|
297
309
|
|
|
298
310
|
| Parameter | Type | Required | Description |
|
|
299
311
|
| ------------ | -------- | -------- | ---------------------------------------- |
|
|
300
|
-
| `diff` | `string` | Yes | Unified diff text. |
|
|
301
312
|
| `repository` | `string` | Yes | Repository identifier (e.g. `org/repo`). |
|
|
302
313
|
| `language` | `string` | No | Primary language hint. |
|
|
303
314
|
|
|
@@ -305,11 +316,10 @@ Summarize a pull request diff and assess high-level risk using the Flash model.
|
|
|
305
316
|
|
|
306
317
|
### `inspect_code_quality`
|
|
307
318
|
|
|
308
|
-
Deep-dive code review using the
|
|
319
|
+
Deep-dive code review using the Flash model with high thinking (16K token budget).
|
|
309
320
|
|
|
310
321
|
| Parameter | Type | Required | Description |
|
|
311
322
|
| ------------- | ---------- | -------- | --------------------------------------------- |
|
|
312
|
-
| `diff` | `string` | Yes | Unified diff text. |
|
|
313
323
|
| `repository` | `string` | Yes | Repository identifier (e.g. `org/repo`). |
|
|
314
324
|
| `language` | `string` | No | Primary language hint. |
|
|
315
325
|
| `focusAreas` | `string[]` | No | Areas to inspect: security, correctness, etc. |
|
|
@@ -322,11 +332,10 @@ Deep-dive code review using the Pro model with thinking (16K token budget).
|
|
|
322
332
|
|
|
323
333
|
### `suggest_search_replace`
|
|
324
334
|
|
|
325
|
-
Generate verbatim search-and-replace blocks to fix a specific finding using the
|
|
335
|
+
Generate verbatim search-and-replace blocks to fix a specific finding using the Flash model with high thinking.
|
|
326
336
|
|
|
327
337
|
| Parameter | Type | Required | Description |
|
|
328
338
|
| ---------------- | -------- | -------- | ---------------------------------------- |
|
|
329
|
-
| `diff` | `string` | Yes | Unified diff that contains the issue. |
|
|
330
339
|
| `findingTitle` | `string` | Yes | Short title of the finding to fix. |
|
|
331
340
|
| `findingDetails` | `string` | Yes | Detailed explanation of the bug or risk. |
|
|
332
341
|
|
|
@@ -338,7 +347,6 @@ Create a test plan covering the changes in the diff using the Flash model with t
|
|
|
338
347
|
|
|
339
348
|
| Parameter | Type | Required | Description |
|
|
340
349
|
| --------------- | -------- | -------- | ------------------------------------------- |
|
|
341
|
-
| `diff` | `string` | Yes | Unified diff to generate tests for. |
|
|
342
350
|
| `repository` | `string` | Yes | Repository identifier (e.g. `org/repo`). |
|
|
343
351
|
| `language` | `string` | No | Primary language hint. |
|
|
344
352
|
| `testFramework` | `string` | No | Test framework (e.g. jest, vitest, pytest). |
|
|
@@ -391,8 +399,8 @@ Create a test plan covering the changes in the diff using the Flash model with t
|
|
|
391
399
|
| ------------------------- | ------------------------ | -------------- |
|
|
392
400
|
| `analyze_pr_impact` | `gemini-3-flash-preview` | `minimal` |
|
|
393
401
|
| `generate_review_summary` | `gemini-3-flash-preview` | `minimal` |
|
|
394
|
-
| `inspect_code_quality` | `gemini-3-
|
|
395
|
-
| `suggest_search_replace` | `gemini-3-
|
|
402
|
+
| `inspect_code_quality` | `gemini-3-flash-preview` | `high` |
|
|
403
|
+
| `suggest_search_replace` | `gemini-3-flash-preview` | `high` |
|
|
396
404
|
| `generate_test_plan` | `gemini-3-flash-preview` | `medium` |
|
|
397
405
|
|
|
398
406
|
## Workflows
|
|
@@ -456,7 +464,7 @@ The pipeline runs lint, type-check, test, and build, then publishes to three tar
|
|
|
456
464
|
| ------------------------------------------ | ------------------------------------------------------------------------------------ |
|
|
457
465
|
| `Missing GEMINI_API_KEY or GOOGLE_API_KEY` | Set one of the API key env vars in your MCP client config. |
|
|
458
466
|
| `E_INPUT_TOO_LARGE` | Diff exceeds budget. Split into smaller diffs. |
|
|
459
|
-
| `Gemini request timed out` |
|
|
467
|
+
| `Gemini request timed out` | Deep analysis tasks may take 60-120s. Increase your client timeout. |
|
|
460
468
|
| `Too many concurrent Gemini calls` | Reduce parallel tool calls or increase `MAX_CONCURRENT_CALLS`. |
|
|
461
469
|
| No tool output visible | Ensure your MCP client is not swallowing `stderr` — the server uses stdio transport. |
|
|
462
470
|
|
package/dist/lib/diff-store.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
|
2
2
|
import type { ParsedFile } from './diff.js';
|
|
3
3
|
import { createErrorToolResponse } from './tool-response.js';
|
|
4
4
|
export declare const DIFF_RESOURCE_URI = "diff://current";
|
|
5
|
+
export declare const diffStaleWarningMs: import("./env-config.js").CachedEnvInt;
|
|
5
6
|
export interface DiffStats {
|
|
6
7
|
files: number;
|
|
7
8
|
added: number;
|
package/dist/lib/diff-store.js
CHANGED
|
@@ -3,6 +3,8 @@ import { createErrorToolResponse } from './tool-response.js';
|
|
|
3
3
|
export const DIFF_RESOURCE_URI = 'diff://current';
|
|
4
4
|
const diffCacheTtlMs = createCachedEnvInt('DIFF_CACHE_TTL_MS', 60 * 60 * 1_000 // 1 hour default
|
|
5
5
|
);
|
|
6
|
+
export const diffStaleWarningMs = createCachedEnvInt('DIFF_STALE_WARNING_MS', 5 * 60 * 1_000 // 5 minutes default
|
|
7
|
+
);
|
|
6
8
|
const diffSlots = new Map();
|
|
7
9
|
let sendResourceUpdated;
|
|
8
10
|
function setDiffSlot(key, data) {
|
package/dist/lib/gemini.js
CHANGED
|
@@ -38,6 +38,8 @@ const DIGITS_ONLY_PATTERN = /^\d+$/;
|
|
|
38
38
|
const TRUE_ENV_VALUES = new Set(['1', 'true', 'yes', 'on']);
|
|
39
39
|
const FALSE_ENV_VALUES = new Set(['0', 'false', 'no', 'off']);
|
|
40
40
|
const SLEEP_UNREF_OPTIONS = { ref: false };
|
|
41
|
+
const JSON_CODE_BLOCK_PATTERN = /```(?:json)?\n?([\s\S]*?)(?=\n?```)/u;
|
|
42
|
+
const NEVER_ABORT_SIGNAL = new AbortController().signal;
|
|
41
43
|
const maxConcurrentCallsConfig = createCachedEnvInt('MAX_CONCURRENT_CALLS', 10);
|
|
42
44
|
const maxConcurrentBatchCallsConfig = createCachedEnvInt('MAX_CONCURRENT_BATCH_CALLS', 2);
|
|
43
45
|
const concurrencyWaitMsConfig = createCachedEnvInt('MAX_CONCURRENT_CALLS_WAIT_MS', 2_000);
|
|
@@ -45,8 +47,8 @@ const batchPollIntervalMsConfig = createCachedEnvInt('GEMINI_BATCH_POLL_INTERVAL
|
|
|
45
47
|
const batchTimeoutMsConfig = createCachedEnvInt('GEMINI_BATCH_TIMEOUT_MS', 120_000);
|
|
46
48
|
let activeCalls = 0;
|
|
47
49
|
let activeBatchCalls = 0;
|
|
48
|
-
const slotWaiters =
|
|
49
|
-
const batchSlotWaiters =
|
|
50
|
+
const slotWaiters = new Set();
|
|
51
|
+
const batchSlotWaiters = new Set();
|
|
50
52
|
const RETRYABLE_TRANSIENT_CODES = new Set([
|
|
51
53
|
'RESOURCE_EXHAUSTED',
|
|
52
54
|
'UNAVAILABLE',
|
|
@@ -54,6 +56,36 @@ const RETRYABLE_TRANSIENT_CODES = new Set([
|
|
|
54
56
|
'INTERNAL',
|
|
55
57
|
'ABORTED',
|
|
56
58
|
]);
|
|
59
|
+
function getWaiterCount(waiters) {
|
|
60
|
+
return waiters instanceof Set ? waiters.size : waiters.length;
|
|
61
|
+
}
|
|
62
|
+
function addWaiter(waiters, waiter) {
|
|
63
|
+
if (waiters instanceof Set) {
|
|
64
|
+
waiters.add(waiter);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
waiters.push(waiter);
|
|
68
|
+
}
|
|
69
|
+
function removeWaiter(waiters, waiter) {
|
|
70
|
+
if (waiters instanceof Set) {
|
|
71
|
+
waiters.delete(waiter);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const index = waiters.indexOf(waiter);
|
|
75
|
+
if (index !== -1) {
|
|
76
|
+
waiters.splice(index, 1);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function popNextWaiter(waiters) {
|
|
80
|
+
if (waiters instanceof Set) {
|
|
81
|
+
const next = waiters.values().next().value;
|
|
82
|
+
if (next !== undefined) {
|
|
83
|
+
waiters.delete(next);
|
|
84
|
+
}
|
|
85
|
+
return next;
|
|
86
|
+
}
|
|
87
|
+
return waiters.shift();
|
|
88
|
+
}
|
|
57
89
|
const SAFETY_CATEGORIES = [
|
|
58
90
|
HarmCategory.HARM_CATEGORY_HATE_SPEECH,
|
|
59
91
|
HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT,
|
|
@@ -371,7 +403,7 @@ function parseStructuredResponse(responseText) {
|
|
|
371
403
|
catch {
|
|
372
404
|
// fast-path failed; try extracting from markdown block
|
|
373
405
|
}
|
|
374
|
-
const jsonMatch =
|
|
406
|
+
const jsonMatch = JSON_CODE_BLOCK_PATTERN.exec(responseText);
|
|
375
407
|
const jsonText = jsonMatch?.[1] ?? responseText;
|
|
376
408
|
try {
|
|
377
409
|
return JSON.parse(jsonText);
|
|
@@ -501,13 +533,13 @@ function canRetryAttempt(attempt, maxRetries, error) {
|
|
|
501
533
|
return attempt < maxRetries && shouldRetry(error);
|
|
502
534
|
}
|
|
503
535
|
function tryWakeNextWaiter() {
|
|
504
|
-
const next = slotWaiters
|
|
536
|
+
const next = popNextWaiter(slotWaiters);
|
|
505
537
|
if (next !== undefined) {
|
|
506
538
|
next();
|
|
507
539
|
}
|
|
508
540
|
}
|
|
509
541
|
async function waitForSlot(limit, getActiveCount, acquireSlot, waiters, requestSignal) {
|
|
510
|
-
if (waiters
|
|
542
|
+
if (getWaiterCount(waiters) === 0 && getActiveCount() < limit) {
|
|
511
543
|
acquireSlot();
|
|
512
544
|
return;
|
|
513
545
|
}
|
|
@@ -526,12 +558,9 @@ async function waitForSlot(limit, getActiveCount, acquireSlot, waiters, requestS
|
|
|
526
558
|
acquireSlot();
|
|
527
559
|
resolve();
|
|
528
560
|
};
|
|
529
|
-
waiters
|
|
530
|
-
const
|
|
531
|
-
|
|
532
|
-
if (idx !== -1) {
|
|
533
|
-
waiters.splice(idx, 1);
|
|
534
|
-
}
|
|
561
|
+
addWaiter(waiters, waiter);
|
|
562
|
+
const removeCurrentWaiter = () => {
|
|
563
|
+
removeWaiter(waiters, waiter);
|
|
535
564
|
};
|
|
536
565
|
const detachAbortListener = () => {
|
|
537
566
|
if (requestSignal) {
|
|
@@ -542,7 +571,7 @@ async function waitForSlot(limit, getActiveCount, acquireSlot, waiters, requestS
|
|
|
542
571
|
if (settled)
|
|
543
572
|
return;
|
|
544
573
|
settled = true;
|
|
545
|
-
|
|
574
|
+
removeCurrentWaiter();
|
|
546
575
|
detachAbortListener();
|
|
547
576
|
reject(new Error(formatConcurrencyLimitErrorMessage(limit, waitLimitMs)));
|
|
548
577
|
}, waitLimitMs);
|
|
@@ -551,7 +580,7 @@ async function waitForSlot(limit, getActiveCount, acquireSlot, waiters, requestS
|
|
|
551
580
|
if (settled)
|
|
552
581
|
return;
|
|
553
582
|
settled = true;
|
|
554
|
-
|
|
583
|
+
removeCurrentWaiter();
|
|
555
584
|
clearTimeout(deadlineTimer);
|
|
556
585
|
reject(new Error('Gemini request was cancelled.'));
|
|
557
586
|
};
|
|
@@ -566,7 +595,7 @@ async function waitForConcurrencySlot(limit, requestSignal) {
|
|
|
566
595
|
}, slotWaiters, requestSignal);
|
|
567
596
|
}
|
|
568
597
|
function tryWakeNextBatchWaiter() {
|
|
569
|
-
const next = batchSlotWaiters
|
|
598
|
+
const next = popNextWaiter(batchSlotWaiters);
|
|
570
599
|
if (next !== undefined) {
|
|
571
600
|
next();
|
|
572
601
|
}
|
|
@@ -718,12 +747,13 @@ async function runInlineBatchWithPolling(request, model, onLog) {
|
|
|
718
747
|
let completed = false;
|
|
719
748
|
let timedOut = false;
|
|
720
749
|
try {
|
|
750
|
+
const createSignal = request.signal ?? NEVER_ABORT_SIGNAL;
|
|
721
751
|
const createPayload = {
|
|
722
752
|
model,
|
|
723
753
|
src: [
|
|
724
754
|
{
|
|
725
755
|
contents: [{ role: 'user', parts: [{ text: request.prompt }] }],
|
|
726
|
-
config: buildGenerationConfig(request,
|
|
756
|
+
config: buildGenerationConfig(request, createSignal),
|
|
727
757
|
},
|
|
728
758
|
],
|
|
729
759
|
};
|
|
@@ -770,7 +800,7 @@ async function runInlineBatchWithPolling(request, model, onLog) {
|
|
|
770
800
|
export function getGeminiQueueSnapshot() {
|
|
771
801
|
return {
|
|
772
802
|
activeCalls,
|
|
773
|
-
waitingCalls: slotWaiters.
|
|
803
|
+
waitingCalls: slotWaiters.size,
|
|
774
804
|
};
|
|
775
805
|
}
|
|
776
806
|
export async function generateStructuredJson(request) {
|
|
@@ -793,7 +823,7 @@ export async function generateStructuredJson(request) {
|
|
|
793
823
|
await safeCallOnLog(onLog, 'info', {
|
|
794
824
|
event: 'gemini_queue_acquired',
|
|
795
825
|
queueWaitMs,
|
|
796
|
-
waitingCalls: batchMode === 'inline' ? batchSlotWaiters.
|
|
826
|
+
waitingCalls: batchMode === 'inline' ? batchSlotWaiters.size : slotWaiters.size,
|
|
797
827
|
activeCalls,
|
|
798
828
|
activeBatchCalls,
|
|
799
829
|
mode: batchMode,
|
|
@@ -1,34 +1,32 @@
|
|
|
1
1
|
/** Fast, cost-effective model for summarization and light analysis. */
|
|
2
2
|
export declare const FLASH_MODEL = "gemini-3-flash-preview";
|
|
3
|
-
/** High-capability model for deep reasoning, quality inspection, and reliable code generation. */
|
|
4
|
-
export declare const PRO_MODEL = "gemini-3-pro-preview";
|
|
5
3
|
/** Default language hint. */
|
|
6
4
|
export declare const DEFAULT_LANGUAGE = "detect";
|
|
7
5
|
/** Default test-framework hint. */
|
|
8
6
|
export declare const DEFAULT_FRAMEWORK = "detect";
|
|
9
|
-
/** Extended timeout for
|
|
10
|
-
export declare const
|
|
7
|
+
/** Extended timeout for deep analysis calls (ms). */
|
|
8
|
+
export declare const DEFAULT_TIMEOUT_EXTENDED_MS = 120000;
|
|
11
9
|
export declare const MODEL_TIMEOUT_MS: {
|
|
12
|
-
readonly
|
|
10
|
+
readonly extended: 120000;
|
|
13
11
|
};
|
|
14
12
|
/** Thinking level for Flash triage. */
|
|
15
13
|
export declare const FLASH_TRIAGE_THINKING_LEVEL: "minimal";
|
|
16
14
|
/** Thinking level for Flash analysis. */
|
|
17
15
|
export declare const FLASH_THINKING_LEVEL: "medium";
|
|
18
|
-
/** Thinking level for
|
|
19
|
-
export declare const
|
|
16
|
+
/** Thinking level for Flash deep analysis. */
|
|
17
|
+
export declare const FLASH_HIGH_THINKING_LEVEL: "high";
|
|
20
18
|
/** Output cap for Flash API breaking-change detection. */
|
|
21
|
-
export declare const FLASH_API_BREAKING_MAX_OUTPUT_TOKENS
|
|
19
|
+
export declare const FLASH_API_BREAKING_MAX_OUTPUT_TOKENS = 65536;
|
|
22
20
|
/** Output cap for Flash complexity analysis. */
|
|
23
|
-
export declare const FLASH_COMPLEXITY_MAX_OUTPUT_TOKENS
|
|
21
|
+
export declare const FLASH_COMPLEXITY_MAX_OUTPUT_TOKENS = 65536;
|
|
24
22
|
/** Output cap for Flash test-plan generation. */
|
|
25
|
-
export declare const FLASH_TEST_PLAN_MAX_OUTPUT_TOKENS
|
|
23
|
+
export declare const FLASH_TEST_PLAN_MAX_OUTPUT_TOKENS = 65536;
|
|
26
24
|
/** Output cap for Flash triage tools. */
|
|
27
|
-
export declare const FLASH_TRIAGE_MAX_OUTPUT_TOKENS
|
|
28
|
-
/** Output cap for
|
|
29
|
-
export declare const
|
|
30
|
-
/** Output cap for
|
|
31
|
-
export declare const
|
|
25
|
+
export declare const FLASH_TRIAGE_MAX_OUTPUT_TOKENS = 65536;
|
|
26
|
+
/** Output cap for Flash patch generation. */
|
|
27
|
+
export declare const FLASH_PATCH_MAX_OUTPUT_TOKENS = 65536;
|
|
28
|
+
/** Output cap for Flash deep review findings. */
|
|
29
|
+
export declare const FLASH_REVIEW_MAX_OUTPUT_TOKENS = 65536;
|
|
32
30
|
/** Temperature for analytical tools. */
|
|
33
31
|
export declare const ANALYSIS_TEMPERATURE: 1;
|
|
34
32
|
/** Temperature for creative synthesis (test plans). */
|
package/dist/lib/model-config.js
CHANGED
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
/** Fast, cost-effective model for summarization and light analysis. */
|
|
2
2
|
export const FLASH_MODEL = 'gemini-3-flash-preview';
|
|
3
|
-
/** High-capability model for deep reasoning, quality inspection, and reliable code generation. */
|
|
4
|
-
export const PRO_MODEL = 'gemini-3-pro-preview';
|
|
5
3
|
/** Default language hint. */
|
|
6
4
|
export const DEFAULT_LANGUAGE = 'detect';
|
|
7
5
|
/** Default test-framework hint. */
|
|
8
6
|
export const DEFAULT_FRAMEWORK = 'detect';
|
|
9
|
-
/** Extended timeout for
|
|
10
|
-
export const
|
|
7
|
+
/** Extended timeout for deep analysis calls (ms). */
|
|
8
|
+
export const DEFAULT_TIMEOUT_EXTENDED_MS = 120_000;
|
|
11
9
|
export const MODEL_TIMEOUT_MS = {
|
|
12
|
-
|
|
10
|
+
extended: DEFAULT_TIMEOUT_EXTENDED_MS,
|
|
13
11
|
};
|
|
14
12
|
Object.freeze(MODEL_TIMEOUT_MS);
|
|
15
13
|
// ---------------------------------------------------------------------------
|
|
@@ -21,35 +19,28 @@ const THINKING_LEVELS = {
|
|
|
21
19
|
/** Medium thinking for analysis tasks. */
|
|
22
20
|
flash: 'medium',
|
|
23
21
|
/** High thinking for deep review and patches. */
|
|
24
|
-
|
|
25
|
-
};
|
|
26
|
-
// Thinking budget in tokens for Flash and Pro tools. Note that these are not hard limits, but rather guidelines to encourage concise responses and manage latency/cost.
|
|
27
|
-
const OUTPUT_TOKEN_BUDGET = {
|
|
28
|
-
flashApiBreaking: 4_096,
|
|
29
|
-
flashComplexity: 4_096,
|
|
30
|
-
flashTestPlan: 8_192,
|
|
31
|
-
flashTriage: 4_096,
|
|
32
|
-
proPatch: 8_192,
|
|
33
|
-
proReview: 12_288,
|
|
22
|
+
flashHigh: 'high',
|
|
34
23
|
};
|
|
35
24
|
/** Thinking level for Flash triage. */
|
|
36
25
|
export const FLASH_TRIAGE_THINKING_LEVEL = THINKING_LEVELS.flashTriage;
|
|
37
26
|
/** Thinking level for Flash analysis. */
|
|
38
27
|
export const FLASH_THINKING_LEVEL = THINKING_LEVELS.flash;
|
|
39
|
-
/** Thinking level for
|
|
40
|
-
export const
|
|
28
|
+
/** Thinking level for Flash deep analysis. */
|
|
29
|
+
export const FLASH_HIGH_THINKING_LEVEL = THINKING_LEVELS.flashHigh;
|
|
30
|
+
// Output token caps for various tools. Set to a high default to avoid cutting off important information, but can be adjusted as needed.
|
|
31
|
+
const DEFAULT_OUTPUT_CAP = 65_536;
|
|
41
32
|
/** Output cap for Flash API breaking-change detection. */
|
|
42
|
-
export const FLASH_API_BREAKING_MAX_OUTPUT_TOKENS =
|
|
33
|
+
export const FLASH_API_BREAKING_MAX_OUTPUT_TOKENS = DEFAULT_OUTPUT_CAP;
|
|
43
34
|
/** Output cap for Flash complexity analysis. */
|
|
44
|
-
export const FLASH_COMPLEXITY_MAX_OUTPUT_TOKENS =
|
|
35
|
+
export const FLASH_COMPLEXITY_MAX_OUTPUT_TOKENS = DEFAULT_OUTPUT_CAP;
|
|
45
36
|
/** Output cap for Flash test-plan generation. */
|
|
46
|
-
export const FLASH_TEST_PLAN_MAX_OUTPUT_TOKENS =
|
|
37
|
+
export const FLASH_TEST_PLAN_MAX_OUTPUT_TOKENS = DEFAULT_OUTPUT_CAP;
|
|
47
38
|
/** Output cap for Flash triage tools. */
|
|
48
|
-
export const FLASH_TRIAGE_MAX_OUTPUT_TOKENS =
|
|
49
|
-
/** Output cap for
|
|
50
|
-
export const
|
|
51
|
-
/** Output cap for
|
|
52
|
-
export const
|
|
39
|
+
export const FLASH_TRIAGE_MAX_OUTPUT_TOKENS = DEFAULT_OUTPUT_CAP;
|
|
40
|
+
/** Output cap for Flash patch generation. */
|
|
41
|
+
export const FLASH_PATCH_MAX_OUTPUT_TOKENS = DEFAULT_OUTPUT_CAP;
|
|
42
|
+
/** Output cap for Flash deep review findings. */
|
|
43
|
+
export const FLASH_REVIEW_MAX_OUTPUT_TOKENS = DEFAULT_OUTPUT_CAP;
|
|
53
44
|
// ---------------------------------------------------------------------------
|
|
54
45
|
// Temperatures
|
|
55
46
|
// ---------------------------------------------------------------------------
|
|
@@ -57,7 +57,7 @@ export declare const TOOL_CONTRACTS: readonly [{
|
|
|
57
57
|
readonly model: "gemini-3-flash-preview";
|
|
58
58
|
readonly timeoutMs: 90000;
|
|
59
59
|
readonly thinkingLevel: "minimal";
|
|
60
|
-
readonly maxOutputTokens:
|
|
60
|
+
readonly maxOutputTokens: 65536;
|
|
61
61
|
readonly temperature: 1;
|
|
62
62
|
readonly deterministicJson: true;
|
|
63
63
|
readonly params: readonly [{
|
|
@@ -82,7 +82,7 @@ export declare const TOOL_CONTRACTS: readonly [{
|
|
|
82
82
|
readonly model: "gemini-3-flash-preview";
|
|
83
83
|
readonly timeoutMs: 90000;
|
|
84
84
|
readonly thinkingLevel: "minimal";
|
|
85
|
-
readonly maxOutputTokens:
|
|
85
|
+
readonly maxOutputTokens: 65536;
|
|
86
86
|
readonly temperature: 1;
|
|
87
87
|
readonly deterministicJson: true;
|
|
88
88
|
readonly params: readonly [{
|
|
@@ -104,10 +104,10 @@ export declare const TOOL_CONTRACTS: readonly [{
|
|
|
104
104
|
}, {
|
|
105
105
|
readonly name: "inspect_code_quality";
|
|
106
106
|
readonly purpose: "Deep code review over the cached diff.";
|
|
107
|
-
readonly model: "gemini-3-
|
|
107
|
+
readonly model: "gemini-3-flash-preview";
|
|
108
108
|
readonly timeoutMs: 120000;
|
|
109
109
|
readonly thinkingLevel: "high";
|
|
110
|
-
readonly maxOutputTokens:
|
|
110
|
+
readonly maxOutputTokens: 65536;
|
|
111
111
|
readonly temperature: 1;
|
|
112
112
|
readonly deterministicJson: true;
|
|
113
113
|
readonly params: readonly [{
|
|
@@ -142,10 +142,10 @@ export declare const TOOL_CONTRACTS: readonly [{
|
|
|
142
142
|
}, {
|
|
143
143
|
readonly name: "suggest_search_replace";
|
|
144
144
|
readonly purpose: "Generate verbatim search/replace fix blocks for one finding.";
|
|
145
|
-
readonly model: "gemini-3-
|
|
145
|
+
readonly model: "gemini-3-flash-preview";
|
|
146
146
|
readonly timeoutMs: 120000;
|
|
147
147
|
readonly thinkingLevel: "high";
|
|
148
|
-
readonly maxOutputTokens:
|
|
148
|
+
readonly maxOutputTokens: 65536;
|
|
149
149
|
readonly temperature: 1;
|
|
150
150
|
readonly deterministicJson: true;
|
|
151
151
|
readonly params: readonly [{
|
|
@@ -171,7 +171,7 @@ export declare const TOOL_CONTRACTS: readonly [{
|
|
|
171
171
|
readonly model: "gemini-3-flash-preview";
|
|
172
172
|
readonly timeoutMs: 90000;
|
|
173
173
|
readonly thinkingLevel: "medium";
|
|
174
|
-
readonly maxOutputTokens:
|
|
174
|
+
readonly maxOutputTokens: 65536;
|
|
175
175
|
readonly temperature: 1;
|
|
176
176
|
readonly deterministicJson: true;
|
|
177
177
|
readonly params: readonly [{
|
|
@@ -208,7 +208,7 @@ export declare const TOOL_CONTRACTS: readonly [{
|
|
|
208
208
|
readonly model: "gemini-3-flash-preview";
|
|
209
209
|
readonly timeoutMs: 90000;
|
|
210
210
|
readonly thinkingLevel: "medium";
|
|
211
|
-
readonly maxOutputTokens:
|
|
211
|
+
readonly maxOutputTokens: 65536;
|
|
212
212
|
readonly temperature: 1;
|
|
213
213
|
readonly deterministicJson: true;
|
|
214
214
|
readonly params: readonly [{
|
|
@@ -227,7 +227,7 @@ export declare const TOOL_CONTRACTS: readonly [{
|
|
|
227
227
|
readonly model: "gemini-3-flash-preview";
|
|
228
228
|
readonly timeoutMs: 90000;
|
|
229
229
|
readonly thinkingLevel: "minimal";
|
|
230
|
-
readonly maxOutputTokens:
|
|
230
|
+
readonly maxOutputTokens: 65536;
|
|
231
231
|
readonly temperature: 1;
|
|
232
232
|
readonly deterministicJson: true;
|
|
233
233
|
readonly params: readonly [{
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ANALYSIS_TEMPERATURE, CREATIVE_TEMPERATURE,
|
|
1
|
+
import { ANALYSIS_TEMPERATURE, CREATIVE_TEMPERATURE, DEFAULT_TIMEOUT_EXTENDED_MS, FLASH_API_BREAKING_MAX_OUTPUT_TOKENS, FLASH_COMPLEXITY_MAX_OUTPUT_TOKENS, FLASH_HIGH_THINKING_LEVEL, FLASH_MODEL, FLASH_PATCH_MAX_OUTPUT_TOKENS, FLASH_REVIEW_MAX_OUTPUT_TOKENS, FLASH_TEST_PLAN_MAX_OUTPUT_TOKENS, FLASH_THINKING_LEVEL, FLASH_TRIAGE_MAX_OUTPUT_TOKENS, FLASH_TRIAGE_THINKING_LEVEL, PATCH_TEMPERATURE, TRIAGE_TEMPERATURE, } from './model-config.js';
|
|
2
2
|
const DEFAULT_TIMEOUT_FLASH_MS = 90_000;
|
|
3
3
|
export const INSPECTION_FOCUS_AREAS = [
|
|
4
4
|
'security',
|
|
@@ -119,10 +119,10 @@ export const TOOL_CONTRACTS = [
|
|
|
119
119
|
{
|
|
120
120
|
name: 'inspect_code_quality',
|
|
121
121
|
purpose: 'Deep code review over the cached diff.',
|
|
122
|
-
model:
|
|
123
|
-
timeoutMs:
|
|
124
|
-
thinkingLevel:
|
|
125
|
-
maxOutputTokens:
|
|
122
|
+
model: FLASH_MODEL,
|
|
123
|
+
timeoutMs: DEFAULT_TIMEOUT_EXTENDED_MS,
|
|
124
|
+
thinkingLevel: FLASH_HIGH_THINKING_LEVEL,
|
|
125
|
+
maxOutputTokens: FLASH_REVIEW_MAX_OUTPUT_TOKENS,
|
|
126
126
|
temperature: ANALYSIS_TEMPERATURE,
|
|
127
127
|
deterministicJson: true,
|
|
128
128
|
params: [
|
|
@@ -169,10 +169,10 @@ export const TOOL_CONTRACTS = [
|
|
|
169
169
|
{
|
|
170
170
|
name: 'suggest_search_replace',
|
|
171
171
|
purpose: 'Generate verbatim search/replace fix blocks for one finding.',
|
|
172
|
-
model:
|
|
173
|
-
timeoutMs:
|
|
174
|
-
thinkingLevel:
|
|
175
|
-
maxOutputTokens:
|
|
172
|
+
model: FLASH_MODEL,
|
|
173
|
+
timeoutMs: DEFAULT_TIMEOUT_EXTENDED_MS,
|
|
174
|
+
thinkingLevel: FLASH_HIGH_THINKING_LEVEL,
|
|
175
|
+
maxOutputTokens: FLASH_PATCH_MAX_OUTPUT_TOKENS,
|
|
176
176
|
temperature: PATCH_TEMPERATURE,
|
|
177
177
|
deterministicJson: true,
|
|
178
178
|
params: [
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { CreateTaskRequestHandlerExtra } from '@modelcontextprotocol/sdk/experimental/tasks/interfaces.js';
|
|
2
1
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
2
|
import type { ZodRawShapeCompat } from '@modelcontextprotocol/sdk/server/zod-compat.js';
|
|
4
3
|
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
|
@@ -26,6 +25,11 @@ interface ProgressNotificationParams {
|
|
|
26
25
|
total?: number;
|
|
27
26
|
message?: string;
|
|
28
27
|
}
|
|
28
|
+
interface ProgressPayload {
|
|
29
|
+
current: number;
|
|
30
|
+
total?: number;
|
|
31
|
+
message?: string;
|
|
32
|
+
}
|
|
29
33
|
interface ProgressExtra {
|
|
30
34
|
_meta?: {
|
|
31
35
|
progressToken?: unknown;
|
|
@@ -66,8 +70,6 @@ export interface StructuredToolTaskConfig<TInput extends object = Record<string,
|
|
|
66
70
|
requiresDiff?: boolean;
|
|
67
71
|
/** Optional override for schema validation retries. Defaults to GEMINI_SCHEMA_RETRIES env var. */
|
|
68
72
|
schemaRetries?: number;
|
|
69
|
-
/** Optional Gemini model to use (e.g. 'gemini-3-pro-preview'). */
|
|
70
|
-
model?: string;
|
|
71
73
|
/** Optional thinking level. */
|
|
72
74
|
thinkingLevel?: 'minimal' | 'low' | 'medium' | 'high';
|
|
73
75
|
/** Optional timeout in ms for the Gemini call. Defaults to 90,000 ms. Use DEFAULT_TIMEOUT_PRO_MS for Pro model calls. */
|
|
@@ -101,27 +103,33 @@ export declare function wrapToolHandler<TInput, TResult extends CallToolResult>(
|
|
|
101
103
|
toolName: string;
|
|
102
104
|
progressContext?: (input: TInput) => string;
|
|
103
105
|
}, handler: (input: TInput, extra: ProgressExtra) => Promise<TResult> | TResult): (input: TInput, extra: ProgressExtra) => Promise<TResult>;
|
|
104
|
-
interface
|
|
105
|
-
|
|
106
|
+
interface TaskStatusReporter {
|
|
107
|
+
updateStatus: (message: string) => Promise<void>;
|
|
108
|
+
storeResult?: (status: 'completed' | 'failed', result: CallToolResult) => Promise<void>;
|
|
106
109
|
}
|
|
107
|
-
export declare class
|
|
108
|
-
private readonly server;
|
|
110
|
+
export declare class ToolExecutionRunner<TInput extends object, TResult extends object, TFinal extends TResult> {
|
|
109
111
|
private readonly config;
|
|
110
|
-
private readonly
|
|
111
|
-
private readonly task;
|
|
112
|
+
private readonly signal?;
|
|
112
113
|
private diffSlotSnapshot;
|
|
113
114
|
private hasSnapshot;
|
|
114
|
-
private
|
|
115
|
+
private responseSchema;
|
|
115
116
|
private readonly onLog;
|
|
116
117
|
private readonly reportProgress;
|
|
118
|
+
private readonly statusReporter;
|
|
117
119
|
private progressContext;
|
|
118
|
-
|
|
120
|
+
private lastStatusMessage;
|
|
121
|
+
constructor(config: StructuredToolTaskConfig<TInput, TResult, TFinal>, dependencies: {
|
|
122
|
+
onLog: (level: string, data: unknown) => Promise<void>;
|
|
123
|
+
reportProgress: (payload: ProgressPayload) => Promise<void>;
|
|
124
|
+
statusReporter: TaskStatusReporter;
|
|
125
|
+
}, signal?: AbortSignal | undefined);
|
|
126
|
+
setResponseSchemaOverride(responseSchema: Record<string, unknown>): void;
|
|
119
127
|
setDiffSlotSnapshot(diffSlotSnapshot: DiffSlot | undefined): void;
|
|
120
128
|
private updateStatusMessage;
|
|
121
129
|
private storeResultSafely;
|
|
122
130
|
private executeValidation;
|
|
123
131
|
private executeModelCall;
|
|
124
|
-
run(input: unknown): Promise<
|
|
132
|
+
run(input: unknown): Promise<CallToolResult>;
|
|
125
133
|
}
|
|
126
134
|
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;
|
|
127
135
|
export {};
|
package/dist/lib/tool-factory.js
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { DefaultOutputSchema } from '../schemas/outputs.js';
|
|
3
|
-
import { createNoDiffError, getDiff } from './diff-store.js';
|
|
3
|
+
import { createNoDiffError, diffStaleWarningMs, getDiff, } from './diff-store.js';
|
|
4
4
|
import { validateDiffBudget } from './diff.js';
|
|
5
5
|
import { createCachedEnvInt } from './env-config.js';
|
|
6
6
|
import { getErrorMessage, RETRYABLE_UPSTREAM_ERROR_PATTERN } from './errors.js';
|
|
7
7
|
import { stripJsonSchemaConstraints } from './gemini-schema.js';
|
|
8
|
-
import { generateStructuredJson
|
|
8
|
+
import { generateStructuredJson } from './gemini.js';
|
|
9
9
|
import { createErrorToolResponse, createToolResponse, } from './tool-response.js';
|
|
10
|
-
const DEFAULT_TASK_TTL_MS = 30 * 60 * 1_000;
|
|
11
10
|
// Named progress step indices for 7-step progress (0–6).
|
|
12
11
|
const STEP_STARTING = 0;
|
|
13
12
|
const STEP_VALIDATING = 1;
|
|
@@ -28,6 +27,8 @@ const DEFAULT_SCHEMA_RETRY_ERROR_CHARS = 1_500;
|
|
|
28
27
|
const schemaRetryErrorCharsConfig = createCachedEnvInt('MAX_SCHEMA_RETRY_ERROR_CHARS', DEFAULT_SCHEMA_RETRY_ERROR_CHARS);
|
|
29
28
|
const DETERMINISTIC_JSON_RETRY_NOTE = 'Deterministic JSON mode: keep key names exactly as schema-defined and preserve stable field ordering.';
|
|
30
29
|
const JSON_PARSE_ERROR_PATTERN = /model produced invalid json/i;
|
|
30
|
+
const responseSchemaCache = new WeakMap();
|
|
31
|
+
const progressReporterCache = new WeakMap();
|
|
31
32
|
function buildToolAnnotations(annotations) {
|
|
32
33
|
if (!annotations) {
|
|
33
34
|
return {
|
|
@@ -49,6 +50,18 @@ function createGeminiResponseSchema(config) {
|
|
|
49
50
|
const sourceSchema = config.geminiSchema ?? config.resultSchema;
|
|
50
51
|
return stripJsonSchemaConstraints(z.toJSONSchema(sourceSchema));
|
|
51
52
|
}
|
|
53
|
+
function getCachedGeminiResponseSchema(config) {
|
|
54
|
+
const cached = responseSchemaCache.get(config);
|
|
55
|
+
if (cached) {
|
|
56
|
+
return cached;
|
|
57
|
+
}
|
|
58
|
+
const responseSchema = createGeminiResponseSchema({
|
|
59
|
+
geminiSchema: config.geminiSchema,
|
|
60
|
+
resultSchema: config.resultSchema,
|
|
61
|
+
});
|
|
62
|
+
responseSchemaCache.set(config, responseSchema);
|
|
63
|
+
return responseSchema;
|
|
64
|
+
}
|
|
52
65
|
function parseToolInput(input, fullInputSchema) {
|
|
53
66
|
return fullInputSchema.parse(input);
|
|
54
67
|
}
|
|
@@ -92,9 +105,6 @@ function createGenerationRequest(config, promptParts, responseSchema, onLog, sig
|
|
|
92
105
|
responseSchema,
|
|
93
106
|
onLog,
|
|
94
107
|
};
|
|
95
|
-
if (config.model !== undefined) {
|
|
96
|
-
request.model = config.model;
|
|
97
|
-
}
|
|
98
108
|
if (config.thinkingLevel !== undefined) {
|
|
99
109
|
request.thinkingLevel = config.thinkingLevel;
|
|
100
110
|
}
|
|
@@ -165,24 +175,14 @@ function isRetryableUpstreamMessage(message) {
|
|
|
165
175
|
return (RETRYABLE_UPSTREAM_ERROR_PATTERN.test(message) ||
|
|
166
176
|
BUSY_ERROR_PATTERN.test(message));
|
|
167
177
|
}
|
|
168
|
-
function
|
|
178
|
+
function createProgressReporter(extra) {
|
|
169
179
|
const rawToken = extra._meta?.progressToken;
|
|
170
180
|
if (typeof rawToken !== 'string' && typeof rawToken !== 'number') {
|
|
171
|
-
return
|
|
181
|
+
return async () => {
|
|
182
|
+
// Request did not provide a progress token.
|
|
183
|
+
};
|
|
172
184
|
}
|
|
173
|
-
const
|
|
174
|
-
progressToken: rawToken,
|
|
175
|
-
progress: payload.current,
|
|
176
|
-
...(payload.total !== undefined ? { total: payload.total } : {}),
|
|
177
|
-
...(payload.message !== undefined ? { message: payload.message } : {}),
|
|
178
|
-
};
|
|
179
|
-
return extra
|
|
180
|
-
.sendNotification({ method: 'notifications/progress', params })
|
|
181
|
-
.catch(() => {
|
|
182
|
-
// Progress notifications are best-effort; never fail tool execution.
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
function createProgressReporter(extra) {
|
|
185
|
+
const progressToken = rawToken;
|
|
186
186
|
let lastCurrent = 0;
|
|
187
187
|
let didSendTerminal = false;
|
|
188
188
|
return async (payload) => {
|
|
@@ -200,13 +200,36 @@ function createProgressReporter(extra) {
|
|
|
200
200
|
if (payload.message !== undefined) {
|
|
201
201
|
progressPayload.message = payload.message;
|
|
202
202
|
}
|
|
203
|
-
|
|
203
|
+
const params = {
|
|
204
|
+
progressToken,
|
|
205
|
+
progress: progressPayload.current,
|
|
206
|
+
...(progressPayload.total !== undefined
|
|
207
|
+
? { total: progressPayload.total }
|
|
208
|
+
: {}),
|
|
209
|
+
...(progressPayload.message !== undefined
|
|
210
|
+
? { message: progressPayload.message }
|
|
211
|
+
: {}),
|
|
212
|
+
};
|
|
213
|
+
await extra
|
|
214
|
+
.sendNotification({ method: 'notifications/progress', params })
|
|
215
|
+
.catch(() => {
|
|
216
|
+
// Progress notifications are best-effort; never fail tool execution.
|
|
217
|
+
});
|
|
204
218
|
lastCurrent = current;
|
|
205
219
|
if (total !== undefined && total === current) {
|
|
206
220
|
didSendTerminal = true;
|
|
207
221
|
}
|
|
208
222
|
};
|
|
209
223
|
}
|
|
224
|
+
function getOrCreateProgressReporter(extra) {
|
|
225
|
+
const cached = progressReporterCache.get(extra);
|
|
226
|
+
if (cached) {
|
|
227
|
+
return cached;
|
|
228
|
+
}
|
|
229
|
+
const created = createProgressReporter(extra);
|
|
230
|
+
progressReporterCache.set(extra, created);
|
|
231
|
+
return created;
|
|
232
|
+
}
|
|
210
233
|
function normalizeProgressContext(context) {
|
|
211
234
|
const compact = context?.replace(/\s+/g, ' ').trim();
|
|
212
235
|
if (!compact) {
|
|
@@ -220,16 +243,6 @@ function normalizeProgressContext(context) {
|
|
|
220
243
|
function formatProgressStep(toolName, context, metadata) {
|
|
221
244
|
return `${toolName}: ${context} [${metadata}]`;
|
|
222
245
|
}
|
|
223
|
-
function friendlyModelName(model) {
|
|
224
|
-
if (!model)
|
|
225
|
-
return 'calling model';
|
|
226
|
-
const normalized = model.toLowerCase();
|
|
227
|
-
if (normalized.includes('pro'))
|
|
228
|
-
return 'calling Pro';
|
|
229
|
-
if (normalized.includes('flash'))
|
|
230
|
-
return 'calling Flash';
|
|
231
|
-
return 'calling model';
|
|
232
|
-
}
|
|
233
246
|
function formatProgressCompletion(toolName, context, outcome) {
|
|
234
247
|
return `🗒 ${toolName}: ${context} • ${outcome}`;
|
|
235
248
|
}
|
|
@@ -240,8 +253,10 @@ function createFailureStatusMessage(outcome, errorMessage) {
|
|
|
240
253
|
return errorMessage;
|
|
241
254
|
}
|
|
242
255
|
async function sendSingleStepProgress(extra, toolName, context, current, state) {
|
|
243
|
-
|
|
256
|
+
const reporter = getOrCreateProgressReporter(extra);
|
|
257
|
+
await reporter({
|
|
244
258
|
current,
|
|
259
|
+
total: 1,
|
|
245
260
|
message: current === 0
|
|
246
261
|
? formatProgressStep(toolName, context, state)
|
|
247
262
|
: formatProgressCompletion(toolName, context, state),
|
|
@@ -288,32 +303,6 @@ function asObjectRecord(value) {
|
|
|
288
303
|
}
|
|
289
304
|
return { payload: value };
|
|
290
305
|
}
|
|
291
|
-
function createGeminiLogger(server, taskId) {
|
|
292
|
-
return async (level, data) => {
|
|
293
|
-
try {
|
|
294
|
-
await server.sendLoggingMessage({
|
|
295
|
-
level: toLoggingLevel(level),
|
|
296
|
-
logger: 'gemini',
|
|
297
|
-
data: {
|
|
298
|
-
requestId: getCurrentRequestId(),
|
|
299
|
-
taskId,
|
|
300
|
-
...asObjectRecord(data),
|
|
301
|
-
},
|
|
302
|
-
});
|
|
303
|
-
}
|
|
304
|
-
catch {
|
|
305
|
-
try {
|
|
306
|
-
const timestamp = new Date().toISOString();
|
|
307
|
-
const payload = JSON.stringify(asObjectRecord(data));
|
|
308
|
-
console.error(`[${timestamp}] [gemini:${level}] ${taskId} - ${payload}`);
|
|
309
|
-
}
|
|
310
|
-
catch {
|
|
311
|
-
// Safe fallback if JSON stringify fails
|
|
312
|
-
console.error(`[gemini:${level}] ${taskId} - (logging failed)`);
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
};
|
|
316
|
-
}
|
|
317
306
|
export function wrapToolHandler(options, handler) {
|
|
318
307
|
return async (input, extra) => {
|
|
319
308
|
const context = normalizeProgressContext(options.progressContext?.(input));
|
|
@@ -366,45 +355,52 @@ async function validateRequest(config, inputRecord, ctx) {
|
|
|
366
355
|
}
|
|
367
356
|
return undefined;
|
|
368
357
|
}
|
|
369
|
-
export class
|
|
370
|
-
server;
|
|
358
|
+
export class ToolExecutionRunner {
|
|
371
359
|
config;
|
|
372
|
-
|
|
373
|
-
task;
|
|
360
|
+
signal;
|
|
374
361
|
diffSlotSnapshot;
|
|
375
362
|
hasSnapshot = false;
|
|
376
363
|
responseSchema;
|
|
377
364
|
onLog;
|
|
378
365
|
reportProgress;
|
|
366
|
+
statusReporter;
|
|
379
367
|
progressContext;
|
|
380
|
-
|
|
381
|
-
|
|
368
|
+
lastStatusMessage;
|
|
369
|
+
constructor(config, dependencies, signal) {
|
|
382
370
|
this.config = config;
|
|
383
|
-
this.
|
|
384
|
-
this.
|
|
385
|
-
this.
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
});
|
|
389
|
-
this.onLog = createGeminiLogger(server, task.taskId);
|
|
390
|
-
this.reportProgress = createProgressReporter(extra);
|
|
371
|
+
this.signal = signal;
|
|
372
|
+
this.responseSchema = getCachedGeminiResponseSchema(config);
|
|
373
|
+
this.onLog = dependencies.onLog;
|
|
374
|
+
this.reportProgress = dependencies.reportProgress;
|
|
375
|
+
this.statusReporter = dependencies.statusReporter;
|
|
391
376
|
this.progressContext = DEFAULT_PROGRESS_CONTEXT;
|
|
392
377
|
}
|
|
378
|
+
setResponseSchemaOverride(responseSchema) {
|
|
379
|
+
this.responseSchema = responseSchema;
|
|
380
|
+
responseSchemaCache.set(this.config, responseSchema);
|
|
381
|
+
}
|
|
393
382
|
setDiffSlotSnapshot(diffSlotSnapshot) {
|
|
394
383
|
this.diffSlotSnapshot = diffSlotSnapshot;
|
|
395
384
|
this.hasSnapshot = true;
|
|
396
385
|
}
|
|
397
386
|
async updateStatusMessage(message) {
|
|
387
|
+
if (this.lastStatusMessage === message) {
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
398
390
|
try {
|
|
399
|
-
await this.
|
|
391
|
+
await this.statusReporter.updateStatus(message);
|
|
392
|
+
this.lastStatusMessage = message;
|
|
400
393
|
}
|
|
401
394
|
catch {
|
|
402
395
|
// Best-effort
|
|
403
396
|
}
|
|
404
397
|
}
|
|
405
398
|
async storeResultSafely(status, result) {
|
|
399
|
+
if (!this.statusReporter.storeResult) {
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
406
402
|
try {
|
|
407
|
-
await this.
|
|
403
|
+
await this.statusReporter.storeResult(status, result);
|
|
408
404
|
}
|
|
409
405
|
catch (storeErr) {
|
|
410
406
|
await this.onLog('error', {
|
|
@@ -432,9 +428,9 @@ export class ToolTaskRunner {
|
|
|
432
428
|
await this.updateStatusMessage(validationMessage);
|
|
433
429
|
await reportProgressCompletionUpdate(this.reportProgress, this.config.name, this.progressContext, 'rejected');
|
|
434
430
|
await this.storeResultSafely('completed', validationError);
|
|
435
|
-
return
|
|
431
|
+
return validationError;
|
|
436
432
|
}
|
|
437
|
-
return
|
|
433
|
+
return undefined;
|
|
438
434
|
}
|
|
439
435
|
async executeModelCall(systemInstruction, prompt) {
|
|
440
436
|
let parsed;
|
|
@@ -442,7 +438,7 @@ export class ToolTaskRunner {
|
|
|
442
438
|
const maxRetries = this.config.schemaRetries ?? geminiSchemaRetriesConfig.get();
|
|
443
439
|
for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
|
|
444
440
|
try {
|
|
445
|
-
const raw = await generateStructuredJson(createGenerationRequest(this.config, { systemInstruction, prompt: retryPrompt }, this.responseSchema, this.onLog, this.
|
|
441
|
+
const raw = await generateStructuredJson(createGenerationRequest(this.config, { systemInstruction, prompt: retryPrompt }, this.responseSchema, this.onLog, this.signal));
|
|
446
442
|
if (attempt === 0) {
|
|
447
443
|
await this.updateStatusMessage('validating response');
|
|
448
444
|
await reportProgressStepUpdate(this.reportProgress, this.config.name, this.progressContext, STEP_VALIDATING_RESPONSE, 'validating response');
|
|
@@ -477,8 +473,6 @@ export class ToolTaskRunner {
|
|
|
477
473
|
try {
|
|
478
474
|
const inputRecord = parseToolInput(input, this.config.fullInputSchema);
|
|
479
475
|
this.progressContext = normalizeProgressContext(this.config.progressContext?.(inputRecord));
|
|
480
|
-
// Prefer createTask snapshot; fallback preserves backward compatibility
|
|
481
|
-
// for any direct constructor callers.
|
|
482
476
|
const ctx = {
|
|
483
477
|
diffSlot: this.hasSnapshot ? this.diffSlotSnapshot : getDiff(),
|
|
484
478
|
};
|
|
@@ -486,82 +480,89 @@ export class ToolTaskRunner {
|
|
|
486
480
|
await this.updateStatusMessage('starting');
|
|
487
481
|
await reportProgressStepUpdate(this.reportProgress, this.config.name, this.progressContext, STEP_VALIDATING, 'validating input');
|
|
488
482
|
await this.updateStatusMessage('validating input');
|
|
489
|
-
|
|
490
|
-
|
|
483
|
+
const validationError = await this.executeValidation(inputRecord, ctx);
|
|
484
|
+
if (validationError) {
|
|
485
|
+
return validationError;
|
|
491
486
|
}
|
|
492
487
|
await reportProgressStepUpdate(this.reportProgress, this.config.name, this.progressContext, STEP_BUILDING_PROMPT, 'building prompt');
|
|
493
488
|
await this.updateStatusMessage('building prompt');
|
|
494
489
|
const promptParts = this.config.buildPrompt(inputRecord, ctx);
|
|
495
490
|
const { prompt, systemInstruction } = promptParts;
|
|
496
|
-
|
|
497
|
-
await
|
|
498
|
-
await this.updateStatusMessage(modelLabel);
|
|
491
|
+
await reportProgressStepUpdate(this.reportProgress, this.config.name, this.progressContext, STEP_CALLING_MODEL, 'calling model');
|
|
492
|
+
await this.updateStatusMessage('calling model');
|
|
499
493
|
const parsed = await this.executeModelCall(systemInstruction, prompt);
|
|
500
494
|
await reportProgressStepUpdate(this.reportProgress, this.config.name, this.progressContext, STEP_FINALIZING, 'finalizing');
|
|
501
495
|
await this.updateStatusMessage('finalizing');
|
|
502
496
|
const finalResult = (this.config.transformResult
|
|
503
497
|
? this.config.transformResult(inputRecord, parsed, ctx)
|
|
504
498
|
: parsed);
|
|
505
|
-
|
|
499
|
+
let textContent = this.config.formatOutput
|
|
506
500
|
? this.config.formatOutput(finalResult)
|
|
507
501
|
: undefined;
|
|
502
|
+
if (ctx.diffSlot) {
|
|
503
|
+
const ageMs = Date.now() - new Date(ctx.diffSlot.generatedAt).getTime();
|
|
504
|
+
if (ageMs > diffStaleWarningMs.get()) {
|
|
505
|
+
const ageMinutes = Math.round(ageMs / 60_000);
|
|
506
|
+
const warning = `\n\n⚠️ Warning: The analyzed diff is over ${ageMinutes} minutes old. If you have made recent changes, please run generate_diff again.`;
|
|
507
|
+
textContent = textContent ? textContent + warning : warning;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
508
510
|
const outcome = this.config.formatOutcome?.(finalResult) ?? 'completed';
|
|
509
511
|
await reportProgressCompletionUpdate(this.reportProgress, this.config.name, this.progressContext, outcome);
|
|
510
512
|
await this.updateStatusMessage(`completed: ${outcome}`);
|
|
511
|
-
|
|
513
|
+
const successResponse = createToolResponse({
|
|
512
514
|
ok: true,
|
|
513
515
|
result: finalResult,
|
|
514
|
-
}, textContent)
|
|
516
|
+
}, textContent);
|
|
517
|
+
await this.storeResultSafely('completed', successResponse);
|
|
518
|
+
return successResponse;
|
|
515
519
|
}
|
|
516
520
|
catch (error) {
|
|
517
521
|
const errorMessage = getErrorMessage(error);
|
|
518
522
|
const errorMeta = classifyErrorMeta(error, errorMessage);
|
|
519
523
|
const outcome = errorMeta.kind === 'cancelled' ? 'cancelled' : 'failed';
|
|
520
524
|
await this.updateStatusMessage(createFailureStatusMessage(outcome, errorMessage));
|
|
521
|
-
|
|
525
|
+
const errorResponse = createErrorToolResponse(this.config.errorCode, errorMessage, undefined, errorMeta);
|
|
526
|
+
await this.storeResultSafely('failed', errorResponse);
|
|
522
527
|
await reportProgressCompletionUpdate(this.reportProgress, this.config.name, this.progressContext, outcome);
|
|
528
|
+
return errorResponse; // Return safe error response
|
|
523
529
|
}
|
|
524
530
|
}
|
|
525
531
|
}
|
|
526
532
|
export function registerStructuredToolTask(server, config) {
|
|
527
|
-
|
|
533
|
+
const responseSchema = createGeminiResponseSchema({
|
|
534
|
+
geminiSchema: config.geminiSchema,
|
|
535
|
+
resultSchema: config.resultSchema,
|
|
536
|
+
});
|
|
537
|
+
server.registerTool(config.name, {
|
|
528
538
|
title: config.title,
|
|
529
539
|
description: config.description,
|
|
530
540
|
inputSchema: config.inputSchema,
|
|
531
541
|
outputSchema: DefaultOutputSchema,
|
|
532
542
|
annotations: buildToolAnnotations(config.annotations),
|
|
533
|
-
}, {
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
});
|
|
558
|
-
return { task };
|
|
559
|
-
},
|
|
560
|
-
getTask: async (_input, extra) => {
|
|
561
|
-
return await extra.taskStore.getTask(extra.taskId);
|
|
562
|
-
},
|
|
563
|
-
getTaskResult: async (_input, extra) => {
|
|
564
|
-
return (await extra.taskStore.getTaskResult(extra.taskId));
|
|
565
|
-
},
|
|
543
|
+
}, async (input, extra) => {
|
|
544
|
+
const runner = new ToolExecutionRunner(config, {
|
|
545
|
+
onLog: async (level, data) => {
|
|
546
|
+
// Standard logging for tool calls
|
|
547
|
+
try {
|
|
548
|
+
await server.sendLoggingMessage({
|
|
549
|
+
level: toLoggingLevel(level),
|
|
550
|
+
logger: 'gemini',
|
|
551
|
+
data: asObjectRecord(data),
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
catch {
|
|
555
|
+
// Fallback if logging fails
|
|
556
|
+
}
|
|
557
|
+
},
|
|
558
|
+
reportProgress: createProgressReporter(extra),
|
|
559
|
+
statusReporter: {
|
|
560
|
+
updateStatus: async () => {
|
|
561
|
+
// No-op for standard tool calls as they don't have a persistent task status
|
|
562
|
+
},
|
|
563
|
+
},
|
|
564
|
+
});
|
|
565
|
+
runner.setResponseSchemaOverride(responseSchema);
|
|
566
|
+
return await runner.run(input);
|
|
566
567
|
});
|
|
567
568
|
}
|
package/dist/resources/index.js
CHANGED
|
@@ -83,7 +83,7 @@ function registerToolInfoResources(server) {
|
|
|
83
83
|
}
|
|
84
84
|
export const DIFF_RESOURCE_DESCRIPTION = 'The most recently generated diff, cached by generate_diff. Read by all review tools automatically.';
|
|
85
85
|
function registerDiffResource(server) {
|
|
86
|
-
server.registerResource('diff-current',
|
|
86
|
+
server.registerResource('diff-current', DIFF_RESOURCE_URI, {
|
|
87
87
|
title: 'Current Diff',
|
|
88
88
|
description: DIFF_RESOURCE_DESCRIPTION,
|
|
89
89
|
mimeType: PATCH_MIME_TYPE,
|
|
@@ -22,7 +22,7 @@ ${parameterLines.join('\n')}
|
|
|
22
22
|
- **Output**: \`${contract.outputShape}\``;
|
|
23
23
|
}
|
|
24
24
|
const modelInfo = [
|
|
25
|
-
|
|
25
|
+
'Flash',
|
|
26
26
|
contract.thinkingLevel ? `Thinking:${contract.thinkingLevel}` : '',
|
|
27
27
|
`${Math.round(contract.timeoutMs / 1_000)}s`,
|
|
28
28
|
`MaxTokens:${contract.maxOutputTokens}`,
|
|
@@ -26,8 +26,8 @@ generate_review_summary ──→ overallRisk ──────┤
|
|
|
26
26
|
|
|
27
27
|
## When to Use Each Tool
|
|
28
28
|
|
|
29
|
-
- **Triage**: \`analyze_pr_impact\`, \`generate_review_summary
|
|
30
|
-
- **Inspection**: \`inspect_code_quality
|
|
29
|
+
- **Triage**: \`analyze_pr_impact\`, \`generate_review_summary\`.
|
|
30
|
+
- **Inspection**: \`inspect_code_quality\`.
|
|
31
31
|
- **Fixes**: \`suggest_search_replace\` (one finding/call).
|
|
32
32
|
- **Tests**: \`generate_test_plan\`.
|
|
33
33
|
- **Complexity**: \`analyze_time_space_complexity\`.
|
|
@@ -19,7 +19,6 @@ export function registerAnalyzeComplexityTool(server) {
|
|
|
19
19
|
fullInputSchema: AnalyzeComplexityInputSchema,
|
|
20
20
|
resultSchema: AnalyzeComplexityResultSchema,
|
|
21
21
|
errorCode: 'E_ANALYZE_COMPLEXITY',
|
|
22
|
-
model: TOOL_CONTRACT.model,
|
|
23
22
|
timeoutMs: TOOL_CONTRACT.timeoutMs,
|
|
24
23
|
maxOutputTokens: TOOL_CONTRACT.maxOutputTokens,
|
|
25
24
|
...buildStructuredToolRuntimeOptions(TOOL_CONTRACT),
|
|
@@ -22,7 +22,6 @@ export function registerAnalyzePrImpactTool(server) {
|
|
|
22
22
|
fullInputSchema: AnalyzePrImpactInputSchema,
|
|
23
23
|
resultSchema: PrImpactResultSchema,
|
|
24
24
|
errorCode: 'E_ANALYZE_IMPACT',
|
|
25
|
-
model: TOOL_CONTRACT.model,
|
|
26
25
|
timeoutMs: TOOL_CONTRACT.timeoutMs,
|
|
27
26
|
maxOutputTokens: TOOL_CONTRACT.maxOutputTokens,
|
|
28
27
|
...buildStructuredToolRuntimeOptions(TOOL_CONTRACT),
|
|
@@ -19,7 +19,6 @@ export function registerDetectApiBreakingTool(server) {
|
|
|
19
19
|
fullInputSchema: DetectApiBreakingInputSchema,
|
|
20
20
|
resultSchema: DetectApiBreakingResultSchema,
|
|
21
21
|
errorCode: 'E_DETECT_API_BREAKING',
|
|
22
|
-
model: TOOL_CONTRACT.model,
|
|
23
22
|
timeoutMs: TOOL_CONTRACT.timeoutMs,
|
|
24
23
|
maxOutputTokens: TOOL_CONTRACT.maxOutputTokens,
|
|
25
24
|
...buildStructuredToolRuntimeOptions(TOOL_CONTRACT),
|
|
@@ -9,12 +9,19 @@ import { DefaultOutputSchema } from '../schemas/outputs.js';
|
|
|
9
9
|
const GIT_TIMEOUT_MS = 30_000;
|
|
10
10
|
const GIT_MAX_BUFFER = 10 * 1024 * 1024; // 10 MB
|
|
11
11
|
const execFileAsync = promisify(execFile);
|
|
12
|
-
|
|
12
|
+
const gitRootByCwd = new Map();
|
|
13
|
+
async function findGitRoot(cwd = process.cwd()) {
|
|
14
|
+
const cached = gitRootByCwd.get(cwd);
|
|
15
|
+
if (cached) {
|
|
16
|
+
return cached;
|
|
17
|
+
}
|
|
13
18
|
const { stdout } = await execFileAsync('git', ['rev-parse', '--show-toplevel'], {
|
|
14
|
-
cwd
|
|
19
|
+
cwd,
|
|
15
20
|
encoding: 'utf8',
|
|
16
21
|
});
|
|
17
|
-
|
|
22
|
+
const gitRoot = stdout.trim();
|
|
23
|
+
gitRootByCwd.set(cwd, gitRoot);
|
|
24
|
+
return gitRoot;
|
|
18
25
|
}
|
|
19
26
|
function buildGitArgs(mode) {
|
|
20
27
|
const args = ['diff', '--no-color', '--no-ext-diff'];
|
|
@@ -36,7 +36,6 @@ export function registerGenerateReviewSummaryTool(server) {
|
|
|
36
36
|
fullInputSchema: GenerateReviewSummaryInputSchema,
|
|
37
37
|
resultSchema: ReviewSummaryModelSchema,
|
|
38
38
|
errorCode: 'E_REVIEW_SUMMARY',
|
|
39
|
-
model: TOOL_CONTRACT.model,
|
|
40
39
|
timeoutMs: TOOL_CONTRACT.timeoutMs,
|
|
41
40
|
maxOutputTokens: TOOL_CONTRACT.maxOutputTokens,
|
|
42
41
|
...buildStructuredToolRuntimeOptions(TOOL_CONTRACT),
|
|
@@ -23,7 +23,6 @@ export function registerGenerateTestPlanTool(server) {
|
|
|
23
23
|
fullInputSchema: GenerateTestPlanInputSchema,
|
|
24
24
|
resultSchema: TestPlanResultSchema,
|
|
25
25
|
errorCode: 'E_GENERATE_TEST_PLAN',
|
|
26
|
-
model: TOOL_CONTRACT.model,
|
|
27
26
|
timeoutMs: TOOL_CONTRACT.timeoutMs,
|
|
28
27
|
maxOutputTokens: TOOL_CONTRACT.maxOutputTokens,
|
|
29
28
|
...buildStructuredToolRuntimeOptions(TOOL_CONTRACT),
|
|
@@ -28,7 +28,6 @@ export function registerInspectCodeQualityTool(server) {
|
|
|
28
28
|
resultSchema: CodeQualityOutputSchema,
|
|
29
29
|
geminiSchema: CodeQualityResultSchema,
|
|
30
30
|
errorCode: 'E_INSPECT_QUALITY',
|
|
31
|
-
model: TOOL_CONTRACT.model,
|
|
32
31
|
timeoutMs: TOOL_CONTRACT.timeoutMs,
|
|
33
32
|
maxOutputTokens: TOOL_CONTRACT.maxOutputTokens,
|
|
34
33
|
...buildStructuredToolRuntimeOptions(TOOL_CONTRACT),
|
|
@@ -23,7 +23,6 @@ export function registerSuggestSearchReplaceTool(server) {
|
|
|
23
23
|
fullInputSchema: SuggestSearchReplaceInputSchema,
|
|
24
24
|
resultSchema: SearchReplaceResultSchema,
|
|
25
25
|
errorCode: 'E_SUGGEST_SEARCH_REPLACE',
|
|
26
|
-
model: TOOL_CONTRACT.model,
|
|
27
26
|
timeoutMs: TOOL_CONTRACT.timeoutMs,
|
|
28
27
|
maxOutputTokens: TOOL_CONTRACT.maxOutputTokens,
|
|
29
28
|
...buildStructuredToolRuntimeOptions(TOOL_CONTRACT),
|