shelfmark-mcp
OfficialREADME.md
# shelfmark-mcp
An [MCP](https://modelcontextprotocol.io) server for
[Shelfmark](https://github.com/calibrain/shelfmark), the self-hosted book and
audiobook downloader. It lets an AI assistant do what a person does in
Shelfmark's web UI: search for a book, look at the releases its sources offer,
pick one, queue the download and follow it until it lands in the library. It
can also work through Shelfmark's request queue, which its Hardcover "Want to
Read" sync fills.
It speaks MCP over streamable HTTP behind a bearer token, talks to Shelfmark's
REST API, and keeps nothing on disk. Built against Shelfmark 1.6.1; 1.5.x has
the same API.
## Tools
The usual flow is `search_books` → `list_releases` → `download_release` →
`download_status`.
Finding a book
| Tool | |
|---|---|
| `shelfmark_status` | Health, version, the enabled release sources and metadata provider, default content type and languages, accepted formats, the release search budget, and whether the request tools work. |
| `search_books` | Search the metadata provider (Hardcover, Open Library, ...) by free text or by author, title or series. Each book has a `book_id`, year, series position, whether the library already holds it, and the provider's ratings and reader counts. |
| `get_book` | One book's full metadata: description, ISBNs, publisher, series, provider link. |
| `list_releases` | Ask the enabled release sources for copies of a book: source, indexer, format, size, language, protocol, seeders and peers, download counts. Filters by format; `expand` broadens the search. |
Downloading
| Tool | |
|---|---|
| `download_release` | Queue a release; returns a `download_id`. |
| `download_status` | The queue now, each download's state and progress; or one download, also found in history. |
| `download_history` | Completed, failed and cancelled downloads, newest first, including ones cleared from the activity list. |
| `cancel_download` | Stop a queued or running download. |
| `retry_download` | Queue a failed or cancelled download again. |
Requests (Shelfmark with user accounts only)
| Tool | |
|---|---|
| `list_requests` | Books people asked for, with who asked, the content type and a `book_id` to list releases for. |
| `fulfil_request` | Approve one with a chosen release, the requester's own pick, or as already in the library. |
| `reject_request` | Decline one, with a note. |
Annotations are honest: the reads are `readOnlyHint`; `download_release`,
`retry_download` and `fulfil_request` start downloads from the internet
(`openWorldHint`); `cancel_download` and `reject_request` are
`destructiveHint`, so a client can ask before them.
## Running it
Requirements: a reachable Shelfmark, and Python 3.12+ with the dependencies in
`pyproject.toml`, or Nix:
```sh
nix run . # from a checkout; or: nix build
```
| Variable | |
|---|---|
| `MCP_BEARER_TOKEN` | Required, 16+ characters, no whitespace: the token clients present as `Authorization: Bearer …`. The server refuses to start without it. |
| `SHELFMARK_URL` | Required: Shelfmark's address, e.g. `http://shelfmark:8084`, with its base path if it has one (`https://example.org/shelfmark`). |
| `SHELFMARK_API_KEY` | Optional: Shelfmark's `SHELFMARK_API_KEY`, sent as `X-Api-Key`. Needed when Shelfmark has user accounts (any `AUTH_METHOD` but `none`); with it, this server acts as Shelfmark's first admin. |
| `MCP_LISTEN` | Optional, default `0.0.0.0:8080`. |
MCP is served at `/mcp`. `GET /healthz` needs no token and returns no library
data: `ok` (200) only when Shelfmark's `/api/health` answers `ok`, otherwise
503 with a reason (`unavailable: shelfmark_unavailable`, `…_coordinator_stopped`,
`…_http_<status>`).
Logs go to stderr, one line per tool call with its name, outcome, duration and
how many connection attempts had to be repeated. They never carry a token, a
key or an upstream body.
### Behind an MCP gateway
The server is stateless (every POST to `/mcp` is one JSON exchange), so a
gateway can open a session per call. For [LiteLLM](https://docs.litellm.ai):
```yaml
mcp_servers:
shelfmark:
url: http://shelfmark-mcp:8080/mcp
transport: http
auth_type: bearer_token
auth_value: <MCP_BEARER_TOKEN>
description: Search, download and follow books in Shelfmark
```
`list_releases` can take as long as Shelfmark's release search budget (its
`RELEASE_SEARCH_TIMEOUT`, 300 s by default, shown by `shelfmark_status`): give
the gateway's and the client's tool-call timeouts at least that much, or lower
the budget in Shelfmark.
## How it behaves
- **Text from outside is untrusted.** Titles, authors, descriptions, release
names, notes and status messages come back under an `untrusted` key, and the
server's instructions tell the model to read them, never follow them.
- **Opaque ids.** `book_id` is Shelfmark's own `provider:id`. Shelfmark's
download task ids can be a kilobyte long (a torrent source's id embeds its
magnet link), so `download_id` is a short digest the server resolves against
Shelfmark's queue and history. A `release_id` names a release from the last
listing: queueing one takes the whole release as Shelfmark listed it, so
listings are kept in memory for 12 hours; after that, or a restart, the id is
`stale_id` and the releases have to be listed again.
- **Downloads are queued the way the web UI queues them**: title, authors,
year, cover and series from the book's metadata, everything else from the
release as listed.
- **Connections are retried, requests are not.** A new connection to a port
published from a rootless container's network namespace sometimes stalls on
the SYN and fails only after ~10 s. Each request connects with escalating
timeouts (0.4, 0.8, 1.5, 2.5 s) and retries only a failed connection, where
nothing was sent, a POST included. A request that was sent is never repeated:
no answer is `no_answer` (for a write, `outcome: unknown`: check
`download_status` before repeating it), and an HTTP error is Shelfmark
answering, returned as a typed error.
- **Typed errors.** `{"error": kind, "message": …}`, with Shelfmark's status
and code where it gave one: `shelfmark_unavailable`, `no_answer`,
`auth_failed`, `forbidden`, `refused_by_policy`, `requests_unavailable`,
`source_unavailable`, `not_found`, `conflict`, `already_queued`,
`invalid_argument`, `invalid_id`, `stale_id`, `shelfmark_error`.
## Shelfmark's behaviour it is built around
From reading Shelfmark 1.6.1 (and 1.5.1); the tests use answers shaped like a
real instance's:
- `GET /api/releases` is synchronous and searches each enabled source in turn
under one wall-clock budget, so it is slow; when every source fails it
answers 503 with the first source's error.
- `POST /api/releases/download` wants the release as listed plus the book's
metadata, and answers a duplicate with HTTP 500 "Release is already in the
download queue". The task id is the release's `source_id`.
- `GET /api/status` holds the live queue; finished downloads also live in the
activity snapshot and, once cleared in the UI, in `/api/activity/history`.
JSON keys come back sorted, so order comes from timestamps.
- A release's `content_type` is the source's label (for example
`📕 book (fiction)`), not `ebook` or `audiobook`; the content type searched
for is what the download is queued as.
- The request endpoints answer 403 `requests_unavailable` when Shelfmark runs
with `AUTH_METHOD=none`, and an API key is ignored in that mode. With user
accounts, `SHELFMARK_API_KEY` makes each request an admin's.
- The Hardcover sync and its auto-download can run even in `AUTH_METHOD=none`:
the requests stay out of reach, but their downloads appear in the queue with
a `request_id`, and with the source's label (`📘 book (non-fiction)`) as
their content type, which is reported here as `ebook` or `audiobook`.
- 1.6.1's `/api/health` adds `coordinator` and answers 503 when the download
coordinator has stopped; 1.5.x always answers `{"status": "ok"}`.
## Development
```sh
nix flake check -L # the test suite (in the package build), ruff and nixfmt
nix fmt # format the nix files; ruff format for the Python
```
## License
MIT
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessSyncing