Content
# holst-mcp
MCP server for working with Holst.so boards (Russian alternative to Miro/FigJam) via Claude Code or any other MCP client.
Holst does not have a public API, so integration works around - through **locally launched desktop client Holst.app** with enabled Chrome DevTools Protocol. The server connects to the local port, reads and writes to the Yjs document of the board through its live user session.
## What it can do
### Reading
| Tool | Description |
|---|---|
| `holst_whoami` | Check connection and authorization. Returns the logged-in user. |
| `holst_list_boards` | List of all boards and folders in all workspaces. |
| `holst_get_board_content` | Full raw Yjs dump of the board: all objects + all documents. |
| `holst_get_board_summary` | Structured markdown: stickers grouped by the nearest shape header. |
| `holst_search_text` | Text search across all objects of the board. |
### Creating objects
| Tool | Type | Description |
|---|---|---|
| `holst_add_sticker` | sticker | Yellow sticker 192×192 |
| `holst_add_shape` | shape | Figure with text — square, ellipse, roundedRectangle, basic-star, etc. |
| `holst_add_simple_text` | simple-text | Simple text label without background |
| `holst_add_arrow` | arrow | Arrow between points or objects |
| `holst_add_frame` | frame | Large container with signature (placed under content) |
| `holst_add_stamp` | stamp | Emoji / stamp 60×60 |
### Editing
| Tool | Description |
|---|---|
| `holst_update_text` | Rewrite text in sticker / shape / simple-text |
| `holst_move_object` | Move object |
| `holst_resize_object` | Change size (for shape, sticker) |
| `holst_set_color` | Set fillColor / strokeColor |
| `holst_delete_object` | Delete object |
All changes are automatically synced on other connected clients (Yjs CRDT).
### Not supported in v0.2.0
- **drawing (P)** — hand drawing. Contains complex path in the form of a string of coordinates (`"0,0,8.2,-11.7,..."`), Bezier curves, pivot. Requires writing a path generator. Can be added in v0.3 if needed.
- **task-card / kanban / voting / reaction-stamp** — niche types, not required.
- **Attaching objects to parentId** (embedding in frame) — added by one `move_object` after, but no automatic embedding.
## Requirements
- macOS / Linux / Windows (tested on macOS)
- Node.js ≥ 20
- Holst.app installed and **launched with the flag `--remote-debugging-port=9222`**
- User logged in to Holst desktop
## Installation
```bash
cd ~/Developer/holst-mcp
npm install
chmod +x bin/start-holst.sh
```
Register in `~/.claude/settings.json`:
```json
{
"mcpServers": {
"holst": {
"command": "node",
"args": ["/Users/andrej/Developer/holst-mcp/src/server.js"]
}
}
}
```
## Launching Holst in CDP mode
Before using MCP, **mandatory** to launch Holst with debug port:
```bash
~/Developer/holst-mcp/bin/start-holst.sh
```
The script:
1. Removes quarantine from the application (otherwise, macOS blocks the CDP port)
2. If Holst is already launched with CDP — does nothing
3. If launched without CDP — restarts
4. If not launched — launches again
5. The session of the logged-in user is saved (user-data-dir is not touched)
After the first launch, you need to log in once through Google/Yandex/email — then the session lives in `~/Library/Application Support/Holst/` and survives restarts.
## How it works technically
### Architecture
```
Claude Code
│ (stdio)
▼
holst-mcp server (Node.js)
│ (CDP WebSocket)
▼
Holst.app:9222 (Electron, local)
│ (HTTPS + WSS, authorized session)
▼
api.holst.so / hud.holst.so
```
MCP server is just an intermediary between Claude and the running Holst browser. All requests go through **the session of a real user**: cookies, tokens, rights — everything as in ordinary work with the application.
### Reading the board
1. Connect to `127.0.0.1:9222`, find the tab with `app.holst.so/board/{id}`. If there is no such tab — take any Holst tab and navigate it to the desired board.
2. Through CDP `Runtime.evaluate` execute in the page context:
```js
window.ds.yPersistence.yDoc.toJSON()
```
3. Get JSON with all content:
- `documents`: `{ docId: "text" }` — text content
- `objects`: `{ oid: { type, position, documentId, ... } }` — visual objects
- `task-card-statusesStore`, `kanban-*Store`, `reactionsStore` — other subsystems
### Writing to the board
Holst uses **Yjs CRDT** (`window.Yjs` exported globally). Data model:
- `yDoc.getMap('objects')` — Y.Map, key = object UUID, value = Y.Map with fields (type, position, documentId, width, height, ...).
- `yDoc.getMap('documents')` — Y.Map, key = documentId, value = Y.Text.
To add a sticker:
```js
const yObjects = yDoc.getMap('objects');
const yDocs = yDoc.getMap('documents');
const stickerMap = new Yjs.Map();
stickerMap.set('type', 'sticker');
stickerMap.set('documentId', newDocId);
stickerMap.set('position', { x, y });
// ... other fields
const yText = new Yjs.Text();
yText.insert(0, 'sticker text');
yDoc.transact(() => {
yDocs.set(newDocId, yText);
yObjects.set(newId, stickerMap);
});
```
Yjs automatically:
- Syncs changes via WebSocket provider to Holst server
- Delivers changes to other connected clients
- Resolves conflicts during concurrent editing
### Authorization
Holst uses Bearer token in `Authorization` header (NOT in cookies). Token is stored in `localStorage['social-auth-store'].token`. MCP does not know the token itself — it is executed in the context of the page where local storage is already available.
## Tools — details
### `holst_get_board_content`
```json
{ "boardId": "372ea1bf-8bac-481b-94f2-ddd9088461ca" }
```
Returns:
```json
{
"meta": { "id": "...", "name": "Nastya", "workspace_id": "...", ... },
"content": {
"documents": { "bb3c0ac3-...": "VALUES", ... },
"objects": { "04a54183-...": { "type": "shape", "position": {...}, ... } },
...
}
}
```
### `holst_add_sticker`
```json
{
"boardId": "...",
"text": "new sticker",
"x": 100,
"y": 200,
"width": 192,
"height": 192
}
```
Returns `{ ok: true, id, documentId }` — id of the new sticker and its document.
### `holst_update_text`
```json
{ "boardId": "...", "objectId": "...", "text": "new text" }
```
Changes text in an existing object (sticker/section/simple text). Internally — `Y.Text.delete + insert`, does not recreate the object.
## Pitfalls
- **Holst update may break the scheme.** Version checked: Holst 0.0.2 (Electron 29.4.6). If the names of share keys (`documents`, `objects`), Yjs cases, or window globals (`window.ds`, `window.Yjs`) change in a new version — you need to fix `src/holst.js`.
- **Bearer token rotates during logout.** If the user logs out — MCP will return an error, you need to run `start-holst.sh` and log in again.
- **CDP port must be kept open.** If Holst crashes or restarts without a flag — MCP will not connect. `start-holst.sh` fixes this.
- **Nothing blocks concurrent editing** — if you write through MCP, and simultaneously a person edits the same board in UI, Yjs will merge everything. But if an object has just been deleted by a person and MCP tries to update it — there will be an `object not found` error.
## Troubleshooting
| Symptom | Cause | What to do |
|---|---|---|
| `No app.holst.so tab found` | Holst not launched or without CDP flag | `~/Developer/holst-mcp/bin/start-holst.sh` |
| `Holst API 401: unauthorized` | Session lost / logged out | Log in to Holst desktop, then repeat |
| `JS exception: TypeError: e.toJSON is not a function` | Recorded plain object instead of Y.Map | Possible incompatibility with the new version of Holst — check `holst.js` |
| `JS exception: Cannot read properties of undefined (reading 'yDoc')` | Board not loaded yet | Wait 5 seconds, repeat |
## Development
Smoke test locally (Holst must be launched):
```bash
cd ~/Developer/holst-mcp
node -e "import('./src/holst.js').then(m => m.whoAmI()).then(console.log)"
```
Structure:
```
src/
├── server.js # MCP server (stdio)
├── cdp.js # CDP client over WebSocket
└── holst.js # Holst specifics (yDoc, gapi, tools)
bin/
└── start-holst.sh # Launches Holst.app with debug flag
```
## License
MIT.