Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save eonist/4541fea7f8654c7bea83c0e3338ed074 to your computer and use it in GitHub Desktop.
Save eonist/4541fea7f8654c7bea83c0e3338ed074 to your computer and use it in GitHub Desktop.
previous reasoning around the batch set gradient issue

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):

  1. The handleGetTextStyle function receives a nodeId (or nodeIds).
  2. For each nodeId, it calls the helper function _getSingleTextStyle.
  3. Inside _getSingleTextStyle, it executes the GET_NODE_INFO command to fetch detailed node information from the Figma plugin.
  4. The Figma plugin successfully returns comprehensive node data, which includes a nested style object containing the text style properties.
  5. When _getSingleTextStyle receives this data, it stores it in a node variable (which corresponds to nodeInfo.document).
  6. The point of failure: My current logic then tries to read properties like node.fontName or node.fontSize. However, these properties exist as node.style.fontName and node.style.fontSize. Because the direct property access fails, the textStyle object remains empty.

My Plan to Fix This:

  1. Modify src/server/commands/figma/style/text/TextStyleGet.ts:
    • I will update the _getSingleTextStyle function to correctly access the text style properties. Instead of checking TextStyleProperty.PropertyName in node, I will check TextStyleProperty.PropertyName in node.style and retrieve the values from (node.style as any).propertyName.
    • I will ensure all existing logging for nodeInfo and the final textStyle object remains, and I will add specific logs for each property extraction to provide maximum visibility into what's being read (or not read) from node.style.
  2. 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.
  3. 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.
  4. Rejoin Channel (User Action Required): After you confirm the server has been manually restarted and is ready, I will rejoin the channel.
  5. 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 the textStyle 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.

1. Verify the raw node payload first

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:

{
  "id": "123:45",
  "type": "TEXT",
  "style": {
    "fontFamily": "Inter",
    "fontPostScriptName": "Inter-Regular",
    "fontWeight": 400,
    "fontSize": 16,
    "textAlignHorizontal": "LEFT",
    "textAlignVertical": "TOP",
    "letterSpacing": 0,
    ...
  },
  ...
}

If the style block is missing, the problem is in the plugin-side fetch, not your server.

2. Update _getSingleTextStyle so it digs into style

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

3. Rebuild & restart

cd src/server
bun run build     # or bun build ...
# then restart the server process (pm2, docker restart, etc.)

4. Smoke-test the command

  1. Create a new text layer in Figma, give it a unique font/size.
  2. Run get_text_style with that nodeId.
  3. 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).

5. Optional hardening tips

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


Why textStyle stays empty with the code you just posted

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 {}.

Minimal change that fixes it

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:

  1. styleBlock is guaranteed to be an object—even if the node inherits a shared style and Figma omits some keys.
  2. No more duplicated if-statements; adding a new enum entry automatically gets picked up.
  3. The in check now works ("fontSize" in styleBlock is true).

Full patched function (drop-in)

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.


Diagnosing the still-missing style fields

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.

1. Ask Figma to “paint” inherited values

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.

2. Fall back to default values inside your server

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

Recommended minimal patch

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:

1 — One-to-one map of Figma → internal keys

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"
};

2 — Patch _getSingleTextStyle once

// 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};

Why this fixes the issue

  • 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:

Comprehensive Plan to Fix set_gradient Batch and gradientStyleId Issues

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

  1. 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.
  2. Apply Consistent Logging in GradientApplier.ts:
    • Action: Use replace_in_file to modify GradientApplier.ts.
      • Replace existing console.log statements with console.debug.
      • Ensure console.warn and console.error are used appropriately for warnings and errors.
    • 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 and gradientStyleId applications, without cluttering user-facing notifications.
  3. 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.
  4. Apply Consistent Logging in GradientProcessor.ts:
    • Action: Use replace_in_file to modify GradientProcessor.ts.
      • Replace existing console.log statements with console.debug.
      • Ensure console.warn and console.error are used appropriately in processGradientEntry and processCreateGradientStyleOperation methods.
    • 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.

Phase 2: Build & Deployment

  1. 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 and GradientProcessor.ts into the JavaScript bundle that the Figma plugin uses.
  2. 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.
  3. 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

  1. 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.
  2. 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.
  3. 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 and gradientStyleId failures.
    • Purpose: To identify the precise bug(s) that need to be fixed.

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.

Feedback on the Proposed Plan

1. Clarify the Real Goal

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-batch set_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.

2. Logging Strategy Tweaks

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 with console.debug.
  • Prefix each debug line with a consistent tag ([GradientApplier], [GradientProcessor]) so you can filter quickly.

3. Instrument Where the Failure Likely Happens

Based on previous traces, the two usual choke-points are:

  1. figma.getLocalPaintStylesAsync() occasionally returns an empty list during the first few milliseconds after plugin launch.
  2. 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.

