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 CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  [![Tests](https://github.com/SokolskyNikita/annas-mcp/actions/workflows/test.yml/badge.svg)](https://github.com/SokolskyNikita/annas-mcp/actions/workflows/test.yml)
4
4
  [![Release](https://img.shields.io/github/v/release/SokolskyNikita/annas-mcp)](https://github.com/SokolskyNikita/annas-mcp/releases)
5
+ [![npm](https://img.shields.io/npm/v/annas-mcp)](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 github:SokolskyNikita/annas-mcp
41
+ -- npx -y annas-mcp
41
42
  ```
42
43
 
43
- On native Windows (not WSL), end the command with `-- cmd /c npx -y github:SokolskyNikita/annas-mcp`.
44
+ On native Windows (not WSL), end the command with `-- cmd /c npx -y annas-mcp`.
44
45
 
45
- **Claude Desktop and Cursor.** Add this to Claude Desktop's config (Settings → Developer → Edit Config) or to Cursor's `~/.cursor/mcp.json`:
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", "github:SokolskyNikita/annas-mcp"],
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", "github:SokolskyNikita/annas-mcp"]
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 github:SokolskyNikita/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.
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 github:SokolskyNikita/annas-mcp book-search "pride and prejudice" --language en
158
- npx -y github:SokolskyNikita/annas-mcp article-search 10.1038/nature14539
159
- npx -y github:SokolskyNikita/annas-mcp book-download <hash> "Pride and Prejudice.epub"
160
- npx -y github:SokolskyNikita/annas-mcp article-download 10.1038/nature14539
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 listingArgs = extension === "zip" ? ["-tf", archive] : ["-tJf", archive];
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 = extension === "zip" ? ["-tvf", archive] : ["-tvJf", archive];
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
- await rename(temporary, path.join(dir, "current.json"));
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
- await writeCacheMeta(root, meta);
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 asset = selectAsset(release.assets, target);
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, target, expectedAssetSha256, {
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.xz",
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.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"