photopea-api
An MCP server that provides headless, read/write access to Photopea for image editing, including layers, text, filters, and document operations, enabling agents to edit images programmatically.
README
photopea-api
A headless, read/write client for Photopea — plus an MCP server, so Claude Code (or any agent) can edit images on its own.
from photopea_api import Photopea
async with Photopea.session() as pp:
await pp.open_path("photo.jpg")
await pp.draw.gradient(["#e63946", "#1d3557"], angle=45)
await pp.text.add("SALE", x=40, y=120, size=64, color="#ffffff")
await pp.filter.gaussian_blur(2)
await pp.export_to("out.psd") # layers preserved
No visible browser window. No tab to keep open. No human in the loop.
On Photopea. This drives Photopea through Live Messaging, its own documented and sanctioned automation API. Nothing here bypasses ads, defeats a protection, or touches the paid AI features (Remove BG, Magic Cut) that cost its author money per call — Photopea is one developer's ad-funded product and those features should not be driven programmatically. It opens one editor per session and does local, deterministic editing. If you need volume, self-host with Photopea-Offline rather than pointing a fleet at photopea.com.
Why this exists
Photopea has no backend: it is a client-side WASM app, so there is nothing to
scrape and no API to call. The only sanctioned automation surface is
Live Messaging — postMessage a string of
JavaScript into an iframe and read what comes back. That means automating
Photopea always means running Photopea in a browser.
Two projects got halfway there. attalla1/photopea-mcp-server
has the MCP layer but opens a visible browser tab the user must leave alone,
and has been unmaintained since April 2026. yikuansun/HeadlessPhotopea
runs headless but exposes no agent interface. Nobody combined the two.
This does, and fixes the failure modes the reference implementation ships with — see RECON.md for the measurements behind each of the following.
| reference | here | |
|---|---|---|
| browser | visible tab, must stay open | headless, owned by the process |
| call framing | counts "done" messages |
unique sentinel per call |
| gradients | 16 solid bands | true ramps (206 distinct colours over 300 px) |
| text | fails silently right after boot | blocks until fonts actually rasterise |
| colours | hexValue assignment → black |
SolidColor components |
| send layer to front | move(doc, PLACEATBEGINNING) — a no-op |
move(topLayer, PLACEBEFORE) |
| after a crashed script | every later call times out | health check + page reload |
| stealth | none | camoufox (patched Firefox) by default |
Each row on the right is covered by a test that asserts on exported pixels, because every failure in the left column looks like success from the API.
Install
cd ~/photopea-api
python3.12 -m venv .venv
./.venv/bin/pip install -r requirements.txt
./.venv/bin/python -m camoufox fetch # one-off, 663 MB
Verify both engines really drive Photopea on your machine:
./.venv/bin/python scripts/gate_check.py
# [PASS] camoufox boot 4635ms png 126B
# [PASS] chromium boot 1813ms png 126B
Use as an MCP server
claude mcp add -s user photopea -- ~/photopea-api/.venv/bin/python -m mcp_server.server
The editor boots lazily on the first tool call and is reused for the rest of the
session. Environment overrides: PHOTOPEA_ENGINE (camoufox | chromium |
firefox, default camoufox) and PHOTOPEA_HEADLESS (0 to watch it work,
useful when debugging).
Tools cover documents (create, open, resize, crop, rotate, flip, flatten,
export), layers (add, select, rename, opacity, blend mode, visibility, move,
scale, rotate, flip, reorder, group, delete, tree), drawing (gradients,
rectangles, ellipses, image placement), text (point, paragraph, edit), colour
adjustments, filters, selections, and run_script as the escape hatch.
Engine choice
camoufox is the default because it was asked for, and it works. But Photopea
serves no anti-bot whatsoever — the recon found zero protection vendors and
zero XHR endpoints — so stealth buys nothing functional here and costs ~2.8 s of
extra boot. Use chromium when you control the environment:
from photopea_api.browser import BrowserConfig
async with Photopea.session(BrowserConfig(engine="chromium")) as pp:
...
Closing Photopea's gaps
Photopea implements a large slice of Photoshop, but it is missing channels, every layer effect, eight filters, four colour spaces, guides and AVIF — and because its Action Manager is a phantom there is no lower level to drop to.
So the architecture inverts: Photopea is the document engine, Python is the
pixel engine. One primitive makes it work —
:meth:PixelOps.export_layer isolates a layer and exports it with alpha, and
place_image composites arbitrary RGBA back in. Everything missing is rebuilt
on top of that pair.
| gap | how it is closed |
|---|---|
| layer effects (0/1 native) | pp.fx.drop_shadow / outer_glow / inner_glow / stroke / color_overlay / gradient_overlay / bevel — rendered from the layer's alpha |
| 8 missing filters | pp.pyfilter.radial_blur / smart_blur / spherize / zigzag / glass / lens_flare / deinterlace / clouds |
| channels + histograms (0/4) | pp.analyze.histogram / histograms / statistics / channel / palette — computed from exported pixels, exact |
| HSB / CMYK / Lab / Gray | script.hsb_to_hex, cmyk_to_hex, lab_to_hex, gray_to_hex |
| guides | pp.guides — tracked Python-side, render() draws them onto a layer |
selection.store |
pp.selections.save/load — replays the geometry we asked for |
| AVIF export | pp.analyze.export_avif() via Pillow |
Live layer styles, not just raster
pp.fx.* gives raster effects — an effect arrives as an ordinary layer named
title (drop_shadow). For a real, editable layer style use
pp.fx.apply_live(), which writes an lfx2 block into the PSD and hands the
file back:
await pp.fx.apply_live({"drop_shadow": {"color": "#000000", "size": 12}}, layer="title")
This works because Photopea does render PSD layer effects — verified by opening a Photoshop-authored file: drop shadow, both glows, both shadows, bevel/emboss, satin, all three overlays and stroke all render correctly. The style survives PSD export and stays editable in Photoshop.
All ten of Photoshop's layer styles are live: drop_shadow,
inner_shadow, outer_glow, inner_glow, bevel, satin,
gradient_overlay, pattern_overlay, stroke, color_overlay. The raster
path in pp.fx.* remains as an alternative, not a fallback — it keeps undo
history, which the live path discards when it re-opens the document.
pattern_overlay also takes a tile (PNG bytes): its pixels live in a global
Patt block rather than in the effect descriptor, so the tile is written into
the file and referenced by UUID.
Each raster operation costs one export plus one import — tens of milliseconds at typical sizes, not free.
One capability falls out of this for free and is worth knowing about:
pp.analyze.drew_anything(layer) answers "did this layer actually rasterise?"
It is the only reliable guard against Photopea's silent no-ops, and cheap enough
to call after every text layer in a generated pipeline.
What Photopea can and cannot do
docs/API_MAP.md is a capability matrix built by calling every
candidate member against a live editor. It exists because the API cannot be
introspected — the objects are virtual, typeof lies, and an unsupported call
does not raise, it aborts your whole script.
The headline result: Photopea implements a large slice of Photoshop's layer,
filter and selection surface, but its Action Manager is a phantom.
stringIDToTypeID() echoes its own argument back and executeAction() crashes
the interpreter, so the usual "drop to the Action Manager for anything missing"
escape hatch does not exist. Gradients are the visible casualty — hence
photopea_api/raster.py, which renders them in Python and composites them in.
Tests
./.venv/bin/python -m pytest # 70 tests, ~13 min
./.venv/bin/python -m pytest -m "not slow" # skip the per-engine boots
./.venv/bin/python scripts/demo.py # compose a poster end to end
Every visual assertion is made on exported pixels, never on layer state — because the failure mode that matters (text that renders nothing) leaves the layer looking perfectly correct.
Layout
photopea_api/
browser.py engine launcher (camoufox | chromium | firefox)
bridge.py Live Messaging transport, sentinel framing
host.html the page that hosts the Photopea iframe
client.py session, file I/O, export, font readiness
script.py JS generation for Photopea's restricted dialect
raster.py gradients rendered in Python
ops/ document, layer, text, draw, adjust, filters, selection
mcp_server/ MCP server over the client
scripts/ recon, gate check, API mapper, targeted probes
docs/API_MAP.md the measured capability matrix
RECON.md how Photopea works and what it forces on us
Etiquette
Photopea is one developer's ad-funded product and its AI features cost him money
per call. This client drives local deterministic editing only, never touches
showWindow("magiccut") or other paid surfaces, and opens one editor per
session. For volume, self-host
(Photopea-Offline) instead.
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.
Neon Database
MCP server for interacting with Neon Management API and databases
E2B
Using MCP to run code via e2b.
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.