Figma MCP local
README.md
# Design Intelligence Platform (Local MVP)
A local, high-performance replacement for the hosted Figma MCP server. It downloads Figma files
once into a SQLite cache, builds a full-text search index over them, and exposes a local MCP
endpoint that Cursor (or any MCP client) queries — without touching the Figma API.
```
Cursor ──► http://localhost:8787/mcp ──► SQLite cache ──► FTS5 search index
▲
│ only during explicit sync
Figma API
```
The cache is the source of truth. The Figma API is called in exactly three situations: validating
your token on connect, a cheap version check at the start of a sync, and the full file download
when the version changed (or `--force`).
## Architecture
Clean architecture with constructor-based dependency injection; [src/app.ts](src/app.ts) is the
composition root that wires everything together.
```
src/
api/ REST routes (connect, sync, projects, status, logs, health)
mcp/ MCP server: 8 tools over Streamable HTTP (stateless)
figma/ Figma REST API client (axios, typed errors, request logging)
sync/ Parser: Figma document tree -> flattened node rows + search docs
database/ Drizzle schema, better-sqlite3 client (WAL), idempotent migrations
search/ FTS5 search repository (safe query building, bm25 ranking, snippets)
cache/ Cache metadata key/value store (file versions, bookkeeping)
config/ Zod-validated .env configuration
services/ AccountService, SyncService (job engine), QueryService, simplifier
repositories/ Row-level access: accounts, files/pages, nodes, components, styles, variables
types/ Figma API types + simplified domain types
utils/ Structured logger (JSON lines + ring buffer for the dashboard)
shared/ Typed application errors
frontend/ React + Vite + Tailwind dashboard (Connect / Projects / MCP Status)
tests/ Vitest suites incl. end-to-end MCP-over-HTTP tests with a mocked Figma API
scripts/ CLI sync
```
### How data is stored
A Figma file is a single huge JSON tree. Instead of storing one blob (slow to query) or fully
normalizing every property (schema churn), each node becomes one row: queryable columns
(`node_id`, `parent_id`, `page_id`, `type`, `name`, `depth`, `child_index`) plus the node's own
JSON with children stripped. Subtrees are rebuilt breadth-first at query time down to a depth
limit. Search runs over an FTS5 virtual table indexing names and text content.
### Simplified node format
`get_node` defaults to a compact design representation built for code generation: hex colors,
auto-layout expressed as flex terms (`mode/gap/padding/justify/align`), resolved typography, and
component instance names. Raw Figma JSON is available with `format: "raw"`.
## Installation
Requires Node.js 20+.
```bash
npm install
cp .env.example .env # optional; defaults work out of the box
npm run dev
```
- Dashboard (dev): http://localhost:5173 (proxies to the API)
- API + MCP server: http://localhost:8787
For a production-style run: `npm run build && npm start` — the built dashboard is then served
directly at http://localhost:8787.
## Environment variables
| Variable | Default | Description |
| ------------------------------ | ----------- | ------------------------------------------------ |
| `PORT` | `8787` | REST + MCP server port |
| `DATABASE_URL` | `sqlite.db` | SQLite database file path |
| `LOG_LEVEL` | `info` | `debug` \| `info` \| `warn` \| `error` |
| `FIGMA_MAX_RATE_LIMIT_RETRIES` | `3` | Automatic retries on a Figma 429 (0 disables) |
## Using it
1. **Connect** — open the dashboard, paste a Figma personal access token
(Figma → Settings → Security → Personal access tokens). When creating the token, grant these
scopes:
- `current_user:read` — used once to validate the token
- `file_content:read` — required to download files
- `file_metadata:read` — recommended; lets re-syncs use the cheap Tier 3 metadata endpoint
(the app falls back to a Tier 1 shallow fetch without it)
- `file_variables:read` — optional, Enterprise plans only
The token is validated against `/v1/me` and stored in the local SQLite database. *It is
stored in plaintext — this is a single-user local dev tool; treat `sqlite.db` accordingly.*
2. **Sync** — paste a file key (the segment after `figma.com/design/` in a file URL) and click
Sync. The full file is downloaded, flattened into SQLite, and indexed. Re-syncs first do a
cheap version check and no-op when nothing changed.
3. **Connect Cursor** — add to your Cursor MCP settings:
```json
{
"mcpServers": {
"design-intelligence": {
"type": "http",
"url": "http://localhost:8787/mcp"
}
}
}
```
Then ask Cursor things like *"Generate the Login page"*, *"Find all buttons"*, *"List every
page"*, *"Find typography styles"* — all answered from the local cache.
### CLI sync
```bash
npm run sync -- <fileKey> # sync (version-checked)
npm run sync -- <fileKey> --force # re-download unconditionally
```
## MCP tools
| Tool | Purpose |
| ------------------ | ------------------------------------------------------------------------ |
| `sync_file` | Explicitly (re)download a file into the cache; `chunked` for huge files |
| `sync_node` | Incrementally re-sync one node/frame/page subtree (must already be cached) |
| `get_file_summary` | One-call overview: counts, page list, top components — orient before drilling in |
| `list_pages` | Pages of a cached file with their top-level frames |
| `get_node` | Node subtree — simplified (default) or raw; `depth` bounds the size |
| `search` | FTS over pages, frames, components, text and styles; `limit`/`offset` |
| `list_components` | Components / component sets with descriptions; `limit`/`offset` + total |
| `get_styles` | Shared styles and design variables; `limit`/`offset` + totals |
`fileKey` is optional on read tools when exactly one file is cached. Read tools are marked
`readOnlyHint` so clients like Cursor can auto-run them safely.
## REST API
`POST /api/connect` · `POST /api/sync` · `POST /api/sync-node` · `GET /api/projects` ·
`GET /api/status` · `GET /api/logs` · `GET /api/health` · `GET /api/connection`
## How sync works
1. `GET /v1/files/:key/meta` — compare `version` against the cached one; stop if unchanged.
This is a Tier 3 endpoint (large rate budget). Without the `file_metadata:read` scope it
falls back to `GET /v1/files/:key?depth=1` (Tier 1).
2. `GET /v1/files/:key` — full download (no vector geometry). Tier 1 — the scarcest rate-limit
class (per Figma's [rate limits](https://developers.figma.com/docs/rest-api/rate-limits/),
roughly 10–20 requests/min on Dev/Full seats since Nov 2025), which is exactly why this
platform caches instead of proxying.
3. Parse: flatten the document tree into node rows and search documents.
4. Single SQLite transaction: replace file, pages, nodes, components, styles.
5. Variables via `GET /v1/files/:key/variables/local` — **Enterprise-plan only**; a 403 is
expected on personal/pro plans and logged as a warning, not an error.
6. Rebuild the FTS5 index for the file.
Sync runs as a background job; poll `GET /api/status` or watch the dashboard. Nothing else ever
calls the Figma API.
For very large files, pass `chunked: true` (the `sync_file` tool / `POST /api/sync`) to download
page-by-page — a shallow `?depth=1` fetch for the page list, then one `?ids=<pageId>` request per
page — instead of one giant whole-file response. It bounds peak payload/memory at the cost of more
Tier-1 requests (the 429 retry above absorbs transient limits).
### Incremental sync
Iterating on one screen shouldn't re-download the whole file. `sync_node` (MCP) /
`POST /api/sync-node` fetches a single node via `GET /v1/files/:key/nodes?ids=`, re-roots the
returned subtree from the cached node's position, and atomically replaces just that branch — node
rows and their search index — while upserting any component/style metadata the response carries.
The node must already be cached from a prior full sync; a full `sync_file` remains the way to
reconcile structural changes such as added or deleted pages.
## Scripts
`npm run dev` · `npm run build` · `npm start` · `npm test` · `npm run lint` · `npm run format` ·
`npm run sync -- <fileKey>`
## Error handling
Typed errors with stable codes surface everywhere: `INVALID_TOKEN`, `NOT_CONNECTED`,
`FILE_NOT_FOUND`, `CACHE_MISS`, `CACHE_EMPTY`, `RATE_LIMITED`, `SYNC_IN_PROGRESS`,
`VALIDATION_ERROR`, `FIGMA_API_ERROR`. MCP tools return them as tool errors instead of crashing
the transport.
## Future roadmap (designed for, not implemented)
Multiple accounts (the API client is already token-stateless), semantic/vector search (sits next
to the FTS table), AI code generation (consumes the simplified node format), background sync,
component dependency graphs, and Electron packaging.