Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ out/
# Keep the local debug helper itself in Git; only its generated captures stay local.
.debug-server/data/
.debug-server/server.log
.mcp-server/venv/
.mcp-server/__pycache__/

# ========================
# Editor & IDE
Expand Down
560 changes: 560 additions & 0 deletions .mcp-server/mcp_server.py

Large diffs are not rendered by default.

68 changes: 68 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# CLAUDE.md

Guidance for working in this repo. See [README.md](README.md) for user-facing
docs (features, usage, project structure).

## What this is

A Figma plugin (no build step, no bundler). `code.js` runs in the Figma
plugin sandbox and has access to the `figma` global; `ui.html` is the plugin
panel (iframe) and only ever talks to `code.js` via `postMessage`. There is
no server component except the optional local debug HTTP server in
`.debug-server/` (git-ignored, not part of the shipped plugin).

## Core invariant: don't break the fixed-pixel converter

The CSS/HTML/Dart extraction (`extractCss`, `generateHtmlTree`,
`generateDartForNode`, and everything they call) reproduces the Figma
selection at its exact captured pixel size, verified against a real Figma
render in the Preview tab. This is the thing users trust most about the
plugin — new features must be **additive**, never a rewrite of this path:

- New output modes (e.g. Responsive/fluid) are opt-in via an extra parameter
that defaults to today's exact behavior, so every existing call site
(including tests) stays byte-identical unless it explicitly opts in.
- Prefer post-processing a copy of the CSS object these functions already
return over threading new logic into them directly.

## Tests

```
node --test tests/code-regression.test.js
```

Loads `code.js` into a `vm` sandbox with a minimal fake `figma` global
(including fake variables/styles/component-instance APIs) and asserts
directly against the exported functions — no Figma installation needed. CI
runs this on every push/PR (`.github/workflows/test.yml`).

When adding a feature, extend this file's fixtures rather than creating a
new test file, and keep the existing assertions passing unmodified — that's
the proof the default path didn't regress.

## Conventions

- Plain functions, no classes. Small, single-purpose helpers (see the
`PRIMARY_AXIS_ALIGN_CSS`-style constant tables and `getBorderRadiusCss`-style
helpers near the top of `code.js`) composed by the top-level
`extractCss`/`generateDartForNode`/`generateHtmlTree`.
- Comments explain *why*, not what — a hidden Figma API quirk, a
browser/Flutter constraint workaround, a non-obvious invariant. Don't
narrate what the code already says.
- No new npm dependencies — this ships as two plain files Figma loads
directly, and `tests/` runs on bare Node with no `node_modules`.

## Known gaps (see README's "Known limitations" for the user-facing version)

- Assets (rasterized icons, rotated groups) are inlined as base64 today, not
exported as separate files.
- Dart-side GRID children don't participate in Responsive mode yet.
- The Prototype tab's Smart Animate diffing (`diffFramesForSmartAnimate`)
matches layers between two frames by name + sibling occurrence order, same
as Figma's own heuristic — duplicate/renamed layers can match the wrong
node or fall back to a cross-dissolve cut instead of a tween.
- The opt-in "Inline SVG icons" HTML mode (`inlineSvgHtml`) only applies to
vector-only-subtree assets (real SVG bytes) — image-fill/rotated-container
assets are still raster PNGs and always stay a background-image, and the
variant is only computed for the fixed-pixel geometry, not combined with
Responsive.
78 changes: 78 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,82 @@ python3 .debug-server/debug-server.py

Dumps land in `.debug-server/data/latest.json` / `latest.png` (git-ignored).

### Local MCP server

Settings → **MCP Connect** runs a local [MCP](https://modelcontextprotocol.io)
server so an AI client (e.g. Claude Desktop) can pull whatever it needs from
the current Figma selection — generated CSS/HTML/Dart, design tokens, the
clickable prototype bundle, and real image/preview files written straight to
disk (not left as inline base64). It stays off by default and everything
stays on your own machine.

1. **Get the script** — either use `.mcp-server/mcp_server.py` from this repo,
or, if you only installed the plugin from Community (no repo checkout),
copy/download it straight from the plugin's Settings → MCP Connect panel.
2. **Create a virtual environment next to the script and install into it**
(plain `pip install` fails on modern macOS/Homebrew Python with an
"externally-managed-environment" error — a venv sidesteps that, and keeps
these two packages out of your system Python entirely):

```
cd .mcp-server
python3 -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
python3 -m ensurepip --upgrade # skip if `pip --version` already works
pip install mcp websockets
```

That `ensurepip` line is only needed if `pip install` above errors with
`command not found: pip` — some Python builds don't bootstrap pip into a
new venv automatically. If `ensurepip` itself then fails too (some
Homebrew Python builds strip pip's bundled installer wheel to shrink the
bottle), fall back to fetching the real installer instead:

```
curl -sS https://bootstrap.pypa.io/get-pip.py -o get-pip.py
python3 get-pip.py
pip install mcp websockets
```

Then, each time you want the server running:

```
source venv/bin/activate
python3 mcp_server.py
```

Leave that process running — it holds the live bridge to the plugin and
answers the MCP client's tool calls.
3. **In Figma**, open the plugin, select something, and toggle
Settings → MCP Connect → **Connect**. Every new selection streams to the
server automatically from then on.
4. **Point your MCP client at the script over stdio.** MCP clients spawn the
process themselves — they don't inherit an activated venv from your shell
— so `command` must point at the venv's own Python executable, not the
system `python3`. For Claude Desktop, add this to
`claude_desktop_config.json` (using the absolute paths to wherever you
saved the script):

```json
{
"mcpServers": {
"figma-to-code": {
"command": "/absolute/path/to/.mcp-server/venv/bin/python3",
"args": ["/absolute/path/to/.mcp-server/mcp_server.py"]
}
}
}
```

Restart Claude Desktop, and it can then call `list_selected_nodes`,
`get_node_code`, `export_node` (code + real asset files + preview PNG for
one layer), `export_selection` (the whole selection at once),
`get_design_tokens`/`get_design_export`, and `get_prototype_html`.

Like the debug server, this needs Python 3 on your machine and is meant for
developers/power users wiring the plugin into an AI workflow — most
plugin users will never need it.

## Project structure

```
Expand All @@ -84,6 +160,8 @@ ui.html Plugin UI (tabs, toggles, copy buttons) — postMessage'd
tests/ Node-runnable regression suite (see below)
.debug-server/ Optional local debug HTTP server (see above); generated JSON,
PNG captures, and logs stay git-ignored
.mcp-server/ Optional local MCP server (see above) bridging the plugin's
live selection to an MCP client like Claude Desktop
```

## Development
Expand Down
Loading
Loading