audiobookshelf-mcp 0.1.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.
- package/LICENSE +21 -0
- package/README.md +255 -0
- package/dist/api.d.ts +38 -0
- package/dist/api.js +122 -0
- package/dist/api.js.map +1 -0
- package/dist/config.d.ts +24 -0
- package/dist/config.js +78 -0
- package/dist/config.js.map +1 -0
- package/dist/confirm.d.ts +36 -0
- package/dist/confirm.js +72 -0
- package/dist/confirm.js.map +1 -0
- package/dist/filters.d.ts +32 -0
- package/dist/filters.js +81 -0
- package/dist/filters.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/result.d.ts +18 -0
- package/dist/result.js +71 -0
- package/dist/result.js.map +1 -0
- package/dist/schema.d.ts +12 -0
- package/dist/schema.js +36 -0
- package/dist/schema.js.map +1 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.js +43 -0
- package/dist/server.js.map +1 -0
- package/dist/shape.d.ts +72 -0
- package/dist/shape.js +397 -0
- package/dist/shape.js.map +1 -0
- package/dist/tools/collections.d.ts +5 -0
- package/dist/tools/collections.js +183 -0
- package/dist/tools/collections.js.map +1 -0
- package/dist/tools/items.d.ts +3 -0
- package/dist/tools/items.js +117 -0
- package/dist/tools/items.js.map +1 -0
- package/dist/tools/libraries.d.ts +3 -0
- package/dist/tools/libraries.js +337 -0
- package/dist/tools/libraries.js.map +1 -0
- package/dist/tools/me.d.ts +3 -0
- package/dist/tools/me.js +141 -0
- package/dist/tools/me.js.map +1 -0
- package/dist/tools/playlists.d.ts +5 -0
- package/dist/tools/playlists.js +200 -0
- package/dist/tools/playlists.js.map +1 -0
- package/dist/tools/progress.d.ts +5 -0
- package/dist/tools/progress.js +161 -0
- package/dist/tools/progress.js.map +1 -0
- package/package.json +61 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Willi Thiel
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
# audiobookshelf-mcp
|
|
2
|
+
|
|
3
|
+
[](https://github.com/ni-c/audiobookshelf-mcp/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/audiobookshelf-mcp)
|
|
5
|
+
[](https://www.npmjs.com/package/audiobookshelf-mcp)
|
|
6
|
+
[](https://nodejs.org)
|
|
7
|
+
[](https://github.com/ni-c/audiobookshelf-mcp/pkgs/container/audiobookshelf-mcp)
|
|
8
|
+
[](LICENSE)
|
|
9
|
+
[](https://audiobookshelf-mcp.ni-c.de)
|
|
10
|
+
|
|
11
|
+
An [MCP](https://modelcontextprotocol.io) server for
|
|
12
|
+
[Audiobookshelf](https://www.audiobookshelf.org/), the self-hosted audiobook and
|
|
13
|
+
podcast server. It lets an AI assistant browse your libraries, answer questions
|
|
14
|
+
about what you own and what you have listened to, and — unless you switch it off —
|
|
15
|
+
keep your listening progress, bookmarks, collections and playlists up to date.
|
|
16
|
+
|
|
17
|
+
44 tools: 29 read, 15 write.
|
|
18
|
+
|
|
19
|
+
📖 **Full documentation: <https://audiobookshelf-mcp.ni-c.de>**
|
|
20
|
+
|
|
21
|
+
<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">
|
|
22
|
+
|
|
23
|
+
<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">
|
|
24
|
+
|
|
25
|
+
## Requirements
|
|
26
|
+
|
|
27
|
+
- Node.js 22 or newer
|
|
28
|
+
- Audiobookshelf **2.26.0 or newer** — earlier versions have no API keys
|
|
29
|
+
- An Audiobookshelf API key
|
|
30
|
+
|
|
31
|
+
## Getting an API key
|
|
32
|
+
|
|
33
|
+
API keys are managed by an admin under **Settings → Users → API Keys**. A key acts
|
|
34
|
+
on behalf of exactly one Audiobookshelf user and inherits that user's permissions,
|
|
35
|
+
so a key issued for a normal account cannot see libraries that account cannot see,
|
|
36
|
+
and cannot delete anything unless that account may delete. The key is shown only
|
|
37
|
+
once, at creation.
|
|
38
|
+
|
|
39
|
+
## Configuration
|
|
40
|
+
|
|
41
|
+
| Variable | Required | Description |
|
|
42
|
+
| ----------------------------- | -------- | --------------------------------------------------------------------------------------- |
|
|
43
|
+
| `AUDIOBOOKSHELF_URL` | yes | Base URL of the instance, e.g. `https://abs.example.com`. Must not contain credentials. |
|
|
44
|
+
| `AUDIOBOOKSHELF_API_KEY` | yes | API key, sent as `Authorization: Bearer …` |
|
|
45
|
+
| `AUDIOBOOKSHELF_READ_ONLY` | no | `true` registers only the 29 read tools |
|
|
46
|
+
| `AUDIOBOOKSHELF_INSECURE_TLS` | no | `true` accepts self-signed certificates — scoped to this connection, not process-wide |
|
|
47
|
+
|
|
48
|
+
The server starts without configuration: it completes the MCP handshake and lists
|
|
49
|
+
its tools, and every call then fails with the setup instructions. That is
|
|
50
|
+
deliberate, so registries and sandbox inspectors can introspect it.
|
|
51
|
+
|
|
52
|
+
## Install
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
claude mcp add audiobookshelf \
|
|
56
|
+
-e AUDIOBOOKSHELF_URL=https://abs.example.com \
|
|
57
|
+
-e AUDIOBOOKSHELF_API_KEY=… \
|
|
58
|
+
-- npx -y audiobookshelf-mcp
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Claude Desktop (`claude_desktop_config.json`):
|
|
62
|
+
|
|
63
|
+
```json
|
|
64
|
+
{
|
|
65
|
+
"mcpServers": {
|
|
66
|
+
"audiobookshelf": {
|
|
67
|
+
"command": "npx",
|
|
68
|
+
"args": ["-y", "audiobookshelf-mcp"],
|
|
69
|
+
"env": {
|
|
70
|
+
"AUDIOBOOKSHELF_URL": "https://abs.example.com",
|
|
71
|
+
"AUDIOBOOKSHELF_API_KEY": "…"
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Codex (`~/.codex/config.toml`):
|
|
79
|
+
|
|
80
|
+
```toml
|
|
81
|
+
[mcp_servers.audiobookshelf]
|
|
82
|
+
command = "npx"
|
|
83
|
+
args = ["-y", "audiobookshelf-mcp"]
|
|
84
|
+
env = { AUDIOBOOKSHELF_URL = "https://abs.example.com", AUDIOBOOKSHELF_API_KEY = "…" }
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Container (multi-arch, with SBOM and build provenance):
|
|
88
|
+
|
|
89
|
+
```sh
|
|
90
|
+
docker run -i --rm \
|
|
91
|
+
-e AUDIOBOOKSHELF_URL=https://abs.example.com \
|
|
92
|
+
-e AUDIOBOOKSHELF_API_KEY=… \
|
|
93
|
+
ghcr.io/ni-c/audiobookshelf-mcp
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`-i` is required — the protocol runs over stdin and stdout. There is no port to
|
|
97
|
+
publish. More client recipes, including how to keep the key off the `docker run`
|
|
98
|
+
command line, are in the
|
|
99
|
+
[client guide](https://audiobookshelf-mcp.ni-c.de/guide/clients).
|
|
100
|
+
|
|
101
|
+
## Tools
|
|
102
|
+
|
|
103
|
+
### Reading
|
|
104
|
+
|
|
105
|
+
| Tool | What it does |
|
|
106
|
+
| ------------------------------------- | --------------------------------------------------------------------------------------------------- |
|
|
107
|
+
| `list_libraries` | The accessible libraries with id, name and media type — the entry point |
|
|
108
|
+
| `get_library` | One library with its folders and settings |
|
|
109
|
+
| `get_library_stats` | Item, author and genre counts, total duration and size |
|
|
110
|
+
| `get_library_filter_data` | The filterable values of a library: authors, genres, tags, series, narrators, languages, publishers |
|
|
111
|
+
| `list_library_items` | Items of a library, paginated, sortable, filterable |
|
|
112
|
+
| `search_library` | Full-text search across books, podcasts, series, authors, narrators and tags |
|
|
113
|
+
| `get_personalized_shelves` | The home screen shelves: Continue Listening, Recently Added, … |
|
|
114
|
+
| `list_series` / `get_series` | Series with book count and total duration |
|
|
115
|
+
| `list_authors` / `get_author` | Authors, optionally with their items |
|
|
116
|
+
| `list_tags` / `list_genres` | All tags / genres used on the server |
|
|
117
|
+
| `get_library_item` | One book or podcast with metadata, tags and your progress |
|
|
118
|
+
| `get_item_chapters` | The chapter list of a book, separate because it can be long |
|
|
119
|
+
| `get_podcast_episode` | One episode with publication date, duration and description |
|
|
120
|
+
| `list_recent_episodes` | Newest episodes of a podcast library |
|
|
121
|
+
| `get_me` | The user the API key acts for, with permissions and libraries |
|
|
122
|
+
| `list_items_in_progress` | Started but unfinished items across all libraries |
|
|
123
|
+
| `get_media_progress` | Position, percentage and finished state for one item |
|
|
124
|
+
| `get_listening_stats` | Total time, time per day and per weekday, most listened items |
|
|
125
|
+
| `get_year_stats` | The "year in review" figures for one calendar year |
|
|
126
|
+
| `list_listening_sessions` | Playback sessions with device, position and time listened |
|
|
127
|
+
| `list_bookmarks` | Bookmarks, all of them or those of one item |
|
|
128
|
+
| `list_collections` / `get_collection` | Collections — shared, ordered groups of books |
|
|
129
|
+
| `list_playlists` / `get_playlist` | Playlists — private per user, books or episodes |
|
|
130
|
+
| `get_server_status` | Version and initialization state of the server |
|
|
131
|
+
|
|
132
|
+
### Writing
|
|
133
|
+
|
|
134
|
+
| Tool | What it does |
|
|
135
|
+
| --------------------------------------------------------------- | ----------------------------------------------------------------------- |
|
|
136
|
+
| `set_media_progress` | Set position, mark finished or unfinished, hide from Continue Listening |
|
|
137
|
+
| `delete_media_progress` | Delete a progress record — needs a confirmation token |
|
|
138
|
+
| `create_bookmark` / `update_bookmark` / `delete_bookmark` | Named positions in a book |
|
|
139
|
+
| `create_collection` / `update_collection` / `delete_collection` | Collections; delete needs a confirmation token |
|
|
140
|
+
| `add_books_to_collection` / `remove_books_from_collection` | Collection membership |
|
|
141
|
+
| `create_playlist` / `update_playlist` / `delete_playlist` | Playlists; delete needs a confirmation token |
|
|
142
|
+
| `add_items_to_playlist` / `remove_items_from_playlist` | Playlist membership |
|
|
143
|
+
|
|
144
|
+
### Response size
|
|
145
|
+
|
|
146
|
+
Audiobookshelf returns very large objects — an expanded library item carries every
|
|
147
|
+
audio file, track and chapter with full ffprobe metadata. Every tool that returns
|
|
148
|
+
media therefore answers with a compact projection by default and accepts
|
|
149
|
+
`detail: "full"` for the raw object. List tools are capped at 100 entries per call
|
|
150
|
+
and say how to page on when more match.
|
|
151
|
+
|
|
152
|
+
### Filtering
|
|
153
|
+
|
|
154
|
+
`list_library_items` takes `filter_group` plus `filter_value` and builds the
|
|
155
|
+
base64-encoded `filter` parameter the API expects. The valid values come from
|
|
156
|
+
`get_library_filter_data`. A valued group without a value is rejected, because
|
|
157
|
+
Audiobookshelf would silently answer with the _unfiltered_ library instead.
|
|
158
|
+
|
|
159
|
+
```
|
|
160
|
+
filter_group="authors", filter_value="<author id>"
|
|
161
|
+
filter_group="progress", filter_value="finished" | "in-progress" | "not-started" | "not-finished"
|
|
162
|
+
filter_group="issues" (standalone, no value)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Safety
|
|
166
|
+
|
|
167
|
+
- **Read-only mode.** `AUDIOBOOKSHELF_READ_ONLY=true` does not register the write
|
|
168
|
+
tools at all, rather than refusing them at call time.
|
|
169
|
+
- **Confirmation tokens.** `delete_collection`, `delete_playlist` and
|
|
170
|
+
`delete_media_progress` answer the first call with a single-use token that is
|
|
171
|
+
bound to the target id and expires after five minutes; only a second call
|
|
172
|
+
carrying that token performs the deletion. A plain `confirm: true` flag could be
|
|
173
|
+
set by the model on the first try, or be talked into it by text coming out of
|
|
174
|
+
the library. Operations that are cheap to undo — removing an item from a
|
|
175
|
+
collection, deleting a bookmark — are marked destructive but do not require a
|
|
176
|
+
token.
|
|
177
|
+
- **Confirmation prompts never quote API content.** Collection and playlist names
|
|
178
|
+
are user-supplied text and are read by a model, so the prompts name only ids.
|
|
179
|
+
- **Untrusted content is marked.** Book descriptions come from metadata providers
|
|
180
|
+
and podcast summaries come from RSS feeds — third parties write them. Every
|
|
181
|
+
result carrying such content is labelled as data, not instructions.
|
|
182
|
+
- **The API key is deleted from the environment** once the configuration has been
|
|
183
|
+
read, so it is not visible to child processes or in `/proc/<pid>/environ`.
|
|
184
|
+
- **No redirects are followed** (`redirect: 'error'`), so the `Authorization`
|
|
185
|
+
header cannot be replayed against another host, and every request has a 15
|
|
186
|
+
second timeout.
|
|
187
|
+
- **Ids are validated** before they enter a URL path.
|
|
188
|
+
- **Upstream error bodies are sanitized**: HTML error pages are dropped, anything
|
|
189
|
+
else is truncated to 2000 characters.
|
|
190
|
+
- **Progress updates send whitelisted fields only.** The Audiobookshelf endpoint
|
|
191
|
+
applies its payload to the progress record wholesale.
|
|
192
|
+
- **What this server cannot do**, by design: no user management, no server
|
|
193
|
+
settings, no backups, no cache purging, no filesystem browsing, no library or
|
|
194
|
+
item deletion, no metadata rewriting, no file uploads.
|
|
195
|
+
|
|
196
|
+
One caveat that comes from Audiobookshelf itself: removing the _last_ entry from a
|
|
197
|
+
playlist deletes the playlist. `remove_items_from_playlist` says so in its result
|
|
198
|
+
when it happens.
|
|
199
|
+
|
|
200
|
+
## Development
|
|
201
|
+
|
|
202
|
+
```sh
|
|
203
|
+
npm install
|
|
204
|
+
npm run lint # eslint + prettier --check
|
|
205
|
+
npm run build # tsc
|
|
206
|
+
npm test # vitest
|
|
207
|
+
npm run test:coverage # with thresholds
|
|
208
|
+
npm run docs:tools # regenerate docs/reference/tools.md from the registered tools
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
The table above is hand-curated; the complete
|
|
212
|
+
[tool reference](https://audiobookshelf-mcp.ni-c.de/reference/tools) with every
|
|
213
|
+
parameter is generated from the code, and CI fails if the committed copy is stale.
|
|
214
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for a throwaway Audiobookshelf you can
|
|
215
|
+
safely write to — the write tools change progress and bookmarks on the API key's
|
|
216
|
+
own user, so don't develop against a library you care about.
|
|
217
|
+
|
|
218
|
+
The tool definitions were derived from the Audiobookshelf server source
|
|
219
|
+
(`server/routers/ApiRouter.js` and the controllers) rather than from
|
|
220
|
+
[api.audiobookshelf.org](https://api.audiobookshelf.org/), which is out of date in
|
|
221
|
+
several places — the filter data endpoint is `/filterdata` not `/filter`, progress
|
|
222
|
+
updates are `PATCH /api/me/progress/:id` not `POST /api/me/progress`, and bookmarks
|
|
223
|
+
live under `/api/me/item/:id/bookmark`.
|
|
224
|
+
|
|
225
|
+
## Releasing
|
|
226
|
+
|
|
227
|
+
Tag-driven, no manual publish step:
|
|
228
|
+
|
|
229
|
+
1. Move the `[Unreleased]` entries into a new `## [x.y.z] - YYYY-MM-DD` section in
|
|
230
|
+
`CHANGELOG.md` and bump `package.json`.
|
|
231
|
+
2. `npm run lint && npm run build && npm run test:coverage`.
|
|
232
|
+
3. Commit, then a **signed annotated** tag: `git tag -s vx.y.z -m "vx.y.z"`.
|
|
233
|
+
4. `git push origin main vx.y.z`.
|
|
234
|
+
|
|
235
|
+
`release.yml` then runs the tests, publishes to npm with provenance via Trusted
|
|
236
|
+
Publishing (no token secret involved), creates the GitHub release from the
|
|
237
|
+
CHANGELOG section, and publishes to the
|
|
238
|
+
[MCP registry](https://registry.modelcontextprotocol.io) as
|
|
239
|
+
`io.github.ni-c/audiobookshelf-mcp`. `ci.yml` pushes the multi-arch image to GHCR
|
|
240
|
+
on the same tag.
|
|
241
|
+
|
|
242
|
+
If the registry step fails, fix it on `main` and dispatch the
|
|
243
|
+
`Publish to MCP Registry` workflow — do **not** re-run the tag job, which would
|
|
244
|
+
check out the old tree.
|
|
245
|
+
|
|
246
|
+
## Contributing
|
|
247
|
+
|
|
248
|
+
Issues, discussions and pull requests are welcome — see
|
|
249
|
+
[CONTRIBUTING.md](CONTRIBUTING.md). For vulnerabilities please use
|
|
250
|
+
[private reporting](https://github.com/ni-c/audiobookshelf-mcp/security/advisories/new)
|
|
251
|
+
rather than a public issue; the policy is in [SECURITY.md](SECURITY.md).
|
|
252
|
+
|
|
253
|
+
## License
|
|
254
|
+
|
|
255
|
+
MIT
|
package/dist/api.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type Config } from './config.js';
|
|
2
|
+
export declare class AudiobookshelfApiError extends Error {
|
|
3
|
+
readonly status: number;
|
|
4
|
+
readonly body: string;
|
|
5
|
+
constructor(status: number, body: string, method: string, path: string);
|
|
6
|
+
}
|
|
7
|
+
/** Minimal client for the Audiobookshelf REST API. */
|
|
8
|
+
export declare class AudiobookshelfApi {
|
|
9
|
+
private readonly config;
|
|
10
|
+
private readonly baseUrl;
|
|
11
|
+
/**
|
|
12
|
+
* Only set when AUDIOBOOKSHELF_INSECURE_TLS is enabled. Scopes the relaxed
|
|
13
|
+
* certificate validation to requests against the configured host instead of
|
|
14
|
+
* disabling it process-wide via NODE_TLS_REJECT_UNAUTHORIZED.
|
|
15
|
+
*/
|
|
16
|
+
private readonly insecureDispatcher?;
|
|
17
|
+
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>;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Guards an id that ends up in a URL path. Path traversal here would let a
|
|
26
|
+
* caller reach a different resource — or a different API entirely.
|
|
27
|
+
*
|
|
28
|
+
* Audiobookshelf ids are UUIDs on current servers and prefixed slugs
|
|
29
|
+
* (`li_…`, `lib_…`, `col_…`, `pl_…`, `aut_…`) on older ones; both fit.
|
|
30
|
+
*/
|
|
31
|
+
export declare function assertPathSegment(value: string, what: string): string;
|
|
32
|
+
/**
|
|
33
|
+
* Builds a query string from defined values only.
|
|
34
|
+
*
|
|
35
|
+
* URLSearchParams does the percent-encoding, which matters for the `filter`
|
|
36
|
+
* parameter: its base64 payload can contain `+`, `/` and `=`.
|
|
37
|
+
*/
|
|
38
|
+
export declare function query(params: Record<string, string | number | boolean | undefined>): string;
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { Agent, fetch as undiciFetch, } from 'undici';
|
|
2
|
+
import { missingConfigKeys, missingConfigMessage, } from './config.js';
|
|
3
|
+
const REQUEST_TIMEOUT_MS = 15_000;
|
|
4
|
+
export class AudiobookshelfApiError extends Error {
|
|
5
|
+
status;
|
|
6
|
+
body;
|
|
7
|
+
constructor(status, body, method, path) {
|
|
8
|
+
super(`Audiobookshelf API ${method} ${path} failed with HTTP ${status}`);
|
|
9
|
+
this.status = status;
|
|
10
|
+
this.body = body;
|
|
11
|
+
this.name = 'AudiobookshelfApiError';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/** Minimal client for the Audiobookshelf REST API. */
|
|
15
|
+
export class AudiobookshelfApi {
|
|
16
|
+
config;
|
|
17
|
+
baseUrl;
|
|
18
|
+
/**
|
|
19
|
+
* Only set when AUDIOBOOKSHELF_INSECURE_TLS is enabled. Scopes the relaxed
|
|
20
|
+
* certificate validation to requests against the configured host instead of
|
|
21
|
+
* disabling it process-wide via NODE_TLS_REJECT_UNAUTHORIZED.
|
|
22
|
+
*/
|
|
23
|
+
insecureDispatcher;
|
|
24
|
+
constructor(config) {
|
|
25
|
+
this.config = config;
|
|
26
|
+
this.baseUrl = config.url ?? '';
|
|
27
|
+
if (config.insecureTls) {
|
|
28
|
+
this.insecureDispatcher = new Agent({
|
|
29
|
+
connect: { rejectUnauthorized: false },
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
async request(method, path, body) {
|
|
34
|
+
// The credentials are only required here, not at startup, so the server can
|
|
35
|
+
// still be started and introspected without them.
|
|
36
|
+
const missing = missingConfigKeys(this.config);
|
|
37
|
+
if (missing.length > 0) {
|
|
38
|
+
throw new Error(missingConfigMessage(missing));
|
|
39
|
+
}
|
|
40
|
+
const headers = {
|
|
41
|
+
Authorization: `Bearer ${this.config.apiKey ?? ''}`,
|
|
42
|
+
Accept: 'application/json',
|
|
43
|
+
};
|
|
44
|
+
const init = {
|
|
45
|
+
method,
|
|
46
|
+
headers,
|
|
47
|
+
// Never follow a redirect: it would resend the Authorization header to
|
|
48
|
+
// whatever host the upstream points at.
|
|
49
|
+
redirect: 'error',
|
|
50
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
51
|
+
};
|
|
52
|
+
if (body !== undefined) {
|
|
53
|
+
headers['Content-Type'] = 'application/json';
|
|
54
|
+
init.body = JSON.stringify(body);
|
|
55
|
+
}
|
|
56
|
+
const url = `${this.baseUrl}${path}`;
|
|
57
|
+
// The insecure dispatcher requires undici's own fetch; the default path uses
|
|
58
|
+
// the (stubbable) global fetch so tests can intercept it.
|
|
59
|
+
const response = this.insecureDispatcher
|
|
60
|
+
? await undiciFetch(url, {
|
|
61
|
+
...init,
|
|
62
|
+
dispatcher: this.insecureDispatcher,
|
|
63
|
+
})
|
|
64
|
+
: await fetch(url, init);
|
|
65
|
+
const text = await response.text();
|
|
66
|
+
if (!response.ok) {
|
|
67
|
+
throw new AudiobookshelfApiError(response.status, text, method, path);
|
|
68
|
+
}
|
|
69
|
+
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
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return text;
|
|
79
|
+
}
|
|
80
|
+
get(path) {
|
|
81
|
+
return this.request('GET', path);
|
|
82
|
+
}
|
|
83
|
+
post(path, body) {
|
|
84
|
+
return this.request('POST', path, body);
|
|
85
|
+
}
|
|
86
|
+
patch(path, body) {
|
|
87
|
+
return this.request('PATCH', path, body);
|
|
88
|
+
}
|
|
89
|
+
delete(path) {
|
|
90
|
+
return this.request('DELETE', path);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Guards an id that ends up in a URL path. Path traversal here would let a
|
|
95
|
+
* caller reach a different resource — or a different API entirely.
|
|
96
|
+
*
|
|
97
|
+
* Audiobookshelf ids are UUIDs on current servers and prefixed slugs
|
|
98
|
+
* (`li_…`, `lib_…`, `col_…`, `pl_…`, `aut_…`) on older ones; both fit.
|
|
99
|
+
*/
|
|
100
|
+
export function assertPathSegment(value, what) {
|
|
101
|
+
if (!/^[A-Za-z0-9._-]+$/.test(value) || value === '.' || value === '..') {
|
|
102
|
+
throw new Error(`invalid ${what}: only letters, digits, dot, underscore and hyphen are allowed`);
|
|
103
|
+
}
|
|
104
|
+
return value;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Builds a query string from defined values only.
|
|
108
|
+
*
|
|
109
|
+
* URLSearchParams does the percent-encoding, which matters for the `filter`
|
|
110
|
+
* parameter: its base64 payload can contain `+`, `/` and `=`.
|
|
111
|
+
*/
|
|
112
|
+
export function query(params) {
|
|
113
|
+
const search = new URLSearchParams();
|
|
114
|
+
for (const [key, value] of Object.entries(params)) {
|
|
115
|
+
if (value === undefined)
|
|
116
|
+
continue;
|
|
117
|
+
search.set(key, typeof value === 'boolean' ? (value ? '1' : '0') : String(value));
|
|
118
|
+
}
|
|
119
|
+
const encoded = search.toString();
|
|
120
|
+
return encoded ? `?${encoded}` : '';
|
|
121
|
+
}
|
|
122
|
+
//# sourceMappingURL=api.js.map
|
package/dist/api.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface Config {
|
|
2
|
+
/**
|
|
3
|
+
* Base URL of the Audiobookshelf instance, e.g. `https://abs.example.com`.
|
|
4
|
+
* May be undefined together with the API key: the server still starts and
|
|
5
|
+
* lists its tools, every API call then fails with {@link missingConfigMessage}.
|
|
6
|
+
*/
|
|
7
|
+
url: string | undefined;
|
|
8
|
+
apiKey: string | undefined;
|
|
9
|
+
insecureTls: boolean;
|
|
10
|
+
readOnly: boolean;
|
|
11
|
+
}
|
|
12
|
+
/** Shown when the configuration is incomplete — at startup and on every API call. */
|
|
13
|
+
export declare function missingConfigMessage(missing: string[]): string;
|
|
14
|
+
/** Names of the required environment variables that are unset in `config`. */
|
|
15
|
+
export declare function missingConfigKeys(config: Config): string[];
|
|
16
|
+
/**
|
|
17
|
+
* Reads the configuration from environment variables.
|
|
18
|
+
*
|
|
19
|
+
* Missing credentials are only a warning, not a fatal error: the server must be
|
|
20
|
+
* able to complete the MCP handshake and answer `tools/list` without them, so
|
|
21
|
+
* registries and sandbox inspectors can introspect it. A malformed URL still
|
|
22
|
+
* exits — that one could send the API key to the wrong host.
|
|
23
|
+
*/
|
|
24
|
+
export declare function loadConfig(env?: NodeJS.ProcessEnv): Config;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/** Shown when the configuration is incomplete — at startup and on every API call. */
|
|
2
|
+
export function missingConfigMessage(missing) {
|
|
3
|
+
return (`missing required environment variable(s): ${missing.join(', ')}\n` +
|
|
4
|
+
'Required: AUDIOBOOKSHELF_URL (e.g. https://abs.example.com), AUDIOBOOKSHELF_API_KEY\n' +
|
|
5
|
+
'Create the API key in Audiobookshelf under Settings → Users → API Keys ' +
|
|
6
|
+
'(admin only, requires server 2.26.0 or newer).\n' +
|
|
7
|
+
'Optional: AUDIOBOOKSHELF_READ_ONLY=true to expose only read tools, ' +
|
|
8
|
+
'AUDIOBOOKSHELF_INSECURE_TLS=true to accept self-signed certificates');
|
|
9
|
+
}
|
|
10
|
+
/** Names of the required environment variables that are unset in `config`. */
|
|
11
|
+
export function missingConfigKeys(config) {
|
|
12
|
+
return [
|
|
13
|
+
!config.url && 'AUDIOBOOKSHELF_URL',
|
|
14
|
+
!config.apiKey && 'AUDIOBOOKSHELF_API_KEY',
|
|
15
|
+
].filter((v) => Boolean(v));
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Reads the configuration from environment variables.
|
|
19
|
+
*
|
|
20
|
+
* Missing credentials are only a warning, not a fatal error: the server must be
|
|
21
|
+
* able to complete the MCP handshake and answer `tools/list` without them, so
|
|
22
|
+
* registries and sandbox inspectors can introspect it. A malformed URL still
|
|
23
|
+
* exits — that one could send the API key to the wrong host.
|
|
24
|
+
*/
|
|
25
|
+
export function loadConfig(env = process.env) {
|
|
26
|
+
const url = env.AUDIOBOOKSHELF_URL;
|
|
27
|
+
const apiKey = env.AUDIOBOOKSHELF_API_KEY;
|
|
28
|
+
const insecureTls = env.AUDIOBOOKSHELF_INSECURE_TLS === 'true';
|
|
29
|
+
const readOnly = env.AUDIOBOOKSHELF_READ_ONLY === 'true';
|
|
30
|
+
const missing = [
|
|
31
|
+
!url && 'AUDIOBOOKSHELF_URL',
|
|
32
|
+
!apiKey && 'AUDIOBOOKSHELF_API_KEY',
|
|
33
|
+
].filter((v) => Boolean(v));
|
|
34
|
+
if (missing.length > 0) {
|
|
35
|
+
console.error(`audiobookshelf-mcp: ${missingConfigMessage(missing)}`);
|
|
36
|
+
}
|
|
37
|
+
if (!url) {
|
|
38
|
+
return { url: undefined, apiKey, insecureTls, readOnly };
|
|
39
|
+
}
|
|
40
|
+
let parsed;
|
|
41
|
+
try {
|
|
42
|
+
parsed = new URL(url);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
console.error(`audiobookshelf-mcp: AUDIOBOOKSHELF_URL is not a valid URL: ${url}`);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
49
|
+
console.error(`audiobookshelf-mcp: AUDIOBOOKSHELF_URL must use http:// or https:// (got ${parsed.protocol})`);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
// Credentials embedded in the URL would end up in logs and error messages.
|
|
53
|
+
if (parsed.username || parsed.password) {
|
|
54
|
+
console.error('audiobookshelf-mcp: AUDIOBOOKSHELF_URL must not contain credentials — use AUDIOBOOKSHELF_API_KEY');
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
if (parsed.protocol === 'http:' && !isLoopbackHost(parsed.hostname)) {
|
|
58
|
+
console.error('audiobookshelf-mcp: WARNING: AUDIOBOOKSHELF_URL uses plain http to a non-local host — ' +
|
|
59
|
+
'the API key will be sent unencrypted. Use https:// instead.');
|
|
60
|
+
}
|
|
61
|
+
const config = {
|
|
62
|
+
url: url.replace(/\/+$/, ''),
|
|
63
|
+
apiKey,
|
|
64
|
+
insecureTls,
|
|
65
|
+
readOnly,
|
|
66
|
+
};
|
|
67
|
+
// Don't keep the key in the environment for the process lifetime — it is
|
|
68
|
+
// visible to child processes and in /proc/<pid>/environ.
|
|
69
|
+
delete env.AUDIOBOOKSHELF_API_KEY;
|
|
70
|
+
return config;
|
|
71
|
+
}
|
|
72
|
+
function isLoopbackHost(hostname) {
|
|
73
|
+
return (hostname === 'localhost' ||
|
|
74
|
+
hostname.endsWith('.localhost') ||
|
|
75
|
+
hostname.startsWith('127.') ||
|
|
76
|
+
hostname === '::1');
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Issues short-lived confirmation tokens for irreversible operations.
|
|
3
|
+
*
|
|
4
|
+
* A plain boolean `confirm` parameter could be set by the model on the very
|
|
5
|
+
* first call — or be talked into it by instructions hidden in upstream content —
|
|
6
|
+
* whereas a random token that only ever appears in a *previous* tool result
|
|
7
|
+
* cannot be guessed. The token is bound to a resource key, so a confirmation for
|
|
8
|
+
* one target cannot be replayed for another.
|
|
9
|
+
*/
|
|
10
|
+
export declare class ConfirmationStore {
|
|
11
|
+
private readonly ttlMs;
|
|
12
|
+
private readonly pending;
|
|
13
|
+
constructor(ttlMs?: number);
|
|
14
|
+
/** Creates (or replaces) the pending token for `resource`. */
|
|
15
|
+
issue(resource: string): string;
|
|
16
|
+
/**
|
|
17
|
+
* Returns true and consumes the token when it matches the pending one for
|
|
18
|
+
* `resource` and has not expired. Tokens are single-use.
|
|
19
|
+
*/
|
|
20
|
+
consume(resource: string, token: string | undefined): boolean;
|
|
21
|
+
/** Minutes the issued tokens stay valid, for use in messages. */
|
|
22
|
+
get ttlMinutes(): number;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Resource key for an operation on a *set* of targets. Without the fingerprint a
|
|
26
|
+
* confirmation for ["a.txt"] would also execute ["a.txt", "secrets.env"] — the
|
|
27
|
+
* model chooses the second list, and only the id would have been checked.
|
|
28
|
+
*/
|
|
29
|
+
export declare function setResourceKey(operation: string, targets: string[]): string;
|
|
30
|
+
/**
|
|
31
|
+
* Builds the text returned by the first call of a destructive tool.
|
|
32
|
+
*
|
|
33
|
+
* Note what is NOT in here: no title, description or filename coming from the
|
|
34
|
+
* API. Those are attacker-controllable and this string is read by a model.
|
|
35
|
+
*/
|
|
36
|
+
export declare function confirmationPrompt(what: string, token: string, ttlMinutes: number): string;
|