anni MCP server
Commit Oct 31, 2026A CSS animation editor that lets coding agents tune keyframes on a timeline over a real page and write changes back to the project files.
Also known as mwallbrecher/anni, anni-mcp
- Stars
- 1
- Tools
- —
- Downloads, 30 days
- —
- Latest version
- —
- Last commit
- Oct 31, 2026
- License
- —
What is anni MCP?
anni is a CSS animation editor that lets coding agents tune keyframes on a timeline over a real page and write changes back to the project files.
Summary generated from the project's README
About anni MCP server
Developers working with CSS animations can use anni to visually tune keyframes and styles directly on a live page. Instead of manually editing code, an AI agent uses this server to open existing animations in a browser-based timeline editor. Once you adjust timing, colors, or transforms in the UI and hit save, the server writes those specific changes back to your local project files.
The system operates via a local sidecar process or a remote HTTP endpoint. When using the local sidecar, the agent passes file paths, and the tool handles the file uploads and writes. This approach keeps large file contents out of the model context, making editing sessions efficient. The server includes tools like open_animation to load styles, list_animations to find keyframes, and get_animation_state to retrieve current progress. Beyond keyframes, the present tool allows agents to create custom control panels for any code values, linking UI sliders or color pickers directly to CSS properties or source code snippets.
What you can do with it
- 1Visual keyframe editingAn agent can open a specific CSS animation from a stylesheet to allow manual tuning on a timeline.
- 2Live property manipulationA developer can instruct an agent to create a UI panel for adjusting specific CSS declarations like border radius or colors.
- 3Synchronizing UI changes to codeYou can use the tool to ensure that visual adjustments made in the browser are written back to the correct lines in your source files.
Written from the project's README. Every use case is checked against a quote from it.
How to install anni MCP
Formats verified 10 Nov 2026Add to CursorAdd to GooseInstall anni MCP in Claude Code
$ claude mcp add anni -- npx -y anni-mcpInstall anni MCP in Cursor
~/.cursor/mcp.json
{
"mcpServers": {
"anni": {
"command": "npx",
"args": [
"-y",
"anni-mcp"
]
}
}
}Install anni MCP in VS Code
.vscode/mcp.json
{
"servers": {
"anni": {
"type": "stdio",
"command": "npx",
"args": [
"-y",
"anni-mcp"
]
}
}
}Install anni MCP in VS Code CLI
$ code --add-mcp '{"name":"anni","type":"stdio","command":"npx","args":["-y","anni-mcp"]}'Install anni MCP in Claude Desktop
claude_desktop_config.json
{
"mcpServers": {
"anni": {
"command": "npx",
"args": [
"-y",
"anni-mcp"
]
}
}
}Install anni MCP in Codex
$ codex mcp add anni -- npx -y anni-mcpInstall anni MCP in Codex config.toml
~/.codex/config.toml
[mcp_servers.anni] command = "npx" args = ["-y", "anni-mcp"]
Install anni MCP in Gemini CLI
$ gemini mcp add -s user anni npx -- -y anni-mcpInstall anni MCP in Gemini settings.json
~/.gemini/settings.json
{
"mcpServers": {
"anni": {
"command": "npx",
"args": [
"-y",
"anni-mcp"
]
}
}
}README
View on GitHubanni
A small timeline editor for CSS @keyframes, driven by an agent over MCP. The agent opens an
animation from your project, you tune timing and values in the browser, hit save – and the
CSS file is updated in place. Not a standalone motion tool: it edits the snippets and
animations that already live in your project, on your own page.
Setup
The editor runs on https://anni.run. Connect your agent through the sidecar
(anni-mcp on npm, a tiny local MCP process):
claude mcp add anni -- npx -y anni-mcp
# Codex: ~/.codex/config.toml
[mcp_servers.anni]
command = "npx"
args = ["-y", "anni-mcp"]
Then, in Claude Code: “open the bounce animation from styles.css in anni”.
Tools not loaded yet? An MCP server added during a session only loads in the next one. Until then, run the same sidecar as a command – same upload, same editor on anni.run, saves written back to the file until Ctrl+C:
npx -y anni-mcp open styles.css [animation]
Don't use the anni command for this: that is local mode, a separate editor on 127.0.0.1.
Where updates land. The editor UI is served by anni.run and ships with the Worker (npm run deploy): editor
fixes are live for everyone as soon as it deploys – no npm release needed. An npm release only updates the sidecar:
file upload, write-back, and the summaries it prints itself (open_animation's animation list, list_animations),
which parse CSS locally with src/css.js.
To run the newest sidecar code from this repository instead of the last npm release: npx -y github:mwallbrecher/anni.
From a checkout: claude mcp add anni -- node /path/to/anni/src/sidecar.js.
The model only passes file paths; the sidecar reads and uploads the files to anni.run itself (plus the
HTML page and the stylesheets/scripts it links), opens the browser tab, and when you hit Save it writes the
edited file back to disk and hands the model a short diff. File contents never travel through the model's
context, so a whole editing session costs a few hundred tokens, and every drag in between costs none.
Tools: open_animation(file, animation?, preview?, extra_files?, session?), wait_for_save(session?),
get_animation_state(session?), list_animations(file), plus present / wait_for_changes (below) and
snapshot_page(url), which fetches a route of a running app – dev server, staging, anything – into static files
(page, stylesheets, fonts, images; scripts stripped) so the preview is the real page. ANNI_URL points the sidecar at a
self-hosted instance. Each wait ends after at most 50 s and every call to anni.run is capped at 55 s, so a call never
outlives an MCP client's 60 s limit – a slow answer comes back as "no save yet" and the agent waits again.
Zero-install – the remote MCP endpoint (Streamable HTTP), for clients that can't run a local process:
claude mcp add --transport http anni https://anni.run/mcp
Here the agent does the file I/O itself: it passes file contents to open_animation(files, file, …) and
writes back what wait_for_save(session) returns. Same editor, same sessions – just more tokens.
Any value, not just keyframes: present
The agent can turn any values from code into a panel. It describes controls; anni renders them next to the page; each control carries two wires:
{ "id": "radius", "label": "Radius", "type": "length", "value": "8px", "min": 0, "max": 32,
"preview": { "css": { "selector": ".primary-button", "prop": "border-radius" } },
"source": { "file": "src/app/globals.css",
"find": " border-radius: 8px;\n background: var(--ink);",
"replace": " border-radius: {{value}};\n background: var(--ink);" } }
- preview wire – how the value shows on the uploaded page, applied live by anni while you drag:
css: {selector, prop}(a declaration),var: {name, scope?}(a custom property) ortext: {selector}. - source wire – where the value lives in code:
find(a snippet that occurs exactly once) with an optionalreplacetemplate, orrange: [start, end]with atemplate. Omit it and the change is only reported.
Types: number, length, color, enum, boolean, text. The preview page is typically a rendered route of the
running app (fetch its HTML and CSS to files) – so this works for React, Vue, Rails, anything that renders to the browser;
the code wires can point at a .tsx, a theme file, Tailwind config, wherever the literal is.
present(schema, files?, preview?) → link · wait_for_changes(session?) → on Save the sidecar applies every change
with a source wire to your files and returns the diffs; changes without one are listed for the agent. Over the remote
endpoint the agent receives {id, from, to, source} and applies them itself.
Sessions are unguessable ids; there are no accounts. A session – the files you opened included – is wiped
7 days after its last activity. Uploads are capped (1.5 MB per file, 24 MB per session, 300 files). The HTTP API
the sidecar uses is plain JSON: POST /api/sessions (auth: true returns an owner token), then
GET /api/sessions/<id>/wait?since=&timeout= and GET /api/sessions/<id>/state with the token as x-anni-owner.
Who sees a session. The browser that opens the agent's link becomes the session's owner: the sidecar opens it
with an owner token in the #fragment (never sent to a server), the editor keeps it as a cookie and drops it from the
address bar, and the agent sends the same token on its own calls. The owner's name comes from git config user.name.
From the lock button in the top bar the owner picks who else may open the plain link:
- Request (default for new sessions) – others see a lock screen and can ask for access; the owner allows or declines from the top bar and can remove people again.
- Public – anyone with the link watches.
- Locked – no new people; nobody can ask.
Only the owner edits, from one tab at a time (another tab of theirs can take over). Everybody else watches live and
can Edit a copy: a new session with the same files (as last saved) that they own. A copy remembers the revision it
started from; Export changes in the copy writes what changed since then – timing, moved keyframes, every value as
before → after, the resulting CSS, a diff – as an .anni.md brief for a coding agent. It is downloaded and sent to
the original session, whose owner sees it in the top bar (view, copy, download). The owner's agent can read these
proposals with get_proposals and apply them to code that may have moved on ("apply Lena's changes but keep my shaking
animation"): the brief tells it to change only what is listed and to ask when a before value no longer matches. The token belongs to the browser – another
browser or a private window is a guest until the owner lets it in. Sessions from before access control, or from a
sidecar older than this, have no owner yet: the first browser that opens them claims them.
Private instance: wrangler secret put ANNI_TOKEN --env production makes /mcp and the session API require
Authorization: Bearer <token>. The sidecar sends it from the ANNI_TOKEN environment variable; for the remote
endpoint use claude mcp add --transport http anni https://… --header "Authorization: Bearer <token>".
Deploying your own
npm install
npm run dev:worker # http://localhost:8787 – sessions at /s/<id>/
npm run deploy # wrangler deploy --env production
Edit the production block in wrangler.jsonc:
ANNI_HOST– the host of the root site, e.g.anni.example.com.ANNI_URL_MODE–subdomaingives every session its own origin (<id>.anni.example.com), which is what isolates one user's preview page from another's. It needs the wildcard route in the config and a proxied wildcard DNS record (*.anni→192.0.2.1, orange cloud). Note that Cloudflare's free Universal SSL only covers one level (*.example.com): a second-level wildcard like*.anni.example.comneeds Advanced Certificate Manager, or run anni on its own domain (ANNI_HOST=anni.run, sessions at<id>.anni.run).pathmode (/s/<id>/on one origin) needs no wildcard and works onworkers.dev, but all sessions share an origin, so a malicious page uploaded to one session could script another. Fine for a trusted team, not for the open web.
Everything is one Durable Object per session (files, live model, open tabs, waiting agents); it fits the free plan.
Local mode (offline)
anni can also run entirely on your machine, without anni.run. It serves the editor bundled in the installed package
version – it does not get anni.run deploys – on http://127.0.0.1:4747 (ANNI_PORT to change it). Both entry
points print a "local mode" banner so it can't be mistaken for the hosted editor.
npm install
claude mcp add --scope user anni -- node /path/to/anni/src/mcp.js # or: npx -y -p anni-mcp anni-mcp-local
node src/cli.js examples/demo.css # without an agent (the `anni` command)
| Tool | Purpose |
|---|---|
open_animation(file, animation?, preview?) |
Opens a .css or .html (with <style>) in the editor. Reuses the open tab if there is one, otherwise opens a new one. |
get_animation_state() |
Returns the current animation as CSS – including unsaved edits. |
list_animations(file) |
Lists every @keyframes in a file with its timing. |
Editor
- Preview: for a
.cssfile anni looks for an.htmlnext to it that links it (or passpreview). If the animation doesn't run there (e.g. only on:hover), there's a demo box. - Pick (
V, a toggle, on by default): click an element right in the preview, like in Figma or Rive; turn it off for view mode, where the page reacts to clicks as usual. anni jumps to its animation. An element without one shows its own values in the Properties panel: the first value you change (or+ Keyframe at playhead) creates@keyframes <class>-animfor it, keyed at the playhead.+ Animationdoes the same with a name you choose, or applies an existing animation (class, id or path selectable; an existing rule is extended, never overwritten). - Editor | Animate (
Tab): two views over the same page, Rive-style. Animate is the timeline. Editor shows the picked element's winning CSS declarations – read from the browser's CSSOM, so they're the truth, in their authored shorthand form (padding: 8px 16px) – as controls, grouped Layout / Typography / Appearance / Transform, each labelled with the rule and file it comes from. Edits preview live; on Save anni rewrites that declaration in the uploaded stylesheet (a unique snippet is located around it, nothing else is touched) and the sidecar writes the file back. Declarations anni can't place are handed to the agent. Content edits the element's own text in place (written to the uploaded HTML; text that lives in components, e.g. a snapshot of a React app, goes to the agent as before → after). Align puts the text left / center / right (text-align), and the element itself within its parent, with whatever the parent's layout needs (align-selfin a flex column,justify-selfin a grid, auto margins otherwise); missing declarations are added at the end of the element's most specific rule. - Properties (Figma-style panel): always shows the common properties (
scale,rotate,translate,transform,opacity,filter, colors, sizes) with their value at the playhead – keyed, interpolated, or the element's base value. Changing a value keys it at the playhead automatically. The diamond after each value shows its state: filled = set in the keyframe at the playhead (row highlighted), outline = animated but interpolated here, grey = not animated; click it to key/unkey. Editors fit the value: sliders foropacity/scale, number + unit for lengths and angles, color picker + alpha, and a function list fortransform/filter(translateY,scale,rotate,blur, …). Click a property name to edit it as text; type any other property into the field at the bottom. - Timeline: one scene on the page's clock. Every animated element in the preview is a group (bar = delay + duration, hatched = further iterations) with its keyframes; expand it for one track per property. The playhead pauses and drives every CSS animation on the page – nothing keeps running on the side. Clicking a group selects its animation and element; animations from other files show greyed out. Drag diamonds to move them (Shift = 0.1 %), double-click a track for a new keyframe, click the ruler to scrub.
- Inspector: duration, delay, easing (with curve), iterations, direction, fill mode.
- Duration, two ways: in the Timing panel (Animation duration) keyframes keep their percentages and stretch with the duration (plain CSS). In the time box next to play they keep their times in ms, like a work area in a design tool – a longer duration adds time at the end (a held 100% keyframe keeps the final state), a shorter one trims it (keyframes past the new end move to 100%). The time box also takes a time to jump the playhead to.
- Delay is
animation-delay: the wait before the first iteration. It moves the animation later on the page timeline; repeats don't wait again, and a negative delay starts it partway in. - Zoom (like After Effects): trackpad pinch or
⌘/Ctrl+ wheel zooms whatever is under the cursor – the timeline around that point in time, or the preview around that spot.Shift+ wheel pans the timeline; a plain wheel pans the zoomed preview.0or the zoom readout resets. Arrow steps follow the zoom: one frame (1/60 s) when zoomed in, larger steps when zoomed out. - Copy & paste keyframes:
⌘C/⌘Xcopies the selected keyframe with all its values (as CSS, so it also pastes into your editor),⌘Vpastes at the playhead – into the keyframe there (values overwrite, the rest stays) or as a new one, also into another animation. CSS copied from elsewhere (50% { … }or plainprop: value;lines) pastes the same way. A shared0%, 100%keyframe is split so only the one under the playhead changes.⌥-drag a keyframe to duplicate it. - Keys:
Spaceplay/pause ·←/→playhead by 1 ms,Shift+←/→by 10 ms (a keyframe in between stops it) ·Alt+←/→jump to the previous/next keyframe of any element ·Home/End·0reset zoom ·Vpick / view mode ·Deleteremove keyframe ·⌘C/⌘X/⌘Vkeyframes ·Ctrl+Z/Ctrl+Shift+Z·Ctrl+Ssave. - If the agent changes the file, anni reloads it automatically (and asks first when you have unsaved edits).
Only what changed is written – the rest of the file stays untouched.
anni MCP questions
How do I add anni MCP to Claude Code?
Run claude mcp add anni -- npx -y anni-mcp in your terminal. Add --scope user to make it available in every project.
How do I add anni MCP to Cursor?
Open ~/.cursor/mcp.json and add the server under mcpServers, using the Cursor config shown in the install section above.
Does the editor work with existing project animations?
Yes, it is designed to edit snippets and animations that already live in your project on your own page rather than being a standalone tool.
How are file updates handled?
When you hit Save in the browser, the sidecar writes the edited file back to your disk and provides the model with a short diff.