4. Build & Restart Order

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.

5. Automated Self-Check after Build

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.

6. Time-Box Each Phase

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.

Revised Minimal-Effort Sequence

  1. Baseline test: run a single failing set_gradient call (non-batch) with the troublesome gradientStyleId. Note the exact error.
  2. Add two targeted debug lines in applyStyledGradientToNode (before getLocalPaintStylesAsync and before setFillStyleIdAsync).
  3. Re-build plugin only (cd src/plugin && bun run build) and reload the plugin (no server restart).
  4. Re-run the failing call; copy the console output.
  5. 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 but setFillStyleIdAsync throws, the style is from another file (keys vs ids mismatch).
  6. Implement the fix (e.g., map “S:” keys to local duplicates or add an exponential retry).
  7. Optional: only after the command works, replace generic console.log with console.debug project-wide.

Decision Point

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.


Why your set_gradient test is still failing

  1. The style never resolves The plugin’s applyStyledGradientToNode does 20 retries, each time calling figma.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.

Minimal, targeted fix (requires one code change)

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:

  1. style will be found on the first attempt.
  2. The gradient style is applied; no retry loop.
  3. The promise resolves cleanly, so the “not a function” path is never reached.

Recommended next steps

  1. Apply the one-line replace_in_file in GradientApplier.ts (lookup section).
  2. 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.

Super Detailed Plan to Solve set_gradient and gradientStyleId Issues

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)

  1. Objective: Ensure that style IDs are correctly matched during lookup, resolving the "style not found" error.
  2. Location of Fix: src/plugin/src/commands/styles/utils/gradient/GradientApplier.ts
  3. Specific Action (replace_in_file):
    • Context: Within the applyStyledGradientToNode method, locate the styles.find() call.
    • Change: Modify the comparison logic to strip any trailing commas from the s.id and s.key properties of the PaintStyle objects retrieved from Figma before comparing them with the incoming gradientStyleId.
    • 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.
// 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;
  1. 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.

Phase 2: Comprehensive Fresh Test (Automated via MCP Commands)

  1. 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.
  2. Pre-requisite: Phase 1 must be successfully completed and the plugin rebuilt.
  3. 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 their nodeIds 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 these nodeIds 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 exact ids 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 gradientStyleIds (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. and Successfully 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

  1. Objective: Analyze the fresh logs to confirm the primary fix and identify any new or persistent issues.
  2. 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.
  3. 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 in GradientApplier.ts), then a secondary fix in GradientProcessor.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:

1. Phase 1 (Critical Fix)

  • Keep the lookup change exactly as proposed but also sanitize the incoming gradientStyleId once, at the top of applyStyledGradientToNode:
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.

2. Phase 2 (Fresh Test)

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 subsequent set_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.

3. Phase 3 (Diagnosis)

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.

Go / No-Go

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:

  1. The reference to GradientApplier.applyStyledGradientToNode is itself undefined (for example, you imported GradientApplier incorrectly or are calling a renamed function).
  2. The argument sent to that function is undefined/null, and the calling code attempts to treat it like a function (e.g., undefined(...)).
  3. A promise-chain or .then handler on the result of processGradientEntry 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 or node, 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.


Key findings from the latest logs

  1. create_rectangle succeeds
    • All three rectangles (1110:295-297) were created without errors and the plugin returned their IDs on the first round-trip.
  2. create_gradient_style fails immediately
    • The plugin throws TypeError: not a function in StyleGradient.createGradientStyle before any paint-style creation logs appear.
    • Because the failure happens inside processCreateGradientStyleOperation, no call ever reaches figma.createPaintStyle()—the first debug line after that call is missing.
  3. Probable culprit: an accidentally overwritten helper
    • In GradientProcessor.processCreateGradientStyleOperation the very first validation uses the enum GradientTypeEnum and the helper GradientNormalizers.mapGradientType.
    • A common slip is shadowing GradientNormalizers with a non-function value (e.g., import * as … typo) or exporting mapGradientType as a constant during a refactor.
    • When the code later does GradientNormalizers.mapGradientType(gradientType) and mapGradientType is not a function, JavaScript throws exactly the error you see.
  4. Why the error bubbles as “not a function” at the server
    • StyleGradient.createGradientStyle catches any error and wraps it in a GradientOperationError.
    • That error is serialized and sent back through the socket; the server just prints the wrapper message.

Quick verification step

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.

Minimal fix

  1. Ensure proper export in GradientNormalizers.ts:
export class GradientNormalizers {
  static mapGradientType(type: string): "GRADIENT_LINEAR" | "GRADIENT_RADIAL" |  {
    
  }
  
}
  1. 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.)

  1. Re-build the plugin only, reload, and re-run the earlier three-style test.

What success should look like

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment