diff --git a/.github/workflows/publish-mcp.yml b/.github/workflows/publish-mcp.yml
index 6b816c45..d3061404 100644
--- a/.github/workflows/publish-mcp.yml
+++ b/.github/workflows/publish-mcp.yml
@@ -1,7 +1,8 @@
name: Publish MCP Server
# Publishes @next-ai-drawio/mcp-server to npm via OIDC trusted publishing
-# (no token, no OTP). Triggers when packages/mcp-server changes on main;
+# (no token, no OTP). Triggers when packages/mcp-server or the web app code
+# bundled into the canvas shell (scripts/build-shell.mjs) changes on main;
# skips silently if the package.json version is already on npm — so a
# release is just "bump the version in a PR and merge".
on:
@@ -10,6 +11,16 @@ on:
- main
paths:
- "packages/mcp-server/**"
+ # The canvas shell is bundled from these with the root's lockfile
+ - "components/**"
+ - "contexts/**"
+ - "hooks/**"
+ - "lib/**"
+ - "stores/**"
+ - "app/globals.css"
+ - "package-lock.json"
+ # The package's build scripts import the root's scripts/drawio-zip.mjs
+ - "scripts/**"
workflow_dispatch:
permissions:
@@ -35,7 +46,9 @@ jobs:
with:
node-version: 24
cache: "npm"
- cache-dependency-path: packages/mcp-server/package-lock.json
+ cache-dependency-path: |
+ package-lock.json
+ packages/mcp-server/package-lock.json
registry-url: "https://registry.npmjs.org"
# Trusted publishing requires npm >= 11.5.1
@@ -54,6 +67,13 @@ jobs:
echo "publish=true" >> "$GITHUB_OUTPUT"
fi
+ # The canvas shell (scripts/build-shell.mjs) is bundled from the web
+ # app's components with the root's esbuild, Tailwind and React
+ - name: Install repository dependencies
+ if: steps.version.outputs.publish == 'true'
+ working-directory: .
+ run: npm ci
+
- name: Install dependencies
if: steps.version.outputs.publish == 'true'
run: npm ci
@@ -62,6 +82,9 @@ jobs:
if: steps.version.outputs.publish == 'true'
run: npm test
+ # The build downloads the pinned draw.war from GitHub releases
+ # (scripts/fetch-drawio.mjs) and bundles a trimmed draw.io into
+ # dist/drawio; check-package confirms it is in the tarball
- name: Build and check package contents
if: steps.version.outputs.publish == 'true'
run: npm run build && npm run check-package
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 6ec47895..c90a969e 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -38,20 +38,104 @@ jobs:
- name: Run unit tests
run: npm run test -- --run
- # The MCP server package ships its own vitest because its DOM polyfill
- # (linkedom) needs `environment: node`, while the root vitest uses jsdom
- # for the Next.js app. Install + run its tests separately so CI catches
- # multi-page mxfile regressions.
+ # The MCP server package: its own vitest (its DOM polyfill, linkedom, needs
+ # `environment: node`, while the root vitest uses jsdom for the Next.js
+ # app), the npm package as it is built, the draw.io file list, and the
+ # canvas shell in a browser.
+ mcp-shell:
+ name: MCP Server & Shell
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v6
+ with:
+ # HEAD^1 is the base branch for the version bump reminder below
+ fetch-depth: 2
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v6
+ with:
+ node-version: "20"
+ cache: "npm"
+ cache-dependency-path: |
+ package-lock.json
+ packages/mcp-server/package-lock.json
+
+ # The shell is bundled from the web app's components with the root's
+ # esbuild, Tailwind and React; Playwright comes from the root too
+ - name: Install dependencies
+ run: npm ci
+
- name: Install MCP server dependencies
run: npm --prefix packages/mcp-server ci
+ - name: Cache Playwright browsers
+ uses: actions/cache@v5
+ id: playwright-cache
+ with:
+ path: ~/.cache/ms-playwright
+ key: playwright-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
+
+ - name: Install Playwright browsers
+ if: steps.playwright-cache.outputs.cache-hit != 'true'
+ run: npx playwright install chromium --with-deps
+
+ - name: Install Playwright deps (cached)
+ if: steps.playwright-cache.outputs.cache-hit == 'true'
+ run: npx playwright install-deps chromium
+
- name: Run MCP server unit tests
run: npm --prefix packages/mcp-server test
- # Tests run from src/, so check the built npm package separately
+ # Tests run from src/, so check the built npm package separately. The
+ # build downloads the pinned draw.war and bundles a trimmed draw.io.
- name: Build MCP server and check package contents
run: npm --prefix packages/mcp-server run build && npm --prefix packages/mcp-server run check-package
+ # The trimmed draw.io is cut from this list; the guard drives the full
+ # copy (public/drawio) in Chromium and fails when the editor requests a
+ # file the list does not have
+ - name: Download the full draw.io
+ run: node scripts/fetch-drawio.mjs
+
+ - name: Check the MCP draw.io file list
+ run: npm --prefix packages/mcp-server run check-drawio
+
+ # The canvas shell in a browser, from the package as npm installs it:
+ # the packed tarball goes into an empty directory, its dist/index.js is
+ # started over stdio and headless Chromium opens the URL start_session
+ # returns (packages/mcp-server/tests/e2e)
+ - name: Run the MCP shell E2E tests on the packed tarball
+ run: |
+ mkdir -p "$RUNNER_TEMP/mcp-pack" "$RUNNER_TEMP/mcp-install"
+ npm pack ./packages/mcp-server --pack-destination "$RUNNER_TEMP/mcp-pack"
+ npm --prefix "$RUNNER_TEMP/mcp-install" install "$RUNNER_TEMP"/mcp-pack/*.tgz
+ MCP_SERVER_ENTRY="$RUNNER_TEMP/mcp-install/node_modules/@next-ai-drawio/mcp-server/dist/index.js" \
+ npm --prefix packages/mcp-server run test:e2e
+ env:
+ CI: true
+
+ # The shell bundles the web app's canvas and the root's dependencies,
+ # so a change under these paths (the ones publish-mcp.yml triggers on)
+ # reaches users with the next MCP release, and publish-mcp.yml only
+ # publishes a new version number
+ - name: Remind to bump the MCP version when bundled code changed
+ if: github.event_name == 'pull_request'
+ run: |
+ changed=$(git diff --name-only HEAD^1 HEAD -- components contexts hooks lib stores app/globals.css package-lock.json scripts packages/mcp-server)
+ base_version=$(git show HEAD^1:packages/mcp-server/package.json | node -p "JSON.parse(require('fs').readFileSync(0, 'utf8')).version")
+ head_version=$(node -p "require('./packages/mcp-server/package.json').version")
+ if [ -n "$changed" ] && [ "$base_version" = "$head_version" ]; then
+ echo "::warning::Code bundled into the MCP server changed without a version bump in packages/mcp-server/package.json (still ${head_version}). Bump it if the change should be published."
+ fi
+
+ - name: Upload test results
+ uses: actions/upload-artifact@v6
+ if: always()
+ with:
+ name: playwright-report-mcp-shell
+ path: packages/mcp-server/test-results/
+ retention-days: 7
+
e2e:
# Six jobs at once, each with a sixth of the tests and one worker: the
# same conditions per test, a fraction of the time
diff --git a/.gitignore b/.gitignore
index 0e8010a0..6893a058 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,6 +16,8 @@ packages/*/dist
/coverage
/playwright-report/
/test-results/
+packages/*/playwright-report/
+packages/*/test-results/
# next.js
/.next/
diff --git a/README.md b/README.md
index bc9731ea..ba378c38 100644
--- a/README.md
+++ b/README.md
@@ -114,6 +114,8 @@ claude mcp add drawio -- npx @next-ai-drawio/mcp-server@latest
Then ask the AI for "a flowchart of user authentication with login, MFA and session management" and the diagram appears in your browser as it is drawn. The MCP server has most of the web app's drawing features:
- The same drawing rules and icon libraries (AWS, Azure, GCP, Kubernetes and more)
+- The preview is the same canvas as the web app: the AI's changes are outlined, one Ctrl+Z takes them back, and the AI can read the shapes you select
+- The draw.io editor ships inside the package, so after install the preview works offline (`DRAWIO_BASE_URL` points it at an external draw.io instead)
- A screenshot tool, so the AI can look at the rendered diagram and fix it
- Version history, multi-page diagrams, and download as `.drawio`, `.png`, `.svg` or `.drawio.svg`
- Auto-save to `~/.next-ai-drawio/`, so a diagram survives a restart
diff --git a/app/[lang]/layout.tsx b/app/[lang]/layout.tsx
index 05470c14..ede4a684 100644
--- a/app/[lang]/layout.tsx
+++ b/app/[lang]/layout.tsx
@@ -3,6 +3,7 @@ import type { Metadata, Viewport } from "next"
import { Instrument_Sans, JetBrains_Mono } from "next/font/google"
import { notFound } from "next/navigation"
import Script from "next/script"
+import { LocaleProvider } from "@/components/canvas/locale-context"
import { AppToaster } from "@/components/workspace/app-toaster"
import { DiagramProvider } from "@/contexts/diagram-context"
import { DictionaryProvider } from "@/hooks/use-dictionary"
@@ -181,7 +182,9 @@ export default async function RootLayout({
className={`${instrumentSans.variable} ${jetbrainsMono.variable} antialiased`}
>
- {children}
+
+ {children}
+