{
  "markdown": "# gemini-mcp\n\n[![CI](https://github.com/chrischall/gemini-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/chrischall/gemini-mcp/actions/workflows/ci.yml)\n[![npm](https://img.shields.io/npm/v/@chrischall/gemini-mcp)](https://www.npmjs.com/package/@chrischall/gemini-mcp)\n[![license](https://img.shields.io/npm/l/@chrischall/gemini-mcp)](LICENSE)\n\nMCP server for Google Gemini media generation. Exposes eleven tools to Claude over stdio: list available models, generate/edit/compose images, generate a consistent set of images from a master prompt, multi-turn image refinement (Interactions API), **video** generation (omni), **music** generation (Lyria), an async result poll for long generations, and Files API upload/list/delete for reusable image references. Output is written to disk by default (path returned) or returned inline as base64. Built on the Gemini v1beta API (`generativelanguage.googleapis.com`) using the Nano Banana / Nano Banana Pro (images), omni (video), and Lyria (music) model families.\n\nDeveloped and maintained by AI (Claude Code).\n\n## Environment Variables\n\n| Variable | Required | Description |\n|---|---|---|\n| `GEMINI_API_KEY` | Yes | Your Google Gemini API key ([aistudio.google.com/apikey](https://aistudio.google.com/apikey)) |\n| `GEMINI_IMAGE_MODEL` | No | Override the default image model (default: `gemini-3.1-flash-image`) |\n| `GEMINI_OUTPUT_DIR` | No | Default directory for generated images (default: current working directory) |\n| `GEMINI_INPUT_DIR` | No | Directory to resolve bare input-image filenames against (so `images: [\"foo.jpg\"]` works) |\n| `GEMINI_TIMEOUT_MS` | No | Upstream request timeout in ms (default: `60000`, or `120000` for `image_size: \"4K\"`); each generation tool also takes a per-call `timeout_ms` |\n| `GEMINI_HEARTBEAT_MS` | No | Progress-notification cadence in ms while a generation runs (default: `10000`; `0` disables) — keeps MCP hosts that reset their timeout on progress from timing out long generations |\n| `GEMINI_CHAIN_RETRY_MS` | No | How long to wait out interactions-store lag when a chained call 404s (default: `120000`; `0` disables retrying) |\n\n### Long generations and client timeouts\n\n4K / Pro-model generations can outrun an MCP host's own `tools/call` timeout (error `-32001`).\nThe server sends `notifications/progress` heartbeats so hosts that reset their timeout on\nprogress wait it out. If the host still gives up, the server-side generation usually completes\nanyway: the image is written to the output dir, `gemini_interact` also writes an\n`<image>.json` sidecar recording the `interaction_id`, and `continue_last: true` resumes the\ninteraction the lost response belonged to.\n\n### When a chained call 404s\n\nA 404 on a request carrying `previous_interaction_id` is **not** proof the chain expired. The\nonly 404 body observed live is generic — `\"Requested entity was not found.\"` — and never names\n*which* entity. An unknown or renamed **model id**, and an expired Files API **`files/…` uri**\n(~48h TTL), return exactly the same thing. So the server no longer asserts a cause it can't\nestablish: the upstream text is surfaced verbatim, and `gemini_interact` runs an experiment to\nfind out which it was.\n\nMost often the id isn't missing at all — it just isn't *visible* yet. The interactions store is\neventually consistent, and a freshly created id can 404 while the same id resolves fine minutes\nlater; heavy turns (4K, Pro, `thinking_level: high`) are the likeliest to hit it, which is\nexactly the turn you most want to chain from. So a chained 404 is retried with exponential\nbackoff for up to **120s** (`GEMINI_CHAIN_RETRY_MS`) before anything is declared broken. The\n404 generates nothing and isn't billed, so the wait costs only time.\n\nAfter that budget is spent, the tool looks up that id's sidecar, re-attaches the image it\nproduced, and re-issues the request **without** the chain:\n\n- **The re-issue succeeds** → the chain really was the problem, and you get your image anyway,\n  reported as `chain_recovered: { expired_interaction_id, reanchored_on }`. The 404'd attempt\n  generates nothing, so this costs the one generation you'd have paid for re-anchoring manually.\n- **The re-issue 404s too** → the interaction id was never the cause. You get told exactly that,\n  with the upstream text, and pointed at the model id and any `files/…` uri instead of being\n  sent to chase an interaction that was fine all along.\n- **No sidecar matches the dead id** → the original error, rather than a guess. Re-anchoring on\n  the wrong picture would silently corrupt the edit.\n\nSeparately, `continue_last` no longer dies with the server process: with no in-memory id it\nresumes from the newest `<image>.json` sidecar in the output dir and reports\n`continued_from_sidecar: true`. That case was never an expired chain at all — the interaction\nwas alive upstream the whole time; only our memory of its id was gone.\n\nFor hosts whose timeout can't be tamed (e.g. Claude Desktop, a fixed ~30s cap that ignores\nprogress), two guards make re-issuing safe and unnecessary:\n\n- **`async: true`** returns a `job_id` immediately instead of the image, so the call can't\n  time out at all; poll `gemini_get_result` with the `job_id` until it's `done`.\n- **`idempotency_key`** makes a repeat call idempotent — a retry with the same key returns the\n  recorded result (`reused: true`) instead of billing a second generation. (Even without a key,\n  two identical in-flight calls are deduplicated automatically.)\n\n## Tools\n\n| Tool | Description |\n|------|-------------|\n| `gemini_list_models` | List available Gemini image models and the current default |\n| `gemini_image_generate` | Generate image(s) from a text prompt |\n| `gemini_image_edit` | One-off edits or multi-image composition with a text instruction (for a series of edits, use `gemini_interact`) |\n| `gemini_image_set` | Generate a master image plus N consistent images referencing it |\n| `gemini_interact` | Preferred tool for iterative refinement: multi-turn generation/editing via the Interactions API — chain the returned `interaction_id` via `previous_interaction_id` (or `continue_last: true`) |\n| `gemini_video_generate` | Generate a short video (text→video, image→video, or `edit`) via the Gemini omni model (preview); written to disk as MP4 |\n| `gemini_music_generate` | Generate music from a text prompt via a Lyria model — `lyria-3-clip-preview` (~30s, default) or `lyria-3-pro-preview` (longer, WAV-capable); written to disk as MP3/WAV (preview) |\n| `gemini_get_result` | Fetch an async generation started with `async: true` by its `job_id` (status `running` → `done` result). Lets a long generation outlive a host's `tools/call` timeout |\n| `gemini_token_usage` | Token usage and an estimated USD cost for this session so far. Call it before and after a workflow and subtract to attribute that workflow's spend. Priced per call against each call's own model from a dated rate card (`GEMINI_RATE_CARD` overrides it); there is no account-balance endpoint to read, so this is how spend is attributed |\n| `gemini_upload_file` | Upload an image (or video/audio) to the Gemini Files API once — from a `url`, `data_base64`, or a local `path` — and get a reusable `files/<id>` reference |\n| `gemini_list_files` | List the files currently uploaded under this API key, with MIME types and expiry times |\n| `gemini_delete_file` | Delete an uploaded file before its ~48h expiry (confirm-gated) |\n| `gemini_sign_media` | *(hosted deployments only)* Mint a fresh signed URL for generated media from its `r2_key` — an expired link is not a dead end |\n| `gemini_get_upload_url` | *(hosted deployments only)* Mint a short-lived signed PUT URL so a shell can upload a reference image with no auth header; the PUT returns an `r2_key` usable in `images_r2_keys`, `gemini_save_character`, or `gemini_upload_file` |\n| `gemini_save_character` / `gemini_list_characters` / `gemini_delete_character` | *(hosted deployments only)* Persistent per-account character library: save a reference image + description under a name, then pass `characters: [\"name\"]` on generation tools. No expiry |\n| `gemini_save_style` / `gemini_list_styles` / `gemini_delete_style` | *(hosted deployments only)* Persistent per-account style presets: a reusable prompt fragment (optionally with a reference image), applied by passing `style: \"name\"` on generation tools. No expiry |\n\nGeneration tools also share three throughput/latency controls: `async: true` (return a `job_id`\nimmediately), `max_wait_ms` (wait up to a budget, then hand back the `job_id` — fast results stay\nin-band, slow batches never trip the host timeout), and `idempotency_key` (a retry returns the\nrecorded result instead of re-billing). On a hosted deployment, a `gemini_image_set` result with\nmore than one image also carries a **`bundle_url`** — one signed URL for a zip of every image in\nthe set — and set links are signed for ~7 days instead of the default ~48h. (A set too large to\nzip safely in memory skips the bundle and says so via `bundle_skipped`; the per-image\nlinks are unaffected.)\n\n## Seeing your images (hosted)\n\nOn a hosted deployment there is no filesystem, so a generated image has to come back as\nsomething you can *open*. It does: **every result includes a URL**, with no configuration.\n\n```jsonc\n{\n  \"images\": [\"https://mcp.nullnet.app/b/<account>/gemini/gen/2026-07-29/ab12cd34-a-cat.png?exp=…&sig=…\"],\n  \"media\":  [{ \"url\": \"https://…\", \"r2_key\": \"gen/2026-07-29/ab12cd34-a-cat.png\",\n               \"expires_at\": \"2026-07-31T12:00:00.000Z\",\n               \"curl_hint\": \"curl -sS -o a-cat.png \\\"https://…\\\"\" }]\n}\n```\n\nThose links need no auth header — the signature is in the URL — so they work in a browser, in\n`curl`, and in a chat message. They expire (48h by default) and the objects behind them are\nswept on a retention schedule. The `r2_key` is the durable handle for that window:\n\n- **`gemini_sign_media`** (hosted deployments only) mints a fresh signed URL from an `r2_key`,\n  so an expired link never forces you to re-generate — and re-pay for — the image.\n- **`gemini_upload_file` with `r2_key`** turns media this server generated into a Files API\n  reference (the server reads it back with its own key — no signature for you to mint), so\n  a generated image can become the reference image for the next generation in one cheap call.\n- Idempotent replays (`idempotency_key`) re-mint the URLs inside the recorded result before\n  returning it, so a reused result never carries a dead link.\n\n**Why this matters:** MCP's inline image content blocks (`inline: true`) are visible to the\n*assistant* but many chat clients never render them to the user, and the assistant cannot\nextract bytes back out of its own context to save them elsewhere. A generation could bill\nsuccessfully and be invisible. A URL is the portable answer; `inline` remains available, but\nit is no longer the only way to receive media.\n\n### How the bytes are served\n\nThe host stores each generated object and serves it back at a signed, expiring\nURL — `https://<host>/b/<account>/gemini/<key>?exp=&sig=`. No setup, and no auth\nheader: the signature in the link is the authorization, so `curl` and a browser\nboth work.\n\n**Auth is a signed, expiring URL rather than an unlisted key.** Random keys would\nbe simpler, but they never expire and never revoke: anything that ever logged or forwarded the\nlink keeps working forever. A signature scopes access to one object with a deadline, and\nrotating `MEDIA_URL_SECRET` invalidates every outstanding link at once. The tradeoff is that\nlinks are long and cannot be shortened by hand.\n\n### For assistants relaying a result\n\nShow the user the URL. If your sandbox has network egress to the host, fetching it\nand attaching the bytes as a file gives the nicest result; otherwise present the link itself.\nWhether a given client renders `![](url)` markdown inline varies by client — a bare URL is the\nsafe form, and a markdown link is a reasonable enhancement where you know it renders.\n\n### Retention\n\n| Variable | Default | Effect |\n|---|---|---|\n| `MEDIA_TTL_DAYS` | `7` | Objects older than this are deleted by a daily cron — generated media (`gen/`, legacy `media/`) and signed uploads (`up/`). The character/style library (`lib/`) is exempt: saved entries never expire |\n| `MEDIA_URL_SECRET` | generated | HMAC key for `/media` links; rotate to revoke all outstanding URLs |\n\nSigned-URL lifetime is clamped to `MEDIA_TTL_DAYS`, so a link never outlives the object it\npoints at.\n\n## Sending reference images without burning context\n\nEvery tool that takes a reference image — `gemini_image_generate`, `gemini_image_edit`,\n`gemini_image_set`, `gemini_interact`, plus `gemini_video_generate` (reference stills) and\n`gemini_music_generate` — accepts them four ways. Only one of them costs model context:\n\n| Parameter | Where the bytes travel | Context cost |\n|---|---|---|\n| `images_url` (`master_images_url`) | the **server** downloads the https URL | none |\n| `images_file_uris` (`master_images_file_uris`) | a `files/<id>` reference, already uploaded | none |\n| `images_r2_keys` (`master_images_r2_keys`) | the server reads its **own store** (hosted deployments only) | none |\n| `images` | read off local disk (stdio builds only) | none |\n| `characters` / `style` | saved library entries, attached by name (hosted deployments only) | none |\n| `images_base64` | **through the tool-call JSON** | **~14k tokens per JPEG** |\n\n`images_base64` is the fallback of last resort. It costs roughly 14k tokens per modest photo,\nand it is silently corrupted whenever the file read that produced it was truncated — the\npayload still looks like base64, so the failure surfaces as a bad generation rather than an\nerror. Prefer any of the other three.\n\n### `images_url` — the server fetches it\n\n```jsonc\n{ \"prompt\": \"make it look like winter\", \"images_url\": [\"https://example.com/photo.jpg\"] }\n```\n\nFetches are restricted to public `https://` URLs — private, loopback and link-local hosts are\nrefused (IPv6 literals are parsed, so `[::ffff:7f00:1]` is caught as loopback), every redirect\nhop is revalidated, and each hop is bounded by a timeout. The response must be\n`Content-Type: image/*` and is capped at **15MB**, enforced while streaming rather than trusted\nfrom `Content-Length`. A failure names the offending URL. Anything over 6MB is uploaded to the\nFiles API and referenced by uri instead of inlined, since `generateContent` caps a whole\nrequest near 20MB.\n\n### `images_file_uris` — upload once, reference many times\n\n```jsonc\n// 1. upload\n{ \"tool\": \"gemini_upload_file\", \"url\": \"https://example.com/photo.jpg\" }\n// → { \"file_uri\": \"files/abc123\", \"mime_type\": \"image/jpeg\", \"expires\": \"...\" }\n\n// 2. reference it, as many times as you like\n{ \"prompt\": \"make it winter\",  \"images_file_uris\": [\"files/abc123\"] }\n{ \"prompt\": \"make it sunrise\", \"images_file_uris\": [\"files/abc123\"] }\n```\n\nUploads are retained **~48h**; after that the reference stops resolving (as a generic 404 —\nsee the chained-404 section above). `gemini_image_set` fetches or resolves such a reference\n**once** and passes it to the master and every scene call.\n\nOn stdio builds, a local `images` path that gets referenced **more than once in a session** is\nuploaded to the Files API automatically (keyed on path + mtime + size), so repeated edits of\nthe same photo stop re-sending the bytes. Editing the file invalidates the cached upload.\n\n### Signed upload URLs — no token at all (hosted)\n\nThis is the intended path for an agent with a shell: disk file → curl → `r2_key` → tool call,\nwith the image never entering the conversation. `gemini_get_upload_url` mints a short-lived\n(~10 min) signed **PUT** URL, and the shell uploads with zero auth headers — the signature in\nthe URL is the authorization, mirroring how the download links work.\n\n```bash\n# 1. tool call: gemini_get_upload_url { filename: \"photo.jpg\", content_type: \"image/jpeg\" }\n#    → { upload_url, r2_key, expires_at, curl_hint }\n\n# 2. shell:\ncurl -sS -X PUT -H \"Content-Type: image/jpeg\" --data-binary @photo.jpg \"$UPLOAD_URL\"\n# → { \"r2_key\": \"up/<tenant>/2026-07-31/ab12cd34-photo.jpg\", \"size_bytes\": 812345, ... }\n```\n\nThe signature covers one tenant-scoped object key, the declared content type and the expiry;\nuploads are capped at 15MB, enforced while reading the stream. Only **raster** image types are\naccepted (jpeg/png/webp/gif/avif/heic/heif/bmp/tiff) — SVG is deliberately refused, because an\nSVG is a scriptable document and `/media` serves from the server's own origin. The returned\n`r2_key` is then usable three ways: directly as `images_r2_keys` on any generation tool (the\nserver reads its own bucket — no bytes in the conversation), permanently via\n`gemini_save_character`, or as a ~48h Files API reference via `gemini_upload_file({ r2_key })`.\nUploads themselves follow the media retention schedule (`up/` prefix, default 7 days).\n\n### Character & style library (hosted)\n\nRecurring subjects and styles can be saved once, per account, with **no expiry** (the retention\ncron deliberately skips the library's `lib/` prefix):\n\n```jsonc\n// once:\n{ \"tool\": \"gemini_save_character\", \"name\": \"finn\",\n  \"description\": \"6-year-old boy, curly red hair\", \"image_r2_key\": \"up/…/photo.jpg\" }\n{ \"tool\": \"gemini_save_style\", \"name\": \"bold-cartoon-sports\",\n  \"prompt_fragment\": \"bold cartoon style, thick outlines, saturated colors\" }\n\n// afterwards, on any generation:\n{ \"tool\": \"gemini_image_set\",\n  \"master_prompt\": \"Finn on a soccer field\",\n  \"scenes\": [\"kicking the ball\", \"celebrating a goal\"],\n  \"characters\": [\"finn\"], \"style\": \"bold-cartoon-sports\" }\n```\n\nNaming a character attaches its saved reference image and weaves its description into the\nprompt; naming a style appends its fragment (and attaches its reference image, if it has one).\n`gemini_image_set` passes character references to the master *and every scene call*, which is\nwhat keeps the subject consistent across the set.\n\n## Quick Start\n\n```json\n{\n  \"mcpServers\": {\n    \"gemini\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"@chrischall/gemini-mcp\"],\n      \"env\": {\n        \"GEMINI_API_KEY\": \"your-api-key-here\"\n      }\n    }\n  }\n}\n```\n\nSee [SKILL.md](./skills/gemini-mcp/SKILL.md) for full usage documentation.\n",
  "bytes": 18225,
  "sha": "bb56e8bda6e4c2e817c0eda7ff04d6c7d43bfd18aa2358c74d35b7788ac09efd",
  "repo_slug": "chrischall/gemini-mcp",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_chrischall_gemini_mcp_8ee29b74/readme"
}