FCPXML MCP
Edit Final Cut Pro by talking to Claude, and watch the cut change.
One prompt, seven tool calls, and the timeline re-rendered inline after each
write. Every frame is the real ui://fcp/timeline app drawing a real
view_timeline result; the B-roll provider is mocked and the recording says
so on screen. Full-size mp4, re-recorded
with demo/hero/build.py.
See it. view returns the timeline as exact rational-time JSON, and a
host with MCP Apps draws it inline: lanes, thumbnails, markers, transcript
words, a playhead, buttons that call the other tools.
Cut it. 15 grouped tools, 102 operations: trims, splits, silence removal, markers from transcripts and beats, rough cuts, keyframes and speed ramps, transcript edits, cross-NLE export, and a push into a running Final Cut Pro.
Trust it. Every write is journaled with its sha256 and can be undone.
deliver refuses an unreviewed cut, expected_sha256 refuses a file that
changed under the model, and generation is quote-then-confirm under a
per-call cap. 182 adversarial-input security tests, defusedxml everywhere,
sandboxed writes, no private APIs, and a disclosure channel.
claude mcp add fcpxml -e FCP_PROJECTS_DIR=~/Movies -- uvx fcp-mcp-server
Then File → Export XML… in Final Cut Pro, and ask Claude to work on the file.
Proof

