@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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.mcp.json +5 -2
- package/README.md +19 -16
- package/dist/bundle.js +32280 -539
- package/dist/client.js +12 -3
- package/dist/fetch-image.js +70 -138
- package/dist/images.js +16 -23
- package/dist/media-url.js +4 -59
- package/dist/storage/media.js +3 -15
- package/dist/tools/_confirm.js +13 -33
- package/dist/tools/files.js +8 -8
- package/dist/tools/library.js +6 -6
- package/dist/tools/shared.js +6 -3
- package/dist/tools/uploads.js +1 -1
- package/dist/version.js +1 -1
- package/mint.yaml +31 -2
- package/package.json +8 -3
- package/server.json +3 -3
- package/skills/gemini-mcp/SKILL.md +34 -16
- package/dist/zip.js +0 -113
package/dist/tools/uploads.js
CHANGED
|
@@ -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
|
|
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.
|
|
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:
|
|
21
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
9
|
+
"version": "2.4.2",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "@chrischall/gemini-mcp",
|
|
14
|
-
"version": "2.4.
|
|
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:
|
|
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 —
|
|
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
|
-
|
|
134
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
#
|
|
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
|
|
203
|
+
**Reuse one photo across many generations (hosted deployment, agent with a shell):**
|
|
187
204
|
```
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
gemini_image_edit(prompt: "make it winter",
|
|
193
|
-
gemini_image_edit(prompt: "make it sunrise",
|
|
194
|
-
→ two edits, one upload, zero image bytes in context.
|
|
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
|
-
}
|