@chrischall/gemini-mcp 1.3.0 → 1.5.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/README.md +76 -3
- package/dist/bundle.js +523 -51
- package/dist/client.js +24 -0
- package/dist/inputs.js +37 -1
- package/dist/jobs.js +122 -2
- package/dist/library.js +151 -0
- package/dist/media-cleanup.js +4 -1
- package/dist/media-endpoint.js +109 -15
- package/dist/media-url.js +2 -2
- package/dist/put-endpoint.js +122 -0
- package/dist/storage/media.js +53 -4
- package/dist/tools/files.js +61 -3
- package/dist/tools/generate.js +75 -30
- package/dist/tools/interact.js +3 -2
- package/dist/tools/library.js +186 -0
- package/dist/tools/music.js +3 -2
- package/dist/tools/set.js +76 -12
- package/dist/tools/shared.js +202 -2
- package/dist/tools/uploads.js +69 -0
- package/dist/tools/video.js +3 -2
- package/dist/upload-url.js +138 -0
- package/dist/version.js +1 -1
- package/dist/zip.js +113 -0
- package/package.json +1 -1
- package/server.json +2 -2
package/dist/bundle.js
CHANGED
|
@@ -32783,6 +32783,15 @@ var init_clipboard = __esm({
|
|
|
32783
32783
|
}
|
|
32784
32784
|
});
|
|
32785
32785
|
|
|
32786
|
+
// src/media-url.ts
|
|
32787
|
+
var MEDIA_URL_TTL_MS;
|
|
32788
|
+
var init_media_url = __esm({
|
|
32789
|
+
"src/media-url.ts"() {
|
|
32790
|
+
"use strict";
|
|
32791
|
+
MEDIA_URL_TTL_MS = 48 * 60 * 60 * 1e3;
|
|
32792
|
+
}
|
|
32793
|
+
});
|
|
32794
|
+
|
|
32786
32795
|
// src/index.ts
|
|
32787
32796
|
init_dist();
|
|
32788
32797
|
|
|
@@ -32796,7 +32805,7 @@ async function loadStdioDotenv() {
|
|
|
32796
32805
|
}
|
|
32797
32806
|
|
|
32798
32807
|
// src/version.ts
|
|
32799
|
-
var VERSION = "1.
|
|
32808
|
+
var VERSION = "1.5.0";
|
|
32800
32809
|
|
|
32801
32810
|
// src/tools/models.ts
|
|
32802
32811
|
init_dist();
|
|
@@ -33093,7 +33102,7 @@ async function fetchRemoteImage(requestedUrl, opts = {}) {
|
|
|
33093
33102
|
// src/inputs.ts
|
|
33094
33103
|
function hasImageInput(args) {
|
|
33095
33104
|
return Boolean(
|
|
33096
|
-
args.images?.length || args.images_base64?.length || args.images_url?.length || args.images_file_uris?.length || args.from_clipboard
|
|
33105
|
+
args.images?.length || args.images_base64?.length || args.images_url?.length || args.images_file_uris?.length || args.images_r2_keys?.length || args.from_clipboard
|
|
33097
33106
|
);
|
|
33098
33107
|
}
|
|
33099
33108
|
var ASSUMED_TTL_MS = 47 * 60 * 60 * 1e3;
|
|
@@ -33142,6 +33151,32 @@ async function resolveImageInputs(args, client2) {
|
|
|
33142
33151
|
(report.file_uris ??= []).push(file2.name);
|
|
33143
33152
|
inputs.push(input);
|
|
33144
33153
|
}
|
|
33154
|
+
const seenKeys = /* @__PURE__ */ new Map();
|
|
33155
|
+
for (const key of args.images_r2_keys ?? []) {
|
|
33156
|
+
const hit = seenKeys.get(key);
|
|
33157
|
+
if (hit) {
|
|
33158
|
+
inputs.push(hit);
|
|
33159
|
+
continue;
|
|
33160
|
+
}
|
|
33161
|
+
const stored = await client2.readStoredMedia(key);
|
|
33162
|
+
const entry = {
|
|
33163
|
+
r2_key: key.trim(),
|
|
33164
|
+
mime_type: stored.mimeType,
|
|
33165
|
+
bytes: stored.bytes.byteLength
|
|
33166
|
+
};
|
|
33167
|
+
(report.r2_keys ??= []).push(entry);
|
|
33168
|
+
let input;
|
|
33169
|
+
if (stored.bytes.byteLength <= IMAGE_INLINE_MAX_BYTES) {
|
|
33170
|
+
input = { base64: bytesToBase64(stored.bytes), mimeType: stored.mimeType };
|
|
33171
|
+
} else {
|
|
33172
|
+
const displayName = key.trim().split("/").pop() || "image";
|
|
33173
|
+
const uploaded = await client2.uploadBytes(stored.bytes, stored.mimeType, displayName);
|
|
33174
|
+
entry.file_uri = uploaded.name;
|
|
33175
|
+
input = { uri: uploaded.uri, mimeType: uploaded.mimeType };
|
|
33176
|
+
}
|
|
33177
|
+
seenKeys.set(key, input);
|
|
33178
|
+
inputs.push(input);
|
|
33179
|
+
}
|
|
33145
33180
|
return { inputs, report: Object.keys(report).length > 0 ? report : void 0 };
|
|
33146
33181
|
}
|
|
33147
33182
|
async function resolveLocalPath(path, client2, onDisk, report) {
|
|
@@ -33196,7 +33231,7 @@ async function resolveUrl(url2, client2, report) {
|
|
|
33196
33231
|
function requireImageInput(args) {
|
|
33197
33232
|
if (hasImageInput(args)) return;
|
|
33198
33233
|
throw new McpToolError(
|
|
33199
|
-
"Provide at least one input image via `images_url` (an https URL the server fetches), `images_file_uris` (from gemini_upload_file), `images` (a local path), `images_base64`, or `from_clipboard`."
|
|
33234
|
+
"Provide at least one input image via `images_url` (an https URL the server fetches), `images_file_uris` (from gemini_upload_file), `images_r2_keys` (a hosted-connector upload or earlier generation), `images` (a local path), `images_base64`, or `from_clipboard`."
|
|
33200
33235
|
);
|
|
33201
33236
|
}
|
|
33202
33237
|
|
|
@@ -33221,6 +33256,104 @@ function createDiskSink() {
|
|
|
33221
33256
|
};
|
|
33222
33257
|
}
|
|
33223
33258
|
|
|
33259
|
+
// src/media-endpoint.ts
|
|
33260
|
+
init_media_url();
|
|
33261
|
+
function downloadFilename(key) {
|
|
33262
|
+
const last = key.split("/").pop() || "media";
|
|
33263
|
+
const stripped = last.replace(/^[0-9a-f]{6,12}-/i, "");
|
|
33264
|
+
return stripped || last;
|
|
33265
|
+
}
|
|
33266
|
+
|
|
33267
|
+
// src/zip.ts
|
|
33268
|
+
var CRC_TABLE = (() => {
|
|
33269
|
+
const table = new Uint32Array(256);
|
|
33270
|
+
for (let n = 0; n < 256; n++) {
|
|
33271
|
+
let c = n;
|
|
33272
|
+
for (let k = 0; k < 8; k++) c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
|
|
33273
|
+
table[n] = c >>> 0;
|
|
33274
|
+
}
|
|
33275
|
+
return table;
|
|
33276
|
+
})();
|
|
33277
|
+
function crc32(bytes) {
|
|
33278
|
+
let crc = 4294967295;
|
|
33279
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
33280
|
+
crc = CRC_TABLE[(crc ^ bytes[i]) & 255] ^ crc >>> 8;
|
|
33281
|
+
}
|
|
33282
|
+
return (crc ^ 4294967295) >>> 0;
|
|
33283
|
+
}
|
|
33284
|
+
function dosDateTime(at) {
|
|
33285
|
+
const year = Math.max(1980, at.getUTCFullYear());
|
|
33286
|
+
return {
|
|
33287
|
+
time: at.getUTCHours() << 11 | at.getUTCMinutes() << 5 | at.getUTCSeconds() >> 1,
|
|
33288
|
+
date: year - 1980 << 9 | at.getUTCMonth() + 1 << 5 | at.getUTCDate()
|
|
33289
|
+
};
|
|
33290
|
+
}
|
|
33291
|
+
var LOCAL_HEADER = 67324752;
|
|
33292
|
+
var CENTRAL_HEADER = 33639248;
|
|
33293
|
+
var EOCD = 101010256;
|
|
33294
|
+
var VERSION2 = 20;
|
|
33295
|
+
var FLAG_UTF8 = 2048;
|
|
33296
|
+
function buildZip(entries, modifiedAt = new Date(Date.UTC(1980, 0, 1))) {
|
|
33297
|
+
const { time: time3, date: date5 } = dosDateTime(modifiedAt);
|
|
33298
|
+
const encoder = new TextEncoder();
|
|
33299
|
+
const parts = [];
|
|
33300
|
+
const central = [];
|
|
33301
|
+
let offset = 0;
|
|
33302
|
+
for (const entry of entries) {
|
|
33303
|
+
const name = encoder.encode(entry.name);
|
|
33304
|
+
const crc = crc32(entry.bytes);
|
|
33305
|
+
const size = entry.bytes.byteLength;
|
|
33306
|
+
const local = new Uint8Array(30 + name.byteLength);
|
|
33307
|
+
const lv = new DataView(local.buffer);
|
|
33308
|
+
lv.setUint32(0, LOCAL_HEADER, true);
|
|
33309
|
+
lv.setUint16(4, VERSION2, true);
|
|
33310
|
+
lv.setUint16(6, FLAG_UTF8, true);
|
|
33311
|
+
lv.setUint16(8, 0, true);
|
|
33312
|
+
lv.setUint16(10, time3, true);
|
|
33313
|
+
lv.setUint16(12, date5, true);
|
|
33314
|
+
lv.setUint32(14, crc, true);
|
|
33315
|
+
lv.setUint32(18, size, true);
|
|
33316
|
+
lv.setUint32(22, size, true);
|
|
33317
|
+
lv.setUint16(26, name.byteLength, true);
|
|
33318
|
+
lv.setUint16(28, 0, true);
|
|
33319
|
+
local.set(name, 30);
|
|
33320
|
+
const dir = new Uint8Array(46 + name.byteLength);
|
|
33321
|
+
const dv = new DataView(dir.buffer);
|
|
33322
|
+
dv.setUint32(0, CENTRAL_HEADER, true);
|
|
33323
|
+
dv.setUint16(4, VERSION2, true);
|
|
33324
|
+
dv.setUint16(6, VERSION2, true);
|
|
33325
|
+
dv.setUint16(8, FLAG_UTF8, true);
|
|
33326
|
+
dv.setUint16(10, 0, true);
|
|
33327
|
+
dv.setUint16(12, time3, true);
|
|
33328
|
+
dv.setUint16(14, date5, true);
|
|
33329
|
+
dv.setUint32(16, crc, true);
|
|
33330
|
+
dv.setUint32(20, size, true);
|
|
33331
|
+
dv.setUint32(24, size, true);
|
|
33332
|
+
dv.setUint16(28, name.byteLength, true);
|
|
33333
|
+
dv.setUint32(42, offset, true);
|
|
33334
|
+
dir.set(name, 46);
|
|
33335
|
+
parts.push(local, entry.bytes);
|
|
33336
|
+
central.push(dir);
|
|
33337
|
+
offset += local.byteLength + size;
|
|
33338
|
+
}
|
|
33339
|
+
const centralSize = central.reduce((n, c) => n + c.byteLength, 0);
|
|
33340
|
+
const eocd = new Uint8Array(22);
|
|
33341
|
+
const ev = new DataView(eocd.buffer);
|
|
33342
|
+
ev.setUint32(0, EOCD, true);
|
|
33343
|
+
ev.setUint16(8, entries.length, true);
|
|
33344
|
+
ev.setUint16(10, entries.length, true);
|
|
33345
|
+
ev.setUint32(12, centralSize, true);
|
|
33346
|
+
ev.setUint32(16, offset, true);
|
|
33347
|
+
const total = offset + centralSize + eocd.byteLength;
|
|
33348
|
+
const out = new Uint8Array(total);
|
|
33349
|
+
let cursor = 0;
|
|
33350
|
+
for (const part of [...parts, ...central, eocd]) {
|
|
33351
|
+
out.set(part, cursor);
|
|
33352
|
+
cursor += part.byteLength;
|
|
33353
|
+
}
|
|
33354
|
+
return out;
|
|
33355
|
+
}
|
|
33356
|
+
|
|
33224
33357
|
// src/tools/shared.ts
|
|
33225
33358
|
var DEFAULT_SINK = createDiskSink();
|
|
33226
33359
|
var ASPECT_RATIOS = ["1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "21:9", "1:4", "4:1", "1:8", "8:1"];
|
|
@@ -33235,6 +33368,77 @@ var idempotencyKeySchema = external_exports.string().min(1).optional().describe(
|
|
|
33235
33368
|
var asyncSchema = external_exports.boolean().optional().describe(
|
|
33236
33369
|
"Run in the background and return a job_id immediately instead of the image, so a long (Pro/4K) generation cannot hit the host tools/call timeout (-32001). Poll gemini_get_result with the job_id to fetch the result (jobs are per-process and expire ~10 min after completion)."
|
|
33237
33370
|
);
|
|
33371
|
+
var maxWaitMsSchema = external_exports.number().int().positive().max(6e5).optional().describe(
|
|
33372
|
+
'Wait up to this many ms for the result; if generation is still running when the budget expires, return { job_id, status: "running" } immediately instead (poll gemini_get_result). Keeps fast results in-band while a slow batch can never trip the host tools/call timeout (-32001) \u2014 e.g. 20000 for multi-image sets. Ignored when async is set.'
|
|
33373
|
+
);
|
|
33374
|
+
function imagesR2KeysSchema(label = "Reference images") {
|
|
33375
|
+
return external_exports.array(external_exports.string().min(1)).optional().describe(
|
|
33376
|
+
`${label} by r2_key from THIS connector's store: a signed upload (gemini_get_upload_url \u2192 curl PUT) or an earlier generation's media[].r2_key. The server reads its own bucket directly \u2014 no bytes in the conversation, no signed URL, no ~48h Files API expiry. Hosted connector only.`
|
|
33377
|
+
);
|
|
33378
|
+
}
|
|
33379
|
+
var charactersSchema = external_exports.array(external_exports.string().min(1)).max(8).optional().describe(
|
|
33380
|
+
"Names of saved characters (see gemini_list_characters / gemini_save_character): each one's reference image and description are attached to the request automatically, keeping recurring subjects consistent without re-sending anything. Hosted connector only."
|
|
33381
|
+
);
|
|
33382
|
+
var styleSchema = external_exports.string().min(1).optional().describe(
|
|
33383
|
+
"Name of a saved style preset (see gemini_list_styles / gemini_save_style): its prompt fragment \u2014 and reference image, if it has one \u2014 is applied to the request automatically. Hosted connector only."
|
|
33384
|
+
);
|
|
33385
|
+
async function resolveCharacterRefs(client2, names, styleName) {
|
|
33386
|
+
if (!names?.length && !styleName) return { inputs: [] };
|
|
33387
|
+
const library = client2.library;
|
|
33388
|
+
if (!library) {
|
|
33389
|
+
throw new McpToolError(
|
|
33390
|
+
"`characters` / `style` reference this account's saved library, which exists on the hosted connector only. On this server pass the reference images directly (images / images_url / images_file_uris) and put the style text in the prompt."
|
|
33391
|
+
);
|
|
33392
|
+
}
|
|
33393
|
+
const inputs = [];
|
|
33394
|
+
const described = [];
|
|
33395
|
+
const toInput = async (image, displayName) => {
|
|
33396
|
+
if (image.bytes.byteLength <= IMAGE_INLINE_MAX_BYTES) {
|
|
33397
|
+
return { base64: bytesToBase64(image.bytes), mimeType: image.mimeType };
|
|
33398
|
+
}
|
|
33399
|
+
const uploaded = await client2.uploadBytes(image.bytes, image.mimeType, displayName);
|
|
33400
|
+
return { uri: uploaded.uri, mimeType: uploaded.mimeType };
|
|
33401
|
+
};
|
|
33402
|
+
for (const name of names ?? []) {
|
|
33403
|
+
const record2 = await library.getCharacter(name);
|
|
33404
|
+
const image = record2 && await library.readCharacterImage(name);
|
|
33405
|
+
if (!record2 || !image) {
|
|
33406
|
+
const available = (await library.listCharacters()).map((c) => c.name);
|
|
33407
|
+
throw new McpToolError(
|
|
33408
|
+
`No saved character "${name}". ` + (available.length ? `Saved characters: ${available.join(", ")}. ` : "None are saved yet. ") + "Save one with gemini_save_character."
|
|
33409
|
+
);
|
|
33410
|
+
}
|
|
33411
|
+
inputs.push(await toInput(image, name));
|
|
33412
|
+
described.push(`${record2.name} \u2014 ${record2.description}`);
|
|
33413
|
+
}
|
|
33414
|
+
let styleFragment;
|
|
33415
|
+
let styleImageAttached = false;
|
|
33416
|
+
if (styleName) {
|
|
33417
|
+
const record2 = await library.getStyle(styleName);
|
|
33418
|
+
if (!record2) {
|
|
33419
|
+
const available = (await library.listStyles()).map((s) => s.name);
|
|
33420
|
+
throw new McpToolError(
|
|
33421
|
+
`No saved style "${styleName}". ` + (available.length ? `Saved styles: ${available.join(", ")}. ` : "None are saved yet. ") + "Save one with gemini_save_style."
|
|
33422
|
+
);
|
|
33423
|
+
}
|
|
33424
|
+
styleFragment = record2.prompt_fragment;
|
|
33425
|
+
if (record2.image_key) {
|
|
33426
|
+
const image = await library.readStyleImage(styleName);
|
|
33427
|
+
if (image) {
|
|
33428
|
+
inputs.push(await toInput(image, styleName));
|
|
33429
|
+
styleImageAttached = true;
|
|
33430
|
+
}
|
|
33431
|
+
}
|
|
33432
|
+
}
|
|
33433
|
+
const characterLine = described.length ? `Use the attached reference images for these characters, in order: ${described.map((d, i) => `(${i + 1}) ${d}`).join("; ")}. Keep each character visually consistent with their reference image.` + (styleImageAttached ? ` Attached image (${described.length + 1}) is a style reference, not a character.` : "") : styleImageAttached ? "The first attached image is a style reference." : void 0;
|
|
33434
|
+
const meta3 = {};
|
|
33435
|
+
if (names?.length) meta3.characters = names;
|
|
33436
|
+
if (styleName) meta3.style = styleName;
|
|
33437
|
+
return { inputs, characterLine, styleFragment, meta: Object.keys(meta3).length ? meta3 : void 0 };
|
|
33438
|
+
}
|
|
33439
|
+
function composePrompt(prompt, refs) {
|
|
33440
|
+
return [refs.characterLine, prompt, refs.styleFragment ? `Style: ${refs.styleFragment}` : void 0].filter(Boolean).join("\n\n");
|
|
33441
|
+
}
|
|
33238
33442
|
function imagesUrlSchema(label = "Reference images") {
|
|
33239
33443
|
return external_exports.array(external_exports.string().url()).optional().describe(
|
|
33240
33444
|
`${label} as public https URLs \u2014 the SERVER downloads them, so no image bytes travel through the conversation. Preferred over images_base64, which costs ~14k tokens per photo and breaks if a file read was truncated. Max ${wholeMb(IMAGE_URL_MAX_BYTES)}MB each; must be a directly-linked image (Content-Type image/*).`
|
|
@@ -33260,7 +33464,8 @@ var sharedImageSchema = {
|
|
|
33260
33464
|
from_clipboard: external_exports.boolean().optional().describe("Use the image currently on the macOS system clipboard as an input (downscaled to JPEG)"),
|
|
33261
33465
|
timeout_ms: timeoutMsSchema,
|
|
33262
33466
|
idempotency_key: idempotencyKeySchema,
|
|
33263
|
-
async: asyncSchema
|
|
33467
|
+
async: asyncSchema,
|
|
33468
|
+
max_wait_ms: maxWaitMsSchema
|
|
33264
33469
|
};
|
|
33265
33470
|
async function resolveVideoInput(args, client2) {
|
|
33266
33471
|
if (args.video_url && args.video_path) {
|
|
@@ -33347,7 +33552,8 @@ async function emitMedia(named, kind, opts, meta3, onWritten) {
|
|
|
33347
33552
|
}
|
|
33348
33553
|
const sink = opts.sink ?? DEFAULT_SINK;
|
|
33349
33554
|
const persisted = await sink.persist(named.map((n) => ({ base: n.base, base64: n.media.base64, mimeType: n.media.mimeType })), {
|
|
33350
|
-
output_dir: opts.output_dir
|
|
33555
|
+
output_dir: opts.output_dir,
|
|
33556
|
+
urlTtlMs: opts.urlTtlMs
|
|
33351
33557
|
});
|
|
33352
33558
|
const refs = persisted.map((p) => p.ref);
|
|
33353
33559
|
if (sink.persistsFiles) await onWritten?.(refs);
|
|
@@ -33361,7 +33567,13 @@ async function emitMedia(named, kind, opts, meta3, onWritten) {
|
|
|
33361
33567
|
media: persisted.map((p) => ({
|
|
33362
33568
|
url: p.ref,
|
|
33363
33569
|
...p.key ? { r2_key: p.key } : {},
|
|
33364
|
-
...p.expiresAt ? { expires_at: p.expiresAt } : {}
|
|
33570
|
+
...p.expiresAt ? { expires_at: p.expiresAt } : {},
|
|
33571
|
+
// A ready-to-run command, not a description of one. Models copy an
|
|
33572
|
+
// exact string far more reliably than they compose one — and in a
|
|
33573
|
+
// chat client this download-then-attach flow is the ONLY way the end
|
|
33574
|
+
// user actually sees the image (inline MCP blocks and markdown embeds
|
|
33575
|
+
// both render for the model and not for them).
|
|
33576
|
+
...p.key ? { curl_hint: `curl -sS -o ${downloadFilename(p.key)} "${p.ref}"` } : {}
|
|
33365
33577
|
}))
|
|
33366
33578
|
} : {};
|
|
33367
33579
|
const storage = sink.persistsFiles ? {} : { storage: sink.kind, storage_note: storageNote };
|
|
@@ -33370,6 +33582,40 @@ async function emitMedia(named, kind, opts, meta3, onWritten) {
|
|
|
33370
33582
|
async function emit(named, opts, meta3, onWritten) {
|
|
33371
33583
|
return emitMedia(named.map((n) => ({ media: n.image, base: n.base })), "image", opts, meta3, onWritten);
|
|
33372
33584
|
}
|
|
33585
|
+
var SET_URL_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
33586
|
+
var BUNDLE_MAX_TOTAL_BYTES = 24 * 1024 * 1024;
|
|
33587
|
+
async function persistBundle(named, sink, base) {
|
|
33588
|
+
if (!sink || sink.persistsFiles || named.length < 2) return {};
|
|
33589
|
+
const totalBytes = named.reduce((sum, n) => sum + Math.floor(n.image.base64.length * 3 / 4), 0);
|
|
33590
|
+
if (totalBytes > BUNDLE_MAX_TOTAL_BYTES) {
|
|
33591
|
+
return {
|
|
33592
|
+
bundle_skipped: `The set totals ~${wholeMb(totalBytes)}MB decoded, over the ${wholeMb(BUNDLE_MAX_TOTAL_BYTES)}MB bundling cap (zipping peaks at ~4x that in Worker memory). Download the images individually via their media urls / curl_hints.`
|
|
33593
|
+
};
|
|
33594
|
+
}
|
|
33595
|
+
try {
|
|
33596
|
+
const { mediaExt: mediaExt2 } = await Promise.resolve().then(() => (init_images(), images_exports));
|
|
33597
|
+
const zip = buildZip(named.map((n) => ({ name: `${n.base}.${mediaExt2(n.image.mimeType)}`, bytes: base64ToBytes(n.image.base64) })));
|
|
33598
|
+
const [persisted] = await sink.persist(
|
|
33599
|
+
[{ base: `${base}-bundle`, base64: bytesToBase64(zip), mimeType: "application/zip" }],
|
|
33600
|
+
{ urlTtlMs: SET_URL_TTL_MS }
|
|
33601
|
+
);
|
|
33602
|
+
if (!persisted || persisted.unavailable) {
|
|
33603
|
+
return { bundle_skipped: "The bundle zip could not be stored; the images themselves are unaffected \u2014 download them individually via their media urls / curl_hints." };
|
|
33604
|
+
}
|
|
33605
|
+
return {
|
|
33606
|
+
bundle_url: persisted.ref,
|
|
33607
|
+
bundle: {
|
|
33608
|
+
url: persisted.ref,
|
|
33609
|
+
...persisted.key ? { r2_key: persisted.key } : {},
|
|
33610
|
+
...persisted.expiresAt ? { expires_at: persisted.expiresAt } : {},
|
|
33611
|
+
files: named.map((n) => n.base),
|
|
33612
|
+
...persisted.key ? { curl_hint: `curl -sS -o ${downloadFilename(persisted.key)} "${persisted.ref}"` } : {}
|
|
33613
|
+
}
|
|
33614
|
+
};
|
|
33615
|
+
} catch {
|
|
33616
|
+
return { bundle_skipped: "Bundling failed; the images themselves are unaffected \u2014 download them individually via their media urls / curl_hints." };
|
|
33617
|
+
}
|
|
33618
|
+
}
|
|
33373
33619
|
function assertLocalInputsAvailable(sink, args) {
|
|
33374
33620
|
if ((sink ?? DEFAULT_SINK).persistsFiles) return;
|
|
33375
33621
|
const unavailable = [];
|
|
@@ -33396,6 +33642,64 @@ function isExpired(entry, now) {
|
|
|
33396
33642
|
function fingerprintRequest(toolName, parts) {
|
|
33397
33643
|
return createHash("sha256").update(`${toolName}\0${JSON.stringify(parts)}`).digest("hex");
|
|
33398
33644
|
}
|
|
33645
|
+
async function refreshMedia(result, sink) {
|
|
33646
|
+
const first = result.content?.[0];
|
|
33647
|
+
if (!sink?.resign || !first || first.type !== "text") return result;
|
|
33648
|
+
let body;
|
|
33649
|
+
try {
|
|
33650
|
+
body = JSON.parse(first.text);
|
|
33651
|
+
} catch {
|
|
33652
|
+
return result;
|
|
33653
|
+
}
|
|
33654
|
+
const entries = Array.isArray(body.media) ? body.media : [];
|
|
33655
|
+
if (!entries.some((e) => typeof e.r2_key === "string")) return result;
|
|
33656
|
+
const refreshed = await Promise.all(entries.map(async (e) => {
|
|
33657
|
+
if (typeof e.r2_key !== "string") return void 0;
|
|
33658
|
+
try {
|
|
33659
|
+
return await sink.resign(e.r2_key);
|
|
33660
|
+
} catch {
|
|
33661
|
+
return void 0;
|
|
33662
|
+
}
|
|
33663
|
+
}));
|
|
33664
|
+
if (refreshed.every((r) => r === void 0)) return result;
|
|
33665
|
+
const mediaKeyName = ["images", "videos", "audios"].find((k) => Array.isArray(body[k]));
|
|
33666
|
+
const refs = [];
|
|
33667
|
+
refreshed.forEach((fresh, i) => {
|
|
33668
|
+
if (!fresh) {
|
|
33669
|
+
refs.push(String(entries[i]?.url ?? ""));
|
|
33670
|
+
return;
|
|
33671
|
+
}
|
|
33672
|
+
const { expires_at: _stale, ...rest } = entries[i];
|
|
33673
|
+
entries[i] = {
|
|
33674
|
+
...rest,
|
|
33675
|
+
url: fresh.ref,
|
|
33676
|
+
...fresh.expiresAt ? { expires_at: fresh.expiresAt } : {},
|
|
33677
|
+
curl_hint: `curl -sS -o ${downloadFilename(fresh.key ?? "media")} "${fresh.ref}"`
|
|
33678
|
+
};
|
|
33679
|
+
refs.push(fresh.ref);
|
|
33680
|
+
});
|
|
33681
|
+
body.media = entries;
|
|
33682
|
+
if (mediaKeyName) body[mediaKeyName] = refs;
|
|
33683
|
+
const bundle = body.bundle;
|
|
33684
|
+
if (bundle && typeof bundle.r2_key === "string") {
|
|
33685
|
+
try {
|
|
33686
|
+
const fresh = await sink.resign(bundle.r2_key);
|
|
33687
|
+
if (fresh) {
|
|
33688
|
+
const { expires_at: _staleBundle, ...rest } = bundle;
|
|
33689
|
+
body.bundle = {
|
|
33690
|
+
...rest,
|
|
33691
|
+
url: fresh.ref,
|
|
33692
|
+
...fresh.expiresAt ? { expires_at: fresh.expiresAt } : {},
|
|
33693
|
+
curl_hint: `curl -sS -o ${downloadFilename(fresh.key ?? "media")} "${fresh.ref}"`
|
|
33694
|
+
};
|
|
33695
|
+
body.bundle_url = fresh.ref;
|
|
33696
|
+
}
|
|
33697
|
+
} catch {
|
|
33698
|
+
}
|
|
33699
|
+
}
|
|
33700
|
+
body.media_urls_refreshed = true;
|
|
33701
|
+
return { ...result, content: [{ ...first, text: JSON.stringify(body, null, 2) }, ...(result.content ?? []).slice(1)] };
|
|
33702
|
+
}
|
|
33399
33703
|
function annotateReused(result, jobId) {
|
|
33400
33704
|
const content = result.content ?? [];
|
|
33401
33705
|
const first = content[0];
|
|
@@ -33422,7 +33726,29 @@ function jobHandle(jobId, status) {
|
|
|
33422
33726
|
hint: `Generation running in the background. Poll gemini_get_result with job_id "${jobId}" until status is "done" (results are per-process and expire ~10 min after completion).`
|
|
33423
33727
|
});
|
|
33424
33728
|
}
|
|
33729
|
+
function awaitWithBudget(promise2, waitMs) {
|
|
33730
|
+
return new Promise((resolve2, reject) => {
|
|
33731
|
+
const timer = setTimeout(() => resolve2(void 0), waitMs);
|
|
33732
|
+
timer.unref?.();
|
|
33733
|
+
promise2.then(
|
|
33734
|
+
(result) => {
|
|
33735
|
+
clearTimeout(timer);
|
|
33736
|
+
resolve2(result);
|
|
33737
|
+
},
|
|
33738
|
+
(err) => {
|
|
33739
|
+
clearTimeout(timer);
|
|
33740
|
+
reject(err);
|
|
33741
|
+
}
|
|
33742
|
+
);
|
|
33743
|
+
});
|
|
33744
|
+
}
|
|
33425
33745
|
var JobRegistry = class {
|
|
33746
|
+
/**
|
|
33747
|
+
* Lets a replayed result have its media URLs re-minted. Set by the session
|
|
33748
|
+
* that owns this registry; absent on stdio, where refs are file paths that
|
|
33749
|
+
* never expire.
|
|
33750
|
+
*/
|
|
33751
|
+
resigner;
|
|
33426
33752
|
jobs = /* @__PURE__ */ new Map();
|
|
33427
33753
|
byKey = /* @__PURE__ */ new Map();
|
|
33428
33754
|
// idempotencyKey -> jobId
|
|
@@ -33468,7 +33794,7 @@ var JobRegistry = class {
|
|
|
33468
33794
|
* different registry and therefore a genuinely separate generation.
|
|
33469
33795
|
*/
|
|
33470
33796
|
async dispatch(opts, work) {
|
|
33471
|
-
const { toolName, fingerprint, idempotencyKey, async } = opts;
|
|
33797
|
+
const { toolName, fingerprint, idempotencyKey, async, waitMs } = opts;
|
|
33472
33798
|
const now = Date.now();
|
|
33473
33799
|
let hit;
|
|
33474
33800
|
if (idempotencyKey !== void 0) {
|
|
@@ -33482,7 +33808,9 @@ var JobRegistry = class {
|
|
|
33482
33808
|
}
|
|
33483
33809
|
if (hit) {
|
|
33484
33810
|
if (async) return jobHandle(hit.jobId, hit.status);
|
|
33485
|
-
|
|
33811
|
+
const settled = waitMs !== void 0 && hit.status === "running" ? await awaitWithBudget(hit.promise, waitMs) : await hit.promise;
|
|
33812
|
+
if (settled === void 0) return jobHandle(hit.jobId, "running");
|
|
33813
|
+
return annotateReused(await refreshMedia(settled, this.resigner), hit.jobId);
|
|
33486
33814
|
}
|
|
33487
33815
|
const jobId = randomUUID();
|
|
33488
33816
|
this.evict(now);
|
|
@@ -33506,6 +33834,10 @@ var JobRegistry = class {
|
|
|
33506
33834
|
if (this.runningByFingerprint.get(fingerprint) === jobId) this.runningByFingerprint.delete(fingerprint);
|
|
33507
33835
|
});
|
|
33508
33836
|
if (async) return jobHandle(jobId, "running");
|
|
33837
|
+
if (waitMs !== void 0) {
|
|
33838
|
+
const settled = await awaitWithBudget(promise2, waitMs);
|
|
33839
|
+
return settled ?? jobHandle(jobId, "running");
|
|
33840
|
+
}
|
|
33509
33841
|
return promise2;
|
|
33510
33842
|
}
|
|
33511
33843
|
/**
|
|
@@ -33573,7 +33905,10 @@ function registerGenerateTools(server, client2) {
|
|
|
33573
33905
|
images: external_exports.array(external_exports.string().min(1)).optional().describe("Paths to reference input images (image-conditioned generation)"),
|
|
33574
33906
|
images_url: imagesUrlSchema(),
|
|
33575
33907
|
images_file_uris: imagesFileUrisSchema(),
|
|
33908
|
+
images_r2_keys: imagesR2KeysSchema(),
|
|
33576
33909
|
images_base64: external_exports.array(external_exports.string().min(1)).optional().describe("Reference images as base64 strings or data URIs. Last resort: prefer images_url or images_file_uris, which keep image bytes out of the conversation"),
|
|
33910
|
+
characters: charactersSchema,
|
|
33911
|
+
style: styleSchema,
|
|
33577
33912
|
video_url: external_exports.string().url().optional().describe("Public YouTube URL (or a previously uploaded Files API uri) as a video reference (video\u2192image; use a Flash model e.g. gemini-3.1-flash-image)"),
|
|
33578
33913
|
video_path: videoPathSchema,
|
|
33579
33914
|
confirm: schemaConfirm,
|
|
@@ -33599,42 +33934,61 @@ function registerGenerateTools(server, client2) {
|
|
|
33599
33934
|
from_clipboard: args.from_clipboard,
|
|
33600
33935
|
images_url: args.images_url,
|
|
33601
33936
|
images_file_uris: args.images_file_uris,
|
|
33937
|
+
images_r2_keys: args.images_r2_keys,
|
|
33938
|
+
characters: args.characters,
|
|
33939
|
+
style: args.style,
|
|
33602
33940
|
video_url: args.video_url,
|
|
33603
33941
|
video_path: args.video_path
|
|
33604
33942
|
});
|
|
33605
|
-
return client2.session.jobs.dispatch({ toolName: "gemini_image_generate", fingerprint, idempotencyKey: args.idempotency_key, async: args.async }, async () => {
|
|
33943
|
+
return client2.session.jobs.dispatch({ toolName: "gemini_image_generate", fingerprint, idempotencyKey: args.idempotency_key, async: args.async, waitMs: args.max_wait_ms }, async () => {
|
|
33606
33944
|
const seed = pickSeed(args.seed);
|
|
33607
33945
|
const slug = args.filename ? baseName(args.filename) : slugify2(args.prompt);
|
|
33608
|
-
const
|
|
33946
|
+
const charRefs = await resolveCharacterRefs(client2, args.characters, args.style);
|
|
33947
|
+
const prompt = composePrompt(args.prompt, charRefs);
|
|
33948
|
+
const { inputs: directInputs, report } = await withProgressHeartbeat(extra, "Resolving reference images", () => resolveImageInputs(args, client2));
|
|
33949
|
+
const refInputs = [...charRefs.inputs, ...directInputs];
|
|
33609
33950
|
const video = await withProgressHeartbeat(extra, "Uploading video to the Gemini Files API", () => resolveVideoInput(args, client2));
|
|
33610
33951
|
const named = [];
|
|
33611
33952
|
let capturedText;
|
|
33612
33953
|
let capturedGrounding;
|
|
33954
|
+
const failed = [];
|
|
33955
|
+
let firstError;
|
|
33613
33956
|
await withProgressHeartbeat(extra, `Generating ${count > 1 ? `${count} images` : "image"} (${model})`, async () => {
|
|
33614
33957
|
for (let i = 0; i < count; i++) {
|
|
33615
|
-
|
|
33616
|
-
|
|
33617
|
-
|
|
33618
|
-
|
|
33619
|
-
|
|
33620
|
-
|
|
33621
|
-
|
|
33622
|
-
|
|
33623
|
-
|
|
33624
|
-
|
|
33625
|
-
|
|
33626
|
-
|
|
33627
|
-
|
|
33628
|
-
|
|
33629
|
-
|
|
33630
|
-
|
|
33631
|
-
|
|
33958
|
+
try {
|
|
33959
|
+
const result = await client2.generate({
|
|
33960
|
+
prompt,
|
|
33961
|
+
images: refInputs.length > 0 ? refInputs : void 0,
|
|
33962
|
+
model: args.model,
|
|
33963
|
+
aspectRatio: args.aspect_ratio,
|
|
33964
|
+
imageSize: args.image_size,
|
|
33965
|
+
seed: seed + i,
|
|
33966
|
+
thinkingLevel: args.thinking_level,
|
|
33967
|
+
googleSearch: args.google_search,
|
|
33968
|
+
videoUrl: video.videoUrl,
|
|
33969
|
+
videoMimeType: video.videoMimeType,
|
|
33970
|
+
timeoutMs: args.timeout_ms
|
|
33971
|
+
});
|
|
33972
|
+
const { images: [img], text } = result;
|
|
33973
|
+
if (text && !capturedText) capturedText = text;
|
|
33974
|
+
if (result.grounding && !capturedGrounding) capturedGrounding = result.grounding;
|
|
33975
|
+
named.push({ image: img, base: count === 1 ? slug : `${slug}-${String(i + 1).padStart(2, "0")}` });
|
|
33976
|
+
} catch (err) {
|
|
33977
|
+
firstError ??= err;
|
|
33978
|
+
failed.push({ image: i + 1, error: err instanceof Error ? err.message : String(err) });
|
|
33979
|
+
}
|
|
33632
33980
|
}
|
|
33633
33981
|
});
|
|
33982
|
+
if (named.length === 0) throw firstError;
|
|
33634
33983
|
const meta3 = buildMeta(model, seed, args);
|
|
33635
33984
|
if (capturedText) meta3.text = capturedText;
|
|
33636
33985
|
if (capturedGrounding) meta3.grounding = capturedGrounding;
|
|
33637
33986
|
if (report) meta3.image_inputs = report;
|
|
33987
|
+
if (charRefs.meta) Object.assign(meta3, charRefs.meta);
|
|
33988
|
+
if (failed.length) {
|
|
33989
|
+
meta3.failed_images = failed;
|
|
33990
|
+
meta3.partial = `${failed.length} of ${count} image(s) failed; the ${named.length} that succeeded are returned above. Re-run with count set to the shortfall rather than the whole batch \u2014 each failure message is in failed_images.`;
|
|
33991
|
+
}
|
|
33638
33992
|
if (video.videoFileMeta) meta3.video_file = video.videoFileMeta;
|
|
33639
33993
|
meta3.hint = REFINE_HINT;
|
|
33640
33994
|
const risk = timeoutRiskHint({ model, imageSize: args.image_size, count, persistsFiles: client2.mediaSink?.persistsFiles });
|
|
@@ -33653,7 +34007,10 @@ function registerGenerateTools(server, client2) {
|
|
|
33653
34007
|
images: external_exports.array(external_exports.string().min(1)).optional().describe("Paths to input image file(s) (1 = edit, 2+ = compose)"),
|
|
33654
34008
|
images_url: imagesUrlSchema("Input images"),
|
|
33655
34009
|
images_file_uris: imagesFileUrisSchema("Input images"),
|
|
34010
|
+
images_r2_keys: imagesR2KeysSchema("Input images"),
|
|
33656
34011
|
images_base64: external_exports.array(external_exports.string().min(1)).optional().describe("Input images as base64 strings or data URIs. Last resort: prefer images_url or images_file_uris, which keep image bytes out of the conversation"),
|
|
34012
|
+
characters: charactersSchema,
|
|
34013
|
+
style: styleSchema,
|
|
33657
34014
|
filename: external_exports.string().optional().describe("Base filename for the output image (extension stripped; default: slugified prompt)"),
|
|
33658
34015
|
confirm: schemaConfirm,
|
|
33659
34016
|
...sharedImageSchema
|
|
@@ -33661,7 +34018,7 @@ function registerGenerateTools(server, client2) {
|
|
|
33661
34018
|
},
|
|
33662
34019
|
async (args, extra) => {
|
|
33663
34020
|
assertLocalInputsAvailable(client2.mediaSink, args);
|
|
33664
|
-
requireImageInput(args);
|
|
34021
|
+
if (!args.characters?.length) requireImageInput(args);
|
|
33665
34022
|
const gate = await previewLocalInputsUnlessConfirmed(args.confirm, SEND_LOCAL_ACTION, GENERATE_ENDPOINT, args.images);
|
|
33666
34023
|
if (gate) return gate;
|
|
33667
34024
|
const model = resolveModel(args.model, readEnvVar("GEMINI_IMAGE_MODEL"));
|
|
@@ -33676,14 +34033,20 @@ function registerGenerateTools(server, client2) {
|
|
|
33676
34033
|
images_base64: args.images_base64,
|
|
33677
34034
|
from_clipboard: args.from_clipboard,
|
|
33678
34035
|
images_url: args.images_url,
|
|
33679
|
-
images_file_uris: args.images_file_uris
|
|
34036
|
+
images_file_uris: args.images_file_uris,
|
|
34037
|
+
images_r2_keys: args.images_r2_keys,
|
|
34038
|
+
characters: args.characters,
|
|
34039
|
+
style: args.style
|
|
33680
34040
|
});
|
|
33681
|
-
return client2.session.jobs.dispatch({ toolName: "gemini_image_edit", fingerprint, idempotencyKey: args.idempotency_key, async: args.async }, async () => {
|
|
34041
|
+
return client2.session.jobs.dispatch({ toolName: "gemini_image_edit", fingerprint, idempotencyKey: args.idempotency_key, async: args.async, waitMs: args.max_wait_ms }, async () => {
|
|
33682
34042
|
const seed = pickSeed(args.seed);
|
|
33683
34043
|
const slug = args.filename ? baseName(args.filename) : slugify2(args.prompt);
|
|
33684
|
-
const
|
|
34044
|
+
const charRefs = await resolveCharacterRefs(client2, args.characters, args.style);
|
|
34045
|
+
const prompt = composePrompt(args.prompt, charRefs);
|
|
34046
|
+
const { inputs: directInputs, report } = await withProgressHeartbeat(extra, "Resolving input images", () => resolveImageInputs(args, client2));
|
|
34047
|
+
const inputs = [...charRefs.inputs, ...directInputs];
|
|
33685
34048
|
const result = await withProgressHeartbeat(extra, `Editing image (${model})`, () => client2.generate({
|
|
33686
|
-
prompt
|
|
34049
|
+
prompt,
|
|
33687
34050
|
images: inputs,
|
|
33688
34051
|
model: args.model,
|
|
33689
34052
|
aspectRatio: args.aspect_ratio,
|
|
@@ -33698,6 +34061,7 @@ function registerGenerateTools(server, client2) {
|
|
|
33698
34061
|
if (text) meta3.text = text;
|
|
33699
34062
|
if (result.grounding) meta3.grounding = result.grounding;
|
|
33700
34063
|
if (report) meta3.image_inputs = report;
|
|
34064
|
+
if (charRefs.meta) Object.assign(meta3, charRefs.meta);
|
|
33701
34065
|
meta3.hint = REFINE_HINT;
|
|
33702
34066
|
const risk = timeoutRiskHint({ model, imageSize: args.image_size, persistsFiles: client2.mediaSink?.persistsFiles });
|
|
33703
34067
|
if (risk) meta3.timeout_risk = risk;
|
|
@@ -33715,7 +34079,7 @@ function registerSetTools(server, client2) {
|
|
|
33715
34079
|
server.registerTool(
|
|
33716
34080
|
"gemini_image_set",
|
|
33717
34081
|
{
|
|
33718
|
-
description:
|
|
34082
|
+
description: 'Generate a consistent SET of images: a master image from master_prompt, then one image per scene that references the master so the subject/style stays consistent. Provide `scenes` (explicit per-image prompts) OR `count` (variations of the master). Scene generations run in parallel (reference_mode "master", the default). On the hosted connector: saved `characters` and a saved `style` can seed the whole set by name, multi-image results include a `bundle_url` zip of every image (one curl instead of N), and `max_wait_ms` returns a pollable job handle if the batch runs long.',
|
|
33719
34083
|
annotations: { readOnlyHint: false, openWorldHint: true },
|
|
33720
34084
|
inputSchema: {
|
|
33721
34085
|
master_prompt: external_exports.string().min(1).describe("Prompt for the master/reference image"),
|
|
@@ -33726,6 +34090,9 @@ function registerSetTools(server, client2) {
|
|
|
33726
34090
|
master_images: external_exports.array(external_exports.string().min(1)).optional().describe("Reference image paths passed to the master generation call"),
|
|
33727
34091
|
master_images_url: imagesUrlSchema("Reference images passed to the master AND to every scene call (fetched once)"),
|
|
33728
34092
|
master_images_file_uris: imagesFileUrisSchema("Reference images passed to the master AND to every scene call"),
|
|
34093
|
+
master_images_r2_keys: imagesR2KeysSchema("Reference images passed to the master AND to every scene call"),
|
|
34094
|
+
characters: charactersSchema,
|
|
34095
|
+
style: styleSchema,
|
|
33729
34096
|
master_images_base64: external_exports.array(external_exports.string().min(1)).optional().describe("Reference images as base64 strings or data URIs for master generation. Last resort: prefer master_images_url or master_images_file_uris, which keep image bytes out of the conversation"),
|
|
33730
34097
|
confirm: schemaConfirm,
|
|
33731
34098
|
...sharedImageSchema
|
|
@@ -33753,26 +34120,34 @@ function registerSetTools(server, client2) {
|
|
|
33753
34120
|
master_images_base64: args.master_images_base64,
|
|
33754
34121
|
from_clipboard: args.from_clipboard,
|
|
33755
34122
|
master_images_url: args.master_images_url,
|
|
33756
|
-
master_images_file_uris: args.master_images_file_uris
|
|
34123
|
+
master_images_file_uris: args.master_images_file_uris,
|
|
34124
|
+
master_images_r2_keys: args.master_images_r2_keys,
|
|
34125
|
+
characters: args.characters,
|
|
34126
|
+
style: args.style
|
|
33757
34127
|
});
|
|
33758
|
-
return client2.session.jobs.dispatch({ toolName: "gemini_image_set", fingerprint, idempotencyKey: args.idempotency_key, async: args.async }, async () => {
|
|
34128
|
+
return client2.session.jobs.dispatch({ toolName: "gemini_image_set", fingerprint, idempotencyKey: args.idempotency_key, async: args.async, waitMs: args.max_wait_ms }, async () => {
|
|
33759
34129
|
const seed = pickSeed(args.seed);
|
|
33760
34130
|
const cfg = { model: args.model, aspectRatio: args.aspect_ratio, imageSize: args.image_size, thinkingLevel: args.thinking_level, googleSearch: args.google_search, timeoutMs: args.timeout_ms };
|
|
33761
34131
|
const slug = args.basename ? baseName(args.basename) : slugify2(args.master_prompt);
|
|
34132
|
+
const charRefs = await resolveCharacterRefs(client2, args.characters, args.style);
|
|
34133
|
+
const masterPrompt = composePrompt(args.master_prompt, charRefs);
|
|
34134
|
+
const styleOnly = { styleFragment: charRefs.styleFragment };
|
|
33762
34135
|
const carried = await resolveImageInputs(
|
|
33763
|
-
{ images_url: args.master_images_url, images_file_uris: args.master_images_file_uris },
|
|
34136
|
+
{ images_url: args.master_images_url, images_file_uris: args.master_images_file_uris, images_r2_keys: args.master_images_r2_keys },
|
|
33764
34137
|
client2
|
|
33765
34138
|
);
|
|
34139
|
+
const carriedRefs = [...charRefs.inputs, ...carried.inputs];
|
|
33766
34140
|
const masterOnly = await resolveImageInputs(
|
|
33767
34141
|
{ images: args.master_images, images_base64: args.master_images_base64, from_clipboard: args.from_clipboard },
|
|
33768
34142
|
client2
|
|
33769
34143
|
);
|
|
33770
|
-
const masterRefInputs = [...masterOnly.inputs, ...carried.inputs];
|
|
34144
|
+
const masterRefInputs = [...charRefs.inputs, ...masterOnly.inputs, ...carried.inputs];
|
|
34145
|
+
const failed = [];
|
|
33771
34146
|
const report = carried.report || masterOnly.report ? { ...masterOnly.report, ...carried.report } : void 0;
|
|
33772
34147
|
const named = [];
|
|
33773
34148
|
const masterResult = await withProgressHeartbeat(extra, `Generating image set (${model})`, async () => {
|
|
33774
34149
|
const result = await client2.generate({
|
|
33775
|
-
prompt:
|
|
34150
|
+
prompt: masterPrompt,
|
|
33776
34151
|
images: masterRefInputs.length > 0 ? masterRefInputs : void 0,
|
|
33777
34152
|
seed,
|
|
33778
34153
|
...cfg
|
|
@@ -33781,18 +34156,33 @@ function registerSetTools(server, client2) {
|
|
|
33781
34156
|
named.push({ image: master, base: `${slug}-master` });
|
|
33782
34157
|
const scenePrompts = args.scenes ?? Array.from({ length: args.count ?? 0 }, () => args.master_prompt);
|
|
33783
34158
|
const mode = args.reference_mode ?? "master";
|
|
34159
|
+
const sceneName = (i) => `${slug}-${String(i + 1).padStart(2, "0")}`;
|
|
34160
|
+
const record2 = (err, i) => {
|
|
34161
|
+
failed.push({
|
|
34162
|
+
scene: i + 1,
|
|
34163
|
+
prompt: scenePrompts[i],
|
|
34164
|
+
error: err instanceof Error ? err.message : String(err)
|
|
34165
|
+
});
|
|
34166
|
+
};
|
|
33784
34167
|
if (mode === "chain") {
|
|
33785
34168
|
let ref = master;
|
|
33786
34169
|
for (let i = 0; i < scenePrompts.length; i++) {
|
|
33787
|
-
|
|
33788
|
-
|
|
33789
|
-
|
|
34170
|
+
try {
|
|
34171
|
+
const { images: [img] } = await client2.generate({ prompt: composePrompt(scenePrompts[i], styleOnly), images: [ref, ...carriedRefs], seed: seed + i + 1, ...cfg });
|
|
34172
|
+
named.push({ image: img, base: sceneName(i) });
|
|
34173
|
+
ref = img;
|
|
34174
|
+
} catch (err) {
|
|
34175
|
+
record2(err, i);
|
|
34176
|
+
}
|
|
33790
34177
|
}
|
|
33791
34178
|
} else {
|
|
33792
|
-
const
|
|
33793
|
-
scenePrompts.map((p, i) => client2.generate({ prompt: p, images: [master, ...
|
|
34179
|
+
const settled = await Promise.allSettled(
|
|
34180
|
+
scenePrompts.map((p, i) => client2.generate({ prompt: composePrompt(p, styleOnly), images: [master, ...carriedRefs], seed: seed + i + 1, ...cfg }).then((r) => r.images[0]))
|
|
33794
34181
|
);
|
|
33795
|
-
|
|
34182
|
+
settled.forEach((outcome, i) => {
|
|
34183
|
+
if (outcome.status === "fulfilled") named.push({ image: outcome.value, base: sceneName(i) });
|
|
34184
|
+
else record2(outcome.reason, i);
|
|
34185
|
+
});
|
|
33796
34186
|
}
|
|
33797
34187
|
return result;
|
|
33798
34188
|
});
|
|
@@ -33801,9 +34191,15 @@ function registerSetTools(server, client2) {
|
|
|
33801
34191
|
if (masterText) meta3.text = masterText;
|
|
33802
34192
|
if (masterResult.grounding) meta3.grounding = masterResult.grounding;
|
|
33803
34193
|
if (report) meta3.image_inputs = report;
|
|
34194
|
+
if (charRefs.meta) Object.assign(meta3, charRefs.meta);
|
|
34195
|
+
if (failed.length) {
|
|
34196
|
+
meta3.failed_scenes = failed;
|
|
34197
|
+
meta3.partial = `${failed.length} of ${failed.length + named.length - 1} scene(s) failed; the master and the ${named.length - 1} scene(s) that succeeded are returned above. Re-run just the failed prompts rather than the whole set \u2014 each failure message is in failed_scenes.`;
|
|
34198
|
+
}
|
|
33804
34199
|
const risk = timeoutRiskHint({ model, imageSize: args.image_size, count: named.length, persistsFiles: client2.mediaSink?.persistsFiles });
|
|
33805
34200
|
if (risk) meta3.timeout_risk = risk;
|
|
33806
|
-
|
|
34201
|
+
if (!args.inline) Object.assign(meta3, await persistBundle(named, client2.mediaSink, slug));
|
|
34202
|
+
return emit(named, { ...args, sink: client2.mediaSink, urlTtlMs: SET_URL_TTL_MS }, meta3);
|
|
33807
34203
|
});
|
|
33808
34204
|
}
|
|
33809
34205
|
);
|
|
@@ -33968,8 +34364,15 @@ var GeminiClient = class {
|
|
|
33968
34364
|
* server is single-user, so its one singleton client is one session.
|
|
33969
34365
|
*/
|
|
33970
34366
|
session = new SessionState();
|
|
34367
|
+
/** Per-account character/style library — hosted connector only (see options). */
|
|
34368
|
+
library;
|
|
34369
|
+
/** Signed upload-URL minter — hosted connector only (see options). */
|
|
34370
|
+
uploadUrls;
|
|
33971
34371
|
constructor(opts = {}) {
|
|
33972
34372
|
this.mediaSink = opts.mediaSink ?? createDiskSink();
|
|
34373
|
+
this.library = opts.library;
|
|
34374
|
+
this.uploadUrls = opts.uploadUrls;
|
|
34375
|
+
this.session.jobs.resigner = this.mediaSink;
|
|
33973
34376
|
this.explicitApiKey = opts.apiKey?.trim() || void 0;
|
|
33974
34377
|
this.fetchImpl = opts.fetchImpl ?? fetch;
|
|
33975
34378
|
this.sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
@@ -34039,6 +34442,22 @@ var GeminiClient = class {
|
|
|
34039
34442
|
async fetchRemoteImage(url2, opts = {}) {
|
|
34040
34443
|
return fetchRemoteImage(url2, { ...opts, fetchImpl: this.fetchImpl, timeoutMs: resolveTimeoutMs(opts.timeoutMs) });
|
|
34041
34444
|
}
|
|
34445
|
+
/**
|
|
34446
|
+
* Read back media this connector generated, straight out of its own store.
|
|
34447
|
+
*
|
|
34448
|
+
* The alternative — fetching our own `/media` URL — needs a signature the
|
|
34449
|
+
* caller cannot mint and returns 403, which is the loop this exists to close.
|
|
34450
|
+
*/
|
|
34451
|
+
async readStoredMedia(key) {
|
|
34452
|
+
const stored = await this.mediaSink.read?.(key.trim());
|
|
34453
|
+
if (!stored) {
|
|
34454
|
+
throw new McpToolError(
|
|
34455
|
+
`No stored media for r2_key "${key}" \u2014 objects are removed on a retention schedule, so an old key may be gone. Re-run the generation, or upload the bytes with \`url\` / \`data_base64\`.`,
|
|
34456
|
+
{ hint: "Use the exact media[].r2_key from a generation result." }
|
|
34457
|
+
);
|
|
34458
|
+
}
|
|
34459
|
+
return stored;
|
|
34460
|
+
}
|
|
34042
34461
|
/** The default model after env override (no per-call arg). */
|
|
34043
34462
|
defaultModel() {
|
|
34044
34463
|
return resolveModel(void 0, readEnvVar("GEMINI_IMAGE_MODEL"));
|
|
@@ -34552,6 +34971,7 @@ function registerInteractTools(server, client2) {
|
|
|
34552
34971
|
timeout_ms: timeoutMsSchema,
|
|
34553
34972
|
idempotency_key: idempotencyKeySchema,
|
|
34554
34973
|
async: asyncSchema,
|
|
34974
|
+
max_wait_ms: maxWaitMsSchema,
|
|
34555
34975
|
from_clipboard: external_exports.boolean().optional().describe("Use the image currently on the macOS system clipboard as an input (downscaled to JPEG)"),
|
|
34556
34976
|
confirm: schemaConfirm
|
|
34557
34977
|
}
|
|
@@ -34598,7 +35018,7 @@ function registerInteractTools(server, client2) {
|
|
|
34598
35018
|
video_url: args.video_url,
|
|
34599
35019
|
video_path: args.video_path
|
|
34600
35020
|
});
|
|
34601
|
-
return client2.session.jobs.dispatch({ toolName: "gemini_interact", fingerprint, idempotencyKey: args.idempotency_key, async: args.async }, async () => {
|
|
35021
|
+
return client2.session.jobs.dispatch({ toolName: "gemini_interact", fingerprint, idempotencyKey: args.idempotency_key, async: args.async, waitMs: args.max_wait_ms }, async () => {
|
|
34602
35022
|
const { inputs, report } = await resolveImageInputs({ ...args, images }, client2);
|
|
34603
35023
|
const video = await withProgressHeartbeat(extra, "Uploading video to the Gemini Files API", () => resolveVideoInput(args, client2));
|
|
34604
35024
|
const callOpts = {
|
|
@@ -34702,6 +35122,7 @@ function registerVideoTools(server, client2) {
|
|
|
34702
35122
|
timeout_ms: timeoutMsSchema,
|
|
34703
35123
|
idempotency_key: idempotencyKeySchema,
|
|
34704
35124
|
async: asyncSchema,
|
|
35125
|
+
max_wait_ms: maxWaitMsSchema,
|
|
34705
35126
|
confirm: schemaConfirm
|
|
34706
35127
|
}
|
|
34707
35128
|
},
|
|
@@ -34731,7 +35152,7 @@ function registerVideoTools(server, client2) {
|
|
|
34731
35152
|
images_file_uris: args.images_file_uris,
|
|
34732
35153
|
previous_interaction_id: previousInteractionId
|
|
34733
35154
|
});
|
|
34734
|
-
return client2.session.jobs.dispatch({ toolName: "gemini_video_generate", fingerprint, idempotencyKey: args.idempotency_key, async: args.async }, async () => {
|
|
35155
|
+
return client2.session.jobs.dispatch({ toolName: "gemini_video_generate", fingerprint, idempotencyKey: args.idempotency_key, async: args.async, waitMs: args.max_wait_ms }, async () => {
|
|
34735
35156
|
const { inputs, report } = await resolveImageInputs(args, client2);
|
|
34736
35157
|
const r = await withProgressHeartbeat(extra, `Generating video (${model})`, () => client2.generateVideo({
|
|
34737
35158
|
input: args.prompt,
|
|
@@ -34791,6 +35212,7 @@ function registerMusicTools(server, client2) {
|
|
|
34791
35212
|
timeout_ms: timeoutMsSchema,
|
|
34792
35213
|
idempotency_key: idempotencyKeySchema,
|
|
34793
35214
|
async: asyncSchema,
|
|
35215
|
+
max_wait_ms: maxWaitMsSchema,
|
|
34794
35216
|
confirm: schemaConfirm
|
|
34795
35217
|
}
|
|
34796
35218
|
},
|
|
@@ -34824,7 +35246,7 @@ function registerMusicTools(server, client2) {
|
|
|
34824
35246
|
images_file_uris: args.images_file_uris,
|
|
34825
35247
|
previous_interaction_id: previousInteractionId
|
|
34826
35248
|
});
|
|
34827
|
-
return client2.session.jobs.dispatch({ toolName: "gemini_music_generate", fingerprint, idempotencyKey: args.idempotency_key, async: args.async }, async () => {
|
|
35249
|
+
return client2.session.jobs.dispatch({ toolName: "gemini_music_generate", fingerprint, idempotencyKey: args.idempotency_key, async: args.async, waitMs: args.max_wait_ms }, async () => {
|
|
34828
35250
|
const { inputs, report } = await resolveImageInputs(args, client2);
|
|
34829
35251
|
const r = await withProgressHeartbeat(extra, `Generating music (${model})`, () => client2.generateMusic({
|
|
34830
35252
|
input: args.prompt,
|
|
@@ -34894,13 +35316,16 @@ function registerFileTools(server, client2) {
|
|
|
34894
35316
|
server.registerTool(
|
|
34895
35317
|
"gemini_upload_file",
|
|
34896
35318
|
{
|
|
34897
|
-
description: "Upload a file \u2014 an image, reference photo, picture, screenshot, video or audio clip \u2014 to the Gemini Files API ONCE, and get back a reusable `file_uri` (`files/<id>`) to attach to later image, video or music generations. Keywords: upload, upload file, upload image, upload photo, attach, reference image, reference photo, file_uri, files api, image reference, reuse across calls. Use this instead of pasting base64 into a tool call: the reference is a short string, so no image bytes ever enter the conversation, and it can be reused across many generations until it expires (~48h). Provide exactly one of `url` (the server downloads it), `data_base64`" + (onDisk ? ", or `path` (a local file)." : ".") + (onDisk ? "" : " This connector also accepts raw bytes over HTTP: POST them to /upload with the same Authorization header and a Content-Type of image/*, which avoids base64 entirely."),
|
|
35319
|
+
description: "Upload a file \u2014 an image, reference photo, picture, screenshot, video or audio clip \u2014 to the Gemini Files API ONCE, and get back a reusable `file_uri` (`files/<id>`) to attach to later image, video or music generations. Keywords: upload, upload file, upload image, upload photo, attach, reference image, reference photo, file_uri, files api, image reference, reuse across calls. Use this instead of pasting base64 into a tool call: the reference is a short string, so no image bytes ever enter the conversation, and it can be reused across many generations until it expires (~48h). Provide exactly one of `url` (the server downloads it), `data_base64`" + (onDisk ? ", or `path` (a local file)." : ", or `r2_key` (re-upload media this connector generated, from media[].r2_key in an earlier result).") + (onDisk ? "" : " This connector also accepts raw bytes over HTTP: POST them to /upload with the same Authorization header and a Content-Type of image/*, which avoids base64 entirely."),
|
|
34898
35320
|
annotations: { readOnlyHint: false, openWorldHint: true },
|
|
34899
35321
|
inputSchema: {
|
|
34900
35322
|
url: external_exports.string().url().optional().describe(
|
|
34901
35323
|
`Public https URL the SERVER downloads and uploads (image/video/audio, up to ${wholeMb(urlMaxBytes)}MB). No bytes pass through the conversation.` + (onDisk ? "" : " Larger files: POST the raw bytes to /upload, which streams instead of buffering.")
|
|
34902
35324
|
),
|
|
34903
35325
|
data_base64: external_exports.string().min(1).optional().describe("Raw base64 or a data: URI. Last resort \u2014 this is the one form that costs model context (~14k tokens for a modest JPEG)."),
|
|
35326
|
+
r2_key: external_exports.string().min(1).optional().describe(
|
|
35327
|
+
onDisk ? "Unavailable on this server (media is written to local disk) \u2014 pass the file path via `path` instead." : "An `r2_key` from a previous generation result (media[].r2_key) \u2014 re-uploads media THIS connector generated, so a generated image can become a reference image in one cheap call. The server reads its own bucket directly: no HTTP request, no signed URL, and no expiry to worry about. Do NOT pass a /media URL as `url` for this \u2014 that would be the connector fetching itself and needs a signature."
|
|
35328
|
+
),
|
|
34904
35329
|
path: external_exports.string().min(1).optional().describe(
|
|
34905
35330
|
onDisk ? "Path to a local file (absolute, or resolved against $GEMINI_INPUT_DIR). Confirm-gated like every other local-file input." : "Unavailable on this hosted connector (no filesystem) \u2014 use `url`, `data_base64`, or POST /upload."
|
|
34906
35331
|
),
|
|
@@ -34910,10 +35335,10 @@ function registerFileTools(server, client2) {
|
|
|
34910
35335
|
}
|
|
34911
35336
|
},
|
|
34912
35337
|
async (args, extra) => {
|
|
34913
|
-
const sources = [args.url && "url", args.data_base64 && "data_base64", args.path && "path"].filter(Boolean);
|
|
35338
|
+
const sources = [args.url && "url", args.data_base64 && "data_base64", args.r2_key && "r2_key", args.path && "path"].filter(Boolean);
|
|
34914
35339
|
if (sources.length !== 1) {
|
|
34915
35340
|
throw new McpToolError(
|
|
34916
|
-
sources.length === 0 ? "Provide exactly one of `url`, `data_base64`" + (onDisk ? " or `path`." : "
|
|
35341
|
+
sources.length === 0 ? "Provide exactly one of `url`, `data_base64`" + (onDisk ? " or `path`." : " or `r2_key`.") : `Provide exactly one source, not ${sources.length} (${sources.join(", ")}).`
|
|
34917
35342
|
);
|
|
34918
35343
|
}
|
|
34919
35344
|
if (args.path) {
|
|
@@ -34942,6 +35367,19 @@ function registerFileTools(server, client2) {
|
|
|
34942
35367
|
args.display_name ?? fileNameFromUrl(args.url, "upload")
|
|
34943
35368
|
);
|
|
34944
35369
|
}
|
|
35370
|
+
if (args.r2_key) {
|
|
35371
|
+
if (onDisk) {
|
|
35372
|
+
throw new McpToolError(
|
|
35373
|
+
"`r2_key` is only available on the hosted connector \u2014 this server writes media to local disk, so pass the generated file via `path` instead."
|
|
35374
|
+
);
|
|
35375
|
+
}
|
|
35376
|
+
const stored = await client2.readStoredMedia(args.r2_key);
|
|
35377
|
+
return client2.uploadBytes(
|
|
35378
|
+
stored.bytes,
|
|
35379
|
+
args.mime_type ?? stored.mimeType,
|
|
35380
|
+
args.display_name ?? downloadFilename(args.r2_key)
|
|
35381
|
+
);
|
|
35382
|
+
}
|
|
34945
35383
|
if (args.data_base64) {
|
|
34946
35384
|
const { decodeImageInput: decodeImageInput2 } = await Promise.resolve().then(() => (init_images(), images_exports));
|
|
34947
35385
|
const decoded = decodeImageInput2(args.data_base64);
|
|
@@ -34988,6 +35426,40 @@ function registerFileTools(server, client2) {
|
|
|
34988
35426
|
});
|
|
34989
35427
|
}
|
|
34990
35428
|
);
|
|
35429
|
+
if (!onDisk) {
|
|
35430
|
+
server.registerTool(
|
|
35431
|
+
"gemini_sign_media",
|
|
35432
|
+
{
|
|
35433
|
+
description: "Mint a FRESH signed URL for media this connector generated, from its `r2_key`. Signed media links expire well before the object behind them is cleaned up, so an expired link is not a dead end \u2014 re-sign it instead of re-generating (and re-paying for) the image. Keywords: expired link, refresh url, re-sign, media url, download again.",
|
|
35434
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
35435
|
+
inputSchema: {
|
|
35436
|
+
r2_key: external_exports.string().min(1).describe("The `media[].r2_key` from an earlier generation result")
|
|
35437
|
+
}
|
|
35438
|
+
},
|
|
35439
|
+
async (args) => {
|
|
35440
|
+
const sink = client2.mediaSink;
|
|
35441
|
+
if (!sink?.resign) {
|
|
35442
|
+
throw new McpToolError("This server does not serve media URLs \u2014 generated files are written to disk and the path in the result is already usable.", {
|
|
35443
|
+
hint: "gemini_sign_media applies to the hosted connector only."
|
|
35444
|
+
});
|
|
35445
|
+
}
|
|
35446
|
+
const fresh = await sink.resign(args.r2_key.trim());
|
|
35447
|
+
if (!fresh) {
|
|
35448
|
+
throw new McpToolError(
|
|
35449
|
+
`No stored media for r2_key "${args.r2_key}" \u2014 it has probably passed its retention window. Re-run the generation to get a new one.`,
|
|
35450
|
+
{ hint: "Objects are swept on a retention schedule (MEDIA_TTL_DAYS, default 7 days)." }
|
|
35451
|
+
);
|
|
35452
|
+
}
|
|
35453
|
+
return textResult({
|
|
35454
|
+
url: fresh.ref,
|
|
35455
|
+
r2_key: fresh.key,
|
|
35456
|
+
...fresh.expiresAt ? { expires_at: fresh.expiresAt } : {},
|
|
35457
|
+
curl_hint: `curl -sS -o ${downloadFilename(fresh.key ?? "media")} "${fresh.ref}"`,
|
|
35458
|
+
hint: "In a Claude chat client, download this URL into your sandbox and present it as an output file \u2014 inline image blocks and markdown embeds do not render for the end user."
|
|
35459
|
+
});
|
|
35460
|
+
}
|
|
35461
|
+
);
|
|
35462
|
+
}
|
|
34991
35463
|
server.registerTool(
|
|
34992
35464
|
"gemini_delete_file",
|
|
34993
35465
|
{
|