{"id":"300687ac-a77e-4531-bb05-c4efebda017d","slug":"shotplan","kind":"mcp","name":"shotplan","summary":"MCP server for content-aware landscape → vertical (9:16) video reframing. Analyse, preview, then render — entirely local.","owner":"lrvaka","repoUrl":"https://github.com/lrvaka/shotplan","license":"Apache-2.0","licenseTier":"permissive","trust":"community","stars":0,"match":null,"fewStars":true,"format":"mcp","createdAt":"2026-07-28T00:48:46.000Z","addedAt":"2026-10-21T04:54:51.929Z","logoUrl":"https://avatars.githubusercontent.com/u/3280206?u=adbcd6646e5d66d5e23874ddb88f6cfcafc48db6&v=4","linkOnly":false,"collection":null,"skillPath":null,"categories":["design-media","cloud-infra","memory-knowledge"],"tags":["agent-tools","claude","ffmpeg","opencv","python","vertical-video","video-processing"],"runtime":"python","local":true,"cloudCapable":false,"cloudCapability":"unknown","cloudReadyOnPlatform":false,"needsApiKey":false,"reviewed":false,"syncedAt":"2026-10-23T02:46:03.173Z","sample":false,"url":"https://conduits.dev/tools/shotplan","language":"Python","transports":["stdio"],"install":{"pypi":"shotplan-mcp"},"requirements":{"env":[],"upstreamMayCharge":false},"tools":null,"readme":"# shotplan\n\n[![CI](https://github.com/lrvaka/shotplan/actions/workflows/ci.yml/badge.svg)](https://github.com/lrvaka/shotplan/actions/workflows/ci.yml)\n[![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org/downloads/)\n[![License: Apache 2.0](https://img.shields.io/badge/license-Apache%202.0-green.svg)](LICENSE)\n\nAn MCP server for turning landscape video into vertical reels — where the model can **look at\nwhat it is about to do** before committing to a four-minute render.\n\n<!-- mcp-name: io.github.lrvaka/shotplan -->\n\nContent-aware reframing runs entirely on your machine: spectral-residual saliency plus\non-screen text detection build a per-frame importance map, and a smoothed virtual camera\nholds the important region so UI text stays legible at 9:16.\n\n## This is deliberately not an ffmpeg wrapper\n\nThere are already a dozen ffmpeg MCP servers. The largest exposes 161 tools and has a couple\nhundred stars, and that tool count *is* the diagnosis: transliterating a CLI into JSON Schema\nproduces something strictly worse than letting the model call `ffmpeg` in a shell. The model\nalready knows ffmpeg. A schema is a lossy re-encoding of knowledge it has, plus a permanent\ncontext cost on every session that loads it.\n\nWhat MCP adds here is a loop the model can actually participate in:\n\n```\nanalyze_shots  →  look at the contact sheet  →  preview_shots  →  adjust  →  render_reel\n   (seconds)         (the model sees frames)      (seconds)                   (minutes, once)\n```\n\n`preview_shots` is the load-bearing tool. It renders the proposed treatment for specific shots\nas small stills in about a second, which turns each iteration from four minutes into three.\nWithout it the loop is technically expressible and practically unusable.\n\n## Install\n\n```bash\nclaude mcp add shotplan -- uvx shotplan-mcp\n```\n\nOr by hand:\n\n```json\n{\n  \"mcpServers\": {\n    \"shotplan\": {\n      \"command\": \"uvx\",\n      \"args\": [\"shotplan-mcp\"],\n      \"env\": { \"SHOTPLAN_WORKSPACE\": \"~/Movies/shotplan\" }\n    }\n  }\n}\n```\n\n`ffmpeg` must be on `PATH`, or install `shotplan-mcp[bundled-ffmpeg]` to get a wheel-bundled\nbuild. Two optional extras add capabilities: `[ocr]` for text-aware framing (also needs the\n`tesseract` binary) and `[captions]` for Whisper burn-in. **Call `doctor` first** — it reports\nexactly what is present, what each missing piece would unlock, and how to install it. Nothing\nfails silently and nothing fails late.\n\n## Tools\n\n| Tool | Cost | What it's for |\n|---|---|---|\n| `doctor` | instant | What's installed, what's missing, what that costs you. |\n| `probe_video` | instant | Dimensions, fps, duration, and a render-time estimate. |\n| `analyze_shots` | seconds | Scene cuts plus a per-shot plan: suggested treatment, raw metrics, **warnings**, and a labelled contact sheet. |\n| `preview_shots` | ~1s | The proposed treatment for chosen shots, as stills. |\n| `read_frame` | instant | One frame, when a contact sheet is too coarse. |\n| `render_reel` | minutes | The full render. Returns a job handle. |\n| `get_job` / `cancel_job` | instant | Poll or kill a render. |\n| `burn_captions` | minutes | Whisper transcription burnt in. Run after reframing. |\n\n`analyze_shots` returns `warnings` per shot, not just numbers, because a JSON Schema cannot\nexpress *\"`extend` will streak on this shot because content reaches the frame edge.\"* That\njudgement is the difference between output that looks intentional and output that looks cheap,\nso it ships as a resource (`shotplan://guide/treatments`) and as per-shot warnings on the\nspecific shot where it applies.\n\n## Long-running work: why there's a job store\n\nThis is the part of building an MCP server that isn't obvious, so here is the reasoning\nexplicitly.\n\n**Progress notifications can't be load-bearing.** `notifications/progress` only fires if the\nclient attached a `progressToken` to the request. If it didn't, progress is a silent no-op. It\nis a liveness mechanism, never a correctness one.\n\n**Client timeouts are the real constraint, and there is more than one.** Claude Code's\nper-server `timeout` is a hard wall-clock ceiling that progress notifications explicitly do\n*not* extend. On HTTP/SSE transports there is also a 60-second timer to the server's first\nresponse byte. And on stdio the binding limit is the *idle* timeout — 30 minutes of silence\naborts the call, while hours of chatter is fine. Other clients are reported to drop results\nfar sooner (Claude Desktop at around 60s, per community bug reports rather than\ndocumentation). Timeout behaviour varies per client, often isn't configurable by your user,\nand your server cannot discover it — so don't design against a number you can't see.\n\n**The Tasks extension isn't ready to be the only path.** It lands formally in the 2026-07-28\nspec, the earlier 2025-11-25 shape is already dead, and client support in the wild is\neffectively nil.\n\nSo the server owns job state itself. `render_reel` returns a handle immediately and accepts\n`wait: {max_seconds: N}` — if the work settles inside N (default 20s, capped at 45s to stay\nunder every client's timeout) the completed result comes back inline, so short clips feel\nsynchronous. Otherwise you poll `get_job`. State persists to\n`$SHOTPLAN_WORKSPACE/.shotplan/jobs/`, so jobs survive a restart and a Tasks adapter can later\nbe a thin shim over the same store rather than a rewrite.\n\nOne honesty note: the renderer does not emit frame-level progress, so `percent` is an\nelapsed-versus-estimate figure and says so in its `message`. It is not measured, and it is not\npresented as though it were.\n\n**The full reasoning, with sources and the exact timeout numbers, is written up in\n[docs/long-running-mcp.md](docs/long-running-mcp.md)** — including the payload-size traps\n(`maxResultSizeChars` is text-only; tool descriptions truncate at 2KB) that the quickstarts\ndon't cover.\n\n## Other design decisions\n\n**Videos are never inlined.** A 30-second 1080×1920 clip is 5–20 MB, which is 7–27 million\nbase64 characters; Claude Code truncates MCP output at 25,000 tokens. Videos come back as file\npaths with structured metadata. The one place inline images are spent is the contact sheet,\ncapped at 9 tiles, 320px each, JPEG q70 — past that the server returns a note telling the\nmodel to use `read_frame` instead, which is both cheaper and a better debugging idiom.\n\n**File access is sandboxed to the client's roots.** The server calls `roots/list` and refuses\npaths outside those directories plus `SHOTPLAN_WORKSPACE`. Most MCP servers will read anything\ntheir user can read; honouring roots costs very little.\n\n**Renders run in a subprocess.** So a cancel can kill the process group, an OpenCV or ffmpeg\ncrash can't take the server down, and ffmpeg's stderr is captured as a real log.\n\n**Sampling is not used.** Support is patchy and it is deprecated in the 2026-07-28 spec — and\nthe architecture is inverted anyway. The server returns metrics and pictures; the client's\nmodel decides. Reasoning belongs where the reasoning lives.\n\n## Environment\n\n| Variable | Purpose |\n|---|---|\n| `SHOTPLAN_WORKSPACE` | Where outputs and job state go. Defaults to the working directory. |\n| `SHOTPLAN_ALLOW` | Extra allowed directories, `os.pathsep`-separated. |\n| `SHOTPLAN_FFMPEG` | Override the ffmpeg binary. |\n\n## Development\n\n```bash\npip install -e '.[dev]'\npytest tests/ -v\n```\n\nThe tests drive the server over stdio as a real client — handshake, tool surface, resource\nreads, sandbox refusal, and a full analyse-then-render round trip. A unit test on a Python\nfunction proves nothing about whether a tool is callable or whether its result survives\nserialisation, which is where MCP servers actually break.\n\n## Limits\n\n- Tested on macOS and Linux. Windows is best-effort.\n- Tuned for 16:9 → 9:16. Other source aspects work but the treatment classifier isn't tuned\n  for them.\n- `anim` and `reveal` treatments render but preview as a plain slice; full keyframe preview is\n  planned.\n- The `extend` classifier is heuristic. Check the contact sheet rather than trusting it.\n\n## License\n\nApache-2.0. The reframing engine lives in [reel-maker](https://github.com/lrvaka/reel-maker).\n"}