feat(mcp-server): get_selection reads the cells the user selected in the shell

The server asks the preview tab for the selection the way it asks for an export (a random request id in GET /api/state, the answer in a POST with that id, 10 s to answer). The shell answers through the editor bridge with each cell's id, label, an edge's ends, a shape's geometry and the page on screen; without a same-origin editor it says so, and the tool names the external draw.io. The classic page cannot answer, so the tool says that at once. screenshot_diagram's description now says a page selector renders that page without changing the page on screen (PNG exports by pageId already did).
This commit is contained in:
dayuan.jiang
2026-10-11 20:56:07 +09:00
parent 5c923adf7b
commit b3dcd91f54
7 changed files with 367 additions and 6 deletions
+26 -3
View File
@@ -21,6 +21,7 @@ import {
parseMxfile,
serializeMxfile,
} from "@/packages/mcp-server/src/pages.ts"
import type { SelectionAnswer } from "@/packages/mcp-server/src/selection.ts"
export type ExportFormat = "png" | "svg" | "xmlsvg"
@@ -35,6 +36,8 @@ export interface ServerState {
exportXml: string | null
exportOptions: { width?: number; pageId?: string } | null
exportId: string | null
/** A get_selection request waiting for this tab's answer */
selectionId?: string | null
}
/** An entry of GET /api/history */
@@ -93,6 +96,9 @@ export interface SyncCanvas {
currentXml(): string
/** The page on screen; null when unknown (external draw.io) */
currentPageId(): string | null
/** The cells selected in the editor, for get_selection; `unavailable`
* when the page cannot reach the editor (external draw.io) */
readSelection(): SelectionAnswer
}
export type SyncStatus = "waiting" | "connected" | "offline"
@@ -305,6 +311,9 @@ export function createMcpSync(options: SyncOptions): McpSync {
let forceReload = false
let pendingSyncExport = false
let syncExportSeq = 0
// The selection request answered last: the server shows a request until
// the answer arrives, and polls overlap
let answeredSelectionId: string | null = null
let status: SyncStatus = "waiting"
let interval: ReturnType<typeof setInterval> | null = null
@@ -722,9 +731,23 @@ export function createMcpSync(options: SyncOptions): McpSync {
) {
startMcpExport(s, justLoaded)
}
// Extension point (plan step 6, the get_selection tool): a
// selection request in the state would be answered here, like
// the sync request above, with the ids the editor bridge reads
// A selection request (get_selection): the editor's selection
// on the page the user is viewing, once draw.io is up (the
// bridge attaches to it at its first load), and not while a
// projection replaces that page
if (
s.selectionId &&
s.selectionId !== answeredSelectionId &&
isReady &&
!projectionExportActive
) {
answeredSelectionId = s.selectionId
postJson("/state", {
sessionId,
selectionId: s.selectionId,
selection: canvas.readSelection(),
}).catch(() => {})
}
} catch {
setStatus("offline")
}
+13 -1
View File
@@ -3,7 +3,10 @@ import { toast } from "sonner"
import { useDiagram } from "@/contexts/diagram-context"
import { useDictionary } from "@/hooks/use-dictionary"
import { diffDiagrams } from "@/lib/diagram-diff"
import { highlightChangedCells } from "@/lib/drawio/editor-bridge"
import {
highlightChangedCells,
readSelectionDetails,
} from "@/lib/drawio/editor-bridge"
import { useCanvasStore } from "@/stores/canvas-store"
import { createMcpSync, type McpSync, type SyncStatus } from "./mcp-sync-core"
import type { ShellConfig } from "./runtime-config"
@@ -53,6 +56,15 @@ export function useMcpSync(config: ShellConfig): {
diagramRef.current.requestExport(request, timeoutMs),
currentXml: () => diagramRef.current.chartXMLRef.current,
currentPageId: () => useCanvasStore.getState().currentPageId,
// The bridge reads the editor directly; without it (an
// external draw.io) the selection cannot be read
readSelection: () =>
readSelectionDetails() ?? {
pageId: null,
pageName: null,
cells: [],
unavailable: true,
},
},
onNotice: (notice) =>
toast(dictRef.current.shell[notice], { duration: 8000 }),