Skip to main content
Glama

CPA Image MCP

English | 简体中文

A remote MCP wrapper for an existing CLIProxyAPI image endpoint. It lets Claude and other cloud-based MCP clients generate, edit, and download full-resolution images, with reusable signed download links and automatic retention cleanup.

All examples use images.example.com. Replace it with your own domain when deploying. This repository contains no real account credentials, deployment domains, images, logs, or environment files.

Features

Tool

Purpose

generate_image

Generate an image and return its ID, requested/actual dimensions, and a 512px preview

edit_image

Edit a retained/uploaded image by ID, or an external base64/URL source; create a new ID

get_image

Return full-resolution MCP ImageContent/base64, optionally resizing proportionally or converting to JPEG

list_images

List images still within their retention period and today's usage

list_models

Refresh the CPA image-model list and show the current automatic choice; no generation

create_upload_url

Reserve an image ID and issue a short-lived, one-use upload link

The four image-result tools include download_url and download_expires_at in their results. Download links require no API request headers, remain valid for 72 hours by default, and can be reused. Original images are retained for 3 days by default, with cleanup at startup and every hour.

Note the deletion default for get_image: delete_after_return=True deletes the original image from the server after the HTTP response containing the image at its original dimensions has been fully sent. Its download links then stop working. Pass delete_after_return=False to keep the links usable or to continue editing or syncing the image. Thumbnails and ordinary link downloads do not trigger this deletion.

Related MCP server: free-ai-image-gen-tools-gateway

How it works

flowchart LR
  Claude[Claude / MCP client] -->|HTTPS + Authorization| Tunnel[Cloudflare Tunnel]
  Tunnel -->|HTTP 127.0.0.1:8787| MCP[CPA Image MCP]
  MCP -->|Local API key| CPA[CLIProxyAPI /v1/images]
  CPA --> MCP
  MCP --> Store[Original images + manifest]
  Browser[Browser / download client] -->|HTTPS + expiry + HMAC| Tunnel
  MCP -->|Attachment after signature verification| Browser
  • The MCP service listens only on the loopback address. No additional public port is needed on a NAT VPS.

  • MCP connects to CPA at http://127.0.0.1:8317/v1 by default and sends MCP heartbeats while waiting for image generation.

  • Generation and editing return small previews. Original images are transferred separately as MCP image content or HTTPS attachments.

  • HMAC uses an independent random key and binds the image ID, relative path, and expiry time. URLs contain neither the CPA API key nor the MCP token.

  • size is passed as width × height, with additional prompt guidance for dimensions, orientation, and aspect ratio. Returned dimensions are decoded from the actual image bytes. If the upstream service does not honor the requested dimensions, the result explicitly reports the mismatch and preserves the upstream image.

This project does not deploy, sign in to, or modify CPA, and does not provide accounts. You need your own CPA instance with an authorized, working image backend. The verified setup includes Debian 12, Python 3.11, CLIProxyAPI 8.0.30, and MCP Python SDK 1.30.0. For other backends, check image endpoint and model compatibility.

1. Prepare your VPS and CPA

The installer supports Linux with systemd and Python 3.10+. It stops explicitly on Alpine/OpenRC rather than automatically switching service managers. Make sure Python venv/ensurepip is available first; the installer does not automatically upgrade system packages.

On Debian/Ubuntu, install the prerequisites:

sudo apt-get update
sudo apt-get install -y python3 python3-venv ca-certificates curl git

Confirm that CPA's /v1/images/generations and /v1/images/edits endpoints work and support your configured model. The service also reads /v1/models for automatic model discovery; it does not read or modify CPA's configuration or account files.

2. Install the service

git clone https://github.com/Bing2000me/cpa-image-mcp.git
cd cpa-image-mcp
sudo bash install.sh

On the first run, the installer only creates /etc/cpa-image-mcp.env with permissions 0600 and generates separate random values for MCP_TOKEN and DOWNLOAD_SIGNING_KEY. It does not print the keys, create the service user, or install the application or service yet.

Edit the file on the VPS:

sudo nano /etc/cpa-image-mcp.env

Set at least:

CPA_API_KEY=your-own-cpa-api-key
CPA_BASE=http://127.0.0.1:8317/v1
PUBLIC_HOST=images.example.com

Replace the example domain and keep the automatically generated random keys. Quote values containing spaces, #, or other special characters. Leave MCP_SECRET empty to use request-header authentication.

