@fridaplatform-stk/figma2frida-mcp 1.0.0

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.
@@ -0,0 +1,2840 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/figma2frida/figma2frida.ts
4
+ import "dotenv/config";
5
+ import { z } from "zod";
6
+ import yargs from "yargs";
7
+ import { hideBin } from "yargs/helpers";
8
+ import { FastMCP } from "fastmcp";
9
+
10
+ // src/figma2frida/clients/figma-client.ts
11
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
12
+ import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
13
+ var FigmaClient = class {
14
+ client = null;
15
+ connected = false;
16
+ figmaUrl;
17
+ constructor(figmaUrl = "http://127.0.0.1:3845/sse") {
18
+ this.figmaUrl = figmaUrl;
19
+ }
20
+ /**
21
+ * Connect to Figma MCP server
22
+ */
23
+ async connect(retries = 3) {
24
+ if (this.connected && this.client) {
25
+ console.log("[FIGMA CLIENT] Already connected, skipping connection attempt");
26
+ return;
27
+ }
28
+ console.log(`[FIGMA CLIENT] ========================================`);
29
+ console.log(`[FIGMA CLIENT] \u{1F504} Starting connection attempt`);
30
+ console.log(`[FIGMA CLIENT] URL: ${this.figmaUrl}`);
31
+ console.log(`[FIGMA CLIENT] Max retries: ${retries}`);
32
+ console.log(`[FIGMA CLIENT] Current status: connected=${this.connected}, client=${!!this.client}`);
33
+ console.log(`[FIGMA CLIENT] ========================================`);
34
+ for (let attempt = 0; attempt < retries; attempt++) {
35
+ const attemptStartTime = Date.now();
36
+ console.log(`[FIGMA CLIENT] \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
37
+ console.log(`[FIGMA CLIENT] Attempt ${attempt + 1}/${retries} starting...`);
38
+ try {
39
+ console.log(`[FIGMA CLIENT] Creating MCP Client object...`);
40
+ this.client = new Client(
41
+ {
42
+ name: "figma-proxy-client",
43
+ version: "1.0.0"
44
+ },
45
+ {
46
+ capabilities: {
47
+ tools: {}
48
+ }
49
+ }
50
+ );
51
+ console.log(`[FIGMA CLIENT] \u2713 Client object created`);
52
+ console.log(`[FIGMA CLIENT] Creating SSE Transport for: ${this.figmaUrl}`);
53
+ const transport = new SSEClientTransport(new URL(this.figmaUrl));
54
+ console.log(`[FIGMA CLIENT] \u2713 Transport object created`);
55
+ console.log(`[FIGMA CLIENT] Attempting to connect...`);
56
+ const connectStartTime = Date.now();
57
+ await this.client.connect(transport);
58
+ const connectTime = Date.now() - connectStartTime;
59
+ console.log(`[FIGMA CLIENT] \u2713 Connection established in ${connectTime}ms`);
60
+ this.connected = true;
61
+ const totalTime = Date.now() - attemptStartTime;
62
+ console.log(`[FIGMA CLIENT] ========================================`);
63
+ console.log(`[FIGMA CLIENT] \u2705 CONNECTION SUCCESS!`);
64
+ console.log(`[FIGMA CLIENT] URL: ${this.figmaUrl}`);
65
+ console.log(`[FIGMA CLIENT] Attempt: ${attempt + 1}/${retries}`);
66
+ console.log(`[FIGMA CLIENT] Total time: ${totalTime}ms`);
67
+ console.log(`[FIGMA CLIENT] ========================================`);
68
+ return;
69
+ } catch (error) {
70
+ const attemptTime = Date.now() - attemptStartTime;
71
+ console.log(`[FIGMA CLIENT] \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
72
+ console.log(`[FIGMA CLIENT] \u274C Connection attempt ${attempt + 1}/${retries} FAILED`);
73
+ console.log(`[FIGMA CLIENT] Time elapsed: ${attemptTime}ms`);
74
+ console.log(`[FIGMA CLIENT] Error type: ${error?.constructor?.name || typeof error}`);
75
+ console.log(`[FIGMA CLIENT] Error message: ${error instanceof Error ? error.message : String(error)}`);
76
+ if (error && typeof error === "object") {
77
+ const errorObj = error;
78
+ if ("code" in errorObj) {
79
+ console.log(`[FIGMA CLIENT] Error code: ${errorObj.code}`);
80
+ }
81
+ if ("cause" in errorObj) {
82
+ console.log(`[FIGMA CLIENT] Error cause:`, errorObj.cause);
83
+ }
84
+ if ("status" in errorObj) {
85
+ console.log(`[FIGMA CLIENT] HTTP status: ${errorObj.status}`);
86
+ }
87
+ if ("statusText" in errorObj) {
88
+ console.log(`[FIGMA CLIENT] HTTP status text: ${errorObj.statusText}`);
89
+ }
90
+ }
91
+ if (error instanceof Error && error.stack) {
92
+ console.log(`[FIGMA CLIENT] Error stack:`);
93
+ console.log(error.stack);
94
+ }
95
+ if (attempt === retries - 1) {
96
+ console.log(`[FIGMA CLIENT] ========================================`);
97
+ console.log(`[FIGMA CLIENT] \u274C ALL CONNECTION ATTEMPTS FAILED`);
98
+ console.log(`[FIGMA CLIENT] Total attempts: ${retries}`);
99
+ console.log(`[FIGMA CLIENT] URL: ${this.figmaUrl}`);
100
+ console.log(`[FIGMA CLIENT] ========================================`);
101
+ throw new Error(
102
+ `Failed to connect to Figma MCP at ${this.figmaUrl} after ${retries} attempts. Make sure Figma Desktop is running with MCP enabled.`
103
+ );
104
+ }
105
+ const waitTime = 1e3 * (attempt + 1);
106
+ console.log(`[FIGMA CLIENT] Waiting ${waitTime}ms before retry...`);
107
+ await new Promise(
108
+ (resolve) => setTimeout(resolve, waitTime)
109
+ );
110
+ console.log(`[FIGMA CLIENT] Wait complete, retrying...`);
111
+ }
112
+ }
113
+ }
114
+ /**
115
+ * Ensure connection is established
116
+ */
117
+ async ensureConnected() {
118
+ if (!this.connected || !this.client) {
119
+ console.log("[FIGMA CLIENT] ensureConnected() called - not connected, attempting connection...");
120
+ await this.connect();
121
+ } else {
122
+ console.log("[FIGMA CLIENT] ensureConnected() called - already connected, skipping");
123
+ }
124
+ }
125
+ /**
126
+ * Call Figma's get_design_context tool
127
+ */
128
+ async getDesignContext(nodeId, options) {
129
+ await this.ensureConnected();
130
+ const args = {
131
+ ...nodeId ? { nodeId } : {},
132
+ clientLanguages: options?.clientLanguages || "typescript",
133
+ clientFrameworks: options?.clientFrameworks || "react",
134
+ forceCode: options?.forceCode ?? true
135
+ };
136
+ console.log(`[FIGMA CLIENT] Calling get_design_context with args:`, args);
137
+ try {
138
+ const result = await this.client.callTool({
139
+ name: "get_design_context",
140
+ arguments: args
141
+ });
142
+ console.log(`[FIGMA CLIENT] get_design_context response type:`, typeof result);
143
+ console.log(
144
+ `[FIGMA CLIENT] get_design_context response keys:`,
145
+ Object.keys(result)
146
+ );
147
+ return result;
148
+ } catch (error) {
149
+ console.warn("Error calling get_design_context, attempting reconnect...");
150
+ this.connected = false;
151
+ await this.connect();
152
+ const result = await this.client.callTool({
153
+ name: "get_design_context",
154
+ arguments: {
155
+ ...nodeId ? { nodeId } : {},
156
+ clientLanguages: options?.clientLanguages || "typescript",
157
+ clientFrameworks: options?.clientFrameworks || "react",
158
+ forceCode: options?.forceCode ?? true
159
+ }
160
+ });
161
+ return result;
162
+ }
163
+ }
164
+ /**
165
+ * Call Figma's get_metadata tool to get actual node structure
166
+ */
167
+ async getMetadata(nodeId, options) {
168
+ await this.ensureConnected();
169
+ const args = {
170
+ ...nodeId ? { nodeId } : {},
171
+ clientLanguages: options?.clientLanguages || "typescript",
172
+ clientFrameworks: options?.clientFrameworks || "react"
173
+ };
174
+ console.log(`[FIGMA CLIENT] Calling get_metadata with args:`, args);
175
+ try {
176
+ const result = await this.client.callTool({
177
+ name: "get_metadata",
178
+ arguments: args
179
+ });
180
+ console.log(`[FIGMA CLIENT] get_metadata response type:`, typeof result);
181
+ console.log(
182
+ `[FIGMA CLIENT] get_metadata response keys:`,
183
+ Object.keys(result)
184
+ );
185
+ return result;
186
+ } catch (error) {
187
+ console.warn("Error calling get_metadata, attempting reconnect...");
188
+ this.connected = false;
189
+ await this.connect();
190
+ const result = await this.client.callTool({
191
+ name: "get_metadata",
192
+ arguments: args
193
+ });
194
+ return result;
195
+ }
196
+ }
197
+ /**
198
+ * Call Figma's get_screenshot tool
199
+ */
200
+ async getScreenshot(nodeId, options) {
201
+ await this.ensureConnected();
202
+ try {
203
+ const result = await this.client.callTool({
204
+ name: "get_screenshot",
205
+ arguments: {
206
+ ...nodeId ? { nodeId } : {},
207
+ clientLanguages: options?.clientLanguages || "typescript",
208
+ clientFrameworks: options?.clientFrameworks || "react"
209
+ }
210
+ });
211
+ return result;
212
+ } catch (error) {
213
+ console.warn("Error calling get_screenshot, attempting reconnect...");
214
+ this.connected = false;
215
+ await this.connect();
216
+ const result = await this.client.callTool({
217
+ name: "get_screenshot",
218
+ arguments: {
219
+ ...nodeId ? { nodeId } : {},
220
+ clientLanguages: options?.clientLanguages || "typescript",
221
+ clientFrameworks: options?.clientFrameworks || "react"
222
+ }
223
+ });
224
+ return result;
225
+ }
226
+ }
227
+ /**
228
+ * Close connection
229
+ */
230
+ async disconnect() {
231
+ if (this.client) {
232
+ await this.client.close();
233
+ this.connected = false;
234
+ this.client = null;
235
+ console.log("Disconnected from Figma MCP");
236
+ }
237
+ }
238
+ };
239
+
240
+ // src/figma2frida/handlers/component-suggestion-handler.ts
241
+ import "dotenv/config";
242
+
243
+ // src/figma2frida/services/pinecone/pinecone-service.ts
244
+ import { Pinecone } from "@pinecone-database/pinecone";
245
+ import "dotenv/config";
246
+ var PineconeSearchService = class {
247
+ apiKey;
248
+ indexName;
249
+ namespace;
250
+ minScore;
251
+ topK;
252
+ fridaEmbeddingUrl;
253
+ fridaApiKey;
254
+ pinecone = null;
255
+ index = null;
256
+ constructor(options = {}) {
257
+ this.apiKey = options.apiKey || process.env.PINECONE_API_KEY || (() => {
258
+ throw new Error(
259
+ "PINECONE_API_KEY is required. Set it in .env or pass it in options."
260
+ );
261
+ })();
262
+ this.indexName = process.env.PINECONE_INDEX;
263
+ this.namespace = options.namespace;
264
+ this.minScore = parseFloat(process.env.PINECONE_MIN_SCORE);
265
+ this.topK = parseInt(process.env.PINECONE_TOP_K, 10);
266
+ this.fridaEmbeddingUrl = options.fridaEmbeddingUrl || process.env.FRIDA_EMBEDDING_URL || (() => {
267
+ throw new Error(
268
+ "FRIDA_EMBEDDING_URL is required. Set it in .env or pass it in options."
269
+ );
270
+ })();
271
+ this.fridaApiKey = options.fridaApiKey || process.env.FRIDA_BEARER_TOKEN || (() => {
272
+ throw new Error(
273
+ "FRIDA_BEARER_TOKEN is required. Set it in .env or pass it in options."
274
+ );
275
+ })();
276
+ this.pinecone = new Pinecone({ apiKey: this.apiKey });
277
+ }
278
+ /**
279
+ * Initializes the Pinecone index (lazy loading)
280
+ * Namespace is set here when getting the index reference using method chaining
281
+ */
282
+ async initialize() {
283
+ if (!this.index) {
284
+ console.log(`[PINECONE] Connecting to index: ${this.indexName}${this.namespace ? ` namespace "${this.namespace}"` : ""}`);
285
+ const indexRef = this.pinecone.index(this.indexName);
286
+ this.index = this.namespace ? indexRef.namespace(this.namespace) : indexRef;
287
+ console.log(`[PINECONE] Connected to index: ${this.indexName}${this.namespace ? ` namespace "${this.namespace}"` : ""}`);
288
+ }
289
+ }
290
+ /**
291
+ * Generates embedding for a text using Frida API
292
+ * @param text - Text to embed
293
+ * @returns Promise<number[]> Embedding vector (1536 dimensions)
294
+ */
295
+ async generateEmbedding(text) {
296
+ try {
297
+ console.log(`[PINECONE] Calling Frida Embedding API...`);
298
+ const startTime = Date.now();
299
+ const response = await fetch(this.fridaEmbeddingUrl, {
300
+ method: "POST",
301
+ headers: {
302
+ accept: "application/json",
303
+ Authorization: `Bearer ${this.fridaApiKey}`,
304
+ "Content-Type": "application/json"
305
+ },
306
+ body: JSON.stringify({
307
+ input: text,
308
+ model: "text-embedding-ada-002",
309
+ user_id: "pinecone-search",
310
+ email: "design_system@example.com"
311
+ })
312
+ });
313
+ console.log(`[PINECONE] API response status: ${response.status}`);
314
+ if (!response.ok) {
315
+ const errorText = await response.text();
316
+ console.error(`[PINECONE] API Error response:`, errorText);
317
+ throw new Error(`API Error: ${response.status} ${response.statusText}`);
318
+ }
319
+ console.log(`[PINECONE] Parsing API response...`);
320
+ const data = await response.json();
321
+ if (!data.data || !data.data[0] || !data.data[0].embedding) {
322
+ console.error(`[PINECONE] Invalid response format:`, JSON.stringify(data).substring(0, 200));
323
+ throw new Error("Invalid API response format");
324
+ }
325
+ const embedding = data.data[0].embedding;
326
+ const elapsed = Date.now() - startTime;
327
+ console.log(`[PINECONE] \u2713 Embedding generated (${embedding.length} dims) in ${elapsed}ms`);
328
+ return embedding;
329
+ } catch (error) {
330
+ console.error(`[PINECONE] \u2717 Error generating embedding:`, error instanceof Error ? error.message : String(error));
331
+ throw new Error(`Failed to generate embedding: ${error instanceof Error ? error.message : String(error)}`);
332
+ }
333
+ }
334
+ /**
335
+ * Parses a Pinecone match to structured format
336
+ * Supports new schema with properties, events, methods, slots, etc.
337
+ */
338
+ parseMatch(match) {
339
+ const metadata = match.metadata || {};
340
+ let props = [];
341
+ try {
342
+ if (metadata.properties) {
343
+ const parsedProps = JSON.parse(
344
+ metadata.properties
345
+ );
346
+ if (Array.isArray(parsedProps)) {
347
+ props = parsedProps.map((p) => ({
348
+ name: p.name || "unknown",
349
+ type_signature: p.type_signature || "unknown",
350
+ default_value: p.default_value,
351
+ description: p.description || "",
352
+ is_utility_prop: p.is_utility_prop || false,
353
+ reflects_to_attribute: p.reflects_to_attribute || false
354
+ }));
355
+ }
356
+ }
357
+ } catch {
358
+ props = [];
359
+ }
360
+ let events = [];
361
+ try {
362
+ if (metadata.events) {
363
+ const parsedEvents = JSON.parse(
364
+ metadata.events
365
+ );
366
+ if (Array.isArray(parsedEvents)) {
367
+ events = parsedEvents.map((e) => ({
368
+ name: e.name || "unknown",
369
+ payload_type: e.payload_type || "any",
370
+ description: e.description || ""
371
+ }));
372
+ }
373
+ }
374
+ } catch {
375
+ events = [];
376
+ }
377
+ let methods = [];
378
+ try {
379
+ if (metadata.methods) {
380
+ const parsedMethods = JSON.parse(metadata.methods);
381
+ if (Array.isArray(parsedMethods)) {
382
+ methods = parsedMethods.map((m) => ({
383
+ name: m.name || "unknown",
384
+ parameters: m.parameters || "null",
385
+ description: m.description || ""
386
+ }));
387
+ }
388
+ }
389
+ } catch {
390
+ methods = [];
391
+ }
392
+ let slots = [];
393
+ try {
394
+ if (metadata.slots) {
395
+ const parsedSlots = JSON.parse(metadata.slots);
396
+ if (Array.isArray(parsedSlots)) {
397
+ slots = parsedSlots.map((s) => ({
398
+ name: s.name || "unknown",
399
+ description: s.description || ""
400
+ }));
401
+ }
402
+ }
403
+ } catch {
404
+ slots = [];
405
+ }
406
+ let usage_examples = [];
407
+ try {
408
+ if (metadata.usage_examples) {
409
+ const parsedExamples = JSON.parse(metadata.usage_examples);
410
+ if (Array.isArray(parsedExamples)) {
411
+ usage_examples = parsedExamples.map((ex) => ({
412
+ title: ex.title || "",
413
+ description: ex.description || "",
414
+ code: {
415
+ template: ex.code?.template || "",
416
+ script: ex.code?.script
417
+ }
418
+ }));
419
+ }
420
+ }
421
+ } catch {
422
+ usage_examples = [];
423
+ }
424
+ let usage_limitations = [];
425
+ try {
426
+ if (metadata.usage_limitations) {
427
+ const parsedLimitations = JSON.parse(metadata.usage_limitations);
428
+ if (Array.isArray(parsedLimitations)) {
429
+ usage_limitations = parsedLimitations;
430
+ }
431
+ }
432
+ } catch {
433
+ usage_limitations = [];
434
+ }
435
+ let hooks_or_services = [];
436
+ try {
437
+ if (metadata.hooks_or_services) {
438
+ const parsedHooks = JSON.parse(metadata.hooks_or_services);
439
+ if (Array.isArray(parsedHooks)) {
440
+ hooks_or_services = parsedHooks;
441
+ }
442
+ }
443
+ } catch {
444
+ hooks_or_services = [];
445
+ }
446
+ const tag = metadata.selector || metadata.id || "unknown";
447
+ return {
448
+ tag,
449
+ score: match.score || 0,
450
+ category: metadata.category || "General",
451
+ description: metadata.short_description || "",
452
+ keywords: metadata.keywords || "",
453
+ props,
454
+ events,
455
+ // New fields
456
+ id: metadata.id,
457
+ selector: metadata.selector,
458
+ human_name: metadata.human_name,
459
+ full_readme: metadata.full_readme,
460
+ visual_variants_description: metadata.visual_variants_description,
461
+ framework_type: metadata.framework_type,
462
+ import_statement: metadata.import_statement,
463
+ is_global: metadata.is_global,
464
+ module_import: metadata.module_import,
465
+ content_injection_method: metadata.content_injection_method,
466
+ methods,
467
+ slots,
468
+ usage_limitations,
469
+ usage_examples,
470
+ hooks_or_services
471
+ };
472
+ }
473
+ /**
474
+ * Searches for components in Pinecone
475
+ * @param query - Search query
476
+ * @param options - Search options
477
+ * @returns Structured results
478
+ */
479
+ async search(query, options = {}) {
480
+ if (!query || typeof query !== "string") {
481
+ throw new Error("Query must be a non-empty string");
482
+ }
483
+ await this.initialize();
484
+ const minScore = options.minScore !== void 0 ? options.minScore : this.minScore;
485
+ const topK = options.topK !== void 0 ? options.topK : this.topK;
486
+ console.log(`[PINECONE] Starting search - Query: "${query}", topK: ${topK}, minScore: ${minScore}${this.namespace ? `, namespace: "${this.namespace}"` : ""}`);
487
+ const embedding = await this.generateEmbedding(query);
488
+ console.log(`[PINECONE] Querying Pinecone index "${this.indexName}"${this.namespace ? ` namespace "${this.namespace}"` : ""}...`);
489
+ const startTime = Date.now();
490
+ const queryResponse = await this.index.query({
491
+ vector: embedding,
492
+ topK,
493
+ includeMetadata: true
494
+ });
495
+ const queryTime = Date.now() - startTime;
496
+ console.log(`[PINECONE] Query completed in ${queryTime}ms - Found ${queryResponse.matches?.length || 0} matches`);
497
+ const allMatches = queryResponse.matches || [];
498
+ const relevantMatches = allMatches.filter((m) => (m.score || 0) >= minScore);
499
+ console.log(`[PINECONE] Filtered results: ${relevantMatches.length} relevant (score >= ${minScore}) out of ${allMatches.length} total`);
500
+ return {
501
+ query,
502
+ totalMatches: allMatches.length,
503
+ relevantMatches: relevantMatches.length,
504
+ matches: relevantMatches.map((match) => this.parseMatch(match)),
505
+ lowScoreMatches: allMatches.filter((m) => (m.score || 0) < minScore).map((match) => {
506
+ const metadata = match.metadata || {};
507
+ const tag = metadata.selector || metadata.id || metadata.tag || "unknown";
508
+ return {
509
+ tag,
510
+ score: match.score || 0
511
+ };
512
+ }),
513
+ searchMetadata: {
514
+ minScore,
515
+ topK,
516
+ indexName: this.indexName,
517
+ namespace: this.namespace
518
+ }
519
+ };
520
+ }
521
+ /**
522
+ * Gets details of a specific component by tag, selector, or id
523
+ * @param tag - Component tag, selector, or id (e.g.: 'my-badge', 'Button', 'custom-table')
524
+ * @returns Component details or null if it doesn't exist
525
+ */
526
+ async getComponentDetails(tag) {
527
+ if (!tag || typeof tag !== "string") {
528
+ throw new Error("Tag must be a non-empty string");
529
+ }
530
+ await this.initialize();
531
+ const results = await this.search(tag, { topK: 10, minScore: 0 });
532
+ const exactMatch = results.matches.find(
533
+ (m) => m.tag === tag || m.selector === tag || m.id === tag
534
+ );
535
+ if (exactMatch) {
536
+ return exactMatch;
537
+ }
538
+ if (results.matches.length > 0) {
539
+ return results.matches[0];
540
+ }
541
+ return null;
542
+ }
543
+ /**
544
+ * Searches for multiple components by tags
545
+ * @param tags - Array of component tags
546
+ * @returns Array of component details
547
+ */
548
+ async getMultipleComponents(tags) {
549
+ if (!Array.isArray(tags)) {
550
+ throw new Error("Tags must be an array");
551
+ }
552
+ const results = await Promise.all(
553
+ tags.map((tag) => this.getComponentDetails(tag))
554
+ );
555
+ return results.filter(
556
+ (r) => r !== null
557
+ );
558
+ }
559
+ };
560
+
561
+ // src/figma2frida/services/frida/frida-client.ts
562
+ import "dotenv/config";
563
+ var FridaClient = class {
564
+ apiKey;
565
+ apiUrl;
566
+ model;
567
+ maxTokens;
568
+ enabled;
569
+ constructor(options = {}) {
570
+ this.apiKey = options.apiKey || process.env.FRIDA_BEARER_TOKEN;
571
+ this.apiUrl = options.apiUrl || process.env.FRIDA_API_URL;
572
+ this.model = options.model || process.env.FRIDA_MODEL || "Innovation-gpt4o";
573
+ this.maxTokens = options.maxTokens || (process.env.FRIDA_MAX_TOKENS ? parseInt(process.env.FRIDA_MAX_TOKENS, 10) : 4096);
574
+ this.enabled = !!(this.apiKey && this.apiUrl);
575
+ }
576
+ /**
577
+ * Checks if Frida is configured
578
+ */
579
+ isEnabled() {
580
+ return this.enabled;
581
+ }
582
+ /**
583
+ * Generates the optimized prompt for code generation
584
+ */
585
+ buildPrompt(question, context) {
586
+ return `You are an expert agent in code generation for UI design systems and component libraries.
587
+ Your goal is to provide technical, precise, and ready-to-use answers that enable generating functional code immediately.
588
+
589
+ **\u{1F6A8} CRITICAL RULES - MANDATORY COMPLIANCE:**
590
+
591
+ 1. **You MUST use ONLY the components** from the AVAILABLE COMPONENT INFORMATION below
592
+ 2. **NEVER suggest or use:**
593
+ - \u274C Components from other libraries not listed in the AVAILABLE COMPONENT INFORMATION
594
+ - \u274C Generic HTML form elements: <button>, <input>, <select>, <textarea> (unless they are wrapped in design system components)
595
+ - \u274C Any other component library or framework-specific components not in the AVAILABLE COMPONENT INFORMATION
596
+
597
+ 3. **Use components exactly as documented** - they may be Web Components, React components, Angular components, or any other framework
598
+ 4. **If a component is not in the AVAILABLE COMPONENT INFORMATION**, say so clearly - DO NOT invent or use generic alternatives
599
+
600
+ **EXAMPLES OF WHAT NOT TO DO:**
601
+ - \u274C WRONG: Using components from other libraries (mat-*, MUI, etc.) unless they appear in AVAILABLE COMPONENT INFORMATION
602
+ - \u274C WRONG: Using any component that is NOT in the AVAILABLE COMPONENT INFORMATION below
603
+ - \u274C WRONG: Inventing component names or props not in the documentation
604
+
605
+ **EXAMPLES OF WHAT TO DO:**
606
+ - \u2705 Use ONLY components from the AVAILABLE COMPONENT INFORMATION below
607
+ - \u2705 Check the component tag/selector in the AVAILABLE COMPONENT INFORMATION before using it
608
+ - \u2705 Use the exact component names, props, and events as shown in the AVAILABLE COMPONENT INFORMATION
609
+ - \u2705 Follow the framework_type and import_statement provided for each component
610
+ - \u2705 If a component is not listed in AVAILABLE COMPONENT INFORMATION, do NOT use it - say clearly that it doesn't exist
611
+
612
+ ## AVAILABLE COMPONENT INFORMATION:
613
+
614
+ The component information below is provided in JSON format for accurate parsing.
615
+ Use the exact prop names, types, and structure as shown in the JSON.
616
+
617
+ ${context}
618
+
619
+ **IMPORTANT:** The component information above is in JSON format. Parse it carefully and use:
620
+ - Exact prop names as shown in the props array
621
+ - Exact prop types from type_signature field
622
+ - Event names exactly as shown in the events array
623
+ - Framework type from framework_type field
624
+ - Import statements from import_statement field
625
+ - Follow the structure and patterns from the JSON data
626
+
627
+ ## RESPONSE INSTRUCTIONS:
628
+
629
+ ### 1. MANDATORY STRUCTURE:
630
+ Your response MUST follow this format:
631
+
632
+ \u{1F6A8} **RECOMMENDED COMPONENT:** [component name - MUST be from AVAILABLE COMPONENT INFORMATION]
633
+ **CATEGORY:** [component category]
634
+ **FRAMEWORK:** [framework_type from the component data]
635
+
636
+ **DESCRIPTION:**
637
+ [Brief explanation of the component and when to use it]
638
+
639
+ **IMPORT:**
640
+ \`\`\`
641
+ [import_statement from the component data if available]
642
+ \`\`\`
643
+
644
+ **EXAMPLE CODE:**
645
+ \`\`\`html
646
+ [Minimal functional and complete example]
647
+ \`\`\`
648
+
649
+ **IMPORTANT PROPERTIES:**
650
+ [List of key properties with types and possible values]
651
+ - \`property\`: \`type_signature\` - [description]
652
+
653
+ **EVENT HANDLING:**
654
+ [If applicable, how to listen and handle events]
655
+ \`\`\`javascript
656
+ [Example code to handle events]
657
+ \`\`\`
658
+
659
+ **ADDITIONAL NOTES:**
660
+ [Any important considerations, best practices, or limitations]
661
+
662
+ ### 2. CODE GENERATION RULES:
663
+
664
+ 1. **Use ONLY components from the AVAILABLE COMPONENT INFORMATION above** - Do NOT use components from other libraries
665
+ 2. **Do not invent properties or events** - Use only what exists in the component documentation provided
666
+ 3. **Exact types:** Include the exact types of properties from type_signature field
667
+ 4. **Functional code:** The example code MUST be copyable and functional
668
+ 5. **Required props:** If a prop is required, indicate it clearly
669
+ 6. **Default values:** If there are default values (default_value field), mention them
670
+ 7. **Events:** Include examples of how to listen to events if the component emits them
671
+ 8. **Framework compatibility:** Use the framework_type and import_statement from the component data
672
+ 9. **Necessary imports:** Include the import_statement from the component data
673
+ 10. **If a component doesn't exist:** Say clearly "No component available for [feature]. Available components are: [list]"
674
+
675
+ ### 3. QUESTION ANALYSIS:
676
+
677
+ - If asking about a specific component: Provide complete technical documentation
678
+ - If asking "how to do X": Suggest the most suitable component AND provide functional code
679
+ - If asking about properties: List ALL relevant properties with exact types
680
+ - If asking about events: Provide complete examples of event handling
681
+
682
+ ### 4. TECHNICAL ACCURACY:
683
+
684
+ - Use the exact names of properties as they appear in the documentation
685
+ - Respect exact types (union types, enums, etc.)
686
+ - Include all attributes necessary for the code to work
687
+ - Mention if there are slots available and how to use them
688
+
689
+ ### 5. IF NO INFORMATION:
690
+
691
+ If the context does not contain enough information to answer, say clearly:
692
+ "I don't have enough information about [aspect] in the provided context. Available components are: [list]"
693
+
694
+ ## USER QUESTION:
695
+ ${question}
696
+
697
+ ## RESPONSE (Follow the mandatory structure above):`.trim();
698
+ }
699
+ /**
700
+ * Builds a specialized prompt for code generation from Figma designs
701
+ */
702
+ buildCodeGenerationPrompt(originalCode, componentContext) {
703
+ const elementMatches = originalCode.match(/<\w+/g) || [];
704
+ const elementCount = elementMatches.length;
705
+ return `You are an expert in generating code using design system components.
706
+
707
+ **TASK:** Transform the Figma-generated code below to use design system components from the AVAILABLE COMPONENT INFORMATION.
708
+
709
+ **\u{1F6A8} CRITICAL RULES - MANDATORY:**
710
+
711
+ 1. **PRESERVE EVERY SINGLE ELEMENT** - The original code has approximately ${elementCount} HTML elements. You MUST transform ALL of them, not just a few examples. Do NOT summarize, simplify, or create examples.
712
+ 2. **PRESERVE ALL TEXT CONTENT EXACTLY** - Keep ALL text, labels, placeholders, button text, input values, and content exactly as shown in the original code. DO NOT change, invent, or modify any text content.
713
+ 3. **PRESERVE ALL STRUCTURE** - Maintain EVERY div, container, class, style, id, data attribute, and layout element exactly as in the original.
714
+ 4. **Use ONLY components from AVAILABLE COMPONENT INFORMATION** - Do NOT use components from other libraries or generic HTML elements unless no alternative exists
715
+ 5. **Use EXACT prop names and types** from the component documentation (check type_signature field)
716
+ 6. **Map HTML attributes to correct component props** based on component documentation
717
+ 7. **Handle events properly** if the component emits them (check events array)
718
+ 8. **Use slots or content_injection_method** if the component supports them
719
+ 9. **Generate functional, copyable code** that works immediately
720
+ 10. **Follow framework_type and import_statement** from the component data
721
+
722
+ **CRITICAL REQUIREMENTS:**
723
+ - Transform EVERY element in the original code (approximately ${elementCount} elements)
724
+ - Keep ALL text content exactly as it appears in the original (no changes to labels, placeholders, values, etc.)
725
+ - Maintain ALL CSS classes, inline styles, ids, data attributes, and structure
726
+ - Do NOT summarize, simplify, or create examples - transform the ENTIRE code
727
+ - If you cannot transform an element (component doesn't exist), preserve it as-is with all its attributes and content
728
+
729
+ **VALIDATION:**
730
+ After transformation, you should have approximately the same number of elements as the original (${elementCount}). Every element must be accounted for.
731
+
732
+ ## AVAILABLE COMPONENT INFORMATION:
733
+
734
+ ${componentContext}
735
+
736
+ ## ORIGINAL CODE FROM FIGMA:
737
+
738
+ \`\`\`html
739
+ ${originalCode}
740
+ \`\`\`
741
+
742
+ ## YOUR TASK:
743
+
744
+ Transform the ENTIRE code above to use design system components. Return ONLY the complete transformed HTML code wrapped in \`\`\`html code blocks. Include ALL ${elementCount} elements and ALL text from the original. No explanations or markdown outside the code block.
745
+
746
+ **Transformed Code (must include all ${elementCount} elements with all original text):**
747
+ \`\`\`html
748
+ `;
749
+ }
750
+ /**
751
+ * Generates code using component documentation from Pinecone
752
+ * @param originalCode - Original code from Figma
753
+ * @param componentContext - Component documentation from Pinecone (JSON format)
754
+ * @returns Frida response with generated code
755
+ */
756
+ async generateCode(originalCode, componentContext) {
757
+ if (!this.enabled) {
758
+ return {
759
+ success: false,
760
+ error: "Frida AI is not configured. Set FRIDA_BEARER_TOKEN and FRIDA_API_URL in .env",
761
+ response: null
762
+ };
763
+ }
764
+ if (!originalCode || typeof originalCode !== "string") {
765
+ return {
766
+ success: false,
767
+ error: "Original code must be a non-empty string",
768
+ response: null
769
+ };
770
+ }
771
+ console.log(`
772
+ [FRIDA CODE GEN] ========================================`);
773
+ console.log(`[FRIDA CODE GEN] \u{1F680} CALLING FRIDA AI FOR CODE GENERATION`);
774
+ console.log(`[FRIDA CODE GEN] ========================================`);
775
+ const prompt = this.buildCodeGenerationPrompt(originalCode, componentContext);
776
+ const codeGenMaxTokens = process.env.FRIDA_CODE_GEN_MAX_TOKENS ? parseInt(process.env.FRIDA_CODE_GEN_MAX_TOKENS, 10) : Math.max(this.maxTokens, 16384);
777
+ console.log(`[FRIDA CODE GEN] Model: ${this.model}`);
778
+ console.log(`[FRIDA CODE GEN] Original code length: ${originalCode.length} chars`);
779
+ console.log(`[FRIDA CODE GEN] Component context length: ${componentContext.length} chars`);
780
+ console.log(`[FRIDA CODE GEN] Max tokens: ${codeGenMaxTokens} (default: ${this.maxTokens})`);
781
+ try {
782
+ const startTime = Date.now();
783
+ const response = await fetch(this.apiUrl, {
784
+ method: "POST",
785
+ headers: {
786
+ "Content-Type": "application/json",
787
+ Authorization: `Bearer ${this.apiKey}`
788
+ },
789
+ body: JSON.stringify({
790
+ model: this.model,
791
+ input: prompt,
792
+ max_tokens: codeGenMaxTokens,
793
+ stream: false,
794
+ email: "design_system@example.com"
795
+ })
796
+ });
797
+ const fetchTime = Date.now() - startTime;
798
+ console.log(`[FRIDA CODE GEN] API request completed in ${fetchTime}ms - Status: ${response.status}`);
799
+ if (!response.ok) {
800
+ console.error(`[FRIDA CODE GEN] API Error: ${response.status} ${response.statusText}`);
801
+ throw new Error(`API Error: ${response.status} ${response.statusText}`);
802
+ }
803
+ const data = await response.json();
804
+ console.log(`[FRIDA CODE GEN] Response received successfully`);
805
+ const answer = data.response || data.output || data.choices?.[0]?.message?.content || data.content || JSON.stringify(data);
806
+ const tokensUsed = data.usage?.total_tokens || null;
807
+ const answerLength = typeof answer === "string" ? answer.length : String(answer).length;
808
+ console.log(`[FRIDA CODE GEN] ========================================`);
809
+ console.log(`[FRIDA CODE GEN] \u2705 CODE GENERATION COMPLETE`);
810
+ console.log(`[FRIDA CODE GEN] ========================================`);
811
+ console.log(`[FRIDA CODE GEN] \u{1F4CA} TOKENS USED: ${tokensUsed || "N/A"}`);
812
+ console.log(`[FRIDA CODE GEN] \u{1F4DD} Answer length: ${answerLength} chars`);
813
+ console.log(`[FRIDA CODE GEN] \u23F1\uFE0F Total time: ${fetchTime}ms`);
814
+ console.log(`[FRIDA CODE GEN] ========================================
815
+ `);
816
+ return {
817
+ success: true,
818
+ error: null,
819
+ response: typeof answer === "string" ? answer : String(answer),
820
+ metadata: {
821
+ model: this.model,
822
+ tokensUsed
823
+ }
824
+ };
825
+ } catch (error) {
826
+ console.error(`[FRIDA CODE GEN] Error calling Frida AI:`, error instanceof Error ? error.message : String(error));
827
+ return {
828
+ success: false,
829
+ error: error instanceof Error ? error.message : String(error),
830
+ response: null
831
+ };
832
+ }
833
+ }
834
+ /**
835
+ * Asks a question to Frida AI
836
+ * @param question - User question
837
+ * @param context - Context with component information
838
+ * @returns Frida response
839
+ */
840
+ async ask(question, context) {
841
+ if (!this.enabled) {
842
+ return {
843
+ success: false,
844
+ error: "Frida AI is not configured. Set FRIDA_BEARER_TOKEN and FRIDA_API_URL in .env",
845
+ response: null
846
+ };
847
+ }
848
+ if (!question || typeof question !== "string") {
849
+ return {
850
+ success: false,
851
+ error: "Question must be a non-empty string",
852
+ response: null
853
+ };
854
+ }
855
+ console.log(`
856
+ [FRIDA] ========================================`);
857
+ console.log(`[FRIDA] \u{1F680} CALLING FRIDA AI`);
858
+ console.log(`[FRIDA] ========================================`);
859
+ const prompt = this.buildPrompt(question, context);
860
+ console.log(`[FRIDA] API URL: ${this.apiUrl}`);
861
+ console.log(`[FRIDA] Model: ${this.model}`);
862
+ console.log(`[FRIDA] Question length: ${question.length} chars`);
863
+ console.log(`[FRIDA] Context length: ${context.length} chars`);
864
+ console.log(`[FRIDA] Max tokens: ${this.maxTokens}`);
865
+ try {
866
+ const startTime = Date.now();
867
+ const response = await fetch(this.apiUrl, {
868
+ method: "POST",
869
+ headers: {
870
+ "Content-Type": "application/json",
871
+ Authorization: `Bearer ${this.apiKey}`
872
+ },
873
+ body: JSON.stringify({
874
+ model: this.model,
875
+ input: prompt,
876
+ max_tokens: this.maxTokens,
877
+ stream: false,
878
+ email: "design_system@example.com"
879
+ })
880
+ });
881
+ const fetchTime = Date.now() - startTime;
882
+ console.log(`[FRIDA] API request completed in ${fetchTime}ms - Status: ${response.status}`);
883
+ if (!response.ok) {
884
+ console.error(`[FRIDA] API Error: ${response.status} ${response.statusText}`);
885
+ throw new Error(`API Error: ${response.status} ${response.statusText}`);
886
+ }
887
+ const data = await response.json();
888
+ console.log(`[FRIDA] Response received successfully`);
889
+ const answer = data.response || data.output || data.choices?.[0]?.message?.content || data.content || JSON.stringify(data);
890
+ const tokensUsed = data.usage?.total_tokens || null;
891
+ const answerLength = typeof answer === "string" ? answer.length : String(answer).length;
892
+ console.log(`[FRIDA] ========================================`);
893
+ console.log(`[FRIDA] \u2705 FRIDA AI RESPONSE RECEIVED`);
894
+ console.log(`[FRIDA] ========================================`);
895
+ console.log(`[FRIDA] \u{1F4CA} TOKENS USED: ${tokensUsed || "N/A"}`);
896
+ console.log(`[FRIDA] \u{1F4DD} Answer length: ${answerLength} chars`);
897
+ console.log(`[FRIDA] \u23F1\uFE0F Total time: ${fetchTime}ms`);
898
+ console.log(`[FRIDA] ========================================
899
+ `);
900
+ return {
901
+ success: true,
902
+ error: null,
903
+ response: typeof answer === "string" ? answer : String(answer),
904
+ metadata: {
905
+ model: this.model,
906
+ tokensUsed
907
+ }
908
+ };
909
+ } catch (error) {
910
+ console.error(`[FRIDA] Error calling Frida AI:`, error instanceof Error ? error.message : String(error));
911
+ return {
912
+ success: false,
913
+ error: error instanceof Error ? error.message : String(error),
914
+ response: null
915
+ };
916
+ }
917
+ }
918
+ };
919
+
920
+ // src/figma2frida/utils/formatters.ts
921
+ var Formatters = class {
922
+ /**
923
+ * Formats results in structured format (JSON-friendly)
924
+ */
925
+ static toStructured(searchResults) {
926
+ return {
927
+ query: searchResults.query,
928
+ summary: {
929
+ totalMatches: searchResults.totalMatches,
930
+ relevantMatches: searchResults.relevantMatches,
931
+ minScore: searchResults.searchMetadata.minScore
932
+ },
933
+ components: searchResults.matches.map((match) => ({
934
+ tag: match.tag,
935
+ score: match.score,
936
+ category: match.category,
937
+ description: match.description,
938
+ keywords: match.keywords || "",
939
+ props: match.props,
940
+ events: match.events
941
+ })),
942
+ lowScoreComponents: searchResults.lowScoreMatches || []
943
+ };
944
+ }
945
+ /**
946
+ * Formats results in readable markdown format
947
+ */
948
+ static toMarkdown(searchResults) {
949
+ if (!searchResults.matches || searchResults.matches.length === 0) {
950
+ return `## No similar components found
951
+
952
+ **Query:** "${searchResults.query}"
953
+
954
+ Suggestion: Try more general terms or verify that components are indexed.`;
955
+ }
956
+ let markdown = `## Search Results
957
+
958
+ **Query:** "${searchResults.query}"
959
+ **Components found:** ${searchResults.relevantMatches} of ${searchResults.totalMatches}
960
+ **Minimum score:** ${searchResults.searchMetadata.minScore}
961
+
962
+ ---
963
+
964
+ `;
965
+ searchResults.matches.forEach((match, index) => {
966
+ markdown += `### ${index + 1}. ${match.tag} (Score: ${match.score.toFixed(4)})
967
+
968
+ **Category:** ${match.category}
969
+
970
+ **Description:**
971
+ ${match.description || "No description available"}
972
+
973
+ **Keywords:** ${match.keywords || "N/A"}
974
+
975
+ `;
976
+ if (match.props && match.props.length > 0) {
977
+ markdown += `**Properties (${match.props.length}):**
978
+
979
+ `;
980
+ match.props.forEach((prop, idx) => {
981
+ markdown += `${idx + 1}. \`${prop.name}\`
982
+ `;
983
+ markdown += ` - **Type:** \`${prop.type_signature}\`
984
+ `;
985
+ if (prop.default_value !== void 0) {
986
+ markdown += ` - **Default:** \`${prop.default_value}\`
987
+ `;
988
+ }
989
+ if (prop.description) {
990
+ markdown += ` - **Documentation:** ${prop.description}
991
+ `;
992
+ }
993
+ markdown += "\n";
994
+ });
995
+ } else {
996
+ markdown += `**Properties:** None
997
+
998
+ `;
999
+ }
1000
+ if (match.events && match.events.length > 0) {
1001
+ markdown += `**Events (${match.events.length}):**
1002
+
1003
+ `;
1004
+ match.events.forEach((event, idx) => {
1005
+ markdown += `${idx + 1}. \`${event.name}\`
1006
+ `;
1007
+ markdown += ` - **Payload type:** ${event.payload_type}
1008
+ `;
1009
+ if (event.description) {
1010
+ markdown += ` - **Documentation:** ${event.description}
1011
+ `;
1012
+ }
1013
+ markdown += "\n";
1014
+ });
1015
+ } else {
1016
+ markdown += `**Events:** None
1017
+
1018
+ `;
1019
+ }
1020
+ markdown += "---\n\n";
1021
+ });
1022
+ if (searchResults.lowScoreMatches && searchResults.lowScoreMatches.length > 0) {
1023
+ markdown += `
1024
+ ### Components with low relevance (score < ${searchResults.searchMetadata.minScore})
1025
+
1026
+ `;
1027
+ searchResults.lowScoreMatches.forEach((match, idx) => {
1028
+ markdown += `${idx + 1}. ${match.tag} (Score: ${match.score.toFixed(4)})
1029
+ `;
1030
+ });
1031
+ }
1032
+ return markdown;
1033
+ }
1034
+ /**
1035
+ * Formats an individual component in context for prompts
1036
+ * Returns JSON-structured format for better LLM understanding
1037
+ */
1038
+ static toContextFormat(match) {
1039
+ const componentJson = {
1040
+ tag: match.tag,
1041
+ readme: match.description,
1042
+ props: match.props.map((p) => ({
1043
+ name: p.name,
1044
+ type: p.type_signature,
1045
+ default_value: p.default_value,
1046
+ docs: p.description || ""
1047
+ })),
1048
+ events: match.events.map((e) => ({
1049
+ name: e.name,
1050
+ payload_type: e.payload_type,
1051
+ docs: e.description || ""
1052
+ })),
1053
+ // Include new fields if available
1054
+ ...match.usage_examples && match.usage_examples.length > 0 ? { usage_examples: match.usage_examples } : {},
1055
+ ...match.methods && match.methods.length > 0 ? { methods: match.methods } : {},
1056
+ ...match.slots && match.slots.length > 0 ? { slots: match.slots } : {}
1057
+ };
1058
+ const jsonStr = JSON.stringify(componentJson, null, 2);
1059
+ return `=== COMPONENT: ${match.tag || "N/A"} ===
1060
+ ${match.human_name ? `HUMAN NAME: ${match.human_name}
1061
+ ` : ""}CATEGORY: ${match.category || "General"}
1062
+ RELEVANCE SCORE: ${match.score.toFixed(4)}
1063
+
1064
+ DESCRIPTION:
1065
+ ${match.description || "No description available"}
1066
+
1067
+ ${match.full_readme ? `FULL README:
1068
+ ${match.full_readme}
1069
+
1070
+ ` : ""}${match.keywords ? `KEYWORDS: ${match.keywords}
1071
+
1072
+ ` : ""}PROPERTIES (${match.props?.length || 0}):
1073
+ ${match.props && match.props.length > 0 ? match.props.map((p, idx) => {
1074
+ return `${idx + 1}. \`${p.name}\`
1075
+ Type: \`${p.type_signature}\`
1076
+ ${p.default_value !== void 0 ? `Default: \`${p.default_value}\`
1077
+ ` : ""}Documentation: ${p.description || "No documentation"}`;
1078
+ }).join("\n\n") : "None"}
1079
+
1080
+ EVENTS (${match.events?.length || 0}):
1081
+ ${match.events && match.events.length > 0 ? match.events.map((e, idx) => {
1082
+ return `${idx + 1}. ${e.name}
1083
+ Payload type: ${e.payload_type}
1084
+ Documentation: ${e.description || "No documentation"}`;
1085
+ }).join("\n\n") : "None"}
1086
+
1087
+ ${match.methods && match.methods.length > 0 ? `METHODS (${match.methods.length}):
1088
+ ${match.methods.map((m, idx) => {
1089
+ return `${idx + 1}. ${m.name}(${m.parameters})
1090
+ Documentation: ${m.description || "No documentation"}`;
1091
+ }).join("\n\n")}
1092
+
1093
+ ` : ""}${match.usage_examples && match.usage_examples.length > 0 ? `USAGE EXAMPLES:
1094
+ ${match.usage_examples.map((ex, idx) => {
1095
+ return `${idx + 1}. ${ex.title}
1096
+ ${ex.description}
1097
+ Template: ${ex.code.template}
1098
+ ${ex.code.script ? `Script: ${ex.code.script}` : ""}`;
1099
+ }).join("\n\n")}
1100
+
1101
+ ` : ""}COMPONENT JSON STRUCTURE (for reference):
1102
+ \`\`\`json
1103
+ ${jsonStr}
1104
+ \`\`\`
1105
+
1106
+ ---`;
1107
+ }
1108
+ /**
1109
+ * Formats components as JSON array for component documentation
1110
+ * This format worked well with Copilot
1111
+ */
1112
+ static toJSONContext(matches) {
1113
+ if (!matches || matches.length === 0) {
1114
+ return "No components found.";
1115
+ }
1116
+ const componentsArray = matches.map((match) => ({
1117
+ tag: match.tag,
1118
+ readme: match.description,
1119
+ props: match.props.map((p) => ({
1120
+ name: p.name,
1121
+ type: p.type_signature,
1122
+ default_value: p.default_value,
1123
+ docs: p.description || ""
1124
+ })),
1125
+ events: match.events.map((e) => ({
1126
+ name: e.name,
1127
+ payload_type: e.payload_type,
1128
+ docs: e.description || ""
1129
+ }))
1130
+ }));
1131
+ return JSON.stringify({ components: componentsArray }, null, 2);
1132
+ }
1133
+ /**
1134
+ * Formats multiple components for context in prompts
1135
+ * Uses JSON format for better LLM understanding
1136
+ */
1137
+ static matchesToContext(matches) {
1138
+ if (!matches || matches.length === 0) {
1139
+ return "No components found.";
1140
+ }
1141
+ const componentsArray = matches.map((match) => ({
1142
+ tag: match.tag,
1143
+ readme: match.description,
1144
+ category: match.category,
1145
+ keywords: match.keywords,
1146
+ props: match.props.map((p) => ({
1147
+ name: p.name,
1148
+ type: p.type_signature,
1149
+ default_value: p.default_value,
1150
+ docs: p.description || ""
1151
+ })),
1152
+ events: match.events.map((e) => ({
1153
+ name: e.name,
1154
+ payload_type: e.payload_type,
1155
+ docs: e.description || ""
1156
+ })),
1157
+ ...match.usage_examples && match.usage_examples.length > 0 ? { usage_examples: match.usage_examples } : {},
1158
+ ...match.methods && match.methods.length > 0 ? { methods: match.methods } : {}
1159
+ }));
1160
+ return JSON.stringify({ components: componentsArray }, null, 2);
1161
+ }
1162
+ /**
1163
+ * Formats results in pure JSON format
1164
+ */
1165
+ static toJSON(searchResults) {
1166
+ return JSON.stringify(this.toStructured(searchResults), null, 2);
1167
+ }
1168
+ /**
1169
+ * Formats results according to the requested format
1170
+ */
1171
+ static format(searchResults, format = "structured") {
1172
+ switch (format.toLowerCase()) {
1173
+ case "json":
1174
+ return this.toJSON(searchResults);
1175
+ case "markdown":
1176
+ case "md":
1177
+ return this.toMarkdown(searchResults);
1178
+ case "structured":
1179
+ return this.toStructured(searchResults);
1180
+ case "context":
1181
+ return this.matchesToContext(searchResults.matches || []);
1182
+ default:
1183
+ return this.toStructured(searchResults);
1184
+ }
1185
+ }
1186
+ };
1187
+
1188
+ // src/figma2frida/utils/design-query-extractor.ts
1189
+ var DesignQueryExtractor = class {
1190
+ strategies;
1191
+ constructor() {
1192
+ this.strategies = [
1193
+ new TextExtractionStrategy(),
1194
+ new PatternMatchingStrategy(),
1195
+ new MetadataExtractionStrategy()
1196
+ ];
1197
+ }
1198
+ /**
1199
+ * Extracts query from design context using all available strategies
1200
+ */
1201
+ extractQuery(designContext, fallbackQuery = "UI component") {
1202
+ const queries = [];
1203
+ for (const strategy of this.strategies) {
1204
+ try {
1205
+ const extracted = strategy.extract(designContext);
1206
+ queries.push(...extracted);
1207
+ } catch (error) {
1208
+ console.warn(
1209
+ `[QUERY EXTRACTOR] Strategy ${strategy.name} failed:`,
1210
+ error instanceof Error ? error.message : String(error)
1211
+ );
1212
+ }
1213
+ }
1214
+ const uniqueQueries = Array.from(
1215
+ new Set(queries.filter((q) => q && q.trim().length > 0))
1216
+ );
1217
+ if (uniqueQueries.length > 0) {
1218
+ const topQueries = uniqueQueries.slice(0, 3);
1219
+ return `${topQueries.join(" ")} component`.trim();
1220
+ }
1221
+ return fallbackQuery;
1222
+ }
1223
+ /**
1224
+ * Extracts multiple candidate queries for experimentation
1225
+ */
1226
+ extractMultipleQueries(designContext) {
1227
+ const allQueries = [];
1228
+ for (const strategy of this.strategies) {
1229
+ try {
1230
+ const extracted = strategy.extract(designContext);
1231
+ allQueries.push(...extracted);
1232
+ } catch {
1233
+ }
1234
+ }
1235
+ return Array.from(
1236
+ new Set(allQueries.filter((q) => q && q.trim().length > 0))
1237
+ );
1238
+ }
1239
+ };
1240
+ var TextExtractionStrategy = class {
1241
+ name = "text-extraction";
1242
+ extract(context) {
1243
+ const queries = [];
1244
+ if (!context.content) {
1245
+ return queries;
1246
+ }
1247
+ for (const item of context.content) {
1248
+ if (item.type === "text" && typeof item.text === "string") {
1249
+ const text = item.text.trim();
1250
+ if (text.length > 0) {
1251
+ const componentKeywords = this.extractComponentKeywords(text);
1252
+ queries.push(...componentKeywords);
1253
+ const descriptivePhrases = this.extractDescriptivePhrases(text);
1254
+ queries.push(...descriptivePhrases);
1255
+ }
1256
+ }
1257
+ }
1258
+ return queries;
1259
+ }
1260
+ extractComponentKeywords(text) {
1261
+ const keywords = [];
1262
+ const patterns = [
1263
+ { pattern: /button|btn/gi, keyword: "button" },
1264
+ { pattern: /input|textfield|text field/gi, keyword: "input" },
1265
+ { pattern: /card|panel/gi, keyword: "card" },
1266
+ { pattern: /badge|label|tag/gi, keyword: "badge" },
1267
+ { pattern: /alert|message|notification/gi, keyword: "alert" },
1268
+ { pattern: /modal|dialog|popup/gi, keyword: "modal" },
1269
+ { pattern: /dropdown|select|picker/gi, keyword: "dropdown" },
1270
+ { pattern: /checkbox|check box/gi, keyword: "checkbox" },
1271
+ { pattern: /radio|radio button/gi, keyword: "radio" },
1272
+ { pattern: /slider|range/gi, keyword: "slider" },
1273
+ { pattern: /table|grid|list/gi, keyword: "table" },
1274
+ { pattern: /breadcrumb|breadcrumb navigation/gi, keyword: "breadcrumb" },
1275
+ { pattern: /tooltip|tip/gi, keyword: "tooltip" },
1276
+ { pattern: /accordion|collapsible/gi, keyword: "accordion" },
1277
+ { pattern: /progress|loader|spinner/gi, keyword: "progress" }
1278
+ ];
1279
+ for (const { pattern, keyword } of patterns) {
1280
+ if (pattern.test(text)) {
1281
+ keywords.push(keyword);
1282
+ }
1283
+ }
1284
+ return keywords;
1285
+ }
1286
+ extractDescriptivePhrases(text) {
1287
+ const phrases = [];
1288
+ const descriptionPatterns = [
1289
+ /(?:a |an |the )?([a-z]+(?: [a-z]+){0,3}) (?:that |for |to |with |in |on )/gi,
1290
+ /(?:create |make |add |use |show |display )([a-z]+(?: [a-z]+){0,3})/gi
1291
+ ];
1292
+ for (const pattern of descriptionPatterns) {
1293
+ const matches = text.matchAll(pattern);
1294
+ for (const match of matches) {
1295
+ if (match[1] && match[1].length > 2) {
1296
+ phrases.push(match[1].trim());
1297
+ }
1298
+ }
1299
+ }
1300
+ return phrases.slice(0, 5);
1301
+ }
1302
+ };
1303
+ var PatternMatchingStrategy = class {
1304
+ name = "pattern-matching";
1305
+ extract(context) {
1306
+ const queries = [];
1307
+ if (!context.content) {
1308
+ return queries;
1309
+ }
1310
+ const combinedText = context.content.filter((item) => item.type === "text" && typeof item.text === "string").map((item) => item.text).join(" ");
1311
+ if (combinedText.length === 0) {
1312
+ return queries;
1313
+ }
1314
+ const elementTypes = [];
1315
+ if (/<(?:button|Button)[^>]*>/gi.test(combinedText)) {
1316
+ elementTypes.push("button");
1317
+ }
1318
+ if (/<(?:input|Input)[^>]*>/gi.test(combinedText)) {
1319
+ elementTypes.push("input");
1320
+ if (/type=["'](?:text|email|password|number)/gi.test(combinedText)) {
1321
+ elementTypes.push("text input");
1322
+ }
1323
+ if (/type=["']checkbox/gi.test(combinedText)) {
1324
+ elementTypes.push("checkbox");
1325
+ }
1326
+ if (/type=["']radio/gi.test(combinedText)) {
1327
+ elementTypes.push("radio");
1328
+ }
1329
+ if (/type=["']range/gi.test(combinedText)) {
1330
+ elementTypes.push("slider");
1331
+ }
1332
+ }
1333
+ if (/<(?:textarea|Textarea)[^>]*>/gi.test(combinedText)) {
1334
+ elementTypes.push("textarea");
1335
+ }
1336
+ if (/<(?:select|Select)[^>]*>/gi.test(combinedText)) {
1337
+ elementTypes.push("dropdown");
1338
+ elementTypes.push("select");
1339
+ }
1340
+ if (/<(?:div|Div)[^>]*class[^>]*(?:card|Card|panel|Panel)/gi.test(combinedText)) {
1341
+ elementTypes.push("card");
1342
+ }
1343
+ if (/<(?:span|Span)[^>]*class[^>]*(?:badge|Badge|label|Label|tag|Tag)/gi.test(combinedText)) {
1344
+ elementTypes.push("badge");
1345
+ }
1346
+ if (/<(?:div|Div)[^>]*class[^>]*(?:alert|Alert|message|Message|notification|Notification)/gi.test(combinedText)) {
1347
+ elementTypes.push("alert");
1348
+ }
1349
+ if (/<(?:form|Form)[^>]*>/gi.test(combinedText)) {
1350
+ elementTypes.push("form");
1351
+ }
1352
+ if (/<(?:nav|Nav|ul|Ul)[^>]*class[^>]*(?:nav|Nav|menu|Menu|breadcrumb|Breadcrumb)/gi.test(combinedText)) {
1353
+ elementTypes.push("navigation");
1354
+ }
1355
+ if (/<(?:a|A)[^>]*href/gi.test(combinedText)) {
1356
+ elementTypes.push("link");
1357
+ elementTypes.push("action link");
1358
+ }
1359
+ for (const elementType of elementTypes) {
1360
+ queries.push(`${elementType} component`);
1361
+ }
1362
+ if (elementTypes.includes("button")) {
1363
+ queries.push("button Buttons category");
1364
+ }
1365
+ if (elementTypes.some((e) => e.includes("input") || e.includes("select") || e.includes("textarea"))) {
1366
+ queries.push("form Forms category");
1367
+ }
1368
+ if (elementTypes.includes("alert") || elementTypes.includes("badge")) {
1369
+ queries.push("feedback Feedback category");
1370
+ }
1371
+ return queries;
1372
+ }
1373
+ };
1374
+ var MetadataExtractionStrategy = class {
1375
+ name = "metadata-extraction";
1376
+ extract(context) {
1377
+ const queries = [];
1378
+ if (!context.content) {
1379
+ return queries;
1380
+ }
1381
+ const componentPattern = /<([a-z]+-[\w-]+)/gi;
1382
+ for (const item of context.content) {
1383
+ if (item.type === "text" && typeof item.text === "string") {
1384
+ const matches = item.text.matchAll(componentPattern);
1385
+ for (const match of matches) {
1386
+ const componentName = match[2];
1387
+ queries.push(componentName.replace(/-/g, " "));
1388
+ }
1389
+ }
1390
+ }
1391
+ return queries;
1392
+ }
1393
+ };
1394
+
1395
+ // src/figma2frida/handlers/component-suggestion-handler.ts
1396
+ var componentSuggestionsCache = /* @__PURE__ */ new Map();
1397
+ var COMPONENT_CACHE_TTL_MS = 3e5;
1398
+ var pineconeService = null;
1399
+ var pineconeServiceNamespace = void 0;
1400
+ var fridaClient = null;
1401
+ var queryExtractor = null;
1402
+ function getPineconeService(namespace) {
1403
+ if (!pineconeService || pineconeServiceNamespace !== namespace) {
1404
+ pineconeService = new PineconeSearchService({ namespace });
1405
+ pineconeServiceNamespace = namespace;
1406
+ }
1407
+ return pineconeService;
1408
+ }
1409
+ function getFridaClient() {
1410
+ if (!fridaClient) {
1411
+ fridaClient = new FridaClient();
1412
+ }
1413
+ return fridaClient;
1414
+ }
1415
+ function getQueryExtractor() {
1416
+ if (!queryExtractor) {
1417
+ queryExtractor = new DesignQueryExtractor();
1418
+ }
1419
+ return queryExtractor;
1420
+ }
1421
+ async function handleComponentSuggestion(options) {
1422
+ const {
1423
+ nodeId,
1424
+ minScore = 0.05,
1425
+ topK = 10,
1426
+ useFrida = true,
1427
+ query: customQuery,
1428
+ namespace,
1429
+ designContext,
1430
+ streamContent
1431
+ } = options;
1432
+ try {
1433
+ const cacheKey = `${nodeId || "current"}:${minScore}:${topK}:${useFrida}:${customQuery || "auto"}`;
1434
+ const now = Date.now();
1435
+ const cached = componentSuggestionsCache.get(cacheKey);
1436
+ if (cached && now - cached.timestamp < COMPONENT_CACHE_TTL_MS) {
1437
+ console.log(
1438
+ `[COMPONENT SUGGEST] Using cached result (age: ${((now - cached.timestamp) / 1e3).toFixed(1)}s)`
1439
+ );
1440
+ if (streamContent) {
1441
+ await streamContent({
1442
+ type: "text",
1443
+ text: `\u26A1 Using cached component suggestions (${((now - cached.timestamp) / 1e3).toFixed(1)}s old)`
1444
+ });
1445
+ }
1446
+ return { content: cached.data };
1447
+ }
1448
+ if (!designContext.content || designContext.content.length === 0) {
1449
+ return {
1450
+ content: [
1451
+ {
1452
+ type: "text",
1453
+ text: `\u274C No design context returned from Figma MCP.
1454
+
1455
+ **Node ID:** ${nodeId || "current selection"}
1456
+
1457
+ Cannot suggest components without design context.`
1458
+ }
1459
+ ]
1460
+ };
1461
+ }
1462
+ if (streamContent) {
1463
+ await streamContent({
1464
+ type: "text",
1465
+ text: "\u{1F50D} Extracting semantic query from design..."
1466
+ });
1467
+ }
1468
+ const extractor = getQueryExtractor();
1469
+ const searchQuery = customQuery || extractor.extractQuery(designContext);
1470
+ console.log(`[COMPONENT SUGGEST] Extracted query: "${searchQuery}"`);
1471
+ if (streamContent) {
1472
+ await streamContent({
1473
+ type: "text",
1474
+ text: `\u{1F50E} Searching Pinecone for: "${searchQuery}"...`
1475
+ });
1476
+ }
1477
+ console.log(`[COMPONENT SUGGEST] Calling Pinecone search service...`);
1478
+ const searchService = getPineconeService(namespace);
1479
+ const searchStartTime = Date.now();
1480
+ const searchResults = await searchService.search(searchQuery, {
1481
+ minScore,
1482
+ topK
1483
+ });
1484
+ const searchTime = Date.now() - searchStartTime;
1485
+ console.log(
1486
+ `[COMPONENT SUGGEST] Pinecone search completed in ${searchTime}ms - Found ${searchResults.relevantMatches} relevant components`
1487
+ );
1488
+ if (searchResults.matches.length === 0) {
1489
+ const namespaceInfo = searchResults.searchMetadata.namespace ? `
1490
+ **Namespace:** ${searchResults.searchMetadata.namespace}` : "";
1491
+ return {
1492
+ content: [
1493
+ {
1494
+ type: "text",
1495
+ text: `\u26A0\uFE0F **No Components Found**
1496
+
1497
+ **Query:** "${searchQuery}"
1498
+ **Min Score:** ${minScore}${namespaceInfo}
1499
+ **Index:** ${searchResults.searchMetadata.indexName}
1500
+
1501
+ No components from your design system matched the design context with sufficient relevance.
1502
+
1503
+ **Possible Causes:**
1504
+ - The namespace "${searchResults.searchMetadata.namespace || "default"}" may not exist or may be empty
1505
+ - Components may not be indexed in this namespace
1506
+ - The query may not match any components in this namespace
1507
+
1508
+ **Suggestions:**
1509
+ - Verify that components are indexed in Pinecone${searchResults.searchMetadata.namespace ? ` namespace "${searchResults.searchMetadata.namespace}"` : ""}
1510
+ - Set namespace via \`--pinecone-namespace\` CLI argument if not set
1511
+ - Try lowering the \`minScore\` parameter
1512
+ - Provide a more specific \`query\` parameter`
1513
+ }
1514
+ ]
1515
+ };
1516
+ }
1517
+ const componentTags = searchResults.matches.map((m) => m.tag).join(", ");
1518
+ const frameworkType = searchResults.matches[0]?.framework_type || "Web Components";
1519
+ const prefixPatterns = Array.from(
1520
+ new Set(
1521
+ searchResults.matches.map((m) => {
1522
+ const match = m.tag.match(/^([a-z]+-)/);
1523
+ return match ? match[1] : null;
1524
+ }).filter(Boolean)
1525
+ )
1526
+ ).join(", ");
1527
+ const content = [
1528
+ {
1529
+ type: "text",
1530
+ text: `\u{1F6A8} **MANDATORY: USE ONLY COMPONENTS FROM YOUR DESIGN SYSTEM**
1531
+
1532
+ **CRITICAL INSTRUCTIONS:**
1533
+ - You MUST use ONLY the components listed below from your organization's design system
1534
+ - **DO NOT** use components from other libraries unless they appear in the search results
1535
+ - **DO NOT** use generic HTML elements directly - use the design system components instead
1536
+ ${prefixPatterns ? `- Component prefixes in your system: ${prefixPatterns}` : ""}
1537
+ ${frameworkType ? `- Framework type: ${frameworkType}` : ""}
1538
+
1539
+ **Available Components for this design:**
1540
+ ${componentTags || "No components found"}
1541
+
1542
+ ---
1543
+
1544
+ \u2705 **Component Suggestions**
1545
+
1546
+ **Query:** "${searchQuery}"
1547
+ **Components found:** ${searchResults.relevantMatches} of ${searchResults.totalMatches}
1548
+ **Min Score:** ${minScore}
1549
+
1550
+ ---
1551
+
1552
+ `
1553
+ }
1554
+ ];
1555
+ content.push({
1556
+ type: "text",
1557
+ text: `## \u{1F4CB} MANDATORY USAGE INSTRUCTIONS
1558
+
1559
+ **You MUST follow these rules when generating code:**
1560
+
1561
+ 1. **Use ONLY the components from the search results below**
1562
+ 2. **DO NOT use:**
1563
+ - \u274C Components from other libraries not listed in the results
1564
+ - \u274C Generic HTML elements unless the component wraps them
1565
+ - \u274C Any component not found in your organization's design system
1566
+
1567
+ 3. **Use the recommended components:**
1568
+ - \u2705 Use ONLY the components listed in the search results below
1569
+ - \u2705 Follow the import statements and usage examples provided
1570
+ - \u2705 Respect the component's props, events, and methods as documented
1571
+ - \u2705 If a component is not in the list below, do NOT use it
1572
+
1573
+ 4. **Framework compatibility:** Check the \`framework_type\` and \`import_statement\` fields for each component
1574
+
1575
+ ---
1576
+
1577
+ ## Search Results (${searchResults.relevantMatches} components)
1578
+
1579
+ ${Formatters.toMarkdown(searchResults)}
1580
+
1581
+ ---
1582
+
1583
+ `
1584
+ });
1585
+ if (useFrida) {
1586
+ const frida = getFridaClient();
1587
+ if (frida.isEnabled()) {
1588
+ if (streamContent) {
1589
+ await streamContent({
1590
+ type: "text",
1591
+ text: "\u{1F916} Processing with Frida AI for intelligent recommendations..."
1592
+ });
1593
+ }
1594
+ const contextForFrida = Formatters.matchesToContext(
1595
+ searchResults.matches
1596
+ );
1597
+ const question = `Based on this Figma design context, which component(s) from the design system should I use? Design description: ${searchQuery}`;
1598
+ console.log(`[COMPONENT SUGGEST] Calling Frida AI with ${searchResults.matches.length} component matches...`);
1599
+ const fridaStartTime = Date.now();
1600
+ const fridaResponse = await frida.ask(question, contextForFrida);
1601
+ const fridaTime = Date.now() - fridaStartTime;
1602
+ console.log(`[COMPONENT SUGGEST] Frida AI completed in ${fridaTime}ms - Success: ${fridaResponse.success}`);
1603
+ if (fridaResponse.success && fridaResponse.response) {
1604
+ content.push({
1605
+ type: "text",
1606
+ text: `## \u{1F916} Frida AI Recommendation
1607
+
1608
+ ${fridaResponse.response}
1609
+
1610
+ ---
1611
+
1612
+ `
1613
+ });
1614
+ const topComponents = searchResults.matches.slice(0, 3);
1615
+ if (topComponents.length > 0) {
1616
+ let examplesText = `## \u{1F4DD} CODE GENERATION RULES - Recommended Usage
1617
+
1618
+ **MANDATORY: Use ONLY components from your design system. Here are examples:**
1619
+
1620
+ `;
1621
+ topComponents.forEach((comp) => {
1622
+ const componentName = comp.tag;
1623
+ const importStmt = comp.import_statement;
1624
+ const frameworkType2 = comp.framework_type || "Web Component";
1625
+ examplesText += `### ${comp.human_name || componentName}
1626
+ **Tag:** \`${componentName}\`
1627
+ **Framework:** ${frameworkType2}
1628
+ ${importStmt ? `**Import:** \`${importStmt}\`` : ""}
1629
+
1630
+ `;
1631
+ if (comp.usage_examples && comp.usage_examples.length > 0) {
1632
+ comp.usage_examples.slice(0, 1).forEach((example) => {
1633
+ examplesText += `\u2705 **${example.title}**
1634
+ ${example.description ? `*${example.description}*
1635
+ ` : ""}
1636
+ \`\`\`html
1637
+ ${example.code.template}
1638
+ \`\`\`
1639
+ ${example.code.script ? `\`\`\`javascript
1640
+ ${example.code.script}
1641
+ \`\`\`` : ""}
1642
+
1643
+ `;
1644
+ });
1645
+ } else {
1646
+ const propsExample = comp.props.filter((p) => !p.is_utility_prop).slice(0, 2).map((p) => `${p.name}="${p.default_value || "value"}"`).join(" ");
1647
+ examplesText += `\u2705 **Recommended Usage:**
1648
+ \`\`\`html
1649
+ <${componentName}${propsExample ? ` ${propsExample}` : ""}></${componentName}>
1650
+ \`\`\`
1651
+
1652
+ `;
1653
+ }
1654
+ if (comp.props.length > 0) {
1655
+ examplesText += `**Key Props:**
1656
+ ${comp.props.slice(0, 3).map((p) => `- \`${p.name}\`: ${p.description || p.type_signature}`).join("\n")}
1657
+
1658
+ `;
1659
+ }
1660
+ examplesText += `---
1661
+ `;
1662
+ });
1663
+ examplesText += `
1664
+ **Remember:** Always use components from your design system as listed above. Follow the import statements and usage examples provided.
1665
+
1666
+ ---
1667
+
1668
+ `;
1669
+ content.push({
1670
+ type: "text",
1671
+ text: examplesText
1672
+ });
1673
+ }
1674
+ if (fridaResponse.metadata?.tokensUsed) {
1675
+ content.push({
1676
+ type: "text",
1677
+ text: `*(Tokens used: ${fridaResponse.metadata.tokensUsed})*
1678
+
1679
+ ---
1680
+
1681
+ `
1682
+ });
1683
+ }
1684
+ } else {
1685
+ content.push({
1686
+ type: "text",
1687
+ text: `\u26A0\uFE0F **Frida AI Processing Failed**
1688
+
1689
+ ${fridaResponse.error || "Unknown error"}
1690
+
1691
+ Falling back to raw search results.
1692
+
1693
+ ---
1694
+
1695
+ `
1696
+ });
1697
+ }
1698
+ } else {
1699
+ content.push({
1700
+ type: "text",
1701
+ text: `\u2139\uFE0F **Frida AI Not Configured**
1702
+
1703
+ Frida AI is not enabled. Set \`FRIDA_BEARER_TOKEN\` and \`FRIDA_API_URL\` environment variables to enable intelligent recommendations.
1704
+
1705
+ Showing raw search results instead.
1706
+
1707
+ ---
1708
+
1709
+ `
1710
+ });
1711
+ }
1712
+ }
1713
+ componentSuggestionsCache.set(cacheKey, {
1714
+ data: content,
1715
+ timestamp: now
1716
+ });
1717
+ console.log(`[COMPONENT SUGGEST] Completed successfully`);
1718
+ return { content };
1719
+ } catch (error) {
1720
+ console.error(`[COMPONENT SUGGEST ERROR]`, error);
1721
+ return {
1722
+ content: [
1723
+ {
1724
+ type: "text",
1725
+ text: `\u274C **Failed to Suggest Components**
1726
+
1727
+ **Error:** ${error instanceof Error ? error.message : String(error)}
1728
+
1729
+ **Troubleshooting:**
1730
+ 1. Is Figma Desktop running?
1731
+ 2. Is Pinecone configured? (Check PINECONE_API_KEY)
1732
+ 3. Are components indexed in Pinecone?
1733
+ 4. Check the console for detailed error messages
1734
+
1735
+ ${error instanceof Error && error.stack ? `
1736
+ **Stack:**
1737
+ \`\`\`
1738
+ ${error.stack}
1739
+ \`\`\`` : ""}`
1740
+ }
1741
+ ]
1742
+ };
1743
+ }
1744
+ }
1745
+
1746
+ // src/figma2frida/utils/component-extractor.ts
1747
+ var ComponentExtractor = class {
1748
+ /**
1749
+ * Pattern to match custom components in various formats:
1750
+ * - Web Components (hyphenated): <my-button>, <custom-badge>
1751
+ * - PascalCase components: <Button>, <CustomBadge>
1752
+ * - Standalone references
1753
+ * - Import statements
1754
+ */
1755
+ static COMPONENT_PATTERNS = [
1756
+ // Web Component tags (hyphenated custom elements): <my-button>, <custom-badge>
1757
+ /<([a-z]+-[\w-]+)/gi,
1758
+ // PascalCase component tags (React/Vue/etc): <Button>, <CustomBadge>
1759
+ /<([A-Z][a-zA-Z0-9]+)(?:\s|>|\/)/g,
1760
+ // Standalone hyphenated references: my-button, custom-badge
1761
+ /\b([a-z]+-[\w-]+)\b/gi
1762
+ ];
1763
+ /**
1764
+ * Extracts all unique component tags from code content
1765
+ * @param content - Code content as string or array of content items
1766
+ * @returns Array of unique component tags found
1767
+ */
1768
+ static extractComponents(content) {
1769
+ const componentMap = /* @__PURE__ */ new Map();
1770
+ const textContents = [];
1771
+ if (typeof content === "string") {
1772
+ textContents.push(content);
1773
+ } else if (Array.isArray(content)) {
1774
+ for (const item of content) {
1775
+ if (item.type === "text" && typeof item.text === "string") {
1776
+ textContents.push(item.text);
1777
+ }
1778
+ }
1779
+ }
1780
+ for (const text of textContents) {
1781
+ for (const pattern of this.COMPONENT_PATTERNS) {
1782
+ const matches = text.matchAll(pattern);
1783
+ for (const match of matches) {
1784
+ if (!match[1]) continue;
1785
+ let fullTag = match[1];
1786
+ const normalizedTag = fullTag.includes("-") ? fullTag.toLowerCase().replace(/-+$/, "") : fullTag;
1787
+ if (this.isCommonHtmlTag(normalizedTag)) {
1788
+ continue;
1789
+ }
1790
+ if (componentMap.has(normalizedTag)) {
1791
+ componentMap.get(normalizedTag).occurrences++;
1792
+ } else {
1793
+ componentMap.set(normalizedTag, {
1794
+ tag: normalizedTag,
1795
+ fullTag: normalizedTag,
1796
+ occurrences: 1
1797
+ });
1798
+ }
1799
+ }
1800
+ }
1801
+ }
1802
+ return Array.from(componentMap.values()).sort(
1803
+ (a, b) => b.occurrences - a.occurrences
1804
+ );
1805
+ }
1806
+ /**
1807
+ * Check if a tag is a common HTML tag (not a custom component)
1808
+ */
1809
+ static isCommonHtmlTag(tag) {
1810
+ const commonTags = /* @__PURE__ */ new Set([
1811
+ "div",
1812
+ "span",
1813
+ "p",
1814
+ "a",
1815
+ "img",
1816
+ "ul",
1817
+ "li",
1818
+ "ol",
1819
+ "table",
1820
+ "tr",
1821
+ "td",
1822
+ "th",
1823
+ "form",
1824
+ "input",
1825
+ "button",
1826
+ "select",
1827
+ "option",
1828
+ "textarea",
1829
+ "label",
1830
+ "h1",
1831
+ "h2",
1832
+ "h3",
1833
+ "h4",
1834
+ "h5",
1835
+ "h6",
1836
+ "header",
1837
+ "footer",
1838
+ "nav",
1839
+ "main",
1840
+ "section",
1841
+ "article",
1842
+ "aside",
1843
+ "figure",
1844
+ "figcaption",
1845
+ "strong",
1846
+ "em",
1847
+ "code",
1848
+ "pre",
1849
+ "iframe",
1850
+ "video",
1851
+ "audio",
1852
+ "canvas",
1853
+ "svg",
1854
+ "path",
1855
+ "g",
1856
+ "circle",
1857
+ "rect",
1858
+ "script",
1859
+ "style",
1860
+ "link",
1861
+ "meta",
1862
+ "title",
1863
+ "head",
1864
+ "body",
1865
+ "html"
1866
+ ]);
1867
+ return commonTags.has(tag.toLowerCase());
1868
+ }
1869
+ /**
1870
+ * Extracts component tags and returns just the tag names
1871
+ * @param content - Code content
1872
+ * @returns Array of unique component tag names
1873
+ */
1874
+ static extractComponentTags(content) {
1875
+ return this.extractComponents(content).map((c) => c.tag);
1876
+ }
1877
+ /**
1878
+ * Checks if content contains any custom components
1879
+ * @param content - Code content
1880
+ * @returns true if components are found
1881
+ */
1882
+ static hasComponents(content) {
1883
+ const components = this.extractComponents(content);
1884
+ return components.length > 0;
1885
+ }
1886
+ };
1887
+
1888
+ // src/figma2frida/services/component-lookup.ts
1889
+ var ComponentLookupService = class {
1890
+ pineconeService;
1891
+ lookupCache = /* @__PURE__ */ new Map();
1892
+ cacheEnabled;
1893
+ constructor(pineconeService2, options = {}) {
1894
+ this.pineconeService = pineconeService2 || new PineconeSearchService({ namespace: options.namespace });
1895
+ this.cacheEnabled = options.cacheResults !== false;
1896
+ }
1897
+ /**
1898
+ * Looks up a single component by tag
1899
+ * @param tag - Component tag/selector (e.g., 'my-button', 'Button')
1900
+ * @param options - Lookup options
1901
+ * @returns Component lookup result
1902
+ */
1903
+ async lookupComponent(tag, options = {}) {
1904
+ const cacheKey = options.namespace ? `${tag}:${options.namespace}` : tag;
1905
+ if (this.cacheEnabled && this.lookupCache.has(cacheKey)) {
1906
+ const cached = this.lookupCache.get(cacheKey) ?? null;
1907
+ return {
1908
+ tag,
1909
+ found: cached !== null,
1910
+ component: cached
1911
+ };
1912
+ }
1913
+ try {
1914
+ const component = await this.pineconeService.getComponentDetails(tag);
1915
+ const componentResult = component ?? null;
1916
+ if (this.cacheEnabled) {
1917
+ this.lookupCache.set(cacheKey, componentResult);
1918
+ }
1919
+ return {
1920
+ tag,
1921
+ found: componentResult !== null,
1922
+ component: componentResult
1923
+ };
1924
+ } catch (error) {
1925
+ return {
1926
+ tag,
1927
+ found: false,
1928
+ component: null,
1929
+ error: error instanceof Error ? error.message : String(error)
1930
+ };
1931
+ }
1932
+ }
1933
+ /**
1934
+ * Looks up multiple components in parallel
1935
+ * @param tags - Array of component tags
1936
+ * @param options - Lookup options
1937
+ * @returns Array of lookup results
1938
+ */
1939
+ async lookupMultipleComponents(tags, options = {}) {
1940
+ if (!tags || tags.length === 0) {
1941
+ return [];
1942
+ }
1943
+ const uniqueTags = Array.from(new Set(tags));
1944
+ const lookupPromises = uniqueTags.map(
1945
+ (tag) => this.lookupComponent(tag, options)
1946
+ );
1947
+ const results = await Promise.all(lookupPromises);
1948
+ return results.sort((a, b) => {
1949
+ if (a.found !== b.found) {
1950
+ return a.found ? -1 : 1;
1951
+ }
1952
+ return a.tag.localeCompare(b.tag);
1953
+ });
1954
+ }
1955
+ /**
1956
+ * Gets a summary of found components
1957
+ * @param results - Lookup results
1958
+ * @returns Summary object
1959
+ */
1960
+ static getSummary(results) {
1961
+ const found = results.filter((r) => r.found);
1962
+ const notFound = results.filter((r) => !r.found);
1963
+ return {
1964
+ total: results.length,
1965
+ found: found.length,
1966
+ notFound: notFound.length,
1967
+ foundComponents: found.map((r) => r.component).filter((c) => c !== null),
1968
+ notFoundTags: notFound.map((r) => r.tag)
1969
+ };
1970
+ }
1971
+ /**
1972
+ * Clears the lookup cache
1973
+ */
1974
+ clearCache() {
1975
+ this.lookupCache.clear();
1976
+ }
1977
+ /**
1978
+ * Gets cache statistics
1979
+ */
1980
+ getCacheStats() {
1981
+ return {
1982
+ size: this.lookupCache.size,
1983
+ keys: Array.from(this.lookupCache.keys())
1984
+ };
1985
+ }
1986
+ };
1987
+
1988
+ // src/figma2frida/figma2frida.ts
1989
+ console.log("\n[CONFIG] Validating environment variables...\n");
1990
+ var missingVars = [];
1991
+ if (!process.env.PINECONE_API_KEY) {
1992
+ console.error(`[CONFIG] \u274C ERROR: PINECONE_API_KEY is missing from .env file`);
1993
+ missingVars.push("PINECONE_API_KEY");
1994
+ } else {
1995
+ console.log(`[CONFIG] \u2713 PINECONE_API_KEY: ${process.env.PINECONE_API_KEY.substring(0, 8)}... (hidden)`);
1996
+ }
1997
+ if (!process.env.PINECONE_INDEX) {
1998
+ console.error(`[CONFIG] \u274C ERROR: PINECONE_INDEX is missing from .env file`);
1999
+ missingVars.push("PINECONE_INDEX");
2000
+ } else {
2001
+ console.log(`[CONFIG] \u2713 PINECONE_INDEX: ${process.env.PINECONE_INDEX}`);
2002
+ }
2003
+ if (!process.env.PINECONE_MIN_SCORE) {
2004
+ console.error(`[CONFIG] \u274C ERROR: PINECONE_MIN_SCORE is missing from .env file`);
2005
+ missingVars.push("PINECONE_MIN_SCORE");
2006
+ } else {
2007
+ const parsed = parseFloat(process.env.PINECONE_MIN_SCORE);
2008
+ if (isNaN(parsed)) {
2009
+ console.error(`[CONFIG] \u274C ERROR: PINECONE_MIN_SCORE is not a valid number: "${process.env.PINECONE_MIN_SCORE}"`);
2010
+ missingVars.push("PINECONE_MIN_SCORE");
2011
+ } else {
2012
+ console.log(`[CONFIG] \u2713 PINECONE_MIN_SCORE: ${parsed}`);
2013
+ }
2014
+ }
2015
+ if (!process.env.PINECONE_TOP_K) {
2016
+ console.error(`[CONFIG] \u274C ERROR: PINECONE_TOP_K is missing from .env file`);
2017
+ missingVars.push("PINECONE_TOP_K");
2018
+ } else {
2019
+ const parsed = parseInt(process.env.PINECONE_TOP_K, 10);
2020
+ if (isNaN(parsed)) {
2021
+ console.error(`[CONFIG] \u274C ERROR: PINECONE_TOP_K is not a valid integer: "${process.env.PINECONE_TOP_K}"`);
2022
+ missingVars.push("PINECONE_TOP_K");
2023
+ } else {
2024
+ console.log(`[CONFIG] \u2713 PINECONE_TOP_K: ${parsed}`);
2025
+ }
2026
+ }
2027
+ if (missingVars.length > 0) {
2028
+ console.error(`
2029
+ [CONFIG] \u274C Missing required environment variables: ${missingVars.join(", ")}
2030
+ `);
2031
+ process.exit(1);
2032
+ }
2033
+ console.log(`[CONFIG] \u2713 All required Pinecone environment variables are set
2034
+ `);
2035
+ var argv = yargs(hideBin(process.argv)).option("pinecone-namespace", {
2036
+ type: "string",
2037
+ description: "Pinecone namespace (passed via options, not from .env)"
2038
+ }).parseSync();
2039
+ var PINECONE_NAMESPACE = argv.pineconeNamespace || void 0;
2040
+ if (PINECONE_NAMESPACE) {
2041
+ console.log(`[CONFIG] \u2713 PINECONE_NAMESPACE: ${PINECONE_NAMESPACE} (from CLI)
2042
+ `);
2043
+ }
2044
+ var HTTP_STREAM = process.env.HTTP_STREAM === "true" || process.env.HTTP_STREAM === "1";
2045
+ var PORT = parseInt(process.env.PORT || "8080", 10);
2046
+ var designContextCache = /* @__PURE__ */ new Map();
2047
+ var CACHE_TTL_MS = 6e4;
2048
+ var server = new FastMCP({
2049
+ instructions: `You are a Figma design assistant optimized for fast code generation using your organization's design system components.
2050
+
2051
+ **\u{1F6A8} CRITICAL: Follow Figma Design Exactly**
2052
+
2053
+ When generating code from Figma designs, you MUST:
2054
+
2055
+ 1. **USE THE TRANSFORMED CODE AS YOUR BASE** - The \`figma_get_design_context\` tool with \`transformToDesignSystem: true\` returns code that:
2056
+ - Preserves the EXACT structure, layout, and styling from Figma
2057
+ - Transforms generic HTML elements to design system components
2058
+ - Maintains all divs, containers, classes, and CSS from the original design
2059
+ - **DO NOT invent new layouts or structures** - Use the transformed code exactly as provided
2060
+
2061
+ 2. **SHOW ELEMENT ANALYSIS** - Before showing code, clearly display:
2062
+ - What HTML elements were found in the Figma design
2063
+ - Which design system components will be used for each element
2064
+ - The mapping: HTML element \u2192 Design system component
2065
+ - Any elements that couldn't be transformed (and why)
2066
+
2067
+ 3. **PRESERVE DESIGN STRUCTURE** - The transformed code maintains:
2068
+ - All container divs and layout structure
2069
+ - All CSS classes and inline styles
2070
+ - All spacing, positioning, and visual hierarchy
2071
+ - The exact visual appearance from Figma
2072
+
2073
+ 4. **USE ONLY DESIGN SYSTEM COMPONENTS** - Components must be:
2074
+ - From your organization's design system library
2075
+ - From the component suggestions provided
2076
+ - NEVER use generic component libraries unless they appear in search results
2077
+ - Framework/language agnostic - use whatever framework type is specified in component metadata (Web Components, React, Angular, Vue, etc.)
2078
+
2079
+ **Available tools:**
2080
+ - figma_get_screenshot: Get visual screenshot (fast, ~0.5-1s)
2081
+ - figma_get_design_context: Get design context and transformed code with design system components (main tool)
2082
+ - figma_suggest_components: Suggest design system components based on Figma design
2083
+
2084
+ **Workflow for code generation:**
2085
+ 1. User asks for code from Figma design
2086
+ 2. Call \`figma_get_design_context\` with \`forceCode: true\` and \`transformToDesignSystem: true\`
2087
+ 3. The tool will return:
2088
+ - Analysis of HTML elements found
2089
+ - Mapping to design system components
2090
+ - Transformed code preserving Figma design structure
2091
+ - Component metadata including framework type, import statements, and usage examples
2092
+ 4. **USE THE TRANSFORMED CODE AS PROVIDED** - Do not modify the structure or layout
2093
+ 5. Show the element analysis and mapping clearly to the user
2094
+ 6. Present the transformed code maintaining all Figma design elements
2095
+ 7. **Use the component's framework type and import statements** as specified in the component metadata
2096
+
2097
+ **IMPORTANT:** The transformed code from Figma is the source of truth. Do not create new layouts or modify the structure. Use only the design system components as mapped in the transformation. The framework/language depends on what's configured in your component library.
2098
+
2099
+ Connection is established on startup. Always use design system components from the configured library.`,
2100
+ name: "figma-proxy",
2101
+ version: "1.0.0"
2102
+ });
2103
+ console.log("\n[FIGMA2FRIDA] ========================================");
2104
+ console.log("[FIGMA2FRIDA] Initializing Figma Client...");
2105
+ var figmaClient = new FigmaClient("http://127.0.0.1:3845/sse");
2106
+ console.log("[FIGMA2FRIDA] \u2713 Figma Client object created");
2107
+ console.log("[FIGMA2FRIDA] ========================================\n");
2108
+ var figmaConnected = false;
2109
+ console.log("[FIGMA2FRIDA] Starting initial connection attempt to Figma MCP...");
2110
+ console.log("[FIGMA2FRIDA] This is a non-blocking async operation - server will continue starting\n");
2111
+ var connectionStartTime = Date.now();
2112
+ figmaClient.connect().then(() => {
2113
+ const connectionTime = Date.now() - connectionStartTime;
2114
+ figmaConnected = true;
2115
+ console.log("\n[FIGMA2FRIDA] ========================================");
2116
+ console.log("[FIGMA2FRIDA] \u2705 STARTUP CONNECTION SUCCESS!");
2117
+ console.log(`[FIGMA2FRIDA] Connection time: ${connectionTime}ms`);
2118
+ console.log(`[FIGMA2FRIDA] Status: figmaConnected = ${figmaConnected}`);
2119
+ console.log("[FIGMA2FRIDA] ========================================\n");
2120
+ }).catch((error) => {
2121
+ const connectionTime = Date.now() - connectionStartTime;
2122
+ console.log("\n[FIGMA2FRIDA] ========================================");
2123
+ console.log("[FIGMA2FRIDA] \u274C STARTUP CONNECTION FAILED");
2124
+ console.log(`[FIGMA2FRIDA] Time elapsed: ${connectionTime}ms`);
2125
+ console.log(`[FIGMA2FRIDA] Status: figmaConnected = ${figmaConnected}`);
2126
+ console.log("[FIGMA2FRIDA] \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
2127
+ console.error("[FIGMA2FRIDA] Error type:", error?.constructor?.name || typeof error);
2128
+ console.error("[FIGMA2FRIDA] Error message:", error instanceof Error ? error.message : String(error));
2129
+ if (error && typeof error === "object") {
2130
+ const errorObj = error;
2131
+ if ("code" in errorObj) {
2132
+ console.error("[FIGMA2FRIDA] Error code:", errorObj.code);
2133
+ }
2134
+ }
2135
+ if (error instanceof Error && error.stack) {
2136
+ console.error("[FIGMA2FRIDA] Error stack:");
2137
+ console.error(error.stack);
2138
+ }
2139
+ console.log("[FIGMA2FRIDA] \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
2140
+ console.error("[FIGMA2FRIDA] Make sure Figma Desktop is running with MCP enabled at http://127.0.0.1:3845/sse");
2141
+ console.log("[FIGMA2FRIDA] Note: Tools will attempt to reconnect when called");
2142
+ console.log("[FIGMA2FRIDA] ========================================\n");
2143
+ });
2144
+ server.addTool({
2145
+ name: "figma_get_screenshot",
2146
+ description: `Get a screenshot of a Figma node for visual reference.
2147
+
2148
+ **Fast & Simple:**
2149
+ - Takes ~0.5-1 second
2150
+ - Returns just the image, no code generation
2151
+ - Use this when you need to see what the design looks like
2152
+
2153
+ **Usage:**
2154
+ - Provide a nodeId (e.g., "315-2920")
2155
+ - Or leave empty to use current Figma selection
2156
+
2157
+ **Example:**
2158
+ \`\`\`json
2159
+ {
2160
+ "nodeId": "315-2920"
2161
+ }
2162
+ \`\`\``,
2163
+ annotations: {
2164
+ title: "Get Screenshot",
2165
+ readOnlyHint: true,
2166
+ openWorldHint: false
2167
+ },
2168
+ parameters: z.object({
2169
+ nodeId: z.string().optional().describe('Figma node ID (e.g., "315-2920"). Leave empty for current selection.')
2170
+ }),
2171
+ execute: async (args, _context) => {
2172
+ try {
2173
+ console.log(`[FIGMA_GET_SCREENSHOT] Tool called with nodeId: ${args.nodeId || "current selection"}`);
2174
+ console.log(`[FIGMA_GET_SCREENSHOT] figmaConnected flag: ${figmaConnected}`);
2175
+ if (!figmaConnected) {
2176
+ console.log(`[FIGMA_GET_SCREENSHOT] \u26A0\uFE0F Early return: figmaConnected is false`);
2177
+ const result = {
2178
+ content: [{
2179
+ type: "text",
2180
+ text: "\u274C Not connected to Figma MCP. Check logs for connection details."
2181
+ }]
2182
+ };
2183
+ return result;
2184
+ }
2185
+ console.log(`[FIGMA_GET_SCREENSHOT] Calling figmaClient.getScreenshot()...`);
2186
+ const screenshot = await figmaClient.getScreenshot(args.nodeId);
2187
+ if (!screenshot.content || screenshot.content.length === 0) {
2188
+ const result = {
2189
+ content: [{ type: "text", text: "\u274C No screenshot returned" }]
2190
+ };
2191
+ return result;
2192
+ }
2193
+ return { content: screenshot.content };
2194
+ } catch (error) {
2195
+ const result = {
2196
+ content: [{
2197
+ type: "text",
2198
+ text: `\u274C Failed to get screenshot: ${error instanceof Error ? error.message : String(error)}`
2199
+ }]
2200
+ };
2201
+ return result;
2202
+ }
2203
+ }
2204
+ });
2205
+ server.addTool({
2206
+ name: "figma_get_design_context",
2207
+ description: `Get design context and generated code directly from Figma Desktop MCP.
2208
+
2209
+ **What this does:**
2210
+ - Calls Figma's get_design_context tool
2211
+ - Returns design data and optionally generated code (React, HTML, etc.)
2212
+ - Automatically searches for design system components in generated code
2213
+ - Results are cached for 60 seconds for instant subsequent access
2214
+ - Streams progress updates for better responsiveness
2215
+
2216
+ **Performance Modes:**
2217
+ - \`forceCode: false\` (default) - Fast metadata-only mode (~1-2s)
2218
+ - \`forceCode: true\` - Full code generation (~3-15s depending on complexity)
2219
+
2220
+ **Component Search:**
2221
+ - When \`forceCode: true\` and \`autoSearchComponents: true\` (default), automatically:
2222
+ - Extracts component tags from generated code
2223
+ - Searches component information in Pinecone
2224
+ - Streams component details while processing
2225
+ - Adds summary section with found/not found components
2226
+
2227
+ **Code Transformation:**
2228
+ - When \`forceCode: true\` and \`transformToDesignSystem: true\` (default), automatically:
2229
+ - **Analyzes** all HTML elements found in Figma design
2230
+ - **Shows mapping** of HTML elements \u2192 design system components before transformation
2231
+ - **Transforms** generic HTML elements (button, input, etc.) to design system components
2232
+ - **Preserves** ALL structure, layout, CSS classes, and styling from Figma design
2233
+ - **Maintains** all container divs, spacing, positioning, and visual hierarchy
2234
+ - **Uses component metadata** including framework type, import statements, and usage examples
2235
+ - Returns transformed code as the main output, with original code for reference
2236
+ - Shows detailed analysis and transformation summary
2237
+
2238
+ **Output includes:**
2239
+ 1. Element Analysis - What HTML elements were found in Figma
2240
+ 2. Element Mapping - Which design system components will be used for each element
2241
+ 3. Transformed Code - Ready-to-use code preserving Figma design structure
2242
+ 4. Original Code - For reference and comparison
2243
+
2244
+ **Usage:**
2245
+ - Provide a nodeId (e.g., "315-2920")
2246
+ - Or leave empty to use current Figma selection
2247
+ - Set forceCode to true only when you need the actual React/HTML code
2248
+ - Set autoSearchComponents to false to disable automatic component search
2249
+
2250
+ **Example:**
2251
+ \`\`\`json
2252
+ {
2253
+ "nodeId": "315-2920",
2254
+ "forceCode": true,
2255
+ "autoSearchComponents": true
2256
+ }
2257
+ \`\`\`
2258
+
2259
+ **Tip:** Start with \`forceCode: false\` to quickly see what's available, then request code if needed.`,
2260
+ annotations: {
2261
+ title: "Get Design Context from Figma",
2262
+ readOnlyHint: true,
2263
+ openWorldHint: false
2264
+ },
2265
+ parameters: z.object({
2266
+ nodeId: z.string().optional().describe('Figma node ID (e.g., "315-2920" or "315:2920"). Leave empty for current selection.'),
2267
+ forceCode: z.boolean().optional().describe("Force code generation even for large outputs (default: false). Set to true only when you need the actual React/HTML code."),
2268
+ autoSearchComponents: z.boolean().optional().describe("Automatically search for design system components in the generated code (default: true). Only works when forceCode is true."),
2269
+ transformToDesignSystem: z.boolean().optional().describe("Transform Figma-generated code to use design system components (default: true). Only works when forceCode is true. Replaces generic HTML elements (button, input, etc.) with components from your design system library. Framework and language are determined by component metadata.")
2270
+ }),
2271
+ execute: async (args, context) => {
2272
+ try {
2273
+ console.log(`
2274
+ [FIGMA_GET_DESIGN_CONTEXT] ========================================`);
2275
+ console.log(`[FIGMA_GET_DESIGN_CONTEXT] Tool called`);
2276
+ console.log(`[FIGMA_GET_DESIGN_CONTEXT] nodeId: ${args.nodeId || "current selection"}`);
2277
+ console.log(`[FIGMA_GET_DESIGN_CONTEXT] forceCode: ${args.forceCode ?? false}`);
2278
+ console.log(`[FIGMA_GET_DESIGN_CONTEXT] figmaConnected flag: ${figmaConnected}`);
2279
+ console.log(`[FIGMA_GET_DESIGN_CONTEXT] ========================================`);
2280
+ if (!figmaConnected) {
2281
+ console.log(`[FIGMA_GET_DESIGN_CONTEXT] \u26A0\uFE0F Early return: figmaConnected is false`);
2282
+ return {
2283
+ content: [
2284
+ {
2285
+ type: "text",
2286
+ text: "\u274C Not connected to Figma MCP. Check logs for connection details."
2287
+ }
2288
+ ]
2289
+ };
2290
+ }
2291
+ console.log(`[FIGMA_GET_DESIGN_CONTEXT] Calling figmaClient.getDesignContext()...`);
2292
+ console.log(`
2293
+ [DESIGN CONTEXT] Starting...`);
2294
+ const nodeId = args.nodeId;
2295
+ const forceCode = args.forceCode ?? false;
2296
+ const autoSearchComponents = args.autoSearchComponents ?? true;
2297
+ const transformToDesignSystem = args.transformToDesignSystem ?? true;
2298
+ console.log(`[DESIGN CONTEXT] NodeId:`, nodeId || "current selection");
2299
+ console.log(`[DESIGN CONTEXT] ForceCode:`, forceCode);
2300
+ console.log(`[DESIGN CONTEXT] AutoSearchComponents:`, autoSearchComponents);
2301
+ console.log(`[DESIGN CONTEXT] TransformToDesignSystem:`, transformToDesignSystem);
2302
+ const cacheKey = `${nodeId || "current"}:${forceCode}`;
2303
+ const now = Date.now();
2304
+ const cached = designContextCache.get(cacheKey);
2305
+ if (cached && now - cached.timestamp < CACHE_TTL_MS) {
2306
+ console.log(`[DESIGN CONTEXT] Using cached result (age: ${(now - cached.timestamp).toFixed(0)}ms)`);
2307
+ await context.streamContent({
2308
+ type: "text",
2309
+ text: `\u26A1 Using cached design context (${((now - cached.timestamp) / 1e3).toFixed(1)}s old)`
2310
+ });
2311
+ const header2 = {
2312
+ type: "text",
2313
+ text: `\u2705 **Design Context from Figma** (Cached)
2314
+
2315
+ **Node ID:** ${nodeId || "current selection"}
2316
+ **Content Items:** ${cached.data.content?.length || 0}
2317
+ **Cache Age:** ${((now - cached.timestamp) / 1e3).toFixed(1)}s
2318
+ **Mode:** ${forceCode ? "Full code generation" : "Metadata only (fast mode)"}
2319
+
2320
+ \u{1F4BE} **Cache hit!** This response was retrieved from cache for instant performance.
2321
+
2322
+ ---
2323
+
2324
+ `
2325
+ };
2326
+ return {
2327
+ content: [header2, ...cached.data.content || []]
2328
+ };
2329
+ }
2330
+ await context.streamContent({
2331
+ type: "text",
2332
+ text: `\u{1F504} Fetching design from Figma${forceCode ? " (including code generation)" : ""}...`
2333
+ });
2334
+ console.log(`[DESIGN CONTEXT] Calling Figma MCP get_design_context...`);
2335
+ const startTime = performance.now();
2336
+ const designResult = await figmaClient.getDesignContext(nodeId, {
2337
+ clientLanguages: "typescript",
2338
+ clientFrameworks: "react",
2339
+ forceCode
2340
+ });
2341
+ const endTime = performance.now();
2342
+ designContextCache.set(cacheKey, {
2343
+ data: designResult,
2344
+ timestamp: now,
2345
+ forceCode
2346
+ });
2347
+ console.log(`[DESIGN CONTEXT] Cached result for key: ${cacheKey}`);
2348
+ console.log(`[DESIGN CONTEXT] Response received in ${(endTime - startTime).toFixed(0)}ms`);
2349
+ console.log(`[DESIGN CONTEXT] Content items:`, designResult.content?.length || 0);
2350
+ if (!designResult.content || designResult.content.length === 0) {
2351
+ return {
2352
+ content: [
2353
+ {
2354
+ type: "text",
2355
+ text: `\u274C No design context returned from Figma MCP.
2356
+
2357
+ **Node ID:** ${nodeId || "current selection"}
2358
+
2359
+ **Troubleshooting:**
2360
+ - Ensure the node exists and is accessible in Figma
2361
+ - Try selecting the node in Figma first
2362
+ - Check if the node has any visible design elements`
2363
+ }
2364
+ ]
2365
+ };
2366
+ }
2367
+ designResult.content.forEach((item, idx) => {
2368
+ console.log(`[DESIGN CONTEXT] Content[${idx}]:`, {
2369
+ type: item.type,
2370
+ hasText: item.type === "text" && "text" in item,
2371
+ textLength: item.type === "text" && "text" in item ? item.text?.length : 0,
2372
+ hasData: item.type === "image" && "data" in item
2373
+ });
2374
+ });
2375
+ const header = {
2376
+ type: "text",
2377
+ text: `\u2705 **Design Context from Figma**
2378
+
2379
+ **Node ID:** ${nodeId || "current selection"}
2380
+ **Content Items:** ${designResult.content.length}
2381
+ **Fetch Time:** ${(endTime - startTime).toFixed(0)}ms
2382
+ **Mode:** ${forceCode ? "Full code generation" : "Metadata only (fast mode)"}
2383
+
2384
+ ${forceCode ? "" : "\u{1F4A1} **Tip:** If you need the actual React/HTML code, call this tool again with `forceCode: true`\n\n"}**What you're seeing:** Raw output from Figma's get_design_context tool.
2385
+
2386
+ ---
2387
+
2388
+ `
2389
+ };
2390
+ console.log(`\u2705 Design context retrieved successfully`);
2391
+ let transformedCodeSection = null;
2392
+ let componentSuggestions = [];
2393
+ if (forceCode && transformToDesignSystem && designResult.content) {
2394
+ try {
2395
+ await context.streamContent({
2396
+ type: "text",
2397
+ text: "\u{1F504} Transforming code to use design system components..."
2398
+ });
2399
+ if (autoSearchComponents) {
2400
+ await context.streamContent({
2401
+ type: "text",
2402
+ text: "\u{1F50D} Discovering design system components from design analysis..."
2403
+ });
2404
+ try {
2405
+ const queryExtractor2 = new DesignQueryExtractor();
2406
+ const searchQuery = queryExtractor2.extractQuery(designResult);
2407
+ console.log(`[TRANSFORM] Extracted query: "${searchQuery}"`);
2408
+ await context.streamContent({
2409
+ type: "text",
2410
+ text: `\u{1F50E} Searching Pinecone for: "${searchQuery}"...`
2411
+ });
2412
+ const searchService = new PineconeSearchService({
2413
+ namespace: PINECONE_NAMESPACE
2414
+ });
2415
+ const searchResults = await searchService.search(searchQuery, {
2416
+ // topK and minScore use service defaults (read from PINECONE_TOP_K and PINECONE_MIN_SCORE env vars)
2417
+ });
2418
+ componentSuggestions = searchResults.matches;
2419
+ const componentNames = componentSuggestions.map((comp) => comp.tag).join(", ");
2420
+ console.log(
2421
+ `[TRANSFORM] Found ${componentSuggestions.length} components from Pinecone: ${componentNames}`
2422
+ );
2423
+ if (componentSuggestions.length > 0) {
2424
+ await context.streamContent({
2425
+ type: "text",
2426
+ text: `\u2705 Discovered ${componentSuggestions.length} design system components:
2427
+ ${componentSuggestions.map((c) => ` - ${c.tag} (${c.category})`).join("\n")}`
2428
+ });
2429
+ } else {
2430
+ await context.streamContent({
2431
+ type: "text",
2432
+ text: `\u26A0\uFE0F No components found in Pinecone. Using default mappings.`
2433
+ });
2434
+ }
2435
+ } catch (error) {
2436
+ console.warn(
2437
+ `[TRANSFORM] Could not get component suggestions:`,
2438
+ error
2439
+ );
2440
+ await context.streamContent({
2441
+ type: "text",
2442
+ text: `\u26A0\uFE0F Error searching Pinecone: ${error instanceof Error ? error.message : String(error)}
2443
+ Falling back to default mappings.`
2444
+ });
2445
+ }
2446
+ }
2447
+ if (componentSuggestions.length > 0) {
2448
+ await context.streamContent({
2449
+ type: "text",
2450
+ text: `\u2705 Found ${componentSuggestions.length} design system components from Pinecone. Returning component documentation to agent.`
2451
+ });
2452
+ const queryExtractor2 = new DesignQueryExtractor();
2453
+ const searchQuery = queryExtractor2.extractQuery(designResult);
2454
+ const minScore = parseFloat(process.env.PINECONE_MIN_SCORE);
2455
+ const topK = parseInt(process.env.PINECONE_TOP_K, 10);
2456
+ const indexName = process.env.PINECONE_INDEX;
2457
+ transformedCodeSection = {
2458
+ type: "text",
2459
+ text: `## \u{1F50D} **Available Design System Components from Pinecone**
2460
+
2461
+ **Search Query:** "${searchQuery}"
2462
+ **Components Found:** ${componentSuggestions.length}
2463
+
2464
+ Use the component documentation below to transform the Figma code into design system components. The agent will generate the code using these component specifications.
2465
+
2466
+ ---
2467
+
2468
+ ${Formatters.toMarkdown({
2469
+ query: searchQuery,
2470
+ matches: componentSuggestions,
2471
+ relevantMatches: componentSuggestions.length,
2472
+ totalMatches: componentSuggestions.length,
2473
+ searchMetadata: { minScore, topK, indexName },
2474
+ lowScoreMatches: []
2475
+ })}
2476
+
2477
+ **\u{1F4A1} Instructions:** Use the component documentation above to transform the Figma code. Each component includes:
2478
+ - Exact prop names and types
2479
+ - Event names and types
2480
+ - Full documentation
2481
+ - Usage examples
2482
+
2483
+ Generate code that uses these components with the correct props and structure.
2484
+
2485
+ ---
2486
+
2487
+ `
2488
+ };
2489
+ } else {
2490
+ await context.streamContent({
2491
+ type: "text",
2492
+ text: `\u26A0\uFE0F No components found in Pinecone for this design.`
2493
+ });
2494
+ }
2495
+ } catch (error) {
2496
+ console.error(`[CODE TRANSFORM ERROR]`, error);
2497
+ await context.streamContent({
2498
+ type: "text",
2499
+ text: `\u26A0\uFE0F Error transforming code: ${error instanceof Error ? error.message : String(error)}
2500
+ `
2501
+ });
2502
+ }
2503
+ }
2504
+ let componentSection = null;
2505
+ if (forceCode && autoSearchComponents && designResult.content) {
2506
+ try {
2507
+ await context.streamContent({
2508
+ type: "text",
2509
+ text: "\u{1F50D} Analyzing generated code for design system components..."
2510
+ });
2511
+ const extractedComponents = ComponentExtractor.extractComponents(
2512
+ designResult.content
2513
+ );
2514
+ if (extractedComponents.length > 0) {
2515
+ await context.streamContent({
2516
+ type: "text",
2517
+ text: `\u{1F4E6} Found ${extractedComponents.length} component reference(s): ${extractedComponents.map((c) => c.tag).join(", ")}`
2518
+ });
2519
+ const lookupService = new ComponentLookupService(void 0, { namespace: PINECONE_NAMESPACE });
2520
+ const componentTags = extractedComponents.map((c) => c.tag);
2521
+ await context.streamContent({
2522
+ type: "text",
2523
+ text: `\u{1F50E} Searching component information in Pinecone...`
2524
+ });
2525
+ const lookupResults = await lookupService.lookupMultipleComponents(
2526
+ componentTags
2527
+ );
2528
+ const summary = ComponentLookupService.getSummary(lookupResults);
2529
+ for (const result of lookupResults) {
2530
+ if (result.found && result.component) {
2531
+ const comp = result.component;
2532
+ await context.streamContent({
2533
+ type: "text",
2534
+ text: `\u2705 **${comp.tag}** - ${comp.category}
2535
+ ${comp.description || "No description available"}
2536
+ `
2537
+ });
2538
+ } else {
2539
+ await context.streamContent({
2540
+ type: "text",
2541
+ text: `\u26A0\uFE0F **${result.tag}** - Not found in component library
2542
+ `
2543
+ });
2544
+ }
2545
+ }
2546
+ let summaryText = `
2547
+ ## \u{1F4CB} **Component Analysis Summary**
2548
+
2549
+ **Total components found in code:** ${summary.total}
2550
+ **Found in library:** ${summary.found}
2551
+ **Not found:** ${summary.notFound}
2552
+
2553
+ `;
2554
+ if (summary.foundComponents.length > 0) {
2555
+ summaryText += `### \u2705 Components Found:
2556
+
2557
+ `;
2558
+ summary.foundComponents.forEach((comp) => {
2559
+ summaryText += `- **${comp.tag}** (${comp.category}) - Score: ${comp.score.toFixed(4)}
2560
+ `;
2561
+ if (comp.description) {
2562
+ summaryText += ` - ${comp.description.substring(0, 100)}${comp.description.length > 100 ? "..." : ""}
2563
+ `;
2564
+ }
2565
+ });
2566
+ summaryText += `
2567
+ `;
2568
+ }
2569
+ if (summary.notFoundTags.length > 0) {
2570
+ summaryText += `### \u26A0\uFE0F Components Not Found:
2571
+
2572
+ `;
2573
+ summary.notFoundTags.forEach((tag) => {
2574
+ summaryText += `- **${tag}** - This component may not be indexed in Pinecone or may not exist
2575
+ `;
2576
+ });
2577
+ summaryText += `
2578
+ `;
2579
+ }
2580
+ summaryText += `\u{1F4A1} **Tip:** Use \`figma_suggest_components\` tool to get detailed information and usage examples for these components.
2581
+
2582
+ ---
2583
+
2584
+ `;
2585
+ componentSection = {
2586
+ type: "text",
2587
+ text: summaryText
2588
+ };
2589
+ } else {
2590
+ await context.streamContent({
2591
+ type: "text",
2592
+ text: `\u2139\uFE0F No design system components found in the generated code.
2593
+ `
2594
+ });
2595
+ }
2596
+ } catch (error) {
2597
+ console.error(`[COMPONENT SEARCH ERROR]`, error);
2598
+ await context.streamContent({
2599
+ type: "text",
2600
+ text: `\u26A0\uFE0F Error searching for components: ${error instanceof Error ? error.message : String(error)}
2601
+ `
2602
+ });
2603
+ }
2604
+ }
2605
+ const finalContent = [header];
2606
+ finalContent.push(...designResult.content);
2607
+ if (transformedCodeSection) {
2608
+ finalContent.push(transformedCodeSection);
2609
+ }
2610
+ if (componentSection) {
2611
+ finalContent.push(componentSection);
2612
+ }
2613
+ return {
2614
+ content: finalContent
2615
+ };
2616
+ } catch (error) {
2617
+ console.error(`[DESIGN CONTEXT ERROR]`, error);
2618
+ return {
2619
+ content: [
2620
+ {
2621
+ type: "text",
2622
+ text: `\u274C **Failed to Get Design Context**
2623
+
2624
+ **Error:** ${error instanceof Error ? error.message : String(error)}
2625
+
2626
+ **Troubleshooting:**
2627
+ 1. Is Figma Desktop running?
2628
+ 2. Is the node ID valid?
2629
+ 3. Is Figma MCP enabled? (Figma \u2192 Preferences \u2192 Enable MCP)
2630
+ 4. Try restarting Figma Desktop
2631
+
2632
+ ${error instanceof Error && error.stack ? `
2633
+ **Stack:**
2634
+ ${error.stack}` : ""}`
2635
+ }
2636
+ ]
2637
+ };
2638
+ }
2639
+ }
2640
+ });
2641
+ server.addTool({
2642
+ name: "figma_suggest_components",
2643
+ description: `**USE THIS TOOL FIRST** when generating code from Figma designs.
2644
+
2645
+ Get component suggestions from your organization's design system based on a Figma design. This tool searches the component library configured in your environment and returns specific components that match your design.
2646
+
2647
+ **CRITICAL:**
2648
+ - This tool returns ONLY components from your organization's design system
2649
+ - NEVER use generic component libraries unless explicitly found in the search results
2650
+ - Always call this tool BEFORE generating code from Figma designs
2651
+ - Use the recommended components from the response in your code generation
2652
+
2653
+ **What this does:**
2654
+ - Fetches design context from Figma (uses cache when available)
2655
+ - Extracts semantic query from the design
2656
+ - Searches your component library for relevant components
2657
+ - Processes results with Frida AI for intelligent recommendations
2658
+ - Returns component suggestions with code examples and usage guidelines
2659
+
2660
+ **Performance:**
2661
+ - Typically completes in 3-10 seconds (depending on Frida processing)
2662
+ - Uses cache to avoid redundant Figma calls
2663
+ - Component search is fast (~1-2s)
2664
+ - Frida AI processing adds 2-5s for intelligent suggestions
2665
+
2666
+ **Usage:**
2667
+ - Provide a nodeId (e.g., "315-2920") or leave empty for current selection
2668
+ - Optionally provide a custom query to override automatic extraction
2669
+ - Configure minScore and topK to control search results
2670
+ - Set useFrida to false to skip AI processing (faster, less intelligent)
2671
+
2672
+ **Example:**
2673
+ \`\`\`json
2674
+ {
2675
+ "nodeId": "315-2920",
2676
+ "minScore": 0.1,
2677
+ "topK": 5,
2678
+ "useFrida": true
2679
+ }
2680
+ \`\`\`
2681
+
2682
+ **Returns:**
2683
+ - Recommended components from Frida AI (if enabled)
2684
+ - Search results with components from your design system
2685
+ - Component details (props, events, methods, slots, usage examples)
2686
+ - Generated code examples with framework-specific imports`,
2687
+ annotations: {
2688
+ title: "Suggest Design System Components",
2689
+ readOnlyHint: true,
2690
+ openWorldHint: false
2691
+ },
2692
+ parameters: z.object({
2693
+ nodeId: z.string().optional().describe('Figma node ID (e.g., "315-2920" or "315:2920"). Leave empty for current selection.'),
2694
+ minScore: z.number().optional().describe("Minimum Pinecone relevance score (default: 0.05). Higher values return more relevant results."),
2695
+ topK: z.number().optional().describe("Maximum number of components to retrieve (default: 10)."),
2696
+ useFrida: z.boolean().optional().describe("Enable Frida AI processing for intelligent recommendations (default: true)."),
2697
+ query: z.string().optional().describe("Custom search query. If not provided, will be extracted automatically from the Figma design context.")
2698
+ }),
2699
+ execute: async (args, context) => {
2700
+ console.log(`
2701
+ [FIGMA_SUGGEST_COMPONENTS] ========================================`);
2702
+ console.log(`[FIGMA_SUGGEST_COMPONENTS] Tool called`);
2703
+ console.log(`[FIGMA_SUGGEST_COMPONENTS] nodeId: ${args.nodeId || "current selection"}`);
2704
+ console.log(`[FIGMA_SUGGEST_COMPONENTS] figmaConnected flag: ${figmaConnected}`);
2705
+ console.log(`[FIGMA_SUGGEST_COMPONENTS] ========================================`);
2706
+ if (!figmaConnected) {
2707
+ console.log(`[FIGMA_SUGGEST_COMPONENTS] \u26A0\uFE0F Early return: figmaConnected is false`);
2708
+ return {
2709
+ content: [
2710
+ {
2711
+ type: "text",
2712
+ text: "\u274C Not connected to Figma MCP. Check logs for connection details."
2713
+ }
2714
+ ]
2715
+ };
2716
+ }
2717
+ console.log(`[FIGMA_SUGGEST_COMPONENTS] Proceeding with component suggestion...`);
2718
+ const nodeId = args.nodeId;
2719
+ const minScore = args.minScore ?? 0.05;
2720
+ const topK = args.topK ?? 10;
2721
+ const useFrida = args.useFrida ?? true;
2722
+ const customQuery = args.query;
2723
+ console.log(`
2724
+ [COMPONENT SUGGEST] Starting...`);
2725
+ console.log(`[COMPONENT SUGGEST] NodeId:`, nodeId || "current selection");
2726
+ console.log(`[COMPONENT SUGGEST] MinScore:`, minScore);
2727
+ console.log(`[COMPONENT SUGGEST] TopK:`, topK);
2728
+ console.log(`[COMPONENT SUGGEST] UseFrida:`, useFrida);
2729
+ await context.streamContent({
2730
+ type: "text",
2731
+ text: "\u{1F3A8} Fetching design context from Figma..."
2732
+ });
2733
+ const designCacheKey = `${nodeId || "current"}:false`;
2734
+ const now = Date.now();
2735
+ let designResult;
2736
+ const cachedDesign = designContextCache.get(designCacheKey);
2737
+ const designCacheAge = cachedDesign ? now - cachedDesign.timestamp : CACHE_TTL_MS + 1;
2738
+ if (cachedDesign && designCacheAge < CACHE_TTL_MS) {
2739
+ console.log(`[COMPONENT SUGGEST] Using cached design context`);
2740
+ designResult = cachedDesign.data;
2741
+ } else {
2742
+ designResult = await figmaClient.getDesignContext(nodeId, {
2743
+ clientLanguages: "typescript",
2744
+ clientFrameworks: "react",
2745
+ forceCode: false
2746
+ // Use metadata mode for faster response
2747
+ });
2748
+ designContextCache.set(designCacheKey, {
2749
+ data: designResult,
2750
+ timestamp: now,
2751
+ forceCode: false
2752
+ });
2753
+ }
2754
+ return handleComponentSuggestion({
2755
+ nodeId,
2756
+ minScore,
2757
+ topK,
2758
+ useFrida,
2759
+ query: customQuery,
2760
+ namespace: PINECONE_NAMESPACE,
2761
+ designContext: designResult,
2762
+ designContextCache,
2763
+ getDesignContext: figmaClient.getDesignContext.bind(figmaClient),
2764
+ streamContent: async (content) => {
2765
+ if (Array.isArray(content)) {
2766
+ for (const item of content) {
2767
+ await context.streamContent(item);
2768
+ }
2769
+ } else {
2770
+ await context.streamContent(content);
2771
+ }
2772
+ }
2773
+ });
2774
+ }
2775
+ });
2776
+ server.addTool({
2777
+ name: "figma_improve_layout",
2778
+ description: `Trigger layout improvement to match the Figma frame reference.
2779
+
2780
+ **What this does:**
2781
+ - Returns an instruction to improve the generated layout to match the Figma design
2782
+ - Use this tool after code generation to refine the layout
2783
+ - The agent will use this instruction to adjust spacing, positioning, and styling
2784
+
2785
+ **Usage:**
2786
+ - Call this tool after generating code from Figma
2787
+ - No parameters required - simply triggers the improvement instruction
2788
+
2789
+ **Example:**
2790
+ \`\`\`json
2791
+ {}
2792
+ \`\`\``,
2793
+ annotations: {
2794
+ title: "Improve Layout to Match Figma",
2795
+ readOnlyHint: false,
2796
+ openWorldHint: false
2797
+ },
2798
+ parameters: z.object({}),
2799
+ execute: async (_args, _context) => {
2800
+ return {
2801
+ content: [
2802
+ {
2803
+ type: "text",
2804
+ text: "#figma2frida improve the layout to match the figma frame reference"
2805
+ }
2806
+ ]
2807
+ };
2808
+ }
2809
+ });
2810
+ if (HTTP_STREAM) {
2811
+ server.start({
2812
+ httpStream: {
2813
+ port: PORT
2814
+ },
2815
+ transportType: "httpStream"
2816
+ });
2817
+ console.log(`
2818
+ \u2705 Figma Proxy MCP Server running on HTTP Stream!
2819
+
2820
+ Endpoint: http://localhost:${PORT}/mcp
2821
+
2822
+ Available Tools:
2823
+ - figma_get_screenshot: Get visual screenshot (fast)
2824
+ - figma_get_design_context: Get design data and generated code (with caching & streaming)
2825
+ - figma_suggest_components: Suggest design system components based on Figma design (with Pinecone & Frida AI)
2826
+ - figma_improve_layout: Trigger layout improvement to match Figma frame reference
2827
+
2828
+ Configuration:
2829
+ - Pinecone Namespace: ${PINECONE_NAMESPACE || "not set (use --pinecone-namespace)"}
2830
+
2831
+ Test with curl:
2832
+ curl -X POST http://localhost:${PORT}/mcp \\
2833
+ -H "Content-Type: application/json" \\
2834
+ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
2835
+ `);
2836
+ } else {
2837
+ server.start({ transportType: "stdio" });
2838
+ console.log("Started with stdio transport (for MCP clients like Cursor)");
2839
+ }
2840
+ //# sourceMappingURL=figma2frida.js.map