@chrischall/gemini-mcp 0.6.1 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +72 -15
- package/dist/client.js +68 -14
- 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 +1 -1
- 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.1"
|
|
11
11
|
},
|
|
12
12
|
"plugins": [
|
|
13
13
|
{
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"displayName": "Gemini Images",
|
|
16
16
|
"source": "./",
|
|
17
17
|
"description": "Generate and edit images with Google Gemini image models (Nano Banana / Nano Banana Pro) via natural language prompts",
|
|
18
|
-
"version": "0.
|
|
18
|
+
"version": "0.7.1",
|
|
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
|
@@ -32510,7 +32510,7 @@ var init_clipboard = __esm({
|
|
|
32510
32510
|
init_dist();
|
|
32511
32511
|
|
|
32512
32512
|
// src/version.ts
|
|
32513
|
-
var VERSION = "0.
|
|
32513
|
+
var VERSION = "0.7.1";
|
|
32514
32514
|
|
|
32515
32515
|
// src/tools/models.ts
|
|
32516
32516
|
init_dist();
|
|
@@ -32521,7 +32521,7 @@ import { basename, dirname, join } from "node:path";
|
|
|
32521
32521
|
import { fileURLToPath } from "node:url";
|
|
32522
32522
|
|
|
32523
32523
|
// src/models.ts
|
|
32524
|
-
var DEFAULT_IMAGE_MODEL = "gemini-3-
|
|
32524
|
+
var DEFAULT_IMAGE_MODEL = "gemini-3.1-flash-image";
|
|
32525
32525
|
function resolveModel(perCall, envOverride) {
|
|
32526
32526
|
return perCall?.trim() || envOverride?.trim() || DEFAULT_IMAGE_MODEL;
|
|
32527
32527
|
}
|
|
@@ -32539,12 +32539,13 @@ await loadDotenvSafely({ path: join(__dirname, "..", ".env"), override: false })
|
|
|
32539
32539
|
var BASE_URL = "https://generativelanguage.googleapis.com/v1beta";
|
|
32540
32540
|
var SERVICE = "Gemini";
|
|
32541
32541
|
var INTERACTIONS_SERVICE = "Gemini Interactions";
|
|
32542
|
-
var API_REVISION = "2026-05-20";
|
|
32543
32542
|
var REQUEST_TIMEOUT_MS = 6e4;
|
|
32544
32543
|
var UPLOAD_BASE_URL = "https://generativelanguage.googleapis.com/upload/v1beta";
|
|
32545
32544
|
var FILE_MAX_BYTES = 2 * 1024 ** 3;
|
|
32546
32545
|
var FILE_POLL_INTERVAL_MS = 2e3;
|
|
32547
32546
|
var FILE_POLL_MAX_ATTEMPTS = 150;
|
|
32547
|
+
var CHAIN_404_RETRIES = 2;
|
|
32548
|
+
var CHAIN_404_RETRY_MS = 2e3;
|
|
32548
32549
|
var GeminiClient = class {
|
|
32549
32550
|
apiKey;
|
|
32550
32551
|
configError;
|
|
@@ -32575,7 +32576,6 @@ var GeminiClient = class {
|
|
|
32575
32576
|
this.interactionsApi = createApiClient({
|
|
32576
32577
|
baseUrl: BASE_URL,
|
|
32577
32578
|
serviceName: INTERACTIONS_SERVICE,
|
|
32578
|
-
baseHeaders: { "Api-Revision": API_REVISION },
|
|
32579
32579
|
...shared
|
|
32580
32580
|
});
|
|
32581
32581
|
}
|
|
@@ -32753,18 +32753,48 @@ var GeminiClient = class {
|
|
|
32753
32753
|
const body = { model, input: inputParts, response_format: responseFormat };
|
|
32754
32754
|
if (opts.thinkingLevel !== void 0) body.generation_config = { thinking_level: opts.thinkingLevel };
|
|
32755
32755
|
if (opts.previousInteractionId !== void 0) body.previous_interaction_id = opts.previousInteractionId;
|
|
32756
|
-
if (opts.googleSearch
|
|
32757
|
-
|
|
32758
|
-
|
|
32759
|
-
|
|
32756
|
+
if (opts.googleSearch || opts.searchTypes?.length) {
|
|
32757
|
+
const searchTool = { type: "google_search" };
|
|
32758
|
+
if (opts.searchTypes?.length) searchTool.search_types = opts.searchTypes;
|
|
32759
|
+
body.tools = [searchTool];
|
|
32760
|
+
}
|
|
32761
|
+
let data;
|
|
32762
|
+
for (let attempt = 0; ; attempt++) {
|
|
32763
|
+
try {
|
|
32764
|
+
data = await this.interactionsApi.fetchJson("POST", "/interactions", {
|
|
32765
|
+
body
|
|
32766
|
+
});
|
|
32767
|
+
break;
|
|
32768
|
+
} catch (err) {
|
|
32769
|
+
if (!(opts.previousInteractionId && err instanceof ApiError && err.status === 404)) throw err;
|
|
32770
|
+
if (attempt < CHAIN_404_RETRIES) {
|
|
32771
|
+
await this.sleep(CHAIN_404_RETRY_MS * (attempt + 1));
|
|
32772
|
+
continue;
|
|
32773
|
+
}
|
|
32774
|
+
throw new McpToolError(
|
|
32775
|
+
`Previous interaction "${opts.previousInteractionId}" was not found (retried ${CHAIN_404_RETRIES}\xD7 \u2014 the interactions store can lag briefly after a turn completes). If the id is old: interactions are retained 55 days on the paid tier (1 day on the free tier) and are scoped to the API key that created them.`,
|
|
32776
|
+
{
|
|
32777
|
+
hint: "Retry once more in a few seconds; if it keeps failing, start a new chain: call gemini_interact again with the last output image passed via `images` (or `images_base64`) plus the same instruction, then chain the NEW interaction id.",
|
|
32778
|
+
cause: err
|
|
32779
|
+
}
|
|
32780
|
+
);
|
|
32781
|
+
}
|
|
32782
|
+
}
|
|
32760
32783
|
const images = [];
|
|
32761
32784
|
const textParts = [];
|
|
32762
32785
|
const searchQueries = [];
|
|
32786
|
+
const searchSuggestions = /* @__PURE__ */ new Set();
|
|
32763
32787
|
for (const step of data.steps ?? []) {
|
|
32764
32788
|
if (step.type === "google_search_call") {
|
|
32765
32789
|
for (const q of step.arguments?.queries ?? []) if (q?.trim()) searchQueries.push(q);
|
|
32766
32790
|
continue;
|
|
32767
32791
|
}
|
|
32792
|
+
if (step.type === "google_search_result") {
|
|
32793
|
+
if (opts.searchTypes?.includes("image_search")) {
|
|
32794
|
+
for (const r of step.result ?? []) if (r.search_suggestions?.trim()) searchSuggestions.add(r.search_suggestions);
|
|
32795
|
+
}
|
|
32796
|
+
continue;
|
|
32797
|
+
}
|
|
32768
32798
|
if (step.type !== "model_output") continue;
|
|
32769
32799
|
for (const parts of [step.content ?? [], step.summary ?? []]) {
|
|
32770
32800
|
for (const part of parts) {
|
|
@@ -32782,7 +32812,12 @@ var GeminiClient = class {
|
|
|
32782
32812
|
});
|
|
32783
32813
|
}
|
|
32784
32814
|
const resultText = textParts.join("\n") || void 0;
|
|
32785
|
-
|
|
32815
|
+
let grounding;
|
|
32816
|
+
if (searchQueries.length > 0 || searchSuggestions.size > 0) {
|
|
32817
|
+
grounding = {};
|
|
32818
|
+
if (searchQueries.length > 0) grounding.queries = searchQueries;
|
|
32819
|
+
if (searchSuggestions.size > 0) grounding.search_suggestions = [...searchSuggestions];
|
|
32820
|
+
}
|
|
32786
32821
|
return { id: data.id, images, text: resultText, grounding };
|
|
32787
32822
|
}
|
|
32788
32823
|
};
|
|
@@ -32938,11 +32973,12 @@ init_zod();
|
|
|
32938
32973
|
init_dist();
|
|
32939
32974
|
var ASPECT_RATIOS = ["1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "21:9", "1:4", "4:1", "1:8", "8:1"];
|
|
32940
32975
|
var IMAGE_SIZES = ["512", "1K", "2K", "4K"];
|
|
32976
|
+
var MODEL_CHOICE_GUIDE = "gemini-3.1-flash-image (Nano Banana 2) is the versatile generalist workhorse \u2014 balances speed with state-of-the-art 4K generation, world knowledge, and reliable text rendering; excels at multi-reference-image processing and consistency. gemini-3-pro-image (Nano Banana Pro) is the premium choice for the most complex visual tasks \u2014 highest world knowledge, advanced localization, accurate brand consistency, precision creative control. gemini-3.1-flash-lite-image (Nano Banana 2 Lite) is the fastest/cheapest for simple tasks (1K only, no search grounding).";
|
|
32941
32977
|
var sharedImageSchema = {
|
|
32942
32978
|
// Bare id only: the model is interpolated into the URL path
|
|
32943
32979
|
// (`/models/${model}:generateContent`), so slashes/colons/queries must be
|
|
32944
32980
|
// rejected to keep a crafted value from escaping the path segment.
|
|
32945
|
-
model: external_exports.string().regex(/^[\w.-]+$/, 'must be a bare model id (letters, digits, ".", "_", "-")').optional().describe(
|
|
32981
|
+
model: external_exports.string().regex(/^[\w.-]+$/, 'must be a bare model id (letters, digits, ".", "_", "-")').optional().describe(`Model id override (default: server default; see gemini_list_models). ${MODEL_CHOICE_GUIDE}`),
|
|
32946
32982
|
aspect_ratio: external_exports.enum(ASPECT_RATIOS).optional().describe("Output aspect ratio"),
|
|
32947
32983
|
image_size: external_exports.enum(IMAGE_SIZES).optional().describe("Output resolution (512 = 0.5K, Flash-only)"),
|
|
32948
32984
|
output_dir: external_exports.string().optional().describe("Directory to write images to (default: $GEMINI_OUTPUT_DIR or cwd)"),
|
|
@@ -32992,11 +33028,12 @@ async function emit(named, opts, meta3) {
|
|
|
32992
33028
|
}
|
|
32993
33029
|
|
|
32994
33030
|
// src/tools/generate.ts
|
|
33031
|
+
var REFINE_HINT = "To iteratively refine this image, use gemini_interact (pass the output file via `images` on the first call, then chain `previous_interaction_id` on each subsequent call).";
|
|
32995
33032
|
function registerGenerateTools(server) {
|
|
32996
33033
|
server.registerTool(
|
|
32997
33034
|
"gemini_generate_image",
|
|
32998
33035
|
{
|
|
32999
|
-
description: "Generate image(s) from a text prompt with a Gemini image model (Nano Banana / Nano Banana Pro).",
|
|
33036
|
+
description: "Generate image(s) from a text prompt with a Gemini image model (Nano Banana / Nano Banana Pro). If the result will likely be refined iteratively, prefer gemini_interact (multi-turn) as the entry point.",
|
|
33000
33037
|
annotations: { readOnlyHint: false, openWorldHint: true },
|
|
33001
33038
|
inputSchema: {
|
|
33002
33039
|
prompt: external_exports.string().min(1).describe("Text prompt describing the image"),
|
|
@@ -33041,13 +33078,14 @@ function registerGenerateTools(server) {
|
|
|
33041
33078
|
if (capturedText) meta3.text = capturedText;
|
|
33042
33079
|
if (capturedGrounding) meta3.grounding = capturedGrounding;
|
|
33043
33080
|
if (video.videoFileMeta) meta3.video_file = video.videoFileMeta;
|
|
33081
|
+
meta3.hint = REFINE_HINT;
|
|
33044
33082
|
return emit(named, args, meta3);
|
|
33045
33083
|
}
|
|
33046
33084
|
);
|
|
33047
33085
|
server.registerTool(
|
|
33048
33086
|
"gemini_edit_image",
|
|
33049
33087
|
{
|
|
33050
|
-
description: "Edit or compose images: provide one or more input images (paths or base64), plus a text instruction. Gemini over-preserves the input; there is no edit-strength control \u2014 for large structural changes, reroll with a different `seed` or more forceful wording.",
|
|
33088
|
+
description: "Edit or compose images: provide one or more input images (paths or base64), plus a text instruction. For a SERIES of successive edits to the same image, prefer gemini_interact (multi-turn) \u2014 it keeps edit context and avoids re-processing the full image each round; use gemini_edit_image for one-off edits or composing multiple distinct inputs. Gemini over-preserves the input; there is no edit-strength control \u2014 for large structural changes, reroll with a different `seed` or more forceful wording.",
|
|
33051
33089
|
annotations: { readOnlyHint: false, openWorldHint: true },
|
|
33052
33090
|
inputSchema: {
|
|
33053
33091
|
prompt: external_exports.string().min(1).describe("Instruction describing the edit or composition"),
|
|
@@ -33081,6 +33119,7 @@ function registerGenerateTools(server) {
|
|
|
33081
33119
|
const meta3 = buildMeta(model, seed, args);
|
|
33082
33120
|
if (text) meta3.text = text;
|
|
33083
33121
|
if (result.grounding) meta3.grounding = result.grounding;
|
|
33122
|
+
meta3.hint = REFINE_HINT;
|
|
33084
33123
|
return emit([{ image: img, base: slug }], args, meta3);
|
|
33085
33124
|
}
|
|
33086
33125
|
);
|
|
@@ -33149,18 +33188,20 @@ function registerSetTools(server) {
|
|
|
33149
33188
|
// src/tools/interact.ts
|
|
33150
33189
|
init_zod();
|
|
33151
33190
|
init_dist();
|
|
33191
|
+
var lastInteractionId;
|
|
33152
33192
|
function registerInteractTools(server) {
|
|
33153
33193
|
server.registerTool(
|
|
33154
33194
|
"gemini_interact",
|
|
33155
33195
|
{
|
|
33156
|
-
description: "
|
|
33196
|
+
description: "Preferred tool for iterative or multi-step refinement of a single image \u2014 multi-turn generation/editing via Gemini's Interactions API. To refine, capture the returned interaction `id` and pass it as `previous_interaction_id` on the next call \u2014 do NOT start a new interaction or re-upload the image for each tweak. `continue_last: true` chains from this session's most recent interaction without threading the id. Output is JPEG.",
|
|
33157
33197
|
annotations: { readOnlyHint: false, openWorldHint: true },
|
|
33158
33198
|
inputSchema: {
|
|
33159
33199
|
input: external_exports.string().min(1).describe("Text prompt or editing instruction"),
|
|
33160
33200
|
previous_interaction_id: external_exports.string().optional().describe("ID from a prior gemini_interact call \u2014 continues that multi-turn conversation"),
|
|
33201
|
+
continue_last: external_exports.boolean().optional().describe("Continue from the most recent interaction this server created (convenience for previous_interaction_id; an explicit id wins)"),
|
|
33161
33202
|
images: external_exports.array(external_exports.string().min(1)).optional().describe("Paths to reference input images"),
|
|
33162
33203
|
images_base64: external_exports.array(external_exports.string().min(1)).optional().describe("Reference images as base64 strings or data URIs"),
|
|
33163
|
-
model: external_exports.string().optional().describe(
|
|
33204
|
+
model: external_exports.string().optional().describe(`Model id override (default: server default; see gemini_list_models). ${MODEL_CHOICE_GUIDE}`),
|
|
33164
33205
|
aspect_ratio: external_exports.enum(ASPECT_RATIOS).optional().describe("Output aspect ratio"),
|
|
33165
33206
|
image_size: external_exports.enum(IMAGE_SIZES).optional().describe("Output resolution (512 = 0.5K, Flash-only)"),
|
|
33166
33207
|
thinking_level: external_exports.enum(["minimal", "high"]).optional().describe("Reasoning depth; higher can help complex/structural edits"),
|
|
@@ -33168,12 +33209,24 @@ function registerInteractTools(server) {
|
|
|
33168
33209
|
output_dir: external_exports.string().optional().describe("Directory to write images to (default: $GEMINI_OUTPUT_DIR or cwd)"),
|
|
33169
33210
|
inline: external_exports.boolean().optional().describe("Return base64 images inline instead of writing to disk"),
|
|
33170
33211
|
google_search: external_exports.boolean().optional().describe("Ground the image in live Google Search results (current events, weather, data)"),
|
|
33212
|
+
search_types: external_exports.array(external_exports.enum(["web_search", "image_search"])).optional().describe(
|
|
33213
|
+
"Grounding search types (implies google_search). image_search (gemini-3.1-flash-image only) uses Google Image Search results as visual references; per Google ToS the returned grounding.search_suggestions HTML must then be displayed to the user. Cannot depict real people from web images."
|
|
33214
|
+
),
|
|
33171
33215
|
video_url: external_exports.string().url().optional().describe("Public YouTube URL (or a previously uploaded Files API uri) as a video reference (video\u2192image; use a Flash model e.g. gemini-3.1-flash-image)"),
|
|
33172
33216
|
video_path: videoPathSchema,
|
|
33173
33217
|
from_clipboard: external_exports.boolean().optional().describe("Use the image currently on the macOS system clipboard as an input (downscaled to JPEG)")
|
|
33174
33218
|
}
|
|
33175
33219
|
},
|
|
33176
33220
|
async (args) => {
|
|
33221
|
+
let previousInteractionId = args.previous_interaction_id;
|
|
33222
|
+
if (!previousInteractionId && args.continue_last) {
|
|
33223
|
+
if (!lastInteractionId) {
|
|
33224
|
+
throw new McpToolError("No previous interaction to continue in this session.", {
|
|
33225
|
+
hint: "Call gemini_interact once without continue_last first, or pass an explicit previous_interaction_id."
|
|
33226
|
+
});
|
|
33227
|
+
}
|
|
33228
|
+
previousInteractionId = lastInteractionId;
|
|
33229
|
+
}
|
|
33177
33230
|
const inputs = await gatherImageInputs({ images: args.images, images_base64: args.images_base64, from_clipboard: args.from_clipboard });
|
|
33178
33231
|
const video = await resolveVideoInput(args);
|
|
33179
33232
|
const r = await client.interact({
|
|
@@ -33183,13 +33236,17 @@ function registerInteractTools(server) {
|
|
|
33183
33236
|
aspectRatio: args.aspect_ratio,
|
|
33184
33237
|
imageSize: args.image_size,
|
|
33185
33238
|
thinkingLevel: args.thinking_level,
|
|
33186
|
-
previousInteractionId
|
|
33239
|
+
previousInteractionId,
|
|
33187
33240
|
googleSearch: args.google_search,
|
|
33241
|
+
searchTypes: args.search_types,
|
|
33188
33242
|
videoUrl: video.videoUrl,
|
|
33189
33243
|
videoMimeType: video.videoMimeType
|
|
33190
33244
|
});
|
|
33245
|
+
lastInteractionId = r.id;
|
|
33191
33246
|
const model = resolveModel(args.model, readEnvVar("GEMINI_IMAGE_MODEL"));
|
|
33192
33247
|
const meta3 = { model, interaction_id: r.id };
|
|
33248
|
+
if (previousInteractionId) meta3.previous_interaction_id = previousInteractionId;
|
|
33249
|
+
meta3.hint = `To refine this image, call gemini_interact again with previous_interaction_id: "${r.id}" (or continue_last: true).`;
|
|
33193
33250
|
if (r.text) meta3.text = r.text;
|
|
33194
33251
|
if (r.grounding) meta3.grounding = r.grounding;
|
|
33195
33252
|
if (video.videoFileMeta) meta3.video_file = video.videoFileMeta;
|
package/dist/client.js
CHANGED
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
import { basename, dirname, join } from 'node:path';
|
|
2
2
|
import { fileURLToPath } from 'node:url';
|
|
3
|
-
import { loadDotenvSafely, readEnvVar, McpToolError, createApiClient, formatApiError, fileBlob } from '@chrischall/mcp-utils';
|
|
3
|
+
import { loadDotenvSafely, readEnvVar, McpToolError, ApiError, createApiClient, formatApiError, fileBlob } from '@chrischall/mcp-utils';
|
|
4
4
|
import { resolveModel, filterImageModels } from './models.js';
|
|
5
5
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
6
|
await loadDotenvSafely({ path: join(__dirname, '..', '.env'), override: false });
|
|
7
7
|
const BASE_URL = 'https://generativelanguage.googleapis.com/v1beta'; // v1 lacks gemini-3-pro-image; confirmed via Task 5
|
|
8
8
|
const SERVICE = 'Gemini';
|
|
9
9
|
const INTERACTIONS_SERVICE = 'Gemini Interactions';
|
|
10
|
-
// Required revision header for the beta Interactions API (verified 2026-06-08;
|
|
11
|
-
// see docs/GEMINI-API.md) — load-bearing, requests 400 without it.
|
|
12
|
-
const API_REVISION = '2026-05-20';
|
|
13
10
|
// Per-request abort budget. Image generation routinely runs 30s+ on the Pro
|
|
14
11
|
// model, so this is deliberately 60s (NOT the fleet's usual 15–30s).
|
|
15
12
|
const REQUEST_TIMEOUT_MS = 60_000;
|
|
@@ -24,6 +21,13 @@ const FILE_MAX_BYTES = 2 * 1024 ** 3;
|
|
|
24
21
|
// in ~1s (verified); longer videos take proportionally longer to process.
|
|
25
22
|
const FILE_POLL_INTERVAL_MS = 2_000;
|
|
26
23
|
const FILE_POLL_MAX_ATTEMPTS = 150;
|
|
24
|
+
// The interactions store is eventually consistent: a freshly returned id can
|
|
25
|
+
// 404 for a short window right after its turn completes (observed live
|
|
26
|
+
// 2026-07-06 — an id that 404'd on an immediate chain resolved fine minutes
|
|
27
|
+
// later). A chained 404 generated nothing (no cost), so retry before giving up:
|
|
28
|
+
// attempts wait 2s then 4s (~6s total).
|
|
29
|
+
const CHAIN_404_RETRIES = 2;
|
|
30
|
+
const CHAIN_404_RETRY_MS = 2_000;
|
|
27
31
|
export class GeminiClient {
|
|
28
32
|
apiKey;
|
|
29
33
|
configError;
|
|
@@ -58,12 +62,13 @@ export class GeminiClient {
|
|
|
58
62
|
fetchImpl: this.fetchImpl,
|
|
59
63
|
};
|
|
60
64
|
this.api = createApiClient({ baseUrl: BASE_URL, serviceName: SERVICE, ...shared });
|
|
61
|
-
// Separate client for the
|
|
62
|
-
//
|
|
65
|
+
// Separate client for the Interactions API so errors name the right API.
|
|
66
|
+
// The beta-era Api-Revision header is gone: the API went GA and requests
|
|
67
|
+
// succeed without it (verified live 2026-07-06; see docs/GEMINI-API.md).
|
|
68
|
+
// Don't re-add a pinned revision — it would freeze us on old semantics.
|
|
63
69
|
this.interactionsApi = createApiClient({
|
|
64
70
|
baseUrl: BASE_URL,
|
|
65
71
|
serviceName: INTERACTIONS_SERVICE,
|
|
66
|
-
baseHeaders: { 'Api-Revision': API_REVISION },
|
|
67
72
|
...shared,
|
|
68
73
|
});
|
|
69
74
|
}
|
|
@@ -252,11 +257,39 @@ export class GeminiClient {
|
|
|
252
257
|
body.generation_config = { thinking_level: opts.thinkingLevel };
|
|
253
258
|
if (opts.previousInteractionId !== undefined)
|
|
254
259
|
body.previous_interaction_id = opts.previousInteractionId;
|
|
255
|
-
if (opts.googleSearch)
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
+
if (opts.googleSearch || opts.searchTypes?.length) {
|
|
261
|
+
const searchTool = { type: 'google_search' };
|
|
262
|
+
if (opts.searchTypes?.length)
|
|
263
|
+
searchTool.search_types = opts.searchTypes;
|
|
264
|
+
body.tools = [searchTool];
|
|
265
|
+
}
|
|
266
|
+
let data;
|
|
267
|
+
for (let attempt = 0;; attempt++) {
|
|
268
|
+
try {
|
|
269
|
+
data = await this.interactionsApi.fetchJson('POST', '/interactions', {
|
|
270
|
+
body,
|
|
271
|
+
});
|
|
272
|
+
break;
|
|
273
|
+
}
|
|
274
|
+
catch (err) {
|
|
275
|
+
// A 404 on a chained call means the referenced interaction wasn't
|
|
276
|
+
// found. Two causes: (a) store lag right after the id was created —
|
|
277
|
+
// retry with backoff; (b) genuinely gone — interactions are retained
|
|
278
|
+
// 55 days (paid tier) / 1 day (free tier) and are scoped to the API
|
|
279
|
+
// key's project. The chain is recoverable from the last output file,
|
|
280
|
+
// so the exhausted-retries error says how.
|
|
281
|
+
if (!(opts.previousInteractionId && err instanceof ApiError && err.status === 404))
|
|
282
|
+
throw err;
|
|
283
|
+
if (attempt < CHAIN_404_RETRIES) {
|
|
284
|
+
await this.sleep(CHAIN_404_RETRY_MS * (attempt + 1));
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
throw new McpToolError(`Previous interaction "${opts.previousInteractionId}" was not found (retried ${CHAIN_404_RETRIES}× — the interactions store can lag briefly after a turn completes). If the id is old: interactions are retained 55 days on the paid tier (1 day on the free tier) and are scoped to the API key that created them.`, {
|
|
288
|
+
hint: 'Retry once more in a few seconds; if it keeps failing, start a new chain: call gemini_interact again with the last output image passed via `images` (or `images_base64`) plus the same instruction, then chain the NEW interaction id.',
|
|
289
|
+
cause: err,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
}
|
|
260
293
|
// Only surface the `model_output` step — that's the caller-facing result.
|
|
261
294
|
// `thought` steps hold internal reasoning (and, with includeThoughts, draft
|
|
262
295
|
// "thinking" images); collecting those would leak reasoning into `text` and
|
|
@@ -265,17 +298,31 @@ export class GeminiClient {
|
|
|
265
298
|
const images = [];
|
|
266
299
|
const textParts = [];
|
|
267
300
|
const searchQueries = [];
|
|
301
|
+
const searchSuggestions = new Set();
|
|
268
302
|
for (const step of data.steps ?? []) {
|
|
269
303
|
// Grounding queries live on the `google_search_call` step's arguments.
|
|
270
304
|
// (The `google_search_result` step only carries HTML `search_suggestions`
|
|
271
305
|
// chips — no clean source uri/title list like generateContent's
|
|
272
|
-
// groundingChunks — so interact surfaces queries
|
|
306
|
+
// groundingChunks — so interact surfaces queries, plus the suggestion
|
|
307
|
+
// chips when image_search was requested, ToS below.)
|
|
273
308
|
if (step.type === 'google_search_call') {
|
|
274
309
|
for (const q of step.arguments?.queries ?? [])
|
|
275
310
|
if (q?.trim())
|
|
276
311
|
searchQueries.push(q);
|
|
277
312
|
continue;
|
|
278
313
|
}
|
|
314
|
+
// ToS: image_search results' `search_suggestions` chips MUST be displayed
|
|
315
|
+
// by the caller, so surface them (deduped — the chips repeat) only when
|
|
316
|
+
// image_search grounding was requested; web-only grounding keeps the
|
|
317
|
+
// lean queries-only meta.
|
|
318
|
+
if (step.type === 'google_search_result') {
|
|
319
|
+
if (opts.searchTypes?.includes('image_search')) {
|
|
320
|
+
for (const r of step.result ?? [])
|
|
321
|
+
if (r.search_suggestions?.trim())
|
|
322
|
+
searchSuggestions.add(r.search_suggestions);
|
|
323
|
+
}
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
279
326
|
if (step.type !== 'model_output')
|
|
280
327
|
continue;
|
|
281
328
|
for (const parts of [step.content ?? [], step.summary ?? []]) {
|
|
@@ -295,7 +342,14 @@ export class GeminiClient {
|
|
|
295
342
|
});
|
|
296
343
|
}
|
|
297
344
|
const resultText = textParts.join('\n') || undefined;
|
|
298
|
-
|
|
345
|
+
let grounding;
|
|
346
|
+
if (searchQueries.length > 0 || searchSuggestions.size > 0) {
|
|
347
|
+
grounding = {};
|
|
348
|
+
if (searchQueries.length > 0)
|
|
349
|
+
grounding.queries = searchQueries;
|
|
350
|
+
if (searchSuggestions.size > 0)
|
|
351
|
+
grounding.search_suggestions = [...searchSuggestions];
|
|
352
|
+
}
|
|
299
353
|
return { id: data.id, images, text: resultText, grounding };
|
|
300
354
|
}
|
|
301
355
|
}
|
package/dist/models.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
/**
|
|
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.1'; // 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.1",
|
|
4
4
|
"mcpName": "io.github.chrischall/gemini-mcp",
|
|
5
5
|
"description": "Gemini image-generation MCP server for Claude — developed and maintained by AI (Claude Code)",
|
|
6
6
|
"author": "Claude Code (AI) <https://www.anthropic.com/claude>",
|
package/server.json
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
"url": "https://github.com/chrischall/gemini-mcp",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.7.1",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "@chrischall/gemini-mcp",
|
|
14
|
-
"version": "0.
|
|
14
|
+
"version": "0.7.1",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
},
|
|
26
26
|
{
|
|
27
27
|
"name": "GEMINI_IMAGE_MODEL",
|
|
28
|
-
"description": "Override the default image model (default: gemini-3-
|
|
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
|