@kezlahd/atlas-mcp 0.2.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api.js +1 -1
- package/dist/index.js +535 -86
- package/dist/index.js.map +1 -1
- package/dist/section-catalog.js +25 -14
- package/dist/section-catalog.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -48,7 +48,7 @@ const TOOLS = [
|
|
|
48
48
|
},
|
|
49
49
|
{
|
|
50
50
|
name: "atlas_doc_create",
|
|
51
|
-
description: "Create a new doc
|
|
51
|
+
description: "Create a new doc. For a STRUCTURED doc (drops / shoot plans / campaigns / lookbooks / any of the catalogue in atlas_list_doc_presets), prefer atlas_create_doc_from_preset — pass the brief as the cover subtitle and then read the preset's `agent_guidance` for the next-step fill order. For a BESPOKE one-off doc, use this tool and build sections manually: template_content: { cover: { subtitle }, sections: [...] }; a cover is strongly recommended as the first element. Returns { id, etag, ... }; pipe into atlas_update_section / atlas_build_moodboard / atlas_add_products_to_doc / atlas_add_section / atlas_update_cover etc. to fill sections one at a time (safer than another full-template PUT, which fights the web editor's autosave).",
|
|
52
52
|
inputSchema: {
|
|
53
53
|
type: "object",
|
|
54
54
|
properties: {
|
|
@@ -80,7 +80,7 @@ const TOOLS = [
|
|
|
80
80
|
},
|
|
81
81
|
{
|
|
82
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
|
|
83
|
+
description: "List every doc preset available in Atlas (shoot-day, deliverable, planning, narrative-first templates). Returns the full catalogue with each preset's id, label, blurb, category, estimated fill time, default title, ordered section list, and (where present) an `agent_guidance` runbook explaining how to fill the skeleton. Design principle to carry across every doc: docs follow a NARRATIVE — start with cover + story, then supporting evidence (moodboards, palettes), then concrete deliverables (products, shots, dates, people), then logistics (notes, call sheets). Pick the preset whose section order already matches that arc rather than reshuffling after creation.",
|
|
84
84
|
inputSchema: {
|
|
85
85
|
type: "object",
|
|
86
86
|
properties: {},
|
|
@@ -88,7 +88,7 @@ const TOOLS = [
|
|
|
88
88
|
},
|
|
89
89
|
{
|
|
90
90
|
name: "atlas_create_doc_from_preset",
|
|
91
|
-
description: "
|
|
91
|
+
description: "Create a new doc from a preset. Choose a preset based on the doc's purpose (list via atlas_list_doc_presets — each entry includes a one-line blurb). After creation, the doc has EMPTY sections in the right narrative order; fill each with the appropriate tool: atlas_update_cover for the cover subtitle + date, atlas_build_moodboard for moodboard images, atlas_extract_palette for the palette (points at a moodboard section), atlas_add_products_to_doc for product rows, atlas_add_shots_to_doc for the shot list, atlas_add_calendar_event_to_doc for dates, atlas_update_section for text/notes/table bodies. Each preset's `agent_guidance` field (fetched via atlas_list_doc_presets) tells you how to fill it — read it before filling. Fill the cover subtitle + section copy from the conversation context you already have (brand voice, brief, references); don't ask the user for anything already in the conversation. Title defaults to the preset's defaultTitle; override to taste. Returns the created doc record (same shape as atlas_doc_create).",
|
|
92
92
|
inputSchema: {
|
|
93
93
|
type: "object",
|
|
94
94
|
properties: {
|
|
@@ -173,7 +173,7 @@ const TOOLS = [
|
|
|
173
173
|
},
|
|
174
174
|
{
|
|
175
175
|
name: "atlas_update_section",
|
|
176
|
-
description: "Edit
|
|
176
|
+
description: "Edit ONE existing section by id — this tool cannot create sections. Use atlas_add_section, atlas_update_template, or a dedicated add-tool (atlas_build_moodboard, atlas_add_products_to_doc, …) to create. Fields: `data` REPLACES the section's data object (per-key merge is ambiguous for arrays); `style` merges over existing style keys; `hidden: true/false` toggles visibility (hidden sections stay in the doc but are omitted from the printed output — use this instead of deleting a section the user might want back). On concurrent edit the tool auto-retries once (re-GET → re-apply → re-PATCH); if the second attempt also races, bubble the error so the user can decide whether to force.",
|
|
177
177
|
inputSchema: {
|
|
178
178
|
type: "object",
|
|
179
179
|
properties: {
|
|
@@ -213,7 +213,7 @@ const TOOLS = [
|
|
|
213
213
|
},
|
|
214
214
|
{
|
|
215
215
|
name: "atlas_describe_section_type",
|
|
216
|
-
description: "Look up
|
|
216
|
+
description: "Look up a section type's shape BEFORE calling atlas_update_section / atlas_add_section. Returns { type, purpose, data_shape, common_fields, hidden_supported: true }. Every section accepts a top-level `hidden: boolean`. Image/asset fields (moodboard images[].url, cover bgImageUrl, cover presentedBy.logoUrl, people-link photoUrl) require a SIGNED URL, not a storage key — pass storage keys through atlas_get_file_url first. Call with no `type` for the full catalogue of known types.",
|
|
217
217
|
inputSchema: {
|
|
218
218
|
type: "object",
|
|
219
219
|
properties: {
|
|
@@ -241,7 +241,7 @@ const TOOLS = [
|
|
|
241
241
|
},
|
|
242
242
|
{
|
|
243
243
|
name: "atlas_upload_file",
|
|
244
|
-
description: "Upload a local file (path on the user's machine) to the workspace bucket. Three-step signed-URL flow. Returns the storage path
|
|
244
|
+
description: "Upload a local file (path on the user's machine) to the workspace bucket. Three-step signed-URL flow. Returns the storage path AND a long-lived (1-year) signed URL — drop the signed URL straight into section fields (moodboard images[].url, cover bgImageUrl, people-link photoUrl, …) that need a URL rather than a storage key. Extension whitelist enforced (images/video/audio/pdf/txt/md/json/csv); executables/archives refused.",
|
|
245
245
|
inputSchema: {
|
|
246
246
|
type: "object",
|
|
247
247
|
properties: {
|
|
@@ -967,7 +967,7 @@ const TOOLS = [
|
|
|
967
967
|
},
|
|
968
968
|
{
|
|
969
969
|
name: "atlas_build_moodboard",
|
|
970
|
-
description: "
|
|
970
|
+
description: "Build a moodboard section wired to real images. Uploads any local paths through the signed-URL flow, re-uses already-uploaded storage keys, SIGNS every storage key into a 1-year signed URL before storing it on the section (raw storage keys render as 404s), and appends (or inserts at `position`) a moodboard section with the shape { title, rows, cols, aspect, spacing, radius, images: [{ id, alt?, url (signed) } | null] }. images.length === rows*cols; empty slots are null. Default 3×3 square. 20 MB per-file limit. For colour extraction, follow with atlas_extract_palette.",
|
|
971
971
|
inputSchema: {
|
|
972
972
|
type: "object",
|
|
973
973
|
properties: {
|
|
@@ -997,7 +997,7 @@ const TOOLS = [
|
|
|
997
997
|
},
|
|
998
998
|
{
|
|
999
999
|
name: "atlas_extract_palette",
|
|
1000
|
-
description: "
|
|
1000
|
+
description: "Append a palette section wired to a moodboard. The moodboard must already contain at least one image (storage keys are re-signed server-side for extraction). Writes { title, sourceId, rows, cols, numColors: rows*cols, extractedColours: [{ hex, weight }] } and inserts the palette right below the source moodboard. Returns { palette_section_id, colours }. If called twice on the same moodboard the existing palette section is patched in place.",
|
|
1001
1001
|
inputSchema: {
|
|
1002
1002
|
type: "object",
|
|
1003
1003
|
properties: {
|
|
@@ -1116,6 +1116,96 @@ const TOOLS = [
|
|
|
1116
1116
|
required: ["doc_id", "shot_ids"],
|
|
1117
1117
|
},
|
|
1118
1118
|
},
|
|
1119
|
+
{
|
|
1120
|
+
name: "atlas_add_calendar_event_to_doc",
|
|
1121
|
+
description: "Schedule a shoot AND wire its id into a calendar-event section in one call. Creates the event via /api/v1/events, then either appends the new event id to section_id's `eventIds` (must be a calendar-event section) or creates a new calendar-event section with it. Use when a doc needs to reference a shoot that doesn't exist yet — calendar-event sections only embed already-scheduled shoots, atlas_create_event on its own won't surface in the doc.",
|
|
1122
|
+
inputSchema: {
|
|
1123
|
+
type: "object",
|
|
1124
|
+
properties: {
|
|
1125
|
+
doc_id: { type: "string" },
|
|
1126
|
+
section_id: {
|
|
1127
|
+
type: "string",
|
|
1128
|
+
description: "Append to this existing calendar-event section. Omit to create a new section at the end of the doc.",
|
|
1129
|
+
},
|
|
1130
|
+
title: {
|
|
1131
|
+
type: "string",
|
|
1132
|
+
description: "Section title when creating a new section (default 'Schedule').",
|
|
1133
|
+
},
|
|
1134
|
+
layout: {
|
|
1135
|
+
type: "string",
|
|
1136
|
+
enum: ["card", "table", "banner"],
|
|
1137
|
+
description: "Section layout when creating. Default 'card'.",
|
|
1138
|
+
},
|
|
1139
|
+
event: {
|
|
1140
|
+
type: "object",
|
|
1141
|
+
description: "Shoot payload — same shape as atlas_create_event. `title` + `starts_at` required.",
|
|
1142
|
+
properties: {
|
|
1143
|
+
title: { type: "string" },
|
|
1144
|
+
starts_at: { type: "string", description: "ISO 8601 start." },
|
|
1145
|
+
ends_at: { type: "string", description: "ISO 8601 end." },
|
|
1146
|
+
all_day: { type: "boolean" },
|
|
1147
|
+
location: { type: "string" },
|
|
1148
|
+
shoot_type: { type: "string" },
|
|
1149
|
+
colour_key: {
|
|
1150
|
+
type: "string",
|
|
1151
|
+
enum: ["rose", "amber", "sky", "violet", "lime", "stone"],
|
|
1152
|
+
},
|
|
1153
|
+
colour_hex: { type: "string" },
|
|
1154
|
+
status: {
|
|
1155
|
+
type: "string",
|
|
1156
|
+
enum: ["planned", "confirmed", "wrapped", "cancelled"],
|
|
1157
|
+
},
|
|
1158
|
+
notes: { type: "string" },
|
|
1159
|
+
description: { type: "string" },
|
|
1160
|
+
},
|
|
1161
|
+
required: ["title", "starts_at"],
|
|
1162
|
+
},
|
|
1163
|
+
},
|
|
1164
|
+
required: ["doc_id", "event"],
|
|
1165
|
+
},
|
|
1166
|
+
},
|
|
1167
|
+
{
|
|
1168
|
+
name: "atlas_add_section",
|
|
1169
|
+
description: "Generic 'create section' for any type that doesn't have a dedicated add-tool (text, notes, table, code-block, calendar-event, people-link, toc, …). Uses the scoped append endpoint — doesn't fight the web editor's autosave the way a full-template PUT does. Call atlas_describe_section_type first to learn the data shape for your target type.",
|
|
1170
|
+
inputSchema: {
|
|
1171
|
+
type: "object",
|
|
1172
|
+
properties: {
|
|
1173
|
+
doc_id: { type: "string" },
|
|
1174
|
+
type: {
|
|
1175
|
+
type: "string",
|
|
1176
|
+
description: "Section type — e.g. 'text', 'notes', 'table', 'code-block', 'calendar-event', 'people-link', 'toc'. See atlas_describe_section_type for the full list.",
|
|
1177
|
+
},
|
|
1178
|
+
data: {
|
|
1179
|
+
type: "object",
|
|
1180
|
+
description: "section.data payload matching the type's shape. Use atlas_describe_section_type for field names.",
|
|
1181
|
+
},
|
|
1182
|
+
position: {
|
|
1183
|
+
type: "number",
|
|
1184
|
+
description: "Insert at this section index. Omit to append.",
|
|
1185
|
+
},
|
|
1186
|
+
hidden: {
|
|
1187
|
+
type: "boolean",
|
|
1188
|
+
description: "Mark the new section hidden (dimmed in editor, omitted from printed output). Default false.",
|
|
1189
|
+
},
|
|
1190
|
+
},
|
|
1191
|
+
required: ["doc_id", "type", "data"],
|
|
1192
|
+
},
|
|
1193
|
+
},
|
|
1194
|
+
{
|
|
1195
|
+
name: "atlas_update_cover",
|
|
1196
|
+
description: "Scoped merge-update of the doc cover (title, subtitle, bg image, presentedBy lockup, …). Cover lives on template_content.cover — not in sections[] — so atlas_update_section can't touch it. The server shallow-merges the fields you send, so passing `{ cover: { subtitle: 'x' } }` just updates subtitle. Image fields (bgImageUrl, presentedBy.logoUrl) require a SIGNED URL — pass storage keys through atlas_get_file_url first.",
|
|
1197
|
+
inputSchema: {
|
|
1198
|
+
type: "object",
|
|
1199
|
+
properties: {
|
|
1200
|
+
doc_id: { type: "string" },
|
|
1201
|
+
cover: {
|
|
1202
|
+
type: "object",
|
|
1203
|
+
description: "Partial cover payload — any subset of { subtitle, date, bgImageUrl, titlePosition, accentColor, presentedBy, ... }.",
|
|
1204
|
+
},
|
|
1205
|
+
},
|
|
1206
|
+
required: ["doc_id", "cover"],
|
|
1207
|
+
},
|
|
1208
|
+
},
|
|
1119
1209
|
{
|
|
1120
1210
|
name: "atlas_upload_workspace_logo",
|
|
1121
1211
|
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`).",
|
|
@@ -1404,6 +1494,62 @@ const TOOLS = [
|
|
|
1404
1494
|
required: ["entryId"],
|
|
1405
1495
|
},
|
|
1406
1496
|
},
|
|
1497
|
+
{
|
|
1498
|
+
name: "atlas_list_pending_bugs",
|
|
1499
|
+
description: "List pending (or otherwise unresolved) MCP bug reports in the current Atlas workspace. Requires bugs:read. Call this BEFORE atlas_report_bug to avoid filing duplicates — if a similar bug already exists in pending/triaged/in_progress state, DON'T file a new one. Returns { items, next_cursor } with each item carrying title, description, mcp_tool, severity, status, created_at. If a similar bug exists, note it in your reply to the user rather than filing again (there is no comment surface yet — just skip).",
|
|
1500
|
+
inputSchema: {
|
|
1501
|
+
type: "object",
|
|
1502
|
+
properties: {
|
|
1503
|
+
mcp_tool: {
|
|
1504
|
+
type: "string",
|
|
1505
|
+
description: "Optional — filter to bugs filed against this exact MCP tool name (e.g. 'atlas_build_moodboard').",
|
|
1506
|
+
},
|
|
1507
|
+
status: {
|
|
1508
|
+
type: "string",
|
|
1509
|
+
description: "Optional CSV filter. Defaults to 'pending,triaged,in_progress' (unresolved). Pass 'fixed,closed,duplicate' to see resolved bugs.",
|
|
1510
|
+
},
|
|
1511
|
+
limit: {
|
|
1512
|
+
type: "number",
|
|
1513
|
+
description: "Max rows to return (default 50, max 500).",
|
|
1514
|
+
},
|
|
1515
|
+
cursor: {
|
|
1516
|
+
type: "string",
|
|
1517
|
+
description: "Opaque next_cursor from a previous response. Omit for the first page.",
|
|
1518
|
+
},
|
|
1519
|
+
},
|
|
1520
|
+
},
|
|
1521
|
+
},
|
|
1522
|
+
{
|
|
1523
|
+
name: "atlas_report_bug",
|
|
1524
|
+
description: "File a bug report against the Atlas MCP. Call this whenever an MCP tool returns an unexpected error, a false success, or silently produces broken output. The developer (Kieran) reads these to prioritise fixes. REQUIREMENT: Check atlas_list_pending_bugs FIRST to avoid duplicates — if a similar bug already exists in pending/triaged/in_progress state, DON'T file a new one (just mention the existing bug id in your reply). Requires bugs:write. Returns the created row (id, title, status, created_at…). Severity defaults to 'medium'; use 'critical' only for data-loss / corruption / repro-every-time issues. Keep `description` concrete: what you called, with what args, what you got back, what you expected.",
|
|
1525
|
+
inputSchema: {
|
|
1526
|
+
type: "object",
|
|
1527
|
+
properties: {
|
|
1528
|
+
title: {
|
|
1529
|
+
type: "string",
|
|
1530
|
+
description: "Short title (3–160 chars). Lead with the broken tool / symptom.",
|
|
1531
|
+
},
|
|
1532
|
+
description: {
|
|
1533
|
+
type: "string",
|
|
1534
|
+
description: "What happened (10–8000 chars). Include the tool name, arguments passed, the error / wrong output, and what you expected. Precise > verbose.",
|
|
1535
|
+
},
|
|
1536
|
+
mcp_tool: {
|
|
1537
|
+
type: "string",
|
|
1538
|
+
description: "The MCP tool that misbehaved (e.g. 'atlas_build_moodboard'). Omit for cross-cutting bugs that aren't tied to one tool.",
|
|
1539
|
+
},
|
|
1540
|
+
reproduction: {
|
|
1541
|
+
type: "string",
|
|
1542
|
+
description: "Optional repro steps or the minimum repro args payload. Helps triage faster.",
|
|
1543
|
+
},
|
|
1544
|
+
severity: {
|
|
1545
|
+
type: "string",
|
|
1546
|
+
enum: ["low", "medium", "high", "critical"],
|
|
1547
|
+
description: "Default 'medium'. 'critical' = data loss / corruption / repros every time; 'high' = blocks a real workflow; 'low' = cosmetic / nice-to-have.",
|
|
1548
|
+
},
|
|
1549
|
+
},
|
|
1550
|
+
required: ["title", "description"],
|
|
1551
|
+
},
|
|
1552
|
+
},
|
|
1407
1553
|
{
|
|
1408
1554
|
name: "export_mindmap",
|
|
1409
1555
|
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.",
|
|
@@ -1437,7 +1583,7 @@ const TOOLS = [
|
|
|
1437
1583
|
},
|
|
1438
1584
|
];
|
|
1439
1585
|
/* ─── Server setup ─────────────────────────────────────────────────── */
|
|
1440
|
-
const server = new Server({ name: "atlas-mcp", version: "0.
|
|
1586
|
+
const server = new Server({ name: "atlas-mcp", version: "0.4.0" }, { capabilities: { tools: {} } });
|
|
1441
1587
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
1442
1588
|
tools: TOOLS,
|
|
1443
1589
|
}));
|
|
@@ -1590,6 +1736,12 @@ async function dispatch(name, args) {
|
|
|
1590
1736
|
return addGearToDoc(args);
|
|
1591
1737
|
case "atlas_add_shots_to_doc":
|
|
1592
1738
|
return addShotsToDoc(args);
|
|
1739
|
+
case "atlas_add_calendar_event_to_doc":
|
|
1740
|
+
return addCalendarEventToDoc(args);
|
|
1741
|
+
case "atlas_add_section":
|
|
1742
|
+
return addSection(args);
|
|
1743
|
+
case "atlas_update_cover":
|
|
1744
|
+
return updateCover(args);
|
|
1593
1745
|
case "read_achievements":
|
|
1594
1746
|
return readAchievements();
|
|
1595
1747
|
case "atlas_list_mindmaps":
|
|
@@ -1618,6 +1770,10 @@ async function dispatch(name, args) {
|
|
|
1618
1770
|
return listActivity_(args);
|
|
1619
1771
|
case "atlas_get_activity":
|
|
1620
1772
|
return getActivity_(args);
|
|
1773
|
+
case "atlas_list_pending_bugs":
|
|
1774
|
+
return listPendingBugs_(args);
|
|
1775
|
+
case "atlas_report_bug":
|
|
1776
|
+
return reportBug_(args);
|
|
1621
1777
|
case "export_mindmap":
|
|
1622
1778
|
return exportMindmapTool(args);
|
|
1623
1779
|
default:
|
|
@@ -1669,36 +1825,54 @@ async function updateSection(args) {
|
|
|
1669
1825
|
if (newData === undefined && newStyle === undefined && hidden === undefined) {
|
|
1670
1826
|
throw new Error("Provide at least one of: data, style, hidden.");
|
|
1671
1827
|
}
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1828
|
+
const body = {
|
|
1829
|
+
...(newData !== undefined ? { data: newData } : {}),
|
|
1830
|
+
...(newStyle !== undefined ? { style: newStyle } : {}),
|
|
1831
|
+
...(hidden !== undefined ? { hidden } : {}),
|
|
1832
|
+
};
|
|
1833
|
+
// Retry on precondition failure. The web editor's autosave bumps
|
|
1834
|
+
// updated_at every few seconds, so a GET → PATCH window that spans
|
|
1835
|
+
// one autosave will 412 even when nothing conflicts. The user's
|
|
1836
|
+
// patch intent is the same shape regardless of what the editor
|
|
1837
|
+
// wrote, so re-applying it against the fresh state is safe.
|
|
1838
|
+
const MAX_ATTEMPTS = 2;
|
|
1839
|
+
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
|
1840
|
+
const current = await getTemplate({ docId });
|
|
1841
|
+
try {
|
|
1842
|
+
const { data } = await apiRequest(cfg, `/api/v1/docs/${docId}/template/sections/${sectionId}`, {
|
|
1843
|
+
method: "PATCH",
|
|
1844
|
+
body,
|
|
1845
|
+
ifMatch: current.etag,
|
|
1846
|
+
});
|
|
1847
|
+
return {
|
|
1848
|
+
updated_section: sectionId,
|
|
1849
|
+
section: data.section,
|
|
1850
|
+
etag: data.etag,
|
|
1851
|
+
updated_at: data.updated_at,
|
|
1852
|
+
...(attempt > 1 ? { retried: true } : {}),
|
|
1853
|
+
};
|
|
1696
1854
|
}
|
|
1697
|
-
|
|
1698
|
-
|
|
1855
|
+
catch (e) {
|
|
1856
|
+
if (e instanceof ApiError && e.status === 404) {
|
|
1857
|
+
throw new Error(`Section '${sectionId}' not found in doc '${docId}'. Call atlas_list_sections to see current section ids.`);
|
|
1858
|
+
}
|
|
1859
|
+
// Server now returns 412 for If-Match failures (RFC 7232); legacy
|
|
1860
|
+
// deployments may still return 409 with the same message, so
|
|
1861
|
+
// treat both as precondition failures.
|
|
1862
|
+
if (e instanceof ApiError &&
|
|
1863
|
+
(e.status === 412 || e.status === 409) &&
|
|
1864
|
+
attempt < MAX_ATTEMPTS) {
|
|
1865
|
+
process.stderr.write(`[atlas-mcp] updateSection ${sectionId}: ${e.status} on attempt ${attempt}, re-fetching + retrying\n`);
|
|
1866
|
+
continue;
|
|
1867
|
+
}
|
|
1868
|
+
if (e instanceof ApiError && (e.status === 412 || e.status === 409)) {
|
|
1869
|
+
throw new Error(`Doc kept racing after ${MAX_ATTEMPTS} attempts — someone else is actively editing. Pause the web editor (or hand off) and retry, or ask the user to make this edit themselves.`);
|
|
1870
|
+
}
|
|
1871
|
+
throw e;
|
|
1699
1872
|
}
|
|
1700
|
-
throw e;
|
|
1701
1873
|
}
|
|
1874
|
+
// Unreachable — the loop always either returns or throws.
|
|
1875
|
+
throw new Error("updateSection: unreachable retry fallthrough");
|
|
1702
1876
|
}
|
|
1703
1877
|
async function createDoc(args) {
|
|
1704
1878
|
const cfg = resolveConfig();
|
|
@@ -1785,27 +1959,41 @@ async function updateTemplate(args) {
|
|
|
1785
1959
|
throw new Error("`template_content` must be an object matching TemplateContent shape.");
|
|
1786
1960
|
}
|
|
1787
1961
|
const title = typeof args.title === "string" ? args.title : undefined;
|
|
1788
|
-
// Get-then-put
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1962
|
+
// Get-then-put with one retry on concurrent-edit race. The caller
|
|
1963
|
+
// passed us an explicit template_content, so unlike patchTemplateSections
|
|
1964
|
+
// we can't re-apply "the user's intent against fresh state" — the
|
|
1965
|
+
// intent IS the full template. We just re-check the etag and retry.
|
|
1966
|
+
const MAX_ATTEMPTS = 2;
|
|
1967
|
+
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
|
1968
|
+
const current = await getTemplate({ docId });
|
|
1969
|
+
try {
|
|
1970
|
+
const { data } = await apiRequest(cfg, `/api/v1/docs/${docId}/template`, {
|
|
1971
|
+
method: "PUT",
|
|
1972
|
+
body: { template_content: templateContent, ...(title ? { title } : {}) },
|
|
1973
|
+
ifMatch: current.etag,
|
|
1974
|
+
});
|
|
1975
|
+
return {
|
|
1976
|
+
docId: data.id,
|
|
1977
|
+
title: data.title,
|
|
1978
|
+
etag: data.etag,
|
|
1979
|
+
updated_at: data.updated_at,
|
|
1980
|
+
...(attempt > 1 ? { retried: true } : {}),
|
|
1981
|
+
};
|
|
1982
|
+
}
|
|
1983
|
+
catch (e) {
|
|
1984
|
+
if (e instanceof ApiError &&
|
|
1985
|
+
(e.status === 412 || e.status === 409) &&
|
|
1986
|
+
attempt < MAX_ATTEMPTS) {
|
|
1987
|
+
process.stderr.write(`[atlas-mcp] updateTemplate ${docId}: ${e.status} on attempt ${attempt}, re-fetching + retrying\n`);
|
|
1988
|
+
continue;
|
|
1989
|
+
}
|
|
1990
|
+
if (e instanceof ApiError && (e.status === 412 || e.status === 409)) {
|
|
1991
|
+
throw new Error(`Doc kept racing after ${MAX_ATTEMPTS} attempts — someone else is actively editing. Pause the web editor and retry, or ask the user to make this edit themselves.`);
|
|
1992
|
+
}
|
|
1993
|
+
throw e;
|
|
1806
1994
|
}
|
|
1807
|
-
throw e;
|
|
1808
1995
|
}
|
|
1996
|
+
throw new Error("updateTemplate: unreachable retry fallthrough");
|
|
1809
1997
|
}
|
|
1810
1998
|
async function describeSectionType(args) {
|
|
1811
1999
|
const type = args.type;
|
|
@@ -1871,8 +2059,22 @@ async function uploadFile(args) {
|
|
|
1871
2059
|
method: "POST",
|
|
1872
2060
|
body: { path: signRes.path, alt },
|
|
1873
2061
|
});
|
|
2062
|
+
// Step 4: mint a long-lived signed URL so the caller can drop it
|
|
2063
|
+
// straight into a section field (moodboard images[].url, cover
|
|
2064
|
+
// bgImageUrl, people-link photoUrl, …) that needs a URL rather than a
|
|
2065
|
+
// storage key.
|
|
2066
|
+
let signedUrl = null;
|
|
2067
|
+
try {
|
|
2068
|
+
const { data: signed } = await apiRequest(cfg, "/api/v1/files/sign", { method: "POST", body: { path: signRes.path } });
|
|
2069
|
+
signedUrl = signed.signed_url;
|
|
2070
|
+
}
|
|
2071
|
+
catch {
|
|
2072
|
+
// Non-fatal — the file is uploaded + registered, caller can mint a
|
|
2073
|
+
// signed URL later with atlas_get_file_url.
|
|
2074
|
+
}
|
|
1874
2075
|
return {
|
|
1875
2076
|
path: signRes.path,
|
|
2077
|
+
signed_url: signedUrl,
|
|
1876
2078
|
size: info.size,
|
|
1877
2079
|
content_type: contentType,
|
|
1878
2080
|
metadata: reg.file,
|
|
@@ -2436,7 +2638,7 @@ async function uploadWorkspaceLogo(args) {
|
|
|
2436
2638
|
headers: {
|
|
2437
2639
|
Authorization: `Bearer ${cfg.token}`,
|
|
2438
2640
|
Accept: "application/json",
|
|
2439
|
-
"User-Agent": "atlas-mcp/0.
|
|
2641
|
+
"User-Agent": "atlas-mcp/0.4.0",
|
|
2440
2642
|
},
|
|
2441
2643
|
body: form,
|
|
2442
2644
|
});
|
|
@@ -2469,7 +2671,7 @@ async function exportDocTool(args) {
|
|
|
2469
2671
|
headers: {
|
|
2470
2672
|
Authorization: `Bearer ${cfg.token}`,
|
|
2471
2673
|
Accept: "text/plain",
|
|
2472
|
-
"User-Agent": "atlas-mcp/0.
|
|
2674
|
+
"User-Agent": "atlas-mcp/0.4.0",
|
|
2473
2675
|
},
|
|
2474
2676
|
});
|
|
2475
2677
|
if (!res.ok || !res.body) {
|
|
@@ -2544,6 +2746,14 @@ async function exportDocTool(args) {
|
|
|
2544
2746
|
return { out_path: outPath, pages: done.pages, bytes: bytes.length };
|
|
2545
2747
|
}
|
|
2546
2748
|
/* ─── Moodboard, palette, products, locations, gear, shots helpers ─── */
|
|
2749
|
+
/** Mint a 1-year signed URL for a storage key. Mirrors the browser
|
|
2750
|
+
* upload path (lib/doc-image-upload.ts), which signs for the same
|
|
2751
|
+
* duration so a doc's images keep rendering across normal lifetimes.
|
|
2752
|
+
* Returns the signed URL (or throws if the API rejects). */
|
|
2753
|
+
async function signStorageKey(cfg, path) {
|
|
2754
|
+
const { data } = await apiRequest(cfg, "/api/v1/files/sign", { method: "POST", body: { path } });
|
|
2755
|
+
return data.signed_url;
|
|
2756
|
+
}
|
|
2547
2757
|
async function uploadLocalImage(cfg, localPath) {
|
|
2548
2758
|
const { readFile, stat } = await import("node:fs/promises");
|
|
2549
2759
|
const { basename, extname, isAbsolute, resolve: resolvePath } = await import("node:path");
|
|
@@ -2596,31 +2806,46 @@ function looksLikeStorageKey(input) {
|
|
|
2596
2806
|
function randId(prefix) {
|
|
2597
2807
|
return `${prefix}_${Math.random().toString(36).slice(2, 10)}`;
|
|
2598
2808
|
}
|
|
2599
|
-
/** Shared get-template → mutate sections → put-template flow.
|
|
2809
|
+
/** Shared get-template → mutate sections → put-template flow.
|
|
2810
|
+
*
|
|
2811
|
+
* Retries once on precondition failure (412 — or legacy 409 — from a
|
|
2812
|
+
* concurrent web-editor autosave). The mutate callback is re-run against
|
|
2813
|
+
* the freshly-fetched state so append/insert operations land on top of
|
|
2814
|
+
* whatever the autosave wrote. */
|
|
2600
2815
|
async function patchTemplateSections(cfg, docId, mutate) {
|
|
2601
|
-
const
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2816
|
+
const MAX_ATTEMPTS = 2;
|
|
2817
|
+
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
|
2818
|
+
const { data: current } = await apiRequest(cfg, `/api/v1/docs/${docId}/template`);
|
|
2819
|
+
const existingSections = Array.isArray(current.template_content?.sections)
|
|
2820
|
+
? current.template_content.sections
|
|
2821
|
+
: [];
|
|
2822
|
+
const { sections: nextSections, message } = mutate([...existingSections]);
|
|
2823
|
+
const nextContent = {
|
|
2824
|
+
...(current.template_content ?? { sections: [] }),
|
|
2825
|
+
sections: nextSections,
|
|
2826
|
+
};
|
|
2827
|
+
try {
|
|
2828
|
+
const { data } = await apiRequest(cfg, `/api/v1/docs/${docId}/template`, {
|
|
2829
|
+
method: "PUT",
|
|
2830
|
+
body: { template_content: nextContent, ...(message ? { message } : {}) },
|
|
2831
|
+
ifMatch: current.etag,
|
|
2832
|
+
});
|
|
2833
|
+
return { etag: data.etag, updated_at: data.updated_at };
|
|
2834
|
+
}
|
|
2835
|
+
catch (e) {
|
|
2836
|
+
if (e instanceof ApiError &&
|
|
2837
|
+
(e.status === 412 || e.status === 409) &&
|
|
2838
|
+
attempt < MAX_ATTEMPTS) {
|
|
2839
|
+
process.stderr.write(`[atlas-mcp] patchTemplateSections ${docId}: ${e.status} on attempt ${attempt}, re-fetching + retrying\n`);
|
|
2840
|
+
continue;
|
|
2841
|
+
}
|
|
2842
|
+
if (e instanceof ApiError && (e.status === 412 || e.status === 409)) {
|
|
2843
|
+
throw new Error(`Doc kept racing after ${MAX_ATTEMPTS} attempts — someone else is actively editing. Pause the web editor and retry.`);
|
|
2844
|
+
}
|
|
2845
|
+
throw e;
|
|
2621
2846
|
}
|
|
2622
|
-
throw e;
|
|
2623
2847
|
}
|
|
2848
|
+
throw new Error("patchTemplateSections: unreachable retry fallthrough");
|
|
2624
2849
|
}
|
|
2625
2850
|
async function buildMoodboard(args) {
|
|
2626
2851
|
const cfg = resolveConfig();
|
|
@@ -2635,24 +2860,41 @@ async function buildMoodboard(args) {
|
|
|
2635
2860
|
const spacing = typeof args.spacing === "number" ? args.spacing : 8;
|
|
2636
2861
|
const radius = typeof args.radius === "number" ? args.radius : 8;
|
|
2637
2862
|
const position = typeof args.position === "number" ? Math.round(args.position) : null;
|
|
2863
|
+
// For every image — whether we just uploaded it or the caller handed
|
|
2864
|
+
// us a pre-existing storage key — mint a 1-year signed URL. Storing
|
|
2865
|
+
// raw storage keys here would render as 404s (storage objects are
|
|
2866
|
+
// private; the renderer uses `<img src>` with no server rewrite).
|
|
2638
2867
|
let uploadedCount = 0;
|
|
2639
|
-
const
|
|
2868
|
+
const resolved = [];
|
|
2640
2869
|
for (const p of paths) {
|
|
2641
2870
|
if (typeof p !== "string" || !p)
|
|
2642
2871
|
continue;
|
|
2872
|
+
let key;
|
|
2643
2873
|
if (looksLikeStorageKey(p)) {
|
|
2644
|
-
|
|
2874
|
+
key = p;
|
|
2645
2875
|
}
|
|
2646
2876
|
else {
|
|
2647
|
-
|
|
2648
|
-
storageKeys.push(key);
|
|
2877
|
+
key = await uploadLocalImage(cfg, p);
|
|
2649
2878
|
uploadedCount++;
|
|
2650
2879
|
}
|
|
2880
|
+
const signedUrl = await signStorageKey(cfg, key);
|
|
2881
|
+
resolved.push({ key, signedUrl });
|
|
2651
2882
|
}
|
|
2652
2883
|
const total = rows * cols;
|
|
2653
2884
|
const images = [];
|
|
2654
2885
|
for (let i = 0; i < total; i++) {
|
|
2655
|
-
|
|
2886
|
+
const r = resolved[i];
|
|
2887
|
+
if (r) {
|
|
2888
|
+
images.push({
|
|
2889
|
+
id: randId("img"),
|
|
2890
|
+
url: r.signedUrl,
|
|
2891
|
+
// No per-image alt input yet — tool caller sets alts via the
|
|
2892
|
+
// web editor or atlas_set_file_alt.
|
|
2893
|
+
});
|
|
2894
|
+
}
|
|
2895
|
+
else {
|
|
2896
|
+
images.push(null);
|
|
2897
|
+
}
|
|
2656
2898
|
}
|
|
2657
2899
|
const sectionId = randId("sec");
|
|
2658
2900
|
const result = await patchTemplateSections(cfg, docId, (sections) => {
|
|
@@ -2937,6 +3179,171 @@ async function addShotsToDoc(args) {
|
|
|
2937
3179
|
});
|
|
2938
3180
|
return { section_id: targetId, added: rows.length, missing, etag: res.etag };
|
|
2939
3181
|
}
|
|
3182
|
+
/** Schedule a shoot AND wire its id into a calendar-event section in one
|
|
3183
|
+
* call. Mirrors addProductsToDoc's shape: POST to /api/v1/events, then
|
|
3184
|
+
* patchTemplateSections to either append the new id to an existing
|
|
3185
|
+
* calendar-event section or create a fresh one. */
|
|
3186
|
+
async function addCalendarEventToDoc(args) {
|
|
3187
|
+
const cfg = resolveConfig();
|
|
3188
|
+
const docId = requireString(args, "doc_id");
|
|
3189
|
+
const sectionId = typeof args.section_id === "string" ? args.section_id : null;
|
|
3190
|
+
const title = typeof args.title === "string" ? args.title : "Schedule";
|
|
3191
|
+
const layout = typeof args.layout === "string" && ["card", "table", "banner"].includes(args.layout)
|
|
3192
|
+
? args.layout
|
|
3193
|
+
: "card";
|
|
3194
|
+
if (!isRecord(args.event)) {
|
|
3195
|
+
throw new Error("`event` must be an object with at least { title, starts_at }.");
|
|
3196
|
+
}
|
|
3197
|
+
const eventBody = args.event;
|
|
3198
|
+
if (typeof eventBody.title !== "string" || !eventBody.title.trim()) {
|
|
3199
|
+
throw new Error("`event.title` is required.");
|
|
3200
|
+
}
|
|
3201
|
+
if (typeof eventBody.starts_at !== "string" || !eventBody.starts_at.trim()) {
|
|
3202
|
+
throw new Error("`event.starts_at` is required (ISO 8601).");
|
|
3203
|
+
}
|
|
3204
|
+
// Step 1: create the shoot.
|
|
3205
|
+
const { data: eventRes } = await apiRequest(cfg, "/api/v1/events", { method: "POST", body: eventBody });
|
|
3206
|
+
const eventId = eventRes.event?.id;
|
|
3207
|
+
if (!eventId) {
|
|
3208
|
+
throw new Error("Event creation returned no id — cannot wire into section.");
|
|
3209
|
+
}
|
|
3210
|
+
// Step 2: wire the id into a calendar-event section (append to
|
|
3211
|
+
// existing, or create a new one).
|
|
3212
|
+
let targetId = sectionId;
|
|
3213
|
+
const res = await patchTemplateSections(cfg, docId, (sections) => {
|
|
3214
|
+
if (targetId) {
|
|
3215
|
+
const idx = sections.findIndex((s) => s.id === targetId);
|
|
3216
|
+
if (idx === -1)
|
|
3217
|
+
throw new Error(`Section '${targetId}' not found`);
|
|
3218
|
+
if (sections[idx].type !== "calendar-event") {
|
|
3219
|
+
throw new Error(`Section '${targetId}' is '${sections[idx].type}', not 'calendar-event'`);
|
|
3220
|
+
}
|
|
3221
|
+
const existing = (sections[idx].data ?? {});
|
|
3222
|
+
const prev = Array.isArray(existing.eventIds)
|
|
3223
|
+
? existing.eventIds.filter((v) => typeof v === "string")
|
|
3224
|
+
: [];
|
|
3225
|
+
sections[idx] = {
|
|
3226
|
+
...sections[idx],
|
|
3227
|
+
data: {
|
|
3228
|
+
...(sections[idx].data ?? {}),
|
|
3229
|
+
eventIds: [...prev, eventId],
|
|
3230
|
+
},
|
|
3231
|
+
};
|
|
3232
|
+
}
|
|
3233
|
+
else {
|
|
3234
|
+
targetId = randId("sec");
|
|
3235
|
+
sections.push({
|
|
3236
|
+
id: targetId,
|
|
3237
|
+
type: "calendar-event",
|
|
3238
|
+
data: {
|
|
3239
|
+
title,
|
|
3240
|
+
eventIds: [eventId],
|
|
3241
|
+
layout,
|
|
3242
|
+
showAttendees: true,
|
|
3243
|
+
showLocation: true,
|
|
3244
|
+
showDescription: true,
|
|
3245
|
+
},
|
|
3246
|
+
});
|
|
3247
|
+
}
|
|
3248
|
+
return { sections, message: "Add calendar event to doc" };
|
|
3249
|
+
});
|
|
3250
|
+
return {
|
|
3251
|
+
section_id: targetId,
|
|
3252
|
+
event_id: eventId,
|
|
3253
|
+
etag: res.etag,
|
|
3254
|
+
};
|
|
3255
|
+
}
|
|
3256
|
+
/** Append (or insert at `position`) a new section of any supported type.
|
|
3257
|
+
* Uses the scoped append endpoint so the write doesn't fight the web
|
|
3258
|
+
* editor's autosave. Retries once on precondition failure. */
|
|
3259
|
+
async function addSection(args) {
|
|
3260
|
+
const cfg = resolveConfig();
|
|
3261
|
+
const docId = requireString(args, "doc_id");
|
|
3262
|
+
const type = requireString(args, "type");
|
|
3263
|
+
if (!isRecord(args.data)) {
|
|
3264
|
+
throw new Error("`data` must be an object matching the target section type's shape.");
|
|
3265
|
+
}
|
|
3266
|
+
const data = args.data;
|
|
3267
|
+
const position = typeof args.position === "number" ? Math.round(args.position) : undefined;
|
|
3268
|
+
const hidden = typeof args.hidden === "boolean" ? args.hidden : undefined;
|
|
3269
|
+
// Fetch etag so If-Match survives a concurrent autosave.
|
|
3270
|
+
const MAX_ATTEMPTS = 2;
|
|
3271
|
+
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
|
3272
|
+
const current = await getTemplate({ docId });
|
|
3273
|
+
try {
|
|
3274
|
+
const { data: res } = await apiRequest(cfg, `/api/v1/docs/${docId}/template/sections`, {
|
|
3275
|
+
method: "POST",
|
|
3276
|
+
body: {
|
|
3277
|
+
type,
|
|
3278
|
+
data,
|
|
3279
|
+
...(hidden !== undefined ? { hidden } : {}),
|
|
3280
|
+
...(position !== undefined ? { position } : {}),
|
|
3281
|
+
},
|
|
3282
|
+
ifMatch: current.etag,
|
|
3283
|
+
});
|
|
3284
|
+
return {
|
|
3285
|
+
section: res.section,
|
|
3286
|
+
section_id: res.section.id,
|
|
3287
|
+
etag: res.etag,
|
|
3288
|
+
updated_at: res.updated_at,
|
|
3289
|
+
...(attempt > 1 ? { retried: true } : {}),
|
|
3290
|
+
};
|
|
3291
|
+
}
|
|
3292
|
+
catch (e) {
|
|
3293
|
+
if (e instanceof ApiError &&
|
|
3294
|
+
(e.status === 412 || e.status === 409) &&
|
|
3295
|
+
attempt < MAX_ATTEMPTS) {
|
|
3296
|
+
process.stderr.write(`[atlas-mcp] addSection ${docId}: ${e.status} on attempt ${attempt}, re-fetching + retrying\n`);
|
|
3297
|
+
continue;
|
|
3298
|
+
}
|
|
3299
|
+
if (e instanceof ApiError && (e.status === 412 || e.status === 409)) {
|
|
3300
|
+
throw new Error(`Doc kept racing after ${MAX_ATTEMPTS} attempts — someone else is actively editing. Pause the web editor and retry.`);
|
|
3301
|
+
}
|
|
3302
|
+
throw e;
|
|
3303
|
+
}
|
|
3304
|
+
}
|
|
3305
|
+
throw new Error("addSection: unreachable retry fallthrough");
|
|
3306
|
+
}
|
|
3307
|
+
/** Scoped merge-update of template_content.cover. Uses the dedicated
|
|
3308
|
+
* cover PATCH endpoint — doesn't touch sections[] or docSettings. */
|
|
3309
|
+
async function updateCover(args) {
|
|
3310
|
+
const cfg = resolveConfig();
|
|
3311
|
+
const docId = requireString(args, "doc_id");
|
|
3312
|
+
if (!isRecord(args.cover)) {
|
|
3313
|
+
throw new Error("`cover` must be an object of fields to merge onto template_content.cover.");
|
|
3314
|
+
}
|
|
3315
|
+
const cover = args.cover;
|
|
3316
|
+
const MAX_ATTEMPTS = 2;
|
|
3317
|
+
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
|
3318
|
+
const current = await getTemplate({ docId });
|
|
3319
|
+
try {
|
|
3320
|
+
const { data } = await apiRequest(cfg, `/api/v1/docs/${docId}/template/cover`, {
|
|
3321
|
+
method: "PATCH",
|
|
3322
|
+
body: { cover },
|
|
3323
|
+
ifMatch: current.etag,
|
|
3324
|
+
});
|
|
3325
|
+
return {
|
|
3326
|
+
cover: data.cover,
|
|
3327
|
+
etag: data.etag,
|
|
3328
|
+
updated_at: data.updated_at,
|
|
3329
|
+
...(attempt > 1 ? { retried: true } : {}),
|
|
3330
|
+
};
|
|
3331
|
+
}
|
|
3332
|
+
catch (e) {
|
|
3333
|
+
if (e instanceof ApiError &&
|
|
3334
|
+
(e.status === 412 || e.status === 409) &&
|
|
3335
|
+
attempt < MAX_ATTEMPTS) {
|
|
3336
|
+
process.stderr.write(`[atlas-mcp] updateCover ${docId}: ${e.status} on attempt ${attempt}, re-fetching + retrying\n`);
|
|
3337
|
+
continue;
|
|
3338
|
+
}
|
|
3339
|
+
if (e instanceof ApiError && (e.status === 412 || e.status === 409)) {
|
|
3340
|
+
throw new Error(`Doc kept racing after ${MAX_ATTEMPTS} attempts — someone else is actively editing. Pause the web editor and retry.`);
|
|
3341
|
+
}
|
|
3342
|
+
throw e;
|
|
3343
|
+
}
|
|
3344
|
+
}
|
|
3345
|
+
throw new Error("updateCover: unreachable retry fallthrough");
|
|
3346
|
+
}
|
|
2940
3347
|
/* ─── Mindmap tools ────────────────────────────────────────────────── */
|
|
2941
3348
|
async function listMindmaps_(args) {
|
|
2942
3349
|
const cfg = resolveConfig();
|
|
@@ -3155,6 +3562,48 @@ async function getActivity_(args) {
|
|
|
3155
3562
|
const { data } = await apiRequest(cfg, `/api/v1/activity/${entryId}`);
|
|
3156
3563
|
return data;
|
|
3157
3564
|
}
|
|
3565
|
+
/* ─── Bug reports ──────────────────────────────────────────────────── */
|
|
3566
|
+
/** List pending (or otherwise unresolved) bugs so an agent can dedupe
|
|
3567
|
+
* before filing a new one. Default filter: pending + triaged +
|
|
3568
|
+
* in_progress — the three "we haven't fixed this yet" statuses. */
|
|
3569
|
+
async function listPendingBugs_(args) {
|
|
3570
|
+
const cfg = resolveConfig();
|
|
3571
|
+
const query = {};
|
|
3572
|
+
if (typeof args.limit === "number")
|
|
3573
|
+
query.limit = args.limit;
|
|
3574
|
+
if (typeof args.cursor === "string")
|
|
3575
|
+
query.cursor = args.cursor;
|
|
3576
|
+
if (typeof args.mcp_tool === "string" && args.mcp_tool) {
|
|
3577
|
+
query.mcp_tool = args.mcp_tool;
|
|
3578
|
+
}
|
|
3579
|
+
query.status =
|
|
3580
|
+
typeof args.status === "string" && args.status.trim()
|
|
3581
|
+
? args.status
|
|
3582
|
+
: "pending,triaged,in_progress";
|
|
3583
|
+
const { data } = await apiRequest(cfg, "/api/v1/bugs", { query });
|
|
3584
|
+
return data;
|
|
3585
|
+
}
|
|
3586
|
+
async function reportBug_(args) {
|
|
3587
|
+
const cfg = resolveConfig();
|
|
3588
|
+
const body = {
|
|
3589
|
+
title: requireString(args, "title"),
|
|
3590
|
+
description: requireString(args, "description"),
|
|
3591
|
+
};
|
|
3592
|
+
if (typeof args.mcp_tool === "string" && args.mcp_tool) {
|
|
3593
|
+
body.mcp_tool = args.mcp_tool;
|
|
3594
|
+
}
|
|
3595
|
+
if (typeof args.reproduction === "string" && args.reproduction) {
|
|
3596
|
+
body.reproduction = args.reproduction;
|
|
3597
|
+
}
|
|
3598
|
+
if (typeof args.severity === "string" && args.severity) {
|
|
3599
|
+
body.severity = args.severity;
|
|
3600
|
+
}
|
|
3601
|
+
const { data } = await apiRequest(cfg, "/api/v1/bugs", {
|
|
3602
|
+
method: "POST",
|
|
3603
|
+
body,
|
|
3604
|
+
});
|
|
3605
|
+
return data;
|
|
3606
|
+
}
|
|
3158
3607
|
/* ─── Mindmap export (PDF / PNG / SVG) ───────────────────────────── */
|
|
3159
3608
|
async function exportMindmapTool(args) {
|
|
3160
3609
|
const cfg = resolveConfig();
|
|
@@ -3175,7 +3624,7 @@ async function exportMindmapTool(args) {
|
|
|
3175
3624
|
headers: {
|
|
3176
3625
|
Authorization: `Bearer ${cfg.token}`,
|
|
3177
3626
|
Accept: "image/svg+xml",
|
|
3178
|
-
"User-Agent": "atlas-mcp/0.
|
|
3627
|
+
"User-Agent": "atlas-mcp/0.4.0",
|
|
3179
3628
|
},
|
|
3180
3629
|
});
|
|
3181
3630
|
if (!res.ok) {
|
|
@@ -3200,7 +3649,7 @@ async function exportMindmapTool(args) {
|
|
|
3200
3649
|
headers: {
|
|
3201
3650
|
Authorization: `Bearer ${cfg.token}`,
|
|
3202
3651
|
Accept: "application/json",
|
|
3203
|
-
"User-Agent": "atlas-mcp/0.
|
|
3652
|
+
"User-Agent": "atlas-mcp/0.4.0",
|
|
3204
3653
|
},
|
|
3205
3654
|
});
|
|
3206
3655
|
if (!tokRes.ok) {
|