@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/dist/abracadabra-mcp.cjs +200 -4
- package/dist/abracadabra-mcp.cjs.map +1 -1
- package/dist/abracadabra-mcp.esm.js +200 -4
- package/dist/abracadabra-mcp.esm.js.map +1 -1
- package/package.json +1 -1
- package/src/converters/markdownToYjs.ts +14 -1
- package/src/converters/types.ts +54 -0
- package/src/converters/yjsToMarkdown.ts +7 -0
- package/src/index.ts +2 -0
- package/src/tools/meta.ts +1 -1
- package/src/tools/svg.ts +83 -0
- package/src/tools/tree.ts +2 -2
- package/src/utils/sanitizeSvg.ts +71 -0
package/dist/abracadabra-mcp.cjs
CHANGED
|
@@ -20005,7 +20005,7 @@ function registerTreeTools(mcp, server) {
|
|
|
20005
20005
|
mcp.tool("create_document", "Create a new document in the tree. Returns the new document ID.", {
|
|
20006
20006
|
parentId: zod.z.string().optional().describe("Parent document ID. Omit for top-level pages. Use a document ID for nested/child pages."),
|
|
20007
20007
|
label: zod.z.string().describe("Display name / title for the document."),
|
|
20008
|
-
type: zod.z.string().optional().describe("Page type — sets how this document renders. \"doc\" (rich text), \"kanban\" (columns → cards), \"table\" (columns →
|
|
20008
|
+
type: zod.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."),
|
|
20009
20009
|
meta: zod.z.record(zod.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.")
|
|
20010
20010
|
}, async ({ parentId, label, type, meta }) => {
|
|
20011
20011
|
server.setAutoStatus("creating");
|
|
@@ -20163,7 +20163,7 @@ function registerTreeTools(mcp, server) {
|
|
|
20163
20163
|
});
|
|
20164
20164
|
mcp.tool("change_document_type", "Change the page type view of a document (data is preserved).", {
|
|
20165
20165
|
id: zod.z.string().describe("Document ID."),
|
|
20166
|
-
type: zod.z.string().describe("New page type
|
|
20166
|
+
type: zod.z.string().describe("New page type: \"doc\", \"kanban\", \"table\", \"calendar\", \"timeline\", \"checklist\", \"outline\", \"gallery\", \"map\", \"graph\", \"dashboard\", \"spatial\", \"media\", \"slides\", \"chart\", \"sheets\", \"overview\", \"call\".")
|
|
20167
20167
|
}, async ({ id, type }) => {
|
|
20168
20168
|
server.setAutoStatus("writing");
|
|
20169
20169
|
server.setActiveToolCall({
|
|
@@ -20530,7 +20530,14 @@ function parseBlocks(markdown) {
|
|
|
20530
20530
|
i++;
|
|
20531
20531
|
}
|
|
20532
20532
|
i++;
|
|
20533
|
-
|
|
20533
|
+
if (lang === "svg" || lang.startsWith("svg ")) {
|
|
20534
|
+
const svgTitle = lang === "svg" ? "" : lang.slice(4).trim();
|
|
20535
|
+
blocks.push({
|
|
20536
|
+
type: "svgEmbed",
|
|
20537
|
+
svg: codeLines.join("\n"),
|
|
20538
|
+
title: svgTitle
|
|
20539
|
+
});
|
|
20540
|
+
} else blocks.push({
|
|
20534
20541
|
type: "codeBlock",
|
|
20535
20542
|
lang,
|
|
20536
20543
|
code: codeLines.join("\n")
|
|
@@ -20869,6 +20876,7 @@ function blockElName(b) {
|
|
|
20869
20876
|
case "fieldGroup": return "fieldGroup";
|
|
20870
20877
|
case "image": return "image";
|
|
20871
20878
|
case "docEmbed": return "docEmbed";
|
|
20879
|
+
case "svgEmbed": return "svgEmbed";
|
|
20872
20880
|
}
|
|
20873
20881
|
}
|
|
20874
20882
|
function fillBlock(el, block) {
|
|
@@ -21087,6 +21095,10 @@ function fillBlock(el, block) {
|
|
|
21087
21095
|
case "docEmbed":
|
|
21088
21096
|
el.setAttribute("docId", block.docId);
|
|
21089
21097
|
break;
|
|
21098
|
+
case "svgEmbed":
|
|
21099
|
+
el.setAttribute("svg", block.svg);
|
|
21100
|
+
if (block.title) el.setAttribute("title", block.title);
|
|
21101
|
+
break;
|
|
21090
21102
|
}
|
|
21091
21103
|
}
|
|
21092
21104
|
function populateYDocFromMarkdown(fragment, markdown, fallbackTitle = "Untitled") {
|
|
@@ -21136,6 +21148,7 @@ function populateYDocFromMarkdown(fragment, markdown, fallbackTitle = "Untitled"
|
|
|
21136
21148
|
case "fieldGroup": return new yjs.XmlElement("fieldGroup");
|
|
21137
21149
|
case "image": return new yjs.XmlElement("image");
|
|
21138
21150
|
case "docEmbed": return new yjs.XmlElement("docEmbed");
|
|
21151
|
+
case "svgEmbed": return new yjs.XmlElement("svgEmbed");
|
|
21139
21152
|
}
|
|
21140
21153
|
});
|
|
21141
21154
|
fragment.insert(0, [
|
|
@@ -21211,6 +21224,12 @@ function serializeElement(el, indent = "") {
|
|
|
21211
21224
|
const docId = el.getAttribute("docId");
|
|
21212
21225
|
return docId ? `![[${docId}]]` : "";
|
|
21213
21226
|
}
|
|
21227
|
+
case "svgEmbed": {
|
|
21228
|
+
const svg = el.getAttribute("svg") || "";
|
|
21229
|
+
const svgTitle = el.getAttribute("title") || "";
|
|
21230
|
+
if (!svg) return "";
|
|
21231
|
+
return `\`\`\`svg${svgTitle ? ` ${svgTitle}` : ""}\n${svg}\n\`\`\``;
|
|
21232
|
+
}
|
|
21214
21233
|
case "image": {
|
|
21215
21234
|
const src = el.getAttribute("src") || "";
|
|
21216
21235
|
const alt = el.getAttribute("alt") || "";
|
|
@@ -21525,7 +21544,7 @@ function registerMetaTools(mcp, server) {
|
|
|
21525
21544
|
});
|
|
21526
21545
|
mcp.tool("update_metadata", "Update metadata fields on a document. Merges the provided fields into existing metadata.", {
|
|
21527
21546
|
docId: zod.z.string().describe("Document ID."),
|
|
21528
|
-
meta: zod.z.record(zod.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.")
|
|
21547
|
+
meta: zod.z.record(zod.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.")
|
|
21529
21548
|
}, async ({ docId, meta }) => {
|
|
21530
21549
|
server.setAutoStatus("writing", docId);
|
|
21531
21550
|
server.setActiveToolCall({
|
|
@@ -21870,6 +21889,182 @@ function registerChannelTools(mcp, server) {
|
|
|
21870
21889
|
});
|
|
21871
21890
|
}
|
|
21872
21891
|
|
|
21892
|
+
//#endregion
|
|
21893
|
+
//#region packages/mcp/src/utils/sanitizeSvg.ts
|
|
21894
|
+
/**
|
|
21895
|
+
* Lightweight SVG sanitizer for server-side use (no DOM dependency).
|
|
21896
|
+
* Strips dangerous elements and attributes from SVG markup using allowlists.
|
|
21897
|
+
*/
|
|
21898
|
+
const ALLOWED_ELEMENTS = new Set([
|
|
21899
|
+
"svg",
|
|
21900
|
+
"g",
|
|
21901
|
+
"path",
|
|
21902
|
+
"circle",
|
|
21903
|
+
"ellipse",
|
|
21904
|
+
"rect",
|
|
21905
|
+
"line",
|
|
21906
|
+
"polyline",
|
|
21907
|
+
"polygon",
|
|
21908
|
+
"text",
|
|
21909
|
+
"tspan",
|
|
21910
|
+
"textPath",
|
|
21911
|
+
"defs",
|
|
21912
|
+
"use",
|
|
21913
|
+
"symbol",
|
|
21914
|
+
"clipPath",
|
|
21915
|
+
"mask",
|
|
21916
|
+
"pattern",
|
|
21917
|
+
"linearGradient",
|
|
21918
|
+
"radialGradient",
|
|
21919
|
+
"stop",
|
|
21920
|
+
"marker",
|
|
21921
|
+
"image",
|
|
21922
|
+
"title",
|
|
21923
|
+
"desc",
|
|
21924
|
+
"metadata",
|
|
21925
|
+
"animate",
|
|
21926
|
+
"animateTransform",
|
|
21927
|
+
"animateMotion",
|
|
21928
|
+
"set",
|
|
21929
|
+
"filter",
|
|
21930
|
+
"feGaussianBlur",
|
|
21931
|
+
"feOffset",
|
|
21932
|
+
"feMerge",
|
|
21933
|
+
"feMergeNode",
|
|
21934
|
+
"feFlood",
|
|
21935
|
+
"feComposite",
|
|
21936
|
+
"feBlend",
|
|
21937
|
+
"feColorMatrix",
|
|
21938
|
+
"feComponentTransfer",
|
|
21939
|
+
"feFuncR",
|
|
21940
|
+
"feFuncG",
|
|
21941
|
+
"feFuncB",
|
|
21942
|
+
"feFuncA",
|
|
21943
|
+
"feConvolveMatrix",
|
|
21944
|
+
"feDiffuseLighting",
|
|
21945
|
+
"feDisplacementMap",
|
|
21946
|
+
"feDropShadow",
|
|
21947
|
+
"feImage",
|
|
21948
|
+
"feMorphology",
|
|
21949
|
+
"fePointLight",
|
|
21950
|
+
"feSpecularLighting",
|
|
21951
|
+
"feSpotLight",
|
|
21952
|
+
"feTile",
|
|
21953
|
+
"feTurbulence"
|
|
21954
|
+
]);
|
|
21955
|
+
const FORBIDDEN_ELEMENTS = new Set([
|
|
21956
|
+
"script",
|
|
21957
|
+
"foreignObject",
|
|
21958
|
+
"iframe",
|
|
21959
|
+
"object",
|
|
21960
|
+
"embed",
|
|
21961
|
+
"applet"
|
|
21962
|
+
]);
|
|
21963
|
+
/** Matches event handler attributes: on* */
|
|
21964
|
+
const EVENT_HANDLER_RE = /\bon\w+\s*=/gi;
|
|
21965
|
+
/** Matches javascript: URLs in href/xlink:href */
|
|
21966
|
+
const JS_URL_RE = /(href\s*=\s*["'])\s*javascript:/gi;
|
|
21967
|
+
/** Matches a full element tag (opening or self-closing) */
|
|
21968
|
+
const TAG_RE = /<\/?([a-zA-Z][a-zA-Z0-9-]*)((?:\s[^>]*)?)>/g;
|
|
21969
|
+
/** Matches complete forbidden element blocks including content */
|
|
21970
|
+
function stripForbiddenBlocks(svg) {
|
|
21971
|
+
for (const tag of FORBIDDEN_ELEMENTS) {
|
|
21972
|
+
const blockRe = new RegExp(`<${tag}[\\s>][\\s\\S]*?</${tag}>`, "gi");
|
|
21973
|
+
svg = svg.replace(blockRe, "");
|
|
21974
|
+
const selfCloseRe = new RegExp(`<${tag}[\\s/][^>]*/?>`, "gi");
|
|
21975
|
+
svg = svg.replace(selfCloseRe, "");
|
|
21976
|
+
}
|
|
21977
|
+
return svg;
|
|
21978
|
+
}
|
|
21979
|
+
/**
|
|
21980
|
+
* Sanitize SVG markup by removing dangerous elements and attributes.
|
|
21981
|
+
* Uses an allowlist approach — only known-safe SVG elements are kept.
|
|
21982
|
+
*/
|
|
21983
|
+
function sanitizeSvg(svg) {
|
|
21984
|
+
if (!svg) return "";
|
|
21985
|
+
let clean = stripForbiddenBlocks(svg);
|
|
21986
|
+
clean = clean.replace(EVENT_HANDLER_RE, "");
|
|
21987
|
+
clean = clean.replace(JS_URL_RE, "$1");
|
|
21988
|
+
clean = clean.replace(TAG_RE, (match, tagName) => {
|
|
21989
|
+
const lower = tagName.toLowerCase();
|
|
21990
|
+
if (ALLOWED_ELEMENTS.has(lower) || ALLOWED_ELEMENTS.has(tagName)) return match;
|
|
21991
|
+
return "";
|
|
21992
|
+
});
|
|
21993
|
+
return clean.trim();
|
|
21994
|
+
}
|
|
21995
|
+
|
|
21996
|
+
//#endregion
|
|
21997
|
+
//#region packages/mcp/src/tools/svg.ts
|
|
21998
|
+
/**
|
|
21999
|
+
* SVG tools — insert SVG diagrams/images into documents via Y.js.
|
|
22000
|
+
*/
|
|
22001
|
+
/**
|
|
22002
|
+
* Find the insertion index after documentHeader and documentMeta elements.
|
|
22003
|
+
*/
|
|
22004
|
+
function findContentStart(fragment) {
|
|
22005
|
+
let idx = 0;
|
|
22006
|
+
for (let i = 0; i < fragment.length; i++) {
|
|
22007
|
+
const child = fragment.get(i);
|
|
22008
|
+
if (child instanceof yjs.XmlElement) {
|
|
22009
|
+
const name = child.nodeName;
|
|
22010
|
+
if (name === "documentHeader" || name === "documentMeta") idx = i + 1;
|
|
22011
|
+
else break;
|
|
22012
|
+
}
|
|
22013
|
+
}
|
|
22014
|
+
return idx;
|
|
22015
|
+
}
|
|
22016
|
+
function registerSvgTools(mcp, server) {
|
|
22017
|
+
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.", {
|
|
22018
|
+
docId: zod.z.string().describe("Document ID to write SVG into."),
|
|
22019
|
+
svg: zod.z.string().describe("Raw SVG markup string (the full <svg>...</svg> element). Will be sanitized."),
|
|
22020
|
+
title: zod.z.string().optional().describe("Optional title/caption displayed above the SVG."),
|
|
22021
|
+
position: zod.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.")
|
|
22022
|
+
}, async ({ docId, svg, title, position }) => {
|
|
22023
|
+
try {
|
|
22024
|
+
server.setAutoStatus("writing", docId);
|
|
22025
|
+
server.setActiveToolCall({
|
|
22026
|
+
name: "write_svg",
|
|
22027
|
+
target: docId
|
|
22028
|
+
});
|
|
22029
|
+
const cleanSvg = sanitizeSvg(svg);
|
|
22030
|
+
if (!cleanSvg) {
|
|
22031
|
+
server.setActiveToolCall(null);
|
|
22032
|
+
return {
|
|
22033
|
+
content: [{
|
|
22034
|
+
type: "text",
|
|
22035
|
+
text: "Error: SVG markup was empty or entirely stripped by sanitizer."
|
|
22036
|
+
}],
|
|
22037
|
+
isError: true
|
|
22038
|
+
};
|
|
22039
|
+
}
|
|
22040
|
+
const doc = (await server.getChildProvider(docId)).document;
|
|
22041
|
+
const fragment = doc.getXmlFragment("default");
|
|
22042
|
+
doc.transact(() => {
|
|
22043
|
+
const el = new yjs.XmlElement("svgEmbed");
|
|
22044
|
+
el.setAttribute("svg", cleanSvg);
|
|
22045
|
+
if (title) el.setAttribute("title", title);
|
|
22046
|
+
const insertPos = position === "prepend" ? findContentStart(fragment) : fragment.length;
|
|
22047
|
+
fragment.insert(insertPos, [el]);
|
|
22048
|
+
});
|
|
22049
|
+
server.setFocusedDoc(docId);
|
|
22050
|
+
server.setActiveToolCall(null);
|
|
22051
|
+
return { content: [{
|
|
22052
|
+
type: "text",
|
|
22053
|
+
text: `SVG inserted into document ${docId}${title ? ` ("${title}")` : ""}`
|
|
22054
|
+
}] };
|
|
22055
|
+
} catch (error) {
|
|
22056
|
+
server.setActiveToolCall(null);
|
|
22057
|
+
return {
|
|
22058
|
+
content: [{
|
|
22059
|
+
type: "text",
|
|
22060
|
+
text: `Error writing SVG: ${error.message}`
|
|
22061
|
+
}],
|
|
22062
|
+
isError: true
|
|
22063
|
+
};
|
|
22064
|
+
}
|
|
22065
|
+
});
|
|
22066
|
+
}
|
|
22067
|
+
|
|
21873
22068
|
//#endregion
|
|
21874
22069
|
//#region packages/mcp/src/resources/agent-guide.ts
|
|
21875
22070
|
const AGENT_GUIDE = `# Abracadabra AI Agent Guide
|
|
@@ -22662,6 +22857,7 @@ Read the resource at abracadabra://agent-guide for the complete guide covering p
|
|
|
22662
22857
|
registerFileTools(mcp, server);
|
|
22663
22858
|
registerAwarenessTools(mcp, server);
|
|
22664
22859
|
registerChannelTools(mcp, server);
|
|
22860
|
+
registerSvgTools(mcp, server);
|
|
22665
22861
|
registerAgentGuide(mcp);
|
|
22666
22862
|
registerTreeResource(mcp, server);
|
|
22667
22863
|
registerServerInfoResource(mcp, server);
|