fix(mcp-server): reject text between tags, which draw.io cannot open

draw.io reads any text inside a page as compressed page data, so a stray
text node makes the whole page fail with an atob error. gpt-5-mini sends
new cells with a literal "\n" between the tags; the edit card said
Complete while draw.io showed the error and kept the old diagram.

Validation now reports text between tags, and auto-fix turns a literal
\n, \t or \r between tags into whitespace. Other text goes back to the
model as an error. The compressed data directly under <diagram> is fine.
This commit is contained in:
dayuan.jiang
2026-10-04 20:15:44 +09:00
parent 6236124338
commit 6f5f7b668b
3 changed files with 78 additions and 1 deletions
+33
View File
@@ -404,6 +404,21 @@ function findOrphanMxPoints(
* <mxPoint>s. Used for XML the model wrote, not for files or browser state.
* @returns null if valid, error message string if invalid
*/
/** The first non-blank text under el, skipping a page's compressed data */
function findTextBetweenTags(el: Element | null): string | null {
if (!el) return null
for (const node of Array.from(el.childNodes)) {
if (node.nodeType === 1) {
const text = findTextBetweenTags(node as Element)
if (text) return text
} else if (node.nodeType === 3 && el.tagName !== "diagram") {
const text = node.textContent?.trim()
if (text) return text.slice(0, 40)
}
}
return null
}
export function validateMxCellStructure(
xml: string,
opts: { strict?: boolean } = {},
@@ -429,6 +444,15 @@ export function validateMxCellStructure(
return `Invalid XML: Found nested mxCell (id="${id}"). Cells should be siblings, not nested inside other mxCell elements.`
}
}
// draw.io reads any text inside a page as compressed page data and
// then fails to open the page
if (!doc.querySelector("parsererror")) {
const text = findTextBetweenTags(doc.documentElement)
if (text) {
return `Invalid XML: Found text "${text}" between tags. Labels belong in the value attribute; remove any other text between tags.`
}
}
} catch (error) {
console.warn(
"[validateMxCellStructure] DOMParser threw unexpected error, falling back to regex validation:",
@@ -542,6 +566,15 @@ export function autoFixXml(xml: string): { fixed: string; fixes: string[] } {
fixes.push("Fixed JSON-escaped XML")
}
// 0b. Literal \n, \t or \r between tags, from escaping the XML twice
const unescaped = fixed.replace(/>(?:\s|\\[nrt])+</g, (gap) =>
gap.replace(/\\n/g, "\n").replace(/\\t/g, "\t").replace(/\\r/g, ""),
)
if (unescaped !== fixed) {
fixed = unescaped
fixes.push("Replaced literal \\n between tags with line breaks")
}
// 1. Remove CDATA wrapper
if (/^\s*<!\[CDATA\[/.test(fixed)) {
fixed = fixed.replace(/^\s*<!\[CDATA\[/, "").replace(/\]\]>\s*$/, "")