mcp-rule34 1.1.0 → 2.0.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/CHANGELOG.md ADDED
@@ -0,0 +1,88 @@
1
+ # Changelog
2
+
3
+ ## 2.0.0
4
+
5
+ - **This server now needs node 24 or later.** Node 20 reached its end of
6
+ support on 2026-04-30 and node 22 is no longer what this code is built and
7
+ typed against. That is what makes this a major version: an install on an
8
+ older node is refused rather than left to fail somewhere later.
9
+ - **A container image is published for each version**, on ghcr, for amd64 and
10
+ arm64. The readme carries the configuration that runs it.
11
+ - The published package carries its changelog, and the entry point it declares
12
+ for the package root now publishes its types.
13
+
14
+ All notable changes to this project are documented here. The format follows
15
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
16
+ follows [semantic versioning](https://semver.org/spec/v2.0.0.html).
17
+
18
+ ## [1.1.0] - 2026-08-24
19
+
20
+ ### Added
21
+
22
+ - Every error carries the error that led to it as its `cause`. A caller still
23
+ reads what this server could not do; a maintainer reading a stack finds the
24
+ parse or URL failure underneath.
25
+
26
+ ### Changed
27
+
28
+ - The lint configuration matches the one the rest of the collection runs, which
29
+ is sixty-three rules wider. Reading two repositories the same way is worth
30
+ more than either set of rules.
31
+ - Shutdown closes the server without discarding the promise, and leaves the
32
+ process on either path.
33
+
34
+ ## [1.0.0] - 2026-08-24
35
+
36
+ ### Added
37
+
38
+ - `find_tags`: ask rule34.xxx which tag names begin with a piece of text, and
39
+ how many posts each one carries. The route needs no credentials, so a server
40
+ with none configured can still say how a tag is spelled.
41
+ - A search that finds nothing because a required tag does not exist now offers
42
+ the names the site does hold that begin like it.
43
+ - A page beyond the end of a search says how many pages the search fills and
44
+ that the page asked for lies past them, rather than serving an empty list with
45
+ nothing said.
46
+ - A search whose results outrun the page ceiling says so, and names raising
47
+ `limit` as the way to reach the rest.
48
+
49
+ ### Changed
50
+
51
+ - A search row leads with the tags the search asked for, so a post matching on a
52
+ name the site lists late still shows why it is there. Only names the post
53
+ carries are lifted.
54
+ - `created_at` is described as the date rule34.xxx took the post in. The site
55
+ imported much of its older catalogue in bulk, so thousands of posts share one
56
+ day in November 2010, and reading that as a publication date states something
57
+ about the work that the data says about the upload.
58
+ - The `sort` argument describes all four orders it accepts, `updated` and
59
+ `random` included.
60
+
61
+ ### Fixed
62
+
63
+ - A link carrying `id` twice with different values is refused instead of
64
+ answering about the first of the two.
65
+ - A link whose `id` is not a usable number says so, rather than reporting that
66
+ the link carries no id at all.
67
+ - A tag written with spaces alone is refused for being empty, without quoting a
68
+ length it does not exceed.
69
+
70
+ ## [0.1.0] - 2026-08-23
71
+
72
+ First release.
73
+
74
+ ### Added
75
+
76
+ - `search_posts`: search rule34.xxx posts by one tag or by several at once, with
77
+ alternatives and exclusions as their own arguments, a media type, a rating, a
78
+ sort, and pagination. A row shows the first twelve tags of a post and states
79
+ how many it carries in all.
80
+ - `get_post`: read one post named by its id or by a link to its page, with every
81
+ tag typed and counted, the uploader, the comment count and the publication
82
+ date. The post is read from both formats the API publishes, because neither
83
+ carries all of that on its own.
84
+ - Credentials read from `RULE34_USER_ID` and `RULE34_API_KEY`. A server without
85
+ them starts, publishes its tools, and answers each call by naming what to set.
86
+ - An empty search of several tags names the tags the site does not hold.
87
+ - The reading layer published on its own under the `./client` subpath, with the
88
+ same pacing, cache and error taxonomy and no protocol attached.
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 smeet666
3
+ Copyright (c) 2026 Damien Bouissou
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -42,6 +42,36 @@ claude mcp add rule34 --env RULE34_USER_ID=your-id --env RULE34_API_KEY=your-key
42
42
  }
