Skip to main content
Glama
README.md
# kdenlive-editing-mcp

Scripted video editing with **Kdenlive 26.08.1 / MLT 7.41.0**, driven from
Python, with an MCP server on top. Built because a `.kdenlive` file is an
undocumented internal serialisation and the toolchain around it fails silently —
wrong frame rate, missing audio, clips in the wrong place — rather than with an
error.

Pointed at a folder of footage, it writes a project you can open and start
cutting, then proves the result is right by rendering frames back out of the
finished timeline and comparing them against the source.

> Verified against Kdenlive **26.08.1**, MLT **7.41.0**, document version
> **1.1**. The project format is not a public API and changes between releases;
> treat that as a version pin.

## Layout

| path | what |
|---|---|
| `bin/melt-run` | runs melt on a project with the silent traps guarded, and real exit codes |
| `bin/kdenlive-mcp` | starts the MCP server on stdio |
| `lib/kdenlivelib.py` | probe media, transcode to CFR, build the project XML |
| `lib/verify.py` | render frames back out of a timeline and compare them to their sources |
| `lib/mcp_server.py` | the MCP tools |
| `scripts/stringout.py` | worked example: N seconds of every clip in a folder, on one timeline |
| `CLAUDE.md` | the verified format notes and the traps, with measurements |

## Usage

```sh
scripts/stringout.py raw-videos/ -s 1.5 -o assembly.kdenlive
scripts/stringout.py raw-videos/ --transcode proxies/ --verify
```

A stringout is every take in order, trimmed to a glance — a starting point to
cut against rather than a finished edit. Clips land on V1 with their audio on
A1, grouped so they move together; the bin keeps every clip at full length, so
any slice can be pulled longer by hand afterwards.

From Python:

```python
from kdenlivelib import Profile, Project
from verify import verify_project

p = Project(Profile.hd1080p60())
for clip in clips:
    p.append(clip, seconds=1.5)
p.save("assembly.kdenlive")

for r in verify_project("assembly.kdenlive"):
    print(r.name, r.verdict)          # match / MISMATCH
```

## MCP server

`bin/kdenlive-mcp` speaks stdio. Point a client at it:

```json
{"mcpServers": {"kdenlive": {"command": "/path/to/kdenlive-editing-mcp/bin/kdenlive-mcp"}}}
```

| tool | what |
|---|---|
| `probe_media` | duration, geometry, frame rate, rotation, audio, VFR |
| `scan_folder` | inventory a folder, flagging VFR clips and byte-identical duplicates |
| `transcode_to_cfr` | constant-frame-rate proxy, rotation preserved |
| `build_stringout` | assemble a timeline from files or folders |
| `verify_project` | render each clip's midpoint back out and compare to source |
| `check_audio` | measure the timeline's audio level over a window |
| `render_project` | render to a video file, frame rate handled safely |

The tools are coarse on purpose. The useful unit of work is a whole project
file; an agent placing clips one call at a time would get the frame arithmetic
wrong in exactly the ways `CLAUDE.md` describes, and nothing would report it.

## Why the wrapper

- Passing `width=`/`height=` to an MLT consumer **silently resets the output
  frame rate to 25**. The render succeeds with the right number of frames,
  stamped at the wrong rate, so the file reports the wrong duration and plays at
  the wrong speed.
- Kdenlive is a Flatpak, and a Flatpak has its **own private `/tmp`**. Give melt
  an output path there and it writes nothing and **exits 0**.
- A failing render still needs to look like a failure to the shell.

`CLAUDE.md` has the rest — memory that scales with clip count rather than
timeline length, why a near-lossless proxy is slower to edit with than the phone
original, and why the obvious VFR test misses real VFR footage. Each item was
measured, not assumed.

## Requirements

Kdenlive 26.08.1 as a Flatpak (`org.kde.kdenlive`, which supplies `melt`) or a
host MLT, plus ffmpeg/ffprobe and Python 3.10+. The MCP server needs `mcp`
(`pip install -e .`). Use absolute paths: the sandbox's working directory is not
yours.

## Note on metadata

Phone video carries creation timestamps and frequently GPS coordinates, and
`to_cfr()` does not strip them — an edit proxy is meant to match its source.
Projects also embed absolute paths into your home directory. `.gitignore` keeps
media and `.kdenlive` files out of the repo for that reason; strip metadata
before publishing renders.