Releases: DareDev256/fcp-mcp-server
Release list
v0.15.0 — it can edit a music video now
Yesterday this tool could read a real project correctly and could not edit one. That is fixed.
snap_to_beats moved 0 cuts and called it success
A music video in Final Cut is built by laying an audio bed and hanging every visual off it as a connected clip. The spine holds one <gap>. Every edit handler looked for cuts on the spine, found nothing, and reported success.
Measured on a real 164-second music video: 128 cuts across 14 lanes, 71 moved, 22 already on a beat, 35 skipped as collisions. Previously: 0 clips, 0 cuts, "synced to the beat!"
Non-rippling, and lanes move independently. Moving one connected clip does not shift the rest of its lane — connected clips are not magnetic to each other, and rippling would rearrange an edit you already made. A move that would overlap a neighbour is skipped and reported, never forced. Negative (audio) lanes are excluded by default, since on a music video that is the track the beats came from.
Four bugs surfaced underneath and had to be fixed for any of it to work: import_beat_markers raised on every music video, a connected clip's offset is in its host's time frame rather than the timeline's, _timeline_duration reported a 164s timeline as 170.96s, and the validator flagged Final Cut's own 1001/24000s timebase as non-standard.
detect_flash_frames was also spine-only and missed a 1-frame connected clip on the real project. Fixed. rapid_trim, fix_flash_frames and fill_gaps have the same blind spot and are deliberately unchanged — a wrong write on someone's edit costs more than a missing feature. Tracked.
detect_beats returned onsets, not a grid
librosa reports beats it hears, so an intro with no percussion and a drum-out before the outro came back empty — exactly the sections an editor is eyeballing, and you cannot snap a cut to a beat that was never reported.
When enough observed beats agree on one fixed grid, the period and phase are now solved from them and the grid is extended across the whole file. Rubato still returns observed beats untouched; inventing beats on a track that genuinely moves is worse than reporting none. source says which path was taken.
The load-bearing detail: the median inter-onset interval is not the period. librosa quantises to ~11.6ms frames, so a true 0.5s grid lands on 43 frames more often than 44 and the median comes back 0.4992s. That 0.8ms error compounds to 0.23s across 286 beats. It needs a least-squares refit over beat index.
| before | after | independent reference tracker | |
|---|---|---|---|
| BPM | 120.185 | 119.996 | 120.005 |
| Beats | 286 | 329 | 328 |
| Coverage | 16.0-158.6s | 0.03-164.03s | full |
Security: roots that confine reads, and three caps that were only ever claimed
FCP_PROJECTS_DIRS (new, opt-in, colon-separated) confines reads, not just listing. FCP_PROJECTS_DIR is unchanged and still confines listing only, so nobody breaks on upgrade — which matters, because the README tells every user to set it.
A path is allowed if the path as given or its resolved target is inside a root. Final Cut imports media leave-files-in-place, so Original Media/ is full of symlinks; resolving them first would reject the media Final Cut itself put in your library. Traversal still normalises before the check.
Three caps that the security matrix used to claim and never had, all configurable, all returning an explicit truncation notice rather than silently returning a partial result:
FCP_MAX_DISCOVERY_FILES(10,000) — the walk stops, rather than collecting everything and slicing. A cap applied afterlist(rglob(...))still walks the filesystem, which was the actual denial of service.FCP_MAX_BATCH_MARKERS(10,000)FCP_MAX_TRANSCRIPT_CHARS(1 MiB), cut on a line boundary so a timestamp is never split
Also
The minimum-mcp CI job now reads the dependency floor out of pyproject.toml instead of hardcoding it in a third place. A check that fails spuriously is a check somebody deletes.
1195 tests passing, up from 1108. Every new test was sabotage-verified — 23 separate mutations, each reverted after measuring which tests failed.
Full Changelog: v0.14.5...v0.15.0
v0.14.5 — preview header fixes
Three display bugs, all found while rendering the README screenshot. All three showed on every connected-clip project and none were visible against examples/sample.fcpxml.
- The header read "0 clips" on a timeline holding 129 of them.
Timeline.total_clipscounts spine clips only, so every music-video-shaped project reported zero. It now reports the real total and how many lanes they span. - An empty spine rendered as a grey box taking a third of the frame, on projects where nothing sits on the spine by design. The row is omitted when there is nothing to draw.
- Frame rate printed as
23.976023976023978fps.
Fourth time in one day that fixture hid something, which is why v0.14.4 added one shaped like real work.
Full Changelog: v0.14.4...v0.14.5
v0.14.4 — the fixture that would have caught all three
examples/sample.fcpxml is spine-based, starts at 0s, and has no connected clips. That shape hid three separate bugs in a single afternoon, every one of them found only by pointing the tool at an actual project.
examples/music-video.fcpxml reproduces all three conditions on purpose:
tcStart="0s"disagreeing with element offsets that begin at 3600s- a 23.98 sequence format followed in the file by a 50p source format
- a spine holding one
<gap>, with all 8 clips connected across lanes -1, 1 and 2
Proven rather than assumed. Reintroducing each fixed bug against it:
| sabotage | result |
|---|---|
| origin forced to 0 | every clip at left:100% |
| last-format-wins restored | 50.0 fps on a 23.98 timeline |
Neither is reachable with sample.fcpxml.
Guard tests assert the fixture keeps those properties, so it cannot quietly drift back toward the shape that hid the bugs. Issue #16 now has something to be fixed against.
Full Changelog: v0.14.3...v0.14.4
v0.14.3 — frame rate was wrong by 2x on mixed-format projects
Second bug found dogfooding the same real music video.
A 3840x2160 timeline at 23.98 fps, in a project holding one 25p drone clip, parsed as 50.0 fps. The parser overwrote its frame rate from every <format> in resources, so whichever came last in the file won — it never resolved the sequence's own format attribute.
The wrong header was the visible symptom. The real problem is downstream: Timecode.frame_rate drives total_frames and to_smpte(), so every seconds-to-frames conversion was off by that factor.
164 seconds -> 8200 frames (wrong, at 50.0)
164 seconds -> 3932 frames (right, at 23.976)
Anything placing a cut or a marker on a specific frame was computing against a rate the timeline never had. Snapping a cut to a beat would have landed on the wrong frame.
The sequence's format is now resolved to its <format> element and that rate wins, applied before any Timecode is constructed. An unresolvable format falls back to the first declared, which is conventionally the sequence's own.
Closes #15.
Full Changelog: v0.14.2...v0.14.3
v0.14.2 — the preview works on real projects now
Found on the first real music video, an hour after shipping 0.14.0.
Final Cut Pro starts sequences at 01:00:00:00 by broadcast convention. So in an exported project, element offsets begin at 3600s — a 164-second timeline whose clips sit between 3600s and 3743s.
The preview assumed an origin of 0. It computed left: 2195% for the first block and clamped every one of them to the right edge, rendering a real project as a single stripe.
examples/sample.fcpxml starts at 0. That is the only reason the entire 0.14.0 test suite passed.
The origin is now derived from the earliest element, which handles a 0-based sequence, an hour-offset one, and anything else. It deliberately does not read tcStart — that attribute reads 0s on real projects whose clips nonetheless start at 3600s, so trusting it would have reproduced the bug.
Verified against the project that found it: 129 connected clips across lanes 1-14, now spanning 0.0% to 87.18%, none clamped.
Full Changelog: v0.14.1...v0.14.2
v0.14.1 — say which argument is missing
Found on the first real-footage test of 0.14.0.
Grouped calls nest parameters under args, so an action's required fields are no longer visible in the advertised schema and the caller has to guess them. Most handlers take filepath. A few do not — the beat tools take media_path — and guessing wrong returned:
Error: KeyError
No key name, nothing to correct, no way for a model to recover. That dead end did not exist with the flat schema, so it was a regression the 0.14.0 grouping introduced.
Now:
Missing required argument: media_path
'detect_beats' accepts:
media_path (required): Path to audio/video file (.wav, .mp3, .m4a, .aac, .aif, .flac, .mov, .mp4)
Applies to flat calls as well as grouped ones.
Full Changelog: v0.14.0...v0.14.1
v0.14.0 — 62 tools become 7
Sixty-two tools became seven. Nothing broke.
84.7% less schema in every conversation
The server advertised 62 flat tools. Every one of them injected its full JSON schema into the context before you typed a word: 34,409 characters. That is a tax paid on every single turn, and it makes the model worse at picking a tool, not better.
The same capability is now seven grouped verbs:
inspect · diagnose · edit · mark · generate · transcript · deliver
Each takes {"action": "...", "args": {...}} and dispatches into exactly the same 62 handlers. Same code, same behaviour, same results. 5,258 characters.
If you pass an action a group does not own, the error lists the valid ones rather than just failing.
Nothing breaks
call_tool resolves handlers from a registry that is independent of what list_tools advertises. So an existing config calling trim_clip by name keeps working exactly as before, whether or not the flat tools are advertised. Not a shim, not a translation layer — the flat names were never the dispatch mechanism.
Want the old list back? FCP_MCP_LEGACY_TOOLS=1 advertises the original 62 alongside the seven groups. They will not be removed before 1.0.
See the cut before you import it
Editing FCPXML through an MCP server was blind. You called a tool, got text back, and had no idea what the timeline looked like until you imported into Final Cut Pro.
Reading preview://<path> now returns a self-contained HTML render of the timeline: clip blocks sized in proportion to duration, connected clips drawn on lane rows above and below the spine, marker ticks, everything escaped, served as real text/html.
A Claude Code skill
skill/ ships a final-cut-pro skill that wraps this server with the workflow order and the FCPXML gotchas that do not fit in a tool description — which silence-removal path actually reads your audio, why offset and start are different numbers, what a lane means, and why you diagnose before you edit.
This closes the design question @MikeChongCan raised in #2 back in March: should this be a skill instead of an MCP server? The answer turned out to be both, in that order.
git clone https://github.com/DareDev256/fcp-mcp-server
ln -s "$PWD/fcp-mcp-server/skill" ~/.claude/skills/final-cut-proThe dependency floor was wrong
server.py imports mcp.server.lowlevel.helper_types, which does not exist before mcp 1.3.0 — while pyproject.toml declared mcp>=1.0.0. In any environment where another package held mcp below 1.3.0, this server died at import and the client just showed "disconnected" with no surfaced error.
The floor is now mcp>=1.3.0,<2.0.0, verified by execution rather than assumption: 1.2.1 fails, 1.3.0 works.
A week ago the upper bound was the problem — mcp 2.0.0 removed the low-level decorator API and broke every fresh install for seven days before anyone said anything. Both failures are the same shape: a declared version range that was not true. So CI now runs the suite against the minimum supported mcp, not only the latest, and runs daily at 06:00 UTC instead of only when someone pushes.
Also fixed
preview://andfile://resource URIs are percent-decoded, so filenames containing spaces work.~/Moviesis full of them.- The five built-in prompts no longer instruct the model to call tools it cannot see.
- Five rows of the README security matrix described protections that were never implemented. They are gone, and the remaining thirteen are backed by tests.
Upgrading
uvx fcp-mcp-server # or
pip install -U fcp-mcp-serverYour config needs no changes. Your client will refresh from 62 tools to 7. Anything calling a flat name by hand keeps working.
Full Changelog: v0.13.2...v0.14.0
v0.13.2 — unbreak installs (mcp 2.0 pin) + faster silence detection
Two fixes. If you installed this server on or after 2026-07-28, you want this release.
Pinned mcp<2.0.0, which unbreaks every fresh install
mcp 2.0.0 shipped to PyPI on 2026-07-28 and removed the low level Server decorator API this server is built on. list_tools, call_tool, list_resources, read_resource, list_prompts and get_prompt are all gone in 2.x, replaced by on_* constructor callbacks.
Because the dependency was declared as an open-ended mcp>=1.0.0, every install after that date resolved to 2.0.0 and server.py failed at import:
AttributeError: 'Server' object has no attribute 'list_resources'
That took six test modules down during collection. Main's last CI run was 2026-07-27, one day before the release, so the breakage first surfaced on an unrelated contributor PR and looked like that PR's fault. It was not.
The bound resolves to mcp 1.29.0. Migration to the 2.x callback API is tracked in #9. The pin is the stopgap, not the answer.
detect_media_silence no longer decodes the video stream
detect_silence() ran ffmpeg's silencedetect filter with no stream selection, so ffmpeg decoded the entire video track into the null muxer just to analyse audio. On long or high bitrate camera files that blew past PROBE_TIMEOUT_SECONDS (120s) and silence analysis returned nothing. A 2.5GB, 971 second iPhone MOV timed out every time.
Adding -vn restricts the pass to the audio stream. The same file now analyses in about a second, with byte identical silencedetect output, since the filter only ever looked at audio.
Thanks to @jardelapp for the report and the fix (#8).
Also
Test count 1032 to 1033. Suite green on Python 3.10, 3.11 and 3.12.
Full Changelog: v0.13.1...v0.13.2
v0.13.1 — Official MCP Registry release
Registry release. Adds the mcp-name ownership marker to the README (required by the official MCP registry to bind the PyPI package to io.github.DareDev256/fcpxml-mcp-server) and trims server.json's description to the registry's 100-char limit. No code changes from v0.13.0.
Install
uvx fcpxml-mcp-server- PyPI: https://pypi.org/project/fcpxml-mcp-server/
- Listed on the official MCP registry as
io.github.DareDev256/fcpxml-mcp-server
62 tools across Final Cut Pro XML — including Transcript Intelligence (edit_by_transcript, remove_filler_words, transcribe_media) shipped in v0.13.0.
v0.13.0 — Transcript Intelligence
Text-based editing lands. 59 → 62 tools.
Apple put Final Cut Pro's AI (Transcript Search, Generate Captions) behind the Creator Studio subscription. This release brings the agentic version to everyone, free, via local Whisper — and goes further: the transcript doesn't just search, it cuts.
New tools
transcribe_media— word-level transcription of each clip's source media, locally (faster-whisper, optional[transcribe]extra). Transcripts are cached as_transcript.jsonnext to each media file so transcription is a one-time cost. Optional SRT output plugs intoimport_srt_markers.edit_by_transcript— cut timeline content by what was SAID.mode=removecuts every occurrence of the given phrases with ripple;mode=keep_onlykeeps only the matches (clips with no matches are never deleted).remove_filler_words— um/uh cleanup from real source audio with ripple. Conservative defaults: "like" and "so" are speech, not noise — opt-in only.
Notes
- Same graceful-degradation contract as ffmpeg/librosa: no faster-whisper → install hint, never a crash.
- 1032 tests (42 new), all green. Whisper model names allowlist-validated; per-call transcription capped at 10 media files.
Full details in the CHANGELOG.