@createlex/figma-swiftui-mcp 1.0.6 → 1.0.8

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.
@@ -51,13 +51,50 @@ async function waitForExistingBridge({ host, port }) {
51
51
  throw new Error(`HTTP ${response.status}`);
52
52
  }
53
53
  const data = await response.json();
54
+
55
+ // Verify WebSocket is also healthy, not just HTTP
56
+ await new Promise((resolve, reject) => {
57
+ const testWs = new WebSocket(`ws://${host}:${port}/bridge`);
58
+ const timeout = setTimeout(() => {
59
+ testWs.terminate();
60
+ reject(new Error('WebSocket health check timed out'));
61
+ }, 3000);
62
+ testWs.on('open', () => {
63
+ clearTimeout(timeout);
64
+ testWs.close();
65
+ resolve();
66
+ });
67
+ testWs.on('error', (err) => {
68
+ clearTimeout(timeout);
69
+ reject(new Error(`WebSocket health check failed: ${err.message}`));
70
+ });
71
+ });
72
+
54
73
  return {
55
74
  ok: true,
56
75
  alreadyRunning: true,
57
76
  info: data,
58
77
  };
59
78
  } catch (error) {
60
- throw new Error(`Port ${port} is already in use and does not look like a Figma SwiftUI bridge`);
79
+ throw new Error(`Port ${port} is already in use and does not look like a healthy Figma SwiftUI bridge (${error.message})`);
80
+ }
81
+ }
82
+
83
+ async function killStaleAndRetry({ host, port, logger }) {
84
+ const { execSync } = require('child_process');
85
+ try {
86
+ const lsofOutput = execSync(`lsof -ti :${port}`, { encoding: 'utf8' }).trim();
87
+ const pids = lsofOutput.split('\n').filter(Boolean);
88
+ if (pids.length > 0) {
89
+ logger.warn(`⚠️ Killing ${pids.length} stale process(es) on port ${port}: ${pids.join(', ')}`);
90
+ for (const pid of pids) {
91
+ try { process.kill(Number(pid), 'SIGTERM'); } catch (_) {}
92
+ }
93
+ // Wait for port to free up
94
+ await new Promise((r) => setTimeout(r, 1500));
95
+ }
96
+ } catch (_) {
97
+ // lsof returns non-zero if no matches — port is already free
61
98
  }
62
99
  }
63
100
 
