@chrischall/gemini-mcp 0.6.0 → 0.7.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.
@@ -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.0"
10
+ "version": "0.7.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": "0.6.0",
18
+ "version": "0.7.0",
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.0",
4
+ "version": "0.7.0",
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
@@ -5,11 +5,20 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __getProtoOf = Object.getPrototypeOf;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
- var __esm = (fn, res) => function __init() {
9
- return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
8
+ var __esm = (fn, res, err) => function __init() {
9
+ if (err) throw err[0];
10
+ try {
11
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
12
+ } catch (e) {
13
+ throw err = [e], e;
14
+ }
10
15
  };
11
16
  var __commonJS = (cb, mod) => function __require() {
12
- return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
17
+ try {
18
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
19
+ } catch (e) {
20
+ throw mod = 0, e;
21
+ }
13
22
  };
14
23
  var __export = (target, all) => {
15
24
  for (var name in all)
@@ -32375,6 +32384,12 @@ var init_http = __esm({
32375
32384
  }
32376
32385
  });
32377
32386
 
32387
+ // node_modules/@chrischall/mcp-utils/dist/concurrency/index.js
32388
+ var init_concurrency = __esm({
32389
+ "node_modules/@chrischall/mcp-utils/dist/concurrency/index.js"() {
32390
+ }
32391
+ });
32392
+
32378
32393
  // node_modules/@chrischall/mcp-utils/dist/dates/index.js
32379
32394
  var init_dates = __esm({
32380
32395
  "node_modules/@chrischall/mcp-utils/dist/dates/index.js"() {
@@ -32382,7 +32397,7 @@ var init_dates = __esm({
32382
32397
  });
32383
32398
 
32384
32399
  // node_modules/@chrischall/mcp-utils/dist/zod/index.js
32385
- var PositiveInt, NonNegInt, NonEmptyString, IsoDate, IsoTime, schemaOrigin, schemaConfirm, paginationSchema, pageSchema;
32400
+ var PositiveInt, NonNegInt, NonEmptyString, IsoDate, IsoTime, NumericIdString, SafePathSegment, schemaOrigin, schemaConfirm, paginationSchema, pageSchema;
32386
32401
  var init_zod2 = __esm({
32387
32402
  "node_modules/@chrischall/mcp-utils/dist/zod/index.js"() {
32388
32403
  init_zod();
@@ -32391,6 +32406,8 @@ var init_zod2 = __esm({
32391
32406
  NonEmptyString = external_exports.string().min(1);
32392
32407
  IsoDate = external_exports.iso.date();
32393
32408
  IsoTime = external_exports.string().regex(/^([01]?\d|2[0-3]):[0-5]\d$/, "must be HH:MM (24h), e.g. 19:30");
32409
+ NumericIdString = external_exports.string().regex(/^\d+$/, "must be a numeric id (digits only)").describe("A numeric id string (digits only), safe to interpolate into a URL path.");
32410
+ SafePathSegment = external_exports.string().min(1).regex(/^[^/?#\s]+$/, 'must not contain "/", "?", "#", or whitespace').refine((v) => !v.includes(".."), 'must not contain ".."').describe('A single URL path segment, safe to interpolate: no "/", "..", "?", "#", or whitespace.');
32394
32411
  schemaOrigin = external_exports.string().optional().describe("Portal origin (e.g. https://<vendor>.example.co) selecting which active session to use. Optional when only one session is active.");
32395
32412
  schemaConfirm = external_exports.boolean().optional().describe("Must be true to proceed. Without this, the tool returns a preview.");
32396
32413
  paginationSchema = {
@@ -32422,6 +32439,7 @@ var init_dist = __esm({
32422
32439
  init_config();
32423
32440
  init_fs();
32424
32441
  init_http();
32442
+ init_concurrency();
32425
32443
  init_dates();
32426
32444
  init_zod2();
32427
32445
  init_auth();
@@ -32492,7 +32510,7 @@ var init_clipboard = __esm({
32492
32510
  init_dist();
32493
32511
 
32494
32512
  // src/version.ts
32495
- var VERSION = "0.6.0";
32513
+ var VERSION = "0.7.0";
32496
32514
 
32497
32515
  // src/tools/models.ts
32498
32516
  init_dist();
@@ -32503,7 +32521,7 @@ import { basename, dirname, join } from "node:path";
32503
32521
  import { fileURLToPath } from "node:url";
32504
32522
 
32505
32523
  // src/models.ts
32506
- var DEFAULT_IMAGE_MODEL = "gemini-3-pro-image";
32524
+ var DEFAULT_IMAGE_MODEL = "gemini-3.1-flash-image";
32507
32525
  function resolveModel(perCall, envOverride) {
32508
32526
  return perCall?.trim() || envOverride?.trim() || DEFAULT_IMAGE_MODEL;
32509
32527
  }
@@ -32521,7 +32539,6 @@ await loadDotenvSafely({ path: join(__dirname, "..", ".env"), override: false })
32521
32539
  var BASE_URL = "https://generativelanguage.googleapis.com/v1beta";
32522
32540
  var SERVICE = "Gemini";
32523
32541
  var INTERACTIONS_SERVICE = "Gemini Interactions";
32524
- var API_REVISION = "2026-05-20";
32525
32542
  var REQUEST_TIMEOUT_MS = 6e4;
32526
32543
  var UPLOAD_BASE_URL = "https://generativelanguage.googleapis.com/upload/v1beta";
32527
32544
  var FILE_MAX_BYTES = 2 * 1024 ** 3;
@@ -32557,7 +32574,6 @@ var GeminiClient = class {
32557
32574
  this.interactionsApi = createApiClient({
32558
32575
  baseUrl: BASE_URL,
32559
32576
  serviceName: INTERACTIONS_SERVICE,
32560
- baseHeaders: { "Api-Revision": API_REVISION },
32561
32577
  ...shared
32562
32578
  });
32563
32579
  }
@@ -32735,18 +32751,29 @@ var GeminiClient = class {
32735
32751
  const body = { model, input: inputParts, response_format: responseFormat };
32736
32752
  if (opts.thinkingLevel !== void 0) body.generation_config = { thinking_level: opts.thinkingLevel };
32737
32753
  if (opts.previousInteractionId !== void 0) body.previous_interaction_id = opts.previousInteractionId;
32738
- if (opts.googleSearch) body.tools = [{ type: "google_search" }];
32754
+ if (opts.googleSearch || opts.searchTypes?.length) {
32755
+ const searchTool = { type: "google_search" };
32756
+ if (opts.searchTypes?.length) searchTool.search_types = opts.searchTypes;
32757
+ body.tools = [searchTool];
32758
+ }
32739
32759
  const data = await this.interactionsApi.fetchJson("POST", "/interactions", {
32740
32760
  body
32741
32761
  });
32742
32762
  const images = [];
32743
32763
  const textParts = [];
32744
32764
  const searchQueries = [];
32765
+ const searchSuggestions = /* @__PURE__ */ new Set();
32745
32766
  for (const step of data.steps ?? []) {
32746
32767
  if (step.type === "google_search_call") {
32747
32768
  for (const q of step.arguments?.queries ?? []) if (q?.trim()) searchQueries.push(q);
32748
32769
  continue;
32749
32770
  }
32771
+ if (step.type === "google_search_result") {
32772
+ if (opts.searchTypes?.includes("image_search")) {
32773
+ for (const r of step.result ?? []) if (r.search_suggestions?.trim()) searchSuggestions.add(r.search_suggestions);
32774
+ }
32775
+ continue;
32776
+ }
32750
32777
  if (step.type !== "model_output") continue;
32751
32778
  for (const parts of [step.content ?? [], step.summary ?? []]) {
32752
32779
  for (const part of parts) {
@@ -32764,7 +32791,12 @@ var GeminiClient = class {
32764
32791
  });
32765
32792
  }
32766
32793
  const resultText = textParts.join("\n") || void 0;
32767
- const grounding = searchQueries.length > 0 ? { queries: searchQueries } : void 0;
32794
+ let grounding;
32795
+ if (searchQueries.length > 0 || searchSuggestions.size > 0) {
32796
+ grounding = {};
32797
+ if (searchQueries.length > 0) grounding.queries = searchQueries;
32798
+ if (searchSuggestions.size > 0) grounding.search_suggestions = [...searchSuggestions];
32799
+ }
32768
32800
  return { id: data.id, images, text: resultText, grounding };
32769
32801
  }
32770
32802
  };
@@ -32920,11 +32952,12 @@ init_zod();
32920
32952
  init_dist();
32921
32953
  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"];
32922
32954
  var IMAGE_SIZES = ["512", "1K", "2K", "4K"];
32955
+ 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).";
32923
32956
  var sharedImageSchema = {
32924
32957
  // Bare id only: the model is interpolated into the URL path
32925
32958
  // (`/models/${model}:generateContent`), so slashes/colons/queries must be
32926
32959
  // rejected to keep a crafted value from escaping the path segment.
32927
- 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)"),
32960
+ 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}`),
32928
32961
  aspect_ratio: external_exports.enum(ASPECT_RATIOS).optional().describe("Output aspect ratio"),
32929
32962
  image_size: external_exports.enum(IMAGE_SIZES).optional().describe("Output resolution (512 = 0.5K, Flash-only)"),
32930
32963
  output_dir: external_exports.string().optional().describe("Directory to write images to (default: $GEMINI_OUTPUT_DIR or cwd)"),
@@ -32974,11 +33007,12 @@ async function emit(named, opts, meta3) {
32974
33007
  }
32975
33008
 
32976
33009
  // src/tools/generate.ts
33010
+ 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).";
32977
33011
  function registerGenerateTools(server) {
32978
33012
  server.registerTool(
32979
33013
  "gemini_generate_image",
32980
33014
  {
32981
- description: "Generate image(s) from a text prompt with a Gemini image model (Nano Banana / Nano Banana Pro).",
33015
+ 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.",
32982
33016
  annotations: { readOnlyHint: false, openWorldHint: true },
32983
33017
  inputSchema: {
32984
33018
  prompt: external_exports.string().min(1).describe("Text prompt describing the image"),
@@ -33023,13 +33057,14 @@ function registerGenerateTools(server) {
33023
33057
  if (capturedText) meta3.text = capturedText;
33024
33058
  if (capturedGrounding) meta3.grounding = capturedGrounding;
33025
33059
  if (video.videoFileMeta) meta3.video_file = video.videoFileMeta;
33060
+ meta3.hint = REFINE_HINT;
33026
33061
  return emit(named, args, meta3);
33027
33062
  }
33028
33063
  );
33029
33064
  server.registerTool(
33030
33065
  "gemini_edit_image",
33031
33066
  {
33032
- 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.",
33067
+ 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.",
33033
33068
  annotations: { readOnlyHint: false, openWorldHint: true },
33034
33069
  inputSchema: {
33035
33070
  prompt: external_exports.string().min(1).describe("Instruction describing the edit or composition"),
@@ -33063,6 +33098,7 @@ function registerGenerateTools(server) {
33063
33098
  const meta3 = buildMeta(model, seed, args);
33064
33099
  if (text) meta3.text = text;
33065
33100
  if (result.grounding) meta3.grounding = result.grounding;
33101
+ meta3.hint = REFINE_HINT;
33066
33102
  return emit([{ image: img, base: slug }], args, meta3);
33067
33103
  }
33068
33104
  );
@@ -33131,18 +33167,20 @@ function registerSetTools(server) {
33131
33167
  // src/tools/interact.ts
33132
33168
  init_zod();
33133
33169
  init_dist();
33170
+ var lastInteractionId;
33134
33171
  function registerInteractTools(server) {
33135
33172
  server.registerTool(
33136
33173
  "gemini_interact",
33137
33174
  {
33138
- 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.",
33175
+ 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.",
33139
33176
  annotations: { readOnlyHint: false, openWorldHint: true },
33140
33177
  inputSchema: {
33141
33178
  input: external_exports.string().min(1).describe("Text prompt or editing instruction"),
33142
33179
  previous_interaction_id: external_exports.string().optional().describe("ID from a prior gemini_interact call \u2014 continues that multi-turn conversation"),
33180
+ 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)"),
33143
33181
  images: external_exports.array(external_exports.string().min(1)).optional().describe("Paths to reference input images"),
33144
33182
  images_base64: external_exports.array(external_exports.string().min(1)).optional().describe("Reference images as base64 strings or data URIs"),
33145
- model: external_exports.string().optional().describe("Model id override (default: server default; see gemini_list_models)"),
33183
+ model: external_exports.string().optional().describe(`Model id override (default: server default; see gemini_list_models). ${MODEL_CHOICE_GUIDE}`),
33146
33184
  aspect_ratio: external_exports.enum(ASPECT_RATIOS).optional().describe("Output aspect ratio"),
33147
33185
  image_size: external_exports.enum(IMAGE_SIZES).optional().describe("Output resolution (512 = 0.5K, Flash-only)"),
33148
33186
  thinking_level: external_exports.enum(["minimal", "high"]).optional().describe("Reasoning depth; higher can help complex/structural edits"),
@@ -33150,12 +33188,24 @@ function registerInteractTools(server) {
33150
33188
  output_dir: external_exports.string().optional().describe("Directory to write images to (default: $GEMINI_OUTPUT_DIR or cwd)"),
33151
33189
  inline: external_exports.boolean().optional().describe("Return base64 images inline instead of writing to disk"),
33152
33190
  google_search: external_exports.boolean().optional().describe("Ground the image in live Google Search results (current events, weather, data)"),
33191
+ search_types: external_exports.array(external_exports.enum(["web_search", "image_search"])).optional().describe(
33192
+ "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."
33193
+ ),
33153
33194
  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)"),
33154
33195
  video_path: videoPathSchema,
33155
33196
  from_clipboard: external_exports.boolean().optional().describe("Use the image currently on the macOS system clipboard as an input (downscaled to JPEG)")
33156
33197
  }
33157
33198
  },
33158
33199
  async (args) => {
33200
+ let previousInteractionId = args.previous_interaction_id;
33201
+ if (!previousInteractionId && args.continue_last) {
33202
+ if (!lastInteractionId) {
33203
+ throw new McpToolError("No previous interaction to continue in this session.", {
33204
+ hint: "Call gemini_interact once without continue_last first, or pass an explicit previous_interaction_id."
33205
+ });
33206
+ }
33207
+ previousInteractionId = lastInteractionId;
33208
+ }
33159
33209
  const inputs = await gatherImageInputs({ images: args.images, images_base64: args.images_base64, from_clipboard: args.from_clipboard });
33160
33210
  const video = await resolveVideoInput(args);
33161
33211
  const r = await client.interact({
@@ -33165,13 +33215,17 @@ function registerInteractTools(server) {
33165
33215
  aspectRatio: args.aspect_ratio,
33166
33216
  imageSize: args.image_size,
33167
33217
  thinkingLevel: args.thinking_level,
33168
- previousInteractionId: args.previous_interaction_id,
33218
+ previousInteractionId,
33169
33219
  googleSearch: args.google_search,
33220
+ searchTypes: args.search_types,
33170
33221
  videoUrl: video.videoUrl,
33171
33222
  videoMimeType: video.videoMimeType
33172
33223
  });
33224
+ lastInteractionId = r.id;
33173
33225
  const model = resolveModel(args.model, readEnvVar("GEMINI_IMAGE_MODEL"));
33174
33226
  const meta3 = { model, interaction_id: r.id };
33227
+ if (previousInteractionId) meta3.previous_interaction_id = previousInteractionId;
33228
+ meta3.hint = `To refine this image, call gemini_interact again with previous_interaction_id: "${r.id}" (or continue_last: true).`;
33175
33229
  if (r.text) meta3.text = r.text;
33176
33230
  if (r.grounding) meta3.grounding = r.grounding;
33177
33231
  if (video.videoFileMeta) meta3.video_file = video.videoFileMeta;
package/dist/client.js CHANGED
@@ -7,9 +7,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;
@@ -58,12 +55,13 @@ export class GeminiClient {
58
55
  fetchImpl: this.fetchImpl,
59
56
  };
60
57
  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).
58
+ // Separate client for the Interactions API so errors name the right API.
59
+ // The beta-era Api-Revision header is gone: the API went GA and requests
60
+ // succeed without it (verified live 2026-07-06; see docs/GEMINI-API.md).
61
+ // Don't re-add a pinned revision — it would freeze us on old semantics.
63
62
  this.interactionsApi = createApiClient({
64
63
  baseUrl: BASE_URL,
65
64
  serviceName: INTERACTIONS_SERVICE,
66
- baseHeaders: { 'Api-Revision': API_REVISION },
67
65
  ...shared,
68
66
  });
69
67
  }
@@ -252,8 +250,12 @@ export class GeminiClient {
252
250
  body.generation_config = { thinking_level: opts.thinkingLevel };
253
251
  if (opts.previousInteractionId !== undefined)
254
252
  body.previous_interaction_id = opts.previousInteractionId;
255
- if (opts.googleSearch)
256
- body.tools = [{ type: 'google_search' }];
253
+ if (opts.googleSearch || opts.searchTypes?.length) {
254
+ const searchTool = { type: 'google_search' };
255
+ if (opts.searchTypes?.length)
256
+ searchTool.search_types = opts.searchTypes;
257
+ body.tools = [searchTool];
258
+ }
257
259
  const data = await this.interactionsApi.fetchJson('POST', '/interactions', {
258
260
  body,
259
261
  });
@@ -265,17 +267,31 @@ export class GeminiClient {
265
267
  const images = [];
266
268
  const textParts = [];
267
269
  const searchQueries = [];
270
+ const searchSuggestions = new Set();
268
271
  for (const step of data.steps ?? []) {
269
272
  // Grounding queries live on the `google_search_call` step's arguments.
270
273
  // (The `google_search_result` step only carries HTML `search_suggestions`
271
274
  // chips — no clean source uri/title list like generateContent's
272
- // groundingChunks — so interact surfaces queries only, not sources.)
275
+ // groundingChunks — so interact surfaces queries, plus the suggestion
276
+ // chips when image_search was requested, ToS below.)
273
277
  if (step.type === 'google_search_call') {
274
278
  for (const q of step.arguments?.queries ?? [])
275
279
  if (q?.trim())
276
280
  searchQueries.push(q);
277
281
  continue;
278
282
  }
283
+ // ToS: image_search results' `search_suggestions` chips MUST be displayed
284
+ // by the caller, so surface them (deduped — the chips repeat) only when
285
+ // image_search grounding was requested; web-only grounding keeps the
286
+ // lean queries-only meta.
287
+ if (step.type === 'google_search_result') {
288
+ if (opts.searchTypes?.includes('image_search')) {
289
+ for (const r of step.result ?? [])
290
+ if (r.search_suggestions?.trim())
291
+ searchSuggestions.add(r.search_suggestions);
292
+ }
293
+ continue;
294
+ }
279
295
  if (step.type !== 'model_output')
280
296
  continue;
281
297
  for (const parts of [step.content ?? [], step.summary ?? []]) {
@@ -295,7 +311,14 @@ export class GeminiClient {
295
311
  });
296
312
  }
297
313
  const resultText = textParts.join('\n') || undefined;
298
- const grounding = searchQueries.length > 0 ? { queries: searchQueries } : undefined;
314
+ let grounding;
315
+ if (searchQueries.length > 0 || searchSuggestions.size > 0) {
316
+ grounding = {};
317
+ if (searchQueries.length > 0)
318
+ grounding.queries = searchQueries;
319
+ if (searchSuggestions.size > 0)
320
+ grounding.search_suggestions = [...searchSuggestions];
321
+ }
299
322
  return { id: data.id, images, text: resultText, grounding };
300
323
  }
301
324
  }
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.0'; // x-release-please-version
2
+ export const VERSION = '0.7.0'; // x-release-please-version
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chrischall/gemini-mcp",
3
- "version": "0.6.0",
3
+ "version": "0.7.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>",
@@ -44,13 +44,13 @@
44
44
  "test:coverage": "vitest run --coverage"
45
45
  },
46
46
  "dependencies": {
47
- "@chrischall/mcp-utils": "^0.7.0",
47
+ "@chrischall/mcp-utils": "^0.10.3",
48
48
  "@modelcontextprotocol/sdk": "^1.29.0",
49
49
  "dotenv": "^17.4.0",
50
50
  "zod": "^4.4.2"
51
51
  },
52
52
  "devDependencies": {
53
- "@types/node": "^25.5.2",
53
+ "@types/node": "^26.0.0",
54
54
  "@vitest/coverage-v8": "^4.1.2",
55
55
  "esbuild": "^0.28.0",
56
56
  "typescript": "^6.0.2",
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.0",
9
+ "version": "0.7.0",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "@chrischall/gemini-mcp",
14
- "version": "0.6.0",
14
+ "version": "0.7.0",
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