@chrischall/gemini-mcp 1.0.4 → 1.2.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 +75 -1
- package/dist/bundle.js +854 -137
- package/dist/bytes.js +46 -0
- package/dist/client.js +194 -17
- package/dist/fetch-image.js +361 -0
- package/dist/images.js +0 -19
- package/dist/index.js +2 -1
- package/dist/inputs.js +165 -0
- package/dist/session.js +39 -0
- package/dist/storage/media.js +2 -13
- package/dist/tools/files.js +188 -0
- package/dist/tools/generate.js +21 -12
- package/dist/tools/interact.js +12 -5
- package/dist/tools/music.js +10 -4
- package/dist/tools/set.js +29 -7
- package/dist/tools/shared.js +44 -6
- package/dist/tools/video.js +10 -4
- package/dist/upload-endpoint.js +79 -0
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/server.json +2 -2
- package/skills/gemini-mcp/SKILL.md +65 -6
package/dist/bundle.js
CHANGED
|
@@ -1109,11 +1109,11 @@ var init_types = __esm({
|
|
|
1109
1109
|
status.dirty();
|
|
1110
1110
|
}
|
|
1111
1111
|
} else if (check2.kind === "length") {
|
|
1112
|
-
const
|
|
1112
|
+
const tooBig2 = input.data.length > check2.value;
|
|
1113
1113
|
const tooSmall = input.data.length < check2.value;
|
|
1114
|
-
if (
|
|
1114
|
+
if (tooBig2 || tooSmall) {
|
|
1115
1115
|
ctx = this._getOrReturnCtx(input, ctx);
|
|
1116
|
-
if (
|
|
1116
|
+
if (tooBig2) {
|
|
1117
1117
|
addIssueToContext(ctx, {
|
|
1118
1118
|
code: ZodIssueCode.too_big,
|
|
1119
1119
|
maximum: check2.value,
|
|
@@ -1660,8 +1660,8 @@ var init_types = __esm({
|
|
|
1660
1660
|
status.dirty();
|
|
1661
1661
|
}
|
|
1662
1662
|
} else if (check2.kind === "max") {
|
|
1663
|
-
const
|
|
1664
|
-
if (
|
|
1663
|
+
const tooBig2 = check2.inclusive ? input.data > check2.value : input.data >= check2.value;
|
|
1664
|
+
if (tooBig2) {
|
|
1665
1665
|
ctx = this._getOrReturnCtx(input, ctx);
|
|
1666
1666
|
addIssueToContext(ctx, {
|
|
1667
1667
|
code: ZodIssueCode.too_big,
|
|
@@ -1877,8 +1877,8 @@ var init_types = __esm({
|
|
|
1877
1877
|
status.dirty();
|
|
1878
1878
|
}
|
|
1879
1879
|
} else if (check2.kind === "max") {
|
|
1880
|
-
const
|
|
1881
|
-
if (
|
|
1880
|
+
const tooBig2 = check2.inclusive ? input.data > check2.value : input.data >= check2.value;
|
|
1881
|
+
if (tooBig2) {
|
|
1882
1882
|
ctx = this._getOrReturnCtx(input, ctx);
|
|
1883
1883
|
addIssueToContext(ctx, {
|
|
1884
1884
|
code: ZodIssueCode.too_big,
|
|
@@ -2292,13 +2292,13 @@ var init_types = __esm({
|
|
|
2292
2292
|
return INVALID;
|
|
2293
2293
|
}
|
|
2294
2294
|
if (def.exactLength !== null) {
|
|
2295
|
-
const
|
|
2295
|
+
const tooBig2 = ctx.data.length > def.exactLength.value;
|
|
2296
2296
|
const tooSmall = ctx.data.length < def.exactLength.value;
|
|
2297
|
-
if (
|
|
2297
|
+
if (tooBig2 || tooSmall) {
|
|
2298
2298
|
addIssueToContext(ctx, {
|
|
2299
|
-
code:
|
|
2299
|
+
code: tooBig2 ? ZodIssueCode.too_big : ZodIssueCode.too_small,
|
|
2300
2300
|
minimum: tooSmall ? def.exactLength.value : void 0,
|
|
2301
|
-
maximum:
|
|
2301
|
+
maximum: tooBig2 ? def.exactLength.value : void 0,
|
|
2302
2302
|
type: "array",
|
|
2303
2303
|
inclusive: true,
|
|
2304
2304
|
exact: true,
|
|
@@ -5462,10 +5462,10 @@ var init_checks = __esm({
|
|
|
5462
5462
|
const size = input.size;
|
|
5463
5463
|
if (size === def.size)
|
|
5464
5464
|
return;
|
|
5465
|
-
const
|
|
5465
|
+
const tooBig2 = size > def.size;
|
|
5466
5466
|
payload.issues.push({
|
|
5467
5467
|
origin: getSizableOrigin(input),
|
|
5468
|
-
...
|
|
5468
|
+
...tooBig2 ? { code: "too_big", maximum: def.size } : { code: "too_small", minimum: def.size },
|
|
5469
5469
|
inclusive: true,
|
|
5470
5470
|
exact: true,
|
|
5471
5471
|
input: payload.value,
|
|
@@ -5551,10 +5551,10 @@ var init_checks = __esm({
|
|
|
5551
5551
|
if (length === def.length)
|
|
5552
5552
|
return;
|
|
5553
5553
|
const origin = getLengthableOrigin(input);
|
|
5554
|
-
const
|
|
5554
|
+
const tooBig2 = length > def.length;
|
|
5555
5555
|
payload.issues.push({
|
|
5556
5556
|
origin,
|
|
5557
|
-
...
|
|
5557
|
+
...tooBig2 ? { code: "too_big", maximum: def.length } : { code: "too_small", minimum: def.length },
|
|
5558
5558
|
inclusive: true,
|
|
5559
5559
|
exact: true,
|
|
5560
5560
|
input: payload.value,
|
|
@@ -25669,11 +25669,11 @@ var require_resolve = __commonJS({
|
|
|
25669
25669
|
return id ? id.replace(TRAILING_SLASH_HASH, "") : "";
|
|
25670
25670
|
}
|
|
25671
25671
|
exports.normalizeId = normalizeId;
|
|
25672
|
-
function
|
|
25672
|
+
function resolveUrl2(resolver, baseId, id) {
|
|
25673
25673
|
id = normalizeId(id);
|
|
25674
25674
|
return resolver.resolve(baseId, id);
|
|
25675
25675
|
}
|
|
25676
|
-
exports.resolveUrl =
|
|
25676
|
+
exports.resolveUrl = resolveUrl2;
|
|
25677
25677
|
var ANCHOR = /^[a-z_][-a-z0-9._]*$/i;
|
|
25678
25678
|
function getSchemaRefs(schema, baseId) {
|
|
25679
25679
|
if (typeof schema == "boolean")
|
|
@@ -32548,73 +32548,11 @@ var init_dist = __esm({
|
|
|
32548
32548
|
}
|
|
32549
32549
|
});
|
|
32550
32550
|
|
|
32551
|
-
// src/clipboard.ts
|
|
32552
|
-
var clipboard_exports = {};
|
|
32553
|
-
__export(clipboard_exports, {
|
|
32554
|
-
readClipboardImage: () => readClipboardImage
|
|
32555
|
-
});
|
|
32556
|
-
import { execFile } from "node:child_process";
|
|
32557
|
-
import { promisify } from "node:util";
|
|
32558
|
-
import { readFile, unlink } from "node:fs/promises";
|
|
32559
|
-
import { tmpdir } from "node:os";
|
|
32560
|
-
import { join as join2 } from "node:path";
|
|
32561
|
-
async function readClipboardImage(run = defaultRun) {
|
|
32562
|
-
if (process.platform !== "darwin") {
|
|
32563
|
-
throw new McpToolError("from_clipboard is only supported on macOS", {
|
|
32564
|
-
hint: "Save the image to a file and pass it via `images` instead."
|
|
32565
|
-
});
|
|
32566
|
-
}
|
|
32567
|
-
const id = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
32568
|
-
const png = join2(tmpdir(), `gemini-clip-${id}.png`);
|
|
32569
|
-
const jpg = join2(tmpdir(), `gemini-clip-${id}.jpg`);
|
|
32570
|
-
try {
|
|
32571
|
-
try {
|
|
32572
|
-
await run("osascript", [
|
|
32573
|
-
"-e",
|
|
32574
|
-
`set f to (open for access POSIX file "${png}" with write permission)`,
|
|
32575
|
-
"-e",
|
|
32576
|
-
"write (the clipboard as \xABclass PNGf\xBB) to f",
|
|
32577
|
-
"-e",
|
|
32578
|
-
"close access f"
|
|
32579
|
-
]);
|
|
32580
|
-
} catch {
|
|
32581
|
-
throw new McpToolError("No image found on the clipboard", {
|
|
32582
|
-
hint: "Copy an image (\u2318C) first, or pass `images`/`images_base64`."
|
|
32583
|
-
});
|
|
32584
|
-
}
|
|
32585
|
-
try {
|
|
32586
|
-
await run("sips", ["-Z", "2048", "-s", "format", "jpeg", png, "--out", jpg]);
|
|
32587
|
-
} catch {
|
|
32588
|
-
throw new McpToolError("Failed to convert the clipboard image", {
|
|
32589
|
-
hint: "The clipboard may contain a non-standard image format."
|
|
32590
|
-
});
|
|
32591
|
-
}
|
|
32592
|
-
const buf = await readFile(jpg);
|
|
32593
|
-
return { base64: buf.toString("base64"), mimeType: "image/jpeg" };
|
|
32594
|
-
} finally {
|
|
32595
|
-
await unlink(png).catch(() => void 0);
|
|
32596
|
-
await unlink(jpg).catch(() => void 0);
|
|
32597
|
-
}
|
|
32598
|
-
}
|
|
32599
|
-
var execFileAsync, defaultRun;
|
|
32600
|
-
var init_clipboard = __esm({
|
|
32601
|
-
"src/clipboard.ts"() {
|
|
32602
|
-
"use strict";
|
|
32603
|
-
init_dist();
|
|
32604
|
-
execFileAsync = promisify(execFile);
|
|
32605
|
-
defaultRun = async (cmd, args) => {
|
|
32606
|
-
await execFileAsync(cmd, args);
|
|
32607
|
-
};
|
|
32608
|
-
}
|
|
32609
|
-
});
|
|
32610
|
-
|
|
32611
32551
|
// src/images.ts
|
|
32612
32552
|
var images_exports = {};
|
|
32613
32553
|
__export(images_exports, {
|
|
32614
32554
|
baseName: () => baseName,
|
|
32615
32555
|
decodeImageInput: () => decodeImageInput,
|
|
32616
|
-
gatherImageInputs: () => gatherImageInputs,
|
|
32617
|
-
loadImageInputs: () => loadImageInputs,
|
|
32618
32556
|
mediaExt: () => mediaExt,
|
|
32619
32557
|
previewImageInput: () => previewImageInput,
|
|
32620
32558
|
previewVideoInput: () => previewVideoInput,
|
|
@@ -32629,9 +32567,9 @@ __export(images_exports, {
|
|
|
32629
32567
|
writeMedia: () => writeMedia,
|
|
32630
32568
|
writeSidecar: () => writeSidecar
|
|
32631
32569
|
});
|
|
32632
|
-
import { mkdir, readFile
|
|
32570
|
+
import { mkdir, readFile, writeFile, access, stat, open } from "node:fs/promises";
|
|
32633
32571
|
import { existsSync } from "node:fs";
|
|
32634
|
-
import { join as
|
|
32572
|
+
import { join as join2, resolve, isAbsolute } from "node:path";
|
|
32635
32573
|
function slugify2(text, max = 40) {
|
|
32636
32574
|
const s = text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, max).replace(/-+$/g, "");
|
|
32637
32575
|
return s || "image";
|
|
@@ -32645,10 +32583,10 @@ async function exists(p) {
|
|
|
32645
32583
|
}
|
|
32646
32584
|
}
|
|
32647
32585
|
async function uniquePath(dir, base, ext) {
|
|
32648
|
-
let candidate =
|
|
32586
|
+
let candidate = join2(dir, `${base}.${ext}`);
|
|
32649
32587
|
let n = 2;
|
|
32650
32588
|
while (await exists(candidate)) {
|
|
32651
|
-
candidate =
|
|
32589
|
+
candidate = join2(dir, `${base}-${n}.${ext}`);
|
|
32652
32590
|
n++;
|
|
32653
32591
|
}
|
|
32654
32592
|
return candidate;
|
|
@@ -32694,7 +32632,7 @@ function resolveInputPath(p, kind) {
|
|
|
32694
32632
|
if (isAbsolute(p) && existsSync(p)) return p;
|
|
32695
32633
|
const inputDir = readEnvVar("GEMINI_INPUT_DIR");
|
|
32696
32634
|
if (inputDir) {
|
|
32697
|
-
const candidate =
|
|
32635
|
+
const candidate = join2(inputDir, p);
|
|
32698
32636
|
if (existsSync(candidate)) return resolve(candidate);
|
|
32699
32637
|
}
|
|
32700
32638
|
const cwd = resolve(p);
|
|
@@ -32740,7 +32678,7 @@ async function previewVideoInput(path) {
|
|
|
32740
32678
|
}
|
|
32741
32679
|
async function readImageAsInline(path) {
|
|
32742
32680
|
const resolved = resolveImagePath(path);
|
|
32743
|
-
const buf = await
|
|
32681
|
+
const buf = await readFile(resolved);
|
|
32744
32682
|
const mimeType = sniffMimeBytes(buf);
|
|
32745
32683
|
return { base64: buf.toString("base64"), mimeType };
|
|
32746
32684
|
}
|
|
@@ -32757,16 +32695,6 @@ function decodeImageInput(input) {
|
|
|
32757
32695
|
const buf = Buffer.from(trimmed, "base64");
|
|
32758
32696
|
return { base64: trimmed, mimeType: sniffMimeBytes(buf) };
|
|
32759
32697
|
}
|
|
32760
|
-
async function loadImageInputs(paths = [], base64s = []) {
|
|
32761
|
-
const fromPaths = await Promise.all(paths.map((p) => readImageAsInline(p)));
|
|
32762
|
-
const fromBase64s = base64s.map((b) => decodeImageInput(b));
|
|
32763
|
-
return [...fromPaths, ...fromBase64s];
|
|
32764
|
-
}
|
|
32765
|
-
async function gatherImageInputs(opts) {
|
|
32766
|
-
const clipboardImages = opts.from_clipboard ? [await (await Promise.resolve().then(() => (init_clipboard(), clipboard_exports))).readClipboardImage()] : [];
|
|
32767
|
-
const fileImages = await loadImageInputs(opts.images, opts.images_base64);
|
|
32768
|
-
return [...clipboardImages, ...fileImages];
|
|
32769
|
-
}
|
|
32770
32698
|
function baseName(name) {
|
|
32771
32699
|
const stripped = name.replace(/\.(png|jpe?g|webp|mp4|mov|webm|mp3|wav|ogg)$/i, "");
|
|
32772
32700
|
const safe = stripped.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
|
|
@@ -32795,6 +32723,66 @@ var init_images = __esm({
|
|
|
32795
32723
|
}
|
|
32796
32724
|
});
|
|
32797
32725
|
|
|
32726
|
+
// src/clipboard.ts
|
|
32727
|
+
var clipboard_exports = {};
|
|
32728
|
+
__export(clipboard_exports, {
|
|
32729
|
+
readClipboardImage: () => readClipboardImage
|
|
32730
|
+
});
|
|
32731
|
+
import { execFile } from "node:child_process";
|
|
32732
|
+
import { promisify } from "node:util";
|
|
32733
|
+
import { readFile as readFile2, unlink } from "node:fs/promises";
|
|
32734
|
+
import { tmpdir } from "node:os";
|
|
32735
|
+
import { join as join3 } from "node:path";
|
|
32736
|
+
async function readClipboardImage(run = defaultRun) {
|
|
32737
|
+
if (process.platform !== "darwin") {
|
|
32738
|
+
throw new McpToolError("from_clipboard is only supported on macOS", {
|
|
32739
|
+
hint: "Save the image to a file and pass it via `images` instead."
|
|
32740
|
+
});
|
|
32741
|
+
}
|
|
32742
|
+
const id = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
32743
|
+
const png = join3(tmpdir(), `gemini-clip-${id}.png`);
|
|
32744
|
+
const jpg = join3(tmpdir(), `gemini-clip-${id}.jpg`);
|
|
32745
|
+
try {
|
|
32746
|
+
try {
|
|
32747
|
+
await run("osascript", [
|
|
32748
|
+
"-e",
|
|
32749
|
+
`set f to (open for access POSIX file "${png}" with write permission)`,
|
|
32750
|
+
"-e",
|
|
32751
|
+
"write (the clipboard as \xABclass PNGf\xBB) to f",
|
|
32752
|
+
"-e",
|
|
32753
|
+
"close access f"
|
|
32754
|
+
]);
|
|
32755
|
+
} catch {
|
|
32756
|
+
throw new McpToolError("No image found on the clipboard", {
|
|
32757
|
+
hint: "Copy an image (\u2318C) first, or pass `images`/`images_base64`."
|
|
32758
|
+
});
|
|
32759
|
+
}
|
|
32760
|
+
try {
|
|
32761
|
+
await run("sips", ["-Z", "2048", "-s", "format", "jpeg", png, "--out", jpg]);
|
|
32762
|
+
} catch {
|
|
32763
|
+
throw new McpToolError("Failed to convert the clipboard image", {
|
|
32764
|
+
hint: "The clipboard may contain a non-standard image format."
|
|
32765
|
+
});
|
|
32766
|
+
}
|
|
32767
|
+
const buf = await readFile2(jpg);
|
|
32768
|
+
return { base64: buf.toString("base64"), mimeType: "image/jpeg" };
|
|
32769
|
+
} finally {
|
|
32770
|
+
await unlink(png).catch(() => void 0);
|
|
32771
|
+
await unlink(jpg).catch(() => void 0);
|
|
32772
|
+
}
|
|
32773
|
+
}
|
|
32774
|
+
var execFileAsync, defaultRun;
|
|
32775
|
+
var init_clipboard = __esm({
|
|
32776
|
+
"src/clipboard.ts"() {
|
|
32777
|
+
"use strict";
|
|
32778
|
+
init_dist();
|
|
32779
|
+
execFileAsync = promisify(execFile);
|
|
32780
|
+
defaultRun = async (cmd, args) => {
|
|
32781
|
+
await execFileAsync(cmd, args);
|
|
32782
|
+
};
|
|
32783
|
+
}
|
|
32784
|
+
});
|
|
32785
|
+
|
|
32798
32786
|
// src/index.ts
|
|
32799
32787
|
init_dist();
|
|
32800
32788
|
|
|
@@ -32808,7 +32796,7 @@ async function loadStdioDotenv() {
|
|
|
32808
32796
|
}
|
|
32809
32797
|
|
|
32810
32798
|
// src/version.ts
|
|
32811
|
-
var VERSION = "1.0
|
|
32799
|
+
var VERSION = "1.2.0";
|
|
32812
32800
|
|
|
32813
32801
|
// src/tools/models.ts
|
|
32814
32802
|
init_dist();
|
|
@@ -32848,6 +32836,370 @@ function filterImageModels(raw) {
|
|
|
32848
32836
|
// src/tools/generate.ts
|
|
32849
32837
|
init_images();
|
|
32850
32838
|
|
|
32839
|
+
// src/inputs.ts
|
|
32840
|
+
init_dist();
|
|
32841
|
+
|
|
32842
|
+
// src/fetch-image.ts
|
|
32843
|
+
init_dist();
|
|
32844
|
+
|
|
32845
|
+
// src/bytes.ts
|
|
32846
|
+
function base64ToBytes(base643) {
|
|
32847
|
+
const binary = atob(base643);
|
|
32848
|
+
const bytes = new Uint8Array(binary.length);
|
|
32849
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
32850
|
+
return bytes;
|
|
32851
|
+
}
|
|
32852
|
+
function bytesToBase64(bytes) {
|
|
32853
|
+
const CHUNK = 32768;
|
|
32854
|
+
let binary = "";
|
|
32855
|
+
for (let i = 0; i < bytes.length; i += CHUNK) {
|
|
32856
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + CHUNK));
|
|
32857
|
+
}
|
|
32858
|
+
return btoa(binary);
|
|
32859
|
+
}
|
|
32860
|
+
function wholeMb(bytes) {
|
|
32861
|
+
return Math.round(bytes / (1024 * 1024));
|
|
32862
|
+
}
|
|
32863
|
+
function formatMb(bytes) {
|
|
32864
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
32865
|
+
}
|
|
32866
|
+
|
|
32867
|
+
// src/fetch-image.ts
|
|
32868
|
+
var IMAGE_URL_MAX_BYTES = 15 * 1024 * 1024;
|
|
32869
|
+
var IMAGE_INLINE_MAX_BYTES = 6 * 1024 * 1024;
|
|
32870
|
+
var MAX_REDIRECTS = 5;
|
|
32871
|
+
var DEFAULT_FETCH_TIMEOUT_MS = 6e4;
|
|
32872
|
+
function ipv4Octets(host) {
|
|
32873
|
+
const parts = host.split(".");
|
|
32874
|
+
if (parts.length !== 4) return void 0;
|
|
32875
|
+
const octets = parts.map((p) => /^\d{1,3}$/.test(p) ? Number(p) : NaN);
|
|
32876
|
+
return octets.every((n) => Number.isInteger(n) && n >= 0 && n <= 255) ? octets : void 0;
|
|
32877
|
+
}
|
|
32878
|
+
function isPrivateIpv4([a, b]) {
|
|
32879
|
+
if (a === 0 || a === 127) return true;
|
|
32880
|
+
if (a === 10) return true;
|
|
32881
|
+
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
32882
|
+
if (a === 192 && b === 168) return true;
|
|
32883
|
+
if (a === 169 && b === 254) return true;
|
|
32884
|
+
if (a === 100 && b >= 64 && b <= 127) return true;
|
|
32885
|
+
if (a === 192 && b === 0) return true;
|
|
32886
|
+
if (a === 198 && (b === 18 || b === 19)) return true;
|
|
32887
|
+
if (a >= 224) return true;
|
|
32888
|
+
return false;
|
|
32889
|
+
}
|
|
32890
|
+
function ipv6Groups(host) {
|
|
32891
|
+
if (!host.includes(":")) return void 0;
|
|
32892
|
+
let text = host;
|
|
32893
|
+
const tail = [];
|
|
32894
|
+
const dotted = /:((?:\d{1,3}\.){3}\d{1,3})$/.exec(text);
|
|
32895
|
+
if (dotted) {
|
|
32896
|
+
const octets = ipv4Octets(dotted[1]);
|
|
32897
|
+
if (!octets) return void 0;
|
|
32898
|
+
tail.push(octets[0] << 8 | octets[1], octets[2] << 8 | octets[3]);
|
|
32899
|
+
text = text.slice(0, dotted.index);
|
|
32900
|
+
}
|
|
32901
|
+
const halves = text.split("::");
|
|
32902
|
+
if (halves.length > 2) return void 0;
|
|
32903
|
+
const parse3 = (part) => {
|
|
32904
|
+
if (part === "") return [];
|
|
32905
|
+
const out = [];
|
|
32906
|
+
for (const piece of part.split(":")) {
|
|
32907
|
+
if (!/^[0-9a-f]{1,4}$/.test(piece)) return void 0;
|
|
32908
|
+
out.push(parseInt(piece, 16));
|
|
32909
|
+
}
|
|
32910
|
+
return out;
|
|
32911
|
+
};
|
|
32912
|
+
const head = parse3(halves[0]);
|
|
32913
|
+
const rest = halves.length === 2 ? parse3(halves[1]) : [];
|
|
32914
|
+
if (!head || !rest) return void 0;
|
|
32915
|
+
const explicit = [...head, ...rest, ...tail];
|
|
32916
|
+
if (halves.length === 2) {
|
|
32917
|
+
if (explicit.length > 7) return void 0;
|
|
32918
|
+
return [...head, ...Array(8 - explicit.length).fill(0), ...rest, ...tail];
|
|
32919
|
+
}
|
|
32920
|
+
return explicit.length === 8 ? explicit : void 0;
|
|
32921
|
+
}
|
|
32922
|
+
var V4_BEARING_PREFIXES = [
|
|
32923
|
+
[0, 0, 0, 0, 0, 65535],
|
|
32924
|
+
// ::ffff:0:0/96 — IPv4-mapped
|
|
32925
|
+
[0, 0, 0, 0, 0, 0],
|
|
32926
|
+
// ::/96 — IPv4-compatible (deprecated)
|
|
32927
|
+
[0, 0, 0, 0, 65535, 0],
|
|
32928
|
+
// ::ffff:0:0:0/96 — IPv4-translated (deprecated SIIT)
|
|
32929
|
+
[100, 65435, 0, 0, 0, 0]
|
|
32930
|
+
// 64:ff9b::/96 — NAT64 well-known (RFC 6052)
|
|
32931
|
+
];
|
|
32932
|
+
function isPrivateIpv6(groups) {
|
|
32933
|
+
if (groups.every((g) => g === 0)) return true;
|
|
32934
|
+
if (groups.slice(0, 7).every((g) => g === 0) && groups[7] === 1) return true;
|
|
32935
|
+
if ((groups[0] & 65472) === 65152) return true;
|
|
32936
|
+
if ((groups[0] & 65024) === 64512) return true;
|
|
32937
|
+
if (V4_BEARING_PREFIXES.some((prefix) => prefix.every((g, i) => groups[i] === g))) {
|
|
32938
|
+
return isPrivateIpv4([groups[6] >> 8, groups[6] & 255, groups[7] >> 8, groups[7] & 255]);
|
|
32939
|
+
}
|
|
32940
|
+
return false;
|
|
32941
|
+
}
|
|
32942
|
+
function assertPublicHttpsUrl(raw, requested) {
|
|
32943
|
+
let url2;
|
|
32944
|
+
try {
|
|
32945
|
+
url2 = new URL(raw);
|
|
32946
|
+
} catch {
|
|
32947
|
+
throw new McpToolError(`Not a valid URL: ${raw}`, {
|
|
32948
|
+
hint: "images_url entries must be absolute https:// URLs."
|
|
32949
|
+
});
|
|
32950
|
+
}
|
|
32951
|
+
if (url2.protocol !== "https:") {
|
|
32952
|
+
throw new McpToolError(`Refusing to fetch ${describeUrl(raw, requested)}: only https:// URLs are fetched (got "${url2.protocol}").`, {
|
|
32953
|
+
hint: "Use an https:// URL, or send the bytes with images_base64 / gemini_upload_file."
|
|
32954
|
+
});
|
|
32955
|
+
}
|
|
32956
|
+
const rawHost = url2.hostname.toLowerCase();
|
|
32957
|
+
const isIpv6Literal = rawHost.startsWith("[") && rawHost.endsWith("]");
|
|
32958
|
+
const host = isIpv6Literal ? rawHost.slice(1, -1) : rawHost;
|
|
32959
|
+
const v4 = isIpv6Literal ? void 0 : ipv4Octets(host);
|
|
32960
|
+
const v6 = isIpv6Literal ? ipv6Groups(host) : void 0;
|
|
32961
|
+
const blocked = isIpv6Literal ? v6 === void 0 || isPrivateIpv6(v6) : host === "localhost" || host.endsWith(".localhost") || host.endsWith(".local") || (v4 ? isPrivateIpv4(v4) : false);
|
|
32962
|
+
if (blocked) {
|
|
32963
|
+
throw new McpToolError(
|
|
32964
|
+
`Refusing to fetch ${describeUrl(raw, requested)}: "${url2.hostname}" is a private, loopback or link-local address, not a public host.`,
|
|
32965
|
+
{ hint: "images_url is fetched by the server, so it must point at a publicly reachable https:// image." }
|
|
32966
|
+
);
|
|
32967
|
+
}
|
|
32968
|
+
return url2;
|
|
32969
|
+
}
|
|
32970
|
+
function fileNameFromUrl(url2, fallback) {
|
|
32971
|
+
try {
|
|
32972
|
+
const name = new URL(url2).pathname.split("/").filter(Boolean).pop();
|
|
32973
|
+
return name ? decodeURIComponent(name).slice(0, 120) : fallback;
|
|
32974
|
+
} catch {
|
|
32975
|
+
return fallback;
|
|
32976
|
+
}
|
|
32977
|
+
}
|
|
32978
|
+
function describeUrl(current, requested) {
|
|
32979
|
+
return current === requested ? current : `${current} (redirected from ${requested})`;
|
|
32980
|
+
}
|
|
32981
|
+
async function readCapped(res, maxBytes, url2, requested) {
|
|
32982
|
+
const declared = Number(res.headers.get("content-length"));
|
|
32983
|
+
if (Number.isFinite(declared) && declared > maxBytes) {
|
|
32984
|
+
throw tooBig(url2, requested, declared, maxBytes);
|
|
32985
|
+
}
|
|
32986
|
+
const reader = res.body?.getReader();
|
|
32987
|
+
if (!reader) {
|
|
32988
|
+
const buf = new Uint8Array(await res.arrayBuffer());
|
|
32989
|
+
if (buf.byteLength > maxBytes) throw tooBig(url2, requested, buf.byteLength, maxBytes);
|
|
32990
|
+
return buf;
|
|
32991
|
+
}
|
|
32992
|
+
const chunks = [];
|
|
32993
|
+
let total = 0;
|
|
32994
|
+
try {
|
|
32995
|
+
for (; ; ) {
|
|
32996
|
+
const { done, value } = await reader.read();
|
|
32997
|
+
if (done) break;
|
|
32998
|
+
if (!value) continue;
|
|
32999
|
+
total += value.byteLength;
|
|
33000
|
+
if (total > maxBytes) {
|
|
33001
|
+
await reader.cancel().catch(() => {
|
|
33002
|
+
});
|
|
33003
|
+
throw tooBig(url2, requested, total, maxBytes, true);
|
|
33004
|
+
}
|
|
33005
|
+
chunks.push(value);
|
|
33006
|
+
}
|
|
33007
|
+
} finally {
|
|
33008
|
+
reader.releaseLock?.();
|
|
33009
|
+
}
|
|
33010
|
+
const out = new Uint8Array(total);
|
|
33011
|
+
let offset = 0;
|
|
33012
|
+
for (const c of chunks) {
|
|
33013
|
+
out.set(c, offset);
|
|
33014
|
+
offset += c.byteLength;
|
|
33015
|
+
}
|
|
33016
|
+
return out;
|
|
33017
|
+
}
|
|
33018
|
+
function fetchFailure(err, current, requested, timeoutMs) {
|
|
33019
|
+
const name = err?.name;
|
|
33020
|
+
if (name === "TimeoutError" || name === "AbortError") {
|
|
33021
|
+
return new McpToolError(`Fetching ${describeUrl(current, requested)} timed out after ${timeoutMs}ms.`, {
|
|
33022
|
+
hint: "The server accepted the connection but did not finish sending the image. Try a different URL, or raise timeout_ms."
|
|
33023
|
+
});
|
|
33024
|
+
}
|
|
33025
|
+
return new McpToolError(
|
|
33026
|
+
`Could not fetch ${describeUrl(current, requested)}: ${err instanceof Error ? err.message : String(err)}`,
|
|
33027
|
+
{ hint: "Check the URL is reachable from the server and serves the image without authentication." }
|
|
33028
|
+
);
|
|
33029
|
+
}
|
|
33030
|
+
function tooBig(url2, requested, size, max, partial2 = false) {
|
|
33031
|
+
return new McpToolError(
|
|
33032
|
+
`Image at ${describeUrl(url2, requested)} is ${partial2 ? "over" : ""} ${formatMb(size)}, above the ${formatMb(max)} limit for images_url.`,
|
|
33033
|
+
{ hint: "Resize or re-encode the image, or upload it yourself with gemini_upload_file and pass the returned file_uri." }
|
|
33034
|
+
);
|
|
33035
|
+
}
|
|
33036
|
+
async function fetchRemoteImage(requestedUrl, opts = {}) {
|
|
33037
|
+
const doFetch = opts.fetchImpl ?? fetch;
|
|
33038
|
+
const maxBytes = opts.maxBytes ?? IMAGE_URL_MAX_BYTES;
|
|
33039
|
+
const accept = opts.accept ?? ["image/"];
|
|
33040
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
|
|
33041
|
+
let current = requestedUrl;
|
|
33042
|
+
for (let hop = 0; ; hop++) {
|
|
33043
|
+
const url2 = assertPublicHttpsUrl(current, requestedUrl);
|
|
33044
|
+
const signal = timeoutMs > 0 ? AbortSignal.timeout(timeoutMs) : void 0;
|
|
33045
|
+
let res;
|
|
33046
|
+
try {
|
|
33047
|
+
res = await doFetch(url2.toString(), {
|
|
33048
|
+
redirect: "manual",
|
|
33049
|
+
headers: { accept: accept.map((p) => `${p.replace(/\/$/, "")}/*`).join(", ") },
|
|
33050
|
+
...signal ? { signal } : {}
|
|
33051
|
+
});
|
|
33052
|
+
} catch (err) {
|
|
33053
|
+
throw fetchFailure(err, current, requestedUrl, timeoutMs);
|
|
33054
|
+
}
|
|
33055
|
+
if (res.status >= 300 && res.status < 400) {
|
|
33056
|
+
const location = res.headers.get("location");
|
|
33057
|
+
if (!location) {
|
|
33058
|
+
throw new McpToolError(`Fetching ${describeUrl(current, requestedUrl)} returned HTTP ${res.status} with no Location header.`);
|
|
33059
|
+
}
|
|
33060
|
+
if (hop >= MAX_REDIRECTS) {
|
|
33061
|
+
throw new McpToolError(`Fetching ${requestedUrl} exceeded ${MAX_REDIRECTS} redirects (last hop: ${location}).`);
|
|
33062
|
+
}
|
|
33063
|
+
current = new URL(location, url2).toString();
|
|
33064
|
+
continue;
|
|
33065
|
+
}
|
|
33066
|
+
if (!res.ok) {
|
|
33067
|
+
throw new McpToolError(`Fetching ${describeUrl(current, requestedUrl)} returned HTTP ${res.status} ${res.statusText || ""}`.trim(), {
|
|
33068
|
+
hint: "The URL must be publicly readable \u2014 a login page or signed-URL expiry shows up here as 401/403/404."
|
|
33069
|
+
});
|
|
33070
|
+
}
|
|
33071
|
+
const contentType = (res.headers.get("content-type") ?? "").split(";")[0].trim().toLowerCase();
|
|
33072
|
+
if (!accept.some((prefix) => contentType.startsWith(prefix))) {
|
|
33073
|
+
const expected = accept.map((p) => `${p.replace(/\/$/, "")}/*`).join(" or ");
|
|
33074
|
+
throw new McpToolError(
|
|
33075
|
+
`${describeUrl(current, requestedUrl)} is not ${accept.length === 1 && accept[0] === "image/" ? "an image" : "usable media"}: the server returned Content-Type "${contentType || "(none)"}", expected ${expected}.`,
|
|
33076
|
+
{ hint: "Link directly to the media file, not to the page that displays it." }
|
|
33077
|
+
);
|
|
33078
|
+
}
|
|
33079
|
+
let bytes;
|
|
33080
|
+
try {
|
|
33081
|
+
bytes = await readCapped(res, maxBytes, current, requestedUrl);
|
|
33082
|
+
} catch (err) {
|
|
33083
|
+
if (err instanceof McpToolError) throw err;
|
|
33084
|
+
throw fetchFailure(err, current, requestedUrl, timeoutMs);
|
|
33085
|
+
}
|
|
33086
|
+
if (bytes.byteLength === 0) {
|
|
33087
|
+
throw new McpToolError(`${describeUrl(current, requestedUrl)} returned an empty body.`);
|
|
33088
|
+
}
|
|
33089
|
+
return { bytes, mimeType: contentType, size: bytes.byteLength, finalUrl: current, requestedUrl };
|
|
33090
|
+
}
|
|
33091
|
+
}
|
|
33092
|
+
|
|
33093
|
+
// src/inputs.ts
|
|
33094
|
+
function hasImageInput(args) {
|
|
33095
|
+
return Boolean(
|
|
33096
|
+
args.images?.length || args.images_base64?.length || args.images_url?.length || args.images_file_uris?.length || args.from_clipboard
|
|
33097
|
+
);
|
|
33098
|
+
}
|
|
33099
|
+
var ASSUMED_TTL_MS = 47 * 60 * 60 * 1e3;
|
|
33100
|
+
function expiresAtMs(expirationTime, now) {
|
|
33101
|
+
if (expirationTime) {
|
|
33102
|
+
const t = Date.parse(expirationTime);
|
|
33103
|
+
if (Number.isFinite(t)) return t;
|
|
33104
|
+
}
|
|
33105
|
+
return now + ASSUMED_TTL_MS;
|
|
33106
|
+
}
|
|
33107
|
+
async function resolveImageInputs(args, client2) {
|
|
33108
|
+
const report = {};
|
|
33109
|
+
const inputs = [];
|
|
33110
|
+
const onDisk = client2.mediaSink?.persistsFiles ?? true;
|
|
33111
|
+
if (args.from_clipboard) {
|
|
33112
|
+
inputs.push(await (await Promise.resolve().then(() => (init_clipboard(), clipboard_exports))).readClipboardImage());
|
|
33113
|
+
}
|
|
33114
|
+
for (const path of args.images ?? []) {
|
|
33115
|
+
inputs.push(await resolveLocalPath(path, client2, onDisk, report));
|
|
33116
|
+
}
|
|
33117
|
+
if (args.images_base64?.length) {
|
|
33118
|
+
const { decodeImageInput: decodeImageInput2 } = await Promise.resolve().then(() => (init_images(), images_exports));
|
|
33119
|
+
for (const b64 of args.images_base64) inputs.push(decodeImageInput2(b64));
|
|
33120
|
+
}
|
|
33121
|
+
const fetched = /* @__PURE__ */ new Map();
|
|
33122
|
+
for (const url2 of args.images_url ?? []) {
|
|
33123
|
+
const hit = fetched.get(url2);
|
|
33124
|
+
if (hit) {
|
|
33125
|
+
inputs.push(hit);
|
|
33126
|
+
continue;
|
|
33127
|
+
}
|
|
33128
|
+
const resolved = await resolveUrl(url2, client2, report);
|
|
33129
|
+
fetched.set(url2, resolved);
|
|
33130
|
+
inputs.push(resolved);
|
|
33131
|
+
}
|
|
33132
|
+
const seenUris = /* @__PURE__ */ new Map();
|
|
33133
|
+
for (const ref of args.images_file_uris ?? []) {
|
|
33134
|
+
const hit = seenUris.get(ref);
|
|
33135
|
+
if (hit) {
|
|
33136
|
+
inputs.push(hit);
|
|
33137
|
+
continue;
|
|
33138
|
+
}
|
|
33139
|
+
const file2 = await client2.getFile(ref);
|
|
33140
|
+
const input = { uri: file2.uri, mimeType: file2.mimeType };
|
|
33141
|
+
seenUris.set(ref, input);
|
|
33142
|
+
(report.file_uris ??= []).push(file2.name);
|
|
33143
|
+
inputs.push(input);
|
|
33144
|
+
}
|
|
33145
|
+
return { inputs, report: Object.keys(report).length > 0 ? report : void 0 };
|
|
33146
|
+
}
|
|
33147
|
+
async function resolveLocalPath(path, client2, onDisk, report) {
|
|
33148
|
+
const { readImageAsInline: readImageAsInline2, resolveImagePath: resolveImagePath2 } = await Promise.resolve().then(() => (init_images(), images_exports));
|
|
33149
|
+
if (!onDisk) return readImageAsInline2(path);
|
|
33150
|
+
const { stat: stat3 } = await import("node:fs/promises");
|
|
33151
|
+
const resolved = resolveImagePath2(path);
|
|
33152
|
+
let key;
|
|
33153
|
+
try {
|
|
33154
|
+
const { mtimeMs, size } = await stat3(resolved);
|
|
33155
|
+
key = `path\0${resolved}\0${mtimeMs}\0${size}`;
|
|
33156
|
+
} catch {
|
|
33157
|
+
return readImageAsInline2(path);
|
|
33158
|
+
}
|
|
33159
|
+
const cached2 = client2.session.cachedUpload(key);
|
|
33160
|
+
if (cached2) return { uri: cached2.uri, mimeType: cached2.mimeType };
|
|
33161
|
+
const references = client2.session.countReference(key);
|
|
33162
|
+
const inline = await readImageAsInline2(resolved);
|
|
33163
|
+
if (references < 2) return inline;
|
|
33164
|
+
try {
|
|
33165
|
+
const bytes = base64ToBytes(inline.base64);
|
|
33166
|
+
const displayName = resolved.split(/[\\/]/).pop() || "image";
|
|
33167
|
+
const uploaded = await client2.uploadBytes(bytes, inline.mimeType, displayName);
|
|
33168
|
+
client2.session.uploadCache.set(key, {
|
|
33169
|
+
name: uploaded.name,
|
|
33170
|
+
uri: uploaded.uri,
|
|
33171
|
+
mimeType: uploaded.mimeType,
|
|
33172
|
+
expiresAtMs: expiresAtMs(uploaded.expirationTime, Date.now())
|
|
33173
|
+
});
|
|
33174
|
+
(report.auto_uploaded ??= []).push({ path: resolved, file_uri: uploaded.name, expires: uploaded.expirationTime });
|
|
33175
|
+
return { uri: uploaded.uri, mimeType: uploaded.mimeType };
|
|
33176
|
+
} catch {
|
|
33177
|
+
return inline;
|
|
33178
|
+
}
|
|
33179
|
+
}
|
|
33180
|
+
async function resolveUrl(url2, client2, report) {
|
|
33181
|
+
const fetched = await client2.fetchRemoteImage(url2);
|
|
33182
|
+
const entry = {
|
|
33183
|
+
url: url2,
|
|
33184
|
+
mime_type: fetched.mimeType,
|
|
33185
|
+
bytes: fetched.size
|
|
33186
|
+
};
|
|
33187
|
+
(report.fetched_urls ??= []).push(entry);
|
|
33188
|
+
if (fetched.size <= IMAGE_INLINE_MAX_BYTES) {
|
|
33189
|
+
return { base64: bytesToBase64(fetched.bytes), mimeType: fetched.mimeType };
|
|
33190
|
+
}
|
|
33191
|
+
const displayName = fileNameFromUrl(url2, "image");
|
|
33192
|
+
const uploaded = await client2.uploadBytes(fetched.bytes, fetched.mimeType, displayName);
|
|
33193
|
+
entry.file_uri = uploaded.name;
|
|
33194
|
+
return { uri: uploaded.uri, mimeType: uploaded.mimeType };
|
|
33195
|
+
}
|
|
33196
|
+
function requireImageInput(args) {
|
|
33197
|
+
if (hasImageInput(args)) return;
|
|
33198
|
+
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`."
|
|
33200
|
+
);
|
|
33201
|
+
}
|
|
33202
|
+
|
|
32851
33203
|
// src/tools/shared.ts
|
|
32852
33204
|
init_zod();
|
|
32853
33205
|
init_dist();
|
|
@@ -32883,6 +33235,16 @@ var idempotencyKeySchema = external_exports.string().min(1).optional().describe(
|
|
|
32883
33235
|
var asyncSchema = external_exports.boolean().optional().describe(
|
|
32884
33236
|
"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)."
|
|
32885
33237
|
);
|
|
33238
|
+
function imagesUrlSchema(label = "Reference images") {
|
|
33239
|
+
return external_exports.array(external_exports.string().url()).optional().describe(
|
|
33240
|
+
`${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/*).`
|
|
33241
|
+
);
|
|
33242
|
+
}
|
|
33243
|
+
function imagesFileUrisSchema(label = "Reference images") {
|
|
33244
|
+
return external_exports.array(external_exports.string().min(1)).optional().describe(
|
|
33245
|
+
`${label} by Gemini Files API reference ("files/<id>", or the full uri) from gemini_upload_file or POST /upload. Upload once, then reference it across as many calls as you like \u2014 no bytes are re-sent and none enter the conversation. Files are retained ~48h, after which the reference stops resolving.`
|
|
33246
|
+
);
|
|
33247
|
+
}
|
|
32886
33248
|
var sharedImageSchema = {
|
|
32887
33249
|
// Bare id only: the model is interpolated into the URL path
|
|
32888
33250
|
// (`/models/${model}:generateContent`), so slashes/colons/queries must be
|
|
@@ -33004,7 +33366,7 @@ function assertLocalInputsAvailable(sink, args) {
|
|
|
33004
33366
|
if (args.from_clipboard) unavailable.push("`from_clipboard` (reads the macOS clipboard via child_process)");
|
|
33005
33367
|
if (args.video_path) unavailable.push("`video_path` (uploads a file streamed from disk)");
|
|
33006
33368
|
if (unavailable.length === 0) return;
|
|
33007
|
-
const fix = "
|
|
33369
|
+
const fix = "Reference images WITHOUT putting bytes in the conversation, in order of preference: (1) `images_url` \u2014 public https URLs the server fetches itself; (2) `images_file_uris` \u2014 upload once with `gemini_upload_file` (it takes a `url` or `data_base64`) and reuse the returned files/\u2026 uri; (3) `POST /upload` on this connector with the raw bytes as the body (`curl -X POST <connector-url>/upload -H 'Authorization: Bearer <token>' -H 'Content-Type: image/jpeg' --data-binary @photo.jpg`), then pass the returned file_uri. As a last resort `images_base64` still works, but it costs ~14k tokens per photo and breaks on a truncated read. Reference video by `video_url` (a public YouTube URL, or a files/\u2026 uri uploaded elsewhere) instead of `video_path`.";
|
|
33008
33370
|
throw new McpToolError(
|
|
33009
33371
|
`${unavailable.join(", ")} ${unavailable.length > 1 ? "are" : "is"} unavailable on the hosted connector: it runs on a Cloudflare Worker, which has no filesystem and no local clipboard. ${fix}`,
|
|
33010
33372
|
{ hint: fix }
|
|
@@ -33197,7 +33559,9 @@ function registerGenerateTools(server, client2) {
|
|
|
33197
33559
|
count: external_exports.number().int().positive().max(8).optional().describe("Number of independent images (default 1)"),
|
|
33198
33560
|
filename: external_exports.string().optional().describe("Base filename for the output image (extension stripped; default: slugified prompt)"),
|
|
33199
33561
|
images: external_exports.array(external_exports.string().min(1)).optional().describe("Paths to reference input images (image-conditioned generation)"),
|
|
33200
|
-
|
|
33562
|
+
images_url: imagesUrlSchema(),
|
|
33563
|
+
images_file_uris: imagesFileUrisSchema(),
|
|
33564
|
+
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"),
|
|
33201
33565
|
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)"),
|
|
33202
33566
|
video_path: videoPathSchema,
|
|
33203
33567
|
confirm: schemaConfirm,
|
|
@@ -33221,13 +33585,15 @@ function registerGenerateTools(server, client2) {
|
|
|
33221
33585
|
images: args.images,
|
|
33222
33586
|
images_base64: args.images_base64,
|
|
33223
33587
|
from_clipboard: args.from_clipboard,
|
|
33588
|
+
images_url: args.images_url,
|
|
33589
|
+
images_file_uris: args.images_file_uris,
|
|
33224
33590
|
video_url: args.video_url,
|
|
33225
33591
|
video_path: args.video_path
|
|
33226
33592
|
});
|
|
33227
33593
|
return client2.session.jobs.dispatch({ toolName: "gemini_image_generate", fingerprint, idempotencyKey: args.idempotency_key, async: args.async }, async () => {
|
|
33228
33594
|
const seed = pickSeed(args.seed);
|
|
33229
33595
|
const slug = args.filename ? baseName(args.filename) : slugify2(args.prompt);
|
|
33230
|
-
const refInputs = await
|
|
33596
|
+
const { inputs: refInputs, report } = await withProgressHeartbeat(extra, "Resolving reference images", () => resolveImageInputs(args, client2));
|
|
33231
33597
|
const video = await withProgressHeartbeat(extra, "Uploading video to the Gemini Files API", () => resolveVideoInput(args, client2));
|
|
33232
33598
|
const named = [];
|
|
33233
33599
|
let capturedText;
|
|
@@ -33256,6 +33622,7 @@ function registerGenerateTools(server, client2) {
|
|
|
33256
33622
|
const meta3 = buildMeta(model, seed, args);
|
|
33257
33623
|
if (capturedText) meta3.text = capturedText;
|
|
33258
33624
|
if (capturedGrounding) meta3.grounding = capturedGrounding;
|
|
33625
|
+
if (report) meta3.image_inputs = report;
|
|
33259
33626
|
if (video.videoFileMeta) meta3.video_file = video.videoFileMeta;
|
|
33260
33627
|
meta3.hint = REFINE_HINT;
|
|
33261
33628
|
const risk = timeoutRiskHint({ model, imageSize: args.image_size, count, persistsFiles: client2.mediaSink?.persistsFiles });
|
|
@@ -33272,7 +33639,9 @@ function registerGenerateTools(server, client2) {
|
|
|
33272
33639
|
inputSchema: {
|
|
33273
33640
|
prompt: external_exports.string().min(1).describe("Instruction describing the edit or composition"),
|
|
33274
33641
|
images: external_exports.array(external_exports.string().min(1)).optional().describe("Paths to input image file(s) (1 = edit, 2+ = compose)"),
|
|
33275
|
-
|
|
33642
|
+
images_url: imagesUrlSchema("Input images"),
|
|
33643
|
+
images_file_uris: imagesFileUrisSchema("Input images"),
|
|
33644
|
+
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"),
|
|
33276
33645
|
filename: external_exports.string().optional().describe("Base filename for the output image (extension stripped; default: slugified prompt)"),
|
|
33277
33646
|
confirm: schemaConfirm,
|
|
33278
33647
|
...sharedImageSchema
|
|
@@ -33280,11 +33649,7 @@ function registerGenerateTools(server, client2) {
|
|
|
33280
33649
|
},
|
|
33281
33650
|
async (args, extra) => {
|
|
33282
33651
|
assertLocalInputsAvailable(client2.mediaSink, args);
|
|
33283
|
-
|
|
33284
|
-
const hasBase64 = (args.images_base64?.length ?? 0) > 0;
|
|
33285
|
-
if (!hasPaths && !hasBase64 && !args.from_clipboard) {
|
|
33286
|
-
throw new McpToolError("Provide at least one input image via `images`, `images_base64`, or `from_clipboard`.");
|
|
33287
|
-
}
|
|
33652
|
+
requireImageInput(args);
|
|
33288
33653
|
const gate = await previewLocalInputsUnlessConfirmed(args.confirm, SEND_LOCAL_ACTION, GENERATE_ENDPOINT, args.images);
|
|
33289
33654
|
if (gate) return gate;
|
|
33290
33655
|
const model = resolveModel(args.model, readEnvVar("GEMINI_IMAGE_MODEL"));
|
|
@@ -33297,12 +33662,14 @@ function registerGenerateTools(server, client2) {
|
|
|
33297
33662
|
google_search: args.google_search,
|
|
33298
33663
|
images: args.images,
|
|
33299
33664
|
images_base64: args.images_base64,
|
|
33300
|
-
from_clipboard: args.from_clipboard
|
|
33665
|
+
from_clipboard: args.from_clipboard,
|
|
33666
|
+
images_url: args.images_url,
|
|
33667
|
+
images_file_uris: args.images_file_uris
|
|
33301
33668
|
});
|
|
33302
33669
|
return client2.session.jobs.dispatch({ toolName: "gemini_image_edit", fingerprint, idempotencyKey: args.idempotency_key, async: args.async }, async () => {
|
|
33303
33670
|
const seed = pickSeed(args.seed);
|
|
33304
33671
|
const slug = args.filename ? baseName(args.filename) : slugify2(args.prompt);
|
|
33305
|
-
const inputs = await
|
|
33672
|
+
const { inputs, report } = await withProgressHeartbeat(extra, "Resolving input images", () => resolveImageInputs(args, client2));
|
|
33306
33673
|
const result = await withProgressHeartbeat(extra, `Editing image (${model})`, () => client2.generate({
|
|
33307
33674
|
prompt: args.prompt,
|
|
33308
33675
|
images: inputs,
|
|
@@ -33318,6 +33685,7 @@ function registerGenerateTools(server, client2) {
|
|
|
33318
33685
|
const meta3 = buildMeta(model, seed, args);
|
|
33319
33686
|
if (text) meta3.text = text;
|
|
33320
33687
|
if (result.grounding) meta3.grounding = result.grounding;
|
|
33688
|
+
if (report) meta3.image_inputs = report;
|
|
33321
33689
|
meta3.hint = REFINE_HINT;
|
|
33322
33690
|
const risk = timeoutRiskHint({ model, imageSize: args.image_size, persistsFiles: client2.mediaSink?.persistsFiles });
|
|
33323
33691
|
if (risk) meta3.timeout_risk = risk;
|
|
@@ -33344,7 +33712,9 @@ function registerSetTools(server, client2) {
|
|
|
33344
33712
|
reference_mode: external_exports.enum(["master", "chain"]).optional().describe("master: every image references the master (default). chain: each references the previous."),
|
|
33345
33713
|
basename: external_exports.string().optional().describe("Base filename prefix for output images (default: slugified master_prompt)"),
|
|
33346
33714
|
master_images: external_exports.array(external_exports.string().min(1)).optional().describe("Reference image paths passed to the master generation call"),
|
|
33347
|
-
|
|
33715
|
+
master_images_url: imagesUrlSchema("Reference images passed to the master AND to every scene call (fetched once)"),
|
|
33716
|
+
master_images_file_uris: imagesFileUrisSchema("Reference images passed to the master AND to every scene call"),
|
|
33717
|
+
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"),
|
|
33348
33718
|
confirm: schemaConfirm,
|
|
33349
33719
|
...sharedImageSchema
|
|
33350
33720
|
}
|
|
@@ -33369,13 +33739,24 @@ function registerSetTools(server, client2) {
|
|
|
33369
33739
|
google_search: args.google_search,
|
|
33370
33740
|
master_images: args.master_images,
|
|
33371
33741
|
master_images_base64: args.master_images_base64,
|
|
33372
|
-
from_clipboard: args.from_clipboard
|
|
33742
|
+
from_clipboard: args.from_clipboard,
|
|
33743
|
+
master_images_url: args.master_images_url,
|
|
33744
|
+
master_images_file_uris: args.master_images_file_uris
|
|
33373
33745
|
});
|
|
33374
33746
|
return client2.session.jobs.dispatch({ toolName: "gemini_image_set", fingerprint, idempotencyKey: args.idempotency_key, async: args.async }, async () => {
|
|
33375
33747
|
const seed = pickSeed(args.seed);
|
|
33376
33748
|
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 };
|
|
33377
33749
|
const slug = args.basename ? baseName(args.basename) : slugify2(args.master_prompt);
|
|
33378
|
-
const
|
|
33750
|
+
const carried = await resolveImageInputs(
|
|
33751
|
+
{ images_url: args.master_images_url, images_file_uris: args.master_images_file_uris },
|
|
33752
|
+
client2
|
|
33753
|
+
);
|
|
33754
|
+
const masterOnly = await resolveImageInputs(
|
|
33755
|
+
{ images: args.master_images, images_base64: args.master_images_base64, from_clipboard: args.from_clipboard },
|
|
33756
|
+
client2
|
|
33757
|
+
);
|
|
33758
|
+
const masterRefInputs = [...masterOnly.inputs, ...carried.inputs];
|
|
33759
|
+
const report = carried.report || masterOnly.report ? { ...masterOnly.report, ...carried.report } : void 0;
|
|
33379
33760
|
const named = [];
|
|
33380
33761
|
const masterResult = await withProgressHeartbeat(extra, `Generating image set (${model})`, async () => {
|
|
33381
33762
|
const result = await client2.generate({
|
|
@@ -33391,13 +33772,13 @@ function registerSetTools(server, client2) {
|
|
|
33391
33772
|
if (mode === "chain") {
|
|
33392
33773
|
let ref = master;
|
|
33393
33774
|
for (let i = 0; i < scenePrompts.length; i++) {
|
|
33394
|
-
const { images: [img] } = await client2.generate({ prompt: scenePrompts[i], images: [ref], seed: seed + i + 1, ...cfg });
|
|
33775
|
+
const { images: [img] } = await client2.generate({ prompt: scenePrompts[i], images: [ref, ...carried.inputs], seed: seed + i + 1, ...cfg });
|
|
33395
33776
|
named.push({ image: img, base: `${slug}-${String(i + 1).padStart(2, "0")}` });
|
|
33396
33777
|
ref = img;
|
|
33397
33778
|
}
|
|
33398
33779
|
} else {
|
|
33399
33780
|
const scenes = await Promise.all(
|
|
33400
|
-
scenePrompts.map((p, i) => client2.generate({ prompt: p, images: [master], seed: seed + i + 1, ...cfg }).then((r) => r.images[0]))
|
|
33781
|
+
scenePrompts.map((p, i) => client2.generate({ prompt: p, images: [master, ...carried.inputs], seed: seed + i + 1, ...cfg }).then((r) => r.images[0]))
|
|
33401
33782
|
);
|
|
33402
33783
|
scenes.forEach((img, i) => named.push({ image: img, base: `${slug}-${String(i + 1).padStart(2, "0")}` }));
|
|
33403
33784
|
}
|
|
@@ -33407,6 +33788,7 @@ function registerSetTools(server, client2) {
|
|
|
33407
33788
|
const meta3 = buildMeta(model, seed, args);
|
|
33408
33789
|
if (masterText) meta3.text = masterText;
|
|
33409
33790
|
if (masterResult.grounding) meta3.grounding = masterResult.grounding;
|
|
33791
|
+
if (report) meta3.image_inputs = report;
|
|
33410
33792
|
const risk = timeoutRiskHint({ model, imageSize: args.image_size, count: named.length, persistsFiles: client2.mediaSink?.persistsFiles });
|
|
33411
33793
|
if (risk) meta3.timeout_risk = risk;
|
|
33412
33794
|
return emit(named, { ...args, sink: client2.mediaSink }, meta3);
|
|
@@ -33448,6 +33830,42 @@ var SessionState = class {
|
|
|
33448
33830
|
* model against the requested edit.
|
|
33449
33831
|
*/
|
|
33450
33832
|
writtenOutputs = /* @__PURE__ */ new Set();
|
|
33833
|
+
/**
|
|
33834
|
+
* Files API uploads this session can reuse, keyed by an opaque identity
|
|
33835
|
+
* string (for a local path: absolute path + mtime + size, so an edited file
|
|
33836
|
+
* is a different key rather than a stale hit).
|
|
33837
|
+
*
|
|
33838
|
+
* Backs the stdio "referenced twice → upload once" optimization: the second
|
|
33839
|
+
* time a session sends the same picture, the bytes are replaced by a
|
|
33840
|
+
* `files/…` reference that costs nothing to repeat. Session-scoped, like
|
|
33841
|
+
* everything else here — a uri is minted by ONE user's API key and is only
|
|
33842
|
+
* readable with that key, so sharing this map across the connector's tenants
|
|
33843
|
+
* would hand out references another user cannot use and this user did not
|
|
33844
|
+
* create.
|
|
33845
|
+
*/
|
|
33846
|
+
uploadCache = /* @__PURE__ */ new Map();
|
|
33847
|
+
/**
|
|
33848
|
+
* How many times each identity has been referenced this session. The
|
|
33849
|
+
* threshold for auto-uploading is "more than once", so the count has to
|
|
33850
|
+
* outlive the call that first saw it.
|
|
33851
|
+
*/
|
|
33852
|
+
referenceCounts = /* @__PURE__ */ new Map();
|
|
33853
|
+
/** Record a reference and return the running total (1 on first sight). */
|
|
33854
|
+
countReference(key) {
|
|
33855
|
+
const next = (this.referenceCounts.get(key) ?? 0) + 1;
|
|
33856
|
+
this.referenceCounts.set(key, next);
|
|
33857
|
+
return next;
|
|
33858
|
+
}
|
|
33859
|
+
/** A cached upload, or `undefined` if absent or past its Files API TTL. */
|
|
33860
|
+
cachedUpload(key, now = Date.now()) {
|
|
33861
|
+
const hit = this.uploadCache.get(key);
|
|
33862
|
+
if (!hit) return void 0;
|
|
33863
|
+
if (hit.expiresAtMs <= now) {
|
|
33864
|
+
this.uploadCache.delete(key);
|
|
33865
|
+
return void 0;
|
|
33866
|
+
}
|
|
33867
|
+
return hit;
|
|
33868
|
+
}
|
|
33451
33869
|
/** Forget everything. Tests use this; nothing in `src/` calls it. */
|
|
33452
33870
|
reset() {
|
|
33453
33871
|
this.jobs.reset();
|
|
@@ -33455,6 +33873,8 @@ var SessionState = class {
|
|
|
33455
33873
|
this.lastMusicInteractionId = void 0;
|
|
33456
33874
|
this.lastVideoInteractionId = void 0;
|
|
33457
33875
|
this.writtenOutputs.clear();
|
|
33876
|
+
this.uploadCache.clear();
|
|
33877
|
+
this.referenceCounts.clear();
|
|
33458
33878
|
}
|
|
33459
33879
|
};
|
|
33460
33880
|
|
|
@@ -33495,6 +33915,22 @@ var ChainedRequest404Error = class extends McpToolError {
|
|
|
33495
33915
|
upstreamMessage;
|
|
33496
33916
|
retries;
|
|
33497
33917
|
};
|
|
33918
|
+
function generatePart(img) {
|
|
33919
|
+
return img.uri ? { file_data: { file_uri: img.uri, mime_type: img.mimeType } } : { inline_data: { mime_type: img.mimeType, data: img.base64 } };
|
|
33920
|
+
}
|
|
33921
|
+
function interactPart(img) {
|
|
33922
|
+
return img.uri ? { type: "image", uri: img.uri, mime_type: img.mimeType } : { type: "image", mime_type: img.mimeType, data: img.base64 };
|
|
33923
|
+
}
|
|
33924
|
+
function normalizeFileName(ref) {
|
|
33925
|
+
const trimmed = ref.trim();
|
|
33926
|
+
const match = /(?:^|\/)(files\/[\w-]+)\/?$/.exec(trimmed);
|
|
33927
|
+
if (!match) {
|
|
33928
|
+
throw new McpToolError(`Not a Gemini Files API reference: "${ref}"`, {
|
|
33929
|
+
hint: "Pass the `files/<id>` name (or the full https://\u2026/v1beta/files/<id> uri) returned by gemini_upload_file / POST /upload."
|
|
33930
|
+
});
|
|
33931
|
+
}
|
|
33932
|
+
return match[1];
|
|
33933
|
+
}
|
|
33498
33934
|
var GeminiClient = class {
|
|
33499
33935
|
/**
|
|
33500
33936
|
* A key handed in explicitly (the connector's per-session key). When absent
|
|
@@ -33583,6 +34019,14 @@ var GeminiClient = class {
|
|
|
33583
34019
|
}
|
|
33584
34020
|
return key;
|
|
33585
34021
|
}
|
|
34022
|
+
/**
|
|
34023
|
+
* Fetch one `images_url` reference server-side. Routed through the client so
|
|
34024
|
+
* it uses the SAME injected `fetchImpl` as every upstream call — which is
|
|
34025
|
+
* what makes it mockable in tests and correct on the Worker.
|
|
34026
|
+
*/
|
|
34027
|
+
async fetchRemoteImage(url2, opts = {}) {
|
|
34028
|
+
return fetchRemoteImage(url2, { ...opts, fetchImpl: this.fetchImpl, timeoutMs: resolveTimeoutMs(opts.timeoutMs) });
|
|
34029
|
+
}
|
|
33586
34030
|
/** The default model after env override (no per-call arg). */
|
|
33587
34031
|
defaultModel() {
|
|
33588
34032
|
return resolveModel(void 0, readEnvVar("GEMINI_IMAGE_MODEL"));
|
|
@@ -33614,8 +34058,60 @@ var GeminiClient = class {
|
|
|
33614
34058
|
* (enforced locally before any bytes are sent).
|
|
33615
34059
|
*/
|
|
33616
34060
|
async uploadVideo(path, mimeType) {
|
|
33617
|
-
const key = this.requireKey();
|
|
33618
34061
|
const blob = await fileBlob(path, { type: mimeType, maxBytes: FILE_MAX_BYTES, label: "Video" });
|
|
34062
|
+
return this.uploadToFilesApi(blob, mimeType, basename(path), blob.size);
|
|
34063
|
+
}
|
|
34064
|
+
/**
|
|
34065
|
+
* Upload already-in-memory bytes (a fetched `images_url`, a `data_base64`
|
|
34066
|
+
* payload, an inline `gemini_upload_file` body) to the Files API. Same three
|
|
34067
|
+
* steps as {@link uploadVideo}, minus the filesystem — which is what makes it
|
|
34068
|
+
* usable on the hosted connector, where there is no disk to stream from.
|
|
34069
|
+
*/
|
|
34070
|
+
async uploadBytes(bytes, mimeType, displayName) {
|
|
34071
|
+
const view = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
|
|
34072
|
+
if (view.byteLength > FILE_MAX_BYTES) {
|
|
34073
|
+
throw new McpToolError(`File is ${view.byteLength} bytes, over the Gemini Files API limit of ${FILE_MAX_BYTES} bytes (2 GB).`);
|
|
34074
|
+
}
|
|
34075
|
+
const body = new Blob([view], { type: mimeType });
|
|
34076
|
+
return this.uploadToFilesApi(body, mimeType, displayName, view.byteLength);
|
|
34077
|
+
}
|
|
34078
|
+
/**
|
|
34079
|
+
* Upload a request body straight through to the Files API without buffering
|
|
34080
|
+
* it — the direct-upload HTTP endpoint (`POST /upload` on the Worker) hands
|
|
34081
|
+
* us a `ReadableStream` and a `Content-Length`, and the whole point of that
|
|
34082
|
+
* endpoint is that the bytes never materialize anywhere they'd cost context
|
|
34083
|
+
* or memory.
|
|
34084
|
+
*
|
|
34085
|
+
* `contentLength` is REQUIRED because the resumable protocol's start step
|
|
34086
|
+
* declares it up front (`X-Goog-Upload-Header-Content-Length`); a stream of
|
|
34087
|
+
* unknown length has to be buffered by the caller first.
|
|
34088
|
+
*/
|
|
34089
|
+
async uploadStream(stream, mimeType, displayName, contentLength) {
|
|
34090
|
+
if (contentLength > FILE_MAX_BYTES) {
|
|
34091
|
+
throw new McpToolError(`File is ${contentLength} bytes, over the Gemini Files API limit of ${FILE_MAX_BYTES} bytes (2 GB).`);
|
|
34092
|
+
}
|
|
34093
|
+
return this.uploadToFilesApi(stream, mimeType, displayName, contentLength);
|
|
34094
|
+
}
|
|
34095
|
+
/**
|
|
34096
|
+
* The resumable upload itself. Three live-verified steps (docs/GEMINI-API.md
|
|
34097
|
+
* "Files API — local video upload"):
|
|
34098
|
+
*
|
|
34099
|
+
* 1. `POST {UPLOAD_BASE_URL}/files` (command `start`) → the upload session
|
|
34100
|
+
* URL arrives in the `x-goog-upload-url` RESPONSE HEADER (body is empty).
|
|
34101
|
+
* 2. `POST <session url>` (command `upload, finalize`) with the bytes.
|
|
34102
|
+
* Response: `{file:…}`.
|
|
34103
|
+
* 3. Poll `GET /v1beta/files/<id>` while `state` is `PROCESSING`, until
|
|
34104
|
+
* `ACTIVE` (the poll response is the File object UNWRAPPED). Images are
|
|
34105
|
+
* usually `ACTIVE` immediately; video is what processes.
|
|
34106
|
+
*
|
|
34107
|
+
* These two upload calls are raw `fetchImpl` (not `createApiClient`): step 1
|
|
34108
|
+
* needs the response *header* and step 2 posts a binary body — neither fits
|
|
34109
|
+
* `fetchJson`. The poll DOES go through the shared client (timeout + retry).
|
|
34110
|
+
* Uploads deliberately have no abort timeout: a multi-hundred-MB file can
|
|
34111
|
+
* legitimately take longer than any fixed budget.
|
|
34112
|
+
*/
|
|
34113
|
+
async uploadToFilesApi(body, mimeType, displayName, contentLength) {
|
|
34114
|
+
const key = this.requireKey();
|
|
33619
34115
|
const startPath = "/files";
|
|
33620
34116
|
const startRes = await this.fetchImpl(`${UPLOAD_BASE_URL}${startPath}`, {
|
|
33621
34117
|
method: "POST",
|
|
@@ -33623,11 +34119,11 @@ var GeminiClient = class {
|
|
|
33623
34119
|
"x-goog-api-key": key,
|
|
33624
34120
|
"X-Goog-Upload-Protocol": "resumable",
|
|
33625
34121
|
"X-Goog-Upload-Command": "start",
|
|
33626
|
-
"X-Goog-Upload-Header-Content-Length": String(
|
|
34122
|
+
"X-Goog-Upload-Header-Content-Length": String(contentLength),
|
|
33627
34123
|
"X-Goog-Upload-Header-Content-Type": mimeType,
|
|
33628
34124
|
"Content-Type": "application/json"
|
|
33629
34125
|
},
|
|
33630
|
-
body: JSON.stringify({ file: { display_name:
|
|
34126
|
+
body: JSON.stringify({ file: { display_name: displayName } })
|
|
33631
34127
|
});
|
|
33632
34128
|
if (!startRes.ok) {
|
|
33633
34129
|
throw new McpToolError(
|
|
@@ -33640,13 +34136,17 @@ var GeminiClient = class {
|
|
|
33640
34136
|
hint: "The Files API resumable-upload contract may have changed \u2014 see docs/GEMINI-API.md."
|
|
33641
34137
|
});
|
|
33642
34138
|
}
|
|
34139
|
+
const streaming = !(body instanceof Blob);
|
|
33643
34140
|
const upRes = await this.fetchImpl(uploadUrl, {
|
|
33644
34141
|
method: "POST",
|
|
33645
34142
|
headers: {
|
|
33646
34143
|
"X-Goog-Upload-Offset": "0",
|
|
33647
|
-
"X-Goog-Upload-Command": "upload, finalize"
|
|
34144
|
+
"X-Goog-Upload-Command": "upload, finalize",
|
|
34145
|
+
...streaming ? { "Content-Length": String(contentLength) } : {}
|
|
33648
34146
|
},
|
|
33649
|
-
body
|
|
34147
|
+
body,
|
|
34148
|
+
// Required by undici/workerd to send a ReadableStream body at all.
|
|
34149
|
+
...streaming ? { duplex: "half" } : {}
|
|
33650
34150
|
});
|
|
33651
34151
|
if (!upRes.ok) {
|
|
33652
34152
|
throw new McpToolError(
|
|
@@ -33660,8 +34160,8 @@ var GeminiClient = class {
|
|
|
33660
34160
|
}
|
|
33661
34161
|
for (let attempt = 0; file2.state === "PROCESSING"; attempt++) {
|
|
33662
34162
|
if (attempt >= FILE_POLL_MAX_ATTEMPTS) {
|
|
33663
|
-
throw new McpToolError(`${SERVICE}
|
|
33664
|
-
hint: "Very long videos can take a while to process \u2014 retry later by passing the file uri as video_url."
|
|
34163
|
+
throw new McpToolError(`${SERVICE} file ${name} is still processing after ${FILE_POLL_INTERVAL_MS * FILE_POLL_MAX_ATTEMPTS / 6e4} minutes`, {
|
|
34164
|
+
hint: "Very long videos can take a while to process \u2014 retry later by passing the file uri as video_url / images_file_uris."
|
|
33665
34165
|
});
|
|
33666
34166
|
}
|
|
33667
34167
|
await this.sleep(FILE_POLL_INTERVAL_MS);
|
|
@@ -33669,14 +34169,78 @@ var GeminiClient = class {
|
|
|
33669
34169
|
}
|
|
33670
34170
|
if (file2.state !== "ACTIVE") {
|
|
33671
34171
|
throw new McpToolError(
|
|
33672
|
-
`${SERVICE}
|
|
33673
|
-
{ hint: "Check the
|
|
34172
|
+
`${SERVICE} file processing failed (state ${file2.state ?? "unknown"})${file2.error?.message ? `: ${file2.error.message}` : ""}`,
|
|
34173
|
+
{ hint: "Check the file is a supported format/codec and under 2 GB." }
|
|
33674
34174
|
);
|
|
33675
34175
|
}
|
|
33676
34176
|
if (!file2.uri) {
|
|
33677
34177
|
throw new McpToolError(`${SERVICE} did not return a uri for uploaded file ${name}`);
|
|
33678
34178
|
}
|
|
33679
|
-
return {
|
|
34179
|
+
return {
|
|
34180
|
+
name,
|
|
34181
|
+
uri: file2.uri,
|
|
34182
|
+
mimeType: file2.mimeType ?? mimeType,
|
|
34183
|
+
expirationTime: file2.expirationTime,
|
|
34184
|
+
sizeBytes: file2.sizeBytes,
|
|
34185
|
+
displayName: file2.displayName
|
|
34186
|
+
};
|
|
34187
|
+
}
|
|
34188
|
+
/**
|
|
34189
|
+
* List the files this API key currently has in the Files API. Retention is
|
|
34190
|
+
* ~48h, so the list is naturally short-lived — an empty list after a day is
|
|
34191
|
+
* expiry, not a bug.
|
|
34192
|
+
*/
|
|
34193
|
+
async listFiles(pageSize = 100) {
|
|
34194
|
+
const size = Math.min(Math.max(Math.trunc(pageSize), 1), 100);
|
|
34195
|
+
const data = await this.call("GET", `/files?pageSize=${size}`);
|
|
34196
|
+
return (data.files ?? []).filter((f) => !!f.name && !!f.uri).map((f) => ({
|
|
34197
|
+
name: f.name,
|
|
34198
|
+
uri: f.uri,
|
|
34199
|
+
mimeType: f.mimeType ?? "application/octet-stream",
|
|
34200
|
+
state: f.state,
|
|
34201
|
+
createTime: f.createTime,
|
|
34202
|
+
expirationTime: f.expirationTime,
|
|
34203
|
+
sizeBytes: f.sizeBytes,
|
|
34204
|
+
displayName: f.displayName
|
|
34205
|
+
}));
|
|
34206
|
+
}
|
|
34207
|
+
/**
|
|
34208
|
+
* Fetch one file's metadata. Used to resolve a caller-supplied
|
|
34209
|
+
* `images_file_uris` entry to its real MIME type — and, as a side effect, to
|
|
34210
|
+
* fail fast with a clear "that file is gone" instead of the Interactions
|
|
34211
|
+
* API's generic 404 after a billable request was already built.
|
|
34212
|
+
*/
|
|
34213
|
+
async getFile(ref) {
|
|
34214
|
+
const resource = normalizeFileName(ref);
|
|
34215
|
+
const file2 = await this.call("GET", `/${resource}`);
|
|
34216
|
+
if (!file2.uri) {
|
|
34217
|
+
throw new McpToolError(`${SERVICE} returned no uri for ${resource}`, {
|
|
34218
|
+
hint: "Files expire ~48h after upload \u2014 re-upload with gemini_upload_file (or POST /upload) and use the new uri."
|
|
34219
|
+
});
|
|
34220
|
+
}
|
|
34221
|
+
if (file2.state && file2.state !== "ACTIVE") {
|
|
34222
|
+
throw new McpToolError(`${SERVICE} file ${resource} is not usable yet (state ${file2.state}).`, {
|
|
34223
|
+
hint: "Wait for the upload to finish processing, then retry."
|
|
34224
|
+
});
|
|
34225
|
+
}
|
|
34226
|
+
return {
|
|
34227
|
+
name: file2.name ?? resource,
|
|
34228
|
+
uri: file2.uri,
|
|
34229
|
+
mimeType: file2.mimeType ?? "application/octet-stream",
|
|
34230
|
+
expirationTime: file2.expirationTime,
|
|
34231
|
+
sizeBytes: file2.sizeBytes,
|
|
34232
|
+
displayName: file2.displayName
|
|
34233
|
+
};
|
|
34234
|
+
}
|
|
34235
|
+
/**
|
|
34236
|
+
* Delete one uploaded file. `name` is interpolated into the URL path, so it
|
|
34237
|
+
* is validated to a bare `files/<id>` for the same reason the model id is
|
|
34238
|
+
* (see {@link uploadToFilesApi}); a full `https://…/v1beta/files/<id>` uri is
|
|
34239
|
+
* accepted and reduced to that form, since that is what every result echoes.
|
|
34240
|
+
*/
|
|
34241
|
+
async deleteFile(name) {
|
|
34242
|
+
const resource = normalizeFileName(name);
|
|
34243
|
+
await this.call("DELETE", `/${resource}`);
|
|
33680
34244
|
}
|
|
33681
34245
|
async listModels() {
|
|
33682
34246
|
const data = await this.call("GET", "/models?pageSize=200");
|
|
@@ -33685,9 +34249,7 @@ var GeminiClient = class {
|
|
|
33685
34249
|
async generate(opts) {
|
|
33686
34250
|
const model = resolveModel(opts.model, readEnvVar("GEMINI_IMAGE_MODEL"));
|
|
33687
34251
|
const parts = [{ text: opts.prompt }];
|
|
33688
|
-
for (const img of opts.images ?? [])
|
|
33689
|
-
parts.push({ inline_data: { mime_type: img.mimeType, data: img.base64 } });
|
|
33690
|
-
}
|
|
34252
|
+
for (const img of opts.images ?? []) parts.push(generatePart(img));
|
|
33691
34253
|
if (opts.videoUrl) {
|
|
33692
34254
|
parts.push({ file_data: { file_uri: opts.videoUrl, mime_type: opts.videoMimeType ?? "video/mp4" } });
|
|
33693
34255
|
}
|
|
@@ -33743,9 +34305,7 @@ var GeminiClient = class {
|
|
|
33743
34305
|
async interact(opts) {
|
|
33744
34306
|
const model = resolveModel(opts.model, readEnvVar("GEMINI_IMAGE_MODEL"));
|
|
33745
34307
|
const inputParts = [{ type: "text", text: opts.input }];
|
|
33746
|
-
for (const img of opts.images ?? [])
|
|
33747
|
-
inputParts.push({ type: "image", mime_type: img.mimeType, data: img.base64 });
|
|
33748
|
-
}
|
|
34308
|
+
for (const img of opts.images ?? []) inputParts.push(interactPart(img));
|
|
33749
34309
|
if (opts.videoUrl) {
|
|
33750
34310
|
inputParts.push({ type: "video", uri: opts.videoUrl, mime_type: opts.videoMimeType ?? "video/mp4" });
|
|
33751
34311
|
}
|
|
@@ -33778,7 +34338,7 @@ var GeminiClient = class {
|
|
|
33778
34338
|
async generateVideo(opts) {
|
|
33779
34339
|
const model = opts.model ?? DEFAULT_VIDEO_MODEL;
|
|
33780
34340
|
const inputParts = [{ type: "text", text: opts.input }];
|
|
33781
|
-
for (const img of opts.images ?? []) inputParts.push(
|
|
34341
|
+
for (const img of opts.images ?? []) inputParts.push(interactPart(img));
|
|
33782
34342
|
const responseFormat = { type: "video", delivery: "inline" };
|
|
33783
34343
|
if (opts.aspectRatio) responseFormat.aspect_ratio = opts.aspectRatio;
|
|
33784
34344
|
const body = { model, input: inputParts, response_format: responseFormat };
|
|
@@ -33800,7 +34360,7 @@ var GeminiClient = class {
|
|
|
33800
34360
|
async generateMusic(opts) {
|
|
33801
34361
|
const model = opts.model ?? DEFAULT_MUSIC_MODEL;
|
|
33802
34362
|
const inputParts = [{ type: "text", text: opts.input }];
|
|
33803
|
-
for (const img of opts.images ?? []) inputParts.push(
|
|
34363
|
+
for (const img of opts.images ?? []) inputParts.push(interactPart(img));
|
|
33804
34364
|
const responseFormat = { type: "audio" };
|
|
33805
34365
|
if (opts.audioFormat) responseFormat.audio_format = opts.audioFormat;
|
|
33806
34366
|
const body = { model, input: inputParts, response_format: responseFormat };
|
|
@@ -33959,7 +34519,11 @@ function registerInteractTools(server, client2) {
|
|
|
33959
34519
|
previous_interaction_id: external_exports.string().optional().describe("ID from a prior gemini_interact call \u2014 continues that multi-turn conversation"),
|
|
33960
34520
|
continue_last: external_exports.boolean().optional().describe("Continue from the most recent interaction this server created (convenience for previous_interaction_id; an explicit id wins). Survives a server restart by falling back to the newest <image>.json sidecar in the output dir."),
|
|
33961
34521
|
images: external_exports.array(external_exports.string().min(1)).optional().describe(`Paths to reference input images. ${NEW_REFERENCES_ONLY}`),
|
|
33962
|
-
|
|
34522
|
+
images_url: imagesUrlSchema(`Reference images. ${NEW_REFERENCES_ONLY} Given`),
|
|
34523
|
+
images_file_uris: imagesFileUrisSchema(`Reference images. ${NEW_REFERENCES_ONLY} Given`),
|
|
34524
|
+
images_base64: external_exports.array(external_exports.string().min(1)).optional().describe(
|
|
34525
|
+
`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. ${NEW_REFERENCES_ONLY}`
|
|
34526
|
+
),
|
|
33963
34527
|
model: external_exports.string().optional().describe(`Model id override (default: server default; see gemini_list_models). ${MODEL_CHOICE_GUIDE}`),
|
|
33964
34528
|
aspect_ratio: external_exports.enum(ASPECT_RATIOS).optional().describe("Output aspect ratio"),
|
|
33965
34529
|
image_size: external_exports.enum(IMAGE_SIZES).optional().describe("Output resolution (512 = 0.5K, Flash-only)"),
|
|
@@ -34017,11 +34581,13 @@ function registerInteractTools(server, client2) {
|
|
|
34017
34581
|
images,
|
|
34018
34582
|
images_base64: args.images_base64,
|
|
34019
34583
|
from_clipboard: args.from_clipboard,
|
|
34584
|
+
images_url: args.images_url,
|
|
34585
|
+
images_file_uris: args.images_file_uris,
|
|
34020
34586
|
video_url: args.video_url,
|
|
34021
34587
|
video_path: args.video_path
|
|
34022
34588
|
});
|
|
34023
34589
|
return client2.session.jobs.dispatch({ toolName: "gemini_interact", fingerprint, idempotencyKey: args.idempotency_key, async: args.async }, async () => {
|
|
34024
|
-
const inputs = await
|
|
34590
|
+
const { inputs, report } = await resolveImageInputs({ ...args, images }, client2);
|
|
34025
34591
|
const video = await withProgressHeartbeat(extra, "Uploading video to the Gemini Files API", () => resolveVideoInput(args, client2));
|
|
34026
34592
|
const callOpts = {
|
|
34027
34593
|
input: args.input,
|
|
@@ -34073,6 +34639,7 @@ function registerInteractTools(server, client2) {
|
|
|
34073
34639
|
meta3.hint = `To refine this image, call gemini_interact again with previous_interaction_id: "${r.id}" (or continue_last: true) \u2014 do NOT re-attach the returned image as an input; the interaction already contains it.`;
|
|
34074
34640
|
if (r.text) meta3.text = r.text;
|
|
34075
34641
|
if (r.grounding) meta3.grounding = r.grounding;
|
|
34642
|
+
if (report) meta3.image_inputs = report;
|
|
34076
34643
|
if (video.videoFileMeta) meta3.video_file = video.videoFileMeta;
|
|
34077
34644
|
const risk = timeoutRiskHint({ model, imageSize: args.image_size, persistsFiles: onDisk });
|
|
34078
34645
|
if (risk) meta3.timeout_risk = risk;
|
|
@@ -34111,7 +34678,9 @@ function registerVideoTools(server, client2) {
|
|
|
34111
34678
|
aspect_ratio: external_exports.enum(VIDEO_ASPECT_RATIOS).optional().describe("Output aspect ratio (omni: 16:9 or 9:16)"),
|
|
34112
34679
|
task: external_exports.enum(VIDEO_TASKS).optional().describe("text_to_video (default), image_to_video / reference_to_video (need image input), or edit (needs previous_interaction_id)"),
|
|
34113
34680
|
images: external_exports.array(external_exports.string().min(1)).optional().describe("Reference image path(s) for image_to_video / reference_to_video"),
|
|
34114
|
-
|
|
34681
|
+
images_url: imagesUrlSchema("Reference stills"),
|
|
34682
|
+
images_file_uris: imagesFileUrisSchema("Reference stills"),
|
|
34683
|
+
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"),
|
|
34115
34684
|
from_clipboard: external_exports.boolean().optional().describe("Use the image currently on the macOS clipboard as a reference"),
|
|
34116
34685
|
filename: external_exports.string().optional().describe("Base filename for the output video (extension stripped; default: slugified prompt)"),
|
|
34117
34686
|
output_dir: external_exports.string().optional().describe("Directory to write the video to (default: $GEMINI_OUTPUT_DIR or cwd)"),
|
|
@@ -34146,10 +34715,12 @@ function registerVideoTools(server, client2) {
|
|
|
34146
34715
|
images: args.images,
|
|
34147
34716
|
images_base64: args.images_base64,
|
|
34148
34717
|
from_clipboard: args.from_clipboard,
|
|
34718
|
+
images_url: args.images_url,
|
|
34719
|
+
images_file_uris: args.images_file_uris,
|
|
34149
34720
|
previous_interaction_id: previousInteractionId
|
|
34150
34721
|
});
|
|
34151
34722
|
return client2.session.jobs.dispatch({ toolName: "gemini_video_generate", fingerprint, idempotencyKey: args.idempotency_key, async: args.async }, async () => {
|
|
34152
|
-
const inputs = await
|
|
34723
|
+
const { inputs, report } = await resolveImageInputs(args, client2);
|
|
34153
34724
|
const r = await withProgressHeartbeat(extra, `Generating video (${model})`, () => client2.generateVideo({
|
|
34154
34725
|
input: args.prompt,
|
|
34155
34726
|
images: inputs.length ? inputs : void 0,
|
|
@@ -34163,6 +34734,7 @@ function registerVideoTools(server, client2) {
|
|
|
34163
34734
|
const meta3 = { model, interaction_id: r.id };
|
|
34164
34735
|
if (previousInteractionId) meta3.previous_interaction_id = previousInteractionId;
|
|
34165
34736
|
if (r.text) meta3.text = r.text;
|
|
34737
|
+
if (report) meta3.image_inputs = report;
|
|
34166
34738
|
meta3.hint = `To edit this video, call gemini_video_generate again with task: "edit" and previous_interaction_id: "${r.id}" (or continue_last: true).`;
|
|
34167
34739
|
const slug = args.filename ? baseName(args.filename) : slugify2(args.prompt);
|
|
34168
34740
|
const named = r.videos.map((media, i) => ({
|
|
@@ -34195,7 +34767,9 @@ function registerMusicTools(server, client2) {
|
|
|
34195
34767
|
model: external_exports.enum(MUSIC_MODELS).optional().describe(`Lyria model (default: ${DEFAULT_MUSIC_MODEL}). Pro is longer-form and supports WAV.`),
|
|
34196
34768
|
audio_format: external_exports.enum(AUDIO_FORMATS).optional().describe("Output format (default mp3). wav is lyria-3-pro-preview-only."),
|
|
34197
34769
|
images: external_exports.array(external_exports.string().min(1)).optional().describe("Optional reference image path(s) to condition the music"),
|
|
34198
|
-
|
|
34770
|
+
images_url: imagesUrlSchema("Reference images"),
|
|
34771
|
+
images_file_uris: imagesFileUrisSchema("Reference images"),
|
|
34772
|
+
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"),
|
|
34199
34773
|
from_clipboard: external_exports.boolean().optional().describe("Use the image currently on the macOS clipboard as a reference"),
|
|
34200
34774
|
filename: external_exports.string().optional().describe("Base filename for the output audio (extension stripped; default: slugified prompt)"),
|
|
34201
34775
|
output_dir: external_exports.string().optional().describe("Directory to write audio to (default: $GEMINI_OUTPUT_DIR or cwd)"),
|
|
@@ -34234,10 +34808,12 @@ function registerMusicTools(server, client2) {
|
|
|
34234
34808
|
images: args.images,
|
|
34235
34809
|
images_base64: args.images_base64,
|
|
34236
34810
|
from_clipboard: args.from_clipboard,
|
|
34811
|
+
images_url: args.images_url,
|
|
34812
|
+
images_file_uris: args.images_file_uris,
|
|
34237
34813
|
previous_interaction_id: previousInteractionId
|
|
34238
34814
|
});
|
|
34239
34815
|
return client2.session.jobs.dispatch({ toolName: "gemini_music_generate", fingerprint, idempotencyKey: args.idempotency_key, async: args.async }, async () => {
|
|
34240
|
-
const inputs = await
|
|
34816
|
+
const { inputs, report } = await resolveImageInputs(args, client2);
|
|
34241
34817
|
const r = await withProgressHeartbeat(extra, `Generating music (${model})`, () => client2.generateMusic({
|
|
34242
34818
|
input: args.prompt,
|
|
34243
34819
|
images: inputs.length ? inputs : void 0,
|
|
@@ -34250,6 +34826,7 @@ function registerMusicTools(server, client2) {
|
|
|
34250
34826
|
const meta3 = { model, interaction_id: r.id };
|
|
34251
34827
|
if (previousInteractionId) meta3.previous_interaction_id = previousInteractionId;
|
|
34252
34828
|
if (r.text) meta3.text = r.text;
|
|
34829
|
+
if (report) meta3.image_inputs = report;
|
|
34253
34830
|
meta3.hint = `To continue this track, call gemini_music_generate again with previous_interaction_id: "${r.id}" (or continue_last: true).`;
|
|
34254
34831
|
const slug = args.filename ? baseName(args.filename) : slugify2(args.prompt);
|
|
34255
34832
|
const named = r.audios.map((media, i) => ({
|
|
@@ -34278,12 +34855,152 @@ function registerJobTools(server, client2) {
|
|
|
34278
34855
|
);
|
|
34279
34856
|
}
|
|
34280
34857
|
|
|
34858
|
+
// src/tools/files.ts
|
|
34859
|
+
init_zod();
|
|
34860
|
+
init_dist();
|
|
34861
|
+
var UPLOAD_URL_MAX_BYTES_DISK = 100 * 1024 * 1024;
|
|
34862
|
+
var UPLOAD_URL_MAX_BYTES_HOSTED = 25 * 1024 * 1024;
|
|
34863
|
+
var UPLOAD_ACCEPT = ["image/", "video/", "audio/"];
|
|
34864
|
+
var RETENTION_NOTE = "Gemini retains uploaded files for ~48h. Until then this reference can be reused across any number of calls (images_file_uris on gemini_image_generate / _edit / _set / gemini_interact, or video_url) without re-sending the bytes.";
|
|
34865
|
+
function uploadResult(file2) {
|
|
34866
|
+
return textResult({
|
|
34867
|
+
// The `files/<id>` form is what every tool parameter documents, so it leads.
|
|
34868
|
+
file_uri: file2.name,
|
|
34869
|
+
// The absolute uri, for callers that would rather pass it through verbatim.
|
|
34870
|
+
uri: file2.uri,
|
|
34871
|
+
name: file2.name,
|
|
34872
|
+
mime_type: file2.mimeType,
|
|
34873
|
+
...file2.sizeBytes ? { size_bytes: Number(file2.sizeBytes) } : {},
|
|
34874
|
+
...file2.displayName ? { display_name: file2.displayName } : {},
|
|
34875
|
+
expires: file2.expirationTime ?? "~48h from upload",
|
|
34876
|
+
hint: `Pass "${file2.name}" in images_file_uris (or video_url for video) on any image tool. ${RETENTION_NOTE}`
|
|
34877
|
+
});
|
|
34878
|
+
}
|
|
34879
|
+
function registerFileTools(server, client2) {
|
|
34880
|
+
const onDisk = client2.mediaSink?.persistsFiles ?? true;
|
|
34881
|
+
const urlMaxBytes = onDisk ? UPLOAD_URL_MAX_BYTES_DISK : UPLOAD_URL_MAX_BYTES_HOSTED;
|
|
34882
|
+
server.registerTool(
|
|
34883
|
+
"gemini_upload_file",
|
|
34884
|
+
{
|
|
34885
|
+
description: "Upload an image (or video/audio) to the Gemini Files API ONCE and get back a reusable `files/<id>` reference. 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."),
|
|
34886
|
+
annotations: { readOnlyHint: false, openWorldHint: true },
|
|
34887
|
+
inputSchema: {
|
|
34888
|
+
url: external_exports.string().url().optional().describe(
|
|
34889
|
+
`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.")
|
|
34890
|
+
),
|
|
34891
|
+
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)."),
|
|
34892
|
+
path: external_exports.string().min(1).optional().describe(
|
|
34893
|
+
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."
|
|
34894
|
+
),
|
|
34895
|
+
mime_type: external_exports.string().regex(/^[\w.+-]+\/[\w.+-]+$/, "must be a MIME type like image/png").optional().describe("Override the detected MIME type (sniffed from the bytes / taken from the server response otherwise)"),
|
|
34896
|
+
display_name: external_exports.string().min(1).optional().describe("Human-readable name recorded against the upload"),
|
|
34897
|
+
confirm: schemaConfirm
|
|
34898
|
+
}
|
|
34899
|
+
},
|
|
34900
|
+
async (args, extra) => {
|
|
34901
|
+
const sources = [args.url && "url", args.data_base64 && "data_base64", args.path && "path"].filter(Boolean);
|
|
34902
|
+
if (sources.length !== 1) {
|
|
34903
|
+
throw new McpToolError(
|
|
34904
|
+
sources.length === 0 ? "Provide exactly one of `url`, `data_base64`" + (onDisk ? " or `path`." : ".") : `Provide exactly one source, not ${sources.length} (${sources.join(", ")}).`
|
|
34905
|
+
);
|
|
34906
|
+
}
|
|
34907
|
+
if (args.path) {
|
|
34908
|
+
if (!onDisk) {
|
|
34909
|
+
throw new McpToolError(
|
|
34910
|
+
"`path` is unavailable on the hosted connector: it runs on a Cloudflare Worker, which has no filesystem. Use `url` (the server downloads it), `data_base64`, or POST the raw bytes to /upload with the same Authorization header.",
|
|
34911
|
+
{ hint: "Use `url`, `data_base64`, or POST /upload." }
|
|
34912
|
+
);
|
|
34913
|
+
}
|
|
34914
|
+
const gate = await previewLocalInputsUnlessConfirmed(
|
|
34915
|
+
args.confirm,
|
|
34916
|
+
"Upload a local file to the Gemini Files API",
|
|
34917
|
+
"/upload/v1beta/files",
|
|
34918
|
+
[args.path]
|
|
34919
|
+
);
|
|
34920
|
+
if (gate) return gate;
|
|
34921
|
+
}
|
|
34922
|
+
const uploaded = await withProgressHeartbeat(extra, "Uploading to the Gemini Files API", async () => {
|
|
34923
|
+
if (args.url) {
|
|
34924
|
+
const fetched = await client2.fetchRemoteImage(args.url, { maxBytes: urlMaxBytes, accept: UPLOAD_ACCEPT });
|
|
34925
|
+
return client2.uploadBytes(
|
|
34926
|
+
fetched.bytes,
|
|
34927
|
+
args.mime_type ?? fetched.mimeType,
|
|
34928
|
+
// 'upload', matching the data_base64 and path branches below —
|
|
34929
|
+
// this tool takes video and audio too, so 'image' would be a lie.
|
|
34930
|
+
args.display_name ?? fileNameFromUrl(args.url, "upload")
|
|
34931
|
+
);
|
|
34932
|
+
}
|
|
34933
|
+
if (args.data_base64) {
|
|
34934
|
+
const { decodeImageInput: decodeImageInput2 } = await Promise.resolve().then(() => (init_images(), images_exports));
|
|
34935
|
+
const decoded = decodeImageInput2(args.data_base64);
|
|
34936
|
+
const bytes2 = base64ToBytes(decoded.base64);
|
|
34937
|
+
return client2.uploadBytes(bytes2, args.mime_type ?? decoded.mimeType, args.display_name ?? "upload");
|
|
34938
|
+
}
|
|
34939
|
+
const { readImageAsInline: readImageAsInline2, resolveImagePath: resolveImagePath2 } = await Promise.resolve().then(() => (init_images(), images_exports));
|
|
34940
|
+
const resolved = resolveImagePath2(args.path);
|
|
34941
|
+
const inline = await readImageAsInline2(resolved);
|
|
34942
|
+
const bytes = base64ToBytes(inline.base64);
|
|
34943
|
+
return client2.uploadBytes(
|
|
34944
|
+
bytes,
|
|
34945
|
+
args.mime_type ?? inline.mimeType,
|
|
34946
|
+
args.display_name ?? (resolved.split(/[\\/]/).pop() || "upload")
|
|
34947
|
+
);
|
|
34948
|
+
});
|
|
34949
|
+
return uploadResult(uploaded);
|
|
34950
|
+
}
|
|
34951
|
+
);
|
|
34952
|
+
server.registerTool(
|
|
34953
|
+
"gemini_list_files",
|
|
34954
|
+
{
|
|
34955
|
+
description: "List the files currently uploaded to the Gemini Files API under this API key, with their `files/<id>` references, MIME types and expiry times. Retention is ~48h, so an entry that has vanished has expired rather than failed.",
|
|
34956
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
34957
|
+
inputSchema: {
|
|
34958
|
+
page_size: external_exports.number().int().positive().max(100).optional().describe("Maximum files to return (1-100, default 100)")
|
|
34959
|
+
}
|
|
34960
|
+
},
|
|
34961
|
+
async (args) => {
|
|
34962
|
+
const files = await client2.listFiles(args.page_size ?? 100);
|
|
34963
|
+
return textResult({
|
|
34964
|
+
count: files.length,
|
|
34965
|
+
files: files.map((f) => ({
|
|
34966
|
+
file_uri: f.name,
|
|
34967
|
+
uri: f.uri,
|
|
34968
|
+
mime_type: f.mimeType,
|
|
34969
|
+
...f.displayName ? { display_name: f.displayName } : {},
|
|
34970
|
+
...f.sizeBytes ? { size_bytes: Number(f.sizeBytes) } : {},
|
|
34971
|
+
...f.state ? { state: f.state } : {},
|
|
34972
|
+
...f.createTime ? { created: f.createTime } : {},
|
|
34973
|
+
...f.expirationTime ? { expires: f.expirationTime } : {}
|
|
34974
|
+
})),
|
|
34975
|
+
hint: files.length ? `Pass any file_uri above in images_file_uris on an image tool. ${RETENTION_NOTE}` : "No files uploaded (or all have expired \u2014 retention is ~48h). Upload one with gemini_upload_file."
|
|
34976
|
+
});
|
|
34977
|
+
}
|
|
34978
|
+
);
|
|
34979
|
+
server.registerTool(
|
|
34980
|
+
"gemini_delete_file",
|
|
34981
|
+
{
|
|
34982
|
+
description: "Delete an uploaded file from the Gemini Files API before its ~48h expiry. Any tool call still referencing it will then fail with a generic 404, so delete only references you are finished with.",
|
|
34983
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
34984
|
+
inputSchema: {
|
|
34985
|
+
file_uri: external_exports.string().min(1).describe("The `files/<id>` reference (or full uri) to delete"),
|
|
34986
|
+
confirm: schemaConfirm
|
|
34987
|
+
}
|
|
34988
|
+
},
|
|
34989
|
+
async (args) => {
|
|
34990
|
+
const gate = previewUnlessConfirmed(args.confirm, "Delete an uploaded Gemini file", "DELETE", `/v1beta/${args.file_uri}`);
|
|
34991
|
+
if (gate) return gate;
|
|
34992
|
+
await client2.deleteFile(args.file_uri);
|
|
34993
|
+
return textResult({ deleted: args.file_uri });
|
|
34994
|
+
}
|
|
34995
|
+
);
|
|
34996
|
+
}
|
|
34997
|
+
|
|
34281
34998
|
// src/index.ts
|
|
34282
34999
|
await loadStdioDotenv();
|
|
34283
35000
|
await runMcp({
|
|
34284
35001
|
name: "gemini-mcp",
|
|
34285
35002
|
version: VERSION,
|
|
34286
35003
|
banner: "[gemini-mcp] This project was developed and is maintained by AI (Claude). Use at your own discretion.",
|
|
34287
|
-
tools: [registerModelTools, registerGenerateTools, registerSetTools, registerInteractTools, registerVideoTools, registerMusicTools, registerJobTools],
|
|
35004
|
+
tools: [registerModelTools, registerGenerateTools, registerSetTools, registerInteractTools, registerVideoTools, registerMusicTools, registerJobTools, registerFileTools],
|
|
34288
35005
|
deps: client
|
|
34289
35006
|
});
|