Instagram Carousel MCP
by vaheedsk36
README.md
# Instagram Carousel MCP
An MCP server that **designs multi-slide Instagram carousels** as crisp SVG,
shows them in a **live swipeable preview**, and exports them to **PNG** ready to
upload. Pure-Python, no system image libraries required.
## What it does
- Describe a carousel as a list of slide specs → it renders each slide to SVG.
- Six slide templates: `title`, `content`, `list`, `quote`, `stat`, `cta`.
- Six themes: `midnight`, `sunset`, `mono`, `forest`, `slate`, `bubblegum`.
- Three sizes: `portrait` (1080×1350, recommended), `square` (1080×1080),
`story` (1080×1920).
- A live preview page (swipe / arrow keys / dots) that also rasterises each
slide to PNG **in the browser** — no extra dependencies for export.
## Architecture
Claude (the MCP client) calls the tools; the server renders SVG, sources media
from a provider chain, and (for reels) rasterises + composites with ffmpeg.
```mermaid
flowchart TB
Client["Claude Code / Desktop<br/>(MCP client)"]
subgraph Server["server.py — FastMCP (stdio)"]
Tools["Tools:<br/>trending_topics · create_carousel · create_reel<br/>save_brand · list_brands · update_slide · add_slide<br/>list_themes · get_preview_url · export_png"]
end
subgraph Core["carousel/ package"]
render["render.py<br/>SVG: templates, wrap, highlight,<br/>drop-shadow, logo/image embed, layers"]
themes["themes.py<br/>themes + custom brand theme"]
brand["brand.py<br/>brand profiles + caption (≤5 tags)"]
news["news.py<br/>Google News RSS"]
images["images.py<br/>image + logo sourcing"]
videos["videos.py<br/>Seedance video clips"]
reel["reel.py<br/>rasterize + ffmpeg<br/>scrim · bug · stitch"]
preview["preview.py<br/>HTTP preview server + viewer"]
end
subgraph Ext["External (keyless unless noted)"]
replicate["Replicate<br/>Flux img + Seedance video<br/>(token)"]
pexels["Pexels (key)"]
openverse["Openverse / Picsum"]
wiki["Wikipedia / Wikimedia"]
gnews["Google News RSS"]
end
subgraph Sys["System binaries"]
ffmpeg["ffmpeg"]
rsvg["rsvg-convert"]
end
Out["output/<id>/<br/>slides.svg · PNGs · reel.mp4<br/>caption.txt · music.txt · index.html"]
Browser["Browser / Preview panel"]
Client <-->|MCP| Tools
Tools --> render & brand & news & reel
render --> themes & images
reel --> render & videos & rsvg & ffmpeg
news --> gnews
images --> replicate & pexels & openverse & wiki
videos --> replicate
Tools --> Out
Tools --> preview --> Browser
Out --> preview
```
**Image-source policy** (per scene/slide): real entities → `portrait`/logo
(Wikipedia/Commons); news/real-world → `background_photo` (Pexels→Openverse→
Picsum); concepts → `background_query` (Flux); motion → `video_query` (Seedance).
<details><summary>Request flow — <code>create_reel</code></summary>
```mermaid
sequenceDiagram
participant C as Claude
participant S as create_reel
participant R as render.py
participant M as images/videos
participant F as ffmpeg + rsvg
C->>S: scenes + brand + caption + music
loop each scene
S->>R: render fg text layer (SVG)
S->>M: bg → video_query (Seedance) / photo / Flux / still
S->>F: rasterize fg; composite bg+scrim+text+bug
end
S->>F: crossfade-stitch scenes → reel.mp4
S->>C: preview_url + caption + music
```
</details>
## Tools
| Tool | Purpose |
|------|---------|
| `list_themes` | List available themes with colours. |
| `trending_topics` | Find recent/trending news headlines for a subject (Google News, no key) to pick a timely angle. |
| `create_carousel` | Build a carousel from slide specs; writes SVGs + preview. Accepts `brand`, `caption`, `hashtags`. Returns `preview_url`. |
| `create_reel` | Compile vertical 9:16 scenes (same specs as slides) into an MP4 Reel — Ken-Burns zoom + crossfades, no audio. Needs ffmpeg + rsvg-convert. |
| `update_slide` | Replace one slide (by index) and re-render. |
| `add_slide` | Insert/append a slide. |
| `save_brand` | Create/update a brand profile (handle, logo, custom theme, default hashtags). |
| `list_brands` | List saved brand profiles. |
| `get_preview_url` | Get the live preview URL for an existing carousel. |
| `export_png` | Server-side PNG export (optional; needs Playwright). |
### Slide fields
```
title eyebrow?, heading, subheading?, handle?
content eyebrow?, heading, body
list eyebrow?, heading, items[] (strings), ordered? (bool)
quote quote, author?, role?
stat value, label?, caption?
cta eyebrow?, heading, body?, button?, handle?
```
Any slide also accepts `handle` (e.g. `@brand`) and `page` (bool — show `n/total`).
### Images
Slides aren't text-only — you can add photos:
- **`background_image`** (any slide): a full-bleed photo behind the content. Text
automatically switches to light and a gradient scrim is added so it stays
readable on top of the image.
- **`image`** (on the `content` template): an inline rounded image card shown
between the heading and the body text.
You don't have to supply files at all — use **`background_query`** / **`image_query`**
with a text description and the server auto-sources a fitting image, trying in
order: **Replicate/Flux** (AI, needs `REPLICATE_API_TOKEN`) → **Pexels** (stock,
needs `PEXELS_API_KEY`) → **Openverse** (free CC photos, no key) → **Picsum**
(random, guaranteed). Results cache under `assets/cache/`. Override the order
with `IMAGE_PROVIDER_ORDER`. So image-sourcing works with zero setup (real
photos) and upgrades to custom AI art once a Replicate token is present.
```jsonc
{ "template": "title", "heading": "The State of Remote Work",
"background_query": "minimal home office, soft morning light" }
{ "template": "content", "heading": "Hybrid is winning",
"image_query": "team collaborating in a bright office",
"body": "58% of teams now run hybrid." }
```
Or supply images explicitly: `background_image` / `image` values can be a
**local file path**, an **http(s) URL** (downloaded), or a **data URI** — all
base64-embedded so exported PNGs are self-contained. Drop files in `assets/`:
```jsonc
{ "template": "title", "heading": "2026 Report",
"background_image": "assets/cover.jpg" }
{ "template": "content", "heading": "Hybrid is winning",
"image": "https://example.com/chart.png",
"body": "58% of teams now run hybrid — up from 41% last year." }
```
## Reels (vertical video)
`create_reel` turns a list of **scenes** (same spec as carousel slides) into a
1080×1920 MP4: each scene gets a subtle Ken-Burns zoom and scenes crossfade into
each other. No baked-in audio — add trending audio in the Instagram app. Brand,
caption, hashtags, and the image options (`background_query`, `portrait`, etc.)
all work the same as carousels.
```
create_reel(scenes=[...], brand="mypage", per_scene=3.2, transition=0.6)
# -> output/<id>/reel.mp4
```
Requires `ffmpeg` and `rsvg-convert` (`brew install ffmpeg librsvg`). 3–7 scenes
works best.
**Real-video backgrounds.** A scene can have a moving video background instead of
a Ken-Burns still:
- `video_query: "prompt"` — generates a real AI clip (Seedance on Replicate;
needs `.replicate_token`, ~$0.10–0.20/clip, cached).
- `video: "path-or-url"` — use a clip you already have.
Every scene gets a dark **scrim plate + text drop-shadow** for legibility over
busy footage, and the brand bug sits in Instagram's safe zone. Backgrounds can
be mixed per scene (some video, some generated stills, some real photos).
## Brand your page (theme + logo)
Save a brand profile once, then pass `brand: "<name>"` to `create_carousel` and
every slide gets your colours, logo, @handle, and default hashtags.
```jsonc
// save_brand({ profile: { ... } }) -> brands/mypage.json
{
"name": "mypage",
"handle": "@mypage",
"logo": "mypage-logo.png", // path; absolute, or relative to brands/
"base_theme": "midnight", // theme to extend
"theme": { // override only what you want
"accent": "#f472b6",
"bg": ["#1e1b4b", "#312e81"] // "#hex" solid, or ["#a","#b"] gradient
},
"default_hashtags": ["#buildinpublic", "#startup"],
"caption_signature": "Follow @mypage 🚀",
"default_size": "portrait"
}
```
- **Logo:** any PNG/JPG/SVG. It's base64-embedded into each slide (top-left), so
exported PNGs are fully self-contained. Drop your logo in `brands/` (or give an
absolute path) and set `logo` to it.
- **Theme:** start from a built-in `base_theme` and override any of `accent`,
`bg`, `bg_angle`, `text`, `muted`, `accent_fg`, `font_sans`, `font_serif`.
- Profiles are JSON in `brands/`, so they commit to the repo and sync across
devices.
## Captions & hashtags
`create_carousel` (and a follow-up) take `caption` and `hashtags`. The final
post text is assembled as **caption → brand signature → merged hashtags**
(per-call + brand defaults, deduped), written to `caption.txt`, and shown in the
live preview with a **Copy** button — paste straight into Instagram.
```
create_carousel(
slides=[...],
brand="mypage",
caption="Speed compounds. Here are 5 moves that cut our cycle time in half.",
hashtags=["#shipfast", "#engineering"],
)
```
## Viewing the output interactively
`create_carousel` returns a `preview_url` served by the MCP server itself
(e.g. `http://127.0.0.1:<port>/<carousel-id>/`). Open it in any browser for the
full interactive carousel + **Download all (ZIP)** button.
**In the Claude Code desktop app**, the Preview tool reads
`~/.claude/launch.json`. The included `carousel-preview` config serves a
carousel directory over `http://127.0.0.1:8745/`. To preview a specific
carousel, point its `--directory` arg at `output/<carousel-id>` and start the
preview. (Claude can do this for you on request.)
## Exporting PNGs
- **Easiest:** click **Download all (ZIP)** in the live preview — rasterises all
slides in the browser and bundles them into one ZIP (a single download, so the
browser's multi-download block never trips). Zero setup. "Download this slide
(PNG)" grabs just the current one.
- **Headless / programmatic:** `export_png`. One-time setup:
```
./.venv/bin/python -m pip install playwright
./.venv/bin/python -m playwright install chromium
```
## Run it on another device
Prerequisites: **Python 3.10+** and the **`claude`** CLI on PATH.
```bash
git clone https://github.com/vaheedsk36/instagram-carousel-mcp.git
cd instagram-carousel-mcp
./setup.sh # creates .venv, installs deps, registers the MCP server
```
`setup.sh` registers the server at user scope so it's available in every Claude
Code session on that machine. Verify with `claude mcp list | grep carousel`.
Your brand profiles travel with the repo (they live in `brands/`), so the same
look is available everywhere.
Manual equivalent if you'd rather not run the script:
```bash
python3 -m venv .venv
./.venv/bin/python -m pip install -r requirements.txt
claude mcp add instagram-carousel --scope user -- \
"$(pwd)/.venv/bin/python" "$(pwd)/server.py"
```
> Note: the `.venv` is machine-specific and git-ignored — always recreate it per
> device. Only the source and `brands/` profiles are committed.
## Project layout
```
server.py MCP server (FastMCP) — the tools
carousel/themes.py theme palettes + custom-theme builder
carousel/brand.py brand profiles (handle, logo, theme, hashtags) + caption assembly
carousel/render.py slide spec -> SVG (text wrapping, templates, logo embedding)
carousel/preview.py background HTTP preview server + viewer HTML (caption + copy)
carousel/export.py optional Playwright SVG->PNG
brands/ saved brand profiles (committed; sync across devices)
output/<id>/ generated slides, manifest, preview, caption.txt, spec
setup.sh one-shot setup for a fresh machine
requirements.txt Python dependencies
test_render.py smoke test covering all templates
test_brand.py smoke test for brand + logo + caption
```
## Note on this machine
The Homebrew Python 3.14 bottle shipped with a mis-linked `pyexpat` (pointed at
the system `libexpat` which lacks a newer symbol). It was repaired by repointing
the extension at Homebrew's expat:
```
install_name_tool -change /usr/lib/libexpat.1.dylib \
/opt/homebrew/opt/expat/lib/libexpat.1.dylib <pyexpat.so>
codesign --force -s - <pyexpat.so>
```
A future `brew upgrade python@3.14` may revert this; re-run if `import
xml.parsers.expat` fails again.
This server cannot be deployed
Maintenance
ActivityInactive
ResponsivenessNo issues