@abraca/mcp 1.0.21 → 1.0.25
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 +197 -1
- package/dist/abracadabra-mcp.cjs.map +1 -1
- package/dist/abracadabra-mcp.esm.js +197 -1
- package/dist/abracadabra-mcp.esm.js.map +1 -1
- package/package.json +1 -1
- package/src/converters/markdownToYjs.ts +14 -1
- package/src/converters/yjsToMarkdown.ts +7 -0
- package/src/index.ts +2 -0
- package/src/tools/svg.ts +83 -0
- package/src/utils/sanitizeSvg.ts +71 -0
|
@@ -20526,7 +20526,14 @@ function parseBlocks(markdown) {
|
|
|
20526
20526
|
i++;
|
|
20527
20527
|
}
|
|
20528
20528
|
i++;
|
|
20529
|
-
|
|
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") || "";
|
|
@@ -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);
|