@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.
Files changed (33) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/README.md +99 -9
  3. package/dist/cloudflare/cloud-cors.js +40 -0
  4. package/dist/cloudflare/cloud-mode-kv.js +86 -0
  5. package/dist/cloudflare/cloud-mode-routes.js +97 -0
  6. package/dist/cloudflare/cloud-relay-session.js +141 -0
  7. package/dist/cloudflare/core/config.js +1 -1
  8. package/dist/cloudflare/core/figma-url.js +48 -0
  9. package/dist/cloudflare/core/plugin-bridge-connector.js +52 -43
  10. package/dist/cloudflare/core/plugin-bridge-server.js +211 -87
  11. package/dist/cloudflare/index.js +243 -4
  12. package/dist/core/config.js +1 -1
  13. package/dist/core/config.js.map +1 -1
  14. package/dist/core/figma-url.d.ts +10 -0
  15. package/dist/core/figma-url.d.ts.map +1 -0
  16. package/dist/core/figma-url.js +49 -0
  17. package/dist/core/figma-url.js.map +1 -0
  18. package/dist/core/plugin-bridge-connector.d.ts +6 -1
  19. package/dist/core/plugin-bridge-connector.d.ts.map +1 -1
  20. package/dist/core/plugin-bridge-connector.js +52 -43
  21. package/dist/core/plugin-bridge-connector.js.map +1 -1
  22. package/dist/core/plugin-bridge-server.d.ts +47 -14
  23. package/dist/core/plugin-bridge-server.d.ts.map +1 -1
  24. package/dist/core/plugin-bridge-server.js +211 -87
  25. package/dist/core/plugin-bridge-server.js.map +1 -1
  26. package/dist/local-plugin-only.d.ts.map +1 -1
  27. package/dist/local-plugin-only.js +163 -43
  28. package/dist/local-plugin-only.js.map +1 -1
  29. package/f-mcp-plugin/README.md +13 -5
  30. package/f-mcp-plugin/code.js +216 -2
  31. package/f-mcp-plugin/manifest.json +6 -2
  32. package/f-mcp-plugin/ui.html +694 -213
  33. 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() {