@chrischall/gemini-mcp 1.0.4 → 1.2.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 +75 -1
- package/dist/bundle.js +854 -137
- package/dist/bytes.js +46 -0
- package/dist/client.js +194 -17
- package/dist/fetch-image.js +361 -0
- package/dist/images.js +0 -19
- package/dist/index.js +2 -1
- package/dist/inputs.js +165 -0
- package/dist/session.js +39 -0
- package/dist/storage/media.js +2 -13
- package/dist/tools/files.js +188 -0
- package/dist/tools/generate.js +21 -12
- package/dist/tools/interact.js +12 -5
- package/dist/tools/music.js +10 -4
- package/dist/tools/set.js +29 -7
- package/dist/tools/shared.js +44 -6
- package/dist/tools/video.js +10 -4
- package/dist/upload-endpoint.js +79 -0
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/server.json +2 -2
- package/skills/gemini-mcp/SKILL.md +65 -6
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/** Media the Files API is useful for. Anything else is a caller mistake. */
|
|
2
|
+
const ACCEPTED_PREFIXES = ['image/', 'video/', 'audio/'];
|
|
3
|
+
function json(body, status) {
|
|
4
|
+
return new Response(JSON.stringify(body, null, 2), {
|
|
5
|
+
status,
|
|
6
|
+
headers: { 'content-type': 'application/json; charset=utf-8' },
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
/** Every error names the fix, because the caller here is a shell, not a model. */
|
|
10
|
+
function fail(status, error, hint) {
|
|
11
|
+
return json({ error, hint }, status);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Build the `/upload` route handler. `buildClient` is injected rather than
|
|
15
|
+
* imported so the Worker can construct a client from *this request's* OAuth
|
|
16
|
+
* props (never a shared singleton — see the cross-tenant note in session.ts),
|
|
17
|
+
* and so tests can drive the whole handler against a stub client.
|
|
18
|
+
*/
|
|
19
|
+
export function createUploadHandler(buildClient) {
|
|
20
|
+
return async function handleUpload(request, ctx) {
|
|
21
|
+
if (request.method === 'OPTIONS') {
|
|
22
|
+
return new Response(null, { status: 204, headers: { allow: 'POST, OPTIONS' } });
|
|
23
|
+
}
|
|
24
|
+
if (request.method !== 'POST') {
|
|
25
|
+
return fail(405, `Method ${request.method} not allowed on /upload.`, 'POST the raw file bytes as the request body.');
|
|
26
|
+
}
|
|
27
|
+
// The OAuth provider populates props from the validated bearer token. Its
|
|
28
|
+
// absence means the route was reached some other way — refuse rather than
|
|
29
|
+
// fall back to an ambient key, which on a multi-tenant Worker would bill an
|
|
30
|
+
// unauthenticated upload to whoever configured the environment.
|
|
31
|
+
const apiKey = ctx.props?.apiKey;
|
|
32
|
+
if (typeof apiKey !== 'string' || !apiKey) {
|
|
33
|
+
return fail(401, 'Unauthenticated.', 'Send the same Authorization: Bearer <access-token> you use for the /mcp endpoint.');
|
|
34
|
+
}
|
|
35
|
+
const contentType = (request.headers.get('content-type') ?? '').split(';')[0].trim().toLowerCase();
|
|
36
|
+
if (!ACCEPTED_PREFIXES.some((p) => contentType.startsWith(p))) {
|
|
37
|
+
return fail(415, `Unsupported Content-Type "${contentType || '(none)'}".`, 'Set Content-Type to the file\'s media type, e.g. -H "Content-Type: image/jpeg".');
|
|
38
|
+
}
|
|
39
|
+
const declared = request.headers.get('content-length');
|
|
40
|
+
const contentLength = Number(declared);
|
|
41
|
+
if (!declared || !Number.isFinite(contentLength) || contentLength <= 0) {
|
|
42
|
+
return fail(411, 'Content-Length is required (and must be > 0).', 'The Files API resumable upload declares the size up front, so the body cannot be sent chunked. ' +
|
|
43
|
+
'curl sets this automatically with --data-binary @file.');
|
|
44
|
+
}
|
|
45
|
+
const body = request.body;
|
|
46
|
+
if (!body) {
|
|
47
|
+
return fail(400, 'Request had no body.', 'Send the file bytes as the request body, e.g. --data-binary @photo.jpg.');
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
const file = await buildClient(apiKey).uploadStream(body, contentType, displayNameFor(request, contentType), contentLength);
|
|
51
|
+
return json({
|
|
52
|
+
file_uri: file.name,
|
|
53
|
+
uri: file.uri,
|
|
54
|
+
name: file.name,
|
|
55
|
+
mime_type: file.mimeType,
|
|
56
|
+
...(file.sizeBytes ? { size_bytes: Number(file.sizeBytes) } : {}),
|
|
57
|
+
expires: file.expirationTime ?? '~48h from upload',
|
|
58
|
+
hint: `Pass "${file.name}" as images_file_uris (or video_url) on any image tool. Gemini retains it ~48h.`,
|
|
59
|
+
}, 201);
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
// Upstream failures are the interesting case here (bad key, quota, a file
|
|
63
|
+
// over the 2 GB cap), so the message is passed through rather than
|
|
64
|
+
// flattened into "upload failed".
|
|
65
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
66
|
+
return fail(502, `Upload to the Gemini Files API failed: ${message}`, 'Check the API key on this connection is still valid and funded.');
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* A display name for the upload: an explicit `?name=` wins, else the
|
|
72
|
+
* `X-Filename` header (what a wrapper script would send), else the media type.
|
|
73
|
+
* Cosmetic — the Files API keys on the returned resource name, not this.
|
|
74
|
+
*/
|
|
75
|
+
function displayNameFor(request, contentType) {
|
|
76
|
+
const fromQuery = new URL(request.url).searchParams.get('name');
|
|
77
|
+
const raw = fromQuery ?? request.headers.get('x-filename') ?? `upload.${contentType.split('/')[1] || 'bin'}`;
|
|
78
|
+
return raw.replace(/[^\w.\-]+/g, '-').slice(0, 120) || 'upload';
|
|
79
|
+
}
|
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 = '1.0
|
|
2
|
+
export const VERSION = '1.2.0'; // x-release-please-version
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chrischall/gemini-mcp",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.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>",
|
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": "1.0
|
|
9
|
+
"version": "1.2.0",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "@chrischall/gemini-mcp",
|
|
14
|
-
"version": "1.0
|
|
14
|
+
"version": "1.2.0",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|
|
@@ -91,20 +91,62 @@ Note: Image generation requires a billing-enabled Google Cloud project.
|
|
|
91
91
|
### Image Generation
|
|
92
92
|
| Tool | Description |
|
|
93
93
|
|------|-------------|
|
|
94
|
-
| `gemini_image_generate(prompt, count?, images?, images_base64?, video_url?, video_path?, google_search?, seed?, filename?, model?, aspect_ratio?, image_size?, thinking_level?, output_dir?, inline?)` | Generate image(s) from a text prompt (optionally image-conditioned
|
|
95
|
-
| `gemini_image_edit(prompt, images?, images_base64?, google_search?, seed?, filename?, model?, aspect_ratio?, image_size?, thinking_level?, output_dir?, inline?)` | Edit or compose input image(s)
|
|
96
|
-
| `gemini_image_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 |
|
|
94
|
+
| `gemini_image_generate(prompt, count?, images_url?, images_file_uris?, images?, images_base64?, video_url?, video_path?, google_search?, seed?, filename?, model?, aspect_ratio?, image_size?, thinking_level?, output_dir?, inline?)` | Generate image(s) from a text prompt (optionally image-conditioned — see **Reference images** below — or video-conditioned via `video_url`/`video_path`) |
|
|
95
|
+
| `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 |
|
|
96
|
+
| `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 |
|
|
97
|
+
|
|
98
|
+
### Reference images — four ways in, one that costs context
|
|
99
|
+
|
|
100
|
+
These apply to **every** tool that takes a reference image: the four image tools, plus
|
|
101
|
+
`gemini_video_generate` (reference stills) and `gemini_music_generate`.
|
|
102
|
+
|
|
103
|
+
| Parameter | Where the bytes travel | Context cost |
|
|
104
|
+
|---|---|---|
|
|
105
|
+
| `images_url` (`master_images_url`) | the **server** downloads the https URL | none |
|
|
106
|
+
| `images_file_uris` (`master_images_file_uris`) | a `files/<id>` reference from `gemini_upload_file` | none |
|
|
107
|
+
| `images` | read off local disk (stdio builds only) | none |
|
|
108
|
+
| `images_base64` | **through the tool-call JSON** | **~14k tokens per JPEG** |
|
|
109
|
+
|
|
110
|
+
**Reach for `images_base64` last.** It costs ~14k tokens per modest photo, and a truncated file
|
|
111
|
+
read produces base64 that still *looks* valid — so the corruption surfaces as a bad generation,
|
|
112
|
+
not an error.
|
|
113
|
+
|
|
114
|
+
- `images_url` accepts public `https://` URLs only (private/loopback/link-local hosts refused,
|
|
115
|
+
every redirect revalidated), must be `Content-Type: image/*`, capped at 15MB. Errors name the
|
|
116
|
+
failing URL. Over 6MB is auto-uploaded to the Files API instead of inlined.
|
|
117
|
+
- `images_file_uris` accepts `files/<id>` or the full uri. Retained **~48h**; reusable across
|
|
118
|
+
any number of calls until then.
|
|
119
|
+
- On stdio, an `images` path referenced **more than once in a session** is auto-uploaded to the
|
|
120
|
+
Files API (keyed on path + mtime + size) so the bytes stop being re-sent.
|
|
121
|
+
|
|
122
|
+
### Files API
|
|
123
|
+
| Tool | Description |
|
|
124
|
+
|------|-------------|
|
|
125
|
+
| `gemini_upload_file(url? \| data_base64? \| path?, mime_type?, display_name?, confirm?)` | Upload once, get a reusable `files/<id>`. Exactly one source. `url` is fetched by the server (image/video/audio, ≤100MB); `path` is stdio-only and confirm-gated; `data_base64` is the last resort |
|
|
126
|
+
| `gemini_list_files(page_size?)` | List current uploads with MIME types and expiry |
|
|
127
|
+
| `gemini_delete_file(file_uri, confirm)` | Delete an upload before its ~48h expiry |
|
|
128
|
+
|
|
129
|
+
On the **hosted connector** there is also `POST /upload`, behind the same OAuth token as
|
|
130
|
+
`/mcp` — the zero-base64 path for an agent with a shell:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
curl -X POST https://<connector>/upload \
|
|
134
|
+
-H "Authorization: Bearer $ACCESS_TOKEN" \
|
|
135
|
+
-H "Content-Type: image/jpeg" \
|
|
136
|
+
--data-binary @photo.jpg
|
|
137
|
+
# → {"file_uri":"files/abc123", ...} then: images_file_uris: ["files/abc123"]
|
|
138
|
+
```
|
|
97
139
|
|
|
98
140
|
### Multi-turn (Interactions API)
|
|
99
141
|
| Tool | Description |
|
|
100
142
|
|------|-------------|
|
|
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**. |
|
|
143
|
+
| `gemini_interact(input, previous_interaction_id?, continue_last?, images_url?, images_file_uris?, 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**. |
|
|
102
144
|
|
|
103
145
|
### Video & Music (preview — funded account)
|
|
104
146
|
| Tool | Description |
|
|
105
147
|
|------|-------------|
|
|
106
|
-
| `gemini_video_generate(prompt, aspect_ratio?, task?, images?, images_base64?, from_clipboard?, previous_interaction_id?, continue_last?, model?, filename?, output_dir?, timeout_ms?, idempotency_key?, async?)` | Generate a short video via the Gemini omni model: `text_to_video` (default), `image_to_video` / `reference_to_video` (supply reference image[s]), or `edit` (with `previous_interaction_id` / `continue_last`). `aspect_ratio` is `16:9` or `9:16`. Written to disk as **MP4**. Runs long — use `async: true` + `gemini_get_result`, or raise `timeout_ms`. |
|
|
107
|
-
| `gemini_music_generate(prompt, model?, audio_format?, images?, images_base64?, from_clipboard?, previous_interaction_id?, continue_last?, filename?, output_dir?, inline?, timeout_ms?, idempotency_key?, async?)` | Generate music from a prompt (mood/genre/instruments/structure/lyrics) via a Lyria model: `lyria-3-clip-preview` (~30s, default) or `lyria-3-pro-preview` (longer, WAV-capable). `audio_format` `mp3` (default) or `wav` (**Pro-only**). Written to disk as MP3/WAV, or returned inline. |
|
|
148
|
+
| `gemini_video_generate(prompt, aspect_ratio?, task?, images_url?, images_file_uris?, images?, images_base64?, from_clipboard?, previous_interaction_id?, continue_last?, model?, filename?, output_dir?, timeout_ms?, idempotency_key?, async?)` | Generate a short video via the Gemini omni model: `text_to_video` (default), `image_to_video` / `reference_to_video` (supply reference image[s]), or `edit` (with `previous_interaction_id` / `continue_last`). `aspect_ratio` is `16:9` or `9:16`. Written to disk as **MP4**. Runs long — use `async: true` + `gemini_get_result`, or raise `timeout_ms`. |
|
|
149
|
+
| `gemini_music_generate(prompt, model?, audio_format?, images_url?, images_file_uris?, images?, images_base64?, from_clipboard?, previous_interaction_id?, continue_last?, filename?, output_dir?, inline?, timeout_ms?, idempotency_key?, async?)` | Generate music from a prompt (mood/genre/instruments/structure/lyrics) via a Lyria model: `lyria-3-clip-preview` (~30s, default) or `lyria-3-pro-preview` (longer, WAV-capable). `audio_format` `mp3` (default) or `wav` (**Pro-only**). Written to disk as MP3/WAV, or returned inline. |
|
|
108
150
|
|
|
109
151
|
### Async / idempotency (any generation tool)
|
|
110
152
|
| Tool | Description |
|
|
@@ -131,6 +173,23 @@ gemini_image_edit(prompt: "make the background blue", images: ["/path/to/image.p
|
|
|
131
173
|
→ returns path to edited PNG
|
|
132
174
|
```
|
|
133
175
|
|
|
176
|
+
**Edit an image you only have a URL for (nothing downloads into the conversation):**
|
|
177
|
+
```
|
|
178
|
+
gemini_image_edit(prompt: "make the background blue", images_url: ["https://example.com/photo.jpg"])
|
|
179
|
+
→ the server fetches the URL itself; returns path to edited PNG
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
**Reuse one photo across many generations (hosted connector, agent with a shell):**
|
|
183
|
+
```
|
|
184
|
+
$ curl -X POST https://<connector>/upload -H "Authorization: Bearer $TOKEN" \
|
|
185
|
+
-H "Content-Type: image/jpeg" --data-binary @photo.jpg
|
|
186
|
+
→ {"file_uri": "files/abc123"}
|
|
187
|
+
|
|
188
|
+
gemini_image_edit(prompt: "make it winter", images_file_uris: ["files/abc123"])
|
|
189
|
+
gemini_image_edit(prompt: "make it sunrise", images_file_uris: ["files/abc123"])
|
|
190
|
+
→ two edits, one upload, zero image bytes in context. Valid ~48h.
|
|
191
|
+
```
|
|
192
|
+
|
|
134
193
|
**Generate a consistent set (master + scenes):**
|
|
135
194
|
```
|
|
136
195
|
gemini_image_set(
|