Run the installer again, then check the service:

sudo bash install.sh
systemctl is-active cpa-image-mcp
curl http://127.0.0.1:8787/healthz

The expected outputs are active and ok, respectively. The application is installed at /opt/cpa-image-mcp, stores data in /var/lib/cpa-image-mcp/images, runs as the dedicated cpaimg user, and starts at boot. During upgrades, existing code, environment configuration, and the systemd unit are backed up to /var/backups/cpa-image-mcp/<UTC-timestamp>/. The configuration helper also backs up the environment file before filling in missing settings. Upgrades preserve existing non-empty credentials.

3. Connect your existing HTTPS tunnel

Add a Public Hostname / application route to your existing Cloudflare Tunnel:

Field

Example

Hostname

images.example.com — replace with your own domain

Service type

HTTP

Service URL

127.0.0.1:8787

Cloudflare provides HTTPS externally; the connection to the origin uses HTTP. Do not enter https://127.0.0.1:8787. Keep existing routes and leave CPA and VPN ports unchanged.

If you manage the tunnel with a local YAML file, add the route before the catch-all 404 rule:

ingress:
  # Keep your existing routes here.
  - hostname: images.example.com
    service: http://127.0.0.1:8787
  - service: http_status:404

Back up the configuration first, then follow the official Cloudflare Tunnel documentation to validate it, bind DNS, and apply the change. The installer does not modify tunnels, DNS, or firewall rules.

Check from another device:

curl https://images.example.com/healthz
curl -s -o /dev/null -w '%{http_code}\n' -X POST https://images.example.com/mcp

The expected outputs are ok and 401. If the local check succeeds but the public endpoint returns 502, first verify the origin's HTTP service type and address.

4. Set up a Claude custom connector

Setting

Value

URL

https://images.example.com/mcp — replace with your own domain

Authentication

No sign-in — this service uses an API request header and does not implement OAuth

Request header name

Authorization

Request header value

Bearer <MCP_TOKEN> — include one space after Bearer

Read MCP_TOKEN from the environment file on the VPS and enter it directly into the client's private settings. MCP_TOKEN is the MCP service's credential, separate from CPA_API_KEY. After connecting, you should see six tools.

Example request for generate_image:

{"prompt":"A watercolor lighthouse at dusk","size":"1024x1536","quality":"low","name_hint":"lighthouse"}

To keep the original available for repeated downloads during the link's validity period, call get_image with:

{"image_id":"ID-from-the-generation-result","delete_after_return":false}

If you only want to download the image, you can use the download_url from the generation result or list_images directly, without calling get_image again.

Configuration

See .env.example for the complete template. Keep real environment files outside the repository.

Variable

Default

Purpose

CPA_API_KEY

Required

API credential for your own CPA instance

CPA_BASE

http://127.0.0.1:8317/v1

Base URL of CPA on the same VPS; adjust the port as needed

IMAGE_MODEL

auto

Automatically select the newest generation, or set a fixed model name

IMAGE_MODEL_PREFER

Empty

Preferred variant within the newest generation, e.g. sunburst

IMAGE_MODEL_FALLBACK

gpt-image-2

Concrete fallback model for automatic selection

UPLOAD_TTL_MINUTES

15

Expiry of one-use upload links; positive integer minutes

PUBLIC_HOST

Replace the example

Public HTTPS hostname, without a scheme or path

MCP_TOKEN

Randomly generated by the installer

Request-header authentication; at least 24 characters using letters, digits, _, or -

MCP_SECRET

Empty

Optional secret path segment; usually unnecessary

DOWNLOAD_SIGNING_KEY

Independently generated by the installer

HMAC signing key, at least 32 characters; changing it invalidates existing links

DOWNLOAD_TTL_HOURS

72

Link validity in hours; a positive integer

IMAGE_RETENTION_DAYS

3

Image retention in days; a positive integer

DAILY_LIMIT

40

Maximum daily generation/editing calls; deleting images does not restore quota

PREVIEW_PX

512

Maximum preview side length

PORT

8787

Local listening port; also update the tunnel origin if changed

TZ_NAME

Asia/Shanghai

Time zone for date grouping and daily quota

IMAGE_DIR

/var/lib/cpa-image-mcp/images

Data directory, used with systemd's StateDirectory

