@chrischall/gemini-mcp 0.6.1 → 0.7.1

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.
@@ -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": "0.6.1"
10
+ "version": "0.7.1"
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": "0.6.1",
18
+ "version": "0.7.1",
19
19
  "author": {
20
20
  "name": "Chris Hall"
21
21
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "gemini-mcp",
3
3
  "displayName": "Gemini Images",
4
- "version": "0.6.1",
4
+ "version": "0.7.1",
5
5
  "description": "MCP server for Google Gemini image generation and editing — generate and edit images via the Gemini API",
6
6
  "author": {
7
7
  "name": "Chris Hall",
package/README.md CHANGED
@@ -13,7 +13,7 @@ Developed and maintained by AI (Claude Code).
13
13
  | Variable | Required | Description |
14
14
  |---|---|---|
15
15
  | `GEMINI_API_KEY` | Yes | Your Google Gemini API key ([aistudio.google.com/apikey](https://aistudio.google.com/apikey)) |
16
- | `GEMINI_IMAGE_MODEL` | No | Override the default image model (default: `gemini-3-pro-image`) |
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
19
 
@@ -23,9 +23,9 @@ Developed and maintained by AI (Claude Code).
23
23
  |------|-------------|
24
24
  | `gemini_list_models` | List available Gemini image models and the current default |
25
25
  | `gemini_generate_image` | Generate image(s) from a text prompt |
26
- | `gemini_edit_image` | Edit or compose one or more input images with a text instruction |
26
+ | `gemini_edit_image` | One-off edits or multi-image composition with a text instruction (for a series of edits, use `gemini_interact`) |
27
27
  | `gemini_generate_set` | Generate a master image plus N consistent images referencing it |
28
- | `gemini_interact` | Multi-turn generation/editing via the Interactions API (Beta); returns an `interaction_id` to refine the same image conversationally |
28
+ | `gemini_interact` | Preferred tool for iterative refinement: multi-turn generation/editing via the Interactions API — chain the returned `interaction_id` via `previous_interaction_id` (or `continue_last: true`) |
29
29
 
30
30
  ## Quick Start
31
31
 
package/SKILL.md CHANGED
@@ -69,7 +69,7 @@ Note: Image generation requires a billing-enabled Google Cloud project.
69
69
  | Variable | Required | Description |
70
70
  |---|---|---|
71
71
  | `GEMINI_API_KEY` | Yes | Your Google Gemini API key |
72
- | `GEMINI_IMAGE_MODEL` | No | Override the default image model (default: `gemini-3-pro-image`) |
72
+ | `GEMINI_IMAGE_MODEL` | No | Override the default image model (default: `gemini-3.1-flash-image`) |
73
73
  | `GEMINI_OUTPUT_DIR` | No | Default directory for saved images (default: current working directory) |
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
 
@@ -80,6 +80,14 @@ Note: Image generation requires a billing-enabled Google Cloud project.
80
80
  |------|-------------|
81
81
  | `gemini_list_models` | List available Gemini image models and the current default |
82
82
 
83
+ **Which model to pick** (per-call `model`, or `GEMINI_IMAGE_MODEL`):
84
+
85
+ | Model | When to use | Reference-image caps (of 14 max) |
86
+ |-------|-------------|----------------------------------|
87
+ | `gemini-3.1-flash-image` (Nano Banana 2) | The versatile generalist workhorse for all tasks — balances speed with state-of-the-art 4K generation, world knowledge, and reliable text rendering; excels at multi-reference-image processing and consistency. Only model with video input + image_search grounding | 10 objects + 4 characters + 3 style refs |
88
+ | `gemini-3-pro-image` (Nano Banana Pro) | The premium choice for the most complex visual tasks — highest world knowledge, advanced localization, accurate brand consistency, precision creative control | 6 objects + 5 characters |
89
+ | `gemini-3.1-flash-lite-image` (Nano Banana 2 Lite) | The fastest/cheapest for simple tasks — 1K output only, no Google Search grounding | 14 objects (no character consistency) |
90
+
83
91
  ### Image Generation
84
92
  | Tool | Description |
85
93
  |------|-------------|
@@ -87,10 +95,10 @@ Note: Image generation requires a billing-enabled Google Cloud project.
87
95
  | `gemini_edit_image(prompt, images?, images_base64?, google_search?, seed?, filename?, model?, aspect_ratio?, image_size?, thinking_level?, output_dir?, inline?)` | Edit or compose input image(s) — by **path** (`images`) or **value** (`images_base64`: data URI or raw base64) — with a text instruction. Requires ≥1 input |
88
96
  | `gemini_generate_set(master_prompt, scenes? \| count?, reference_mode?, master_images?, master_images_base64?, google_search?, seed?, basename?, model?, thinking_level?, ...)` | Master image (optionally seeded from a reference photo) plus N consistent images referencing it |
89
97
 
90
- ### Multi-turn (Interactions API — Beta)
98
+ ### Multi-turn (Interactions API)
91
99
  | Tool | Description |
92
100
  |------|-------------|
93
- | `gemini_interact(input, previous_interaction_id?, images?, images_base64?, video_url?, video_path?, google_search?, model?, aspect_ratio?, image_size?, thinking_level?, filename?, output_dir?, inline?)` | Generate/edit via Gemini's **Interactions API**. Returns an `interaction_id`; pass it back as `previous_interaction_id` to **iteratively refine the same image** conversationally — the recommended way to make incremental edits. Output is **JPEG**. (Beta API.) |
101
+ | `gemini_interact(input, previous_interaction_id?, continue_last?, images?, images_base64?, video_url?, video_path?, google_search?, search_types?, model?, aspect_ratio?, image_size?, thinking_level?, filename?, output_dir?, inline?)` | **Preferred tool for iterative refinement.** Generate/edit via Gemini's **Interactions API**. Returns an `interaction_id`; pass it back as `previous_interaction_id` (or set `continue_last: true` to reuse the session's most recent one) to **iteratively refine the same image** conversationally — do NOT start a new interaction or re-upload the image per tweak. Output is **JPEG**. |
94
102
 
95
103
  ## Workflows
96
104
 
@@ -148,8 +156,10 @@ r1 = gemini_interact(input: "a cozy reading nook, watercolor")
148
156
  r2 = gemini_interact(input: "add a sleeping cat on the chair",
149
157
  previous_interaction_id: r1.interaction_id)
150
158
  → refined image that preserves r1; returns a NEW interaction_id
159
+ r3 = gemini_interact(input: "warmer lighting", continue_last: true)
160
+ → same chain, without threading the id (uses the session's most recent interaction)
151
161
  ```
152
- Prefer this over re-running `gemini_edit_image` when you're making a *series* of incremental edits — the model keeps the prior result in context.
162
+ Prefer this over re-running `gemini_edit_image` when you're making a *series* of incremental edits — the model keeps the prior result in context. Every result echoes `interaction_id` (and `previous_interaction_id` when chaining) plus a `hint` with the exact follow-up call.
153
163
  **⚠️ Chat-pasted/attached images can't be fed to these tools directly.** A pasted
154
164
  image reaches the assistant as a *vision* block — the assistant can SEE it but
155
165
  never receives the original bytes, and the host doesn't write it to disk. So
@@ -170,6 +180,36 @@ extraction:
170
180
  - **`GEMINI_INPUT_DIR`** — point it at a folder (e.g. Cowork's `uploads/`); then a
171
181
  **bare filename** resolves against it: `gemini_edit_image(prompt: "…", images: ["house.jpg"])`.
172
182
 
183
+ ## Prompting playbook
184
+
185
+ Condensed from Google's official Nano Banana prompting guide. Core rule:
186
+ **describe the scene, don't list keywords** — narrative sentences beat tag soups.
187
+
188
+ **Best practices:**
189
+ - **Be hyper-specific.** "Ornate elven plate armor, etched with silver leaf patterns" beats "fantasy armor".
190
+ - **Give context & intent.** "Create a logo for a high-end minimalist skincare brand" beats "create a logo".
191
+ - **Iterate conversationally** (`gemini_interact`): "warmer lighting", "same, but more serious expression".
192
+ - **Step-by-step for complex scenes.** "First, a misty forest background. Then a stone altar in the foreground. Finally a glowing sword on the altar."
193
+ - **Semantic negatives.** Describe what you want positively — "an empty, deserted street" — instead of "no cars".
194
+ - **Camera language controls composition.** Wide-angle / macro / low-angle perspective / 85mm portrait lens / three-point softbox lighting.
195
+
196
+ **Generation templates (abbreviated):**
197
+ - *Photorealistic:* `photorealistic [shot type] of [subject] in [setting], [lighting], shot from [angle] with [lens]`
198
+ - *Sticker/illustration:* `[style] sticker of [subject] doing [activity], bold outlines, cel-shading, [palette], white background`
199
+ - *Text in image:* `create a [type] for [brand] with the text "[exact text]" in a [font style]` — Gemini renders text well; Pro is best for professional assets. Tip: generate the wording first, then ask for the image containing it.
200
+ - *Product shot:* `high-resolution studio-lit photo of [product] on [surface], [lighting setup], [angle], sharp focus on [detail]`
201
+ - *Minimalist/negative space:* `single [subject] in [frame position], vast empty [color] background` — for text-overlay backgrounds.
202
+ - *Comic/storyboard:* `make a 3 panel comic in [style]; put the character in [scene]` (Pro or 3.1 Flash).
203
+
204
+ **Editing templates (abbreviated):**
205
+ - *Add/remove:* `using the provided image of [subject], [add/remove] [element]; match the original style/lighting/perspective`
206
+ - *Inpaint (semantic mask):* `change only the [element] to [new element]; keep everything else exactly the same`
207
+ - *Style transfer:* `transform the photo of [subject] into the style of [artist/style]; preserve composition`
208
+ - *Compose:* `take the [element from image 1] and place it with [element from image 2]; adjust lighting/shadows to match`
209
+ - *Detail preservation:* describe the critical element (face, logo) in detail and say it must "remain completely unchanged"
210
+ - *Sketch → finished:* `turn this rough sketch of [subject] into a [style] photo; keep [features], add [details]`
211
+ - *Character 360°:* iterate angles via `gemini_interact` ("in profile looking right"), feeding prior outputs back for consistency
212
+
173
213
  ## Notes
174
214
 
175
215
  - **Input images** accept either file **paths** (`images` / `master_images`) or **base64/data-URI values** (`images_base64` / `master_images_base64`).
@@ -178,12 +218,15 @@ extraction:
178
218
  - **No edit-strength control.** Gemini exposes no denoise/strength knob, and Nano Banana over-preserves the input — big structural edits ("move/remove/shrink", add a mat border) are often ignored. Workarounds: reroll with a different `seed`, raise `thinking_level` to `high`, use forceful wording, do layout changes (padding/borders) externally, or use `gemini_interact` multi-turn.
179
219
  - **`thinking_level`** (`minimal`/`high`, Gemini 3 models) controls reasoning depth — `high` can improve complex compositions/edits at higher latency/cost.
180
220
  - **Model text.** When the model returns a caption/explanation (mostly Gemini 3 **Pro**), it's surfaced as `text` in the result metadata.
181
- - **`google_search: true`** grounds the image in live Google Search (current events, weather, real data — great for infographics). The result metadata includes `grounding` with the `queries` run and the `sources` (`{uri, title}`) used. (`gemini_interact` surfaces `grounding.queries` only — the Interactions API returns no clean source list.)
221
+ - **`google_search: true`** grounds the image in live Google Search (current events, weather, real data — great for infographics). The result metadata includes `grounding` with the `queries` run and the `sources` (`{uri, title}`) used. (`gemini_interact` surfaces `grounding.queries` — the Interactions API returns no clean source list.)
222
+ - **`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.
182
223
  - **`video_url`** (a public YouTube URL, on `gemini_generate_image` / `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.
183
- - **`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**. It's a **Beta** API (separate from the stable `generate`/`edit`/`set` tools).
224
+ - **`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.)
184
225
  - `output_dir` per-call overrides `$GEMINI_OUTPUT_DIR` overrides cwd. `inline: true` returns bytes (with a metadata text block) instead of writing.
185
226
  - `count` and `scenes` are mutually exclusive in `gemini_generate_set`; `reference_mode: "chain"` references the previous image instead of the master.
186
227
  - 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.
187
228
  - All generated images carry a **SynthID** watermark (Google).
188
229
  - The model can mis-render text/Roman numerals (e.g. years) — verify any text in the output; it's a model limitation, not a tool setting.
230
+ - **Best-performance languages:** EN, plus ar, de, es-MX, fr, hi, id, it, ja, ko, pt-BR, ru, ua, vi, zh-CN — prefer prompting in one of these.
231
+ - Asking the *model* for "N images" in one prompt is unreliable (documented limitation) — use the `count` parameter instead; it makes N independent calls with distinct seeds.
189
232
  - Server logs to stderr only — stdout is reserved for JSON-RPC.
package/dist/bundle.js CHANGED
@@ -32510,7 +32510,7 @@ var init_clipboard = __esm({
32510
32510
  init_dist();
32511
32511
 
32512
32512
  // src/version.ts
32513
- var VERSION = "0.6.1";
32513
+ var VERSION = "0.7.1";
32514
32514
 
32515
32515
  // src/tools/models.ts
32516
32516
  init_dist();
@@ -32521,7 +32521,7 @@ import { basename, dirname, join } from "node:path";
32521
32521
  import { fileURLToPath } from "node:url";
32522
32522
 
32523
32523
  // src/models.ts
32524
- var DEFAULT_IMAGE_MODEL = "gemini-3-pro-image";
32524
+ var DEFAULT_IMAGE_MODEL = "gemini-3.1-flash-image";
32525
32525
  function resolveModel(perCall, envOverride) {
32526
32526
  return perCall?.trim() || envOverride?.trim() || DEFAULT_IMAGE_MODEL;
32527
32527
  }
@@ -32539,12 +32539,13 @@ await loadDotenvSafely({ path: join(__dirname, "..", ".env"), override: false })
32539
32539
  var BASE_URL = "https://generativelanguage.googleapis.com/v1beta";
32540
32540
  var SERVICE = "Gemini";
32541
32541
  var INTERACTIONS_SERVICE = "Gemini Interactions";
32542
- var API_REVISION = "2026-05-20";
32543
32542
  var REQUEST_TIMEOUT_MS = 6e4;
32544
32543
  var UPLOAD_BASE_URL = "https://generativelanguage.googleapis.com/upload/v1beta";
32545
32544
  var FILE_MAX_BYTES = 2 * 1024 ** 3;
32546
32545
  var FILE_POLL_INTERVAL_MS = 2e3;
32547
32546
  var FILE_POLL_MAX_ATTEMPTS = 150;
32547
+ var CHAIN_404_RETRIES = 2;
32548
+ var CHAIN_404_RETRY_MS = 2e3;
32548
32549
  var GeminiClient = class {
32549
32550
  apiKey;
32550
32551
  configError;
@@ -32575,7 +32576,6 @@ var GeminiClient = class {
32575
32576
  this.interactionsApi = createApiClient({
32576
32577
  baseUrl: BASE_URL,
32577
32578
  serviceName: INTERACTIONS_SERVICE,
32578
- baseHeaders: { "Api-Revision": API_REVISION },
32579
32579
  ...shared
32580
32580
  });
32581
32581
  }
@@ -32753,18 +32753,48 @@ var GeminiClient = class {
32753
32753
  const body = { model, input: inputParts, response_format: responseFormat };
32754
32754
  if (opts.thinkingLevel !== void 0) body.generation_config = { thinking_level: opts.thinkingLevel };
32755
32755
  if (opts.previousInteractionId !== void 0) body.previous_interaction_id = opts.previousInteractionId;
32756
- if (opts.googleSearch) body.tools = [{ type: "google_search" }];
32757
- const data = await this.interactionsApi.fetchJson("POST", "/interactions", {
32758
- body
32759
- });
32756
+ if (opts.googleSearch || opts.searchTypes?.length) {
32757
+ const searchTool = { type: "google_search" };
32758
+ if (opts.searchTypes?.length) searchTool.search_types = opts.searchTypes;
32759
+ body.tools = [searchTool];
32760
+ }
32761
+ let data;
32762
+ for (let attempt = 0; ; attempt++) {
32763
+ try {
32764
+ data = await this.interactionsApi.fetchJson("POST", "/interactions", {
32765
+ body
32766
+ });
32767
+ break;
32768
+ } catch (err) {
32769
+ if (!(opts.previousInteractionId && err instanceof ApiError && err.status === 404)) throw err;
32770
+ if (attempt < CHAIN_404_RETRIES) {
32771
+ await this.sleep(CHAIN_404_RETRY_MS * (attempt + 1));
32772
+ continue;
32773
+ }
32774
+ throw new McpToolError(
32775
+ `Previous interaction "${opts.previousInteractionId}" was not found (retried ${CHAIN_404_RETRIES}\xD7 \u2014 the interactions store can lag briefly after a turn completes). If the id is old: interactions are retained 55 days on the paid tier (1 day on the free tier) and are scoped to the API key that created them.`,
32776
+ {
32777
+ hint: "Retry once more in a few seconds; if it keeps failing, start a new chain: call gemini_interact again with the last output image passed via `images` (or `images_base64`) plus the same instruction, then chain the NEW interaction id.",
32778
+ cause: err
32779
+ }
32780
+ );
32781
+ }
32782
+ }
32760
32783
  const images = [];
32761
32784
  const textParts = [];
32762
32785
  const searchQueries = [];
32786
+ const searchSuggestions = /* @__PURE__ */ new Set();
32763
32787
  for (const step of data.steps ?? []) {
32764
32788
  if (step.type === "google_search_call") {
32765
32789
  for (const q of step.arguments?.queries ?? []) if (q?.trim()) searchQueries.push(q);
32766
32790
  continue;
32767
32791
  }
32792
+ if (step.type === "google_search_result") {
32793
+ if (opts.searchTypes?.includes("image_search")) {
32794
+ for (const r of step.result ?? []) if (r.search_suggestions?.trim()) searchSuggestions.add(r.search_suggestions);
32795
+ }
32796
+ continue;
32797
+ }
32768
32798
  if (step.type !== "model_output") continue;
32769
32799
  for (const parts of [step.content ?? [], step.summary ?? []]) {
32770
32800
  for (const part of parts) {
@@ -32782,7 +32812,12 @@ var GeminiClient = class {
32782
32812
  });
32783
32813
  }
32784
32814
  const resultText = textParts.join("\n") || void 0;
32785
- const grounding = searchQueries.length > 0 ? { queries: searchQueries } : void 0;
32815
+ let grounding;
32816
+ if (searchQueries.length > 0 || searchSuggestions.size > 0) {
32817
+ grounding = {};
32818
+ if (searchQueries.length > 0) grounding.queries = searchQueries;
32819
+ if (searchSuggestions.size > 0) grounding.search_suggestions = [...searchSuggestions];
32820
+ }
32786
32821
  return { id: data.id, images, text: resultText, grounding };
32787
32822
  }
32788
32823
  };
@@ -32938,11 +32973,12 @@ init_zod();
32938
32973
  init_dist();
32939
32974
  var ASPECT_RATIOS = ["1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "21:9", "1:4", "4:1", "1:8", "8:1"];
32940
32975
  var IMAGE_SIZES = ["512", "1K", "2K", "4K"];
32976
+ var MODEL_CHOICE_GUIDE = "gemini-3.1-flash-image (Nano Banana 2) is the versatile generalist workhorse \u2014 balances speed with state-of-the-art 4K generation, world knowledge, and reliable text rendering; excels at multi-reference-image processing and consistency. gemini-3-pro-image (Nano Banana Pro) is the premium choice for the most complex visual tasks \u2014 highest world knowledge, advanced localization, accurate brand consistency, precision creative control. gemini-3.1-flash-lite-image (Nano Banana 2 Lite) is the fastest/cheapest for simple tasks (1K only, no search grounding).";
32941
32977
  var sharedImageSchema = {
32942
32978
  // Bare id only: the model is interpolated into the URL path
32943
32979
  // (`/models/${model}:generateContent`), so slashes/colons/queries must be
32944
32980
  // rejected to keep a crafted value from escaping the path segment.
32945
- model: external_exports.string().regex(/^[\w.-]+$/, 'must be a bare model id (letters, digits, ".", "_", "-")').optional().describe("Model id override (default: server default; see gemini_list_models)"),
32981
+ model: external_exports.string().regex(/^[\w.-]+$/, 'must be a bare model id (letters, digits, ".", "_", "-")').optional().describe(`Model id override (default: server default; see gemini_list_models). ${MODEL_CHOICE_GUIDE}`),
32946
32982
  aspect_ratio: external_exports.enum(ASPECT_RATIOS).optional().describe("Output aspect ratio"),
32947
32983
  image_size: external_exports.enum(IMAGE_SIZES).optional().describe("Output resolution (512 = 0.5K, Flash-only)"),
32948
32984
  output_dir: external_exports.string().optional().describe("Directory to write images to (default: $GEMINI_OUTPUT_DIR or cwd)"),
@@ -32992,11 +33028,12 @@ async function emit(named, opts, meta3) {
32992
33028
  }
32993
33029
 
32994
33030
  // src/tools/generate.ts
33031
+ var REFINE_HINT = "To iteratively refine this image, use gemini_interact (pass the output file via `images` on the first call, then chain `previous_interaction_id` on each subsequent call).";
32995
33032
  function registerGenerateTools(server) {
32996
33033
  server.registerTool(
32997
33034
  "gemini_generate_image",
32998
33035
  {
32999
- description: "Generate image(s) from a text prompt with a Gemini image model (Nano Banana / Nano Banana Pro).",
33036
+ description: "Generate image(s) from a text prompt with a Gemini image model (Nano Banana / Nano Banana Pro). If the result will likely be refined iteratively, prefer gemini_interact (multi-turn) as the entry point.",
33000
33037
  annotations: { readOnlyHint: false, openWorldHint: true },
33001
33038
  inputSchema: {
33002
33039
  prompt: external_exports.string().min(1).describe("Text prompt describing the image"),
@@ -33041,13 +33078,14 @@ function registerGenerateTools(server) {
33041
33078
  if (capturedText) meta3.text = capturedText;
33042
33079
  if (capturedGrounding) meta3.grounding = capturedGrounding;
33043
33080
  if (video.videoFileMeta) meta3.video_file = video.videoFileMeta;
33081
+ meta3.hint = REFINE_HINT;
33044
33082
  return emit(named, args, meta3);
33045
33083
  }
33046
33084
  );
33047
33085
  server.registerTool(
33048
33086
  "gemini_edit_image",
33049
33087
  {
33050
- description: "Edit or compose images: provide one or more input images (paths or base64), plus a text instruction. Gemini over-preserves the input; there is no edit-strength control \u2014 for large structural changes, reroll with a different `seed` or more forceful wording.",
33088
+ description: "Edit or compose images: provide one or more input images (paths or base64), plus a text instruction. For a SERIES of successive edits to the same image, prefer gemini_interact (multi-turn) \u2014 it keeps edit context and avoids re-processing the full image each round; use gemini_edit_image for one-off edits or composing multiple distinct inputs. Gemini over-preserves the input; there is no edit-strength control \u2014 for large structural changes, reroll with a different `seed` or more forceful wording.",
33051
33089
  annotations: { readOnlyHint: false, openWorldHint: true },
33052
33090
  inputSchema: {
33053
33091
  prompt: external_exports.string().min(1).describe("Instruction describing the edit or composition"),
@@ -33081,6 +33119,7 @@ function registerGenerateTools(server) {
33081
33119
  const meta3 = buildMeta(model, seed, args);
33082
33120
  if (text) meta3.text = text;
33083
33121
  if (result.grounding) meta3.grounding = result.grounding;
33122
+ meta3.hint = REFINE_HINT;
33084
33123
  return emit([{ image: img, base: slug }], args, meta3);
33085
33124
  }
33086
33125
  );
@@ -33149,18 +33188,20 @@ function registerSetTools(server) {
33149
33188
  // src/tools/interact.ts
33150
33189
  init_zod();
33151
33190
  init_dist();
33191
+ var lastInteractionId;
33152
33192
  function registerInteractTools(server) {
33153
33193
  server.registerTool(
33154
33194
  "gemini_interact",
33155
33195
  {
33156
- description: "Multi-turn image generation/editing via Gemini's Interactions API (Beta). Returns an interaction `id`; pass it back as `previous_interaction_id` to iteratively refine the SAME image conversationally (the recommended way to make incremental edits). Output is JPEG.",
33196
+ description: "Preferred tool for iterative or multi-step refinement of a single image \u2014 multi-turn generation/editing via Gemini's Interactions API. To refine, capture the returned interaction `id` and pass it as `previous_interaction_id` on the next call \u2014 do NOT start a new interaction or re-upload the image for each tweak. `continue_last: true` chains from this session's most recent interaction without threading the id. Output is JPEG.",
33157
33197
  annotations: { readOnlyHint: false, openWorldHint: true },
33158
33198
  inputSchema: {
33159
33199
  input: external_exports.string().min(1).describe("Text prompt or editing instruction"),
33160
33200
  previous_interaction_id: external_exports.string().optional().describe("ID from a prior gemini_interact call \u2014 continues that multi-turn conversation"),
33201
+ 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)"),
33161
33202
  images: external_exports.array(external_exports.string().min(1)).optional().describe("Paths to reference input images"),
33162
33203
  images_base64: external_exports.array(external_exports.string().min(1)).optional().describe("Reference images as base64 strings or data URIs"),
33163
- model: external_exports.string().optional().describe("Model id override (default: server default; see gemini_list_models)"),
33204
+ model: external_exports.string().optional().describe(`Model id override (default: server default; see gemini_list_models). ${MODEL_CHOICE_GUIDE}`),
33164
33205
  aspect_ratio: external_exports.enum(ASPECT_RATIOS).optional().describe("Output aspect ratio"),
33165
33206
  image_size: external_exports.enum(IMAGE_SIZES).optional().describe("Output resolution (512 = 0.5K, Flash-only)"),
33166
33207
  thinking_level: external_exports.enum(["minimal", "high"]).optional().describe("Reasoning depth; higher can help complex/structural edits"),
@@ -33168,12 +33209,24 @@ function registerInteractTools(server) {
33168
33209
  output_dir: external_exports.string().optional().describe("Directory to write images to (default: $GEMINI_OUTPUT_DIR or cwd)"),
33169
33210
  inline: external_exports.boolean().optional().describe("Return base64 images inline instead of writing to disk"),
33170
33211
  google_search: external_exports.boolean().optional().describe("Ground the image in live Google Search results (current events, weather, data)"),
33212
+ search_types: external_exports.array(external_exports.enum(["web_search", "image_search"])).optional().describe(
33213
+ "Grounding search types (implies google_search). image_search (gemini-3.1-flash-image only) uses Google Image Search results as visual references; per Google ToS the returned grounding.search_suggestions HTML must then be displayed to the user. Cannot depict real people from web images."
33214
+ ),
33171
33215
  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)"),
33172
33216
  video_path: videoPathSchema,
33173
33217
  from_clipboard: external_exports.boolean().optional().describe("Use the image currently on the macOS system clipboard as an input (downscaled to JPEG)")
33174
33218
  }
33175
33219
  },
33176
33220
  async (args) => {
33221
+ let previousInteractionId = args.previous_interaction_id;
33222
+ if (!previousInteractionId && args.continue_last) {
33223
+ if (!lastInteractionId) {
33224
+ throw new McpToolError("No previous interaction to continue in this session.", {
33225
+ hint: "Call gemini_interact once without continue_last first, or pass an explicit previous_interaction_id."
33226
+ });
33227
+ }
33228
+ previousInteractionId = lastInteractionId;
33229
+ }
33177
33230
  const inputs = await gatherImageInputs({ images: args.images, images_base64: args.images_base64, from_clipboard: args.from_clipboard });
33178
33231
  const video = await resolveVideoInput(args);
33179
33232
  const r = await client.interact({
@@ -33183,13 +33236,17 @@ function registerInteractTools(server) {
33183
33236
  aspectRatio: args.aspect_ratio,
33184
33237
  imageSize: args.image_size,
33185
33238
  thinkingLevel: args.thinking_level,
33186
- previousInteractionId: args.previous_interaction_id,
33239
+ previousInteractionId,
33187
33240
  googleSearch: args.google_search,
33241
+ searchTypes: args.search_types,
33188
33242
  videoUrl: video.videoUrl,
33189
33243
  videoMimeType: video.videoMimeType
33190
33244
  });
33245
+ lastInteractionId = r.id;
33191
33246
  const model = resolveModel(args.model, readEnvVar("GEMINI_IMAGE_MODEL"));
33192
33247
  const meta3 = { model, interaction_id: r.id };
33248
+ if (previousInteractionId) meta3.previous_interaction_id = previousInteractionId;
33249
+ meta3.hint = `To refine this image, call gemini_interact again with previous_interaction_id: "${r.id}" (or continue_last: true).`;
33193
33250
  if (r.text) meta3.text = r.text;
33194
33251
  if (r.grounding) meta3.grounding = r.grounding;
33195
33252
  if (video.videoFileMeta) meta3.video_file = video.videoFileMeta;
package/dist/client.js CHANGED
@@ -1,15 +1,12 @@
1
1
  import { basename, dirname, join } from 'node:path';
2
2
  import { fileURLToPath } from 'node:url';
3
- import { loadDotenvSafely, readEnvVar, McpToolError, createApiClient, formatApiError, fileBlob } from '@chrischall/mcp-utils';
3
+ import { loadDotenvSafely, readEnvVar, McpToolError, ApiError, createApiClient, formatApiError, fileBlob } from '@chrischall/mcp-utils';
4
4
  import { resolveModel, filterImageModels } from './models.js';
5
5
  const __dirname = dirname(fileURLToPath(import.meta.url));
6
6
  await loadDotenvSafely({ path: join(__dirname, '..', '.env'), override: false });
7
7
  const BASE_URL = 'https://generativelanguage.googleapis.com/v1beta'; // v1 lacks gemini-3-pro-image; confirmed via Task 5
8
8
  const SERVICE = 'Gemini';
9
9
  const INTERACTIONS_SERVICE = 'Gemini Interactions';
10
- // Required revision header for the beta Interactions API (verified 2026-06-08;
11
- // see docs/GEMINI-API.md) — load-bearing, requests 400 without it.
12
- const API_REVISION = '2026-05-20';
13
10
  // Per-request abort budget. Image generation routinely runs 30s+ on the Pro
14
11
  // model, so this is deliberately 60s (NOT the fleet's usual 15–30s).
15
12
  const REQUEST_TIMEOUT_MS = 60_000;
@@ -24,6 +21,13 @@ const FILE_MAX_BYTES = 2 * 1024 ** 3;
24
21
  // in ~1s (verified); longer videos take proportionally longer to process.
25
22
  const FILE_POLL_INTERVAL_MS = 2_000;
26
23
  const FILE_POLL_MAX_ATTEMPTS = 150;
24
+ // The interactions store is eventually consistent: a freshly returned id can
25
+ // 404 for a short window right after its turn completes (observed live
26
+ // 2026-07-06 — an id that 404'd on an immediate chain resolved fine minutes
27
+ // later). A chained 404 generated nothing (no cost), so retry before giving up:
28
+ // attempts wait 2s then 4s (~6s total).
29
+ const CHAIN_404_RETRIES = 2;
30
+ const CHAIN_404_RETRY_MS = 2_000;
27
31
  export class GeminiClient {
28
32
  apiKey;
29
33
  configError;
@@ -58,12 +62,13 @@ export class GeminiClient {
58
62
  fetchImpl: this.fetchImpl,
59
63
  };
60
64
  this.api = createApiClient({ baseUrl: BASE_URL, serviceName: SERVICE, ...shared });
61
- // Separate client for the beta Interactions API: same base URL, but every
62
- // request must carry the Api-Revision header (and errors name the API).
65
+ // Separate client for the Interactions API so errors name the right API.
66
+ // The beta-era Api-Revision header is gone: the API went GA and requests
67
+ // succeed without it (verified live 2026-07-06; see docs/GEMINI-API.md).
68
+ // Don't re-add a pinned revision — it would freeze us on old semantics.
63
69
  this.interactionsApi = createApiClient({
64
70
  baseUrl: BASE_URL,
65
71
  serviceName: INTERACTIONS_SERVICE,
66
- baseHeaders: { 'Api-Revision': API_REVISION },
67
72
  ...shared,
68
73
  });
69
74
  }
@@ -252,11 +257,39 @@ export class GeminiClient {
252
257
  body.generation_config = { thinking_level: opts.thinkingLevel };
253
258
  if (opts.previousInteractionId !== undefined)
254
259
  body.previous_interaction_id = opts.previousInteractionId;
255
- if (opts.googleSearch)
256
- body.tools = [{ type: 'google_search' }];
257
- const data = await this.interactionsApi.fetchJson('POST', '/interactions', {
258
- body,
259
- });
260
+ if (opts.googleSearch || opts.searchTypes?.length) {
261
+ const searchTool = { type: 'google_search' };
262
+ if (opts.searchTypes?.length)
263
+ searchTool.search_types = opts.searchTypes;
264
+ body.tools = [searchTool];
265
+ }
266
+ let data;
267
+ for (let attempt = 0;; attempt++) {
268
+ try {
269
+ data = await this.interactionsApi.fetchJson('POST', '/interactions', {
270
+ body,
271
+ });
272
+ break;
273
+ }
274
+ catch (err) {
275
+ // A 404 on a chained call means the referenced interaction wasn't
276
+ // found. Two causes: (a) store lag right after the id was created —
277
+ // retry with backoff; (b) genuinely gone — interactions are retained
278
+ // 55 days (paid tier) / 1 day (free tier) and are scoped to the API
279
+ // key's project. The chain is recoverable from the last output file,
280
+ // so the exhausted-retries error says how.
281
+ if (!(opts.previousInteractionId && err instanceof ApiError && err.status === 404))
282
+ throw err;
283
+ if (attempt < CHAIN_404_RETRIES) {
284
+ await this.sleep(CHAIN_404_RETRY_MS * (attempt + 1));
285
+ continue;
286
+ }
287
+ throw new McpToolError(`Previous interaction "${opts.previousInteractionId}" was not found (retried ${CHAIN_404_RETRIES}× — the interactions store can lag briefly after a turn completes). If the id is old: interactions are retained 55 days on the paid tier (1 day on the free tier) and are scoped to the API key that created them.`, {
288
+ hint: 'Retry once more in a few seconds; if it keeps failing, start a new chain: call gemini_interact again with the last output image passed via `images` (or `images_base64`) plus the same instruction, then chain the NEW interaction id.',
289
+ cause: err,
290
+ });
291
+ }
292
+ }
260
293
  // Only surface the `model_output` step — that's the caller-facing result.
261
294
  // `thought` steps hold internal reasoning (and, with includeThoughts, draft
262
295
  // "thinking" images); collecting those would leak reasoning into `text` and
@@ -265,17 +298,31 @@ export class GeminiClient {
265
298
  const images = [];
266
299
  const textParts = [];
267
300
  const searchQueries = [];
301
+ const searchSuggestions = new Set();
268
302
  for (const step of data.steps ?? []) {
269
303
  // Grounding queries live on the `google_search_call` step's arguments.
270
304
  // (The `google_search_result` step only carries HTML `search_suggestions`
271
305
  // chips — no clean source uri/title list like generateContent's
272
- // groundingChunks — so interact surfaces queries only, not sources.)
306
+ // groundingChunks — so interact surfaces queries, plus the suggestion
307
+ // chips when image_search was requested, ToS below.)
273
308
  if (step.type === 'google_search_call') {
274
309
  for (const q of step.arguments?.queries ?? [])
275
310
  if (q?.trim())
276
311
  searchQueries.push(q);
277
312
  continue;
278
313
  }
314
+ // ToS: image_search results' `search_suggestions` chips MUST be displayed
315
+ // by the caller, so surface them (deduped — the chips repeat) only when
316
+ // image_search grounding was requested; web-only grounding keeps the
317
+ // lean queries-only meta.
318
+ if (step.type === 'google_search_result') {
319
+ if (opts.searchTypes?.includes('image_search')) {
320
+ for (const r of step.result ?? [])
321
+ if (r.search_suggestions?.trim())
322
+ searchSuggestions.add(r.search_suggestions);
323
+ }
324
+ continue;
325
+ }
279
326
  if (step.type !== 'model_output')
280
327
  continue;
281
328
  for (const parts of [step.content ?? [], step.summary ?? []]) {
@@ -295,7 +342,14 @@ export class GeminiClient {
295
342
  });
296
343
  }
297
344
  const resultText = textParts.join('\n') || undefined;
298
- const grounding = searchQueries.length > 0 ? { queries: searchQueries } : undefined;
345
+ let grounding;
346
+ if (searchQueries.length > 0 || searchSuggestions.size > 0) {
347
+ grounding = {};
348
+ if (searchQueries.length > 0)
349
+ grounding.queries = searchQueries;
350
+ if (searchSuggestions.size > 0)
351
+ grounding.search_suggestions = [...searchSuggestions];
352
+ }
299
353
  return { id: data.id, images, text: resultText, grounding };
300
354
  }
301
355
  }
package/dist/models.js CHANGED
@@ -1,5 +1,10 @@
1
- /** Latest top-end image model; overridable per-call and via GEMINI_IMAGE_MODEL. */
2
- export const DEFAULT_IMAGE_MODEL = 'gemini-3-pro-image';
1
+ /**
2
+ * The generalist workhorse (Nano Banana 2) — overridable per-call and via
3
+ * GEMINI_IMAGE_MODEL. The premium gemini-3-pro-image is a deliberate opt-in:
4
+ * it costs more and needs a funded account, so callers reach for it via the
5
+ * `model` param when the task warrants it (see MODEL_CHOICE_GUIDE).
6
+ */
7
+ export const DEFAULT_IMAGE_MODEL = 'gemini-3.1-flash-image';
3
8
  /** Per-call → env override → hardcoded default. Blank/whitespace counts as unset. */
4
9
  export function resolveModel(perCall, envOverride) {
5
10
  return perCall?.trim() || envOverride?.trim() || DEFAULT_IMAGE_MODEL;
@@ -4,9 +4,14 @@ import { resolveModel } from '../models.js';
4
4
  import { client } from '../client.js';
5
5
  import { slugify, baseName, gatherImageInputs } from '../images.js';
6
6
  import { emit, sharedImageSchema, pickSeed, buildMeta, resolveVideoInput, videoPathSchema } from './shared.js';
7
+ // generateContent output can't seed an Interactions-API conversation, so
8
+ // results point follow-up edits at gemini_interact instead of an id.
9
+ const REFINE_HINT = 'To iteratively refine this image, use gemini_interact (pass the output file via `images` on the first call, ' +
10
+ 'then chain `previous_interaction_id` on each subsequent call).';
7
11
  export function registerGenerateTools(server) {
8
12
  server.registerTool('gemini_generate_image', {
9
- description: 'Generate image(s) from a text prompt with a Gemini image model (Nano Banana / Nano Banana Pro).',
13
+ description: 'Generate image(s) from a text prompt with a Gemini image model (Nano Banana / Nano Banana Pro). ' +
14
+ 'If the result will likely be refined iteratively, prefer gemini_interact (multi-turn) as the entry point.',
10
15
  annotations: { readOnlyHint: false, openWorldHint: true },
11
16
  inputSchema: {
12
17
  prompt: z.string().min(1).describe('Text prompt describing the image'),
@@ -61,10 +66,13 @@ export function registerGenerateTools(server) {
61
66
  meta.grounding = capturedGrounding;
62
67
  if (video.videoFileMeta)
63
68
  meta.video_file = video.videoFileMeta;
69
+ meta.hint = REFINE_HINT;
64
70
  return emit(named, args, meta);
65
71
  });
66
72
  server.registerTool('gemini_edit_image', {
67
73
  description: 'Edit or compose images: provide one or more input images (paths or base64), plus a text instruction. ' +
74
+ 'For a SERIES of successive edits to the same image, prefer gemini_interact (multi-turn) — it keeps edit context ' +
75
+ 'and avoids re-processing the full image each round; use gemini_edit_image for one-off edits or composing multiple distinct inputs. ' +
68
76
  'Gemini over-preserves the input; there is no edit-strength control — for large structural changes, reroll with a different `seed` or more forceful wording.',
69
77
  annotations: { readOnlyHint: false, openWorldHint: true },
70
78
  inputSchema: {
@@ -100,6 +108,7 @@ export function registerGenerateTools(server) {
100
108
  meta.text = text;
101
109
  if (result.grounding)
102
110
  meta.grounding = result.grounding;
111
+ meta.hint = REFINE_HINT;
103
112
  return emit([{ image: img, base: slug }], args, meta);
104
113
  });
105
114
  }
@@ -1,14 +1,20 @@
1
1
  import { z } from 'zod';
2
- import { readEnvVar } from '@chrischall/mcp-utils';
2
+ import { McpToolError, readEnvVar } from '@chrischall/mcp-utils';
3
3
  import { resolveModel } from '../models.js';
4
4
  import { client } from '../client.js';
5
5
  import { slugify, baseName, gatherImageInputs } from '../images.js';
6
- import { emit, ASPECT_RATIOS, IMAGE_SIZES, resolveVideoInput, videoPathSchema } from './shared.js';
6
+ import { emit, ASPECT_RATIOS, IMAGE_SIZES, MODEL_CHOICE_GUIDE, resolveVideoInput, videoPathSchema } from './shared.js';
7
+ // The most recent interaction id this server process created — what
8
+ // `continue_last: true` resumes. In-memory only: an MCP server lives for the
9
+ // host session, so "last" means "last in this session".
10
+ let lastInteractionId;
7
11
  export function registerInteractTools(server) {
8
12
  server.registerTool('gemini_interact', {
9
- description: "Multi-turn image generation/editing via Gemini's Interactions API (Beta). " +
10
- 'Returns an interaction `id`; pass it back as `previous_interaction_id` to iteratively ' +
11
- 'refine the SAME image conversationally (the recommended way to make incremental edits). ' +
13
+ description: 'Preferred tool for iterative or multi-step refinement of a single image — ' +
14
+ "multi-turn generation/editing via Gemini's Interactions API. " +
15
+ 'To refine, capture the returned interaction `id` and pass it as `previous_interaction_id` ' +
16
+ 'on the next call — do NOT start a new interaction or re-upload the image for each tweak. ' +
17
+ '`continue_last: true` chains from this session\'s most recent interaction without threading the id. ' +
12
18
  'Output is JPEG.',
13
19
  annotations: { readOnlyHint: false, openWorldHint: true },
14
20
  inputSchema: {
@@ -17,6 +23,10 @@ export function registerInteractTools(server) {
17
23
  .string()
18
24
  .optional()
19
25
  .describe('ID from a prior gemini_interact call — continues that multi-turn conversation'),
26
+ continue_last: z
27
+ .boolean()
28
+ .optional()
29
+ .describe('Continue from the most recent interaction this server created (convenience for previous_interaction_id; an explicit id wins)'),
20
30
  images: z
21
31
  .array(z.string().min(1))
22
32
  .optional()
@@ -28,7 +38,7 @@ export function registerInteractTools(server) {
28
38
  model: z
29
39
  .string()
30
40
  .optional()
31
- .describe('Model id override (default: server default; see gemini_list_models)'),
41
+ .describe(`Model id override (default: server default; see gemini_list_models). ${MODEL_CHOICE_GUIDE}`),
32
42
  aspect_ratio: z.enum(ASPECT_RATIOS).optional().describe('Output aspect ratio'),
33
43
  image_size: z
34
44
  .enum(IMAGE_SIZES)
@@ -54,6 +64,11 @@ export function registerInteractTools(server) {
54
64
  .boolean()
55
65
  .optional()
56
66
  .describe('Ground the image in live Google Search results (current events, weather, data)'),
67
+ search_types: z
68
+ .array(z.enum(['web_search', 'image_search']))
69
+ .optional()
70
+ .describe('Grounding search types (implies google_search). image_search (gemini-3.1-flash-image only) uses Google Image Search results as visual references; ' +
71
+ 'per Google ToS the returned grounding.search_suggestions HTML must then be displayed to the user. Cannot depict real people from web images.'),
57
72
  video_url: z
58
73
  .string()
59
74
  .url()
@@ -66,6 +81,15 @@ export function registerInteractTools(server) {
66
81
  .describe('Use the image currently on the macOS system clipboard as an input (downscaled to JPEG)'),
67
82
  },
68
83
  }, async (args) => {
84
+ let previousInteractionId = args.previous_interaction_id;
85
+ if (!previousInteractionId && args.continue_last) {
86
+ if (!lastInteractionId) {
87
+ throw new McpToolError('No previous interaction to continue in this session.', {
88
+ hint: 'Call gemini_interact once without continue_last first, or pass an explicit previous_interaction_id.',
89
+ });
90
+ }
91
+ previousInteractionId = lastInteractionId;
92
+ }
69
93
  const inputs = await gatherImageInputs({ images: args.images, images_base64: args.images_base64, from_clipboard: args.from_clipboard });
70
94
  const video = await resolveVideoInput(args);
71
95
  const r = await client.interact({
@@ -75,13 +99,18 @@ export function registerInteractTools(server) {
75
99
  aspectRatio: args.aspect_ratio,
76
100
  imageSize: args.image_size,
77
101
  thinkingLevel: args.thinking_level,
78
- previousInteractionId: args.previous_interaction_id,
102
+ previousInteractionId,
79
103
  googleSearch: args.google_search,
104
+ searchTypes: args.search_types,
80
105
  videoUrl: video.videoUrl,
81
106
  videoMimeType: video.videoMimeType,
82
107
  });
108
+ lastInteractionId = r.id;
83
109
  const model = resolveModel(args.model, readEnvVar('GEMINI_IMAGE_MODEL'));
84
110
  const meta = { model, interaction_id: r.id };
111
+ if (previousInteractionId)
112
+ meta.previous_interaction_id = previousInteractionId;
113
+ meta.hint = `To refine this image, call gemini_interact again with previous_interaction_id: "${r.id}" (or continue_last: true).`;
85
114
  if (r.text)
86
115
  meta.text = r.text;
87
116
  if (r.grounding)
@@ -11,6 +11,15 @@ export const IMAGE_SIZES = ['512', '1K', '2K', '4K'];
11
11
  * here so descriptions can't drift between `generate.ts` and `set.ts`. Spread
12
12
  * into each tool's `inputSchema`.
13
13
  */
14
+ /**
15
+ * When to pick which Nano Banana model — shared by every tool's `model` param
16
+ * so the guidance can't drift between them.
17
+ */
18
+ export const MODEL_CHOICE_GUIDE = 'gemini-3.1-flash-image (Nano Banana 2) is the versatile generalist workhorse — balances speed with ' +
19
+ 'state-of-the-art 4K generation, world knowledge, and reliable text rendering; excels at multi-reference-image ' +
20
+ 'processing and consistency. gemini-3-pro-image (Nano Banana Pro) is the premium choice for the most complex ' +
21
+ 'visual tasks — highest world knowledge, advanced localization, accurate brand consistency, precision creative control. ' +
22
+ 'gemini-3.1-flash-lite-image (Nano Banana 2 Lite) is the fastest/cheapest for simple tasks (1K only, no search grounding).';
14
23
  export const sharedImageSchema = {
15
24
  // Bare id only: the model is interpolated into the URL path
16
25
  // (`/models/${model}:generateContent`), so slashes/colons/queries must be
@@ -19,7 +28,7 @@ export const sharedImageSchema = {
19
28
  .string()
20
29
  .regex(/^[\w.-]+$/, 'must be a bare model id (letters, digits, ".", "_", "-")')
21
30
  .optional()
22
- .describe('Model id override (default: server default; see gemini_list_models)'),
31
+ .describe(`Model id override (default: server default; see gemini_list_models). ${MODEL_CHOICE_GUIDE}`),
23
32
  aspect_ratio: z.enum(ASPECT_RATIOS).optional().describe('Output aspect ratio'),
24
33
  image_size: z.enum(IMAGE_SIZES).optional().describe('Output resolution (512 = 0.5K, Flash-only)'),
25
34
  output_dir: z.string().optional().describe('Directory to write images to (default: $GEMINI_OUTPUT_DIR or cwd)'),
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 = '0.6.1'; // x-release-please-version
2
+ export const VERSION = '0.7.1'; // x-release-please-version
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chrischall/gemini-mcp",
3
- "version": "0.6.1",
3
+ "version": "0.7.1",
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>",
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": "0.6.1",
9
+ "version": "0.7.1",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "@chrischall/gemini-mcp",
14
- "version": "0.6.1",
14
+ "version": "0.7.1",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },
@@ -25,7 +25,7 @@
25
25
  },
26
26
  {
27
27
  "name": "GEMINI_IMAGE_MODEL",
28
- "description": "Override the default image model (default: gemini-3-pro-image)",
28
+ "description": "Override the default image model (default: gemini-3.1-flash-image)",
29
29
  "isRequired": false,
30
30
  "format": "string",
31
31
  "isSecret": false