@chrischall/gemini-mcp 2.3.2 → 2.3.4

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.
@@ -89,7 +89,7 @@ export function registerFileTools(server, client) {
89
89
  ? ' ' + confirmNote('A local `path` is confirmed first')
90
90
  : ' To upload a local file without base64: mint a signed PUT URL with gemini_get_upload_url, PUT the bytes to it, ' +
91
91
  'then pass the returned r2_key here.'),
92
- annotations: { readOnlyHint: false, openWorldHint: true },
92
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
93
93
  inputSchema: z.object({
94
94
  url: z
95
95
  .string()
@@ -168,6 +168,7 @@ export function registerFileTools(server, client) {
168
168
  body: { inputs },
169
169
  target: resolved,
170
170
  payload: { inputs, display_name: args.display_name },
171
+ args,
171
172
  confirmToken: args.confirmToken,
172
173
  });
173
174
  if (gate)
@@ -380,6 +381,7 @@ export function registerFileTools(server, client) {
380
381
  path: `/v1beta/${args.file_uri}`,
381
382
  target: args.file_uri,
382
383
  payload: { file_uri: args.file_uri },
384
+ args,
383
385
  confirmToken: args.confirmToken,
384
386
  });
385
387
  if (gate)
@@ -19,7 +19,7 @@ export function registerGenerateTools(server, client) {
19
19
  description: 'Generate image(s) from a text prompt with a Gemini image model (Nano Banana / Nano Banana Pro). ' +
20
20
  'If the result will likely be refined iteratively, prefer gemini_interact (multi-turn) as the entry point. ' +
21
21
  confirmNote('Local file inputs are confirmed first'),
22
- annotations: { readOnlyHint: false, openWorldHint: true },
22
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
23
23
  inputSchema: z.object({
24
24
  prompt: z.string().min(1).describe('Text prompt describing the image'),
25
25
  count: z.number().int().positive().max(8).optional().describe('Number of independent images (default 1)'),
@@ -166,7 +166,7 @@ export function registerGenerateTools(server, client) {
166
166
  'and avoids re-processing the full image each round; use gemini_image_edit for one-off edits or composing multiple distinct inputs. ' +
167
167
  'Gemini over-preserves the input; there is no edit-strength control — for large structural changes, reroll with a different `seed` or more forceful wording. ' +
168
168
  confirmNote('Local file inputs are confirmed first'),
169
- annotations: { readOnlyHint: false, openWorldHint: true },
169
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
170
170
  inputSchema: z.object({
171
171
  prompt: z.string().min(1).describe('Instruction describing the edit or composition'),
172
172
  images: z.array(z.string().min(1)).optional().describe('Paths to input image file(s) (1 = edit, 2+ = compose). Local paths must be inside GEMINI_REFERENCE_DIR (else GEMINI_UPLOAD_DIR) when set.'),
@@ -11,10 +11,10 @@ import { fingerprintRequest } from '../jobs.js';
11
11
  import { attachCost } from '../pricing.js';
12
12
  import { confirmLocalInputs, confirmNote, confirmTokenParam } from './_confirm.js';
13
13
  // The last-interaction id and the written-outputs set used to live here, at
14
- // module scope. On the hosted connector that leaks across tenants — one
15
- // Cloudflare isolate serves many authenticated sessions, so `continue_last`
16
- // from user B would resume user A's interaction under B's key. Both now live
17
- // on `client.session` (src/session.ts), one per authenticated session.
14
+ // module scope, which leaked across users on the retired multi-user Worker:
15
+ // `continue_last` from user B resumed user A's interaction. Both now live on
16
+ // `client.session` (src/session.ts) — one per client, and one client per
17
+ // single-user process today (see the invariant there).
18
18
  /** Shared warning for the reference-image params. */
19
19
  const NEW_REFERENCES_ONLY = 'NEW reference images only (e.g. a style or target photo). When chaining with previous_interaction_id, ' +
20
20
  "do NOT re-attach the prior turn's output — the interaction already contains it, and re-sending it " +
@@ -91,7 +91,7 @@ export function registerInteractTools(server, client) {
91
91
  recoveryDescription +
92
92
  'Output is JPEG. ' +
93
93
  confirmNote('Local file inputs are confirmed first'),
94
- annotations: { readOnlyHint: false, openWorldHint: true },
94
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
95
95
  inputSchema: z.object({
96
96
  input: z.string().min(1).describe('Text prompt or editing instruction'),
97
97
  previous_interaction_id: z
@@ -133,7 +133,7 @@ export function registerInteractTools(server, client) {
133
133
  output_dir: z
134
134
  .string()
135
135
  .optional()
136
- .describe('Directory to write images to (default: $GEMINI_OUTPUT_DIR or cwd)'),
136
+ .describe('Directory to write images to (default: $GEMINI_OUTPUT_DIR or ~/Downloads/gemini-mcp)'),
137
137
  inline: z
138
138
  .boolean()
139
139
  .optional()
@@ -23,10 +23,10 @@ export function registerJobTools(server, client) {
23
23
  'A killed video/music job started with `background: true` is recovered from its upstream interaction when it finished there.',
24
24
  // NOT read-only any more: recovering a killed job writes the media it
25
25
  // pulls back, exactly as the generation tool would have.
26
- annotations: { readOnlyHint: false, openWorldHint: true },
26
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
27
27
  inputSchema: z.object({
28
28
  job_id: z.string().min(1).describe('The job_id returned by a generation tool called with async: true'),
29
- output_dir: z.string().optional().describe('Where to write media recovered from a killed job (default: $GEMINI_OUTPUT_DIR or cwd)'),
29
+ output_dir: z.string().optional().describe('Where to write media recovered from a killed job (default: $GEMINI_OUTPUT_DIR or ~/Downloads/gemini-mcp)'),
30
30
  }),
31
31
  }, async (args) => {
32
32
  try {
@@ -62,7 +62,7 @@ export function registerLibraryTools(server, client) {
62
62
  'description. Saved characters have NO expiry — unlike uploads (~retention window) and Files API references (~48h) ' +
63
63
  '— so a name saved once keeps working in every later session. Re-saving a name replaces it. ' +
64
64
  USE_CHARACTERS_HINT,
65
- annotations: { readOnlyHint: false, openWorldHint: false },
65
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
66
66
  inputSchema: z.object({
67
67
  name: NAME_FIELD,
68
68
  description: z
@@ -124,6 +124,7 @@ export function registerLibraryTools(server, client) {
124
124
  path: `characters/${args.name}`,
125
125
  target: args.name,
126
126
  payload: { name: args.name },
127
+ args,
127
128
  confirmToken: args.confirmToken,
128
129
  });
129
130
  if (gate)
@@ -138,7 +139,7 @@ export function registerLibraryTools(server, client) {
138
139
  description: 'Save a named style preset to this account\'s persistent reference library: a reusable prompt fragment (and ' +
139
140
  'optionally a reference image) that a single word then applies to any generation. No expiry; re-saving a name replaces it. ' +
140
141
  USE_STYLE_HINT,
141
- annotations: { readOnlyHint: false, openWorldHint: false },
142
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
142
143
  inputSchema: z.object({
143
144
  name: NAME_FIELD,
144
145
  prompt_fragment: z
@@ -193,6 +194,7 @@ export function registerLibraryTools(server, client) {
193
194
  path: `styles/${args.name}`,
194
195
  target: args.name,
195
196
  payload: { name: args.name },
197
+ args,
196
198
  confirmToken: args.confirmToken,
197
199
  });
198
200
  if (gate)
@@ -1,8 +1,8 @@
1
- import { minifiedResult } from '@chrischall/mcp-utils';
1
+ import { minifiedResult, toolAnnotations } from '@chrischall/mcp-utils';
2
2
  export function registerModelTools(server, client) {
3
3
  server.registerTool('gemini_list_models', {
4
4
  description: 'List the Gemini image-generation models available to your API key (Nano Banana / Nano Banana Pro family), and the current default model.',
5
- annotations: { readOnlyHint: true },
5
+ annotations: toolAnnotations({ readOnly: true, openWorld: true }),
6
6
  }, async () => {
7
7
  const models = await client.listModels();
8
8
  return minifiedResult({ default: client.defaultModel(), models });
@@ -30,7 +30,7 @@ export function registerMusicTools(server, client) {
30
30
  'cannot be refined by a follow-up call, so put the whole brief in the prompt. Runs long — give it a `max_wait_ms` ' +
31
31
  'budget (or `async: true` + gemini_get_result on a local install), or raise `timeout_ms`. Needs a funded account. ' +
32
32
  confirmNote('Local file inputs are confirmed first'),
33
- annotations: { readOnlyHint: false, openWorldHint: true },
33
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
34
34
  inputSchema: z.object({
35
35
  prompt: z.string().min(1).describe('Description of the music: mood, genre, instruments, tempo, structure, or lyrics'),
36
36
  model: z.enum(MUSIC_MODELS).optional().describe(`Lyria model (default: ${DEFAULT_MUSIC_MODEL} — 30s, $0.04). lyria-3.5 and lyria-3-pro-preview run minutes-long at $0.08.`),
@@ -40,7 +40,7 @@ export function registerMusicTools(server, client) {
40
40
  images_base64: imagesBase64Schema(),
41
41
  from_clipboard: z.boolean().optional().describe('Use the image currently on the macOS clipboard as a reference'),
42
42
  filename: z.string().optional().describe('Base filename for the output audio (extension stripped; default: slugified prompt)'),
43
- output_dir: z.string().optional().describe('Directory to write audio to (default: $GEMINI_OUTPUT_DIR or cwd)'),
43
+ output_dir: z.string().optional().describe('Directory to write audio to (default: $GEMINI_OUTPUT_DIR or ~/Downloads/gemini-mcp)'),
44
44
  inline: z.boolean().optional().describe('Return base64 audio inline instead of writing to disk. A 30s track is ~1.9MB of base64 — the default hands back a path instead'),
45
45
  background: z.boolean().optional().describe('Run the generation on Google\'s side and poll it, so a killed job can be recovered by gemini_get_result. Off by default — see gemini_video_generate'),
46
46
  timeout_ms: timeoutMsSchema,
package/dist/tools/set.js CHANGED
@@ -13,7 +13,7 @@ export function registerSetTools(server, client) {
13
13
  description: 'Generate a consistent SET of images: a master image from master_prompt, then one image per scene that references the master so the subject/style stays consistent. Provide `scenes` (explicit per-image prompts) OR `count` (variations of the master). ' +
14
14
  'Scene generations run in parallel (reference_mode "master", the default). On the hosted connector: saved `characters` and a saved `style` can seed the whole set by name, multi-image results include a `bundle_url` zip of every image (one curl instead of N), and `max_wait_ms` returns a pollable job handle if the batch runs long. ' +
15
15
  confirmNote('Local file inputs are confirmed first'),
16
- annotations: { readOnlyHint: false, openWorldHint: true },
16
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
17
17
  inputSchema: z.object({
18
18
  master_prompt: z.string().min(1).describe('Prompt for the master/reference image'),
19
19
  scenes: z.array(z.string().min(1)).min(1).max(8).optional().describe('Per-image prompts (1-8); each references the master'),
@@ -300,7 +300,7 @@ export const sharedImageSchema = {
300
300
  .describe('Exact output aspect ratio. For a plain landscape/portrait/square request, `orientation` is the shorthand; this wins if both are given.'),
301
301
  orientation: orientationSchema,
302
302
  image_size: z.enum(IMAGE_SIZES).optional().describe('Output resolution (512 = 0.5K, Flash-only)'),
303
- output_dir: z.string().optional().describe('Directory to write images to (default: $GEMINI_OUTPUT_DIR or cwd)'),
303
+ output_dir: z.string().optional().describe('Directory to write images to (default: $GEMINI_OUTPUT_DIR or ~/Downloads/gemini-mcp)'),
304
304
  inline: z.boolean().optional().describe('Return the image as an inline image block you can SEE, instead of a path (stdio) or link (hosted). The default costs nothing to carry and hands back a reference you can reuse; use this when you need to check the result yourself. On the hosted connector gemini_view_media does the same for an image you already have'),
305
305
  seed: z.number().int().optional().describe('Seed for reproducible generation; random if omitted'),
306
306
  thinking_level: z.enum(['minimal', 'high']).optional().describe('Reasoning depth (Gemini 3 models); higher can help complex/structural edits'),
@@ -33,7 +33,7 @@ export function registerUsageTools(server, client) {
33
33
  description: 'Token usage for this session so far — what every generation has cost in tokens, added up. ' +
34
34
  'Call it before and after a workflow and subtract to get that workflow\'s cost; call it after a ' +
35
35
  'single generation for that call\'s. Reports tokens AND an estimated USD cost, priced per call against each call\'s own model and stamped with the date its rates were read (override with GEMINI_RATE_CARD). Note there is no account-balance endpoint to query — Google Cloud is post-paid and its billing data lags by hours — so this is the accurate way to attribute spend to a call.',
36
- annotations: toolAnnotations({ readOnly: true }),
36
+ annotations: toolAnnotations({ readOnly: true, openWorld: false }),
37
37
  inputSchema: z.object({
38
38
  reset: z
39
39
  .boolean()
@@ -23,11 +23,9 @@ const VIDEO_RESOLUTIONS = ['360p', '720p', '1080p', '4k'];
23
23
  // escape hatch for a video model that rejects delivery outright.
24
24
  const VIDEO_DELIVERY = ['inline', 'uri'];
25
25
  // The most-recent video interaction id (for continue_last) lives on
26
- // `client.session`, NOT at module scope — one Cloudflare isolate serves many
27
- // authenticated connector sessions, so a module-level id would let one user's
28
- // continue_last resume another user's interaction under their own key. Video is
29
- // not registered on the Worker today, but the hazard is identical, so it is
30
- // scoped the same way. It stays separate from the image interact memory: a
26
+ // `client.session`, NOT at module scope — on any host serving several users
27
+ // from one process, a module-level id would let one user's continue_last
28
+ // resume another user's interaction (see the invariant in src/session.ts). It stays separate from the image interact memory: a
31
29
  // "last video" and a "last image" are distinct chains. See src/session.ts.
32
30
  export function registerVideoTools(server, client) {
33
31
  server.registerTool('gemini_video_generate', {
@@ -38,7 +36,7 @@ export function registerVideoTools(server, client) {
38
36
  'Output is written to disk as MP4 (video has no inline MCP block). Video runs long — give it a `max_wait_ms` budget ' +
39
37
  '(or `async: true` + gemini_get_result on a local install), or raise `timeout_ms`. Needs a funded account. ' +
40
38
  confirmNote('Local file inputs are confirmed first'),
41
- annotations: { readOnlyHint: false, openWorldHint: true },
39
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
42
40
  inputSchema: z.object({
43
41
  prompt: z.string().min(1).describe('Description of the video to generate (or the edit instruction when task=edit)'),
44
42
  aspect_ratio: z.enum(VIDEO_ASPECT_RATIOS).optional().describe('Exact output aspect ratio (omni: 16:9 or 9:16). `orientation` is the plain-language shorthand; this wins if both are given.'),
@@ -51,7 +49,7 @@ export function registerVideoTools(server, client) {
51
49
  images_base64: imagesBase64Schema(),
52
50
  from_clipboard: z.boolean().optional().describe('Use the image currently on the macOS clipboard as a reference'),
53
51
  filename: z.string().optional().describe('Base filename for the output video (extension stripped; default: slugified prompt)'),
54
- output_dir: z.string().optional().describe('Directory to write the video to (default: $GEMINI_OUTPUT_DIR or cwd)'),
52
+ output_dir: z.string().optional().describe('Directory to write the video to (default: $GEMINI_OUTPUT_DIR or ~/Downloads/gemini-mcp)'),
55
53
  model: z
56
54
  .string()
57
55
  .regex(/^[\w.-]+$/, 'must be a bare model id (letters, digits, ".", "_", "-")')
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.3.2'; // x-release-please-version
2
+ export const VERSION = '2.3.4'; // x-release-please-version
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chrischall/gemini-mcp",
3
- "version": "2.3.2",
3
+ "version": "2.3.4",
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>",
@@ -46,7 +46,7 @@
46
46
  "typecheck": "tsc -p tsconfig.test.json"
47
47
  },
48
48
  "dependencies": {
49
- "@chrischall/mcp-utils": "^2.14.0",
49
+ "@chrischall/mcp-utils": "^3.0.0",
50
50
  "@modelcontextprotocol/server": "^2.3.0",
51
51
  "dotenv": "^18.0.2",
52
52
  "zod": "^4.6.5"
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.3.2",
9
+ "version": "2.3.4",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "@chrischall/gemini-mcp",
14
- "version": "2.3.2",
14
+ "version": "2.3.4",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },
@@ -19,7 +19,7 @@
19
19
  {
20
20
  "name": "GEMINI_API_KEY",
21
21
  "description": "Your Google Gemini API key (aistudio.google.com/apikey)",
22
- "isRequired": true,
22
+ "isRequired": false,
23
23
  "format": "string",
24
24
  "isSecret": true
25
25
  },
@@ -57,6 +57,41 @@
57
57
  "isRequired": false,
58
58
  "format": "string",
59
59
  "isSecret": false
60
+ },
61
+ {
62
+ "name": "GEMINI_TIMEOUT_MS",
63
+ "description": "Upstream request timeout in ms (default: 60000, or 120000 for image_size: \"4K\"); each generation tool also takes a per-call timeout_ms",
64
+ "isRequired": false,
65
+ "format": "string",
66
+ "isSecret": false
67
+ },
68
+ {
69
+ "name": "GEMINI_HEARTBEAT_MS",
70
+ "description": "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",
71
+ "isRequired": false,
72
+ "format": "string",
73
+ "isSecret": false
74
+ },
75
+ {
76
+ "name": "GEMINI_CHAIN_RETRY_MS",
77
+ "description": "How long to wait out interactions-store lag when a chained call 404s (default: 120000; 0 disables retrying)",
78
+ "isRequired": false,
79
+ "format": "string",
80
+ "isSecret": false
81
+ },
82
+ {
83
+ "name": "GEMINI_DEBUG",
84
+ "description": "When set, emit heartbeat diagnostics to stderr. Off by default.",
85
+ "isRequired": false,
86
+ "format": "string",
87
+ "isSecret": false
88
+ },
89
+ {
90
+ "name": "GEMINI_RATE_CARD",
91
+ "description": "Override/extend the shipped USD-per-1M-token rates used by gemini_token_usage's cost estimate, e.g. {\"gemini-3-pro-image\":{\"input\":2,\"text_output\":12,\"image_output\":120}}. Malformed JSON is ignored (a bad card must not fail generations).",
92
+ "isRequired": false,
93
+ "format": "string",
94
+ "isSecret": false
60
95
  }
61
96
  ]
62
97
  }
@@ -70,7 +70,7 @@ Note: Image generation requires a billing-enabled Google Cloud project.
70
70
  |---|---|---|
71
71
  | `GEMINI_API_KEY` | Yes | Your Google Gemini API key |
72
72
  | `GEMINI_IMAGE_MODEL` | No | Override the default image model (default: `gemini-3.1-flash-image`) |
73
- | `GEMINI_OUTPUT_DIR` | No | Default directory for saved images (default: current working directory) |
73
+ | `GEMINI_OUTPUT_DIR` | No | Default directory for saved images (default: ~/Downloads/gemini-mcp) |
74
74
  | `GEMINI_INPUT_DIR` | No | Directory to resolve bare input-image filenames against (e.g. point at Cowork's `uploads/` folder so `images: ["house.jpg"]` works) |
75
75
 
76
76
  ## Tools
@@ -307,7 +307,7 @@ Condensed from Google's official Nano Banana prompting guide. Core rule:
307
307
 
308
308
  ## Notes
309
309
 
310
- - **Confirmations.** Local file inputs (`images`, `master_images`, `video_path`, `gemini_upload_file`'s `path`) and every delete are confirmed before anything is sent: a confirmation prompt where the client supports one; otherwise the first call does nothing and returns `status: "confirmation-required"` with a `preview` (resolved paths, MIME types, sizes — or the method/path being deleted) and a `confirmToken`. Show the preview to the user, and only after they approve call again with the **same arguments** plus `confirmToken`. The token is single-use, expires (default 10 min), and is bound to those exact arguments — change anything and the call is refused (`DRAFT_CHANGED`, with a fresh preview and token). `MCP_CONFIRM_MODE` on the server picks `ask-user` (default), `auto` (the model may approve after reviewing the preview) or `refuse`. Text prompts, URLs, `files/` references and base64 inputs are not gated.
310
+ - **Confirmations.** Local file inputs (`images`, `master_images`, `video_path`, `gemini_upload_file`'s `path`) and every delete are confirmed before anything is sent: a confirmation prompt where the client supports one (unless the server sets `MCP_CONFIRM_ELICITATION=off`); otherwise the first call does nothing and returns `status: "confirmation-required"` with a `preview` (resolved paths, MIME types, sizes — or the method/path being deleted) and a `confirmToken`. Show the preview to the user, and only after they approve call again with the **same arguments** plus `confirmToken`. The token is single-use, expires (default 10 min), and is bound to those exact arguments — change anything and the call is refused (`DRAFT_CHANGED`, with a fresh preview and token). `MCP_CONFIRM_MODE` on the server picks `ask-user` (default), `auto` (the model may approve after reviewing the preview) or `refuse`. Text prompts, URLs, `files/` references and base64 inputs are not gated.
311
311
  - **Input images** accept either file **paths** (`images` / `master_images`) or **base64/data-URI values** (`images_base64` / `master_images_base64`).
312
312
  - **`seed`** makes a result reproducible; it's echoed in the result metadata (a random one is chosen + echoed when omitted). `count>1` uses `seed, seed+1, …` so the images differ. Determinism isn't fully guaranteed by the model.
313
313
  - **`filename`/`basename`** set the output name (extension stripped); names never overwrite (a `-2`, `-3` suffix is added). The result echoes the absolute path(s), `model`, `seed`, and aspect/size.
@@ -318,7 +318,7 @@ Condensed from Google's official Nano Banana prompting guide. Core rule:
318
318
  - **`search_types`** (`gemini_interact` only): `["web_search", "image_search"]` picks the grounding search types (setting it implies `google_search`). **`image_search`** (gemini-3.1-flash-image only) pulls web images via Google Image Search as *visual* references — useful for real-world subjects (a specific butterfly species, a landmark, a product). ⚠️ Two catches: Google ToS require **displaying the returned `grounding.search_suggestions` HTML chips** to the user, and image_search won't depict real people from web images.
319
319
  - **`video_url`** (a public YouTube URL, on `gemini_image_generate` / `gemini_interact`) generates an image from a video reference — **requires a Flash model** (e.g. `model: "gemini-3.1-flash-image"`). For a **local video file**, use **`video_path`** instead: the file is uploaded to the Gemini Files API (streamed from disk, 2 GB max), waited to `ACTIVE`, and referenced by its `files/…` uri. The result metadata echoes `video_file` (`{uri, name, expires}`, ~48h retention) — reuse that uri as `video_url` in later calls to skip re-uploading.
320
320
  - **`gemini_interact`** is the multi-turn path: it returns an `interaction_id`; thread it back via `previous_interaction_id` for conversational refinement. Output is **JPEG only**. (The Interactions API is GA as of 2026-07; it uses a different request shape than the `generate`/`edit`/`set` tools.)
321
- - `output_dir` per-call overrides `$GEMINI_OUTPUT_DIR` overrides cwd. `inline: true` returns bytes (with a metadata text block) instead of writing.
321
+ - `output_dir` per-call overrides `$GEMINI_OUTPUT_DIR` overrides `~/Downloads/gemini-mcp`. `inline: true` returns bytes (with a metadata text block) instead of writing.
322
322
  - `count` and `scenes` are mutually exclusive in `gemini_image_set`; `reference_mode: "chain"` references the previous image instead of the master.
323
323
  - Aspect ratios: `1:1`, `16:9`, `9:16`, `4:3`, `3:4`, `2:3`, `3:2`, … · Image sizes: `512` (0.5K, Flash only), `1K`, `2K`, `4K`. `4K` is the max native output — true 18×24 in @ 300 DPI (5400×7200) needs an external upscale step.
324
324
  - All generated images carry a **SynthID** watermark (Google).