audiobookshelf-mcp 0.1.4 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +190 -35
  2. package/dist/api.d.ts +35 -5
  3. package/dist/api.js +112 -18
  4. package/dist/api.js.map +1 -1
  5. package/dist/config.d.ts +30 -0
  6. package/dist/config.js +55 -4
  7. package/dist/config.js.map +1 -1
  8. package/dist/filters.d.ts +3 -3
  9. package/dist/index.js +43 -3
  10. package/dist/index.js.map +1 -1
  11. package/dist/output-schema.d.ts +55 -0
  12. package/dist/output-schema.js +57 -0
  13. package/dist/output-schema.js.map +1 -0
  14. package/dist/result.d.ts +53 -5
  15. package/dist/result.js +182 -10
  16. package/dist/result.js.map +1 -1
  17. package/dist/server.d.ts +1 -1
  18. package/dist/server.js +41 -9
  19. package/dist/server.js.map +1 -1
  20. package/dist/shape.d.ts +1 -1
  21. package/dist/shape.js +27 -2
  22. package/dist/shape.js.map +1 -1
  23. package/dist/tools/annotations.d.ts +32 -0
  24. package/dist/tools/annotations.js +33 -0
  25. package/dist/tools/annotations.js.map +1 -0
  26. package/dist/tools/catalogue.d.ts +28 -0
  27. package/dist/tools/catalogue.js +84 -0
  28. package/dist/tools/catalogue.js.map +1 -0
  29. package/dist/tools/collections.d.ts +3 -3
  30. package/dist/tools/collections.js +184 -47
  31. package/dist/tools/collections.js.map +1 -1
  32. package/dist/tools/items.d.ts +1 -1
  33. package/dist/tools/items.js +19 -13
  34. package/dist/tools/items.js.map +1 -1
  35. package/dist/tools/libraries.d.ts +1 -1
  36. package/dist/tools/libraries.js +74 -41
  37. package/dist/tools/libraries.js.map +1 -1
  38. package/dist/tools/me.d.ts +1 -1
  39. package/dist/tools/me.js +48 -29
  40. package/dist/tools/me.js.map +1 -1
  41. package/dist/tools/playlists.d.ts +3 -3
  42. package/dist/tools/playlists.js +172 -42
  43. package/dist/tools/playlists.js.map +1 -1
  44. package/dist/tools/progress.d.ts +4 -4
  45. package/dist/tools/progress.js +121 -34
  46. package/dist/tools/progress.js.map +1 -1
  47. package/package.json +16 -11
  48. package/dist/confirm.d.ts +0 -36
  49. package/dist/confirm.js +0 -72
  50. package/dist/confirm.js.map +0 -1
  51. package/dist/hosts.d.ts +0 -22
  52. package/dist/hosts.js +0 -144
  53. package/dist/hosts.js.map +0 -1
