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:
| Resource | List | Read | Create | Update |
|---|---|---|---|---|
| Generative plugin | list_generative_plugins | get_generative_plugin | create_generative_plugin | update_generative_plugin |
| Shader (effect or fill) | list_shaders, list_file_shaders | get_shader | create_shader (kind) | update_shader (kind) |
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.
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:
| Value | Meaning |
|---|---|
| Your email address | You own it: source readable and writable |
figma | A Figma first-party resource |
| A public publisher handle | A 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:
| File | Owns |
|---|---|
code.ts | The sandbox entrypoint. The only side that can call figma.*. |
ui.html | The 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.
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.
| Resource | Replaceable | Not replaceable |
|---|---|---|
| Shader | main.ts | features.json: where animation and mouse capabilities live |
| Generative plugin | code.ts, ui.html | manifest.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.
| Resource | Source readable by | Writable by |
|---|---|---|
| Your own resources | You | You |
| Figma's first-party published resources | Anyone | No one directly: goes through the publish flow |
| Third-party published resources | Owner only (built artifact is public) | No one directly: goes through the publish flow |
| Someone else's unpublished resources | Anyone with file access, via list_file_shaders | Owner |
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_shaderworks on what you own.list_file_shadersworks on what you can open. - The
ownerfield tells you which. Every list and get response includes it: your email,figma, or a publisher handle. Check it rather than inferring.
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
| Situation | Expected behavior |
|---|---|
| Source fails to build | Error containing compiler output. Make the smallest correction it identifies and retry once. |
| Write to a resource you don't own | Error |
planKey malformed or not one of your plans | Error: must match (team|organization)::<numeric-id> |
kind doesn't match the existing shader | Error |
update_* before create_* | Error: no resource to update |
commitMessage omitted | Error: required |
path other than main.ts (shaders) or code.ts / ui.html (plugins) | Rejected: the parameter is an enum |
| File references more than 100 shaders | Top-level truncated: true |
| Shader manifest exceeds 10,000 files | Per-shader truncated: true |
includeSource exceeds 100 files or 1,000,000 bytes | Truncated; result reports which limit was hit |
A non-error response is success even when it omits a version.