@createlex/figma-swiftui-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,17 @@
1
+ {
2
+ "name": "figma-swiftui-companion",
3
+ "version": "1.0.0",
4
+ "description": "Local server that writes Figma-generated SwiftUI code and images into an Xcode project",
5
+ "main": "server.js",
6
+ "scripts": {
7
+ "login": "node login.mjs",
8
+ "start": "node mcp-server.mjs"
9
+ },
10
+ "dependencies": {
11
+ "@modelcontextprotocol/sdk": "^1.28.0",
12
+ "cors": "^2.8.5",
13
+ "express": "^4.18.0",
14
+ "ws": "^8.20.0",
15
+ "zod": "^4.3.6"
16
+ }
17
+ }
@@ -0,0 +1,64 @@
1
+ const { startBridgeServer } = require('./bridge-server.cjs');
2
+ const { authorizeRuntimeStartup, validateRuntimeSession } = require('./createlex-auth.cjs');
3
+
4
+ const AUTH_REVALIDATION_INTERVAL_MS = Number(process.env.FIGMA_SWIFTUI_AUTH_REVALIDATION_MS || (10 * 60 * 1000));
5
+
6
+ function readProjectPathArg(argv) {
7
+ const argIdx = argv.indexOf('--project');
8
+ if (argIdx !== -1 && argv[argIdx + 1]) {
9
+ return argv[argIdx + 1];
10
+ }
11
+ return null;
12
+ }
13
+
14
+ async function main() {
15
+ let authState = await authorizeRuntimeStartup();
16
+ console.log(
17
+ authState.bypass
18
+ ? '[figma-swiftui-bridge] Authorization bypass enabled'
19
+ : `[figma-swiftui-bridge] Authorized CreateLex user ${authState.email || authState.userId || 'unknown-user'}`
20
+ );
21
+
22
+ const projectPath = readProjectPathArg(process.argv);
23
+ const bridgeRuntime = await startBridgeServer({
24
+ projectPath,
25
+ logger: console,
26
+ });
27
+
28
+ const authValidationTimer = setInterval(async () => {
29
+ try {
30
+ const validation = await validateRuntimeSession(authState);
31
+ if (!validation.valid) {
32
+ console.error(`[figma-swiftui-bridge] Authorization lost: ${validation.error}`);
33
+ if (bridgeRuntime && typeof bridgeRuntime.close === 'function' && !bridgeRuntime.alreadyRunning) {
34
+ await bridgeRuntime.close().catch((error) => {
35
+ console.error('[figma-swiftui-bridge] Failed to close bridge cleanly:', error.message);
36
+ });
37
+ }
38
+ process.exit(1);
39
+ return;
40
+ }
41
+
42
+ authState = validation.session;
43
+ if (validation.refreshed) {
44
+ console.log('[figma-swiftui-bridge] Refreshed CreateLex MCP authorization');
45
+ }
46
+ } catch (error) {
47
+ console.error(`[figma-swiftui-bridge] Authorization revalidation failed: ${error instanceof Error ? error.message : 'unknown_error'}`);
48
+ if (bridgeRuntime && typeof bridgeRuntime.close === 'function' && !bridgeRuntime.alreadyRunning) {
49
+ await bridgeRuntime.close().catch((closeError) => {
50
+ console.error('[figma-swiftui-bridge] Failed to close bridge cleanly:', closeError.message);
51
+ });
52
+ }
53
+ process.exit(1);
54
+ }
55
+ }, AUTH_REVALIDATION_INTERVAL_MS);
56
+
57
+ authValidationTimer.unref?.();
58
+ console.log('\nReady to receive from Figma plugin.\n');
59
+ }
60
+
61
+ main().catch((error) => {
62
+ console.error('[figma-swiftui-bridge] Server error:', error);
63
+ process.exit(1);
64
+ });
@@ -0,0 +1,429 @@
1
+ const crypto = require('crypto');
2
+ const fs = require('fs');
3
+ const os = require('os');
4
+ const path = require('path');
5
+
6
+ const GENERATED_ROOT_DIRNAME = 'FigmaGenerated';
7
+ const GENERATED_LAYOUT_VERSION = 1;
8
+
9
+ function getConfigDirectory() {
10
+ if (process.platform === 'darwin') {
11
+ return path.join(os.homedir(), 'Library', 'Application Support', 'FigmaSwiftUICompanion');
12
+ }
13
+ return path.join(os.homedir(), '.config', 'FigmaSwiftUICompanion');
14
+ }
15
+
16
+ function getConfigPath() {
17
+ return path.join(getConfigDirectory(), 'config.json');
18
+ }
19
+
20
+ function readConfig() {
21
+ const configPath = getConfigPath();
22
+ if (!fs.existsSync(configPath)) {
23
+ return {};
24
+ }
25
+
26
+ try {
27
+ const parsed = JSON.parse(fs.readFileSync(configPath, 'utf8'));
28
+ return parsed && typeof parsed === 'object' ? parsed : {};
29
+ } catch {
30
+ return {};
31
+ }
32
+ }
33
+
34
+ function writeConfig(config) {
35
+ const configPath = getConfigPath();
36
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
37
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
38
+ }
39
+
40
+ function getSavedProjectPath() {
41
+ const config = readConfig();
42
+ return typeof config.projectPath === 'string' && config.projectPath.length > 0
43
+ ? config.projectPath
44
+ : null;
45
+ }
46
+
47
+ function setSavedProjectPath(projectPath) {
48
+ const resolved = resolveWritableProjectPath(projectPath);
49
+ writeConfig({
50
+ ...readConfig(),
51
+ projectPath: resolved,
52
+ updatedAt: new Date().toISOString(),
53
+ });
54
+ return resolved;
55
+ }
56
+
57
+ function clearSavedProjectPath() {
58
+ const config = readConfig();
59
+ delete config.projectPath;
60
+ config.updatedAt = new Date().toISOString();
61
+ writeConfig(config);
62
+ }
63
+
64
+ function loadProjectPath({ explicitPath } = {}) {
65
+ if (explicitPath) {
66
+ return resolveWritableProjectPath(explicitPath);
67
+ }
68
+
69
+ if (process.env.FIGMA_SWIFTUI_PROJECT_PATH) {
70
+ return resolveWritableProjectPath(process.env.FIGMA_SWIFTUI_PROJECT_PATH);
71
+ }
72
+
73
+ return getSavedProjectPath();
74
+ }
75
+
76
+ function writeSwiftUIScreen({ targetDir, code, structName, images = [] }) {
77
+ const resolvedTarget = resolveWritableProjectPath(targetDir);
78
+ const generatedDirs = ensureGeneratedLayout(resolvedTarget);
79
+ const results = {
80
+ swiftFile: null,
81
+ images: [],
82
+ manifestFiles: [],
83
+ generatedRoot: generatedDirs.rootDir,
84
+ errors: [],
85
+ };
86
+
87
+ if (code && structName) {
88
+ const swiftPath = path.join(generatedDirs.screensDir, `${structName}.swift`);
89
+ try {
90
+ fs.writeFileSync(swiftPath, code, 'utf8');
91
+ results.swiftFile = swiftPath;
92
+ } catch (error) {
93
+ results.errors.push(`Failed to write ${structName}.swift: ${error.message}`);
94
+ }
95
+ }
96
+
97
+ if (Array.isArray(images) && images.length > 0) {
98
+ const assetResult = writeAssetCatalogEntries({
99
+ targetDir: resolvedTarget,
100
+ assets: images.map((image) => ({
101
+ name: image.name,
102
+ format: image.format || 'png',
103
+ base64: image.base64,
104
+ svg: image.svg,
105
+ })),
106
+ });
107
+ results.images.push(...assetResult.files);
108
+ results.errors.push(...assetResult.errors);
109
+ }
110
+
111
+ if (code && structName) {
112
+ try {
113
+ const manifestFiles = writeGeneratedManifest(resolvedTarget, generatedDirs, {
114
+ structName,
115
+ code,
116
+ swiftFile: results.swiftFile,
117
+ images: Array.isArray(images) ? images : [],
118
+ });
119
+ results.manifestFiles.push(...manifestFiles);
120
+ } catch (error) {
121
+ results.errors.push(`Failed to update generated manifest: ${error.message}`);
122
+ }
123
+ }
124
+
125
+ return {
126
+ ok: results.errors.length === 0,
127
+ results,
128
+ projectPath: resolvedTarget,
129
+ generatedRoot: generatedDirs.rootDir,
130
+ layoutVersion: GENERATED_LAYOUT_VERSION,
131
+ };
132
+ }
133
+
134
+ function writeAssetCatalogEntries({ targetDir, assets = [] }) {
135
+ const resolvedTarget = resolveWritableProjectPath(targetDir);
136
+ const xcassetsDir = findOrCreateXcassets(resolvedTarget);
137
+ const files = [];
138
+ const errors = [];
139
+
140
+ for (const asset of assets) {
141
+ try {
142
+ const imagesetDir = path.join(xcassetsDir, `${asset.name}.imageset`);
143
+ fs.mkdirSync(imagesetDir, { recursive: true });
144
+
145
+ if (asset.format === 'svg') {
146
+ const svgPath = path.join(imagesetDir, `${asset.name}.svg`);
147
+ fs.writeFileSync(svgPath, asset.svg, 'utf8');
148
+ fs.writeFileSync(
149
+ path.join(imagesetDir, 'Contents.json'),
150
+ JSON.stringify({
151
+ images: [
152
+ { idiom: 'universal', filename: `${asset.name}.svg` },
153
+ ],
154
+ properties: {
155
+ 'preserves-vector-representation': true,
156
+ },
157
+ info: { version: 1, author: 'figma-swiftui-plugin' },
158
+ }, null, 2),
159
+ 'utf8'
160
+ );
161
+ files.push(svgPath);
162
+ continue;
163
+ }
164
+
165
+ const pngPath = path.join(imagesetDir, `${asset.name}.png`);
166
+ const pngBuffer = Buffer.from(asset.base64, 'base64');
167
+ fs.writeFileSync(pngPath, pngBuffer);
168
+ fs.writeFileSync(
169
+ path.join(imagesetDir, 'Contents.json'),
170
+ JSON.stringify({
171
+ images: [
172
+ { idiom: 'universal', scale: '1x' },
173
+ { idiom: 'universal', scale: '2x', filename: `${asset.name}.png` },
174
+ { idiom: 'universal', scale: '3x' },
175
+ ],
176
+ info: { version: 1, author: 'figma-swiftui-plugin' },
177
+ }, null, 2),
178
+ 'utf8'
179
+ );
180
+ files.push(pngPath);
181
+ } catch (error) {
182
+ errors.push(`Failed to write image ${asset.name}: ${error.message}`);
183
+ }
184
+ }
185
+
186
+ return {
187
+ ok: errors.length === 0,
188
+ xcassetsDir,
189
+ files,
190
+ errors,
191
+ };
192
+ }
193
+
194
+ function inferStructName({ structName, code, selectionNames = [] }) {
195
+ if (structName && typeof structName === 'string') {
196
+ return sanitizeName(structName) || 'GeneratedView';
197
+ }
198
+
199
+ if (typeof code === 'string') {
200
+ const match = code.match(/struct\s+([A-Za-z_][A-Za-z0-9_]*)\s*:\s*View\b/);
201
+ if (match?.[1]) {
202
+ return match[1];
203
+ }
204
+ }
205
+
206
+ if (selectionNames.length > 0) {
207
+ return sanitizeName(selectionNames[0]) || 'GeneratedView';
208
+ }
209
+
210
+ return 'GeneratedView';
211
+ }
212
+
213
+ function sanitizeName(name) {
214
+ return String(name)
215
+ .replace(/[^a-zA-Z0-9 ]/g, '')
216
+ .replace(/\s+(.)/g, (_, c) => c.toUpperCase())
217
+ .replace(/^\w/, c => c.toUpperCase())
218
+ .trim();
219
+ }
220
+
221
+ function findOrCreateXcassets(targetDir) {
222
+ const entries = fs.readdirSync(targetDir);
223
+ const existing = entries.find((entry) => entry.endsWith('.xcassets'));
224
+ if (existing) {
225
+ return path.join(targetDir, existing);
226
+ }
227
+
228
+ const newDir = path.join(targetDir, 'Assets.xcassets');
229
+ fs.mkdirSync(newDir, { recursive: true });
230
+ fs.writeFileSync(
231
+ path.join(newDir, 'Contents.json'),
232
+ JSON.stringify({ info: { version: 1, author: 'xcode' } }, null, 2),
233
+ 'utf8'
234
+ );
235
+ return newDir;
236
+ }
237
+
238
+ function ensureGeneratedLayout(targetDir) {
239
+ const rootDir = path.join(targetDir, GENERATED_ROOT_DIRNAME);
240
+ const screensDir = path.join(rootDir, 'Screens');
241
+ const componentsDir = path.join(rootDir, 'Components');
242
+ const manifestDir = path.join(rootDir, 'Manifest');
243
+
244
+ [rootDir, screensDir, componentsDir, manifestDir].forEach((dirPath) => {
245
+ fs.mkdirSync(dirPath, { recursive: true });
246
+ });
247
+
248
+ return { rootDir, screensDir, componentsDir, manifestDir };
249
+ }
250
+
251
+ function writeGeneratedManifest(targetDir, generatedDirs, payload) {
252
+ const { structName, code, swiftFile, images } = payload;
253
+ const generatedAt = new Date().toISOString();
254
+ const codeHash = crypto.createHash('sha256').update(code, 'utf8').digest('hex');
255
+ const screenManifestPath = path.join(generatedDirs.manifestDir, `${structName}.json`);
256
+ const indexManifestPath = path.join(generatedDirs.manifestDir, 'index.json');
257
+ const xcassetsDir = findOrCreateXcassets(targetDir);
258
+
259
+ const screenManifest = {
260
+ schemaVersion: GENERATED_LAYOUT_VERSION,
261
+ generatedAt,
262
+ structName,
263
+ codeHash,
264
+ screenFile: swiftFile ? toProjectRelativePath(targetDir, swiftFile) : null,
265
+ componentFiles: [],
266
+ assetCatalog: toProjectRelativePath(targetDir, xcassetsDir),
267
+ assets: images.map((image) => ({
268
+ name: image.name,
269
+ imageset: `Assets.xcassets/${image.name}.imageset`,
270
+ })),
271
+ };
272
+
273
+ fs.writeFileSync(screenManifestPath, JSON.stringify(screenManifest, null, 2), 'utf8');
274
+
275
+ let indexManifest = {
276
+ schemaVersion: GENERATED_LAYOUT_VERSION,
277
+ updatedAt: generatedAt,
278
+ generatedRoot: GENERATED_ROOT_DIRNAME,
279
+ projectPath: targetDir,
280
+ screens: [],
281
+ };
282
+
283
+ if (fs.existsSync(indexManifestPath)) {
284
+ try {
285
+ const parsed = JSON.parse(fs.readFileSync(indexManifestPath, 'utf8'));
286
+ if (parsed && typeof parsed === 'object' && Array.isArray(parsed.screens)) {
287
+ indexManifest = {
288
+ ...indexManifest,
289
+ ...parsed,
290
+ screens: parsed.screens,
291
+ };
292
+ }
293
+ } catch {
294
+ // Ignore invalid existing manifest; it will be replaced.
295
+ }
296
+ }
297
+
298
+ indexManifest.updatedAt = generatedAt;
299
+ indexManifest.projectPath = targetDir;
300
+ indexManifest.generatedRoot = GENERATED_ROOT_DIRNAME;
301
+ indexManifest.screens = [
302
+ ...indexManifest.screens.filter((screen) => screen.structName !== structName),
303
+ {
304
+ structName,
305
+ generatedAt,
306
+ codeHash,
307
+ screenFile: screenManifest.screenFile,
308
+ manifestFile: toProjectRelativePath(targetDir, screenManifestPath),
309
+ assetCount: screenManifest.assets.length,
310
+ assets: screenManifest.assets.map((asset) => asset.name),
311
+ },
312
+ ].sort((left, right) => left.structName.localeCompare(right.structName));
313
+
314
+ fs.writeFileSync(indexManifestPath, JSON.stringify(indexManifest, null, 2), 'utf8');
315
+ return [screenManifestPath, indexManifestPath];
316
+ }
317
+
318
+ function toProjectRelativePath(targetDir, filePath) {
319
+ return path.relative(targetDir, filePath).replace(/\\/g, '/');
320
+ }
321
+
322
+ function resolveWritableProjectPath(inputPath) {
323
+ const resolved = path.resolve(inputPath);
324
+ if (!fs.existsSync(resolved)) {
325
+ return resolved;
326
+ }
327
+
328
+ const stats = fs.statSync(resolved);
329
+ if (!stats.isDirectory()) {
330
+ const parentDir = path.dirname(resolved);
331
+ const projectBaseName = path.basename(resolved, path.extname(resolved));
332
+ const matchingChild = path.join(parentDir, projectBaseName);
333
+ if (looksLikeSourceDirectory(matchingChild)) {
334
+ return matchingChild;
335
+ }
336
+ return parentDir;
337
+ }
338
+
339
+ const localProjectNames = getLocalProjectNames(resolved);
340
+ const currentScore = scoreSourceDirectory(resolved);
341
+
342
+ const childDirs = fs.readdirSync(resolved, { withFileTypes: true })
343
+ .filter((entry) => entry.isDirectory() && !entry.name.endsWith('.xcodeproj') && !entry.name.endsWith('.xcworkspace'))
344
+ .map((entry) => path.join(resolved, entry.name));
345
+
346
+ const sourceCandidates = childDirs
347
+ .filter(looksLikeSourceDirectory)
348
+ .sort((left, right) => scoreSourceDirectory(right, localProjectNames) - scoreSourceDirectory(left, localProjectNames));
349
+
350
+ if (sourceCandidates.length > 0) {
351
+ const bestChild = sourceCandidates[0];
352
+ const bestChildScore = scoreSourceDirectory(bestChild, localProjectNames);
353
+ const childName = path.basename(bestChild);
354
+
355
+ if (
356
+ localProjectNames.has(childName) ||
357
+ bestChildScore > currentScore ||
358
+ !looksLikeSourceDirectory(resolved)
359
+ ) {
360
+ return bestChild;
361
+ }
362
+ }
363
+
364
+ if (looksLikeSourceDirectory(resolved)) {
365
+ return resolved;
366
+ }
367
+
368
+ return resolved;
369
+ }
370
+
371
+ function getLocalProjectNames(dirPath) {
372
+ const projectNames = new Set();
373
+ for (const entry of fs.readdirSync(dirPath, { withFileTypes: true })) {
374
+ if (!entry.isDirectory()) {
375
+ continue;
376
+ }
377
+ if (entry.name.endsWith('.xcodeproj') || entry.name.endsWith('.xcworkspace')) {
378
+ projectNames.add(path.basename(entry.name, path.extname(entry.name)));
379
+ }
380
+ }
381
+ return projectNames;
382
+ }
383
+
384
+ function looksLikeSourceDirectory(dirPath) {
385
+ if (!fs.existsSync(dirPath) || !fs.statSync(dirPath).isDirectory()) {
386
+ return false;
387
+ }
388
+
389
+ const entries = fs.readdirSync(dirPath, { withFileTypes: true });
390
+ return entries.some((entry) => entry.isFile() && entry.name.endsWith('.swift'))
391
+ || entries.some((entry) => entry.isDirectory() && entry.name.endsWith('.xcassets'));
392
+ }
393
+
394
+ function scoreSourceDirectory(dirPath, preferredNames = new Set()) {
395
+ const entries = fs.readdirSync(dirPath, { withFileTypes: true });
396
+ let score = 0;
397
+
398
+ for (const entry of entries) {
399
+ if (entry.isFile() && entry.name.endsWith('.swift')) {
400
+ score += 2;
401
+ if (entry.name.endsWith('App.swift')) {
402
+ score += 3;
403
+ }
404
+ }
405
+ if (entry.isDirectory() && entry.name.endsWith('.xcassets')) {
406
+ score += 4;
407
+ }
408
+ }
409
+
410
+ if (preferredNames.has(path.basename(dirPath))) {
411
+ score += 10;
412
+ }
413
+
414
+ return score;
415
+ }
416
+
417
+ module.exports = {
418
+ GENERATED_ROOT_DIRNAME,
419
+ GENERATED_LAYOUT_VERSION,
420
+ clearSavedProjectPath,
421
+ getConfigPath,
422
+ getSavedProjectPath,
423
+ inferStructName,
424
+ loadProjectPath,
425
+ resolveWritableProjectPath,
426
+ setSavedProjectPath,
427
+ writeAssetCatalogEntries,
428
+ writeSwiftUIScreen,
429
+ };
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@createlex/figma-swiftui-mcp",
3
+ "version": "1.0.0",
4
+ "description": "CreateLex MCP runtime for Figma-to-SwiftUI generation and Xcode export",
5
+ "bin": {
6
+ "figma-swiftui-mcp": "bin/figma-swiftui-mcp.js"
7
+ },
8
+ "files": [
9
+ "bin/",
10
+ "companion/bridge-server.cjs",
11
+ "companion/createlex-auth.cjs",
12
+ "companion/login.mjs",
13
+ "companion/mcp-server.mjs",
14
+ "companion/package.json",
15
+ "companion/server.js",
16
+ "companion/xcode-writer.cjs",
17
+ "README.md"
18
+ ],
19
+ "publishConfig": {
20
+ "access": "public"
21
+ },
22
+ "scripts": {
23
+ "postinstall": "npm --prefix companion install",
24
+ "build": "tsc",
25
+ "login": "node bin/figma-swiftui-mcp.js login",
26
+ "start": "node bin/figma-swiftui-mcp.js start",
27
+ "watch": "tsc --watch",
28
+ "test:bridge-smoke": "node ../tests/mcp/figma_plugin_swiftui_bridge_smoke.mjs",
29
+ "test:high-fidelity-smoke": "node ../tests/mcp/figma_plugin_swiftui_high_fidelity_smoke.mjs",
30
+ "start:companion:mcp": "node bin/figma-swiftui-mcp.js start"
31
+ },
32
+ "engines": {
33
+ "node": ">=18.0.0"
34
+ },
35
+ "devDependencies": {
36
+ "@figma/plugin-typings": "*",
37
+ "typescript": "^5.4.0"
38
+ }
39
+ }