@chrischall/gemini-mcp 1.8.0 → 1.10.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/dist/bundle.js +137 -74
- package/dist/tools/generate.js +17 -7
- package/dist/tools/interact.js +13 -5
- package/dist/tools/set.js +9 -4
- package/dist/tools/shared.js +85 -4
- package/dist/tools/video.js +12 -5
- package/dist/version.js +1 -1
- package/mint.yaml +57 -0
- package/package.json +4 -3
- 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": "1.
|
|
10
|
+
"version": "1.10.0"
|
|
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": "1.
|
|
18
|
+
"version": "1.10.0",
|
|
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": "1.
|
|
4
|
+
"version": "1.10.0",
|
|
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
|
@@ -32099,59 +32099,6 @@ var init_stdio2 = __esm({
|
|
|
32099
32099
|
}
|
|
32100
32100
|
});
|
|
32101
32101
|
|
|
32102
|
-
// node_modules/@chrischall/mcp-utils/dist/server/index.js
|
|
32103
|
-
async function createMcpServer(opts) {
|
|
32104
|
-
const server = new McpServer({ name: opts.name, version: opts.version });
|
|
32105
|
-
if (opts.banner !== void 0) {
|
|
32106
|
-
console.error(opts.banner);
|
|
32107
|
-
}
|
|
32108
|
-
const deps = opts.deps;
|
|
32109
|
-
for (const register of opts.tools) {
|
|
32110
|
-
await register(server, deps);
|
|
32111
|
-
}
|
|
32112
|
-
return server;
|
|
32113
|
-
}
|
|
32114
|
-
function withGracefulShutdown(server, opts = {}) {
|
|
32115
|
-
const shouldExit = opts.exit ?? true;
|
|
32116
|
-
let shuttingDown = false;
|
|
32117
|
-
const handler = (signal) => {
|
|
32118
|
-
if (shuttingDown)
|
|
32119
|
-
return;
|
|
32120
|
-
shuttingDown = true;
|
|
32121
|
-
void (async () => {
|
|
32122
|
-
try {
|
|
32123
|
-
if (opts.onSignal)
|
|
32124
|
-
await opts.onSignal(signal);
|
|
32125
|
-
await server.close();
|
|
32126
|
-
} catch (err) {
|
|
32127
|
-
console.error(`[mcp-utils] error during graceful shutdown on ${signal}: ${err instanceof Error ? err.message : String(err)}`);
|
|
32128
|
-
} finally {
|
|
32129
|
-
if (shouldExit)
|
|
32130
|
-
process.exit(0);
|
|
32131
|
-
}
|
|
32132
|
-
})();
|
|
32133
|
-
};
|
|
32134
|
-
process.on("SIGINT", () => handler("SIGINT"));
|
|
32135
|
-
process.on("SIGTERM", () => handler("SIGTERM"));
|
|
32136
|
-
}
|
|
32137
|
-
async function runMcp(opts) {
|
|
32138
|
-
const server = await createMcpServer(opts);
|
|
32139
|
-
const shutdown = opts.shutdown ?? true;
|
|
32140
|
-
if (shutdown !== false) {
|
|
32141
|
-
withGracefulShutdown(server, shutdown === true ? {} : shutdown);
|
|
32142
|
-
}
|
|
32143
|
-
const spec = opts.transport ?? "stdio";
|
|
32144
|
-
const transport = spec === "stdio" ? new StdioServerTransport() : spec;
|
|
32145
|
-
await server.connect(transport);
|
|
32146
|
-
return server;
|
|
32147
|
-
}
|
|
32148
|
-
var init_server3 = __esm({
|
|
32149
|
-
"node_modules/@chrischall/mcp-utils/dist/server/index.js"() {
|
|
32150
|
-
init_mcp();
|
|
32151
|
-
init_stdio2();
|
|
32152
|
-
}
|
|
32153
|
-
});
|
|
32154
|
-
|
|
32155
32102
|
// node_modules/@chrischall/mcp-utils/dist/errors/index.js
|
|
32156
32103
|
function redactSecrets(text) {
|
|
32157
32104
|
return text.replace(BEARER_RE, "$1[REDACTED]").replace(BASIC_AUTH_RE, "$1[REDACTED]").replace(SET_COOKIE_RE, "$1$2=[REDACTED]").replace(COOKIE_HEADER_RE, (_m, prefix, pairs) => `${prefix}${pairs.replace(/=[^;,\s]*/g, "=[REDACTED]")}`).replace(API_KEY_RE, "[REDACTED]").replace(QUERY_SECRET_RE, "$1[REDACTED]").replace(AWS_SIGV4_RE, "$1[REDACTED]").replace(JSON_SECRET_DQ_RE, "$1[REDACTED]$2").replace(JSON_SECRET_SQ_RE, "$1[REDACTED]$2").replace(JWT_RE, "[REDACTED]");
|
|
@@ -32211,12 +32158,95 @@ function textResult(data) {
|
|
|
32211
32158
|
content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
|
|
32212
32159
|
};
|
|
32213
32160
|
}
|
|
32161
|
+
function errorResult(message) {
|
|
32162
|
+
return {
|
|
32163
|
+
content: [{ type: "text", text: redactSecrets(message) }],
|
|
32164
|
+
isError: true
|
|
32165
|
+
};
|
|
32166
|
+
}
|
|
32214
32167
|
var init_response = __esm({
|
|
32215
32168
|
"node_modules/@chrischall/mcp-utils/dist/response/index.js"() {
|
|
32216
32169
|
init_errors4();
|
|
32217
32170
|
}
|
|
32218
32171
|
});
|
|
32219
32172
|
|
|
32173
|
+
// node_modules/@chrischall/mcp-utils/dist/server/index.js
|
|
32174
|
+
function hintResultOrRethrow(err) {
|
|
32175
|
+
if (err instanceof McpToolError && err.hint) {
|
|
32176
|
+
return errorResult(`${err.message}
|
|
32177
|
+
|
|
32178
|
+
Hint: ${err.hint}`);
|
|
32179
|
+
}
|
|
32180
|
+
throw err;
|
|
32181
|
+
}
|
|
32182
|
+
function surfaceToolHints(server) {
|
|
32183
|
+
const register = server.registerTool.bind(server);
|
|
32184
|
+
server.registerTool = (name, config2, cb) => register(name, config2, (...args) => {
|
|
32185
|
+
let result;
|
|
32186
|
+
try {
|
|
32187
|
+
result = cb(...args);
|
|
32188
|
+
} catch (err) {
|
|
32189
|
+
return hintResultOrRethrow(err);
|
|
32190
|
+
}
|
|
32191
|
+
return result instanceof Promise ? result.catch(hintResultOrRethrow) : result;
|
|
32192
|
+
});
|
|
32193
|
+
}
|
|
32194
|
+
async function createMcpServer(opts) {
|
|
32195
|
+
const server = new McpServer({ name: opts.name, version: opts.version });
|
|
32196
|
+
if (opts.surfaceHints !== false)
|
|
32197
|
+
surfaceToolHints(server);
|
|
32198
|
+
if (opts.banner !== void 0) {
|
|
32199
|
+
console.error(opts.banner);
|
|
32200
|
+
}
|
|
32201
|
+
const deps = opts.deps;
|
|
32202
|
+
for (const register of opts.tools) {
|
|
32203
|
+
await register(server, deps);
|
|
32204
|
+
}
|
|
32205
|
+
return server;
|
|
32206
|
+
}
|
|
32207
|
+
function withGracefulShutdown(server, opts = {}) {
|
|
32208
|
+
const shouldExit = opts.exit ?? true;
|
|
32209
|
+
let shuttingDown = false;
|
|
32210
|
+
const handler = (signal) => {
|
|
32211
|
+
if (shuttingDown)
|
|
32212
|
+
return;
|
|
32213
|
+
shuttingDown = true;
|
|
32214
|
+
void (async () => {
|
|
32215
|
+
try {
|
|
32216
|
+
if (opts.onSignal)
|
|
32217
|
+
await opts.onSignal(signal);
|
|
32218
|
+
await server.close();
|
|
32219
|
+
} catch (err) {
|
|
32220
|
+
console.error(`[mcp-utils] error during graceful shutdown on ${signal}: ${err instanceof Error ? err.message : String(err)}`);
|
|
32221
|
+
} finally {
|
|
32222
|
+
if (shouldExit)
|
|
32223
|
+
process.exit(0);
|
|
32224
|
+
}
|
|
32225
|
+
})();
|
|
32226
|
+
};
|
|
32227
|
+
process.on("SIGINT", () => handler("SIGINT"));
|
|
32228
|
+
process.on("SIGTERM", () => handler("SIGTERM"));
|
|
32229
|
+
}
|
|
32230
|
+
async function runMcp(opts) {
|
|
32231
|
+
const server = await createMcpServer(opts);
|
|
32232
|
+
const shutdown = opts.shutdown ?? true;
|
|
32233
|
+
if (shutdown !== false) {
|
|
32234
|
+
withGracefulShutdown(server, shutdown === true ? {} : shutdown);
|
|
32235
|
+
}
|
|
32236
|
+
const spec = opts.transport ?? "stdio";
|
|
32237
|
+
const transport = spec === "stdio" ? new StdioServerTransport() : spec;
|
|
32238
|
+
await server.connect(transport);
|
|
32239
|
+
return server;
|
|
32240
|
+
}
|
|
32241
|
+
var init_server3 = __esm({
|
|
32242
|
+
"node_modules/@chrischall/mcp-utils/dist/server/index.js"() {
|
|
32243
|
+
init_mcp();
|
|
32244
|
+
init_stdio2();
|
|
32245
|
+
init_errors4();
|
|
32246
|
+
init_response();
|
|
32247
|
+
}
|
|
32248
|
+
});
|
|
32249
|
+
|
|
32220
32250
|
// node_modules/@chrischall/mcp-utils/dist/config/index.js
|
|
32221
32251
|
function readEnvVar(key, opts = {}) {
|
|
32222
32252
|
const env = opts.env ?? process.env;
|
|
@@ -32838,7 +32868,7 @@ async function loadStdioDotenv() {
|
|
|
32838
32868
|
}
|
|
32839
32869
|
|
|
32840
32870
|
// src/version.ts
|
|
32841
|
-
var VERSION = "1.
|
|
32871
|
+
var VERSION = "1.10.0";
|
|
32842
32872
|
|
|
32843
32873
|
// src/errors.ts
|
|
32844
32874
|
init_dist();
|
|
@@ -33583,6 +33613,27 @@ function buildZip(entries, modifiedAt = new Date(Date.UTC(1980, 0, 1))) {
|
|
|
33583
33613
|
var DEFAULT_SINK = createDiskSink();
|
|
33584
33614
|
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"];
|
|
33585
33615
|
var IMAGE_SIZES = ["512", "1K", "2K", "4K"];
|
|
33616
|
+
var ORIENTATIONS = ["landscape", "portrait", "square"];
|
|
33617
|
+
var ORIENTATION_RATIO = {
|
|
33618
|
+
landscape: "16:9",
|
|
33619
|
+
portrait: "9:16",
|
|
33620
|
+
square: "1:1"
|
|
33621
|
+
};
|
|
33622
|
+
function resolveAspectRatio(opts, supported) {
|
|
33623
|
+
if (opts.aspect_ratio) return opts.aspect_ratio;
|
|
33624
|
+
if (!opts.orientation) return void 0;
|
|
33625
|
+
const ratio = ORIENTATION_RATIO[opts.orientation];
|
|
33626
|
+
if (!supported.includes(ratio)) {
|
|
33627
|
+
throw new McpToolError(
|
|
33628
|
+
`orientation "${opts.orientation}" (${ratio}) is not available for this model, which supports ${supported.join(" and ")}. Ask for ${supported.map((r) => `"${r}"`).join(" or ")} via aspect_ratio, or choose another orientation.`,
|
|
33629
|
+
{ hint: `Supported aspect ratios: ${supported.join(", ")}.` }
|
|
33630
|
+
);
|
|
33631
|
+
}
|
|
33632
|
+
return ratio;
|
|
33633
|
+
}
|
|
33634
|
+
var orientationSchema = external_exports.enum(ORIENTATIONS).optional().describe(
|
|
33635
|
+
'Shape of the output, in plain terms: "landscape" (wide, 16:9), "portrait" (tall, 9:16) or "square" (1:1). Use this for a request phrased as landscape/portrait/vertical/horizontal. For any other proportion \u2014 35mm photo (3:2), print (4:3), social (4:5), cinematic (21:9) \u2014 name it with aspect_ratio instead, which overrides this when both are given.'
|
|
33636
|
+
);
|
|
33586
33637
|
var MODEL_CHOICE_GUIDE = "gemini-3.1-flash-image (Nano Banana 2) is the versatile generalist workhorse \u2014 balances speed with state-of-the-art 4K generation, world knowledge, and reliable text rendering; excels at multi-reference-image processing and consistency. gemini-3-pro-image (Nano Banana Pro) is the premium choice for the most complex visual tasks \u2014 highest world knowledge, advanced localization, accurate brand consistency, precision creative control. gemini-3.1-flash-lite-image (Nano Banana 2 Lite) is the fastest/cheapest for simple tasks (1K only, no search grounding).";
|
|
33587
33638
|
var timeoutMsSchema = external_exports.number().int().positive().optional().describe(
|
|
33588
33639
|
"Upstream request timeout in ms for this call (default: $GEMINI_TIMEOUT_MS, else 60000 \u2014 or 120000 when image_size is 4K, which routinely runs past 60s)"
|
|
@@ -33679,7 +33730,8 @@ var sharedImageSchema = {
|
|
|
33679
33730
|
// (`/models/${model}:generateContent`), so slashes/colons/queries must be
|
|
33680
33731
|
// rejected to keep a crafted value from escaping the path segment.
|
|
33681
33732
|
model: external_exports.string().regex(/^[\w.-]+$/, 'must be a bare model id (letters, digits, ".", "_", "-")').optional().describe(`Model id override (default: server default; see gemini_list_models). ${MODEL_CHOICE_GUIDE}`),
|
|
33682
|
-
aspect_ratio: external_exports.enum(ASPECT_RATIOS).optional().describe("
|
|
33733
|
+
aspect_ratio: external_exports.enum(ASPECT_RATIOS).optional().describe("Exact output aspect ratio. For a plain landscape/portrait/square request, `orientation` is the shorthand; this wins if both are given."),
|
|
33734
|
+
orientation: orientationSchema,
|
|
33683
33735
|
image_size: external_exports.enum(IMAGE_SIZES).optional().describe("Output resolution (512 = 0.5K, Flash-only)"),
|
|
33684
33736
|
output_dir: external_exports.string().optional().describe("Directory to write images to (default: $GEMINI_OUTPUT_DIR or cwd)"),
|
|
33685
33737
|
inline: external_exports.boolean().optional().describe("Return base64 images inline instead of writing to disk"),
|
|
@@ -33761,11 +33813,15 @@ function timeoutRiskHint(opts) {
|
|
|
33761
33813
|
return lead + `If the host gives up mid-generation the image is still written to disk (and gemini_interact leaves an <image>.json sidecar with the interaction id) \u2014 no rerun needed, just look in the output dir. For a faster in-band result use a Flash model, drop image_size to 1K/2K, or \u2014 in Claude Code \u2014 set MCP_TOOL_TIMEOUT.`;
|
|
33762
33814
|
}
|
|
33763
33815
|
function buildMeta(model, seed, opts) {
|
|
33764
|
-
const meta3 = { model, seed };
|
|
33765
|
-
if (opts.aspect_ratio) meta3.aspect_ratio = opts.aspect_ratio;
|
|
33816
|
+
const meta3 = reportShape({ model, seed }, opts);
|
|
33766
33817
|
if (opts.image_size) meta3.image_size = opts.image_size;
|
|
33767
33818
|
return meta3;
|
|
33768
33819
|
}
|
|
33820
|
+
function reportShape(meta3, opts) {
|
|
33821
|
+
if (opts.aspect_ratio) meta3.aspect_ratio = opts.aspect_ratio;
|
|
33822
|
+
if (opts.orientation) meta3.orientation = opts.orientation;
|
|
33823
|
+
return meta3;
|
|
33824
|
+
}
|
|
33769
33825
|
async function emitMedia(named, kind, opts, meta3, onWritten) {
|
|
33770
33826
|
const inline = opts.inline && kind !== "video";
|
|
33771
33827
|
if (inline) {
|
|
@@ -34437,10 +34493,11 @@ function registerGenerateTools(server, client2) {
|
|
|
34437
34493
|
if (gate) return gate;
|
|
34438
34494
|
const count = args.count ?? 1;
|
|
34439
34495
|
const model = resolveModel(args.model, readEnvVar("GEMINI_IMAGE_MODEL"));
|
|
34496
|
+
const aspectRatio = resolveAspectRatio(args, ASPECT_RATIOS);
|
|
34440
34497
|
const fingerprint = fingerprintRequest("gemini_image_generate", {
|
|
34441
34498
|
model,
|
|
34442
34499
|
prompt: args.prompt,
|
|
34443
|
-
aspect_ratio:
|
|
34500
|
+
aspect_ratio: aspectRatio,
|
|
34444
34501
|
image_size: args.image_size,
|
|
34445
34502
|
thinking_level: args.thinking_level,
|
|
34446
34503
|
google_search: args.google_search,
|
|
@@ -34476,7 +34533,7 @@ function registerGenerateTools(server, client2) {
|
|
|
34476
34533
|
prompt,
|
|
34477
34534
|
images: refInputs.length > 0 ? refInputs : void 0,
|
|
34478
34535
|
model: args.model,
|
|
34479
|
-
aspectRatio
|
|
34536
|
+
aspectRatio,
|
|
34480
34537
|
imageSize: args.image_size,
|
|
34481
34538
|
seed: seed + i,
|
|
34482
34539
|
thinkingLevel: args.thinking_level,
|
|
@@ -34496,7 +34553,7 @@ function registerGenerateTools(server, client2) {
|
|
|
34496
34553
|
}
|
|
34497
34554
|
});
|
|
34498
34555
|
if (named.length === 0) throw firstError;
|
|
34499
|
-
const meta3 = buildMeta(model, seed, args);
|
|
34556
|
+
const meta3 = buildMeta(model, seed, { ...args, aspect_ratio: aspectRatio });
|
|
34500
34557
|
if (capturedText) meta3.text = capturedText;
|
|
34501
34558
|
if (capturedGrounding) meta3.grounding = capturedGrounding;
|
|
34502
34559
|
if (report) meta3.image_inputs = report;
|
|
@@ -34538,10 +34595,11 @@ function registerGenerateTools(server, client2) {
|
|
|
34538
34595
|
const gate = await previewLocalInputsUnlessConfirmed(args.confirm, SEND_LOCAL_ACTION, GENERATE_ENDPOINT, args.images);
|
|
34539
34596
|
if (gate) return gate;
|
|
34540
34597
|
const model = resolveModel(args.model, readEnvVar("GEMINI_IMAGE_MODEL"));
|
|
34598
|
+
const aspectRatio = resolveAspectRatio(args, ASPECT_RATIOS);
|
|
34541
34599
|
const fingerprint = fingerprintRequest("gemini_image_edit", {
|
|
34542
34600
|
model,
|
|
34543
34601
|
prompt: args.prompt,
|
|
34544
|
-
aspect_ratio:
|
|
34602
|
+
aspect_ratio: aspectRatio,
|
|
34545
34603
|
image_size: args.image_size,
|
|
34546
34604
|
thinking_level: args.thinking_level,
|
|
34547
34605
|
google_search: args.google_search,
|
|
@@ -34565,7 +34623,7 @@ function registerGenerateTools(server, client2) {
|
|
|
34565
34623
|
prompt,
|
|
34566
34624
|
images: inputs,
|
|
34567
34625
|
model: args.model,
|
|
34568
|
-
aspectRatio
|
|
34626
|
+
aspectRatio,
|
|
34569
34627
|
imageSize: args.image_size,
|
|
34570
34628
|
seed,
|
|
34571
34629
|
thinkingLevel: args.thinking_level,
|
|
@@ -34573,7 +34631,7 @@ function registerGenerateTools(server, client2) {
|
|
|
34573
34631
|
timeoutMs: args.timeout_ms
|
|
34574
34632
|
}));
|
|
34575
34633
|
const { images: [img], text } = result;
|
|
34576
|
-
const meta3 = buildMeta(model, seed, args);
|
|
34634
|
+
const meta3 = buildMeta(model, seed, { ...args, aspect_ratio: aspectRatio });
|
|
34577
34635
|
if (text) meta3.text = text;
|
|
34578
34636
|
if (result.grounding) meta3.grounding = result.grounding;
|
|
34579
34637
|
if (report) meta3.image_inputs = report;
|
|
@@ -34622,13 +34680,14 @@ function registerSetTools(server, client2) {
|
|
|
34622
34680
|
const gate = await previewLocalInputsUnlessConfirmed(args.confirm, "Send local master image input(s) to the Gemini API", "/v1beta/models/{model}:generateContent", args.master_images);
|
|
34623
34681
|
if (gate) return gate;
|
|
34624
34682
|
const model = resolveModel(args.model, readEnvVar("GEMINI_IMAGE_MODEL"));
|
|
34683
|
+
const aspectRatio = resolveAspectRatio(args, ASPECT_RATIOS);
|
|
34625
34684
|
const fingerprint = fingerprintRequest("gemini_image_set", {
|
|
34626
34685
|
model,
|
|
34627
34686
|
master_prompt: args.master_prompt,
|
|
34628
34687
|
scenes: args.scenes,
|
|
34629
34688
|
count: args.count,
|
|
34630
34689
|
reference_mode: args.reference_mode,
|
|
34631
|
-
aspect_ratio:
|
|
34690
|
+
aspect_ratio: aspectRatio,
|
|
34632
34691
|
image_size: args.image_size,
|
|
34633
34692
|
thinking_level: args.thinking_level,
|
|
34634
34693
|
google_search: args.google_search,
|
|
@@ -34643,7 +34702,7 @@ function registerSetTools(server, client2) {
|
|
|
34643
34702
|
});
|
|
34644
34703
|
return client2.session.jobs.dispatch({ toolName: "gemini_image_set", fingerprint, idempotencyKey: args.idempotency_key, async: args.async, waitMs: args.max_wait_ms }, async () => {
|
|
34645
34704
|
const seed = pickSeed(args.seed);
|
|
34646
|
-
const cfg = { model: args.model, aspectRatio
|
|
34705
|
+
const cfg = { model: args.model, aspectRatio, imageSize: args.image_size, thinkingLevel: args.thinking_level, googleSearch: args.google_search, timeoutMs: args.timeout_ms };
|
|
34647
34706
|
const slug = args.basename ? baseName(args.basename) : slugify2(args.master_prompt);
|
|
34648
34707
|
const charRefs = await resolveCharacterRefs(client2, args.characters, args.style);
|
|
34649
34708
|
const masterPrompt = composePrompt(args.master_prompt, charRefs);
|
|
@@ -34703,7 +34762,7 @@ function registerSetTools(server, client2) {
|
|
|
34703
34762
|
return result;
|
|
34704
34763
|
});
|
|
34705
34764
|
const masterText = masterResult.text;
|
|
34706
|
-
const meta3 = buildMeta(model, seed, args);
|
|
34765
|
+
const meta3 = buildMeta(model, seed, { ...args, aspect_ratio: aspectRatio });
|
|
34707
34766
|
if (masterText) meta3.text = masterText;
|
|
34708
34767
|
if (masterResult.grounding) meta3.grounding = masterResult.grounding;
|
|
34709
34768
|
if (report) meta3.image_inputs = report;
|
|
@@ -36028,7 +36087,8 @@ function registerInteractTools(server, client2) {
|
|
|
36028
36087
|
`Reference images as base64 strings or data URIs. Last resort: prefer images_url or images_file_uris, which keep image bytes out of the conversation. ${NEW_REFERENCES_ONLY}`
|
|
36029
36088
|
),
|
|
36030
36089
|
model: external_exports.string().optional().describe(`Model id override (default: server default; see gemini_list_models). ${MODEL_CHOICE_GUIDE}`),
|
|
36031
|
-
aspect_ratio: external_exports.enum(ASPECT_RATIOS).optional().describe("
|
|
36090
|
+
aspect_ratio: external_exports.enum(ASPECT_RATIOS).optional().describe("Exact output aspect ratio. For a plain landscape/portrait/square request, `orientation` is the shorthand; this wins if both are given."),
|
|
36091
|
+
orientation: orientationSchema,
|
|
36032
36092
|
image_size: external_exports.enum(IMAGE_SIZES).optional().describe("Output resolution (512 = 0.5K, Flash-only)"),
|
|
36033
36093
|
thinking_level: external_exports.enum(["minimal", "high"]).optional().describe("Reasoning depth; higher can help complex/structural edits"),
|
|
36034
36094
|
filename: external_exports.string().optional().describe("Base filename for the output image (extension stripped; default: slugified input)"),
|
|
@@ -36073,10 +36133,11 @@ function registerInteractTools(server, client2) {
|
|
|
36073
36133
|
const gate = await previewLocalInputsUnlessConfirmed(args.confirm, "Send local file input(s) to the Gemini Interactions API", "/v1beta/interactions", images, args.video_path);
|
|
36074
36134
|
if (gate) return gate;
|
|
36075
36135
|
const model = resolveModel(args.model, readEnvVar("GEMINI_IMAGE_MODEL"));
|
|
36136
|
+
const aspectRatio = resolveAspectRatio(args, ASPECT_RATIOS);
|
|
36076
36137
|
const fingerprint = fingerprintRequest("gemini_interact", {
|
|
36077
36138
|
model,
|
|
36078
36139
|
input: args.input,
|
|
36079
|
-
aspect_ratio:
|
|
36140
|
+
aspect_ratio: aspectRatio,
|
|
36080
36141
|
image_size: args.image_size,
|
|
36081
36142
|
thinking_level: args.thinking_level,
|
|
36082
36143
|
previous_interaction_id: previousInteractionId,
|
|
@@ -36096,7 +36157,7 @@ function registerInteractTools(server, client2) {
|
|
|
36096
36157
|
const callOpts = {
|
|
36097
36158
|
input: args.input,
|
|
36098
36159
|
model: args.model,
|
|
36099
|
-
aspectRatio
|
|
36160
|
+
aspectRatio,
|
|
36100
36161
|
imageSize: args.image_size,
|
|
36101
36162
|
thinkingLevel: args.thinking_level,
|
|
36102
36163
|
googleSearch: args.google_search,
|
|
@@ -36136,7 +36197,7 @@ function registerInteractTools(server, client2) {
|
|
|
36136
36197
|
}
|
|
36137
36198
|
});
|
|
36138
36199
|
client2.session.lastInteractionId = r.id;
|
|
36139
|
-
const meta3 = { model, interaction_id: r.id };
|
|
36200
|
+
const meta3 = reportShape({ model, interaction_id: r.id }, { ...args, aspect_ratio: aspectRatio });
|
|
36140
36201
|
if (chainRecovered) meta3.chain_recovered = chainRecovered;
|
|
36141
36202
|
else if (previousInteractionId) meta3.previous_interaction_id = previousInteractionId;
|
|
36142
36203
|
if (continuedFromSidecar) meta3.continued_from_sidecar = true;
|
|
@@ -36181,7 +36242,8 @@ function registerVideoTools(server, client2) {
|
|
|
36181
36242
|
annotations: { readOnlyHint: false, openWorldHint: true },
|
|
36182
36243
|
inputSchema: {
|
|
36183
36244
|
prompt: external_exports.string().min(1).describe("Description of the video to generate (or the edit instruction when task=edit)"),
|
|
36184
|
-
aspect_ratio: external_exports.enum(VIDEO_ASPECT_RATIOS).optional().describe("
|
|
36245
|
+
aspect_ratio: external_exports.enum(VIDEO_ASPECT_RATIOS).optional().describe("Exact output aspect ratio (omni: 16:9 or 9:16). `orientation` is the plain-language shorthand; this wins if both are given."),
|
|
36246
|
+
orientation: orientationSchema,
|
|
36185
36247
|
task: external_exports.enum(VIDEO_TASKS).optional().describe("text_to_video (default), image_to_video / reference_to_video (need image input), or edit (needs previous_interaction_id)"),
|
|
36186
36248
|
images: external_exports.array(external_exports.string().min(1)).optional().describe("Reference image path(s) for image_to_video / reference_to_video"),
|
|
36187
36249
|
images_url: imagesUrlSchema("Reference stills"),
|
|
@@ -36216,10 +36278,11 @@ function registerVideoTools(server, client2) {
|
|
|
36216
36278
|
const gate = await previewLocalInputsUnlessConfirmed(args.confirm, "Send local image input(s) to the Gemini omni API", "/v1beta/interactions", args.images);
|
|
36217
36279
|
if (gate) return gate;
|
|
36218
36280
|
const model = args.model?.trim() || DEFAULT_VIDEO_MODEL;
|
|
36281
|
+
const aspectRatio = resolveAspectRatio(args, VIDEO_ASPECT_RATIOS);
|
|
36219
36282
|
const fingerprint = fingerprintRequest("gemini_video_generate", {
|
|
36220
36283
|
model,
|
|
36221
36284
|
prompt: args.prompt,
|
|
36222
|
-
aspect_ratio:
|
|
36285
|
+
aspect_ratio: aspectRatio,
|
|
36223
36286
|
task: args.task,
|
|
36224
36287
|
images: args.images,
|
|
36225
36288
|
images_base64: args.images_base64,
|
|
@@ -36234,7 +36297,7 @@ function registerVideoTools(server, client2) {
|
|
|
36234
36297
|
input: args.prompt,
|
|
36235
36298
|
images: inputs.length ? inputs : void 0,
|
|
36236
36299
|
model: args.model,
|
|
36237
|
-
aspectRatio
|
|
36300
|
+
aspectRatio,
|
|
36238
36301
|
task: args.task,
|
|
36239
36302
|
previousInteractionId,
|
|
36240
36303
|
timeoutMs: args.timeout_ms,
|
|
@@ -36246,7 +36309,7 @@ function registerVideoTools(server, client2) {
|
|
|
36246
36309
|
onInteractionStarted: ctx.reportInteraction
|
|
36247
36310
|
}));
|
|
36248
36311
|
client2.session.lastVideoInteractionId = r.id;
|
|
36249
|
-
const meta3 = { model, interaction_id: r.id };
|
|
36312
|
+
const meta3 = reportShape({ model, interaction_id: r.id }, { ...args, aspect_ratio: aspectRatio });
|
|
36250
36313
|
if (previousInteractionId) meta3.previous_interaction_id = previousInteractionId;
|
|
36251
36314
|
if (r.text) meta3.text = r.text;
|
|
36252
36315
|
if (report) meta3.image_inputs = report;
|
package/dist/tools/generate.js
CHANGED
|
@@ -3,7 +3,7 @@ import { readEnvVar } from '@chrischall/mcp-utils';
|
|
|
3
3
|
import { resolveModel } from '../models.js';
|
|
4
4
|
import { slugify, baseName } from '../images.js';
|
|
5
5
|
import { resolveImageInputs, requireImageInput } from '../inputs.js';
|
|
6
|
-
import { emit, sharedImageSchema, pickSeed, buildMeta, timeoutRiskHint, resolveVideoInput, videoPathSchema, withProgressHeartbeat, assertLocalInputsAvailable, imagesUrlSchema, imagesFileUrisSchema, imagesR2KeysSchema, charactersSchema, styleSchema, resolveCharacterRefs, composePrompt } from './shared.js';
|
|
6
|
+
import { emit, resolveAspectRatio, ASPECT_RATIOS, sharedImageSchema, pickSeed, buildMeta, timeoutRiskHint, resolveVideoInput, videoPathSchema, withProgressHeartbeat, assertLocalInputsAvailable, imagesUrlSchema, imagesFileUrisSchema, imagesR2KeysSchema, charactersSchema, styleSchema, resolveCharacterRefs, composePrompt } from './shared.js';
|
|
7
7
|
import { fingerprintRequest } from '../jobs.js';
|
|
8
8
|
import { previewLocalInputsUnlessConfirmed, schemaConfirm } from './_confirm.js';
|
|
9
9
|
const GENERATE_ENDPOINT = '/v1beta/models/{model}:generateContent';
|
|
@@ -46,10 +46,15 @@ export function registerGenerateTools(server, client) {
|
|
|
46
46
|
return gate;
|
|
47
47
|
const count = args.count ?? 1;
|
|
48
48
|
const model = resolveModel(args.model, readEnvVar('GEMINI_IMAGE_MODEL'));
|
|
49
|
+
// Resolve shape BEFORE fingerprinting: `orientation: 'portrait'` and
|
|
50
|
+
// `aspect_ratio: '9:16'` are the same upstream request, so they must
|
|
51
|
+
// dedup to the same job rather than billing twice (hence the resolved
|
|
52
|
+
// ratio in the fingerprint below, and no `orientation` alongside it).
|
|
53
|
+
const aspectRatio = resolveAspectRatio(args, ASPECT_RATIOS);
|
|
49
54
|
// Fingerprint the request identity (excl. server-picked seed / output path)
|
|
50
55
|
// so a blind re-issue after a host timeout dedups instead of re-billing.
|
|
51
56
|
const fingerprint = fingerprintRequest('gemini_image_generate', {
|
|
52
|
-
model, prompt: args.prompt, aspect_ratio:
|
|
57
|
+
model, prompt: args.prompt, aspect_ratio: aspectRatio, image_size: args.image_size,
|
|
53
58
|
thinking_level: args.thinking_level, google_search: args.google_search, count,
|
|
54
59
|
images: args.images, images_base64: args.images_base64, from_clipboard: args.from_clipboard,
|
|
55
60
|
images_url: args.images_url, images_file_uris: args.images_file_uris, images_r2_keys: args.images_r2_keys,
|
|
@@ -92,7 +97,7 @@ export function registerGenerateTools(server, client) {
|
|
|
92
97
|
prompt,
|
|
93
98
|
images: refInputs.length > 0 ? refInputs : undefined,
|
|
94
99
|
model: args.model,
|
|
95
|
-
aspectRatio
|
|
100
|
+
aspectRatio,
|
|
96
101
|
imageSize: args.image_size,
|
|
97
102
|
seed: seed + i,
|
|
98
103
|
thinkingLevel: args.thinking_level,
|
|
@@ -116,7 +121,7 @@ export function registerGenerateTools(server, client) {
|
|
|
116
121
|
});
|
|
117
122
|
if (named.length === 0)
|
|
118
123
|
throw firstError;
|
|
119
|
-
const meta = buildMeta(model, seed, args);
|
|
124
|
+
const meta = buildMeta(model, seed, { ...args, aspect_ratio: aspectRatio });
|
|
120
125
|
if (capturedText)
|
|
121
126
|
meta.text = capturedText;
|
|
122
127
|
if (capturedGrounding)
|
|
@@ -171,8 +176,13 @@ export function registerGenerateTools(server, client) {
|
|
|
171
176
|
if (gate)
|
|
172
177
|
return gate;
|
|
173
178
|
const model = resolveModel(args.model, readEnvVar('GEMINI_IMAGE_MODEL'));
|
|
179
|
+
// Resolve shape BEFORE fingerprinting: `orientation: 'portrait'` and
|
|
180
|
+
// `aspect_ratio: '9:16'` are the same upstream request, so they must
|
|
181
|
+
// dedup to the same job rather than billing twice (hence the resolved
|
|
182
|
+
// ratio in the fingerprint below, and no `orientation` alongside it).
|
|
183
|
+
const aspectRatio = resolveAspectRatio(args, ASPECT_RATIOS);
|
|
174
184
|
const fingerprint = fingerprintRequest('gemini_image_edit', {
|
|
175
|
-
model, prompt: args.prompt, aspect_ratio:
|
|
185
|
+
model, prompt: args.prompt, aspect_ratio: aspectRatio, image_size: args.image_size,
|
|
176
186
|
thinking_level: args.thinking_level, google_search: args.google_search,
|
|
177
187
|
images: args.images, images_base64: args.images_base64, from_clipboard: args.from_clipboard,
|
|
178
188
|
images_url: args.images_url, images_file_uris: args.images_file_uris, images_r2_keys: args.images_r2_keys,
|
|
@@ -189,7 +199,7 @@ export function registerGenerateTools(server, client) {
|
|
|
189
199
|
prompt,
|
|
190
200
|
images: inputs,
|
|
191
201
|
model: args.model,
|
|
192
|
-
aspectRatio
|
|
202
|
+
aspectRatio,
|
|
193
203
|
imageSize: args.image_size,
|
|
194
204
|
seed,
|
|
195
205
|
thinkingLevel: args.thinking_level,
|
|
@@ -197,7 +207,7 @@ export function registerGenerateTools(server, client) {
|
|
|
197
207
|
timeoutMs: args.timeout_ms,
|
|
198
208
|
}));
|
|
199
209
|
const { images: [img], text } = result;
|
|
200
|
-
const meta = buildMeta(model, seed, args);
|
|
210
|
+
const meta = buildMeta(model, seed, { ...args, aspect_ratio: aspectRatio });
|
|
201
211
|
if (text)
|
|
202
212
|
meta.text = text;
|
|
203
213
|
if (result.grounding)
|
package/dist/tools/interact.js
CHANGED
|
@@ -5,7 +5,7 @@ import { ChainedRequest404Error } from '../client.js';
|
|
|
5
5
|
import { slugify, baseName, resolveImagePath, writeSidecar, resolveOutputDir, readImageAsInline } from '../images.js';
|
|
6
6
|
import { resolveImageInputs } from '../inputs.js';
|
|
7
7
|
import { findInteractionImages, latestInteractionId } from '../sidecar.js';
|
|
8
|
-
import { emit, ASPECT_RATIOS, IMAGE_SIZES, MODEL_CHOICE_GUIDE, resolveVideoInput, videoPathSchema, timeoutMsSchema, timeoutRiskHint, idempotencyKeySchema, asyncSchema, maxWaitMsSchema, withProgressHeartbeat, assertLocalInputsAvailable, imagesUrlSchema, imagesFileUrisSchema } from './shared.js';
|
|
8
|
+
import { emit, reportShape, resolveAspectRatio, orientationSchema, ASPECT_RATIOS, IMAGE_SIZES, MODEL_CHOICE_GUIDE, resolveVideoInput, videoPathSchema, timeoutMsSchema, timeoutRiskHint, idempotencyKeySchema, asyncSchema, maxWaitMsSchema, withProgressHeartbeat, assertLocalInputsAvailable, imagesUrlSchema, imagesFileUrisSchema } from './shared.js';
|
|
9
9
|
import { fingerprintRequest } from '../jobs.js';
|
|
10
10
|
import { previewLocalInputsUnlessConfirmed, schemaConfirm } from './_confirm.js';
|
|
11
11
|
// The last-interaction id and the written-outputs set used to live here, at
|
|
@@ -87,7 +87,11 @@ export function registerInteractTools(server, client) {
|
|
|
87
87
|
.string()
|
|
88
88
|
.optional()
|
|
89
89
|
.describe(`Model id override (default: server default; see gemini_list_models). ${MODEL_CHOICE_GUIDE}`),
|
|
90
|
-
aspect_ratio: z
|
|
90
|
+
aspect_ratio: z
|
|
91
|
+
.enum(ASPECT_RATIOS)
|
|
92
|
+
.optional()
|
|
93
|
+
.describe('Exact output aspect ratio. For a plain landscape/portrait/square request, `orientation` is the shorthand; this wins if both are given.'),
|
|
94
|
+
orientation: orientationSchema,
|
|
91
95
|
image_size: z
|
|
92
96
|
.enum(IMAGE_SIZES)
|
|
93
97
|
.optional()
|
|
@@ -173,8 +177,10 @@ export function registerInteractTools(server, client) {
|
|
|
173
177
|
if (gate)
|
|
174
178
|
return gate;
|
|
175
179
|
const model = resolveModel(args.model, readEnvVar('GEMINI_IMAGE_MODEL'));
|
|
180
|
+
// Resolved before the fingerprint so "portrait" and "9:16" are one job.
|
|
181
|
+
const aspectRatio = resolveAspectRatio(args, ASPECT_RATIOS);
|
|
176
182
|
const fingerprint = fingerprintRequest('gemini_interact', {
|
|
177
|
-
model, input: args.input, aspect_ratio:
|
|
183
|
+
model, input: args.input, aspect_ratio: aspectRatio, image_size: args.image_size,
|
|
178
184
|
thinking_level: args.thinking_level, previous_interaction_id: previousInteractionId,
|
|
179
185
|
google_search: args.google_search, search_types: args.search_types,
|
|
180
186
|
images, images_base64: args.images_base64, from_clipboard: args.from_clipboard,
|
|
@@ -187,7 +193,7 @@ export function registerInteractTools(server, client) {
|
|
|
187
193
|
const callOpts = {
|
|
188
194
|
input: args.input,
|
|
189
195
|
model: args.model,
|
|
190
|
-
aspectRatio
|
|
196
|
+
aspectRatio,
|
|
191
197
|
imageSize: args.image_size,
|
|
192
198
|
thinkingLevel: args.thinking_level,
|
|
193
199
|
googleSearch: args.google_search,
|
|
@@ -253,7 +259,9 @@ export function registerInteractTools(server, client) {
|
|
|
253
259
|
}
|
|
254
260
|
});
|
|
255
261
|
client.session.lastInteractionId = r.id;
|
|
256
|
-
|
|
262
|
+
// `aspectRatio` is the RESOLVED value (see resolveAspectRatio above), so
|
|
263
|
+
// an `orientation: 'portrait'` request can be seen to have become 9:16.
|
|
264
|
+
const meta = reportShape({ model, interaction_id: r.id }, { ...args, aspect_ratio: aspectRatio });
|
|
257
265
|
// On recovery the prior id was NOT used — reporting it as
|
|
258
266
|
// previous_interaction_id would misrepresent what the model saw.
|
|
259
267
|
if (chainRecovered)
|
package/dist/tools/set.js
CHANGED
|
@@ -3,7 +3,7 @@ import { McpToolError, readEnvVar } from '@chrischall/mcp-utils';
|
|
|
3
3
|
import { resolveModel } from '../models.js';
|
|
4
4
|
import { slugify, baseName } from '../images.js';
|
|
5
5
|
import { resolveImageInputs } from '../inputs.js';
|
|
6
|
-
import { emit, sharedImageSchema, pickSeed, buildMeta, timeoutRiskHint, withProgressHeartbeat, assertLocalInputsAvailable, imagesUrlSchema, imagesFileUrisSchema, imagesR2KeysSchema, charactersSchema, styleSchema, resolveCharacterRefs, composePrompt, persistBundle, SET_URL_TTL_MS } from './shared.js';
|
|
6
|
+
import { emit, resolveAspectRatio, ASPECT_RATIOS, sharedImageSchema, pickSeed, buildMeta, timeoutRiskHint, withProgressHeartbeat, assertLocalInputsAvailable, imagesUrlSchema, imagesFileUrisSchema, imagesR2KeysSchema, charactersSchema, styleSchema, resolveCharacterRefs, composePrompt, persistBundle, SET_URL_TTL_MS } from './shared.js';
|
|
7
7
|
import { fingerprintRequest } from '../jobs.js';
|
|
8
8
|
import { previewLocalInputsUnlessConfirmed, schemaConfirm } from './_confirm.js';
|
|
9
9
|
export function registerSetTools(server, client) {
|
|
@@ -39,9 +39,14 @@ export function registerSetTools(server, client) {
|
|
|
39
39
|
if (gate)
|
|
40
40
|
return gate;
|
|
41
41
|
const model = resolveModel(args.model, readEnvVar('GEMINI_IMAGE_MODEL'));
|
|
42
|
+
// Resolve shape BEFORE fingerprinting: `orientation: 'portrait'` and
|
|
43
|
+
// `aspect_ratio: '9:16'` are the same upstream request, so they must
|
|
44
|
+
// dedup to the same job rather than billing twice (hence the resolved
|
|
45
|
+
// ratio in the fingerprint below, and no `orientation` alongside it).
|
|
46
|
+
const aspectRatio = resolveAspectRatio(args, ASPECT_RATIOS);
|
|
42
47
|
const fingerprint = fingerprintRequest('gemini_image_set', {
|
|
43
48
|
model, master_prompt: args.master_prompt, scenes: args.scenes, count: args.count,
|
|
44
|
-
reference_mode: args.reference_mode, aspect_ratio:
|
|
49
|
+
reference_mode: args.reference_mode, aspect_ratio: aspectRatio, image_size: args.image_size,
|
|
45
50
|
thinking_level: args.thinking_level, google_search: args.google_search,
|
|
46
51
|
master_images: args.master_images, master_images_base64: args.master_images_base64, from_clipboard: args.from_clipboard,
|
|
47
52
|
master_images_url: args.master_images_url, master_images_file_uris: args.master_images_file_uris,
|
|
@@ -49,7 +54,7 @@ export function registerSetTools(server, client) {
|
|
|
49
54
|
});
|
|
50
55
|
return client.session.jobs.dispatch({ toolName: 'gemini_image_set', fingerprint, idempotencyKey: args.idempotency_key, async: args.async, waitMs: args.max_wait_ms }, async () => {
|
|
51
56
|
const seed = pickSeed(args.seed);
|
|
52
|
-
const cfg = { model: args.model, aspectRatio
|
|
57
|
+
const cfg = { model: args.model, aspectRatio, imageSize: args.image_size, thinkingLevel: args.thinking_level, googleSearch: args.google_search, timeoutMs: args.timeout_ms };
|
|
53
58
|
const slug = args.basename ? baseName(args.basename) : slugify(args.master_prompt);
|
|
54
59
|
// 1. master (optionally seeded from reference images).
|
|
55
60
|
//
|
|
@@ -141,7 +146,7 @@ export function registerSetTools(server, client) {
|
|
|
141
146
|
return result;
|
|
142
147
|
});
|
|
143
148
|
const masterText = masterResult.text;
|
|
144
|
-
const meta = buildMeta(model, seed, args);
|
|
149
|
+
const meta = buildMeta(model, seed, { ...args, aspect_ratio: aspectRatio });
|
|
145
150
|
if (masterText)
|
|
146
151
|
meta.text = masterText;
|
|
147
152
|
if (masterResult.grounding)
|
package/dist/tools/shared.js
CHANGED
|
@@ -16,6 +16,65 @@ const DEFAULT_SINK = createDiskSink();
|
|
|
16
16
|
export const 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'];
|
|
17
17
|
/** Supported output resolutions. `512` is Flash-only (0.5K); `1K`/`2K`/`4K` work on all image models. */
|
|
18
18
|
export const IMAGE_SIZES = ['512', '1K', '2K', '4K'];
|
|
19
|
+
/**
|
|
20
|
+
* The words people actually use for shape, as a shorthand over
|
|
21
|
+
* {@link ASPECT_RATIOS}.
|
|
22
|
+
*
|
|
23
|
+
* Landscape and portrait output was always *possible* — every generation tool
|
|
24
|
+
* has taken `aspect_ratio` since the beginning. But that parameter is a
|
|
25
|
+
* 14-value enum described as "Output aspect ratio", with nothing linking
|
|
26
|
+
* "portrait" to `9:16`, so a request phrased the way a person phrases it did
|
|
27
|
+
* not reliably reach the API. A capability that cannot be addressed is, from
|
|
28
|
+
* the caller's side, a missing one.
|
|
29
|
+
*/
|
|
30
|
+
export const ORIENTATIONS = ['landscape', 'portrait', 'square'];
|
|
31
|
+
/**
|
|
32
|
+
* One mapping, deliberately shared by images and video.
|
|
33
|
+
*
|
|
34
|
+
* `16:9`/`9:16` rather than a photo ratio like `3:2` so that "portrait" means
|
|
35
|
+
* the same shape whatever is being generated — omni offers only these two, and
|
|
36
|
+
* an orientation that silently meant something different per tool would be a
|
|
37
|
+
* worse shorthand than no shorthand. Anyone wanting 35mm or print proportions
|
|
38
|
+
* still names the ratio outright, which always wins (see
|
|
39
|
+
* {@link resolveAspectRatio}).
|
|
40
|
+
*/
|
|
41
|
+
const ORIENTATION_RATIO = {
|
|
42
|
+
landscape: '16:9',
|
|
43
|
+
portrait: '9:16',
|
|
44
|
+
square: '1:1',
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Settle `aspect_ratio` and `orientation` into the one value sent upstream.
|
|
48
|
+
*
|
|
49
|
+
* An explicit ratio wins: it is the more specific request, and a caller who
|
|
50
|
+
* named `21:9` while also saying "landscape" meant `21:9`. `supported` is the
|
|
51
|
+
* calling tool's own enum — the image tools accept all of
|
|
52
|
+
* {@link ASPECT_RATIOS}, omni accepts two — and an orientation outside it is
|
|
53
|
+
* REFUSED rather than rounded to the nearest shape. Quietly substituting
|
|
54
|
+
* `16:9` for an unsupported `square` would hand back a picture the caller
|
|
55
|
+
* never asked for and give them no way to tell it had been swapped.
|
|
56
|
+
*/
|
|
57
|
+
export function resolveAspectRatio(opts, supported) {
|
|
58
|
+
if (opts.aspect_ratio)
|
|
59
|
+
return opts.aspect_ratio;
|
|
60
|
+
if (!opts.orientation)
|
|
61
|
+
return undefined;
|
|
62
|
+
const ratio = ORIENTATION_RATIO[opts.orientation];
|
|
63
|
+
if (!supported.includes(ratio)) {
|
|
64
|
+
// Remediation belongs in the MESSAGE — the host drops `hint`.
|
|
65
|
+
throw new McpToolError(`orientation "${opts.orientation}" (${ratio}) is not available for this model, which supports ${supported.join(' and ')}. ` +
|
|
66
|
+
`Ask for ${supported.map((r) => `"${r}"`).join(' or ')} via aspect_ratio, or choose another orientation.`, { hint: `Supported aspect ratios: ${supported.join(', ')}.` });
|
|
67
|
+
}
|
|
68
|
+
return ratio;
|
|
69
|
+
}
|
|
70
|
+
/** The `orientation` parameter, worded so the shorthand is discoverable. */
|
|
71
|
+
export const orientationSchema = z
|
|
72
|
+
.enum(ORIENTATIONS)
|
|
73
|
+
.optional()
|
|
74
|
+
.describe('Shape of the output, in plain terms: "landscape" (wide, 16:9), "portrait" (tall, 9:16) or "square" (1:1). ' +
|
|
75
|
+
'Use this for a request phrased as landscape/portrait/vertical/horizontal. For any other proportion — ' +
|
|
76
|
+
'35mm photo (3:2), print (4:3), social (4:5), cinematic (21:9) — name it with aspect_ratio instead, ' +
|
|
77
|
+
'which overrides this when both are given.');
|
|
19
78
|
/**
|
|
20
79
|
* When to pick which Nano Banana model — shared by every tool's `model` param
|
|
21
80
|
* so the guidance can't drift between them.
|
|
@@ -228,7 +287,11 @@ export const sharedImageSchema = {
|
|
|
228
287
|
.regex(/^[\w.-]+$/, 'must be a bare model id (letters, digits, ".", "_", "-")')
|
|
229
288
|
.optional()
|
|
230
289
|
.describe(`Model id override (default: server default; see gemini_list_models). ${MODEL_CHOICE_GUIDE}`),
|
|
231
|
-
aspect_ratio: z
|
|
290
|
+
aspect_ratio: z
|
|
291
|
+
.enum(ASPECT_RATIOS)
|
|
292
|
+
.optional()
|
|
293
|
+
.describe('Exact output aspect ratio. For a plain landscape/portrait/square request, `orientation` is the shorthand; this wins if both are given.'),
|
|
294
|
+
orientation: orientationSchema,
|
|
232
295
|
image_size: z.enum(IMAGE_SIZES).optional().describe('Output resolution (512 = 0.5K, Flash-only)'),
|
|
233
296
|
output_dir: z.string().optional().describe('Directory to write images to (default: $GEMINI_OUTPUT_DIR or cwd)'),
|
|
234
297
|
inline: z.boolean().optional().describe('Return base64 images inline instead of writing to disk'),
|
|
@@ -383,13 +446,31 @@ export function timeoutRiskHint(opts) {
|
|
|
383
446
|
}
|
|
384
447
|
/** Build the result metadata echoed to the caller (omitting unset optionals). */
|
|
385
448
|
export function buildMeta(model, seed, opts) {
|
|
386
|
-
const meta = { model, seed };
|
|
387
|
-
if (opts.aspect_ratio)
|
|
388
|
-
meta.aspect_ratio = opts.aspect_ratio;
|
|
449
|
+
const meta = reportShape({ model, seed }, opts);
|
|
389
450
|
if (opts.image_size)
|
|
390
451
|
meta.image_size = opts.image_size;
|
|
391
452
|
return meta;
|
|
392
453
|
}
|
|
454
|
+
/**
|
|
455
|
+
* Record the shape a generation produced, into any meta object.
|
|
456
|
+
*
|
|
457
|
+
* The RESOLVED ratio, and the `orientation` only when one was actually asked
|
|
458
|
+
* for — so a caller who said "portrait" sees both what they asked and what it
|
|
459
|
+
* became, while a bare `21:9` is not handed back a word nobody used.
|
|
460
|
+
*
|
|
461
|
+
* It lives apart from `buildMeta` because two tools cannot use that function:
|
|
462
|
+
* `gemini_interact` and `gemini_video_generate` build their metas around
|
|
463
|
+
* `interaction_id` and chain state rather than a seed, and so reported no shape
|
|
464
|
+
* at all until #166. Sharing the rule rather than copying it is what keeps
|
|
465
|
+
* those hand-rolled metas from drifting again.
|
|
466
|
+
*/
|
|
467
|
+
export function reportShape(meta, opts) {
|
|
468
|
+
if (opts.aspect_ratio)
|
|
469
|
+
meta.aspect_ratio = opts.aspect_ratio;
|
|
470
|
+
if (opts.orientation)
|
|
471
|
+
meta.orientation = opts.orientation;
|
|
472
|
+
return meta;
|
|
473
|
+
}
|
|
393
474
|
/**
|
|
394
475
|
* Return generated media inline (base64 content blocks) or write to disk and
|
|
395
476
|
* return the paths. `kind` selects the MCP inline block type: `image`/`audio`
|
package/dist/tools/video.js
CHANGED
|
@@ -3,7 +3,7 @@ import { McpToolError } from '@chrischall/mcp-utils';
|
|
|
3
3
|
import { slugify, baseName } from '../images.js';
|
|
4
4
|
import { resolveImageInputs } from '../inputs.js';
|
|
5
5
|
import { DEFAULT_VIDEO_MODEL } from '../models.js';
|
|
6
|
-
import { emitMedia, timeoutMsSchema, idempotencyKeySchema, asyncSchema, maxWaitMsSchema, withProgressHeartbeat, assertLocalInputsAvailable, imagesUrlSchema, imagesFileUrisSchema } from './shared.js';
|
|
6
|
+
import { emitMedia, reportShape, resolveAspectRatio, orientationSchema, timeoutMsSchema, idempotencyKeySchema, asyncSchema, maxWaitMsSchema, withProgressHeartbeat, assertLocalInputsAvailable, imagesUrlSchema, imagesFileUrisSchema } from './shared.js';
|
|
7
7
|
import { fingerprintRequest } from '../jobs.js';
|
|
8
8
|
import { previewLocalInputsUnlessConfirmed, schemaConfirm } from './_confirm.js';
|
|
9
9
|
/** omni's own aspect-ratio enum — NOT the image tools' ASPECT_RATIOS. */
|
|
@@ -30,7 +30,8 @@ export function registerVideoTools(server, client) {
|
|
|
30
30
|
annotations: { readOnlyHint: false, openWorldHint: true },
|
|
31
31
|
inputSchema: {
|
|
32
32
|
prompt: z.string().min(1).describe('Description of the video to generate (or the edit instruction when task=edit)'),
|
|
33
|
-
aspect_ratio: z.enum(VIDEO_ASPECT_RATIOS).optional().describe('
|
|
33
|
+
aspect_ratio: z.enum(VIDEO_ASPECT_RATIOS).optional().describe('Exact output aspect ratio (omni: 16:9 or 9:16). `orientation` is the plain-language shorthand; this wins if both are given.'),
|
|
34
|
+
orientation: orientationSchema,
|
|
34
35
|
task: z.enum(VIDEO_TASKS).optional().describe('text_to_video (default), image_to_video / reference_to_video (need image input), or edit (needs previous_interaction_id)'),
|
|
35
36
|
images: z.array(z.string().min(1)).optional().describe('Reference image path(s) for image_to_video / reference_to_video'),
|
|
36
37
|
images_url: imagesUrlSchema('Reference stills'),
|
|
@@ -71,8 +72,12 @@ export function registerVideoTools(server, client) {
|
|
|
71
72
|
if (gate)
|
|
72
73
|
return gate;
|
|
73
74
|
const model = args.model?.trim() || DEFAULT_VIDEO_MODEL;
|
|
75
|
+
// omni has no square, so `orientation: 'square'` is refused here rather
|
|
76
|
+
// than rounded to 16:9 — see resolveAspectRatio. Resolved before the
|
|
77
|
+
// fingerprint so "portrait" and "9:16" dedup to one billable job.
|
|
78
|
+
const aspectRatio = resolveAspectRatio(args, VIDEO_ASPECT_RATIOS);
|
|
74
79
|
const fingerprint = fingerprintRequest('gemini_video_generate', {
|
|
75
|
-
model, prompt: args.prompt, aspect_ratio:
|
|
80
|
+
model, prompt: args.prompt, aspect_ratio: aspectRatio, task: args.task,
|
|
76
81
|
images: args.images, images_base64: args.images_base64, from_clipboard: args.from_clipboard,
|
|
77
82
|
images_url: args.images_url, images_file_uris: args.images_file_uris,
|
|
78
83
|
previous_interaction_id: previousInteractionId,
|
|
@@ -83,7 +88,7 @@ export function registerVideoTools(server, client) {
|
|
|
83
88
|
input: args.prompt,
|
|
84
89
|
images: inputs.length ? inputs : undefined,
|
|
85
90
|
model: args.model,
|
|
86
|
-
aspectRatio
|
|
91
|
+
aspectRatio,
|
|
87
92
|
task: args.task,
|
|
88
93
|
previousInteractionId,
|
|
89
94
|
timeoutMs: args.timeout_ms,
|
|
@@ -95,7 +100,9 @@ export function registerVideoTools(server, client) {
|
|
|
95
100
|
onInteractionStarted: ctx.reportInteraction,
|
|
96
101
|
}));
|
|
97
102
|
client.session.lastVideoInteractionId = r.id;
|
|
98
|
-
|
|
103
|
+
// The RESOLVED ratio: omni takes only 16:9/9:16, so a caller who asked
|
|
104
|
+
// in words can see which of the two their request became.
|
|
105
|
+
const meta = reportShape({ model, interaction_id: r.id }, { ...args, aspect_ratio: aspectRatio });
|
|
99
106
|
if (previousInteractionId)
|
|
100
107
|
meta.previous_interaction_id = previousInteractionId;
|
|
101
108
|
if (r.text)
|
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 = '1.
|
|
2
|
+
export const VERSION = '1.10.0'; // x-release-please-version
|
package/mint.yaml
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
version: 1
|
|
2
|
+
name: Gemini Images
|
|
3
|
+
slug: gemini
|
|
4
|
+
summary: >-
|
|
5
|
+
Generate and edit images with Google Gemini image models (Nano Banana /
|
|
6
|
+
Nano Banana Pro) via natural language prompts
|
|
7
|
+
env:
|
|
8
|
+
- name: GEMINI_API_KEY
|
|
9
|
+
secret: true
|
|
10
|
+
required: true
|
|
11
|
+
help: >-
|
|
12
|
+
Your Google Gemini API key (aistudio.google.com/apikey)
|
|
13
|
+
- name: GEMINI_IMAGE_MODEL
|
|
14
|
+
required: false
|
|
15
|
+
help: >-
|
|
16
|
+
Override the default image model (default: gemini-3.1-flash-image)
|
|
17
|
+
- name: GEMINI_OUTPUT_DIR
|
|
18
|
+
required: false
|
|
19
|
+
help: >-
|
|
20
|
+
Default directory for generated images (default: current working
|
|
21
|
+
directory)
|
|
22
|
+
- name: GEMINI_INPUT_DIR
|
|
23
|
+
required: false
|
|
24
|
+
help: >-
|
|
25
|
+
Directory to resolve bare input-image filenames against
|
|
26
|
+
- name: GEMINI_TIMEOUT_MS
|
|
27
|
+
required: false
|
|
28
|
+
help: >-
|
|
29
|
+
Upstream request timeout in ms (default: 60000, or 120000 for 4K output)
|
|
30
|
+
- name: GEMINI_CHAIN_RETRY_MS
|
|
31
|
+
required: false
|
|
32
|
+
help: >-
|
|
33
|
+
Retry budget in milliseconds for a chained request that 404s while the
|
|
34
|
+
upstream result settles (default 120000).
|
|
35
|
+
- name: GEMINI_HEARTBEAT_MS
|
|
36
|
+
required: false
|
|
37
|
+
help: >-
|
|
38
|
+
How often, in milliseconds, to emit a progress notification while a
|
|
39
|
+
generation is in flight. Hosts reset their tools/call timeout on progress,
|
|
40
|
+
so this lets a long 4K/Pro generation outlive the default budget.
|
|
41
|
+
egress:
|
|
42
|
+
allow:
|
|
43
|
+
# Only hosts the SERVER process actually fetches. Hosts that appear
|
|
44
|
+
# solely in a URL this server BUILDS and returns are excluded.
|
|
45
|
+
- generativelanguage.googleapis.com
|
|
46
|
+
#
|
|
47
|
+
# NOTE — this server also reaches at least one host determined at
|
|
48
|
+
# RUNTIME, which cannot appear as a literal here:
|
|
49
|
+
# - a signed upload URL returned by the API (often storage.googleapis.com / s3)
|
|
50
|
+
# - a media/CDN host supplied at runtime
|
|
51
|
+
# Confirm the concrete host(s) against a live run and add them, or
|
|
52
|
+
# the isolated tier will block the call.
|
|
53
|
+
#
|
|
54
|
+
# Also: the images_url argument makes this server fetch an ARBITRARY
|
|
55
|
+
# caller-supplied https host. That cannot be enumerated in advance, so on
|
|
56
|
+
# the isolated tier either allow the hosts you intend to pass, or expect
|
|
57
|
+
# image-by-URL to be blocked there.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chrischall/gemini-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.0",
|
|
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>",
|
|
@@ -33,7 +33,8 @@
|
|
|
33
33
|
".claude-plugin",
|
|
34
34
|
"skills/",
|
|
35
35
|
".mcp.json",
|
|
36
|
-
"server.json"
|
|
36
|
+
"server.json",
|
|
37
|
+
"mint.yaml"
|
|
37
38
|
],
|
|
38
39
|
"scripts": {
|
|
39
40
|
"build": "tsc && npm run bundle",
|
|
@@ -45,7 +46,7 @@
|
|
|
45
46
|
"typecheck": "tsc -p tsconfig.test.json"
|
|
46
47
|
},
|
|
47
48
|
"dependencies": {
|
|
48
|
-
"@chrischall/mcp-utils": "^0.
|
|
49
|
+
"@chrischall/mcp-utils": "^0.15.0",
|
|
49
50
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
50
51
|
"dotenv": "^17.4.0",
|
|
51
52
|
"zod": "^4.4.2"
|
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": "1.
|
|
9
|
+
"version": "1.10.0",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "@chrischall/gemini-mcp",
|
|
14
|
-
"version": "1.
|
|
14
|
+
"version": "1.10.0",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|