@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.
@@ -20001,7 +20001,7 @@ function registerTreeTools(mcp, server) {
20001
20001
  mcp.tool("create_document", "Create a new document in the tree. Returns the new document ID.", {
20002
20002
  parentId: z.string().optional().describe("Parent document ID. Omit for top-level pages. Use a document ID for nested/child pages."),
20003
20003
  label: z.string().describe("Display name / title for the document."),
20004
- 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."),
20004
+ 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."),
20005
20005
  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.")
20006
20006
  }, async ({ parentId, label, type, meta }) => {
20007
20007
  server.setAutoStatus("creating");
@@ -20159,7 +20159,7 @@ function registerTreeTools(mcp, server) {
20159
20159
  });
20160
20160
  mcp.tool("change_document_type", "Change the page type view of a document (data is preserved).", {
20161
20161
  id: z.string().describe("Document ID."),
20162
- type: z.string().describe("New page type (e.g. \"doc\", \"kanban\", \"table\", \"calendar\", \"outline\", \"gallery\", \"slides\", \"timeline\", \"whiteboard\", \"map\", \"dashboard\", \"mindmap\", \"graph\").")
20162
+ 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\".")
20163
20163
  }, async ({ id, type }) => {
20164
20164
  server.setAutoStatus("writing");
20165
20165
  server.setActiveToolCall({
@@ -20526,7 +20526,14 @@ function parseBlocks(markdown) {
20526
20526
  i++;
20527
20527
  }
20528
20528
  i++;
20529
- blocks.push({
20529
+ if (lang === "svg" || lang.startsWith("svg ")) {
20530
+ const svgTitle = lang === "svg" ? "" : lang.slice(4).trim();
20531
+ blocks.push({
20532
+ type: "svgEmbed",
20533
+ svg: codeLines.join("\n"),
20534
+ title: svgTitle
20535
+ });
20536
+ } else blocks.push({
20530
20537
  type: "codeBlock",
20531
20538
  lang,
20532
20539
  code: codeLines.join("\n")
@@ -20865,6 +20872,7 @@ function blockElName(b) {
20865
20872
  case "fieldGroup": return "fieldGroup";
20866
20873
  case "image": return "image";
20867
20874
  case "docEmbed": return "docEmbed";
20875
+ case "svgEmbed": return "svgEmbed";
20868
20876
  }
20869
20877
  }
20870
20878
  function fillBlock(el, block) {
@@ -21083,6 +21091,10 @@ function fillBlock(el, block) {
21083
21091
  case "docEmbed":
21084
21092
  el.setAttribute("docId", block.docId);
21085
21093
  break;
21094
+ case "svgEmbed":
21095
+ el.setAttribute("svg", block.svg);
21096
+ if (block.title) el.setAttribute("title", block.title);
21097
+ break;
21086
21098
  }
21087
21099
  }
21088
21100
  function populateYDocFromMarkdown(fragment, markdown, fallbackTitle = "Untitled") {
@@ -21132,6 +21144,7 @@ function populateYDocFromMarkdown(fragment, markdown, fallbackTitle = "Untitled"
21132
21144
  case "fieldGroup": return new Y.XmlElement("fieldGroup");
21133
21145
  case "image": return new Y.XmlElement("image");
21134
21146
  case "docEmbed": return new Y.XmlElement("docEmbed");
21147
+ case "svgEmbed": return new Y.XmlElement("svgEmbed");
21135
21148
  }
21136
21149
  });
21137
21150
  fragment.insert(0, [
@@ -21207,6 +21220,12 @@ function serializeElement(el, indent = "") {
21207
21220
  const docId = el.getAttribute("docId");
21208
21221
  return docId ? `![[${docId}]]` : "";
21209
21222
  }
21223
+ case "svgEmbed": {
21224
+ const svg = el.getAttribute("svg") || "";
21225
+ const svgTitle = el.getAttribute("title") || "";
21226
+ if (!svg) return "";
21227
+ return `\`\`\`svg${svgTitle ? ` ${svgTitle}` : ""}\n${svg}\n\`\`\``;
21228
+ }
21210
21229
  case "image": {
21211
21230
  const src = el.getAttribute("src") || "";
21212
21231
  const alt = el.getAttribute("alt") || "";
@@ -21518,7 +21537,7 @@ function registerMetaTools(mcp, server) {
21518
21537
  });
21519
21538
  mcp.tool("update_metadata", "Update metadata fields on a document. Merges the provided fields into existing metadata.", {
21520
21539
  docId: z.string().describe("Document ID."),
21521
- 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.")
21540
+ 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.")
21522
21541
  }, async ({ docId, meta }) => {
21523
21542
  server.setAutoStatus("writing", docId);
21524
21543
  server.setActiveToolCall({
@@ -21863,6 +21882,182 @@ function registerChannelTools(mcp, server) {
21863
21882
  });
21864
21883
  }
21865
21884
 
21885
+ //#endregion
21886
+ //#region packages/mcp/src/utils/sanitizeSvg.ts
21887
+ /**
21888
+ * Lightweight SVG sanitizer for server-side use (no DOM dependency).
21889
+ * Strips dangerous elements and attributes from SVG markup using allowlists.
21890
+ */
21891
+ const ALLOWED_ELEMENTS = new Set([
21892
+ "svg",
21893
+ "g",
21894
+ "path",
21895
+ "circle",
21896
+ "ellipse",
21897
+ "rect",
21898
+ "line",
21899
+ "polyline",
21900
+ "polygon",
21901
+ "text",
21902
+ "tspan",
21903
+ "textPath",
21904
+ "defs",
21905
+ "use",
21906
+ "symbol",
21907
+ "clipPath",
21908
+ "mask",
21909
+ "pattern",
21910
+ "linearGradient",
21911
+ "radialGradient",
21912
+ "stop",
21913
+ "marker",
21914
+ "image",
21915
+ "title",
21916
+ "desc",
21917
+ "metadata",
21918
+ "animate",
21919
+ "animateTransform",
21920
+ "animateMotion",
21921
+ "set",
21922
+ "filter",
21923
+ "feGaussianBlur",
21924
+ "feOffset",
21925
+ "feMerge",
21926
+ "feMergeNode",
21927
+ "feFlood",
21928
+ "feComposite",
21929
+ "feBlend",
21930
+ "feColorMatrix",
21931
+ "feComponentTransfer",
21932
+ "feFuncR",
21933
+ "feFuncG",
21934
+ "feFuncB",
21935
+ "feFuncA",
21936
+ "feConvolveMatrix",
21937
+ "feDiffuseLighting",
21938
+ "feDisplacementMap",
21939
+ "feDropShadow",
21940
+ "feImage",
21941
+ "feMorphology",
21942
+ "fePointLight",
21943
+ "feSpecularLighting",
21944
+ "feSpotLight",
21945
+ "feTile",
21946
+ "feTurbulence"
21947
+ ]);
21948
+ const FORBIDDEN_ELEMENTS = new Set([
21949
+ "script",
21950
+ "foreignObject",
21951
+ "iframe",
21952
+ "object",
21953
+ "embed",
21954
+ "applet"
21955
+ ]);
21956
+ /** Matches event handler attributes: on* */
21957
+ const EVENT_HANDLER_RE = /\bon\w+\s*=/gi;
21958
+ /** Matches javascript: URLs in href/xlink:href */
21959
+ const JS_URL_RE = /(href\s*=\s*["'])\s*javascript:/gi;
21960
+ /** Matches a full element tag (opening or self-closing) */
21961
+ const TAG_RE = /<\/?([a-zA-Z][a-zA-Z0-9-]*)((?:\s[^>]*)?)>/g;
21962
+ /** Matches complete forbidden element blocks including content */
21963
+ function stripForbiddenBlocks(svg) {
21964
+ for (const tag of FORBIDDEN_ELEMENTS) {
21965
+ const blockRe = new RegExp(`<${tag}[\\s>][\\s\\S]*?</${tag}>`, "gi");
21966
+ svg = svg.replace(blockRe, "");
21967
+ const selfCloseRe = new RegExp(`<${tag}[\\s/][^>]*/?>`, "gi");
21968
+ svg = svg.replace(selfCloseRe, "");
21969
+ }
21970
+ return svg;
21971
+ }
21972
+ /**
21973
+ * Sanitize SVG markup by removing dangerous elements and attributes.
21974
+ * Uses an allowlist approach — only known-safe SVG elements are kept.
21975
+ */
21976
+ function sanitizeSvg(svg) {
21977
+ if (!svg) return "";
21978
+ let clean = stripForbiddenBlocks(svg);
21979
+ clean = clean.replace(EVENT_HANDLER_RE, "");
21980
+ clean = clean.replace(JS_URL_RE, "$1");
21981
+ clean = clean.replace(TAG_RE, (match, tagName) => {
21982
+ const lower = tagName.toLowerCase();
21983
+ if (ALLOWED_ELEMENTS.has(lower) || ALLOWED_ELEMENTS.has(tagName)) return match;
21984
+ return "";
21985
+ });
21986
+ return clean.trim();
21987
+ }
21988
+
21989
+ //#endregion
21990
+ //#region packages/mcp/src/tools/svg.ts
21991
+ /**
21992
+ * SVG tools — insert SVG diagrams/images into documents via Y.js.
21993
+ */
21994
+ /**
21995
+ * Find the insertion index after documentHeader and documentMeta elements.
21996
+ */
21997
+ function findContentStart(fragment) {
21998
+ let idx = 0;
21999
+ for (let i = 0; i < fragment.length; i++) {
22000
+ const child = fragment.get(i);
22001
+ if (child instanceof Y.XmlElement) {
22002
+ const name = child.nodeName;
22003
+ if (name === "documentHeader" || name === "documentMeta") idx = i + 1;
22004
+ else break;
22005
+ }
22006
+ }
22007
+ return idx;
22008
+ }
22009
+ function registerSvgTools(mcp, server) {
22010
+ mcp.tool("write_svg", "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.", {
22011
+ docId: z.string().describe("Document ID to write SVG into."),
22012
+ svg: z.string().describe("Raw SVG markup string (the full <svg>...</svg> element). Will be sanitized."),
22013
+ title: z.string().optional().describe("Optional title/caption displayed above the SVG."),
22014
+ 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.")
22015
+ }, async ({ docId, svg, title, position }) => {
22016
+ try {
22017
+ server.setAutoStatus("writing", docId);
22018
+ server.setActiveToolCall({
22019
+ name: "write_svg",
22020
+ target: docId
22021
+ });
22022
+ const cleanSvg = sanitizeSvg(svg);
22023
+ if (!cleanSvg) {
22024
+ server.setActiveToolCall(null);
22025
+ return {
22026
+ content: [{
22027
+ type: "text",
22028
+ text: "Error: SVG markup was empty or entirely stripped by sanitizer."
22029
+ }],
22030
+ isError: true
22031
+ };
22032
+ }
22033
+ const doc = (await server.getChildProvider(docId)).document;
22034
+ const fragment = doc.getXmlFragment("default");
22035
+ doc.transact(() => {
22036
+ const el = new Y.XmlElement("svgEmbed");
22037
+ el.setAttribute("svg", cleanSvg);
22038
+ if (title) el.setAttribute("title", title);
22039
+ const insertPos = position === "prepend" ? findContentStart(fragment) : fragment.length;
22040
+ fragment.insert(insertPos, [el]);
22041
+ });
22042
+ server.setFocusedDoc(docId);
22043
+ server.setActiveToolCall(null);
22044
+ return { content: [{
22045
+ type: "text",
22046
+ text: `SVG inserted into document ${docId}${title ? ` ("${title}")` : ""}`
22047
+ }] };
22048
+ } catch (error) {
22049
+ server.setActiveToolCall(null);
22050
+ return {
22051
+ content: [{
22052
+ type: "text",
22053
+ text: `Error writing SVG: ${error.message}`
22054
+ }],
22055
+ isError: true
22056
+ };
22057
+ }
22058
+ });
22059
+ }
22060
+
21866
22061
  //#endregion
21867
22062
  //#region packages/mcp/src/resources/agent-guide.ts
21868
22063
  const AGENT_GUIDE = `# Abracadabra AI Agent Guide
@@ -22655,6 +22850,7 @@ Read the resource at abracadabra://agent-guide for the complete guide covering p
22655
22850
  registerFileTools(mcp, server);
22656
22851
  registerAwarenessTools(mcp, server);
22657
22852
  registerChannelTools(mcp, server);
22853
+ registerSvgTools(mcp, server);
22658
22854
  registerAgentGuide(mcp);
22659
22855
  registerTreeResource(mcp, server);
22660
22856
  registerServerInfoResource(mcp, server);