@createlex/figma-swiftui-mcp 1.0.7 → 1.0.9

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.
@@ -30,6 +30,8 @@ const BRIDGE_HTTP_URL = process.env.FIGMA_SWIFTUI_BRIDGE_HTTP_URL || 'http://loc
30
30
  const BRIDGE_WS_URL = process.env.FIGMA_SWIFTUI_BRIDGE_WS_URL || 'ws://localhost:7765/bridge';
31
31
  const REQUEST_TIMEOUT_MS = Number(process.env.FIGMA_SWIFTUI_BRIDGE_TIMEOUT_MS || 30000);
32
32
  const AUTH_REVALIDATION_INTERVAL_MS = Number(process.env.FIGMA_SWIFTUI_AUTH_REVALIDATION_MS || (10 * 60 * 1000));
33
+ const RESPONSE_SIZE_CAP = Number(process.env.FIGMA_SWIFTUI_RESPONSE_SIZE_CAP || 102400); // 100 KB default
34
+ const MAX_DIAGNOSTICS = 20;
33
35
 
34
36
  let bridgeSocket = null;
35
37
  let connectPromise = null;
@@ -666,13 +668,13 @@ server.registerTool('write_generated_swiftui_to_xcode', {
666
668
  }
667
669
 
668
670
  return jsonResult({
669
- ...result,
671
+ ...stripImageData(result),
670
672
  structName: effectiveStructName,
671
673
  });
672
674
  });
673
675
 
