@chrischall/gemini-mcp 1.8.0 → 1.9.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 +130 -70
- package/dist/tools/generate.js +17 -7
- package/dist/tools/interact.js +10 -4
- package/dist/tools/set.js +9 -4
- package/dist/tools/shared.js +68 -1
- package/dist/tools/video.js +9 -4
- package/dist/version.js +1 -1
- package/package.json +2 -2
- 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.9.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.9.0",
|
|
19
19
|
"author": {
|
|
20
20
|
"name": "Chris Hall"
|
|
21
21
|
},
|
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.9.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"),
|
|
@@ -33763,6 +33815,7 @@ function timeoutRiskHint(opts) {
|
|
|
33763
33815
|
function buildMeta(model, seed, opts) {
|
|
33764
33816
|
const meta3 = { model, seed };
|
|
33765
33817
|
if (opts.aspect_ratio) meta3.aspect_ratio = opts.aspect_ratio;
|
|
33818
|
+
if (opts.orientation) meta3.orientation = opts.orientation;
|
|
33766
33819
|
if (opts.image_size) meta3.image_size = opts.image_size;
|
|
33767
33820
|
return meta3;
|
|
33768
33821
|
}
|
|
@@ -34437,10 +34490,11 @@ function registerGenerateTools(server, client2) {
|
|
|
34437
34490
|
if (gate) return gate;
|
|
34438
34491
|
const count = args.count ?? 1;
|
|
34439
34492
|
const model = resolveModel(args.model, readEnvVar("GEMINI_IMAGE_MODEL"));
|
|
34493
|
+
const aspectRatio = resolveAspectRatio(args, ASPECT_RATIOS);
|
|
34440
34494
|
const fingerprint = fingerprintRequest("gemini_image_generate", {
|
|
34441
34495
|
model,
|
|
34442
34496
|
prompt: args.prompt,
|
|
34443
|
-
aspect_ratio:
|
|
34497
|
+
aspect_ratio: aspectRatio,
|
|
34444
34498
|
image_size: args.image_size,
|
|
34445
34499
|
thinking_level: args.thinking_level,
|
|
34446
34500
|
google_search: args.google_search,
|
|
@@ -34476,7 +34530,7 @@ function registerGenerateTools(server, client2) {
|
|
|
34476
34530
|
prompt,
|
|
34477
34531
|
images: refInputs.length > 0 ? refInputs : void 0,
|
|
34478
34532
|
model: args.model,
|
|
34479
|
-
aspectRatio
|
|
34533
|
+
aspectRatio,
|
|
34480
34534
|
imageSize: args.image_size,
|
|
34481
34535
|
seed: seed + i,
|
|
34482
34536
|
thinkingLevel: args.thinking_level,
|
|
@@ -34496,7 +34550,7 @@ function registerGenerateTools(server, client2) {
|
|
|
34496
34550
|
}
|
|
34497
34551
|
});
|
|
34498
34552
|
if (named.length === 0) throw firstError;
|
|
34499
|
-
const meta3 = buildMeta(model, seed, args);
|
|
34553
|
+
const meta3 = buildMeta(model, seed, { ...args, aspect_ratio: aspectRatio });
|
|
34500
34554
|
if (capturedText) meta3.text = capturedText;
|
|
34501
34555
|
if (capturedGrounding) meta3.grounding = capturedGrounding;
|
|
34502
34556
|
if (report) meta3.image_inputs = report;
|
|
@@ -34538,10 +34592,11 @@ function registerGenerateTools(server, client2) {
|
|
|
34538
34592
|
const gate = await previewLocalInputsUnlessConfirmed(args.confirm, SEND_LOCAL_ACTION, GENERATE_ENDPOINT, args.images);
|
|
34539
34593
|
if (gate) return gate;
|
|
34540
34594
|
const model = resolveModel(args.model, readEnvVar("GEMINI_IMAGE_MODEL"));
|
|
34595
|
+
const aspectRatio = resolveAspectRatio(args, ASPECT_RATIOS);
|
|
34541
34596
|
const fingerprint = fingerprintRequest("gemini_image_edit", {
|
|
34542
34597
|
model,
|
|
34543
34598
|
prompt: args.prompt,
|
|
34544
|
-
aspect_ratio:
|
|
34599
|
+
aspect_ratio: aspectRatio,
|
|
34545
34600
|
image_size: args.image_size,
|
|
34546
34601
|
thinking_level: args.thinking_level,
|
|
34547
34602
|
google_search: args.google_search,
|
|
@@ -34565,7 +34620,7 @@ function registerGenerateTools(server, client2) {
|
|
|
34565
34620
|
prompt,
|
|
34566
34621
|
images: inputs,
|
|
34567
34622
|
model: args.model,
|
|
34568
|
-
aspectRatio
|
|
34623
|
+
aspectRatio,
|
|
34569
34624
|
imageSize: args.image_size,
|
|
34570
34625
|
seed,
|
|
34571
34626
|
thinkingLevel: args.thinking_level,
|
|
@@ -34573,7 +34628,7 @@ function registerGenerateTools(server, client2) {
|
|
|
34573
34628
|
timeoutMs: args.timeout_ms
|
|
34574
34629
|
}));
|
|
34575
34630
|
const { images: [img], text } = result;
|
|
34576
|
-
const meta3 = buildMeta(model, seed, args);
|
|
34631
|
+
const meta3 = buildMeta(model, seed, { ...args, aspect_ratio: aspectRatio });
|
|
34577
34632
|
if (text) meta3.text = text;
|
|
34578
34633
|
if (result.grounding) meta3.grounding = result.grounding;
|
|
34579
34634
|
if (report) meta3.image_inputs = report;
|
|
@@ -34622,13 +34677,14 @@ function registerSetTools(server, client2) {
|
|
|
34622
34677
|
const gate = await previewLocalInputsUnlessConfirmed(args.confirm, "Send local master image input(s) to the Gemini API", "/v1beta/models/{model}:generateContent", args.master_images);
|
|
34623
34678
|
if (gate) return gate;
|
|
34624
34679
|
const model = resolveModel(args.model, readEnvVar("GEMINI_IMAGE_MODEL"));
|
|
34680
|
+
const aspectRatio = resolveAspectRatio(args, ASPECT_RATIOS);
|
|
34625
34681
|
const fingerprint = fingerprintRequest("gemini_image_set", {
|
|
34626
34682
|
model,
|
|
34627
34683
|
master_prompt: args.master_prompt,
|
|
34628
34684
|
scenes: args.scenes,
|
|
34629
34685
|
count: args.count,
|
|
34630
34686
|
reference_mode: args.reference_mode,
|
|
34631
|
-
aspect_ratio:
|
|
34687
|
+
aspect_ratio: aspectRatio,
|
|
34632
34688
|
image_size: args.image_size,
|
|
34633
34689
|
thinking_level: args.thinking_level,
|
|
34634
34690
|
google_search: args.google_search,
|
|
@@ -34643,7 +34699,7 @@ function registerSetTools(server, client2) {
|
|
|
34643
34699
|
});
|
|
34644
34700
|
return client2.session.jobs.dispatch({ toolName: "gemini_image_set", fingerprint, idempotencyKey: args.idempotency_key, async: args.async, waitMs: args.max_wait_ms }, async () => {
|
|
34645
34701
|
const seed = pickSeed(args.seed);
|
|
34646
|
-
const cfg = { model: args.model, aspectRatio
|
|
34702
|
+
const cfg = { model: args.model, aspectRatio, imageSize: args.image_size, thinkingLevel: args.thinking_level, googleSearch: args.google_search, timeoutMs: args.timeout_ms };
|
|
34647
34703
|
const slug = args.basename ? baseName(args.basename) : slugify2(args.master_prompt);
|
|
34648
34704
|
const charRefs = await resolveCharacterRefs(client2, args.characters, args.style);
|
|
34649
34705
|
const masterPrompt = composePrompt(args.master_prompt, charRefs);
|
|
@@ -34703,7 +34759,7 @@ function registerSetTools(server, client2) {
|
|
|
34703
34759
|
return result;
|
|
34704
34760
|
});
|
|
34705
34761
|
const masterText = masterResult.text;
|
|
34706
|
-
const meta3 = buildMeta(model, seed, args);
|
|
34762
|
+
const meta3 = buildMeta(model, seed, { ...args, aspect_ratio: aspectRatio });
|
|
34707
34763
|
if (masterText) meta3.text = masterText;
|
|
34708
34764
|
if (masterResult.grounding) meta3.grounding = masterResult.grounding;
|
|
34709
34765
|
if (report) meta3.image_inputs = report;
|
|
@@ -36028,7 +36084,8 @@ function registerInteractTools(server, client2) {
|
|
|
36028
36084
|
`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
36085
|
),
|
|
36030
36086
|
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("
|
|
36087
|
+
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."),
|
|
36088
|
+
orientation: orientationSchema,
|
|
36032
36089
|
image_size: external_exports.enum(IMAGE_SIZES).optional().describe("Output resolution (512 = 0.5K, Flash-only)"),
|
|
36033
36090
|
thinking_level: external_exports.enum(["minimal", "high"]).optional().describe("Reasoning depth; higher can help complex/structural edits"),
|
|
36034
36091
|
filename: external_exports.string().optional().describe("Base filename for the output image (extension stripped; default: slugified input)"),
|
|
@@ -36073,10 +36130,11 @@ function registerInteractTools(server, client2) {
|
|
|
36073
36130
|
const gate = await previewLocalInputsUnlessConfirmed(args.confirm, "Send local file input(s) to the Gemini Interactions API", "/v1beta/interactions", images, args.video_path);
|
|
36074
36131
|
if (gate) return gate;
|
|
36075
36132
|
const model = resolveModel(args.model, readEnvVar("GEMINI_IMAGE_MODEL"));
|
|
36133
|
+
const aspectRatio = resolveAspectRatio(args, ASPECT_RATIOS);
|
|
36076
36134
|
const fingerprint = fingerprintRequest("gemini_interact", {
|
|
36077
36135
|
model,
|
|
36078
36136
|
input: args.input,
|
|
36079
|
-
aspect_ratio:
|
|
36137
|
+
aspect_ratio: aspectRatio,
|
|
36080
36138
|
image_size: args.image_size,
|
|
36081
36139
|
thinking_level: args.thinking_level,
|
|
36082
36140
|
previous_interaction_id: previousInteractionId,
|
|
@@ -36096,7 +36154,7 @@ function registerInteractTools(server, client2) {
|
|
|
36096
36154
|
const callOpts = {
|
|
36097
36155
|
input: args.input,
|
|
36098
36156
|
model: args.model,
|
|
36099
|
-
aspectRatio
|
|
36157
|
+
aspectRatio,
|
|
36100
36158
|
imageSize: args.image_size,
|
|
36101
36159
|
thinkingLevel: args.thinking_level,
|
|
36102
36160
|
googleSearch: args.google_search,
|
|
@@ -36181,7 +36239,8 @@ function registerVideoTools(server, client2) {
|
|
|
36181
36239
|
annotations: { readOnlyHint: false, openWorldHint: true },
|
|
36182
36240
|
inputSchema: {
|
|
36183
36241
|
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("
|
|
36242
|
+
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."),
|
|
36243
|
+
orientation: orientationSchema,
|
|
36185
36244
|
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
36245
|
images: external_exports.array(external_exports.string().min(1)).optional().describe("Reference image path(s) for image_to_video / reference_to_video"),
|
|
36187
36246
|
images_url: imagesUrlSchema("Reference stills"),
|
|
@@ -36216,10 +36275,11 @@ function registerVideoTools(server, client2) {
|
|
|
36216
36275
|
const gate = await previewLocalInputsUnlessConfirmed(args.confirm, "Send local image input(s) to the Gemini omni API", "/v1beta/interactions", args.images);
|
|
36217
36276
|
if (gate) return gate;
|
|
36218
36277
|
const model = args.model?.trim() || DEFAULT_VIDEO_MODEL;
|
|
36278
|
+
const aspectRatio = resolveAspectRatio(args, VIDEO_ASPECT_RATIOS);
|
|
36219
36279
|
const fingerprint = fingerprintRequest("gemini_video_generate", {
|
|
36220
36280
|
model,
|
|
36221
36281
|
prompt: args.prompt,
|
|
36222
|
-
aspect_ratio:
|
|
36282
|
+
aspect_ratio: aspectRatio,
|
|
36223
36283
|
task: args.task,
|
|
36224
36284
|
images: args.images,
|
|
36225
36285
|
images_base64: args.images_base64,
|
|
@@ -36234,7 +36294,7 @@ function registerVideoTools(server, client2) {
|
|
|
36234
36294
|
input: args.prompt,
|
|
36235
36295
|
images: inputs.length ? inputs : void 0,
|
|
36236
36296
|
model: args.model,
|
|
36237
|
-
aspectRatio
|
|
36297
|
+
aspectRatio,
|
|
36238
36298
|
task: args.task,
|
|
36239
36299
|
previousInteractionId,
|
|
36240
36300
|
timeoutMs: args.timeout_ms,
|
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, 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,
|
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'),
|
|
@@ -384,8 +447,12 @@ export function timeoutRiskHint(opts) {
|
|
|
384
447
|
/** Build the result metadata echoed to the caller (omitting unset optionals). */
|
|
385
448
|
export function buildMeta(model, seed, opts) {
|
|
386
449
|
const meta = { model, seed };
|
|
450
|
+
// The RESOLVED ratio, and the orientation only when one was asked for — so a
|
|
451
|
+
// caller who said "portrait" can see both what they asked and what it became.
|
|
387
452
|
if (opts.aspect_ratio)
|
|
388
453
|
meta.aspect_ratio = opts.aspect_ratio;
|
|
454
|
+
if (opts.orientation)
|
|
455
|
+
meta.orientation = opts.orientation;
|
|
389
456
|
if (opts.image_size)
|
|
390
457
|
meta.image_size = opts.image_size;
|
|
391
458
|
return meta;
|
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, 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,
|
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.9.0'; // x-release-please-version
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chrischall/gemini-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.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>",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"typecheck": "tsc -p tsconfig.test.json"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@chrischall/mcp-utils": "^0.
|
|
48
|
+
"@chrischall/mcp-utils": "^0.15.0",
|
|
49
49
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
50
50
|
"dotenv": "^17.4.0",
|
|
51
51
|
"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.9.0",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "@chrischall/gemini-mcp",
|
|
14
|
-
"version": "1.
|
|
14
|
+
"version": "1.9.0",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|