43
43
  ```
44
44
 
45
+ ### With Docker
46
+
47
+ ```json
48
+ {
49
+ "mcpServers": {
50
+ "rule34": {
51
+ "command": "docker",
52
+ "args": [
53
+ "run",
54
+ "-i",
55
+ "--rm",
56
+ "-e",
57
+ "RULE34_USER_ID",
58
+ "-e",
59
+ "RULE34_API_KEY",
60
+ "ghcr.io/smeet666/mcp-rule34:2.0.0"
61
+ ],
62
+ "env": {
63
+ "RULE34_USER_ID": "your-key",
64
+ "RULE34_API_KEY": "your-key"
65
+ }
66
+ }
67
+ }
68
+ }
69
+ ```
70
+
71
+ `-i` keeps stdin open, which is where the protocol travels, and no `-t` is
72
+ passed: a TTY rewrites the stream and breaks it. The container needs outbound
73
+ HTTPS to `rule34.xxx` and `api.rule34.xxx`, and nothing else: no volume, no port, and the keys travel through the environment rather than inside the image.
74
+
45
75
  Put this in your **user** configuration rather than in a project file: a
46
76
  `.mcp.json` at the root of a repository travels with the repository, and the key
47
77
  travels with it.
@@ -262,6 +292,37 @@ claude mcp add rule34 --env RULE34_USER_ID=votre-id --env RULE34_API_KEY=votre-c
262
292
  }
263
293
  ```
264
294
 
295
+ ### Avec Docker
296
+
297
+ ```json
298
+ {
299
+ "mcpServers": {
300
+ "rule34": {
301
+ "command": "docker",
302
+ "args": [
303
+ "run",
304
+ "-i",
305
+ "--rm",
306
+ "-e",
307
+ "RULE34_USER_ID",
308
+ "-e",
309
+ "RULE34_API_KEY",
310
+ "ghcr.io/smeet666/mcp-rule34:2.0.0"
311
+ ],
312
+ "env": {
313
+ "RULE34_USER_ID": "your-key",
314
+ "RULE34_API_KEY": "your-key"
315
+ }
316
+ }
317
+ }
318
+ }
319
+ ```
320
+
321
+ `-i` garde l'entrée standard ouverte, qui est le canal du protocole, et aucun
322
+ `-t` n'est passé : un terminal réécrit le flux et le casse. Le conteneur a besoin
323
+ d'un accès HTTPS sortant vers `rule34.xxx` et `api.rule34.xxx`, et de rien d'autre :
324
+ aucun volume, aucun port, et les clés voyagent par l'environnement plutôt que dans l'image.
325
+
265
326
  À placer dans votre configuration **utilisateur** plutôt que dans un fichier de
266
327
  projet : un `.mcp.json` à la racine d'un dépôt part avec le dépôt, et la clé
267
328
  part avec lui.
package/dist/index.js CHANGED
@@ -1,10 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
+ import process2 from "process";
4
5
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
6
 
7
+ // src/config.ts
8
+ import process from "process";
9
+
6
10
  // src/version.ts
7
- var PKG_VERSION = "1.1.0";
11
+ var PKG_VERSION = "2.0.0";
8
12
  var REPO_URL = "https://github.com/smeet666/mcp-rule34";
9
13
 
10
14
  // src/config.ts
@@ -590,6 +594,8 @@ function asTransportError(error, safeUrl) {
590
594
  }
591
595
 
592
596
  // src/rule34/parsers/postDetail.ts
