@pixi-board/board-plugin-canvas 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1131 -0
- package/package.json +27 -0
- package/src/dto.ts +151 -0
- package/src/errors.ts +11 -0
- package/src/filter.ts +106 -0
- package/src/index.ts +497 -0
- package/src/projects.ts +269 -0
- package/src/reader.ts +177 -0
- package/src/writerClient.ts +141 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
McpCreateNodeInput,
|
|
3
|
+
McpUpdateAssetInput,
|
|
4
|
+
McpUpdateNodeInput,
|
|
5
|
+
McpWriteCommand,
|
|
6
|
+
ModelAssetFormat,
|
|
7
|
+
} from "@canvas/board-domain";
|
|
8
|
+
import type { BoardPlugin, BoardTool, JSONSchema } from "@canvas/board-plugin-sdk";
|
|
9
|
+
import { toAssetDto, toNodeDto, toSnapshotDto } from "./dto";
|
|
10
|
+
import { BoardToolUserError } from "./errors";
|
|
11
|
+
import { filterNodes } from "./filter";
|
|
12
|
+
import {
|
|
13
|
+
getAssetOrThrow,
|
|
14
|
+
getNodeOrThrow,
|
|
15
|
+
getOriginAssetForNode,
|
|
16
|
+
loadProject,
|
|
17
|
+
} from "./reader";
|
|
18
|
+
import { listKnownProjects } from "./projects";
|
|
19
|
+
import { sendWriteCommand } from "./writerClient";
|
|
20
|
+
|
|
21
|
+
const PROJECT_ROOT_DESCRIPTION = 'Canvas project root absolute path, or "active" for the current canvas';
|
|
22
|
+
const ASSET_KIND_VALUES = ["image", "video", "audio", "model", "text", "markdown", "html", "importing", "generating"] as const;
|
|
23
|
+
const NODE_TYPE_VALUES = ASSET_KIND_VALUES;
|
|
24
|
+
const MODEL_FORMAT_VALUES: ModelAssetFormat[] = [
|
|
25
|
+
"glb",
|
|
26
|
+
"gltf",
|
|
27
|
+
"obj",
|
|
28
|
+
"fbx",
|
|
29
|
+
"stl",
|
|
30
|
+
"ply",
|
|
31
|
+
"dae",
|
|
32
|
+
"3mf",
|
|
33
|
+
"3ds",
|
|
34
|
+
"vrml",
|
|
35
|
+
"wrl",
|
|
36
|
+
"zip",
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
const anyOutputSchema = {
|
|
40
|
+
type: "object",
|
|
41
|
+
additionalProperties: true,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const canvasPlugin: BoardPlugin = {
|
|
45
|
+
name: "@pixi-board/board-plugin-canvas",
|
|
46
|
+
version: "0.1.0",
|
|
47
|
+
register(api) {
|
|
48
|
+
for (const tool of canvasTools) {
|
|
49
|
+
api.registerTool(tool);
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const plugin = canvasPlugin;
|
|
55
|
+
|
|
56
|
+
export const canvasTools: BoardTool[] = [
|
|
57
|
+
tool({
|
|
58
|
+
name: "canvas.get_project_list",
|
|
59
|
+
description: "List known canvas projects from the shared canvas project registry.",
|
|
60
|
+
inputSchema: objectSchema({}, []),
|
|
61
|
+
async run() {
|
|
62
|
+
return {
|
|
63
|
+
projects: await listKnownProjects(),
|
|
64
|
+
};
|
|
65
|
+
},
|
|
66
|
+
}),
|
|
67
|
+
tool({
|
|
68
|
+
name: "canvas.get_board_snapshot",
|
|
69
|
+
description: "Read a compact board snapshot from local project files. Does not require the desktop app.",
|
|
70
|
+
inputSchema: objectSchema({
|
|
71
|
+
projectRoot: stringSchema(PROJECT_ROOT_DESCRIPTION),
|
|
72
|
+
}),
|
|
73
|
+
async run(input) {
|
|
74
|
+
const project = await loadProject(readString(input, "projectRoot"));
|
|
75
|
+
return toSnapshotDto(project.root, project.snapshot);
|
|
76
|
+
},
|
|
77
|
+
}),
|
|
78
|
+
tool({
|
|
79
|
+
name: "canvas.list_nodes",
|
|
80
|
+
description: "List compact board nodes, optionally filtered by bounds or keyword.",
|
|
81
|
+
inputSchema: objectSchema({
|
|
82
|
+
projectRoot: stringSchema(PROJECT_ROOT_DESCRIPTION),
|
|
83
|
+
filter: {
|
|
84
|
+
type: "object",
|
|
85
|
+
additionalProperties: true,
|
|
86
|
+
properties: {
|
|
87
|
+
type: { type: "string", enum: NODE_TYPE_VALUES },
|
|
88
|
+
keyword: { type: "string" },
|
|
89
|
+
name: { type: "string" },
|
|
90
|
+
text: { type: "string" },
|
|
91
|
+
bounds: {
|
|
92
|
+
type: "object",
|
|
93
|
+
additionalProperties: false,
|
|
94
|
+
properties: {
|
|
95
|
+
x: { type: "number" },
|
|
96
|
+
y: { type: "number" },
|
|
97
|
+
width: { type: "number" },
|
|
98
|
+
height: { type: "number" },
|
|
99
|
+
minX: { type: "number" },
|
|
100
|
+
minY: { type: "number" },
|
|
101
|
+
maxX: { type: "number" },
|
|
102
|
+
maxY: { type: "number" },
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
}),
|
|
108
|
+
async run(input) {
|
|
109
|
+
const args = asRecord(input);
|
|
110
|
+
const project = await loadProject(readString(args, "projectRoot"));
|
|
111
|
+
return {
|
|
112
|
+
nodes: filterNodes(project.snapshot.nodes, args.filter).map(toNodeDto),
|
|
113
|
+
};
|
|
114
|
+
},
|
|
115
|
+
}),
|
|
116
|
+
tool({
|
|
117
|
+
name: "canvas.get_node",
|
|
118
|
+
description: "Read compact details for one board node from local project files.",
|
|
119
|
+
inputSchema: objectSchema({
|
|
120
|
+
projectRoot: stringSchema(PROJECT_ROOT_DESCRIPTION),
|
|
121
|
+
nodeId: stringSchema("Node id"),
|
|
122
|
+
}, ["projectRoot", "nodeId"]),
|
|
123
|
+
async run(input) {
|
|
124
|
+
const args = asRecord(input);
|
|
125
|
+
const project = await loadProject(readString(args, "projectRoot"));
|
|
126
|
+
return {
|
|
127
|
+
node: toNodeDto(getNodeOrThrow(project.snapshot, args.nodeId)),
|
|
128
|
+
};
|
|
129
|
+
},
|
|
130
|
+
}),
|
|
131
|
+
tool({
|
|
132
|
+
name: "canvas.create_nodes",
|
|
133
|
+
description: "Ask the running desktop app to create asset-driven BoardNode entries from local files, then save through desktop logic. Pass a file path unless kind is generating. Write text, markdown, or html content to a source file first and pass that path. Do not provide x/y; the desktop canvas chooses placement and returns the created node coordinates.",
|
|
134
|
+
inputSchema: objectSchema({
|
|
135
|
+
projectRoot: stringSchema(PROJECT_ROOT_DESCRIPTION),
|
|
136
|
+
nodes: {
|
|
137
|
+
type: "array",
|
|
138
|
+
minItems: 1,
|
|
139
|
+
items: {
|
|
140
|
+
type: "object",
|
|
141
|
+
additionalProperties: true,
|
|
142
|
+
properties: {
|
|
143
|
+
path: { type: "string" },
|
|
144
|
+
kind: {
|
|
145
|
+
type: "string",
|
|
146
|
+
description: "Optional compatibility hint. Use generating to create a lightweight placeholder without a path; otherwise the desktop importer determines the actual asset kind from the file.",
|
|
147
|
+
enum: ASSET_KIND_VALUES,
|
|
148
|
+
},
|
|
149
|
+
width: { type: "number" },
|
|
150
|
+
height: { type: "number" },
|
|
151
|
+
options: {
|
|
152
|
+
type: "object",
|
|
153
|
+
description: "Renderer parameters only. Do not put text, markdown, or html document content here.",
|
|
154
|
+
additionalProperties: true,
|
|
155
|
+
},
|
|
156
|
+
name: { type: "string" },
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
}, ["projectRoot", "nodes"]),
|
|
161
|
+
async run(input) {
|
|
162
|
+
const args = asRecord(input);
|
|
163
|
+
const command: McpWriteCommand = {
|
|
164
|
+
kind: "create_nodes",
|
|
165
|
+
projectRoot: readString(args, "projectRoot"),
|
|
166
|
+
nodes: readCreateNodes(args.nodes),
|
|
167
|
+
};
|
|
168
|
+
const result = await sendWriteCommand(command);
|
|
169
|
+
const project = await loadProject(command.projectRoot);
|
|
170
|
+
return {
|
|
171
|
+
nodes: result.nodes.map(toNodeDto),
|
|
172
|
+
assets: (result.assets ?? []).map((asset) => toAssetDto(project.root, asset)),
|
|
173
|
+
};
|
|
174
|
+
},
|
|
175
|
+
}),
|
|
176
|
+
tool({
|
|
177
|
+
name: "canvas.generating_node_install",
|
|
178
|
+
description: "Replace an existing generating placeholder node with a real local file asset. The desktop app imports the file, updates the node type/asset/name, and resizes the node to the real asset dimensions.",
|
|
179
|
+
inputSchema: objectSchema({
|
|
180
|
+
projectRoot: stringSchema(PROJECT_ROOT_DESCRIPTION),
|
|
181
|
+
nodeId: stringSchema("Generating board node id"),
|
|
182
|
+
path: stringSchema("Local file path to install into the generating node"),
|
|
183
|
+
}, ["projectRoot", "nodeId", "path"]),
|
|
184
|
+
async run(input) {
|
|
185
|
+
const args = asRecord(input);
|
|
186
|
+
const command: McpWriteCommand = {
|
|
187
|
+
kind: "generating_node_install",
|
|
188
|
+
projectRoot: readString(args, "projectRoot"),
|
|
189
|
+
nodeId: readString(args, "nodeId"),
|
|
190
|
+
path: readString(args, "path"),
|
|
191
|
+
};
|
|
192
|
+
const result = await sendWriteCommand(command);
|
|
193
|
+
const project = await loadProject(command.projectRoot);
|
|
194
|
+
return {
|
|
195
|
+
nodes: result.nodes.map(toNodeDto),
|
|
196
|
+
assets: (result.assets ?? []).map((asset) => toAssetDto(project.root, asset)),
|
|
197
|
+
};
|
|
198
|
+
},
|
|
199
|
+
}),
|
|
200
|
+
tool({
|
|
201
|
+
name: "canvas.update_nodes",
|
|
202
|
+
description: "Ask the running desktop app to merge node geometry, name, and node option updates, then save through desktop logic. Do not send text, markdown, or html content here; edit the source file for content changes.",
|
|
203
|
+
inputSchema: objectSchema({
|
|
204
|
+
projectRoot: stringSchema(PROJECT_ROOT_DESCRIPTION),
|
|
205
|
+
updates: {
|
|
206
|
+
type: "array",
|
|
207
|
+
minItems: 1,
|
|
208
|
+
items: {
|
|
209
|
+
type: "object",
|
|
210
|
+
additionalProperties: true,
|
|
211
|
+
required: ["id"],
|
|
212
|
+
properties: {
|
|
213
|
+
id: { type: "string" },
|
|
214
|
+
name: { type: "string" },
|
|
215
|
+
options: {
|
|
216
|
+
type: "object",
|
|
217
|
+
description: "Renderer parameters only. Do not put text, markdown, or html document content here.",
|
|
218
|
+
additionalProperties: true,
|
|
219
|
+
},
|
|
220
|
+
x: { type: "number" },
|
|
221
|
+
y: { type: "number" },
|
|
222
|
+
width: { type: "number" },
|
|
223
|
+
height: { type: "number" },
|
|
224
|
+
rotation: { type: "number" },
|
|
225
|
+
zIndex: { type: "number" },
|
|
226
|
+
locked: { type: "boolean" },
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
}, ["projectRoot", "updates"]),
|
|
231
|
+
async run(input) {
|
|
232
|
+
const args = asRecord(input);
|
|
233
|
+
const command: McpWriteCommand = {
|
|
234
|
+
kind: "update_nodes",
|
|
235
|
+
projectRoot: readString(args, "projectRoot"),
|
|
236
|
+
updates: readUpdates(args.updates),
|
|
237
|
+
};
|
|
238
|
+
const result = await sendWriteCommand(command);
|
|
239
|
+
return {
|
|
240
|
+
nodes: result.nodes.map(toNodeDto),
|
|
241
|
+
};
|
|
242
|
+
},
|
|
243
|
+
}),
|
|
244
|
+
tool({
|
|
245
|
+
name: "canvas.update_assets",
|
|
246
|
+
description: "Ask the running desktop app to replace asset metadata and media dimensions, then save through desktop logic. Do not use this to write text, markdown, or html content; edit the source file instead.",
|
|
247
|
+
inputSchema: objectSchema({
|
|
248
|
+
projectRoot: stringSchema(PROJECT_ROOT_DESCRIPTION),
|
|
249
|
+
assets: {
|
|
250
|
+
type: "array",
|
|
251
|
+
minItems: 1,
|
|
252
|
+
items: {
|
|
253
|
+
type: "object",
|
|
254
|
+
additionalProperties: true,
|
|
255
|
+
required: ["id"],
|
|
256
|
+
properties: {
|
|
257
|
+
id: { type: "string" },
|
|
258
|
+
metadata: { type: "object", additionalProperties: true },
|
|
259
|
+
width: { type: "number" },
|
|
260
|
+
height: { type: "number" },
|
|
261
|
+
duration: { type: "number" },
|
|
262
|
+
format: {
|
|
263
|
+
type: "string",
|
|
264
|
+
enum: MODEL_FORMAT_VALUES,
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
}, ["projectRoot", "assets"]),
|
|
270
|
+
async run(input) {
|
|
271
|
+
const args = asRecord(input);
|
|
272
|
+
const command: McpWriteCommand = {
|
|
273
|
+
kind: "update_assets",
|
|
274
|
+
projectRoot: readString(args, "projectRoot"),
|
|
275
|
+
assets: readAssetUpdates(args.assets),
|
|
276
|
+
};
|
|
277
|
+
const result = await sendWriteCommand(command);
|
|
278
|
+
const project = await loadProject(command.projectRoot);
|
|
279
|
+
return {
|
|
280
|
+
assets: (result.assets ?? []).map((asset) => toAssetDto(project.root, asset)),
|
|
281
|
+
};
|
|
282
|
+
},
|
|
283
|
+
}),
|
|
284
|
+
tool({
|
|
285
|
+
name: "canvas.refresh_node_preview",
|
|
286
|
+
description: "Ask the running desktop app to regenerate a text, markdown, or html node preview from its source file using the node's current bounds.",
|
|
287
|
+
inputSchema: objectSchema({
|
|
288
|
+
projectRoot: stringSchema(PROJECT_ROOT_DESCRIPTION),
|
|
289
|
+
nodeId: stringSchema("Text, markdown, or html board node id"),
|
|
290
|
+
}, ["projectRoot", "nodeId"]),
|
|
291
|
+
async run(input) {
|
|
292
|
+
const args = asRecord(input);
|
|
293
|
+
const command: McpWriteCommand = {
|
|
294
|
+
kind: "refresh_node_preview",
|
|
295
|
+
projectRoot: readString(args, "projectRoot"),
|
|
296
|
+
nodeId: readString(args, "nodeId"),
|
|
297
|
+
};
|
|
298
|
+
const result = await sendWriteCommand(command);
|
|
299
|
+
const project = await loadProject(command.projectRoot);
|
|
300
|
+
return {
|
|
301
|
+
nodes: result.nodes.map(toNodeDto),
|
|
302
|
+
assets: (result.assets ?? []).map((asset) => toAssetDto(project.root, asset)),
|
|
303
|
+
};
|
|
304
|
+
},
|
|
305
|
+
}),
|
|
306
|
+
tool({
|
|
307
|
+
name: "canvas.get_origin_asset_by_node",
|
|
308
|
+
description: "Return original asset information for a file-backed board node.",
|
|
309
|
+
inputSchema: objectSchema({
|
|
310
|
+
projectRoot: stringSchema(PROJECT_ROOT_DESCRIPTION),
|
|
311
|
+
nodeId: stringSchema("Board node id"),
|
|
312
|
+
}, ["projectRoot", "nodeId"]),
|
|
313
|
+
async run(input) {
|
|
314
|
+
const args = asRecord(input);
|
|
315
|
+
const project = await loadProject(readString(args, "projectRoot"));
|
|
316
|
+
const node = getNodeOrThrow(project.snapshot, args.nodeId);
|
|
317
|
+
const asset = getOriginAssetForNode(project.snapshot, args.nodeId);
|
|
318
|
+
return {
|
|
319
|
+
node: toNodeDto(node),
|
|
320
|
+
asset: {
|
|
321
|
+
...toAssetDto(project.root, asset),
|
|
322
|
+
accessibleUrl: asset.webLink ?? asset.sourceUrl ?? null,
|
|
323
|
+
},
|
|
324
|
+
};
|
|
325
|
+
},
|
|
326
|
+
}),
|
|
327
|
+
tool({
|
|
328
|
+
name: "canvas.get_asset",
|
|
329
|
+
description: "Return compact asset details including original, derivatives, and metadata.",
|
|
330
|
+
inputSchema: objectSchema({
|
|
331
|
+
projectRoot: stringSchema(PROJECT_ROOT_DESCRIPTION),
|
|
332
|
+
assetId: stringSchema("Asset id"),
|
|
333
|
+
}, ["projectRoot", "assetId"]),
|
|
334
|
+
async run(input) {
|
|
335
|
+
const args = asRecord(input);
|
|
336
|
+
const project = await loadProject(readString(args, "projectRoot"));
|
|
337
|
+
return {
|
|
338
|
+
asset: toAssetDto(project.root, getAssetOrThrow(project.snapshot, args.assetId)),
|
|
339
|
+
};
|
|
340
|
+
},
|
|
341
|
+
}),
|
|
342
|
+
tool({
|
|
343
|
+
name: "canvas.read_project_info",
|
|
344
|
+
description: "Read compact project info, counts, viewport, and update timestamps.",
|
|
345
|
+
inputSchema: objectSchema({
|
|
346
|
+
projectRoot: stringSchema(PROJECT_ROOT_DESCRIPTION),
|
|
347
|
+
}),
|
|
348
|
+
async run(input) {
|
|
349
|
+
const project = await loadProject(readString(input, "projectRoot"));
|
|
350
|
+
return {
|
|
351
|
+
projectRoot: project.root,
|
|
352
|
+
boardPath: project.boardPath,
|
|
353
|
+
assetsPath: project.assetsPath,
|
|
354
|
+
nodeCount: project.snapshot.nodes.length,
|
|
355
|
+
assetCount: project.snapshot.assets.length,
|
|
356
|
+
viewport: project.snapshot.viewport ?? null,
|
|
357
|
+
updatedAt: Math.max(project.boardUpdatedAt ?? 0, project.assetsUpdatedAt ?? 0),
|
|
358
|
+
boardUpdatedAt: project.boardUpdatedAt,
|
|
359
|
+
assetsUpdatedAt: project.assetsUpdatedAt,
|
|
360
|
+
};
|
|
361
|
+
},
|
|
362
|
+
}),
|
|
363
|
+
];
|
|
364
|
+
|
|
365
|
+
function tool(definition: Omit<BoardTool, "kind" | "outputSchema"> & { outputSchema?: JSONSchema }): BoardTool {
|
|
366
|
+
return {
|
|
367
|
+
kind: "builtin",
|
|
368
|
+
outputSchema: definition.outputSchema ?? anyOutputSchema,
|
|
369
|
+
...definition,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function objectSchema(properties: Record<string, unknown>, required = ["projectRoot"]) {
|
|
374
|
+
return {
|
|
375
|
+
type: "object",
|
|
376
|
+
additionalProperties: false,
|
|
377
|
+
required,
|
|
378
|
+
properties,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function stringSchema(description: string) {
|
|
383
|
+
return { type: "string", description };
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function readCreateNodes(value: unknown): McpCreateNodeInput[] {
|
|
387
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
388
|
+
throw new BoardToolUserError("nodes must be a non-empty array");
|
|
389
|
+
}
|
|
390
|
+
return value.map((entry, index) => {
|
|
391
|
+
const node = asRecord(entry, `nodes[${index}]`);
|
|
392
|
+
if (node.x !== undefined || node.y !== undefined) {
|
|
393
|
+
throw new BoardToolUserError(`nodes[${index}].x/y is not supported; the desktop canvas chooses placement`);
|
|
394
|
+
}
|
|
395
|
+
if (node.kind !== undefined && !isAssetKind(node.kind)) {
|
|
396
|
+
throw new BoardToolUserError(`nodes[${index}].kind must be one of ${ASSET_KIND_VALUES.join(", ")}`);
|
|
397
|
+
}
|
|
398
|
+
const kind = node.kind;
|
|
399
|
+
const hasPath = typeof node.path === "string" && node.path.trim() !== "";
|
|
400
|
+
if (kind === "generating") {
|
|
401
|
+
if (node.path !== undefined && !hasPath) {
|
|
402
|
+
throw new BoardToolUserError(`nodes[${index}].path must be a non-empty string when provided`);
|
|
403
|
+
}
|
|
404
|
+
} else if (!hasPath) {
|
|
405
|
+
throw new BoardToolUserError(`nodes[${index}].path must be a non-empty string unless kind is generating`);
|
|
406
|
+
}
|
|
407
|
+
if (node.metadata !== undefined) {
|
|
408
|
+
throw new BoardToolUserError(`nodes[${index}].metadata is not supported; write content to a source file and pass path`);
|
|
409
|
+
}
|
|
410
|
+
if (node.text !== undefined) {
|
|
411
|
+
throw new BoardToolUserError(`nodes[${index}].text is not supported; write content to a source file and pass path`);
|
|
412
|
+
}
|
|
413
|
+
if (node.options !== undefined) {
|
|
414
|
+
asRecord(node.options, `nodes[${index}].options`);
|
|
415
|
+
}
|
|
416
|
+
return node as McpCreateNodeInput;
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function isAssetKind(value: unknown): value is NonNullable<McpCreateNodeInput["kind"]> {
|
|
421
|
+
return typeof value === "string" && ASSET_KIND_VALUES.includes(value as NonNullable<McpCreateNodeInput["kind"]>);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function readAssetUpdates(value: unknown): McpUpdateAssetInput[] {
|
|
425
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
426
|
+
throw new BoardToolUserError("assets must be a non-empty array");
|
|
427
|
+
}
|
|
428
|
+
return value.map((entry, index) => {
|
|
429
|
+
const update = asRecord(entry, `assets[${index}]`);
|
|
430
|
+
readString(update, "id");
|
|
431
|
+
readOptionalNumber(update, "width", `assets[${index}]`);
|
|
432
|
+
readOptionalNumber(update, "height", `assets[${index}]`);
|
|
433
|
+
readOptionalNumber(update, "duration", `assets[${index}]`);
|
|
434
|
+
if (update.format !== undefined && !MODEL_FORMAT_VALUES.includes(update.format as ModelAssetFormat)) {
|
|
435
|
+
throw new BoardToolUserError(`assets[${index}].format must be a supported model format`);
|
|
436
|
+
}
|
|
437
|
+
if (update.metadata !== undefined) {
|
|
438
|
+
const metadata = asRecord(update.metadata, `assets[${index}].metadata`);
|
|
439
|
+
assertNoContentMetadata(metadata, `assets[${index}].metadata`);
|
|
440
|
+
}
|
|
441
|
+
return update as McpUpdateAssetInput;
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function assertNoContentMetadata(metadata: Record<string, unknown>, label: string): void {
|
|
446
|
+
for (const key of ["html", "markdown", "text", "content"]) {
|
|
447
|
+
if (metadata[key] !== undefined) {
|
|
448
|
+
throw new BoardToolUserError(`${label}.${key} is not supported; edit the source file instead`);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function readUpdates(value: unknown): McpUpdateNodeInput[] {
|
|
454
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
455
|
+
throw new BoardToolUserError("updates must be a non-empty array");
|
|
456
|
+
}
|
|
457
|
+
return value.map((entry, index) => {
|
|
458
|
+
const update = asRecord(entry, `updates[${index}]`);
|
|
459
|
+
readString(update, "id");
|
|
460
|
+
if ("text" in update) {
|
|
461
|
+
throw new BoardToolUserError(`updates[${index}].text is not supported; edit the source file instead`);
|
|
462
|
+
}
|
|
463
|
+
readOptionalNumber(update, "x", `updates[${index}]`);
|
|
464
|
+
readOptionalNumber(update, "y", `updates[${index}]`);
|
|
465
|
+
readOptionalNumber(update, "width", `updates[${index}]`);
|
|
466
|
+
readOptionalNumber(update, "height", `updates[${index}]`);
|
|
467
|
+
readOptionalNumber(update, "rotation", `updates[${index}]`);
|
|
468
|
+
readOptionalNumber(update, "zIndex", `updates[${index}]`);
|
|
469
|
+
if (update.options !== undefined) {
|
|
470
|
+
asRecord(update.options, `updates[${index}].options`);
|
|
471
|
+
}
|
|
472
|
+
return update as McpUpdateNodeInput;
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function readString(input: unknown, key: string): string {
|
|
477
|
+
const record = asRecord(input);
|
|
478
|
+
const value = record[key];
|
|
479
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
480
|
+
throw new BoardToolUserError(`${key} must be a non-empty string`);
|
|
481
|
+
}
|
|
482
|
+
return value;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function readOptionalNumber(input: Record<string, unknown>, key: string, label: string): void {
|
|
486
|
+
const value = input[key];
|
|
487
|
+
if (value !== undefined && (typeof value !== "number" || !Number.isFinite(value))) {
|
|
488
|
+
throw new BoardToolUserError(`${label}.${key} must be a finite number`);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function asRecord(input: unknown, label = "input"): Record<string, unknown> {
|
|
493
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
494
|
+
throw new BoardToolUserError(`${label} must be an object`);
|
|
495
|
+
}
|
|
496
|
+
return input as Record<string, unknown>;
|
|
497
|
+
}
|