Metadata-Version: 2.4
Name: mcp-ebook-read
Version: 0.2.4
Summary: MCP server for reading and searching EPUB/PDF documents
License-File: LICENSE
Requires-Python: >=3.13
Requires-Dist: docling>=2.96.1
Requires-Dist: ebooklib>=0.19
Requires-Dist: fastembed>=0.8.0
Requires-Dist: httpx>=0.28.1
Requires-Dist: lxml>=6.1.1
Requires-Dist: mcp>=1.27.2
Requires-Dist: pix2text>=1.1.4
Requires-Dist: pydantic>=2.13.4
Requires-Dist: pymupdf>=1.27.2.3
Requires-Dist: qdrant-client[fastembed]>=1.18.0
Description-Content-Type: text/markdown

# mcp-ebook-read

A local MCP server for Codex to read and retrieve content from EPUB/PDF documents.

## One-Command Docker Setup

### Qdrant (required)

```bash
docker rm -f qdrant 2>/dev/null || true && docker run -d --name qdrant -p 6333:6333 -p 6334:6334 qdrant/qdrant:v1.18.1
```

### GROBID (required by startup preflight and `document_ingest_pdf_paper`)

```bash
docker rm -f grobid 2>/dev/null || true && docker run -d --name grobid --init --ulimit core=0 -p 8070:8070 grobid/grobid:0.9.0-crf
```

## Verify Services

```bash
curl -sS http://localhost:6333/collections
curl -sS http://localhost:8070/api/isalive
```

Expected:
- Qdrant returns JSON with `"status":"ok"`
- GROBID returns `true`

## Run MCP Server (PyPI via `uvx`)

```bash
QDRANT_URL=http://localhost:6333 GROBID_URL=http://localhost:8070 GROBID_TIMEOUT_SECONDS=120 uvx mcp-ebook-read
```

If startup preflight fails, the server exits with a structured error payload on stderr that includes missing env vars and setup hints.

### First Run Recommendation

Before configuring this MCP inside an MCP client, run it once manually from a terminal:

```bash
QDRANT_URL=http://localhost:6333 GROBID_URL=http://localhost:8070 GROBID_TIMEOUT_SECONDS=120 uvx mcp-ebook-read
```

This pre-resolves and aligns runtime dependencies, which helps avoid long first-time activation latency after MCP client configuration.

When you want to refresh `uvx` to the latest published version, run:

```bash
QDRANT_URL=http://localhost:6333 GROBID_URL=http://localhost:8070 GROBID_TIMEOUT_SECONDS=120 uvx mcp-ebook-read@latest
```

If you installed the tool persistently via `uv tool install`, use `uv tool upgrade mcp-ebook-read` instead.

## Environment Variables

Required:
- `QDRANT_URL` (for example `http://127.0.0.1:6333`)
- `GROBID_URL` (for example `http://127.0.0.1:8070`)

Optional:
- `GROBID_TIMEOUT_SECONDS` (default `20`; recommended `120` for large papers)
- `QDRANT_COLLECTION` (default `mcp_ebook_read_chunks`)
- `QDRANT_TIMEOUT_SECONDS` (default `10`)
- `FASTEMBED_MODEL` (FastEmbed model override)
- `FASTEMBED_CACHE_PATH` (FastEmbed cache root override; defaults to `~/Library/Caches/mcp-ebook-read/fastembed` on macOS and `$XDG_CACHE_HOME/mcp-ebook-read/fastembed` or `~/.cache/mcp-ebook-read/fastembed` elsewhere)
- `DOCLING_FORMULA_ENRICHMENT` (`true` by default)
- `PDF_FORMULA_REQUIRE_ENGINE` (`true` by default)
- `PDF_FORMULA_BATCH_SIZE` (`auto` by default; or an explicit integer)
- `PDF_DOCLING_NUM_THREADS` (override Docling CPU threads)
- `PDF_DOCLING_BATCH_SIZE` (override Docling OCR/layout/table batch sizes together)
- `PDF_DOCLING_DEVICE` (override Docling accelerator device, for example `auto` or `cpu`)
- `PDF_DOCLING_TUNING_PROFILE_PATH` (override the local autotune profile JSON path)

## Persistence Model

- Persistence is sidecar-based and auto-routed by document location.
- For each document, MCP writes state to `<document_dir>/.mcp-ebook-read/`.
- Sidecar contains:
  - `catalog.db`
  - `docs/<doc_id>/reading/reading.md`
  - `docs/<doc_id>/assets/...`
  - `docs/<doc_id>/evidence/...`