After changing the environment file, run sudo systemctl restart cpa-image-mcp. CPA does not need to be restarted.

Automatic image models

IMAGE_MODEL=auto is the default. CPA's authenticated {CPA_BASE}/models response is the source of availability; the MCP does not guess model names from release announcements.

  1. Consider IDs starting with gpt-image- that have a numeric version. Exclude dated snapshots such as gpt-image-3-2026-10-25 or gpt-image-3-sunburst-20261025 from automatic selection.

  2. Compare version components numerically: 1.5 < 2 < 2.5 < 3; a variant preference never downgrades to an older generation.

  3. Within the newest generation, choose IMAGE_MODEL_PREFER if its exact variant exists; otherwise prefer the plain name, then the alphabetically first variant.

  4. Cache a successful list for 30 minutes and refresh on the next generation/edit request after expiry. A failed refresh keeps the last successful list, with a brief 30-second retry backoff. list_models explicitly refreshes without generating an image.

  5. If discovery is unavailable/empty, use IMAGE_MODEL_FALLBACK. If an automatically selected model is explicitly rejected as unsupported/not found (HTTP 400/404/422), retry once with that fallback and report it. Do not retry auth, quota, timeout, quality/size parameter errors, or 5xx errors. Explicit/fixed model requests do not silently fall back.

generate_image and edit_image accept model="auto". Omit it unless the user asks for a specific model. The default respects IMAGE_MODEL: an existing fixed environment setting remains fixed; a specific per-call model overrides it. Results and manifest records report the actual model requested from CPA and any fallback. list_models shows the latest auto-policy choice plus the environment configuration.

For example, with 2, 2.5, 2.5-flare, and 2.5-sunburst, the default is gpt-image-2.5; IMAGE_MODEL_PREFER=sunburst chooses gpt-image-2.5-sunburst. New generations require upgrading CPA first. Once CPA advertises them, MCP recognizes them automatically without changing the auto configuration.

Generation quality supports forwarding low, medium, high, plus auto, xhigh, and max only on newer models that implement them. Support is determined by the actual CPA/backend model, not by this wrapper; unsupported quality is reported without an automatic retry. Sizes use width × height or auto and likewise remain backend-dependent.

Upload a user photo, then edit it

For Claude user attachments, prefer this upload channel. Do not ask Claude to transcribe an attachment into base64. MCP cannot read a file from the user's computer or dereference a private Claude attachment ID by itself.

  1. Ask Claude to call create_upload_url. It returns upload_url, image_id, and upload_expires_at.

  2. Open upload_url in a phone or desktop browser, select a PNG/JPEG/WebP image, and upload. The minimal page uses a same-origin PUT request. Alternatively, upload from a machine that has the file:

    curl --fail-with-body --upload-file ./photo.jpg '<upload_url>'
    # Raw POST is also supported:
    curl --fail-with-body -X POST --data-binary @./photo.jpg '<upload_url>'
  3. No MCP_TOKEN is required for uploading. The signed link expires after 15 minutes by default and permits only one successful upload; opening the browser page does not consume it.

  4. After upload, use the reserved image_id with edit_image, get_image, or list_images. Uploading does not consume daily generation quota. Retention, signed downloads, and get_image deletion rules are the same as for generated images.

Example edit arguments after upload:

{"image_id":"uploaded-ID-from-create_upload_url","prompt":"Change the background to a watercolor garden; preserve the person's face."}

Only PNG/JPEG/WebP with a valid decoded image stream are accepted, up to 20 MiB and 40 megapixels. Invalid uploads can be corrected and retried while the ticket remains pending. Concurrent attempts are rejected, and a successfully used link cannot be reused. If an upload is interrupted by a service restart, request a fresh link. Uploads also preserve at least 128 MiB of free filesystem space; low-space writes are refused (507), and a storage failure requires a new upload link. Uploaded originals use the same startup/hourly, default three-day retention cleanup as generated images.

Programmatic external sources

For API clients that already possess image bytes or a public URL, edit_image also accepts exactly one of image_id, image_base64, or image_url. Raw base64 and data:image/...;base64,... are supported, but are not the recommended Claude attachment workflow.

{"prompt":"Make the background blue","image_url":"https://images.example.com/source.jpg","model":"auto"}

