@sxl-studio/bridge 1.7.2 → 1.8.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.
Files changed (203) hide show
  1. package/README.md +342 -16
  2. package/dist/agent-recipes.d.ts +781 -11
  3. package/dist/agent-recipes.js +886 -13
  4. package/dist/agent-recipes.js.map +1 -1
  5. package/dist/agent-runbook.d.ts +50 -0
  6. package/dist/agent-runbook.js +243 -0
  7. package/dist/agent-runbook.js.map +1 -0
  8. package/dist/asset-upload.d.ts +63 -0
  9. package/dist/asset-upload.js +225 -0
  10. package/dist/asset-upload.js.map +1 -0
  11. package/dist/audit-store.d.ts +15 -0
  12. package/dist/audit-store.js +100 -0
  13. package/dist/audit-store.js.map +1 -0
  14. package/dist/audit.d.ts +4 -3
  15. package/dist/audit.js +37 -4
  16. package/dist/audit.js.map +1 -1
  17. package/dist/auth.d.ts +8 -1
  18. package/dist/auth.js +41 -1
  19. package/dist/auth.js.map +1 -1
  20. package/dist/bridge-agent-workflow-validation-cli.d.ts +2 -0
  21. package/dist/bridge-agent-workflow-validation-cli.js +68 -0
  22. package/dist/bridge-agent-workflow-validation-cli.js.map +1 -0
  23. package/dist/bridge-agent-workflow-validation.d.ts +42 -0
  24. package/dist/bridge-agent-workflow-validation.js +170 -0
  25. package/dist/bridge-agent-workflow-validation.js.map +1 -0
  26. package/dist/bridge-contract-audit.d.ts +45 -0
  27. package/dist/bridge-contract-audit.js +345 -0
  28. package/dist/bridge-contract-audit.js.map +1 -0
  29. package/dist/bridge-health-cli.d.ts +2 -0
  30. package/dist/bridge-health-cli.js +115 -0
  31. package/dist/bridge-health-cli.js.map +1 -0
  32. package/dist/bridge-health.d.ts +33 -0
  33. package/dist/bridge-health.js +594 -0
  34. package/dist/bridge-health.js.map +1 -0
  35. package/dist/bridge-live-validation-cli.d.ts +2 -0
  36. package/dist/bridge-live-validation-cli.js +114 -0
  37. package/dist/bridge-live-validation-cli.js.map +1 -0
  38. package/dist/bridge-live-validation.d.ts +39 -0
  39. package/dist/bridge-live-validation.js +1141 -0
  40. package/dist/bridge-live-validation.js.map +1 -0
  41. package/dist/bridge-performance-profile.d.ts +81 -0
  42. package/dist/bridge-performance-profile.js +227 -0
  43. package/dist/bridge-performance-profile.js.map +1 -0
  44. package/dist/bridge-readiness-cli.d.ts +30 -0
  45. package/dist/bridge-readiness-cli.js +242 -0
  46. package/dist/bridge-readiness-cli.js.map +1 -0
  47. package/dist/bridge-runtime-summary.d.ts +50 -0
  48. package/dist/bridge-runtime-summary.js +112 -0
  49. package/dist/bridge-runtime-summary.js.map +1 -0
  50. package/dist/bridge-workflow-smoke-cli.d.ts +2 -0
  51. package/dist/bridge-workflow-smoke-cli.js +126 -0
  52. package/dist/bridge-workflow-smoke-cli.js.map +1 -0
  53. package/dist/bridge-workflow-smoke.d.ts +39 -0
  54. package/dist/bridge-workflow-smoke.js +431 -0
  55. package/dist/bridge-workflow-smoke.js.map +1 -0
  56. package/dist/codeconnect-suggestions.d.ts +74 -0
  57. package/dist/codeconnect-suggestions.js +398 -0
  58. package/dist/codeconnect-suggestions.js.map +1 -0
  59. package/dist/codeconnect-template.d.ts +98 -0
  60. package/dist/codeconnect-template.js +280 -0
  61. package/dist/codeconnect-template.js.map +1 -0
  62. package/dist/command-queue.d.ts +11 -1
  63. package/dist/command-queue.js +200 -1
  64. package/dist/command-queue.js.map +1 -1
  65. package/dist/command-safety.d.ts +13 -0
  66. package/dist/command-safety.js +59 -0
  67. package/dist/command-safety.js.map +1 -0
  68. package/dist/enabled-library-search.d.ts +49 -0
  69. package/dist/enabled-library-search.js +151 -0
  70. package/dist/enabled-library-search.js.map +1 -0
  71. package/dist/figma-mcp-parity.d.ts +49 -0
  72. package/dist/figma-mcp-parity.js +368 -0
  73. package/dist/figma-mcp-parity.js.map +1 -0
  74. package/dist/figma-mcp-skills-parity.d.ts +61 -0
  75. package/dist/figma-mcp-skills-parity.js +434 -0
  76. package/dist/figma-mcp-skills-parity.js.map +1 -0
  77. package/dist/figma-rest-diagnostics.d.ts +50 -0
  78. package/dist/figma-rest-diagnostics.js +314 -0
  79. package/dist/figma-rest-diagnostics.js.map +1 -0
  80. package/dist/figma-rest.d.ts +27 -0
  81. package/dist/figma-rest.js +116 -0
  82. package/dist/figma-rest.js.map +1 -0
  83. package/dist/http-api.d.ts +16 -2
  84. package/dist/http-api.js +329 -17
  85. package/dist/http-api.js.map +1 -1
  86. package/dist/index.js +25 -1
  87. package/dist/index.js.map +1 -1
  88. package/dist/mcp-factory.d.ts +6 -1
  89. package/dist/mcp-factory.js +23 -4
  90. package/dist/mcp-factory.js.map +1 -1
  91. package/dist/mcp-runtime-probe.d.ts +22 -0
  92. package/dist/mcp-runtime-probe.js +777 -0
  93. package/dist/mcp-runtime-probe.js.map +1 -0
  94. package/dist/mcp-server.d.ts +2 -1
  95. package/dist/mcp-server.js +2 -2
  96. package/dist/mcp-server.js.map +1 -1
  97. package/dist/sxl-mcp-instructions.js +99 -26
  98. package/dist/sxl-mcp-instructions.js.map +1 -1
  99. package/dist/tools/audit.d.ts +22 -6
  100. package/dist/tools/audit.js +49 -7
  101. package/dist/tools/audit.js.map +1 -1
  102. package/dist/tools/capability-matrix.d.ts +22 -0
  103. package/dist/tools/capability-matrix.js +38 -0
  104. package/dist/tools/capability-matrix.js.map +1 -0
  105. package/dist/tools/catalogue-bootstrap.d.ts +1 -0
  106. package/dist/tools/catalogue-bootstrap.js +665 -30
  107. package/dist/tools/catalogue-bootstrap.js.map +1 -1
  108. package/dist/tools/code-connect-context.d.ts +3 -0
  109. package/dist/tools/code-connect-context.js +319 -0
  110. package/dist/tools/code-connect-context.js.map +1 -0
  111. package/dist/tools/code-connect-template.d.ts +3 -0
  112. package/dist/tools/code-connect-template.js +111 -0
  113. package/dist/tools/code-connect-template.js.map +1 -0
  114. package/dist/tools/composition.js +15 -30
  115. package/dist/tools/composition.js.map +1 -1
  116. package/dist/tools/compositions-orchestration.d.ts +14 -14
  117. package/dist/tools/compositions-orchestration.js +2 -2
  118. package/dist/tools/compositions-orchestration.js.map +1 -1
  119. package/dist/tools/data.js +839 -27
  120. package/dist/tools/data.js.map +1 -1
  121. package/dist/tools/design-context.d.ts +3 -0
  122. package/dist/tools/design-context.js +197 -0
  123. package/dist/tools/design-context.js.map +1 -0
  124. package/dist/tools/destructive-confirmation.d.ts +10 -0
  125. package/dist/tools/destructive-confirmation.js +25 -0
  126. package/dist/tools/destructive-confirmation.js.map +1 -0
  127. package/dist/tools/diagnostics.js +76 -51
  128. package/dist/tools/diagnostics.js.map +1 -1
  129. package/dist/tools/figma-mcp-design.d.ts +3 -0
  130. package/dist/tools/figma-mcp-design.js +377 -0
  131. package/dist/tools/figma-mcp-design.js.map +1 -0
  132. package/dist/tools/figma-nodes.js +57 -43
  133. package/dist/tools/figma-nodes.js.map +1 -1
  134. package/dist/tools/figma-rc-extended.js +23 -6
  135. package/dist/tools/figma-rc-extended.js.map +1 -1
  136. package/dist/tools/figma-rest.d.ts +39 -0
  137. package/dist/tools/figma-rest.js +279 -0
  138. package/dist/tools/figma-rest.js.map +1 -0
  139. package/dist/tools/git.js +11 -7
  140. package/dist/tools/git.js.map +1 -1
  141. package/dist/tools/large-data.d.ts +14 -0
  142. package/dist/tools/large-data.js +189 -0
  143. package/dist/tools/large-data.js.map +1 -0
  144. package/dist/tools/meta.d.ts +6 -1
  145. package/dist/tools/meta.js +89 -11
  146. package/dist/tools/meta.js.map +1 -1
  147. package/dist/tools/metadata.d.ts +3 -0
  148. package/dist/tools/metadata.js +140 -0
  149. package/dist/tools/metadata.js.map +1 -0
  150. package/dist/tools/mockup.d.ts +15 -156
  151. package/dist/tools/mockup.js +54 -121
  152. package/dist/tools/mockup.js.map +1 -1
  153. package/dist/tools/orchestration.js +75 -47
  154. package/dist/tools/orchestration.js.map +1 -1
  155. package/dist/tools/prompts.d.ts +3 -0
  156. package/dist/tools/prompts.js +219 -0
  157. package/dist/tools/prompts.js.map +1 -0
  158. package/dist/tools/registry.d.ts +19 -1
  159. package/dist/tools/registry.js +4 -4
  160. package/dist/tools/registry.js.map +1 -1
  161. package/dist/tools/resources.d.ts +19 -2
  162. package/dist/tools/resources.js +149 -5
  163. package/dist/tools/resources.js.map +1 -1
  164. package/dist/tools/schema-contracts.d.ts +4763 -0
  165. package/dist/tools/schema-contracts.js +814 -0
  166. package/dist/tools/schema-contracts.js.map +1 -0
  167. package/dist/tools/screenshot.d.ts +3 -0
  168. package/dist/tools/screenshot.js +144 -0
  169. package/dist/tools/screenshot.js.map +1 -0
  170. package/dist/tools/shared.d.ts +11 -1
  171. package/dist/tools/shared.js +55 -2
  172. package/dist/tools/shared.js.map +1 -1
  173. package/dist/tools/styles-orchestration.d.ts +2 -2
  174. package/dist/tools/styles-orchestration.js +13 -5
  175. package/dist/tools/styles-orchestration.js.map +1 -1
  176. package/dist/tools/styles.js +22 -8
  177. package/dist/tools/styles.js.map +1 -1
  178. package/dist/tools/tokens.d.ts +31 -692
  179. package/dist/tools/tokens.js +175 -135
  180. package/dist/tools/tokens.js.map +1 -1
  181. package/dist/tools/variable-defs.d.ts +3 -0
  182. package/dist/tools/variable-defs.js +338 -0
  183. package/dist/tools/variable-defs.js.map +1 -0
  184. package/dist/tools/variables-orchestration.js +13 -5
  185. package/dist/tools/variables-orchestration.js.map +1 -1
  186. package/dist/tools/variables.js +18 -15
  187. package/dist/tools/variables.js.map +1 -1
  188. package/dist/types.d.ts +53 -0
  189. package/dist/ultimate-readiness-audit.d.ts +37 -0
  190. package/dist/ultimate-readiness-audit.js +431 -0
  191. package/dist/ultimate-readiness-audit.js.map +1 -0
  192. package/dist/workflow-planner.d.ts +57 -0
  193. package/dist/workflow-planner.js +464 -0
  194. package/dist/workflow-planner.js.map +1 -0
  195. package/dist/workspace-blob-store.d.ts +6 -0
  196. package/dist/workspace-blob-store.js +9 -0
  197. package/dist/workspace-blob-store.js.map +1 -1
  198. package/dist/workspace-path-http.d.ts +24 -0
  199. package/dist/workspace-path-http.js +146 -0
  200. package/dist/workspace-path-http.js.map +1 -0
  201. package/dist/ws-server.js +16 -3
  202. package/dist/ws-server.js.map +1 -1
  203. package/package.json +21 -2
