@chrischall/gemini-mcp 1.13.0 → 1.14.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 +12 -3
- package/dist/bundle.js +7112 -2400
- package/dist/client.js +38 -35
- package/dist/inputs.js +80 -3
- package/dist/jobs.js +47 -13
- package/dist/models.js +14 -4
- package/dist/pricing.js +9 -1
- package/dist/session.js +7 -6
- package/dist/storage/media.js +265 -42
- package/dist/tools/files.js +67 -11
- package/dist/tools/generate.js +5 -5
- package/dist/tools/interact.js +65 -32
- package/dist/tools/music.js +31 -44
- package/dist/tools/set.js +3 -3
- package/dist/tools/shared.js +72 -41
- package/dist/tools/uploads.js +6 -6
- package/dist/tools/video.js +27 -10
- package/dist/upload-url.js +3 -3
- package/dist/version.js +1 -1
- package/package.json +6 -6
- package/server.json +2 -2
- package/skills/gemini-mcp/SKILL.md +16 -8
package/dist/tools/video.js
CHANGED
|
@@ -3,13 +3,20 @@ import { McpToolError } from '@chrischall/mcp-utils';
|
|
|
3
3
|
import { slugify, baseName } from '../images.js';
|
|
4
4
|
import { resolveImageInputs } from '../inputs.js';
|
|
5
5
|
import { DEFAULT_VIDEO_MODEL } from '../models.js';
|
|
6
|
-
import { emitMedia, reportShape, resolveAspectRatio, orientationSchema, timeoutMsSchema, idempotencyKeySchema, asyncSchema, maxWaitMsSchema, withProgressHeartbeat, assertLocalInputsAvailable, imagesUrlSchema, imagesFileUrisSchema } from './shared.js';
|
|
6
|
+
import { emitMedia, reportShape, resolveAspectRatio, orientationSchema, timeoutMsSchema, idempotencyKeySchema, asyncSchema, maxWaitMsSchema, withProgressHeartbeat, assertLocalInputsAvailable, imagesUrlSchema, imagesFileUrisSchema, imagesBase64Schema } from './shared.js';
|
|
7
7
|
import { fingerprintRequest } from '../jobs.js';
|
|
8
8
|
import { attachCost } from '../pricing.js';
|
|
9
9
|
import { previewLocalInputsUnlessConfirmed, schemaConfirm } from './_confirm.js';
|
|
10
10
|
/** omni's own aspect-ratio enum — NOT the image tools' ASPECT_RATIOS. */
|
|
11
11
|
const VIDEO_ASPECT_RATIOS = ['16:9', '9:16'];
|
|
12
|
-
const VIDEO_TASKS = ['text_to_video', 'image_to_video', 'reference_to_video', 'edit'];
|
|
12
|
+
const VIDEO_TASKS = ['text_to_video', 'image_to_video', 'reference_to_video', 'edit', 'extend'];
|
|
13
|
+
/**
|
|
14
|
+
* omni's output resolutions (GA, verified live 2026-09-09). Unlike `delivery`
|
|
15
|
+
* this is content and not transport: it is the cost lever on the whole video
|
|
16
|
+
* path, since omni bills video output per token and a 10s clip came back at
|
|
17
|
+
* ~$0.34 in 360p against roughly triple that in 720p.
|
|
18
|
+
*/
|
|
19
|
+
const VIDEO_RESOLUTIONS = ['360p', '720p', '1080p', '4k'];
|
|
13
20
|
// `delivery` is transport, not content: uri hands back a Files API link the
|
|
14
21
|
// client downloads with the api key, inline returns base64 capped at ~4MB.
|
|
15
22
|
// Left unset here so the client's verified default (uri) applies; this is an
|
|
@@ -24,20 +31,23 @@ const VIDEO_DELIVERY = ['inline', 'uri'];
|
|
|
24
31
|
// "last video" and a "last image" are distinct chains. See src/session.ts.
|
|
25
32
|
export function registerVideoTools(server, client) {
|
|
26
33
|
server.registerTool('gemini_video_generate', {
|
|
27
|
-
description: 'Generate a short video via the Gemini omni model
|
|
28
|
-
'(supply reference image[s]),
|
|
29
|
-
'
|
|
30
|
-
'
|
|
34
|
+
description: 'Generate a short video (~10s) via the Gemini omni model: text→video, image→video / reference→video ' +
|
|
35
|
+
'(supply reference image[s]), interpolate between two stills (pass first frame then last frame as images), ' +
|
|
36
|
+
'or continue a prior video (task: "edit" or "extend" + previous_interaction_id / continue_last; extensions ' +
|
|
37
|
+
'add ~3-10s each, to ~40s total). Cost scales with `resolution` — draft at 360p, keep at 1080p/4k. ' +
|
|
38
|
+
'Output is written to disk as MP4 (video has no inline MCP block). Video runs long — give it a `max_wait_ms` budget ' +
|
|
39
|
+
'(or `async: true` + gemini_get_result on a local install), or raise `timeout_ms`. Needs a funded account.',
|
|
31
40
|
annotations: { readOnlyHint: false, openWorldHint: true },
|
|
32
41
|
inputSchema: {
|
|
33
42
|
prompt: z.string().min(1).describe('Description of the video to generate (or the edit instruction when task=edit)'),
|
|
34
43
|
aspect_ratio: z.enum(VIDEO_ASPECT_RATIOS).optional().describe('Exact output aspect ratio (omni: 16:9 or 9:16). `orientation` is the plain-language shorthand; this wins if both are given.'),
|
|
35
44
|
orientation: orientationSchema,
|
|
36
|
-
task: z.enum(VIDEO_TASKS).optional().describe('text_to_video (default), image_to_video / reference_to_video (need image input), or edit (
|
|
45
|
+
task: z.enum(VIDEO_TASKS).optional().describe('text_to_video (default), image_to_video / reference_to_video (need image input), or edit / extend (need previous_interaction_id)'),
|
|
46
|
+
resolution: z.enum(VIDEO_RESOLUTIONS).optional().describe('Output resolution (default 720p). Video is billed per output token, so 360p costs roughly a third of 720p — use it for drafts'),
|
|
37
47
|
images: z.array(z.string().min(1)).optional().describe('Reference image path(s) for image_to_video / reference_to_video'),
|
|
38
48
|
images_url: imagesUrlSchema('Reference stills'),
|
|
39
49
|
images_file_uris: imagesFileUrisSchema('Reference stills'),
|
|
40
|
-
images_base64:
|
|
50
|
+
images_base64: imagesBase64Schema(),
|
|
41
51
|
from_clipboard: z.boolean().optional().describe('Use the image currently on the macOS clipboard as a reference'),
|
|
42
52
|
filename: z.string().optional().describe('Base filename for the output video (extension stripped; default: slugified prompt)'),
|
|
43
53
|
output_dir: z.string().optional().describe('Directory to write the video to (default: $GEMINI_OUTPUT_DIR or cwd)'),
|
|
@@ -78,7 +88,9 @@ export function registerVideoTools(server, client) {
|
|
|
78
88
|
// fingerprint so "portrait" and "9:16" dedup to one billable job.
|
|
79
89
|
const aspectRatio = resolveAspectRatio(args, VIDEO_ASPECT_RATIOS);
|
|
80
90
|
const fingerprint = fingerprintRequest('gemini_video_generate', {
|
|
81
|
-
|
|
91
|
+
// `resolution` is IN the fingerprint (a 4k render is not the 360p one)
|
|
92
|
+
// where `delivery` is not (same bytes, different transport).
|
|
93
|
+
model, prompt: args.prompt, aspect_ratio: aspectRatio, resolution: args.resolution, task: args.task,
|
|
82
94
|
images: args.images, images_base64: args.images_base64, from_clipboard: args.from_clipboard,
|
|
83
95
|
images_url: args.images_url, images_file_uris: args.images_file_uris,
|
|
84
96
|
previous_interaction_id: previousInteractionId,
|
|
@@ -90,6 +102,7 @@ export function registerVideoTools(server, client) {
|
|
|
90
102
|
images: inputs.length ? inputs : undefined,
|
|
91
103
|
model: args.model,
|
|
92
104
|
aspectRatio,
|
|
105
|
+
resolution: args.resolution,
|
|
93
106
|
task: args.task,
|
|
94
107
|
previousInteractionId,
|
|
95
108
|
timeoutMs: args.timeout_ms,
|
|
@@ -104,6 +117,10 @@ export function registerVideoTools(server, client) {
|
|
|
104
117
|
// The RESOLVED ratio: omni takes only 16:9/9:16, so a caller who asked
|
|
105
118
|
// in words can see which of the two their request became.
|
|
106
119
|
const meta = reportShape({ model, interaction_id: r.id }, { ...args, aspect_ratio: aspectRatio });
|
|
120
|
+
// Echoed for the same reason the resolved ratio is: it changed what
|
|
121
|
+
// the caller was billed, so it has to be visible in the result.
|
|
122
|
+
if (args.resolution)
|
|
123
|
+
meta.resolution = args.resolution;
|
|
107
124
|
if (previousInteractionId)
|
|
108
125
|
meta.previous_interaction_id = previousInteractionId;
|
|
109
126
|
if (r.usage)
|
|
@@ -119,7 +136,7 @@ export function registerVideoTools(server, client) {
|
|
|
119
136
|
media,
|
|
120
137
|
base: r.videos.length > 1 ? `${slug}-${String(i + 1).padStart(2, '0')}` : slug,
|
|
121
138
|
}));
|
|
122
|
-
return emitMedia(named, 'video', { output_dir: args.output_dir, sink: client.mediaSink }, meta);
|
|
139
|
+
return emitMedia(named, 'video', { output_dir: args.output_dir, sink: client.mediaSink, sidecar: { prompt: args.prompt } }, meta);
|
|
123
140
|
});
|
|
124
141
|
});
|
|
125
142
|
}
|
package/dist/upload-url.js
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
* The problem these solve is the mirror image of `/media`: a signed GET lets a
|
|
6
6
|
* shell *download* generated media with no auth header, but uploading a
|
|
7
7
|
* reference photo still required `images_base64` (~14k tokens per JPEG, silently
|
|
8
|
-
* corrupting on a truncated read) or `POST /upload
|
|
9
|
-
*
|
|
10
|
-
* that gap: the authenticated MCP session mints it, and then a bare
|
|
8
|
+
* corrupting on a truncated read) or the Worker's `POST /upload`, which 401'd
|
|
9
|
+
* from any shell without the MCP session's bearer token and is gone entirely
|
|
10
|
+
* now that the Worker is. A signed PUT URL closes that gap: the authenticated MCP session mints it, and then a bare
|
|
11
11
|
* `curl -X PUT --data-binary @photo.jpg "<url>"` stores the bytes.
|
|
12
12
|
*
|
|
13
13
|
* Same HMAC secret as the media GET URLs, but a DIFFERENT payload shape, so the
|
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.
|
|
2
|
+
export const VERSION = '1.14.1'; // x-release-please-version
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chrischall/gemini-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.14.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>",
|
|
@@ -46,16 +46,16 @@
|
|
|
46
46
|
"typecheck": "tsc -p tsconfig.test.json"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@chrischall/mcp-utils": "^0.
|
|
50
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
49
|
+
"@chrischall/mcp-utils": "^0.26.1",
|
|
50
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
51
51
|
"dotenv": "^17.4.0",
|
|
52
|
-
"zod": "^4.4
|
|
52
|
+
"zod": "^4.5.4"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
55
|
"@types/node": "^26.0.0",
|
|
56
|
-
"@vitest/coverage-v8": "^
|
|
56
|
+
"@vitest/coverage-v8": "^5.0.0",
|
|
57
57
|
"esbuild": "^0.28.0",
|
|
58
58
|
"typescript": "^7.0.2",
|
|
59
|
-
"vitest": "^
|
|
59
|
+
"vitest": "^5.0.0"
|
|
60
60
|
}
|
|
61
61
|
}
|
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.
|
|
9
|
+
"version": "1.14.1",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "@chrischall/gemini-mcp",
|
|
14
|
-
"version": "1.
|
|
14
|
+
"version": "1.14.1",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|
|
@@ -118,6 +118,9 @@ not an error.
|
|
|
118
118
|
any number of calls until then.
|
|
119
119
|
- On stdio, an `images` path referenced **more than once in a session** is auto-uploaded to the
|
|
120
120
|
Files API (keyed on path + mtime + size) so the bytes stop being re-sent.
|
|
121
|
+
- `images_base64` is uploaded on the **first** sighting, not the second — the tokens are already
|
|
122
|
+
spent by then. The result reports it under `image_inputs.base64_uploaded[].file_uri`: pass that
|
|
123
|
+
to `images_file_uris` on the next call instead of pasting the bytes again.
|
|
121
124
|
|
|
122
125
|
### Files API
|
|
123
126
|
| Tool | Description |
|
|
@@ -145,8 +148,8 @@ curl -X POST https://<hosted deployment>/upload \
|
|
|
145
148
|
### Video & Music (preview — funded account)
|
|
146
149
|
| Tool | Description |
|
|
147
150
|
|------|-------------|
|
|
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
|
|
149
|
-
| `gemini_music_generate(prompt, model?,
|
|
151
|
+
| `gemini_video_generate(prompt, aspect_ratio?, resolution?, 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 (~10s) via the Gemini omni model: `text_to_video` (default), `image_to_video` / `reference_to_video` (supply reference image[s]), interpolation (pass first frame then last frame as `images`), or `edit` / `extend` (with `previous_interaction_id` / `continue_last`; extensions add ~3-10s each, to ~40s). `aspect_ratio` is `16:9` or `9:16`; `resolution` is `360p`/`720p` (default)/`1080p`/`4k` and is the cost lever — a 10s 360p clip runs about a third of 720p. Written to disk as **MP4**. Runs long — use `async: true` + `gemini_get_result`, or raise `timeout_ms`. |
|
|
152
|
+
| `gemini_music_generate(prompt, model?, images_url?, images_file_uris?, images?, images_base64?, from_clipboard?, 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 instrumental, default, $0.04), `lyria-3.5` (full-length song with vocals, $0.08) or `lyria-3-pro-preview` ($0.08). Output is MP3. **Single-turn** — there is no follow-up refinement, so put the whole brief in the prompt. Written to disk, or returned inline. |
|
|
150
153
|
|
|
151
154
|
### Async / idempotency (any generation tool)
|
|
152
155
|
| Tool | Description |
|
|
@@ -217,7 +220,8 @@ gemini_image_edit(
|
|
|
217
220
|
→ returns path to the edited image
|
|
218
221
|
```
|
|
219
222
|
`images_base64` is for bytes you actually have — a file you `Read`/encode, a URL
|
|
220
|
-
you fetch, or a `data:` URI the user pastes as **text**.
|
|
223
|
+
you fetch, or a `data:` URI the user pastes as **text**. Send them once: the result's
|
|
224
|
+
`image_inputs.base64_uploaded[].file_uri` is a `files/<id>` to reuse via `images_file_uris`.
|
|
221
225
|
|
|
222
226
|
**Iterate on ONE image conversationally (multi-turn):**
|
|
223
227
|
```
|
|
@@ -231,21 +235,25 @@ r3 = gemini_interact(input: "warmer lighting", continue_last: true)
|
|
|
231
235
|
```
|
|
232
236
|
Prefer this over re-running `gemini_image_edit` 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.
|
|
233
237
|
|
|
234
|
-
**Generate a video (
|
|
238
|
+
**Generate a video (runs long, use async):**
|
|
235
239
|
```
|
|
236
240
|
job = gemini_video_generate(prompt: "a paper boat sailing down a rain gutter, cinematic",
|
|
237
|
-
aspect_ratio: "16:9", async: true)
|
|
241
|
+
aspect_ratio: "16:9", resolution: "360p", async: true)
|
|
238
242
|
→ { job_id, status: "running" } (returns immediately — no host timeout)
|
|
239
243
|
gemini_get_result(job_id: job.job_id)
|
|
240
244
|
→ "running" until done, then the MP4 path on disk
|
|
245
|
+
# Draft at 360p, re-run the keeper at 1080p — video bills per output token, so the
|
|
246
|
+
# resolution is roughly the price.
|
|
241
247
|
# Animate a still instead: gemini_video_generate(prompt: "…", task: "image_to_video", images: ["/path/still.png"])
|
|
248
|
+
# Interpolate between two stills: images: ["/path/first.png", "/path/last.png"]
|
|
249
|
+
# Extend a clip: gemini_video_generate(prompt: "…", task: "extend", continue_last: true)
|
|
242
250
|
```
|
|
243
251
|
|
|
244
|
-
**Generate music
|
|
252
|
+
**Generate music:**
|
|
245
253
|
```
|
|
246
254
|
gemini_music_generate(prompt: "warm lo-fi hip hop, mellow Rhodes, vinyl crackle, 70bpm")
|
|
247
|
-
→
|
|
248
|
-
#
|
|
255
|
+
→ 30s MP3 on disk (lyria-3-clip-preview)
|
|
256
|
+
# Full-length song with vocals: gemini_music_generate(prompt: "…", model: "lyria-3.5")
|
|
249
257
|
```
|
|
250
258
|
**⚠️ Chat-pasted/attached images can't be fed to these tools directly.** A pasted
|
|
251
259
|
image reaches the assistant as a *vision* block — the assistant can SEE it but
|