@j0hanz/code-review-analyst-mcp 1.6.4 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/gemini.js +44 -16
- package/dist/lib/tool-factory.d.ts +3 -1
- package/dist/lib/tool-factory.js +68 -22
- package/dist/tools/generate-diff.js +10 -3
- package/package.json +1 -1
package/dist/lib/gemini.js
CHANGED
|
@@ -38,6 +38,7 @@ 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;
|
|
41
42
|
const maxConcurrentCallsConfig = createCachedEnvInt('MAX_CONCURRENT_CALLS', 10);
|
|
42
43
|
const maxConcurrentBatchCallsConfig = createCachedEnvInt('MAX_CONCURRENT_BATCH_CALLS', 2);
|
|
43
44
|
const concurrencyWaitMsConfig = createCachedEnvInt('MAX_CONCURRENT_CALLS_WAIT_MS', 2_000);
|
|
@@ -45,8 +46,8 @@ const batchPollIntervalMsConfig = createCachedEnvInt('GEMINI_BATCH_POLL_INTERVAL
|
|
|
45
46
|
const batchTimeoutMsConfig = createCachedEnvInt('GEMINI_BATCH_TIMEOUT_MS', 120_000);
|
|
46
47
|
let activeCalls = 0;
|
|
47
48
|
let activeBatchCalls = 0;
|
|
48
|
-
const slotWaiters =
|
|
49
|
-
const batchSlotWaiters =
|
|
49
|
+
const slotWaiters = new Set();
|
|
50
|
+
const batchSlotWaiters = new Set();
|
|
50
51
|
const RETRYABLE_TRANSIENT_CODES = new Set([
|
|
51
52
|
'RESOURCE_EXHAUSTED',
|
|
52
53
|
'UNAVAILABLE',
|
|
@@ -54,6 +55,36 @@ const RETRYABLE_TRANSIENT_CODES = new Set([
|
|
|
54
55
|
'INTERNAL',
|
|
55
56
|
'ABORTED',
|
|
56
57
|
]);
|
|
58
|
+
function getWaiterCount(waiters) {
|
|
59
|
+
return waiters instanceof Set ? waiters.size : waiters.length;
|
|
60
|
+
}
|
|
61
|
+
function addWaiter(waiters, waiter) {
|
|
62
|
+
if (waiters instanceof Set) {
|
|
63
|
+
waiters.add(waiter);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
waiters.push(waiter);
|
|
67
|
+
}
|
|
68
|
+
function removeWaiter(waiters, waiter) {
|
|
69
|
+
if (waiters instanceof Set) {
|
|
70
|
+
waiters.delete(waiter);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const index = waiters.indexOf(waiter);
|
|
74
|
+
if (index !== -1) {
|
|
75
|
+
waiters.splice(index, 1);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function popNextWaiter(waiters) {
|
|
79
|
+
if (waiters instanceof Set) {
|
|
80
|
+
const next = waiters.values().next().value;
|
|
81
|
+
if (next !== undefined) {
|
|
82
|
+
waiters.delete(next);
|
|
83
|
+
}
|
|
84
|
+
return next;
|
|
85
|
+
}
|
|
86
|
+
return waiters.shift();
|
|
87
|
+
}
|
|
57
88
|
const SAFETY_CATEGORIES = [
|
|
58
89
|
HarmCategory.HARM_CATEGORY_HATE_SPEECH,
|
|
59
90
|
HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT,
|
|
@@ -371,7 +402,7 @@ function parseStructuredResponse(responseText) {
|
|
|
371
402
|
catch {
|
|
372
403
|
// fast-path failed; try extracting from markdown block
|
|
373
404
|
}
|
|
374
|
-
const jsonMatch =
|
|
405
|
+
const jsonMatch = JSON_CODE_BLOCK_PATTERN.exec(responseText);
|
|
375
406
|
const jsonText = jsonMatch?.[1] ?? responseText;
|
|
376
407
|
try {
|
|
377
408
|
return JSON.parse(jsonText);
|
|
@@ -501,13 +532,13 @@ function canRetryAttempt(attempt, maxRetries, error) {
|
|
|
501
532
|
return attempt < maxRetries && shouldRetry(error);
|
|
502
533
|
}
|
|
503
534
|
function tryWakeNextWaiter() {
|
|
504
|
-
const next = slotWaiters
|
|
535
|
+
const next = popNextWaiter(slotWaiters);
|
|
505
536
|
if (next !== undefined) {
|
|
506
537
|
next();
|
|
507
538
|
}
|
|
508
539
|
}
|
|
509
540
|
async function waitForSlot(limit, getActiveCount, acquireSlot, waiters, requestSignal) {
|
|
510
|
-
if (waiters
|
|
541
|
+
if (getWaiterCount(waiters) === 0 && getActiveCount() < limit) {
|
|
511
542
|
acquireSlot();
|
|
512
543
|
return;
|
|
513
544
|
}
|
|
@@ -526,12 +557,9 @@ async function waitForSlot(limit, getActiveCount, acquireSlot, waiters, requestS
|
|
|
526
557
|
acquireSlot();
|
|
527
558
|
resolve();
|
|
528
559
|
};
|
|
529
|
-
waiters
|
|
530
|
-
const
|
|
531
|
-
|
|
532
|
-
if (idx !== -1) {
|
|
533
|
-
waiters.splice(idx, 1);
|
|
534
|
-
}
|
|
560
|
+
addWaiter(waiters, waiter);
|
|
561
|
+
const removeCurrentWaiter = () => {
|
|
562
|
+
removeWaiter(waiters, waiter);
|
|
535
563
|
};
|
|
536
564
|
const detachAbortListener = () => {
|
|
537
565
|
if (requestSignal) {
|
|
@@ -542,7 +570,7 @@ async function waitForSlot(limit, getActiveCount, acquireSlot, waiters, requestS
|
|
|
542
570
|
if (settled)
|
|
543
571
|
return;
|
|
544
572
|
settled = true;
|
|
545
|
-
|
|
573
|
+
removeCurrentWaiter();
|
|
546
574
|
detachAbortListener();
|
|
547
575
|
reject(new Error(formatConcurrencyLimitErrorMessage(limit, waitLimitMs)));
|
|
548
576
|
}, waitLimitMs);
|
|
@@ -551,7 +579,7 @@ async function waitForSlot(limit, getActiveCount, acquireSlot, waiters, requestS
|
|
|
551
579
|
if (settled)
|
|
552
580
|
return;
|
|
553
581
|
settled = true;
|
|
554
|
-
|
|
582
|
+
removeCurrentWaiter();
|
|
555
583
|
clearTimeout(deadlineTimer);
|
|
556
584
|
reject(new Error('Gemini request was cancelled.'));
|
|
557
585
|
};
|
|
@@ -566,7 +594,7 @@ async function waitForConcurrencySlot(limit, requestSignal) {
|
|
|
566
594
|
}, slotWaiters, requestSignal);
|
|
567
595
|
}
|
|
568
596
|
function tryWakeNextBatchWaiter() {
|
|
569
|
-
const next = batchSlotWaiters
|
|
597
|
+
const next = popNextWaiter(batchSlotWaiters);
|
|
570
598
|
if (next !== undefined) {
|
|
571
599
|
next();
|
|
572
600
|
}
|
|
@@ -770,7 +798,7 @@ async function runInlineBatchWithPolling(request, model, onLog) {
|
|
|
770
798
|
export function getGeminiQueueSnapshot() {
|
|
771
799
|
return {
|
|
772
800
|
activeCalls,
|
|
773
|
-
waitingCalls: slotWaiters.
|
|
801
|
+
waitingCalls: slotWaiters.size,
|
|
774
802
|
};
|
|
775
803
|
}
|
|
776
804
|
export async function generateStructuredJson(request) {
|
|
@@ -793,7 +821,7 @@ export async function generateStructuredJson(request) {
|
|
|
793
821
|
await safeCallOnLog(onLog, 'info', {
|
|
794
822
|
event: 'gemini_queue_acquired',
|
|
795
823
|
queueWaitMs,
|
|
796
|
-
waitingCalls: batchMode === 'inline' ? batchSlotWaiters.
|
|
824
|
+
waitingCalls: batchMode === 'inline' ? batchSlotWaiters.size : slotWaiters.size,
|
|
797
825
|
activeCalls,
|
|
798
826
|
activeBatchCalls,
|
|
799
827
|
mode: batchMode,
|
|
@@ -111,11 +111,13 @@ export declare class ToolTaskRunner<TInput extends object, TResult extends objec
|
|
|
111
111
|
private readonly task;
|
|
112
112
|
private diffSlotSnapshot;
|
|
113
113
|
private hasSnapshot;
|
|
114
|
-
private
|
|
114
|
+
private responseSchema;
|
|
115
115
|
private readonly onLog;
|
|
116
116
|
private readonly reportProgress;
|
|
117
117
|
private progressContext;
|
|
118
|
+
private lastStatusMessage;
|
|
118
119
|
constructor(server: McpServer, config: StructuredToolTaskConfig<TInput, TResult, TFinal>, extra: CreateTaskRequestHandlerExtra, task: TaskLike);
|
|
120
|
+
setResponseSchemaOverride(responseSchema: Record<string, unknown>): void;
|
|
119
121
|
setDiffSlotSnapshot(diffSlotSnapshot: DiffSlot | undefined): void;
|
|
120
122
|
private updateStatusMessage;
|
|
121
123
|
private storeResultSafely;
|
package/dist/lib/tool-factory.js
CHANGED
|
@@ -28,6 +28,9 @@ const DEFAULT_SCHEMA_RETRY_ERROR_CHARS = 1_500;
|
|
|
28
28
|
const schemaRetryErrorCharsConfig = createCachedEnvInt('MAX_SCHEMA_RETRY_ERROR_CHARS', DEFAULT_SCHEMA_RETRY_ERROR_CHARS);
|
|
29
29
|
const DETERMINISTIC_JSON_RETRY_NOTE = 'Deterministic JSON mode: keep key names exactly as schema-defined and preserve stable field ordering.';
|
|
30
30
|
const JSON_PARSE_ERROR_PATTERN = /model produced invalid json/i;
|
|
31
|
+
const MODEL_IMMEDIATE_RESPONSE_META_KEY = 'io.modelcontextprotocol/model-immediate-response';
|
|
32
|
+
const responseSchemaCache = new WeakMap();
|
|
33
|
+
const progressReporterCache = new WeakMap();
|
|
31
34
|
function buildToolAnnotations(annotations) {
|
|
32
35
|
if (!annotations) {
|
|
33
36
|
return {
|
|
@@ -49,6 +52,18 @@ function createGeminiResponseSchema(config) {
|
|
|
49
52
|
const sourceSchema = config.geminiSchema ?? config.resultSchema;
|
|
50
53
|
return stripJsonSchemaConstraints(z.toJSONSchema(sourceSchema));
|
|
51
54
|
}
|
|
55
|
+
function getCachedGeminiResponseSchema(config) {
|
|
56
|
+
const cached = responseSchemaCache.get(config);
|
|
57
|
+
if (cached) {
|
|
58
|
+
return cached;
|
|
59
|
+
}
|
|
60
|
+
const responseSchema = createGeminiResponseSchema({
|
|
61
|
+
geminiSchema: config.geminiSchema,
|
|
62
|
+
resultSchema: config.resultSchema,
|
|
63
|
+
});
|
|
64
|
+
responseSchemaCache.set(config, responseSchema);
|
|
65
|
+
return responseSchema;
|
|
66
|
+
}
|
|
52
67
|
function parseToolInput(input, fullInputSchema) {
|
|
53
68
|
return fullInputSchema.parse(input);
|
|
54
69
|
}
|
|
@@ -165,24 +180,14 @@ function isRetryableUpstreamMessage(message) {
|
|
|
165
180
|
return (RETRYABLE_UPSTREAM_ERROR_PATTERN.test(message) ||
|
|
166
181
|
BUSY_ERROR_PATTERN.test(message));
|
|
167
182
|
}
|
|
168
|
-
function
|
|
183
|
+
function createProgressReporter(extra) {
|
|
169
184
|
const rawToken = extra._meta?.progressToken;
|
|
170
185
|
if (typeof rawToken !== 'string' && typeof rawToken !== 'number') {
|
|
171
|
-
return
|
|
186
|
+
return async () => {
|
|
187
|
+
// Request did not provide a progress token.
|
|
188
|
+
};
|
|
172
189
|
}
|
|
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) {
|
|
190
|
+
const progressToken = rawToken;
|
|
186
191
|
let lastCurrent = 0;
|
|
187
192
|
let didSendTerminal = false;
|
|
188
193
|
return async (payload) => {
|
|
@@ -200,13 +205,36 @@ function createProgressReporter(extra) {
|
|
|
200
205
|
if (payload.message !== undefined) {
|
|
201
206
|
progressPayload.message = payload.message;
|
|
202
207
|
}
|
|
203
|
-
|
|
208
|
+
const params = {
|
|
209
|
+
progressToken,
|
|
210
|
+
progress: progressPayload.current,
|
|
211
|
+
...(progressPayload.total !== undefined
|
|
212
|
+
? { total: progressPayload.total }
|
|
213
|
+
: {}),
|
|
214
|
+
...(progressPayload.message !== undefined
|
|
215
|
+
? { message: progressPayload.message }
|
|
216
|
+
: {}),
|
|
217
|
+
};
|
|
218
|
+
await extra
|
|
219
|
+
.sendNotification({ method: 'notifications/progress', params })
|
|
220
|
+
.catch(() => {
|
|
221
|
+
// Progress notifications are best-effort; never fail tool execution.
|
|
222
|
+
});
|
|
204
223
|
lastCurrent = current;
|
|
205
224
|
if (total !== undefined && total === current) {
|
|
206
225
|
didSendTerminal = true;
|
|
207
226
|
}
|
|
208
227
|
};
|
|
209
228
|
}
|
|
229
|
+
function getOrCreateProgressReporter(extra) {
|
|
230
|
+
const cached = progressReporterCache.get(extra);
|
|
231
|
+
if (cached) {
|
|
232
|
+
return cached;
|
|
233
|
+
}
|
|
234
|
+
const created = createProgressReporter(extra);
|
|
235
|
+
progressReporterCache.set(extra, created);
|
|
236
|
+
return created;
|
|
237
|
+
}
|
|
210
238
|
function normalizeProgressContext(context) {
|
|
211
239
|
const compact = context?.replace(/\s+/g, ' ').trim();
|
|
212
240
|
if (!compact) {
|
|
@@ -240,8 +268,10 @@ function createFailureStatusMessage(outcome, errorMessage) {
|
|
|
240
268
|
return errorMessage;
|
|
241
269
|
}
|
|
242
270
|
async function sendSingleStepProgress(extra, toolName, context, current, state) {
|
|
243
|
-
|
|
271
|
+
const reporter = getOrCreateProgressReporter(extra);
|
|
272
|
+
await reporter({
|
|
244
273
|
current,
|
|
274
|
+
total: 1,
|
|
245
275
|
message: current === 0
|
|
246
276
|
? formatProgressStep(toolName, context, state)
|
|
247
277
|
: formatProgressCompletion(toolName, context, state),
|
|
@@ -377,26 +407,32 @@ export class ToolTaskRunner {
|
|
|
377
407
|
onLog;
|
|
378
408
|
reportProgress;
|
|
379
409
|
progressContext;
|
|
410
|
+
lastStatusMessage;
|
|
380
411
|
constructor(server, config, extra, task) {
|
|
381
412
|
this.server = server;
|
|
382
413
|
this.config = config;
|
|
383
414
|
this.extra = extra;
|
|
384
415
|
this.task = task;
|
|
385
|
-
this.responseSchema =
|
|
386
|
-
geminiSchema: config.geminiSchema,
|
|
387
|
-
resultSchema: config.resultSchema,
|
|
388
|
-
});
|
|
416
|
+
this.responseSchema = getCachedGeminiResponseSchema(config);
|
|
389
417
|
this.onLog = createGeminiLogger(server, task.taskId);
|
|
390
418
|
this.reportProgress = createProgressReporter(extra);
|
|
391
419
|
this.progressContext = DEFAULT_PROGRESS_CONTEXT;
|
|
392
420
|
}
|
|
421
|
+
setResponseSchemaOverride(responseSchema) {
|
|
422
|
+
this.responseSchema = responseSchema;
|
|
423
|
+
responseSchemaCache.set(this.config, responseSchema);
|
|
424
|
+
}
|
|
393
425
|
setDiffSlotSnapshot(diffSlotSnapshot) {
|
|
394
426
|
this.diffSlotSnapshot = diffSlotSnapshot;
|
|
395
427
|
this.hasSnapshot = true;
|
|
396
428
|
}
|
|
397
429
|
async updateStatusMessage(message) {
|
|
430
|
+
if (this.lastStatusMessage === message) {
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
398
433
|
try {
|
|
399
434
|
await this.extra.taskStore.updateTaskStatus(this.task.taskId, 'working', message);
|
|
435
|
+
this.lastStatusMessage = message;
|
|
400
436
|
}
|
|
401
437
|
catch {
|
|
402
438
|
// Best-effort
|
|
@@ -524,6 +560,10 @@ export class ToolTaskRunner {
|
|
|
524
560
|
}
|
|
525
561
|
}
|
|
526
562
|
export function registerStructuredToolTask(server, config) {
|
|
563
|
+
const responseSchema = createGeminiResponseSchema({
|
|
564
|
+
geminiSchema: config.geminiSchema,
|
|
565
|
+
resultSchema: config.resultSchema,
|
|
566
|
+
});
|
|
527
567
|
server.experimental.tasks.registerToolTask(config.name, {
|
|
528
568
|
title: config.title,
|
|
529
569
|
description: config.description,
|
|
@@ -540,6 +580,7 @@ export function registerStructuredToolTask(server, config) {
|
|
|
540
580
|
// preserves task-level TOCTOU safety without deep-clone overhead.
|
|
541
581
|
const diffSlotSnapshot = currentDiff;
|
|
542
582
|
const runner = new ToolTaskRunner(server, config, extra, task);
|
|
583
|
+
runner.setResponseSchemaOverride(responseSchema);
|
|
543
584
|
runner.setDiffSlotSnapshot(diffSlotSnapshot);
|
|
544
585
|
setImmediate(() => {
|
|
545
586
|
void runner.run(input).catch(async (error) => {
|
|
@@ -555,7 +596,12 @@ export function registerStructuredToolTask(server, config) {
|
|
|
555
596
|
}
|
|
556
597
|
});
|
|
557
598
|
});
|
|
558
|
-
return {
|
|
599
|
+
return {
|
|
600
|
+
task,
|
|
601
|
+
_meta: {
|
|
602
|
+
[MODEL_IMMEDIATE_RESPONSE_META_KEY]: `${config.name} accepted as task ${task.taskId}`,
|
|
603
|
+
},
|
|
604
|
+
};
|
|
559
605
|
},
|
|
560
606
|
getTask: async (_input, extra) => {
|
|
561
607
|
return await extra.taskStore.getTask(extra.taskId);
|
|
@@ -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'];
|