@createlex/figma-swiftui-mcp 1.1.0 → 1.2.2
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.
- package/companion/bridge-server.cjs +58 -5
- package/companion/mcp-server.mjs +32 -3
- package/package.json +2 -3
|
@@ -230,6 +230,55 @@ function startBridgeServer(options = {}) {
|
|
|
230
230
|
});
|
|
231
231
|
});
|
|
232
232
|
|
|
233
|
+
// GET /design-context — returns current selection's design context as JSON.
|
|
234
|
+
// Lets any AI tool (Windsurf, a script, a browser) fetch the live Figma context
|
|
235
|
+
// via a simple HTTP GET without needing MCP protocol or copy-paste.
|
|
236
|
+
// Usage: GET http://localhost:7765/design-context?maxDepth=4
|
|
237
|
+
app.get('/design-context', async (req, res) => {
|
|
238
|
+
if (!pluginBridgeClient || pluginBridgeClient.readyState !== WebSocket.OPEN) {
|
|
239
|
+
return res.status(503).json({
|
|
240
|
+
ok: false,
|
|
241
|
+
error: 'No Figma plugin is connected to the bridge. Open the plugin in Figma first.',
|
|
242
|
+
hint: 'npx @createlex/figma-swiftui-mcp start --project ./MyApp',
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const maxDepth = Math.min(parseInt(req.query.maxDepth, 10) || 4, 8);
|
|
247
|
+
const requestId = `http-ctx-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
248
|
+
|
|
249
|
+
const response = await new Promise((resolve) => {
|
|
250
|
+
const timeout = setTimeout(() => {
|
|
251
|
+
pendingBridgeRequests.delete(requestId);
|
|
252
|
+
resolve({ ok: false, error: 'Timed out waiting for design context from plugin' });
|
|
253
|
+
}, 20000);
|
|
254
|
+
|
|
255
|
+
pendingBridgeRequests.set(requestId, {
|
|
256
|
+
origin: null,
|
|
257
|
+
action: 'get_design_context',
|
|
258
|
+
resolveCallback: (msg) => {
|
|
259
|
+
clearTimeout(timeout);
|
|
260
|
+
resolve(msg);
|
|
261
|
+
},
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
sendBridgeMessage(pluginBridgeClient, {
|
|
265
|
+
type: 'bridge-request',
|
|
266
|
+
requestId,
|
|
267
|
+
action: 'get_design_context',
|
|
268
|
+
params: { maxDepth, includeScreenshot: false },
|
|
269
|
+
protocolVersion: BRIDGE_PROTOCOL_VERSION,
|
|
270
|
+
timestamp: new Date().toISOString(),
|
|
271
|
+
});
|
|
272
|
+
broadcastBridgeStatus();
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
if (!response.ok) {
|
|
276
|
+
return res.status(502).json({ ok: false, error: response.error || 'Plugin returned an error' });
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
res.json({ ok: true, ...(response.data || {}) });
|
|
280
|
+
});
|
|
281
|
+
|
|
233
282
|
function sendBridgeMessage(ws, payload) {
|
|
234
283
|
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
235
284
|
return false;
|
|
@@ -497,11 +546,15 @@ function startBridgeServer(options = {}) {
|
|
|
497
546
|
}
|
|
498
547
|
|
|
499
548
|
pendingBridgeRequests.delete(message.requestId);
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
549
|
+
if (typeof pending.resolveCallback === 'function') {
|
|
550
|
+
pending.resolveCallback(message);
|
|
551
|
+
} else {
|
|
552
|
+
sendBridgeMessage(pending.origin, {
|
|
553
|
+
...message,
|
|
554
|
+
protocolVersion: BRIDGE_PROTOCOL_VERSION,
|
|
555
|
+
timestamp: new Date().toISOString(),
|
|
556
|
+
});
|
|
557
|
+
}
|
|
505
558
|
broadcastBridgeStatus();
|
|
506
559
|
return;
|
|
507
560
|
}
|
package/companion/mcp-server.mjs
CHANGED
|
@@ -706,7 +706,7 @@ server.registerTool('analyze_generation', {
|
|
|
706
706
|
});
|
|
707
707
|
|
|
708
708
|
server.registerTool('write_generated_swiftui_to_xcode', {
|
|
709
|
-
description: 'Write generated SwiftUI code
|
|
709
|
+
description: 'Write generated SwiftUI code into the configured Xcode project. Images are automatically exported from the current Figma selection and written to Assets.xcassets — you do not need to provide them manually. Supports multiple files: use additionalFiles for DesignTokens.swift (dir: "shared"), reusable components (dir: "components"), or extra screen files (dir: "screens"). AI tools generating code with their own model should call this after generating SwiftUI from get_design_context or the /design-context URL.',
|
|
710
710
|
inputSchema: {
|
|
711
711
|
code: z.string().describe('SwiftUI source code for the primary view'),
|
|
712
712
|
structName: z.string().optional().describe('Optional Swift struct name; if omitted it will be inferred from the code'),
|
|
@@ -725,11 +725,31 @@ server.registerTool('write_generated_swiftui_to_xcode', {
|
|
|
725
725
|
}, async ({ code, structName, projectPath, images, selectionNames, additionalFiles }) => {
|
|
726
726
|
const targetDir = resolveTargetProjectPath(projectPath);
|
|
727
727
|
const effectiveStructName = inferStructName({ structName, code, selectionNames });
|
|
728
|
+
|
|
729
|
+
// Auto-fetch images from the current Figma selection when none were provided.
|
|
730
|
+
// This ensures Assets.xcassets is always populated even when the AI supplied
|
|
731
|
+
// its own SwiftUI code (e.g. from get_design_context / the /design-context URL).
|
|
732
|
+
let effectiveImages = Array.isArray(images) && images.length > 0 ? images : [];
|
|
733
|
+
if (effectiveImages.length === 0) {
|
|
734
|
+
try {
|
|
735
|
+
const generated = await callBridge('generate_swiftui', {
|
|
736
|
+
includeImages: true,
|
|
737
|
+
generationMode: 'editable',
|
|
738
|
+
includeOverflow: false,
|
|
739
|
+
});
|
|
740
|
+
if (Array.isArray(generated.images) && generated.images.length > 0) {
|
|
741
|
+
effectiveImages = generated.images;
|
|
742
|
+
}
|
|
743
|
+
} catch {
|
|
744
|
+
// Non-fatal: bridge may not be connected or selection may have no images.
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
728
748
|
const result = writeSwiftUIScreen({
|
|
729
749
|
targetDir,
|
|
730
750
|
code,
|
|
731
751
|
structName: effectiveStructName,
|
|
732
|
-
images,
|
|
752
|
+
images: effectiveImages,
|
|
733
753
|
additionalFiles,
|
|
734
754
|
});
|
|
735
755
|
|
|
@@ -740,6 +760,7 @@ server.registerTool('write_generated_swiftui_to_xcode', {
|
|
|
740
760
|
return jsonResult({
|
|
741
761
|
...stripImageData(result),
|
|
742
762
|
structName: effectiveStructName,
|
|
763
|
+
imagesWritten: effectiveImages.length,
|
|
743
764
|
});
|
|
744
765
|
});
|
|
745
766
|
|
|
@@ -940,8 +961,16 @@ function buildRefinementInstructions({ diagnostics, rasterNodes, analysisHints,
|
|
|
940
961
|
|
|
941
962
|
// Raster replacements
|
|
942
963
|
if (rasterNodes.length > 0) {
|
|
964
|
+
const rasterNames = rasterNodes
|
|
965
|
+
.map((d) => d.assetName || d.nodeName || d.name)
|
|
966
|
+
.filter(Boolean)
|
|
967
|
+
.slice(0, 8)
|
|
968
|
+
.join(', ');
|
|
943
969
|
instructions.push(
|
|
944
|
-
|
|
970
|
+
`IMPORTANT: ${rasterNodes.length} element(s) were rasterized and written to Assets.xcassets as PNG files${rasterNames ? ` (${rasterNames})` : ''}. ` +
|
|
971
|
+
`The generated code already references them with Image("name") calls — DO NOT remove or replace these Image() calls. ` +
|
|
972
|
+
`Only replace Image() calls if the asset name clearly identifies a native UI control (button, toggle, tab bar, text field). ` +
|
|
973
|
+
`Photos, illustrations, 3D renders, icons, and decorative graphics MUST stay as Image("name") — do not attempt to reconstruct them with SwiftUI shapes or colors.`
|
|
945
974
|
);
|
|
946
975
|
}
|
|
947
976
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@createlex/figma-swiftui-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.2",
|
|
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"
|
|
@@ -41,8 +41,7 @@
|
|
|
41
41
|
"zod": "^4.3.6"
|
|
42
42
|
},
|
|
43
43
|
"optionalDependencies": {
|
|
44
|
-
"@anthropic-ai/sdk": "^0.52.0"
|
|
45
|
-
"openai": "^4.100.0"
|
|
44
|
+
"@anthropic-ai/sdk": "^0.52.0"
|
|
46
45
|
},
|
|
47
46
|
"devDependencies": {
|
|
48
47
|
"@figma/plugin-typings": "*",
|