revspot-mcp
by rakshith-co
README.md
# 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
## 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.