AllCrew Figma Workspace MCP Server
Provides a local-first Figma development plugin and MCP workspace that exposes Figma variables, components, prototype edges, assets, and live file state to agents; supports controlled writes to the open Figma file and exports design tokens and project deliverables.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@AllCrew Figma Workspace MCP Serverexport the design tokens from the Figma file I have open"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
AllCrew Figma Workspace
A local-first Figma development plugin and MCP workspace for Claude Code, Codex, Cursor, design tokens, project export, and controlled agent writes.
Download the latest release · Install · Security model · Report an issue
Why
Design handoff is more than pixels. Agents need the variables behind values, component behavior, prototype edges, assets, and the live state of the file a designer has open. AllCrew exposes that context through a user-run loopback bridge, then keeps reads and writes behind separate switches visible in Figma.
It also exports usable deliverables: mode-aware tokens, React, Django, Tauri, native color resources, documentation, and declarative custom plugin screens.
Core extraction and generation run inside Figma. Optional Delivery and Agent Listener connections go only to endpoints the user configures; AllCrew operates no hosted relay and collects no telemetry.
Related MCP server: figma-mcp
AllCrew vs. the official Figma MCP server
The tools overlap, but they optimize for different workflows. Figma recommends its remote MCP server for broad official access. AllCrew is a development-plugin workspace for explicit, open-file automation and repository artifacts.
Capability | Official Figma MCP | AllCrew Figma Workspace |
Structured components, variables, layout context | Yes | Yes |
Generate code from selected frames | Yes | Yes, plus packaged project exports |
Write native Figma content | Remote MCP beta | Open plugin session with Allow writes |
Open-file, observable operation log | Not the primary model | Yes |
Design-token / Django / React / Tauri packages | Not its focus | Yes |
Custom declarative plugin screens | No | Yes, through AllCrew SDK modules |
Hosted service required | Remote is preferred; desktop also exists | No AllCrew service; loopback bridge |
Distribution | Official remote/desktop MCP | GitHub development-plugin release |
Official capability references: Figma MCP guide and Codex setup.
Install from a GitHub Release
Download
allcrew-figma-workspace-v<version>.zipandSHA256SUMSfrom the latest release.Verify the archive:
shasum -a 256 -c SHA256SUMSUnzip it without flattening the directory structure.
In Figma Desktop, open Menu → Plugins → Development → Import plugin from manifest….
Select
manifest.jsonin the extracted folder.Run AllCrew Figma Workspace from Plugins → Development.
Figma's browser app cannot import development plugins. Updating means downloading the new release, replacing the extracted directory, and restarting the plugin.
Build and install from source
npm ci
npm run buildImport this checkout's manifest.json. npm run dev rebuilds on change; npm run package
creates the release archive and refuses to package stale bundles.
Use
Open the Figma file that holds your design-system variables & collections.
Run the plugin. It scans automatically and shows a summary: collections, modes (themes) and variable counts.
Click ⚙Settings (top-right) to open the settings page and tune the output (see below). Choices persist per-user via
figma.clientStorageand re-apply on every run.Download package (.zip) — or grab any single file. Rescan after you edit variables.
Agent Listener quick start
The release archive includes the exact bridge and MCP front built with the plugin:
node tools/bridge.mjsOpen Agent Listener in the plugin, pair during the five-minute window, then enable Allow reads. Enable Allow writes only while an intended automation is running. For an MCP client:
node tools/mcp.mjs --install claude
# cursor, windsurf and vscode are also supportedDevelopment-plugin grants are session-only. Restarting the plugin closes both gates.
What you get
File | Contents |
| CSS custom properties. One self-contained block per theme. The merged single file. |
| The same blocks split one CSS Module per theme ( |
| Canonical W3C token tree — every mode under |
| Typed |
| (iOS & Android tokens on) One colorset per colour, in sRGB components, with a |
| (iOS & Android tokens on) The same, by Android resource qualifier. The night file is an override list: it carries only the colours that differ. |
| (iOS & Android tokens on) Constants for what a catalogue cannot hold (spacing, radii, the type scale) and for the colours too, for a build that themes in code. Values identical in every theme are declared once, above the per-theme blocks; a length gets |
| Auto-generated usage notes for that specific export. |
Settings
The Export Settings page (open it from the gear in the header) isolates the opinionated parts of the output so different consumers can keep their own preferences (the choices persist per-user):
Setting | Default | What it does |
Inline primitives | on | Resolves aliases that point at a primitive (raw, single-mode value) into the literal, and drops the primitive layer. Aliases between semantic tokens stay as |
Flatten all aliases | off | With Inline primitives on, also resolves the semantic→semantic |
Theme attribute |
| The attribute the theme blocks key off ( |
Theme collections table | empty | Maps any number of free-plan single-mode collections into exported multi-theme structures. Each row names the output structure, source collection and exported theme; repeat a structure name for Light, Dark, High contrast or additional brand themes. Conventional |
Per-theme | on | Whether to emit the per-theme module files alongside |
CSS Modules | on | Wrap module-file selectors in |
iOS & Android tokens | off | Also emit the asset catalogue, |
Include library variables | off | Read the variables of every enabled library, not just the local ones. It is a network read per token — 44 s on a file with 213 of them — and the only way to export a theme this file consumes rather than owns. Left off, the export's summary names the library collections it skipped, so a package with no theme in it says so instead of looking complete. |
tokens.css shape
Every mode becomes one self-contained block declaring all variables for
that theme. The default theme also occupies :root, so an un-themed page still
renders.
With Inline primitives on (the default), the raw scale is collapsed into the tokens that use it — no primitive variables in the output:
:root,
[data-theme-name="Light"] {
/* Tokens */
--colors-action-default: #002780;
--spacing-md: 8px;
}
/* add a "Dark" mode to the collection in Figma → a second full block appears */
[data-theme-name="Dark"] {
/* …all variables again, dark values… */
}Turn it off to keep the primitive layer and reference it via aliases instead (byte-identical to the AllCrew Figma Workspace board, modulo the attribute name):
:root,
[data-theme-name="Light"] {
/* Primitives */
--colors-b-800: #002780;
--sizes-1: 8px;
/* Tokens */
--colors-action-default: var(--colors-b-800);
--spacing-md: var(--sizes-1);
}A primitive used directly in code (not via an alias) can't be detected by the plugin — it will be inlined away. Turn Inline primitives off if you rely on the raw scale at call sites.
Switch theme by setting the attribute on any ancestor:
<html data-theme-name="Dark">Per-theme <theme>.module.css
The same theme blocks are also emitted one file per theme, named after the mode
(light.module.css, dark.module.css, … — slugified), wrapped in :global(…)
so they're valid in a CSS-Modules project (Next.js, etc.):
/* light.module.css */
:global(:root),
:global([data-theme-name="Light"]) {
--colors-action-default: var(--colors-b-800);
--spacing-md: var(--sizes-1);
}The default theme also lands on :global(:root). Concatenating every
*.module.css (or letting your bundler merge the ones you import) reproduces
tokens.css — that's the "merged single file". For runtime theme-switching,
keep every theme present (use tokens.css, or import all *.module.css) and
toggle data-theme-name.
Django app export
With the Django × Bootstrap preset the package is not a folder of templates waiting for someone to write a URLconf — it is a site you can run:
unzip export.zip -d site && cd site
pip install -r requirements.txt
DJANGO_DEBUG=1 python manage.py runserverWhat ships | Where it comes from |
| Settings → Target → Django project (off when you apply onto a project that already has its own) |
| the frames in scope; a component set becomes one partial, its variants CSS modifiers |
| layout/paint, design tokens, reaction states, prototype page transitions |
| Settings → i18n → Target languages |
design/pages.py is the generated page registry — the one file a re-export
overwrites. Everything else reads it: the prototype's start frame serves at /,
every other page at /<slug>/, and the navMap context processor reverses those
routes so the {{ navMap.nav_… }} hrefs the markup already emits resolve (and pick
up the active locale prefix). Configure Target languages and the routes move
under i18n_patterns with a no-JS language switcher in the shell.
Markup. A frame named Header/Nav/Footer/Sidebar becomes that element, a page root's
other direct children become <section>, a text layer whose name states a level (Heading 2,
Title/H3) becomes <h2>/<h3> — with exactly one <h1> per page — and a layer that only reacts
to a click becomes a real <button type="button"> instead of a div no keyboard can reach. Figma's
own auto-names (Rectangle 12, Vector) render as alt=""/aria-hidden, because a screen reader
announcing "Rectangle 12" is worse than silence. A layer the designer pinned with Fixed position
when scrolling gets position: fixed/sticky — that choice used to be read and then dropped.
What the CSS pass could only approximate or could not draw at all (squircle corners, NOISE /
TEXTURE / GLASS / SHADER effects and fills) is listed per node in export-report.json under
fidelityNotes, instead of disappearing into a console.
Motion. Reaction states (ON_HOVER/ON_PRESS/ON_CLICK → CHANGE_TO) are
diffed against the destination variant including its descendants, so a hover that
recolours an icon, re-pads a button or reveals a badge survives. Overlays render the
destination frame's own markup into a <dialog> with an open/close animation.
Page-to-page prototype transitions (DISSOLVE / PUSH / MOVE / SLIDE / SMART_ANIMATE)
become cross-document view transitions — Smart Animate assigns a shared
view-transition-name to the layers matched across both screens. Everything is
wrapped in prefers-reduced-motion guards.
The Smart Animate diff behind those states covers what Figma covers: position, size, rotation,
opacity, corner radii, strokes, the whole fill stack (gradients included, marked non-interpolable
where CSS cannot tween them), shadows and blurs, text size/weight/spacing/colour, auto-layout
padding and gap, visibility (with transition-behavior: allow-discrete) and blend mode — on the
changed layer itself as well as its descendants. A state that swaps a photo ships the
destination variant's image with the page assets and references it; when that image cannot be
exported the declaration is dropped rather than emitted as the url(<path-to-image>) placeholder
Figma hands out, which resolves to a 404 and blanks the element on hover.
Springs. A CUSTOM_SPRING reaction is solved from its own physics (ω₀ = √(k/m),
ζ = c / 2√(km), plus the designer's initialVelocity), and a named preset (GENTLE, BOUNCY, …)
from its bounce at the frequency whose period is the duration typed next to it. The emitted
linear() curve runs for the spring's real settle time, so a 150 ms bouncy press is a 150 ms-ish
bounce rather than the same half-second curve every spring used to collapse into.
Figma publishes no physics for its named presets, so those bounces come from a reverse-engineered
table — the one estimate left in the emitted motion, and export-report.json says so:
estimatedSpringPresets counts the curves that rest on it, and springPresetCalibration reports
what the file's own springs say the table should hold whenever Figma does supply the numbers. A
preset that arrives with real parameters is solved from them and never touches the table at all.
Adaptivity. Frames named Home / desktop + Home / mobile collapse into one
page: the widest frame is the DOM, the narrower ones become @media blocks, and
nodes that exist only in a narrow frame (the burger, a stacked CTA) are spliced in
hidden and revealed at their breakpoint. base.html carries the viewport meta, so
those queries apply on a real phone.
React export & Code Connect
The React target writes a component per Figma component set, and beside each one a
<Component>.figma.tsx — the Code Connect mapping that tells Figma this is the code for that
design component. From then on Dev Mode and Figma's official MCP answer with
<Button type="Primary" …> and a real import, instead of anonymous markup a developer has to
recognise. A root figma.config.json ships with them, so the repository is publishable as it
lands:
npx figma connect publish --token <token>Props are not guessed: the same variant/text/boolean definitions the component's own union types
come from become figma.enum / figma.string / figma.boolean, with the Figma property names
verbatim — Button-lable and Meduim are mapped as spelled, because that is what the file
contains. A component with no properties still gets a file with an empty props.
Two things worth knowing before troubleshooting a publish:
The library does not have to be published. A mapping addresses its component by node URL (
?node-id=819-95512— a dash, not the colon Figma stores), sopublishStatus: UNPUBLISHEDpublishes fine. Publishing only matters for the MCP route, whereadd_code_connect_maprefuses with "Published component not found".The token must be the right category.
file_code_connect:writeis not among the scopes a personal access token can carry — a PAT answers403 Invalid scope(s), and a plan token of the REST API category has those endpoints switched off. It takes a plan access token of the Figma CLI category (figma.com/developers/tokens → Figma CLI tab; organization admin, 2FA).
figma.fileKey is granted only to a private plugin on an Organization plan. Without it a node URL
cannot be built, so no mapping files are written at all and the export report names that as the
reason — a half-written mapping pointing at a placeholder key would be worse than none.
Automated delivery
Reading variable values over REST is Enterprise-only, so on Organization the plugin itself is the extractor — but everything around it is automated. The designer publishing the library is the trigger; the plugin ships the package; a small receiver does the git/npm/folder work.
Plugin side (Export Settings → Delivery):
Download receiver.mjs — the script ships inside the plugin, because a designer who installed it from Figma has no checkout of this repo. It is injected at build time from
server/receiver.mjs, so the copy they get can never be a different version.Receiver endpoint — where to POST (configurable; nothing is pinned to one host).
Shared secret — sent as
x-allcrew-channel-secret; must match the receiver. (This is the only secret in the plugin — git/npm credentials never leave the receiver.) Pair fills both fields from a receiver running on this machine; see Secrets below.Target —
folder/git(commit + push) /pr(branch +gh pr) /npm(publish), plus the non-secret route (repo / branch / path / package).Triggers — Deliver now (manual), Auto-deliver on change (re-scan every ~7s while the plugin is open and push on real change), Deliver on open.
Receiver side (server/receiver.mjs) — a dependency-free Node script you run on any
host; it executes the target using the host's own git / gh / npm auth:
ALLCREW_CHANNEL_FOLDER_BASE=/abs/path/for/folder/target \
node receiver.mjs # listens on :8787 (override with PORT)Secrets
Nothing is baked into the build and nothing is distributed. A receiver with no
ALLCREW_CHANNEL_SECRET mints its own into ~/.allcrew-channel/receiver-secret (0600) and opens a
five-minute pairing window; the plugin's Pair button collects it, and the first pair
closes the window. Ten designers means ten different secrets, none of which anyone had to
send anyone. Rotate by deleting the file and restarting.
Pairing is unauthenticated by design, bounded three ways: loopback only, five minutes from a start someone typed by hand, and closed by the first success. It grants nothing a local process could not get by reading the same file.
For a shared or remote host, set ALLCREW_CHANNEL_SECRET yourself — the receiver uses it and opens
no pairing window (--pair forces one), and you type the same value into the plugin.
Never bake one secret into the plugin for everyone: it is a key to every teammate's host
that cannot be rotated without a rebuild.
The plugin only ever HTTP-POSTs, so the receiver is portable: point the plugin's endpoint
at wherever it runs (localhost, a Tailscale host, CI). The contract is one POST of
{ files, target, route, options, meta }; the receiver verifies the secret, runs the
target, and returns { ok, detail }.
Fully hands-off (no designer either) would need the Variables REST API = Enterprise. The transform is already shareable, so that upgrade is a small step if you ever take it.
Deployment & Integration
Prerequisites
The receiver uses only Node.js built-ins — no npm install. The host needs:
Target | Required on the host |
| Node.js only |
|
|
|
|
|
|
Environment variables
Variable | Required | Default | Description |
| no | minted | Set it to manage the secret by hand; suppresses pairing |
| no |
| Where a minted secret is stored |
| no |
| HTTP port |
| for | — | Absolute base dir; |
| no |
| Scratch dir for git clones |
Option 1: developer machine (Tailscale)
The simplest setup — run the receiver locally, expose it to other Figma sessions via Tailscale:
# .env (keep out of git)
ALLCREW_CHANNEL_SECRET=some-random-string
ALLCREW_CHANNEL_FOLDER_BASE=/Users/you/projects/design-tokens/src
# run
ALLCREW_CHANNEL_SECRET=some-random-string \
ALLCREW_CHANNEL_FOLDER_BASE=/Users/you/projects/design-tokens/src \
node server/receiver.mjsPlugin endpoint: http://<tailscale-hostname>:8787
Option 2: long-running server with pm2
npm install -g pm2
# create ecosystem file — .cjs, since this repo is "type": "module"
# (do NOT commit — contains the secret)
cat > ecosystem.config.cjs <<'EOF'
module.exports = {
apps: [{
name: "allcrew-channel-receiver",
script: "server/receiver.mjs",
env: {
ALLCREW_CHANNEL_SECRET: "your-secret-here",
ALLCREW_CHANNEL_FOLDER_BASE: "/srv/tokens",
PORT: "8787"
}
}]
}
EOF
pm2 start ecosystem.config.cjs
pm2 save && pm2 startup # survive rebootsOption 3: systemd service
# /etc/systemd/system/allcrew-channel-receiver.service
[Unit]
Description=AllCrew Figma Workspace receiver
After=network.target
[Service]
ExecStart=/usr/bin/node /opt/allcrew-channel/server/receiver.mjs
Restart=on-failure
Environment=PORT=8787
Environment=ALLCREW_CHANNEL_SECRET=your-secret-here
Environment=ALLCREW_CHANNEL_FOLDER_BASE=/srv/tokens
WorkingDirectory=/opt/allcrew-channel
[Install]
WantedBy=multi-user.targetsystemctl enable --now allcrew-channel-receiverOption 4: Docker
FROM node:20-alpine
RUN apk add --no-cache git github-cli npm
WORKDIR /app
COPY server/receiver.mjs ./
EXPOSE 8787
CMD ["node", "receiver.mjs"]docker build -t allcrew-channel-receiver .
docker run -d \
-p 8787:8787 \
-e ALLCREW_CHANNEL_SECRET=your-secret \
-e ALLCREW_CHANNEL_FOLDER_BASE=/tokens \
-v /host/tokens:/tokens \
allcrew-channel-receiverOption 5: GitHub Actions (webhook-style)
The receiver itself is a push-side HTTP server, not a pull-side CI job. But you can use the git or pr target and let the receiver commit into the repo — Actions will then pick it up on push.
Designer triggers delivery in Figma
→ receiver commits tokens to `tokens` branch
→ Actions workflow runs on push to that branch
→ (optional) opens PR into main, runs style-dictionary, etc.)Example Actions workflow for the downstream step:
name: Sync design tokens
on:
push:
branches: [tokens]
paths: ["tokens/**"]
jobs:
sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npx style-dictionary build # or whatever your pipeline is
- run: |
git config user.name "tokens-bot"
git config user.email "tokens@allcrew-channel.local"
git add -A && git diff --cached --quiet || git commit -m "chore: rebuild tokens"
git pushCredentials
The receiver inherits credentials from the host environment — never from the plugin or the POST body.
Target | How to auth |
| SSH key in |
|
|
|
|
Health check
curl http://localhost:8787
# {"ok":true,"service":"allcrew-channel-tokens-receiver"}Troubleshooting
Symptom | Likely cause |
| Secret in plugin doesn't match |
| Host's git auth not set up for that remote |
|
|
| Env var not set |
No changes committed | Tokens were already up-to-date (not an error) |
Notes
Themes = the modes of your semantic (alias-bearing) collection. A raw primitives collection with a single placeholder mode (e.g.
Mode 1) is not treated as a theme; its values fold into every theme block.Token extraction is offline. Reading variables, building the package and writing every artifact happen entirely in-editor; fonts (Museo Sans, Geist Mono) are embedded in
ui.html.manifest.jsondoes declarenetworkAccess.allowedDomains: ["*"], and it has to: two optional features dial out and neither host can be known ahead of time — the delivery step POSTs the package to a receiver the user configures, and the agent listener talks to a loopback bridge on a port the designer chooses.allowedDomainstakes whole URLs and a port cannot be wildcarded, so a narrower list would be wrong for half of any team. Nothing in the extraction path uses it.Editors & plan: runs in both Figma Design and Dev Mode (
editorType: ["figma", "dev"],capabilities: ["inspect"]). It reads variables through the in-editor Plugin API, so no Enterprise / Variables REST API is required — it works on any plan, including Organization. Dev Mode itself is gated by Figma to a Dev or Full seat (no manifest setting bypasses that); anyone with a normal editor seat can still run it in Design mode with no extra cost.Selector: defaults to
[data-theme-name="…"](per spec) but is configurable in Export settings — set it todata-themeto match the AllCrew Figma Workspace board.tokens.jsonis always the full, un-inlined tree regardless of the Inline primitives setting, so it stays lossless for diffing / re-import.
Agent Listener security
The bridge listens on
127.0.0.1by default. Do not expose it on a public interface.Read and write access are separate switches, and both start off.
Figma development plugins do not expose a stable file key. Agent grants therefore last only for the current plugin session and are never restored by file name.
Treat
~/.allcrew-channel/agent-secret, receiver secrets, and Figma tokens as credentials.Stop the listener before opening an untrusted file. Enable writes only while an intended automation is running.
See SECURITY.md for the trust model and private vulnerability reporting.
Support
Data handling: PRIVACY.md.
Bugs and feature requests: https://github.com/mal4i6ka/allcrew-figma-plugin/issues
Security reports: https://github.com/mal4i6ka/allcrew-figma-plugin/security/advisories/new
License
MIT © 2026 Alexander Lugachev.
Maintenance
The transform in code.js is a dependency-free port of the AllCrew Figma Workspace board's
src/lib/figma.ts (variablesToW3CMultiMode) + src/lib/design-system/tokens-transform.ts.
With Inline primitives off and the theme attribute set to data-theme, the
output is byte-identical to the board. The export options layer on top of that core
transform; keep the core in sync with the board if the token format changes.
Files:
manifest.json plugin manifest
dist/code.js sandbox and Codegen runtime
dist/ui.html plugin panel
agent/ local bridge, MCP front, and channel documentation
server/ optional Delivery receiverThis server cannot be deployed
Maintenance
Related MCP Connectors
Serves your design system and coding standards to coding agents, so they stop guessing.
Live React design-system APIs, patterns, and code validation so AI agents build real UI, not slop.
Connect AI coding agents to Anima Playground, Figma, and your design system.
Access and maintain design system docs, tokens, components, skills, and contexts across any project.
Related MCP Servers
- AlicenseNot gradedqualityBmaintenanceEnables AI coding agents to read and write a user's Figma file through the Figma Plugin API, offline and privately, without API tokens or rate limits.13 npmMIT
- AlicenseNot gradedqualityAmaintenanceEnables AI coding assistants to draw UI directly on a Figma Desktop canvas and read existing designs back as structured JSON, tokens, CSS, and screenshots, all over a localhost bridge without external API keys.184 npmMIT
- AlicenseNot gradedqualityAmaintenanceEnables AI agents to inspect Figma selections, navigate pages, render previews, generate starter code, and submit user-approved canvas edits through a local bridge.3 npmMIT
- AlicenseNot gradedqualityCmaintenanceEnables a locally run agent or script to read and write a real Figma layer tree through a local bridge — inspecting node structure and variables, exporting screenshots into the model's context, and performing batched create/update/move/rename/delete operations that collapse into a single undo step. All traffic stays on localhost between the bridge and the Figma plugin, with design-token auditing, binding, and creation included.2 npmMIT