From a2ca229830140cf32bca1135f91b3434438db4b6 Mon Sep 17 00:00:00 2001 From: "dayuan.jiang" Date: Sun, 11 Oct 2026 13:44:34 +0900 Subject: [PATCH] feat(mcp-server): the shell syncs with the server like the classic page shell/mcp-sync-core.ts ports preview.js's protocol without the page or React: polling GET /api/state every 2 s with the state id and version, pushes of the user's edits with their base version and state, 409 and 413 handling, recovery of a recreated session (the tab's copy goes to History), the token refresh after a 403, sync and export requests (PNG by page id, SVG of another page through a transient one-page projection with autosave ignored, the 600 ms wait for icons), thumbnails for History, and History reads and restores that name the state the list belongs to. A server write goes on the canvas as one undo step (loadDiagram commit, with the change marked) when decideLoad finds it changes only the page on screen, keeps the file variables and needs no load-only setting; the other pages are compared with the MCP core's contentFingerprint. draw.io's own re-serialized copy of such a write is recognized with isSameDocument and not pushed back as an edit. shell/use-mcp-sync.ts mounts the sync once inside DiagramProvider, reads draw.io's autosave and load messages, and reports the connection state, which the shell's status bar shows; notices come as toasts from the dictionaries. --- lib/i18n/dictionaries/en.json | 10 +- lib/i18n/dictionaries/ja.json | 10 +- lib/i18n/dictionaries/zh-Hant.json | 10 +- lib/i18n/dictionaries/zh.json | 10 +- packages/mcp-server/README.md | 2 +- packages/mcp-server/shell/canvas-shell.tsx | 7 +- packages/mcp-server/shell/mcp-sync-core.ts | 744 +++++++++++++++++++++ packages/mcp-server/shell/use-mcp-sync.ts | 118 ++++ tests/unit/mcp-shell-config.test.ts | 8 + 9 files changed, 913 insertions(+), 6 deletions(-) create mode 100644 packages/mcp-server/shell/mcp-sync-core.ts create mode 100644 packages/mcp-server/shell/use-mcp-sync.ts diff --git a/lib/i18n/dictionaries/en.json b/lib/i18n/dictionaries/en.json index c37fa777..322b142c 100644 --- a/lib/i18n/dictionaries/en.json +++ b/lib/i18n/dictionaries/en.json @@ -702,6 +702,14 @@ }, "shell": { "waiting": "Waiting for the MCP server…", - "toggleTheme": "Switch between light and dark" + "toggleTheme": "Switch between light and dark", + "connected": "Connected to the MCP server", + "offline": "Can't reach the MCP server", + "tooLarge": "This diagram is too large to save to the MCP server (over 10 MB).", + "unreachable": "Can't reach the MCP server. Your changes are only in this tab for now.", + "restoredFromFile": "The diagram was restored from its saved file. What this tab showed before is in History.", + "aiChanged": "The AI changed the diagram while you were editing. Your last change was saved in History.", + "historyChanged": "History changed on the server. Please open it again.", + "restoreFailed": "Restore failed. Please try again." } } diff --git a/lib/i18n/dictionaries/ja.json b/lib/i18n/dictionaries/ja.json index c5191529..08303f6b 100644 --- a/lib/i18n/dictionaries/ja.json +++ b/lib/i18n/dictionaries/ja.json @@ -702,6 +702,14 @@ }, "shell": { "waiting": "MCP サーバーを待っています…", - "toggleTheme": "ライトとダークを切り替え" + "toggleTheme": "ライトとダークを切り替え", + "connected": "MCP サーバーに接続しました", + "offline": "MCP サーバーに接続できません", + "tooLarge": "この図は大きすぎて MCP サーバーに保存できません(10 MB 超)。", + "unreachable": "MCP サーバーに接続できません。変更は今のところこのタブにだけあります。", + "restoredFromFile": "図は保存済みファイルから復元されました。このタブに表示されていた内容は履歴にあります。", + "aiChanged": "編集中に AI が図を変更しました。最後の変更は履歴に保存されました。", + "historyChanged": "サーバー上の履歴が変わりました。もう一度開いてください。", + "restoreFailed": "復元に失敗しました。もう一度お試しください。" } } diff --git a/lib/i18n/dictionaries/zh-Hant.json b/lib/i18n/dictionaries/zh-Hant.json index 03239fb2..222dab38 100644 --- a/lib/i18n/dictionaries/zh-Hant.json +++ b/lib/i18n/dictionaries/zh-Hant.json @@ -702,6 +702,14 @@ }, "shell": { "waiting": "正在等待 MCP 伺服器…", - "toggleTheme": "切換淺色或深色" + "toggleTheme": "切換淺色或深色", + "connected": "已連接 MCP 伺服器", + "offline": "無法連接 MCP 伺服器", + "tooLarge": "這張圖太大,無法儲存到 MCP 伺服器(超過 10 MB)。", + "unreachable": "無法連接 MCP 伺服器。您的修改暫時只保存在這個分頁裡。", + "restoredFromFile": "圖表已從儲存的檔案還原。這個分頁之前顯示的內容已放入歷史記錄。", + "aiChanged": "您編輯時 AI 修改了圖表。您的最後一次修改已儲存到歷史記錄。", + "historyChanged": "伺服器上的歷史記錄已變化,請重新開啟。", + "restoreFailed": "還原失敗,請再試一次。" } } diff --git a/lib/i18n/dictionaries/zh.json b/lib/i18n/dictionaries/zh.json index 3865c7ef..0a958437 100644 --- a/lib/i18n/dictionaries/zh.json +++ b/lib/i18n/dictionaries/zh.json @@ -702,6 +702,14 @@ }, "shell": { "waiting": "正在等待 MCP 服务器…", - "toggleTheme": "切换浅色或深色" + "toggleTheme": "切换浅色或深色", + "connected": "已连接 MCP 服务器", + "offline": "无法连接 MCP 服务器", + "tooLarge": "这张图太大,无法保存到 MCP 服务器(超过 10 MB)。", + "unreachable": "无法连接 MCP 服务器。您的修改暂时只保存在这个标签页里。", + "restoredFromFile": "图表已从保存的文件恢复。这个标签页之前显示的内容已放入历史记录。", + "aiChanged": "您编辑时 AI 修改了图表。您的最后一次修改已保存到历史记录。", + "historyChanged": "服务器上的历史记录已变化,请重新打开。", + "restoreFailed": "恢复失败,请重试。" } } diff --git a/packages/mcp-server/README.md b/packages/mcp-server/README.md index 6db288a0..3f2bafa4 100644 --- a/packages/mcp-server/README.md +++ b/packages/mcp-server/README.md @@ -188,7 +188,7 @@ To give the AI your own drawing rules, write them in `~/.next-ai-drawio/instruct | `DRAWIO_LANG` | unset | Language of the draw.io editor. Unset, draw.io chooses: the browser language on `embed.diagrams.net`, English on a self-hosted draw.io until the user picks one under **Extras > Language**. A code such as `en`, `zh`, `zh-tw`, `ja` or `de` fixes it and hides that submenu. | | `DRAWIO_UI` | unset | draw.io theme. Unset, the user picks one under **Extras > Theme** and draw.io remembers it. `kennedy`, `atlas`, `dark`, `min`, `sketch` or `simple` fixes the theme and hides that menu. | | `DRAWIO_DARK` | `auto` | Dark mode of the draw.io editor: `auto` follows the system, `1` forces dark, `0` forces light. The page header keeps following the system. | -| `DRAWIO_PREVIEW_UI` | `classic` | The preview page `start_session` opens. `shell` opens the new canvas page built from the web app's canvas (in progress: it does not sync with the server yet). | +| `DRAWIO_PREVIEW_UI` | `classic` | The preview page `start_session` opens. `shell` opens the new canvas page built from the web app's canvas: it syncs with the server like the classic page, and an AI change of the page on screen is marked and undone with one Ctrl+Z (in progress: no History panel or download yet). | | `DRAWIO_AUTO_SCREENSHOT` | unset | Set to `true` to attach a screenshot to every `create_new_diagram` and `edit_diagram` result, so the AI checks each drawing. Costs 2 to 10 s per call; the preview tab must be open and in front. A call can still pass `screenshot: false`. | | `DEBUG` | unset | Set to `true` to log debug messages to stderr. | diff --git a/packages/mcp-server/shell/canvas-shell.tsx b/packages/mcp-server/shell/canvas-shell.tsx index cd31ffc2..881e3e00 100644 --- a/packages/mcp-server/shell/canvas-shell.tsx +++ b/packages/mcp-server/shell/canvas-shell.tsx @@ -7,11 +7,13 @@ import { VersionsProvider, type VersionsSource, } from "@/components/canvas/versions-context" +import { AppToaster } from "@/components/workspace/app-toaster" import { useDictionary } from "@/hooks/use-dictionary" import type { DrawioSource } from "@/lib/drawio/drawio-config" import { useSettingsStore } from "@/stores/settings-store" import type { ShellConfig } from "./runtime-config" import { setDark } from "./theme" +import { useMcpSync } from "./use-mcp-sync" // No versions until the shell reads the server's History (a later step) const NO_VERSIONS: VersionsSource = { @@ -34,6 +36,7 @@ const NO_VERSIONS: VersionsSource = { export function CanvasShell({ config }: { config: ShellConfig }) { const dict = useDictionary() const isDark = useSettingsStore((s) => s.isDark) + const status = useMcpSync(config) const drawioSource = useMemo( () => ({ editorUrl: config.drawioBaseUrl, @@ -62,8 +65,9 @@ export function CanvasShell({ config }: { config: ShellConfig }) { - {dict.shell.waiting} + {dict.shell[status]}