@chrischall/gemini-mcp 0.9.0 → 1.0.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 +2 -2
- package/README.md +7 -5
- package/dist/bundle.js +285 -43
- package/dist/client.js +92 -35
- package/dist/images.js +35 -6
- package/dist/index.js +3 -1
- package/dist/models.js +5 -0
- package/dist/tools/generate.js +8 -8
- package/dist/tools/music.js +90 -0
- package/dist/tools/set.js +3 -3
- package/dist/tools/shared.js +26 -19
- package/dist/tools/video.js +92 -0
- package/dist/version.js +1 -1
- package/package.json +4 -4
- package/server.json +2 -2
- package/{SKILL.md → skills/gemini-mcp/SKILL.md} +44 -16
|
@@ -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": "1.0.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": "1.0.1",
|
|
19
19
|
"author": {
|
|
20
20
|
"name": "Chris Hall"
|
|
21
21
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gemini-mcp",
|
|
3
3
|
"displayName": "Gemini Images",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "1.0.1",
|
|
5
5
|
"description": "MCP server for Google Gemini image generation and editing — generate and edit images via the Gemini API",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Chris Hall",
|
|
@@ -19,6 +19,6 @@
|
|
|
19
19
|
"google-gemini",
|
|
20
20
|
"mcp"
|
|
21
21
|
],
|
|
22
|
-
"skills": "./
|
|
22
|
+
"skills": "./skills/",
|
|
23
23
|
"mcp": "./.mcp.json"
|
|
24
24
|
}
|
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
[](https://www.npmjs.com/package/@chrischall/gemini-mcp)
|
|
5
5
|
[](LICENSE)
|
|
6
6
|
|
|
7
|
-
MCP server for Google Gemini
|
|
7
|
+
MCP server for Google Gemini media generation. Exposes eight tools to Claude over stdio: list available models, generate/edit/compose images, generate a consistent set of images from a master prompt, multi-turn image refinement (Interactions API), **video** generation (omni), **music** generation (Lyria), and an async result poll for long generations. Output is written to disk by default (path returned) or returned inline as base64. Built on the Gemini v1beta API (`generativelanguage.googleapis.com`) using the Nano Banana / Nano Banana Pro (images), omni (video), and Lyria (music) model families.
|
|
8
8
|
|
|
9
9
|
Developed and maintained by AI (Claude Code).
|
|
10
10
|
|
|
@@ -42,10 +42,12 @@ progress), two guards make re-issuing safe and unnecessary:
|
|
|
42
42
|
| Tool | Description |
|
|
43
43
|
|------|-------------|
|
|
44
44
|
| `gemini_list_models` | List available Gemini image models and the current default |
|
|
45
|
-
| `
|
|
46
|
-
| `
|
|
47
|
-
| `
|
|
45
|
+
| `gemini_image_generate` | Generate image(s) from a text prompt |
|
|
46
|
+
| `gemini_image_edit` | One-off edits or multi-image composition with a text instruction (for a series of edits, use `gemini_interact`) |
|
|
47
|
+
| `gemini_image_set` | Generate a master image plus N consistent images referencing it |
|
|
48
48
|
| `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`) |
|
|
49
|
+
| `gemini_video_generate` | Generate a short video (text→video, image→video, or `edit`) via the Gemini omni model (preview); written to disk as MP4 |
|
|
50
|
+
| `gemini_music_generate` | Generate music from a text prompt via a Lyria model — `lyria-3-clip-preview` (~30s, default) or `lyria-3-pro-preview` (longer, WAV-capable); written to disk as MP3/WAV (preview) |
|
|
49
51
|
| `gemini_get_result` | Fetch an async generation started with `async: true` by its `job_id` (status `running` → `done` result). Lets a long generation outlive a host's `tools/call` timeout |
|
|
50
52
|
|
|
51
53
|
## Quick Start
|
|
@@ -64,4 +66,4 @@ progress), two guards make re-issuing safe and unnecessary:
|
|
|
64
66
|
}
|
|
65
67
|
```
|
|
66
68
|
|
|
67
|
-
See [SKILL.md](./SKILL.md) for full usage documentation.
|
|
69
|
+
See [SKILL.md](./skills/gemini-mcp/SKILL.md) for full usage documentation.
|
package/dist/bundle.js
CHANGED
|
@@ -32606,7 +32606,7 @@ var init_clipboard = __esm({
|
|
|
32606
32606
|
init_dist();
|
|
32607
32607
|
|
|
32608
32608
|
// src/version.ts
|
|
32609
|
-
var VERSION = "0.
|
|
32609
|
+
var VERSION = "1.0.1";
|
|
32610
32610
|
|
|
32611
32611
|
// src/tools/models.ts
|
|
32612
32612
|
init_dist();
|
|
@@ -32618,6 +32618,8 @@ import { fileURLToPath } from "node:url";
|
|
|
32618
32618
|
|
|
32619
32619
|
// src/models.ts
|
|
32620
32620
|
var DEFAULT_IMAGE_MODEL = "gemini-3.1-flash-image";
|
|
32621
|
+
var DEFAULT_VIDEO_MODEL = "gemini-omni-flash-preview";
|
|
32622
|
+
var DEFAULT_MUSIC_MODEL = "lyria-3-clip-preview";
|
|
32621
32623
|
function resolveModel(perCall, envOverride) {
|
|
32622
32624
|
return perCall?.trim() || envOverride?.trim() || DEFAULT_IMAGE_MODEL;
|
|
32623
32625
|
}
|
|
@@ -32881,14 +32883,66 @@ var GeminiClient = class {
|
|
|
32881
32883
|
if (opts.searchTypes?.length) searchTool.search_types = opts.searchTypes;
|
|
32882
32884
|
body.tools = [searchTool];
|
|
32883
32885
|
}
|
|
32886
|
+
const data = await this.postInteraction(body, opts);
|
|
32887
|
+
const { images, text, grounding } = this.extractInteraction(data, opts.searchTypes);
|
|
32888
|
+
if (images.length === 0) {
|
|
32889
|
+
throw new McpToolError("Gemini returned no image", {
|
|
32890
|
+
hint: "The request may have been blocked by safety filters \u2014 try rephrasing the prompt."
|
|
32891
|
+
});
|
|
32892
|
+
}
|
|
32893
|
+
return { id: data.id, images, text, grounding };
|
|
32894
|
+
}
|
|
32895
|
+
/**
|
|
32896
|
+
* Generate video via the omni model (Interactions API, inline delivery). Same
|
|
32897
|
+
* plumbing as {@link interact} — text/image input parts, a `video`
|
|
32898
|
+
* response_format, optional `video_config.task`, and `previous_interaction_id`
|
|
32899
|
+
* for edits. Preview model: shapes are docs-derived; parsing stays tolerant.
|
|
32900
|
+
*/
|
|
32901
|
+
async generateVideo(opts) {
|
|
32902
|
+
const model = opts.model ?? DEFAULT_VIDEO_MODEL;
|
|
32903
|
+
const inputParts = [{ type: "text", text: opts.input }];
|
|
32904
|
+
for (const img of opts.images ?? []) inputParts.push({ type: "image", mime_type: img.mimeType, data: img.base64 });
|
|
32905
|
+
const responseFormat = { type: "video", delivery: "inline" };
|
|
32906
|
+
if (opts.aspectRatio) responseFormat.aspect_ratio = opts.aspectRatio;
|
|
32907
|
+
const body = { model, input: inputParts, response_format: responseFormat };
|
|
32908
|
+
if (opts.task) body.generation_config = { video_config: { task: opts.task } };
|
|
32909
|
+
if (opts.previousInteractionId !== void 0) body.previous_interaction_id = opts.previousInteractionId;
|
|
32910
|
+
const data = await this.postInteraction(body, opts);
|
|
32911
|
+
const { videos, text } = this.extractInteraction(data);
|
|
32912
|
+
if (videos.length === 0) {
|
|
32913
|
+
throw new McpToolError("Gemini returned no video", {
|
|
32914
|
+
hint: "The request may have been blocked by a safety filter, or the clip exceeded the ~4MB inline limit \u2014 try a shorter/simpler prompt or a different aspect ratio."
|
|
32915
|
+
});
|
|
32916
|
+
}
|
|
32917
|
+
return { id: data.id, videos, text };
|
|
32918
|
+
}
|
|
32919
|
+
/**
|
|
32920
|
+
* Generate music via a Lyria model (Interactions API, inline audio). Reuses
|
|
32921
|
+
* the interact plumbing with an `audio` response_format. Preview model.
|
|
32922
|
+
*/
|
|
32923
|
+
async generateMusic(opts) {
|
|
32924
|
+
const model = opts.model ?? DEFAULT_MUSIC_MODEL;
|
|
32925
|
+
const inputParts = [{ type: "text", text: opts.input }];
|
|
32926
|
+
for (const img of opts.images ?? []) inputParts.push({ type: "image", mime_type: img.mimeType, data: img.base64 });
|
|
32927
|
+
const responseFormat = { type: "audio" };
|
|
32928
|
+
if (opts.audioFormat) responseFormat.audio_format = opts.audioFormat;
|
|
32929
|
+
const body = { model, input: inputParts, response_format: responseFormat };
|
|
32930
|
+
if (opts.previousInteractionId !== void 0) body.previous_interaction_id = opts.previousInteractionId;
|
|
32931
|
+
const data = await this.postInteraction(body, opts);
|
|
32932
|
+
const { audios, text } = this.extractInteraction(data);
|
|
32933
|
+
if (audios.length === 0) {
|
|
32934
|
+
throw new McpToolError("Gemini returned no audio", {
|
|
32935
|
+
hint: "The request may have been blocked by a safety filter \u2014 try rephrasing the prompt."
|
|
32936
|
+
});
|
|
32937
|
+
}
|
|
32938
|
+
return { id: data.id, audios, text };
|
|
32939
|
+
}
|
|
32940
|
+
/** POST `/interactions` with the chained-404 retry. Shared by all media. */
|
|
32941
|
+
async postInteraction(body, opts) {
|
|
32884
32942
|
const { interactionsApi } = this.apisFor(resolveTimeoutMs(opts.timeoutMs, opts.imageSize));
|
|
32885
|
-
let data;
|
|
32886
32943
|
for (let attempt = 0; ; attempt++) {
|
|
32887
32944
|
try {
|
|
32888
|
-
|
|
32889
|
-
body
|
|
32890
|
-
});
|
|
32891
|
-
break;
|
|
32945
|
+
return await interactionsApi.fetchJson("POST", "/interactions", { body });
|
|
32892
32946
|
} catch (err) {
|
|
32893
32947
|
if (!(opts.previousInteractionId && err instanceof ApiError && err.status === 404)) throw err;
|
|
32894
32948
|
if (attempt < CHAIN_404_RETRIES) {
|
|
@@ -32898,13 +32952,23 @@ var GeminiClient = class {
|
|
|
32898
32952
|
throw new McpToolError(
|
|
32899
32953
|
`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.`,
|
|
32900
32954
|
{
|
|
32901
|
-
hint: "Retry once more in a few seconds; if it keeps failing, start a new chain
|
|
32955
|
+
hint: "Retry once more in a few seconds; if it keeps failing, start a new chain by re-attaching the last output as an input plus the same instruction, then chain the NEW interaction id.",
|
|
32902
32956
|
cause: err
|
|
32903
32957
|
}
|
|
32904
32958
|
);
|
|
32905
32959
|
}
|
|
32906
32960
|
}
|
|
32961
|
+
}
|
|
32962
|
+
/**
|
|
32963
|
+
* Collect `model_output` media (image/video/audio) + text + grounding from an
|
|
32964
|
+
* interactions response. Only the `model_output` step is surfaced — `thought`
|
|
32965
|
+
* steps (internal reasoning / draft images) are dropped on purpose so they
|
|
32966
|
+
* can't leak into text or pollute the outputs. Tolerant of snake/camel MIME.
|
|
32967
|
+
*/
|
|
32968
|
+
extractInteraction(data, searchTypes) {
|
|
32907
32969
|
const images = [];
|
|
32970
|
+
const videos = [];
|
|
32971
|
+
const audios = [];
|
|
32908
32972
|
const textParts = [];
|
|
32909
32973
|
const searchQueries = [];
|
|
32910
32974
|
const searchSuggestions = /* @__PURE__ */ new Set();
|
|
@@ -32914,7 +32978,7 @@ var GeminiClient = class {
|
|
|
32914
32978
|
continue;
|
|
32915
32979
|
}
|
|
32916
32980
|
if (step.type === "google_search_result") {
|
|
32917
|
-
if (
|
|
32981
|
+
if (searchTypes?.includes("image_search")) {
|
|
32918
32982
|
for (const r of step.result ?? []) if (r.search_suggestions?.trim()) searchSuggestions.add(r.search_suggestions);
|
|
32919
32983
|
}
|
|
32920
32984
|
continue;
|
|
@@ -32922,27 +32986,24 @@ var GeminiClient = class {
|
|
|
32922
32986
|
if (step.type !== "model_output") continue;
|
|
32923
32987
|
for (const parts of [step.content ?? [], step.summary ?? []]) {
|
|
32924
32988
|
for (const part of parts) {
|
|
32925
|
-
|
|
32926
|
-
|
|
32989
|
+
const mime = part.mime_type ?? part.mimeType;
|
|
32990
|
+
if (part.data) {
|
|
32991
|
+
if (part.type === "image") images.push({ base64: part.data, mimeType: mime ?? "image/jpeg" });
|
|
32992
|
+
else if (part.type === "video") videos.push({ base64: part.data, mimeType: mime ?? "video/mp4" });
|
|
32993
|
+
else if (part.type === "audio") audios.push({ base64: part.data, mimeType: mime ?? "audio/mpeg" });
|
|
32927
32994
|
} else if (part.type === "text" && part.text?.trim()) {
|
|
32928
32995
|
textParts.push(part.text);
|
|
32929
32996
|
}
|
|
32930
32997
|
}
|
|
32931
32998
|
}
|
|
32932
32999
|
}
|
|
32933
|
-
if (images.length === 0) {
|
|
32934
|
-
throw new McpToolError("Gemini returned no image", {
|
|
32935
|
-
hint: "The request may have been blocked by safety filters \u2014 try rephrasing the prompt."
|
|
32936
|
-
});
|
|
32937
|
-
}
|
|
32938
|
-
const resultText = textParts.join("\n") || void 0;
|
|
32939
33000
|
let grounding;
|
|
32940
33001
|
if (searchQueries.length > 0 || searchSuggestions.size > 0) {
|
|
32941
33002
|
grounding = {};
|
|
32942
33003
|
if (searchQueries.length > 0) grounding.queries = searchQueries;
|
|
32943
33004
|
if (searchSuggestions.size > 0) grounding.search_suggestions = [...searchSuggestions];
|
|
32944
33005
|
}
|
|
32945
|
-
return {
|
|
33006
|
+
return { images, videos, audios, text: textParts.join("\n") || void 0, grounding };
|
|
32946
33007
|
}
|
|
32947
33008
|
};
|
|
32948
33009
|
var client = new GeminiClient();
|
|
@@ -33004,10 +33065,22 @@ function sniffMimeBytes(buf) {
|
|
|
33004
33065
|
}
|
|
33005
33066
|
return "image/png";
|
|
33006
33067
|
}
|
|
33007
|
-
|
|
33068
|
+
function mediaExt(mimeType) {
|
|
33069
|
+
const m = mimeType.toLowerCase();
|
|
33070
|
+
if (m.includes("jpeg")) return "jpg";
|
|
33071
|
+
if (m.includes("webp")) return "webp";
|
|
33072
|
+
if (m.includes("png")) return "png";
|
|
33073
|
+
if (m.includes("mp4")) return "mp4";
|
|
33074
|
+
if (m.includes("quicktime")) return "mov";
|
|
33075
|
+
if (m.includes("webm")) return "webm";
|
|
33076
|
+
if (m.includes("wav")) return "wav";
|
|
33077
|
+
if (m.includes("mpeg") || m.includes("mp3")) return "mp3";
|
|
33078
|
+
if (m.includes("ogg")) return "ogg";
|
|
33079
|
+
return m.split("/")[1]?.split(";")[0]?.replace(/^x-/, "") || "bin";
|
|
33080
|
+
}
|
|
33081
|
+
async function writeMedia(dir, base, base643, mimeType) {
|
|
33008
33082
|
await mkdir(dir, { recursive: true });
|
|
33009
|
-
const
|
|
33010
|
-
const path = await uniquePath(dir, base, ext);
|
|
33083
|
+
const path = await uniquePath(dir, base, mediaExt(mimeType));
|
|
33011
33084
|
await writeFile(path, Buffer.from(base643, "base64"));
|
|
33012
33085
|
return resolve(path);
|
|
33013
33086
|
}
|
|
@@ -33104,7 +33177,7 @@ async function gatherImageInputs(opts) {
|
|
|
33104
33177
|
return [...clipboardImages, ...fileImages];
|
|
33105
33178
|
}
|
|
33106
33179
|
function baseName(name) {
|
|
33107
|
-
const stripped = name.replace(/\.(png|jpe?g|webp)$/i, "");
|
|
33180
|
+
const stripped = name.replace(/\.(png|jpe?g|webp|mp4|mov|webm|mp3|wav|ogg)$/i, "");
|
|
33108
33181
|
const safe = stripped.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
|
|
33109
33182
|
return safe || "image";
|
|
33110
33183
|
}
|
|
@@ -33214,21 +33287,24 @@ function buildMeta(model, seed, opts) {
|
|
|
33214
33287
|
if (opts.image_size) meta3.image_size = opts.image_size;
|
|
33215
33288
|
return meta3;
|
|
33216
33289
|
}
|
|
33217
|
-
async function
|
|
33218
|
-
|
|
33219
|
-
|
|
33290
|
+
async function emitMedia(named, kind, opts, meta3, onWritten) {
|
|
33291
|
+
const inline = opts.inline && kind !== "video";
|
|
33292
|
+
if (inline) {
|
|
33293
|
+
const blocks = named.map((n) => ({ type: kind, data: n.media.base64, mimeType: n.media.mimeType }));
|
|
33220
33294
|
if (meta3 && Object.keys(meta3).length > 0) {
|
|
33221
|
-
return {
|
|
33222
|
-
content: [{ type: "text", text: JSON.stringify(meta3, null, 2) }, ...imageBlocks]
|
|
33223
|
-
};
|
|
33295
|
+
return { content: [{ type: "text", text: JSON.stringify(meta3, null, 2) }, ...blocks] };
|
|
33224
33296
|
}
|
|
33225
|
-
return { content:
|
|
33297
|
+
return { content: blocks };
|
|
33226
33298
|
}
|
|
33227
33299
|
const dir = resolveOutputDir(opts.output_dir);
|
|
33228
|
-
const
|
|
33229
|
-
for (const n of named)
|
|
33230
|
-
await onWritten?.(
|
|
33231
|
-
|
|
33300
|
+
const paths = [];
|
|
33301
|
+
for (const n of named) paths.push(await writeMedia(dir, n.base, n.media.base64, n.media.mimeType));
|
|
33302
|
+
await onWritten?.(paths);
|
|
33303
|
+
const note = opts.inline && kind === "video" ? { inline_unsupported: "video has no MCP inline content type \u2014 written to disk instead" } : {};
|
|
33304
|
+
return textResult({ [`${kind}s`]: paths, ...note, ...meta3 });
|
|
33305
|
+
}
|
|
33306
|
+
async function emit(named, opts, meta3, onWritten) {
|
|
33307
|
+
return emitMedia(named.map((n) => ({ media: n.image, base: n.base })), "image", opts, meta3, onWritten);
|
|
33232
33308
|
}
|
|
33233
33309
|
|
|
33234
33310
|
// src/jobs.ts
|
|
@@ -33371,7 +33447,7 @@ var SEND_LOCAL_ACTION = "Send local file input(s) to the Gemini API";
|
|
|
33371
33447
|
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).";
|
|
33372
33448
|
function registerGenerateTools(server) {
|
|
33373
33449
|
server.registerTool(
|
|
33374
|
-
"
|
|
33450
|
+
"gemini_image_generate",
|
|
33375
33451
|
{
|
|
33376
33452
|
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.",
|
|
33377
33453
|
annotations: { readOnlyHint: false, openWorldHint: true },
|
|
@@ -33392,7 +33468,7 @@ function registerGenerateTools(server) {
|
|
|
33392
33468
|
if (gate) return gate;
|
|
33393
33469
|
const count = args.count ?? 1;
|
|
33394
33470
|
const model = resolveModel(args.model, readEnvVar("GEMINI_IMAGE_MODEL"));
|
|
33395
|
-
const fingerprint = fingerprintRequest("
|
|
33471
|
+
const fingerprint = fingerprintRequest("gemini_image_generate", {
|
|
33396
33472
|
model,
|
|
33397
33473
|
prompt: args.prompt,
|
|
33398
33474
|
aspect_ratio: args.aspect_ratio,
|
|
@@ -33406,7 +33482,7 @@ function registerGenerateTools(server) {
|
|
|
33406
33482
|
video_url: args.video_url,
|
|
33407
33483
|
video_path: args.video_path
|
|
33408
33484
|
});
|
|
33409
|
-
return dispatch({ toolName: "
|
|
33485
|
+
return dispatch({ toolName: "gemini_image_generate", fingerprint, idempotencyKey: args.idempotency_key, async: args.async }, async () => {
|
|
33410
33486
|
const seed = pickSeed(args.seed);
|
|
33411
33487
|
const slug = args.filename ? baseName(args.filename) : slugify2(args.prompt);
|
|
33412
33488
|
const refInputs = await gatherImageInputs({ images: args.images, images_base64: args.images_base64, from_clipboard: args.from_clipboard });
|
|
@@ -33447,9 +33523,9 @@ function registerGenerateTools(server) {
|
|
|
33447
33523
|
}
|
|
33448
33524
|
);
|
|
33449
33525
|
server.registerTool(
|
|
33450
|
-
"
|
|
33526
|
+
"gemini_image_edit",
|
|
33451
33527
|
{
|
|
33452
|
-
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
|
|
33528
|
+
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_image_edit 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.",
|
|
33453
33529
|
annotations: { readOnlyHint: false, openWorldHint: true },
|
|
33454
33530
|
inputSchema: {
|
|
33455
33531
|
prompt: external_exports.string().min(1).describe("Instruction describing the edit or composition"),
|
|
@@ -33469,7 +33545,7 @@ function registerGenerateTools(server) {
|
|
|
33469
33545
|
const gate = await previewLocalInputsUnlessConfirmed(args.confirm, SEND_LOCAL_ACTION, GENERATE_ENDPOINT, args.images);
|
|
33470
33546
|
if (gate) return gate;
|
|
33471
33547
|
const model = resolveModel(args.model, readEnvVar("GEMINI_IMAGE_MODEL"));
|
|
33472
|
-
const fingerprint = fingerprintRequest("
|
|
33548
|
+
const fingerprint = fingerprintRequest("gemini_image_edit", {
|
|
33473
33549
|
model,
|
|
33474
33550
|
prompt: args.prompt,
|
|
33475
33551
|
aspect_ratio: args.aspect_ratio,
|
|
@@ -33480,7 +33556,7 @@ function registerGenerateTools(server) {
|
|
|
33480
33556
|
images_base64: args.images_base64,
|
|
33481
33557
|
from_clipboard: args.from_clipboard
|
|
33482
33558
|
});
|
|
33483
|
-
return dispatch({ toolName: "
|
|
33559
|
+
return dispatch({ toolName: "gemini_image_edit", fingerprint, idempotencyKey: args.idempotency_key, async: args.async }, async () => {
|
|
33484
33560
|
const seed = pickSeed(args.seed);
|
|
33485
33561
|
const slug = args.filename ? baseName(args.filename) : slugify2(args.prompt);
|
|
33486
33562
|
const inputs = await gatherImageInputs({ images: args.images, images_base64: args.images_base64, from_clipboard: args.from_clipboard });
|
|
@@ -33513,7 +33589,7 @@ init_zod();
|
|
|
33513
33589
|
init_dist();
|
|
33514
33590
|
function registerSetTools(server) {
|
|
33515
33591
|
server.registerTool(
|
|
33516
|
-
"
|
|
33592
|
+
"gemini_image_set",
|
|
33517
33593
|
{
|
|
33518
33594
|
description: "Generate a consistent SET of images: a master image from master_prompt, then one image per scene that references the master so the subject/style stays consistent. Provide `scenes` (explicit per-image prompts) OR `count` (variations of the master).",
|
|
33519
33595
|
annotations: { readOnlyHint: false, openWorldHint: true },
|
|
@@ -33536,7 +33612,7 @@ function registerSetTools(server) {
|
|
|
33536
33612
|
const gate = await previewLocalInputsUnlessConfirmed(args.confirm, "Send local master image input(s) to the Gemini API", "/v1beta/models/{model}:generateContent", args.master_images);
|
|
33537
33613
|
if (gate) return gate;
|
|
33538
33614
|
const model = resolveModel(args.model, readEnvVar("GEMINI_IMAGE_MODEL"));
|
|
33539
|
-
const fingerprint = fingerprintRequest("
|
|
33615
|
+
const fingerprint = fingerprintRequest("gemini_image_set", {
|
|
33540
33616
|
model,
|
|
33541
33617
|
master_prompt: args.master_prompt,
|
|
33542
33618
|
scenes: args.scenes,
|
|
@@ -33550,7 +33626,7 @@ function registerSetTools(server) {
|
|
|
33550
33626
|
master_images_base64: args.master_images_base64,
|
|
33551
33627
|
from_clipboard: args.from_clipboard
|
|
33552
33628
|
});
|
|
33553
|
-
return dispatch({ toolName: "
|
|
33629
|
+
return dispatch({ toolName: "gemini_image_set", fingerprint, idempotencyKey: args.idempotency_key, async: args.async }, async () => {
|
|
33554
33630
|
const seed = pickSeed(args.seed);
|
|
33555
33631
|
const cfg = { model: args.model, aspectRatio: args.aspect_ratio, imageSize: args.image_size, thinkingLevel: args.thinking_level, googleSearch: args.google_search, timeoutMs: args.timeout_ms };
|
|
33556
33632
|
const slug = args.basename ? baseName(args.basename) : slugify2(args.master_prompt);
|
|
@@ -33723,6 +33799,172 @@ function registerInteractTools(server) {
|
|
|
33723
33799
|
);
|
|
33724
33800
|
}
|
|
33725
33801
|
|
|
33802
|
+
// src/tools/video.ts
|
|
33803
|
+
init_zod();
|
|
33804
|
+
init_dist();
|
|
33805
|
+
var VIDEO_ASPECT_RATIOS = ["16:9", "9:16"];
|
|
33806
|
+
var VIDEO_TASKS = ["text_to_video", "image_to_video", "reference_to_video", "edit"];
|
|
33807
|
+
var lastVideoInteractionId;
|
|
33808
|
+
function registerVideoTools(server) {
|
|
33809
|
+
server.registerTool(
|
|
33810
|
+
"gemini_video_generate",
|
|
33811
|
+
{
|
|
33812
|
+
description: 'Generate a short video via the Gemini omni model (preview): text\u2192video, image\u2192video / reference\u2192video (supply reference image[s]), or edit a prior video (task: "edit" + previous_interaction_id / continue_last). Output is written to disk as MP4 (video has no inline MCP block). Video runs long \u2014 use `async: true` to get a job_id immediately and poll gemini_get_result, or raise `timeout_ms`. Preview model: needs a funded account.',
|
|
33813
|
+
annotations: { readOnlyHint: false, openWorldHint: true },
|
|
33814
|
+
inputSchema: {
|
|
33815
|
+
prompt: external_exports.string().min(1).describe("Description of the video to generate (or the edit instruction when task=edit)"),
|
|
33816
|
+
aspect_ratio: external_exports.enum(VIDEO_ASPECT_RATIOS).optional().describe("Output aspect ratio (omni: 16:9 or 9:16)"),
|
|
33817
|
+
task: external_exports.enum(VIDEO_TASKS).optional().describe("text_to_video (default), image_to_video / reference_to_video (need image input), or edit (needs previous_interaction_id)"),
|
|
33818
|
+
images: external_exports.array(external_exports.string().min(1)).optional().describe("Reference image path(s) for image_to_video / reference_to_video"),
|
|
33819
|
+
images_base64: external_exports.array(external_exports.string().min(1)).optional().describe("Reference images as base64 strings or data URIs"),
|
|
33820
|
+
from_clipboard: external_exports.boolean().optional().describe("Use the image currently on the macOS clipboard as a reference"),
|
|
33821
|
+
filename: external_exports.string().optional().describe("Base filename for the output video (extension stripped; default: slugified prompt)"),
|
|
33822
|
+
output_dir: external_exports.string().optional().describe("Directory to write the video to (default: $GEMINI_OUTPUT_DIR or cwd)"),
|
|
33823
|
+
model: external_exports.string().regex(/^[\w.-]+$/, 'must be a bare model id (letters, digits, ".", "_", "-")').optional().describe(`Model id override (default: ${DEFAULT_VIDEO_MODEL})`),
|
|
33824
|
+
previous_interaction_id: external_exports.string().optional().describe('Interaction id to edit/continue (with task: "edit")'),
|
|
33825
|
+
continue_last: external_exports.boolean().optional().describe("Continue from the most recent video interaction this server created (explicit previous_interaction_id wins)"),
|
|
33826
|
+
timeout_ms: timeoutMsSchema,
|
|
33827
|
+
idempotency_key: idempotencyKeySchema,
|
|
33828
|
+
async: asyncSchema,
|
|
33829
|
+
confirm: schemaConfirm
|
|
33830
|
+
}
|
|
33831
|
+
},
|
|
33832
|
+
async (args, extra) => {
|
|
33833
|
+
let previousInteractionId = args.previous_interaction_id;
|
|
33834
|
+
if (!previousInteractionId && args.continue_last) {
|
|
33835
|
+
if (!lastVideoInteractionId) {
|
|
33836
|
+
throw new McpToolError("No previous video interaction to continue in this session.", {
|
|
33837
|
+
hint: "Generate a video first, or pass an explicit previous_interaction_id."
|
|
33838
|
+
});
|
|
33839
|
+
}
|
|
33840
|
+
previousInteractionId = lastVideoInteractionId;
|
|
33841
|
+
}
|
|
33842
|
+
const gate = await previewLocalInputsUnlessConfirmed(args.confirm, "Send local image input(s) to the Gemini omni API", "/v1beta/interactions", args.images);
|
|
33843
|
+
if (gate) return gate;
|
|
33844
|
+
const model = args.model?.trim() || DEFAULT_VIDEO_MODEL;
|
|
33845
|
+
const fingerprint = fingerprintRequest("gemini_video_generate", {
|
|
33846
|
+
model,
|
|
33847
|
+
prompt: args.prompt,
|
|
33848
|
+
aspect_ratio: args.aspect_ratio,
|
|
33849
|
+
task: args.task,
|
|
33850
|
+
images: args.images,
|
|
33851
|
+
images_base64: args.images_base64,
|
|
33852
|
+
from_clipboard: args.from_clipboard,
|
|
33853
|
+
previous_interaction_id: previousInteractionId
|
|
33854
|
+
});
|
|
33855
|
+
return dispatch({ toolName: "gemini_video_generate", fingerprint, idempotencyKey: args.idempotency_key, async: args.async }, async () => {
|
|
33856
|
+
const inputs = await gatherImageInputs({ images: args.images, images_base64: args.images_base64, from_clipboard: args.from_clipboard });
|
|
33857
|
+
const r = await withProgressHeartbeat(extra, `Generating video (${model})`, () => client.generateVideo({
|
|
33858
|
+
input: args.prompt,
|
|
33859
|
+
images: inputs.length ? inputs : void 0,
|
|
33860
|
+
model: args.model,
|
|
33861
|
+
aspectRatio: args.aspect_ratio,
|
|
33862
|
+
task: args.task,
|
|
33863
|
+
previousInteractionId,
|
|
33864
|
+
timeoutMs: args.timeout_ms
|
|
33865
|
+
}));
|
|
33866
|
+
lastVideoInteractionId = r.id;
|
|
33867
|
+
const meta3 = { model, interaction_id: r.id };
|
|
33868
|
+
if (previousInteractionId) meta3.previous_interaction_id = previousInteractionId;
|
|
33869
|
+
if (r.text) meta3.text = r.text;
|
|
33870
|
+
meta3.hint = `To edit this video, call gemini_video_generate again with task: "edit" and previous_interaction_id: "${r.id}" (or continue_last: true).`;
|
|
33871
|
+
const slug = args.filename ? baseName(args.filename) : slugify2(args.prompt);
|
|
33872
|
+
const named = r.videos.map((media, i) => ({
|
|
33873
|
+
media,
|
|
33874
|
+
base: r.videos.length > 1 ? `${slug}-${String(i + 1).padStart(2, "0")}` : slug
|
|
33875
|
+
}));
|
|
33876
|
+
return emitMedia(named, "video", { output_dir: args.output_dir }, meta3);
|
|
33877
|
+
});
|
|
33878
|
+
}
|
|
33879
|
+
);
|
|
33880
|
+
}
|
|
33881
|
+
|
|
33882
|
+
// src/tools/music.ts
|
|
33883
|
+
init_zod();
|
|
33884
|
+
init_dist();
|
|
33885
|
+
var MUSIC_MODELS = ["lyria-3-clip-preview", "lyria-3-pro-preview"];
|
|
33886
|
+
var AUDIO_FORMATS = ["mp3", "wav"];
|
|
33887
|
+
function isProModel(model) {
|
|
33888
|
+
return /pro/i.test(model);
|
|
33889
|
+
}
|
|
33890
|
+
var lastMusicInteractionId;
|
|
33891
|
+
function registerMusicTools(server) {
|
|
33892
|
+
server.registerTool(
|
|
33893
|
+
"gemini_music_generate",
|
|
33894
|
+
{
|
|
33895
|
+
description: "Generate music from a text prompt (mood, genre, instruments, structure, or lyrics inline) via a Lyria model (preview): lyria-3-clip-preview (~30s clips, default) or lyria-3-pro-preview (longer, WAV-capable). Written to disk as MP3/WAV (or returned inline). Runs long \u2014 use `async: true` + gemini_get_result, or raise `timeout_ms`. Preview model: needs a funded account.",
|
|
33896
|
+
annotations: { readOnlyHint: false, openWorldHint: true },
|
|
33897
|
+
inputSchema: {
|
|
33898
|
+
prompt: external_exports.string().min(1).describe("Description of the music: mood, genre, instruments, tempo, structure, or lyrics"),
|
|
33899
|
+
model: external_exports.enum(MUSIC_MODELS).optional().describe(`Lyria model (default: ${DEFAULT_MUSIC_MODEL}). Pro is longer-form and supports WAV.`),
|
|
33900
|
+
audio_format: external_exports.enum(AUDIO_FORMATS).optional().describe("Output format (default mp3). wav is lyria-3-pro-preview-only."),
|
|
33901
|
+
images: external_exports.array(external_exports.string().min(1)).optional().describe("Optional reference image path(s) to condition the music"),
|
|
33902
|
+
images_base64: external_exports.array(external_exports.string().min(1)).optional().describe("Reference images as base64 strings or data URIs"),
|
|
33903
|
+
from_clipboard: external_exports.boolean().optional().describe("Use the image currently on the macOS clipboard as a reference"),
|
|
33904
|
+
filename: external_exports.string().optional().describe("Base filename for the output audio (extension stripped; default: slugified prompt)"),
|
|
33905
|
+
output_dir: external_exports.string().optional().describe("Directory to write audio to (default: $GEMINI_OUTPUT_DIR or cwd)"),
|
|
33906
|
+
inline: external_exports.boolean().optional().describe("Return base64 audio inline instead of writing to disk"),
|
|
33907
|
+
previous_interaction_id: external_exports.string().optional().describe("Interaction id to continue from"),
|
|
33908
|
+
continue_last: external_exports.boolean().optional().describe("Continue from the most recent music interaction this server created (explicit previous_interaction_id wins)"),
|
|
33909
|
+
timeout_ms: timeoutMsSchema,
|
|
33910
|
+
idempotency_key: idempotencyKeySchema,
|
|
33911
|
+
async: asyncSchema,
|
|
33912
|
+
confirm: schemaConfirm
|
|
33913
|
+
}
|
|
33914
|
+
},
|
|
33915
|
+
async (args, extra) => {
|
|
33916
|
+
const model = args.model ?? DEFAULT_MUSIC_MODEL;
|
|
33917
|
+
if (args.audio_format === "wav" && !isProModel(model)) {
|
|
33918
|
+
throw new McpToolError(`WAV output requires a Pro model; ${model} only produces MP3.`, {
|
|
33919
|
+
hint: 'Use model: "lyria-3-pro-preview" for WAV, or drop audio_format to get MP3.'
|
|
33920
|
+
});
|
|
33921
|
+
}
|
|
33922
|
+
let previousInteractionId = args.previous_interaction_id;
|
|
33923
|
+
if (!previousInteractionId && args.continue_last) {
|
|
33924
|
+
if (!lastMusicInteractionId) {
|
|
33925
|
+
throw new McpToolError("No previous music interaction to continue in this session.", {
|
|
33926
|
+
hint: "Generate music first, or pass an explicit previous_interaction_id."
|
|
33927
|
+
});
|
|
33928
|
+
}
|
|
33929
|
+
previousInteractionId = lastMusicInteractionId;
|
|
33930
|
+
}
|
|
33931
|
+
const gate = await previewLocalInputsUnlessConfirmed(args.confirm, "Send local image input(s) to the Gemini Lyria API", "/v1beta/interactions", args.images);
|
|
33932
|
+
if (gate) return gate;
|
|
33933
|
+
const fingerprint = fingerprintRequest("gemini_music_generate", {
|
|
33934
|
+
model,
|
|
33935
|
+
prompt: args.prompt,
|
|
33936
|
+
audio_format: args.audio_format,
|
|
33937
|
+
images: args.images,
|
|
33938
|
+
images_base64: args.images_base64,
|
|
33939
|
+
from_clipboard: args.from_clipboard,
|
|
33940
|
+
previous_interaction_id: previousInteractionId
|
|
33941
|
+
});
|
|
33942
|
+
return dispatch({ toolName: "gemini_music_generate", fingerprint, idempotencyKey: args.idempotency_key, async: args.async }, async () => {
|
|
33943
|
+
const inputs = await gatherImageInputs({ images: args.images, images_base64: args.images_base64, from_clipboard: args.from_clipboard });
|
|
33944
|
+
const r = await withProgressHeartbeat(extra, `Generating music (${model})`, () => client.generateMusic({
|
|
33945
|
+
input: args.prompt,
|
|
33946
|
+
images: inputs.length ? inputs : void 0,
|
|
33947
|
+
model: args.model,
|
|
33948
|
+
audioFormat: args.audio_format,
|
|
33949
|
+
previousInteractionId,
|
|
33950
|
+
timeoutMs: args.timeout_ms
|
|
33951
|
+
}));
|
|
33952
|
+
lastMusicInteractionId = r.id;
|
|
33953
|
+
const meta3 = { model, interaction_id: r.id };
|
|
33954
|
+
if (previousInteractionId) meta3.previous_interaction_id = previousInteractionId;
|
|
33955
|
+
if (r.text) meta3.text = r.text;
|
|
33956
|
+
meta3.hint = `To continue this track, call gemini_music_generate again with previous_interaction_id: "${r.id}" (or continue_last: true).`;
|
|
33957
|
+
const slug = args.filename ? baseName(args.filename) : slugify2(args.prompt);
|
|
33958
|
+
const named = r.audios.map((media, i) => ({
|
|
33959
|
+
media,
|
|
33960
|
+
base: r.audios.length > 1 ? `${slug}-${String(i + 1).padStart(2, "0")}` : slug
|
|
33961
|
+
}));
|
|
33962
|
+
return emitMedia(named, "audio", { inline: args.inline, output_dir: args.output_dir }, meta3);
|
|
33963
|
+
});
|
|
33964
|
+
}
|
|
33965
|
+
);
|
|
33966
|
+
}
|
|
33967
|
+
|
|
33726
33968
|
// src/tools/jobs.ts
|
|
33727
33969
|
init_zod();
|
|
33728
33970
|
function registerJobTools(server) {
|
|
@@ -33744,5 +33986,5 @@ await runMcp({
|
|
|
33744
33986
|
name: "gemini-mcp",
|
|
33745
33987
|
version: VERSION,
|
|
33746
33988
|
banner: "[gemini-mcp] This project was developed and is maintained by AI (Claude). Use at your own discretion.",
|
|
33747
|
-
tools: [registerModelTools, registerGenerateTools, registerSetTools, registerInteractTools, registerJobTools]
|
|
33989
|
+
tools: [registerModelTools, registerGenerateTools, registerSetTools, registerInteractTools, registerVideoTools, registerMusicTools, registerJobTools]
|
|
33748
33990
|
});
|