@chrischall/gemini-mcp 1.0.3 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +74 -1
- package/dist/bundle.js +773 -56
- package/dist/client.js +194 -17
- package/dist/fetch-image.js +339 -0
- package/dist/index.js +2 -1
- package/dist/inputs.js +186 -0
- package/dist/session.js +39 -0
- package/dist/tools/files.js +194 -0
- package/dist/tools/generate.js +21 -12
- package/dist/tools/interact.js +11 -4
- package/dist/tools/set.js +29 -7
- package/dist/tools/shared.js +43 -6
- package/dist/upload-endpoint.js +79 -0
- package/dist/version.js +1 -1
- package/package.json +3 -3
- package/server.json +2 -2
- package/skills/gemini-mcp/SKILL.md +60 -4
package/dist/tools/set.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { McpToolError, readEnvVar } from '@chrischall/mcp-utils';
|
|
3
3
|
import { resolveModel } from '../models.js';
|
|
4
|
-
import { slugify, baseName
|
|
5
|
-
import {
|
|
4
|
+
import { slugify, baseName } from '../images.js';
|
|
5
|
+
import { resolveImageInputs } from '../inputs.js';
|
|
6
|
+
import { emit, sharedImageSchema, pickSeed, buildMeta, timeoutRiskHint, withProgressHeartbeat, assertLocalInputsAvailable, imagesUrlSchema, imagesFileUrisSchema } from './shared.js';
|
|
6
7
|
import { fingerprintRequest } from '../jobs.js';
|
|
7
8
|
import { previewLocalInputsUnlessConfirmed, schemaConfirm } from './_confirm.js';
|
|
8
9
|
export function registerSetTools(server, client) {
|
|
@@ -16,7 +17,9 @@ export function registerSetTools(server, client) {
|
|
|
16
17
|
reference_mode: z.enum(['master', 'chain']).optional().describe('master: every image references the master (default). chain: each references the previous.'),
|
|
17
18
|
basename: z.string().optional().describe('Base filename prefix for output images (default: slugified master_prompt)'),
|
|
18
19
|
master_images: z.array(z.string().min(1)).optional().describe('Reference image paths passed to the master generation call'),
|
|
19
|
-
|
|
20
|
+
master_images_url: imagesUrlSchema('Reference images passed to the master AND to every scene call (fetched once)'),
|
|
21
|
+
master_images_file_uris: imagesFileUrisSchema('Reference images passed to the master AND to every scene call'),
|
|
22
|
+
master_images_base64: z.array(z.string().min(1)).optional().describe('Reference images as base64 strings or data URIs for master generation. Last resort: prefer master_images_url or master_images_file_uris, which keep image bytes out of the conversation'),
|
|
20
23
|
confirm: schemaConfirm,
|
|
21
24
|
...sharedImageSchema,
|
|
22
25
|
},
|
|
@@ -37,13 +40,30 @@ export function registerSetTools(server, client) {
|
|
|
37
40
|
reference_mode: args.reference_mode, aspect_ratio: args.aspect_ratio, image_size: args.image_size,
|
|
38
41
|
thinking_level: args.thinking_level, google_search: args.google_search,
|
|
39
42
|
master_images: args.master_images, master_images_base64: args.master_images_base64, from_clipboard: args.from_clipboard,
|
|
43
|
+
master_images_url: args.master_images_url, master_images_file_uris: args.master_images_file_uris,
|
|
40
44
|
});
|
|
41
45
|
return client.session.jobs.dispatch({ toolName: 'gemini_image_set', fingerprint, idempotencyKey: args.idempotency_key, async: args.async }, async () => {
|
|
42
46
|
const seed = pickSeed(args.seed);
|
|
43
47
|
const cfg = { model: args.model, aspectRatio: args.aspect_ratio, imageSize: args.image_size, thinkingLevel: args.thinking_level, googleSearch: args.google_search, timeoutMs: args.timeout_ms };
|
|
44
48
|
const slug = args.basename ? baseName(args.basename) : slugify(args.master_prompt);
|
|
45
|
-
// 1. master (optionally seeded from reference images)
|
|
46
|
-
|
|
49
|
+
// 1. master (optionally seeded from reference images).
|
|
50
|
+
//
|
|
51
|
+
// Two resolutions, because the two groups have different lifetimes:
|
|
52
|
+
//
|
|
53
|
+
// - `carried` — URL and Files API references. Fetched/uploaded ONCE for
|
|
54
|
+
// the whole set (a URL repeated across master + N scenes is one
|
|
55
|
+
// download, and a `files/…` reference costs nothing to repeat), and
|
|
56
|
+
// passed to EVERY scene call, where they do the most good: they are
|
|
57
|
+
// what keeps the subject consistent scene to scene.
|
|
58
|
+
// - `masterOnly` — path / base64 / clipboard references. Master-only,
|
|
59
|
+
// exactly as before these parameters existed, so pre-existing calls
|
|
60
|
+
// still build byte-identical requests.
|
|
61
|
+
const carried = await resolveImageInputs({ images_url: args.master_images_url, images_file_uris: args.master_images_file_uris }, client);
|
|
62
|
+
const masterOnly = await resolveImageInputs({ images: args.master_images, images_base64: args.master_images_base64, from_clipboard: args.from_clipboard }, client);
|
|
63
|
+
const masterRefInputs = [...masterOnly.inputs, ...carried.inputs];
|
|
64
|
+
const report = carried.report || masterOnly.report
|
|
65
|
+
? { ...masterOnly.report, ...carried.report }
|
|
66
|
+
: undefined;
|
|
47
67
|
const named = [];
|
|
48
68
|
const masterResult = await withProgressHeartbeat(extra, `Generating image set (${model})`, async () => {
|
|
49
69
|
const result = await client.generate({
|
|
@@ -62,13 +82,13 @@ export function registerSetTools(server, client) {
|
|
|
62
82
|
if (mode === 'chain') {
|
|
63
83
|
let ref = master;
|
|
64
84
|
for (let i = 0; i < scenePrompts.length; i++) {
|
|
65
|
-
const { images: [img] } = await client.generate({ prompt: scenePrompts[i], images: [ref], seed: seed + i + 1, ...cfg });
|
|
85
|
+
const { images: [img] } = await client.generate({ prompt: scenePrompts[i], images: [ref, ...carried.inputs], seed: seed + i + 1, ...cfg });
|
|
66
86
|
named.push({ image: img, base: `${slug}-${String(i + 1).padStart(2, '0')}` });
|
|
67
87
|
ref = img;
|
|
68
88
|
}
|
|
69
89
|
}
|
|
70
90
|
else {
|
|
71
|
-
const scenes = await Promise.all(scenePrompts.map((p, i) => client.generate({ prompt: p, images: [master], seed: seed + i + 1, ...cfg }).then((r) => r.images[0])));
|
|
91
|
+
const scenes = await Promise.all(scenePrompts.map((p, i) => client.generate({ prompt: p, images: [master, ...carried.inputs], seed: seed + i + 1, ...cfg }).then((r) => r.images[0])));
|
|
72
92
|
scenes.forEach((img, i) => named.push({ image: img, base: `${slug}-${String(i + 1).padStart(2, '0')}` }));
|
|
73
93
|
}
|
|
74
94
|
return result;
|
|
@@ -79,6 +99,8 @@ export function registerSetTools(server, client) {
|
|
|
79
99
|
meta.text = masterText;
|
|
80
100
|
if (masterResult.grounding)
|
|
81
101
|
meta.grounding = masterResult.grounding;
|
|
102
|
+
if (report)
|
|
103
|
+
meta.image_inputs = report;
|
|
82
104
|
// A set is master + N scenes in one tools/call — the most timeout-prone
|
|
83
105
|
// tool. Effective image count drives the risk hint.
|
|
84
106
|
const risk = timeoutRiskHint({ model, imageSize: args.image_size, count: named.length, persistsFiles: client.mediaSink?.persistsFiles });
|
package/dist/tools/shared.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { textResult, McpToolError, readEnvVar } from '@chrischall/mcp-utils';
|
|
3
3
|
import { resolveVideoPath, videoMimeType } from '../images.js';
|
|
4
|
+
import { IMAGE_URL_MAX_BYTES } from '../fetch-image.js';
|
|
4
5
|
import { createDiskSink } from '../storage/media.js';
|
|
5
6
|
/**
|
|
6
7
|
* Fallback sink for call sites that pass no explicit one (every pre-existing
|
|
@@ -52,6 +53,31 @@ export const asyncSchema = z
|
|
|
52
53
|
.boolean()
|
|
53
54
|
.optional()
|
|
54
55
|
.describe('Run in the background and return a job_id immediately instead of the image, so a long (Pro/4K) generation cannot hit the host tools/call timeout (-32001). Poll gemini_get_result with the job_id to fetch the result (jobs are per-process and expire ~10 min after completion).');
|
|
56
|
+
/**
|
|
57
|
+
* Reference images the SERVER fetches, so the bytes never pass through the
|
|
58
|
+
* model's context window. Defined once and reused under both the `images_url`
|
|
59
|
+
* and `master_images_url` names.
|
|
60
|
+
*/
|
|
61
|
+
export function imagesUrlSchema(label = 'Reference images') {
|
|
62
|
+
return z
|
|
63
|
+
.array(z.string().url())
|
|
64
|
+
.optional()
|
|
65
|
+
.describe(`${label} as public https URLs — the SERVER downloads them, so no image bytes travel through the conversation. ` +
|
|
66
|
+
'Preferred over images_base64, which costs ~14k tokens per photo and breaks if a file read was truncated. ' +
|
|
67
|
+
`Max ${Math.round(IMAGE_URL_MAX_BYTES / (1024 * 1024))}MB each; must be a directly-linked image (Content-Type image/*).`);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Reference images already in the Gemini Files API. The cheapest form there is:
|
|
71
|
+
* a short string, reusable across calls until the file expires.
|
|
72
|
+
*/
|
|
73
|
+
export function imagesFileUrisSchema(label = 'Reference images') {
|
|
74
|
+
return z
|
|
75
|
+
.array(z.string().min(1))
|
|
76
|
+
.optional()
|
|
77
|
+
.describe(`${label} by Gemini Files API reference ("files/<id>", or the full uri) from gemini_upload_file or POST /upload. ` +
|
|
78
|
+
'Upload once, then reference it across as many calls as you like — no bytes are re-sent and none enter the conversation. ' +
|
|
79
|
+
'Files are retained ~48h, after which the reference stops resolving.');
|
|
80
|
+
}
|
|
55
81
|
/**
|
|
56
82
|
* The model/aspect/size/output fields every generation tool shares. Defined once
|
|
57
83
|
* here so descriptions can't drift between `generate.ts` and `set.ts`. Spread
|
|
@@ -271,10 +297,15 @@ export async function emit(named, opts, meta, onWritten) {
|
|
|
271
297
|
* - `video_path` — the Files API upload streams the file off disk.
|
|
272
298
|
*
|
|
273
299
|
* Called at the top of every generation handler, BEFORE any billable upstream
|
|
274
|
-
* call, so a caller gets a clear
|
|
275
|
-
*
|
|
276
|
-
*
|
|
277
|
-
*
|
|
300
|
+
* call, so a caller gets a clear alternative instead of an obscure
|
|
301
|
+
* module-resolution or ENOENT crash. A no-op on the disk sink, so the stdio
|
|
302
|
+
* behaviour (including its own "Image not found" errors) is untouched.
|
|
303
|
+
*
|
|
304
|
+
* The remediation deliberately does NOT lead with `images_base64` any more.
|
|
305
|
+
* Inlining a photo as a tool argument costs ~14k tokens of the model's context
|
|
306
|
+
* and silently corrupts whenever the read that produced it was truncated — so
|
|
307
|
+
* it is named last, after the three routes that move bytes without the model
|
|
308
|
+
* ever seeing them.
|
|
278
309
|
*/
|
|
279
310
|
export function assertLocalInputsAvailable(sink, args) {
|
|
280
311
|
if ((sink ?? DEFAULT_SINK).persistsFiles)
|
|
@@ -293,8 +324,14 @@ export function assertLocalInputsAvailable(sink, args) {
|
|
|
293
324
|
// Remediation goes in the MESSAGE, not just `hint`: MCP error serialization
|
|
294
325
|
// surfaces the message and drops the hint, so hint-only advice is invisible
|
|
295
326
|
// exactly when it is needed.
|
|
296
|
-
const fix = '
|
|
297
|
-
'
|
|
327
|
+
const fix = 'Reference images WITHOUT putting bytes in the conversation, in order of preference: ' +
|
|
328
|
+
'(1) `images_url` — public https URLs the server fetches itself; ' +
|
|
329
|
+
'(2) `images_file_uris` — upload once with `gemini_upload_file` (it takes a `url` or `data_base64`) and reuse the returned files/… uri; ' +
|
|
330
|
+
'(3) `POST /upload` on this connector with the raw bytes as the body ' +
|
|
331
|
+
"(`curl -X POST <connector-url>/upload -H 'Authorization: Bearer <token>' -H 'Content-Type: image/jpeg' --data-binary @photo.jpg`), " +
|
|
332
|
+
'then pass the returned file_uri. ' +
|
|
333
|
+
'As a last resort `images_base64` still works, but it costs ~14k tokens per photo and breaks on a truncated read. ' +
|
|
334
|
+
'Reference video by `video_url` (a public YouTube URL, or a files/… uri uploaded elsewhere) instead of `video_path`.';
|
|
298
335
|
throw new McpToolError(`${unavailable.join(', ')} ${unavailable.length > 1 ? 'are' : 'is'} unavailable on the hosted connector: ` +
|
|
299
336
|
`it runs on a Cloudflare Worker, which has no filesystem and no local clipboard. ${fix}`, { hint: fix });
|
|
300
337
|
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/** Media the Files API is useful for. Anything else is a caller mistake. */
|
|
2
|
+
const ACCEPTED_PREFIXES = ['image/', 'video/', 'audio/'];
|
|
3
|
+
function json(body, status) {
|
|
4
|
+
return new Response(JSON.stringify(body, null, 2), {
|
|
5
|
+
status,
|
|
6
|
+
headers: { 'content-type': 'application/json; charset=utf-8' },
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
/** Every error names the fix, because the caller here is a shell, not a model. */
|
|
10
|
+
function fail(status, error, hint) {
|
|
11
|
+
return json({ error, hint }, status);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Build the `/upload` route handler. `buildClient` is injected rather than
|
|
15
|
+
* imported so the Worker can construct a client from *this request's* OAuth
|
|
16
|
+
* props (never a shared singleton — see the cross-tenant note in session.ts),
|
|
17
|
+
* and so tests can drive the whole handler against a stub client.
|
|
18
|
+
*/
|
|
19
|
+
export function createUploadHandler(buildClient) {
|
|
20
|
+
return async function handleUpload(request, ctx) {
|
|
21
|
+
if (request.method === 'OPTIONS') {
|
|
22
|
+
return new Response(null, { status: 204, headers: { allow: 'POST, OPTIONS' } });
|
|
23
|
+
}
|
|
24
|
+
if (request.method !== 'POST') {
|
|
25
|
+
return fail(405, `Method ${request.method} not allowed on /upload.`, 'POST the raw file bytes as the request body.');
|
|
26
|
+
}
|
|
27
|
+
// The OAuth provider populates props from the validated bearer token. Its
|
|
28
|
+
// absence means the route was reached some other way — refuse rather than
|
|
29
|
+
// fall back to an ambient key, which on a multi-tenant Worker would bill an
|
|
30
|
+
// unauthenticated upload to whoever configured the environment.
|
|
31
|
+
const apiKey = ctx.props?.apiKey;
|
|
32
|
+
if (typeof apiKey !== 'string' || !apiKey) {
|
|
33
|
+
return fail(401, 'Unauthenticated.', 'Send the same Authorization: Bearer <access-token> you use for the /mcp endpoint.');
|
|
34
|
+
}
|
|
35
|
+
const contentType = (request.headers.get('content-type') ?? '').split(';')[0].trim().toLowerCase();
|
|
36
|
+
if (!ACCEPTED_PREFIXES.some((p) => contentType.startsWith(p))) {
|
|
37
|
+
return fail(415, `Unsupported Content-Type "${contentType || '(none)'}".`, 'Set Content-Type to the file\'s media type, e.g. -H "Content-Type: image/jpeg".');
|
|
38
|
+
}
|
|
39
|
+
const declared = request.headers.get('content-length');
|
|
40
|
+
const contentLength = Number(declared);
|
|
41
|
+
if (!declared || !Number.isFinite(contentLength) || contentLength <= 0) {
|
|
42
|
+
return fail(411, 'Content-Length is required (and must be > 0).', 'The Files API resumable upload declares the size up front, so the body cannot be sent chunked. ' +
|
|
43
|
+
'curl sets this automatically with --data-binary @file.');
|
|
44
|
+
}
|
|
45
|
+
const body = request.body;
|
|
46
|
+
if (!body) {
|
|
47
|
+
return fail(400, 'Request had no body.', 'Send the file bytes as the request body, e.g. --data-binary @photo.jpg.');
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
const file = await buildClient(apiKey).uploadStream(body, contentType, displayNameFor(request, contentType), contentLength);
|
|
51
|
+
return json({
|
|
52
|
+
file_uri: file.name,
|
|
53
|
+
uri: file.uri,
|
|
54
|
+
name: file.name,
|
|
55
|
+
mime_type: file.mimeType,
|
|
56
|
+
...(file.sizeBytes ? { size_bytes: Number(file.sizeBytes) } : {}),
|
|
57
|
+
expires: file.expirationTime ?? '~48h from upload',
|
|
58
|
+
hint: `Pass "${file.name}" as images_file_uris (or video_url) on any image tool. Gemini retains it ~48h.`,
|
|
59
|
+
}, 201);
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
// Upstream failures are the interesting case here (bad key, quota, a file
|
|
63
|
+
// over the 2 GB cap), so the message is passed through rather than
|
|
64
|
+
// flattened into "upload failed".
|
|
65
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
66
|
+
return fail(502, `Upload to the Gemini Files API failed: ${message}`, 'Check the API key on this connection is still valid and funded.');
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* A display name for the upload: an explicit `?name=` wins, else the
|
|
72
|
+
* `X-Filename` header (what a wrapper script would send), else the media type.
|
|
73
|
+
* Cosmetic — the Files API keys on the returned resource name, not this.
|
|
74
|
+
*/
|
|
75
|
+
function displayNameFor(request, contentType) {
|
|
76
|
+
const fromQuery = new URL(request.url).searchParams.get('name');
|
|
77
|
+
const raw = fromQuery ?? request.headers.get('x-filename') ?? `upload.${contentType.split('/')[1] || 'bin'}`;
|
|
78
|
+
return raw.replace(/[^\w.\-]+/g, '-').slice(0, 120) || 'upload';
|
|
79
|
+
}
|
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.0
|
|
2
|
+
export const VERSION = '1.1.0'; // x-release-please-version
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chrischall/gemini-mcp",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.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>",
|
|
@@ -55,13 +55,13 @@
|
|
|
55
55
|
"zod": "^4.4.2"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
|
-
"@chrischall/mcp-connector": "^1.
|
|
58
|
+
"@chrischall/mcp-connector": "^1.1.1",
|
|
59
59
|
"@cloudflare/vitest-pool-workers": "^0.18.6",
|
|
60
60
|
"@cloudflare/workers-oauth-provider": "^0.8.2",
|
|
61
61
|
"@cloudflare/workers-types": "^5.20260719.1",
|
|
62
62
|
"@types/node": "^26.0.0",
|
|
63
63
|
"@vitest/coverage-v8": "^4.1.2",
|
|
64
|
-
"agents": "^0.
|
|
64
|
+
"agents": "^0.19.0",
|
|
65
65
|
"esbuild": "^0.28.0",
|
|
66
66
|
"typescript": "^7.0.2",
|
|
67
67
|
"vitest": "^4.1.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.0
|
|
9
|
+
"version": "1.1.0",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "@chrischall/gemini-mcp",
|
|
14
|
-
"version": "1.0
|
|
14
|
+
"version": "1.1.0",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|
|
@@ -91,14 +91,53 @@ Note: Image generation requires a billing-enabled Google Cloud project.
|
|
|
91
91
|
### Image Generation
|
|
92
92
|
| Tool | Description |
|
|
93
93
|
|------|-------------|
|
|
94
|
-
| `gemini_image_generate(prompt, count?, images?, images_base64?, video_url?, video_path?, google_search?, seed?, filename?, model?, aspect_ratio?, image_size?, thinking_level?, output_dir?, inline?)` | Generate image(s) from a text prompt (optionally image-conditioned
|
|
95
|
-
| `gemini_image_edit(prompt, images?, images_base64?, google_search?, seed?, filename?, model?, aspect_ratio?, image_size?, thinking_level?, output_dir?, inline?)` | Edit or compose input image(s)
|
|
96
|
-
| `gemini_image_set(master_prompt, scenes? \| count?, reference_mode?, master_images?, master_images_base64?, google_search?, seed?, basename?, model?, thinking_level?, ...)` | Master image (optionally seeded from a reference photo) plus N consistent images referencing it |
|
|
94
|
+
| `gemini_image_generate(prompt, count?, images_url?, images_file_uris?, images?, images_base64?, video_url?, video_path?, google_search?, seed?, filename?, model?, aspect_ratio?, image_size?, thinking_level?, output_dir?, inline?)` | Generate image(s) from a text prompt (optionally image-conditioned — see **Reference images** below — or video-conditioned via `video_url`/`video_path`) |
|
|
95
|
+
| `gemini_image_edit(prompt, images_url?, images_file_uris?, images?, images_base64?, google_search?, seed?, filename?, model?, aspect_ratio?, image_size?, thinking_level?, output_dir?, inline?)` | Edit or compose input image(s) with a text instruction. Requires ≥1 input from any of the four reference forms |
|
|
96
|
+
| `gemini_image_set(master_prompt, scenes? \| count?, reference_mode?, master_images_url?, master_images_file_uris?, master_images?, master_images_base64?, google_search?, seed?, basename?, model?, thinking_level?, ...)` | Master image (optionally seeded from a reference photo) plus N consistent images referencing it. `master_images_url` / `master_images_file_uris` are resolved **once** and passed to the master *and* every scene |
|
|
97
|
+
|
|
98
|
+
### Reference images — four ways in, one that costs context
|
|
99
|
+
|
|
100
|
+
| Parameter | Where the bytes travel | Context cost |
|
|
101
|
+
|---|---|---|
|
|
102
|
+
| `images_url` (`master_images_url`) | the **server** downloads the https URL | none |
|
|
103
|
+
| `images_file_uris` (`master_images_file_uris`) | a `files/<id>` reference from `gemini_upload_file` | none |
|
|
104
|
+
| `images` | read off local disk (stdio builds only) | none |
|
|
105
|
+
| `images_base64` | **through the tool-call JSON** | **~14k tokens per JPEG** |
|
|
106
|
+
|
|
107
|
+
**Reach for `images_base64` last.** It costs ~14k tokens per modest photo, and a truncated file
|
|
108
|
+
read produces base64 that still *looks* valid — so the corruption surfaces as a bad generation,
|
|
109
|
+
not an error.
|
|
110
|
+
|
|
111
|
+
- `images_url` accepts public `https://` URLs only (private/loopback/link-local hosts refused,
|
|
112
|
+
every redirect revalidated), must be `Content-Type: image/*`, capped at 15MB. Errors name the
|
|
113
|
+
failing URL. Over 6MB is auto-uploaded to the Files API instead of inlined.
|
|
114
|
+
- `images_file_uris` accepts `files/<id>` or the full uri. Retained **~48h**; reusable across
|
|
115
|
+
any number of calls until then.
|
|
116
|
+
- On stdio, an `images` path referenced **more than once in a session** is auto-uploaded to the
|
|
117
|
+
Files API (keyed on path + mtime + size) so the bytes stop being re-sent.
|
|
118
|
+
|
|
119
|
+
### Files API
|
|
120
|
+
| Tool | Description |
|
|
121
|
+
|------|-------------|
|
|
122
|
+
| `gemini_upload_file(url? \| data_base64? \| path?, mime_type?, display_name?, confirm?)` | Upload once, get a reusable `files/<id>`. Exactly one source. `url` is fetched by the server (image/video/audio, ≤100MB); `path` is stdio-only and confirm-gated; `data_base64` is the last resort |
|
|
123
|
+
| `gemini_list_files(page_size?)` | List current uploads with MIME types and expiry |
|
|
124
|
+
| `gemini_delete_file(file_uri, confirm)` | Delete an upload before its ~48h expiry |
|
|
125
|
+
|
|
126
|
+
On the **hosted connector** there is also `POST /upload`, behind the same OAuth token as
|
|
127
|
+
`/mcp` — the zero-base64 path for an agent with a shell:
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
curl -X POST https://<connector>/upload \
|
|
131
|
+
-H "Authorization: Bearer $ACCESS_TOKEN" \
|
|
132
|
+
-H "Content-Type: image/jpeg" \
|
|
133
|
+
--data-binary @photo.jpg
|
|
134
|
+
# → {"file_uri":"files/abc123", ...} then: images_file_uris: ["files/abc123"]
|
|
135
|
+
```
|
|
97
136
|
|
|
98
137
|
### Multi-turn (Interactions API)
|
|
99
138
|
| Tool | Description |
|
|
100
139
|
|------|-------------|
|
|
101
|
-
| `gemini_interact(input, previous_interaction_id?, continue_last?, images?, images_base64?, video_url?, video_path?, google_search?, search_types?, model?, aspect_ratio?, image_size?, thinking_level?, filename?, output_dir?, inline?)` | **Preferred tool for iterative refinement.** Generate/edit via Gemini's **Interactions API**. Returns an `interaction_id`; pass it back as `previous_interaction_id` (or set `continue_last: true` to reuse the session's most recent one) to **iteratively refine the same image** conversationally — do NOT start a new interaction or re-upload the image per tweak. Output is **JPEG**. |
|
|
140
|
+
| `gemini_interact(input, previous_interaction_id?, continue_last?, images_url?, images_file_uris?, images?, images_base64?, video_url?, video_path?, google_search?, search_types?, model?, aspect_ratio?, image_size?, thinking_level?, filename?, output_dir?, inline?)` | **Preferred tool for iterative refinement.** Generate/edit via Gemini's **Interactions API**. Returns an `interaction_id`; pass it back as `previous_interaction_id` (or set `continue_last: true` to reuse the session's most recent one) to **iteratively refine the same image** conversationally — do NOT start a new interaction or re-upload the image per tweak. Output is **JPEG**. |
|
|
102
141
|
|
|
103
142
|
### Video & Music (preview — funded account)
|
|
104
143
|
| Tool | Description |
|
|
@@ -131,6 +170,23 @@ gemini_image_edit(prompt: "make the background blue", images: ["/path/to/image.p
|
|
|
131
170
|
→ returns path to edited PNG
|
|
132
171
|
```
|
|
133
172
|
|
|
173
|
+
**Edit an image you only have a URL for (nothing downloads into the conversation):**
|
|
174
|
+
```
|
|
175
|
+
gemini_image_edit(prompt: "make the background blue", images_url: ["https://example.com/photo.jpg"])
|
|
176
|
+
→ the server fetches the URL itself; returns path to edited PNG
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
**Reuse one photo across many generations (hosted connector, agent with a shell):**
|
|
180
|
+
```
|
|
181
|
+
$ curl -X POST https://<connector>/upload -H "Authorization: Bearer $TOKEN" \
|
|
182
|
+
-H "Content-Type: image/jpeg" --data-binary @photo.jpg
|
|
183
|
+
→ {"file_uri": "files/abc123"}
|
|
184
|
+
|
|
185
|
+
gemini_image_edit(prompt: "make it winter", images_file_uris: ["files/abc123"])
|
|
186
|
+
gemini_image_edit(prompt: "make it sunrise", images_file_uris: ["files/abc123"])
|
|
187
|
+
→ two edits, one upload, zero image bytes in context. Valid ~48h.
|
|
188
|
+
```
|
|
189
|
+
|
|
134
190
|
**Generate a consistent set (master + scenes):**
|
|
135
191
|
```
|
|
136
192
|
gemini_image_set(
|