Figma MCP
Designers bind haptic presets to layers with the Pulsar Haptics Figma plugin. When you implement that screen — by hand or with a coding agent driving the Figma MCP server — those haptics should land in the app too. This page is the runbook for that: where the bindings live, how to read them, and what to call.
Why the Figma MCP alone isn’t enough
Section titled “Why the Figma MCP alone isn’t enough”The Figma MCP’s design tools (get_design_context, get_metadata,
get_screenshot) return visuals, layout, and text — never haptics. A preset
binding is not a visual property, so nothing in the design context hints at it.
Pulsar writes each binding into the file as shared plugin data on the bound
node. That is readable by anything with file access, including the Figma MCP’s
use_figma tool — but only if you go and ask for it. So implementing haptics is
one extra read, joined to the UI you just generated by node ID.
1. Connect the Figma MCP
Section titled “1. Connect the Figma MCP”The hosted server needs no local Figma app:
claude mcp add --transport http figma https://mcp.figma.com/mcpCodex:
codex mcp add figma --url https://mcp.figma.com/mcpThere is also a desktop server at http://127.0.0.1:3845/mcp, enabled from the
Figma desktop app under Dev Mode → Enable desktop MCP server. Either one works
here; the remote server is Figma’s recommended default.
2. Install the Pulsar skill
Section titled “2. Install the Pulsar skill”The pulsar-haptics skill teaches your agent the Pulsar SDKs
and points it at this workflow, so “implement this Figma screen, haptics
included” does the right thing:
npx skills add software-mansion-labs/skills --skill pulsar-hapticsWhat the plugin writes into the file
Section titled “What the plugin writes into the file”Every bound node carries, in shared plugin data:
| Namespace | Key | Value |
|---|---|---|
pulsar | binding | JSON: { "v": 1, "presetId": string, "presetName": string, "customPattern"?: object } |
pulsar | binding-negated | non-empty ⇒ the node is explicitly unbound; skip it |
presetName— the human name of a built-in preset ("DogBark","TickTock","Bloom"). This is what becomes a call.presetId— Pulsar’s internal id. Keep it for traceability; drive codegen offpresetName.customPattern— only on legacy inline-pattern bindings. See Custom patterns.
The plugin also keeps a private pulsar:binding copy as its own source of
truth. Private plugin data is readable only by the plugin that wrote it, so always
read the shared pulsar / binding pair.
The binding sits on whichever node the designer selected — usually a component instance or a frame.
The workflow
Section titled “The workflow”1. Generate the UI, keep the node IDs
Section titled “1. Generate the UI, keep the node IDs”Call get_design_context / get_metadata on the frame and build the screen as
usual. Hold on to the data-node-id of each element you emit: those IDs are the
join key to the haptic data. Frames that have haptics also carry a Pulsar
annotation in Dev Mode — treat it as a hint that this screen is worth checking,
not as the data itself.
2. Read the bindings with use_figma
Section titled “2. Read the bindings with use_figma”use_figma runs JavaScript against the open file. This lists every bound node on
the current page:
const NS = 'pulsar';return figma.currentPage .findAllWithCriteria({ sharedPluginData: { namespace: NS, keys: ['binding'] } }) .filter((n) => !n.getSharedPluginData(NS, 'binding-negated')) .map((n) => ({ nodeId: n.id, nodeName: n.name, ...JSON.parse(n.getSharedPluginData(NS, 'binding')), }));To resolve just the IDs you got back from the design context:
const NS = 'pulsar';const ids = ['1:23', '1:45']; // node IDs from get_design_contextconst out = [];for (const id of ids) { const n = await figma.getNodeByIdAsync(id); if (!n || n.getSharedPluginData(NS, 'binding-negated')) continue; const raw = n.getSharedPluginData(NS, 'binding'); if (raw) out.push({ nodeId: id, nodeName: n.name, ...JSON.parse(raw) });}return out;You get back one row per bound element — node ID, layer name, preset name — which maps straight onto the components you just wrote.
3. Turn presetName into an SDK call
Section titled “3. Turn presetName into an SDK call”Built-in presets are exposed as no-arg methods named after the preset in
camelCase: "DogBark" → dogBark, "TickTock" → tickTock.
| Platform | Package | Call for DogBark |
|---|---|---|
| React Native | react-native-pulsar | Presets.dogBark() |
| iOS (Swift) | PulsarHaptics | pulsar.getPresets().dogBark() |
| Android (Kotlin) | com.swmansion:pulsar | pulsar.getPresets().dogBark() |
| Kotlin Multiplatform | com.swmansion:pulsar-kmp | pulsar.getPresets().dogBark() |
| Flutter | pulsar_haptics | await pulsar.getPresets().dogBark() |
| Web | pulsar-haptics | await pulsar.getPresets().dogBark() |
Every SDK also has a play-by-name escape hatch, which is handy when you generate code straight from the binding string — see the platform page under the SDK overview for the exact spelling it expects.
4. Fire it on the real interaction
Section titled “4. Fire it on the real interaction”Attach the call to the element’s natural event — press for a button, change for a toggle, the success branch for a submit — following whatever handler conventions the project already uses:
import { Presets } from 'react-native-pulsar';
<Pressable onPress={() => { Presets.dogBark(); // ← from the node's Pulsar binding onSubmit(); }}> <Text>Submit</Text></Pressable>;Preload anything that must feel instant on first touch:
Settings.preloadPresets(['DogBark']);Without the MCP: the REST API
Section titled “Without the MCP: the REST API”Shared plugin data also comes back from the REST API with plugin_data=shared,
which needs a Figma token but not an open file:
curl -s -H "X-Figma-Token: $FIGMA_TOKEN" \ "https://api.figma.com/v1/files/$FILE_KEY/nodes?ids=$NODE_IDS&plugin_data=shared" \ | jq '.. | .sharedPluginData? // empty'Look for sharedPluginData.pulsar.binding on each node and JSON.parse it.
Custom patterns
Section titled “Custom patterns”If a binding carries a customPattern object instead of a built-in preset, play it
as a raw pattern rather than a named preset — in React Native through
usePatternComposer, which takes the same discretePattern + continuousPattern
shape. See the React Native SDK page.
Gotchas
Section titled “Gotchas”- Read the shared data, not the private one.
pulsar:binding(private) is invisible outside the plugin;pulsar/binding(shared) is the public contract. binding-negatedwins. An instance can inherit a binding from its component master and then be explicitly opted out. Skip any node that has the flag set.- Component masters. If the designer bound the master rather than the instance,
only the master node carries the shared binding, and the REST route may not
surface it on instances. Reading through
use_figmahonours inheritance; if in doubt, ask the designer to bind the instance. - Annotations are a hint, not a source. Nested-instance annotations don’t reach the Figma MCP. The shared plugin data is the source of truth.