Skip to content

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.

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.

The hosted server needs no local Figma app:

Terminal window
claude mcp add --transport http figma https://mcp.figma.com/mcp

Codex:

Terminal window
codex mcp add figma --url https://mcp.figma.com/mcp

There 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.

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:

Terminal window
npx skills add software-mansion-labs/skills --skill pulsar-haptics

Every bound node carries, in shared plugin data:

NamespaceKeyValue
pulsarbindingJSON: { "v": 1, "presetId": string, "presetName": string, "customPattern"?: object }
pulsarbinding-negatednon-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 off presetName.
  • 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.

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.

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_context
const 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.

Built-in presets are exposed as no-arg methods named after the preset in camelCase: "DogBark"dogBark, "TickTock"tickTock.

PlatformPackageCall for DogBark
React Nativereact-native-pulsarPresets.dogBark()
iOS (Swift)PulsarHapticspulsar.getPresets().dogBark()
Android (Kotlin)com.swmansion:pulsarpulsar.getPresets().dogBark()
Kotlin Multiplatformcom.swmansion:pulsar-kmppulsar.getPresets().dogBark()
Flutterpulsar_hapticsawait pulsar.getPresets().dogBark()
Webpulsar-hapticsawait 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.

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']);

Shared plugin data also comes back from the REST API with plugin_data=shared, which needs a Figma token but not an open file:

Terminal window
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.

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.

  • Read the shared data, not the private one. pulsar:binding (private) is invisible outside the plugin; pulsar / binding (shared) is the public contract.
  • binding-negated wins. 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_figma honours 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.