{
  "markdown": "# calibreweb-mcp\n\n<!-- badges: start -->\n\n[![CI](https://img.shields.io/github/actions/workflow/status/ni-c/calibreweb-mcp/ci.yml?branch=main&label=CI)](https://github.com/ni-c/calibreweb-mcp/actions/workflows/ci.yml)\n[![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/ni-c/calibreweb-mcp/badge)](https://scorecard.dev/viewer/?uri=github.com/ni-c/calibreweb-mcp)\n<a href=\"https://socket.dev/npm/package/calibreweb-mcp\"><img src=\"https://socket.dev/api/badge/npm/package/calibreweb-mcp\" alt=\"Socket supply-chain report\" height=\"20\"></a>\n[![Glama score](https://glama.ai/mcp/servers/ni-c/calibreweb-mcp/badges/score.svg)](https://glama.ai/mcp/servers/ni-c/calibreweb-mcp)\n<br>\n[![npm version](https://img.shields.io/npm/v/calibreweb-mcp)](https://www.npmjs.com/package/calibreweb-mcp)\n[![container image](https://img.shields.io/badge/ghcr.io-ni--c%2Fcalibreweb--mcp-4f46e5?logo=docker&logoColor=white)](https://github.com/ni-c/calibreweb-mcp/pkgs/container/calibreweb-mcp)\n[![HTTP via mcp-hub](https://img.shields.io/badge/HTTP-via%20mcp--hub-4f46e5?logo=modelcontextprotocol&logoColor=white)](https://mcp-hub.ni-c.de)\n<br>\n[![docs](https://img.shields.io/badge/docs-calibreweb--mcp.ni--c.de-4f46e5?logo=readthedocs&logoColor=white)](https://calibreweb-mcp.ni-c.de)\n[![sponsor](https://img.shields.io/badge/sponsor-ni--c-ea4aaa?logo=githubsponsors&logoColor=white)](https://github.com/sponsors/ni-c)\n<!-- badges: end -->\n\nA read-only [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server for\n[Calibre-Web](https://github.com/janeczku/calibre-web) (and\n[Calibre-Web Automated](https://github.com/crocodilestick/Calibre-Web-Automated)),\nthe self-hosted ebook library web UI.\n\nLets MCP clients like Claude Code, Claude Desktop or Codex search your library,\nbrowse the curated views and shelves, follow per-format download links and pull cover\nimages straight into the conversation. It never writes anything: every tool is a GET.\n\nSix tools is the ceiling, not the floor: `CALIBRE_WEB_ALLOW_TOOLS=essential`\nregisters a curated five instead, and a model picks the right tool far more\nreliably from five than from six — see\n[choosing which tools load](#choosing-which-tools-load).\n\nCalibre-Web has no REST API — its only stable machine-readable interface is the OPDS\ncatalog feed it serves for e-reader apps. This server speaks that feed: Atom XML with\nHTTP Basic auth in, structured book data out.\n\n<!-- <picture> is resolved against the colour scheme of the page showing it, so GitHub\n     picks the variant that matches its own theme toggle. npm strips <picture> and\n     <source> when it sanitises the README and keeps the <img>, which is why that\n     fallback brings its own dark card instead of relying on a media query. -->\n<picture>\n  <source media=\"(prefers-color-scheme: dark)\" srcset=\"https://calibreweb-mcp.ni-c.de/architecture-dark.svg\">\n  <source media=\"(prefers-color-scheme: light)\" srcset=\"https://calibreweb-mcp.ni-c.de/architecture-light.svg\">\n  <img src=\"https://calibreweb-mcp.ni-c.de/architecture.svg\" alt=\"An MCP client speaks stdio to calibreweb-mcp, which reads the OPDS feed of Calibre-Web over HTTPS with Basic auth\" width=\"800\">\n</picture>\n\n<!-- Recorded with vhs from docs/demo.tape against the bundled fixture library\n     (docs/demo-server.mjs) — no Calibre-Web instance needed to reproduce it. -->\n\n![Demo: listing the tools, searching the library and reading the stats through the MCP Inspector CLI](https://calibreweb-mcp.ni-c.de/demo.gif)\n\n## What makes it different\n\n**The API Calibre-Web never had.** Calibre-Web exposes no REST API — its only\nstable machine interface is the OPDS Atom feed built for e-reader apps. These\ntools parse that feed into structured book data with numeric ids, per-format\ndownload URLs and bounded summaries.\n\n**Read-only by construction.** All six tools are GETs. Redirects are refused so\nBasic credentials never travel, XML carrying a DOCTYPE is rejected outright,\nhrefs are locked to the configured origin, and metadata is marked as the\nuntrusted data it is.\n\n## Requirements\n\n- Node.js 22 or newer\n- A Calibre-Web instance (developed against the current\n  `linuxserver/calibre-web` image; Calibre-Web Automated works the same way)\n- A Calibre-Web user for the server. The OPDS feed authenticates with the\n  normal web login — use a **dedicated account with only the View and Download\n  roles**, not your admin account. If the instance allows anonymous browsing,\n  the server can also run without credentials.\n\n## Configuration\n\n| Variable                   | Required | Description                                                                                             |\n| -------------------------- | -------- | ------------------------------------------------------------------------------------------------------- |\n| `CALIBRE_WEB_URL`          | yes      | Root URL of the instance, e.g. `https://books.example.com`. The `/opds` path is appended automatically. |\n| `CALIBRE_WEB_USERNAME`     | yes¹     | Username of the Calibre-Web account.                                                                    |\n| `CALIBRE_WEB_PASSWORD`     | yes¹     | Password of that account (the web login password).                                                      |\n| `CALIBRE_WEB_INSECURE_TLS` | no       | `true` to accept self-signed certificates — scoped to the configured host only.                         |\n| `CALIBRE_WEB_ALLOW_TOOLS`  | no       | Comma-separated tool names, `list_*` prefixes, or `essential` for a curated preset                      |\n| `CALIBRE_WEB_DENY_TOOLS`   | no       | Same syntax; removed from whatever `CALIBRE_WEB_ALLOW_TOOLS` left                                       |\n\n¹ Leave **both** unset for an instance that allows anonymous browsing; setting\nonly one of them is a configuration error.\n\n### Choosing which tools load\n\n`CALIBRE_WEB_ALLOW_TOOLS` and `CALIBRE_WEB_DENY_TOOLS` take comma-separated tool names;\na trailing `*` matches a whole family. `essential` is a curated preset of\nfive: `search_books`, `list_books`, `list_shelves`, `get_shelf_books`, `get_stats`.\n\n```sh\nCALIBRE_WEB_ALLOW_TOOLS=essential\nCALIBRE_WEB_ALLOW_TOOLS=search_books,list_shelves\nCALIBRE_WEB_DENY_TOOLS=get_cover\n```\n\nAn entry that matches no tool aborts startup and names it, so a typo cannot\nsilently hide a tool — an absent tool is not something anyone traces back to an\nenvironment variable. A filtered tool is never registered, so it is absent from\n`tools/list` and unknown to `tools/call` alike.\n\nIf you run several of these servers at once, [mcp-hub](https://mcp-hub.ni-c.de)\nis the other answer — its `/hub` endpoint replaces every server's tools with six\nmeta-tools.\n\n## Installation\n\n### Claude Code\n\n```sh\nclaude mcp add calibreweb \\\n  -e CALIBRE_WEB_URL=https://books.example.com \\\n  -e CALIBRE_WEB_USERNAME=reader \\\n  -e CALIBRE_WEB_PASSWORD=... \\\n  -- npx calibreweb-mcp\n```\n\n### Claude Desktop\n\n```json\n{\n  \"mcpServers\": {\n    \"calibreweb\": {\n      \"command\": \"npx\",\n      \"args\": [\"calibreweb-mcp\"],\n      \"env\": {\n        \"CALIBRE_WEB_URL\": \"https://books.example.com\",\n        \"CALIBRE_WEB_USERNAME\": \"reader\",\n        \"CALIBRE_WEB_PASSWORD\": \"...\"\n      }\n    }\n  }\n}\n```\n\n### Codex\n\n```toml\n[mcp_servers.calibreweb]\ncommand = \"npx\"\nargs = [\"calibreweb-mcp\"]\nenv = { CALIBRE_WEB_URL = \"https://books.example.com\", CALIBRE_WEB_USERNAME = \"reader\", CALIBRE_WEB_PASSWORD = \"...\" }\n```\n\n### Docker\n\n```sh\ndocker run -i --rm \\\n  -e CALIBRE_WEB_URL=https://books.example.com \\\n  -e CALIBRE_WEB_USERNAME=reader \\\n  -e CALIBRE_WEB_PASSWORD=... \\\n  ghcr.io/ni-c/calibreweb-mcp\n```\n\n### Through mcp-hub\n\nA client that cannot spawn a local process — ChatGPT connectors, Claude on the web,\nCursor, LibreChat — reaches calibreweb-mcp through [mcp-hub](https://mcp-hub.ni-c.de): one\ncontainer serves many stdio MCP servers over Streamable HTTP, with an OAuth 2.1 login\nbehind a single password and long-lived tokens for the clients that cannot do OAuth. Its\n`/hub` endpoint puts every server behind six meta-tools, so one connector reaches all of\nthem without N×tool schemas in the model's context, and it speaks both protocol revisions\n— a question this server asks travels through it to the person at the far end.\n\nIts `/config/mcp.json` uses Claude Code's format, so the entry is the one you already\nhave:\n\n```json\n{\n  \"mcpServers\": {\n    \"calibreweb\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"calibreweb-mcp\"],\n      \"env\": { \"CALIBRE_WEB_ALLOW_TOOLS\": \"essential\" },\n      \"denyTools\": [\"get_cover\"]\n    }\n  }\n}\n```\n\n`allowTools` and `denyTools` there are the hub's **own** per-server filter, which is not\nthe same thing as `*_ALLOW_TOOLS` in `env` — the difference, and the mistake it invites,\nare in the [client guide](https://calibreweb-mcp.ni-c.de/guide/clients#through-mcp-hub).\n\n## Tools\n\nAll tools are read-only (`readOnlyHint: true`).\n\n| Tool              | Description                                                                                                                                                                            |\n| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `search_books`    | Search by title, author, series, publisher and tags. Calibre-Web returns every match at once; the result is capped client-side (`limit`, default 50) and reports the real match count. |\n| `list_books`      | Book listings by view: `new` (default), `hot`, `rated`, `discover` (random), `read`, `unread`, or `all` (optionally narrowed to an initial letter).                                    |\n| `list_shelves`    | Public shelves plus the configured user's own shelves.                                                                                                                                 |\n| `get_shelf_books` | The books on a shelf, in shelf order.                                                                                                                                                  |\n| `get_cover`       | A book's cover, returned as an image the client can display.                                                                                                                           |\n| `get_stats`       | Total books, authors, categories and series.                                                                                                                                           |\n\nBook entries include authors, tags, series (with index), rating, a bounded\nsummary, a cover URL and per-format download URLs — ready-made links a human can\nopen, since the model itself has no reason to download an EPUB.\n\n### Structured output\n\nEvery tool declares an `outputSchema` and answers with `structuredContent`\nalongside the text block, so a client can use the result without parsing prose:\n\n```jsonc\n{\n  \"untrusted\": true,\n  \"source\": \"calibre-web\",\n  \"totalFound\": 2,\n  \"truncated\": false,\n  \"books\": [{ \"id\": 7, \"title\": \"Dune\", \"authors\": [\"Frank Herbert\"] }],\n  \"notes\": [\"Book titles, authors, tags, series and summaries come from …\"],\n}\n```\n\nThe `untrusted` marker is a field and not only a line in `notes`, because a\nclient that reads the structured half would otherwise have to find the warning\nin a list of sentences. The two tools without it are `get_stats`, which is four\ncounters this server has checked are numbers, and `get_cover`, which reports an\nid, a media type from a four-entry allowlist and a byte count — the image itself\nstays in the content block where a client renders it.\n\nAn over-budget result drops book summaries as before. Where that is still not\nenough it is now an **error** rather than JSON cut at the ceiling: unparseable\ntext was tolerable in a text block and is not something `structuredContent` can\ncarry, and the two channels have to hold the same value.\n\n### Pagination\n\nFeeds are paginated by the instance's _books per page_ setting (default 60); the\npage size is not client-controllable. Every listing returns\n`pagination.nextOffset` when more pages exist — pass it as `offset` in the next\ncall. The `discover` view is a random selection and not paginated.\n\n## Not exposed, on purpose\n\n- **No writes.** The OPDS feed has none, and this server would not add any.\n- **No file downloads.** Tools return download URLs, not ebook payloads.\n- **No facet browsing** (authors/series/tags/publishers/languages/formats as\n  their own index feeds). `search_books` covers those lookups; the routes exist\n  and tools for them can be added if there is a real use case.\n\n## Safety\n\n- The server is read-only by construction — GET requests only, no state anywhere.\n- Book metadata is untrusted third-party data; every result says so, control\n  characters are stripped, and XML entity processing is disabled (documents\n  declaring a DOCTYPE or entities are refused outright).\n- Responses are bounded before parsing (8 MB feeds, 1 MB covers) and again\n  before they reach the model (per-book and per-response budgets).\n- Feed hrefs are only passed through when they resolve to the configured\n  origin over http(s) — a hostile feed cannot plant `javascript:`, `file:` or\n  cross-origin URLs into the results.\n- Redirects are refused so the Basic credentials can never be replayed to\n  another host; covers are only passed through for real image content types.\n- The password is scrubbed from the process environment at startup, and URLs\n  are credential-redacted before they appear in any log or result.\n\n## Documentation\n\nThe full guide, tool reference and security notes live at\n**[calibreweb-mcp.ni-c.de](https://calibreweb-mcp.ni-c.de)** (source in [`docs/`](docs/)).\n\n## Development\n\n```sh\nnpm install\nnpm test            # unit tests against a stubbed OPDS feed, no instance needed\nnpm run lint\nnpm run build\n```\n\nSee [CONTRIBUTING.md](CONTRIBUTING.md).\n\n## Releasing\n\n1. Update `CHANGELOG.md` and bump the version in `package.json` (+ lockfile).\n2. `npm run lint && npm run test:coverage && npm run build`\n3. Tag the release: `git tag -s vX.Y.Z -m \"vX.Y.Z\" && git push origin vX.Y.Z`\n\n## Contributing\n\nIssues, discussions and pull requests are welcome — see\n[CONTRIBUTING.md](CONTRIBUTING.md). For vulnerabilities please use\n[private reporting](https://github.com/ni-c/calibreweb-mcp/security/advisories/new)\nrather than a public issue; the policy is in [SECURITY.md](SECURITY.md).\n\n## License\n\n[MIT](LICENSE) © Willi Thiel\n",
  "bytes": 14500,
  "sha": "c3b2a00a77c85e65de5caace67e9d4fd857611cb26dcd734682620ee40479d75",
  "repo_slug": "ni-c/calibreweb-mcp",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_ni_c_calibreweb_mcp_57f9b579/readme"
}