Skip to main content

Generative plugins and shaders

The Figma MCP server can read and write the source code behind generative plugins, shader fills, and shader effects. That means an agent can pull a resource you built with the Figma agent, modify it in your own editor with your own tooling, and push the result back to Figma, where it rebuilds, versions, and deploys.

Two resource families, each with its own tools:

ResourceListReadCreateUpdate
Generative pluginlist_generative_pluginsget_generative_plugincreate_generative_pluginupdate_generative_plugin
Shader (effect or fill)list_shaders, list_file_shadersget_shadercreate_shader (kind)update_shader (kind)
note

list_shaders and get_shader handle both shader effects and shader fills. The earlier per-kind tools are deprecated: list_shader_effects, list_shader_fills, get_shader_effect, and get_shader_fill. Use the consolidated tools.

caution

Loading a skill is a required first step for writing. create_shader and update_shader require the figma-shaders skill. create_generative_plugin and update_generative_plugin require the figma-generative-plugins skill. These are mandatory prerequisites, not suggestions: they carry the authoring contract, the file layout, and the PropsKit reference. Read them with get_figma_skill, or skill://figma/figma-shaders/SKILL.md and skill://figma/figma-generative-plugins/SKILL.md.


Reading source code

Reads are a two-step process. The list and get tools return a manifest of files with URIs: the file contents are not inlined. Fetch each uri as an MCP resource.

A real example. Calling get_shader on Figma's Glowing wave returns:

{
"id": "38c5fa52-a2a8-46db-affb-efd028b890e2",
"name": "Glowing wave",
"description": "Generate luminous wave graphics with on-canvas controls for color, glow, and motion.",
"type": "fill",
"owner": "figma",
"version": "184e861619102db4dcfcb5e967255194dd4478d9",
"files": [
{ "filename": ".build/main.js", "bytes": 8871, "uri": "file://…/.build/main.js" },
{ "filename": "features.json", "bytes": 112, "uri": "file://…/features.json" },
{ "filename": "main.ts", "bytes": 8343, "uri": "file://…/main.ts" }
],
"truncated": false
}

main.ts is the authored source. .build/main.js is the compiled artifact. features.json holds resource-level flags and cannot be modified through the MCP server.

The owner field

owner tells you where a resource came from:

ValueMeaning
Your email addressYou own it: source readable and writable
figmaA Figma first-party resource
A public publisher handleA third-party published plugin

If your client can't read MCP resources

Both get tools accept includeSource: true, which inlines each file's contents in the tool result instead of returning URIs. Capped at 100 files and 1,000,000 cumulative bytes; the result reports which limit caused truncation. Leave it false unless your client needs it.

Reading a specific version

Both get tools take an optional version: a 40-character commit SHA. Omit it for the latest built version. Use it to read exactly what a file is rendering, or to diff two builds.


Creating and updating

Writing follows a scaffold-then-replace pattern. There is no single call that creates a finished resource.

1. Get a plan key

create_* needs a planKey naming the plan that will own the resource. Call whoami and use a key from the returned plans list verbatim:

organization::1234567890
team::9876543210

If the user belongs to more than one plan, ask which to use rather than guessing.

2. Create the scaffold

create_shader({
name: "Topographic contours",
description: "Elevation-ring contour lines with adjustable spacing and ruggedness",
planKey: "team::9876543210",
kind: "fill"
})

This returns an id. The resource already exists and already runs: it is a starter, not an empty file. That is deliberate: the scaffold carries the structure your replacement source needs to match.

3. Replace the source

update_shader({
id: "<id from step 2>",
kind: "fill",
files: [{ path: "main.ts", content: "<complete file contents>" }],
commitMessage: "Add contour line shader with spacing and ruggedness controls"
})

Each files entry must be complete replacement content, not a diff or a fragment. When updating an existing resource rather than a fresh scaffold, read the current source first so you do not silently drop code.

kind must match the existing shader. Do not switch a fill to an effect during an update.

The update is built, versioned, and deployed as part of the call: there is no separate publish step. Treat any non-error response as success and record the version when present; a successful response may omit it.

Renaming without changing code

Both update tools accept a metadata object with name and description. For a metadata-only change, pass an empty files array:

update_shader({
id: "<id>",
kind: "fill",
files: [],
metadata: { name: "Topographic contours v2" },
commitMessage: "Rename"
})

When a build fails

The response contains compiler output. Make the smallest source correction it points to and retry once. If it fails again, surface the error rather than rewriting the resource repeatedly.


Writing shader source

Shader source lives in a single file, main.ts. Read the scaffold before replacing it. This establishes the runtime imports and the fixed metadata contract your replacement has to match.

Authoring rules

  • Expose controls for values users are likely to tune per layer. Hardcode implementation details.
  • Keep numeric ranges bounded and defaults visually useful.
  • For effects, sample the input raster intentionally. For fills, do not assume an input raster exists.
  • Never embed API keys, tokens, or signed URLs. Source is readable by anyone with access.

The figma-shaders skill's references/authoring.md carries the required module shape, the WebGPU lifecycle, supported parameter schemas, effect and fill alpha rules, and a WGSL failure checklist. Read it before writing a replacement.


Writing generative plugin source

A generative plugin has two authored files you can replace:

