@cinerads/mcp-server
v0.1.4
Published
CineRads MCP server — let coding agents build, customize, publish and schedule TikTok slideshows via the CineRads edge-function API.
Downloads
69
Maintainers
Readme
CineRads MCP Server
An MCP server that lets a coding agent (Claude Code, Codex, or any MCP client) build, customize, publish, and schedule CineRads TikTok slideshows and MP4 videos end to end through the CineRads edge-function API — authenticated with a CineRads API key (cra_...).
It is a thin, declarative wrapper over the existing CineRads edge functions (no parallel orchestration layer). Every tool maps 1:1 to a function and uses its exact contract field names, so an agent can drive the same pipeline the app does: hooks → copy → images → slideshow (with full branding) → render → publish → schedule, image-only HTML/Canva decks, local/hosted MP4 ingestion, plus analytics.
1. Prerequisites
- Node.js ≥ 18 (uses the built-in
fetch). - A CineRads API key (
cra_<hex>). Mint one inside the CineRads app — thecreate-api-tokenendpoint is JWT-only by design, so a key cannot mint other keys, and key minting is not exposed as a tool here. Keys carry explicit scopes:slideshow:write— create/update/render slideshows, manage automations, publish (default).slideshow:read— read slideshows, products, TikTok connections, image collections + their images, and the curated use-case library.analytics:read— read per-hook analytics, per-post deck-content + TikTok performance (the learning loop), and per-account insights (default).
- The base URL of the CineRads edge functions. Defaults to prod:
https://nuodqvvgfwptnnlvmqbe.supabase.co/functions/v1.
Whether a given endpoint accepts an API key is enforced server-side (the function must opt in via
requireUserOrApiKey). If a tool returns403 Forbidden, your key is missing the required scope; if401, the key is invalid/expired.
2. Install & build
cd mcp
bun install # or: npm install
bun run build # compiles TypeScript to dist/Verify the wiring with the offline smoke check (no network, no creds needed):
node dist/smoke.js
# → SMOKE OK: 48 tools registered, config validated.3. Configuration
The server reads two environment variables:
| Var | Required | Default | Notes |
|---|---|---|---|
| CINERADS_API_KEY | yes | — | Your cra_... token. Sent as x-api-key. |
| CINERADS_BASE_URL | no | prod functions URL | Override for staging/local. No trailing slash. |
A missing or malformed key fails fast at startup with a clear message.
4. Add to Claude Code
Add the server to your MCP config (~/.claude/mcp.json, a project .mcp.json, or via claude mcp add). Use the absolute path to the built entrypoint:
{
"mcpServers": {
"cinerads": {
"command": "node",
"args": ["/absolute/path/to/AIUGCFARM/mcp/dist/index.js"],
"env": {
"CINERADS_API_KEY": "cra_your_key_here"
}
}
}
}Or from the CLI:
claude mcp add cinerads --env CINERADS_API_KEY=cra_your_key_here -- node /absolute/path/to/AIUGCFARM/mcp/dist/index.jsRestart Claude Code; the cinerads tools appear in the tool list.
5. Add to Codex
Codex reads MCP servers from ~/.codex/config.toml:
[mcp_servers.cinerads]
command = "node"
args = ["/absolute/path/to/AIUGCFARM/mcp/dist/index.js"]
[mcp_servers.cinerads.env]
CINERADS_API_KEY = "cra_your_key_here"(Equivalently, the JSON form under mcpServers works for any MCP-compatible client. npx/bunx invocation works too once published; until then point at dist/index.js.)
6. Tools
| Tool | Wraps | Scope | What it does |
|---|---|---|---|
| generate_hooks | generate-slideshow-hooks | write | Hook lines for a product/prompt. |
| generate_slide_copy | generate-slide-copy | write | Body/CTA copy (tone, length, style, research, verbatim soft_cta). |
| assign_slide_images | assign-slide-images | write | Rank+assign collection images to slides. |
| regenerate_slide_image | regenerate-slide-image | write | AI-regen one slide image (admin-gated server-side). |
| generate_persona_image | generate-slide-persona-image | write | AI persona/ICP photo for the HOOK slide (realistic phone-selfie of the ideal customer); use the returned url as the hook slide's customImageUrl. |
| generate_slide_image | generate-image | write | Custom text-to-image for any slide from your own prompt (optional aspect_ratio, default 9:16); use the returned url as a slide's customImageUrl. |
| create_slideshow | create-slideshow | write | Create a draft with full customization. |
| update_slideshow | update-slideshow | write | Patch name/settings/slides/status/etc. |
| get_slideshow | get-slideshow | read | Fetch one slideshow (image URLs resolved). |
| list_slideshows | list-slideshows | read | Paginated list. |
| trigger_slideshow_render | trigger-slideshow-render | write | Render a draft on the VPS → complete. |
| set_product_cta_images | update-product-cta-images | write | Set a product's reusable CTA images (auto-used on CTA slides). |
| get_product_cta_images | update-product-cta-images | read | Read a product's reusable CTA images. |
| set_brand_references | update-brand | write | Attach reference material (competitor research, swipe copy, voice notes) to a brand; copy-gen studies it for style (never copies it) to write on-brand, human copy. |
| set_brand_profile | update-brand-profile | write | Fill the brand-voice profile (who_youre_for, what_you_offer, voice_dos/donts, content_pillars, notes) the in-app Brand Voice page shows + persists. Target by brand_id or product_id (a brand-less product gets a brand created + linked). |
| get_brand_profile | get-brand-profile | read | Read the brand-voice profile (same data the in-app Brand Voice page shows). Target by brand_id/product_id, or omit both for the first brand. |
| save_preset / list_presets / apply_preset | create-preset / list-presets / update-preset-used | write/read | Save & reuse a full generation config. |
| create_slideshow_template | manage-slideshow-templates (create) | write | Save a reusable brand frame (full settings + optional slide skeleton). |
| list_slideshow_templates / get_slideshow_template | manage-slideshow-templates (list / get) | read | List / fetch your saved templates. |
| publish_slideshow_tiktok | publish-slideshow-tiktok | write | Publish a rendered deck to TikTok. |
| ingest_external_video_url | ingest-external-video | write | Ingest a hosted MP4 URL as a completed generation; returns { generation_id, storage_path, bytes, content_type }. |
| upload_video | upload-video | write | Upload an absolute local .mp4 path via multipart/form-data as a completed generation; returns { generation_id, storage_path, bytes, content_type }. |
| publish_video_tiktok | publish-tiktok | write | Publish a completed video generation to TikTok. Defaults privacy_level to SELF_ONLY. |
| get_publish_status | tiktok-publish-status | read | Poll a publish (by id/slideshow, or all). |
| list/create/update/delete_automation | manage-automations | write | Recurring generated or library posting recipes (full settings). |
| list_automation_queue | manage-automations queue_list | write | List queued pre-built decks for a library automation. |
| queue_slideshows | manage-automations queue_add | write | Append approved slideshow ids to a library automation queue. |
| remove_queued_slideshow | manage-automations queue_remove | write | Remove a queued slideshow by queue id or automation + slideshow id. |
| get_hook_analytics | get-hook-analytics | analytics:read | "Which hook works" — perf by hook. |
| get_post_performance | get-post-performance | analytics:read | The learning loop. Per published post: the FULL deck content you authored (hook + body + CTA text + the image on every slide + slide count) joined to real TikTok metrics + a metric time-series. Read it to learn which hooks/CTAs/visuals/slide-counts perform, then author the next deck accordingly. Supports limit + optional connection_id. |
| get_account_insights | get-account-insights | analytics:read | Pre-aggregated per-account "what's working": each (account, dimension, bucket) with sample size n + avg_vs_median. |
| upload_image | upload-image | write | Upload a local image (logo/diagram/screenshot) from disk; returns a hosted url for customImageUrl or an overlay. |
| create_image_collection | create-image-collection | write | Create a named image collection (a bucket of brand/product images assign_slide_images draws from); returns { collection_id }. |
| upload_collection_image | upload-collection-image | write | Upload a local image from disk into a collection (base64 JSON; stored in R2, deduped by content hash). |
| list_products | list-products | read | List confirmed products; use a row's id as product_id. Optional brand_id filter. |
| list_tiktok_connections | list-tiktok-connections | read | List connected TikTok accounts; use a row's id as connection_id. |
| list_collections | list-image-collections | read | List image collections (+ shared/public) with preview images; use a row's id as collection_id / image_collection_ids. |
| list_collection_images | list-collection-images | read | Paginated images in one collection, each with a hosted url; use an id as selected_image_id or the url as customImageUrl. |
| scrape_product | scrape-product | write | Scrape a website/product URL into draft product rows + an AI brand summary (same as the in-app URL box). Products save UNCONFIRMED — confirm a brand in-app before use. |
| list_use_cases | list-presets | read | The curated/system preset library (seeded use-cases / templates) + your own presets, so new users/agents have proven configs to build from. |
Full customization (text color, font, custom images, extra text, image overlays, logo)
These flow through the settings object and per-slide fields, using the exact CineRads contract names (frontend/src/types/slideshow.ts):
- Text color →
settings.text.color(deck) or per-slideslides[].textColor. - Font →
settings.fontFamily(deck font passthrough) orsettings.text.fontFamily. - Logo / watermark →
settings.logo={ url, enabled, position, sizePct }. - Custom images → per-slide
slides[].customImageUrl(pair withimageId: "custom-upload"). - Extra text overlays → per-slide
slides[].extraText[]=[{ id, text, xPct, yPct, color?, sizePct? }]. - Image overlays (free-positioned logo / framework graphic) → per-slide
slides[].imageOverlays[]=[{ id, url, xPct, yPct, sizePct?, opacity? }](xPct/yPct = the image CENTER; sizePct = width % of slide). - Overlay / caption style / pill →
settings.overlay,settings.captionStyle,settings.showPill.
Presets (save_preset.config.settings) and automations (create_automation.settings) carry the same settings object verbatim, so a brand look is remembered across runs. To stamp a saved frame at deck-build time, pass create_slideshow.template_id — the template's settings (+ slide skeleton) become the deck DEFAULTS and any settings/slides you pass override them (so brand-consistent decks at scale need only the per-deck copy).
7. Production paths agents should use
A. Normal generated slideshow
Use this when CineRads should write the hook/copy and choose images:
scrape_product -> list_products -> generate_hooks -> generate_slide_copy ->
assign_slide_images -> create_slideshow -> trigger_slideshow_render ->
publish_slideshow_tiktok or create_automationtrigger_slideshow_render is synchronous. When it returns status:"complete",
the deck can be reviewed, published, or queued.
B. HTML or Canva baked-image slideshow
Use this when another renderer already produced final 1080x1920 page images, for example an HTML slideshow renderer, Canva full-resolution export, or a local design pipeline.
- Upload each page image with
upload_image. - Create one slideshow with one
imageOnly:trueslide per page. - Set
settings.showPill:falseandsettings.overlay.enabled:falseso the baked image is not modified by CineRads. - Verify the public
/s/<slideshow_id>link. - Queue only after review approval, and only if the images are full-resolution.
Minimal image-only slide:
{
"id": "s1",
"type": "hook",
"order": 1,
"text": "",
"imageId": "custom-upload",
"imageUrl": "https://...",
"customImageUrl": "https://...",
"imageOnly": true
}C. Library scheduling for pre-built decks
Use this when decks already exist and should post on a schedule:
create_automation {
content_source: "library",
post_mode: "MEDIA_UPLOAD",
product_id,
connection_id,
posts_per_day: 1,
post_times: ["09:00"],
timezone: "America/Los_Angeles",
hashtags: ["consulting", "caseinterview"],
force_hashtags: true
}
list_automation_queue { automation_id }
queue_slideshows { automation_id, slideshow_ids: ["..."] }Use MEDIA_UPLOAD for TikTok inbox drafts by default. DIRECT_POST posts
immediately and should only be used when the user explicitly approves it.
ramp_schedule can gradually change cadence:
[
{ "after_days": 0, "posts_per_day": 1, "post_times": ["09:00"] },
{ "after_days": 7, "posts_per_day": 2, "post_times": ["09:00", "18:00"] }
]D. Local or hosted MP4 video
Use this when the final output is an MP4, not a slideshow. Do not use
publish_slideshow_tiktok for videos.
upload_video { path: "/absolute/path/final.mp4", product_id, persona_id }
-> generation_id
publish_video_tiktok { generation_id, connection_id, title, privacy_level: "SELF_ONLY" }
get_publish_status { generation_id }For an already hosted MP4, use ingest_external_video_url instead of
upload_video.
8. Example: build + schedule a fully-branded slideshow
A typical agent flow (each step is one tool call; ids thread forward):
1. generate_hooks { product_id, prompt: "why solo founders churn", count: 5 }
→ pick hooks[0].text (and hooks[0].id for hook_id)
2. generate_slide_copy { hook_text, product_id, carousel_style: "tips_list",
copy_length: "short", research_mode: "always",
soft_cta: "link in bio for the free playbook" }
→ slides[] (+ starterTemplateId)
3. assign_slide_images { slides, product_id, preferred_collection_ids: [...] }
→ merge assignments back onto slides (imageId/imageUrl per slideId)
4. create_slideshow { name, product_id, hook_id, slides,
settings: {
text: { color: "#0B1F3A" },
fontFamily: "Poppins",
logo: { url: "https://.../logo.png",
enabled: true, position: "bottom-right", sizePct: 12 }
} }
→ slideshow.id
5. trigger_slideshow_render { slideshow_id } → status: "complete"
6a. (post now) publish_slideshow_tiktok { slideshow_id, connection_id, title: "..." }
get_publish_status { publish_id }
6b. (schedule it) create_automation {
product_id, connection_id,
posts_per_day: 2, post_times: ["09:00","18:00"], timezone: "America/Los_Angeles",
carousel_style: "tips_list", soft_cta: "link in bio for the free playbook",
hashtags: ["saas","founders"],
settings: { text: { color: "#0B1F3A" }, fontFamily: "Poppins",
logo: { url: "https://.../logo.png", enabled: true,
position: "bottom-right" } }
}
7. later: get_hook_analytics { limit: 50 } → see which hook drove the most viewsThe connection_id comes from the user's connected TikTok account (managed in-app, not via this server). Save the branded config once with save_preset and reuse it with apply_preset on the next run.
9. Copy-paste prompts
Paste these into Claude Code, Codex, or any MCP client after the server is connected.
Setup audit
Audit my CineRads agent setup. Use list_products, list_tiktok_connections,
list_automations, list_slideshows, get_post_performance, and
get_account_insights. Tell me what can be generated, what can be scheduled,
what is already queued, and what blockers remain. Do not create, publish, or
queue anything.URL to rendered draft
Onboard this product from a URL: <url>. Scrape it, pick or create the brand
profile, generate 5 hooks, write a 6-slide tips_list deck, assign images,
create the slideshow, render it, and leave it as a draft for review. Do not
publish until I approve.HTML or Canva page images to CineRads
Build a baked image slideshow from local HTML/PNG output. I have one folder per
deck at <folder>, with 1080x1920 page images in order. Upload each page with
upload_image, create an image-only slideshow with showPill false and overlay
disabled, verify the public /s/ link, and save the slideshow ids. Do not queue
thumbnail or low-res images.Slideshow-lab style batch
Run a slideshow-lab style batch from <research folder>. Extract only reusable
mechanics, write original copy, choose the right lane for each deck, build
full-res drafts, upload imageOnly decks when pages are pre-rendered, verify
every /s link, and return a manifest with slideshow_id, share link, dimensions,
caption, CTA, and QA notes. Do not queue until I approve.Queue approved library decks
Queue these approved pre-built slideshow ids as TikTok inbox drafts: <ids>.
First run list_automations and list_automation_queue for <automation_id>.
Confirm the automation uses content_source library and post_mode MEDIA_UPLOAD.
Then run queue_slideshows and show the new queue count. Never use DIRECT_POST
unless I say so.MP4 to TikTok
Publish a locally rendered MP4 through CineRads. Upload /absolute/path/final.mp4
with upload_video, then publish_video_tiktok using privacy_level SELF_ONLY
unless I explicitly request public direct posting. Poll get_publish_status until
it is not uploading, and report the generation_id, publish_id, status, and any
TikTok error.Performance learning loop
Run the performance learning loop. Pull get_post_performance and
get_hook_analytics for the last 50 posts, identify the best hooks, CTAs,
visuals, and slide counts, then create 3 new draft decks that reuse only the
winning patterns. Render them, return the /s/ links, and do not publish.10. Development
bun run typecheck # tsc --noEmit
bun run build # emit dist/
bun run dev # run from source via tsx (needs CINERADS_API_KEY)
node dist/smoke.js # offline: assert tools register + config validatesTests are type-check + an offline smoke check only — no real endpoints are called (the server has no creds in CI). To exercise a live endpoint, set a real CINERADS_API_KEY and run a tool from your MCP client.
