A unified MCP (Model Context Protocol) server that wraps eight content creation and stock media APIs into 31 tools for avatar videos, text-to-speech, AI video/image/music generation, and stock photo/video search. Deployed as a Cloudflare Worker.
- Node.js 20+ and npm
- A Cloudflare account (free tier is fine)
- API keys for the services you intend to use (see the Environment variables table below)
heygen_upload_photo, heygen_upload_audio, heygen_create_photo_avatar, heygen_get_photo_avatar_status, heygen_list_avatars, heygen_generate_avatar_video, heygen_check_video_status
elevenlabs_list_voices, elevenlabs_get_voice, elevenlabs_text_to_speech
runway_generate_video, runway_extend_video, runway_check_status
nano_banana_generate_image, nano_banana_check_status
kling_generate_video, kling_check_status
hailuo_image_to_video, hailuo_check_status
suno_generate_music, suno_generate_sounds, suno_extend_music, suno_convert_to_wav, suno_check_music_status, suno_check_wav_status
pexels_search_photos, pexels_get_photo, pexels_curated_photos, pexels_search_videos, pexels_get_video, pexels_popular_videos
This repo is a template — one codebase, N deployed Workers. Each instance is differentiated at deploy time via:
--nameflag onwrangler deploy(unique Worker name per instance)- Cloudflare Dashboard Secrets (per-instance API keys and auth token)
- Optional Custom Domain attached to the Worker
wrangler.jsonc stays generic — no routes, account IDs, or instance-specific values baked in.
The Worker is stateless: every /mcp request creates a fresh McpServer + WebStandardStreamableHTTPServerTransport (the Fetch-API-native MCP transport, shipped in @modelcontextprotocol/sdk). No session map, no persistence, no cold-start penalty beyond the isolate boot. Long-running tasks (video/music generation) are submitted to the upstream provider and polled via separate status-check tools, so nothing runs longer than a request.
All required unless marked optional. Configured as Cloudflare Secrets per Worker.
| Key | Purpose |
|---|---|
HEYGEN_API_KEY |
HeyGen API key — https://app.heygen.com/settings?nav=API |
ELEVENLABS_API_KEY |
ElevenLabs API key — https://elevenlabs.io/app/settings/api-keys |
KIEAI_API_KEY |
Kie.ai API key (powers Runway, Nano Banana, Kling, Hailuo, Suno) — https://kie.ai |
PEXELS_API_KEY |
Pexels API key — https://www.pexels.com/api/ |
MCP_AUTH_TOKEN |
Optional. If set, requires Authorization: Bearer <token> on all /mcp requests. |
npm install
cp .dev.vars.example .dev.vars # then fill in real keys
npx tsc --noEmit # typecheck
npx wrangler dev # starts on http://localhost:8787Smoke tests:
# Health
curl http://localhost:8787/health
# tools/list (expects Bearer auth only if MCP_AUTH_TOKEN set in .dev.vars)
curl -X POST http://localhost:8787/mcp \
-H "Authorization: Bearer $MCP_AUTH_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
# End-to-end tool call
curl -X POST http://localhost:8787/mcp \
-H "Authorization: Bearer $MCP_AUTH_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"pexels_curated_photos","arguments":{"per_page":2}}}'Same runbook for the first instance and every additional one. Don't decommission the previous host (Railway or otherwise) until cutover is verified.
- Provision API keys for the services the instance needs.
- Cloudflare Dashboard → Workers & Pages → Create → Connect to Git → select this repo.
- Deploy command:
npx wrangler deploy --name <unique-worker-name>Example:mcp-social-content-alex-cf,mcp-social-content-brand-b-cf, etc. Must be stable and unique per instance. - Build command: blank. API token: one with
Workers Scripts: Edit+Workers Routes: Edit. - The first build will likely fail (no secrets yet) — expected.
- Settings → Variables and Secrets → add each secret (table above) as type Secret. Saving triggers auto-redeploy.
- (Optional) Settings → Domains & Routes → Add Custom Domain (e.g.
social-content.mcp.<apex>). Cloudflare issues the cert automatically. - Verify:
Then
curl https://<custom-domain-or-workers.dev-url>/health
tools/list+ one auth-gated tool call (see local smoke tests above — swap the URL). - Point your MCP client (Claude Desktop, Claude Code, etc.) at the new URL and exercise every tool before decommissioning any old host. See Connect an MCP client below.
This server speaks the Streamable HTTP MCP transport. Any client that supports remote MCP over HTTP (Claude Desktop, Claude Code, Cursor, etc.) can connect.
Edit the MCP config (Settings → Developer → Edit Config, or ~/Library/Application Support/Claude/claude_desktop_config.json on macOS) and add:
{
"mcpServers": {
"social-content": {
"type": "http",
"url": "https://<your-custom-domain-or-workers.dev>/mcp",
"headers": {
"Authorization": "Bearer <your-MCP_AUTH_TOKEN>"
}
}
}
}Omit the headers block if you did not set MCP_AUTH_TOKEN. Restart the client; the 31 tools should appear.
Settings → Connectors → Add custom connector. Use the same /mcp URL. If you set MCP_AUTH_TOKEN, add an Authorization: Bearer <token> header.
One repo, N Workers. Repeat Deploy an instance with a different --name and a different set of secret values. wrangler.jsonc is not edited.
Without an explicit --name on wrangler deploy, every CF Project connected to this repo would overwrite the same Worker (the one defined in wrangler.jsonc). Each instance MUST pass its own unique name so deploys target distinct Workers.
- Do not use
@modelcontextprotocol/expressor@modelcontextprotocol/node— they pull Node APIs Workers can't run. @cfworker/json-schemaMUST stay an explicit top-level dependency even though the SDK imports it internally — Wrangler bundling fails otherwise.- Pin
wranglerto^4.0.0. Wrangler v3 is EOL. - The
<name>.<account>.workers.devURL may 404 depending on account settings — the custom domain is canonical. - Fresh
McpServer+ fresh transport per request insidefetch. Don't hoist — statelessness depends on it. - Never bake instance-specific values into
wrangler.jsonc— no routes, no account IDs, no bucket names, no public URLs. All differentiation is Secrets +--name. This is what makes the repo forkable.