@abraca/mcp 1.0.18 → 1.0.21

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abraca/mcp",
3
- "version": "1.0.18",
3
+ "version": "1.0.21",
4
4
  "description": "MCP server for Abracadabra — AI agent collaboration on CRDT documents",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/index.ts CHANGED
@@ -48,6 +48,13 @@ async function main() {
48
48
  capabilities: { experimental: { 'claude/channel': {} } },
49
49
  instructions: `Abracadabra is a CRDT collaboration platform where everything is a document in a tree.
50
50
 
51
+ ## CRITICAL: Responding to Channel Events
52
+ The user CANNOT see your plain text output — they only see messages sent via MCP tools. When you receive a channel event:
53
+ - **Chat messages**: Use send_chat_message with the SAME channel for ALL replies AND progress updates.
54
+ - **AI tasks**: Use reply tool for the final response; use send_chat_message for progress updates.
55
+ - For multi-step work, send brief status updates via send_chat_message so the user sees progress.
56
+ - NEVER output plain text as a response or status update — it is invisible to the user.
57
+
51
58
  ## Quick Start Workflow
52
59
  1. list_spaces → note the active space's doc_id (this is the hub/root document)
53
60
  2. get_document_tree(rootId: hubDocId) → see the FULL hierarchy before doing anything
@@ -57,12 +64,12 @@ async function main() {
57
64
  ## Key Concepts
58
65
  - Documents form a tree. A kanban board's columns are child documents; cards are grandchildren.
59
66
  - A document's label IS its display name everywhere. Children ARE the content (not just the body text).
60
- - Page types (doc, kanban, table, calendar, timeline, checklist, outline, gallery, map, graph, dashboard, spatial, media, mindmap, etc.) are views over the SAME tree — switching types preserves data.
67
+ - Page types are views over the SAME tree — switching types preserves data.
61
68
  - An empty markdown body does NOT mean empty content — always check the children array.
62
69
  - Use ![[docId]] in content to embed another document, or [[docId|label]] for inline links.
63
70
 
64
71
  ## Finding Documents
65
- - list_documents only shows ONE level of children. If you don't find what you need, use find_document to search the entire tree by name, or get_document_tree to see the full hierarchy.
72
+ - list_documents only shows ONE level of children. Use find_document to search by name, or get_document_tree to see the full hierarchy.
66
73
  - NEVER conclude a document doesn't exist after only checking root-level documents.
67
74
 
68
75
  ## Rules
@@ -72,12 +79,6 @@ async function main() {
72
79
  - Never rename or write content to the hub document itself.
73
80
  - Use universal meta keys (color, icon, dateStart) — never prefix with page type names.
74
81
 
75
- ## Channel Events
76
- Events from the abracadabra channel arrive as <channel source="abracadabra" ...>. They may be:
77
- - ai:task events from human users (includes sender, doc_id context)
78
- - chat messages from the platform chat system (includes channel, sender, sender_id)
79
- When you receive a channel event, read it, do the work using your tools, and reply using the reply tool (for document responses) or send_chat_message (for chat responses).
80
-
81
82
  ## Full Reference
82
83
  Read the resource at abracadabra://agent-guide for the complete guide covering page type schemas, metadata reference, awareness/presence, content structure, and detailed examples.`,
83
84
  },
