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 +88 -0
- package/LICENSE +1 -1
- package/README.md +61 -0
- package/dist/index.js +22 -14
- package/dist/index.js.map +1 -1
- package/dist/rule34/client.js +9 -5
- package/dist/rule34/client.js.map +1 -1
- package/package.json +11 -9
- package/server.json +2 -2
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
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 = "
|
|
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 (
|
|
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(
|
|
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 (
|
|
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(
|
|
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(() =>
|
|
1600
|
+
server.close().catch(() => void 0).finally(() => process2.exit(0));
|
|
1593
1601
|
};
|
|
1594
|
-
|
|
1595
|
-
|
|
1602
|
+
process2.on("SIGINT", shutdown);
|
|
1603
|
+
process2.on("SIGTERM", shutdown);
|
|
1596
1604
|
}
|
|
1597
1605
|
main().catch((error) => {
|
|
1598
|
-
|
|
1606
|
+
process2.stderr.write(
|
|
1599
1607
|
`[mcp-rule34] fatal: ${error instanceof Error ? error.stack ?? error.message : String(error)}
|
|
1600
1608
|
`
|
|
1601
1609
|
);
|
|
1602
|
-
|
|
1610
|
+
process2.exit(1);
|
|
1603
1611
|
});
|
|
1604
1612
|
//# sourceMappingURL=index.js.map
|