@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.
- package/dist/abracadabra-mcp.cjs +21007 -0
- package/dist/abracadabra-mcp.cjs.map +1 -0
- package/dist/abracadabra-mcp.esm.js +20999 -0
- package/dist/abracadabra-mcp.esm.js.map +1 -0
- package/dist/index.d.ts +8785 -0
- package/package.json +39 -0
- package/src/converters/markdownToYjs.ts +852 -0
- package/src/converters/types.ts +75 -0
- package/src/converters/yjsToMarkdown.ts +334 -0
- package/src/index.ts +103 -0
- package/src/resources/agent-guide.ts +324 -0
- package/src/resources/server-info.ts +47 -0
- package/src/resources/tree-resource.ts +71 -0
- package/src/server.ts +431 -0
- package/src/tools/awareness.ts +156 -0
- package/src/tools/channel.ts +92 -0
- package/src/tools/content.ts +133 -0
- package/src/tools/files.ts +110 -0
- package/src/tools/meta.ts +59 -0
- package/src/tools/tree.ts +296 -0
- package/src/utils.ts +37 -0
|
@@ -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 `${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'
|