@mp3wizard/figma-console-mcp 1.19.0 → 1.19.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/dist/cloudflare/core/annotation-tools.js +230 -0
  2. package/dist/cloudflare/core/cloud-websocket-connector.js +93 -0
  3. package/dist/cloudflare/core/deep-component-tools.js +128 -0
  4. package/dist/cloudflare/core/design-code-tools.js +65 -7
  5. package/dist/cloudflare/core/enrichment/enrichment-service.js +108 -12
  6. package/dist/cloudflare/core/figjam-tools.js +485 -0
  7. package/dist/cloudflare/core/figma-api.js +7 -4
  8. package/dist/cloudflare/core/figma-desktop-connector.js +108 -0
  9. package/dist/cloudflare/core/figma-tools.js +445 -55
  10. package/dist/cloudflare/core/port-discovery.js +88 -0
  11. package/dist/cloudflare/core/resolve-package-root.js +11 -0
  12. package/dist/cloudflare/core/slides-tools.js +607 -0
  13. package/dist/cloudflare/core/websocket-connector.js +93 -0
  14. package/dist/cloudflare/core/websocket-server.js +18 -9
  15. package/dist/cloudflare/index.js +164 -41
  16. package/dist/core/figma-api.d.ts.map +1 -1
  17. package/dist/core/figma-api.js +5 -2
  18. package/dist/core/figma-api.js.map +1 -1
  19. package/dist/core/figma-tools.d.ts.map +1 -1
  20. package/dist/core/figma-tools.js +11 -6
  21. package/dist/core/figma-tools.js.map +1 -1
  22. package/dist/core/resolve-package-root.d.ts +2 -0
  23. package/dist/core/resolve-package-root.d.ts.map +1 -0
  24. package/dist/core/resolve-package-root.js +12 -0
  25. package/dist/core/resolve-package-root.js.map +1 -0
  26. package/dist/core/websocket-server.d.ts.map +1 -1
  27. package/dist/core/websocket-server.js +7 -9
  28. package/dist/core/websocket-server.js.map +1 -1
  29. package/dist/local.d.ts +6 -0
  30. package/dist/local.d.ts.map +1 -1
  31. package/dist/local.js +22 -0
  32. package/dist/local.js.map +1 -1
  33. package/figma-desktop-bridge/code.js +103 -4
  34. package/package.json +1 -1
