@abraca/mcp 1.0.22 → 1.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abraca/mcp",
3
- "version": "1.0.22",
3
+ "version": "1.1.0",
4
4
  "description": "MCP server for Abracadabra — AI agent collaboration on CRDT documents",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -248,6 +248,7 @@ type Block =
248
248
  | { type: 'fieldGroup'; fields: Block[] }
249
249
  | { type: 'image'; src: string; alt: string; width?: string; height?: string }
250
250
  | { type: 'docEmbed'; docId: string }
251
+ | { type: 'svgEmbed'; svg: string; title: string }
251
252
 
252
253
  function parseTableRow(line: string): string[] {
253
254
  const parts = line.split('|')
@@ -359,7 +360,12 @@ function parseBlocks(markdown: string): Block[] {
359
360
  i++
360
361
  }
361
362
  i++
362
- blocks.push({ type: 'codeBlock', lang, code: codeLines.join('\n') })
363
+ if (lang === 'svg' || lang.startsWith('svg ')) {
364
+ const svgTitle = lang === 'svg' ? '' : lang.slice(4).trim()
365
+ blocks.push({ type: 'svgEmbed', svg: codeLines.join('\n'), title: svgTitle })
366
+ } else {
367
+ blocks.push({ type: 'codeBlock', lang, code: codeLines.join('\n') })
368
+ }
363
369
  continue
364
370
  }
365
371
 
@@ -635,6 +641,7 @@ function blockElName(b: Block): string {
635
641
  case 'fieldGroup': return 'fieldGroup'
636
642
  case 'image': return 'image'
637
643
  case 'docEmbed': return 'docEmbed'
644
+ case 'svgEmbed': return 'svgEmbed'
638
645
  }
639
646
  }
640
647
 
@@ -832,6 +839,11 @@ function fillBlock(el: Y.XmlElement, block: Block): void {
832
839
  el.setAttribute('docId', block.docId)
833
840
  break
834
841
  }
842
+ case 'svgEmbed': {
843
+ el.setAttribute('svg', block.svg)
844
+ if (block.title) el.setAttribute('title', block.title)
845
+ break
846
+ }
835
847
  }
836
848
  }
837
849
 
@@ -888,6 +900,7 @@ export function populateYDocFromMarkdown(
888
900
  case 'fieldGroup': return new Y.XmlElement('fieldGroup')
889
901
  case 'image': return new Y.XmlElement('image')
890
902
  case 'docEmbed': return new Y.XmlElement('docEmbed')
903
+ case 'svgEmbed': return new Y.XmlElement('svgEmbed')
891
904
  }
892
905
  })
893
906
 
