@chrischall/gemini-mcp 0.7.1 → 0.7.2

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.7.1"
10
+ "version": "0.7.2"
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.7.1",
18
+ "version": "0.7.2",
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.7.1",
4
+ "version": "0.7.2",
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",
package/dist/bundle.js CHANGED
@@ -32510,7 +32510,7 @@ var init_clipboard = __esm({
32510
32510
  init_dist();
32511
32511
 
32512
32512
  // src/version.ts
32513
- var VERSION = "0.7.1";
32513
+ var VERSION = "0.7.2";
32514
32514
 
32515
32515
  // src/tools/models.ts
32516
32516
  init_dist();
@@ -32892,7 +32892,7 @@ function resolveInputPath(p, kind) {
32892
32892
  const inputDir = readEnvVar("GEMINI_INPUT_DIR");
32893
32893
  if (inputDir) {
32894
32894
  const candidate = join3(inputDir, p);
32895
- if (existsSync(candidate)) return candidate;
32895
+ if (existsSync(candidate)) return resolve(candidate);
32896
32896
  }
32897
32897
  const cwd = resolve(p);
32898
32898
  if (existsSync(cwd)) return cwd;
@@ -33011,7 +33011,7 @@ function buildMeta(model, seed, opts) {
33011
33011
  if (opts.image_size) meta3.image_size = opts.image_size;
33012
33012
  return meta3;
33013
33013
  }
33014
- async function emit(named, opts, meta3) {
33014
+ async function emit(named, opts, meta3, onWritten) {
33015
33015
  if (opts.inline) {
33016
33016
  const imageBlocks = named.map((n) => ({ type: "image", data: n.image.base64, mimeType: n.image.mimeType }));
33017
33017
  if (meta3 && Object.keys(meta3).length > 0) {
@@ -33024,6 +33024,7 @@ async function emit(named, opts, meta3) {
33024
33024
  const dir = resolveOutputDir(opts.output_dir);
33025
33025
  const images = [];
33026
33026
  for (const n of named) images.push(await writeImage(dir, n.base, n.image.base64, n.image.mimeType));
33027
+ onWritten?.(images);
33027
33028
  return textResult({ images, ...meta3 });
33028
33029
  }
33029
33030
 
@@ -33189,6 +33190,21 @@ function registerSetTools(server) {
33189
33190
  init_zod();
33190
33191
  init_dist();
33191
33192
  var lastInteractionId;
33193
+ var writtenOutputs = /* @__PURE__ */ new Set();
33194
+ var NEW_REFERENCES_ONLY = "NEW reference images only (e.g. a style or target photo). When chaining with previous_interaction_id, do NOT re-attach the prior turn's output \u2014 the interaction already contains it, and re-sending it anchors the model against your edit.";
33195
+ function splitReattachedOutputs(images) {
33196
+ const kept = [];
33197
+ const dropped = [];
33198
+ for (const p of images) {
33199
+ let resolved;
33200
+ try {
33201
+ resolved = resolveImagePath(p);
33202
+ } catch {
33203
+ }
33204
+ (resolved && writtenOutputs.has(resolved) ? dropped : kept).push(p);
33205
+ }
33206
+ return { kept, dropped };
33207
+ }
33192
33208
  function registerInteractTools(server) {
33193
33209
  server.registerTool(
33194
33210
  "gemini_interact",
@@ -33199,8 +33215,8 @@ function registerInteractTools(server) {
33199
33215
  input: external_exports.string().min(1).describe("Text prompt or editing instruction"),
33200
33216
  previous_interaction_id: external_exports.string().optional().describe("ID from a prior gemini_interact call \u2014 continues that multi-turn conversation"),
33201
33217
  continue_last: external_exports.boolean().optional().describe("Continue from the most recent interaction this server created (convenience for previous_interaction_id; an explicit id wins)"),
33202
- images: external_exports.array(external_exports.string().min(1)).optional().describe("Paths to reference input images"),
33203
- images_base64: external_exports.array(external_exports.string().min(1)).optional().describe("Reference images as base64 strings or data URIs"),
33218
+ images: external_exports.array(external_exports.string().min(1)).optional().describe(`Paths to reference input images. ${NEW_REFERENCES_ONLY}`),
33219
+ images_base64: external_exports.array(external_exports.string().min(1)).optional().describe(`Reference images as base64 strings or data URIs. ${NEW_REFERENCES_ONLY}`),
33204
33220
  model: external_exports.string().optional().describe(`Model id override (default: server default; see gemini_list_models). ${MODEL_CHOICE_GUIDE}`),
33205
33221
  aspect_ratio: external_exports.enum(ASPECT_RATIOS).optional().describe("Output aspect ratio"),
33206
33222
  image_size: external_exports.enum(IMAGE_SIZES).optional().describe("Output resolution (512 = 0.5K, Flash-only)"),
@@ -33227,7 +33243,12 @@ function registerInteractTools(server) {
33227
33243
  }
33228
33244
  previousInteractionId = lastInteractionId;
33229
33245
  }
33230
- const inputs = await gatherImageInputs({ images: args.images, images_base64: args.images_base64, from_clipboard: args.from_clipboard });
33246
+ let images = args.images;
33247
+ let dropped = [];
33248
+ if (previousInteractionId && images?.length) {
33249
+ ({ kept: images, dropped } = splitReattachedOutputs(images));
33250
+ }
33251
+ const inputs = await gatherImageInputs({ images, images_base64: args.images_base64, from_clipboard: args.from_clipboard });
33231
33252
  const video = await resolveVideoInput(args);
33232
33253
  const r = await client.interact({
33233
33254
  input: args.input,
@@ -33246,7 +33267,8 @@ function registerInteractTools(server) {
33246
33267
  const model = resolveModel(args.model, readEnvVar("GEMINI_IMAGE_MODEL"));
33247
33268
  const meta3 = { model, interaction_id: r.id };
33248
33269
  if (previousInteractionId) meta3.previous_interaction_id = previousInteractionId;
33249
- meta3.hint = `To refine this image, call gemini_interact again with previous_interaction_id: "${r.id}" (or continue_last: true).`;
33270
+ if (dropped.length) meta3.dropped_previous_output = dropped;
33271
+ meta3.hint = `To refine this image, call gemini_interact again with previous_interaction_id: "${r.id}" (or continue_last: true) \u2014 do NOT re-attach the returned image as an input; the interaction already contains it.`;
33250
33272
  if (r.text) meta3.text = r.text;
33251
33273
  if (r.grounding) meta3.grounding = r.grounding;
33252
33274
  if (video.videoFileMeta) meta3.video_file = video.videoFileMeta;
@@ -33255,7 +33277,9 @@ function registerInteractTools(server) {
33255
33277
  image,
33256
33278
  base: r.images.length > 1 ? `${slug}-${String(i + 1).padStart(2, "0")}` : slug
33257
33279
  }));
33258
- return emit(named, args, meta3);
33280
+ return emit(named, args, meta3, (paths) => {
33281
+ for (const p of paths) writtenOutputs.add(p);
33282
+ });
33259
33283
  }
33260
33284
  );
33261
33285
  }
package/dist/images.js CHANGED
@@ -62,7 +62,9 @@ export async function writeImage(dir, base, base64, mimeType) {
62
62
  * 1. Absolute path that exists → returned as-is.
63
63
  * 2. `GEMINI_INPUT_DIR` env var is set → look for `join(inputDir, p)`.
64
64
  * 3. Relative to cwd → `resolve(p)`.
65
- * Throws a helpful `McpToolError` (labelled `kind`) if none are found.
65
+ * Always returns an absolute path (callers compare resolved paths — e.g. the
66
+ * interact re-attach guard — so a relative GEMINI_INPUT_DIR must not leak
67
+ * through). Throws a helpful `McpToolError` (labelled `kind`) if none are found.
66
68
  */
67
69
  function resolveInputPath(p, kind) {
68
70
  if (isAbsolute(p) && existsSync(p))
@@ -73,7 +75,7 @@ function resolveInputPath(p, kind) {
73
75
  // single-user MCP — the caller already has the user's own filesystem access.
74
76
  const candidate = join(inputDir, p);
75
77
  if (existsSync(candidate))
76
- return candidate;
78
+ return resolve(candidate);
77
79
  }
78
80
  const cwd = resolve(p);
79
81
  if (existsSync(cwd))
@@ -2,12 +2,40 @@ import { z } from 'zod';
2
2
  import { McpToolError, readEnvVar } from '@chrischall/mcp-utils';
3
3
  import { resolveModel } from '../models.js';
4
4
  import { client } from '../client.js';
5
- import { slugify, baseName, gatherImageInputs } from '../images.js';
5
+ import { slugify, baseName, gatherImageInputs, resolveImagePath } from '../images.js';
6
6
  import { emit, ASPECT_RATIOS, IMAGE_SIZES, MODEL_CHOICE_GUIDE, resolveVideoInput, videoPathSchema } from './shared.js';
7
7
  // The most recent interaction id this server process created — what
8
8
  // `continue_last: true` resumes. In-memory only: an MCP server lives for the
9
9
  // host session, so "last" means "last in this session".
10
10
  let lastInteractionId;
11
+ // Absolute paths of every image this tool has written this session. Used to
12
+ // drop a chained call's re-attached prior output: the interaction state
13
+ // already contains that image, and re-sending it as a fresh reference anchors
14
+ // the model against the requested edit (observed in the wild — callers pass
15
+ // the last result path back via `images` despite the description saying not to).
16
+ const writtenOutputs = new Set();
17
+ /** Shared warning for the reference-image params. */
18
+ const NEW_REFERENCES_ONLY = 'NEW reference images only (e.g. a style or target photo). When chaining with previous_interaction_id, ' +
19
+ "do NOT re-attach the prior turn's output — the interaction already contains it, and re-sending it " +
20
+ 'anchors the model against your edit.';
21
+ /**
22
+ * On a chained call, split `images` into genuinely new references vs paths
23
+ * this server itself generated (which the interaction already contains).
24
+ * Unresolvable paths are kept so `gatherImageInputs` surfaces its usual error.
25
+ */
26
+ function splitReattachedOutputs(images) {
27
+ const kept = [];
28
+ const dropped = [];
29
+ for (const p of images) {
30
+ let resolved;
31
+ try {
32
+ resolved = resolveImagePath(p);
33
+ }
34
+ catch { /* kept; loading will error actionably */ }
35
+ (resolved && writtenOutputs.has(resolved) ? dropped : kept).push(p);
36
+ }
37
+ return { kept, dropped };
38
+ }
11
39
  export function registerInteractTools(server) {
12
40
  server.registerTool('gemini_interact', {
13
41
  description: 'Preferred tool for iterative or multi-step refinement of a single image — ' +
@@ -30,11 +58,11 @@ export function registerInteractTools(server) {
30
58
  images: z
31
59
  .array(z.string().min(1))
32
60
  .optional()
33
- .describe('Paths to reference input images'),
61
+ .describe(`Paths to reference input images. ${NEW_REFERENCES_ONLY}`),
34
62
  images_base64: z
35
63
  .array(z.string().min(1))
36
64
  .optional()
37
- .describe('Reference images as base64 strings or data URIs'),
65
+ .describe(`Reference images as base64 strings or data URIs. ${NEW_REFERENCES_ONLY}`),
38
66
  model: z
39
67
  .string()
40
68
  .optional()
@@ -90,7 +118,12 @@ export function registerInteractTools(server) {
90
118
  }
91
119
  previousInteractionId = lastInteractionId;
92
120
  }
93
- const inputs = await gatherImageInputs({ images: args.images, images_base64: args.images_base64, from_clipboard: args.from_clipboard });
121
+ let images = args.images;
122
+ let dropped = [];
123
+ if (previousInteractionId && images?.length) {
124
+ ({ kept: images, dropped } = splitReattachedOutputs(images));
125
+ }
126
+ const inputs = await gatherImageInputs({ images, images_base64: args.images_base64, from_clipboard: args.from_clipboard });
94
127
  const video = await resolveVideoInput(args);
95
128
  const r = await client.interact({
96
129
  input: args.input,
@@ -110,7 +143,11 @@ export function registerInteractTools(server) {
110
143
  const meta = { model, interaction_id: r.id };
111
144
  if (previousInteractionId)
112
145
  meta.previous_interaction_id = previousInteractionId;
113
- meta.hint = `To refine this image, call gemini_interact again with previous_interaction_id: "${r.id}" (or continue_last: true).`;
146
+ if (dropped.length)
147
+ meta.dropped_previous_output = dropped;
148
+ meta.hint =
149
+ `To refine this image, call gemini_interact again with previous_interaction_id: "${r.id}" (or continue_last: true) — ` +
150
+ 'do NOT re-attach the returned image as an input; the interaction already contains it.';
114
151
  if (r.text)
115
152
  meta.text = r.text;
116
153
  if (r.grounding)
@@ -122,6 +159,8 @@ export function registerInteractTools(server) {
122
159
  image,
123
160
  base: r.images.length > 1 ? `${slug}-${String(i + 1).padStart(2, '0')}` : slug,
124
161
  }));
125
- return emit(named, args, meta);
162
+ // writeImage returns already-resolved absolute paths.
163
+ return emit(named, args, meta, (paths) => { for (const p of paths)
164
+ writtenOutputs.add(p); });
126
165
  });
127
166
  }
@@ -85,8 +85,10 @@ export function buildMeta(model, seed, opts) {
85
85
  * return their paths as a text result. `inline` wins when true.
86
86
  * Optional `meta` is merged into the disk-path JSON result, or prepended as a
87
87
  * text block in inline mode (only if meta has keys).
88
+ * Optional `onWritten` receives the absolute paths written in disk mode (never
89
+ * called inline) — lets a tool remember its own outputs.
88
90
  */
89
- export async function emit(named, opts, meta) {
91
+ export async function emit(named, opts, meta, onWritten) {
90
92
  if (opts.inline) {
91
93
  const imageBlocks = named.map((n) => ({ type: 'image', data: n.image.base64, mimeType: n.image.mimeType }));
92
94
  if (meta && Object.keys(meta).length > 0) {
@@ -100,5 +102,6 @@ export async function emit(named, opts, meta) {
100
102
  const images = [];
101
103
  for (const n of named)
102
104
  images.push(await writeImage(dir, n.base, n.image.base64, n.image.mimeType));
105
+ onWritten?.(images);
103
106
  return textResult({ images, ...meta });
104
107
  }
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  /** Single source of the server version. release-please bumps the literal below. */
2
- export const VERSION = '0.7.1'; // x-release-please-version
2
+ export const VERSION = '0.7.2'; // x-release-please-version
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chrischall/gemini-mcp",
3
- "version": "0.7.1",
3
+ "version": "0.7.2",
4
4
  "mcpName": "io.github.chrischall/gemini-mcp",
5
5
  "description": "Gemini image-generation MCP server for Claude — developed and maintained by AI (Claude Code)",
6
6
  "author": "Claude Code (AI) <https://www.anthropic.com/claude>",
package/server.json CHANGED
@@ -6,12 +6,12 @@
6
6
  "url": "https://github.com/chrischall/gemini-mcp",
7
7
  "source": "github"
8
8
  },
9
- "version": "0.7.1",
9
+ "version": "0.7.2",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "@chrischall/gemini-mcp",
14
- "version": "0.7.1",
14
+ "version": "0.7.2",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },