Whiteboard Canvas#

The Chat page shows an Excalidraw whiteboard beside the conversation. It works in both directions: you draw and send the result into the chat, and the assistant draws onto the same board.

Setup#

The canvas needs its MCP server running.

make docker-rebuild-canvas     # under compose (recommended)
make run-canvas-mcp            # or standalone, port 8766

Then enable it. SKIP_MCP=true disables the canvas as well as the printer, so both have to be off:

SKIP_MCP=false SKIP_CANVAS=false make run-ui

Compose already sets both, so make docker-up needs nothing further.

Note

If the canvas server is not reachable, the assistant is not offered the whiteboard at all — the agent is dropped rather than advertised, so it will never claim to have drawn something it could not. Chat is unaffected.

That covers both ways it can be off: disabled by SKIP_CANVAS/SKIP_MCP, and enabled but unreachable when the app started. The second case loads no tools rather than failing outright, so it is checked by asking whether the agent has any tools, not merely whether it exists.

Using it#

Send a drawing to the chat. Draw, optionally add a message, and press 📤 Send to Chat (or Ctrl/Cmd + Enter). The drawing enters the conversation as an image, and its elements are sent to the canvas server so the assistant can read them as data rather than only look at a picture.

Ask for a drawing. “Draw a simply supported beam with two supports and a downward load.” The assistant draws on the board and describes what it drew.

Iterate. “Add a label to the beam”, “remove the arrow”, “what’s on the whiteboard?” — it reads the current scene before editing, so it works with what is actually there, including your own strokes.

Important

Press Send to Chat before asking the assistant to modify something you drew. The UI only learns what is on the canvas when you export it, so an unsent drawing is replaced by whatever the assistant draws next. Sending first makes it both visible to the assistant and safe.

What persists, and where#

Two copies of every scene, for different reasons:

Where

Lifetime

Canvas MCP server

Survives a container restart with CANVAS_STATE_DIR set; dropped on eviction past CANVAS_MAX_SCENES (default 64 conversations)

EngiAI database

Durable, per conversation, deleted with the conversation

The database copy is the one that matters. It is restored when you reopen a conversation, so a drawing outlives both eviction and a server that was running without a state directory.

Each conversation has its own whiteboard. Switching chats switches boards.

Configuration#

Variable

Default

Purpose

SKIP_CANVAS

false in code, True in .env.example

Disable the whiteboard agent

CANVAS_MCP_URL

http://localhost:8766

Where the agent reaches the server

CANVAS_MCP_HOST / CANVAS_MCP_PORT

0.0.0.0 / 8766

Bind address and port

CANVAS_MCP_ALLOWED_HOSTS

see below

Host-header allowlist for the SSE transport

CANVAS_STATE_DIR

unset locally, /app/data/canvas in Docker

Where the server persists scenes

CANVAS_MAX_SCENES

64

LRU bound on concurrent canvases

CANVAS_MAX_ELEMENTS

2000

Per-canvas element cap

CANVAS_MAX_BYTES

2000000

Per-call payload cap

.env.example ships SKIP_CANVAS=True while the code default is enabled. Not a contradiction: compose sets it explicitly and runs the server, whereas a fresh local checkout has neither.

CANVAS_MCP_ALLOWED_HOSTS replaces the default list rather than extending it — localhost, 127.0.0.1 and excalidraw-mcp-server, each in bare and :PORT form. The SDK matches the Host header exactly, so omitting the port-qualified form answers 421. The un-namespaced MCP_ALLOWED_HOSTS is deliberately not read here; see Prusa transport security for why that name is shared.

How it fits together#

Excalidraw component  <--elements/scene_version--  Streamlit UI
                                                        |
                                              pull / push (MCP over SSE)
                                                        v
ExcalidrawAgent  --draw_elements, get_canvas-->  Canvas MCP server
                                                        |
                                                   scene per canvas_id

canvas_id is the conversation’s thread_id, injected by the supervisor rather than chosen by the model — so the assistant cannot draw into another conversation’s whiteboard, nor claim you synced a drawing you did not. See services/excalidraw_mcp_server/README.md for the server’s own documentation and its security posture.

Troubleshooting#

The assistant says it cannot draw. The canvas server is unreachable. Check docker compose ps excalidraw-mcp-server and that SKIP_MCP and SKIP_CANVAS are both false.

A drawing did not appear. The whiteboard is written only when the scene version changes, which happens after a turn that called a drawing tool. If the assistant answered in prose without calling one, there is nothing to show — ask it to draw explicitly.

My drawing vanished when the assistant drew. It was not sent first; see the note above.

Nothing renders but chat works. That is the intended failure mode — every canvas call is fail-soft, so a server that is down costs a drawing, never an answer. ENGIAI_LOG_LEVEL=DEBUG shows the reason.