@abraca/mcp 1.0.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.
@@ -0,0 +1,324 @@
1
+ /**
2
+ * Static resource: AI agent guide for working with Abracadabra documents.
3
+ */
4
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
5
+
6
+ const AGENT_GUIDE = `# Abracadabra AI Agent Guide
7
+
8
+ ## Core Principle: "Everything is a Document"
9
+
10
+ Every piece of visible data in Abracadabra — a calendar event, a kanban card, a table cell, a map marker — is a **document** in the tree. The document's **label** (name) IS the displayed value. Always.
11
+
12
+ - A column's name IS the column header
13
+ - A card's name IS the card title
14
+ - A cell's name IS the cell content
15
+ - An event's name IS the event title
16
+
17
+ **Page types are views over the same document hierarchy**, not isolated data stores. Switching a page from Kanban to Table to Outline works without migration — the tree doesn't change, only the rendering.
18
+
19
+ ---
20
+
21
+ ## Getting Oriented in a Space
22
+
23
+ Before creating any documents, always call \`list_spaces\` to understand the current workspace. Each space has a \`doc_id\` — this is the **hub document** (the space root). All user-created documents must be direct or indirect children of this hub doc.
24
+
25
+ \`\`\`
26
+ list_spaces → pick the active space → note its doc_id (= hub doc ID)
27
+ create_document(parentId: hubDocId, ...) ← top-level pages go here
28
+ \`\`\`
29
+
30
+ The hub doc itself is a system document — do not rename, move, or write content to it. It is just a container.
31
+
32
+ If you are adding content to an existing space, call \`get_document_tree(rootId: hubDocId)\` first to understand what already exists before creating anything.
33
+
34
+ ---
35
+
36
+ ## Page Types Reference
37
+
38
+ | Type | Children Are | Grandchildren Are | Depth Limit |
39
+ |------|-------------|-------------------|-------------|
40
+ | **doc** | Sub-documents | Sub-sub-documents | Unlimited |
41
+ | **kanban** | Columns | Cards | 2 |
42
+ | **table** | Columns | Cells (positional rows) | 2 |
43
+ | **calendar** | Events | — | 1 |
44
+ | **timeline** | Epics | Tasks | 2 |
45
+ | **outline** | Items | Sub-items | Unlimited |
46
+ | **mindmap** | Central nodes | Branches | Unlimited |
47
+ | **graph** | Nodes | — | 1 |
48
+ | **gallery** | Items | — | 1 |
49
+ | **slides** | Slides | — | 1 |
50
+ | **whiteboard** | Objects | — | 1 |
51
+ | **map** | Markers/Lines | Points (for lines) | 2 |
52
+ | **desktop** | Items | — | 1 |
53
+ | **call** | — | — | 0 |
54
+ | **game** | — | — | 0 |
55
+
56
+ ---
57
+
58
+ ## Document Tree Operations
59
+
60
+ ### Creating Well-Structured Hierarchies
61
+
62
+ **Kanban board:**
63
+ 1. Create the kanban document: \`create_document(parentId, "Project Board", "kanban")\`
64
+ 2. Create columns: \`create_document(boardId, "To Do")\`, \`create_document(boardId, "In Progress")\`, \`create_document(boardId, "Done")\`
65
+ 3. Create cards under columns: \`create_document(toDoColumnId, "Fix bug #123")\`
66
+
67
+ **Calendar with events:**
68
+ 1. Create calendar: \`create_document(parentId, "Team Calendar", "calendar")\`
69
+ 2. Create events: \`create_document(calendarId, "Sprint Planning")\`
70
+ 3. Set event times: \`update_metadata(eventId, { datetimeStart: "2026-03-20T10:00:00Z", datetimeEnd: "2026-03-20T11:00:00Z" })\`
71
+
72
+ **Timeline with tasks:**
73
+ 1. Create timeline: \`create_document(parentId, "Q1 Roadmap", "timeline")\`
74
+ 2. Create epics: \`create_document(timelineId, "Auth Rewrite")\`
75
+ 3. Create tasks: \`create_document(epicId, "Implement JWT refresh")\`
76
+ 4. Set dates/progress: \`update_metadata(taskId, { dateStart: "2026-03-01", dateEnd: "2026-03-15", taskProgress: 50 })\`
77
+
78
+ **Table with data:**
79
+ 1. Create table: \`create_document(parentId, "Contacts", "table")\`
80
+ 2. Create columns: \`create_document(tableId, "Name")\`, \`create_document(tableId, "Email")\`, \`create_document(tableId, "Phone")\`
81
+ 3. Create cells (children of columns): \`create_document(nameColId, "Alice")\`, \`create_document(emailColId, "alice@example.com")\`
82
+ 4. Rows are positional — the Nth child of each column forms row N
83
+
84
+ ---
85
+
86
+ ## Metadata Reference
87
+
88
+ ### Universal Keys (use these — never invent prefixed variants)
89
+
90
+ | Key | Type | Meaning |
91
+ |-----|------|---------|
92
+ | \`color\` | string | Hex/CSS color (e.g. "#6366f1") |
93
+ | \`icon\` | string | Lucide icon name in kebab-case (e.g. "star", "code-2", "users", "calendar-days", "book-open"). **Never emoji.** Omit to use the page type's default icon. |
94
+ | \`datetimeStart\` / \`datetimeEnd\` | string | ISO datetime with time |
95
+ | \`allDay\` | boolean | Whether datetime is all-day |
96
+ | \`dateStart\` / \`dateEnd\` | string | ISO date-only (e.g. "2026-03-20") |
97
+ | \`timeStart\` / \`timeEnd\` | string | HH:MM time |
98
+ | \`tags\` | string[] | Tag labels |
99
+ | \`checked\` | boolean | Done/checked state |
100
+ | \`priority\` | number | 0=none, 1=low, 2=medium, 3=high, 4=urgent |
101
+ | \`status\` | string | Status string |
102
+ | \`rating\` | number | 0–5 stars |
103
+ | \`url\` | string | URL |
104
+ | \`email\` | string | Email address |
105
+ | \`phone\` | string | Phone number |
106
+ | \`number\` | number | Generic numeric value |
107
+ | \`unit\` | string | Unit label for \`number\` |
108
+ | \`subtitle\` | string | Secondary text |
109
+ | \`note\` | string | Free-text note |
110
+ | \`taskProgress\` | number | 0–100 progress (timeline tasks) |
111
+
112
+ ### Type-Specific Meta Schemas
113
+
114
+ **Calendar events** require: \`datetimeStart\`, \`datetimeEnd\`, optionally \`allDay\`, \`color\`
115
+
116
+ **Timeline tasks** require: \`dateStart\`, \`dateEnd\`, optionally \`taskProgress\` (0–100), \`color\`
117
+
118
+ **Map markers** require: \`geoLat\`, \`geoLng\`, \`geoType\` ("marker"|"line"|"measure"), optionally \`icon\`, \`color\`
119
+
120
+ ---
121
+
122
+ ## Content Structure
123
+
124
+ Documents use a TipTap editor schema with these top-level elements:
125
+ - **documentHeader** — The document title (H1)
126
+ - **documentMeta** — Metadata display block (skip when reading/writing)
127
+ - **Body blocks** — paragraphs, headings (H2-H6), lists, code blocks, tables, images, and custom components
128
+
129
+ ### Supported Markdown Elements
130
+ - Headings (# through ######)
131
+ - Paragraphs with **bold**, *italic*, ~~strikethrough~~, \`code\`, [links](url)
132
+ - Bullet lists, ordered lists, task lists
133
+ - Code blocks with language
134
+ - Blockquotes
135
+ - Tables (pipe syntax)
136
+ - Horizontal rules
137
+ - Images
138
+ - MDC components: callout, collapsible, steps, card, accordion, tabs, code-group, etc.
139
+
140
+ ### Writing Content with Frontmatter
141
+
142
+ You can set document title and metadata via YAML frontmatter:
143
+
144
+ \`\`\`markdown
145
+ ---
146
+ title: My Document
147
+ tags: [important, review]
148
+ color: "#6366f1"
149
+ priority: high
150
+ ---
151
+
152
+ Content goes here...
153
+ \`\`\`
154
+
155
+ ---
156
+
157
+ ## Awareness / Presence
158
+
159
+ When you connect, you appear in the awareness system alongside human users. Presence runs at two levels:
160
+ - **Root awareness**: your name, color, and which document you're currently viewing (\`docId\`)
161
+ - **Child doc awareness**: per-document presence including TipTap cursor position and renderer-specific state
162
+
163
+ ### What happens automatically
164
+
165
+ You do **not** need to call \`set_presence\` after reading or writing content — it's done for you:
166
+
167
+ | Action | Automatic effect |
168
+ |---|---|
169
+ | \`read_document\` | Root \`docId\` updated; TipTap cursor placed at start of document |
170
+ | \`write_document\` | Root \`docId\` updated; TipTap cursor placed at end of written content |
171
+
172
+ The TipTap cursor (\`anchor\`/\`head\` as \`Y.RelativePosition\`) makes your colored caret with name label appear inline in the document editor for human collaborators.
173
+
174
+ ### set_presence — manual overrides only
175
+
176
+ Use \`set_presence\` when you want to:
177
+ - Override \`status\` to communicate intent ("thinking", "reviewing", "writing")
178
+ - Explicitly update \`docId\` when browsing the document tree without reading content
179
+
180
+ ### set_doc_awareness — renderer-specific item presence
181
+
182
+ For structured renderers (kanban, table, calendar, etc.), use \`set_doc_awareness(docId, fields)\` to broadcast which item you are interacting with. This is visible as highlighted cards, cells, events, etc. in the dashboard.
183
+
184
+ Always clear fields when done by setting them to \`null\`.
185
+
186
+ | Renderer | Key | Value | When to set |
187
+ |---|---|---|---|
188
+ | **Kanban** | \`kanban:hovering\` | cardId | Inspecting or about to edit a card |
189
+ | **Kanban** | \`kanban:dragging\` | \`{ cardId, toColumnId }\` | Moving a card between columns |
190
+ | **Table** | \`table:editing\` | \`{ rowIdx, fieldId }\` | Editing a cell |
191
+ | **Calendar** | \`calendar:focused\` | eventId | Interacting with an event |
192
+ | **Calendar** | \`calendar:viewing\` | "YYYY-MM" | The month/week you're looking at |
193
+ | **Outline** | \`outline:editing\` | nodeId | Editing an outline node |
194
+ | **Outline** | \`outline:selected\` | nodeId | Node selected/hovered |
195
+ | **Slides** | \`slides:viewing\` | slideId | The slide you're currently on |
196
+ | **Gallery** | \`gallery:focused\` | itemId | Item hovered/selected |
197
+ | **Timeline** | \`timeline:focused\` | taskId | Task selected |
198
+ | **Mindmap** | \`mindmap:focused\` | nodeId | Node selected/edited |
199
+ | **Graph** | \`graph:focused\` | nodeId | Node hovered/selected |
200
+ | **Map** | \`map:focused\` | markerId | Marker hovered/selected |
201
+ | **Doc** | \`doc:scroll\` | 0–1 number | Scroll position in document |
202
+
203
+ Example — mark a kanban card as being inspected, then clear on completion:
204
+ \`\`\`
205
+ set_doc_awareness(boardDocId, { "kanban:hovering": cardId })
206
+ // ... do work ...
207
+ set_doc_awareness(boardDocId, { "kanban:hovering": null })
208
+ \`\`\`
209
+
210
+ ---
211
+
212
+ ## Receiving Instructions from Humans
213
+
214
+ Three ways humans can send you tasks:
215
+
216
+ **Channel events (preferred):** When running in channel mode, \`ai:task\` events from
217
+ human users arrive as \`<channel source="abracadabra" ...>\` notifications directly in
218
+ your session. These include the sender name, task ID, and document context. Use the
219
+ \`reply\` tool to send your response back — it creates a reply document and signals
220
+ task completion.
221
+
222
+ **Chat document watching:** Use \`watch_chat\` to observe a document for new messages.
223
+ Changes are pushed as channel notifications in real time. Reply using the \`reply\` tool
224
+ with the chat document's ID.
225
+
226
+ **Polling (fallback):** Call \`poll_inbox\` to read the "AI Inbox" document.
227
+ Act on any pending tasks you find there. Write your response back with \`write_document\`
228
+ or \`create_document\` under the inbox.
229
+
230
+ ### Channel Tools
231
+
232
+ | Tool | Purpose |
233
+ |------|---------|
234
+ | \`reply\` | Send a reply to Abracadabra — creates a child doc with your response. Pass \`task_id\` to signal ai:task completion. |
235
+ | \`watch_chat\` | Start/stop watching a document for chat messages. New content arrives as channel notifications. |
236
+
237
+ When you complete a task, always use \`reply\` (in channel mode) or write a response
238
+ document (in polling mode) so the human knows the work is done.
239
+
240
+ ---
241
+
242
+ ## Common Patterns
243
+
244
+ ### Populate a kanban board from a list
245
+ \`\`\`
246
+ 1. create_document → kanban parent
247
+ 2. For each column: create_document under kanban
248
+ 3. For each card: create_document under the appropriate column
249
+ 4. Optionally set metadata (color, tags, priority) on cards
250
+ \`\`\`
251
+
252
+ ### Explore a document fully (the correct way)
253
+ \`\`\`
254
+ User: "Check out the Marketing doc"
255
+
256
+ 1. read_document(marketingId)
257
+ → returns { markdown, children: [...] }
258
+ 2. The body may be empty — that does NOT mean the doc is empty.
259
+ Children ARE the content. Read every child:
260
+ read_document(childId1), read_document(childId2), ...
261
+ 3. Each child's response also includes ITS children.
262
+ Continue recursively until no children remain.
263
+ \`\`\`
264
+
265
+ **Never conclude a document is "empty" just because its markdown body is empty.
266
+ Always check and traverse the \`children\` array returned by \`read_document\`.**
267
+
268
+ ### Write rich content to a document
269
+ \`\`\`
270
+ 1. read_document to see current content (and children)
271
+ 2. write_document with markdown (mode: "replace" to overwrite, "append" to add)
272
+ 3. Include frontmatter for title/metadata if needed
273
+ \`\`\`
274
+
275
+ ### Organize documents into a hierarchy
276
+ \`\`\`
277
+ 1. get_document_tree to understand current structure
278
+ 2. create_document to add new nodes
279
+ 3. move_document to reorganize
280
+ 4. change_document_type to switch views
281
+ \`\`\`
282
+
283
+ ---
284
+
285
+ ## Rules
286
+
287
+ ### MUST:
288
+ - Call \`list_spaces\` first to find the hub doc ID before creating any documents
289
+ - Use document \`label\` as the display value — renaming a document renames it everywhere
290
+ - Create child documents for every visible item (columns, cards, events, etc.)
291
+ - Use universal meta keys (color, icon, dateStart) — never prefix with page type
292
+ - Set meaningful labels on every document you create
293
+ - Use \`get_document_tree\` to understand existing structure before adding to a space
294
+ - Traverse the full \`children\` tree when asked to "check out", "explore", or "summarize" a document
295
+
296
+ ### MUST NOT:
297
+ - Conclude a document is empty because its markdown body is empty — check children first
298
+ - Stop at the top-level doc when exploring — children and grandchildren are part of the content
299
+ - Store display values in metadata — the label IS the display value
300
+ - Use special metadata to distinguish document roles — the hierarchy does that
301
+ - Create documents just for metadata — metadata lives ON documents
302
+ - Break interchangeability — switching page types must not lose data
303
+ - Use emoji as \`icon\` values — only lowercase kebab-case Lucide icon names are valid
304
+ - Create top-level documents without first confirming the correct hub doc ID
305
+ - Rename or write content to the space hub document itself
306
+ `
307
+
308
+ export function registerAgentGuide(mcp: McpServer) {
309
+ mcp.resource(
310
+ 'agent-guide',
311
+ 'abracadabra://agent-guide',
312
+ {
313
+ description: 'Comprehensive guide for AI agents working with Abracadabra documents. Covers page types, tree operations, metadata, content structure, and best practices.',
314
+ mimeType: 'text/markdown',
315
+ },
316
+ async () => ({
317
+ contents: [{
318
+ uri: 'abracadabra://agent-guide',
319
+ text: AGENT_GUIDE,
320
+ mimeType: 'text/markdown',
321
+ }],
322
+ })
323
+ )
324
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Dynamic resource: server health + info.
3
+ */
4
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
5
+ import type { AbracadabraMCPServer } from '../server.ts'
6
+
7
+ export function registerServerInfoResource(mcp: McpServer, server: AbracadabraMCPServer) {
8
+ mcp.resource(
9
+ 'server-info',
10
+ 'abracadabra://server-info',
11
+ {
12
+ description: 'Abracadabra server info: name, version, root document ID, and health status.',
13
+ mimeType: 'application/json',
14
+ },
15
+ async () => {
16
+ let health = null
17
+ try {
18
+ health = await server.client.health()
19
+ } catch {
20
+ // health endpoint unavailable
21
+ }
22
+
23
+ const spaces = server.spaces
24
+ const activeSpace = spaces.find(s => s.doc_id === server.rootDocId) ?? null
25
+
26
+ const info = {
27
+ serverInfo: server.serverInfo,
28
+ activeSpaceDocId: server.rootDocId,
29
+ activeSpace,
30
+ spaces: spaces.length > 0 ? spaces : undefined,
31
+ health,
32
+ agent: {
33
+ name: server.agentName,
34
+ color: server.agentColor,
35
+ },
36
+ }
37
+
38
+ return {
39
+ contents: [{
40
+ uri: 'abracadabra://server-info',
41
+ text: JSON.stringify(info, null, 2),
42
+ mimeType: 'application/json',
43
+ }],
44
+ }
45
+ }
46
+ )
47
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Dynamic resource: document tree as navigable JSON.
3
+ */
4
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
5
+ import type { AbracadabraMCPServer } from '../server.ts'
6
+ import type { TreeEntry } from '../converters/types.ts'
7
+
8
+ function readEntries(treeMap: any): TreeEntry[] {
9
+ const entries: TreeEntry[] = []
10
+ treeMap.forEach((value: any, id: string) => {
11
+ entries.push({
12
+ id,
13
+ label: value.label || 'Untitled',
14
+ parentId: value.parentId ?? null,
15
+ order: value.order ?? 0,
16
+ type: value.type,
17
+ meta: value.meta,
18
+ })
19
+ })
20
+ return entries
21
+ }
22
+
23
+ function childrenOf(entries: TreeEntry[], parentId: string | null): TreeEntry[] {
24
+ return entries
25
+ .filter(e => e.parentId === parentId)
26
+ .sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
27
+ }
28
+
29
+ function buildTree(entries: TreeEntry[], rootId: string | null, maxDepth: number, depth = 0): any[] {
30
+ if (maxDepth >= 0 && depth >= maxDepth) return []
31
+ return childrenOf(entries, rootId).map(e => ({
32
+ id: e.id,
33
+ label: e.label,
34
+ type: e.type,
35
+ children: buildTree(entries, e.id, maxDepth, depth + 1),
36
+ }))
37
+ }
38
+
39
+ export function registerTreeResource(mcp: McpServer, server: AbracadabraMCPServer) {
40
+ mcp.resource(
41
+ 'tree',
42
+ 'abracadabra://tree',
43
+ {
44
+ description: 'Full document tree as nested JSON. Shows the complete hierarchy of all documents.',
45
+ mimeType: 'application/json',
46
+ },
47
+ async () => {
48
+ const treeMap = server.getTreeMap()
49
+ if (!treeMap) {
50
+ return {
51
+ contents: [{
52
+ uri: 'abracadabra://tree',
53
+ text: '{"error": "Not connected"}',
54
+ mimeType: 'application/json',
55
+ }],
56
+ }
57
+ }
58
+
59
+ const entries = readEntries(treeMap)
60
+ const tree = buildTree(entries, null, 10)
61
+
62
+ return {
63
+ contents: [{
64
+ uri: 'abracadabra://tree',
65
+ text: JSON.stringify(tree, null, 2),
66
+ mimeType: 'application/json',
67
+ }],
68
+ }
69
+ }
70
+ )
71
+ }