From 2418c4776438b79820e86dc6745eaf6b2767cb86 Mon Sep 17 00:00:00 2001 From: Dayuan Jiang <34411969+DayuanJiang@users.noreply.github.com> Date: Fri, 9 Oct 2026 21:06:49 +0900 Subject: [PATCH] fix(edit): refuse cut-off cell XML in edit_diagram operations (#960) While an edit streams, the preview applies the operations received so far. Chrome's DOMParser keeps a cell whose XML is cut off, so a vertex without its geometry reached the canvas. draw.io placed it at the origin, shifted the page layout and compensated the scroll position, and the browser's clamping of that compensation left the diagram out of view once the edit finished. Check each new_xml with getXmlSyntaxError, as the whole document already is, so an incomplete operation is reported instead of applied. --- packages/mcp-server/src/diagram-operations.ts | 28 +++++++++++ .../tests/diagram-operations.test.ts | 49 +++++++++++++++++++ 2 files changed, 77 insertions(+) diff --git a/packages/mcp-server/src/diagram-operations.ts b/packages/mcp-server/src/diagram-operations.ts index 5315f191..64dd810a 100644 --- a/packages/mcp-server/src/diagram-operations.ts +++ b/packages/mcp-server/src/diagram-operations.ts @@ -216,6 +216,20 @@ export function applyDiagramOperations( continue } + // A cut-off cell (its XML still streaming in a preview) would + // parse in Chrome to a cell without its geometry + const syntaxError = getXmlSyntaxError( + `${op.new_xml}`, + ) + if (syntaxError) { + errors.push({ + type: "update", + cellId: op.cell_id, + message: `new_xml is not well-formed XML: ${syntaxError}`, + }) + continue + } + // Parse the new XML const newDoc = parser.parseFromString( `${op.new_xml}`, @@ -268,6 +282,20 @@ export function applyDiagramOperations( continue } + // A cut-off cell (its XML still streaming in a preview) would + // parse in Chrome to a cell without its geometry + const syntaxError = getXmlSyntaxError( + `${op.new_xml}`, + ) + if (syntaxError) { + errors.push({ + type: "add", + cellId: op.cell_id, + message: `new_xml is not well-formed XML: ${syntaxError}`, + }) + continue + } + // Parse the new XML const newDoc = parser.parseFromString( `${op.new_xml}`, diff --git a/packages/mcp-server/tests/diagram-operations.test.ts b/packages/mcp-server/tests/diagram-operations.test.ts index 77d4de13..97af0dcd 100644 --- a/packages/mcp-server/tests/diagram-operations.test.ts +++ b/packages/mcp-server/tests/diagram-operations.test.ts @@ -149,3 +149,52 @@ describe("a wrapped mxCell with its wrapper's id", () => { expect(result).toContain('label="B"') }) }) + +describe("cut-off new_xml", () => { + // While edit_diagram streams, the last operation's XML is often + // incomplete. Chrome's DOMParser keeps the partial cell, so it must be + // refused before it reaches the diagram. + const CUT = `` + + it("refuses an update whose XML is cut off", () => { + const { result, errors } = applyDiagramOperations(DOC, [ + { operation: "update", cell_id: "b", new_xml: CUT }, + ]) + expect(errors).toHaveLength(1) + expect(errors[0]).toMatchObject({ type: "update", cellId: "b" }) + expect(errors[0].message).toContain("not well-formed") + expect(result).toContain('value="B"') + expect(result).not.toContain("B2") + }) + + it("refuses an add whose XML is cut off", () => { + const { result, errors } = applyDiagramOperations(DOC, [ + { + operation: "add", + cell_id: "n", + new_xml: ` { + const { result, errors } = applyDiagramOperations(DOC, [ + { + operation: "update", + cell_id: "b", + new_xml: ``, + }, + { + operation: "add", + cell_id: "n", + new_xml: ``, + }, + ]) + expect(errors).toHaveLength(1) + expect(result).toContain('value="B3"') + expect(result).not.toContain('id="n"') + }) +})