URL inputs must be directly accessible public HTTP(S) images. Redirects are revalidated; private/reserved addresses and credential-bearing URLs are rejected. Connections are pinned to the validated address while preserving TLS hostname verification. CPA/MCP credentials, cookies, and environment proxies are never forwarded. Downloads have byte/pixel/time limits. A private upload URL inside Claude, a login page, or a local file path is not an accessible source. Only the resulting edited image is stored for base64/URL inputs; the input URL/base64 is not saved in the manifest.

A signed URL looks like this:

https://images.example.com/download/<image_id>?expires=<UNIX-seconds>&token=<HMAC-signature>
  • The same link supports repeated GET or HEAD requests. Downloading does not consume generation quota or delete the original image.

  • The effective expiry is the earlier of “current time + link validity” and “image creation time + retention period.” Issuing a new link does not extend image retention.

  • download_expires_at is a timezone-aware ISO 8601 UTC timestamp.

  • Responses include the image's actual Content-Type, Content-Disposition: attachment with the original filename, and headers that disable caching.

  • Invalid signatures and missing or deleted originals return 404. A valid signature with an expired link or retention period returns 410. Anyone with the link can download the image, so treat the link as a temporary credential.

  • By default, get_image deletes the original after the HTTP response containing the image at its original dimensions has been fully sent. The server cannot confirm whether the client saved it. Pass delete_after_return=False to retain it. Resized images do not trigger deletion.

Cleanup runs at startup and every hour. It removes expired originals, associated thumbnails, and manifest entries, as well as expired orphaned images in the image directory. It also removes associated records when an original is missing. Expired or missing entries are hidden from image lists and sync manifests immediately, without waiting for the next physical cleanup. Cleanup stays within the image directory and does not delete usage files. The 512px previews are generated in memory without extra files on disk.

Optional: sync original images to your computer

sync_images.py downloads only originals that are not already present locally. Only images still retained on the server can be synced; images already deleted by get_image cannot be synced again.

Set these variables in your computer's private environment configuration:

export CPA_IMG_BASE="https://images.example.com"
export CPA_IMG_TOKEN="<your-MCP_TOKEN>"
python3 sync_images.py ./local-images

To filter by date:

python3 sync_images.py ./local-images --since 2026-01-01

The sync client sends an explicit User-Agent. If Cloudflare still rejects requests, check the access controls on your domain.

Testing and development

python3 -m venv .venv
.venv/bin/python -m pip install -r requirements.txt
.venv/bin/python -m unittest discover -s tests -v
bash -n install.sh

Tests use only synthetic images, temporary directories, and fake credentials. They do not call CPA or consume generation quota. Coverage includes original image bytes, resizing and formats, dimension forwarding, deletion after a response finishes sending, retention on errors or disconnects, signature tampering, expiry, repeated downloads, response headers, cleanup boundary protection, lifecycle behavior, and configuration generation, permissions, backups, and idempotency. The GitHub Actions template supports testing multiple Python versions. It is not enabled by default. To enable it, use GitHub authorization with workflow permission, copy it to .github/workflows/tests.yml, and commit the file.

Project layout:

server.py                  MCP tools, upload/download routes, and cleanup
model_selection.py         Version selection, cached discovery, and fallback policy
image_inputs.py            Bounded external image validation/download
upload_manager.py          Persistent signed one-use upload tickets
install.sh                 systemd installer
scripts/configure_env.py   Private environment file generation, validation, and health checks
cpa-image-mcp.service      systemd unit
.env.example               Configuration template with empty credentials
sync_images.py             Optional local sync client
tests/                     Isolated tests without real credentials
read.md                    Brief setup guide (Chinese)

Troubleshooting

  • Missing ensurepip / venv: Install your distribution's python3-venv package rather than switching service frameworks to work around it.

  • Service fails to start: Run journalctl -u cpa-image-mcp -n 50 on the server. Remove sensitive content before sharing logs.

  • Original image link no longer works: Check download_expires_at, whether the image has exceeded its retention period, and whether get_image deleted it by default.

  • Portrait dimensions do not match: Use the reported actual dimensions. The service preserves the upstream image; it does not rotate or stretch it to fake the requested dimensions, or automatically repeat generation to probe for a match.

  • A large image exceeds the MCP client's size limit: Use the signed download link, or request a smaller image with max_side / format="jpeg".

This project depends on the MCP Python SDK, httpx, and Pillow. See LICENSE for the MIT license.

Related MCP Connectors

Related MCP Servers