Skip to main content
Glama
calibre-web-mcp

calibre-web-mcp

by daedalus-sh

calibre-web-mcp

An MCP server that gives an AI assistant your Calibre-Web library, built for Calibre-Web-Automated (CWA): find books and look them up, see what is new, mark books read or unread, keep shelves, and send a book to your e-reader.

It logs in to Calibre-Web as one user and uses the same routes the web UI uses, so that user's permissions apply unchanged. It speaks MCP over streamable HTTP behind a bearer token and keeps nothing on disk. No tool deletes a book, edits metadata, or touches users or settings.

Tools

Read: these change nothing.

Tool

search_books

Search, or list the whole library; sorted by date added (newest first), title, author or series; paged.

recent_books

The books added most recently.

get_book

Title, authors, series, tags, languages, publisher, dates, rating, formats with sizes, read and archived state, shelves, what can be sent to the e-reader, description.

get_send_status

This user's e-mail and conversion tasks: waiting, started, finished, failed.

list_shelves

The shelves the user can see, with book counts, and the magic shelves (rule-based; no tool changes them).

get_shelf

The books on a shelf, a page at a time.

Write: what each one changes, and how it is undone.

Tool

Effect

Undo

set_read_status

Sets this user's read flag on a book. Idempotent.

Call it again with the other value.

create_shelf

Creates an empty shelf, private, or public if the user may. Returns the existing shelf if there is one with that name and visibility.

Delete the shelf in the web UI; no tool deletes one.

add_to_shelf

Puts a book at the end of a shelf. Idempotent.

remove_from_shelf.

remove_from_shelf

Takes a book off a shelf; the book itself is untouched. Idempotent.

add_to_shelf, which puts it back at the end of the shelf's order.

send_to_ereader

E-mails the book file now to the e-reader address in the user's Calibre-Web profile.

None: a sent e-mail cannot be recalled.

Each tool's description opens with the same: a read says it is read-only, and a write says what it changes and whether it can be undone. The annotations match:

readOnlyHint

destructiveHint

idempotentHint

openWorldHint

the six reads

true

false

true

false

set_read_status, create_shelf, add_to_shelf

false

false

true

false

remove_from_shelf

false

true

true

false

send_to_ereader

false

true

false

true

remove_from_shelf is destructive because it deletes the book's place on the shelf; putting it back puts it at the end. send_to_ereader is the one tool whose effect leaves the library: it cannot be taken back, and each call sends another e-mail. A client can ask before either.

Results: what is untrusted

