@chrischall/gemini-mcp 1.7.0 → 1.8.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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/dist/bundle.js +474 -70
- package/dist/client.js +382 -34
- package/dist/fetch-image.js +19 -7
- package/dist/jobs.js +71 -2
- package/dist/tools/interact.js +9 -0
- package/dist/tools/jobs.js +128 -3
- package/dist/tools/music.js +5 -1
- package/dist/tools/video.js +14 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/server.json +2 -2
package/dist/bundle.js
CHANGED
|
@@ -459,15 +459,15 @@ var init_parseUtil = __esm({
|
|
|
459
459
|
message: issueData.message
|
|
460
460
|
};
|
|
461
461
|
}
|
|
462
|
-
let
|
|
462
|
+
let errorMessage2 = "";
|
|
463
463
|
const maps = errorMaps.filter((m) => !!m).slice().reverse();
|
|
464
464
|
for (const map2 of maps) {
|
|
465
|
-
|
|
465
|
+
errorMessage2 = map2(fullIssue, { data, defaultError: errorMessage2 }).message;
|
|
466
466
|
}
|
|
467
467
|
return {
|
|
468
468
|
...issueData,
|
|
469
469
|
path: fullPath,
|
|
470
|
-
message:
|
|
470
|
+
message: errorMessage2
|
|
471
471
|
};
|
|
472
472
|
};
|
|
473
473
|
ParseStatus = class _ParseStatus {
|
|
@@ -20948,19 +20948,19 @@ var init_Refs = __esm({
|
|
|
20948
20948
|
});
|
|
20949
20949
|
|
|
20950
20950
|
// node_modules/zod-to-json-schema/dist/esm/errorMessages.js
|
|
20951
|
-
function addErrorMessage(res, key,
|
|
20951
|
+
function addErrorMessage(res, key, errorMessage2, refs) {
|
|
20952
20952
|
if (!refs?.errorMessages)
|
|
20953
20953
|
return;
|
|
20954
|
-
if (
|
|
20954
|
+
if (errorMessage2) {
|
|
20955
20955
|
res.errorMessage = {
|
|
20956
20956
|
...res.errorMessage,
|
|
20957
|
-
[key]:
|
|
20957
|
+
[key]: errorMessage2
|
|
20958
20958
|
};
|
|
20959
20959
|
}
|
|
20960
20960
|
}
|
|
20961
|
-
function setResponseValueAndErrors(res, key, value,
|
|
20961
|
+
function setResponseValueAndErrors(res, key, value, errorMessage2, refs) {
|
|
20962
20962
|
res[key] = value;
|
|
20963
|
-
addErrorMessage(res, key,
|
|
20963
|
+
addErrorMessage(res, key, errorMessage2, refs);
|
|
20964
20964
|
}
|
|
20965
20965
|
var init_errorMessages = __esm({
|
|
20966
20966
|
"node_modules/zod-to-json-schema/dist/esm/errorMessages.js"() {
|
|
@@ -22582,8 +22582,8 @@ var init_protocol = __esm({
|
|
|
22582
22582
|
if (queuedMessage.type === "response") {
|
|
22583
22583
|
resolver(message);
|
|
22584
22584
|
} else {
|
|
22585
|
-
const
|
|
22586
|
-
const error51 = new McpError(
|
|
22585
|
+
const errorMessage2 = message;
|
|
22586
|
+
const error51 = new McpError(errorMessage2.error.code, errorMessage2.error.message, errorMessage2.error.data);
|
|
22587
22587
|
resolver(error51);
|
|
22588
22588
|
}
|
|
22589
22589
|
} else {
|
|
@@ -30763,23 +30763,23 @@ var init_server2 = __esm({
|
|
|
30763
30763
|
const wrappedHandler = async (request, extra) => {
|
|
30764
30764
|
const validatedRequest = safeParse2(CallToolRequestSchema, request);
|
|
30765
30765
|
if (!validatedRequest.success) {
|
|
30766
|
-
const
|
|
30767
|
-
throw new McpError(ErrorCode.InvalidParams, `Invalid tools/call request: ${
|
|
30766
|
+
const errorMessage2 = validatedRequest.error instanceof Error ? validatedRequest.error.message : String(validatedRequest.error);
|
|
30767
|
+
throw new McpError(ErrorCode.InvalidParams, `Invalid tools/call request: ${errorMessage2}`);
|
|
30768
30768
|
}
|
|
30769
30769
|
const { params } = validatedRequest.data;
|
|
30770
30770
|
const result = await Promise.resolve(handler(request, extra));
|
|
30771
30771
|
if (params.task) {
|
|
30772
30772
|
const taskValidationResult = safeParse2(CreateTaskResultSchema, result);
|
|
30773
30773
|
if (!taskValidationResult.success) {
|
|
30774
|
-
const
|
|
30775
|
-
throw new McpError(ErrorCode.InvalidParams, `Invalid task creation result: ${
|
|
30774
|
+
const errorMessage2 = taskValidationResult.error instanceof Error ? taskValidationResult.error.message : String(taskValidationResult.error);
|
|
30775
|
+
throw new McpError(ErrorCode.InvalidParams, `Invalid task creation result: ${errorMessage2}`);
|
|
30776
30776
|
}
|
|
30777
30777
|
return taskValidationResult.data;
|
|
30778
30778
|
}
|
|
30779
30779
|
const validationResult = safeParse2(CallToolResultSchema, result);
|
|
30780
30780
|
if (!validationResult.success) {
|
|
30781
|
-
const
|
|
30782
|
-
throw new McpError(ErrorCode.InvalidParams, `Invalid tools/call result: ${
|
|
30781
|
+
const errorMessage2 = validationResult.error instanceof Error ? validationResult.error.message : String(validationResult.error);
|
|
30782
|
+
throw new McpError(ErrorCode.InvalidParams, `Invalid tools/call result: ${errorMessage2}`);
|
|
30783
30783
|
}
|
|
30784
30784
|
return validationResult.data;
|
|
30785
30785
|
};
|
|
@@ -31380,12 +31380,12 @@ var init_mcp = __esm({
|
|
|
31380
31380
|
* @param errorMessage - The error message.
|
|
31381
31381
|
* @returns The tool error result.
|
|
31382
31382
|
*/
|
|
31383
|
-
createToolError(
|
|
31383
|
+
createToolError(errorMessage2) {
|
|
31384
31384
|
return {
|
|
31385
31385
|
content: [
|
|
31386
31386
|
{
|
|
31387
31387
|
type: "text",
|
|
31388
|
-
text:
|
|
31388
|
+
text: errorMessage2
|
|
31389
31389
|
}
|
|
31390
31390
|
],
|
|
31391
31391
|
isError: true
|
|
@@ -31403,8 +31403,8 @@ var init_mcp = __esm({
|
|
|
31403
31403
|
const parseResult = await safeParseAsync2(schemaToParse, args);
|
|
31404
31404
|
if (!parseResult.success) {
|
|
31405
31405
|
const error51 = "error" in parseResult ? parseResult.error : "Unknown error";
|
|
31406
|
-
const
|
|
31407
|
-
throw new McpError(ErrorCode.InvalidParams, `Input validation error: Invalid arguments for tool ${toolName}: ${
|
|
31406
|
+
const errorMessage2 = getParseErrorMessage(error51);
|
|
31407
|
+
throw new McpError(ErrorCode.InvalidParams, `Input validation error: Invalid arguments for tool ${toolName}: ${errorMessage2}`);
|
|
31408
31408
|
}
|
|
31409
31409
|
return parseResult.data;
|
|
31410
31410
|
}
|
|
@@ -31428,8 +31428,8 @@ var init_mcp = __esm({
|
|
|
31428
31428
|
const parseResult = await safeParseAsync2(outputObj, result.structuredContent);
|
|
31429
31429
|
if (!parseResult.success) {
|
|
31430
31430
|
const error51 = "error" in parseResult ? parseResult.error : "Unknown error";
|
|
31431
|
-
const
|
|
31432
|
-
throw new McpError(ErrorCode.InvalidParams, `Output validation error: Invalid structured content for tool ${toolName}: ${
|
|
31431
|
+
const errorMessage2 = getParseErrorMessage(error51);
|
|
31432
|
+
throw new McpError(ErrorCode.InvalidParams, `Output validation error: Invalid structured content for tool ${toolName}: ${errorMessage2}`);
|
|
31433
31433
|
}
|
|
31434
31434
|
}
|
|
31435
31435
|
/**
|
|
@@ -31641,8 +31641,8 @@ var init_mcp = __esm({
|
|
|
31641
31641
|
const parseResult = await safeParseAsync2(argsObj, request.params.arguments);
|
|
31642
31642
|
if (!parseResult.success) {
|
|
31643
31643
|
const error51 = "error" in parseResult ? parseResult.error : "Unknown error";
|
|
31644
|
-
const
|
|
31645
|
-
throw new McpError(ErrorCode.InvalidParams, `Invalid arguments for prompt ${request.params.name}: ${
|
|
31644
|
+
const errorMessage2 = getParseErrorMessage(error51);
|
|
31645
|
+
throw new McpError(ErrorCode.InvalidParams, `Invalid arguments for prompt ${request.params.name}: ${errorMessage2}`);
|
|
31646
31646
|
}
|
|
31647
31647
|
const args = parseResult.data;
|
|
31648
31648
|
const cb = prompt.callback;
|
|
@@ -32838,7 +32838,7 @@ async function loadStdioDotenv() {
|
|
|
32838
32838
|
}
|
|
32839
32839
|
|
|
32840
32840
|
// src/version.ts
|
|
32841
|
-
var VERSION = "1.
|
|
32841
|
+
var VERSION = "1.8.0";
|
|
32842
32842
|
|
|
32843
32843
|
// src/errors.ts
|
|
32844
32844
|
init_dist();
|
|
@@ -33078,15 +33078,15 @@ function fileNameFromUrl(url2, fallback) {
|
|
|
33078
33078
|
function describeUrl(current, requested) {
|
|
33079
33079
|
return current === requested ? current : `${current} (redirected from ${requested})`;
|
|
33080
33080
|
}
|
|
33081
|
-
async function readCapped(res, maxBytes, url2, requested) {
|
|
33081
|
+
async function readCapped(res, maxBytes, url2, requested, subject) {
|
|
33082
33082
|
const declared = Number(res.headers.get("content-length"));
|
|
33083
33083
|
if (Number.isFinite(declared) && declared > maxBytes) {
|
|
33084
|
-
throw tooBig(url2, requested, declared, maxBytes);
|
|
33084
|
+
throw tooBig(url2, requested, declared, maxBytes, false, subject);
|
|
33085
33085
|
}
|
|
33086
33086
|
const reader = res.body?.getReader();
|
|
33087
33087
|
if (!reader) {
|
|
33088
33088
|
const buf = new Uint8Array(await res.arrayBuffer());
|
|
33089
|
-
if (buf.byteLength > maxBytes) throw tooBig(url2, requested, buf.byteLength, maxBytes);
|
|
33089
|
+
if (buf.byteLength > maxBytes) throw tooBig(url2, requested, buf.byteLength, maxBytes, false, subject);
|
|
33090
33090
|
return buf;
|
|
33091
33091
|
}
|
|
33092
33092
|
const chunks = [];
|
|
@@ -33100,7 +33100,7 @@ async function readCapped(res, maxBytes, url2, requested) {
|
|
|
33100
33100
|
if (total > maxBytes) {
|
|
33101
33101
|
await reader.cancel().catch(() => {
|
|
33102
33102
|
});
|
|
33103
|
-
throw tooBig(url2, requested, total, maxBytes, true);
|
|
33103
|
+
throw tooBig(url2, requested, total, maxBytes, true, subject);
|
|
33104
33104
|
}
|
|
33105
33105
|
chunks.push(value);
|
|
33106
33106
|
}
|
|
@@ -33127,12 +33127,18 @@ function fetchFailure(err, current, requested, timeoutMs) {
|
|
|
33127
33127
|
{ hint: "Check the URL is reachable from the server and serves the image without authentication." }
|
|
33128
33128
|
);
|
|
33129
33129
|
}
|
|
33130
|
-
function tooBig(url2, requested, size, max, partial2 = false) {
|
|
33130
|
+
function tooBig(url2, requested, size, max, partial2 = false, subject) {
|
|
33131
|
+
const what = subject ?? IMAGE_CAP_SUBJECT;
|
|
33131
33132
|
return new McpToolError(
|
|
33132
|
-
|
|
33133
|
-
{ hint:
|
|
33133
|
+
`${what.noun} at ${describeUrl(url2, requested)} is ${partial2 ? "over " : ""}${formatMb(size)}, above the ${formatMb(max)} limit${what.limitFor ? ` for ${what.limitFor}` : ""}.`,
|
|
33134
|
+
{ hint: what.hint }
|
|
33134
33135
|
);
|
|
33135
33136
|
}
|
|
33137
|
+
var IMAGE_CAP_SUBJECT = {
|
|
33138
|
+
noun: "Image",
|
|
33139
|
+
limitFor: "images_url",
|
|
33140
|
+
hint: "Resize or re-encode the image, or upload it yourself with gemini_upload_file and pass the returned file_uri."
|
|
33141
|
+
};
|
|
33136
33142
|
async function fetchRemoteImage(requestedUrl, opts = {}) {
|
|
33137
33143
|
const doFetch = opts.fetchImpl ?? fetch;
|
|
33138
33144
|
const maxBytes = opts.maxBytes ?? IMAGE_URL_MAX_BYTES;
|
|
@@ -34120,6 +34126,7 @@ var JobRegistry = class {
|
|
|
34120
34126
|
status: entry.status,
|
|
34121
34127
|
createdAt: entry.createdAt,
|
|
34122
34128
|
updatedAt,
|
|
34129
|
+
...entry.interactionId !== void 0 ? { interactionId: entry.interactionId } : {},
|
|
34123
34130
|
...entry.error ? { error: entry.error } : {},
|
|
34124
34131
|
...entry.status === "done" && entry.result ? persistableResult(entry.result) : {}
|
|
34125
34132
|
};
|
|
@@ -34205,8 +34212,30 @@ var JobRegistry = class {
|
|
|
34205
34212
|
}
|
|
34206
34213
|
const jobId = randomUUID();
|
|
34207
34214
|
this.evict(now);
|
|
34208
|
-
|
|
34209
|
-
|
|
34215
|
+
let entry;
|
|
34216
|
+
let earlyInteractionId;
|
|
34217
|
+
const ctx = {
|
|
34218
|
+
reportInteraction: (interactionId) => {
|
|
34219
|
+
if (!entry) {
|
|
34220
|
+
earlyInteractionId = interactionId;
|
|
34221
|
+
return;
|
|
34222
|
+
}
|
|
34223
|
+
if (entry.interactionId === interactionId) return;
|
|
34224
|
+
entry.interactionId = interactionId;
|
|
34225
|
+
if (this.store) this.track(this.writeRecord(entry));
|
|
34226
|
+
}
|
|
34227
|
+
};
|
|
34228
|
+
const promise2 = work(ctx);
|
|
34229
|
+
entry = {
|
|
34230
|
+
jobId,
|
|
34231
|
+
toolName,
|
|
34232
|
+
fingerprint,
|
|
34233
|
+
idempotencyKey,
|
|
34234
|
+
status: "running",
|
|
34235
|
+
promise: promise2,
|
|
34236
|
+
createdAt: now,
|
|
34237
|
+
...earlyInteractionId !== void 0 ? { interactionId: earlyInteractionId } : {}
|
|
34238
|
+
};
|
|
34210
34239
|
this.jobs.set(jobId, entry);
|
|
34211
34240
|
if (idempotencyKey !== void 0) this.byKey.set(idempotencyKey, jobId);
|
|
34212
34241
|
this.runningByFingerprint.set(fingerprint, jobId);
|
|
@@ -34266,6 +34295,45 @@ var JobRegistry = class {
|
|
|
34266
34295
|
* the unknown-id path — the poll tool cannot be used to read another user's
|
|
34267
34296
|
* result even if they know the uuid.
|
|
34268
34297
|
*/
|
|
34298
|
+
/**
|
|
34299
|
+
* The durable record for `jobId`, if there is one — including the
|
|
34300
|
+
* executor-lost rewrite. Read-only view for a caller that wants to act on a
|
|
34301
|
+
* record's contents (see `tools/jobs.ts` recovering an interaction id) rather
|
|
34302
|
+
* than just render it.
|
|
34303
|
+
*/
|
|
34304
|
+
async peekRecord(jobId) {
|
|
34305
|
+
return this.store ? await this.store.get(jobId) : void 0;
|
|
34306
|
+
}
|
|
34307
|
+
/**
|
|
34308
|
+
* Adopt a result produced OUTSIDE this registry — recovery reading a lost
|
|
34309
|
+
* job's output back off the upstream interaction (`tools/jobs.ts`).
|
|
34310
|
+
*
|
|
34311
|
+
* Without this the job stays `failed` and every later poll re-enters
|
|
34312
|
+
* recovery: another interaction fetch, another media download, and another
|
|
34313
|
+
* copy on disk (`…-2.mp4`, `…-3.mp4`) or in R2, with nothing telling the
|
|
34314
|
+
* caller they are duplicates. The ordinary done path is idempotent, so this
|
|
34315
|
+
* one has to be too.
|
|
34316
|
+
*/
|
|
34317
|
+
async adoptResult(jobId, toolName, result, interactionId) {
|
|
34318
|
+
const now = this.now();
|
|
34319
|
+
let entry = this.jobs.get(jobId);
|
|
34320
|
+
if (!entry) {
|
|
34321
|
+
entry = {
|
|
34322
|
+
jobId,
|
|
34323
|
+
toolName,
|
|
34324
|
+
fingerprint: `recovered:${jobId}`,
|
|
34325
|
+
status: "done",
|
|
34326
|
+
promise: Promise.resolve(result),
|
|
34327
|
+
createdAt: now
|
|
34328
|
+
};
|
|
34329
|
+
this.jobs.set(jobId, entry);
|
|
34330
|
+
}
|
|
34331
|
+
entry.status = "done";
|
|
34332
|
+
entry.result = result;
|
|
34333
|
+
entry.settledAt = now;
|
|
34334
|
+
if (interactionId !== void 0) entry.interactionId = interactionId;
|
|
34335
|
+
if (this.store) await this.writeRecord(entry);
|
|
34336
|
+
}
|
|
34269
34337
|
async getResult(jobId) {
|
|
34270
34338
|
const entry = this.jobs.get(jobId);
|
|
34271
34339
|
if (!entry) {
|
|
@@ -35010,6 +35078,21 @@ var UPLOAD_BASE_URL = "https://generativelanguage.googleapis.com/upload/v1beta";
|
|
|
35010
35078
|
var FILE_MAX_BYTES = 2 * 1024 ** 3;
|
|
35011
35079
|
var FILE_POLL_INTERVAL_MS = 2e3;
|
|
35012
35080
|
var FILE_POLL_MAX_ATTEMPTS = 150;
|
|
35081
|
+
var INTERACTION_POLL_INTERVAL_MS = 3e3;
|
|
35082
|
+
var TERMINAL_INTERACTION_STATUSES = /* @__PURE__ */ new Set([
|
|
35083
|
+
"completed",
|
|
35084
|
+
"failed",
|
|
35085
|
+
"cancelled",
|
|
35086
|
+
"incomplete",
|
|
35087
|
+
"budget_exceeded"
|
|
35088
|
+
]);
|
|
35089
|
+
var MEDIA_URI_HOST = "generativelanguage.googleapis.com";
|
|
35090
|
+
var MEDIA_DOWNLOAD_MAX_BYTES = 128 * 1024 * 1024;
|
|
35091
|
+
var MEDIA_CAP_SUBJECT = {
|
|
35092
|
+
noun: "Generated media",
|
|
35093
|
+
hint: "Ask for a shorter clip or a lower resolution \u2014 the file itself is still in the Files API for ~48h if you need it another way."
|
|
35094
|
+
};
|
|
35095
|
+
var INTERACTION_ID_RE = /^[\w-]+$/;
|
|
35013
35096
|
var CHAIN_404_RETRY_BUDGET_MS = 12e4;
|
|
35014
35097
|
var CHAIN_404_RETRY_BASE_MS = 2e3;
|
|
35015
35098
|
var CHAIN_404_RETRY_MAX_SLEEP_MS = 3e4;
|
|
@@ -35018,18 +35101,20 @@ function resolveChainRetryBudgetMs() {
|
|
|
35018
35101
|
return Number.isFinite(env) && env >= 0 ? env : CHAIN_404_RETRY_BUDGET_MS;
|
|
35019
35102
|
}
|
|
35020
35103
|
var ChainedRequest404Error = class extends McpToolError {
|
|
35021
|
-
constructor(previousInteractionId, upstreamMessage, retries, opts) {
|
|
35104
|
+
constructor(previousInteractionId, upstreamMessage, retries, opts, chainExists, verdict) {
|
|
35022
35105
|
super(
|
|
35023
|
-
`Gemini returned HTTP 404 for a chained request (previous_interaction_id: "${previousInteractionId}"), retried ${retries.attempts}\xD7 over ~${Math.round(retries.waitedMs / 1e3)}s for store lag. Upstream said: ${upstreamMessage}. This does not necessarily mean the interaction expired \u2014 an unknown model id or an expired files/\u2026 uri returns the same generic 404. (For reference: interactions are retained 55 days on the paid tier, 1 day on the free tier, and are scoped to the API key that created them.)
|
|
35106
|
+
`Gemini returned HTTP 404 for a chained request (previous_interaction_id: "${previousInteractionId}"), retried ${retries.attempts}\xD7 over ~${Math.round(retries.waitedMs / 1e3)}s for store lag. Upstream said: ${upstreamMessage}. ` + (verdict ?? "This does not necessarily mean the interaction expired \u2014 an unknown model id or an expired files/\u2026 uri returns the same generic 404. ") + "(For reference: interactions are retained 55 days on the paid tier, 1 day on the free tier, and are scoped to the API key that created them.)",
|
|
35024
35107
|
opts
|
|
35025
35108
|
);
|
|
35026
35109
|
this.previousInteractionId = previousInteractionId;
|
|
35027
35110
|
this.upstreamMessage = upstreamMessage;
|
|
35028
35111
|
this.retries = retries;
|
|
35112
|
+
this.chainExists = chainExists;
|
|
35029
35113
|
}
|
|
35030
35114
|
previousInteractionId;
|
|
35031
35115
|
upstreamMessage;
|
|
35032
35116
|
retries;
|
|
35117
|
+
chainExists;
|
|
35033
35118
|
};
|
|
35034
35119
|
function generatePart(img) {
|
|
35035
35120
|
return img.uri ? { file_data: { file_uri: img.uri, mime_type: img.mimeType } } : { inline_data: { mime_type: img.mimeType, data: img.base64 } };
|
|
@@ -35469,31 +35554,52 @@ var GeminiClient = class {
|
|
|
35469
35554
|
hint: "The request may have been blocked by safety filters \u2014 try rephrasing the prompt."
|
|
35470
35555
|
});
|
|
35471
35556
|
}
|
|
35472
|
-
return { id: data.id, images, text, grounding };
|
|
35557
|
+
return { id: data.id, images: await this.downloadAll(images), text, grounding };
|
|
35473
35558
|
}
|
|
35474
35559
|
/**
|
|
35475
|
-
* Generate video via the omni model (Interactions API
|
|
35476
|
-
*
|
|
35477
|
-
*
|
|
35478
|
-
*
|
|
35560
|
+
* Generate video via the omni model (Interactions API). Same plumbing as
|
|
35561
|
+
* {@link interact} — text/image input parts, a `video` response_format,
|
|
35562
|
+
* optional `video_config.task`, and `previous_interaction_id` for edits.
|
|
35563
|
+
*
|
|
35564
|
+
* Delivered by uri unless the caller says otherwise, so the clip is not
|
|
35565
|
+
* capped by the ~4MB inline limit; the bytes are downloaded here either way.
|
|
35566
|
+
* Verified live 2026-08-22 end to end (a real MP4 off the wire), which is
|
|
35567
|
+
* why this path no longer carries the "docs-derived" caveat that music
|
|
35568
|
+
* still does.
|
|
35479
35569
|
*/
|
|
35480
35570
|
async generateVideo(opts) {
|
|
35481
35571
|
const model = opts.model ?? DEFAULT_VIDEO_MODEL;
|
|
35482
35572
|
const inputParts = [{ type: "text", text: opts.input }];
|
|
35483
35573
|
for (const img of opts.images ?? []) inputParts.push(interactPart(img));
|
|
35484
|
-
const
|
|
35485
|
-
|
|
35486
|
-
|
|
35487
|
-
|
|
35488
|
-
|
|
35489
|
-
|
|
35574
|
+
const delivery = opts.delivery ?? "uri";
|
|
35575
|
+
const buildBody = (withDelivery) => {
|
|
35576
|
+
const responseFormat = { type: "video" };
|
|
35577
|
+
if (withDelivery) responseFormat.delivery = delivery;
|
|
35578
|
+
if (opts.aspectRatio) responseFormat.aspect_ratio = opts.aspectRatio;
|
|
35579
|
+
const body2 = { model, input: inputParts, response_format: responseFormat };
|
|
35580
|
+
if (opts.task) body2.generation_config = { video_config: { task: opts.task } };
|
|
35581
|
+
if (opts.previousInteractionId !== void 0) body2.previous_interaction_id = opts.previousInteractionId;
|
|
35582
|
+
if (opts.background === true) body2.background = true;
|
|
35583
|
+
return body2;
|
|
35584
|
+
};
|
|
35585
|
+
const timeoutMs = resolveTimeoutMs(opts.timeoutMs, void 0);
|
|
35586
|
+
let body = buildBody(true);
|
|
35587
|
+
let started;
|
|
35588
|
+
try {
|
|
35589
|
+
started = await this.startInteraction(body, opts, timeoutMs);
|
|
35590
|
+
} catch (err) {
|
|
35591
|
+
if (!isDeliveryUnsupported(err)) throw err;
|
|
35592
|
+
body = buildBody(false);
|
|
35593
|
+
started = await this.startInteraction(body, opts, timeoutMs);
|
|
35594
|
+
}
|
|
35595
|
+
const data = await this.settleInteraction(started, body, timeoutMs);
|
|
35490
35596
|
const { videos, text } = this.extractInteraction(data);
|
|
35491
35597
|
if (videos.length === 0) {
|
|
35492
35598
|
throw new McpToolError("Gemini returned no video", {
|
|
35493
|
-
hint: "The request may have been blocked by a safety filter
|
|
35599
|
+
hint: "The request may have been blocked by a safety filter \u2014 try a shorter/simpler prompt or a different aspect ratio."
|
|
35494
35600
|
});
|
|
35495
35601
|
}
|
|
35496
|
-
return { id: data.id, videos, text };
|
|
35602
|
+
return { id: data.id, videos: await this.downloadAll(videos), text };
|
|
35497
35603
|
}
|
|
35498
35604
|
/**
|
|
35499
35605
|
* Generate music via a Lyria model (Interactions API, inline audio). Reuses
|
|
@@ -35507,6 +35613,7 @@ var GeminiClient = class {
|
|
|
35507
35613
|
if (opts.audioFormat) responseFormat.audio_format = opts.audioFormat;
|
|
35508
35614
|
const body = { model, input: inputParts, response_format: responseFormat };
|
|
35509
35615
|
if (opts.previousInteractionId !== void 0) body.previous_interaction_id = opts.previousInteractionId;
|
|
35616
|
+
if (opts.background === true) body.background = true;
|
|
35510
35617
|
const data = await this.postInteraction(body, opts);
|
|
35511
35618
|
const { audios, text } = this.extractInteraction(data);
|
|
35512
35619
|
if (audios.length === 0) {
|
|
@@ -35514,11 +35621,37 @@ var GeminiClient = class {
|
|
|
35514
35621
|
hint: "The request may have been blocked by a safety filter \u2014 try rephrasing the prompt."
|
|
35515
35622
|
});
|
|
35516
35623
|
}
|
|
35517
|
-
return { id: data.id, audios, text };
|
|
35624
|
+
return { id: data.id, audios: await this.downloadAll(audios), text };
|
|
35518
35625
|
}
|
|
35519
|
-
/**
|
|
35626
|
+
/**
|
|
35627
|
+
* POST `/interactions`, wait out the store lag on a chained 404, and — when
|
|
35628
|
+
* the request was backgrounded — poll until the interaction reaches a
|
|
35629
|
+
* terminal state. Shared by all media.
|
|
35630
|
+
*/
|
|
35520
35631
|
async postInteraction(body, opts) {
|
|
35521
|
-
const
|
|
35632
|
+
const timeoutMs = resolveTimeoutMs(opts.timeoutMs, opts.imageSize);
|
|
35633
|
+
return await this.settleInteraction(await this.startInteraction(body, opts, timeoutMs), body, timeoutMs);
|
|
35634
|
+
}
|
|
35635
|
+
/**
|
|
35636
|
+
* The POST half: issue the request and announce the id.
|
|
35637
|
+
*
|
|
35638
|
+
* Split from the wait so a caller can retry the POST — and ONLY the POST —
|
|
35639
|
+
* without the retry predicate ever seeing an error from after the generation
|
|
35640
|
+
* started. Past this point the work is running and billable.
|
|
35641
|
+
*/
|
|
35642
|
+
async startInteraction(body, opts, timeoutMs) {
|
|
35643
|
+
const started = await this.postInteractionOnce(body, opts, timeoutMs);
|
|
35644
|
+
if (started.id) opts.onInteractionStarted?.(started.id);
|
|
35645
|
+
return started;
|
|
35646
|
+
}
|
|
35647
|
+
/** The wait half: poll a backgrounded request to a terminal state. */
|
|
35648
|
+
async settleInteraction(started, body, timeoutMs) {
|
|
35649
|
+
if (body.background !== true) return started;
|
|
35650
|
+
return await this.awaitInteraction(started, timeoutMs);
|
|
35651
|
+
}
|
|
35652
|
+
/** The POST itself, with the chained-404 retry + diagnosis. */
|
|
35653
|
+
async postInteractionOnce(body, opts, timeoutMs) {
|
|
35654
|
+
const { interactionsApi } = this.apisFor(timeoutMs);
|
|
35522
35655
|
const budgetMs = resolveChainRetryBudgetMs();
|
|
35523
35656
|
let waitedMs = 0;
|
|
35524
35657
|
for (let attempt = 0; ; attempt++) {
|
|
@@ -35532,13 +35665,183 @@ var GeminiClient = class {
|
|
|
35532
35665
|
await this.sleep(delay);
|
|
35533
35666
|
continue;
|
|
35534
35667
|
}
|
|
35535
|
-
|
|
35536
|
-
|
|
35537
|
-
|
|
35538
|
-
|
|
35668
|
+
const { chainExists, verdict } = await this.diagnoseChain(opts.previousInteractionId);
|
|
35669
|
+
throw new ChainedRequest404Error(
|
|
35670
|
+
opts.previousInteractionId,
|
|
35671
|
+
err.message,
|
|
35672
|
+
{ attempts: attempt, waitedMs },
|
|
35673
|
+
{
|
|
35674
|
+
hint: chainExists === true ? "The chain is alive, so re-issuing without previous_interaction_id would re-bill and still fail. Check the model id and any files/\u2026 uri instead." : "Re-issue the request WITHOUT previous_interaction_id (re-attaching the prior output image if you were editing). If that also 404s, the interaction id is not the cause \u2014 check the model id and any files/\u2026 uri (they expire ~48h). gemini_interact does this probe automatically.",
|
|
35675
|
+
cause: err
|
|
35676
|
+
},
|
|
35677
|
+
chainExists,
|
|
35678
|
+
verdict
|
|
35679
|
+
);
|
|
35680
|
+
}
|
|
35681
|
+
}
|
|
35682
|
+
}
|
|
35683
|
+
/**
|
|
35684
|
+
* Ask the API whether the chained interaction is actually gone.
|
|
35685
|
+
*
|
|
35686
|
+
* Three answers, three remediations — and a fourth state that matters just as
|
|
35687
|
+
* much: if the probe itself cannot be completed we return `undefined`, so the
|
|
35688
|
+
* caller says "unknown" rather than inventing "gone".
|
|
35689
|
+
*/
|
|
35690
|
+
async diagnoseChain(id) {
|
|
35691
|
+
try {
|
|
35692
|
+
await this.getInteraction(id);
|
|
35693
|
+
return {
|
|
35694
|
+
chainExists: true,
|
|
35695
|
+
verdict: `That interaction still exists (GET /interactions/${id} returned it), so the chain is NOT the cause \u2014 a generic 404 also covers an unknown model id and an expired files/\u2026 uri (~48h). Check those. `
|
|
35696
|
+
};
|
|
35697
|
+
} catch (err) {
|
|
35698
|
+
if (err instanceof ApiError && err.status === 404) {
|
|
35699
|
+
return {
|
|
35700
|
+
chainExists: false,
|
|
35701
|
+
verdict: `That interaction no longer exists (GET /interactions/${id} returned 404 too) \u2014 it expired or was deleted, so re-anchor on the image it produced. That does not necessarily mean it was the cause, though: an unknown model id or an expired files/\u2026 uri returns the same generic 404, so read the upstream text above. `
|
|
35702
|
+
};
|
|
35539
35703
|
}
|
|
35704
|
+
if (err instanceof ApiError && err.status === 400) {
|
|
35705
|
+
return {
|
|
35706
|
+
chainExists: false,
|
|
35707
|
+
verdict: `"${id}" is not a valid interaction id \u2014 the API rejected the name as malformed, so it was never a live chain. Check how the id was captured. `
|
|
35708
|
+
};
|
|
35709
|
+
}
|
|
35710
|
+
return {};
|
|
35540
35711
|
}
|
|
35541
35712
|
}
|
|
35713
|
+
/**
|
|
35714
|
+
* Poll a backgrounded interaction to a terminal state.
|
|
35715
|
+
*
|
|
35716
|
+
* ⚠️ **A poll of work that is still in flight answers `403
|
|
35717
|
+
* permission_denied`** — "There was a problem processing your request. You
|
|
35718
|
+
* will not be charged." (verified live 2026-08-22, against a paid key that
|
|
35719
|
+
* GETs the very same interaction fine once it completes). It is a transient
|
|
35720
|
+
* state dressed as an authorization failure, and taking it at face value
|
|
35721
|
+
* fails every backgrounded generation a few seconds in. A 404 is transient
|
|
35722
|
+
* here too — the same store lag the chained POST retries.
|
|
35723
|
+
*
|
|
35724
|
+
* Neither is distinguishable from the real thing, so the caller's timeout is
|
|
35725
|
+
* what bounds it, and the last poll error rides along in the timeout message
|
|
35726
|
+
* so a genuine permission problem is not invisible.
|
|
35727
|
+
*
|
|
35728
|
+
* A body with no `status` field is likewise treated as "still running"
|
|
35729
|
+
* unless a `model_output` step is already present.
|
|
35730
|
+
*/
|
|
35731
|
+
async awaitInteraction(started, budgetMs) {
|
|
35732
|
+
let current = started;
|
|
35733
|
+
let waitedMs = 0;
|
|
35734
|
+
let lastPollError;
|
|
35735
|
+
for (; ; ) {
|
|
35736
|
+
const status = current.status;
|
|
35737
|
+
if (!status && (current.steps ?? []).some((step2) => step2.type === "model_output")) return current;
|
|
35738
|
+
if (status && TERMINAL_INTERACTION_STATUSES.has(status)) {
|
|
35739
|
+
if (status === "completed") return current;
|
|
35740
|
+
throw new McpToolError(
|
|
35741
|
+
`${SERVICE} interaction ${current.id} ${status} (background execution).`,
|
|
35742
|
+
{ hint: "A safety filter or an unsupported prompt is the usual cause \u2014 rephrase and retry. The interaction id above can be inspected while it is retained." }
|
|
35743
|
+
);
|
|
35744
|
+
}
|
|
35745
|
+
if (waitedMs + INTERACTION_POLL_INTERVAL_MS > budgetMs) {
|
|
35746
|
+
throw new McpToolError(
|
|
35747
|
+
`${SERVICE} interaction ${current.id} is still running after ${Math.round(waitedMs / 1e3)}s. It continues on Google's side \u2014 the work is not lost.` + (lastPollError ? ` Last poll said: ${lastPollError}` : ""),
|
|
35748
|
+
{ hint: "Raise timeout_ms (or max_wait_ms) and retry, or keep the interaction id: the generation completes upstream regardless of this request." }
|
|
35749
|
+
);
|
|
35750
|
+
}
|
|
35751
|
+
await this.sleep(INTERACTION_POLL_INTERVAL_MS);
|
|
35752
|
+
waitedMs += INTERACTION_POLL_INTERVAL_MS;
|
|
35753
|
+
try {
|
|
35754
|
+
current = await this.getInteraction(current.id);
|
|
35755
|
+
lastPollError = void 0;
|
|
35756
|
+
} catch (err) {
|
|
35757
|
+
if (!isTransientPoll(err)) throw err;
|
|
35758
|
+
lastPollError = err instanceof Error ? err.message : String(err);
|
|
35759
|
+
}
|
|
35760
|
+
}
|
|
35761
|
+
}
|
|
35762
|
+
/**
|
|
35763
|
+
* Fetch one interaction. Backs the background poll and the chained-404
|
|
35764
|
+
* diagnosis; also the honest answer to "is that id still alive?".
|
|
35765
|
+
*/
|
|
35766
|
+
async getInteraction(id) {
|
|
35767
|
+
if (!INTERACTION_ID_RE.test(id)) {
|
|
35768
|
+
throw new McpToolError(`Not a valid interaction id: "${id}"`, {
|
|
35769
|
+
hint: "An interaction id is the opaque `id` from a previous response (e.g. v1_Chd\u2026), with no slashes or query string."
|
|
35770
|
+
});
|
|
35771
|
+
}
|
|
35772
|
+
const { interactionsApi } = this.apisFor(resolveTimeoutMs(void 0));
|
|
35773
|
+
return await interactionsApi.fetchJson("GET", `/interactions/${id}`);
|
|
35774
|
+
}
|
|
35775
|
+
/**
|
|
35776
|
+
* Fetch a stored interaction and resolve its media to bytes.
|
|
35777
|
+
*
|
|
35778
|
+
* The recovery read: a generation whose executor died is still retrievable
|
|
35779
|
+
* by id for as long as the interaction is retained (55 days paid, 1 day
|
|
35780
|
+
* free). Returns the status alongside the media so a caller can tell "still
|
|
35781
|
+
* running" from "finished with nothing".
|
|
35782
|
+
*/
|
|
35783
|
+
async fetchInteractionMedia(id) {
|
|
35784
|
+
const data = await this.getInteraction(id);
|
|
35785
|
+
const { images, videos, audios, text } = this.extractInteraction(data);
|
|
35786
|
+
return {
|
|
35787
|
+
status: data.status,
|
|
35788
|
+
images: await this.downloadAll(images),
|
|
35789
|
+
videos: await this.downloadAll(videos),
|
|
35790
|
+
audios: await this.downloadAll(audios),
|
|
35791
|
+
text
|
|
35792
|
+
};
|
|
35793
|
+
}
|
|
35794
|
+
/**
|
|
35795
|
+
* Resolve one extracted medium to bytes.
|
|
35796
|
+
*
|
|
35797
|
+
* `delivery: 'uri'` hands back a Files API link, and that link **needs the
|
|
35798
|
+
* api key** (verified: 403 without it), so it is not something a caller could
|
|
35799
|
+
* use — the bytes have to arrive here exactly as inline delivery would. The
|
|
35800
|
+
* uri is API-supplied but addresses a request we make with credentials
|
|
35801
|
+
* attached, so the host is checked before it is fetched.
|
|
35802
|
+
*/
|
|
35803
|
+
async downloadMedia(media) {
|
|
35804
|
+
if (media.base64) return { base64: media.base64, mimeType: media.mimeType };
|
|
35805
|
+
const requested = media.uri ?? "";
|
|
35806
|
+
const doFetch = this.fetchImpl;
|
|
35807
|
+
const key = this.requireKey();
|
|
35808
|
+
let current = requested;
|
|
35809
|
+
for (let hop = 0; ; hop++) {
|
|
35810
|
+
const url2 = assertMediaHost(current, requested);
|
|
35811
|
+
const res = await doFetch(url2.toString(), {
|
|
35812
|
+
redirect: "manual",
|
|
35813
|
+
headers: { "x-goog-api-key": key }
|
|
35814
|
+
});
|
|
35815
|
+
if (res.status >= 300 && res.status < 400) {
|
|
35816
|
+
const location = res.headers?.get("location");
|
|
35817
|
+
if (!location) {
|
|
35818
|
+
throw new McpToolError(`${SERVICE} media download returned HTTP ${res.status} with no Location header for ${current}`);
|
|
35819
|
+
}
|
|
35820
|
+
if (hop >= MAX_REDIRECTS) {
|
|
35821
|
+
throw new McpToolError(`${SERVICE} media download for ${requested} exceeded ${MAX_REDIRECTS} redirects (last hop: ${location}).`);
|
|
35822
|
+
}
|
|
35823
|
+
current = new URL(location, url2).toString();
|
|
35824
|
+
continue;
|
|
35825
|
+
}
|
|
35826
|
+
if (!res.ok) {
|
|
35827
|
+
throw new McpToolError(
|
|
35828
|
+
`${SERVICE} media download failed (HTTP ${res.status}) for ${current}`,
|
|
35829
|
+
{ hint: "Generated files live in the Files API for ~48h and the download requires the api key that created them." }
|
|
35830
|
+
);
|
|
35831
|
+
}
|
|
35832
|
+
const bytes = await readCapped(res, MEDIA_DOWNLOAD_MAX_BYTES, current, requested, MEDIA_CAP_SUBJECT);
|
|
35833
|
+
return {
|
|
35834
|
+
base64: bytesToBase64(bytes),
|
|
35835
|
+
mimeType: media.mimeType || res.headers?.get("content-type") || "application/octet-stream"
|
|
35836
|
+
};
|
|
35837
|
+
}
|
|
35838
|
+
}
|
|
35839
|
+
/** {@link downloadMedia} over a list, preserving order. */
|
|
35840
|
+
async downloadAll(media) {
|
|
35841
|
+
const out = [];
|
|
35842
|
+
for (const m of media) out.push(await this.downloadMedia(m));
|
|
35843
|
+
return out;
|
|
35844
|
+
}
|
|
35542
35845
|
/**
|
|
35543
35846
|
* Collect `model_output` media (image/video/audio) + text + grounding from an
|
|
35544
35847
|
* interactions response. Only the `model_output` step is surfaced — `thought`
|
|
@@ -35567,12 +35870,13 @@ var GeminiClient = class {
|
|
|
35567
35870
|
for (const parts of [step2.content ?? [], step2.summary ?? []]) {
|
|
35568
35871
|
for (const part of parts) {
|
|
35569
35872
|
const mime = part.mime_type ?? part.mimeType;
|
|
35570
|
-
if (part.
|
|
35571
|
-
if (part.
|
|
35572
|
-
|
|
35573
|
-
|
|
35574
|
-
|
|
35575
|
-
|
|
35873
|
+
if (part.type === "text") {
|
|
35874
|
+
if (part.text?.trim()) textParts.push(part.text);
|
|
35875
|
+
} else if (part.data || part.uri) {
|
|
35876
|
+
const carrier = part.data ? { base64: part.data } : { uri: part.uri };
|
|
35877
|
+
if (part.type === "image") images.push({ ...carrier, mimeType: mime ?? "image/jpeg" });
|
|
35878
|
+
else if (part.type === "video") videos.push({ ...carrier, mimeType: mime ?? "video/mp4" });
|
|
35879
|
+
else if (part.type === "audio") audios.push({ ...carrier, mimeType: mime ?? "audio/mpeg" });
|
|
35576
35880
|
}
|
|
35577
35881
|
}
|
|
35578
35882
|
}
|
|
@@ -35586,6 +35890,27 @@ var GeminiClient = class {
|
|
|
35586
35890
|
return { images, videos, audios, text: textParts.join("\n") || void 0, grounding };
|
|
35587
35891
|
}
|
|
35588
35892
|
};
|
|
35893
|
+
function assertMediaHost(candidate, requested) {
|
|
35894
|
+
let parsed;
|
|
35895
|
+
try {
|
|
35896
|
+
parsed = new URL(candidate);
|
|
35897
|
+
} catch {
|
|
35898
|
+
throw new McpToolError(`${SERVICE} returned a media reference that is not a URL: ${candidate || "(empty)"}`);
|
|
35899
|
+
}
|
|
35900
|
+
if (parsed.protocol !== "https:" || parsed.hostname !== MEDIA_URI_HOST) {
|
|
35901
|
+
throw new McpToolError(
|
|
35902
|
+
`${SERVICE} media download was pointed at an unexpected host (${parsed.hostname || candidate}); expected ${MEDIA_URI_HOST}` + (candidate === requested ? "" : ` (redirected from ${requested})`),
|
|
35903
|
+
{ hint: "Generated media is served from the Files API on that host only. Report this if it reproduces \u2014 it should not happen." }
|
|
35904
|
+
);
|
|
35905
|
+
}
|
|
35906
|
+
return parsed;
|
|
35907
|
+
}
|
|
35908
|
+
function isTransientPoll(err) {
|
|
35909
|
+
return err instanceof ApiError && (err.status === 403 || err.status === 404);
|
|
35910
|
+
}
|
|
35911
|
+
function isDeliveryUnsupported(err) {
|
|
35912
|
+
return err instanceof ApiError && err.status === 400 && /delivery/i.test(err.message);
|
|
35913
|
+
}
|
|
35589
35914
|
function hostedStorage() {
|
|
35590
35915
|
const blob = blobStoreFromEnv();
|
|
35591
35916
|
if (!blob) return {};
|
|
@@ -35786,6 +36111,7 @@ function registerInteractTools(server, client2) {
|
|
|
35786
36111
|
return await client2.interact({ ...callOpts, images: inputs.length ? inputs : void 0, previousInteractionId });
|
|
35787
36112
|
} catch (err) {
|
|
35788
36113
|
if (!(err instanceof ChainedRequest404Error)) throw err;
|
|
36114
|
+
if (err.chainExists === true) throw err;
|
|
35789
36115
|
if (!onDisk) throw err;
|
|
35790
36116
|
const reanchorOn = await findInteractionImages(resolveOutputDir(args.output_dir), err.previousInteractionId);
|
|
35791
36117
|
if (!reanchorOn.length) throw err;
|
|
@@ -35846,6 +36172,7 @@ init_dist();
|
|
|
35846
36172
|
init_images();
|
|
35847
36173
|
var VIDEO_ASPECT_RATIOS = ["16:9", "9:16"];
|
|
35848
36174
|
var VIDEO_TASKS = ["text_to_video", "image_to_video", "reference_to_video", "edit"];
|
|
36175
|
+
var VIDEO_DELIVERY = ["inline", "uri"];
|
|
35849
36176
|
function registerVideoTools(server, client2) {
|
|
35850
36177
|
server.registerTool(
|
|
35851
36178
|
"gemini_video_generate",
|
|
@@ -35864,6 +36191,8 @@ function registerVideoTools(server, client2) {
|
|
|
35864
36191
|
filename: external_exports.string().optional().describe("Base filename for the output video (extension stripped; default: slugified prompt)"),
|
|
35865
36192
|
output_dir: external_exports.string().optional().describe("Directory to write the video to (default: $GEMINI_OUTPUT_DIR or cwd)"),
|
|
35866
36193
|
model: external_exports.string().regex(/^[\w.-]+$/, 'must be a bare model id (letters, digits, ".", "_", "-")').optional().describe(`Model id override (default: ${DEFAULT_VIDEO_MODEL})`),
|
|
36194
|
+
delivery: external_exports.enum(VIDEO_DELIVERY).optional().describe('How the clip comes back: "uri" (default \u2014 a Files API link the server downloads, no size ceiling) or "inline" (base64, capped ~4MB)'),
|
|
36195
|
+
background: external_exports.boolean().optional().describe("Run the generation on Google's side and poll it, so a killed job can be recovered by gemini_get_result. Trade-off: retrieving a backgrounded interaction is unreliable today (some become permanently unreadable), so the default is off"),
|
|
35867
36196
|
previous_interaction_id: external_exports.string().optional().describe('Interaction id to edit/continue (with task: "edit")'),
|
|
35868
36197
|
continue_last: external_exports.boolean().optional().describe("Continue from the most recent video interaction this server created (explicit previous_interaction_id wins)"),
|
|
35869
36198
|
timeout_ms: timeoutMsSchema,
|
|
@@ -35899,7 +36228,7 @@ function registerVideoTools(server, client2) {
|
|
|
35899
36228
|
images_file_uris: args.images_file_uris,
|
|
35900
36229
|
previous_interaction_id: previousInteractionId
|
|
35901
36230
|
});
|
|
35902
|
-
return client2.session.jobs.dispatch({ toolName: "gemini_video_generate", fingerprint, idempotencyKey: args.idempotency_key, async: args.async, waitMs: args.max_wait_ms }, async () => {
|
|
36231
|
+
return client2.session.jobs.dispatch({ toolName: "gemini_video_generate", fingerprint, idempotencyKey: args.idempotency_key, async: args.async, waitMs: args.max_wait_ms }, async (ctx) => {
|
|
35903
36232
|
const { inputs, report } = await resolveImageInputs(args, client2);
|
|
35904
36233
|
const r = await withProgressHeartbeat(extra, `Generating video (${model})`, () => client2.generateVideo({
|
|
35905
36234
|
input: args.prompt,
|
|
@@ -35908,7 +36237,13 @@ function registerVideoTools(server, client2) {
|
|
|
35908
36237
|
aspectRatio: args.aspect_ratio,
|
|
35909
36238
|
task: args.task,
|
|
35910
36239
|
previousInteractionId,
|
|
35911
|
-
timeoutMs: args.timeout_ms
|
|
36240
|
+
timeoutMs: args.timeout_ms,
|
|
36241
|
+
delivery: args.delivery,
|
|
36242
|
+
background: args.background,
|
|
36243
|
+
// Hand the id to the job record the moment it exists: if this
|
|
36244
|
+
// machine stops mid-generation, that id is the only route back to
|
|
36245
|
+
// an output that finishes upstream anyway.
|
|
36246
|
+
onInteractionStarted: ctx.reportInteraction
|
|
35912
36247
|
}));
|
|
35913
36248
|
client2.session.lastVideoInteractionId = r.id;
|
|
35914
36249
|
const meta3 = { model, interaction_id: r.id };
|
|
@@ -35956,6 +36291,7 @@ function registerMusicTools(server, client2) {
|
|
|
35956
36291
|
inline: external_exports.boolean().optional().describe("Return base64 audio inline instead of writing to disk"),
|
|
35957
36292
|
previous_interaction_id: external_exports.string().optional().describe("Interaction id to continue from"),
|
|
35958
36293
|
continue_last: external_exports.boolean().optional().describe("Continue from the most recent music interaction this server created (explicit previous_interaction_id wins)"),
|
|
36294
|
+
background: external_exports.boolean().optional().describe("Run the generation on Google's side and poll it, so a killed job can be recovered by gemini_get_result. Off by default \u2014 see gemini_video_generate"),
|
|
35959
36295
|
timeout_ms: timeoutMsSchema,
|
|
35960
36296
|
idempotency_key: idempotencyKeySchema,
|
|
35961
36297
|
async: asyncSchema,
|
|
@@ -35993,7 +36329,7 @@ function registerMusicTools(server, client2) {
|
|
|
35993
36329
|
images_file_uris: args.images_file_uris,
|
|
35994
36330
|
previous_interaction_id: previousInteractionId
|
|
35995
36331
|
});
|
|
35996
|
-
return client2.session.jobs.dispatch({ toolName: "gemini_music_generate", fingerprint, idempotencyKey: args.idempotency_key, async: args.async, waitMs: args.max_wait_ms }, async () => {
|
|
36332
|
+
return client2.session.jobs.dispatch({ toolName: "gemini_music_generate", fingerprint, idempotencyKey: args.idempotency_key, async: args.async, waitMs: args.max_wait_ms }, async (ctx) => {
|
|
35997
36333
|
const { inputs, report } = await resolveImageInputs(args, client2);
|
|
35998
36334
|
const r = await withProgressHeartbeat(extra, `Generating music (${model})`, () => client2.generateMusic({
|
|
35999
36335
|
input: args.prompt,
|
|
@@ -36001,7 +36337,10 @@ function registerMusicTools(server, client2) {
|
|
|
36001
36337
|
model: args.model,
|
|
36002
36338
|
audioFormat: args.audio_format,
|
|
36003
36339
|
previousInteractionId,
|
|
36004
|
-
timeoutMs: args.timeout_ms
|
|
36340
|
+
timeoutMs: args.timeout_ms,
|
|
36341
|
+
background: args.background,
|
|
36342
|
+
// See gemini_video_generate: the id is what survives the executor.
|
|
36343
|
+
onInteractionStarted: ctx.reportInteraction
|
|
36005
36344
|
}));
|
|
36006
36345
|
client2.session.lastMusicInteractionId = r.id;
|
|
36007
36346
|
const meta3 = { model, interaction_id: r.id };
|
|
@@ -36022,19 +36361,84 @@ function registerMusicTools(server, client2) {
|
|
|
36022
36361
|
|
|
36023
36362
|
// src/tools/jobs.ts
|
|
36024
36363
|
init_zod();
|
|
36364
|
+
init_dist();
|
|
36365
|
+
init_images();
|
|
36025
36366
|
function registerJobTools(server, client2) {
|
|
36026
36367
|
server.registerTool(
|
|
36027
36368
|
"gemini_get_result",
|
|
36028
36369
|
{
|
|
36029
|
-
description: 'Retrieve a generation started with `async: true` or handed off by `max_wait_ms`. Pass the returned `job_id`: while running it reports status "running"; on completion it returns the normal result (image URLs/paths + meta); on failure it raises the recorded error, including the case where the generation was killed before it finished. On the hosted connector job records are stored durably and survive a restart; on a local stdio server they live with the process and expire ~10 min after completion, where the output dir / <image>.json sidecar is the fallback.',
|
|
36030
|
-
|
|
36370
|
+
description: 'Retrieve a generation started with `async: true` or handed off by `max_wait_ms`. Pass the returned `job_id`: while running it reports status "running"; on completion it returns the normal result (image URLs/paths + meta); on failure it raises the recorded error, including the case where the generation was killed before it finished. On the hosted connector job records are stored durably and survive a restart; on a local stdio server they live with the process and expire ~10 min after completion, where the output dir / <image>.json sidecar is the fallback. A killed video/music job started with `background: true` is recovered from its upstream interaction when it finished there.',
|
|
36371
|
+
// NOT read-only any more: recovering a killed job writes the media it
|
|
36372
|
+
// pulls back, exactly as the generation tool would have.
|
|
36373
|
+
annotations: { readOnlyHint: false, openWorldHint: true },
|
|
36031
36374
|
inputSchema: {
|
|
36032
|
-
job_id: external_exports.string().min(1).describe("The job_id returned by a generation tool called with async: true")
|
|
36375
|
+
job_id: external_exports.string().min(1).describe("The job_id returned by a generation tool called with async: true"),
|
|
36376
|
+
output_dir: external_exports.string().optional().describe("Where to write media recovered from a killed job (default: $GEMINI_OUTPUT_DIR or cwd)")
|
|
36033
36377
|
}
|
|
36034
36378
|
},
|
|
36035
|
-
async (args) =>
|
|
36379
|
+
async (args) => {
|
|
36380
|
+
try {
|
|
36381
|
+
return await client2.session.jobs.getResult(args.job_id);
|
|
36382
|
+
} catch (err) {
|
|
36383
|
+
const recovered = await recoverFromInteraction(client2, args.job_id, args.output_dir, err);
|
|
36384
|
+
if (recovered) return recovered;
|
|
36385
|
+
throw err;
|
|
36386
|
+
}
|
|
36387
|
+
}
|
|
36036
36388
|
);
|
|
36037
36389
|
}
|
|
36390
|
+
async function recoverFromInteraction(client2, jobId, outputDir, original) {
|
|
36391
|
+
const record2 = await client2.session.jobs.peekRecord(jobId);
|
|
36392
|
+
const interactionId = record2?.interactionId;
|
|
36393
|
+
if (!interactionId || record2?.status !== "failed") return void 0;
|
|
36394
|
+
let found;
|
|
36395
|
+
try {
|
|
36396
|
+
found = await client2.fetchInteractionMedia(interactionId);
|
|
36397
|
+
} catch (probeErr) {
|
|
36398
|
+
if (isInFlight(probeErr)) return stillRunning(jobId, interactionId);
|
|
36399
|
+
throw new McpToolError(
|
|
36400
|
+
`${errorMessage(original)} Its upstream interaction "${interactionId}" could not be read either (${errorMessage(probeErr)}), so the output is not recoverable.`,
|
|
36401
|
+
{ hint: "Any media the lost run did write is still listed by gemini_list_recent_media. Otherwise the generation has to be re-run." }
|
|
36402
|
+
);
|
|
36403
|
+
}
|
|
36404
|
+
const [kind, media] = found.videos.length ? ["video", found.videos] : found.audios.length ? ["audio", found.audios] : found.images.length ? ["image", found.images] : ["image", []];
|
|
36405
|
+
if (media.length === 0) {
|
|
36406
|
+
if (found.status && !TERMINAL_INTERACTION_STATUSES.has(found.status)) return stillRunning(jobId, interactionId);
|
|
36407
|
+
return void 0;
|
|
36408
|
+
}
|
|
36409
|
+
const slug = slugify2(`recovered-${interactionId}`.slice(0, 60));
|
|
36410
|
+
const named = media.map((m, i) => ({
|
|
36411
|
+
media: m,
|
|
36412
|
+
base: media.length > 1 ? `${slug}-${String(i + 1).padStart(2, "0")}` : slug
|
|
36413
|
+
}));
|
|
36414
|
+
const result = await emitMedia(named, kind, { output_dir: outputDir, sink: client2.mediaSink }, {
|
|
36415
|
+
job_id: jobId,
|
|
36416
|
+
recovered_from_interaction: interactionId,
|
|
36417
|
+
note: recoveryNote(record2.error?.message),
|
|
36418
|
+
...found.text ? { text: found.text } : {}
|
|
36419
|
+
});
|
|
36420
|
+
await client2.session.jobs.adoptResult(jobId, record2.toolName, result, interactionId);
|
|
36421
|
+
return result;
|
|
36422
|
+
}
|
|
36423
|
+
function recoveryNote(recordedError) {
|
|
36424
|
+
const cause = /was lost:/.test(recordedError ?? "") ? "The executor that started this job was reclaimed before it could return." : "This job failed here before it could return its output.";
|
|
36425
|
+
return `${cause} The generation had already completed upstream and its output was recovered \u2014 it was not re-run, and you were not billed twice.`;
|
|
36426
|
+
}
|
|
36427
|
+
function stillRunning(jobId, interactionId) {
|
|
36428
|
+
return textResult({
|
|
36429
|
+
job_id: jobId,
|
|
36430
|
+
status: "running",
|
|
36431
|
+
interaction_id: interactionId,
|
|
36432
|
+
note: "The executor that started this job is gone, but the generation is still running upstream. Poll again \u2014 it has not been lost, and re-running it would bill a second time."
|
|
36433
|
+
});
|
|
36434
|
+
}
|
|
36435
|
+
function isInFlight(err) {
|
|
36436
|
+
const status = err?.status;
|
|
36437
|
+
return status === 403 || status === 404;
|
|
36438
|
+
}
|
|
36439
|
+
function errorMessage(err) {
|
|
36440
|
+
return err instanceof Error ? err.message : String(err);
|
|
36441
|
+
}
|
|
36038
36442
|
|
|
36039
36443
|
// src/tools/files.ts
|
|
36040
36444
|
init_zod();
|