@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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +3 -3
- package/SKILL.md +49 -6
- package/dist/bundle.js +70 -16
- package/dist/client.js +33 -10
- package/dist/models.js +7 -2
- package/dist/tools/generate.js +10 -1
- package/dist/tools/interact.js +36 -7
- package/dist/tools/shared.js +10 -1
- package/dist/version.js +1 -1
- package/package.json +3 -3
- package/server.json +3 -3
|
@@ -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.
|
|
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.
|
|
18
|
+
"version": "0.7.0",
|
|
19
19
|
"author": {
|
|
20
20
|
"name": "Chris Hall"
|
|
21
21
|
},
|
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-
|
|
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` |
|
|
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` |
|
|
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-
|
|
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
|
|
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 —
|
|
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`
|
|
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**.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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-
|
|
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
|
|
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
|
-
|
|
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(
|
|
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: "
|
|
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(
|
|
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
|
|
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
|
|
62
|
-
//
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
/**
|
|
2
|
-
|
|
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;
|
package/dist/tools/generate.js
CHANGED
|
@@ -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
|
}
|
package/dist/tools/interact.js
CHANGED
|
@@ -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:
|
|
10
|
-
|
|
11
|
-
'refine the
|
|
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(
|
|
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
|
|
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)
|
package/dist/tools/shared.js
CHANGED
|
@@ -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(
|
|
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.
|
|
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.
|
|
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.
|
|
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": "^
|
|
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.
|
|
9
|
+
"version": "0.7.0",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "@chrischall/gemini-mcp",
|
|
14
|
-
"version": "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-
|
|
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
|