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:
- The harness install path. Where
dshactually runs from:which dsh; readlink -f "$(which dsh)" - The harness home.
echo $DSH_HOME(default~/.dsh). - The profile to modify. Usually
web. Confirm it exists:ls "$DSH_HOME/profiles/" - Whether a GUI is running. Config changes need a restart; see "Restart" below.
Every command below uses $DSH_HOME and $PROFILE; set them once:
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):
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
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):
- 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):
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:
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
- 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/.envand$DSH_HOME/mcp-oauth/*.jsonare written0600; 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.