@@ -2,55 +2,867 @@
2
2
  * MCP tools for Data / Mapping operations.
3
3
  */
4
4
  import { z } from "zod";
5
+ import { prepareUploadAssets, uploadAssetReports, } from "../asset-upload.js";
6
+ import { buildCodeConnectSuggestions } from "../codeconnect-suggestions.js";
7
+ import { buildEnabledLibrarySearchResult } from "../enabled-library-search.js";
8
+ import { callPluginCommand, callPluginWriteCommand, err, executePluginCommand, executePluginWriteCommand, ok, } from "./shared.js";
9
+ import { destructiveConfirmationArgs, withDestructiveConfirmation } from "./destructive-confirmation.js";
10
+ import { largeDataResponseArgs, normalizeLargeDataResult } from "./large-data.js";
11
+ const datasetZod = z.object({ id: z.string() }).passthrough();
12
+ const assetZod = z.object({ id: z.string() }).passthrough();
13
+ const mappingZod = z.object({ id: z.string() }).passthrough();
14
+ const dryRunZod = z.boolean().optional().describe("Preview the operation without modifying plugin state.");
15
+ const applyScopeZod = z.enum(["selection", "page", "document"]);
16
+ const databaseAssetPayloadZod = z.union([
17
+ assetZod,
18
+ z.object({
19
+ asset: assetZod,
20
+ content: z.string().optional(),
21
+ data: z.string().optional(),
22
+ }).passthrough(),
23
+ ]);
24
+ const databasePayloadZod = z.object({
25
+ kind: z.string().optional(),
26
+ version: z.number().optional(),
27
+ exportedAt: z.string().optional(),
28
+ datasets: z.array(datasetZod).optional(),
29
+ assets: z.array(databaseAssetPayloadZod).optional(),
30
+ mappings: z.array(mappingZod).optional(),
31
+ }).passthrough();
32
+ const imageScaleModeZod = z.enum(["FILL", "FIT", "CROP", "TILE"]);
33
+ const codeConnectFrameworkZod = z.enum(["react", "angular", "html", "swift", "kotlin", "vue3", "compose"]);
34
+ const enabledLibraryAssetKindZod = z.enum(["component", "style", "variable", "variableCollection"]);
35
+ const variableResolvedTypeZod = z.enum(["BOOLEAN", "COLOR", "FLOAT", "STRING"]);
36
+ const codeConnectTargetZod = z.object({
37
+ nodeId: z.string().optional(),
38
+ nodeName: z.string().describe("Figma node/component name to match against local code files."),
39
+ nodeType: z.string().optional(),
40
+ });
41
+ const uploadAssetZod = z.object({
42
+ url: z.string().url().optional().describe("HTTP(S) image URL. Supported: PNG, JPG/JPEG, GIF, WebP."),
43
+ dataBase64: z.string().optional().describe("Optional base64 image body or data:image/*;base64,... URI."),
44
+ contentType: z.string().optional().describe("Optional MIME type override, e.g. image/png."),
45
+ name: z.string().optional(),
46
+ nodeId: z.string().optional().describe("Existing node to receive this image as fill. If omitted Bridge creates a rectangle."),
47
+ parentId: z.string().optional(),
48
+ x: z.number().optional(),
49
+ y: z.number().optional(),
50
+ width: z.number().positive().optional(),
51
+ height: z.number().positive().optional(),
52
+ cornerRadius: z.number().min(0).optional(),
53
+ scaleMode: imageScaleModeZod.optional(),
54
+ }).refine((value) => Boolean(value.url || value.dataBase64), {
55
+ message: "Each asset requires url or dataBase64.",
56
+ });
57
+ function dryRunPreview(action, id, extra = {}) {
58
+ return ok({
59
+ ok: true,
60
+ dryRun: true,
61
+ action,
62
+ id,
63
+ willCallPlugin: false,
64
+ ...extra,
65
+ });
66
+ }
67
+ function isRecord(value) {
68
+ return typeof value === "object" && value !== null && !Array.isArray(value);
69
+ }
70
+ function readArrayResult(result, key) {
71
+ if (!isRecord(result))
72
+ return [];
73
+ const value = result[key];
74
+ if (!Array.isArray(value))
75
+ return [];
76
+ return value.filter(isRecord);
77
+ }
78
+ function readSingleResult(result, key) {
79
+ if (!isRecord(result))
80
+ return null;
81
+ const value = result[key];
82
+ return isRecord(value) ? value : null;
83
+ }
84
+ function readIdValue(item) {
85
+ return typeof item.id === "string" && item.id.length > 0 ? item.id : null;
86
+ }
87
+ function filterByIds(items, ids) {
88
+ if (!Array.isArray(ids) || ids.length === 0)
89
+ return items;
90
+ const wanted = new Set(ids);
91
+ return items.filter((item) => {
92
+ const id = readIdValue(item);
93
+ return id ? wanted.has(id) : false;
94
+ });
95
+ }
96
+ function idSet(items) {
97
+ return new Set(items.map(readIdValue).filter((id) => Boolean(id)));
98
+ }
99
+ function isExistingId(id, existing) {
100
+ return typeof id === "string" && existing.has(id);
101
+ }
102
+ function normalizeDatabaseAssetEntry(entry, warnings, index) {
103
+ if (!isRecord(entry)) {
104
+ warnings.push(`assets[${index}] ignored: entry must be an object`);
105
+ return null;
106
+ }
107
+ if (isRecord(entry.asset)) {
108
+ const id = readIdValue(entry.asset);
109
+ if (!id) {
110
+ warnings.push(`assets[${index}] ignored: asset.id is required`);
111
+ return null;
112
+ }
113
+ const content = typeof entry.content === "string"
114
+ ? entry.content
115
+ : typeof entry.data === "string"
116
+ ? entry.data
117
+ : undefined;
118
+ return { asset: entry.asset, content };
119
+ }
120
+ const id = readIdValue(entry);
121
+ if (!id) {
122
+ warnings.push(`assets[${index}] ignored: id is required`);
123
+ return null;
124
+ }
125
+ const content = typeof entry.data === "string" ? entry.data : undefined;
126
+ return { asset: entry, content };
127
+ }
128
+ function normalizeDatabasePayload(payload) {
129
+ const warnings = [];
130
+ if (!isRecord(payload)) {
131
+ return { datasets: [], assets: [], mappings: [], warnings: ["payload must be an object"] };
132
+ }
133
+ const datasets = Array.isArray(payload.datasets)
134
+ ? payload.datasets.filter((dataset, index) => {
135
+ const valid = isRecord(dataset) && Boolean(readIdValue(dataset));
136
+ if (!valid)
137
+ warnings.push(`datasets[${index}] ignored: id is required`);
138
+ return valid;
139
+ })
140
+ : [];
141
+ const assets = Array.isArray(payload.assets)
142
+ ? payload.assets
143
+ .map((asset, index) => normalizeDatabaseAssetEntry(asset, warnings, index))
144
+ .filter((asset) => Boolean(asset))
145
+ : [];
146
+ const mappings = Array.isArray(payload.mappings)
147
+ ? payload.mappings.filter((mapping, index) => {
148
+ const valid = isRecord(mapping) && Boolean(readIdValue(mapping));
149
+ if (!valid)
150
+ warnings.push(`mappings[${index}] ignored: id is required`);
151
+ return valid;
152
+ })
153
+ : [];
154
+ return { datasets, assets, mappings, warnings };
155
+ }
156
+ function databaseCounts(payload) {
157
+ return {
158
+ datasets: payload.datasets.length,
159
+ assets: payload.assets.length,
160
+ assetContents: payload.assets.filter((asset) => typeof asset.content === "string").length,
161
+ mappings: payload.mappings.length,
162
+ };
163
+ }
164
+ function readCountResult(result) {
165
+ if (!isRecord(result))
166
+ return null;
167
+ return typeof result.count === "number" && Number.isFinite(result.count) ? result.count : null;
168
+ }
169
+ function mappingFields(mapping) {
170
+ if (!mapping || !Array.isArray(mapping.fields))
171
+ return [];
172
+ return mapping.fields.filter(isRecord);
173
+ }
174
+ function mappingTargetLayerNames(mapping) {
175
+ return [...new Set(mappingFields(mapping)
176
+ .map((field) => field.layerName)
177
+ .filter((name) => typeof name === "string" && name.length > 0))];
178
+ }
179
+ function mappingDatasetIds(mapping) {
180
+ if (!mapping)
181
+ return [];
182
+ const ids = new Set();
183
+ if (typeof mapping.datasetId === "string" && mapping.datasetId.length > 0)
184
+ ids.add(mapping.datasetId);
185
+ for (const field of mappingFields(mapping)) {
186
+ if (typeof field.datasetId === "string" && field.datasetId.length > 0)
187
+ ids.add(field.datasetId);
188
+ }
189
+ return [...ids];
190
+ }
191
+ function rowCountFromDataset(dataset) {
192
+ if (!dataset)
193
+ return null;
194
+ if (Array.isArray(dataset.data))
195
+ return dataset.data.length;
196
+ if (dataset.data !== undefined && dataset.data !== null)
197
+ return 1;
198
+ if (typeof dataset.lines === "number" && Number.isFinite(dataset.lines))
199
+ return dataset.lines;
200
+ return null;
201
+ }
202
+ function resolveRequestedRowCount(datasetRows, requested) {
203
+ if (typeof requested === "number" && Number.isFinite(requested)) {
204
+ return datasetRows === null ? Math.max(0, Math.floor(requested)) : Math.min(datasetRows, Math.max(0, Math.floor(requested)));
205
+ }
206
+ return datasetRows;
207
+ }
208
+ function summarizeMapping(mapping) {
209
+ if (!mapping)
210
+ return null;
211
+ return {
212
+ id: typeof mapping.id === "string" ? mapping.id : null,
213
+ name: typeof mapping.name === "string" ? mapping.name : null,
214
+ fieldCount: mappingFields(mapping).length,
215
+ datasetIds: mappingDatasetIds(mapping),
216
+ targetLayerNames: mappingTargetLayerNames(mapping),
217
+ };
218
+ }
219
+ async function getMappingById(queue, mappingId) {
220
+ const result = await executePluginCommand(queue, "get_mapping", { mappingId });
221
+ return readSingleResult(result, "mapping");
222
+ }
223
+ function readTargetNodeIds(args) {
224
+ const direct = Array.isArray(args.targetNodeIds)
225
+ ? args.targetNodeIds.filter((id) => typeof id === "string" && id.length > 0)
226
+ : [];
227
+ const legacy = Array.isArray(args.targetNodes)
228
+ ? args.targetNodes
229
+ .map((node) => isRecord(node) ? node.id : undefined)
230
+ .filter((id) => typeof id === "string" && id.length > 0)
231
+ : [];
232
+ return [...new Set([...direct, ...legacy])];
233
+ }
234
+ async function countApplyTargetsSafely(queue, scope, targetNodeIds = []) {
235
+ try {
236
+ const result = await executePluginCommand(queue, "count_apply_targets", {
237
+ scope,
238
+ ...(targetNodeIds.length > 0 ? { targetNodeIds } : {}),
239
+ });
240
+ return { count: readCountResult(result) };
241
+ }
242
+ catch (error) {
243
+ return { count: null, warning: `count_apply_targets failed: ${error instanceof Error ? error.message : String(error)}` };
244
+ }
245
+ }
246
+ async function datasetSummariesForMapping(queue, mapping) {
247
+ const warnings = [];
248
+ const datasets = [];
249
+ for (const datasetId of mappingDatasetIds(mapping)) {
250
+ try {
251
+ const dataset = readSingleResult(await executePluginCommand(queue, "get_dataset", { datasetId }), "dataset");
252
+ datasets.push({
253
+ id: datasetId,
254
+ exists: Boolean(dataset),
255
+ name: dataset && typeof dataset.name === "string" ? dataset.name : null,
256
+ rowCount: rowCountFromDataset(dataset),
257
+ });
258
+ }
259
+ catch (error) {
260
+ warnings.push(`Dataset ${datasetId} could not be read: ${error instanceof Error ? error.message : String(error)}`);
261
+ datasets.push({ id: datasetId, exists: false, rowCount: null });
262
+ }
263
+ }
264
+ return { datasets, warnings };
265
+ }
266
+ async function readExistingDatabaseIds(queue) {
267
+ const [datasetsResult, assetsResult, mappingsResult] = await Promise.all([
268
+ executePluginCommand(queue, "list_datasets"),
269
+ executePluginCommand(queue, "list_assets"),
270
+ executePluginCommand(queue, "list_mappings"),
271
+ ]);
272
+ return {
273
+ datasets: idSet(readArrayResult(datasetsResult, "datasets")),
274
+ assets: idSet(readArrayResult(assetsResult, "assets")),
275
+ mappings: idSet(readArrayResult(mappingsResult, "mappings")),
276
+ };
277
+ }
278
+ function findConflicts(payload, existing) {
279
+ const datasets = payload.datasets.map(readIdValue).filter((id) => isExistingId(id, existing.datasets));
280
+ const assets = payload.assets
281
+ .map((entry) => readIdValue(entry.asset))
282
+ .filter((id) => isExistingId(id, existing.assets));
283
+ const mappings = payload.mappings.map(readIdValue).filter((id) => isExistingId(id, existing.mappings));
284
+ return {
285
+ datasets,
286
+ assets,
287
+ mappings,
288
+ total: datasets.length + assets.length + mappings.length,
289
+ };
290
+ }
291
+ async function exportDatabasePayload(queue, args) {
292
+ const includeDatasets = args.includeDatasets !== false;
293
+ const includeAssets = args.includeAssets !== false;
294
+ const includeMappings = args.includeMappings !== false;
295
+ const includeAssetContent = args.includeAssetContent === true;
296
+ const datasetIds = Array.isArray(args.datasetIds) ? args.datasetIds.filter((id) => typeof id === "string") : undefined;
297
+ const assetIds = Array.isArray(args.assetIds) ? args.assetIds.filter((id) => typeof id === "string") : undefined;
298
+ const mappingIds = Array.isArray(args.mappingIds) ? args.mappingIds.filter((id) => typeof id === "string") : undefined;
299
+ const warnings = [];
300
+ let datasets = [];
301
+ if (includeDatasets) {
302
+ const listResult = await executePluginCommand(queue, "list_datasets");
303
+ const listed = filterByIds(readArrayResult(listResult, "datasets"), datasetIds);
304
+ datasets = await Promise.all(listed.map(async (dataset) => {
305
+ const id = readIdValue(dataset);
306
+ if (!id)
307
+ return dataset;
308
+ try {
309
+ return readSingleResult(await executePluginCommand(queue, "get_dataset", { datasetId: id }), "dataset") ?? dataset;
310
+ }
311
+ catch (error) {
312
+ warnings.push(`Dataset ${id} could not be hydrated: ${error instanceof Error ? error.message : String(error)}`);
313
+ return dataset;
314
+ }
315
+ }));
316
+ }
317
+ let assets = [];
318
+ if (includeAssets) {
319
+ const listResult = await executePluginCommand(queue, "list_assets");
320
+ const listed = filterByIds(readArrayResult(listResult, "assets"), assetIds);
321
+ assets = await Promise.all(listed.map(async (asset) => {
322
+ const id = readIdValue(asset);
323
+ if (!id)
324
+ return { asset };
325
+ try {
326
+ const result = await executePluginCommand(queue, "get_asset", { assetId: id, includeContent: includeAssetContent });
327
+ const hydratedAsset = readSingleResult(result, "asset") ?? asset;
328
+ const content = includeAssetContent && isRecord(result) && typeof result.content === "string"
329
+ ? result.content
330
+ : typeof hydratedAsset.data === "string"
331
+ ? hydratedAsset.data
332
+ : undefined;
333
+ return { asset: hydratedAsset, ...(includeAssetContent && content ? { content } : {}) };
334
+ }
335
+ catch (error) {
336
+ warnings.push(`Asset ${id} could not be hydrated: ${error instanceof Error ? error.message : String(error)}`);
337
+ return { asset };
338
+ }
339
+ }));
340
+ }
341
+ let mappings = [];
342
+ if (includeMappings) {
343
+ const listResult = await executePluginCommand(queue, "list_mappings");
344
+ const listed = filterByIds(readArrayResult(listResult, "mappings"), mappingIds);
345
+ mappings = await Promise.all(listed.map(async (mapping) => {
346
+ const id = readIdValue(mapping);
347
+ if (!id)
348
+ return mapping;
349
+ try {
350
+ return readSingleResult(await executePluginCommand(queue, "get_mapping", { mappingId: id }), "mapping") ?? mapping;
351
+ }
352
+ catch (error) {
353
+ warnings.push(`Mapping ${id} could not be hydrated: ${error instanceof Error ? error.message : String(error)}`);
354
+ return mapping;
355
+ }
356
+ }));
357
+ }
358
+ const normalized = normalizeDatabasePayload({ datasets, assets, mappings });
359
+ return {
360
+ ok: true,
361
+ kind: "sxl-database-payload",
362
+ version: 1,
363
+ exportedAt: new Date().toISOString(),
364
+ counts: databaseCounts(normalized),
365
+ datasets: normalized.datasets,
366
+ assets: normalized.assets,
367
+ mappings: normalized.mappings,
368
+ warnings: [...warnings, ...normalized.warnings],
369
+ };
370
+ }
5
371
  export function registerDataTools(server, queue) {
6
- server.tool("apply_mapping", "Apply a data mapping to target nodes in Figma. Maps dataset fields to Figma layers.", {
372
+ server.tool("list_datasets", "List SXL database datasets in the plugin workspace. Large responses are summarized automatically for MCP unless bridgeResponseMode='full'.", { ...largeDataResponseArgs }, async (args) => callPluginCommand(queue, "list_datasets", args, { largeData: true }));
373
+ server.tool("get_dataset", "Get one SXL dataset by id. Large responses are summarized automatically for MCP unless bridgeResponseMode='full'.", { datasetId: z.string(), ...largeDataResponseArgs }, async ({ datasetId, ...controls }) => callPluginCommand(queue, "get_dataset", { datasetId, ...controls }, { largeData: true }));
374
+ server.tool("save_dataset", "Create or update one SXL dataset. Set dryRun=true to preview without writing.", { dataset: datasetZod, dryRun: dryRunZod }, async ({ dataset, dryRun }) => dryRun
375
+ ? dryRunPreview("save_dataset", dataset.id)
376
+ : callPluginWriteCommand(queue, "save_dataset", { dataset }, {
377
+ okFalseErrorCode: "SXL_DATABASE_WRITE_FAILED",
378
+ }));
379
+ server.tool("delete_dataset", "Delete one SXL dataset by id. Set dryRun=true to preview without deleting.", { datasetId: z.string(), dryRun: dryRunZod, ...destructiveConfirmationArgs }, async ({ datasetId, dryRun, confirmDestructive, destructiveReason }) => dryRun
380
+ ? dryRunPreview("delete_dataset", datasetId, { destructive: true })
381
+ : callPluginWriteCommand(queue, "delete_dataset", withDestructiveConfirmation({ datasetId }, { confirmDestructive, destructiveReason }), { okFalseErrorCode: "SXL_DATABASE_WRITE_FAILED" }));
382
+ server.tool("list_assets", "List SXL database local assets in the plugin workspace. Large responses are summarized automatically for MCP unless bridgeResponseMode='full'.", { ...largeDataResponseArgs }, async (args) => callPluginCommand(queue, "list_assets", args, { largeData: true }));
383
+ server.tool("get_asset", "Get one SXL asset by id. Set includeContent=true to include stored base64 content. Large responses are summarized automatically for MCP unless bridgeResponseMode='full'.", { assetId: z.string(), includeContent: z.boolean().optional(), ...largeDataResponseArgs }, async ({ assetId, includeContent, ...controls }) => callPluginCommand(queue, "get_asset", { assetId, includeContent, ...controls }, { largeData: true }));
384
+ server.tool("save_asset", "Create or update one SXL asset. Optional data is base64 content. Set dryRun=true to preview without writing.", { asset: assetZod, data: z.string().optional(), dryRun: dryRunZod }, async ({ asset, data, dryRun }) => dryRun
385
+ ? dryRunPreview("save_asset", asset.id, { hasData: typeof data === "string" })
386
+ : callPluginWriteCommand(queue, "save_asset", { asset, data }, {
387
+ okFalseErrorCode: "SXL_DATABASE_WRITE_FAILED",
388
+ }));
389
+ server.tool("delete_asset", "Delete one SXL asset by id. Set dryRun=true to preview without deleting.", { assetId: z.string(), dryRun: dryRunZod, ...destructiveConfirmationArgs }, async ({ assetId, dryRun, confirmDestructive, destructiveReason }) => dryRun
390
+ ? dryRunPreview("delete_asset", assetId, { destructive: true })
391
+ : callPluginWriteCommand(queue, "delete_asset", withDestructiveConfirmation({ assetId }, { confirmDestructive, destructiveReason }), { okFalseErrorCode: "SXL_DATABASE_WRITE_FAILED" }));
392
+ server.tool("list_mappings", "List SXL database mappings in the plugin workspace. Large responses are summarized automatically for MCP unless bridgeResponseMode='full'.", { ...largeDataResponseArgs }, async (args) => callPluginCommand(queue, "list_mappings", args, { largeData: true }));
393
+ server.tool("get_mapping", "Get one SXL mapping by id. Large responses are summarized automatically for MCP unless bridgeResponseMode='full'.", { mappingId: z.string(), ...largeDataResponseArgs }, async ({ mappingId, ...controls }) => callPluginCommand(queue, "get_mapping", { mappingId, ...controls }, { largeData: true }));
394
+ server.tool("save_mapping", "Create or update one SXL mapping. Set dryRun=true to preview without writing.", { mapping: mappingZod, dryRun: dryRunZod }, async ({ mapping, dryRun }) => dryRun
395
+ ? dryRunPreview("save_mapping", mapping.id)
396
+ : callPluginWriteCommand(queue, "save_mapping", { mapping }, {
397
+ okFalseErrorCode: "SXL_DATABASE_WRITE_FAILED",
398
+ }));
399
+ server.tool("delete_mapping", "Delete one SXL mapping by id. Set dryRun=true to preview without deleting.", { mappingId: z.string(), dryRun: dryRunZod, ...destructiveConfirmationArgs }, async ({ mappingId, dryRun, confirmDestructive, destructiveReason }) => dryRun
400
+ ? dryRunPreview("delete_mapping", mappingId, { destructive: true })
401
+ : callPluginWriteCommand(queue, "delete_mapping", withDestructiveConfirmation({ mappingId }, { confirmDestructive, destructiveReason }), { okFalseErrorCode: "SXL_DATABASE_WRITE_FAILED" }));
402
+ server.tool("export_database_payload", "Export a portable SXL database payload (datasets, local assets, mappings) through existing plugin CRUD commands. Asset content is excluded by default; set includeAssetContent=true for backups that need embedded image data.", {
403
+ includeDatasets: z.boolean().optional(),
404
+ includeAssets: z.boolean().optional(),
405
+ includeMappings: z.boolean().optional(),
406
+ includeAssetContent: z.boolean().optional().describe("Include stored base64/data URL asset content. Default false to keep MCP responses small."),
407
+ datasetIds: z.array(z.string()).optional(),
408
+ assetIds: z.array(z.string()).optional(),
409
+ mappingIds: z.array(z.string()).optional(),
410
+ ...largeDataResponseArgs,
411
+ }, async (args) => {
412
+ try {
413
+ const payload = await exportDatabasePayload(queue, args);
414
+ return ok(normalizeLargeDataResult("export_database_payload", args, payload));
415
+ }
416
+ catch (error) {
417
+ return err(error instanceof Error ? error.message : String(error), {
418
+ code: "EXPORT_DATABASE_PAYLOAD_ERROR",
419
+ retryable: /No plugin connected|Session disconnected/i.test(error instanceof Error ? error.message : String(error)),
420
+ });
421
+ }
422
+ });
423
+ server.tool("import_database_payload", "Import a portable SXL database payload by upserting datasets, local assets, and mappings through existing plugin CRUD commands. Defaults to dryRun=true. Existing IDs require confirmDestructive=true when writing.", {
424
+ payload: databasePayloadZod,
425
+ dryRun: dryRunZod,
426
+ confirmDestructive: destructiveConfirmationArgs.confirmDestructive,
427
+ destructiveReason: destructiveConfirmationArgs.destructiveReason,
428
+ ...largeDataResponseArgs,
429
+ }, async ({ payload, dryRun, confirmDestructive, destructiveReason, ...controls }) => {
430
+ try {
431
+ const normalized = normalizeDatabasePayload(payload);
432
+ const existing = await readExistingDatabaseIds(queue);
433
+ const conflicts = findConflicts(normalized, existing);
434
+ const effectiveDryRun = dryRun !== false;
435
+ const preview = {
436
+ ok: true,
437
+ dryRun: effectiveDryRun,
438
+ action: "import_database_payload",
439
+ counts: databaseCounts(normalized),
440
+ conflicts,
441
+ warnings: normalized.warnings,
442
+ willCallPlugin: !effectiveDryRun,
443
+ };
444
+ if (effectiveDryRun) {
445
+ return ok(normalizeLargeDataResult("import_database_payload", controls, preview));
446
+ }
447
+ if (conflicts.total > 0 && confirmDestructive !== true) {
448
+ return err("import_database_payload would overwrite existing datasets/assets/mappings. Re-run with confirmDestructive=true and destructiveReason.", {
449
+ code: "DATABASE_IMPORT_CONFIRMATION_REQUIRED",
450
+ retryable: false,
451
+ conflicts,
452
+ });
453
+ }
454
+ if (conflicts.total > 0 && (typeof destructiveReason !== "string" || destructiveReason.trim().length === 0)) {
455
+ return err("destructiveReason is required when import_database_payload overwrites existing IDs.", {
456
+ code: "DATABASE_IMPORT_REASON_REQUIRED",
457
+ retryable: false,
458
+ conflicts,
459
+ });
460
+ }
461
+ const results = [];
462
+ for (const dataset of normalized.datasets) {
463
+ const id = readIdValue(dataset);
464
+ await executePluginWriteCommand(queue, "save_dataset", { dataset });
465
+ results.push({ type: "dataset", id, status: "saved" });
466
+ }
467
+ for (const entry of normalized.assets) {
468
+ const id = readIdValue(entry.asset);
469
+ await executePluginWriteCommand(queue, "save_asset", { asset: entry.asset, data: entry.content });
470
+ results.push({ type: "asset", id, status: "saved", hasContent: typeof entry.content === "string" });
471
+ }
472
+ for (const mapping of normalized.mappings) {
473
+ const id = readIdValue(mapping);
474
+ await executePluginWriteCommand(queue, "save_mapping", { mapping });
475
+ results.push({ type: "mapping", id, status: "saved" });
476
+ }
477
+ return ok(normalizeLargeDataResult("import_database_payload", controls, {
478
+ ...preview,
479
+ ok: true,
480
+ dryRun: false,
481
+ applied: true,
482
+ results,
483
+ }));
484
+ }
485
+ catch (error) {
486
+ return err(error instanceof Error ? error.message : String(error), {
487
+ code: "IMPORT_DATABASE_PAYLOAD_ERROR",
488
+ retryable: /No plugin connected|Session disconnected/i.test(error instanceof Error ? error.message : String(error)),
489
+ });
490
+ }
491
+ });
492
+ server.tool("apply_mapping", "Apply a data mapping to explicit targetNodeIds or the current Figma selection. Use dryRun=true first to inspect mapping fields, dataset ids, and target count without writing.", {
7
493
  mappingId: z.string().describe("ID of the mapping to apply"),
8
- targetNodes: z.array(z.unknown()).optional().describe("Optional target nodes array"),
9
- }, async ({ mappingId, targetNodes }) => {
10
- const result = await queue.execute("apply_mapping", { mappingId, targetNodes: targetNodes ?? [] });
11
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
494
+ targetNodeIds: z.array(z.string()).optional().describe("Optional explicit scene node ids to use as selection roots for this apply."),
495
+ targetNodes: z.array(z.unknown()).optional().describe("Legacy target node objects with id fields. Prefer targetNodeIds."),
496
+ dryRun: dryRunZod,
497
+ }, async ({ mappingId, targetNodeIds, targetNodes, dryRun }) => {
498
+ const resolvedTargetNodeIds = readTargetNodeIds({ targetNodeIds, targetNodes });
499
+ if (dryRun) {
500
+ try {
501
+ const mapping = await getMappingById(queue, mappingId);
502
+ const targetCount = await countApplyTargetsSafely(queue, "selection", resolvedTargetNodeIds);
503
+ const datasets = await datasetSummariesForMapping(queue, mapping);
504
+ const warnings = [
505
+ ...datasets.warnings,
506
+ ...(targetCount.warning ? [targetCount.warning] : []),
507
+ ...(Array.isArray(targetNodes) && targetNodes.length > 0
508
+ ? ["targetNodes is supported as a legacy id carrier; prefer targetNodeIds for new clients."]
509
+ : []),
510
+ ];
511
+ return ok({
512
+ ok: Boolean(mapping),
513
+ dryRun: true,
514
+ action: "apply_mapping",
515
+ mappingId,
516
+ mappingExists: Boolean(mapping),
517
+ mapping: summarizeMapping(mapping),
518
+ scope: "selection",
519
+ targetMode: resolvedTargetNodeIds.length > 0 ? "explicit-targets" : "current-selection",
520
+ selectionTargetCount: targetCount.count,
521
+ targetNodeIds: resolvedTargetNodeIds,
522
+ providedTargetNodes: resolvedTargetNodeIds.length,
523
+ datasets: datasets.datasets,
524
+ warnings,
525
+ willCallPlugin: false,
526
+ });
527
+ }
528
+ catch (error) {
529
+ return err(error instanceof Error ? error.message : String(error), {
530
+ code: "APPLY_MAPPING_PREVIEW_ERROR",
531
+ retryable: /No plugin connected|Session disconnected/i.test(error instanceof Error ? error.message : String(error)),
532
+ });
533
+ }
534
+ }
535
+ return callPluginWriteCommand(queue, "apply_mapping", {
536
+ mappingId,
537
+ ...(resolvedTargetNodeIds.length > 0 ? { targetNodeIds: resolvedTargetNodeIds } : {}),
538
+ ...(targetNodes ? { targetNodes } : {}),
539
+ }, { okFalseErrorCode: "SXL_DATA_WRITE_FAILED" });
12
540
  });
13
- server.tool("apply_all_mappings", "Apply all mappings within a scope (selection, page, or document).", { scope: z.string().optional().describe("Scope: selection, page, or document") }, async ({ scope }) => {
14
- const result = await queue.execute("apply_all_mappings", scope ? { scope } : {});
15
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
541
+ server.tool("apply_all_mappings", "Apply all mappings within a scope (selection, page, or document). Use dryRun=true first to count mappings and matching targets without writing.", {
542
+ scope: applyScopeZod.optional().describe("Scope: selection, page, or document. Default: selection."),
543
+ targetNodeIds: z.array(z.string()).optional().describe("Optional explicit scene node ids; when provided Bridge applies/counts these as selection roots."),
544
+ targetNodes: z.array(z.unknown()).optional().describe("Legacy target node objects with id fields. Prefer targetNodeIds."),
545
+ dryRun: dryRunZod,
546
+ }, async ({ scope, targetNodeIds, targetNodes, dryRun }) => {
547
+ const effectiveScope = scope ?? "selection";
548
+ const resolvedTargetNodeIds = readTargetNodeIds({ targetNodeIds, targetNodes });
549
+ if (dryRun) {
550
+ try {
551
+ const mappingsResult = await executePluginCommand(queue, "list_mappings");
552
+ const mappings = readArrayResult(mappingsResult, "mappings");
553
+ const targetCount = await countApplyTargetsSafely(queue, effectiveScope, resolvedTargetNodeIds);
554
+ return ok({
555
+ ok: true,
556
+ dryRun: true,
557
+ action: "apply_all_mappings",
558
+ scope: effectiveScope,
559
+ targetMode: resolvedTargetNodeIds.length > 0 ? "explicit-targets" : "scope",
560
+ targetNodeIds: resolvedTargetNodeIds,
561
+ mappingCount: mappings.length,
562
+ mappings: mappings.map((mapping) => summarizeMapping(mapping)),
563
+ targetCount: targetCount.count,
564
+ warnings: targetCount.warning ? [targetCount.warning] : [],
565
+ willCallPlugin: false,
566
+ });
567
+ }
568
+ catch (error) {
569
+ return err(error instanceof Error ? error.message : String(error), {
570
+ code: "APPLY_ALL_MAPPINGS_PREVIEW_ERROR",
571
+ retryable: /No plugin connected|Session disconnected/i.test(error instanceof Error ? error.message : String(error)),
572
+ });
573
+ }
574
+ }
575
+ return callPluginWriteCommand(queue, "apply_all_mappings", {
576
+ scope: effectiveScope,
577
+ ...(resolvedTargetNodeIds.length > 0 ? { targetNodeIds: resolvedTargetNodeIds } : {}),
578
+ ...(targetNodes ? { targetNodes } : {}),
579
+ }, { okFalseErrorCode: "SXL_DATA_WRITE_FAILED" });
16
580
  });
17
- server.tool("count_apply_targets", "Count how many nodes match a mapping scope.", { scope: z.string().describe("Scope: selection, page, or document") }, async ({ scope }) => {
18
- const result = await queue.execute("count_apply_targets", { scope });
19
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
581
+ server.tool("count_apply_targets", "Count how many nodes match a mapping scope.", {
582
+ scope: applyScopeZod.describe("Scope: selection, page, or document"),
583
+ targetNodeIds: z.array(z.string()).optional().describe("Optional explicit scene node ids to count as selection roots."),
584
+ targetNodes: z.array(z.unknown()).optional().describe("Legacy target node objects with id fields. Prefer targetNodeIds."),
585
+ }, async ({ scope, targetNodeIds, targetNodes }) => {
586
+ const resolvedTargetNodeIds = readTargetNodeIds({ targetNodeIds, targetNodes });
587
+ return callPluginCommand(queue, "count_apply_targets", {
588
+ scope,
589
+ ...(resolvedTargetNodeIds.length > 0 ? { targetNodeIds: resolvedTargetNodeIds } : {}),
590
+ ...(targetNodes ? { targetNodes } : {}),
591
+ });
20
592
  });
21
- server.tool("generate_instances", "Generate component instances from mapping data.", {
593
+ server.tool("generate_instances", "Generate component instances from mapping data into explicit targetNodeIds or the current Figma selection. Use dryRun=true first to estimate rows and validate mapping/dataset references without writing.", {
22
594
  mappingId: z.string().describe("Mapping ID to generate from"),
23
595
  rowCount: z.number().nullable().optional().describe("Number of rows to generate (null for all)"),
24
- }, async ({ mappingId, rowCount }) => {
25
- const result = await queue.execute("generate_instances", {
596
+ targetNodeIds: z.array(z.string()).optional().describe("Optional explicit scene node ids to use as the template/container source."),
597
+ targetNodes: z.array(z.unknown()).optional().describe("Legacy target node objects with id fields. Prefer targetNodeIds."),
598
+ dryRun: dryRunZod,
599
+ }, async ({ mappingId, rowCount, targetNodeIds, targetNodes, dryRun }) => {
600
+ const resolvedTargetNodeIds = readTargetNodeIds({ targetNodeIds, targetNodes });
601
+ if (dryRun) {
602
+ try {
603
+ const mapping = await getMappingById(queue, mappingId);
604
+ const datasets = await datasetSummariesForMapping(queue, mapping);
605
+ const primaryRows = datasets.datasets.find((dataset) => typeof dataset.rowCount === "number")?.rowCount;
606
+ const warnings = [
607
+ ...datasets.warnings,
608
+ ...(Array.isArray(targetNodes) && targetNodes.length > 0
609
+ ? ["targetNodes is supported as a legacy id carrier; prefer targetNodeIds for new clients."]
610
+ : []),
611
+ ...(resolvedTargetNodeIds.length === 0
612
+ ? ["No targetNodeIds provided; committed generate_instances will use the current Figma selection as the template/container source."]
613
+ : []),
614
+ ];
615
+ return ok({
616
+ ok: Boolean(mapping),
617
+ dryRun: true,
618
+ action: "generate_instances",
619
+ mappingId,
620
+ mappingExists: Boolean(mapping),
621
+ mapping: summarizeMapping(mapping),
622
+ scope: "selection",
623
+ targetMode: resolvedTargetNodeIds.length > 0 ? "explicit-targets" : "current-selection",
624
+ targetNodeIds: resolvedTargetNodeIds,
625
+ providedTargetNodes: resolvedTargetNodeIds.length,
626
+ requestedRowCount: rowCount ?? null,
627
+ estimatedRows: resolveRequestedRowCount(primaryRows ?? null, rowCount ?? null),
628
+ datasets: datasets.datasets,
629
+ warnings,
630
+ willCallPlugin: false,
631
+ });
632
+ }
633
+ catch (error) {
634
+ return err(error instanceof Error ? error.message : String(error), {
635
+ code: "GENERATE_INSTANCES_PREVIEW_ERROR",
636
+ retryable: /No plugin connected|Session disconnected/i.test(error instanceof Error ? error.message : String(error)),
637
+ });
638
+ }
639
+ }
640
+ return callPluginWriteCommand(queue, "generate_instances", {
26
641
  mappingId,
27
642
  rowCount: rowCount ?? null,
28
643
  scope: "selection",
29
- });
30
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
644
+ ...(resolvedTargetNodeIds.length > 0 ? { targetNodeIds: resolvedTargetNodeIds } : {}),
645
+ ...(targetNodes ? { targetNodes } : {}),
646
+ }, { okFalseErrorCode: "SXL_DATA_WRITE_FAILED" });
31
647
  });
32
648
  server.tool("apply_image", "Apply an image from URL to the current selection.", {
33
649
  url: z.string().describe("Image URL"),
34
650
  name: z.string().describe("Image name"),
35
- }, async ({ url, name }) => {
36
- const result = await queue.execute("apply_image", { url, name });
37
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
651
+ }, async ({ url, name }) => callPluginWriteCommand(queue, "apply_image", { url, name }, {
652
+ okFalseErrorCode: "SXL_CANVAS_WRITE_FAILED",
653
+ }));
654
+ server.tool("upload_assets", "Bridge asset uploader for Figma Design: fetch/validate PNG, JPG/JPEG, GIF, or WebP assets up to 10MB each and apply them as image fills. If nodeId is supplied the asset fills that node; otherwise Bridge creates image rectangles. Use dryRun=true first.", {
655
+ assets: z.array(uploadAssetZod).min(1).max(10),
656
+ dryRun: dryRunZod,
657
+ fetchInDryRun: z.boolean().optional().describe("If true, dryRun also fetches assets to verify MIME/size."),
658
+ maxBytes: z.number().int().positive().max(10 * 1024 * 1024).optional().describe("Per-asset byte limit. Default/max: 10MB."),
659
+ parentId: z.string().optional(),
660
+ targetNodeId: z.string().optional().describe("Default nodeId for assets that omit nodeId."),
661
+ x: z.number().optional(),
662
+ y: z.number().optional(),
663
+ width: z.number().positive().optional(),
664
+ height: z.number().positive().optional(),
665
+ gap: z.number().min(0).optional(),
666
+ scaleMode: imageScaleModeZod.optional(),
667
+ }, async (args) => {
668
+ try {
669
+ const prepared = await prepareUploadAssets(args);
670
+ if (prepared.dryRun) {
671
+ return ok({
672
+ ok: true,
673
+ dryRun: true,
674
+ count: prepared.count,
675
+ maxBytes: prepared.maxBytes,
676
+ assets: uploadAssetReports(prepared),
677
+ });
678
+ }
679
+ const results = [];
680
+ for (const asset of prepared.assets) {
681
+ if (!asset.payload) {
682
+ results.push({ ...asset.report, status: "failed", error: "Missing prepared plugin payload" });
683
+ continue;
684
+ }
685
+ try {
686
+ const pluginResult = await executePluginCommand(queue, "set_image_fill", asset.payload);
687
+ if (isRecord(pluginResult) && pluginResult.ok === false) {
688
+ results.push({
689
+ ...asset.report,
690
+ status: "failed",
691
+ error: typeof pluginResult.error === "string"
692
+ ? pluginResult.error
693
+ : typeof pluginResult.message === "string"
694
+ ? pluginResult.message
695
+ : "set_image_fill returned ok:false",
696
+ result: pluginResult,
697
+ });
698
+ continue;
699
+ }
700
+ results.push({ ...asset.report, status: "completed", result: pluginResult });
701
+ }
702
+ catch (error) {
703
+ results.push({
704
+ ...asset.report,
705
+ status: "failed",
706
+ error: error instanceof Error ? error.message : String(error),
707
+ });
708
+ }
709
+ }
710
+ const failures = results.filter((result) => result.status !== "completed");
711
+ return ok({
712
+ ok: failures.length === 0,
713
+ dryRun: false,
714
+ count: prepared.count,
715
+ completed: results.length - failures.length,
716
+ failed: failures.length,
717
+ assets: results,
718
+ });
719
+ }
720
+ catch (error) {
721
+ return err(error instanceof Error ? error.message : String(error), {
722
+ code: "UPLOAD_ASSETS_ERROR",
723
+ retryable: false,
724
+ });
725
+ }
726
+ });
727
+ server.tool("get_code_connect_suggestions", "Bridge-local Code Connect suggestion engine. Scans a local codebase for likely component files, ranks candidate CodeConnectBinding payloads by selected Figma node/component names, and returns reviewable bindings for codeconnect_save_binding. If targets/componentNames are omitted Bridge reads the current Figma selection via get_selection_summary.", {
728
+ targets: z.array(codeConnectTargetZod).optional(),
729
+ componentNames: z.array(z.string()).optional(),
730
+ rootDir: z.string().optional().describe("Local codebase root to scan. Defaults to SXL_CODECONNECT_ROOT or the Bridge process cwd."),
731
+ repositoryUrl: z.string().optional().describe("Repository web URL copied into suggested binding.gitlabUrl."),
732
+ gitlabUrl: z.string().optional().describe("Explicit CodeConnectBinding.gitlabUrl override."),
733
+ documentationUrl: z.string().optional(),
734
+ documentationDocsQuerySuffix: z.string().optional(),
735
+ documentationStoryPathTemplate: z.string().optional(),
736
+ frameworks: z.array(codeConnectFrameworkZod).optional(),
737
+ maxSuggestionsPerTarget: z.number().int().positive().max(20).optional(),
738
+ maxFiles: z.number().int().positive().max(10_000).optional(),
739
+ maxFileBytes: z.number().int().positive().max(1024 * 1024).optional(),
740
+ includeLowConfidence: z.boolean().optional(),
741
+ ...largeDataResponseArgs,
742
+ }, async (args) => {
743
+ let hasDirectTargets = false;
744
+ try {
745
+ let selectionSummary;
746
+ hasDirectTargets =
747
+ (Array.isArray(args.targets) && args.targets.length > 0) ||
748
+ (Array.isArray(args.componentNames) && args.componentNames.length > 0);
749
+ if (!hasDirectTargets) {
750
+ selectionSummary = await executePluginCommand(queue, "get_selection_summary");
751
+ }
752
+ const result = buildCodeConnectSuggestions(args, selectionSummary);
753
+ if (result.targets.length === 0) {
754
+ return err("No Code Connect targets found. Provide targets/componentNames or select component nodes in Figma with Remote Connect active.", {
755
+ code: "CODECONNECT_SUGGESTIONS_NO_TARGETS",
756
+ retryable: true,
757
+ });
758
+ }
759
+ return ok(normalizeLargeDataResult("get_code_connect_suggestions", args, result));
760
+ }
761
+ catch (error) {
762
+ const message = error instanceof Error ? error.message : String(error);
763
+ const retryable = !hasDirectTargets && /No plugin connected|Session disconnected/i.test(message);
764
+ return err(message, {
765
+ code: "CODECONNECT_SUGGESTIONS_ERROR",
766
+ retryable,
767
+ });
768
+ }
769
+ });
770
+ server.tool("inspect_enabled_libraries", "Read enabled team-library descriptors for the current Figma file through the plugin teamLibrary API. Returns enabled variable collections and, when available in the Figma runtime, component/style descriptors. This cannot enable libraries or enumerate arbitrary organization/community libraries.", {
771
+ query: z.string().optional().describe("Optional case-insensitive filter by library/name."),
772
+ limitPerKind: z.number().int().positive().max(5_000).optional(),
773
+ includeVariableCollections: z.boolean().optional(),
774
+ includeStyles: z.boolean().optional(),
775
+ includeComponents: z.boolean().optional(),
776
+ ...largeDataResponseArgs,
777
+ }, async (args) => callPluginCommand(queue, "inspect_enabled_libraries", args, { largeData: true }));
778
+ server.tool("list_enabled_library_variables", "Read variables from enabled team-library variable collections. Provide collectionKey, collectionKeys, collectionName, libraryName, or includeAllCollections=true. Large responses are summarized automatically for MCP unless bridgeResponseMode='full'.", {
779
+ collectionKey: z.string().optional(),
780
+ collectionKeys: z.array(z.string()).optional(),
781
+ collectionName: z.string().optional(),
782
+ libraryName: z.string().optional(),
783
+ includeAllCollections: z.boolean().optional(),
784
+ resolvedTypes: z.array(variableResolvedTypeZod).optional(),
785
+ query: z.string().optional(),
786
+ limit: z.number().int().positive().max(10_000).optional(),
787
+ ...largeDataResponseArgs,
788
+ }, async (args) => callPluginCommand(queue, "list_enabled_library_variables", args, { largeData: true }));
789
+ server.tool("search_enabled_library_assets", "Bridge orchestration search over libraries already enabled for the current Figma file. Searches component/style/variable collection descriptors plus enabled library variables with bounded scans. This cannot enable libraries or discover arbitrary organization/community libraries.", {
790
+ query: z.string().min(1),
791
+ kinds: z.array(enabledLibraryAssetKindZod).optional(),
792
+ libraryName: z.string().optional(),
793
+ collectionKey: z.string().optional(),
794
+ collectionName: z.string().optional(),
795
+ resolvedTypes: z.array(variableResolvedTypeZod).optional(),
796
+ maxMatches: z.number().int().positive().max(500).optional(),
797
+ maxVariables: z.number().int().positive().max(10_000).optional(),
798
+ includeVariables: z.boolean().optional().describe("Default true. Set false for a cheaper descriptor-only search."),
799
+ ...largeDataResponseArgs,
800
+ }, async (args) => {
801
+ try {
802
+ const kinds = Array.isArray(args.kinds) && args.kinds.length > 0
803
+ ? args.kinds
804
+ : ["component", "style", "variable", "variableCollection"];
805
+ const includeVariables = args.includeVariables !== false && kinds.includes("variable");
806
+ const inspectResult = await executePluginCommand(queue, "inspect_enabled_libraries", {
807
+ query: args.query,
808
+ limitPerKind: args.maxMatches ?? 100,
809
+ includeComponents: kinds.includes("component"),
810
+ includeStyles: kinds.includes("style"),
811
+ includeVariableCollections: true,
812
+ });
813
+ let variableResult;
814
+ const warnings = [];
815
+ if (includeVariables) {
816
+ try {
817
+ variableResult = await executePluginCommand(queue, "list_enabled_library_variables", {
818
+ query: args.query,
819
+ collectionKey: args.collectionKey,
820
+ collectionName: args.collectionName,
821
+ libraryName: args.libraryName,
822
+ includeAllCollections: !args.collectionKey && !args.collectionName && !args.libraryName,
823
+ resolvedTypes: args.resolvedTypes,
824
+ limit: args.maxVariables ?? 1_000,
825
+ });
826
+ }
827
+ catch (error) {
828
+ warnings.push(`Variable scan failed: ${error instanceof Error ? error.message : String(error)}`);
829
+ }
830
+ }
831
+ const result = buildEnabledLibrarySearchResult({
832
+ args: {
833
+ query: args.query,
834
+ kinds,
835
+ libraryName: args.libraryName,
836
+ collectionKey: args.collectionKey,
837
+ collectionName: args.collectionName,
838
+ maxMatches: args.maxMatches,
839
+ },
840
+ inspectResult,
841
+ variableResult,
842
+ warnings,
843
+ });
844
+ return ok(normalizeLargeDataResult("search_enabled_library_assets", args, result));
845
+ }
846
+ catch (error) {
847
+ return err(error instanceof Error ? error.message : String(error), {
848
+ code: "SEARCH_ENABLED_LIBRARY_ASSETS_ERROR",
849
+ retryable: /No plugin connected|Session disconnected/i.test(error instanceof Error ? error.message : String(error)),
850
+ });
851
+ }
38
852
  });
39
853
  server.tool("set_node_text", "Set text content on a text layer in the currently selected nodes (or a specific node by ID). If layerName is provided, finds text nodes with that name inside the selection. Otherwise sets text on all text nodes in selection.", {
40
854
  text: z.string().describe("Text content to set"),
41
855
  layerName: z.string().optional().describe("Name of the text layer to target (e.g. 'label', 'title'). Omit to set all text nodes."),
42
856
  nodeId: z.string().optional().describe("Specific Figma node ID. Omit to use current selection."),
43
- }, async ({ text, layerName, nodeId }) => {
44
- const result = await queue.execute("set_node_text", { text, layerName, nodeId });
45
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
46
- });
857
+ }, async ({ text, layerName, nodeId }) => callPluginWriteCommand(queue, "set_node_text", { text, layerName, nodeId }, {
858
+ okFalseErrorCode: "SXL_CANVAS_WRITE_FAILED",
859
+ }));
47
860
  server.tool("set_node_property", "Set a component property on a selected instance or component. For instances, sets instance property. For components, sets default value.", {
48
861
  propertyName: z.string().describe("Property name (e.g. 'label', 'isIcon', 'size')"),
49
862
  propertyValue: z.union([z.string(), z.boolean(), z.number()]).describe("Property value"),
50
863
  nodeId: z.string().optional().describe("Specific Figma node ID. Omit to use current selection."),
51
- }, async ({ propertyName, propertyValue, nodeId }) => {
52
- const result = await queue.execute("set_node_property", { propertyName, propertyValue, nodeId });
53
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
54
- });
864
+ }, async ({ propertyName, propertyValue, nodeId }) => callPluginWriteCommand(queue, "set_node_property", { propertyName, propertyValue, nodeId }, {
865
+ okFalseErrorCode: "SXL_CANVAS_WRITE_FAILED",
866
+ }));
55
867
  }
56
868
  //# sourceMappingURL=data.js.map