@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.
- package/dist/abracadabra-mcp.cjs +1165 -190
- package/dist/abracadabra-mcp.cjs.map +1 -1
- package/dist/abracadabra-mcp.esm.js +1165 -190
- package/dist/abracadabra-mcp.esm.js.map +1 -1
- package/dist/index.d.ts +32 -1
- package/package.json +1 -1
- package/src/converters/page-types.ts +408 -0
- package/src/converters/types.ts +15 -11
- package/src/hook-bridge.ts +18 -8
- package/src/index.ts +29 -2
- package/src/mentions.ts +42 -0
- package/src/resources/agent-guide.ts +151 -29
- package/src/server.ts +139 -14
- package/src/tools/awareness.ts +3 -0
- package/src/tools/channel.ts +18 -8
- package/src/tools/content.ts +0 -5
- package/src/tools/files.ts +8 -0
- package/src/tools/meta.ts +1 -7
- package/src/tools/svg.ts +0 -3
- package/src/tools/tree.ts +28 -22
package/src/index.ts
CHANGED
|
@@ -3,15 +3,23 @@
|
|
|
3
3
|
* Abracadabra MCP Server — entry point.
|
|
4
4
|
*
|
|
5
5
|
* Environment variables:
|
|
6
|
-
* ABRA_URL
|
|
6
|
+
* ABRA_URL (required) — Server URL (e.g. http://localhost:1234)
|
|
7
7
|
* ABRA_AGENT_NAME — Display name (default: "AI Assistant")
|
|
8
8
|
* ABRA_AGENT_COLOR — HSL color for presence (default: "hsl(270, 80%, 60%)")
|
|
9
9
|
* ABRA_INVITE_CODE — Invite code for first-run registration (grants role)
|
|
10
10
|
* ABRA_KEY_FILE — Path to Ed25519 key file (default: ~/.abracadabra/agent.key)
|
|
11
|
+
* ABRA_AGENT_TRIGGER_MODE — When to respond in group chats:
|
|
12
|
+
* all → every message (legacy)
|
|
13
|
+
* mention → only when @<alias> is used
|
|
14
|
+
* task → only ai:task awareness events
|
|
15
|
+
* mention+task → mention OR ai:task (default)
|
|
16
|
+
* DMs always trigger regardless of mode.
|
|
17
|
+
* ABRA_AGENT_MENTION_ALIASES — Comma-separated aliases for @mentions
|
|
18
|
+
* (default: [ABRA_AGENT_NAME])
|
|
11
19
|
*/
|
|
12
20
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
13
21
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
14
|
-
import { AbracadabraMCPServer } from './server.ts'
|
|
22
|
+
import { AbracadabraMCPServer, type TriggerMode } from './server.ts'
|
|
15
23
|
import { registerTreeTools } from './tools/tree.ts'
|
|
16
24
|
import { registerContentTools } from './tools/content.ts'
|
|
17
25
|
import { registerMetaTools } from './tools/meta.ts'
|
|
@@ -33,6 +41,21 @@ async function main() {
|
|
|
33
41
|
process.exit(1)
|
|
34
42
|
}
|
|
35
43
|
|
|
44
|
+
// Parse trigger mode (defaults to mention+task)
|
|
45
|
+
const rawMode = (process.env.ABRA_AGENT_TRIGGER_MODE ?? 'mention+task').trim().toLowerCase()
|
|
46
|
+
const validModes: TriggerMode[] = ['all', 'mention', 'task', 'mention+task']
|
|
47
|
+
const triggerMode = (validModes as string[]).includes(rawMode)
|
|
48
|
+
? (rawMode as TriggerMode)
|
|
49
|
+
: 'mention+task'
|
|
50
|
+
if (rawMode && !(validModes as string[]).includes(rawMode)) {
|
|
51
|
+
console.error(`[abracadabra-mcp] Invalid ABRA_AGENT_TRIGGER_MODE="${rawMode}", falling back to "mention+task"`)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const aliasEnv = process.env.ABRA_AGENT_MENTION_ALIASES
|
|
55
|
+
const mentionAliases: string[] | undefined = aliasEnv
|
|
56
|
+
? aliasEnv.split(',').map((a: string) => a.trim()).filter((a: string) => a.length > 0)
|
|
57
|
+
: undefined
|
|
58
|
+
|
|
36
59
|
// Create the Abracadabra connection manager
|
|
37
60
|
const server = new AbracadabraMCPServer({
|
|
38
61
|
url,
|
|
@@ -40,8 +63,12 @@ async function main() {
|
|
|
40
63
|
agentColor: process.env.ABRA_AGENT_COLOR,
|
|
41
64
|
inviteCode: process.env.ABRA_INVITE_CODE,
|
|
42
65
|
keyFile: process.env.ABRA_KEY_FILE,
|
|
66
|
+
triggerMode,
|
|
67
|
+
mentionAliases,
|
|
43
68
|
})
|
|
44
69
|
|
|
70
|
+
console.error(`[abracadabra-mcp] Trigger mode: ${triggerMode}; aliases: ${server.mentionAliases.join(', ')}`)
|
|
71
|
+
|
|
45
72
|
// Create MCP server with channel capability
|
|
46
73
|
const mcp = new McpServer(
|
|
47
74
|
{ name: 'abracadabra', version: '1.0.0' },
|
package/src/mentions.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mention parsing for chat messages.
|
|
3
|
+
*
|
|
4
|
+
* Recognizes `@alias` tokens (case-insensitive, word-boundary) so the agent
|
|
5
|
+
* can decide whether a group-chat message is directed at it.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Escape regex metacharacters in an alias string. */
|
|
9
|
+
function escapeRegex(s: string): string {
|
|
10
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Build a regex that matches `@<alias>` for any of the given aliases.
|
|
15
|
+
* Requires a non-word char (or start) before `@` and a word boundary after the alias
|
|
16
|
+
* so `@Claude` matches but `email@claudesomething` does not.
|
|
17
|
+
*/
|
|
18
|
+
function buildMentionRegex(aliases: string[]): RegExp | null {
|
|
19
|
+
const cleaned = aliases.map(a => a.trim()).filter(a => a.length > 0)
|
|
20
|
+
if (cleaned.length === 0) return null
|
|
21
|
+
const alt = cleaned.map(escapeRegex).join('|')
|
|
22
|
+
return new RegExp(`(?:^|[^\\w@])@(?:${alt})\\b`, 'i')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Returns true if `text` contains `@alias` for any alias (case-insensitive). */
|
|
26
|
+
export function containsMention(text: string, aliases: string[]): boolean {
|
|
27
|
+
const re = buildMentionRegex(aliases)
|
|
28
|
+
if (!re) return false
|
|
29
|
+
return re.test(text)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Remove `@alias` tokens from the text. Leaves surrounding whitespace tidy so
|
|
34
|
+
* the cleaned prompt reads naturally (e.g. `"@Claude help"` → `"help"`).
|
|
35
|
+
*/
|
|
36
|
+
export function stripMention(text: string, aliases: string[]): string {
|
|
37
|
+
const cleaned = aliases.map(a => a.trim()).filter(a => a.length > 0)
|
|
38
|
+
if (cleaned.length === 0) return text
|
|
39
|
+
const alt = cleaned.map(escapeRegex).join('|')
|
|
40
|
+
const re = new RegExp(`(^|\\s)@(?:${alt})\\b[,:]?\\s*`, 'gi')
|
|
41
|
+
return text.replace(re, (_m, lead) => (lead ? ' ' : '')).replace(/\s{2,}/g, ' ').trim()
|
|
42
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Static resource: AI agent guide for working with Abracadabra documents.
|
|
3
3
|
*/
|
|
4
|
-
import type { McpServer } from
|
|
4
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
5
|
|
|
6
6
|
const AGENT_GUIDE = `# Abracadabra AI Agent Guide
|
|
7
7
|
|
|
@@ -47,24 +47,38 @@ If you are adding content to an existing space, call \`get_document_tree(rootId:
|
|
|
47
47
|
|
|
48
48
|
## Page Types Reference
|
|
49
49
|
|
|
50
|
+
### Core Types (always available)
|
|
51
|
+
|
|
50
52
|
| Type | Children Are | Grandchildren Are | Depth | Key Meta on Children |
|
|
51
53
|
|------|-------------|-------------------|-------|---------------------|
|
|
52
54
|
| **doc** | Sub-documents | Sub-sub-documents | ∞ | — |
|
|
53
55
|
| **kanban** | Columns | Cards | 2 | color, icon on cards |
|
|
54
56
|
| **table** | Columns | Cells (positional rows) | 2 | — |
|
|
55
|
-
| **calendar** | Events | — | 1 | datetimeStart, datetimeEnd, allDay, color |
|
|
57
|
+
| **calendar** | Events | — | 1 | datetimeStart, datetimeEnd, allDay, color, icon |
|
|
56
58
|
| **timeline** | Epics | Tasks | 2 | dateStart, dateEnd, taskProgress, color |
|
|
57
59
|
| **checklist** | Tasks | Sub-tasks | ∞ | checked, priority, dateEnd |
|
|
58
60
|
| **outline** | Items | Sub-items | ∞ | — |
|
|
59
|
-
| **mindmap** | Central nodes | Branches | ∞ | mmX, mmY |
|
|
60
61
|
| **graph** | Nodes | — | 1 | graphX, graphY, graphPinned, color |
|
|
61
|
-
| **gallery** | Items | — | 1 | geoLat, geoLng, datetimeStart, tags |
|
|
62
|
+
| **gallery** | Items | — | 1 | geoLat, geoLng, datetimeStart, tags, rating, icon, color |
|
|
62
63
|
| **map** | Markers/Lines | Points (for lines) | 2 | geoType, geoLat, geoLng, icon, color |
|
|
63
|
-
| **
|
|
64
|
-
| **
|
|
65
|
-
| **
|
|
66
|
-
| **
|
|
67
|
-
| **
|
|
64
|
+
| **slides** | Slides | Sub-slides | 2 | slidesTransition, color |
|
|
65
|
+
| **dashboard** | Items | — | 1 | deskX, deskY, deskZ, deskMode |
|
|
66
|
+
| **chart** | Data points | Data points | 2 | number (value), color, tags |
|
|
67
|
+
| **sheets** | Columns | Cells | 2 | formula, bold, italic, textColor, bgColor, align |
|
|
68
|
+
| **overview** | Pages | — | 1 | — |
|
|
69
|
+
| **call** | — (no children) | — | 0 | — |
|
|
70
|
+
|
|
71
|
+
Alias: \`desktop\` → \`dashboard\`.
|
|
72
|
+
|
|
73
|
+
### Plugin Types (require plugin enabled on the server)
|
|
74
|
+
|
|
75
|
+
| Type | Plugin | Children Are | Grandchildren Are | Depth | Key Meta on Children |
|
|
76
|
+
|------|--------|-------------|-------------------|-------|---------------------|
|
|
77
|
+
| **spatial** | spatial | Objects | Sub-parts | ∞ | spShape, spX/Y/Z, spRX/RY/RZ, spSX/SY/SZ, color, spOpacity |
|
|
78
|
+
| **media** | media | Tracks | — | ∞ | tags |
|
|
79
|
+
| **coder** | coder | Files/Folders | — | ∞ | fileType, entry |
|
|
80
|
+
|
|
81
|
+
> Spatial uses the universal \`color\` key for object color — there is no \`spColor\`.
|
|
68
82
|
|
|
69
83
|
---
|
|
70
84
|
|
|
@@ -137,18 +151,65 @@ If you are adding content to an existing space, call \`get_document_tree(rootId:
|
|
|
137
151
|
4. Modes: \`"icon"\` (small), \`"widget-sm"\` (240×180), \`"widget-lg"\` (400×320)
|
|
138
152
|
5. Grid uses 80px cells
|
|
139
153
|
|
|
140
|
-
**Spatial (3D scene)
|
|
154
|
+
**Spatial (3D scene)** *(requires spatial plugin):*
|
|
141
155
|
1. \`create_document(parentId, "3D Scene", "spatial")\`
|
|
142
156
|
2. Create objects: \`create_document(sceneId, "Red Cube")\`
|
|
143
|
-
3. Set 3D properties: \`update_metadata(objId, { spShape: "box",
|
|
144
|
-
4. Shapes: \`"box"\`, \`"sphere"\`, \`"cylinder"\`, \`"cone"\`, \`"plane"\`, \`"torus"\`, \`"glb"\` (uploaded 3D model)
|
|
157
|
+
3. Set 3D properties: \`update_metadata(objId, { spShape: "box", color: "#ef4444", spX: 0, spY: 1, spZ: 0, spSX: 2, spSY: 2, spSZ: 2 })\`
|
|
158
|
+
4. Shapes: \`"box"\`, \`"sphere"\`, \`"cylinder"\`, \`"cone"\`, \`"plane"\`, \`"torus"\`, \`"glb"\` (uploaded 3D model — set \`spModelUploadId\` and \`spModelDocId\`)
|
|
145
159
|
5. Rotation (degrees): \`spRX\`, \`spRY\`, \`spRZ\`. Scale: \`spSX\`, \`spSY\`, \`spSZ\` (default 1). Opacity: \`spOpacity\` (0–100)
|
|
160
|
+
6. Use the universal \`color\` key — **never \`spColor\`**
|
|
146
161
|
|
|
147
162
|
**Outline (nested items):**
|
|
148
163
|
1. \`create_document(parentId, "Meeting Notes", "outline")\`
|
|
149
164
|
2. Create items: \`create_document(outlineId, "Agenda Item 1")\`
|
|
150
165
|
3. Create sub-items (unlimited depth): \`create_document(itemId, "Sub-point")\`
|
|
151
166
|
|
|
167
|
+
**Slides (presentation with two-axis nav):**
|
|
168
|
+
1. \`create_document(parentId, "Q1 Review", "slides")\`
|
|
169
|
+
2. Create slides as direct children: \`create_document(deckId, "Intro")\`
|
|
170
|
+
3. Create sub-slides (vertical navigation) as grandchildren: \`create_document(slideId, "Deep dive")\`
|
|
171
|
+
4. Per-slide transition: \`update_metadata(slideId, { slidesTransition: "fade", color: "#6366f1" })\`
|
|
172
|
+
5. Deck-level theme: \`update_metadata(deckId, { slidesTheme: "dark" })\`
|
|
173
|
+
|
|
174
|
+
**Chart (data viz — manual or aggregation):**
|
|
175
|
+
1. \`create_document(parentId, "Sales", "chart")\`
|
|
176
|
+
2. Configure: \`update_metadata(chartId, { chartType: "bar", chartMetric: "value", chartShowLegend: true, chartLimit: 10 })\`
|
|
177
|
+
3. Modes:
|
|
178
|
+
- **Manual data points**: create children with \`number\` (value) and optional \`tags\`/\`color\`
|
|
179
|
+
\`create_document(chartId, "Q1")\` then \`update_metadata(dpId, { number: 42500, color: "#6366f1" })\`
|
|
180
|
+
- **Aggregation**: set \`chartMetric\` to \`"type"\`/\`"tag"\`/\`"status"\`/\`"priority"\`/\`"activity"\`/\`"completion"\` and point the chart at a subtree — it aggregates the descendants' meta automatically
|
|
181
|
+
4. Chart types: \`"bar"\`, \`"stacked bar"\`, \`"line"\`, \`"donut"\`, \`"treemap"\`
|
|
182
|
+
5. Color schemes: \`"default"\`, \`"warm"\`, \`"cool"\`, \`"mono"\`
|
|
183
|
+
|
|
184
|
+
**Sheets (spreadsheet with formulas):**
|
|
185
|
+
1. \`create_document(parentId, "Budget", "sheets")\`
|
|
186
|
+
2. Create columns: \`create_document(sheetId, "A")\`, \`create_document(sheetId, "B")\`
|
|
187
|
+
3. Create cells under columns (positional rows, like \`table\`)
|
|
188
|
+
4. Formulas on cells: \`update_metadata(cellId, { formula: "=A1+B1" })\`
|
|
189
|
+
5. Cell formatting: \`update_metadata(cellId, { bold: true, bgColor: "#fef3c7", align: "right" })\`
|
|
190
|
+
6. Deck config: \`update_metadata(sheetId, { sheetsDefaultColWidth: 120, sheetsDefaultRowHeight: 28, sheetsShowGridlines: true, sheetsFreezeRows: 1, sheetsFreezeCols: 1 })\`
|
|
191
|
+
|
|
192
|
+
**Overview (space home):**
|
|
193
|
+
1. \`create_document(parentId, "Home", "overview")\`
|
|
194
|
+
2. No children required — renders activity, people, stats from the surrounding space
|
|
195
|
+
3. Children (if any) show as linked pages
|
|
196
|
+
|
|
197
|
+
**Call (video room):**
|
|
198
|
+
1. \`create_document(parentId, "Daily Standup", "call")\`
|
|
199
|
+
2. Video rooms have **no children** — do not add documents underneath
|
|
200
|
+
|
|
201
|
+
**Coder (multi-file collaborative editor)** *(requires coder plugin):*
|
|
202
|
+
1. \`create_document(parentId, "My App", "coder")\`
|
|
203
|
+
2. Create files/folders: \`create_document(projectId, "App.vue")\`, \`create_document(projectId, "src")\`
|
|
204
|
+
3. Set file type: \`update_metadata(fileId, { fileType: "vue", entry: true })\`
|
|
205
|
+
4. \`fileType\` options: \`"vue"\`, \`"ts"\`, \`"js"\`, \`"css"\`, \`"json"\`, \`"folder"\`
|
|
206
|
+
5. Mark the entry file with \`entry: true\` — the renderer uses it as the preview root
|
|
207
|
+
|
|
208
|
+
**Media (audio/video playlist)** *(requires media plugin):*
|
|
209
|
+
1. \`create_document(parentId, "Focus Mix", "media")\`
|
|
210
|
+
2. Create tracks: \`create_document(playlistId, "Track 1")\` — attach audio/video file via upload tool
|
|
211
|
+
3. Playlist config: \`update_metadata(playlistId, { mediaRepeat: "all", mediaShuffle: false })\`
|
|
212
|
+
|
|
152
213
|
---
|
|
153
214
|
|
|
154
215
|
## Document References
|
|
@@ -223,22 +284,55 @@ In the **graph** page type, document references (embeds and links) create visibl
|
|
|
223
284
|
|-----|------|-----------|--------|
|
|
224
285
|
| \`kanbanColumnWidth\` | string | kanban | "narrow", "default", "wide" |
|
|
225
286
|
| \`galleryColumns\` | number | gallery | 1–6 |
|
|
226
|
-
| \`galleryAspect\` | string | gallery | "square", "4:3", "16:9" |
|
|
287
|
+
| \`galleryAspect\` | string | gallery | "square", "4:3", "3:2", "16:9", "free" |
|
|
288
|
+
| \`galleryCardStyle\` | string | gallery | "default", "compact", "detailed" |
|
|
289
|
+
| \`galleryShowLabels\` | boolean | gallery | show item labels |
|
|
290
|
+
| \`gallerySortBy\` | string | gallery | "manual", "date", "name", "rating" |
|
|
227
291
|
| \`calendarView\` | string | calendar | "month", "week", "day" |
|
|
228
292
|
| \`calendarWeekStart\` | string | calendar | "sun", "mon" |
|
|
293
|
+
| \`calendarShowWeekNumbers\` | boolean | calendar | — |
|
|
229
294
|
| \`tableMode\` | string | table | "hierarchy", "flat" |
|
|
295
|
+
| \`tableSortKey\` | string | table | meta key to sort by |
|
|
296
|
+
| \`tableSortDir\` | string | table | "asc", "desc" |
|
|
297
|
+
| \`timelineZoom\` | string | timeline | "week", "month", "quarter" |
|
|
298
|
+
| \`timelinePixelsPerDay\` | number | timeline | zoom granularity |
|
|
299
|
+
| \`timelineCenterDate\` | string | timeline | ISO date to center view |
|
|
300
|
+
| \`checklistFilter\` | string | checklist | "all", "active", "completed" |
|
|
301
|
+
| \`checklistSort\` | string | checklist | "manual", "priority", "due" |
|
|
302
|
+
| \`mapShowLabels\` | boolean | map | — |
|
|
303
|
+
| \`graphSpacing\` | string | graph | "compact", "default", "spacious" |
|
|
304
|
+
| \`graphShowLabels\` | boolean | graph | — |
|
|
305
|
+
| \`graphEdgeThickness\` | string | graph | "thin", "normal", "thick" |
|
|
230
306
|
| \`showRefEdges\` | boolean | graph | show doc-reference edges |
|
|
231
|
-
|
|
232
|
-
|
|
307
|
+
| \`mmSpacing\` | string | mindmap-layout | spacing between branches |
|
|
308
|
+
| \`spatialGridVisible\` | boolean | spatial | show ground grid |
|
|
309
|
+
| \`slidesTheme\` | string | slides | "dark", "light" |
|
|
310
|
+
| \`chartType\` | string | chart | "bar", "stacked bar", "line", "donut", "treemap" |
|
|
311
|
+
| \`chartMetric\` | string | chart | "value", "type", "tag", "status", "priority", "activity", "completion" |
|
|
312
|
+
| \`chartColorScheme\` | string | chart | "default", "warm", "cool", "mono" |
|
|
313
|
+
| \`chartLimit\` | number | chart | 3–30 (max items) |
|
|
314
|
+
| \`chartShowLegend\` | boolean | chart | — |
|
|
315
|
+
| \`chartShowValues\` | boolean | chart | — |
|
|
316
|
+
| \`sheetsDefaultColWidth\` | number | sheets | 40–500 |
|
|
317
|
+
| \`sheetsDefaultRowHeight\` | number | sheets | 20–100 |
|
|
318
|
+
| \`sheetsShowGridlines\` | boolean | sheets | — |
|
|
319
|
+
| \`sheetsFreezeRows\` | number | sheets | frozen rows count |
|
|
320
|
+
| \`sheetsFreezeCols\` | number | sheets | frozen cols count |
|
|
321
|
+
| \`mediaRepeat\` | string | media | "off", "all", "one" (plugin) |
|
|
322
|
+
| \`mediaShuffle\` | boolean | media | plugin |
|
|
323
|
+
|
|
324
|
+
### Spatial 3D Keys (for spatial children — requires spatial plugin)
|
|
233
325
|
|
|
234
326
|
| Key | Type | Default | Meaning |
|
|
235
327
|
|-----|------|---------|---------|
|
|
236
328
|
| \`spShape\` | string | — | "box", "sphere", "cylinder", "cone", "plane", "torus", "glb" |
|
|
237
|
-
| \`
|
|
329
|
+
| \`color\` | string | — | CSS color — universal key, **not** \`spColor\` |
|
|
238
330
|
| \`spOpacity\` | number | 100 | 0–100 |
|
|
239
331
|
| \`spX\`, \`spY\`, \`spZ\` | number | 0 | Position |
|
|
240
332
|
| \`spRX\`, \`spRY\`, \`spRZ\` | number | 0 | Rotation (degrees) |
|
|
241
333
|
| \`spSX\`, \`spSY\`, \`spSZ\` | number | 1 | Scale |
|
|
334
|
+
| \`spModelUploadId\` | string | — | GLB upload ID (when \`spShape: "glb"\`) |
|
|
335
|
+
| \`spModelDocId\` | string | — | Doc ID that owns the GLB upload |
|
|
242
336
|
|
|
243
337
|
### Dashboard Keys (for dashboard children)
|
|
244
338
|
|
|
@@ -248,6 +342,28 @@ In the **graph** page type, document references (embeds and links) create visibl
|
|
|
248
342
|
| \`deskZ\` | number | Z-index (layering) |
|
|
249
343
|
| \`deskMode\` | string | "icon", "widget-sm" (240×180), "widget-lg" (400×320) |
|
|
250
344
|
|
|
345
|
+
### Sheets Cell Formatting (per-cell meta)
|
|
346
|
+
|
|
347
|
+
| Key | Type | Meaning |
|
|
348
|
+
|-----|------|---------|
|
|
349
|
+
| \`formula\` | string | Cell formula (e.g. "=A1+B1") |
|
|
350
|
+
| \`bold\` | boolean | — |
|
|
351
|
+
| \`italic\` | boolean | — |
|
|
352
|
+
| \`textColor\` | string | CSS color |
|
|
353
|
+
| \`bgColor\` | string | CSS color |
|
|
354
|
+
| \`align\` | string | "left", "center", "right" |
|
|
355
|
+
|
|
356
|
+
### Coder Keys (for coder children — plugin)
|
|
357
|
+
|
|
358
|
+
| Key | Type | Meaning |
|
|
359
|
+
|-----|------|---------|
|
|
360
|
+
| \`fileType\` | string | "vue", "ts", "js", "css", "json", "folder" |
|
|
361
|
+
| \`entry\` | boolean | Mark this file as the preview entry point |
|
|
362
|
+
|
|
363
|
+
### Discovering What Metadata Applies
|
|
364
|
+
|
|
365
|
+
Use the \`list_page_types\` tool to enumerate all known page types and their declared \`metaSchema\` (fields that apply to children/descendants) and \`defaultMetaFields\` (renderer config fields on the page itself). It's the authoritative list of what meta keys make sense for any given page type.
|
|
366
|
+
|
|
251
367
|
---
|
|
252
368
|
|
|
253
369
|
## Content Structure
|
|
@@ -330,8 +446,11 @@ Always clear fields when done by setting them to \`null\`.
|
|
|
330
446
|
| **Outline** | \`outline:editing\` | nodeId | Editing an outline node |
|
|
331
447
|
| **Gallery** | \`gallery:focused\` | itemId | Item hovered/selected |
|
|
332
448
|
| **Timeline** | \`timeline:focused\` | taskId | Task selected |
|
|
333
|
-
| **Mindmap** | \`mindmap:focused\` | nodeId | Node selected/edited |
|
|
334
449
|
| **Graph** | \`graph:focused\` | nodeId | Node hovered/selected |
|
|
450
|
+
| **Slides** | \`slides:current\` | slideId | Slide being presented |
|
|
451
|
+
| **Spatial** | \`spatial:selected\`, \`spatial:camera\` | objectId / camera state | plugin |
|
|
452
|
+
| **Media** | \`media:playing\`, \`media:position\` | trackId / 0–1 | plugin |
|
|
453
|
+
| **Coder** | \`coder:activeFile\` | fileId | plugin |
|
|
335
454
|
| **Map** | \`map:focused\` | markerId | Marker hovered/selected |
|
|
336
455
|
| **Doc** | \`doc:scroll\` | 0–1 number | Scroll position in document |
|
|
337
456
|
|
|
@@ -414,22 +533,25 @@ Always check and traverse the \`children\` array returned by \`read_document\`.*
|
|
|
414
533
|
- Create top-level documents without first confirming the correct hub doc ID
|
|
415
534
|
- Rename or write content to the space hub document itself
|
|
416
535
|
- Set \`type\` on child items (cards, cells, events) — only set type on the parent page
|
|
417
|
-
|
|
536
|
+
`;
|
|
418
537
|
|
|
419
538
|
export function registerAgentGuide(mcp: McpServer) {
|
|
420
539
|
mcp.resource(
|
|
421
|
-
|
|
422
|
-
|
|
540
|
+
"agent-guide",
|
|
541
|
+
"abracadabra://agent-guide",
|
|
423
542
|
{
|
|
424
|
-
description:
|
|
425
|
-
|
|
543
|
+
description:
|
|
544
|
+
"Comprehensive guide for AI agents working with Abracadabra documents. Covers page types, tree operations, metadata, content structure, and best practices.",
|
|
545
|
+
mimeType: "text/markdown",
|
|
426
546
|
},
|
|
427
547
|
async () => ({
|
|
428
|
-
contents: [
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
548
|
+
contents: [
|
|
549
|
+
{
|
|
550
|
+
uri: "abracadabra://agent-guide",
|
|
551
|
+
text: AGENT_GUIDE,
|
|
552
|
+
mimeType: "text/markdown",
|
|
553
|
+
},
|
|
554
|
+
],
|
|
555
|
+
}),
|
|
556
|
+
);
|
|
435
557
|
}
|
package/src/server.ts
CHANGED
|
@@ -12,6 +12,16 @@ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
|
12
12
|
import type { Server } from '@modelcontextprotocol/sdk/server/index.js'
|
|
13
13
|
import { waitForSync } from './utils.ts'
|
|
14
14
|
import { loadOrCreateKeypair, signChallenge } from './crypto.ts'
|
|
15
|
+
import { containsMention, stripMention } from './mentions.ts'
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Controls when the agent reacts to incoming chat:
|
|
19
|
+
* - `all` — respond to every message in every visible channel (legacy)
|
|
20
|
+
* - `mention` — group chats require `@<alias>`; DMs always respond
|
|
21
|
+
* - `task` — ignore chat entirely; only respond to ai:task awareness events
|
|
22
|
+
* - `mention+task` — group chats require mention OR ai:task; DMs always respond (default)
|
|
23
|
+
*/
|
|
24
|
+
export type TriggerMode = 'all' | 'mention' | 'task' | 'mention+task'
|
|
15
25
|
|
|
16
26
|
export interface MCPServerConfig {
|
|
17
27
|
url: string
|
|
@@ -19,6 +29,9 @@ export interface MCPServerConfig {
|
|
|
19
29
|
agentColor?: string
|
|
20
30
|
inviteCode?: string
|
|
21
31
|
keyFile?: string
|
|
32
|
+
triggerMode?: TriggerMode
|
|
33
|
+
/** Aliases matched in `@<alias>` tokens. Defaults to `[agentName]`. */
|
|
34
|
+
mentionAliases?: string[]
|
|
22
35
|
}
|
|
23
36
|
|
|
24
37
|
interface SpaceConnection {
|
|
@@ -52,6 +65,9 @@ export class AbracadabraMCPServer {
|
|
|
52
65
|
private _typingInterval: ReturnType<typeof setInterval> | null = null
|
|
53
66
|
private _lastChatChannel: string | null = null
|
|
54
67
|
private _signFn: ((challenge: string) => Promise<string>) | null = null
|
|
68
|
+
/** Rolling buffer of the last N tool calls in the current turn, surfaced via awareness. */
|
|
69
|
+
private _toolHistory: Array<{ tool: string; target?: string; ts: number; channel: string | null }> = []
|
|
70
|
+
private static readonly TOOL_HISTORY_MAX = 20
|
|
55
71
|
|
|
56
72
|
constructor(config: MCPServerConfig) {
|
|
57
73
|
this.config = config
|
|
@@ -69,6 +85,16 @@ export class AbracadabraMCPServer {
|
|
|
69
85
|
return this.config.agentColor || 'hsl(270, 80%, 60%)'
|
|
70
86
|
}
|
|
71
87
|
|
|
88
|
+
get triggerMode(): TriggerMode {
|
|
89
|
+
return this.config.triggerMode ?? 'mention+task'
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
get mentionAliases(): string[] {
|
|
93
|
+
const explicit = this.config.mentionAliases?.filter(a => a.trim().length > 0)
|
|
94
|
+
if (explicit && explicit.length > 0) return explicit
|
|
95
|
+
return [this.agentName]
|
|
96
|
+
}
|
|
97
|
+
|
|
72
98
|
get serverInfo(): ServerInfo | null {
|
|
73
99
|
return this._serverInfo
|
|
74
100
|
}
|
|
@@ -121,7 +147,7 @@ export class AbracadabraMCPServer {
|
|
|
121
147
|
throw err
|
|
122
148
|
}
|
|
123
149
|
}
|
|
124
|
-
console.error(`[abracadabra-mcp] Authenticated as ${this.agentName} (
|
|
150
|
+
console.error(`[abracadabra-mcp] Authenticated as ${this.agentName} (pubkey=${keypair.publicKeyB64})`)
|
|
125
151
|
|
|
126
152
|
// Step 3: Discover server info
|
|
127
153
|
this._serverInfo = await this.client.serverInfo()
|
|
@@ -209,6 +235,8 @@ export class AbracadabraMCPServer {
|
|
|
209
235
|
provider.awareness.setLocalStateField('status', null)
|
|
210
236
|
provider.awareness.setLocalStateField('activeToolCall', null)
|
|
211
237
|
provider.awareness.setLocalStateField('statusContext', null)
|
|
238
|
+
provider.awareness.setLocalStateField('turnId', null)
|
|
239
|
+
provider.awareness.setLocalStateField('toolHistory', [])
|
|
212
240
|
|
|
213
241
|
const conn: SpaceConnection = { doc, provider, docId }
|
|
214
242
|
this._spaceConnections.set(docId, conn)
|
|
@@ -249,6 +277,19 @@ export class AbracadabraMCPServer {
|
|
|
249
277
|
return this._activeConnection?.doc.getMap('doc-trash') ?? null
|
|
250
278
|
}
|
|
251
279
|
|
|
280
|
+
/** Get plugin names enabled in the active space via space-plugins Y.Map. */
|
|
281
|
+
getEnabledPluginNames(): string[] {
|
|
282
|
+
const doc = this._activeConnection?.doc
|
|
283
|
+
if (!doc) return []
|
|
284
|
+
const pluginsMap = doc.getMap('space-plugins')
|
|
285
|
+
const names: string[] = []
|
|
286
|
+
pluginsMap.forEach((value: any, key: string) => {
|
|
287
|
+
const entry = value?.toJSON ? value.toJSON() : value
|
|
288
|
+
if (entry?.enabled) names.push(key)
|
|
289
|
+
})
|
|
290
|
+
return names
|
|
291
|
+
}
|
|
292
|
+
|
|
252
293
|
/**
|
|
253
294
|
* Get or create a child provider for a given document ID.
|
|
254
295
|
* Caches providers and waits for sync before returning.
|
|
@@ -346,6 +387,9 @@ export class AbracadabraMCPServer {
|
|
|
346
387
|
private _observeRootAwareness(provider: AbracadabraProvider): void {
|
|
347
388
|
const selfId = provider.awareness.clientID
|
|
348
389
|
provider.awareness.on('change', () => {
|
|
390
|
+
// Strict `mention` mode ignores ai:task awareness; every other mode honors it.
|
|
391
|
+
if (this.triggerMode === 'mention') return
|
|
392
|
+
|
|
349
393
|
const states = provider.awareness.getStates()
|
|
350
394
|
for (const [clientId, state] of states) {
|
|
351
395
|
if (clientId === selfId) continue
|
|
@@ -363,6 +407,7 @@ export class AbracadabraMCPServer {
|
|
|
363
407
|
: 'Unknown'
|
|
364
408
|
|
|
365
409
|
console.error(`[abracadabra-mcp] Handling ai:task id=${id} from ${senderName}: ${text.slice(0, 80)}`)
|
|
410
|
+
this._beginTurn()
|
|
366
411
|
this.setAutoStatus('thinking')
|
|
367
412
|
this._dispatchAiTask({
|
|
368
413
|
id,
|
|
@@ -433,14 +478,44 @@ export class AbracadabraMCPServer {
|
|
|
433
478
|
|
|
434
479
|
const channel = data.channel as string | undefined
|
|
435
480
|
const docId = channel?.startsWith('group:') ? channel.slice(6) : ''
|
|
481
|
+
const isDM = channel?.startsWith('dm:') ?? false
|
|
482
|
+
const isGroup = channel?.startsWith('group:') ?? false
|
|
436
483
|
|
|
437
484
|
// Only process DMs where the agent is a participant
|
|
438
|
-
if (
|
|
439
|
-
const parts = channel
|
|
485
|
+
if (isDM) {
|
|
486
|
+
const parts = channel!.split(':')
|
|
440
487
|
if (parts.length === 3 && parts[1] !== this._userId && parts[2] !== this._userId) {
|
|
488
|
+
console.error(
|
|
489
|
+
`[abracadabra-mcp] Dropping DM: agent _userId=${this._userId} not in channel parts=[${parts[1]}, ${parts[2]}] — channel='${channel}'. ` +
|
|
490
|
+
`The dashboard's awareness likely points at a stale Claude identity.`,
|
|
491
|
+
)
|
|
492
|
+
return
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// ── Trigger mode gate ─────────────────────────────────────────────
|
|
497
|
+
// DMs always dispatch (implicit summon). Groups obey the trigger mode.
|
|
498
|
+
const mode = this.triggerMode
|
|
499
|
+
const content = typeof data.content === 'string' ? data.content : ''
|
|
500
|
+
let dispatchContent = content
|
|
501
|
+
|
|
502
|
+
if (isGroup) {
|
|
503
|
+
if (mode === 'task') {
|
|
504
|
+
// Chat never triggers in task-only mode
|
|
441
505
|
return
|
|
442
506
|
}
|
|
507
|
+
if (mode === 'mention' || mode === 'mention+task') {
|
|
508
|
+
const aliases = this.mentionAliases
|
|
509
|
+
if (!containsMention(content, aliases)) {
|
|
510
|
+
console.error(`[abracadabra-mcp] skipped message on ${channel} — no @mention for ${aliases.join('|')}`)
|
|
511
|
+
return
|
|
512
|
+
}
|
|
513
|
+
// Strip the mention so Claude sees a clean prompt
|
|
514
|
+
dispatchContent = stripMention(content, aliases) || content
|
|
515
|
+
}
|
|
516
|
+
// mode === 'all' falls through unchanged
|
|
443
517
|
}
|
|
518
|
+
// ──────────────────────────────────────────────────────────────────
|
|
444
519
|
|
|
445
520
|
// Auto-mark channel as read so sender sees a read receipt
|
|
446
521
|
if (channel) {
|
|
@@ -452,19 +527,22 @@ export class AbracadabraMCPServer {
|
|
|
452
527
|
timestamp: Math.floor(Date.now() / 1000),
|
|
453
528
|
}))
|
|
454
529
|
}
|
|
455
|
-
//
|
|
530
|
+
// Remember the channel so setAutoStatus can scope statusContext to it.
|
|
531
|
+
// NOTE: no typing indicator here — "thinking" status + tool pills are
|
|
532
|
+
// the correct signal while Claude is working; typing is reserved for
|
|
533
|
+
// the actual send_chat_message burst below.
|
|
456
534
|
this._lastChatChannel = channel
|
|
457
|
-
this.sendTypingIndicator(channel)
|
|
458
|
-
this._startTypingInterval(channel)
|
|
459
535
|
}
|
|
460
536
|
|
|
461
|
-
//
|
|
537
|
+
// Mint a fresh turn id so the dashboard ties its incantation + tool
|
|
538
|
+
// trace to THIS turn, and set status to "thinking".
|
|
539
|
+
this._beginTurn()
|
|
462
540
|
this.setAutoStatus('thinking')
|
|
463
541
|
|
|
464
542
|
await this._serverRef.notification({
|
|
465
543
|
method: 'notifications/claude/channel',
|
|
466
544
|
params: {
|
|
467
|
-
content:
|
|
545
|
+
content: dispatchContent,
|
|
468
546
|
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.`,
|
|
469
547
|
meta: {
|
|
470
548
|
source: 'abracadabra',
|
|
@@ -505,23 +583,47 @@ export class AbracadabraMCPServer {
|
|
|
505
583
|
const context = status ? (statusContext !== undefined ? statusContext : this._lastChatChannel) : null
|
|
506
584
|
provider.awareness.setLocalStateField('statusContext', context ?? null)
|
|
507
585
|
|
|
508
|
-
// When clearing status
|
|
586
|
+
// When clearing status this is the authoritative end-of-turn signal:
|
|
587
|
+
// drop the tool pill, the turn id, and the running tool history so the
|
|
588
|
+
// dashboard's incantation + activity trace all collapse in lockstep.
|
|
509
589
|
if (!status) {
|
|
510
590
|
this._stopTypingInterval()
|
|
591
|
+
provider.awareness.setLocalStateField('activeToolCall', null)
|
|
592
|
+
provider.awareness.setLocalStateField('turnId', null)
|
|
593
|
+
this._toolHistory = []
|
|
594
|
+
provider.awareness.setLocalStateField('toolHistory', [])
|
|
511
595
|
}
|
|
512
596
|
|
|
513
|
-
// Auto-clear status after
|
|
514
|
-
//
|
|
597
|
+
// Auto-clear status after 10s of no updates. Short enough that a real
|
|
598
|
+
// idle is noticed quickly; long enough that normal inter-tool gaps
|
|
599
|
+
// (PostToolUse → next PreToolUse) don't flicker.
|
|
515
600
|
if (status) {
|
|
516
601
|
this._statusClearTimer = setTimeout(() => {
|
|
517
602
|
provider.awareness.setLocalStateField('status', null)
|
|
518
603
|
provider.awareness.setLocalStateField('activeToolCall', null)
|
|
519
604
|
provider.awareness.setLocalStateField('statusContext', null)
|
|
605
|
+
provider.awareness.setLocalStateField('turnId', null)
|
|
606
|
+
this._toolHistory = []
|
|
607
|
+
provider.awareness.setLocalStateField('toolHistory', [])
|
|
520
608
|
this._stopTypingInterval()
|
|
521
|
-
},
|
|
609
|
+
}, 10_000)
|
|
522
610
|
}
|
|
523
611
|
}
|
|
524
612
|
|
|
613
|
+
/**
|
|
614
|
+
* Start a new agent turn. Mints a fresh UUID and writes it to awareness so
|
|
615
|
+
* the dashboard can gate the incantation on "there is an active turn",
|
|
616
|
+
* decoupled from the (racier) status field. Called from chat arrival and
|
|
617
|
+
* ai:task dispatch right before `setAutoStatus('thinking')`.
|
|
618
|
+
*/
|
|
619
|
+
private _beginTurn(): void {
|
|
620
|
+
const provider = this._activeConnection?.provider
|
|
621
|
+
if (!provider) return
|
|
622
|
+
this._toolHistory = []
|
|
623
|
+
provider.awareness.setLocalStateField('toolHistory', [])
|
|
624
|
+
provider.awareness.setLocalStateField('turnId', crypto.randomUUID())
|
|
625
|
+
}
|
|
626
|
+
|
|
525
627
|
/** Re-send typing indicator every 2s so dashboard keeps showing it (expires at 3s). */
|
|
526
628
|
private _startTypingInterval(channel: string): void {
|
|
527
629
|
this._stopTypingInterval()
|
|
@@ -540,10 +642,30 @@ export class AbracadabraMCPServer {
|
|
|
540
642
|
|
|
541
643
|
/**
|
|
542
644
|
* Broadcast which tool the agent is currently executing.
|
|
543
|
-
*
|
|
645
|
+
*
|
|
646
|
+
* Renders as a ChatTool pill on the dashboard. On non-null calls, the tool
|
|
647
|
+
* is also appended to `toolHistory` (capped at TOOL_HISTORY_MAX) and written
|
|
648
|
+
* to awareness so the dashboard's inline trace can show the turn's recent
|
|
649
|
+
* activity. Tools do NOT clear (`setActiveToolCall(null)`) on completion —
|
|
650
|
+
* the pill stays until the next tool replaces it or `setAutoStatus(null)`
|
|
651
|
+
* flushes the turn. This keeps pills visible long enough to see.
|
|
544
652
|
*/
|
|
545
653
|
setActiveToolCall(toolCall: { name: string; target?: string } | null): void {
|
|
546
|
-
this._activeConnection?.provider
|
|
654
|
+
const provider = this._activeConnection?.provider
|
|
655
|
+
if (!provider) return
|
|
656
|
+
provider.awareness.setLocalStateField('activeToolCall', toolCall)
|
|
657
|
+
if (toolCall) {
|
|
658
|
+
this._toolHistory.push({
|
|
659
|
+
tool: toolCall.name,
|
|
660
|
+
target: toolCall.target,
|
|
661
|
+
ts: Date.now(),
|
|
662
|
+
channel: this._lastChatChannel,
|
|
663
|
+
})
|
|
664
|
+
if (this._toolHistory.length > AbracadabraMCPServer.TOOL_HISTORY_MAX) {
|
|
665
|
+
this._toolHistory.splice(0, this._toolHistory.length - AbracadabraMCPServer.TOOL_HISTORY_MAX)
|
|
666
|
+
}
|
|
667
|
+
provider.awareness.setLocalStateField('toolHistory', [...this._toolHistory])
|
|
668
|
+
}
|
|
547
669
|
}
|
|
548
670
|
|
|
549
671
|
/**
|
|
@@ -581,8 +703,11 @@ export class AbracadabraMCPServer {
|
|
|
581
703
|
conn.provider.awareness.setLocalStateField('status', null)
|
|
582
704
|
conn.provider.awareness.setLocalStateField('activeToolCall', null)
|
|
583
705
|
conn.provider.awareness.setLocalStateField('statusContext', null)
|
|
706
|
+
conn.provider.awareness.setLocalStateField('turnId', null)
|
|
707
|
+
conn.provider.awareness.setLocalStateField('toolHistory', [])
|
|
584
708
|
conn.provider.destroy()
|
|
585
709
|
}
|
|
710
|
+
this._toolHistory = []
|
|
586
711
|
this._spaceConnections.clear()
|
|
587
712
|
this._activeConnection = null
|
|
588
713
|
|