Skip to content

Releases: DareDev256/fcp-mcp-server

v0.15.0 — it can edit a music video now

Choose a tag to compare

@DareDev256 DareDev256 released this 04 Aug 14:28

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 after list(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

Choose a tag to compare

@DareDev256 DareDev256 released this 04 Aug 13:34

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_clips counts 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

Choose a tag to compare

@DareDev256 DareDev256 released this 04 Aug 09:58

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

Choose a tag to compare

@DareDev256 DareDev256 released this 04 Aug 09:52

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

Choose a tag to compare

@DareDev256 DareDev256 released this 04 Aug 09:24

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

Choose a tag to compare

@DareDev256 DareDev256 released this 04 Aug 03:52

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

Choose a tag to compare

@DareDev256 DareDev256 released this 04 Aug 01:03

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-pro

The 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:// and file:// resource URIs are percent-decoded, so filenames containing spaces work. ~/Movies is 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-server

Your 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

Choose a tag to compare

@DareDev256 DareDev256 released this 03 Aug 21:50

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

Choose a tag to compare

@DareDev256 DareDev256 released this 25 Jul 09:07

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

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

Choose a tag to compare

@DareDev256 DareDev256 released this 23 Jul 22:35

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.json next to each media file so transcription is a one-time cost. Optional SRT output plugs into import_srt_markers.
  • edit_by_transcript — cut timeline content by what was SAID. mode=remove cuts every occurrence of the given phrases with ripple; mode=keep_only keeps 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.