Working with the reMarkable via remarkable-mcp
remarkable-mcp is a stdio MCP server (binary at ~/.local/bin/remarkable-mcp,
uvx remarkable-mcp also works) that talks to the reMarkable cloud (or the
tablet over SSH/USB). It exposes read and write tools for documents on the
tablet.
Setup and auth
Register the server once, at user scope so it is available in every project:
claude mcp add remarkable -s user -- remarkable-mcp
claude mcp list # expect: remarkable: remarkable-mcp - ✔ Connected
Auth uses a cloud token stored in ~/.rmapi. If that file exists, the server is
already authenticated (remarkable_status reports "authenticated": true) — no
further action. To register a new device, get a one-time code from
https://my.remarkable.com/device/desktop and run:
remarkable-mcp --register <code> # prints/stores the token
Transports: cloud API by default; --ssh (developer mode) or --usb for a
directly-connected tablet; --no-cloud-fallback to forbid the cloud fallback.
Write tools (upload/mkdir/move/rename/delete) are on by default; pass
--read-only to expose a read-only server.
Tool inventory
| Tool |
Purpose |
Key args |
remarkable_status |
auth/transport/capabilities, document count |
— |
remarkable_browse |
list a folder / filter |
path, query, tags |
remarkable_recent |
most recently modified docs |
limit, include_preview |
remarkable_search |
find a doc; returns path + page OCR text |
query, grep, limit, include_ocr |
remarkable_read |
page text / OCR; content_type=annotations lists annotated pages + highlighted text |
document, page, content_type, include_ocr |
remarkable_image |
render a page to PNG; render_merged=true composites PDF + strokes + highlights |
document, page, render_merged, output_format |
remarkable_canvas |
interactive canvas render of a page |
document, page |
remarkable_upload |
upload a local file |
file_path (req), parent_folder, document_name |
remarkable_mkdir / move / rename / delete |
manage docs/folders |
see schema |
Get exact schemas at runtime with tools/list (see the script below).
Reading annotations: merged render + highlight/notes extraction
Requires the patched build — the fixes below live on the
fix/render-merged-imported-pdfs branch of ~/devel/remarkable-mcp, installed
editable via uv tool install --editable ~/devel/remarkable-mcp --force. The
PyPI remarkable-mcp (≤ 1.0.0) still has the bugs listed under "Unpatched
release" — check with uv tool list (a file:// / local version = patched).
With the patched build:
- Composite an annotated page:
remarkable_image(document, page, render_merged=True) returns one PNG with the PDF page + pen strokes + text
highlights at correct positions (PNG only; the page needs a PDF underlay).
Save the blob and Read it (see the resource-blob gotcha).
- Find/extract annotations without scanning every page:
remarkable_read(document, content_type="annotations") returns an "Annotated
pages" section — only the pages that carry annotations, each showing whether
it has handwritten notes and its highlighted text. The underlying
extraction result also exposes highlights (flat list) and annotated_pages
([{page, page_id, has_handwriting, highlights}], 1-based page numbers).
Highlight text is the reMarkable's stored text selection: great for locating
annotated passages, but occasionally glitchy (a dropped letter, or a sentence
split across entries) — treat it as a finder, not a verbatim transcript.
Unpatched release (PyPI ≤ 1.0.0): use the native email export
On the release build, render_merged is broken for imported PDFs
(formatVersion-1 docs render annotation-only; the coordinate transform
mis-places ink — single strokes off-position, multi-stroke pages fill solid
black), text highlights never render, and content_type=annotations returns no
highlights. For correct output there, use reMarkable's own on-device export:
- Tablet: open the document → Page overview → long-press the page → ⋯
(More) → Send by email → PDF (or PNG) → send to the user's own
address.
- Retrieve it via a connected Gmail/mail MCP (
claude mcp list to check) — pull
the attachment — or ask the user to share the file. Then Read it; positions
are exact because reMarkable rendered it. You cannot trigger this export
headlessly (no cloud endpoint); the user taps once, you fetch and read.
Gotchas (learned the hard way — read before using)
Address documents by PATH or name, not the upload UUID. remarkable_upload
returns a uuid, but remarkable_read/remarkable_image look up by path
(e.g. /My Paper (draft)) or name — passing the uuid gives
document_not_found. After uploading, call remarkable_search/remarkable_recent
to get the path, then use that.
Pages are 1-based. page=0 → page_out_of_range ("use page=1 to N").
Mid-session claude mcp add (or reinstalling the tool) does not reach the
current session. claude mcp list shows ✔ Connected, but the
mcp__remarkable__* tools — and any freshly-installed build — are only picked
up at session start, so ToolSearch won't find the tools until a new
session. To act immediately in the same session, drive the stdio server
directly with scripts/rm_mcp.py (a JSON-RPC client).
Annotations only exist in the cloud once the tablet has synced. A layer
made on the device is invisible to the cloud API until the tablet syncs up.
Signs it hasn't: the document's modified time still equals the upload time;
render_merged/content_type=annotations show no ink or highlights. Do not
conclude there is no annotation — ask the user to sync (Wi-Fi, wake the device
/ manual sync), then retry.
Image results come back as an MCP resource blob. In the tools/call
result, content[] holds an item of type resource with base64 in
resource.blob (or type image with data). Save those bytes to a .png
and Read it. Do not rely on resources/read of the remarkableimg:///…
URI — spaces/parentheses in document names break it ("unbalanced
parenthesis"); use the inline blob from tools/call instead. Note some modes
(compatibility=true) return the PNG as a data:image/png;base64,… string in
a data_uri JSON field instead — handle that shape too.
Driving the server directly (in-session fallback)
When the MCP tools are not yet loaded (gotcha 3), or for scripted/batch use, run
scripts/rm_mcp.py. It speaks the newline-delimited JSON-RPC stdio protocol
(initialize → notifications/initialized → tools/call) and handles saving
image blobs to files.
# discover tools and schemas
scripts/rm_mcp.py list
# status / find a just-uploaded doc's path
scripts/rm_mcp.py call remarkable_status
scripts/rm_mcp.py call remarkable_recent '{"limit":5}'
# upload a PDF (root folder)
scripts/rm_mcp.py call remarkable_upload \
'{"file_path":"/abs/path/article.pdf","document_name":"My Paper (draft)"}'
# render page 1 with annotations composited onto the PDF (patched build)
scripts/rm_mcp.py render "/My Paper (draft)" 1 out.png
# list annotated pages + highlighted text
scripts/rm_mcp.py call remarkable_read \
'{"document":"/My Paper (draft)","content_type":"annotations"}'
Then Read the saved PNG. On the patched build render composites the page + ink
- highlights at correct positions; on the unpatched release it returns only the
(cropped, possibly mis-placed) ink layer — see the version note above. The script
is small and provider-agnostic; read it before extending.
Verify an upload
remarkable_upload returns "uploaded": true with a uuid; confirm it landed
with remarkable_recent/remarkable_search (the cloud may take a moment to
index). It syncs to the physical device when the tablet is online.
Versioned review workflow (send a draft, iterate, keep history)
The common loop is: send a document to the tablet, the user reviews and
annotates it there, you read the annotations, revise the source, and send
the revised build back. Do it as a versioned trail, not an overwrite.
Why this matters: remarkable_upload to the cloud is add-only — it
never replaces. Re-uploading a revised PDF under the same name creates a
second document with that same name, not a new revision of the first. So
uploads naturally accumulate; embrace that instead of fighting it.
The workflow, unless the user says otherwise:
- Bake the version — and the branch — into the name at upload time.
Every upload gets a unique
document_name with date and version:
"<Title> (draft YYYY-MM-DD, vN)" on the default branch (the vt-debug
paper's convention; "<Title> (review vN)" works for non-drafts), and
"<Title> (<branch>, draft YYYY-MM-DD, vN)" when the build comes from a
feature branch or worktree — e.g. learnlog (metrics, draft 2026-08-31, v8). Take <branch> from git branch --show-current, dropping a
worktree- prefix, so the user can tell on the tablet which line of
work a draft belongs to and two branches' drafts do not interleave
silently in one version sequence. <Title> is the existing document
family's name — before the first upload in a session, run
remarkable_browse(query="<title word>") and reuse the family name and
its highest vN; inventing a new family ("learnlog documentation (metrics, …, v1)" beside an existing "learnlog (draft …, v7)")
breaks the version trail and costs a rename. Because no two uploads
share a name, there is no rename step and no name-collision
ambiguity — every later tool call addresses the document by its
unique name. Never upload under a bare base name planning to rename
afterwards.
- Leave the earlier versions in place. The tablet then holds the
whole review history, each version distinguishable at a glance, and
the user can compare against their earlier annotations. Do not
delete or overwrite prior versions unless the user asks (a version
superseded within minutes, before the user opened it, is fair to offer
to delete — still ask).
- Pick N by incrementing the last uploaded version — from the
conversation, the repo's commit messages (pairing each upload with its
commit, message noting "uploaded as draft YYYY-MM-DD, vN", keeps this
trail in git), or by
remarkable_search-ing the title and taking the
highest existing suffix across all branches (one counter per
document family, not per branch, so v8 on a feature branch follows v7
from main and a later main build is v9). Version numbers are cheap: a
same-day follow-up change gets vN+1, never a silent re-upload of vN.
- Read annotations from the version the user reviewed — usually the
latest, but if they name an older draft, read that one. Before
concluding a version has no annotations, remember the sync trap
(gotcha 4): ask the user to sync rather than report "no comments".
If you did end up with two documents sharing a name (e.g. an upload
that was meant to replace): rename by path/name, never the upload UUID
(gotcha 1 — the UUID lookup fails). remarkable_browse/_recent lists
the most recently modified first, so the fresh upload is the first
match and rename-by-name targets it — but verify afterward: re-browse
and check that the new name landed on the entry whose modified time is
the newest, not on an older version.
If the user instead wants a single evolving document (no history), the
add-only cloud API can't update in place — delete the prior version after a
successful new upload (ask first; deletion is destructive), or keep one
name and let them tell versions apart by modified date.
For research papers, the surrounding loop (read all annotated pages, apply
the round, rebuild, one commit per round named after the draft, push,
upload the next version) is owned by the scientific-writing skill;
this section owns only the tablet mechanics.
1---2name: remarkable3description: IMPORTANT: load this skill BEFORE calling ANY remarkable-mcp tool — even a one-line upload — because the tools have silent-failure gotchas (address docs by path/name, NOT the UUID that upload returns; annotations only appear after the tablet syncs; render_merged needs the patched build). Work with a reMarkable tablet via the remarkable-mcp MCP server: upload PDFs/EPUBs, browse/search the cloud, render pages (optionally compositing annotations onto the PDF), read page text/OCR, extract highlighted text, and list which pages carry notes/highlights. Use when: (1) sending/uploading a document, paper, or PDF to the reMarkable ('put this on my reMarkable', 'upload to remarkable'); (2) reading/rendering/checking an annotation, note, or highlight, or extracting only the annotated pages; (3) the user mentions reMarkable, remarkable-mcp, rmapi, or their tablet; (4) setting up or troubleshooting the server. Covers auth, the tool inventory, annotation extraction, and more gotchas (1-based pages, mid-session MCP loading).4---56# Working with the reMarkable via remarkable-mcp78`remarkable-mcp` is a stdio MCP server (binary at `~/.local/bin/remarkable-mcp`,9`uvx remarkable-mcp` also works) that talks to the reMarkable cloud (or the10tablet over SSH/USB). It exposes read and write tools for documents on the11tablet.1213## Setup and auth1415Register the server once, at **user** scope so it is available in every project:1617```bash18claude mcp add remarkable -s user -- remarkable-mcp19claude mcp list # expect: remarkable: remarkable-mcp - ✔ Connected20```2122Auth uses a cloud token stored in `~/.rmapi`. If that file exists, the server is23already authenticated (`remarkable_status` reports `"authenticated": true`) — no24further action. To register a new device, get a one-time code from25`https://my.remarkable.com/device/desktop` and run:2627```bash28remarkable-mcp --register <code> # prints/stores the token29```3031Transports: cloud API by default; `--ssh` (developer mode) or `--usb` for a32directly-connected tablet; `--no-cloud-fallback` to forbid the cloud fallback.33Write tools (upload/mkdir/move/rename/delete) are **on by default**; pass34`--read-only` to expose a read-only server.3536## Tool inventory3738| Tool | Purpose | Key args |39|------|---------|----------|40| `remarkable_status` | auth/transport/capabilities, document count | — |41| `remarkable_browse` | list a folder / filter | `path`, `query`, `tags` |42| `remarkable_recent` | most recently modified docs | `limit`, `include_preview` |43| `remarkable_search` | find a doc; returns `path` + page OCR text | `query`, `grep`, `limit`, `include_ocr` |44| `remarkable_read` | page text / OCR; `content_type=annotations` lists annotated pages + highlighted text | `document`, `page`, `content_type`, `include_ocr` |45| `remarkable_image` | render a page to PNG; `render_merged=true` composites PDF + strokes + highlights | `document`, `page`, `render_merged`, `output_format` |46| `remarkable_canvas` | interactive canvas render of a page | `document`, `page` |47| `remarkable_upload` | upload a local file | `file_path` (req), `parent_folder`, `document_name` |48| `remarkable_mkdir` / `move` / `rename` / `delete` | manage docs/folders | see schema |4950Get exact schemas at runtime with `tools/list` (see the script below).5152## Reading annotations: merged render + highlight/notes extraction5354**Requires the patched build** — the fixes below live on the55`fix/render-merged-imported-pdfs` branch of `~/devel/remarkable-mcp`, installed56editable via `uv tool install --editable ~/devel/remarkable-mcp --force`. The57PyPI `remarkable-mcp` (≤ 1.0.0) still has the bugs listed under "Unpatched58release" — check with `uv tool list` (a `file://` / local version = patched).5960With the patched build:6162- **Composite an annotated page:** `remarkable_image(document, page,63 render_merged=True)` returns one PNG with the **PDF page + pen strokes + text64 highlights** at correct positions (PNG only; the page needs a PDF underlay).65 Save the blob and Read it (see the resource-blob gotcha).66- **Find/extract annotations without scanning every page:**67 `remarkable_read(document, content_type="annotations")` returns an **"Annotated68 pages"** section — only the pages that carry annotations, each showing whether69 it has handwritten notes and its **highlighted text**. The underlying70 extraction result also exposes `highlights` (flat list) and `annotated_pages`71 (`[{page, page_id, has_handwriting, highlights}]`, 1-based page numbers).7273Highlight text is the reMarkable's stored text selection: great for *locating*74annotated passages, but occasionally glitchy (a dropped letter, or a sentence75split across entries) — treat it as a finder, not a verbatim transcript.7677### Unpatched release (PyPI ≤ 1.0.0): use the native email export78On the release build, `render_merged` is broken for imported PDFs79(formatVersion-1 docs render annotation-only; the coordinate transform80mis-places ink — single strokes off-position, multi-stroke pages fill solid81black), text highlights never render, and `content_type=annotations` returns no82highlights. For correct output there, use reMarkable's **own on-device export**:83841. Tablet: open the document → **Page overview** → long-press the page → **⋯85 (More)** → **Send by email** → **PDF** (or **PNG**) → send to the user's own86 address.872. Retrieve it via a connected Gmail/mail MCP (`claude mcp list` to check) — pull88 the attachment — or ask the user to share the file. Then Read it; positions89 are exact because reMarkable rendered it. You **cannot** trigger this export90 headlessly (no cloud endpoint); the user taps once, you fetch and read.9192## Gotchas (learned the hard way — read before using)93941. **Address documents by PATH or name, not the upload UUID.** `remarkable_upload`95 returns a `uuid`, but `remarkable_read`/`remarkable_image` look up by path96 (e.g. `/My Paper (draft)`) or name — passing the uuid gives97 `document_not_found`. After uploading, call `remarkable_search`/`remarkable_recent`98 to get the `path`, then use that.991002. **Pages are 1-based.** `page=0` → `page_out_of_range` ("use page=1 to N").1011023. **Mid-session `claude mcp add` (or reinstalling the tool) does not reach the103 current session.** `claude mcp list` shows `✔ Connected`, but the104 `mcp__remarkable__*` tools — and any freshly-installed build — are only picked105 up at session start, so `ToolSearch` won't find the tools until a **new**106 session. To act immediately in the same session, drive the stdio server107 directly with `scripts/rm_mcp.py` (a JSON-RPC client).1081094. **Annotations only exist in the cloud once the tablet has synced.** A layer110 made on the device is invisible to the cloud API until the tablet syncs up.111 Signs it hasn't: the document's `modified` time still equals the upload time;112 `render_merged`/`content_type=annotations` show no ink or highlights. Do not113 conclude there is no annotation — ask the user to sync (Wi-Fi, wake the device114 / manual sync), then retry.1151165. **Image results come back as an MCP resource blob.** In the `tools/call`117 result, `content[]` holds an item of type `resource` with base64 in118 `resource.blob` (or type `image` with `data`). Save those bytes to a `.png`119 and Read it. Do **not** rely on `resources/read` of the `remarkableimg:///…`120 URI — spaces/parentheses in document names break it ("unbalanced121 parenthesis"); use the inline blob from `tools/call` instead. Note some modes122 (`compatibility=true`) return the PNG as a `data:image/png;base64,…` string in123 a `data_uri` JSON field instead — handle that shape too.124125## Driving the server directly (in-session fallback)126127When the MCP tools are not yet loaded (gotcha 3), or for scripted/batch use, run128`scripts/rm_mcp.py`. It speaks the newline-delimited JSON-RPC stdio protocol129(`initialize` → `notifications/initialized` → `tools/call`) and handles saving130image blobs to files.131132```bash133# discover tools and schemas134scripts/rm_mcp.py list135136# status / find a just-uploaded doc's path137scripts/rm_mcp.py call remarkable_status138scripts/rm_mcp.py call remarkable_recent '{"limit":5}'139140# upload a PDF (root folder)141scripts/rm_mcp.py call remarkable_upload \142 '{"file_path":"/abs/path/article.pdf","document_name":"My Paper (draft)"}'143144# render page 1 with annotations composited onto the PDF (patched build)145scripts/rm_mcp.py render "/My Paper (draft)" 1 out.png146147# list annotated pages + highlighted text148scripts/rm_mcp.py call remarkable_read \149 '{"document":"/My Paper (draft)","content_type":"annotations"}'150```151152Then Read the saved PNG. On the patched build `render` composites the page + ink153+ highlights at correct positions; on the unpatched release it returns only the154(cropped, possibly mis-placed) ink layer — see the version note above. The script155is small and provider-agnostic; read it before extending.156157## Verify an upload158159`remarkable_upload` returns `"uploaded": true` with a uuid; confirm it landed160with `remarkable_recent`/`remarkable_search` (the cloud may take a moment to161index). It syncs to the physical device when the tablet is online.162163## Versioned review workflow (send a draft, iterate, keep history)164165The common loop is: send a document to the tablet, the user reviews and166annotates it there, you read the annotations, revise the source, and send167the revised build back. Do it as a **versioned trail**, not an overwrite.168169**Why this matters:** `remarkable_upload` to the cloud is **add-only — it170never replaces**. Re-uploading a revised PDF under the same name creates a171*second* document with that same name, not a new revision of the first. So172uploads naturally accumulate; embrace that instead of fighting it.173174The workflow, unless the user says otherwise:1751761. **Bake the version — and the branch — into the name at upload time.**177 Every upload gets a unique `document_name` with date and version:178 `"<Title> (draft YYYY-MM-DD, vN)"` on the default branch (the vt-debug179 paper's convention; `"<Title> (review vN)"` works for non-drafts), and180 `"<Title> (<branch>, draft YYYY-MM-DD, vN)"` when the build comes from a181 feature branch or worktree — e.g. `learnlog (metrics, draft 2026-08-31,182 v8)`. Take `<branch>` from `git branch --show-current`, dropping a183 `worktree-` prefix, so the user can tell on the tablet which line of184 work a draft belongs to and two branches' drafts do not interleave185 silently in one version sequence. `<Title>` is the **existing document186 family's name** — before the first upload in a session, run187 `remarkable_browse(query="<title word>")` and reuse the family name and188 its highest vN; inventing a new family (`"learnlog documentation189 (metrics, …, v1)"` beside an existing `"learnlog (draft …, v7)"`)190 breaks the version trail and costs a rename. Because no two uploads191 share a name, there is **no rename step and no name-collision192 ambiguity** — every later tool call addresses the document by its193 unique name. Never upload under a bare base name planning to rename194 afterwards.1952. **Leave the earlier versions in place.** The tablet then holds the196 whole review history, each version distinguishable at a glance, and197 the user can compare against their earlier annotations. Do **not**198 delete or overwrite prior versions unless the user asks (a version199 superseded within minutes, before the user opened it, is fair to offer200 to delete — still ask).2013. **Pick N by incrementing the last uploaded version** — from the202 conversation, the repo's commit messages (pairing each upload with its203 commit, message noting "uploaded as draft YYYY-MM-DD, vN", keeps this204 trail in git), or by `remarkable_search`-ing the title and taking the205 highest existing suffix **across all branches** (one counter per206 document family, not per branch, so v8 on a feature branch follows v7207 from main and a later main build is v9). Version numbers are cheap: a208 same-day follow-up change gets vN+1, never a silent re-upload of vN.2094. **Read annotations from the version the user reviewed** — usually the210 latest, but if they name an older draft, read that one. Before211 concluding a version has no annotations, remember the sync trap212 (gotcha 4): ask the user to sync rather than report "no comments".213214**If you did end up with two documents sharing a name** (e.g. an upload215that was meant to replace): rename by path/name, never the upload UUID216(gotcha 1 — the UUID lookup fails). `remarkable_browse`/`_recent` lists217the **most recently modified first**, so the fresh upload is the first218match and rename-by-name targets it — but **verify afterward**: re-browse219and check that the new name landed on the entry whose `modified` time is220the newest, not on an older version.221222If the user instead wants a single evolving document (no history), the223add-only cloud API can't update in place — delete the prior version after a224successful new upload (ask first; deletion is destructive), or keep one225name and let them tell versions apart by modified date.226227For research papers, the surrounding loop (read all annotated pages, apply228the round, rebuild, one commit per round named after the draft, push,229upload the next version) is owned by the **scientific-writing** skill;230this section owns only the tablet mechanics.