audiobookshelf-mcp 0.1.4 → 0.2.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/README.md CHANGED
@@ -4,20 +4,24 @@
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
+ [![sponsor](https://img.shields.io/badge/sponsor-ni--c-ea4aaa?logo=githubsponsors&logoColor=white)](https://github.com/sponsors/ni-c)
11
11
 
12
- An [MCP](https://modelcontextprotocol.io) server for
12
+ A [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server for
13
13
  [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.
14
+ podcast server.
17
15
 
18
- 44 tools: 29 read, 15 write.
16
+ Lets MCP clients like Claude Code, Claude Desktop or Codex browse your libraries,
17
+ answer questions about what you own and what you have listened to, and — unless you
18
+ switch it off — keep your listening progress, bookmarks, collections and playlists up
19
+ to date: 44 tools, 29 read and 15 write.
19
20
 
20
- 📖 **Full documentation: <https://audiobookshelf-mcp.ni-c.de>**
21
+ Forty-four tools is the ceiling, not the floor: `AUDIOBOOKSHELF_ALLOW_TOOLS=essential`
22
+ registers a curated eight instead, and a model picks the right tool far more
23
+ reliably from eight than from forty-four — see
24
+ [choosing which tools load](#choosing-which-tools-load).
21
25
 
22
26
  <!-- <picture> is resolved against the colour scheme of the page showing it, so GitHub
23
27
  picks the variant that matches its own theme toggle. npm strips <picture> and
@@ -53,11 +57,35 @@ once, at creation.
53
57
  | `AUDIOBOOKSHELF_API_KEY` | yes | API key, sent as `Authorization: Bearer …` |
54
58
  | `AUDIOBOOKSHELF_READ_ONLY` | no | `true` registers only the 29 read tools |
55
59
  | `AUDIOBOOKSHELF_INSECURE_TLS` | no | `true` accepts self-signed certificates — scoped to this connection, not process-wide |
60
+ | `AUDIOBOOKSHELF_ALLOW_TOOLS` | no | Comma-separated tool names, `list_*` prefixes, or `essential` for a curated preset |
61
+ | `AUDIOBOOKSHELF_DENY_TOOLS` | no | Same syntax; removed from whatever `AUDIOBOOKSHELF_ALLOW_TOOLS` left |
56
62
 
57
63
  The server starts without configuration: it completes the MCP handshake and lists
58
64
  its tools, and every call then fails with the setup instructions. That is
59
65
  deliberate, so registries and sandbox inspectors can introspect it.
60
66
 
67
+ ### Choosing which tools load
68
+
69
+ `AUDIOBOOKSHELF_ALLOW_TOOLS` and `AUDIOBOOKSHELF_DENY_TOOLS` take comma-separated tool names;
70
+ a trailing `*` matches a whole family. `essential` is a curated preset of
71
+ eight: `list_libraries`, `search_library`, `list_library_items`, `get_library_item`, `get_item_chapters`, `list_items_in_progress`, `get_media_progress`, `set_media_progress`.
72
+
73
+ ```sh
74
+ AUDIOBOOKSHELF_ALLOW_TOOLS=essential
75
+ AUDIOBOOKSHELF_ALLOW_TOOLS=search_library,get_library_item,set_media_progress
76
+ AUDIOBOOKSHELF_DENY_TOOLS=delete_*
77
+ ```
78
+
79
+ An entry that matches no tool aborts startup and names it, so a typo cannot
80
+ silently hide a tool — an absent tool is not something anyone traces back to an
81
+ environment variable. A filtered tool is never registered, so it is absent from
82
+ `tools/list` and unknown to `tools/call` alike, exactly like a write tool under
83
+ `AUDIOBOOKSHELF_READ_ONLY`.
84
+
85
+ If you run several of these servers at once, [mcp-hub](https://mcp-hub.ni-c.de)
86
+ is the other answer — its `/hub` endpoint replaces every server's tools with six
87
+ meta-tools.
88
+
61
89
  ## Install
62
90
 
63
91
  ```sh
package/dist/config.d.ts CHANGED
@@ -7,7 +7,15 @@ export interface Config {
7
7
  url: string | undefined;
8
8
  apiKey: string | undefined;
9
9
  insecureTls: boolean;
10
- readOnly: boolean;
10
+ readOnly: boolean; /**
11
+ * Raw value of `AUDIOBOOKSHELF_ALLOW_TOOLS` — comma-separated tool names, `list_*`
12
+ * prefixes, or `essential`. Kept unparsed on purpose: this file is a mirror of
13
+ * the environment, and the names can only be checked against the tool
14
+ * catalogue, which `buildToolFilter` does.
15
+ */
16
+ allowTools: string | undefined;
17
+ /** Raw value of `AUDIOBOOKSHELF_DENY_TOOLS`, same shape, subtracted from the above. */
18
+ denyTools: string | undefined;
11
19
  }
12
20
  /** Shown when the configuration is incomplete — at startup and on every API call. */
13
21
  export declare function missingConfigMessage(missing: string[]): string;
package/dist/config.js CHANGED
@@ -28,6 +28,8 @@ export function loadConfig(env = process.env) {
28
28
  const apiKey = env.AUDIOBOOKSHELF_API_KEY;
29
29
  const insecureTls = env.AUDIOBOOKSHELF_INSECURE_TLS === 'true';
30
30
  const readOnly = env.AUDIOBOOKSHELF_READ_ONLY === 'true';
31
+ const allowTools = env.AUDIOBOOKSHELF_ALLOW_TOOLS;
32
+ const denyTools = env.AUDIOBOOKSHELF_DENY_TOOLS;
31
33
  // Don't keep the key in the environment for the process lifetime — it is
32
34
  // visible to child processes and in /proc/<pid>/environ. This happens before
33
35
  // any branch on purpose: the paths below either exit or return early, and
@@ -44,7 +46,14 @@ export function loadConfig(env = process.env) {
44
46
  console.error(`audiobookshelf-mcp: ${missingConfigMessage(missing)}`);
45
47
  }
46
48
  if (!url) {
47
- return { url: undefined, apiKey, insecureTls, readOnly };
49
+ return {
50
+ url: undefined,
51
+ apiKey,
52
+ insecureTls,
53
+ readOnly,
54
+ allowTools,
55
+ denyTools,
56
+ };
48
57
  }
49
58
  let parsed;
50
59
  try {
@@ -76,6 +85,8 @@ export function loadConfig(env = process.env) {
76
85
  apiKey,
77
86
  insecureTls,
78
87
  readOnly,
88
+ allowTools,
89
+ denyTools,
79
90
  };
80
91
  }
81
92
  function isLoopbackHost(hostname) {
@@ -1 +1 @@
1
- {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
3
  import { loadConfig } from './config.js';
4
4
  import { createServer } from './server.js';
5
+ import { ToolFilterError } from './tool-filter.js';
5
6
  async function main() {
6
7
  const config = loadConfig();
7
8
  if (config.insecureTls) {
@@ -10,7 +11,19 @@ async function main() {
10
11
  if (config.readOnly) {
11
12
  console.error('audiobookshelf-mcp: AUDIOBOOKSHELF_READ_ONLY=true — write tools are not registered');
12
13
  }
13
- const server = createServer(config);
14
+ let server;
15
+ try {
16
+ server = createServer(config);
17
+ }
18
+ catch (error) {
19
+ // A bad tool list is operator feedback, not a crash: print the
20
+ // sentence on its own rather than behind "fatal error:".
21
+ if (error instanceof ToolFilterError) {
22
+ console.error(`audiobookshelf-mcp: ${error.message}`);
23
+ process.exit(1);
24
+ }
25
+ throw error;
26
+ }
14
27
  // stdout belongs to the protocol; everything human-readable goes to stderr.
15
28
  await server.connect(new StdioServerTransport());
16
29
  console.error(config.url
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AAEjF,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,KAAK,UAAU,IAAI;IACjB,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;IAE5B,IAAI,MAAM,CAAC,WAAW,EAAE,CAAC;QACvB,OAAO,CAAC,KAAK,CACX,iIAAiI,CAClI,CAAC;IACJ,CAAC;IACD,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC;QACpB,OAAO,CAAC,KAAK,CACX,oFAAoF,CACrF,CAAC;IACJ,CAAC;IAED,MAAM,MAAM,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC;IACpC,4EAA4E;IAC5E,MAAM,MAAM,CAAC,OAAO,CAAC,IAAI,oBAAoB,EAAE,CAAC,CAAC;IACjD,OAAO,CAAC,KAAK,CACX,MAAM,CAAC,GAAG;QACR,CAAC,CAAC,4CAA4C,MAAM,CAAC,GAAG,EAAE;QAC1D,CAAC,CAAC,iGAAiG,CACtG,CAAC;AACJ,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;IAC9B,OAAO,CAAC,KAAK,CAAC,kCAAkC,EAAE,KAAK,CAAC,CAAC;IACzD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AAEjF,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAEnD,KAAK,UAAU,IAAI;IACjB,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;IAE5B,IAAI,MAAM,CAAC,WAAW,EAAE,CAAC;QACvB,OAAO,CAAC,KAAK,CACX,iIAAiI,CAClI,CAAC;IACJ,CAAC;IACD,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC;QACpB,OAAO,CAAC,KAAK,CACX,oFAAoF,CACrF,CAAC;IACJ,CAAC;IAED,IAAI,MAAM,CAAC;IACX,IAAI,CAAC;QACH,MAAM,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC;IAChC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,+DAA+D;QAC/D,yDAAyD;QACzD,IAAI,KAAK,YAAY,eAAe,EAAE,CAAC;YACrC,OAAO,CAAC,KAAK,CAAC,uBAAuB,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;YACtD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;QACD,MAAM,KAAK,CAAC;IACd,CAAC;IACD,4EAA4E;IAC5E,MAAM,MAAM,CAAC,OAAO,CAAC,IAAI,oBAAoB,EAAE,CAAC,CAAC;IACjD,OAAO,CAAC,KAAK,CACX,MAAM,CAAC,GAAG;QACR,CAAC,CAAC,4CAA4C,MAAM,CAAC,GAAG,EAAE;QAC1D,CAAC,CAAC,iGAAiG,CACtG,CAAC;AACJ,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;IAC9B,OAAO,CAAC,KAAK,CAAC,kCAAkC,EAAE,KAAK,CAAC,CAAC;IACzD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
package/dist/server.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { createRequire } from 'node:module';
2
2
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
3
  import { AudiobookshelfApi } from './api.js';
4
+ import { buildToolFilter, installToolFilter } from './tool-filter.js';
4
5
  import { ConfirmationStore } from './confirm.js';
5
6
  import { registerCollectionReadTools, registerCollectionWriteTools, } from './tools/collections.js';
6
7
  import { registerItemReadTools } from './tools/items.js';
@@ -19,12 +20,18 @@ function packageVersion() {
19
20
  }
20
21
  }
21
22
  export function createServer(config) {
23
+ // Before anything is built: an unusable tool list should fail on the
24
+ // way in, not leave a server running with tools quietly missing.
25
+ const filter = buildToolFilter(config);
22
26
  const api = new AudiobookshelfApi(config);
23
27
  const confirmations = new ConfirmationStore();
24
28
  const server = new McpServer({
25
29
  name: 'audiobookshelf-mcp',
26
30
  version: packageVersion(),
27
31
  });
32
+ // Wraps server.registerTool, so it has to sit before the first
33
+ // register call and does not care how they are organised.
34
+ installToolFilter(server, filter);
28
35
  registerLibraryReadTools(server, api);
29
36
  registerItemReadTools(server, api);
30
37
  registerMeReadTools(server, api);
@@ -1 +1 @@
1
- {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE5C,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEpE,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAE7C,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,EACL,2BAA2B,EAC3B,4BAA4B,GAC7B,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AACzD,OAAO,EAAE,wBAAwB,EAAE,MAAM,sBAAsB,CAAC;AAChE,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AACpD,OAAO,EACL,yBAAyB,EACzB,0BAA0B,GAC3B,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACL,0BAA0B,EAC1B,0BAA0B,GAC3B,MAAM,qBAAqB,CAAC;AAE7B,SAAS,cAAc;IACrB,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAC/C,MAAM,GAAG,GAAG,OAAO,CAAC,iBAAiB,CAAwB,CAAC;QAC9D,OAAO,GAAG,CAAC,OAAO,CAAC;IACrB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,OAAO,CAAC;IACjB,CAAC;AACH,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,MAAc;IACzC,MAAM,GAAG,GAAG,IAAI,iBAAiB,CAAC,MAAM,CAAC,CAAC;IAC1C,MAAM,aAAa,GAAG,IAAI,iBAAiB,EAAE,CAAC;IAE9C,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC;QAC3B,IAAI,EAAE,oBAAoB;QAC1B,OAAO,EAAE,cAAc,EAAE;KAC1B,CAAC,CAAC;IAEH,wBAAwB,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACtC,qBAAqB,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACnC,mBAAmB,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACjC,2BAA2B,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACzC,yBAAyB,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAEvC,6EAA6E;IAC7E,8EAA8E;IAC9E,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC;QACrB,0BAA0B,CAAC,MAAM,EAAE,GAAG,EAAE,aAAa,CAAC,CAAC;QACvD,0BAA0B,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;QACxC,4BAA4B,CAAC,MAAM,EAAE,GAAG,EAAE,aAAa,CAAC,CAAC;QACzD,0BAA0B,CAAC,MAAM,EAAE,GAAG,EAAE,aAAa,CAAC,CAAC;IACzD,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC"}
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE5C,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEpE,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAC7C,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAEtE,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,EACL,2BAA2B,EAC3B,4BAA4B,GAC7B,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AACzD,OAAO,EAAE,wBAAwB,EAAE,MAAM,sBAAsB,CAAC;AAChE,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AACpD,OAAO,EACL,yBAAyB,EACzB,0BAA0B,GAC3B,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACL,0BAA0B,EAC1B,0BAA0B,GAC3B,MAAM,qBAAqB,CAAC;AAE7B,SAAS,cAAc;IACrB,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAC/C,MAAM,GAAG,GAAG,OAAO,CAAC,iBAAiB,CAAwB,CAAC;QAC9D,OAAO,GAAG,CAAC,OAAO,CAAC;IACrB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,OAAO,CAAC;IACjB,CAAC;AACH,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,MAAc;IACzC,qEAAqE;IACrE,iEAAiE;IACjE,MAAM,MAAM,GAAG,eAAe,CAAC,MAAM,CAAC,CAAC;IAEvC,MAAM,GAAG,GAAG,IAAI,iBAAiB,CAAC,MAAM,CAAC,CAAC;IAC1C,MAAM,aAAa,GAAG,IAAI,iBAAiB,EAAE,CAAC;IAE9C,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC;QAC3B,IAAI,EAAE,oBAAoB;QAC1B,OAAO,EAAE,cAAc,EAAE;KAC1B,CAAC,CAAC;IAEH,+DAA+D;IAC/D,0DAA0D;IAC1D,iBAAiB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAElC,wBAAwB,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACtC,qBAAqB,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACnC,mBAAmB,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACjC,2BAA2B,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACzC,yBAAyB,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAEvC,6EAA6E;IAC7E,8EAA8E;IAC9E,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC;QACrB,0BAA0B,CAAC,MAAM,EAAE,GAAG,EAAE,aAAa,CAAC,CAAC;QACvD,0BAA0B,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;QACxC,4BAA4B,CAAC,MAAM,EAAE,GAAG,EAAE,aAAa,CAAC,CAAC;QACzD,0BAA0B,CAAC,MAAM,EAAE,GAAG,EAAE,aAAa,CAAC,CAAC;IACzD,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC"}
@@ -0,0 +1,45 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ /**
3
+ * A tool list that names something this server does not have.
4
+ *
5
+ * Thrown rather than `process.exit(1)`: `createServer` is called in-process by
6
+ * the tests, and an exiting constructor cannot be tested. `src/index.ts` turns
7
+ * it back into an exit code.
8
+ */
9
+ export declare class ToolFilterError extends Error {
10
+ constructor(message: string);
11
+ }
12
+ export interface ToolFilter {
13
+ /** False when neither variable was set — then nothing is wrapped at all. */
14
+ readonly active: boolean;
15
+ /** The tools that survive. Only meaningful while `active`. */
16
+ readonly selected: ReadonlySet<string>;
17
+ }
18
+ /**
19
+ * Reads `<PREFIX>_ALLOW_TOOLS` / `<PREFIX>_DENY_TOOLS` and works out which
20
+ * tools survive.
21
+ *
22
+ * Every entry has to match at least one tool in the catalogue. An entry that
23
+ * matches nothing is fatal rather than ignored, because the failure it produces
24
+ * otherwise — a tool quietly missing from `tools/list` — is invisible: nobody
25
+ * looks for the cause of an absence in an environment variable.
26
+ */
27
+ export declare function buildToolFilter(config: {
28
+ allowTools: string | undefined;
29
+ denyTools: string | undefined;
30
+ readOnly: boolean;
31
+ }): ToolFilter;
32
+ /**
33
+ * Makes `server` register only the tools the filter selected.
34
+ *
35
+ * The tool is registered and then removed again rather than skipped. Skipping
36
+ * looks cheaper and breaks one case: the SDK installs its `tools/list` handler
37
+ * from inside the registration path, so a server whose every tool was skipped
38
+ * would answer `tools/list` with "method not found" instead of an empty list.
39
+ * `remove()` deletes the entry from the SDK's tool map outright, which makes a
40
+ * filtered tool answer `Tool X not found` — exactly what a tool the read-only
41
+ * mode never registered already does. `disable()` would be wrong: it hides the
42
+ * tool from `tools/list` but still answers a call with "disabled", which is the
43
+ * advertising-a-refusal this server avoids everywhere else.
44
+ */
45
+ export declare function installToolFilter(server: McpServer, filter: ToolFilter): void;
@@ -0,0 +1,171 @@
1
+ import { ALL_TOOLS, ESSENTIAL_TOOLS, READ_TOOLS } from './tools/catalogue.js';
2
+ /**
3
+ * A tool list that names something this server does not have.
4
+ *
5
+ * Thrown rather than `process.exit(1)`: `createServer` is called in-process by
6
+ * the tests, and an exiting constructor cannot be tested. `src/index.ts` turns
7
+ * it back into an exit code.
8
+ */
9
+ export class ToolFilterError extends Error {
10
+ constructor(message) {
11
+ super(message);
12
+ this.name = 'ToolFilterError';
13
+ }
14
+ }
15
+ /** The `essential` preset is spelled out here so it cannot collide with a tool name. */
16
+ const PRESET = 'essential';
17
+ /**
18
+ * Splits a comma-separated value into entries.
19
+ *
20
+ * Empty entries are dropped, so `a,,b` and a trailing comma are both fine, and
21
+ * a value that is empty or only whitespace counts as *unset* — `X_ALLOW_TOOLS=`
22
+ * in a compose file must not mean "allow nothing". Entries are lowercased: the
23
+ * catalogue is entirely lowercase, so this is lossless, and a shell that
24
+ * upper-cased a name should not take the server down.
25
+ */
26
+ function entriesOf(raw) {
27
+ if (raw === undefined)
28
+ return undefined;
29
+ const entries = raw
30
+ .split(',')
31
+ .map((entry) => entry.trim().toLowerCase())
32
+ .filter((entry) => entry.length > 0);
33
+ return entries.length > 0 ? entries : undefined;
34
+ }
35
+ /**
36
+ * Expands one entry to the catalogue tools it names.
37
+ *
38
+ * A pattern is a literal prefix plus exactly one trailing `*`. Anything else is
39
+ * rejected outright: `*_zone` and `list_*_x` look plausible, match nothing, and
40
+ * would otherwise be silent forever.
41
+ */
42
+ function expand(entry, variable) {
43
+ const star = entry.indexOf('*');
44
+ if (star !== -1) {
45
+ if (star !== entry.length - 1) {
46
+ throw new ToolFilterError(`${variable}: "${entry}" is not a valid entry — a pattern is a prefix ` +
47
+ 'followed by a single trailing "*", for example "list_*". Everything ' +
48
+ 'else is an exact tool name.');
49
+ }
50
+ const prefix = entry.slice(0, -1);
51
+ return ALL_TOOLS.filter((tool) => tool.startsWith(prefix));
52
+ }
53
+ return ALL_TOOLS.filter((tool) => tool === entry);
54
+ }
55
+ /** The full catalogue, for the "these are the names that exist" half of an error. */
56
+ function catalogueList() {
57
+ return [...ALL_TOOLS].sort().join(', ');
58
+ }
59
+ /**
60
+ * Reads `<PREFIX>_ALLOW_TOOLS` / `<PREFIX>_DENY_TOOLS` and works out which
61
+ * tools survive.
62
+ *
63
+ * Every entry has to match at least one tool in the catalogue. An entry that
64
+ * matches nothing is fatal rather than ignored, because the failure it produces
65
+ * otherwise — a tool quietly missing from `tools/list` — is invisible: nobody
66
+ * looks for the cause of an absence in an environment variable.
67
+ */
68
+ export function buildToolFilter(config) {
69
+ const allow = entriesOf(config.allowTools);
70
+ const deny = entriesOf(config.denyTools);
71
+ if (allow === undefined && deny === undefined) {
72
+ return { active: false, selected: new Set() };
73
+ }
74
+ // What would be registered without any filter. In read-only mode the write
75
+ // tools never reach `registerTool`, but they stay in the catalogue so that a
76
+ // name from that half is answered with "read-only suppresses it", never with
77
+ // "no such tool".
78
+ const registered = new Set(config.readOnly ? READ_TOOLS : ALL_TOOLS);
79
+ // Set when an allow entry named real tools and read-only suppressed all of
80
+ // them, so that "nothing is left" can name the reason rather than shrugging.
81
+ let suppressedByReadOnly = false;
82
+ let selected;
83
+ if (allow === undefined) {
84
+ selected = new Set(registered);
85
+ }
86
+ else {
87
+ selected = new Set();
88
+ for (const entry of allow) {
89
+ if (entry === PRESET) {
90
+ // Preset members are not names the operator typed, so a member that
91
+ // read-only suppresses is dropped silently rather than being an error.
92
+ for (const tool of ESSENTIAL_TOOLS) {
93
+ if (registered.has(tool))
94
+ selected.add(tool);
95
+ }
96
+ continue;
97
+ }
98
+ const matches = expand(entry, 'AUDIOBOOKSHELF_ALLOW_TOOLS');
99
+ if (matches.length === 0) {
100
+ throw new ToolFilterError(`AUDIOBOOKSHELF_ALLOW_TOOLS: no tool matches "${entry}". ` +
101
+ `Valid tools: ${catalogueList()}. "${PRESET}" selects the curated preset.`);
102
+ }
103
+ const survivors = matches.filter((tool) => registered.has(tool));
104
+ if (survivors.length === 0) {
105
+ if (entry.endsWith('*')) {
106
+ // A pattern is a template, not a claim about one tool: warn, continue.
107
+ console.error(`audiobookshelf-mcp: AUDIOBOOKSHELF_ALLOW_TOOLS: "${entry}" matches only write ` +
108
+ 'tools, which AUDIOBOOKSHELF_READ_ONLY suppresses — it contributes nothing.');
109
+ suppressedByReadOnly = true;
110
+ continue;
111
+ }
112
+ // An exact name, though, was typed by someone who believes it is exposed.
113
+ throw new ToolFilterError(`AUDIOBOOKSHELF_ALLOW_TOOLS: "${entry}" is a write tool, but AUDIOBOOKSHELF_READ_ONLY ` +
114
+ 'is set — it is never registered. Remove it from AUDIOBOOKSHELF_ALLOW_TOOLS, ' +
115
+ `or unset AUDIOBOOKSHELF_READ_ONLY. Available in read-only mode: ${[...READ_TOOLS].sort().join(', ')}.`);
116
+ }
117
+ for (const tool of survivors)
118
+ selected.add(tool);
119
+ }
120
+ }
121
+ for (const entry of deny ?? []) {
122
+ // Deny lists are written defensively — "never expose delete_*, whatever
123
+ // else is on" — so matching nothing that survives is fine. Matching nothing
124
+ // in the catalogue is still a typo.
125
+ const matches = expand(entry, 'AUDIOBOOKSHELF_DENY_TOOLS');
126
+ if (matches.length === 0) {
127
+ throw new ToolFilterError(`AUDIOBOOKSHELF_DENY_TOOLS: no tool matches "${entry}". Valid tools: ${catalogueList()}.`);
128
+ }
129
+ for (const tool of matches)
130
+ selected.delete(tool);
131
+ }
132
+ if (selected.size === 0) {
133
+ throw new ToolFilterError(suppressedByReadOnly
134
+ ? 'AUDIOBOOKSHELF_ALLOW_TOOLS selects only write tools, but AUDIOBOOKSHELF_READ_ONLY is ' +
135
+ 'set — the server would start with an empty tool list.'
136
+ : 'AUDIOBOOKSHELF_ALLOW_TOOLS/AUDIOBOOKSHELF_DENY_TOOLS leave no tools registered — the ' +
137
+ 'server would start with an empty tool list.');
138
+ }
139
+ return { active: true, selected };
140
+ }
141
+ /**
142
+ * Makes `server` register only the tools the filter selected.
143
+ *
144
+ * The tool is registered and then removed again rather than skipped. Skipping
145
+ * looks cheaper and breaks one case: the SDK installs its `tools/list` handler
146
+ * from inside the registration path, so a server whose every tool was skipped
147
+ * would answer `tools/list` with "method not found" instead of an empty list.
148
+ * `remove()` deletes the entry from the SDK's tool map outright, which makes a
149
+ * filtered tool answer `Tool X not found` — exactly what a tool the read-only
150
+ * mode never registered already does. `disable()` would be wrong: it hides the
151
+ * tool from `tools/list` but still answers a call with "disabled", which is the
152
+ * advertising-a-refusal this server avoids everywhere else.
153
+ */
154
+ export function installToolFilter(server, filter) {
155
+ if (!filter.active)
156
+ return;
157
+ const register = server.registerTool.bind(server);
158
+ // An object literal rather than a plain function so the method picks up the
159
+ // SDK's generic signature by contextual typing — the call sites keep their
160
+ // typed handler arguments and nothing needs a cast.
161
+ const wrapper = {
162
+ registerTool(name, config, cb) {
163
+ const tool = register(name, config, cb);
164
+ if (!filter.selected.has(name))
165
+ tool.remove();
166
+ return tool;
167
+ },
168
+ };
169
+ server.registerTool = wrapper.registerTool;
170
+ }
171
+ //# sourceMappingURL=tool-filter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tool-filter.js","sourceRoot":"","sources":["../src/tool-filter.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * The tools this server can register, declared rather than discovered.
3
+ *
4
+ * Declared, because the tool filter has to answer "is this a name you have?"
5
+ * *before* anything is registered — and in read-only mode the write tools are
6
+ * never registered at all. Deriving the catalogue from what actually reached
7
+ * `registerTool` would make `AUDIOBOOKSHELF_ALLOW_TOOLS=add_books_to_collection` report
8
+ * "unknown tool" under `AUDIOBOOKSHELF_READ_ONLY=true`, which is the one answer that
9
+ * is wrong.
10
+ *
11
+ * This is also the full tool surface, hard-coded on purpose: a tool that appears
12
+ * or disappears by accident is a change to the server's contract and has to be a
13
+ * deliberate edit here. `test/tool-filter.test.ts` asserts that these lists and
14
+ * the tools the server really registers are the same set.
15
+ */
16
+ /** Registered always. Every one carries `readOnlyHint: true`. */
17
+ export declare const READ_TOOLS: readonly ["get_author", "get_collection", "get_item_chapters", "get_library", "get_library_filter_data", "get_library_item", "get_library_stats", "get_listening_stats", "get_me", "get_media_progress", "get_personalized_shelves", "get_playlist", "get_podcast_episode", "get_series", "get_server_status", "get_year_stats", "list_authors", "list_bookmarks", "list_collections", "list_genres", "list_items_in_progress", "list_libraries", "list_library_items", "list_listening_sessions", "list_playlists", "list_recent_episodes", "list_series", "list_tags", "search_library"];
18
+ /** Registered unless `AUDIOBOOKSHELF_READ_ONLY` is set. */
19
+ export declare const WRITE_TOOLS: readonly ["add_books_to_collection", "add_items_to_playlist", "create_bookmark", "create_collection", "create_playlist", "delete_bookmark", "delete_collection", "delete_media_progress", "delete_playlist", "remove_books_from_collection", "remove_items_from_playlist", "set_media_progress", "update_bookmark", "update_collection", "update_playlist"];
20
+ /** Every tool, read-only mode aside. */
21
+ export declare const ALL_TOOLS: readonly string[];
22
+ /**
23
+ * What `AUDIOBOOKSHELF_ALLOW_TOOLS=essential` selects: find, inspect, resume.
24
+ *
25
+ * 8 of 44. Left out on purpose: the collection, playlist and bookmark CRUD, and every statistics tool —
26
+ * organisational and retrospective rather than operational.
27
+ */
28
+ export declare const ESSENTIAL_TOOLS: readonly string[];
@@ -0,0 +1,84 @@
1
+ /**
2
+ * The tools this server can register, declared rather than discovered.
3
+ *
4
+ * Declared, because the tool filter has to answer "is this a name you have?"
5
+ * *before* anything is registered — and in read-only mode the write tools are
6
+ * never registered at all. Deriving the catalogue from what actually reached
7
+ * `registerTool` would make `AUDIOBOOKSHELF_ALLOW_TOOLS=add_books_to_collection` report
8
+ * "unknown tool" under `AUDIOBOOKSHELF_READ_ONLY=true`, which is the one answer that
9
+ * is wrong.
10
+ *
11
+ * This is also the full tool surface, hard-coded on purpose: a tool that appears
12
+ * or disappears by accident is a change to the server's contract and has to be a
13
+ * deliberate edit here. `test/tool-filter.test.ts` asserts that these lists and
14
+ * the tools the server really registers are the same set.
15
+ */
16
+ /** Registered always. Every one carries `readOnlyHint: true`. */
17
+ export const READ_TOOLS = [
18
+ 'get_author',
19
+ 'get_collection',
20
+ 'get_item_chapters',
21
+ 'get_library',
22
+ 'get_library_filter_data',
23
+ 'get_library_item',
24
+ 'get_library_stats',
25
+ 'get_listening_stats',
26
+ 'get_me',
27
+ 'get_media_progress',
28
+ 'get_personalized_shelves',
29
+ 'get_playlist',
30
+ 'get_podcast_episode',
31
+ 'get_series',
32
+ 'get_server_status',
33
+ 'get_year_stats',
34
+ 'list_authors',
35
+ 'list_bookmarks',
36
+ 'list_collections',
37
+ 'list_genres',
38
+ 'list_items_in_progress',
39
+ 'list_libraries',
40
+ 'list_library_items',
41
+ 'list_listening_sessions',
42
+ 'list_playlists',
43
+ 'list_recent_episodes',
44
+ 'list_series',
45
+ 'list_tags',
46
+ 'search_library',
47
+ ];
48
+ /** Registered unless `AUDIOBOOKSHELF_READ_ONLY` is set. */
49
+ export const WRITE_TOOLS = [
50
+ 'add_books_to_collection',
51
+ 'add_items_to_playlist',
52
+ 'create_bookmark',
53
+ 'create_collection',
54
+ 'create_playlist',
55
+ 'delete_bookmark',
56
+ 'delete_collection',
57
+ 'delete_media_progress',
58
+ 'delete_playlist',
59
+ 'remove_books_from_collection',
60
+ 'remove_items_from_playlist',
61
+ 'set_media_progress',
62
+ 'update_bookmark',
63
+ 'update_collection',
64
+ 'update_playlist',
65
+ ];
66
+ /** Every tool, read-only mode aside. */
67
+ export const ALL_TOOLS = [...READ_TOOLS, ...WRITE_TOOLS];
68
+ /**
69
+ * What `AUDIOBOOKSHELF_ALLOW_TOOLS=essential` selects: find, inspect, resume.
70
+ *
71
+ * 8 of 44. Left out on purpose: the collection, playlist and bookmark CRUD, and every statistics tool —
72
+ * organisational and retrospective rather than operational.
73
+ */
74
+ export const ESSENTIAL_TOOLS = [
75
+ 'list_libraries',
76
+ 'search_library',
77
+ 'list_library_items',
78
+ 'get_library_item',
79
+ 'get_item_chapters',
80
+ 'list_items_in_progress',
81
+ 'get_media_progress',
82
+ 'set_media_progress',
83
+ ];
84
+ //# sourceMappingURL=catalogue.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalogue.js","sourceRoot":"","sources":["../../src/tools/catalogue.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,iEAAiE;AACjE,MAAM,CAAC,MAAM,UAAU,GAAG;IACxB,YAAY;IACZ,gBAAgB;IAChB,mBAAmB;IACnB,aAAa;IACb,yBAAyB;IACzB,kBAAkB;IAClB,mBAAmB;IACnB,qBAAqB;IACrB,QAAQ;IACR,oBAAoB;IACpB,0BAA0B;IAC1B,cAAc;IACd,qBAAqB;IACrB,YAAY;IACZ,mBAAmB;IACnB,gBAAgB;IAChB,cAAc;IACd,gBAAgB;IAChB,kBAAkB;IAClB,aAAa;IACb,wBAAwB;IACxB,gBAAgB;IAChB,oBAAoB;IACpB,yBAAyB;IACzB,gBAAgB;IAChB,sBAAsB;IACtB,aAAa;IACb,WAAW;IACX,gBAAgB;CACR,CAAC;AAEX,2DAA2D;AAC3D,MAAM,CAAC,MAAM,WAAW,GAAG;IACzB,yBAAyB;IACzB,uBAAuB;IACvB,iBAAiB;IACjB,mBAAmB;IACnB,iBAAiB;IACjB,iBAAiB;IACjB,mBAAmB;IACnB,uBAAuB;IACvB,iBAAiB;IACjB,8BAA8B;IAC9B,4BAA4B;IAC5B,oBAAoB;IACpB,iBAAiB;IACjB,mBAAmB;IACnB,iBAAiB;CACT,CAAC;AAEX,wCAAwC;AACxC,MAAM,CAAC,MAAM,SAAS,GAAsB,CAAC,GAAG,UAAU,EAAE,GAAG,WAAW,CAAC,CAAC;AAE5E;;;;;GAKG;AACH,MAAM,CAAC,MAAM,eAAe,GAAsB;IAChD,gBAAgB;IAChB,gBAAgB;IAChB,oBAAoB;IACpB,kBAAkB;IAClB,mBAAmB;IACnB,wBAAwB;IACxB,oBAAoB;IACpB,oBAAoB;CACrB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "audiobookshelf-mcp",
3
- "version": "0.1.4",
3
+ "version": "0.2.0",
4
4
  "mcpName": "io.github.ni-c/audiobookshelf-mcp",
5
5
  "description": "MCP server for Audiobookshelf, the self-hosted audiobook and podcast server",
6
6
  "keywords": [