Skip to main content
Glama
README.md
# Image MCP Server

Production MCP server for image generation, editing, and local background removal. Cursor connects over a public HTTPS URL. Cursor does **not** send an API key, Bearer token, OAuth login, or password.

The Kie AI key stays on the server in `.env`.

## What this is

A FastAPI process that exposes:

- Streamable HTTP MCP at `/mcp`
- `GET /`, `GET /health`, `GET /health/ready`
- `GET /files/{filename}` for generated assets

MCP tools:

- `generate_image` — Kie AI text-to-image with automatic model fallback
- `edit_image` — Kie AI Seedream V4 Edit
- `remove_background` — local `rembg` + `birefnet-general-lite` (no Kie call)

## Architecture

```text
Cursor
  → HTTPS
  → Cloudflare Tunnel
  → FastAPI / MCP (127.0.0.1:8000)
       ├── generate_image → Kie AI
       │     nano-banana-pro → gpt-image-2-text-to-image → other models by cost_rank
       ├── edit_image → bytedance/seedream-v4-edit
       └── remove_background → rembg / birefnet-general-lite (local ONNX)
```

Public MCP endpoint:

```text
https://PUBLIC_DOMAIN/mcp
```

## Capabilities

- Nano Banana Pro first, then GPT Image 2, then automatic fallback
- Results downloaded from Kie, converted to WebP, and re-hosted on `/files`
- Local free background removal
- Rate limits, concurrency semaphore, daily caps
- SSRF protection on inbound image URLs
- systemd + Cloudflare Tunnel installers for Ubuntu 22.04/24.04

## Installation

On Ubuntu 22.04 or 24.04:

```bash
sudo bash install.sh
```

The installer:

1. Installs Python 3.11+, venv, cloudflared
2. Copies the app to `/opt/image-mcp-server`
3. Creates system user `image-mcp`
4. Installs dependencies
5. Tries to preload `birefnet-general-lite`
6. Enables `image-mcp.service`
7. Checks `http://127.0.0.1:8000/health`
8. Offers Cloudflare Quick Tunnel, Named Tunnel, or skip

If model preload fails because of network, installation still succeeds. The next process start retries the download.

## Configure KIE_API_KEY

```bash
sudo nano /opt/image-mcp-server/.env
```

Set:

```env
KIE_API_KEY=your_real_key
```

Restart:

```bash
sudo systemctl restart image-mcp
```

Never put this key in Cursor, in the MCP URL, or in git.

## Quick Cloudflare Tunnel

Use this if you do not have a domain. The installer can start it, or:

```bash
sudo systemctl enable --now cloudflared-image-mcp
sudo bash /opt/image-mcp-server/get-mcp-url.sh
```

You get a URL like:

```text
https://xxxxx.trycloudflare.com/mcp
```

**Quick Tunnel URLs change after every cloudflared restart.** After reboot or `systemctl restart cloudflared-image-mcp`, run `get-mcp-url.sh` again and update Cursor.

Also set:

```env
PUBLIC_BASE_URL=https://xxxxx.trycloudflare.com
```

so `/files` links are absolute HTTPS URLs that Kie and Cursor can fetch.

## Stable Cloudflare Tunnel

Recommended production setup:

```text
Internet → mcp.example.com → Cloudflare Tunnel → 127.0.0.1:8000
```

```bash
sudo cloudflared tunnel login
sudo cloudflared tunnel create image-mcp
sudo cloudflared tunnel route dns image-mcp mcp.example.com
```

Copy and edit the example config. Do not hardcode a tunnel ID from this repo:

```bash
sudo mkdir -p /etc/cloudflared
sudo cp /opt/image-mcp-server/deploy/cloudflared-config.example.yml /etc/cloudflared/config.yml
sudo nano /etc/cloudflared/config.yml
```

Replace `IMAGE_MCP_TUNNEL_ID` and `mcp.example.com`, then:

```bash
sudo cloudflared tunnel run image-mcp
```

Or install Cloudflare's `cloudflared` systemd unit and enable it.

Then:

```env
PUBLIC_BASE_URL=https://mcp.example.com
ENABLE_DNS_REBINDING_PROTECTION=true
MCP_ALLOWED_HOSTS=mcp.example.com,127.0.0.1,localhost
```

```bash
sudo systemctl restart image-mcp
```

## Connect Cursor

No authentication from Cursor.

User MCP config (`~/.cursor/mcp.json` or Cursor Settings → MCP):

```json
{
  "mcpServers": {
    "image-mcp": {
      "url": "https://mcp.example.com/mcp"
    }
  }
}
```

Authentication: none. Do not add `headers`, Bearer, API key, or OAuth.

After a Quick Tunnel restart, replace the URL with the new `get-mcp-url.sh` value.

## MCP tools