674
676
  server.registerTool('write_selection_to_xcode', {
675
- description: 'Generate SwiftUI from the connected Figma selection and write it directly into the configured Xcode project.',
677
+ 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.',
676
678
  inputSchema: {
677
679
  nodeIds: z.array(z.string()).optional().describe('Optional list of Figma node ids. If omitted, uses the current selection'),
678
680
  includeOverflow: z.boolean().default(false).describe('Ignore Figma clipping when generating layout'),
@@ -681,6 +683,29 @@ server.registerTool('write_selection_to_xcode', {
681
683
  },
682
684
  }, async ({ nodeIds, includeOverflow, generationMode, projectPath }) => {
683
685
  const targetDir = resolveTargetProjectPath(projectPath);
686
+
687
+ // Try hosted analysis first to get refinement hints alongside code
688
+ let analysisHints = null;
689
+ try {
690
+ const analysis = await tryHostedSemanticGeneration({
691
+ nodeIds,
692
+ generationMode,
693
+ includeOverflow,
694
+ analyze: true,
695
+ });
696
+ if (analysis) {
697
+ analysisHints = {
698
+ generationHints: analysis.generationHints,
699
+ manualRefinementHints: analysis.manualRefinementHints,
700
+ reusableComponents: analysis.reusableComponents,
701
+ assetExportPlan: analysis.assetExportPlan,
702
+ };
703
+ }
704
+ } catch (err) {
705
+ // Non-fatal — continue with generation even if analysis fails
706
+ console.error('[figma-swiftui-mcp] Hosted analysis failed (non-fatal):', err?.message ?? err);
707
+ }
708
+
684
709
  const generated = await callBridge('generate_swiftui', {
685
710
  nodeIds,
686
711
  includeOverflow,
@@ -704,14 +729,192 @@ server.registerTool('write_selection_to_xcode', {
704
729
  throw new Error(result.results.errors.join(' | ') || 'Failed to write generated selection to Xcode');
705
730
  }
706
731
 
707
- return jsonResult({
708
- ...result,
732
+ // Build refinement instructions from diagnostics and hints
733
+ const rasterNodes = (generated.diagnostics ?? []).filter(
734
+ (d) => d.rasterized || d.reason?.toLowerCase().includes('raster')
735
+ );
736
+ const refinementInstructions = buildRefinementInstructions({
737
+ diagnostics: generated.diagnostics ?? [],
738
+ rasterNodes,
739
+ analysisHints,
740
+ structName: effectiveStructName,
741
+ filePath: result.results?.swiftFile ?? null,
742
+ });
743
+
744
+ return jsonResult(buildCompactResponse({
745
+ result,
709
746
  structName: effectiveStructName,
710
747
  selection: generated.selection ?? null,
711
748
  diagnostics: generated.diagnostics ?? [],
712
- });
749
+ refinementInstructions,
750
+ analysisHints,
751
+ }));
713
752
  });
714
753
 
754
+ // ---------------------------------------------------------------------------
755
+ // Response-size mitigation helpers
756
+ // ---------------------------------------------------------------------------
757
+
758
+ /**
759
+ * Remove base64 / svg binary data from any image results.
760
+ * Files are already on disk — the model only needs names and paths.
761
+ */
762
+ function stripImageData(obj) {
763
+ if (!obj || typeof obj !== 'object') return obj;
764
+
765
+ // Deep-clone to avoid mutating the original
766
+ const clone = JSON.parse(JSON.stringify(obj));
767
+
768
+ const walk = (node) => {
769
+ if (Array.isArray(node)) {
770
+ node.forEach(walk);
771
+ return;
772
+ }
773
+ if (node && typeof node === 'object') {
774
+ delete node.base64;
775
+ delete node.svg;
776
+ delete node.data;
777
+ for (const value of Object.values(node)) {
778
+ walk(value);
779
+ }
780
+ }
781
+ };
782
+
783
+ walk(clone);
784
+ return clone;
785
+ }
786
+
787
+ /**
788
+ * Cap the diagnostics array, prioritising rasterized nodes first.
789
+ */
790
+ function capDiagnostics(diagnostics) {
791
+ if (!Array.isArray(diagnostics) || diagnostics.length <= MAX_DIAGNOSTICS) {
792
+ return diagnostics;
793
+ }
794
+
795
+ // Sort: rasterized nodes first (most useful for refinement)
796
+ const sorted = [...diagnostics].sort((a, b) => {
797
+ const aRaster = a.rasterized || a.reason?.toLowerCase().includes('raster') ? 1 : 0;
798
+ const bRaster = b.rasterized || b.reason?.toLowerCase().includes('raster') ? 1 : 0;
799
+ return bRaster - aRaster;
800
+ });
801
+
802
+ const kept = sorted.slice(0, MAX_DIAGNOSTICS);
803
+ kept.push({
804
+ _truncated: true,
805
+ totalCount: diagnostics.length,
806
+ shownCount: MAX_DIAGNOSTICS,
807
+ message: `${diagnostics.length - MAX_DIAGNOSTICS} additional diagnostic(s) omitted to reduce response size.`,
808
+ });
809
+ return kept;
810
+ }
811
+
812
+ /**
813
+ * Build a compact MCP response for write_selection_to_xcode.
814
+ *
815
+ * Strategy:
816
+ * 1. Strip all binary (base64/svg) data from image results.
817
+ * 2. Cap diagnostics to MAX_DIAGNOSTICS most relevant entries.
818
+ * 3. Estimate serialised size; if still over RESPONSE_SIZE_CAP,
819
+ * compress analysis hints to summary-only.
820
+ */
821
+ function buildCompactResponse({ result, structName, selection, diagnostics, refinementInstructions, analysisHints }) {
822
+ const compact = {
823
+ ...stripImageData(result),
824
+ structName,
825
+ selection,
826
+ diagnostics: capDiagnostics(diagnostics),
827
+ refinementInstructions,
828
+ ...(analysisHints ? { analysisHints } : {}),
829
+ };
830
+
831
+ // First size check
832
+ let serialised = JSON.stringify(compact);
833
+ if (serialised.length <= RESPONSE_SIZE_CAP) {
834
+ return compact;
835
+ }
836
+
837
+ // Compress: remove verbose analysis hints, keep only summary-level data
838
+ if (compact.analysisHints) {
839
+ const { generationHints, manualRefinementHints, ...rest } = compact.analysisHints;
840
+ compact.analysisHints = {
841
+ ...rest,
842
+ _compressed: true,
843
+ generationHintCount: generationHints?.interactiveElements?.length ?? 0,
844
+ manualRefinementHintCount: manualRefinementHints?.length ?? 0,
845
+ };
846
+ }
847
+
848
+ // Second size check — drop the full Swift code echo if still too large
849
+ serialised = JSON.stringify(compact);
850
+ if (serialised.length > RESPONSE_SIZE_CAP && compact.results?.swiftFile) {
851
+ // The code is already on disk at swiftFile; no need to echo it
852
+ if (compact.results) {
853
+ compact.results = { ...compact.results };
854
+ delete compact.results.code;
855
+ }
856
+ compact._responseCapped = true;
857
+ compact._responseNote = `Response was compressed to stay under ${Math.round(RESPONSE_SIZE_CAP / 1024)}KB. All files were written successfully to disk.`;
858
+ }
859
+
860
+ return compact;
861
+ }
862
+
863
+ function buildRefinementInstructions({ diagnostics, rasterNodes, analysisHints, structName, filePath }) {
864
+ const instructions = [];
865
+
866
+ // Adaptive layout
867
+ instructions.push(
868
+ `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.`
869
+ );
870
+
871
+ // Raster replacements
872
+ if (rasterNodes.length > 0) {
873
+ instructions.push(
874
+ `${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.`
875
+ );
876
+ }
877
+
878
+ // Hosted analysis hints
879
+ if (analysisHints?.manualRefinementHints?.length) {
880
+ for (const hint of analysisHints.manualRefinementHints) {
881
+ const text = typeof hint === 'string' ? hint : hint.description || hint.message || hint.text || hint.title || null;
882
+ if (text) {
883
+ instructions.push(text);
884
+ }
885
+ }
886
+ }
887
+
888
+ if (analysisHints?.generationHints?.interactiveElements?.length) {
889
+ const names = analysisHints.generationHints.interactiveElements
890
+ .map((e) => e.name || e.nodeId || e.id || '(unnamed)')
891
+ .filter(Boolean)
892
+ .join(', ');
893
+ if (names) {
894
+ instructions.push(`These elements likely need interaction: ${names}`);
895
+ }
896
+ }
897
+
898
+ // Reusable components
899
+ if (analysisHints?.reusableComponents?.length) {
900
+ const names = analysisHints.reusableComponents
901
+ .map((c) => c.name || c.componentName || c.id || '(unnamed)')
902
+ .filter(Boolean)
903
+ .join(', ');
904
+ if (names) {
905
+ instructions.push(
906
+ `Consider extracting ${analysisHints.reusableComponents.length} reusable component(s): ${names}`
907
+ );
908
+ }
909
+ }
910
+
911
+ return {
912
+ summary: `After writing ${structName}.swift, apply these refinements to make it production-ready:`,
913
+ steps: instructions,
914
+ filePath,
915
+ };
916
+ }
917
+
715
918
  async function shutdownBridgeAndExit(message, exitCode = 1) {
716
919
  console.error(message);
717
920
  if (authValidationTimer) {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "figma-swiftui-companion",
3
- "version": "1.0.0",
3
+ "version": "1.0.9",
4
4
  "description": "Local server that writes Figma-generated SwiftUI code and images into an Xcode project",
5
5
  "main": "server.js",
6
6
  "scripts": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@createlex/figma-swiftui-mcp",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
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"