-
-
Notifications
You must be signed in to change notification settings - Fork 2k
feat: add SVG and PNG export support for export_diagram tool #566
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -65,7 +65,7 @@ const server = new McpServer({ | |||||||||||||||||||||||||||||||||||||||||||||||||||||
| version: "0.1.2", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Register prompt with workflow guidance | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Register prompt with workflow guidelines | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| server.prompt( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "diagram-workflow", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "Guidelines for creating and editing draw.io diagrams", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -89,7 +89,7 @@ server.prompt( | |||||||||||||||||||||||||||||||||||||||||||||||||||||
| ## Modifying or Deleting Existing Elements | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 1. FIRST call get_diagram to see current cell IDs and structure | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 2. THEN call edit_diagram with "update" or "delete" operations | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 3. For update, provide the cell_id and complete new mxCell XML | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 3. For update, provide the cell_id and complete new_xml | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ## Important Notes | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - create_new_diagram REPLACES the entire diagram - only use for new diagrams | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -541,16 +541,75 @@ server.registerTool( | |||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Decodes a base64 data URL to a Buffer | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Supports: data:image/svg+xml;base64, data:image/png;base64, etc. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function decodeDataUrl(dataUrl: string): Buffer | null { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!dataUrl || !dataUrl.startsWith("data:")) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return null | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const match = dataUrl.match(/^data:([^;]+);base64,(.+)$/) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!match) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return null | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const mimeType = match[1] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const base64Data = match[2] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return Buffer.from(base64Data, "base64") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return null | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Detects export format from file extension | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type ExportFormat = "drawio" | "svg" | "png" | "drawio-svg" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function detectExportFormat(path: string): ExportFormat { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const lowerPath = path.toLowerCase() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (lowerPath.endsWith(".svg")) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return "svg" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (lowerPath.endsWith(".png")) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return "png" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (lowerPath.endsWith(".drawio.svg")) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return "drawio-svg" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (lowerPath.endsWith(".drawio.png")) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return "drawio-svg" // PNG with embedded XML | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return "drawio" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Tool: export_diagram | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| server.registerTool( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "export_diagram", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| description: "Export the current diagram to a .drawio file.", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| description: `Export the current diagram to a file. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Supported formats: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - .drawio - XML format (default, editable in draw.io) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - .svg - Standalone SVG image | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - .png - PNG image | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - .drawio.svg - SVG with embedded draw.io XML (editable + viewable) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - .drawio.png - PNG with embedded draw.io XML | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Examples: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - export_diagram({ path: "./diagram.drawio" }) - XML format | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - export_diagram({ path: "./diagram.svg" }) - SVG image | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - export_diagram({ path: "./diagram.png" }) - PNG image | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - export_diagram({ path: "./diagram.drawio.svg" }) - Hybrid format`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| inputSchema: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| path: z | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .string() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .describe( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "File path to save the diagram (e.g., ./diagram.drawio)", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "File path to save the diagram. Format is detected from extension (.drawio, .svg, .png, .drawio.svg, .drawio.png)", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -590,22 +649,145 @@ server.registerTool( | |||||||||||||||||||||||||||||||||||||||||||||||||||||
| const nodePath = await import("node:path") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let filePath = path | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!filePath.endsWith(".drawio")) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| filePath = `${filePath}.drawio` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const format = detectExportFormat(filePath) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Add extension if not present | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const ext = nodePath.extname(filePath) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!ext) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| switch (format) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| case "svg": | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| case "drawio-svg": | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| filePath = `${filePath}.svg` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| break | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| case "png": | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| filePath = `${filePath}.png` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| break | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| default: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| filePath = `${filePath}.drawio` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+652
to
669
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const format = detectExportFormat(filePath) | |
| // Add extension if not present | |
| const ext = nodePath.extname(filePath) | |
| if (!ext) { | |
| switch (format) { | |
| case "svg": | |
| case "drawio-svg": | |
| filePath = `${filePath}.svg` | |
| break | |
| case "png": | |
| filePath = `${filePath}.png` | |
| break | |
| default: | |
| filePath = `${filePath}.drawio` | |
| } | |
| } | |
| // Ensure the file has an extension before detecting the export format | |
| const ext = nodePath.extname(filePath) | |
| if (!ext) { | |
| // Default to .drawio when no extension is provided | |
| filePath = `${filePath}.drawio` | |
| } | |
| const format = detectExportFormat(filePath) |
Copilot
AI
Jan 10, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a user requests .png export, the code saves SVG data to a .png file extension. This creates a file with incorrect extension that won't open properly in image viewers. Either convert the SVG to actual PNG format, or change the file extension to .svg before saving and inform the user of the extension change.
Copilot
AI
Jan 10, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using indexOf('>') to find the SVG tag insertion point is fragile. It will incorrectly match the first > character, which could be in an XML declaration (<?xml version='1.0'?>) or attribute value. Use a more robust approach like matching /<svg[^>]*>/ to find the actual opening SVG tag.
| const svgInsertIndex = svgContent.indexOf(">") + 1 | |
| const svgTagMatch = svgContent.match(/<svg[^>]*>/i) | |
| let svgInsertIndex: number | |
| if (svgTagMatch && typeof svgTagMatch.index === "number") { | |
| svgInsertIndex = svgTagMatch.index + svgTagMatch[0].length | |
| } else { | |
| // Fallback to previous behavior if <svg> tag is not found | |
| svgInsertIndex = svgContent.indexOf(">") + 1 | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
mimeTypevariable is extracted but never used. Consider removing it or adding validation to ensure only expected MIME types (image/svg+xml, image/png) are processed.