Files
next-ai-draw-io/packages/mcp-server/src/index.ts

1592 lines
61 KiB
TypeScript
Raw Normal View History

#!/usr/bin/env node
/**
* MCP Server for Next AI Draw.io
*
* Enables AI agents (Claude Desktop, Cursor, etc.) to generate and edit
* draw.io diagrams with real-time browser preview.
*
* Uses an embedded HTTP server - no external dependencies required.
feat(mcp): add multi-page (mxfile) support to MCP server (#862) * feat(mcp): add multi-page (mxfile) support The MCP server's write path could only address a single drawio page even though the underlying .drawio file format and the embedded editor both natively support multi-page documents. A user asking for "a second page with a CNN diagram" would hit the validator with the error "Expected closing tag </root> but found </mxCell>" because the validator assumed input was a bare <mxGraphModel> and could not walk past the <mxfile><diagram>...</diagram></mxfile> wrapper. This patch closes the gap end to end: * New helper module `pages.ts` centralises page CRUD (normalize, parse, list, find, add, rename, delete) so every layer agrees that the canonical in-memory shape is always <mxfile>. normalizeToMxfile and addPageToDoc both strip any leading <?xml ?> declaration before embedding a fragment inside <diagram> (the declaration is only valid at document start). addPageToDoc explicitly rejects full <mxfile> inputs so a caller cannot accidentally nest a document inside a page. * `xml-validation.ts` now detects an <mxfile> root and scopes the duplicate-id check per <diagram>. The legacy regex check would otherwise reject every multi-page doc, because cells "0" and "1" repeat in each page's <root> by design. The DOM-parse path is gated by a cheap regex pre-check so legacy bare <mxGraphModel> callers don't pay any extra cost. The autoFix duplicate-id rename step is also guarded against mxfile inputs — renaming those sentinel cells would silently break drawio's parent references. * `diagram-operations.ts` accepts an optional PageSelector. For <mxfile> input it resolves the page first and scopes all querySelectorAll calls to that page's <root>, so a delete on page 2's cell "2" no longer touches page 1's cell "2". * `create_new_diagram` accepts either a bare <mxGraphModel> (legacy, auto-wrapped into a single-page mxfile) or a full <mxfile> with N diagrams. All existing single-page callers keep working unchanged. * `edit_diagram`, `get_diagram`, and `export_diagram` gain optional `page_id` / `page_name` / `page_index` parameters. When omitted they target the first page — the "active by convention" default. Tool handlers with all-optional input schemas coalesce missing arguments via `input ?? {}` so a no-args MCP invocation can't crash on destructure before reaching the session-existence check. * New tools: `list_pages`, `add_page`, `rename_page`, `delete_page`. * Page-targeted PNG/SVG export uses a "load + export + restore" dance: the server projects the target page into a single-page <mxfile>, pushes it into the transient state so the browser reloads the iframe with just that page, waits for drawio to render (~3s), triggers the export, captures the data, and then restores the original multi-page document. The dance is wrapped in `try/finally` so the restore runs unconditionally — even if an exception is thrown mid-dance, the user's multi-tab view is recovered before the function returns. The earlier attempt to use drawio's `selectPage` postMessage was a no-op because drawio's JSON embed protocol does not expose that action — silently exporting whatever tab happened to be active. The load-export-restore approach trades a brief visible tab-flicker for correctness: the exported image is guaranteed to match the requested page. * Tool description strings reflect the multi-page semantics so the LLM client learns the new contract. * Package version bumped 0.2.0 → 0.3.0 (additive surface — four new tools, three extended input schemas, canonical XML shape change). * CI: `.github/workflows/test.yml` gains an explicit install + vitest run for the mcp-server package so the new multi-page invariants are covered by automation, not just local runs. Backward compatibility: every existing single-page caller continues to work without modification. The session.xml shape is normalised on every write, removing the wrapper-injection hack from the .drawio download path. Tests: 43 unit tests under `packages/mcp-server/tests/multi-page.test.ts` pin the validator's mxfile path, the page-scoped operations, the XML declaration-prefix handling for both normalizeToMxfile and addPageToDoc, addPageToDoc's rejection of full <mxfile> inputs, the single-page projection used by export_diagram (a direct regression test for the selectPage bug — two distinct page selectors must produce visually different projections), and the Transformer + CNN motivating scenario. A `tests/smoke.mjs` smoke test drives the built `dist/index.js` over JSON-RPC and asserts all 9 tools register with the right input schemas. Root vitest suite (107 tests) still green. * fix(mcp): rewrite page-targeted export browser-side; harden edit/get The page-targeted PNG/SVG export never worked: export_diagram swapped the live session to a single-page projection, slept 3s, then wrote the export flag onto a state object that setState() had already replaced in the store Map — so the browser never saw the request and every such export timed out. The swap+restore also clobbered concurrent edits. Move the projection entirely browser-side: requestExport() hands a single -page <mxfile> to the bridge via state.exportXml; the bridge loads it, lets draw.io render, exports, then reloads the user's real document. The canonical session state is never mutated, so there is no restore race and no fixed-delay guessing. The export poll now re-reads the live store entry each tick instead of a captured reference. autosave is suppressed and the version-bump reload is skipped while a projection is on screen; if no real document was captured, restore forces a server reload rather than leaving the iframe stuck on the projection. Also: - edit_diagram now returns isError on a page-level failure (selector matched no page / page has no <root>) instead of reporting success-with-warnings and persisting a no-op; the pre-edit history snapshot is taken only after that gate so a failed edit leaves no phantom undo entry. - edit_diagram/get_diagram re-normalise browser-pushed xml to mxfile so a bare <mxGraphModel> can't silently strip a multi-page document. - get_diagram now errors (instead of silently returning the full doc) when a selector is given but the session isn't a parseable mxfile. - page_id / page_name / add_page.id get .min(1) so empty strings can't silently target the first page. - Extract pages.ts:projectPage(), collapsing three copies of the parse→find→serialise projection logic in index.ts. - Replace the never-in-CI tests/smoke.mjs with tests/server-wiring.test.ts, which boots the server from source via tsx and runs under the existing vitest CI step. * chore(mcp): set version to 0.2.1 for release --------- Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2026-06-16 05:45:50 +05:30
*
* Multi-page support
* ------------------
* The canonical in-memory shape for the session XML is always an <mxfile>
* containing one or more <diagram> pages. Legacy callers that pass a bare
* <mxGraphModel> to create_new_diagram are auto-wrapped into a single-page
* mxfile. All page-targeting parameters (page_id / page_name / page_index)
* on edit_diagram, get_diagram, and export_diagram are optional and default
* to the first page. See packages/mcp-server/src/pages.ts for the helper
* surface.
*/
// Setup DOM polyfill for Node.js (required for XML operations)
import { DOMParser } from "linkedom"
;(globalThis as any).DOMParser = DOMParser
// Create XMLSerializer polyfill using outerHTML
class XMLSerializerPolyfill {
serializeToString(node: any): string {
if (node.outerHTML !== undefined) {
return node.outerHTML
}
if (node.documentElement) {
return node.documentElement.outerHTML
}
return ""
}
}
;(globalThis as any).XMLSerializer = XMLSerializerPolyfill
import { createRequire } from "node:module"
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
import open from "open"
import { z } from "zod"
import {
applyDiagramOperations,
type DiagramOperation,
} from "./diagram-operations.js"
fix(mcp): replace edit_diagram 30s time gate with content comparison (#890) * fix(mcp): keep diagram context valid during edits Closes #885 * fix(mcp): replace edit_diagram time gate with content comparison The 30s wall-clock gate rejected slow-but-correct clients (#885). Instead of a timeout, remember the exact state-store XML the model last saw (get_diagram / create_new_diagram / edit_diagram / page CRUD) and reject edit_diagram only when the live browser state differs - i.e. the user made edits the model hasn't seen yet. Slow reasoning no longer trips the gate, while unseen manual edits still do. * docs(mcp): align edit_diagram/get_diagram descriptions with content-based gate The 'You MUST call get_diagram BEFORE this tool' requirement and the 'Skipping get_diagram WILL cause user's changes to be LOST' warning no longer match server behavior: a stale edit is rejected with no side effects, never silently applied. Describe the freshness check instead, and direct get_diagram usage at its real purpose - learning the current diagram content when the model doesn't already know it. * fix(mcp): compare diagram content structurally in the edit gate draw.io re-serialises the document when pushing state back (attribute order, pretty-printing, regenerated diagram ids, viewport attributes, mxfile host), so byte comparison could flag an unchanged diagram as stale. Fingerprint what a user can actually change instead - page set, page names, and each page's root cell tree with sorted attributes - keeping byte equality as the fast path. A bare mxGraphModel now also fingerprints identically to its single-page mxfile wrapping. * fix(mcp): don't compare page names against bare mxGraphModel pushes A bare <mxGraphModel> pushed by the embed/sync path carries no page name, so normalizeToMxfile invents "Page-1" — falsely reading any custom page name as a content change and re-triggering the stale rejection on every edit. When either side of the gate comparison is a bare mxGraphModel, fingerprint cell trees only; full-mxfile comparisons still detect renames. * chore(mcp): bump version to 0.2.2 * chore(mcp): sync package-lock.json version to 0.2.2 --------- Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2026-07-12 13:33:20 +07:00
import { checkEditGate } from "./edit-gate.js"
import { addHistory } from "./history.js"
import {
getState,
feat(mcp): add multi-page (mxfile) support to MCP server (#862) * feat(mcp): add multi-page (mxfile) support The MCP server's write path could only address a single drawio page even though the underlying .drawio file format and the embedded editor both natively support multi-page documents. A user asking for "a second page with a CNN diagram" would hit the validator with the error "Expected closing tag </root> but found </mxCell>" because the validator assumed input was a bare <mxGraphModel> and could not walk past the <mxfile><diagram>...</diagram></mxfile> wrapper. This patch closes the gap end to end: * New helper module `pages.ts` centralises page CRUD (normalize, parse, list, find, add, rename, delete) so every layer agrees that the canonical in-memory shape is always <mxfile>. normalizeToMxfile and addPageToDoc both strip any leading <?xml ?> declaration before embedding a fragment inside <diagram> (the declaration is only valid at document start). addPageToDoc explicitly rejects full <mxfile> inputs so a caller cannot accidentally nest a document inside a page. * `xml-validation.ts` now detects an <mxfile> root and scopes the duplicate-id check per <diagram>. The legacy regex check would otherwise reject every multi-page doc, because cells "0" and "1" repeat in each page's <root> by design. The DOM-parse path is gated by a cheap regex pre-check so legacy bare <mxGraphModel> callers don't pay any extra cost. The autoFix duplicate-id rename step is also guarded against mxfile inputs — renaming those sentinel cells would silently break drawio's parent references. * `diagram-operations.ts` accepts an optional PageSelector. For <mxfile> input it resolves the page first and scopes all querySelectorAll calls to that page's <root>, so a delete on page 2's cell "2" no longer touches page 1's cell "2". * `create_new_diagram` accepts either a bare <mxGraphModel> (legacy, auto-wrapped into a single-page mxfile) or a full <mxfile> with N diagrams. All existing single-page callers keep working unchanged. * `edit_diagram`, `get_diagram`, and `export_diagram` gain optional `page_id` / `page_name` / `page_index` parameters. When omitted they target the first page — the "active by convention" default. Tool handlers with all-optional input schemas coalesce missing arguments via `input ?? {}` so a no-args MCP invocation can't crash on destructure before reaching the session-existence check. * New tools: `list_pages`, `add_page`, `rename_page`, `delete_page`. * Page-targeted PNG/SVG export uses a "load + export + restore" dance: the server projects the target page into a single-page <mxfile>, pushes it into the transient state so the browser reloads the iframe with just that page, waits for drawio to render (~3s), triggers the export, captures the data, and then restores the original multi-page document. The dance is wrapped in `try/finally` so the restore runs unconditionally — even if an exception is thrown mid-dance, the user's multi-tab view is recovered before the function returns. The earlier attempt to use drawio's `selectPage` postMessage was a no-op because drawio's JSON embed protocol does not expose that action — silently exporting whatever tab happened to be active. The load-export-restore approach trades a brief visible tab-flicker for correctness: the exported image is guaranteed to match the requested page. * Tool description strings reflect the multi-page semantics so the LLM client learns the new contract. * Package version bumped 0.2.0 → 0.3.0 (additive surface — four new tools, three extended input schemas, canonical XML shape change). * CI: `.github/workflows/test.yml` gains an explicit install + vitest run for the mcp-server package so the new multi-page invariants are covered by automation, not just local runs. Backward compatibility: every existing single-page caller continues to work without modification. The session.xml shape is normalised on every write, removing the wrapper-injection hack from the .drawio download path. Tests: 43 unit tests under `packages/mcp-server/tests/multi-page.test.ts` pin the validator's mxfile path, the page-scoped operations, the XML declaration-prefix handling for both normalizeToMxfile and addPageToDoc, addPageToDoc's rejection of full <mxfile> inputs, the single-page projection used by export_diagram (a direct regression test for the selectPage bug — two distinct page selectors must produce visually different projections), and the Transformer + CNN motivating scenario. A `tests/smoke.mjs` smoke test drives the built `dist/index.js` over JSON-RPC and asserts all 9 tools register with the right input schemas. Root vitest suite (107 tests) still green. * fix(mcp): rewrite page-targeted export browser-side; harden edit/get The page-targeted PNG/SVG export never worked: export_diagram swapped the live session to a single-page projection, slept 3s, then wrote the export flag onto a state object that setState() had already replaced in the store Map — so the browser never saw the request and every such export timed out. The swap+restore also clobbered concurrent edits. Move the projection entirely browser-side: requestExport() hands a single -page <mxfile> to the bridge via state.exportXml; the bridge loads it, lets draw.io render, exports, then reloads the user's real document. The canonical session state is never mutated, so there is no restore race and no fixed-delay guessing. The export poll now re-reads the live store entry each tick instead of a captured reference. autosave is suppressed and the version-bump reload is skipped while a projection is on screen; if no real document was captured, restore forces a server reload rather than leaving the iframe stuck on the projection. Also: - edit_diagram now returns isError on a page-level failure (selector matched no page / page has no <root>) instead of reporting success-with-warnings and persisting a no-op; the pre-edit history snapshot is taken only after that gate so a failed edit leaves no phantom undo entry. - edit_diagram/get_diagram re-normalise browser-pushed xml to mxfile so a bare <mxGraphModel> can't silently strip a multi-page document. - get_diagram now errors (instead of silently returning the full doc) when a selector is given but the session isn't a parseable mxfile. - page_id / page_name / add_page.id get .min(1) so empty strings can't silently target the first page. - Extract pages.ts:projectPage(), collapsing three copies of the parse→find→serialise projection logic in index.ts. - Replace the never-in-CI tests/smoke.mjs with tests/server-wiring.test.ts, which boots the server from source via tsx and runs under the existing vitest CI step. * chore(mcp): set version to 0.2.1 for release --------- Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2026-06-16 05:45:50 +05:30
requestExport,
requestSync,
setState,
shutdown,
startHttpServer,
waitForSync,
} from "./http-server.js"
import { parseDrawioFileContent } from "./load-diagram.js"
import { log } from "./logger.js"
feat(mcp): add multi-page (mxfile) support to MCP server (#862) * feat(mcp): add multi-page (mxfile) support The MCP server's write path could only address a single drawio page even though the underlying .drawio file format and the embedded editor both natively support multi-page documents. A user asking for "a second page with a CNN diagram" would hit the validator with the error "Expected closing tag </root> but found </mxCell>" because the validator assumed input was a bare <mxGraphModel> and could not walk past the <mxfile><diagram>...</diagram></mxfile> wrapper. This patch closes the gap end to end: * New helper module `pages.ts` centralises page CRUD (normalize, parse, list, find, add, rename, delete) so every layer agrees that the canonical in-memory shape is always <mxfile>. normalizeToMxfile and addPageToDoc both strip any leading <?xml ?> declaration before embedding a fragment inside <diagram> (the declaration is only valid at document start). addPageToDoc explicitly rejects full <mxfile> inputs so a caller cannot accidentally nest a document inside a page. * `xml-validation.ts` now detects an <mxfile> root and scopes the duplicate-id check per <diagram>. The legacy regex check would otherwise reject every multi-page doc, because cells "0" and "1" repeat in each page's <root> by design. The DOM-parse path is gated by a cheap regex pre-check so legacy bare <mxGraphModel> callers don't pay any extra cost. The autoFix duplicate-id rename step is also guarded against mxfile inputs — renaming those sentinel cells would silently break drawio's parent references. * `diagram-operations.ts` accepts an optional PageSelector. For <mxfile> input it resolves the page first and scopes all querySelectorAll calls to that page's <root>, so a delete on page 2's cell "2" no longer touches page 1's cell "2". * `create_new_diagram` accepts either a bare <mxGraphModel> (legacy, auto-wrapped into a single-page mxfile) or a full <mxfile> with N diagrams. All existing single-page callers keep working unchanged. * `edit_diagram`, `get_diagram`, and `export_diagram` gain optional `page_id` / `page_name` / `page_index` parameters. When omitted they target the first page — the "active by convention" default. Tool handlers with all-optional input schemas coalesce missing arguments via `input ?? {}` so a no-args MCP invocation can't crash on destructure before reaching the session-existence check. * New tools: `list_pages`, `add_page`, `rename_page`, `delete_page`. * Page-targeted PNG/SVG export uses a "load + export + restore" dance: the server projects the target page into a single-page <mxfile>, pushes it into the transient state so the browser reloads the iframe with just that page, waits for drawio to render (~3s), triggers the export, captures the data, and then restores the original multi-page document. The dance is wrapped in `try/finally` so the restore runs unconditionally — even if an exception is thrown mid-dance, the user's multi-tab view is recovered before the function returns. The earlier attempt to use drawio's `selectPage` postMessage was a no-op because drawio's JSON embed protocol does not expose that action — silently exporting whatever tab happened to be active. The load-export-restore approach trades a brief visible tab-flicker for correctness: the exported image is guaranteed to match the requested page. * Tool description strings reflect the multi-page semantics so the LLM client learns the new contract. * Package version bumped 0.2.0 → 0.3.0 (additive surface — four new tools, three extended input schemas, canonical XML shape change). * CI: `.github/workflows/test.yml` gains an explicit install + vitest run for the mcp-server package so the new multi-page invariants are covered by automation, not just local runs. Backward compatibility: every existing single-page caller continues to work without modification. The session.xml shape is normalised on every write, removing the wrapper-injection hack from the .drawio download path. Tests: 43 unit tests under `packages/mcp-server/tests/multi-page.test.ts` pin the validator's mxfile path, the page-scoped operations, the XML declaration-prefix handling for both normalizeToMxfile and addPageToDoc, addPageToDoc's rejection of full <mxfile> inputs, the single-page projection used by export_diagram (a direct regression test for the selectPage bug — two distinct page selectors must produce visually different projections), and the Transformer + CNN motivating scenario. A `tests/smoke.mjs` smoke test drives the built `dist/index.js` over JSON-RPC and asserts all 9 tools register with the right input schemas. Root vitest suite (107 tests) still green. * fix(mcp): rewrite page-targeted export browser-side; harden edit/get The page-targeted PNG/SVG export never worked: export_diagram swapped the live session to a single-page projection, slept 3s, then wrote the export flag onto a state object that setState() had already replaced in the store Map — so the browser never saw the request and every such export timed out. The swap+restore also clobbered concurrent edits. Move the projection entirely browser-side: requestExport() hands a single -page <mxfile> to the bridge via state.exportXml; the bridge loads it, lets draw.io render, exports, then reloads the user's real document. The canonical session state is never mutated, so there is no restore race and no fixed-delay guessing. The export poll now re-reads the live store entry each tick instead of a captured reference. autosave is suppressed and the version-bump reload is skipped while a projection is on screen; if no real document was captured, restore forces a server reload rather than leaving the iframe stuck on the projection. Also: - edit_diagram now returns isError on a page-level failure (selector matched no page / page has no <root>) instead of reporting success-with-warnings and persisting a no-op; the pre-edit history snapshot is taken only after that gate so a failed edit leaves no phantom undo entry. - edit_diagram/get_diagram re-normalise browser-pushed xml to mxfile so a bare <mxGraphModel> can't silently strip a multi-page document. - get_diagram now errors (instead of silently returning the full doc) when a selector is given but the session isn't a parseable mxfile. - page_id / page_name / add_page.id get .min(1) so empty strings can't silently target the first page. - Extract pages.ts:projectPage(), collapsing three copies of the parse→find→serialise projection logic in index.ts. - Replace the never-in-CI tests/smoke.mjs with tests/server-wiring.test.ts, which boots the server from source via tsx and runs under the existing vitest CI step. * chore(mcp): set version to 0.2.1 for release --------- Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2026-06-16 05:45:50 +05:30
import {
addPageToDoc,
deletePageFromDoc,
hasPageSelector,
listPagesFromDoc,
normalizeToMxfile,
type PageSelector,
parseMxfile,
projectPage,
renamePageInDoc,
serializeMxfile,
} from "./pages.js"
import { validateAndFixXml } from "./xml-validation.js"
// Server configuration
const config = {
port: parseInt(process.env.PORT || "6002", 10),
}
// Session state (single session for simplicity)
let currentSession: {
id: string
xml: string
version: number
fix(mcp): replace edit_diagram 30s time gate with content comparison (#890) * fix(mcp): keep diagram context valid during edits Closes #885 * fix(mcp): replace edit_diagram time gate with content comparison The 30s wall-clock gate rejected slow-but-correct clients (#885). Instead of a timeout, remember the exact state-store XML the model last saw (get_diagram / create_new_diagram / edit_diagram / page CRUD) and reject edit_diagram only when the live browser state differs - i.e. the user made edits the model hasn't seen yet. Slow reasoning no longer trips the gate, while unseen manual edits still do. * docs(mcp): align edit_diagram/get_diagram descriptions with content-based gate The 'You MUST call get_diagram BEFORE this tool' requirement and the 'Skipping get_diagram WILL cause user's changes to be LOST' warning no longer match server behavior: a stale edit is rejected with no side effects, never silently applied. Describe the freshness check instead, and direct get_diagram usage at its real purpose - learning the current diagram content when the model doesn't already know it. * fix(mcp): compare diagram content structurally in the edit gate draw.io re-serialises the document when pushing state back (attribute order, pretty-printing, regenerated diagram ids, viewport attributes, mxfile host), so byte comparison could flag an unchanged diagram as stale. Fingerprint what a user can actually change instead - page set, page names, and each page's root cell tree with sorted attributes - keeping byte equality as the fast path. A bare mxGraphModel now also fingerprints identically to its single-page mxfile wrapping. * fix(mcp): don't compare page names against bare mxGraphModel pushes A bare <mxGraphModel> pushed by the embed/sync path carries no page name, so normalizeToMxfile invents "Page-1" — falsely reading any custom page name as a content change and re-triggering the stale rejection on every edit. When either side of the gate comparison is a bare mxGraphModel, fingerprint cell trees only; full-mxfile comparisons still detect renames. * chore(mcp): bump version to 0.2.2 * chore(mcp): sync package-lock.json version to 0.2.2 --------- Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2026-07-12 13:33:20 +07:00
// The exact state-store XML the model last saw (get_diagram) or wrote
// itself (create/edit/page CRUD). The store only changes on server
// writes or browser pushes (user autosave / sync), so edit_diagram can
// detect unseen user edits by comparing the live store against this.
// Empty = no diagram context established yet.
lastSeenXml: string
} | null = null
// Create MCP server. The version reported in the MCP handshake is read from
// package.json so it can never drift from the published npm version again
// (it sat hardcoded at stale values for most of this package's history).
// Both src/ (tsx dev) and dist/ (published build) live one level below the
// package root, so the relative path works in either runtime.
const require = createRequire(import.meta.url)
const packageVersion: string = require("../package.json").version
const server = new McpServer({
name: "next-ai-drawio",
version: packageVersion,
})
feat(mcp): add multi-page (mxfile) support to MCP server (#862) * feat(mcp): add multi-page (mxfile) support The MCP server's write path could only address a single drawio page even though the underlying .drawio file format and the embedded editor both natively support multi-page documents. A user asking for "a second page with a CNN diagram" would hit the validator with the error "Expected closing tag </root> but found </mxCell>" because the validator assumed input was a bare <mxGraphModel> and could not walk past the <mxfile><diagram>...</diagram></mxfile> wrapper. This patch closes the gap end to end: * New helper module `pages.ts` centralises page CRUD (normalize, parse, list, find, add, rename, delete) so every layer agrees that the canonical in-memory shape is always <mxfile>. normalizeToMxfile and addPageToDoc both strip any leading <?xml ?> declaration before embedding a fragment inside <diagram> (the declaration is only valid at document start). addPageToDoc explicitly rejects full <mxfile> inputs so a caller cannot accidentally nest a document inside a page. * `xml-validation.ts` now detects an <mxfile> root and scopes the duplicate-id check per <diagram>. The legacy regex check would otherwise reject every multi-page doc, because cells "0" and "1" repeat in each page's <root> by design. The DOM-parse path is gated by a cheap regex pre-check so legacy bare <mxGraphModel> callers don't pay any extra cost. The autoFix duplicate-id rename step is also guarded against mxfile inputs — renaming those sentinel cells would silently break drawio's parent references. * `diagram-operations.ts` accepts an optional PageSelector. For <mxfile> input it resolves the page first and scopes all querySelectorAll calls to that page's <root>, so a delete on page 2's cell "2" no longer touches page 1's cell "2". * `create_new_diagram` accepts either a bare <mxGraphModel> (legacy, auto-wrapped into a single-page mxfile) or a full <mxfile> with N diagrams. All existing single-page callers keep working unchanged. * `edit_diagram`, `get_diagram`, and `export_diagram` gain optional `page_id` / `page_name` / `page_index` parameters. When omitted they target the first page — the "active by convention" default. Tool handlers with all-optional input schemas coalesce missing arguments via `input ?? {}` so a no-args MCP invocation can't crash on destructure before reaching the session-existence check. * New tools: `list_pages`, `add_page`, `rename_page`, `delete_page`. * Page-targeted PNG/SVG export uses a "load + export + restore" dance: the server projects the target page into a single-page <mxfile>, pushes it into the transient state so the browser reloads the iframe with just that page, waits for drawio to render (~3s), triggers the export, captures the data, and then restores the original multi-page document. The dance is wrapped in `try/finally` so the restore runs unconditionally — even if an exception is thrown mid-dance, the user's multi-tab view is recovered before the function returns. The earlier attempt to use drawio's `selectPage` postMessage was a no-op because drawio's JSON embed protocol does not expose that action — silently exporting whatever tab happened to be active. The load-export-restore approach trades a brief visible tab-flicker for correctness: the exported image is guaranteed to match the requested page. * Tool description strings reflect the multi-page semantics so the LLM client learns the new contract. * Package version bumped 0.2.0 → 0.3.0 (additive surface — four new tools, three extended input schemas, canonical XML shape change). * CI: `.github/workflows/test.yml` gains an explicit install + vitest run for the mcp-server package so the new multi-page invariants are covered by automation, not just local runs. Backward compatibility: every existing single-page caller continues to work without modification. The session.xml shape is normalised on every write, removing the wrapper-injection hack from the .drawio download path. Tests: 43 unit tests under `packages/mcp-server/tests/multi-page.test.ts` pin the validator's mxfile path, the page-scoped operations, the XML declaration-prefix handling for both normalizeToMxfile and addPageToDoc, addPageToDoc's rejection of full <mxfile> inputs, the single-page projection used by export_diagram (a direct regression test for the selectPage bug — two distinct page selectors must produce visually different projections), and the Transformer + CNN motivating scenario. A `tests/smoke.mjs` smoke test drives the built `dist/index.js` over JSON-RPC and asserts all 9 tools register with the right input schemas. Root vitest suite (107 tests) still green. * fix(mcp): rewrite page-targeted export browser-side; harden edit/get The page-targeted PNG/SVG export never worked: export_diagram swapped the live session to a single-page projection, slept 3s, then wrote the export flag onto a state object that setState() had already replaced in the store Map — so the browser never saw the request and every such export timed out. The swap+restore also clobbered concurrent edits. Move the projection entirely browser-side: requestExport() hands a single -page <mxfile> to the bridge via state.exportXml; the bridge loads it, lets draw.io render, exports, then reloads the user's real document. The canonical session state is never mutated, so there is no restore race and no fixed-delay guessing. The export poll now re-reads the live store entry each tick instead of a captured reference. autosave is suppressed and the version-bump reload is skipped while a projection is on screen; if no real document was captured, restore forces a server reload rather than leaving the iframe stuck on the projection. Also: - edit_diagram now returns isError on a page-level failure (selector matched no page / page has no <root>) instead of reporting success-with-warnings and persisting a no-op; the pre-edit history snapshot is taken only after that gate so a failed edit leaves no phantom undo entry. - edit_diagram/get_diagram re-normalise browser-pushed xml to mxfile so a bare <mxGraphModel> can't silently strip a multi-page document. - get_diagram now errors (instead of silently returning the full doc) when a selector is given but the session isn't a parseable mxfile. - page_id / page_name / add_page.id get .min(1) so empty strings can't silently target the first page. - Extract pages.ts:projectPage(), collapsing three copies of the parse→find→serialise projection logic in index.ts. - Replace the never-in-CI tests/smoke.mjs with tests/server-wiring.test.ts, which boots the server from source via tsx and runs under the existing vitest CI step. * chore(mcp): set version to 0.2.1 for release --------- Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2026-06-16 05:45:50 +05:30
// Shared Zod schema fragment for page-targeting parameters.
// Every multi-page-aware tool reuses these three optional fields so the LLM
// learns one consistent interface.
const pageSelectorSchema = {
page_id: z
.string()
.min(1)
.optional()
.describe(
"Target a page by its id (as returned by list_pages or add_page). Wins over page_name and page_index when multiple are set.",
),
page_name: z
.string()
.min(1)
.optional()
.describe(
'Target a page by its display name (e.g. "CNN"). Used only when page_id is not set.',
),
page_index: z
.number()
.int()
.nonnegative()
.optional()
.describe(
"Target a page by its 0-based tab index. Used only when page_id and page_name are not set.",
),
}
/**
* Pull a clean PageSelector out of a tool's parsed input.
* Returns an empty object when none of the page_* fields are set, so callers
* can simply pass it through to the lower layers (they treat empty as "first
* page" by convention).
*/
function pickPageSelector(input: {
page_id?: string
page_name?: string
page_index?: number
}): PageSelector {
const selector: PageSelector = {}
if (input.page_id) selector.page_id = input.page_id
if (input.page_name) selector.page_name = input.page_name
if (input.page_index !== undefined) selector.page_index = input.page_index
return selector
}
/** Format a selector for human-readable error messages. */
function describeSelector(s: PageSelector): string {
if (s.page_id) return `id="${s.page_id}"`
if (s.page_name) return `name="${s.page_name}"`
if (s.page_index !== undefined) return `index=${s.page_index}`
return "first page"
}
// Register prompt with workflow guidance
server.prompt(
"diagram-workflow",
"Guidelines for creating and editing draw.io diagrams",
() => ({
messages: [
{
role: "user",
content: {
type: "text",
text: `# Draw.io Diagram Workflow Guidelines
## Creating a New Diagram
1. Call start_session to open the browser preview
feat(mcp): add multi-page (mxfile) support to MCP server (#862) * feat(mcp): add multi-page (mxfile) support The MCP server's write path could only address a single drawio page even though the underlying .drawio file format and the embedded editor both natively support multi-page documents. A user asking for "a second page with a CNN diagram" would hit the validator with the error "Expected closing tag </root> but found </mxCell>" because the validator assumed input was a bare <mxGraphModel> and could not walk past the <mxfile><diagram>...</diagram></mxfile> wrapper. This patch closes the gap end to end: * New helper module `pages.ts` centralises page CRUD (normalize, parse, list, find, add, rename, delete) so every layer agrees that the canonical in-memory shape is always <mxfile>. normalizeToMxfile and addPageToDoc both strip any leading <?xml ?> declaration before embedding a fragment inside <diagram> (the declaration is only valid at document start). addPageToDoc explicitly rejects full <mxfile> inputs so a caller cannot accidentally nest a document inside a page. * `xml-validation.ts` now detects an <mxfile> root and scopes the duplicate-id check per <diagram>. The legacy regex check would otherwise reject every multi-page doc, because cells "0" and "1" repeat in each page's <root> by design. The DOM-parse path is gated by a cheap regex pre-check so legacy bare <mxGraphModel> callers don't pay any extra cost. The autoFix duplicate-id rename step is also guarded against mxfile inputs — renaming those sentinel cells would silently break drawio's parent references. * `diagram-operations.ts` accepts an optional PageSelector. For <mxfile> input it resolves the page first and scopes all querySelectorAll calls to that page's <root>, so a delete on page 2's cell "2" no longer touches page 1's cell "2". * `create_new_diagram` accepts either a bare <mxGraphModel> (legacy, auto-wrapped into a single-page mxfile) or a full <mxfile> with N diagrams. All existing single-page callers keep working unchanged. * `edit_diagram`, `get_diagram`, and `export_diagram` gain optional `page_id` / `page_name` / `page_index` parameters. When omitted they target the first page — the "active by convention" default. Tool handlers with all-optional input schemas coalesce missing arguments via `input ?? {}` so a no-args MCP invocation can't crash on destructure before reaching the session-existence check. * New tools: `list_pages`, `add_page`, `rename_page`, `delete_page`. * Page-targeted PNG/SVG export uses a "load + export + restore" dance: the server projects the target page into a single-page <mxfile>, pushes it into the transient state so the browser reloads the iframe with just that page, waits for drawio to render (~3s), triggers the export, captures the data, and then restores the original multi-page document. The dance is wrapped in `try/finally` so the restore runs unconditionally — even if an exception is thrown mid-dance, the user's multi-tab view is recovered before the function returns. The earlier attempt to use drawio's `selectPage` postMessage was a no-op because drawio's JSON embed protocol does not expose that action — silently exporting whatever tab happened to be active. The load-export-restore approach trades a brief visible tab-flicker for correctness: the exported image is guaranteed to match the requested page. * Tool description strings reflect the multi-page semantics so the LLM client learns the new contract. * Package version bumped 0.2.0 → 0.3.0 (additive surface — four new tools, three extended input schemas, canonical XML shape change). * CI: `.github/workflows/test.yml` gains an explicit install + vitest run for the mcp-server package so the new multi-page invariants are covered by automation, not just local runs. Backward compatibility: every existing single-page caller continues to work without modification. The session.xml shape is normalised on every write, removing the wrapper-injection hack from the .drawio download path. Tests: 43 unit tests under `packages/mcp-server/tests/multi-page.test.ts` pin the validator's mxfile path, the page-scoped operations, the XML declaration-prefix handling for both normalizeToMxfile and addPageToDoc, addPageToDoc's rejection of full <mxfile> inputs, the single-page projection used by export_diagram (a direct regression test for the selectPage bug — two distinct page selectors must produce visually different projections), and the Transformer + CNN motivating scenario. A `tests/smoke.mjs` smoke test drives the built `dist/index.js` over JSON-RPC and asserts all 9 tools register with the right input schemas. Root vitest suite (107 tests) still green. * fix(mcp): rewrite page-targeted export browser-side; harden edit/get The page-targeted PNG/SVG export never worked: export_diagram swapped the live session to a single-page projection, slept 3s, then wrote the export flag onto a state object that setState() had already replaced in the store Map — so the browser never saw the request and every such export timed out. The swap+restore also clobbered concurrent edits. Move the projection entirely browser-side: requestExport() hands a single -page <mxfile> to the bridge via state.exportXml; the bridge loads it, lets draw.io render, exports, then reloads the user's real document. The canonical session state is never mutated, so there is no restore race and no fixed-delay guessing. The export poll now re-reads the live store entry each tick instead of a captured reference. autosave is suppressed and the version-bump reload is skipped while a projection is on screen; if no real document was captured, restore forces a server reload rather than leaving the iframe stuck on the projection. Also: - edit_diagram now returns isError on a page-level failure (selector matched no page / page has no <root>) instead of reporting success-with-warnings and persisting a no-op; the pre-edit history snapshot is taken only after that gate so a failed edit leaves no phantom undo entry. - edit_diagram/get_diagram re-normalise browser-pushed xml to mxfile so a bare <mxGraphModel> can't silently strip a multi-page document. - get_diagram now errors (instead of silently returning the full doc) when a selector is given but the session isn't a parseable mxfile. - page_id / page_name / add_page.id get .min(1) so empty strings can't silently target the first page. - Extract pages.ts:projectPage(), collapsing three copies of the parse→find→serialise projection logic in index.ts. - Replace the never-in-CI tests/smoke.mjs with tests/server-wiring.test.ts, which boots the server from source via tsx and runs under the existing vitest CI step. * chore(mcp): set version to 0.2.1 for release --------- Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2026-06-16 05:45:50 +05:30
2. Use create_new_diagram with either a bare <mxGraphModel> (single page) or a full <mxfile> with one or more <diagram> children (multi-page)
## Opening an Existing .drawio File
- Use load_diagram with the file path the server reads and decompresses the file itself; don't read it and pass the XML through create_new_diagram
- After loading, call get_diagram once before editing (you haven't seen the file's cell IDs yet)
feat(mcp): add multi-page (mxfile) support to MCP server (#862) * feat(mcp): add multi-page (mxfile) support The MCP server's write path could only address a single drawio page even though the underlying .drawio file format and the embedded editor both natively support multi-page documents. A user asking for "a second page with a CNN diagram" would hit the validator with the error "Expected closing tag </root> but found </mxCell>" because the validator assumed input was a bare <mxGraphModel> and could not walk past the <mxfile><diagram>...</diagram></mxfile> wrapper. This patch closes the gap end to end: * New helper module `pages.ts` centralises page CRUD (normalize, parse, list, find, add, rename, delete) so every layer agrees that the canonical in-memory shape is always <mxfile>. normalizeToMxfile and addPageToDoc both strip any leading <?xml ?> declaration before embedding a fragment inside <diagram> (the declaration is only valid at document start). addPageToDoc explicitly rejects full <mxfile> inputs so a caller cannot accidentally nest a document inside a page. * `xml-validation.ts` now detects an <mxfile> root and scopes the duplicate-id check per <diagram>. The legacy regex check would otherwise reject every multi-page doc, because cells "0" and "1" repeat in each page's <root> by design. The DOM-parse path is gated by a cheap regex pre-check so legacy bare <mxGraphModel> callers don't pay any extra cost. The autoFix duplicate-id rename step is also guarded against mxfile inputs — renaming those sentinel cells would silently break drawio's parent references. * `diagram-operations.ts` accepts an optional PageSelector. For <mxfile> input it resolves the page first and scopes all querySelectorAll calls to that page's <root>, so a delete on page 2's cell "2" no longer touches page 1's cell "2". * `create_new_diagram` accepts either a bare <mxGraphModel> (legacy, auto-wrapped into a single-page mxfile) or a full <mxfile> with N diagrams. All existing single-page callers keep working unchanged. * `edit_diagram`, `get_diagram`, and `export_diagram` gain optional `page_id` / `page_name` / `page_index` parameters. When omitted they target the first page — the "active by convention" default. Tool handlers with all-optional input schemas coalesce missing arguments via `input ?? {}` so a no-args MCP invocation can't crash on destructure before reaching the session-existence check. * New tools: `list_pages`, `add_page`, `rename_page`, `delete_page`. * Page-targeted PNG/SVG export uses a "load + export + restore" dance: the server projects the target page into a single-page <mxfile>, pushes it into the transient state so the browser reloads the iframe with just that page, waits for drawio to render (~3s), triggers the export, captures the data, and then restores the original multi-page document. The dance is wrapped in `try/finally` so the restore runs unconditionally — even if an exception is thrown mid-dance, the user's multi-tab view is recovered before the function returns. The earlier attempt to use drawio's `selectPage` postMessage was a no-op because drawio's JSON embed protocol does not expose that action — silently exporting whatever tab happened to be active. The load-export-restore approach trades a brief visible tab-flicker for correctness: the exported image is guaranteed to match the requested page. * Tool description strings reflect the multi-page semantics so the LLM client learns the new contract. * Package version bumped 0.2.0 → 0.3.0 (additive surface — four new tools, three extended input schemas, canonical XML shape change). * CI: `.github/workflows/test.yml` gains an explicit install + vitest run for the mcp-server package so the new multi-page invariants are covered by automation, not just local runs. Backward compatibility: every existing single-page caller continues to work without modification. The session.xml shape is normalised on every write, removing the wrapper-injection hack from the .drawio download path. Tests: 43 unit tests under `packages/mcp-server/tests/multi-page.test.ts` pin the validator's mxfile path, the page-scoped operations, the XML declaration-prefix handling for both normalizeToMxfile and addPageToDoc, addPageToDoc's rejection of full <mxfile> inputs, the single-page projection used by export_diagram (a direct regression test for the selectPage bug — two distinct page selectors must produce visually different projections), and the Transformer + CNN motivating scenario. A `tests/smoke.mjs` smoke test drives the built `dist/index.js` over JSON-RPC and asserts all 9 tools register with the right input schemas. Root vitest suite (107 tests) still green. * fix(mcp): rewrite page-targeted export browser-side; harden edit/get The page-targeted PNG/SVG export never worked: export_diagram swapped the live session to a single-page projection, slept 3s, then wrote the export flag onto a state object that setState() had already replaced in the store Map — so the browser never saw the request and every such export timed out. The swap+restore also clobbered concurrent edits. Move the projection entirely browser-side: requestExport() hands a single -page <mxfile> to the bridge via state.exportXml; the bridge loads it, lets draw.io render, exports, then reloads the user's real document. The canonical session state is never mutated, so there is no restore race and no fixed-delay guessing. The export poll now re-reads the live store entry each tick instead of a captured reference. autosave is suppressed and the version-bump reload is skipped while a projection is on screen; if no real document was captured, restore forces a server reload rather than leaving the iframe stuck on the projection. Also: - edit_diagram now returns isError on a page-level failure (selector matched no page / page has no <root>) instead of reporting success-with-warnings and persisting a no-op; the pre-edit history snapshot is taken only after that gate so a failed edit leaves no phantom undo entry. - edit_diagram/get_diagram re-normalise browser-pushed xml to mxfile so a bare <mxGraphModel> can't silently strip a multi-page document. - get_diagram now errors (instead of silently returning the full doc) when a selector is given but the session isn't a parseable mxfile. - page_id / page_name / add_page.id get .min(1) so empty strings can't silently target the first page. - Extract pages.ts:projectPage(), collapsing three copies of the parse→find→serialise projection logic in index.ts. - Replace the never-in-CI tests/smoke.mjs with tests/server-wiring.test.ts, which boots the server from source via tsx and runs under the existing vitest CI step. * chore(mcp): set version to 0.2.1 for release --------- Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2026-06-16 05:45:50 +05:30
## Working with Multiple Pages
- Use list_pages to discover existing pages (id, name, index)
- Use add_page to append a new page (without losing existing ones unlike create_new_diagram which REPLACES everything)
- Use rename_page / delete_page for management
- edit_diagram, get_diagram, and export_diagram all accept optional page_id / page_name / page_index when omitted they target the first page
fix(mcp): replace edit_diagram 30s time gate with content comparison (#890) * fix(mcp): keep diagram context valid during edits Closes #885 * fix(mcp): replace edit_diagram time gate with content comparison The 30s wall-clock gate rejected slow-but-correct clients (#885). Instead of a timeout, remember the exact state-store XML the model last saw (get_diagram / create_new_diagram / edit_diagram / page CRUD) and reject edit_diagram only when the live browser state differs - i.e. the user made edits the model hasn't seen yet. Slow reasoning no longer trips the gate, while unseen manual edits still do. * docs(mcp): align edit_diagram/get_diagram descriptions with content-based gate The 'You MUST call get_diagram BEFORE this tool' requirement and the 'Skipping get_diagram WILL cause user's changes to be LOST' warning no longer match server behavior: a stale edit is rejected with no side effects, never silently applied. Describe the freshness check instead, and direct get_diagram usage at its real purpose - learning the current diagram content when the model doesn't already know it. * fix(mcp): compare diagram content structurally in the edit gate draw.io re-serialises the document when pushing state back (attribute order, pretty-printing, regenerated diagram ids, viewport attributes, mxfile host), so byte comparison could flag an unchanged diagram as stale. Fingerprint what a user can actually change instead - page set, page names, and each page's root cell tree with sorted attributes - keeping byte equality as the fast path. A bare mxGraphModel now also fingerprints identically to its single-page mxfile wrapping. * fix(mcp): don't compare page names against bare mxGraphModel pushes A bare <mxGraphModel> pushed by the embed/sync path carries no page name, so normalizeToMxfile invents "Page-1" — falsely reading any custom page name as a content change and re-triggering the stale rejection on every edit. When either side of the gate comparison is a bare mxGraphModel, fingerprint cell trees only; full-mxfile comparisons still detect renames. * chore(mcp): bump version to 0.2.2 * chore(mcp): sync package-lock.json version to 0.2.2 --------- Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2026-07-12 13:33:20 +07:00
## Editing a Page (add / update / delete cells)
1. Call edit_diagram with your operations, optionally with a page selector
2. If you don't know the current cell IDs or structure, call get_diagram first
3. For add/update, provide the cell_id and complete mxCell XML
4. No need to call get_diagram before every edit: the server rejects the edit (with no side effects) if the user changed the diagram in the browser since you last saw it, and tells you to call get_diagram once and retry
## Important Notes
feat(mcp): add multi-page (mxfile) support to MCP server (#862) * feat(mcp): add multi-page (mxfile) support The MCP server's write path could only address a single drawio page even though the underlying .drawio file format and the embedded editor both natively support multi-page documents. A user asking for "a second page with a CNN diagram" would hit the validator with the error "Expected closing tag </root> but found </mxCell>" because the validator assumed input was a bare <mxGraphModel> and could not walk past the <mxfile><diagram>...</diagram></mxfile> wrapper. This patch closes the gap end to end: * New helper module `pages.ts` centralises page CRUD (normalize, parse, list, find, add, rename, delete) so every layer agrees that the canonical in-memory shape is always <mxfile>. normalizeToMxfile and addPageToDoc both strip any leading <?xml ?> declaration before embedding a fragment inside <diagram> (the declaration is only valid at document start). addPageToDoc explicitly rejects full <mxfile> inputs so a caller cannot accidentally nest a document inside a page. * `xml-validation.ts` now detects an <mxfile> root and scopes the duplicate-id check per <diagram>. The legacy regex check would otherwise reject every multi-page doc, because cells "0" and "1" repeat in each page's <root> by design. The DOM-parse path is gated by a cheap regex pre-check so legacy bare <mxGraphModel> callers don't pay any extra cost. The autoFix duplicate-id rename step is also guarded against mxfile inputs — renaming those sentinel cells would silently break drawio's parent references. * `diagram-operations.ts` accepts an optional PageSelector. For <mxfile> input it resolves the page first and scopes all querySelectorAll calls to that page's <root>, so a delete on page 2's cell "2" no longer touches page 1's cell "2". * `create_new_diagram` accepts either a bare <mxGraphModel> (legacy, auto-wrapped into a single-page mxfile) or a full <mxfile> with N diagrams. All existing single-page callers keep working unchanged. * `edit_diagram`, `get_diagram`, and `export_diagram` gain optional `page_id` / `page_name` / `page_index` parameters. When omitted they target the first page — the "active by convention" default. Tool handlers with all-optional input schemas coalesce missing arguments via `input ?? {}` so a no-args MCP invocation can't crash on destructure before reaching the session-existence check. * New tools: `list_pages`, `add_page`, `rename_page`, `delete_page`. * Page-targeted PNG/SVG export uses a "load + export + restore" dance: the server projects the target page into a single-page <mxfile>, pushes it into the transient state so the browser reloads the iframe with just that page, waits for drawio to render (~3s), triggers the export, captures the data, and then restores the original multi-page document. The dance is wrapped in `try/finally` so the restore runs unconditionally — even if an exception is thrown mid-dance, the user's multi-tab view is recovered before the function returns. The earlier attempt to use drawio's `selectPage` postMessage was a no-op because drawio's JSON embed protocol does not expose that action — silently exporting whatever tab happened to be active. The load-export-restore approach trades a brief visible tab-flicker for correctness: the exported image is guaranteed to match the requested page. * Tool description strings reflect the multi-page semantics so the LLM client learns the new contract. * Package version bumped 0.2.0 → 0.3.0 (additive surface — four new tools, three extended input schemas, canonical XML shape change). * CI: `.github/workflows/test.yml` gains an explicit install + vitest run for the mcp-server package so the new multi-page invariants are covered by automation, not just local runs. Backward compatibility: every existing single-page caller continues to work without modification. The session.xml shape is normalised on every write, removing the wrapper-injection hack from the .drawio download path. Tests: 43 unit tests under `packages/mcp-server/tests/multi-page.test.ts` pin the validator's mxfile path, the page-scoped operations, the XML declaration-prefix handling for both normalizeToMxfile and addPageToDoc, addPageToDoc's rejection of full <mxfile> inputs, the single-page projection used by export_diagram (a direct regression test for the selectPage bug — two distinct page selectors must produce visually different projections), and the Transformer + CNN motivating scenario. A `tests/smoke.mjs` smoke test drives the built `dist/index.js` over JSON-RPC and asserts all 9 tools register with the right input schemas. Root vitest suite (107 tests) still green. * fix(mcp): rewrite page-targeted export browser-side; harden edit/get The page-targeted PNG/SVG export never worked: export_diagram swapped the live session to a single-page projection, slept 3s, then wrote the export flag onto a state object that setState() had already replaced in the store Map — so the browser never saw the request and every such export timed out. The swap+restore also clobbered concurrent edits. Move the projection entirely browser-side: requestExport() hands a single -page <mxfile> to the bridge via state.exportXml; the bridge loads it, lets draw.io render, exports, then reloads the user's real document. The canonical session state is never mutated, so there is no restore race and no fixed-delay guessing. The export poll now re-reads the live store entry each tick instead of a captured reference. autosave is suppressed and the version-bump reload is skipped while a projection is on screen; if no real document was captured, restore forces a server reload rather than leaving the iframe stuck on the projection. Also: - edit_diagram now returns isError on a page-level failure (selector matched no page / page has no <root>) instead of reporting success-with-warnings and persisting a no-op; the pre-edit history snapshot is taken only after that gate so a failed edit leaves no phantom undo entry. - edit_diagram/get_diagram re-normalise browser-pushed xml to mxfile so a bare <mxGraphModel> can't silently strip a multi-page document. - get_diagram now errors (instead of silently returning the full doc) when a selector is given but the session isn't a parseable mxfile. - page_id / page_name / add_page.id get .min(1) so empty strings can't silently target the first page. - Extract pages.ts:projectPage(), collapsing three copies of the parse→find→serialise projection logic in index.ts. - Replace the never-in-CI tests/smoke.mjs with tests/server-wiring.test.ts, which boots the server from source via tsx and runs under the existing vitest CI step. * chore(mcp): set version to 0.2.1 for release --------- Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2026-06-16 05:45:50 +05:30
- create_new_diagram REPLACES the entire document, including ALL pages - only use for new diagrams. Use add_page to add a tab without losing existing content.
- edit_diagram PRESERVES the user's manual changes (fetches browser state first)
- Always use unique cell_ids within a page (cell ids "0" and "1" are reserved root sentinels and can repeat across pages)`,
},
},
],
}),
)
// Tool: start_session
server.registerTool(
"start_session",
{
description:
"Start a new diagram session and open the browser for real-time preview. " +
"Starts an embedded server and opens a browser window with draw.io. " +
"The browser will show diagram updates as they happen.",
inputSchema: {},
},
async () => {
try {
// Start embedded HTTP server
const port = await startHttpServer(config.port)
// Create session
const sessionId = `mcp-${Date.now().toString(36)}-${Math.random().toString(36).substring(2, 8)}`
currentSession = {
id: sessionId,
xml: "",
version: 0,
fix(mcp): replace edit_diagram 30s time gate with content comparison (#890) * fix(mcp): keep diagram context valid during edits Closes #885 * fix(mcp): replace edit_diagram time gate with content comparison The 30s wall-clock gate rejected slow-but-correct clients (#885). Instead of a timeout, remember the exact state-store XML the model last saw (get_diagram / create_new_diagram / edit_diagram / page CRUD) and reject edit_diagram only when the live browser state differs - i.e. the user made edits the model hasn't seen yet. Slow reasoning no longer trips the gate, while unseen manual edits still do. * docs(mcp): align edit_diagram/get_diagram descriptions with content-based gate The 'You MUST call get_diagram BEFORE this tool' requirement and the 'Skipping get_diagram WILL cause user's changes to be LOST' warning no longer match server behavior: a stale edit is rejected with no side effects, never silently applied. Describe the freshness check instead, and direct get_diagram usage at its real purpose - learning the current diagram content when the model doesn't already know it. * fix(mcp): compare diagram content structurally in the edit gate draw.io re-serialises the document when pushing state back (attribute order, pretty-printing, regenerated diagram ids, viewport attributes, mxfile host), so byte comparison could flag an unchanged diagram as stale. Fingerprint what a user can actually change instead - page set, page names, and each page's root cell tree with sorted attributes - keeping byte equality as the fast path. A bare mxGraphModel now also fingerprints identically to its single-page mxfile wrapping. * fix(mcp): don't compare page names against bare mxGraphModel pushes A bare <mxGraphModel> pushed by the embed/sync path carries no page name, so normalizeToMxfile invents "Page-1" — falsely reading any custom page name as a content change and re-triggering the stale rejection on every edit. When either side of the gate comparison is a bare mxGraphModel, fingerprint cell trees only; full-mxfile comparisons still detect renames. * chore(mcp): bump version to 0.2.2 * chore(mcp): sync package-lock.json version to 0.2.2 --------- Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2026-07-12 13:33:20 +07:00
lastSeenXml: "",
}
// Open browser
const browserUrl = `http://localhost:${port}?mcp=${sessionId}`
await open(browserUrl)
log.info(`Started session ${sessionId}, browser at ${browserUrl}`)
return {
content: [
{
type: "text",
text: `Session started successfully!\n\nSession ID: ${sessionId}\nBrowser URL: ${browserUrl}\n\nThe browser will now show real-time diagram updates.`,
},
],
}
} catch (error) {
const message =
error instanceof Error ? error.message : String(error)
log.error("start_session failed:", message)
return {
content: [{ type: "text", text: `Error: ${message}` }],
isError: true,
}
}
},
)
// Tool: create_new_diagram
server.registerTool(
"create_new_diagram",
{
feat(mcp): add multi-page (mxfile) support to MCP server (#862) * feat(mcp): add multi-page (mxfile) support The MCP server's write path could only address a single drawio page even though the underlying .drawio file format and the embedded editor both natively support multi-page documents. A user asking for "a second page with a CNN diagram" would hit the validator with the error "Expected closing tag </root> but found </mxCell>" because the validator assumed input was a bare <mxGraphModel> and could not walk past the <mxfile><diagram>...</diagram></mxfile> wrapper. This patch closes the gap end to end: * New helper module `pages.ts` centralises page CRUD (normalize, parse, list, find, add, rename, delete) so every layer agrees that the canonical in-memory shape is always <mxfile>. normalizeToMxfile and addPageToDoc both strip any leading <?xml ?> declaration before embedding a fragment inside <diagram> (the declaration is only valid at document start). addPageToDoc explicitly rejects full <mxfile> inputs so a caller cannot accidentally nest a document inside a page. * `xml-validation.ts` now detects an <mxfile> root and scopes the duplicate-id check per <diagram>. The legacy regex check would otherwise reject every multi-page doc, because cells "0" and "1" repeat in each page's <root> by design. The DOM-parse path is gated by a cheap regex pre-check so legacy bare <mxGraphModel> callers don't pay any extra cost. The autoFix duplicate-id rename step is also guarded against mxfile inputs — renaming those sentinel cells would silently break drawio's parent references. * `diagram-operations.ts` accepts an optional PageSelector. For <mxfile> input it resolves the page first and scopes all querySelectorAll calls to that page's <root>, so a delete on page 2's cell "2" no longer touches page 1's cell "2". * `create_new_diagram` accepts either a bare <mxGraphModel> (legacy, auto-wrapped into a single-page mxfile) or a full <mxfile> with N diagrams. All existing single-page callers keep working unchanged. * `edit_diagram`, `get_diagram`, and `export_diagram` gain optional `page_id` / `page_name` / `page_index` parameters. When omitted they target the first page — the "active by convention" default. Tool handlers with all-optional input schemas coalesce missing arguments via `input ?? {}` so a no-args MCP invocation can't crash on destructure before reaching the session-existence check. * New tools: `list_pages`, `add_page`, `rename_page`, `delete_page`. * Page-targeted PNG/SVG export uses a "load + export + restore" dance: the server projects the target page into a single-page <mxfile>, pushes it into the transient state so the browser reloads the iframe with just that page, waits for drawio to render (~3s), triggers the export, captures the data, and then restores the original multi-page document. The dance is wrapped in `try/finally` so the restore runs unconditionally — even if an exception is thrown mid-dance, the user's multi-tab view is recovered before the function returns. The earlier attempt to use drawio's `selectPage` postMessage was a no-op because drawio's JSON embed protocol does not expose that action — silently exporting whatever tab happened to be active. The load-export-restore approach trades a brief visible tab-flicker for correctness: the exported image is guaranteed to match the requested page. * Tool description strings reflect the multi-page semantics so the LLM client learns the new contract. * Package version bumped 0.2.0 → 0.3.0 (additive surface — four new tools, three extended input schemas, canonical XML shape change). * CI: `.github/workflows/test.yml` gains an explicit install + vitest run for the mcp-server package so the new multi-page invariants are covered by automation, not just local runs. Backward compatibility: every existing single-page caller continues to work without modification. The session.xml shape is normalised on every write, removing the wrapper-injection hack from the .drawio download path. Tests: 43 unit tests under `packages/mcp-server/tests/multi-page.test.ts` pin the validator's mxfile path, the page-scoped operations, the XML declaration-prefix handling for both normalizeToMxfile and addPageToDoc, addPageToDoc's rejection of full <mxfile> inputs, the single-page projection used by export_diagram (a direct regression test for the selectPage bug — two distinct page selectors must produce visually different projections), and the Transformer + CNN motivating scenario. A `tests/smoke.mjs` smoke test drives the built `dist/index.js` over JSON-RPC and asserts all 9 tools register with the right input schemas. Root vitest suite (107 tests) still green. * fix(mcp): rewrite page-targeted export browser-side; harden edit/get The page-targeted PNG/SVG export never worked: export_diagram swapped the live session to a single-page projection, slept 3s, then wrote the export flag onto a state object that setState() had already replaced in the store Map — so the browser never saw the request and every such export timed out. The swap+restore also clobbered concurrent edits. Move the projection entirely browser-side: requestExport() hands a single -page <mxfile> to the bridge via state.exportXml; the bridge loads it, lets draw.io render, exports, then reloads the user's real document. The canonical session state is never mutated, so there is no restore race and no fixed-delay guessing. The export poll now re-reads the live store entry each tick instead of a captured reference. autosave is suppressed and the version-bump reload is skipped while a projection is on screen; if no real document was captured, restore forces a server reload rather than leaving the iframe stuck on the projection. Also: - edit_diagram now returns isError on a page-level failure (selector matched no page / page has no <root>) instead of reporting success-with-warnings and persisting a no-op; the pre-edit history snapshot is taken only after that gate so a failed edit leaves no phantom undo entry. - edit_diagram/get_diagram re-normalise browser-pushed xml to mxfile so a bare <mxGraphModel> can't silently strip a multi-page document. - get_diagram now errors (instead of silently returning the full doc) when a selector is given but the session isn't a parseable mxfile. - page_id / page_name / add_page.id get .min(1) so empty strings can't silently target the first page. - Extract pages.ts:projectPage(), collapsing three copies of the parse→find→serialise projection logic in index.ts. - Replace the never-in-CI tests/smoke.mjs with tests/server-wiring.test.ts, which boots the server from source via tsx and runs under the existing vitest CI step. * chore(mcp): set version to 0.2.1 for release --------- Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2026-06-16 05:45:50 +05:30
description: `Create a NEW diagram from XML. ONLY use this when creating a diagram from scratch.
feat(mcp): add multi-page (mxfile) support to MCP server (#862) * feat(mcp): add multi-page (mxfile) support The MCP server's write path could only address a single drawio page even though the underlying .drawio file format and the embedded editor both natively support multi-page documents. A user asking for "a second page with a CNN diagram" would hit the validator with the error "Expected closing tag </root> but found </mxCell>" because the validator assumed input was a bare <mxGraphModel> and could not walk past the <mxfile><diagram>...</diagram></mxfile> wrapper. This patch closes the gap end to end: * New helper module `pages.ts` centralises page CRUD (normalize, parse, list, find, add, rename, delete) so every layer agrees that the canonical in-memory shape is always <mxfile>. normalizeToMxfile and addPageToDoc both strip any leading <?xml ?> declaration before embedding a fragment inside <diagram> (the declaration is only valid at document start). addPageToDoc explicitly rejects full <mxfile> inputs so a caller cannot accidentally nest a document inside a page. * `xml-validation.ts` now detects an <mxfile> root and scopes the duplicate-id check per <diagram>. The legacy regex check would otherwise reject every multi-page doc, because cells "0" and "1" repeat in each page's <root> by design. The DOM-parse path is gated by a cheap regex pre-check so legacy bare <mxGraphModel> callers don't pay any extra cost. The autoFix duplicate-id rename step is also guarded against mxfile inputs — renaming those sentinel cells would silently break drawio's parent references. * `diagram-operations.ts` accepts an optional PageSelector. For <mxfile> input it resolves the page first and scopes all querySelectorAll calls to that page's <root>, so a delete on page 2's cell "2" no longer touches page 1's cell "2". * `create_new_diagram` accepts either a bare <mxGraphModel> (legacy, auto-wrapped into a single-page mxfile) or a full <mxfile> with N diagrams. All existing single-page callers keep working unchanged. * `edit_diagram`, `get_diagram`, and `export_diagram` gain optional `page_id` / `page_name` / `page_index` parameters. When omitted they target the first page — the "active by convention" default. Tool handlers with all-optional input schemas coalesce missing arguments via `input ?? {}` so a no-args MCP invocation can't crash on destructure before reaching the session-existence check. * New tools: `list_pages`, `add_page`, `rename_page`, `delete_page`. * Page-targeted PNG/SVG export uses a "load + export + restore" dance: the server projects the target page into a single-page <mxfile>, pushes it into the transient state so the browser reloads the iframe with just that page, waits for drawio to render (~3s), triggers the export, captures the data, and then restores the original multi-page document. The dance is wrapped in `try/finally` so the restore runs unconditionally — even if an exception is thrown mid-dance, the user's multi-tab view is recovered before the function returns. The earlier attempt to use drawio's `selectPage` postMessage was a no-op because drawio's JSON embed protocol does not expose that action — silently exporting whatever tab happened to be active. The load-export-restore approach trades a brief visible tab-flicker for correctness: the exported image is guaranteed to match the requested page. * Tool description strings reflect the multi-page semantics so the LLM client learns the new contract. * Package version bumped 0.2.0 → 0.3.0 (additive surface — four new tools, three extended input schemas, canonical XML shape change). * CI: `.github/workflows/test.yml` gains an explicit install + vitest run for the mcp-server package so the new multi-page invariants are covered by automation, not just local runs. Backward compatibility: every existing single-page caller continues to work without modification. The session.xml shape is normalised on every write, removing the wrapper-injection hack from the .drawio download path. Tests: 43 unit tests under `packages/mcp-server/tests/multi-page.test.ts` pin the validator's mxfile path, the page-scoped operations, the XML declaration-prefix handling for both normalizeToMxfile and addPageToDoc, addPageToDoc's rejection of full <mxfile> inputs, the single-page projection used by export_diagram (a direct regression test for the selectPage bug — two distinct page selectors must produce visually different projections), and the Transformer + CNN motivating scenario. A `tests/smoke.mjs` smoke test drives the built `dist/index.js` over JSON-RPC and asserts all 9 tools register with the right input schemas. Root vitest suite (107 tests) still green. * fix(mcp): rewrite page-targeted export browser-side; harden edit/get The page-targeted PNG/SVG export never worked: export_diagram swapped the live session to a single-page projection, slept 3s, then wrote the export flag onto a state object that setState() had already replaced in the store Map — so the browser never saw the request and every such export timed out. The swap+restore also clobbered concurrent edits. Move the projection entirely browser-side: requestExport() hands a single -page <mxfile> to the bridge via state.exportXml; the bridge loads it, lets draw.io render, exports, then reloads the user's real document. The canonical session state is never mutated, so there is no restore race and no fixed-delay guessing. The export poll now re-reads the live store entry each tick instead of a captured reference. autosave is suppressed and the version-bump reload is skipped while a projection is on screen; if no real document was captured, restore forces a server reload rather than leaving the iframe stuck on the projection. Also: - edit_diagram now returns isError on a page-level failure (selector matched no page / page has no <root>) instead of reporting success-with-warnings and persisting a no-op; the pre-edit history snapshot is taken only after that gate so a failed edit leaves no phantom undo entry. - edit_diagram/get_diagram re-normalise browser-pushed xml to mxfile so a bare <mxGraphModel> can't silently strip a multi-page document. - get_diagram now errors (instead of silently returning the full doc) when a selector is given but the session isn't a parseable mxfile. - page_id / page_name / add_page.id get .min(1) so empty strings can't silently target the first page. - Extract pages.ts:projectPage(), collapsing three copies of the parse→find→serialise projection logic in index.ts. - Replace the never-in-CI tests/smoke.mjs with tests/server-wiring.test.ts, which boots the server from source via tsx and runs under the existing vitest CI step. * chore(mcp): set version to 0.2.1 for release --------- Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2026-06-16 05:45:50 +05:30
DESTRUCTIVE: This tool REPLACES the entire document, INCLUDING every existing page/tab and any unsaved user changes. To add a tab without losing existing content, use add_page instead. To modify cells on an existing page, use edit_diagram.
CRITICAL: You MUST provide the 'xml' argument in EVERY call. Do NOT call this tool without xml.
When to use this tool:
feat(mcp): add multi-page (mxfile) support to MCP server (#862) * feat(mcp): add multi-page (mxfile) support The MCP server's write path could only address a single drawio page even though the underlying .drawio file format and the embedded editor both natively support multi-page documents. A user asking for "a second page with a CNN diagram" would hit the validator with the error "Expected closing tag </root> but found </mxCell>" because the validator assumed input was a bare <mxGraphModel> and could not walk past the <mxfile><diagram>...</diagram></mxfile> wrapper. This patch closes the gap end to end: * New helper module `pages.ts` centralises page CRUD (normalize, parse, list, find, add, rename, delete) so every layer agrees that the canonical in-memory shape is always <mxfile>. normalizeToMxfile and addPageToDoc both strip any leading <?xml ?> declaration before embedding a fragment inside <diagram> (the declaration is only valid at document start). addPageToDoc explicitly rejects full <mxfile> inputs so a caller cannot accidentally nest a document inside a page. * `xml-validation.ts` now detects an <mxfile> root and scopes the duplicate-id check per <diagram>. The legacy regex check would otherwise reject every multi-page doc, because cells "0" and "1" repeat in each page's <root> by design. The DOM-parse path is gated by a cheap regex pre-check so legacy bare <mxGraphModel> callers don't pay any extra cost. The autoFix duplicate-id rename step is also guarded against mxfile inputs — renaming those sentinel cells would silently break drawio's parent references. * `diagram-operations.ts` accepts an optional PageSelector. For <mxfile> input it resolves the page first and scopes all querySelectorAll calls to that page's <root>, so a delete on page 2's cell "2" no longer touches page 1's cell "2". * `create_new_diagram` accepts either a bare <mxGraphModel> (legacy, auto-wrapped into a single-page mxfile) or a full <mxfile> with N diagrams. All existing single-page callers keep working unchanged. * `edit_diagram`, `get_diagram`, and `export_diagram` gain optional `page_id` / `page_name` / `page_index` parameters. When omitted they target the first page — the "active by convention" default. Tool handlers with all-optional input schemas coalesce missing arguments via `input ?? {}` so a no-args MCP invocation can't crash on destructure before reaching the session-existence check. * New tools: `list_pages`, `add_page`, `rename_page`, `delete_page`. * Page-targeted PNG/SVG export uses a "load + export + restore" dance: the server projects the target page into a single-page <mxfile>, pushes it into the transient state so the browser reloads the iframe with just that page, waits for drawio to render (~3s), triggers the export, captures the data, and then restores the original multi-page document. The dance is wrapped in `try/finally` so the restore runs unconditionally — even if an exception is thrown mid-dance, the user's multi-tab view is recovered before the function returns. The earlier attempt to use drawio's `selectPage` postMessage was a no-op because drawio's JSON embed protocol does not expose that action — silently exporting whatever tab happened to be active. The load-export-restore approach trades a brief visible tab-flicker for correctness: the exported image is guaranteed to match the requested page. * Tool description strings reflect the multi-page semantics so the LLM client learns the new contract. * Package version bumped 0.2.0 → 0.3.0 (additive surface — four new tools, three extended input schemas, canonical XML shape change). * CI: `.github/workflows/test.yml` gains an explicit install + vitest run for the mcp-server package so the new multi-page invariants are covered by automation, not just local runs. Backward compatibility: every existing single-page caller continues to work without modification. The session.xml shape is normalised on every write, removing the wrapper-injection hack from the .drawio download path. Tests: 43 unit tests under `packages/mcp-server/tests/multi-page.test.ts` pin the validator's mxfile path, the page-scoped operations, the XML declaration-prefix handling for both normalizeToMxfile and addPageToDoc, addPageToDoc's rejection of full <mxfile> inputs, the single-page projection used by export_diagram (a direct regression test for the selectPage bug — two distinct page selectors must produce visually different projections), and the Transformer + CNN motivating scenario. A `tests/smoke.mjs` smoke test drives the built `dist/index.js` over JSON-RPC and asserts all 9 tools register with the right input schemas. Root vitest suite (107 tests) still green. * fix(mcp): rewrite page-targeted export browser-side; harden edit/get The page-targeted PNG/SVG export never worked: export_diagram swapped the live session to a single-page projection, slept 3s, then wrote the export flag onto a state object that setState() had already replaced in the store Map — so the browser never saw the request and every such export timed out. The swap+restore also clobbered concurrent edits. Move the projection entirely browser-side: requestExport() hands a single -page <mxfile> to the bridge via state.exportXml; the bridge loads it, lets draw.io render, exports, then reloads the user's real document. The canonical session state is never mutated, so there is no restore race and no fixed-delay guessing. The export poll now re-reads the live store entry each tick instead of a captured reference. autosave is suppressed and the version-bump reload is skipped while a projection is on screen; if no real document was captured, restore forces a server reload rather than leaving the iframe stuck on the projection. Also: - edit_diagram now returns isError on a page-level failure (selector matched no page / page has no <root>) instead of reporting success-with-warnings and persisting a no-op; the pre-edit history snapshot is taken only after that gate so a failed edit leaves no phantom undo entry. - edit_diagram/get_diagram re-normalise browser-pushed xml to mxfile so a bare <mxGraphModel> can't silently strip a multi-page document. - get_diagram now errors (instead of silently returning the full doc) when a selector is given but the session isn't a parseable mxfile. - page_id / page_name / add_page.id get .min(1) so empty strings can't silently target the first page. - Extract pages.ts:projectPage(), collapsing three copies of the parse→find→serialise projection logic in index.ts. - Replace the never-in-CI tests/smoke.mjs with tests/server-wiring.test.ts, which boots the server from source via tsx and runs under the existing vitest CI step. * chore(mcp): set version to 0.2.1 for release --------- Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2026-06-16 05:45:50 +05:30
- Creating a new diagram from scratch (no existing diagram, or wanting to wipe and start over)
- The user explicitly asks to "start over" or "create a new diagram"
feat(mcp): add multi-page (mxfile) support to MCP server (#862) * feat(mcp): add multi-page (mxfile) support The MCP server's write path could only address a single drawio page even though the underlying .drawio file format and the embedded editor both natively support multi-page documents. A user asking for "a second page with a CNN diagram" would hit the validator with the error "Expected closing tag </root> but found </mxCell>" because the validator assumed input was a bare <mxGraphModel> and could not walk past the <mxfile><diagram>...</diagram></mxfile> wrapper. This patch closes the gap end to end: * New helper module `pages.ts` centralises page CRUD (normalize, parse, list, find, add, rename, delete) so every layer agrees that the canonical in-memory shape is always <mxfile>. normalizeToMxfile and addPageToDoc both strip any leading <?xml ?> declaration before embedding a fragment inside <diagram> (the declaration is only valid at document start). addPageToDoc explicitly rejects full <mxfile> inputs so a caller cannot accidentally nest a document inside a page. * `xml-validation.ts` now detects an <mxfile> root and scopes the duplicate-id check per <diagram>. The legacy regex check would otherwise reject every multi-page doc, because cells "0" and "1" repeat in each page's <root> by design. The DOM-parse path is gated by a cheap regex pre-check so legacy bare <mxGraphModel> callers don't pay any extra cost. The autoFix duplicate-id rename step is also guarded against mxfile inputs — renaming those sentinel cells would silently break drawio's parent references. * `diagram-operations.ts` accepts an optional PageSelector. For <mxfile> input it resolves the page first and scopes all querySelectorAll calls to that page's <root>, so a delete on page 2's cell "2" no longer touches page 1's cell "2". * `create_new_diagram` accepts either a bare <mxGraphModel> (legacy, auto-wrapped into a single-page mxfile) or a full <mxfile> with N diagrams. All existing single-page callers keep working unchanged. * `edit_diagram`, `get_diagram`, and `export_diagram` gain optional `page_id` / `page_name` / `page_index` parameters. When omitted they target the first page — the "active by convention" default. Tool handlers with all-optional input schemas coalesce missing arguments via `input ?? {}` so a no-args MCP invocation can't crash on destructure before reaching the session-existence check. * New tools: `list_pages`, `add_page`, `rename_page`, `delete_page`. * Page-targeted PNG/SVG export uses a "load + export + restore" dance: the server projects the target page into a single-page <mxfile>, pushes it into the transient state so the browser reloads the iframe with just that page, waits for drawio to render (~3s), triggers the export, captures the data, and then restores the original multi-page document. The dance is wrapped in `try/finally` so the restore runs unconditionally — even if an exception is thrown mid-dance, the user's multi-tab view is recovered before the function returns. The earlier attempt to use drawio's `selectPage` postMessage was a no-op because drawio's JSON embed protocol does not expose that action — silently exporting whatever tab happened to be active. The load-export-restore approach trades a brief visible tab-flicker for correctness: the exported image is guaranteed to match the requested page. * Tool description strings reflect the multi-page semantics so the LLM client learns the new contract. * Package version bumped 0.2.0 → 0.3.0 (additive surface — four new tools, three extended input schemas, canonical XML shape change). * CI: `.github/workflows/test.yml` gains an explicit install + vitest run for the mcp-server package so the new multi-page invariants are covered by automation, not just local runs. Backward compatibility: every existing single-page caller continues to work without modification. The session.xml shape is normalised on every write, removing the wrapper-injection hack from the .drawio download path. Tests: 43 unit tests under `packages/mcp-server/tests/multi-page.test.ts` pin the validator's mxfile path, the page-scoped operations, the XML declaration-prefix handling for both normalizeToMxfile and addPageToDoc, addPageToDoc's rejection of full <mxfile> inputs, the single-page projection used by export_diagram (a direct regression test for the selectPage bug — two distinct page selectors must produce visually different projections), and the Transformer + CNN motivating scenario. A `tests/smoke.mjs` smoke test drives the built `dist/index.js` over JSON-RPC and asserts all 9 tools register with the right input schemas. Root vitest suite (107 tests) still green. * fix(mcp): rewrite page-targeted export browser-side; harden edit/get The page-targeted PNG/SVG export never worked: export_diagram swapped the live session to a single-page projection, slept 3s, then wrote the export flag onto a state object that setState() had already replaced in the store Map — so the browser never saw the request and every such export timed out. The swap+restore also clobbered concurrent edits. Move the projection entirely browser-side: requestExport() hands a single -page <mxfile> to the bridge via state.exportXml; the bridge loads it, lets draw.io render, exports, then reloads the user's real document. The canonical session state is never mutated, so there is no restore race and no fixed-delay guessing. The export poll now re-reads the live store entry each tick instead of a captured reference. autosave is suppressed and the version-bump reload is skipped while a projection is on screen; if no real document was captured, restore forces a server reload rather than leaving the iframe stuck on the projection. Also: - edit_diagram now returns isError on a page-level failure (selector matched no page / page has no <root>) instead of reporting success-with-warnings and persisting a no-op; the pre-edit history snapshot is taken only after that gate so a failed edit leaves no phantom undo entry. - edit_diagram/get_diagram re-normalise browser-pushed xml to mxfile so a bare <mxGraphModel> can't silently strip a multi-page document. - get_diagram now errors (instead of silently returning the full doc) when a selector is given but the session isn't a parseable mxfile. - page_id / page_name / add_page.id get .min(1) so empty strings can't silently target the first page. - Extract pages.ts:projectPage(), collapsing three copies of the parse→find→serialise projection logic in index.ts. - Replace the never-in-CI tests/smoke.mjs with tests/server-wiring.test.ts, which boots the server from source via tsx and runs under the existing vitest CI step. * chore(mcp): set version to 0.2.1 for release --------- Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2026-06-16 05:45:50 +05:30
When to use add_page instead:
- The user wants ANOTHER tab/page alongside what's already there (e.g. "add a CNN diagram on a new page")
When to use edit_diagram instead:
- ANY modifications to an existing page's cells (add/remove/move shapes, change labels, etc.)
ACCEPTED XML SHAPES:
feat(mcp): add multi-page (mxfile) support to MCP server (#862) * feat(mcp): add multi-page (mxfile) support The MCP server's write path could only address a single drawio page even though the underlying .drawio file format and the embedded editor both natively support multi-page documents. A user asking for "a second page with a CNN diagram" would hit the validator with the error "Expected closing tag </root> but found </mxCell>" because the validator assumed input was a bare <mxGraphModel> and could not walk past the <mxfile><diagram>...</diagram></mxfile> wrapper. This patch closes the gap end to end: * New helper module `pages.ts` centralises page CRUD (normalize, parse, list, find, add, rename, delete) so every layer agrees that the canonical in-memory shape is always <mxfile>. normalizeToMxfile and addPageToDoc both strip any leading <?xml ?> declaration before embedding a fragment inside <diagram> (the declaration is only valid at document start). addPageToDoc explicitly rejects full <mxfile> inputs so a caller cannot accidentally nest a document inside a page. * `xml-validation.ts` now detects an <mxfile> root and scopes the duplicate-id check per <diagram>. The legacy regex check would otherwise reject every multi-page doc, because cells "0" and "1" repeat in each page's <root> by design. The DOM-parse path is gated by a cheap regex pre-check so legacy bare <mxGraphModel> callers don't pay any extra cost. The autoFix duplicate-id rename step is also guarded against mxfile inputs — renaming those sentinel cells would silently break drawio's parent references. * `diagram-operations.ts` accepts an optional PageSelector. For <mxfile> input it resolves the page first and scopes all querySelectorAll calls to that page's <root>, so a delete on page 2's cell "2" no longer touches page 1's cell "2". * `create_new_diagram` accepts either a bare <mxGraphModel> (legacy, auto-wrapped into a single-page mxfile) or a full <mxfile> with N diagrams. All existing single-page callers keep working unchanged. * `edit_diagram`, `get_diagram`, and `export_diagram` gain optional `page_id` / `page_name` / `page_index` parameters. When omitted they target the first page — the "active by convention" default. Tool handlers with all-optional input schemas coalesce missing arguments via `input ?? {}` so a no-args MCP invocation can't crash on destructure before reaching the session-existence check. * New tools: `list_pages`, `add_page`, `rename_page`, `delete_page`. * Page-targeted PNG/SVG export uses a "load + export + restore" dance: the server projects the target page into a single-page <mxfile>, pushes it into the transient state so the browser reloads the iframe with just that page, waits for drawio to render (~3s), triggers the export, captures the data, and then restores the original multi-page document. The dance is wrapped in `try/finally` so the restore runs unconditionally — even if an exception is thrown mid-dance, the user's multi-tab view is recovered before the function returns. The earlier attempt to use drawio's `selectPage` postMessage was a no-op because drawio's JSON embed protocol does not expose that action — silently exporting whatever tab happened to be active. The load-export-restore approach trades a brief visible tab-flicker for correctness: the exported image is guaranteed to match the requested page. * Tool description strings reflect the multi-page semantics so the LLM client learns the new contract. * Package version bumped 0.2.0 → 0.3.0 (additive surface — four new tools, three extended input schemas, canonical XML shape change). * CI: `.github/workflows/test.yml` gains an explicit install + vitest run for the mcp-server package so the new multi-page invariants are covered by automation, not just local runs. Backward compatibility: every existing single-page caller continues to work without modification. The session.xml shape is normalised on every write, removing the wrapper-injection hack from the .drawio download path. Tests: 43 unit tests under `packages/mcp-server/tests/multi-page.test.ts` pin the validator's mxfile path, the page-scoped operations, the XML declaration-prefix handling for both normalizeToMxfile and addPageToDoc, addPageToDoc's rejection of full <mxfile> inputs, the single-page projection used by export_diagram (a direct regression test for the selectPage bug — two distinct page selectors must produce visually different projections), and the Transformer + CNN motivating scenario. A `tests/smoke.mjs` smoke test drives the built `dist/index.js` over JSON-RPC and asserts all 9 tools register with the right input schemas. Root vitest suite (107 tests) still green. * fix(mcp): rewrite page-targeted export browser-side; harden edit/get The page-targeted PNG/SVG export never worked: export_diagram swapped the live session to a single-page projection, slept 3s, then wrote the export flag onto a state object that setState() had already replaced in the store Map — so the browser never saw the request and every such export timed out. The swap+restore also clobbered concurrent edits. Move the projection entirely browser-side: requestExport() hands a single -page <mxfile> to the bridge via state.exportXml; the bridge loads it, lets draw.io render, exports, then reloads the user's real document. The canonical session state is never mutated, so there is no restore race and no fixed-delay guessing. The export poll now re-reads the live store entry each tick instead of a captured reference. autosave is suppressed and the version-bump reload is skipped while a projection is on screen; if no real document was captured, restore forces a server reload rather than leaving the iframe stuck on the projection. Also: - edit_diagram now returns isError on a page-level failure (selector matched no page / page has no <root>) instead of reporting success-with-warnings and persisting a no-op; the pre-edit history snapshot is taken only after that gate so a failed edit leaves no phantom undo entry. - edit_diagram/get_diagram re-normalise browser-pushed xml to mxfile so a bare <mxGraphModel> can't silently strip a multi-page document. - get_diagram now errors (instead of silently returning the full doc) when a selector is given but the session isn't a parseable mxfile. - page_id / page_name / add_page.id get .min(1) so empty strings can't silently target the first page. - Extract pages.ts:projectPage(), collapsing three copies of the parse→find→serialise projection logic in index.ts. - Replace the never-in-CI tests/smoke.mjs with tests/server-wiring.test.ts, which boots the server from source via tsx and runs under the existing vitest CI step. * chore(mcp): set version to 0.2.1 for release --------- Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2026-06-16 05:45:50 +05:30
1) Bare mxGraphModel (single-page, legacy):
<mxGraphModel>
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="2" value="Shape" style="rounded=1;" vertex="1" parent="1">
<mxGeometry x="100" y="100" width="120" height="60" as="geometry"/>
</mxCell>
</root>
</mxGraphModel>
feat(mcp): add multi-page (mxfile) support to MCP server (#862) * feat(mcp): add multi-page (mxfile) support The MCP server's write path could only address a single drawio page even though the underlying .drawio file format and the embedded editor both natively support multi-page documents. A user asking for "a second page with a CNN diagram" would hit the validator with the error "Expected closing tag </root> but found </mxCell>" because the validator assumed input was a bare <mxGraphModel> and could not walk past the <mxfile><diagram>...</diagram></mxfile> wrapper. This patch closes the gap end to end: * New helper module `pages.ts` centralises page CRUD (normalize, parse, list, find, add, rename, delete) so every layer agrees that the canonical in-memory shape is always <mxfile>. normalizeToMxfile and addPageToDoc both strip any leading <?xml ?> declaration before embedding a fragment inside <diagram> (the declaration is only valid at document start). addPageToDoc explicitly rejects full <mxfile> inputs so a caller cannot accidentally nest a document inside a page. * `xml-validation.ts` now detects an <mxfile> root and scopes the duplicate-id check per <diagram>. The legacy regex check would otherwise reject every multi-page doc, because cells "0" and "1" repeat in each page's <root> by design. The DOM-parse path is gated by a cheap regex pre-check so legacy bare <mxGraphModel> callers don't pay any extra cost. The autoFix duplicate-id rename step is also guarded against mxfile inputs — renaming those sentinel cells would silently break drawio's parent references. * `diagram-operations.ts` accepts an optional PageSelector. For <mxfile> input it resolves the page first and scopes all querySelectorAll calls to that page's <root>, so a delete on page 2's cell "2" no longer touches page 1's cell "2". * `create_new_diagram` accepts either a bare <mxGraphModel> (legacy, auto-wrapped into a single-page mxfile) or a full <mxfile> with N diagrams. All existing single-page callers keep working unchanged. * `edit_diagram`, `get_diagram`, and `export_diagram` gain optional `page_id` / `page_name` / `page_index` parameters. When omitted they target the first page — the "active by convention" default. Tool handlers with all-optional input schemas coalesce missing arguments via `input ?? {}` so a no-args MCP invocation can't crash on destructure before reaching the session-existence check. * New tools: `list_pages`, `add_page`, `rename_page`, `delete_page`. * Page-targeted PNG/SVG export uses a "load + export + restore" dance: the server projects the target page into a single-page <mxfile>, pushes it into the transient state so the browser reloads the iframe with just that page, waits for drawio to render (~3s), triggers the export, captures the data, and then restores the original multi-page document. The dance is wrapped in `try/finally` so the restore runs unconditionally — even if an exception is thrown mid-dance, the user's multi-tab view is recovered before the function returns. The earlier attempt to use drawio's `selectPage` postMessage was a no-op because drawio's JSON embed protocol does not expose that action — silently exporting whatever tab happened to be active. The load-export-restore approach trades a brief visible tab-flicker for correctness: the exported image is guaranteed to match the requested page. * Tool description strings reflect the multi-page semantics so the LLM client learns the new contract. * Package version bumped 0.2.0 → 0.3.0 (additive surface — four new tools, three extended input schemas, canonical XML shape change). * CI: `.github/workflows/test.yml` gains an explicit install + vitest run for the mcp-server package so the new multi-page invariants are covered by automation, not just local runs. Backward compatibility: every existing single-page caller continues to work without modification. The session.xml shape is normalised on every write, removing the wrapper-injection hack from the .drawio download path. Tests: 43 unit tests under `packages/mcp-server/tests/multi-page.test.ts` pin the validator's mxfile path, the page-scoped operations, the XML declaration-prefix handling for both normalizeToMxfile and addPageToDoc, addPageToDoc's rejection of full <mxfile> inputs, the single-page projection used by export_diagram (a direct regression test for the selectPage bug — two distinct page selectors must produce visually different projections), and the Transformer + CNN motivating scenario. A `tests/smoke.mjs` smoke test drives the built `dist/index.js` over JSON-RPC and asserts all 9 tools register with the right input schemas. Root vitest suite (107 tests) still green. * fix(mcp): rewrite page-targeted export browser-side; harden edit/get The page-targeted PNG/SVG export never worked: export_diagram swapped the live session to a single-page projection, slept 3s, then wrote the export flag onto a state object that setState() had already replaced in the store Map — so the browser never saw the request and every such export timed out. The swap+restore also clobbered concurrent edits. Move the projection entirely browser-side: requestExport() hands a single -page <mxfile> to the bridge via state.exportXml; the bridge loads it, lets draw.io render, exports, then reloads the user's real document. The canonical session state is never mutated, so there is no restore race and no fixed-delay guessing. The export poll now re-reads the live store entry each tick instead of a captured reference. autosave is suppressed and the version-bump reload is skipped while a projection is on screen; if no real document was captured, restore forces a server reload rather than leaving the iframe stuck on the projection. Also: - edit_diagram now returns isError on a page-level failure (selector matched no page / page has no <root>) instead of reporting success-with-warnings and persisting a no-op; the pre-edit history snapshot is taken only after that gate so a failed edit leaves no phantom undo entry. - edit_diagram/get_diagram re-normalise browser-pushed xml to mxfile so a bare <mxGraphModel> can't silently strip a multi-page document. - get_diagram now errors (instead of silently returning the full doc) when a selector is given but the session isn't a parseable mxfile. - page_id / page_name / add_page.id get .min(1) so empty strings can't silently target the first page. - Extract pages.ts:projectPage(), collapsing three copies of the parse→find→serialise projection logic in index.ts. - Replace the never-in-CI tests/smoke.mjs with tests/server-wiring.test.ts, which boots the server from source via tsx and runs under the existing vitest CI step. * chore(mcp): set version to 0.2.1 for release --------- Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2026-06-16 05:45:50 +05:30
The server auto-wraps this in <mxfile><diagram id="..." name="Page-1">...</diagram></mxfile>.
2) Full mxfile (one or more pages):
<mxfile host="app.diagrams.net">
<diagram id="page-1" name="Architecture">
<mxGraphModel><root>...</root></mxGraphModel>
</diagram>
<diagram id="page-2" name="Sequence">
<mxGraphModel><root>...</root></mxGraphModel>
</diagram>
</mxfile>
Each <diagram> becomes a tab in the embedded editor. Cell ids "0" and "1" are reserved root sentinels and MUST repeat in every page's <root>.
LAYOUT CONSTRAINTS (per page):
- Keep all elements within x=0-800, y=0-600 (single page viewport)
- Start from margins (x=40, y=40), keep elements grouped closely
feat(mcp): add multi-page (mxfile) support to MCP server (#862) * feat(mcp): add multi-page (mxfile) support The MCP server's write path could only address a single drawio page even though the underlying .drawio file format and the embedded editor both natively support multi-page documents. A user asking for "a second page with a CNN diagram" would hit the validator with the error "Expected closing tag </root> but found </mxCell>" because the validator assumed input was a bare <mxGraphModel> and could not walk past the <mxfile><diagram>...</diagram></mxfile> wrapper. This patch closes the gap end to end: * New helper module `pages.ts` centralises page CRUD (normalize, parse, list, find, add, rename, delete) so every layer agrees that the canonical in-memory shape is always <mxfile>. normalizeToMxfile and addPageToDoc both strip any leading <?xml ?> declaration before embedding a fragment inside <diagram> (the declaration is only valid at document start). addPageToDoc explicitly rejects full <mxfile> inputs so a caller cannot accidentally nest a document inside a page. * `xml-validation.ts` now detects an <mxfile> root and scopes the duplicate-id check per <diagram>. The legacy regex check would otherwise reject every multi-page doc, because cells "0" and "1" repeat in each page's <root> by design. The DOM-parse path is gated by a cheap regex pre-check so legacy bare <mxGraphModel> callers don't pay any extra cost. The autoFix duplicate-id rename step is also guarded against mxfile inputs — renaming those sentinel cells would silently break drawio's parent references. * `diagram-operations.ts` accepts an optional PageSelector. For <mxfile> input it resolves the page first and scopes all querySelectorAll calls to that page's <root>, so a delete on page 2's cell "2" no longer touches page 1's cell "2". * `create_new_diagram` accepts either a bare <mxGraphModel> (legacy, auto-wrapped into a single-page mxfile) or a full <mxfile> with N diagrams. All existing single-page callers keep working unchanged. * `edit_diagram`, `get_diagram`, and `export_diagram` gain optional `page_id` / `page_name` / `page_index` parameters. When omitted they target the first page — the "active by convention" default. Tool handlers with all-optional input schemas coalesce missing arguments via `input ?? {}` so a no-args MCP invocation can't crash on destructure before reaching the session-existence check. * New tools: `list_pages`, `add_page`, `rename_page`, `delete_page`. * Page-targeted PNG/SVG export uses a "load + export + restore" dance: the server projects the target page into a single-page <mxfile>, pushes it into the transient state so the browser reloads the iframe with just that page, waits for drawio to render (~3s), triggers the export, captures the data, and then restores the original multi-page document. The dance is wrapped in `try/finally` so the restore runs unconditionally — even if an exception is thrown mid-dance, the user's multi-tab view is recovered before the function returns. The earlier attempt to use drawio's `selectPage` postMessage was a no-op because drawio's JSON embed protocol does not expose that action — silently exporting whatever tab happened to be active. The load-export-restore approach trades a brief visible tab-flicker for correctness: the exported image is guaranteed to match the requested page. * Tool description strings reflect the multi-page semantics so the LLM client learns the new contract. * Package version bumped 0.2.0 → 0.3.0 (additive surface — four new tools, three extended input schemas, canonical XML shape change). * CI: `.github/workflows/test.yml` gains an explicit install + vitest run for the mcp-server package so the new multi-page invariants are covered by automation, not just local runs. Backward compatibility: every existing single-page caller continues to work without modification. The session.xml shape is normalised on every write, removing the wrapper-injection hack from the .drawio download path. Tests: 43 unit tests under `packages/mcp-server/tests/multi-page.test.ts` pin the validator's mxfile path, the page-scoped operations, the XML declaration-prefix handling for both normalizeToMxfile and addPageToDoc, addPageToDoc's rejection of full <mxfile> inputs, the single-page projection used by export_diagram (a direct regression test for the selectPage bug — two distinct page selectors must produce visually different projections), and the Transformer + CNN motivating scenario. A `tests/smoke.mjs` smoke test drives the built `dist/index.js` over JSON-RPC and asserts all 9 tools register with the right input schemas. Root vitest suite (107 tests) still green. * fix(mcp): rewrite page-targeted export browser-side; harden edit/get The page-targeted PNG/SVG export never worked: export_diagram swapped the live session to a single-page projection, slept 3s, then wrote the export flag onto a state object that setState() had already replaced in the store Map — so the browser never saw the request and every such export timed out. The swap+restore also clobbered concurrent edits. Move the projection entirely browser-side: requestExport() hands a single -page <mxfile> to the bridge via state.exportXml; the bridge loads it, lets draw.io render, exports, then reloads the user's real document. The canonical session state is never mutated, so there is no restore race and no fixed-delay guessing. The export poll now re-reads the live store entry each tick instead of a captured reference. autosave is suppressed and the version-bump reload is skipped while a projection is on screen; if no real document was captured, restore forces a server reload rather than leaving the iframe stuck on the projection. Also: - edit_diagram now returns isError on a page-level failure (selector matched no page / page has no <root>) instead of reporting success-with-warnings and persisting a no-op; the pre-edit history snapshot is taken only after that gate so a failed edit leaves no phantom undo entry. - edit_diagram/get_diagram re-normalise browser-pushed xml to mxfile so a bare <mxGraphModel> can't silently strip a multi-page document. - get_diagram now errors (instead of silently returning the full doc) when a selector is given but the session isn't a parseable mxfile. - page_id / page_name / add_page.id get .min(1) so empty strings can't silently target the first page. - Extract pages.ts:projectPage(), collapsing three copies of the parse→find→serialise projection logic in index.ts. - Replace the never-in-CI tests/smoke.mjs with tests/server-wiring.test.ts, which boots the server from source via tsx and runs under the existing vitest CI step. * chore(mcp): set version to 0.2.1 for release --------- Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2026-06-16 05:45:50 +05:30
- Use unique IDs starting from "2" within each page (0 and 1 are reserved)
- Set parent="1" for top-level shapes
- Space shapes 150-200px apart for clear edge routing
EDGE ROUTING RULES:
- Never let multiple edges share the same path - use different exitY/entryY values
- For bidirectional connections (AB), use OPPOSITE sides
- Always specify exitX, exitY, entryX, entryY explicitly in edge style
- Route edges AROUND obstacles using waypoints (add 20-30px clearance)
- Use natural connection points based on flow (not corners)
COMMON STYLES:
- Shapes: rounded=1; fillColor=#hex; strokeColor=#hex
- Edges: endArrow=classic; edgeStyle=orthogonalEdgeStyle; curved=1
- Text: fontSize=14; fontStyle=1 (bold); align=center`,
inputSchema: {
xml: z
.string()
.describe(
feat(mcp): add multi-page (mxfile) support to MCP server (#862) * feat(mcp): add multi-page (mxfile) support The MCP server's write path could only address a single drawio page even though the underlying .drawio file format and the embedded editor both natively support multi-page documents. A user asking for "a second page with a CNN diagram" would hit the validator with the error "Expected closing tag </root> but found </mxCell>" because the validator assumed input was a bare <mxGraphModel> and could not walk past the <mxfile><diagram>...</diagram></mxfile> wrapper. This patch closes the gap end to end: * New helper module `pages.ts` centralises page CRUD (normalize, parse, list, find, add, rename, delete) so every layer agrees that the canonical in-memory shape is always <mxfile>. normalizeToMxfile and addPageToDoc both strip any leading <?xml ?> declaration before embedding a fragment inside <diagram> (the declaration is only valid at document start). addPageToDoc explicitly rejects full <mxfile> inputs so a caller cannot accidentally nest a document inside a page. * `xml-validation.ts` now detects an <mxfile> root and scopes the duplicate-id check per <diagram>. The legacy regex check would otherwise reject every multi-page doc, because cells "0" and "1" repeat in each page's <root> by design. The DOM-parse path is gated by a cheap regex pre-check so legacy bare <mxGraphModel> callers don't pay any extra cost. The autoFix duplicate-id rename step is also guarded against mxfile inputs — renaming those sentinel cells would silently break drawio's parent references. * `diagram-operations.ts` accepts an optional PageSelector. For <mxfile> input it resolves the page first and scopes all querySelectorAll calls to that page's <root>, so a delete on page 2's cell "2" no longer touches page 1's cell "2". * `create_new_diagram` accepts either a bare <mxGraphModel> (legacy, auto-wrapped into a single-page mxfile) or a full <mxfile> with N diagrams. All existing single-page callers keep working unchanged. * `edit_diagram`, `get_diagram`, and `export_diagram` gain optional `page_id` / `page_name` / `page_index` parameters. When omitted they target the first page — the "active by convention" default. Tool handlers with all-optional input schemas coalesce missing arguments via `input ?? {}` so a no-args MCP invocation can't crash on destructure before reaching the session-existence check. * New tools: `list_pages`, `add_page`, `rename_page`, `delete_page`. * Page-targeted PNG/SVG export uses a "load + export + restore" dance: the server projects the target page into a single-page <mxfile>, pushes it into the transient state so the browser reloads the iframe with just that page, waits for drawio to render (~3s), triggers the export, captures the data, and then restores the original multi-page document. The dance is wrapped in `try/finally` so the restore runs unconditionally — even if an exception is thrown mid-dance, the user's multi-tab view is recovered before the function returns. The earlier attempt to use drawio's `selectPage` postMessage was a no-op because drawio's JSON embed protocol does not expose that action — silently exporting whatever tab happened to be active. The load-export-restore approach trades a brief visible tab-flicker for correctness: the exported image is guaranteed to match the requested page. * Tool description strings reflect the multi-page semantics so the LLM client learns the new contract. * Package version bumped 0.2.0 → 0.3.0 (additive surface — four new tools, three extended input schemas, canonical XML shape change). * CI: `.github/workflows/test.yml` gains an explicit install + vitest run for the mcp-server package so the new multi-page invariants are covered by automation, not just local runs. Backward compatibility: every existing single-page caller continues to work without modification. The session.xml shape is normalised on every write, removing the wrapper-injection hack from the .drawio download path. Tests: 43 unit tests under `packages/mcp-server/tests/multi-page.test.ts` pin the validator's mxfile path, the page-scoped operations, the XML declaration-prefix handling for both normalizeToMxfile and addPageToDoc, addPageToDoc's rejection of full <mxfile> inputs, the single-page projection used by export_diagram (a direct regression test for the selectPage bug — two distinct page selectors must produce visually different projections), and the Transformer + CNN motivating scenario. A `tests/smoke.mjs` smoke test drives the built `dist/index.js` over JSON-RPC and asserts all 9 tools register with the right input schemas. Root vitest suite (107 tests) still green. * fix(mcp): rewrite page-targeted export browser-side; harden edit/get The page-targeted PNG/SVG export never worked: export_diagram swapped the live session to a single-page projection, slept 3s, then wrote the export flag onto a state object that setState() had already replaced in the store Map — so the browser never saw the request and every such export timed out. The swap+restore also clobbered concurrent edits. Move the projection entirely browser-side: requestExport() hands a single -page <mxfile> to the bridge via state.exportXml; the bridge loads it, lets draw.io render, exports, then reloads the user's real document. The canonical session state is never mutated, so there is no restore race and no fixed-delay guessing. The export poll now re-reads the live store entry each tick instead of a captured reference. autosave is suppressed and the version-bump reload is skipped while a projection is on screen; if no real document was captured, restore forces a server reload rather than leaving the iframe stuck on the projection. Also: - edit_diagram now returns isError on a page-level failure (selector matched no page / page has no <root>) instead of reporting success-with-warnings and persisting a no-op; the pre-edit history snapshot is taken only after that gate so a failed edit leaves no phantom undo entry. - edit_diagram/get_diagram re-normalise browser-pushed xml to mxfile so a bare <mxGraphModel> can't silently strip a multi-page document. - get_diagram now errors (instead of silently returning the full doc) when a selector is given but the session isn't a parseable mxfile. - page_id / page_name / add_page.id get .min(1) so empty strings can't silently target the first page. - Extract pages.ts:projectPage(), collapsing three copies of the parse→find→serialise projection logic in index.ts. - Replace the never-in-CI tests/smoke.mjs with tests/server-wiring.test.ts, which boots the server from source via tsx and runs under the existing vitest CI step. * chore(mcp): set version to 0.2.1 for release --------- Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2026-06-16 05:45:50 +05:30
"REQUIRED: Either a complete <mxGraphModel> (legacy single-page) or a full <mxfile> with one or more <diagram> children (multi-page).",
),
},
},
async ({ xml: inputXml }) => {
try {
if (!currentSession) {
return {
content: [
{
type: "text",
text: "Error: No active session. Please call start_session first.",
},
],
isError: true,
}
}
feat(mcp): add multi-page (mxfile) support to MCP server (#862) * feat(mcp): add multi-page (mxfile) support The MCP server's write path could only address a single drawio page even though the underlying .drawio file format and the embedded editor both natively support multi-page documents. A user asking for "a second page with a CNN diagram" would hit the validator with the error "Expected closing tag </root> but found </mxCell>" because the validator assumed input was a bare <mxGraphModel> and could not walk past the <mxfile><diagram>...</diagram></mxfile> wrapper. This patch closes the gap end to end: * New helper module `pages.ts` centralises page CRUD (normalize, parse, list, find, add, rename, delete) so every layer agrees that the canonical in-memory shape is always <mxfile>. normalizeToMxfile and addPageToDoc both strip any leading <?xml ?> declaration before embedding a fragment inside <diagram> (the declaration is only valid at document start). addPageToDoc explicitly rejects full <mxfile> inputs so a caller cannot accidentally nest a document inside a page. * `xml-validation.ts` now detects an <mxfile> root and scopes the duplicate-id check per <diagram>. The legacy regex check would otherwise reject every multi-page doc, because cells "0" and "1" repeat in each page's <root> by design. The DOM-parse path is gated by a cheap regex pre-check so legacy bare <mxGraphModel> callers don't pay any extra cost. The autoFix duplicate-id rename step is also guarded against mxfile inputs — renaming those sentinel cells would silently break drawio's parent references. * `diagram-operations.ts` accepts an optional PageSelector. For <mxfile> input it resolves the page first and scopes all querySelectorAll calls to that page's <root>, so a delete on page 2's cell "2" no longer touches page 1's cell "2". * `create_new_diagram` accepts either a bare <mxGraphModel> (legacy, auto-wrapped into a single-page mxfile) or a full <mxfile> with N diagrams. All existing single-page callers keep working unchanged. * `edit_diagram`, `get_diagram`, and `export_diagram` gain optional `page_id` / `page_name` / `page_index` parameters. When omitted they target the first page — the "active by convention" default. Tool handlers with all-optional input schemas coalesce missing arguments via `input ?? {}` so a no-args MCP invocation can't crash on destructure before reaching the session-existence check. * New tools: `list_pages`, `add_page`, `rename_page`, `delete_page`. * Page-targeted PNG/SVG export uses a "load + export + restore" dance: the server projects the target page into a single-page <mxfile>, pushes it into the transient state so the browser reloads the iframe with just that page, waits for drawio to render (~3s), triggers the export, captures the data, and then restores the original multi-page document. The dance is wrapped in `try/finally` so the restore runs unconditionally — even if an exception is thrown mid-dance, the user's multi-tab view is recovered before the function returns. The earlier attempt to use drawio's `selectPage` postMessage was a no-op because drawio's JSON embed protocol does not expose that action — silently exporting whatever tab happened to be active. The load-export-restore approach trades a brief visible tab-flicker for correctness: the exported image is guaranteed to match the requested page. * Tool description strings reflect the multi-page semantics so the LLM client learns the new contract. * Package version bumped 0.2.0 → 0.3.0 (additive surface — four new tools, three extended input schemas, canonical XML shape change). * CI: `.github/workflows/test.yml` gains an explicit install + vitest run for the mcp-server package so the new multi-page invariants are covered by automation, not just local runs. Backward compatibility: every existing single-page caller continues to work without modification. The session.xml shape is normalised on every write, removing the wrapper-injection hack from the .drawio download path. Tests: 43 unit tests under `packages/mcp-server/tests/multi-page.test.ts` pin the validator's mxfile path, the page-scoped operations, the XML declaration-prefix handling for both normalizeToMxfile and addPageToDoc, addPageToDoc's rejection of full <mxfile> inputs, the single-page projection used by export_diagram (a direct regression test for the selectPage bug — two distinct page selectors must produce visually different projections), and the Transformer + CNN motivating scenario. A `tests/smoke.mjs` smoke test drives the built `dist/index.js` over JSON-RPC and asserts all 9 tools register with the right input schemas. Root vitest suite (107 tests) still green. * fix(mcp): rewrite page-targeted export browser-side; harden edit/get The page-targeted PNG/SVG export never worked: export_diagram swapped the live session to a single-page projection, slept 3s, then wrote the export flag onto a state object that setState() had already replaced in the store Map — so the browser never saw the request and every such export timed out. The swap+restore also clobbered concurrent edits. Move the projection entirely browser-side: requestExport() hands a single -page <mxfile> to the bridge via state.exportXml; the bridge loads it, lets draw.io render, exports, then reloads the user's real document. The canonical session state is never mutated, so there is no restore race and no fixed-delay guessing. The export poll now re-reads the live store entry each tick instead of a captured reference. autosave is suppressed and the version-bump reload is skipped while a projection is on screen; if no real document was captured, restore forces a server reload rather than leaving the iframe stuck on the projection. Also: - edit_diagram now returns isError on a page-level failure (selector matched no page / page has no <root>) instead of reporting success-with-warnings and persisting a no-op; the pre-edit history snapshot is taken only after that gate so a failed edit leaves no phantom undo entry. - edit_diagram/get_diagram re-normalise browser-pushed xml to mxfile so a bare <mxGraphModel> can't silently strip a multi-page document. - get_diagram now errors (instead of silently returning the full doc) when a selector is given but the session isn't a parseable mxfile. - page_id / page_name / add_page.id get .min(1) so empty strings can't silently target the first page. - Extract pages.ts:projectPage(), collapsing three copies of the parse→find→serialise projection logic in index.ts. - Replace the never-in-CI tests/smoke.mjs with tests/server-wiring.test.ts, which boots the server from source via tsx and runs under the existing vitest CI step. * chore(mcp): set version to 0.2.1 for release --------- Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2026-06-16 05:45:50 +05:30
// Validate and auto-fix XML (works for both mxfile and mxGraphModel inputs).
let xml = inputXml
const { valid, error, fixed, fixes } = validateAndFixXml(xml)
if (fixed) {
xml = fixed
log.info(`XML auto-fixed: ${fixes.join(", ")}`)
}
if (!valid && error) {
log.error(`XML validation failed: ${error}`)
return {
content: [
{
type: "text",
text: `Error: XML validation failed - ${error}`,
},
],
isError: true,
}
}
feat(mcp): add multi-page (mxfile) support to MCP server (#862) * feat(mcp): add multi-page (mxfile) support The MCP server's write path could only address a single drawio page even though the underlying .drawio file format and the embedded editor both natively support multi-page documents. A user asking for "a second page with a CNN diagram" would hit the validator with the error "Expected closing tag </root> but found </mxCell>" because the validator assumed input was a bare <mxGraphModel> and could not walk past the <mxfile><diagram>...</diagram></mxfile> wrapper. This patch closes the gap end to end: * New helper module `pages.ts` centralises page CRUD (normalize, parse, list, find, add, rename, delete) so every layer agrees that the canonical in-memory shape is always <mxfile>. normalizeToMxfile and addPageToDoc both strip any leading <?xml ?> declaration before embedding a fragment inside <diagram> (the declaration is only valid at document start). addPageToDoc explicitly rejects full <mxfile> inputs so a caller cannot accidentally nest a document inside a page. * `xml-validation.ts` now detects an <mxfile> root and scopes the duplicate-id check per <diagram>. The legacy regex check would otherwise reject every multi-page doc, because cells "0" and "1" repeat in each page's <root> by design. The DOM-parse path is gated by a cheap regex pre-check so legacy bare <mxGraphModel> callers don't pay any extra cost. The autoFix duplicate-id rename step is also guarded against mxfile inputs — renaming those sentinel cells would silently break drawio's parent references. * `diagram-operations.ts` accepts an optional PageSelector. For <mxfile> input it resolves the page first and scopes all querySelectorAll calls to that page's <root>, so a delete on page 2's cell "2" no longer touches page 1's cell "2". * `create_new_diagram` accepts either a bare <mxGraphModel> (legacy, auto-wrapped into a single-page mxfile) or a full <mxfile> with N diagrams. All existing single-page callers keep working unchanged. * `edit_diagram`, `get_diagram`, and `export_diagram` gain optional `page_id` / `page_name` / `page_index` parameters. When omitted they target the first page — the "active by convention" default. Tool handlers with all-optional input schemas coalesce missing arguments via `input ?? {}` so a no-args MCP invocation can't crash on destructure before reaching the session-existence check. * New tools: `list_pages`, `add_page`, `rename_page`, `delete_page`. * Page-targeted PNG/SVG export uses a "load + export + restore" dance: the server projects the target page into a single-page <mxfile>, pushes it into the transient state so the browser reloads the iframe with just that page, waits for drawio to render (~3s), triggers the export, captures the data, and then restores the original multi-page document. The dance is wrapped in `try/finally` so the restore runs unconditionally — even if an exception is thrown mid-dance, the user's multi-tab view is recovered before the function returns. The earlier attempt to use drawio's `selectPage` postMessage was a no-op because drawio's JSON embed protocol does not expose that action — silently exporting whatever tab happened to be active. The load-export-restore approach trades a brief visible tab-flicker for correctness: the exported image is guaranteed to match the requested page. * Tool description strings reflect the multi-page semantics so the LLM client learns the new contract. * Package version bumped 0.2.0 → 0.3.0 (additive surface — four new tools, three extended input schemas, canonical XML shape change). * CI: `.github/workflows/test.yml` gains an explicit install + vitest run for the mcp-server package so the new multi-page invariants are covered by automation, not just local runs. Backward compatibility: every existing single-page caller continues to work without modification. The session.xml shape is normalised on every write, removing the wrapper-injection hack from the .drawio download path. Tests: 43 unit tests under `packages/mcp-server/tests/multi-page.test.ts` pin the validator's mxfile path, the page-scoped operations, the XML declaration-prefix handling for both normalizeToMxfile and addPageToDoc, addPageToDoc's rejection of full <mxfile> inputs, the single-page projection used by export_diagram (a direct regression test for the selectPage bug — two distinct page selectors must produce visually different projections), and the Transformer + CNN motivating scenario. A `tests/smoke.mjs` smoke test drives the built `dist/index.js` over JSON-RPC and asserts all 9 tools register with the right input schemas. Root vitest suite (107 tests) still green. * fix(mcp): rewrite page-targeted export browser-side; harden edit/get The page-targeted PNG/SVG export never worked: export_diagram swapped the live session to a single-page projection, slept 3s, then wrote the export flag onto a state object that setState() had already replaced in the store Map — so the browser never saw the request and every such export timed out. The swap+restore also clobbered concurrent edits. Move the projection entirely browser-side: requestExport() hands a single -page <mxfile> to the bridge via state.exportXml; the bridge loads it, lets draw.io render, exports, then reloads the user's real document. The canonical session state is never mutated, so there is no restore race and no fixed-delay guessing. The export poll now re-reads the live store entry each tick instead of a captured reference. autosave is suppressed and the version-bump reload is skipped while a projection is on screen; if no real document was captured, restore forces a server reload rather than leaving the iframe stuck on the projection. Also: - edit_diagram now returns isError on a page-level failure (selector matched no page / page has no <root>) instead of reporting success-with-warnings and persisting a no-op; the pre-edit history snapshot is taken only after that gate so a failed edit leaves no phantom undo entry. - edit_diagram/get_diagram re-normalise browser-pushed xml to mxfile so a bare <mxGraphModel> can't silently strip a multi-page document. - get_diagram now errors (instead of silently returning the full doc) when a selector is given but the session isn't a parseable mxfile. - page_id / page_name / add_page.id get .min(1) so empty strings can't silently target the first page. - Extract pages.ts:projectPage(), collapsing three copies of the parse→find→serialise projection logic in index.ts. - Replace the never-in-CI tests/smoke.mjs with tests/server-wiring.test.ts, which boots the server from source via tsx and runs under the existing vitest CI step. * chore(mcp): set version to 0.2.1 for release --------- Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2026-06-16 05:45:50 +05:30
// Normalise to the canonical mxfile shape so every later tool can
// assume "session.xml is always an mxfile". Bare <mxGraphModel>
// inputs are wrapped into a single-page mxfile here.
const normalized = normalizeToMxfile(xml)
if (!normalized) {
return {
content: [
{
type: "text",
text: "Error: XML must be either a <mxGraphModel> or an <mxfile> with one or more <diagram> children.",
},
],
isError: true,
}
}
xml = normalized
log.info(`Setting diagram content, ${xml.length} chars`)
// Sync from browser state first
const browserState = getState(currentSession.id)
if (browserState?.xml) {
currentSession.xml = browserState.xml
}
// Save user's state before AI overwrites (with cached SVG)
if (currentSession.xml) {
addHistory(
currentSession.id,
currentSession.xml,
browserState?.svg || "",
)
}
// Update session state
currentSession.xml = xml
currentSession.version++
fix(mcp): replace edit_diagram 30s time gate with content comparison (#890) * fix(mcp): keep diagram context valid during edits Closes #885 * fix(mcp): replace edit_diagram time gate with content comparison The 30s wall-clock gate rejected slow-but-correct clients (#885). Instead of a timeout, remember the exact state-store XML the model last saw (get_diagram / create_new_diagram / edit_diagram / page CRUD) and reject edit_diagram only when the live browser state differs - i.e. the user made edits the model hasn't seen yet. Slow reasoning no longer trips the gate, while unseen manual edits still do. * docs(mcp): align edit_diagram/get_diagram descriptions with content-based gate The 'You MUST call get_diagram BEFORE this tool' requirement and the 'Skipping get_diagram WILL cause user's changes to be LOST' warning no longer match server behavior: a stale edit is rejected with no side effects, never silently applied. Describe the freshness check instead, and direct get_diagram usage at its real purpose - learning the current diagram content when the model doesn't already know it. * fix(mcp): compare diagram content structurally in the edit gate draw.io re-serialises the document when pushing state back (attribute order, pretty-printing, regenerated diagram ids, viewport attributes, mxfile host), so byte comparison could flag an unchanged diagram as stale. Fingerprint what a user can actually change instead - page set, page names, and each page's root cell tree with sorted attributes - keeping byte equality as the fast path. A bare mxGraphModel now also fingerprints identically to its single-page mxfile wrapping. * fix(mcp): don't compare page names against bare mxGraphModel pushes A bare <mxGraphModel> pushed by the embed/sync path carries no page name, so normalizeToMxfile invents "Page-1" — falsely reading any custom page name as a content change and re-triggering the stale rejection on every edit. When either side of the gate comparison is a bare mxGraphModel, fingerprint cell trees only; full-mxfile comparisons still detect renames. * chore(mcp): bump version to 0.2.2 * chore(mcp): sync package-lock.json version to 0.2.2 --------- Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2026-07-12 13:33:20 +07:00
// Push to embedded server state. The model just authored this
// exact XML, so record it as seen — edit_diagram may follow
// without a redundant get_diagram round-trip.
setState(currentSession.id, xml)
fix(mcp): replace edit_diagram 30s time gate with content comparison (#890) * fix(mcp): keep diagram context valid during edits Closes #885 * fix(mcp): replace edit_diagram time gate with content comparison The 30s wall-clock gate rejected slow-but-correct clients (#885). Instead of a timeout, remember the exact state-store XML the model last saw (get_diagram / create_new_diagram / edit_diagram / page CRUD) and reject edit_diagram only when the live browser state differs - i.e. the user made edits the model hasn't seen yet. Slow reasoning no longer trips the gate, while unseen manual edits still do. * docs(mcp): align edit_diagram/get_diagram descriptions with content-based gate The 'You MUST call get_diagram BEFORE this tool' requirement and the 'Skipping get_diagram WILL cause user's changes to be LOST' warning no longer match server behavior: a stale edit is rejected with no side effects, never silently applied. Describe the freshness check instead, and direct get_diagram usage at its real purpose - learning the current diagram content when the model doesn't already know it. * fix(mcp): compare diagram content structurally in the edit gate draw.io re-serialises the document when pushing state back (attribute order, pretty-printing, regenerated diagram ids, viewport attributes, mxfile host), so byte comparison could flag an unchanged diagram as stale. Fingerprint what a user can actually change instead - page set, page names, and each page's root cell tree with sorted attributes - keeping byte equality as the fast path. A bare mxGraphModel now also fingerprints identically to its single-page mxfile wrapping. * fix(mcp): don't compare page names against bare mxGraphModel pushes A bare <mxGraphModel> pushed by the embed/sync path carries no page name, so normalizeToMxfile invents "Page-1" — falsely reading any custom page name as a content change and re-triggering the stale rejection on every edit. When either side of the gate comparison is a bare mxGraphModel, fingerprint cell trees only; full-mxfile comparisons still detect renames. * chore(mcp): bump version to 0.2.2 * chore(mcp): sync package-lock.json version to 0.2.2 --------- Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2026-07-12 13:33:20 +07:00
currentSession.lastSeenXml = xml
// Save AI result (no SVG yet - will be captured by browser)
addHistory(currentSession.id, xml, "")
feat(mcp): add multi-page (mxfile) support to MCP server (#862) * feat(mcp): add multi-page (mxfile) support The MCP server's write path could only address a single drawio page even though the underlying .drawio file format and the embedded editor both natively support multi-page documents. A user asking for "a second page with a CNN diagram" would hit the validator with the error "Expected closing tag </root> but found </mxCell>" because the validator assumed input was a bare <mxGraphModel> and could not walk past the <mxfile><diagram>...</diagram></mxfile> wrapper. This patch closes the gap end to end: * New helper module `pages.ts` centralises page CRUD (normalize, parse, list, find, add, rename, delete) so every layer agrees that the canonical in-memory shape is always <mxfile>. normalizeToMxfile and addPageToDoc both strip any leading <?xml ?> declaration before embedding a fragment inside <diagram> (the declaration is only valid at document start). addPageToDoc explicitly rejects full <mxfile> inputs so a caller cannot accidentally nest a document inside a page. * `xml-validation.ts` now detects an <mxfile> root and scopes the duplicate-id check per <diagram>. The legacy regex check would otherwise reject every multi-page doc, because cells "0" and "1" repeat in each page's <root> by design. The DOM-parse path is gated by a cheap regex pre-check so legacy bare <mxGraphModel> callers don't pay any extra cost. The autoFix duplicate-id rename step is also guarded against mxfile inputs — renaming those sentinel cells would silently break drawio's parent references. * `diagram-operations.ts` accepts an optional PageSelector. For <mxfile> input it resolves the page first and scopes all querySelectorAll calls to that page's <root>, so a delete on page 2's cell "2" no longer touches page 1's cell "2". * `create_new_diagram` accepts either a bare <mxGraphModel> (legacy, auto-wrapped into a single-page mxfile) or a full <mxfile> with N diagrams. All existing single-page callers keep working unchanged. * `edit_diagram`, `get_diagram`, and `export_diagram` gain optional `page_id` / `page_name` / `page_index` parameters. When omitted they target the first page — the "active by convention" default. Tool handlers with all-optional input schemas coalesce missing arguments via `input ?? {}` so a no-args MCP invocation can't crash on destructure before reaching the session-existence check. * New tools: `list_pages`, `add_page`, `rename_page`, `delete_page`. * Page-targeted PNG/SVG export uses a "load + export + restore" dance: the server projects the target page into a single-page <mxfile>, pushes it into the transient state so the browser reloads the iframe with just that page, waits for drawio to render (~3s), triggers the export, captures the data, and then restores the original multi-page document. The dance is wrapped in `try/finally` so the restore runs unconditionally — even if an exception is thrown mid-dance, the user's multi-tab view is recovered before the function returns. The earlier attempt to use drawio's `selectPage` postMessage was a no-op because drawio's JSON embed protocol does not expose that action — silently exporting whatever tab happened to be active. The load-export-restore approach trades a brief visible tab-flicker for correctness: the exported image is guaranteed to match the requested page. * Tool description strings reflect the multi-page semantics so the LLM client learns the new contract. * Package version bumped 0.2.0 → 0.3.0 (additive surface — four new tools, three extended input schemas, canonical XML shape change). * CI: `.github/workflows/test.yml` gains an explicit install + vitest run for the mcp-server package so the new multi-page invariants are covered by automation, not just local runs. Backward compatibility: every existing single-page caller continues to work without modification. The session.xml shape is normalised on every write, removing the wrapper-injection hack from the .drawio download path. Tests: 43 unit tests under `packages/mcp-server/tests/multi-page.test.ts` pin the validator's mxfile path, the page-scoped operations, the XML declaration-prefix handling for both normalizeToMxfile and addPageToDoc, addPageToDoc's rejection of full <mxfile> inputs, the single-page projection used by export_diagram (a direct regression test for the selectPage bug — two distinct page selectors must produce visually different projections), and the Transformer + CNN motivating scenario. A `tests/smoke.mjs` smoke test drives the built `dist/index.js` over JSON-RPC and asserts all 9 tools register with the right input schemas. Root vitest suite (107 tests) still green. * fix(mcp): rewrite page-targeted export browser-side; harden edit/get The page-targeted PNG/SVG export never worked: export_diagram swapped the live session to a single-page projection, slept 3s, then wrote the export flag onto a state object that setState() had already replaced in the store Map — so the browser never saw the request and every such export timed out. The swap+restore also clobbered concurrent edits. Move the projection entirely browser-side: requestExport() hands a single -page <mxfile> to the bridge via state.exportXml; the bridge loads it, lets draw.io render, exports, then reloads the user's real document. The canonical session state is never mutated, so there is no restore race and no fixed-delay guessing. The export poll now re-reads the live store entry each tick instead of a captured reference. autosave is suppressed and the version-bump reload is skipped while a projection is on screen; if no real document was captured, restore forces a server reload rather than leaving the iframe stuck on the projection. Also: - edit_diagram now returns isError on a page-level failure (selector matched no page / page has no <root>) instead of reporting success-with-warnings and persisting a no-op; the pre-edit history snapshot is taken only after that gate so a failed edit leaves no phantom undo entry. - edit_diagram/get_diagram re-normalise browser-pushed xml to mxfile so a bare <mxGraphModel> can't silently strip a multi-page document. - get_diagram now errors (instead of silently returning the full doc) when a selector is given but the session isn't a parseable mxfile. - page_id / page_name / add_page.id get .min(1) so empty strings can't silently target the first page. - Extract pages.ts:projectPage(), collapsing three copies of the parse→find→serialise projection logic in index.ts. - Replace the never-in-CI tests/smoke.mjs with tests/server-wiring.test.ts, which boots the server from source via tsx and runs under the existing vitest CI step. * chore(mcp): set version to 0.2.1 for release --------- Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2026-06-16 05:45:50 +05:30
// Report page count back to the caller so the LLM learns whether
// multi-page worked or fell back to single.
const doc = parseMxfile(xml)
const pages = doc ? listPagesFromDoc(doc) : []
const pageSummary =
pages.length > 1
? `${pages.length} pages: ${pages.map((p) => `${p.index}:${p.name}`).join(", ")}`
: pages.length === 1
? `1 page: ${pages[0].name}`
: "no pages parsed"
log.info(`Diagram content set successfully (${pageSummary})`)
return {
content: [
{
type: "text",
feat(mcp): add multi-page (mxfile) support to MCP server (#862) * feat(mcp): add multi-page (mxfile) support The MCP server's write path could only address a single drawio page even though the underlying .drawio file format and the embedded editor both natively support multi-page documents. A user asking for "a second page with a CNN diagram" would hit the validator with the error "Expected closing tag </root> but found </mxCell>" because the validator assumed input was a bare <mxGraphModel> and could not walk past the <mxfile><diagram>...</diagram></mxfile> wrapper. This patch closes the gap end to end: * New helper module `pages.ts` centralises page CRUD (normalize, parse, list, find, add, rename, delete) so every layer agrees that the canonical in-memory shape is always <mxfile>. normalizeToMxfile and addPageToDoc both strip any leading <?xml ?> declaration before embedding a fragment inside <diagram> (the declaration is only valid at document start). addPageToDoc explicitly rejects full <mxfile> inputs so a caller cannot accidentally nest a document inside a page. * `xml-validation.ts` now detects an <mxfile> root and scopes the duplicate-id check per <diagram>. The legacy regex check would otherwise reject every multi-page doc, because cells "0" and "1" repeat in each page's <root> by design. The DOM-parse path is gated by a cheap regex pre-check so legacy bare <mxGraphModel> callers don't pay any extra cost. The autoFix duplicate-id rename step is also guarded against mxfile inputs — renaming those sentinel cells would silently break drawio's parent references. * `diagram-operations.ts` accepts an optional PageSelector. For <mxfile> input it resolves the page first and scopes all querySelectorAll calls to that page's <root>, so a delete on page 2's cell "2" no longer touches page 1's cell "2". * `create_new_diagram` accepts either a bare <mxGraphModel> (legacy, auto-wrapped into a single-page mxfile) or a full <mxfile> with N diagrams. All existing single-page callers keep working unchanged. * `edit_diagram`, `get_diagram`, and `export_diagram` gain optional `page_id` / `page_name` / `page_index` parameters. When omitted they target the first page — the "active by convention" default. Tool handlers with all-optional input schemas coalesce missing arguments via `input ?? {}` so a no-args MCP invocation can't crash on destructure before reaching the session-existence check. * New tools: `list_pages`, `add_page`, `rename_page`, `delete_page`. * Page-targeted PNG/SVG export uses a "load + export + restore" dance: the server projects the target page into a single-page <mxfile>, pushes it into the transient state so the browser reloads the iframe with just that page, waits for drawio to render (~3s), triggers the export, captures the data, and then restores the original multi-page document. The dance is wrapped in `try/finally` so the restore runs unconditionally — even if an exception is thrown mid-dance, the user's multi-tab view is recovered before the function returns. The earlier attempt to use drawio's `selectPage` postMessage was a no-op because drawio's JSON embed protocol does not expose that action — silently exporting whatever tab happened to be active. The load-export-restore approach trades a brief visible tab-flicker for correctness: the exported image is guaranteed to match the requested page. * Tool description strings reflect the multi-page semantics so the LLM client learns the new contract. * Package version bumped 0.2.0 → 0.3.0 (additive surface — four new tools, three extended input schemas, canonical XML shape change). * CI: `.github/workflows/test.yml` gains an explicit install + vitest run for the mcp-server package so the new multi-page invariants are covered by automation, not just local runs. Backward compatibility: every existing single-page caller continues to work without modification. The session.xml shape is normalised on every write, removing the wrapper-injection hack from the .drawio download path. Tests: 43 unit tests under `packages/mcp-server/tests/multi-page.test.ts` pin the validator's mxfile path, the page-scoped operations, the XML declaration-prefix handling for both normalizeToMxfile and addPageToDoc, addPageToDoc's rejection of full <mxfile> inputs, the single-page projection used by export_diagram (a direct regression test for the selectPage bug — two distinct page selectors must produce visually different projections), and the Transformer + CNN motivating scenario. A `tests/smoke.mjs` smoke test drives the built `dist/index.js` over JSON-RPC and asserts all 9 tools register with the right input schemas. Root vitest suite (107 tests) still green. * fix(mcp): rewrite page-targeted export browser-side; harden edit/get The page-targeted PNG/SVG export never worked: export_diagram swapped the live session to a single-page projection, slept 3s, then wrote the export flag onto a state object that setState() had already replaced in the store Map — so the browser never saw the request and every such export timed out. The swap+restore also clobbered concurrent edits. Move the projection entirely browser-side: requestExport() hands a single -page <mxfile> to the bridge via state.exportXml; the bridge loads it, lets draw.io render, exports, then reloads the user's real document. The canonical session state is never mutated, so there is no restore race and no fixed-delay guessing. The export poll now re-reads the live store entry each tick instead of a captured reference. autosave is suppressed and the version-bump reload is skipped while a projection is on screen; if no real document was captured, restore forces a server reload rather than leaving the iframe stuck on the projection. Also: - edit_diagram now returns isError on a page-level failure (selector matched no page / page has no <root>) instead of reporting success-with-warnings and persisting a no-op; the pre-edit history snapshot is taken only after that gate so a failed edit leaves no phantom undo entry. - edit_diagram/get_diagram re-normalise browser-pushed xml to mxfile so a bare <mxGraphModel> can't silently strip a multi-page document. - get_diagram now errors (instead of silently returning the full doc) when a selector is given but the session isn't a parseable mxfile. - page_id / page_name / add_page.id get .min(1) so empty strings can't silently target the first page. - Extract pages.ts:projectPage(), collapsing three copies of the parse→find→serialise projection logic in index.ts. - Replace the never-in-CI tests/smoke.mjs with tests/server-wiring.test.ts, which boots the server from source via tsx and runs under the existing vitest CI step. * chore(mcp): set version to 0.2.1 for release --------- Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2026-06-16 05:45:50 +05:30
text: `Diagram content set successfully!\n\nThe diagram is now visible in your browser.\n\nXML length: ${xml.length} characters\n${pageSummary}`,
},
],
}
} catch (error) {
const message =
error instanceof Error ? error.message : String(error)
log.error("create_new_diagram failed:", message)
return {
content: [{ type: "text", text: `Error: ${message}` }],
isError: true,
}
}
},
)
// Tool: load_diagram
server.registerTool(
"load_diagram",
{
description:
"Load a .drawio file from disk into the current session, REPLACING the entire diagram (all pages). " +
"The server reads the file directly — you do NOT need to read the file yourself or pass its XML through create_new_diagram. " +
"Handles both plain-XML and draw.io's compressed save format.\n\n" +
"After loading, call get_diagram before edit_diagram — you haven't seen the file's cell IDs or structure yet.",
inputSchema: {
path: z
.string()
.describe(
"Path to the .drawio file to load (e.g., ./diagram.drawio)",
),
},
},
async ({ path }) => {
try {
if (!currentSession) {
return {
content: [
{
type: "text",
text: "Error: No active session. Please call start_session first.",
},
],
isError: true,
}
}
const fs = await import("node:fs/promises")
const nodePath = await import("node:path")
const absolutePath = nodePath.resolve(path)
let content: string
try {
content = await fs.readFile(absolutePath, "utf-8")
} catch (e) {
const msg = e instanceof Error ? e.message : String(e)
return {
content: [
{
type: "text",
text: `Error: Cannot read file ${absolutePath}: ${msg}`,
},
],
isError: true,
}
}
const loaded = parseDrawioFileContent(content)
if (!loaded.ok) {
return {
content: [{ type: "text", text: `Error: ${loaded.error}` }],
isError: true,
}
}
const xml = loaded.xml
log.info(
`Loading diagram from ${absolutePath} (${xml.length} chars)`,
)
// Save the user's current state before replacing (same flow as
// create_new_diagram).
const browserState = getState(currentSession.id)
if (browserState?.xml) {
currentSession.xml = browserState.xml
}
if (currentSession.xml) {
addHistory(
currentSession.id,
currentSession.xml,
browserState?.svg || "",
)
}
currentSession.xml = xml
currentSession.version++
setState(currentSession.id, xml)
// Deliberately NOT marking the loaded XML as seen: the model only
// supplied a path, so it doesn't know the file's cell IDs. The
// edit gate will require one get_diagram before edits.
currentSession.lastSeenXml = ""
addHistory(currentSession.id, xml, "")
const doc = parseMxfile(xml)
const pages = doc ? listPagesFromDoc(doc) : []
const pageSummary =
pages.length > 0
? `Pages (${pages.length}): ${pages.map((p) => `[${p.index}] id=${p.id} name="${p.name}" cells=${p.cellCount}`).join(" | ")}`
: "no pages parsed"
log.info(`Diagram loaded from file (${pageSummary})`)
return {
content: [
{
type: "text",
text: `Diagram loaded from ${absolutePath}!\n\nThe diagram is now visible in your browser.\n\n${pageSummary}\n\nCall get_diagram before edit_diagram — you haven't seen this file's cell IDs yet.`,
},
],
}
} catch (error) {
const message =
error instanceof Error ? error.message : String(error)
log.error("load_diagram failed:", message)
return {
content: [{ type: "text", text: `Error: ${message}` }],
isError: true,
}
}
},
)
// Tool: edit_diagram
server.registerTool(
"edit_diagram",
{
description:
feat(mcp): add multi-page (mxfile) support to MCP server (#862) * feat(mcp): add multi-page (mxfile) support The MCP server's write path could only address a single drawio page even though the underlying .drawio file format and the embedded editor both natively support multi-page documents. A user asking for "a second page with a CNN diagram" would hit the validator with the error "Expected closing tag </root> but found </mxCell>" because the validator assumed input was a bare <mxGraphModel> and could not walk past the <mxfile><diagram>...</diagram></mxfile> wrapper. This patch closes the gap end to end: * New helper module `pages.ts` centralises page CRUD (normalize, parse, list, find, add, rename, delete) so every layer agrees that the canonical in-memory shape is always <mxfile>. normalizeToMxfile and addPageToDoc both strip any leading <?xml ?> declaration before embedding a fragment inside <diagram> (the declaration is only valid at document start). addPageToDoc explicitly rejects full <mxfile> inputs so a caller cannot accidentally nest a document inside a page. * `xml-validation.ts` now detects an <mxfile> root and scopes the duplicate-id check per <diagram>. The legacy regex check would otherwise reject every multi-page doc, because cells "0" and "1" repeat in each page's <root> by design. The DOM-parse path is gated by a cheap regex pre-check so legacy bare <mxGraphModel> callers don't pay any extra cost. The autoFix duplicate-id rename step is also guarded against mxfile inputs — renaming those sentinel cells would silently break drawio's parent references. * `diagram-operations.ts` accepts an optional PageSelector. For <mxfile> input it resolves the page first and scopes all querySelectorAll calls to that page's <root>, so a delete on page 2's cell "2" no longer touches page 1's cell "2". * `create_new_diagram` accepts either a bare <mxGraphModel> (legacy, auto-wrapped into a single-page mxfile) or a full <mxfile> with N diagrams. All existing single-page callers keep working unchanged. * `edit_diagram`, `get_diagram`, and `export_diagram` gain optional `page_id` / `page_name` / `page_index` parameters. When omitted they target the first page — the "active by convention" default. Tool handlers with all-optional input schemas coalesce missing arguments via `input ?? {}` so a no-args MCP invocation can't crash on destructure before reaching the session-existence check. * New tools: `list_pages`, `add_page`, `rename_page`, `delete_page`. * Page-targeted PNG/SVG export uses a "load + export + restore" dance: the server projects the target page into a single-page <mxfile>, pushes it into the transient state so the browser reloads the iframe with just that page, waits for drawio to render (~3s), triggers the export, captures the data, and then restores the original multi-page document. The dance is wrapped in `try/finally` so the restore runs unconditionally — even if an exception is thrown mid-dance, the user's multi-tab view is recovered before the function returns. The earlier attempt to use drawio's `selectPage` postMessage was a no-op because drawio's JSON embed protocol does not expose that action — silently exporting whatever tab happened to be active. The load-export-restore approach trades a brief visible tab-flicker for correctness: the exported image is guaranteed to match the requested page. * Tool description strings reflect the multi-page semantics so the LLM client learns the new contract. * Package version bumped 0.2.0 → 0.3.0 (additive surface — four new tools, three extended input schemas, canonical XML shape change). * CI: `.github/workflows/test.yml` gains an explicit install + vitest run for the mcp-server package so the new multi-page invariants are covered by automation, not just local runs. Backward compatibility: every existing single-page caller continues to work without modification. The session.xml shape is normalised on every write, removing the wrapper-injection hack from the .drawio download path. Tests: 43 unit tests under `packages/mcp-server/tests/multi-page.test.ts` pin the validator's mxfile path, the page-scoped operations, the XML declaration-prefix handling for both normalizeToMxfile and addPageToDoc, addPageToDoc's rejection of full <mxfile> inputs, the single-page projection used by export_diagram (a direct regression test for the selectPage bug — two distinct page selectors must produce visually different projections), and the Transformer + CNN motivating scenario. A `tests/smoke.mjs` smoke test drives the built `dist/index.js` over JSON-RPC and asserts all 9 tools register with the right input schemas. Root vitest suite (107 tests) still green. * fix(mcp): rewrite page-targeted export browser-side; harden edit/get The page-targeted PNG/SVG export never worked: export_diagram swapped the live session to a single-page projection, slept 3s, then wrote the export flag onto a state object that setState() had already replaced in the store Map — so the browser never saw the request and every such export timed out. The swap+restore also clobbered concurrent edits. Move the projection entirely browser-side: requestExport() hands a single -page <mxfile> to the bridge via state.exportXml; the bridge loads it, lets draw.io render, exports, then reloads the user's real document. The canonical session state is never mutated, so there is no restore race and no fixed-delay guessing. The export poll now re-reads the live store entry each tick instead of a captured reference. autosave is suppressed and the version-bump reload is skipped while a projection is on screen; if no real document was captured, restore forces a server reload rather than leaving the iframe stuck on the projection. Also: - edit_diagram now returns isError on a page-level failure (selector matched no page / page has no <root>) instead of reporting success-with-warnings and persisting a no-op; the pre-edit history snapshot is taken only after that gate so a failed edit leaves no phantom undo entry. - edit_diagram/get_diagram re-normalise browser-pushed xml to mxfile so a bare <mxGraphModel> can't silently strip a multi-page document. - get_diagram now errors (instead of silently returning the full doc) when a selector is given but the session isn't a parseable mxfile. - page_id / page_name / add_page.id get .min(1) so empty strings can't silently target the first page. - Extract pages.ts:projectPage(), collapsing three copies of the parse→find→serialise projection logic in index.ts. - Replace the never-in-CI tests/smoke.mjs with tests/server-wiring.test.ts, which boots the server from source via tsx and runs under the existing vitest CI step. * chore(mcp): set version to 0.2.1 for release --------- Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2026-06-16 05:45:50 +05:30
"Edit a specific page in the current diagram by ID-based operations (update/add/delete cells).\n\n" +
fix(mcp): replace edit_diagram 30s time gate with content comparison (#890) * fix(mcp): keep diagram context valid during edits Closes #885 * fix(mcp): replace edit_diagram time gate with content comparison The 30s wall-clock gate rejected slow-but-correct clients (#885). Instead of a timeout, remember the exact state-store XML the model last saw (get_diagram / create_new_diagram / edit_diagram / page CRUD) and reject edit_diagram only when the live browser state differs - i.e. the user made edits the model hasn't seen yet. Slow reasoning no longer trips the gate, while unseen manual edits still do. * docs(mcp): align edit_diagram/get_diagram descriptions with content-based gate The 'You MUST call get_diagram BEFORE this tool' requirement and the 'Skipping get_diagram WILL cause user's changes to be LOST' warning no longer match server behavior: a stale edit is rejected with no side effects, never silently applied. Describe the freshness check instead, and direct get_diagram usage at its real purpose - learning the current diagram content when the model doesn't already know it. * fix(mcp): compare diagram content structurally in the edit gate draw.io re-serialises the document when pushing state back (attribute order, pretty-printing, regenerated diagram ids, viewport attributes, mxfile host), so byte comparison could flag an unchanged diagram as stale. Fingerprint what a user can actually change instead - page set, page names, and each page's root cell tree with sorted attributes - keeping byte equality as the fast path. A bare mxGraphModel now also fingerprints identically to its single-page mxfile wrapping. * fix(mcp): don't compare page names against bare mxGraphModel pushes A bare <mxGraphModel> pushed by the embed/sync path carries no page name, so normalizeToMxfile invents "Page-1" — falsely reading any custom page name as a content change and re-triggering the stale rejection on every edit. When either side of the gate comparison is a bare mxGraphModel, fingerprint cell trees only; full-mxfile comparisons still detect renames. * chore(mcp): bump version to 0.2.2 * chore(mcp): sync package-lock.json version to 0.2.2 --------- Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2026-07-12 13:33:20 +07:00
"Freshness: the server remembers the last diagram state you have seen, and rejects this call " +
"only if the user edited the diagram in the browser since then. You do NOT need to call " +
"get_diagram before every edit — if your view is stale, the call is rejected (with no side " +
"effects) and the error tells you to call get_diagram once and retry.\n\n" +
"Call get_diagram first only when you don't know the current diagram content (cell IDs, " +
"structure) — e.g. the diagram wasn't created in this conversation, or you're unsure your " +
"memory of it is accurate.\n\n" +
feat(mcp): add multi-page (mxfile) support to MCP server (#862) * feat(mcp): add multi-page (mxfile) support The MCP server's write path could only address a single drawio page even though the underlying .drawio file format and the embedded editor both natively support multi-page documents. A user asking for "a second page with a CNN diagram" would hit the validator with the error "Expected closing tag </root> but found </mxCell>" because the validator assumed input was a bare <mxGraphModel> and could not walk past the <mxfile><diagram>...</diagram></mxfile> wrapper. This patch closes the gap end to end: * New helper module `pages.ts` centralises page CRUD (normalize, parse, list, find, add, rename, delete) so every layer agrees that the canonical in-memory shape is always <mxfile>. normalizeToMxfile and addPageToDoc both strip any leading <?xml ?> declaration before embedding a fragment inside <diagram> (the declaration is only valid at document start). addPageToDoc explicitly rejects full <mxfile> inputs so a caller cannot accidentally nest a document inside a page. * `xml-validation.ts` now detects an <mxfile> root and scopes the duplicate-id check per <diagram>. The legacy regex check would otherwise reject every multi-page doc, because cells "0" and "1" repeat in each page's <root> by design. The DOM-parse path is gated by a cheap regex pre-check so legacy bare <mxGraphModel> callers don't pay any extra cost. The autoFix duplicate-id rename step is also guarded against mxfile inputs — renaming those sentinel cells would silently break drawio's parent references. * `diagram-operations.ts` accepts an optional PageSelector. For <mxfile> input it resolves the page first and scopes all querySelectorAll calls to that page's <root>, so a delete on page 2's cell "2" no longer touches page 1's cell "2". * `create_new_diagram` accepts either a bare <mxGraphModel> (legacy, auto-wrapped into a single-page mxfile) or a full <mxfile> with N diagrams. All existing single-page callers keep working unchanged. * `edit_diagram`, `get_diagram`, and `export_diagram` gain optional `page_id` / `page_name` / `page_index` parameters. When omitted they target the first page — the "active by convention" default. Tool handlers with all-optional input schemas coalesce missing arguments via `input ?? {}` so a no-args MCP invocation can't crash on destructure before reaching the session-existence check. * New tools: `list_pages`, `add_page`, `rename_page`, `delete_page`. * Page-targeted PNG/SVG export uses a "load + export + restore" dance: the server projects the target page into a single-page <mxfile>, pushes it into the transient state so the browser reloads the iframe with just that page, waits for drawio to render (~3s), triggers the export, captures the data, and then restores the original multi-page document. The dance is wrapped in `try/finally` so the restore runs unconditionally — even if an exception is thrown mid-dance, the user's multi-tab view is recovered before the function returns. The earlier attempt to use drawio's `selectPage` postMessage was a no-op because drawio's JSON embed protocol does not expose that action — silently exporting whatever tab happened to be active. The load-export-restore approach trades a brief visible tab-flicker for correctness: the exported image is guaranteed to match the requested page. * Tool description strings reflect the multi-page semantics so the LLM client learns the new contract. * Package version bumped 0.2.0 → 0.3.0 (additive surface — four new tools, three extended input schemas, canonical XML shape change). * CI: `.github/workflows/test.yml` gains an explicit install + vitest run for the mcp-server package so the new multi-page invariants are covered by automation, not just local runs. Backward compatibility: every existing single-page caller continues to work without modification. The session.xml shape is normalised on every write, removing the wrapper-injection hack from the .drawio download path. Tests: 43 unit tests under `packages/mcp-server/tests/multi-page.test.ts` pin the validator's mxfile path, the page-scoped operations, the XML declaration-prefix handling for both normalizeToMxfile and addPageToDoc, addPageToDoc's rejection of full <mxfile> inputs, the single-page projection used by export_diagram (a direct regression test for the selectPage bug — two distinct page selectors must produce visually different projections), and the Transformer + CNN motivating scenario. A `tests/smoke.mjs` smoke test drives the built `dist/index.js` over JSON-RPC and asserts all 9 tools register with the right input schemas. Root vitest suite (107 tests) still green. * fix(mcp): rewrite page-targeted export browser-side; harden edit/get The page-targeted PNG/SVG export never worked: export_diagram swapped the live session to a single-page projection, slept 3s, then wrote the export flag onto a state object that setState() had already replaced in the store Map — so the browser never saw the request and every such export timed out. The swap+restore also clobbered concurrent edits. Move the projection entirely browser-side: requestExport() hands a single -page <mxfile> to the bridge via state.exportXml; the bridge loads it, lets draw.io render, exports, then reloads the user's real document. The canonical session state is never mutated, so there is no restore race and no fixed-delay guessing. The export poll now re-reads the live store entry each tick instead of a captured reference. autosave is suppressed and the version-bump reload is skipped while a projection is on screen; if no real document was captured, restore forces a server reload rather than leaving the iframe stuck on the projection. Also: - edit_diagram now returns isError on a page-level failure (selector matched no page / page has no <root>) instead of reporting success-with-warnings and persisting a no-op; the pre-edit history snapshot is taken only after that gate so a failed edit leaves no phantom undo entry. - edit_diagram/get_diagram re-normalise browser-pushed xml to mxfile so a bare <mxGraphModel> can't silently strip a multi-page document. - get_diagram now errors (instead of silently returning the full doc) when a selector is given but the session isn't a parseable mxfile. - page_id / page_name / add_page.id get .min(1) so empty strings can't silently target the first page. - Extract pages.ts:projectPage(), collapsing three copies of the parse→find→serialise projection logic in index.ts. - Replace the never-in-CI tests/smoke.mjs with tests/server-wiring.test.ts, which boots the server from source via tsx and runs under the existing vitest CI step. * chore(mcp): set version to 0.2.1 for release --------- Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2026-06-16 05:45:50 +05:30
"Multi-page targeting:\n" +
"- page_id / page_name / page_index are optional; when all omitted, the FIRST page is targeted\n" +
"- Use list_pages to discover what pages exist\n\n" +
"Operations:\n" +
feat(mcp): add multi-page (mxfile) support to MCP server (#862) * feat(mcp): add multi-page (mxfile) support The MCP server's write path could only address a single drawio page even though the underlying .drawio file format and the embedded editor both natively support multi-page documents. A user asking for "a second page with a CNN diagram" would hit the validator with the error "Expected closing tag </root> but found </mxCell>" because the validator assumed input was a bare <mxGraphModel> and could not walk past the <mxfile><diagram>...</diagram></mxfile> wrapper. This patch closes the gap end to end: * New helper module `pages.ts` centralises page CRUD (normalize, parse, list, find, add, rename, delete) so every layer agrees that the canonical in-memory shape is always <mxfile>. normalizeToMxfile and addPageToDoc both strip any leading <?xml ?> declaration before embedding a fragment inside <diagram> (the declaration is only valid at document start). addPageToDoc explicitly rejects full <mxfile> inputs so a caller cannot accidentally nest a document inside a page. * `xml-validation.ts` now detects an <mxfile> root and scopes the duplicate-id check per <diagram>. The legacy regex check would otherwise reject every multi-page doc, because cells "0" and "1" repeat in each page's <root> by design. The DOM-parse path is gated by a cheap regex pre-check so legacy bare <mxGraphModel> callers don't pay any extra cost. The autoFix duplicate-id rename step is also guarded against mxfile inputs — renaming those sentinel cells would silently break drawio's parent references. * `diagram-operations.ts` accepts an optional PageSelector. For <mxfile> input it resolves the page first and scopes all querySelectorAll calls to that page's <root>, so a delete on page 2's cell "2" no longer touches page 1's cell "2". * `create_new_diagram` accepts either a bare <mxGraphModel> (legacy, auto-wrapped into a single-page mxfile) or a full <mxfile> with N diagrams. All existing single-page callers keep working unchanged. * `edit_diagram`, `get_diagram`, and `export_diagram` gain optional `page_id` / `page_name` / `page_index` parameters. When omitted they target the first page — the "active by convention" default. Tool handlers with all-optional input schemas coalesce missing arguments via `input ?? {}` so a no-args MCP invocation can't crash on destructure before reaching the session-existence check. * New tools: `list_pages`, `add_page`, `rename_page`, `delete_page`. * Page-targeted PNG/SVG export uses a "load + export + restore" dance: the server projects the target page into a single-page <mxfile>, pushes it into the transient state so the browser reloads the iframe with just that page, waits for drawio to render (~3s), triggers the export, captures the data, and then restores the original multi-page document. The dance is wrapped in `try/finally` so the restore runs unconditionally — even if an exception is thrown mid-dance, the user's multi-tab view is recovered before the function returns. The earlier attempt to use drawio's `selectPage` postMessage was a no-op because drawio's JSON embed protocol does not expose that action — silently exporting whatever tab happened to be active. The load-export-restore approach trades a brief visible tab-flicker for correctness: the exported image is guaranteed to match the requested page. * Tool description strings reflect the multi-page semantics so the LLM client learns the new contract. * Package version bumped 0.2.0 → 0.3.0 (additive surface — four new tools, three extended input schemas, canonical XML shape change). * CI: `.github/workflows/test.yml` gains an explicit install + vitest run for the mcp-server package so the new multi-page invariants are covered by automation, not just local runs. Backward compatibility: every existing single-page caller continues to work without modification. The session.xml shape is normalised on every write, removing the wrapper-injection hack from the .drawio download path. Tests: 43 unit tests under `packages/mcp-server/tests/multi-page.test.ts` pin the validator's mxfile path, the page-scoped operations, the XML declaration-prefix handling for both normalizeToMxfile and addPageToDoc, addPageToDoc's rejection of full <mxfile> inputs, the single-page projection used by export_diagram (a direct regression test for the selectPage bug — two distinct page selectors must produce visually different projections), and the Transformer + CNN motivating scenario. A `tests/smoke.mjs` smoke test drives the built `dist/index.js` over JSON-RPC and asserts all 9 tools register with the right input schemas. Root vitest suite (107 tests) still green. * fix(mcp): rewrite page-targeted export browser-side; harden edit/get The page-targeted PNG/SVG export never worked: export_diagram swapped the live session to a single-page projection, slept 3s, then wrote the export flag onto a state object that setState() had already replaced in the store Map — so the browser never saw the request and every such export timed out. The swap+restore also clobbered concurrent edits. Move the projection entirely browser-side: requestExport() hands a single -page <mxfile> to the bridge via state.exportXml; the bridge loads it, lets draw.io render, exports, then reloads the user's real document. The canonical session state is never mutated, so there is no restore race and no fixed-delay guessing. The export poll now re-reads the live store entry each tick instead of a captured reference. autosave is suppressed and the version-bump reload is skipped while a projection is on screen; if no real document was captured, restore forces a server reload rather than leaving the iframe stuck on the projection. Also: - edit_diagram now returns isError on a page-level failure (selector matched no page / page has no <root>) instead of reporting success-with-warnings and persisting a no-op; the pre-edit history snapshot is taken only after that gate so a failed edit leaves no phantom undo entry. - edit_diagram/get_diagram re-normalise browser-pushed xml to mxfile so a bare <mxGraphModel> can't silently strip a multi-page document. - get_diagram now errors (instead of silently returning the full doc) when a selector is given but the session isn't a parseable mxfile. - page_id / page_name / add_page.id get .min(1) so empty strings can't silently target the first page. - Extract pages.ts:projectPage(), collapsing three copies of the parse→find→serialise projection logic in index.ts. - Replace the never-in-CI tests/smoke.mjs with tests/server-wiring.test.ts, which boots the server from source via tsx and runs under the existing vitest CI step. * chore(mcp): set version to 0.2.1 for release --------- Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2026-06-16 05:45:50 +05:30
"- add: Add a new cell. Provide cell_id (new unique id within the page) and new_xml.\n" +
"- update: Replace an existing cell by its id. Provide cell_id and complete new_xml.\n" +
"- delete: Remove a cell by its id. Only cell_id is needed.\n\n" +
"For add/update, new_xml must be a complete mxCell element including mxGeometry.\n\n" +
feat(mcp): add multi-page (mxfile) support to MCP server (#862) * feat(mcp): add multi-page (mxfile) support The MCP server's write path could only address a single drawio page even though the underlying .drawio file format and the embedded editor both natively support multi-page documents. A user asking for "a second page with a CNN diagram" would hit the validator with the error "Expected closing tag </root> but found </mxCell>" because the validator assumed input was a bare <mxGraphModel> and could not walk past the <mxfile><diagram>...</diagram></mxfile> wrapper. This patch closes the gap end to end: * New helper module `pages.ts` centralises page CRUD (normalize, parse, list, find, add, rename, delete) so every layer agrees that the canonical in-memory shape is always <mxfile>. normalizeToMxfile and addPageToDoc both strip any leading <?xml ?> declaration before embedding a fragment inside <diagram> (the declaration is only valid at document start). addPageToDoc explicitly rejects full <mxfile> inputs so a caller cannot accidentally nest a document inside a page. * `xml-validation.ts` now detects an <mxfile> root and scopes the duplicate-id check per <diagram>. The legacy regex check would otherwise reject every multi-page doc, because cells "0" and "1" repeat in each page's <root> by design. The DOM-parse path is gated by a cheap regex pre-check so legacy bare <mxGraphModel> callers don't pay any extra cost. The autoFix duplicate-id rename step is also guarded against mxfile inputs — renaming those sentinel cells would silently break drawio's parent references. * `diagram-operations.ts` accepts an optional PageSelector. For <mxfile> input it resolves the page first and scopes all querySelectorAll calls to that page's <root>, so a delete on page 2's cell "2" no longer touches page 1's cell "2". * `create_new_diagram` accepts either a bare <mxGraphModel> (legacy, auto-wrapped into a single-page mxfile) or a full <mxfile> with N diagrams. All existing single-page callers keep working unchanged. * `edit_diagram`, `get_diagram`, and `export_diagram` gain optional `page_id` / `page_name` / `page_index` parameters. When omitted they target the first page — the "active by convention" default. Tool handlers with all-optional input schemas coalesce missing arguments via `input ?? {}` so a no-args MCP invocation can't crash on destructure before reaching the session-existence check. * New tools: `list_pages`, `add_page`, `rename_page`, `delete_page`. * Page-targeted PNG/SVG export uses a "load + export + restore" dance: the server projects the target page into a single-page <mxfile>, pushes it into the transient state so the browser reloads the iframe with just that page, waits for drawio to render (~3s), triggers the export, captures the data, and then restores the original multi-page document. The dance is wrapped in `try/finally` so the restore runs unconditionally — even if an exception is thrown mid-dance, the user's multi-tab view is recovered before the function returns. The earlier attempt to use drawio's `selectPage` postMessage was a no-op because drawio's JSON embed protocol does not expose that action — silently exporting whatever tab happened to be active. The load-export-restore approach trades a brief visible tab-flicker for correctness: the exported image is guaranteed to match the requested page. * Tool description strings reflect the multi-page semantics so the LLM client learns the new contract. * Package version bumped 0.2.0 → 0.3.0 (additive surface — four new tools, three extended input schemas, canonical XML shape change). * CI: `.github/workflows/test.yml` gains an explicit install + vitest run for the mcp-server package so the new multi-page invariants are covered by automation, not just local runs. Backward compatibility: every existing single-page caller continues to work without modification. The session.xml shape is normalised on every write, removing the wrapper-injection hack from the .drawio download path. Tests: 43 unit tests under `packages/mcp-server/tests/multi-page.test.ts` pin the validator's mxfile path, the page-scoped operations, the XML declaration-prefix handling for both normalizeToMxfile and addPageToDoc, addPageToDoc's rejection of full <mxfile> inputs, the single-page projection used by export_diagram (a direct regression test for the selectPage bug — two distinct page selectors must produce visually different projections), and the Transformer + CNN motivating scenario. A `tests/smoke.mjs` smoke test drives the built `dist/index.js` over JSON-RPC and asserts all 9 tools register with the right input schemas. Root vitest suite (107 tests) still green. * fix(mcp): rewrite page-targeted export browser-side; harden edit/get The page-targeted PNG/SVG export never worked: export_diagram swapped the live session to a single-page projection, slept 3s, then wrote the export flag onto a state object that setState() had already replaced in the store Map — so the browser never saw the request and every such export timed out. The swap+restore also clobbered concurrent edits. Move the projection entirely browser-side: requestExport() hands a single -page <mxfile> to the bridge via state.exportXml; the bridge loads it, lets draw.io render, exports, then reloads the user's real document. The canonical session state is never mutated, so there is no restore race and no fixed-delay guessing. The export poll now re-reads the live store entry each tick instead of a captured reference. autosave is suppressed and the version-bump reload is skipped while a projection is on screen; if no real document was captured, restore forces a server reload rather than leaving the iframe stuck on the projection. Also: - edit_diagram now returns isError on a page-level failure (selector matched no page / page has no <root>) instead of reporting success-with-warnings and persisting a no-op; the pre-edit history snapshot is taken only after that gate so a failed edit leaves no phantom undo entry. - edit_diagram/get_diagram re-normalise browser-pushed xml to mxfile so a bare <mxGraphModel> can't silently strip a multi-page document. - get_diagram now errors (instead of silently returning the full doc) when a selector is given but the session isn't a parseable mxfile. - page_id / page_name / add_page.id get .min(1) so empty strings can't silently target the first page. - Extract pages.ts:projectPage(), collapsing three copies of the parse→find→serialise projection logic in index.ts. - Replace the never-in-CI tests/smoke.mjs with tests/server-wiring.test.ts, which boots the server from source via tsx and runs under the existing vitest CI step. * chore(mcp): set version to 0.2.1 for release --------- Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2026-06-16 05:45:50 +05:30
"Example - Add a rectangle on the default (first) page:\n" +
'{"operations": [{"operation": "add", "cell_id": "rect-1", "new_xml": "<mxCell id=\\"rect-1\\" value=\\"Hello\\" style=\\"rounded=0;\\" vertex=\\"1\\" parent=\\"1\\"><mxGeometry x=\\"100\\" y=\\"100\\" width=\\"120\\" height=\\"60\\" as=\\"geometry\\"/></mxCell>"}]}\n\n' +
feat(mcp): add multi-page (mxfile) support to MCP server (#862) * feat(mcp): add multi-page (mxfile) support The MCP server's write path could only address a single drawio page even though the underlying .drawio file format and the embedded editor both natively support multi-page documents. A user asking for "a second page with a CNN diagram" would hit the validator with the error "Expected closing tag </root> but found </mxCell>" because the validator assumed input was a bare <mxGraphModel> and could not walk past the <mxfile><diagram>...</diagram></mxfile> wrapper. This patch closes the gap end to end: * New helper module `pages.ts` centralises page CRUD (normalize, parse, list, find, add, rename, delete) so every layer agrees that the canonical in-memory shape is always <mxfile>. normalizeToMxfile and addPageToDoc both strip any leading <?xml ?> declaration before embedding a fragment inside <diagram> (the declaration is only valid at document start). addPageToDoc explicitly rejects full <mxfile> inputs so a caller cannot accidentally nest a document inside a page. * `xml-validation.ts` now detects an <mxfile> root and scopes the duplicate-id check per <diagram>. The legacy regex check would otherwise reject every multi-page doc, because cells "0" and "1" repeat in each page's <root> by design. The DOM-parse path is gated by a cheap regex pre-check so legacy bare <mxGraphModel> callers don't pay any extra cost. The autoFix duplicate-id rename step is also guarded against mxfile inputs — renaming those sentinel cells would silently break drawio's parent references. * `diagram-operations.ts` accepts an optional PageSelector. For <mxfile> input it resolves the page first and scopes all querySelectorAll calls to that page's <root>, so a delete on page 2's cell "2" no longer touches page 1's cell "2". * `create_new_diagram` accepts either a bare <mxGraphModel> (legacy, auto-wrapped into a single-page mxfile) or a full <mxfile> with N diagrams. All existing single-page callers keep working unchanged. * `edit_diagram`, `get_diagram`, and `export_diagram` gain optional `page_id` / `page_name` / `page_index` parameters. When omitted they target the first page — the "active by convention" default. Tool handlers with all-optional input schemas coalesce missing arguments via `input ?? {}` so a no-args MCP invocation can't crash on destructure before reaching the session-existence check. * New tools: `list_pages`, `add_page`, `rename_page`, `delete_page`. * Page-targeted PNG/SVG export uses a "load + export + restore" dance: the server projects the target page into a single-page <mxfile>, pushes it into the transient state so the browser reloads the iframe with just that page, waits for drawio to render (~3s), triggers the export, captures the data, and then restores the original multi-page document. The dance is wrapped in `try/finally` so the restore runs unconditionally — even if an exception is thrown mid-dance, the user's multi-tab view is recovered before the function returns. The earlier attempt to use drawio's `selectPage` postMessage was a no-op because drawio's JSON embed protocol does not expose that action — silently exporting whatever tab happened to be active. The load-export-restore approach trades a brief visible tab-flicker for correctness: the exported image is guaranteed to match the requested page. * Tool description strings reflect the multi-page semantics so the LLM client learns the new contract. * Package version bumped 0.2.0 → 0.3.0 (additive surface — four new tools, three extended input schemas, canonical XML shape change). * CI: `.github/workflows/test.yml` gains an explicit install + vitest run for the mcp-server package so the new multi-page invariants are covered by automation, not just local runs. Backward compatibility: every existing single-page caller continues to work without modification. The session.xml shape is normalised on every write, removing the wrapper-injection hack from the .drawio download path. Tests: 43 unit tests under `packages/mcp-server/tests/multi-page.test.ts` pin the validator's mxfile path, the page-scoped operations, the XML declaration-prefix handling for both normalizeToMxfile and addPageToDoc, addPageToDoc's rejection of full <mxfile> inputs, the single-page projection used by export_diagram (a direct regression test for the selectPage bug — two distinct page selectors must produce visually different projections), and the Transformer + CNN motivating scenario. A `tests/smoke.mjs` smoke test drives the built `dist/index.js` over JSON-RPC and asserts all 9 tools register with the right input schemas. Root vitest suite (107 tests) still green. * fix(mcp): rewrite page-targeted export browser-side; harden edit/get The page-targeted PNG/SVG export never worked: export_diagram swapped the live session to a single-page projection, slept 3s, then wrote the export flag onto a state object that setState() had already replaced in the store Map — so the browser never saw the request and every such export timed out. The swap+restore also clobbered concurrent edits. Move the projection entirely browser-side: requestExport() hands a single -page <mxfile> to the bridge via state.exportXml; the bridge loads it, lets draw.io render, exports, then reloads the user's real document. The canonical session state is never mutated, so there is no restore race and no fixed-delay guessing. The export poll now re-reads the live store entry each tick instead of a captured reference. autosave is suppressed and the version-bump reload is skipped while a projection is on screen; if no real document was captured, restore forces a server reload rather than leaving the iframe stuck on the projection. Also: - edit_diagram now returns isError on a page-level failure (selector matched no page / page has no <root>) instead of reporting success-with-warnings and persisting a no-op; the pre-edit history snapshot is taken only after that gate so a failed edit leaves no phantom undo entry. - edit_diagram/get_diagram re-normalise browser-pushed xml to mxfile so a bare <mxGraphModel> can't silently strip a multi-page document. - get_diagram now errors (instead of silently returning the full doc) when a selector is given but the session isn't a parseable mxfile. - page_id / page_name / add_page.id get .min(1) so empty strings can't silently target the first page. - Extract pages.ts:projectPage(), collapsing three copies of the parse→find→serialise projection logic in index.ts. - Replace the never-in-CI tests/smoke.mjs with tests/server-wiring.test.ts, which boots the server from source via tsx and runs under the existing vitest CI step. * chore(mcp): set version to 0.2.1 for release --------- Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2026-06-16 05:45:50 +05:30
"Example - Add a cell on a specific page by name:\n" +
'{"page_name": "CNN", "operations": [{"operation": "add", "cell_id": "conv-1", "new_xml": "<mxCell id=\\"conv-1\\" ... />"}]}\n\n' +
"Example - Update a cell on page index 1:\n" +
'{"page_index": 1, "operations": [{"operation": "update", "cell_id": "3", "new_xml": "<mxCell id=\\"3\\" .../>"}]}\n\n' +
"Example - Delete a cell on the default page:\n" +
'{"operations": [{"operation": "delete", "cell_id": "rect-1"}]}',
inputSchema: {
feat(mcp): add multi-page (mxfile) support to MCP server (#862) * feat(mcp): add multi-page (mxfile) support The MCP server's write path could only address a single drawio page even though the underlying .drawio file format and the embedded editor both natively support multi-page documents. A user asking for "a second page with a CNN diagram" would hit the validator with the error "Expected closing tag </root> but found </mxCell>" because the validator assumed input was a bare <mxGraphModel> and could not walk past the <mxfile><diagram>...</diagram></mxfile> wrapper. This patch closes the gap end to end: * New helper module `pages.ts` centralises page CRUD (normalize, parse, list, find, add, rename, delete) so every layer agrees that the canonical in-memory shape is always <mxfile>. normalizeToMxfile and addPageToDoc both strip any leading <?xml ?> declaration before embedding a fragment inside <diagram> (the declaration is only valid at document start). addPageToDoc explicitly rejects full <mxfile> inputs so a caller cannot accidentally nest a document inside a page. * `xml-validation.ts` now detects an <mxfile> root and scopes the duplicate-id check per <diagram>. The legacy regex check would otherwise reject every multi-page doc, because cells "0" and "1" repeat in each page's <root> by design. The DOM-parse path is gated by a cheap regex pre-check so legacy bare <mxGraphModel> callers don't pay any extra cost. The autoFix duplicate-id rename step is also guarded against mxfile inputs — renaming those sentinel cells would silently break drawio's parent references. * `diagram-operations.ts` accepts an optional PageSelector. For <mxfile> input it resolves the page first and scopes all querySelectorAll calls to that page's <root>, so a delete on page 2's cell "2" no longer touches page 1's cell "2". * `create_new_diagram` accepts either a bare <mxGraphModel> (legacy, auto-wrapped into a single-page mxfile) or a full <mxfile> with N diagrams. All existing single-page callers keep working unchanged. * `edit_diagram`, `get_diagram`, and `export_diagram` gain optional `page_id` / `page_name` / `page_index` parameters. When omitted they target the first page — the "active by convention" default. Tool handlers with all-optional input schemas coalesce missing arguments via `input ?? {}` so a no-args MCP invocation can't crash on destructure before reaching the session-existence check. * New tools: `list_pages`, `add_page`, `rename_page`, `delete_page`. * Page-targeted PNG/SVG export uses a "load + export + restore" dance: the server projects the target page into a single-page <mxfile>, pushes it into the transient state so the browser reloads the iframe with just that page, waits for drawio to render (~3s), triggers the export, captures the data, and then restores the original multi-page document. The dance is wrapped in `try/finally` so the restore runs unconditionally — even if an exception is thrown mid-dance, the user's multi-tab view is recovered before the function returns. The earlier attempt to use drawio's `selectPage` postMessage was a no-op because drawio's JSON embed protocol does not expose that action — silently exporting whatever tab happened to be active. The load-export-restore approach trades a brief visible tab-flicker for correctness: the exported image is guaranteed to match the requested page. * Tool description strings reflect the multi-page semantics so the LLM client learns the new contract. * Package version bumped 0.2.0 → 0.3.0 (additive surface — four new tools, three extended input schemas, canonical XML shape change). * CI: `.github/workflows/test.yml` gains an explicit install + vitest run for the mcp-server package so the new multi-page invariants are covered by automation, not just local runs. Backward compatibility: every existing single-page caller continues to work without modification. The session.xml shape is normalised on every write, removing the wrapper-injection hack from the .drawio download path. Tests: 43 unit tests under `packages/mcp-server/tests/multi-page.test.ts` pin the validator's mxfile path, the page-scoped operations, the XML declaration-prefix handling for both normalizeToMxfile and addPageToDoc, addPageToDoc's rejection of full <mxfile> inputs, the single-page projection used by export_diagram (a direct regression test for the selectPage bug — two distinct page selectors must produce visually different projections), and the Transformer + CNN motivating scenario. A `tests/smoke.mjs` smoke test drives the built `dist/index.js` over JSON-RPC and asserts all 9 tools register with the right input schemas. Root vitest suite (107 tests) still green. * fix(mcp): rewrite page-targeted export browser-side; harden edit/get The page-targeted PNG/SVG export never worked: export_diagram swapped the live session to a single-page projection, slept 3s, then wrote the export flag onto a state object that setState() had already replaced in the store Map — so the browser never saw the request and every such export timed out. The swap+restore also clobbered concurrent edits. Move the projection entirely browser-side: requestExport() hands a single -page <mxfile> to the bridge via state.exportXml; the bridge loads it, lets draw.io render, exports, then reloads the user's real document. The canonical session state is never mutated, so there is no restore race and no fixed-delay guessing. The export poll now re-reads the live store entry each tick instead of a captured reference. autosave is suppressed and the version-bump reload is skipped while a projection is on screen; if no real document was captured, restore forces a server reload rather than leaving the iframe stuck on the projection. Also: - edit_diagram now returns isError on a page-level failure (selector matched no page / page has no <root>) instead of reporting success-with-warnings and persisting a no-op; the pre-edit history snapshot is taken only after that gate so a failed edit leaves no phantom undo entry. - edit_diagram/get_diagram re-normalise browser-pushed xml to mxfile so a bare <mxGraphModel> can't silently strip a multi-page document. - get_diagram now errors (instead of silently returning the full doc) when a selector is given but the session isn't a parseable mxfile. - page_id / page_name / add_page.id get .min(1) so empty strings can't silently target the first page. - Extract pages.ts:projectPage(), collapsing three copies of the parse→find→serialise projection logic in index.ts. - Replace the never-in-CI tests/smoke.mjs with tests/server-wiring.test.ts, which boots the server from source via tsx and runs under the existing vitest CI step. * chore(mcp): set version to 0.2.1 for release --------- Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2026-06-16 05:45:50 +05:30
...pageSelectorSchema,
operations: z
.array(
z.object({
operation: z
.enum(["update", "add", "delete"])
.describe(
"Operation to perform: add, update, or delete",
),
cell_id: z.string().describe("The id of the mxCell"),
new_xml: z
.string()
.optional()
.describe(
"Complete mxCell XML element (required for update/add)",
),
}),
)
.describe("Array of operations to apply"),
},
},
feat(mcp): add multi-page (mxfile) support to MCP server (#862) * feat(mcp): add multi-page (mxfile) support The MCP server's write path could only address a single drawio page even though the underlying .drawio file format and the embedded editor both natively support multi-page documents. A user asking for "a second page with a CNN diagram" would hit the validator with the error "Expected closing tag </root> but found </mxCell>" because the validator assumed input was a bare <mxGraphModel> and could not walk past the <mxfile><diagram>...</diagram></mxfile> wrapper. This patch closes the gap end to end: * New helper module `pages.ts` centralises page CRUD (normalize, parse, list, find, add, rename, delete) so every layer agrees that the canonical in-memory shape is always <mxfile>. normalizeToMxfile and addPageToDoc both strip any leading <?xml ?> declaration before embedding a fragment inside <diagram> (the declaration is only valid at document start). addPageToDoc explicitly rejects full <mxfile> inputs so a caller cannot accidentally nest a document inside a page. * `xml-validation.ts` now detects an <mxfile> root and scopes the duplicate-id check per <diagram>. The legacy regex check would otherwise reject every multi-page doc, because cells "0" and "1" repeat in each page's <root> by design. The DOM-parse path is gated by a cheap regex pre-check so legacy bare <mxGraphModel> callers don't pay any extra cost. The autoFix duplicate-id rename step is also guarded against mxfile inputs — renaming those sentinel cells would silently break drawio's parent references. * `diagram-operations.ts` accepts an optional PageSelector. For <mxfile> input it resolves the page first and scopes all querySelectorAll calls to that page's <root>, so a delete on page 2's cell "2" no longer touches page 1's cell "2". * `create_new_diagram` accepts either a bare <mxGraphModel> (legacy, auto-wrapped into a single-page mxfile) or a full <mxfile> with N diagrams. All existing single-page callers keep working unchanged. * `edit_diagram`, `get_diagram`, and `export_diagram` gain optional `page_id` / `page_name` / `page_index` parameters. When omitted they target the first page — the "active by convention" default. Tool handlers with all-optional input schemas coalesce missing arguments via `input ?? {}` so a no-args MCP invocation can't crash on destructure before reaching the session-existence check. * New tools: `list_pages`, `add_page`, `rename_page`, `delete_page`. * Page-targeted PNG/SVG export uses a "load + export + restore" dance: the server projects the target page into a single-page <mxfile>, pushes it into the transient state so the browser reloads the iframe with just that page, waits for drawio to render (~3s), triggers the export, captures the data, and then restores the original multi-page document. The dance is wrapped in `try/finally` so the restore runs unconditionally — even if an exception is thrown mid-dance, the user's multi-tab view is recovered before the function returns. The earlier attempt to use drawio's `selectPage` postMessage was a no-op because drawio's JSON embed protocol does not expose that action — silently exporting whatever tab happened to be active. The load-export-restore approach trades a brief visible tab-flicker for correctness: the exported image is guaranteed to match the requested page. * Tool description strings reflect the multi-page semantics so the LLM client learns the new contract. * Package version bumped 0.2.0 → 0.3.0 (additive surface — four new tools, three extended input schemas, canonical XML shape change). * CI: `.github/workflows/test.yml` gains an explicit install + vitest run for the mcp-server package so the new multi-page invariants are covered by automation, not just local runs. Backward compatibility: every existing single-page caller continues to work without modification. The session.xml shape is normalised on every write, removing the wrapper-injection hack from the .drawio download path. Tests: 43 unit tests under `packages/mcp-server/tests/multi-page.test.ts` pin the validator's mxfile path, the page-scoped operations, the XML declaration-prefix handling for both normalizeToMxfile and addPageToDoc, addPageToDoc's rejection of full <mxfile> inputs, the single-page projection used by export_diagram (a direct regression test for the selectPage bug — two distinct page selectors must produce visually different projections), and the Transformer + CNN motivating scenario. A `tests/smoke.mjs` smoke test drives the built `dist/index.js` over JSON-RPC and asserts all 9 tools register with the right input schemas. Root vitest suite (107 tests) still green. * fix(mcp): rewrite page-targeted export browser-side; harden edit/get The page-targeted PNG/SVG export never worked: export_diagram swapped the live session to a single-page projection, slept 3s, then wrote the export flag onto a state object that setState() had already replaced in the store Map — so the browser never saw the request and every such export timed out. The swap+restore also clobbered concurrent edits. Move the projection entirely browser-side: requestExport() hands a single -page <mxfile> to the bridge via state.exportXml; the bridge loads it, lets draw.io render, exports, then reloads the user's real document. The canonical session state is never mutated, so there is no restore race and no fixed-delay guessing. The export poll now re-reads the live store entry each tick instead of a captured reference. autosave is suppressed and the version-bump reload is skipped while a projection is on screen; if no real document was captured, restore forces a server reload rather than leaving the iframe stuck on the projection. Also: - edit_diagram now returns isError on a page-level failure (selector matched no page / page has no <root>) instead of reporting success-with-warnings and persisting a no-op; the pre-edit history snapshot is taken only after that gate so a failed edit leaves no phantom undo entry. - edit_diagram/get_diagram re-normalise browser-pushed xml to mxfile so a bare <mxGraphModel> can't silently strip a multi-page document. - get_diagram now errors (instead of silently returning the full doc) when a selector is given but the session isn't a parseable mxfile. - page_id / page_name / add_page.id get .min(1) so empty strings can't silently target the first page. - Extract pages.ts:projectPage(), collapsing three copies of the parse→find→serialise projection logic in index.ts. - Replace the never-in-CI tests/smoke.mjs with tests/server-wiring.test.ts, which boots the server from source via tsx and runs under the existing vitest CI step. * chore(mcp): set version to 0.2.1 for release --------- Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2026-06-16 05:45:50 +05:30
async ({ operations, page_id, page_name, page_index }) => {
try {
if (!currentSession) {
return {
content: [
{
type: "text",
text: "Error: No active session. Please call start_session first.",
},
],
isError: true,
}
}
feat(mcp): add multi-page (mxfile) support to MCP server (#862) * feat(mcp): add multi-page (mxfile) support The MCP server's write path could only address a single drawio page even though the underlying .drawio file format and the embedded editor both natively support multi-page documents. A user asking for "a second page with a CNN diagram" would hit the validator with the error "Expected closing tag </root> but found </mxCell>" because the validator assumed input was a bare <mxGraphModel> and could not walk past the <mxfile><diagram>...</diagram></mxfile> wrapper. This patch closes the gap end to end: * New helper module `pages.ts` centralises page CRUD (normalize, parse, list, find, add, rename, delete) so every layer agrees that the canonical in-memory shape is always <mxfile>. normalizeToMxfile and addPageToDoc both strip any leading <?xml ?> declaration before embedding a fragment inside <diagram> (the declaration is only valid at document start). addPageToDoc explicitly rejects full <mxfile> inputs so a caller cannot accidentally nest a document inside a page. * `xml-validation.ts` now detects an <mxfile> root and scopes the duplicate-id check per <diagram>. The legacy regex check would otherwise reject every multi-page doc, because cells "0" and "1" repeat in each page's <root> by design. The DOM-parse path is gated by a cheap regex pre-check so legacy bare <mxGraphModel> callers don't pay any extra cost. The autoFix duplicate-id rename step is also guarded against mxfile inputs — renaming those sentinel cells would silently break drawio's parent references. * `diagram-operations.ts` accepts an optional PageSelector. For <mxfile> input it resolves the page first and scopes all querySelectorAll calls to that page's <root>, so a delete on page 2's cell "2" no longer touches page 1's cell "2". * `create_new_diagram` accepts either a bare <mxGraphModel> (legacy, auto-wrapped into a single-page mxfile) or a full <mxfile> with N diagrams. All existing single-page callers keep working unchanged. * `edit_diagram`, `get_diagram`, and `export_diagram` gain optional `page_id` / `page_name` / `page_index` parameters. When omitted they target the first page — the "active by convention" default. Tool handlers with all-optional input schemas coalesce missing arguments via `input ?? {}` so a no-args MCP invocation can't crash on destructure before reaching the session-existence check. * New tools: `list_pages`, `add_page`, `rename_page`, `delete_page`. * Page-targeted PNG/SVG export uses a "load + export + restore" dance: the server projects the target page into a single-page <mxfile>, pushes it into the transient state so the browser reloads the iframe with just that page, waits for drawio to render (~3s), triggers the export, captures the data, and then restores the original multi-page document. The dance is wrapped in `try/finally` so the restore runs unconditionally — even if an exception is thrown mid-dance, the user's multi-tab view is recovered before the function returns. The earlier attempt to use drawio's `selectPage` postMessage was a no-op because drawio's JSON embed protocol does not expose that action — silently exporting whatever tab happened to be active. The load-export-restore approach trades a brief visible tab-flicker for correctness: the exported image is guaranteed to match the requested page. * Tool description strings reflect the multi-page semantics so the LLM client learns the new contract. * Package version bumped 0.2.0 → 0.3.0 (additive surface — four new tools, three extended input schemas, canonical XML shape change). * CI: `.github/workflows/test.yml` gains an explicit install + vitest run for the mcp-server package so the new multi-page invariants are covered by automation, not just local runs. Backward compatibility: every existing single-page caller continues to work without modification. The session.xml shape is normalised on every write, removing the wrapper-injection hack from the .drawio download path. Tests: 43 unit tests under `packages/mcp-server/tests/multi-page.test.ts` pin the validator's mxfile path, the page-scoped operations, the XML declaration-prefix handling for both normalizeToMxfile and addPageToDoc, addPageToDoc's rejection of full <mxfile> inputs, the single-page projection used by export_diagram (a direct regression test for the selectPage bug — two distinct page selectors must produce visually different projections), and the Transformer + CNN motivating scenario. A `tests/smoke.mjs` smoke test drives the built `dist/index.js` over JSON-RPC and asserts all 9 tools register with the right input schemas. Root vitest suite (107 tests) still green. * fix(mcp): rewrite page-targeted export browser-side; harden edit/get The page-targeted PNG/SVG export never worked: export_diagram swapped the live session to a single-page projection, slept 3s, then wrote the export flag onto a state object that setState() had already replaced in the store Map — so the browser never saw the request and every such export timed out. The swap+restore also clobbered concurrent edits. Move the projection entirely browser-side: requestExport() hands a single -page <mxfile> to the bridge via state.exportXml; the bridge loads it, lets draw.io render, exports, then reloads the user's real document. The canonical session state is never mutated, so there is no restore race and no fixed-delay guessing. The export poll now re-reads the live store entry each tick instead of a captured reference. autosave is suppressed and the version-bump reload is skipped while a projection is on screen; if no real document was captured, restore forces a server reload rather than leaving the iframe stuck on the projection. Also: - edit_diagram now returns isError on a page-level failure (selector matched no page / page has no <root>) instead of reporting success-with-warnings and persisting a no-op; the pre-edit history snapshot is taken only after that gate so a failed edit leaves no phantom undo entry. - edit_diagram/get_diagram re-normalise browser-pushed xml to mxfile so a bare <mxGraphModel> can't silently strip a multi-page document. - get_diagram now errors (instead of silently returning the full doc) when a selector is given but the session isn't a parseable mxfile. - page_id / page_name / add_page.id get .min(1) so empty strings can't silently target the first page. - Extract pages.ts:projectPage(), collapsing three copies of the parse→find→serialise projection logic in index.ts. - Replace the never-in-CI tests/smoke.mjs with tests/server-wiring.test.ts, which boots the server from source via tsx and runs under the existing vitest CI step. * chore(mcp): set version to 0.2.1 for release --------- Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2026-06-16 05:45:50 +05:30
// Fetch latest state from browser. Re-normalise to mxfile: the
// embed/sync path can hand back a bare <mxGraphModel>, and adopting
// it verbatim would silently strip a multi-page document down to
// one page on the next write.
const browserState = getState(currentSession.id)
if (browserState?.xml) {
feat(mcp): add multi-page (mxfile) support to MCP server (#862) * feat(mcp): add multi-page (mxfile) support The MCP server's write path could only address a single drawio page even though the underlying .drawio file format and the embedded editor both natively support multi-page documents. A user asking for "a second page with a CNN diagram" would hit the validator with the error "Expected closing tag </root> but found </mxCell>" because the validator assumed input was a bare <mxGraphModel> and could not walk past the <mxfile><diagram>...</diagram></mxfile> wrapper. This patch closes the gap end to end: * New helper module `pages.ts` centralises page CRUD (normalize, parse, list, find, add, rename, delete) so every layer agrees that the canonical in-memory shape is always <mxfile>. normalizeToMxfile and addPageToDoc both strip any leading <?xml ?> declaration before embedding a fragment inside <diagram> (the declaration is only valid at document start). addPageToDoc explicitly rejects full <mxfile> inputs so a caller cannot accidentally nest a document inside a page. * `xml-validation.ts` now detects an <mxfile> root and scopes the duplicate-id check per <diagram>. The legacy regex check would otherwise reject every multi-page doc, because cells "0" and "1" repeat in each page's <root> by design. The DOM-parse path is gated by a cheap regex pre-check so legacy bare <mxGraphModel> callers don't pay any extra cost. The autoFix duplicate-id rename step is also guarded against mxfile inputs — renaming those sentinel cells would silently break drawio's parent references. * `diagram-operations.ts` accepts an optional PageSelector. For <mxfile> input it resolves the page first and scopes all querySelectorAll calls to that page's <root>, so a delete on page 2's cell "2" no longer touches page 1's cell "2". * `create_new_diagram` accepts either a bare <mxGraphModel> (legacy, auto-wrapped into a single-page mxfile) or a full <mxfile> with N diagrams. All existing single-page callers keep working unchanged. * `edit_diagram`, `get_diagram`, and `export_diagram` gain optional `page_id` / `page_name` / `page_index` parameters. When omitted they target the first page — the "active by convention" default. Tool handlers with all-optional input schemas coalesce missing arguments via `input ?? {}` so a no-args MCP invocation can't crash on destructure before reaching the session-existence check. * New tools: `list_pages`, `add_page`, `rename_page`, `delete_page`. * Page-targeted PNG/SVG export uses a "load + export + restore" dance: the server projects the target page into a single-page <mxfile>, pushes it into the transient state so the browser reloads the iframe with just that page, waits for drawio to render (~3s), triggers the export, captures the data, and then restores the original multi-page document. The dance is wrapped in `try/finally` so the restore runs unconditionally — even if an exception is thrown mid-dance, the user's multi-tab view is recovered before the function returns. The earlier attempt to use drawio's `selectPage` postMessage was a no-op because drawio's JSON embed protocol does not expose that action — silently exporting whatever tab happened to be active. The load-export-restore approach trades a brief visible tab-flicker for correctness: the exported image is guaranteed to match the requested page. * Tool description strings reflect the multi-page semantics so the LLM client learns the new contract. * Package version bumped 0.2.0 → 0.3.0 (additive surface — four new tools, three extended input schemas, canonical XML shape change). * CI: `.github/workflows/test.yml` gains an explicit install + vitest run for the mcp-server package so the new multi-page invariants are covered by automation, not just local runs. Backward compatibility: every existing single-page caller continues to work without modification. The session.xml shape is normalised on every write, removing the wrapper-injection hack from the .drawio download path. Tests: 43 unit tests under `packages/mcp-server/tests/multi-page.test.ts` pin the validator's mxfile path, the page-scoped operations, the XML declaration-prefix handling for both normalizeToMxfile and addPageToDoc, addPageToDoc's rejection of full <mxfile> inputs, the single-page projection used by export_diagram (a direct regression test for the selectPage bug — two distinct page selectors must produce visually different projections), and the Transformer + CNN motivating scenario. A `tests/smoke.mjs` smoke test drives the built `dist/index.js` over JSON-RPC and asserts all 9 tools register with the right input schemas. Root vitest suite (107 tests) still green. * fix(mcp): rewrite page-targeted export browser-side; harden edit/get The page-targeted PNG/SVG export never worked: export_diagram swapped the live session to a single-page projection, slept 3s, then wrote the export flag onto a state object that setState() had already replaced in the store Map — so the browser never saw the request and every such export timed out. The swap+restore also clobbered concurrent edits. Move the projection entirely browser-side: requestExport() hands a single -page <mxfile> to the bridge via state.exportXml; the bridge loads it, lets draw.io render, exports, then reloads the user's real document. The canonical session state is never mutated, so there is no restore race and no fixed-delay guessing. The export poll now re-reads the live store entry each tick instead of a captured reference. autosave is suppressed and the version-bump reload is skipped while a projection is on screen; if no real document was captured, restore forces a server reload rather than leaving the iframe stuck on the projection. Also: - edit_diagram now returns isError on a page-level failure (selector matched no page / page has no <root>) instead of reporting success-with-warnings and persisting a no-op; the pre-edit history snapshot is taken only after that gate so a failed edit leaves no phantom undo entry. - edit_diagram/get_diagram re-normalise browser-pushed xml to mxfile so a bare <mxGraphModel> can't silently strip a multi-page document. - get_diagram now errors (instead of silently returning the full doc) when a selector is given but the session isn't a parseable mxfile. - page_id / page_name / add_page.id get .min(1) so empty strings can't silently target the first page. - Extract pages.ts:projectPage(), collapsing three copies of the parse→find→serialise projection logic in index.ts. - Replace the never-in-CI tests/smoke.mjs with tests/server-wiring.test.ts, which boots the server from source via tsx and runs under the existing vitest CI step. * chore(mcp): set version to 0.2.1 for release --------- Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2026-06-16 05:45:50 +05:30
currentSession.xml =
normalizeToMxfile(browserState.xml) ?? browserState.xml
log.info("Fetched latest diagram state from browser")
}
if (!currentSession.xml) {
return {
content: [
{
type: "text",
text: "Error: No diagram to edit. Please create a diagram first with create_new_diagram.",
},
],
isError: true,
}
}
fix(mcp): replace edit_diagram 30s time gate with content comparison (#890) * fix(mcp): keep diagram context valid during edits Closes #885 * fix(mcp): replace edit_diagram time gate with content comparison The 30s wall-clock gate rejected slow-but-correct clients (#885). Instead of a timeout, remember the exact state-store XML the model last saw (get_diagram / create_new_diagram / edit_diagram / page CRUD) and reject edit_diagram only when the live browser state differs - i.e. the user made edits the model hasn't seen yet. Slow reasoning no longer trips the gate, while unseen manual edits still do. * docs(mcp): align edit_diagram/get_diagram descriptions with content-based gate The 'You MUST call get_diagram BEFORE this tool' requirement and the 'Skipping get_diagram WILL cause user's changes to be LOST' warning no longer match server behavior: a stale edit is rejected with no side effects, never silently applied. Describe the freshness check instead, and direct get_diagram usage at its real purpose - learning the current diagram content when the model doesn't already know it. * fix(mcp): compare diagram content structurally in the edit gate draw.io re-serialises the document when pushing state back (attribute order, pretty-printing, regenerated diagram ids, viewport attributes, mxfile host), so byte comparison could flag an unchanged diagram as stale. Fingerprint what a user can actually change instead - page set, page names, and each page's root cell tree with sorted attributes - keeping byte equality as the fast path. A bare mxGraphModel now also fingerprints identically to its single-page mxfile wrapping. * fix(mcp): don't compare page names against bare mxGraphModel pushes A bare <mxGraphModel> pushed by the embed/sync path carries no page name, so normalizeToMxfile invents "Page-1" — falsely reading any custom page name as a content change and re-triggering the stale rejection on every edit. When either side of the gate comparison is a bare mxGraphModel, fingerprint cell trees only; full-mxfile comparisons still detect renames. * chore(mcp): bump version to 0.2.2 * chore(mcp): sync package-lock.json version to 0.2.2 --------- Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2026-07-12 13:33:20 +07:00
// Enforce workflow: the model must have seen the current diagram
// state. Content comparison instead of a wall-clock timeout —
// slow reasoning between get_diagram and edit_diagram is fine as
// long as nothing changed in the browser meanwhile (#885).
const gate = checkEditGate(
currentSession.lastSeenXml,
browserState?.xml ?? "",
)
if (!gate.ok) {
log.warn(
gate.reason === "stale"
? "edit_diagram called with unseen browser changes - rejecting to prevent data loss"
: "edit_diagram called without get_diagram - rejecting to prevent data loss",
)
return {
content: [
{
type: "text",
text:
gate.reason === "stale"
? "Error: The diagram changed in the browser since you last fetched it (e.g. manual user edits).\n\n" +
"Call get_diagram to see the latest state, then rebuild your edit operations on top of it."
: "Error: You must call get_diagram first before edit_diagram.\n\n" +
"This ensures you have the latest diagram state including any manual edits the user made in the browser. " +
"Please call get_diagram, then use that XML to construct your edit operations.",
},
],
isError: true,
}
}
feat(mcp): add multi-page (mxfile) support to MCP server (#862) * feat(mcp): add multi-page (mxfile) support The MCP server's write path could only address a single drawio page even though the underlying .drawio file format and the embedded editor both natively support multi-page documents. A user asking for "a second page with a CNN diagram" would hit the validator with the error "Expected closing tag </root> but found </mxCell>" because the validator assumed input was a bare <mxGraphModel> and could not walk past the <mxfile><diagram>...</diagram></mxfile> wrapper. This patch closes the gap end to end: * New helper module `pages.ts` centralises page CRUD (normalize, parse, list, find, add, rename, delete) so every layer agrees that the canonical in-memory shape is always <mxfile>. normalizeToMxfile and addPageToDoc both strip any leading <?xml ?> declaration before embedding a fragment inside <diagram> (the declaration is only valid at document start). addPageToDoc explicitly rejects full <mxfile> inputs so a caller cannot accidentally nest a document inside a page. * `xml-validation.ts` now detects an <mxfile> root and scopes the duplicate-id check per <diagram>. The legacy regex check would otherwise reject every multi-page doc, because cells "0" and "1" repeat in each page's <root> by design. The DOM-parse path is gated by a cheap regex pre-check so legacy bare <mxGraphModel> callers don't pay any extra cost. The autoFix duplicate-id rename step is also guarded against mxfile inputs — renaming those sentinel cells would silently break drawio's parent references. * `diagram-operations.ts` accepts an optional PageSelector. For <mxfile> input it resolves the page first and scopes all querySelectorAll calls to that page's <root>, so a delete on page 2's cell "2" no longer touches page 1's cell "2". * `create_new_diagram` accepts either a bare <mxGraphModel> (legacy, auto-wrapped into a single-page mxfile) or a full <mxfile> with N diagrams. All existing single-page callers keep working unchanged. * `edit_diagram`, `get_diagram`, and `export_diagram` gain optional `page_id` / `page_name` / `page_index` parameters. When omitted they target the first page — the "active by convention" default. Tool handlers with all-optional input schemas coalesce missing arguments via `input ?? {}` so a no-args MCP invocation can't crash on destructure before reaching the session-existence check. * New tools: `list_pages`, `add_page`, `rename_page`, `delete_page`. * Page-targeted PNG/SVG export uses a "load + export + restore" dance: the server projects the target page into a single-page <mxfile>, pushes it into the transient state so the browser reloads the iframe with just that page, waits for drawio to render (~3s), triggers the export, captures the data, and then restores the original multi-page document. The dance is wrapped in `try/finally` so the restore runs unconditionally — even if an exception is thrown mid-dance, the user's multi-tab view is recovered before the function returns. The earlier attempt to use drawio's `selectPage` postMessage was a no-op because drawio's JSON embed protocol does not expose that action — silently exporting whatever tab happened to be active. The load-export-restore approach trades a brief visible tab-flicker for correctness: the exported image is guaranteed to match the requested page. * Tool description strings reflect the multi-page semantics so the LLM client learns the new contract. * Package version bumped 0.2.0 → 0.3.0 (additive surface — four new tools, three extended input schemas, canonical XML shape change). * CI: `.github/workflows/test.yml` gains an explicit install + vitest run for the mcp-server package so the new multi-page invariants are covered by automation, not just local runs. Backward compatibility: every existing single-page caller continues to work without modification. The session.xml shape is normalised on every write, removing the wrapper-injection hack from the .drawio download path. Tests: 43 unit tests under `packages/mcp-server/tests/multi-page.test.ts` pin the validator's mxfile path, the page-scoped operations, the XML declaration-prefix handling for both normalizeToMxfile and addPageToDoc, addPageToDoc's rejection of full <mxfile> inputs, the single-page projection used by export_diagram (a direct regression test for the selectPage bug — two distinct page selectors must produce visually different projections), and the Transformer + CNN motivating scenario. A `tests/smoke.mjs` smoke test drives the built `dist/index.js` over JSON-RPC and asserts all 9 tools register with the right input schemas. Root vitest suite (107 tests) still green. * fix(mcp): rewrite page-targeted export browser-side; harden edit/get The page-targeted PNG/SVG export never worked: export_diagram swapped the live session to a single-page projection, slept 3s, then wrote the export flag onto a state object that setState() had already replaced in the store Map — so the browser never saw the request and every such export timed out. The swap+restore also clobbered concurrent edits. Move the projection entirely browser-side: requestExport() hands a single -page <mxfile> to the bridge via state.exportXml; the bridge loads it, lets draw.io render, exports, then reloads the user's real document. The canonical session state is never mutated, so there is no restore race and no fixed-delay guessing. The export poll now re-reads the live store entry each tick instead of a captured reference. autosave is suppressed and the version-bump reload is skipped while a projection is on screen; if no real document was captured, restore forces a server reload rather than leaving the iframe stuck on the projection. Also: - edit_diagram now returns isError on a page-level failure (selector matched no page / page has no <root>) instead of reporting success-with-warnings and persisting a no-op; the pre-edit history snapshot is taken only after that gate so a failed edit leaves no phantom undo entry. - edit_diagram/get_diagram re-normalise browser-pushed xml to mxfile so a bare <mxGraphModel> can't silently strip a multi-page document. - get_diagram now errors (instead of silently returning the full doc) when a selector is given but the session isn't a parseable mxfile. - page_id / page_name / add_page.id get .min(1) so empty strings can't silently target the first page. - Extract pages.ts:projectPage(), collapsing three copies of the parse→find→serialise projection logic in index.ts. - Replace the never-in-CI tests/smoke.mjs with tests/server-wiring.test.ts, which boots the server from source via tsx and runs under the existing vitest CI step. * chore(mcp): set version to 0.2.1 for release --------- Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2026-06-16 05:45:50 +05:30
const pageSelector = pickPageSelector({
page_id,
page_name,
page_index,
})
log.info(
`Editing diagram with ${operations.length} operation(s) on ${describeSelector(pageSelector)}`,
)
// Validate and auto-fix new_xml for each operation
const validatedOps = operations.map((op) => {
if (op.new_xml) {
const { valid, error, fixed, fixes } = validateAndFixXml(
op.new_xml,
)
if (fixed) {
log.info(
`Operation ${op.operation} ${op.cell_id}: XML auto-fixed: ${fixes.join(", ")}`,
)
return { ...op, new_xml: fixed }
}
if (!valid && error) {
log.warn(
`Operation ${op.operation} ${op.cell_id}: XML validation failed: ${error}`,
)
}
}
return op
})
feat(mcp): add multi-page (mxfile) support to MCP server (#862) * feat(mcp): add multi-page (mxfile) support The MCP server's write path could only address a single drawio page even though the underlying .drawio file format and the embedded editor both natively support multi-page documents. A user asking for "a second page with a CNN diagram" would hit the validator with the error "Expected closing tag </root> but found </mxCell>" because the validator assumed input was a bare <mxGraphModel> and could not walk past the <mxfile><diagram>...</diagram></mxfile> wrapper. This patch closes the gap end to end: * New helper module `pages.ts` centralises page CRUD (normalize, parse, list, find, add, rename, delete) so every layer agrees that the canonical in-memory shape is always <mxfile>. normalizeToMxfile and addPageToDoc both strip any leading <?xml ?> declaration before embedding a fragment inside <diagram> (the declaration is only valid at document start). addPageToDoc explicitly rejects full <mxfile> inputs so a caller cannot accidentally nest a document inside a page. * `xml-validation.ts` now detects an <mxfile> root and scopes the duplicate-id check per <diagram>. The legacy regex check would otherwise reject every multi-page doc, because cells "0" and "1" repeat in each page's <root> by design. The DOM-parse path is gated by a cheap regex pre-check so legacy bare <mxGraphModel> callers don't pay any extra cost. The autoFix duplicate-id rename step is also guarded against mxfile inputs — renaming those sentinel cells would silently break drawio's parent references. * `diagram-operations.ts` accepts an optional PageSelector. For <mxfile> input it resolves the page first and scopes all querySelectorAll calls to that page's <root>, so a delete on page 2's cell "2" no longer touches page 1's cell "2". * `create_new_diagram` accepts either a bare <mxGraphModel> (legacy, auto-wrapped into a single-page mxfile) or a full <mxfile> with N diagrams. All existing single-page callers keep working unchanged. * `edit_diagram`, `get_diagram`, and `export_diagram` gain optional `page_id` / `page_name` / `page_index` parameters. When omitted they target the first page — the "active by convention" default. Tool handlers with all-optional input schemas coalesce missing arguments via `input ?? {}` so a no-args MCP invocation can't crash on destructure before reaching the session-existence check. * New tools: `list_pages`, `add_page`, `rename_page`, `delete_page`. * Page-targeted PNG/SVG export uses a "load + export + restore" dance: the server projects the target page into a single-page <mxfile>, pushes it into the transient state so the browser reloads the iframe with just that page, waits for drawio to render (~3s), triggers the export, captures the data, and then restores the original multi-page document. The dance is wrapped in `try/finally` so the restore runs unconditionally — even if an exception is thrown mid-dance, the user's multi-tab view is recovered before the function returns. The earlier attempt to use drawio's `selectPage` postMessage was a no-op because drawio's JSON embed protocol does not expose that action — silently exporting whatever tab happened to be active. The load-export-restore approach trades a brief visible tab-flicker for correctness: the exported image is guaranteed to match the requested page. * Tool description strings reflect the multi-page semantics so the LLM client learns the new contract. * Package version bumped 0.2.0 → 0.3.0 (additive surface — four new tools, three extended input schemas, canonical XML shape change). * CI: `.github/workflows/test.yml` gains an explicit install + vitest run for the mcp-server package so the new multi-page invariants are covered by automation, not just local runs. Backward compatibility: every existing single-page caller continues to work without modification. The session.xml shape is normalised on every write, removing the wrapper-injection hack from the .drawio download path. Tests: 43 unit tests under `packages/mcp-server/tests/multi-page.test.ts` pin the validator's mxfile path, the page-scoped operations, the XML declaration-prefix handling for both normalizeToMxfile and addPageToDoc, addPageToDoc's rejection of full <mxfile> inputs, the single-page projection used by export_diagram (a direct regression test for the selectPage bug — two distinct page selectors must produce visually different projections), and the Transformer + CNN motivating scenario. A `tests/smoke.mjs` smoke test drives the built `dist/index.js` over JSON-RPC and asserts all 9 tools register with the right input schemas. Root vitest suite (107 tests) still green. * fix(mcp): rewrite page-targeted export browser-side; harden edit/get The page-targeted PNG/SVG export never worked: export_diagram swapped the live session to a single-page projection, slept 3s, then wrote the export flag onto a state object that setState() had already replaced in the store Map — so the browser never saw the request and every such export timed out. The swap+restore also clobbered concurrent edits. Move the projection entirely browser-side: requestExport() hands a single -page <mxfile> to the bridge via state.exportXml; the bridge loads it, lets draw.io render, exports, then reloads the user's real document. The canonical session state is never mutated, so there is no restore race and no fixed-delay guessing. The export poll now re-reads the live store entry each tick instead of a captured reference. autosave is suppressed and the version-bump reload is skipped while a projection is on screen; if no real document was captured, restore forces a server reload rather than leaving the iframe stuck on the projection. Also: - edit_diagram now returns isError on a page-level failure (selector matched no page / page has no <root>) instead of reporting success-with-warnings and persisting a no-op; the pre-edit history snapshot is taken only after that gate so a failed edit leaves no phantom undo entry. - edit_diagram/get_diagram re-normalise browser-pushed xml to mxfile so a bare <mxGraphModel> can't silently strip a multi-page document. - get_diagram now errors (instead of silently returning the full doc) when a selector is given but the session isn't a parseable mxfile. - page_id / page_name / add_page.id get .min(1) so empty strings can't silently target the first page. - Extract pages.ts:projectPage(), collapsing three copies of the parse→find→serialise projection logic in index.ts. - Replace the never-in-CI tests/smoke.mjs with tests/server-wiring.test.ts, which boots the server from source via tsx and runs under the existing vitest CI step. * chore(mcp): set version to 0.2.1 for release --------- Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2026-06-16 05:45:50 +05:30
// Apply operations on the targeted page
const { result, errors } = applyDiagramOperations(
currentSession.xml,
validatedOps as DiagramOperation[],
feat(mcp): add multi-page (mxfile) support to MCP server (#862) * feat(mcp): add multi-page (mxfile) support The MCP server's write path could only address a single drawio page even though the underlying .drawio file format and the embedded editor both natively support multi-page documents. A user asking for "a second page with a CNN diagram" would hit the validator with the error "Expected closing tag </root> but found </mxCell>" because the validator assumed input was a bare <mxGraphModel> and could not walk past the <mxfile><diagram>...</diagram></mxfile> wrapper. This patch closes the gap end to end: * New helper module `pages.ts` centralises page CRUD (normalize, parse, list, find, add, rename, delete) so every layer agrees that the canonical in-memory shape is always <mxfile>. normalizeToMxfile and addPageToDoc both strip any leading <?xml ?> declaration before embedding a fragment inside <diagram> (the declaration is only valid at document start). addPageToDoc explicitly rejects full <mxfile> inputs so a caller cannot accidentally nest a document inside a page. * `xml-validation.ts` now detects an <mxfile> root and scopes the duplicate-id check per <diagram>. The legacy regex check would otherwise reject every multi-page doc, because cells "0" and "1" repeat in each page's <root> by design. The DOM-parse path is gated by a cheap regex pre-check so legacy bare <mxGraphModel> callers don't pay any extra cost. The autoFix duplicate-id rename step is also guarded against mxfile inputs — renaming those sentinel cells would silently break drawio's parent references. * `diagram-operations.ts` accepts an optional PageSelector. For <mxfile> input it resolves the page first and scopes all querySelectorAll calls to that page's <root>, so a delete on page 2's cell "2" no longer touches page 1's cell "2". * `create_new_diagram` accepts either a bare <mxGraphModel> (legacy, auto-wrapped into a single-page mxfile) or a full <mxfile> with N diagrams. All existing single-page callers keep working unchanged. * `edit_diagram`, `get_diagram`, and `export_diagram` gain optional `page_id` / `page_name` / `page_index` parameters. When omitted they target the first page — the "active by convention" default. Tool handlers with all-optional input schemas coalesce missing arguments via `input ?? {}` so a no-args MCP invocation can't crash on destructure before reaching the session-existence check. * New tools: `list_pages`, `add_page`, `rename_page`, `delete_page`. * Page-targeted PNG/SVG export uses a "load + export + restore" dance: the server projects the target page into a single-page <mxfile>, pushes it into the transient state so the browser reloads the iframe with just that page, waits for drawio to render (~3s), triggers the export, captures the data, and then restores the original multi-page document. The dance is wrapped in `try/finally` so the restore runs unconditionally — even if an exception is thrown mid-dance, the user's multi-tab view is recovered before the function returns. The earlier attempt to use drawio's `selectPage` postMessage was a no-op because drawio's JSON embed protocol does not expose that action — silently exporting whatever tab happened to be active. The load-export-restore approach trades a brief visible tab-flicker for correctness: the exported image is guaranteed to match the requested page. * Tool description strings reflect the multi-page semantics so the LLM client learns the new contract. * Package version bumped 0.2.0 → 0.3.0 (additive surface — four new tools, three extended input schemas, canonical XML shape change). * CI: `.github/workflows/test.yml` gains an explicit install + vitest run for the mcp-server package so the new multi-page invariants are covered by automation, not just local runs. Backward compatibility: every existing single-page caller continues to work without modification. The session.xml shape is normalised on every write, removing the wrapper-injection hack from the .drawio download path. Tests: 43 unit tests under `packages/mcp-server/tests/multi-page.test.ts` pin the validator's mxfile path, the page-scoped operations, the XML declaration-prefix handling for both normalizeToMxfile and addPageToDoc, addPageToDoc's rejection of full <mxfile> inputs, the single-page projection used by export_diagram (a direct regression test for the selectPage bug — two distinct page selectors must produce visually different projections), and the Transformer + CNN motivating scenario. A `tests/smoke.mjs` smoke test drives the built `dist/index.js` over JSON-RPC and asserts all 9 tools register with the right input schemas. Root vitest suite (107 tests) still green. * fix(mcp): rewrite page-targeted export browser-side; harden edit/get The page-targeted PNG/SVG export never worked: export_diagram swapped the live session to a single-page projection, slept 3s, then wrote the export flag onto a state object that setState() had already replaced in the store Map — so the browser never saw the request and every such export timed out. The swap+restore also clobbered concurrent edits. Move the projection entirely browser-side: requestExport() hands a single -page <mxfile> to the bridge via state.exportXml; the bridge loads it, lets draw.io render, exports, then reloads the user's real document. The canonical session state is never mutated, so there is no restore race and no fixed-delay guessing. The export poll now re-reads the live store entry each tick instead of a captured reference. autosave is suppressed and the version-bump reload is skipped while a projection is on screen; if no real document was captured, restore forces a server reload rather than leaving the iframe stuck on the projection. Also: - edit_diagram now returns isError on a page-level failure (selector matched no page / page has no <root>) instead of reporting success-with-warnings and persisting a no-op; the pre-edit history snapshot is taken only after that gate so a failed edit leaves no phantom undo entry. - edit_diagram/get_diagram re-normalise browser-pushed xml to mxfile so a bare <mxGraphModel> can't silently strip a multi-page document. - get_diagram now errors (instead of silently returning the full doc) when a selector is given but the session isn't a parseable mxfile. - page_id / page_name / add_page.id get .min(1) so empty strings can't silently target the first page. - Extract pages.ts:projectPage(), collapsing three copies of the parse→find→serialise projection logic in index.ts. - Replace the never-in-CI tests/smoke.mjs with tests/server-wiring.test.ts, which boots the server from source via tsx and runs under the existing vitest CI step. * chore(mcp): set version to 0.2.1 for release --------- Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2026-06-16 05:45:50 +05:30
pageSelector,
)
if (errors.length > 0) {
const errorMessages = errors
.map((e) => `${e.type} ${e.cellId}: ${e.message}`)
.join("\n")
log.warn(`Edit had ${errors.length} error(s): ${errorMessages}`)
}
feat(mcp): add multi-page (mxfile) support to MCP server (#862) * feat(mcp): add multi-page (mxfile) support The MCP server's write path could only address a single drawio page even though the underlying .drawio file format and the embedded editor both natively support multi-page documents. A user asking for "a second page with a CNN diagram" would hit the validator with the error "Expected closing tag </root> but found </mxCell>" because the validator assumed input was a bare <mxGraphModel> and could not walk past the <mxfile><diagram>...</diagram></mxfile> wrapper. This patch closes the gap end to end: * New helper module `pages.ts` centralises page CRUD (normalize, parse, list, find, add, rename, delete) so every layer agrees that the canonical in-memory shape is always <mxfile>. normalizeToMxfile and addPageToDoc both strip any leading <?xml ?> declaration before embedding a fragment inside <diagram> (the declaration is only valid at document start). addPageToDoc explicitly rejects full <mxfile> inputs so a caller cannot accidentally nest a document inside a page. * `xml-validation.ts` now detects an <mxfile> root and scopes the duplicate-id check per <diagram>. The legacy regex check would otherwise reject every multi-page doc, because cells "0" and "1" repeat in each page's <root> by design. The DOM-parse path is gated by a cheap regex pre-check so legacy bare <mxGraphModel> callers don't pay any extra cost. The autoFix duplicate-id rename step is also guarded against mxfile inputs — renaming those sentinel cells would silently break drawio's parent references. * `diagram-operations.ts` accepts an optional PageSelector. For <mxfile> input it resolves the page first and scopes all querySelectorAll calls to that page's <root>, so a delete on page 2's cell "2" no longer touches page 1's cell "2". * `create_new_diagram` accepts either a bare <mxGraphModel> (legacy, auto-wrapped into a single-page mxfile) or a full <mxfile> with N diagrams. All existing single-page callers keep working unchanged. * `edit_diagram`, `get_diagram`, and `export_diagram` gain optional `page_id` / `page_name` / `page_index` parameters. When omitted they target the first page — the "active by convention" default. Tool handlers with all-optional input schemas coalesce missing arguments via `input ?? {}` so a no-args MCP invocation can't crash on destructure before reaching the session-existence check. * New tools: `list_pages`, `add_page`, `rename_page`, `delete_page`. * Page-targeted PNG/SVG export uses a "load + export + restore" dance: the server projects the target page into a single-page <mxfile>, pushes it into the transient state so the browser reloads the iframe with just that page, waits for drawio to render (~3s), triggers the export, captures the data, and then restores the original multi-page document. The dance is wrapped in `try/finally` so the restore runs unconditionally — even if an exception is thrown mid-dance, the user's multi-tab view is recovered before the function returns. The earlier attempt to use drawio's `selectPage` postMessage was a no-op because drawio's JSON embed protocol does not expose that action — silently exporting whatever tab happened to be active. The load-export-restore approach trades a brief visible tab-flicker for correctness: the exported image is guaranteed to match the requested page. * Tool description strings reflect the multi-page semantics so the LLM client learns the new contract. * Package version bumped 0.2.0 → 0.3.0 (additive surface — four new tools, three extended input schemas, canonical XML shape change). * CI: `.github/workflows/test.yml` gains an explicit install + vitest run for the mcp-server package so the new multi-page invariants are covered by automation, not just local runs. Backward compatibility: every existing single-page caller continues to work without modification. The session.xml shape is normalised on every write, removing the wrapper-injection hack from the .drawio download path. Tests: 43 unit tests under `packages/mcp-server/tests/multi-page.test.ts` pin the validator's mxfile path, the page-scoped operations, the XML declaration-prefix handling for both normalizeToMxfile and addPageToDoc, addPageToDoc's rejection of full <mxfile> inputs, the single-page projection used by export_diagram (a direct regression test for the selectPage bug — two distinct page selectors must produce visually different projections), and the Transformer + CNN motivating scenario. A `tests/smoke.mjs` smoke test drives the built `dist/index.js` over JSON-RPC and asserts all 9 tools register with the right input schemas. Root vitest suite (107 tests) still green. * fix(mcp): rewrite page-targeted export browser-side; harden edit/get The page-targeted PNG/SVG export never worked: export_diagram swapped the live session to a single-page projection, slept 3s, then wrote the export flag onto a state object that setState() had already replaced in the store Map — so the browser never saw the request and every such export timed out. The swap+restore also clobbered concurrent edits. Move the projection entirely browser-side: requestExport() hands a single -page <mxfile> to the bridge via state.exportXml; the bridge loads it, lets draw.io render, exports, then reloads the user's real document. The canonical session state is never mutated, so there is no restore race and no fixed-delay guessing. The export poll now re-reads the live store entry each tick instead of a captured reference. autosave is suppressed and the version-bump reload is skipped while a projection is on screen; if no real document was captured, restore forces a server reload rather than leaving the iframe stuck on the projection. Also: - edit_diagram now returns isError on a page-level failure (selector matched no page / page has no <root>) instead of reporting success-with-warnings and persisting a no-op; the pre-edit history snapshot is taken only after that gate so a failed edit leaves no phantom undo entry. - edit_diagram/get_diagram re-normalise browser-pushed xml to mxfile so a bare <mxGraphModel> can't silently strip a multi-page document. - get_diagram now errors (instead of silently returning the full doc) when a selector is given but the session isn't a parseable mxfile. - page_id / page_name / add_page.id get .min(1) so empty strings can't silently target the first page. - Extract pages.ts:projectPage(), collapsing three copies of the parse→find→serialise projection logic in index.ts. - Replace the never-in-CI tests/smoke.mjs with tests/server-wiring.test.ts, which boots the server from source via tsx and runs under the existing vitest CI step. * chore(mcp): set version to 0.2.1 for release --------- Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2026-06-16 05:45:50 +05:30
// A page-level error (empty cellId — e.g. the selector matched no
// page, or the page had no <root>) means NOTHING was applied and
// `result` is the unchanged input. Surface it as a hard error
// instead of persisting a no-op and reporting success, so the
// caller doesn't build on a wrong assumption.
const pageError = errors.find((e) => e.cellId === "")
if (pageError) {
return {
content: [
{
type: "text",
text: `Error: ${pageError.message}`,
},
],
isError: true,
}
}
// Save the pre-edit state for undo (with cached SVG from browser).
// Done only now that we know the edit applied — a page-level error
// returns above without leaving a phantom history entry.
addHistory(
currentSession.id,
currentSession.xml,
browserState?.svg || "",
)
// Update state
currentSession.xml = result
currentSession.version++
fix(mcp): replace edit_diagram 30s time gate with content comparison (#890) * fix(mcp): keep diagram context valid during edits Closes #885 * fix(mcp): replace edit_diagram time gate with content comparison The 30s wall-clock gate rejected slow-but-correct clients (#885). Instead of a timeout, remember the exact state-store XML the model last saw (get_diagram / create_new_diagram / edit_diagram / page CRUD) and reject edit_diagram only when the live browser state differs - i.e. the user made edits the model hasn't seen yet. Slow reasoning no longer trips the gate, while unseen manual edits still do. * docs(mcp): align edit_diagram/get_diagram descriptions with content-based gate The 'You MUST call get_diagram BEFORE this tool' requirement and the 'Skipping get_diagram WILL cause user's changes to be LOST' warning no longer match server behavior: a stale edit is rejected with no side effects, never silently applied. Describe the freshness check instead, and direct get_diagram usage at its real purpose - learning the current diagram content when the model doesn't already know it. * fix(mcp): compare diagram content structurally in the edit gate draw.io re-serialises the document when pushing state back (attribute order, pretty-printing, regenerated diagram ids, viewport attributes, mxfile host), so byte comparison could flag an unchanged diagram as stale. Fingerprint what a user can actually change instead - page set, page names, and each page's root cell tree with sorted attributes - keeping byte equality as the fast path. A bare mxGraphModel now also fingerprints identically to its single-page mxfile wrapping. * fix(mcp): don't compare page names against bare mxGraphModel pushes A bare <mxGraphModel> pushed by the embed/sync path carries no page name, so normalizeToMxfile invents "Page-1" — falsely reading any custom page name as a content change and re-triggering the stale rejection on every edit. When either side of the gate comparison is a bare mxGraphModel, fingerprint cell trees only; full-mxfile comparisons still detect renames. * chore(mcp): bump version to 0.2.2 * chore(mcp): sync package-lock.json version to 0.2.2 --------- Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2026-07-12 13:33:20 +07:00
// Push to embedded server; the pushed XML is now the latest
// state the model has seen.
setState(currentSession.id, result)
fix(mcp): replace edit_diagram 30s time gate with content comparison (#890) * fix(mcp): keep diagram context valid during edits Closes #885 * fix(mcp): replace edit_diagram time gate with content comparison The 30s wall-clock gate rejected slow-but-correct clients (#885). Instead of a timeout, remember the exact state-store XML the model last saw (get_diagram / create_new_diagram / edit_diagram / page CRUD) and reject edit_diagram only when the live browser state differs - i.e. the user made edits the model hasn't seen yet. Slow reasoning no longer trips the gate, while unseen manual edits still do. * docs(mcp): align edit_diagram/get_diagram descriptions with content-based gate The 'You MUST call get_diagram BEFORE this tool' requirement and the 'Skipping get_diagram WILL cause user's changes to be LOST' warning no longer match server behavior: a stale edit is rejected with no side effects, never silently applied. Describe the freshness check instead, and direct get_diagram usage at its real purpose - learning the current diagram content when the model doesn't already know it. * fix(mcp): compare diagram content structurally in the edit gate draw.io re-serialises the document when pushing state back (attribute order, pretty-printing, regenerated diagram ids, viewport attributes, mxfile host), so byte comparison could flag an unchanged diagram as stale. Fingerprint what a user can actually change instead - page set, page names, and each page's root cell tree with sorted attributes - keeping byte equality as the fast path. A bare mxGraphModel now also fingerprints identically to its single-page mxfile wrapping. * fix(mcp): don't compare page names against bare mxGraphModel pushes A bare <mxGraphModel> pushed by the embed/sync path carries no page name, so normalizeToMxfile invents "Page-1" — falsely reading any custom page name as a content change and re-triggering the stale rejection on every edit. When either side of the gate comparison is a bare mxGraphModel, fingerprint cell trees only; full-mxfile comparisons still detect renames. * chore(mcp): bump version to 0.2.2 * chore(mcp): sync package-lock.json version to 0.2.2 --------- Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2026-07-12 13:33:20 +07:00
currentSession.lastSeenXml = result
// Save AI result (no SVG yet - will be captured by browser)
addHistory(currentSession.id, result, "")
log.info(`Diagram edited successfully`)
feat(mcp): add multi-page (mxfile) support to MCP server (#862) * feat(mcp): add multi-page (mxfile) support The MCP server's write path could only address a single drawio page even though the underlying .drawio file format and the embedded editor both natively support multi-page documents. A user asking for "a second page with a CNN diagram" would hit the validator with the error "Expected closing tag </root> but found </mxCell>" because the validator assumed input was a bare <mxGraphModel> and could not walk past the <mxfile><diagram>...</diagram></mxfile> wrapper. This patch closes the gap end to end: * New helper module `pages.ts` centralises page CRUD (normalize, parse, list, find, add, rename, delete) so every layer agrees that the canonical in-memory shape is always <mxfile>. normalizeToMxfile and addPageToDoc both strip any leading <?xml ?> declaration before embedding a fragment inside <diagram> (the declaration is only valid at document start). addPageToDoc explicitly rejects full <mxfile> inputs so a caller cannot accidentally nest a document inside a page. * `xml-validation.ts` now detects an <mxfile> root and scopes the duplicate-id check per <diagram>. The legacy regex check would otherwise reject every multi-page doc, because cells "0" and "1" repeat in each page's <root> by design. The DOM-parse path is gated by a cheap regex pre-check so legacy bare <mxGraphModel> callers don't pay any extra cost. The autoFix duplicate-id rename step is also guarded against mxfile inputs — renaming those sentinel cells would silently break drawio's parent references. * `diagram-operations.ts` accepts an optional PageSelector. For <mxfile> input it resolves the page first and scopes all querySelectorAll calls to that page's <root>, so a delete on page 2's cell "2" no longer touches page 1's cell "2". * `create_new_diagram` accepts either a bare <mxGraphModel> (legacy, auto-wrapped into a single-page mxfile) or a full <mxfile> with N diagrams. All existing single-page callers keep working unchanged. * `edit_diagram`, `get_diagram`, and `export_diagram` gain optional `page_id` / `page_name` / `page_index` parameters. When omitted they target the first page — the "active by convention" default. Tool handlers with all-optional input schemas coalesce missing arguments via `input ?? {}` so a no-args MCP invocation can't crash on destructure before reaching the session-existence check. * New tools: `list_pages`, `add_page`, `rename_page`, `delete_page`. * Page-targeted PNG/SVG export uses a "load + export + restore" dance: the server projects the target page into a single-page <mxfile>, pushes it into the transient state so the browser reloads the iframe with just that page, waits for drawio to render (~3s), triggers the export, captures the data, and then restores the original multi-page document. The dance is wrapped in `try/finally` so the restore runs unconditionally — even if an exception is thrown mid-dance, the user's multi-tab view is recovered before the function returns. The earlier attempt to use drawio's `selectPage` postMessage was a no-op because drawio's JSON embed protocol does not expose that action — silently exporting whatever tab happened to be active. The load-export-restore approach trades a brief visible tab-flicker for correctness: the exported image is guaranteed to match the requested page. * Tool description strings reflect the multi-page semantics so the LLM client learns the new contract. * Package version bumped 0.2.0 → 0.3.0 (additive surface — four new tools, three extended input schemas, canonical XML shape change). * CI: `.github/workflows/test.yml` gains an explicit install + vitest run for the mcp-server package so the new multi-page invariants are covered by automation, not just local runs. Backward compatibility: every existing single-page caller continues to work without modification. The session.xml shape is normalised on every write, removing the wrapper-injection hack from the .drawio download path. Tests: 43 unit tests under `packages/mcp-server/tests/multi-page.test.ts` pin the validator's mxfile path, the page-scoped operations, the XML declaration-prefix handling for both normalizeToMxfile and addPageToDoc, addPageToDoc's rejection of full <mxfile> inputs, the single-page projection used by export_diagram (a direct regression test for the selectPage bug — two distinct page selectors must produce visually different projections), and the Transformer + CNN motivating scenario. A `tests/smoke.mjs` smoke test drives the built `dist/index.js` over JSON-RPC and asserts all 9 tools register with the right input schemas. Root vitest suite (107 tests) still green. * fix(mcp): rewrite page-targeted export browser-side; harden edit/get The page-targeted PNG/SVG export never worked: export_diagram swapped the live session to a single-page projection, slept 3s, then wrote the export flag onto a state object that setState() had already replaced in the store Map — so the browser never saw the request and every such export timed out. The swap+restore also clobbered concurrent edits. Move the projection entirely browser-side: requestExport() hands a single -page <mxfile> to the bridge via state.exportXml; the bridge loads it, lets draw.io render, exports, then reloads the user's real document. The canonical session state is never mutated, so there is no restore race and no fixed-delay guessing. The export poll now re-reads the live store entry each tick instead of a captured reference. autosave is suppressed and the version-bump reload is skipped while a projection is on screen; if no real document was captured, restore forces a server reload rather than leaving the iframe stuck on the projection. Also: - edit_diagram now returns isError on a page-level failure (selector matched no page / page has no <root>) instead of reporting success-with-warnings and persisting a no-op; the pre-edit history snapshot is taken only after that gate so a failed edit leaves no phantom undo entry. - edit_diagram/get_diagram re-normalise browser-pushed xml to mxfile so a bare <mxGraphModel> can't silently strip a multi-page document. - get_diagram now errors (instead of silently returning the full doc) when a selector is given but the session isn't a parseable mxfile. - page_id / page_name / add_page.id get .min(1) so empty strings can't silently target the first page. - Extract pages.ts:projectPage(), collapsing three copies of the parse→find→serialise projection logic in index.ts. - Replace the never-in-CI tests/smoke.mjs with tests/server-wiring.test.ts, which boots the server from source via tsx and runs under the existing vitest CI step. * chore(mcp): set version to 0.2.1 for release --------- Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2026-06-16 05:45:50 +05:30
const successMsg = `Diagram edited successfully!\n\nApplied ${operations.length} operation(s) on ${describeSelector(pageSelector)}.`
const errorMsg =
errors.length > 0
? `\n\nWarnings:\n${errors.map((e) => `- ${e.type} ${e.cellId}: ${e.message}`).join("\n")}`
: ""
return {
content: [
{
type: "text",
text: successMsg + errorMsg,
},
],
}
} catch (error) {
const message =
error instanceof Error ? error.message : String(error)
log.error("edit_diagram failed:", message)
return {
content: [{ type: "text", text: `Error: ${message}` }],
isError: true,
}
}
},
)
// Tool: get_diagram
server.registerTool(
"get_diagram",
{
description:
"Get the current diagram XML (fetches latest from browser, including user's manual edits). " +
fix(mcp): replace edit_diagram 30s time gate with content comparison (#890) * fix(mcp): keep diagram context valid during edits Closes #885 * fix(mcp): replace edit_diagram time gate with content comparison The 30s wall-clock gate rejected slow-but-correct clients (#885). Instead of a timeout, remember the exact state-store XML the model last saw (get_diagram / create_new_diagram / edit_diagram / page CRUD) and reject edit_diagram only when the live browser state differs - i.e. the user made edits the model hasn't seen yet. Slow reasoning no longer trips the gate, while unseen manual edits still do. * docs(mcp): align edit_diagram/get_diagram descriptions with content-based gate The 'You MUST call get_diagram BEFORE this tool' requirement and the 'Skipping get_diagram WILL cause user's changes to be LOST' warning no longer match server behavior: a stale edit is rejected with no side effects, never silently applied. Describe the freshness check instead, and direct get_diagram usage at its real purpose - learning the current diagram content when the model doesn't already know it. * fix(mcp): compare diagram content structurally in the edit gate draw.io re-serialises the document when pushing state back (attribute order, pretty-printing, regenerated diagram ids, viewport attributes, mxfile host), so byte comparison could flag an unchanged diagram as stale. Fingerprint what a user can actually change instead - page set, page names, and each page's root cell tree with sorted attributes - keeping byte equality as the fast path. A bare mxGraphModel now also fingerprints identically to its single-page mxfile wrapping. * fix(mcp): don't compare page names against bare mxGraphModel pushes A bare <mxGraphModel> pushed by the embed/sync path carries no page name, so normalizeToMxfile invents "Page-1" — falsely reading any custom page name as a content change and re-triggering the stale rejection on every edit. When either side of the gate comparison is a bare mxGraphModel, fingerprint cell trees only; full-mxfile comparisons still detect renames. * chore(mcp): bump version to 0.2.2 * chore(mcp): sync package-lock.json version to 0.2.2 --------- Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2026-07-12 13:33:20 +07:00
"Call this when you don't know the current diagram content (cell IDs, pages, structure) — " +
"e.g. before editing a diagram you didn't create in this conversation, or after edit_diagram " +
"was rejected because the user changed the diagram in the browser.\n\n" +
feat(mcp): add multi-page (mxfile) support to MCP server (#862) * feat(mcp): add multi-page (mxfile) support The MCP server's write path could only address a single drawio page even though the underlying .drawio file format and the embedded editor both natively support multi-page documents. A user asking for "a second page with a CNN diagram" would hit the validator with the error "Expected closing tag </root> but found </mxCell>" because the validator assumed input was a bare <mxGraphModel> and could not walk past the <mxfile><diagram>...</diagram></mxfile> wrapper. This patch closes the gap end to end: * New helper module `pages.ts` centralises page CRUD (normalize, parse, list, find, add, rename, delete) so every layer agrees that the canonical in-memory shape is always <mxfile>. normalizeToMxfile and addPageToDoc both strip any leading <?xml ?> declaration before embedding a fragment inside <diagram> (the declaration is only valid at document start). addPageToDoc explicitly rejects full <mxfile> inputs so a caller cannot accidentally nest a document inside a page. * `xml-validation.ts` now detects an <mxfile> root and scopes the duplicate-id check per <diagram>. The legacy regex check would otherwise reject every multi-page doc, because cells "0" and "1" repeat in each page's <root> by design. The DOM-parse path is gated by a cheap regex pre-check so legacy bare <mxGraphModel> callers don't pay any extra cost. The autoFix duplicate-id rename step is also guarded against mxfile inputs — renaming those sentinel cells would silently break drawio's parent references. * `diagram-operations.ts` accepts an optional PageSelector. For <mxfile> input it resolves the page first and scopes all querySelectorAll calls to that page's <root>, so a delete on page 2's cell "2" no longer touches page 1's cell "2". * `create_new_diagram` accepts either a bare <mxGraphModel> (legacy, auto-wrapped into a single-page mxfile) or a full <mxfile> with N diagrams. All existing single-page callers keep working unchanged. * `edit_diagram`, `get_diagram`, and `export_diagram` gain optional `page_id` / `page_name` / `page_index` parameters. When omitted they target the first page — the "active by convention" default. Tool handlers with all-optional input schemas coalesce missing arguments via `input ?? {}` so a no-args MCP invocation can't crash on destructure before reaching the session-existence check. * New tools: `list_pages`, `add_page`, `rename_page`, `delete_page`. * Page-targeted PNG/SVG export uses a "load + export + restore" dance: the server projects the target page into a single-page <mxfile>, pushes it into the transient state so the browser reloads the iframe with just that page, waits for drawio to render (~3s), triggers the export, captures the data, and then restores the original multi-page document. The dance is wrapped in `try/finally` so the restore runs unconditionally — even if an exception is thrown mid-dance, the user's multi-tab view is recovered before the function returns. The earlier attempt to use drawio's `selectPage` postMessage was a no-op because drawio's JSON embed protocol does not expose that action — silently exporting whatever tab happened to be active. The load-export-restore approach trades a brief visible tab-flicker for correctness: the exported image is guaranteed to match the requested page. * Tool description strings reflect the multi-page semantics so the LLM client learns the new contract. * Package version bumped 0.2.0 → 0.3.0 (additive surface — four new tools, three extended input schemas, canonical XML shape change). * CI: `.github/workflows/test.yml` gains an explicit install + vitest run for the mcp-server package so the new multi-page invariants are covered by automation, not just local runs. Backward compatibility: every existing single-page caller continues to work without modification. The session.xml shape is normalised on every write, removing the wrapper-injection hack from the .drawio download path. Tests: 43 unit tests under `packages/mcp-server/tests/multi-page.test.ts` pin the validator's mxfile path, the page-scoped operations, the XML declaration-prefix handling for both normalizeToMxfile and addPageToDoc, addPageToDoc's rejection of full <mxfile> inputs, the single-page projection used by export_diagram (a direct regression test for the selectPage bug — two distinct page selectors must produce visually different projections), and the Transformer + CNN motivating scenario. A `tests/smoke.mjs` smoke test drives the built `dist/index.js` over JSON-RPC and asserts all 9 tools register with the right input schemas. Root vitest suite (107 tests) still green. * fix(mcp): rewrite page-targeted export browser-side; harden edit/get The page-targeted PNG/SVG export never worked: export_diagram swapped the live session to a single-page projection, slept 3s, then wrote the export flag onto a state object that setState() had already replaced in the store Map — so the browser never saw the request and every such export timed out. The swap+restore also clobbered concurrent edits. Move the projection entirely browser-side: requestExport() hands a single -page <mxfile> to the bridge via state.exportXml; the bridge loads it, lets draw.io render, exports, then reloads the user's real document. The canonical session state is never mutated, so there is no restore race and no fixed-delay guessing. The export poll now re-reads the live store entry each tick instead of a captured reference. autosave is suppressed and the version-bump reload is skipped while a projection is on screen; if no real document was captured, restore forces a server reload rather than leaving the iframe stuck on the projection. Also: - edit_diagram now returns isError on a page-level failure (selector matched no page / page has no <root>) instead of reporting success-with-warnings and persisting a no-op; the pre-edit history snapshot is taken only after that gate so a failed edit leaves no phantom undo entry. - edit_diagram/get_diagram re-normalise browser-pushed xml to mxfile so a bare <mxGraphModel> can't silently strip a multi-page document. - get_diagram now errors (instead of silently returning the full doc) when a selector is given but the session isn't a parseable mxfile. - page_id / page_name / add_page.id get .min(1) so empty strings can't silently target the first page. - Extract pages.ts:projectPage(), collapsing three copies of the parse→find→serialise projection logic in index.ts. - Replace the never-in-CI tests/smoke.mjs with tests/server-wiring.test.ts, which boots the server from source via tsx and runs under the existing vitest CI step. * chore(mcp): set version to 0.2.1 for release --------- Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2026-06-16 05:45:50 +05:30
"Returns the full <mxfile> by default. If a page selector is provided, returns just that page's <mxGraphModel> embedded in a one-page <mxfile> wrapper.",
inputSchema: {
...pageSelectorSchema,
},
},
feat(mcp): add multi-page (mxfile) support to MCP server (#862) * feat(mcp): add multi-page (mxfile) support The MCP server's write path could only address a single drawio page even though the underlying .drawio file format and the embedded editor both natively support multi-page documents. A user asking for "a second page with a CNN diagram" would hit the validator with the error "Expected closing tag </root> but found </mxCell>" because the validator assumed input was a bare <mxGraphModel> and could not walk past the <mxfile><diagram>...</diagram></mxfile> wrapper. This patch closes the gap end to end: * New helper module `pages.ts` centralises page CRUD (normalize, parse, list, find, add, rename, delete) so every layer agrees that the canonical in-memory shape is always <mxfile>. normalizeToMxfile and addPageToDoc both strip any leading <?xml ?> declaration before embedding a fragment inside <diagram> (the declaration is only valid at document start). addPageToDoc explicitly rejects full <mxfile> inputs so a caller cannot accidentally nest a document inside a page. * `xml-validation.ts` now detects an <mxfile> root and scopes the duplicate-id check per <diagram>. The legacy regex check would otherwise reject every multi-page doc, because cells "0" and "1" repeat in each page's <root> by design. The DOM-parse path is gated by a cheap regex pre-check so legacy bare <mxGraphModel> callers don't pay any extra cost. The autoFix duplicate-id rename step is also guarded against mxfile inputs — renaming those sentinel cells would silently break drawio's parent references. * `diagram-operations.ts` accepts an optional PageSelector. For <mxfile> input it resolves the page first and scopes all querySelectorAll calls to that page's <root>, so a delete on page 2's cell "2" no longer touches page 1's cell "2". * `create_new_diagram` accepts either a bare <mxGraphModel> (legacy, auto-wrapped into a single-page mxfile) or a full <mxfile> with N diagrams. All existing single-page callers keep working unchanged. * `edit_diagram`, `get_diagram`, and `export_diagram` gain optional `page_id` / `page_name` / `page_index` parameters. When omitted they target the first page — the "active by convention" default. Tool handlers with all-optional input schemas coalesce missing arguments via `input ?? {}` so a no-args MCP invocation can't crash on destructure before reaching the session-existence check. * New tools: `list_pages`, `add_page`, `rename_page`, `delete_page`. * Page-targeted PNG/SVG export uses a "load + export + restore" dance: the server projects the target page into a single-page <mxfile>, pushes it into the transient state so the browser reloads the iframe with just that page, waits for drawio to render (~3s), triggers the export, captures the data, and then restores the original multi-page document. The dance is wrapped in `try/finally` so the restore runs unconditionally — even if an exception is thrown mid-dance, the user's multi-tab view is recovered before the function returns. The earlier attempt to use drawio's `selectPage` postMessage was a no-op because drawio's JSON embed protocol does not expose that action — silently exporting whatever tab happened to be active. The load-export-restore approach trades a brief visible tab-flicker for correctness: the exported image is guaranteed to match the requested page. * Tool description strings reflect the multi-page semantics so the LLM client learns the new contract. * Package version bumped 0.2.0 → 0.3.0 (additive surface — four new tools, three extended input schemas, canonical XML shape change). * CI: `.github/workflows/test.yml` gains an explicit install + vitest run for the mcp-server package so the new multi-page invariants are covered by automation, not just local runs. Backward compatibility: every existing single-page caller continues to work without modification. The session.xml shape is normalised on every write, removing the wrapper-injection hack from the .drawio download path. Tests: 43 unit tests under `packages/mcp-server/tests/multi-page.test.ts` pin the validator's mxfile path, the page-scoped operations, the XML declaration-prefix handling for both normalizeToMxfile and addPageToDoc, addPageToDoc's rejection of full <mxfile> inputs, the single-page projection used by export_diagram (a direct regression test for the selectPage bug — two distinct page selectors must produce visually different projections), and the Transformer + CNN motivating scenario. A `tests/smoke.mjs` smoke test drives the built `dist/index.js` over JSON-RPC and asserts all 9 tools register with the right input schemas. Root vitest suite (107 tests) still green. * fix(mcp): rewrite page-targeted export browser-side; harden edit/get The page-targeted PNG/SVG export never worked: export_diagram swapped the live session to a single-page projection, slept 3s, then wrote the export flag onto a state object that setState() had already replaced in the store Map — so the browser never saw the request and every such export timed out. The swap+restore also clobbered concurrent edits. Move the projection entirely browser-side: requestExport() hands a single -page <mxfile> to the bridge via state.exportXml; the bridge loads it, lets draw.io render, exports, then reloads the user's real document. The canonical session state is never mutated, so there is no restore race and no fixed-delay guessing. The export poll now re-reads the live store entry each tick instead of a captured reference. autosave is suppressed and the version-bump reload is skipped while a projection is on screen; if no real document was captured, restore forces a server reload rather than leaving the iframe stuck on the projection. Also: - edit_diagram now returns isError on a page-level failure (selector matched no page / page has no <root>) instead of reporting success-with-warnings and persisting a no-op; the pre-edit history snapshot is taken only after that gate so a failed edit leaves no phantom undo entry. - edit_diagram/get_diagram re-normalise browser-pushed xml to mxfile so a bare <mxGraphModel> can't silently strip a multi-page document. - get_diagram now errors (instead of silently returning the full doc) when a selector is given but the session isn't a parseable mxfile. - page_id / page_name / add_page.id get .min(1) so empty strings can't silently target the first page. - Extract pages.ts:projectPage(), collapsing three copies of the parse→find→serialise projection logic in index.ts. - Replace the never-in-CI tests/smoke.mjs with tests/server-wiring.test.ts, which boots the server from source via tsx and runs under the existing vitest CI step. * chore(mcp): set version to 0.2.1 for release --------- Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2026-06-16 05:45:50 +05:30
async (input) => {
// Defensive: when every field is optional an MCP client could in
// principle invoke us with no `arguments` field. The SDK's zod parse
// normally produces `{}` in that case, but we coalesce explicitly so
// a destructure of `undefined` can never throw before we reach the
// session-existence check.
const { page_id, page_name, page_index } = input ?? {}
try {
if (!currentSession) {
return {
content: [
{
type: "text",
text: "Error: No active session. Please call start_session first.",
},
],
isError: true,
}
}
// Request browser to push fresh state and wait for it
const syncRequested = requestSync(currentSession.id)
if (syncRequested) {
const synced = await waitForSync(currentSession.id)
if (!synced) {
log.warn("get_diagram: sync timeout - state may be stale")
}
}
feat(mcp): add multi-page (mxfile) support to MCP server (#862) * feat(mcp): add multi-page (mxfile) support The MCP server's write path could only address a single drawio page even though the underlying .drawio file format and the embedded editor both natively support multi-page documents. A user asking for "a second page with a CNN diagram" would hit the validator with the error "Expected closing tag </root> but found </mxCell>" because the validator assumed input was a bare <mxGraphModel> and could not walk past the <mxfile><diagram>...</diagram></mxfile> wrapper. This patch closes the gap end to end: * New helper module `pages.ts` centralises page CRUD (normalize, parse, list, find, add, rename, delete) so every layer agrees that the canonical in-memory shape is always <mxfile>. normalizeToMxfile and addPageToDoc both strip any leading <?xml ?> declaration before embedding a fragment inside <diagram> (the declaration is only valid at document start). addPageToDoc explicitly rejects full <mxfile> inputs so a caller cannot accidentally nest a document inside a page. * `xml-validation.ts` now detects an <mxfile> root and scopes the duplicate-id check per <diagram>. The legacy regex check would otherwise reject every multi-page doc, because cells "0" and "1" repeat in each page's <root> by design. The DOM-parse path is gated by a cheap regex pre-check so legacy bare <mxGraphModel> callers don't pay any extra cost. The autoFix duplicate-id rename step is also guarded against mxfile inputs — renaming those sentinel cells would silently break drawio's parent references. * `diagram-operations.ts` accepts an optional PageSelector. For <mxfile> input it resolves the page first and scopes all querySelectorAll calls to that page's <root>, so a delete on page 2's cell "2" no longer touches page 1's cell "2". * `create_new_diagram` accepts either a bare <mxGraphModel> (legacy, auto-wrapped into a single-page mxfile) or a full <mxfile> with N diagrams. All existing single-page callers keep working unchanged. * `edit_diagram`, `get_diagram`, and `export_diagram` gain optional `page_id` / `page_name` / `page_index` parameters. When omitted they target the first page — the "active by convention" default. Tool handlers with all-optional input schemas coalesce missing arguments via `input ?? {}` so a no-args MCP invocation can't crash on destructure before reaching the session-existence check. * New tools: `list_pages`, `add_page`, `rename_page`, `delete_page`. * Page-targeted PNG/SVG export uses a "load + export + restore" dance: the server projects the target page into a single-page <mxfile>, pushes it into the transient state so the browser reloads the iframe with just that page, waits for drawio to render (~3s), triggers the export, captures the data, and then restores the original multi-page document. The dance is wrapped in `try/finally` so the restore runs unconditionally — even if an exception is thrown mid-dance, the user's multi-tab view is recovered before the function returns. The earlier attempt to use drawio's `selectPage` postMessage was a no-op because drawio's JSON embed protocol does not expose that action — silently exporting whatever tab happened to be active. The load-export-restore approach trades a brief visible tab-flicker for correctness: the exported image is guaranteed to match the requested page. * Tool description strings reflect the multi-page semantics so the LLM client learns the new contract. * Package version bumped 0.2.0 → 0.3.0 (additive surface — four new tools, three extended input schemas, canonical XML shape change). * CI: `.github/workflows/test.yml` gains an explicit install + vitest run for the mcp-server package so the new multi-page invariants are covered by automation, not just local runs. Backward compatibility: every existing single-page caller continues to work without modification. The session.xml shape is normalised on every write, removing the wrapper-injection hack from the .drawio download path. Tests: 43 unit tests under `packages/mcp-server/tests/multi-page.test.ts` pin the validator's mxfile path, the page-scoped operations, the XML declaration-prefix handling for both normalizeToMxfile and addPageToDoc, addPageToDoc's rejection of full <mxfile> inputs, the single-page projection used by export_diagram (a direct regression test for the selectPage bug — two distinct page selectors must produce visually different projections), and the Transformer + CNN motivating scenario. A `tests/smoke.mjs` smoke test drives the built `dist/index.js` over JSON-RPC and asserts all 9 tools register with the right input schemas. Root vitest suite (107 tests) still green. * fix(mcp): rewrite page-targeted export browser-side; harden edit/get The page-targeted PNG/SVG export never worked: export_diagram swapped the live session to a single-page projection, slept 3s, then wrote the export flag onto a state object that setState() had already replaced in the store Map — so the browser never saw the request and every such export timed out. The swap+restore also clobbered concurrent edits. Move the projection entirely browser-side: requestExport() hands a single -page <mxfile> to the bridge via state.exportXml; the bridge loads it, lets draw.io render, exports, then reloads the user's real document. The canonical session state is never mutated, so there is no restore race and no fixed-delay guessing. The export poll now re-reads the live store entry each tick instead of a captured reference. autosave is suppressed and the version-bump reload is skipped while a projection is on screen; if no real document was captured, restore forces a server reload rather than leaving the iframe stuck on the projection. Also: - edit_diagram now returns isError on a page-level failure (selector matched no page / page has no <root>) instead of reporting success-with-warnings and persisting a no-op; the pre-edit history snapshot is taken only after that gate so a failed edit leaves no phantom undo entry. - edit_diagram/get_diagram re-normalise browser-pushed xml to mxfile so a bare <mxGraphModel> can't silently strip a multi-page document. - get_diagram now errors (instead of silently returning the full doc) when a selector is given but the session isn't a parseable mxfile. - page_id / page_name / add_page.id get .min(1) so empty strings can't silently target the first page. - Extract pages.ts:projectPage(), collapsing three copies of the parse→find→serialise projection logic in index.ts. - Replace the never-in-CI tests/smoke.mjs with tests/server-wiring.test.ts, which boots the server from source via tsx and runs under the existing vitest CI step. * chore(mcp): set version to 0.2.1 for release --------- Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2026-06-16 05:45:50 +05:30
// Fetch latest state from browser, re-normalising to mxfile so a
// bare <mxGraphModel> pushed back by the embed/sync path doesn't
// strip page structure (see edit_diagram for the same guard).
const browserState = getState(currentSession.id)
if (browserState?.xml) {
feat(mcp): add multi-page (mxfile) support to MCP server (#862) * feat(mcp): add multi-page (mxfile) support The MCP server's write path could only address a single drawio page even though the underlying .drawio file format and the embedded editor both natively support multi-page documents. A user asking for "a second page with a CNN diagram" would hit the validator with the error "Expected closing tag </root> but found </mxCell>" because the validator assumed input was a bare <mxGraphModel> and could not walk past the <mxfile><diagram>...</diagram></mxfile> wrapper. This patch closes the gap end to end: * New helper module `pages.ts` centralises page CRUD (normalize, parse, list, find, add, rename, delete) so every layer agrees that the canonical in-memory shape is always <mxfile>. normalizeToMxfile and addPageToDoc both strip any leading <?xml ?> declaration before embedding a fragment inside <diagram> (the declaration is only valid at document start). addPageToDoc explicitly rejects full <mxfile> inputs so a caller cannot accidentally nest a document inside a page. * `xml-validation.ts` now detects an <mxfile> root and scopes the duplicate-id check per <diagram>. The legacy regex check would otherwise reject every multi-page doc, because cells "0" and "1" repeat in each page's <root> by design. The DOM-parse path is gated by a cheap regex pre-check so legacy bare <mxGraphModel> callers don't pay any extra cost. The autoFix duplicate-id rename step is also guarded against mxfile inputs — renaming those sentinel cells would silently break drawio's parent references. * `diagram-operations.ts` accepts an optional PageSelector. For <mxfile> input it resolves the page first and scopes all querySelectorAll calls to that page's <root>, so a delete on page 2's cell "2" no longer touches page 1's cell "2". * `create_new_diagram` accepts either a bare <mxGraphModel> (legacy, auto-wrapped into a single-page mxfile) or a full <mxfile> with N diagrams. All existing single-page callers keep working unchanged. * `edit_diagram`, `get_diagram`, and `export_diagram` gain optional `page_id` / `page_name` / `page_index` parameters. When omitted they target the first page — the "active by convention" default. Tool handlers with all-optional input schemas coalesce missing arguments via `input ?? {}` so a no-args MCP invocation can't crash on destructure before reaching the session-existence check. * New tools: `list_pages`, `add_page`, `rename_page`, `delete_page`. * Page-targeted PNG/SVG export uses a "load + export + restore" dance: the server projects the target page into a single-page <mxfile>, pushes it into the transient state so the browser reloads the iframe with just that page, waits for drawio to render (~3s), triggers the export, captures the data, and then restores the original multi-page document. The dance is wrapped in `try/finally` so the restore runs unconditionally — even if an exception is thrown mid-dance, the user's multi-tab view is recovered before the function returns. The earlier attempt to use drawio's `selectPage` postMessage was a no-op because drawio's JSON embed protocol does not expose that action — silently exporting whatever tab happened to be active. The load-export-restore approach trades a brief visible tab-flicker for correctness: the exported image is guaranteed to match the requested page. * Tool description strings reflect the multi-page semantics so the LLM client learns the new contract. * Package version bumped 0.2.0 → 0.3.0 (additive surface — four new tools, three extended input schemas, canonical XML shape change). * CI: `.github/workflows/test.yml` gains an explicit install + vitest run for the mcp-server package so the new multi-page invariants are covered by automation, not just local runs. Backward compatibility: every existing single-page caller continues to work without modification. The session.xml shape is normalised on every write, removing the wrapper-injection hack from the .drawio download path. Tests: 43 unit tests under `packages/mcp-server/tests/multi-page.test.ts` pin the validator's mxfile path, the page-scoped operations, the XML declaration-prefix handling for both normalizeToMxfile and addPageToDoc, addPageToDoc's rejection of full <mxfile> inputs, the single-page projection used by export_diagram (a direct regression test for the selectPage bug — two distinct page selectors must produce visually different projections), and the Transformer + CNN motivating scenario. A `tests/smoke.mjs` smoke test drives the built `dist/index.js` over JSON-RPC and asserts all 9 tools register with the right input schemas. Root vitest suite (107 tests) still green. * fix(mcp): rewrite page-targeted export browser-side; harden edit/get The page-targeted PNG/SVG export never worked: export_diagram swapped the live session to a single-page projection, slept 3s, then wrote the export flag onto a state object that setState() had already replaced in the store Map — so the browser never saw the request and every such export timed out. The swap+restore also clobbered concurrent edits. Move the projection entirely browser-side: requestExport() hands a single -page <mxfile> to the bridge via state.exportXml; the bridge loads it, lets draw.io render, exports, then reloads the user's real document. The canonical session state is never mutated, so there is no restore race and no fixed-delay guessing. The export poll now re-reads the live store entry each tick instead of a captured reference. autosave is suppressed and the version-bump reload is skipped while a projection is on screen; if no real document was captured, restore forces a server reload rather than leaving the iframe stuck on the projection. Also: - edit_diagram now returns isError on a page-level failure (selector matched no page / page has no <root>) instead of reporting success-with-warnings and persisting a no-op; the pre-edit history snapshot is taken only after that gate so a failed edit leaves no phantom undo entry. - edit_diagram/get_diagram re-normalise browser-pushed xml to mxfile so a bare <mxGraphModel> can't silently strip a multi-page document. - get_diagram now errors (instead of silently returning the full doc) when a selector is given but the session isn't a parseable mxfile. - page_id / page_name / add_page.id get .min(1) so empty strings can't silently target the first page. - Extract pages.ts:projectPage(), collapsing three copies of the parse→find→serialise projection logic in index.ts. - Replace the never-in-CI tests/smoke.mjs with tests/server-wiring.test.ts, which boots the server from source via tsx and runs under the existing vitest CI step. * chore(mcp): set version to 0.2.1 for release --------- Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2026-06-16 05:45:50 +05:30
currentSession.xml =
normalizeToMxfile(browserState.xml) ?? browserState.xml
}
if (!currentSession.xml) {
return {
content: [
{
type: "text",
text: "No diagram exists yet. Use create_new_diagram to create one.",
},
],
}
}
fix(mcp): replace edit_diagram 30s time gate with content comparison (#890) * fix(mcp): keep diagram context valid during edits Closes #885 * fix(mcp): replace edit_diagram time gate with content comparison The 30s wall-clock gate rejected slow-but-correct clients (#885). Instead of a timeout, remember the exact state-store XML the model last saw (get_diagram / create_new_diagram / edit_diagram / page CRUD) and reject edit_diagram only when the live browser state differs - i.e. the user made edits the model hasn't seen yet. Slow reasoning no longer trips the gate, while unseen manual edits still do. * docs(mcp): align edit_diagram/get_diagram descriptions with content-based gate The 'You MUST call get_diagram BEFORE this tool' requirement and the 'Skipping get_diagram WILL cause user's changes to be LOST' warning no longer match server behavior: a stale edit is rejected with no side effects, never silently applied. Describe the freshness check instead, and direct get_diagram usage at its real purpose - learning the current diagram content when the model doesn't already know it. * fix(mcp): compare diagram content structurally in the edit gate draw.io re-serialises the document when pushing state back (attribute order, pretty-printing, regenerated diagram ids, viewport attributes, mxfile host), so byte comparison could flag an unchanged diagram as stale. Fingerprint what a user can actually change instead - page set, page names, and each page's root cell tree with sorted attributes - keeping byte equality as the fast path. A bare mxGraphModel now also fingerprints identically to its single-page mxfile wrapping. * fix(mcp): don't compare page names against bare mxGraphModel pushes A bare <mxGraphModel> pushed by the embed/sync path carries no page name, so normalizeToMxfile invents "Page-1" — falsely reading any custom page name as a content change and re-triggering the stale rejection on every edit. When either side of the gate comparison is a bare mxGraphModel, fingerprint cell trees only; full-mxfile comparisons still detect renames. * chore(mcp): bump version to 0.2.2 * chore(mcp): sync package-lock.json version to 0.2.2 --------- Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2026-07-12 13:33:20 +07:00
// The model is now looking at the current state. Record the raw
// store value — the gate's fast path is plain string equality
// against the store, with a structural comparison as fallback.
currentSession.lastSeenXml = browserState?.xml || currentSession.xml
feat(mcp): add multi-page (mxfile) support to MCP server (#862) * feat(mcp): add multi-page (mxfile) support The MCP server's write path could only address a single drawio page even though the underlying .drawio file format and the embedded editor both natively support multi-page documents. A user asking for "a second page with a CNN diagram" would hit the validator with the error "Expected closing tag </root> but found </mxCell>" because the validator assumed input was a bare <mxGraphModel> and could not walk past the <mxfile><diagram>...</diagram></mxfile> wrapper. This patch closes the gap end to end: * New helper module `pages.ts` centralises page CRUD (normalize, parse, list, find, add, rename, delete) so every layer agrees that the canonical in-memory shape is always <mxfile>. normalizeToMxfile and addPageToDoc both strip any leading <?xml ?> declaration before embedding a fragment inside <diagram> (the declaration is only valid at document start). addPageToDoc explicitly rejects full <mxfile> inputs so a caller cannot accidentally nest a document inside a page. * `xml-validation.ts` now detects an <mxfile> root and scopes the duplicate-id check per <diagram>. The legacy regex check would otherwise reject every multi-page doc, because cells "0" and "1" repeat in each page's <root> by design. The DOM-parse path is gated by a cheap regex pre-check so legacy bare <mxGraphModel> callers don't pay any extra cost. The autoFix duplicate-id rename step is also guarded against mxfile inputs — renaming those sentinel cells would silently break drawio's parent references. * `diagram-operations.ts` accepts an optional PageSelector. For <mxfile> input it resolves the page first and scopes all querySelectorAll calls to that page's <root>, so a delete on page 2's cell "2" no longer touches page 1's cell "2". * `create_new_diagram` accepts either a bare <mxGraphModel> (legacy, auto-wrapped into a single-page mxfile) or a full <mxfile> with N diagrams. All existing single-page callers keep working unchanged. * `edit_diagram`, `get_diagram`, and `export_diagram` gain optional `page_id` / `page_name` / `page_index` parameters. When omitted they target the first page — the "active by convention" default. Tool handlers with all-optional input schemas coalesce missing arguments via `input ?? {}` so a no-args MCP invocation can't crash on destructure before reaching the session-existence check. * New tools: `list_pages`, `add_page`, `rename_page`, `delete_page`. * Page-targeted PNG/SVG export uses a "load + export + restore" dance: the server projects the target page into a single-page <mxfile>, pushes it into the transient state so the browser reloads the iframe with just that page, waits for drawio to render (~3s), triggers the export, captures the data, and then restores the original multi-page document. The dance is wrapped in `try/finally` so the restore runs unconditionally — even if an exception is thrown mid-dance, the user's multi-tab view is recovered before the function returns. The earlier attempt to use drawio's `selectPage` postMessage was a no-op because drawio's JSON embed protocol does not expose that action — silently exporting whatever tab happened to be active. The load-export-restore approach trades a brief visible tab-flicker for correctness: the exported image is guaranteed to match the requested page. * Tool description strings reflect the multi-page semantics so the LLM client learns the new contract. * Package version bumped 0.2.0 → 0.3.0 (additive surface — four new tools, three extended input schemas, canonical XML shape change). * CI: `.github/workflows/test.yml` gains an explicit install + vitest run for the mcp-server package so the new multi-page invariants are covered by automation, not just local runs. Backward compatibility: every existing single-page caller continues to work without modification. The session.xml shape is normalised on every write, removing the wrapper-injection hack from the .drawio download path. Tests: 43 unit tests under `packages/mcp-server/tests/multi-page.test.ts` pin the validator's mxfile path, the page-scoped operations, the XML declaration-prefix handling for both normalizeToMxfile and addPageToDoc, addPageToDoc's rejection of full <mxfile> inputs, the single-page projection used by export_diagram (a direct regression test for the selectPage bug — two distinct page selectors must produce visually different projections), and the Transformer + CNN motivating scenario. A `tests/smoke.mjs` smoke test drives the built `dist/index.js` over JSON-RPC and asserts all 9 tools register with the right input schemas. Root vitest suite (107 tests) still green. * fix(mcp): rewrite page-targeted export browser-side; harden edit/get The page-targeted PNG/SVG export never worked: export_diagram swapped the live session to a single-page projection, slept 3s, then wrote the export flag onto a state object that setState() had already replaced in the store Map — so the browser never saw the request and every such export timed out. The swap+restore also clobbered concurrent edits. Move the projection entirely browser-side: requestExport() hands a single -page <mxfile> to the bridge via state.exportXml; the bridge loads it, lets draw.io render, exports, then reloads the user's real document. The canonical session state is never mutated, so there is no restore race and no fixed-delay guessing. The export poll now re-reads the live store entry each tick instead of a captured reference. autosave is suppressed and the version-bump reload is skipped while a projection is on screen; if no real document was captured, restore forces a server reload rather than leaving the iframe stuck on the projection. Also: - edit_diagram now returns isError on a page-level failure (selector matched no page / page has no <root>) instead of reporting success-with-warnings and persisting a no-op; the pre-edit history snapshot is taken only after that gate so a failed edit leaves no phantom undo entry. - edit_diagram/get_diagram re-normalise browser-pushed xml to mxfile so a bare <mxGraphModel> can't silently strip a multi-page document. - get_diagram now errors (instead of silently returning the full doc) when a selector is given but the session isn't a parseable mxfile. - page_id / page_name / add_page.id get .min(1) so empty strings can't silently target the first page. - Extract pages.ts:projectPage(), collapsing three copies of the parse→find→serialise projection logic in index.ts. - Replace the never-in-CI tests/smoke.mjs with tests/server-wiring.test.ts, which boots the server from source via tsx and runs under the existing vitest CI step. * chore(mcp): set version to 0.2.1 for release --------- Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2026-06-16 05:45:50 +05:30
const pageSelector = pickPageSelector({
page_id,
page_name,
page_index,
})
const doc = parseMxfile(currentSession.xml)
const pages = doc ? listPagesFromDoc(doc) : []
const pageList = pages.length
? `Pages (${pages.length}): ${pages.map((p) => `[${p.index}] id=${p.id} name="${p.name}" cells=${p.cellCount}`).join(" | ")}`
: "No <mxfile> wrapper detected (legacy single-page session)."
// No selector → return full mxfile
if (!hasPageSelector(pageSelector)) {
return {
content: [
{
type: "text",
text: `Current diagram XML:\n\n${currentSession.xml}\n\n${pageList}`,
},
],
}
}
// Selector → return a single-page projection
const projection = projectPage(currentSession.xml, pageSelector)
if (!projection.ok) {
return {
content: [
{
type: "text",
text:
projection.reason === "parse"
? `Error: a page selector was given but the current session XML could not be parsed as a multi-page <mxfile> (it may be a legacy single-page document or malformed), so it has no addressable pages.\n\n${pageList}`
: `Error: Page ${describeSelector(pageSelector)} not found.\n\n${pageList}`,
},
],
isError: true,
}
}
return {
content: [
{
type: "text",
feat(mcp): add multi-page (mxfile) support to MCP server (#862) * feat(mcp): add multi-page (mxfile) support The MCP server's write path could only address a single drawio page even though the underlying .drawio file format and the embedded editor both natively support multi-page documents. A user asking for "a second page with a CNN diagram" would hit the validator with the error "Expected closing tag </root> but found </mxCell>" because the validator assumed input was a bare <mxGraphModel> and could not walk past the <mxfile><diagram>...</diagram></mxfile> wrapper. This patch closes the gap end to end: * New helper module `pages.ts` centralises page CRUD (normalize, parse, list, find, add, rename, delete) so every layer agrees that the canonical in-memory shape is always <mxfile>. normalizeToMxfile and addPageToDoc both strip any leading <?xml ?> declaration before embedding a fragment inside <diagram> (the declaration is only valid at document start). addPageToDoc explicitly rejects full <mxfile> inputs so a caller cannot accidentally nest a document inside a page. * `xml-validation.ts` now detects an <mxfile> root and scopes the duplicate-id check per <diagram>. The legacy regex check would otherwise reject every multi-page doc, because cells "0" and "1" repeat in each page's <root> by design. The DOM-parse path is gated by a cheap regex pre-check so legacy bare <mxGraphModel> callers don't pay any extra cost. The autoFix duplicate-id rename step is also guarded against mxfile inputs — renaming those sentinel cells would silently break drawio's parent references. * `diagram-operations.ts` accepts an optional PageSelector. For <mxfile> input it resolves the page first and scopes all querySelectorAll calls to that page's <root>, so a delete on page 2's cell "2" no longer touches page 1's cell "2". * `create_new_diagram` accepts either a bare <mxGraphModel> (legacy, auto-wrapped into a single-page mxfile) or a full <mxfile> with N diagrams. All existing single-page callers keep working unchanged. * `edit_diagram`, `get_diagram`, and `export_diagram` gain optional `page_id` / `page_name` / `page_index` parameters. When omitted they target the first page — the "active by convention" default. Tool handlers with all-optional input schemas coalesce missing arguments via `input ?? {}` so a no-args MCP invocation can't crash on destructure before reaching the session-existence check. * New tools: `list_pages`, `add_page`, `rename_page`, `delete_page`. * Page-targeted PNG/SVG export uses a "load + export + restore" dance: the server projects the target page into a single-page <mxfile>, pushes it into the transient state so the browser reloads the iframe with just that page, waits for drawio to render (~3s), triggers the export, captures the data, and then restores the original multi-page document. The dance is wrapped in `try/finally` so the restore runs unconditionally — even if an exception is thrown mid-dance, the user's multi-tab view is recovered before the function returns. The earlier attempt to use drawio's `selectPage` postMessage was a no-op because drawio's JSON embed protocol does not expose that action — silently exporting whatever tab happened to be active. The load-export-restore approach trades a brief visible tab-flicker for correctness: the exported image is guaranteed to match the requested page. * Tool description strings reflect the multi-page semantics so the LLM client learns the new contract. * Package version bumped 0.2.0 → 0.3.0 (additive surface — four new tools, three extended input schemas, canonical XML shape change). * CI: `.github/workflows/test.yml` gains an explicit install + vitest run for the mcp-server package so the new multi-page invariants are covered by automation, not just local runs. Backward compatibility: every existing single-page caller continues to work without modification. The session.xml shape is normalised on every write, removing the wrapper-injection hack from the .drawio download path. Tests: 43 unit tests under `packages/mcp-server/tests/multi-page.test.ts` pin the validator's mxfile path, the page-scoped operations, the XML declaration-prefix handling for both normalizeToMxfile and addPageToDoc, addPageToDoc's rejection of full <mxfile> inputs, the single-page projection used by export_diagram (a direct regression test for the selectPage bug — two distinct page selectors must produce visually different projections), and the Transformer + CNN motivating scenario. A `tests/smoke.mjs` smoke test drives the built `dist/index.js` over JSON-RPC and asserts all 9 tools register with the right input schemas. Root vitest suite (107 tests) still green. * fix(mcp): rewrite page-targeted export browser-side; harden edit/get The page-targeted PNG/SVG export never worked: export_diagram swapped the live session to a single-page projection, slept 3s, then wrote the export flag onto a state object that setState() had already replaced in the store Map — so the browser never saw the request and every such export timed out. The swap+restore also clobbered concurrent edits. Move the projection entirely browser-side: requestExport() hands a single -page <mxfile> to the bridge via state.exportXml; the bridge loads it, lets draw.io render, exports, then reloads the user's real document. The canonical session state is never mutated, so there is no restore race and no fixed-delay guessing. The export poll now re-reads the live store entry each tick instead of a captured reference. autosave is suppressed and the version-bump reload is skipped while a projection is on screen; if no real document was captured, restore forces a server reload rather than leaving the iframe stuck on the projection. Also: - edit_diagram now returns isError on a page-level failure (selector matched no page / page has no <root>) instead of reporting success-with-warnings and persisting a no-op; the pre-edit history snapshot is taken only after that gate so a failed edit leaves no phantom undo entry. - edit_diagram/get_diagram re-normalise browser-pushed xml to mxfile so a bare <mxGraphModel> can't silently strip a multi-page document. - get_diagram now errors (instead of silently returning the full doc) when a selector is given but the session isn't a parseable mxfile. - page_id / page_name / add_page.id get .min(1) so empty strings can't silently target the first page. - Extract pages.ts:projectPage(), collapsing three copies of the parse→find→serialise projection logic in index.ts. - Replace the never-in-CI tests/smoke.mjs with tests/server-wiring.test.ts, which boots the server from source via tsx and runs under the existing vitest CI step. * chore(mcp): set version to 0.2.1 for release --------- Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2026-06-16 05:45:50 +05:30
text: `Page ${projection.index} ("${projection.name}"):\n\n${projection.xml}\n\n${pageList}`,
},
],
}
} catch (error) {
const message =
error instanceof Error ? error.message : String(error)
log.error("get_diagram failed:", message)
return {
content: [{ type: "text", text: `Error: ${message}` }],
isError: true,
}
}
},
)
// Tool: export_diagram
server.registerTool(
"export_diagram",
{
feat: add PNG/SVG export to MCP server (#687) * feat: add PNG/SVG export support to MCP server export_diagram tool Previously export_diagram only supported .drawio XML files. This adds PNG and SVG export by leveraging the existing browser sync mechanism: the MCP tool sets an exportFormat flag on the session state, the browser detects it via polling and triggers an iframe export, then POSTs the result back as exportData which the tool reads and writes to disk. * fix: address PR review feedback for export feature - Validate exportData is a string in POST /api/state - Update lastUpdated in setExportFormat to prevent session expiry - Gate export postMessage on isReady to avoid lost messages - Remove unused fmt variable - Fix double extension when path has a different supported extension * fix: resolve high severity npm audit vulnerabilities Run npm audit fix to update @aws-sdk and @smithy transitive dependencies that had high severity advisories, which was failing the CI security audit step. * fix: address second round of PR review feedback - Add 8s timeout for pendingMcpExport to prevent permanent blocking - Move export trigger after version update in poll() to export latest diagram - Return 404 when session not found for exportData POST - Sync browser state before .drawio export to avoid stale XML - Handle URL-encoded SVG data URIs in addition to base64 * fix: address third round of PR review feedback - Sync browser state before PNG/SVG export (not just drawio) - Add 10MB body size limit on POST /api/state - Validate export response format matches request to prevent race conditions * refactor: remove over-engineered defensive code from export feature Strip unnecessary validation/guards added from Copilot review that don't make sense for a localhost-only MCP server: body size limit, type validation, 404 for missing session, lastUpdated refresh, URL-encoded SVG handling. Also deduplicate requestSync call. * refactor: keep original drawio export path unchanged Don't restructure the existing drawio logic - just add png/svg as a separate branch after it. * refactor: remove redundant helper functions, inline state access Remove setExportFormat/getExportData/clearExportData wrappers that were each called once. Access state fields directly via getState(). * chore: bump mcp-server version to 0.1.16
2026-02-07 12:55:09 +09:00
description:
"Export the current diagram to a file. Supports .drawio (XML), .png, and .svg formats. " +
feat(mcp): add multi-page (mxfile) support to MCP server (#862) * feat(mcp): add multi-page (mxfile) support The MCP server's write path could only address a single drawio page even though the underlying .drawio file format and the embedded editor both natively support multi-page documents. A user asking for "a second page with a CNN diagram" would hit the validator with the error "Expected closing tag </root> but found </mxCell>" because the validator assumed input was a bare <mxGraphModel> and could not walk past the <mxfile><diagram>...</diagram></mxfile> wrapper. This patch closes the gap end to end: * New helper module `pages.ts` centralises page CRUD (normalize, parse, list, find, add, rename, delete) so every layer agrees that the canonical in-memory shape is always <mxfile>. normalizeToMxfile and addPageToDoc both strip any leading <?xml ?> declaration before embedding a fragment inside <diagram> (the declaration is only valid at document start). addPageToDoc explicitly rejects full <mxfile> inputs so a caller cannot accidentally nest a document inside a page. * `xml-validation.ts` now detects an <mxfile> root and scopes the duplicate-id check per <diagram>. The legacy regex check would otherwise reject every multi-page doc, because cells "0" and "1" repeat in each page's <root> by design. The DOM-parse path is gated by a cheap regex pre-check so legacy bare <mxGraphModel> callers don't pay any extra cost. The autoFix duplicate-id rename step is also guarded against mxfile inputs — renaming those sentinel cells would silently break drawio's parent references. * `diagram-operations.ts` accepts an optional PageSelector. For <mxfile> input it resolves the page first and scopes all querySelectorAll calls to that page's <root>, so a delete on page 2's cell "2" no longer touches page 1's cell "2". * `create_new_diagram` accepts either a bare <mxGraphModel> (legacy, auto-wrapped into a single-page mxfile) or a full <mxfile> with N diagrams. All existing single-page callers keep working unchanged. * `edit_diagram`, `get_diagram`, and `export_diagram` gain optional `page_id` / `page_name` / `page_index` parameters. When omitted they target the first page — the "active by convention" default. Tool handlers with all-optional input schemas coalesce missing arguments via `input ?? {}` so a no-args MCP invocation can't crash on destructure before reaching the session-existence check. * New tools: `list_pages`, `add_page`, `rename_page`, `delete_page`. * Page-targeted PNG/SVG export uses a "load + export + restore" dance: the server projects the target page into a single-page <mxfile>, pushes it into the transient state so the browser reloads the iframe with just that page, waits for drawio to render (~3s), triggers the export, captures the data, and then restores the original multi-page document. The dance is wrapped in `try/finally` so the restore runs unconditionally — even if an exception is thrown mid-dance, the user's multi-tab view is recovered before the function returns. The earlier attempt to use drawio's `selectPage` postMessage was a no-op because drawio's JSON embed protocol does not expose that action — silently exporting whatever tab happened to be active. The load-export-restore approach trades a brief visible tab-flicker for correctness: the exported image is guaranteed to match the requested page. * Tool description strings reflect the multi-page semantics so the LLM client learns the new contract. * Package version bumped 0.2.0 → 0.3.0 (additive surface — four new tools, three extended input schemas, canonical XML shape change). * CI: `.github/workflows/test.yml` gains an explicit install + vitest run for the mcp-server package so the new multi-page invariants are covered by automation, not just local runs. Backward compatibility: every existing single-page caller continues to work without modification. The session.xml shape is normalised on every write, removing the wrapper-injection hack from the .drawio download path. Tests: 43 unit tests under `packages/mcp-server/tests/multi-page.test.ts` pin the validator's mxfile path, the page-scoped operations, the XML declaration-prefix handling for both normalizeToMxfile and addPageToDoc, addPageToDoc's rejection of full <mxfile> inputs, the single-page projection used by export_diagram (a direct regression test for the selectPage bug — two distinct page selectors must produce visually different projections), and the Transformer + CNN motivating scenario. A `tests/smoke.mjs` smoke test drives the built `dist/index.js` over JSON-RPC and asserts all 9 tools register with the right input schemas. Root vitest suite (107 tests) still green. * fix(mcp): rewrite page-targeted export browser-side; harden edit/get The page-targeted PNG/SVG export never worked: export_diagram swapped the live session to a single-page projection, slept 3s, then wrote the export flag onto a state object that setState() had already replaced in the store Map — so the browser never saw the request and every such export timed out. The swap+restore also clobbered concurrent edits. Move the projection entirely browser-side: requestExport() hands a single -page <mxfile> to the bridge via state.exportXml; the bridge loads it, lets draw.io render, exports, then reloads the user's real document. The canonical session state is never mutated, so there is no restore race and no fixed-delay guessing. The export poll now re-reads the live store entry each tick instead of a captured reference. autosave is suppressed and the version-bump reload is skipped while a projection is on screen; if no real document was captured, restore forces a server reload rather than leaving the iframe stuck on the projection. Also: - edit_diagram now returns isError on a page-level failure (selector matched no page / page has no <root>) instead of reporting success-with-warnings and persisting a no-op; the pre-edit history snapshot is taken only after that gate so a failed edit leaves no phantom undo entry. - edit_diagram/get_diagram re-normalise browser-pushed xml to mxfile so a bare <mxGraphModel> can't silently strip a multi-page document. - get_diagram now errors (instead of silently returning the full doc) when a selector is given but the session isn't a parseable mxfile. - page_id / page_name / add_page.id get .min(1) so empty strings can't silently target the first page. - Extract pages.ts:projectPage(), collapsing three copies of the parse→find→serialise projection logic in index.ts. - Replace the never-in-CI tests/smoke.mjs with tests/server-wiring.test.ts, which boots the server from source via tsx and runs under the existing vitest CI step. * chore(mcp): set version to 0.2.1 for release --------- Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2026-06-16 05:45:50 +05:30
"The format is auto-detected from the file extension, or can be specified explicitly.\n\n" +
"Multi-page behaviour:\n" +
"- .drawio with NO page selector: writes the full <mxfile> (all pages).\n" +
"- .drawio with a page selector: writes a single-page <mxfile> containing only that page.\n" +
"- .png / .svg with NO page selector: exports the currently active page in the browser.\n" +
"- .png / .svg with a page selector: temporarily loads a single-page projection of that page into the browser, captures the rendered image, then restores the full document. The user will see a brief tab-flicker (~1-2s) but the exported image is guaranteed to be the requested page.",
inputSchema: {
feat(mcp): add multi-page (mxfile) support to MCP server (#862) * feat(mcp): add multi-page (mxfile) support The MCP server's write path could only address a single drawio page even though the underlying .drawio file format and the embedded editor both natively support multi-page documents. A user asking for "a second page with a CNN diagram" would hit the validator with the error "Expected closing tag </root> but found </mxCell>" because the validator assumed input was a bare <mxGraphModel> and could not walk past the <mxfile><diagram>...</diagram></mxfile> wrapper. This patch closes the gap end to end: * New helper module `pages.ts` centralises page CRUD (normalize, parse, list, find, add, rename, delete) so every layer agrees that the canonical in-memory shape is always <mxfile>. normalizeToMxfile and addPageToDoc both strip any leading <?xml ?> declaration before embedding a fragment inside <diagram> (the declaration is only valid at document start). addPageToDoc explicitly rejects full <mxfile> inputs so a caller cannot accidentally nest a document inside a page. * `xml-validation.ts` now detects an <mxfile> root and scopes the duplicate-id check per <diagram>. The legacy regex check would otherwise reject every multi-page doc, because cells "0" and "1" repeat in each page's <root> by design. The DOM-parse path is gated by a cheap regex pre-check so legacy bare <mxGraphModel> callers don't pay any extra cost. The autoFix duplicate-id rename step is also guarded against mxfile inputs — renaming those sentinel cells would silently break drawio's parent references. * `diagram-operations.ts` accepts an optional PageSelector. For <mxfile> input it resolves the page first and scopes all querySelectorAll calls to that page's <root>, so a delete on page 2's cell "2" no longer touches page 1's cell "2". * `create_new_diagram` accepts either a bare <mxGraphModel> (legacy, auto-wrapped into a single-page mxfile) or a full <mxfile> with N diagrams. All existing single-page callers keep working unchanged. * `edit_diagram`, `get_diagram`, and `export_diagram` gain optional `page_id` / `page_name` / `page_index` parameters. When omitted they target the first page — the "active by convention" default. Tool handlers with all-optional input schemas coalesce missing arguments via `input ?? {}` so a no-args MCP invocation can't crash on destructure before reaching the session-existence check. * New tools: `list_pages`, `add_page`, `rename_page`, `delete_page`. * Page-targeted PNG/SVG export uses a "load + export + restore" dance: the server projects the target page into a single-page <mxfile>, pushes it into the transient state so the browser reloads the iframe with just that page, waits for drawio to render (~3s), triggers the export, captures the data, and then restores the original multi-page document. The dance is wrapped in `try/finally` so the restore runs unconditionally — even if an exception is thrown mid-dance, the user's multi-tab view is recovered before the function returns. The earlier attempt to use drawio's `selectPage` postMessage was a no-op because drawio's JSON embed protocol does not expose that action — silently exporting whatever tab happened to be active. The load-export-restore approach trades a brief visible tab-flicker for correctness: the exported image is guaranteed to match the requested page. * Tool description strings reflect the multi-page semantics so the LLM client learns the new contract. * Package version bumped 0.2.0 → 0.3.0 (additive surface — four new tools, three extended input schemas, canonical XML shape change). * CI: `.github/workflows/test.yml` gains an explicit install + vitest run for the mcp-server package so the new multi-page invariants are covered by automation, not just local runs. Backward compatibility: every existing single-page caller continues to work without modification. The session.xml shape is normalised on every write, removing the wrapper-injection hack from the .drawio download path. Tests: 43 unit tests under `packages/mcp-server/tests/multi-page.test.ts` pin the validator's mxfile path, the page-scoped operations, the XML declaration-prefix handling for both normalizeToMxfile and addPageToDoc, addPageToDoc's rejection of full <mxfile> inputs, the single-page projection used by export_diagram (a direct regression test for the selectPage bug — two distinct page selectors must produce visually different projections), and the Transformer + CNN motivating scenario. A `tests/smoke.mjs` smoke test drives the built `dist/index.js` over JSON-RPC and asserts all 9 tools register with the right input schemas. Root vitest suite (107 tests) still green. * fix(mcp): rewrite page-targeted export browser-side; harden edit/get The page-targeted PNG/SVG export never worked: export_diagram swapped the live session to a single-page projection, slept 3s, then wrote the export flag onto a state object that setState() had already replaced in the store Map — so the browser never saw the request and every such export timed out. The swap+restore also clobbered concurrent edits. Move the projection entirely browser-side: requestExport() hands a single -page <mxfile> to the bridge via state.exportXml; the bridge loads it, lets draw.io render, exports, then reloads the user's real document. The canonical session state is never mutated, so there is no restore race and no fixed-delay guessing. The export poll now re-reads the live store entry each tick instead of a captured reference. autosave is suppressed and the version-bump reload is skipped while a projection is on screen; if no real document was captured, restore forces a server reload rather than leaving the iframe stuck on the projection. Also: - edit_diagram now returns isError on a page-level failure (selector matched no page / page has no <root>) instead of reporting success-with-warnings and persisting a no-op; the pre-edit history snapshot is taken only after that gate so a failed edit leaves no phantom undo entry. - edit_diagram/get_diagram re-normalise browser-pushed xml to mxfile so a bare <mxGraphModel> can't silently strip a multi-page document. - get_diagram now errors (instead of silently returning the full doc) when a selector is given but the session isn't a parseable mxfile. - page_id / page_name / add_page.id get .min(1) so empty strings can't silently target the first page. - Extract pages.ts:projectPage(), collapsing three copies of the parse→find→serialise projection logic in index.ts. - Replace the never-in-CI tests/smoke.mjs with tests/server-wiring.test.ts, which boots the server from source via tsx and runs under the existing vitest CI step. * chore(mcp): set version to 0.2.1 for release --------- Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2026-06-16 05:45:50 +05:30
...pageSelectorSchema,
path: z
.string()
.describe(
feat: add PNG/SVG export to MCP server (#687) * feat: add PNG/SVG export support to MCP server export_diagram tool Previously export_diagram only supported .drawio XML files. This adds PNG and SVG export by leveraging the existing browser sync mechanism: the MCP tool sets an exportFormat flag on the session state, the browser detects it via polling and triggers an iframe export, then POSTs the result back as exportData which the tool reads and writes to disk. * fix: address PR review feedback for export feature - Validate exportData is a string in POST /api/state - Update lastUpdated in setExportFormat to prevent session expiry - Gate export postMessage on isReady to avoid lost messages - Remove unused fmt variable - Fix double extension when path has a different supported extension * fix: resolve high severity npm audit vulnerabilities Run npm audit fix to update @aws-sdk and @smithy transitive dependencies that had high severity advisories, which was failing the CI security audit step. * fix: address second round of PR review feedback - Add 8s timeout for pendingMcpExport to prevent permanent blocking - Move export trigger after version update in poll() to export latest diagram - Return 404 when session not found for exportData POST - Sync browser state before .drawio export to avoid stale XML - Handle URL-encoded SVG data URIs in addition to base64 * fix: address third round of PR review feedback - Sync browser state before PNG/SVG export (not just drawio) - Add 10MB body size limit on POST /api/state - Validate export response format matches request to prevent race conditions * refactor: remove over-engineered defensive code from export feature Strip unnecessary validation/guards added from Copilot review that don't make sense for a localhost-only MCP server: body size limit, type validation, 404 for missing session, lastUpdated refresh, URL-encoded SVG handling. Also deduplicate requestSync call. * refactor: keep original drawio export path unchanged Don't restructure the existing drawio logic - just add png/svg as a separate branch after it. * refactor: remove redundant helper functions, inline state access Remove setExportFormat/getExportData/clearExportData wrappers that were each called once. Access state fields directly via getState(). * chore: bump mcp-server version to 0.1.16
2026-02-07 12:55:09 +09:00
"File path to save the diagram (e.g., ./diagram.drawio, ./diagram.png, ./diagram.svg)",
),
format: z
.enum(["drawio", "png", "svg"])
.optional()
.describe(
"Export format. If omitted, detected from file extension. Defaults to drawio.",
),
},
},
feat(mcp): add multi-page (mxfile) support to MCP server (#862) * feat(mcp): add multi-page (mxfile) support The MCP server's write path could only address a single drawio page even though the underlying .drawio file format and the embedded editor both natively support multi-page documents. A user asking for "a second page with a CNN diagram" would hit the validator with the error "Expected closing tag </root> but found </mxCell>" because the validator assumed input was a bare <mxGraphModel> and could not walk past the <mxfile><diagram>...</diagram></mxfile> wrapper. This patch closes the gap end to end: * New helper module `pages.ts` centralises page CRUD (normalize, parse, list, find, add, rename, delete) so every layer agrees that the canonical in-memory shape is always <mxfile>. normalizeToMxfile and addPageToDoc both strip any leading <?xml ?> declaration before embedding a fragment inside <diagram> (the declaration is only valid at document start). addPageToDoc explicitly rejects full <mxfile> inputs so a caller cannot accidentally nest a document inside a page. * `xml-validation.ts` now detects an <mxfile> root and scopes the duplicate-id check per <diagram>. The legacy regex check would otherwise reject every multi-page doc, because cells "0" and "1" repeat in each page's <root> by design. The DOM-parse path is gated by a cheap regex pre-check so legacy bare <mxGraphModel> callers don't pay any extra cost. The autoFix duplicate-id rename step is also guarded against mxfile inputs — renaming those sentinel cells would silently break drawio's parent references. * `diagram-operations.ts` accepts an optional PageSelector. For <mxfile> input it resolves the page first and scopes all querySelectorAll calls to that page's <root>, so a delete on page 2's cell "2" no longer touches page 1's cell "2". * `create_new_diagram` accepts either a bare <mxGraphModel> (legacy, auto-wrapped into a single-page mxfile) or a full <mxfile> with N diagrams. All existing single-page callers keep working unchanged. * `edit_diagram`, `get_diagram`, and `export_diagram` gain optional `page_id` / `page_name` / `page_index` parameters. When omitted they target the first page — the "active by convention" default. Tool handlers with all-optional input schemas coalesce missing arguments via `input ?? {}` so a no-args MCP invocation can't crash on destructure before reaching the session-existence check. * New tools: `list_pages`, `add_page`, `rename_page`, `delete_page`. * Page-targeted PNG/SVG export uses a "load + export + restore" dance: the server projects the target page into a single-page <mxfile>, pushes it into the transient state so the browser reloads the iframe with just that page, waits for drawio to render (~3s), triggers the export, captures the data, and then restores the original multi-page document. The dance is wrapped in `try/finally` so the restore runs unconditionally — even if an exception is thrown mid-dance, the user's multi-tab view is recovered before the function returns. The earlier attempt to use drawio's `selectPage` postMessage was a no-op because drawio's JSON embed protocol does not expose that action — silently exporting whatever tab happened to be active. The load-export-restore approach trades a brief visible tab-flicker for correctness: the exported image is guaranteed to match the requested page. * Tool description strings reflect the multi-page semantics so the LLM client learns the new contract. * Package version bumped 0.2.0 → 0.3.0 (additive surface — four new tools, three extended input schemas, canonical XML shape change). * CI: `.github/workflows/test.yml` gains an explicit install + vitest run for the mcp-server package so the new multi-page invariants are covered by automation, not just local runs. Backward compatibility: every existing single-page caller continues to work without modification. The session.xml shape is normalised on every write, removing the wrapper-injection hack from the .drawio download path. Tests: 43 unit tests under `packages/mcp-server/tests/multi-page.test.ts` pin the validator's mxfile path, the page-scoped operations, the XML declaration-prefix handling for both normalizeToMxfile and addPageToDoc, addPageToDoc's rejection of full <mxfile> inputs, the single-page projection used by export_diagram (a direct regression test for the selectPage bug — two distinct page selectors must produce visually different projections), and the Transformer + CNN motivating scenario. A `tests/smoke.mjs` smoke test drives the built `dist/index.js` over JSON-RPC and asserts all 9 tools register with the right input schemas. Root vitest suite (107 tests) still green. * fix(mcp): rewrite page-targeted export browser-side; harden edit/get The page-targeted PNG/SVG export never worked: export_diagram swapped the live session to a single-page projection, slept 3s, then wrote the export flag onto a state object that setState() had already replaced in the store Map — so the browser never saw the request and every such export timed out. The swap+restore also clobbered concurrent edits. Move the projection entirely browser-side: requestExport() hands a single -page <mxfile> to the bridge via state.exportXml; the bridge loads it, lets draw.io render, exports, then reloads the user's real document. The canonical session state is never mutated, so there is no restore race and no fixed-delay guessing. The export poll now re-reads the live store entry each tick instead of a captured reference. autosave is suppressed and the version-bump reload is skipped while a projection is on screen; if no real document was captured, restore forces a server reload rather than leaving the iframe stuck on the projection. Also: - edit_diagram now returns isError on a page-level failure (selector matched no page / page has no <root>) instead of reporting success-with-warnings and persisting a no-op; the pre-edit history snapshot is taken only after that gate so a failed edit leaves no phantom undo entry. - edit_diagram/get_diagram re-normalise browser-pushed xml to mxfile so a bare <mxGraphModel> can't silently strip a multi-page document. - get_diagram now errors (instead of silently returning the full doc) when a selector is given but the session isn't a parseable mxfile. - page_id / page_name / add_page.id get .min(1) so empty strings can't silently target the first page. - Extract pages.ts:projectPage(), collapsing three copies of the parse→find→serialise projection logic in index.ts. - Replace the never-in-CI tests/smoke.mjs with tests/server-wiring.test.ts, which boots the server from source via tsx and runs under the existing vitest CI step. * chore(mcp): set version to 0.2.1 for release --------- Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2026-06-16 05:45:50 +05:30
async ({ path, format, page_id, page_name, page_index }) => {
try {
if (!currentSession) {
return {
content: [
{
type: "text",
text: "Error: No active session. Please call start_session first.",
},
],
isError: true,
}
}
// Fetch latest state
const browserState = getState(currentSession.id)
if (browserState?.xml) {
currentSession.xml = browserState.xml
}
if (!currentSession.xml) {
return {
content: [
{
type: "text",
text: "Error: No diagram to export. Please create a diagram first.",
},
],
isError: true,
}
}
feat(mcp): add multi-page (mxfile) support to MCP server (#862) * feat(mcp): add multi-page (mxfile) support The MCP server's write path could only address a single drawio page even though the underlying .drawio file format and the embedded editor both natively support multi-page documents. A user asking for "a second page with a CNN diagram" would hit the validator with the error "Expected closing tag </root> but found </mxCell>" because the validator assumed input was a bare <mxGraphModel> and could not walk past the <mxfile><diagram>...</diagram></mxfile> wrapper. This patch closes the gap end to end: * New helper module `pages.ts` centralises page CRUD (normalize, parse, list, find, add, rename, delete) so every layer agrees that the canonical in-memory shape is always <mxfile>. normalizeToMxfile and addPageToDoc both strip any leading <?xml ?> declaration before embedding a fragment inside <diagram> (the declaration is only valid at document start). addPageToDoc explicitly rejects full <mxfile> inputs so a caller cannot accidentally nest a document inside a page. * `xml-validation.ts` now detects an <mxfile> root and scopes the duplicate-id check per <diagram>. The legacy regex check would otherwise reject every multi-page doc, because cells "0" and "1" repeat in each page's <root> by design. The DOM-parse path is gated by a cheap regex pre-check so legacy bare <mxGraphModel> callers don't pay any extra cost. The autoFix duplicate-id rename step is also guarded against mxfile inputs — renaming those sentinel cells would silently break drawio's parent references. * `diagram-operations.ts` accepts an optional PageSelector. For <mxfile> input it resolves the page first and scopes all querySelectorAll calls to that page's <root>, so a delete on page 2's cell "2" no longer touches page 1's cell "2". * `create_new_diagram` accepts either a bare <mxGraphModel> (legacy, auto-wrapped into a single-page mxfile) or a full <mxfile> with N diagrams. All existing single-page callers keep working unchanged. * `edit_diagram`, `get_diagram`, and `export_diagram` gain optional `page_id` / `page_name` / `page_index` parameters. When omitted they target the first page — the "active by convention" default. Tool handlers with all-optional input schemas coalesce missing arguments via `input ?? {}` so a no-args MCP invocation can't crash on destructure before reaching the session-existence check. * New tools: `list_pages`, `add_page`, `rename_page`, `delete_page`. * Page-targeted PNG/SVG export uses a "load + export + restore" dance: the server projects the target page into a single-page <mxfile>, pushes it into the transient state so the browser reloads the iframe with just that page, waits for drawio to render (~3s), triggers the export, captures the data, and then restores the original multi-page document. The dance is wrapped in `try/finally` so the restore runs unconditionally — even if an exception is thrown mid-dance, the user's multi-tab view is recovered before the function returns. The earlier attempt to use drawio's `selectPage` postMessage was a no-op because drawio's JSON embed protocol does not expose that action — silently exporting whatever tab happened to be active. The load-export-restore approach trades a brief visible tab-flicker for correctness: the exported image is guaranteed to match the requested page. * Tool description strings reflect the multi-page semantics so the LLM client learns the new contract. * Package version bumped 0.2.0 → 0.3.0 (additive surface — four new tools, three extended input schemas, canonical XML shape change). * CI: `.github/workflows/test.yml` gains an explicit install + vitest run for the mcp-server package so the new multi-page invariants are covered by automation, not just local runs. Backward compatibility: every existing single-page caller continues to work without modification. The session.xml shape is normalised on every write, removing the wrapper-injection hack from the .drawio download path. Tests: 43 unit tests under `packages/mcp-server/tests/multi-page.test.ts` pin the validator's mxfile path, the page-scoped operations, the XML declaration-prefix handling for both normalizeToMxfile and addPageToDoc, addPageToDoc's rejection of full <mxfile> inputs, the single-page projection used by export_diagram (a direct regression test for the selectPage bug — two distinct page selectors must produce visually different projections), and the Transformer + CNN motivating scenario. A `tests/smoke.mjs` smoke test drives the built `dist/index.js` over JSON-RPC and asserts all 9 tools register with the right input schemas. Root vitest suite (107 tests) still green. * fix(mcp): rewrite page-targeted export browser-side; harden edit/get The page-targeted PNG/SVG export never worked: export_diagram swapped the live session to a single-page projection, slept 3s, then wrote the export flag onto a state object that setState() had already replaced in the store Map — so the browser never saw the request and every such export timed out. The swap+restore also clobbered concurrent edits. Move the projection entirely browser-side: requestExport() hands a single -page <mxfile> to the bridge via state.exportXml; the bridge loads it, lets draw.io render, exports, then reloads the user's real document. The canonical session state is never mutated, so there is no restore race and no fixed-delay guessing. The export poll now re-reads the live store entry each tick instead of a captured reference. autosave is suppressed and the version-bump reload is skipped while a projection is on screen; if no real document was captured, restore forces a server reload rather than leaving the iframe stuck on the projection. Also: - edit_diagram now returns isError on a page-level failure (selector matched no page / page has no <root>) instead of reporting success-with-warnings and persisting a no-op; the pre-edit history snapshot is taken only after that gate so a failed edit leaves no phantom undo entry. - edit_diagram/get_diagram re-normalise browser-pushed xml to mxfile so a bare <mxGraphModel> can't silently strip a multi-page document. - get_diagram now errors (instead of silently returning the full doc) when a selector is given but the session isn't a parseable mxfile. - page_id / page_name / add_page.id get .min(1) so empty strings can't silently target the first page. - Extract pages.ts:projectPage(), collapsing three copies of the parse→find→serialise projection logic in index.ts. - Replace the never-in-CI tests/smoke.mjs with tests/server-wiring.test.ts, which boots the server from source via tsx and runs under the existing vitest CI step. * chore(mcp): set version to 0.2.1 for release --------- Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2026-06-16 05:45:50 +05:30
const pageSelector = pickPageSelector({
page_id,
page_name,
page_index,
})
const fs = await import("node:fs/promises")
const nodePath = await import("node:path")
feat: add PNG/SVG export to MCP server (#687) * feat: add PNG/SVG export support to MCP server export_diagram tool Previously export_diagram only supported .drawio XML files. This adds PNG and SVG export by leveraging the existing browser sync mechanism: the MCP tool sets an exportFormat flag on the session state, the browser detects it via polling and triggers an iframe export, then POSTs the result back as exportData which the tool reads and writes to disk. * fix: address PR review feedback for export feature - Validate exportData is a string in POST /api/state - Update lastUpdated in setExportFormat to prevent session expiry - Gate export postMessage on isReady to avoid lost messages - Remove unused fmt variable - Fix double extension when path has a different supported extension * fix: resolve high severity npm audit vulnerabilities Run npm audit fix to update @aws-sdk and @smithy transitive dependencies that had high severity advisories, which was failing the CI security audit step. * fix: address second round of PR review feedback - Add 8s timeout for pendingMcpExport to prevent permanent blocking - Move export trigger after version update in poll() to export latest diagram - Return 404 when session not found for exportData POST - Sync browser state before .drawio export to avoid stale XML - Handle URL-encoded SVG data URIs in addition to base64 * fix: address third round of PR review feedback - Sync browser state before PNG/SVG export (not just drawio) - Add 10MB body size limit on POST /api/state - Validate export response format matches request to prevent race conditions * refactor: remove over-engineered defensive code from export feature Strip unnecessary validation/guards added from Copilot review that don't make sense for a localhost-only MCP server: body size limit, type validation, 404 for missing session, lastUpdated refresh, URL-encoded SVG handling. Also deduplicate requestSync call. * refactor: keep original drawio export path unchanged Don't restructure the existing drawio logic - just add png/svg as a separate branch after it. * refactor: remove redundant helper functions, inline state access Remove setExportFormat/getExportData/clearExportData wrappers that were each called once. Access state fields directly via getState(). * chore: bump mcp-server version to 0.1.16
2026-02-07 12:55:09 +09:00
// Detect format from extension if not specified
const ext = nodePath.extname(path).toLowerCase()
const detectedFormat =
format ||
(ext === ".png" ? "png" : ext === ".svg" ? "svg" : "drawio")
feat(mcp): add multi-page (mxfile) support to MCP server (#862) * feat(mcp): add multi-page (mxfile) support The MCP server's write path could only address a single drawio page even though the underlying .drawio file format and the embedded editor both natively support multi-page documents. A user asking for "a second page with a CNN diagram" would hit the validator with the error "Expected closing tag </root> but found </mxCell>" because the validator assumed input was a bare <mxGraphModel> and could not walk past the <mxfile><diagram>...</diagram></mxfile> wrapper. This patch closes the gap end to end: * New helper module `pages.ts` centralises page CRUD (normalize, parse, list, find, add, rename, delete) so every layer agrees that the canonical in-memory shape is always <mxfile>. normalizeToMxfile and addPageToDoc both strip any leading <?xml ?> declaration before embedding a fragment inside <diagram> (the declaration is only valid at document start). addPageToDoc explicitly rejects full <mxfile> inputs so a caller cannot accidentally nest a document inside a page. * `xml-validation.ts` now detects an <mxfile> root and scopes the duplicate-id check per <diagram>. The legacy regex check would otherwise reject every multi-page doc, because cells "0" and "1" repeat in each page's <root> by design. The DOM-parse path is gated by a cheap regex pre-check so legacy bare <mxGraphModel> callers don't pay any extra cost. The autoFix duplicate-id rename step is also guarded against mxfile inputs — renaming those sentinel cells would silently break drawio's parent references. * `diagram-operations.ts` accepts an optional PageSelector. For <mxfile> input it resolves the page first and scopes all querySelectorAll calls to that page's <root>, so a delete on page 2's cell "2" no longer touches page 1's cell "2". * `create_new_diagram` accepts either a bare <mxGraphModel> (legacy, auto-wrapped into a single-page mxfile) or a full <mxfile> with N diagrams. All existing single-page callers keep working unchanged. * `edit_diagram`, `get_diagram`, and `export_diagram` gain optional `page_id` / `page_name` / `page_index` parameters. When omitted they target the first page — the "active by convention" default. Tool handlers with all-optional input schemas coalesce missing arguments via `input ?? {}` so a no-args MCP invocation can't crash on destructure before reaching the session-existence check. * New tools: `list_pages`, `add_page`, `rename_page`, `delete_page`. * Page-targeted PNG/SVG export uses a "load + export + restore" dance: the server projects the target page into a single-page <mxfile>, pushes it into the transient state so the browser reloads the iframe with just that page, waits for drawio to render (~3s), triggers the export, captures the data, and then restores the original multi-page document. The dance is wrapped in `try/finally` so the restore runs unconditionally — even if an exception is thrown mid-dance, the user's multi-tab view is recovered before the function returns. The earlier attempt to use drawio's `selectPage` postMessage was a no-op because drawio's JSON embed protocol does not expose that action — silently exporting whatever tab happened to be active. The load-export-restore approach trades a brief visible tab-flicker for correctness: the exported image is guaranteed to match the requested page. * Tool description strings reflect the multi-page semantics so the LLM client learns the new contract. * Package version bumped 0.2.0 → 0.3.0 (additive surface — four new tools, three extended input schemas, canonical XML shape change). * CI: `.github/workflows/test.yml` gains an explicit install + vitest run for the mcp-server package so the new multi-page invariants are covered by automation, not just local runs. Backward compatibility: every existing single-page caller continues to work without modification. The session.xml shape is normalised on every write, removing the wrapper-injection hack from the .drawio download path. Tests: 43 unit tests under `packages/mcp-server/tests/multi-page.test.ts` pin the validator's mxfile path, the page-scoped operations, the XML declaration-prefix handling for both normalizeToMxfile and addPageToDoc, addPageToDoc's rejection of full <mxfile> inputs, the single-page projection used by export_diagram (a direct regression test for the selectPage bug — two distinct page selectors must produce visually different projections), and the Transformer + CNN motivating scenario. A `tests/smoke.mjs` smoke test drives the built `dist/index.js` over JSON-RPC and asserts all 9 tools register with the right input schemas. Root vitest suite (107 tests) still green. * fix(mcp): rewrite page-targeted export browser-side; harden edit/get The page-targeted PNG/SVG export never worked: export_diagram swapped the live session to a single-page projection, slept 3s, then wrote the export flag onto a state object that setState() had already replaced in the store Map — so the browser never saw the request and every such export timed out. The swap+restore also clobbered concurrent edits. Move the projection entirely browser-side: requestExport() hands a single -page <mxfile> to the bridge via state.exportXml; the bridge loads it, lets draw.io render, exports, then reloads the user's real document. The canonical session state is never mutated, so there is no restore race and no fixed-delay guessing. The export poll now re-reads the live store entry each tick instead of a captured reference. autosave is suppressed and the version-bump reload is skipped while a projection is on screen; if no real document was captured, restore forces a server reload rather than leaving the iframe stuck on the projection. Also: - edit_diagram now returns isError on a page-level failure (selector matched no page / page has no <root>) instead of reporting success-with-warnings and persisting a no-op; the pre-edit history snapshot is taken only after that gate so a failed edit leaves no phantom undo entry. - edit_diagram/get_diagram re-normalise browser-pushed xml to mxfile so a bare <mxGraphModel> can't silently strip a multi-page document. - get_diagram now errors (instead of silently returning the full doc) when a selector is given but the session isn't a parseable mxfile. - page_id / page_name / add_page.id get .min(1) so empty strings can't silently target the first page. - Extract pages.ts:projectPage(), collapsing three copies of the parse→find→serialise projection logic in index.ts. - Replace the never-in-CI tests/smoke.mjs with tests/server-wiring.test.ts, which boots the server from source via tsx and runs under the existing vitest CI step. * chore(mcp): set version to 0.2.1 for release --------- Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2026-06-16 05:45:50 +05:30
// .drawio path - write XML directly (no browser round-trip).
feat: add PNG/SVG export to MCP server (#687) * feat: add PNG/SVG export support to MCP server export_diagram tool Previously export_diagram only supported .drawio XML files. This adds PNG and SVG export by leveraging the existing browser sync mechanism: the MCP tool sets an exportFormat flag on the session state, the browser detects it via polling and triggers an iframe export, then POSTs the result back as exportData which the tool reads and writes to disk. * fix: address PR review feedback for export feature - Validate exportData is a string in POST /api/state - Update lastUpdated in setExportFormat to prevent session expiry - Gate export postMessage on isReady to avoid lost messages - Remove unused fmt variable - Fix double extension when path has a different supported extension * fix: resolve high severity npm audit vulnerabilities Run npm audit fix to update @aws-sdk and @smithy transitive dependencies that had high severity advisories, which was failing the CI security audit step. * fix: address second round of PR review feedback - Add 8s timeout for pendingMcpExport to prevent permanent blocking - Move export trigger after version update in poll() to export latest diagram - Return 404 when session not found for exportData POST - Sync browser state before .drawio export to avoid stale XML - Handle URL-encoded SVG data URIs in addition to base64 * fix: address third round of PR review feedback - Sync browser state before PNG/SVG export (not just drawio) - Add 10MB body size limit on POST /api/state - Validate export response format matches request to prevent race conditions * refactor: remove over-engineered defensive code from export feature Strip unnecessary validation/guards added from Copilot review that don't make sense for a localhost-only MCP server: body size limit, type validation, 404 for missing session, lastUpdated refresh, URL-encoded SVG handling. Also deduplicate requestSync call. * refactor: keep original drawio export path unchanged Don't restructure the existing drawio logic - just add png/svg as a separate branch after it. * refactor: remove redundant helper functions, inline state access Remove setExportFormat/getExportData/clearExportData wrappers that were each called once. Access state fields directly via getState(). * chore: bump mcp-server version to 0.1.16
2026-02-07 12:55:09 +09:00
if (detectedFormat === "drawio") {
let filePath = path
if (!filePath.endsWith(".drawio")) {
filePath = `${filePath}.drawio`
}
const absolutePath = nodePath.resolve(filePath)
feat(mcp): add multi-page (mxfile) support to MCP server (#862) * feat(mcp): add multi-page (mxfile) support The MCP server's write path could only address a single drawio page even though the underlying .drawio file format and the embedded editor both natively support multi-page documents. A user asking for "a second page with a CNN diagram" would hit the validator with the error "Expected closing tag </root> but found </mxCell>" because the validator assumed input was a bare <mxGraphModel> and could not walk past the <mxfile><diagram>...</diagram></mxfile> wrapper. This patch closes the gap end to end: * New helper module `pages.ts` centralises page CRUD (normalize, parse, list, find, add, rename, delete) so every layer agrees that the canonical in-memory shape is always <mxfile>. normalizeToMxfile and addPageToDoc both strip any leading <?xml ?> declaration before embedding a fragment inside <diagram> (the declaration is only valid at document start). addPageToDoc explicitly rejects full <mxfile> inputs so a caller cannot accidentally nest a document inside a page. * `xml-validation.ts` now detects an <mxfile> root and scopes the duplicate-id check per <diagram>. The legacy regex check would otherwise reject every multi-page doc, because cells "0" and "1" repeat in each page's <root> by design. The DOM-parse path is gated by a cheap regex pre-check so legacy bare <mxGraphModel> callers don't pay any extra cost. The autoFix duplicate-id rename step is also guarded against mxfile inputs — renaming those sentinel cells would silently break drawio's parent references. * `diagram-operations.ts` accepts an optional PageSelector. For <mxfile> input it resolves the page first and scopes all querySelectorAll calls to that page's <root>, so a delete on page 2's cell "2" no longer touches page 1's cell "2". * `create_new_diagram` accepts either a bare <mxGraphModel> (legacy, auto-wrapped into a single-page mxfile) or a full <mxfile> with N diagrams. All existing single-page callers keep working unchanged. * `edit_diagram`, `get_diagram`, and `export_diagram` gain optional `page_id` / `page_name` / `page_index` parameters. When omitted they target the first page — the "active by convention" default. Tool handlers with all-optional input schemas coalesce missing arguments via `input ?? {}` so a no-args MCP invocation can't crash on destructure before reaching the session-existence check. * New tools: `list_pages`, `add_page`, `rename_page`, `delete_page`. * Page-targeted PNG/SVG export uses a "load + export + restore" dance: the server projects the target page into a single-page <mxfile>, pushes it into the transient state so the browser reloads the iframe with just that page, waits for drawio to render (~3s), triggers the export, captures the data, and then restores the original multi-page document. The dance is wrapped in `try/finally` so the restore runs unconditionally — even if an exception is thrown mid-dance, the user's multi-tab view is recovered before the function returns. The earlier attempt to use drawio's `selectPage` postMessage was a no-op because drawio's JSON embed protocol does not expose that action — silently exporting whatever tab happened to be active. The load-export-restore approach trades a brief visible tab-flicker for correctness: the exported image is guaranteed to match the requested page. * Tool description strings reflect the multi-page semantics so the LLM client learns the new contract. * Package version bumped 0.2.0 → 0.3.0 (additive surface — four new tools, three extended input schemas, canonical XML shape change). * CI: `.github/workflows/test.yml` gains an explicit install + vitest run for the mcp-server package so the new multi-page invariants are covered by automation, not just local runs. Backward compatibility: every existing single-page caller continues to work without modification. The session.xml shape is normalised on every write, removing the wrapper-injection hack from the .drawio download path. Tests: 43 unit tests under `packages/mcp-server/tests/multi-page.test.ts` pin the validator's mxfile path, the page-scoped operations, the XML declaration-prefix handling for both normalizeToMxfile and addPageToDoc, addPageToDoc's rejection of full <mxfile> inputs, the single-page projection used by export_diagram (a direct regression test for the selectPage bug — two distinct page selectors must produce visually different projections), and the Transformer + CNN motivating scenario. A `tests/smoke.mjs` smoke test drives the built `dist/index.js` over JSON-RPC and asserts all 9 tools register with the right input schemas. Root vitest suite (107 tests) still green. * fix(mcp): rewrite page-targeted export browser-side; harden edit/get The page-targeted PNG/SVG export never worked: export_diagram swapped the live session to a single-page projection, slept 3s, then wrote the export flag onto a state object that setState() had already replaced in the store Map — so the browser never saw the request and every such export timed out. The swap+restore also clobbered concurrent edits. Move the projection entirely browser-side: requestExport() hands a single -page <mxfile> to the bridge via state.exportXml; the bridge loads it, lets draw.io render, exports, then reloads the user's real document. The canonical session state is never mutated, so there is no restore race and no fixed-delay guessing. The export poll now re-reads the live store entry each tick instead of a captured reference. autosave is suppressed and the version-bump reload is skipped while a projection is on screen; if no real document was captured, restore forces a server reload rather than leaving the iframe stuck on the projection. Also: - edit_diagram now returns isError on a page-level failure (selector matched no page / page has no <root>) instead of reporting success-with-warnings and persisting a no-op; the pre-edit history snapshot is taken only after that gate so a failed edit leaves no phantom undo entry. - edit_diagram/get_diagram re-normalise browser-pushed xml to mxfile so a bare <mxGraphModel> can't silently strip a multi-page document. - get_diagram now errors (instead of silently returning the full doc) when a selector is given but the session isn't a parseable mxfile. - page_id / page_name / add_page.id get .min(1) so empty strings can't silently target the first page. - Extract pages.ts:projectPage(), collapsing three copies of the parse→find→serialise projection logic in index.ts. - Replace the never-in-CI tests/smoke.mjs with tests/server-wiring.test.ts, which boots the server from source via tsx and runs under the existing vitest CI step. * chore(mcp): set version to 0.2.1 for release --------- Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2026-06-16 05:45:50 +05:30
let outXml = currentSession.xml
if (hasPageSelector(pageSelector)) {
const projection = projectPage(
currentSession.xml,
pageSelector,
)
if (!projection.ok) {
return {
content: [
{
type: "text",
text:
projection.reason === "parse"
? "Error: Cannot parse current session XML as <mxfile>; cannot project a single page."
: `Error: Page ${describeSelector(pageSelector)} not found for export.`,
},
],
isError: true,
}
}
outXml = projection.xml
}
await fs.writeFile(absolutePath, outXml, "utf-8")
feat: add PNG/SVG export to MCP server (#687) * feat: add PNG/SVG export support to MCP server export_diagram tool Previously export_diagram only supported .drawio XML files. This adds PNG and SVG export by leveraging the existing browser sync mechanism: the MCP tool sets an exportFormat flag on the session state, the browser detects it via polling and triggers an iframe export, then POSTs the result back as exportData which the tool reads and writes to disk. * fix: address PR review feedback for export feature - Validate exportData is a string in POST /api/state - Update lastUpdated in setExportFormat to prevent session expiry - Gate export postMessage on isReady to avoid lost messages - Remove unused fmt variable - Fix double extension when path has a different supported extension * fix: resolve high severity npm audit vulnerabilities Run npm audit fix to update @aws-sdk and @smithy transitive dependencies that had high severity advisories, which was failing the CI security audit step. * fix: address second round of PR review feedback - Add 8s timeout for pendingMcpExport to prevent permanent blocking - Move export trigger after version update in poll() to export latest diagram - Return 404 when session not found for exportData POST - Sync browser state before .drawio export to avoid stale XML - Handle URL-encoded SVG data URIs in addition to base64 * fix: address third round of PR review feedback - Sync browser state before PNG/SVG export (not just drawio) - Add 10MB body size limit on POST /api/state - Validate export response format matches request to prevent race conditions * refactor: remove over-engineered defensive code from export feature Strip unnecessary validation/guards added from Copilot review that don't make sense for a localhost-only MCP server: body size limit, type validation, 404 for missing session, lastUpdated refresh, URL-encoded SVG handling. Also deduplicate requestSync call. * refactor: keep original drawio export path unchanged Don't restructure the existing drawio logic - just add png/svg as a separate branch after it. * refactor: remove redundant helper functions, inline state access Remove setExportFormat/getExportData/clearExportData wrappers that were each called once. Access state fields directly via getState(). * chore: bump mcp-server version to 0.1.16
2026-02-07 12:55:09 +09:00
log.info(`Diagram exported to ${absolutePath}`)
return {
content: [
{
type: "text",
feat(mcp): add multi-page (mxfile) support to MCP server (#862) * feat(mcp): add multi-page (mxfile) support The MCP server's write path could only address a single drawio page even though the underlying .drawio file format and the embedded editor both natively support multi-page documents. A user asking for "a second page with a CNN diagram" would hit the validator with the error "Expected closing tag </root> but found </mxCell>" because the validator assumed input was a bare <mxGraphModel> and could not walk past the <mxfile><diagram>...</diagram></mxfile> wrapper. This patch closes the gap end to end: * New helper module `pages.ts` centralises page CRUD (normalize, parse, list, find, add, rename, delete) so every layer agrees that the canonical in-memory shape is always <mxfile>. normalizeToMxfile and addPageToDoc both strip any leading <?xml ?> declaration before embedding a fragment inside <diagram> (the declaration is only valid at document start). addPageToDoc explicitly rejects full <mxfile> inputs so a caller cannot accidentally nest a document inside a page. * `xml-validation.ts` now detects an <mxfile> root and scopes the duplicate-id check per <diagram>. The legacy regex check would otherwise reject every multi-page doc, because cells "0" and "1" repeat in each page's <root> by design. The DOM-parse path is gated by a cheap regex pre-check so legacy bare <mxGraphModel> callers don't pay any extra cost. The autoFix duplicate-id rename step is also guarded against mxfile inputs — renaming those sentinel cells would silently break drawio's parent references. * `diagram-operations.ts` accepts an optional PageSelector. For <mxfile> input it resolves the page first and scopes all querySelectorAll calls to that page's <root>, so a delete on page 2's cell "2" no longer touches page 1's cell "2". * `create_new_diagram` accepts either a bare <mxGraphModel> (legacy, auto-wrapped into a single-page mxfile) or a full <mxfile> with N diagrams. All existing single-page callers keep working unchanged. * `edit_diagram`, `get_diagram`, and `export_diagram` gain optional `page_id` / `page_name` / `page_index` parameters. When omitted they target the first page — the "active by convention" default. Tool handlers with all-optional input schemas coalesce missing arguments via `input ?? {}` so a no-args MCP invocation can't crash on destructure before reaching the session-existence check. * New tools: `list_pages`, `add_page`, `rename_page`, `delete_page`. * Page-targeted PNG/SVG export uses a "load + export + restore" dance: the server projects the target page into a single-page <mxfile>, pushes it into the transient state so the browser reloads the iframe with just that page, waits for drawio to render (~3s), triggers the export, captures the data, and then restores the original multi-page document. The dance is wrapped in `try/finally` so the restore runs unconditionally — even if an exception is thrown mid-dance, the user's multi-tab view is recovered before the function returns. The earlier attempt to use drawio's `selectPage` postMessage was a no-op because drawio's JSON embed protocol does not expose that action — silently exporting whatever tab happened to be active. The load-export-restore approach trades a brief visible tab-flicker for correctness: the exported image is guaranteed to match the requested page. * Tool description strings reflect the multi-page semantics so the LLM client learns the new contract. * Package version bumped 0.2.0 → 0.3.0 (additive surface — four new tools, three extended input schemas, canonical XML shape change). * CI: `.github/workflows/test.yml` gains an explicit install + vitest run for the mcp-server package so the new multi-page invariants are covered by automation, not just local runs. Backward compatibility: every existing single-page caller continues to work without modification. The session.xml shape is normalised on every write, removing the wrapper-injection hack from the .drawio download path. Tests: 43 unit tests under `packages/mcp-server/tests/multi-page.test.ts` pin the validator's mxfile path, the page-scoped operations, the XML declaration-prefix handling for both normalizeToMxfile and addPageToDoc, addPageToDoc's rejection of full <mxfile> inputs, the single-page projection used by export_diagram (a direct regression test for the selectPage bug — two distinct page selectors must produce visually different projections), and the Transformer + CNN motivating scenario. A `tests/smoke.mjs` smoke test drives the built `dist/index.js` over JSON-RPC and asserts all 9 tools register with the right input schemas. Root vitest suite (107 tests) still green. * fix(mcp): rewrite page-targeted export browser-side; harden edit/get The page-targeted PNG/SVG export never worked: export_diagram swapped the live session to a single-page projection, slept 3s, then wrote the export flag onto a state object that setState() had already replaced in the store Map — so the browser never saw the request and every such export timed out. The swap+restore also clobbered concurrent edits. Move the projection entirely browser-side: requestExport() hands a single -page <mxfile> to the bridge via state.exportXml; the bridge loads it, lets draw.io render, exports, then reloads the user's real document. The canonical session state is never mutated, so there is no restore race and no fixed-delay guessing. The export poll now re-reads the live store entry each tick instead of a captured reference. autosave is suppressed and the version-bump reload is skipped while a projection is on screen; if no real document was captured, restore forces a server reload rather than leaving the iframe stuck on the projection. Also: - edit_diagram now returns isError on a page-level failure (selector matched no page / page has no <root>) instead of reporting success-with-warnings and persisting a no-op; the pre-edit history snapshot is taken only after that gate so a failed edit leaves no phantom undo entry. - edit_diagram/get_diagram re-normalise browser-pushed xml to mxfile so a bare <mxGraphModel> can't silently strip a multi-page document. - get_diagram now errors (instead of silently returning the full doc) when a selector is given but the session isn't a parseable mxfile. - page_id / page_name / add_page.id get .min(1) so empty strings can't silently target the first page. - Extract pages.ts:projectPage(), collapsing three copies of the parse→find→serialise projection logic in index.ts. - Replace the never-in-CI tests/smoke.mjs with tests/server-wiring.test.ts, which boots the server from source via tsx and runs under the existing vitest CI step. * chore(mcp): set version to 0.2.1 for release --------- Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2026-06-16 05:45:50 +05:30
text: `Diagram exported successfully!\n\nFile: ${absolutePath}\nSize: ${outXml.length} characters`,
feat: add PNG/SVG export to MCP server (#687) * feat: add PNG/SVG export support to MCP server export_diagram tool Previously export_diagram only supported .drawio XML files. This adds PNG and SVG export by leveraging the existing browser sync mechanism: the MCP tool sets an exportFormat flag on the session state, the browser detects it via polling and triggers an iframe export, then POSTs the result back as exportData which the tool reads and writes to disk. * fix: address PR review feedback for export feature - Validate exportData is a string in POST /api/state - Update lastUpdated in setExportFormat to prevent session expiry - Gate export postMessage on isReady to avoid lost messages - Remove unused fmt variable - Fix double extension when path has a different supported extension * fix: resolve high severity npm audit vulnerabilities Run npm audit fix to update @aws-sdk and @smithy transitive dependencies that had high severity advisories, which was failing the CI security audit step. * fix: address second round of PR review feedback - Add 8s timeout for pendingMcpExport to prevent permanent blocking - Move export trigger after version update in poll() to export latest diagram - Return 404 when session not found for exportData POST - Sync browser state before .drawio export to avoid stale XML - Handle URL-encoded SVG data URIs in addition to base64 * fix: address third round of PR review feedback - Sync browser state before PNG/SVG export (not just drawio) - Add 10MB body size limit on POST /api/state - Validate export response format matches request to prevent race conditions * refactor: remove over-engineered defensive code from export feature Strip unnecessary validation/guards added from Copilot review that don't make sense for a localhost-only MCP server: body size limit, type validation, 404 for missing session, lastUpdated refresh, URL-encoded SVG handling. Also deduplicate requestSync call. * refactor: keep original drawio export path unchanged Don't restructure the existing drawio logic - just add png/svg as a separate branch after it. * refactor: remove redundant helper functions, inline state access Remove setExportFormat/getExportData/clearExportData wrappers that were each called once. Access state fields directly via getState(). * chore: bump mcp-server version to 0.1.16
2026-02-07 12:55:09 +09:00
},
],
}
}
feat: add PNG/SVG export to MCP server (#687) * feat: add PNG/SVG export support to MCP server export_diagram tool Previously export_diagram only supported .drawio XML files. This adds PNG and SVG export by leveraging the existing browser sync mechanism: the MCP tool sets an exportFormat flag on the session state, the browser detects it via polling and triggers an iframe export, then POSTs the result back as exportData which the tool reads and writes to disk. * fix: address PR review feedback for export feature - Validate exportData is a string in POST /api/state - Update lastUpdated in setExportFormat to prevent session expiry - Gate export postMessage on isReady to avoid lost messages - Remove unused fmt variable - Fix double extension when path has a different supported extension * fix: resolve high severity npm audit vulnerabilities Run npm audit fix to update @aws-sdk and @smithy transitive dependencies that had high severity advisories, which was failing the CI security audit step. * fix: address second round of PR review feedback - Add 8s timeout for pendingMcpExport to prevent permanent blocking - Move export trigger after version update in poll() to export latest diagram - Return 404 when session not found for exportData POST - Sync browser state before .drawio export to avoid stale XML - Handle URL-encoded SVG data URIs in addition to base64 * fix: address third round of PR review feedback - Sync browser state before PNG/SVG export (not just drawio) - Add 10MB body size limit on POST /api/state - Validate export response format matches request to prevent race conditions * refactor: remove over-engineered defensive code from export feature Strip unnecessary validation/guards added from Copilot review that don't make sense for a localhost-only MCP server: body size limit, type validation, 404 for missing session, lastUpdated refresh, URL-encoded SVG handling. Also deduplicate requestSync call. * refactor: keep original drawio export path unchanged Don't restructure the existing drawio logic - just add png/svg as a separate branch after it. * refactor: remove redundant helper functions, inline state access Remove setExportFormat/getExportData/clearExportData wrappers that were each called once. Access state fields directly via getState(). * chore: bump mcp-server version to 0.1.16
2026-02-07 12:55:09 +09:00
// PNG or SVG: request browser to export via iframe
let filePath = path
if (ext !== `.${detectedFormat}`) {
if (ext === ".drawio" || ext === ".png" || ext === ".svg") {
filePath = filePath.slice(0, -ext.length)
}
filePath = `${filePath}.${detectedFormat}`
}
const absolutePath = nodePath.resolve(filePath)
feat: add PNG/SVG export to MCP server (#687) * feat: add PNG/SVG export support to MCP server export_diagram tool Previously export_diagram only supported .drawio XML files. This adds PNG and SVG export by leveraging the existing browser sync mechanism: the MCP tool sets an exportFormat flag on the session state, the browser detects it via polling and triggers an iframe export, then POSTs the result back as exportData which the tool reads and writes to disk. * fix: address PR review feedback for export feature - Validate exportData is a string in POST /api/state - Update lastUpdated in setExportFormat to prevent session expiry - Gate export postMessage on isReady to avoid lost messages - Remove unused fmt variable - Fix double extension when path has a different supported extension * fix: resolve high severity npm audit vulnerabilities Run npm audit fix to update @aws-sdk and @smithy transitive dependencies that had high severity advisories, which was failing the CI security audit step. * fix: address second round of PR review feedback - Add 8s timeout for pendingMcpExport to prevent permanent blocking - Move export trigger after version update in poll() to export latest diagram - Return 404 when session not found for exportData POST - Sync browser state before .drawio export to avoid stale XML - Handle URL-encoded SVG data URIs in addition to base64 * fix: address third round of PR review feedback - Sync browser state before PNG/SVG export (not just drawio) - Add 10MB body size limit on POST /api/state - Validate export response format matches request to prevent race conditions * refactor: remove over-engineered defensive code from export feature Strip unnecessary validation/guards added from Copilot review that don't make sense for a localhost-only MCP server: body size limit, type validation, 404 for missing session, lastUpdated refresh, URL-encoded SVG handling. Also deduplicate requestSync call. * refactor: keep original drawio export path unchanged Don't restructure the existing drawio logic - just add png/svg as a separate branch after it. * refactor: remove redundant helper functions, inline state access Remove setExportFormat/getExportData/clearExportData wrappers that were each called once. Access state fields directly via getState(). * chore: bump mcp-server version to 0.1.16
2026-02-07 12:55:09 +09:00
const state = getState(currentSession.id)
if (!state) {
return {
content: [
{
type: "text",
text: "Error: Session state not found. Is the browser open?",
},
],
isError: true,
}
}
feat(mcp): add multi-page (mxfile) support to MCP server (#862) * feat(mcp): add multi-page (mxfile) support The MCP server's write path could only address a single drawio page even though the underlying .drawio file format and the embedded editor both natively support multi-page documents. A user asking for "a second page with a CNN diagram" would hit the validator with the error "Expected closing tag </root> but found </mxCell>" because the validator assumed input was a bare <mxGraphModel> and could not walk past the <mxfile><diagram>...</diagram></mxfile> wrapper. This patch closes the gap end to end: * New helper module `pages.ts` centralises page CRUD (normalize, parse, list, find, add, rename, delete) so every layer agrees that the canonical in-memory shape is always <mxfile>. normalizeToMxfile and addPageToDoc both strip any leading <?xml ?> declaration before embedding a fragment inside <diagram> (the declaration is only valid at document start). addPageToDoc explicitly rejects full <mxfile> inputs so a caller cannot accidentally nest a document inside a page. * `xml-validation.ts` now detects an <mxfile> root and scopes the duplicate-id check per <diagram>. The legacy regex check would otherwise reject every multi-page doc, because cells "0" and "1" repeat in each page's <root> by design. The DOM-parse path is gated by a cheap regex pre-check so legacy bare <mxGraphModel> callers don't pay any extra cost. The autoFix duplicate-id rename step is also guarded against mxfile inputs — renaming those sentinel cells would silently break drawio's parent references. * `diagram-operations.ts` accepts an optional PageSelector. For <mxfile> input it resolves the page first and scopes all querySelectorAll calls to that page's <root>, so a delete on page 2's cell "2" no longer touches page 1's cell "2". * `create_new_diagram` accepts either a bare <mxGraphModel> (legacy, auto-wrapped into a single-page mxfile) or a full <mxfile> with N diagrams. All existing single-page callers keep working unchanged. * `edit_diagram`, `get_diagram`, and `export_diagram` gain optional `page_id` / `page_name` / `page_index` parameters. When omitted they target the first page — the "active by convention" default. Tool handlers with all-optional input schemas coalesce missing arguments via `input ?? {}` so a no-args MCP invocation can't crash on destructure before reaching the session-existence check. * New tools: `list_pages`, `add_page`, `rename_page`, `delete_page`. * Page-targeted PNG/SVG export uses a "load + export + restore" dance: the server projects the target page into a single-page <mxfile>, pushes it into the transient state so the browser reloads the iframe with just that page, waits for drawio to render (~3s), triggers the export, captures the data, and then restores the original multi-page document. The dance is wrapped in `try/finally` so the restore runs unconditionally — even if an exception is thrown mid-dance, the user's multi-tab view is recovered before the function returns. The earlier attempt to use drawio's `selectPage` postMessage was a no-op because drawio's JSON embed protocol does not expose that action — silently exporting whatever tab happened to be active. The load-export-restore approach trades a brief visible tab-flicker for correctness: the exported image is guaranteed to match the requested page. * Tool description strings reflect the multi-page semantics so the LLM client learns the new contract. * Package version bumped 0.2.0 → 0.3.0 (additive surface — four new tools, three extended input schemas, canonical XML shape change). * CI: `.github/workflows/test.yml` gains an explicit install + vitest run for the mcp-server package so the new multi-page invariants are covered by automation, not just local runs. Backward compatibility: every existing single-page caller continues to work without modification. The session.xml shape is normalised on every write, removing the wrapper-injection hack from the .drawio download path. Tests: 43 unit tests under `packages/mcp-server/tests/multi-page.test.ts` pin the validator's mxfile path, the page-scoped operations, the XML declaration-prefix handling for both normalizeToMxfile and addPageToDoc, addPageToDoc's rejection of full <mxfile> inputs, the single-page projection used by export_diagram (a direct regression test for the selectPage bug — two distinct page selectors must produce visually different projections), and the Transformer + CNN motivating scenario. A `tests/smoke.mjs` smoke test drives the built `dist/index.js` over JSON-RPC and asserts all 9 tools register with the right input schemas. Root vitest suite (107 tests) still green. * fix(mcp): rewrite page-targeted export browser-side; harden edit/get The page-targeted PNG/SVG export never worked: export_diagram swapped the live session to a single-page projection, slept 3s, then wrote the export flag onto a state object that setState() had already replaced in the store Map — so the browser never saw the request and every such export timed out. The swap+restore also clobbered concurrent edits. Move the projection entirely browser-side: requestExport() hands a single -page <mxfile> to the bridge via state.exportXml; the bridge loads it, lets draw.io render, exports, then reloads the user's real document. The canonical session state is never mutated, so there is no restore race and no fixed-delay guessing. The export poll now re-reads the live store entry each tick instead of a captured reference. autosave is suppressed and the version-bump reload is skipped while a projection is on screen; if no real document was captured, restore forces a server reload rather than leaving the iframe stuck on the projection. Also: - edit_diagram now returns isError on a page-level failure (selector matched no page / page has no <root>) instead of reporting success-with-warnings and persisting a no-op; the pre-edit history snapshot is taken only after that gate so a failed edit leaves no phantom undo entry. - edit_diagram/get_diagram re-normalise browser-pushed xml to mxfile so a bare <mxGraphModel> can't silently strip a multi-page document. - get_diagram now errors (instead of silently returning the full doc) when a selector is given but the session isn't a parseable mxfile. - page_id / page_name / add_page.id get .min(1) so empty strings can't silently target the first page. - Extract pages.ts:projectPage(), collapsing three copies of the parse→find→serialise projection logic in index.ts. - Replace the never-in-CI tests/smoke.mjs with tests/server-wiring.test.ts, which boots the server from source via tsx and runs under the existing vitest CI step. * chore(mcp): set version to 0.2.1 for release --------- Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2026-06-16 05:45:50 +05:30
// -----------------------------------------------------------------
// Page-targeted PNG/SVG export.
//
// drawio's JSON embed protocol has no working `selectPage` action,
// so to export a specific page we build a single-page <mxfile>
// projection and hand it to the browser bridge alongside the export
// request. The bridge loads the projection, waits for draw.io's own
// render, exports, then reloads the user's real document — entirely
// browser-side. The canonical session state is never mutated here,
// so there is no restore race and no concurrent-edit clobbering.
// -----------------------------------------------------------------
let projectionXml: string | undefined
if (hasPageSelector(pageSelector)) {
const projection = projectPage(currentSession.xml, pageSelector)
if (!projection.ok) {
return {
content: [
{
type: "text",
text:
projection.reason === "parse"
? "Error: Cannot parse current session XML as <mxfile>; cannot target page for export."
: `Error: Page ${describeSelector(pageSelector)} not found for export.`,
},
],
isError: true,
}
}
projectionXml = projection.xml
}
// Ask the browser to export (optionally via a page projection) and
// poll for the resulting image data.
requestExport(
currentSession.id,
detectedFormat as "png" | "svg",
projectionXml,
)
// A projection export does an extra load + render round-trip in the
// browser, so give it a longer window. Re-read the live store entry
// each tick: setState() (from a concurrent autosave or tool call)
// replaces the Map entry with a new object, so a captured reference
// would go stale and never observe the browser's exportData.
const timeoutMs = projectionXml ? 15000 : 10000
feat: add PNG/SVG export to MCP server (#687) * feat: add PNG/SVG export support to MCP server export_diagram tool Previously export_diagram only supported .drawio XML files. This adds PNG and SVG export by leveraging the existing browser sync mechanism: the MCP tool sets an exportFormat flag on the session state, the browser detects it via polling and triggers an iframe export, then POSTs the result back as exportData which the tool reads and writes to disk. * fix: address PR review feedback for export feature - Validate exportData is a string in POST /api/state - Update lastUpdated in setExportFormat to prevent session expiry - Gate export postMessage on isReady to avoid lost messages - Remove unused fmt variable - Fix double extension when path has a different supported extension * fix: resolve high severity npm audit vulnerabilities Run npm audit fix to update @aws-sdk and @smithy transitive dependencies that had high severity advisories, which was failing the CI security audit step. * fix: address second round of PR review feedback - Add 8s timeout for pendingMcpExport to prevent permanent blocking - Move export trigger after version update in poll() to export latest diagram - Return 404 when session not found for exportData POST - Sync browser state before .drawio export to avoid stale XML - Handle URL-encoded SVG data URIs in addition to base64 * fix: address third round of PR review feedback - Sync browser state before PNG/SVG export (not just drawio) - Add 10MB body size limit on POST /api/state - Validate export response format matches request to prevent race conditions * refactor: remove over-engineered defensive code from export feature Strip unnecessary validation/guards added from Copilot review that don't make sense for a localhost-only MCP server: body size limit, type validation, 404 for missing session, lastUpdated refresh, URL-encoded SVG handling. Also deduplicate requestSync call. * refactor: keep original drawio export path unchanged Don't restructure the existing drawio logic - just add png/svg as a separate branch after it. * refactor: remove redundant helper functions, inline state access Remove setExportFormat/getExportData/clearExportData wrappers that were each called once. Access state fields directly via getState(). * chore: bump mcp-server version to 0.1.16
2026-02-07 12:55:09 +09:00
const start = Date.now()
feat(mcp): add multi-page (mxfile) support to MCP server (#862) * feat(mcp): add multi-page (mxfile) support The MCP server's write path could only address a single drawio page even though the underlying .drawio file format and the embedded editor both natively support multi-page documents. A user asking for "a second page with a CNN diagram" would hit the validator with the error "Expected closing tag </root> but found </mxCell>" because the validator assumed input was a bare <mxGraphModel> and could not walk past the <mxfile><diagram>...</diagram></mxfile> wrapper. This patch closes the gap end to end: * New helper module `pages.ts` centralises page CRUD (normalize, parse, list, find, add, rename, delete) so every layer agrees that the canonical in-memory shape is always <mxfile>. normalizeToMxfile and addPageToDoc both strip any leading <?xml ?> declaration before embedding a fragment inside <diagram> (the declaration is only valid at document start). addPageToDoc explicitly rejects full <mxfile> inputs so a caller cannot accidentally nest a document inside a page. * `xml-validation.ts` now detects an <mxfile> root and scopes the duplicate-id check per <diagram>. The legacy regex check would otherwise reject every multi-page doc, because cells "0" and "1" repeat in each page's <root> by design. The DOM-parse path is gated by a cheap regex pre-check so legacy bare <mxGraphModel> callers don't pay any extra cost. The autoFix duplicate-id rename step is also guarded against mxfile inputs — renaming those sentinel cells would silently break drawio's parent references. * `diagram-operations.ts` accepts an optional PageSelector. For <mxfile> input it resolves the page first and scopes all querySelectorAll calls to that page's <root>, so a delete on page 2's cell "2" no longer touches page 1's cell "2". * `create_new_diagram` accepts either a bare <mxGraphModel> (legacy, auto-wrapped into a single-page mxfile) or a full <mxfile> with N diagrams. All existing single-page callers keep working unchanged. * `edit_diagram`, `get_diagram`, and `export_diagram` gain optional `page_id` / `page_name` / `page_index` parameters. When omitted they target the first page — the "active by convention" default. Tool handlers with all-optional input schemas coalesce missing arguments via `input ?? {}` so a no-args MCP invocation can't crash on destructure before reaching the session-existence check. * New tools: `list_pages`, `add_page`, `rename_page`, `delete_page`. * Page-targeted PNG/SVG export uses a "load + export + restore" dance: the server projects the target page into a single-page <mxfile>, pushes it into the transient state so the browser reloads the iframe with just that page, waits for drawio to render (~3s), triggers the export, captures the data, and then restores the original multi-page document. The dance is wrapped in `try/finally` so the restore runs unconditionally — even if an exception is thrown mid-dance, the user's multi-tab view is recovered before the function returns. The earlier attempt to use drawio's `selectPage` postMessage was a no-op because drawio's JSON embed protocol does not expose that action — silently exporting whatever tab happened to be active. The load-export-restore approach trades a brief visible tab-flicker for correctness: the exported image is guaranteed to match the requested page. * Tool description strings reflect the multi-page semantics so the LLM client learns the new contract. * Package version bumped 0.2.0 → 0.3.0 (additive surface — four new tools, three extended input schemas, canonical XML shape change). * CI: `.github/workflows/test.yml` gains an explicit install + vitest run for the mcp-server package so the new multi-page invariants are covered by automation, not just local runs. Backward compatibility: every existing single-page caller continues to work without modification. The session.xml shape is normalised on every write, removing the wrapper-injection hack from the .drawio download path. Tests: 43 unit tests under `packages/mcp-server/tests/multi-page.test.ts` pin the validator's mxfile path, the page-scoped operations, the XML declaration-prefix handling for both normalizeToMxfile and addPageToDoc, addPageToDoc's rejection of full <mxfile> inputs, the single-page projection used by export_diagram (a direct regression test for the selectPage bug — two distinct page selectors must produce visually different projections), and the Transformer + CNN motivating scenario. A `tests/smoke.mjs` smoke test drives the built `dist/index.js` over JSON-RPC and asserts all 9 tools register with the right input schemas. Root vitest suite (107 tests) still green. * fix(mcp): rewrite page-targeted export browser-side; harden edit/get The page-targeted PNG/SVG export never worked: export_diagram swapped the live session to a single-page projection, slept 3s, then wrote the export flag onto a state object that setState() had already replaced in the store Map — so the browser never saw the request and every such export timed out. The swap+restore also clobbered concurrent edits. Move the projection entirely browser-side: requestExport() hands a single -page <mxfile> to the bridge via state.exportXml; the bridge loads it, lets draw.io render, exports, then reloads the user's real document. The canonical session state is never mutated, so there is no restore race and no fixed-delay guessing. The export poll now re-reads the live store entry each tick instead of a captured reference. autosave is suppressed and the version-bump reload is skipped while a projection is on screen; if no real document was captured, restore forces a server reload rather than leaving the iframe stuck on the projection. Also: - edit_diagram now returns isError on a page-level failure (selector matched no page / page has no <root>) instead of reporting success-with-warnings and persisting a no-op; the pre-edit history snapshot is taken only after that gate so a failed edit leaves no phantom undo entry. - edit_diagram/get_diagram re-normalise browser-pushed xml to mxfile so a bare <mxGraphModel> can't silently strip a multi-page document. - get_diagram now errors (instead of silently returning the full doc) when a selector is given but the session isn't a parseable mxfile. - page_id / page_name / add_page.id get .min(1) so empty strings can't silently target the first page. - Extract pages.ts:projectPage(), collapsing three copies of the parse→find→serialise projection logic in index.ts. - Replace the never-in-CI tests/smoke.mjs with tests/server-wiring.test.ts, which boots the server from source via tsx and runs under the existing vitest CI step. * chore(mcp): set version to 0.2.1 for release --------- Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2026-06-16 05:45:50 +05:30
let exportData: string | undefined
feat: add PNG/SVG export to MCP server (#687) * feat: add PNG/SVG export support to MCP server export_diagram tool Previously export_diagram only supported .drawio XML files. This adds PNG and SVG export by leveraging the existing browser sync mechanism: the MCP tool sets an exportFormat flag on the session state, the browser detects it via polling and triggers an iframe export, then POSTs the result back as exportData which the tool reads and writes to disk. * fix: address PR review feedback for export feature - Validate exportData is a string in POST /api/state - Update lastUpdated in setExportFormat to prevent session expiry - Gate export postMessage on isReady to avoid lost messages - Remove unused fmt variable - Fix double extension when path has a different supported extension * fix: resolve high severity npm audit vulnerabilities Run npm audit fix to update @aws-sdk and @smithy transitive dependencies that had high severity advisories, which was failing the CI security audit step. * fix: address second round of PR review feedback - Add 8s timeout for pendingMcpExport to prevent permanent blocking - Move export trigger after version update in poll() to export latest diagram - Return 404 when session not found for exportData POST - Sync browser state before .drawio export to avoid stale XML - Handle URL-encoded SVG data URIs in addition to base64 * fix: address third round of PR review feedback - Sync browser state before PNG/SVG export (not just drawio) - Add 10MB body size limit on POST /api/state - Validate export response format matches request to prevent race conditions * refactor: remove over-engineered defensive code from export feature Strip unnecessary validation/guards added from Copilot review that don't make sense for a localhost-only MCP server: body size limit, type validation, 404 for missing session, lastUpdated refresh, URL-encoded SVG handling. Also deduplicate requestSync call. * refactor: keep original drawio export path unchanged Don't restructure the existing drawio logic - just add png/svg as a separate branch after it. * refactor: remove redundant helper functions, inline state access Remove setExportFormat/getExportData/clearExportData wrappers that were each called once. Access state fields directly via getState(). * chore: bump mcp-server version to 0.1.16
2026-02-07 12:55:09 +09:00
while (Date.now() - start < timeoutMs) {
feat(mcp): add multi-page (mxfile) support to MCP server (#862) * feat(mcp): add multi-page (mxfile) support The MCP server's write path could only address a single drawio page even though the underlying .drawio file format and the embedded editor both natively support multi-page documents. A user asking for "a second page with a CNN diagram" would hit the validator with the error "Expected closing tag </root> but found </mxCell>" because the validator assumed input was a bare <mxGraphModel> and could not walk past the <mxfile><diagram>...</diagram></mxfile> wrapper. This patch closes the gap end to end: * New helper module `pages.ts` centralises page CRUD (normalize, parse, list, find, add, rename, delete) so every layer agrees that the canonical in-memory shape is always <mxfile>. normalizeToMxfile and addPageToDoc both strip any leading <?xml ?> declaration before embedding a fragment inside <diagram> (the declaration is only valid at document start). addPageToDoc explicitly rejects full <mxfile> inputs so a caller cannot accidentally nest a document inside a page. * `xml-validation.ts` now detects an <mxfile> root and scopes the duplicate-id check per <diagram>. The legacy regex check would otherwise reject every multi-page doc, because cells "0" and "1" repeat in each page's <root> by design. The DOM-parse path is gated by a cheap regex pre-check so legacy bare <mxGraphModel> callers don't pay any extra cost. The autoFix duplicate-id rename step is also guarded against mxfile inputs — renaming those sentinel cells would silently break drawio's parent references. * `diagram-operations.ts` accepts an optional PageSelector. For <mxfile> input it resolves the page first and scopes all querySelectorAll calls to that page's <root>, so a delete on page 2's cell "2" no longer touches page 1's cell "2". * `create_new_diagram` accepts either a bare <mxGraphModel> (legacy, auto-wrapped into a single-page mxfile) or a full <mxfile> with N diagrams. All existing single-page callers keep working unchanged. * `edit_diagram`, `get_diagram`, and `export_diagram` gain optional `page_id` / `page_name` / `page_index` parameters. When omitted they target the first page — the "active by convention" default. Tool handlers with all-optional input schemas coalesce missing arguments via `input ?? {}` so a no-args MCP invocation can't crash on destructure before reaching the session-existence check. * New tools: `list_pages`, `add_page`, `rename_page`, `delete_page`. * Page-targeted PNG/SVG export uses a "load + export + restore" dance: the server projects the target page into a single-page <mxfile>, pushes it into the transient state so the browser reloads the iframe with just that page, waits for drawio to render (~3s), triggers the export, captures the data, and then restores the original multi-page document. The dance is wrapped in `try/finally` so the restore runs unconditionally — even if an exception is thrown mid-dance, the user's multi-tab view is recovered before the function returns. The earlier attempt to use drawio's `selectPage` postMessage was a no-op because drawio's JSON embed protocol does not expose that action — silently exporting whatever tab happened to be active. The load-export-restore approach trades a brief visible tab-flicker for correctness: the exported image is guaranteed to match the requested page. * Tool description strings reflect the multi-page semantics so the LLM client learns the new contract. * Package version bumped 0.2.0 → 0.3.0 (additive surface — four new tools, three extended input schemas, canonical XML shape change). * CI: `.github/workflows/test.yml` gains an explicit install + vitest run for the mcp-server package so the new multi-page invariants are covered by automation, not just local runs. Backward compatibility: every existing single-page caller continues to work without modification. The session.xml shape is normalised on every write, removing the wrapper-injection hack from the .drawio download path. Tests: 43 unit tests under `packages/mcp-server/tests/multi-page.test.ts` pin the validator's mxfile path, the page-scoped operations, the XML declaration-prefix handling for both normalizeToMxfile and addPageToDoc, addPageToDoc's rejection of full <mxfile> inputs, the single-page projection used by export_diagram (a direct regression test for the selectPage bug — two distinct page selectors must produce visually different projections), and the Transformer + CNN motivating scenario. A `tests/smoke.mjs` smoke test drives the built `dist/index.js` over JSON-RPC and asserts all 9 tools register with the right input schemas. Root vitest suite (107 tests) still green. * fix(mcp): rewrite page-targeted export browser-side; harden edit/get The page-targeted PNG/SVG export never worked: export_diagram swapped the live session to a single-page projection, slept 3s, then wrote the export flag onto a state object that setState() had already replaced in the store Map — so the browser never saw the request and every such export timed out. The swap+restore also clobbered concurrent edits. Move the projection entirely browser-side: requestExport() hands a single -page <mxfile> to the bridge via state.exportXml; the bridge loads it, lets draw.io render, exports, then reloads the user's real document. The canonical session state is never mutated, so there is no restore race and no fixed-delay guessing. The export poll now re-reads the live store entry each tick instead of a captured reference. autosave is suppressed and the version-bump reload is skipped while a projection is on screen; if no real document was captured, restore forces a server reload rather than leaving the iframe stuck on the projection. Also: - edit_diagram now returns isError on a page-level failure (selector matched no page / page has no <root>) instead of reporting success-with-warnings and persisting a no-op; the pre-edit history snapshot is taken only after that gate so a failed edit leaves no phantom undo entry. - edit_diagram/get_diagram re-normalise browser-pushed xml to mxfile so a bare <mxGraphModel> can't silently strip a multi-page document. - get_diagram now errors (instead of silently returning the full doc) when a selector is given but the session isn't a parseable mxfile. - page_id / page_name / add_page.id get .min(1) so empty strings can't silently target the first page. - Extract pages.ts:projectPage(), collapsing three copies of the parse→find→serialise projection logic in index.ts. - Replace the never-in-CI tests/smoke.mjs with tests/server-wiring.test.ts, which boots the server from source via tsx and runs under the existing vitest CI step. * chore(mcp): set version to 0.2.1 for release --------- Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2026-06-16 05:45:50 +05:30
exportData = getState(currentSession.id)?.exportData
if (exportData) break
feat: add PNG/SVG export to MCP server (#687) * feat: add PNG/SVG export support to MCP server export_diagram tool Previously export_diagram only supported .drawio XML files. This adds PNG and SVG export by leveraging the existing browser sync mechanism: the MCP tool sets an exportFormat flag on the session state, the browser detects it via polling and triggers an iframe export, then POSTs the result back as exportData which the tool reads and writes to disk. * fix: address PR review feedback for export feature - Validate exportData is a string in POST /api/state - Update lastUpdated in setExportFormat to prevent session expiry - Gate export postMessage on isReady to avoid lost messages - Remove unused fmt variable - Fix double extension when path has a different supported extension * fix: resolve high severity npm audit vulnerabilities Run npm audit fix to update @aws-sdk and @smithy transitive dependencies that had high severity advisories, which was failing the CI security audit step. * fix: address second round of PR review feedback - Add 8s timeout for pendingMcpExport to prevent permanent blocking - Move export trigger after version update in poll() to export latest diagram - Return 404 when session not found for exportData POST - Sync browser state before .drawio export to avoid stale XML - Handle URL-encoded SVG data URIs in addition to base64 * fix: address third round of PR review feedback - Sync browser state before PNG/SVG export (not just drawio) - Add 10MB body size limit on POST /api/state - Validate export response format matches request to prevent race conditions * refactor: remove over-engineered defensive code from export feature Strip unnecessary validation/guards added from Copilot review that don't make sense for a localhost-only MCP server: body size limit, type validation, 404 for missing session, lastUpdated refresh, URL-encoded SVG handling. Also deduplicate requestSync call. * refactor: keep original drawio export path unchanged Don't restructure the existing drawio logic - just add png/svg as a separate branch after it. * refactor: remove redundant helper functions, inline state access Remove setExportFormat/getExportData/clearExportData wrappers that were each called once. Access state fields directly via getState(). * chore: bump mcp-server version to 0.1.16
2026-02-07 12:55:09 +09:00
await new Promise((r) => setTimeout(r, 200))
}
feat(mcp): add multi-page (mxfile) support to MCP server (#862) * feat(mcp): add multi-page (mxfile) support The MCP server's write path could only address a single drawio page even though the underlying .drawio file format and the embedded editor both natively support multi-page documents. A user asking for "a second page with a CNN diagram" would hit the validator with the error "Expected closing tag </root> but found </mxCell>" because the validator assumed input was a bare <mxGraphModel> and could not walk past the <mxfile><diagram>...</diagram></mxfile> wrapper. This patch closes the gap end to end: * New helper module `pages.ts` centralises page CRUD (normalize, parse, list, find, add, rename, delete) so every layer agrees that the canonical in-memory shape is always <mxfile>. normalizeToMxfile and addPageToDoc both strip any leading <?xml ?> declaration before embedding a fragment inside <diagram> (the declaration is only valid at document start). addPageToDoc explicitly rejects full <mxfile> inputs so a caller cannot accidentally nest a document inside a page. * `xml-validation.ts` now detects an <mxfile> root and scopes the duplicate-id check per <diagram>. The legacy regex check would otherwise reject every multi-page doc, because cells "0" and "1" repeat in each page's <root> by design. The DOM-parse path is gated by a cheap regex pre-check so legacy bare <mxGraphModel> callers don't pay any extra cost. The autoFix duplicate-id rename step is also guarded against mxfile inputs — renaming those sentinel cells would silently break drawio's parent references. * `diagram-operations.ts` accepts an optional PageSelector. For <mxfile> input it resolves the page first and scopes all querySelectorAll calls to that page's <root>, so a delete on page 2's cell "2" no longer touches page 1's cell "2". * `create_new_diagram` accepts either a bare <mxGraphModel> (legacy, auto-wrapped into a single-page mxfile) or a full <mxfile> with N diagrams. All existing single-page callers keep working unchanged. * `edit_diagram`, `get_diagram`, and `export_diagram` gain optional `page_id` / `page_name` / `page_index` parameters. When omitted they target the first page — the "active by convention" default. Tool handlers with all-optional input schemas coalesce missing arguments via `input ?? {}` so a no-args MCP invocation can't crash on destructure before reaching the session-existence check. * New tools: `list_pages`, `add_page`, `rename_page`, `delete_page`. * Page-targeted PNG/SVG export uses a "load + export + restore" dance: the server projects the target page into a single-page <mxfile>, pushes it into the transient state so the browser reloads the iframe with just that page, waits for drawio to render (~3s), triggers the export, captures the data, and then restores the original multi-page document. The dance is wrapped in `try/finally` so the restore runs unconditionally — even if an exception is thrown mid-dance, the user's multi-tab view is recovered before the function returns. The earlier attempt to use drawio's `selectPage` postMessage was a no-op because drawio's JSON embed protocol does not expose that action — silently exporting whatever tab happened to be active. The load-export-restore approach trades a brief visible tab-flicker for correctness: the exported image is guaranteed to match the requested page. * Tool description strings reflect the multi-page semantics so the LLM client learns the new contract. * Package version bumped 0.2.0 → 0.3.0 (additive surface — four new tools, three extended input schemas, canonical XML shape change). * CI: `.github/workflows/test.yml` gains an explicit install + vitest run for the mcp-server package so the new multi-page invariants are covered by automation, not just local runs. Backward compatibility: every existing single-page caller continues to work without modification. The session.xml shape is normalised on every write, removing the wrapper-injection hack from the .drawio download path. Tests: 43 unit tests under `packages/mcp-server/tests/multi-page.test.ts` pin the validator's mxfile path, the page-scoped operations, the XML declaration-prefix handling for both normalizeToMxfile and addPageToDoc, addPageToDoc's rejection of full <mxfile> inputs, the single-page projection used by export_diagram (a direct regression test for the selectPage bug — two distinct page selectors must produce visually different projections), and the Transformer + CNN motivating scenario. A `tests/smoke.mjs` smoke test drives the built `dist/index.js` over JSON-RPC and asserts all 9 tools register with the right input schemas. Root vitest suite (107 tests) still green. * fix(mcp): rewrite page-targeted export browser-side; harden edit/get The page-targeted PNG/SVG export never worked: export_diagram swapped the live session to a single-page projection, slept 3s, then wrote the export flag onto a state object that setState() had already replaced in the store Map — so the browser never saw the request and every such export timed out. The swap+restore also clobbered concurrent edits. Move the projection entirely browser-side: requestExport() hands a single -page <mxfile> to the bridge via state.exportXml; the bridge loads it, lets draw.io render, exports, then reloads the user's real document. The canonical session state is never mutated, so there is no restore race and no fixed-delay guessing. The export poll now re-reads the live store entry each tick instead of a captured reference. autosave is suppressed and the version-bump reload is skipped while a projection is on screen; if no real document was captured, restore forces a server reload rather than leaving the iframe stuck on the projection. Also: - edit_diagram now returns isError on a page-level failure (selector matched no page / page has no <root>) instead of reporting success-with-warnings and persisting a no-op; the pre-edit history snapshot is taken only after that gate so a failed edit leaves no phantom undo entry. - edit_diagram/get_diagram re-normalise browser-pushed xml to mxfile so a bare <mxGraphModel> can't silently strip a multi-page document. - get_diagram now errors (instead of silently returning the full doc) when a selector is given but the session isn't a parseable mxfile. - page_id / page_name / add_page.id get .min(1) so empty strings can't silently target the first page. - Extract pages.ts:projectPage(), collapsing three copies of the parse→find→serialise projection logic in index.ts. - Replace the never-in-CI tests/smoke.mjs with tests/server-wiring.test.ts, which boots the server from source via tsx and runs under the existing vitest CI step. * chore(mcp): set version to 0.2.1 for release --------- Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2026-06-16 05:45:50 +05:30
const live = getState(currentSession.id)
if (live) {
live.exportData = undefined
live.exportFormat = undefined
live.exportXml = undefined
}
feat: add PNG/SVG export to MCP server (#687) * feat: add PNG/SVG export support to MCP server export_diagram tool Previously export_diagram only supported .drawio XML files. This adds PNG and SVG export by leveraging the existing browser sync mechanism: the MCP tool sets an exportFormat flag on the session state, the browser detects it via polling and triggers an iframe export, then POSTs the result back as exportData which the tool reads and writes to disk. * fix: address PR review feedback for export feature - Validate exportData is a string in POST /api/state - Update lastUpdated in setExportFormat to prevent session expiry - Gate export postMessage on isReady to avoid lost messages - Remove unused fmt variable - Fix double extension when path has a different supported extension * fix: resolve high severity npm audit vulnerabilities Run npm audit fix to update @aws-sdk and @smithy transitive dependencies that had high severity advisories, which was failing the CI security audit step. * fix: address second round of PR review feedback - Add 8s timeout for pendingMcpExport to prevent permanent blocking - Move export trigger after version update in poll() to export latest diagram - Return 404 when session not found for exportData POST - Sync browser state before .drawio export to avoid stale XML - Handle URL-encoded SVG data URIs in addition to base64 * fix: address third round of PR review feedback - Sync browser state before PNG/SVG export (not just drawio) - Add 10MB body size limit on POST /api/state - Validate export response format matches request to prevent race conditions * refactor: remove over-engineered defensive code from export feature Strip unnecessary validation/guards added from Copilot review that don't make sense for a localhost-only MCP server: body size limit, type validation, 404 for missing session, lastUpdated refresh, URL-encoded SVG handling. Also deduplicate requestSync call. * refactor: keep original drawio export path unchanged Don't restructure the existing drawio logic - just add png/svg as a separate branch after it. * refactor: remove redundant helper functions, inline state access Remove setExportFormat/getExportData/clearExportData wrappers that were each called once. Access state fields directly via getState(). * chore: bump mcp-server version to 0.1.16
2026-02-07 12:55:09 +09:00
if (!exportData) {
return {
content: [
{
type: "text",
feat(mcp): add multi-page (mxfile) support to MCP server (#862) * feat(mcp): add multi-page (mxfile) support The MCP server's write path could only address a single drawio page even though the underlying .drawio file format and the embedded editor both natively support multi-page documents. A user asking for "a second page with a CNN diagram" would hit the validator with the error "Expected closing tag </root> but found </mxCell>" because the validator assumed input was a bare <mxGraphModel> and could not walk past the <mxfile><diagram>...</diagram></mxfile> wrapper. This patch closes the gap end to end: * New helper module `pages.ts` centralises page CRUD (normalize, parse, list, find, add, rename, delete) so every layer agrees that the canonical in-memory shape is always <mxfile>. normalizeToMxfile and addPageToDoc both strip any leading <?xml ?> declaration before embedding a fragment inside <diagram> (the declaration is only valid at document start). addPageToDoc explicitly rejects full <mxfile> inputs so a caller cannot accidentally nest a document inside a page. * `xml-validation.ts` now detects an <mxfile> root and scopes the duplicate-id check per <diagram>. The legacy regex check would otherwise reject every multi-page doc, because cells "0" and "1" repeat in each page's <root> by design. The DOM-parse path is gated by a cheap regex pre-check so legacy bare <mxGraphModel> callers don't pay any extra cost. The autoFix duplicate-id rename step is also guarded against mxfile inputs — renaming those sentinel cells would silently break drawio's parent references. * `diagram-operations.ts` accepts an optional PageSelector. For <mxfile> input it resolves the page first and scopes all querySelectorAll calls to that page's <root>, so a delete on page 2's cell "2" no longer touches page 1's cell "2". * `create_new_diagram` accepts either a bare <mxGraphModel> (legacy, auto-wrapped into a single-page mxfile) or a full <mxfile> with N diagrams. All existing single-page callers keep working unchanged. * `edit_diagram`, `get_diagram`, and `export_diagram` gain optional `page_id` / `page_name` / `page_index` parameters. When omitted they target the first page — the "active by convention" default. Tool handlers with all-optional input schemas coalesce missing arguments via `input ?? {}` so a no-args MCP invocation can't crash on destructure before reaching the session-existence check. * New tools: `list_pages`, `add_page`, `rename_page`, `delete_page`. * Page-targeted PNG/SVG export uses a "load + export + restore" dance: the server projects the target page into a single-page <mxfile>, pushes it into the transient state so the browser reloads the iframe with just that page, waits for drawio to render (~3s), triggers the export, captures the data, and then restores the original multi-page document. The dance is wrapped in `try/finally` so the restore runs unconditionally — even if an exception is thrown mid-dance, the user's multi-tab view is recovered before the function returns. The earlier attempt to use drawio's `selectPage` postMessage was a no-op because drawio's JSON embed protocol does not expose that action — silently exporting whatever tab happened to be active. The load-export-restore approach trades a brief visible tab-flicker for correctness: the exported image is guaranteed to match the requested page. * Tool description strings reflect the multi-page semantics so the LLM client learns the new contract. * Package version bumped 0.2.0 → 0.3.0 (additive surface — four new tools, three extended input schemas, canonical XML shape change). * CI: `.github/workflows/test.yml` gains an explicit install + vitest run for the mcp-server package so the new multi-page invariants are covered by automation, not just local runs. Backward compatibility: every existing single-page caller continues to work without modification. The session.xml shape is normalised on every write, removing the wrapper-injection hack from the .drawio download path. Tests: 43 unit tests under `packages/mcp-server/tests/multi-page.test.ts` pin the validator's mxfile path, the page-scoped operations, the XML declaration-prefix handling for both normalizeToMxfile and addPageToDoc, addPageToDoc's rejection of full <mxfile> inputs, the single-page projection used by export_diagram (a direct regression test for the selectPage bug — two distinct page selectors must produce visually different projections), and the Transformer + CNN motivating scenario. A `tests/smoke.mjs` smoke test drives the built `dist/index.js` over JSON-RPC and asserts all 9 tools register with the right input schemas. Root vitest suite (107 tests) still green. * fix(mcp): rewrite page-targeted export browser-side; harden edit/get The page-targeted PNG/SVG export never worked: export_diagram swapped the live session to a single-page projection, slept 3s, then wrote the export flag onto a state object that setState() had already replaced in the store Map — so the browser never saw the request and every such export timed out. The swap+restore also clobbered concurrent edits. Move the projection entirely browser-side: requestExport() hands a single -page <mxfile> to the bridge via state.exportXml; the bridge loads it, lets draw.io render, exports, then reloads the user's real document. The canonical session state is never mutated, so there is no restore race and no fixed-delay guessing. The export poll now re-reads the live store entry each tick instead of a captured reference. autosave is suppressed and the version-bump reload is skipped while a projection is on screen; if no real document was captured, restore forces a server reload rather than leaving the iframe stuck on the projection. Also: - edit_diagram now returns isError on a page-level failure (selector matched no page / page has no <root>) instead of reporting success-with-warnings and persisting a no-op; the pre-edit history snapshot is taken only after that gate so a failed edit leaves no phantom undo entry. - edit_diagram/get_diagram re-normalise browser-pushed xml to mxfile so a bare <mxGraphModel> can't silently strip a multi-page document. - get_diagram now errors (instead of silently returning the full doc) when a selector is given but the session isn't a parseable mxfile. - page_id / page_name / add_page.id get .min(1) so empty strings can't silently target the first page. - Extract pages.ts:projectPage(), collapsing three copies of the parse→find→serialise projection logic in index.ts. - Replace the never-in-CI tests/smoke.mjs with tests/server-wiring.test.ts, which boots the server from source via tsx and runs under the existing vitest CI step. * chore(mcp): set version to 0.2.1 for release --------- Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2026-06-16 05:45:50 +05:30
text: projectionXml
? "Error: Export timed out after loading the single-page projection. The browser may be closed or unresponsive."
: "Error: Export timed out. Make sure the browser tab is open and the diagram is loaded.",
feat: add PNG/SVG export to MCP server (#687) * feat: add PNG/SVG export support to MCP server export_diagram tool Previously export_diagram only supported .drawio XML files. This adds PNG and SVG export by leveraging the existing browser sync mechanism: the MCP tool sets an exportFormat flag on the session state, the browser detects it via polling and triggers an iframe export, then POSTs the result back as exportData which the tool reads and writes to disk. * fix: address PR review feedback for export feature - Validate exportData is a string in POST /api/state - Update lastUpdated in setExportFormat to prevent session expiry - Gate export postMessage on isReady to avoid lost messages - Remove unused fmt variable - Fix double extension when path has a different supported extension * fix: resolve high severity npm audit vulnerabilities Run npm audit fix to update @aws-sdk and @smithy transitive dependencies that had high severity advisories, which was failing the CI security audit step. * fix: address second round of PR review feedback - Add 8s timeout for pendingMcpExport to prevent permanent blocking - Move export trigger after version update in poll() to export latest diagram - Return 404 when session not found for exportData POST - Sync browser state before .drawio export to avoid stale XML - Handle URL-encoded SVG data URIs in addition to base64 * fix: address third round of PR review feedback - Sync browser state before PNG/SVG export (not just drawio) - Add 10MB body size limit on POST /api/state - Validate export response format matches request to prevent race conditions * refactor: remove over-engineered defensive code from export feature Strip unnecessary validation/guards added from Copilot review that don't make sense for a localhost-only MCP server: body size limit, type validation, 404 for missing session, lastUpdated refresh, URL-encoded SVG handling. Also deduplicate requestSync call. * refactor: keep original drawio export path unchanged Don't restructure the existing drawio logic - just add png/svg as a separate branch after it. * refactor: remove redundant helper functions, inline state access Remove setExportFormat/getExportData/clearExportData wrappers that were each called once. Access state fields directly via getState(). * chore: bump mcp-server version to 0.1.16
2026-02-07 12:55:09 +09:00
},
],
isError: true,
}
}
// Decode and write
if (detectedFormat === "png") {
const base64 = exportData.replace(
/^data:image\/png;base64,/,
"",
)
await fs.writeFile(absolutePath, Buffer.from(base64, "base64"))
} else {
let svgContent = exportData
if (svgContent.startsWith("data:image/svg+xml;base64,")) {
const base64 = svgContent.replace(
/^data:image\/svg\+xml;base64,/,
"",
)
svgContent = Buffer.from(base64, "base64").toString("utf-8")
}
await fs.writeFile(absolutePath, svgContent, "utf-8")
}
feat: add PNG/SVG export to MCP server (#687) * feat: add PNG/SVG export support to MCP server export_diagram tool Previously export_diagram only supported .drawio XML files. This adds PNG and SVG export by leveraging the existing browser sync mechanism: the MCP tool sets an exportFormat flag on the session state, the browser detects it via polling and triggers an iframe export, then POSTs the result back as exportData which the tool reads and writes to disk. * fix: address PR review feedback for export feature - Validate exportData is a string in POST /api/state - Update lastUpdated in setExportFormat to prevent session expiry - Gate export postMessage on isReady to avoid lost messages - Remove unused fmt variable - Fix double extension when path has a different supported extension * fix: resolve high severity npm audit vulnerabilities Run npm audit fix to update @aws-sdk and @smithy transitive dependencies that had high severity advisories, which was failing the CI security audit step. * fix: address second round of PR review feedback - Add 8s timeout for pendingMcpExport to prevent permanent blocking - Move export trigger after version update in poll() to export latest diagram - Return 404 when session not found for exportData POST - Sync browser state before .drawio export to avoid stale XML - Handle URL-encoded SVG data URIs in addition to base64 * fix: address third round of PR review feedback - Sync browser state before PNG/SVG export (not just drawio) - Add 10MB body size limit on POST /api/state - Validate export response format matches request to prevent race conditions * refactor: remove over-engineered defensive code from export feature Strip unnecessary validation/guards added from Copilot review that don't make sense for a localhost-only MCP server: body size limit, type validation, 404 for missing session, lastUpdated refresh, URL-encoded SVG handling. Also deduplicate requestSync call. * refactor: keep original drawio export path unchanged Don't restructure the existing drawio logic - just add png/svg as a separate branch after it. * refactor: remove redundant helper functions, inline state access Remove setExportFormat/getExportData/clearExportData wrappers that were each called once. Access state fields directly via getState(). * chore: bump mcp-server version to 0.1.16
2026-02-07 12:55:09 +09:00
const stat = await fs.stat(absolutePath)
log.info(
`Diagram exported to ${absolutePath} (${detectedFormat}, ${stat.size} bytes)`,
)
return {
content: [
{
type: "text",
feat: add PNG/SVG export to MCP server (#687) * feat: add PNG/SVG export support to MCP server export_diagram tool Previously export_diagram only supported .drawio XML files. This adds PNG and SVG export by leveraging the existing browser sync mechanism: the MCP tool sets an exportFormat flag on the session state, the browser detects it via polling and triggers an iframe export, then POSTs the result back as exportData which the tool reads and writes to disk. * fix: address PR review feedback for export feature - Validate exportData is a string in POST /api/state - Update lastUpdated in setExportFormat to prevent session expiry - Gate export postMessage on isReady to avoid lost messages - Remove unused fmt variable - Fix double extension when path has a different supported extension * fix: resolve high severity npm audit vulnerabilities Run npm audit fix to update @aws-sdk and @smithy transitive dependencies that had high severity advisories, which was failing the CI security audit step. * fix: address second round of PR review feedback - Add 8s timeout for pendingMcpExport to prevent permanent blocking - Move export trigger after version update in poll() to export latest diagram - Return 404 when session not found for exportData POST - Sync browser state before .drawio export to avoid stale XML - Handle URL-encoded SVG data URIs in addition to base64 * fix: address third round of PR review feedback - Sync browser state before PNG/SVG export (not just drawio) - Add 10MB body size limit on POST /api/state - Validate export response format matches request to prevent race conditions * refactor: remove over-engineered defensive code from export feature Strip unnecessary validation/guards added from Copilot review that don't make sense for a localhost-only MCP server: body size limit, type validation, 404 for missing session, lastUpdated refresh, URL-encoded SVG handling. Also deduplicate requestSync call. * refactor: keep original drawio export path unchanged Don't restructure the existing drawio logic - just add png/svg as a separate branch after it. * refactor: remove redundant helper functions, inline state access Remove setExportFormat/getExportData/clearExportData wrappers that were each called once. Access state fields directly via getState(). * chore: bump mcp-server version to 0.1.16
2026-02-07 12:55:09 +09:00
text: `Diagram exported successfully!\n\nFile: ${absolutePath}\nFormat: ${detectedFormat}\nSize: ${stat.size} bytes`,
},
],
}
} catch (error) {
const message =
error instanceof Error ? error.message : String(error)
log.error("export_diagram failed:", message)
return {
content: [{ type: "text", text: `Error: ${message}` }],
isError: true,
}
}
},
)
feat(mcp): add multi-page (mxfile) support to MCP server (#862) * feat(mcp): add multi-page (mxfile) support The MCP server's write path could only address a single drawio page even though the underlying .drawio file format and the embedded editor both natively support multi-page documents. A user asking for "a second page with a CNN diagram" would hit the validator with the error "Expected closing tag </root> but found </mxCell>" because the validator assumed input was a bare <mxGraphModel> and could not walk past the <mxfile><diagram>...</diagram></mxfile> wrapper. This patch closes the gap end to end: * New helper module `pages.ts` centralises page CRUD (normalize, parse, list, find, add, rename, delete) so every layer agrees that the canonical in-memory shape is always <mxfile>. normalizeToMxfile and addPageToDoc both strip any leading <?xml ?> declaration before embedding a fragment inside <diagram> (the declaration is only valid at document start). addPageToDoc explicitly rejects full <mxfile> inputs so a caller cannot accidentally nest a document inside a page. * `xml-validation.ts` now detects an <mxfile> root and scopes the duplicate-id check per <diagram>. The legacy regex check would otherwise reject every multi-page doc, because cells "0" and "1" repeat in each page's <root> by design. The DOM-parse path is gated by a cheap regex pre-check so legacy bare <mxGraphModel> callers don't pay any extra cost. The autoFix duplicate-id rename step is also guarded against mxfile inputs — renaming those sentinel cells would silently break drawio's parent references. * `diagram-operations.ts` accepts an optional PageSelector. For <mxfile> input it resolves the page first and scopes all querySelectorAll calls to that page's <root>, so a delete on page 2's cell "2" no longer touches page 1's cell "2". * `create_new_diagram` accepts either a bare <mxGraphModel> (legacy, auto-wrapped into a single-page mxfile) or a full <mxfile> with N diagrams. All existing single-page callers keep working unchanged. * `edit_diagram`, `get_diagram`, and `export_diagram` gain optional `page_id` / `page_name` / `page_index` parameters. When omitted they target the first page — the "active by convention" default. Tool handlers with all-optional input schemas coalesce missing arguments via `input ?? {}` so a no-args MCP invocation can't crash on destructure before reaching the session-existence check. * New tools: `list_pages`, `add_page`, `rename_page`, `delete_page`. * Page-targeted PNG/SVG export uses a "load + export + restore" dance: the server projects the target page into a single-page <mxfile>, pushes it into the transient state so the browser reloads the iframe with just that page, waits for drawio to render (~3s), triggers the export, captures the data, and then restores the original multi-page document. The dance is wrapped in `try/finally` so the restore runs unconditionally — even if an exception is thrown mid-dance, the user's multi-tab view is recovered before the function returns. The earlier attempt to use drawio's `selectPage` postMessage was a no-op because drawio's JSON embed protocol does not expose that action — silently exporting whatever tab happened to be active. The load-export-restore approach trades a brief visible tab-flicker for correctness: the exported image is guaranteed to match the requested page. * Tool description strings reflect the multi-page semantics so the LLM client learns the new contract. * Package version bumped 0.2.0 → 0.3.0 (additive surface — four new tools, three extended input schemas, canonical XML shape change). * CI: `.github/workflows/test.yml` gains an explicit install + vitest run for the mcp-server package so the new multi-page invariants are covered by automation, not just local runs. Backward compatibility: every existing single-page caller continues to work without modification. The session.xml shape is normalised on every write, removing the wrapper-injection hack from the .drawio download path. Tests: 43 unit tests under `packages/mcp-server/tests/multi-page.test.ts` pin the validator's mxfile path, the page-scoped operations, the XML declaration-prefix handling for both normalizeToMxfile and addPageToDoc, addPageToDoc's rejection of full <mxfile> inputs, the single-page projection used by export_diagram (a direct regression test for the selectPage bug — two distinct page selectors must produce visually different projections), and the Transformer + CNN motivating scenario. A `tests/smoke.mjs` smoke test drives the built `dist/index.js` over JSON-RPC and asserts all 9 tools register with the right input schemas. Root vitest suite (107 tests) still green. * fix(mcp): rewrite page-targeted export browser-side; harden edit/get The page-targeted PNG/SVG export never worked: export_diagram swapped the live session to a single-page projection, slept 3s, then wrote the export flag onto a state object that setState() had already replaced in the store Map — so the browser never saw the request and every such export timed out. The swap+restore also clobbered concurrent edits. Move the projection entirely browser-side: requestExport() hands a single -page <mxfile> to the bridge via state.exportXml; the bridge loads it, lets draw.io render, exports, then reloads the user's real document. The canonical session state is never mutated, so there is no restore race and no fixed-delay guessing. The export poll now re-reads the live store entry each tick instead of a captured reference. autosave is suppressed and the version-bump reload is skipped while a projection is on screen; if no real document was captured, restore forces a server reload rather than leaving the iframe stuck on the projection. Also: - edit_diagram now returns isError on a page-level failure (selector matched no page / page has no <root>) instead of reporting success-with-warnings and persisting a no-op; the pre-edit history snapshot is taken only after that gate so a failed edit leaves no phantom undo entry. - edit_diagram/get_diagram re-normalise browser-pushed xml to mxfile so a bare <mxGraphModel> can't silently strip a multi-page document. - get_diagram now errors (instead of silently returning the full doc) when a selector is given but the session isn't a parseable mxfile. - page_id / page_name / add_page.id get .min(1) so empty strings can't silently target the first page. - Extract pages.ts:projectPage(), collapsing three copies of the parse→find→serialise projection logic in index.ts. - Replace the never-in-CI tests/smoke.mjs with tests/server-wiring.test.ts, which boots the server from source via tsx and runs under the existing vitest CI step. * chore(mcp): set version to 0.2.1 for release --------- Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2026-06-16 05:45:50 +05:30
/**
* Shared helper for page-CRUD tools.
* Loads the latest session XML, normalises to mxfile if needed, returns a
* parsed Document the caller can mutate, plus a writer that persists.
*/
async function loadMxfileForMutation(): Promise<
| { ok: true; doc: Document; writeBack: (newDoc: Document) => void }
| { ok: false; message: string }
> {
if (!currentSession) {
return {
ok: false,
message: "No active session. Please call start_session first.",
}
}
// Pull latest from browser so we don't clobber autosaved changes.
const browserState = getState(currentSession.id)
if (browserState?.xml) {
currentSession.xml = browserState.xml
}
if (!currentSession.xml) {
return {
ok: false,
message:
"No diagram exists yet. Use create_new_diagram first, then page tools.",
}
}
// Make sure the in-memory shape is canonical mxfile before any CRUD.
const normalized = normalizeToMxfile(currentSession.xml)
if (!normalized) {
return {
ok: false,
message:
"Current session XML is neither <mxGraphModel> nor <mxfile>; cannot perform page operations.",
}
}
currentSession.xml = normalized
const doc = parseMxfile(currentSession.xml)
if (!doc) {
return {
ok: false,
message: "Failed to parse current session XML as <mxfile>.",
}
}
const sessionRef = currentSession
return {
ok: true,
doc,
writeBack: (newDoc: Document) => {
const newXml = serializeMxfile(newDoc)
// Save history before overwriting so the user can undo.
addHistory(sessionRef.id, sessionRef.xml, browserState?.svg || "")
sessionRef.xml = newXml
sessionRef.version++
setState(sessionRef.id, newXml)
fix(mcp): replace edit_diagram 30s time gate with content comparison (#890) * fix(mcp): keep diagram context valid during edits Closes #885 * fix(mcp): replace edit_diagram time gate with content comparison The 30s wall-clock gate rejected slow-but-correct clients (#885). Instead of a timeout, remember the exact state-store XML the model last saw (get_diagram / create_new_diagram / edit_diagram / page CRUD) and reject edit_diagram only when the live browser state differs - i.e. the user made edits the model hasn't seen yet. Slow reasoning no longer trips the gate, while unseen manual edits still do. * docs(mcp): align edit_diagram/get_diagram descriptions with content-based gate The 'You MUST call get_diagram BEFORE this tool' requirement and the 'Skipping get_diagram WILL cause user's changes to be LOST' warning no longer match server behavior: a stale edit is rejected with no side effects, never silently applied. Describe the freshness check instead, and direct get_diagram usage at its real purpose - learning the current diagram content when the model doesn't already know it. * fix(mcp): compare diagram content structurally in the edit gate draw.io re-serialises the document when pushing state back (attribute order, pretty-printing, regenerated diagram ids, viewport attributes, mxfile host), so byte comparison could flag an unchanged diagram as stale. Fingerprint what a user can actually change instead - page set, page names, and each page's root cell tree with sorted attributes - keeping byte equality as the fast path. A bare mxGraphModel now also fingerprints identically to its single-page mxfile wrapping. * fix(mcp): don't compare page names against bare mxGraphModel pushes A bare <mxGraphModel> pushed by the embed/sync path carries no page name, so normalizeToMxfile invents "Page-1" — falsely reading any custom page name as a content change and re-triggering the stale rejection on every edit. When either side of the gate comparison is a bare mxGraphModel, fingerprint cell trees only; full-mxfile comparisons still detect renames. * chore(mcp): bump version to 0.2.2 * chore(mcp): sync package-lock.json version to 0.2.2 --------- Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2026-07-12 13:33:20 +07:00
// The model just wrote this exact state, so mark it as seen —
// subsequent edit_diagram calls don't need a redundant
// get_diagram round-trip.
sessionRef.lastSeenXml = newXml
feat(mcp): add multi-page (mxfile) support to MCP server (#862) * feat(mcp): add multi-page (mxfile) support The MCP server's write path could only address a single drawio page even though the underlying .drawio file format and the embedded editor both natively support multi-page documents. A user asking for "a second page with a CNN diagram" would hit the validator with the error "Expected closing tag </root> but found </mxCell>" because the validator assumed input was a bare <mxGraphModel> and could not walk past the <mxfile><diagram>...</diagram></mxfile> wrapper. This patch closes the gap end to end: * New helper module `pages.ts` centralises page CRUD (normalize, parse, list, find, add, rename, delete) so every layer agrees that the canonical in-memory shape is always <mxfile>. normalizeToMxfile and addPageToDoc both strip any leading <?xml ?> declaration before embedding a fragment inside <diagram> (the declaration is only valid at document start). addPageToDoc explicitly rejects full <mxfile> inputs so a caller cannot accidentally nest a document inside a page. * `xml-validation.ts` now detects an <mxfile> root and scopes the duplicate-id check per <diagram>. The legacy regex check would otherwise reject every multi-page doc, because cells "0" and "1" repeat in each page's <root> by design. The DOM-parse path is gated by a cheap regex pre-check so legacy bare <mxGraphModel> callers don't pay any extra cost. The autoFix duplicate-id rename step is also guarded against mxfile inputs — renaming those sentinel cells would silently break drawio's parent references. * `diagram-operations.ts` accepts an optional PageSelector. For <mxfile> input it resolves the page first and scopes all querySelectorAll calls to that page's <root>, so a delete on page 2's cell "2" no longer touches page 1's cell "2". * `create_new_diagram` accepts either a bare <mxGraphModel> (legacy, auto-wrapped into a single-page mxfile) or a full <mxfile> with N diagrams. All existing single-page callers keep working unchanged. * `edit_diagram`, `get_diagram`, and `export_diagram` gain optional `page_id` / `page_name` / `page_index` parameters. When omitted they target the first page — the "active by convention" default. Tool handlers with all-optional input schemas coalesce missing arguments via `input ?? {}` so a no-args MCP invocation can't crash on destructure before reaching the session-existence check. * New tools: `list_pages`, `add_page`, `rename_page`, `delete_page`. * Page-targeted PNG/SVG export uses a "load + export + restore" dance: the server projects the target page into a single-page <mxfile>, pushes it into the transient state so the browser reloads the iframe with just that page, waits for drawio to render (~3s), triggers the export, captures the data, and then restores the original multi-page document. The dance is wrapped in `try/finally` so the restore runs unconditionally — even if an exception is thrown mid-dance, the user's multi-tab view is recovered before the function returns. The earlier attempt to use drawio's `selectPage` postMessage was a no-op because drawio's JSON embed protocol does not expose that action — silently exporting whatever tab happened to be active. The load-export-restore approach trades a brief visible tab-flicker for correctness: the exported image is guaranteed to match the requested page. * Tool description strings reflect the multi-page semantics so the LLM client learns the new contract. * Package version bumped 0.2.0 → 0.3.0 (additive surface — four new tools, three extended input schemas, canonical XML shape change). * CI: `.github/workflows/test.yml` gains an explicit install + vitest run for the mcp-server package so the new multi-page invariants are covered by automation, not just local runs. Backward compatibility: every existing single-page caller continues to work without modification. The session.xml shape is normalised on every write, removing the wrapper-injection hack from the .drawio download path. Tests: 43 unit tests under `packages/mcp-server/tests/multi-page.test.ts` pin the validator's mxfile path, the page-scoped operations, the XML declaration-prefix handling for both normalizeToMxfile and addPageToDoc, addPageToDoc's rejection of full <mxfile> inputs, the single-page projection used by export_diagram (a direct regression test for the selectPage bug — two distinct page selectors must produce visually different projections), and the Transformer + CNN motivating scenario. A `tests/smoke.mjs` smoke test drives the built `dist/index.js` over JSON-RPC and asserts all 9 tools register with the right input schemas. Root vitest suite (107 tests) still green. * fix(mcp): rewrite page-targeted export browser-side; harden edit/get The page-targeted PNG/SVG export never worked: export_diagram swapped the live session to a single-page projection, slept 3s, then wrote the export flag onto a state object that setState() had already replaced in the store Map — so the browser never saw the request and every such export timed out. The swap+restore also clobbered concurrent edits. Move the projection entirely browser-side: requestExport() hands a single -page <mxfile> to the bridge via state.exportXml; the bridge loads it, lets draw.io render, exports, then reloads the user's real document. The canonical session state is never mutated, so there is no restore race and no fixed-delay guessing. The export poll now re-reads the live store entry each tick instead of a captured reference. autosave is suppressed and the version-bump reload is skipped while a projection is on screen; if no real document was captured, restore forces a server reload rather than leaving the iframe stuck on the projection. Also: - edit_diagram now returns isError on a page-level failure (selector matched no page / page has no <root>) instead of reporting success-with-warnings and persisting a no-op; the pre-edit history snapshot is taken only after that gate so a failed edit leaves no phantom undo entry. - edit_diagram/get_diagram re-normalise browser-pushed xml to mxfile so a bare <mxGraphModel> can't silently strip a multi-page document. - get_diagram now errors (instead of silently returning the full doc) when a selector is given but the session isn't a parseable mxfile. - page_id / page_name / add_page.id get .min(1) so empty strings can't silently target the first page. - Extract pages.ts:projectPage(), collapsing three copies of the parse→find→serialise projection logic in index.ts. - Replace the never-in-CI tests/smoke.mjs with tests/server-wiring.test.ts, which boots the server from source via tsx and runs under the existing vitest CI step. * chore(mcp): set version to 0.2.1 for release --------- Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2026-06-16 05:45:50 +05:30
addHistory(sessionRef.id, newXml, "")
},
}
}
// Tool: list_pages
server.registerTool(
"list_pages",
{
description:
"List every page (tab) in the current diagram. Returns each page's id, name, 0-based index, and cell count. Use this to discover what pages exist before targeting one with edit_diagram, get_diagram, export_diagram, rename_page, or delete_page.",
inputSchema: {},
},
async () => {
try {
const loaded = await loadMxfileForMutation()
if (!loaded.ok) {
return {
content: [
{ type: "text", text: `Error: ${loaded.message}` },
],
isError: true,
}
}
const pages = listPagesFromDoc(loaded.doc)
if (pages.length === 0) {
return {
content: [
{
type: "text",
text: "No pages in document.",
},
],
}
}
const lines = pages.map(
(p) =>
` [${p.index}] id="${p.id}" name="${p.name}" cells=${p.cellCount}`,
)
return {
content: [
{
type: "text",
text: `Pages (${pages.length}):\n${lines.join("\n")}`,
},
],
}
} catch (error) {
const message =
error instanceof Error ? error.message : String(error)
log.error("list_pages failed:", message)
return {
content: [{ type: "text", text: `Error: ${message}` }],
isError: true,
}
}
},
)
// Tool: add_page
server.registerTool(
"add_page",
{
description:
'Append a new page (tab) to the current diagram WITHOUT touching existing pages or unsaved user changes. Use this when the user wants "another diagram alongside" — e.g. "add a CNN page" — instead of create_new_diagram which wipes everything.\n\n' +
"Inputs:\n" +
"- name: optional display name for the tab (defaults to Page-N where N = existing-page-count + 1)\n" +
"- id: optional explicit page id; if omitted the server generates a short alphanumeric id\n" +
'- xml: optional starting <mxGraphModel> for the new page. If omitted, the page starts blank with the standard root sentinel cells ("0" and "1").\n\n' +
"Returns the new page's id, name, and index so the caller can immediately target it with edit_diagram.",
inputSchema: {
name: z
.string()
.optional()
.describe(
'Optional display name for the new tab (e.g. "CNN"). Defaults to "Page-N".',
),
id: z
.string()
.min(1)
.optional()
.describe(
"Optional explicit page id. If omitted the server generates one. Must be unique across pages.",
),
xml: z
.string()
.optional()
.describe(
'Optional starting <mxGraphModel> XML for the new page. Must include <root> with id="0" and id="1" cells. If omitted the page starts blank.',
),
},
},
async (input) => {
// All three fields optional — coalesce so a no-args call doesn't
// crash on destructure before we surface a proper MCP error.
const { name, id, xml } = input ?? {}
try {
const loaded = await loadMxfileForMutation()
if (!loaded.ok) {
return {
content: [
{ type: "text", text: `Error: ${loaded.message}` },
],
isError: true,
}
}
// If caller provided XML, validate it before splicing it in so we
// never get a half-broken mxfile written to the session.
let cleanXml: string | undefined = xml
if (cleanXml) {
const { valid, error, fixed, fixes } =
validateAndFixXml(cleanXml)
if (fixed) {
cleanXml = fixed
log.info(
`add_page: starting XML auto-fixed: ${fixes.join(", ")}`,
)
}
if (!valid && error) {
return {
content: [
{
type: "text",
text: `Error: starting xml validation failed - ${error}`,
},
],
isError: true,
}
}
}
let info
try {
info = addPageToDoc(loaded.doc, { id, name, xml: cleanXml })
} catch (e) {
const msg = e instanceof Error ? e.message : String(e)
return {
content: [{ type: "text", text: `Error: ${msg}` }],
isError: true,
}
}
loaded.writeBack(loaded.doc)
log.info(
`Added page id=${info.id} name="${info.name}" index=${info.index}`,
)
return {
content: [
{
type: "text",
text: `Page added.\n\nid=${info.id}\nname=${info.name}\nindex=${info.index}\ncells=${info.cellCount}`,
},
],
}
} catch (error) {
const message =
error instanceof Error ? error.message : String(error)
log.error("add_page failed:", message)
return {
content: [{ type: "text", text: `Error: ${message}` }],
isError: true,
}
}
},
)
// Tool: rename_page
server.registerTool(
"rename_page",
{
description:
"Rename an existing page (tab). At least one of page_id / page_name / page_index is required to identify which page to rename. The new_name becomes the visible tab label in the editor.",
inputSchema: {
...pageSelectorSchema,
new_name: z
.string()
.min(1)
.describe("The new display name for the page tab."),
},
},
async ({ new_name, page_id, page_name, page_index }) => {
try {
const loaded = await loadMxfileForMutation()
if (!loaded.ok) {
return {
content: [
{ type: "text", text: `Error: ${loaded.message}` },
],
isError: true,
}
}
const pageSelector = pickPageSelector({
page_id,
page_name,
page_index,
})
if (!hasPageSelector(pageSelector)) {
return {
content: [
{
type: "text",
text: "Error: rename_page requires one of page_id, page_name, or page_index to identify the page.",
},
],
isError: true,
}
}
const ok = renamePageInDoc(loaded.doc, pageSelector, new_name)
if (!ok) {
return {
content: [
{
type: "text",
text: `Error: Page ${describeSelector(pageSelector)} not found.`,
},
],
isError: true,
}
}
loaded.writeBack(loaded.doc)
log.info(
`Renamed page ${describeSelector(pageSelector)} → "${new_name}"`,
)
return {
content: [
{
type: "text",
text: `Page ${describeSelector(pageSelector)} renamed to "${new_name}".`,
},
],
}
} catch (error) {
const message =
error instanceof Error ? error.message : String(error)
log.error("rename_page failed:", message)
return {
content: [{ type: "text", text: `Error: ${message}` }],
isError: true,
}
}
},
)
// Tool: delete_page
server.registerTool(
"delete_page",
{
description:
"Delete a page (tab) from the current diagram. At least one of page_id / page_name / page_index is required. Refuses to delete the last remaining page — the editor needs at least one tab.",
inputSchema: {
...pageSelectorSchema,
},
},
async (input) => {
// All three fields are optional — coalesce so a no-args call returns
// a clean error message instead of crashing on destructure.
const { page_id, page_name, page_index } = input ?? {}
try {
const loaded = await loadMxfileForMutation()
if (!loaded.ok) {
return {
content: [
{ type: "text", text: `Error: ${loaded.message}` },
],
isError: true,
}
}
const pageSelector = pickPageSelector({
page_id,
page_name,
page_index,
})
if (!hasPageSelector(pageSelector)) {
return {
content: [
{
type: "text",
text: "Error: delete_page requires one of page_id, page_name, or page_index to identify the page.",
},
],
isError: true,
}
}
const outcome = deletePageFromDoc(loaded.doc, pageSelector)
if (!outcome.ok) {
return {
content: [
{
type: "text",
text: `Error: ${outcome.reason}.`,
},
],
isError: true,
}
}
loaded.writeBack(loaded.doc)
log.info(
`Deleted page id=${outcome.deletedId} index=${outcome.deletedIndex}`,
)
return {
content: [
{
type: "text",
text: `Page deleted (id=${outcome.deletedId}, was at index ${outcome.deletedIndex}).`,
},
],
}
} catch (error) {
const message =
error instanceof Error ? error.message : String(error)
log.error("delete_page failed:", message)
return {
content: [{ type: "text", text: `Error: ${message}` }],
isError: true,
}
}
},
)
// Graceful shutdown handler
let isShuttingDown = false
function gracefulShutdown(reason: string) {
if (isShuttingDown) return
isShuttingDown = true
log.info(`Shutting down: ${reason}`)
shutdown()
process.exit(0)
}
// Handle stdin close (primary method - works on all platforms including Windows)
process.stdin.on("close", () => gracefulShutdown("stdin closed"))
process.stdin.on("end", () => gracefulShutdown("stdin ended"))
// Handle signals (may not work reliably on Windows)
process.on("SIGINT", () => gracefulShutdown("SIGINT"))
process.on("SIGTERM", () => gracefulShutdown("SIGTERM"))
// Handle broken pipe (writing to closed stdout)
process.stdout.on("error", (err) => {
if (err.code === "EPIPE" || err.code === "ERR_STREAM_DESTROYED") {
gracefulShutdown("stdout error")
}
})
// Start the MCP server
async function main() {
log.info("Starting MCP server for Next AI Draw.io (embedded mode)...")
const transport = new StdioServerTransport()
await server.connect(transport)
log.info("MCP server running on stdio")
}
main().catch((error) => {
log.error("Fatal error:", error)
process.exit(1)
})