fix(mcp-server): keep both pages when get_diagram meets a page export, and more review fixes

Found by the PR review, each with a test that failed first:
- get_diagram during a page export returned the one-page projection on
  screen as the whole document (6 of 6 times when timed so). The preview
  page no longer answers a sync while a projection shows, and syncs after
  reloading, so the poll that restores the real document exports it.
- Exports are numbered on the server too: a late result of an export that
  timed out was saved as the next export's file.
- In Chrome, a new_xml with a syntax error counted the <parsererror>
  element as a second cell, so the web app rejected edits that auto-fix
  repairs ("must contain exactly one cell").
- hasCells missed single-quoted ids, so screenshot_diagram called such a
  diagram empty and auto-save never created its file.
- A literal \n directly under a <diagram> that has a model passed
  validation; only text-only pages are compressed data.
- A wrapped mxCell repeating its UserObject's id took the wrapper's place
  in edits, so delete and update left an empty or nested wrapper.
- Bare cells with a shape or edge id of "0" or "1" are rejected with a
  clear message instead of being renamed, which broke their edges.
- DRAWIO_DATA_DIR expands ~, which JSON configs pass on as it is.
This commit is contained in:
dayuan.jiang
2026-10-04 23:04:21 +09:00
parent 2bb69b58af
commit 504d2fa812
15 changed files with 189 additions and 26 deletions
+14 -1
View File
@@ -106,6 +106,7 @@ interface SessionState {
exportFormat?: ExportFormat // Set by MCP tool to request browser export
exportXml?: string // Single-page projection to load before a page-targeted export
exportOptions?: ExportOptions // Extra draw.io export parameters (PNG only)
exportId?: number // Number of the pending export, echoed with its result
exportData?: string // Base64/SVG data returned by browser after export
}
@@ -161,6 +162,7 @@ export function setState(
exportFormat: existing?.exportFormat, // Preserve pending export request
exportXml: existing?.exportXml, // Preserve pending projection
exportOptions: existing?.exportOptions,
exportId: existing?.exportId,
exportData: existing?.exportData, // Preserve export result
})
log.debug(`State updated: session=${sessionId}, version=${newVersion}`)
@@ -191,9 +193,14 @@ export function requestExport(
state.exportXml = projectionXml
state.exportOptions = options
state.exportFormat = format
// The browser sends this back with the result, so a late result of an
// export that timed out is not taken for this one
state.exportId = ++lastExportId
return true
}
let lastExportId = 0
export function requestSync(sessionId: string): boolean {
const state = stateStore.get(sessionId)
if (state) {
@@ -414,6 +421,7 @@ function handleStateApi(
exportFormat: state?.exportFormat || null,
exportXml: state?.exportXml || null,
exportOptions: state?.exportOptions || null,
exportId: state?.exportId ?? null,
}),
)
} else if (req.method === "POST") {
@@ -432,14 +440,19 @@ function handleStateApi(
// Browser is returning export data (png/svg)
if (data.exportData !== undefined) {
const state = stateStore.get(sessionId)
if (state) {
if (state && data.exportId === state.exportId) {
state.exportData = data.exportData
state.exportFormat = undefined
state.exportXml = undefined
state.exportOptions = undefined
state.exportId = undefined
log.debug(
`Export data received for session=${sessionId}`,
)
} else if (state) {
log.debug(
`Ignored a late export result for session=${sessionId}`,
)
}
res.writeHead(200, { "Content-Type": "application/json" })
res.end(JSON.stringify({ success: true }))