Stateless MCP server that wraps OpenAI's Sora, Whisper, GPT-4o Audio, and TTS APIs for generating videos, images, and processing audio.
SaferSkills independently audited sanzaru (MCP Server) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
<div align="center"> <img src="https://raw.githubusercontent.com/TJC-LP/sanzaru/main/assets/logo.png" alt="sanzaru logo" width="400">
A stateless, lightweight MCP server and agent CLI that wraps Higgsfield video generation (Seedance 2.5, Kling 3.0) and OpenAI's image, transcription, audio and TTS APIs, plus ElevenLabs for podcasts.
0.14.0: the base install is the agent CLI only. Anything that launches the MCP server (.mcp.json, Claude Desktop,sanzaru serve) must installsanzaru[mcp]orsanzaru[all].HIGGSFIELD_BASE_URLcan point video at a credential proxy.
>
0.13.0: OpenAI removed the Sora Videos API and every Sora model on 2026-09-24. sanzaru now generates video through the Higgsfield API — setHF_KEY. Oldvideo_…ids and thelist_videos/delete_video/remix_videotools are gone.
(std / pro / 4K / turbo), or any Higgsfield catalog model by id
hf_… idmax_cost_usdrefuses anything over budget (nothing uploaded or charged), dry_run prices for free
wait_for(download=true) call waits server-side and saves the file into your media directoryeleven_v4 (the ElevenLabs default) in dialogue render mode with --verify. OpenAI is the zero-setup fallbackdialogue render mode: consecutive turns go out together so the model paces them--verify transcribes the rendered audio and re-renders segments the TTS silently droppedgpt-realtime agents actually talk to each otherdocs/audio/simulated-podcasts.mdsanzaru video create, sanzaru image generate, ...create ... -o out.mp4 submits, polls, downloads in one commandwait) and arbitrary -o output pathsdocs/cli.md — bare sanzaru still runs the MCP server (nothing breaks)Note: Content guardrails are enforced by OpenAI. This server does not run local moderation.
OPENAI_API_KEY for images, audio and podcastsHF_KEY for video — a Higgsfield API key (key_id:key_secret) from the API console. The API isprepaid and billed separately from a Higgsfield app/CLI subscription; the video tools register only when it is set
Media storage (choose one):
# Recommended: unified path (auto-creates videos/, images/, audio/ subdirs)
SANZARU_MEDIA_PATH="/path/to/media"
# Or individual paths (legacy, still supported)
VIDEO_PATH="/path/to/videos"
IMAGE_PATH="/path/to/images"
AUDIO_PATH="/path/to/audio"Features are auto-detected based on configured paths. Set only what you need.
git clone https://github.com/TJC-LP/sanzaru.git
cd sanzaru ./setup.shThe script will:
.env configurationuv sync --all-extras --dev claudeThat's it! Claude Code will automatically connect and you can start generating videos, images, and processing audio.
uv tool install sanzaru && export OPENAI_API_KEY=sk-...
# One command: price → submit → wait → download → print the file path
sanzaru video create "a tabby cat stretches on a windowsill" --duration 4 --resolution 480p --max-cost 1 -o ./cat.mp4 | jq -r .result.file.path
# Synchronous image generation (gpt-image-2.5), batch fan-out, JSONL output
sanzaru image generate "app icon" "hero banner" --quality high -o ./art/
sanzaru capabilities # machine-readable: what's enabled hereJSON envelopes on stdout, progress on stderr, exit 4 = still-running-and-resumable. Full reference: docs/cli.md.
Install as a plugin — auto-configures the MCP server and ships three skills: sanzaru-mcp (the tool surface, incl. wait_for), sanzaru-cli (the shell surface), prompt-guidance (how to write video/image prompts):
/plugin marketplace add TJC-LP/sanzaruRequires OPENAI_API_KEY and SANZARU_MEDIA_PATH environment variables to be set (and HF_KEY for video).
# All features
uv add "sanzaru[all]"
# Specific features
uv add "sanzaru[audio]" # With audio support
uv add "sanzaru[elevenlabs]" # ElevenLabs as a second TTS provider
uv add sanzaru # Base: the agent CLI only (video via HF_KEY; no MCP dependencies)
uv add "sanzaru[mcp]" # + the MCP server (bare `sanzaru` / `sanzaru serve`)The base install is the CLI and carries nothing from the MCP stack (mcp, starlette, uvicorn). Anything that launches the server — a .mcp.json, Claude Desktop, codex mcp add — must install the mcp extra; sanzaru[all] includes it. Running bare sanzaru without it exits 3 with the install command.
<details> <summary><strong>Alternative Installation Methods</strong></summary>
git clone https://github.com/TJC-LP/sanzaru.git
cd sanzaru
uv sync --all-extrasAdd to your claude_desktop_config.json:
{
"mcpServers": {
"sanzaru": {
"command": "uvx",
"args": ["sanzaru[all]"],
"env": {
"OPENAI_API_KEY": "your-api-key-here",
"HF_KEY": "your-higgsfield-key-id:secret",
"SANZARU_MEDIA_PATH": "/absolute/path/to/media"
}
}
}
}Or from source:
{
"mcpServers": {
"sanzaru": {
"command": "uv",
"args": ["run", "--directory", "/path/to/sanzaru", "sanzaru"]
}
}
}# Using uvx (from PyPI)
codex mcp add sanzaru \
--env OPENAI_API_KEY="sk-..." \
--env SANZARU_MEDIA_PATH="$HOME/sanzaru-media" \
-- uvx "sanzaru[all]"uv venv
uv sync
# Set required environment variables
export OPENAI_API_KEY=sk-...
export SANZARU_MEDIA_PATH=~/sanzaru-media
# Run server (stdio for MCP clients)
uv run sanzaru
# Or HTTP mode (for remote access)
uv run sanzaru --transport http --port 8000</details>
| Category | Tools | Description |
|---|---|---|
| Video | create_video, edit_video, extend_video, get_video_status, download_video, cancel_video, list_local_videos, inspect_video_frame | Generate, edit and extend video on Higgsfield (priced before submit, optional cost cap); inspect framess with optional reference images |
| Jobs | wait_for | Block server-side on any mix of hf_*/resp_* ids until they finish (progress on every poll, optional download); replaces model-driven polling |
| Image | generate_image, edit_image, create_image, get_image_status, download_image, inspect_image | generate_image is synchronous — one call, finished file (the default). create_image starts a background job for batches and refinement chains; collect with wait_for. edit_image edits; inspect_image shows the model what it rendered |
| Reference | list_reference_images, prepare_reference_image | Manage reference images; crop to a video aspect ratio or exact size |
| Audio | transcribe_audio, chat_with_audio, create_audio, convert_audio, compress_audio, list_audio_files, get_latest_audio, transcribe_with_enhancement | Transcription, analysis, TTS (OpenAI or ElevenLabs), and file management |
| Podcast | generate_podcast | The recommended podcast tool: multi-voice episodes with parallel TTS and stitching (ElevenLabs v4 + dialogue + verify recommended); speakers may mix TTS providers |
| Simulated Podcast (experimental) | simulate_podcast | Unscripted: realtime agents converse from a rundown — parallel acts, checkpointing, cost ceiling, QC |
| Media | view_media | Interactive media player via MCP App protocol |
Full API documentation: See docs/api-reference.md
# Create video from text
video = create_video(
prompt="A serene mountain landscape at sunrise, slow aerial push-in",
duration=8, # Seedance 2.5 (default): 4-30 s
aspect_ratio="16:9",
resolution="720p",
max_cost_usd=5, # refused (and nothing charged) if the estimate is higher
) # returns id "hf_...", status, and the estimated cost
# Wait server-side and save it — one call, no polling loop
wait_for([video.id], download=True)# 1. Generate reference image (gpt-image-2.5-flare, synchronous)
generate_image(
prompt="futuristic pilot in mech cockpit",
size="1536x1024",
filename="pilot.png"
)
# 2. Crop to the frame shape you want (image-to-video framing follows the image)
prepare_reference_image("pilot.png", aspect_ratio="16:9", resize_mode="crop")
# 3. Animate — prompt only the motion; the image already fixes subject and style
video = create_video(
prompt="The pilot looks up and smiles",
reference_image="pilot_1280x720.png",
duration=5, resolution="480p", max_cost_usd=2,
)
wait_for([video.id], download=True)
# 4. Keep going from the finished job — no download/re-upload
extend_video("He reaches for the throttle.", source_video=video.id, duration=5, max_cost_usd=3)# List available audio files
files = list_audio_files(format="mp3")
# Transcribe — long files are auto-windowed, so nothing truncates silently
result = transcribe_audio("interview.mp3")
# Or analyze with GPT-4o
analysis = chat_with_audio(
"meeting.mp3",
user_prompt="Summarize key decisions and action items"
)generate_podcast(script={
"title": "AI Weekly",
"speakers": [
{"id": "host", "name": "Alex", "voice": "nova"},
{"id": "guest", "name": "Sam", "voice": "echo"}
],
"segments": [
{"speaker": "host", "text": "Welcome to AI Weekly!"},
{"speaker": "guest", "text": "Thanks for having me."}
]
})# 1. Plan it. Cheap, and the JSON is yours to edit.
sanzaru podcast rundown "why TTS providers drop sentence tails" \
--acts 3 -m 6 --host "Avery" --host "Rory:cedar:You chased the bug." \
-o rundown.json
# 2. See what it would cost. Records nothing.
sanzaru podcast simulate @rundown.json --dry-run
# 3. Record it. Acts run in parallel and each is checkpointed as it lands.
sanzaru podcast simulate @rundown.json --model gpt-realtime-2.1-mini \
--max-cost 2.00 --stems -o ep1.mp3
# Interrupted? The run id is on stderr from the start.
sanzaru podcast simulate --resume 6f1a9c02| Mode | Command | Use Case |
|---|---|---|
| stdio (default) | uv run sanzaru | Claude Desktop, Claude Code, local MCP clients |
| HTTP | uv run sanzaru --transport http | Remote access, Databricks Apps, web clients |
HTTP mode exposes the full toolset — paid generation, cancel_video, and every stored media file — to whoever can reach the port. Set a token and send it as Authorization: Bearer <token> on both /mcp and /media:
export SANZARU_HTTP_TOKEN="$(openssl rand -hex 32)"
uv run sanzaru --transport http --host 0.0.0.0Binding to anything other than loopback requires the token: sanzaru refuses to start otherwise (exit 3). Set SANZARU_ALLOW_UNAUTHENTICATED_HTTP=1 only when something in front of it already authenticates every request — and name that proxy's hostnames in SANZARU_ALLOWED_HOSTS (comma-separated, host or host:*), which the hatch requires: it keeps the SDK's DNS-rebinding check on, Origin included, so the unauthenticated path is never also the least-protected one. With a token and no allowlist the Host/Origin check is switched off and the token is the control. On a loopback bind the token is optional but still recommended.
Databricks Apps authenticates in front of the app, so run it with SANZARU_ALLOW_UNAUTHENTICATED_HTTP=1 and SANZARU_ALLOWED_HOSTS set to the Host value the platform proxy forwards (include whatever its health probe sends). Include the loopback names too if the probe uses them, e.g. SANZARU_ALLOWED_HOSTS=myapp.example.com,localhost:*.
Upgrading from 0.10.x: sanzaru --transport http --host 0.0.0.0 used to start with no credential. It now refuses unless SANZARU_HTTP_TOKEN or the hatch above is set — deliberate, and worth a line in your deployment notes.
/media/{type}/{name} requires the same Authorization header, and every response is Content-Disposition: attachment, so it is for programmatic clients; a browser media element cannot send the header and should use the viewer's _get_media_data path (the bundled MCP App already does).
Embedding the server in your own ASGI stack? Use sanzaru.server.build_http_app() — the same authenticated app the CLI serves — never the bare mcp.streamable_http_app(), which carries none of the middleware. See CLAUDE.md, Transport Modes.
.env file can (and cannot) configureThe sanzaru command — every subcommand, sanzaru serve included — autoloads a .env for local development. python-dotenv is a runtime dependency, so this works in any install, not only under uv sync. Two deliberate limits apply, because a file found on disk must not be able to redirect credentials, relax transport security, or change what a run is allowed to cost:
search of parent directories, so a .env at your project root is not found when you run from a subdirectory.
storage backend credentials, tuning knobs), matched exactly and case-sensitively. Everything else in the file is ignored with a warning naming the keys. Notably ignored on purpose: DATABRICKS_HOST and any *_BASE_URL/proxy variable (they decide where credentials are sent); every HTTP-security variable — SANZARU_HTTP_TOKEN, SANZARU_ALLOW_UNAUTHENTICATED_HTTP, SANZARU_ALLOWED_HOSTS, SANZARU_ALLOWED_ORIGINS, SANZARU_IDENTITY_HEADER, SANZARU_REQUIRE_USER_CONTEXT (a planted file must not weaken or satisfy transport auth, nor pick whose identity is trusted); SANZARU_RUN_SECRET (the signing key); SANZARU_REALTIME_PRICE_* (the price table is what --max-cost is enforced against — a planted 0,0,0,0,0,0 would make every turn free); and DATABRICKS_VIDEO_DIR/_IMAGE_DIR/_AUDIO_DIR (joined into the volume path unsanitized, so .. in one walks into another tenant's files).
Anything the allowlist skips still works exported in the real environment, or injected explicitly with npx dotenv-cli -- <command> — both are deliberate operator actions rather than a file discovered on disk.
| Backend | Config | Use Case |
|---|---|---|
| Local (default) | SANZARU_MEDIA_PATH=/path/to/media | Development, local deployments |
| Databricks | STORAGE_BACKEND=databricks | Databricks Apps with Unity Catalog Volumes |
The Databricks backend supports per-user storage isolation via the user_context module, enabling multi-tenant deployments where each user's media is stored under their own volume prefix (<local-part>-<hash>, injective over email addresses; the prefix format changed after 0.10.0 — see CLAUDE.md for the migration note). In HTTP mode the identity comes from a proxy-injected header, and trusting one is opt-in: set SANZARU_IDENTITY_HEADER (e.g. x-forwarded-email on Databricks Apps) only when a proxy in front of sanzaru both injects that header and strips any client-supplied copy. When it is unset, no header is trusted and every request resolves to the shared volume root. A request carrying the header twice (an appending proxy forwards the client's copy first) or malformed is refused with 400 rather than binding either copy. Set SANZARU_REQUIRE_USER_CONTEXT=1 on a shared deployment so a request with no identity is refused (403 on /media) instead of silently served out of the shared root.
Multi-tenant deployments should also set SANZARU_RUN_SECRET, which signs simulated-podcast run manifests and act checkpoints so --resume refuses bookkeeping this installation did not write.
See CLAUDE.md for full configuration details.
Fully asynchronous architecture with proven scalability:
aiofiles + anyioSee docs/async-optimizations.md for technical details.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.