@chrischall/gemini-mcp 2.4.0 → 2.4.2

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.
@@ -86,7 +86,7 @@ export function registerUploadUrlTools(server, client) {
86
86
  '(the signature in the URL is the auth, and it covers the content type). On a 2xx, reference the `r2_key` from ' +
87
87
  'THIS result — not the PUT response: images_r2_keys on generation tools (master_images_r2_keys on ' +
88
88
  'gemini_image_set), gemini_save_character to keep it with no expiry, or gemini_upload_file({ r2_key }) for a ' +
89
- '~48h files/… reference. Uploads themselves follow the media retention schedule (default 7 days).',
89
+ '~48h files/… reference. Uploads themselves follow the host\'s blob-store retention schedule.',
90
90
  });
91
91
  });
92
92
  }
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  /** Single source of the server version. release-please bumps the literal below. */
2
- export const VERSION = '2.4.0'; // x-release-please-version
2
+ export const VERSION = '2.4.2'; // x-release-please-version
package/mint.yaml CHANGED
@@ -4,6 +4,19 @@ slug: gemini
4
4
  summary: >-
5
5
  Generate and edit images with Google Gemini image models (Nano Banana /
6
6
  Nano Banana Pro) via natural language prompts
7
+ examples:
8
+ - prompt: >-
9
+ Generate a watercolor illustration of a lighthouse on a rocky coast at
10
+ sunset, in a 16:9 aspect ratio.
11
+ note: Text-to-image with gemini_image_generate.
12
+ - prompt: >-
13
+ Make a set of four images of the same cartoon fox mascot: waving,
14
+ reading a book, riding a bike, and sleeping under a tree.
15
+ note: A consistent character across scenes with gemini_image_set.
16
+ - prompt: >-
17
+ Compose a 30-second upbeat acoustic guitar instrumental for a cooking
18
+ video intro.
19
+ note: A short Lyria clip with gemini_music_generate.
7
20
  env:
8
21
  - name: GEMINI_API_KEY
9
22
  secret: true
@@ -17,12 +30,24 @@ env:
17
30
  - name: GEMINI_OUTPUT_DIR
18
31
  required: false
19
32
  help: >-
20
- Default directory for generated images (default: current working
21
- directory)
33
+ Default directory for generated images (default: ~/Downloads/gemini-mcp,
34
+ not the cwd). When set, a per-call output_dir must be inside it.
22
35
  - name: GEMINI_INPUT_DIR
23
36
  required: false
24
37
  help: >-
25
38
  Directory to resolve bare input-image filenames against
39
+ - name: GEMINI_UPLOAD_DIR
40
+ required: false
41
+ help: >-
42
+ Directories (path.delimiter-separated) that local files uploaded to the
43
+ Gemini Files API must come from. Mostly a local-install knob: on a hosted
44
+ server, uploads come from gemini_get_upload_url instead.
45
+ - name: GEMINI_REFERENCE_DIR
46
+ required: false
47
+ help: >-
48
+ Directories (path.delimiter-separated) that local reference images must
49
+ come from (else GEMINI_UPLOAD_DIR). Unset on a hosted server, only
50
+ $MCP_DATA_DIR/uploads is allowed, so leave it unset there.
26
51
  - name: GEMINI_TIMEOUT_MS
27
52
  required: false
28
53
  help: >-
@@ -38,6 +63,10 @@ env:
38
63
  How often, in milliseconds, to emit a progress notification while a
39
64
  generation is in flight. Hosts reset their tools/call timeout on progress,
40
65
  so this lets a long 4K/Pro generation outlive the default budget.
66
+ - name: GEMINI_DEBUG
67
+ required: false
68
+ help: >-
69
+ When set, emit heartbeat diagnostics to stderr. Off by default.
41
70
  - name: GEMINI_RATE_CARD
42
71
  required: false
43
72
  help: >-
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chrischall/gemini-mcp",
3
- "version": "2.4.0",
3
+ "version": "2.4.2",
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>",
@@ -38,7 +38,7 @@
38
38
  ],
39
39
  "scripts": {
40
40
  "build": "tsc && npm run bundle",
41
- "bundle": "esbuild src/index.ts --bundle --alias:zod/v4=./node_modules/zod/v4/index.cjs --platform=node --format=esm --external:dotenv --outfile=dist/bundle.js",
41
+ "bundle": "esbuild src/index.ts --bundle --alias:zod/v4=./node_modules/zod/v4/index.cjs --platform=node --format=esm --external:dotenv --banner:js=\"import { createRequire as __createRequire } from 'module'; import { fileURLToPath as __fileURLToPath } from 'url'; import { dirname as __pathDirname } from 'path'; const require = __createRequire(import.meta.url); const __filename = __fileURLToPath(import.meta.url); const __dirname = __pathDirname(__filename);\" --outfile=dist/bundle.js",
42
42
  "dev": "node dist/index.js",
43
43
  "test": "npm run typecheck && vitest run",
44
44
  "test:watch": "vitest",
@@ -46,9 +46,10 @@
46
46
  "typecheck": "tsc -p tsconfig.test.json"
47
47
  },
