Skip to main content
Glama

revspot-mcp

Revspot's image-generation MCP server. Exposes a DTC-ads tool surface (generate_image, show_marketing_studio, …) over MCP, so any MCP client — Claude included — can generate ad creatives against your product library.

Run it

npm install
cp .env.example .env.local     # OPENAI_API_KEY at minimum
npm run dev

POST /api/mcp — JSON-RPC, MCP over HTTP. Thirteen tools:

generate_image            generate_image_batch      jobs_wait
job_status                show_marketing_studio     show_generations
show_generation_by_ids    show_marketing_studio_generations
edit_image                media_upload              media_confirm
models_explore            balance

Related MCP server: ImageMcp

Layout

app/api/mcp/route.ts        the MCP endpoint — JSON-RPC, tool dispatch,
                            widget resources
app/api/media/[id]          PUT target for media_upload; the id is the
                            capability, write-once
app/assets, app/outputs     serve product-pack images and generated files
lib/core/dtc-ads/           the surface: prompt assembly, ad-format
                            catalogue, tool schemas, handlers, widgets
lib/core/image-generation/  provider + the canvas table
lib/core/                   product-pack store, run store, storage

The prompt layer — lib/core/dtc-ads/

This is the part that decides what a creative looks like. Four files carry it:

assets/system-base.txt        system prompt, no brand kit
assets/system-brandkit.txt    system prompt when a brand kit is supplied
assets/formats-base.json      ad-format definitions (layout per template)
assets/formats-brandkit.json  the same, brand-kit variant
assets/catalogue.json         the 42 ad formats: id, name, ordering

assembly.ts composes these into the enhanced_prompt payload the image model receives. Key ordering, whitespace and which blocks appear are all significant — the payload is consumed as a whole, so a change anywhere shifts output.

Two things worth knowing before editing:

  • style_id has no default and is the dominant creative driver. Its parameter description in tools.ts is load-bearing: wording there changes which template agents pick. Treat tool descriptions as behaviour, not docs.

  • The brand-kit path switches system prompt. Supplying a kit selects system-brandkit.txt and folds the kit into the payload. Passing a bare brand_kit_id with nothing to resolve is refused rather than silently degraded — otherwise the model is told to hold a brand it was never shown.

Canvas table — read before touching aspect ratios

lib/core/image-generation/openai.ts

The provider enforces two rules, quoted from its own rejections: both sides divisible by 16, and within a per-resolution pixel budget. The sizes in OBSERVED satisfy both, and no formula reproduces them — 3:4 at 2k is 1744×2336, not 2× the 1k 880×1168. The relationship is bespoke per ratio.

So there are two outcomes: a canvas backed by a measurement, or auto. Do not compute one — an earlier computed table produced sizes the provider rejects and broke 17 of 45 aspect/resolution combinations.

COMPUTED holds 4:5 only, the documented exception: it is the Meta feed standard, and each size is the largest exact-0.8 canvas satisfying both rules. Marked observed: false so a run record says the number rests on arithmetic. Before it existed, asking for 4:5 silently returned a 1254×1254 square.

Widgets

Three MCP-Apps panels — generation gallery, ad-format picker, media upload — served as ui://revspot/<name>.<version>.html from lib/core/dtc-ads/widgets.ts.

Hosts cache widget HTML against its URI, so markup changes are invisible to any client that already opened a panel. widgets:version enforces that: change the markup without bumping WIDGET_VERSION and it fails with both hashes. Bump the constant, commit the regenerated lockfile, and clients refetch.

Before you deploy

npm run predeploy

widgets:version   resource versions consistent
widgets:parse     every widget script parses
widgets:paint     widgets render against a strict host
tsc --noEmit      types

widgets:paint needs REVSPOT_BACKEND_URL pointing at a running instance.

Storage

REVSPOT_DATA_DIR is the root — a mounted volume in production, any writable path locally.

.prizm-data/store.json           generation runs
data/prizm-product-packs.json    product packs
assets/product-packs/<id>/       pack images
outputs/                         generated images

Those two prizm-prefixed names, and the legacy /prizm-assets and /prizm-outputs URL prefixes accepted in servedUrlToFilePath, are held over from an earlier name. They are not dead: records already on disk reference them, so renaming orphans every pack and run written before the change. Rename only alongside a migration. New writes already use assets/ and outputs/.

Known gaps

  1. Tools not implemented that MCP clients may reach for: media_import_url, media_upload_widget, show_medias, and the transform set — upscale_image, reframe, outpaint_image, remove_background. Without the last group, "make it bigger" or "make it vertical" becomes a fresh generation with different copy rather than a transform of the existing image.

  2. No brand-kit store. An inline brand_kit object works; a bare id is refused.

  3. No cost preflight, so spend cannot be quoted before running.

  4. Run history has no created_at and no date filter, so "what did I make today" cannot be scoped server-side.

  5. Jobs occasionally accept and sit in queued without starting. Re-submitting clears it and a 15-minute watchdog reaps them; root cause not diagnosed.

  6. Concurrency cap is 8 — larger batches need a queue that waits for slots.

Things that will bite you

  • Product packs do not transfer between instances. Rebuild them with the same images. A pack with the right name and the wrong images renders a different product and looks like a model failure. Verify image by image, not by title or count.

  • Never reconstruct a record from a read that may have missed. A {...(await get(id))!} spread once persisted a row with no id and no createdAt; the listing sorted on createdAt.localeCompare and threw. Because the concurrency check calls that listing at the top of every generate, one bad row took generation down completely, and restarting could not clear it.

Related MCP Connectors

Related MCP Servers