diff --git a/lib/drawio/drawio-config.ts b/lib/drawio/drawio-config.ts index 431e748f..13672add 100644 --- a/lib/drawio/drawio-config.ts +++ b/lib/drawio/drawio-config.ts @@ -47,7 +47,10 @@ export function getDrawioSrc(source: DrawioSource, dark: boolean): string { proto: "json", configure: "1", } + // Without a fixed theme, embedded draw.io shows Extras > Theme only + // with themes=1 (its isThemeMenuVisible) if (source.ui) params.ui = source.ui + else params.themes = "1" Object.assign(params, { spin: "0", libraries: "0", diff --git a/packages/mcp-server/README.md b/packages/mcp-server/README.md index 3f2bafa4..cc38f5d1 100644 --- a/packages/mcp-server/README.md +++ b/packages/mcp-server/README.md @@ -190,6 +190,7 @@ To give the AI your own drawing rules, write them in `~/.next-ai-drawio/instruct | `DRAWIO_DARK` | `auto` | Dark mode of the draw.io editor: `auto` follows the system, `1` forces dark, `0` forces light. The page header keeps following the system. | | `DRAWIO_PREVIEW_UI` | `classic` | The preview page `start_session` opens. `shell` opens the new canvas page built from the web app's canvas: it syncs with the server like the classic page, and an AI change of the page on screen is marked and undone with one Ctrl+Z (in progress: no History panel or download yet). | | `DRAWIO_AUTO_SCREENSHOT` | unset | Set to `true` to attach a screenshot to every `create_new_diagram` and `edit_diagram` result, so the AI checks each drawing. Costs 2 to 10 s per call; the preview tab must be open and in front. A call can still pass `screenshot: false`. | +| `BROWSER` | unset | Set to `none` and `start_session` does not open the preview in the system browser (the convention of Vite and Create React App dev servers); the result still names the URL. | | `DEBUG` | unset | Set to `true` to log debug messages to stderr. | ### Private Deployment (Self-hosted draw.io) diff --git a/packages/mcp-server/src/http-server.ts b/packages/mcp-server/src/http-server.ts index 89f95b54..be25934c 100644 --- a/packages/mcp-server/src/http-server.ts +++ b/packages/mcp-server/src/http-server.ts @@ -122,7 +122,9 @@ export function previewUrl( sessionId: string, ui: PreviewUi = PREVIEW_UI, ): string { - const path = ui === "shell" ? "/shell/" : "" + // The classic page stands in while the shell is not built (a run from + // the sources), as embed.diagrams.net does for a missing dist/drawio + const path = ui === "shell" && shellDir !== null ? "/shell/" : "" return `http://localhost:${port}${path}?mcp=${sessionId}` } @@ -430,6 +432,11 @@ export function startHttpServer(port = 6002): Promise { "No bundled draw.io (dist/drawio missing); the preview loads it from embed.diagrams.net", ) } + if (PREVIEW_UI === "shell" && shellDir === null) { + log.warn( + "The canvas shell is not built (dist/shell missing); start_session opens the classic page", + ) + } resolve(port) }) }) diff --git a/packages/mcp-server/src/index.ts b/packages/mcp-server/src/index.ts index 8eb3142f..525fd526 100644 --- a/packages/mcp-server/src/index.ts +++ b/packages/mcp-server/src/index.ts @@ -382,9 +382,10 @@ registerWriteTool( lastSeenXml: "", } - // Open browser + // Open the browser. BROWSER=none (the convention of CRA and Vite + // dev servers) skips it: the e2e tests and headless hosts set it const browserUrl = previewUrl(port, sessionId) - await open(browserUrl) + if (process.env.BROWSER !== "none") await open(browserUrl) // A saved diagram comes back from its file. lastSeenXml stays // empty, so edit_diagram asks for get_diagram first. @@ -1287,7 +1288,7 @@ function previewStalled(sessionId: string): boolean { * already succeeded must not run again, so it points to screenshot_diagram. */ function previewStalledNote(sessionId: string, next = "then retry") { - return `The preview tab is not responding (browsers pause background tabs). Ask the user to bring the preview tab to the front (http://localhost:${getServerPort()}?mcp=${sessionId}), ${next}.` + return `The preview tab is not responding (browsers pause background tabs). Ask the user to bring the preview tab to the front (${previewUrl(getServerPort(), sessionId)}), ${next}.` } function previewStalledError(sessionId: string) { diff --git a/packages/mcp-server/tests/http-server.test.ts b/packages/mcp-server/tests/http-server.test.ts index 7caca9a1..ede85c56 100644 --- a/packages/mcp-server/tests/http-server.test.ts +++ b/packages/mcp-server/tests/http-server.test.ts @@ -870,6 +870,15 @@ describe("canvas shell page", () => { expect(previewUrl(6002, "mcp-x", "classic")).toBe( "http://localhost:6002?mcp=mcp-x", ) + // Not built: the classic page, which works, rather than a 404 + setShellDir(null) + try { + expect(previewUrl(6002, "mcp-x", "shell")).toBe( + "http://localhost:6002?mcp=mcp-x", + ) + } finally { + setShellDir(shellDir) + } }) }) diff --git a/tests/unit/mcp-shell-config.test.ts b/tests/unit/mcp-shell-config.test.ts index ac5ed417..15bf1768 100644 --- a/tests/unit/mcp-shell-config.test.ts +++ b/tests/unit/mcp-shell-config.test.ts @@ -5,6 +5,7 @@ import { readFileSync } from "node:fs" import { join } from "node:path" import { describe, expect, it } from "vitest" +import { getDrawioSrc } from "@/lib/drawio/drawio-config" import { i18n } from "@/lib/i18n/config" import { getShellDictionary, @@ -59,6 +60,25 @@ describe("shell runtime config", () => { }) }) +describe("shell draw.io URL", () => { + it("offers draw.io's theme menu unless a theme is fixed", () => { + // Embedded draw.io shows Extras > Theme only with themes=1 and no ui + const free = new URL( + getDrawioSrc({ editorUrl: "/drawio/index.html" }, false), + ) + expect(free.searchParams.get("themes")).toBe("1") + expect(free.searchParams.get("ui")).toBeNull() + const fixed = new URL( + getDrawioSrc( + { editorUrl: "/drawio/index.html", ui: "kennedy" }, + false, + ), + ) + expect(fixed.searchParams.get("ui")).toBe("kennedy") + expect(fixed.searchParams.get("themes")).toBeNull() + }) +}) + describe("shell language", () => { it("maps language tags to the dictionaries we have", () => { expect(matchLocale("en-US")).toBe("en")