48
48
  "dependencies": {
49
- "@chrischall/mcp-utils": "^3.0.0",
49
+ "@chrischall/mcp-utils": "^3.3.0",
50
50
  "@modelcontextprotocol/server": "^2.3.0",
51
51
  "dotenv": "^18.0.2",
52
+ "undici": "^8.12.2",
52
53
  "zod": "^4.6.5"
53
54
  },
54
55
  "devDependencies": {
@@ -58,5 +59,9 @@
58
59
  "esbuild": "^0.28.0",
59
60
  "typescript": "^7.0.2",
60
61
  "vitest": "^5.0.0"
62
+ },
63
+ "allowScripts": {
64
+ "esbuild": false,
65
+ "fsevents": false
61
66
  }
62
67
  }
package/server.json CHANGED
@@ -6,12 +6,12 @@
6
6
  "url": "https://github.com/chrischall/gemini-mcp",
7
7
  "source": "github"
8
8
  },
9
- "version": "2.4.0",
9
+ "version": "2.4.2",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "@chrischall/gemini-mcp",
14
- "version": "2.4.0",
14
+ "version": "2.4.2",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },
@@ -32,7 +32,7 @@
32
32
  },
33
33
  {
34
34
  "name": "GEMINI_OUTPUT_DIR",
35
- "description": "Default directory for generated images (default: current working directory)",
35
+ "description": "Default directory for generated images (default: ~/Downloads/gemini-mcp). When set, a per-call output_dir must be inside it.",
36
36
  "isRequired": false,
37
37
  "format": "string",
38
38
  "isSecret": false
@@ -96,7 +96,7 @@ Note: Image generation requires a billing-enabled Google Cloud project.
96
96
  | `gemini_image_edit(prompt, images_url?, images_file_uris?, images?, images_base64?, google_search?, seed?, filename?, model?, aspect_ratio?, image_size?, thinking_level?, output_dir?, inline?)` | Edit or compose input image(s) with a text instruction. Requires ≥1 input from any of the four reference forms |
97
97
  | `gemini_image_set(master_prompt, scenes? \| count?, reference_mode?, master_images_url?, master_images_file_uris?, 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. `master_images_url` / `master_images_file_uris` are resolved **once** and passed to the master *and* every scene |
98
98
 
99
- ### Reference images — four ways in, one that costs context
99
+ ### Reference images — several ways in, one that costs context
100
100
 
101
101
  These apply to **every** tool that takes a reference image: the four image tools, plus
102
102
  `gemini_video_generate` (reference stills) and `gemini_music_generate`.
@@ -105,7 +105,9 @@ These apply to **every** tool that takes a reference image: the four image tools
105
105
  |---|---|---|
106
106
  | `images_url` (`master_images_url`) | the **server** downloads the https URL | none |
107
107
  | `images_file_uris` (`master_images_file_uris`) | a `files/<id>` reference from `gemini_upload_file` | none |
108
+ | `images_r2_keys` (`master_images_r2_keys`) | the server reads its **own store** — an `r2_key` from `gemini_get_upload_url` or a generation result (hosted deployments only) | none |
108
109
  | `images` | read off local disk (stdio builds only) | none |
110
+ | `characters` / `style` | saved library entries, attached by name (hosted deployments only) | none |
109
111
  | `images_base64` | **through the tool-call JSON** | **~14k tokens per JPEG** |
110
112
 
111
113
  **Reach for `images_base64` last.** It costs ~14k tokens per modest photo, and a truncated file
@@ -130,17 +132,32 @@ not an error.
130
132
  | `gemini_list_files(page_size?)` | List current uploads with MIME types and expiry |
131
133
  | `gemini_delete_file(file_uri, confirmToken?)` | Delete an upload before its ~48h expiry (confirmed first — see Notes) |
132
134
 
133
- On the **hosted hosted deployment** there is also `POST /upload`, behind the same OAuth token as
134
- `/mcp` — the zero-base64 path for an agent with a shell:
135
+ ### Hosted deployment only
136
+
137
+ A hosted deployment has no filesystem, so these tools stand in for it. There is no `POST /upload`
138
+ endpoint (it went with the retired Worker connector): an agent with a shell uploads through a
139
+ signed URL instead.
140
+
141
+ | Tool | Description |
142
+ |------|-------------|
143
+ | `gemini_get_upload_url(filename, content_type)` | Mint a ~10-minute signed **PUT** URL. `curl -X PUT -H "Content-Type: <type>" --data-binary @file "<upload_url>"` needs no auth header. The result's `r2_key` goes to `images_r2_keys`, `gemini_save_character`, or `gemini_upload_file({ r2_key })` |
144
+ | `gemini_sign_media(r2_key)` | Mint a fresh signed link for stored media (links live at most 24h) — an expired link is not a reason to re-generate |
145
+ | `gemini_view_media(r2_key)` | Return a stored image inline so you can look at what was made before refining it |
146
+ | `gemini_list_recent_media(limit?, since_day?)` | Recent stored media with prompt, model and interaction id — how a lost result or turn is found again |
147
+ | `gemini_save_character` / `gemini_list_characters` / `gemini_delete_character` | Per-account character library (reference image + description, no expiry); attach with `characters: ["name"]` |
148
+ | `gemini_save_style` / `gemini_list_styles` / `gemini_delete_style` | Per-account style presets (prompt fragment, optional reference image, no expiry); attach with `style: "name"` |
135
149
 
136
150
  ```bash
137
- curl -X POST https://<hosted deployment>/upload \
138
- -H "Authorization: Bearer $ACCESS_TOKEN" \
139
- -H "Content-Type: image/jpeg" \
140
- --data-binary @photo.jpg
141
- # → {"file_uri":"files/abc123", ...} then: images_file_uris: ["files/abc123"]
151
+ # 1. tool: gemini_get_upload_url { filename: "photo.jpg", content_type: "image/jpeg" }
152
+ # → { upload_url, r2_key, expires_at, curl_hint }
153
+ # 2. shell:
154
+ curl -sS -X PUT -H "Content-Type: image/jpeg" --data-binary @photo.jpg "$UPLOAD_URL"
155
+ # 3. tool: gemini_image_edit { prompt: "make it winter", images_r2_keys: ["<r2_key from step 1>"] }
142
156
  ```
143
157
 
158
+ Uploads are raster images only, capped at 15MB, and follow the host's retention schedule; save
159
+ anything you want to keep with `gemini_save_character`.
160
+
144
161
  ### Multi-turn (Interactions API)
145
162
  | Tool | Description |
146
163
  |------|-------------|
@@ -183,15 +200,16 @@ gemini_image_edit(prompt: "make the background blue", images_url: ["https://exam
183
200
  → the server fetches the URL itself; returns path to edited PNG
184
201
  ```
185
202
 
186
- **Reuse one photo across many generations (hosted hosted deployment, agent with a shell):**
203
+ **Reuse one photo across many generations (hosted deployment, agent with a shell):**
187
204
  ```
188
- $ curl -X POST https://<hosted deployment>/upload -H "Authorization: Bearer $TOKEN" \
189
- -H "Content-Type: image/jpeg" --data-binary @photo.jpg
190
- → {"file_uri": "files/abc123"}
191
-
192
- gemini_image_edit(prompt: "make it winter", images_file_uris: ["files/abc123"])
193
- gemini_image_edit(prompt: "make it sunrise", images_file_uris: ["files/abc123"])
194
- → two edits, one upload, zero image bytes in context. Valid ~48h.
205
+ gemini_get_upload_url(filename: "photo.jpg", content_type: "image/jpeg")
206
+ → { upload_url, r2_key: "up/…/photo.jpg" }
207
+ $ curl -sS -X PUT -H "Content-Type: image/jpeg" --data-binary @photo.jpg "$UPLOAD_URL"
208
+
209
+ gemini_image_edit(prompt: "make it winter", images_r2_keys: ["up/…/photo.jpg"])
210
+ gemini_image_edit(prompt: "make it sunrise", images_r2_keys: ["up/…/photo.jpg"])
211
+ → two edits, one upload, zero image bytes in context. To keep the photo past the
212
+ upload retention window, gemini_save_character it and pass characters: ["name"].
195
213
  ```
196
214
 
197
215
  **Generate a consistent set (master + scenes):**
package/dist/zip.js DELETED
@@ -1,113 +0,0 @@
1
- /**
2
- * Minimal ZIP writer (STORE only, no compression) for bundling a multi-image
3
- * result into one downloadable archive.
4
- *
5
- * Why hand-rolled: the Worker has no filesystem and no archiver dependency,
6
- * and the images being bundled are already-compressed PNG/JPEG — DEFLATE would
7
- * buy nothing. STORE entries are just headers + raw bytes + CRC32, which is
8
- * ~100 lines and runs identically under Node and workerd.
9
- *
10
- * Layout: [local header + name + data]* [central directory]* [EOCD]. No
11
- * ZIP64 (a bundle is a handful of images, far under the 4 GB / 65k-entry
12
- * limits), no encryption, no streaming.
13
- */
14
- /** Standard CRC-32 (IEEE 802.3), table-driven. The table is immutable. */
15
- const CRC_TABLE = (() => {
16
- const table = new Uint32Array(256);
17
- for (let n = 0; n < 256; n++) {
18
- let c = n;
19
- for (let k = 0; k < 8; k++)
20
- c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
21
- table[n] = c >>> 0;
22
- }
23
- return table;
24
- })();
25
- export function crc32(bytes) {
26
- let crc = 0xffffffff;
27
- for (let i = 0; i < bytes.length; i++) {
28
- crc = CRC_TABLE[(crc ^ bytes[i]) & 0xff] ^ (crc >>> 8);
29
- }
30
- return (crc ^ 0xffffffff) >>> 0;
31
- }
32
- /** MS-DOS date/time pair (ZIP's native timestamp format). */
33
- function dosDateTime(at) {
34
- const year = Math.max(1980, at.getUTCFullYear());
35
- return {
36
- time: (at.getUTCHours() << 11) | (at.getUTCMinutes() << 5) | (at.getUTCSeconds() >> 1),
37
- date: ((year - 1980) << 9) | ((at.getUTCMonth() + 1) << 5) | at.getUTCDate(),
38
- };
39
- }
40
- const LOCAL_HEADER = 0x04034b50;
41
- const CENTRAL_HEADER = 0x02014b50;
42
- const EOCD = 0x06054b50;
43
- /** Version 2.0 — the minimum that understands STORE + directories. */
44
- const VERSION = 20;
45
- /** General-purpose bit 11: names are UTF-8. */
46
- const FLAG_UTF8 = 0x0800;
47
- /**
48
- * Build a STORE-only zip. `modifiedAt` stamps every entry (injectable so a
49
- * given input always produces identical bytes; defaults to the epoch of the
50
- * format, 1980).
51
- */
52
- export function buildZip(entries, modifiedAt = new Date(Date.UTC(1980, 0, 1))) {
53
- const { time, date } = dosDateTime(modifiedAt);
54
- const encoder = new TextEncoder();
55
- const parts = [];
56
- const central = [];
57
- let offset = 0;
58
- for (const entry of entries) {
59
- const name = encoder.encode(entry.name);
60
- const crc = crc32(entry.bytes);
61
- const size = entry.bytes.byteLength;
62
- const local = new Uint8Array(30 + name.byteLength);
63
- const lv = new DataView(local.buffer);
64
- lv.setUint32(0, LOCAL_HEADER, true);
65
- lv.setUint16(4, VERSION, true);
66
- lv.setUint16(6, FLAG_UTF8, true);
67
- lv.setUint16(8, 0, true); // method: STORE
68
- lv.setUint16(10, time, true);
69
- lv.setUint16(12, date, true);
70
- lv.setUint32(14, crc, true);
71
- lv.setUint32(18, size, true); // compressed == uncompressed under STORE
72
- lv.setUint32(22, size, true);
73
- lv.setUint16(26, name.byteLength, true);
74
- lv.setUint16(28, 0, true); // extra length
75
- local.set(name, 30);
76
- const dir = new Uint8Array(46 + name.byteLength);
77
- const dv = new DataView(dir.buffer);
78
- dv.setUint32(0, CENTRAL_HEADER, true);
79
- dv.setUint16(4, VERSION, true); // version made by
80
- dv.setUint16(6, VERSION, true); // version needed
81
- dv.setUint16(8, FLAG_UTF8, true);
82
- dv.setUint16(10, 0, true);
83
- dv.setUint16(12, time, true);
84
- dv.setUint16(14, date, true);
85
- dv.setUint32(16, crc, true);
86
- dv.setUint32(20, size, true);
87
- dv.setUint32(24, size, true);
88
- dv.setUint16(28, name.byteLength, true);
89
- // extra len, comment len, disk start, internal attrs, external attrs = 0
90
- dv.setUint32(42, offset, true);
91
- dir.set(name, 46);
92
- parts.push(local, entry.bytes);
93
- central.push(dir);
94
- offset += local.byteLength + size;
95
- }
96
- const centralSize = central.reduce((n, c) => n + c.byteLength, 0);
97
- const eocd = new Uint8Array(22);
98
- const ev = new DataView(eocd.buffer);
99
- ev.setUint32(0, EOCD, true);
100
- ev.setUint16(8, entries.length, true); // entries on this disk
101
- ev.setUint16(10, entries.length, true); // entries total
102
- ev.setUint32(12, centralSize, true);
103
- ev.setUint32(16, offset, true); // central directory offset
104
- // comment length = 0
105
- const total = offset + centralSize + eocd.byteLength;
106
- const out = new Uint8Array(total);
107
- let cursor = 0;
108
- for (const part of [...parts, ...central, eocd]) {
109
- out.set(part, cursor);
110
- cursor += part.byteLength;
111
- }
112
- return out;
113
- }