package/README.md CHANGED
@@ -4,20 +4,25 @@
4
4
  [![npm version](https://img.shields.io/npm/v/audiobookshelf-mcp)](https://www.npmjs.com/package/audiobookshelf-mcp)
5
5
  [![npm downloads](https://img.shields.io/npm/dm/audiobookshelf-mcp)](https://www.npmjs.com/package/audiobookshelf-mcp)
6
6
  [![node](https://img.shields.io/node/v/audiobookshelf-mcp)](https://nodejs.org)
7
- [![Container](https://img.shields.io/badge/ghcr.io-ni--c%2Faudiobookshelf--mcp-2496ED?logo=docker&logoColor=white)](https://github.com/ni-c/audiobookshelf-mcp/pkgs/container/audiobookshelf-mcp)
8
7
  [![license](https://img.shields.io/npm/l/audiobookshelf-mcp)](LICENSE)
9
- [![Docs](https://img.shields.io/badge/docs-audiobookshelf--mcp.ni--c.de-4f46e5)](https://audiobookshelf-mcp.ni-c.de)
10
- [![Sponsor](https://img.shields.io/badge/sponsor-ni--c-ea4aaa?logo=githubsponsors&logoColor=white)](https://github.com/sponsors/ni-c)
8
+ [![container](https://img.shields.io/badge/ghcr.io-ni--c%2Faudiobookshelf--mcp-blue)](https://github.com/ni-c/audiobookshelf-mcp/pkgs/container/audiobookshelf-mcp)
9
+ [![docs](https://img.shields.io/badge/docs-audiobookshelf--mcp.ni--c.de-informational)](https://audiobookshelf-mcp.ni-c.de)
10
+ [![HTTP • via mcp-hub](https://img.shields.io/badge/HTTP-via%20mcp--hub-6f42c1)](https://mcp-hub.ni-c.de)
11
+ [![sponsor](https://img.shields.io/badge/sponsor-ni--c-ea4aaa?logo=githubsponsors&logoColor=white)](https://github.com/sponsors/ni-c)
11
12
 
12
- An [MCP](https://modelcontextprotocol.io) server for
13
+ A [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server for
13
14
  [Audiobookshelf](https://www.audiobookshelf.org/), the self-hosted audiobook and
14
- podcast server. It lets an AI assistant browse your libraries, answer questions
15
- about what you own and what you have listened to, and — unless you switch it off —
16
- keep your listening progress, bookmarks, collections and playlists up to date.
15
+ podcast server.
17
16
 
18
- 44 tools: 29 read, 15 write.
17
+ Lets MCP clients like Claude Code, Claude Desktop or Codex browse your libraries,
18
+ answer questions about what you own and what you have listened to, and — unless you
19
+ switch it off — keep your listening progress, bookmarks, collections and playlists up
20
+ to date: 44 tools, 29 read and 15 write.
19
21
 
20
- 📖 **Full documentation: <https://audiobookshelf-mcp.ni-c.de>**
22
+ Forty-four tools is the ceiling, not the floor: `AUDIOBOOKSHELF_ALLOW_TOOLS=essential`
23
+ registers a curated eight instead, and a model picks the right tool far more
24
+ reliably from eight than from forty-four — see
25
+ [choosing which tools load](#choosing-which-tools-load).
21
26
 
22
27
  <!-- <picture> is resolved against the colour scheme of the page showing it, so GitHub
23
28
  picks the variant that matches its own theme toggle. npm strips <picture> and
@@ -31,6 +36,21 @@ keep your listening progress, bookmarks, collections and playlists up to date.
31
36
 
32
37
  <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">
33
38
 
39
+ ## What makes it different
40
+
41
+ **Every response is a projection, not the raw object.** An expanded library item
42
+ carries every audio file, track and chapter with full ffprobe metadata. The media
43
+ tools answer with a compact shape instead, and `detail="full"` is there for when
44
+ the raw object really is what you want.
45
+
46
+ **Twenty-nine of the forty-four tools only read.** `AUDIOBOOKSHELF_READ_ONLY=true`
47
+ registers those and nothing else, so a write tool is absent from `tools/list`
48
+ rather than refused when it is called.
49
+
50
+ **The six tools that take something out ask a person first**, through MCP
51
+ elicitation — a dialog the model cannot answer on its behalf, falling back to a
52
+ single-use token bound to the exact targets where the client cannot show one.
53
+
34
54
  ## Requirements
35
55
 
36
56
  - Node.js 22 or newer
@@ -53,12 +73,39 @@ once, at creation.
53
73
  | `AUDIOBOOKSHELF_API_KEY` | yes | API key, sent as `Authorization: Bearer …` |
54
74
  | `AUDIOBOOKSHELF_READ_ONLY` | no | `true` registers only the 29 read tools |
55
75
  | `AUDIOBOOKSHELF_INSECURE_TLS` | no | `true` accepts self-signed certificates — scoped to this connection, not process-wide |
76
+ | `AUDIOBOOKSHELF_ALLOW_TOOLS` | no | Comma-separated tool names, `list_*` prefixes, or `essential` for a curated preset |
77
+ | `AUDIOBOOKSHELF_DENY_TOOLS` | no | Same syntax; removed from whatever `AUDIOBOOKSHELF_ALLOW_TOOLS` left |
78
+ | `ELICITATION` | no | `false` replaces the approval dialog with the two-call token. **Not prefixed** |
56
79
 
57
80
  The server starts without configuration: it completes the MCP handshake and lists
58
81
  its tools, and every call then fails with the setup instructions. That is
59
82
  deliberate, so registries and sandbox inspectors can introspect it.
60
83
 
61
- ## Install
84
+ ### Choosing which tools load
85
+
86
+ `AUDIOBOOKSHELF_ALLOW_TOOLS` and `AUDIOBOOKSHELF_DENY_TOOLS` take comma-separated tool names;
87
+ a trailing `*` matches a whole family. `essential` is a curated preset of
88
+ eight: `list_libraries`, `search_library`, `list_library_items`, `get_library_item`, `get_item_chapters`, `list_items_in_progress`, `get_media_progress`, `set_media_progress`.
89
+
90
+ ```sh
91
+ AUDIOBOOKSHELF_ALLOW_TOOLS=essential
92
+ AUDIOBOOKSHELF_ALLOW_TOOLS=search_library,get_library_item,set_media_progress
93
+ AUDIOBOOKSHELF_DENY_TOOLS=delete_*
94
+ ```
95
+
96
+ An entry that matches no tool aborts startup and names it, so a typo cannot
97
+ silently hide a tool — an absent tool is not something anyone traces back to an
98
+ environment variable. A filtered tool is never registered, so it is absent from
99
+ `tools/list` and unknown to `tools/call` alike, exactly like a write tool under
100
+ `AUDIOBOOKSHELF_READ_ONLY`.
101
+
102
+ If you run several of these servers at once, [mcp-hub](https://mcp-hub.ni-c.de)
103
+ is the other answer — its `/hub` endpoint replaces every server's tools with six
104
+ meta-tools.
105
+
106
+ ## Installation
107
+
108
+ ### Claude Code
62
109
 
63
110
  ```sh
64
111
  claude mcp add audiobookshelf \
@@ -67,7 +114,9 @@ claude mcp add audiobookshelf \
67
114
  -- npx -y audiobookshelf-mcp
68
115
  ```
69
116
 
70
- Claude Desktop (`claude_desktop_config.json`):
117
+ ### Claude Desktop
118
+
119
+ `claude_desktop_config.json`:
71
120
 
72
121
  ```json
73
122
  {
@@ -84,7 +133,9 @@ Claude Desktop (`claude_desktop_config.json`):
84
133
  }
85
134
  ```
86
135
 
87
- Codex (`~/.codex/config.toml`):
136
+ ### Codex
137
+
138
+ `~/.codex/config.toml`:
88
139
 
89
140
  ```toml
90
141
  [mcp_servers.audiobookshelf]
@@ -93,7 +144,9 @@ args = ["-y", "audiobookshelf-mcp"]
93
144
  env = { AUDIOBOOKSHELF_URL = "https://abs.example.com", AUDIOBOOKSHELF_API_KEY = "…" }
94
145
  ```
95
146
 
96
- Container (multi-arch, with SBOM and build provenance):
147
+ ### Docker
148
+
149
+ Multi-arch, with SBOM and build provenance:
97
150
 
98
151
  ```sh
99
152
  docker run -i --rm \
@@ -107,8 +160,59 @@ publish. More client recipes, including how to keep the key off the `docker run`
107
160
  command line, are in the
108
161
  [client guide](https://audiobookshelf-mcp.ni-c.de/guide/clients).
109
162
 
163
+ ### Through mcp-hub
164
+
165
+ A client that cannot spawn a local process — ChatGPT connectors, Claude on the web,
166
+ Cursor, LibreChat — reaches audiobookshelf-mcp through [mcp-hub](https://mcp-hub.ni-c.de): one
167
+ container serves many stdio MCP servers over Streamable HTTP, with an OAuth 2.1 login
168
+ behind a single password and long-lived tokens for the clients that cannot do OAuth. Its
169
+ `/hub` endpoint puts every server behind six meta-tools, so one connector reaches all of
170
+ them without N×tool schemas in the model's context, and it speaks both protocol revisions
171
+ — a question this server asks travels through it to the person at the far end.
172
+
173
+ Its `/config/mcp.json` uses Claude Code's format, so the entry is the one you already
174
+ have:
175
+
176
+ ```json
177
+ {
178
+ "mcpServers": {
179
+ "audiobookshelf": {
180
+ "command": "npx",
181
+ "args": ["-y", "audiobookshelf-mcp"],
182
+ "env": { "AUDIOBOOKSHELF_ALLOW_TOOLS": "essential" },
183
+ "denyTools": ["delete_*"]
184
+ }
185
+ }
186
+ }
187
+ ```
188
+
189
+ `allowTools` and `denyTools` there are the hub's **own** per-server filter, which is not
190
+ the same thing as `*_ALLOW_TOOLS` in `env` — the difference, and the mistake it invites,
191
+ are in the [client guide](https://audiobookshelf-mcp.ni-c.de/guide/clients#through-mcp-hub).
192
+
110
193
  ## Tools
111
194
 
195
+ Every tool declares an `outputSchema` and answers with `structuredContent`
196
+ alongside the text block, so a client can use the result without parsing prose.
197
+ Seven tools that answered with a sentence — _"Collection col_1 deleted."_ — now
198
+ answer with the fields as well.
199
+
200
+ The tools that report library metadata carry `untrusted: true` and
201
+ `source: "audiobookshelf"` as fields: book descriptions pulled from metadata
202
+ providers, podcast feed summaries and episode titles are all written by someone
203
+ else. The rest are without it — an id this server was given, the account it
204
+ authenticates as, counters the instance keeps about itself.
205
+
206
+ The documents are described as open objects with the top-level keys this server
207
+ builds. `detail: "full"` hands the API record back whole, so the same tool
208
+ answers with far more keys than it names — and the SDK validates each result
209
+ against its schema before it goes out, which is exactly why a strict shape would
210
+ be wrong here.
211
+
212
+ `get_personalized_shelves` answers `{items: [...]}` rather than the bare array
213
+ the API sends: a schema whose root is an array is served to a 2025-era client
214
+ rewritten as `{result: …}`, so it would otherwise answer in two shapes.
215
+
112
216
  ### Reading
113
217
 
114
218
  | Tool | What it does |
@@ -140,23 +244,45 @@ command line, are in the
140
244
 
141
245
  ### Writing
142
246
 
143
- | Tool | What it does |
144
- | --------------------------------------------------------------- | ----------------------------------------------------------------------- |
145
- | `set_media_progress` | Set position, mark finished or unfinished, hide from Continue Listening |
146
- | `delete_media_progress` | Delete a progress record — needs a confirmation token |
147
- | `create_bookmark` / `update_bookmark` / `delete_bookmark` | Named positions in a book |
148
- | `create_collection` / `update_collection` / `delete_collection` | Collections; delete needs a confirmation token |
149
- | `add_books_to_collection` / `remove_books_from_collection` | Collection membership |
150
- | `create_playlist` / `update_playlist` / `delete_playlist` | Playlists; delete needs a confirmation token |
151
- | `add_items_to_playlist` / `remove_items_from_playlist` | Playlist membership |
247
+ | Tool | What it does |
248
+ | ------------------------------------------------------------------ | ----------------------------------------------------------------------- |
249
+ | `set_media_progress` | Set position, mark finished or unfinished, hide from Continue Listening |
250
+ | `delete_media_progress` 👤 | Delete a progress record — the listening history of that item |
251
+ | `create_bookmark` / `update_bookmark` / `delete_bookmark` 👤 | Named positions in a book |
252
+ | `create_collection` / `update_collection` / `delete_collection` 👤 | Collections |
253
+ | `add_books_to_collection` / `remove_books_from_collection` 👤 | Collection membership |
254
+ | `create_playlist` / `update_playlist` / `delete_playlist` 👤 | Playlists |
255
+ | `add_items_to_playlist` / `remove_items_from_playlist` 👤 | Playlist membership |
256
+
257
+ 👤 asks a person through MCP elicitation · falls back to a two-call
258
+ `confirm_token` where the client cannot show a dialog.
152
259
 
153
260
  ### Response size
154
261
 
155
262
  Audiobookshelf returns very large objects — an expanded library item carries every
156
263
  audio file, track and chapter with full ffprobe metadata. Every tool that returns
157
264
  media therefore answers with a compact projection by default and accepts
158
- `detail: "full"` for the raw object. List tools are capped at 100 entries per call
159
- and say how to page on when more match.
265
+ `detail: "full"` for the raw object.
266
+
267
+ Three bounds, because one is not enough:
268
+
269
+ - **A response ceiling of 5 MB.** `content-length` is checked before a byte is
270
+ read and a chunked body is counted while reading, so an oversized answer is
271
+ refused rather than parsed.
272
+ - **A result ceiling of 100 000 bytes**, applied in `jsonResult` — so it covers
273
+ `detail: "full"` too. Whole entries are dropped, never characters: a truncated
274
+ document is not a smaller answer, it is an unparseable one. The result then
275
+ carries a `truncated` block naming what to call instead.
276
+ - **A cap on embedded members.** A compact collection or playlist embeds the
277
+ first 25 of its books or entries and reports the real count; `get_collection`
278
+ and `get_playlist` return the whole membership for one of them.
279
+
280
+ `list_library_items` pages properly, with `limit` and `page`. Seven listing tools
281
+ have neither — `list_libraries`, `list_authors`, `list_tags`, `list_genres`,
282
+ `list_collections`, `list_playlists` and `list_bookmarks` — because the
283
+ Audiobookshelf routes behind them return everything in one answer and take no
284
+ paging parameters. `library_id` narrows the two collection routes; the rest are
285
+ bounded by the ceilings above.
160
286
 
161
287
  ### Filtering
162
288
 
@@ -171,20 +297,44 @@ filter_group="progress", filter_value="finished" | "in-progress" | "not-started"
171
297
  filter_group="issues" (standalone, no value)
172
298
  ```
173
299
 
300
+ ## Not exposed, on purpose
301
+
302
+ **No playback.** Pausing, seeking and playing are a session state machine that
303
+ belongs in a real client. `set_media_progress` covers "mark this finished" and
304
+ "jump me to chapter 12"; your phone does the playing.
305
+
306
+ **No administration, even with an admin key.** There is no tool for user
307
+ management, server settings, backups, cache purging, filesystem browsing, library
308
+ or item deletion, metadata rewriting or file uploads. Those endpoints exist in
309
+ Audiobookshelf; they are simply not wired up here, because the blast radius of a
310
+ confused or manipulated model is bounded by the tool list.
311
+
174
312
  ## Safety
175
313
 
176
314
  - **Read-only mode.** `AUDIOBOOKSHELF_READ_ONLY=true` does not register the write
177
315
  tools at all, rather than refusing them at call time.
178
- - **Confirmation tokens.** `delete_collection`, `delete_playlist` and
179
- `delete_media_progress` answer the first call with a single-use token that is
180
- bound to the target id and expires after five minutes; only a second call
181
- carrying that token performs the deletion. A plain `confirm: true` flag could be
182
- set by the model on the first try, or be talked into it by text coming out of
183
- the library. Operations that are cheap to undo removing an item from a
184
- collection, deleting a bookmark are marked destructive but do not require a
185
- token.
316
+ - **A person is asked, not just told.** The eight tools that can take something
317
+ out the three deletes, `delete_bookmark`, `remove_books_from_collection`,
318
+ `remove_items_from_playlist`, and `update_collection` / `update_playlist` when
319
+ they are asked to reorder, which replaces an order nobody can reconstruct
320
+ raise a real dialog through MCP elicitation,
321
+ which the model cannot answer on its behalf. A plain `confirm: true` flag could
322
+ be set by the model on the first try, or be talked into it by text coming out
323
+ of the library.
324
+
325
+ Where the client cannot show a dialog they fall back to a single-use token
326
+ bound to the exact targets and expiring after five minutes. That fallback
327
+ proves the call was made twice with the same arguments and nothing more, and
328
+ the text says so rather than implying somebody approved. `ELICITATION=false`
329
+ takes it deliberately; it never removes the guard. See
330
+ [Asking a person](https://audiobookshelf-mcp.ni-c.de/guide/approval).
331
+
186
332
  - **Confirmation prompts never quote API content.** Collection and playlist names
187
- are user-supplied text and are read by a model, so the prompts name only ids.
333
+ are user-supplied text and are read by a model, so the prompts name ids and
334
+ counts only.
335
+ - **A 200 that is not JSON is an error.** Returning the body as a string made an
336
+ SSO portal or a captive proxy in front of the instance look like an empty
337
+ library rather than like a failure.
188
338
  - **Untrusted content is marked.** Book descriptions come from metadata providers
189
339
  and podcast summaries come from RSS feeds — third parties write them. Every
190
340
  result carrying such content is labelled as data, not instructions.
@@ -206,11 +356,16 @@ One caveat that comes from Audiobookshelf itself: removing the _last_ entry from
206
356
  playlist deletes the playlist. `remove_items_from_playlist` says so in its result
207
357
  when it happens.
208
358
 
359
+ ## Documentation
360
+
361
+ The full guide, tool reference and security notes live at
362
+ **[audiobookshelf-mcp.ni-c.de](https://audiobookshelf-mcp.ni-c.de)** (source in [`docs/`](docs/)).
363
+
209
364
  ## Development
210
365
 
211
366
  ```sh
212
367
  npm install
213
- npm run lint # eslint + prettier --check
368
+ npm run lint # oxlint + prettier --check
214
369
  npm run build # tsc
215
370
  npm test # vitest
216
371
  npm run test:coverage # with thresholds
@@ -261,4 +416,4 @@ rather than a public issue; the policy is in [SECURITY.md](SECURITY.md).
261
416
 
262
417
  ## License
263
418
 
264
- MIT
419
+ [MIT](LICENSE) © Willi Thiel
package/dist/api.d.ts CHANGED
@@ -4,6 +4,36 @@ export declare class AudiobookshelfApiError extends Error {
4
4
  readonly body: string;
5
5
  constructor(status: number, body: string, method: string, path: string);
6
6
  }
7
+ /** Thrown when a response is larger than this server is willing to read. */
8
+ export declare class ResponseTooLargeError extends Error {
9
+ constructor(path: string, limit: number);
10
+ }
11
+ /**
12
+ * Thrown when a 200 carries something other than JSON.
13
+ *
14
+ * Returning the body instead would send an HTML login page into `listFrom`,
15
+ * which finds neither an array nor an envelope and answers `[]` — so
16
+ * `list_libraries` reports "you have no libraries" where the truth is that an
17
+ * SSO portal or a misconfigured reverse proxy answered instead of the API. A
18
+ * swallowed error replaced by a plausible wrong answer is worse than an error.
19
+ */
20
+ export declare class UnexpectedContentTypeError extends Error {
21
+ constructor(path: string, contentType: string);
22
+ }
23
+ export interface RequestOptions {
24
+ /**
25
+ * Accept a body that is not JSON.
26
+ *
27
+ * Only for the five routes that are known to answer `200 text/plain "OK"`
28
+ * rather than a document — verified against 2.29.0:
29
+ * `DELETE /api/collections/{id}`, `DELETE /api/playlists/{id}`,
30
+ * `PATCH /api/me/progress/{id}`, `DELETE /api/me/progress/{id}` and
31
+ * `DELETE /api/me/item/{id}/bookmark/{time}`. Every one of them is a mutation
32
+ * whose caller ignores the value, so the honest thing is to say so at the
33
+ * call site rather than to weaken the check for everything.
34
+ */
35
+ text?: boolean;
36
+ }
7
37
  /** Minimal client for the Audiobookshelf REST API. */
8
38
  export declare class AudiobookshelfApi {
9
39
  private readonly config;
@@ -15,11 +45,11 @@ export declare class AudiobookshelfApi {
15
45
  */
16
46
  private readonly insecureDispatcher?;
17
47
  constructor(config: Config);
18
- request(method: string, path: string, body?: unknown): Promise<unknown>;
19
- get(path: string): Promise<unknown>;
20
- post(path: string, body?: unknown): Promise<unknown>;
21
- patch(path: string, body?: unknown): Promise<unknown>;
22
- delete(path: string): Promise<unknown>;
48
+ request(method: string, path: string, body?: unknown, options?: RequestOptions): Promise<unknown>;
49
+ get(path: string, options?: RequestOptions): Promise<unknown>;
50
+ post(path: string, body?: unknown, options?: RequestOptions): Promise<unknown>;
51
+ patch(path: string, body?: unknown, options?: RequestOptions): Promise<unknown>;
52
+ delete(path: string, options?: RequestOptions): Promise<unknown>;
23
53
  }
24
54
  /**
25
55
  * Guards an id that ends up in a URL path. Path traversal here would let a
package/dist/api.js CHANGED
@@ -1,6 +1,17 @@
1
1
  import { Agent, fetch as undiciFetch, } from 'undici';
2
2
  import { missingConfigKeys, missingConfigMessage, } from './config.js';
3
3
  const REQUEST_TIMEOUT_MS = 15_000;
4
+ /**
5
+ * Ceiling on a single upstream response.
6
+ *
7
+ * Not a theoretical number. `/api/collections` has no pagination at all, and a
8
+ * collection carries every one of its books expanded — a shared server with
9
+ * forty collections of three hundred books answers in double-digit megabytes,
10
+ * and `response.text()` followed by `JSON.parse` holds roughly three copies of
11
+ * that at once. The documented 95 kB of `/api/me/listening-stats` is the
12
+ * harmless end of the same range.
13
+ */
14
+ const MAX_RESPONSE_BYTES = 5 * 1024 * 1024;
4
15
  export class AudiobookshelfApiError extends Error {
5
16
  status;
6
17
  body;
@@ -11,6 +22,74 @@ export class AudiobookshelfApiError extends Error {
11
22
  this.name = 'AudiobookshelfApiError';
12
23
  }
13
24
  }
25
+ /** Thrown when a response is larger than this server is willing to read. */
26
+ export class ResponseTooLargeError extends Error {
27
+ constructor(path, limit) {
28
+ super(`the Audiobookshelf response for ${path} exceeds the ` +
29
+ `${Math.round(limit / 1024 / 1024)} MB ceiling and was not read. ` +
30
+ 'Narrow the request — most listing tools take limit and page, and ' +
31
+ 'library_id restricts a server-wide listing to one library.');
32
+ this.name = 'ResponseTooLargeError';
33
+ }
34
+ }
35
+ /**
36
+ * Thrown when a 200 carries something other than JSON.
37
+ *
38
+ * Returning the body instead would send an HTML login page into `listFrom`,
39
+ * which finds neither an array nor an envelope and answers `[]` — so
40
+ * `list_libraries` reports "you have no libraries" where the truth is that an
41
+ * SSO portal or a misconfigured reverse proxy answered instead of the API. A
42
+ * swallowed error replaced by a plausible wrong answer is worse than an error.
43
+ */
44
+ export class UnexpectedContentTypeError extends Error {
45
+ constructor(path, contentType) {
46
+ super(`Audiobookshelf answered ${path} with "${contentType || 'no content type'}" ` +
47
+ 'instead of JSON. A 200 that is not JSON usually means something in ' +
48
+ 'front of the instance answered instead of the API — an SSO portal, a ' +
49
+ 'captive proxy or a login page. Check AUDIOBOOKSHELF_URL.');
50
+ this.name = 'UnexpectedContentTypeError';
51
+ }
52
+ }
53
+ /**
54
+ * Reads a response body with a hard byte ceiling.
55
+ *
56
+ * Both halves matter: `content-length` catches an oversized answer before a
57
+ * single byte is read, and the streaming count catches a chunked response,
58
+ * which declares no length at all.
59
+ */
60
+ async function readCapped(response, maxBytes, allowTruncation) {
61
+ const declared = Number(response.headers.get('content-length'));
62
+ if (Number.isFinite(declared) && declared > maxBytes && !allowTruncation) {
63
+ // Nothing has been read yet, so the body can simply be discarded.
64
+ await response.body?.cancel();
65
+ return { text: '', truncated: true };
66
+ }
67
+ const body = response.body;
68
+ if (!body)
69
+ return { text: '', truncated: false };
70
+ const reader = body.getReader();
71
+ const chunks = [];
72
+ let total = 0;
73
+ let truncated = false;
74
+ for (;;) {
75
+ const { done, value } = await reader.read();
76
+ if (done)
77
+ break;
78
+ if (value === undefined)
79
+ continue;
80
+ if (total + value.byteLength > maxBytes) {
81
+ // `maxBytes - total` is exactly the remaining budget, and the `>` above
82
+ // makes an exactly-maxBytes response legal rather than truncated.
83
+ chunks.push(value.subarray(0, maxBytes - total));
84
+ truncated = true;
85
+ await reader.cancel();
86
+ break;
87
+ }
88
+ chunks.push(value);
89
+ total += value.byteLength;
90
+ }
91
+ return { text: Buffer.concat(chunks).toString('utf8'), truncated };
92
+ }
14
93
  /** Minimal client for the Audiobookshelf REST API. */
15
94
  export class AudiobookshelfApi {
16
95
  config;
@@ -30,7 +109,7 @@ export class AudiobookshelfApi {
30
109
  });
31
110
  }
32
111
  }
33
- async request(method, path, body) {
112
+ async request(method, path, body, options = {}) {
34
113
  // The credentials are only required here, not at startup, so the server can
35
114
  // still be started and introspected without them.
36
115
  const missing = missingConfigKeys(this.config);
@@ -62,32 +141,47 @@ export class AudiobookshelfApi {
62
141
  dispatcher: this.insecureDispatcher,
63
142
  })
64
143
  : await fetch(url, init);
65
- const text = await response.text();
144
+ // An error body is only ever quoted back after `sanitizeErrorBody` cuts it
145
+ // to 2 000 characters, so truncating it costs nothing and keeps the status
146
+ // code — which is the diagnostic — instead of replacing it with a size
147
+ // complaint. A successful body cannot be truncated: half a JSON document is
148
+ // not a smaller answer.
149
+ const { text, truncated } = await readCapped(response, MAX_RESPONSE_BYTES, !response.ok);
66
150
  if (!response.ok) {
67
151
  throw new AudiobookshelfApiError(response.status, text, method, path);
68
152
  }
153
+ if (truncated) {
154
+ throw new ResponseTooLargeError(path, MAX_RESPONSE_BYTES);
155
+ }
156
+ // A body-less success. Several routes answer 200 or 204 with nothing at
157
+ // all, and "nothing" has no content type to check.
158
+ if (text.length === 0)
159
+ return undefined;
160
+ // A route that is known to answer text. Its caller ignores the value.
161
+ if (options.text)
162
+ return text;
69
163
  const contentType = response.headers.get('content-type') ?? '';
70
- if (contentType.includes('application/json')) {
71
- try {
72
- return JSON.parse(text);
73
- }
74
- catch {
75
- return text;
76
- }
164
+ if (!contentType.includes('application/json')) {
165
+ throw new UnexpectedContentTypeError(path, contentType);
166
+ }
167
+ try {
168
+ return JSON.parse(text);
169
+ }
170
+ catch {
171
+ throw new UnexpectedContentTypeError(path, `${contentType} (unparseable)`);
77
172
  }
78
- return text;
79
173
  }
80
- get(path) {
81
- return this.request('GET', path);
174
+ get(path, options) {
175
+ return this.request('GET', path, undefined, options);
82
176
  }
83
- post(path, body) {
84
- return this.request('POST', path, body);
177
+ post(path, body, options) {
178
+ return this.request('POST', path, body, options);
85
179
  }
86
- patch(path, body) {
87
- return this.request('PATCH', path, body);
180
+ patch(path, body, options) {
181
+ return this.request('PATCH', path, body, options);
88
182
  }
89
- delete(path) {
90
- return this.request('DELETE', path);
183
+ delete(path, options) {
184
+ return this.request('DELETE', path, undefined, options);
91
185
  }
92
186
  }
93
187
  /**
package/dist/api.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"}
package/dist/config.d.ts CHANGED
@@ -8,11 +8,41 @@ export interface Config {
8
8
  apiKey: string | undefined;
9
9
  insecureTls: boolean;
10
10
  readOnly: boolean;
11
+ /**
12
+ * Whether a client that *can* show a dialog is asked before a guarded tool
13
+ * acts. `ELICITATION=false` turns the dialog off — the guard stays and falls
14
+ * back to the two-call token, so there is no setting in which a guarded call
15
+ * goes unannounced.
16
+ */
17
+ elicitation: boolean;
18
+ /**
19
+ * Raw value of `AUDIOBOOKSHELF_ALLOW_TOOLS` — comma-separated tool names, `list_*`
20
+ * prefixes, or `essential`. Kept unparsed on purpose: this file is a mirror of
21
+ * the environment, and the names can only be checked against the tool
22
+ * catalogue, which `buildToolFilter` does.
23
+ */
24
+ allowTools: string | undefined;
25
+ /** Raw value of `AUDIOBOOKSHELF_DENY_TOOLS`, same shape, subtracted from the above. */
26
+ denyTools: string | undefined;
11
27
  }
12
28
  /** Shown when the configuration is incomplete — at startup and on every API call. */
13
29
  export declare function missingConfigMessage(missing: string[]): string;
14
30
  /** Names of the required environment variables that are unset in `config`. */
15
31
  export declare function missingConfigKeys(config: Config): string[];
32
+ /**
33
+ * Reads `ELICITATION` — deliberately unprefixed, and deliberately fatal on
34
+ * anything it does not recognise.
35
+ *
36
+ * Unprefixed: environment variables are process-wide, so this is one switch for
37
+ * every server in the same environment. That is also its risk, which is why a
38
+ * server started with it off says so on its startup line.
39
+ *
40
+ * Fatal: this is the first variable of the family that defaults to *on*. The
41
+ * others fail open on a typo, which is the safe direction for them. Here a typo
42
+ * would leave the dialog running while the operator believes it is off — and an
43
+ * operator who believes that has no way to find out.
44
+ */
45
+ export declare function parseElicitation(raw: string | undefined): boolean;
16
46
  /**
17
47
  * Reads the configuration from environment variables.
18
48
  *