Mockuuups Studio ship their own MCP server. It exposes one tool, generate_mockup, which covers the case where you already know the mockup ID and already host the image somewhere public. Neither was true for me, and getting to those two prerequisites turned out to be the actual work.
So I wrapped their REST API instead. The published reference sits behind a login wall, which meant an afternoon of probing the live API to find out how it really behaves. Four things turned up that aren’t obvious from the outside, and each one changed the design.
The catalogue has no search
GET /v1/mockups accepts q, search, type, family and tag. It ignores all of them. Every request returns the same unfiltered first page with HTTP 200 and no warning, so there is no signal that it is being ignored. I noticed when the results looked wrong, not when I made the mistake.
What does work is limit. And limit=6000 returns all 5,314 mockups in a single 3.3MB response.
That turns the whole problem inside out. Instead of paginating a search API, the server fetches the catalogue once, caches it, and searches locally:
# 5,314 rows, one linear scan, measured well under 20ms,
# which is nothing next to a 17-second render.
rows = await catalog.load()
matches = catalog.search(rows, query="tablet on a desk", limit=12)Local search also buys something the API could never offer: an alias table. My callers say “tablet”; the catalogue says “iPad”. Nothing in it contains the word “poster” at all; that family is called “Paper”. Eight lines of aliases bridge the gap.
Omitting size means hi-res, and hi-res means failure
Leave size out and the API renders at full resolution. On any plan without the hi-res feature, that fails the entire render with feature-not-available.
So the default is the expensive option, and the expensive option is the one most accounts cannot use. The server never lets that field go unset:
json={
"mockup": mockup_id,
# Never omit: a missing size is treated as hi-res, which hard-fails
# on any plan without that feature.
"size": size,
...
}There is no upload endpoint
contents[].url is mandatory. No base64 field, no data URI, no multipart. To render an image, that image must be fetchable from the public internet.
Which means the obvious use case (“put this design, the one on my laptop, into a mockup”) requires you to first solve image hosting. That is a bucket, a CDN, a lifecycle policy, and a bill, all to show a PNG to a renderer for four seconds.
The server does it differently. It holds the bytes in memory under a 256-bit token and serves them from one route on itself, then lets them expire:
create_mockups(mockup_ids=[...], image_base64="<your png>")
-> staged at https://.../i/<token>.png (15 min TTL, in memory)
-> Mockuuups fetches it, renders, done
-> token expires, bytes are goneNo bucket. The one honest caveat is that the route cannot be authenticated, because the client fetching it is Mockuuups’ renderer and not you. Standing in for auth: an unguessable token, a short TTL, a size cap, and magic-byte sniffing so only real images are ever served back out. I would not put anything sensitive through a mockup service anyway.
Synchronous renders are slower than your timeout
A screenshot render measured 17.3 seconds. Four devices in sequence is roughly 69 seconds, and the Cloudflare MCP portal these servers sit behind severs anything past about 60. So doing it synchronously works perfectly for one mockup and breaks the moment you ask for a set, which is exactly when it matters.
mode: "async" returns in 0.6 seconds, with the CDN URLs already allocated before the render has started. So every render is dispatched concurrently, then polled together inside a bounded budget:
create_mockups(
mockup_ids=["Zkn1GMTfiAFX5ZOn", "Zkn2BcTfiAFX5ZPB",
"Zkn2DsTfiAFX5ZPD", "Zkn1dMTfiAFX5ZOz"],
screenshot_url="https://wtdib.cdit-works.de/",
)Four devices cost about as much waiting as one. Anything still running comes back as a handle to poll, and the delivery links are already valid.
The tag trick
The nicest find isn’t an API behaviour at all. Mockups shot in the same session share a tag, so a consistent set across devices is two calls: search one mockup you like, then filter by its tag to get the rest of that shoot. Same room, same light, different hardware.
That is how the demo set was made: one Berlin city guide, four screens, one photoshoot.
Four tools
search_mockups, create_mockups, get_renders, account_status. That last one exists because credits are scarce and plan-gated: one render costs a credit, a website screenshot costs two, hi-res costs another. I check it before a batch now, rather than after.
The whole thing is about a thousand lines, and most of the time went into the four findings above rather than into the code.
If this poked at something you're building: talk to me.
BUILT.