@muloka/figma-console-mcp 0.5.2 → 0.5.4
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 +5 -4
- package/dist/apps/design-system-dashboard/mcp-app.html +521 -0
- package/dist/apps/token-browser/mcp-app.html +410 -0
- package/dist/cloudflare/apps/design-system-dashboard/scoring/accessibility.js +277 -0
- package/dist/cloudflare/apps/design-system-dashboard/scoring/component-metadata.js +357 -0
- package/dist/cloudflare/apps/design-system-dashboard/scoring/consistency.js +341 -0
- package/dist/cloudflare/apps/design-system-dashboard/scoring/coverage.js +230 -0
- package/dist/cloudflare/apps/design-system-dashboard/scoring/engine.js +92 -0
- package/dist/cloudflare/apps/design-system-dashboard/scoring/naming-semantics.js +308 -0
- package/dist/cloudflare/apps/design-system-dashboard/scoring/token-architecture.js +349 -0
- package/dist/cloudflare/apps/design-system-dashboard/scoring/types.js +40 -0
- package/dist/cloudflare/apps/design-system-dashboard/server.js +159 -0
- package/dist/cloudflare/apps/token-browser/server.js +136 -0
- package/dist/cloudflare/browser-manager.js +157 -0
- package/dist/cloudflare/core/accessibility-tools.js +361 -0
- package/dist/cloudflare/core/annotation-tools.js +230 -0
- package/dist/cloudflare/core/cloud-websocket-connector.js +419 -0
- package/dist/cloudflare/core/cloud-websocket-relay.js +198 -0
- package/dist/cloudflare/core/comment-tools.js +292 -0
- package/dist/cloudflare/core/config.js +153 -0
- package/dist/cloudflare/core/console-monitor.js +427 -0
- package/dist/cloudflare/core/deep-component-tools.js +128 -0
- package/dist/cloudflare/core/design-code-tools.js +2793 -0
- package/dist/cloudflare/core/design-system-manifest.js +265 -0
- package/dist/cloudflare/core/design-system-tools.js +882 -0
- package/dist/cloudflare/core/diagnose-tool.js +100 -0
- package/dist/cloudflare/core/diff/changelog-formatter.js +275 -0
- package/dist/cloudflare/core/diff/diff-engine.js +334 -0
- package/dist/cloudflare/core/diff/property-compare.js +36 -0
- package/dist/cloudflare/core/diff/version-cache.js +74 -0
- package/dist/cloudflare/core/enrichment/enrichment-service.js +374 -0
- package/dist/cloudflare/core/enrichment/index.js +7 -0
- package/dist/cloudflare/core/enrichment/relationship-mapper.js +351 -0
- package/dist/cloudflare/core/enrichment/style-resolver.js +346 -0
- package/dist/cloudflare/core/figjam-tools.js +547 -0
- package/dist/cloudflare/core/figma-api.js +509 -0
- package/dist/cloudflare/core/figma-connector.js +7 -0
- package/dist/cloudflare/core/figma-reconstruction-spec.js +402 -0
- package/dist/cloudflare/core/figma-style-extractor.js +311 -0
- package/dist/cloudflare/core/figma-tools.js +3353 -0
- package/dist/cloudflare/core/identity.js +96 -0
- package/dist/cloudflare/core/library-tools.js +580 -0
- package/dist/cloudflare/core/logger.js +53 -0
- package/dist/cloudflare/core/port-discovery.js +935 -0
- package/dist/cloudflare/core/resolve-package-root.js +11 -0
- package/dist/cloudflare/core/slides-tools.js +714 -0
- package/dist/cloudflare/core/slot-tools.js +334 -0
- package/dist/cloudflare/core/snippet-injector.js +96 -0
- package/dist/cloudflare/core/tokens/alias-resolver.js +205 -0
- package/dist/cloudflare/core/tokens/config.js +294 -0
- package/dist/cloudflare/core/tokens/dialect.js +232 -0
- package/dist/cloudflare/core/tokens/figma-converter.js +323 -0
- package/dist/cloudflare/core/tokens/formatters/css-vars.js +338 -0
- package/dist/cloudflare/core/tokens/formatters/dtcg.js +376 -0
- package/dist/cloudflare/core/tokens/formatters/index.js +45 -0
- package/dist/cloudflare/core/tokens/formatters/json.js +205 -0
- package/dist/cloudflare/core/tokens/formatters/less.js +4 -0
- package/dist/cloudflare/core/tokens/formatters/scss.js +258 -0
- package/dist/cloudflare/core/tokens/formatters/stubs.js +13 -0
- package/dist/cloudflare/core/tokens/formatters/style-dictionary-v3.js +213 -0
- package/dist/cloudflare/core/tokens/formatters/tailwind-v3.js +237 -0
- package/dist/cloudflare/core/tokens/formatters/tailwind-v4.js +335 -0
- package/dist/cloudflare/core/tokens/formatters/tokens-studio.js +256 -0
- package/dist/cloudflare/core/tokens/formatters/ts-module.js +198 -0
- package/dist/cloudflare/core/tokens/index.js +16 -0
- package/dist/cloudflare/core/tokens/parsers/css-vars.js +4 -0
- package/dist/cloudflare/core/tokens/parsers/dtcg.js +280 -0
- package/dist/cloudflare/core/tokens/parsers/index.js +138 -0
- package/dist/cloudflare/core/tokens/parsers/json.js +7 -0
- package/dist/cloudflare/core/tokens/parsers/scss.js +4 -0
- package/dist/cloudflare/core/tokens/parsers/stubs.js +20 -0
- package/dist/cloudflare/core/tokens/parsers/style-dictionary-v3.js +4 -0
- package/dist/cloudflare/core/tokens/parsers/tailwind-v3.js +4 -0
- package/dist/cloudflare/core/tokens/parsers/tailwind-v4.js +4 -0
- package/dist/cloudflare/core/tokens/parsers/tokens-studio.js +4 -0
- package/dist/cloudflare/core/tokens/schemas.js +152 -0
- package/dist/cloudflare/core/tokens/transforms/color.js +12 -0
- package/dist/cloudflare/core/tokens/transforms/index.js +29 -0
- package/dist/cloudflare/core/tokens/transforms/size.js +7 -0
- package/dist/cloudflare/core/tokens/types.js +18 -0
- package/dist/cloudflare/core/tokens-tools.js +1788 -0
- package/dist/cloudflare/core/types/design-code.js +4 -0
- package/dist/cloudflare/core/types/enriched.js +5 -0
- package/dist/cloudflare/core/types/index.js +4 -0
- package/dist/cloudflare/core/variable-resolver.js +85 -0
- package/dist/cloudflare/core/version-tools.js +1199 -0
- package/dist/cloudflare/core/websocket-connector.js +436 -0
- package/dist/cloudflare/core/websocket-server.js +1009 -0
- package/dist/cloudflare/core/write-tools.js +2506 -0
- package/dist/cloudflare/index.js +3010 -0
- package/dist/core/accessibility-tools.d.ts.map +1 -1
- package/dist/core/accessibility-tools.js +7 -6
- package/dist/core/accessibility-tools.js.map +1 -1
- package/dist/core/http-request-guard.d.ts +20 -0
- package/dist/core/http-request-guard.d.ts.map +1 -0
- package/dist/core/http-request-guard.js +61 -0
- package/dist/core/http-request-guard.js.map +1 -0
- package/dist/core/tokens/schemas.d.ts +6 -6
- package/dist/local.d.ts.map +1 -1
- package/dist/local.js +4 -0
- package/dist/local.js.map +1 -1
- package/figma-desktop-bridge/ui.html +7 -0
- package/package.json +2 -2
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloud WebSocket Connector
|
|
3
|
+
*
|
|
4
|
+
* Implements IFigmaConnector by routing commands through the PluginRelayDO
|
|
5
|
+
* Durable Object. Each method maps to a command sent via fetch() RPC to
|
|
6
|
+
* the relay, which forwards it to the Figma Desktop Bridge plugin over
|
|
7
|
+
* WebSocket.
|
|
8
|
+
*
|
|
9
|
+
* Structurally mirrors WebSocketConnector — same methods, different transport.
|
|
10
|
+
*/
|
|
11
|
+
export class CloudWebSocketConnector {
|
|
12
|
+
constructor(relayStub) {
|
|
13
|
+
this.relayStub = relayStub;
|
|
14
|
+
}
|
|
15
|
+
async initialize() {
|
|
16
|
+
const res = await this.relayStub.fetch('https://relay/relay/status');
|
|
17
|
+
const status = await res.json();
|
|
18
|
+
if (!status.connected) {
|
|
19
|
+
throw new Error('No plugin connected to cloud relay. User must pair the Desktop Bridge plugin first (use figma_pair_plugin tool).');
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
getTransportType() {
|
|
23
|
+
return 'websocket';
|
|
24
|
+
}
|
|
25
|
+
// ============================================================================
|
|
26
|
+
// Core execution
|
|
27
|
+
// ============================================================================
|
|
28
|
+
async executeInPluginContext(code) {
|
|
29
|
+
return this.sendCommand('EXECUTE_CODE', { code, timeout: 5000 }, 7000);
|
|
30
|
+
}
|
|
31
|
+
async getVariablesFromPluginUI(fileKey) {
|
|
32
|
+
return this.sendCommand('GET_VARIABLES_DATA', {}, 10000);
|
|
33
|
+
}
|
|
34
|
+
async getVariables(fileKey) {
|
|
35
|
+
// IMPORTANT: bare try/catch with top-level `return`, NO inner IIFE.
|
|
36
|
+
// See issue #68 + the matching note in websocket-connector.ts. The plugin
|
|
37
|
+
// (code.js) wraps every EXECUTE_CODE payload in its own async IIFE; nesting
|
|
38
|
+
// another swallows the inner return and silently drops the variables.
|
|
39
|
+
const code = `
|
|
40
|
+
try {
|
|
41
|
+
if (typeof figma === 'undefined') {
|
|
42
|
+
throw new Error('Figma API not available in this context');
|
|
43
|
+
}
|
|
44
|
+
const variables = await figma.variables.getLocalVariablesAsync();
|
|
45
|
+
const collections = await figma.variables.getLocalVariableCollectionsAsync();
|
|
46
|
+
return {
|
|
47
|
+
success: true,
|
|
48
|
+
timestamp: Date.now(),
|
|
49
|
+
fileMetadata: { fileName: figma.root.name, fileKey: figma.fileKey || null },
|
|
50
|
+
variables: variables.map(function(v) { return { id: v.id, name: v.name, key: v.key, resolvedType: v.resolvedType, valuesByMode: v.valuesByMode, variableCollectionId: v.variableCollectionId, scopes: v.scopes, description: v.description, hiddenFromPublishing: v.hiddenFromPublishing }; }),
|
|
51
|
+
variableCollections: collections.map(function(c) { return { id: c.id, name: c.name, key: c.key, modes: c.modes, defaultModeId: c.defaultModeId, variableIds: c.variableIds }; })
|
|
52
|
+
};
|
|
53
|
+
} catch (error) {
|
|
54
|
+
return { success: false, error: error.message };
|
|
55
|
+
}
|
|
56
|
+
`;
|
|
57
|
+
return this.sendCommand('EXECUTE_CODE', { code, timeout: 30000 }, 32000);
|
|
58
|
+
}
|
|
59
|
+
async executeCodeViaUI(code, timeoutMs = 5000) {
|
|
60
|
+
return this.sendCommand('EXECUTE_CODE', { code, timeout: timeoutMs }, timeoutMs + 2000);
|
|
61
|
+
}
|
|
62
|
+
// ============================================================================
|
|
63
|
+
// Variable operations
|
|
64
|
+
// ============================================================================
|
|
65
|
+
async updateVariable(variableId, modeId, value) {
|
|
66
|
+
return this.sendCommand('UPDATE_VARIABLE', { variableId, modeId, value });
|
|
67
|
+
}
|
|
68
|
+
async createVariable(name, collectionId, resolvedType, options) {
|
|
69
|
+
const params = { name, collectionId, resolvedType };
|
|
70
|
+
if (options) {
|
|
71
|
+
if (options.valuesByMode)
|
|
72
|
+
params.valuesByMode = options.valuesByMode;
|
|
73
|
+
if (options.description)
|
|
74
|
+
params.description = options.description;
|
|
75
|
+
if (options.scopes)
|
|
76
|
+
params.scopes = options.scopes;
|
|
77
|
+
}
|
|
78
|
+
return this.sendCommand('CREATE_VARIABLE', params);
|
|
79
|
+
}
|
|
80
|
+
async deleteVariable(variableId) {
|
|
81
|
+
return this.sendCommand('DELETE_VARIABLE', { variableId });
|
|
82
|
+
}
|
|
83
|
+
async refreshVariables() {
|
|
84
|
+
return this.sendCommand('REFRESH_VARIABLES', {}, 300000);
|
|
85
|
+
}
|
|
86
|
+
async renameVariable(variableId, newName) {
|
|
87
|
+
const result = await this.sendCommand('RENAME_VARIABLE', { variableId, newName });
|
|
88
|
+
if (!result.oldName && result.variable?.oldName)
|
|
89
|
+
result.oldName = result.variable.oldName;
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
async setVariableDescription(variableId, description) {
|
|
93
|
+
return this.sendCommand('SET_VARIABLE_DESCRIPTION', { variableId, description });
|
|
94
|
+
}
|
|
95
|
+
// ============================================================================
|
|
96
|
+
// Mode operations
|
|
97
|
+
// ============================================================================
|
|
98
|
+
async addMode(collectionId, modeName) {
|
|
99
|
+
return this.sendCommand('ADD_MODE', { collectionId, modeName });
|
|
100
|
+
}
|
|
101
|
+
async renameMode(collectionId, modeId, newName) {
|
|
102
|
+
const result = await this.sendCommand('RENAME_MODE', { collectionId, modeId, newName });
|
|
103
|
+
if (!result.oldName && result.collection?.oldName)
|
|
104
|
+
result.oldName = result.collection.oldName;
|
|
105
|
+
return result;
|
|
106
|
+
}
|
|
107
|
+
// ============================================================================
|
|
108
|
+
// Collection operations
|
|
109
|
+
// ============================================================================
|
|
110
|
+
async createVariableCollection(name, options) {
|
|
111
|
+
const params = { name };
|
|
112
|
+
if (options) {
|
|
113
|
+
if (options.initialModeName)
|
|
114
|
+
params.initialModeName = options.initialModeName;
|
|
115
|
+
if (options.additionalModes)
|
|
116
|
+
params.additionalModes = options.additionalModes;
|
|
117
|
+
}
|
|
118
|
+
return this.sendCommand('CREATE_VARIABLE_COLLECTION', params);
|
|
119
|
+
}
|
|
120
|
+
async deleteVariableCollection(collectionId) {
|
|
121
|
+
return this.sendCommand('DELETE_VARIABLE_COLLECTION', { collectionId });
|
|
122
|
+
}
|
|
123
|
+
// ============================================================================
|
|
124
|
+
// Component operations
|
|
125
|
+
// ============================================================================
|
|
126
|
+
async getComponentFromPluginUI(nodeId) {
|
|
127
|
+
return this.sendCommand('GET_COMPONENT', { nodeId }, 10000);
|
|
128
|
+
}
|
|
129
|
+
async getLocalComponents() {
|
|
130
|
+
return this.sendCommand('GET_LOCAL_COMPONENTS', {}, 300000);
|
|
131
|
+
}
|
|
132
|
+
async setNodeDescription(nodeId, description, descriptionMarkdown) {
|
|
133
|
+
return this.sendCommand('SET_NODE_DESCRIPTION', { nodeId, description, descriptionMarkdown });
|
|
134
|
+
}
|
|
135
|
+
// ============================================================================
|
|
136
|
+
// Annotation operations
|
|
137
|
+
// ============================================================================
|
|
138
|
+
async getAnnotations(nodeId, includeChildren, depth) {
|
|
139
|
+
return this.sendCommand('GET_ANNOTATIONS', { nodeId, includeChildren, depth }, 10000);
|
|
140
|
+
}
|
|
141
|
+
async setAnnotations(nodeId, annotations, mode) {
|
|
142
|
+
return this.sendCommand('SET_ANNOTATIONS', { nodeId, annotations, mode: mode || 'replace' });
|
|
143
|
+
}
|
|
144
|
+
async getAnnotationCategories() {
|
|
145
|
+
return this.sendCommand('GET_ANNOTATION_CATEGORIES', {}, 5000);
|
|
146
|
+
}
|
|
147
|
+
async deepGetComponent(nodeId, depth) {
|
|
148
|
+
return this.sendCommand('DEEP_GET_COMPONENT', { nodeId, depth: depth || 10 }, 30000);
|
|
149
|
+
}
|
|
150
|
+
async analyzeComponentSet(nodeId) {
|
|
151
|
+
return this.sendCommand('ANALYZE_COMPONENT_SET', { nodeId }, 30000);
|
|
152
|
+
}
|
|
153
|
+
async addComponentProperty(nodeId, propertyName, type, defaultValue, options) {
|
|
154
|
+
const params = { nodeId, propertyName, propertyType: type, defaultValue };
|
|
155
|
+
if (options?.preferredValues)
|
|
156
|
+
params.preferredValues = options.preferredValues;
|
|
157
|
+
if (options?.description)
|
|
158
|
+
params.description = options.description;
|
|
159
|
+
return this.sendCommand('ADD_COMPONENT_PROPERTY', params);
|
|
160
|
+
}
|
|
161
|
+
async editComponentProperty(nodeId, propertyName, newValue) {
|
|
162
|
+
return this.sendCommand('EDIT_COMPONENT_PROPERTY', { nodeId, propertyName, newValue });
|
|
163
|
+
}
|
|
164
|
+
async deleteComponentProperty(nodeId, propertyName) {
|
|
165
|
+
return this.sendCommand('DELETE_COMPONENT_PROPERTY', { nodeId, propertyName });
|
|
166
|
+
}
|
|
167
|
+
async instantiateComponent(componentKey, options) {
|
|
168
|
+
const params = { componentKey };
|
|
169
|
+
if (options) {
|
|
170
|
+
if (options.nodeId)
|
|
171
|
+
params.nodeId = options.nodeId;
|
|
172
|
+
if (options.position)
|
|
173
|
+
params.position = options.position;
|
|
174
|
+
if (options.size)
|
|
175
|
+
params.size = options.size;
|
|
176
|
+
if (options.overrides)
|
|
177
|
+
params.overrides = options.overrides;
|
|
178
|
+
if (options.variant)
|
|
179
|
+
params.variant = options.variant;
|
|
180
|
+
if (options.parentId)
|
|
181
|
+
params.parentId = options.parentId;
|
|
182
|
+
}
|
|
183
|
+
return this.sendCommand('INSTANTIATE_COMPONENT', params);
|
|
184
|
+
}
|
|
185
|
+
async createComponentSet(params) {
|
|
186
|
+
// Timeout scales with variant count — the plugin builds all variants in
|
|
187
|
+
// one uncancellable pass, so a fixed 30s ceiling on big matrices reports
|
|
188
|
+
// failure while the set still gets created (retry → duplicates).
|
|
189
|
+
// Mirrors componentSetTimeoutMs in websocket-connector.ts (not imported:
|
|
190
|
+
// that module pulls the Node 'ws' stack into the Workers bundle).
|
|
191
|
+
let variantCount = 1;
|
|
192
|
+
if (params.componentIds?.length) {
|
|
193
|
+
variantCount = params.componentIds.length;
|
|
194
|
+
}
|
|
195
|
+
else if (params.properties) {
|
|
196
|
+
for (const values of Object.values(params.properties)) {
|
|
197
|
+
variantCount *= Math.max(1, values?.length ?? 1);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
const timeout = Math.min(120000, Math.max(30000, variantCount * 1200)) + 5000;
|
|
201
|
+
return this.sendCommand('CREATE_COMPONENT_SET', params, timeout);
|
|
202
|
+
}
|
|
203
|
+
// ============================================================================
|
|
204
|
+
// Slot operations
|
|
205
|
+
// ============================================================================
|
|
206
|
+
async createSlot(nodeId, options) {
|
|
207
|
+
return this.sendCommand('CREATE_SLOT', { nodeId, ...options });
|
|
208
|
+
}
|
|
209
|
+
async getSlots(nodeId) {
|
|
210
|
+
return this.sendCommand('GET_SLOTS', { nodeId });
|
|
211
|
+
}
|
|
212
|
+
async appendToSlot(params) {
|
|
213
|
+
return this.sendCommand('APPEND_TO_SLOT', params);
|
|
214
|
+
}
|
|
215
|
+
async resetSlot(params) {
|
|
216
|
+
return this.sendCommand('RESET_SLOT', params);
|
|
217
|
+
}
|
|
218
|
+
// ============================================================================
|
|
219
|
+
// Node manipulation
|
|
220
|
+
// ============================================================================
|
|
221
|
+
async resizeNode(nodeId, width, height, withConstraints = true) {
|
|
222
|
+
return this.sendCommand('RESIZE_NODE', { nodeId, width, height, withConstraints });
|
|
223
|
+
}
|
|
224
|
+
async moveNode(nodeId, x, y) {
|
|
225
|
+
return this.sendCommand('MOVE_NODE', { nodeId, x, y });
|
|
226
|
+
}
|
|
227
|
+
async setNodeFills(nodeId, fills) {
|
|
228
|
+
return this.sendCommand('SET_NODE_FILLS', { nodeId, fills });
|
|
229
|
+
}
|
|
230
|
+
async setNodeStrokes(nodeId, strokes, strokeWeight) {
|
|
231
|
+
const params = { nodeId, strokes };
|
|
232
|
+
if (strokeWeight !== undefined)
|
|
233
|
+
params.strokeWeight = strokeWeight;
|
|
234
|
+
return this.sendCommand('SET_NODE_STROKES', params);
|
|
235
|
+
}
|
|
236
|
+
async setNodeOpacity(nodeId, opacity) {
|
|
237
|
+
return this.sendCommand('SET_NODE_OPACITY', { nodeId, opacity });
|
|
238
|
+
}
|
|
239
|
+
async setNodeCornerRadius(nodeId, radius) {
|
|
240
|
+
return this.sendCommand('SET_NODE_CORNER_RADIUS', { nodeId, radius });
|
|
241
|
+
}
|
|
242
|
+
async cloneNode(nodeId) {
|
|
243
|
+
return this.sendCommand('CLONE_NODE', { nodeId });
|
|
244
|
+
}
|
|
245
|
+
async deleteNode(nodeId) {
|
|
246
|
+
return this.sendCommand('DELETE_NODE', { nodeId });
|
|
247
|
+
}
|
|
248
|
+
async renameNode(nodeId, newName) {
|
|
249
|
+
return this.sendCommand('RENAME_NODE', { nodeId, newName });
|
|
250
|
+
}
|
|
251
|
+
async setTextContent(nodeId, characters, options) {
|
|
252
|
+
const params = { nodeId, text: characters };
|
|
253
|
+
if (options) {
|
|
254
|
+
if (options.fontSize)
|
|
255
|
+
params.fontSize = options.fontSize;
|
|
256
|
+
if (options.fontWeight)
|
|
257
|
+
params.fontWeight = options.fontWeight;
|
|
258
|
+
if (options.fontFamily)
|
|
259
|
+
params.fontFamily = options.fontFamily;
|
|
260
|
+
if (options.fontStyle)
|
|
261
|
+
params.fontStyle = options.fontStyle;
|
|
262
|
+
}
|
|
263
|
+
return this.sendCommand('SET_TEXT_CONTENT', params);
|
|
264
|
+
}
|
|
265
|
+
async createChildNode(parentId, nodeType, properties) {
|
|
266
|
+
return this.sendCommand('CREATE_CHILD_NODE', { parentId, nodeType, properties: properties || {} });
|
|
267
|
+
}
|
|
268
|
+
// ============================================================================
|
|
269
|
+
// Screenshot & instance properties
|
|
270
|
+
// ============================================================================
|
|
271
|
+
async captureScreenshot(nodeId, options) {
|
|
272
|
+
const params = { nodeId };
|
|
273
|
+
if (options?.format)
|
|
274
|
+
params.format = options.format;
|
|
275
|
+
if (options?.scale)
|
|
276
|
+
params.scale = options.scale;
|
|
277
|
+
return this.sendCommand('CAPTURE_SCREENSHOT', params, 30000);
|
|
278
|
+
}
|
|
279
|
+
async setInstanceProperties(nodeId, properties) {
|
|
280
|
+
return this.sendCommand('SET_INSTANCE_PROPERTIES', { nodeId, properties });
|
|
281
|
+
}
|
|
282
|
+
// ============================================================================
|
|
283
|
+
// Image fill
|
|
284
|
+
// ============================================================================
|
|
285
|
+
async setImageFill(nodeIds, imageData, scaleMode = 'FILL') {
|
|
286
|
+
return this.sendCommand('SET_IMAGE_FILL', { nodeIds, imageData, scaleMode }, 60000);
|
|
287
|
+
}
|
|
288
|
+
// ============================================================================
|
|
289
|
+
// Design lint
|
|
290
|
+
// ============================================================================
|
|
291
|
+
async lintDesign(nodeId, rules, maxDepth, maxFindings) {
|
|
292
|
+
const params = {};
|
|
293
|
+
if (nodeId)
|
|
294
|
+
params.nodeId = nodeId;
|
|
295
|
+
if (rules)
|
|
296
|
+
params.rules = rules;
|
|
297
|
+
if (maxDepth !== undefined)
|
|
298
|
+
params.maxDepth = maxDepth;
|
|
299
|
+
if (maxFindings !== undefined)
|
|
300
|
+
params.maxFindings = maxFindings;
|
|
301
|
+
return this.sendCommand('LINT_DESIGN', params, 120000);
|
|
302
|
+
}
|
|
303
|
+
// ============================================================================
|
|
304
|
+
// Component accessibility audit
|
|
305
|
+
// ============================================================================
|
|
306
|
+
async auditComponentAccessibility(nodeId, targetSize) {
|
|
307
|
+
const params = {};
|
|
308
|
+
if (nodeId)
|
|
309
|
+
params.nodeId = nodeId;
|
|
310
|
+
if (targetSize !== undefined)
|
|
311
|
+
params.targetSize = targetSize;
|
|
312
|
+
return this.sendCommand('AUDIT_COMPONENT_ACCESSIBILITY', params, 120000);
|
|
313
|
+
}
|
|
314
|
+
// ============================================================================
|
|
315
|
+
// FigJam operations
|
|
316
|
+
// ============================================================================
|
|
317
|
+
async createSticky(params) {
|
|
318
|
+
return this.sendCommand('CREATE_STICKY', params);
|
|
319
|
+
}
|
|
320
|
+
async createStickies(params) {
|
|
321
|
+
return this.sendCommand('CREATE_STICKIES', params, 30000);
|
|
322
|
+
}
|
|
323
|
+
async createConnector(params) {
|
|
324
|
+
return this.sendCommand('CREATE_CONNECTOR', params);
|
|
325
|
+
}
|
|
326
|
+
async createShapeWithText(params) {
|
|
327
|
+
return this.sendCommand('CREATE_SHAPE_WITH_TEXT', params);
|
|
328
|
+
}
|
|
329
|
+
async createSection(params) {
|
|
330
|
+
return this.sendCommand('CREATE_SECTION', params);
|
|
331
|
+
}
|
|
332
|
+
async createTable(params) {
|
|
333
|
+
return this.sendCommand('CREATE_TABLE', params, 30000);
|
|
334
|
+
}
|
|
335
|
+
async createCodeBlock(params) {
|
|
336
|
+
return this.sendCommand('CREATE_CODE_BLOCK', params);
|
|
337
|
+
}
|
|
338
|
+
async getBoardContents(params) {
|
|
339
|
+
return this.sendCommand('GET_BOARD_CONTENTS', params, 30000);
|
|
340
|
+
}
|
|
341
|
+
async getConnections() {
|
|
342
|
+
return this.sendCommand('GET_CONNECTIONS', {}, 15000);
|
|
343
|
+
}
|
|
344
|
+
// ============================================================================
|
|
345
|
+
// Slides operations
|
|
346
|
+
// ============================================================================
|
|
347
|
+
async listSlides() {
|
|
348
|
+
return this.sendCommand('LIST_SLIDES', {}, 10000);
|
|
349
|
+
}
|
|
350
|
+
async getSlideContent(params) {
|
|
351
|
+
return this.sendCommand('GET_SLIDE_CONTENT', params, 10000);
|
|
352
|
+
}
|
|
353
|
+
async createSlide(params) {
|
|
354
|
+
return this.sendCommand('CREATE_SLIDE', params, 10000);
|
|
355
|
+
}
|
|
356
|
+
async deleteSlide(params) {
|
|
357
|
+
return this.sendCommand('DELETE_SLIDE', params, 5000);
|
|
358
|
+
}
|
|
359
|
+
async duplicateSlide(params) {
|
|
360
|
+
return this.sendCommand('DUPLICATE_SLIDE', params, 5000);
|
|
361
|
+
}
|
|
362
|
+
async getSlideGrid() {
|
|
363
|
+
return this.sendCommand('GET_SLIDE_GRID', {}, 10000);
|
|
364
|
+
}
|
|
365
|
+
async reorderSlides(params) {
|
|
366
|
+
return this.sendCommand('REORDER_SLIDES', params, 15000);
|
|
367
|
+
}
|
|
368
|
+
async setSlideTransition(params) {
|
|
369
|
+
return this.sendCommand('SET_SLIDE_TRANSITION', params, 5000);
|
|
370
|
+
}
|
|
371
|
+
async getSlideTransition(params) {
|
|
372
|
+
return this.sendCommand('GET_SLIDE_TRANSITION', params, 5000);
|
|
373
|
+
}
|
|
374
|
+
async setSlidesViewMode(params) {
|
|
375
|
+
return this.sendCommand('SET_SLIDES_VIEW_MODE', params, 5000);
|
|
376
|
+
}
|
|
377
|
+
async getFocusedSlide() {
|
|
378
|
+
return this.sendCommand('GET_FOCUSED_SLIDE', {}, 5000);
|
|
379
|
+
}
|
|
380
|
+
async focusSlide(params) {
|
|
381
|
+
return this.sendCommand('FOCUS_SLIDE', params, 5000);
|
|
382
|
+
}
|
|
383
|
+
async skipSlide(params) {
|
|
384
|
+
return this.sendCommand('SKIP_SLIDE', params, 5000);
|
|
385
|
+
}
|
|
386
|
+
async addTextToSlide(params) {
|
|
387
|
+
return this.sendCommand('ADD_TEXT_TO_SLIDE', params, 10000);
|
|
388
|
+
}
|
|
389
|
+
async addShapeToSlide(params) {
|
|
390
|
+
return this.sendCommand('ADD_SHAPE_TO_SLIDE', params, 5000);
|
|
391
|
+
}
|
|
392
|
+
async setSlideBackground(params) {
|
|
393
|
+
return this.sendCommand('SET_SLIDE_BACKGROUND', params, 5000);
|
|
394
|
+
}
|
|
395
|
+
async getTextStyles() {
|
|
396
|
+
return this.sendCommand('GET_TEXT_STYLES', {}, 5000);
|
|
397
|
+
}
|
|
398
|
+
// ============================================================================
|
|
399
|
+
// Cache management (no-op for cloud relay)
|
|
400
|
+
// ============================================================================
|
|
401
|
+
clearFrameCache() {
|
|
402
|
+
// No frame cache in cloud relay mode
|
|
403
|
+
}
|
|
404
|
+
// ============================================================================
|
|
405
|
+
// Transport — fetch-based RPC to the relay DO
|
|
406
|
+
// ============================================================================
|
|
407
|
+
async sendCommand(method, params = {}, timeoutMs = 15000) {
|
|
408
|
+
const res = await this.relayStub.fetch('https://relay/relay/command', {
|
|
409
|
+
method: 'POST',
|
|
410
|
+
headers: { 'Content-Type': 'application/json' },
|
|
411
|
+
body: JSON.stringify({ method, params, timeoutMs }),
|
|
412
|
+
});
|
|
413
|
+
const data = await res.json();
|
|
414
|
+
if (data.error) {
|
|
415
|
+
throw new Error(data.error);
|
|
416
|
+
}
|
|
417
|
+
return data.result;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloud WebSocket Relay — Durable Object
|
|
3
|
+
*
|
|
4
|
+
* Bridges the Figma Desktop Bridge plugin to the cloud MCP server.
|
|
5
|
+
* The plugin connects via WebSocket (hibernation-aware); the MCP DO
|
|
6
|
+
* sends commands via fetch() RPC and receives responses.
|
|
7
|
+
*
|
|
8
|
+
* IMPORTANT: Uses hibernation-safe patterns throughout:
|
|
9
|
+
* - WebSocket retrieved via this.ctx.getWebSockets() (not class property)
|
|
10
|
+
* - File info persisted in DO storage (not in-memory)
|
|
11
|
+
* - Pending requests kept in-memory (safe: fetch keeps DO alive)
|
|
12
|
+
*
|
|
13
|
+
* Routes:
|
|
14
|
+
* /ws/connect — WebSocket upgrade from plugin (paired via code)
|
|
15
|
+
* /relay/command — RPC from MCP DO → plugin (holds response open)
|
|
16
|
+
* /relay/status — Connection & file info query
|
|
17
|
+
*/
|
|
18
|
+
import { DurableObject } from 'cloudflare:workers';
|
|
19
|
+
/**
|
|
20
|
+
* Generate a 6-character alphanumeric pairing code (uppercase).
|
|
21
|
+
*/
|
|
22
|
+
export function generatePairingCode() {
|
|
23
|
+
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; // No 0/O/1/I confusion
|
|
24
|
+
let code = '';
|
|
25
|
+
const arr = new Uint8Array(6);
|
|
26
|
+
crypto.getRandomValues(arr);
|
|
27
|
+
for (let i = 0; i < 6; i++) {
|
|
28
|
+
code += chars[arr[i] % chars.length];
|
|
29
|
+
}
|
|
30
|
+
return code;
|
|
31
|
+
}
|
|
32
|
+
export class PluginRelayDO extends DurableObject {
|
|
33
|
+
constructor() {
|
|
34
|
+
super(...arguments);
|
|
35
|
+
this.pendingRequests = new Map();
|
|
36
|
+
this.requestIdCounter = 0;
|
|
37
|
+
}
|
|
38
|
+
// ======================================================================
|
|
39
|
+
// Hibernation-safe WebSocket retrieval
|
|
40
|
+
// ======================================================================
|
|
41
|
+
/**
|
|
42
|
+
* Get the active plugin WebSocket. Uses ctx.getWebSockets() which
|
|
43
|
+
* survives DO hibernation (unlike a class property reference).
|
|
44
|
+
*/
|
|
45
|
+
getPluginWs() {
|
|
46
|
+
const sockets = this.ctx.getWebSockets('plugin');
|
|
47
|
+
return sockets.length > 0 ? sockets[0] : null;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Incoming fetch handler — dispatches to routes.
|
|
51
|
+
*/
|
|
52
|
+
async fetch(request) {
|
|
53
|
+
const url = new URL(request.url);
|
|
54
|
+
if (url.pathname === '/ws/connect') {
|
|
55
|
+
return this.handlePluginConnect(request);
|
|
56
|
+
}
|
|
57
|
+
if (url.pathname === '/relay/command') {
|
|
58
|
+
return this.handleRelayCommand(request);
|
|
59
|
+
}
|
|
60
|
+
if (url.pathname === '/relay/status') {
|
|
61
|
+
return this.handleRelayStatus();
|
|
62
|
+
}
|
|
63
|
+
return new Response('Not found', { status: 404 });
|
|
64
|
+
}
|
|
65
|
+
// ==========================================================================
|
|
66
|
+
// WebSocket — plugin connects here
|
|
67
|
+
// ==========================================================================
|
|
68
|
+
handlePluginConnect(request) {
|
|
69
|
+
const upgradeHeader = request.headers.get('Upgrade');
|
|
70
|
+
if (!upgradeHeader || upgradeHeader.toLowerCase() !== 'websocket') {
|
|
71
|
+
return new Response('Expected WebSocket upgrade', { status: 426 });
|
|
72
|
+
}
|
|
73
|
+
// Close any existing plugin connection (e.g., re-pairing)
|
|
74
|
+
const existing = this.getPluginWs();
|
|
75
|
+
if (existing) {
|
|
76
|
+
try {
|
|
77
|
+
existing.close(1000, 'Replaced by new connection');
|
|
78
|
+
}
|
|
79
|
+
catch { /* ignore */ }
|
|
80
|
+
}
|
|
81
|
+
const pair = new WebSocketPair();
|
|
82
|
+
const [client, server] = Object.values(pair);
|
|
83
|
+
// Accept with 'plugin' tag — this.ctx.getWebSockets('plugin') retrieves
|
|
84
|
+
// the socket even after the DO wakes from hibernation.
|
|
85
|
+
this.ctx.acceptWebSocket(server, ['plugin']);
|
|
86
|
+
return new Response(null, { status: 101, webSocket: client });
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Hibernation callback — incoming message from plugin WebSocket.
|
|
90
|
+
*/
|
|
91
|
+
async webSocketMessage(ws, message) {
|
|
92
|
+
if (typeof message !== 'string')
|
|
93
|
+
return;
|
|
94
|
+
try {
|
|
95
|
+
const data = JSON.parse(message);
|
|
96
|
+
// FILE_INFO identification from plugin — persist to DO storage
|
|
97
|
+
if (data.type === 'FILE_INFO' && data.data) {
|
|
98
|
+
const fileInfo = {
|
|
99
|
+
fileName: data.data.fileName,
|
|
100
|
+
fileKey: data.data.fileKey || null,
|
|
101
|
+
currentPage: data.data.currentPage,
|
|
102
|
+
currentPageId: data.data.currentPageId || null,
|
|
103
|
+
connectedAt: Date.now(),
|
|
104
|
+
};
|
|
105
|
+
await this.ctx.storage.put('fileInfo', fileInfo);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
// Event broadcasts from plugin (PAGE_CHANGE, etc.)
|
|
109
|
+
if (data.type === 'PAGE_CHANGE' && data.data) {
|
|
110
|
+
const fileInfo = await this.ctx.storage.get('fileInfo');
|
|
111
|
+
if (fileInfo) {
|
|
112
|
+
fileInfo.currentPage = data.data.pageName;
|
|
113
|
+
fileInfo.currentPageId = data.data.pageId || null;
|
|
114
|
+
await this.ctx.storage.put('fileInfo', fileInfo);
|
|
115
|
+
}
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
// Response to a relayed command
|
|
119
|
+
if (data.id && this.pendingRequests.has(data.id)) {
|
|
120
|
+
const pending = this.pendingRequests.get(data.id);
|
|
121
|
+
clearTimeout(pending.timeoutId);
|
|
122
|
+
this.pendingRequests.delete(data.id);
|
|
123
|
+
const body = JSON.stringify(data.error
|
|
124
|
+
? { error: data.error }
|
|
125
|
+
: { result: data.result });
|
|
126
|
+
pending.resolve(new Response(body, {
|
|
127
|
+
headers: { 'Content-Type': 'application/json' },
|
|
128
|
+
}));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
// Malformed message — ignore
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Hibernation callback — WebSocket closed.
|
|
137
|
+
*/
|
|
138
|
+
webSocketClose(ws, code, reason, wasClean) {
|
|
139
|
+
this.handleDisconnect();
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Hibernation callback — WebSocket error.
|
|
143
|
+
*/
|
|
144
|
+
webSocketError(ws, error) {
|
|
145
|
+
this.handleDisconnect();
|
|
146
|
+
}
|
|
147
|
+
handleDisconnect() {
|
|
148
|
+
// Clear persisted file info
|
|
149
|
+
this.ctx.storage.delete('fileInfo');
|
|
150
|
+
// Reject all in-flight commands
|
|
151
|
+
for (const [id, pending] of this.pendingRequests) {
|
|
152
|
+
clearTimeout(pending.timeoutId);
|
|
153
|
+
pending.resolve(new Response(JSON.stringify({ error: 'Plugin disconnected' }), { status: 502, headers: { 'Content-Type': 'application/json' } }));
|
|
154
|
+
}
|
|
155
|
+
this.pendingRequests.clear();
|
|
156
|
+
}
|
|
157
|
+
// ==========================================================================
|
|
158
|
+
// Relay — MCP DO sends commands here
|
|
159
|
+
// ==========================================================================
|
|
160
|
+
async handleRelayCommand(request) {
|
|
161
|
+
const pluginWs = this.getPluginWs();
|
|
162
|
+
if (!pluginWs) {
|
|
163
|
+
return new Response(JSON.stringify({ error: 'No plugin connected. User must pair the Desktop Bridge plugin first.' }), { status: 502, headers: { 'Content-Type': 'application/json' } });
|
|
164
|
+
}
|
|
165
|
+
const body = await request.json();
|
|
166
|
+
const { method, params = {}, timeoutMs = 15000 } = body;
|
|
167
|
+
const id = `relay_${++this.requestIdCounter}_${Date.now()}`;
|
|
168
|
+
// Send command to plugin
|
|
169
|
+
try {
|
|
170
|
+
pluginWs.send(JSON.stringify({ id, method, params }));
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
return new Response(JSON.stringify({ error: 'Failed to send command to plugin — connection may be stale' }), { status: 502, headers: { 'Content-Type': 'application/json' } });
|
|
174
|
+
}
|
|
175
|
+
// Wait for plugin response (the DO stays alive because fetch is active)
|
|
176
|
+
return new Promise((resolve) => {
|
|
177
|
+
const timeoutId = setTimeout(() => {
|
|
178
|
+
if (this.pendingRequests.has(id)) {
|
|
179
|
+
this.pendingRequests.delete(id);
|
|
180
|
+
resolve(new Response(JSON.stringify({ error: `Command ${method} timed out after ${timeoutMs}ms` }), { status: 504, headers: { 'Content-Type': 'application/json' } }));
|
|
181
|
+
}
|
|
182
|
+
}, timeoutMs);
|
|
183
|
+
this.pendingRequests.set(id, { resolve, reject: () => { }, timeoutId });
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
// ==========================================================================
|
|
187
|
+
// Status
|
|
188
|
+
// ==========================================================================
|
|
189
|
+
async handleRelayStatus() {
|
|
190
|
+
const pluginWs = this.getPluginWs();
|
|
191
|
+
const fileInfo = await this.ctx.storage.get('fileInfo');
|
|
192
|
+
return new Response(JSON.stringify({
|
|
193
|
+
connected: pluginWs !== null,
|
|
194
|
+
fileInfo: fileInfo || null,
|
|
195
|
+
pendingCommands: this.pendingRequests.size,
|
|
196
|
+
}), { headers: { 'Content-Type': 'application/json' } });
|
|
197
|
+
}
|
|
198
|
+
}
|