calibreweb-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 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,167 @@
1
+ # calibreweb-mcp
2
+
3
+ [![CI](https://img.shields.io/github/actions/workflow/status/ni-c/calibreweb-mcp/ci.yml?branch=main&label=CI)](https://github.com/ni-c/calibreweb-mcp/actions/workflows/ci.yml)
4
+ [![npm version](https://img.shields.io/npm/v/calibreweb-mcp)](https://www.npmjs.com/package/calibreweb-mcp)
5
+ [![node](https://img.shields.io/node/v/calibreweb-mcp)](https://nodejs.org)
6
+ [![license](https://img.shields.io/npm/l/calibreweb-mcp)](LICENSE)
7
+ [![container](https://img.shields.io/badge/ghcr.io-ni--c%2Fcalibreweb--mcp-2496ed?logo=docker&logoColor=white)](https://github.com/ni-c/calibreweb-mcp/pkgs/container/calibreweb-mcp)
8
+
9
+ A read-only [Model Context Protocol](https://modelcontextprotocol.io) server for
10
+ [Calibre-Web](https://github.com/janeczku/calibre-web) (and
11
+ [Calibre-Web Automated](https://github.com/crocodilestick/Calibre-Web-Automated)),
12
+ the self-hosted ebook library web UI.
13
+
14
+ Calibre-Web has no REST API — its only stable machine-readable interface is the
15
+ OPDS catalog feed it serves for e-reader apps. This server speaks that feed:
16
+ Atom XML with HTTP Basic auth in, structured book data out. Search the library,
17
+ browse the curated views and shelves, follow per-format download links, and pull
18
+ cover images straight into the conversation. It never writes anything: every
19
+ tool is a GET against the OPDS routes.
20
+
21
+ <!-- <picture> is resolved against the colour scheme of the page showing it, so GitHub
22
+ picks the variant that matches its own theme toggle. npm strips <picture> and
23
+ <source> when it sanitises the README and keeps the <img>, which is why that
24
+ fallback brings its own dark card instead of relying on a media query. -->
25
+ <picture>
26
+ <source media="(prefers-color-scheme: dark)" srcset="https://calibreweb-mcp.ni-c.de/architecture-dark.svg">
27
+ <source media="(prefers-color-scheme: light)" srcset="https://calibreweb-mcp.ni-c.de/architecture-light.svg">
28
+ <img src="https://calibreweb-mcp.ni-c.de/architecture.svg" alt="An MCP client speaks stdio to calibreweb-mcp, which reads the OPDS feed of Calibre-Web over HTTPS with Basic auth" width="800">
29
+ </picture>
30
+
31
+ ## Requirements
32
+
33
+ - Node.js 22 or newer
34
+ - A Calibre-Web instance (developed against the current
35
+ `linuxserver/calibre-web` image; Calibre-Web Automated works the same way)
36
+ - A Calibre-Web user for the server. The OPDS feed authenticates with the
37
+ normal web login — use a **dedicated account with only the View and Download
38
+ roles**, not your admin account. If the instance allows anonymous browsing,
39
+ the server can also run without credentials.
40
+
41
+ ## Configuration
42
+
43
+ | Variable | Required | Description |
44
+ | -------------------------- | -------- | ------------------------------------------------------------------------------------------------------- |
45
+ | `CALIBRE_WEB_URL` | yes | Root URL of the instance, e.g. `https://books.example.com`. The `/opds` path is appended automatically. |
46
+ | `CALIBRE_WEB_USERNAME` | yes¹ | Username of the Calibre-Web account. |
47
+ | `CALIBRE_WEB_PASSWORD` | yes¹ | Password of that account (the web login password). |
48
+ | `CALIBRE_WEB_INSECURE_TLS` | no | `true` to accept self-signed certificates — scoped to the configured host only. |
49
+
50
+ ¹ Leave **both** unset for an instance that allows anonymous browsing; setting
51
+ only one of them is a configuration error.
52
+
53
+ ### Claude Code
54
+
55
+ ```sh
56
+ claude mcp add calibreweb \
57
+ -e CALIBRE_WEB_URL=https://books.example.com \
58
+ -e CALIBRE_WEB_USERNAME=reader \
59
+ -e CALIBRE_WEB_PASSWORD=... \
60
+ -- npx calibreweb-mcp
61
+ ```
62
+
63
+ ### Claude Desktop
64
+
65
+ ```json
66
+ {
67
+ "mcpServers": {
68
+ "calibreweb": {
69
+ "command": "npx",
70
+ "args": ["calibreweb-mcp"],
71
+ "env": {
72
+ "CALIBRE_WEB_URL": "https://books.example.com",
73
+ "CALIBRE_WEB_USERNAME": "reader",
74
+ "CALIBRE_WEB_PASSWORD": "..."
75
+ }
76
+ }
77
+ }
78
+ }
79
+ ```
80
+
81
+ ### Codex
82
+
83
+ ```toml
84
+ [mcp_servers.calibreweb]
85
+ command = "npx"
86
+ args = ["calibreweb-mcp"]
87
+ env = { CALIBRE_WEB_URL = "https://books.example.com", CALIBRE_WEB_USERNAME = "reader", CALIBRE_WEB_PASSWORD = "..." }
88
+ ```
89
+
90
+ ## Tools
91
+
92
+ All tools are read-only (`readOnlyHint: true`).
93
+
94
+ | Tool | Description |
95
+ | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
96
+ | `search_books` | Search by title, author, series, publisher and tags. Calibre-Web returns every match at once; the result is capped client-side (`limit`, default 50) and reports the real match count. |
97
+ | `list_books` | Book listings by view: `new` (default), `hot`, `rated`, `discover` (random), `read`, `unread`, or `all` (optionally narrowed to an initial letter). |
98
+ | `list_shelves` | Public shelves plus the configured user's own shelves. |
99
+ | `get_shelf_books` | The books on a shelf, in shelf order. |
100
+ | `get_cover` | A book's cover, returned as an image the client can display. |
101
+ | `get_stats` | Total books, authors, categories and series. |
102
+
103
+ Book entries include authors, tags, series (with index), rating, a bounded
104
+ summary, a cover URL and per-format download URLs — ready-made links a human can
105
+ open, since the model itself has no reason to download an EPUB.
106
+
107
+ ### Pagination
108
+
109
+ Feeds are paginated by the instance's _books per page_ setting (default 60); the
110
+ page size is not client-controllable. Every listing returns
111
+ `pagination.nextOffset` when more pages exist — pass it as `offset` in the next
112
+ call. The `discover` view is a random selection and not paginated.
113
+
114
+ ### Deliberately out of scope
115
+
116
+ - **No writes.** The OPDS feed has none, and this server would not add any.
117
+ - **No file downloads.** Tools return download URLs, not ebook payloads.
118
+ - **No facet browsing** (authors/series/tags/publishers/languages/formats as
119
+ their own index feeds). `search_books` covers those lookups; the routes exist
120
+ and tools for them can be added if there is a real use case.
121
+
122
+ ## Safety
123
+
124
+ - The server is read-only by construction — GET requests only, no state anywhere.
125
+ - Book metadata is untrusted third-party data; every result says so, control
126
+ characters are stripped, and XML entity processing is disabled (documents
127
+ declaring a DOCTYPE or entities are refused outright).
128
+ - Responses are bounded before parsing (8 MB feeds, 1 MB covers) and again
129
+ before they reach the model (per-book and per-response budgets).
130
+ - Feed hrefs are only passed through when they resolve to the configured
131
+ origin over http(s) — a hostile feed cannot plant `javascript:`, `file:` or
132
+ cross-origin URLs into the results.
133
+ - Redirects are refused so the Basic credentials can never be replayed to
134
+ another host; covers are only passed through for real image content types.
135
+ - The password is scrubbed from the process environment at startup, and URLs
136
+ are credential-redacted before they appear in any log or result.
137
+
138
+ ## Container
139
+
140
+ ```sh
141
+ docker run -i --rm \
142
+ -e CALIBRE_WEB_URL=https://books.example.com \
143
+ -e CALIBRE_WEB_USERNAME=reader \
144
+ -e CALIBRE_WEB_PASSWORD=... \
145
+ ghcr.io/ni-c/calibreweb-mcp
146
+ ```
147
+
148
+ ## Development
149
+
150
+ ```sh
151
+ npm install
152
+ npm test # unit tests against a stubbed OPDS feed, no instance needed
153
+ npm run lint
154
+ npm run build
155
+ ```
156
+
157
+ See [CONTRIBUTING.md](CONTRIBUTING.md).
158
+
159
+ ## Releasing
160
+
161
+ 1. Update `CHANGELOG.md` and bump the version in `package.json` (+ lockfile).
162
+ 2. `npm run lint && npm run test:coverage && npm run build`
163
+ 3. Tag the release: `git tag -s vX.Y.Z -m "vX.Y.Z" && git push origin vX.Y.Z`
164
+
165
+ ## License
166
+
167
+ [MIT](LICENSE)
package/dist/api.d.ts ADDED
@@ -0,0 +1,33 @@
1
+ import { type Config } from './config.js';
2
+ export declare class CalibreWebApiError 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 Calibre-Web OPDS endpoints, using HTTP Basic auth. */
8
+ export declare class CalibreWebApi {
9
+ private readonly config;
10
+ private readonly baseUrl;
11
+ /** Unset when both credentials are absent — anonymous-browsing instances. */
12
+ private readonly authHeader?;
13
+ /**
14
+ * Only set when `CALIBRE_WEB_INSECURE_TLS` is enabled. Scopes the relaxed
15
+ * certificate validation to requests against the configured host instead of
16
+ * disabling it process-wide via NODE_TLS_REJECT_UNAUTHORIZED.
17
+ */
18
+ private readonly insecureDispatcher?;
19
+ constructor(config: Config);
20
+ /** Base URL for absolutizing feed hrefs; empty string when unconfigured. */
21
+ get url(): string;
22
+ private send;
23
+ /** Fetches an OPDS feed and returns the parsed XML document. */
24
+ getFeed(path: string, params?: Record<string, string | number | undefined>): Promise<unknown>;
25
+ /** Fetches a JSON endpoint (`/opds/stats`). */
26
+ getJson(path: string): Promise<unknown>;
27
+ /** Fetches a binary body (cover images), bounded by {@link MAX_COVER_BYTES}. */
28
+ getBinary(path: string): Promise<{
29
+ data: Buffer;
30
+ contentType: string;
31
+ }>;
32
+ private isConfiguredOrigin;
33
+ }
package/dist/api.js ADDED
@@ -0,0 +1,231 @@
1
+ import { XMLParser } from 'fast-xml-parser';
2
+ import { Agent, fetch as undiciFetch, } from 'undici';
3
+ import { missingConfigKeys, missingConfigMessage, } from './config.js';
4
+ const REQUEST_TIMEOUT_MS = 30_000;
5
+ /**
6
+ * Ceiling on a single feed response. The OPDS search endpoint is not paginated
7
+ * server-side — a broad query on a huge library returns every match in one
8
+ * document — and the per-tool budgets in `shape.ts` and `result.ts` only trim
9
+ * data that is already resident as a string. 8 MB is far above any legitimate
10
+ * feed page and far below trouble.
11
+ */
12
+ const MAX_FEED_BYTES = 8 * 1024 * 1024;
13
+ /**
14
+ * Ceiling on a cover image. Calibre-Web serves the full-size cover on the OPDS
15
+ * cover route (the thumbnail aliases are client compat only). The result goes
16
+ * into the client's context as base64 (+33%), so this is deliberately tight —
17
+ * an oversized scan is better retrieved out-of-band via the book's coverUrl.
18
+ */
19
+ const MAX_COVER_BYTES = 1 * 1024 * 1024;
20
+ export class CalibreWebApiError extends Error {
21
+ status;
22
+ body;
23
+ constructor(status, body, method, path) {
24
+ super(`Calibre-Web ${method} ${path} failed with HTTP ${status}`);
25
+ this.status = status;
26
+ this.body = body;
27
+ this.name = 'CalibreWebApiError';
28
+ }
29
+ }
30
+ /**
31
+ * The parser is deliberately dumb: no entity processing (the five XML built-ins
32
+ * and numeric references are decoded — with a control-character guard — in
33
+ * `shape.ts`), no value coercion (a book titled "1984" must stay a string), and
34
+ * the xhtml `<content>` blob is kept as a raw string for `htmlToText` instead
35
+ * of being exploded into objects.
36
+ */
37
+ const feedParser = new XMLParser({
38
+ ignoreAttributes: false,
39
+ attributeNamePrefix: '@_',
40
+ processEntities: false,
41
+ parseTagValue: false,
42
+ parseAttributeValue: false,
43
+ isArray: (name) => ['entry', 'link', 'author', 'category', 'dcterms:language'].includes(name),
44
+ stopNodes: ['feed.entry.content'],
45
+ });
46
+ /**
47
+ * Refuses any document that declares a DTD or entities. The parser above does
48
+ * not process entities, so there is no local expansion exposure — this guard
49
+ * exists so that can never silently change with a parser update, and because a
50
+ * legitimate Calibre-Web feed simply never contains a DOCTYPE.
51
+ */
52
+ function assertNoDoctype(xml, path) {
53
+ if (/<!(doctype|entity)\b/i.test(xml)) {
54
+ throw new Error(`Calibre-Web GET ${path} returned XML containing a DOCTYPE or ENTITY ` +
55
+ 'declaration, which this server refuses to parse.');
56
+ }
57
+ }
58
+ /** Minimal client for the Calibre-Web OPDS endpoints, using HTTP Basic auth. */
59
+ export class CalibreWebApi {
60
+ config;
61
+ baseUrl;
62
+ /** Unset when both credentials are absent — anonymous-browsing instances. */
63
+ authHeader;
64
+ /**
65
+ * Only set when `CALIBRE_WEB_INSECURE_TLS` is enabled. Scopes the relaxed
66
+ * certificate validation to requests against the configured host instead of
67
+ * disabling it process-wide via NODE_TLS_REJECT_UNAUTHORIZED.
68
+ */
69
+ insecureDispatcher;
70
+ constructor(config) {
71
+ this.config = config;
72
+ this.baseUrl = config.url ?? '';
73
+ if (config.username && config.password) {
74
+ this.authHeader = `Basic ${Buffer.from(`${config.username}:${config.password}`).toString('base64')}`;
75
+ }
76
+ if (config.insecureTls) {
77
+ this.insecureDispatcher = new Agent({
78
+ connect: { rejectUnauthorized: false },
79
+ });
80
+ }
81
+ }
82
+ /** Base URL for absolutizing feed hrefs; empty string when unconfigured. */
83
+ get url() {
84
+ return this.baseUrl;
85
+ }
86
+ async send(path, accept, params) {
87
+ // The credentials are only required here, not at startup, so the server can
88
+ // still be started and introspected without them.
89
+ const missing = missingConfigKeys(this.config);
90
+ if (missing.length > 0) {
91
+ throw new Error(missingConfigMessage(missing));
92
+ }
93
+ const headers = { Accept: accept };
94
+ if (this.authHeader !== undefined) {
95
+ headers.Authorization = this.authHeader;
96
+ }
97
+ const init = {
98
+ method: 'GET',
99
+ headers,
100
+ // Never follow a redirect: it would resend the Basic credentials to
101
+ // whatever host the upstream points at. (Calibre-Web redirects to its
102
+ // HTML login page when Basic auth is refused behind some proxies — that
103
+ // case surfaces as an explicit error here instead.)
104
+ redirect: 'error',
105
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
106
+ };
107
+ const search = new URLSearchParams();
108
+ for (const [key, value] of Object.entries(params ?? {})) {
109
+ if (value !== undefined)
110
+ search.set(key, String(value));
111
+ }
112
+ const query = search.size > 0 ? `?${search.toString()}` : '';
113
+ const url = `${this.baseUrl}${path}${query}`;
114
+ // The insecure dispatcher requires undici's own fetch; the default path
115
+ // uses the (stubbable) global fetch. Only requests that actually go to the
116
+ // configured instance may use the relaxed dispatcher.
117
+ const useInsecure = this.insecureDispatcher !== undefined && this.isConfiguredOrigin(url);
118
+ const response = useInsecure
119
+ ? (await undiciFetch(url, {
120
+ ...init,
121
+ dispatcher: this.insecureDispatcher,
122
+ }))
123
+ : await fetch(url, init);
124
+ return {
125
+ status: response.status,
126
+ ok: response.ok,
127
+ headers: response.headers,
128
+ response,
129
+ };
130
+ }
131
+ /** Fetches an OPDS feed and returns the parsed XML document. */
132
+ async getFeed(path, params) {
133
+ const { ok, status, response } = await this.send(path, 'application/atom+xml', params);
134
+ const bytes = await readBoundedBody(response, path, MAX_FEED_BYTES);
135
+ const text = bytes.toString('utf8');
136
+ if (!ok) {
137
+ throw new CalibreWebApiError(status, text, 'GET', path);
138
+ }
139
+ const trimmed = text.trimStart();
140
+ if (/^(<!doctype\s+html|<html[\s>])/i.test(trimmed)) {
141
+ throw new Error(`Calibre-Web GET ${path} returned an HTML page instead of an Atom feed — ` +
142
+ 'CALIBRE_WEB_URL is probably not the root of the Calibre-Web instance, ' +
143
+ 'or a proxy in front of it answered with a login page.');
144
+ }
145
+ assertNoDoctype(text, path);
146
+ try {
147
+ return feedParser.parse(text);
148
+ }
149
+ catch {
150
+ throw new Error(`Calibre-Web GET ${path} did not return parseable Atom XML — check CALIBRE_WEB_URL.`);
151
+ }
152
+ }
153
+ /** Fetches a JSON endpoint (`/opds/stats`). */
154
+ async getJson(path) {
155
+ const { ok, status, response } = await this.send(path, 'application/json');
156
+ const bytes = await readBoundedBody(response, path, MAX_FEED_BYTES);
157
+ const text = bytes.toString('utf8');
158
+ if (!ok) {
159
+ throw new CalibreWebApiError(status, text, 'GET', path);
160
+ }
161
+ try {
162
+ return JSON.parse(text);
163
+ }
164
+ catch {
165
+ throw new Error(`Calibre-Web GET ${path} did not return valid JSON — check CALIBRE_WEB_URL.`);
166
+ }
167
+ }
168
+ /** Fetches a binary body (cover images), bounded by {@link MAX_COVER_BYTES}. */
169
+ async getBinary(path) {
170
+ const { ok, status, headers, response } = await this.send(path, 'image/*');
171
+ const data = await readBoundedBody(response, path, MAX_COVER_BYTES);
172
+ if (!ok) {
173
+ throw new CalibreWebApiError(status, data.toString('utf8'), 'GET', path);
174
+ }
175
+ return { data, contentType: headers.get('content-type') ?? '' };
176
+ }
177
+ isConfiguredOrigin(url) {
178
+ try {
179
+ return new URL(url).origin === new URL(this.baseUrl).origin;
180
+ }
181
+ catch {
182
+ return false;
183
+ }
184
+ }
185
+ }
186
+ function hasStreamingBody(body) {
187
+ return (typeof body === 'object' &&
188
+ body !== null &&
189
+ typeof body.getReader === 'function');
190
+ }
191
+ /**
192
+ * Reads a response body, refusing anything past `maxBytes`.
193
+ *
194
+ * A declared `content-length` is rejected before a single byte is read; a
195
+ * chunked response is aborted as soon as the accumulated size crosses the
196
+ * ceiling. Responses without a streamable body — which is what the test stubs
197
+ * of global `fetch` return — fall back to `arrayBuffer()` and are checked
198
+ * afterwards.
199
+ */
200
+ async function readBoundedBody(response, path, maxBytes) {
201
+ const tooLarge = () => new Error(`Calibre-Web GET ${path} returned a response larger than ` +
202
+ `${maxBytes} bytes and was refused.`);
203
+ const declared = Number(response.headers.get('content-length'));
204
+ if (Number.isFinite(declared) && declared > maxBytes)
205
+ throw tooLarge();
206
+ const body = response.body;
207
+ if (!hasStreamingBody(body)) {
208
+ const buffer = Buffer.from(await response.arrayBuffer());
209
+ if (buffer.byteLength > maxBytes)
210
+ throw tooLarge();
211
+ return buffer;
212
+ }
213
+ const reader = body.getReader();
214
+ const chunks = [];
215
+ let total = 0;
216
+ for (;;) {
217
+ const { done, value } = await reader.read();
218
+ if (done)
219
+ break;
220
+ if (value === undefined)
221
+ continue;
222
+ total += value.byteLength;
223
+ if (total > maxBytes) {
224
+ await reader.cancel();
225
+ throw tooLarge();
226
+ }
227
+ chunks.push(value);
228
+ }
229
+ return Buffer.concat(chunks);
230
+ }
231
+ //# sourceMappingURL=api.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EACL,KAAK,EACL,KAAK,IAAI,WAAW,GAErB,MAAM,QAAQ,CAAC;AAEhB,OAAO,EACL,iBAAiB,EACjB,oBAAoB,GAErB,MAAM,aAAa,CAAC;AAErB,MAAM,kBAAkB,GAAG,MAAM,CAAC;AAElC;;;;;;GAMG;AACH,MAAM,cAAc,GAAG,CAAC,GAAG,IAAI,GAAG,IAAI,CAAC;AAEvC;;;;;GAKG;AACH,MAAM,eAAe,GAAG,CAAC,GAAG,IAAI,GAAG,IAAI,CAAC;AAExC,MAAM,OAAO,kBAAmB,SAAQ,KAAK;IAEzB;IACA;IAFlB,YACkB,MAAc,EACd,IAAY,EAC5B,MAAc,EACd,IAAY;QAEZ,KAAK,CAAC,eAAe,MAAM,IAAI,IAAI,qBAAqB,MAAM,EAAE,CAAC,CAAC;QALlD,WAAM,GAAN,MAAM,CAAQ;QACd,SAAI,GAAJ,IAAI,CAAQ;QAK5B,IAAI,CAAC,IAAI,GAAG,oBAAoB,CAAC;IACnC,CAAC;CACF;AAED;;;;;;GAMG;AACH,MAAM,UAAU,GAAG,IAAI,SAAS,CAAC;IAC/B,gBAAgB,EAAE,KAAK;IACvB,mBAAmB,EAAE,IAAI;IACzB,eAAe,EAAE,KAAK;IACtB,aAAa,EAAE,KAAK;IACpB,mBAAmB,EAAE,KAAK;IAC1B,OAAO,EAAE,CAAC,IAAI,EAAE,EAAE,CAChB,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,UAAU,EAAE,kBAAkB,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC;IAC5E,SAAS,EAAE,CAAC,oBAAoB,CAAC;CAClC,CAAC,CAAC;AAEH;;;;;GAKG;AACH,SAAS,eAAe,CAAC,GAAW,EAAE,IAAY;IAChD,IAAI,uBAAuB,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;QACtC,MAAM,IAAI,KAAK,CACb,mBAAmB,IAAI,+CAA+C;YACpE,kDAAkD,CACrD,CAAC;IACJ,CAAC;AACH,CAAC;AAED,gFAAgF;AAChF,MAAM,OAAO,aAAa;IACP,MAAM,CAAS;IACf,OAAO,CAAS;IACjC,6EAA6E;IAC5D,UAAU,CAAU;IACrC;;;;OAIG;IACc,kBAAkB,CAAS;IAE5C,YAAY,MAAc;QACxB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC,GAAG,IAAI,EAAE,CAAC;QAChC,IAAI,MAAM,CAAC,QAAQ,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC;YACvC,IAAI,CAAC,UAAU,GAAG,SAAS,MAAM,CAAC,IAAI,CACpC,GAAG,MAAM,CAAC,QAAQ,IAAI,MAAM,CAAC,QAAQ,EAAE,CACxC,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;QACzB,CAAC;QACD,IAAI,MAAM,CAAC,WAAW,EAAE,CAAC;YACvB,IAAI,CAAC,kBAAkB,GAAG,IAAI,KAAK,CAAC;gBAClC,OAAO,EAAE,EAAE,kBAAkB,EAAE,KAAK,EAAE;aACvC,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,4EAA4E;IAC5E,IAAI,GAAG;QACL,OAAO,IAAI,CAAC,OAAO,CAAC;IACtB,CAAC;IAEO,KAAK,CAAC,IAAI,CAChB,IAAY,EACZ,MAAc,EACd,MAAoD;QAOpD,4EAA4E;QAC5E,kDAAkD;QAClD,MAAM,OAAO,GAAG,iBAAiB,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAC/C,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACvB,MAAM,IAAI,KAAK,CAAC,oBAAoB,CAAC,OAAO,CAAC,CAAC,CAAC;QACjD,CAAC;QAED,MAAM,OAAO,GAA2B,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;QAC3D,IAAI,IAAI,CAAC,UAAU,KAAK,SAAS,EAAE,CAAC;YAClC,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,UAAU,CAAC;QAC1C,CAAC;QACD,MAAM,IAAI,GAAgB;YACxB,MAAM,EAAE,KAAK;YACb,OAAO;YACP,oEAAoE;YACpE,sEAAsE;YACtE,wEAAwE;YACxE,oDAAoD;YACpD,QAAQ,EAAE,OAAO;YACjB,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,kBAAkB,CAAC;SAChD,CAAC;QAEF,MAAM,MAAM,GAAG,IAAI,eAAe,EAAE,CAAC;QACrC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,IAAI,EAAE,CAAC,EAAE,CAAC;YACxD,IAAI,KAAK,KAAK,SAAS;gBAAE,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;QAC1D,CAAC;QACD,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC7D,MAAM,GAAG,GAAG,GAAG,IAAI,CAAC,OAAO,GAAG,IAAI,GAAG,KAAK,EAAE,CAAC;QAE7C,wEAAwE;QACxE,2EAA2E;QAC3E,sDAAsD;QACtD,MAAM,WAAW,GACf,IAAI,CAAC,kBAAkB,KAAK,SAAS,IAAI,IAAI,CAAC,kBAAkB,CAAC,GAAG,CAAC,CAAC;QACxE,MAAM,QAAQ,GAAG,WAAW;YAC1B,CAAC,CAAE,CAAC,MAAM,WAAW,CAAC,GAAG,EAAE;gBACvB,GAAG,IAAI;gBACP,UAAU,EAAE,IAAI,CAAC,kBAAkB;aACf,CAAC,CAAyB;YAClD,CAAC,CAAC,MAAM,KAAK,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QAC3B,OAAO;YACL,MAAM,EAAE,QAAQ,CAAC,MAAM;YACvB,EAAE,EAAE,QAAQ,CAAC,EAAE;YACf,OAAO,EAAE,QAAQ,CAAC,OAAO;YACzB,QAAQ;SACT,CAAC;IACJ,CAAC;IAED,gEAAgE;IAChE,KAAK,CAAC,OAAO,CACX,IAAY,EACZ,MAAoD;QAEpD,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,MAAM,IAAI,CAAC,IAAI,CAC9C,IAAI,EACJ,sBAAsB,EACtB,MAAM,CACP,CAAC;QACF,MAAM,KAAK,GAAG,MAAM,eAAe,CAAC,QAAQ,EAAE,IAAI,EAAE,cAAc,CAAC,CAAC;QACpE,MAAM,IAAI,GAAG,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;QACpC,IAAI,CAAC,EAAE,EAAE,CAAC;YACR,MAAM,IAAI,kBAAkB,CAAC,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC;QAC1D,CAAC;QACD,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC;QACjC,IAAI,iCAAiC,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;YACpD,MAAM,IAAI,KAAK,CACb,mBAAmB,IAAI,mDAAmD;gBACxE,wEAAwE;gBACxE,uDAAuD,CAC1D,CAAC;QACJ,CAAC;QACD,eAAe,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QAC5B,IAAI,CAAC;YACH,OAAO,UAAU,CAAC,KAAK,CAAC,IAAI,CAAY,CAAC;QAC3C,CAAC;QAAC,MAAM,CAAC;YACP,MAAM,IAAI,KAAK,CACb,mBAAmB,IAAI,6DAA6D,CACrF,CAAC;QACJ,CAAC;IACH,CAAC;IAED,+CAA+C;IAC/C,KAAK,CAAC,OAAO,CAAC,IAAY;QACxB,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,kBAAkB,CAAC,CAAC;QAC3E,MAAM,KAAK,GAAG,MAAM,eAAe,CAAC,QAAQ,EAAE,IAAI,EAAE,cAAc,CAAC,CAAC;QACpE,MAAM,IAAI,GAAG,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;QACpC,IAAI,CAAC,EAAE,EAAE,CAAC;YACR,MAAM,IAAI,kBAAkB,CAAC,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC;QAC1D,CAAC;QACD,IAAI,CAAC;YACH,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAY,CAAC;QACrC,CAAC;QAAC,MAAM,CAAC;YACP,MAAM,IAAI,KAAK,CACb,mBAAmB,IAAI,qDAAqD,CAC7E,CAAC;QACJ,CAAC;IACH,CAAC;IAED,gFAAgF;IAChF,KAAK,CAAC,SAAS,CACb,IAAY;QAEZ,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;QAC3E,MAAM,IAAI,GAAG,MAAM,eAAe,CAAC,QAAQ,EAAE,IAAI,EAAE,eAAe,CAAC,CAAC;QACpE,IAAI,CAAC,EAAE,EAAE,CAAC;YACR,MAAM,IAAI,kBAAkB,CAAC,MAAM,EAAE,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC;QAC3E,CAAC;QACD,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,IAAI,EAAE,EAAE,CAAC;IAClE,CAAC;IAEO,kBAAkB,CAAC,GAAW;QACpC,IAAI,CAAC;YACH,OAAO,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC,MAAM,KAAK,IAAI,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC;QAC9D,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC;CACF;AAUD,SAAS,gBAAgB,CAAC,IAAa;IACrC,OAAO,CACL,OAAO,IAAI,KAAK,QAAQ;QACxB,IAAI,KAAK,IAAI;QACb,OAAQ,IAAsB,CAAC,SAAS,KAAK,UAAU,CACxD,CAAC;AACJ,CAAC;AAED;;;;;;;;GAQG;AACH,KAAK,UAAU,eAAe,CAC5B,QAIC,EACD,IAAY,EACZ,QAAgB;IAEhB,MAAM,QAAQ,GAAG,GAAU,EAAE,CAC3B,IAAI,KAAK,CACP,mBAAmB,IAAI,mCAAmC;QACxD,GAAG,QAAQ,yBAAyB,CACvC,CAAC;IAEJ,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC,CAAC;IAChE,IAAI,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,QAAQ,GAAG,QAAQ;QAAE,MAAM,QAAQ,EAAE,CAAC;IAEvE,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,CAAC;IAC3B,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,EAAE,CAAC;QAC5B,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,QAAQ,CAAC,WAAW,EAAE,CAAC,CAAC;QACzD,IAAI,MAAM,CAAC,UAAU,GAAG,QAAQ;YAAE,MAAM,QAAQ,EAAE,CAAC;QACnD,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,MAAM,MAAM,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC;IAChC,MAAM,MAAM,GAAiB,EAAE,CAAC;IAChC,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,SAAS,CAAC;QACR,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;QAC5C,IAAI,IAAI;YAAE,MAAM;QAChB,IAAI,KAAK,KAAK,SAAS;YAAE,SAAS;QAClC,KAAK,IAAI,KAAK,CAAC,UAAU,CAAC;QAC1B,IAAI,KAAK,GAAG,QAAQ,EAAE,CAAC;YACrB,MAAM,MAAM,CAAC,MAAM,EAAE,CAAC;YACtB,MAAM,QAAQ,EAAE,CAAC;QACnB,CAAC;QACD,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACrB,CAAC;IACD,OAAO,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;AAC/B,CAAC"}
@@ -0,0 +1,32 @@
1
+ export interface Config {
2
+ /**
3
+ * Base URL of the Calibre-Web instance, e.g. `https://books.example.com` —
4
+ * the root of the web UI; the `/opds` path is appended automatically. May be
5
+ * undefined together with the credentials: the server still starts and lists
6
+ * its tools, every API call then fails with {@link missingConfigMessage}.
7
+ */
8
+ url: string | undefined;
9
+ username: string | undefined;
10
+ /** The normal web-login password of the Calibre-Web user (OPDS uses HTTP Basic auth). */
11
+ password: string | undefined;
12
+ insecureTls: boolean;
13
+ }
14
+ /** Shown when the configuration is incomplete — at startup and on every API call. */
15
+ export declare function missingConfigMessage(missing: string[]): string;
16
+ /**
17
+ * Names of the required environment variables that are unset in `config`.
18
+ *
19
+ * Username and password count as a pair: both unset is a supported mode
20
+ * (instances with anonymous browsing serve the OPDS feed without auth), only
21
+ * one of them set is a configuration error.
22
+ */
23
+ export declare function missingConfigKeys(config: Config): string[];
24
+ /**
25
+ * Reads the configuration from environment variables.
26
+ *
27
+ * Missing credentials are only a warning, not a fatal error: the server must be
28
+ * able to complete the MCP handshake and answer `tools/list` without them, so
29
+ * registries and sandbox inspectors can introspect it. A malformed URL still
30
+ * exits — that one could send the credentials to the wrong host.
31
+ */
32
+ export declare function loadConfig(env?: NodeJS.ProcessEnv): Config;
package/dist/config.js ADDED
@@ -0,0 +1,98 @@
1
+ import { redactUrlCredentials } from './redact.js';
2
+ /** Shown when the configuration is incomplete — at startup and on every API call. */
3
+ export function missingConfigMessage(missing) {
4
+ return (`missing required environment variable(s): ${missing.join(', ')}\n` +
5
+ 'Required: CALIBRE_WEB_URL (e.g. https://books.example.com), ' +
6
+ 'CALIBRE_WEB_USERNAME, CALIBRE_WEB_PASSWORD\n' +
7
+ 'The credentials are the normal web login of a Calibre-Web user; the OPDS ' +
8
+ 'feed authenticates with HTTP Basic auth. If the instance allows anonymous ' +
9
+ 'browsing, leave BOTH username and password unset.\n' +
10
+ 'Optional: CALIBRE_WEB_INSECURE_TLS=true to accept self-signed certificates');
11
+ }
12
+ /**
13
+ * Names of the required environment variables that are unset in `config`.
14
+ *
15
+ * Username and password count as a pair: both unset is a supported mode
16
+ * (instances with anonymous browsing serve the OPDS feed without auth), only
17
+ * one of them set is a configuration error.
18
+ */
19
+ export function missingConfigKeys(config) {
20
+ const missing = [];
21
+ if (!config.url)
22
+ missing.push('CALIBRE_WEB_URL');
23
+ if (!config.username !== !config.password) {
24
+ missing.push(!config.username ? 'CALIBRE_WEB_USERNAME' : 'CALIBRE_WEB_PASSWORD');
25
+ }
26
+ return missing;
27
+ }
28
+ /**
29
+ * Reads the configuration from environment variables.
30
+ *
31
+ * Missing credentials are only a warning, not a fatal error: the server must be
32
+ * able to complete the MCP handshake and answer `tools/list` without them, so
33
+ * registries and sandbox inspectors can introspect it. A malformed URL still
34
+ * exits — that one could send the credentials to the wrong host.
35
+ */
36
+ export function loadConfig(env = process.env) {
37
+ const url = env.CALIBRE_WEB_URL;
38
+ const username = env.CALIBRE_WEB_USERNAME;
39
+ const password = env.CALIBRE_WEB_PASSWORD;
40
+ const insecureTls = env.CALIBRE_WEB_INSECURE_TLS === 'true';
41
+ // Don't keep the password in process.env for the process lifetime: it would be
42
+ // inherited by child processes and show up in env dumps. (The kernel's
43
+ // /proc/<pid>/environ snapshot is NOT rewritten by this — it always keeps the
44
+ // startup environment.) Deleted before any early return below so no code path
45
+ // leaves it behind.
46
+ delete env.CALIBRE_WEB_PASSWORD;
47
+ if (!url) {
48
+ console.error(`calibreweb-mcp: ${missingConfigMessage(['CALIBRE_WEB_URL'])}`);
49
+ return { url: undefined, username, password, insecureTls };
50
+ }
51
+ if (!username !== !password) {
52
+ console.error('calibreweb-mcp: CALIBRE_WEB_USERNAME and CALIBRE_WEB_PASSWORD must be ' +
53
+ 'set together (or both left unset for an instance with anonymous browsing)');
54
+ }
55
+ let parsed;
56
+ try {
57
+ parsed = new URL(url);
58
+ }
59
+ catch {
60
+ // Redacted, and deliberately so: the userinfo check below only runs once the
61
+ // URL parses, so a value that does not parse at all but still carries
62
+ // credentials — "https://admin:s3cret@host:99999", an out-of-range port —
63
+ // would otherwise print the password into the MCP client's log file.
64
+ console.error(`calibreweb-mcp: CALIBRE_WEB_URL is not a valid URL: ${redactUrlCredentials(url)}`);
65
+ process.exit(1);
66
+ }
67
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
68
+ console.error(`calibreweb-mcp: CALIBRE_WEB_URL must use http:// or https:// (got ${parsed.protocol})`);
69
+ process.exit(1);
70
+ }
71
+ // Credentials embedded in the URL would end up in logs and error messages.
72
+ if (parsed.username || parsed.password) {
73
+ console.error('calibreweb-mcp: CALIBRE_WEB_URL must not contain credentials — use ' +
74
+ 'CALIBRE_WEB_USERNAME and CALIBRE_WEB_PASSWORD');
75
+ process.exit(1);
76
+ }
77
+ if (parsed.protocol === 'http:' && !isLoopbackHost(parsed.hostname)) {
78
+ console.error('calibreweb-mcp: WARNING: CALIBRE_WEB_URL uses plain http to a non-local ' +
79
+ 'host — the password will be sent unencrypted. Use https:// instead.');
80
+ }
81
+ return {
82
+ url: url.replace(/\/+$/, ''),
83
+ username,
84
+ password,
85
+ insecureTls,
86
+ };
87
+ }
88
+ function isLoopbackHost(hostname) {
89
+ // URL.hostname keeps the brackets around an IPv6 literal, so comparing against
90
+ // a bare '::1' never matches and the plain-http warning fires on a loopback
91
+ // URL written as http://[::1]:8083.
92
+ const host = hostname.replace(/^\[|\]$/g, '');
93
+ return (host === 'localhost' ||
94
+ host.endsWith('.localhost') ||
95
+ host.startsWith('127.') ||
96
+ host === '::1');
97
+ }
98
+ //# 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,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env node
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import { loadConfig } from './config.js';
4
+ import { createServer } from './server.js';
5
+ async function main() {
6
+ const config = loadConfig();
7
+ if (config.insecureTls) {
8
+ console.error('calibreweb-mcp: CALIBRE_WEB_INSECURE_TLS=true — TLS certificate validation is disabled for the Calibre-Web connection');
9
+ }
10
+ const server = createServer(config);
11
+ // stdout belongs to the protocol; everything human-readable goes to stderr.
12
+ await server.connect(new StdioServerTransport());
13
+ console.error(config.url
14
+ ? `calibreweb-mcp: connected, targeting ${config.url}`
15
+ : 'calibreweb-mcp: connected without configuration — tools are listed but every call will fail');
16
+ }
17
+ main().catch((error) => {
18
+ console.error('calibreweb-mcp: fatal error:', error);
19
+ process.exit(1);
20
+ });
21
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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,uHAAuH,CACxH,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,wCAAwC,MAAM,CAAC,GAAG,EAAE;QACtD,CAAC,CAAC,6FAA6F,CAClG,CAAC;AACJ,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;IAC9B,OAAO,CAAC,KAAK,CAAC,8BAA8B,EAAE,KAAK,CAAC,CAAC;IACrD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Removes credentials from a URL before it reaches the model or a log.
3
+ *
4
+ * The URLs this server emits are built from `CALIBRE_WEB_URL` plus feed hrefs, so
5
+ * they should never carry userinfo — but a misconfigured value or a reverse proxy
6
+ * rewriting `Location`-style hrefs could smuggle one in, and Basic-auth users are
7
+ * exactly the audience that pastes `https://user:pass@host` into config files.
8
+ */
9
+ export declare function redactUrlCredentials(url: string): string;