@chrischall/gemini-mcp 0.2.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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +1 -0
- package/SKILL.md +43 -10
- package/dist/bundle.js +293 -50
- package/dist/client.js +124 -7
- package/dist/images.js +63 -6
- package/dist/index.js +2 -1
- package/dist/tools/generate.js +60 -10
- package/dist/tools/interact.js +89 -0
- package/dist/tools/set.js +30 -10
- package/dist/tools/shared.js +33 -8
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/server.json +2 -2
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 =
|
|
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
|
|
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
|
|
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((
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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(
|
|
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((
|
|
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((
|
|
30940
|
+
return new Promise((resolve2) => {
|
|
30941
30941
|
const json2 = serializeMessage(message);
|
|
30942
30942
|
if (this._stdout.write(json2)) {
|
|
30943
|
-
|
|
30943
|
+
resolve2();
|
|
30944
30944
|
} else {
|
|
30945
|
-
this._stdout.once("drain",
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
31169
|
+
requestBody
|
|
31162
31170
|
);
|
|
31163
|
-
const
|
|
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)
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
31249
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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)).
|
|
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
|
|
31300
|
-
const
|
|
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
|
-
|
|
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
|
|
31332
|
-
const
|
|
31333
|
-
const
|
|
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
|
-
|
|
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
|
});
|