Skip to main content
Glama
README.md
# tldraw-page-mcp

> **Unofficial.** Not affiliated with, endorsed by, or officially associated with tldraw, Inc.
> tldraw is a trademark of tldraw, Inc. This project relies on an undocumented global on
> tldraw.com (`window.editor`) and may stop working whenever tldraw.com changes.

An MCP server that lets an agent draw on a real tldraw.com board. It's a fork of the idea behind
tldraw's official [MCP app](https://github.com/tldraw/tldraw/tree/main/apps/mcp-app): the same
`exec` tool, the same focused shape format and helpers, but pointed at the `window.editor` that
tldraw.com exposes on every file page, instead of a canvas widget inside the chat. Edits land on
the shared board, so everyone who has it open sees them.

## How it works

```
MCP client ──stdio──▶ src/server.mjs ──DevTools Protocol──▶ Chrome tab on tldraw.com/f/…
                                                              └─ dist/page-runtime.js
                                                                   wraps window.editor
```

- The server finds or launches a Chrome with remote debugging, using its own profile
  (`~/.tldraw-page-mcp/chrome-profile`), and keeps one tab on the board.
- It injects `dist/page-runtime.js` into that tab: tldraw's focused editor proxy and exec helpers
  (copied from apps/mcp-app), wrapped around the page's own editor.
- `exec` sends the model's code to the tab through Chrome DevTools (`Runtime.evaluate`), the
  same way code runs from the browser console. The official widget loads code as a blob-URL
  module, which tldraw.com's page doesn't allow, so here it arrives through DevTools instead.

## Tools

| Tool | Does |
|---|---|
| `open_board(url)` | Opens a tldraw.com file in the controlled Chrome, or switches to it |
| `exec(code)` | Runs JavaScript with `editor` (focused proxy), `rawEditor` (tldraw.com's editor) and the helpers in scope |
| `get_image(scope?, shapeIds?)` | A PNG of what's in view, the whole page, the selection, or given shapes |

## Setup

```bash
npm install
npm run build
claude mcp add tldraw-page -- node "$PWD/src/server.mjs"
```

The first `open_board` opens a Chrome window with its own profile. Link-shared boards work as a
guest; for a private board, sign in to tldraw.com once in that window (the profile keeps it).
Environment knobs: `TLDRAW_CDP_PORT` (default 9333), `TLDRAW_CHROME` (the Chrome binary),
`TLDRAW_CHROME_PROFILE`, and `TLDRAW_HEADLESS=1` (see below).

Try it without an MCP client:

```bash
npm run smoke -- "https://www.tldraw.com/f/<file-id>" info
```

## Is `window.editor` enough?

Tested on 2026-10-18 against a link-shared board, tldraw.com running a tldraw 5.x editor.

- **Editing:** yes. Every Editor method the official app's tools rely on exists (create, update,
  delete, bounds, select, group, align and distribute, zoom, pages, undo), and the focused proxy
  works unchanged on top of it. A note created with `exec` showed up in a separate browser's
  fresh load, at the same position, and its delete synced too.
- **Images:** yes. `editor.putExternalContent` uploads files to tldraw.com's asset store
  (`tldrawusercontent.com`) like a drag-and-drop does (tested by hand through Chrome; there's no
  upload tool here yet, since the server would have to pass the file bytes into the page), and
  `editor.toImage` exports the PNGs behind `get_image`.
- **Running the model's code:** through DevTools. tldraw.com's page doesn't let scripts evaluate
  code (its content security policy has no `'unsafe-eval'` and no `blob:` scripts), so code
  reaches the editor the way it does from the browser console: through DevTools (this server) or
  an extension using `chrome.debugger`. A page script could instead take a fixed set of JSON
  commands over a local WebSocket.
- **Syncing:** edits sync from both a normal and a headless Chrome. The catch: a write the editor
  rejects (a validation error, such as an unknown `_type`) puts tldraw.com into its "Something went
  wrong" state, and from then on that tab stops syncing while `window.editor` keeps working
  locally, so later calls look fine and are lost. Only a reload clears it. The server guards
  against this (below). Edits also need a moment to go out: `exec` waits 600 ms before returning,
  and a backgrounded tab that's reloaded right after an edit can drop it.
- **Stability:** `window.editor` on tldraw.com isn't a documented API. It's there today (tldraw's
  own debugging hooks sit beside it), but tldraw could rename or remove it without notice.

## Keeping the board in sync

A write tldraw.com's store rejects marks the store as possibly corrupted, and tldraw.com stops
syncing that tab. So `page/guard.ts`:

- refuses an unknown focused `_type` with the list of real ones
- runs tldraw's own record validators on every shape create and update, from the focused
  `editor`, `rawEditor` and the helpers, and refuses a bad one before the store sees it
- checks `editor.store.isPossiblyCorrupted()` and `editor.getCrashingError()` before and after
  each call

If a tab has crashed anyway (code that writes through the page's unguarded `window.editor` can
still do it), the server reloads it from tldraw.com and tells the agent its changes may be lost
and to re-read the board. `open_board` and `get_image` reload a crashed tab too.

## Staying faithful

tldraw's MCP app and tldraw.com both change without notice. [AUDIT.md](AUDIT.md) is the recipe
for checking the fork against both: `npm run faithfulness` for upstream drift, `--board <url>` for
tldraw.com's live contract, and `--agents` for a set of tasks run by fresh agents.

## Differences from the official app

- No canvas widget or checkpoints: the board itself is the state.
- No `search` tool yet. The official one serves an Editor API spec extracted with a TypeScript 7
  build of the tldraw monorepo; here the model can introspect with `exec` instead.
- `page/method-map.json` is generated from the published `@tldraw/editor` types
  (`scripts/gen-method-map.mjs`) with the official rules.
- One fix in the proxy: methods mapped as returning `this` only return the proxy when the real
  method returned the editor. Upstream, `editor.getShapePageBounds('a')` comes back as the editor
  instead of its bounds.
- The page runtime bundles its own copy of tldraw's helpers (about 1.9 MB, injected once per page
  load). It could be trimmed to the few utilities it uses.

## Using it responsibly

- Use it on boards you can already edit. The server drives your own browser, signed in as you or
  as a guest, and its edits sync like yours.
- Your use of tldraw.com is governed by its [Terms of Service](https://www.tldraw.com/tos.html).
- Exporting a board to an AI provider shares its contents. Ask before doing that with a board
  other people work on.

## Licenses and credits

This project's own code is MIT (`LICENSE`).

`page/focused/` and `page/helpers.ts` come from tldraw's
[apps/mcp-app](https://github.com/tldraw/tldraw/tree/main/apps/mcp-app), which is MIT licensed
(`LICENSE.tldraw-mcp-app.md`).

The `tldraw` npm package it builds against is **not** MIT: it's under the
[tldraw license](https://github.com/tldraw/tldraw/blob/main/LICENSE.md). `npm run build` bundles
parts of it into `dist/page-runtime.js` on your machine. That bundle isn't committed or
distributed here, and using it is subject to tldraw's license.

Maintenance

ActivityMaintained
ResponsivenessNo issues