image-gen-mcp
Enables generating images from text prompts using Google Gemini's native image models, with configurable aspect ratios and resolution, returning images inline or as signed URLs for larger outputs.
README
image-gen-mcp
An MCP server that generates images with Google's Gemini native image models ("Nano Banana"), served over Streamable HTTP.
One tool, generate_image. No state anywhere, and no auth of its own —
in production it runs as a backend behind
mcp-oauth-proxy, deployed by
cloudrun-mcp-deployment.
DESIGN.md explains why it is built this way; this file explains how to run it.
Quick start (local, token auth)
uv sync
export GEMINI_API_KEY="…" # from Google AI Studio
export IMAGE_MCP_TOKEN="$(openssl rand -base64 32)"
uv run python -m image_gen_mcp
Point a client at http://127.0.0.1:8080/mcp with
Authorization: Bearer $IMAGE_MCP_TOKEN. For Claude Code:
claude mcp add --transport http image-gen http://127.0.0.1:8080/mcp \
--header "Authorization: Bearer $IMAGE_MCP_TOKEN"
The tool
generate_image(prompt, aspect_ratio="1:1", image_size="1K", model=None)
| Parameter | Values |
|---|---|
prompt |
free text, up to IMAGE_MCP_MAX_PROMPT characters |
aspect_ratio |
1:1 16:9 9:16 4:3 3:4 3:2 2:3 21:9 4:5 5:4 |
image_size |
1K 2K 4K — 2K/4K need a capable model and a bucket |
model |
optional; must be in IMAGE_MCP_ALLOWED_MODELS |
Images at or under IMAGE_MCP_INLINE_MAX_BYTES (default 1.5 MB) come back
inline and render in the chat. Larger ones are uploaded to Cloud Storage and
returned as a signed URL. Structured output always reports the true pixel
dimensions, the model used, and which delivery route was taken.
Model / resolution support
| Model | 1K | 2K | 4K |
|---|---|---|---|
gemini-2.5-flash-image (default) |
✅ | — | — |
gemini-3-pro-image-preview / gemini-3-pro-image |
✅ | ✅ | ✅ |
gemini-3.1-flash-lite-image |
✅ | ✅ | ✅ |
Unsupported combinations are rejected at the tool boundary, in milliseconds, with a message naming what is supported — rather than after a 30-second round trip.
Configuration
Everything comes from the environment. See .env.example for the annotated
list; the essentials:
| Variable | Required | Purpose |
|---|---|---|
GEMINI_API_KEY |
yes | Google AI Studio key |
IMAGE_MCP_TOKEN |
unless proxied | static bearer token |
IMAGE_MCP_MODEL |
default model | |
IMAGE_MCP_ALLOWED_MODELS |
models a caller may select | |
IMAGE_MCP_TRUST_PROXY_HEADERS |
behind proxy | take identity from X-Auth-* |
ALLOWED_EMAILS |
optional narrowing of the proxy's allowlist | |
IMAGE_MCP_GCS_BUCKET |
for 2K/4K | bucket for oversized images |
The server refuses to start rather than run misconfigured: no API key, and no auth boundary at all (neither a static token nor proxy mode) are both startup failures.
Auth
Two shapes, and the server refuses to start in neither.
Behind the proxy (production). Set IMAGE_MCP_TRUST_PROXY_HEADERS=1. The
proxy authenticates the user against Google, enforces its allowlist, strips the
client's Authorization header, and forwards the identity as X-Auth-Email /
X-Auth-Subject / X-Auth-Scope. This server reads those and requires no token
of its own — a request without X-Auth-Email gets a 401.
Trusting headers is safe only because nothing else can reach the process: in the Cloud Run multi-container layout the backend declares no ingress port, so the only things that can open a socket to it are the proxy in the same instance and the startup prober. Never enable this on a routable port.
ALLOWED_EMAILS is optional here and narrows the proxy's allowlist — useful
when the proxy admits a whole domain but image generation should cost money for
only a few people. Unset means "whoever the proxy admitted".
Token (local, Claude Code). Leave IMAGE_MCP_TRUST_PROXY_HEADERS unset and
set IMAGE_MCP_TOKEN. Callers send Authorization: Bearer <token>; the
X-Auth-* headers are ignored entirely, since without the proxy they are just
untrusted request data.
The server does not terminate OAuth and has no /authorize, /token or
/register endpoints. Setting GOOGLE_OAUTH_CLIENT_ID or _SECRET is a
startup error rather than a silent no-op — those belong on the proxy.
Building the image
.github/workflows/build.yml runs the tests, builds the image, and publishes
it to GitHub Container Registry. It does not deploy — deployment is handled
by a separate workflow or repository.
| Event | Test | Build | Push |
|---|---|---|---|
| pull request | ✅ | ✅ | — |
push to main |
✅ | ✅ | latest, sha-<full-sha> |
tag v* |
✅ | ✅ | 1.2.3, 1.2, sha-<full-sha> |
Published as ghcr.io/ramzpat/image-gen-mcp. Nothing needs configuring: the
workflow authenticates with the built-in GITHUB_TOKEN.
Consuming it from a deploy workflow
Deploy by digest, not by tag. A pull-through cache in front of a mutable
tag like :latest will happily serve a previous image; a digest cannot go
stale. Each run prints the digest to its job summary, and the workflow is
callable if you want to build and deploy in one pipeline:
jobs:
build:
uses: ramzpat/image-gen-mcp/.github/workflows/build.yml@main
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- run: echo "deploying ${{ needs.build.outputs.image }}@${{ needs.build.outputs.digest }}"
From a separate repository, resolve the digest at deploy time instead:
DIGEST=$(docker buildx imagetools inspect \
ghcr.io/ramzpat/image-gen-mcp:latest --format '{{.Manifest.Digest}}')
The GHCR package is private by default. A deploy job in another repository
needs either a PAT with read:packages, or the package set to public under
its GitHub package settings.
Deployment
Deployed by
cloudrun-mcp-deployment
(.github/workflows/deploy-image-gen-mcp.yml), which runs this image as the
backend container of a Cloud Run multi-container service with mcp-oauth-proxy
in front. That repo owns the GCP project, region, allowlist, and secrets; this
one only publishes the image.
What that deployment sets on this container:
PORT / HOST |
8000 / 0.0.0.0 (set by the shared deploy action) |
IMAGE_MCP_TRUST_PROXY_HEADERS |
1 |
GEMINI_API_KEY |
from the service's GitHub Environment secret |
| entrypoint | /app/.venv/bin/python -m image_gen_mcp |
HOST=0.0.0.0 rather than loopback is required, not a leak: Cloud Run's
startup prober runs outside the container's network namespace and cannot reach
a loopback-only socket. Only the container declaring --port (the proxy)
receives ingress, so the backend stays unreachable from outside the instance.
Cloud Storage for 2K/4K
gcloud storage buckets create gs://BUCKET --uniform-bucket-level-access
gcloud storage buckets update gs://BUCKET \
--lifecycle-file=<(echo '{"rule":[{"action":{"type":"Delete"},"condition":{"age":30}}]}')
# The runtime service account signs URLs through the IAM Credentials API,
# because it has no private key file. It needs this role *on itself*:
gcloud iam service-accounts add-iam-policy-binding RUNTIME_SA \
--member="serviceAccount:RUNTIME_SA" --role=roles/iam.serviceAccountTokenCreator
gcloud storage buckets add-iam-policy-binding gs://BUCKET \
--member="serviceAccount:RUNTIME_SA" --role=roles/storage.objectAdmin
Omitting the serviceAccountTokenCreator binding is the most common way signed
URLs ship broken.
Tests
uv run pytest -q
57 tests: startup guards, identity resolution in both shapes (proxy headers, static token, and each ignoring the other's credential), tool validation and delivery, and end-to-end smoke tests driving a real MCP client over real HTTP against uvicorn — in token mode and in the proxied production shape.
Cost
Every allowlisted user draws on one API key. The controls, in order of how much
they actually help: --max-instances, IMAGE_MCP_MAX_CONCURRENCY, a GCP
billing budget alert, and IMAGE_MCP_RATE_PER_HOUR. The rate limit is counted
per instance, so its true ceiling is IMAGE_MCP_RATE_PER_HOUR × --max-instances.
Recommended Servers
playwright-mcp
A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.
Magic Component Platform (MCP)
An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.
Audiense Insights MCP Server
Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.
VeyraX MCP
Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.
graphlit-mcp-server
The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.
Kagi MCP Server
An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.
E2B
Using MCP to run code via e2b.
Neon Database
MCP server for interacting with Neon Management API and databases
Exa Search
A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.
Qdrant Server
This repository is an example of how to create a MCP server for Qdrant, a vector search engine.