@chrischall/gemini-mcp 0.1.0 → 0.3.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/dist/bundle.js CHANGED
@@ -2980,7 +2980,7 @@ var require_compile = __commonJS({
2980
2980
  const schOrFunc = root.refs[ref];
2981
2981
  if (schOrFunc)
2982
2982
  return schOrFunc;
2983
- let _sch = resolve.call(this, root, ref);
2983
+ let _sch = resolve2.call(this, root, ref);
2984
2984
  if (_sch === void 0) {
2985
2985
  const schema = (_a3 = root.localRefs) === null || _a3 === void 0 ? void 0 : _a3[ref];
2986
2986
  const { schemaId } = this.opts;
@@ -3007,7 +3007,7 @@ var require_compile = __commonJS({
3007
3007
  function sameSchemaEnv(s1, s2) {
3008
3008
  return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
3009
3009
  }
3010
- function resolve(root, ref) {
3010
+ function resolve2(root, ref) {
3011
3011
  let sch;
3012
3012
  while (typeof (sch = this.refs[ref]) == "string")
3013
3013
  ref = sch;
@@ -3638,7 +3638,7 @@ var require_fast_uri = __commonJS({
3638
3638
  }
3639
3639
  return uri;
3640
3640
  }
3641
- function resolve(baseURI, relativeURI, options) {
3641
+ function resolve2(baseURI, relativeURI, options) {
3642
3642
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
3643
3643
  const resolved = resolveComponent(parse3(baseURI, schemelessOptions), parse3(relativeURI, schemelessOptions), schemelessOptions, true);
3644
3644
  schemelessOptions.skipEscape = true;
@@ -3896,7 +3896,7 @@ var require_fast_uri = __commonJS({
3896
3896
  var fastUri = {
3897
3897
  SCHEMES,
3898
3898
  normalize,
3899
- resolve,
3899
+ resolve: resolve2,
3900
3900
  resolveComponent,
3901
3901
  equal,
3902
3902
  serialize,
@@ -28827,7 +28827,7 @@ var Protocol = class {
28827
28827
  return;
28828
28828
  }
28829
28829
  const pollInterval = task2.pollInterval ?? this._options?.defaultTaskPollInterval ?? 1e3;
28830
- await new Promise((resolve) => setTimeout(resolve, pollInterval));
28830
+ await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
28831
28831
  options?.signal?.throwIfAborted();
28832
28832
  }
28833
28833
  } catch (error51) {
@@ -28844,7 +28844,7 @@ var Protocol = class {
28844
28844
  */
28845
28845
  request(request, resultSchema, options) {
28846
28846
  const { relatedRequestId, resumptionToken, onresumptiontoken, task, relatedTask } = options ?? {};
28847
- return new Promise((resolve, reject) => {
28847
+ return new Promise((resolve2, reject) => {
28848
28848
  const earlyReject = (error51) => {
28849
28849
  reject(error51);
28850
28850
  };
@@ -28922,7 +28922,7 @@ var Protocol = class {
28922
28922
  if (!parseResult.success) {
28923
28923
  reject(parseResult.error);
28924
28924
  } else {
28925
- resolve(parseResult.data);
28925
+ resolve2(parseResult.data);
28926
28926
  }
28927
28927
  } catch (error51) {
28928
28928
  reject(error51);
@@ -29183,12 +29183,12 @@ var Protocol = class {
29183
29183
  }
29184
29184
  } catch {
29185
29185
  }
29186
- return new Promise((resolve, reject) => {
29186
+ return new Promise((resolve2, reject) => {
29187
29187
  if (signal.aborted) {
29188
29188
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
29189
29189
  return;
29190
29190
  }
29191
- const timeoutId = setTimeout(resolve, interval);
29191
+ const timeoutId = setTimeout(resolve2, interval);
29192
29192
  signal.addEventListener("abort", () => {
29193
29193
  clearTimeout(timeoutId);
29194
29194
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
@@ -30288,7 +30288,7 @@ var McpServer = class {
30288
30288
  let task = createTaskResult.task;
30289
30289
  const pollInterval = task.pollInterval ?? 5e3;
30290
30290
  while (task.status !== "completed" && task.status !== "failed" && task.status !== "cancelled") {
30291
- await new Promise((resolve) => setTimeout(resolve, pollInterval));
30291
+ await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
30292
30292
  const updatedTask = await extra.taskStore.getTask(taskId);
30293
30293
  if (!updatedTask) {
30294
30294
  throw new McpError(ErrorCode.InternalError, `Task ${taskId} not found during polling`);
@@ -30937,12 +30937,12 @@ var StdioServerTransport = class {
30937
30937
  this.onclose?.();
30938
30938
  }
30939
30939
  send(message) {
30940
- return new Promise((resolve) => {
30940
+ return new Promise((resolve2) => {
30941
30941
  const json2 = serializeMessage(message);
30942
30942
  if (this._stdout.write(json2)) {
30943
- resolve();
30943
+ resolve2();
30944
30944
  } else {
30945
- this._stdout.once("drain", resolve);
30945
+ this._stdout.once("drain", resolve2);
30946
30946
  }
30947
30947
  });
30948
30948
  }
@@ -31076,7 +31076,7 @@ var pageSchema = {
31076
31076
  };
31077
31077
 
31078
31078
  // src/version.ts
31079
- var VERSION = "0.1.0";
31079
+ var VERSION = "0.3.0";
31080
31080
 
31081
31081
  // src/client.ts
31082
31082
  import { dirname, join } from "node:path";
@@ -31099,6 +31099,7 @@ function filterImageModels(raw) {
31099
31099
  var __dirname = dirname(fileURLToPath(import.meta.url));
31100
31100
  await loadDotenvSafely({ path: join(__dirname, "..", ".env"), override: false });
31101
31101
  var BASE_URL = "https://generativelanguage.googleapis.com/v1beta";
31102
+ var INTERACTIONS_URL = "https://generativelanguage.googleapis.com/v1beta/interactions";
31102
31103
  var SERVICE = "Gemini";
31103
31104
  var GeminiClient = class {
31104
31105
  apiKey;
@@ -31148,31 +31149,121 @@ var GeminiClient = class {
31148
31149
  for (const img of opts.images ?? []) {
31149
31150
  parts.push({ inline_data: { mime_type: img.mimeType, data: img.base64 } });
31150
31151
  }
31151
- const generationConfig = { responseModalities: ["IMAGE"] };
31152
+ if (opts.videoUrl) {
31153
+ parts.push({ file_data: { file_uri: opts.videoUrl, mime_type: "video/mp4" } });
31154
+ }
31155
+ const generationConfig = { responseModalities: ["TEXT", "IMAGE"] };
31152
31156
  if (opts.aspectRatio || opts.imageSize) {
31153
31157
  const imageConfig = {};
31154
31158
  if (opts.aspectRatio) imageConfig.aspectRatio = opts.aspectRatio;
31155
31159
  if (opts.imageSize) imageConfig.imageSize = opts.imageSize;
31156
31160
  generationConfig.imageConfig = imageConfig;
31157
31161
  }
31162
+ if (opts.seed !== void 0) generationConfig.seed = opts.seed;
31163
+ if (opts.thinkingLevel !== void 0) generationConfig.thinkingConfig = { thinkingLevel: opts.thinkingLevel };
31164
+ const requestBody = { contents: [{ parts }], generationConfig };
31165
+ if (opts.googleSearch) requestBody.tools = [{ google_search: {} }];
31158
31166
  const data = await this.call(
31159
31167
  "POST",
31160
31168
  `/models/${model}:generateContent`,
31161
- { contents: [{ parts }], generationConfig }
31169
+ requestBody
31162
31170
  );
31163
- const out = [];
31171
+ const images = [];
31172
+ const textParts = [];
31173
+ let groundingMeta;
31164
31174
  for (const cand of data.candidates ?? []) {
31165
31175
  for (const part of cand.content?.parts ?? []) {
31166
31176
  const inline = part.inline_data ?? part.inlineData;
31167
- if (inline?.data) out.push({ base64: inline.data, mimeType: inline.mime_type ?? inline.mimeType ?? "image/jpeg" });
31177
+ if (inline?.data) {
31178
+ images.push({ base64: inline.data, mimeType: inline.mime_type ?? inline.mimeType ?? "image/jpeg" });
31179
+ } else if (typeof part.text === "string" && part.text.trim()) {
31180
+ textParts.push(part.text);
31181
+ }
31168
31182
  }
31183
+ if (cand.groundingMetadata) groundingMeta = cand.groundingMetadata;
31169
31184
  }
31170
- if (out.length === 0) {
31185
+ if (images.length === 0) {
31171
31186
  throw new McpToolError(`${SERVICE} returned no image`, {
31172
31187
  hint: "The request may have been blocked by safety filters \u2014 try rephrasing the prompt."
31173
31188
  });
31174
31189
  }
31175
- return out;
31190
+ const text = textParts.join("\n") || void 0;
31191
+ let grounding;
31192
+ if (groundingMeta) {
31193
+ const queries = (groundingMeta.webSearchQueries ?? []).filter(Boolean);
31194
+ const sources = (groundingMeta.groundingChunks ?? []).map((c) => c.web).filter((w) => !!w);
31195
+ if (queries.length > 0 || sources.length > 0) {
31196
+ grounding = {};
31197
+ if (queries.length > 0) grounding.queries = queries;
31198
+ if (sources.length > 0) grounding.sources = sources;
31199
+ }
31200
+ }
31201
+ return { images, text, grounding };
31202
+ }
31203
+ async interact(opts) {
31204
+ const key = this.requireKey();
31205
+ const model = resolveModel(opts.model, readEnvVar("GEMINI_IMAGE_MODEL"));
31206
+ const inputParts = [{ type: "text", text: opts.input }];
31207
+ for (const img of opts.images ?? []) {
31208
+ inputParts.push({ type: "image", mime_type: img.mimeType, data: img.base64 });
31209
+ }
31210
+ if (opts.videoUrl) {
31211
+ inputParts.push({ type: "video", uri: opts.videoUrl, mime_type: "video/mp4" });
31212
+ }
31213
+ const responseFormat = { type: "image", mime_type: "image/jpeg" };
31214
+ if (opts.aspectRatio) responseFormat.aspect_ratio = opts.aspectRatio;
31215
+ if (opts.imageSize) responseFormat.image_size = opts.imageSize;
31216
+ const body = { model, input: inputParts, response_format: responseFormat };
31217
+ if (opts.thinkingLevel !== void 0) body.generation_config = { thinking_level: opts.thinkingLevel };
31218
+ if (opts.previousInteractionId !== void 0) body.previous_interaction_id = opts.previousInteractionId;
31219
+ if (opts.googleSearch) body.tools = [{ type: "google_search" }];
31220
+ const res = await this.fetchImpl(INTERACTIONS_URL, {
31221
+ method: "POST",
31222
+ headers: {
31223
+ "x-goog-api-key": key,
31224
+ "content-type": "application/json",
31225
+ "Api-Revision": "2026-05-20"
31226
+ },
31227
+ body: JSON.stringify(body)
31228
+ });
31229
+ if (!res.ok) {
31230
+ const text = await res.text().catch(() => "");
31231
+ let message = text;
31232
+ try {
31233
+ const parsed = JSON.parse(text);
31234
+ if (parsed.error?.message) message = parsed.error.message;
31235
+ } catch {
31236
+ }
31237
+ throw new McpToolError(`Gemini Interactions API ${res.status}: ${truncateErrorMessage(message)}`);
31238
+ }
31239
+ const data = await res.json();
31240
+ const images = [];
31241
+ const textParts = [];
31242
+ const searchQueries = [];
31243
+ for (const step of data.steps ?? []) {
31244
+ if (step.type === "google_search_call") {
31245
+ for (const q of step.arguments?.queries ?? []) if (q?.trim()) searchQueries.push(q);
31246
+ continue;
31247
+ }
31248
+ if (step.type !== "model_output") continue;
31249
+ for (const parts of [step.content ?? [], step.summary ?? []]) {
31250
+ for (const part of parts) {
31251
+ if (part.type === "image" && part.data) {
31252
+ images.push({ base64: part.data, mimeType: part.mime_type ?? "image/jpeg" });
31253
+ } else if (part.type === "text" && part.text?.trim()) {
31254
+ textParts.push(part.text);
31255
+ }
31256
+ }
31257
+ }
31258
+ }
31259
+ if (images.length === 0) {
31260
+ throw new McpToolError("Gemini returned no image", {
31261
+ hint: "The request may have been blocked by safety filters \u2014 try rephrasing the prompt."
31262
+ });
31263
+ }
31264
+ const resultText = textParts.join("\n") || void 0;
31265
+ const grounding = searchQueries.length > 0 ? { queries: searchQueries } : void 0;
31266
+ return { id: data.id, images, text: resultText, grounding };
31176
31267
  }
31177
31268
  };
31178
31269
  var client = new GeminiClient();
@@ -31194,7 +31285,7 @@ function registerModelTools(server) {
31194
31285
 
31195
31286
  // src/images.ts
31196
31287
  import { mkdir, readFile, writeFile, access } from "node:fs/promises";
31197
- import { join as join2 } from "node:path";
31288
+ import { join as join2, resolve } from "node:path";
31198
31289
  function slugify2(text, max = 40) {
31199
31290
  const s = text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, max).replace(/-+$/g, "");
31200
31291
  return s || "image";
@@ -31216,43 +31307,93 @@ async function uniquePath(dir, base, ext) {
31216
31307
  }
31217
31308
  return candidate;
31218
31309
  }
31310
+ function sniffMimeBytes(buf) {
31311
+ if (buf.length >= 4 && buf[0] === 137 && buf[1] === 80 && buf[2] === 78 && buf[3] === 71) {
31312
+ return "image/png";
31313
+ }
31314
+ if (buf.length >= 3 && buf[0] === 255 && buf[1] === 216 && buf[2] === 255) {
31315
+ return "image/jpeg";
31316
+ }
31317
+ if (buf.length >= 12 && buf[0] === 82 && buf[1] === 73 && buf[2] === 70 && buf[3] === 70 && buf[8] === 87 && buf[9] === 69 && buf[10] === 66 && buf[11] === 80) {
31318
+ return "image/webp";
31319
+ }
31320
+ return "image/png";
31321
+ }
31219
31322
  async function writeImage(dir, base, base643, mimeType) {
31220
31323
  await mkdir(dir, { recursive: true });
31221
- const ext = mimeType.includes("jpeg") ? "jpg" : "png";
31324
+ const ext = mimeType.includes("jpeg") ? "jpg" : mimeType.includes("webp") ? "webp" : "png";
31222
31325
  const path = await uniquePath(dir, base, ext);
31223
31326
  await writeFile(path, Buffer.from(base643, "base64"));
31224
- return path;
31327
+ return resolve(path);
31225
31328
  }
31226
31329
  async function readImageAsInline(path) {
31227
31330
  const buf = await readFile(path);
31228
- const lower = path.toLowerCase();
31229
- const mimeType = lower.endsWith(".jpg") || lower.endsWith(".jpeg") ? "image/jpeg" : "image/png";
31331
+ const mimeType = sniffMimeBytes(buf);
31230
31332
  return { base64: buf.toString("base64"), mimeType };
31231
31333
  }
31334
+ function decodeImageInput(input) {
31335
+ const trimmed = input.trim();
31336
+ if (trimmed.startsWith("data:")) {
31337
+ const marker = trimmed.indexOf(";base64,");
31338
+ if (marker === -1) {
31339
+ throw new Error(`Unsupported data URI (expected ;base64,<data>): ${trimmed.slice(0, 48)}\u2026`);
31340
+ }
31341
+ const mimeType = trimmed.slice(5, trimmed.indexOf(";", 5)) || "image/png";
31342
+ return { mimeType, base64: trimmed.slice(marker + ";base64,".length) };
31343
+ }
31344
+ const buf = Buffer.from(trimmed, "base64");
31345
+ return { base64: trimmed, mimeType: sniffMimeBytes(buf) };
31346
+ }
31347
+ async function loadImageInputs(paths = [], base64s = []) {
31348
+ const fromPaths = await Promise.all(paths.map((p) => readImageAsInline(p)));
31349
+ const fromBase64s = base64s.map((b) => decodeImageInput(b));
31350
+ return [...fromPaths, ...fromBase64s];
31351
+ }
31352
+ function baseName(name) {
31353
+ const stripped = name.replace(/\.(png|jpe?g|webp)$/i, "");
31354
+ const safe = stripped.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
31355
+ return safe || "image";
31356
+ }
31232
31357
  function resolveOutputDir(perCall) {
31233
31358
  return perCall?.trim() || readEnvVar("GEMINI_OUTPUT_DIR") || process.cwd();
31234
31359
  }
31235
31360
 
31236
31361
  // src/tools/shared.ts
31237
31362
  var ASPECT_RATIOS = ["1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "21:9", "1:4", "4:1", "1:8", "8:1"];
31238
- var IMAGE_SIZES = ["1K", "2K", "4K"];
31363
+ var IMAGE_SIZES = ["512", "1K", "2K", "4K"];
31239
31364
  var sharedImageSchema = {
31240
31365
  model: external_exports.string().optional().describe("Model id override (default: server default; see gemini_list_models)"),
31241
31366
  aspect_ratio: external_exports.enum(ASPECT_RATIOS).optional().describe("Output aspect ratio"),
31242
- image_size: external_exports.enum(IMAGE_SIZES).optional().describe("Output resolution"),
31367
+ image_size: external_exports.enum(IMAGE_SIZES).optional().describe("Output resolution (512 = 0.5K, Flash-only)"),
31243
31368
  output_dir: external_exports.string().optional().describe("Directory to write images to (default: $GEMINI_OUTPUT_DIR or cwd)"),
31244
- inline: external_exports.boolean().optional().describe("Return base64 images inline instead of writing to disk")
31369
+ inline: external_exports.boolean().optional().describe("Return base64 images inline instead of writing to disk"),
31370
+ seed: external_exports.number().int().optional().describe("Seed for reproducible generation; random if omitted"),
31371
+ thinking_level: external_exports.enum(["minimal", "high"]).optional().describe("Reasoning depth (Gemini 3 models); higher can help complex/structural edits"),
31372
+ google_search: external_exports.boolean().optional().describe("Ground the image in live Google Search results (current events, weather, data)")
31245
31373
  };
31246
- async function emit(named, opts) {
31374
+ function pickSeed(seed) {
31375
+ return seed ?? Math.floor(Math.random() * 2147483e3);
31376
+ }
31377
+ function buildMeta(model, seed, opts) {
31378
+ const meta3 = { model, seed };
31379
+ if (opts.aspect_ratio) meta3.aspect_ratio = opts.aspect_ratio;
31380
+ if (opts.image_size) meta3.image_size = opts.image_size;
31381
+ return meta3;
31382
+ }
31383
+ async function emit(named, opts, meta3) {
31247
31384
  if (opts.inline) {
31248
- return {
31249
- content: named.map((n) => ({ type: "image", data: n.image.base64, mimeType: n.image.mimeType }))
31250
- };
31385
+ const imageBlocks = named.map((n) => ({ type: "image", data: n.image.base64, mimeType: n.image.mimeType }));
31386
+ if (meta3 && Object.keys(meta3).length > 0) {
31387
+ return {
31388
+ content: [{ type: "text", text: JSON.stringify(meta3, null, 2) }, ...imageBlocks]
31389
+ };
31390
+ }
31391
+ return { content: imageBlocks };
31251
31392
  }
31252
31393
  const dir = resolveOutputDir(opts.output_dir);
31253
31394
  const images = [];
31254
31395
  for (const n of named) images.push(await writeImage(dir, n.base, n.image.base64, n.image.mimeType));
31255
- return textResult({ images });
31396
+ return textResult({ images, ...meta3 });
31256
31397
  }
31257
31398
 
31258
31399
  // src/tools/generate.ts
@@ -31265,46 +31406,83 @@ function registerGenerateTools(server) {
31265
31406
  inputSchema: {
31266
31407
  prompt: external_exports.string().min(1).describe("Text prompt describing the image"),
31267
31408
  count: external_exports.number().int().positive().max(8).optional().describe("Number of independent images (default 1)"),
31409
+ filename: external_exports.string().optional().describe("Base filename for the output image (extension stripped; default: slugified prompt)"),
31410
+ images: external_exports.array(external_exports.string().min(1)).optional().describe("Paths to reference input images (image-conditioned generation)"),
31411
+ images_base64: external_exports.array(external_exports.string().min(1)).optional().describe("Reference images as base64 strings or data URIs"),
31412
+ video_url: external_exports.string().url().optional().describe("Public YouTube URL as a video reference (video\u2192image; use a Flash model e.g. gemini-3.1-flash-image)"),
31268
31413
  ...sharedImageSchema
31269
31414
  }
31270
31415
  },
31271
31416
  async (args) => {
31272
31417
  const count = args.count ?? 1;
31273
- const slug = slugify2(args.prompt);
31418
+ const seed = pickSeed(args.seed);
31419
+ const model = resolveModel(args.model, readEnvVar("GEMINI_IMAGE_MODEL"));
31420
+ const slug = args.filename ? baseName(args.filename) : slugify2(args.prompt);
31421
+ const refInputs = await loadImageInputs(args.images, args.images_base64);
31274
31422
  const named = [];
31423
+ let capturedText;
31424
+ let capturedGrounding;
31275
31425
  for (let i = 0; i < count; i++) {
31276
- const [img] = await client.generate({
31426
+ const result = await client.generate({
31277
31427
  prompt: args.prompt,
31428
+ images: refInputs.length > 0 ? refInputs : void 0,
31278
31429
  model: args.model,
31279
31430
  aspectRatio: args.aspect_ratio,
31280
- imageSize: args.image_size
31431
+ imageSize: args.image_size,
31432
+ seed: seed + i,
31433
+ thinkingLevel: args.thinking_level,
31434
+ googleSearch: args.google_search,
31435
+ videoUrl: args.video_url
31281
31436
  });
31437
+ const { images: [img], text } = result;
31438
+ if (text && !capturedText) capturedText = text;
31439
+ if (result.grounding && !capturedGrounding) capturedGrounding = result.grounding;
31282
31440
  named.push({ image: img, base: count === 1 ? slug : `${slug}-${String(i + 1).padStart(2, "0")}` });
31283
31441
  }
31284
- return emit(named, args);
31442
+ const meta3 = buildMeta(model, seed, args);
31443
+ if (capturedText) meta3.text = capturedText;
31444
+ if (capturedGrounding) meta3.grounding = capturedGrounding;
31445
+ return emit(named, args, meta3);
31285
31446
  }
31286
31447
  );
31287
31448
  server.registerTool(
31288
31449
  "gemini_edit_image",
31289
31450
  {
31290
- description: "Edit or compose images: provide one image to edit, or multiple images to combine/blend, plus a text instruction.",
31451
+ description: "Edit or compose images: provide one or more input images (paths or base64), plus a text instruction. 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.",
31291
31452
  annotations: { readOnlyHint: false, openWorldHint: true },
31292
31453
  inputSchema: {
31293
31454
  prompt: external_exports.string().min(1).describe("Instruction describing the edit or composition"),
31294
- images: external_exports.array(external_exports.string().min(1)).min(1).describe("Paths to input image file(s) (1 = edit, 2+ = compose)"),
31455
+ images: external_exports.array(external_exports.string().min(1)).optional().describe("Paths to input image file(s) (1 = edit, 2+ = compose)"),
31456
+ images_base64: external_exports.array(external_exports.string().min(1)).optional().describe("Input images as base64 strings or data URIs"),
31457
+ filename: external_exports.string().optional().describe("Base filename for the output image (extension stripped; default: slugified prompt)"),
31295
31458
  ...sharedImageSchema
31296
31459
  }
31297
31460
  },
31298
31461
  async (args) => {
31299
- const inputs = await Promise.all(args.images.map((p) => readImageAsInline(p)));
31300
- const [img] = await client.generate({
31462
+ const hasPaths = (args.images?.length ?? 0) > 0;
31463
+ const hasBase64 = (args.images_base64?.length ?? 0) > 0;
31464
+ if (!hasPaths && !hasBase64) {
31465
+ throw new McpToolError("Provide at least one input image via `images` or `images_base64`.");
31466
+ }
31467
+ const seed = pickSeed(args.seed);
31468
+ const model = resolveModel(args.model, readEnvVar("GEMINI_IMAGE_MODEL"));
31469
+ const slug = args.filename ? baseName(args.filename) : slugify2(args.prompt);
31470
+ const inputs = await loadImageInputs(args.images, args.images_base64);
31471
+ const result = await client.generate({
31301
31472
  prompt: args.prompt,
31302
31473
  images: inputs,
31303
31474
  model: args.model,
31304
31475
  aspectRatio: args.aspect_ratio,
31305
- imageSize: args.image_size
31476
+ imageSize: args.image_size,
31477
+ seed,
31478
+ thinkingLevel: args.thinking_level,
31479
+ googleSearch: args.google_search
31306
31480
  });
31307
- return emit([{ image: img, base: slugify2(args.prompt) }], args);
31481
+ const { images: [img], text } = result;
31482
+ const meta3 = buildMeta(model, seed, args);
31483
+ if (text) meta3.text = text;
31484
+ if (result.grounding) meta3.grounding = result.grounding;
31485
+ return emit([{ image: img, base: slug }], args, meta3);
31308
31486
  }
31309
31487
  );
31310
31488
  }
@@ -31321,6 +31499,9 @@ function registerSetTools(server) {
31321
31499
  scenes: external_exports.array(external_exports.string().min(1)).min(1).max(8).optional().describe("Per-image prompts (1-8); each references the master"),
31322
31500
  count: external_exports.number().int().positive().max(8).optional().describe("Number of variations of master_prompt (when scenes omitted)"),
31323
31501
  reference_mode: external_exports.enum(["master", "chain"]).optional().describe("master: every image references the master (default). chain: each references the previous."),
31502
+ basename: external_exports.string().optional().describe("Base filename prefix for output images (default: slugified master_prompt)"),
31503
+ master_images: external_exports.array(external_exports.string().min(1)).optional().describe("Reference image paths passed to the master generation call"),
31504
+ master_images_base64: external_exports.array(external_exports.string().min(1)).optional().describe("Reference images as base64 strings or data URIs for master generation"),
31324
31505
  ...sharedImageSchema
31325
31506
  }
31326
31507
  },
@@ -31328,26 +31509,88 @@ function registerSetTools(server) {
31328
31509
  if (args.scenes && args.count || !args.scenes && !args.count) {
31329
31510
  throw new McpToolError("Provide exactly one of `scenes` or `count`.");
31330
31511
  }
31331
- const cfg = { model: args.model, aspectRatio: args.aspect_ratio, imageSize: args.image_size };
31332
- const slug = slugify2(args.master_prompt);
31333
- const [master] = await client.generate({ prompt: args.master_prompt, ...cfg });
31512
+ const seed = pickSeed(args.seed);
31513
+ const model = resolveModel(args.model, readEnvVar("GEMINI_IMAGE_MODEL"));
31514
+ const cfg = { model: args.model, aspectRatio: args.aspect_ratio, imageSize: args.image_size, thinkingLevel: args.thinking_level, googleSearch: args.google_search };
31515
+ const slug = args.basename ? baseName(args.basename) : slugify2(args.master_prompt);
31516
+ const masterRefInputs = await loadImageInputs(args.master_images, args.master_images_base64);
31517
+ const masterResult = await client.generate({
31518
+ prompt: args.master_prompt,
31519
+ images: masterRefInputs.length > 0 ? masterRefInputs : void 0,
31520
+ seed,
31521
+ ...cfg
31522
+ });
31523
+ const { images: [master], text: masterText } = masterResult;
31334
31524
  const named = [{ image: master, base: `${slug}-master` }];
31335
31525
  const scenePrompts = args.scenes ?? Array.from({ length: args.count ?? 0 }, () => args.master_prompt);
31336
31526
  const mode = args.reference_mode ?? "master";
31337
31527
  if (mode === "chain") {
31338
31528
  let ref = master;
31339
31529
  for (let i = 0; i < scenePrompts.length; i++) {
31340
- const [img] = await client.generate({ prompt: scenePrompts[i], images: [ref], ...cfg });
31530
+ const { images: [img] } = await client.generate({ prompt: scenePrompts[i], images: [ref], seed: seed + i + 1, ...cfg });
31341
31531
  named.push({ image: img, base: `${slug}-${String(i + 1).padStart(2, "0")}` });
31342
31532
  ref = img;
31343
31533
  }
31344
31534
  } else {
31345
31535
  const scenes = await Promise.all(
31346
- scenePrompts.map((p) => client.generate({ prompt: p, images: [master], ...cfg }).then((r) => r[0]))
31536
+ scenePrompts.map((p, i) => client.generate({ prompt: p, images: [master], seed: seed + i + 1, ...cfg }).then((r) => r.images[0]))
31347
31537
  );
31348
31538
  scenes.forEach((img, i) => named.push({ image: img, base: `${slug}-${String(i + 1).padStart(2, "0")}` }));
31349
31539
  }
31350
- return emit(named, args);
31540
+ const meta3 = buildMeta(model, seed, args);
31541
+ if (masterText) meta3.text = masterText;
31542
+ if (masterResult.grounding) meta3.grounding = masterResult.grounding;
31543
+ return emit(named, args, meta3);
31544
+ }
31545
+ );
31546
+ }
31547
+
31548
+ // src/tools/interact.ts
31549
+ function registerInteractTools(server) {
31550
+ server.registerTool(
31551
+ "gemini_interact",
31552
+ {
31553
+ description: "Multi-turn image generation/editing via Gemini's Interactions API (Beta). Returns an interaction `id`; pass it back as `previous_interaction_id` to iteratively refine the SAME image conversationally (the recommended way to make incremental edits). Output is JPEG.",
31554
+ annotations: { readOnlyHint: false, openWorldHint: true },
31555
+ inputSchema: {
31556
+ input: external_exports.string().min(1).describe("Text prompt or editing instruction"),
31557
+ previous_interaction_id: external_exports.string().optional().describe("ID from a prior gemini_interact call \u2014 continues that multi-turn conversation"),
31558
+ images: external_exports.array(external_exports.string().min(1)).optional().describe("Paths to reference input images"),
31559
+ images_base64: external_exports.array(external_exports.string().min(1)).optional().describe("Reference images as base64 strings or data URIs"),
31560
+ model: external_exports.string().optional().describe("Model id override (default: server default; see gemini_list_models)"),
31561
+ aspect_ratio: external_exports.enum(ASPECT_RATIOS).optional().describe("Output aspect ratio"),
31562
+ image_size: external_exports.enum(IMAGE_SIZES).optional().describe("Output resolution (512 = 0.5K, Flash-only)"),
31563
+ thinking_level: external_exports.enum(["minimal", "high"]).optional().describe("Reasoning depth; higher can help complex/structural edits"),
31564
+ filename: external_exports.string().optional().describe("Base filename for the output image (extension stripped; default: slugified input)"),
31565
+ output_dir: external_exports.string().optional().describe("Directory to write images to (default: $GEMINI_OUTPUT_DIR or cwd)"),
31566
+ inline: external_exports.boolean().optional().describe("Return base64 images inline instead of writing to disk"),
31567
+ google_search: external_exports.boolean().optional().describe("Ground the image in live Google Search results (current events, weather, data)"),
31568
+ video_url: external_exports.string().url().optional().describe("Public YouTube URL as a video reference (video\u2192image; use a Flash model e.g. gemini-3.1-flash-image)")
31569
+ }
31570
+ },
31571
+ async (args) => {
31572
+ const inputs = await loadImageInputs(args.images, args.images_base64);
31573
+ const r = await client.interact({
31574
+ input: args.input,
31575
+ images: inputs.length ? inputs : void 0,
31576
+ model: args.model,
31577
+ aspectRatio: args.aspect_ratio,
31578
+ imageSize: args.image_size,
31579
+ thinkingLevel: args.thinking_level,
31580
+ previousInteractionId: args.previous_interaction_id,
31581
+ googleSearch: args.google_search,
31582
+ videoUrl: args.video_url
31583
+ });
31584
+ const model = resolveModel(args.model, readEnvVar("GEMINI_IMAGE_MODEL"));
31585
+ const meta3 = { model, interaction_id: r.id };
31586
+ if (r.text) meta3.text = r.text;
31587
+ if (r.grounding) meta3.grounding = r.grounding;
31588
+ const slug = args.filename ? baseName(args.filename) : slugify2(args.input);
31589
+ const named = r.images.map((image, i) => ({
31590
+ image,
31591
+ base: r.images.length > 1 ? `${slug}-${String(i + 1).padStart(2, "0")}` : slug
31592
+ }));
31593
+ return emit(named, args, meta3);
31351
31594
  }
31352
31595
  );
31353
31596
  }
@@ -31357,5 +31600,5 @@ await runMcp({
31357
31600
  name: "gemini-mcp",
31358
31601
  version: VERSION,
31359
31602
  banner: "[gemini-mcp] This project was developed and is maintained by AI (Claude). Use at your own discretion.",
31360
- tools: [registerModelTools, registerGenerateTools, registerSetTools]
31603
+ tools: [registerModelTools, registerGenerateTools, registerSetTools, registerInteractTools]
31361
31604
  });