Every text written by someone other than the person using the assistant comes back under an untrusted key, next to the ids it belongs to:

  • books: title, authors, series, tags, publisher(s) and the description;

  • shelves: the name. Public shelves are named by other users. The user's own shelves follow the same rule, so a shelf is always shaped the same way;

  • tasks: the kind, description and error (Calibre-Web builds the kind and the description from one message, which carries the book's title);

  • whatever Calibre-Web says back: the answer of its send route, and in errors its flash messages and response bodies (cut to 300 characters).

Ids, counts, flags, dates, ratings, formats, file sizes and language names stay at the top level. The language names are Calibre-Web's own names for ISO language codes. A book in a list looks like this:

{
  "id": 164,
  "series_index": 2,
  "languages": ["English"],
  "read": false,
  "added": "2026-07-21T05:23:52+00:00",
  "untrusted": {"title": "…", "authors": ["…"], "series": "…", "tags": ["…"]}
}

An error is {"error": <kind>, "message": ...}. The message is this server's own text, and anything Calibre-Web said is under the error's untrusted.

search_books uses Calibre-Web's own search, which matches the query as one piece of text inside a title, tag, series or publisher, or as words that all appear in the authors' names. It does not combine fields: "dune herbert" finds nothing, while "dune" or "frank herbert" do. The tool description says so, so the model searches for one thing at a time.

Related MCP server: calibre-mcp

Running it

Requirements: a Calibre-Web or CWA instance, and Python 3.12+ with the dependencies in pyproject.toml, or Nix:

nix run .            # or: nix build, then ./result/bin/calibre-web-mcp

The server listens on MCP_LISTEN (default 0.0.0.0:8080), with MCP at /mcp and GET /healthz. The health check needs no auth and returns no library data. It answers ok only when Calibre-Web answers an authenticated request, and 503 unavailable: <kind> otherwise. Every other request needs Authorization: Bearer $MCP_BEARER_TOKEN.

Configuration

Variable

MCP_BEARER_TOKEN

Required, 16+ characters: the token MCP clients present.

MCP_LISTEN

host:port, default 0.0.0.0:8080.

CALIBRE_WEB_URL

Required: where this server reaches Calibre-Web, e.g. http://calibre-web:8083. A path prefix is fine (https://example.org/calibre).

CALIBRE_WEB_AUTH_MODE

Required: header or form (below).

CALIBRE_WEB_USER

Required: the Calibre-Web user this server acts as.

CALIBRE_WEB_AUTH_HEADER

Header mode only: the header name, default Remote-User.

CALIBRE_WEB_PASSWORD or CALIBRE_WEB_PASSWORD_FILE

Form mode only: the user's password, or a file holding it.

The server refuses to start, with a message naming the variable, when a required setting is missing or when the settings mix the two modes.

Logging in: two modes

Header mode (CALIBRE_WEB_AUTH_MODE=header) uses Calibre-Web's reverse proxy authentication: Admin › Edit Basic Configuration › Feature Configuration › "Allow Reverse Proxy Authentication", with "Reverse Proxy Header Name" set to the same value as CALIBRE_WEB_AUTH_HEADER. Every request carries the header with CALIBRE_WEB_USER as its value, and Calibre-Web treats each request as that user. With "Auto-create users from reverse proxy" on, a name Calibre-Web does not know becomes a new account.

Security: in header mode the header is the credential. Anything that can reach Calibre-Web and set that header is logged in as whichever user it names. Run this server where it reaches Calibre-Web on a network nothing else can reach, such as a private container network shared only by the two. If Calibre-Web is also published through a reverse proxy, that proxy must remove the header from incoming requests.

Form mode (CALIBRE_WEB_AUTH_MODE=form) logs in through Calibre-Web's login form once and then uses its session cookie (with "remember me"), logging in again when the session ends. Calibre-Web rate-limits logins per username (3 a minute, 40 a day), and reaching that limit locks the person out of the web UI as well. So after a refused login, or one that got no answer, this server waits a minute before it tries again, then two minutes, doubling up to an hour, and reports auth_failed in the meantime. Each successful login is a remembered session in Calibre-Web, which keeps it for 31 days.

Behind an MCP gateway

The endpoint is stateless and answers in JSON, so each POST to /mcp is complete on its own. That suits gateways that open a session per call, such as LiteLLM's MCP gateway. Register it as a streamable-HTTP server at http://<host>:8080/mcp with the header Authorization: Bearer <token>, and give the gateway's own keys access to it as you would to any other tool. Point the gateway's health check, or the container's, at /healthz.

Safety model

  • The library's text is untrusted. Titles, descriptions, tags, series, publishers and shelf names often come from internet metadata sources or other users. They come back under an untrusted key (see Results), and the server's instructions name that key and tell the model to read what is under it as data and never to follow it.

  • Nothing from a page goes into a URL unchecked. Every request carries this server's login, and in header mode that is a header Calibre-Web trusts outright. So a book's uuid from its page must be a UUID before /ajax/book/<uuid> is asked for, and a send format must be letters and digits (with a convert value of 0, 1 or 2) before it goes into /send/<id>/<format>/<convert>; anything else is dropped or refused. Each value goes in as one path segment with every reserved character escaped, / included. Book and shelf ids are integers.

  • Sending is guarded. send_to_ereader uses the route behind the book page's own send button, which mails the user's configured address and nothing else. It offers only the formats that page offers, and refuses while a send of the same book is still waiting or running, so a retried call does not mail the book twice. If the user has no e-reader address, it says so (no_ereader_address) without trying.

  • Writes are idempotent where Calibre-Web's are not. Calibre-Web only toggles read status, so the tool reads the state first, toggles only when it differs, and reads it again to confirm. Adding a book that is already on a shelf, or removing one that is not, reports changed: false.

  • A small, fixed surface. No route that deletes, edits metadata, changes users or settings, or uploads is ever called.

  • No state, no background work. Nothing goes to disk. The only state in memory is the cookie jar, the CSRF token and the login backoff.

Calibre-Web behaviour it is built around

These come from reading CWA v4.0.8's source (cps/web.py, shelf.py, helper.py, opds.py, tasks_status.py, the templates) and checking against a running instance:

  • CSRF. Every POST needs a Flask-WTF token, and the token is valid only with the session cookie it was issued with. The server takes the token from the hidden csrf_token input that every page carries, sends it the way the web UI does (form field plus X-CSRFToken), and when Calibre-Web answers its generic 400 page it fetches a fresh token and retries once. That applies in header mode too: the header logs the user in, but POSTs still need the cookie and the token.

  • Secure cookies over plain HTTP. With "use HTTPS" or OAuth configured, Calibre-Web marks its session cookie Secure. A browser-style cookie jar would never send that cookie to an internal http:// URL, which would break every POST. The server keeps its cookies itself.

  • Read status only toggles, for any id. POST /ajax/toggleread/<id> flips the state, and for an id with no book behind it, it records that non-existent book as read. The tool checks that the book exists (through its page) before it ever toggles.

  • Book lists. /ajax/listbooks is the JSON behind the book table. It joins authors with & and tags with ,, and leaves datetime columns empty (atom_timestamp holds the date added). Without a search, it converts the offset to a page number through a float, so some offsets are rounded away (offset 1 with limit 3 returns from 0). The server asks for a page size whose round trip lands exactly on the offset.

  • Book details. No JSON endpoint has the per-user state: read, archived, shelves and send options. Those come from the book page, using element ids, classes and data-* attributes rather than translated text, so the parsing works in any UI language. Exact sizes and dates come from /ajax/book/<uuid>. That endpoint takes HTTP Basic auth rather than the session, and it ignores the user's tag restrictions, so tags come from the page. In v4.0.8 the book page's XHR fragment (X-Requested-With) fails with a 500, so the server fetches the full page.

  • Sending. POST /send/<id>/<format>/<convert> queues an e-mail to every address in the user's "Send to eReader Email Address" profile setting. convert 1 or 2 first converts MOBI or AZW3 to the format. Sending needs the download permission. The book page lists only formats under the configured attachment size limit, and the route answers [{type, message}] with a translated message. The result is a queued task. Its progress shows in /ajax/emailstat, which keeps tasks in memory (a restart clears them) and shows an admin everyone's tasks; the server keeps only its own user's.

  • Shelves. Add and remove, sent as XHR, answer with plain status codes: 204 for done, 400 for already on the shelf or an invalid shelf or book, 410 for already removed, and 403 for not allowed. The 400 bodies are fixed English strings. Adding gives the book the shelf's highest order plus one, and removing deletes its entry, so a book taken off and put back lands at the end. Creating a shelf redirects to the new shelf on success. When creation is refused, the reason is a flash message. Viewing a shelf (/shelf/<id>/stored/<page>) applies the sort order the user last chose for shelves in the web UI, just as the web UI does, and saves nothing. Magic shelves are rule-based, so they are listed but never changed.

Plain Calibre-Web shares these routes, but not all of the page markup the server reads (CWA's book page, the profile flag for a configured e-reader). It is built and tested against CWA v4.0.8.

Tests

nix flake check -L      # lint (ruff, nixfmt), then the package build, which runs the suite

The suite runs in the package's checkPhase:

  • configuration: each mode, and each way of mixing or leaving out settings;

  • the parsers, on pages and JSON rebuilt from CWA v4.0.8's templates;

  • the HTTP session against a mocked Calibre-Web (respx): the header on every request, the form login and its backoff, CSRF refresh, Secure cookies, timeouts and failures;

  • every tool against that mock, checked for the routes it calls and how it reads their answers. That includes send_to_ereader, which is only ever tested against the mock;

  • the URLs built from page values: uuids and send formats that are not plain are never requested;

  • the untrusted split: a marker planted in every outside-written field of every tool's answer, and in Calibre-Web's error text, shows up only under an untrusted key;

  • the MCP layer through the SDK's own client: tool listing, annotations and descriptions, bearer auth, health, typed errors, and one log line per call with no secrets in the log.

Without Nix: pip install -e '.[test]', then pytest.

License

MIT.

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    D
    maintenance
    Enables searching, reading, and managing a Calibre ebook library through natural language, with features like metadata search, full-text search, content extraction, and library management.
    349 npm
    Apache 2.0
  • F
    license
    A
    quality
    D
    maintenance
    Enables AI assistants to interact with BookLore self-hosted libraries, allowing natural language queries to search books, manage reading status, ratings, series, authors, and highlights.
    7
    1
    -
  • A
    license
    Not graded
    quality
    D
    maintenance
    Enables LLMs to interact with a local Calibre Content Server to search libraries, list books, retrieve ebook content, chapters, and more.
    6 npm
    MIT