grimmory-mcp
by jensopt
README.md
# grimmory-mcp
A small MCP server for [Grimmory](https://github.com/grimmory-tools/grimmory), the self-hosted ebook, comic and audiobook library. It lets an AI assistant such as Claude Code search your library and tidy metadata, tags, genres and shelves.
Built and tested against **Grimmory v3.5.0**. Grimmory marks its API as unstable, so check it again after you upgrade.
## Tools
| Tool | Kind | What it does |
|---|---|---|
| `search_books` | read | Free-text search plus facet filters (`genre:Romance`, `shelf_status:unshelved`, …), sorting, cursor paging |
| `list_facets` | read | Lists the authors, genres, tags, series, publishers and shelves already in use, so new values match existing ones |
| `get_book` | read | One book's metadata, shelves and locked fields |
| `list_shelves` | read | Regular shelves visible to the user |
| `update_metadata` | write | Changes chosen fields on one book and returns before/after values |
| `bulk_edit_metadata` | write | Tags, genres, moods, authors, series, publisher or language on up to 100 books; shows a preview first, writes only on `confirm=True` |
| `create_shelf` | write | Creates a shelf; refuses duplicate names |
| `assign_shelves` | write | Adds books to shelves or removes them |
## Safety by design
These rules are enforced in code, not in a prompt:
- **No delete tools.** Nothing can delete books, files, shelves, users or metadata values.
- **Locks are respected.** A field locked in Grimmory is refused, never overwritten. No tool can change locks.
- **No accidental wipes.** Grimmory's metadata endpoint defaults to `replaceMode=REPLACE_ALL`, which clears every field you don't send. This server always sends `REPLACE_WHEN_PROVIDED`.
- **Bulk edits are two-step.** The first call returns a per-book preview. Nothing is written until the same call is repeated with `confirm=True`.
- **Nothing is cleared.** Empty values are refused. Remove the last tag from a book in the Grimmory UI.
Use a dedicated Grimmory user for the assistant. The server is the only thing between the assistant and whatever that user is allowed to do.
## Install
Requires Python 3.12+ and [uv](https://docs.astral.sh/uv/).
```bash
git clone https://github.com/jensopt/grimmory-mcp
cd grimmory-mcp
uv sync --frozen
```
### Configuration (environment variables)
| Variable | Required | Meaning |
|---|---|---|
| `GRIMMORY_URL` | yes | Base URL, e.g. `https://books.example.com` |
| `GRIMMORY_USERNAME` | yes | Grimmory **username** (not email; Grimmory logs in by username) |
| `GRIMMORY_PASSWORD` | yes | Its password |
| `PANGOLIN_TOKEN_ID` | no | Only if Grimmory sits behind [Pangolin](https://github.com/fosrl/pangolin), see below |
| `PANGOLIN_TOKEN` | no | Second half of the same token |
### Claude Code
```bash
claude mcp add grimmory -s user \
-e GRIMMORY_URL=https://books.example.com \
-e GRIMMORY_USERNAME=assistant -e GRIMMORY_PASSWORD=... \
-- uv run --quiet --frozen --project /path/to/grimmory-mcp grimmory-mcp
```
Restart Claude Code afterwards, because MCP servers only load at startup.
## Behind Pangolin
Pangolin's **header authentication** won't work for this server. It is HTTP Basic auth in the `Authorization` header, and Grimmory needs that same header for its `Bearer` token. A request can only carry one.
Use a **shareable link** (access token) on the Grimmory resource instead. Pangolin shows it as `?p_token=<id>.<token>`. Put the part before the dot in `PANGOLIN_TOKEN_ID` and the part after it in `PANGOLIN_TOKEN`. The server sends them as `P-Access-Token-Id` / `P-Access-Token` headers, which leaves `Authorization` free for Grimmory.
## Notes
- Access tokens last 2 hours. On v3.5.0, `/api/v1/auth/refresh` returned 401 in testing, so the server logs in again when the token expires.
- Grimmory calls genres `categories` on a single book and `genres` in bulk edits. The tools use the API's names.
- To see the full API, set `API_DOCS_ENABLED=true` on your instance and read `/api/openapi.json`.
- Uses the `mcp` Python SDK 2.x (`MCPServer`, formerly `FastMCP`).
## License
MIT