figma-listen
by mikekelly
README.md
# Figma watch
**A companion to the official Figma MCP for reviewing designs with your agent.**
Your agent updates a design in Figma and gives you a link to check it over in the app. It can then subscribe to activity on the file, page, section or frame it's working on. Leave a comment, react to feedback, resolve a thread, or edit the design yourself. For agents that support MCP Events streams, Figma watch forwards matching comments and reactions as soon as polling detects them and batches design edits until the scope is quiet, letting you collaborate directly in Figma.
The agent can reply in Figma too. The official Figma MCP can't post comments; Figma watch can, when your token allows it, and comments the agent posts through it are never delivered back to that agent as events, so its own words don't wake it up.
> Figma watch was previously published as `@realmikekelly/figma-listen`. See [Upgrading from figma-listen](#upgrading-from-figma-listen).
## Setup
You'll need Node.js **20.19 or newer** and npm.
### 1. Generate an access token
In Figma, go to **Settings > Security > Personal access tokens > Generate new token**.
- Give it a name, such as **Figma watch**.
- Set the expiration to **90 days**.
- Give it **all the read scopes**.
- To let the agent comment back, also give it **Comments: write** (`file_comments:write`).
Save your token by running this in your terminal, then paste it into the hidden prompt:
```sh
npx -y @realmikekelly/figma-watch auth
```
If you already export `FIGMA_ACCESS_TOKEN`, you can use that instead of saving a token with `auth`.
### 2. Install the MCP
Ask your agent:
> Add Figma watch as a local stdio MCP server in my agent's MCP configuration. Run `npx` with arguments `-y` and `@realmikekelly/figma-watch`.
For clients that use an `mcpServers` JSON configuration:
```json
{
"mcpServers": {
"figma_watch": {
"command": "npx",
"args": ["-y", "@realmikekelly/figma-watch"]
}
}
}
```
The configuration location and format depend on your client. This example uses the token saved by `auth`. If you use `FIGMA_ACCESS_TOKEN` instead, configure your client to pass that environment variable to the MCP process. Allow enough startup time for the first `npx` download.
Check that authentication is working:
```sh
npx -y @realmikekelly/figma-watch doctor
```
Once connected, ask your agent to subscribe to the files you're reviewing. For example:
> Use Figma watch to subscribe to comments containing #bot in this Figma file. Include replies to those threads, and check for feedback while we work. Use the official Figma MCP for the design work, and reply to my comments with Figma watch.
## Upgrading from figma-listen
Version 2.0 renamed the package, command and tools:
| Before | After |
| --- | --- |
| `@realmikekelly/figma-listen` | `@realmikekelly/figma-watch` |
| `figma-listen` command | `figma-watch` |
| `listen_subscribe`, `listen_get_events`, … | `watch_subscribe`, `watch_get_events`, … |
| `FIGMA_LISTEN_STATE_DIR` | `FIGMA_WATCH_STATE_DIR` (the old name still works) |
Point your MCP configuration at the new package and rename the server entry if you like. A token saved by `figma-listen auth` is found and copied automatically; you don't need to run `auth` again. Update any prompts or agent instructions that name the old tools.
## Notification support
Figma watch checks for activity on a **three-second target interval**, subject to request pacing and Figma's rate limits. Comments and reactions are delivered as soon as polling detects a matching change. Design edits are collected into a changeset and delivered after **120 seconds without an observed design change in the subscribed scope**.
Automatic notifications require an agent host that implements the experimental MCP Events streaming extension: it must open an `events/stream` request and handle the resulting notifications. Whether an event wakes an idle agent depends on the host. A stdio connection alone does not provide that behavior.
Clients without event-stream support can use `watch_subscribe` and `watch_get_events` to retrieve feedback during an active session. Those tools do not establish a push stream.
Figma watch runs locally and polls Figma's REST API, delivering events over MCP stdio. It observes comments, reactions and design changes and can post comments; the agent uses the official Figma MCP for design work and decides how to act on feedback.
## Commenting
With the `file_comments:write` scope, the agent gets a `watch_post_comment` tool:
```json
{ "url": "https://www.figma.com/design/YOUR_FILE_KEY/Design?node-id=12-34", "message": "🤖 Tightened the spacing on this card" }
```
- Give `file_key` or a Figma URL. A `node_id`, or a URL with `node-id`, pins a new thread to that node; `node_offset` `{ "x": 0, "y": 0 }` positions the pin relative to the node's top-left.
- `reply_to` replies to a thread. It accepts the ID of any comment in the thread and replies to the thread's root, as Figma requires. Replies can't be pinned, so a URL's `node-id` is ignored for a reply.
- The comment is posted as the token's account, so it looks like it came from you. Prefixing messages with 🤖 tells people which comments came from the agent.
**No self-echo.** Your token is the agent's identity, so the server can't tell the agent's comments from yours by author. Instead it records the ID of every comment it posts. That comment never becomes a `figma.comment.created` event in the process that posted it, even if a poll observes it before Figma's response arrives. Replies, reactions, edits and resolutions from people still arrive as usual. Comments posted by other tools, or by another agent's Figma watch process, are delivered normally.
**Scope detection.** Figma has no endpoint that lists a token's scopes. At startup, Figma watch makes requests that a typical token isn't allowed to make: `GET /v2/webhooks`, then, if needed, `POST /v1/dev_resources` with an empty body, which can't create anything. Figma's rejection lists the token's granted scopes. If `file_comments:write` isn't among them, `watch_post_comment` isn't offered. If neither request is rejected for scope, the token has broad write scopes, so the tool is offered; if Figma then rejects a post for missing scope, the tool is removed and a `notifications/tools/list_changed` notification is sent. `doctor` reports the detected scopes and whether the tool is offered. This relies on the wording of Figma's error message, which isn't a documented API.
## Authentication details
The `auth` command validates your token and saves it in macOS Keychain, Windows Credential Manager, or Linux Secret Service. Linux requires an available Secret Service; environment authentication also works without the optional keyring dependency.
The `FIGMA_ACCESS_TOKEN` environment variable takes precedence over a saved credential. Figma watch uses the environment token without copying it into its config or state files. `logout` removes the saved credential and leaves environment configuration alone. When your token expires, replace the environment value or run `auth` again.
All available read scopes work; add `file_comments:write` to enable commenting. If you prefer to select only the scopes used by Figma watch:
| Scope | Used for |
| --- | --- |
| `current_user:read` | Authentication check and binding local state to your Figma account |
| `file_comments:read` | Reading comments and replies |
| `file_content:read` | Design snapshots and mapping comments into page/section/frame scopes |
| `folders:read` | Discovering files in folders and teams |
| `file_comments:write` | Optional: `watch_post_comment` |
Access is limited to resources visible to the token's account. The official Figma MCP's OAuth credentials are managed separately and are not reused by this server.
## Other ways to run
Run the npm package directly with `npx`:
```sh
npx -y @realmikekelly/figma-watch --help
```
Or install it globally:
```sh
npm install -g @realmikekelly/figma-watch
figma-watch --help
```
With no subcommand, `figma-watch` starts the MCP server. Help, version, and doctor output go to stdout; while serving MCP, stdout contains only protocol messages and diagnostics go to stderr.
For a local checkout:
```sh
git clone https://github.com/mikekelly/figma-watch.git
cd figma-watch
npm ci
node dist/cli.js doctor
node dist/cli.js
```
`npm ci` builds the TypeScript source. Compiled tarballs are also available from [GitHub releases](https://github.com/mikekelly/figma-watch/releases).
## Process configuration
When using environment authentication, `FIGMA_ACCESS_TOKEN` must be available to the MCP process. A desktop app launched outside your terminal may not inherit your shell environment; saved credential authentication avoids that dependency. Do not put your token into command arguments or checked-in config.
For the checkout at `~/code/figma-watch`, you can instead use an absolute path to Node and the built `dist/cli.js`. Run `which node` to find your Node executable; MCP processes do not expand `~` in arguments.
Each agent gets an independent MCP process with in-memory state by default. Parallel agents need no extra configuration. Subscriptions, snapshots, buffered events and pending design batches end when their process exits; the agent must subscribe again after restarting. Polling, deduplication and backoff are shared within each process, not across agents. Multiple agents watching the same file make independent requests.
If you explicitly want restart recovery, opt into disk persistence. Use a different directory for each simultaneously connected process:
Set the server's `args` to:
```json
["-y", "@realmikekelly/figma-watch", "--state-dir", "/absolute/path/to/session-state"]
```
## Subscriptions
Call `watch_subscribe` with a scope, optional event types and optional comment tag:
```json
{
"scope": { "kind": "section", "file_key": "YOUR_FILE_KEY", "node_id": "12:34" },
"event_types": ["figma.design.changed", "figma.scope.deleted", "figma.comment.created", "figma.comment.edited"],
"tag": "#bot",
"include_thread_replies": true
}
```
This watches the section's design changes and tagged comments/replies. Omit `event_types` to receive **all supported event types**. For comments only, explicitly select the `figma.comment.*` names you want (wildcards are not accepted); these subscriptions do not poll design snapshots. Tags filter **comments and reactions only**, so a tag never suppresses design changes.
| Scope | Required fields | Coverage |
| --- | --- | --- |
| `file` | `file_key` | Activity throughout the file |
| `page` | `file_key`, `page_id` | The page and its descendants |
| `section` | `file_key`, `node_id` | The section and its descendants |
| `frame` | `file_key`, `node_id` | The frame and its descendants |
| `folder` | `folder_id`, optional `recursive` (default `true`) | Visible files in the folder and, by default, its subfolders |
| `team` | `team_id` | Visible files discovered through that team's folders |
| `organization` | `organization_id`, `team_ids` | Visible files in the supplied teams; explicitly partial organization coverage |
For file/page/section/frame scopes you can supply a Figma URL instead of IDs, for example:
```json
{ "scope": { "kind": "section", "url": "https://www.figma.com/design/YOUR_FILE_KEY/Design?node-id=12-34" } }
```
Node IDs accept `1:2` or URL form `1-2`. The node ID must identify the intended page, section or frame. Subscriptions follow that ID through renames and moves. If the target disappears and remains absent through the quiet period, `figma.scope.deleted` is emitted once and coverage reports `target_status: "missing"`. The subscription remains available for replay and resumes if the same node is restored; unsubscribe to stop it permanently. Folder/team IDs come from Figma's folder/team URLs; folder IDs replace legacy project IDs in the v2 folder API.
Tags match whole, **case-sensitive** tokens: `#bot` matches `Please #bot review`, but not `#botnet` or `#Bot`. By default, each comment/reply must contain the tag itself. With `include_thread_replies: true`, replies also match the root comment's tag. Reactions inherit their comment's filtering. Comment edits match the old or new text, so removing `#bot` still delivers that edit. Deletions retain the last known text and anchor.
## Supported events
| Event | Observed change |
| --- | --- |
| `figma.comment.created` | New comment or reply (`parent_id` distinguishes replies) |
| `figma.comment.edited` | Comment text or pin position changed |
| `figma.comment.deleted` | Previously observed comment absent from a successful comments response |
| `figma.comment.resolved` | Comment/thread became resolved |
| `figma.comment.reopened` | Comment/thread became unresolved |
| `figma.reaction.added` | Emoji reaction added to a comment or reply |
| `figma.reaction.removed` | Previously observed emoji reaction removed |
| `figma.design.changed` | Net node changes within the watched scope, or a file rename, after the quiet period |
| `figma.scope.deleted` | Previously observed page/section/frame target remains absent when its changeset flushes |
Design events contain version IDs (which may be equal before and after an edit), the watched `target_id` (`null` for a file), affected node IDs, before/after names and hierarchy, and `changed_properties` such as `characters`, `fills` or `children`. Changes are classified as `added`, `updated`, `removed`, `moved`, `entered` or `left`. Both previous and current ancestry are checked when nodes cross scope boundaries. Property values are hashed locally; events report changed property names rather than full before/after document values. The agent can inspect the design through the official Figma MCP. Each event includes up to 1,000 node changes, with `total_changes` and `changes_truncated` explicitly reporting larger deltas.
### Design changesets
Each subscription has a **120-second quiet timer**. Every observed design change within its scope resets that timer; changes outside that scope, comments, reactions, and version-ID changes alone do not. A file subscription watches its whole file; folder/team/organization subscriptions share a timer across their discovered files. After the quiet period, a successful design read for every covered file confirms the flush. Polling delays, slow requests, backoff and failed reads can make delivery later than two minutes. Continuous editing keeps the changeset open; there is no forced maximum-age flush.
A flush emits one `figma.design.changed` event per changed file, with the net difference from before the collected edits to the latest observed state. Repeated edits collapse; fully reverted edits and temporary additions/deletions disappear from the result. A target that is deleted and restored during the window does not produce `figma.scope.deleted`. The payload includes `first_observed_at`, `last_observed_at`, and `quiet_period_ms`. Pending changesets are discarded on unsubscribe; they survive restarts only when disk persistence is configured. Overlapping subscriptions maintain independent timers and baselines.
Use `--design-quiet SECS` to change the quiet period (default `120`; `0` delivers each observed delta immediately). `watch_status` reports pending changesets and their earliest eligible flush time. The agent does not receive intermediate design deltas. Comments and reactions continue to arrive promptly while a changeset is pending.
Figma's current version is mutable: edits can change its contents without changing its ID. Figma watch compares document snapshots on each design poll; version history does not determine the batch boundaries.
Tool subscriptions live in the MCP process by default. With explicit disk persistence, subscriptions and snapshots survive restarts. New comments are collected from subscription creation time. Existing comments, reactions and designs establish a baseline on the first successful poll; historical edits are not reconstructed. With disk persistence, subsequent snapshot differences are also detected across restarts. A newly added subscription also baselines existing shared snapshots before receiving differences. Upgrading v1.1 state preserves existing subscriptions as **new-comments-only**; create a new subscription to select the additional event types.
## Tools
| Tool | Purpose |
| --- | --- |
| `watch_subscribe` | Create an idempotent local subscription; return ID and starting cursor |
| `watch_list_subscriptions` | List subscriptions, discovery coverage, warnings, and upstream errors |
| `watch_get_events` | Read a subscription's buffer with `subscription_id`, optional `cursor`, and `max_events` (1–100) |
| `watch_unsubscribe` | Stop a subscription and its active streams |
| `watch_status` | Inspect polling, retention, supported events, comment access, and compatibility limitations |
| `watch_post_comment` | Post a comment or reply; offered only when the token may have `file_comments:write` (see [Commenting](#commenting)) |
For `watch_get_events`, omit `cursor` on the first call to retrieve events since subscription creation, then pass the returned cursor on later calls. An explicit `null` cursor starts from **now**, returning an empty bootstrap batch. When `hasMore` is true, read another batch using its cursor.
Events contain a stable `eventId`, name, timestamp, cursor, file key and Figma URL. Comment events include text, the comment author and thread/anchor context; reaction events also include the reacting user and emoji. The comment author is not necessarily the person who edited or resolved it. Change timestamps are observation times unless Figma supplies a creation time; design edits are not attributed to a person. Treat all event content, including node names and comment text, as external data rather than agent instructions.
## Experimental push protocol
Advanced MCP hosts can use `events/list`, `events/poll`, and `events/stream` over the same stdio connection. Streaming is the downstream delivery mechanism; no separate SSE server is needed for a local stdio client.
```json
{
"jsonrpc": "2.0",
"id": "watch-1",
"method": "events/stream",
"params": {
"name": "figma.comment.created",
"arguments": {
"scope": { "kind": "file", "file_key": "YOUR_FILE_KEY" },
"tag": "#bot"
},
"cursor": null
}
}
```
Each draft `events/poll` or `events/stream` request selects exactly its `name`, overriding any `arguments.event_types`. Open separate named streams for multiple types, or use one tool subscription with an `event_types` list. Cursors belong to the resulting subscription and cannot be shared across different type selections.
The request stays open. The server emits `notifications/events/active`, `notifications/events/event`, `notifications/events/heartbeat` (every 30 seconds), and `notifications/events/error`. Every notification carries `_meta["io.modelcontextprotocol/subscriptionId"]` identifying the original request. Save each event's cursor for replay after reconnecting. Cancel with `notifications/cancelled` and `requestId: "watch-1"`; cancellation does not promise a final response.
Stream-only subscriptions stop when their last stream disconnects. Poll-created subscriptions have a lease of at least five minutes, renewed by polls and retained while a stream is open. Tool-created subscriptions last until unsubscribed or the process exits; explicit disk persistence restores them on restart. Shared subscriptions poll each file only once per cycle; independent consumers must keep independent cursors.
This is a draft extension, not an assertion that every MCP host supports it. See the [MCP Events proposal](https://github.com/modelcontextprotocol/experimental-ext-triggers-events/blob/main/docs/design-sketch-proposal.md).
## Polling, state, and limits
- Default desired polling interval: **3 seconds**. The producer keeps ticking independently of responses. It submits separate FIFO jobs for comments, reactions, designs and discovery scopes; a resource already queued or running is not submitted again. Overlapping subscriptions share file requests and scope discovery.
- Design jobs fetch a shared full document snapshot with geometry paths on each poll and compare property hashes, detecting edits even when Figma keeps the same version ID. This transfers more data than a shallow version check, but overlapping subscriptions still share one design job per file. Scope filtering is applied before changes enter a changeset. Comments may also fetch a document to resolve anchors.
- Reactions use inline reaction lists when provided, otherwise the paginated reaction endpoint per matching comment. Fallback reads are separate from comment delivery; scoped fallback reactions refresh the document to resolve current anchors. Large reaction subscriptions cost more requests and can take longer to cycle.
- Jobs dispatch concurrently, with at most **4 HTTP requests in flight** and **2 seconds between request starts** by default. A slow response holds its own slot, not the producer timer or all other requests. If all slots are occupied, additional jobs remain queued. Under saturation, effective per-resource polling slows rather than accumulating duplicate work. The two-second safeguard is our configurable limit, not a universal Figma rule.
- HTTP **429** pauses the shared HTTP dispatch queue for `Retry-After`; already sent requests may finish, and pending jobs keep their FIFO order. Individual failed file/discovery jobs also retry with exponential backoff, capped at 15 minutes, and reset after a successful attempt. Healthy resources keep running unless a global rate-limit pause applies. Folder discovery refreshes on a five-minute target.
- `--poll-interval SECS` changes the desired interval (minimum **1 second**). `--request-interval MS` changes global request-start spacing. For example, `--poll-interval 3 --request-interval 1000` targets three seconds while allowing up to one request start per second; use a rate appropriate to your Figma allowance. The desired interval does not override request pacing, concurrency limits, or backoff.
- Figma publishes PAT limits, but the budget is shared **per user and resource plan**, not independently per token. Comments/reactions are Tier 2; design document reads are Tier 1; limits depend on the seat type and the plan containing the file. Other tools or server processes using that account can consume the same budget. See [Figma rate limits](https://developers.figma.com/docs/rest-api/rate-limits/).
- `watch_status` exposes the desired interval, resource queue depth, active resources, coalesced job count, and the upstream dispatch queue's concurrency and backoff deadline. Coverage reports the most recent successful poll for each subscription.
- State stays in memory by default; no state file or lock is created. `--state-dir` or `FIGMA_WATCH_STATE_DIR` (or the older `FIGMA_LISTEN_STATE_DIR`) explicitly enables persistence. One process owns each configured directory, tied to the authenticated Figma account. Existing default state from earlier releases is left untouched and is not automatically loaded; pass its directory explicitly if you want to restore it.
- The state contains comment/reaction snapshots, node names and hierarchy, design property hashes, pending changesets, subscriptions, seen IDs, and cursors; **never the token**. When persistence is enabled, state files use mode `0600`, newly created state directories `0700`, and writes use atomic rename.
- The event buffer retains up to **7 days / 10,000 events**, whichever limit comes first, within the process lifetime unless disk persistence is enabled. Cursors crossing a retention boundary report `truncated: true`; consumers should report the gap rather than assume complete delivery. Observed IDs survive event eviction so retained comments do not reappear as new events.
- Discovery is capped at 1,000 folders, 500 files per subscription, 100 subscriptions, and 100,000 observed comment IDs per file. Document snapshots are capped at 100,000 nodes and 200 hierarchy levels. Reaction fallback is capped at 1,000 matching comments per file and 100 pagination cursors per comment. Narrow overly broad scopes or event selections when a limit is reported.
Polling observes the state exposed by Figma REST, not individual editor operations. Several edits can collapse into one delta; create/delete or edit/revert activity between polls can be missed. API visibility and rate limits affect latency. Failed reads preserve the previous snapshot and never imply deletion. Detected 401/403/404 failures suppress buffered delivery for the affected polling channel; other channels retain their own status. These observations are not a complete audit log.
The REST API cannot enumerate every team in an organization, so organization subscriptions require explicitly supplied team IDs; this server cannot verify those teams' affiliation. Folder/team discovery excludes undisclosed or inaccessible resources and may omit drafts or files outside the hierarchy. Coverage warnings expose those limits.
Page/section/frame comment filtering uses node anchors and document ancestry. Deletions and edits retain prior anchor context when available. Coordinate-only comments, unknown/deleted anchors and replies whose root is unavailable cannot always be mapped; those gaps are reported in coverage. File-level subscriptions still receive them. Library, variable and other changes absent from the fetched document JSON cannot be localized to a page or section; a version-ID change alone does not produce a design event.
## Development and validation
```sh
npm ci
npm run check
npm pack
# Optional, using your exported token; checks /v1/me and MCP status only:
node scripts/smoke-live.mjs
```
Automated tests cover comment posting, scope detection and self-echo suppression (including a poll that lands before the post's response), comment lifecycle and reaction deltas, design property diffs, scope movement/deletion, URL scopes, baseline migration and restart replay, paginated reactions, event type selection, filtering, shared polling/discovery, nonblocking FIFO dispatch, bounded concurrency, duplicate coalescing, independent scheduling during slow requests, responsive subscription tools, rate-limit and exponential backoff, authentication error redaction, persistence, retention, both MCP handshake generations, push notifications, replay, and cancellation. CI checks Node 20, 22, and 24. Live testing verified authentication, MCP tool discovery/subscription creation, new comments and untagged replies, comment edits/deletions/resolution/reopening, and reaction additions/removals. Live file-level design changes were also detected, including metadata lag and mutable current version IDs. Those findings are covered by regression tests; design detection now compares full snapshots. Automated tests also cover the 120-second quiet boundary, scope isolation, net reverts, restart persistence, failure recovery, overlapping subscriptions and multi-file batches. A subsequent v1.3.0 live test used the official Figma plugin for edits and the Figma desktop UI for a comment: a real stdio MCP Events client received the comment in 1.1 seconds, two same-version edits as one changeset after 122.6 seconds of quiet, and target deletion after 124.6 seconds. Out-of-scope edits did not reset the timer, and an untouched frame received no design events. Temporary shapes were removed and the test comment resolved. Page/section filtering retains automated coverage; frame filtering and deletion now also have live coverage.
API references: [files](https://developers.figma.com/docs/rest-api/file-endpoints/), [comments](https://developers.figma.com/docs/rest-api/comments-endpoints/), [folders](https://developers.figma.com/docs/rest-api/folders-endpoints/), [scopes](https://developers.figma.com/docs/rest-api/scopes/), [rate limits](https://developers.figma.com/docs/rest-api/rate-limits/).
## License
MIT. See [LICENSE](LICENSE).
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues