@abraca/mcp 1.6.0 → 1.8.1

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.
@@ -38,6 +38,7 @@ export function registerAwarenessTools(mcp: McpServer, server: AbracadabraMCPSer
38
38
  fields: z.record(z.string(), z.unknown()).describe('Key-value pairs to set on the child document\'s awareness state. Use namespaced keys like "kanban:hovering", "table:editing", "slides:viewing", "outline:editing", "calendar:focused", "gallery:focused", "timeline:focused", "graph:focused", "map:focused", "doc:scroll". Set a key to null to clear it.'),
39
39
  },
40
40
  async ({ docId, fields }) => {
41
+ server.setActiveToolCall({ name: 'set_doc_awareness', target: docId })
41
42
  try {
42
43
  const provider = await server.getChildProvider(docId)
43
44
  for (const [key, value] of Object.entries(fields)) {
@@ -58,6 +59,7 @@ export function registerAwarenessTools(mcp: McpServer, server: AbracadabraMCPSer
58
59
  'Check the "AI Inbox" document for pending instructions from humans. Returns the inbox content and any pending task sub-documents. Create the inbox as a doc called "AI Inbox" under the hub doc if it does not exist yet. Note: channel-based watching via watch_chat is preferred for real-time use.',
59
60
  {},
60
61
  async () => {
62
+ server.setActiveToolCall({ name: 'poll_inbox' })
61
63
  try {
62
64
  const treeMap = server.getTreeMap()
63
65
  const rootDocId = server.rootDocId
@@ -124,6 +126,7 @@ export function registerAwarenessTools(mcp: McpServer, server: AbracadabraMCPSer
124
126
  docId: z.string().optional().describe('If provided, list users connected to this specific document. Otherwise lists users from root awareness.'),
125
127
  },
126
128
  async ({ docId }) => {
129
+ server.setActiveToolCall({ name: 'list_connected_users', target: docId })
127
130
  try {
128
131
  let awareness
129
132
  if (docId) {
@@ -23,7 +23,6 @@ export function registerChannelTools(mcp: McpServer, server: AbracadabraMCPServe
23
23
  const treeMap = server.getTreeMap()
24
24
  const rootDoc = server.rootDocument
25
25
  if (!treeMap || !rootDoc) {
26
- server.setActiveToolCall(null)
27
26
  return { content: [{ type: 'text' as const, text: 'Not connected' }], isError: true }
28
27
  }
29
28
 
@@ -51,13 +50,11 @@ export function registerChannelTools(mcp: McpServer, server: AbracadabraMCPServe
51
50
  server.clearAiTask(task_id)
52
51
  }
53
52
 
54
- server.setActiveToolCall(null)
55
53
 
56
54
  return {
57
55
  content: [{ type: 'text' as const, text: JSON.stringify({ replyDocId: replyId, parentId: doc_id }) }],
58
56
  }
59
57
  } catch (error: any) {
60
- server.setActiveToolCall(null)
61
58
  return {
62
59
  content: [{ type: 'text' as const, text: `Error: ${error.message}` }],
63
60
  isError: true,
@@ -80,17 +77,30 @@ export function registerChannelTools(mcp: McpServer, server: AbracadabraMCPServe
80
77
  return { content: [{ type: 'text' as const, text: 'Not connected' }], isError: true }
81
78
  }
82
79
 
80
+ // Normalize literal escape sequences. Some LLM outputs emit the
81
+ // 2-char `\n` / `\t` / `\r` instead of the real control chars, which
82
+ // then render literally in the chat (markdown renderer can't see a
83
+ // newline where there is just "\n"). Convert them back to real chars.
84
+ const normalized = text
85
+ .replace(/\\r\\n/g, '\n')
86
+ .replace(/\\n/g, '\n')
87
+ .replace(/\\t/g, '\t')
88
+ .replace(/\\r/g, '\n')
89
+
90
+ // Order matters: clear status + tool pill FIRST so the dashboard's
91
+ // typing-indicator filter (which hides typing while an activeToolCall
92
+ // exists) doesn't swallow the burst. Then emit the typing frame, then
93
+ // the actual chat:send. The clear also flushes toolHistory + turnId.
94
+ server.setAutoStatus(null)
95
+ server.sendTypingIndicator(channel)
96
+
83
97
  rootProvider.sendStateless(JSON.stringify({
84
98
  type: 'chat:send',
85
99
  channel,
86
- content: text,
100
+ content: normalized,
87
101
  sender_name: server.agentName,
88
102
  }))
89
103
 
90
- // Clear thinking/typing status after sending the reply
91
- server.setAutoStatus(null)
92
- server.setActiveToolCall(null)
93
-
94
104
  return { content: [{ type: 'text' as const, text: `Sent to ${channel}` }] }
95
105
  } catch (error: any) {
96
106
  return {
@@ -51,8 +51,6 @@ export function registerContentTools(mcp: McpServer, server: AbracadabraMCPServe
51
51
  children.sort((a: any, b: any) => ((treeMap.get(a.id)?.order ?? 0) - (treeMap.get(b.id)?.order ?? 0)))
52
52
  }
53
53
 
54
- server.setActiveToolCall(null)
55
-
56
54
  const result: Record<string, unknown> = { label, type, meta, markdown, children }
57
55
  return {
58
56
  content: [{
@@ -61,7 +59,6 @@ export function registerContentTools(mcp: McpServer, server: AbracadabraMCPServe
61
59
  }],
62
60
  }
63
61
  } catch (error: any) {
64
- server.setActiveToolCall(null)
65
62
  return {
66
63
  content: [{ type: 'text', text: `Error reading document: ${error.message}` }],
67
64
  isError: true,
@@ -128,13 +125,11 @@ export function registerContentTools(mcp: McpServer, server: AbracadabraMCPServe
128
125
  server.setFocusedDoc(docId)
129
126
  server.setDocCursor(docId, fragment.length)
130
127
 
131
- server.setActiveToolCall(null)
132
128
 
133
129
  return {
134
130
  content: [{ type: 'text', text: `Document ${docId} updated (${writeMode} mode)` }],
135
131
  }
136
132
  } catch (error: any) {
137
- server.setActiveToolCall(null)
138
133
  return {
139
134
  content: [{ type: 'text', text: `Error writing document: ${error.message}` }],
140
135
  isError: true,
@@ -15,6 +15,8 @@ export function registerFileTools(mcp: McpServer, server: AbracadabraMCPServer)
15
15
  docId: z.string().describe('Document ID.'),
16
16
  },
17
17
  async ({ docId }) => {
18
+ server.setAutoStatus('reading', docId)
19
+ server.setActiveToolCall({ name: 'list_uploads', target: docId })
18
20
  try {
19
21
  const uploads = await server.client.listUploads(docId)
20
22
  return {
@@ -41,6 +43,8 @@ export function registerFileTools(mcp: McpServer, server: AbracadabraMCPServer)
41
43
  filename: z.string().optional().describe('Override filename (defaults to basename of filePath).'),
42
44
  },
43
45
  async ({ docId, filePath, filename }) => {
46
+ server.setAutoStatus('uploading', docId)
47
+ server.setActiveToolCall({ name: 'upload_file', target: path.basename(filePath) })
44
48
  try {
45
49
  const resolvedPath = path.resolve(filePath)
46
50
  const data = fs.readFileSync(resolvedPath)
@@ -71,6 +75,8 @@ export function registerFileTools(mcp: McpServer, server: AbracadabraMCPServer)
71
75
  saveTo: z.string().describe('Absolute local file path to save the download.'),
72
76
  },
73
77
  async ({ docId, uploadId, saveTo }) => {
78
+ server.setAutoStatus('reading', docId)
79
+ server.setActiveToolCall({ name: 'download_file', target: path.basename(saveTo) })
74
80
  try {
75
81
  const blob = await server.client.getUpload(docId, uploadId)
76
82
  const buffer = Buffer.from(await blob.arrayBuffer())
@@ -96,6 +102,8 @@ export function registerFileTools(mcp: McpServer, server: AbracadabraMCPServer)
96
102
  uploadId: z.string().describe('Upload ID to delete.'),
97
103
  },
98
104
  async ({ docId, uploadId }) => {
105
+ server.setAutoStatus('writing', docId)
106
+ server.setActiveToolCall({ name: 'delete_file', target: uploadId })
99
107
  try {
100
108
  await server.client.deleteUpload(docId, uploadId)
101
109
  return { content: [{ type: 'text', text: `Deleted upload ${uploadId}` }] }
package/src/tools/meta.ts CHANGED
@@ -17,17 +17,14 @@ export function registerMetaTools(mcp: McpServer, server: AbracadabraMCPServer)
17
17
  server.setActiveToolCall({ name: 'get_metadata', target: docId })
18
18
  const treeMap = server.getTreeMap()
19
19
  if (!treeMap) {
20
- server.setActiveToolCall(null)
21
20
  return { content: [{ type: 'text', text: 'Not connected' }] }
22
21
  }
23
22
 
24
23
  const entry = treeMap.get(docId)
25
24
  if (!entry) {
26
- server.setActiveToolCall(null)
27
25
  return { content: [{ type: 'text', text: `Document ${docId} not found` }] }
28
26
  }
29
27
 
30
- server.setActiveToolCall(null)
31
28
  return {
32
29
  content: [{
33
30
  type: 'text',
@@ -47,20 +44,18 @@ export function registerMetaTools(mcp: McpServer, server: AbracadabraMCPServer)
47
44
  'Update metadata fields on a document. Merges the provided fields into existing metadata.',
48
45
  {
49
46
  docId: z.string().describe('Document ID.'),
50
- meta: z.record(z.unknown()).describe('Metadata fields to update (merged with existing). Universal keys: color (hex), icon (Lucide kebab-case — NEVER emoji), dateStart/dateEnd, datetimeStart/datetimeEnd, allDay, tags (string[]), checked (bool), priority (0=none,1=low,2=med,3=high,4=urgent), status, rating (0-5), url, email, phone, number, unit, subtitle, note, taskProgress (0-100), members ({id,label}[]), coverUploadId. Geo/Map: geoType ("marker"|"line"|"measure"), geoLat, geoLng, geoDescription. Spatial 3D: spShape ("box"|"sphere"|"cylinder"|"cone"|"plane"|"torus"|"glb"), spX/spY/spZ, spRX/spRY/spRZ, spSX/spSY/spSZ, spColor, spOpacity (0-100). Dashboard: deskX, deskY, deskZ, deskMode ("icon"|"widget-sm"|"widget-lg"). Slides: slidesTransition ("none"|"fade"|"slide"), slidesTheme ("dark"|"light"). Chart: chartType ("bar"|"stacked bar"|"line"|"donut"|"treemap"), chartMetric, chartColorScheme, chartLimit, chartShowLegend, chartShowValues. Sheets: sheetsDefaultColWidth, sheetsDefaultRowHeight, sheetsShowGridlines, sheetsFreezeRows, sheetsFreezeCols. Cell formatting: bold, italic, textColor, bgColor, align, formula. Renderer config (on the page doc itself): kanbanColumnWidth, galleryColumns, galleryAspect, galleryCardStyle, galleryShowLabels, gallerySortBy, calendarView, calendarWeekStart, calendarShowWeekNumbers, tableMode, tableSortDir, checklistFilter, checklistSort, mapShowLabels, spatialGridVisible, showRefEdges, mediaRepeat, mediaShuffle. Set a key to null to clear it.'),
47
+ meta: z.record(z.string(), z.unknown()).describe('Metadata fields to update (merged with existing). Universal keys: color (hex), icon (Lucide kebab-case — NEVER emoji), dateStart/dateEnd, datetimeStart/datetimeEnd, allDay, timeStart/timeEnd, tags (string[]), checked (bool), priority (0=none,1=low,2=med,3=high,4=urgent), status, rating (0-5), url, email, phone, number, unit, subtitle, note, taskProgress (0-100), members ({id,label}[]), coverUploadId, coverDocId, dateTaken. Geo/Map (children): geoType ("marker"|"line"|"measure"), geoLat, geoLng, geoDescription. Spatial 3D (children, plugin: spatial): spShape ("box"|"sphere"|"cylinder"|"cone"|"plane"|"torus"|"glb"), spX/spY/spZ, spRX/spRY/spRZ (deg), spSX/spSY/spSZ (scale), spOpacity (0-100), spModelUploadId, spModelDocId — spatial uses the universal `color` key, NOT spColor. Dashboard (children): deskX, deskY, deskZ, deskMode ("icon"|"widget-sm"|"widget-lg"). Mindmap-layout (children): mmX, mmY. Graph-layout (children): graphX, graphY, graphPinned. Slides (children): slidesTransition ("none"|"fade"|"slide"). Coder (children, plugin: coder): fileType ("vue"|"ts"|"js"|"css"|"json"|"folder"), entry (bool). Cell formatting (sheets cells): bold, italic, textColor, bgColor, align ("left"|"center"|"right"), formula. Renderer config (on the PAGE doc itself, not children): kanbanColumnWidth ("narrow"|"default"|"wide"), galleryColumns (1-6), galleryAspect ("square"|"4:3"|"3:2"|"16:9"|"free"), galleryCardStyle ("default"|"compact"|"detailed"), galleryShowLabels, gallerySortBy ("manual"|"date"|"name"|"rating"), calendarView ("month"|"week"|"day"), calendarWeekStart ("sun"|"mon"), calendarShowWeekNumbers, tableMode ("hierarchy"|"flat"), tableSortKey, tableSortDir ("asc"|"desc"), timelineZoom ("week"|"month"|"quarter"), timelinePixelsPerDay, timelineCenterDate (ISO date), checklistFilter ("all"|"active"|"completed"), checklistSort ("manual"|"priority"|"due"), mapShowLabels, graphSpacing ("compact"|"default"|"spacious"), graphShowLabels, graphEdgeThickness ("thin"|"normal"|"thick"), showRefEdges, mmSpacing, spatialGridVisible, slidesTheme ("dark"|"light"), chartType ("bar"|"stacked bar"|"line"|"donut"|"treemap"), chartMetric ("value"|"type"|"tag"|"status"|"priority"|"activity"|"completion"), chartColorScheme ("default"|"warm"|"cool"|"mono"), chartLimit (3-30), chartShowLegend, chartShowValues, sheetsDefaultColWidth (40-500), sheetsDefaultRowHeight (20-100), sheetsShowGridlines, sheetsFreezeRows, sheetsFreezeCols, mediaRepeat ("off"|"all"|"one"), mediaShuffle. Set a key to null to clear it.'),
51
48
  },
52
49
  async ({ docId, meta }) => {
53
50
  server.setAutoStatus('writing', docId)
54
51
  server.setActiveToolCall({ name: 'update_metadata', target: docId })
55
52
  const treeMap = server.getTreeMap()
56
53
  if (!treeMap) {
57
- server.setActiveToolCall(null)
58
54
  return { content: [{ type: 'text', text: 'Not connected' }] }
59
55
  }
60
56
 
61
57
  const entry = treeMap.get(docId)
62
58
  if (!entry) {
63
- server.setActiveToolCall(null)
64
59
  return { content: [{ type: 'text', text: `Document ${docId} not found` }] }
65
60
  }
66
61
 
@@ -70,7 +65,6 @@ export function registerMetaTools(mcp: McpServer, server: AbracadabraMCPServer)
70
65
  updatedAt: Date.now(),
71
66
  })
72
67
 
73
- server.setActiveToolCall(null)
74
68
  return { content: [{ type: 'text', text: `Metadata updated for ${docId}` }] }
75
69
  }
76
70
  )
package/src/tools/svg.ts CHANGED
@@ -43,7 +43,6 @@ export function registerSvgTools(mcp: McpServer, server: AbracadabraMCPServer) {
43
43
 
44
44
  const cleanSvg = sanitizeSvg(svg)
45
45
  if (!cleanSvg) {
46
- server.setActiveToolCall(null)
47
46
  return {
48
47
  content: [{ type: 'text', text: 'Error: SVG markup was empty or entirely stripped by sanitizer.' }],
49
48
  isError: true,
@@ -66,13 +65,11 @@ export function registerSvgTools(mcp: McpServer, server: AbracadabraMCPServer) {
66
65
  })
67
66
 
68
67
  server.setFocusedDoc(docId)
69
- server.setActiveToolCall(null)
70
68
 
71
69
  return {
72
70
  content: [{ type: 'text', text: `SVG inserted into document ${docId}${title ? ` ("${title}")` : ''}` }],
73
71
  }
74
72
  } catch (error: any) {
75
- server.setActiveToolCall(null)
76
73
  return {
77
74
  content: [{ type: 'text', text: `Error writing SVG: ${error.message}` }],
78
75
  isError: true,
package/src/tools/tree.ts CHANGED
@@ -6,6 +6,7 @@ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
6
6
  import { z } from 'zod'
7
7
  import type { AbracadabraMCPServer } from '../server.ts'
8
8
  import type { TreeEntry, PageMeta } from '../converters/types.ts'
9
+ import { PAGE_TYPES, TYPE_ALIASES, resolvePageType } from '../converters/page-types.ts'
9
10
 
10
11
  /**
11
12
  * Normalize a document ID so the hub/root doc ID is treated as the tree root (null).
@@ -89,7 +90,6 @@ export function registerTreeTools(mcp: McpServer, server: AbracadabraMCPServer)
89
90
  server.setActiveToolCall({ name: 'list_documents' })
90
91
  const treeMap = server.getTreeMap()
91
92
  if (!treeMap) {
92
- server.setActiveToolCall(null)
93
93
  return { content: [{ type: 'text', text: 'Not connected' }] }
94
94
  }
95
95
 
@@ -97,7 +97,6 @@ export function registerTreeTools(mcp: McpServer, server: AbracadabraMCPServer)
97
97
  const entries = readEntries(treeMap)
98
98
  const children = childrenOf(entries, targetId)
99
99
 
100
- server.setActiveToolCall(null)
101
100
  return {
102
101
  content: [{
103
102
  type: 'text',
@@ -119,7 +118,6 @@ export function registerTreeTools(mcp: McpServer, server: AbracadabraMCPServer)
119
118
  server.setActiveToolCall({ name: 'get_document_tree' })
120
119
  const treeMap = server.getTreeMap()
121
120
  if (!treeMap) {
122
- server.setActiveToolCall(null)
123
121
  return { content: [{ type: 'text', text: 'Not connected' }] }
124
122
  }
125
123
 
@@ -128,7 +126,6 @@ export function registerTreeTools(mcp: McpServer, server: AbracadabraMCPServer)
128
126
  const entries = readEntries(treeMap)
129
127
  const tree = buildTree(entries, targetId, maxDepth)
130
128
 
131
- server.setActiveToolCall(null)
132
129
  return {
133
130
  content: [{
134
131
  type: 'text',
@@ -150,7 +147,6 @@ export function registerTreeTools(mcp: McpServer, server: AbracadabraMCPServer)
150
147
  server.setActiveToolCall({ name: 'find_document', target: query })
151
148
  const treeMap = server.getTreeMap()
152
149
  if (!treeMap) {
153
- server.setActiveToolCall(null)
154
150
  return { content: [{ type: 'text', text: 'Not connected' }] }
155
151
  }
156
152
 
@@ -188,7 +184,6 @@ export function registerTreeTools(mcp: McpServer, server: AbracadabraMCPServer)
188
184
  }
189
185
  })
190
186
 
191
- server.setActiveToolCall(null)
192
187
  if (results.length === 0) {
193
188
  return {
194
189
  content: [{ type: 'text', text: `No documents found matching "${query}". Try get_document_tree to see the full hierarchy.` }],
@@ -207,8 +202,8 @@ export function registerTreeTools(mcp: McpServer, server: AbracadabraMCPServer)
207
202
  {
208
203
  parentId: z.string().optional().describe('Parent document ID. Omit for top-level pages. Use a document ID for nested/child pages.'),
209
204
  label: z.string().describe('Display name / title for the document.'),
210
- type: z.string().optional().describe('Page type — sets how this document renders. "doc" (rich text), "kanban" (columns → cards), "table" (columns → rows with custom fields), "calendar" (events with datetimeStart/End), "timeline" (epics → tasks with dateStart/End + taskProgress), "checklist" (tasks with checked/priority, unlimited nesting), "outline" (nested items, unlimited depth), "gallery" (visual grid with covers/ratings), "map" (markers/lines with geoLat/geoLng), "graph" (force-directed knowledge graph), "dashboard" (positioned widgets with deskX/deskY/deskMode), "spatial" (3D scene with spShape/spX/spY/spZ), "media" (audio/video player with playlists), "slides" (presentation with transitions), "chart" (bar/line/donut/treemap from data points or aggregation), "sheets" (spreadsheet with formulas and formatting), "overview" (space home — activity and stats), "call" (video call room, no children). Omit to inherit parent view. Only set on the parent page, NEVER on child items.'),
211
- meta: z.record(z.unknown()).optional().describe('Initial metadata (PageMeta fields: color as hex string, icon as Lucide kebab-case name like "star"/"code-2"/"users" — never emoji, dateStart, dateEnd, priority 0-4, tags array, etc). Omit icon entirely to use page type default.'),
205
+ type: z.string().optional().describe('Page type — sets how this document renders. Core types (always available): "doc" (rich text), "kanban" (columns → cards), "table" (columns → rows, positional), "calendar" (events with datetimeStart/End), "timeline" (epics → tasks with dateStart/End + taskProgress), "checklist" (tasks with checked/priority, unlimited nesting), "outline" (nested items, unlimited depth), "gallery" (visual grid with covers/ratings), "map" (markers/lines with geoLat/geoLng), "graph" (force-directed knowledge graph), "dashboard" (positioned widgets with deskX/deskY/deskMode), "slides" (slides sub-slides with transitions), "chart" (bar/stacked bar/line/donut/treemap from data points or aggregation), "sheets" (spreadsheet with formulas and formatting), "overview" (space home — activity and stats), "call" (video call room, no children). Plugin types (require plugin enabled on the server): "spatial" (3D scene with spShape/spX/spY/spZ + universal color, plugin: spatial), "media" (audio/video player with playlists, plugin: media), "coder" (collaborative multi-file coding env with fileType/entry, plugin: coder). Alias: "desktop" → "dashboard". Omit to inherit parent view. Only set on the parent page, NEVER on child items.'),
206
+ meta: z.record(z.string(), z.unknown()).optional().describe('Initial metadata (PageMeta fields: color as hex string, icon as Lucide kebab-case name like "star"/"code-2"/"users" — never emoji, dateStart, dateEnd, priority 0-4, tags array, etc). Omit icon entirely to use page type default.'),
212
207
  },
213
208
  async ({ parentId, label, type, meta }) => {
214
209
  server.setAutoStatus('creating')
@@ -217,7 +212,6 @@ export function registerTreeTools(mcp: McpServer, server: AbracadabraMCPServer)
217
212
  const treeMap = server.getTreeMap()
218
213
  const rootDoc = server.rootDocument
219
214
  if (!treeMap || !rootDoc) {
220
- server.setActiveToolCall(null)
221
215
  return { content: [{ type: 'text', text: 'Not connected' }] }
222
216
  }
223
217
 
@@ -237,7 +231,6 @@ export function registerTreeTools(mcp: McpServer, server: AbracadabraMCPServer)
237
231
  })
238
232
 
239
233
  server.setFocusedDoc(id)
240
- server.setActiveToolCall(null)
241
234
 
242
235
  return {
243
236
  content: [{
@@ -260,19 +253,16 @@ export function registerTreeTools(mcp: McpServer, server: AbracadabraMCPServer)
260
253
  server.setActiveToolCall({ name: 'rename_document', target: id })
261
254
  const treeMap = server.getTreeMap()
262
255
  if (!treeMap) {
263
- server.setActiveToolCall(null)
264
256
  return { content: [{ type: 'text', text: 'Not connected' }] }
265
257
  }
266
258
 
267
259
  const raw = treeMap.get(id)
268
260
  if (!raw) {
269
- server.setActiveToolCall(null)
270
261
  return { content: [{ type: 'text', text: `Document ${id} not found` }] }
271
262
  }
272
263
 
273
264
  const entry = toPlain(raw)
274
265
  treeMap.set(id, { ...entry, label, updatedAt: Date.now() })
275
- server.setActiveToolCall(null)
276
266
  return { content: [{ type: 'text', text: `Renamed to "${label}"` }] }
277
267
  }
278
268
  )
@@ -290,13 +280,11 @@ export function registerTreeTools(mcp: McpServer, server: AbracadabraMCPServer)
290
280
  server.setActiveToolCall({ name: 'move_document', target: id })
291
281
  const treeMap = server.getTreeMap()
292
282
  if (!treeMap) {
293
- server.setActiveToolCall(null)
294
283
  return { content: [{ type: 'text', text: 'Not connected' }] }
295
284
  }
296
285
 
297
286
  const raw = treeMap.get(id)
298
287
  if (!raw) {
299
- server.setActiveToolCall(null)
300
288
  return { content: [{ type: 'text', text: `Document ${id} not found` }] }
301
289
  }
302
290
 
@@ -307,7 +295,6 @@ export function registerTreeTools(mcp: McpServer, server: AbracadabraMCPServer)
307
295
  order: order ?? Date.now(),
308
296
  updatedAt: Date.now(),
309
297
  })
310
- server.setActiveToolCall(null)
311
298
  return { content: [{ type: 'text', text: `Moved ${id} to parent ${newParentId}` }] }
312
299
  }
313
300
  )
@@ -325,7 +312,6 @@ export function registerTreeTools(mcp: McpServer, server: AbracadabraMCPServer)
325
312
  const trashMap = server.getTrashMap()
326
313
  const rootDoc = server.rootDocument
327
314
  if (!treeMap || !trashMap || !rootDoc) {
328
- server.setActiveToolCall(null)
329
315
  return { content: [{ type: 'text', text: 'Not connected' }] }
330
316
  }
331
317
 
@@ -350,7 +336,6 @@ export function registerTreeTools(mcp: McpServer, server: AbracadabraMCPServer)
350
336
  }
351
337
  })
352
338
 
353
- server.setActiveToolCall(null)
354
339
  return { content: [{ type: 'text', text: `Deleted ${toDelete.length} document(s)` }] }
355
340
  }
356
341
  )
@@ -360,26 +345,23 @@ export function registerTreeTools(mcp: McpServer, server: AbracadabraMCPServer)
360
345
  'Change the page type view of a document (data is preserved).',
361
346
  {
362
347
  id: z.string().describe('Document ID.'),
363
- type: z.string().describe('New page type: "doc", "kanban", "table", "calendar", "timeline", "checklist", "outline", "gallery", "map", "graph", "dashboard", "spatial", "media", "slides", "chart", "sheets", "overview", "call".'),
348
+ type: z.string().describe('New page type. Core: "doc", "kanban", "table", "calendar", "timeline", "checklist", "outline", "gallery", "map", "graph", "dashboard", "slides", "chart", "sheets", "overview", "call". Plugin (require plugin enabled): "spatial", "media", "coder". Switching preserves the tree — children, labels, and meta are all retained; only the view changes.'),
364
349
  },
365
350
  async ({ id, type }) => {
366
351
  server.setAutoStatus('writing')
367
352
  server.setActiveToolCall({ name: 'change_document_type', target: id })
368
353
  const treeMap = server.getTreeMap()
369
354
  if (!treeMap) {
370
- server.setActiveToolCall(null)
371
355
  return { content: [{ type: 'text', text: 'Not connected' }] }
372
356
  }
373
357
 
374
358
  const raw = treeMap.get(id)
375
359
  if (!raw) {
376
- server.setActiveToolCall(null)
377
360
  return { content: [{ type: 'text', text: `Document ${id} not found` }] }
378
361
  }
379
362
 
380
363
  const entry = toPlain(raw)
381
364
  treeMap.set(id, { ...entry, type, updatedAt: Date.now() })
382
- server.setActiveToolCall(null)
383
365
  return { content: [{ type: 'text', text: `Changed type to "${type}"` }] }
384
366
  }
385
367
  )
@@ -442,4 +424,28 @@ export function registerTreeTools(mcp: McpServer, server: AbracadabraMCPServer)
442
424
  }
443
425
  }
444
426
  )
427
+
428
+ mcp.tool(
429
+ 'list_page_types',
430
+ 'Enumerate all known Abracadabra page types with their metadata schemas. Returns an array of { key, label, icon, description, core, plugin, supportsChildren, childLabel, grandchildLabel, defaultDepth, metaSchema, defaultMetaFields }. `metaSchema` describes fields that apply to DESCENDANTS (children, grandchildren, ...) of a page of this type. `defaultMetaFields` are view-config fields on the page doc itself. Plugin types (core: false) require the named plugin to be enabled on the server. Use this to discover what meta keys a given renderer supports instead of guessing.',
431
+ {
432
+ key: z.string().optional().describe('Filter to a single type by key (e.g. "kanban", "calendar"). Aliases are resolved (e.g. "desktop" → "dashboard"). Omit to list all types.'),
433
+ },
434
+ async ({ key }) => {
435
+ if (key) {
436
+ const resolved = resolvePageType(key)
437
+ if (!resolved) {
438
+ return { content: [{ type: 'text', text: `Unknown page type "${key}". Call list_page_types with no args to see all types.` }] }
439
+ }
440
+ return { content: [{ type: 'text', text: JSON.stringify(resolved, null, 2) }] }
441
+ }
442
+ const all = Object.values(PAGE_TYPES)
443
+ return {
444
+ content: [{
445
+ type: 'text',
446
+ text: JSON.stringify({ types: all, aliases: TYPE_ALIASES }, null, 2),
447
+ }],
448
+ }
449
+ }
450
+ )
445
451
  }