annas-mcp 0.1.1 → 0.1.2
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 +13 -10
- package/lib/archive.js +19 -6
- package/lib/launcher.js +28 -6
- package/lib/target.js +16 -1
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/SokolskyNikita/annas-mcp/actions/workflows/test.yml)
|
|
4
4
|
[](https://github.com/SokolskyNikita/annas-mcp/releases)
|
|
5
|
+
[](https://www.npmjs.com/package/annas-mcp)
|
|
5
6
|
|
|
6
7
|
An MCP server that lets Claude, Cursor, Codex and other AI clients search [Anna's Archive](https://annas-archive.gl) for books and papers and download the file you pick. It also works as a command-line tool.
|
|
7
8
|
|
|
@@ -37,19 +38,21 @@ claude mcp add annas-mcp \
|
|
|
37
38
|
--env ANNAS_ACCOUNT_COOKIE=your-cookie \
|
|
38
39
|
--env ANNAS_SECRET_KEY=your-api-key \
|
|
39
40
|
--env ANNAS_DOWNLOAD_PATH=/absolute/path/to/downloads \
|
|
40
|
-
-- npx -y
|
|
41
|
+
-- npx -y annas-mcp
|
|
41
42
|
```
|
|
42
43
|
|
|
43
|
-
On native Windows (not WSL), end the command with `-- cmd /c npx -y
|
|
44
|
+
On native Windows (not WSL), end the command with `-- cmd /c npx -y annas-mcp`.
|
|
44
45
|
|
|
45
|
-
**Claude Desktop
|
|
46
|
+
**Claude Desktop, one click.** Download [annas-mcp.mcpb](https://github.com/SokolskyNikita/annas-mcp/releases/latest/download/annas-mcp.mcpb) and open it, or drag it onto Settings → Extensions. Claude Desktop asks for the cookie, API key and download folder.
|
|
47
|
+
|
|
48
|
+
**Claude Desktop and Cursor, by hand.** Add this to Claude Desktop's config (Settings → Developer → Edit Config) or to Cursor's `~/.cursor/mcp.json`:
|
|
46
49
|
|
|
47
50
|
```json
|
|
48
51
|
{
|
|
49
52
|
"mcpServers": {
|
|
50
53
|
"annas-mcp": {
|
|
51
54
|
"command": "npx",
|
|
52
|
-
"args": ["-y", "
|
|
55
|
+
"args": ["-y", "annas-mcp"],
|
|
53
56
|
"env": {
|
|
54
57
|
"ANNAS_ACCOUNT_COOKIE": "your-cookie",
|
|
55
58
|
"ANNAS_SECRET_KEY": "your-api-key",
|
|
@@ -65,7 +68,7 @@ On native Windows (not WSL), end the command with `-- cmd /c npx -y github:Sokol
|
|
|
65
68
|
```toml
|
|
66
69
|
[mcp_servers.annas-mcp]
|
|
67
70
|
command = "npx"
|
|
68
|
-
args = ["-y", "
|
|
71
|
+
args = ["-y", "annas-mcp"]
|
|
69
72
|
|
|
70
73
|
[mcp_servers.annas-mcp.env]
|
|
71
74
|
ANNAS_ACCOUNT_COOKIE = "your-cookie"
|
|
@@ -147,17 +150,17 @@ Errors start with a code:
|
|
|
147
150
|
|
|
148
151
|
A failed download lists every server it tried and why each one failed. A server starting up cleanly doesn't prove your credentials work; only a search or download tests them.
|
|
149
152
|
|
|
150
|
-
If the server doesn't start at all, run `npx -y
|
|
153
|
+
If the server doesn't start at all, run `npx -y annas-mcp --version` in a terminal to see the error. The launcher unpacks releases with the system's `tar`, which comes preinstalled on macOS, Windows 10 and later, and mainstream Linux distributions.
|
|
151
154
|
|
|
152
155
|
## Command line
|
|
153
156
|
|
|
154
157
|
Every tool is also a command. Set the same variables (or put them in a `.env` file), then:
|
|
155
158
|
|
|
156
159
|
```bash
|
|
157
|
-
npx -y
|
|
158
|
-
npx -y
|
|
159
|
-
npx -y
|
|
160
|
-
npx -y
|
|
160
|
+
npx -y annas-mcp book-search "pride and prejudice" --language en
|
|
161
|
+
npx -y annas-mcp article-search 10.1038/nature14539
|
|
162
|
+
npx -y annas-mcp book-download <hash> "Pride and Prejudice.epub"
|
|
163
|
+
npx -y annas-mcp article-download 10.1038/nature14539
|
|
161
164
|
```
|
|
162
165
|
|
|
163
166
|
Add `--json` for machine-readable output, `--timeout 10m` to change the timeout, and `--help` to any command for all of its options.
|
package/lib/archive.js
CHANGED
|
@@ -34,9 +34,25 @@ export function validateArchiveEntryName(rawName) {
|
|
|
34
34
|
return name;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
// Releases ship .tar.gz because gzip is present on minimal systems where xz
|
|
38
|
+
// often is not. .tar.xz is still understood for older releases.
|
|
39
|
+
function compressionFlag(extension) {
|
|
40
|
+
switch (extension) {
|
|
41
|
+
case "zip":
|
|
42
|
+
return "";
|
|
43
|
+
case "tar.gz":
|
|
44
|
+
return "z";
|
|
45
|
+
case "tar.xz":
|
|
46
|
+
return "J";
|
|
47
|
+
default:
|
|
48
|
+
throw new Error(`Unsupported archive format: ${extension}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
37
52
|
export function validateArchiveEntries(archive, extension, spawnSync = defaultSpawnSync) {
|
|
38
53
|
const tar = tarExecutable();
|
|
39
|
-
const
|
|
54
|
+
const compression = compressionFlag(extension);
|
|
55
|
+
const listingArgs = [`-t${compression}f`, archive];
|
|
40
56
|
const listing = spawnSync(tar, listingArgs, { encoding: "utf8" });
|
|
41
57
|
if (listing.error || listing.status !== 0) {
|
|
42
58
|
throw tarFailure("inspect", archive, listing);
|
|
@@ -47,7 +63,7 @@ export function validateArchiveEntries(archive, extension, spawnSync = defaultSp
|
|
|
47
63
|
}
|
|
48
64
|
}
|
|
49
65
|
|
|
50
|
-
const verboseArgs =
|
|
66
|
+
const verboseArgs = [`-tv${compression}f`, archive];
|
|
51
67
|
const verbose = spawnSync(tar, verboseArgs, { encoding: "utf8" });
|
|
52
68
|
if (verbose.error || verbose.status !== 0) {
|
|
53
69
|
throw tarFailure("inspect", archive, verbose);
|
|
@@ -66,10 +82,7 @@ export function validateArchiveEntries(archive, extension, spawnSync = defaultSp
|
|
|
66
82
|
export function extractArchive(archive, destination, extension, spawnSync = defaultSpawnSync) {
|
|
67
83
|
validateArchiveEntries(archive, extension, spawnSync);
|
|
68
84
|
const tar = tarExecutable();
|
|
69
|
-
const args =
|
|
70
|
-
extension === "zip"
|
|
71
|
-
? ["-xf", archive, "-C", destination]
|
|
72
|
-
: ["-xJf", archive, "-C", destination];
|
|
85
|
+
const args = [`-x${compressionFlag(extension)}f`, archive, "-C", destination];
|
|
73
86
|
const result = spawnSync(tar, args, { encoding: "utf8" });
|
|
74
87
|
if (result.error || result.status !== 0) {
|
|
75
88
|
throw tarFailure("extract", archive, result);
|
package/lib/launcher.js
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
selectAsset,
|
|
28
28
|
selectChecksumAsset,
|
|
29
29
|
sha256Digest,
|
|
30
|
+
targetForRelease,
|
|
30
31
|
} from "./target.js";
|
|
31
32
|
|
|
32
33
|
export const GITHUB_REPO = "SokolskyNikita/annas-mcp";
|
|
@@ -70,12 +71,17 @@ export function isDirectRun(moduleURL = import.meta.url, argv = process.argv) {
|
|
|
70
71
|
}
|
|
71
72
|
}
|
|
72
73
|
|
|
74
|
+
// Windows refuses to replace or open a file while another process is
|
|
75
|
+
// replacing it, so concurrent launchers can briefly collide on current.json.
|
|
76
|
+
const SHARING_ERROR_CODES = new Set(["EPERM", "EACCES", "EBUSY"]);
|
|
77
|
+
const CACHE_META_RENAME_ATTEMPTS = 10;
|
|
78
|
+
|
|
73
79
|
export async function readCacheMeta(dir) {
|
|
74
80
|
try {
|
|
75
81
|
const meta = JSON.parse(await readFile(path.join(dir, "current.json"), "utf8"));
|
|
76
82
|
return meta && typeof meta === "object" && !Array.isArray(meta) ? meta : null;
|
|
77
83
|
} catch (error) {
|
|
78
|
-
if (error.code === "ENOENT" || error instanceof SyntaxError) {
|
|
84
|
+
if (error.code === "ENOENT" || SHARING_ERROR_CODES.has(error.code) || error instanceof SyntaxError) {
|
|
79
85
|
return null;
|
|
80
86
|
}
|
|
81
87
|
throw error;
|
|
@@ -221,11 +227,21 @@ export async function expectedArchiveChecksum(
|
|
|
221
227
|
return expected;
|
|
222
228
|
}
|
|
223
229
|
|
|
224
|
-
export async function writeCacheMeta(dir, meta) {
|
|
230
|
+
export async function writeCacheMeta(dir, meta, { renameImpl = rename, retryDelayMs = 25 } = {}) {
|
|
225
231
|
const temporary = path.join(dir, `.current.json.${process.pid}.${randomUUID()}.tmp`);
|
|
226
232
|
try {
|
|
227
233
|
await writeFile(temporary, `${JSON.stringify(meta)}\n`);
|
|
228
|
-
|
|
234
|
+
for (let attempt = 1; ; attempt += 1) {
|
|
235
|
+
try {
|
|
236
|
+
await renameImpl(temporary, path.join(dir, "current.json"));
|
|
237
|
+
return;
|
|
238
|
+
} catch (error) {
|
|
239
|
+
if (!SHARING_ERROR_CODES.has(error.code) || attempt >= CACHE_META_RENAME_ATTEMPTS) {
|
|
240
|
+
throw error;
|
|
241
|
+
}
|
|
242
|
+
await new Promise((resolve) => setTimeout(resolve, retryDelayMs * attempt));
|
|
243
|
+
}
|
|
244
|
+
}
|
|
229
245
|
} finally {
|
|
230
246
|
await rm(temporary, { force: true });
|
|
231
247
|
}
|
|
@@ -297,7 +313,12 @@ export async function installBinary(
|
|
|
297
313
|
binaryName: target.binaryName,
|
|
298
314
|
},
|
|
299
315
|
};
|
|
300
|
-
|
|
316
|
+
try {
|
|
317
|
+
await writeCacheMeta(root, meta);
|
|
318
|
+
} catch (error) {
|
|
319
|
+
// current.json only points later launches at this verified binary.
|
|
320
|
+
console.error(`annas-mcp: could not record the cached binary. ${error.message}`);
|
|
321
|
+
}
|
|
301
322
|
return binaryPath;
|
|
302
323
|
} finally {
|
|
303
324
|
await rm(work, { recursive: true, force: true });
|
|
@@ -326,7 +347,8 @@ export async function resolveBinary(
|
|
|
326
347
|
}
|
|
327
348
|
try {
|
|
328
349
|
const release = await fetchLatestRelease({ env, fetchImpl });
|
|
329
|
-
const
|
|
350
|
+
const releaseTarget = targetForRelease(release.assets, target);
|
|
351
|
+
const asset = selectAsset(release.assets, releaseTarget);
|
|
330
352
|
const expectedAssetSha256 = await expectedArchiveChecksum(release, asset, { env, fetchImpl });
|
|
331
353
|
const currentRelease = await cachedBinary(
|
|
332
354
|
dir,
|
|
@@ -339,7 +361,7 @@ export async function resolveBinary(
|
|
|
339
361
|
if (currentRelease) {
|
|
340
362
|
return currentRelease;
|
|
341
363
|
}
|
|
342
|
-
return await installBinaryImpl(release, asset,
|
|
364
|
+
return await installBinaryImpl(release, asset, releaseTarget, expectedAssetSha256, {
|
|
343
365
|
env,
|
|
344
366
|
fetchImpl,
|
|
345
367
|
});
|
package/lib/target.js
CHANGED
|
@@ -23,7 +23,7 @@ export function goreleaserTarget(platform = process.platform, arch = process.arc
|
|
|
23
23
|
return {
|
|
24
24
|
os: osName,
|
|
25
25
|
arch: archName,
|
|
26
|
-
extension: osName === "windows" ? "zip" : "tar.
|
|
26
|
+
extension: osName === "windows" ? "zip" : "tar.gz",
|
|
27
27
|
binaryName: osName === "windows" ? "annas-mcp.exe" : "annas-mcp",
|
|
28
28
|
};
|
|
29
29
|
}
|
|
@@ -98,6 +98,21 @@ export function checksumFor(text, filename) {
|
|
|
98
98
|
throw new Error(`Checksum file has no entry for ${filename}`);
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
// Releases before v0.1.2 ship only .tar.xz for non-Windows targets.
|
|
102
|
+
export function targetForRelease(assets, target) {
|
|
103
|
+
if (target.extension !== "tar.gz") {
|
|
104
|
+
return target;
|
|
105
|
+
}
|
|
106
|
+
const suffix = `_${target.os}_${target.arch}.tar.gz`;
|
|
107
|
+
const hasGzip = (assets || []).some(
|
|
108
|
+
(asset) =>
|
|
109
|
+
typeof asset?.name === "string" &&
|
|
110
|
+
asset.name.startsWith("annas-mcp_") &&
|
|
111
|
+
asset.name.endsWith(suffix),
|
|
112
|
+
);
|
|
113
|
+
return hasGzip ? target : { ...target, extension: "tar.xz" };
|
|
114
|
+
}
|
|
115
|
+
|
|
101
116
|
export function selectAsset(assets, target) {
|
|
102
117
|
const suffix = `_${target.os}_${target.arch}.${target.extension}`;
|
|
103
118
|
const matches = (assets || []).filter(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "annas-mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "MCP server and CLI for searching and downloading books and articles from Anna's Archive",
|
|
6
6
|
"mcpName": "io.github.SokolskyNikita/annas-mcp",
|
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
"check:version": "node scripts/check-version.mjs",
|
|
28
28
|
"coverage": "go test -race -coverprofile coverage.out ./... && node scripts/check-coverage.mjs coverage.out",
|
|
29
29
|
"smoke": "node scripts/smoke-mcp.mjs",
|
|
30
|
-
"pack:mcpb": "node scripts/pack-mcpb.mjs"
|
|
30
|
+
"pack:mcpb": "node scripts/pack-mcpb.mjs",
|
|
31
|
+
"publish:smithery": "node scripts/publish-smithery.mjs"
|
|
31
32
|
},
|
|
32
33
|
"bin": {
|
|
33
34
|
"annas-mcp": "bin/annas-mcp.js"
|