597
+ var AUTHENTICATION = /authenticat/i;
598
+ var WHITESPACE = /\s+/;
593
599
  var TYPES = {
594
600
  tag: "general",
595
601
  artist: "artist",
@@ -609,7 +615,7 @@ function parsePostDetail(body, url, id) {
609
615
  throw parseFailure(safeUrl, "the response is not JSON", error);
610
616
  }
611
617
  if (typeof parsed === "string") {
612
- if (/authenticat/i.test(parsed)) {
618
+ if (AUTHENTICATION.test(parsed)) {
613
619
  throw missingCredentials(safeUrl);
614
620
  }
615
621
  throw upstreamFailure(safeUrl, parsed);
@@ -628,7 +634,7 @@ function parsePostDetail(body, url, id) {
628
634
  }
629
635
  function readPost(raw, askedFor) {
630
636
  const id = readInteger(raw.id) ?? askedFor;
631
- const tags = readString(raw.tags).split(/\s+/).filter((tag) => tag !== "");
637
+ const tags = readString(raw.tags).split(WHITESPACE).filter((tag) => tag !== "");
632
638
  return {
633
639
  id,
634
640
  postUrl: buildPostPageUrl(id),
@@ -728,6 +734,7 @@ function parseTagSuggestions(body, url) {
728
734
 
729
735
  // src/rule34/parsers/document.ts
730
736
  import { XMLParser } from "fast-xml-parser";
737
+ var AUTHENTICATION2 = /authenticat/i;
731
738
  var parser = new XMLParser({
732
739
  ignoreAttributes: false,
733
740
  attributeNamePrefix: "@_",
@@ -753,7 +760,7 @@ function readDocument(xml, safeUrl) {
753
760
  const failure = parsed.error;
754
761
  if (failure !== void 0) {
755
762
  const message = typeof failure === "string" ? failure : String(failure);
756
- if (/authenticat/i.test(message)) {
763
+ if (AUTHENTICATION2.test(message)) {
757
764
  throw missingCredentials(safeUrl);
758
765
  }
759
766
  throw upstreamFailure(safeUrl, message);
@@ -780,6 +787,7 @@ function readInteger2(value) {
780
787
  }
781
788
 
782
789
  // src/rule34/parsers/posts.ts
790
+ var WHITESPACE2 = /\s+/;
783
791
  var RATINGS = {
784
792
  e: "explicit",
785
793
  q: "questionable",
@@ -819,7 +827,7 @@ function readPost2(attributes) {
819
827
  // An unknown letter is carried as it came: naming it would state a rating
820
828
  // the site never wrote.
821
829
  rating: RATINGS[rating] ?? rating,
822
- tags: (attributes["@_tags"] ?? "").split(/\s+/).filter((tag) => tag !== ""),
830
+ tags: (attributes["@_tags"] ?? "").split(WHITESPACE2).filter((tag) => tag !== ""),
823
831
  source: readText2(attributes["@_source"]),
824
832
  // The attribute is present and empty on a post with no parent, and 0 is a
825
833
  // post id that belongs to somebody else.
@@ -991,7 +999,7 @@ var Rule34Client = class {
991
999
  config: this.deps.config,
992
1000
  limiter: this.limiter,
993
1001
  logger: this.deps.logger,
994
- fetchImpl: this.deps.fetchImpl
1002
+ ...this.deps.fetchImpl ? { fetchImpl: this.deps.fetchImpl } : {}
995
1003
  });
996
1004
  this.cache.set(key, body);
997
1005
  return { body, cached: false };
@@ -1392,10 +1400,10 @@ var searchPostsOutputShape = {
1392
1400
  function toSearch(args) {
1393
1401
  return {
1394
1402
  tags: args.tags,
1395
- anyOf: args.any_of,
1396
- exclude: args.exclude,
1403
+ ...args.any_of ? { anyOf: args.any_of } : {},
1404
+ ...args.exclude ? { exclude: args.exclude } : {},
1397
1405
  mediaType: args.media_type,
1398
- rating: args.rating,
1406
+ ...args.rating ? { rating: args.rating } : {},
1399
1407
  sort: args.sort,
1400
1408
  limit: args.limit,
1401
1409
  page: args.page
@@ -1589,16 +1597,16 @@ async function main() {
1589
1597
  const transport = new StdioServerTransport();
1590
1598
  await server.connect(transport);
1591
1599
  const shutdown = () => {
1592
- server.close().catch(() => void 0).finally(() => process.exit(0));
1600
+ server.close().catch(() => void 0).finally(() => process2.exit(0));
1593
1601
  };
1594
- process.on("SIGINT", shutdown);
1595
- process.on("SIGTERM", shutdown);
1602
+ process2.on("SIGINT", shutdown);
1603
+ process2.on("SIGTERM", shutdown);
1596
1604
  }
1597
1605
  main().catch((error) => {
1598
- process.stderr.write(
1606
+ process2.stderr.write(
1599
1607
  `[mcp-rule34] fatal: ${error instanceof Error ? error.stack ?? error.message : String(error)}
1600
1608
  `
1601
1609
  );
1602
- process.exit(1);
1610
+ process2.exit(1);
1603
1611
  });
1604
1612
  //# sourceMappingURL=index.js.map