WORKBENCH / DISPATCH 001

An EPUB, via email.

why mcp-readwise has fourteen tools, but the one that actually mattered ships markdown to your Reader Library by SMTP — and what that detour says about API design.

  • python
  • fastmcp
  • pandoc
  • aiosmtplib
  • resend
  • docker
STATUS · SHIPPED

I keep most of what I read in Readwise. Highlights from books, articles I send through Reader, the occasional PDF dropped from Kindle. The reading lives there; the highlights surface elsewhere via API. That has been the shape for years.

What changed in 2026 is the source of the long markdown showing up in my life. Deep research from Claude. Multi-thousand-word briefs from OpenAI’s research mode. Agents that produce 4000–8000 word reports in a single tool turn. These arrive as markdown in a chat window and they should not stay there — they are the shape of a small book, not a paragraph. They want chapter navigation, a TOC sidebar, the option to be downloaded to a Kobo or a Boox or a Kindle, and — most often, in my actual day — to be read in the Readwise iOS and iPad apps, which are quietly the best long-form reading and highlighting clients on those devices. The chat window is where the markdown was generated; it is not where the markdown wants to be read.

The Reader API has save_url and save_html. Neither produces a real EPUB. I checked twice — once against the v3 docs, once against Readwise’s own official CLI to confirm I wasn’t missing a hidden endpoint. The CLI doesn’t expose file upload either. The API surface for “drop a file into my library” does not exist.

The detour is email.

What every Reader account quietly has.

Every Reader account ships with a <custom>@library.readwise.io address. Send it an EPUB attachment, the ingest pipeline picks it up, the document appears in your Library a minute or two later with proper chapter nav and downloadable formats. That mechanism has been there the whole time; it’s just not part of the API.

So mcp-readwise automates that path. The whole detour is six lines of pipeline:

// the EPUB path inside mcp_readwise/tools/epub_sender.py
markdown blob → pandoc renders EPUB 3 (CDIT brand styling)
             → aiosmtplib delivers via Resend SMTP
             → email lands at <custom>@library.readwise.io
             → Readwise ingest pipeline picks it up (15 min)
             → real EPUB appears in Library

What the LLM sees from the outside is a single tool call:

result = save_markdown_as_epub(
    markdown="""---
    title: Q2 Planning Brief
    author: Casey
    tags: [planning, brief]
    note: Context for the team — read this before Thursday's call.
    ---
    # Background
    ...
    """,
    idempotency_key="brief-2026-q2-v1",
)
# → EpubSendResult(title=..., accepted_at=..., recipient=..., ...)

# 1–2 minutes later:
verify = verify_epub_received(title=result.title, since=result.accepted_at)
# → VerifyResult(found=True, document=ReaderDocument(...), ...)

The tool returns immediately after SMTP delivery — the ingest is async by nature, no amount of polite waiting at the API edge would change that. So the surface ships two tools instead of one: save_markdown_as_epub accepts the markdown and posts the email, verify_epub_received polls Reader and returns time-aware retry guidance. The docstring on the first tool leads loudly with the async contract so an LLM caller does not tell the human “done” until the verifier confirms.

The other twelve tools.

This started, before the EPUB detour, as a normal Readwise MCP. Engagement-aware reads sit on top of a per-source score that joins Readwise v2 books with Reader v3 documents — books and articles, finished and saved, recent and legacy, all on one comparable axis. Two read tools collapse what used to be a 7-tool surface into intent-shaped calls:

  • reading_status — single-call snapshot: recent activity, evergreen top, current attention, junk drawer, signal density. Accepts a window_days and a week_offset so an agent can ask about last week or the week of the call I missed.
  • writing_material — bundle highlights for drafting. Source-first (book_id / document_id / title_search) or topic-first (topic). Filters by min_engagement so the draft is built from the strong signal, not the noise.

Then standard write tools — save_url, save_markdown (the HTML-with-epub-UI-hint path, lighter than a real EPUB), update_progress, the highlight CRUD trio. And tag management — list_tags, create_tag / delete_tag, tag_highlight.

Fourteen tools. The EPUB detour is two of them. The other twelve do the boring well.

Why a brand stylesheet is a first-class file.

Pandoc renders the EPUB with a hand-tuned CSS at mcp_readwise/assets/epub/cdit-style.css. The palette inherits from cdit-works.de — Carbon body, Cloud Dancer page background, Strong Blue links, Mint blockquote rail. Inter weights 400 / 700 / 800 ship as static woff2 subsets inside the EPUB, so the document keeps its typography offline on Kobo or Boox.

One deliberate divergence from the website: chapter headings inside the EPUB use Inter at weight 800 with -0.02em tracking, not the website’s League Gothic. Condensed display fonts are great for a press masthead and fatiguing across a hundred chapter breaks in a long-form read. Different physical scene, different type.

The stylesheet is editable. Fork it, rebuild the Docker image, ship your own brand. That is the point — it is not generated from Python, it is a first-class asset that a human can read and change.

What the tool-reviewer pass surfaced.

Before v0.6.0 shipped, I ran the surface past an mcp-tool-reviewer agent. It pointed at three real things:

  • The original tool returned a generic dict[str, Any] — agents could not predict the shape, and the LLM kept making follow-up calls to figure out what it just got. Tightening to EpubSendResult (a Pydantic model with a documented identifier_scheme) collapsed those follow-ups.
  • The send tool’s docstring buried the “this is async, ingest takes 1–5 minutes” line in the middle. Most LLMs read the first paragraph and stopped. The fix was structural: lead with the async contract, then list the parameters.
  • The verifier returned a bool initially. An LLM caller could not distinguish “not found yet, retry in 60s” from “not found, give up.” VerifyResult now ships a note field with time-aware guidance — retry in 60s if less than two minutes have passed since accepted_at, give up after five minutes — and the agent behaves correctly without reading the parent docstring at all.

Tool-shaped feedback on a tool surface. Cheaper than every alternative.

What this whole detour is actually about.

The interesting thing is not the EPUB. The EPUB is a transporter.

What this server is solving, underneath the API surface, is the gap between where long markdown gets generated in 2026 and where long markdown gets read. Generation has moved into chat windows and agent loops — Claude, OpenAI, anything else producing 4–8k word reports as a single tool turn. Reading has not moved with it. Reading still wants Kindle, Kobo, Boox, Reader. A bridge that takes one markdown blob from an agent’s hands and lands it in a Reader Library, with a proper TOC and chapter nav and offline download, is a small thing that quietly removes a daily friction.

The email mechanism reveals something useful too: Readwise has an ingest pipeline that accepts a file format, parses it correctly, indexes it for highlight extraction, and routes it into the same place where every other reading source lives. That pipeline is one HTTP endpoint away from being a proper API. It is not exposed because the email path has been good enough, and the email path is honest — it is a real protocol, with real authentication via your library address, well-understood by every SMTP client on earth. “We have the capability, but the interface to it is email” is sometimes the correct architectural choice, not a hack to be wished away.

The 14 tools live at github.com/CaseyRo/mcp-readwise. Engagement scoring formula in engagement.py. Email pipeline in tools/epub_sender.py plus smtp_client.py. The Docker image bakes in pandoc, the brand stylesheet, and the Inter subsets — so a single docker compose up is the entire deploy path.

If you generate long markdown in a chat window and you read long markdown in the Readwise apps on iOS or iPad — where highlighting, note-taking, and revisiting are first-class affordances rather than afterthoughts — the bridge between those two halves of your day is now one tool call.

BUILT.