Skip to main content
Glama

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

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 (read-only, rule-based).

get_shelf

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

Write

Tool

set_read_status

Mark read or unread. Idempotent.

create_shelf

A private shelf, or a public one if the user may. Returns the existing shelf if there is one with that name.

add_to_shelf, remove_from_shelf

Shelf membership. Idempotent; removing never touches the book.

send_to_ereader

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

Annotations are honest: readOnlyHint on reads, and destructiveHint plus openWorldHint on send_to_ereader, the one tool whose effect leaves the library and cannot be taken back. A client can ask before that one.

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. The server's instructions tell the model to read them as data and never to follow them.

  • 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. Creating a shelf redirects to the new shelf on success. When creation is refused, the reason is a flash message. Viewing a shelf applies the sort order the user last chose for shelves in the web UI, just as the web UI does. 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 MCP layer through the SDK's own client: tool listing and annotations, 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.
    1,175 npm
    Apache 2.0
  • F
    license
    A
    quality
    C
    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.
    18 npm
    MIT