Skip to main content
Glama

perchance-mcp

CI

A self-hosted image generator that speaks both MCP and the OpenAI images API.

Endpoint

Use it for

POST /mcp

MCP clients (Claude Desktop, Claude Code, Zed, this chat app, ...)

POST /v1/images/generations

Anything that speaks the OpenAI images API (OpenWebUI, LM Studio, ComfyUI, your own scripts)

GET /health

Uptime checks. No auth.

GET /status

Deep diagnostics: browser state, user key, ad code.

One container. One API key. Both surfaces are protected by it.


1. Why this needs a browser (read this before you try to "optimise" it)

Perchance has no public API. This speaks the same protocol the official embed page uses, reverse engineered from image-generation.perchance.org/embed:

Step

Endpoint

Notes

1

GET perchance.org/api/getAccessCodeForAdPoweredStuff

Returns 64 hex chars as plain text. Works with a plain HTTP client (verified).

2

GET image-generation.perchance.org/api/verifyUser?browserId=..&token=..

Needs a Turnstile token.

3

POST .../api/createTicket

body {"kind","generatorName"}

4

POST .../api/generate

body {"prompt","seed","resolution","guidanceScale",..}

5

image bytes

data URL in the response, or an imageId to fetch

The finding that decides the architecture

Calling step 2 without a token gives you:

{"status": "failed_verification", "reason": "token_required"}

A Turnstile token can only be minted by executing the Turnstile widget in a real browser. No amount of TLS-fingerprint impersonation (curl_cffi, tls-client, ...) gets you there. Verified directly.

The obvious next idea is "put a Cloudflare solver next to the API". That does not work either, and this is worth knowing:

Cloudflare's cf_clearance cookie is bound to the IP that solved it. Verified by taking a clearance cookie produced on IP A and replaying it from IP B: HTTP 403.

So a solver on another host, another container or another Cloud account hands you a cookie that is worthless to you.

Therefore: one browser, in this container, on this IP. It solves the challenge and then makes the API calls itself. No solver, no key handoff, no IP problem. That is the whole design.

Memory

Chromium with --single-process and swiftshader peaks around 600-800 MB. Cloady's free tier gives 1 GB, so it fits, but it is the tightest part of the project. If it gets OOM-killed, drop n to 1 and lower the resolution.


Related MCP server: Flow2API MCP Server

2. Repository layout

perchance-mcp/
├── app/
│   ├── config.py       all settings, every one read from an env var
│   ├── auth.py         the API key middleware
│   ├── browser.py      Chromium session: cloudflare/Turnstile + generation
│   ├── storage.py      writes images to disk
│   ├── images.py       raw bytes -> MCP image content blocks
│   ├── mcp_app.py      the MCP tool
│   ├── http_app.py     the OpenAI-compatible REST endpoints
│   └── main.py         assembles both into one ASGI app
├── Dockerfile          Chromium + Xvfb + the API, one container
├── docker-compose.yml  local development
├── start.sh            entrypoint
├── .env.example        every setting, documented
└── tests/

3. Run it locally first (10 minutes)

Prove it works before touching GitHub.

git clone https://github.com/YOUR-USERNAME/perchance-mcp.git
cd perchance-mcp

cp .env.example .env
python -c "import secrets; print(secrets.token_urlsafe(32))"
# paste that into PERCHANCE_API_KEY= inside .env

docker compose up --build

The first boot takes a minute: Chromium downloads, Xvfb starts, and the browser solves Cloudflare.

Check it:

curl http://localhost:8080/health
# {"service":"perchance-mcp","status":"ok","browser":"ready","auth":"enabled"}

curl http://localhost:8080/status
# browser state, whether the user key was found, etc.

curl -X POST http://localhost:8080/v1/images/generations \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"prompt":"a red circle on green","size":"768x768","n":1}'

4. Push it to GitHub (the part you asked about)

Three commands. Everything else is clicking.

4a. Create the repo

  1. Go to https://github.com/new

  2. Repository name: perchance-mcp

  3. Private — tick it.

  4. Do not tick "Add a README", "Add .gitignore", or "Choose a license".

  5. Create repository.

4b. Send the code up

In a terminal, in the folder that has the files:

cd perchance-mcp

git init
git add .
git commit -m "Initial commit: Perchance MCP + OpenAI-compatible API"
git branch -M main
git remote add origin https://github.com/YOUR-USERNAME/perchance-mcp.git
git push -u origin main

Git asks for a username and a Personal Access Token, not your password. To make one:

  1. GitHub -> avatar -> Settings

  2. Developer settings -> Personal access tokens -> Tokens (classic)

  3. Generate new token -> name perchance, 30 days, tick the repo box

  4. Generate, copy the ghp_... string. You only see it once.

If git push says Support for password authentication was removed, that means it wanted a token. Paste the ghp_... when it asks for a password.

4c. Check

Refresh the GitHub page. Your files should be there.


5. Deploy to Cloady

Cloady's free workspace allows 0.5 vCPU, 1 GB RAM, 5 GB SSD and 200 GB of traffic, no card needed.

5a. Create the app

  1. Sign in at https://cloady.com

  2. New app -> Connect GitHub (read access to your repos)

  3. Pick perchance-mcp, branch main

  4. Region: EU1 (Germany), SIN1, USE1 or USW1

5b. Build

You have two options.

Option A - build from the repo (simplest). Cloady finds the Dockerfile automatically. Nothing to change.

Option B - deploy a prebuilt image. A GitHub Actions workflow (.github/workflows/ci.yml) builds the image on every push and publishes it to GitHub Container Registry as:

ghcr.io/pophvllo-art/perchance-mcp:latest

It is also tagged with the commit SHA (...:0f0ea2a), so you can pin an exact build. The image is about 545 MB compressed (Chromium is most of it) and exposes port 8080.

