@chrischall/gemini-mcp 2.2.0 → 2.3.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 +10 -0
- package/dist/bundle.js +44 -10
- package/dist/client.js +32 -3
- package/dist/images.js +19 -3
- package/dist/version.js +1 -1
- package/package.json +2 -2
- package/server.json +9 -2
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
},
|
|
8
8
|
"metadata": {
|
|
9
9
|
"description": "MCP server for Google Gemini image generation and editing — generate and edit images via the Gemini API",
|
|
10
|
-
"version": "2.
|
|
10
|
+
"version": "2.3.0"
|
|
11
11
|
},
|
|
12
12
|
"plugins": [
|
|
13
13
|
{
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"displayName": "Gemini Images",
|
|
16
16
|
"source": "./",
|
|
17
17
|
"description": "Generate and edit images with Google Gemini image models (Nano Banana / Nano Banana Pro) via natural language prompts",
|
|
18
|
-
"version": "2.
|
|
18
|
+
"version": "2.3.0",
|
|
19
19
|
"author": {
|
|
20
20
|
"name": "Chris Hall"
|
|
21
21
|
},
|
package/README.md
CHANGED
|
@@ -16,6 +16,7 @@ Developed and maintained by AI (Claude Code).
|
|
|
16
16
|
| `GEMINI_IMAGE_MODEL` | No | Override the default image model (default: `gemini-3.1-flash-image`) |
|
|
17
17
|
| `GEMINI_OUTPUT_DIR` | No | Default directory for generated images (default: current working directory) |
|
|
18
18
|
| `GEMINI_INPUT_DIR` | No | Directory to resolve bare input-image filenames against (so `images: ["foo.jpg"]` works) |
|
|
19
|
+
| `GEMINI_UPLOAD_DIR` | No | Restrict local files streamed to the Gemini Files API (`gemini_upload_file` `path`, `video_path`) to these directories — one or more, separated by `:` (`;` on Windows); `~` allowed. A path outside is refused before any upload. Unset: no restriction |
|
|
19
20
|
| `GEMINI_TIMEOUT_MS` | No | Upstream request timeout in ms (default: `60000`, or `120000` for `image_size: "4K"`); each generation tool also takes a per-call `timeout_ms` |
|
|
20
21
|
| `GEMINI_HEARTBEAT_MS` | No | Progress-notification cadence in ms while a generation runs (default: `10000`; `0` disables) — keeps MCP hosts that reset their timeout on progress from timing out long generations |
|
|
21
22
|
| `GEMINI_CHAIN_RETRY_MS` | No | How long to wait out interactions-store lag when a chained call 404s (default: `120000`; `0` disables retrying) |
|
|
@@ -189,6 +190,15 @@ safe form, and a markdown link is a reasonable enhancement where you know it ren
|
|
|
189
190
|
Signed-URL lifetime is clamped to `MEDIA_TTL_DAYS`, so a link never outlives the object it
|
|
190
191
|
points at.
|
|
191
192
|
|
|
193
|
+
**Prompts are stored too.** Each generated object on a hosted deployment gets a small JSON
|
|
194
|
+
record beside it holding the prompt (first 500 characters), model and interaction id — that is
|
|
195
|
+
what `gemini_list_recent_media` shows and what lets a lost turn be recovered. The record lives
|
|
196
|
+
and is swept with its object, so it is kept for the same `MEDIA_TTL_DAYS` window, not longer.
|
|
197
|
+
Locally, `gemini_interact` writes an `<image>.json` sidecar (model, interaction id, options and
|
|
198
|
+
any text the model replied with — not the prompt itself) next to each image in the output dir;
|
|
199
|
+
those files are yours to keep or delete and never expire on their own. If your
|
|
200
|
+
prompts describe real people, bear in mind they sit in your bucket for the retention window.
|
|
201
|
+
|
|
192
202
|
## Sending reference images without burning context
|
|
193
203
|
|
|
194
204
|
Every tool that takes a reference image — `gemini_image_generate`, `gemini_image_edit`,
|
package/dist/bundle.js
CHANGED
|
@@ -1722,8 +1722,8 @@ var require_regexes = __commonJS({
|
|
|
1722
1722
|
}
|
|
1723
1723
|
exports.ipv4 = /^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])$/;
|
|
1724
1724
|
exports.ipv6 = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))$/;
|
|
1725
|
-
var mac3 = (
|
|
1726
|
-
const escapedDelim = util.escapeRegex(
|
|
1725
|
+
var mac3 = (delimiter2) => {
|
|
1726
|
+
const escapedDelim = util.escapeRegex(delimiter2 ?? ":");
|
|
1727
1727
|
return new RegExp(`^(?:[0-9A-F]{2}${escapedDelim}){5}[0-9A-F]{2}$|^(?:[0-9a-f]{2}${escapedDelim}){5}[0-9a-f]{2}$`);
|
|
1728
1728
|
};
|
|
1729
1729
|
exports.mac = mac3;
|
|
@@ -39648,8 +39648,8 @@ var init_regexes = __esm({
|
|
|
39648
39648
|
_emoji = `^(?=[\\s\\S]*[\\p{Extended_Pictographic}\\p{Regional_Indicator}\\u20E3])[\\p{Extended_Pictographic}\\p{Emoji_Component}]+$`;
|
|
39649
39649
|
ipv4 = /^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])$/;
|
|
39650
39650
|
ipv6 = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))$/;
|
|
39651
|
-
mac = (
|
|
39652
|
-
const escapedDelim = escapeRegex(
|
|
39651
|
+
mac = (delimiter2) => {
|
|
39652
|
+
const escapedDelim = escapeRegex(delimiter2 ?? ":");
|
|
39653
39653
|
return new RegExp(`^(?:[0-9A-F]{2}${escapedDelim}){5}[0-9A-F]{2}$|^(?:[0-9a-f]{2}${escapedDelim}){5}[0-9a-f]{2}$`);
|
|
39654
39654
|
};
|
|
39655
39655
|
cidrv4 = /^((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\/([0-9]|[1-2][0-9]|3[0-2])$/;
|
|
@@ -59728,6 +59728,16 @@ async function uniquePath(dir, base, ext) {
|
|
|
59728
59728
|
function sniffMimeBytes(buf) {
|
|
59729
59729
|
return sniffImageMime(buf) ?? "image/png";
|
|
59730
59730
|
}
|
|
59731
|
+
function localImageMime(resolvedPath, head) {
|
|
59732
|
+
const mime = sniffImageMime(head);
|
|
59733
|
+
if (!mime) {
|
|
59734
|
+
throw new McpToolError(
|
|
59735
|
+
`Cannot tell what kind of image ${resolvedPath} is \u2014 its bytes are not PNG, JPEG or WebP, so it is not sent to Gemini as a reference image.`,
|
|
59736
|
+
{ hint: "Pass a PNG, JPEG or WebP file (convert other formats first)." }
|
|
59737
|
+
);
|
|
59738
|
+
}
|
|
59739
|
+
return mime;
|
|
59740
|
+
}
|
|
59731
59741
|
function sniffImageMime(buf) {
|
|
59732
59742
|
if (buf.length >= 4 && buf[0] === 137 && buf[1] === 80 && buf[2] === 78 && buf[3] === 71) {
|
|
59733
59743
|
return "image/png";
|
|
@@ -59803,7 +59813,7 @@ async function previewImageInput(path) {
|
|
|
59803
59813
|
try {
|
|
59804
59814
|
const head = Buffer.alloc(16);
|
|
59805
59815
|
const { bytesRead } = await fh.read(head, 0, 16, 0);
|
|
59806
|
-
return { path: resolved, mimeType:
|
|
59816
|
+
return { path: resolved, mimeType: localImageMime(resolved, head.subarray(0, bytesRead)), size };
|
|
59807
59817
|
} finally {
|
|
59808
59818
|
await fh.close();
|
|
59809
59819
|
}
|
|
@@ -59832,7 +59842,7 @@ async function detectUploadMime(resolvedPath) {
|
|
|
59832
59842
|
async function readImageAsInline(path) {
|
|
59833
59843
|
const resolved = resolveImagePath(path);
|
|
59834
59844
|
const buf = await readFile(resolved);
|
|
59835
|
-
const mimeType =
|
|
59845
|
+
const mimeType = localImageMime(resolved, buf);
|
|
59836
59846
|
return { base64: buf.toString("base64"), mimeType };
|
|
59837
59847
|
}
|
|
59838
59848
|
function decodeImageInput(input2) {
|
|
@@ -59966,7 +59976,7 @@ async function loadStdioDotenv() {
|
|
|
59966
59976
|
}
|
|
59967
59977
|
|
|
59968
59978
|
// src/version.ts
|
|
59969
|
-
var VERSION = "2.
|
|
59979
|
+
var VERSION = "2.3.0";
|
|
59970
59980
|
|
|
59971
59981
|
// src/errors.ts
|
|
59972
59982
|
init_dist2();
|
|
@@ -62556,7 +62566,7 @@ init_dist2();
|
|
|
62556
62566
|
|
|
62557
62567
|
// src/client.ts
|
|
62558
62568
|
init_dist2();
|
|
62559
|
-
import { basename } from "node:path";
|
|
62569
|
+
import { basename, delimiter } from "node:path";
|
|
62560
62570
|
|
|
62561
62571
|
// src/library.ts
|
|
62562
62572
|
var LIBRARY_KEY_PREFIX = "lib";
|
|
@@ -63008,6 +63018,18 @@ function normalizeFileName(ref) {
|
|
|
63008
63018
|
}
|
|
63009
63019
|
return match[1];
|
|
63010
63020
|
}
|
|
63021
|
+
function uploadRoots() {
|
|
63022
|
+
const roots = readEnvVar("GEMINI_UPLOAD_DIR")?.split(delimiter).map((r) => r.trim()).filter(Boolean);
|
|
63023
|
+
return roots && roots.length > 0 ? roots : void 0;
|
|
63024
|
+
}
|
|
63025
|
+
function explainOutsideUploadDir(err) {
|
|
63026
|
+
if (err instanceof Error && err.message.startsWith("Path is outside the allowed directories")) {
|
|
63027
|
+
throw new McpToolError(
|
|
63028
|
+
"Refusing to upload a file outside GEMINI_UPLOAD_DIR, which restricts which local files can be uploaded to Google. Move the file into GEMINI_UPLOAD_DIR, or add its directory to GEMINI_UPLOAD_DIR."
|
|
63029
|
+
);
|
|
63030
|
+
}
|
|
63031
|
+
throw err;
|
|
63032
|
+
}
|
|
63011
63033
|
var GeminiClient = class {
|
|
63012
63034
|
/**
|
|
63013
63035
|
* A key handed in explicitly (the connector's per-session key). When absent
|
|
@@ -63170,7 +63192,13 @@ var GeminiClient = class {
|
|
|
63170
63192
|
* (enforced locally before any bytes are sent).
|
|
63171
63193
|
*/
|
|
63172
63194
|
async uploadVideo(path, mimeType) {
|
|
63173
|
-
const
|
|
63195
|
+
const roots = uploadRoots();
|
|
63196
|
+
const blob = await fileBlob(path, {
|
|
63197
|
+
type: mimeType,
|
|
63198
|
+
maxBytes: FILE_MAX_BYTES,
|
|
63199
|
+
label: "Video",
|
|
63200
|
+
...roots ? { allowedRoots: roots } : {}
|
|
63201
|
+
}).catch(explainOutsideUploadDir);
|
|
63174
63202
|
return this.uploadToFilesApi(blob, mimeType, basename(path), blob.size);
|
|
63175
63203
|
}
|
|
63176
63204
|
/**
|
|
@@ -63181,7 +63209,13 @@ var GeminiClient = class {
|
|
|
63181
63209
|
* length on a few-hundred-MB video (chrischall/fleet-audit#117).
|
|
63182
63210
|
*/
|
|
63183
63211
|
async uploadFile(path, mimeType, displayName = basename(path)) {
|
|
63184
|
-
const
|
|
63212
|
+
const roots = uploadRoots();
|
|
63213
|
+
const blob = await fileBlob(path, {
|
|
63214
|
+
type: mimeType,
|
|
63215
|
+
maxBytes: FILE_MAX_BYTES,
|
|
63216
|
+
label: "File",
|
|
63217
|
+
...roots ? { allowedRoots: roots } : {}
|
|
63218
|
+
}).catch(explainOutsideUploadDir);
|
|
63185
63219
|
return this.uploadToFilesApi(blob, mimeType, displayName, blob.size);
|
|
63186
63220
|
}
|
|
63187
63221
|
/**
|
package/dist/client.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { basename } from 'node:path';
|
|
1
|
+
import { basename, delimiter } from 'node:path';
|
|
2
2
|
import { readEnvVar, McpToolError, ApiError, createApiClient, formatApiError, fileBlob } from '@chrischall/mcp-utils';
|
|
3
3
|
import { resolveModel, filterImageModels, DEFAULT_VIDEO_MODEL, DEFAULT_MUSIC_MODEL } from './models.js';
|
|
4
4
|
import { readUsage } from './usage.js';
|
|
@@ -181,6 +181,29 @@ export function normalizeFileName(ref) {
|
|
|
181
181
|
}
|
|
182
182
|
return match[1];
|
|
183
183
|
}
|
|
184
|
+
/**
|
|
185
|
+
* `GEMINI_UPLOAD_DIR` — the optional allow-list for local files streamed to
|
|
186
|
+
* the Files API (`gemini_upload_file` `path`, `video_path`). One or more
|
|
187
|
+
* directories separated by the platform path delimiter (`:` / `;`); `~` is
|
|
188
|
+
* expanded by mcp-utils. Unset → `undefined` → uploads stay unconfined.
|
|
189
|
+
* The paths are model-chosen, so a prompt-injected path could otherwise send
|
|
190
|
+
* any readable file to Google.
|
|
191
|
+
*/
|
|
192
|
+
function uploadRoots() {
|
|
193
|
+
const roots = readEnvVar('GEMINI_UPLOAD_DIR')?.split(delimiter).map((r) => r.trim()).filter(Boolean);
|
|
194
|
+
return roots && roots.length > 0 ? roots : undefined;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Turn mcp-utils' bare "outside the allowed directories" refusal into an
|
|
198
|
+
* actionable tool error. The remediation is in the MESSAGE — hosts show the
|
|
199
|
+
* message and drop the hint (see CLAUDE.md, Errors).
|
|
200
|
+
*/
|
|
201
|
+
function explainOutsideUploadDir(err) {
|
|
202
|
+
if (err instanceof Error && err.message.startsWith('Path is outside the allowed directories')) {
|
|
203
|
+
throw new McpToolError('Refusing to upload a file outside GEMINI_UPLOAD_DIR, which restricts which local files can be uploaded to Google. Move the file into GEMINI_UPLOAD_DIR, or add its directory to GEMINI_UPLOAD_DIR.');
|
|
204
|
+
}
|
|
205
|
+
throw err;
|
|
206
|
+
}
|
|
184
207
|
export class GeminiClient {
|
|
185
208
|
/**
|
|
186
209
|
* A key handed in explicitly (the connector's per-session key). When absent
|
|
@@ -365,7 +388,10 @@ export class GeminiClient {
|
|
|
365
388
|
* (enforced locally before any bytes are sent).
|
|
366
389
|
*/
|
|
367
390
|
async uploadVideo(path, mimeType) {
|
|
368
|
-
const
|
|
391
|
+
const roots = uploadRoots();
|
|
392
|
+
const blob = await fileBlob(path, {
|
|
393
|
+
type: mimeType, maxBytes: FILE_MAX_BYTES, label: 'Video', ...(roots ? { allowedRoots: roots } : {}),
|
|
394
|
+
}).catch(explainOutsideUploadDir);
|
|
369
395
|
return this.uploadToFilesApi(blob, mimeType, basename(path), blob.size);
|
|
370
396
|
}
|
|
371
397
|
/**
|
|
@@ -376,7 +402,10 @@ export class GeminiClient {
|
|
|
376
402
|
* length on a few-hundred-MB video (chrischall/fleet-audit#117).
|
|
377
403
|
*/
|
|
378
404
|
async uploadFile(path, mimeType, displayName = basename(path)) {
|
|
379
|
-
const
|
|
405
|
+
const roots = uploadRoots();
|
|
406
|
+
const blob = await fileBlob(path, {
|
|
407
|
+
type: mimeType, maxBytes: FILE_MAX_BYTES, label: 'File', ...(roots ? { allowedRoots: roots } : {}),
|
|
408
|
+
}).catch(explainOutsideUploadDir);
|
|
380
409
|
return this.uploadToFilesApi(blob, mimeType, displayName, blob.size);
|
|
381
410
|
}
|
|
382
411
|
/**
|
package/dist/images.js
CHANGED
|
@@ -31,10 +31,26 @@ export async function uniquePath(dir, base, ext) {
|
|
|
31
31
|
}
|
|
32
32
|
return candidate;
|
|
33
33
|
}
|
|
34
|
-
/** Sniff MIME type from the first bytes of an image buffer (PNG fallback).
|
|
34
|
+
/** Sniff MIME type from the first bytes of an image buffer (PNG fallback).
|
|
35
|
+
* Only for bytes that arrive as base64 with no other type information; a LOCAL
|
|
36
|
+
* file goes through {@link localImageMime}, which never guesses. */
|
|
35
37
|
function sniffMimeBytes(buf) {
|
|
36
38
|
return sniffImageMime(buf) ?? 'image/png';
|
|
37
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* The MIME of a LOCAL image input (PNG/JPEG/WebP, sniffed from its bytes), or a
|
|
42
|
+
* refusal. Never defaults: an unidentifiable file used to be previewed and sent
|
|
43
|
+
* as image/png, so a prompt-injected `images: ['~/.ssh/id_rsa']` showed up in
|
|
44
|
+
* the confirmation preview mislabelled as a PNG (chrischall/fleet-audit#929).
|
|
45
|
+
*/
|
|
46
|
+
function localImageMime(resolvedPath, head) {
|
|
47
|
+
const mime = sniffImageMime(head);
|
|
48
|
+
if (!mime) {
|
|
49
|
+
throw new McpToolError(`Cannot tell what kind of image ${resolvedPath} is — its bytes are not PNG, JPEG or WebP, ` +
|
|
50
|
+
'so it is not sent to Gemini as a reference image.', { hint: 'Pass a PNG, JPEG or WebP file (convert other formats first).' });
|
|
51
|
+
}
|
|
52
|
+
return mime;
|
|
53
|
+
}
|
|
38
54
|
/** PNG/JPEG/WebP from the leading bytes, or `undefined` when it is none of them. */
|
|
39
55
|
function sniffImageMime(buf) {
|
|
40
56
|
// PNG: 89 50 4E 47
|
|
@@ -174,7 +190,7 @@ export async function previewImageInput(path) {
|
|
|
174
190
|
try {
|
|
175
191
|
const head = Buffer.alloc(16);
|
|
176
192
|
const { bytesRead } = await fh.read(head, 0, 16, 0);
|
|
177
|
-
return { path: resolved, mimeType:
|
|
193
|
+
return { path: resolved, mimeType: localImageMime(resolved, head.subarray(0, bytesRead)), size };
|
|
178
194
|
}
|
|
179
195
|
finally {
|
|
180
196
|
await fh.close();
|
|
@@ -239,7 +255,7 @@ export async function detectUploadMime(resolvedPath) {
|
|
|
239
255
|
export async function readImageAsInline(path) {
|
|
240
256
|
const resolved = resolveImagePath(path);
|
|
241
257
|
const buf = await readFile(resolved);
|
|
242
|
-
const mimeType =
|
|
258
|
+
const mimeType = localImageMime(resolved, buf);
|
|
243
259
|
return { base64: buf.toString('base64'), mimeType };
|
|
244
260
|
}
|
|
245
261
|
/**
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** Single source of the server version. release-please bumps the literal below. */
|
|
2
|
-
export const VERSION = '2.
|
|
2
|
+
export const VERSION = '2.3.0'; // x-release-please-version
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chrischall/gemini-mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"mcpName": "io.github.chrischall/gemini-mcp",
|
|
5
5
|
"description": "Gemini image-generation MCP server for Claude — developed and maintained by AI (Claude Code)",
|
|
6
6
|
"author": "Claude Code (AI) <https://www.anthropic.com/claude>",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"@chrischall/mcp-utils": "^2.6.0",
|
|
50
50
|
"@modelcontextprotocol/server": "^2.0.0",
|
|
51
|
-
"dotenv": "^
|
|
51
|
+
"dotenv": "^18.0.2",
|
|
52
52
|
"zod": "^4.6.5"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
package/server.json
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
"url": "https://github.com/chrischall/gemini-mcp",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "2.
|
|
9
|
+
"version": "2.3.0",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "@chrischall/gemini-mcp",
|
|
14
|
-
"version": "2.
|
|
14
|
+
"version": "2.3.0",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|
|
@@ -43,6 +43,13 @@
|
|
|
43
43
|
"isRequired": false,
|
|
44
44
|
"format": "string",
|
|
45
45
|
"isSecret": false
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"name": "GEMINI_UPLOAD_DIR",
|
|
49
|
+
"description": "Restrict local-file uploads to the Gemini Files API to these directories (path.delimiter-separated; unset = unrestricted)",
|
|
50
|
+
"isRequired": false,
|
|
51
|
+
"format": "string",
|
|
52
|
+
"isSecret": false
|
|
46
53
|
}
|
|
47
54
|
]
|
|
48
55
|
}
|