@@ -527,7 +564,7 @@ function startBridgeServer(options = {}) {
527
564
  if (error && error.code === 'EADDRINUSE') {
528
565
  try {
529
566
  const running = await waitForExistingBridge({ host, port });
530
- logger.warn(`ℹ️ Using existing bridge at http://${host}:${port}`);
567
+ logger.warn(`ℹ️ Using existing healthy bridge at http://${host}:${port}`);
531
568
  resolve({
532
569
  app,
533
570
  server: null,
@@ -542,8 +579,47 @@ function startBridgeServer(options = {}) {
542
579
  });
543
580
  return;
544
581
  } catch (existingError) {
545
- reject(existingError);
546
- return;
582
+ // Existing bridge is unhealthy — kill stale processes and start fresh
583
+ logger.warn(`⚠️ ${existingError.message}`);
584
+ try {
585
+ await killStaleAndRetry({ host, port, logger });
586
+ // Re-attempt listen after killing stale processes
587
+ server.listen(port, host, () => {
588
+ logger.info(`🚀 Figma SwiftUI bridge running at http://${host}:${port} (recovered from stale)`);
589
+ if (projectPath) {
590
+ logger.info(`📂 Project path: ${projectPath}`);
591
+ }
592
+ logger.info(`🌉 Bridge ready at ws://${host}:${port}/bridge`);
593
+ resolve({
594
+ app,
595
+ server,
596
+ bridgeWss,
597
+ port,
598
+ host,
599
+ alreadyRunning: false,
600
+ getProjectPath: () => projectPath,
601
+ getBridgeInfo: () => ({
602
+ protocolVersion: BRIDGE_PROTOCOL_VERSION,
603
+ pluginConnected: !!pluginBridgeClient,
604
+ connectedAgents: agentBridgeClients.size,
605
+ pendingRequests: pendingBridgeRequests.size,
606
+ supportedActions: SUPPORTED_BRIDGE_ACTIONS,
607
+ }),
608
+ close: () => new Promise((closeResolve, closeReject) => {
609
+ bridgeWss.close(() => {
610
+ server.close((closeError) => {
611
+ if (closeError) closeReject(closeError);
612
+ else closeResolve();
613
+ });
614
+ });
615
+ }),
616
+ });
617
+ });
618
+ return;
619
+ } catch (retryError) {
620
+ reject(new Error(`Failed to recover bridge: ${retryError.message}`));
621
+ return;
622
+ }
547
623
  }
548
624
  }
549
625
 
@@ -237,7 +237,15 @@ function handleBridgeMessage(rawMessage) {
237
237
  }
238
238
 
239
239
  if (message.type === 'bridge-event') {
240
- console.error(`[figma-swiftui-mcp] Bridge event ${message.event}: ${JSON.stringify(message.data)}`);
240
+ // Log bridge events concisely — skip full JSON payloads for noisy events
241
+ const summary = message.data && message.event === 'selectionchange'
242
+ ? `${message.data.selection?.count ?? 0} node(s) selected`
243
+ : message.data && message.event === 'currentpagechange'
244
+ ? `page "${message.data.currentPage?.name || 'unknown'}"`
245
+ : '';
246
+ if (summary) {
247
+ console.error(`[figma-swiftui-mcp] ${message.event}: ${summary}`);
248
+ }
241
249
  }
242
250
  }
243
251
 
@@ -664,7 +672,7 @@ server.registerTool('write_generated_swiftui_to_xcode', {
664
672
  });
665
673
 
666
674
  server.registerTool('write_selection_to_xcode', {
667
- description: 'Generate SwiftUI from the connected Figma selection and write it directly into the configured Xcode project.',
675
+ description: 'Generate SwiftUI from the connected Figma selection, write it into the configured Xcode project, and return refinement hints. After writing, the AI assistant SHOULD automatically apply the returned refinementInstructions to make the generated code adaptive and interactive — replacing raster placeholders with native SwiftUI controls, adding GeometryReader for adaptive layout, and wiring basic interactions.',
668
676
  inputSchema: {
669
677
  nodeIds: z.array(z.string()).optional().describe('Optional list of Figma node ids. If omitted, uses the current selection'),
670
678
  includeOverflow: z.boolean().default(false).describe('Ignore Figma clipping when generating layout'),
@@ -673,6 +681,29 @@ server.registerTool('write_selection_to_xcode', {
673
681
  },
674
682
  }, async ({ nodeIds, includeOverflow, generationMode, projectPath }) => {
675
683
  const targetDir = resolveTargetProjectPath(projectPath);
684
+
685
+ // Try hosted analysis first to get refinement hints alongside code
686
+ let analysisHints = null;
687
+ try {
688
+ const analysis = await tryHostedSemanticGeneration({
689
+ nodeIds,
690
+ generationMode,
691
+ includeOverflow,
692
+ analyze: true,
693
+ });
694
+ if (analysis) {
695
+ analysisHints = {
696
+ generationHints: analysis.generationHints,
697
+ manualRefinementHints: analysis.manualRefinementHints,
698
+ reusableComponents: analysis.reusableComponents,
699
+ assetExportPlan: analysis.assetExportPlan,
700
+ };
701
+ }
702
+ } catch (err) {
703
+ // Non-fatal — continue with generation even if analysis fails
704
+ console.error('[figma-swiftui-mcp] Hosted analysis failed (non-fatal):', err?.message ?? err);
705
+ }
706
+
676
707
  const generated = await callBridge('generate_swiftui', {
677
708
  nodeIds,
678
709
  includeOverflow,
@@ -696,14 +727,83 @@ server.registerTool('write_selection_to_xcode', {
696
727
  throw new Error(result.results.errors.join(' | ') || 'Failed to write generated selection to Xcode');
697
728
  }
698
729
 
730
+ // Build refinement instructions from diagnostics and hints
731
+ const rasterNodes = (generated.diagnostics ?? []).filter(
732
+ (d) => d.rasterized || d.reason?.toLowerCase().includes('raster')
733
+ );
734
+ const refinementInstructions = buildRefinementInstructions({
735
+ diagnostics: generated.diagnostics ?? [],
736
+ rasterNodes,
737
+ analysisHints,
738
+ structName: effectiveStructName,
739
+ filePath: result.results?.swiftFile ?? null,
740
+ });
741
+
699
742
  return jsonResult({
700
743
  ...result,
701
744
  structName: effectiveStructName,
702
745
  selection: generated.selection ?? null,
703
746
  diagnostics: generated.diagnostics ?? [],
747
+ refinementInstructions,
748
+ ...(analysisHints ? { analysisHints } : {}),
704
749
  });
705
750
  });
706
751
 
752
+ function buildRefinementInstructions({ diagnostics, rasterNodes, analysisHints, structName, filePath }) {
753
+ const instructions = [];
754
+
755
+ // Adaptive layout
756
+ instructions.push(
757
+ `Make ${structName} adaptive: replace any fixed .frame(width:height:) on the root body with GeometryReader or .frame(maxWidth: .infinity, maxHeight: .infinity) so the screen fills any device.`
758
+ );
759
+
760
+ // Raster replacements
761
+ if (rasterNodes.length > 0) {
762
+ instructions.push(
763
+ `${rasterNodes.length} element(s) were exported as raster images because Figma could not map them to native SwiftUI. Review these and replace obvious UI controls (buttons, text fields, toggles, tab bars) with native SwiftUI equivalents. Keep actual images/illustrations as Image assets.`
764
+ );
765
+ }
766
+
767
+ // Hosted analysis hints
768
+ if (analysisHints?.manualRefinementHints?.length) {
769
+ for (const hint of analysisHints.manualRefinementHints) {
770
+ const text = typeof hint === 'string' ? hint : hint.description || hint.message || hint.text || hint.title || null;
771
+ if (text) {
772
+ instructions.push(text);
773
+ }
774
+ }
775
+ }
776
+
777
+ if (analysisHints?.generationHints?.interactiveElements?.length) {
778
+ const names = analysisHints.generationHints.interactiveElements
779
+ .map((e) => e.name || e.nodeId || e.id || '(unnamed)')
780
+ .filter(Boolean)
781
+ .join(', ');
782
+ if (names) {
783
+ instructions.push(`These elements likely need interaction: ${names}`);
784
+ }
785
+ }
786
+
787
+ // Reusable components
788
+ if (analysisHints?.reusableComponents?.length) {
789
+ const names = analysisHints.reusableComponents
790
+ .map((c) => c.name || c.componentName || c.id || '(unnamed)')
791
+ .filter(Boolean)
792
+ .join(', ');
793
+ if (names) {
794
+ instructions.push(
795
+ `Consider extracting ${analysisHints.reusableComponents.length} reusable component(s): ${names}`
796
+ );
797
+ }
798
+ }
799
+
800
+ return {
801
+ summary: `After writing ${structName}.swift, apply these refinements to make it production-ready:`,
802
+ steps: instructions,
803
+ filePath,
804
+ };
805
+ }
806
+
707
807
  async function shutdownBridgeAndExit(message, exitCode = 1) {
708
808
  console.error(message);
709
809
  if (authValidationTimer) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@createlex/figma-swiftui-mcp",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "description": "CreateLex MCP runtime for Figma-to-SwiftUI generation and Xcode export",
5
5
  "bin": {
6
6
  "figma-swiftui-mcp": "bin/figma-swiftui-mcp.js"