@kezlahd/atlas-mcp 0.1.0 → 0.2.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/README.md +113 -77
- package/dist/api.js +3 -0
- package/dist/api.js.map +1 -1
- package/dist/index.js +2925 -307
- package/dist/index.js.map +1 -1
- package/dist/section-catalog.js +42 -8
- package/dist/section-catalog.js.map +1 -1
- package/dist/tools/read-achievements.js +32 -0
- package/dist/tools/read-achievements.js.map +1 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -20,11 +20,12 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
20
20
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
21
21
|
import { apiRequest, ApiError, resolveConfig } from "./api.js";
|
|
22
22
|
import { SECTION_CATALOG, findSection } from "./section-catalog.js";
|
|
23
|
+
import { READ_ACHIEVEMENTS_TOOL, readAchievements, } from "./tools/read-achievements.js";
|
|
23
24
|
/* ─── Tool definitions ─────────────────────────────────────────────── */
|
|
24
25
|
const TOOLS = [
|
|
25
26
|
{
|
|
26
27
|
name: "atlas_list_docs",
|
|
27
|
-
description: "List docs in the current Atlas workspace. Returns { docs, next_cursor }. Use this to find a docId before pulling a template. To page through more, pass the previous next_cursor back in as `cursor`.",
|
|
28
|
+
description: "List docs in the current Atlas workspace. Returns { docs, next_cursor }. Use this to find a docId before pulling a template. To page through more, pass the previous next_cursor back in as `cursor`. Pass `deleted: true` to list the Recently deleted bucket instead of live docs.",
|
|
28
29
|
inputSchema: {
|
|
29
30
|
type: "object",
|
|
30
31
|
properties: {
|
|
@@ -38,7 +39,114 @@ const TOOLS = [
|
|
|
38
39
|
type: "string",
|
|
39
40
|
description: "Opaque next_cursor from a previous response. Omit for the first page.",
|
|
40
41
|
},
|
|
42
|
+
deleted: {
|
|
43
|
+
type: "boolean",
|
|
44
|
+
description: "When true, list ONLY soft-deleted docs (the Recently deleted bucket). Default: false — live docs only.",
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: "atlas_doc_create",
|
|
51
|
+
description: "Create a new doc in the current workspace. Returns the fresh doc's id + etag so you can pipe straight into atlas_update_section / atlas_update_template. Default kind is 'template' (the doc-crafter surface); pass 'freeform' for a Tiptap doc. Optional folder_id / shoot_id slot the doc into the same tree the web UI uses. Pass `preset_id` to stamp a server-side preset skeleton (see atlas_list_doc_presets for the catalogue) — mutually exclusive with hand-rolled template_content (template_content wins if both are set).",
|
|
52
|
+
inputSchema: {
|
|
53
|
+
type: "object",
|
|
54
|
+
properties: {
|
|
55
|
+
title: { type: "string", description: "Human-readable title (≤ 200 chars)." },
|
|
56
|
+
doc_kind: {
|
|
57
|
+
type: "string",
|
|
58
|
+
enum: ["template", "freeform"],
|
|
59
|
+
description: "Defaults to 'template' if omitted.",
|
|
60
|
+
},
|
|
61
|
+
folder_id: {
|
|
62
|
+
type: "string",
|
|
63
|
+
description: "Optional doc_folders id to nest under.",
|
|
64
|
+
},
|
|
65
|
+
shoot_id: {
|
|
66
|
+
type: "string",
|
|
67
|
+
description: "Optional shoots id — attach the doc to a shoot.",
|
|
68
|
+
},
|
|
69
|
+
template_content: {
|
|
70
|
+
type: "object",
|
|
71
|
+
description: "Optional starter { cover?, docSettings?, sections: [] } payload. Omit to create empty and fill later.",
|
|
72
|
+
},
|
|
73
|
+
preset_id: {
|
|
74
|
+
type: "string",
|
|
75
|
+
description: "Optional preset id from atlas_list_doc_presets. Stamps that preset's section skeleton into the new doc. Ignored when template_content is also supplied.",
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
required: ["title"],
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: "atlas_list_doc_presets",
|
|
83
|
+
description: "List every doc preset available in Atlas (shoot-day, deliverable, planning templates). Returns the full catalogue with each preset's id, label, blurb, category, estimated fill time, default title, and the ordered list of sections it'll stamp. Use this to show the user options before calling atlas_create_doc or atlas_create_doc_from_preset.",
|
|
84
|
+
inputSchema: {
|
|
85
|
+
type: "object",
|
|
86
|
+
properties: {},
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: "atlas_create_doc_from_preset",
|
|
91
|
+
description: "Convenience wrapper over atlas_doc_create — creates a new doc and stamps the chosen preset's section skeleton in one call. Use when the user says 'create me a model shoot day doc' or similar. Title defaults to the preset's defaultTitle; override to taste. Returns the created doc record (same shape as atlas_doc_create).",
|
|
92
|
+
inputSchema: {
|
|
93
|
+
type: "object",
|
|
94
|
+
properties: {
|
|
95
|
+
preset_id: {
|
|
96
|
+
type: "string",
|
|
97
|
+
description: "Preset id from atlas_list_doc_presets (e.g. 'model-shoot-day', 'brand-narrative-drop').",
|
|
98
|
+
},
|
|
99
|
+
title: {
|
|
100
|
+
type: "string",
|
|
101
|
+
description: "Override for the doc title. Defaults to the preset's defaultTitle when omitted.",
|
|
102
|
+
},
|
|
103
|
+
folder_id: {
|
|
104
|
+
type: "string",
|
|
105
|
+
description: "Optional doc_folders id to nest under.",
|
|
106
|
+
},
|
|
107
|
+
shoot_id: {
|
|
108
|
+
type: "string",
|
|
109
|
+
description: "Optional shoots id — attach the doc to a shoot.",
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
required: ["preset_id"],
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
name: "atlas_doc_delete",
|
|
117
|
+
description: "Soft-delete a doc — moves it to the workspace's Recently deleted bucket for 30 days. Reversible with atlas_restore_doc. Pass `purge: true` to hard-delete forever instead (requires the doc is already soft-deleted). Destructive — always confirm with the user before calling. Returns { ok, id, title, deleted, recoverable_until? } or { ok, id, purged: true }.",
|
|
118
|
+
inputSchema: {
|
|
119
|
+
type: "object",
|
|
120
|
+
properties: {
|
|
121
|
+
docId: { type: "string", description: "Doc id from atlas_list_docs." },
|
|
122
|
+
purge: {
|
|
123
|
+
type: "boolean",
|
|
124
|
+
description: "When true, hard-delete forever (requires the doc is already soft-deleted). Default: false — soft-delete only.",
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
required: ["docId"],
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
name: "atlas_restore_doc",
|
|
132
|
+
description: "Restore a soft-deleted doc from the Recently deleted bucket. Requires templates:update. Silent no-op on a live doc (responds with restored:false).",
|
|
133
|
+
inputSchema: {
|
|
134
|
+
type: "object",
|
|
135
|
+
properties: {
|
|
136
|
+
docId: { type: "string", description: "Doc id from atlas_list_docs?deleted=1." },
|
|
137
|
+
},
|
|
138
|
+
required: ["docId"],
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
name: "atlas_purge_doc",
|
|
143
|
+
description: "Hard-delete a soft-deleted doc — permanent, no 30-day undo. Requires the doc is already in the trash (returns 409 otherwise, prompting you to atlas_doc_delete first). Always double-confirm with the user before calling.",
|
|
144
|
+
inputSchema: {
|
|
145
|
+
type: "object",
|
|
146
|
+
properties: {
|
|
147
|
+
docId: { type: "string", description: "Doc id (must already be soft-deleted)." },
|
|
41
148
|
},
|
|
149
|
+
required: ["docId"],
|
|
42
150
|
},
|
|
43
151
|
},
|
|
44
152
|
{
|
|
@@ -65,7 +173,7 @@ const TOOLS = [
|
|
|
65
173
|
},
|
|
66
174
|
{
|
|
67
175
|
name: "atlas_update_section",
|
|
68
|
-
description: "Edit a single section by id.
|
|
176
|
+
description: "Edit a single section by id. Uses the section-scoped PATCH endpoint — the server merges just this section into template_content, so concurrent Claude sessions editing different sections no longer clobber each other. `data` replaces the section's data object; `style` merges over existing style keys.",
|
|
69
177
|
inputSchema: {
|
|
70
178
|
type: "object",
|
|
71
179
|
properties: {
|
|
@@ -79,8 +187,12 @@ const TOOLS = [
|
|
|
79
187
|
type: "object",
|
|
80
188
|
description: "Optional section.style overrides (titleFont, titleSize, breakBefore, etc). Merged into existing style.",
|
|
81
189
|
},
|
|
190
|
+
hidden: {
|
|
191
|
+
type: "boolean",
|
|
192
|
+
description: "Optional — mark section hidden or visible.",
|
|
193
|
+
},
|
|
82
194
|
},
|
|
83
|
-
required: ["docId", "sectionId"
|
|
195
|
+
required: ["docId", "sectionId"],
|
|
84
196
|
},
|
|
85
197
|
},
|
|
86
198
|
{
|
|
@@ -178,7 +290,7 @@ const TOOLS = [
|
|
|
178
290
|
},
|
|
179
291
|
{
|
|
180
292
|
name: "atlas_list_gear",
|
|
181
|
-
description: "List gear items (brand, name, quantity, photo) in the workspace. Returns { items, next_cursor }. Each item has a 1-hour signed image_url when it has an image. To page through more, pass the previous next_cursor back in as `cursor`.",
|
|
293
|
+
description: "List gear items (brand, name, quantity, photo) in the workspace. Returns { items, next_cursor }. Each item has a 1-hour signed image_url when it has an image. To page through more, pass the previous next_cursor back in as `cursor`. Pass `deleted: true` to list the Recently deleted bucket instead of live gear.",
|
|
182
294
|
inputSchema: {
|
|
183
295
|
type: "object",
|
|
184
296
|
properties: {
|
|
@@ -187,6 +299,10 @@ const TOOLS = [
|
|
|
187
299
|
type: "string",
|
|
188
300
|
description: "Opaque next_cursor from a previous response. Omit for the first page.",
|
|
189
301
|
},
|
|
302
|
+
deleted: {
|
|
303
|
+
type: "boolean",
|
|
304
|
+
description: "When true, list ONLY soft-deleted gear (the Recently deleted bucket). Default: false — live gear only.",
|
|
305
|
+
},
|
|
190
306
|
},
|
|
191
307
|
},
|
|
192
308
|
},
|
|
@@ -234,7 +350,33 @@ const TOOLS = [
|
|
|
234
350
|
},
|
|
235
351
|
{
|
|
236
352
|
name: "atlas_delete_gear",
|
|
237
|
-
description: "
|
|
353
|
+
description: "Soft-delete a gear item — moves it to the Recently deleted bucket for 30 days. Reversible with atlas_restore_gear. Pass `purge: true` to hard-delete forever (requires the item is already soft-deleted). Always confirm with the user before calling.",
|
|
354
|
+
inputSchema: {
|
|
355
|
+
type: "object",
|
|
356
|
+
properties: {
|
|
357
|
+
gearId: { type: "string" },
|
|
358
|
+
purge: {
|
|
359
|
+
type: "boolean",
|
|
360
|
+
description: "When true, hard-delete forever (requires the item is already soft-deleted). Default: false — soft-delete only.",
|
|
361
|
+
},
|
|
362
|
+
},
|
|
363
|
+
required: ["gearId"],
|
|
364
|
+
},
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
name: "atlas_restore_gear",
|
|
368
|
+
description: "Restore a soft-deleted gear item from the Recently deleted bucket. Requires gear:update.",
|
|
369
|
+
inputSchema: {
|
|
370
|
+
type: "object",
|
|
371
|
+
properties: {
|
|
372
|
+
gearId: { type: "string", description: "Gear id (from atlas_list_gear with deleted:true)." },
|
|
373
|
+
},
|
|
374
|
+
required: ["gearId"],
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
{
|
|
378
|
+
name: "atlas_purge_gear",
|
|
379
|
+
description: "Hard-delete a soft-deleted gear item — permanent, no undo. Requires the item is already in the trash (409 otherwise). Always double-confirm with the user.",
|
|
238
380
|
inputSchema: {
|
|
239
381
|
type: "object",
|
|
240
382
|
properties: {
|
|
@@ -262,188 +404,1394 @@ const TOOLS = [
|
|
|
262
404
|
},
|
|
263
405
|
},
|
|
264
406
|
{
|
|
265
|
-
name: "
|
|
266
|
-
description: "
|
|
407
|
+
name: "atlas_list_shots",
|
|
408
|
+
description: "List shot items (still/video with reference photo + linked gear) in the workspace. Requires shots:list. Returns { items, next_cursor }. Each item has a 1-hour signed image_url when it has an image and a gear_ids array of gear_items UUIDs. To page through more, pass the previous next_cursor back in as `cursor`. Pass `deleted: true` to list the Recently deleted bucket instead of live shots.",
|
|
267
409
|
inputSchema: {
|
|
268
410
|
type: "object",
|
|
269
411
|
properties: {
|
|
270
|
-
|
|
271
|
-
|
|
412
|
+
limit: { type: "number", description: "Default 50, max 500." },
|
|
413
|
+
cursor: {
|
|
414
|
+
type: "string",
|
|
415
|
+
description: "Opaque next_cursor from a previous response. Omit for the first page.",
|
|
416
|
+
},
|
|
417
|
+
deleted: {
|
|
418
|
+
type: "boolean",
|
|
419
|
+
description: "When true, list ONLY soft-deleted shots (the Recently deleted bucket). Default: false — live shots only.",
|
|
420
|
+
},
|
|
272
421
|
},
|
|
273
|
-
required: ["path", "new_name"],
|
|
274
422
|
},
|
|
275
423
|
},
|
|
276
424
|
{
|
|
277
|
-
name: "
|
|
278
|
-
description: "
|
|
425
|
+
name: "atlas_add_shot",
|
|
426
|
+
description: "Add a single shot item. Requires shots:add. For many items at once, use atlas_import_shots. `gear_ids` must resolve to gear_items in this workspace — unknown ids surface as a 400 with the offending ids listed.",
|
|
279
427
|
inputSchema: {
|
|
280
428
|
type: "object",
|
|
281
429
|
properties: {
|
|
282
|
-
|
|
283
|
-
|
|
430
|
+
name: { type: "string", description: "Required. ≤ 160 chars." },
|
|
431
|
+
kind: {
|
|
432
|
+
type: "string",
|
|
433
|
+
enum: ["still", "video"],
|
|
434
|
+
description: "Defaults to 'still' when omitted.",
|
|
435
|
+
},
|
|
436
|
+
notes: { type: "string", description: "Optional library-general notes. ≤ 2000 chars." },
|
|
437
|
+
image_path: {
|
|
438
|
+
type: "string",
|
|
439
|
+
description: "Optional. Storage key from atlas_upload_file. Must be in this workspace's prefix.",
|
|
440
|
+
},
|
|
441
|
+
gear_ids: {
|
|
442
|
+
type: "array",
|
|
443
|
+
items: { type: "string" },
|
|
444
|
+
description: "Optional. gear_items UUIDs to link to this shot (from atlas_list_gear).",
|
|
445
|
+
},
|
|
284
446
|
},
|
|
285
|
-
required: ["
|
|
447
|
+
required: ["name"],
|
|
286
448
|
},
|
|
287
449
|
},
|
|
288
450
|
{
|
|
289
|
-
name: "
|
|
290
|
-
description: "
|
|
451
|
+
name: "atlas_update_shot",
|
|
452
|
+
description: "Update an existing shot item. Requires shots:update. Only provided fields are patched — send just the fields you want to change. Empty string on `image_path` or `notes` clears them. `gear_ids` replaces the existing link array (pass [] to clear all links).",
|
|
291
453
|
inputSchema: {
|
|
292
454
|
type: "object",
|
|
293
455
|
properties: {
|
|
294
|
-
|
|
295
|
-
|
|
456
|
+
shotId: { type: "string", description: "Shot item id from atlas_list_shots." },
|
|
457
|
+
name: { type: "string", description: "New shot name (≤ 160 chars)." },
|
|
458
|
+
kind: {
|
|
459
|
+
type: "string",
|
|
460
|
+
enum: ["still", "video"],
|
|
461
|
+
},
|
|
462
|
+
notes: {
|
|
463
|
+
type: "string",
|
|
464
|
+
description: "New library-general notes (≤ 2000 chars). Empty string clears.",
|
|
465
|
+
},
|
|
466
|
+
image_path: {
|
|
467
|
+
type: "string",
|
|
468
|
+
description: "Storage path from atlas_upload_file. Must live in this workspace's prefix. Empty string clears the image.",
|
|
469
|
+
},
|
|
470
|
+
gear_ids: {
|
|
471
|
+
type: "array",
|
|
472
|
+
items: { type: "string" },
|
|
473
|
+
description: "Full replacement array of gear_items UUIDs. Unknown ids for the workspace return a 400.",
|
|
474
|
+
},
|
|
296
475
|
},
|
|
297
|
-
required: ["
|
|
476
|
+
required: ["shotId"],
|
|
298
477
|
},
|
|
299
478
|
},
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
479
|
+
{
|
|
480
|
+
name: "atlas_delete_shot",
|
|
481
|
+
description: "Soft-delete a shot item — moves it to the Recently deleted bucket for 30 days. Reversible with atlas_restore_shot. Pass `purge: true` to hard-delete forever (requires the item is already soft-deleted). Reference image in workspace-photos is preserved. Requires shots:delete. Always confirm with the user.",
|
|
482
|
+
inputSchema: {
|
|
483
|
+
type: "object",
|
|
484
|
+
properties: {
|
|
485
|
+
shotId: { type: "string" },
|
|
486
|
+
purge: {
|
|
487
|
+
type: "boolean",
|
|
488
|
+
description: "When true, hard-delete forever (requires the item is already soft-deleted). Default: false — soft-delete only.",
|
|
489
|
+
},
|
|
490
|
+
},
|
|
491
|
+
required: ["shotId"],
|
|
492
|
+
},
|
|
493
|
+
},
|
|
494
|
+
{
|
|
495
|
+
name: "atlas_restore_shot",
|
|
496
|
+
description: "Restore a soft-deleted shot item from the Recently deleted bucket. Requires shots:update.",
|
|
497
|
+
inputSchema: {
|
|
498
|
+
type: "object",
|
|
499
|
+
properties: {
|
|
500
|
+
shotId: { type: "string" },
|
|
501
|
+
},
|
|
502
|
+
required: ["shotId"],
|
|
503
|
+
},
|
|
504
|
+
},
|
|
505
|
+
{
|
|
506
|
+
name: "atlas_purge_shot",
|
|
507
|
+
description: "Hard-delete a soft-deleted shot item — permanent, no undo. Requires the item is already in the trash (409 otherwise). Always double-confirm with the user.",
|
|
508
|
+
inputSchema: {
|
|
509
|
+
type: "object",
|
|
510
|
+
properties: {
|
|
511
|
+
shotId: { type: "string" },
|
|
512
|
+
},
|
|
513
|
+
required: ["shotId"],
|
|
514
|
+
},
|
|
515
|
+
},
|
|
516
|
+
{
|
|
517
|
+
name: "atlas_import_shots",
|
|
518
|
+
description: "Bulk-import shot rows in one shot. Requires shots:import. Faster than calling atlas_add_shot per item. `mode: 'replace'` clears the workspace's shot library before inserting; `'append'` (default) adds on top. The endpoint only imports name/kind/notes — wire gear links up per-row via atlas_update_shot after the bulk insert.",
|
|
519
|
+
inputSchema: {
|
|
520
|
+
type: "object",
|
|
521
|
+
properties: {
|
|
522
|
+
mode: { type: "string", enum: ["append", "replace"] },
|
|
523
|
+
rows: {
|
|
524
|
+
type: "array",
|
|
525
|
+
description: "Up to 2000 rows. Each: { name (required), kind?, notes? }.",
|
|
526
|
+
items: {
|
|
527
|
+
type: "object",
|
|
528
|
+
properties: {
|
|
529
|
+
name: { type: "string" },
|
|
530
|
+
kind: { type: "string", enum: ["still", "video"] },
|
|
531
|
+
notes: { type: "string" },
|
|
532
|
+
},
|
|
533
|
+
required: ["name"],
|
|
534
|
+
},
|
|
535
|
+
},
|
|
536
|
+
},
|
|
537
|
+
required: ["rows"],
|
|
538
|
+
},
|
|
539
|
+
},
|
|
540
|
+
{
|
|
541
|
+
name: "atlas_list_events",
|
|
542
|
+
description: "List calendar events (shoots) in the workspace. Requires events:list. Returns { items, next_cursor }. Each item is a ShootSummary: id, title, starts_at, ends_at, all_day, location, shoot_type, colour_key, colour_hex, status, team_count, objectives_total/done. Filter by date range with `from`/`to` (ISO 8601), by status, or by colour_key. To page through more, pass the previous next_cursor back in as `cursor`.",
|
|
543
|
+
inputSchema: {
|
|
544
|
+
type: "object",
|
|
545
|
+
properties: {
|
|
546
|
+
limit: { type: "number", description: "Default 50, max 500." },
|
|
547
|
+
cursor: {
|
|
548
|
+
type: "string",
|
|
549
|
+
description: "Opaque next_cursor from a previous response. Omit for the first page.",
|
|
550
|
+
},
|
|
551
|
+
from: {
|
|
552
|
+
type: "string",
|
|
553
|
+
description: "ISO 8601 lower bound on starts_at (inclusive).",
|
|
554
|
+
},
|
|
555
|
+
to: {
|
|
556
|
+
type: "string",
|
|
557
|
+
description: "ISO 8601 upper bound on starts_at (inclusive).",
|
|
558
|
+
},
|
|
559
|
+
status: {
|
|
560
|
+
type: "array",
|
|
561
|
+
items: {
|
|
562
|
+
type: "string",
|
|
563
|
+
enum: ["planned", "confirmed", "wrapped", "cancelled"],
|
|
564
|
+
},
|
|
565
|
+
description: "Filter to these statuses only.",
|
|
566
|
+
},
|
|
567
|
+
colour: {
|
|
568
|
+
type: "array",
|
|
569
|
+
items: {
|
|
570
|
+
type: "string",
|
|
571
|
+
enum: ["rose", "amber", "sky", "violet", "lime", "stone"],
|
|
572
|
+
},
|
|
573
|
+
description: "Filter to these colour keys only.",
|
|
574
|
+
},
|
|
575
|
+
},
|
|
576
|
+
},
|
|
577
|
+
},
|
|
578
|
+
{
|
|
579
|
+
name: "atlas_get_event",
|
|
580
|
+
description: "Fetch a single calendar event + its crew assignments. Requires events:get. Returns { event, assignments }. Each assignment carries the workspace member's display_name, email, shoot_role, responsibilities[] and per-assignment notes.",
|
|
581
|
+
inputSchema: {
|
|
582
|
+
type: "object",
|
|
583
|
+
properties: {
|
|
584
|
+
eventId: {
|
|
585
|
+
type: "string",
|
|
586
|
+
description: "Event id from atlas_list_events.",
|
|
587
|
+
},
|
|
588
|
+
},
|
|
589
|
+
required: ["eventId"],
|
|
590
|
+
},
|
|
591
|
+
},
|
|
592
|
+
{
|
|
593
|
+
name: "atlas_create_event",
|
|
594
|
+
description: "Schedule a new calendar event. Requires events:create. `title` and `starts_at` (ISO 8601) are required. Timed events must be at least 30 minutes — the API rejects shorter durations. Defaults: `status` = 'planned', `all_day` = false. Returns { event: ShootSummary }.",
|
|
595
|
+
inputSchema: {
|
|
596
|
+
type: "object",
|
|
597
|
+
properties: {
|
|
598
|
+
title: { type: "string", description: "Event title. Required." },
|
|
599
|
+
starts_at: {
|
|
600
|
+
type: "string",
|
|
601
|
+
description: "ISO 8601 start timestamp. Required.",
|
|
602
|
+
},
|
|
603
|
+
ends_at: { type: "string", description: "ISO 8601 end timestamp." },
|
|
604
|
+
all_day: { type: "boolean", description: "Mark as an all-day event." },
|
|
605
|
+
location: { type: "string" },
|
|
606
|
+
shoot_type: {
|
|
607
|
+
type: "string",
|
|
608
|
+
description: "Free-form shoot type label.",
|
|
609
|
+
},
|
|
610
|
+
colour_key: {
|
|
611
|
+
type: "string",
|
|
612
|
+
enum: ["rose", "amber", "sky", "violet", "lime", "stone"],
|
|
613
|
+
},
|
|
614
|
+
colour_hex: {
|
|
615
|
+
type: "string",
|
|
616
|
+
description: "Custom colour hex, e.g. #ff8899.",
|
|
617
|
+
},
|
|
618
|
+
status: {
|
|
619
|
+
type: "string",
|
|
620
|
+
enum: ["planned", "confirmed", "wrapped", "cancelled"],
|
|
621
|
+
},
|
|
622
|
+
notes: { type: "string", description: "Internal notes." },
|
|
623
|
+
description: { type: "string", description: "Public description." },
|
|
624
|
+
},
|
|
625
|
+
required: ["title", "starts_at"],
|
|
626
|
+
},
|
|
627
|
+
},
|
|
628
|
+
{
|
|
629
|
+
name: "atlas_update_event",
|
|
630
|
+
description: "Patch a calendar event. Requires events:update. Only provided fields are updated — send just the fields you want to change. Same validation as create (timed events ≥ 30 minutes).",
|
|
631
|
+
inputSchema: {
|
|
632
|
+
type: "object",
|
|
633
|
+
properties: {
|
|
634
|
+
eventId: {
|
|
635
|
+
type: "string",
|
|
636
|
+
description: "Event id from atlas_list_events.",
|
|
637
|
+
},
|
|
638
|
+
title: { type: "string" },
|
|
639
|
+
starts_at: { type: "string", description: "New ISO 8601 start." },
|
|
640
|
+
ends_at: { type: "string", description: "New ISO 8601 end." },
|
|
641
|
+
all_day: { type: "boolean" },
|
|
642
|
+
location: { type: "string" },
|
|
643
|
+
shoot_type: { type: "string" },
|
|
644
|
+
colour_key: {
|
|
645
|
+
type: "string",
|
|
646
|
+
enum: ["rose", "amber", "sky", "violet", "lime", "stone"],
|
|
647
|
+
},
|
|
648
|
+
colour_hex: { type: "string" },
|
|
649
|
+
status: {
|
|
650
|
+
type: "string",
|
|
651
|
+
enum: ["planned", "confirmed", "wrapped", "cancelled"],
|
|
652
|
+
},
|
|
653
|
+
notes: { type: "string" },
|
|
654
|
+
description: { type: "string" },
|
|
655
|
+
},
|
|
656
|
+
required: ["eventId"],
|
|
657
|
+
},
|
|
658
|
+
},
|
|
659
|
+
{
|
|
660
|
+
name: "atlas_delete_event",
|
|
661
|
+
description: "Permanently delete a calendar event. Requires events:delete. Permanent — no trash for events, no atlas_restore_event. Always confirm with the user before calling; there is no undo.",
|
|
662
|
+
inputSchema: {
|
|
663
|
+
type: "object",
|
|
664
|
+
properties: {
|
|
665
|
+
eventId: {
|
|
666
|
+
type: "string",
|
|
667
|
+
description: "Event id from atlas_list_events.",
|
|
668
|
+
},
|
|
669
|
+
},
|
|
670
|
+
required: ["eventId"],
|
|
671
|
+
},
|
|
672
|
+
},
|
|
673
|
+
{
|
|
674
|
+
name: "atlas_assign_event",
|
|
675
|
+
description: "Assign a workspace member to a calendar event. Requires events:assign. Idempotent — assigning the same user twice returns the existing assignment row rather than duplicating. Returns { assignment }.",
|
|
676
|
+
inputSchema: {
|
|
677
|
+
type: "object",
|
|
678
|
+
properties: {
|
|
679
|
+
eventId: {
|
|
680
|
+
type: "string",
|
|
681
|
+
description: "Event id from atlas_list_events.",
|
|
682
|
+
},
|
|
683
|
+
user_id: {
|
|
684
|
+
type: "string",
|
|
685
|
+
description: "Workspace member user id. Required.",
|
|
686
|
+
},
|
|
687
|
+
shoot_role: {
|
|
688
|
+
type: "string",
|
|
689
|
+
description: "Role label (e.g. Photographer, Assistant).",
|
|
690
|
+
},
|
|
691
|
+
responsibilities: {
|
|
692
|
+
type: "array",
|
|
693
|
+
items: { type: "string" },
|
|
694
|
+
description: "Responsibility bullets for this assignment.",
|
|
695
|
+
},
|
|
696
|
+
notes: {
|
|
697
|
+
type: "string",
|
|
698
|
+
description: "Per-assignment notes.",
|
|
699
|
+
},
|
|
700
|
+
},
|
|
701
|
+
required: ["eventId", "user_id"],
|
|
702
|
+
},
|
|
703
|
+
},
|
|
704
|
+
{
|
|
705
|
+
name: "atlas_unassign_event",
|
|
706
|
+
description: "Remove a crew assignment from a calendar event. Requires events:assign. Confirm with the user before calling — unassigning is immediate.",
|
|
707
|
+
inputSchema: {
|
|
708
|
+
type: "object",
|
|
709
|
+
properties: {
|
|
710
|
+
eventId: {
|
|
711
|
+
type: "string",
|
|
712
|
+
description: "Event id from atlas_list_events.",
|
|
713
|
+
},
|
|
714
|
+
assignmentId: {
|
|
715
|
+
type: "string",
|
|
716
|
+
description: "Assignment id from atlas_get_event's assignments[].",
|
|
717
|
+
},
|
|
718
|
+
},
|
|
719
|
+
required: ["eventId", "assignmentId"],
|
|
720
|
+
},
|
|
721
|
+
},
|
|
722
|
+
{
|
|
723
|
+
name: "atlas_rename_file",
|
|
724
|
+
description: "Rename a workspace file. Requires files:rename. Pass just the new filename — the workspace prefix + folder are preserved. Extension changes are refused server-side.",
|
|
725
|
+
inputSchema: {
|
|
726
|
+
type: "object",
|
|
727
|
+
properties: {
|
|
728
|
+
path: { type: "string", description: "Full storage key returned by atlas_list_files." },
|
|
729
|
+
new_name: { type: "string", description: "New filename only, no folder path. Keep the same extension." },
|
|
730
|
+
},
|
|
731
|
+
required: ["path", "new_name"],
|
|
732
|
+
},
|
|
733
|
+
},
|
|
734
|
+
{
|
|
735
|
+
name: "atlas_list_revisions",
|
|
736
|
+
description: "List revision history for a doc template. Requires templates:list-revisions. Returns revisions newest first with id, source (ui/api/cli), author, based_on_updated_at, created_at.",
|
|
737
|
+
inputSchema: {
|
|
738
|
+
type: "object",
|
|
739
|
+
properties: {
|
|
740
|
+
docId: { type: "string", description: "Doc id from atlas_list_docs." },
|
|
741
|
+
limit: { type: "number", description: "Max rows (default 50, max 200)." },
|
|
742
|
+
},
|
|
743
|
+
required: ["docId"],
|
|
744
|
+
},
|
|
745
|
+
},
|
|
746
|
+
{
|
|
747
|
+
name: "atlas_restore_revision",
|
|
748
|
+
description: "Restore a doc template to an earlier revision. Requires templates:restore-revision. Overwrites the current template_content. The pre-restore state is journaled first so the restore is itself reversible.",
|
|
749
|
+
inputSchema: {
|
|
750
|
+
type: "object",
|
|
751
|
+
properties: {
|
|
752
|
+
docId: { type: "string", description: "Doc id from atlas_list_docs." },
|
|
753
|
+
revisionId: { type: "string", description: "Revision id from atlas_list_revisions." },
|
|
754
|
+
},
|
|
755
|
+
required: ["docId", "revisionId"],
|
|
756
|
+
},
|
|
757
|
+
},
|
|
758
|
+
{
|
|
759
|
+
name: "atlas_whoami",
|
|
760
|
+
description: "Describe the current token — workspace, user, granted scopes, expiry. Requires no specific scope. Call this before attempting a mutation if you're unsure which capabilities the token has; a missing scope will surface as an obvious gap in the returned scopes array rather than a 403 mid-flight.",
|
|
761
|
+
inputSchema: {
|
|
762
|
+
type: "object",
|
|
763
|
+
properties: {},
|
|
764
|
+
},
|
|
765
|
+
},
|
|
766
|
+
{
|
|
767
|
+
name: "atlas_verify_connection",
|
|
768
|
+
description: "Prove to Atlas that the Claude Desktop MCP config actually works — call this ONCE at the end of the onboarding walkthrough. Stamps workspace_members.onboarding.mcp_verified_at for the caller so the home page's Connect-Claude step auto-completes. Distinct from atlas_whoami: only this endpoint ticks the walkthrough's final phase, so accidental whoami calls won't false-positive the setup flow.",
|
|
769
|
+
inputSchema: { type: "object", properties: {} },
|
|
770
|
+
},
|
|
771
|
+
{
|
|
772
|
+
name: "atlas_list_metafield_definitions",
|
|
773
|
+
description: "List every metafield display definition in the workspace. Definitions tell Atlas how to render each Shopify metafield on the product page. Returns { definitions: [{ id, namespace, key, label, description, section, display_style, ... }] }.",
|
|
774
|
+
inputSchema: { type: "object", properties: {} },
|
|
775
|
+
},
|
|
776
|
+
{
|
|
777
|
+
name: "atlas_suggest_metafields",
|
|
778
|
+
description: "Return the shipped preset library — a curated set of Shopify metafields worth adding to a store (secondary gallery, size chart, composition, care, drop badge, etc.) with sensible display style + section pre-picked. Use this to see the full menu before deciding what to create.",
|
|
779
|
+
inputSchema: { type: "object", properties: {} },
|
|
780
|
+
},
|
|
781
|
+
{
|
|
782
|
+
name: "atlas_scan_shopify_metafields",
|
|
783
|
+
description: "Scan the workspace's live Shopify store (first 40 products) and return every unique metafield actually present with a sample value + product count. Great to call before creating definitions so you know what real data exists.",
|
|
784
|
+
inputSchema: { type: "object", properties: {} },
|
|
785
|
+
},
|
|
786
|
+
{
|
|
787
|
+
name: "atlas_create_metafield_definition",
|
|
788
|
+
description: "Create or upsert a metafield display definition. Two modes: (a) fast path — pass `preset` set to an id from atlas_suggest_metafields; (b) manual — pass namespace, key, label, section, display_style. Upserts on (workspace, namespace, key) so calling with the same coordinates edits the existing row.",
|
|
789
|
+
inputSchema: {
|
|
790
|
+
type: "object",
|
|
791
|
+
properties: {
|
|
792
|
+
preset: {
|
|
793
|
+
type: "string",
|
|
794
|
+
description: "Preset id from atlas_suggest_metafields (fast path).",
|
|
795
|
+
},
|
|
796
|
+
namespace: { type: "string", description: "Shopify namespace (e.g. custom)." },
|
|
797
|
+
key: { type: "string", description: "Shopify key (e.g. size_chart)." },
|
|
798
|
+
label: { type: "string", description: "Display label shown to team members." },
|
|
799
|
+
description: { type: "string", description: "Optional helper text." },
|
|
800
|
+
section: {
|
|
801
|
+
type: "string",
|
|
802
|
+
enum: ["overview", "gallery", "sizing", "care", "details", "specs", "custom"],
|
|
803
|
+
description: "Where on the product page this metafield renders.",
|
|
804
|
+
},
|
|
805
|
+
display_style: {
|
|
806
|
+
type: "string",
|
|
807
|
+
enum: [
|
|
808
|
+
"text",
|
|
809
|
+
"rich_text",
|
|
810
|
+
"bullet_list",
|
|
811
|
+
"image",
|
|
812
|
+
"image_gallery",
|
|
813
|
+
"video",
|
|
814
|
+
"file_download",
|
|
815
|
+
"badge",
|
|
816
|
+
"badge_list",
|
|
817
|
+
"key_value_table",
|
|
818
|
+
"measurement_table",
|
|
819
|
+
"specs_grid",
|
|
820
|
+
"number_with_unit",
|
|
821
|
+
"rating",
|
|
822
|
+
"link",
|
|
823
|
+
"link_list",
|
|
824
|
+
"color_swatches",
|
|
825
|
+
"product_reference",
|
|
826
|
+
],
|
|
827
|
+
},
|
|
828
|
+
},
|
|
829
|
+
},
|
|
830
|
+
},
|
|
831
|
+
{
|
|
832
|
+
name: "atlas_delete_metafield_definition",
|
|
833
|
+
description: "Delete a metafield definition by id (from atlas_list_metafield_definitions). Shopify data unaffected — this only removes Atlas' display rule for the metafield.",
|
|
834
|
+
inputSchema: {
|
|
835
|
+
type: "object",
|
|
836
|
+
properties: { id: { type: "string" } },
|
|
837
|
+
required: ["id"],
|
|
838
|
+
},
|
|
839
|
+
},
|
|
840
|
+
{
|
|
841
|
+
name: "atlas_list_loyalty_tiers",
|
|
842
|
+
description: "Return the current loyalty tier ladder (Diamond / Gold / Silver / Bronze or workspace-custom). Sorted highest sort_order first; `source: \"defaults\"` when the workspace inherits the Atlas ladder. Safe read-only call — no confirmation needed before invoking.",
|
|
843
|
+
inputSchema: { type: "object", properties: {} },
|
|
844
|
+
},
|
|
845
|
+
{
|
|
846
|
+
name: "atlas_set_loyalty_tiers",
|
|
847
|
+
description: "Atomically replace the workspace loyalty tier ladder. **Destructive** — server deletes every existing tier for the workspace and inserts the payload, so a partial or wrong payload wipes real merchant config. ALWAYS call atlas_list_loyalty_tiers first, show the user a diff (old vs. new), and only PUT after they confirm. Requires an API token minted by an admin/owner.",
|
|
848
|
+
inputSchema: {
|
|
849
|
+
type: "object",
|
|
850
|
+
properties: {
|
|
851
|
+
tiers: {
|
|
852
|
+
type: "array",
|
|
853
|
+
description: "Full replacement array. Each entry: { key: string (unique), name: string, minSpendCents: int (>=0), minOrders: int|null, metallic: 'diamond'|'gold'|'silver'|'bronze'|null, sortOrder: int (higher = higher priority) }. Pass [] to clear the ladder — but prefer atlas_reset_loyalty_tiers for that.",
|
|
854
|
+
items: { type: "object" },
|
|
855
|
+
},
|
|
856
|
+
},
|
|
857
|
+
required: ["tiers"],
|
|
858
|
+
},
|
|
859
|
+
},
|
|
860
|
+
{
|
|
861
|
+
name: "atlas_reset_loyalty_tiers",
|
|
862
|
+
description: "Clear the workspace's tier ladder so it falls back to Atlas defaults. Destructive — confirm with the user first. Requires admin/owner.",
|
|
863
|
+
inputSchema: { type: "object", properties: {} },
|
|
864
|
+
},
|
|
865
|
+
{
|
|
866
|
+
name: "atlas_get_analytics_prefs",
|
|
867
|
+
description: "Read the workspace analytics defaults — excluded order tags, excluded Shopify financial statuses, excluded customer-level tags, and the three customer-level boolean flags (exclude_non_subscribers, exclude_guest_checkouts, exclude_unverified_emails). Applied to every roll-up on the customer intelligence page. Safe read-only call.",
|
|
868
|
+
inputSchema: { type: "object", properties: {} },
|
|
869
|
+
},
|
|
870
|
+
{
|
|
871
|
+
name: "atlas_set_analytics_prefs",
|
|
872
|
+
description: "Update the workspace analytics defaults. Patch semantics — omitted fields are unchanged; pass an empty array or explicit `false` to clear. Common flow: user says 'exclude non-subscribers from analytics' → call atlas_get_analytics_prefs, show current state, then PUT with exclude_non_subscribers=true after confirmation. Requires admin/owner. Financial statuses must be Shopify's canonical set: voided, refunded, partially_refunded, pending, paid.",
|
|
873
|
+
inputSchema: {
|
|
874
|
+
type: "object",
|
|
875
|
+
properties: {
|
|
876
|
+
excluded_tags: {
|
|
877
|
+
type: "array",
|
|
878
|
+
items: { type: "string" },
|
|
879
|
+
description: "Order tags to strip from analytics (e.g. ['PR', 'UGC']).",
|
|
880
|
+
},
|
|
881
|
+
excluded_statuses: {
|
|
882
|
+
type: "array",
|
|
883
|
+
items: { type: "string" },
|
|
884
|
+
description: "Shopify financial statuses to strip. Typical: voided, refunded, partially_refunded, pending.",
|
|
885
|
+
},
|
|
886
|
+
excluded_customer_tags: {
|
|
887
|
+
type: "array",
|
|
888
|
+
items: { type: "string" },
|
|
889
|
+
description: "Shopify customer-level tags to hide (e.g. ['wholesale', 'staff']). Distinct from order tags — matches on the customer record.",
|
|
890
|
+
},
|
|
891
|
+
exclude_non_subscribers: {
|
|
892
|
+
type: "boolean",
|
|
893
|
+
description: "Hide customers not opted in to email marketing (reads email_marketing_consent.state, falls back to accepts_marketing).",
|
|
894
|
+
},
|
|
895
|
+
exclude_guest_checkouts: {
|
|
896
|
+
type: "boolean",
|
|
897
|
+
description: "Hide customers whose Shopify state is 'disabled' — i.e. they checked out without creating an account.",
|
|
898
|
+
},
|
|
899
|
+
exclude_unverified_emails: {
|
|
900
|
+
type: "boolean",
|
|
901
|
+
description: "Hide customers where verified_email is false.",
|
|
902
|
+
},
|
|
903
|
+
},
|
|
904
|
+
},
|
|
905
|
+
},
|
|
906
|
+
{
|
|
907
|
+
name: "atlas_get_workspace",
|
|
908
|
+
description: "Fetch the current workspace's identity — id, name, slug, logo_url (storage path), logo_bg (hex/rgba/transparent/null), theme_json (HSL triplets + `_raw` hex block), and a signed logo URL. Requires workspace:read. The `logo_signed_url` expires in 1 hour — re-call this tool to refresh it if you need to render the logo again later.",
|
|
909
|
+
inputSchema: {
|
|
910
|
+
type: "object",
|
|
911
|
+
properties: {},
|
|
912
|
+
},
|
|
913
|
+
},
|
|
914
|
+
{
|
|
915
|
+
name: "atlas_update_workspace",
|
|
916
|
+
description: "Patch the current workspace. Only the fields you send are updated — omitted fields are untouched. Slug renames cascade across every URL under the workspace, so double-check with the user before changing it. Theme changes are merged over the existing `_raw` block (so passing just `theme.accent` keeps bg/primary/radius/font). Requires workspace:write.",
|
|
917
|
+
inputSchema: {
|
|
918
|
+
type: "object",
|
|
919
|
+
properties: {
|
|
920
|
+
name: { type: "string", description: "New workspace name (1..80 chars, trimmed)." },
|
|
921
|
+
slug: {
|
|
922
|
+
type: "string",
|
|
923
|
+
description: "New workspace slug — lowercase letters, digits, hyphens (2..48, must start + end alnum). Rejected if reserved or already taken.",
|
|
924
|
+
},
|
|
925
|
+
logo_bg: {
|
|
926
|
+
type: ["string", "null"],
|
|
927
|
+
description: "Tile background behind the logo. Accepts `#rrggbb`, `rgba(r, g, b, a)`, the literal `\"transparent\"`, or `null` to clear.",
|
|
928
|
+
},
|
|
929
|
+
theme: {
|
|
930
|
+
type: "object",
|
|
931
|
+
description: "Brand theme partial. Any provided field is merged over the existing `_raw` block; omitted fields keep their current values.",
|
|
932
|
+
properties: {
|
|
933
|
+
bg: { type: "string", description: "Brand background (#rrggbb)." },
|
|
934
|
+
primary: { type: "string", description: "Brand primary (#rrggbb)." },
|
|
935
|
+
accent: { type: "string", description: "Brand accent (#rrggbb)." },
|
|
936
|
+
radiusPx: {
|
|
937
|
+
type: "number",
|
|
938
|
+
description: "Border-radius in px — integer 0..24.",
|
|
939
|
+
},
|
|
940
|
+
font: {
|
|
941
|
+
type: "string",
|
|
942
|
+
description: "Brand font family — must match one of the shipped families in lib/fonts.ts (Inter, Manrope, DM Sans, IBM Plex Sans, Space Grotesk, Bricolage Grotesque, Instrument Serif, Fraunces, Playfair Display, JetBrains Mono).",
|
|
943
|
+
},
|
|
944
|
+
},
|
|
945
|
+
},
|
|
946
|
+
},
|
|
947
|
+
},
|
|
948
|
+
},
|
|
949
|
+
{
|
|
950
|
+
name: "atlas_export_doc",
|
|
951
|
+
description: "Render a doc to PDF via the PAT-auth export endpoint and write it to a local file on the machine running this MCP server. Streams the same NDJSON pipeline the CLI uses (status / warn / done events). On success returns { out_path, pages, bytes } and, when `open` is true (default), spawns the OS default PDF viewer. Use for 'export this doc' / 'save as PDF' requests. The file lives at `out_path` (resolved relative to cwd when not absolute).",
|
|
952
|
+
inputSchema: {
|
|
953
|
+
type: "object",
|
|
954
|
+
properties: {
|
|
955
|
+
doc_id: { type: "string", description: "Doc id from atlas_list_docs." },
|
|
956
|
+
out_path: {
|
|
957
|
+
type: "string",
|
|
958
|
+
description: "Where to write the PDF. Defaults to '<sanitised-title>.pdf' in the MCP's cwd.",
|
|
959
|
+
},
|
|
960
|
+
open: {
|
|
961
|
+
type: "boolean",
|
|
962
|
+
description: "Open the file in the OS default viewer after writing. Default true.",
|
|
963
|
+
},
|
|
964
|
+
},
|
|
965
|
+
required: ["doc_id"],
|
|
966
|
+
},
|
|
967
|
+
},
|
|
968
|
+
{
|
|
969
|
+
name: "atlas_build_moodboard",
|
|
970
|
+
description: "Assemble a moodboard section in one call. Uploads any local image paths through the signed-URL flow, re-uses already-uploaded storage keys, builds a rows×cols grid padded with nulls, and appends (or inserts at `position`) a new moodboard section. Returns { section_id, uploaded_count, total_slots }. Default grid 3×3, aspect 'square'.",
|
|
971
|
+
inputSchema: {
|
|
972
|
+
type: "object",
|
|
973
|
+
properties: {
|
|
974
|
+
doc_id: { type: "string" },
|
|
975
|
+
image_paths: {
|
|
976
|
+
type: "array",
|
|
977
|
+
items: { type: "string" },
|
|
978
|
+
description: "Local file paths OR already-uploaded storage keys (e.g. '<workspace-uuid>/files/...').",
|
|
979
|
+
},
|
|
980
|
+
rows: { type: "number", description: "Default 3." },
|
|
981
|
+
cols: { type: "number", description: "Default 3." },
|
|
982
|
+
aspect: {
|
|
983
|
+
type: "string",
|
|
984
|
+
enum: ["square", "portrait", "landscape"],
|
|
985
|
+
description: "Default 'square'.",
|
|
986
|
+
},
|
|
987
|
+
title: { type: "string", description: "Section title. Default 'Moodboard'." },
|
|
988
|
+
position: {
|
|
989
|
+
type: "number",
|
|
990
|
+
description: "Insert at this section index. Omit to append.",
|
|
991
|
+
},
|
|
992
|
+
spacing: { type: "number", description: "Cell spacing in px. Default 8." },
|
|
993
|
+
radius: { type: "number", description: "Cell corner radius in px. Default 8." },
|
|
994
|
+
},
|
|
995
|
+
required: ["doc_id", "image_paths"],
|
|
996
|
+
},
|
|
997
|
+
},
|
|
998
|
+
{
|
|
999
|
+
name: "atlas_extract_palette",
|
|
1000
|
+
description: "Extract a colour palette from a moodboard section via server-side node-vibrant. Pulls each image, extracts up to 6 swatches per image, aggregates + perceptually de-dupes to rows×cols colours, and either patches an existing palette section wired to that moodboard (sourceSectionId match) or appends a new one right below it. Returns { palette_section_id, colours: string[] }.",
|
|
1001
|
+
inputSchema: {
|
|
1002
|
+
type: "object",
|
|
1003
|
+
properties: {
|
|
1004
|
+
doc_id: { type: "string" },
|
|
1005
|
+
moodboard_section_id: {
|
|
1006
|
+
type: "string",
|
|
1007
|
+
description: "Section id of the moodboard to extract from.",
|
|
1008
|
+
},
|
|
1009
|
+
rows: { type: "number", description: "Default 3." },
|
|
1010
|
+
cols: { type: "number", description: "Default 3." },
|
|
1011
|
+
},
|
|
1012
|
+
required: ["doc_id", "moodboard_section_id"],
|
|
1013
|
+
},
|
|
1014
|
+
},
|
|
1015
|
+
{
|
|
1016
|
+
name: "atlas_search_products",
|
|
1017
|
+
description: "Search the workspace's connected Shopify catalogue by free-form text (title / handle / vendor / tags). Returns up to `limit` matches, each with { shopifyProductId, title, handle, vendor, price, imageUrl, status }. Requires the products:search scope AND an active Shopify connection.",
|
|
1018
|
+
inputSchema: {
|
|
1019
|
+
type: "object",
|
|
1020
|
+
properties: {
|
|
1021
|
+
query: { type: "string", description: "Search text. Empty = most recent 'any' status." },
|
|
1022
|
+
limit: { type: "number", description: "Default 25, max 100." },
|
|
1023
|
+
},
|
|
1024
|
+
},
|
|
1025
|
+
},
|
|
1026
|
+
{
|
|
1027
|
+
name: "atlas_add_products_to_doc",
|
|
1028
|
+
description: "Snapshot a list of Shopify products into a Products section. Each product's title/handle/price/image/vendor is embedded so the tile renders even if the Shopify connection later drops. If `section_id` is provided and refers to an existing Products section, items are appended; otherwise a new section is created and appended to the doc.",
|
|
1029
|
+
inputSchema: {
|
|
1030
|
+
type: "object",
|
|
1031
|
+
properties: {
|
|
1032
|
+
doc_id: { type: "string" },
|
|
1033
|
+
shopify_product_ids: {
|
|
1034
|
+
type: "array",
|
|
1035
|
+
items: { type: "string" },
|
|
1036
|
+
description: "Shopify product ids (string form).",
|
|
1037
|
+
},
|
|
1038
|
+
section_id: {
|
|
1039
|
+
type: "string",
|
|
1040
|
+
description: "Append to this existing products section if set.",
|
|
1041
|
+
},
|
|
1042
|
+
title: {
|
|
1043
|
+
type: "string",
|
|
1044
|
+
description: "Section title when creating a new section (default 'Products').",
|
|
1045
|
+
},
|
|
1046
|
+
},
|
|
1047
|
+
required: ["doc_id", "shopify_product_ids"],
|
|
1048
|
+
},
|
|
1049
|
+
},
|
|
1050
|
+
{
|
|
1051
|
+
name: "atlas_resolve_location",
|
|
1052
|
+
description: "Resolve a free-form address or place name via Google Places Text Search (server-side). Returns up to 5 matches: { place_id, text, lat, lng, verified: true }.",
|
|
1053
|
+
inputSchema: {
|
|
1054
|
+
type: "object",
|
|
1055
|
+
properties: {
|
|
1056
|
+
query: { type: "string", description: "Search text, e.g. 'Bondi Icebergs' or '123 George St'." },
|
|
1057
|
+
},
|
|
1058
|
+
required: ["query"],
|
|
1059
|
+
},
|
|
1060
|
+
},
|
|
1061
|
+
{
|
|
1062
|
+
name: "atlas_add_location_to_doc",
|
|
1063
|
+
description: "Resolve an address (via place_id or free-form text) and append it to a Locations section. Creates a new section if `section_id` is omitted. Captures place_id + lat/lng so the Atlas map preview can render without a second geocode on the next doc open.",
|
|
1064
|
+
inputSchema: {
|
|
1065
|
+
type: "object",
|
|
1066
|
+
properties: {
|
|
1067
|
+
doc_id: { type: "string" },
|
|
1068
|
+
address: { type: "string", description: "Free-form address." },
|
|
1069
|
+
place_id: { type: "string", description: "Google Places place_id (preferred when known)." },
|
|
1070
|
+
nickname: { type: "string", description: "Display name for the card." },
|
|
1071
|
+
notes: { type: "string", description: "Per-location notes." },
|
|
1072
|
+
section_id: { type: "string", description: "Append to this existing locations section." },
|
|
1073
|
+
title: { type: "string", description: "Section title when creating a new section." },
|
|
1074
|
+
},
|
|
1075
|
+
required: ["doc_id"],
|
|
1076
|
+
},
|
|
1077
|
+
},
|
|
1078
|
+
{
|
|
1079
|
+
name: "atlas_add_gear_to_doc",
|
|
1080
|
+
description: "Add existing gear library items to a gear section by referencing their gear_items UUIDs. Gear sections store `itemIds` only (the renderer joins against the workspace gear library at open time), so this is a thin append to that array. If `section_id` is omitted a new gear section is appended.",
|
|
1081
|
+
inputSchema: {
|
|
1082
|
+
type: "object",
|
|
1083
|
+
properties: {
|
|
1084
|
+
doc_id: { type: "string" },
|
|
1085
|
+
gear_ids: {
|
|
1086
|
+
type: "array",
|
|
1087
|
+
items: { type: "string" },
|
|
1088
|
+
description: "gear_items UUIDs from atlas_list_gear.",
|
|
1089
|
+
},
|
|
1090
|
+
section_id: { type: "string", description: "Append to this existing gear section." },
|
|
1091
|
+
title: { type: "string", description: "Section title when creating (default 'Gear')." },
|
|
1092
|
+
format: {
|
|
1093
|
+
type: "string",
|
|
1094
|
+
enum: ["table", "bullets", "images"],
|
|
1095
|
+
description: "Section layout when creating. Default 'table'.",
|
|
1096
|
+
},
|
|
1097
|
+
},
|
|
1098
|
+
required: ["doc_id", "gear_ids"],
|
|
1099
|
+
},
|
|
1100
|
+
},
|
|
1101
|
+
{
|
|
1102
|
+
name: "atlas_add_shots_to_doc",
|
|
1103
|
+
description: "Snapshot shots from the workspace library into a shot-list section. Each snapshot copies name + kind + notes into a row; the referenced library shot can later diverge without affecting the doc. If `section_id` is omitted a new shot-list section is appended.",
|
|
1104
|
+
inputSchema: {
|
|
1105
|
+
type: "object",
|
|
1106
|
+
properties: {
|
|
1107
|
+
doc_id: { type: "string" },
|
|
1108
|
+
shot_ids: {
|
|
1109
|
+
type: "array",
|
|
1110
|
+
items: { type: "string" },
|
|
1111
|
+
description: "shot library ids from atlas_list_shots.",
|
|
1112
|
+
},
|
|
1113
|
+
section_id: { type: "string", description: "Append to this existing shot-list section." },
|
|
1114
|
+
title: { type: "string", description: "Section title when creating (default 'Shot list')." },
|
|
1115
|
+
},
|
|
1116
|
+
required: ["doc_id", "shot_ids"],
|
|
1117
|
+
},
|
|
1118
|
+
},
|
|
1119
|
+
{
|
|
1120
|
+
name: "atlas_upload_workspace_logo",
|
|
1121
|
+
description: "Upload a local image file as the workspace's logo. Multipart POST — reads the file at `path` and streams it to /api/v1/workspace/logo. Accepts PNG, JPG, or WebP up to 2 MB. Returns the new `logo_url` + a 1-hour signed URL. Requires workspace:write. `path` is a local file path (same shape as atlas_upload_file's `local_path`).",
|
|
1122
|
+
inputSchema: {
|
|
1123
|
+
type: "object",
|
|
1124
|
+
properties: {
|
|
1125
|
+
path: {
|
|
1126
|
+
type: "string",
|
|
1127
|
+
description: "Absolute or CWD-relative path to a PNG/JPG/WebP file on the machine running this MCP server.",
|
|
1128
|
+
},
|
|
1129
|
+
},
|
|
1130
|
+
required: ["path"],
|
|
1131
|
+
},
|
|
1132
|
+
},
|
|
1133
|
+
READ_ACHIEVEMENTS_TOOL,
|
|
1134
|
+
{
|
|
1135
|
+
name: "atlas_list_mindmaps",
|
|
1136
|
+
description: "List mindmaps (planning boards) in the current Atlas workspace. Requires mindmaps:list. Returns { items, next_cursor } — each item is a MindmapSummary (id, title, description, shoot_id, node_count, node_done_count, created_at, updated_at). Pass `cursor` to page.",
|
|
1137
|
+
inputSchema: {
|
|
1138
|
+
type: "object",
|
|
1139
|
+
properties: {
|
|
1140
|
+
limit: { type: "number", description: "Default 50, max 500." },
|
|
1141
|
+
cursor: {
|
|
1142
|
+
type: "string",
|
|
1143
|
+
description: "Opaque next_cursor from a previous response. Omit for the first page.",
|
|
1144
|
+
},
|
|
1145
|
+
},
|
|
1146
|
+
},
|
|
1147
|
+
},
|
|
1148
|
+
{
|
|
1149
|
+
name: "atlas_get_mindmap",
|
|
1150
|
+
description: "Fetch a full mindmap — metadata + every node + every edge. Requires mindmaps:read. Returns { mindmap, nodes[], edges[] }. Nodes carry x/y/title/kind/colour_key; edges connect two node ids. Use before atlas_update_mindmap_node / atlas_delete_mindmap_node so you have fresh ids.",
|
|
1151
|
+
inputSchema: {
|
|
1152
|
+
type: "object",
|
|
1153
|
+
properties: {
|
|
1154
|
+
mindmapId: { type: "string", description: "Mindmap id from atlas_list_mindmaps." },
|
|
1155
|
+
},
|
|
1156
|
+
required: ["mindmapId"],
|
|
1157
|
+
},
|
|
1158
|
+
},
|
|
1159
|
+
{
|
|
1160
|
+
name: "atlas_create_mindmap",
|
|
1161
|
+
description: "Create a new mindmap. Requires mindmaps:write. `title` is required; `description` and `shoot_id` are optional. Optional `nodes[]` and `edges[]` seed the canvas atomically — edges may reference node ids supplied inline (we remap client-provided ids to the generated UUIDs). Returns the new mindmap record.",
|
|
1162
|
+
inputSchema: {
|
|
1163
|
+
type: "object",
|
|
1164
|
+
properties: {
|
|
1165
|
+
title: { type: "string", description: "Mindmap title (≤ 200 chars)." },
|
|
1166
|
+
description: { type: "string", description: "Optional description (≤ 2000 chars)." },
|
|
1167
|
+
shoot_id: {
|
|
1168
|
+
type: "string",
|
|
1169
|
+
description: "Optional shoots id to attach the mindmap to.",
|
|
1170
|
+
},
|
|
1171
|
+
nodes: {
|
|
1172
|
+
type: "array",
|
|
1173
|
+
description: "Optional seed nodes. Each: { title (required) | label, x?, y?, width?, height?, kind?, shape?, colour_key?, description?, image_url?, image_alt?, font_size?, text_color?, bg_color?, id? (used to wire inline edges) }.",
|
|
1174
|
+
items: { type: "object" },
|
|
1175
|
+
},
|
|
1176
|
+
edges: {
|
|
1177
|
+
type: "array",
|
|
1178
|
+
description: "Optional seed edges. Each: { from_node_id, to_node_id, label? }. Ids may reference either existing nodes OR the `id` field of a node in the inline `nodes` array.",
|
|
1179
|
+
items: { type: "object" },
|
|
1180
|
+
},
|
|
1181
|
+
},
|
|
1182
|
+
required: ["title"],
|
|
1183
|
+
},
|
|
1184
|
+
},
|
|
1185
|
+
{
|
|
1186
|
+
name: "atlas_update_mindmap",
|
|
1187
|
+
description: "Patch a mindmap's metadata (title / description / shoot link). Requires mindmaps:write. Only the fields you send are updated. Pass `shoot_id: null` to detach the current shoot link.",
|
|
1188
|
+
inputSchema: {
|
|
1189
|
+
type: "object",
|
|
1190
|
+
properties: {
|
|
1191
|
+
mindmapId: { type: "string", description: "Mindmap id." },
|
|
1192
|
+
title: { type: "string", description: "New title (≤ 200 chars)." },
|
|
1193
|
+
description: {
|
|
1194
|
+
type: ["string", "null"],
|
|
1195
|
+
description: "New description (≤ 2000 chars). Pass null to clear.",
|
|
1196
|
+
},
|
|
1197
|
+
shoot_id: {
|
|
1198
|
+
type: ["string", "null"],
|
|
1199
|
+
description: "shoots id to attach, or null to detach.",
|
|
1200
|
+
},
|
|
1201
|
+
},
|
|
1202
|
+
required: ["mindmapId"],
|
|
1203
|
+
},
|
|
1204
|
+
},
|
|
1205
|
+
{
|
|
1206
|
+
name: "atlas_delete_mindmap",
|
|
1207
|
+
description: "Hard-delete a mindmap. Requires mindmaps:delete. Nodes, edges, and freehand strokes cascade via FK — no undo. Always confirm with the user before calling.",
|
|
1208
|
+
inputSchema: {
|
|
1209
|
+
type: "object",
|
|
1210
|
+
properties: {
|
|
1211
|
+
mindmapId: { type: "string", description: "Mindmap id from atlas_list_mindmaps." },
|
|
1212
|
+
},
|
|
1213
|
+
required: ["mindmapId"],
|
|
1214
|
+
},
|
|
1215
|
+
},
|
|
1216
|
+
{
|
|
1217
|
+
name: "atlas_add_mindmap_node",
|
|
1218
|
+
description: "Append a single node to a mindmap. Requires mindmaps:write. `label` (or `title`) is required; everything else defaults. Returns the inserted node row. order_index is auto-computed as max+1 so the node lands at the end of the canvas.",
|
|
1219
|
+
inputSchema: {
|
|
1220
|
+
type: "object",
|
|
1221
|
+
properties: {
|
|
1222
|
+
mindmapId: { type: "string", description: "Mindmap id." },
|
|
1223
|
+
label: { type: "string", description: "Node label (synonym for title). One of label/title required." },
|
|
1224
|
+
title: { type: "string", description: "Node title. One of label/title required." },
|
|
1225
|
+
x: { type: "number", description: "Canvas x coordinate (default 0)." },
|
|
1226
|
+
y: { type: "number", description: "Canvas y coordinate (default 0)." },
|
|
1227
|
+
width: { type: "number", description: "Node width (default 200)." },
|
|
1228
|
+
height: { type: "number", description: "Node height (default 120)." },
|
|
1229
|
+
kind: {
|
|
1230
|
+
type: "string",
|
|
1231
|
+
enum: ["text", "shape", "image", "textbox"],
|
|
1232
|
+
description: "Node kind. Default 'text'.",
|
|
1233
|
+
},
|
|
1234
|
+
shape: {
|
|
1235
|
+
type: "string",
|
|
1236
|
+
enum: ["rect", "circle", "diamond", "ellipse", "triangle", "rounded"],
|
|
1237
|
+
description: "Required when kind='shape'.",
|
|
1238
|
+
},
|
|
1239
|
+
colour_key: {
|
|
1240
|
+
type: "string",
|
|
1241
|
+
enum: ["rose", "amber", "sky", "violet", "lime", "stone"],
|
|
1242
|
+
},
|
|
1243
|
+
description: { type: "string" },
|
|
1244
|
+
image_url: { type: "string", description: "https: URL (image nodes only)." },
|
|
1245
|
+
image_alt: { type: "string" },
|
|
1246
|
+
font_size: { type: "number", description: "8..96" },
|
|
1247
|
+
text_color: { type: "string", description: "Hex colour #rrggbb (or #rgb / with alpha)." },
|
|
1248
|
+
bg_color: { type: "string", description: "Hex colour #rrggbb." },
|
|
1249
|
+
},
|
|
1250
|
+
required: ["mindmapId"],
|
|
1251
|
+
},
|
|
1252
|
+
},
|
|
1253
|
+
{
|
|
1254
|
+
name: "atlas_update_mindmap_node",
|
|
1255
|
+
description: "Patch a mindmap node. Requires mindmaps:write. Only the fields you send are updated. `label` is a synonym for `title`. Set `completed: true` to mark a node done.",
|
|
1256
|
+
inputSchema: {
|
|
1257
|
+
type: "object",
|
|
1258
|
+
properties: {
|
|
1259
|
+
mindmapId: { type: "string" },
|
|
1260
|
+
nodeId: { type: "string" },
|
|
1261
|
+
label: { type: "string" },
|
|
1262
|
+
title: { type: "string" },
|
|
1263
|
+
x: { type: "number" },
|
|
1264
|
+
y: { type: "number" },
|
|
1265
|
+
width: { type: "number" },
|
|
1266
|
+
height: { type: "number" },
|
|
1267
|
+
kind: {
|
|
1268
|
+
type: "string",
|
|
1269
|
+
enum: ["text", "shape", "image", "textbox"],
|
|
1270
|
+
},
|
|
1271
|
+
shape: {
|
|
1272
|
+
type: "string",
|
|
1273
|
+
enum: ["rect", "circle", "diamond", "ellipse", "triangle", "rounded"],
|
|
1274
|
+
},
|
|
1275
|
+
colour_key: {
|
|
1276
|
+
type: "string",
|
|
1277
|
+
enum: ["rose", "amber", "sky", "violet", "lime", "stone"],
|
|
1278
|
+
},
|
|
1279
|
+
description: { type: "string" },
|
|
1280
|
+
completed: { type: "boolean" },
|
|
1281
|
+
image_url: { type: "string" },
|
|
1282
|
+
image_alt: { type: "string" },
|
|
1283
|
+
font_size: { type: "number" },
|
|
1284
|
+
text_color: { type: "string" },
|
|
1285
|
+
bg_color: { type: "string" },
|
|
1286
|
+
order_index: { type: "number" },
|
|
1287
|
+
},
|
|
1288
|
+
required: ["mindmapId", "nodeId"],
|
|
1289
|
+
},
|
|
1290
|
+
},
|
|
1291
|
+
{
|
|
1292
|
+
name: "atlas_delete_mindmap_node",
|
|
1293
|
+
description: "Delete one node from a mindmap. Requires mindmaps:delete. Edges touching this node cascade-delete via FK. Confirm with the user before calling.",
|
|
1294
|
+
inputSchema: {
|
|
1295
|
+
type: "object",
|
|
1296
|
+
properties: {
|
|
1297
|
+
mindmapId: { type: "string" },
|
|
1298
|
+
nodeId: { type: "string" },
|
|
1299
|
+
},
|
|
1300
|
+
required: ["mindmapId", "nodeId"],
|
|
1301
|
+
},
|
|
1302
|
+
},
|
|
1303
|
+
{
|
|
1304
|
+
name: "atlas_add_mindmap_edge",
|
|
1305
|
+
description: "Create an edge between two nodes in a mindmap. Requires mindmaps:write. Both nodes must belong to the same mindmap. The DB enforces UNIQUE(mindmap_id, from_node_id, to_node_id) — duplicates return 409.",
|
|
1306
|
+
inputSchema: {
|
|
1307
|
+
type: "object",
|
|
1308
|
+
properties: {
|
|
1309
|
+
mindmapId: { type: "string" },
|
|
1310
|
+
from_node_id: { type: "string" },
|
|
1311
|
+
to_node_id: { type: "string" },
|
|
1312
|
+
label: { type: "string", description: "Optional edge label (≤ 200 chars)." },
|
|
1313
|
+
},
|
|
1314
|
+
required: ["mindmapId", "from_node_id", "to_node_id"],
|
|
1315
|
+
},
|
|
1316
|
+
},
|
|
1317
|
+
{
|
|
1318
|
+
name: "atlas_delete_mindmap_edge",
|
|
1319
|
+
description: "Delete a mindmap edge. Requires mindmaps:delete. Nodes untouched. Confirm with the user before calling.",
|
|
1320
|
+
inputSchema: {
|
|
1321
|
+
type: "object",
|
|
1322
|
+
properties: {
|
|
1323
|
+
mindmapId: { type: "string" },
|
|
1324
|
+
edgeId: { type: "string" },
|
|
1325
|
+
},
|
|
1326
|
+
required: ["mindmapId", "edgeId"],
|
|
1327
|
+
},
|
|
1328
|
+
},
|
|
1329
|
+
{
|
|
1330
|
+
name: "atlas_create_mindmap_group",
|
|
1331
|
+
description: "Create a mindmap group — stamps a shared `group_id` onto a set of existing nodes (and optional edges) and writes a row to mindmap_groups carrying the group's inspector-pane metadata (name, bg colour, outline, radius, padding, custom CSS). Requires mindmaps:write. All referenced ids must live in this mindmap.",
|
|
1332
|
+
inputSchema: {
|
|
1333
|
+
type: "object",
|
|
1334
|
+
properties: {
|
|
1335
|
+
mindmapId: { type: "string", description: "Mindmap id." },
|
|
1336
|
+
name: { type: "string", description: "Group display name (≤ 200 chars). Synonym: title." },
|
|
1337
|
+
title: { type: "string", description: "Alias for name." },
|
|
1338
|
+
nodes: {
|
|
1339
|
+
type: "array",
|
|
1340
|
+
items: { type: "string" },
|
|
1341
|
+
description: "mindmap_nodes UUIDs to tag with the new group id. Non-empty.",
|
|
1342
|
+
},
|
|
1343
|
+
edges: {
|
|
1344
|
+
type: "array",
|
|
1345
|
+
items: { type: "string" },
|
|
1346
|
+
description: "Optional mindmap_edges UUIDs to tag with the new group id.",
|
|
1347
|
+
},
|
|
1348
|
+
bg_color: { type: "string", description: "Group background colour (#rrggbb / #rrggbbaa)." },
|
|
1349
|
+
outline_color: { type: "string", description: "Group outline colour (#rrggbb / #rrggbbaa)." },
|
|
1350
|
+
outline_width: { type: "number", description: "Outline width in px (0–32)." },
|
|
1351
|
+
corner_radius: { type: "number", description: "Corner radius in px (0–128)." },
|
|
1352
|
+
padding: { type: "number", description: "Padding in px (0–256)." },
|
|
1353
|
+
custom_css: { type: "string", description: "Arbitrary CSS block (≤ 4000 chars)." },
|
|
1354
|
+
},
|
|
1355
|
+
required: ["mindmapId", "nodes"],
|
|
1356
|
+
},
|
|
1357
|
+
},
|
|
1358
|
+
{
|
|
1359
|
+
name: "atlas_list_activity",
|
|
1360
|
+
description: "List workspace activity-log entries, newest first. Requires activity:list. Returns { items, next_cursor }. Each item: { id, workspace_id, actor_user_id, source (ui|cli|mcp|system), action, resource_type, resource_id, summary, meta, created_at }. Optional filters: `since`/`until` (ISO 8601), `user` (uuid OR email), `source` (csv of ui,cli,mcp,system), `action` (exact verb like doc.create), `resource_type` (exact kind like doc, gear, mindmap).",
|
|
1361
|
+
inputSchema: {
|
|
1362
|
+
type: "object",
|
|
1363
|
+
properties: {
|
|
1364
|
+
limit: { type: "number", description: "Default 50, max 500." },
|
|
1365
|
+
cursor: {
|
|
1366
|
+
type: "string",
|
|
1367
|
+
description: "Opaque next_cursor from a previous response. Omit for the first page.",
|
|
1368
|
+
},
|
|
1369
|
+
since: {
|
|
1370
|
+
type: "string",
|
|
1371
|
+
description: "ISO 8601 lower bound on created_at (inclusive).",
|
|
1372
|
+
},
|
|
1373
|
+
until: {
|
|
1374
|
+
type: "string",
|
|
1375
|
+
description: "ISO 8601 upper bound on created_at (exclusive).",
|
|
1376
|
+
},
|
|
1377
|
+
user: {
|
|
1378
|
+
type: "string",
|
|
1379
|
+
description: "Restrict to this actor — accepts a user uuid OR an email address.",
|
|
1380
|
+
},
|
|
1381
|
+
source: {
|
|
1382
|
+
type: "string",
|
|
1383
|
+
description: "Comma-separated source filter: any of ui, cli, mcp, system.",
|
|
1384
|
+
},
|
|
1385
|
+
action: {
|
|
1386
|
+
type: "string",
|
|
1387
|
+
description: "Exact-match verb filter, e.g. 'doc.create', 'mindmap.update'.",
|
|
1388
|
+
},
|
|
1389
|
+
resource_type: {
|
|
1390
|
+
type: "string",
|
|
1391
|
+
description: "Exact-match on resource_type: 'doc', 'gear', 'mindmap', etc.",
|
|
1392
|
+
},
|
|
1393
|
+
},
|
|
1394
|
+
},
|
|
1395
|
+
},
|
|
1396
|
+
{
|
|
1397
|
+
name: "atlas_get_activity",
|
|
1398
|
+
description: "Fetch a single workspace activity-log entry plus the (best-effort) hydrated actor record. Requires activity:read. Returns { entry, actor } where actor may be null (system events, deleted users).",
|
|
1399
|
+
inputSchema: {
|
|
1400
|
+
type: "object",
|
|
1401
|
+
properties: {
|
|
1402
|
+
entryId: { type: "string", description: "Activity-log entry id." },
|
|
1403
|
+
},
|
|
1404
|
+
required: ["entryId"],
|
|
1405
|
+
},
|
|
1406
|
+
},
|
|
1407
|
+
{
|
|
1408
|
+
name: "export_mindmap",
|
|
1409
|
+
description: "Export an Atlas mindmap to PDF, PNG, or SVG. SVG is server-rendered vector output (fastest, retains ink as paths). PDF + PNG spawn local Chromium via playwright-core on the machine running this MCP server. Returns { out_path, format, bytes }. Requires mindmaps:export.",
|
|
1410
|
+
inputSchema: {
|
|
1411
|
+
type: "object",
|
|
1412
|
+
properties: {
|
|
1413
|
+
mindmap_id: { type: "string" },
|
|
1414
|
+
format: {
|
|
1415
|
+
type: "string",
|
|
1416
|
+
enum: ["pdf", "png", "svg"],
|
|
1417
|
+
description: "Output format. Default pdf.",
|
|
1418
|
+
},
|
|
1419
|
+
out_path: {
|
|
1420
|
+
type: "string",
|
|
1421
|
+
description: "Absolute path to write the file to. Default: '<title>.<ext>' in cwd.",
|
|
1422
|
+
},
|
|
1423
|
+
dpi: {
|
|
1424
|
+
type: "number",
|
|
1425
|
+
enum: [1, 2, 3],
|
|
1426
|
+
description: "Device scale factor for PNG. Default 2.",
|
|
1427
|
+
},
|
|
1428
|
+
w: { type: "number", description: "Canvas width in CSS pixels. Default 1920." },
|
|
1429
|
+
h: { type: "number", description: "Canvas height in CSS pixels. Default 1080." },
|
|
1430
|
+
open: {
|
|
1431
|
+
type: "boolean",
|
|
1432
|
+
description: "Open the file in the OS default viewer after writing. Default true.",
|
|
1433
|
+
},
|
|
1434
|
+
},
|
|
1435
|
+
required: ["mindmap_id"],
|
|
1436
|
+
},
|
|
1437
|
+
},
|
|
1438
|
+
];
|
|
1439
|
+
/* ─── Server setup ─────────────────────────────────────────────────── */
|
|
1440
|
+
const server = new Server({ name: "atlas-mcp", version: "0.1.0" }, { capabilities: { tools: {} } });
|
|
1441
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
1442
|
+
tools: TOOLS,
|
|
1443
|
+
}));
|
|
1444
|
+
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
1445
|
+
const name = req.params.name;
|
|
1446
|
+
const args = (req.params.arguments ?? {});
|
|
1447
|
+
try {
|
|
1448
|
+
const result = await dispatch(name, args);
|
|
1449
|
+
return {
|
|
1450
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
1451
|
+
};
|
|
1452
|
+
}
|
|
1453
|
+
catch (e) {
|
|
1454
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1455
|
+
return {
|
|
1456
|
+
isError: true,
|
|
1457
|
+
content: [{ type: "text", text: `error: ${msg}` }],
|
|
1458
|
+
};
|
|
1459
|
+
}
|
|
1460
|
+
});
|
|
1461
|
+
async function dispatch(name, args) {
|
|
1462
|
+
switch (name) {
|
|
1463
|
+
case "atlas_doc_create":
|
|
1464
|
+
return createDoc(args);
|
|
1465
|
+
case "atlas_list_doc_presets":
|
|
1466
|
+
return listDocPresets();
|
|
1467
|
+
case "atlas_create_doc_from_preset":
|
|
1468
|
+
return createDocFromPreset(args);
|
|
1469
|
+
case "atlas_doc_delete":
|
|
1470
|
+
return deleteDoc(args);
|
|
1471
|
+
case "atlas_restore_doc":
|
|
1472
|
+
return restoreDoc(args);
|
|
1473
|
+
case "atlas_purge_doc":
|
|
1474
|
+
return purgeDoc(args);
|
|
1475
|
+
case "atlas_list_docs":
|
|
1476
|
+
return listDocs(args);
|
|
1477
|
+
case "atlas_get_template":
|
|
1478
|
+
return getTemplate(args);
|
|
1479
|
+
case "atlas_list_sections":
|
|
1480
|
+
return listSections(args);
|
|
1481
|
+
case "atlas_update_section":
|
|
1482
|
+
return updateSection(args);
|
|
1483
|
+
case "atlas_list_files":
|
|
1484
|
+
return listFiles(args);
|
|
1485
|
+
case "atlas_upload_file":
|
|
1486
|
+
return uploadFile(args);
|
|
1487
|
+
case "atlas_get_file_url":
|
|
1488
|
+
return getFileUrl(args);
|
|
1489
|
+
case "atlas_set_file_alt":
|
|
1490
|
+
return setFileAlt(args);
|
|
1491
|
+
case "atlas_delete_file":
|
|
1492
|
+
return deleteFile(args);
|
|
1493
|
+
case "atlas_list_gear":
|
|
1494
|
+
return listGear(args);
|
|
1495
|
+
case "atlas_add_gear":
|
|
1496
|
+
return addGear(args);
|
|
1497
|
+
case "atlas_import_gear":
|
|
1498
|
+
return importGear(args);
|
|
1499
|
+
case "atlas_delete_gear":
|
|
1500
|
+
return deleteGear(args);
|
|
1501
|
+
case "atlas_restore_gear":
|
|
1502
|
+
return restoreGear(args);
|
|
1503
|
+
case "atlas_purge_gear":
|
|
1504
|
+
return purgeGear(args);
|
|
1505
|
+
case "atlas_update_template":
|
|
1506
|
+
return updateTemplate(args);
|
|
1507
|
+
case "atlas_describe_section_type":
|
|
1508
|
+
return describeSectionType(args);
|
|
1509
|
+
case "atlas_update_gear":
|
|
1510
|
+
return updateGear(args);
|
|
1511
|
+
case "atlas_list_shots":
|
|
1512
|
+
return listShots(args);
|
|
1513
|
+
case "atlas_add_shot":
|
|
1514
|
+
return addShot(args);
|
|
1515
|
+
case "atlas_update_shot":
|
|
1516
|
+
return updateShot(args);
|
|
1517
|
+
case "atlas_delete_shot":
|
|
1518
|
+
return deleteShot(args);
|
|
1519
|
+
case "atlas_restore_shot":
|
|
1520
|
+
return restoreShot(args);
|
|
1521
|
+
case "atlas_purge_shot":
|
|
1522
|
+
return purgeShot(args);
|
|
1523
|
+
case "atlas_import_shots":
|
|
1524
|
+
return importShots(args);
|
|
1525
|
+
case "atlas_list_events":
|
|
1526
|
+
return listEvents(args);
|
|
1527
|
+
case "atlas_get_event":
|
|
1528
|
+
return getEvent(args);
|
|
1529
|
+
case "atlas_create_event":
|
|
1530
|
+
return createEvent(args);
|
|
1531
|
+
case "atlas_update_event":
|
|
1532
|
+
return updateEvent(args);
|
|
1533
|
+
case "atlas_delete_event":
|
|
1534
|
+
return deleteEvent(args);
|
|
1535
|
+
case "atlas_assign_event":
|
|
1536
|
+
return assignEvent(args);
|
|
1537
|
+
case "atlas_unassign_event":
|
|
1538
|
+
return unassignEvent(args);
|
|
1539
|
+
case "atlas_rename_file":
|
|
1540
|
+
return renameFile(args);
|
|
1541
|
+
case "atlas_list_revisions":
|
|
1542
|
+
return listRevisions(args);
|
|
1543
|
+
case "atlas_restore_revision":
|
|
1544
|
+
return restoreRevision(args);
|
|
1545
|
+
case "atlas_whoami":
|
|
1546
|
+
return whoami();
|
|
1547
|
+
case "atlas_verify_connection":
|
|
1548
|
+
return verifyConnection();
|
|
1549
|
+
case "atlas_list_metafield_definitions":
|
|
1550
|
+
return listMetafieldDefinitions();
|
|
1551
|
+
case "atlas_suggest_metafields":
|
|
1552
|
+
return suggestMetafields();
|
|
1553
|
+
case "atlas_scan_shopify_metafields":
|
|
1554
|
+
return scanShopifyMetafields();
|
|
1555
|
+
case "atlas_create_metafield_definition":
|
|
1556
|
+
return createMetafieldDefinition(args);
|
|
1557
|
+
case "atlas_delete_metafield_definition":
|
|
1558
|
+
return deleteMetafieldDefinition(args);
|
|
1559
|
+
case "atlas_list_loyalty_tiers":
|
|
1560
|
+
return listLoyaltyTiers();
|
|
1561
|
+
case "atlas_set_loyalty_tiers":
|
|
1562
|
+
return setLoyaltyTiers(args);
|
|
1563
|
+
case "atlas_reset_loyalty_tiers":
|
|
1564
|
+
return resetLoyaltyTiers();
|
|
1565
|
+
case "atlas_get_analytics_prefs":
|
|
1566
|
+
return getAnalyticsPrefs();
|
|
1567
|
+
case "atlas_set_analytics_prefs":
|
|
1568
|
+
return setAnalyticsPrefs(args);
|
|
1569
|
+
case "atlas_get_workspace":
|
|
1570
|
+
return getWorkspace();
|
|
1571
|
+
case "atlas_update_workspace":
|
|
1572
|
+
return updateWorkspace(args);
|
|
1573
|
+
case "atlas_upload_workspace_logo":
|
|
1574
|
+
return uploadWorkspaceLogo(args);
|
|
1575
|
+
case "atlas_export_doc":
|
|
1576
|
+
return exportDocTool(args);
|
|
1577
|
+
case "atlas_build_moodboard":
|
|
1578
|
+
return buildMoodboard(args);
|
|
1579
|
+
case "atlas_extract_palette":
|
|
1580
|
+
return extractPaletteTool(args);
|
|
1581
|
+
case "atlas_search_products":
|
|
1582
|
+
return searchProducts(args);
|
|
1583
|
+
case "atlas_add_products_to_doc":
|
|
1584
|
+
return addProductsToDoc(args);
|
|
1585
|
+
case "atlas_resolve_location":
|
|
1586
|
+
return resolveLocationTool(args);
|
|
1587
|
+
case "atlas_add_location_to_doc":
|
|
1588
|
+
return addLocationToDoc(args);
|
|
1589
|
+
case "atlas_add_gear_to_doc":
|
|
1590
|
+
return addGearToDoc(args);
|
|
1591
|
+
case "atlas_add_shots_to_doc":
|
|
1592
|
+
return addShotsToDoc(args);
|
|
1593
|
+
case "read_achievements":
|
|
1594
|
+
return readAchievements();
|
|
1595
|
+
case "atlas_list_mindmaps":
|
|
1596
|
+
return listMindmaps_(args);
|
|
1597
|
+
case "atlas_get_mindmap":
|
|
1598
|
+
return getMindmap_(args);
|
|
1599
|
+
case "atlas_create_mindmap":
|
|
1600
|
+
return createMindmap_(args);
|
|
1601
|
+
case "atlas_update_mindmap":
|
|
1602
|
+
return updateMindmap_(args);
|
|
1603
|
+
case "atlas_delete_mindmap":
|
|
1604
|
+
return deleteMindmap_(args);
|
|
1605
|
+
case "atlas_add_mindmap_node":
|
|
1606
|
+
return addMindmapNode_(args);
|
|
1607
|
+
case "atlas_update_mindmap_node":
|
|
1608
|
+
return updateMindmapNode_(args);
|
|
1609
|
+
case "atlas_delete_mindmap_node":
|
|
1610
|
+
return deleteMindmapNode_(args);
|
|
1611
|
+
case "atlas_add_mindmap_edge":
|
|
1612
|
+
return addMindmapEdge_(args);
|
|
1613
|
+
case "atlas_delete_mindmap_edge":
|
|
1614
|
+
return deleteMindmapEdge_(args);
|
|
1615
|
+
case "atlas_create_mindmap_group":
|
|
1616
|
+
return createMindmapGroup_(args);
|
|
1617
|
+
case "atlas_list_activity":
|
|
1618
|
+
return listActivity_(args);
|
|
1619
|
+
case "atlas_get_activity":
|
|
1620
|
+
return getActivity_(args);
|
|
1621
|
+
case "export_mindmap":
|
|
1622
|
+
return exportMindmapTool(args);
|
|
1623
|
+
default:
|
|
1624
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
/* ─── Tool implementations ─────────────────────────────────────────── */
|
|
1628
|
+
async function listDocs(args) {
|
|
1629
|
+
const cfg = resolveConfig();
|
|
1630
|
+
const query = {};
|
|
1631
|
+
if (typeof args.kind === "string")
|
|
1632
|
+
query.kind = args.kind;
|
|
1633
|
+
if (typeof args.limit === "number")
|
|
1634
|
+
query.limit = args.limit;
|
|
1635
|
+
if (typeof args.cursor === "string")
|
|
1636
|
+
query.cursor = args.cursor;
|
|
1637
|
+
if (args.deleted === true)
|
|
1638
|
+
query.deleted = 1;
|
|
1639
|
+
const { data } = await apiRequest(cfg, "/api/v1/docs", { query });
|
|
1640
|
+
return data;
|
|
1641
|
+
}
|
|
1642
|
+
async function getTemplate(args) {
|
|
1643
|
+
const cfg = resolveConfig();
|
|
1644
|
+
const docId = requireString(args, "docId");
|
|
1645
|
+
const { data } = await apiRequest(cfg, `/api/v1/docs/${docId}/template`);
|
|
1646
|
+
return data;
|
|
1647
|
+
}
|
|
1648
|
+
async function listSections(args) {
|
|
1649
|
+
const tpl = await getTemplate(args);
|
|
1650
|
+
const sections = tpl.template_content?.sections ?? [];
|
|
1651
|
+
const summary = sections.map((s) => ({
|
|
1652
|
+
id: s.id,
|
|
1653
|
+
type: s.type,
|
|
1654
|
+
// Best-effort title extraction — most sections stash a title on data.title.
|
|
1655
|
+
title: (s.data && typeof s.data.title === "string"
|
|
1656
|
+
? s.data.title
|
|
1657
|
+
: null) ?? "",
|
|
1658
|
+
hidden: !!s.hidden,
|
|
1659
|
+
}));
|
|
1660
|
+
return { docId: tpl.id, title: tpl.title, section_count: summary.length, sections: summary };
|
|
1661
|
+
}
|
|
1662
|
+
async function updateSection(args) {
|
|
1663
|
+
const cfg = resolveConfig();
|
|
1664
|
+
const docId = requireString(args, "docId");
|
|
1665
|
+
const sectionId = requireString(args, "sectionId");
|
|
1666
|
+
const newData = isRecord(args.data) ? args.data : undefined;
|
|
1667
|
+
const newStyle = isRecord(args.style) ? args.style : undefined;
|
|
1668
|
+
const hidden = typeof args.hidden === "boolean" ? args.hidden : undefined;
|
|
1669
|
+
if (newData === undefined && newStyle === undefined && hidden === undefined) {
|
|
1670
|
+
throw new Error("Provide at least one of: data, style, hidden.");
|
|
1671
|
+
}
|
|
1672
|
+
// Fetch current etag first so the PATCH's If-Match survives a concurrent
|
|
1673
|
+
// web edit. On 409 we surface the message so the model can re-fetch and
|
|
1674
|
+
// retry.
|
|
1675
|
+
const current = await getTemplate({ docId });
|
|
1676
|
+
try {
|
|
1677
|
+
const { data } = await apiRequest(cfg, `/api/v1/docs/${docId}/template/sections/${sectionId}`, {
|
|
1678
|
+
method: "PATCH",
|
|
1679
|
+
body: {
|
|
1680
|
+
...(newData !== undefined ? { data: newData } : {}),
|
|
1681
|
+
...(newStyle !== undefined ? { style: newStyle } : {}),
|
|
1682
|
+
...(hidden !== undefined ? { hidden } : {}),
|
|
1683
|
+
},
|
|
1684
|
+
ifMatch: current.etag,
|
|
1685
|
+
});
|
|
1686
|
+
return {
|
|
1687
|
+
updated_section: sectionId,
|
|
1688
|
+
section: data.section,
|
|
1689
|
+
etag: data.etag,
|
|
1690
|
+
updated_at: data.updated_at,
|
|
1691
|
+
};
|
|
1692
|
+
}
|
|
1693
|
+
catch (e) {
|
|
1694
|
+
if (e instanceof ApiError && e.status === 404) {
|
|
1695
|
+
throw new Error(`Section '${sectionId}' not found in doc '${docId}'. Call atlas_list_sections to see current section ids.`);
|
|
1696
|
+
}
|
|
1697
|
+
if (e instanceof ApiError && e.status === 409) {
|
|
1698
|
+
throw new Error("Doc was modified since we fetched it — re-run atlas_get_template to pick up the concurrent change, then retry.");
|
|
1699
|
+
}
|
|
1700
|
+
throw e;
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
async function createDoc(args) {
|
|
1704
|
+
const cfg = resolveConfig();
|
|
1705
|
+
const title = requireString(args, "title");
|
|
1706
|
+
const body = { title };
|
|
1707
|
+
if (typeof args.doc_kind === "string")
|
|
1708
|
+
body.doc_kind = args.doc_kind;
|
|
1709
|
+
if (typeof args.folder_id === "string")
|
|
1710
|
+
body.folder_id = args.folder_id;
|
|
1711
|
+
if (typeof args.shoot_id === "string")
|
|
1712
|
+
body.shoot_id = args.shoot_id;
|
|
1713
|
+
if (isRecord(args.template_content))
|
|
1714
|
+
body.template_content = args.template_content;
|
|
1715
|
+
if (typeof args.preset_id === "string")
|
|
1716
|
+
body.preset_id = args.preset_id;
|
|
1717
|
+
const { data } = await apiRequest(cfg, "/api/v1/docs", { method: "POST", body });
|
|
1718
|
+
return data;
|
|
1719
|
+
}
|
|
1720
|
+
/** List the server-side preset catalogue. One GET → the full
|
|
1721
|
+
* `{ presets: DocPreset[] }` payload the web picker also uses. */
|
|
1722
|
+
async function listDocPresets() {
|
|
1723
|
+
const cfg = resolveConfig();
|
|
1724
|
+
const { data } = await apiRequest(cfg, "/api/v1/presets", { method: "GET" });
|
|
1725
|
+
return data;
|
|
1726
|
+
}
|
|
1727
|
+
/** Convenience wrapper — resolves the preset-id defaulted title on the
|
|
1728
|
+
* client so Claude doesn't have to list presets first just to grab the
|
|
1729
|
+
* default string. API still validates the preset_id. */
|
|
1730
|
+
async function createDocFromPreset(args) {
|
|
1731
|
+
const presetId = requireString(args, "preset_id");
|
|
1732
|
+
// Resolve the default title client-side so Claude can create docs
|
|
1733
|
+
// without a second round-trip to list presets. The API validates
|
|
1734
|
+
// preset_id again (and 404s on unknown ids) regardless.
|
|
1735
|
+
let defaultTitle = presetId;
|
|
1736
|
+
try {
|
|
1737
|
+
const list = (await listDocPresets());
|
|
1738
|
+
const found = list.presets?.find((p) => p.id === presetId);
|
|
1739
|
+
if (found?.defaultTitle)
|
|
1740
|
+
defaultTitle = found.defaultTitle;
|
|
1741
|
+
}
|
|
1742
|
+
catch {
|
|
1743
|
+
// Non-fatal — fall through to presetId as the title. API still
|
|
1744
|
+
// validates preset_id below.
|
|
1745
|
+
}
|
|
1746
|
+
const title = typeof args.title === "string" && args.title.trim().length > 0
|
|
1747
|
+
? args.title
|
|
1748
|
+
: defaultTitle;
|
|
1749
|
+
return createDoc({
|
|
1750
|
+
title,
|
|
1751
|
+
preset_id: presetId,
|
|
1752
|
+
...(typeof args.folder_id === "string" ? { folder_id: args.folder_id } : {}),
|
|
1753
|
+
...(typeof args.shoot_id === "string" ? { shoot_id: args.shoot_id } : {}),
|
|
1754
|
+
});
|
|
1755
|
+
}
|
|
1756
|
+
async function deleteDoc(args) {
|
|
1757
|
+
const cfg = resolveConfig();
|
|
1758
|
+
const docId = requireString(args, "docId");
|
|
1759
|
+
const purge = args.purge === true;
|
|
1760
|
+
const path = purge ? `/api/v1/docs/${docId}?purge=1` : `/api/v1/docs/${docId}`;
|
|
1761
|
+
const { data } = await apiRequest(cfg, path, { method: "DELETE" });
|
|
1762
|
+
return data;
|
|
1763
|
+
}
|
|
1764
|
+
async function restoreDoc(args) {
|
|
1765
|
+
const cfg = resolveConfig();
|
|
1766
|
+
const docId = requireString(args, "docId");
|
|
1767
|
+
const { data } = await apiRequest(cfg, `/api/v1/docs/${docId}/restore`, {
|
|
1768
|
+
method: "POST",
|
|
1769
|
+
});
|
|
1770
|
+
return data;
|
|
1771
|
+
}
|
|
1772
|
+
async function purgeDoc(args) {
|
|
1773
|
+
const cfg = resolveConfig();
|
|
1774
|
+
const docId = requireString(args, "docId");
|
|
1775
|
+
const { data } = await apiRequest(cfg, `/api/v1/docs/${docId}?purge=1`, {
|
|
1776
|
+
method: "DELETE",
|
|
1777
|
+
});
|
|
1778
|
+
return data;
|
|
1779
|
+
}
|
|
1780
|
+
async function updateTemplate(args) {
|
|
1781
|
+
const cfg = resolveConfig();
|
|
1782
|
+
const docId = requireString(args, "docId");
|
|
1783
|
+
const templateContent = args.template_content;
|
|
1784
|
+
if (!isRecord(templateContent)) {
|
|
1785
|
+
throw new Error("`template_content` must be an object matching TemplateContent shape.");
|
|
1786
|
+
}
|
|
1787
|
+
const title = typeof args.title === "string" ? args.title : undefined;
|
|
1788
|
+
// Get-then-put for etag freshness.
|
|
1789
|
+
const current = await getTemplate({ docId });
|
|
1790
|
+
try {
|
|
1791
|
+
const { data } = await apiRequest(cfg, `/api/v1/docs/${docId}/template`, {
|
|
1792
|
+
method: "PUT",
|
|
1793
|
+
body: { template_content: templateContent, ...(title ? { title } : {}) },
|
|
1794
|
+
ifMatch: current.etag,
|
|
447
1795
|
});
|
|
448
1796
|
return {
|
|
449
1797
|
docId: data.id,
|
|
@@ -452,224 +1800,1494 @@ async function updateTemplate(args) {
|
|
|
452
1800
|
updated_at: data.updated_at,
|
|
453
1801
|
};
|
|
454
1802
|
}
|
|
455
|
-
catch (e) {
|
|
456
|
-
if (e instanceof ApiError && e.status === 409) {
|
|
457
|
-
throw new Error("Doc was modified since we fetched it — re-run atlas_get_template to pick up the concurrent change, then retry.");
|
|
1803
|
+
catch (e) {
|
|
1804
|
+
if (e instanceof ApiError && e.status === 409) {
|
|
1805
|
+
throw new Error("Doc was modified since we fetched it — re-run atlas_get_template to pick up the concurrent change, then retry.");
|
|
1806
|
+
}
|
|
1807
|
+
throw e;
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
async function describeSectionType(args) {
|
|
1811
|
+
const type = args.type;
|
|
1812
|
+
if (typeof type === "string") {
|
|
1813
|
+
const entry = findSection(type);
|
|
1814
|
+
if (!entry) {
|
|
1815
|
+
throw new Error(`Unknown section type '${type}'. Known types: ${SECTION_CATALOG.map((s) => s.type).join(", ")}.`);
|
|
1816
|
+
}
|
|
1817
|
+
return entry;
|
|
1818
|
+
}
|
|
1819
|
+
return { types: SECTION_CATALOG };
|
|
1820
|
+
}
|
|
1821
|
+
/* ─── Files tools ──────────────────────────────────────────────────── */
|
|
1822
|
+
async function listFiles(args) {
|
|
1823
|
+
const cfg = resolveConfig();
|
|
1824
|
+
const query = {};
|
|
1825
|
+
if (typeof args.folder_id === "string")
|
|
1826
|
+
query.folder_id = args.folder_id;
|
|
1827
|
+
if (typeof args.limit === "number")
|
|
1828
|
+
query.limit = args.limit;
|
|
1829
|
+
if (typeof args.cursor === "string")
|
|
1830
|
+
query.cursor = args.cursor;
|
|
1831
|
+
const { data } = await apiRequest(cfg, "/api/v1/files", { query });
|
|
1832
|
+
return data;
|
|
1833
|
+
}
|
|
1834
|
+
async function uploadFile(args) {
|
|
1835
|
+
const cfg = resolveConfig();
|
|
1836
|
+
const localPath = requireString(args, "local_path");
|
|
1837
|
+
const alt = typeof args.alt === "string" ? args.alt : undefined;
|
|
1838
|
+
const { readFile, stat } = await import("node:fs/promises");
|
|
1839
|
+
const { basename, extname } = await import("node:path");
|
|
1840
|
+
const info = await stat(localPath);
|
|
1841
|
+
if (!info.isFile())
|
|
1842
|
+
throw new Error(`Not a file: ${localPath}`);
|
|
1843
|
+
const filename = basename(localPath);
|
|
1844
|
+
const ext = extname(filename).toLowerCase();
|
|
1845
|
+
const contentTypes = {
|
|
1846
|
+
".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg",
|
|
1847
|
+
".gif": "image/gif", ".webp": "image/webp", ".svg": "image/svg+xml",
|
|
1848
|
+
".mp4": "video/mp4", ".mov": "video/quicktime", ".pdf": "application/pdf",
|
|
1849
|
+
".txt": "text/plain", ".md": "text/markdown", ".json": "application/json",
|
|
1850
|
+
".csv": "text/csv",
|
|
1851
|
+
};
|
|
1852
|
+
const contentType = contentTypes[ext] ?? "application/octet-stream";
|
|
1853
|
+
// Step 1: signed URL
|
|
1854
|
+
const { data: signRes } = await apiRequest(cfg, "/api/v1/files/upload-url", {
|
|
1855
|
+
method: "POST",
|
|
1856
|
+
body: { filename },
|
|
1857
|
+
});
|
|
1858
|
+
// Step 2: PUT bytes
|
|
1859
|
+
const bytes = await readFile(localPath);
|
|
1860
|
+
const putRes = await fetch(signRes.upload_url, {
|
|
1861
|
+
method: "PUT",
|
|
1862
|
+
headers: { "Content-Type": contentType, "x-upsert": "false" },
|
|
1863
|
+
body: bytes,
|
|
1864
|
+
});
|
|
1865
|
+
if (!putRes.ok) {
|
|
1866
|
+
const text = await putRes.text().catch(() => "");
|
|
1867
|
+
throw new Error(`Storage upload failed (HTTP ${putRes.status}): ${text.slice(0, 200)}`);
|
|
1868
|
+
}
|
|
1869
|
+
// Step 3: register metadata
|
|
1870
|
+
const { data: reg } = await apiRequest(cfg, "/api/v1/files", {
|
|
1871
|
+
method: "POST",
|
|
1872
|
+
body: { path: signRes.path, alt },
|
|
1873
|
+
});
|
|
1874
|
+
return {
|
|
1875
|
+
path: signRes.path,
|
|
1876
|
+
size: info.size,
|
|
1877
|
+
content_type: contentType,
|
|
1878
|
+
metadata: reg.file,
|
|
1879
|
+
};
|
|
1880
|
+
}
|
|
1881
|
+
async function getFileUrl(args) {
|
|
1882
|
+
const cfg = resolveConfig();
|
|
1883
|
+
const path = requireString(args, "path");
|
|
1884
|
+
const { data } = await apiRequest(cfg, "/api/v1/files", { query: { limit: 500 } });
|
|
1885
|
+
const match = data.files.find((f) => f.path === path);
|
|
1886
|
+
if (!match)
|
|
1887
|
+
throw new Error(`File not found or no metadata row: ${path}`);
|
|
1888
|
+
return { path: match.path, url: match.signed_url, alt: match.alt };
|
|
1889
|
+
}
|
|
1890
|
+
async function setFileAlt(args) {
|
|
1891
|
+
const cfg = resolveConfig();
|
|
1892
|
+
const path = requireString(args, "path");
|
|
1893
|
+
const alt = typeof args.alt === "string" ? args.alt : null;
|
|
1894
|
+
const { data } = await apiRequest(cfg, "/api/v1/files/alt", {
|
|
1895
|
+
method: "POST",
|
|
1896
|
+
body: { path, alt },
|
|
1897
|
+
});
|
|
1898
|
+
return data;
|
|
1899
|
+
}
|
|
1900
|
+
async function deleteFile(args) {
|
|
1901
|
+
const cfg = resolveConfig();
|
|
1902
|
+
const path = requireString(args, "path");
|
|
1903
|
+
const { data } = await apiRequest(cfg, "/api/v1/files", {
|
|
1904
|
+
method: "DELETE",
|
|
1905
|
+
query: { path },
|
|
1906
|
+
});
|
|
1907
|
+
return data;
|
|
1908
|
+
}
|
|
1909
|
+
/* ─── Gear tools ───────────────────────────────────────────────────── */
|
|
1910
|
+
async function listGear(args) {
|
|
1911
|
+
const cfg = resolveConfig();
|
|
1912
|
+
const query = {};
|
|
1913
|
+
if (typeof args.limit === "number")
|
|
1914
|
+
query.limit = args.limit;
|
|
1915
|
+
if (typeof args.cursor === "string")
|
|
1916
|
+
query.cursor = args.cursor;
|
|
1917
|
+
if (args.deleted === true)
|
|
1918
|
+
query.deleted = 1;
|
|
1919
|
+
const { data } = await apiRequest(cfg, "/api/v1/gear", { query });
|
|
1920
|
+
return data;
|
|
1921
|
+
}
|
|
1922
|
+
async function addGear(args) {
|
|
1923
|
+
const cfg = resolveConfig();
|
|
1924
|
+
const body = { name: requireString(args, "name") };
|
|
1925
|
+
if (typeof args.brand === "string")
|
|
1926
|
+
body.brand = args.brand;
|
|
1927
|
+
if (typeof args.quantity === "number")
|
|
1928
|
+
body.quantity = args.quantity;
|
|
1929
|
+
if (typeof args.image_path === "string")
|
|
1930
|
+
body.image_path = args.image_path;
|
|
1931
|
+
const { data } = await apiRequest(cfg, "/api/v1/gear", {
|
|
1932
|
+
method: "POST",
|
|
1933
|
+
body,
|
|
1934
|
+
});
|
|
1935
|
+
return data;
|
|
1936
|
+
}
|
|
1937
|
+
async function importGear(args) {
|
|
1938
|
+
const cfg = resolveConfig();
|
|
1939
|
+
const rows = args.rows;
|
|
1940
|
+
if (!Array.isArray(rows))
|
|
1941
|
+
throw new Error("`rows` must be an array.");
|
|
1942
|
+
const mode = args.mode === "replace" ? "replace" : "append";
|
|
1943
|
+
const { data } = await apiRequest(cfg, "/api/v1/gear/import", {
|
|
1944
|
+
method: "POST",
|
|
1945
|
+
body: { mode, rows },
|
|
1946
|
+
});
|
|
1947
|
+
return data;
|
|
1948
|
+
}
|
|
1949
|
+
async function deleteGear(args) {
|
|
1950
|
+
const cfg = resolveConfig();
|
|
1951
|
+
const gearId = requireString(args, "gearId");
|
|
1952
|
+
const purge = args.purge === true;
|
|
1953
|
+
const path = purge ? `/api/v1/gear/${gearId}?purge=1` : `/api/v1/gear/${gearId}`;
|
|
1954
|
+
const { data } = await apiRequest(cfg, path, { method: "DELETE" });
|
|
1955
|
+
return data;
|
|
1956
|
+
}
|
|
1957
|
+
async function restoreGear(args) {
|
|
1958
|
+
const cfg = resolveConfig();
|
|
1959
|
+
const gearId = requireString(args, "gearId");
|
|
1960
|
+
const { data } = await apiRequest(cfg, `/api/v1/gear/${gearId}/restore`, {
|
|
1961
|
+
method: "POST",
|
|
1962
|
+
});
|
|
1963
|
+
return data;
|
|
1964
|
+
}
|
|
1965
|
+
async function purgeGear(args) {
|
|
1966
|
+
const cfg = resolveConfig();
|
|
1967
|
+
const gearId = requireString(args, "gearId");
|
|
1968
|
+
const { data } = await apiRequest(cfg, `/api/v1/gear/${gearId}?purge=1`, {
|
|
1969
|
+
method: "DELETE",
|
|
1970
|
+
});
|
|
1971
|
+
return data;
|
|
1972
|
+
}
|
|
1973
|
+
async function updateGear(args) {
|
|
1974
|
+
const cfg = resolveConfig();
|
|
1975
|
+
const gearId = requireString(args, "gearId");
|
|
1976
|
+
const body = {};
|
|
1977
|
+
if (typeof args.name === "string")
|
|
1978
|
+
body.name = args.name;
|
|
1979
|
+
if (typeof args.brand === "string")
|
|
1980
|
+
body.brand = args.brand;
|
|
1981
|
+
if (typeof args.quantity === "number")
|
|
1982
|
+
body.quantity = args.quantity;
|
|
1983
|
+
if (typeof args.image_path === "string")
|
|
1984
|
+
body.image_path = args.image_path;
|
|
1985
|
+
if (Object.keys(body).length === 0) {
|
|
1986
|
+
throw new Error("At least one of name, brand, quantity, image_path is required.");
|
|
1987
|
+
}
|
|
1988
|
+
const { data } = await apiRequest(cfg, `/api/v1/gear/${gearId}`, {
|
|
1989
|
+
method: "PUT",
|
|
1990
|
+
body,
|
|
1991
|
+
});
|
|
1992
|
+
return data;
|
|
1993
|
+
}
|
|
1994
|
+
/* ─── Shot list tools ──────────────────────────────────────────────── */
|
|
1995
|
+
async function listShots(args) {
|
|
1996
|
+
const cfg = resolveConfig();
|
|
1997
|
+
const query = {};
|
|
1998
|
+
if (typeof args.limit === "number")
|
|
1999
|
+
query.limit = args.limit;
|
|
2000
|
+
if (typeof args.cursor === "string")
|
|
2001
|
+
query.cursor = args.cursor;
|
|
2002
|
+
if (args.deleted === true)
|
|
2003
|
+
query.deleted = 1;
|
|
2004
|
+
const { data } = await apiRequest(cfg, "/api/v1/shots", { query });
|
|
2005
|
+
return data;
|
|
2006
|
+
}
|
|
2007
|
+
async function addShot(args) {
|
|
2008
|
+
const cfg = resolveConfig();
|
|
2009
|
+
const body = { name: requireString(args, "name") };
|
|
2010
|
+
if (typeof args.kind === "string")
|
|
2011
|
+
body.kind = args.kind;
|
|
2012
|
+
if (typeof args.notes === "string")
|
|
2013
|
+
body.notes = args.notes;
|
|
2014
|
+
if (typeof args.image_path === "string")
|
|
2015
|
+
body.image_path = args.image_path;
|
|
2016
|
+
if (Array.isArray(args.gear_ids))
|
|
2017
|
+
body.gear_ids = args.gear_ids;
|
|
2018
|
+
const { data } = await apiRequest(cfg, "/api/v1/shots", {
|
|
2019
|
+
method: "POST",
|
|
2020
|
+
body,
|
|
2021
|
+
});
|
|
2022
|
+
return data;
|
|
2023
|
+
}
|
|
2024
|
+
async function updateShot(args) {
|
|
2025
|
+
const cfg = resolveConfig();
|
|
2026
|
+
const shotId = requireString(args, "shotId");
|
|
2027
|
+
const body = {};
|
|
2028
|
+
if (typeof args.name === "string")
|
|
2029
|
+
body.name = args.name;
|
|
2030
|
+
if (typeof args.kind === "string")
|
|
2031
|
+
body.kind = args.kind;
|
|
2032
|
+
if (typeof args.notes === "string")
|
|
2033
|
+
body.notes = args.notes;
|
|
2034
|
+
if (typeof args.image_path === "string")
|
|
2035
|
+
body.image_path = args.image_path;
|
|
2036
|
+
if (Array.isArray(args.gear_ids))
|
|
2037
|
+
body.gear_ids = args.gear_ids;
|
|
2038
|
+
if (Object.keys(body).length === 0) {
|
|
2039
|
+
throw new Error("At least one of name, kind, notes, image_path, gear_ids is required.");
|
|
2040
|
+
}
|
|
2041
|
+
const { data } = await apiRequest(cfg, `/api/v1/shots/${shotId}`, {
|
|
2042
|
+
method: "PATCH",
|
|
2043
|
+
body,
|
|
2044
|
+
});
|
|
2045
|
+
return data;
|
|
2046
|
+
}
|
|
2047
|
+
async function deleteShot(args) {
|
|
2048
|
+
const cfg = resolveConfig();
|
|
2049
|
+
const shotId = requireString(args, "shotId");
|
|
2050
|
+
const purge = args.purge === true;
|
|
2051
|
+
const path = purge ? `/api/v1/shots/${shotId}?purge=1` : `/api/v1/shots/${shotId}`;
|
|
2052
|
+
const { data } = await apiRequest(cfg, path, { method: "DELETE" });
|
|
2053
|
+
return data;
|
|
2054
|
+
}
|
|
2055
|
+
async function restoreShot(args) {
|
|
2056
|
+
const cfg = resolveConfig();
|
|
2057
|
+
const shotId = requireString(args, "shotId");
|
|
2058
|
+
const { data } = await apiRequest(cfg, `/api/v1/shots/${shotId}/restore`, {
|
|
2059
|
+
method: "POST",
|
|
2060
|
+
});
|
|
2061
|
+
return data;
|
|
2062
|
+
}
|
|
2063
|
+
async function purgeShot(args) {
|
|
2064
|
+
const cfg = resolveConfig();
|
|
2065
|
+
const shotId = requireString(args, "shotId");
|
|
2066
|
+
const { data } = await apiRequest(cfg, `/api/v1/shots/${shotId}?purge=1`, {
|
|
2067
|
+
method: "DELETE",
|
|
2068
|
+
});
|
|
2069
|
+
return data;
|
|
2070
|
+
}
|
|
2071
|
+
async function importShots(args) {
|
|
2072
|
+
const cfg = resolveConfig();
|
|
2073
|
+
const rows = args.rows;
|
|
2074
|
+
if (!Array.isArray(rows))
|
|
2075
|
+
throw new Error("`rows` must be an array.");
|
|
2076
|
+
const mode = args.mode === "replace" ? "replace" : "append";
|
|
2077
|
+
const { data } = await apiRequest(cfg, "/api/v1/shots/import", {
|
|
2078
|
+
method: "POST",
|
|
2079
|
+
body: { mode, rows },
|
|
2080
|
+
});
|
|
2081
|
+
return data;
|
|
2082
|
+
}
|
|
2083
|
+
/* ─── Events (calendar shoots) ─────────────────────────────────────── */
|
|
2084
|
+
async function listEvents(args) {
|
|
2085
|
+
const cfg = resolveConfig();
|
|
2086
|
+
const query = {};
|
|
2087
|
+
if (typeof args.limit === "number")
|
|
2088
|
+
query.limit = args.limit;
|
|
2089
|
+
if (typeof args.cursor === "string")
|
|
2090
|
+
query.cursor = args.cursor;
|
|
2091
|
+
if (typeof args.from === "string")
|
|
2092
|
+
query.from = args.from;
|
|
2093
|
+
if (typeof args.to === "string")
|
|
2094
|
+
query.to = args.to;
|
|
2095
|
+
if (Array.isArray(args.status) && args.status.length > 0) {
|
|
2096
|
+
query.status = args.status.filter((s) => typeof s === "string").join(",");
|
|
2097
|
+
}
|
|
2098
|
+
if (Array.isArray(args.colour) && args.colour.length > 0) {
|
|
2099
|
+
query.colour = args.colour.filter((s) => typeof s === "string").join(",");
|
|
2100
|
+
}
|
|
2101
|
+
const { data } = await apiRequest(cfg, "/api/v1/events", { query });
|
|
2102
|
+
return data;
|
|
2103
|
+
}
|
|
2104
|
+
async function getEvent(args) {
|
|
2105
|
+
const cfg = resolveConfig();
|
|
2106
|
+
const eventId = requireString(args, "eventId");
|
|
2107
|
+
const { data } = await apiRequest(cfg, `/api/v1/events/${eventId}`);
|
|
2108
|
+
return data;
|
|
2109
|
+
}
|
|
2110
|
+
async function createEvent(args) {
|
|
2111
|
+
const cfg = resolveConfig();
|
|
2112
|
+
const body = {
|
|
2113
|
+
title: requireString(args, "title"),
|
|
2114
|
+
starts_at: requireString(args, "starts_at"),
|
|
2115
|
+
};
|
|
2116
|
+
if (typeof args.ends_at === "string")
|
|
2117
|
+
body.ends_at = args.ends_at;
|
|
2118
|
+
if (typeof args.all_day === "boolean")
|
|
2119
|
+
body.all_day = args.all_day;
|
|
2120
|
+
if (typeof args.location === "string")
|
|
2121
|
+
body.location = args.location;
|
|
2122
|
+
if (typeof args.shoot_type === "string")
|
|
2123
|
+
body.shoot_type = args.shoot_type;
|
|
2124
|
+
if (typeof args.colour_key === "string")
|
|
2125
|
+
body.colour_key = args.colour_key;
|
|
2126
|
+
if (typeof args.colour_hex === "string")
|
|
2127
|
+
body.colour_hex = args.colour_hex;
|
|
2128
|
+
if (typeof args.status === "string")
|
|
2129
|
+
body.status = args.status;
|
|
2130
|
+
if (typeof args.notes === "string")
|
|
2131
|
+
body.notes = args.notes;
|
|
2132
|
+
if (typeof args.description === "string")
|
|
2133
|
+
body.description = args.description;
|
|
2134
|
+
const { data } = await apiRequest(cfg, "/api/v1/events", {
|
|
2135
|
+
method: "POST",
|
|
2136
|
+
body,
|
|
2137
|
+
});
|
|
2138
|
+
return data;
|
|
2139
|
+
}
|
|
2140
|
+
async function updateEvent(args) {
|
|
2141
|
+
const cfg = resolveConfig();
|
|
2142
|
+
const eventId = requireString(args, "eventId");
|
|
2143
|
+
const body = {};
|
|
2144
|
+
if (typeof args.title === "string")
|
|
2145
|
+
body.title = args.title;
|
|
2146
|
+
if (typeof args.starts_at === "string")
|
|
2147
|
+
body.starts_at = args.starts_at;
|
|
2148
|
+
if (typeof args.ends_at === "string")
|
|
2149
|
+
body.ends_at = args.ends_at;
|
|
2150
|
+
if (typeof args.all_day === "boolean")
|
|
2151
|
+
body.all_day = args.all_day;
|
|
2152
|
+
if (typeof args.location === "string")
|
|
2153
|
+
body.location = args.location;
|
|
2154
|
+
if (typeof args.shoot_type === "string")
|
|
2155
|
+
body.shoot_type = args.shoot_type;
|
|
2156
|
+
if (typeof args.colour_key === "string")
|
|
2157
|
+
body.colour_key = args.colour_key;
|
|
2158
|
+
if (typeof args.colour_hex === "string")
|
|
2159
|
+
body.colour_hex = args.colour_hex;
|
|
2160
|
+
if (typeof args.status === "string")
|
|
2161
|
+
body.status = args.status;
|
|
2162
|
+
if (typeof args.notes === "string")
|
|
2163
|
+
body.notes = args.notes;
|
|
2164
|
+
if (typeof args.description === "string")
|
|
2165
|
+
body.description = args.description;
|
|
2166
|
+
if (Object.keys(body).length === 0) {
|
|
2167
|
+
throw new Error("At least one writable field (title, starts_at, ends_at, all_day, location, shoot_type, colour_key, colour_hex, status, notes, description) is required.");
|
|
2168
|
+
}
|
|
2169
|
+
const { data } = await apiRequest(cfg, `/api/v1/events/${eventId}`, { method: "PATCH", body });
|
|
2170
|
+
return data;
|
|
2171
|
+
}
|
|
2172
|
+
async function deleteEvent(args) {
|
|
2173
|
+
const cfg = resolveConfig();
|
|
2174
|
+
const eventId = requireString(args, "eventId");
|
|
2175
|
+
const { data } = await apiRequest(cfg, `/api/v1/events/${eventId}`, {
|
|
2176
|
+
method: "DELETE",
|
|
2177
|
+
});
|
|
2178
|
+
return data;
|
|
2179
|
+
}
|
|
2180
|
+
async function assignEvent(args) {
|
|
2181
|
+
const cfg = resolveConfig();
|
|
2182
|
+
const eventId = requireString(args, "eventId");
|
|
2183
|
+
const body = {
|
|
2184
|
+
user_id: requireString(args, "user_id"),
|
|
2185
|
+
};
|
|
2186
|
+
if (typeof args.shoot_role === "string")
|
|
2187
|
+
body.shoot_role = args.shoot_role;
|
|
2188
|
+
if (Array.isArray(args.responsibilities)) {
|
|
2189
|
+
body.responsibilities = args.responsibilities;
|
|
2190
|
+
}
|
|
2191
|
+
if (typeof args.notes === "string")
|
|
2192
|
+
body.notes = args.notes;
|
|
2193
|
+
const { data } = await apiRequest(cfg, `/api/v1/events/${eventId}/assignments`, { method: "POST", body });
|
|
2194
|
+
return data;
|
|
2195
|
+
}
|
|
2196
|
+
async function unassignEvent(args) {
|
|
2197
|
+
const cfg = resolveConfig();
|
|
2198
|
+
const eventId = requireString(args, "eventId");
|
|
2199
|
+
const assignmentId = requireString(args, "assignmentId");
|
|
2200
|
+
const { data } = await apiRequest(cfg, `/api/v1/events/${eventId}/assignments/${assignmentId}`, { method: "DELETE" });
|
|
2201
|
+
return data;
|
|
2202
|
+
}
|
|
2203
|
+
/* ─── Files rename ─────────────────────────────────────────────────── */
|
|
2204
|
+
async function renameFile(args) {
|
|
2205
|
+
const cfg = resolveConfig();
|
|
2206
|
+
const path = requireString(args, "path");
|
|
2207
|
+
const newName = requireString(args, "new_name");
|
|
2208
|
+
const { data } = await apiRequest(cfg, "/api/v1/files/rename", {
|
|
2209
|
+
method: "POST",
|
|
2210
|
+
body: { path, new_name: newName },
|
|
2211
|
+
});
|
|
2212
|
+
return data;
|
|
2213
|
+
}
|
|
2214
|
+
/* ─── Revisions ────────────────────────────────────────────────────── */
|
|
2215
|
+
async function listRevisions(args) {
|
|
2216
|
+
const cfg = resolveConfig();
|
|
2217
|
+
const docId = requireString(args, "docId");
|
|
2218
|
+
const query = {};
|
|
2219
|
+
if (typeof args.limit === "number")
|
|
2220
|
+
query.limit = args.limit;
|
|
2221
|
+
const { data } = await apiRequest(cfg, `/api/v1/docs/${docId}/revisions`, { query });
|
|
2222
|
+
return data;
|
|
2223
|
+
}
|
|
2224
|
+
async function restoreRevision(args) {
|
|
2225
|
+
const cfg = resolveConfig();
|
|
2226
|
+
const docId = requireString(args, "docId");
|
|
2227
|
+
const revisionId = requireString(args, "revisionId");
|
|
2228
|
+
const { data } = await apiRequest(cfg, `/api/v1/docs/${docId}/revisions/${revisionId}/restore`, {
|
|
2229
|
+
method: "POST",
|
|
2230
|
+
});
|
|
2231
|
+
return {
|
|
2232
|
+
docId: data.id,
|
|
2233
|
+
title: data.title,
|
|
2234
|
+
updated_at: data.updated_at,
|
|
2235
|
+
etag: data.etag,
|
|
2236
|
+
restored_from: data.restored_from,
|
|
2237
|
+
};
|
|
2238
|
+
}
|
|
2239
|
+
async function whoami() {
|
|
2240
|
+
const cfg = resolveConfig();
|
|
2241
|
+
const { data } = await apiRequest(cfg, "/api/v1/whoami");
|
|
2242
|
+
return data;
|
|
2243
|
+
}
|
|
2244
|
+
/** The dedicated "prove Claude Desktop MCP is wired up" call. Only
|
|
2245
|
+
* this endpoint stamps workspace_members.onboarding.mcp_verified_at,
|
|
2246
|
+
* which is the signal the onboarding walkthrough's phase 4 waits for.
|
|
2247
|
+
* Kept intentionally distinct from whoami so accidental introspection
|
|
2248
|
+
* calls can't false-positive the setup flow. */
|
|
2249
|
+
async function verifyConnection() {
|
|
2250
|
+
const cfg = resolveConfig();
|
|
2251
|
+
const { data } = await apiRequest(cfg, "/api/v1/verify", { method: "POST" });
|
|
2252
|
+
return data;
|
|
2253
|
+
}
|
|
2254
|
+
/* ─── Metafield definitions ────────────────────────────────────────── */
|
|
2255
|
+
async function listMetafieldDefinitions() {
|
|
2256
|
+
const cfg = resolveConfig();
|
|
2257
|
+
const { data } = await apiRequest(cfg, "/api/v1/metafields");
|
|
2258
|
+
return data;
|
|
2259
|
+
}
|
|
2260
|
+
async function suggestMetafields() {
|
|
2261
|
+
const cfg = resolveConfig();
|
|
2262
|
+
const { data } = await apiRequest(cfg, "/api/v1/metafields", { query: { suggest: "1" } });
|
|
2263
|
+
return data;
|
|
2264
|
+
}
|
|
2265
|
+
async function scanShopifyMetafields() {
|
|
2266
|
+
const cfg = resolveConfig();
|
|
2267
|
+
const { data } = await apiRequest(cfg, "/api/v1/metafields/scan");
|
|
2268
|
+
return data;
|
|
2269
|
+
}
|
|
2270
|
+
async function createMetafieldDefinition(args) {
|
|
2271
|
+
const cfg = resolveConfig();
|
|
2272
|
+
const body = {};
|
|
2273
|
+
if (typeof args.preset === "string")
|
|
2274
|
+
body.preset = args.preset;
|
|
2275
|
+
if (typeof args.namespace === "string")
|
|
2276
|
+
body.namespace = args.namespace;
|
|
2277
|
+
if (typeof args.key === "string")
|
|
2278
|
+
body.key = args.key;
|
|
2279
|
+
if (typeof args.label === "string")
|
|
2280
|
+
body.label = args.label;
|
|
2281
|
+
if (typeof args.description === "string")
|
|
2282
|
+
body.description = args.description;
|
|
2283
|
+
if (typeof args.section === "string")
|
|
2284
|
+
body.section = args.section;
|
|
2285
|
+
if (typeof args.display_style === "string")
|
|
2286
|
+
body.display_style = args.display_style;
|
|
2287
|
+
const { data } = await apiRequest(cfg, "/api/v1/metafields", { method: "POST", body });
|
|
2288
|
+
return data;
|
|
2289
|
+
}
|
|
2290
|
+
async function deleteMetafieldDefinition(args) {
|
|
2291
|
+
const cfg = resolveConfig();
|
|
2292
|
+
const id = requireString(args, "id");
|
|
2293
|
+
await apiRequest(cfg, `/api/v1/metafields/${encodeURIComponent(id)}`, {
|
|
2294
|
+
method: "DELETE",
|
|
2295
|
+
});
|
|
2296
|
+
return { ok: true };
|
|
2297
|
+
}
|
|
2298
|
+
/* ─── Helpers ──────────────────────────────────────────────────────── */
|
|
2299
|
+
function requireString(args, key) {
|
|
2300
|
+
const v = args[key];
|
|
2301
|
+
if (typeof v !== "string" || !v)
|
|
2302
|
+
throw new Error(`\`${key}\` is required.`);
|
|
2303
|
+
return v;
|
|
2304
|
+
}
|
|
2305
|
+
function isRecord(v) {
|
|
2306
|
+
return typeof v === "object" && v !== null;
|
|
2307
|
+
}
|
|
2308
|
+
/* ─── Loyalty + analytics tools ────────────────────────────────────── */
|
|
2309
|
+
async function listLoyaltyTiers() {
|
|
2310
|
+
const cfg = resolveConfig();
|
|
2311
|
+
const { data } = await apiRequest(cfg, "/api/v1/loyalty/tiers");
|
|
2312
|
+
return data;
|
|
2313
|
+
}
|
|
2314
|
+
async function setLoyaltyTiers(args) {
|
|
2315
|
+
const cfg = resolveConfig();
|
|
2316
|
+
const tiers = args.tiers;
|
|
2317
|
+
if (!Array.isArray(tiers))
|
|
2318
|
+
throw new Error("`tiers` must be an array. See atlas_set_loyalty_tiers for the shape.");
|
|
2319
|
+
const { data } = await apiRequest(cfg, "/api/v1/loyalty/tiers", { method: "PUT", body: { tiers } });
|
|
2320
|
+
return data;
|
|
2321
|
+
}
|
|
2322
|
+
async function resetLoyaltyTiers() {
|
|
2323
|
+
const cfg = resolveConfig();
|
|
2324
|
+
const { data } = await apiRequest(cfg, "/api/v1/loyalty/tiers", { method: "DELETE" });
|
|
2325
|
+
return data;
|
|
2326
|
+
}
|
|
2327
|
+
async function getAnalyticsPrefs() {
|
|
2328
|
+
const cfg = resolveConfig();
|
|
2329
|
+
const { data } = await apiRequest(cfg, "/api/v1/analytics/prefs");
|
|
2330
|
+
return data;
|
|
2331
|
+
}
|
|
2332
|
+
async function setAnalyticsPrefs(args) {
|
|
2333
|
+
const cfg = resolveConfig();
|
|
2334
|
+
const body = {};
|
|
2335
|
+
if (Array.isArray(args.excluded_tags))
|
|
2336
|
+
body.excluded_tags = args.excluded_tags;
|
|
2337
|
+
else if (Array.isArray(args.excludedTags))
|
|
2338
|
+
body.excluded_tags = args.excludedTags;
|
|
2339
|
+
if (Array.isArray(args.excluded_statuses))
|
|
2340
|
+
body.excluded_statuses = args.excluded_statuses;
|
|
2341
|
+
else if (Array.isArray(args.excludedStatuses))
|
|
2342
|
+
body.excluded_statuses = args.excludedStatuses;
|
|
2343
|
+
if (Array.isArray(args.excluded_customer_tags))
|
|
2344
|
+
body.excluded_customer_tags = args.excluded_customer_tags;
|
|
2345
|
+
else if (Array.isArray(args.excludedCustomerTags))
|
|
2346
|
+
body.excluded_customer_tags = args.excludedCustomerTags;
|
|
2347
|
+
if (typeof args.exclude_non_subscribers === "boolean")
|
|
2348
|
+
body.exclude_non_subscribers = args.exclude_non_subscribers;
|
|
2349
|
+
else if (typeof args.excludeNonSubscribers === "boolean")
|
|
2350
|
+
body.exclude_non_subscribers = args.excludeNonSubscribers;
|
|
2351
|
+
if (typeof args.exclude_guest_checkouts === "boolean")
|
|
2352
|
+
body.exclude_guest_checkouts = args.exclude_guest_checkouts;
|
|
2353
|
+
else if (typeof args.excludeGuestCheckouts === "boolean")
|
|
2354
|
+
body.exclude_guest_checkouts = args.excludeGuestCheckouts;
|
|
2355
|
+
if (typeof args.exclude_unverified_emails === "boolean")
|
|
2356
|
+
body.exclude_unverified_emails = args.exclude_unverified_emails;
|
|
2357
|
+
else if (typeof args.excludeUnverifiedEmails === "boolean")
|
|
2358
|
+
body.exclude_unverified_emails = args.excludeUnverifiedEmails;
|
|
2359
|
+
if (Object.keys(body).length === 0)
|
|
2360
|
+
throw new Error("Pass at least one exclusion field (tags, statuses, customer tags, or a customer-level boolean flag).");
|
|
2361
|
+
const { data } = await apiRequest(cfg, "/api/v1/analytics/prefs", {
|
|
2362
|
+
method: "PUT",
|
|
2363
|
+
body,
|
|
2364
|
+
});
|
|
2365
|
+
return data;
|
|
2366
|
+
}
|
|
2367
|
+
/* ─── Workspace tools ──────────────────────────────────────────────── */
|
|
2368
|
+
async function getWorkspace() {
|
|
2369
|
+
const cfg = resolveConfig();
|
|
2370
|
+
const { data } = await apiRequest(cfg, "/api/v1/workspace");
|
|
2371
|
+
return data;
|
|
2372
|
+
}
|
|
2373
|
+
async function updateWorkspace(args) {
|
|
2374
|
+
const cfg = resolveConfig();
|
|
2375
|
+
const body = {};
|
|
2376
|
+
if (typeof args.name === "string")
|
|
2377
|
+
body.name = args.name;
|
|
2378
|
+
if (typeof args.slug === "string")
|
|
2379
|
+
body.slug = args.slug;
|
|
2380
|
+
// logo_bg can be a string OR explicit null; anything else means "leave alone".
|
|
2381
|
+
if (typeof args.logo_bg === "string" || args.logo_bg === null) {
|
|
2382
|
+
body.logo_bg = args.logo_bg;
|
|
2383
|
+
}
|
|
2384
|
+
if (isRecord(args.theme)) {
|
|
2385
|
+
const theme = {};
|
|
2386
|
+
if (typeof args.theme.bg === "string")
|
|
2387
|
+
theme.bg = args.theme.bg;
|
|
2388
|
+
if (typeof args.theme.primary === "string")
|
|
2389
|
+
theme.primary = args.theme.primary;
|
|
2390
|
+
if (typeof args.theme.accent === "string")
|
|
2391
|
+
theme.accent = args.theme.accent;
|
|
2392
|
+
if (typeof args.theme.radiusPx === "number")
|
|
2393
|
+
theme.radiusPx = args.theme.radiusPx;
|
|
2394
|
+
if (typeof args.theme.font === "string")
|
|
2395
|
+
theme.font = args.theme.font;
|
|
2396
|
+
if (Object.keys(theme).length > 0)
|
|
2397
|
+
body.theme = theme;
|
|
2398
|
+
}
|
|
2399
|
+
if (Object.keys(body).length === 0) {
|
|
2400
|
+
throw new Error("Provide at least one of: name, slug, logo_bg, theme.{bg,primary,accent,radiusPx,font}.");
|
|
2401
|
+
}
|
|
2402
|
+
const { data } = await apiRequest(cfg, "/api/v1/workspace", { method: "PATCH", body });
|
|
2403
|
+
return data;
|
|
2404
|
+
}
|
|
2405
|
+
async function uploadWorkspaceLogo(args) {
|
|
2406
|
+
const cfg = resolveConfig();
|
|
2407
|
+
const localPath = requireString(args, "path");
|
|
2408
|
+
const { readFile, stat } = await import("node:fs/promises");
|
|
2409
|
+
const { basename, extname } = await import("node:path");
|
|
2410
|
+
const info = await stat(localPath);
|
|
2411
|
+
if (!info.isFile())
|
|
2412
|
+
throw new Error(`Not a file: ${localPath}`);
|
|
2413
|
+
const MAX = 2 * 1024 * 1024;
|
|
2414
|
+
if (info.size > MAX) {
|
|
2415
|
+
throw new Error(`Logo is ${(info.size / 1024 / 1024).toFixed(1)} MB — exceeds the 2 MB cap.`);
|
|
2416
|
+
}
|
|
2417
|
+
const filename = basename(localPath);
|
|
2418
|
+
const ext = extname(filename).toLowerCase();
|
|
2419
|
+
const contentTypes = {
|
|
2420
|
+
".png": "image/png",
|
|
2421
|
+
".jpg": "image/jpeg",
|
|
2422
|
+
".jpeg": "image/jpeg",
|
|
2423
|
+
".webp": "image/webp",
|
|
2424
|
+
};
|
|
2425
|
+
const contentType = contentTypes[ext];
|
|
2426
|
+
if (!contentType) {
|
|
2427
|
+
throw new Error(`Logo must be a PNG, JPG, or WebP image. Got ${ext || "(no extension)"}.`);
|
|
2428
|
+
}
|
|
2429
|
+
const bytes = await readFile(localPath);
|
|
2430
|
+
const form = new FormData();
|
|
2431
|
+
const blob = new Blob([new Uint8Array(bytes)], { type: contentType });
|
|
2432
|
+
form.set("file", blob, filename);
|
|
2433
|
+
const url = new URL("/api/v1/workspace/logo", cfg.apiUrl);
|
|
2434
|
+
const res = await fetch(url, {
|
|
2435
|
+
method: "POST",
|
|
2436
|
+
headers: {
|
|
2437
|
+
Authorization: `Bearer ${cfg.token}`,
|
|
2438
|
+
Accept: "application/json",
|
|
2439
|
+
"User-Agent": "atlas-mcp/0.1.0",
|
|
2440
|
+
},
|
|
2441
|
+
body: form,
|
|
2442
|
+
});
|
|
2443
|
+
const text = await res.text();
|
|
2444
|
+
let body = null;
|
|
2445
|
+
try {
|
|
2446
|
+
body = text ? JSON.parse(text) : null;
|
|
2447
|
+
}
|
|
2448
|
+
catch {
|
|
2449
|
+
body = text;
|
|
2450
|
+
}
|
|
2451
|
+
if (!res.ok) {
|
|
2452
|
+
const message = (isRecord(body) && typeof body.error === "string" && body.error) ||
|
|
2453
|
+
`HTTP ${res.status}`;
|
|
2454
|
+
throw new ApiError(res.status, body, message);
|
|
2455
|
+
}
|
|
2456
|
+
return body;
|
|
2457
|
+
}
|
|
2458
|
+
/* ─── Export (PDF) ─────────────────────────────────────────────────── */
|
|
2459
|
+
async function exportDocTool(args) {
|
|
2460
|
+
const cfg = resolveConfig();
|
|
2461
|
+
const docId = requireString(args, "doc_id");
|
|
2462
|
+
const open = args.open !== false;
|
|
2463
|
+
// Stream the NDJSON endpoint. We don't surface intermediate status
|
|
2464
|
+
// events to the model — the MCP returns a single JSON blob when done
|
|
2465
|
+
// (or throws on error). Matches the CLI's --json contract.
|
|
2466
|
+
const url = new URL(`/api/v1/docs/${docId}/export`, cfg.apiUrl);
|
|
2467
|
+
const res = await fetch(url, {
|
|
2468
|
+
method: "GET",
|
|
2469
|
+
headers: {
|
|
2470
|
+
Authorization: `Bearer ${cfg.token}`,
|
|
2471
|
+
Accept: "text/plain",
|
|
2472
|
+
"User-Agent": "atlas-mcp/0.1.0",
|
|
2473
|
+
},
|
|
2474
|
+
});
|
|
2475
|
+
if (!res.ok || !res.body) {
|
|
2476
|
+
const text = await res.text().catch(() => "");
|
|
2477
|
+
throw new ApiError(res.status, text, text || `HTTP ${res.status}`);
|
|
2478
|
+
}
|
|
2479
|
+
const reader = res.body.getReader();
|
|
2480
|
+
const decoder = new TextDecoder();
|
|
2481
|
+
let buffer = "";
|
|
2482
|
+
let done = null;
|
|
2483
|
+
while (true) {
|
|
2484
|
+
const { value, done: finished } = await reader.read();
|
|
2485
|
+
if (finished)
|
|
2486
|
+
break;
|
|
2487
|
+
buffer += decoder.decode(value, { stream: true });
|
|
2488
|
+
let nl;
|
|
2489
|
+
while ((nl = buffer.indexOf("\n")) !== -1) {
|
|
2490
|
+
const line = buffer.slice(0, nl).trim();
|
|
2491
|
+
buffer = buffer.slice(nl + 1);
|
|
2492
|
+
if (!line)
|
|
2493
|
+
continue;
|
|
2494
|
+
let ev;
|
|
2495
|
+
try {
|
|
2496
|
+
ev = JSON.parse(line);
|
|
2497
|
+
}
|
|
2498
|
+
catch {
|
|
2499
|
+
continue;
|
|
2500
|
+
}
|
|
2501
|
+
if (ev.type === "error") {
|
|
2502
|
+
throw new Error(typeof ev.message === "string" ? ev.message : "Export failed");
|
|
2503
|
+
}
|
|
2504
|
+
if (ev.type === "done") {
|
|
2505
|
+
done = ev;
|
|
2506
|
+
}
|
|
2507
|
+
}
|
|
2508
|
+
}
|
|
2509
|
+
if (buffer.trim()) {
|
|
2510
|
+
try {
|
|
2511
|
+
const ev = JSON.parse(buffer.trim());
|
|
2512
|
+
if (ev.type === "done")
|
|
2513
|
+
done = ev;
|
|
458
2514
|
}
|
|
459
|
-
|
|
2515
|
+
catch { /* ignore */ }
|
|
460
2516
|
}
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
const
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
2517
|
+
if (!done)
|
|
2518
|
+
throw new Error("Export stream ended without a `done` event");
|
|
2519
|
+
const { writeFile } = await import("node:fs/promises");
|
|
2520
|
+
const { resolve: resolvePath, isAbsolute } = await import("node:path");
|
|
2521
|
+
const outPathRaw = typeof args.out_path === "string" && args.out_path.trim()
|
|
2522
|
+
? args.out_path
|
|
2523
|
+
: done.filename;
|
|
2524
|
+
const outPath = isAbsolute(outPathRaw)
|
|
2525
|
+
? outPathRaw
|
|
2526
|
+
: resolvePath(process.cwd(), outPathRaw);
|
|
2527
|
+
const bytes = Buffer.from(done.pdfBase64, "base64");
|
|
2528
|
+
await writeFile(outPath, bytes);
|
|
2529
|
+
if (open) {
|
|
2530
|
+
const { spawn } = await import("node:child_process");
|
|
2531
|
+
try {
|
|
2532
|
+
if (process.platform === "win32") {
|
|
2533
|
+
spawn("cmd", ["/c", "start", "", outPath], { detached: true, stdio: "ignore" }).unref();
|
|
2534
|
+
}
|
|
2535
|
+
else if (process.platform === "darwin") {
|
|
2536
|
+
spawn("open", [outPath], { detached: true, stdio: "ignore" }).unref();
|
|
2537
|
+
}
|
|
2538
|
+
else {
|
|
2539
|
+
spawn("xdg-open", [outPath], { detached: true, stdio: "ignore" }).unref();
|
|
2540
|
+
}
|
|
468
2541
|
}
|
|
469
|
-
|
|
2542
|
+
catch { /* non-fatal */ }
|
|
470
2543
|
}
|
|
471
|
-
return {
|
|
472
|
-
}
|
|
473
|
-
/* ─── Files tools ──────────────────────────────────────────────────── */
|
|
474
|
-
async function listFiles(args) {
|
|
475
|
-
const cfg = resolveConfig();
|
|
476
|
-
const query = {};
|
|
477
|
-
if (typeof args.folder_id === "string")
|
|
478
|
-
query.folder_id = args.folder_id;
|
|
479
|
-
if (typeof args.limit === "number")
|
|
480
|
-
query.limit = args.limit;
|
|
481
|
-
if (typeof args.cursor === "string")
|
|
482
|
-
query.cursor = args.cursor;
|
|
483
|
-
const { data } = await apiRequest(cfg, "/api/v1/files", { query });
|
|
484
|
-
return data;
|
|
2544
|
+
return { out_path: outPath, pages: done.pages, bytes: bytes.length };
|
|
485
2545
|
}
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
const localPath = requireString(args, "local_path");
|
|
489
|
-
const alt = typeof args.alt === "string" ? args.alt : undefined;
|
|
2546
|
+
/* ─── Moodboard, palette, products, locations, gear, shots helpers ─── */
|
|
2547
|
+
async function uploadLocalImage(cfg, localPath) {
|
|
490
2548
|
const { readFile, stat } = await import("node:fs/promises");
|
|
491
|
-
const { basename, extname } = await import("node:path");
|
|
492
|
-
const
|
|
2549
|
+
const { basename, extname, isAbsolute, resolve: resolvePath } = await import("node:path");
|
|
2550
|
+
const abs = isAbsolute(localPath) ? localPath : resolvePath(process.cwd(), localPath);
|
|
2551
|
+
const info = await stat(abs);
|
|
493
2552
|
if (!info.isFile())
|
|
494
2553
|
throw new Error(`Not a file: ${localPath}`);
|
|
495
|
-
const filename = basename(
|
|
2554
|
+
const filename = basename(abs);
|
|
496
2555
|
const ext = extname(filename).toLowerCase();
|
|
497
2556
|
const contentTypes = {
|
|
498
2557
|
".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg",
|
|
499
2558
|
".gif": "image/gif", ".webp": "image/webp", ".svg": "image/svg+xml",
|
|
500
|
-
".
|
|
501
|
-
".txt": "text/plain", ".md": "text/markdown", ".json": "application/json",
|
|
502
|
-
".csv": "text/csv",
|
|
2559
|
+
".heic": "image/heic", ".heif": "image/heif", ".avif": "image/avif",
|
|
503
2560
|
};
|
|
504
2561
|
const contentType = contentTypes[ext] ?? "application/octet-stream";
|
|
505
|
-
|
|
506
|
-
const
|
|
507
|
-
method: "POST",
|
|
508
|
-
body: { filename },
|
|
509
|
-
});
|
|
510
|
-
// Step 2: PUT bytes
|
|
511
|
-
const bytes = await readFile(localPath);
|
|
2562
|
+
const { data: signRes } = await apiRequest(cfg, "/api/v1/files/upload-url", { method: "POST", body: { filename } });
|
|
2563
|
+
const bytes = await readFile(abs);
|
|
512
2564
|
const putRes = await fetch(signRes.upload_url, {
|
|
513
2565
|
method: "PUT",
|
|
514
2566
|
headers: { "Content-Type": contentType, "x-upsert": "false" },
|
|
515
2567
|
body: bytes,
|
|
516
2568
|
});
|
|
517
2569
|
if (!putRes.ok) {
|
|
518
|
-
|
|
519
|
-
throw new Error(`Storage upload failed (HTTP ${putRes.status}): ${text.slice(0, 200)}`);
|
|
2570
|
+
throw new Error(`Storage upload failed (HTTP ${putRes.status}) for ${localPath}`);
|
|
520
2571
|
}
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
2572
|
+
try {
|
|
2573
|
+
await apiRequest(cfg, "/api/v1/files", {
|
|
2574
|
+
method: "POST",
|
|
2575
|
+
body: { path: signRes.path },
|
|
2576
|
+
});
|
|
2577
|
+
}
|
|
2578
|
+
catch { /* metadata optional */ }
|
|
2579
|
+
return signRes.path;
|
|
2580
|
+
}
|
|
2581
|
+
function looksLikeStorageKey(input) {
|
|
2582
|
+
if (/^https?:\/\//.test(input))
|
|
2583
|
+
return false;
|
|
2584
|
+
if (/^[a-zA-Z]:\\/.test(input))
|
|
2585
|
+
return false; // Windows absolute
|
|
2586
|
+
if (input.startsWith("/"))
|
|
2587
|
+
return false; // POSIX absolute
|
|
2588
|
+
if (input.startsWith("./") || input.startsWith("../"))
|
|
2589
|
+
return false;
|
|
2590
|
+
if (/^[a-f0-9]{8}-[a-f0-9]{4}/i.test(input))
|
|
2591
|
+
return true;
|
|
2592
|
+
if (input.includes("/files/") || input.includes("/photos/"))
|
|
2593
|
+
return true;
|
|
2594
|
+
return false;
|
|
2595
|
+
}
|
|
2596
|
+
function randId(prefix) {
|
|
2597
|
+
return `${prefix}_${Math.random().toString(36).slice(2, 10)}`;
|
|
2598
|
+
}
|
|
2599
|
+
/** Shared get-template → mutate sections → put-template flow. */
|
|
2600
|
+
async function patchTemplateSections(cfg, docId, mutate) {
|
|
2601
|
+
const { data: current } = await apiRequest(cfg, `/api/v1/docs/${docId}/template`);
|
|
2602
|
+
const existingSections = Array.isArray(current.template_content?.sections)
|
|
2603
|
+
? current.template_content.sections
|
|
2604
|
+
: [];
|
|
2605
|
+
const { sections: nextSections, message } = mutate([...existingSections]);
|
|
2606
|
+
const nextContent = {
|
|
2607
|
+
...(current.template_content ?? { sections: [] }),
|
|
2608
|
+
sections: nextSections,
|
|
2609
|
+
};
|
|
2610
|
+
try {
|
|
2611
|
+
const { data } = await apiRequest(cfg, `/api/v1/docs/${docId}/template`, {
|
|
2612
|
+
method: "PUT",
|
|
2613
|
+
body: { template_content: nextContent, ...(message ? { message } : {}) },
|
|
2614
|
+
ifMatch: current.etag,
|
|
2615
|
+
});
|
|
2616
|
+
return { etag: data.etag, updated_at: data.updated_at };
|
|
2617
|
+
}
|
|
2618
|
+
catch (e) {
|
|
2619
|
+
if (e instanceof ApiError && e.status === 409) {
|
|
2620
|
+
throw new Error("Doc was modified since we fetched it — rerun to pick up concurrent edits.");
|
|
2621
|
+
}
|
|
2622
|
+
throw e;
|
|
2623
|
+
}
|
|
2624
|
+
}
|
|
2625
|
+
async function buildMoodboard(args) {
|
|
2626
|
+
const cfg = resolveConfig();
|
|
2627
|
+
const docId = requireString(args, "doc_id");
|
|
2628
|
+
const paths = args.image_paths;
|
|
2629
|
+
if (!Array.isArray(paths))
|
|
2630
|
+
throw new Error("`image_paths` must be an array of strings.");
|
|
2631
|
+
const rows = typeof args.rows === "number" ? Math.max(1, Math.round(args.rows)) : 3;
|
|
2632
|
+
const cols = typeof args.cols === "number" ? Math.max(1, Math.round(args.cols)) : 3;
|
|
2633
|
+
const aspect = typeof args.aspect === "string" ? args.aspect : "square";
|
|
2634
|
+
const title = typeof args.title === "string" ? args.title : "Moodboard";
|
|
2635
|
+
const spacing = typeof args.spacing === "number" ? args.spacing : 8;
|
|
2636
|
+
const radius = typeof args.radius === "number" ? args.radius : 8;
|
|
2637
|
+
const position = typeof args.position === "number" ? Math.round(args.position) : null;
|
|
2638
|
+
let uploadedCount = 0;
|
|
2639
|
+
const storageKeys = [];
|
|
2640
|
+
for (const p of paths) {
|
|
2641
|
+
if (typeof p !== "string" || !p)
|
|
2642
|
+
continue;
|
|
2643
|
+
if (looksLikeStorageKey(p)) {
|
|
2644
|
+
storageKeys.push(p);
|
|
2645
|
+
}
|
|
2646
|
+
else {
|
|
2647
|
+
const key = await uploadLocalImage(cfg, p);
|
|
2648
|
+
storageKeys.push(key);
|
|
2649
|
+
uploadedCount++;
|
|
2650
|
+
}
|
|
2651
|
+
}
|
|
2652
|
+
const total = rows * cols;
|
|
2653
|
+
const images = [];
|
|
2654
|
+
for (let i = 0; i < total; i++) {
|
|
2655
|
+
images.push(storageKeys[i] ? { url: storageKeys[i] } : null);
|
|
2656
|
+
}
|
|
2657
|
+
const sectionId = randId("sec");
|
|
2658
|
+
const result = await patchTemplateSections(cfg, docId, (sections) => {
|
|
2659
|
+
const section = {
|
|
2660
|
+
id: sectionId,
|
|
2661
|
+
type: "moodboard",
|
|
2662
|
+
data: { title, rows, cols, aspect, spacing, radius, images },
|
|
2663
|
+
};
|
|
2664
|
+
const next = [...sections];
|
|
2665
|
+
if (position !== null && Number.isFinite(position)) {
|
|
2666
|
+
next.splice(Math.max(0, Math.min(position, next.length)), 0, section);
|
|
2667
|
+
}
|
|
2668
|
+
else {
|
|
2669
|
+
next.push(section);
|
|
2670
|
+
}
|
|
2671
|
+
return { sections: next, message: "Add moodboard section" };
|
|
525
2672
|
});
|
|
526
2673
|
return {
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
2674
|
+
section_id: sectionId,
|
|
2675
|
+
uploaded_count: uploadedCount,
|
|
2676
|
+
total_slots: total,
|
|
2677
|
+
etag: result.etag,
|
|
531
2678
|
};
|
|
532
2679
|
}
|
|
533
|
-
async function
|
|
2680
|
+
async function extractPaletteTool(args) {
|
|
534
2681
|
const cfg = resolveConfig();
|
|
535
|
-
const
|
|
536
|
-
const
|
|
537
|
-
const
|
|
538
|
-
if (
|
|
539
|
-
|
|
540
|
-
|
|
2682
|
+
const docId = requireString(args, "doc_id");
|
|
2683
|
+
const sectionId = requireString(args, "moodboard_section_id");
|
|
2684
|
+
const body = {};
|
|
2685
|
+
if (typeof args.rows === "number")
|
|
2686
|
+
body.rows = args.rows;
|
|
2687
|
+
if (typeof args.cols === "number")
|
|
2688
|
+
body.cols = args.cols;
|
|
2689
|
+
const { data } = await apiRequest(cfg, `/api/v1/docs/${docId}/sections/${sectionId}/extract-palette`, { method: "POST", body });
|
|
2690
|
+
return {
|
|
2691
|
+
palette_section_id: data.palette_section.id,
|
|
2692
|
+
colours: data.extracted.map((c) => c.hex),
|
|
2693
|
+
etag: data.etag,
|
|
2694
|
+
};
|
|
541
2695
|
}
|
|
542
|
-
async function
|
|
2696
|
+
async function searchProducts(args) {
|
|
543
2697
|
const cfg = resolveConfig();
|
|
544
|
-
const
|
|
545
|
-
const
|
|
546
|
-
const { data } = await apiRequest(cfg, "/api/v1/
|
|
547
|
-
method: "POST",
|
|
548
|
-
body: { path, alt },
|
|
549
|
-
});
|
|
2698
|
+
const query = typeof args.query === "string" ? args.query : "";
|
|
2699
|
+
const limit = typeof args.limit === "number" ? args.limit : 25;
|
|
2700
|
+
const { data } = await apiRequest(cfg, "/api/v1/products/search", { query: { q: query, limit } });
|
|
550
2701
|
return data;
|
|
551
2702
|
}
|
|
552
|
-
async function
|
|
2703
|
+
async function addProductsToDoc(args) {
|
|
553
2704
|
const cfg = resolveConfig();
|
|
554
|
-
const
|
|
555
|
-
const
|
|
556
|
-
|
|
557
|
-
|
|
2705
|
+
const docId = requireString(args, "doc_id");
|
|
2706
|
+
const ids = args.shopify_product_ids;
|
|
2707
|
+
if (!Array.isArray(ids) || ids.length === 0) {
|
|
2708
|
+
throw new Error("`shopify_product_ids` must be a non-empty array of strings.");
|
|
2709
|
+
}
|
|
2710
|
+
const sectionId = typeof args.section_id === "string" ? args.section_id : null;
|
|
2711
|
+
const title = typeof args.title === "string" ? args.title : "Products";
|
|
2712
|
+
// Pull a wide window of live products and filter by id client-side.
|
|
2713
|
+
// Keeps the API surface small (no per-id endpoint yet) and the search
|
|
2714
|
+
// response already carries the full snapshot shape.
|
|
2715
|
+
const { data: searchRes } = await apiRequest(cfg, "/api/v1/products/search", { query: { q: "", limit: 100 } });
|
|
2716
|
+
const byId = new Map(searchRes.products.map((p) => [p.shopifyProductId, p]));
|
|
2717
|
+
const items = [];
|
|
2718
|
+
const missing = [];
|
|
2719
|
+
for (const id of ids) {
|
|
2720
|
+
if (typeof id !== "string")
|
|
2721
|
+
continue;
|
|
2722
|
+
const hit = byId.get(id);
|
|
2723
|
+
if (!hit) {
|
|
2724
|
+
missing.push(id);
|
|
2725
|
+
continue;
|
|
2726
|
+
}
|
|
2727
|
+
items.push({
|
|
2728
|
+
id: randId("prod"),
|
|
2729
|
+
source: "shopify",
|
|
2730
|
+
title: hit.title,
|
|
2731
|
+
...(hit.price ? { price: hit.price } : {}),
|
|
2732
|
+
...(hit.imageUrl ? { imageUrl: hit.imageUrl } : {}),
|
|
2733
|
+
handle: hit.handle,
|
|
2734
|
+
shopifyProductId: hit.shopifyProductId,
|
|
2735
|
+
vendor: hit.vendor,
|
|
2736
|
+
status: hit.status,
|
|
2737
|
+
});
|
|
2738
|
+
}
|
|
2739
|
+
if (items.length === 0) {
|
|
2740
|
+
throw new Error(`None of the requested product ids are in the first 100 live products. Missing: ${missing.join(", ")}`);
|
|
2741
|
+
}
|
|
2742
|
+
let targetId = sectionId;
|
|
2743
|
+
const res = await patchTemplateSections(cfg, docId, (sections) => {
|
|
2744
|
+
if (targetId) {
|
|
2745
|
+
const idx = sections.findIndex((s) => s.id === targetId);
|
|
2746
|
+
if (idx === -1)
|
|
2747
|
+
throw new Error(`Section '${targetId}' not found`);
|
|
2748
|
+
if (sections[idx].type !== "products") {
|
|
2749
|
+
throw new Error(`Section '${targetId}' is '${sections[idx].type}', not 'products'`);
|
|
2750
|
+
}
|
|
2751
|
+
const existing = (sections[idx].data ?? {});
|
|
2752
|
+
const prevItems = Array.isArray(existing.items) ? existing.items : [];
|
|
2753
|
+
sections[idx] = {
|
|
2754
|
+
...sections[idx],
|
|
2755
|
+
data: { ...(sections[idx].data ?? {}), items: [...prevItems, ...items] },
|
|
2756
|
+
};
|
|
2757
|
+
}
|
|
2758
|
+
else {
|
|
2759
|
+
targetId = randId("sec");
|
|
2760
|
+
sections.push({
|
|
2761
|
+
id: targetId,
|
|
2762
|
+
type: "products",
|
|
2763
|
+
data: { title, items },
|
|
2764
|
+
});
|
|
2765
|
+
}
|
|
2766
|
+
return { sections, message: "Add products to doc" };
|
|
558
2767
|
});
|
|
2768
|
+
return { section_id: targetId, added: items.length, missing, etag: res.etag };
|
|
2769
|
+
}
|
|
2770
|
+
async function resolveLocationTool(args) {
|
|
2771
|
+
const cfg = resolveConfig();
|
|
2772
|
+
const query = requireString(args, "query");
|
|
2773
|
+
const { data } = await apiRequest(cfg, "/api/v1/locations/resolve", { query: { query } });
|
|
559
2774
|
return data;
|
|
560
2775
|
}
|
|
561
|
-
|
|
562
|
-
|
|
2776
|
+
async function addLocationToDoc(args) {
|
|
2777
|
+
const cfg = resolveConfig();
|
|
2778
|
+
const docId = requireString(args, "doc_id");
|
|
2779
|
+
const address = typeof args.address === "string" ? args.address : null;
|
|
2780
|
+
const placeId = typeof args.place_id === "string" ? args.place_id : null;
|
|
2781
|
+
if (!address && !placeId) {
|
|
2782
|
+
throw new Error("Provide `address` or `place_id`.");
|
|
2783
|
+
}
|
|
2784
|
+
const nickname = typeof args.nickname === "string" ? args.nickname : null;
|
|
2785
|
+
const notes = typeof args.notes === "string" ? args.notes : null;
|
|
2786
|
+
const sectionId = typeof args.section_id === "string" ? args.section_id : null;
|
|
2787
|
+
const title = typeof args.title === "string" ? args.title : "Locations";
|
|
2788
|
+
const q = address ?? placeId;
|
|
2789
|
+
const { data: resolved } = await apiRequest(cfg, "/api/v1/locations/resolve", { query: { query: q } });
|
|
2790
|
+
let hit = resolved.results[0] ?? null;
|
|
2791
|
+
if (placeId) {
|
|
2792
|
+
hit = resolved.results.find((r) => r.place_id === placeId) ?? hit;
|
|
2793
|
+
}
|
|
2794
|
+
if (!hit)
|
|
2795
|
+
throw new Error(`No Google Places match for '${q}'`);
|
|
2796
|
+
const item = {
|
|
2797
|
+
id: randId("loc"),
|
|
2798
|
+
mode: "auto",
|
|
2799
|
+
nickname: nickname ?? hit.text,
|
|
2800
|
+
...(notes ? { notes } : {}),
|
|
2801
|
+
location: {
|
|
2802
|
+
place_id: hit.place_id,
|
|
2803
|
+
text: hit.text,
|
|
2804
|
+
lat: hit.lat,
|
|
2805
|
+
lng: hit.lng,
|
|
2806
|
+
verified: true,
|
|
2807
|
+
},
|
|
2808
|
+
};
|
|
2809
|
+
let targetId = sectionId;
|
|
2810
|
+
const res = await patchTemplateSections(cfg, docId, (sections) => {
|
|
2811
|
+
if (targetId) {
|
|
2812
|
+
const idx = sections.findIndex((s) => s.id === targetId);
|
|
2813
|
+
if (idx === -1)
|
|
2814
|
+
throw new Error(`Section '${targetId}' not found`);
|
|
2815
|
+
if (sections[idx].type !== "locations") {
|
|
2816
|
+
throw new Error(`Section '${targetId}' is '${sections[idx].type}', not 'locations'`);
|
|
2817
|
+
}
|
|
2818
|
+
const existing = (sections[idx].data ?? {});
|
|
2819
|
+
const prevItems = Array.isArray(existing.items) ? existing.items : [];
|
|
2820
|
+
sections[idx] = {
|
|
2821
|
+
...sections[idx],
|
|
2822
|
+
data: { ...(sections[idx].data ?? {}), items: [...prevItems, item] },
|
|
2823
|
+
};
|
|
2824
|
+
}
|
|
2825
|
+
else {
|
|
2826
|
+
targetId = randId("sec");
|
|
2827
|
+
sections.push({
|
|
2828
|
+
id: targetId,
|
|
2829
|
+
type: "locations",
|
|
2830
|
+
data: { title, items: [item] },
|
|
2831
|
+
});
|
|
2832
|
+
}
|
|
2833
|
+
return { sections, message: "Add location to doc" };
|
|
2834
|
+
});
|
|
2835
|
+
return { section_id: targetId, item, etag: res.etag };
|
|
2836
|
+
}
|
|
2837
|
+
async function addGearToDoc(args) {
|
|
2838
|
+
const cfg = resolveConfig();
|
|
2839
|
+
const docId = requireString(args, "doc_id");
|
|
2840
|
+
const ids = args.gear_ids;
|
|
2841
|
+
if (!Array.isArray(ids) || ids.length === 0) {
|
|
2842
|
+
throw new Error("`gear_ids` must be a non-empty array of UUIDs.");
|
|
2843
|
+
}
|
|
2844
|
+
const gearIds = ids.filter((v) => typeof v === "string" && !!v);
|
|
2845
|
+
const sectionId = typeof args.section_id === "string" ? args.section_id : null;
|
|
2846
|
+
const title = typeof args.title === "string" ? args.title : "Gear";
|
|
2847
|
+
const format = typeof args.format === "string" ? args.format : "table";
|
|
2848
|
+
let targetId = sectionId;
|
|
2849
|
+
const res = await patchTemplateSections(cfg, docId, (sections) => {
|
|
2850
|
+
if (targetId) {
|
|
2851
|
+
const idx = sections.findIndex((s) => s.id === targetId);
|
|
2852
|
+
if (idx === -1)
|
|
2853
|
+
throw new Error(`Section '${targetId}' not found`);
|
|
2854
|
+
if (sections[idx].type !== "gear") {
|
|
2855
|
+
throw new Error(`Section '${targetId}' is '${sections[idx].type}', not 'gear'`);
|
|
2856
|
+
}
|
|
2857
|
+
const existing = (sections[idx].data ?? {});
|
|
2858
|
+
const prev = Array.isArray(existing.itemIds)
|
|
2859
|
+
? existing.itemIds
|
|
2860
|
+
: [];
|
|
2861
|
+
const merged = Array.from(new Set([...prev, ...gearIds]));
|
|
2862
|
+
sections[idx] = {
|
|
2863
|
+
...sections[idx],
|
|
2864
|
+
data: { ...(sections[idx].data ?? {}), itemIds: merged },
|
|
2865
|
+
};
|
|
2866
|
+
}
|
|
2867
|
+
else {
|
|
2868
|
+
targetId = randId("sec");
|
|
2869
|
+
sections.push({
|
|
2870
|
+
id: targetId,
|
|
2871
|
+
type: "gear",
|
|
2872
|
+
data: { title, itemIds: gearIds, format },
|
|
2873
|
+
});
|
|
2874
|
+
}
|
|
2875
|
+
return { sections, message: "Add gear to doc" };
|
|
2876
|
+
});
|
|
2877
|
+
return { section_id: targetId, added: gearIds.length, etag: res.etag };
|
|
2878
|
+
}
|
|
2879
|
+
async function addShotsToDoc(args) {
|
|
2880
|
+
const cfg = resolveConfig();
|
|
2881
|
+
const docId = requireString(args, "doc_id");
|
|
2882
|
+
const ids = args.shot_ids;
|
|
2883
|
+
if (!Array.isArray(ids) || ids.length === 0) {
|
|
2884
|
+
throw new Error("`shot_ids` must be a non-empty array of UUIDs.");
|
|
2885
|
+
}
|
|
2886
|
+
const shotIds = ids.filter((v) => typeof v === "string" && !!v);
|
|
2887
|
+
const sectionId = typeof args.section_id === "string" ? args.section_id : null;
|
|
2888
|
+
const title = typeof args.title === "string" ? args.title : "Shot list";
|
|
2889
|
+
// Pull a wide shot-library window and snapshot name/kind/notes into
|
|
2890
|
+
// rows. Matches the shot-list section's row shape in doc-presets.ts.
|
|
2891
|
+
const { data } = await apiRequest(cfg, "/api/v1/shots", { query: { limit: 500 } });
|
|
2892
|
+
const byId = new Map(data.items.map((s) => [s.id, s]));
|
|
2893
|
+
const rows = [];
|
|
2894
|
+
const missing = [];
|
|
2895
|
+
for (const id of shotIds) {
|
|
2896
|
+
const hit = byId.get(id);
|
|
2897
|
+
if (!hit) {
|
|
2898
|
+
missing.push(id);
|
|
2899
|
+
continue;
|
|
2900
|
+
}
|
|
2901
|
+
rows.push({
|
|
2902
|
+
id: randId("sr"),
|
|
2903
|
+
name: hit.name,
|
|
2904
|
+
kind: hit.kind ?? "",
|
|
2905
|
+
lens: "",
|
|
2906
|
+
notes: hit.notes ?? "",
|
|
2907
|
+
});
|
|
2908
|
+
}
|
|
2909
|
+
if (rows.length === 0) {
|
|
2910
|
+
throw new Error(`None of the shot ids were found. Missing: ${missing.join(", ")}`);
|
|
2911
|
+
}
|
|
2912
|
+
let targetId = sectionId;
|
|
2913
|
+
const res = await patchTemplateSections(cfg, docId, (sections) => {
|
|
2914
|
+
if (targetId) {
|
|
2915
|
+
const idx = sections.findIndex((s) => s.id === targetId);
|
|
2916
|
+
if (idx === -1)
|
|
2917
|
+
throw new Error(`Section '${targetId}' not found`);
|
|
2918
|
+
if (sections[idx].type !== "shot-list") {
|
|
2919
|
+
throw new Error(`Section '${targetId}' is '${sections[idx].type}', not 'shot-list'`);
|
|
2920
|
+
}
|
|
2921
|
+
const existing = (sections[idx].data ?? {});
|
|
2922
|
+
const prev = Array.isArray(existing.rows) ? existing.rows : [];
|
|
2923
|
+
sections[idx] = {
|
|
2924
|
+
...sections[idx],
|
|
2925
|
+
data: { ...(sections[idx].data ?? {}), rows: [...prev, ...rows] },
|
|
2926
|
+
};
|
|
2927
|
+
}
|
|
2928
|
+
else {
|
|
2929
|
+
targetId = randId("sec");
|
|
2930
|
+
sections.push({
|
|
2931
|
+
id: targetId,
|
|
2932
|
+
type: "shot-list",
|
|
2933
|
+
data: { title, rows },
|
|
2934
|
+
});
|
|
2935
|
+
}
|
|
2936
|
+
return { sections, message: "Add shots to doc" };
|
|
2937
|
+
});
|
|
2938
|
+
return { section_id: targetId, added: rows.length, missing, etag: res.etag };
|
|
2939
|
+
}
|
|
2940
|
+
/* ─── Mindmap tools ────────────────────────────────────────────────── */
|
|
2941
|
+
async function listMindmaps_(args) {
|
|
563
2942
|
const cfg = resolveConfig();
|
|
564
2943
|
const query = {};
|
|
565
2944
|
if (typeof args.limit === "number")
|
|
566
2945
|
query.limit = args.limit;
|
|
567
2946
|
if (typeof args.cursor === "string")
|
|
568
2947
|
query.cursor = args.cursor;
|
|
569
|
-
const { data } = await apiRequest(cfg, "/api/v1/
|
|
2948
|
+
const { data } = await apiRequest(cfg, "/api/v1/mindmaps", { query });
|
|
570
2949
|
return data;
|
|
571
2950
|
}
|
|
572
|
-
async function
|
|
2951
|
+
async function getMindmap_(args) {
|
|
573
2952
|
const cfg = resolveConfig();
|
|
574
|
-
const
|
|
575
|
-
|
|
576
|
-
body.brand = args.brand;
|
|
577
|
-
if (typeof args.quantity === "number")
|
|
578
|
-
body.quantity = args.quantity;
|
|
579
|
-
if (typeof args.image_path === "string")
|
|
580
|
-
body.image_path = args.image_path;
|
|
581
|
-
const { data } = await apiRequest(cfg, "/api/v1/gear", {
|
|
582
|
-
method: "POST",
|
|
583
|
-
body,
|
|
584
|
-
});
|
|
2953
|
+
const mindmapId = requireString(args, "mindmapId");
|
|
2954
|
+
const { data } = await apiRequest(cfg, `/api/v1/mindmaps/${mindmapId}`);
|
|
585
2955
|
return data;
|
|
586
2956
|
}
|
|
587
|
-
async function
|
|
2957
|
+
async function createMindmap_(args) {
|
|
588
2958
|
const cfg = resolveConfig();
|
|
589
|
-
const
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
2959
|
+
const title = requireString(args, "title");
|
|
2960
|
+
const body = { title };
|
|
2961
|
+
if (typeof args.description === "string")
|
|
2962
|
+
body.description = args.description;
|
|
2963
|
+
if (typeof args.shoot_id === "string")
|
|
2964
|
+
body.shoot_id = args.shoot_id;
|
|
2965
|
+
if (Array.isArray(args.nodes))
|
|
2966
|
+
body.nodes = args.nodes;
|
|
2967
|
+
if (Array.isArray(args.edges))
|
|
2968
|
+
body.edges = args.edges;
|
|
2969
|
+
const { data } = await apiRequest(cfg, "/api/v1/mindmaps", {
|
|
594
2970
|
method: "POST",
|
|
595
|
-
body
|
|
2971
|
+
body,
|
|
596
2972
|
});
|
|
597
2973
|
return data;
|
|
598
2974
|
}
|
|
599
|
-
async function
|
|
2975
|
+
async function deleteMindmap_(args) {
|
|
600
2976
|
const cfg = resolveConfig();
|
|
601
|
-
const
|
|
602
|
-
const { data } = await apiRequest(cfg, `/api/v1/
|
|
2977
|
+
const mindmapId = requireString(args, "mindmapId");
|
|
2978
|
+
const { data } = await apiRequest(cfg, `/api/v1/mindmaps/${mindmapId}`, {
|
|
603
2979
|
method: "DELETE",
|
|
604
2980
|
});
|
|
605
2981
|
return data;
|
|
606
2982
|
}
|
|
607
|
-
async function
|
|
2983
|
+
async function updateMindmap_(args) {
|
|
608
2984
|
const cfg = resolveConfig();
|
|
609
|
-
const
|
|
2985
|
+
const mindmapId = requireString(args, "mindmapId");
|
|
610
2986
|
const body = {};
|
|
611
|
-
if (typeof args.
|
|
612
|
-
body.
|
|
613
|
-
if (typeof args.
|
|
614
|
-
body.
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
2987
|
+
if (typeof args.title === "string")
|
|
2988
|
+
body.title = args.title;
|
|
2989
|
+
if (args.description === null || typeof args.description === "string") {
|
|
2990
|
+
body.description = args.description;
|
|
2991
|
+
}
|
|
2992
|
+
if (args.shoot_id === null || typeof args.shoot_id === "string") {
|
|
2993
|
+
body.shoot_id = args.shoot_id;
|
|
2994
|
+
}
|
|
619
2995
|
if (Object.keys(body).length === 0) {
|
|
620
|
-
throw new Error("
|
|
2996
|
+
throw new Error("Provide at least one of title, description, shoot_id.");
|
|
621
2997
|
}
|
|
622
|
-
const { data } = await apiRequest(cfg, `/api/v1/
|
|
623
|
-
method: "
|
|
2998
|
+
const { data } = await apiRequest(cfg, `/api/v1/mindmaps/${mindmapId}`, {
|
|
2999
|
+
method: "PATCH",
|
|
624
3000
|
body,
|
|
625
3001
|
});
|
|
626
3002
|
return data;
|
|
627
3003
|
}
|
|
628
|
-
|
|
629
|
-
async function renameFile(args) {
|
|
3004
|
+
async function addMindmapNode_(args) {
|
|
630
3005
|
const cfg = resolveConfig();
|
|
631
|
-
const
|
|
632
|
-
const
|
|
633
|
-
const
|
|
3006
|
+
const mindmapId = requireString(args, "mindmapId");
|
|
3007
|
+
const body = {};
|
|
3008
|
+
for (const key of [
|
|
3009
|
+
"label",
|
|
3010
|
+
"title",
|
|
3011
|
+
"description",
|
|
3012
|
+
"shape",
|
|
3013
|
+
"kind",
|
|
3014
|
+
"colour_key",
|
|
3015
|
+
"image_url",
|
|
3016
|
+
"image_alt",
|
|
3017
|
+
"text_color",
|
|
3018
|
+
"bg_color",
|
|
3019
|
+
]) {
|
|
3020
|
+
if (typeof args[key] === "string")
|
|
3021
|
+
body[key] = args[key];
|
|
3022
|
+
}
|
|
3023
|
+
for (const key of ["x", "y", "width", "height", "font_size", "order_index"]) {
|
|
3024
|
+
if (typeof args[key] === "number")
|
|
3025
|
+
body[key] = args[key];
|
|
3026
|
+
}
|
|
3027
|
+
if (typeof args.completed === "boolean")
|
|
3028
|
+
body.completed = args.completed;
|
|
3029
|
+
if (!body.label && !body.title) {
|
|
3030
|
+
throw new Error("`label` or `title` is required.");
|
|
3031
|
+
}
|
|
3032
|
+
const { data } = await apiRequest(cfg, `/api/v1/mindmaps/${mindmapId}/nodes`, {
|
|
634
3033
|
method: "POST",
|
|
635
|
-
body
|
|
3034
|
+
body,
|
|
636
3035
|
});
|
|
637
3036
|
return data;
|
|
638
3037
|
}
|
|
639
|
-
|
|
640
|
-
|
|
3038
|
+
async function updateMindmapNode_(args) {
|
|
3039
|
+
const cfg = resolveConfig();
|
|
3040
|
+
const mindmapId = requireString(args, "mindmapId");
|
|
3041
|
+
const nodeId = requireString(args, "nodeId");
|
|
3042
|
+
const body = {};
|
|
3043
|
+
for (const key of [
|
|
3044
|
+
"label",
|
|
3045
|
+
"title",
|
|
3046
|
+
"description",
|
|
3047
|
+
"shape",
|
|
3048
|
+
"kind",
|
|
3049
|
+
"colour_key",
|
|
3050
|
+
"image_url",
|
|
3051
|
+
"image_alt",
|
|
3052
|
+
"text_color",
|
|
3053
|
+
"bg_color",
|
|
3054
|
+
]) {
|
|
3055
|
+
if (typeof args[key] === "string")
|
|
3056
|
+
body[key] = args[key];
|
|
3057
|
+
}
|
|
3058
|
+
for (const key of ["x", "y", "width", "height", "font_size", "order_index"]) {
|
|
3059
|
+
if (typeof args[key] === "number")
|
|
3060
|
+
body[key] = args[key];
|
|
3061
|
+
}
|
|
3062
|
+
if (typeof args.completed === "boolean")
|
|
3063
|
+
body.completed = args.completed;
|
|
3064
|
+
if (Object.keys(body).length === 0) {
|
|
3065
|
+
throw new Error("Provide at least one field to patch.");
|
|
3066
|
+
}
|
|
3067
|
+
const { data } = await apiRequest(cfg, `/api/v1/mindmaps/${mindmapId}/nodes/${nodeId}`, { method: "PATCH", body });
|
|
3068
|
+
return data;
|
|
3069
|
+
}
|
|
3070
|
+
async function deleteMindmapNode_(args) {
|
|
3071
|
+
const cfg = resolveConfig();
|
|
3072
|
+
const mindmapId = requireString(args, "mindmapId");
|
|
3073
|
+
const nodeId = requireString(args, "nodeId");
|
|
3074
|
+
const { data } = await apiRequest(cfg, `/api/v1/mindmaps/${mindmapId}/nodes/${nodeId}`, { method: "DELETE" });
|
|
3075
|
+
return data;
|
|
3076
|
+
}
|
|
3077
|
+
async function addMindmapEdge_(args) {
|
|
3078
|
+
const cfg = resolveConfig();
|
|
3079
|
+
const mindmapId = requireString(args, "mindmapId");
|
|
3080
|
+
const from_node_id = requireString(args, "from_node_id");
|
|
3081
|
+
const to_node_id = requireString(args, "to_node_id");
|
|
3082
|
+
const body = { from_node_id, to_node_id };
|
|
3083
|
+
if (typeof args.label === "string")
|
|
3084
|
+
body.label = args.label;
|
|
3085
|
+
const { data } = await apiRequest(cfg, `/api/v1/mindmaps/${mindmapId}/edges`, { method: "POST", body });
|
|
3086
|
+
return data;
|
|
3087
|
+
}
|
|
3088
|
+
async function deleteMindmapEdge_(args) {
|
|
3089
|
+
const cfg = resolveConfig();
|
|
3090
|
+
const mindmapId = requireString(args, "mindmapId");
|
|
3091
|
+
const edgeId = requireString(args, "edgeId");
|
|
3092
|
+
const { data } = await apiRequest(cfg, `/api/v1/mindmaps/${mindmapId}/edges/${edgeId}`, { method: "DELETE" });
|
|
3093
|
+
return data;
|
|
3094
|
+
}
|
|
3095
|
+
async function createMindmapGroup_(args) {
|
|
3096
|
+
const cfg = resolveConfig();
|
|
3097
|
+
const mindmapId = requireString(args, "mindmapId");
|
|
3098
|
+
const nodes = Array.isArray(args.nodes)
|
|
3099
|
+
? args.nodes.filter((v) => typeof v === "string" && !!v)
|
|
3100
|
+
: [];
|
|
3101
|
+
if (nodes.length === 0) {
|
|
3102
|
+
throw new Error("`nodes` must be a non-empty array of node ids.");
|
|
3103
|
+
}
|
|
3104
|
+
const edges = Array.isArray(args.edges)
|
|
3105
|
+
? args.edges.filter((v) => typeof v === "string" && !!v)
|
|
3106
|
+
: [];
|
|
3107
|
+
const body = { nodes };
|
|
3108
|
+
if (edges.length > 0)
|
|
3109
|
+
body.edges = edges;
|
|
3110
|
+
if (typeof args.name === "string")
|
|
3111
|
+
body.name = args.name;
|
|
3112
|
+
else if (typeof args.title === "string")
|
|
3113
|
+
body.name = args.title;
|
|
3114
|
+
if (typeof args.bg_color === "string")
|
|
3115
|
+
body.bg_color = args.bg_color;
|
|
3116
|
+
if (typeof args.outline_color === "string")
|
|
3117
|
+
body.outline_color = args.outline_color;
|
|
3118
|
+
if (typeof args.outline_width === "number")
|
|
3119
|
+
body.outline_width = args.outline_width;
|
|
3120
|
+
if (typeof args.corner_radius === "number")
|
|
3121
|
+
body.corner_radius = args.corner_radius;
|
|
3122
|
+
if (typeof args.padding === "number")
|
|
3123
|
+
body.padding = args.padding;
|
|
3124
|
+
if (typeof args.custom_css === "string")
|
|
3125
|
+
body.custom_css = args.custom_css;
|
|
3126
|
+
const { data } = await apiRequest(cfg, `/api/v1/mindmaps/${mindmapId}/groups`, { method: "POST", body });
|
|
3127
|
+
return data;
|
|
3128
|
+
}
|
|
3129
|
+
/* ─── Activity log ─────────────────────────────────────────────────── */
|
|
3130
|
+
async function listActivity_(args) {
|
|
641
3131
|
const cfg = resolveConfig();
|
|
642
|
-
const docId = requireString(args, "docId");
|
|
643
3132
|
const query = {};
|
|
644
3133
|
if (typeof args.limit === "number")
|
|
645
3134
|
query.limit = args.limit;
|
|
646
|
-
|
|
3135
|
+
if (typeof args.cursor === "string")
|
|
3136
|
+
query.cursor = args.cursor;
|
|
3137
|
+
if (typeof args.since === "string")
|
|
3138
|
+
query.since = args.since;
|
|
3139
|
+
if (typeof args.until === "string")
|
|
3140
|
+
query.until = args.until;
|
|
3141
|
+
if (typeof args.user === "string")
|
|
3142
|
+
query.user = args.user;
|
|
3143
|
+
if (typeof args.source === "string")
|
|
3144
|
+
query.source = args.source;
|
|
3145
|
+
if (typeof args.action === "string")
|
|
3146
|
+
query.action = args.action;
|
|
3147
|
+
if (typeof args.resource_type === "string")
|
|
3148
|
+
query.resource_type = args.resource_type;
|
|
3149
|
+
const { data } = await apiRequest(cfg, "/api/v1/activity", { query });
|
|
647
3150
|
return data;
|
|
648
3151
|
}
|
|
649
|
-
async function
|
|
3152
|
+
async function getActivity_(args) {
|
|
650
3153
|
const cfg = resolveConfig();
|
|
651
|
-
const
|
|
652
|
-
const
|
|
653
|
-
|
|
3154
|
+
const entryId = requireString(args, "entryId");
|
|
3155
|
+
const { data } = await apiRequest(cfg, `/api/v1/activity/${entryId}`);
|
|
3156
|
+
return data;
|
|
3157
|
+
}
|
|
3158
|
+
/* ─── Mindmap export (PDF / PNG / SVG) ───────────────────────────── */
|
|
3159
|
+
async function exportMindmapTool(args) {
|
|
3160
|
+
const cfg = resolveConfig();
|
|
3161
|
+
const mindmapId = requireString(args, "mindmap_id");
|
|
3162
|
+
const formatRaw = typeof args.format === "string" ? args.format.toLowerCase() : "pdf";
|
|
3163
|
+
const format = (formatRaw === "svg" || formatRaw === "png" || formatRaw === "pdf" ? formatRaw : "pdf");
|
|
3164
|
+
const openFile = args.open !== false;
|
|
3165
|
+
const dpi = Math.max(1, Math.min(3, Number(args.dpi ?? 2)));
|
|
3166
|
+
const w = Math.max(320, Math.min(7680, Number(args.w ?? 1920)));
|
|
3167
|
+
const h = Math.max(240, Math.min(7680, Number(args.h ?? 1080)));
|
|
3168
|
+
// Shared out-path resolver.
|
|
3169
|
+
const { resolve: resolvePath, isAbsolute } = await import("node:path");
|
|
3170
|
+
const { writeFile } = await import("node:fs/promises");
|
|
3171
|
+
if (format === "svg") {
|
|
3172
|
+
const url = new URL(`/api/v1/mindmaps/${mindmapId}/export`, cfg.apiUrl);
|
|
3173
|
+
url.searchParams.set("format", "svg");
|
|
3174
|
+
const res = await fetch(url, {
|
|
3175
|
+
headers: {
|
|
3176
|
+
Authorization: `Bearer ${cfg.token}`,
|
|
3177
|
+
Accept: "image/svg+xml",
|
|
3178
|
+
"User-Agent": "atlas-mcp/0.1.0",
|
|
3179
|
+
},
|
|
3180
|
+
});
|
|
3181
|
+
if (!res.ok) {
|
|
3182
|
+
const text = await res.text().catch(() => "");
|
|
3183
|
+
throw new Error(`SVG export failed (${res.status}): ${text}`);
|
|
3184
|
+
}
|
|
3185
|
+
const bytes = Buffer.from(await res.arrayBuffer());
|
|
3186
|
+
const stub = `mindmap-${mindmapId.slice(0, 8)}`;
|
|
3187
|
+
const outPathRaw = typeof args.out_path === "string" && args.out_path.trim()
|
|
3188
|
+
? args.out_path
|
|
3189
|
+
: `${stub}.svg`;
|
|
3190
|
+
const outPath = isAbsolute(outPathRaw) ? outPathRaw : resolvePath(process.cwd(), outPathRaw);
|
|
3191
|
+
await writeFile(outPath, bytes);
|
|
3192
|
+
if (openFile)
|
|
3193
|
+
openWithOs(outPath);
|
|
3194
|
+
return { out_path: outPath, format: "svg", bytes: bytes.length };
|
|
3195
|
+
}
|
|
3196
|
+
// PDF / PNG — mint a token, drive Playwright.
|
|
3197
|
+
const tokenUrl = new URL(`/api/v1/mindmaps/${mindmapId}/export-token`, cfg.apiUrl);
|
|
3198
|
+
const tokRes = await fetch(tokenUrl, {
|
|
654
3199
|
method: "POST",
|
|
3200
|
+
headers: {
|
|
3201
|
+
Authorization: `Bearer ${cfg.token}`,
|
|
3202
|
+
Accept: "application/json",
|
|
3203
|
+
"User-Agent": "atlas-mcp/0.1.0",
|
|
3204
|
+
},
|
|
655
3205
|
});
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
};
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
3206
|
+
if (!tokRes.ok) {
|
|
3207
|
+
const text = await tokRes.text().catch(() => "");
|
|
3208
|
+
throw new Error(`Export-token request failed (${tokRes.status}): ${text}`);
|
|
3209
|
+
}
|
|
3210
|
+
const tok = (await tokRes.json());
|
|
3211
|
+
const { chromium } = await import("playwright-core");
|
|
3212
|
+
const browser = await chromium.launch({ headless: true });
|
|
3213
|
+
let outBuffer;
|
|
3214
|
+
try {
|
|
3215
|
+
const context = await browser.newContext({
|
|
3216
|
+
viewport: { width: w, height: h },
|
|
3217
|
+
deviceScaleFactor: dpi,
|
|
3218
|
+
locale: "en-AU",
|
|
3219
|
+
});
|
|
3220
|
+
await context.addCookies([
|
|
3221
|
+
{
|
|
3222
|
+
name: tok.cookieName,
|
|
3223
|
+
value: tok.token,
|
|
3224
|
+
url: new URL(tok.mindmapUrl).origin,
|
|
3225
|
+
httpOnly: true,
|
|
3226
|
+
secure: tok.mindmapUrl.startsWith("https:"),
|
|
3227
|
+
sameSite: "Lax",
|
|
3228
|
+
},
|
|
3229
|
+
]);
|
|
3230
|
+
const page = await context.newPage();
|
|
3231
|
+
const url = new URL(tok.mindmapUrl);
|
|
3232
|
+
url.searchParams.set("w", String(w));
|
|
3233
|
+
url.searchParams.set("h", String(h));
|
|
3234
|
+
await page.goto(url.toString(), { waitUntil: "domcontentloaded", timeout: 60_000 });
|
|
3235
|
+
await page.waitForSelector(".mindmap-export-ready", { timeout: 30_000 });
|
|
3236
|
+
if (format === "pdf") {
|
|
3237
|
+
outBuffer = Buffer.from(await page.pdf({
|
|
3238
|
+
width: `${w}px`,
|
|
3239
|
+
height: `${h}px`,
|
|
3240
|
+
printBackground: true,
|
|
3241
|
+
margin: { top: "0mm", right: "0mm", bottom: "0mm", left: "0mm" },
|
|
3242
|
+
}));
|
|
3243
|
+
}
|
|
3244
|
+
else {
|
|
3245
|
+
outBuffer = Buffer.from(await page.screenshot({
|
|
3246
|
+
type: "png",
|
|
3247
|
+
clip: { x: 0, y: 0, width: w, height: h },
|
|
3248
|
+
omitBackground: false,
|
|
3249
|
+
}));
|
|
3250
|
+
}
|
|
3251
|
+
}
|
|
3252
|
+
finally {
|
|
3253
|
+
try {
|
|
3254
|
+
await browser.close();
|
|
3255
|
+
}
|
|
3256
|
+
catch {
|
|
3257
|
+
/* non-fatal */
|
|
3258
|
+
}
|
|
3259
|
+
}
|
|
3260
|
+
const safeTitle = (tok.title || "mindmap")
|
|
3261
|
+
.replace(/[^\w\s.-]+/g, "")
|
|
3262
|
+
.replace(/\s+/g, "-")
|
|
3263
|
+
.slice(0, 60) || "mindmap";
|
|
3264
|
+
const outPathRaw = typeof args.out_path === "string" && args.out_path.trim()
|
|
3265
|
+
? args.out_path
|
|
3266
|
+
: `${safeTitle}.${format}`;
|
|
3267
|
+
const outPath = isAbsolute(outPathRaw) ? outPathRaw : resolvePath(process.cwd(), outPathRaw);
|
|
3268
|
+
await writeFile(outPath, outBuffer);
|
|
3269
|
+
if (openFile)
|
|
3270
|
+
openWithOs(outPath);
|
|
3271
|
+
return { out_path: outPath, format, bytes: outBuffer.length };
|
|
670
3272
|
}
|
|
671
|
-
function
|
|
672
|
-
|
|
3273
|
+
function openWithOs(path) {
|
|
3274
|
+
// Non-blocking OS "open" — ignores failures (file is already on disk).
|
|
3275
|
+
import("node:child_process").then(({ spawn }) => {
|
|
3276
|
+
try {
|
|
3277
|
+
if (process.platform === "win32") {
|
|
3278
|
+
spawn("cmd", ["/c", "start", "", path], { detached: true, stdio: "ignore" }).unref();
|
|
3279
|
+
}
|
|
3280
|
+
else if (process.platform === "darwin") {
|
|
3281
|
+
spawn("open", [path], { detached: true, stdio: "ignore" }).unref();
|
|
3282
|
+
}
|
|
3283
|
+
else {
|
|
3284
|
+
spawn("xdg-open", [path], { detached: true, stdio: "ignore" }).unref();
|
|
3285
|
+
}
|
|
3286
|
+
}
|
|
3287
|
+
catch {
|
|
3288
|
+
/* non-fatal */
|
|
3289
|
+
}
|
|
3290
|
+
});
|
|
673
3291
|
}
|
|
674
3292
|
/* ─── Boot ─────────────────────────────────────────────────────────── */
|
|
675
3293
|
async function main() {
|