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.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessSyncing