@atezer/figma-mcp-bridge 1.2.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/CHANGELOG.md +62 -0
- package/README.md +99 -9
- package/dist/cloudflare/cloud-cors.js +40 -0
- package/dist/cloudflare/cloud-mode-kv.js +86 -0
- package/dist/cloudflare/cloud-mode-routes.js +97 -0
- package/dist/cloudflare/cloud-relay-session.js +141 -0
- package/dist/cloudflare/core/config.js +1 -1
- package/dist/cloudflare/core/figma-url.js +48 -0
- package/dist/cloudflare/core/plugin-bridge-connector.js +52 -43
- package/dist/cloudflare/core/plugin-bridge-server.js +211 -87
- package/dist/cloudflare/index.js +243 -4
- package/dist/core/config.js +1 -1
- package/dist/core/config.js.map +1 -1
- package/dist/core/figma-url.d.ts +10 -0
- package/dist/core/figma-url.d.ts.map +1 -0
- package/dist/core/figma-url.js +49 -0
- package/dist/core/figma-url.js.map +1 -0
- package/dist/core/plugin-bridge-connector.d.ts +6 -1
- package/dist/core/plugin-bridge-connector.d.ts.map +1 -1
- package/dist/core/plugin-bridge-connector.js +52 -43
- package/dist/core/plugin-bridge-connector.js.map +1 -1
- package/dist/core/plugin-bridge-server.d.ts +47 -14
- package/dist/core/plugin-bridge-server.d.ts.map +1 -1
- package/dist/core/plugin-bridge-server.js +211 -87
- package/dist/core/plugin-bridge-server.js.map +1 -1
- package/dist/local-plugin-only.d.ts.map +1 -1
- package/dist/local-plugin-only.js +163 -43
- package/dist/local-plugin-only.js.map +1 -1
- package/f-mcp-plugin/README.md +13 -5
- package/f-mcp-plugin/code.js +216 -2
- package/f-mcp-plugin/manifest.json +6 -2
- package/f-mcp-plugin/ui.html +694 -213
- package/package.json +7 -6
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse Figma and FigJam URLs to extract fileKey and optional nodeId.
|
|
3
|
+
* Used for link-based routing in multi-client scenarios.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Supported URL patterns:
|
|
7
|
+
* - https://www.figma.com/design/<fileKey>/...
|
|
8
|
+
* - https://www.figma.com/board/<fileKey>/... (FigJam)
|
|
9
|
+
* - https://www.figma.com/jam/<fileKey>/...
|
|
10
|
+
* - https://www.figma.com/proto/<fileKey>/...
|
|
11
|
+
* - https://figma.com/... (no www)
|
|
12
|
+
* Query: ?node-id=0-1 or ?node-id=0:1 → nodeId "0:1"
|
|
13
|
+
*/
|
|
14
|
+
const FIGMA_PATH_REGEX = /^https?:\/\/(www\.)?figma\.com\/(design|board|jam|proto|file)\/([a-zA-Z0-9_-]{10,128})(?:\/|$)/i;
|
|
15
|
+
export function parseFigmaUrl(url) {
|
|
16
|
+
if (!url || typeof url !== "string")
|
|
17
|
+
return null;
|
|
18
|
+
const trimmed = url.trim();
|
|
19
|
+
if (!trimmed)
|
|
20
|
+
return null;
|
|
21
|
+
let fileKey = null;
|
|
22
|
+
// Try path-based match: /design/KEY, /board/KEY, /jam/KEY, /proto/KEY, /file/KEY
|
|
23
|
+
const pathMatch = trimmed.match(FIGMA_PATH_REGEX);
|
|
24
|
+
if (pathMatch) {
|
|
25
|
+
fileKey = pathMatch[3];
|
|
26
|
+
}
|
|
27
|
+
// Fallback: some Figma links use /file/KEY or just KEY in path
|
|
28
|
+
if (!fileKey) {
|
|
29
|
+
const fileKeyFromPath = trimmed.match(/figma\.com\/(?:design|board|jam|proto|file)\/([a-zA-Z0-9_-]{10,128})/i);
|
|
30
|
+
if (fileKeyFromPath)
|
|
31
|
+
fileKey = fileKeyFromPath[1];
|
|
32
|
+
}
|
|
33
|
+
if (!fileKey)
|
|
34
|
+
return null;
|
|
35
|
+
let nodeId;
|
|
36
|
+
try {
|
|
37
|
+
const parsed = new URL(trimmed);
|
|
38
|
+
const nodeIdParam = parsed.searchParams.get("node-id");
|
|
39
|
+
if (nodeIdParam) {
|
|
40
|
+
// Figma uses 0-1 or 0:1 format; plugin API expects "0:1"
|
|
41
|
+
nodeId = nodeIdParam.replace(/-/g, ":");
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// URL constructor failed, ignore query parsing
|
|
46
|
+
}
|
|
47
|
+
return { fileKey, nodeId };
|
|
48
|
+
}
|
|
@@ -3,56 +3,61 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Implements the same interface as FigmaDesktopConnector but talks to the
|
|
5
5
|
* Figma plugin over WebSocket (PluginBridgeServer). No CDP / debug port needed.
|
|
6
|
+
* Supports optional fileKey routing for multi-client scenarios.
|
|
6
7
|
*/
|
|
7
8
|
import { logger } from "./logger.js";
|
|
8
9
|
export class PluginBridgeConnector {
|
|
9
|
-
constructor(bridge) {
|
|
10
|
+
constructor(bridge, fileKey) {
|
|
10
11
|
this.bridge = bridge;
|
|
12
|
+
this.fileKey = fileKey;
|
|
13
|
+
}
|
|
14
|
+
setFileKey(fileKey) {
|
|
15
|
+
this.fileKey = fileKey;
|
|
11
16
|
}
|
|
12
17
|
async initialize() {
|
|
13
18
|
logger.info("Plugin bridge connector initialized (no CDP)");
|
|
14
19
|
}
|
|
15
20
|
async getVariablesFromPluginUI(fileKey) {
|
|
16
|
-
return this.bridge.request("getVariablesFromPluginUI", { fileKey });
|
|
21
|
+
return this.bridge.request("getVariablesFromPluginUI", { fileKey }, this.fileKey ?? fileKey);
|
|
17
22
|
}
|
|
18
23
|
async getComponentFromPluginUI(nodeId) {
|
|
19
|
-
return this.bridge.request("getComponentFromPluginUI", { nodeId });
|
|
24
|
+
return this.bridge.request("getComponentFromPluginUI", { nodeId }, this.fileKey);
|
|
20
25
|
}
|
|
21
26
|
async getVariables(fileKey) {
|
|
22
|
-
return this.bridge.request("getVariables", { fileKey });
|
|
27
|
+
return this.bridge.request("getVariables", { fileKey }, this.fileKey ?? fileKey);
|
|
23
28
|
}
|
|
24
29
|
async getComponentByNodeId(nodeId) {
|
|
25
|
-
return this.bridge.request("getComponentByNodeId", { nodeId });
|
|
30
|
+
return this.bridge.request("getComponentByNodeId", { nodeId }, this.fileKey);
|
|
26
31
|
}
|
|
27
32
|
async executeCodeViaUI(code, timeout = 5000) {
|
|
28
|
-
return this.bridge.request("executeCodeViaUI", { code, timeout });
|
|
33
|
+
return this.bridge.request("executeCodeViaUI", { code, timeout }, this.fileKey);
|
|
29
34
|
}
|
|
30
35
|
async updateVariable(variableId, modeId, value) {
|
|
31
|
-
return this.bridge.request("updateVariable", { variableId, modeId, value });
|
|
36
|
+
return this.bridge.request("updateVariable", { variableId, modeId, value }, this.fileKey);
|
|
32
37
|
}
|
|
33
38
|
async createVariable(name, collectionId, resolvedType, options) {
|
|
34
|
-
return this.bridge.request("createVariable", { name, collectionId, resolvedType, options });
|
|
39
|
+
return this.bridge.request("createVariable", { name, collectionId, resolvedType, options }, this.fileKey);
|
|
35
40
|
}
|
|
36
41
|
async createVariableCollection(name, options) {
|
|
37
|
-
return this.bridge.request("createVariableCollection", { name, options });
|
|
42
|
+
return this.bridge.request("createVariableCollection", { name, options }, this.fileKey);
|
|
38
43
|
}
|
|
39
44
|
async deleteVariable(variableId) {
|
|
40
|
-
return this.bridge.request("deleteVariable", { variableId });
|
|
45
|
+
return this.bridge.request("deleteVariable", { variableId }, this.fileKey);
|
|
41
46
|
}
|
|
42
47
|
async deleteVariableCollection(collectionId) {
|
|
43
|
-
return this.bridge.request("deleteVariableCollection", { collectionId });
|
|
48
|
+
return this.bridge.request("deleteVariableCollection", { collectionId }, this.fileKey);
|
|
44
49
|
}
|
|
45
50
|
async renameVariable(variableId, newName) {
|
|
46
|
-
return this.bridge.request("renameVariable", { variableId, newName });
|
|
51
|
+
return this.bridge.request("renameVariable", { variableId, newName }, this.fileKey);
|
|
47
52
|
}
|
|
48
53
|
async addMode(collectionId, modeName) {
|
|
49
|
-
return this.bridge.request("addMode", { collectionId, modeName });
|
|
54
|
+
return this.bridge.request("addMode", { collectionId, modeName }, this.fileKey);
|
|
50
55
|
}
|
|
51
56
|
async renameMode(collectionId, modeId, newName) {
|
|
52
|
-
return this.bridge.request("renameMode", { collectionId, modeId, newName });
|
|
57
|
+
return this.bridge.request("renameMode", { collectionId, modeId, newName }, this.fileKey);
|
|
53
58
|
}
|
|
54
59
|
async refreshVariables() {
|
|
55
|
-
return this.bridge.request("refreshVariables", {});
|
|
60
|
+
return this.bridge.request("refreshVariables", {}, this.fileKey);
|
|
56
61
|
}
|
|
57
62
|
async getLocalComponents(opts) {
|
|
58
63
|
const params = {};
|
|
@@ -60,64 +65,66 @@ export class PluginBridgeConnector {
|
|
|
60
65
|
params.currentPageOnly = opts.currentPageOnly;
|
|
61
66
|
if (opts?.limit != null && opts.limit > 0)
|
|
62
67
|
params.limit = opts.limit;
|
|
63
|
-
return this.bridge.request("getLocalComponents", params);
|
|
68
|
+
return this.bridge.request("getLocalComponents", params, this.fileKey);
|
|
64
69
|
}
|
|
65
70
|
async instantiateComponent(componentKey, options) {
|
|
66
|
-
return this.bridge.request("instantiateComponent", { componentKey, options });
|
|
71
|
+
return this.bridge.request("instantiateComponent", { componentKey, options }, this.fileKey);
|
|
67
72
|
}
|
|
68
73
|
async setNodeDescription(nodeId, description, descriptionMarkdown) {
|
|
69
|
-
return this.bridge.request("setNodeDescription", { nodeId, description, descriptionMarkdown });
|
|
74
|
+
return this.bridge.request("setNodeDescription", { nodeId, description, descriptionMarkdown }, this.fileKey);
|
|
70
75
|
}
|
|
71
76
|
async addComponentProperty(nodeId, propertyName, type, defaultValue, options) {
|
|
72
|
-
return this.bridge.request("addComponentProperty", { nodeId, propertyName, type, defaultValue, options });
|
|
77
|
+
return this.bridge.request("addComponentProperty", { nodeId, propertyName, type, defaultValue, options }, this.fileKey);
|
|
73
78
|
}
|
|
74
79
|
async editComponentProperty(nodeId, propertyName, newValue) {
|
|
75
|
-
return this.bridge.request("editComponentProperty", { nodeId, propertyName, newValue });
|
|
80
|
+
return this.bridge.request("editComponentProperty", { nodeId, propertyName, newValue }, this.fileKey);
|
|
76
81
|
}
|
|
77
82
|
async deleteComponentProperty(nodeId, propertyName) {
|
|
78
|
-
return this.bridge.request("deleteComponentProperty", { nodeId, propertyName });
|
|
83
|
+
return this.bridge.request("deleteComponentProperty", { nodeId, propertyName }, this.fileKey);
|
|
79
84
|
}
|
|
80
85
|
async resizeNode(nodeId, width, height, withConstraints = true) {
|
|
81
|
-
return this.bridge.request("resizeNode", { nodeId, width, height, withConstraints });
|
|
86
|
+
return this.bridge.request("resizeNode", { nodeId, width, height, withConstraints }, this.fileKey);
|
|
82
87
|
}
|
|
83
88
|
async moveNode(nodeId, x, y) {
|
|
84
|
-
return this.bridge.request("moveNode", { nodeId, x, y });
|
|
89
|
+
return this.bridge.request("moveNode", { nodeId, x, y }, this.fileKey);
|
|
85
90
|
}
|
|
86
91
|
async setNodeFills(nodeId, fills) {
|
|
87
|
-
return this.bridge.request("setNodeFills", { nodeId, fills });
|
|
92
|
+
return this.bridge.request("setNodeFills", { nodeId, fills }, this.fileKey);
|
|
88
93
|
}
|
|
89
94
|
async setNodeStrokes(nodeId, strokes, strokeWeight) {
|
|
90
|
-
return this.bridge.request("setNodeStrokes", { nodeId, strokes, strokeWeight });
|
|
95
|
+
return this.bridge.request("setNodeStrokes", { nodeId, strokes, strokeWeight }, this.fileKey);
|
|
91
96
|
}
|
|
92
97
|
async setNodeOpacity(nodeId, opacity) {
|
|
93
|
-
return this.bridge.request("setNodeOpacity", { nodeId, opacity });
|
|
98
|
+
return this.bridge.request("setNodeOpacity", { nodeId, opacity }, this.fileKey);
|
|
94
99
|
}
|
|
95
100
|
async setNodeCornerRadius(nodeId, radius) {
|
|
96
|
-
return this.bridge.request("setNodeCornerRadius", { nodeId, radius });
|
|
101
|
+
return this.bridge.request("setNodeCornerRadius", { nodeId, radius }, this.fileKey);
|
|
97
102
|
}
|
|
98
103
|
async cloneNode(nodeId) {
|
|
99
|
-
return this.bridge.request("cloneNode", { nodeId });
|
|
104
|
+
return this.bridge.request("cloneNode", { nodeId }, this.fileKey);
|
|
100
105
|
}
|
|
101
106
|
async deleteNode(nodeId) {
|
|
102
|
-
return this.bridge.request("deleteNode", { nodeId });
|
|
107
|
+
return this.bridge.request("deleteNode", { nodeId }, this.fileKey);
|
|
103
108
|
}
|
|
104
109
|
async renameNode(nodeId, newName) {
|
|
105
|
-
return this.bridge.request("renameNode", { nodeId, newName });
|
|
110
|
+
return this.bridge.request("renameNode", { nodeId, newName }, this.fileKey);
|
|
106
111
|
}
|
|
107
112
|
async setTextContent(nodeId, text, options) {
|
|
108
|
-
return this.bridge.request("setTextContent", { nodeId, text, options });
|
|
113
|
+
return this.bridge.request("setTextContent", { nodeId, text, options }, this.fileKey);
|
|
109
114
|
}
|
|
110
115
|
async createChildNode(parentId, nodeType, properties) {
|
|
111
|
-
return this.bridge.request("createChildNode", { parentId, nodeType, properties });
|
|
116
|
+
return this.bridge.request("createChildNode", { parentId, nodeType, properties }, this.fileKey);
|
|
112
117
|
}
|
|
113
118
|
async captureScreenshot(nodeId, options) {
|
|
114
|
-
return this.bridge.request("captureScreenshot", { nodeId, options });
|
|
119
|
+
return this.bridge.request("captureScreenshot", { nodeId, options }, this.fileKey);
|
|
115
120
|
}
|
|
116
121
|
async setInstanceProperties(nodeId, properties) {
|
|
117
|
-
return this.bridge.request("setInstanceProperties", { nodeId, properties });
|
|
122
|
+
return this.bridge.request("setInstanceProperties", { nodeId, properties }, this.fileKey);
|
|
118
123
|
}
|
|
119
124
|
async getDocumentStructure(depth, verbosity, opts) {
|
|
120
125
|
const params = { depth: depth ?? 1, verbosity: verbosity ?? "summary" };
|
|
126
|
+
if (opts?.excludeScreenshot !== undefined)
|
|
127
|
+
params.excludeScreenshot = opts.excludeScreenshot;
|
|
121
128
|
if (opts?.includeLayout !== undefined)
|
|
122
129
|
params.includeLayout = opts.includeLayout;
|
|
123
130
|
if (opts?.includeVisual !== undefined)
|
|
@@ -128,7 +135,7 @@ export class PluginBridgeConnector {
|
|
|
128
135
|
params.includeCodeReady = opts.includeCodeReady;
|
|
129
136
|
if (opts?.outputHint !== undefined)
|
|
130
137
|
params.outputHint = opts.outputHint;
|
|
131
|
-
return this.bridge.request("getDocumentStructure", params);
|
|
138
|
+
return this.bridge.request("getDocumentStructure", params, this.fileKey);
|
|
132
139
|
}
|
|
133
140
|
async getNodeContext(nodeId, depth, verbosity, opts) {
|
|
134
141
|
const params = {
|
|
@@ -136,6 +143,8 @@ export class PluginBridgeConnector {
|
|
|
136
143
|
depth: depth ?? 2,
|
|
137
144
|
verbosity: verbosity ?? "standard",
|
|
138
145
|
};
|
|
146
|
+
if (opts?.excludeScreenshot !== undefined)
|
|
147
|
+
params.excludeScreenshot = opts.excludeScreenshot;
|
|
139
148
|
if (opts?.includeLayout !== undefined)
|
|
140
149
|
params.includeLayout = opts.includeLayout;
|
|
141
150
|
if (opts?.includeVisual !== undefined)
|
|
@@ -146,24 +155,24 @@ export class PluginBridgeConnector {
|
|
|
146
155
|
params.includeCodeReady = opts.includeCodeReady;
|
|
147
156
|
if (opts?.outputHint !== undefined)
|
|
148
157
|
params.outputHint = opts.outputHint;
|
|
149
|
-
return this.bridge.request("getNodeContext", params);
|
|
158
|
+
return this.bridge.request("getNodeContext", params, this.fileKey);
|
|
150
159
|
}
|
|
151
160
|
async getLocalStyles(verbosity) {
|
|
152
|
-
return this.bridge.request("getLocalStyles", { verbosity: verbosity ?? "summary" });
|
|
161
|
+
return this.bridge.request("getLocalStyles", { verbosity: verbosity ?? "summary" }, this.fileKey);
|
|
153
162
|
}
|
|
154
163
|
async getConsoleLogs(limit = 50) {
|
|
155
|
-
const res = await this.bridge.request("getConsoleLogs", { limit });
|
|
164
|
+
const res = await this.bridge.request("getConsoleLogs", { limit }, this.fileKey);
|
|
156
165
|
return res?.data ?? { logs: [], total: 0 };
|
|
157
166
|
}
|
|
158
167
|
async clearConsole() {
|
|
159
|
-
await this.bridge.request("clearConsole", {});
|
|
168
|
+
await this.bridge.request("clearConsole", {}, this.fileKey);
|
|
160
169
|
}
|
|
161
170
|
async batchCreateVariables(items) {
|
|
162
|
-
const res = await this.bridge.request("batchCreateVariables", { items });
|
|
171
|
+
const res = await this.bridge.request("batchCreateVariables", { items }, this.fileKey);
|
|
163
172
|
return res ?? { created: [], failed: [] };
|
|
164
173
|
}
|
|
165
174
|
async batchUpdateVariables(items) {
|
|
166
|
-
const res = await this.bridge.request("batchUpdateVariables", { items });
|
|
175
|
+
const res = await this.bridge.request("batchUpdateVariables", { items }, this.fileKey);
|
|
167
176
|
return res ?? { updated: [], failed: [] };
|
|
168
177
|
}
|
|
169
178
|
async setupDesignTokens(payload) {
|
|
@@ -176,10 +185,10 @@ export class PluginBridgeConnector {
|
|
|
176
185
|
collectionName: payload.collectionName,
|
|
177
186
|
modes: payload.modes,
|
|
178
187
|
tokens,
|
|
179
|
-
});
|
|
188
|
+
}, this.fileKey);
|
|
180
189
|
}
|
|
181
190
|
async arrangeComponentSet(nodeIds) {
|
|
182
|
-
const res = (await this.bridge.request("arrangeComponentSet", { nodeIds }));
|
|
191
|
+
const res = (await this.bridge.request("arrangeComponentSet", { nodeIds }, this.fileKey));
|
|
183
192
|
return res?.data ?? res ?? { nodeId: "", name: "" };
|
|
184
193
|
}
|
|
185
194
|
async dispose() {
|