Real v0.25.0 output against a running copy of Final Cut Pro. A ten clip
timeline is read back off the document, a marker is placed on every cut, and
dialogue, music and effects roles are assigned across the clips. Final Cut is
open on the left the whole time and is never touched by hand. Screen recorded
from demo/session.sh, which calls the same handlers an MCP
client calls; each import is verified by polling Final Cut until the project
appears, so a delivered Apple event is never reported as a landed import.
The earlier terminal only demo is still at docs/assets/demo.gif.
Re-record it with vhs demo/demo.tape; the media is synthesised by ffmpeg at
run time, so there is no fixture to keep.
Keyframes and speed curves
Contributed by @travisoa in
#26: the inspect
and edit groups include five video/audio animation actions and three speed
curve actions, imported into Final Cut Pro 12.4 without warnings. See
keyframe usage for installation and animation, and
speed curves for holds, sampled ramps, ripple and reset.
Quick Start
Claude Code (fastest)
claude mcp add fcpxml -e FCP_PROJECTS_DIR=~/Movies -- uvx fcp-mcp-server
Or project-scoped — commit a .mcp.json so your whole team gets it:
{
"mcpServers": {
"fcpxml": {
"command": "uvx",
"args": ["fcp-mcp-server"],
"env": { "FCP_PROJECTS_DIR": "/Users/you/Movies" }
}
}
}
With media intelligence (beat detection) and transcript editing (local Whisper):
claude mcp add fcpxml -e FCP_PROJECTS_DIR=~/Movies -- uvx --from "fcp-mcp-server[intelligence,transcribe]" fcp-mcp-server
Claude Desktop
Add to ~/Library/Application Support/Claude/claude_desktop_config.json:
{
"mcpServers": {
"fcpxml": {
"command": "uvx",
"args": ["fcp-mcp-server"],
"env": { "FCP_PROJECTS_DIR": "/Users/you/Movies" }
}
}
}
From source (contributors)
git clone https://github.com/DareDev256/fcp-mcp-server.git
cd fcp-mcp-server
pip install -e .
# then point your MCP client at: python /path/to/fcp-mcp-server/server.py
Use It
Export XML from Final Cut Pro (File → Export XML…), open your MCP client, and ask it to work with your timeline.
Why This Exists
After a decade directing music videos (Chief Keef, Migos, Masicka), I noticed the same editing bottlenecks on every project: counting cuts manually, extracting chapter markers one by one, hunting flash frames by scrubbing, building rough cuts clip by clip.
These are batch operations that don't need visual feedback. Export the XML, let Claude handle the tedium, import the result. That's the entire philosophy.
See It In Action
You: "Run a health check on my wedding edit"
Claude: ✓ Analyzed WeddingFinal.fcpxml
├─ 247 clips · 42:18 total · 24fps · 1920×1080
├─ 3 flash frames detected (clips 44, 112, 198)
├─ 2 unintentional gaps at 12:04 and 31:47
├─ 14 duplicate source clips
└─ Health score: 72/100
You: "Fix the flash frames and gaps, then add chapter markers from
this transcript"
Claude: ✓ Extended adjacent clips to cover 3 flash frames
✓ Filled 2 gaps by extending previous clips
✓ Added 18 chapter markers from transcript
→ Saved: WeddingFinal_modified.fcpxml
Import the modified XML back into Final Cut Pro. Every change is non-destructive — your original file is never touched.
What Claude Actually Sees
This is the magic trick. When you export XML from Final Cut Pro, your timeline becomes structured data that Claude can reason about:
<asset-clip ref="r2" offset="342/24s" name="Interview_A"
start="120s" duration="720/24s" format="r1">
<marker start="48/24s" duration="1/24s" value="Key quote"/>
<keyword start="0s" duration="720/24s" value="Interview"/>
</asset-clip>
# What Claude works with (after parsing)
Clip(
name="Interview_A",
offset=TimeValue(342, 24), # timeline position: 14.25s
start=TimeValue(120, 1), # source in-point: 2:00
duration=TimeValue(720, 24), # 30 seconds
markers=[Marker(value="Key quote", start=TimeValue(48, 24))],
keywords=["Interview"]
)
Every time value stays as a rational fraction — 720/24s, not 30.0 — so trim, split, and speed operations have zero rounding error across any frame rate, including the NTSC-fractional ones. A broadcast rate is carried as the exact rational it is (23.976 is 24000/1001, not a decimal), never as a truncated integer. Comparisons use cross-multiplication (a/b < c/d → a*d < c*b) to stay in integer-land end to end. Denominators are always normalized to positive values at construction, so sign lives on the numerator and cross-multiplication is always correct. Addition and subtraction share a single _binop() code path that handles same-denominator fast paths and LCM alignment in one place.
How It Works
┌──────────┐ ┌──────────────────────────────┐ ┌──────────┐
│ Final Cut│ │ parser.py → Python objects │ │ Final Cut│
│ Pro │─XML─>│ writer.py → Modify & save │─XML─>│ Pro │
│ │ │ rough_cut.py→ Generate new │ │ │
└──────────┘ │ diff.py → Compare │ └──────────┘
│ export.py → Resolve / FCP7 │
└──────────────────────────────┘
▲
Claude Desktop / MCP client
- Export from FCP —
File → Export XML... - Ask Claude — analyze, edit, generate, QC, export
- Import back —
File → Import → XML
What This Is NOT
- Not a plugin — it doesn't run inside Final Cut Pro
- Not for creative calls — color, framing, motion still need your eyes
New in v0.9 — Live Mode. The server can now push an FCPXML straight into the running Final Cut Pro with zero clicks, using Apple's official Open Document event — no XML re-import step. See Live Mode below.
Live Mode (macOS)
XML mode is offline and portable; Live mode drives a running Final Cut Pro through Apple's sanctioned surfaces — no patched binary, no private APIs, no accessibility scripting. Two tools, both verified end-to-end against FCP 12.2:
You: "Build a rough cut from my Interview clips and push it into Final Cut"
Claude: ✓ Generated RoughCut.fcpxml (8 clips, 0:54)
✓ Pushed into Final Cut Pro → library "ProjectX", event 2026-06-11
→ Open Final Cut Pro to keep editing
The asymmetry you must know: Apple makes import scriptable but offers no
programmatic export — to pull your current timeline back out for further AI
work, you still run File > Export XML yourself. Live mode pushes; round-trips
come back through the XML tools.
Notes (all live-verified): pass a library_location ending in .fcpbundle for
a true zero-click import (a new path is auto-created); omitting it makes FCP
show a modal library picker that blocks until you answer. First use triggers a
one-time macOS Automation permission prompt for your terminal/MCP host. The
capability audit maps the full surface and
the optional SpliceKit/CommandPost bridges planned for v1.0.
The Round Trip
Final Cut Pro has a fully scriptable import and no programmatic export — verified unchanged across FCP 11.0 → 12.2. So the loop closes on exactly one keystroke, and everything either side of it is automated:
watch_start ← once per session (or set FCP_WATCH_DIR)
↓
edit / generate / mark ← the change
↓
preview_check ← SEE it. Reads the media, not the XML
↓
deliver.push_to_fcp ← zero-click import (or FCP_MCP_AUTOPUSH=1)
↓
Cmd-E in Final Cut Pro ← the one manual step Apple leaves you
↓
watch_pull ← detected and diffed against the last export
preview_check is the part that matters. The preview:// resource and
preview_timeline both draw from the XML — they show what was written, so a
fixed flash frame and a broken one read identically through them. preview_check
samples the source media into a filmstrip over an audio waveform. It is the
difference between a tool reporting success and you knowing the cut is right.
If you have SpliceKit or CommandPost installed, watch_start says so. This
server does not call either one — their RPC signatures have not been verified
against a live install, and it never patches or injects anything. That is why it
runs on a managed Mac and survives an FCP update.
When To Use This
How It Compares
Three projects have connected AI agents to Final Cut Pro. They make different trade-offs:
SpliceKit's runtime depth is genuinely impressive — if you're on your own Mac and comfortable patching FCP, it can do things XML never will. This project stays on the no-patch side so it runs anywhere, survives every FCP update, and can be trusted with client libraries. Full ecosystem analysis: capability audit.
Prompt Cookbook
Copy-paste these into Claude Desktop. Each one maps to a real tool chain under the hood.
Analysis
"Give me a full breakdown of ProjectX.fcpxml — clips, duration, frame rate, markers, everything"
"Show me pacing analysis for my timeline — where are the slow sections?"
"Export an EDL and CSV of all clips with timecodes"
QC & Fixes
"Run a health check on my timeline and fix anything under 2 frames"
"Find all gaps and flash frames, then auto-fix them"
"Are there any duplicate source clips I can consolidate?"
Markers & Chapters
"Add chapter markers from this transcript: [paste transcript]"
"Import markers from my-subtitles.srt onto the timeline"
"List all markers and export them as YouTube chapter timestamps"
Generation
"Build a 60-second rough cut from clips tagged 'Interview' — medium pacing"
"Generate a montage from all B-roll clips with accelerating pacing"
"Create an A/B roll: Interview_A as primary, B-roll cuts every 8 seconds"
Cross-NLE & Reformat
"Export this timeline for DaVinci Resolve"
"Convert to FCP7 XML so I can open it in Premiere"
"Reformat my 16:9 timeline to 9:16 for Instagram Reels"
Under the Hood
When you say "Run a health check on my wedding edit", Claude chains these tools:
analyze_timeline → stats, frame rate, resolution
detect_flash_frames → clips under threshold duration
detect_gaps → unintentional silence/black
detect_duplicates → repeated source media
validate_timeline → structural health score (0-100)
Each tool returns structured text that Claude synthesizes into the summary you see. No magic — just batch XML queries that would take 20 minutes by hand.
Music videos and connected clips
A music video is usually built by laying an audio bed and hanging every visual
off it as a connected clip, so the spine holds one <gap> and the entire edit
lives on lanes. snap_to_beats and detect_flash_frames work on that shape:
snapping runs lane by lane, does not ripple the clips after the one it moves,
skips (and names) any move that would collide with a neighbour in the same
lane, and leaves the audio bed alone unless you pass include_audio_lanes.
It reports cuts considered, moved, already on a beat, out of reach, and
skipped — so "nothing moved" is something you are told rather than something
you discover in Final Cut.
reorder_clips, rapid_trim, fix_flash_frames and fill_gaps are still
primary-storyline only.
Pre-Built Prompts
Select these from Claude's prompt menu (⌘/) — they chain multiple tools automatically.
Every call takes the grouped form — the tool name is the group, and the
operation goes in action:
{ "action": "validate_timeline", "args": { "filepath": "/path/to/project.fcpxml" } }
Tools
As of v0.19.0, the MCP tool list Claude sees by default is 15 grouped verbs, not 102 flat tool names:
Each call has the same shape: {"action": "trim_clip", "args": {...}}. The
action is one of the 102 operation names below; args is whatever that
tool always took. The group dispatches straight into the same handler — the
behavior is identical, only the schema Claude sees up front is smaller. An
unknown or cross-group action returns an error listing the valid actions for
that group, so a wrong guess is recoverable in one turn.
Grouping is what's advertised, not what's callable. call_tool resolves
every one of the 102 operation names from a handler registry that doesn't care
what list_tools chose to show — an existing MCP config that calls trim_clip
directly keeps working with no changes. If you'd rather also see the flat tool
schemas (e.g. for debugging, or a client that doesn't like the grouped shape),
set:
FCP_MCP_LEGACY_TOOLS=1
This advertises the 71 flat schemas alongside the 15 groups — 86 tools in
total. The 31 operations that were born as group actions (preview, watch,
index, scenes, organize, find, plus import_edl_json) have no flat
schema and are reached through their group.
The flat tools will not be removed before a 1.0 release.
See the timeline before you touch it
Reading the preview://<path> MCP resource (any FCPXML path the server can
already reach) returns a self-contained HTML render of the timeline: clip
blocks sized proportionally to duration, connected clips on their own lane
rows above/below the primary storyline, and marker ticks — all values
HTML-escaped, served as text/html. Point your MCP client's resource viewer
at it, or fetch it directly, to see a cut without opening Final Cut Pro.

