# io.github.duncanbottrill/deskhand (npm · deskhand)

Claude in your Figma file. Builds with your components, checks what it wrote. Runs on your computer.

- Trust score: 64/100 (medium)
- Registry status: active
- Liveness: live
- Owner verified: no
- Last scored: 2026-11-05

## Components

- npm · `deskhand`: 64/100 (this document), [markdown](https://verifymcp.io/servers/duncanbottrill-deskhand/deskhand.md), [page](https://verifymcp.io/servers/duncanbottrill-deskhand/deskhand)

## Channel facts

- Registry: `npm`
- Package: `deskhand`
- Version: `1.0.1`
- Transport: `stdio`

## Trust breakdown

How this component scores in each security and reliability category. Every signal is checked automatically from public evidence about the published package, including repeated runs of it in an isolated sandbox, and we only credit what we can confirm. Scores are 0–100 per category. Scoring method: https://verifymcp.io/docs/scoring (what has changed: https://verifymcp.io/docs/scoring/changelog)

Scored 2026-11-05.

- **Supply Chain Security**: 98/100
  - No malware found by supply-chain analysis.
  - No known CVEs affecting this package version or its production dependencies.
  - No install/post-install scripts declared.
  - 31 of 96 dependencies flagged as unhealthy.
- **Provenance & Transparency**: 19/100
  - Repository check failed: no source repository is declared.
  - Provenance check failed: no build-provenance attestation is published.
  - Clear OSI-approved license (MIT).
  - Actively maintained (last published 3 days ago).
  - Security-disclosure policy not yet verified: we couldn't inspect the source repository.
- **Schema Quality & AI Usability**: 76/100
  - 100% of prompts and resources have a non-trivial description (not blank, and not just the item's name).
  - AI-judged instruction clarity (fair).
  - Tool/resource definitions use about 7975 tokens (~83/item across 96 items; 94 tools + 2 resources), lean.
  - Usage-examples check failed: none of the tools include examples.
- **Stability & Change Management**: 13/100
  - Stability observed for 4 of 30 days with no destabilising changes; credit accrues until the full window elapses.
- **Tool Coverage**: 81/100
  - 100% of tools have a non-trivial description (not blank, and not just the tool's name).
  - 42% of tool parameters carry a description.
- **Tool Safety**: 75/100
  - No prompt-injection markers were found in the server instructions, tool names or descriptions we captured.
  - 0 of 9 tool(s) whose name or description implies an irreversible operation declare an MCP destructiveHint annotation; "figma_execute" implies "execute" and declares no destructiveHint at all, which the MCP spec reads as destructive by default.
  - An AI judge read all 96 captured unit(s) of tool text and found none that tries to manipulate the model reading it.
- **Capabilities**: 100/100
  - Implements a supported MCP spec version (2025-11-25); the latest is 2026-07-28.

## Install

### How do I install the io.github.duncanbottrill/deskhand MCP server?

io.github.duncanbottrill/deskhand runs locally as an npm package, launched with npx -y deskhand. Ready-made configuration for Claude, Cursor, VS Code, Codex and 5 more is on this page, copied from each client's own documentation.

### Claude

```bash
claude mcp add duncanbottrill-deskhand -- npx -y deskhand
```

### Cursor

```json
{
  "mcpServers": {
    "duncanbottrill-deskhand": {
      "command": "npx",
      "args": [
        "-y",
        "deskhand"
      ]
    }
  }
}
```

### VS Code

```json
{
  "servers": {
    "duncanbottrill-deskhand": {
      "command": "npx",
      "args": [
        "-y",
        "deskhand"
      ]
    }
  }
}
```

### Codex

```bash
codex mcp add duncanbottrill-deskhand -- npx -y deskhand
```

### opencode

```json
{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "duncanbottrill-deskhand": {
      "type": "local",
      "command": [
        "npx",
        "-y",
        "deskhand"
      ],
      "enabled": true
    }
  }
}
```

### OpenClaw

```bash
openclaw mcp add duncanbottrill-deskhand --command npx --arg -y --arg deskhand
```

### Hermes

```yaml
mcp_servers:
  duncanbottrill-deskhand:
    command: "npx"
    args: ["-y", "deskhand"]
```

### Netclaw

```json
{
  "McpServers": {
    "duncanbottrill-deskhand": {
      "Transport": "stdio",
      "Command": "npx",
      "Arguments": [
        "-y",
        "deskhand"
      ]
    }
  }
}
```

### Vellum

```bash
assistant mcp add duncanbottrill-deskhand -t stdio -c npx -a -y deskhand
```

### Other

```json
{
  "mcpServers": {
    "duncanbottrill-deskhand": {
      "command": "npx",
      "args": [
        "-y",
        "deskhand"
      ]
    }
  }
}
```

## Changelog

Every change recorded for this component, newest first. Days that predate change tracking, or that we cannot explain, say so: "we were watching and nothing happened" and "we were not watching" are different claims.

### 2026-11-05 (score 64, +1)

No change was recorded against any check on this day. Stability & Change Management went from 10 to 13. That category is still filling its 30-day observation window: 3 days of observed history at the previous scan, 4 at this one. The score rises as the window fills, whether or not the server changes.

### 2026-11-03 (score 63, +1)

No change was recorded against any check on this day. Stability & Change Management went from 3 to 7. That category is still filling its 30-day observation window: 1 days of observed history at the previous scan, 2 at this one. The score rises as the window fills, whether or not the server changes.

### 2026-11-01 (score 62)

First indexed and scored.

## MCP tools (94)

### `figma_server_info` (~24 tokens)

Server version, WebSocket port, connection state and connected document info.

### `figma_get_context` (~34 tokens)

File info, current page and selection. Call once per conversation; every tool reports NOT_CONNECTED if the plugin is down.

### `figma_get_selection_history` (~82 tokens)

Recent selection changes, newest first, with timestamps. Default: the user's own selections; source: 'all' adds those made by bridge commands. Served from a server buffer, so it works after the plugin disconnects. If the default returns nothing, retry with source: 'all'.

Input parameters:

- `limit` (integer)
- `source` (string)

### `figma_bridge_diagnostics` (~41 tokens)

Check the bridge end to end: versions, plugin handshake, buffered events, reverse round-trip timing. Call when a tool fails or after an upgrade.

### `figma_licence_status` (~27 tokens)

Deskhand licence state: trial days left, masked key, last check.

### `figma_licence_activate` (~32 tokens)

Activate a Deskhand licence key.

Input parameters:

- `key` (string, required): Licence key from your purchase email

### `figma_execute` (~246 tokens)

Run JavaScript in the Figma plugin: an async function body with figma (Plugin API) and bridge (log, sleep, icon, text, instance, image, snapshot; progress/cancelled in jobs). Return plain data, never nodes (EXECUTE_BAD_RETURN). Errors: EXECUTE_SYNC_API, EXECUTE_SYNTAX, EXECUTE_FAILED. job: true past 30 s, then figma_job_status (waits). screenshot renders it. Recipes: figma_get_design_rules.

Input parameters:

- `code` (string, required): Async function body; return plain data.
- `job` (boolean): Run as a background job and return { jobId }. One at a time (JOB_BUSY). Cap 10 min.
- `label` (string): Job label
- `lint` (boolean): Lint top-level frames the script created; findings come back as _lint (empty = clean).
- `screenshot` (boolean|string): Render a node in the reply: a node id, or true for the returned { id }.
- `timeoutMs` (integer): Sync timeout ms (default 30000); a script still running then becomes a job (jobId returned).

### `figma_job_status` (~95 tokens)

Waits for a figma_execute job to end (up to waitMs), then returns state (running | done | error | cancelled | timeout), progress and the result or error. Jobs live for the server session.

Input parameters:

- `jobId` (string, required): From figma_execute with job: true
- `waitMs` (integer): Max ms to wait for the end (default 20000; 0 = now).

### `figma_job_cancel` (~52 tokens)

Ask a running figma_execute job to stop (sets the flag bridge.cancelled() returns; scripts that never check it run to completion). Poll figma_job_status.

Input parameters:

- `jobId` (string, required)

### `figma_list_pages` (~27 tokens)

List pages (ids and names). The active page is in figma_get_context.

### `figma_get_nodes` (~157 tokens)

Full node details by id: geometry, fills, strokes, effects, auto-layout, text, constraints, boundVariables, explicitVariableModes, childIds. Default-valued props are omitted unless verbose. Composite instance ids (I<inst>;<child>) resolve; unknown ids land in notFound. Find ids with figma_search_nodes/figma_get_children first.

Input parameters:

- `depth` (string): minimal (id, name, type, childIds), compact (+ x/y/size, parentId, childIds), full (all props; default).
- `nodeIds` (array, required): Node ids, e.g. ["1:23"]
- `verbose` (boolean): Include default-valued props and skip caps on long fields

### `figma_set_fills` (~40 tokens)

Set fill color. Accepts hex shorthand or fills array.

Input parameters:

- `fills` (required): Fill color
- `nodeId` (string, required): Node id

### `figma_set_strokes` (~157 tokens)

Set stroke color and/or weight. Per-side weights (strokeTopWeight etc., for border-top-only dividers) work on RECTANGLE and frame-like nodes only. Omit `strokes` to change weights only.

Input parameters:

- `nodeId` (string, required): Node id
- `strokeBottomWeight` (number)
- `strokeLeftWeight` (number)
- `strokeRightWeight` (number)
- `strokeTopWeight` (number)
- `strokeWeight` (number): Uniform weight (px), applied before per-side weights
- `strokes`: { color: "#RRGGBB[AA]" } | { r, g, b, a? } (0–1) | Figma fills array

### `figma_create_rectangle` (~70 tokens)

Create a rectangle.

Input parameters:

- `fills`: Fill color
- `height` (number)
- `name` (string)
- `parentId` (string): Parent id (default: page)
- `width` (number)
- `x` (number)
- `y` (number)

### `figma_set_text` (~37 tokens)

Set text content. Auto-loads fonts.

Input parameters:

- `nodeId` (string, required): Text node id
- `text` (string, required)

### `figma_clone_nodes` (~45 tokens)

Duplicate nodes.

Input parameters:

- `nodeIds` (array, required)
- `offset` (object): Offset from the originals
- `parentId` (string): Parent id for the clones

### `figma_delete_nodes` (~21 tokens)

Delete nodes.

Input parameters:

- `nodeIds` (array, required)

### `figma_move_nodes` (~70 tokens)

Move nodes. Use relative=true for offset.

Input parameters:

- `nodeIds` (array, required)
- `relative` (boolean): x/y are offsets from the current position
- `x` (number): Absolute X, or offset if relative
- `y` (number): Absolute Y, or offset if relative

### `figma_resize_nodes` (~106 tokens)

Resize nodes to explicit pixels. To fit a child to its parent prefer figma_set_layout_align STRETCH (keeps size binds). Instance sublayers are rejected (INSTANCE_SUBLAYER_RESTRICTED): resize the master. Size binds are re-applied; the result is read back: RESIZE_NO_OP for a no-op, a clamp is a warning.

Input parameters:

- `height` (number)
- `nodeIds` (array, required)
- `width` (number)

### `figma_set_size_limits` (~115 tokens)

Set or clear minWidth/maxWidth/minHeight/maxHeight: a number sets, null clears, omit leaves it. Applies to auto-layout frames and their direct children (others get a warning). Verified by readback (LIMIT_NOT_APPLIED / LIMIT_NOT_CLEARED). A variable-bound field keeps its bound value; unbind with figma_unbind_variable.

Input parameters:

- `maxHeight`
- `maxWidth`
- `minHeight`
- `minWidth`
- `nodeIds` (array, required)

### `figma_set_opacity` (~35 tokens)

Set opacity (0-1).

Input parameters:

- `nodeId` (string, required): Node id
- `opacity` (number, required)

### `figma_set_visible` (~47 tokens)

Show or hide nodes via node.visible; don't bind a BOOLEAN variable or set opacity to 0 for this.

Input parameters:

- `nodeIds` (array, required)
- `visible` (boolean, required)

### `figma_set_clips_content` (~57 tokens)

Set whether frame-like nodes (FRAME, COMPONENT, COMPONENT_SET, INSTANCE, SLOT, SLIDE) clip children to their bounds; others error.

Input parameters:

- `clipsContent` (boolean, required)
- `nodeIds` (array, required)

### `figma_set_corner_radius` (~70 tokens)

Set corner radius. Use individual values for asymmetric.

Input parameters:

- `bottomLeft` (number)
- `bottomRight` (number)
- `nodeId` (string, required): Node id
- `radius` (number)
- `topLeft` (number)
- `topRight` (number)

### `figma_group_nodes` (~28 tokens)

Group nodes.

Input parameters:

- `name` (string)
- `nodeIds` (array, required)

### `figma_ungroup_nodes` (~24 tokens)

Ungroup nodes.

Input parameters:

- `nodeIds` (array, required)

### `figma_create_frame` (~70 tokens)

Create a frame.

Input parameters:

- `fills`: Fill color
- `height` (number)
- `name` (string)
- `parentId` (string): Parent id (default: page)
- `width` (number)
- `x` (number)
- `y` (number)

### `figma_create_text` (~88 tokens)

Create a text node.

Input parameters:

- `fills`: Text color
- `fontFamily` (string)
- `fontSize` (number)
- `fontStyle` (string)
- `name` (string)
- `parentId` (string): Parent id (default: page)
- `text` (string)
- `x` (number)
- `y` (number)

### `figma_set_selection` (~36 tokens)

Set selection. Empty array clears.

Input parameters:

- `nodeIds` (array, required): Array of node IDs to select (empty array to clear)

### `figma_set_current_page` (~30 tokens)

Switch to a different page in the Figma document.

Input parameters:

- `pageId` (string, required)

### `figma_export_node` (~155 tokens)

Export a node as PNG, SVG, JPG or PDF to disk and return { path, format, scale, bytes }; read the file to view it. outputPath sets the destination; returnBase64: true inlines raw data instead. To see the render in context use figma_screenshot.

Input parameters:

- `format` (string)
- `nodeId` (string, required)
- `outputPath` (string): Absolute output path (directories created). Default: auto-named file in the OS temp dir.
- `returnBase64` (boolean): Return base64 inline instead of a file (rarely useful)
- `scale` (number): Scale (1 = 100%), clamped to 0.01–4

### `figma_screenshot` (~146 tokens)

Render a node (or the selection) as PNG and return it inline, with path, pixel size and scale. Scale is reduced to fit maxPixels (default 1,000,000); the inline image stays under 200 KB (one retry at half scale), larger renders stay on disk. Use after layout changes: readback cannot see overlap, clipping or mid-word breaks.

Input parameters:

- `inline` (boolean): false: write to disk only and return the path
- `maxPixels` (integer): Pixel budget (width × height)
- `nodeId` (string): Default: the first selected node
- `scale` (number): Export scale (1 = 100%)

### `figma_compare_to_reference` (~144 tokens)

Pixel-diff a built frame against a reference node. Both render at the reference's effective scale; returns mismatchPercent, a per-quadrant breakdown (tl/tr/bl/br), the size delta and paths to the diff/ref/built PNGs. A mismatch in one quadrant usually means one misplaced element.

Input parameters:

- `builtNodeId` (string, required): Built frame
- `inlineDiff` (boolean): Also return the diff PNG inline
- `maxPixels` (integer)
- `refNodeId` (string, required): Reference node
- `scale` (number)
- `threshold` (number): pixelmatch colour threshold (0 = exact, 1 = anything matches)

### `figma_export_batch` (~128 tokens)

Export many nodes in one call, each at a scale or targetWidth (scale = targetWidth / node width, clamped 0.01–4). Files go to outputDir (default: a fresh folder under ~/.deskhand/exports/) named by namePattern with tokens {name} {id} {scale} {ext} {index}. One row per item (path and size, or an error); nothing is inlined.

Input parameters:

- `items` (array, required)
- `namePattern` (string)
- `outputDir` (string): Absolute directory (created if missing)

### `figma_resolve_icon` (~85 tokens)

Check icon concepts against the bundled Lucide set before drawing anything; never hand-draw glyphs. Aliases resolve (close → x), then exact names; otherwise you get real-name suggestions: pick one, do not invent. Insert with figma_place_icon or bridge.icon.

Input parameters:

- `concepts` (array, required): Concept names, e.g. ["search", "close"]

### `figma_place_icon` (~128 tokens)

Insert a Lucide icon as a vector frame named icon/<concept>. Resolves like figma_resolve_icon. Appended to parentId (or the current page) at x/y, scaled and stroked.

Input parameters:

- `color` (string): Hex colour (default black)
- `concept` (string, required): Icon concept, e.g. "search"
- `parentId` (string): Parent frame id (default: page)
- `size` (number): Rendered size in px (icons are 24 px squares)
- `x` (number)
- `y` (number)

### `figma_place_chrome` (~108 tokens)

Insert an iOS chrome template (status-bar 393×54, home-indicator 393×34, keyboard 393×291), named chrome/<kind>. With parentId it scales to the parent width, top or bottom unless y is given.

Input parameters:

- `kind` (string, required)
- `parentId` (string): Screen frame id (default: page)
- `theme` (string): dark on a dark screen
- `y` (number): Vertical position inside the parent

### `figma_review_screen` (~119 tokens)

Per-screen quality gate: lint one frame → { pass, findings, counts, fixes }; a finding carries its own fix only when it differs from fixes[code]. Errors: invisible text, overflow, zero-size, collapsed auto-layout, low contrast. Warnings: misaligned rows, hand-drawn icons, placeholder avatars, raw fills/text, detached instances. Capped at 200, errors first.

Input parameters:

- `checks` (array): Restrict to these codes (default: all)
- `nodeId` (string, required): The frame to review

### `figma_brand_styles` (~84 tokens)

Read the brand the open file already defines: local paint/text/effect styles, every component and set (keys, variant properties, all pages) and common frame sizes; samples fills and fonts when there are no styles. Use its colours, type and components in everything you build. Cached under ~/.deskhand/brands/ so bridge.instance(name) can import components later.

### `figma_set_image_fill` (~125 tokens)

Fill a node with an image from an absolute path (~/ allowed) or http(s) URL: PNG, JPEG, GIF or WebP, up to 4096 px per side and 20 MB. Replaces the fills with one IMAGE fill. Use for photos, avatars and reference art instead of placeholder shapes; bridge.image does the same in scripts.

Input parameters:

- `nodeId` (string, required): Any node with fills
- `scaleMode` (string): FILL (cover, default), FIT (contain), CROP, TILE
- `source` (string, required)

### `figma_snapshot` (~91 tokens)

Save a restorable copy of a node before a risky edit: cloned onto a "🗄 Bridge Snapshots" page with parent/index/position recorded. Keeps the newest 10 per file. Restore with figma_restore_snapshot or bridge.snapshot() in scripts.

Input parameters:

- `label` (string): Short note shown in the list, e.g. "before restyle"
- `nodeId` (string, required)

### `figma_restore_snapshot` (~83 tokens)

Undo to a snapshot: removes the current node (if present) and moves the saved clone back to its recorded parent/index/position/name. New id: restoredId — update ids you held. Snapshot is consumed. SNAPSHOT_NOT_FOUND if unknown or the clone was deleted.

Input parameters:

- `snapshotId` (string, required): From figma_snapshot or figma_list_snapshots

### `figma_list_snapshots` (~37 tokens)

List saved snapshots in this file, newest first, with whether each clone still exists. Returns { count, page, snapshots }.

### `figma_get_design_rules` (~79 tokens)

One cookbook section as markdown: rules plus a runnable figma_execute recipe. Sections: helpers, auto-layout, components, variants, text, lint, jobs, reference-match. Read it before building that kind of thing. Whole cookbook: skill://deskhand/COOKBOOK.md. Needs no plugin.

Input parameters:

- `section` (string, required)

### `figma_create_ellipse` (~107 tokens)

Create ellipse. Use arcData for arcs/rings.

Input parameters:

- `arcData` (object): Arcs and rings
- `fills`: Fill color
- `height` (number): Height in pixels (same as width for circle)
- `name` (string)
- `parentId` (string): Parent id (default: page)
- `width` (number): Width in pixels (diameter for circle)
- `x` (number)
- `y` (number)

### `figma_set_effects` (~35 tokens)

Set effects. Replaces existing.

Input parameters:

- `effects` (array, required)
- `nodeId` (string, required): Node id

### `figma_set_auto_layout` (~185 tokens)

Configure auto-layout on a frame: direction, sizing modes, alignment, padding, spacing, wrap. Size variable binds are captured and re-applied (rebound/warnings). See skill://deskhand/SKILL.md.

Input parameters:

- `counterAxisAlignItems` (string)
- `counterAxisSizingMode` (string)
- `counterAxisSpacing` (number): Space between wrapped rows
- `itemSpacing` (number)
- `layoutMode` (string): NONE disables; HORIZONTAL = row, VERTICAL = column
- `layoutWrap` (string)
- `nodeId` (string, required)
- `paddingBottom` (number)
- `paddingLeft` (number)
- `paddingRight` (number)
- `paddingTop` (number)
- `primaryAxisAlignItems` (string)
- `primaryAxisSizingMode` (string)

### `figma_get_local_styles` (~76 tokens)

List all local styles (colors, text, effects, grids). To find specific styles by name use figma_search_styles: fewer tokens.

Input parameters:

- `limit` (integer): Per category
- `offset` (integer)
- `type` (string)
- `verbose` (boolean): Full paints array, not compact hex[]

### `figma_apply_style` (~79 tokens)

Apply a local style (paint, text, stroke, effect, grid) to a node using Figma's async setters. The applied id is read back: appliedStyleId with verified: true, or STYLE_NOT_APPLIED.

Input parameters:

- `nodeId` (string, required)
- `property` (string, required)
- `styleId` (string, required)

### `figma_create_component` (~92 tokens)

Create a component.

Input parameters:

- `description` (string)
- `fills`: Fill color
- `fromNodeId` (string): Convert this node to a component
- `height` (number)
- `name` (string)
- `parentId` (string): Parent id (default: page)
- `width` (number)
- `x` (number)
- `y` (number)

### `figma_create_instance` (~62 tokens)

Create a component instance.

Input parameters:

- `componentId` (string, required)
- `name` (string): Default: component name
- `parentId` (string): Parent id (default: page)
- `x` (number)
- `y` (number)

### `figma_get_local_variables` (~83 tokens)

All local variables and collections: types (COLOR, FLOAT, STRING, BOOLEAN), modes, values. Paged: limit (default 100) and offset; next gives the following page.

Input parameters:

- `limit` (integer): Maximum number of variables to return (collections are always returned in full)
- `offset` (integer)
- `type` (string)

### `figma_search_variables` (~127 tokens)

Search variables by name pattern (e.g. "tailwind/orange/*"). Compact results, far fewer tokens than figma_get_local_variables.

Input parameters:

- `collectionName` (string): Collection name (partial ok)
- `compact` (boolean): Minimal data (id, name, hex/value); false for full metadata
- `limit` (number)
- `nameContains` (string): Case-insensitive substring
- `namePattern` (string): Wildcard pattern, e.g. "tailwind/orange/*"
- `offset` (integer)
- `type` (string)

### `figma_search_nodes` (~156 tokens)

Search nodes by name within a scope (parentId required). Preferred for finding frames, sections or elements; compact results (~50 tokens/node).

Input parameters:

- `compact` (boolean): Return minimal data (id, name, type, parentId, childCount)
- `limit` (number)
- `maxDepth` (number): -1 = unlimited, 1 = immediate children
- `nameContains` (string): Case-insensitive substring
- `namePattern` (string): Glob, e.g. "*button*", "Header/*"
- `offset` (integer)
- `parentId` (string, required): Scope: page/frame/section id
- `types` (array): Node types, e.g. FRAME, TEXT, INSTANCE

### `figma_search_components` (~76 tokens)

Search local components by name. Returns compact metadata.

Input parameters:

- `compact` (boolean)
- `includeVariants` (boolean): Include variants from component sets
- `limit` (number)
- `nameContains` (string): Case-insensitive substring
- `namePattern` (string): Glob pattern
- `offset` (integer)

### `figma_search_styles` (~64 tokens)

Search local styles by name; cheaper than figma_get_local_styles.

Input parameters:

- `compact` (boolean)
- `limit` (number)
- `nameContains` (string): Case-insensitive substring
- `offset` (integer)
- `type` (string)

### `figma_get_children` (~115 tokens)

Immediate children of a node — more efficient than figma_get_nodes for browsing hierarchy. Default limit 100, with truncated/next for paging. Compact results include x/y for layout. Composite instance-sublayer parent IDs resolve too.

Input parameters:

- `compact` (boolean): Minimal data (id, name, type, x, y, parentId, childCount); false for full serialization.
- `limit` (integer): Maximum number of children to return
- `offset` (integer)
- `parentId` (string, required)

### `figma_set_variable` (~235 tokens)

Set a variable's value for a mode, or bind it to a node property or local style (styleId + field). Text styles bind font/spacing fields; paint styles bind 'paints'. Verified by readback (BIND_NOT_APPLIED otherwise). width/height on an instance sublayer: INSTANCE_SUBLAYER_RESTRICTED, bind the master.

Input parameters:

- `field` (string): Property or text field (fontSize, lineHeight, letterSpacing, paragraphSpacing, paragraphIndent, fontFamily, fontStyle, fontWeight), or 'paints' for paint styles
- `modeId` (string): Required when setting a value
- `nodeId` (string): Node to bind to (a style id here routes to the style path)
- `paintIndex` (number): Paint index for fills, strokes or a paint style
- `styleId` (string): Local style id (e.g. "S:abc123") to bind a TEXT or PAINT style, instead of nodeId
- `value`: number, string, boolean or color object
- `variableId` (string, required)

### `figma_create_line` (~104 tokens)

Create a line.

Input parameters:

- `length` (number)
- `name` (string)
- `parentId` (string): Parent id (default: page)
- `rotation` (number): Degrees (0 = horizontal)
- `strokeCap` (string): ARROW_LINES/ARROW_EQUILATERAL for arrows
- `strokeWeight` (number)
- `strokes`: Stroke color
- `x` (number)
- `y` (number)

### `figma_set_constraints` (~95 tokens)

Set resize constraints on a node: how it resizes when its parent frame resizes. Only for nodes inside frames (not auto-layout frames).

Input parameters:

- `horizontal` (string): MIN = left, MAX = right, STRETCH = both, SCALE = proportional
- `nodeId` (string, required)
- `vertical` (string): MIN = top, MAX = bottom, STRETCH = both, SCALE = proportional

### `figma_detach_instance` (~40 tokens)

Detach an instance into a regular frame: keeps overrides, severs the link to the main component.

Input parameters:

- `nodeId` (string, required)

### `figma_set_layout_align` (~110 tokens)

Set how a child behaves in an auto-layout frame: alignment (STRETCH), growth (fill container) and absolute positioning.

Input parameters:

- `layoutAlign` (string): Counter-axis alignment: STRETCH to fill width/height
- `layoutGrow` (number): Primary-axis growth: 0 = fixed size, 1 = fill available space
- `layoutPositioning` (string): AUTO = follow auto-layout, ABSOLUTE = manually positioned
- `nodeId` (string, required)

### `figma_rename_node` (~47 tokens)

Rename one or more nodes (a batch shares one name).

Input parameters:

- `name` (string, required)
- `nodeId` (string)
- `nodeIds` (array): Batch

### `figma_reorder_node` (~102 tokens)

Change a node's z-order among its siblings. position is the FINAL index (0 = back/bottom, childCount-1 = front/top); 'back' and 'front' also work. Out-of-range values are clamped. Verified by readback (REORDER_FAILED otherwise). Children of instances cannot be reordered (INSTANCE_SUBLAYER_RESTRICTED): reorder on the master.

Input parameters:

- `nodeId` (string, required)
- `position` (required)

### `figma_set_text_style` (~116 tokens)

Set text font properties.

Input parameters:

- `fontFamily` (string)
- `fontSize` (number)
- `fontStyle` (string)
- `letterSpacing`: Letter spacing (PIXELS or PERCENT with value)
- `lineHeight`: Line height (AUTO, or PIXELS/PERCENT with value)
- `nodeId` (string, required)
- `textAlignHorizontal` (string)
- `textAlignVertical` (string)
- `textCase` (string)
- `textDecoration` (string)

### `figma_create_paint_style` (~49 tokens)

Create a paint style.

Input parameters:

- `description` (string)
- `fills` (required): Fill color
- `name` (string, required): "/" makes folders, e.g. "Brand/Primary"

### `figma_create_text_style` (~85 tokens)

Create a text style.

Input parameters:

- `description` (string)
- `fontFamily` (string)
- `fontSize` (number)
- `fontStyle` (string)
- `letterSpacing`
- `lineHeight`
- `name` (string, required): "/" makes folders
- `textCase` (string)
- `textDecoration` (string)

### `figma_delete_style` (~50 tokens)

Delete a local style (paint, text, effect or grid). Library styles return REMOTE_STYLE. Nodes keep their resolved values but lose the link.

Input parameters:

- `styleId` (string, required): Style id

### `figma_create_variable_collection` (~36 tokens)

Create a variable collection.

Input parameters:

- `modes` (array): Default: ["Mode 1"]
- `name` (string, required)

### `figma_create_variable` (~92 tokens)

Create a variable in a collection.

Input parameters:

- `aliasOf` (string): Variable id to alias instead of a value
- `collectionId` (string, required)
- `description` (string)
- `name` (string, required): "/" makes groups, e.g. "colors/primary"
- `scopes` (array): Allowed scopes
- `type` (string, required)
- `value`: Initial value (default mode)

### `figma_rename_variable` (~34 tokens)

Rename a variable; "/" makes groups.

Input parameters:

- `name` (string, required)
- `variableId` (string, required)

### `figma_delete_variables` (~25 tokens)

Delete variables. Cannot be undone.

Input parameters:

- `variableIds` (array, required)

### `figma_delete_variable_collection` (~32 tokens)

Delete a variable collection and all its variables. Cannot be undone.

Input parameters:

- `collectionId` (string, required)

### `figma_rename_variable_collection` (~32 tokens)

Rename a variable collection.

Input parameters:

- `collectionId` (string, required)
- `name` (string, required)

### `figma_rename_mode` (~42 tokens)

Rename a mode in a variable collection.

Input parameters:

- `collectionId` (string, required)
- `modeId` (string, required)
- `name` (string, required)

### `figma_add_mode` (~34 tokens)

Add a new mode to a variable collection.

Input parameters:

- `collectionId` (string, required)
- `name` (string, required)

### `figma_delete_mode` (~40 tokens)

Delete a mode from a variable collection. Cannot delete the last mode.

Input parameters:

- `collectionId` (string, required)
- `modeId` (string, required)

### `figma_set_variable_mode` (~123 tokens)

Pin an explicit variable mode on nodes or pages (e.g. a mobile frame pinned to the Spacing collection's mobile mode), or clear: true to unpin. Works on scene nodes and page ids. Echoes explicitVariableModes ({} = nothing pinned).

Input parameters:

- `clear` (boolean): Remove this collection's pin instead of setting one
- `collectionId` (string, required): Pins are per collection
- `modeId` (string): Required unless clear is true; must belong to collectionId
- `nodeIds` (array, required): Node or page ids

### `figma_unbind_variable` (~100 tokens)

Remove a variable binding from a node property, verified by readback (UNBIND_FAILED otherwise). For minWidth/maxWidth/minHeight/maxHeight the leftover literal clamp is cleared too (previousLiteral, clearedLiteral).

Input parameters:

- `field` (string, required): e.g. fills, strokes, opacity, cornerRadius, minWidth, maxWidth
- `nodeId` (string, required)
- `paintIndex` (number): Paint index for fills/strokes

### `figma_create_page` (~63 tokens)

Create a page. Figma Design only: FigJam returns FIGMA_DESIGN_ONLY (create FigJam pages in its UI).

Input parameters:

- `index` (number): Position in the page list (0 = first). Defaults to end.
- `name` (string, required)

### `figma_rename_page` (~30 tokens)

Rename a page.

Input parameters:

- `name` (string, required)
- `pageId` (string, required)

### `figma_delete_page` (~29 tokens)

Delete a page. The last page cannot be deleted.

Input parameters:

- `pageId` (string, required)

### `figma_reparent_nodes` (~62 tokens)

Move nodes to a different parent.

Input parameters:

- `index` (number): Position within the new parent (0 = bottom/back). Defaults to top/front.
- `newParentId` (string, required): Frame, group or page
- `nodeIds` (array, required)

### `figma_move_to_page` (~53 tokens)

Move nodes from their current page to a different page.

Input parameters:

- `nodeIds` (array, required)
- `targetPageId` (string, required)
- `x` (number)
- `y` (number)

### `figma_swap_instance` (~40 tokens)

Swap an instance to a different component, keeping position and size.

Input parameters:

- `instanceId` (string, required)
- `newComponentId` (string, required)

### `figma_duplicate_page` (~57 tokens)

Clone a page and its contents, inserted after the original. Figma Design only (FIGMA_DESIGN_ONLY in FigJam).

Input parameters:

- `name` (string): Default: "<original> copy"
- `pageId` (string, required)

### `figma_set_rotation` (~103 tokens)

Rotate nodes by degrees. pivot 'center' (default) keeps the visual centre in place; 'top-left' is Figma's raw rotation, which moves the centre. Children of auto-layout parents (unless ABSOLUTE) cannot use a centre pivot: they get top-left plus a warning. Echoes appliedPivot, rotation and absoluteBoundingBox.

Input parameters:

- `nodeIds` (array, required)
- `pivot` (string)
- `rotation` (number, required)

### `figma_combine_as_variants` (~45 tokens)

Combine components into a variant set; names must use variant naming (e.g. "Size=Large").

Input parameters:

- `componentIds` (array, required): Minimum 2

### `figma_create_section` (~139 tokens)

Create a labeled section (works in both Figma Design and FigJam) to group content. Supports dev status (READY_FOR_DEV/COMPLETED).

Input parameters:

- `devStatus` (string): Only valid on sections directly under a page or section
- `devStatusDescription` (string)
- `fills`: Section background fill
- `height` (number)
- `name` (string)
- `parentId` (string): Parent id (default: page)
- `sectionContentsHidden` (boolean): Collapse the section's contents
- `width` (number)
- `x` (number)
- `y` (number)

### `figma_set_section` (~70 tokens)

Update a section's name, dev status, or content visibility. Pass devStatus: null to clear.

Input parameters:

- `devStatus`
- `devStatusDescription` (string)
- `name` (string)
- `nodeId` (string, required)
- `sectionContentsHidden` (boolean)

### `figma_get_reactions` (~37 tokens)

Prototype (Figma Design only): get a node's reactions (trigger and action details).

Input parameters:

- `nodeId` (string, required)

### `figma_add_reaction` (~52 tokens)

Prototype (Figma Design only): add an interaction to a node; existing reactions are kept.

Input parameters:

- `action` (object, required)
- `nodeId` (string, required)
- `trigger` (object, required)

### `figma_remove_reaction` (~52 tokens)

Prototype (Figma Design only): remove a reaction by its zero-based index; find it with figma_get_reactions first.

Input parameters:

- `index` (integer, required)
- `nodeId` (string, required)

### `figma_set_flow_starting_point` (~91 tokens)

Prototype (Figma Design only): set a top-level frame as a flow starting point on the page, or clear it. Stored page-level as { nodeId, name } entries.

Input parameters:

- `clear` (boolean)
- `flowName` (string): Default "Flow 1"; renames an existing starting point
- `nodeId` (string, required): FRAME, COMPONENT or COMPONENT_SET

## Diagnostics

Captured diagnostic sections: Provenance, Dependencies. The full working is on the page: https://verifymcp.io/servers/duncanbottrill-deskhand/deskhand#diagnostics

## Score history

- 2026-11-05: 64
- 2026-11-04: 63
- 2026-11-03: 63
- 2026-11-02: 62
- 2026-11-01: 62

## Common questions

### What is the io.github.duncanbottrill/deskhand MCP server?

io.github.duncanbottrill/deskhand is an MCP server listed in the public MCP registry as io.github.duncanbottrill/deskhand. Claude in your Figma file. Builds with your components, checks what it wrote. Runs on your computer. This page covers its npm package (deskhand).

### Is the io.github.duncanbottrill/deskhand MCP server safe to use?

io.github.duncanbottrill/deskhand scores 64 out of 100 on VerifyMCP. We found no known CVEs affecting it as of 5 November 2026. It declares no install or post-install scripts. That is a record of what we were able to check automatically, not an endorsement. The category breakdown on this page shows every signal behind the number, including the ones we could not confirm.

### What tools does the io.github.duncanbottrill/deskhand MCP server expose?

io.github.duncanbottrill/deskhand exposes 94 tools: figma_server_info, figma_get_context, figma_get_selection_history, figma_bridge_diagnostics, figma_licence_status, and 89 more. Their descriptions and schemas cost roughly 7,278 tokens of context every time the server is loaded.

### Is the io.github.duncanbottrill/deskhand MCP server still maintained?

io.github.duncanbottrill/deskhand is still listed as active in the MCP registry. We last reached this channel on 5 November 2026. Those dates come from our own scans of the registry and the channel itself, not from anything the publisher announced.

### What licence is the io.github.duncanbottrill/deskhand MCP server under?

io.github.duncanbottrill/deskhand declares the MIT licence, which is OSI-approved. That covers the source only, and says nothing about the cost of any service it calls.

## Links

- npm package: https://www.npmjs.com/package/deskhand
- Socket report: https://socket.dev/npm/package/deskhand
- Website: https://deskhand.studio/
- Changelog RSS feed: https://verifymcp.io/servers/duncanbottrill-deskhand/deskhand.xml
- Changelog JSON feed: https://verifymcp.io/servers/duncanbottrill-deskhand/deskhand.json
- HTML version of this page: https://verifymcp.io/servers/duncanbottrill-deskhand/deskhand
