The full upstream README, mirrored here for reference. Install config, tool schemas, adoption signals, and an original overview live on the Fcpxml MCP Server listing page.
The bridge between Final Cut Pro and AI. 13 grouped tools (88 underlying operations) that turn timeline XML into structured data Claude can read, edit, generate, SEE, find — and undo.
Hardened for real libraries: 182 adversarial-input security tests, defusedxml everywhere, sandboxed writes, no patched binaries, no private APIs — plus a private disclosure channel with externally reported fixes already credited and merged.

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.
Or project-scoped — commit a .mcp.json so your whole team gets it:
With media intelligence (beat detection) and transcript editing (local Whisper):
Add to ~/Library/Application Support/Claude/claude_desktop_config.json:
Export XML from Final Cut Pro (File → Export XML…), open your MCP client, and ask it to work with your timeline.
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.
Import the modified XML back into Final Cut Pro. Every change is non-destructive — your original file is never touched.
This is the magic trick. When you export XML from Final Cut Pro, your timeline becomes structured data that Claude can reason about:
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.
File → Export XML...File → Import → XMLNew 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.
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:
| Tool | What it does |
|---|---|
push_to_fcp | Sends an FCPXML file into FCP with zero clicks (Open Document Apple event). Injects <import-options> (library location, copy/link assets, suppress warnings), launches FCP if needed, and never mutates your original — flat files get an options-injected copy. |
list_fcp_libraries | Enumerates FCP's open libraries → events → projects via the read-only AppleScript dictionary. |
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.
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:
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.
| Good For | Not Ideal For |
|---|---|
| Batch marker insertion (100 chapters from a transcript) | Fine-tuning cuts (faster directly in FCP) |
| QC before delivery (flash frames, gaps, duplicates) | Colour, framing and motion (nothing here grades) |
| Data extraction (EDL, CSV, chapter markers) | Sound mixing beyond stems and role splits |
| Template generation (rough cuts from tagged clips) | Anything needing a scrub through the actual cut |
| Automated assembly (montages from keywords + pacing) | |
| Timeline health checks (validation, stats, scoring) | |
| Logging and search before the edit (scenes, transcript, shot search) |
Three projects have connected AI agents to Final Cut Pro. They make different trade-offs:
| FCPXML MCP (this) | SpliceKit | CommandPost | |
|---|---|---|---|
| Approach | Parses/writes FCPXML + official Apple events only | Patches FCP's binary to expose internal APIs | Accessibility scripting + Lua |
| Raw live control | Push-to-FCP, library inspection | Deepest (full internal API) | Deep (UI-level) |
| Survives FCP updates | Yes — no patching | Re-patch per FCP version | Mostly |
| Works on managed/corporate Macs | Yes | No (requires binary patching) | Varies (Accessibility perms) |
| Works without FCP installed | Yes (pure XML mode) | No | No |
| MCP server | Yes, active (this repo) | Yes (last release Apr 2026) | Planned, PR unmerged |
| Requires | Python 3.10+ | Patched FCP binary | CommandPost app |
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.
Copy-paste these into Claude Desktop. Each one maps to a real tool chain under the hood.
Analysis
QC & Fixes
Markers & Chapters
Generation
Cross-NLE & Reformat
When you say "Run a health check on my wedding edit", Claude chains these tools:
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.
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.
Select these from Claude's prompt menu (⌘/) — they chain multiple tools automatically.
| Prompt | What It Does | Grouped calls it drives |
|---|---|---|
| qc-check | Full quality control — flash frames, gaps, duplicates, health score | diagnose → validate_timeline, detect_flash_frames, detect_gaps, detect_duplicates; then edit → fix_flash_frames, fill_gaps |
| youtube-chapters | Extract chapter markers formatted for YouTube descriptions | inspect → list_markers, analyze_pacing |
| rough-cut | Guided rough cut — shows clips, suggests structure, generates | inspect → list_library_clips, list_keywords; then generate → auto_rough_cut |
| timeline-summary | Quick overview — stats, pacing, keywords, markers, assessment | inspect → analyze_timeline, analyze_pacing, list_keywords, list_markers |
| cleanup | Find and auto-fix flash frames and gaps | diagnose → validate_timeline; then edit → fix_flash_frames, fill_gaps |
Every call takes the grouped form — the tool name is the group, and the
operation goes in action:
As of v0.19.0, the MCP tool list Claude sees by default is 13 grouped verbs, not 88 flat tool names:
| Group | Covers |
|---|---|
inspect | Read-only understanding — stats, clips, markers, keywords, EDL/CSV, pacing |
diagnose | Finding problems — flash frames, gaps, duplicates, health score |
edit | Changing clips — markers, trim, reorder, transitions, speed, split, silence removal |
mark | Markers and chapters — batch add, SRT/VTT import, beat import |
generate | Building new structure — rough cuts, montages, A/B roll, templates |
transcript | Local Whisper transcription and transcript-driven cuts; transcript_pack puts the whole shoot on one page; backend: elevenlabs opts into speaker labels and audio events (audio leaves the machine) |
deliver | Getting the timeline out — NLE export, reformat, relink, push-to-FCP |
preview | Seeing the edit — ffmpeg proxy render, contact sheet, and a filmstrip+waveform check read from the SOURCE MEDIA |
watch | Closing the round-trip — notice the operator's Cmd-E export and diff it against the last one |
index | The analysis cache — status with its age, warm every source in a timeline, clear. Nothing depends on it; FCP_MCP_INDEX=off and every tool still answers, only slower |
scenes | Shot boundaries from the pixels — list cuts per clip in source and timeline time, drop a marker on each, or split the clips there. PySceneDetect when installed, ffmpeg otherwise |
organize | Library housekeeping — bulk keywords, ratings and roles over a clip selection; organize_auto proposes keywords from captions and transcripts; history reads the operation ledger and undo moves the last outputs aside (never deletes) |
find | "Find the shot where…" — a router over transcript words, metadata and offline vision captions that names the tier on every hit; find_index warms every source; find_to_timeline assembles the hits into a selects reel |
Each call has the same shape: {"action": "trim_clip", "args": {...}}. The
action is one of the 88 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 88 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:
This advertises the 63 flat schemas alongside the 13 groups — 76 tools in
total. The 25 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.
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.
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:
The 88 operations below are what the 13 groups in Tools dispatch
to — every action value the groups accept. The first 63 are unchanged from
prior releases and still callable directly with FCP_MCP_LEGACY_TOOLS=1.
| Category | Tools | What It Does |
|---|---|---|
| Analysis | 11 | Stats, clips, markers, keywords, EDL/CSV, pacing |
| Multi-Track | 3 | Connected clips, compound clips, secondary lanes |
| Roles | 4 | List, assign, filter, export stems |
| QC & Validation | 4 | Flash frames, duplicates, gaps, health score |
| Editing | 9 | Markers, trim, reorder, transitions, speed, split |
| Batch Fixes | 3 | Auto-fix flash frames, rapid trim, fill gaps |
| Comparison | 1 | Diff two timelines — added/removed/moved/trimmed |
| Reformat | 1 | Aspect ratio conversion (9:16, 1:1, 4:5, custom) |
| Silence | 2 | Detect and remove silence candidates (XML heuristics) |
| Media Intelligence | 3 | Real silence detection + auto-removal (ffmpeg), musical beat detection (librosa) |
| NLE Export | 2 | DaVinci Resolve v1.9, FCP7 XMEML v5 |
| Generation | 3 | Rough cuts, montages, A/B roll |
| Beat Sync | 2 | Import beat markers, snap cuts to beats |
| Import | 3 | SRT/VTT subtitles, YouTube chapters → markers; video-use edl.json → FCPXML |
| Audio | 1 | Add audio clips, music beds at any lane |
| Compound | 2 | Create/flatten compound clips |
| Templates | 2 | Pre-built timeline structures (intro/outro, lower thirds, music video) |
| Effects | 1 | List FCP transition effects with UUIDs |
| Media | 1 | Bulk relink moved/renamed media (rewrite media-rep src paths) |
| Transcript Intelligence | 4 | Local Whisper transcription, transcript-driven cuts, filler-word removal, the one-page transcript pack |
| Live (macOS) | 2 | Push FCPXML into the running FCP (zero-click Apple-event import); list open libraries |
| Preview | 5 | Proxy render, contact sheet, single frame, filmstrip+waveform check from the source media, HTML timeline |
| Watch | 4 | Start/status/stop an export watch folder; pull the latest export and diff it |
| Index | 3 | Analysis cache status (with age), build, clear |
| Scenes | 3 | Detect shot boundaries, mark them, split on them |
| Organize | 6 | Bulk keywords/ratings/roles, auto-proposed keywords, operation history, hash-checked undo |
| Find | 3 | Shot search across transcript, metadata and vision tiers; warm the index; assemble a selects reel |
| 88 |
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
list_connected_clips · add_connected_clip · list_compound_clips
list_roles · assign_role · filter_by_role · export_role_stems
detect_flash_frames · detect_duplicates · detect_gaps · validate_timeline
add_marker · batch_add_markers · insert_clip · trim_clip · reorder_clips · add_transition · change_speed · delete_clips · split_clip
fix_flash_frames · rapid_trim · fill_gaps
diff_timelines · reformat_timeline · detect_silence_candidates · remove_silence_candidates
export_resolve_xml (DaVinci Resolve FCPXML v1.9) · export_fcp7_xml (Premiere Pro / Resolve / Avid XMEML v5)
auto_rough_cut · generate_montage · generate_ab_roll
import_beat_markers · snap_to_beats
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)
list_effects · add_audio · create_compound_clip · flatten_compound_clip · list_templates · apply_template
relink_media (bulk-rewrite asset/media-rep src paths with dry_run preview — relink a moved drive without opening FCP)
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)
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)
push_to_fcp (zero-click FCPXML import into the running FCP via Apple event) · list_fcp_libraries (enumerate open libraries/events/projects)
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.
watch_start · watch_status · watch_stop · watch_pull
index_status · index_build · index_clear
detect_scenes · scenes_to_markers · scenes_split
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)
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)
| Variable | Required | Default | Description |
|---|---|---|---|
FCP_PROJECTS_DIR | No | ~/Movies | Root directory for FCPXML discovery via list_projects. Confines listing only — it does not restrict which files you can open |
FCP_PROJECTS_DIRS | No | unset | Sandbox roots, separated like PATH (: on macOS/Linux) — e.g. ~/Movies:/Volumes/Scratch/Projects. This is the only variable that confines reads, and it is fully opt-in. Symlinked library media (Final Cut's default "leave files in place" import) stays readable |
FCP_MAX_DISCOVERY_FILES | No | 10000 | Cap on files collected by one list_projects directory walk. The walk stops at the cap and the result says it is incomplete |
FCP_MAX_BATCH_MARKERS | No | 10000 | Cap on markers written by one batch or import operation. Excess markers are reported as dropped, never silently skipped |
FCP_MAX_TRANSCRIPT_CHARS | No | 1048576 | Cap on inline transcript text passed to import_transcript_markers |
FCPXML_DTD_DIR | No | FCP app bundle | Directory of Apple FCPXMLv*_*.dtd files for DTD validation (auto-detected from the installed Final Cut Pro) |
FCP_MCP_LEGACY_TOOLS | No | unset | Set to 1 to advertise the 63 flat tool schemas alongside the 13 grouped tools |
FCP_WATCH_DIR | No | unset | Default folder watch_start observes for Final Cut Pro XML exports |
FCP_MCP_AUTOPUSH | No | unset | Set to 1 so every write also imports into the running Final Cut Pro. Off by default — repeated imports accumulate library churn, which is your call to make |
FCP_MCP_INDEX | No | ~/.fcp-mcp/index.db | Where the analysis cache lives. off disables it entirely; every tool still works, it just recomputes. Any other value is a path |
ELEVENLABS_API_KEY | No | unset | Enables backend: "elevenlabs" on the transcript tools. Sent as the xi-api-key header and nowhere else; never read unless that backend is requested |
FCP_MCP_JOURNAL | No | ~/.fcp-mcp/journal/ | Where the operation ledger lives (paths and hashes, never content). off disables it — history/undo then say so and the deliver review gate refuses to certify anything |
FCP_MCP_VLM_MODEL | No | mlx-community/Qwen2-VL-2B-Instruct-4bit | Hub id of the MLX vision model find captions shots with. Loaded offline only; a missing model is reported with its hf download command, never fetched |
| Component | Supported Versions |
|---|---|
| FCPXML format | reads v1.8 – v1.14 · writes v1.13 (modified files keep their source version) |
| Final Cut Pro | 10.4+ through 12.x · flat .fcpxml and .fcpxmld bundles (sidecars preserved) |
| Python | 3.10, 3.11, 3.12 |
| MCP protocol | 1.0 |
mcp SDK | 1.3.0 through 2.x — both the decorator API and the add_request_handler API that replaced it. CI tests the declared floor and 2.x on every push |
| Export targets | |
| → DaVinci Resolve | FCPXML v1.9 |
| → Premiere Pro / Avid | FCP7 XMEML v5 |
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.
| Layer | Protection |
|---|---|
| File I/O | Path traversal blocked, null bytes rejected, symlinks resolved, 100 MB size limit |
| Output sandbox | All generation, write, export, beat sync, subtitle, and reformat handlers enforce _validate_output_path(anchor_dir=...) — restricts writes to descendants of the source file's directory, blocking LLM-generated path escapes |
| Subprocess bounds | _ensure_video_asset() bounds-checks duration (0 < d ≤ 3600s), fps (1–240), width/height (even, ≤ 7680×4320) before subprocess.run() — blocks inf/NaN, negative values, odd dimensions, string injection, and oversized resolutions that could hang or exhaust ffmpeg |
| Speed validation | handle_change_speed validates speed is positive and ≤100 before any math — prevents ZeroDivisionError crash and nonsensical results |
| Sandbox roots | Opt-in and off by default — an editor's projects live wherever the user keeps them. FCP_PROJECTS_DIRS (several roots, PATH-separated) turns on read confinement: _validate_filepath rejects any path that is neither inside a root as given nor resolves into one. FCP_PROJECTS_DIR is unchanged from 0.15.0 — it confines listing only and never restricts reads, so an existing install is unaffected by upgrading. Traversal still normalises before the check (root/../etc is judged as /etc), and the extension whitelist still runs on the resolved suffix, so innocent.fcpxml → /etc/passwd is rejected either way |
| Directory listing | Confined to FCP_PROJECTS_DIR when set, plus any FCP_PROJECTS_DIRS roots — unchanged behaviour. find_fcpxml_files globs *.fcpxml / *.fcpxmld under the requested directory and every path that is subsequently opened goes back through _validate_filepath; with neither variable set, the caller may name any directory to list |
| Resource caps | The list_projects walk stops at FCP_MAX_DISCOVERY_FILES (10,000) rather than collecting and slicing, so pointing it at / cannot walk the filesystem; marker batches and imports stop at FCP_MAX_BATCH_MARKERS (10,000); inline transcript text stops at FCP_MAX_TRANSCRIPT_CHARS (1 MB), cut on a line boundary so a timestamp is never split. Every cap returns an explicit ⚠️ TRUNCATED notice naming what was dropped — a partial result is never presented as a complete one |
| XML parsing | defusedxml with explicit forbid_entities/external=True blocks XXE, billion laughs, entity expansion, remote DTD attacks at all 4 entry points (parser, writer, exporter, rough cut) — minidom pretty-print path also hardened via defusedxml.minidom. Ruff S314/S320 rules enforce safe parsing in CI |
| JSON depth limit | Iterative BFS depth checker rejects payloads nested beyond 50 levels — immune to RecursionError even at ~1000 nesting |
| Symlink resolution | _validate_filepath calls Path.resolve() before the extension whitelist runs, so a symlink named innocent.fcpxml that points at /etc/passwd is rejected on its resolved suffix — a symlink cannot smuggle a disallowed target past the file gate |
| Marker strings | Sanitized via _sanitize_xml_value() — null bytes, control chars stripped before write |
| Role values | Stripped of control characters before XML attribute assignment |
| Resource URI parsing | file:// and preview:// URIs have their scheme removed with str.removeprefix() (leading match only, so a path containing the scheme string is not mangled) and are then urllib.parse.unquote()d before validation — so percent-encoded traversal, percent-encoded null bytes, and ordinary spaces in filenames are all decoded first and then run through the same _validate_filepath gate as every other path |
| Output suffixes | Path separators and special characters stripped — no traversal via suffix injection |
| Marker types | completed attribute strict-matched ('0'/'1' only) — rejects "true", "1 OR 1=1", whitespace-padded values |
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.
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.
| Format | Example | Parts | Result |
|---|---|---|---|
| Minutes:Seconds | 1:30 | 2 | 90.0s |
| H:MM:SS | 1:05:30 | 3 | 3930.0s |
| HH:MM:SS.ms | 00:02:15.500 | 3 | 135.5s |
| SMPTE (HH:MM:SS:FF) | 01:00:10:12 | 4 | 3610.5s @ 24fps |
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.
None — the marker is silently skipped, not placed incorrectlyfloat() on the seconds component ("15.500" → 15.5)12/24 = 0.5), not rounded to the nearest frame boundary. The resulting float is converted to FCPXML's rational TimeValue downstream, preserving precisionBefore 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.
| Principle | Implementation |
|---|---|
| Rational time, never floats | All durations are fractions (600/2400s) matching FCPXML's native format — zero rounding errors across trim, split, speed |
| Non-destructive by default | Modified files get _modified, _chapters suffixes. Originals are never overwritten |
| Single source of truth | MarkerType enum owns serialization: from_string() for input, from_xml_element() for parsing, xml_attrs for writing. INCOMPLETE is canonical; TODO is a backward-compat alias (same object) |
| Security-first | 13-layer defense-in-depth across all 62 handlers — see Security for the full matrix |
| Dispatch, not conditionals | TOOL_HANDLERS dict maps names → async handlers. No 1000-line if/elif |
| Guide | What's Inside |
|---|---|
| WORKFLOWS.md | 8 production recipes — QC pipelines, beat-synced assembly, cross-NLE handoffs, documentary A/B roll |
| MCP_ECOSYSTEM.md | How this server composes with GitNexus, filesystem, and memory MCP servers |
| CHANGELOG.md | Full version history from v0.1.0 to present |
1808 tests across 68 suites — 1801 pass and 7 skip locally on v0.25.1 (CI also runs mcp 2.x and 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_duration renders a one-second timeline and asserts the probe reads something other than two seconds.test_the_waveform_is_actually_drawn renders 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: showwavespic draws 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_fails proves an autopush failure never costs you a file that is already on disk.test_a_touched_source_drops_its_rows re-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.py asserts server.__version__ matches pyproject.toml; the two had disagreed since v0.17.0 without anything noticing.mcp (1.3.0+, including 2.x), defusedxmldetect_media_silence, remove_media_silence)[scenes] extra (optional) — adds PySceneDetect for scenes detection 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) — adds mlx-vlm + numpy so find can caption shots offline; without it find answers from transcript and metadata and says vision is unavailable.[intelligence] extra (optional) — adds librosa for detect_beats; everything else works without it. Install via uvx --from "fcp-mcp-server[intelligence]" fcp-mcp-server or pip install "fcp-mcp-server[intelligence]" (from source: pip install -e '.[intelligence]').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:
The full ecosystem analysis and the dual-mode architecture plan live in docs/CAPABILITY-AUDIT-2026-06.md.
.fcpxmld bundle support with object-tracking/Cinematic sidecar preservation — v0.8.0relink_media) — v0.8.0detect_media_silence) — v0.10.0remove_media_silence cuts real silence with ripple — v0.11.0detect_beats (librosa) chains into beat markers + snap-to-beats — v0.12.0inspect/diagnose/edit/mark/generate/transcript/deliver replace the 62 flat tools as the advertised default, cutting the schema footprint 84.7%; FCP_MCP_LEGACY_TOOLS=1 keeps the 62 available — v0.14.0preview:// HTML timeline render — see a cut without opening Final Cut Pro — v0.14.0final-cut-pro Claude Code skill — workflow order + FCPXML gotchas — v0.14.0preview (see the edit from the source media), watch (notice the Cmd-E export), import_edl_json, bridge detection, autopush — v0.17.0scenes shot-boundary detection, transcript_pack, opt-in ElevenLabs Scribe diarization — v0.18.0history/undo, review gate on deliver, organize bulk edits + organize_auto, find tiered shot search with offline MLX captions, diversity constraint — v0.19.0xfade, connected video lanes composite over the spine — v0.20.0find vision tier caps the frame it sends the VLM, so a 4K library no longer pays 4× the tokens (#21, @jardelapp) — v0.21.0initialize + tools/list before a release ships; the MCP registry check reads isLatest and is compiled in CI — v0.21.1–0.22.1mc-clip and caption connected 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| Issue | Impact | Workaround |
|---|---|---|
| Still images crash FCP | PNG/JPEG assets referenced directly in FCPXML crash Final Cut Pro on import (addAssetClip null pointer). Confirmed across multiple format configurations, dimension matching, and element types. | Convert stills to short MOVs before referencing: ffmpeg -loop 1 -i image.png -c:v libx264 -t 2 -pix_fmt yuv420p -r 24 output.mov. This is an FCP limitation, not an FCPXML spec issue. |
| A multicam edit reads as an empty project (0.22.1 and earlier) | FCP exports connected-clip multicam edits as a spine of gaps with mc-clip/caption in lanes; the parser dropped both, so list_clips was empty, validate_timeline scored 100 % and the media tools opened nothing. | Fixed in v0.23.0 — upgrade. On an old install, list_connected_clips still shows nothing for these; there is no in-version workaround. |
| "Connection closed" on a PyPI install of 0.19.2–0.21.0 | tools/ was missing from the published package, so the server raised ModuleNotFoundError at import, before it could answer initialize. The MCP client reports only "Connection closed". A git checkout never showed it. | Fixed in v0.21.1 — upgrade (uvx picks it up on the next run; pip install -U fcp-mcp-server otherwise). The publish workflow now installs the built wheel into a clean venv and runs initialize + tools/list before anything ships. |
| Issue | Impact | Workaround |
|---|---|---|
| Non-standard timebases | FCP rejects time values with denominators outside its standard set (e.g. 100800/57600s). Cross-denominator arithmetic previously produced these. | Fixed in v0.5.29 — TimeValue arithmetic now uses LCM, and speed changes snap to frame boundaries in 2400-tick timebase. |
| Malformed frameDuration crash | A frameDuration with zero or negative denominator (e.g. "0/0s") in the writer's _detect_fps would silently produce 0.0 fps, causing downstream ZeroDivisionError in speed/trim operations. The parser already validated this correctly. | Fixed in v0.6.23 — writer now validates both numerator and denominator, falling back to 30.0 fps. |
| Duplicate clip names corrupt edits | When multiple spine clips share the same name (e.g. Interview_A ×4), operations using the name-indexed dict silently target the wrong clip (last-indexed instead of first). Affected: delete_clip, add_marker_at_timeline, trim_clip, change_speed, split_clip, add_transition, reorder_clips. | Fixed in v0.6.37–0.6.39 — all methods now resolve clips via _resolve_clip() which walks the spine directly, returning the first match. |
You do not need a GitHub account, and you do not need to be a programmer.
If the client only says "Connection closed", the server died before it could answer. Run it by hand to see the real error:
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.
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.
Built by @DareDev256 — former music video director, now building AI tools for creators.
MIT — see LICENSE.
mcp-name: io.github.DareDev256/fcpxml-mcp-server