motion-mcp-server
# motion-mcp-server
An [MCP](https://modelcontextprotocol.io) server for reading and editing
Apple Motion (`.motn` / `.moti`) template files.
## Scope — read this first
**This server edits static template XML on disk. It does not, and cannot,
drive a live Motion.app session.**
Final Cut Pro exposes a rich AppleScript/Apple Events dictionary, which is
what makes a "live control" MCP server (see
[fcp-mcp-server](https://github.com/DareDev256/fcp-mcp-server), the project
that inspired this one's architecture) possible for FCP. Apple Motion's own
scriptability is limited to three verbs: `activate`, `open`, `quit`. There is
no dictionary for pushing parameter values, scrubbing the playhead, or
reading back rendered frames from a running Motion document.
So this server's actual capability is: parse a `.motn`/`.moti` file (both are
the same `ozml` XML format Motion.app itself writes), let you inspect and
mutate its layer/parameter/keyframe/publish-settings tree, and save the
result to a new file. Getting the result on screen means opening the written
file in Motion.app yourself — `motion_open_in_motion` does that handoff — or
handing a published/rendered template to Final Cut Pro, which does have the
rich Apple Events surface (see the sibling `fcpxml`/`commandpost` MCP
servers for that half of a pipeline).
## What it can do
Five tool groups, 17 tools:
- **inspect** — `motion_open`, `motion_list_layers`, `motion_list_text_layers`,
`motion_get_parameter`, `motion_list_keyframes`, `motion_list_rig`
- **animate** — `motion_set_text`, `motion_set_parameter_value`,
`motion_set_color`, `motion_create_keyframe_curve`, `motion_add_keyframe`
- **structural** — `motion_clone_layer` (duplicate an existing `<layer>`
subtree in place — see "Adding new layers" below)
- **rig** — `motion_publish_parameter`, `motion_unpublish_parameter`
(Final Cut Pro's Inspector reads these published/rig parameters when the
template is used as an FCP title/generator/effect)
- **deliver** — `motion_save_as`, `motion_open_in_motion`, `motion_validate`
### Authoring motion from scratch
`motion_create_keyframe_curve` seeds a brand-new `<curve>` on a leaf
parameter that has never been keyframed in Motion — actual dynamic/
procedural animation authoring, not just editing curves Motion already
created. This was initially considered too risky to guess at (a curve's
`type` attribute is data-type-specific, and guessing wrong produces a file
Motion can't open), so it's grounded in a structural scan of ~4,700 real
Motion documents on the machine this was built on: every keyframed leaf
parameter observed — Position X/Y/Z, Scale, Angle/Rotation, Opacity, RGB(A)
color channels, behavior `Amount` parameters — used the same universal
scalar curve (`type="1"`), because Motion animates a compound property
(Position, Color, ...) by keyframing each numeric child parameter
independently rather than through one multi-component curve. A second curve
type (`"0"`) does exist on some non-numeric/enum-like parameters (Random
Seed, Blend Mode, Interpolation) but never carried an actual keypoint in
that scan, so this tool deliberately doesn't attempt to synthesize it —
there's no real-file evidence for what a keyed `type="0"` curve should look
like. Verified against both the synthetic fixture and an ephemeral,
never-committed round-trip against a real local `.motn` file.
Once a parameter has a curve — whether Motion created it or
`motion_create_keyframe_curve` did — `motion_add_keyframe` appends further
keypoints to it, in time order.
### Adding new layers
`motion_clone_layer` duplicates an existing `<layer>` subtree (including any
nested child layers, e.g. a Group's children) and inserts the copy as the
next sibling of the original. This is the supported way to add new
structure to a template — it's a *clone*, not a from-scratch synthesizer,
and that's a deliberate, evidence-based choice, not a missing feature.
A structural scan of real Motion documents (same discipline as the curve
work above) found two reasons hand-authoring a brand-new layer from scratch
is materially riskier than editing an existing one: a Text-layer scenenode
tree is far deeper than any parameter this server otherwise touches
(paragraph/scroll/crawl margins, per-run styles, cross-referencing `<host>`
links), and the numeric `factoryID` for the same semantic kind (e.g.
"Text") is **not stable across documents** — it varied across every
personal `.motn`/`.moti` file scanned. Hardcoding a factory id would
silently produce a file that opens fine in the document it was copied from
and fails to open, or resolves to the wrong factory, in another.
Cloning sidesteps both problems: the subtree being duplicated is already
valid, real, Motion-authored XML from the *same* document, so nothing about
its factory graph needs to be guessed. The only thing `motion_clone_layer`
actually computes is id remapping — every `layer`/`scenenode` id inside the
cloned subtree is reassigned to a freshly allocated, document-unique value
(scanned across the *entire* document, not just the subtree, so it can
never collide with anything already present), and any `<host hostID="...">`
link that points *inside* the cloned subtree is rewritten to match. A
`<host>` link pointing *outside* the clone — an intentional cross-reference
to an unrelated part of the document — is left untouched, since remapping
it would silently break that reference. Verified against the synthetic
fixture (including a nested-group case exercising both the internal- and
external-`<host>` fixup paths) and two ephemeral, never-committed
round-trips against real local `.motn` files — one a flat layer, one a
layer with a nested child.
## The `ozml` schema
Ground truth for the tag shapes this server relies on was read directly from
real Motion.app output on the machine this was built on (`Motion
Projects/Autosave Vault/*.motn`, `Motion Templates.localized/*.moti`), not
assumed. See the module docstring in `motion/parser.py` for the annotated
shape, including two nesting traps that are easy to get wrong by guessing:
- Top-level layers are direct children of `<scene>`, not `<timeline>`
(`<timeline>` in this schema is only a UI display-state block).
- Text face/outline/glow color and font live under a `<style>` element
that's a *sibling* of a scenenode's `<parameter>` tree, not nested inside
it.
No personal `.motn`/`.moti` file is committed to this repository. The test
suite runs against `fixtures/sample.motn`, a small hand-written synthetic
document that reproduces the same tag shapes.
## Install
```bash
python3 -m venv .venv
.venv/bin/pip install -e ".[dev]"
```
## Run the tests
```bash
.venv/bin/python3 -m pytest tests/ -v
```
## Use it from Claude
Add to your MCP client config (Claude Code's `.mcp.json`, or the Claude
desktop app's `claude_desktop_config.json`):
```json
{
"mcpServers": {
"motion": {
"command": "/absolute/path/to/motion-mcp-server/.venv/bin/python3",
"args": ["/absolute/path/to/motion-mcp-server/server.py"]
}
}
}
```
Typical workflow: `motion_open` a template → inspect with the `inspect`
tools → mutate with `animate`/`structural`/`rig` tools → `motion_save_as` a
new file (never overwrites the source implicitly) → `motion_validate` to
confirm the write round-trips → `motion_open_in_motion` for visual QC.
## License
MIT — see `LICENSE`. Architecture informed by DareDev256's MIT-licensed
[fcp-mcp-server](https://github.com/DareDev256/fcp-mcp-server); this is an
independent implementation for a different file format, not a fork.
TDQS
Scored across 16 tools
Each tool targets a distinct resource/action. The only potentially overlapping pairs (set_parameter_value vs set_color, create_keyframe_curve vs add_keyframe) are clearly separated by explicit use-case descriptions, leaving no ambiguity.
All tools share a motion_ prefix and use verb-first snake_case. Most follow verb_noun, but a few like motion_open, motion_save_as, and motion_validate deviate slightly from the noun-object pattern, creating minor inconsistency.
At 16 tools, the set is just over the ideal 3-15 range. The count is reasonable for a comprehensive Motion editing server, and each tool has a legitimate role, so it feels only slightly heavy.
The server covers the full editing lifecycle: open, list, inspect, modify, animate, publish, save, and validate. Minor gaps exist (e.g., no delete keyframe or enumerate all parameters), but core workflows are fully supported.