@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,75 @@
1
+ export interface UserMetaField {
2
+ id: string
3
+ type: string
4
+ label?: string
5
+ key?: string
6
+ latKey?: string
7
+ lngKey?: string
8
+ startKey?: string
9
+ endKey?: string
10
+ allDayKey?: string
11
+ presets?: string[]
12
+ options?: string[]
13
+ min?: number
14
+ max?: number
15
+ step?: number
16
+ unit?: string
17
+ }
18
+
19
+ export interface PageMeta extends Record<string, unknown> {
20
+ color?: string
21
+ icon?: string
22
+ datetimeStart?: string
23
+ datetimeEnd?: string
24
+ allDay?: boolean
25
+ dateStart?: string
26
+ dateEnd?: string
27
+ timeStart?: string
28
+ timeEnd?: string
29
+ taskProgress?: number
30
+ tags?: string[]
31
+ checked?: boolean
32
+ priority?: number
33
+ status?: string
34
+ rating?: number
35
+ url?: string
36
+ email?: string
37
+ phone?: string
38
+ number?: number
39
+ unit?: string
40
+ subtitle?: string
41
+ note?: string
42
+ coverUploadId?: string
43
+ coverDocId?: string
44
+ coverMimeType?: string
45
+ geoType?: 'marker' | 'line' | 'measure'
46
+ geoLat?: number
47
+ geoLng?: number
48
+ geoDescription?: string
49
+ wbX?: number
50
+ wbY?: number
51
+ wbW?: number
52
+ wbH?: number
53
+ wbBg?: string
54
+ deskX?: number
55
+ deskY?: number
56
+ deskMode?: string
57
+ mmX?: number
58
+ mmY?: number
59
+ graphX?: number
60
+ graphY?: number
61
+ graphPinned?: boolean
62
+ _metaFields?: UserMetaField[]
63
+ _metaInitialized?: boolean
64
+ }
65
+
66
+ export interface TreeEntry {
67
+ id: string
68
+ label: string
69
+ parentId: string | null
70
+ order: number
71
+ type?: string
72
+ meta?: PageMeta
73
+ createdAt?: number
74
+ updatedAt?: number
75
+ }
@@ -0,0 +1,334 @@
1
+ /**
2
+ * Y.XmlFragment → Markdown serializer.
3
+ * Walks the TipTap document structure and produces markdown text.
4
+ */
5
+ import * as Y from 'yjs'
6
+
7
+ // ── Inline text serialization ────────────────────────────────────────────────
8
+
9
+ function deltaToMarkdown(delta: { insert: string; attributes?: Record<string, any> }[]): string {
10
+ return delta.map(op => {
11
+ let text = op.insert as string
12
+ if (!op.attributes) return text
13
+
14
+ const a = op.attributes
15
+
16
+ if (a.code) text = `\`${text}\``
17
+ if (a.bold) text = `**${text}**`
18
+ if (a.italic) text = `*${text}*`
19
+ if (a.strike) text = `~~${text}~~`
20
+ if (a.link?.href) text = `[${text}](${a.link.href})`
21
+ if (a.badge) text = `:badge[${a.badge.label || text}]`
22
+ if (a.kbd) text = `:kbd{value="${a.kbd.value || text}"}`
23
+ if (a.proseIcon) text = `:icon{name="${a.proseIcon.name}"}`
24
+
25
+ return text
26
+ }).join('')
27
+ }
28
+
29
+ function xmlTextToMarkdown(xmlText: Y.XmlText): string {
30
+ const delta = xmlText.toDelta()
31
+ return deltaToMarkdown(delta)
32
+ }
33
+
34
+ function elementTextContent(el: Y.XmlElement | Y.XmlFragment): string {
35
+ const parts: string[] = []
36
+ for (let i = 0; i < el.length; i++) {
37
+ const child = el.get(i)
38
+ if (child instanceof Y.XmlText) {
39
+ parts.push(xmlTextToMarkdown(child))
40
+ } else if (child instanceof Y.XmlElement) {
41
+ parts.push(elementTextContent(child))
42
+ }
43
+ }
44
+ return parts.join('')
45
+ }
46
+
47
+ // ── Block serialization ──────────────────────────────────────────────────────
48
+
49
+ function serializeElement(el: Y.XmlElement, indent = ''): string {
50
+ const name = el.nodeName
51
+
52
+ switch (name) {
53
+ case 'documentHeader':
54
+ case 'documentMeta':
55
+ return ''
56
+
57
+ case 'heading': {
58
+ const level = Number(el.getAttribute('level')) || 1
59
+ const prefix = '#'.repeat(level)
60
+ return `${prefix} ${elementTextContent(el)}`
61
+ }
62
+
63
+ case 'paragraph':
64
+ return elementTextContent(el)
65
+
66
+ case 'bulletList':
67
+ return serializeList(el, 'bullet', indent)
68
+
69
+ case 'orderedList':
70
+ return serializeList(el, 'ordered', indent)
71
+
72
+ case 'taskList':
73
+ return serializeTaskList(el, indent)
74
+
75
+ case 'codeBlock': {
76
+ const lang = el.getAttribute('language') || ''
77
+ const code = elementTextContent(el)
78
+ return `\`\`\`${lang}\n${code}\n\`\`\``
79
+ }
80
+
81
+ case 'blockquote': {
82
+ const lines: string[] = []
83
+ for (let i = 0; i < el.length; i++) {
84
+ const child = el.get(i)
85
+ if (child instanceof Y.XmlElement) {
86
+ lines.push(serializeElement(child, indent))
87
+ }
88
+ }
89
+ return lines.map(l => `> ${l}`).join('\n')
90
+ }
91
+
92
+ case 'horizontalRule':
93
+ return '---'
94
+
95
+ case 'table':
96
+ return serializeTable(el)
97
+
98
+ case 'image': {
99
+ const src = el.getAttribute('src') || ''
100
+ const alt = el.getAttribute('alt') || ''
101
+ const w = el.getAttribute('width')
102
+ const h = el.getAttribute('height')
103
+ let attrs = ''
104
+ if (w || h) {
105
+ const parts: string[] = []
106
+ if (w) parts.push(`width="${w}"`)
107
+ if (h) parts.push(`height="${h}"`)
108
+ attrs = `{${parts.join(' ')}}`
109
+ }
110
+ return `![${alt}](${src})${attrs}`
111
+ }
112
+
113
+ case 'callout': {
114
+ const type = el.getAttribute('type') || 'note'
115
+ const inner = serializeChildren(el, indent)
116
+ return `::${type}\n${inner}\n::`
117
+ }
118
+
119
+ case 'collapsible': {
120
+ const label = el.getAttribute('label') || 'Details'
121
+ const open = el.getAttribute('open')
122
+ const props: string[] = [`label="${label}"`]
123
+ if (open === true || open === 'true') props.push('open="true"')
124
+ const inner = serializeChildren(el, indent)
125
+ return `::collapsible{${props.join(' ')}}\n${inner}\n::`
126
+ }
127
+
128
+ case 'steps': {
129
+ const inner = serializeChildren(el, indent)
130
+ return `::steps\n${inner}\n::`
131
+ }
132
+
133
+ case 'card': {
134
+ const title = el.getAttribute('title') || ''
135
+ const icon = el.getAttribute('icon') || ''
136
+ const to = el.getAttribute('to') || ''
137
+ const props: string[] = []
138
+ if (title) props.push(`title="${title}"`)
139
+ if (icon) props.push(`icon="${icon}"`)
140
+ if (to) props.push(`to="${to}"`)
141
+ const inner = serializeChildren(el, indent)
142
+ return `::card{${props.join(' ')}}\n${inner}\n::`
143
+ }
144
+
145
+ case 'cardGroup': {
146
+ const inner = serializeChildren(el, indent)
147
+ return `::card-group\n${inner}\n::`
148
+ }
149
+
150
+ case 'codeCollapse': {
151
+ const inner = serializeChildren(el, indent)
152
+ return `::code-collapse\n${inner}\n::`
153
+ }
154
+
155
+ case 'codeGroup': {
156
+ const inner = serializeChildren(el, indent)
157
+ return `::code-group\n${inner}\n::`
158
+ }
159
+
160
+ case 'codePreview': {
161
+ const inner = serializeChildren(el, indent)
162
+ return `::code-preview\n${inner}\n::`
163
+ }
164
+
165
+ case 'codeTree': {
166
+ const files = el.getAttribute('files') || '[]'
167
+ return `::code-tree{files="${files}"}\n::`
168
+ }
169
+
170
+ case 'accordion':
171
+ return serializeSlottedComponent(el, 'accordion', 'accordionItem', 'item')
172
+
173
+ case 'tabs':
174
+ return serializeSlottedComponent(el, 'tabs', 'tabsItem', 'tab')
175
+
176
+ case 'field': {
177
+ const fieldName = el.getAttribute('name') || ''
178
+ const fieldType = el.getAttribute('type') || 'string'
179
+ const required = el.getAttribute('required')
180
+ const props: string[] = []
181
+ if (fieldName) props.push(`name="${fieldName}"`)
182
+ props.push(`type="${fieldType}"`)
183
+ if (required === true || required === 'true') props.push('required="true"')
184
+ const inner = serializeChildren(el, indent)
185
+ return `::field{${props.join(' ')}}\n${inner}\n::`
186
+ }
187
+
188
+ case 'fieldGroup': {
189
+ const inner = serializeChildren(el, indent)
190
+ return `::field-group\n${inner}\n::`
191
+ }
192
+
193
+ default: {
194
+ // Unknown node — try to serialize children
195
+ return serializeChildren(el, indent)
196
+ }
197
+ }
198
+ }
199
+
200
+ function serializeList(el: Y.XmlElement, type: 'bullet' | 'ordered', indent: string): string {
201
+ const lines: string[] = []
202
+ for (let i = 0; i < el.length; i++) {
203
+ const item = el.get(i)
204
+ if (item instanceof Y.XmlElement && item.nodeName === 'listItem') {
205
+ const prefix = type === 'bullet' ? '- ' : `${i + 1}. `
206
+ const content = elementTextContent(item)
207
+ lines.push(`${indent}${prefix}${content}`)
208
+ }
209
+ }
210
+ return lines.join('\n')
211
+ }
212
+
213
+ function serializeTaskList(el: Y.XmlElement, indent: string): string {
214
+ const lines: string[] = []
215
+ for (let i = 0; i < el.length; i++) {
216
+ const item = el.get(i)
217
+ if (item instanceof Y.XmlElement && item.nodeName === 'taskItem') {
218
+ const checked = item.getAttribute('checked')
219
+ const marker = (checked === true || checked === 'true') ? '[x]' : '[ ]'
220
+ const content = elementTextContent(item)
221
+ lines.push(`${indent}- ${marker} ${content}`)
222
+ }
223
+ }
224
+ return lines.join('\n')
225
+ }
226
+
227
+ function serializeTable(el: Y.XmlElement): string {
228
+ const rows: string[][] = []
229
+ let isHeader = true
230
+
231
+ for (let i = 0; i < el.length; i++) {
232
+ const row = el.get(i)
233
+ if (!(row instanceof Y.XmlElement) || row.nodeName !== 'tableRow') continue
234
+
235
+ const cells: string[] = []
236
+ for (let j = 0; j < row.length; j++) {
237
+ const cell = row.get(j)
238
+ if (cell instanceof Y.XmlElement) {
239
+ cells.push(elementTextContent(cell))
240
+ }
241
+ }
242
+ rows.push(cells)
243
+ }
244
+
245
+ if (!rows.length) return ''
246
+
247
+ const headerRow = rows[0]!
248
+ const lines = [`| ${headerRow.join(' | ')} |`]
249
+
250
+ // Separator
251
+ lines.push(`| ${headerRow.map(() => '---').join(' | ')} |`)
252
+
253
+ // Data rows
254
+ for (let i = 1; i < rows.length; i++) {
255
+ lines.push(`| ${rows[i]!.join(' | ')} |`)
256
+ }
257
+
258
+ return lines.join('\n')
259
+ }
260
+
261
+ function serializeSlottedComponent(
262
+ el: Y.XmlElement,
263
+ componentName: string,
264
+ childNodeName: string,
265
+ slotName: string
266
+ ): string {
267
+ const parts: string[] = [`::${componentName}`]
268
+
269
+ for (let i = 0; i < el.length; i++) {
270
+ const child = el.get(i)
271
+ if (!(child instanceof Y.XmlElement) || child.nodeName !== childNodeName) continue
272
+
273
+ const label = child.getAttribute('label') || `Item ${i + 1}`
274
+ const icon = child.getAttribute('icon') || ''
275
+ const props: string[] = [`label="${label}"`]
276
+ if (icon) props.push(`icon="${icon}"`)
277
+ parts.push(`#${slotName}{${props.join(' ')}}`)
278
+
279
+ const inner = serializeChildren(child, '')
280
+ if (inner) parts.push(inner)
281
+ }
282
+
283
+ parts.push('::')
284
+ return parts.join('\n')
285
+ }
286
+
287
+ function serializeChildren(el: Y.XmlElement | Y.XmlFragment, indent: string): string {
288
+ const parts: string[] = []
289
+ for (let i = 0; i < el.length; i++) {
290
+ const child = el.get(i)
291
+ if (child instanceof Y.XmlElement) {
292
+ const serialized = serializeElement(child, indent)
293
+ if (serialized) parts.push(serialized)
294
+ } else if (child instanceof Y.XmlText) {
295
+ const text = xmlTextToMarkdown(child)
296
+ if (text) parts.push(text)
297
+ }
298
+ }
299
+ return parts.join('\n\n')
300
+ }
301
+
302
+ // ── Public API ───────────────────────────────────────────────────────────────
303
+
304
+ /**
305
+ * Converts a Y.XmlFragment (TipTap document) to markdown.
306
+ * Extracts the title from the documentHeader element.
307
+ *
308
+ * @returns `{ title, markdown }` where title is the H1/header text
309
+ */
310
+ export function yjsToMarkdown(fragment: Y.XmlFragment): { title: string; markdown: string } {
311
+ let title = 'Untitled'
312
+ const bodyParts: string[] = []
313
+
314
+ for (let i = 0; i < fragment.length; i++) {
315
+ const child = fragment.get(i)
316
+ if (!(child instanceof Y.XmlElement)) continue
317
+
318
+ if (child.nodeName === 'documentHeader') {
319
+ title = elementTextContent(child) || 'Untitled'
320
+ continue
321
+ }
322
+
323
+ if (child.nodeName === 'documentMeta') {
324
+ continue
325
+ }
326
+
327
+ const serialized = serializeElement(child)
328
+ if (serialized !== '') {
329
+ bodyParts.push(serialized)
330
+ }
331
+ }
332
+
333
+ return { title, markdown: bodyParts.join('\n\n') }
334
+ }
package/src/index.ts ADDED
@@ -0,0 +1,103 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Abracadabra MCP Server — entry point.
4
+ *
5
+ * Environment variables:
6
+ * ABRA_URL (required) — Server URL (e.g. http://localhost:1234)
7
+ * ABRA_USERNAME (required) — Service account username
8
+ * ABRA_PASSWORD (required) — Service account password
9
+ * ABRA_AGENT_NAME — Display name (default: "AI Assistant")
10
+ * ABRA_AGENT_COLOR — HSL color for presence (default: "hsl(270, 80%, 60%)")
11
+ */
12
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
13
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
14
+ import { AbracadabraMCPServer } from './server.ts'
15
+ import { registerTreeTools } from './tools/tree.ts'
16
+ import { registerContentTools } from './tools/content.ts'
17
+ import { registerMetaTools } from './tools/meta.ts'
18
+ import { registerFileTools } from './tools/files.ts'
19
+ import { registerAwarenessTools } from './tools/awareness.ts'
20
+ import { registerChannelTools } from './tools/channel.ts'
21
+ import { registerAgentGuide } from './resources/agent-guide.ts'
22
+ import { registerTreeResource } from './resources/tree-resource.ts'
23
+ import { registerServerInfoResource } from './resources/server-info.ts'
24
+
25
+ async function main() {
26
+ const url = process.env.ABRA_URL
27
+ const username = process.env.ABRA_USERNAME
28
+ const password = process.env.ABRA_PASSWORD
29
+
30
+ if (!url || !username || !password) {
31
+ console.error('Missing required environment variables: ABRA_URL, ABRA_USERNAME, ABRA_PASSWORD')
32
+ process.exit(1)
33
+ }
34
+
35
+ // Create the Abracadabra connection manager
36
+ const server = new AbracadabraMCPServer({
37
+ url,
38
+ username,
39
+ password,
40
+ agentName: process.env.ABRA_AGENT_NAME,
41
+ agentColor: process.env.ABRA_AGENT_COLOR,
42
+ })
43
+
44
+ // Create MCP server with channel capability
45
+ const mcp = new McpServer(
46
+ { name: 'abracadabra', version: '1.0.0' },
47
+ {
48
+ capabilities: { experimental: { 'claude/channel': {} } },
49
+ instructions: `Events from the abracadabra channel arrive as <channel source="abracadabra" ...>. They may be:
50
+ - ai:task events from human users (includes sender, doc_id context)
51
+ - chat messages from the platform chat system (includes channel, sender, sender_id)
52
+ 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) with the appropriate context.`,
53
+ },
54
+ )
55
+
56
+ // Register tools
57
+ registerTreeTools(mcp, server)
58
+ registerContentTools(mcp, server)
59
+ registerMetaTools(mcp, server)
60
+ registerFileTools(mcp, server)
61
+ registerAwarenessTools(mcp, server)
62
+ registerChannelTools(mcp, server)
63
+
64
+ // Register resources
65
+ registerAgentGuide(mcp)
66
+ registerTreeResource(mcp, server)
67
+ registerServerInfoResource(mcp, server)
68
+
69
+ // Connect to Abracadabra server
70
+ try {
71
+ await server.connect()
72
+ } catch (error: any) {
73
+ console.error(`[abracadabra-mcp] Failed to connect: ${error.message}`)
74
+ process.exit(1)
75
+ }
76
+
77
+ // Start MCP stdio transport
78
+ const transport = new StdioServerTransport()
79
+ await mcp.connect(transport)
80
+
81
+ // Wire up real-time channel notifications (awareness → channel dispatch)
82
+ server.startChannelNotifications(mcp)
83
+ console.error('[abracadabra-mcp] MCP server running on stdio')
84
+
85
+ // Graceful shutdown
86
+ const shutdown = async () => {
87
+ console.error('[abracadabra-mcp] Shutting down...')
88
+ await server.destroy()
89
+ process.exit(0)
90
+ }
91
+
92
+ process.on('SIGINT', shutdown)
93
+ process.on('SIGTERM', shutdown)
94
+ }
95
+
96
+ main().catch((error) => {
97
+ console.error('[abracadabra-mcp] Fatal error:', error)
98
+ process.exit(1)
99
+ })
100
+
101
+ // Re-export for library usage
102
+ export { AbracadabraMCPServer } from './server.ts'
103
+ export type { MCPServerConfig } from './server.ts'