# Figma MCP Setup

> Install Figma MCP access into a DeepSeek Harness (dsh) instance, on this machine or a new one. Covers both routes — the PAT/stdio route for quick read access, and the OAuth route for the official remote server that can read files the PAT route is refused. Use when asked to set up Figma access, connect Figma to the harness, fix a broken Figma MCP connection, or diagnose 403/401 errors from a Figma MCP server.

- Skill: `mrwhathuang/figma-mcp-setup` (Agent Skill, multi-file: 8 files)
- Install (CLI): `npx skillmds@latest add mrwhathuang/figma-mcp-setup`
- Raw SKILL.md: https://api.skillmd.com/api/skills/mrwhathuang/figma-mcp-setup/raw
- Safety review: pending (external: skill-scanner WARNING, skillspector WARNING)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Design & Media
- Author: MrWhatHuang (https://skillmd.com/u/mrwhathuang)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/mrwhathuang/figma-mcp-setup

---


# Figma MCP setup for DeepSeek Harness

Two independent routes. Pick one; running both registers duplicate Figma tools.

| | PAT route (A) | OAuth route (B) |
|---|---|---|
| Server | `figma-developer-mcp` (npm, local stdio) | `https://mcp.figma.com/mcp` (official, remote) |
| Auth | Personal Access Token | OAuth 2.0 authorization code |
| Setup cost | ~2 minutes | ~10 minutes (needs a browser consent click) |
| Tools | 2 (`get_figma_data`, `download_figma_images`) | ~26–41 depending on plan/filtering |
| Reads restricted files | **No** — fails `403 File not exportable` | **Yes** |
| Needs Figma desktop app | No | No |

**Choose A** for a fast read-only setup. **Choose B** if A returns
`403 File not exportable`, or if you want the richer tool set (design context,
screenshots, variables, code connect).

Route B needs a plugin that the in-box harness does not ship. The `assets/`
bundle contains it; see step B2.

## Before starting

Gather these facts first; every later step depends on them:

1. **The harness install path.** Where `dsh` actually runs from:
   ```sh
   which dsh; readlink -f "$(which dsh)"
   ```
2. **The harness home.** `echo $DSH_HOME` (default `~/.dsh`).
3. **The profile to modify.** Usually `web`. Confirm it exists:
   ```sh
   ls "$DSH_HOME/profiles/"
   ```
4. **Whether a GUI is running.** Config changes need a restart; see "Restart" below.

Every command below uses `$DSH_HOME` and `$PROFILE`; set them once:

```sh
export DSH_HOME="${DSH_HOME:-$HOME/.dsh}"
export PROFILE=web
```

## Route A — PAT + stdio (quick read access)

### A1. Get a token

Figma → Settings → Security → Personal access tokens → generate.
Scope needed: **File content → Read only**. Token looks like `figd_...`.

Verify it before wiring anything (a bad token here is much cheaper to find now):

```sh
curl -s -H "X-Figma-Token: $FIGMA_TOKEN" https://api.figma.com/v1/me
```

Expect JSON with your `handle`/`email`. A `403 Invalid scope` means the token
lacks `file_content:read`; regenerate it.

### A2. Install the server into the profile

```sh
dsh plugin --profile "$PROFILE" add figma-developer-mcp
```

The warning `declares no dsh.bundle — installed as a plain dependency` is
**expected and correct**: this is an MCP server, not a harness bundle. Wiring it
up is step A3.

### A3. Wire it into the profile patch

Append to `$DSH_HOME/profiles/$PROFILE/cordis.patch.yml` (top-level YAML array):

```yaml
- insert:
    - id: mcp-figma
      name: '@deepseek-ai/dsh-mcp-client'
      config:
        serverName: figma
        transport: stdio
        command: node
        args:
          - !!js process.env.DSH_HOME + '/profiles/web/node_modules/figma-developer-mcp/dist/bin.js'
          - '--stdio'
        env:
          FIGMA_API_KEY: !!js process.env.FIGMA_API_KEY
          IMAGE_DIR: !!js process.env.DSH_HOME + '/figma-assets'
          FRAMELINK_TELEMETRY: 'off'
        toolCallTimeoutMs: 120000
```

Adjust `/profiles/web/` if `$PROFILE` is not `web`.

Two deliberate choices: `IMAGE_DIR` keeps downloaded assets in one place
(otherwise they scatter into the server's inherited cwd), and
`FRAMELINK_TELEMETRY: 'off'` stops the third-party server reporting usage.

### A4. Store the token

Write `$DSH_HOME/.env` (this file does **not** travel with a repository, which is
why it is the right home for a secret):

```sh
printf 'FIGMA_API_KEY=%s\n' "$FIGMA_TOKEN" > "$DSH_HOME/.env"
chmod 600 "$DSH_HOME/.env"
```

The harness loads this file at startup. `FIGMA_API_KEY` is not a bootstrap-only
name, so it is accepted here.

Then restart (see "Restart") and verify with "Verifying" below.

## Route B — OAuth + official remote server

### B1. Understand why the in-box plugin cannot do this

The shipped `@deepseek-ai/dsh-mcp-client` builds its Streamable HTTP transport
with **static headers only** — no `authProvider`, no OAuth code. Servers that
demand an authorization-code flow cannot be reached, and no header configuration
fixes it: the flow needs a browser round-trip, a PKCE verifier, and token refresh.

Do not try to patch the installed package: `$DSH_HOME/profiles/node_modules/*`
are symlinks into the harness install, so edits are lost on update. Ship a
separate plugin instead (supplied in `assets/dsh-mcp-oauth`).

### B2. Install the plugin

Copy `assets/dsh-mcp-oauth/` from this skill to a permanent location, e.g.
`~/dsh-mcp-oauth`. It must not live in a temp directory.

Link the two packages it imports, resolved from the profile's dependency tree:

```sh
PLUGIN=~/dsh-mcp-oauth
mkdir -p "$PLUGIN/node_modules/@modelcontextprotocol" "$PLUGIN/node_modules/@deepseek-ai"
ln -sfn "$DSH_HOME/profiles/node_modules/@modelcontextprotocol/sdk"      "$PLUGIN/node_modules/@modelcontextprotocol/sdk"
ln -sfn "$DSH_HOME/profiles/node_modules/@deepseek-ai/schemastery"       "$PLUGIN/node_modules/@deepseek-ai/schemastery"
node -e "import('$PLUGIN/src/index.js').then(()=>console.log('plugin loads OK'))"
```

If that prints a load error, the symlinks are wrong — fix them before continuing.

### B3. Wire it into the profile patch

```yaml
- insert:
    - id: mcp-figma-remote
      name: '/absolute/path/to/dsh-mcp-oauth/src/index.js'
      config:
        transport: streamable-http
        serverName: figmaRemote
        url: 'https://mcp.figma.com/mcp'
        clientName: 'Claude Code'
        scopes: ['mcp:connect']
        toolCallTimeoutMs: 120000
        failOnStartupError: false
```

`clientName` is **not cosmetic** — Figma allowlists it exactly (see
`references/figma-gotchas.md`). Leave it as `Claude Code` unless Figma changes
its policy.

If you also configured route A, set `disabled: true` on that row to avoid
registering two near-identical sets of Figma tools.

### B4. Authorize

Restart, then trigger the first connection (open a session and ask it to do
anything; the plugin connects at startup). The plugin prints a consent URL and
waits up to 5 minutes:

```
mcp-oauth(figmaRemote): authorization required — open this URL to grant access:
  https://www.figma.com/oauth/mcp?...
```

Open it, click **Allow**. The browser redirects to `127.0.0.1:33418/callback`
and shows "Authorization complete". Tokens are then cached at
`$DSH_HOME/mcp-oauth/figmaRemote.json`, so this happens **once**, not per start.

## Verifying the install

Do not declare success from a config dump — call a real tool and read real data.

**Check the tools are registered.** In a session, the model should see
`mcp__figma__*` (route A) or `mcp__figmaRemote__*` (route B).

**Route A:**

```
mcp__figma__get_figma_data with fileKey <key>
```

**Route B — strongest check**, because it exercises auth end to end:

```
mcp__figmaRemote__whoami
```

This returns your handle, email, and plan — proof the OAuth token is live.

Then read a real node:

```
mcp__figmaRemote__get_metadata with fileKey <key>, nodeId <id>
```

The `fileKey` is the path segment after `/design/` in a Figma URL; `nodeId`
comes from `?node-id=2048-42655` (note: URL uses `-`, tools accept either).

**Use a negative control.** A tool call that "succeeds" against a bogus key may
never have reached the network. Confirm the call really hits Figma by checking
that an intentionally bad input produces a *server* error (a well-formed but
nonexistent key should give `404 Not found` from Figma, not a local validation
message).

## Restarting

`cordis.patch.yml` changes hot-reload under `patchReload: live` **only when the
watcher is running**; plugin *source* changes never hot-reload. Environment
variables (`FIGMA_API_KEY`) are read once at process start.

Restart the harness GUI after any config, plugin, or credential change. If the
figma tools are missing right after a config edit, restart before debugging.

## Troubleshooting

Work through these in order; each is a real failure observed during development.

| Symptom | Cause | Fix |
|---|---|---|
| `EADDRINUSE 127.0.0.1:33418`, 0 tools | Plugin binds the OAuth callback port even when a cached token means no browser is needed, so a second instance dies | Use the supplied plugin, which binds best-effort and releases the port after connecting |
| Tools absent, no error at all | Changed config/source without restarting | Restart; source edits never hot-reload |
| `403 File not exportable` | File's sharing settings forbid API export for this account | Route B; ask the owner to allow export/copy; or duplicate the file into your own space |
| `401 Unauthorized`, consent URL never printed | `redirectUrl` not bound before connect, so the SDK throws a generic error instead of the "consent needed" signal | Supplied plugin binds the listener before connecting; verify you are not running a modified copy |
| `403 Forbidden` from `api.figma.com/v1/oauth/mcp/register` | Figma gates dynamic registration on an exact `client_name` allowlist | Set `clientName: 'Claude Code'` exactly (case and spacing matter) |
| `Invalid scope` from `/v1/me` | PAT lacks `file_content:read` | Regenerate the token with that scope |
| PAT rejected by `mcp.figma.com` | The remote endpoint does not accept PATs at all | Use OAuth (route B); `X-Figma-Token` will not work here |
| Tools work, then stop after hours | Access token expired | The SDK refreshes on the next connection attempt; restart if needed |

More detail, including the measurements behind each row:
`references/figma-gotchas.md`.

## Bundle contents

| Path | Purpose |
|---|---|
| `SKILL.md` | This guide. |
| `assets/dsh-mcp-oauth/` | The OAuth-capable MCP client plugin (route B). Copy to a permanent path. |
| `scripts/verify.sh` | Non-destructive preflight: checks node, harness home, profile, token/plugin state. |
| `references/figma-gotchas.md` | Measured findings: the `client_name` allowlist, the redirect-ordering bug, the port conflict. |

## Security notes

- Never commit a PAT or a token cache. `$DSH_HOME/.env` and
  `$DSH_HOME/mcp-oauth/*.json` are written `0600`; keep them that way.
- A PAT pasted into a chat or log should be treated as compromised — rotate it.
- The OAuth callback listener binds loopback only, so the authorization code is
  never reachable off-box.
- Route B identifies the client to Figma as `Claude Code`. If that is
  unacceptable, use route A — but note it cannot read export-restricted files.