@@ -0,0 +1,230 @@
1
+ /**
2
+ * Figma Annotations MCP Tools
3
+ * Tools for reading, writing, and managing design annotations on Figma nodes.
4
+ * Annotations are a Plugin API feature — requires Desktop Bridge plugin connection.
5
+ *
6
+ * Annotations are distinct from comments: they are node-level design specs that
7
+ * can pin specific properties (fills, width, typography, etc.) and support
8
+ * markdown-formatted labels. Designers use them to communicate animation timings,
9
+ * accessibility requirements, interaction specs, and other implementation details
10
+ * that don't fit in the description field.
11
+ */
12
+ import { z } from "zod";
13
+ import { createChildLogger } from "./logger.js";
14
+ const logger = createChildLogger({ component: "annotation-tools" });
15
+ // Valid AnnotationPropertyType values from the Figma Plugin API
16
+ // Sourced from @figma/plugin-typings — keep in sync with AnnotationPropertyType union
17
+ // Reference: https://developers.figma.com/docs/plugins/api/AnnotationProperty
18
+ const ANNOTATION_PROPERTY_TYPES = [
19
+ "width",
20
+ "height",
21
+ "maxWidth",
22
+ "minWidth",
23
+ "maxHeight",
24
+ "minHeight",
25
+ "fills",
26
+ "strokes",
27
+ "effects",
28
+ "strokeWeight",
29
+ "cornerRadius",
30
+ "textStyleId",
31
+ "textAlignHorizontal",
32
+ "fontFamily",
33
+ "fontStyle",
34
+ "fontSize",
35
+ "fontWeight",
36
+ "lineHeight",
37
+ "letterSpacing",
38
+ "itemSpacing",
39
+ "padding",
40
+ "layoutMode",
41
+ "alignItems",
42
+ "opacity",
43
+ "mainComponent",
44
+ "gridRowGap",
45
+ "gridColumnGap",
46
+ "gridRowCount",
47
+ "gridColumnCount",
48
+ "gridRowAnchorIndex",
49
+ "gridColumnAnchorIndex",
50
+ "gridRowSpan",
51
+ "gridColumnSpan",
52
+ ];
53
+ // Zod schema for annotation property
54
+ const annotationPropertySchema = z.object({
55
+ type: z
56
+ .enum(ANNOTATION_PROPERTY_TYPES)
57
+ .describe("Design property to pin (e.g., 'fills', 'width', 'fontSize')"),
58
+ });
59
+ // Zod schema for a single annotation
60
+ const annotationSchema = z.object({
61
+ label: z
62
+ .string()
63
+ .optional()
64
+ .describe("Plain text annotation label"),
65
+ labelMarkdown: z
66
+ .string()
67
+ .optional()
68
+ .describe("Rich text annotation label with markdown formatting. Supports bold, italic, links, lists, code, and headers."),
69
+ properties: z
70
+ .array(annotationPropertySchema)
71
+ .optional()
72
+ .describe("Design properties to pin to this annotation (e.g., fills, width, fontSize)"),
73
+ categoryId: z
74
+ .string()
75
+ .optional()
76
+ .describe("Annotation category ID. Use figma_get_annotation_categories to list available categories."),
77
+ });
78
+ // ============================================================================
79
+ // Tool Registration
80
+ // ============================================================================
81
+ export function registerAnnotationTools(server, getDesktopConnector) {
82
+ // -----------------------------------------------------------------------
83
+ // Tool: figma_get_annotations
84
+ // -----------------------------------------------------------------------
85
+ server.tool("figma_get_annotations", "Read annotations from a Figma node. Annotations are designer-authored specs attached to nodes — they can include notes (plain text or markdown), pinned design properties (fills, width, fontSize, etc.), and category labels. Use this to discover animation timings, interaction specs, accessibility requirements, and other implementation details that designers annotate directly on the design. Set include_children=true to get annotations from child nodes too (useful for full component documentation). Requires Desktop Bridge plugin.", {
86
+ nodeId: z
87
+ .string()
88
+ .describe("Node ID to read annotations from (e.g., '695:313')"),
89
+ include_children: z.preprocess((v) => (typeof v === "string" ? v === "true" : v), z.boolean().optional().default(false)).describe("Also read annotations from child nodes. Useful for getting all annotations within a component tree."),
90
+ depth: z.preprocess((v) => (typeof v === "string" ? Number(v) : v), z.number().optional().default(1)).describe("How many levels deep to traverse when include_children is true (default: 1, max recommended: 5)"),
91
+ }, async ({ nodeId, include_children = false, depth = 1 }) => {
92
+ try {
93
+ logger.info({ nodeId, include_children, depth }, "Getting annotations");
94
+ const connector = await getDesktopConnector();
95
+ const result = await connector.getAnnotations(nodeId, include_children, Math.min(depth, 10));
96
+ if (!result || (result.success === false)) {
97
+ throw new Error(result?.error || "Failed to get annotations");
98
+ }
99
+ // The result may come back as { success, data } (WebSocket) or directly as data
100
+ const data = result.data || result;
101
+ return {
102
+ content: [
103
+ {
104
+ type: "text",
105
+ text: JSON.stringify(data),
106
+ },
107
+ ],
108
+ };
109
+ }
110
+ catch (error) {
111
+ const message = error instanceof Error ? error.message : String(error);
112
+ logger.error({ error }, "Failed to get annotations");
113
+ return {
114
+ content: [
115
+ {
116
+ type: "text",
117
+ text: JSON.stringify({
118
+ error: "get_annotations_failed",
119
+ message: `Cannot get annotations. ${message}`,
120
+ hint: "Annotations require the Desktop Bridge plugin to be running in Figma.",
121
+ }),
122
+ },
123
+ ],
124
+ isError: true,
125
+ };
126
+ }
127
+ });
128
+ // -----------------------------------------------------------------------
129
+ // Tool: figma_set_annotations
130
+ // -----------------------------------------------------------------------
131
+ server.tool("figma_set_annotations", "Write or clear annotations on a Figma node. Annotations communicate design specs to developers — use them to document animation timings, easing curves, interaction behaviors, accessibility requirements, and implementation notes. Supports plain text labels, rich markdown labels, pinned design properties, and annotation categories. Pass an empty array to clear all annotations. Use mode='append' to add to existing annotations, or mode='replace' (default) to overwrite. Requires Desktop Bridge plugin. This operation is undoable in Figma (Cmd+Z).", {
132
+ nodeId: z
133
+ .string()
134
+ .describe("Node ID to write annotations to (e.g., '695:313')"),
135
+ annotations: z.preprocess((v) => {
136
+ if (typeof v === "string") {
137
+ try {
138
+ return JSON.parse(v);
139
+ }
140
+ catch {
141
+ return v;
142
+ }
143
+ }
144
+ return v;
145
+ }, z.array(annotationSchema)).describe("Array of annotations to set. Each annotation can have a label (plain or markdown), pinned properties, and a category. Pass an empty array [] to clear all annotations."),
146
+ mode: z
147
+ .enum(["replace", "append"])
148
+ .optional()
149
+ .default("replace")
150
+ .describe("'replace' (default) overwrites all existing annotations. 'append' adds new annotations while keeping existing ones."),
151
+ }, async ({ nodeId, annotations, mode = "replace" }) => {
152
+ try {
153
+ logger.info({ nodeId, count: annotations.length, mode }, "Setting annotations");
154
+ const connector = await getDesktopConnector();
155
+ const result = await connector.setAnnotations(nodeId, annotations, mode);
156
+ if (!result || (result.success === false)) {
157
+ throw new Error(result?.error || "Failed to set annotations");
158
+ }
159
+ const data = result.data || result;
160
+ return {
161
+ content: [
162
+ {
163
+ type: "text",
164
+ text: JSON.stringify({
165
+ success: true,
166
+ ...data,
167
+ note: "Annotations set successfully. This operation is undoable in Figma (Cmd+Z).",
168
+ }),
169
+ },
170
+ ],
171
+ };
172
+ }
173
+ catch (error) {
174
+ const message = error instanceof Error ? error.message : String(error);
175
+ logger.error({ error }, "Failed to set annotations");
176
+ return {
177
+ content: [
178
+ {
179
+ type: "text",
180
+ text: JSON.stringify({
181
+ error: "set_annotations_failed",
182
+ message: `Cannot set annotations. ${message}`,
183
+ hint: "Annotations require the Desktop Bridge plugin to be running in Figma.",
184
+ }),
185
+ },
186
+ ],
187
+ isError: true,
188
+ };
189
+ }
190
+ });
191
+ // -----------------------------------------------------------------------
192
+ // Tool: figma_get_annotation_categories
193
+ // -----------------------------------------------------------------------
194
+ server.tool("figma_get_annotation_categories", "List available annotation categories in the current Figma file. Categories group annotations by purpose (e.g., interactions, accessibility, development notes). Use the returned category IDs when creating annotations with figma_set_annotations. Requires Desktop Bridge plugin.", {}, async () => {
195
+ try {
196
+ logger.info("Getting annotation categories");
197
+ const connector = await getDesktopConnector();
198
+ const result = await connector.getAnnotationCategories();
199
+ if (!result || (result.success === false)) {
200
+ throw new Error(result?.error || "Failed to get annotation categories");
201
+ }
202
+ const data = result.data || result;
203
+ return {
204
+ content: [
205
+ {
206
+ type: "text",
207
+ text: JSON.stringify(data),
208
+ },
209
+ ],
210
+ };
211
+ }
212
+ catch (error) {
213
+ const message = error instanceof Error ? error.message : String(error);
214
+ logger.error({ error }, "Failed to get annotation categories");
215
+ return {
216
+ content: [
217
+ {
218
+ type: "text",
219
+ text: JSON.stringify({
220
+ error: "get_annotation_categories_failed",
221
+ message: `Cannot get annotation categories. ${message}`,
222
+ hint: "Annotation categories require the Desktop Bridge plugin to be running in Figma.",
223
+ }),
224
+ },
225
+ ],
226
+ isError: true,
227
+ };
228
+ }
229
+ });
230
+ }
@@ -130,6 +130,24 @@ export class CloudWebSocketConnector {
130
130
  async setNodeDescription(nodeId, description, descriptionMarkdown) {
131
131
  return this.sendCommand('SET_NODE_DESCRIPTION', { nodeId, description, descriptionMarkdown });
132
132
  }
133
+ // ============================================================================
134
+ // Annotation operations
135
+ // ============================================================================
136
+ async getAnnotations(nodeId, includeChildren, depth) {
137
+ return this.sendCommand('GET_ANNOTATIONS', { nodeId, includeChildren, depth }, 10000);
138
+ }
139
+ async setAnnotations(nodeId, annotations, mode) {
140
+ return this.sendCommand('SET_ANNOTATIONS', { nodeId, annotations, mode: mode || 'replace' });
141
+ }
142
+ async getAnnotationCategories() {
143
+ return this.sendCommand('GET_ANNOTATION_CATEGORIES', {}, 5000);
144
+ }
145
+ async deepGetComponent(nodeId, depth) {
146
+ return this.sendCommand('DEEP_GET_COMPONENT', { nodeId, depth: depth || 10 }, 30000);
147
+ }
148
+ async analyzeComponentSet(nodeId) {
149
+ return this.sendCommand('ANALYZE_COMPONENT_SET', { nodeId }, 30000);
150
+ }
133
151
  async addComponentProperty(nodeId, propertyName, type, defaultValue, options) {
134
152
  const params = { nodeId, propertyName, propertyType: type, defaultValue };
135
153
  if (options?.preferredValues)
@@ -244,6 +262,81 @@ export class CloudWebSocketConnector {
244
262
  return this.sendCommand('LINT_DESIGN', params, 120000);
245
263
  }
246
264
  // ============================================================================
265
+ // FigJam operations
266
+ // ============================================================================
267
+ async createSticky(params) {
268
+ return this.sendCommand('CREATE_STICKY', params);
269
+ }
270
+ async createStickies(params) {
271
+ return this.sendCommand('CREATE_STICKIES', params, 30000);
272
+ }
273
+ async createConnector(params) {
274
+ return this.sendCommand('CREATE_CONNECTOR', params);
275
+ }
276
+ async createShapeWithText(params) {
277
+ return this.sendCommand('CREATE_SHAPE_WITH_TEXT', params);
278
+ }
279
+ async createTable(params) {
280
+ return this.sendCommand('CREATE_TABLE', params, 30000);
281
+ }
282
+ async createCodeBlock(params) {
283
+ return this.sendCommand('CREATE_CODE_BLOCK', params);
284
+ }
285
+ async getBoardContents(params) {
286
+ return this.sendCommand('GET_BOARD_CONTENTS', params, 30000);
287
+ }
288
+ async getConnections() {
289
+ return this.sendCommand('GET_CONNECTIONS', {}, 15000);
290
+ }
291
+ // ============================================================================
292
+ // Slides operations
293
+ // ============================================================================
294
+ async listSlides() {
295
+ return this.sendCommand('LIST_SLIDES', {}, 10000);
296
+ }
297
+ async getSlideContent(params) {
298
+ return this.sendCommand('GET_SLIDE_CONTENT', params, 10000);
299
+ }
300
+ async createSlide(params) {
301
+ return this.sendCommand('CREATE_SLIDE', params, 10000);
302
+ }
303
+ async deleteSlide(params) {
304
+ return this.sendCommand('DELETE_SLIDE', params, 5000);
305
+ }
306
+ async duplicateSlide(params) {
307
+ return this.sendCommand('DUPLICATE_SLIDE', params, 5000);
308
+ }
309
+ async getSlideGrid() {
310
+ return this.sendCommand('GET_SLIDE_GRID', {}, 10000);
311
+ }
312
+ async reorderSlides(params) {
313
+ return this.sendCommand('REORDER_SLIDES', params, 15000);
314
+ }
315
+ async setSlideTransition(params) {
316
+ return this.sendCommand('SET_SLIDE_TRANSITION', params, 5000);
317
+ }
318
+ async getSlideTransition(params) {
319
+ return this.sendCommand('GET_SLIDE_TRANSITION', params, 5000);
320
+ }
321
+ async setSlidesViewMode(params) {
322
+ return this.sendCommand('SET_SLIDES_VIEW_MODE', params, 5000);
323
+ }
324
+ async getFocusedSlide() {
325
+ return this.sendCommand('GET_FOCUSED_SLIDE', {}, 5000);
326
+ }
327
+ async focusSlide(params) {
328
+ return this.sendCommand('FOCUS_SLIDE', params, 5000);
329
+ }
330
+ async skipSlide(params) {
331
+ return this.sendCommand('SKIP_SLIDE', params, 5000);
332
+ }
333
+ async addTextToSlide(params) {
334
+ return this.sendCommand('ADD_TEXT_TO_SLIDE', params, 10000);
335
+ }
336
+ async addShapeToSlide(params) {
337
+ return this.sendCommand('ADD_SHAPE_TO_SLIDE', params, 5000);
338
+ }
339
+ // ============================================================================
247
340
  // Cache management (no-op for cloud relay)
248
341
  // ============================================================================
249
342
  clearFrameCache() {
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Deep Component Extraction MCP Tool
3
+ *
4
+ * Provides unlimited-depth component tree extraction via the Desktop Bridge
5
+ * Plugin API. Returns full visual properties, resolved design token names,
6
+ * instance references (mainComponent), prototype reactions, and annotations
7
+ * at every level of the tree.
8
+ *
9
+ * This complements figma_get_component_for_development (REST API, depth 4)
10
+ * with deeper, richer data when the Desktop Bridge plugin is connected.
11
+ */
12
+ import { z } from "zod";
13
+ import { createChildLogger } from "./logger.js";
14
+ const logger = createChildLogger({ component: "deep-component-tools" });
15
+ export function registerDeepComponentTools(server, getDesktopConnector) {
16
+ server.tool("figma_get_component_for_development_deep", "Get a deeply nested component tree with full visual properties, resolved design token names, instance references, prototype interactions, and annotations at every level. Uses the Desktop Bridge Plugin API for unlimited depth traversal — essential for complex components like data tables, nested menus, date pickers, and compound form fields where the standard depth-4 REST API tool misses deeper structure. Returns boundVariables resolved to actual token names (not just IDs), mainComponent references for INSTANCE nodes, and reactions for interaction states. Requires Desktop Bridge plugin. For simpler components (depth ≤ 4), use figma_get_component_for_development instead.", {
17
+ nodeId: z
18
+ .string()
19
+ .describe("Component node ID to extract (e.g., '695:313')"),
20
+ depth: z.preprocess((v) => (typeof v === "string" ? Number(v) : v), z.number().optional().default(10)).describe("Maximum tree depth to traverse (default: 10, max: 20). Use higher values for deeply nested components."),
21
+ }, async ({ nodeId, depth = 10 }) => {
22
+ try {
23
+ const clampedDepth = Math.min(Math.max(depth, 1), 20);
24
+ logger.info({ nodeId, depth: clampedDepth }, "Deep component extraction");
25
+ const connector = await getDesktopConnector();
26
+ const result = await connector.deepGetComponent(nodeId, clampedDepth);
27
+ if (!result || (result.success === false)) {
28
+ throw new Error(result?.error || "Failed to extract component");
29
+ }
30
+ const data = result.data || result;
31
+ // Measure response size and warn if large
32
+ const responseJson = JSON.stringify(data);
33
+ const sizeKB = Math.round(responseJson.length / 1024);
34
+ const response = {
35
+ nodeId,
36
+ component: data,
37
+ metadata: {
38
+ purpose: "deep_component_development",
39
+ treeDepth: clampedDepth,
40
+ responseSizeKB: sizeKB,
41
+ variablesResolved: data._variableMapSize || 0,
42
+ note: [
43
+ `Deep component tree extracted via Plugin API (depth ${clampedDepth}).`,
44
+ "boundVariables are resolved to token names, collections, and codeSyntax.",
45
+ "INSTANCE nodes include mainComponent references (key, name, component set).",
46
+ "Use this data to generate production-quality, token-aware, accessible code.",
47
+ sizeKB > 200 ? "Response is large — consider targeting a specific child node for deeper analysis." : null,
48
+ ].filter(Boolean).join(" "),
49
+ },
50
+ };
51
+ return {
52
+ content: [
53
+ {
54
+ type: "text",
55
+ text: JSON.stringify(response),
56
+ },
57
+ ],
58
+ };
59
+ }
60
+ catch (error) {
61
+ const message = error instanceof Error ? error.message : String(error);
62
+ logger.error({ error }, "Deep component extraction failed");
63
+ return {
64
+ content: [
65
+ {
66
+ type: "text",
67
+ text: JSON.stringify({
68
+ error: "deep_component_failed",
69
+ message: `Cannot extract deep component. ${message}`,
70
+ hint: "This tool requires the Desktop Bridge plugin to be running in Figma. For REST API fallback (depth 4), use figma_get_component_for_development.",
71
+ }),
72
+ },
73
+ ],
74
+ isError: true,
75
+ };
76
+ }
77
+ });
78
+ // -----------------------------------------------------------------------
79
+ // Tool: figma_analyze_component_set
80
+ // -----------------------------------------------------------------------
81
+ server.tool("figma_analyze_component_set", "Analyze a Figma COMPONENT_SET to extract variant state machine and cross-variant diffs for code generation. Returns: (1) variant axes (size, state) with all values, (2) CSS pseudo-class mappings for interaction states (hover→:hover, focus→:focus-visible, disabled→:disabled, error→[aria-invalid]), (3) visual diff from default state per variant (only changed properties — fill token, stroke token, stroke weight, text color, opacity, effects, visibility), (4) component property definitions mapped to code props (BOOLEAN→boolean, TEXT→string, INSTANCE_SWAP→slot/ReactNode). Use this on the parent COMPONENT_SET node, not individual variants. Requires Desktop Bridge plugin.", {
82
+ nodeId: z
83
+ .string()
84
+ .describe("COMPONENT_SET node ID (the parent of all variants, e.g., '214:274')"),
85
+ }, async ({ nodeId }) => {
86
+ try {
87
+ logger.info({ nodeId }, "Analyzing component set");
88
+ const connector = await getDesktopConnector();
89
+ const result = await connector.analyzeComponentSet(nodeId);
90
+ if (!result || (result.success === false)) {
91
+ throw new Error(result?.error || "Failed to analyze component set");
92
+ }
93
+ const data = result.data || result;
94
+ return {
95
+ content: [
96
+ {
97
+ type: "text",
98
+ text: JSON.stringify({
99
+ nodeId,
100
+ analysis: data,
101
+ metadata: {
102
+ purpose: "variant_state_machine",
103
+ note: "Use cssMapping to implement interaction states as CSS pseudo-classes/attributes. diffFromDefault shows only what changes per variant — apply as style overrides. componentProps maps to your component's TypeScript interface.",
104
+ },
105
+ }),
106
+ },
107
+ ],
108
+ };
109
+ }
110
+ catch (error) {
111
+ const message = error instanceof Error ? error.message : String(error);
112
+ logger.error({ error }, "Component set analysis failed");
113
+ return {
114
+ content: [
115
+ {
116
+ type: "text",
117
+ text: JSON.stringify({
118
+ error: "analyze_component_set_failed",
119
+ message: `Cannot analyze component set. ${message}`,
120
+ hint: "This tool requires the Desktop Bridge plugin and a COMPONENT_SET node ID (not an individual variant). Use figma_search_components to find component sets.",
121
+ }),
122
+ },
123
+ ],
124
+ isError: true,
125
+ };
126
+ }
127
+ });
128
+ }
@@ -1748,6 +1748,50 @@ function generateAccessibilitySection(node, parsedDesc, codeInfo) {
1748
1748
  }
1749
1749
  return lines.join("\n");
1750
1750
  }
1751
+ function generateDesignAnnotationsSection(node) {
1752
+ // Annotations come from the Desktop Bridge plugin (node.annotations)
1753
+ // They are available when the component was fetched via Desktop Bridge
1754
+ const annotations = node.annotations || [];
1755
+ if (annotations.length === 0) {
1756
+ return [
1757
+ "",
1758
+ "## Design Annotations",
1759
+ "",
1760
+ "_No design annotations found on this node. Designers can add annotations in Dev Mode to specify animation timings, easing curves, interaction behaviors, and other implementation details._",
1761
+ "_Use `figma_get_annotations` with `include_children=true` to check child nodes for annotations._",
1762
+ "",
1763
+ ].join("\n");
1764
+ }
1765
+ const lines = ["", "## Design Annotations", ""];
1766
+ lines.push(`Found **${annotations.length}** annotation(s) on this component:`, "");
1767
+ for (let i = 0; i < annotations.length; i++) {
1768
+ const ann = annotations[i];
1769
+ const num = i + 1;
1770
+ // Header with category if available
1771
+ const categoryLabel = ann.categoryName ? ` (${ann.categoryName})` : (ann.categoryId ? ` (category: ${ann.categoryId})` : "");
1772
+ lines.push(`### Annotation ${num}${categoryLabel}`);
1773
+ lines.push("");
1774
+ // Label content
1775
+ if (ann.labelMarkdown) {
1776
+ // Indent markdown content and render it
1777
+ lines.push(ann.labelMarkdown);
1778
+ lines.push("");
1779
+ }
1780
+ else if (ann.label) {
1781
+ lines.push(ann.label);
1782
+ lines.push("");
1783
+ }
1784
+ // Pinned properties
1785
+ if (ann.properties && ann.properties.length > 0) {
1786
+ lines.push("**Pinned Properties:**");
1787
+ for (const prop of ann.properties) {
1788
+ lines.push(`- \`${prop.type}\``);
1789
+ }
1790
+ lines.push("");
1791
+ }
1792
+ }
1793
+ return lines.join("\n");
1794
+ }
1751
1795
  function generateChangelogSection(codeInfo) {
1752
1796
  if (!codeInfo?.changelog || codeInfo.changelog.length === 0)
1753
1797
  return "";
@@ -2083,7 +2127,7 @@ export function registerDesignCodeTools(server, getFigmaAPI, getCurrentUrl, vari
2083
2127
  logger.info({ fileKey, nodeId, canonicalSource, enrich }, "Starting design-code parity check");
2084
2128
  const api = await getFigmaAPI();
2085
2129
  // Fetch component node
2086
- const nodesResponse = await api.getNodes(fileKey, [nodeId], { depth: 2 });
2130
+ const nodesResponse = await api.getNodes(fileKey, [nodeId], { depth: 4 });
2087
2131
  const nodeData = nodesResponse?.nodes?.[nodeId];
2088
2132
  if (!nodeData?.document) {
2089
2133
  throw new Error(`Node ${nodeId} not found in file ${fileKey}`);
@@ -2234,7 +2278,7 @@ export function registerDesignCodeTools(server, getFigmaAPI, getCurrentUrl, vari
2234
2278
  // -----------------------------------------------------------------------
2235
2279
  // Tool: figma_generate_component_doc
2236
2280
  // -----------------------------------------------------------------------
2237
- server.tool("figma_generate_component_doc", "Generate AI-complete component documentation from a Figma component. Produces structured markdown with anatomy, per-variant color tokens, typography, content guidelines (parsed from Figma description), icon mapping, spacing tokens, and design-code parity analysis. Merges Figma design data with optional code-side info (CVA definitions, sub-component APIs, source files). Output works with any docs platform. For richest output, read the component source code first and pass codeInfo.", {
2281
+ server.tool("figma_generate_component_doc", "Generate AI-complete component documentation from a Figma component. Produces structured markdown with anatomy, per-variant color tokens, typography, content guidelines (parsed from Figma description), design annotations (animation timings, interaction specs, accessibility notes from Dev Mode), icon mapping, spacing tokens, and design-code parity analysis. Merges Figma design data with optional code-side info (CVA definitions, sub-component APIs, source files). Output works with any docs platform. For richest output, read the component source code first and pass codeInfo.", {
2238
2282
  fileUrl: z
2239
2283
  .string()
2240
2284
  .url()
@@ -2252,6 +2296,7 @@ export function registerDesignCodeTools(server, getFigmaAPI, getCurrentUrl, vari
2252
2296
  behavior: z.boolean().optional().default(false),
2253
2297
  implementation: z.boolean().optional().default(true),
2254
2298
  accessibility: z.boolean().optional().default(true),
2299
+ designAnnotations: z.boolean().optional().default(true),
2255
2300
  relatedComponents: z.boolean().optional().default(false),
2256
2301
  changelog: z.boolean().optional().default(true),
2257
2302
  parity: z.boolean().optional().default(true),
@@ -2350,19 +2395,27 @@ export function registerDesignCodeTools(server, getFigmaAPI, getCurrentUrl, vari
2350
2395
  // REST API getNodes() often returns empty description for COMPONENT_SET nodes,
2351
2396
  // so fall back to Desktop Bridge plugin API which has the reliable description.
2352
2397
  let description = node.descriptionMarkdown || node.description || componentMeta?.description || "";
2353
- if (!description && getDesktopConnector) {
2398
+ if (getDesktopConnector) {
2354
2399
  try {
2355
2400
  const connector = await getDesktopConnector();
2356
2401
  const bridgeResult = await connector.getComponentFromPluginUI(nodeId);
2357
2402
  if (bridgeResult.success && bridgeResult.component) {
2358
- description = bridgeResult.component.descriptionMarkdown || bridgeResult.component.description || "";
2359
- if (description) {
2360
- logger.info("Fetched description via Desktop Bridge (REST API returned empty)");
2403
+ // Fetch description from bridge if REST API returned empty
2404
+ if (!description) {
2405
+ description = bridgeResult.component.descriptionMarkdown || bridgeResult.component.description || "";
2406
+ if (description) {
2407
+ logger.info("Fetched description via Desktop Bridge (REST API returned empty)");
2408
+ }
2409
+ }
2410
+ // Always fetch annotations from bridge (REST API never has them)
2411
+ if (bridgeResult.component.annotations && bridgeResult.component.annotations.length > 0) {
2412
+ node.annotations = bridgeResult.component.annotations;
2413
+ logger.info({ count: node.annotations.length }, "Fetched annotations via Desktop Bridge for documentation");
2361
2414
  }
2362
2415
  }
2363
2416
  }
2364
2417
  catch {
2365
- logger.warn("Desktop Bridge description fetch failed, proceeding without description");
2418
+ logger.warn("Desktop Bridge fetch failed, proceeding without bridge-sourced data");
2366
2419
  }
2367
2420
  }
2368
2421
  const fileUrl_ = `${url}?node-id=${nodeId.replace(":", "-")}`;
@@ -2441,6 +2494,11 @@ export function registerDesignCodeTools(server, getFigmaAPI, getCurrentUrl, vari
2441
2494
  parts.push(generateAccessibilitySection(node, parsedDesc, codeInfo));
2442
2495
  includedSections.push("accessibility");
2443
2496
  }
2497
+ if (s.designAnnotations !== false) {
2498
+ const annotationsSection = generateDesignAnnotationsSection(node);
2499
+ parts.push(annotationsSection);
2500
+ includedSections.push("designAnnotations");
2501
+ }
2444
2502
  if (s.parity && hasCodeInfo && hasFigmaData && codeInfo) {
2445
2503
  const paritySection = generateParitySection(node, codeInfo);
2446
2504
  if (paritySection) {