CNVS
Free, no-signup real-time collaborative whiteboard (draw, todo, kanban) in the browser.
What it can do
- Open Board: ALWAYS call this first when given a board URL or ID. Resolves the canonical board id and auto-creates the board row if it does not exist yet. Returns a summary (item counts, authors). Afte
- Get Board: Full structured JSON state of a board: texts (id, x, y, content, color, width, postit, author), strokes (id, points, color, author), images (id, x, y, width, height, dataUrl, thumbDataUrl,
- Get Preview: Compact schematic SVG render of the board (typically a few kB even for dense boards). Returns both an image/svg+xml content block (you can SEE it) and the raw SVG text. CALL THIS any time
What data it sees
Do you need an account
No: the server works without sign-in
Free, no-signup real-time collaborative whiteboard (draw, todo, kanban) in the browser. Open a URL, share it, draw and write together. Ships a Model Context Protocol (MCP) endpoint at https://cnvs.app/mcp so AI assistants can collaborate on the same live board as humans.
How to connect
How to connect CNVS to Claude
The server address to paste into Claude settings:
https://whiteboard.run.tools
- Open Claude (claude.ai or the desktop app).
- Go to Settings → Connectors.
- Click Add custom connector.
- Paste the server address copied below into Remote MCP server URL and click Add.
- In a chat, click + → Connectors and switch the new connector on.
Custom connectors are available on Free, Pro, Max, Team and Enterprise plans (Free is limited to one). On Team and Enterprise an organization Owner adds the connector first under Organization settings → Connectors.
Authorization
After you click Add, a sign-in window for the service opens. Sign in with your own account and approve access. Claude never sees your password.
How to connect CNVS to ChatGPT
The server address to paste into ChatGPT settings:
https://whiteboard.run.tools
- Open ChatGPT in a browser (chatgpt.com). A Plus, Pro, Business, Enterprise or Edu plan is required.
- Turn on developer mode once: Settings → Apps → Advanced settings → Developer mode.
- Open Settings → Connectors and click Create.
- Fill in the form: Name (anything), Description (one line about what the service does), MCP server URL (copy it below).
- Under Authentication choose OAuth if the service requires sign-in, otherwise None. Click Create.
- In a new chat open + → Apps/Connectors and enable the connector.
OpenAI has renamed this section before (Connectors → Apps/Plugins). If the label differs, search settings for "developer mode". On Business/Enterprise workspaces an admin must allow custom connectors first.
Authorization
On first use ChatGPT opens the service's sign-in window. Sign in and approve access.
How to connect CNVS to Cursor
The server address to paste into Cursor settings:
https://whiteboard.run.tools
Fastest: click Open in Cursor below and confirm the prompt.
Manually:
- In Cursor open Settings → Cursor Settings → MCP and click Add new global MCP server.
- Paste the JSON copied below into
~/.cursor/mcp.json(per project:.cursor/mcp.jsonin the repo root). - Save the file. The server appears in the MCP list; authorize it there if asked.
Authorization
If the service needs sign-in, an authorize button appears next to the server in the MCP list.
Server tool list (24)
Raw names from tools/list. Only developers need these.
| open_board | ALWAYS call this first when given a board URL or ID. Resolves the canonical board id and auto-creates the board row if it does not exist yet. Returns a summary (item counts, authors). After this, call BOTH `get_preview` and `get_board` before editing so you can see the layout visually AND know the exact ids/coordinates — do not skip `get_preview`, otherwise you will place new items blindly on top of existing ones. |
| get_board | Full structured JSON state of a board: texts (id, x, y, content, color, width, postit, author), strokes (id, points, color, author), images (id, x, y, width, height, dataUrl, thumbDataUrl, author; heavy base64 >8 kB elided to dataUrl:null, tiny images inlined). Use this for EXACT ids/coordinates/content (needed for `move`, `erase`, editing a text by id). For visual layout (where is empty space? what overlaps?) call `get_preview` instead — it's much cheaper for spatial reasoning than a huge JSON dump. |
| get_preview | Compact schematic SVG render of the board (typically a few kB even for dense boards). Returns both an image/svg+xml content block (you can SEE it) and the raw SVG text. CALL THIS any time you need to understand where things are — before placing new items, before deciding whether the canvas is crowded, before picking a free region. AI-authored items get a purple border so you can tell which contributions were yours. For precise text content prefer `get_board`. |
| add_text | Create a NEW text node, or update an existing one (pass the same `id` to overwrite content/position in place — preferred over creating a duplicate). Supports cnvs markup (Markdown-ish) and Mermaid diagrams in the content. When using Mermaid, the ENTIRE content of this text node must be a single Mermaid diagram (one ```mermaid fenced block and nothing else — no heading, no prose before or after). If you need prose + a diagram, create two separate text nodes. `postit: true` renders as a yellow sticky; `diagram: true` renders as a framed box (2px border in the text colour, centred text) — the two are mutually exclusive. Coordinates are in board-world pixels, +x right, +y DOWN; pick a spot that does not overlap existing items (check `get_preview` first). Default width auto-fits content up to ~320 px; pass `width` for explicit wrapping (160–4096). Keep content under 100 000 chars. |
| add_link | Drop a URL capsule onto the board — rendered as a clickable pill showing the hostname. Use this instead of `add_text` when the node is just a link; the capsule styling signals clickability to humans. Same coordinate rules as `add_text` (+x right, +y down). |
| add_image | Place a raster or SVG image on the board at (x, y) with explicit width/height in board pixels. `data_url` MUST be a `data:image/(png|jpeg|gif|webp|svg+xml);base64,...` string ≤ ~900 kB; hosted URLs are not accepted. Strongly recommended: also pass a tiny `thumb_data_url` (≤8 kB JPEG/PNG/WebP, ~64 px on the long edge) — it is embedded into the SVG preview so OTHER AI viewers (and you, on later `get_preview` calls) can actually see the image instead of a placeholder box. |
| draw_stroke | Draw a freehand stroke on the board. Use for arrows, underlines, connector lines, annotations, or simple shapes — a straight line needs two points, a rough circle wants ~20. Stroke width is fixed at 3 px; `color` accepts any CSS color (e.g. '#ff0000', 'var(--text-color)'). Accepts three equivalent point formats — pick whichever your MCP client serialises cleanly: nested `[[x,y],[x,y],...]`, flat `[x1,y1,x2,y2,...]`, or a JSON string of either. Some clients (Claude Code as of 2026-04) drop nested arrays during tool-call serialisation, so prefer the flat form or the JSON-string form when in doubt. To delete a stroke later, use `erase` with `kind: 'line'` and the id returned here. |
| move | Reposition an existing item to a new (x, y) without retyping its content. Works for every item kind: `text` and `link` set the top-left to (x, y); `line` translates every point so the stroke's bounding box top-left lands at (x, y); `image` sets the top-left like text. `kind` defaults to `text` for backward compat with older callers. Find the id + kind via `get_board`. Prefer `move` over re-creating an item when only the location changes — it preserves the id, content, author and avoids a round-trip of base64 bytes for images. |
| erase | Delete a single item by id. `kind` MUST match the item type: 'text' for text nodes, 'line' for freehand strokes, 'image' for images — the wrong kind silently targets the wrong table and is a common mistake. Get the id + type from `get_board` (texts[], lines[], images[]). There is no bulk/erase-all tool: loop if you need to delete multiple items. |
| wait_for_update | Long-poll: blocks until the next edit lands on this board, then returns. WHEN TO CALL THIS: if your MCP client does NOT surface `notifications/resources/updated` events from `resources/subscribe` back to the model (most chat clients do not — they receive the SSE event but don't inject it into your context), this tool is how you 'wait for the human' inside a single turn. Typical flow: you draw / write what you were asked to, then instead of ending your turn you call `wait_for_update(board_id)`. When the human adds, moves, or erases something, the call returns and you refresh with `get_preview` / `get_board` and continue the collaboration. Great for turn-based interactions (games like tic-tac-toe, brainstorming where you respond to each sticky the user drops, sketch-and-feedback loops, etc.). If your client DOES deliver resource notifications natively, prefer `resources/subscribe` — it's cheaper and has no timeout ceiling. BEHAVIOUR: resolves ~3 s after the edit burst settles (same debounce as the push notifications — this is intentional so drags and long strokes collapse into one wake-up). Returns `{ updated: true, timedOut: false }` on a real edit, or `{ updated: false, timedOut: true }` if nothing happened within `timeout_ms`. On timeout, just call it again to keep waiting; chaining calls is cheap. `timeout_ms` is clamped to [1000, 55000]; default 25000 (leaves headroom under typical 60 s proxy timeouts). |
| set_board_mode | Choose whether this board is a freeform whiteboard ('draw', the default) or a kanban task board ('todo'). Mode is switchable WHENEVER the board is empty of real content: drawings (text/strokes/images) and tasks. Empty or seeded columns DON'T count (switching to 'draw' clears them), so a cleared board can be switched again, and you can flip draw<->todo freely until the first stroke/text/image or task lands. Setting 'todo' auto-seeds three starter columns (To do / In progress / Done). Returns `{ mode, columns }`. Use the task/column tools (`create_task`, `create_column`, …) once the board is in 'todo' mode. |
| create_column | Add a kanban column (swimlane) to a 'todo' board. Generates and returns a stable column id. `sort` is a float ordering key (ascending); omit to default to 0. `lane` is an integer row index for multi-row layouts (defaults to 0). `color` is an optional title color ('red' / 'blue' / 'green', or 'auto' / omit for default). To rename, reorder, recolor or move between lanes later use `update_column`. |
| update_column | Rename, reorder, recolor or move an existing kanban column. Requires the column `id` (from `list_tasks`). Pass `title`, `sort`, `lane` and/or `color` to change them. |
| delete_column | Delete a kanban column by id. Tasks in the column are removed with it. Get the id from `list_tasks`. |
| create_task | Create a task (card) in a column on a 'todo' board. Generates and returns a stable task id. `column_id` must reference an existing column (see `list_tasks`). `description` is free-form prose stored on the card; `priority` is one of 'H' | 'M' | 'L' (omit for none); `due_date` is an ISO 8601 date string; `assignee` is free text (name / initials / email). `sort` is a float ordering key within the column (ascending), defaulting to 0. |
| update_task | Update an existing task by `id` (from `list_tasks`). Pass only the fields you want to change: `name`, `description`, `due_date`, `priority` ('H'|'M'|'L'), `assignee`, `done` (true once completed), `column_id` (to reassign), `sort`. To move a card between columns while setting its order, `move_task` is the dedicated tool. |
| move_task | Move a task to a column at a given sort position — the kanban drag-and-drop primitive. `column_id` is the destination column and `sort` its float order key (ascending) within that column. |
| delete_task | Delete a task (card) by id. Get the id from `list_tasks`. |
| list_tasks | Read the full kanban state of a 'todo' board: `{ mode, columns, tasks }`, both ordered by their `sort` key (ascending). Each task carries its hot fields (name, due_date, priority, assignee, done, column_id, sort) plus an opaque `content` JSON string (holds the description). Use this to get exact column/task ids before updating, moving or deleting. |
| set_lane | Set (or clear) the title of a kanban row (lane). Lanes group columns into horizontal swimlanes; a lane is identified by its integer `lane` index (the same index columns carry). Pass a `title` to name the row, or an empty string to clear it. Read current lane titles + the shared column width via `list_tasks`. |
| set_column_width | Set the shared kanban column width in pixels for a 'todo' board (all columns share one width). Clamped to [200, 480]. Read the current value as `colWidth` from `list_tasks`. |
| create_tasks | Bulk-create multiple task cards in one call — far cheaper than calling `create_task` N times. Pass `tasks`: an array of task objects, each with `column_id` (required) + the same fields `create_task` accepts (`name`, `description`, `due_date`, `priority`, `assignee`, `done`, `sort`). Returns the array of created ids in order. Stops at the first invalid item and reports its index; items before it are still created (not transactional). |
| query_tasks | Read tasks from a 'todo' board with server-side filtering — handy for 'what's overdue?' / 'what's assigned to X?' without pulling the whole board. All filters are optional and AND together: `assignee` (exact match), `priority` ('H'|'M'|'L'), `done` (boolean), `overdue` (true → due_date strictly before today, not done), `due_before` / `due_after` (ISO date window on due_date). Returns `{ boardId, mode, tasks }` — tasks ordered by sort, each with the same fields as `list_tasks`. |
| export_tasks | Export a 'todo' board's columns + tasks as a single text document. `format` is 'markdown' (a checklist grouped by column, the default) or 'csv' (one row per task with column/name/priority/assignee/due_date/done). Returns the rendered text. Same data as the GET /api/boards/<id>/tasks.md and tasks.csv REST endpoints. |