can you reason about how to get text styles in figma. im developing a figma plugin. my ai agent is struggeling with this: Here is a summary of the current situation and my plan:
The Problem:
The get_text_style
command is not currently extracting text style properties from Figma text nodes. While the command successfully identifies text nodes, the textStyle
object returned is empty.
What's Not Working:
The core issue lies in how the _getSingleTextStyle
function (located in src/server/commands/figma/style/text/TextStyleGet.ts
) attempts to access the text style properties. The properties like fontName
, fontSize
, etc., are not directly available on the main node object (nodeInfo.document
) that is retrieved from Figma. Instead, they are nested within a style
sub-object of that node (i.e., nodeInfo.document.style
). My current code was incorrectly looking for these properties directly on nodeInfo.document
.
What I'm Trying to Do:
My objective is to correctly implement the get_text_style
command so that it accurately retrieves all relevant text style properties from a specified Figma text node and returns them in a structured format.
How I'm Currently Trying to Get Text Styles (and the specific point of failure):
- The
handleGetTextStyle
function receives anodeId
(ornodeIds
). - For each
nodeId
, it calls the helper function_getSingleTextStyle
. - Inside
_getSingleTextStyle
, it executes theGET_NODE_INFO
command to fetch detailed node information from the Figma plugin. - The Figma plugin successfully returns comprehensive node data, which includes a nested
style
object containing the text style properties. - When
_getSingleTextStyle
receives this data, it stores it in anode
variable (which corresponds tonodeInfo.document
). - The point of failure: My current logic then tries to read properties like
node.fontName
ornode.fontSize
. However, these properties exist asnode.style.fontName
andnode.style.fontSize
. Because the direct property access fails, thetextStyle
object remains empty.
My Plan to Fix This:
- Modify
src/server/commands/figma/style/text/TextStyleGet.ts
:- I will update the
_getSingleTextStyle
function to correctly access the text style properties. Instead of checkingTextStyleProperty.PropertyName in node
, I will checkTextStyleProperty.PropertyName in node.style
and retrieve the values from(node.style as any).propertyName
. - I will ensure all existing logging for
nodeInfo
and the finaltextStyle
object remains, and I will add specific logs for each property extraction to provide maximum visibility into what's being read (or not read) fromnode.style
.
- I will update the
- Rebuild the Server: After making these server-side code changes, I will execute the command:
cd src/server && bun run build
This will compile the updated server code. - Manual Server Restart (User Action Required): Once the build is complete, I will inform you that the server needs to be manually restarted. I cannot perform this step directly.
- Rejoin Channel (User Action Required): After you confirm the server has been manually restarted and is ready, I will rejoin the channel.
- Test and Verify: I will then test the
get_text_style
command on a newly created text node. I will carefully review the logs to confirm that the text style properties are now correctly extracted and populated in thetextStyle
object.
This comprehensive approach, with detailed logging at each step, should allow us to quickly diagnose and resolve the issue. Please let me know if this plan is acceptable, and then we can proceed.
Below is a concise checklist you can follow to make sure your get_text_style command actually reaches the properties hidden in node.style and returns them in a predictable shape.
Before touching code, log the exact object that comes back from Figma:
const nodeInfo = await client.single(GET_NODE_INFO, {nodeId});
console.log(JSON.stringify(nodeInfo.document, null, 2));
You should see a structure similar to:
If the style block is missing, the problem is in the plugin-side fetch, not your server.
// TextStyleGet.ts
import {TextStyleProperty} from "./TextStyleTypes";
async function _getSingleTextStyle(nodeId: string): Promise<TextStyle|undefined> {
const {document: node} = await getNodeInfo(nodeId); // unchanged
if (!node?.style) {
console.warn(`Node ${nodeId} has no style block`);
return undefined;
}
const styleBlock = node.style as Record<string, unknown>;
const textStyle: Partial<TextStyle> = {};
// iterate over the enum so new props auto-map
Object.values(TextStyleProperty).forEach(key => {
if (key in styleBlock) {
// store exactly as the enum defines
textStyle[key as keyof TextStyle] = styleBlock[key] as never;
} else {
console.debug(`Property ${key} missing on node ${nodeId}`);
}
});
return Object.keys(textStyle).length ? textStyle as TextStyle : undefined;
}
Why this works:
node.style
is the only place where font & typography live.- Casting to Record<string, unknown> avoids TypeScript complaining.
- Iterating the enum keeps server & shared-types in sync automatically.
cd src/server
bun run build # or bun build ...
# then restart the server process (pm2, docker restart, etc.)
- Create a new text layer in Figma, give it a unique font/size.
- Run get_text_style with that nodeId.
- Expect JSON like:
{
"fontFamily": "Inter",
"fontSize": 16,
"fontWeight": 400,
"textAlignHorizontal": "LEFT",
...
}
If values are still empty:
- Confirm node.style actually contains them (step #1).
- Check that your TextStyleProperty enum names exactly match the keys in style (e.g., fontFamily vs font_name).
- Ensure the Figma API version you target hasn’t renamed fields (rare, but worth checking).
- Nil guard: Some text nodes inherit styles from shared text styles; in those cases Figma sends a style block that only contains overrides. Handle missing keys gracefully.
- Enum ↔ key map: If your enum uses camelCase but Figma sends snake_case, create a mapping object instead of relying on raw equality.
- Batch fetches: If requesting many nodeIds, use GET_FILE_NODES once instead of per-node fetches to cut latency.
Follow the checklist sequentially and your AI agent should start reporting full text style data without further issues.
The new _getSingleTextStyle
is still reading properties directly from node
:
if (TextStyleProperty.FontSize in node) textStyle.fontSize = (node as any).fontSize;
…but a Figma TEXT node looks like
{
"type": "TEXT",
"style": {
"fontSize": 16,
"fontWeight": 400,
...
}
}
All typography lives under node.style
, not on the root object.
Because the checks use the in-operatoragainst node
, every condition fails and nothing is copied into textStyle
, so the object you return is always {}
.
Replace the eight nearly-identical blocks with one loop that looks inside node.style
.
const styleBlock = (node as any).style ?? {};
const textStyle: Record<string, unknown> = {};
// go through every enum value
Object.values(TextStyleProperty).forEach(k => {
if (k in styleBlock) textStyle[k] = styleBlock[k];
});
logger.debug(`_getSingleTextStyle: Final textStyle for ${nodeId}: ${JSON.stringify(textStyle)}`);
Key points:
styleBlock
is guaranteed to be an object—even if the node inherits a shared style and Figma omits some keys.- No more duplicated if-statements; adding a new enum entry automatically gets picked up.
- The
in
check now works ("fontSize" in styleBlock
is true).
private static async _getSingleTextStyle(nodeId: string) {
try {
const nodeInfo = await executeCommand(MCP_COMMANDS.GET_NODE_INFO, {nodeId});
const node = (nodeInfo as NodeInfoResponse).document;
if (node?.type !== "TEXT") {
return {nodeId, success:false, error:TEXT_STYLE_ERROR_MESSAGES.NOT_A_TEXT_NODE};
}
const styleBlock = node.style ?? {};
const textStyle: Record<string, unknown> = {};
Object.values(TextStyleProperty).forEach(key => {
if (key in styleBlock) textStyle[key] = styleBlock[key];
});
return {nodeId, success:true, textStyle};
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
return {nodeId, success:false, error:TEXT_STYLE_ERROR_MESSAGES.UNEXPECTED_ERROR(msg)};
}
}
Build → restart the server, and get_text_style
will start returning:
{
"nodeId": "123:45",
"success": true,
"textStyle": {
"fontName": {"family":"Inter","style":"Regular"},
"fontSize": 16,
"fontWeight": 400,
"letterSpacing": 0,
"lineHeight": {"unit":"PIXELS","value":24},
"paragraphSpacing": 0,
"textCase": "ORIGINAL",
"textDecoration": "NONE",
"textStyleId": ""
}
}
No other parts of your class need to change.
Your latest _getSingleTextStyle
already drills into node.style
, yet four fields often come back as undefined:
- paragraphSpacing
- textCase
- textDecoration
- textStyleId
lineHeight
also returns only the unit.
From Figma’s REST payload this is expected: those keys are not always present. Figma only emits a key when it differs from the style that the text layer inherits (local or shared style). A freshly created layer whose paragraph spacing = 0, text case = “ORIGINAL”, text decoration = “NONE” and that uses a shared text style will have none of those keys in style
. The same happens with line-height: when you leave it on “AUTO”, Figma sends
"lineHeightUnit": "INTRINSIC_%"
and omits any numeric value.
So the server code is now correct; the values are simply missing upstream. To surface complete information you have two choices.
Instead of GET_FILE_NODES
, switch to GET_NODE_BY_ID
with depth=1&geometry=paths
(or call the runtime plugin API) and add the query param plugin_data=true&includeStyle=True
. This forces Figma to hydrate every typographic attribute, even when it matches the parent style. Once hydrated, the same code you already wrote will pick the properties up.
Pros:
- No server-side change.
- You still hit the REST API.
Cons:
- Extra payload size.
- Not available on the public REST endpoint; must be done from an in-document plugin.
If you must stay with the existing REST response, add a post-processing step:
const defaults = {
paragraphSpacing: 0,
textCase: "ORIGINAL",
textDecoration: "NONE",
textStyleId: ""
};
Object.entries(defaults).forEach(([k,v]) => {
if (textStyle[k] === undefined) textStyle[k] = v;
});
// lineHeight
if (textStyle.lineHeight === undefined) {
const unit = styleBlock.lineHeightUnit ?? "INTRINSIC_%";
textStyle.lineHeight = {unit, value:null};
}
This surfaces predictable values to the consumer even when Figma omits the key.
Pros:
- One line of code; no API change.
- Keeps response shape stable.
Cons:
- You are inventing data the user never set (albeit with Figma’s own defaults).
Add the fallback defaults right after you finish copying properties:
// after your Object.values(TextStyleProperty).forEach(...)
const fallback = {
paragraphSpacing: 0,
textCase: "ORIGINAL",
textDecoration: "NONE",
textStyleId: ""
};
for (const [k,v] of Object.entries(fallback)) {
if (textStyle[k] === undefined) textStyle[k] = v;
}
if (textStyle.lineHeight === undefined) {
const unit = styleBlock.lineHeightUnit ?? "INTRINSIC_%";
textStyle.lineHeight = {unit, value:null};
}
Re-build, restart, and run get_text_style
again; you will now see:
{
"paragraphSpacing": 0,
"textCase": "ORIGINAL",
"textDecoration": "NONE",
"textStyleId": "",
"lineHeight": {"unit":"INTRINSIC_%","value":null},
...other fields
}
This confirms the extraction logic is sound and the remaining gaps were due to Figma’s sparse payload.
The last action was a write_to_file
command. The user has provided logs and asked for further diagnosis. I need to analyze the provided logs and then implement the suggested fixes.
The logs confirm that fontName
, fontSize
, fontWeight
, letterSpacing
, and lineHeight
(unit only) are being extracted. The logs also explicitly state that paragraphSpacing
, textCase
, textDecoration
, and textStyleId
are not found or is undefined in styleBlock
. This confirms the diagnosis that Figma's API is not always returning these values.
Now I need to implement the suggested fix: adding fallback defaults and a more robust property mapping strategy. I will update the _getSingleTextStyle
method in src/server/commands/figma/style/text/TextStyleGet.ts
to incorporate these changes.
I have already read the file content in the previous turn, so I have the most up-to-date content.
Your extraction logic is working; the missing fields simply never arrive from Figma when they equal the layer’s inherited defaults. Two short additions make the response predictable and future-proof:
Put the mapping next to the enum so you cannot forget it when Figma renames something:
// TextStyleGet.ts (top of the file)
const STYLE_MAP: Record<TextStyleProperty,string> = {
[TextStyleProperty.FontName]: "fontFamily", // special case
[TextStyleProperty.FontSize]: "fontSize",
[TextStyleProperty.FontWeight]: "fontWeight",
[TextStyleProperty.LetterSpacing]: "letterSpacing",
[TextStyleProperty.LineHeight]: "lineHeightUnit", // handled separately
[TextStyleProperty.ParagraphSpacing]:"paragraphSpacing",
[TextStyleProperty.TextCase]: "textCase",
[TextStyleProperty.TextDecoration]: "textDecoration",
[TextStyleProperty.TextStyleId]: "textStyleId"
};
// after you verify node.type === "TEXT"
const styleBlock = node.style ?? {};
const textStyle: Record<string, unknown> = {};
// copy what exists
Object.entries(STYLE_MAP).forEach(([prop, figmaKey]) => {
if (figmaKey in styleBlock) textStyle[prop] = styleBlock[figmaKey];
});
// special reconstructions
if (TextStyleProperty.FontName in textStyle) {
textStyle[TextStyleProperty.FontName] = {
family: styleBlock.fontFamily,
style: styleBlock.fontStyle ?? styleBlock.fontPostScriptName
};
}
if (TextStyleProperty.LineHeight in styleBlock) {
const unit = styleBlock.lineHeightUnit;
const value = unit === "PIXELS"
? styleBlock.lineHeightPx
: unit === "PERCENT"
? styleBlock.lineHeightPercent
: null; // AUTO / INTRINSIC_% etc.
textStyle.lineHeight = {unit, value};
}
/* ---------- NEW fallback defaults ---------- */
const defaults = {
[TextStyleProperty.ParagraphSpacing]: 0,
[TextStyleProperty.TextCase]: "ORIGINAL",
[TextStyleProperty.TextDecoration]: "NONE",
[TextStyleProperty.TextStyleId]: ""
};
for (const [k,v] of Object.entries(defaults)) {
if (textStyle[k] === undefined) textStyle[k] = v;
}
if (textStyle.lineHeight === undefined) {
const unit = styleBlock.lineHeightUnit ?? "INTRINSIC_%";
textStyle.lineHeight = {unit, value:null};
}
return {nodeId, success:true, textStyle};
- Any real value that Figma sends now lands in
textStyle
. - If Figma omits a key, the consumer still receives a sensible default, so downstream code never has to null-check.
- The separate
STYLE_MAP
keeps your server resilient when Figma renames JSON fields.
Re-build (bun run build
), restart the MCP server, and rerun get_text_style
; the response will always include every property—even when Figma’s payload is sparse—while still reflecting actual overrides whenever they exist.
i have a an issue; a suggestion is this, please give me feedback on this plan. i am also attaching the files that this problem affects to give more context: I understand you'd like a comprehensive plan to address the set_gradient
issues, particularly with batch operations and gradientStyleId
. My approach will focus on systematic debugging through enhanced logging and verification.
Here is the detailed plan:
Objective: To diagnose and resolve the set_gradient
command's failures when applying multiple gradients in a single batch call and when using gradientStyleId
, ensuring robust and appropriate logging for future debugging.
Phase 1: Preparation & Enhanced Observability
- Re-read
GradientApplier.ts
:- Action: Read the content of
src/plugin/src/commands/styles/utils/gradient/GradientApplier.ts
. - Purpose: To confirm the current state of the file after the user's previous revert, ensuring a clean baseline before modifications. This file is critical as it contains the core logic for applying gradients.
- Action: Read the content of
- Apply Consistent Logging in
GradientApplier.ts
:- Action: Use
replace_in_file
to modifyGradientApplier.ts
.- Replace existing
console.log
statements withconsole.debug
. - Ensure
console.warn
andconsole.error
are used appropriately for warnings and errors.
- Replace existing
- Purpose: To introduce detailed, internal logging within the plugin's core gradient application logic. This will allow us to trace the execution flow and inspect values when
set_gradient
is used, especially for batch operations andgradientStyleId
applications, without cluttering user-facing notifications.
- Action: Use
- Re-read
GradientProcessor.ts
:- Action: Read the content of
src/plugin/src/commands/styles/utils/gradient/GradientProcessor.ts
. - Purpose: Similar to
GradientApplier.ts
, this step ensures we have the correct, reverted state of the file before applying our changes. This file processes individual gradient entries, which is directly relevant to batch operations.
- Action: Read the content of
- Apply Consistent Logging in
GradientProcessor.ts
:- Action: Use
replace_in_file
to modifyGradientProcessor.ts
.- Replace existing
console.log
statements withconsole.debug
. - Ensure
console.warn
andconsole.error
are used appropriately inprocessGradientEntry
andprocessCreateGradientStyleOperation
methods.
- Replace existing
- Purpose: To gain visibility into how individual gradient entries (relevant for batch) and gradient style creations are handled, which is crucial for understanding why batch and
gradientStyleId
operations are failing.
- Action: Use
Phase 2: Build & Deployment
- Rebuild Figma Plugin:
- Action: Execute the command
cd src/plugin && bun run build
. - Purpose: To compile the TypeScript changes (including the new logging statements) in
GradientApplier.ts
andGradientProcessor.ts
into the JavaScript bundle that the Figma plugin uses.
- Action: Execute the command
- Rebuild MCP Server:
- Action: Execute the command
cd src/server && bun run build
. - Purpose: Although primary changes are plugin-side, rebuilding the server ensures all components are up-to-date and any potential server-side dependencies or schema updates are incorporated.
- Action: Execute the command
- Inform User of Manual Server Restart:
- Action: I will explicitly inform you that the MCP server instance needs to be manually restarted on your end.
- Purpose: This is a critical step as the changes will not take effect until the server process is reloaded. I do not have direct control over this.
Phase 3: Verification & Diagnosis
- Rejoin Figma Channel:
- Action: Once the server is restarted, I will use the
join
command to re-establish communication with Figma. - Purpose: To ensure the connection is active and the updated plugin is ready for testing.
- Action: Once the server is restarted, I will use the
- Execute Targeted
set_gradient
Tests:- Action: I will perform specific
set_gradient
calls:- A batch operation with multiple gradient entries.
- An operation applying a gradient using a
gradientStyleId
.
- Purpose: To reproduce the failure modes and, with the enhanced
console.debug
logs, gather detailed information about where and why the operations are failing. This will be the diagnostic phase.
- Action: I will perform specific
- Analyze Logs and Determine Root Cause:
- Action: Based on the output from the targeted tests (especially the
console.debug
messages), I will analyze the logs to pinpoint the exact line of code or logic error causing the batch andgradientStyleId
failures. - Purpose: To identify the precise bug(s) that need to be fixed.
- Action: Based on the output from the targeted tests (especially the
Phase 4: Implementation of Fix (Subsequent Step)
Once the root cause is identified in Phase 3, I will propose a specific code fix (e.g., using replace_in_file
) and proceed with implementing it, followed by another build and verification cycle. This current plan covers up to the point of diagnosing the issue.
Are you ready for me to proceed with Phase 1 of this plan, or would you like any adjustments?
Below is feedback on your debugging plan along with a few refinements that should save time and avoid common pitfalls.
Enhanced logging is helpful, but the end-goal is to make the command work, not merely to add logs. Before spending a full cycle on log refactoring, verify whether the problem is reproducible outside of batch mode:
- Run one failing entry (same
gradientStyleId
, same node) with the non-batchset_gradient
. - If it succeeds, the bug is in the batch reducer (
GradientSet.executeSetGradient
or how the plugin iterates). - If it still fails, focus on
GradientApplier.applyStyledGradientToNode
.
This single test can instantly halve the search space.
Replacing every console.log
with console.debug
may obscure useful info because Figma’s DevTools filters don’t persist between plugin reloads. Instead:
- Keep errors and warnings as they are.
- Replace only noisy
console.log
calls inside tight loops withconsole.debug
. - Prefix each debug line with a consistent tag (
[GradientApplier]
,[GradientProcessor]
) so you can filter quickly.
Based on previous traces, the two usual choke-points are:
figma.getLocalPaintStylesAsync()
occasionally returns an empty list during the first few milliseconds after plugin launch.node.setFillStyleIdAsync
rejects when the style belongs to a different Figma file (keys start with “S:”).
Add one-line debug probes right before those calls:
console.debug("[GradientApplier] styles.length =", styles.length);
console.debug("[GradientApplier] applying styleId =", style.id, "to node", node.id);
That often reveals the whole story without a full log rewrite.
You only need to rebuild the plugin (src/plugin
); the server build is unnecessary unless you modified GradientSet.ts
or its schema types. Re-building the server slows iteration and risks sneaking in unrelated issues.
Before asking the user to restart anything, run the TypeScript compiler (bun run build
) locally and let it fail fast. That way you catch syntax errors introduced by the logging refactor.
Because plugin → server → plugin round-trips are slow, set a strict limit (e.g., 15min) to capture logs after Phase 3. If you cannot reproduce within that window, roll back the logging diff and drill into the most probable culprit with targeted console.debug
lines.
- Baseline test: run a single failing
set_gradient
call (non-batch) with the troublesomegradientStyleId
. Note the exact error. - Add two targeted debug lines in
applyStyledGradientToNode
(beforegetLocalPaintStylesAsync
and beforesetFillStyleIdAsync
). - Re-build plugin only (
cd src/plugin && bun run build
) and reload the plugin (no server restart). - Re-run the failing call; copy the console output.
- Diagnose:
- If
styles.length
is 0→8 and style not found, you likely need a retry back-off or to load styles earlier. - If
style.id
exists butsetFillStyleIdAsync
throws, the style is from another file (keys vs ids mismatch).
- If
- Implement the fix (e.g., map “S:” keys to local duplicates or add an exponential retry).
- Optional: only after the command works, replace generic
console.log
withconsole.debug
project-wide.
If you accept the streamlined plan above, start with steps 1–3 now; you will get actionable data faster than a blanket logging rewrite. Let me know and I can guide you through the exact code snippets and commands.
- The style never resolves
The plugin’s
applyStyledGradientToNode
does 20 retries, each time callingfigma.getLocalPaintStylesAsync()
and trying to match the incoming ID/key. All 20 attempts end in
DEBUG: Style not found by getLocalPaintStylesAsync on attempt 20.
...
Gradient style with ID 9ce13d25-eac9-4fd0-956e-4ac5a47862b1 not found …
so the plugin never obtains a PaintStyle
object. Everything that follows—including the “not a function” you see—is a downstream symptom of that miss.
2. Trailing-comma mismatch is still present
Earlier logs from get_doc_style
showed IDs ending with a comma, e.g. "9ce13d25-eac9-4fd0-956e-4ac5a47862b1,"
.
Your last interrupted replace_in_file
was supposed to strip that comma before the ID was persisted or sent across the wire, but the change never made it into the bundle that’s running inside Figma.
Result: the server passes comma-free IDs to the plugin, while the plugin’s local paint-style list still holds comma-suffixed IDs—they will never match.
3. Why you hit “not a function”
After the 20th retry GradientApplier
throws new StyleInvalidInputError(...)
.
Somewhere in the promise chain that error bubbles up to code that assumes it received a plain object and tries to call it like a function—hence “not a function”. Fixing the ID mismatch will stop the throw and therefore remove this secondary error.
You must ensure both sides use the same canonical ID:
// GradientApplier.applyStyledGradientToNode, right after you call figma.createPaintStyle()
const resultId = paintStyle.id.replace(/,$/, ''); // <-- this line exists
// add the same sanitisation when you **read** local styles:
style = styles.find(
s => s.id.replace(/,$/, '') === gradientStyleId ||
s.key.replace(/,$/, '') === gradientStyleId
) as PaintStyle;
No other logic needs to change. With the comma removed on lookup:
style
will be found on the first attempt.- The gradient style is applied; no retry loop.
- The promise resolves cleanly, so the “not a function” path is never reached.
- Apply the one-line
replace_in_file
inGradientApplier.ts
(lookup section). - Re-build the plugin bundle only:
cd src/plugin
bun run build
(Server rebuild is unnecessary.)
3. Reload the plugin in Figma and rerun your set_gradient
test with the same gradientStyleId
.
You should now see:
DEBUG: Style found. ID: 9ce13d25-eac9-4fd0-956e-4ac5a47862b1, Attempt 1.
DEBUG: Successfully applied style … to fills of node …
If the test passes, you can proceed to batch-mode debugging knowing the ID issue is solved.
plan now is: Understood. I will provide a super detailed plan to solve the problem with set_gradient
and gradientStyleId
. This plan integrates all our findings, confirmed diagnoses, and the most efficient path forward.
Problem Statement: The set_gradient
command fails to apply styles using gradientStyleId
, resulting in "style not found" errors and downstream "not a function" errors. This is primarily due to a trailing comma being present in the id
of PaintStyle
objects when they are created in Figma and subsequently looked up.
Root Cause Confirmed: The id
property of PaintStyle
objects, as returned by figma.getLocalPaintStylesAsync()
, sometimes contains a trailing comma (e.g., S:some_id,
). However, when we attempt to search for these styles using the gradientStyleId
provided via the MCP command (which typically does not have this comma, or has an inconsistent number of commas), the exact string match fails. The "not a function" error is a secondary symptom, occurring when error handling attempts to process the primary "style not found" error.
Core Strategy: Implement a targeted fix to ensure style IDs are consistently handled (cleaned of trailing commas) during the lookup process, then perform a comprehensive "fresh test" to confirm the fix and identify any remaining issues.
Phase 1: Apply Critical Fix (ID Canonicalization in Lookup)
- Objective: Ensure that style IDs are correctly matched during lookup, resolving the "style not found" error.
- Location of Fix:
src/plugin/src/commands/styles/utils/gradient/GradientApplier.ts
- Specific Action (
replace_in_file
):- Context: Within the
applyStyledGradientToNode
method, locate thestyles.find()
call. - Change: Modify the comparison logic to strip any trailing commas from the
s.id
ands.key
properties of thePaintStyle
objects retrieved from Figma before comparing them with the incominggradientStyleId
. - Rationale: This ensures that the lookup is always performed using the canonical (comma-free) form of the ID, allowing successful matching even if Figma's internal representation includes a trailing comma.
- Context: Within the
// Pseudo-code for the change in GradientApplier.ts:
// Locate the line similar to:
// style = styles.find(s => s.id === gradientStyleId || s.key === gradientStyleId) as PaintStyle;
// Replace it with:
style = styles.find(s =>
s.id.replace(/,$/, '') === gradientStyleId ||
s.key.replace(/,$/, '') === gradientStyleId
) as PaintStyle;
- Rebuild Figma Plugin:
- Action: Execute the command
cd src/plugin && bun run build
. - Rationale: To compile the applied fix into the plugin's JavaScript bundle. This is a client-side (plugin) change, so no server rebuild is required for this step.
- Action: Execute the command
Phase 2: Comprehensive Fresh Test (Automated via MCP Commands)
- Objective: Verify the fix from Phase 1 and gather fresh, clean logs to confirm the
set_gradient
functionality. This phase involves creating new elements and styles to avoid any lingering state issues from previous failed attempts. - Pre-requisite: Phase 1 must be successfully completed and the plugin rebuilt.
- Sequence of Actions (MCP Tool Calls):
a. Create Test Rectangles:
- Action: I will use the
create_rectangle
MCP tool to create three new rectangle nodes on the Figma canvas. I will capture theirnodeId
s for subsequent use. - Example Tool Calls:
xml <use_mcp_tool> <server_name>ConduitMCPServerLocDepen</server_name> <tool_name>create_rectangle</tool_name> <arguments> { "rectangles": [ {"x": 0, "y": 0, "width": 100, "height": 100, "name": "TestRect1"}, {"x": 110, "y": 0, "width": 100, "height": 100, "name": "TestRect2"}, {"x": 220, "y": 0, "width": 100, "height": 100, "name": "TestRect3"} ] } </arguments> </use_mcp_tool>
- Expected Output: Successful creation of nodes (e.g.,
1108:277
,1108:278
,1108:279
). I will parse thesenodeId
s from the tool output.
b. Create Test Gradient Styles:
- Action: I will use the
create_gradient_style
MCP tool to create three new, distinct gradient styles. I will capture the exactid
s returned for these styles. - Example Tool Call:
xml <use_mcp_tool> <server_name>ConduitMCPServerLocDepen</server_name> <tool_name>create_gradient_style</tool_name> <arguments> { "gradients": [ {"name": "LinearGradientTest", "gradientType": "LINEAR", "stops": [{"position": 0, "color": [1,0,0,1]}, {"position": 1, "color": [0,0,1,1]}]}, {"name": "RadialGradientTest", "gradientType": "RADIAL", "stops": [{"position": 0, "color": [0,1,0,1]}, {"position": 1, "color": [1,0,1,1]}]}, {"name": "MulticolorGradientTest", "gradientType": "LINEAR", "stops": [{"position": 0, "color": [1,1,0,1]}, {"position": 0.5, "color": [0,1,1,1]}, {"position": 1, "color": [1,0,1,1]}]} ] } </arguments> </use_mcp_tool>
- Expected Output: Successful creation of styles with their IDs. I will parse these
gradientStyleId
s (e.g.,S:some_id_linear,
,S:some_id_radial,
,S:some_id_multicolor,
).
c. Apply Single Styled Gradient (Baseline Check):
- Action: I will use the
set_gradient
MCP tool to apply one of the newly created styled gradients to one of the new rectangles. - Example Tool Call:
xml <use_mcp_tool> <server_name>ConduitMCPServerLocDepen</server_name> <tool_name>set_gradient</tool_name> <arguments> { "entries": [ { "nodeId": "[ID of TestRect1]", "gradientStyleId": "[ID of LinearGradientTest]", "applyTo": "FILL" } ] } </arguments> </use_mcp_tool>
- Expected Output: Success. The logs should show
[GradientApplier] Style found. ID: ..., Attempt 1.
andSuccessfully applied style ...
.
d. Apply Batch Styled Gradients:
- Action: I will use the
set_gradient
MCP tool to apply the other two newly created styled gradients to the remaining rectangles in a single batch call. - Example Tool Call:
xml <use_mcp_tool> <server_name>ConduitMCPServerLocDepen</server_name> <tool_name>set_gradient</tool_name> <arguments> { "entries": [ { "nodeId": "[ID of TestRect2]", "gradientStyleId": "[ID of RadialGradientTest]", "applyTo": "FILL" }, { "nodeId": "[ID of TestRect3]", "gradientStyleId": "[ID of MulticolorGradientTest]", "applyTo": "FILL" } ] } </arguments> </use_mcp_tool>
- Expected Output: Success for both applications.
e. Retrieve Full Console Output:
- Action: After all the above MCP commands are executed, I will ask you to copy and paste the entire console output from Figma's developer console here.
- Rationale: This log will contain all the detailed debug messages from the plugin, allowing for comprehensive analysis.
Phase 3: Deep Diagnosis & Further Refinement
- Objective: Analyze the fresh logs to confirm the primary fix and identify any new or persistent issues.
- Action: I will meticulously review the provided console output.
- Confirm that the
[GradientApplier]
logs show successful style lookups on the first attempt. - Verify that gradients are applied correctly to all nodes, both in single and batch operations.
- Identify any new errors or unexpected behaviors.
- Confirm that the
- Potential Next Steps (Based on Analysis):
- If the primary issue is resolved, but new problems arise (e.g., with batch processing or specific gradient types), I will propose further targeted fixes.
- Important Consideration: If the
create_gradient_style
command itself is still creating IDs with trailing commas (despite the fix being inGradientApplier.ts
), then a secondary fix inGradientProcessor.ts
will be required to prevent the comma from being added at the source. (My previous interrupted attempts at this fix are relevant here, but I will only propose this if the logs confirm it's still an issue after Phase 1).
This detailed plan provides a clear, step-by-step approach to solving the problem. Please toggle to Act mode when you are ready for me to begin Phase 1 by applying the critical fix in GradientApplier.ts
.
The plan is solid and targets the confirmed root cause—the comma-mismatched IDs—before adding any extra complexity. A few quick refinements will make it even more efficient:
- Keep the lookup change exactly as proposed but also sanitize the incoming
gradientStyleId
once, at the top ofapplyStyledGradientToNode
:
gradientStyleId = gradientStyleId.replace(/,$/, ""); // canonical form
This guards against future calls that might still pass a comma-suffixed ID.
- You only need to rebuild the plugin (
src/plugin
) after this edit; no server build is necessary.
Your step-by-step sequence is perfect. Two tips:
- After the
create_gradient_style
call, immediately trim the comma from each returned ID before storing it. That way the subsequentset_gradient
calls use canonical IDs. - Between the single-node test (2c) and the batch test (2d), clear the Figma console once—this keeps the debug output readable when you paste it back.
When you review the log, focus on these lines:
[GradientApplier] Style found. ID: ... Attempt 1.
Successfully applied style ...
If every rectangle shows that pattern on Attempt 1, the comma issue is fully resolved. Any remaining problems will surface as new, isolated errors that we can tackle next.
With those minor tweaks, you’re ready to proceed to Phase 1. Let me know when you’ve applied the lookup patch and rebuilt the plugin, and I’ll walk you through the exact MCP calls for the fresh test.
stuck here: Thank you for providing the detailed Figma plugin logs. This is very helpful in pinpointing the issue.
Upon reviewing the logs, I've identified that the TypeError: not a function
is occurring before the applyStyledGradientToNode
function in GradientApplier.ts
even begins its execution (its initial debug log is not present). This suggests the problem lies in how applyStyledGradientToNode
is invoked or with the object it's being called on.
The relevant part of the log is:
vendor-core-0d62532059ea4067.min.js.br:77 DEBUG: CommandManager: _dispatchCommand - Raw result from command handler for set_gradient: [
{
"nodeId": "1109:289",
"success": false,
"error": "not a function",
"details": {
"code": "UNEXPECTED_ERROR",
"culprit": "TypeError: not a function"
}
},
...
]
This indicates the error is being reported by the set_gradient
command handler itself. My next step is to examine the src/plugin/src/commands/styles/StyleSet.ts
file, as this is where the set_gradient
command is handled and where the call to GradientApplier.ts
is likely made. I will look for how applyStyledGradientToNode
is being called and whether any variables are undefined or null at that point.
Please confirm if you are ready for me to proceed with reading this file. If so, please toggle to Act Mode.
It looks like the investigation needs to shift one file higher in the call-stack—into StyleSet (or whichever module actually invokes GradientProcessor.processGradientEntry
and eventually GradientApplier.applyStyledGradientToNode
). Because no log line from applyStyledGradientToNode
appears at all, the TypeError: not a function
must be thrown earlier, most likely by one of three things:
- The reference to
GradientApplier.applyStyledGradientToNode
is itselfundefined
(for example, you importedGradientApplier
incorrectly or are calling a renamed function). - The argument sent to that function is
undefined
/null
, and the calling code attempts to treat it like a function (e.g.,undefined(...)
). - A promise-chain or
.then
handler on the result ofprocessGradientEntry
is expecting a function but receives something else.
The quickest way to confirm which of these is happening is to add a single debug probe immediately before the call inside the handler file (StyleSet.ts
or GradientProcessor.ts
, whichever is calling the applier):
console.debug("[StyleSet] About to invoke applyStyledGradientToNode",
{ hasFunc: typeof GradientApplier.applyStyledGradientToNode,
styleId: gradientStyleId,
node: node?.id });
If hasFunc
prints "function"
, then the reference is good and the problem is likely a bad argument.
If it prints "undefined"
, the export/import path is wrong—check that GradientApplier
actually exports applyStyledGradientToNode
(and that the compiled bundle isn’t tree-shaking it away).
Once this one-line probe is in place, rebuild the plugin only:
cd src/plugin
bun run build
Reload the plugin in Figma, re-run a failing set_gradient
, and watch the very first [StyleSet]
debug line. That single datum will tell you exactly which of the three scenarios is at play and therefore whether you need to:
- Fix an import (
import { GradientApplier } from "./utils/gradient/GradientApplier"
), - Guard against a missing
gradientStyleId
ornode
, or - Correct a misuse of the promise result (e.g., passing the object itself instead of its method).
Let me know what that debug output shows, and we can target the precise fix in the next step.
create_rectangle
succeeds- All three rectangles (
1110:295-297
) were created without errors and the plugin returned their IDs on the first round-trip.
- All three rectangles (
create_gradient_style
fails immediately- The plugin throws
TypeError: not a function
inStyleGradient.createGradientStyle
before any paint-style creation logs appear. - Because the failure happens inside
processCreateGradientStyleOperation
, no call ever reachesfigma.createPaintStyle()
—the first debug line after that call is missing.
- The plugin throws
- Probable culprit: an accidentally overwritten helper
- In
GradientProcessor.processCreateGradientStyleOperation
the very first validation uses the enumGradientTypeEnum
and the helperGradientNormalizers.mapGradientType
. - A common slip is shadowing
GradientNormalizers
with a non-function value (e.g.,import * as …
typo) or exportingmapGradientType
as a constant during a refactor. - When the code later does
GradientNormalizers.mapGradientType(gradientType)
andmapGradientType
is not a function, JavaScript throws exactly the error you see.
- In
- Why the error bubbles as “not a function” at the server
StyleGradient.createGradientStyle
catches any error and wraps it in aGradientOperationError
.- That error is serialized and sent back through the socket; the server just prints the wrapper message.
Add one debug probe at the top of processCreateGradientStyleOperation
(before validations):
console.debug("[GradientProcessor] typeof GradientNormalizers.mapGradientType =",
typeof GradientNormalizers.mapGradientType);
Re-build the plugin (cd src/plugin && bun run build
), reload, and run create_gradient_style
again.
Expected output:
[GradientProcessor] typeof GradientNormalizers.mapGradientType = function
If it prints undefined
or object
, you have confirmed the bad import/export.
- Ensure proper export in
GradientNormalizers.ts
:
export class GradientNormalizers {
static mapGradientType(type: string): "GRADIENT_LINEAR" | "GRADIENT_RADIAL" | … {
…
}
…
}
- Ensure proper import everywhere it is used:
import { GradientNormalizers } from "./GradientNormalizers";
(Avoid import * as GradientNormalizers
which produces an object whose methods are not callable the same way after TypeScript ↔ ESM transpilation.)
- Re-build the plugin only, reload, and re-run the earlier three-style test.
Plugin log:
[GradientProcessor] typeof GradientNormalizers.mapGradientType = function
[GradientProcessor] processCreateGradientStyleOperation called with: name=Gradient Style 1 …
[GradientProcessor] Paint style created with ID: S:123…,
…
[GradientApplier] Style found. ID= S:123…, Attempt 1.
Successfully applied style …
Server result:
{
"success": true,
"results": [
{"nodeId":"1110:295","success":true},
{"nodeId":"1110:296","success":true},
{"nodeId":"1110:297","success":true}
]
}
Once create_gradient_style
works, re-test set_gradient
(single and batch).
If any further issues arise (e.g., comma-suffix IDs, batch iteration), we can address them with targeted patches, but the current blocker is the broken reference to mapGradientType
.