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.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessSyncing