remarkable-mcp
v1.0.1
Published
MCP server for reMarkable 2 — pull handwritten notes into any AI agent
Downloads
253
Maintainers
Readme
remarkable-mcp
MCP server for reMarkable 2 — pull handwritten notes and diagrams into any AI agent.
Works with Claude Desktop, Claude Code, Codex, Cursor, and any MCP-compatible client.
How it connects
- USB connected: uses the tablet's HTTP web interface (
http://10.11.99.1) — fastest, no SSH needed - WiFi only: falls back to SSH over the tablet's WiFi IP — requires SSH key setup
Requirements
- Node.js 20+
- reMarkable 2 tablet running firmware 3.9 or later (older firmware lacks the rmdoc download endpoint used by the USB HTTP path; the SSH fallback still works)
Client configuration
Claude Desktop — single-click install (recommended)
⬇ Download the latest .mcpb — then double-click the file. Claude Desktop opens the install dialog; click Install, restart Claude, and you're ready. No CLI, no config editing.
All releases (with checksums and changelog) are at https://github.com/Scratchydisk/remarkable-mcp/releases.
.mcpb(MCP Bundle, formerly.dxt) is Anthropic's single-file extension format. The bundle ships with all dependencies embedded, so you don't need Node.js ornpmfor the Desktop install path.
Claude Desktop — manual config
If you'd rather edit JSON, add to ~/Library/Application Support/Claude/claude_desktop_config.json:
{
"mcpServers": {
"remarkable": {
"command": "npx",
"args": ["-y", "remarkable-mcp"]
}
}
}Claude Code
{
"mcp": {
"servers": {
"remarkable": {
"command": "npx",
"args": ["-y", "remarkable-mcp"]
}
}
}
}Other MCP clients
Any client that supports stdio MCP servers:
{
"command": "npx",
"args": ["-y", "remarkable-mcp"]
}Optional: global install (faster startup)
npm install -g remarkable-mcpThen use "command": "remarkable-mcp" with no args.
First-time setup
Connect your tablet via USB, then ask your agent:
"Set up my reMarkable. The password is [password from Settings → Help → Copyright and licenses]."
The agent calls remarkable_setup, which:
- Deploys an SSH keypair (for WiFi use)
- Enables the USB web interface on the tablet
- Captures the tablet's SSH host-key fingerprint and pins it for future connections
- Discovers and saves your tablet's WiFi IP
Security note: the password is sent to your MCP client as a tool argument and may appear in agent transcripts or logs. The host-key fingerprint is captured over the USB cable (which is link-local and host-only) and verified on every WiFi SSH connect thereafter, so a man-in-the-middle on your LAN cannot impersonate the tablet.
Tools
remarkable_setup
Two modes:
- Full setup — call with
password(from Settings → Help → Copyright and licenses). Generates an SSH keypair, deploys the public key over USB, enables the on-tablet USB web interface, captures and pins the SSH host-key fingerprint, and saves the WiFi IP. - Refresh — call with no arguments. Assumes a prior full setup and just rediscovers the current WiFi IP using the existing SSH key. Tries USB SSH → saved WiFi IP →
remarkable.local(mDNS). Use this after the tablet rejoins the network on a different DHCP lease — the agent's prompt could simply be "refresh my reMarkable WiFi".
remarkable_list
Lists all documents on the tablet, sorted by most recently modified. Optional folder substring filter.
remarkable_pull
Pulls a document and returns rendered page images.
| Parameter | Type | Description |
|--------------|------------------|----------------------------------------------------------------------------------------------------------------------------|
| document | string? | Document name substring (defaults to most recent) |
| folder | string? | Folder name substring to disambiguate when the same document name exists in multiple folders |
| page | string? | Single page ("3") or inclusive range ("1-4"); defaults to all pages |
| output_dir | string? | Local directory to save rendered images. All pages are saved here, even ones too large to inline |
| prompt | string? | Custom transcription instruction (native mode: returned as agent context) |
| max_width | number? | Render width in pixels (default 1024; native rM2 is 1404). Smaller = smaller payloads |
| format | "png"/"jpeg" | Output format. PNG preserves linework; JPEG is much smaller for OCR-grade pulls |
| inline_images | boolean? | If false, return only saved file paths and skip inline image blocks (bypasses the response-size cap). Requires output_dir. Default true. |
Document caching. Downloaded sources are cached at ~/.cache/remarkable-mcp/<docId>/<mtime>/ (override with REMARKABLE_CACHE_DIR). Repeat pulls of the same document at the same mtime are instant. The cache invalidates automatically when you edit the document on the tablet (mtime moves forward); stale entries are swept on the next write.
Unflushed pages. reMarkable saves a page's .rm file only when you change pages or close the document. If you pull while a notebook is open, any unsaved page is reported in the response (e.g. "Page 3 had no saved data"), and that pull is not cached — re-pull after closing the document on the tablet for a clean copy.
Response sizing. Claude Desktop has a ~1 MB tool-response cap. The server detects this and trims pages that wouldn't fit, returning a clear hint of the next call to make (e.g. page="3-5"). Set format="jpeg" and/or a smaller max_width to fit more pages per response. Override the cap with REMARKABLE_INLINE_BUDGET_BYTES.
remarkable_status
Diagnoses connectivity and configuration. Reports USB HTTP reachability, WiFi SSH reachability, tablet firmware, host-key pinning state, config and cache paths, and the connected MCP client. Useful for the agent to ask itself "what's wrong?" before guessing.
remarkable_search
Full-text search across the OCR'd contents of documents you've previously pulled. Returns ranked page-level hits with a snippet around each match. Powered by an in-process BM25 index (MiniSearch), so it's fast and offline.
| Parameter | Type | Description |
|-----------|---------|----------------------------------------------------------------------------------------|
| query | string | Search query. Supports prefix and fuzzy matching; multi-word queries are AND-combined. |
| limit | number? | Maximum hits to return (default 20, max 100). |
| folder | string? | Restrict to documents whose folder path contains this substring. |
Three ways to populate the search corpus. Pick whichever matches your setup:
- Set
ocr.providerto"ollama"or"local"in~/.config/remarkable-mcp/config.json— everyremarkable_pullthen caches per-page text automatically. - Stay on native OCR mode (the default; the host LLM reads images directly) and call
remarkable_save_transcriptionafter each pull to feed the transcription back into the cache. The agent can do this in the same turn it produces the transcription. remarkable_index— bulk-OCRs everything in one shot (ollama / local providers only).
Ranking favours doc-name and folder matches over body text.
remarkable_index
Bulk-OCRs every document on the tablet (or a folder subset) and adds it to the search corpus. Long-running on large libraries; documents already cached and indexed for their current mtime are skipped unless force: true. Requires ocr.provider set to "ollama" or "local". Run this once for a full library sweep, then let normal pulls keep the corpus current.
remarkable_save_transcription
Bridges native-OCR-mode pulls into the search index. The host LLM reads the page images and produces a transcription in its response; this tool gives the agent a place to put that text back so it survives the conversation.
| Parameter | Type | Description |
|-------------|---------|------------------------------------------------------------------------------------------------------------------------------|
| doc_id | string? | Tablet document UUID (preferred — exact). Included in the remarkable_pull response footer when running in native OCR mode. |
| document | string? | Document name substring (case-insensitive). Resolves to the most recently cached match. Use when doc_id isn't to hand. |
| pages | array | One entry per transcribed page: { pageNum, text }. Empty-text entries are dropped. |
Either doc_id or document must be provided. The text is appended to whatever the cache already has for that document; calling it twice with overlapping pages overwrites the previous entries. After saving, the document is immediately findable via remarkable_search.
Try it — example prompts
Once installed, ask your agent any of these:
"Set up my reMarkable. The password is
…." (plug in via USB; password from Settings → Help → Copyright and licenses) The agent runsremarkable_setuponce — deploys an SSH key, enables the USB web interface, pins the host-key fingerprint, captures the WiFi IP."Pull my latest reMarkable notebook and read it back to me." Agent calls
remarkable_listto find the most recently modified document, thenremarkable_pullto fetch and render its pages. In native OCR mode, the agent transcribes the images itself in its reply; inollama/localmode the OCR text is included in the response."Find my reMarkable notes that mention
<topic>." Agent callsremarkable_searchagainst the local BM25 index. (Requires having pulled some documents previously, or runningremarkable_indexfor a one-shot bulk OCR; see the Search section below.)
Other useful turns to try:
- "What's the status of my reMarkable connection?" →
remarkable_statuswith reachability + categorised error reasons. - "My WiFi changed; refresh the reMarkable IP." →
remarkable_setupwith no password (refresh mode). - "Pull pages 1 to 3 of
<doc>as JPEG and save them to~/Downloads." →remarkable_pullwithpage="1-3",format="jpeg", andoutput_dir.
Testing without a reMarkable tablet
For reviewers and contributors who don't have a tablet on hand:
- Install the
.mcpb(ornpx -y remarkable-mcp) into your MCP client. - Ask the agent: "Run remarkable_status."
- Expected output: a clean diagnostic — "USB HTTP not reachable…", "WiFi SSH no host configured…", "Host key not pinned…", plus the config and cache paths and render defaults. No stack trace, no thrown exception, no generic 500-style error.
- Try error-path coverage: "Pull a document with
max_width: huge." → expect a specific zod validation error naming the offending field, never a server crash.
This exercises the manifest, the schema validation, the tool annotations, and the error-message standard end-to-end, with no hardware. Full functionality (list / pull / search / save_transcription) requires a reMarkable 2 on firmware ≥ 3.9. Repository contributors can also clone and run npm run validate for the same coverage as a CLI-friendly transcript.
OCR modes
Set in ~/.config/remarkable-mcp/config.json:
- native (default): the host agent's LLM reads the image directly — no extra API key needed
- ollama: sends image to a local Ollama instance (
llama3.2-visionor configured model) - local: shells out to Tesseract for plain-text output
Render defaults
The same config file carries render defaults:
{
"render": {
"width": 1024,
"format": "png",
"jpegQuality": 80
}
}These are the fallback values when a remarkable_pull call doesn't specify max_width / format.
Troubleshooting
Run remarkable_status first — it reports each connection path with a categorised reason for any failure. The full list of error categories and what they mean:
| Error in response | What it means | Fix |
|---|---|---|
| timeout — tablet may be asleep, off, or the saved WiFi IP is stale | TCP connect didn't complete in 20 s | Wake the tablet (tap / draw a stroke). If still failing, the saved IP is probably stale — see "WiFi IP changed" below |
| connection refused — SSH daemon not listening | TCP reached the host but port 22 was closed | Tablet is rebooting or the IP is wrong (some other device on the LAN) |
| host unreachable — tablet not on the network | No route to the IP | Tablet is offline or on a different subnet — check the on-tablet WiFi icon |
| auth failed — SSH key not deployed | TCP + host-key OK but key auth was rejected | Re-run remarkable_setup with the password over USB |
| Host-key mismatch for X: expected sha256:…, got sha256:…. The tablet may have been reflashed | The tablet's SSH host key changed since pinning | If you reflashed the tablet, re-run remarkable_setup with the password over USB to re-pin |
WiFi IP changed (most common after a router reboot)
The server automatically tries remarkable.local (mDNS) when the saved IP is unreachable, so list and pull will usually self-recover. If you'd like to update the saved IP for faster future connections, ask the agent: "Refresh my reMarkable WiFi" — that calls remarkable_setup with no password, which probes USB SSH → saved IP → mDNS, picks the first one that works, and writes the discovered IP back to config.
Document came back missing pages
The reMarkable saves a page's .rm file only when you change pages or close the document. Pulling while a notebook is open returns whatever's been flushed; the response lists the unsaved page numbers. Close the document on the tablet (or change pages) and re-pull — the cache is mtime-keyed, so a write on the tablet automatically invalidates it.
Response too large for Claude Desktop
The server detects Claude Desktop's ~1 MB tool-response cap and trims pages that wouldn't fit, telling you the exact next call (page="3-5"). To fit more pages per response, use format="jpeg" and/or a smaller max_width (e.g. 800).
Debugging
Set DEBUG=remarkable-mcp (or DEBUG=*) in the MCP client's environment to get diagnostic output on stderr (stdout is reserved for the MCP transport). The debug output covers:
- USB / WiFi / mDNS lookup attempts and their results
- Cache hits and stale-mtime sweeps
- Render failures with the underlying error (e.g. missing peer dep, malformed
.rmfile) - Host-key mismatches with both fingerprints
Development
npm install
npm run typecheck
npm run lint
npm test
npm run build
npm run validate # spawn the server and assert on every tool's behaviour (writes validation-X.Y.Z.log)
npm run inspect # interactive Inspector UI for end-to-end testing
npm run mcpb # build remarkable-mcp-X.Y.Z.mcpb (Claude Desktop bundle)Releasing
- Bump
versioninpackage.jsonand updateCHANGELOG.md. npm test && npm run lint && npm run typecheck.npm run mcpb— produces bothremarkable-mcp-X.Y.Z.mcpb(versioned, archival) andremarkable-mcp.mcpb(unversioned mirror, used by thereleases/latest/download/link in the README).npm pack— producesremarkable-mcp-X.Y.Z.tgz(npm-installable for CLI users).- Validate end-to-end with MCP Inspector — the test harness referenced in Anthropic's connector review criteria:
This builds and launches the server under the Inspector UI. Pre-submission checklist:npm run inspect- Tools tab — confirm all four tools appear with
title,readOnlyHint,destructiveHint,idempotentHint,openWorldHintpopulated, and that descriptions don't read as instructions to Claude. - Happy paths —
remarkable_status(no args),remarkable_list,remarkable_pull(defaults),remarkable_pullwithpage="1-2"andformat="jpeg". - Error paths — call each tool with bad input (
max_width: "huge",format: "gif", missingpassword). Errors should be specific and actionable, never "Internal Server Error". - Static-test path — run
remarkable_statuswith no tablet attached. It should return a clean diagnostic ("USB not reachable; WiFi SSH no host configured"), not a stack trace. Reviewers can run this without hardware. - Notifications pane — set
DEBUG=remarkable-mcpin the server env field; debug lines should appear here, not on stdout.
- Tools tab — confirm all four tools appear with
- Tag and push:
git tag vX.Y.Z && git push --tags - Create a GitHub Release with all artefacts attached:
Both the versioned and unversionedgh release create vX.Y.Z \ remarkable-mcp-X.Y.Z.mcpb \ remarkable-mcp.mcpb \ remarkable-mcp-X.Y.Z.tgz \ validation-X.Y.Z.log \ --title "vX.Y.Z" --notes-file CHANGELOG.md.mcpbmust be attached so the README's "download latest" link keeps working. npm publish(when ready for the npm registry).- Submit the
.mcpbto Anthropic's curated directory via https://forms.gle/tyiAZvch1kDADKoP9.
Reviewer test plan
Anthropic's review process expects test credentials, but this server talks to physical hardware that reviewers won't have on hand. Provide the following with the submission:
- Repro video demonstrating each of the four tools against a real tablet (setup → status → list → pull). 60–90 seconds is plenty.
- Spec links: the firmware ≥ 3.9 requirement (USB HTTP path), the WiFi SSH fallback, and the on-tablet password location (Settings → Help → Copyright and licenses).
- Static-test path: reviewers can install the
.mcpband runremarkable_statuswith no tablet connected; it returns a clean diagnostic ("USB HTTP not reachable; WiFi SSH no host configured") instead of crashing or returning a generic error. This exercises the schema, the manifest, and the error-message standard without hardware. - Source code is public on GitHub (MIT) — reviewers can audit the SSH/USB code paths directly.
Privacy
remarkable-mcp is a local-only MCP server. No telemetry, no remote services operated by the maintainer; the only network traffic the server originates is to your reMarkable (USB or LAN) and, if you've configured it, your local Ollama instance. See PRIVACY.md for the full data-flow audit.
Security
Vulnerability reports → open an issue at https://github.com/Scratchydisk/remarkable-mcp/issues (use a private security advisory if the issue is sensitive). Setup-time SSH host-key pinning means the WiFi path can't be MITM'd after first USB pairing; see the README's security note for the full threat model.
Built on
- remarkable-rm — parses reMarkable v6
.rmbinary files and renders them to SVG / PNG / JPEG. Maintained by the same author; rendering bugs (mis-strokes, missing colours, wrong line widths) belong on that repo. - MiniSearch — the pure-JS BM25 index powering
remarkable_search. - ssh2 — the WiFi SSH transport.
- sharp — JPEG encoding for smaller payloads.
- Model Context Protocol SDK — stdio MCP server boilerplate.
Licence
MIT