## Notes
- Use `library_scan` to discover `.pdf`/`.epub` files under a root and register updates/removals.
- After a fresh server restart, call `library_scan(root=...)` or `storage_list_sidecars(root=...)` before using tools that only take `doc_id`.
- Use `search` for global semantic retrieval and `read` for locator-based chunk windows.
- Startup preflight is fail-fast and requires both Qdrant and GROBID to be configured and reachable.
- FastEmbed model cache defaults to a stable per-user cache directory under `mcp-ebook-read/fastembed` instead of the system temp directory.
- FastEmbed startup now performs bounded retries and clears broken per-model cache state before retrying when the local cache is corrupted or a transient download failure leaves incomplete model files behind.
- Use `document_ingest_pdf_book` to queue a background ingest job for a PDF book.
- Use `document_ingest_epub_book` to queue a background ingest job for an EPUB book.
- Use `document_ingest_pdf_paper` to queue a background ingest job for a PDF paper. Docling remains the canonical page-aware outline; GROBID enriches paper metadata and title.
- Use `document_ingest_status` to poll the current status of one ingest job (or the latest job for a document).
- Use `document_ingest_list_jobs` to inspect recent ingest job history for one document.
- Use `document_autotune_pdf_parser` before a long PDF ingest when you want to benchmark a few Docling thread/batch profiles on sampled pages and persist the best local profile for later runs.
- Use `search_in_outline_node` when you need chapter-scoped retrieval (recommended for reading workflows).
- Use `get_outline` to fetch document outline nodes before chapter/formula/image scoped reading.
- Use `read_outline_node` to read a chapter/outline node directly without locator stitching.
- Use `render_pdf_page` for PDF evidence rendering.
- PDF image extraction is on-demand: ingest does not pre-extract PDF images.
- Use `pdf_list_images` to trigger/list extracted PDF figure/table images (optionally scoped to one outline node).
- Use `pdf_read_image` to get one extracted PDF image path plus nearby text context.
- Use `pdf_book_list_formulas` / `pdf_book_read_formula` for formula-centric reading on PDF books.
- Use `pdf_paper_list_formulas` / `pdf_paper_read_formula` for formula-centric reading on PDF papers.
- Use `epub_list_images` to list extracted EPUB images (optionally scoped to one outline node).
- Use `epub_read_image` to get one EPUB image path plus nearby text context.
- Use `storage_list_sidecars` to inspect sidecar persistence under a root.
- Use `storage_delete_document` to remove one document's persisted state.
- Use `storage_cleanup_sidecars` to prune missing docs/orphan artifacts and compact catalogs.
- For large papers, increase `GROBID_TIMEOUT_SECONDS` (for example `120`) to reduce timeout failures.
- PDF ingest now uses a mixed formula pipeline:
  - Docling structure extraction with `do_formula_enrichment`.
  - Docling-native `$$...$$` LaTeX blocks are registered directly in the formula catalog.
  - Pix2Text runs as a marker fallback when Docling emits unresolved formula markers.
  - Pix2Text runs on CPU by default to avoid platform accelerator instability.
  - Fail-fast when formula markers exist but Pix2Text is unavailable.
- Optional formula env controls:
  - `DOCLING_FORMULA_ENRICHMENT` (`true` by default)
  - `PDF_FORMULA_REQUIRE_ENGINE` (`true` by default)
  - `PDF_FORMULA_BATCH_SIZE` (`auto` by default; auto-detected from CPU and memory, or set an explicit integer)
- Optional Docling performance controls:
  - `document_autotune_pdf_parser` benchmarks a sampled subset of one PDF and writes the selected profile to a local JSON cache.
  - By default the tuning profile lives at `~/Library/Caches/mcp-ebook-read/docling_pdf_tuning.json` on macOS and `$XDG_CACHE_HOME/mcp-ebook-read/docling_pdf_tuning.json` (or `~/.cache/...`) elsewhere.
  - `PDF_DOCLING_NUM_THREADS` and `PDF_DOCLING_BATCH_SIZE` override the cached profile when you need a fixed setting.
- Sidecar cleanup is explicit:
  - `library_scan` no longer triggers threshold-based auto compaction.
  - Use `storage_cleanup_sidecars(..., compact_catalog=true)` when you want compaction.
- Ingest is now asynchronous by design:
  - the `document_ingest_*` tools submit work and return immediately with `job_id`/`doc_id`;
  - poll `document_ingest_status(doc_id=..., job_id=...)` until `status` becomes `succeeded` or `failed`;
  - use `document_ingest_list_jobs(doc_id=...)` when you need recent history or lost the latest `job_id`.

## No-Label Formula Benchmark

Use your own non-scanned PDF corpus as a no-label regression baseline (without manual annotations).

```bash
uvx mcp-ebook-formula-benchmark \
  --samples-dir /ABSOLUTE/PATH/TO/pdf-formula-benchmark-corpus \
  --passes 2 \
  --max-unresolved-rate 0.15 \
  --min-latex-valid-rate 0.85 \
  --min-stability-rate 1.0
```

Output is JSON with per-document metrics and a threshold pass/fail flag. Exit code is `0` when thresholds pass, otherwise `2`.

## No-Label Reading Benchmark

Use a public/sample EPUB/PDF corpus to track outline, chunk, formula, image, table, and local search replay stability.

```bash
uvx mcp-ebook-reading-benchmark \
  --samples-dir /ABSOLUTE/PATH/TO/reading-benchmark-corpus \
  --passes 2 \
  --min-stability-rate 1.0
```

Output is JSON with per-document structure metrics and a threshold pass/fail flag. Exit code is `0` when thresholds pass, otherwise `2`.

## Claude Code MCP Configuration (JSON via `uvx`)

You can register this server in a Claude Code compatible `mcpServers` JSON config.

### Published package

```json
{
  "mcpServers": {
    "mcp-ebook-read": {
      "command": "uvx",
      "args": [
        "mcp-ebook-read"
      ],
      "env": {
        "QDRANT_URL": "http://127.0.0.1:6333",
        "QDRANT_COLLECTION": "mcp_ebook_read_chunks",
        "GROBID_URL": "http://127.0.0.1:8070",
        "GROBID_TIMEOUT_SECONDS": "120"
      }
    }
  }
}
```

### Security note
- Do not put real passwords, API keys, or tokens directly in committed JSON files.
- Use environment variables or secret managers, and keep example values as placeholders only.

## Codex MCP Configuration (TOML)

You can also configure MCP servers in Codex using TOML style (for example in a Codex MCP config file).

### Example

```toml
[mcp_servers.mcp-ebook-read]
command = "uvx"
args = [ "mcp-ebook-read" ]
startup_timeout_sec = 60

[mcp_servers.mcp-ebook-read.env]
QDRANT_URL = "http://127.0.0.1:6333"
QDRANT_COLLECTION = "mcp_ebook_read_chunks"
GROBID_URL = "http://127.0.0.1:8070"
GROBID_TIMEOUT_SECONDS = "120"
```
