faux-studio 0.4.7 → 0.5.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.
- package/README.md +29 -6
- package/dist/index.js +359 -90
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -14,10 +14,17 @@ On first run, a browser window opens for Figma OAuth. After authentication, the
|
|
|
14
14
|
|
|
15
15
|
### Claude Code
|
|
16
16
|
|
|
17
|
+
```bash
|
|
18
|
+
claude mcp add --scope user faux-studio -- npx faux-studio@latest
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Or manually add to your MCP settings:
|
|
22
|
+
|
|
17
23
|
```json
|
|
18
24
|
{
|
|
19
25
|
"mcpServers": {
|
|
20
26
|
"faux-studio": {
|
|
27
|
+
"type": "stdio",
|
|
21
28
|
"command": "npx",
|
|
22
29
|
"args": ["-y", "faux-studio"]
|
|
23
30
|
}
|
|
@@ -25,21 +32,38 @@ On first run, a browser window opens for Figma OAuth. After authentication, the
|
|
|
25
32
|
}
|
|
26
33
|
```
|
|
27
34
|
|
|
28
|
-
###
|
|
35
|
+
### OpenCode
|
|
29
36
|
|
|
30
|
-
|
|
37
|
+
manually add to your MCP settings (~/.config/opencode/opencode.json):
|
|
31
38
|
|
|
32
39
|
```json
|
|
33
40
|
{
|
|
34
|
-
"
|
|
41
|
+
"mcp": {
|
|
35
42
|
"faux-studio": {
|
|
36
|
-
"
|
|
37
|
-
"
|
|
43
|
+
"type": "local",
|
|
44
|
+
"command": [
|
|
45
|
+
"npx",
|
|
46
|
+
"faux-studio@latest"
|
|
47
|
+
]
|
|
38
48
|
}
|
|
39
49
|
}
|
|
40
50
|
}
|
|
41
51
|
```
|
|
42
52
|
|
|
53
|
+
### Codex
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
codex mcp add faux-studio -- npx -y faux-studio@latest
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Or manually add to your MCP settings (~/.codex/config.toml):
|
|
60
|
+
|
|
61
|
+
```yml
|
|
62
|
+
[mcp_servers.faux-studio]
|
|
63
|
+
command = "npx"
|
|
64
|
+
args = ["-y", "faux-studio@latest"]
|
|
65
|
+
```
|
|
66
|
+
|
|
43
67
|
## How It Works
|
|
44
68
|
|
|
45
69
|
faux-studio runs locally and bridges your AI client to Figma Desktop:
|
|
@@ -58,7 +82,6 @@ faux-studio runs locally and bridges your AI client to Figma Desktop:
|
|
|
58
82
|
|
|
59
83
|
| Variable | Description |
|
|
60
84
|
|----------|-------------|
|
|
61
|
-
| `FAUX_API_KEY` | Skip OAuth — use an API key instead |
|
|
62
85
|
| `FAUX_TRANSPORT` | Force transport: `cdp` or `plugin` |
|
|
63
86
|
|
|
64
87
|
## Links
|
package/dist/index.js
CHANGED
|
@@ -10563,17 +10563,72 @@ var SecurityCliKeychainStore = class {
|
|
|
10563
10563
|
}
|
|
10564
10564
|
}
|
|
10565
10565
|
};
|
|
10566
|
+
var KeyringCredentialStore = class {
|
|
10567
|
+
name = "keychain";
|
|
10568
|
+
entry;
|
|
10569
|
+
cached = void 0;
|
|
10570
|
+
constructor(entry) {
|
|
10571
|
+
this.entry = entry;
|
|
10572
|
+
}
|
|
10573
|
+
async load() {
|
|
10574
|
+
if (this.cached !== void 0) return this.cached;
|
|
10575
|
+
try {
|
|
10576
|
+
const raw = await this.entry.getPassword();
|
|
10577
|
+
if (!raw) {
|
|
10578
|
+
this.cached = null;
|
|
10579
|
+
return null;
|
|
10580
|
+
}
|
|
10581
|
+
const creds = JSON.parse(raw);
|
|
10582
|
+
if (!creds.jwt || !creds.refreshToken || !creds.user) {
|
|
10583
|
+
this.cached = null;
|
|
10584
|
+
return null;
|
|
10585
|
+
}
|
|
10586
|
+
this.cached = creds;
|
|
10587
|
+
return creds;
|
|
10588
|
+
} catch {
|
|
10589
|
+
this.cached = null;
|
|
10590
|
+
return null;
|
|
10591
|
+
}
|
|
10592
|
+
}
|
|
10593
|
+
async save(creds) {
|
|
10594
|
+
try {
|
|
10595
|
+
await this.entry.setPassword(JSON.stringify(creds));
|
|
10596
|
+
this.cached = creds;
|
|
10597
|
+
} catch (err) {
|
|
10598
|
+
warn(`Could not save to keychain: ${err instanceof Error ? err.message : err}`);
|
|
10599
|
+
}
|
|
10600
|
+
}
|
|
10601
|
+
async clear() {
|
|
10602
|
+
try {
|
|
10603
|
+
await this.entry.deletePassword();
|
|
10604
|
+
this.cached = null;
|
|
10605
|
+
} catch {
|
|
10606
|
+
}
|
|
10607
|
+
}
|
|
10608
|
+
};
|
|
10566
10609
|
async function tryCreateKeychainStore() {
|
|
10567
|
-
if (process.platform
|
|
10568
|
-
|
|
10569
|
-
|
|
10610
|
+
if (process.platform === "darwin") {
|
|
10611
|
+
try {
|
|
10612
|
+
log("[keychain] probing macOS security CLI...");
|
|
10613
|
+
const t0 = Date.now();
|
|
10614
|
+
await securityExec("help");
|
|
10615
|
+
log(`[keychain] security CLI available (${Date.now() - t0}ms)`);
|
|
10616
|
+
return new SecurityCliKeychainStore();
|
|
10617
|
+
} catch (err) {
|
|
10618
|
+
warn(
|
|
10619
|
+
`OS keychain not available, using file-based credential storage: ${err instanceof Error ? err.message : err}`
|
|
10620
|
+
);
|
|
10621
|
+
return null;
|
|
10622
|
+
}
|
|
10570
10623
|
}
|
|
10571
10624
|
try {
|
|
10572
|
-
log("[keychain]
|
|
10625
|
+
log("[keychain] importing @napi-rs/keyring...");
|
|
10573
10626
|
const t0 = Date.now();
|
|
10574
|
-
await
|
|
10575
|
-
|
|
10576
|
-
|
|
10627
|
+
const { AsyncEntry } = await import("@napi-rs/keyring");
|
|
10628
|
+
const entry = new AsyncEntry(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT);
|
|
10629
|
+
await entry.getPassword();
|
|
10630
|
+
log(`[keychain] keyring available (${Date.now() - t0}ms)`);
|
|
10631
|
+
return new KeyringCredentialStore(entry);
|
|
10577
10632
|
} catch (err) {
|
|
10578
10633
|
warn(
|
|
10579
10634
|
`OS keychain not available, using file-based credential storage: ${err instanceof Error ? err.message : err}`
|
|
@@ -10602,7 +10657,7 @@ async function initStore() {
|
|
|
10602
10657
|
const fileCreds = await fileStore.load();
|
|
10603
10658
|
if (fileCreds) {
|
|
10604
10659
|
await keychainStore.save(fileCreds);
|
|
10605
|
-
keychainStore
|
|
10660
|
+
keychainStore.cached = void 0;
|
|
10606
10661
|
const verified = await keychainStore.load();
|
|
10607
10662
|
if (verified) {
|
|
10608
10663
|
await fileStore.clear();
|
|
@@ -10919,6 +10974,124 @@ async function generateScript(jwt2, toolName, params) {
|
|
|
10919
10974
|
return { script: data.script, signature: data.signature };
|
|
10920
10975
|
}
|
|
10921
10976
|
|
|
10977
|
+
// src/icons.ts
|
|
10978
|
+
function parseIconName(iconName) {
|
|
10979
|
+
if (iconName.includes(":")) {
|
|
10980
|
+
const [prefix, name] = iconName.split(":");
|
|
10981
|
+
return { prefix, name };
|
|
10982
|
+
}
|
|
10983
|
+
const parts = iconName.split("-");
|
|
10984
|
+
if (parts.length > 1) {
|
|
10985
|
+
return { prefix: parts[0], name: parts.slice(1).join("-") };
|
|
10986
|
+
}
|
|
10987
|
+
return { prefix: "lucide", name: iconName };
|
|
10988
|
+
}
|
|
10989
|
+
async function fetchIconSvg(iconName, size = 24) {
|
|
10990
|
+
const { prefix, name } = parseIconName(iconName);
|
|
10991
|
+
const url2 = `https://api.iconify.design/${prefix}/${name}.svg?width=${size}&height=${size}`;
|
|
10992
|
+
const response = await fetch(url2);
|
|
10993
|
+
if (!response.ok) {
|
|
10994
|
+
if (response.status === 404) throw new Error(`Icon not found: ${iconName}`);
|
|
10995
|
+
throw new Error(`Failed to fetch icon: HTTP ${response.status}`);
|
|
10996
|
+
}
|
|
10997
|
+
return await response.text();
|
|
10998
|
+
}
|
|
10999
|
+
function detectIconType(svgContent) {
|
|
11000
|
+
if (svgContent.includes('stroke="currentColor"') || svgContent.includes("stroke='currentColor'")) {
|
|
11001
|
+
return "stroke";
|
|
11002
|
+
}
|
|
11003
|
+
if (svgContent.includes('opacity="0.') || svgContent.includes("opacity='0.")) {
|
|
11004
|
+
return "duotone";
|
|
11005
|
+
}
|
|
11006
|
+
return "fill";
|
|
11007
|
+
}
|
|
11008
|
+
async function searchSingleQuery(query, limit, prefix, category) {
|
|
11009
|
+
let url2 = `https://api.iconify.design/search?query=${encodeURIComponent(query)}&limit=${limit}`;
|
|
11010
|
+
if (prefix) url2 += `&prefix=${encodeURIComponent(prefix)}`;
|
|
11011
|
+
if (category) url2 += `&category=${encodeURIComponent(category)}`;
|
|
11012
|
+
const response = await fetch(url2);
|
|
11013
|
+
if (!response.ok) {
|
|
11014
|
+
if (response.status === 404) throw new Error("Icon search is currently disabled");
|
|
11015
|
+
throw new Error(`Search failed: HTTP ${response.status}`);
|
|
11016
|
+
}
|
|
11017
|
+
const data = await response.json();
|
|
11018
|
+
const icons = data.icons || [];
|
|
11019
|
+
return { query, total: icons.length, icons };
|
|
11020
|
+
}
|
|
11021
|
+
async function searchIcons(params) {
|
|
11022
|
+
const { limit = 3, prefix, category } = params;
|
|
11023
|
+
const effectiveLimit = Math.min(limit, 100);
|
|
11024
|
+
if (params.queries && params.queries.length > 0) {
|
|
11025
|
+
const results = await Promise.all(
|
|
11026
|
+
params.queries.map((q) => searchSingleQuery(q.trim(), effectiveLimit, prefix, category))
|
|
11027
|
+
);
|
|
11028
|
+
return { results, totalQueries: results.length };
|
|
11029
|
+
}
|
|
11030
|
+
const query = params.query;
|
|
11031
|
+
if (!query || query.trim().length === 0) {
|
|
11032
|
+
throw new Error("Search query is required (provide `query` or `queries`)");
|
|
11033
|
+
}
|
|
11034
|
+
return searchSingleQuery(query.trim(), effectiveLimit, prefix, category);
|
|
11035
|
+
}
|
|
11036
|
+
async function resolveIconsInNode(node) {
|
|
11037
|
+
if (node.$icon && typeof node.$icon === "string") {
|
|
11038
|
+
const searchTerm = node.$icon;
|
|
11039
|
+
const size = typeof node.size === "number" ? node.size : 24;
|
|
11040
|
+
try {
|
|
11041
|
+
const searchResult = await searchIcons({ query: searchTerm, limit: 1 });
|
|
11042
|
+
if (searchResult.icons.length === 0) {
|
|
11043
|
+
warn(`No icons found for: ${searchTerm}`);
|
|
11044
|
+
const { $icon: $icon2, ...rest2 } = node;
|
|
11045
|
+
return { ...rest2, $iconError: `No icons found for: ${searchTerm}` };
|
|
11046
|
+
}
|
|
11047
|
+
const iconName = searchResult.icons[0];
|
|
11048
|
+
const svgContent = await fetchIconSvg(iconName, size);
|
|
11049
|
+
const iconType = detectIconType(svgContent);
|
|
11050
|
+
const { $icon, ...rest } = node;
|
|
11051
|
+
return {
|
|
11052
|
+
...rest,
|
|
11053
|
+
$iconResolved: { svgContent, iconType, iconName, searchTerm }
|
|
11054
|
+
};
|
|
11055
|
+
} catch (error3) {
|
|
11056
|
+
error(`Failed to resolve icon "${searchTerm}": ${error3 instanceof Error ? error3.message : String(error3)}`);
|
|
11057
|
+
const { $icon, ...rest } = node;
|
|
11058
|
+
return { ...rest, $iconError: `Failed to load icon: ${error3 instanceof Error ? error3.message : "Unknown error"}` };
|
|
11059
|
+
}
|
|
11060
|
+
}
|
|
11061
|
+
let result = { ...node };
|
|
11062
|
+
if (node.children && Array.isArray(node.children)) {
|
|
11063
|
+
result.children = await Promise.all(
|
|
11064
|
+
node.children.map(async (child) => {
|
|
11065
|
+
if (child && typeof child === "object") {
|
|
11066
|
+
return resolveIconsInNode(child);
|
|
11067
|
+
}
|
|
11068
|
+
return child;
|
|
11069
|
+
})
|
|
11070
|
+
);
|
|
11071
|
+
}
|
|
11072
|
+
if (node.template && typeof node.template === "object") {
|
|
11073
|
+
result.template = await resolveIconsInNode(node.template);
|
|
11074
|
+
}
|
|
11075
|
+
return result;
|
|
11076
|
+
}
|
|
11077
|
+
var ICON_TOOLS = /* @__PURE__ */ new Set(["create_from_schema", "modify_via_schema"]);
|
|
11078
|
+
async function preprocessIconsInParams(toolName, params) {
|
|
11079
|
+
if (!ICON_TOOLS.has(toolName)) return params;
|
|
11080
|
+
const result = { ...params };
|
|
11081
|
+
if (result.schema && typeof result.schema === "object") {
|
|
11082
|
+
result.schema = await resolveIconsInNode(result.schema);
|
|
11083
|
+
}
|
|
11084
|
+
if (result.modifications && Array.isArray(result.modifications)) {
|
|
11085
|
+
result.modifications = await Promise.all(
|
|
11086
|
+
result.modifications.map(async (mod) => ({
|
|
11087
|
+
...mod,
|
|
11088
|
+
schema: await resolveIconsInNode(mod.schema)
|
|
11089
|
+
}))
|
|
11090
|
+
);
|
|
11091
|
+
}
|
|
11092
|
+
return result;
|
|
11093
|
+
}
|
|
11094
|
+
|
|
10922
11095
|
// node_modules/ws/wrapper.mjs
|
|
10923
11096
|
var import_stream = __toESM(require_stream(), 1);
|
|
10924
11097
|
var import_receiver = __toESM(require_receiver(), 1);
|
|
@@ -11396,7 +11569,7 @@ var PORT_RANGE = 8;
|
|
|
11396
11569
|
var SCRIPT_TIMEOUT_MS = 6e4;
|
|
11397
11570
|
var CONNECT_WAIT_MS = 3e4;
|
|
11398
11571
|
var FAUX_DIR3 = join4(homedir3(), ".faux");
|
|
11399
|
-
var PluginWsServer = class {
|
|
11572
|
+
var PluginWsServer = class _PluginWsServer {
|
|
11400
11573
|
name = "plugin-ws";
|
|
11401
11574
|
wss = null;
|
|
11402
11575
|
connections = /* @__PURE__ */ new Map();
|
|
@@ -11404,6 +11577,17 @@ var PluginWsServer = class {
|
|
|
11404
11577
|
connectWaiters = [];
|
|
11405
11578
|
_activeFileId = null;
|
|
11406
11579
|
_port = 0;
|
|
11580
|
+
// File tracking state (mirrors CdpFileTracker API)
|
|
11581
|
+
_lastAcknowledgedFileId = null;
|
|
11582
|
+
_lastAcknowledgedFileName = null;
|
|
11583
|
+
_lastAcknowledgedAt = 0;
|
|
11584
|
+
_lastFocusEventAt = 0;
|
|
11585
|
+
/** How long before focus events are considered stale for high-confidence detection. */
|
|
11586
|
+
static FOCUS_FRESH_MS = 12e4;
|
|
11587
|
+
// 2 minutes
|
|
11588
|
+
/** How long before last-known file is considered stale and untrusted (matches CDP). */
|
|
11589
|
+
static LAST_KNOWN_STALE_MS = 5 * 6e4;
|
|
11590
|
+
// 5 minutes
|
|
11407
11591
|
// -------------------------------------------------------------------------
|
|
11408
11592
|
// Public Getters
|
|
11409
11593
|
// -------------------------------------------------------------------------
|
|
@@ -11422,6 +11606,79 @@ var PluginWsServer = class {
|
|
|
11422
11606
|
get hasConnections() {
|
|
11423
11607
|
return this.connections.size > 0;
|
|
11424
11608
|
}
|
|
11609
|
+
get activeFileName() {
|
|
11610
|
+
if (!this._activeFileId) return null;
|
|
11611
|
+
return this.connections.get(this._activeFileId)?.fileName ?? null;
|
|
11612
|
+
}
|
|
11613
|
+
get lastAcknowledgedFileName() {
|
|
11614
|
+
return this._lastAcknowledgedFileName;
|
|
11615
|
+
}
|
|
11616
|
+
// -------------------------------------------------------------------------
|
|
11617
|
+
// File Tracking
|
|
11618
|
+
// -------------------------------------------------------------------------
|
|
11619
|
+
/**
|
|
11620
|
+
* Detect the active file for the plugin transport.
|
|
11621
|
+
* Unlike CDP (which introspects shell.html), this is synchronous — the
|
|
11622
|
+
* plugin tells us via file-focus events. The cacheTtlMs parameter is
|
|
11623
|
+
* accepted for API compatibility but detection is always in-memory.
|
|
11624
|
+
*/
|
|
11625
|
+
detectActiveFile(_cacheTtlMs = 2e3) {
|
|
11626
|
+
const allFiles = this.getAllFileInfos();
|
|
11627
|
+
if (this.connections.size === 0) {
|
|
11628
|
+
return { activeFile: null, allFiles, method: "none", confidence: "none" };
|
|
11629
|
+
}
|
|
11630
|
+
if (this.connections.size === 1) {
|
|
11631
|
+
const conn = this.connections.values().next().value;
|
|
11632
|
+
return { activeFile: this.connToFileInfo(conn), allFiles, method: "single-file", confidence: "high" };
|
|
11633
|
+
}
|
|
11634
|
+
if (this._activeFileId && this.connections.has(this._activeFileId)) {
|
|
11635
|
+
const conn = this.connections.get(this._activeFileId);
|
|
11636
|
+
const info = this.connToFileInfo(conn);
|
|
11637
|
+
const focusAge = Date.now() - this._lastFocusEventAt;
|
|
11638
|
+
if (this._lastFocusEventAt > 0 && focusAge < _PluginWsServer.FOCUS_FRESH_MS) {
|
|
11639
|
+
return { activeFile: info, allFiles, method: "focus-event", confidence: "high" };
|
|
11640
|
+
}
|
|
11641
|
+
if (this.isLastKnownStale()) {
|
|
11642
|
+
log("File tracker: focus and acknowledgment both stale \u2014 cannot detect active file");
|
|
11643
|
+
return { activeFile: null, allFiles, method: "none", confidence: "none" };
|
|
11644
|
+
}
|
|
11645
|
+
return { activeFile: info, allFiles, method: "last-known", confidence: "low" };
|
|
11646
|
+
}
|
|
11647
|
+
return { activeFile: null, allFiles, method: "none", confidence: "none" };
|
|
11648
|
+
}
|
|
11649
|
+
/** Whether the active file has changed since the last acknowledged execution. */
|
|
11650
|
+
hasFileChanged() {
|
|
11651
|
+
return this._activeFileId !== null && this._lastAcknowledgedFileId !== null && this._activeFileId !== this._lastAcknowledgedFileId;
|
|
11652
|
+
}
|
|
11653
|
+
/**
|
|
11654
|
+
* Acknowledge the current active file as the intended target.
|
|
11655
|
+
* Call AFTER successful script execution.
|
|
11656
|
+
*/
|
|
11657
|
+
acknowledgeActiveFile() {
|
|
11658
|
+
if (this._activeFileId) {
|
|
11659
|
+
this._lastAcknowledgedFileId = this._activeFileId;
|
|
11660
|
+
this._lastAcknowledgedFileName = this.connections.get(this._activeFileId)?.fileName ?? null;
|
|
11661
|
+
this._lastAcknowledgedAt = Date.now();
|
|
11662
|
+
}
|
|
11663
|
+
}
|
|
11664
|
+
/** True if the last acknowledged execution was more than 5 minutes ago. */
|
|
11665
|
+
isLastKnownStale() {
|
|
11666
|
+
if (this._lastAcknowledgedAt === 0) return false;
|
|
11667
|
+
return Date.now() - this._lastAcknowledgedAt > _PluginWsServer.LAST_KNOWN_STALE_MS;
|
|
11668
|
+
}
|
|
11669
|
+
/** All connected files as FigmaFileInfo[]. */
|
|
11670
|
+
getAllFileInfos() {
|
|
11671
|
+
return Array.from(this.connections.values()).map((c) => this.connToFileInfo(c));
|
|
11672
|
+
}
|
|
11673
|
+
/** Build FigmaFileInfo from a PluginConnection. */
|
|
11674
|
+
connToFileInfo(conn) {
|
|
11675
|
+
return {
|
|
11676
|
+
fileKey: conn.fileId,
|
|
11677
|
+
fileName: conn.fileName,
|
|
11678
|
+
targetId: conn.fileId,
|
|
11679
|
+
wsUrl: ""
|
|
11680
|
+
};
|
|
11681
|
+
}
|
|
11425
11682
|
// -------------------------------------------------------------------------
|
|
11426
11683
|
// Server Lifecycle
|
|
11427
11684
|
// -------------------------------------------------------------------------
|
|
@@ -11516,6 +11773,7 @@ var PluginWsServer = class {
|
|
|
11516
11773
|
this.connections.set(fileId, { ws, fileId, fileName, connectedAt: Date.now() });
|
|
11517
11774
|
if (this.connections.size === 1 || !this._activeFileId) {
|
|
11518
11775
|
this._activeFileId = fileId;
|
|
11776
|
+
this._lastFocusEventAt = Date.now();
|
|
11519
11777
|
}
|
|
11520
11778
|
log(`Plugin connected: "${fileName}" (${fileId}) [${this.connections.size} file(s)]`);
|
|
11521
11779
|
for (const waiter of this.connectWaiters) {
|
|
@@ -11576,6 +11834,7 @@ var PluginWsServer = class {
|
|
|
11576
11834
|
const focusFileId = msg.fileId || fileId;
|
|
11577
11835
|
if (this.connections.has(focusFileId)) {
|
|
11578
11836
|
this._activeFileId = focusFileId;
|
|
11837
|
+
this._lastFocusEventAt = Date.now();
|
|
11579
11838
|
}
|
|
11580
11839
|
break;
|
|
11581
11840
|
}
|
|
@@ -25988,10 +26247,10 @@ var RESOURCES = [
|
|
|
25988
26247
|
var INSTRUCTIONS = `You are connected to Figma Desktop via faux-studio. You can create, modify, and inspect designs using the tools below.
|
|
25989
26248
|
|
|
25990
26249
|
## Transport
|
|
25991
|
-
faux-studio connects to Figma Desktop via **
|
|
26250
|
+
faux-studio connects to Figma Desktop via the **faux-studio plugin** by default \u2014 install and run the plugin from Figma's Plugins menu. The plugin communicates via local WebSocket with real-time file focus tracking. If the plugin is not available, faux-studio falls back to CDP (Chrome DevTools Protocol) which requires launching Figma with a debug port. The plugin is the preferred transport.
|
|
25992
26251
|
|
|
25993
26252
|
## Workflow
|
|
25994
|
-
1. Call \`setup_figma\` first to ensure Figma is running and connected (via
|
|
26253
|
+
1. Call \`setup_figma\` first to ensure Figma is running and connected (via plugin or CDP).
|
|
25995
26254
|
2. Call \`get_page_structure\` or \`get_screenshot\` to understand what's on the canvas.
|
|
25996
26255
|
3. Use \`create_from_schema\` for all creation \u2014 it accepts declarative JSON schemas.
|
|
25997
26256
|
4. Use \`modify_via_schema\` to change existing nodes.
|
|
@@ -26031,7 +26290,7 @@ Resources provide quick read-only access to Figma state without tool calls:
|
|
|
26031
26290
|
- Create components for reusable UI patterns.`;
|
|
26032
26291
|
function createMcpServer(deps) {
|
|
26033
26292
|
const server2 = new Server(
|
|
26034
|
-
{ name: "faux-studio", version: "0.
|
|
26293
|
+
{ name: "faux-studio", version: "0.5.0" },
|
|
26035
26294
|
{
|
|
26036
26295
|
capabilities: { tools: { listChanged: true }, resources: {}, logging: {} },
|
|
26037
26296
|
instructions: INSTRUCTIONS
|
|
@@ -26046,7 +26305,7 @@ function createMcpServer(deps) {
|
|
|
26046
26305
|
},
|
|
26047
26306
|
{
|
|
26048
26307
|
name: "setup_figma",
|
|
26049
|
-
description: "Ensure Figma Desktop is running and connected via
|
|
26308
|
+
description: "Ensure Figma Desktop is running and connected via plugin WebSocket (preferred) or CDP (fallback). Call this before any design work. Detects open files and the active tab. Returns connection status, transport mode, active file, and list of all open design files. Idempotent \u2014 safe to call multiple times.",
|
|
26050
26309
|
inputSchema: {
|
|
26051
26310
|
type: "object",
|
|
26052
26311
|
properties: {},
|
|
@@ -26133,6 +26392,13 @@ function createMcpServer(deps) {
|
|
|
26133
26392
|
throw err;
|
|
26134
26393
|
}
|
|
26135
26394
|
}
|
|
26395
|
+
if (name === "search_icons") {
|
|
26396
|
+
log("search_icons called (server-side)");
|
|
26397
|
+
const result2 = await searchIcons(params);
|
|
26398
|
+
return {
|
|
26399
|
+
content: [{ type: "text", text: JSON.stringify(result2, null, 2) }]
|
|
26400
|
+
};
|
|
26401
|
+
}
|
|
26136
26402
|
const intents = {
|
|
26137
26403
|
meso: typeof params._intent_meso === "string" ? params._intent_meso : void 0,
|
|
26138
26404
|
macro: typeof params._intent_macro === "string" ? params._intent_macro : void 0
|
|
@@ -26296,6 +26562,24 @@ async function tryConnectCdp() {
|
|
|
26296
26562
|
}
|
|
26297
26563
|
}
|
|
26298
26564
|
async function executeScript(script, intents, opts) {
|
|
26565
|
+
if (forceTransport !== "cdp" && pluginServer.hasConnections) {
|
|
26566
|
+
const detection = pluginServer.detectActiveFile(opts?.cacheTtlMs);
|
|
26567
|
+
if (!detection.activeFile && detection.allFiles.length > 1) {
|
|
26568
|
+
throw new FileUnknownError(detection.allFiles);
|
|
26569
|
+
}
|
|
26570
|
+
if (pluginServer.hasFileChanged()) {
|
|
26571
|
+
throw new FileChangedError(
|
|
26572
|
+
pluginServer.lastAcknowledgedFileName,
|
|
26573
|
+
pluginServer.activeFileName,
|
|
26574
|
+
pluginServer.activeFileId,
|
|
26575
|
+
detection.allFiles
|
|
26576
|
+
);
|
|
26577
|
+
}
|
|
26578
|
+
log(`Executing via plugin (file: ${pluginServer.activeFileId || "active"})`);
|
|
26579
|
+
const result = await pluginServer.executeScript(script, void 0, intents);
|
|
26580
|
+
pluginServer.acknowledgeActiveFile();
|
|
26581
|
+
return result;
|
|
26582
|
+
}
|
|
26299
26583
|
if (forceTransport !== "plugin") {
|
|
26300
26584
|
const client = await tryConnectCdp();
|
|
26301
26585
|
if (client) {
|
|
@@ -26332,10 +26616,6 @@ async function executeScript(script, intents, opts) {
|
|
|
26332
26616
|
}
|
|
26333
26617
|
}
|
|
26334
26618
|
}
|
|
26335
|
-
if (forceTransport !== "cdp" && pluginServer.hasConnections) {
|
|
26336
|
-
log(`Executing via plugin (file: ${pluginServer.activeFileId || "active"})`);
|
|
26337
|
-
return pluginServer.executeScript(script, void 0, intents);
|
|
26338
|
-
}
|
|
26339
26619
|
log("No transport available, waiting for plugin...");
|
|
26340
26620
|
return pluginServer.executeScript(script, void 0, intents);
|
|
26341
26621
|
}
|
|
@@ -26361,6 +26641,7 @@ async function recoverCdp(client, script) {
|
|
|
26361
26641
|
async function generateWithAuth(toolName, params) {
|
|
26362
26642
|
await lazyInit();
|
|
26363
26643
|
auth = await refreshIfNeeded(auth);
|
|
26644
|
+
params = await preprocessIconsInParams(toolName, params);
|
|
26364
26645
|
let result;
|
|
26365
26646
|
try {
|
|
26366
26647
|
result = await generateScript(auth.jwt, toolName, params);
|
|
@@ -26398,16 +26679,23 @@ async function waitForPlugin() {
|
|
|
26398
26679
|
return false;
|
|
26399
26680
|
}
|
|
26400
26681
|
function pluginReadyResult() {
|
|
26682
|
+
const detection = pluginServer.detectActiveFile();
|
|
26683
|
+
const activeName = detection.activeFile?.fileName;
|
|
26684
|
+
const fileCount = detection.allFiles.length;
|
|
26401
26685
|
return {
|
|
26402
26686
|
status: "ready",
|
|
26403
26687
|
transport: "plugin",
|
|
26404
|
-
message: `Connected via plugin.${
|
|
26405
|
-
activeFile:
|
|
26688
|
+
message: `Connected via plugin.${activeName ? ` Active file: "${activeName}".` : ""} ${fileCount} file(s) open. Ready to design.`,
|
|
26689
|
+
activeFile: activeName || void 0,
|
|
26406
26690
|
pluginFiles: pluginServer.connectedFiles,
|
|
26691
|
+
openFiles: detection.allFiles.map((f) => ({ fileKey: f.fileKey, fileName: f.fileName })),
|
|
26407
26692
|
port: pluginServer.port
|
|
26408
26693
|
};
|
|
26409
26694
|
}
|
|
26410
26695
|
async function setupFigma(params) {
|
|
26696
|
+
if (forceTransport !== "cdp" && pluginServer.hasConnections) {
|
|
26697
|
+
return pluginReadyResult();
|
|
26698
|
+
}
|
|
26411
26699
|
if (forceTransport !== "plugin" && cdpClient?.connected && cdpClient.hasContext) {
|
|
26412
26700
|
if (fileTracker) {
|
|
26413
26701
|
const detection = await fileTracker.detectActiveFile();
|
|
@@ -26425,40 +26713,6 @@ async function setupFigma(params) {
|
|
|
26425
26713
|
message: "Connected via CDP. Ready to design."
|
|
26426
26714
|
};
|
|
26427
26715
|
}
|
|
26428
|
-
if (forceTransport !== "cdp" && pluginServer.hasConnections) {
|
|
26429
|
-
return pluginReadyResult();
|
|
26430
|
-
}
|
|
26431
|
-
if (forceTransport !== "plugin") {
|
|
26432
|
-
const existing = await probeCdpPorts();
|
|
26433
|
-
if (existing) {
|
|
26434
|
-
const target = findFigmaDesignTarget(existing.targets);
|
|
26435
|
-
if (target) {
|
|
26436
|
-
try {
|
|
26437
|
-
cdpClient?.close();
|
|
26438
|
-
cdpClient = null;
|
|
26439
|
-
const client = new CdpClient();
|
|
26440
|
-
await client.connect(target.webSocketDebuggerUrl);
|
|
26441
|
-
await client.discoverFigmaContext();
|
|
26442
|
-
cdpClient = client;
|
|
26443
|
-
log(`CDP connected: ${target.title}`);
|
|
26444
|
-
if (!fileTracker) {
|
|
26445
|
-
fileTracker = new CdpFileTracker(existing.port);
|
|
26446
|
-
}
|
|
26447
|
-
const detection = await fileTracker.detectActiveFile();
|
|
26448
|
-
fileTracker.acknowledgeActiveFile();
|
|
26449
|
-
return {
|
|
26450
|
-
status: "ready",
|
|
26451
|
-
transport: "cdp",
|
|
26452
|
-
message: `Connected via CDP. Active file: "${detection.activeFile?.fileName ?? target.title}". ${detection.allFiles.length} file(s) open. Ready to design.`,
|
|
26453
|
-
activeFile: detection.activeFile?.fileName ?? target.title,
|
|
26454
|
-
openFiles: detection.allFiles.map((f) => ({ fileKey: f.fileKey, fileName: f.fileName })),
|
|
26455
|
-
port: existing.port
|
|
26456
|
-
};
|
|
26457
|
-
} catch {
|
|
26458
|
-
}
|
|
26459
|
-
}
|
|
26460
|
-
}
|
|
26461
|
-
}
|
|
26462
26716
|
if (!findFigmaPath()) {
|
|
26463
26717
|
return {
|
|
26464
26718
|
status: "not_installed",
|
|
@@ -26467,17 +26721,7 @@ async function setupFigma(params) {
|
|
|
26467
26721
|
};
|
|
26468
26722
|
}
|
|
26469
26723
|
if (!isFigmaRunning()) {
|
|
26470
|
-
if (forceTransport === "
|
|
26471
|
-
try {
|
|
26472
|
-
launchFigma();
|
|
26473
|
-
} catch (err) {
|
|
26474
|
-
return {
|
|
26475
|
-
status: "not_installed",
|
|
26476
|
-
transport: "none",
|
|
26477
|
-
message: err instanceof Error ? err.message : "Failed to launch Figma."
|
|
26478
|
-
};
|
|
26479
|
-
}
|
|
26480
|
-
} else {
|
|
26724
|
+
if (forceTransport === "cdp") {
|
|
26481
26725
|
try {
|
|
26482
26726
|
const connection = await launchFigmaWithCdp();
|
|
26483
26727
|
const target = findFigmaDesignTarget(connection.targets);
|
|
@@ -26514,33 +26758,58 @@ async function setupFigma(params) {
|
|
|
26514
26758
|
message: err instanceof Error ? err.message : "Failed to launch Figma."
|
|
26515
26759
|
};
|
|
26516
26760
|
}
|
|
26761
|
+
} else {
|
|
26762
|
+
try {
|
|
26763
|
+
launchFigma();
|
|
26764
|
+
} catch (err) {
|
|
26765
|
+
return {
|
|
26766
|
+
status: "not_installed",
|
|
26767
|
+
transport: "none",
|
|
26768
|
+
message: err instanceof Error ? err.message : "Failed to launch Figma."
|
|
26769
|
+
};
|
|
26770
|
+
}
|
|
26517
26771
|
}
|
|
26518
26772
|
}
|
|
26519
|
-
if (forceTransport !== "
|
|
26520
|
-
|
|
26521
|
-
|
|
26522
|
-
|
|
26523
|
-
|
|
26524
|
-
|
|
26525
|
-
|
|
26526
|
-
|
|
26527
|
-
|
|
26528
|
-
|
|
26529
|
-
Alternatively, install the faux-studio plugin:
|
|
26530
|
-
1. Download: ${PLUGIN_URL}
|
|
26531
|
-
2. In Figma: Plugins \u2192 Development \u2192 Import plugin from manifest
|
|
26532
|
-
3. Run: Plugins \u2192 Development \u2192 faux-studio
|
|
26533
|
-
4. Call setup_figma again once the plugin shows "Ready".`,
|
|
26534
|
-
port: pluginServer.port
|
|
26535
|
-
};
|
|
26773
|
+
if (forceTransport !== "cdp") {
|
|
26774
|
+
log("Waiting for plugin connection...");
|
|
26775
|
+
const connected = await waitForPlugin();
|
|
26776
|
+
if (connected) {
|
|
26777
|
+
return {
|
|
26778
|
+
...pluginReadyResult(),
|
|
26779
|
+
status: isFigmaRunning() ? "ready" : "launched"
|
|
26780
|
+
};
|
|
26781
|
+
}
|
|
26536
26782
|
}
|
|
26537
|
-
|
|
26538
|
-
|
|
26539
|
-
|
|
26540
|
-
|
|
26541
|
-
|
|
26542
|
-
|
|
26543
|
-
|
|
26783
|
+
if (forceTransport !== "plugin") {
|
|
26784
|
+
const existing = await probeCdpPorts();
|
|
26785
|
+
if (existing) {
|
|
26786
|
+
const target = findFigmaDesignTarget(existing.targets);
|
|
26787
|
+
if (target) {
|
|
26788
|
+
try {
|
|
26789
|
+
cdpClient?.close();
|
|
26790
|
+
cdpClient = null;
|
|
26791
|
+
const client = new CdpClient();
|
|
26792
|
+
await client.connect(target.webSocketDebuggerUrl);
|
|
26793
|
+
await client.discoverFigmaContext();
|
|
26794
|
+
cdpClient = client;
|
|
26795
|
+
log(`CDP connected: ${target.title}`);
|
|
26796
|
+
if (!fileTracker) {
|
|
26797
|
+
fileTracker = new CdpFileTracker(existing.port);
|
|
26798
|
+
}
|
|
26799
|
+
const detection = await fileTracker.detectActiveFile();
|
|
26800
|
+
fileTracker.acknowledgeActiveFile();
|
|
26801
|
+
return {
|
|
26802
|
+
status: "ready",
|
|
26803
|
+
transport: "cdp",
|
|
26804
|
+
message: `Connected via CDP. Active file: "${detection.activeFile?.fileName ?? target.title}". ${detection.allFiles.length} file(s) open. Ready to design.`,
|
|
26805
|
+
activeFile: detection.activeFile?.fileName ?? target.title,
|
|
26806
|
+
openFiles: detection.allFiles.map((f) => ({ fileKey: f.fileKey, fileName: f.fileName })),
|
|
26807
|
+
port: existing.port
|
|
26808
|
+
};
|
|
26809
|
+
} catch {
|
|
26810
|
+
}
|
|
26811
|
+
}
|
|
26812
|
+
}
|
|
26544
26813
|
}
|
|
26545
26814
|
return {
|
|
26546
26815
|
status: "waiting_for_plugin",
|
|
@@ -26561,7 +26830,7 @@ Call setup_figma again once the plugin shows "Ready".`,
|
|
|
26561
26830
|
}
|
|
26562
26831
|
async function main() {
|
|
26563
26832
|
const startupT0 = Date.now();
|
|
26564
|
-
log(`faux-studio v${"0.
|
|
26833
|
+
log(`faux-studio v${"0.5.0"} \u2014 process started (PID ${process.pid}, PPID ${process.ppid})`);
|
|
26565
26834
|
try {
|
|
26566
26835
|
const port = await pluginServer.start();
|
|
26567
26836
|
if (forceTransport === "plugin") {
|
|
@@ -26569,7 +26838,7 @@ async function main() {
|
|
|
26569
26838
|
} else if (forceTransport === "cdp") {
|
|
26570
26839
|
log(`Transport: CDP-only mode (FAUX_TRANSPORT=cdp) \u2014 plugin WS on port ${port} (standby)`);
|
|
26571
26840
|
} else {
|
|
26572
|
-
log(`Transport: auto \u2014
|
|
26841
|
+
log(`Transport: auto \u2014 plugin preferred (port ${port}), CDP fallback`);
|
|
26573
26842
|
}
|
|
26574
26843
|
} catch (err) {
|
|
26575
26844
|
warn(`Plugin WS server failed to start: ${err instanceof Error ? err.message : err}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "faux-studio",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "AI-powered Figma design via MCP — connect any AI client to Figma Desktop",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -48,5 +48,8 @@
|
|
|
48
48
|
"darwin",
|
|
49
49
|
"linux",
|
|
50
50
|
"win32"
|
|
51
|
-
]
|
|
51
|
+
],
|
|
52
|
+
"optionalDependencies": {
|
|
53
|
+
"@napi-rs/keyring": "^1.2.0"
|
|
54
|
+
}
|
|
52
55
|
}
|