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"') + }) +})