goodreads-mcp
# π goodreads-mcp
A **read-only** MCP server for Goodreads β built without the Goodreads API, because there hasn't been one since December 2020. Lets an LLM find and research books, ratings, and reviews. Tools ride on RSS feeds, the JSON autocomplete endpoint, and the `__NEXT_DATA__` blob embedded in book pages. No login, no cookies, no writes β public data only.
## tools
| tool | stability |
|---|---|
| `search_books` | stable (JSON endpoint) |
| `get_book` | stable (`__NEXT_DATA__` via `.xml` path) β details, cover, ratings histogram, all series memberships, review-language breakdown |
| `get_reviews` | GraphQL β paginated reader reviews (text, rating, likes, date, spoiler flag, permalink) with server-side `min_rating` / `max_rating` and `exclude_spoilers`; `limit` up to 100 |
| `similar_books` | GraphQL β paginated "readers also enjoyed" recommendations |
| `author_books` | GraphQL β paginated author bibliography (from any of their books) |
| `series_books` | GraphQL β paginated series books with reading-order placement; selectable membership for books in multiple series |
| `get_editions` | GraphQL β paginated editions (format, ISBN, publisher, date) |
| `book_lists` | GraphQL β paginated Listopia lists a book appears on (title, votes, size) |
| `popular_books` | GraphQL β most popular books by release year (or year+month), ranked |
| `compare_books` | takes several book ids, ranks them by rating with positive/critical share |
| `get_shelf` | stable (RSS) β public shelves |
| `list_shelves` | best effort (HTML) β public profiles |
The discovery tools all take a `book_id` and return results carrying `book_id`/title/author/rating/url, so an agent can chain them β e.g. `similar_books` β `get_reviews` on a recommendation. This is the structured book graph a general web search can't assemble.
GraphQL discovery tools page in batches of 20 and accept a total `limit` up to
100. Responses include `returned` and `has_more`, keeping larger lookups useful
without allowing unbounded traffic.
> **WAF note:** Goodreads book HTML pages now sit behind an AWS WAF JavaScript
> challenge (HTTP 202) that plain HTTP clients can't solve. `get_book` routes
> around it via the `.xml`-suffixed page, so it still works without a browser. If
> Goodreads ever extends the WAF to a path we depend on, the client raises
> `WAFChallenge` with a clear message instead of a confusing parse error.
## install
```bash
cd goodreads-mcp
python3.10 -m venv .venv && .venv/bin/pip install -e .
```
Requires Python β₯ 3.10.
## config (optional)
No login or cookies β everything is public data. The only setting is your numeric `user_id`, the default for the shelf tools. It's the number in `goodreads.com/user/show/<ID>-yourname`; you can also pass `user_id` to each shelf tool per call.
```bash
mkdir -p ~/.config/goodreads-mcp
cat > ~/.config/goodreads-mcp/config.json << 'EOF'
{ "user_id": "12345678" }
EOF
```
Env var `GOODREADS_USER_ID` overrides the file.
## Claude Desktop config
`~/Library/Application Support/Claude/claude_desktop_config.json`:
```json
{
"mcpServers": {
"goodreads": {
"command": "/path/to/goodreads-mcp/.venv/bin/goodreads-mcp"
}
}
}
```
Or for development, `mcp dev goodreads_mcp/server.py` gives you the Inspector UI to poke each tool.
## first-run verification
The endpoints are unofficial, so verify in this order:
1. `search_books("project hail mary")` β should just work
2. `get_book("54493401")` β confirms the `.xml`/WAF workaround; check the histogram is populated
3. `get_reviews("54493401")` β should return real review text
4. `get_shelf("to-read")` β checks your `user_id` + RSS
5. `list_shelves()` β best-effort shelf-name scrape
## tests
```bash
.venv/bin/pip install -e ".[test]"
.venv/bin/pytest # offline parser/unit tests
GOODREADS_LIVE=1 .venv/bin/pytest # + live network smoke tests
```
## design notes
- **Request-first, no browser automation.** Everything is `httpx` against JSON/RSS/embedded-JSON/GraphQL surfaces; the only HTML regex is in `list_shelves` and the GraphQL config discovery.
- **GraphQL backbone (reviews).** `get_reviews` calls Goodreads' AppSync GraphQL endpoint β the same backend the website uses. The web app injects a public read-only API key into page-level `__NEXT_DATA__` and keeps the production endpoint in its `_app` bundle; the client resolves both at runtime and caches them, so rotations self-heal (`client.graphql_config`). Legacy bundles that carry a paired key and endpoint are still supported. This is what enables real pagination (past the ~30 reviews a page embeds) and server-side rating filters. GraphQL partial-success is respected: a deleted review's sub-resource just comes back `null` rather than failing the call.
- **WAF-aware.** Book pages sit behind an AWS WAF JS challenge; `get_book` uses the `.xml` path that isn't gated, and the client raises `WAFChallenge` if it ever gets a challenge body so failures are loud, not silent. (The GraphQL endpoint is a separate AppSync host and isn't WAF-gated.)
- **Polite client.** Single persistent session, browser-faithful headers, exponential backoff on 429/503; `get_reviews` caps paging at 100 reviews.
- **Caveats**: all of this is unofficial and depends on markup/endpoints/keys that can drift.
## shipped since v0.1
- **richer book data** β `get_book` includes covers, the ratings histogram, every series membership, normalized publication dates, and configurable review-language depth; `series_books` can traverse any listed membership, and `get_reviews` returns paginated, filterable reader reviews.
- **author bibliography** β `author_books` returns an author's works (ranked by popularity) plus a link to their author page (`author_url`).
- **bounded discovery pagination** β similar books, bibliographies, series, editions, and Listopia memberships can return up to 100 results with `has_more` metadata.
## ideas for v2
- author page detail (bio, photo, follower count) β not currently exposed cleanly: the author page is legacy server-rendered HTML with no structured JSON, and there's no discoverable GraphQL contributor-detail query, so this would require brittle DOM scraping. `author_books` links to the page instead.
- caching layer for repeated lookups (the discovery tools each resolve the book first; a small TTL cache would cut duplicate GraphQL calls)
TDQS
Scored across 12 tools
Most tools target clearly distinct resources: search_books, get_book, get_reviews, get_editions, similar_books, author_books, series_books, compare_books, popular_books, book_lists, and get_shelf are all identifiable by purpose. The main ambiguity is between list_shelves and get_shelfβboth involve shelvesβbut descriptions clarify one returns shelf names and the other returns books on a shelf.
The naming follows a loose verb_noun pattern for many tools (search_books, get_book, get_reviews, get_editions, get_shelf, compare_books, list_shelves), but several tools drop the verb entirely and use bare noun phrases like book_lists, similar_books, author_books, series_books, and popular_books. This mixed convention is readable but not fully consistent.
Twelve tools form a well-scoped set for a Goodreads data-access server. Each tool covers a meaningful, distinct aspect of book discovery and reading data without feeling padded or redundant.
The tool surface covers the major Goodreads information needs: search, full book details, reviews, editions, related books, author bibliographies, series membership, Listopia lists, popularity charts, comparison, and shelf contents. There are no obvious dead ends for a read-only book-data server; result payloads include ids and URLs for chaining into further lookups.