calibreweb-mcp 0.1.3 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +133 -20
- package/dist/api.js.map +1 -1
- package/dist/config.d.ts +9 -1
- package/dist/config.js +13 -2
- package/dist/config.js.map +1 -1
- package/dist/index.js +36 -3
- package/dist/index.js.map +1 -1
- package/dist/result.d.ts +26 -4
- package/dist/result.js +64 -19
- package/dist/result.js.map +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/server.js +21 -1
- package/dist/server.js.map +1 -1
- package/dist/shape.d.ts +70 -25
- package/dist/shape.js +123 -2
- package/dist/shape.js.map +1 -1
- package/dist/tools/annotations.d.ts +19 -0
- package/dist/tools/annotations.js +20 -0
- package/dist/tools/annotations.js.map +1 -0
- package/dist/tools/books.d.ts +1 -1
- package/dist/tools/books.js +36 -10
- package/dist/tools/books.js.map +1 -1
- package/dist/tools/catalogue.d.ts +27 -0
- package/dist/tools/catalogue.js +41 -0
- package/dist/tools/catalogue.js.map +1 -0
- package/dist/tools/covers.d.ts +1 -1
- package/dist/tools/covers.js +20 -3
- package/dist/tools/covers.js.map +1 -1
- package/dist/tools/shelves.d.ts +1 -1
- package/dist/tools/shelves.js +24 -10
- package/dist/tools/shelves.js.map +1 -1
- package/dist/tools/stats.d.ts +1 -1
- package/dist/tools/stats.js +12 -2
- package/dist/tools/stats.js.map +1 -1
- package/package.json +15 -11
- package/dist/hosts.d.ts +0 -22
- package/dist/hosts.js +0 -144
- package/dist/hosts.js.map +0 -1
package/README.md
CHANGED
|
@@ -2,23 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/ni-c/calibreweb-mcp/actions/workflows/ci.yml)
|
|
4
4
|
[](https://www.npmjs.com/package/calibreweb-mcp)
|
|
5
|
+
[](https://www.npmjs.com/package/calibreweb-mcp)
|
|
5
6
|
[](https://nodejs.org)
|
|
6
7
|
[](LICENSE)
|
|
7
|
-
[](https://github.com/ni-c/calibreweb-mcp/pkgs/container/calibreweb-mcp)
|
|
9
|
+
[](https://calibreweb-mcp.ni-c.de)
|
|
10
|
+
[](https://mcp-hub.ni-c.de)
|
|
9
11
|
[](https://github.com/sponsors/ni-c)
|
|
10
12
|
|
|
11
|
-
A read-only [Model Context Protocol](https://modelcontextprotocol.io) server for
|
|
13
|
+
A read-only [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server for
|
|
12
14
|
[Calibre-Web](https://github.com/janeczku/calibre-web) (and
|
|
13
15
|
[Calibre-Web Automated](https://github.com/crocodilestick/Calibre-Web-Automated)),
|
|
14
16
|
the self-hosted ebook library web UI.
|
|
15
17
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
Lets MCP clients like Claude Code, Claude Desktop or Codex search your library,
|
|
19
|
+
browse the curated views and shelves, follow per-format download links and pull cover
|
|
20
|
+
images straight into the conversation. It never writes anything: every tool is a GET.
|
|
21
|
+
|
|
22
|
+
Six tools is the ceiling, not the floor: `CALIBRE_WEB_ALLOW_TOOLS=essential`
|
|
23
|
+
registers a curated five instead, and a model picks the right tool far more
|
|
24
|
+
reliably from five than from six — see
|
|
25
|
+
[choosing which tools load](#choosing-which-tools-load).
|
|
26
|
+
|
|
27
|
+
Calibre-Web has no REST API — its only stable machine-readable interface is the OPDS
|
|
28
|
+
catalog feed it serves for e-reader apps. This server speaks that feed: Atom XML with
|
|
29
|
+
HTTP Basic auth in, structured book data out.
|
|
22
30
|
|
|
23
31
|
<!-- <picture> is resolved against the colour scheme of the page showing it, so GitHub
|
|
24
32
|
picks the variant that matches its own theme toggle. npm strips <picture> and
|
|
@@ -35,7 +43,17 @@ tool is a GET against the OPDS routes.
|
|
|
35
43
|
|
|
36
44
|

|
|
37
45
|
|
|
38
|
-
|
|
46
|
+
## What makes it different
|
|
47
|
+
|
|
48
|
+
**The API Calibre-Web never had.** Calibre-Web exposes no REST API — its only
|
|
49
|
+
stable machine interface is the OPDS Atom feed built for e-reader apps. These
|
|
50
|
+
tools parse that feed into structured book data with numeric ids, per-format
|
|
51
|
+
download URLs and bounded summaries.
|
|
52
|
+
|
|
53
|
+
**Read-only by construction.** All six tools are GETs. Redirects are refused so
|
|
54
|
+
Basic credentials never travel, XML carrying a DOCTYPE is rejected outright,
|
|
55
|
+
hrefs are locked to the configured origin, and metadata is marked as the
|
|
56
|
+
untrusted data it is.
|
|
39
57
|
|
|
40
58
|
## Requirements
|
|
41
59
|
|
|
@@ -55,10 +73,35 @@ tool is a GET against the OPDS routes.
|
|
|
55
73
|
| `CALIBRE_WEB_USERNAME` | yes¹ | Username of the Calibre-Web account. |
|
|
56
74
|
| `CALIBRE_WEB_PASSWORD` | yes¹ | Password of that account (the web login password). |
|
|
57
75
|
| `CALIBRE_WEB_INSECURE_TLS` | no | `true` to accept self-signed certificates — scoped to the configured host only. |
|
|
76
|
+
| `CALIBRE_WEB_ALLOW_TOOLS` | no | Comma-separated tool names, `list_*` prefixes, or `essential` for a curated preset |
|
|
77
|
+
| `CALIBRE_WEB_DENY_TOOLS` | no | Same syntax; removed from whatever `CALIBRE_WEB_ALLOW_TOOLS` left |
|
|
58
78
|
|
|
59
79
|
¹ Leave **both** unset for an instance that allows anonymous browsing; setting
|
|
60
80
|
only one of them is a configuration error.
|
|
61
81
|
|
|
82
|
+
### Choosing which tools load
|
|
83
|
+
|
|
84
|
+
`CALIBRE_WEB_ALLOW_TOOLS` and `CALIBRE_WEB_DENY_TOOLS` take comma-separated tool names;
|
|
85
|
+
a trailing `*` matches a whole family. `essential` is a curated preset of
|
|
86
|
+
five: `search_books`, `list_books`, `list_shelves`, `get_shelf_books`, `get_stats`.
|
|
87
|
+
|
|
88
|
+
```sh
|
|
89
|
+
CALIBRE_WEB_ALLOW_TOOLS=essential
|
|
90
|
+
CALIBRE_WEB_ALLOW_TOOLS=search_books,list_shelves
|
|
91
|
+
CALIBRE_WEB_DENY_TOOLS=get_cover
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
An entry that matches no tool aborts startup and names it, so a typo cannot
|
|
95
|
+
silently hide a tool — an absent tool is not something anyone traces back to an
|
|
96
|
+
environment variable. A filtered tool is never registered, so it is absent from
|
|
97
|
+
`tools/list` and unknown to `tools/call` alike.
|
|
98
|
+
|
|
99
|
+
If you run several of these servers at once, [mcp-hub](https://mcp-hub.ni-c.de)
|
|
100
|
+
is the other answer — its `/hub` endpoint replaces every server's tools with six
|
|
101
|
+
meta-tools.
|
|
102
|
+
|
|
103
|
+
## Installation
|
|
104
|
+
|
|
62
105
|
### Claude Code
|
|
63
106
|
|
|
64
107
|
```sh
|
|
@@ -96,6 +139,46 @@ args = ["calibreweb-mcp"]
|
|
|
96
139
|
env = { CALIBRE_WEB_URL = "https://books.example.com", CALIBRE_WEB_USERNAME = "reader", CALIBRE_WEB_PASSWORD = "..." }
|
|
97
140
|
```
|
|
98
141
|
|
|
142
|
+
### Docker
|
|
143
|
+
|
|
144
|
+
```sh
|
|
145
|
+
docker run -i --rm \
|
|
146
|
+
-e CALIBRE_WEB_URL=https://books.example.com \
|
|
147
|
+
-e CALIBRE_WEB_USERNAME=reader \
|
|
148
|
+
-e CALIBRE_WEB_PASSWORD=... \
|
|
149
|
+
ghcr.io/ni-c/calibreweb-mcp
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Through mcp-hub
|
|
153
|
+
|
|
154
|
+
A client that cannot spawn a local process — ChatGPT connectors, Claude on the web,
|
|
155
|
+
Cursor, LibreChat — reaches calibreweb-mcp through [mcp-hub](https://mcp-hub.ni-c.de): one
|
|
156
|
+
container serves many stdio MCP servers over Streamable HTTP, with an OAuth 2.1 login
|
|
157
|
+
behind a single password and long-lived tokens for the clients that cannot do OAuth. Its
|
|
158
|
+
`/hub` endpoint puts every server behind six meta-tools, so one connector reaches all of
|
|
159
|
+
them without N×tool schemas in the model's context, and it speaks both protocol revisions
|
|
160
|
+
— a question this server asks travels through it to the person at the far end.
|
|
161
|
+
|
|
162
|
+
Its `/config/mcp.json` uses Claude Code's format, so the entry is the one you already
|
|
163
|
+
have:
|
|
164
|
+
|
|
165
|
+
```json
|
|
166
|
+
{
|
|
167
|
+
"mcpServers": {
|
|
168
|
+
"calibreweb": {
|
|
169
|
+
"command": "npx",
|
|
170
|
+
"args": ["-y", "calibreweb-mcp"],
|
|
171
|
+
"env": { "CALIBRE_WEB_ALLOW_TOOLS": "essential" },
|
|
172
|
+
"denyTools": ["get_cover"]
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
`allowTools` and `denyTools` there are the hub's **own** per-server filter, which is not
|
|
179
|
+
the same thing as `*_ALLOW_TOOLS` in `env` — the difference, and the mistake it invites,
|
|
180
|
+
are in the [client guide](https://calibreweb-mcp.ni-c.de/guide/clients#through-mcp-hub).
|
|
181
|
+
|
|
99
182
|
## Tools
|
|
100
183
|
|
|
101
184
|
All tools are read-only (`readOnlyHint: true`).
|
|
@@ -113,6 +196,34 @@ Book entries include authors, tags, series (with index), rating, a bounded
|
|
|
113
196
|
summary, a cover URL and per-format download URLs — ready-made links a human can
|
|
114
197
|
open, since the model itself has no reason to download an EPUB.
|
|
115
198
|
|
|
199
|
+
### Structured output
|
|
200
|
+
|
|
201
|
+
Every tool declares an `outputSchema` and answers with `structuredContent`
|
|
202
|
+
alongside the text block, so a client can use the result without parsing prose:
|
|
203
|
+
|
|
204
|
+
```jsonc
|
|
205
|
+
{
|
|
206
|
+
"untrusted": true,
|
|
207
|
+
"source": "calibre-web",
|
|
208
|
+
"totalFound": 2,
|
|
209
|
+
"truncated": false,
|
|
210
|
+
"books": [{ "id": 7, "title": "Dune", "authors": ["Frank Herbert"] }],
|
|
211
|
+
"notes": ["Book titles, authors, tags, series and summaries come from …"],
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
The `untrusted` marker is a field and not only a line in `notes`, because a
|
|
216
|
+
client that reads the structured half would otherwise have to find the warning
|
|
217
|
+
in a list of sentences. The two tools without it are `get_stats`, which is four
|
|
218
|
+
counters this server has checked are numbers, and `get_cover`, which reports an
|
|
219
|
+
id, a media type from a four-entry allowlist and a byte count — the image itself
|
|
220
|
+
stays in the content block where a client renders it.
|
|
221
|
+
|
|
222
|
+
An over-budget result drops book summaries as before. Where that is still not
|
|
223
|
+
enough it is now an **error** rather than JSON cut at the ceiling: unparseable
|
|
224
|
+
text was tolerable in a text block and is not something `structuredContent` can
|
|
225
|
+
carry, and the two channels have to hold the same value.
|
|
226
|
+
|
|
116
227
|
### Pagination
|
|
117
228
|
|
|
118
229
|
Feeds are paginated by the instance's _books per page_ setting (default 60); the
|
|
@@ -120,7 +231,7 @@ page size is not client-controllable. Every listing returns
|
|
|
120
231
|
`pagination.nextOffset` when more pages exist — pass it as `offset` in the next
|
|
121
232
|
call. The `discover` view is a random selection and not paginated.
|
|
122
233
|
|
|
123
|
-
|
|
234
|
+
## Not exposed, on purpose
|
|
124
235
|
|
|
125
236
|
- **No writes.** The OPDS feed has none, and this server would not add any.
|
|
126
237
|
- **No file downloads.** Tools return download URLs, not ebook payloads.
|
|
@@ -144,15 +255,10 @@ call. The `discover` view is a random selection and not paginated.
|
|
|
144
255
|
- The password is scrubbed from the process environment at startup, and URLs
|
|
145
256
|
are credential-redacted before they appear in any log or result.
|
|
146
257
|
|
|
147
|
-
##
|
|
258
|
+
## Documentation
|
|
148
259
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
-e CALIBRE_WEB_URL=https://books.example.com \
|
|
152
|
-
-e CALIBRE_WEB_USERNAME=reader \
|
|
153
|
-
-e CALIBRE_WEB_PASSWORD=... \
|
|
154
|
-
ghcr.io/ni-c/calibreweb-mcp
|
|
155
|
-
```
|
|
260
|
+
The full guide, tool reference and security notes live at
|
|
261
|
+
**[calibreweb-mcp.ni-c.de](https://calibreweb-mcp.ni-c.de)** (source in [`docs/`](docs/)).
|
|
156
262
|
|
|
157
263
|
## Development
|
|
158
264
|
|
|
@@ -171,6 +277,13 @@ See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
|
171
277
|
2. `npm run lint && npm run test:coverage && npm run build`
|
|
172
278
|
3. Tag the release: `git tag -s vX.Y.Z -m "vX.Y.Z" && git push origin vX.Y.Z`
|
|
173
279
|
|
|
280
|
+
## Contributing
|
|
281
|
+
|
|
282
|
+
Issues, discussions and pull requests are welcome — see
|
|
283
|
+
[CONTRIBUTING.md](CONTRIBUTING.md). For vulnerabilities please use
|
|
284
|
+
[private reporting](https://github.com/ni-c/calibreweb-mcp/security/advisories/new)
|
|
285
|
+
rather than a public issue; the policy is in [SECURITY.md](SECURITY.md).
|
|
286
|
+
|
|
174
287
|
## License
|
|
175
288
|
|
|
176
|
-
[MIT](LICENSE)
|
|
289
|
+
[MIT](LICENSE) © Willi Thiel
|
package/dist/api.js.map
CHANGED
|
@@ -1 +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;
|
|
1
|
+
{"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"}
|
package/dist/config.d.ts
CHANGED
|
@@ -9,7 +9,15 @@ export interface Config {
|
|
|
9
9
|
username: string | undefined;
|
|
10
10
|
/** The normal web-login password of the Calibre-Web user (OPDS uses HTTP Basic auth). */
|
|
11
11
|
password: string | undefined;
|
|
12
|
-
insecureTls: boolean;
|
|
12
|
+
insecureTls: boolean; /**
|
|
13
|
+
* Raw value of `CALIBRE_WEB_ALLOW_TOOLS` — comma-separated tool names, `list_*`
|
|
14
|
+
* prefixes, or `essential`. Kept unparsed on purpose: this file is a mirror of
|
|
15
|
+
* the environment, and the names can only be checked against the tool
|
|
16
|
+
* catalogue, which `buildToolFilter` does.
|
|
17
|
+
*/
|
|
18
|
+
allowTools: string | undefined;
|
|
19
|
+
/** Raw value of `CALIBRE_WEB_DENY_TOOLS`, same shape, subtracted from the above. */
|
|
20
|
+
denyTools: string | undefined;
|
|
13
21
|
}
|
|
14
22
|
/** Shown when the configuration is incomplete — at startup and on every API call. */
|
|
15
23
|
export declare function missingConfigMessage(missing: string[]): string;
|
package/dist/config.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { internalHostKind } from '
|
|
1
|
+
import { internalHostKind } from 'mcp-internal-hosts';
|
|
2
2
|
import { redactUrlCredentials } from './redact.js';
|
|
3
3
|
/** Shown when the configuration is incomplete — at startup and on every API call. */
|
|
4
4
|
export function missingConfigMessage(missing) {
|
|
@@ -39,6 +39,8 @@ export function loadConfig(env = process.env) {
|
|
|
39
39
|
const username = env.CALIBRE_WEB_USERNAME;
|
|
40
40
|
const password = env.CALIBRE_WEB_PASSWORD;
|
|
41
41
|
const insecureTls = env.CALIBRE_WEB_INSECURE_TLS === 'true';
|
|
42
|
+
const allowTools = env.CALIBRE_WEB_ALLOW_TOOLS;
|
|
43
|
+
const denyTools = env.CALIBRE_WEB_DENY_TOOLS;
|
|
42
44
|
// Don't keep the password in process.env for the process lifetime: it would be
|
|
43
45
|
// inherited by child processes and show up in env dumps. (The kernel's
|
|
44
46
|
// /proc/<pid>/environ snapshot is NOT rewritten by this — it always keeps the
|
|
@@ -47,7 +49,14 @@ export function loadConfig(env = process.env) {
|
|
|
47
49
|
delete env.CALIBRE_WEB_PASSWORD;
|
|
48
50
|
if (!url) {
|
|
49
51
|
console.error(`calibreweb-mcp: ${missingConfigMessage(['CALIBRE_WEB_URL'])}`);
|
|
50
|
-
return {
|
|
52
|
+
return {
|
|
53
|
+
url: undefined,
|
|
54
|
+
username,
|
|
55
|
+
password,
|
|
56
|
+
insecureTls,
|
|
57
|
+
allowTools,
|
|
58
|
+
denyTools,
|
|
59
|
+
};
|
|
51
60
|
}
|
|
52
61
|
if (!username !== !password) {
|
|
53
62
|
console.error('calibreweb-mcp: CALIBRE_WEB_USERNAME and CALIBRE_WEB_PASSWORD must be ' +
|
|
@@ -84,6 +93,8 @@ export function loadConfig(env = process.env) {
|
|
|
84
93
|
username,
|
|
85
94
|
password,
|
|
86
95
|
insecureTls,
|
|
96
|
+
allowTools,
|
|
97
|
+
denyTools,
|
|
87
98
|
};
|
|
88
99
|
}
|
|
89
100
|
function isLoopbackHost(hostname) {
|
package/dist/config.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -1,19 +1,52 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { serveStdio } from '@modelcontextprotocol/server/stdio';
|
|
3
3
|
import { loadConfig } from './config.js';
|
|
4
4
|
import { createServer } from './server.js';
|
|
5
|
+
import { ToolFilterError } from 'mcp-tool-allowlist';
|
|
5
6
|
async function main() {
|
|
6
7
|
const config = loadConfig();
|
|
7
8
|
if (config.insecureTls) {
|
|
8
9
|
console.error('calibreweb-mcp: CALIBRE_WEB_INSECURE_TLS=true — TLS certificate validation is disabled for the Calibre-Web connection');
|
|
9
10
|
}
|
|
10
|
-
|
|
11
|
+
// Built before anything is served, so a rejected tool filter still ends
|
|
12
|
+
// the process rather than surfacing as a failed handshake once a client
|
|
13
|
+
// has already connected.
|
|
14
|
+
let pending;
|
|
15
|
+
try {
|
|
16
|
+
pending = 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(`calibreweb-mcp: ${error.message}`);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
11
27
|
// stdout belongs to the protocol; everything human-readable goes to stderr.
|
|
12
|
-
|
|
28
|
+
// `serveStdio` owns the era decision for the connection: the opening
|
|
29
|
+
// exchange selects 2025-11-25 or 2026-07-28 and pins one instance from
|
|
30
|
+
// this factory for its lifetime. A hand-wired `StdioServerTransport`
|
|
31
|
+
// serves only the 2025 era, which is why a negotiating client’s
|
|
32
|
+
// `server/discover` probe was answered with "Method not found".
|
|
33
|
+
//
|
|
34
|
+
// The instance built above serves the first connection; a second call — a
|
|
35
|
+
// modern probe followed by the real connection — builds a fresh one, which
|
|
36
|
+
// is safe because `createServer` only registers tools.
|
|
37
|
+
serveStdio(() => {
|
|
38
|
+
const server = pending ?? createServer(config);
|
|
39
|
+
pending = undefined;
|
|
40
|
+
return server;
|
|
41
|
+
});
|
|
13
42
|
console.error(config.url
|
|
14
43
|
? `calibreweb-mcp: connected, targeting ${config.url}`
|
|
15
44
|
: 'calibreweb-mcp: connected without configuration — tools are listed but every call will fail');
|
|
16
45
|
}
|
|
46
|
+
// In a container node runs as PID 1 with no default signal disposition, so
|
|
47
|
+
// without this handler `docker stop` waits out the grace period and SIGKILLs.
|
|
48
|
+
process.on('SIGTERM', () => process.exit(0));
|
|
49
|
+
process.on('SIGINT', () => process.exit(0));
|
|
17
50
|
main().catch((error) => {
|
|
18
51
|
console.error('calibreweb-mcp: fatal error:', error);
|
|
19
52
|
process.exit(1);
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,UAAU,EAAE,MAAM,oCAAoC,CAAC;AAEhE,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAErD,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,wEAAwE;IACxE,wEAAwE;IACxE,yBAAyB;IACzB,IAAI,OAA8B,CAAC;IACnC,IAAI,CAAC;QACH,OAAO,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC;IACjC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,+DAA+D;QAC/D,yDAAyD;QACzD,IAAI,KAAK,YAAY,eAAe,EAAE,CAAC;YACrC,OAAO,CAAC,KAAK,CAAC,mBAAmB,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;YAClD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;QACD,MAAM,KAAK,CAAC;IACd,CAAC;IACD,4EAA4E;IAC5E,qEAAqE;IACrE,uEAAuE;IACvE,qEAAqE;IACrE,gEAAgE;IAChE,gEAAgE;IAChE,EAAE;IACF,0EAA0E;IAC1E,2EAA2E;IAC3E,uDAAuD;IACvD,UAAU,CAAC,GAAG,EAAE;QACd,MAAM,MAAM,GAAG,OAAO,IAAI,YAAY,CAAC,MAAM,CAAC,CAAC;QAC/C,OAAO,GAAG,SAAS,CAAC;QACpB,OAAO,MAAM,CAAC;IAChB,CAAC,CAAC,CAAC;IACH,OAAO,CAAC,KAAK,CACX,MAAM,CAAC,GAAG;QACR,CAAC,CAAC,wCAAwC,MAAM,CAAC,GAAG,EAAE;QACtD,CAAC,CAAC,6FAA6F,CAClG,CAAC;AACJ,CAAC;AAED,2EAA2E;AAC3E,8EAA8E;AAC9E,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;AAC7C,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;AAE5C,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"}
|
package/dist/result.d.ts
CHANGED
|
@@ -1,14 +1,36 @@
|
|
|
1
|
-
import type { CallToolResult } from '@modelcontextprotocol/
|
|
1
|
+
import type { CallToolResult } from '@modelcontextprotocol/server';
|
|
2
2
|
export declare function textResult(text: string): CallToolResult;
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
* pathologically large after the per-tool truncation.
|
|
4
|
+
* An answer in both channels at once, with book summaries stripped if the
|
|
5
|
+
* payload is still pathologically large after the per-tool truncation.
|
|
6
6
|
*
|
|
7
7
|
* A Calibre library can hold book descriptions of arbitrary length, and the
|
|
8
8
|
* OPDS search endpoint returns every match in one feed. Everything downstream
|
|
9
9
|
* of this function assumes the budget held; this is what guarantees it.
|
|
10
|
+
*
|
|
11
|
+
* `structuredContent` is the machine-readable half and the reason every tool
|
|
12
|
+
* here declares an `outputSchema`; the text block stays because the SDK does
|
|
13
|
+
* NOT synthesize one for an object-shaped value, and a client that reads only
|
|
14
|
+
* `content` would otherwise get an empty answer. Both carry the same object —
|
|
15
|
+
* which is why the over-budget path rebuilds the *value* rather than editing
|
|
16
|
+
* its serialization.
|
|
10
17
|
*/
|
|
11
|
-
export declare function jsonResult(data: unknown): CallToolResult;
|
|
18
|
+
export declare function jsonResult(data: Record<string, unknown>): CallToolResult;
|
|
19
|
+
/**
|
|
20
|
+
* {@link jsonResult}, with the untrusted-content marker on the object.
|
|
21
|
+
*
|
|
22
|
+
* The note has always gone out in `notes`, which is in the text block and in
|
|
23
|
+
* the structured half alike. The two fields are what a client reading only
|
|
24
|
+
* `structuredContent` can *check* rather than have to find in a list of
|
|
25
|
+
* sentences — and they are stripped from the payload before they are set, so
|
|
26
|
+
* the guard cannot be switched off by the content it guards against.
|
|
27
|
+
*/
|
|
28
|
+
export declare function untrustedResult(data: Record<string, unknown>): CallToolResult;
|
|
29
|
+
/** Raised by {@link jsonResult}; `run` turns it into an error result. */
|
|
30
|
+
export declare class ResultTooLargeError extends Error {
|
|
31
|
+
}
|
|
32
|
+
/** A value in both channels, with no budget applied. */
|
|
33
|
+
export declare function structuredResult(data: Record<string, unknown>): CallToolResult;
|
|
12
34
|
export declare function errorResult(text: string): CallToolResult;
|
|
13
35
|
/** Thrown by tools for problems detected before any request goes out. */
|
|
14
36
|
export declare class ToolInputError extends Error {
|
package/dist/result.js
CHANGED
|
@@ -9,31 +9,72 @@ export function textResult(text) {
|
|
|
9
9
|
*/
|
|
10
10
|
const MAX_RESULT_BYTES = 400_000;
|
|
11
11
|
/**
|
|
12
|
-
*
|
|
13
|
-
* pathologically large after the per-tool truncation.
|
|
12
|
+
* An answer in both channels at once, with book summaries stripped if the
|
|
13
|
+
* payload is still pathologically large after the per-tool truncation.
|
|
14
14
|
*
|
|
15
15
|
* A Calibre library can hold book descriptions of arbitrary length, and the
|
|
16
16
|
* OPDS search endpoint returns every match in one feed. Everything downstream
|
|
17
17
|
* of this function assumes the budget held; this is what guarantees it.
|
|
18
|
+
*
|
|
19
|
+
* `structuredContent` is the machine-readable half and the reason every tool
|
|
20
|
+
* here declares an `outputSchema`; the text block stays because the SDK does
|
|
21
|
+
* NOT synthesize one for an object-shaped value, and a client that reads only
|
|
22
|
+
* `content` would otherwise get an empty answer. Both carry the same object —
|
|
23
|
+
* which is why the over-budget path rebuilds the *value* rather than editing
|
|
24
|
+
* its serialization.
|
|
18
25
|
*/
|
|
19
26
|
export function jsonResult(data) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const stripped = JSON.stringify(data, (key, value) => key === 'summary' && typeof value === 'string'
|
|
27
|
+
if (JSON.stringify(data).length <= MAX_RESULT_BYTES) {
|
|
28
|
+
return structuredResult(data);
|
|
29
|
+
}
|
|
30
|
+
const stripped = JSON.parse(JSON.stringify(data, (key, value) => key === 'summary' && typeof value === 'string'
|
|
24
31
|
? '(omitted: result too large)'
|
|
25
|
-
: value
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
32
|
+
: value));
|
|
33
|
+
if (JSON.stringify(stripped).length <= MAX_RESULT_BYTES) {
|
|
34
|
+
return structuredResult({
|
|
35
|
+
...stripped,
|
|
36
|
+
notes: [
|
|
37
|
+
...(Array.isArray(stripped.notes) ? stripped.notes : []),
|
|
38
|
+
`The result exceeded ${MAX_RESULT_BYTES} characters, so book summaries were dropped. Narrow the request to get them back.`,
|
|
39
|
+
],
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
// Dropping summaries is not always enough: the bulk can sit in fields the
|
|
30
43
|
// replacer does not touch — a feed of thousands of books is all titles and
|
|
31
|
-
// URLs.
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
'
|
|
44
|
+
// URLs. This used to answer with the JSON cut at the ceiling, unparseable but
|
|
45
|
+
// visible. That is no longer an option: `structuredContent` has to parse, and
|
|
46
|
+
// the two channels have to carry the same value. So it is an error, which is
|
|
47
|
+
// the honest description of "there is no answer this size".
|
|
48
|
+
throw new ResultTooLargeError(`The result exceeds ${MAX_RESULT_BYTES} characters even without book ` +
|
|
49
|
+
'summaries. Narrow the request — use a more specific query, a lower ' +
|
|
50
|
+
'limit, or the offset parameter.');
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* {@link jsonResult}, with the untrusted-content marker on the object.
|
|
54
|
+
*
|
|
55
|
+
* The note has always gone out in `notes`, which is in the text block and in
|
|
56
|
+
* the structured half alike. The two fields are what a client reading only
|
|
57
|
+
* `structuredContent` can *check* rather than have to find in a list of
|
|
58
|
+
* sentences — and they are stripped from the payload before they are set, so
|
|
59
|
+
* the guard cannot be switched off by the content it guards against.
|
|
60
|
+
*/
|
|
61
|
+
export function untrustedResult(data) {
|
|
62
|
+
const { untrusted: _untrusted, source: _source, ...rest } = data;
|
|
63
|
+
return jsonResult({
|
|
64
|
+
untrusted: true,
|
|
65
|
+
source: 'calibre-web',
|
|
66
|
+
...rest,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
/** Raised by {@link jsonResult}; `run` turns it into an error result. */
|
|
70
|
+
export class ResultTooLargeError extends Error {
|
|
71
|
+
}
|
|
72
|
+
/** A value in both channels, with no budget applied. */
|
|
73
|
+
export function structuredResult(data) {
|
|
74
|
+
return {
|
|
75
|
+
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
|
|
76
|
+
structuredContent: data,
|
|
77
|
+
};
|
|
37
78
|
}
|
|
38
79
|
export function errorResult(text) {
|
|
39
80
|
return { content: [{ type: 'text', text }], isError: true };
|
|
@@ -51,7 +92,10 @@ const UNSAFE_CHARS =
|
|
|
51
92
|
*/
|
|
52
93
|
function sanitizeErrorBody(body) {
|
|
53
94
|
const trimmed = body.replace(UNSAFE_CHARS, '').trim();
|
|
54
|
-
|
|
95
|
+
// Anything markup-shaped: a reverse proxy's error page or a WAF block page.
|
|
96
|
+
// The check is deliberately loose — an XML declaration, a leading comment or
|
|
97
|
+
// a doctype followed by a newline are all the same thing here.
|
|
98
|
+
if (/^(<!doctype|<html[\s>]|<\?xml|<!--)/i.test(trimmed)) {
|
|
55
99
|
return '(HTML error page omitted)';
|
|
56
100
|
}
|
|
57
101
|
if (trimmed.length > MAX_ERROR_BODY_LENGTH) {
|
|
@@ -92,7 +136,8 @@ export async function run(fn) {
|
|
|
92
136
|
return await fn();
|
|
93
137
|
}
|
|
94
138
|
catch (error) {
|
|
95
|
-
if (error instanceof ToolInputError
|
|
139
|
+
if (error instanceof ToolInputError ||
|
|
140
|
+
error instanceof ResultTooLargeError) {
|
|
96
141
|
return errorResult(error.message);
|
|
97
142
|
}
|
|
98
143
|
if (error instanceof CalibreWebApiError) {
|
package/dist/result.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"result.js","sourceRoot":"","sources":["../src/result.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAE9C,MAAM,UAAU,UAAU,CAAC,IAAY;IACrC,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;AAC/C,CAAC;AAED;;;;GAIG;AACH,MAAM,gBAAgB,GAAG,OAAO,CAAC;AAEjC
|
|
1
|
+
{"version":3,"file":"result.js","sourceRoot":"","sources":["../src/result.ts"],"names":[],"mappings":"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"}
|
package/dist/server.d.ts
CHANGED
package/dist/server.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { createRequire } from 'node:module';
|
|
2
|
-
import { McpServer } from '@modelcontextprotocol/
|
|
2
|
+
import { McpServer } from '@modelcontextprotocol/server';
|
|
3
|
+
import { buildToolFilter, installToolFilter } from 'mcp-tool-allowlist';
|
|
4
|
+
import { ALL_TOOLS, ESSENTIAL_TOOLS } from './tools/catalogue.js';
|
|
3
5
|
import { CalibreWebApi } from './api.js';
|
|
4
6
|
import { registerBookTools } from './tools/books.js';
|
|
5
7
|
import { registerCoverTools } from './tools/covers.js';
|
|
@@ -16,11 +18,29 @@ function packageVersion() {
|
|
|
16
18
|
}
|
|
17
19
|
}
|
|
18
20
|
export function createServer(config) {
|
|
21
|
+
// Before anything is built: an unusable tool list should fail on the
|
|
22
|
+
// way in, not leave a server running with tools quietly missing.
|
|
23
|
+
const filter = buildToolFilter({
|
|
24
|
+
allowTools: config.allowTools,
|
|
25
|
+
denyTools: config.denyTools,
|
|
26
|
+
catalogue: {
|
|
27
|
+
all: ALL_TOOLS,
|
|
28
|
+
essential: ESSENTIAL_TOOLS,
|
|
29
|
+
},
|
|
30
|
+
names: {
|
|
31
|
+
allow: 'CALIBRE_WEB_ALLOW_TOOLS',
|
|
32
|
+
deny: 'CALIBRE_WEB_DENY_TOOLS',
|
|
33
|
+
server: 'calibreweb-mcp',
|
|
34
|
+
},
|
|
35
|
+
});
|
|
19
36
|
const api = new CalibreWebApi(config);
|
|
20
37
|
const server = new McpServer({
|
|
21
38
|
name: 'calibreweb-mcp',
|
|
22
39
|
version: packageVersion(),
|
|
23
40
|
});
|
|
41
|
+
// Wraps server.registerTool, so it has to sit before the first
|
|
42
|
+
// register call and does not care how they are organised.
|
|
43
|
+
installToolFilter(server, filter);
|
|
24
44
|
// Every tool is read-only: the OPDS feed has no write surface, so there is
|
|
25
45
|
// no read-only mode to configure and nothing destructive to confirm.
|
|
26
46
|
registerBookTools(server, api);
|
package/dist/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAC;AACzD,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAExE,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAElE,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAEtD,SAAS,cAAc;IACrB,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,aAAa,CAAC,OAAO,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;QAC7B,UAAU,EAAE,MAAM,CAAC,UAAU;QAC7B,SAAS,EAAE,MAAM,CAAC,SAAS;QAC3B,SAAS,EAAE;YACT,GAAG,EAAE,SAAS;YACd,SAAS,EAAE,eAAe;SAC3B;QACD,KAAK,EAAE;YACL,KAAK,EAAE,yBAAyB;YAChC,IAAI,EAAE,wBAAwB;YAC9B,MAAM,EAAE,gBAAgB;SACzB;KACF,CAAC,CAAC;IAEH,MAAM,GAAG,GAAG,IAAI,aAAa,CAAC,MAAM,CAAC,CAAC;IAEtC,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC;QAC3B,IAAI,EAAE,gBAAgB;QACtB,OAAO,EAAE,cAAc,EAAE;KAC1B,CAAC,CAAC;IAEH,+DAA+D;IAC/D,0DAA0D;IAC1D,iBAAiB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAElC,2EAA2E;IAC3E,qEAAqE;IACrE,iBAAiB,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC/B,kBAAkB,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAChC,kBAAkB,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAChC,kBAAkB,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAEhC,OAAO,MAAM,CAAC;AAChB,CAAC"}
|