| Tool | When the model should call it |
| --- | --- |
| `generate_image` | Create / draw / generate a new image from text |
| `edit_image` | There is a source image and the user wants it changed |
| `remove_background` | Transparent background / cutout / remove background |

The user does not pick Kie model IDs. The server does.

## Model fallback

`generate_image` always starts with `nano-banana-pro`. If that attempt succeeds, it stops.

If Nano Banana Pro fails for a retryable reason (timeout, HTTP 429/5xx, task `fail`, empty result, overload), the server tries `gpt-image-2-text-to-image`, then the remaining enabled text-to-image models sorted by `cost_rank` in `config/models.yaml`:

```text
nano-banana-pro
→ gpt-image-2-text-to-image
→ z-image
→ bytedance/seedream-v4-text-to-image
→ qwen3/pro-text-to-image
→ seedream/5-pro-text-to-image
```

Pinned models run first by `priority`. The rest use `cost_rank`. Kie public pricing changes; this repo does not invent live dollar prices.

Fallback does **not** continue on:

- missing prompt
- invalid Kie API key (401/403)
- insufficient Kie credits (402)
- fatal server configuration errors

Adapters translate the common MCP input (`prompt`, `aspect_ratio`, `quality`, `output_format`) into each model's real Kie payload. Edit uses only `bytedance/seedream-v4-edit` and is never used as a text-to-image fallback.

Kie Market API used here (docs.kie.ai):

- `POST /api/v1/jobs/createTask`
- `GET /api/v1/jobs/recordInfo?taskId=`

## WebP output

Public `/files` URLs are WebP. Kie may return PNG or JPEG; the server converts them locally before hosting. Background removal keeps transparency in lossless WebP.

## Background removal

Local only:

```text
rembg + birefnet-general-lite + onnxruntime CPU
```

The rembg session is created once at startup and reused. BRIA RMBG is not the default and is not used.

## Security

Public MCP has no Cursor credential, so the server enforces:

- `RATE_LIMIT_PER_MINUTE`
- `MAX_CONCURRENT_GENERATIONS` (asyncio.Semaphore)
- `MAX_GENERATIONS_PER_DAY`
- `MAX_GENERATIONS_PER_IP_PER_DAY`

Image URL fetch:

- HTTP/HTTPS only
- blocks localhost, loopback, private IPv4/IPv6, link-local, metadata endpoints
- re-validates redirect targets
- checks magic bytes with Pillow, not the filename

`/files/{filename}` only serves UUID-style names from `data/outputs`. Path traversal is rejected.

`KIE_API_KEY` is never returned in MCP results, logs, or URLs.

## Logs

JSON logs include `request_id`, `tool`, `model`, `duration_ms`, `success`, `fallback`, `http_status`, `task_id`.

They do not include the API key, Authorization headers, base64 bodies, or full prompts.

```bash
sudo journalctl -u image-mcp -f
```

Usage rows are stored in SQLite (`METRICS_DB_PATH`). There is no public billing dashboard.

## Update

```bash
cd /path/to/this/repo
sudo rsync -a --exclude '.venv' --exclude '.env' --exclude 'data' ./ /opt/image-mcp-server/
sudo -u image-mcp /opt/image-mcp-server/.venv/bin/pip install -r /opt/image-mcp-server/requirements.txt
sudo systemctl restart image-mcp
curl -fsS http://127.0.0.1:8000/health
```

## Local development

Python 3.11+:

```bash
python -m venv .venv
.venv\Scripts\activate
pip install -r requirements.txt
copy .env.example .env
python -m pytest
uvicorn app.main:app --host 127.0.0.1 --port 8000
```

Health: `http://127.0.0.1:8000/health`

A public Cloudflare URL is not created in a local Windows checkout. After Ubuntu install, run:

```bash
sudo bash /opt/image-mcp-server/get-mcp-url.sh
```

## Troubleshooting

| Symptom | What to check |
| --- | --- |
| `/health` fails | `sudo systemctl status image-mcp` and journal logs |
| Cursor cannot connect | Confirm HTTPS `/mcp`, Cloudflare is up, `get-mcp-url.sh` |
| 421 from MCP | Hostname not allowed; set `MCP_ALLOWED_HOSTS` or keep DNS rebinding off for Quick Tunnel |
| generate_image says key missing | `KIE_API_KEY` in `/opt/image-mcp-server/.env`, then restart |
| 402 / insufficient credits | Top up Kie; fallback will not help |
| remove_background not ready | First-time ONNX download; check disk and network, restart service |
| edit from base64 fails | `PUBLIC_BASE_URL` must be a URL Kie can fetch |
| Files 404 after a few days | `OUTPUT_TTL_HOURS` cleanup (default 72h) |

Uninstall:

```bash
sudo bash uninstall.sh
sudo bash uninstall.sh --purge
```

## License notes

Application code is provided for this project. Third-party licenses are listed in `LICENSES.md`. BiRefNet code is MIT; confirm current weight terms before commercial deployment.