Skip to main content
Glama
DChuhin
by DChuhin
README.md
# Miro MCP server (custom)

TypeScript [Model Context Protocol](https://modelcontextprotocol.io/) server that talks to the **Miro REST API v2** (plus **experimental** mind map endpoints under `v2-experimental`). It exposes tools for listing board items, board metadata, native [mind maps](https://developers.miro.com/docs/mind-maps), creating stickies, frames, shapes, and connectors, and updating, moving, or deleting items. The host (Cursor, Claude Desktop, or another MCP client) spawns this process over **stdio**; the model decides when to call each tool from the tool descriptions.

**Requirements:** Node.js **18+**, a Miro OAuth access token with **`boards:read`** and **`boards:write`**.

---

## Setup

### 1. Clone the repository

```bash
git clone https://github.com/DChuhin/miro-mcp-server.git
cd miro-mcp-server
```

Use your fork’s URL if you cloned from elsewhere.

### 2. Install dependencies and build

```bash
npm install
npm run build
```

The MCP entrypoint is **`dist/index.js`**. Re-run **`npm run build`** after changing TypeScript sources if your client runs the compiled file (see Cursor / Claude below).

### 3. Miro token and board ID

**Token (required for the server to call Miro)**

1. In Miro, open the [Developer / Your apps](https://developers.miro.com/) area and create an app.
2. Enable scopes **`boards:read`** and **`boards:write`**.
3. Complete the OAuth flow to obtain an **access token** (see [Miro OAuth](https://developers.miro.com/docs/getting-started-with-oauth)).

You can keep secrets out of the shell by copying the example env file:

```bash
cp .env.example .env
# Edit .env and set MIRO_TOKEN=...
```

For **Cursor** and **Claude Desktop**, the most reliable approach is to put **`MIRO_TOKEN`** in the MCP config’s **`env`** block (see below). The spawned process may not load `.env` unless the client sets the working directory to the project root.

Never commit `.env` or paste tokens into chat.

**Board ID (required in normal use)**

Tools take a **`board_id`** argument. The model needs the **board ID** whenever you ask for board-specific work. Find it in the board URL: the segment after **`/board/`** (example shape: `uXjVGnXi5V0=`). Mention it explicitly in prompts, for example: “On board `uXjV…`, list sticky notes.”

The server does **not** read a default board from the environment for MCP tool calls. **`MIRO_BOARD_ID`** is only useful for the optional **integration test script** (see [Development](#development)).

### Cursor

1. Complete [setup steps 1–3](#setup) (clone, **`npm install`**, **`npm run build`**, obtain a token).
2. Open **Cursor Settings → MCP** (or edit the MCP config file directly). Cursor commonly uses **`~/.cursor/mcp.json`**.

Example (replace the path with the **absolute** path to **this** repo on your machine):

```json
{
  "mcpServers": {
    "miro-custom": {
      "command": "node",
      "args": ["/absolute/path/to/miro-mcp-server/dist/index.js"],
      "env": {
        "MIRO_TOKEN": "your_miro_oauth_token_here"
      }
    }
  }
}
```

**Alternative (development):** point `args` at `tsx` and the TypeScript entry so you can skip **`npm run build`** while iterating:

```json
{
  "mcpServers": {
    "miro-custom": {
      "command": "npx",
      "args": ["tsx", "/absolute/path/to/miro-mcp-server/src/index.ts"],
      "env": {
        "MIRO_TOKEN": "your_miro_oauth_token_here"
      }
    }
  }
}
```

Reload MCP servers or restart Cursor after saving the config.

### Claude Desktop

1. Complete [setup steps 1–3](#setup) (clone, **`npm install`**, **`npm run build`**, obtain a token).
2. Edit **Claude Desktop** MCP configuration (location varies by OS; search for **`claude_desktop_config.json`** in Claude’s documentation).
3. Under `mcpServers`, add the same shape as in Cursor, using the absolute path to **`dist/index.js`**:

```json
{
  "mcpServers": {
    "miro-custom": {
      "command": "node",
      "args": ["/absolute/path/to/miro-mcp-server/dist/index.js"],
      "env": {
        "MIRO_TOKEN": "your_miro_oauth_token_here"
      }
    }
  }
}
```

Restart Claude Desktop after changes. Enable the **miro-custom** server in the app if your client lists MCP servers per conversation.

### Sanity check (optional)

```bash
npm start
```

This runs the MCP server on stdio. On its own it will appear to “hang”; that is normal. You normally **do not** run it in a terminal unless you are debugging—the editor spawns it once MCP is configured.

---

## Using the agent (LLM + tools)

After the MCP server is connected in Cursor or Claude:

1. Open a chat that has **MCP tools** enabled for this server.
2. Give the **board ID** (from the board URL) when you ask for board-specific work.
3. The model can call tools such as `get_board_info`, `list_board_items`, `list_mindmap_nodes`, `create_mindmap_node`, `delete_mindmap_node`, `create_sticky_note`, `create_frame`, `create_connector`, `update_item_content`, `move_item`, and `delete_item`.

Example prompts:

- “Call `get_board_info` for board `uXjV…` and summarize the board.”
- “List all sticky notes on board `…`, then add a yellow sticky at (100, -50) with the text ‘Review Q2’.”
- “Create a frame titled ‘Backlog’ and two stickies inside it connected by a curved connector.”
- “On board `…`, call `list_mindmap_nodes`, then add a child node under id `…` with text ‘New branch’.”

**Mind maps** use Miro’s native widget (text nodes; connectors are managed by Miro). Use `list_mindmap_nodes` / `create_mindmap_node` / `delete_mindmap_node`—not shapes plus `create_connector`.

**Why coordinates?** In the desktop app, **+** uses Miro’s **internal layout**; the **experimental REST** [create mind map node](https://developers.miro.com/reference/create-mindmap-nodes-experimental) API still expects a **`position`** and **defaults omitted coordinates to `(0,0)`**, which stacks every node. That endpoint does **not** expose the same auto-placement as the UI, so this server **computes `x/y`** unless you pass them explicitly. Parent positions for that math come from **`list_mindmap_nodes`** (or a direct experimental mind map node read), **not** from generic `GET /v2/boards/.../items/{id}`—nested mind map nodes can report **widget-local** coordinates there, which would shift deeper children incorrectly.

**Layout rule (this tool):** **Left-to-right** mind maps only—the **root is leftmost**; **every child is placed to the right** of its parent (`parent.x +` offset). **Siblings** of the same parent share that X column and are **spaced vertically** using **`layout_sibling_index`** (0, 1, 2, …)—pass it when creating several children in a row because `list_mindmap_nodes` can lag. The create payload must stay within **MindmapCreateRequest** only. Overview: [Mind map (Experimental)](https://developers.miro.com/docs/mind-maps).

**Example hierarchy** — use each create response’s `id` (or `list_mindmap_nodes`) as the next `parent_node_id`:

| Order | `content` | `parent_node_id` | `layout_sibling_index` |
|------:|-----------|------------------|-------------------------|
| 1 | Center | *(omit = root)* | *(omit)* |
| 2 | node1 | Center’s id | `0` |
| 3 | node2 | Center’s id | `1` |
| 4–7 | nebula, quartz, velvet, ember | node1’s id | `0`, `1`, `2`, `3` |
| 8–11 | cascade, prism, lotus, raven | node2’s id | `0`, `1`, `2`, `3` |

Optional: set root position with `x` / `y` on the first call (e.g. `1160`, `0`).

The agent chooses tools based on their registered names and descriptions; you do not call the REST API yourself in normal use.

---

## Development

Run the server with `tsx` (no separate build step):

```bash
npm run dev
```

Ensure `MIRO_TOKEN` is set (for example via `.env` in the project root). The dev server still uses stdio, so it is mainly useful with an MCP client attached or for quick sanity checks.

**Optional integration test** against a real board (creates and then deletes test widgets unless `SKIP_CLEANUP=1`):

```bash
export MIRO_TOKEN='your_token'
export MIRO_BOARD_ID='board_id_from_url'
npx tsx scripts/miro-integration-test.ts
```

Compile manually when not using `npm run dev` with `tsx`:

```bash
npm run build
```

### Project layout

| Path | Role |
|------|------|
| `src/index.ts` | MCP server entry, registers tools |
| `src/miro-client.ts` | HTTP client for `https://api.miro.com/v2` |
| `src/tools/items.ts` | `list_board_items`, `get_board_info` |
| `src/tools/mindmap.ts` | `list_mindmap_nodes`, `create_mindmap_node`, `delete_mindmap_node` (experimental API) |
| `src/tools/create.ts` | Create stickies, frames, shapes, connectors |
| `src/tools/mutate.ts` | Update content, move, delete |
| `scripts/miro-integration-test.ts` | Optional real-board test |

---

## Troubleshooting

- **401 / token errors:** Regenerate or refresh the OAuth token; confirm `boards:read` and `boards:write`.
- **Server not listed:** Fix JSON in the MCP config, use absolute paths, restart the app.
- **Old behaviour after edits:** Run `npm run build` again if the client uses `dist/index.js`.

---

## License

MIT

TDQS

A4/5.0

Scored across 13 tools

Disambiguation4/5

Each tool targets a distinct item type or action, and the descriptions explicitly clarify the tricky boundary between generic items (update_item_content, delete_item) and native mind map nodes (update_mindmap_node, delete_mindmap_node). Minor ambiguity remains around whether delete_item/move_item apply to mind map nodes, but overall boundaries are clear.

Naming Consistency5/5

All 13 tools follow a strict snake_case verb_noun pattern (list_board_items, create_sticky_note, update_mindmap_node, delete_item), with a consistent `<verb>_<resource>` structure across both generic and mind-map-specific tools. No convention mixing.

Tool Count5/5

13 tools is well-scoped for a board-manipulation server, with each tool earning its place by covering a distinct item type or lifecycle operation. No redundant or filler tools.

Completeness4/5

Covers solid create/read/update/delete across standard items (sticky, shape, frame, connector) and mind map nodes. Minor gaps exist: no way to discover/list available boards to obtain a board ID, and no update for connector endpoints or item styling/geometry beyond content.

Maintenance

ActivityInactive
ResponsivenessNo issues