calibre-mcp
by monch1962
README.md
# Calibre MCP
[](https://github.com/monch1962/calibre-mcp/actions/workflows/ci.yml)
[](https://www.python.org/)
[](https://modelcontextprotocol.io/)
[](https://calibre-ebook.com/)
[](LICENSE)
A read-only [Model Context Protocol](https://modelcontextprotocol.io/) server for an existing [Calibre](https://calibre-ebook.com/) ebook library.
Calibre MCP lets MCP-compatible clients search book metadata, query Calibre's full-text index, inspect book details, browse library categories, and discover related books. It uses Calibre's supported `calibredb` command-line interface rather than reading `metadata.db` directly.
## Features
- Metadata search using Calibre's search language
- Full-text search with matching snippets
- Detailed metadata for individual books
- Recently added books
- Authors, tags, series, publishers, and language categories
- Related-book discovery
- MCP resources for books, searches, and library status
- Optional Calibre Content Server links
- In-memory TTL cache
- Streamable HTTP transport
- Podman Quadlet deployment
- No metadata-changing MCP tools
## Available tools
| Tool | Purpose |
|---|---|
| `server_info` | Show server, Calibre, cache, and library configuration |
| `library_status` | Show book count and full-text indexing status |
| `search_books` | Search Calibre metadata |
| `search_fulltext` | Search inside indexed ebooks and return snippets |
| `get_book_metadata` | Return all available metadata for one book |
| `list_recent_books` | List books most recently added |
| `list_categories` | Browse authors, tags, series, publishers, and languages |
| `find_related_books` | Find books with overlapping authors, series, or tags |
| `clear_cache` | Clear the in-memory read cache |
## MCP resources
| URI | Purpose |
|---|---|
| `calibre://library/status` | Library and full-text index status |
| `calibre://book/{book_id}` | Detailed metadata for a book |
| `calibre://search/{query}` | Metadata search results |
## Requirements
- A Calibre library with `metadata.db`
- Calibre 9.x
- Python 3.11 or newer
- An MCP client supporting Streamable HTTP
- Podman and systemd for the included Quadlet deployment
The full-text tools require Calibre's full-text index to be enabled and completed.
## Quick start with Podman Quadlet
### 1. Clone the repository
```bash
git clone https://github.com/monch1962/calibre-mcp.git
cd calibre-mcp
```
### 2. Confirm your Calibre library
The supplied Quadlet assumes:
```text
/tank/media/Books
```
Confirm that the library database exists:
```bash
test -f /tank/media/Books/metadata.db && echo "Calibre library found"
```
### 3. Determine the library owner
```bash
stat -c 'uid=%u gid=%g owner=%U:%G' /tank/media/Books
```
Edit `quadlet/calibre-mcp.container` and set `User=` to the returned numeric UID and GID:
```ini
User=1000:1000
```
Also change the host library path if yours is different:
```ini
Volume=/tank/media/Books:/books
```
### 4. Build the image
```bash
sudo podman build \
--build-arg CALIBRE_VERSION=9.11.0 \
-t localhost/calibre-mcp:1.0.0 .
```
### 5. Install the Quadlet
```bash
sudo mkdir -p /etc/containers/systemd
sudo cp quadlet/calibre-mcp.container \
/etc/containers/systemd/calibre-mcp.container
sudo systemctl daemon-reload
sudo systemctl start calibre-mcp.service
```
Do not run `systemctl enable calibre-mcp.service`. The generated service is transient; the Quadlet's `[Install]` section creates the boot dependency.
### 6. Verify the deployment
```bash
sudo systemctl status calibre-mcp.service --no-pager
sudo journalctl -u calibre-mcp.service -n 100 --no-pager
sudo podman ps --filter name=calibre-mcp
```
Verify Calibre inside the container:
```bash
sudo podman exec calibre-mcp \
calibredb list \
--with-library /books \
--for-machine \
--fields title \
--limit 1
sudo podman exec calibre-mcp \
calibredb fts_index status \
--with-library /books
```
The default endpoint is:
```text
http://localhost:8008/mcp
```
## Test with MCP Inspector
```bash
npx @modelcontextprotocol/inspector
```
Select **Streamable HTTP** and connect to:
```text
http://YOUR_SERVER:8008/mcp
```
Example metadata search:
```json
{
"query": "author:asimov",
"limit": 10
}
```
Example full-text search:
```json
{
"query": "zero trust architecture",
"limit": 10
}
```
Example restricted full-text search:
```json
{
"query": "encryption",
"limit": 10,
"restrict_to": "search:tags:security"
}
```
## Connect an MCP client
Use the Streamable HTTP endpoint exposed by the server:
```text
http://YOUR_SERVER:8008/mcp
```
Client configuration formats vary. Consult your client's MCP documentation and select Streamable HTTP rather than stdio or legacy SSE.
## Calibre search examples
`search_books` accepts Calibre search expressions:
```text
author:asimov
title:"i robot"
tags:history
series:"Discworld"
publisher:penguin
languages:eng
rating:>=4
```
An empty query returns all books, subject to the result limit.
## Optional Content Server links
Set the URL of your existing Calibre Content Server in the Quadlet:
```ini
Environment=CALIBRE_CONTENT_SERVER_URL=http://mini-nas:8083
```
When configured, metadata results include browser and format-download links.
## Configuration
| Environment variable | Default | Description |
|---|---:|---|
| `CALIBRE_LIBRARY_PATH` | `/books` | Calibre library inside the container |
| `CALIBREDB` | `calibredb` | Path to the Calibre CLI |
| `CALIBRE_COMMAND_TIMEOUT` | `120` | Command timeout in seconds |
| `CALIBRE_MAX_RESULTS` | `100` | Maximum results returned by a tool |
| `CALIBRE_CACHE_TTL` | `300` | Cache lifetime in seconds; set to `0` to disable |
| `CALIBRE_CACHE_SIZE` | `256` | Maximum cached entries |
| `CALIBRE_MAX_CONCURRENT_COMMANDS` | `4` | Maximum concurrent `calibredb` subprocesses |
| `CALIBRE_CONTENT_SERVER_URL` | unset | Optional Content Server base URL |
| `MCP_HOST` | `0.0.0.0` | MCP HTTP bind address |
| `MCP_PORT` | `8000` | MCP port inside the container |
| `HOME` | `/tmp/calibre-home` | Writable location for Calibre configuration |
## Why the library mount is writable
Calibre checks whether the library filesystem is case-sensitive by briefly creating and deleting a probe file in the library root. Consequently, the bind mount cannot be mounted read-only.
This server remains functionally read-only because it exposes no tools that call Calibre commands such as:
- `add`
- `remove`
- `set_metadata`
- `add_format`
- `remove_format`
Run the container as the same unprivileged UID and GID that owns the library. Do not run it as root unless your environment specifically requires that.
## Security
- Keep port `8008` restricted to trusted LAN or Tailscale clients.
- Do not expose the endpoint directly to the public Internet.
- Streamable HTTP does not add authentication in this deployment.
- Place an authenticated reverse proxy in front of the service before broader exposure.
- Pin release versions rather than using a moving container tag.
- Review [SECURITY.md](SECURITY.md) before reporting a vulnerability.
### Red-team hardening (round 1)
Ten adversarial attack vectors were proven with failing tests and then fixed.
Each `TestAttack_*` test in `tests/attack_round1_test.py` is a permanent
regression fixture for its vector.
| # | Attack vector | Entry point | Defence |
|---|---------------|-------------|---------|
| 1 | Unbounded cache key — a multi-megabyte query is retained in memory per cache entry | `search_books` / `search_fulltext` | Keys over 512 bytes are SHA-256 hashed (`_cache_key`) |
| 2 | Unbounded cache value — large `calibredb` output (comments, snippets) retained per entry | `_run` | Values over 1 MiB bypass the cache (`_cache_put`) |
| 3 | `server_info` subprocess hang — `calibredb --version` ran without a timeout | `server_info` | Timeout applied; `TimeoutExpired` → `ToolError` |
| 4 | Unhandled `JSONDecodeError` on invalid `calibredb` output → raw internal error | `_list_books` / `search_fulltext` | `_loads_json` wrapper → `ToolError` |
| 5 | Unhandled `ValueError` on a non-numeric book-id key → raw internal error | `_normalise_books` | Wrapped → `ToolError` |
| 6 | Search-syntax injection via library metadata — quotes/backslashes in authors, series, or tags break out of the generated query | `find_related_books` | `_exact_match_clause` strips `"` and `\` from clause values |
| 7 | Unbounded query length — MB-scale queries reach `calibredb` and the cache | `search_books` / `search_fulltext` | Queries over 8192 chars rejected with `ToolError` |
| 8 | Unbounded transient `calibredb` stdout capture under concurrent floods | `_run` | Residual risk — bounded by `CALIBRE_COMMAND_TIMEOUT`; documented |
| 9 | Unauthenticated endpoint on `0.0.0.0` | deployment | Accepted posture — documented in [SECURITY.md](SECURITY.md) |
| 10 | Information disclosure — library path, Calibre version | `server_info` / `library_status` | Accepted for a read-only knowledge server; documented |
Known-safe surfaces verified in this round: shell injection (list argv, no
`shell=True`), option-value injection (`--sort-by`/`--categories`/`--restrict-to`
reject leading-dash values in Calibre's parser), resource URI path traversal
(non-numeric ids rejected), result-limit clamping (`_limit`), and cache race
conditions (lock-guarded).
### Red-team hardening (round 2)
Six input-shape validation vectors proven and fixed; fixtures in
`tests/attack_round2_test.py`.
| # | Attack vector | Entry point | Defence |
|---|---------------|-------------|---------|
| 11 | Unbounded `book_id` magnitude — `id:{huge}` query built internally bypasses the round-1 query cap and reaches `calibredb` as an MB-scale argv entry | `get_book_metadata` / `book_resource` / `find_related_books` | `_validate_book_id` bounds ids to 1..2³¹−1 (`_book`) |
| 12 | Unbounded `categories` string → MB-scale argv | `list_categories` | 1024-char cap → `ToolError` |
| 13 | Unbounded `restrict_to` string → MB-scale argv | `search_fulltext` | 2048-char cap → `ToolError` |
| 14 | Unbounded `sort_by` string → MB-scale argv | `search_books` | 128-char cap → `ToolError` |
| 15 | Non-iterable `formats` metadata → `TypeError` → raw 500 | `_content_links` | Non-list/tuple formats ignored; `details` link still returned |
| 16 | Format-extension injection in generated download links (`..`, `x;rm -rf`) | `_content_links` | Extension whitelist `[a-z0-9]{1,10}` — non-matching formats skipped |
### Red-team hardening (round 3)
Three error-path robustness vectors proven and fixed; fixtures in
`tests/attack_round3_test.py`.
| # | Attack vector | Entry point | Defence |
|---|---------------|-------------|---------|
| 17 | Oversized CSV field (over the 128 KiB csv field-size limit) → raw `csv.Error` → 500 | `list_categories` | Iteration wrapped → `ToolError` |
| 18 | `calibredb` list output as an array of non-dict items → `AttributeError` in `search_books` → 500 | `_normalise_books` | Non-dict array items rejected → `ToolError` |
| 19 | `fts_search` dict payload with an unexpected list-valued key passes through uncapped → response amplification | `search_fulltext` | Every list-valued key is sliced to the result limit |
### Red-team hardening (round 4)
Two concurrency/process-flood vectors proven and fixed; fixtures in
`tests/attack_round4_test.py`.
| # | Attack vector | Entry point | Defence |
|---|---------------|-------------|---------|
| 20 | Concurrent `calibredb` process flood — N parallel tool calls spawn N subprocesses (CPU/memory exhaustion, Calibre DB contention) | `_run` | `threading.Semaphore` caps in-flight commands at `CALIBRE_MAX_CONCURRENT_COMMANDS` (default 4); oversubscribed calls → `ToolError` |
| 21 | `server_info` version subprocess flood — one uncached subprocess per call | `server_info` | Version call routed through the same semaphore (`_run_version`) |
### Red-team hardening (round 5 — terminal verification)
Zero new vulnerabilities. A coverage-gap audit added 11 verification tests
(`tests/attack_round5_test.py`) exercising every entry point not yet covered
by rounds 1–4 — `search_resource`, `book_resource` (non-numeric, traversal-like,
in-range), `status_resource`, `library_status`, `list_recent_books`,
`clear_cache`, `search_fulltext` list payloads, zero/negative limits, TTL-zero
cache disable, and whitespace queries. All passed immediately, confirming the
rounds 1–4 defences hold across the full tool/resource surface.
Two deployment-posture documentation findings were recorded in
[SECURITY.md](SECURITY.md) (no code change): the `Containerfile` has no `USER`
directive (runs as root when built outside the Quadlet, which sets
`User=1000:1000`), and the Quadlet sets `SecurityLabelDisable=true`
(SELinux label separation is off).
## Local development
Create a virtual environment:
```bash
python3 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install -e .
python -m pip install pytest ruff
```
Run the tests:
```bash
pytest
```
Run lint checks:
```bash
ruff check .
```
Start the server locally:
```bash
export CALIBRE_LIBRARY_PATH="/path/to/Calibre Library"
python server.py
```
## Project status
Version `1.0.0` is suitable for personal and trusted-network deployments. The public API may gain additional tools and resources in future minor releases, while existing tool names and argument shapes will be kept stable where practical.
## Contributing
Issues and pull requests are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md).
## Licence
Released under the [MIT Licence](LICENSE).
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues