@createlex/figma-swiftui-mcp 1.0.5 → 1.0.7
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 +80 -4
- package/companion/mcp-server.mjs +9 -1
- package/companion/setup.cjs +48 -17
- package/package.json +1 -1
|
@@ -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(`ℹ️
|
|
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
|
-
|
|
546
|
-
|
|
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
|
|
package/companion/mcp-server.mjs
CHANGED
|
@@ -237,7 +237,15 @@ function handleBridgeMessage(rawMessage) {
|
|
|
237
237
|
}
|
|
238
238
|
|
|
239
239
|
if (message.type === 'bridge-event') {
|
|
240
|
-
|
|
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
|
|
package/companion/setup.cjs
CHANGED
|
@@ -21,38 +21,68 @@ const os = require('node:os');
|
|
|
21
21
|
|
|
22
22
|
const MCP_KEY = 'figma-swiftui';
|
|
23
23
|
|
|
24
|
-
// ── Resolve the absolute path to
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
// ── Resolve the absolute path to the bin script and node ─────────────
|
|
25
|
+
// IDEs (Cursor, VS Code, etc.) do NOT source shell profiles, so
|
|
26
|
+
// `#!/usr/bin/env node` fails when node is managed by nvm/fnm/volta.
|
|
27
|
+
// Instead we record the absolute node path and the bin script separately.
|
|
28
|
+
function resolvePaths() {
|
|
29
|
+
const nodePath = process.execPath; // absolute path to node binary
|
|
30
|
+
|
|
31
|
+
// Try to find the bin JS file
|
|
27
32
|
const binScript = process.argv[1];
|
|
28
33
|
if (binScript) {
|
|
29
34
|
const resolved = fs.realpathSync(binScript);
|
|
30
35
|
const dir = path.dirname(resolved);
|
|
36
|
+
// Check for the .js bin entry point
|
|
37
|
+
const jsCandidate = path.join(dir, 'figma-swiftui-mcp.js');
|
|
38
|
+
if (fs.existsSync(jsCandidate)) {
|
|
39
|
+
return { nodePath, scriptPath: jsCandidate };
|
|
40
|
+
}
|
|
41
|
+
// Check for the wrapper (symlink without .js)
|
|
31
42
|
const candidate = path.join(dir, 'figma-swiftui-mcp');
|
|
32
43
|
if (fs.existsSync(candidate)) {
|
|
33
|
-
|
|
44
|
+
// Resolve symlinks to get the actual .js file
|
|
45
|
+
const real = fs.realpathSync(candidate);
|
|
46
|
+
return { nodePath, scriptPath: real };
|
|
34
47
|
}
|
|
35
48
|
}
|
|
36
49
|
|
|
37
|
-
// Fallback: search PATH
|
|
50
|
+
// Fallback: search PATH for the binary, then resolve to its .js source
|
|
38
51
|
const whichCmd = require('node:child_process')
|
|
39
52
|
.execSync('which figma-swiftui-mcp 2>/dev/null || true')
|
|
40
53
|
.toString()
|
|
41
54
|
.trim();
|
|
42
|
-
if (whichCmd)
|
|
55
|
+
if (whichCmd) {
|
|
56
|
+
const real = fs.realpathSync(whichCmd);
|
|
57
|
+
return { nodePath, scriptPath: real };
|
|
58
|
+
}
|
|
43
59
|
|
|
44
60
|
// Last resort: use npx invocation
|
|
45
|
-
return null;
|
|
61
|
+
return { nodePath, scriptPath: null };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ── Resolve absolute npx path for fallback ───────────────────────────
|
|
65
|
+
function resolveNpxPath() {
|
|
66
|
+
try {
|
|
67
|
+
const npxPath = require('node:child_process')
|
|
68
|
+
.execSync('which npx 2>/dev/null || true')
|
|
69
|
+
.toString()
|
|
70
|
+
.trim();
|
|
71
|
+
return npxPath || 'npx';
|
|
72
|
+
} catch {
|
|
73
|
+
return 'npx';
|
|
74
|
+
}
|
|
46
75
|
}
|
|
47
76
|
|
|
48
77
|
// ── IDE config definitions ────────────────────────────────────────────
|
|
49
|
-
function getTargets(
|
|
78
|
+
function getTargets({ nodePath, scriptPath }) {
|
|
50
79
|
const home = os.homedir();
|
|
51
80
|
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
81
|
+
// Use absolute node path + script to avoid #!/usr/bin/env node failures
|
|
82
|
+
// in IDEs that don't source shell profiles (nvm/fnm/volta).
|
|
83
|
+
const stdioEntry = scriptPath
|
|
84
|
+
? { command: nodePath, args: [scriptPath, 'start'] }
|
|
85
|
+
: { command: resolveNpxPath(), args: ['-y', '@createlex/figma-swiftui-mcp', 'start'] };
|
|
56
86
|
|
|
57
87
|
const stdioEntryWithType = { type: 'stdio', ...stdioEntry };
|
|
58
88
|
|
|
@@ -120,19 +150,20 @@ function runSetup(flags = {}) {
|
|
|
120
150
|
const force = flags.force || false;
|
|
121
151
|
const dryRun = flags.dryRun || false;
|
|
122
152
|
|
|
123
|
-
const
|
|
153
|
+
const paths = resolvePaths();
|
|
124
154
|
|
|
125
155
|
console.log();
|
|
126
156
|
console.log(' 🔧 figma-swiftui-mcp setup');
|
|
127
157
|
console.log(' ─────────────────────────────────────');
|
|
128
|
-
|
|
129
|
-
|
|
158
|
+
console.log(` Node: ${paths.nodePath}`);
|
|
159
|
+
if (paths.scriptPath) {
|
|
160
|
+
console.log(` Script: ${paths.scriptPath}`);
|
|
130
161
|
} else {
|
|
131
|
-
console.log('
|
|
162
|
+
console.log(' Script: not found — will use npx fallback');
|
|
132
163
|
}
|
|
133
164
|
console.log();
|
|
134
165
|
|
|
135
|
-
const targets = getTargets(
|
|
166
|
+
const targets = getTargets(paths);
|
|
136
167
|
let configured = 0;
|
|
137
168
|
let skipped = 0;
|
|
138
169
|
let notInstalled = 0;
|
package/package.json
CHANGED