fix(mcp-server): count a one-page view only for that page, and more review fixes

Found by the second PR review:
- get_diagram with a page selector, or a rejected edit's error, counted
  the whole document as seen, so an edit on another page could overwrite
  the user's change there. A one-page view now counts for all pages only
  if the others are unchanged; otherwise the reply says to get them.
- add_page accepted shapes with the root cell ids "0" and "1" and renamed
  them, breaking their edges. The check also missed ids on UserObject
  wrappers and ids written with spaces around the "=".
- Root cells written over two lines were kept as an extra layer, cells
  with id = "a" did not count as cells, and CDATA text before a page's
  model passed the check although draw.io cannot open the page.
- Auto-save cleanup deleted the user's own files that start with mcp-.
  Only names in the session id format are removed now.
- Restoring a history entry dropped edits made in the browser since the
  last entry. They are added to history first.
- A session whose state expired showed a blank page, and the next change
  overwrote its auto-save file. The saved file is loaded instead.
- An edit on a page export's one-page projection, made before the real
  document was back, replaced the whole document.
- A late sync reply could overwrite a newer edit: each sync export is
  numbered, and the server ignores replies older than the current state.
- screenshot_diagram could return another session's image after
  start_session ran during its retries.
This commit is contained in:
dayuan.jiang
2026-10-05 10:52:20 +09:00
parent 2e84e02666
commit 080f44716f
13 changed files with 334 additions and 59 deletions
+26 -4
View File
@@ -40,7 +40,7 @@ import {
updateLastHistorySvg,
} from "./history.ts"
import { log } from "./logger.ts"
import { BLANK_MXFILE } from "./pages.ts"
import { BLANK_MXFILE, hasCells } from "./pages.ts"
// Configurable draw.io embed URL for private deployments
const DRAWIO_BASE_URL =
@@ -87,10 +87,12 @@ function ensureSessionStateInitialized(sessionId: string): void {
if (!isValidSessionId(sessionId)) return
if (stateStore.has(sessionId)) return
// The session's saved diagram, so a blank page never replaces that file.
// Not a change worth saving: the browser fills it on its next push
// A blank diagram keeps the draw.io spinner (spin=1) from waiting
// forever when no load(xml) is ever sent
setState(sessionId, BLANK_MXFILE, undefined, false, false)
const saved = savedStateLoader?.(sessionId)
setState(sessionId, saved || BLANK_MXFILE, undefined, false, false)
}
interface SessionState {
@@ -142,6 +144,16 @@ export function onStateChange(
stateListener = listener
}
// Reads a session's saved diagram when its state is created again (it
// expired, or the MCP process restarted)
let savedStateLoader: ((sessionId: string) => string | null) | null = null
export function onSessionRecreate(
loader: (sessionId: string) => string | null,
): void {
savedStateLoader = loader
}
export function setState(
sessionId: string,
xml: string,
@@ -461,11 +473,15 @@ function handleStateApi(
// The browser edited a version older than the latest AI write
// (it has not loaded that write yet). Keep the AI write; the
// browser loads it on its next poll.
// browser loads it on its next poll. A sync reply is also
// stale after a newer write of the browser's own (a user
// edit saved while the export ran).
const current = stateStore.get(sessionId)
if (
typeof data.baseVersion === "number" &&
data.baseVersion < (current?.serverVersion ?? 0)
(data.baseVersion < (current?.serverVersion ?? 0) ||
(data.source === "sync" &&
data.baseVersion < (current?.version ?? 0)))
) {
let savedToHistory = false
if (data.source === "sync") {
@@ -566,6 +582,12 @@ function handleRestoreApi(
return
}
// Edits in the browser since the last entry are not in history
// yet: keep them, so the restore can be undone
const current = stateStore.get(sessionId)
if (current && hasCells(current.xml)) {
addHistory(sessionId, current.xml, current.svg)
}
const newVersion = setState(sessionId, entry.xml)
addHistory(sessionId, entry.xml, entry.svg)