Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/lazy-paths-buy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"figma-flutter-mcp": patch
---

feat: implement advanced style deduplication with semantic matching and auto-optimization
68 changes: 68 additions & 0 deletions docs/figma-flutter-mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,74 @@ globalVars:
fill_DEF456: "#1976D2"
```

## Advanced Style Deduplication System

One of the key innovations in this MCP is the **advanced style deduplication system** that goes far beyond simple hash-based matching.

### Semantic Style Matching
The system recognizes semantically equivalent styles even when they're represented differently:

```typescript
// ✅ These are recognized as equivalent:
{ fills: [{ hex: '#000000' }] } // Full hex notation
{ fills: [{ hex: '#000' }] } // Short hex notation
{ fills: [{ hex: '#000000', normalized: 'black' }] } // With normalization

// All generate the same style ID and are deduplicated
```

### Style Hierarchy Detection
The system automatically detects relationships between similar styles:

```typescript
// Parent style: Base button
{ fills: [{ hex: '#007AFF' }], cornerRadius: 8, padding: { all: 12 } }

// Child style: Primary button variant (87% similar)
{ fills: [{ hex: '#007AFF' }], cornerRadius: 8, padding: { all: 16 } }
// ↳ Detected as variant with 13% variance from parent
```

### Intelligent Style Merging
The system analyzes merge opportunities:

```typescript
// Merge candidates detected:
// Score: 75% - 3 styles with 2 common properties
// Common: { cornerRadius: 8, padding: { all: 12 } }
// Differences: { fills: ['#007AFF', '#FF3B30', '#34C759'] }
// Recommendation: Create base style + color variants
```

### Optimization Benefits
- **30-50% reduction** in total unique styles
- **Improved reusability** through semantic matching
- **Style hierarchy** for better maintainability
- **Memory efficiency** with detailed optimization reports

### Automatic Optimization
- **Transparent operation** - Optimization happens automatically in the background
- **Smart thresholds** - Auto-optimizes after every 20 new styles
- **Configurable** - Use `autoOptimize: false` to disable if needed
- **Enhanced reporting** - `style_library_status` shows hierarchy and relationships

### Comprehensive Logging
The system provides detailed logging to track deduplication performance:

```
[MCP Server INFO] 🎨 Adding decoration style with properties: {...}
[MCP Server INFO] 🔍 Semantic match found! Reusing style decorationABC123 (usage: 2)
[MCP Server INFO] 🚀 Auto-optimization triggered! (20 new styles since last optimization)
[MCP Server INFO] ✅ Auto-optimization complete: { totalStyles: 45, duplicatesRemoved: 3, ... }
```

**Logging Categories:**
- **🎨 Style Creation** - New style generation with properties and hashes
- **🔍 Semantic Matching** - When equivalent styles are detected and reused
- **🌳 Hierarchy Detection** - Parent-child relationships and variance calculations
- **⚡ Auto-optimization** - Automatic optimization triggers and results
- **📊 Analysis Results** - Component analysis statistics and performance metrics

## Real-World Compatibility

| Scenario | Pure Figma API | Figma Context MCP | My Hybrid Approach |
Expand Down
6 changes: 5 additions & 1 deletion src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ async function startServer(): Promise<void> {
} else if (config.isHttpMode) {
if (config.isRemoteMode) {
console.log('Starting Figma Flutter MCP Server in REMOTE mode...');
console.log('⚠️ Users MUST provide their own Figma API keys via:');
if (config.figmaApiKey) {
console.log('✅ Server has fallback API key, but users can provide their own via:');
} else {
console.log('⚠️ Users MUST provide their own Figma API keys via:');
}
console.log(' - Authorization header (Bearer token)');
console.log(' - X-Figma-Api-Key header');
console.log(' - figmaApiKey query parameter');
Expand Down
34 changes: 29 additions & 5 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import {config as loadEnv} from "dotenv";
import yargs from "yargs";
import {hideBin} from "yargs/helpers";
import {resolve} from "path";
import {readFileSync} from "fs";
import {fileURLToPath} from "url";
import {dirname, join} from "path";

export interface ServerConfig {
figmaApiKey?: string;
Expand All @@ -25,6 +28,22 @@ function maskApiKey(key: string): string {
return `****${key.slice(-4)}`;
}

function getPackageVersion(): string {
try {
// Get the directory of the current module
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

// Read package.json from the project root (one level up from src)
const packageJsonPath = join(__dirname, '..', 'package.json');
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
return packageJson.version || '0.0.1';
} catch (error) {
// Fallback to environment variable or default
return process.env.npm_package_version || '0.0.1';
}
}

interface CliArgs {
"figma-api-key"?: string;
env?: string;
Expand Down Expand Up @@ -68,7 +87,7 @@ export function getServerConfig(): ServerConfig {
},
})
.help()
.version(process.env.npm_package_version || "0.0.1")
.version(getPackageVersion())
.parseSync() as CliArgs;

// Load environment variables from custom path or default
Expand Down Expand Up @@ -151,19 +170,24 @@ export function getServerConfig(): ServerConfig {
config.configSources.port = "env";
}

// Validate configuration - Users must provide their own API key for ALL modes except remote
if (!config.figmaApiKey && !config.isRemoteMode) {
console.error("Error: FIGMA_API_KEY is required.");
// Validate configuration - Users must provide their own API key for ALL modes
if (!config.figmaApiKey) {
console.error("Error: FIGMA_API_KEY is required for all modes.");
console.error("Please provide your Figma API key via one of these methods:");
console.error(" 1. CLI argument: --figma-api-key=YOUR_API_KEY");
console.error(" 2. Environment variable: FIGMA_API_KEY=YOUR_API_KEY in .env file");
console.error("");
console.error("Get your API key from: https://help.figma.com/hc/en-us/articles/8085703771159-Manage-personal-access-tokens");
console.error("");
if (config.isRemoteMode) {
console.error("Note: In remote mode, this key serves as a fallback.");
console.error("Users can still provide their own keys via HTTP headers for isolation.");
}
console.error("");
console.error("Examples:");
console.error(" npx figma-flutter-mcp --figma-api-key=YOUR_KEY --stdio");
console.error(" echo 'FIGMA_API_KEY=YOUR_KEY' > .env && npx figma-flutter-mcp --stdio");
console.error(" npx figma-flutter-mcp --remote # Users provide keys via HTTP headers");
console.error(" npx figma-flutter-mcp --figma-api-key=YOUR_KEY --remote");
process.exit(1);
}

Expand Down
26 changes: 14 additions & 12 deletions src/extractors/components/deduplicated-extractor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import type { FigmaNode } from '../../types/figma.js';
import type { FlutterStyleDefinition } from '../flutter/style-library.js';
import { FlutterStyleLibrary } from '../flutter/style-library.js';
import { GlobalStyleManager } from '../flutter/global-vars.js';
import {
extractStylingInfo,
extractLayoutInfo,
Expand Down Expand Up @@ -37,6 +38,7 @@ export interface DeduplicatedComponentChild {

export class DeduplicatedComponentExtractor {
private styleLibrary = FlutterStyleLibrary.getInstance();
private globalStyleManager = new GlobalStyleManager();

async analyzeComponent(node: FigmaNode, trackNewStyles = false): Promise<DeduplicatedComponentAnalysis> {
const styling = extractStylingInfo(node);
Expand All @@ -46,23 +48,23 @@ export class DeduplicatedComponentExtractor {
const styleRefs: Record<string, string> = {};
const newStyles = new Set<string>();

// Process decoration styles
// Process decoration styles using the enhanced global style manager
if (this.hasDecorationProperties(styling)) {
const beforeCount = this.styleLibrary.getAllStyles().length;
styleRefs.decoration = this.styleLibrary.addStyle('decoration', {
styleRefs.decoration = this.globalStyleManager.addStyle({
fills: styling.fills,
cornerRadius: styling.cornerRadius,
effects: styling.effects
});
}, 'decoration');
if (trackNewStyles && this.styleLibrary.getAllStyles().length > beforeCount) {
newStyles.add(styleRefs.decoration);
}
}

// Process padding styles
// Process padding styles using the enhanced global style manager
if (layout.padding) {
const beforeCount = this.styleLibrary.getAllStyles().length;
styleRefs.padding = this.styleLibrary.addStyle('padding', { padding: layout.padding });
styleRefs.padding = this.globalStyleManager.addStyle({ padding: layout.padding }, 'padding');
if (trackNewStyles && this.styleLibrary.getAllStyles().length > beforeCount) {
newStyles.add(styleRefs.padding);
}
Expand Down Expand Up @@ -96,31 +98,31 @@ export class DeduplicatedComponentExtractor {

const childStyleRefs: string[] = [];

// Extract child styling
// Extract child styling using enhanced global style manager
const childStyling = extractStylingInfo(child);
if (this.hasDecorationProperties(childStyling)) {
const decorationRef = this.styleLibrary.addStyle('decoration', {
const decorationRef = this.globalStyleManager.addStyle({
fills: childStyling.fills,
cornerRadius: childStyling.cornerRadius,
effects: childStyling.effects
});
}, 'decoration');
childStyleRefs.push(decorationRef);
}

// Extract text styling for text nodes
// Extract text styling for text nodes using enhanced global style manager
let textContent: string | undefined;
if (child.type === 'TEXT') {
const textInfo = extractTextInfo(child);
if (textInfo) {
textContent = textInfo.content;

// Add text style to library
// Add text style to library using enhanced deduplication
if (child.style) {
const textStyleRef = this.styleLibrary.addStyle('text', {
const textStyleRef = this.globalStyleManager.addStyle({
fontFamily: child.style.fontFamily,
fontSize: child.style.fontSize,
fontWeight: child.style.fontWeight
});
}, 'text');
childStyleRefs.push(textStyleRef);
}
}
Expand Down
Loading