Skip to main content
Glama
README.md
# bookorbit-mcp

An [MCP](https://modelcontextprotocol.io) server that exposes the **text of your
ebooks** — managed by [BookOrbit](https://github.com/bookorbit/bookorbit) — to
LLMs, so an assistant can answer questions about _what happens inside a book_
without you copy-pasting chapters.

It is deliberately **navigation-first**: instead of dumping a whole 100k-word
book into the model's context (which would overwhelm it), the server lets the
model find a book, look at its chapter list, and pull only the pieces it needs.

## How it works

BookOrbit stores raw EPUB files and exposes a reader backend
(`/epub/{bookId}/info` + `/epub/{bookId}/file/{path}`). This server uses that
backend to read a book's structure, fetch only the needed chapters, strip the
XHTML to plain text, and cache the result on disk. Nothing is re-downloaded
after the first read.

Three guardrails keep the model from drowning:

1. **Structure before text** — `list_chapters` returns chapter labels and sizes,
   never the body.
2. **Bounded, paginated text** — `get_chapter` caps output (~24k chars) and pages
   through long chapters with `offset` / `nextOffset`.
3. **Snippet-only search** — `search_in_book` returns short snippets plus a
   chapter index + offset the model can re-fetch precisely.

## Tools

All tools are **read-only**.

**Find & read book text**

| Tool                                                         | What it does                                                                             |
| ------------------------------------------------------------ | ---------------------------------------------------------------------------------------- |
| `search_books(query, limit?)`                                | Find books by title/author/series; returns `bookId` + formats + `hasEpub`.               |
| `get_book(bookId)`                                           | Full metadata (description, series, genres, files).                                      |
| `list_chapters(bookId)`                                      | Chapter list with per-chapter char/word counts (no text). Parses + caches on first call. |
| `get_chapter(bookId, chapter, offset?, maxChars?)`           | Plain text of one chapter, paginated.                                                    |
| `search_in_book(bookId, query, maxResults?, caseSensitive?)` | Keyword search within a book → snippets + locations.                                     |

**Discovery & browse**

| Tool                                                                    | What it does                                                                                                       |
| ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `get_related_books(bookId, kind)`                                       | `similar` / `same_series` / `same_author` books for a book.                                                        |
| `list_series(page?, size?)`                                             | Library's series with book/read counts.                                                                            |
| `get_series_books(seriesId, page?, size?)`                              | Books in a series, in order.                                                                                       |
| `list_authors(page?, size?)`                                            | Library's authors with book counts.                                                                                |
| `get_author(authorId)`                                                  | One author, including their bio.                                                                                   |
| `get_author_books(authorId, page?, size?)`                              | Books by an author.                                                                                                |
| `list_collections()` / `get_collection(id)` / `get_collection_books(…)` | User collections, one collection's detail, and its books.                                                          |
| `list_smart_scopes()` / `get_smart_scope_books(…)`                      | Saved dynamic filters and the books they match.                                                                    |
| `list_libraries()`                                                      | Libraries with per-library book/size/format stats.                                                                 |
| `suggest_metadata(kind, q)`                                             | Typeahead over authors/series/genres/tags/publishers/languages/narrators/collections — resolve an exact name.      |
| `get_book_shelf(type, limit?)`                                          | A curated shelf: recently-added, want-to-read, up-next-in-series, continue-reading/listening, random, smart-scope. |
| `search_author_metadata(q, …)`                                          | External provider author-bio candidates (enrichment, not your library).                                            |

**Reading state & stats**

| Tool                                       | What it does                                                                                            |
| ------------------------------------------ | ------------------------------------------------------------------------------------------------------- |
| `get_reading_progress(bookId)`             | Per-file progress (+ audiobook progress) for a book.                                                    |
| `list_currently_reading()`                 | Books you're reading now, with progress.                                                                |
| `get_reading_sessions(bookId, page?, …)`   | Reading-session history + aggregate stats for a book.                                                   |
| `get_library_stats()`                      | Library-wide totals (books/authors/series/storage/…).                                                   |
| `get_reading_stats()`                      | Your personal reading totals (started/completed/…).                                                     |
| `get_library_statistic(kind, libraryIds?)` | One library-wide chart: distributions, top-authors/series, timelines, metadata-health gauges.           |
| `get_reading_statistic(kind, days?, …)`    | One personal reading chart: peak-hours, favorite-days, heatmap, completion-latency, goal-trajectory, …  |
| `get_dashboard_widget(kind)`               | One "headline card": reading-streak, reading-goal, year-projection, neglected-gems, library-overview, … |

**Annotations** (your own highlights & notes)

| Tool                                          | What it does                                        |
| --------------------------------------------- | --------------------------------------------------- |
| `list_annotations(page?, pageSize?, bookId?)` | All highlights/notes across the library, paginated. |
| `list_annotated_books()`                      | Which books have annotations, with counts.          |
| `get_annotations(bookId)`                     | One book's highlights/notes, in chapter order.      |

**Images** (returned as image content, not text)

| Tool                                | What it does                                  |
| ----------------------------------- | --------------------------------------------- |
| `get_book_cover(bookId, size?)`     | A book's cover image (`full` or `thumbnail`). |
| `get_author_image(authorId, size?)` | An author's photo (`full` or `thumbnail`).    |

**Typical flow:** `search_books` → `list_chapters` → `get_chapter` /
`search_in_book`.

## Requirements & scope

- **EPUB only** (v1). Books without an EPUB file return a clear error from the
  text tools.
- **Auth:** BookOrbit access tokens expire after ~15 minutes and there is no
  long-lived API key, so the server logs in with your **username + password** and
  refreshes / re-logs in automatically. A static `BOOKORBIT_TOKEN` is supported
  for quick tests but will not auto-refresh.
- Single-user / local: it acts as the account you configure.

## Configuration

Set these as environment variables (or in a local `.env` — see `.env.example`):

| Variable             | Required    | Notes                                                               |
| -------------------- | ----------- | ------------------------------------------------------------------- |
| `BOOKORBIT_URL`      | yes         | Base URL, e.g. `https://bookorbit.example.com` (trailing slash ok). |
| `BOOKORBIT_USERNAME` | recommended | Enables automatic token refresh.                                    |
| `BOOKORBIT_PASSWORD` | recommended |                                                                     |
| `BOOKORBIT_TOKEN`    | alternative | Static Bearer token; dev/testing only (expires ~15 min).            |
| `CACHE_DIR`          | optional    | Where extracted text is cached (default `~/.cache/bookorbit-mcp`).  |

## Install & build

```bash
npm install
npm run build
```

## Use with a Claude client

Add to your MCP client config (e.g. Claude Desktop's `claude_desktop_config.json`,
or `claude mcp add`). Point at the built entry and pass your credentials:

```json
{
  "mcpServers": {
    "bookorbit": {
      "command": "node",
      "args": ["/absolute/path/to/bookorbit-mcp/dist/server.js"],
      "env": {
        "BOOKORBIT_URL": "https://bookorbit.example.com",
        "BOOKORBIT_USERNAME": "your-username",
        "BOOKORBIT_PASSWORD": "your-password"
      }
    }
  }
}
```

## Development

```bash
npm test          # unit tests (offline; run against captured fixtures)
npm run typecheck # type-check only
npm run dev       # run the server from source over stdio

# Live checks against a real server (need .env with credentials):
node --import tsx scripts/smoke.mts      # exercises search → parse → chapter → search
node --import tsx scripts/mcp-check.mts   # drives the built server over the MCP stdio protocol
```

## Design notes

- **Chapters follow the TOC, not raw files.** A single EPUB spine file can bundle
  many chapters separated by `#fragment` anchors; the server resolves the table
  of contents into meaningful sections and slices each one by its anchor range.
- **Cache layout:** `~/.cache/bookorbit-mcp/<bookId>/book.json` (chapter list +
  sizes) and `text/section-NNN.txt` (extracted text per chapter).
- **Discovery, reading-state, stats, and annotation tools are live passthroughs**
  to BookOrbit and are **not cached** (that data is mutable); only extracted EPUB
  text is cached.
- **Not included (v1):** semantic/RAG search, cross-library content search,
  PDF/MOBI, and any write operations. The cache layout leaves room to add a
  cross-book index later.

TDQS

A4/5.0

Scored across 33 tools

Disambiguation4/5

Most tools have clearly distinct purposes, but several groups share similar names (progress/sessions/stats, stats/statistic/dashboard, list/get annotations) which could cause misselection. The detailed descriptions mostly mitigate this, but the naming similarity is noticeable.

Naming Consistency5/5

All tool names follow a consistent verb_noun snake_case pattern (list_*, get_*, search_*, suggest_*). No mixed conventions or irregular verbs are present, making the naming highly predictable.

Tool Count2/5

With 33 tools, the set is well over the 25+ threshold for 'too many'. Even though the domain is broad, the count feels inflated, especially with many overlapping analytics-style tools that could potentially be consolidated.

Completeness4/5

The read-only domain is well covered: search, browse, read chapters, track progress, annotations, collections, and analytics. Minor gaps include no generic 'list all books' without a query and no per-library detail tool, but these are workable.

Maintenance

ActivityStale
ResponsivenessNo issues