@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.
@@ -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.9.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.9.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.9.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": "./SKILL.md",
22
+ "skills": "./skills/",
23
23
  "mcp": "./.mcp.json"
24
24
  }
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
  [![npm](https://img.shields.io/npm/v/@chrischall/gemini-mcp)](https://www.npmjs.com/package/@chrischall/gemini-mcp)
5
5
  [![license](https://img.shields.io/npm/l/@chrischall/gemini-mcp)](LICENSE)
6
6
 
7
- MCP server for Google Gemini image generation and editing. Exposes four tools to Claude over stdio: list available models, generate images from text prompts, edit or compose images with text instructions, and generate a consistent set of images from a master prompt. Images are 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 model family.
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
- | `gemini_generate_image` | Generate image(s) from a text prompt |
46
- | `gemini_edit_image` | One-off edits or multi-image composition with a text instruction (for a series of edits, use `gemini_interact`) |
47
- | `gemini_generate_set` | Generate a master image plus N consistent images referencing it |
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.9.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
- data = await interactionsApi.fetchJson("POST", "/interactions", {
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: call gemini_interact again with the last output image passed via `images` (or `images_base64`) plus the same instruction, then chain the NEW interaction id.",
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 (opts.searchTypes?.includes("image_search")) {
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
- if (part.type === "image" && part.data) {
32926
- images.push({ base64: part.data, mimeType: part.mime_type ?? "image/jpeg" });
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 { id: data.id, images, text: resultText, grounding };
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
- async function writeImage(dir, base, base643, mimeType) {
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 ext = mimeType.includes("jpeg") ? "jpg" : mimeType.includes("webp") ? "webp" : "png";
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 emit(named, opts, meta3, onWritten) {
33218
- if (opts.inline) {
33219
- const imageBlocks = named.map((n) => ({ type: "image", data: n.image.base64, mimeType: n.image.mimeType }));
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: imageBlocks };
33297
+ return { content: blocks };
33226
33298
  }
33227
33299
  const dir = resolveOutputDir(opts.output_dir);
33228
- const images = [];
33229
- for (const n of named) images.push(await writeImage(dir, n.base, n.image.base64, n.image.mimeType));
33230
- await onWritten?.(images);
33231
- return textResult({ images, ...meta3 });
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
- "gemini_generate_image",
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("gemini_generate_image", {
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: "gemini_generate_image", fingerprint, idempotencyKey: args.idempotency_key, async: args.async }, async () => {
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
- "gemini_edit_image",
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 gemini_edit_image 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.",
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("gemini_edit_image", {
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: "gemini_edit_image", fingerprint, idempotencyKey: args.idempotency_key, async: args.async }, async () => {
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
- "gemini_generate_set",
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("gemini_generate_set", {
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: "gemini_generate_set", fingerprint, idempotencyKey: args.idempotency_key, async: args.async }, async () => {
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
  });