A real 164-second music video: 129 connected clips across 15 lanes, rendered from its FCPXML alone. Clip names on reference layers have been relabelled.
See the cut inline — the MCP Apps timeline (v0.26.0)
The view tool turns the same timeline into something a host renders
inside the conversation. Call view with view_timeline and a
filepath; a host that speaks MCP Apps (SEP-1865: Claude Desktop and
claude.ai, ChatGPT, VS Code, Goose) opens the ui://fcp/timeline resource
in a sandboxed iframe and posts the result into it.

The app rendered in headless Chromium through a stub host, 1280 px, with a
clip selected. The phone-width render is
docs/screenshots/2026-10-09_timeline-app-390.png.
What it shows: every lane (spine, connected video lanes above, audio lanes
below), clips sized to duration with their names and roles, markers, gaps,
a timecode ruler with a playhead and drag-to-select range, cached
transcript words under the clip they were said in, and a filmstrip
thumbnail per clip when ffmpeg can make one (bounded; coloured blocks
otherwise). Click a clip for its details. The buttons call back into the
server through the host: Inspect (view_clip) and Find similar
shots (find_shots) are reads; Add marker at playhead (mark) and
Delete clip (edit) are the ordinary edit actions, so the journal, the
project guard and the review gate apply exactly as they do from chat, and
the app re-renders from the new output file when the edit returns.
The payload is exact: every position and duration is a
numerator/denominator string straight from the model's Fractions, and
the tests round-trip both fixtures through it. A host without MCP Apps gets
a one-line summary and the JSON block as text, which is still the timeline.
The shell loads nothing from the network. Two honest limits: the in-iframe
protocol (ui/initialize, ui/notifications/tool-result, tools/call)
follows the spec text and the @modelcontextprotocol/ext-apps client and
has been driven by a stub host, not yet by a shipping one; and on mcp 1.x
the tool carries _meta.ui and the resource is served, but the SDK has no
slot to advertise the extension capability, so a host that insists on the
handshake needs mcp 2.x.
Generative fill — quote, then confirm, then insert (v0.26.0)
The gen tool fills a gap, adds B-roll over a clip, or extends a clip past
its out point with generated video. It spends money, so the order is
fixed and the gate is not optional:
gen {"action": "gen_quote", "args": {"provider": "fal", "seconds": 7, "prompt": "a street at dusk, handheld"}}
→ Quote 61585b429a17 — fal/kling-2.5, 10s billable for 7.00s requested
Estimated: $0.70 (prices as of 2026-10-09; third-party figures, NOT verified against an invoice)
Cap: $5 per call (FCP_MCP_GEN_MAX_USD)
gen {"action": "gen_fill_gap", "args": {"filepath": "…/cut.fcpxml", "prompt": "a street at dusk, handheld",
"seconds": 7, "quote_id": "61585b429a17", "confirm": true}}
Generation refuses — naming the fix each time — without confirm=true,
without a quote id, with a quote whose job changed (the quote is recomputed
from provider, model, billable seconds and prompt; a stale id cannot buy a
different job), without the provider's key in the environment, and above
the per-call cap FCP_MCP_GEN_MAX_USD (default 5). Kling and Wan bill 5
or 10 seconds, so a 7-second gap is a 10-second job and the quote says 10;
Gemini Omni Flash is quoted as one 10-second call (3–10 s output). Prices are the upper end of what the providers' pages
reported on prices_as_of; every quote and every result says they are
unverified.
Providers: fal (kling-2.5 default, wan-2.5) and gemini (omni-1.1-flash)
generate; runway (Aleph) and luma (Ray Modify) are video-to-video and
quote but refuse as not implemented rather than pretend. Keys come from
FAL_KEY, GEMINI_API_KEY, RUNWAY_API_KEY, LUMA_API_KEY only, travel in
one auth header, are never written or logged, and every reply is redacted;
the transport refuses to send a key in a URL or body. What comes back is
ffprobed for its real duration and frame rate (the model never invents
an fps), saved under generated/ beside the project through the sandboxed
output path so the journal records it, attached as a connected clip with
videoRole="generated" and a marker naming prompt and provider, snapped to
the sequence's frame grid, DTD-validated, and reported with the output
file's sha256. gen_broll and gen_extend_clip condition the generation on
a frame of the source clip when the media is present and say so when it is
not.
Claude Code skill
A final-cut-pro skill ships in skill/, wrapping this server with the
workflow order (inspect → diagnose → read preview:// → edit) and the
FCPXML gotchas that don't fit in a tool description. Install it alongside the
MCP server:
git clone https://github.com/DareDev256/fcp-mcp-server
ln -s "$PWD/fcp-mcp-server/skill" ~/.claude/skills/final-cut-pro
All 102 Operations
The 102 operations below are what the 15 groups in Tools dispatch
to — every action value the groups accept. The 71 flat-schema operations are
still callable directly with FCP_MCP_LEGACY_TOOLS=1.
Analysis — 11 tools
list_projects · analyze_timeline · list_clips · list_library_clips · list_markers · find_short_cuts · find_long_clips · list_keywords · export_edl · export_csv · analyze_pacing
Multi-Track — 3 tools
list_connected_clips · add_connected_clip · list_compound_clips
Roles — 4 tools
list_roles · assign_role · filter_by_role · export_role_stems
QC & Validation — 4 tools
detect_flash_frames · detect_duplicates · detect_gaps · validate_timeline
Editing — 9 tools
add_marker · batch_add_markers · insert_clip · trim_clip · reorder_clips · add_transition · change_speed · delete_clips · split_clip
Batch Fixes — 3 tools
fix_flash_frames · rapid_trim · fill_gaps
Comparison · Reformat · Silence
diff_timelines · reformat_timeline · detect_silence_candidates · remove_silence_candidates
NLE Export — 2 tools
export_resolve_xml (DaVinci Resolve FCPXML v1.9) · export_fcp7_xml (Premiere Pro / Resolve / Avid XMEML v5)
Generation — 3 tools
auto_rough_cut · generate_montage · generate_ab_roll
Beat Sync — 2 tools
import_beat_markers · snap_to_beats
Import — 3 tools
import_srt_markers · import_transcript_markers (supports SMPTE HH:MM:SS:FF with frame-accurate placement) · import_edl_json (video-use {sources, ranges, grade?} → FCPXML; ranges[].source is a key into sources, not a path — v0.17.0)
v0.6.0 — Audio, Compound, Templates, Effects — 6 tools
list_effects · add_audio · create_compound_clip · flatten_compound_clip · list_templates · apply_template
v0.8.0 — Media — 1 tool
relink_media (bulk-rewrite asset/media-rep src paths with dry_run preview — relink a moved drive without opening FCP)
v0.10–0.12 — Media Intelligence — 3 tools
detect_media_silence (analyzes each clip's real source audio with ffmpeg silencedetect and maps silence spans into timeline time) · remove_media_silence (cuts detected silence out of the timeline with ripple — clips split around silence, padding keeps edits breathing, non-destructive output) — both require ffmpeg, degrade gracefully without it · detect_beats (musical beat + tempo detection via librosa, writes a beats JSON that chains into import_beat_markers + snap_to_beats; needs the optional [intelligence] extra)
v0.13.0 / v0.18.0 — Transcript Intelligence — 4 tools
transcribe_media · edit_by_transcript · remove_filler_words · transcript_pack (v0.18.0 — one page of everything said; every one of the four takes backend: "elevenlabs" for speakers and audio events)
v0.9.0 — Live Mode (macOS + Final Cut Pro) — 2 tools
push_to_fcp (zero-click FCPXML import into the running FCP via Apple event) · list_fcp_libraries (enumerate open libraries/events/projects)
v0.17.0 — Preview — 5 group actions
preview_render · preview_sheet · preview_frame · preview_check · preview_timeline
Since v0.20.0 preview_render compiles crossfades and video lanes rather than
flattening them: a transition on a cut becomes an ffmpeg xfade (dissolve, dip
to colour, wipe, slide), and a connected clip is overlaid for its own window,
shifted by any crossfade that shortened the timeline before it. What the
renderer cannot honour is printed with the render — a transition with no cut
within its own duration, one whose neighbour is missing its media, a lane
drawn full-frame because transforms and opacity are not read, and audio lanes,
which are never mixed. The reported duration accounts for the overlaps, and
preview_render reads the artifact's own duration back against it.
v0.17.0 — Watch — 4 group actions
watch_start · watch_status · watch_stop · watch_pull
v0.18.0 — Index — 3 group actions
index_status · index_build · index_clear
v0.18.0 — Scenes — 3 group actions
detect_scenes · scenes_to_markers · scenes_split
v0.19.0 — Organize — 6 group actions
organize_keywords (add / remove / replace over a selection by glob name, keyword or role) · organize_rate (favorite / rejected / clear) · organize_roles · organize_auto (proposes keywords from cached captions and transcripts; never transcribes; apply=true writes) · history (the operation ledger as a table with ages) · undo (moves the last N recorded outputs to <journal>/undone/ — never deletes, refuses on hash mismatch)
v0.19.0 — Find — 3 group actions
find_shots (tiered router — transcript, metadata, vision — with the tier and a why on every hit; at most 20 live captions per call) · find_index (warm transcripts, scenes and opt-in captions for every source, reporting each as done / skipped / unavailable) · find_to_timeline (assemble the hits into a _found selects reel under the diversity constraint)
Environment Variables
Compatibility
Architecture
fcp-mcp-server/ ~15.7k lines Python
├── server.py MCP entry point — 15 grouped tools advertised by default
│ (TOOL_GROUPS), dispatching into 102 handlers
│ (TOOL_HANDLERS); 5 prompts, resource discovery.
│ FCP_MCP_LEGACY_TOOLS=1 re-advertises the flat tools.
│ Binds itself to tools/ via bind_server() — group modules
│ must never `import server` (it runs as __main__).
├── tools/ New tool groups, registered without growing server.py
│ ├── __init__.py EXTRA_GROUPS/EXTRA_HANDLERS registry + bind_server()
│ ├── _common.py text_result / parse_project through the bound module
│ ├── preview.py preview group — render, sheet, frame, check, timeline
│ ├── watch.py watch group — start, status, stop, pull
│ ├── index.py index group — status (with age), build, clear
│ ├── scenes.py scenes group — detect, to_markers, split
│ ├── organize.py organize group — keywords, rate, roles, auto, history, undo
│ ├── find.py find group — shots (tiered router), index, to_timeline
│ └── nle.py NLE export, effects, audio, compound clips, templates,
│ relink — moved out of server.py, re-exported by it
│ _resolve_io_paths() / _setup_modifier() / _setup_generator()
│ _format_clip_table() / _markdown_table() / _format_batch_result()
│ _raw_markers_to_batch()
│ _detect_flash_frames() / _detect_gaps() / _detect_duplicate_groups()
│ consolidate path validation, QC detection, rendering, handler boilerplate
├── fcpxml/
│ ├── journal.py Append-only operation ledger — paths + hashes, never content;
│ │ undo is a pointer move into undone/, refused on hash mismatch
│ ├── find.py Pure ranking over transcript words and metadata ranges, tier named
│ ├── vlm.py Offline MLX shot captions — HF offline flags set BEFORE import.
│ │ Captions a 1080p frame: vision tokens scale with pixel area
│ ├── diversity.py Source-separation constraint + diversity score for assemblies
│ ├── index.py SQLite analysis cache — keyed (path, mtime, size), num/den time,
│ │ rebuilt on corruption; NEVER a source of truth (CI runs it off)
│ ├── progress.py Per-clip MCP progress notifications on either SDK
│ ├── scenes.py Shot boundaries — PySceneDetect or ffmpeg's coarser scene filter
│ ├── transcript_pack.py Every transcript on one page; byte-measured 60KB cap
│ ├── transcribe.py Local faster-whisper, or ElevenLabs Scribe opt-in (speakers, events)
│ ├── filtergraph.py Timeline → ffmpeg graph. PURE — Fraction end to end,
│ │ so compilation is asserted without ffmpeg installed.
│ │ xfade crossfades + full-frame lane overlays; what it
│ │ cannot honour is reported, never silently dropped
│ ├── render.py Executes the graph; probes the artifact's OWN duration
│ │ back and reports drift against the timeline rational
│ ├── visual.py Filmstrip + waveform from SOURCE MEDIA (preview_check)
│ ├── watchfolder.py Export detection. Digests CONTENT, not (mtime, size)
│ ├── bridges.py SpliceKit :9876 / CommandPost :27480 — DETECTION ONLY
│ ├── edl.py video-use edl.json → FCPXML
│ ├── models.py TimeValue, Timecode, Clip, ConnectedClip, MarkerType, Timeline
│ ├── parser.py FCPXML → Python (spine, connected clips, roles, markers)
│ ├── writer.py Modify & write (markers, trim, gaps, transitions, silence)
│ │ FCPXMLModifier: index-based editing (clips/resources/formats dicts)
│ │ FCPXMLWriter: generate new FCPXML from Python objects
│ │ Helpers: _resolve_asset, _absorb_into_neighbor, _ripple_from_index
│ ├── rough_cut.py Generate timelines (rough cuts, montages, A/B roll)
│ ├── diff.py Timeline comparison engine (identity matching, threshold docs)
│ ├── export.py DaVinci Resolve v1.9 + FCP7 XMEML v5 export
│ ├── media_intel.py Real media analysis — audio silence detection via bounded ffmpeg subprocess
│ ├── preview.py Standalone HTML timeline render, served as preview://<path>
│ ├── safe_xml.py Centralized defusedxml wrappers (XXE/entity-bomb protection) + serialize_xml()
│ ├── dtd.py Validate output against Apple's official DTDs (located in the FCP app bundle)
│ └── templates.py Template system (intro/outro, lower thirds, music video)
├── skill/ final-cut-pro Claude Code skill wrapping this server
├── tests/ 2379 tests across 85 suites (collected 2026-10-24) — see Testing below
│ ├── test_models.py TimeValue math, Timecode formatting, MarkerType contracts
│ ├── test_parser.py FCPXML parsing, connected clips, edge cases
│ ├── test_writer.py Clip editing, marker writing, speed changes
│ ├── test_fcpxml_writer.py FCPXMLWriter generation from Python objects
│ ├── test_server.py MCP tool handlers, dispatch, path validation
│ ├── test_rough_cut.py Rough cut generation, montage, A/B roll
│ ├── test_diff.py Moved clips, transitions, markers, clip identity
│ ├── test_export.py Attribute stripping, compound flattening, audio tracks
│ ├── test_features_v05.py Multi-track, roles, diff, reformat, export
│ ├── test_features_v06.py Audio, compound clips, templates, effects, validation
│ ├── test_marker_pipeline.py Marker builder, batch modes, output format
│ ├── test_speed_cutting.py Speed cutting, montage config, pacing curves
│ ├── test_security.py Input validation, XML sanitization, XXE protection
│ ├── test_edge_cases.py Boundary arithmetic, clip collisions, split/diff edges
│ ├── test_diversity.py Boundary conditions across diff, models, validation
│ ├── test_refactored_helpers.py _index_elements, _iter_spine_clips, serialize_xml edges
│ ├── test_targeted_gaps.py Targeted branch coverage for diff, export, models
│ ├── test_bundles.py .fcpxmld bundles, sidecar preservation, FCPXML 1.13/1.14 tolerance
│ ├── test_relink.py Bulk media relink (URL + plain paths, dry run, segment matching)
│ ├── test_media_intel.py silencedetect parsing, timeline mapping, real-WAV integration, handler
│ ├── test_transcribe.py Phrase/filler span matching, range merge/invert algebra, Whisper handlers
│ ├── test_validation.py Pydantic input validation models
│ ├── test_live.py push_to_fcp / list_fcp_libraries (Apple events, mocked + live-gated)
│ ├── test_tool_groups.py TOOL_GROUPS dispatch to the TOOL_HANDLERS, legacy flag, schema size
│ ├── test_index*.py The cache: invalidation, rebuild, wiring, and the index group
│ ├── test_scenes*.py Shot boundaries on synthesised colour bars; the scenes group
│ ├── test_transcript_pack*.py The one-page pack and its handler
│ ├── test_transcribe_scribe.py Scribe backend with urlopen patched; key-leak mutation check
│ ├── test_preview.py preview:// HTML timeline render
│ ├── test_skill.py final-cut-pro skill structure
│ └── test_dtd_validation.py Output validated against Apple's shipped DTDs (skips without FCP)
├── docs/
│ ├── WORKFLOWS.md 8 production workflow recipes
│ └── CAPABILITY-AUDIT-2026-06.md Ecosystem audit + dual-mode (XML + Live) roadmap
└── examples/
└── sample.fcpxml 9 clips, 24fps — test fixture
Security
Every tool handler is hardened against adversarial input — critical for MCP servers where prompts may be LLM-generated, not human-typed.
Found a vulnerability? Report it privately via the repo's Security → Report a vulnerability tab — see SECURITY.md.
182 security-specific tests across test_security.py (pytest tests/test_security.py --collect-only -q) covering XXE, path traversal, sandbox root confinement (single and multi-root), resource caps (discovery walk, marker batch, inline transcript), output path anchoring, input validation, subprocess bounds, minidom hardening, JSON depth limits, role sanitization, ffmpeg parameter bounds, symlink resolution, resource-URI decoding, preview:// rejection paths, symlinked Final Cut library media, and write-handler sandbox enforcement. Ruff S (bandit) rules enforced in CI — S314/S320 block unsafe XML parsing, S105 catches hardcoded passwords, S108 flags insecure temp paths. Security events (null bytes, sandbox escapes, unhandled exceptions) are logged via Python logging for audit trails.
Timestamp Parsing — How Import Tools Place Markers
All subtitle and transcript import tools (import_srt_markers, import_transcript_markers) funnel through a single internal function: _parse_timestamp_parts() in server.py. Understanding it matters when timestamps don't land where you expect.
Supported Formats
The SMPTE 4-part format converts the frame component to fractional seconds: frames / frame_rate. The default rate is 24fps — pass frame_rate= to override for 25fps (PAL) or 30fps (NTSC) projects.
The Import Pipeline
SRT / VTT / YouTube chapters / plain transcript
│
▼
parse_srt() / parse_vtt() / parse_transcript_timestamps()
│ │ │
└────────────────┴──────────────────────┘
│
split on ':'
│
▼
_parse_timestamp_parts(parts, frame_rate=24.0)
│
▼
total seconds (float)
│
▼
marker placed on timeline
Edge Cases
- Unrecognized part counts (1 part, 5+ parts) return
None— the marker is silently skipped, not placed incorrectly - Zero frame rate — falls back to base seconds (frames ignored) rather than dividing by zero
- Milliseconds — only carried in 3-part format via
float()on the seconds component ("15.500"→15.5) - Frame rounding — SMPTE frames are divided exactly (
12/24 = 0.5), not rounded to the nearest frame boundary. The resulting float is converted to FCPXML's rationalTimeValuedownstream, preserving precision
Why This Matters
Before v0.6.20, the 4-part SMPTE parser silently dropped frames — 01:00:10:12 became 3610.0s instead of 3610.5s. At 24fps, that's up to ~0.96 seconds of drift per marker. If you imported a subtitle file with SMPTE timecodes, every marker was slightly off. This was subtle enough to pass QC but visible when scrubbing.
Design Principles
Documentation
Testing
uv run --extra dev pytest tests/ -v # or: python3 -m pytest tests/ -v
ruff check . --exclude docs/ # lint — must pass before committing
2373 tests across 85 suites — 2366 pass and 7 skip locally on v0.27.1 (mcp 1.28.1, measured 2026-10-20); 2367 pass and 6 skip on mcp 2.x; 2342 pass and 31 skip with FCP_MCP_INDEX=off (the extra skips there are the tests OF the cache). The other skips are the cases that need ffmpeg, PySceneDetect or Final Cut Pro present. Coverage spans models, parser, writer, FCPXMLWriter generation, server handlers, rough cut generation, speed cutting & pacing curves, marker pipeline, refactored helper functions, regression fixes, security hardening (XXE, entity expansion, path traversal, sandbox boundaries, minidom defense-in-depth, JSON depth limits, input validation, ffmpeg bounds, write-handler sandboxing), connected clips, roles, diff, export, compound clip flattening, audio track generation, templates, effects, .fcpxmld bundles with sidecar preservation, bulk media relink, gap-based multicam storylines (mc-clip angle resolution, caption text, connected-clip timeline positions, the media-tools view), real media silence detection, transcript-driven editing, filtergraph compilation, proxy rendering with artifact duration read-back, source-media visual checks, export watch detection, loopback bridge probing, EDL import, autopush, the operation journal and hash-checked undo, the deliver review gate, bulk organize edits, tiered shot search with its never-transcribes / never-downloads guards, the diversity constraint, the grouped tools dispatching to the flat handlers, the preview:// HTML render and its traversal/extension/null-byte/symlink rejection paths, the final-cut-pro skill, and DTD validation against Apple's official DTDs.
Several of those are mutation checks — they exist to prove an instrument can see the failure it is meant to catch, because a check that reads identically on a good and a bad result certifies nothing:
test_the_instrument_can_see_a_wrong_durationrenders a one-second timeline and asserts the probe reads something other than two seconds.test_the_waveform_is_actually_drawnrenders the same video against loud and near-silent audio, so the filmstrips are identical by construction and any byte difference must come from the waveform. This one caught a real shipped defect:showwavespicdraws on a transparent background that flattens to white, so a white trace was invisible while every ordinary check still passed.test_the_edit_still_reports_success_when_the_push_failsproves an autopush failure never costs you a file that is already on disk.test_a_touched_source_drops_its_rowsre-exports a source and asserts the cache forgets it; with the (mtime, size) check removed the stale rows survive and the test goes red.test_key_goes_in_the_header_and_nowhere_else— moving the ElevenLabs key into the URL makes it fail.test_version.pyassertsserver.__version__matchespyproject.toml; the two had disagreed since v0.17.0 without anything noticing.
Requirements
- Python 3.10+ · Final Cut Pro 10.4+ (FCPXML 1.8+) · Claude Desktop or any MCP client
- Dependencies (auto-installed):
mcp(1.21.1+, including 2.x),defusedxml - ffmpeg (optional) — needed for silence analysis (
detect_media_silence,remove_media_silence) [scenes]extra (optional) — adds PySceneDetect forscenesdetection that can see a cut between similar colours; without it the group falls back to ffmpeg's coarser scene filter and says so.[find]extra (optional, Apple Silicon) — addsmlx-vlm+numpysofindcan caption shots offline; without itfindanswers from transcript and metadata and says vision is unavailable.[intelligence]extra (optional) — adds librosa fordetect_beats; everything else works without it. Install viauvx --from "fcp-mcp-server[intelligence]" fcp-mcp-serverorpip install "fcp-mcp-server[intelligence]"(from source:pip install -e '.[intelligence]').- See Compatibility for full version matrix
Ecosystem — XML Mode Today, Live Mode Next
This server is the safe, offline layer of FCP automation: no patched binaries, no private APIs, runs on managed Macs, works without Final Cut Pro installed. It composes with the live-control side of the ecosystem rather than competing with it:
- SpliceKit by @elliotttate with @latenitefilms — live in-process control of a patched FCP copy with its own ~200-tool MCP server. The deep edit engine here and the live hands there are complementary by design; an optional bridge to SpliceKit's local JSON-RPC endpoint is on this project's roadmap (see below).
- CommandPost by @latenitefilms — nine years of accessibility-layer FCP automation with a built-in WebSocket control surface; another candidate live backend.
The full ecosystem analysis and the dual-mode architecture plan live in docs/CAPABILITY-AUDIT-2026-06.md.
Roadmap
- Core FCPXML parsing (reads v1.8–1.14, writes v1.13) — v0.8.0
-
.fcpxmldbundle support with object-tracking/Cinematic sidecar preservation — v0.8.0 - Bulk media relink (
relink_media) — v0.8.0 - DTD validation against Apple's official DTDs — v0.8.0
- Timeline analysis, markers, EDL/CSV export
- Clip editing (trim, reorder, split, speed, transitions)
- QC tools (flash frames, gaps, duplicates, health scoring)
- Generation (rough cuts, montages, A/B roll, beat sync)
- MCP Prompts + Resources (auto-discovery)
- Subtitle & transcript import as markers
- Multi-track (connected clips, compound clips, roles)
- Timeline diff + social media reformat
- Silence detection & cleanup
- Cross-NLE export (DaVinci Resolve, Premiere Pro, Avid)
- Live mode v1 — zero-click push-to-FCP via Apple events, AppleScript library inspection — v0.9.0
- Backend Protocol refactor (one operation layer shared by XML and Live)
- Media intelligence v1 — real silence detection from source audio (
detect_media_silence) — v0.10.0 - Silence auto-removal —
remove_media_silencecuts real silence with ripple — v0.11.0 - Beat detection —
detect_beats(librosa) chains into beat markers + snap-to-beats — v0.12.0 - Transcript-based editing — local Whisper transcription, edit_by_transcript (remove/keep_only), filler-word removal — v0.13.0
- 7 grouped tools —
inspect/diagnose/edit/mark/generate/transcript/deliverreplace the 62 flat tools as the advertised default, cutting the schema footprint 84.7%;FCP_MCP_LEGACY_TOOLS=1keeps the 62 available — v0.14.0 -
preview://HTML timeline render — see a cut without opening Final Cut Pro — v0.14.0 -
final-cut-proClaude Code skill — workflow order + FCPXML gotchas — v0.14.0 - Daily scheduled CI — catches upstream dependency breaks within 24h — v0.14.0
- The Loop —
preview(see the edit from the source media),watch(notice the Cmd-E export),import_edl_json, bridge detection, autopush — v0.17.0 - Speed and sight — SQLite analysis index with per-clip progress,
scenesshot-boundary detection,transcript_pack, opt-in ElevenLabs Scribe diarization — v0.18.0 - The moat and the ledger — operation journal +
history/undo, review gate ondeliver,organizebulk edits +organize_auto,findtiered shot search with offline MLX captions, diversity constraint — v0.19.0 - Crossfades and lanes in preview — transitions compile to ffmpeg
xfade, connected video lanes composite over the spine — v0.20.0 - Captions look at a 1080p frame —
findvision tier caps the frame it sends the VLM, so a 4K library no longer pays 4× the tokens (#21, @jardelapp) — v0.21.0 - A wheel that starts — the publish workflow installs the built wheel into a clean venv and runs
initialize+tools/listbefore a release ships; the MCP registry check readsisLatestand is compiled in CI — v0.21.1–0.22.1 - Gap-based storylines are visible —
mc-clipandcaptionconnected to a gap spine parse, the multicam angle resolves to its media, and every media tool walks connected clips too (#23, @tomartmedia) — v0.23.0 - Every tool reads the whole edit — the reporting tools (EDL, CSV, markers, keywords, pacing, roles) and the preview follow connected clips too, so a gap-based project no longer exports empty — v0.24.0
- See the cut — the
viewMCP Apps timeline rendered inline by the host,gengenerative fill behind a quote-then-confirm money gate, and theexpected_sha256project guard; the rest of the October idea set is indocs/ROADMAP-2026-10.md— v0.26.0 - Files Final Cut actually opens — transition uids checked against Final Cut's own plugin list (
FxPlug:prefix, seven wipes that matched no plugin, a "fade" that was a colour look), transitions refused when the clips lack handles, DTD-valid rough-cut transitions, assets copied whole, connected clips on the parent's clock and in DTD order (#30-#34, @brannonwinn) — v0.27.0 - Shot embeddings — tier-3 ranking is lexical over captions until embeddings land
- Live bridges — SpliceKit / CommandPost are detected since v0.17.0; the adapters that would drive them are not written
- Audio sync detection
- Premiere Pro native XML support
Known Issues
Reporting a Problem
You do not need a GitHub account, and you do not need to be a programmer.
- GitHub: open an issue — best for anything with a traceback or a sample FCPXML.
- Email: [email protected] — read directly, answered directly. If Claude did the diagnosis for you, paste its findings as-is; that is exactly how the v0.21.1 packaging bug was reported and it was correct in every particular.
If the client only says "Connection closed", the server died before it could answer. Run it by hand to see the real error:
uvx fcp-mcp-server # or: python server.py from a checkout
A Python traceback here (import error, missing dependency) is the whole story; send that.
Want to try builds before they ship? Say so in the email — pre-release wheels go out to a small list of working editors.
Status & Contributing
Actively maintained — live-verified against FCP 12.2, with external contributions already merged and credited: @mikegrant25 (sandbox security fix, #6), @jardelapp (audio duration probing, #7), and Marty Hou, documentary editor, who reported the v0.19.2–0.21.0 packaging failure by email. The diagnosis he forwarded was produced with Claude on his own machine, checked against the published artifacts rather than from memory, and held on every point — his words, recorded here the way he asked for them.
PRs welcome. If you're a video editor who codes (or a coder who edits), let's build this together.
Credits
Built by @DareDev256 — former music video director, now building AI tools for creators.
License
MIT — see LICENSE.
mcp-name: io.github.DareDev256/fcpxml-mcp-server
