| name | using-pencil |
| description | Control Pencil.dev design tool via MCP to create, inspect, and export UI designs programmatically. Covers batch_design DSL, design tokens, components, and code sync. Use when working with .pen files, Pencil MCP, design systems, or mockups. |
Using Pencil
Control Pencil (pencil.dev) via its MCP server. Create designs, manage tokens, build components, and export assets — all from the CLI.
Prerequisites
- Pencil desktop app running (
/Applications/Pencil.app)
- Dependencies:
cd <skill-path>/scripts && npm install && npm run build
MCP Client
Two modes available:
CLI Mode (one-off calls)
node <skill-path>/scripts/pencil.cjs call <tool_name> '<json_args>'
cd <skill-path>/scripts && npm run build
Library Mode (complex workflows)
For scripts with multiple operations, import helpers directly:
import { PencilClient } from './pencil.ts'
import { batch, screenshot, getNodes, setTokens } from './helpers.ts'
const pencil = new PencilClient()
await pencil.connect()
const nodes = await getNodes(pencil)
const { insertedIds } = await batch(pencil, 'btn=I(document,{type:"frame",name:"Btn"})')
await screenshot(pencil, insertedIds[0], './out/btn.png')
await pencil.disconnect()
Run with: npx tsx your-script.ts
Benefits: Type safety, no subprocess overhead, cleaner code.
Tools (15)
| Tool | Use for |
|---|
batch_design | Insert/update/delete nodes (custom DSL, max 25 ops) |
batch_get | Read nodes by ID or pattern (watch depth!) |
get_editor_state | Current file, selection, components (~9K tokens — schema tax!) |
get_screenshot | Visual verification — always call after batch_design. Use { maxWidth: 800 } by default (see mcp-optimization.md for decision rules) |
get_variables / set_variables | Read/write design tokens (max 5-10 vars per call) |
snapshot_layout | Debug layout (overflow, positioning) |
replace_all_matching_properties | Bulk property swap (hex → variable refs) |
search_all_unique_properties | Audit unique values (colors, fonts, sizes) |
export_nodes | Export PNG/JPEG/PDF |
get_guidelines | Design rules by topic (web-app, tailwind, code, etc.) |
get_style_guide / get_style_guide_tags | Style inspiration by tags |
open_document | Open/create .pen file |
find_empty_space_on_canvas | Find free canvas space |
Recommended Workflow
1. get_editor_state → Understand current state (call ONCE per session)
2. get_variables → Read existing tokens
3. set_variables → Create/modify tokens (batches of 5-10)
4. batch_design → Build layout (max 25 ops per call)
5. get_screenshot → ALWAYS verify visually
6. batch_design → Fix/iterate based on screenshot
7. export_nodes → Export when ready
After step 1: Use batch_get depth 0 instead of get_editor_state for subsequent state checks (saves ~9K tokens).
Top Gotchas
- OKLCH = invisible — Only hex colors render (
#RRGGBB or #RRGGBBAA)
- Schema tax —
get_editor_state returns ~9K tokens of static schema every call
- batch_get depth explodes — depth 0: ~700 tokens → depth 3: ~26K tokens
- No image node type — Images are fills:
{fill: {type: "image", url: "..."}}
- No Figma variants — Use Shell Pattern for color variants, separate components for structural variants (see
references/components.md → Component Composition)
- Variables can't be deleted —
null errors; ignore orphans
- Always screenshot —
batch_design returns no visual preview
batch_design DSL (Quick Ref)
name=I(parent, {nodeData}) # Insert (MUST have binding name)
U("nodeId", {updateData}) # Update
name=C("sourceId", parent, {}) # Copy
name=R("nodeId", {nodeData}) # Replace
M("nodeId", parent, index?) # Move
D("nodeId") # Delete
G("nodeId", "ai"|"stock", "prompt") # Image fill
document = root binding. Separate operations with \n. No id property on new nodes.
Detailed References
Load as needed — each file is self-contained:
references/dsl.md — Complete DSL syntax, node types, layout, graphics, text, icons, examples
references/gotchas.md — All pitfalls: colors, layout, images, components, performance, MCP output
references/tokens.md — Design tokens (2-level architecture), theming, bulk swap, Pencil ↔ code sync
references/pen-schema.md — .pen format TypeScript schema (authoritative, from official docs)
references/components.md — Components, instances, descendants, slots, Shell Pattern for variants, component composition
references/mcp-optimization.md — Token audit results, cost per tool, optimization strategies
Examples
TypeScript with helpers (recommended for complex workflows)
import { PencilClient } from './pencil.ts'
import { batch, screenshot, getNodes, setTokens, findNode } from './helpers.ts'
const pencil = new PencilClient()
await pencil.connect()
await setTokens(pencil, {
'primary-500': { type: 'color', value: '#3D7A4F' },
'font-body': { type: 'string', value: 'Inter' },
'radius-md': { type: 'number', value: 12 },
})
const { insertedIds } = await batch(pencil, `
card=I(document,{type:"frame",name:"Component/Card",reusable:true,x:2000,y:0,width:280,layout:"vertical",gap:8,padding:[16,16],fill:"#FFFFFF",cornerRadius:12})
title=I(card,{type:"text",content:"Hello",fontSize:16,fontWeight:"700",fill:"$primary-500"})
`)
await screenshot(pencil, insertedIds[0], './exports/card.png')
await pencil.disconnect()
Component family with Shell Pattern (color variants)
import { PencilClient } from './pencil.ts'
import { createComponentFamily, batch, screenshot } from './helpers.ts'
const pencil = new PencilClient()
await pencil.connect()
const family = await createComponentFamily(pencil, {
shell: {
name: 'Card',
width: 300, layout: 'vertical', gap: 12, padding: [20, 20],
cornerRadius: 12, fill: '#FFFFFF',
children: [
{ type: 'text', name: 'card-title', content: 'Title', fontSize: 16, fontWeight: '600', fill: '#1A2B1F' },
],
slot: { name: 'card-content', height: 'fit_content(80)', layout: 'vertical', gap: 8 },
},
variants: [
{ name: 'Green', fill: '#F0F7F2' },
{ name: 'Promo', fill: '#FFF5F0', stroke: { fill: '#FF8C00', thickness: 2, align: 'inside' } },
],
})
await batch(pencil, `I("parentFrame", {type:"ref", ref:"${family.variants[0].id}"})`)
await batch(pencil, `U("${family.shell.id}", {padding:[24,24], gap:16})`)
await pencil.disconnect()
See references/components.md → Component Composition for full documentation.
CLI (one-off calls)
Create a card component
node pencil.cjs call batch_design '{
"filePath": "design.pen",
"operations": "card=I(document,{type:\"frame\",name:\"Component/Card\",reusable:true,width:300,height:200,x:2000,y:0,layout:\"vertical\",gap:8,padding:[16,16],fill:\"$surface-card\",cornerRadius:16})\ntitle=I(card,{type:\"text\",content:\"Title\",fontFamily:\"$font-display\",fontSize:18,fontWeight:\"600\",fill:\"$text-primary\"})\ndesc=I(card,{type:\"text\",content:\"Description text\",fontFamily:\"$font-body\",fontSize:14,fill:\"$text-secondary\",textGrowth:\"fixed-width\",width:\"fill_container\"})"
}'
Set up design tokens (2-level)
node pencil.cjs call set_variables '{
"filePath": "design.pen",
"variables": {
"primary-500": {"type": "color", "value": "#FF8C00"},
"neutral-50": {"type": "color", "value": "#FFFBF6"},
"neutral-800": {"type": "color", "value": "#3D2B1F"},
"font-body": {"type": "string", "value": "Inter"},
"radius-md": {"type": "number", "value": 12}
}
}'
node pencil.cjs call set_variables '{
"filePath": "design.pen",
"variables": {
"surface-card": {"type": "color", "value": "#FFFFFF"},
"text-primary": {"type": "color", "value": "$neutral-800"},
"text-secondary": {"type": "color", "value": "$neutral-500"},
"surface-body": {"type": "color", "value": "$neutral-50"}
}
}'
Swap hardcoded colors → variables
node pencil.cjs call search_all_unique_properties '{
"filePath": "design.pen",
"parents": ["frameId"],
"properties": ["fillColor"]
}'
node pencil.cjs call replace_all_matching_properties '{
"filePath": "design.pen",
"parents": ["frameId"],
"properties": {"fillColor": [{"from": "#3D2B1F", "to": "$text-primary"}]}
}'