@@ -98,15 +98,69 @@ export interface PageMeta extends Record<string, unknown> {
98
98
  spModelUploadId?: string
99
99
  spModelDocId?: string
100
100
 
101
+ // Slides
102
+ slidesTransition?: string
103
+ slidesTheme?: string
104
+
105
+ // Media metadata (extracted on import)
106
+ mediaDuration?: number
107
+ mediaWidth?: number
108
+ mediaHeight?: number
109
+ mediaCamera?: string
110
+ mediaLens?: string
111
+ mediaIso?: number
112
+ mediaFocalLength?: number
113
+ mediaAperture?: number
114
+ mediaShutterSpeed?: string
115
+ mediaArtist?: string
116
+ mediaAlbum?: string
117
+ mediaGenre?: string
118
+ mediaYear?: number
119
+ dateTaken?: string
120
+
121
+ // Sheets cell formatting
122
+ bold?: boolean
123
+ italic?: boolean
124
+ textColor?: string
125
+ bgColor?: string
126
+ align?: string
127
+ formula?: string
128
+
101
129
  // Renderer config (set on the page doc itself, not children)
102
130
  kanbanColumnWidth?: string
103
131
  galleryColumns?: number
104
132
  galleryAspect?: string
133
+ galleryCardStyle?: string
134
+ galleryShowLabels?: boolean
135
+ gallerySortBy?: string
105
136
  calendarView?: string
106
137
  calendarWeekStart?: string
107
138
  calendarShowWeekNumbers?: boolean
108
139
  tableMode?: string
140
+ tableSortDir?: string
141
+ tableColumns?: any[]
142
+ tableColumnWidths?: Record<string, number>
143
+ tableColumnOrder?: string[]
109
144
  timelineZoom?: string
145
+ checklistFilter?: string
146
+ checklistSort?: string
147
+ mapShowLabels?: boolean
148
+ spatialGridVisible?: boolean
149
+ chartType?: string
150
+ chartMetric?: string
151
+ chartColorScheme?: string
152
+ chartLimit?: number
153
+ chartShowLegend?: boolean
154
+ chartShowValues?: boolean
155
+ mediaRepeat?: string
156
+ mediaShuffle?: boolean
157
+ sheetsDefaultColWidth?: number
158
+ sheetsDefaultRowHeight?: number
159
+ sheetsShowGridlines?: boolean
160
+ sheetsColumnWidths?: Record<string, number>
161
+ sheetsRowHeights?: Record<string, number>
162
+ sheetsFreezeRows?: number
163
+ sheetsFreezeCols?: number
110
164
 
111
165
  // Internal
112
166
  _metaFields?: UserMetaField[]
@@ -100,6 +100,13 @@ function serializeElement(el: Y.XmlElement, indent = ''): string {
100
100
  return docId ? `![[${docId}]]` : ''
101
101
  }
102
102
 
103
+ case 'svgEmbed': {
104
+ const svg = el.getAttribute('svg') || ''
105
+ const svgTitle = el.getAttribute('title') || ''
106
+ if (!svg) return ''
107
+ return `\`\`\`svg${svgTitle ? ` ${svgTitle}` : ''}\n${svg}\n\`\`\``
108
+ }
109
+
103
110
  case 'image': {
104
111
  const src = el.getAttribute('src') || ''
105
112
  const alt = el.getAttribute('alt') || ''
package/src/index.ts CHANGED
@@ -18,6 +18,7 @@ import { registerMetaTools } from './tools/meta.ts'
18
18
  import { registerFileTools } from './tools/files.ts'
19
19
  import { registerAwarenessTools } from './tools/awareness.ts'
20
20
  import { registerChannelTools } from './tools/channel.ts'
21
+ import { registerSvgTools } from './tools/svg.ts'
21
22
  import { registerAgentGuide } from './resources/agent-guide.ts'
22
23
  import { registerTreeResource } from './resources/tree-resource.ts'
23
24
  import { registerServerInfoResource } from './resources/server-info.ts'
@@ -91,6 +92,7 @@ Read the resource at abracadabra://agent-guide for the complete guide covering p
91
92
  registerFileTools(mcp, server)
92
93
  registerAwarenessTools(mcp, server)
93
94
  registerChannelTools(mcp, server)
95
+ registerSvgTools(mcp, server)
94
96
 
95
97
  // Register resources
96
98
  registerAgentGuide(mcp)
package/src/tools/meta.ts CHANGED
@@ -47,7 +47,7 @@ export function registerMetaTools(mcp: McpServer, server: AbracadabraMCPServer)
47
47
  'Update metadata fields on a document. Merges the provided fields into existing metadata.',
48
48
  {
49
49
  docId: z.string().describe('Document ID.'),
50
- meta: z.record(z.unknown()).describe('Metadata fields to update (merged with existing). Universal keys: color (hex), icon (Lucide kebab-case — NEVER emoji), dateStart/dateEnd, datetimeStart/datetimeEnd, allDay, tags (string[]), checked (bool), priority (0=none,1=low,2=med,3=high,4=urgent), status, rating (0-5), url, email, phone, number, unit, subtitle, note, taskProgress (0-100), members ({id,label}[]), coverUploadId. Geo/Map: geoType ("marker"|"line"|"measure"), geoLat, geoLng, geoDescription. Spatial 3D: spShape ("box"|"sphere"|"cylinder"|"cone"|"plane"|"torus"|"glb"), spX/spY/spZ, spRX/spRY/spRZ, spSX/spSY/spSZ, spColor, spOpacity (0-100). Dashboard: deskX, deskY, deskZ, deskMode ("icon"|"widget-sm"|"widget-lg"). Renderer config (on the page doc itself): kanbanColumnWidth, galleryColumns, galleryAspect, calendarView, calendarWeekStart, tableMode, showRefEdges. Set a key to null to clear it.'),
50
+ meta: z.record(z.unknown()).describe('Metadata fields to update (merged with existing). Universal keys: color (hex), icon (Lucide kebab-case — NEVER emoji), dateStart/dateEnd, datetimeStart/datetimeEnd, allDay, tags (string[]), checked (bool), priority (0=none,1=low,2=med,3=high,4=urgent), status, rating (0-5), url, email, phone, number, unit, subtitle, note, taskProgress (0-100), members ({id,label}[]), coverUploadId. Geo/Map: geoType ("marker"|"line"|"measure"), geoLat, geoLng, geoDescription. Spatial 3D: spShape ("box"|"sphere"|"cylinder"|"cone"|"plane"|"torus"|"glb"), spX/spY/spZ, spRX/spRY/spRZ, spSX/spSY/spSZ, spColor, spOpacity (0-100). Dashboard: deskX, deskY, deskZ, deskMode ("icon"|"widget-sm"|"widget-lg"). Slides: slidesTransition ("none"|"fade"|"slide"), slidesTheme ("dark"|"light"). Chart: chartType ("bar"|"stacked bar"|"line"|"donut"|"treemap"), chartMetric, chartColorScheme, chartLimit, chartShowLegend, chartShowValues. Sheets: sheetsDefaultColWidth, sheetsDefaultRowHeight, sheetsShowGridlines, sheetsFreezeRows, sheetsFreezeCols. Cell formatting: bold, italic, textColor, bgColor, align, formula. Renderer config (on the page doc itself): kanbanColumnWidth, galleryColumns, galleryAspect, galleryCardStyle, galleryShowLabels, gallerySortBy, calendarView, calendarWeekStart, calendarShowWeekNumbers, tableMode, tableSortDir, checklistFilter, checklistSort, mapShowLabels, spatialGridVisible, showRefEdges, mediaRepeat, mediaShuffle. Set a key to null to clear it.'),
51
51
  },
52
52
  async ({ docId, meta }) => {
53
53
  server.setAutoStatus('writing', docId)
@@ -0,0 +1,83 @@
1
+ /**
2
+ * SVG tools — insert SVG diagrams/images into documents via Y.js.
3
+ */
4
+ import * as Y from 'yjs'
5
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
6
+ import { z } from 'zod'
7
+ import type { AbracadabraMCPServer } from '../server.ts'
8
+ import { sanitizeSvg } from '../utils/sanitizeSvg.ts'
9
+
10
+ /**
11
+ * Find the insertion index after documentHeader and documentMeta elements.
12
+ */
13
+ function findContentStart(fragment: Y.XmlFragment): number {
14
+ let idx = 0
15
+ for (let i = 0; i < fragment.length; i++) {
16
+ const child = fragment.get(i)
17
+ if (child instanceof Y.XmlElement) {
18
+ const name = child.nodeName
19
+ if (name === 'documentHeader' || name === 'documentMeta') {
20
+ idx = i + 1
21
+ } else {
22
+ break
23
+ }
24
+ }
25
+ }
26
+ return idx
27
+ }
28
+
29
+ export function registerSvgTools(mcp: McpServer, server: AbracadabraMCPServer) {
30
+ mcp.tool(
31
+ 'write_svg',
32
+ 'Insert an SVG diagram or image into a document. Creates an svgEmbed block node containing the SVG markup. Use this for diagrams, charts, flowcharts, illustrations, icons, or any visual content expressed as SVG. The SVG is sanitized server-side before writing.',
33
+ {
34
+ docId: z.string().describe('Document ID to write SVG into.'),
35
+ svg: z.string().describe('Raw SVG markup string (the full <svg>...</svg> element). Will be sanitized.'),
36
+ title: z.string().optional().describe('Optional title/caption displayed above the SVG.'),
37
+ position: z.enum(['append', 'prepend']).optional().describe('Where to insert the SVG block. "append" adds at the end (default), "prepend" adds at the start of content.'),
38
+ },
39
+ async ({ docId, svg, title, position }) => {
40
+ try {
41
+ server.setAutoStatus('writing', docId)
42
+ server.setActiveToolCall({ name: 'write_svg', target: docId })
43
+
44
+ const cleanSvg = sanitizeSvg(svg)
45
+ if (!cleanSvg) {
46
+ server.setActiveToolCall(null)
47
+ return {
48
+ content: [{ type: 'text', text: 'Error: SVG markup was empty or entirely stripped by sanitizer.' }],
49
+ isError: true,
50
+ }
51
+ }
52
+
53
+ const provider = await server.getChildProvider(docId)
54
+ const doc = provider.document
55
+ const fragment = doc.getXmlFragment('default')
56
+
57
+ doc.transact(() => {
58
+ const el = new Y.XmlElement('svgEmbed')
59
+ el.setAttribute('svg', cleanSvg)
60
+ if (title) el.setAttribute('title', title)
61
+
62
+ const insertPos = position === 'prepend'
63
+ ? findContentStart(fragment)
64
+ : fragment.length
65
+ fragment.insert(insertPos, [el])
66
+ })
67
+
68
+ server.setFocusedDoc(docId)
69
+ server.setActiveToolCall(null)
70
+
71
+ return {
72
+ content: [{ type: 'text', text: `SVG inserted into document ${docId}${title ? ` ("${title}")` : ''}` }],
73
+ }
74
+ } catch (error: any) {
75
+ server.setActiveToolCall(null)
76
+ return {
77
+ content: [{ type: 'text', text: `Error writing SVG: ${error.message}` }],
78
+ isError: true,
79
+ }
80
+ }
81
+ }
82
+ )
83
+ }
package/src/tools/tree.ts CHANGED
@@ -200,7 +200,7 @@ export function registerTreeTools(mcp: McpServer, server: AbracadabraMCPServer)
200
200
  {
201
201
  parentId: z.string().optional().describe('Parent document ID. Omit for top-level pages. Use a document ID for nested/child pages.'),
202
202
  label: z.string().describe('Display name / title for the document.'),
203
- type: z.string().optional().describe('Page type — sets how this document renders. "doc" (rich text), "kanban" (columns → cards), "table" (columns → cells, positional rows), "calendar" (events with datetimeStart/End), "timeline" (epics → tasks with dateStart/End + taskProgress), "checklist" (tasks with checked/priority, unlimited nesting), "outline" (nested items, unlimited depth), "gallery" (image/media items), "map" (markers/lines with geoLat/geoLng), "graph" (knowledge graph nodes), "dashboard" (positioned widgets with deskX/deskY/deskMode), "mindmap" (connected nodes), "spatial" (3D objects with spShape/spX/spY/spZ), "media" (audio/video tracks), "slides" (slide deck), "whiteboard" (freeform canvas). Omit to inherit parent view. Only set on the parent page, NEVER on child items.'),
203
+ type: z.string().optional().describe('Page type — sets how this document renders. "doc" (rich text), "kanban" (columns → cards), "table" (columns → rows with custom fields), "calendar" (events with datetimeStart/End), "timeline" (epics → tasks with dateStart/End + taskProgress), "checklist" (tasks with checked/priority, unlimited nesting), "outline" (nested items, unlimited depth), "gallery" (visual grid with covers/ratings), "map" (markers/lines with geoLat/geoLng), "graph" (force-directed knowledge graph), "dashboard" (positioned widgets with deskX/deskY/deskMode), "spatial" (3D scene with spShape/spX/spY/spZ), "media" (audio/video player with playlists), "slides" (presentation with transitions), "chart" (bar/line/donut/treemap from data points or aggregation), "sheets" (spreadsheet with formulas and formatting), "overview" (space home — activity and stats), "call" (video call room, no children). Omit to inherit parent view. Only set on the parent page, NEVER on child items.'),
204
204
  meta: z.record(z.unknown()).optional().describe('Initial metadata (PageMeta fields: color as hex string, icon as Lucide kebab-case name like "star"/"code-2"/"users" — never emoji, dateStart, dateEnd, priority 0-4, tags array, etc). Omit icon entirely to use page type default.'),
205
205
  },
206
206
  async ({ parentId, label, type, meta }) => {
@@ -350,7 +350,7 @@ export function registerTreeTools(mcp: McpServer, server: AbracadabraMCPServer)
350
350
  'Change the page type view of a document (data is preserved).',
351
351
  {
352
352
  id: z.string().describe('Document ID.'),
353
- type: z.string().describe('New page type (e.g. "doc", "kanban", "table", "calendar", "outline", "gallery", "slides", "timeline", "whiteboard", "map", "dashboard", "mindmap", "graph").'),
353
+ type: z.string().describe('New page type: "doc", "kanban", "table", "calendar", "timeline", "checklist", "outline", "gallery", "map", "graph", "dashboard", "spatial", "media", "slides", "chart", "sheets", "overview", "call".'),
354
354
  },
355
355
  async ({ id, type }) => {
356
356
  server.setAutoStatus('writing')
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Lightweight SVG sanitizer for server-side use (no DOM dependency).
3
+ * Strips dangerous elements and attributes from SVG markup using allowlists.
4
+ */
5
+
6
+ const ALLOWED_ELEMENTS = new Set([
7
+ 'svg', 'g', 'path', 'circle', 'ellipse', 'rect', 'line', 'polyline',
8
+ 'polygon', 'text', 'tspan', 'textPath', 'defs', 'use', 'symbol',
9
+ 'clipPath', 'mask', 'pattern', 'linearGradient', 'radialGradient', 'stop',
10
+ 'marker', 'image', 'title', 'desc', 'metadata',
11
+ 'animate', 'animateTransform', 'animateMotion', 'set',
12
+ 'filter', 'feGaussianBlur', 'feOffset', 'feMerge', 'feMergeNode',
13
+ 'feFlood', 'feComposite', 'feBlend', 'feColorMatrix', 'feComponentTransfer',
14
+ 'feFuncR', 'feFuncG', 'feFuncB', 'feFuncA', 'feConvolveMatrix',
15
+ 'feDiffuseLighting', 'feDisplacementMap', 'feDropShadow', 'feImage',
16
+ 'feMorphology', 'fePointLight', 'feSpecularLighting', 'feSpotLight',
17
+ 'feTile', 'feTurbulence',
18
+ ])
19
+
20
+ const FORBIDDEN_ELEMENTS = new Set([
21
+ 'script', 'foreignObject', 'iframe', 'object', 'embed', 'applet',
22
+ ])
23
+
24
+ /** Matches event handler attributes: on* */
25
+ const EVENT_HANDLER_RE = /\bon\w+\s*=/gi
26
+
27
+ /** Matches javascript: URLs in href/xlink:href */
28
+ const JS_URL_RE = /(href\s*=\s*["'])\s*javascript:/gi
29
+
30
+ /** Matches a full element tag (opening or self-closing) */
31
+ const TAG_RE = /<\/?([a-zA-Z][a-zA-Z0-9-]*)((?:\s[^>]*)?)>/g
32
+
33
+ /** Matches complete forbidden element blocks including content */
34
+ function stripForbiddenBlocks(svg: string): string {
35
+ for (const tag of FORBIDDEN_ELEMENTS) {
36
+ // Strip both <tag>...</tag> and self-closing <tag ... />
37
+ const blockRe = new RegExp(`<${tag}[\\s>][\\s\\S]*?</${tag}>`, 'gi')
38
+ svg = svg.replace(blockRe, '')
39
+ const selfCloseRe = new RegExp(`<${tag}[\\s/][^>]*/?>`, 'gi')
40
+ svg = svg.replace(selfCloseRe, '')
41
+ }
42
+ return svg
43
+ }
44
+
45
+ /**
46
+ * Sanitize SVG markup by removing dangerous elements and attributes.
47
+ * Uses an allowlist approach — only known-safe SVG elements are kept.
48
+ */
49
+ export function sanitizeSvg(svg: string): string {
50
+ if (!svg) return ''
51
+
52
+ // 1. Strip forbidden element blocks (script, foreignObject, etc.)
53
+ let clean = stripForbiddenBlocks(svg)
54
+
55
+ // 2. Strip event handler attributes
56
+ clean = clean.replace(EVENT_HANDLER_RE, '')
57
+
58
+ // 3. Strip javascript: URLs
59
+ clean = clean.replace(JS_URL_RE, '$1')
60
+
61
+ // 4. Remove any elements not in the allowlist
62
+ clean = clean.replace(TAG_RE, (match, tagName: string) => {
63
+ const lower = tagName.toLowerCase()
64
+ if (ALLOWED_ELEMENTS.has(lower) || ALLOWED_ELEMENTS.has(tagName)) {
65
+ return match
66
+ }
67
+ return ''
68
+ })
69
+
70
+ return clean.trim()
71
+ }