package/src/server.ts CHANGED
@@ -352,6 +352,7 @@ export class AbracadabraMCPServer {
352
352
  method: 'notifications/claude/channel',
353
353
  params: {
354
354
  content: task.text,
355
+ instructions: `You MUST use the reply tool with doc_id="${task.docId ?? ''}" and task_id="${task.id}" for your final response. The user CANNOT see plain text output; they only see replies sent via MCP tools. For progress updates during multi-step work, use send_chat_message with channel="group:${task.docId ?? ''}" (e.g. "Looking into that..." or "Found it, writing up results..."). Never output plain text as a substitute for MCP tools.`,
355
356
  meta: {
356
357
  source: 'abracadabra',
357
358
  type: 'ai_task',
@@ -433,6 +434,7 @@ export class AbracadabraMCPServer {
433
434
  method: 'notifications/claude/channel',
434
435
  params: {
435
436
  content: data.content ?? '',
437
+ instructions: `You MUST use send_chat_message with channel="${channel ?? ''}" for ALL responses — both progress updates and final answers. The user CANNOT see plain text output; they only see messages sent via send_chat_message. When doing multi-step work, send brief status updates via send_chat_message (e.g. "Looking into that..." or "Found it, writing up results...") so the user knows you're working. Never output plain text as a substitute for send_chat_message.`,
436
438
  meta: {
437
439
  source: 'abracadabra',
438
440
  type: 'chat_message',
package/src/tools/meta.ts CHANGED
@@ -13,12 +13,21 @@ export function registerMetaTools(mcp: McpServer, server: AbracadabraMCPServer)
13
13
  docId: z.string().describe('Document ID.'),
14
14
  },
15
15
  async ({ docId }) => {
16
+ server.setAutoStatus('reading', docId)
17
+ server.setActiveToolCall({ name: 'get_metadata', target: docId })
16
18
  const treeMap = server.getTreeMap()
17
- if (!treeMap) return { content: [{ type: 'text', text: 'Not connected' }] }
19
+ if (!treeMap) {
20
+ server.setActiveToolCall(null)
21
+ return { content: [{ type: 'text', text: 'Not connected' }] }
22
+ }
18
23
 
19
24
  const entry = treeMap.get(docId)
20
- if (!entry) return { content: [{ type: 'text', text: `Document ${docId} not found` }] }
25
+ if (!entry) {
26
+ server.setActiveToolCall(null)
27
+ return { content: [{ type: 'text', text: `Document ${docId} not found` }] }
28
+ }
21
29
 
30
+ server.setActiveToolCall(null)
22
31
  return {
23
32
  content: [{
24
33
  type: 'text',
@@ -41,11 +50,19 @@ export function registerMetaTools(mcp: McpServer, server: AbracadabraMCPServer)
41
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"). Renderer config (on the page doc itself): kanbanColumnWidth, galleryColumns, galleryAspect, calendarView, calendarWeekStart, tableMode, showRefEdges. Set a key to null to clear it.'),
42
51
  },
43
52
  async ({ docId, meta }) => {
53
+ server.setAutoStatus('writing', docId)
54
+ server.setActiveToolCall({ name: 'update_metadata', target: docId })
44
55
  const treeMap = server.getTreeMap()
45
- if (!treeMap) return { content: [{ type: 'text', text: 'Not connected' }] }
56
+ if (!treeMap) {
57
+ server.setActiveToolCall(null)
58
+ return { content: [{ type: 'text', text: 'Not connected' }] }
59
+ }
46
60
 
47
61
  const entry = treeMap.get(docId)
48
- if (!entry) return { content: [{ type: 'text', text: `Document ${docId} not found` }] }
62
+ if (!entry) {
63
+ server.setActiveToolCall(null)
64
+ return { content: [{ type: 'text', text: `Document ${docId} not found` }] }
65
+ }
49
66
 
50
67
  treeMap.set(docId, {
51
68
  ...entry,
@@ -53,6 +70,7 @@ export function registerMetaTools(mcp: McpServer, server: AbracadabraMCPServer)
53
70
  updatedAt: Date.now(),
54
71
  })
55
72
 
73
+ server.setActiveToolCall(null)
56
74
  return { content: [{ type: 'text', text: `Metadata updated for ${docId}` }] }
57
75
  }
58
76
  )
package/src/tools/tree.ts CHANGED
@@ -78,13 +78,19 @@ export function registerTreeTools(mcp: McpServer, server: AbracadabraMCPServer)
78
78
  'List direct children of a document (defaults to root). Returns id, label, type, meta, order. NOTE: Only returns ONE level. Use find_document to search by name across the full tree, or get_document_tree to see the complete hierarchy.',
79
79
  { parentId: z.string().optional().describe('Parent document ID. Omit for root-level documents.') },
80
80
  async ({ parentId }) => {
81
+ server.setAutoStatus('reading')
82
+ server.setActiveToolCall({ name: 'list_documents' })
81
83
  const treeMap = server.getTreeMap()
82
- if (!treeMap) return { content: [{ type: 'text', text: 'Not connected' }] }
84
+ if (!treeMap) {
85
+ server.setActiveToolCall(null)
86
+ return { content: [{ type: 'text', text: 'Not connected' }] }
87
+ }
83
88
 
84
89
  const targetId = normalizeRootId(parentId, server)
85
90
  const entries = readEntries(treeMap)
86
91
  const children = childrenOf(entries, targetId)
87
92
 
93
+ server.setActiveToolCall(null)
88
94
  return {
89
95
  content: [{
90
96
  type: 'text',
@@ -102,14 +108,20 @@ export function registerTreeTools(mcp: McpServer, server: AbracadabraMCPServer)
102
108
  depth: z.number().optional().describe('Maximum depth to traverse. Default 3. Use -1 for unlimited.'),
103
109
  },
104
110
  async ({ rootId, depth }) => {
111
+ server.setAutoStatus('reading')
112
+ server.setActiveToolCall({ name: 'get_document_tree' })
105
113
  const treeMap = server.getTreeMap()
106
- if (!treeMap) return { content: [{ type: 'text', text: 'Not connected' }] }
114
+ if (!treeMap) {
115
+ server.setActiveToolCall(null)
116
+ return { content: [{ type: 'text', text: 'Not connected' }] }
117
+ }
107
118
 
108
119
  const targetId = normalizeRootId(rootId, server)
109
120
  const maxDepth = depth ?? 3
110
121
  const entries = readEntries(treeMap)
111
122
  const tree = buildTree(entries, targetId, maxDepth)
112
123
 
124
+ server.setActiveToolCall(null)
113
125
  return {
114
126
  content: [{
115
127
  type: 'text',
@@ -127,8 +139,13 @@ export function registerTreeTools(mcp: McpServer, server: AbracadabraMCPServer)
127
139
  rootId: z.string().optional().describe('Restrict search to descendants of this document. Omit to search the entire tree.'),
128
140
  },
129
141
  async ({ query, rootId }) => {
142
+ server.setAutoStatus('searching')
143
+ server.setActiveToolCall({ name: 'find_document', target: query })
130
144
  const treeMap = server.getTreeMap()
131
- if (!treeMap) return { content: [{ type: 'text', text: 'Not connected' }] }
145
+ if (!treeMap) {
146
+ server.setActiveToolCall(null)
147
+ return { content: [{ type: 'text', text: 'Not connected' }] }
148
+ }
132
149
 
133
150
  const entries = readEntries(treeMap)
134
151
  const lowerQuery = query.toLowerCase()
@@ -164,6 +181,7 @@ export function registerTreeTools(mcp: McpServer, server: AbracadabraMCPServer)
164
181
  }
165
182
  })
166
183
 
184
+ server.setActiveToolCall(null)
167
185
  if (results.length === 0) {
168
186
  return {
169
187
  content: [{ type: 'text', text: `No documents found matching "${query}". Try get_document_tree to see the full hierarchy.` }],
@@ -231,13 +249,22 @@ export function registerTreeTools(mcp: McpServer, server: AbracadabraMCPServer)
231
249
  label: z.string().describe('New display name.'),
232
250
  },
233
251
  async ({ id, label }) => {
252
+ server.setAutoStatus('writing')
253
+ server.setActiveToolCall({ name: 'rename_document', target: id })
234
254
  const treeMap = server.getTreeMap()
235
- if (!treeMap) return { content: [{ type: 'text', text: 'Not connected' }] }
255
+ if (!treeMap) {
256
+ server.setActiveToolCall(null)
257
+ return { content: [{ type: 'text', text: 'Not connected' }] }
258
+ }
236
259
 
237
260
  const entry = treeMap.get(id)
238
- if (!entry) return { content: [{ type: 'text', text: `Document ${id} not found` }] }
261
+ if (!entry) {
262
+ server.setActiveToolCall(null)
263
+ return { content: [{ type: 'text', text: `Document ${id} not found` }] }
264
+ }
239
265
 
240
266
  treeMap.set(id, { ...entry, label, updatedAt: Date.now() })
267
+ server.setActiveToolCall(null)
241
268
  return { content: [{ type: 'text', text: `Renamed to "${label}"` }] }
242
269
  }
243
270
  )
@@ -251,11 +278,19 @@ export function registerTreeTools(mcp: McpServer, server: AbracadabraMCPServer)
251
278
  order: z.number().optional().describe('New sort order. Defaults to Date.now() (append to end).'),
252
279
  },
253
280
  async ({ id, newParentId, order }) => {
281
+ server.setAutoStatus('writing')
282
+ server.setActiveToolCall({ name: 'move_document', target: id })
254
283
  const treeMap = server.getTreeMap()
255
- if (!treeMap) return { content: [{ type: 'text', text: 'Not connected' }] }
284
+ if (!treeMap) {
285
+ server.setActiveToolCall(null)
286
+ return { content: [{ type: 'text', text: 'Not connected' }] }
287
+ }
256
288
 
257
289
  const entry = treeMap.get(id)
258
- if (!entry) return { content: [{ type: 'text', text: `Document ${id} not found` }] }
290
+ if (!entry) {
291
+ server.setActiveToolCall(null)
292
+ return { content: [{ type: 'text', text: `Document ${id} not found` }] }
293
+ }
259
294
 
260
295
  treeMap.set(id, {
261
296
  ...entry,
@@ -263,6 +298,7 @@ export function registerTreeTools(mcp: McpServer, server: AbracadabraMCPServer)
263
298
  order: order ?? Date.now(),
264
299
  updatedAt: Date.now(),
265
300
  })
301
+ server.setActiveToolCall(null)
266
302
  return { content: [{ type: 'text', text: `Moved ${id} to parent ${newParentId}` }] }
267
303
  }
268
304
  )
@@ -274,10 +310,15 @@ export function registerTreeTools(mcp: McpServer, server: AbracadabraMCPServer)
274
310
  id: z.string().describe('Document ID to delete.'),
275
311
  },
276
312
  async ({ id }) => {
313
+ server.setAutoStatus('writing')
314
+ server.setActiveToolCall({ name: 'delete_document', target: id })
277
315
  const treeMap = server.getTreeMap()
278
316
  const trashMap = server.getTrashMap()
279
317
  const rootDoc = server.rootDocument
280
- if (!treeMap || !trashMap || !rootDoc) return { content: [{ type: 'text', text: 'Not connected' }] }
318
+ if (!treeMap || !trashMap || !rootDoc) {
319
+ server.setActiveToolCall(null)
320
+ return { content: [{ type: 'text', text: 'Not connected' }] }
321
+ }
281
322
 
282
323
  const entries = readEntries(treeMap)
283
324
  const toDelete = [id, ...descendantsOf(entries, id).map(e => e.id)]
@@ -299,6 +340,7 @@ export function registerTreeTools(mcp: McpServer, server: AbracadabraMCPServer)
299
340
  }
300
341
  })
301
342
 
343
+ server.setActiveToolCall(null)
302
344
  return { content: [{ type: 'text', text: `Deleted ${toDelete.length} document(s)` }] }
303
345
  }
304
346
  )
@@ -311,13 +353,22 @@ export function registerTreeTools(mcp: McpServer, server: AbracadabraMCPServer)
311
353
  type: z.string().describe('New page type (e.g. "doc", "kanban", "table", "calendar", "outline", "gallery", "slides", "timeline", "whiteboard", "map", "dashboard", "mindmap", "graph").'),
312
354
  },
313
355
  async ({ id, type }) => {
356
+ server.setAutoStatus('writing')
357
+ server.setActiveToolCall({ name: 'change_document_type', target: id })
314
358
  const treeMap = server.getTreeMap()
315
- if (!treeMap) return { content: [{ type: 'text', text: 'Not connected' }] }
359
+ if (!treeMap) {
360
+ server.setActiveToolCall(null)
361
+ return { content: [{ type: 'text', text: 'Not connected' }] }
362
+ }
316
363
 
317
364
  const entry = treeMap.get(id)
318
- if (!entry) return { content: [{ type: 'text', text: `Document ${id} not found` }] }
365
+ if (!entry) {
366
+ server.setActiveToolCall(null)
367
+ return { content: [{ type: 'text', text: `Document ${id} not found` }] }
368
+ }
319
369
 
320
370
  treeMap.set(id, { ...entry, type, updatedAt: Date.now() })
371
+ server.setActiveToolCall(null)
321
372
  return { content: [{ type: 'text', text: `Changed type to "${type}"` }] }
322
373
  }
323
374
  )