FileOwns
code.tsThe sandbox entrypoint. The only side that can call figma.*.
ui.htmlThe plugin panel. The only side that can use DOM APIs.

The two communicate exclusively through messages. code.ts loads the panel with figma.showUI(__html__, …).

manifest.json exists but cannot be replaced or created through the MCP server.

Every plugin must provide functional UI. Even a plugin with no configurable inputs needs a clear primary action and useful status, validation, and error feedback.

PropsKit

Plugin UI uses PropsKit: a set of fig-* custom elements used inside ui.html, which make the panel look and behave natively:

<fig-content>
<fig-field>
<label>Count</label>
<fig-input-number id="count" value="3" min="1" max="100"></fig-input-number>
</fig-field>
</fig-content>
<fig-footer>
<label id="status">Ready</label>
<fig-button id="run" type="submit">Create layers</fig-button>
</fig-footer>

Structure the panel as <fig-content> followed by <fig-footer>, wrap controls in <fig-field> with a child <label>, and put status and actions in the footer.

Common controls: <fig-slider>, <fig-input-number>, <fig-input-text>, <fig-input-color>, <fig-input-gradient>, <fig-input-palette>, <fig-switch>, <fig-options>, <fig-dropdown>, <fig-joystick>, <fig-image>, <fig-input-file>, <fig-button>.

The full control table, the per-control gotchas, and the Plugin API conversion rules (PropsKit colors are hex and gradient stops are 0..100; the Plugin API wants normalized 0..1) live in the figma-generative-plugins skill's references/authoring.md. You can also explore the components in the PropsKit playground.

note

PropsKit is for generative plugins only. Shaders declare their controls in main.ts instead.


What you cannot change

Updates replace existing authored files only. Unspecified files are preserved, so send only what changed.

ResourceReplaceableNot replaceable
Shadermain.tsfeatures.json: where animation and mouse capabilities live
Generative plugincode.ts, ui.htmlmanifest.json

Through the MCP server you also cannot:

  • Create new files
  • Add imports, dependencies, or build configuration the tool cannot deploy
  • Send a diff or partial fragment: every file entry must be complete content

If a plugin's existing manifest is incompatible with what you want to build, say so. The update tool cannot repair it.


Permissions

Access differs by resource type and by whether the resource is published.

ResourceSource readable byWritable by
Your own resourcesYouYou
Figma's first-party published resourcesAnyoneNo one directly: goes through the publish flow
Third-party published resourcesOwner only (built artifact is public)No one directly: goes through the publish flow
Someone else's unpublished resourcesAnyone with file access, via list_file_shadersOwner

You can access the code for any shader, but only for your own plugins.

  • Source code vs. built artifact. The built artifact is the bundled code Figma sends to the client to run a resource. Anyone who can run a plugin or shader can already see the artifact in their browser's network tab. Source code is the authored code, and is protected differently.
  • Ownership vs. file access. get_shader works on what you own. list_file_shaders works on what you can open.
  • The owner field tells you which. Every list and get response includes it: your email, figma, or a publisher handle. Check it rather than inferring.
caution

Plugin and shader source is readable by anyone who has access to it. Never embed API keys, OAuth tokens, signed URLs, or other secrets. For authenticated integrations, use a static dataset, a public no-auth endpoint, or a different architecture.


Exporting shaders to code

Shader export is behavior added to get_design_context, not a separate tool. There is nothing extra to call.

When your selection contains shader fills or shader effects, get_design_context includes a lightweight Figma shader runtime in its output. The runtime is written as React components.

implement this Figma frame in React using the included shader runtime
→ get_design_context returns layout, styling, assets, and the shader runtime
→ agent wires the runtime into the generated components

If you already use the MCP server for design-to-code, this changes your output. Frames that previously produced a rasterized or CSS-approximated background now carry a real runtime. No configuration change is needed and no new tool call is involved.

Three things to tell your agent

The runtime arriving in the output is not by itself enough to get correct results.

1. Use the runtime, don't approximate in CSS. Left to its own judgment, an agent will often reach for a gradient or a blur that looks roughly right. Your prompt should explicitly direct it to use the runtime and shader source directly. This is the single most common failure mode.

2. HTML-in-Canvas is required for display. The runtime will not render correctly without it.

3. Non-React frameworks need a translation step. The runtime ships as React components. If your project is Vue, Svelte, SwiftUI, or anything else, instruct the agent to translate the runtime to your framework as well as the surrounding code.

Shaders are not image assets

Design-to-code guidance tells agents to render every icon and image from its exported asset and never substitute a screenshot. Shader-backed layers are the exception in the opposite direction: they should be implemented from the included runtime and shader source, not from a rasterized export and not from a CSS approximation.


Errors

SituationExpected behavior
Source fails to buildError containing compiler output. Make the smallest correction it identifies and retry once.
Write to a resource you don't ownError
planKey malformed or not one of your plansError: must match (team|organization)::<numeric-id>
kind doesn't match the existing shaderError
update_* before create_*Error: no resource to update
commitMessage omittedError: required
path other than main.ts (shaders) or code.ts / ui.html (plugins)Rejected: the parameter is an enum
File references more than 100 shadersTop-level truncated: true
Shader manifest exceeds 10,000 filesPer-shader truncated: true
includeSource exceeds 100 files or 1,000,000 bytesTruncated; result reports which limit was hit

A non-error response is success even when it omits a version.