GHCR packages are private by default, so flip it to public once:

  • GitHub -> your profile -> Packages -> perchance-mcp -> Package settings -> Change visibility -> Public

or from the API, with a token that has the write:packages scope:

curl -X PATCH \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Accept: application/vnd.github+json" \
  -H "Content-Type: application/json" \
  -d '{"visibility":"public"}' \
  https://api.github.com/users/pophvllo-art/packages/container/perchance-mcp

Then in Cloady pick Deploy a Docker image and use that reference.

Either way the result is the same container. Option A is one less moving part; Option B is faster to redeploy because the build already happened.

5c. Resources

Setting

Value

Port

8080

CPU

0.5

RAM

1 GB

Disk

5 GB

5d. Environment variables

In the service's Environment tab:

PERCHANCE_API_KEY = <your 32+ char key>
PUBLIC_URL = https://<the-url-cloady-gives-you>

PUBLIC_URL matters: the MCP SDK blocks any Host header that is not localhost, so without it every MCP request fails with

Error while sending message: Streamable HTTP error: Invalid Host header

Setting PUBLIC_URL to your exact hostname fixes it. If a proxy appends a port to the Host header, the code registers a wildcard for that too.

If you ever cannot know the URL in advance, set PERCHANCE_DISABLE_DNS_REBINDING_PROTECTION=1 instead. The server also turns this check off automatically whenever an API key is configured, because the API key middleware has already rejected every unauthenticated request by then

  • the Host check exists to protect localhost MCP servers from a malicious web page, and against a public key-authenticated endpoint it is pure friction.

Do not put the key in GitHub. Put it in Cloady's environment tab. If you ever commit it by accident it is in your git history forever — change the key.

5e. Deploy and watch

Hit Deploy, open the Logs tab. You want:

API key auth is ON (key length 43).
Listening on 0.0.0.0:8080
Xvfb started
loading embed page (warmup)...
Cloudflare passed
browser ready

Cloady gives you a free TLS certificate, so the URL is https://....


5f. Which option do I pick?

Option A (build from repo)

Option B (prebuilt image)

Setup

none

flip the package public

First deploy

builds on Cloady (5-10 min)

pulls a 545 MB image (2-3 min)

Every redeploy

rebuilds

seconds, just a new tag

Needs GHCR public

no

yes

Start with Option A. It is one less thing to get wrong, and you can move to Option B later without changing a line of code.

6. Verify it is alive

# 1. health (no auth)
curl https://YOUR_URL/health

# 2. deep diagnostics
curl https://YOUR_URL/status -H "Authorization: Bearer YOUR_KEY"

# 3. a real image
curl -X POST https://YOUR_URL/v1/images/generations \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"prompt":"a lighthouse in a storm","size":"768x768"}'

/status tells you exactly where it is broken:

/status says

Meaning

Fix

browser: ready, user_key: ok

fully working

-

browser: starting

still booting

wait, check the logs

user_key: missing

Cloudflare/Turnstile did not clear

check the logs for "Cloudflare passed"

error: ... with 403

the container's IP is flagged

change region, or add a volume so the profile persists

browser: not ready + OOM in the logs

out of RAM

lower n, use 512x512, or go one tier up


7. Connect it to an MCP client

In a URL (works everywhere)

https://YOUR_URL/mcp?api_key=YOUR_KEY

With headers (better, the key stays out of logs)

{
  "mcpServers": {
    "perchance": {
      "url": "https://YOUR_URL/mcp",
      "headers": { "Authorization": "Bearer YOUR_KEY" }
    }
  }
}

Claude Desktop

claude_desktop_config.json:

{
  "mcpServers": {
    "perchance": {
      "command": "npx",
      "args": ["-y", "mcp-remote", "https://YOUR_URL/mcp?api_key=YOUR_KEY"]
    }
  }
}

The tool is called generate_image.


8. Using it as a plain image API

Same shape as OpenAI's images endpoint, so most clients work unchanged:

curl -X POST https://YOUR_URL/v1/images/generations \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
        "model": "perchance",
        "prompt": "a watercolour fox in a birch forest",
        "size": "768x768",
        "n": 2,
        "seed": -1,
        "guidance": 7
      }'
{
  "created": 1792930000,
  "data": [
    {
      "url": "https://YOUR_URL/images/abc123....png",
      "revised_prompt": "a watercolour fox in a birch forest",
      "seed": 42,
      "resolution": "768x768",
      "guidance": 7
    }
  ]
}

Add "response_format": "b64_json" to get the bytes inline instead of a URL.

OpenWebUI: Admin -> Settings -> Connections -> OpenAI -> base URL https://YOUR_URL/v1, paste the key, then pick the perchance model.


9. Gotchas worth knowing

  • The MCP image block is the single most important detail. The tool returns a real MCP image content block, so the model can actually see the picture. Do not "simplify" it to a markdown link, or the image stays visible to the human while becoming invisible to the model.

  • Seed is "close", not "exact". The same seed gives a very similar image but not a byte-identical one. The engine is not bit-deterministic.

  • WebP. The endpoint can return WebP. Pillow is in requirements.txt, so it is converted to PNG automatically.

  • Persistent profile. /data/browser holds the Chromium profile, so the Cloudflare clearance survives restarts. On the free tier there is no persistent volume, so each boot re-solves. If a region starts 403-ing, changing region is the quickest fix.

  • Gallery. Set PERCHANCE_GALLERY_NAME to save to your perchance gallery and get permanent CDN links. Blank = skip.

  • One at a time. Perchance queues one generation per user key; the browser session serialises requests with a lock.

10. Licence

MIT. See LICENSE.

Related MCP Connectors

Related MCP Servers