{
  "markdown": "# audiobookshelf-mcp\n\n<!-- badges: start -->\n\n[![CI](https://img.shields.io/github/actions/workflow/status/ni-c/audiobookshelf-mcp/ci.yml?branch=main&label=CI)](https://github.com/ni-c/audiobookshelf-mcp/actions/workflows/ci.yml)\n[![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/ni-c/audiobookshelf-mcp/badge)](https://scorecard.dev/viewer/?uri=github.com/ni-c/audiobookshelf-mcp)\n<a href=\"https://socket.dev/npm/package/audiobookshelf-mcp\"><img src=\"https://socket.dev/api/badge/npm/package/audiobookshelf-mcp\" alt=\"Socket supply-chain report\" height=\"20\"></a>\n[![Glama score](https://glama.ai/mcp/servers/ni-c/audiobookshelf-mcp/badges/score.svg)](https://glama.ai/mcp/servers/ni-c/audiobookshelf-mcp)\n<br>\n[![npm version](https://img.shields.io/npm/v/audiobookshelf-mcp)](https://www.npmjs.com/package/audiobookshelf-mcp)\n[![container image](https://img.shields.io/badge/ghcr.io-ni--c%2Faudiobookshelf--mcp-4f46e5?logo=docker&logoColor=white)](https://github.com/ni-c/audiobookshelf-mcp/pkgs/container/audiobookshelf-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-audiobookshelf--mcp.ni--c.de-4f46e5?logo=readthedocs&logoColor=white)](https://audiobookshelf-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 [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server for\n[Audiobookshelf](https://www.audiobookshelf.org/), the self-hosted audiobook and\npodcast server.\n\nLets MCP clients like Claude Code, Claude Desktop or Codex browse your libraries,\nanswer questions about what you own and what you have listened to, and — unless you\nswitch it off — keep your listening progress, bookmarks, collections and playlists up\nto date: 44 tools, 29 read and 15 write.\n\nForty-four tools is the ceiling, not the floor: `AUDIOBOOKSHELF_ALLOW_TOOLS=essential`\nregisters a curated eight instead, and a model picks the right tool far more\nreliably from eight than from forty-four — see\n[choosing which tools load](#choosing-which-tools-load).\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://audiobookshelf-mcp.ni-c.de/architecture-dark.svg\">\n  <source media=\"(prefers-color-scheme: light)\" srcset=\"https://audiobookshelf-mcp.ni-c.de/architecture-light.svg\">\n  <img src=\"https://audiobookshelf-mcp.ni-c.de/architecture.svg\" alt=\"An MCP client talks to audiobookshelf-mcp over stdio; the server exposes 29 read and 15 write tools, compacts every response, and calls the Audiobookshelf REST API over HTTPS with a bearer API key\" width=\"800\">\n</picture>\n\n<img src=\"https://audiobookshelf-mcp.ni-c.de/demo.gif\" alt=\"Terminal recording: the server reports 44 tools, lists library items as a compact projection, and answers the first delete_collection call with a single-use confirmation token instead of deleting anything\" width=\"800\">\n\n## What makes it different\n\n**Every response is a projection, not the raw object.** An expanded library item\ncarries every audio file, track and chapter with full ffprobe metadata. The media\ntools answer with a compact shape instead, and `detail=\"full\"` is there for when\nthe raw object really is what you want.\n\n**Twenty-nine of the forty-four tools only read.** `AUDIOBOOKSHELF_READ_ONLY=true`\nregisters those and nothing else, so a write tool is absent from `tools/list`\nrather than refused when it is called.\n\n**The six tools that take something out ask a person first**, through MCP\nelicitation — a dialog the model cannot answer on its behalf, falling back to a\nsingle-use token bound to the exact targets where the client cannot show one.\n\n## Requirements\n\n- Node.js 22 or newer\n- Audiobookshelf **2.26.0 or newer** — earlier versions have no API keys\n- An Audiobookshelf API key\n\n## Getting an API key\n\nAPI keys are managed by an admin under **Settings → Users → API Keys**. A key acts\non behalf of exactly one Audiobookshelf user and inherits that user's permissions,\nso a key issued for a normal account cannot see libraries that account cannot see,\nand cannot delete anything unless that account may delete. The key is shown only\nonce, at creation.\n\n## Configuration\n\n| Variable                      | Required | Description                                                                             |\n| ----------------------------- | -------- | --------------------------------------------------------------------------------------- |\n| `AUDIOBOOKSHELF_URL`          | yes      | Base URL of the instance, e.g. `https://abs.example.com`. Must not contain credentials. |\n| `AUDIOBOOKSHELF_API_KEY`      | yes      | API key, sent as `Authorization: Bearer …`                                              |\n| `AUDIOBOOKSHELF_READ_ONLY`    | no       | `true` registers only the 29 read tools                                                 |\n| `AUDIOBOOKSHELF_INSECURE_TLS` | no       | `true` accepts self-signed certificates — scoped to this connection, not process-wide   |\n| `AUDIOBOOKSHELF_ALLOW_TOOLS`  | no       | Comma-separated tool names, `list_*` prefixes, or `essential` for a curated preset      |\n| `AUDIOBOOKSHELF_DENY_TOOLS`   | no       | Same syntax; removed from whatever `AUDIOBOOKSHELF_ALLOW_TOOLS` left                    |\n| `ELICITATION`                 | no       | `false` replaces the approval dialog with the two-call token. **Not prefixed**          |\n\nThe server starts without configuration: it completes the MCP handshake and lists\nits tools, and every call then fails with the setup instructions. That is\ndeliberate, so registries and sandbox inspectors can introspect it.\n\n### Choosing which tools load\n\n`AUDIOBOOKSHELF_ALLOW_TOOLS` and `AUDIOBOOKSHELF_DENY_TOOLS` take comma-separated tool names;\na trailing `*` matches a whole family. `essential` is a curated preset of\neight: `list_libraries`, `search_library`, `list_library_items`, `get_library_item`, `get_item_chapters`, `list_items_in_progress`, `get_media_progress`, `set_media_progress`.\n\n```sh\nAUDIOBOOKSHELF_ALLOW_TOOLS=essential\nAUDIOBOOKSHELF_ALLOW_TOOLS=search_library,get_library_item,set_media_progress\nAUDIOBOOKSHELF_DENY_TOOLS=delete_*\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, exactly like a write tool under\n`AUDIOBOOKSHELF_READ_ONLY`.\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 audiobookshelf \\\n  -e AUDIOBOOKSHELF_URL=https://abs.example.com \\\n  -e AUDIOBOOKSHELF_API_KEY=… \\\n  -- npx -y audiobookshelf-mcp\n```\n\n### Claude Desktop\n\n`claude_desktop_config.json`:\n\n```json\n{\n  \"mcpServers\": {\n    \"audiobookshelf\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"audiobookshelf-mcp\"],\n      \"env\": {\n        \"AUDIOBOOKSHELF_URL\": \"https://abs.example.com\",\n        \"AUDIOBOOKSHELF_API_KEY\": \"…\"\n      }\n    }\n  }\n}\n```\n\n### Codex\n\n`~/.codex/config.toml`:\n\n```toml\n[mcp_servers.audiobookshelf]\ncommand = \"npx\"\nargs = [\"-y\", \"audiobookshelf-mcp\"]\nenv = { AUDIOBOOKSHELF_URL = \"https://abs.example.com\", AUDIOBOOKSHELF_API_KEY = \"…\" }\n```\n\n### Docker\n\nMulti-arch, with SBOM and build provenance:\n\n```sh\ndocker run -i --rm \\\n  -e AUDIOBOOKSHELF_URL=https://abs.example.com \\\n  -e AUDIOBOOKSHELF_API_KEY=… \\\n  ghcr.io/ni-c/audiobookshelf-mcp\n```\n\n`-i` is required — the protocol runs over stdin and stdout. There is no port to\npublish. More client recipes, including how to keep the key off the `docker run`\ncommand line, are in the\n[client guide](https://audiobookshelf-mcp.ni-c.de/guide/clients).\n\n### Through mcp-hub\n\nA client that cannot spawn a local process — ChatGPT connectors, Claude on the web,\nCursor, LibreChat — reaches audiobookshelf-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    \"audiobookshelf\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"audiobookshelf-mcp\"],\n      \"env\": { \"AUDIOBOOKSHELF_ALLOW_TOOLS\": \"essential\" },\n      \"denyTools\": [\"delete_*\"]\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://audiobookshelf-mcp.ni-c.de/guide/clients#through-mcp-hub).\n\n## Tools\n\nEvery tool declares an `outputSchema` and answers with `structuredContent`\nalongside the text block, so a client can use the result without parsing prose.\nSeven tools that answered with a sentence — _\"Collection col_1 deleted.\"_ — now\nanswer with the fields as well.\n\nThe tools that report library metadata carry `untrusted: true` and\n`source: \"audiobookshelf\"` as fields: book descriptions pulled from metadata\nproviders, podcast feed summaries and episode titles are all written by someone\nelse, and so are the bookmark titles and selected tags of an account and the\ntitles of a library's longest and largest items. The rest are without it — an\nid this server was given, a position it was asked to store, the version string\nof the instance, and the library names and folder paths the operator typed.\n\nThe documents are described as open objects with the top-level keys this server\nbuilds. `detail: \"full\"` hands the API record back whole, so the same tool\nanswers with far more keys than it names — and the SDK validates each result\nagainst its schema before it goes out, which is exactly why a strict shape would\nbe wrong here.\n\n`get_personalized_shelves` answers `{items: [...]}` rather than the bare array\nthe API sends: a schema whose root is an array is served to a 2025-era client\nrewritten as `{result: …}`, so it would otherwise answer in two shapes.\n\n### Reading\n\n| Tool                                  | What it does                                                                                        |\n| ------------------------------------- | --------------------------------------------------------------------------------------------------- |\n| `list_libraries`                      | The accessible libraries with id, name and media type — the entry point                             |\n| `get_library`                         | One library with its folders and settings                                                           |\n| `get_library_stats`                   | Item, author and genre counts, total duration and size                                              |\n| `get_library_filter_data`             | The filterable values of a library: authors, genres, tags, series, narrators, languages, publishers |\n| `list_library_items`                  | Items of a library, paginated, sortable, filterable                                                 |\n| `search_library`                      | Full-text search across books, podcasts, series, authors, narrators and tags                        |\n| `get_personalized_shelves`            | The home screen shelves: Continue Listening, Recently Added, …                                      |\n| `list_series` / `get_series`          | Series with book count and total duration                                                           |\n| `list_authors` / `get_author`         | Authors, optionally with their items                                                                |\n| `list_tags` / `list_genres`           | All tags / genres used on the server                                                                |\n| `get_library_item`                    | One book or podcast with metadata, tags and your progress                                           |\n| `get_item_chapters`                   | The chapter list of a book, separate because it can be long                                         |\n| `get_podcast_episode`                 | One episode with publication date, duration and description                                         |\n| `list_recent_episodes`                | Newest episodes of a podcast library                                                                |\n| `get_me`                              | The user the API key acts for, with permissions and libraries                                       |\n| `list_items_in_progress`              | Started but unfinished items across all libraries                                                   |\n| `get_media_progress`                  | Position, percentage and finished state for one item                                                |\n| `get_listening_stats`                 | Total time, time per day and per weekday, most listened items                                       |\n| `get_year_stats`                      | The \"year in review\" figures for one calendar year                                                  |\n| `list_listening_sessions`             | Playback sessions with device, position and time listened                                           |\n| `list_bookmarks`                      | Bookmarks, all of them or those of one item                                                         |\n| `list_collections` / `get_collection` | Collections — shared, ordered groups of books                                                       |\n| `list_playlists` / `get_playlist`     | Playlists — private per user, books or episodes                                                     |\n| `get_server_status`                   | Version and initialization state of the server                                                      |\n\n### Writing\n\n| Tool                                                               | What it does                                                            |\n| ------------------------------------------------------------------ | ----------------------------------------------------------------------- |\n| `set_media_progress`                                               | Set position, mark finished or unfinished, hide from Continue Listening |\n| `delete_media_progress` 👤                                         | Delete a progress record — the listening history of that item           |\n| `create_bookmark` / `update_bookmark` / `delete_bookmark` 👤       | Named positions in a book                                               |\n| `create_collection` / `update_collection` / `delete_collection` 👤 | Collections                                                             |\n| `add_books_to_collection` / `remove_books_from_collection` 👤      | Collection membership                                                   |\n| `create_playlist` / `update_playlist` / `delete_playlist` 👤       | Playlists                                                               |\n| `add_items_to_playlist` / `remove_items_from_playlist` 👤          | Playlist membership                                                     |\n\n👤 asks a person through MCP elicitation · falls back to a two-call\n`confirm_token` where the client cannot show a dialog.\n\n### Response size\n\nAudiobookshelf returns very large objects — an expanded library item carries every\naudio file, track and chapter with full ffprobe metadata. Every tool that returns\nmedia therefore answers with a compact projection by default and accepts\n`detail: \"full\"` for the raw object.\n\nThree bounds, because one is not enough:\n\n- **A response ceiling of 5 MB.** `content-length` is checked before a byte is\n  read and a chunked body is counted while reading, so an oversized answer is\n  refused rather than parsed.\n- **A result ceiling of 100 000 bytes**, applied in `jsonResult` — so it covers\n  `detail: \"full\"` too. Whole entries are dropped, never characters: a truncated\n  document is not a smaller answer, it is an unparseable one. The result then\n  carries a `truncated` block naming what to call instead.\n- **A cap on embedded members.** A compact collection or playlist embeds the\n  first 25 of its books or entries and reports the real count; `get_collection`\n  and `get_playlist` return the whole membership for one of them.\n\n`list_library_items` pages properly, with `limit` and `page`. Seven listing tools\nhave neither — `list_libraries`, `list_authors`, `list_tags`, `list_genres`,\n`list_collections`, `list_playlists` and `list_bookmarks` — because the\nAudiobookshelf routes behind them return everything in one answer and take no\npaging parameters. `library_id` narrows the two collection routes; the rest are\nbounded by the ceilings above.\n\n### Filtering\n\n`list_library_items` takes `filter_group` plus `filter_value` and builds the\nbase64-encoded `filter` parameter the API expects. The valid values come from\n`get_library_filter_data`. A valued group without a value is rejected, because\nAudiobookshelf would silently answer with the _unfiltered_ library instead.\n\n```\nfilter_group=\"authors\",  filter_value=\"<author id>\"\nfilter_group=\"progress\", filter_value=\"finished\" | \"in-progress\" | \"not-started\" | \"not-finished\"\nfilter_group=\"issues\"    (standalone, no value)\n```\n\n## Not exposed, on purpose\n\n**No playback.** Pausing, seeking and playing are a session state machine that\nbelongs in a real client. `set_media_progress` covers \"mark this finished\" and\n\"jump me to chapter 12\"; your phone does the playing.\n\n**No administration, even with an admin key.** There is no tool for user\nmanagement, server settings, backups, cache purging, filesystem browsing, library\nor item deletion, metadata rewriting or file uploads. Those endpoints exist in\nAudiobookshelf; they are simply not wired up here, because the blast radius of a\nconfused or manipulated model is bounded by the tool list.\n\n## Safety\n\n- **Read-only mode.** `AUDIOBOOKSHELF_READ_ONLY=true` does not register the write\n  tools at all, rather than refusing them at call time.\n- **A person is asked, not just told.** The eight tools that can take something\n  out — the three deletes, `delete_bookmark`, `remove_books_from_collection`,\n  `remove_items_from_playlist`, and `update_collection` / `update_playlist` when\n  they are asked to reorder, which replaces an order nobody can reconstruct —\n  raise a real dialog through MCP elicitation,\n  which the model cannot answer on its behalf. A plain `confirm: true` flag could\n  be set by the model on the first try, or be talked into it by text coming out\n  of the library.\n\n  Where the client cannot show a dialog they fall back to a single-use token\n  bound to the exact targets and expiring after five minutes. That fallback\n  proves the call was made twice with the same arguments and nothing more, and\n  the text says so rather than implying somebody approved. `ELICITATION=false`\n  takes it deliberately; it never removes the guard. See\n  [Asking a person](https://audiobookshelf-mcp.ni-c.de/guide/approval).\n\n- **Confirmation prompts never quote API content.** Collection and playlist names\n  are user-supplied text and are read by a model, so the prompts name ids and\n  counts only.\n- **A 200 that is not JSON is an error.** Returning the body as a string made an\n  SSO portal or a captive proxy in front of the instance look like an empty\n  library rather than like a failure.\n- **Untrusted content is marked.** Book descriptions come from metadata providers\n  and podcast summaries come from RSS feeds — third parties write them. Every\n  result carrying such content is labelled as data, not instructions.\n- **The API key is deleted from the environment** once the configuration has been\n  read, so it is not visible to child processes or in `/proc/<pid>/environ`.\n- **No redirects are followed** (`redirect: 'error'`), so the `Authorization`\n  header cannot be replayed against another host, and every request has a 15\n  second timeout.\n- **Ids are validated** before they enter a URL path.\n- **Upstream error bodies are sanitized**: HTML error pages are dropped, anything\n  else is truncated to 2000 characters.\n- **Progress updates send whitelisted fields only.** The Audiobookshelf endpoint\n  applies its payload to the progress record wholesale.\n- **What this server cannot do**, by design: no user management, no server\n  settings, no backups, no cache purging, no filesystem browsing, no library or\n  item deletion, no metadata rewriting, no file uploads.\n\nOne caveat that comes from Audiobookshelf itself: removing the _last_ entry from a\nplaylist deletes the playlist. `remove_items_from_playlist` says so in its result\nwhen it happens.\n\n## Documentation\n\nThe full guide, tool reference and security notes live at\n**[audiobookshelf-mcp.ni-c.de](https://audiobookshelf-mcp.ni-c.de)** (source in [`docs/`](docs/)).\n\n## Development\n\n```sh\nnpm install\nnpm run lint          # oxlint + prettier --check\nnpm run build         # tsc\nnpm test              # vitest\nnpm run test:coverage # with thresholds\nnpm run docs:tools    # regenerate docs/reference/tools.md from the registered tools\n```\n\nThe table above is hand-curated; the complete\n[tool reference](https://audiobookshelf-mcp.ni-c.de/reference/tools) with every\nparameter is generated from the code, and CI fails if the committed copy is stale.\nSee [CONTRIBUTING.md](CONTRIBUTING.md) for a throwaway Audiobookshelf you can\nsafely write to — the write tools change progress and bookmarks on the API key's\nown user, so don't develop against a library you care about.\n\nThe tool definitions were derived from the Audiobookshelf server source\n(`server/routers/ApiRouter.js` and the controllers) rather than from\n[api.audiobookshelf.org](https://api.audiobookshelf.org/), which is out of date in\nseveral places — the filter data endpoint is `/filterdata` not `/filter`, progress\nupdates are `PATCH /api/me/progress/:id` not `POST /api/me/progress`, and bookmarks\nlive under `/api/me/item/:id/bookmark`.\n\n## Releasing\n\nTag-driven, no manual publish step:\n\n1. Move the `[Unreleased]` entries into a new `## [x.y.z] - YYYY-MM-DD` section in\n   `CHANGELOG.md` and bump `package.json`.\n2. `npm run lint && npm run build && npm run test:coverage`.\n3. Commit, then a **signed annotated** tag: `git tag -s vx.y.z -m \"vx.y.z\"`.\n4. `git push origin main vx.y.z`.\n\n`release.yml` then runs the tests, publishes to npm with provenance via Trusted\nPublishing (no token secret involved), creates the GitHub release from the\nCHANGELOG section, and publishes to the\n[MCP registry](https://registry.modelcontextprotocol.io) as\n`io.github.ni-c/audiobookshelf-mcp`. `ci.yml` pushes the multi-arch image to GHCR\non the same tag.\n\nIf the registry step fails, fix it on `main` and dispatch the\n`Publish to MCP Registry` workflow — do **not** re-run the tag job, which would\ncheck out the old tree.\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/audiobookshelf-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": 24050,
  "sha": "aa2945c744a4ce34ab9aa670cf72fb3aeb91f6490324933e2c015e8caeda14f7",
  "repo_slug": "ni-c/audiobookshelf-mcp",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_ni_c_audiobookshelf_mcp_3fea3754/readme"
}