@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.
@@ -20530,7 +20530,14 @@ function parseBlocks(markdown) {
20530
20530
  i++;
20531
20531
  }
20532
20532
  i++;
20533
- blocks.push({
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") || "";
@@ -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);