@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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/dist/bundle.js +32 -8
- package/dist/images.js +4 -2
- package/dist/tools/interact.js +45 -6
- package/dist/tools/shared.js +4 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/server.json +2 -2
|
@@ -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.
|
|
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.
|
|
18
|
+
"version": "0.7.2",
|
|
19
19
|
"author": {
|
|
20
20
|
"name": "Chris Hall"
|
|
21
21
|
},
|
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.
|
|
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(
|
|
33203
|
-
images_base64: external_exports.array(external_exports.string().min(1)).optional().describe(
|
|
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
|
-
|
|
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.
|
|
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
|
-
*
|
|
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))
|
package/dist/tools/interact.js
CHANGED
|
@@ -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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/dist/tools/shared.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
9
|
+
"version": "0.7.2",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "@chrischall/gemini-mcp",
|
|
14
|
-
"version": "0.7.
|
|
14
|
+
"version": "0.7.2",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|