@chrischall/gemini-mcp 1.0.3 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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 tooBig = input.data.length > check2.value;
1112
+ const tooBig2 = input.data.length > check2.value;
1113
1113
  const tooSmall = input.data.length < check2.value;
1114
- if (tooBig || tooSmall) {
1114
+ if (tooBig2 || tooSmall) {
1115
1115
  ctx = this._getOrReturnCtx(input, ctx);
1116
- if (tooBig) {
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 tooBig = check2.inclusive ? input.data > check2.value : input.data >= check2.value;
1664
- if (tooBig) {
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 tooBig = check2.inclusive ? input.data > check2.value : input.data >= check2.value;
1881
- if (tooBig) {
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 tooBig = ctx.data.length > def.exactLength.value;
2295
+ const tooBig2 = ctx.data.length > def.exactLength.value;
2296
2296
  const tooSmall = ctx.data.length < def.exactLength.value;
2297
- if (tooBig || tooSmall) {
2297
+ if (tooBig2 || tooSmall) {
2298
2298
  addIssueToContext(ctx, {
2299
- code: tooBig ? ZodIssueCode.too_big : ZodIssueCode.too_small,
2299
+ code: tooBig2 ? ZodIssueCode.too_big : ZodIssueCode.too_small,
2300
2300
  minimum: tooSmall ? def.exactLength.value : void 0,
2301
- maximum: tooBig ? def.exactLength.value : void 0,
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 tooBig = size > def.size;
5465
+ const tooBig2 = size > def.size;
5466
5466
  payload.issues.push({
5467
5467
  origin: getSizableOrigin(input),
5468
- ...tooBig ? { code: "too_big", maximum: def.size } : { code: "too_small", minimum: def.size },
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 tooBig = length > def.length;
5554
+ const tooBig2 = length > def.length;
5555
5555
  payload.issues.push({
5556
5556
  origin,
5557
- ...tooBig ? { code: "too_big", maximum: def.length } : { code: "too_small", minimum: def.length },
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 resolveUrl(resolver, baseId, id) {
25672
+ function resolveUrl2(resolver, baseId, id) {
25673
25673
  id = normalizeId(id);
25674
25674
  return resolver.resolve(baseId, id);
25675
25675
  }
25676
- exports.resolveUrl = 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")
@@ -27186,6 +27186,7 @@ var require_fast_uri = __commonJS({
27186
27186
  return uriTokens.join("");
27187
27187
  }
27188
27188
  var URI_PARSE = /^(?:([^#/:?]+):)?(?:\/\/((?:([^#/?@]*)@)?(\[[^#/?\]]+\]|[^#/:?]*)(?::(\d*))?))?([^#?]*)(?:\?([^#]*))?(?:#((?:.|[\n\r])*))?/u;
27189
+ var AUTHORITY_PREFIX = /^(?:[^#/:?]+:)?\/\/([^/?#]*)/;
27189
27190
  function getParseError(parsed, matches) {
27190
27191
  if (matches[2] !== void 0 && parsed.path && parsed.path[0] !== "/") {
27191
27192
  return 'URI path must start with "/" when authority is present.';
@@ -27215,6 +27216,11 @@ var require_fast_uri = __commonJS({
27215
27216
  uri = "//" + uri;
27216
27217
  }
27217
27218
  }
27219
+ const authorityMatch = uri.match(AUTHORITY_PREFIX);
27220
+ if (authorityMatch !== null && authorityMatch[1].indexOf("\\") !== -1) {
27221
+ parsed.error = "URI authority must not contain a literal backslash.";
27222
+ malformedAuthorityOrPort = true;
27223
+ }
27218
27224
  const matches = uri.match(URI_PARSE);
27219
27225
  if (matches) {
27220
27226
  parsed.scheme = matches[1];
@@ -27258,7 +27264,7 @@ var require_fast_uri = __commonJS({
27258
27264
  if (!options.unicodeSupport && (!schemeHandler || !schemeHandler.unicodeSupport)) {
27259
27265
  if (parsed.host && (options.domainHost || schemeHandler && schemeHandler.domainHost) && isIP === false && nonSimpleDomain(parsed.host)) {
27260
27266
  try {
27261
- parsed.host = URL.domainToASCII(parsed.host.toLowerCase());
27267
+ parsed.host = new URL("http://" + parsed.host).hostname;
27262
27268
  } catch (e) {
27263
27269
  parsed.error = parsed.error || "Host's domain name can not be converted to ASCII: " + e;
27264
27270
  }
@@ -32802,7 +32808,7 @@ async function loadStdioDotenv() {
32802
32808
  }
32803
32809
 
32804
32810
  // src/version.ts
32805
- var VERSION = "1.0.3";
32811
+ var VERSION = "1.1.0";
32806
32812
 
32807
32813
  // src/tools/models.ts
32808
32814
  init_dist();
@@ -32842,6 +32848,355 @@ function filterImageModels(raw) {
32842
32848
  // src/tools/generate.ts
32843
32849
  init_images();
32844
32850
 
32851
+ // src/inputs.ts
32852
+ init_dist();
32853
+
32854
+ // src/fetch-image.ts
32855
+ init_dist();
32856
+ var IMAGE_URL_MAX_BYTES = 15 * 1024 * 1024;
32857
+ var IMAGE_INLINE_MAX_BYTES = 6 * 1024 * 1024;
32858
+ var MAX_REDIRECTS = 5;
32859
+ var DEFAULT_FETCH_TIMEOUT_MS = 6e4;
32860
+ function ipv4Octets(host) {
32861
+ const parts = host.split(".");
32862
+ if (parts.length !== 4) return void 0;
32863
+ const octets = parts.map((p) => /^\d{1,3}$/.test(p) ? Number(p) : NaN);
32864
+ return octets.every((n) => Number.isInteger(n) && n >= 0 && n <= 255) ? octets : void 0;
32865
+ }
32866
+ function isPrivateIpv4([a, b]) {
32867
+ if (a === 0 || a === 127) return true;
32868
+ if (a === 10) return true;
32869
+ if (a === 172 && b >= 16 && b <= 31) return true;
32870
+ if (a === 192 && b === 168) return true;
32871
+ if (a === 169 && b === 254) return true;
32872
+ if (a === 100 && b >= 64 && b <= 127) return true;
32873
+ if (a === 192 && b === 0) return true;
32874
+ if (a === 198 && (b === 18 || b === 19)) return true;
32875
+ if (a >= 224) return true;
32876
+ return false;
32877
+ }
32878
+ function ipv6Groups(host) {
32879
+ if (!host.includes(":")) return void 0;
32880
+ let text = host;
32881
+ const tail = [];
32882
+ const dotted = /:((?:\d{1,3}\.){3}\d{1,3})$/.exec(text);
32883
+ if (dotted) {
32884
+ const octets = ipv4Octets(dotted[1]);
32885
+ if (!octets) return void 0;
32886
+ tail.push(octets[0] << 8 | octets[1], octets[2] << 8 | octets[3]);
32887
+ text = text.slice(0, dotted.index);
32888
+ }
32889
+ const halves = text.split("::");
32890
+ if (halves.length > 2) return void 0;
32891
+ const parse3 = (part) => {
32892
+ if (part === "") return [];
32893
+ const out = [];
32894
+ for (const piece of part.split(":")) {
32895
+ if (!/^[0-9a-f]{1,4}$/.test(piece)) return void 0;
32896
+ out.push(parseInt(piece, 16));
32897
+ }
32898
+ return out;
32899
+ };
32900
+ const head = parse3(halves[0]);
32901
+ const rest = halves.length === 2 ? parse3(halves[1]) : [];
32902
+ if (!head || !rest) return void 0;
32903
+ const explicit = [...head, ...rest, ...tail];
32904
+ if (halves.length === 2) {
32905
+ if (explicit.length > 7) return void 0;
32906
+ return [...head, ...Array(8 - explicit.length).fill(0), ...rest, ...tail];
32907
+ }
32908
+ return explicit.length === 8 ? explicit : void 0;
32909
+ }
32910
+ var V4_BEARING_PREFIXES = [
32911
+ [0, 0, 0, 0, 0, 65535],
32912
+ // ::ffff:0:0/96 — IPv4-mapped
32913
+ [0, 0, 0, 0, 0, 0],
32914
+ // ::/96 — IPv4-compatible (deprecated)
32915
+ [0, 0, 0, 0, 65535, 0],
32916
+ // ::ffff:0:0:0/96 — IPv4-translated (deprecated SIIT)
32917
+ [100, 65435, 0, 0, 0, 0]
32918
+ // 64:ff9b::/96 — NAT64 well-known (RFC 6052)
32919
+ ];
32920
+ function isPrivateIpv6(groups) {
32921
+ if (groups.every((g) => g === 0)) return true;
32922
+ if (groups.slice(0, 7).every((g) => g === 0) && groups[7] === 1) return true;
32923
+ if ((groups[0] & 65472) === 65152) return true;
32924
+ if ((groups[0] & 65024) === 64512) return true;
32925
+ if (V4_BEARING_PREFIXES.some((prefix) => prefix.every((g, i) => groups[i] === g))) {
32926
+ return isPrivateIpv4([groups[6] >> 8, groups[6] & 255, groups[7] >> 8, groups[7] & 255]);
32927
+ }
32928
+ return false;
32929
+ }
32930
+ function assertPublicHttpsUrl(raw, requested) {
32931
+ let url2;
32932
+ try {
32933
+ url2 = new URL(raw);
32934
+ } catch {
32935
+ throw new McpToolError(`Not a valid URL: ${raw}`, {
32936
+ hint: "images_url entries must be absolute https:// URLs."
32937
+ });
32938
+ }
32939
+ if (url2.protocol !== "https:") {
32940
+ throw new McpToolError(`Refusing to fetch ${describeUrl(raw, requested)}: only https:// URLs are fetched (got "${url2.protocol}").`, {
32941
+ hint: "Use an https:// URL, or send the bytes with images_base64 / gemini_upload_file."
32942
+ });
32943
+ }
32944
+ const rawHost = url2.hostname.toLowerCase();
32945
+ const isIpv6Literal = rawHost.startsWith("[") && rawHost.endsWith("]");
32946
+ const host = isIpv6Literal ? rawHost.slice(1, -1) : rawHost;
32947
+ const v4 = isIpv6Literal ? void 0 : ipv4Octets(host);
32948
+ const v6 = isIpv6Literal ? ipv6Groups(host) : void 0;
32949
+ const blocked = isIpv6Literal ? v6 === void 0 || isPrivateIpv6(v6) : host === "localhost" || host.endsWith(".localhost") || host.endsWith(".local") || (v4 ? isPrivateIpv4(v4) : false);
32950
+ if (blocked) {
32951
+ throw new McpToolError(
32952
+ `Refusing to fetch ${describeUrl(raw, requested)}: "${url2.hostname}" is a private, loopback or link-local address, not a public host.`,
32953
+ { hint: "images_url is fetched by the server, so it must point at a publicly reachable https:// image." }
32954
+ );
32955
+ }
32956
+ return url2;
32957
+ }
32958
+ function describeUrl(current, requested) {
32959
+ return current === requested ? current : `${current} (redirected from ${requested})`;
32960
+ }
32961
+ async function readCapped(res, maxBytes, url2, requested) {
32962
+ const declared = Number(res.headers.get("content-length"));
32963
+ if (Number.isFinite(declared) && declared > maxBytes) {
32964
+ throw tooBig(url2, requested, declared, maxBytes);
32965
+ }
32966
+ const reader = res.body?.getReader();
32967
+ if (!reader) {
32968
+ const buf = new Uint8Array(await res.arrayBuffer());
32969
+ if (buf.byteLength > maxBytes) throw tooBig(url2, requested, buf.byteLength, maxBytes);
32970
+ return buf;
32971
+ }
32972
+ const chunks = [];
32973
+ let total = 0;
32974
+ try {
32975
+ for (; ; ) {
32976
+ const { done, value } = await reader.read();
32977
+ if (done) break;
32978
+ if (!value) continue;
32979
+ total += value.byteLength;
32980
+ if (total > maxBytes) {
32981
+ await reader.cancel().catch(() => {
32982
+ });
32983
+ throw tooBig(url2, requested, total, maxBytes, true);
32984
+ }
32985
+ chunks.push(value);
32986
+ }
32987
+ } finally {
32988
+ reader.releaseLock?.();
32989
+ }
32990
+ const out = new Uint8Array(total);
32991
+ let offset = 0;
32992
+ for (const c of chunks) {
32993
+ out.set(c, offset);
32994
+ offset += c.byteLength;
32995
+ }
32996
+ return out;
32997
+ }
32998
+ function fetchFailure(err, current, requested, timeoutMs) {
32999
+ const name = err?.name;
33000
+ if (name === "TimeoutError" || name === "AbortError") {
33001
+ return new McpToolError(`Fetching ${describeUrl(current, requested)} timed out after ${timeoutMs}ms.`, {
33002
+ hint: "The server accepted the connection but did not finish sending the image. Try a different URL, or raise timeout_ms."
33003
+ });
33004
+ }
33005
+ return new McpToolError(
33006
+ `Could not fetch ${describeUrl(current, requested)}: ${err instanceof Error ? err.message : String(err)}`,
33007
+ { hint: "Check the URL is reachable from the server and serves the image without authentication." }
33008
+ );
33009
+ }
33010
+ function tooBig(url2, requested, size, max, partial2 = false) {
33011
+ const mb2 = (n) => `${(n / (1024 * 1024)).toFixed(1)} MB`;
33012
+ return new McpToolError(
33013
+ `Image at ${describeUrl(url2, requested)} is ${partial2 ? "over" : ""} ${mb2(size)}, above the ${mb2(max)} limit for images_url.`,
33014
+ { hint: "Resize or re-encode the image, or upload it yourself with gemini_upload_file and pass the returned file_uri." }
33015
+ );
33016
+ }
33017
+ async function fetchRemoteImage(requestedUrl, opts = {}) {
33018
+ const doFetch = opts.fetchImpl ?? fetch;
33019
+ const maxBytes = opts.maxBytes ?? IMAGE_URL_MAX_BYTES;
33020
+ const accept = opts.accept ?? ["image/"];
33021
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
33022
+ let current = requestedUrl;
33023
+ for (let hop = 0; ; hop++) {
33024
+ const url2 = assertPublicHttpsUrl(current, requestedUrl);
33025
+ const signal = timeoutMs > 0 ? AbortSignal.timeout(timeoutMs) : void 0;
33026
+ let res;
33027
+ try {
33028
+ res = await doFetch(url2.toString(), {
33029
+ redirect: "manual",
33030
+ headers: { accept: accept.map((p) => `${p.replace(/\/$/, "")}/*`).join(", ") },
33031
+ ...signal ? { signal } : {}
33032
+ });
33033
+ } catch (err) {
33034
+ throw fetchFailure(err, current, requestedUrl, timeoutMs);
33035
+ }
33036
+ if (res.status >= 300 && res.status < 400) {
33037
+ const location = res.headers.get("location");
33038
+ if (!location) {
33039
+ throw new McpToolError(`Fetching ${describeUrl(current, requestedUrl)} returned HTTP ${res.status} with no Location header.`);
33040
+ }
33041
+ if (hop >= MAX_REDIRECTS) {
33042
+ throw new McpToolError(`Fetching ${requestedUrl} exceeded ${MAX_REDIRECTS} redirects (last hop: ${location}).`);
33043
+ }
33044
+ current = new URL(location, url2).toString();
33045
+ continue;
33046
+ }
33047
+ if (!res.ok) {
33048
+ throw new McpToolError(`Fetching ${describeUrl(current, requestedUrl)} returned HTTP ${res.status} ${res.statusText || ""}`.trim(), {
33049
+ hint: "The URL must be publicly readable \u2014 a login page or signed-URL expiry shows up here as 401/403/404."
33050
+ });
33051
+ }
33052
+ const contentType = (res.headers.get("content-type") ?? "").split(";")[0].trim().toLowerCase();
33053
+ if (!accept.some((prefix) => contentType.startsWith(prefix))) {
33054
+ const expected = accept.map((p) => `${p.replace(/\/$/, "")}/*`).join(" or ");
33055
+ throw new McpToolError(
33056
+ `${describeUrl(current, requestedUrl)} is not ${accept.length === 1 && accept[0] === "image/" ? "an image" : "usable media"}: the server returned Content-Type "${contentType || "(none)"}", expected ${expected}.`,
33057
+ { hint: "Link directly to the media file, not to the page that displays it." }
33058
+ );
33059
+ }
33060
+ let bytes;
33061
+ try {
33062
+ bytes = await readCapped(res, maxBytes, current, requestedUrl);
33063
+ } catch (err) {
33064
+ if (err instanceof McpToolError) throw err;
33065
+ throw fetchFailure(err, current, requestedUrl, timeoutMs);
33066
+ }
33067
+ if (bytes.byteLength === 0) {
33068
+ throw new McpToolError(`${describeUrl(current, requestedUrl)} returned an empty body.`);
33069
+ }
33070
+ return { bytes, mimeType: contentType, size: bytes.byteLength, finalUrl: current, requestedUrl };
33071
+ }
33072
+ }
33073
+
33074
+ // src/inputs.ts
33075
+ function hasImageInput(args) {
33076
+ return Boolean(
33077
+ args.images?.length || args.images_base64?.length || args.images_url?.length || args.images_file_uris?.length || args.from_clipboard
33078
+ );
33079
+ }
33080
+ var ASSUMED_TTL_MS = 47 * 60 * 60 * 1e3;
33081
+ function expiresAtMs(expirationTime, now) {
33082
+ if (expirationTime) {
33083
+ const t = Date.parse(expirationTime);
33084
+ if (Number.isFinite(t)) return t;
33085
+ }
33086
+ return now + ASSUMED_TTL_MS;
33087
+ }
33088
+ async function resolveImageInputs(args, client2) {
33089
+ const report = {};
33090
+ const inputs = [];
33091
+ const onDisk = client2.mediaSink?.persistsFiles ?? true;
33092
+ if (args.from_clipboard) {
33093
+ inputs.push(await (await Promise.resolve().then(() => (init_clipboard(), clipboard_exports))).readClipboardImage());
33094
+ }
33095
+ for (const path of args.images ?? []) {
33096
+ inputs.push(await resolveLocalPath(path, client2, onDisk, report));
33097
+ }
33098
+ if (args.images_base64?.length) {
33099
+ const { decodeImageInput: decodeImageInput2 } = await Promise.resolve().then(() => (init_images(), images_exports));
33100
+ for (const b64 of args.images_base64) inputs.push(decodeImageInput2(b64));
33101
+ }
33102
+ const fetched = /* @__PURE__ */ new Map();
33103
+ for (const url2 of args.images_url ?? []) {
33104
+ const hit = fetched.get(url2);
33105
+ if (hit) {
33106
+ inputs.push(hit);
33107
+ continue;
33108
+ }
33109
+ const resolved = await resolveUrl(url2, client2, report);
33110
+ fetched.set(url2, resolved);
33111
+ inputs.push(resolved);
33112
+ }
33113
+ const seenUris = /* @__PURE__ */ new Map();
33114
+ for (const ref of args.images_file_uris ?? []) {
33115
+ const hit = seenUris.get(ref);
33116
+ if (hit) {
33117
+ inputs.push(hit);
33118
+ continue;
33119
+ }
33120
+ const file2 = await client2.getFile(ref);
33121
+ const input = { uri: file2.uri, mimeType: file2.mimeType };
33122
+ seenUris.set(ref, input);
33123
+ (report.file_uris ??= []).push(file2.name);
33124
+ inputs.push(input);
33125
+ }
33126
+ return { inputs, report: Object.keys(report).length > 0 ? report : void 0 };
33127
+ }
33128
+ async function resolveLocalPath(path, client2, onDisk, report) {
33129
+ const { readImageAsInline: readImageAsInline2, resolveImagePath: resolveImagePath2 } = await Promise.resolve().then(() => (init_images(), images_exports));
33130
+ if (!onDisk) return readImageAsInline2(path);
33131
+ const { stat: stat3 } = await import("node:fs/promises");
33132
+ const resolved = resolveImagePath2(path);
33133
+ let key;
33134
+ try {
33135
+ const { mtimeMs, size } = await stat3(resolved);
33136
+ key = `path\0${resolved}\0${mtimeMs}\0${size}`;
33137
+ } catch {
33138
+ return readImageAsInline2(path);
33139
+ }
33140
+ const cached2 = client2.session.cachedUpload(key);
33141
+ if (cached2) return { uri: cached2.uri, mimeType: cached2.mimeType };
33142
+ const references = client2.session.countReference(key);
33143
+ const inline = await readImageAsInline2(resolved);
33144
+ if (references < 2) return inline;
33145
+ try {
33146
+ const bytes = Uint8Array.from(atob(inline.base64), (c) => c.charCodeAt(0));
33147
+ const displayName = resolved.split(/[\\/]/).pop() || "image";
33148
+ const uploaded = await client2.uploadBytes(bytes, inline.mimeType, displayName);
33149
+ client2.session.uploadCache.set(key, {
33150
+ name: uploaded.name,
33151
+ uri: uploaded.uri,
33152
+ mimeType: uploaded.mimeType,
33153
+ expiresAtMs: expiresAtMs(uploaded.expirationTime, Date.now())
33154
+ });
33155
+ (report.auto_uploaded ??= []).push({ path: resolved, file_uri: uploaded.name, expires: uploaded.expirationTime });
33156
+ return { uri: uploaded.uri, mimeType: uploaded.mimeType };
33157
+ } catch {
33158
+ return inline;
33159
+ }
33160
+ }
33161
+ async function resolveUrl(url2, client2, report) {
33162
+ const fetched = await client2.fetchRemoteImage(url2);
33163
+ const entry = {
33164
+ url: url2,
33165
+ mime_type: fetched.mimeType,
33166
+ bytes: fetched.size
33167
+ };
33168
+ (report.fetched_urls ??= []).push(entry);
33169
+ if (fetched.size <= IMAGE_INLINE_MAX_BYTES) {
33170
+ return { base64: encodeBase64(fetched.bytes), mimeType: fetched.mimeType };
33171
+ }
33172
+ const displayName = fileNameFromUrl(url2);
33173
+ const uploaded = await client2.uploadBytes(fetched.bytes, fetched.mimeType, displayName);
33174
+ entry.file_uri = uploaded.name;
33175
+ return { uri: uploaded.uri, mimeType: uploaded.mimeType };
33176
+ }
33177
+ function fileNameFromUrl(url2) {
33178
+ try {
33179
+ const name = new URL(url2).pathname.split("/").filter(Boolean).pop();
33180
+ return name ? decodeURIComponent(name).slice(0, 120) : "image";
33181
+ } catch {
33182
+ return "image";
33183
+ }
33184
+ }
33185
+ function encodeBase64(bytes) {
33186
+ let binary = "";
33187
+ const CHUNK = 32768;
33188
+ for (let i = 0; i < bytes.length; i += CHUNK) {
33189
+ binary += String.fromCharCode(...bytes.subarray(i, i + CHUNK));
33190
+ }
33191
+ return btoa(binary);
33192
+ }
33193
+ function requireImageInput(args) {
33194
+ if (hasImageInput(args)) return;
33195
+ throw new McpToolError(
33196
+ "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`."
33197
+ );
33198
+ }
33199
+
32845
33200
  // src/tools/shared.ts
32846
33201
  init_zod();
32847
33202
  init_dist();
@@ -32877,6 +33232,16 @@ var idempotencyKeySchema = external_exports.string().min(1).optional().describe(
32877
33232
  var asyncSchema = external_exports.boolean().optional().describe(
32878
33233
  "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)."
32879
33234
  );
33235
+ function imagesUrlSchema(label = "Reference images") {
33236
+ return external_exports.array(external_exports.string().url()).optional().describe(
33237
+ `${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 ${Math.round(IMAGE_URL_MAX_BYTES / (1024 * 1024))}MB each; must be a directly-linked image (Content-Type image/*).`
33238
+ );
33239
+ }
33240
+ function imagesFileUrisSchema(label = "Reference images") {
33241
+ return external_exports.array(external_exports.string().min(1)).optional().describe(
33242
+ `${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.`
33243
+ );
33244
+ }
32880
33245
  var sharedImageSchema = {
32881
33246
  // Bare id only: the model is interpolated into the URL path
32882
33247
  // (`/models/${model}:generateContent`), so slashes/colons/queries must be
@@ -32998,7 +33363,7 @@ function assertLocalInputsAvailable(sink, args) {
32998
33363
  if (args.from_clipboard) unavailable.push("`from_clipboard` (reads the macOS clipboard via child_process)");
32999
33364
  if (args.video_path) unavailable.push("`video_path` (uploads a file streamed from disk)");
33000
33365
  if (unavailable.length === 0) return;
33001
- const fix = "Send image bytes as `images_base64` (raw base64 or a data: URI) instead of `images`/`master_images`/`from_clipboard`, and reference video by `video_url` (a public YouTube URL, or a files/\u2026 uri uploaded elsewhere) instead of `video_path`.";
33366
+ 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`.";
33002
33367
  throw new McpToolError(
33003
33368
  `${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}`,
33004
33369
  { hint: fix }
@@ -33191,7 +33556,9 @@ function registerGenerateTools(server, client2) {
33191
33556
  count: external_exports.number().int().positive().max(8).optional().describe("Number of independent images (default 1)"),
33192
33557
  filename: external_exports.string().optional().describe("Base filename for the output image (extension stripped; default: slugified prompt)"),
33193
33558
  images: external_exports.array(external_exports.string().min(1)).optional().describe("Paths to reference input images (image-conditioned generation)"),
33194
- images_base64: external_exports.array(external_exports.string().min(1)).optional().describe("Reference images as base64 strings or data URIs"),
33559
+ images_url: imagesUrlSchema(),
33560
+ images_file_uris: imagesFileUrisSchema(),
33561
+ 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"),
33195
33562
  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)"),
33196
33563
  video_path: videoPathSchema,
33197
33564
  confirm: schemaConfirm,
@@ -33215,13 +33582,15 @@ function registerGenerateTools(server, client2) {
33215
33582
  images: args.images,
33216
33583
  images_base64: args.images_base64,
33217
33584
  from_clipboard: args.from_clipboard,
33585
+ images_url: args.images_url,
33586
+ images_file_uris: args.images_file_uris,
33218
33587
  video_url: args.video_url,
33219
33588
  video_path: args.video_path
33220
33589
  });
33221
33590
  return client2.session.jobs.dispatch({ toolName: "gemini_image_generate", fingerprint, idempotencyKey: args.idempotency_key, async: args.async }, async () => {
33222
33591
  const seed = pickSeed(args.seed);
33223
33592
  const slug = args.filename ? baseName(args.filename) : slugify2(args.prompt);
33224
- const refInputs = await gatherImageInputs({ images: args.images, images_base64: args.images_base64, from_clipboard: args.from_clipboard });
33593
+ const { inputs: refInputs, report } = await withProgressHeartbeat(extra, "Resolving reference images", () => resolveImageInputs(args, client2));
33225
33594
  const video = await withProgressHeartbeat(extra, "Uploading video to the Gemini Files API", () => resolveVideoInput(args, client2));
33226
33595
  const named = [];
33227
33596
  let capturedText;
@@ -33250,6 +33619,7 @@ function registerGenerateTools(server, client2) {
33250
33619
  const meta3 = buildMeta(model, seed, args);
33251
33620
  if (capturedText) meta3.text = capturedText;
33252
33621
  if (capturedGrounding) meta3.grounding = capturedGrounding;
33622
+ if (report) meta3.image_inputs = report;
33253
33623
  if (video.videoFileMeta) meta3.video_file = video.videoFileMeta;
33254
33624
  meta3.hint = REFINE_HINT;
33255
33625
  const risk = timeoutRiskHint({ model, imageSize: args.image_size, count, persistsFiles: client2.mediaSink?.persistsFiles });
@@ -33266,7 +33636,9 @@ function registerGenerateTools(server, client2) {
33266
33636
  inputSchema: {
33267
33637
  prompt: external_exports.string().min(1).describe("Instruction describing the edit or composition"),
33268
33638
  images: external_exports.array(external_exports.string().min(1)).optional().describe("Paths to input image file(s) (1 = edit, 2+ = compose)"),
33269
- images_base64: external_exports.array(external_exports.string().min(1)).optional().describe("Input images as base64 strings or data URIs"),
33639
+ images_url: imagesUrlSchema("Input images"),
33640
+ images_file_uris: imagesFileUrisSchema("Input images"),
33641
+ 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"),
33270
33642
  filename: external_exports.string().optional().describe("Base filename for the output image (extension stripped; default: slugified prompt)"),
33271
33643
  confirm: schemaConfirm,
33272
33644
  ...sharedImageSchema
@@ -33274,11 +33646,7 @@ function registerGenerateTools(server, client2) {
33274
33646
  },
33275
33647
  async (args, extra) => {
33276
33648
  assertLocalInputsAvailable(client2.mediaSink, args);
33277
- const hasPaths = (args.images?.length ?? 0) > 0;
33278
- const hasBase64 = (args.images_base64?.length ?? 0) > 0;
33279
- if (!hasPaths && !hasBase64 && !args.from_clipboard) {
33280
- throw new McpToolError("Provide at least one input image via `images`, `images_base64`, or `from_clipboard`.");
33281
- }
33649
+ requireImageInput(args);
33282
33650
  const gate = await previewLocalInputsUnlessConfirmed(args.confirm, SEND_LOCAL_ACTION, GENERATE_ENDPOINT, args.images);
33283
33651
  if (gate) return gate;
33284
33652
  const model = resolveModel(args.model, readEnvVar("GEMINI_IMAGE_MODEL"));
@@ -33291,12 +33659,14 @@ function registerGenerateTools(server, client2) {
33291
33659
  google_search: args.google_search,
33292
33660
  images: args.images,
33293
33661
  images_base64: args.images_base64,
33294
- from_clipboard: args.from_clipboard
33662
+ from_clipboard: args.from_clipboard,
33663
+ images_url: args.images_url,
33664
+ images_file_uris: args.images_file_uris
33295
33665
  });
33296
33666
  return client2.session.jobs.dispatch({ toolName: "gemini_image_edit", fingerprint, idempotencyKey: args.idempotency_key, async: args.async }, async () => {
33297
33667
  const seed = pickSeed(args.seed);
33298
33668
  const slug = args.filename ? baseName(args.filename) : slugify2(args.prompt);
33299
- const inputs = await gatherImageInputs({ images: args.images, images_base64: args.images_base64, from_clipboard: args.from_clipboard });
33669
+ const { inputs, report } = await withProgressHeartbeat(extra, "Resolving input images", () => resolveImageInputs(args, client2));
33300
33670
  const result = await withProgressHeartbeat(extra, `Editing image (${model})`, () => client2.generate({
33301
33671
  prompt: args.prompt,
33302
33672
  images: inputs,
@@ -33312,6 +33682,7 @@ function registerGenerateTools(server, client2) {
33312
33682
  const meta3 = buildMeta(model, seed, args);
33313
33683
  if (text) meta3.text = text;
33314
33684
  if (result.grounding) meta3.grounding = result.grounding;
33685
+ if (report) meta3.image_inputs = report;
33315
33686
  meta3.hint = REFINE_HINT;
33316
33687
  const risk = timeoutRiskHint({ model, imageSize: args.image_size, persistsFiles: client2.mediaSink?.persistsFiles });
33317
33688
  if (risk) meta3.timeout_risk = risk;
@@ -33338,7 +33709,9 @@ function registerSetTools(server, client2) {
33338
33709
  reference_mode: external_exports.enum(["master", "chain"]).optional().describe("master: every image references the master (default). chain: each references the previous."),
33339
33710
  basename: external_exports.string().optional().describe("Base filename prefix for output images (default: slugified master_prompt)"),
33340
33711
  master_images: external_exports.array(external_exports.string().min(1)).optional().describe("Reference image paths passed to the master generation call"),
33341
- master_images_base64: external_exports.array(external_exports.string().min(1)).optional().describe("Reference images as base64 strings or data URIs for master generation"),
33712
+ master_images_url: imagesUrlSchema("Reference images passed to the master AND to every scene call (fetched once)"),
33713
+ master_images_file_uris: imagesFileUrisSchema("Reference images passed to the master AND to every scene call"),
33714
+ 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"),
33342
33715
  confirm: schemaConfirm,
33343
33716
  ...sharedImageSchema
33344
33717
  }
@@ -33363,13 +33736,24 @@ function registerSetTools(server, client2) {
33363
33736
  google_search: args.google_search,
33364
33737
  master_images: args.master_images,
33365
33738
  master_images_base64: args.master_images_base64,
33366
- from_clipboard: args.from_clipboard
33739
+ from_clipboard: args.from_clipboard,
33740
+ master_images_url: args.master_images_url,
33741
+ master_images_file_uris: args.master_images_file_uris
33367
33742
  });
33368
33743
  return client2.session.jobs.dispatch({ toolName: "gemini_image_set", fingerprint, idempotencyKey: args.idempotency_key, async: args.async }, async () => {
33369
33744
  const seed = pickSeed(args.seed);
33370
33745
  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 };
33371
33746
  const slug = args.basename ? baseName(args.basename) : slugify2(args.master_prompt);
33372
- const masterRefInputs = await gatherImageInputs({ images: args.master_images, images_base64: args.master_images_base64, from_clipboard: args.from_clipboard });
33747
+ const carried = await resolveImageInputs(
33748
+ { images_url: args.master_images_url, images_file_uris: args.master_images_file_uris },
33749
+ client2
33750
+ );
33751
+ const masterOnly = await resolveImageInputs(
33752
+ { images: args.master_images, images_base64: args.master_images_base64, from_clipboard: args.from_clipboard },
33753
+ client2
33754
+ );
33755
+ const masterRefInputs = [...masterOnly.inputs, ...carried.inputs];
33756
+ const report = carried.report || masterOnly.report ? { ...masterOnly.report, ...carried.report } : void 0;
33373
33757
  const named = [];
33374
33758
  const masterResult = await withProgressHeartbeat(extra, `Generating image set (${model})`, async () => {
33375
33759
  const result = await client2.generate({
@@ -33385,13 +33769,13 @@ function registerSetTools(server, client2) {
33385
33769
  if (mode === "chain") {
33386
33770
  let ref = master;
33387
33771
  for (let i = 0; i < scenePrompts.length; i++) {
33388
- const { images: [img] } = await client2.generate({ prompt: scenePrompts[i], images: [ref], seed: seed + i + 1, ...cfg });
33772
+ const { images: [img] } = await client2.generate({ prompt: scenePrompts[i], images: [ref, ...carried.inputs], seed: seed + i + 1, ...cfg });
33389
33773
  named.push({ image: img, base: `${slug}-${String(i + 1).padStart(2, "0")}` });
33390
33774
  ref = img;
33391
33775
  }
33392
33776
  } else {
33393
33777
  const scenes = await Promise.all(
33394
- scenePrompts.map((p, i) => client2.generate({ prompt: p, images: [master], seed: seed + i + 1, ...cfg }).then((r) => r.images[0]))
33778
+ scenePrompts.map((p, i) => client2.generate({ prompt: p, images: [master, ...carried.inputs], seed: seed + i + 1, ...cfg }).then((r) => r.images[0]))
33395
33779
  );
33396
33780
  scenes.forEach((img, i) => named.push({ image: img, base: `${slug}-${String(i + 1).padStart(2, "0")}` }));
33397
33781
  }
@@ -33401,6 +33785,7 @@ function registerSetTools(server, client2) {
33401
33785
  const meta3 = buildMeta(model, seed, args);
33402
33786
  if (masterText) meta3.text = masterText;
33403
33787
  if (masterResult.grounding) meta3.grounding = masterResult.grounding;
33788
+ if (report) meta3.image_inputs = report;
33404
33789
  const risk = timeoutRiskHint({ model, imageSize: args.image_size, count: named.length, persistsFiles: client2.mediaSink?.persistsFiles });
33405
33790
  if (risk) meta3.timeout_risk = risk;
33406
33791
  return emit(named, { ...args, sink: client2.mediaSink }, meta3);
@@ -33442,6 +33827,42 @@ var SessionState = class {
33442
33827
  * model against the requested edit.
33443
33828
  */
33444
33829
  writtenOutputs = /* @__PURE__ */ new Set();
33830
+ /**
33831
+ * Files API uploads this session can reuse, keyed by an opaque identity
33832
+ * string (for a local path: absolute path + mtime + size, so an edited file
33833
+ * is a different key rather than a stale hit).
33834
+ *
33835
+ * Backs the stdio "referenced twice → upload once" optimization: the second
33836
+ * time a session sends the same picture, the bytes are replaced by a
33837
+ * `files/…` reference that costs nothing to repeat. Session-scoped, like
33838
+ * everything else here — a uri is minted by ONE user's API key and is only
33839
+ * readable with that key, so sharing this map across the connector's tenants
33840
+ * would hand out references another user cannot use and this user did not
33841
+ * create.
33842
+ */
33843
+ uploadCache = /* @__PURE__ */ new Map();
33844
+ /**
33845
+ * How many times each identity has been referenced this session. The
33846
+ * threshold for auto-uploading is "more than once", so the count has to
33847
+ * outlive the call that first saw it.
33848
+ */
33849
+ referenceCounts = /* @__PURE__ */ new Map();
33850
+ /** Record a reference and return the running total (1 on first sight). */
33851
+ countReference(key) {
33852
+ const next = (this.referenceCounts.get(key) ?? 0) + 1;
33853
+ this.referenceCounts.set(key, next);
33854
+ return next;
33855
+ }
33856
+ /** A cached upload, or `undefined` if absent or past its Files API TTL. */
33857
+ cachedUpload(key, now = Date.now()) {
33858
+ const hit = this.uploadCache.get(key);
33859
+ if (!hit) return void 0;
33860
+ if (hit.expiresAtMs <= now) {
33861
+ this.uploadCache.delete(key);
33862
+ return void 0;
33863
+ }
33864
+ return hit;
33865
+ }
33445
33866
  /** Forget everything. Tests use this; nothing in `src/` calls it. */
33446
33867
  reset() {
33447
33868
  this.jobs.reset();
@@ -33449,6 +33870,8 @@ var SessionState = class {
33449
33870
  this.lastMusicInteractionId = void 0;
33450
33871
  this.lastVideoInteractionId = void 0;
33451
33872
  this.writtenOutputs.clear();
33873
+ this.uploadCache.clear();
33874
+ this.referenceCounts.clear();
33452
33875
  }
33453
33876
  };
33454
33877
 
@@ -33489,6 +33912,22 @@ var ChainedRequest404Error = class extends McpToolError {
33489
33912
  upstreamMessage;
33490
33913
  retries;
33491
33914
  };
33915
+ function generatePart(img) {
33916
+ return img.uri ? { file_data: { file_uri: img.uri, mime_type: img.mimeType } } : { inline_data: { mime_type: img.mimeType, data: img.base64 } };
33917
+ }
33918
+ function interactPart(img) {
33919
+ return img.uri ? { type: "image", uri: img.uri, mime_type: img.mimeType } : { type: "image", mime_type: img.mimeType, data: img.base64 };
33920
+ }
33921
+ function normalizeFileName(ref) {
33922
+ const trimmed = ref.trim();
33923
+ const match = /(?:^|\/)(files\/[\w-]+)\/?$/.exec(trimmed);
33924
+ if (!match) {
33925
+ throw new McpToolError(`Not a Gemini Files API reference: "${ref}"`, {
33926
+ hint: "Pass the `files/<id>` name (or the full https://\u2026/v1beta/files/<id> uri) returned by gemini_upload_file / POST /upload."
33927
+ });
33928
+ }
33929
+ return match[1];
33930
+ }
33492
33931
  var GeminiClient = class {
33493
33932
  /**
33494
33933
  * A key handed in explicitly (the connector's per-session key). When absent
@@ -33577,6 +34016,14 @@ var GeminiClient = class {
33577
34016
  }
33578
34017
  return key;
33579
34018
  }
34019
+ /**
34020
+ * Fetch one `images_url` reference server-side. Routed through the client so
34021
+ * it uses the SAME injected `fetchImpl` as every upstream call — which is
34022
+ * what makes it mockable in tests and correct on the Worker.
34023
+ */
34024
+ async fetchRemoteImage(url2, opts = {}) {
34025
+ return fetchRemoteImage(url2, { ...opts, fetchImpl: this.fetchImpl, timeoutMs: resolveTimeoutMs(opts.timeoutMs) });
34026
+ }
33580
34027
  /** The default model after env override (no per-call arg). */
33581
34028
  defaultModel() {
33582
34029
  return resolveModel(void 0, readEnvVar("GEMINI_IMAGE_MODEL"));
@@ -33608,8 +34055,60 @@ var GeminiClient = class {
33608
34055
  * (enforced locally before any bytes are sent).
33609
34056
  */
33610
34057
  async uploadVideo(path, mimeType) {
33611
- const key = this.requireKey();
33612
34058
  const blob = await fileBlob(path, { type: mimeType, maxBytes: FILE_MAX_BYTES, label: "Video" });
34059
+ return this.uploadToFilesApi(blob, mimeType, basename(path), blob.size);
34060
+ }
34061
+ /**
34062
+ * Upload already-in-memory bytes (a fetched `images_url`, a `data_base64`
34063
+ * payload, an inline `gemini_upload_file` body) to the Files API. Same three
34064
+ * steps as {@link uploadVideo}, minus the filesystem — which is what makes it
34065
+ * usable on the hosted connector, where there is no disk to stream from.
34066
+ */
34067
+ async uploadBytes(bytes, mimeType, displayName) {
34068
+ const view = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
34069
+ if (view.byteLength > FILE_MAX_BYTES) {
34070
+ throw new McpToolError(`File is ${view.byteLength} bytes, over the Gemini Files API limit of ${FILE_MAX_BYTES} bytes (2 GB).`);
34071
+ }
34072
+ const body = new Blob([view], { type: mimeType });
34073
+ return this.uploadToFilesApi(body, mimeType, displayName, view.byteLength);
34074
+ }
34075
+ /**
34076
+ * Upload a request body straight through to the Files API without buffering
34077
+ * it — the direct-upload HTTP endpoint (`POST /upload` on the Worker) hands
34078
+ * us a `ReadableStream` and a `Content-Length`, and the whole point of that
34079
+ * endpoint is that the bytes never materialize anywhere they'd cost context
34080
+ * or memory.
34081
+ *
34082
+ * `contentLength` is REQUIRED because the resumable protocol's start step
34083
+ * declares it up front (`X-Goog-Upload-Header-Content-Length`); a stream of
34084
+ * unknown length has to be buffered by the caller first.
34085
+ */
34086
+ async uploadStream(stream, mimeType, displayName, contentLength) {
34087
+ if (contentLength > FILE_MAX_BYTES) {
34088
+ throw new McpToolError(`File is ${contentLength} bytes, over the Gemini Files API limit of ${FILE_MAX_BYTES} bytes (2 GB).`);
34089
+ }
34090
+ return this.uploadToFilesApi(stream, mimeType, displayName, contentLength);
34091
+ }
34092
+ /**
34093
+ * The resumable upload itself. Three live-verified steps (docs/GEMINI-API.md
34094
+ * "Files API — local video upload"):
34095
+ *
34096
+ * 1. `POST {UPLOAD_BASE_URL}/files` (command `start`) → the upload session
34097
+ * URL arrives in the `x-goog-upload-url` RESPONSE HEADER (body is empty).
34098
+ * 2. `POST <session url>` (command `upload, finalize`) with the bytes.
34099
+ * Response: `{file:…}`.
34100
+ * 3. Poll `GET /v1beta/files/<id>` while `state` is `PROCESSING`, until
34101
+ * `ACTIVE` (the poll response is the File object UNWRAPPED). Images are
34102
+ * usually `ACTIVE` immediately; video is what processes.
34103
+ *
34104
+ * These two upload calls are raw `fetchImpl` (not `createApiClient`): step 1
34105
+ * needs the response *header* and step 2 posts a binary body — neither fits
34106
+ * `fetchJson`. The poll DOES go through the shared client (timeout + retry).
34107
+ * Uploads deliberately have no abort timeout: a multi-hundred-MB file can
34108
+ * legitimately take longer than any fixed budget.
34109
+ */
34110
+ async uploadToFilesApi(body, mimeType, displayName, contentLength) {
34111
+ const key = this.requireKey();
33613
34112
  const startPath = "/files";
33614
34113
  const startRes = await this.fetchImpl(`${UPLOAD_BASE_URL}${startPath}`, {
33615
34114
  method: "POST",
@@ -33617,11 +34116,11 @@ var GeminiClient = class {
33617
34116
  "x-goog-api-key": key,
33618
34117
  "X-Goog-Upload-Protocol": "resumable",
33619
34118
  "X-Goog-Upload-Command": "start",
33620
- "X-Goog-Upload-Header-Content-Length": String(blob.size),
34119
+ "X-Goog-Upload-Header-Content-Length": String(contentLength),
33621
34120
  "X-Goog-Upload-Header-Content-Type": mimeType,
33622
34121
  "Content-Type": "application/json"
33623
34122
  },
33624
- body: JSON.stringify({ file: { display_name: basename(path) } })
34123
+ body: JSON.stringify({ file: { display_name: displayName } })
33625
34124
  });
33626
34125
  if (!startRes.ok) {
33627
34126
  throw new McpToolError(
@@ -33634,13 +34133,17 @@ var GeminiClient = class {
33634
34133
  hint: "The Files API resumable-upload contract may have changed \u2014 see docs/GEMINI-API.md."
33635
34134
  });
33636
34135
  }
34136
+ const streaming = !(body instanceof Blob);
33637
34137
  const upRes = await this.fetchImpl(uploadUrl, {
33638
34138
  method: "POST",
33639
34139
  headers: {
33640
34140
  "X-Goog-Upload-Offset": "0",
33641
- "X-Goog-Upload-Command": "upload, finalize"
34141
+ "X-Goog-Upload-Command": "upload, finalize",
34142
+ ...streaming ? { "Content-Length": String(contentLength) } : {}
33642
34143
  },
33643
- body: blob
34144
+ body,
34145
+ // Required by undici/workerd to send a ReadableStream body at all.
34146
+ ...streaming ? { duplex: "half" } : {}
33644
34147
  });
33645
34148
  if (!upRes.ok) {
33646
34149
  throw new McpToolError(
@@ -33654,8 +34157,8 @@ var GeminiClient = class {
33654
34157
  }
33655
34158
  for (let attempt = 0; file2.state === "PROCESSING"; attempt++) {
33656
34159
  if (attempt >= FILE_POLL_MAX_ATTEMPTS) {
33657
- throw new McpToolError(`${SERVICE} video file ${name} is still processing after ${FILE_POLL_INTERVAL_MS * FILE_POLL_MAX_ATTEMPTS / 6e4} minutes`, {
33658
- hint: "Very long videos can take a while to process \u2014 retry later by passing the file uri as video_url."
34160
+ throw new McpToolError(`${SERVICE} file ${name} is still processing after ${FILE_POLL_INTERVAL_MS * FILE_POLL_MAX_ATTEMPTS / 6e4} minutes`, {
34161
+ hint: "Very long videos can take a while to process \u2014 retry later by passing the file uri as video_url / images_file_uris."
33659
34162
  });
33660
34163
  }
33661
34164
  await this.sleep(FILE_POLL_INTERVAL_MS);
@@ -33663,14 +34166,78 @@ var GeminiClient = class {
33663
34166
  }
33664
34167
  if (file2.state !== "ACTIVE") {
33665
34168
  throw new McpToolError(
33666
- `${SERVICE} video file processing failed (state ${file2.state ?? "unknown"})${file2.error?.message ? `: ${file2.error.message}` : ""}`,
33667
- { hint: "Check the video is a supported format/codec and under 2 GB." }
34169
+ `${SERVICE} file processing failed (state ${file2.state ?? "unknown"})${file2.error?.message ? `: ${file2.error.message}` : ""}`,
34170
+ { hint: "Check the file is a supported format/codec and under 2 GB." }
33668
34171
  );
33669
34172
  }
33670
34173
  if (!file2.uri) {
33671
34174
  throw new McpToolError(`${SERVICE} did not return a uri for uploaded file ${name}`);
33672
34175
  }
33673
- return { name, uri: file2.uri, mimeType: file2.mimeType ?? mimeType, expirationTime: file2.expirationTime };
34176
+ return {
34177
+ name,
34178
+ uri: file2.uri,
34179
+ mimeType: file2.mimeType ?? mimeType,
34180
+ expirationTime: file2.expirationTime,
34181
+ sizeBytes: file2.sizeBytes,
34182
+ displayName: file2.displayName
34183
+ };
34184
+ }
34185
+ /**
34186
+ * List the files this API key currently has in the Files API. Retention is
34187
+ * ~48h, so the list is naturally short-lived — an empty list after a day is
34188
+ * expiry, not a bug.
34189
+ */
34190
+ async listFiles(pageSize = 100) {
34191
+ const size = Math.min(Math.max(Math.trunc(pageSize), 1), 100);
34192
+ const data = await this.call("GET", `/files?pageSize=${size}`);
34193
+ return (data.files ?? []).filter((f) => !!f.name && !!f.uri).map((f) => ({
34194
+ name: f.name,
34195
+ uri: f.uri,
34196
+ mimeType: f.mimeType ?? "application/octet-stream",
34197
+ state: f.state,
34198
+ createTime: f.createTime,
34199
+ expirationTime: f.expirationTime,
34200
+ sizeBytes: f.sizeBytes,
34201
+ displayName: f.displayName
34202
+ }));
34203
+ }
34204
+ /**
34205
+ * Fetch one file's metadata. Used to resolve a caller-supplied
34206
+ * `images_file_uris` entry to its real MIME type — and, as a side effect, to
34207
+ * fail fast with a clear "that file is gone" instead of the Interactions
34208
+ * API's generic 404 after a billable request was already built.
34209
+ */
34210
+ async getFile(ref) {
34211
+ const resource = normalizeFileName(ref);
34212
+ const file2 = await this.call("GET", `/${resource}`);
34213
+ if (!file2.uri) {
34214
+ throw new McpToolError(`${SERVICE} returned no uri for ${resource}`, {
34215
+ hint: "Files expire ~48h after upload \u2014 re-upload with gemini_upload_file (or POST /upload) and use the new uri."
34216
+ });
34217
+ }
34218
+ if (file2.state && file2.state !== "ACTIVE") {
34219
+ throw new McpToolError(`${SERVICE} file ${resource} is not usable yet (state ${file2.state}).`, {
34220
+ hint: "Wait for the upload to finish processing, then retry."
34221
+ });
34222
+ }
34223
+ return {
34224
+ name: file2.name ?? resource,
34225
+ uri: file2.uri,
34226
+ mimeType: file2.mimeType ?? "application/octet-stream",
34227
+ expirationTime: file2.expirationTime,
34228
+ sizeBytes: file2.sizeBytes,
34229
+ displayName: file2.displayName
34230
+ };
34231
+ }
34232
+ /**
34233
+ * Delete one uploaded file. `name` is interpolated into the URL path, so it
34234
+ * is validated to a bare `files/<id>` for the same reason the model id is
34235
+ * (see {@link uploadToFilesApi}); a full `https://…/v1beta/files/<id>` uri is
34236
+ * accepted and reduced to that form, since that is what every result echoes.
34237
+ */
34238
+ async deleteFile(name) {
34239
+ const resource = normalizeFileName(name);
34240
+ await this.call("DELETE", `/${resource}`);
33674
34241
  }
33675
34242
  async listModels() {
33676
34243
  const data = await this.call("GET", "/models?pageSize=200");
@@ -33679,9 +34246,7 @@ var GeminiClient = class {
33679
34246
  async generate(opts) {
33680
34247
  const model = resolveModel(opts.model, readEnvVar("GEMINI_IMAGE_MODEL"));
33681
34248
  const parts = [{ text: opts.prompt }];
33682
- for (const img of opts.images ?? []) {
33683
- parts.push({ inline_data: { mime_type: img.mimeType, data: img.base64 } });
33684
- }
34249
+ for (const img of opts.images ?? []) parts.push(generatePart(img));
33685
34250
  if (opts.videoUrl) {
33686
34251
  parts.push({ file_data: { file_uri: opts.videoUrl, mime_type: opts.videoMimeType ?? "video/mp4" } });
33687
34252
  }
@@ -33737,9 +34302,7 @@ var GeminiClient = class {
33737
34302
  async interact(opts) {
33738
34303
  const model = resolveModel(opts.model, readEnvVar("GEMINI_IMAGE_MODEL"));
33739
34304
  const inputParts = [{ type: "text", text: opts.input }];
33740
- for (const img of opts.images ?? []) {
33741
- inputParts.push({ type: "image", mime_type: img.mimeType, data: img.base64 });
33742
- }
34305
+ for (const img of opts.images ?? []) inputParts.push(interactPart(img));
33743
34306
  if (opts.videoUrl) {
33744
34307
  inputParts.push({ type: "video", uri: opts.videoUrl, mime_type: opts.videoMimeType ?? "video/mp4" });
33745
34308
  }
@@ -33772,7 +34335,7 @@ var GeminiClient = class {
33772
34335
  async generateVideo(opts) {
33773
34336
  const model = opts.model ?? DEFAULT_VIDEO_MODEL;
33774
34337
  const inputParts = [{ type: "text", text: opts.input }];
33775
- for (const img of opts.images ?? []) inputParts.push({ type: "image", mime_type: img.mimeType, data: img.base64 });
34338
+ for (const img of opts.images ?? []) inputParts.push(interactPart(img));
33776
34339
  const responseFormat = { type: "video", delivery: "inline" };
33777
34340
  if (opts.aspectRatio) responseFormat.aspect_ratio = opts.aspectRatio;
33778
34341
  const body = { model, input: inputParts, response_format: responseFormat };
@@ -33794,7 +34357,7 @@ var GeminiClient = class {
33794
34357
  async generateMusic(opts) {
33795
34358
  const model = opts.model ?? DEFAULT_MUSIC_MODEL;
33796
34359
  const inputParts = [{ type: "text", text: opts.input }];
33797
- for (const img of opts.images ?? []) inputParts.push({ type: "image", mime_type: img.mimeType, data: img.base64 });
34360
+ for (const img of opts.images ?? []) inputParts.push(interactPart(img));
33798
34361
  const responseFormat = { type: "audio" };
33799
34362
  if (opts.audioFormat) responseFormat.audio_format = opts.audioFormat;
33800
34363
  const body = { model, input: inputParts, response_format: responseFormat };
@@ -33953,7 +34516,11 @@ function registerInteractTools(server, client2) {
33953
34516
  previous_interaction_id: external_exports.string().optional().describe("ID from a prior gemini_interact call \u2014 continues that multi-turn conversation"),
33954
34517
  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."),
33955
34518
  images: external_exports.array(external_exports.string().min(1)).optional().describe(`Paths to reference input images. ${NEW_REFERENCES_ONLY}`),
33956
- images_base64: external_exports.array(external_exports.string().min(1)).optional().describe(`Reference images as base64 strings or data URIs. ${NEW_REFERENCES_ONLY}`),
34519
+ images_url: imagesUrlSchema(`Reference images. ${NEW_REFERENCES_ONLY} Given`),
34520
+ images_file_uris: imagesFileUrisSchema(`Reference images. ${NEW_REFERENCES_ONLY} Given`),
34521
+ images_base64: external_exports.array(external_exports.string().min(1)).optional().describe(
34522
+ `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}`
34523
+ ),
33957
34524
  model: external_exports.string().optional().describe(`Model id override (default: server default; see gemini_list_models). ${MODEL_CHOICE_GUIDE}`),
33958
34525
  aspect_ratio: external_exports.enum(ASPECT_RATIOS).optional().describe("Output aspect ratio"),
33959
34526
  image_size: external_exports.enum(IMAGE_SIZES).optional().describe("Output resolution (512 = 0.5K, Flash-only)"),
@@ -34011,11 +34578,13 @@ function registerInteractTools(server, client2) {
34011
34578
  images,
34012
34579
  images_base64: args.images_base64,
34013
34580
  from_clipboard: args.from_clipboard,
34581
+ images_url: args.images_url,
34582
+ images_file_uris: args.images_file_uris,
34014
34583
  video_url: args.video_url,
34015
34584
  video_path: args.video_path
34016
34585
  });
34017
34586
  return client2.session.jobs.dispatch({ toolName: "gemini_interact", fingerprint, idempotencyKey: args.idempotency_key, async: args.async }, async () => {
34018
- const inputs = await gatherImageInputs({ images, images_base64: args.images_base64, from_clipboard: args.from_clipboard });
34587
+ const { inputs, report } = await resolveImageInputs({ ...args, images }, client2);
34019
34588
  const video = await withProgressHeartbeat(extra, "Uploading video to the Gemini Files API", () => resolveVideoInput(args, client2));
34020
34589
  const callOpts = {
34021
34590
  input: args.input,
@@ -34067,6 +34636,7 @@ function registerInteractTools(server, client2) {
34067
34636
  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.`;
34068
34637
  if (r.text) meta3.text = r.text;
34069
34638
  if (r.grounding) meta3.grounding = r.grounding;
34639
+ if (report) meta3.image_inputs = report;
34070
34640
  if (video.videoFileMeta) meta3.video_file = video.videoFileMeta;
34071
34641
  const risk = timeoutRiskHint({ model, imageSize: args.image_size, persistsFiles: onDisk });
34072
34642
  if (risk) meta3.timeout_risk = risk;
@@ -34272,12 +34842,159 @@ function registerJobTools(server, client2) {
34272
34842
  );
34273
34843
  }
34274
34844
 
34845
+ // src/tools/files.ts
34846
+ init_zod();
34847
+ init_dist();
34848
+ var UPLOAD_URL_MAX_BYTES_DISK = 100 * 1024 * 1024;
34849
+ var UPLOAD_URL_MAX_BYTES_HOSTED = 25 * 1024 * 1024;
34850
+ var mb = (bytes) => Math.round(bytes / (1024 * 1024));
34851
+ var UPLOAD_ACCEPT = ["image/", "video/", "audio/"];
34852
+ 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.";
34853
+ function uploadResult(file2) {
34854
+ return textResult({
34855
+ // The `files/<id>` form is what every tool parameter documents, so it leads.
34856
+ file_uri: file2.name,
34857
+ // The absolute uri, for callers that would rather pass it through verbatim.
34858
+ uri: file2.uri,
34859
+ name: file2.name,
34860
+ mime_type: file2.mimeType,
34861
+ ...file2.sizeBytes ? { size_bytes: Number(file2.sizeBytes) } : {},
34862
+ ...file2.displayName ? { display_name: file2.displayName } : {},
34863
+ expires: file2.expirationTime ?? "~48h from upload",
34864
+ hint: `Pass "${file2.name}" in images_file_uris (or video_url for video) on any image tool. ${RETENTION_NOTE}`
34865
+ });
34866
+ }
34867
+ function registerFileTools(server, client2) {
34868
+ const onDisk = client2.mediaSink?.persistsFiles ?? true;
34869
+ const urlMaxBytes = onDisk ? UPLOAD_URL_MAX_BYTES_DISK : UPLOAD_URL_MAX_BYTES_HOSTED;
34870
+ server.registerTool(
34871
+ "gemini_upload_file",
34872
+ {
34873
+ 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."),
34874
+ annotations: { readOnlyHint: false, openWorldHint: true },
34875
+ inputSchema: {
34876
+ url: external_exports.string().url().optional().describe(
34877
+ `Public https URL the SERVER downloads and uploads (image/video/audio, up to ${mb(urlMaxBytes)}MB). No bytes pass through the conversation.` + (onDisk ? "" : " Larger files: POST the raw bytes to /upload, which streams instead of buffering.")
34878
+ ),
34879
+ 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)."),
34880
+ path: external_exports.string().min(1).optional().describe(
34881
+ 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."
34882
+ ),
34883
+ 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)"),
34884
+ display_name: external_exports.string().min(1).optional().describe("Human-readable name recorded against the upload"),
34885
+ confirm: schemaConfirm
34886
+ }
34887
+ },
34888
+ async (args, extra) => {
34889
+ const sources = [args.url && "url", args.data_base64 && "data_base64", args.path && "path"].filter(Boolean);
34890
+ if (sources.length !== 1) {
34891
+ throw new McpToolError(
34892
+ sources.length === 0 ? "Provide exactly one of `url`, `data_base64`" + (onDisk ? " or `path`." : ".") : `Provide exactly one source, not ${sources.length} (${sources.join(", ")}).`
34893
+ );
34894
+ }
34895
+ if (args.path) {
34896
+ if (!onDisk) {
34897
+ throw new McpToolError(
34898
+ "`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.",
34899
+ { hint: "Use `url`, `data_base64`, or POST /upload." }
34900
+ );
34901
+ }
34902
+ const gate = await previewLocalInputsUnlessConfirmed(
34903
+ args.confirm,
34904
+ "Upload a local file to the Gemini Files API",
34905
+ "/upload/v1beta/files",
34906
+ [args.path]
34907
+ );
34908
+ if (gate) return gate;
34909
+ }
34910
+ const uploaded = await withProgressHeartbeat(extra, "Uploading to the Gemini Files API", async () => {
34911
+ if (args.url) {
34912
+ const fetched = await client2.fetchRemoteImage(args.url, { maxBytes: urlMaxBytes, accept: UPLOAD_ACCEPT });
34913
+ return client2.uploadBytes(
34914
+ fetched.bytes,
34915
+ args.mime_type ?? fetched.mimeType,
34916
+ args.display_name ?? nameFromUrl(args.url)
34917
+ );
34918
+ }
34919
+ if (args.data_base64) {
34920
+ const { decodeImageInput: decodeImageInput2 } = await Promise.resolve().then(() => (init_images(), images_exports));
34921
+ const decoded = decodeImageInput2(args.data_base64);
34922
+ const bytes2 = Uint8Array.from(atob(decoded.base64), (c) => c.charCodeAt(0));
34923
+ return client2.uploadBytes(bytes2, args.mime_type ?? decoded.mimeType, args.display_name ?? "upload");
34924
+ }
34925
+ const { readImageAsInline: readImageAsInline2, resolveImagePath: resolveImagePath2 } = await Promise.resolve().then(() => (init_images(), images_exports));
34926
+ const resolved = resolveImagePath2(args.path);
34927
+ const inline = await readImageAsInline2(resolved);
34928
+ const bytes = Uint8Array.from(atob(inline.base64), (c) => c.charCodeAt(0));
34929
+ return client2.uploadBytes(
34930
+ bytes,
34931
+ args.mime_type ?? inline.mimeType,
34932
+ args.display_name ?? (resolved.split(/[\\/]/).pop() || "upload")
34933
+ );
34934
+ });
34935
+ return uploadResult(uploaded);
34936
+ }
34937
+ );
34938
+ server.registerTool(
34939
+ "gemini_list_files",
34940
+ {
34941
+ 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.",
34942
+ annotations: { readOnlyHint: true, openWorldHint: true },
34943
+ inputSchema: {
34944
+ page_size: external_exports.number().int().positive().max(100).optional().describe("Maximum files to return (1-100, default 100)")
34945
+ }
34946
+ },
34947
+ async (args) => {
34948
+ const files = await client2.listFiles(args.page_size ?? 100);
34949
+ return textResult({
34950
+ count: files.length,
34951
+ files: files.map((f) => ({
34952
+ file_uri: f.name,
34953
+ uri: f.uri,
34954
+ mime_type: f.mimeType,
34955
+ ...f.displayName ? { display_name: f.displayName } : {},
34956
+ ...f.sizeBytes ? { size_bytes: Number(f.sizeBytes) } : {},
34957
+ ...f.state ? { state: f.state } : {},
34958
+ ...f.createTime ? { created: f.createTime } : {},
34959
+ ...f.expirationTime ? { expires: f.expirationTime } : {}
34960
+ })),
34961
+ 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."
34962
+ });
34963
+ }
34964
+ );
34965
+ server.registerTool(
34966
+ "gemini_delete_file",
34967
+ {
34968
+ 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.",
34969
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
34970
+ inputSchema: {
34971
+ file_uri: external_exports.string().min(1).describe("The `files/<id>` reference (or full uri) to delete"),
34972
+ confirm: schemaConfirm
34973
+ }
34974
+ },
34975
+ async (args) => {
34976
+ const gate = previewUnlessConfirmed(args.confirm, "Delete an uploaded Gemini file", "DELETE", `/v1beta/${args.file_uri}`);
34977
+ if (gate) return gate;
34978
+ await client2.deleteFile(args.file_uri);
34979
+ return textResult({ deleted: args.file_uri });
34980
+ }
34981
+ );
34982
+ }
34983
+ function nameFromUrl(url2) {
34984
+ try {
34985
+ const name = new URL(url2).pathname.split("/").filter(Boolean).pop();
34986
+ return name ? decodeURIComponent(name).slice(0, 120) : "upload";
34987
+ } catch {
34988
+ return "upload";
34989
+ }
34990
+ }
34991
+
34275
34992
  // src/index.ts
34276
34993
  await loadStdioDotenv();
34277
34994
  await runMcp({
34278
34995
  name: "gemini-mcp",
34279
34996
  version: VERSION,
34280
34997
  banner: "[gemini-mcp] This project was developed and is maintained by AI (Claude). Use at your own discretion.",
34281
- tools: [registerModelTools, registerGenerateTools, registerSetTools, registerInteractTools, registerVideoTools, registerMusicTools, registerJobTools],
34998
+ tools: [registerModelTools, registerGenerateTools, registerSetTools, registerInteractTools, registerVideoTools, registerMusicTools, registerJobTools, registerFileTools],
34282
34999
  deps: client
34283
35000
  });