mcp-rule34 2.0.1 → 2.0.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/CHANGELOG.md +10 -0
- package/README.md +4 -4
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +9 -8
- package/server.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.0.2
|
|
4
|
+
|
|
5
|
+
- **The lowest version of `fast-xml-parser` this package accepts carries no
|
|
6
|
+
known advisory.** The published archive holds no lockfile, so a consumer
|
|
7
|
+
resolves the dependencies from the declared ranges, and the floor of a range
|
|
8
|
+
states which versions the package accepts to run.
|
|
9
|
+
- **A gate audits those floors.** `npm run audit:floors` resolves every range to
|
|
10
|
+
its lowest published match, builds a tree from those and audits it, which is
|
|
11
|
+
what an audit of the installed tree cannot report.
|
|
12
|
+
|
|
3
13
|
## 2.0.1
|
|
4
14
|
|
|
5
15
|
- **Every tool is documented, with its arguments and what its answer carries.**
|
package/README.md
CHANGED
|
@@ -63,7 +63,7 @@ under [Configuration](#configuration) is optional.
|
|
|
63
63
|
"RULE34_USER_ID",
|
|
64
64
|
"-e",
|
|
65
65
|
"RULE34_API_KEY",
|
|
66
|
-
"ghcr.io/smeet666/mcp-rule34:2.0.
|
|
66
|
+
"ghcr.io/smeet666/mcp-rule34:2.0.2"
|
|
67
67
|
]
|
|
68
68
|
}
|
|
69
69
|
}
|
|
@@ -77,7 +77,7 @@ no volume, no port.
|
|
|
77
77
|
|
|
78
78
|
### Bundle, without npm
|
|
79
79
|
|
|
80
|
-
Download `mcp-rule34-2.0.
|
|
80
|
+
Download `mcp-rule34-2.0.2.mcpb` from
|
|
81
81
|
[the latest release](https://github.com/smeet666/mcp-rule34/releases/latest) and
|
|
82
82
|
open it. A client that supports MCP bundles installs it on its own, with no npm
|
|
83
83
|
to run. The credentials are still set in the client's configuration.
|
|
@@ -329,7 +329,7 @@ tout le reste, sous [Configuration](#configuration-1), est facultatif.
|
|
|
329
329
|
"RULE34_USER_ID",
|
|
330
330
|
"-e",
|
|
331
331
|
"RULE34_API_KEY",
|
|
332
|
-
"ghcr.io/smeet666/mcp-rule34:2.0.
|
|
332
|
+
"ghcr.io/smeet666/mcp-rule34:2.0.2"
|
|
333
333
|
]
|
|
334
334
|
}
|
|
335
335
|
}
|
|
@@ -343,7 +343,7 @@ votre environnement : aucun volume, aucun port.
|
|
|
343
343
|
|
|
344
344
|
### Bundle, sans npm
|
|
345
345
|
|
|
346
|
-
Téléchargez `mcp-rule34-2.0.
|
|
346
|
+
Téléchargez `mcp-rule34-2.0.2.mcpb` depuis
|
|
347
347
|
[la dernière publication](https://github.com/smeet666/mcp-rule34/releases/latest)
|
|
348
348
|
et ouvrez-le. Un client qui gère les bundles MCP l'installe seul, sans npm à
|
|
349
349
|
lancer. Les identifiants se posent toujours dans la configuration du client.
|
package/dist/index.js
CHANGED
|
@@ -8,7 +8,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
8
8
|
import process from "process";
|
|
9
9
|
|
|
10
10
|
// src/version.ts
|
|
11
|
-
var PKG_VERSION = "2.0.
|
|
11
|
+
var PKG_VERSION = "2.0.2";
|
|
12
12
|
var REPO_URL = "https://github.com/smeet666/mcp-rule34";
|
|
13
13
|
|
|
14
14
|
// src/config.ts
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/config.ts","../src/version.ts","../src/server.ts","../src/errors.ts","../src/rule34/cache.ts","../src/rule34/rateLimiter.ts","../src/rule34/urls.ts","../src/rule34/http.ts","../src/rule34/parsers/postDetail.ts","../src/rule34/parsers/suggestions.ts","../src/rule34/parsers/document.ts","../src/rule34/parsers/posts.ts","../src/rule34/parsers/tags.ts","../src/rule34/client.ts","../src/tools/findTags.ts","../src/tools/arguments.ts","../src/tools/shared.ts","../src/tools/getPost.ts","../src/tools/searchPosts.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * Entry point: an MCP server for rule34.xxx over stdio.\n */\n\nimport process from \"node:process\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { createLogger, loadConfig } from \"./config.js\";\nimport { createServer } from \"./server.js\";\n\nasync function main(): Promise<void> {\n const config = loadConfig();\n const logger = createLogger(config.logLevel);\n const server = createServer({ config, logger });\n\n const transport = new StdioServerTransport();\n await server.connect(transport);\n\n const shutdown = () => {\n // The close is attempted, and a close that fails changes nothing: the\n // process leaves on either path.\n server\n .close()\n .catch(() => undefined)\n .finally(() => process.exit(0));\n };\n process.on(\"SIGINT\", shutdown);\n process.on(\"SIGTERM\", shutdown);\n}\n\nmain().catch((error: unknown) => {\n process.stderr.write(\n `[mcp-rule34] fatal: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}\\n`,\n );\n process.exit(1);\n});\n","/**\n * Runtime configuration, read from environment variables.\n *\n * A bad value never stops the process: a server that dies at startup because of\n * a typo in a client configuration file is very hard to diagnose from the host\n * application, so an invalid value is clamped and reported on stderr. Missing\n * credentials are handled the same way — the server starts, publishes its\n * tools, and each call answers with what to set and where to get it.\n */\n\nimport process from \"node:process\";\nimport type { Credentials } from \"./rule34/urls.js\";\nimport { PKG_VERSION, REPO_URL } from \"./version.js\";\n\nexport type LogLevel = \"silent\" | \"error\" | \"info\" | \"debug\";\n\nexport interface Config {\n userAgent: string;\n credentials: Credentials | null;\n minIntervalMs: number;\n timeoutMs: number;\n maxRetries: number;\n cacheTtlMs: number;\n cacheMaxEntries: number;\n logLevel: LogLevel;\n}\n\n/** Identifies the client honestly, with an address that reaches a person. */\nexport const DEFAULT_USER_AGENT = `mcp-rule34/${PKG_VERSION} (+${REPO_URL})`;\n\nexport const DEFAULTS = {\n minIntervalMs: 1000,\n timeoutMs: 20_000,\n maxRetries: 3,\n cacheTtlMs: 5 * 60 * 1000,\n cacheMaxEntries: 300,\n logLevel: \"error\" as LogLevel,\n};\n\n/**\n * Floor on the request interval, enforced regardless of configuration.\n *\n * rule34.xxx limits its rate without publishing what the rate is: the account\n * page states a limit whose numbers are missing from the page itself, and a\n * handful of requests in a row is enough to draw a 429. Leaving the pacing\n * configurable down to zero would make the politeness of every installation\n * depend on whoever edited a JSON file.\n */\nexport const MIN_ALLOWED_INTERVAL_MS = 1000;\n\nconst LOG_LEVELS: LogLevel[] = [\"silent\", \"error\", \"info\", \"debug\"];\n\ninterface NumericRange {\n min: number;\n max: number;\n fallback: number;\n}\n\nfunction warn(message: string): void {\n process.stderr.write(`[mcp-rule34] ${message}\\n`);\n}\n\nfunction readNumber(name: string, env: NodeJS.ProcessEnv, range: NumericRange): number {\n const raw = env[name];\n if (raw === undefined || raw.trim() === \"\") {\n return range.fallback;\n }\n\n const parsed = Number(raw);\n if (!Number.isFinite(parsed)) {\n warn(`${name}=\"${raw}\" is not a number, using ${range.fallback}`);\n return range.fallback;\n }\n const clamped = Math.min(range.max, Math.max(range.min, Math.round(parsed)));\n if (clamped !== Math.round(parsed)) {\n warn(`${name}=${raw} is out of range, clamped to ${clamped}`);\n }\n return clamped;\n}\n\n/**\n * Read the request interval, refusing anything below the floor.\n *\n * A value under the floor falls back to the default rather than to the floor\n * itself: someone who set 0 was not asking for the floor, they were asking for\n * no pacing at all, and the safe reading of that request is to ignore it.\n */\nfunction readInterval(env: NodeJS.ProcessEnv): number {\n const raw = env.RULE34_MIN_INTERVAL_MS;\n if (raw === undefined || raw.trim() === \"\") {\n return DEFAULTS.minIntervalMs;\n }\n\n const parsed = Number(raw);\n if (!Number.isFinite(parsed)) {\n warn(`RULE34_MIN_INTERVAL_MS=\"${raw}\" is not a number, using ${DEFAULTS.minIntervalMs}ms`);\n return DEFAULTS.minIntervalMs;\n }\n\n const rounded = Math.round(parsed);\n if (rounded < MIN_ALLOWED_INTERVAL_MS) {\n warn(\n `RULE34_MIN_INTERVAL_MS=${raw} is below the ${MIN_ALLOWED_INTERVAL_MS}ms floor and was ignored; ` +\n `using ${DEFAULTS.minIntervalMs}ms. This floor keeps the client from hammering rule34.xxx.`,\n );\n return DEFAULTS.minIntervalMs;\n }\n\n return Math.min(60_000, rounded);\n}\n\n/**\n * The pair the API route requires, or nothing.\n *\n * One half is as useless as none: the site answers a partial pair with its\n * authentication error, which a caller reads as a fault on the site's side.\n */\nexport function readCredentials(env: NodeJS.ProcessEnv): Credentials | null {\n const userId = env.RULE34_USER_ID?.trim() ?? \"\";\n const apiKey = env.RULE34_API_KEY?.trim() ?? \"\";\n if (userId === \"\" || apiKey === \"\") {\n return null;\n }\n return { userId, apiKey };\n}\n\nexport function loadConfig(env: NodeJS.ProcessEnv = process.env): Config {\n const rawUserAgent = env.RULE34_USER_AGENT?.trim();\n const rawLogLevel = env.RULE34_LOG_LEVEL?.trim().toLowerCase();\n\n let logLevel = DEFAULTS.logLevel;\n if (rawLogLevel) {\n if (LOG_LEVELS.includes(rawLogLevel as LogLevel)) {\n logLevel = rawLogLevel as LogLevel;\n } else {\n warn(`RULE34_LOG_LEVEL=\"${rawLogLevel}\" is unknown, using \"${DEFAULTS.logLevel}\"`);\n }\n }\n\n return {\n userAgent: rawUserAgent || DEFAULT_USER_AGENT,\n credentials: readCredentials(env),\n minIntervalMs: readInterval(env),\n timeoutMs: readNumber(\"RULE34_TIMEOUT_MS\", env, {\n min: 1000,\n max: 120_000,\n fallback: DEFAULTS.timeoutMs,\n }),\n maxRetries: readNumber(\"RULE34_MAX_RETRIES\", env, {\n min: 0,\n max: 10,\n fallback: DEFAULTS.maxRetries,\n }),\n cacheTtlMs: readNumber(\"RULE34_CACHE_TTL_MS\", env, {\n min: 0,\n max: 24 * 60 * 60 * 1000,\n fallback: DEFAULTS.cacheTtlMs,\n }),\n cacheMaxEntries: readNumber(\"RULE34_CACHE_MAX_ENTRIES\", env, {\n min: 0,\n max: 10_000,\n fallback: DEFAULTS.cacheMaxEntries,\n }),\n logLevel,\n };\n}\n\nconst LEVEL_RANK: Record<LogLevel, number> = { silent: 0, error: 1, info: 2, debug: 3 };\n\n/**\n * Logs go to stderr without exception. On a stdio transport, stdout carries the\n * protocol and any stray write there corrupts the session.\n */\nexport function createLogger(level: LogLevel) {\n const emit = (at: LogLevel, message: string) => {\n if (LEVEL_RANK[level] >= LEVEL_RANK[at]) {\n process.stderr.write(`[mcp-rule34] ${message}\\n`);\n }\n };\n return {\n error: (message: string) => emit(\"error\", message),\n info: (message: string) => emit(\"info\", message),\n debug: (message: string) => emit(\"debug\", message),\n };\n}\n\nexport type Logger = ReturnType<typeof createLogger>;\n","/** Version and repository, quoted in the User-Agent this client sends. */\n\nexport const PKG_VERSION = \"2.0.1\";\nexport const REPO_URL = \"https://github.com/smeet666/mcp-rule34\";\n","/**\n * MCP server wiring.\n *\n * One client, one rate limiter and one cache serve every tool: per-tool\n * instances would each open their own request stream and defeat the pacing that\n * keeps this server welcome on a site which limits its rate.\n */\n\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { z } from \"zod\";\nimport type { Config, Logger } from \"./config.js\";\nimport { createLogger, loadConfig } from \"./config.js\";\nimport { Rule34Client } from \"./rule34/client.js\";\nimport type { FindTagsArgs } from \"./tools/findTags.js\";\nimport {\n findTagsDescription,\n findTagsInput,\n findTagsOutputShape,\n runFindTags,\n} from \"./tools/findTags.js\";\nimport type { GetPostArgs } from \"./tools/getPost.js\";\nimport {\n getPostDescription,\n getPostInput,\n getPostOutputShape,\n runGetPost,\n} from \"./tools/getPost.js\";\nimport type { SearchPostsArgs } from \"./tools/searchPosts.js\";\nimport {\n runSearchPosts,\n searchPostsDescription,\n searchPostsInput,\n searchPostsOutputShape,\n} from \"./tools/searchPosts.js\";\nimport { PKG_VERSION } from \"./version.js\";\n\nexport interface CreateServerOptions {\n config?: Config;\n logger?: Logger;\n fetchImpl?: typeof fetch;\n}\n\nconst READ_ONLY = {\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: true,\n} as const;\n\nexport function createServer(options: CreateServerOptions = {}): McpServer {\n const config = options.config ?? loadConfig();\n const logger = options.logger ?? createLogger(config.logLevel);\n const client = new Rule34Client({\n config,\n logger,\n ...(options.fetchImpl ? { fetchImpl: options.fetchImpl } : {}),\n });\n\n const server = new McpServer(\n { name: \"mcp-rule34\", version: PKG_VERSION },\n {\n instructions:\n \"Tools for searching rule34.xxx by tag, through the site's own API. This server reads and never \" +\n \"contributes anything back. It needs credentials: set RULE34_USER_ID and RULE34_API_KEY, both shown \" +\n \"under 'API Access Credentials' at https://rule34.xxx/index.php?page=account&s=options once signed in, \" +\n \"and note that the site issues one key per person. Tags are single tokens: a name said as words, such \" +\n \"as 'asuka langley sohryu', is one tag, and the tools join it for you. The site holds two ratings, \" +\n \"'questionable' and 'explicit'; asking for anything else is refused rather than answered with nothing. \" +\n \"Typical flow: search_posts to find posts by tag, then get_post with an id for one post's whole tag \" +\n \"list, since a search row shows only its first few tags. When a name might be spelled otherwise, or \" +\n \"when a search found nothing because a tag does not exist, find_tags says how the site spells it. \" +\n \"A 'rate_limited' error means the site is refusing this client for now, not that a search found \" +\n \"nothing. When you show a post to a user, link its rule34.xxx page.\",\n },\n );\n\n // Registered in the order a caller meets them: a search names the posts, and\n // a post is then read whole.\n server.registerTool(\n \"search_posts\",\n {\n title: \"Search posts by tag\",\n description: searchPostsDescription,\n inputSchema: searchPostsInput,\n outputSchema: z.object(searchPostsOutputShape),\n annotations: READ_ONLY,\n },\n async (args) => runSearchPosts(client, args as SearchPostsArgs),\n );\n\n server.registerTool(\n \"get_post\",\n {\n title: \"Read one post\",\n description: getPostDescription,\n inputSchema: getPostInput,\n outputSchema: z.object(getPostOutputShape),\n annotations: READ_ONLY,\n },\n async (args) => runGetPost(client, args as GetPostArgs),\n );\n\n server.registerTool(\n \"find_tags\",\n {\n title: \"Find how a tag is spelled\",\n description: findTagsDescription,\n inputSchema: findTagsInput,\n outputSchema: z.object(findTagsOutputShape),\n annotations: READ_ONLY,\n },\n async (args) => runFindTags(client, args as FindTagsArgs),\n );\n\n logger.info(\n `ready: user-agent=\"${config.userAgent}\", min interval ${config.minIntervalMs}ms, ` +\n `cache ${config.cacheTtlMs}ms, credentials ${config.credentials ? \"set\" : \"missing\"}`,\n );\n\n return server;\n}\n","/**\n * Error taxonomy surfaced to the calling model.\n *\n * A caller branches on the code that opens the message, so the code has to say\n * where the fault is. The distinction that matters most on this route is\n * between a document this server could not read and an account it was not given\n * the credentials for: the site answers both with HTTP 200, and only one of\n * them is fixed by editing a configuration file.\n */\n\nexport type ErrorCode =\n | \"not_found\"\n | \"invalid_input\"\n | \"rate_limited\"\n | \"parse_failure\"\n | \"network_error\"\n | \"timeout\";\n\nexport interface ErrorDetails {\n url?: string;\n status?: number;\n retryAfterMs?: number;\n hint?: string;\n}\n\nexport class Rule34Error extends Error {\n constructor(\n readonly code: ErrorCode,\n text: string,\n readonly details: ErrorDetails = {},\n options?: ErrorOptions,\n ) {\n // The message a caller reads says what this server could not do. The error\n // that led there is kept as the cause, where a maintainer reading a stack\n // finds it and a caller never has to.\n super(`[${code}] ${text}`, options);\n this.name = \"Rule34Error\";\n }\n}\n\nconst ISSUES_URL = \"https://github.com/smeet666/mcp-rule34/issues\";\n\nexport function invalidInput(message: string, hint?: string, cause?: unknown): Rule34Error {\n return new Rule34Error(\"invalid_input\", message, hint ? { hint } : {}, { cause });\n}\n\n/**\n * The credentials are missing or refused.\n *\n * This is `invalid_input` rather than a failure of the site: the request was\n * well formed and the site answered it, and what is missing lives in the\n * caller's own configuration.\n */\nexport function missingCredentials(url: string): Rule34Error {\n return new Rule34Error(\n \"invalid_input\",\n \"rule34.xxx answered that this request carries no valid API credentials.\",\n {\n url,\n hint:\n \"Set RULE34_USER_ID and RULE34_API_KEY in the env block of this server's entry in your MCP client \" +\n \"configuration. Both are shown under 'API Access Credentials' at \" +\n \"https://rule34.xxx/index.php?page=account&s=options once you are signed in. The site issues one key \" +\n \"per person, so use your own rather than sharing one.\",\n },\n );\n}\n\nexport function notFound(url: string): Rule34Error {\n return new Rule34Error(\"not_found\", \"rule34.xxx has nothing at this address.\", {\n url,\n status: 404,\n hint: \"Check the post id. Ids come from a search; they are not stable to guess by hand.\",\n });\n}\n\n/**\n * The site is being asked too often.\n *\n * This never becomes an empty result. rule34.xxx limits its rate without\n * publishing one, and a refusal rendered as zero posts reads as \"the site holds\n * nothing on this tag\", which is the one thing a caller must not be told here.\n */\nexport function rateLimited(url: string, retryAfterMs: number): Rule34Error {\n return new Rule34Error(\n \"rate_limited\",\n \"rule34.xxx is refusing requests from this client for now (HTTP 429). \" +\n \"This does NOT mean the search found nothing.\",\n {\n url,\n status: 429,\n retryAfterMs,\n hint:\n \"Wait before asking again. If it keeps happening, raise RULE34_MIN_INTERVAL_MS in your MCP client \" +\n \"configuration to slow this server down.\",\n },\n );\n}\n\nexport function parseFailure(url: string, what: string, cause?: unknown): Rule34Error {\n return new Rule34Error(\n \"parse_failure\",\n `The request was answered, but not with the document this route publishes (${what}).`,\n { url, hint: `Please report this, with the query you used, at ${ISSUES_URL}` },\n { cause },\n );\n}\n\n/** The site answered with a failure of its own, in the shape its API documents. */\nexport function upstreamFailure(url: string, message: string): Rule34Error {\n return new Rule34Error(\"network_error\", `rule34.xxx reported a failure: ${message}`, {\n url,\n hint: \"This is the site's own message. Waiting and asking again is usually the only remedy.\",\n });\n}\n","/**\n * In-memory TTL and LRU cache.\n *\n * Nothing is written to disk. This exists so that the same question asked twice\n * inside one conversation does not reach rule34.xxx twice, which matters more\n * here than elsewhere: the site limits its rate without publishing one, and\n * every request spent on a repeat is a request unavailable to a new question.\n *\n * A Map iterates in insertion order, so re-inserting on every hit is enough to\n * make the first key the least recently used one.\n */\n\ninterface Entry<V> {\n value: V;\n expiresAt: number;\n}\n\nexport class TtlLruCache<V> {\n private readonly store = new Map<string, Entry<V>>();\n\n constructor(\n private readonly maxEntries: number,\n private readonly ttlMs: number,\n ) {}\n\n get(key: string): V | undefined {\n const entry = this.store.get(key);\n if (!entry) {\n return undefined;\n }\n if (Date.now() >= entry.expiresAt) {\n this.store.delete(key);\n return undefined;\n }\n this.store.delete(key);\n this.store.set(key, entry);\n return entry.value;\n }\n\n set(key: string, value: V): void {\n if (this.maxEntries <= 0 || this.ttlMs <= 0) {\n return;\n }\n this.store.delete(key);\n this.store.set(key, { value, expiresAt: Date.now() + this.ttlMs });\n while (this.store.size > this.maxEntries) {\n const oldest = this.store.keys().next();\n if (oldest.done) {\n break;\n }\n this.store.delete(oldest.value);\n }\n }\n\n /** The keys currently held, in least recently used order. */\n keys(): string[] {\n return [...this.store.keys()];\n }\n\n get size(): number {\n return this.store.size;\n }\n\n clear(): void {\n this.store.clear();\n }\n}\n","/**\n * Serial request queue with an adaptive minimum interval.\n *\n * rule34.xxx limits its rate without publishing what the rate is, and answers a\n * burst with HTTP 429. Requests therefore run one at a time with a floor on the\n * gap between starts. When the site does push back, the interval doubles and\n * then decays as requests succeed, which recovers faster than a fixed delay and\n * behaves better than asking at a constant rate the site never agreed to.\n */\n\nexport interface RateLimiterOptions {\n minIntervalMs: number;\n maxIntervalMs?: number;\n}\n\nconst DEFAULT_MAX_INTERVAL_MS = 30_000;\n\nexport class RateLimiter {\n private readonly baseIntervalMs: number;\n private readonly maxIntervalMs: number;\n private intervalMs: number;\n /** Tail of the queue: each task chains onto the previous one. */\n private tail: Promise<unknown> = Promise.resolve();\n private lastStart = 0;\n\n constructor(options: RateLimiterOptions) {\n this.baseIntervalMs = Math.max(0, options.minIntervalMs);\n this.maxIntervalMs = Math.max(\n this.baseIntervalMs,\n options.maxIntervalMs ?? DEFAULT_MAX_INTERVAL_MS,\n );\n this.intervalMs = this.baseIntervalMs;\n }\n\n get currentIntervalMs(): number {\n return this.intervalMs;\n }\n\n /** Queue a task. Tasks run in call order, one at a time. */\n schedule<T>(task: () => Promise<T>): Promise<T> {\n const run = this.tail.then(async () => task());\n // The queue must keep draining even when a task rejects.\n this.tail = run.then(\n () => undefined,\n () => undefined,\n );\n return run;\n }\n\n /**\n * Wait for this request's slot, then claim it.\n *\n * Called once per upstream request rather than once per task, because a task\n * runs a whole retry chain: stamping only its start would let the next task\n * follow the chain's last request with no gap at all.\n */\n async beforeRequest(): Promise<void> {\n await this.waitForSlot();\n this.lastStart = Date.now();\n }\n\n /** Called after the site answers that it is being asked too often. */\n penalize(): void {\n const next = this.intervalMs === 0 ? 500 : this.intervalMs * 2;\n this.intervalMs = Math.min(this.maxIntervalMs, next);\n }\n\n /** Called after a success, so one push-back does not slow things down forever. */\n relax(): void {\n this.intervalMs = Math.max(this.baseIntervalMs, Math.floor(this.intervalMs * 0.75));\n }\n\n private async waitForSlot(): Promise<void> {\n if (this.intervalMs === 0 || this.lastStart === 0) {\n return;\n }\n const elapsed = Date.now() - this.lastStart;\n // Clamped to the interval: a clock stepped backwards would otherwise make\n // this wait for the size of the step, and the queue is serial, so every\n // pending request would wait behind it.\n const remaining = Math.min(this.intervalMs, this.intervalMs - elapsed);\n if (remaining > 0) {\n await sleep(remaining);\n }\n }\n}\n\nexport function sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n","/**\n * URL construction for rule34.xxx, and the one place a tag becomes a query.\n *\n * Everything a caller supplies passes through here, which is why the refusals\n * live here too: a tag is one token, and a token that carries a metatag or a\n * negation would set a filter the answer then reports as something else.\n */\n\nimport { invalidInput } from \"../errors.js\";\nimport type { MediaType, Rating, SortOrder } from \"../types.js\";\n\nexport const API_URL = \"https://api.rule34.xxx/index.php\";\nexport const SUGGEST_URL = \"https://api.rule34.xxx/autocomplete.php\";\nexport const SITE_URL = \"https://rule34.xxx/index.php\";\n\n/** The site refuses more than this in one request, and caps silently below it. */\nexport const MAX_LIMIT = 1000;\n\n/**\n * How each media type is expressed in the site's own tags.\n *\n * The site holds the distinction as tags, so it does the filtering and the\n * total it returns counts what was asked for. `animated` covers the GIFs, which\n * carry that tag without carrying `video`.\n */\nconst MEDIA_TOKENS: Record<MediaType, string[]> = {\n image: [\"-video\", \"-animated\"],\n animated: [\"animated\", \"-video\"],\n video: [\"video\"],\n any: [],\n};\n\nconst SORT_TOKENS: Record<SortOrder, string> = {\n score: \"sort:score:desc\",\n id: \"sort:id:desc\",\n updated: \"sort:updated:desc\",\n random: \"sort:random\",\n};\n\nexport interface Credentials {\n apiKey: string;\n userId: string;\n}\n\nexport interface PostSearch {\n /** Tags a post must all carry. */\n tags: string[];\n /** Tags a post must carry at least one of. */\n anyOf?: string[];\n /** Tags a post must not carry. */\n exclude?: string[];\n mediaType?: MediaType;\n rating?: Rating;\n sort?: SortOrder;\n limit: number;\n /** Counted from 1, the way a caller counts. */\n page: number;\n}\n\n/**\n * How many tags one list may hold.\n *\n * The site accepts more, and a search of a dozen tags answers normally. The\n * bound is here because every tag is one more term to diagnose when a search\n * comes back empty, and a request whose address runs to kilobytes is one no\n * error message can usefully quote.\n */\nexport const MAX_TAGS = 10;\n\n/**\n * Write a name the way rule34.xxx spells a tag.\n *\n * The site stores a tag as one token, joining with underscores what a person\n * says as words, so \"asuka langley sohryu\" and \"asuka_langley_sohryu\" are the\n * same request. Punctuation inside the name is part of it: the series tag is\n * `ranma_1/2`, slash included.\n */\nexport function normalizeTag(raw: string): string {\n const trimmed = raw.trim();\n if (trimmed === \"\") {\n throw invalidInput(\n \"A tag is required.\",\n \"Pass the tag to search for, such as 'black hair' or 'asuka langley sohryu'.\",\n );\n }\n\n if (trimmed.startsWith(\"-\")) {\n throw invalidInput(\n `\"${trimmed}\" starts with '-', which means \"without this tag\" on rule34.xxx.`,\n \"This tool searches for a tag. Pass the tag itself, without the leading dash.\",\n );\n }\n\n if (trimmed.includes(\":\")) {\n throw invalidInput(\n `\"${trimmed}\" carries a ':', which rule34.xxx reads as a filter rather than as a tag.`,\n \"Filters are separate arguments here: use 'rating', 'sort' and 'media_type'.\",\n );\n }\n\n return trimmed.replace(/\\s+/g, \"_\").toLowerCase();\n}\n\n/** Normalize a list of tags, refusing a list longer than one search carries. */\nfunction normalizeList(raw: string[] | undefined, argument: string): string[] {\n const tags = (raw ?? []).map(normalizeTag);\n if (tags.length > MAX_TAGS) {\n throw invalidInput(\n `'${argument}' holds ${tags.length} tags, and this tool searches at most ${MAX_TAGS} at a time.`,\n \"Search on the tags that narrow the most, then look through the results.\",\n );\n }\n return tags;\n}\n\nexport interface NormalizedTags {\n required: string[];\n alternatives: string[];\n excluded: string[];\n}\n\n/**\n * The three lists as rule34.xxx spells them.\n *\n * An answer states the tags it searched for, and taking them back out of the\n * query string would state the site's grammar — its braces and its tilde — as\n * though they were tags.\n */\nexport function normalizeSearch(search: PostSearch): NormalizedTags {\n const required = normalizeList(search.tags, \"tags\");\n if (required.length === 0) {\n throw invalidInput(\n \"At least one tag is required.\",\n \"Pass the tags to search for, such as ['asuka langley sohryu', 'black hair'].\",\n );\n }\n\n const alternatives = normalizeList(search.anyOf, \"any_of\");\n const excluded = normalizeList(search.exclude, \"exclude\");\n\n // A tag both required and excluded matches nothing whatever the site holds,\n // and answering that with an empty result reads as an absence on the site's\n // side rather than a contradiction in the question.\n const contradiction = required.find((tag) => excluded.includes(tag));\n if (contradiction) {\n throw invalidInput(\n `'${contradiction}' is both required and excluded, so no post can match.`,\n \"Remove it from one of the two lists.\",\n );\n }\n\n return { required, alternatives, excluded };\n}\n\n/** The value of the site's `tags` parameter: the tags, then the filters. */\nexport function buildSearchQuery(search: PostSearch): string {\n const { required, alternatives, excluded } = normalizeSearch(search);\n\n const tokens = [...required];\n if (alternatives.length === 1) {\n // A group of one is the same request as the tag itself, and the braces\n // would suggest a choice the search does not make.\n tokens.push(...alternatives);\n } else if (alternatives.length > 1) {\n // The braces and the spaces inside them are the site's own grammar for a\n // choice; without them the tilde is read as part of a tag name.\n tokens.push(`( ${alternatives.join(\" ~ \")} )`);\n }\n\n tokens.push(...excluded.map((tag) => `-${tag}`), ...MEDIA_TOKENS[search.mediaType ?? \"any\"]);\n if (search.rating) {\n tokens.push(`rating:${search.rating}`);\n }\n if (search.sort) {\n tokens.push(SORT_TOKENS[search.sort]);\n }\n return tokens.join(\" \");\n}\n\nexport function buildPostSearchUrl(search: PostSearch, credentials: Credentials): string {\n if (!Number.isInteger(search.limit) || search.limit < 1 || search.limit > MAX_LIMIT) {\n throw invalidInput(\n `limit must be a whole number between 1 and ${MAX_LIMIT}, and ${search.limit} is not.`,\n `rule34.xxx serves at most ${MAX_LIMIT} posts in one request.`,\n );\n }\n if (!Number.isInteger(search.page) || search.page < 1) {\n throw invalidInput(`page must be a whole number from 1 upwards, and ${search.page} is not.`);\n }\n\n const query = encodeURIComponent(buildSearchQuery(search));\n // `pid` on this route is a page number the site multiplies by the limit. The\n // same name means an absolute offset on the site's own pages, so the two are\n // reconciled here and nowhere else.\n const pid = search.page - 1;\n\n return (\n `${API_URL}?page=dapi&s=post&q=index` +\n `&tags=${query}&limit=${search.limit}&pid=${pid}` +\n `&api_key=${encodeURIComponent(credentials.apiKey)}` +\n `&user_id=${encodeURIComponent(credentials.userId)}`\n );\n}\n\n/**\n * Ask the tag route whether one exact name exists.\n *\n * A search of several tags that comes back empty says nothing about which tag\n * emptied it, and a misspelled name looks exactly like a combination the site\n * genuinely has nothing for. This is what tells the two apart.\n */\nexport function buildTagLookupUrl(tag: string, credentials: Credentials): string {\n return (\n `${API_URL}?page=dapi&s=tag&q=index&name=${encodeURIComponent(normalizeTag(tag))}` +\n `&api_key=${encodeURIComponent(credentials.apiKey)}` +\n `&user_id=${encodeURIComponent(credentials.userId)}`\n );\n}\n\n/**\n * The id inside a link to a post's page.\n *\n * The URL is read and never fetched: this server builds its own request from\n * the id it finds. Refusing a foreign host is still right, because a link to\n * somewhere else names no post this site holds.\n */\nexport function readPostPageUrl(raw: string): number {\n let parsed: URL;\n try {\n parsed = new URL(raw.trim());\n } catch (error) {\n throw invalidInput(\n `\"${raw}\" is not a URL.`,\n \"Pass a link to a post's page, such as https://rule34.xxx/index.php?page=post&s=view&id=2195419\",\n error,\n );\n }\n\n const host = parsed.hostname.toLowerCase();\n // The site serves its pages on the bare host and its media from subdomains it\n // adds to over time, so the suffix is what stays true.\n if (host !== \"rule34.xxx\" && !host.endsWith(\".rule34.xxx\")) {\n throw invalidInput(\n `\"${raw}\" is not a rule34.xxx address.`,\n \"This tool reads posts on rule34.xxx. A link elsewhere names no post it holds.\",\n );\n }\n\n if (parsed.searchParams.get(\"page\") !== \"post\" || parsed.searchParams.get(\"s\") !== \"view\") {\n throw invalidInput(\n `\"${raw}\" is a rule34.xxx address, but not a post's page.`,\n \"A post's page looks like https://rule34.xxx/index.php?page=post&s=view&id=2195419\",\n );\n }\n\n // A query may carry `id` more than once, and reading the first would be a\n // coin flip on which post the caller meant. Two different values are the\n // contradiction an `id` and a `url` that disagree are refused for.\n const named: string[] = [...new Set(parsed.searchParams.getAll(\"id\"))];\n if (named.length > 1) {\n throw invalidInput(\n `\"${raw}\" names ${named.length} different posts: ${named.join(\", \")}.`,\n \"Pass a link to one post.\",\n );\n }\n\n const written: string = named[0] ?? \"\";\n if (written === \"\") {\n throw invalidInput(`\"${raw}\" carries no post id.`);\n }\n\n const id = Number(written);\n if (!Number.isInteger(id) || id < 1) {\n throw invalidInput(\n `\"${raw}\" carries '${written}', which is not a post id.`,\n \"A post id is a whole number from 1 upwards.\",\n );\n }\n return id;\n}\n\n/** The one post an `id` and a `url` name, refusing a pair that disagrees. */\nexport function resolvePostRef(ref: { id?: number; url?: string }): number {\n const fromUrl = ref.url === undefined ? null : readPostPageUrl(ref.url);\n const fromId = ref.id;\n\n if (fromId !== undefined) {\n if (!Number.isInteger(fromId) || fromId < 1) {\n throw invalidInput(`id must be a whole number from 1 upwards, and ${fromId} is not.`);\n }\n // Preferring one would be a coin flip on which of the caller's two\n // arguments was the mistake, and the answer would carry no trace of it.\n if (fromUrl !== null && fromUrl !== fromId) {\n throw invalidInput(\n `'id' names post ${fromId} and 'url' names post ${fromUrl}, so they cannot both be read.`,\n \"Pass one of the two, or make them agree.\",\n );\n }\n return fromId;\n }\n\n if (fromUrl !== null) {\n return fromUrl;\n }\n\n throw invalidInput(\n \"Either 'id' or 'url' must be given.\",\n \"Pass the numeric post id, or a link to the post's page on rule34.xxx.\",\n );\n}\n\n/**\n * A post is read from two addresses, because neither format carries everything.\n *\n * The XML states the publication date and the uploader's numeric id. The JSON\n * states the uploader's name, the number of comments, and a type and a count\n * for every tag. A fiche that read one of them would leave out what the other\n * one holds.\n */\nexport function buildPostByIdXmlUrl(id: number, credentials: Credentials): string {\n return `${API_URL}?page=dapi&s=post&q=index&id=${id}${credentialsQuery(credentials)}`;\n}\n\nexport function buildPostByIdJsonUrl(id: number, credentials: Credentials): string {\n return `${API_URL}?page=dapi&s=post&q=index&id=${id}&json=1&fields=tag_info${credentialsQuery(credentials)}`;\n}\n\nfunction credentialsQuery(credentials: Credentials): string {\n return (\n `&api_key=${encodeURIComponent(credentials.apiKey)}` +\n `&user_id=${encodeURIComponent(credentials.userId)}`\n );\n}\n\n/**\n * Ask the site which tag names begin with a piece of text.\n *\n * rule34.xxx publishes this route on its API host and answers it without\n * credentials, so none are sent. It matches from the start of a name and caps\n * its answer at ten, both of which the tool that calls it has to state.\n *\n * An empty query is refused here: the site answers one with the tags it holds\n * most of, which is an answer to a question nobody asked.\n */\nexport function buildTagSuggestUrl(text: string): string {\n return `${SUGGEST_URL}?q=${encodeURIComponent(normalizeTag(text))}`;\n}\n\nexport function buildPostPageUrl(id: number): string {\n return `${SITE_URL}?page=post&s=view&id=${id}`;\n}\n\n/**\n * Strip the key out of a URL before anything shows it.\n *\n * An error names the address it failed on, a model reads that text, and the\n * conversation it lands in outlives the request. The user id stays readable:\n * it names the account without opening it, which keeps a support question\n * answerable.\n */\nexport function redactCredentials(url: string): string {\n return url.replace(/([?&]api_key=)[^&]*/gi, \"$1REDACTED\");\n}\n","/**\n * HTTP layer: one GET of the API route, classified, with backoff.\n *\n * Two conditions decide the shape of this file. rule34.xxx refuses a client\n * that asks too often with HTTP 429, and it answers a request carrying no valid\n * credentials with HTTP 200 and an error document. So a status of 200 is not\n * proof of an answer, and a refusal must never reach a caller as an empty\n * result — the document itself is classified further up, by the parser.\n */\n\nimport type { Config, Logger } from \"../config.js\";\nimport { Rule34Error, notFound, rateLimited } from \"../errors.js\";\nimport { type RateLimiter, sleep } from \"./rateLimiter.js\";\nimport { redactCredentials } from \"./urls.js\";\n\nconst BACKOFF_BASE_MS = 2000;\nconst BACKOFF_FACTOR = 2;\nconst BACKOFF_MAX_MS = 30_000;\n\n/** Exponential backoff with jitter, so parallel clients do not resynchronise. */\nexport function backoffDelay(attempt: number, random: () => number = Math.random): number {\n const uncapped = BACKOFF_BASE_MS * BACKOFF_FACTOR ** attempt;\n const capped = Math.min(BACKOFF_MAX_MS, uncapped);\n return Math.round(capped * (0.5 + random() * 0.5));\n}\n\nexport interface HttpDeps {\n config: Config;\n limiter: RateLimiter;\n logger: Logger;\n fetchImpl?: typeof fetch;\n}\n\n/**\n * Fetch one API document, retrying what is worth retrying.\n *\n * The whole retry chain, backoff waits included, runs inside a single limiter\n * slot. Waiting outside it would let a queued request slip into the same\n * refusal window the current one is backing away from.\n */\nexport async function fetchXml(url: string, deps: HttpDeps): Promise<string> {\n const { config, limiter, logger } = deps;\n const doFetch = deps.fetchImpl ?? fetch;\n // Every message and every log line names the address without the key in it.\n const safeUrl = redactCredentials(url);\n\n return await limiter.schedule(async () => {\n let lastError: Rule34Error | undefined;\n\n // Set when the site says how long to stay away, replacing our own guess for\n // the next attempt. Applied here rather than where it is read, so no wait\n // is ever served after the last attempt, when nobody would use it.\n let askedWaitMs: number | null = null;\n\n for (let attempt = 0; attempt <= config.maxRetries; attempt += 1) {\n if (attempt > 0) {\n const delay = Math.min(askedWaitMs ?? backoffDelay(attempt - 1), BACKOFF_MAX_MS);\n askedWaitMs = null;\n logger.info(`retry ${attempt}/${config.maxRetries} in ${delay}ms for ${safeUrl}`);\n await sleep(delay);\n }\n\n let status: number;\n let body: string;\n let retryAfterMs: number | null = null;\n try {\n await limiter.beforeRequest();\n const response = await doFetch(url, {\n headers: {\n \"User-Agent\": config.userAgent,\n Accept: \"application/xml,text/xml;q=0.9,*/*;q=0.8\",\n },\n redirect: \"follow\",\n signal: AbortSignal.timeout(config.timeoutMs),\n });\n status = response.status;\n retryAfterMs = parseRetryAfter(response.headers.get(\"retry-after\"));\n body = await response.text();\n } catch (error) {\n lastError = asTransportError(error, safeUrl);\n logger.debug(`${lastError.code} for ${safeUrl}: ${lastError.message}`);\n continue;\n }\n\n if (status === 429) {\n limiter.penalize();\n logger.info(`rate limited on ${safeUrl}, interval now ${limiter.currentIntervalMs}ms`);\n askedWaitMs = retryAfterMs;\n lastError = rateLimited(safeUrl, retryAfterMs ?? backoffDelay(attempt));\n continue;\n }\n\n // Retrying this would only repeat the same answer.\n if (status === 404) {\n throw notFound(safeUrl);\n }\n\n if (status >= 500) {\n lastError = new Rule34Error(\"network_error\", `rule34.xxx returned HTTP ${status}.`, {\n url: safeUrl,\n status,\n });\n continue;\n }\n\n if (status >= 400) {\n throw new Rule34Error(\"network_error\", `rule34.xxx refused the request (HTTP ${status}).`, {\n url: safeUrl,\n status,\n });\n }\n\n limiter.relax();\n return body;\n }\n\n throw (\n lastError ??\n new Rule34Error(\"network_error\", `Could not fetch ${safeUrl}.`, {\n url: safeUrl,\n })\n );\n });\n}\n\n/** `Retry-After` carries either seconds or an HTTP date. */\nfunction parseRetryAfter(raw: string | null): number | null {\n if (!raw) {\n return null;\n }\n const seconds = Number(raw.trim());\n if (Number.isFinite(seconds) && seconds >= 0) {\n return Math.round(seconds * 1000);\n }\n const when = Date.parse(raw);\n if (Number.isNaN(when)) {\n return null;\n }\n return Math.max(0, when - Date.now());\n}\n\nfunction asTransportError(error: unknown, safeUrl: string): Rule34Error {\n if (error instanceof Rule34Error) {\n return error;\n }\n const name = error instanceof Error ? error.name : \"\";\n if (name === \"TimeoutError\" || name === \"AbortError\") {\n return new Rule34Error(\"timeout\", \"rule34.xxx did not answer in time.\", {\n url: safeUrl,\n hint: \"Raise RULE34_TIMEOUT_MS if this happens often on a slow connection.\",\n });\n }\n const message = error instanceof Error ? error.message : String(error);\n return new Rule34Error(\"network_error\", `Could not reach rule34.xxx: ${message}`, {\n url: safeUrl,\n });\n}\n","/**\n * Reading one post from the JSON route.\n *\n * This format carries three things the XML does not: the uploader by name, the\n * number of comments, and a type and a post count for every tag. It leaves out\n * the publication date, which is why a fiche reads both formats rather than\n * choosing one.\n */\n\nimport { missingCredentials, notFound, parseFailure, upstreamFailure } from \"../../errors.js\";\nimport type { PostDetail, TagOnPost } from \"../../types.js\";\nimport { buildPostPageUrl, redactCredentials } from \"../urls.js\";\n\n/** The words this route writes where the tag route writes a number. */\n/** The word the site puts in the answer it gives an unauthenticated request. */\nconst AUTHENTICATION = /authenticat/i;\nconst WHITESPACE = /\\s+/;\n\nconst TYPES: Record<string, string> = {\n tag: \"general\",\n artist: \"artist\",\n copyright: \"copyright\",\n character: \"character\",\n metadata: \"metadata\",\n};\n\ninterface RawTagInfo {\n tag?: unknown;\n type?: unknown;\n count?: unknown;\n}\n\nexport function parsePostDetail(body: string, url: string, id: number): PostDetail {\n const safeUrl = redactCredentials(url);\n\n // A request naming an id the site does not hold is answered with an empty\n // body, the same zero bytes a truncated read produces. Asking for one post\n // makes the two the same answer: there is no such post to report.\n if (body.trim() === \"\") {\n throw notFound(safeUrl);\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(body);\n } catch (error) {\n throw parseFailure(safeUrl, \"the response is not JSON\", error);\n }\n\n // An unauthenticated request is answered with HTTP 200 and a bare string,\n // which is a fault in the caller's configuration rather than in the site.\n if (typeof parsed === \"string\") {\n if (AUTHENTICATION.test(parsed)) {\n throw missingCredentials(safeUrl);\n }\n throw upstreamFailure(safeUrl, parsed);\n }\n\n if (!Array.isArray(parsed)) {\n throw parseFailure(safeUrl, \"the response is not a list of posts\");\n }\n const [raw] = parsed;\n if (raw === undefined) {\n throw notFound(safeUrl);\n }\n if (raw === null || typeof raw !== \"object\") {\n throw parseFailure(safeUrl, \"the first entry is not a post\");\n }\n\n return readPost(raw as Record<string, unknown>, id);\n}\n\nfunction readPost(raw: Record<string, unknown>, askedFor: number): PostDetail {\n const id = readInteger(raw.id) ?? askedFor;\n const tags = readString(raw.tags)\n .split(WHITESPACE)\n .filter((tag) => tag !== \"\");\n\n return {\n id,\n postUrl: buildPostPageUrl(id),\n fileUrl: readString(raw.file_url),\n sampleUrl: readText(raw.sample_url),\n previewUrl: readText(raw.preview_url),\n width: readInteger(raw.width) ?? 0,\n height: readInteger(raw.height) ?? 0,\n md5: readString(raw.hash),\n score: readInteger(raw.score) ?? 0,\n rating: readString(raw.rating),\n tags,\n tagDetails: readTagDetails(raw.tag_info),\n source: readText(raw.source),\n // This route writes 0 where the XML writes an empty string, and 0 is a post\n // id that belongs to somebody else.\n parentId: nonZero(readInteger(raw.parent_id)),\n owner: readText(raw.owner),\n commentCount: readInteger(raw.comment_count),\n status: readString(raw.status),\n hasNotes: raw.has_notes === true,\n changedAtUnix: readInteger(raw.change),\n };\n}\n\nfunction readTagDetails(value: unknown): TagOnPost[] {\n if (!Array.isArray(value)) {\n return [];\n }\n return value.map((entry) => {\n const info = (entry ?? {}) as RawTagInfo;\n const type = readString(info.type);\n return {\n name: readString(info.tag),\n // A word this site has not documented is carried as it came: naming it\n // something else would state a kind the site never wrote.\n type: TYPES[type] ?? type,\n postCount: readInteger(info.count) ?? 0,\n };\n });\n}\n\nfunction nonZero(value: number | null): number | null {\n return value === null || value === 0 ? null : value;\n}\n\nfunction readString(value: unknown): string {\n return typeof value === \"string\" ? value : \"\";\n}\n\nfunction readText(value: unknown): string | null {\n const trimmed = readString(value).trim();\n return trimmed === \"\" ? null : trimmed;\n}\n\nfunction readInteger(value: unknown): number | null {\n if (typeof value === \"number\") {\n return Number.isFinite(value) ? value : null;\n }\n const trimmed = readString(value).trim();\n if (trimmed === \"\") {\n return null;\n }\n const parsed = Number(trimmed);\n return Number.isFinite(parsed) ? parsed : null;\n}\n","/**\n * Reading the list of names rule34.xxx suggests for a piece of text.\n *\n * The site writes each entry twice over: a `label` it means to display, which\n * carries the post count in brackets, and a `value` a search actually takes.\n * The count is read out of the label because that is the only place the route\n * puts it.\n */\n\nimport { parseFailure } from \"../../errors.js\";\nimport type { TagSuggestion } from \"../../types.js\";\nimport { redactCredentials } from \"../urls.js\";\n\ninterface RawSuggestion {\n label?: unknown;\n value?: unknown;\n}\n\n/** The count the site prints at the end of a label, as in `kimono (55638)`. */\nconst COUNT_IN_LABEL = /\\((\\d+)\\)\\s*$/;\n\nexport function parseTagSuggestions(body: string, url: string): TagSuggestion[] {\n const safeUrl = redactCredentials(url);\n\n if (body.trim() === \"\") {\n throw parseFailure(safeUrl, \"the response was empty\");\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(body);\n } catch (error) {\n throw parseFailure(safeUrl, \"the response is not JSON\", error);\n }\n\n if (!Array.isArray(parsed)) {\n throw parseFailure(safeUrl, \"the response is not a list of suggestions\");\n }\n\n const suggestions: TagSuggestion[] = [];\n for (const entry of parsed) {\n const raw = (entry ?? {}) as RawSuggestion;\n const name = typeof raw.value === \"string\" ? raw.value.trim() : \"\";\n // An entry with no name names no tag, and carrying it forward would offer a\n // suggestion a caller cannot search for.\n if (name === \"\") {\n continue;\n }\n const label = typeof raw.label === \"string\" ? raw.label : \"\";\n const counted = COUNT_IN_LABEL.exec(label);\n suggestions.push({\n name,\n // Zero is a number of posts. A label the site printed no count into\n // leaves the count unknown.\n postCount: counted?.[1] === undefined ? null : Number(counted[1]),\n });\n }\n return suggestions;\n}\n","/**\n * Turning one API answer into a document, and classifying the answers that are\n * not one.\n *\n * Every route of this API answers a request carrying no valid credentials with\n * HTTP 200 and an `<error>` document, so the transport cannot tell that apart\n * from a result. It is told apart here, once, for every route.\n */\n\nimport { XMLParser } from \"fast-xml-parser\";\nimport { missingCredentials, parseFailure, upstreamFailure } from \"../../errors.js\";\n\n/** The word the site puts in the error it answers an unauthenticated request with. */\nconst AUTHENTICATION = /authenticat/i;\n\nexport type Attributes = Record<string, string | undefined>;\n\nconst parser = new XMLParser({\n ignoreAttributes: false,\n attributeNamePrefix: \"@_\",\n // Attribute values are converted one field at a time by each parser, so that\n // an empty `parent_id` stays empty instead of becoming a number.\n parseAttributeValue: false,\n trimValues: true,\n});\n\nexport function readDocument(xml: string, safeUrl: string): Record<string, unknown> {\n if (xml.trim() === \"\") {\n throw parseFailure(safeUrl, \"the response was empty\");\n }\n\n let document: unknown;\n try {\n document = parser.parse(xml);\n } catch (error) {\n throw parseFailure(safeUrl, error instanceof Error ? error.message : \"unreadable XML\");\n }\n\n if (document === null || typeof document !== \"object\") {\n throw parseFailure(safeUrl, \"the response is not an XML document\");\n }\n\n const parsed = document as Record<string, unknown>;\n const failure = parsed.error;\n if (failure !== undefined) {\n const message = typeof failure === \"string\" ? failure : String(failure);\n // A fault in the caller's configuration rather than in the site.\n if (AUTHENTICATION.test(message)) {\n throw missingCredentials(safeUrl);\n }\n throw upstreamFailure(safeUrl, message);\n }\n\n return parsed;\n}\n\n/** A lone element parses to an object, several to an array, none to nothing. */\nexport function toArray(value: unknown): Attributes[] {\n if (value === undefined || value === null) {\n return [];\n }\n return (Array.isArray(value) ? value : [value]) as Attributes[];\n}\n\nexport function readText(value: string | undefined): string | null {\n const trimmed = value?.trim() ?? \"\";\n return trimmed === \"\" ? null : trimmed;\n}\n\nexport function readInteger(value: string | undefined): number | null {\n const trimmed = value?.trim() ?? \"\";\n if (trimmed === \"\") {\n return null;\n }\n const parsed = Number(trimmed);\n return Number.isFinite(parsed) ? parsed : null;\n}\n","/**\n * Reading the `<posts>` document the API answers a search with.\n *\n * This route is read as XML rather than as JSON for one reason: the XML states\n * the total and states an empty result as `count=\"0\"`, where the JSON answers a\n * search that found nothing with an empty body — the same zero bytes a\n * truncated read produces. A total that cannot be read is unknown here, never\n * zero.\n */\n\nimport { parseFailure } from \"../../errors.js\";\nimport type { PostList, Rule34Post } from \"../../types.js\";\nimport { buildPostPageUrl, redactCredentials } from \"../urls.js\";\nimport { type Attributes, readDocument, readInteger, readText, toArray } from \"./document.js\";\n\n/** The letters this route writes where the site writes a word. */\nconst WHITESPACE = /\\s+/;\n\nconst RATINGS: Record<string, string> = {\n e: \"explicit\",\n q: \"questionable\",\n s: \"safe\",\n};\n\nexport function parsePostList(xml: string, url: string): PostList {\n const safeUrl = redactCredentials(url);\n const document = readDocument(xml, safeUrl);\n\n const posts = document.posts;\n if (posts === undefined || posts === null || typeof posts !== \"object\") {\n throw parseFailure(safeUrl, \"no <posts> element\");\n }\n\n const attributes = posts as Attributes & { post?: unknown };\n const total = readInteger(attributes[\"@_count\"]);\n if (total === null) {\n throw parseFailure(safeUrl, \"the <posts> element carries no count\");\n }\n\n return {\n total,\n offset: readInteger(attributes[\"@_offset\"]) ?? 0,\n posts: toArray(attributes.post).map(readPost),\n };\n}\n\nfunction readPost(attributes: Attributes): Rule34Post {\n const id = readInteger(attributes[\"@_id\"]) ?? 0;\n const rating = attributes[\"@_rating\"]?.trim() ?? \"\";\n\n return {\n id,\n postUrl: buildPostPageUrl(id),\n fileUrl: attributes[\"@_file_url\"] ?? \"\",\n sampleUrl: readText(attributes[\"@_sample_url\"]),\n previewUrl: readText(attributes[\"@_preview_url\"]),\n width: readInteger(attributes[\"@_width\"]) ?? 0,\n height: readInteger(attributes[\"@_height\"]) ?? 0,\n md5: attributes[\"@_md5\"] ?? \"\",\n score: readInteger(attributes[\"@_score\"]) ?? 0,\n // An unknown letter is carried as it came: naming it would state a rating\n // the site never wrote.\n rating: RATINGS[rating] ?? rating,\n tags: (attributes[\"@_tags\"] ?? \"\").split(WHITESPACE).filter((tag) => tag !== \"\"),\n source: readText(attributes[\"@_source\"]),\n // The attribute is present and empty on a post with no parent, and 0 is a\n // post id that belongs to somebody else.\n parentId: readInteger(attributes[\"@_parent_id\"]),\n creatorId: readInteger(attributes[\"@_creator_id\"]),\n status: attributes[\"@_status\"] ?? \"\",\n hasChildren: attributes[\"@_has_children\"] === \"true\",\n hasComments: attributes[\"@_has_comments\"] === \"true\",\n hasNotes: attributes[\"@_has_notes\"] === \"true\",\n createdAt: readDate(attributes[\"@_created_at\"]),\n changedAtUnix: readInteger(attributes[\"@_change\"]),\n };\n}\n\n/**\n * The publication date, which this route prints in words rather than as a\n * stamp. A date that cannot be read is unknown: the post's `change` stamp is\n * the date of the last edit, and standing in for the other one would date an\n * old post to whenever somebody last retagged it.\n */\nfunction readDate(value: string | undefined): string | null {\n const trimmed = value?.trim() ?? \"\";\n if (trimmed === \"\") {\n return null;\n }\n const parsed = Date.parse(trimmed);\n return Number.isNaN(parsed) ? null : new Date(parsed).toISOString();\n}\n","/**\n * Reading the `<tags>` document, which says whether a name is a tag at all.\n *\n * This is what a search of several tags needs when it finds nothing: a\n * misspelled name and a combination the site genuinely holds nothing for\n * produce the same empty result, and only this route separates them.\n */\n\nimport { parseFailure } from \"../../errors.js\";\nimport type { TagRef, TagType } from \"../../types.js\";\nimport { redactCredentials } from \"../urls.js\";\nimport { type Attributes, readDocument, readInteger, toArray } from \"./document.js\";\n\n/**\n * How the site numbers the kinds of tag.\n *\n * Read off the site itself rather than assumed: `black_hair` answers 0,\n * `jall_boint` 1, `prince_of_tennis` 3, `asuka_langley_sohryu` 4 and\n * `crossover` 5.\n */\nconst TYPES: Record<number, TagType> = {\n 0: \"general\",\n 1: \"artist\",\n 3: \"copyright\",\n 4: \"character\",\n 5: \"metadata\",\n};\n\nexport function parseTagList(xml: string, url: string): TagRef[] {\n const safeUrl = redactCredentials(url);\n const document = readDocument(xml, safeUrl);\n\n const tags = document.tags;\n if (tags !== undefined && tags !== null && typeof tags === \"object\") {\n return toArray((tags as { tag?: unknown }).tag).map(readTag);\n }\n\n // A name the site holds nothing for comes back under a root element named in\n // the singular and carrying nothing. Reading that as a broken document would\n // turn \"no such tag\" into \"the site is unreadable\", which is the opposite of\n // what this route is asked.\n const lone = document.tag;\n if (lone !== undefined && lone !== null && typeof lone === \"object\") {\n const attributes = lone as Attributes;\n return attributes[\"@_name\"] === undefined ? [] : [readTag(attributes)];\n }\n\n throw parseFailure(safeUrl, \"no <tags> element\");\n}\n\nfunction readTag(attributes: Attributes): TagRef {\n const typeCode = readInteger(attributes[\"@_type\"]);\n return {\n id: readInteger(attributes[\"@_id\"]) ?? 0,\n name: attributes[\"@_name\"] ?? \"\",\n postCount: readInteger(attributes[\"@_count\"]) ?? 0,\n // A number this site has not documented is left unnamed: calling it\n // something would state a kind the site never wrote.\n type: (typeCode === null ? undefined : TYPES[typeCode]) ?? \"unknown\",\n typeCode,\n ambiguous: attributes[\"@_ambiguous\"] === \"true\",\n };\n}\n","/**\n * The reading layer: requests, pacing, cache, and the shapes they produce.\n *\n * Nothing here imports the MCP SDK, so this file is also what the package\n * publishes under its `./client` entry point: a program can read rule34.xxx\n * with the same pacing and the same error taxonomy without a protocol attached.\n */\n\nimport type { Config, Logger } from \"../config.js\";\nimport { invalidInput } from \"../errors.js\";\nimport type { PostDetail, PostList, TagRef, TagSuggestion } from \"../types.js\";\nimport { TtlLruCache } from \"./cache.js\";\nimport { fetchXml } from \"./http.js\";\nimport { parsePostDetail } from \"./parsers/postDetail.js\";\nimport { parseTagSuggestions } from \"./parsers/suggestions.js\";\nimport { parsePostList } from \"./parsers/posts.js\";\nimport { parseTagList } from \"./parsers/tags.js\";\nimport { RateLimiter } from \"./rateLimiter.js\";\nimport {\n buildPostByIdJsonUrl,\n buildPostByIdXmlUrl,\n buildPostSearchUrl,\n buildTagLookupUrl,\n buildTagSuggestUrl,\n normalizeTag,\n type Credentials,\n type PostSearch,\n redactCredentials,\n resolvePostRef,\n} from \"./urls.js\";\n\n/** Every read says whether it reached the site or answered from memory. */\nexport interface Read<T> {\n data: T;\n cached: boolean;\n}\n\nexport interface Rule34ClientDeps {\n config: Config;\n logger: Logger;\n fetchImpl?: typeof fetch;\n}\n\nexport class Rule34Client {\n private readonly limiter: RateLimiter;\n private readonly cache: TtlLruCache<string>;\n\n constructor(private readonly deps: Rule34ClientDeps) {\n this.limiter = new RateLimiter({ minIntervalMs: deps.config.minIntervalMs });\n this.cache = new TtlLruCache(deps.config.cacheMaxEntries, deps.config.cacheTtlMs);\n }\n\n /** The addresses currently held, for a diagnostic to look at. */\n cacheKeys(): string[] {\n return this.cache.keys();\n }\n\n async searchPosts(search: PostSearch): Promise<Read<PostList>> {\n const url = buildPostSearchUrl(search, this.credentials());\n const { body, cached } = await this.read(url);\n return { data: parsePostList(body, url), cached };\n }\n\n /**\n * One post, read from both formats the API publishes.\n *\n * Neither format carries everything: the publication date and the uploader's\n * numeric id come from the XML, and the uploader's name, the comment count\n * and the tag types come from the JSON. Reading one of them would leave out\n * what the other holds, so a fiche costs two reads of the site.\n */\n async getPost(ref: { id?: number; url?: string }): Promise<Read<PostDetail>> {\n const credentials = this.credentials();\n const id = resolvePostRef(ref);\n\n const jsonUrl = buildPostByIdJsonUrl(id, credentials);\n const json = await this.read(jsonUrl);\n // Reading the JSON first means a post the site no longer holds is reported\n // as absent before a second request is spent on it.\n const detail = parsePostDetail(json.body, jsonUrl, id);\n\n const xmlUrl = buildPostByIdXmlUrl(id, credentials);\n const xml = await this.read(xmlUrl);\n const [fromXml] = parsePostList(xml.body, xmlUrl).posts;\n\n return {\n data: {\n ...detail,\n createdAt: fromXml?.createdAt ?? null,\n creatorId: fromXml?.creatorId ?? null,\n hasComments: fromXml?.hasComments ?? (detail.commentCount ?? 0) > 0,\n hasChildren: fromXml?.hasChildren ?? false,\n },\n cached: json.cached && xml.cached,\n };\n }\n\n /**\n * The names rule34.xxx offers for a piece of text.\n *\n * This route needs no credentials, so none are required to call it: a server\n * with no key configured can still tell a caller how a tag is spelled, which\n * is the one useful thing it can do.\n */\n async findTags(text: string): Promise<Read<TagSuggestion[]>> {\n const url = buildTagSuggestUrl(text);\n const { body, cached } = await this.read(url);\n return { data: parseTagSuggestions(body, url), cached };\n }\n\n /** The tag under this exact name, or nothing when the site holds no such tag. */\n async lookupTag(name: string): Promise<Read<TagRef | null>> {\n const url = buildTagLookupUrl(name, this.credentials());\n const { body, cached } = await this.read(url);\n const [tag] = parseTagList(body, url);\n return { data: tag ?? null, cached };\n }\n\n /**\n * Which of these names the site does not hold as a tag.\n *\n * A search of several tags that finds nothing says nothing about which tag\n * emptied it, and a misspelled name is indistinguishable from a combination\n * the site genuinely has nothing for. Each distinct name costs one request,\n * which is why this is worth spending only once a search has come back empty.\n */\n async findUnknownTags(names: string[]): Promise<string[]> {\n const distinct = [...new Set(names.map(normalizeTag))];\n const unknown: string[] = [];\n for (const name of distinct) {\n const { data } = await this.lookupTag(name);\n if (data === null) {\n unknown.push(name);\n }\n }\n return unknown;\n }\n\n private credentials(): Credentials {\n const credentials = this.deps.config.credentials;\n if (!credentials) {\n throw invalidInput(\n \"This server has no rule34.xxx API credentials, so it cannot ask the site anything.\",\n \"Set RULE34_USER_ID and RULE34_API_KEY in the env block of this server's entry in your MCP client \" +\n \"configuration. Both are shown under 'API Access Credentials' at \" +\n \"https://rule34.xxx/index.php?page=account&s=options once you are signed in. The site issues one \" +\n \"key per person, so use your own rather than sharing one.\",\n );\n }\n return credentials;\n }\n\n /**\n * Fetch one address, remembering the answer under it.\n *\n * The key is the address without the key in it: two searches differ by their\n * query and never by their credentials, and holding the secret in a second\n * place buys nothing.\n */\n private async read(url: string): Promise<{ body: string; cached: boolean }> {\n const key = redactCredentials(url);\n const remembered = this.cache.get(key);\n if (remembered !== undefined) {\n this.deps.logger.debug(`cache hit for ${key}`);\n return { body: remembered, cached: true };\n }\n\n const body = await fetchXml(url, {\n config: this.deps.config,\n limiter: this.limiter,\n logger: this.deps.logger,\n ...(this.deps.fetchImpl ? { fetchImpl: this.deps.fetchImpl } : {}),\n });\n this.cache.set(key, body);\n return { body, cached: false };\n }\n}\n","/**\n * find_tags: ask rule34.xxx how a tag is spelled.\n *\n * A search of a name the site does not hold ends there, and a caller with no\n * way to look a name up has no next step. This is that step, and it reads the\n * one route the site publishes for it.\n */\n\nimport { z } from \"zod\";\nimport type { Rule34Client } from \"../rule34/client.js\";\nimport { normalizeTag } from \"../rule34/urls.js\";\nimport { strictInput, tagName } from \"./arguments.js\";\nimport { ok, toToolError, type ToolResult } from \"./shared.js\";\n\n/**\n * The most names the site ever offers.\n *\n * It answers ten whatever it holds, so ten back means the list was cut rather\n * than exhausted, and an answer that kept quiet about it would pass a cap off\n * as a complete list.\n */\nexport const SUGGEST_CAP = 10;\n\nexport const findTagsDescription = [\n \"Find how rule34.xxx spells a tag, and how many posts carry it.\",\n \"The site matches from the start of a name, so 'kimagure' finds 'kimagure_orange_road'\",\n \"while 'orange road' finds nothing. It offers at most ten names.\",\n \"Use this before searching when a name might be spelled differently, and after a search that found\",\n \"nothing because a tag does not exist.\",\n].join(\" \");\n\nexport const findTagsInput = strictInput({\n query: tagName(\"query\", 60).describe(\n \"The start of a tag name, such as 'kimagure' or 'prince of tennis'. Spaces are joined with underscores.\",\n ),\n});\n\nexport const findTagsOutputShape = {\n query: z.string().describe(\"The text as rule34.xxx would spell it.\"),\n tags: z.array(\n z.object({\n name: z.string().describe(\"The name a search takes.\"),\n post_count: z\n .number()\n .int()\n .nullable()\n .describe(\"Posts the site credits to it, or nothing when it printed no count.\"),\n }),\n ),\n source: z.literal(\"rule34.xxx\"),\n notes: z.array(z.string()),\n};\n\nexport interface FindTagsArgs {\n query: string;\n}\n\nexport async function runFindTags(client: Rule34Client, args: FindTagsArgs): Promise<ToolResult> {\n try {\n const { data, cached } = await client.findTags(args.query);\n const notes = cached ? [\"Served from this server's short-lived in-memory cache.\"] : [];\n\n if (data.length === 0) {\n notes.push(\n \"rule34.xxx offers no name for this text. It matches from the start of a name, so a word \" +\n \"sitting inside a tag finds nothing here; try the words a name begins with.\",\n );\n } else if (data.length >= SUGGEST_CAP) {\n notes.push(\n \"rule34.xxx caps this list at ten names, and it returned ten, so more may begin the same \" +\n \"way. Type more of the name to narrow it.\",\n );\n }\n\n // The site was asked for the text spelled its way, and that spelling is the\n // one a caller has to search with.\n const asked = normalizeTag(args.query);\n\n const structured = {\n query: asked,\n tags: data.map((tag) => ({ name: tag.name, post_count: tag.postCount })),\n source: \"rule34.xxx\" as const,\n notes,\n };\n\n const body =\n data.length === 0\n ? `rule34.xxx offers no tag name beginning with ${asked}.`\n : `${data.length} name(s) rule34.xxx offers for ${asked}:\\n${data\n .map(\n (tag) => ` ${tag.name}${tag.postCount === null ? \"\" : ` (${tag.postCount} posts)`}`,\n )\n .join(\"\\n\")}`;\n\n return ok(structured, body, notes);\n } catch (error) {\n return toToolError(error);\n }\n}\n","/**\n * How a tool's arguments are declared, and what happens to one that is not.\n *\n * An argument this server does not declare is a question it cannot answer.\n * Reading it and dropping it produces an answer computed on the defaults, which\n * a caller reads as the answer to what they asked. So an undeclared argument is\n * refused, and the refusal names it and offers the declared name when one is\n * close enough to be the one that was meant.\n */\n\nimport { z } from \"zod\";\n\n/** The code a caller branches on when the arguments cannot produce a request. */\nconst INVALID_INPUT = \"invalid_input\";\nconst CODE = `[${INVALID_INPUT}]`;\n\n/**\n * Why each builder below stamps its own message.\n *\n * A bound such as `min` raises its issue from the check that carries it, and\n * the issue names that check as the instance whose error map applies. A map set\n * on the schema alone therefore leaves the bounds refusing in zod's own words,\n * with no code for a caller to branch on. So the message is written onto the\n * check itself.\n */\nexport function choice<const Options extends readonly [string, ...string[]]>(\n argument: string,\n options: Options,\n what: string,\n): z.ZodEnum<{ [K in Options[number]]: K }> {\n const error = `${CODE} ${argument} takes one of ${options.join(\", \")}. ${what}`;\n return z.enum(options, { error });\n}\n\nexport function wholeNumber(argument: string, min: number, max: number): z.ZodNumber {\n const error = `${CODE} ${argument} takes a whole number from ${min} to ${max}.`;\n return z.number({ error }).int(error).min(min, error).max(max, error);\n}\n\nexport function tagName(argument: string, most: number): z.ZodString {\n // Two causes, two messages: a name written empty and a name written long are\n // different mistakes, and one message for both sends half the callers\n // counting characters they never wrote.\n const empty = `${CODE} ${argument} takes a tag name. Written empty, spaces alone included, it narrows nothing.`;\n const long = `${CODE} ${argument} takes a tag name of at most ${most} characters.`;\n return z.string({ error: empty }).trim().min(1, empty).max(most, long);\n}\n\nexport function tagNames(argument: string, least: number, most: number): z.ZodArray<z.ZodString> {\n const error = `${CODE} ${argument} takes between ${least} and ${most} tag names.`;\n return z.array(tagName(argument, 120), { error }).min(least, error).max(most, error);\n}\n\n/** Declare a tool's arguments, refusing anything outside the declaration. */\nexport function strictInput<Shape extends z.ZodRawShape>(shape: Shape) {\n const declared = Object.keys(shape);\n\n return z.strictObject(shape, {\n error: (issue) =>\n issue.code === \"unrecognized_keys\" ? unknownArgumentMessage(issue.keys, declared) : undefined,\n });\n}\n\nfunction unknownArgumentMessage(keys: readonly string[], declared: readonly string[]): string {\n const named = keys\n .map((key) => {\n const near = nearestArgument(key, declared);\n return near ? `'${key}' (did you mean '${near}'?)` : `'${key}'`;\n })\n .join(\", \");\n\n return (\n `[${INVALID_INPUT}] Unknown ${keys.length > 1 ? \"arguments\" : \"argument\"} ${named}. ` +\n `This tool takes: ${declared.join(\", \")}.`\n );\n}\n\n/**\n * The declared name a caller most plausibly meant, when there is one.\n *\n * Three readings, ordered by how much each claims: the same name written\n * differently, a name that opens or closes the other, and a name a couple of\n * typing slips away. Anything further is left unnamed, because a suggestion\n * that misses sends a caller to an argument answering a different question.\n */\nfunction nearestArgument(key: string, declared: readonly string[]): string | undefined {\n const flatten = (name: string) => name.toLowerCase().replace(/[^a-z0-9]/g, \"\");\n const flat = flatten(key);\n if (flat.length === 0) {\n return undefined;\n }\n\n const sameName = declared.find((name) => flatten(name) === flat);\n if (sameName) {\n return sameName;\n }\n\n // Either name may be the longer one: a caller can qualify a name this tool\n // keeps plain, or shorten one it spells out.\n const overlapping = declared.find((name) => {\n const other = flatten(name);\n const [shorter, longer] = other.length < flat.length ? [other, flat] : [flat, other];\n // Two characters in common say nothing; three start to.\n return shorter.length >= 3 && (longer.startsWith(shorter) || longer.endsWith(shorter));\n });\n if (overlapping) {\n return overlapping;\n }\n\n let closest: string | undefined;\n let shortest = Number.POSITIVE_INFINITY;\n for (const name of declared) {\n const distance = editDistance(flat, flatten(name));\n if (distance < shortest) {\n shortest = distance;\n closest = name;\n }\n }\n\n // Up to a third of the name may differ. Past that the match is a guess.\n return shortest <= Math.max(1, Math.floor(flat.length / 3)) ? closest : undefined;\n}\n\n/**\n * Single-character insertions, deletions and substitutions between two words.\n *\n * One row of the matrix is kept at a time. The cell above comes from walking\n * that row, and the two other neighbours a cell needs, the one to its left and\n * the one diagonally above it, are carried forward from the step before, so\n * every value the walk uses is one it has just computed.\n */\nfunction editDistance(left: string, right: string): number {\n let previous = Array.from({ length: right.length + 1 }, (_, index) => index);\n let distance = right.length;\n\n for (let row = 1; row <= left.length; row += 1) {\n const current: number[] = [row];\n let diagonal = row - 1;\n let leftward = row;\n\n for (const [column, above] of previous.entries()) {\n if (column === 0) {\n continue;\n }\n const substitution = diagonal + (left[row - 1] === right[column - 1] ? 0 : 1);\n const cell = Math.min(substitution, above + 1, leftward + 1);\n current.push(cell);\n diagonal = above;\n leftward = cell;\n }\n\n previous = current;\n distance = leftward;\n }\n\n return distance;\n}\n","/**\n * Pieces the tool layer shares: the result envelope, error rendering, and the\n * text mirror of a structured answer.\n */\n\nimport { z } from \"zod\";\nimport { Rule34Error } from \"../errors.js\";\nimport type { Rule34Post } from \"../types.js\";\n\n/** Many MCP clients render only the text block, so it must stand on its own. */\nexport const MAX_TEXT_MIRROR_CHARS = 2000;\n\n/**\n * The last page a search will accept.\n *\n * A sentence may only name a page the schema would accept, or it sends a caller\n * into an argument that is refused.\n */\nexport const MAX_PAGE = 200;\n\n/**\n * How many of a post's tags a search row shows.\n *\n * Posts carry a median of about two hundred tags and some carry nine hundred,\n * so a page of twenty would spend four fifths of its answer on tag strings. A\n * row shows an extract and states the real count beside it, and get_post reads\n * the whole list when a caller wants it.\n */\nexport const TAG_PREVIEW = 12;\n\nexport const postSchema = z.object({\n id: z.number().int(),\n post_url: z.string().describe(\"The rule34.xxx page for this post.\"),\n file_url: z.string().describe(\"The image or video itself, on the site's CDN.\"),\n preview_url: z.string().nullable().describe(\"Thumbnail, when the site published one.\"),\n sample_url: z.string().nullable().describe(\"Reduced copy, when the site published one.\"),\n width: z.number().int(),\n height: z.number().int(),\n score: z.number().int().describe(\"The site's own score, updated once a day.\"),\n rating: z.string().describe(\"As the site holds it: 'explicit' or 'questionable'.\"),\n tags: z.array(z.string()).describe(\"The first tags of the post, at most 12 of them.\"),\n tags_total: z.number().int().describe(\"How many tags the post carries in all.\"),\n source: z.string().nullable().describe(\"Whatever the uploader credited, when they credited any.\"),\n created_at: z\n .string()\n .nullable()\n .describe(\n \"When the post was added to rule34.xxx, ISO 8601. The site imported much of its older catalogue in bulk, so thousands of posts share one day.\",\n ),\n has_comments: z.boolean(),\n parent_id: z.number().int().nullable(),\n});\n\nexport type PostOut = z.infer<typeof postSchema>;\n\n/**\n * The tags a row shows: the ones the search asked for first, then the rest in\n * the site's own order.\n *\n * A row that omits the very tag it matched on reads as an off-topic result, and\n * the site lists a post's tags alphabetically often enough that a searched name\n * falls outside the first twelve. Only names the post carries are lifted: a row\n * states what the post holds, never what was asked for.\n */\nfunction previewTags(tags: string[], asked: readonly string[]): string[] {\n const carried = asked.filter((tag) => tags.includes(tag));\n const rest = tags.filter((tag) => !carried.includes(tag));\n return [...carried, ...rest].slice(0, TAG_PREVIEW);\n}\n\nexport function toPostOut(post: Rule34Post, asked: readonly string[] = []): PostOut {\n return {\n id: post.id,\n post_url: post.postUrl,\n file_url: post.fileUrl,\n preview_url: post.previewUrl,\n sample_url: post.sampleUrl,\n width: post.width,\n height: post.height,\n score: post.score,\n rating: post.rating,\n tags: previewTags(post.tags, asked),\n tags_total: post.tags.length,\n source: post.source,\n created_at: post.createdAt,\n has_comments: post.hasComments,\n parent_id: post.parentId,\n };\n}\n\nexport interface ToolResult {\n // The SDK's CallToolResult carries an index signature for protocol extensions.\n [key: string]: unknown;\n content: Array<{ type: \"text\"; text: string }>;\n structuredContent?: Record<string, unknown>;\n isError?: boolean;\n}\n\n/**\n * Keep text from the site out of the shape this server's own lines take.\n *\n * A block ending in lines that open \"Note:\" gives a caller no way to tell one\n * of those from the same words inside a tag somebody else published. Indenting\n * such a line in the body keeps the two apart, and the structured payload still\n * carries the text exactly as it was published.\n */\nfunction indentMarkerLines(body: string): string {\n return body.replace(/^(Note:)/gm, \" $1\");\n}\n\n/**\n * Build a result whose text block ends with its notes.\n *\n * The notes are what qualifies an answer: that a tag in the search does not\n * exist, that the page asked for lies past the end, that the words came from\n * this server's cache. A client rendering only the text reads an unqualified\n * answer without them.\n */\nexport function ok(\n structured: Record<string, unknown>,\n text: string,\n notes: string[] = [],\n): ToolResult {\n const trailer = notes.map((note) => `Note: ${note}`).join(\"\\n\");\n const budget = MAX_TEXT_MIRROR_CHARS - (trailer ? trailer.length + 2 : 0);\n const body = truncate(indentMarkerLines(text), Math.max(0, budget));\n\n return {\n content: [{ type: \"text\", text: trailer ? `${body}\\n\\n${trailer}` : body }],\n structuredContent: structured,\n };\n}\n\n/**\n * Error results carry no structuredContent: the SDK validates it against the\n * tool's declared output schema, which an error payload does not satisfy.\n */\nexport function toToolError(error: unknown): ToolResult {\n const known =\n error instanceof Rule34Error\n ? error\n : new Rule34Error(\"network_error\", error instanceof Error ? error.message : String(error));\n\n const lines = [known.message];\n if (known.details.hint) {\n lines.push(`Hint: ${known.details.hint}`);\n }\n if (known.details.url) {\n lines.push(`URL: ${known.details.url}`);\n }\n\n return { content: [{ type: \"text\", text: lines.join(\"\\n\") }], isError: true };\n}\n\nexport function truncate(text: string, maxChars: number): string {\n if (text.length <= maxChars) {\n return text;\n }\n return `${text.slice(0, maxChars - 1).trimEnd()}…`;\n}\n\n/** Compact listing, one post to a line and a half. */\nexport function renderPostList(posts: PostOut[]): string {\n return posts\n .map((post, index) => {\n const head =\n `${index + 1}. #${post.id} · ${post.width}×${post.height} · score ${post.score}` +\n ` · ${post.rating}`;\n return `${head}\\n ${post.post_url}\\n ${post.file_url}`;\n })\n .join(\"\\n\");\n}\n","/**\n * get_post: read one post whole.\n *\n * A search row shows an extract of a post's tags, because a page of twenty\n * would otherwise spend four fifths of its answer on tag strings. This is where\n * the whole list lives, with a kind and a post count for every tag, alongside\n * the uploader's name and the number of comments.\n */\n\nimport { z } from \"zod\";\nimport type { Rule34Client } from \"../rule34/client.js\";\nimport { strictInput, wholeNumber } from \"./arguments.js\";\nimport { ok, toToolError, type ToolResult } from \"./shared.js\";\n\nexport const getPostDescription = [\n \"Read one rule34.xxx post, named by its id or by a link to its page.\",\n \"Returns every tag the post carries, each with its kind (character, copyright, artist, general or\",\n \"metadata) and how many posts share it, alongside the image or video, the dimensions, the score,\",\n \"the rating, the uploader, the number of comments, the credited source and the date the site\",\n \"took the post in.\",\n \"Use this after a search, whose rows show only the first few tags of each post.\",\n].join(\" \");\n\nexport const getPostInput = strictInput({\n id: wholeNumber(\"id\", 1, Number.MAX_SAFE_INTEGER)\n .optional()\n .describe(\"The post's numeric id, as a search returns it.\"),\n url: z\n .string()\n .max(500)\n .optional()\n .describe(\n \"A link to the post's page, such as https://rule34.xxx/index.php?page=post&s=view&id=2195419. \" +\n \"The link is read rather than followed.\",\n ),\n});\n\nexport const getPostOutputShape = {\n id: z.number().int(),\n post_url: z.string(),\n file_url: z.string(),\n preview_url: z.string().nullable(),\n sample_url: z.string().nullable(),\n width: z.number().int(),\n height: z.number().int(),\n md5: z.string(),\n score: z.number().int().describe(\"The site's own score, updated once a day.\"),\n rating: z.string(),\n tags: z.array(\n z.object({\n name: z.string(),\n type: z.string().describe(\"character, copyright, artist, general or metadata.\"),\n post_count: z.number().int().describe(\"Posts the site credits to this tag.\"),\n }),\n ),\n source: z.string().nullable(),\n parent_id: z.number().int().nullable(),\n owner: z.string().nullable().describe(\"The account that uploaded the post.\"),\n comment_count: z.number().int().nullable(),\n created_at: z\n .string()\n .nullable()\n .describe(\n \"When the post was added to rule34.xxx, ISO 8601. The site imported much of its older catalogue in bulk, so thousands of posts share one day.\",\n ),\n changed_at: z.string().nullable().describe(\"When the post last changed, ISO 8601.\"),\n status: z.string(),\n has_notes: z.boolean(),\n source_site: z.literal(\"rule34.xxx\"),\n notes: z.array(z.string()),\n};\n\nexport interface GetPostArgs {\n id?: number;\n url?: string;\n}\n\nexport async function runGetPost(client: Rule34Client, args: GetPostArgs): Promise<ToolResult> {\n try {\n const { data, cached } = await client.getPost(args);\n const notes = cached ? [\"Served from this server's short-lived in-memory cache.\"] : [];\n\n const structured = {\n id: data.id,\n post_url: data.postUrl,\n file_url: data.fileUrl,\n preview_url: data.previewUrl,\n sample_url: data.sampleUrl,\n width: data.width,\n height: data.height,\n md5: data.md5,\n score: data.score,\n rating: data.rating,\n tags: data.tagDetails.map((tag) => ({\n name: tag.name,\n type: tag.type,\n post_count: tag.postCount,\n })),\n source: data.source,\n parent_id: data.parentId,\n owner: data.owner,\n comment_count: data.commentCount,\n created_at: data.createdAt ?? null,\n changed_at: toIso(data.changedAtUnix),\n status: data.status,\n has_notes: data.hasNotes,\n source_site: \"rule34.xxx\" as const,\n notes,\n };\n\n return ok(structured, render(structured), notes);\n } catch (error) {\n return toToolError(error);\n }\n}\n\nfunction toIso(unixSeconds: number | null): string | null {\n return unixSeconds === null ? null : new Date(unixSeconds * 1000).toISOString();\n}\n\n/** The kinds worth naming first: they say what the post is of. */\nconst NAMED_FIRST = [\"character\", \"copyright\", \"artist\"];\n\nfunction render(post: {\n id: number;\n post_url: string;\n file_url: string;\n width: number;\n height: number;\n score: number;\n rating: string;\n owner: string | null;\n created_at: string | null;\n comment_count: number | null;\n tags: Array<{ name: string; type: string; post_count: number }>;\n}): string {\n const lines = [\n `Post #${post.id} · ${post.width}×${post.height} · score ${post.score} · ${post.rating}`,\n ` ${post.post_url}`,\n ` ${post.file_url}`,\n ];\n\n const added = post.created_at ? post.created_at.slice(0, 10) : \"an unstated date\";\n lines.push(` added to rule34.xxx ${added}${post.owner ? ` by ${post.owner}` : \"\"}`);\n if (post.comment_count !== null) {\n lines.push(` ${post.comment_count} comment(s)`);\n }\n\n for (const kind of NAMED_FIRST) {\n const named = post.tags.filter((tag) => tag.type === kind).map((tag) => tag.name);\n if (named.length > 0) {\n lines.push(` ${kind}: ${named.join(\", \")}`);\n }\n }\n const rest = post.tags.filter((tag) => !NAMED_FIRST.includes(tag.type)).map((tag) => tag.name);\n lines.push(` ${post.tags.length} tag(s): ${rest.join(\" \")}`);\n\n return lines.join(\"\\n\");\n}\n","/**\n * search_posts: find posts carrying a tag, or a combination of tags.\n *\n * The three tag arguments exist rather than one query string because the site's\n * search language can set a rating and a sort of its own. A caller writing\n * `sort:id:asc` inside a free-form query would contradict the `sort` argument\n * declared beside it, and the answer would report a sort it does not have.\n */\n\nimport { z } from \"zod\";\nimport type { Rule34Client } from \"../rule34/client.js\";\nimport { buildSearchQuery, MAX_TAGS, normalizeSearch, type PostSearch } from \"../rule34/urls.js\";\nimport { choice, strictInput, tagNames, wholeNumber } from \"./arguments.js\";\nimport {\n MAX_PAGE,\n ok,\n postSchema,\n renderPostList,\n TAG_PREVIEW,\n toPostOut,\n toToolError,\n type ToolResult,\n} from \"./shared.js\";\n\nexport const searchPostsDescription = [\n \"Search rule34.xxx for posts carrying a tag, or several tags at once.\",\n \"Tags are written as one token, so a name said as words is joined by underscores:\",\n \"'asuka langley sohryu' and 'ranma 1/2' are single tags, and this tool joins them for you.\",\n \"Returns each post's page, its image or video URL, dimensions, score and rating, alongside the\",\n `number of posts the whole search matches. A row shows the first ${TAG_PREVIEW} of a post's tags and`,\n \"says how many it has; call get_post for the whole list with a kind for every tag.\",\n \"Use 'media_type' to ask for still images, animations or video; the site classifies these itself.\",\n \"When a search finds nothing, the answer names the tags rule34.xxx does not hold, or says how many\",\n \"posts the tags match once the restrictions are dropped.\",\n].join(\" \");\n\nexport const searchPostsInput = strictInput({\n tags: tagNames(\"tags\", 1, MAX_TAGS).describe(\n \"Tags a post must all carry, such as ['asuka langley sohryu', 'black hair'].\",\n ),\n any_of: tagNames(\"any_of\", 1, MAX_TAGS)\n .optional()\n .describe(\"Tags a post must carry at least one of.\"),\n exclude: tagNames(\"exclude\", 1, MAX_TAGS)\n .optional()\n .describe(\"Tags a post must not carry, such as ['monochrome'] to leave out black and white.\"),\n media_type: choice(\n \"media_type\",\n [\"image\", \"animated\", \"video\", \"any\"],\n \"The site states this in its own tags.\",\n )\n .default(\"any\")\n .describe(\n \"'image' for stills, 'animated' for GIFs, 'video' for MP4. The site states this in its own \" +\n \"tags, so the classification is as good as the tagging.\",\n ),\n rating: choice(\n \"rating\",\n [\"questionable\", \"explicit\"],\n \"These are the two ratings rule34.xxx holds; any other answers zero posts and no error, which would \" +\n \"hand back an absence the site never had.\",\n )\n .optional()\n .describe(\"The two ratings rule34.xxx holds. Leave unset to search both.\"),\n sort: choice(\"sort\", [\"score\", \"id\", \"updated\", \"random\"], \"The order the site sorts by.\")\n .default(\"score\")\n .describe(\n \"'score' is the site's own tally of votes, recomputed once a day. 'id' is newest first. \" +\n \"'updated' is by when a post last changed, which retagging moves. 'random' draws a \" +\n \"different set on each call.\",\n ),\n limit: wholeNumber(\"limit\", 1, 100).default(20),\n page: wholeNumber(\"page\", 1, MAX_PAGE).default(1),\n});\n\nexport const searchPostsOutputShape = {\n tags: z.array(z.string()).describe(\"The required tags, as rule34.xxx spells them.\"),\n any_of: z.array(z.string()).describe(\"The alternatives, as rule34.xxx spells them.\"),\n exclude: z.array(z.string()).describe(\"The excluded tags, as rule34.xxx spells them.\"),\n query: z.string().describe(\"The search as it was sent, in the site's own language.\"),\n total: z.number().int().describe(\"Posts the whole search matches, as counted by the site.\"),\n page: z.number().int(),\n posts: z.array(postSchema),\n has_more: z.boolean(),\n next_page: z.number().int().nullable(),\n source: z.literal(\"rule34.xxx\"),\n notes: z.array(z.string()),\n};\n\nexport interface SearchPostsArgs {\n tags: string[];\n any_of?: string[];\n exclude?: string[];\n media_type: \"image\" | \"animated\" | \"video\" | \"any\";\n rating?: \"questionable\" | \"explicit\";\n sort: \"score\" | \"id\" | \"updated\" | \"random\";\n limit: number;\n page: number;\n}\n\nfunction toSearch(args: SearchPostsArgs): PostSearch {\n return {\n tags: args.tags,\n ...(args.any_of ? { anyOf: args.any_of } : {}),\n ...(args.exclude ? { exclude: args.exclude } : {}),\n mediaType: args.media_type,\n ...(args.rating ? { rating: args.rating } : {}),\n sort: args.sort,\n limit: args.limit,\n page: args.page,\n };\n}\n\n/**\n * The last page this search fills at the limit asked for.\n *\n * A search that found nothing fills the one page it was asked on, so that a\n * page beyond the end is told apart from a search with nothing in it.\n */\nfunction lastPageOf(total: number, limit: number): number {\n return total === 0 ? 1 : Math.ceil(total / limit);\n}\n\n/** The next step, when there is one the schema would accept. */\nfunction whatFollowsThisPage(nextPage: number | null): string {\n return nextPage === null ? \"\" : `\\n\\nMore posts match: call again with page=${nextPage}.`;\n}\n\nconst quoted = (tags: string[]): string => tags.map((tag) => `'${tag}'`).join(\", \");\n\n/**\n * The opening of a misspelled name, which is the part worth asking about.\n *\n * The suggestion route matches from the start of a name, so a misspelled name\n * asked in full matches nothing. A slip usually lands late enough that the\n * opening survives it, and the opening is what finds the name that was meant.\n */\nfunction openingOf(name: string): string {\n return name.slice(0, Math.max(3, Math.floor(name.length * 0.6)));\n}\n\n/**\n * Names the site offers that begin like the one it does not hold.\n *\n * A suggestion that cannot be fetched is a suggestion not made: the tag that is\n * missing is already named, and a failure here must not replace that answer.\n */\nasync function namesLike(client: Rule34Client, unknown: string): Promise<string> {\n try {\n const { data } = await client.findTags(openingOf(unknown));\n const offered = data\n .slice(0, 3)\n .map((tag) => `'${tag.name}'${tag.postCount === null ? \"\" : ` (${tag.postCount} posts)`}`);\n return offered.length === 0 ? \"\" : ` It does offer ${offered.join(\", \")}.`;\n } catch {\n return \"\";\n }\n}\n\n/** The arguments that narrow a search, named as a caller passed them. */\nfunction restrictionsIn(args: SearchPostsArgs): string[] {\n const named: string[] = [];\n if ((args.any_of ?? []).length > 0) {\n named.push(\"any_of\");\n }\n if ((args.exclude ?? []).length > 0) {\n named.push(\"exclude\");\n }\n if (args.media_type !== \"any\") {\n named.push(\"media_type\");\n }\n if (args.rating) {\n named.push(\"rating\");\n }\n return named;\n}\n\n/**\n * Why a search found nothing.\n *\n * A name the site never had empties a search exactly the way a combination it\n * holds nothing for does, and the two are indistinguishable in the answer. So\n * every empty search asks the tag route which of its names exist, whether it\n * carried one name or ten.\n *\n * Only the names that can be the cause are looked up. A required tag the site\n * does not hold empties the search on its own, and a group of alternatives does\n * the same when the site holds none of them. An excluded name the site never\n * had removes nothing, so it cannot be why the search is empty, and blaming it\n * would send a caller to fix the one thing that was not the problem.\n */\nasync function explainEmptySearch(client: Rule34Client, args: SearchPostsArgs): Promise<string[]> {\n const alternatives = args.any_of ?? [];\n const unknownRequired = await client.findUnknownTags(args.tags);\n const unknownAlternatives =\n alternatives.length > 0 ? await client.findUnknownTags(alternatives) : [];\n\n const [firstUnknown] = unknownRequired;\n if (firstUnknown !== undefined) {\n const near = await namesLike(client, firstUnknown);\n return [\n `rule34.xxx holds no tag named ${quoted(unknownRequired)}. ` +\n \"A tag a search requires and the site does not hold empties that search on its own.\" +\n near,\n ];\n }\n\n if (alternatives.length > 0 && unknownAlternatives.length === alternatives.length) {\n return [\n `rule34.xxx holds none of the alternatives ${quoted(unknownAlternatives)}, ` +\n \"so no post could carry one of them.\",\n ];\n }\n\n // A restriction that leaves nothing looks exactly like tags the site holds\n // nothing for. Asking the same tags without the restrictions is what tells\n // the two apart, and it names the argument a caller would otherwise guess at.\n const restrictions = restrictionsIn(args);\n if (restrictions.length > 0) {\n const { data } = await client.searchPosts({ tags: args.tags, limit: 1, page: 1 });\n if (data.total > 0) {\n return [\n `The tags alone match ${data.total.toLocaleString(\"en-US\")} post(s). ` +\n `What emptied this search is the restriction on ${restrictions.join(\", \")}.`,\n ];\n }\n }\n\n return [\"Every tag this search requires exists on rule34.xxx; no post carries them together.\"];\n}\n\nexport async function runSearchPosts(\n client: Rule34Client,\n args: SearchPostsArgs,\n): Promise<ToolResult> {\n try {\n const search = toSearch(args);\n const spelled = normalizeSearch(search);\n const query = buildSearchQuery(search);\n const { data, cached } = await client.searchPosts(search);\n\n const notes: string[] = [];\n if (cached) {\n notes.push(\"Served from this server's short-lived in-memory cache.\");\n }\n\n // A row leads with the tags the search asked for, so that a post matching\n // on a name the site lists late still shows why it is here.\n const asked = [...spelled.required, ...spelled.alternatives];\n const posts = data.posts.map((post) => toPostOut(post, asked));\n // A truncated list passed off as whole is the one thing this saving must\n // not cost, so the answer says where the rest of the tags are.\n const trimmed = posts.filter((post) => post.tags_total > post.tags.length).length;\n if (trimmed > 0) {\n notes.push(\n `${trimmed} of these posts carry more tags than a row shows. Call get_post with an id for the ` +\n \"whole list, with a kind and a post count for every tag.\",\n );\n }\n // The site counts every match; this page carries at most `limit` of them.\n const lastPage = lastPageOf(data.total, args.limit);\n const hasMore = args.page < lastPage;\n const nextPage = hasMore && args.page < MAX_PAGE ? args.page + 1 : null;\n\n // Two silences a caller cannot tell apart from a failure. A page beyond the\n // end serves nothing while the same answer states a total, and a search\n // whose results outrun the page ceiling states that more remain while\n // offering no page to ask for.\n if (data.total > 0 && args.page > lastPage) {\n notes.push(\n `This search fills ${lastPage} page(s) at limit ${args.limit}, and page ${args.page} lies ` +\n \"past the end, so no post is served.\",\n );\n }\n if (hasMore && nextPage === null) {\n notes.push(\n `rule34.xxx holds more posts, and page ${MAX_PAGE} is as far as this tool reads. ` +\n \"Raise limit to carry more of them per page, or narrow the search.\",\n );\n }\n\n if (data.total === 0) {\n notes.push(...(await explainEmptySearch(client, args)));\n }\n\n const structured = {\n tags: spelled.required,\n any_of: spelled.alternatives,\n exclude: spelled.excluded,\n query,\n total: data.total,\n page: args.page,\n posts,\n has_more: hasMore,\n next_page: nextPage,\n source: \"rule34.xxx\" as const,\n notes,\n };\n\n const header =\n data.total === 0\n ? `No post on rule34.xxx matches ${query}.`\n : `${data.total.toLocaleString(\"en-US\")} post(s) match ${query}. Page ${args.page}:`;\n const body = posts.length > 0 ? `\\n${renderPostList(posts)}` : \"\";\n\n return ok(structured, `${header}${body}${whatFollowsThisPage(nextPage)}`, notes);\n } catch (error) {\n return toToolError(error);\n }\n}\n"],"mappings":"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","names":["process","z","AUTHENTICATION","readText","readInteger","WHITESPACE","readInteger","readPost","readText","TYPES","readInteger","z","z","z","z","z","z","z","z","z","process"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/config.ts","../src/version.ts","../src/server.ts","../src/errors.ts","../src/rule34/cache.ts","../src/rule34/rateLimiter.ts","../src/rule34/urls.ts","../src/rule34/http.ts","../src/rule34/parsers/postDetail.ts","../src/rule34/parsers/suggestions.ts","../src/rule34/parsers/document.ts","../src/rule34/parsers/posts.ts","../src/rule34/parsers/tags.ts","../src/rule34/client.ts","../src/tools/findTags.ts","../src/tools/arguments.ts","../src/tools/shared.ts","../src/tools/getPost.ts","../src/tools/searchPosts.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * Entry point: an MCP server for rule34.xxx over stdio.\n */\n\nimport process from \"node:process\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { createLogger, loadConfig } from \"./config.js\";\nimport { createServer } from \"./server.js\";\n\nasync function main(): Promise<void> {\n const config = loadConfig();\n const logger = createLogger(config.logLevel);\n const server = createServer({ config, logger });\n\n const transport = new StdioServerTransport();\n await server.connect(transport);\n\n const shutdown = () => {\n // The close is attempted, and a close that fails changes nothing: the\n // process leaves on either path.\n server\n .close()\n .catch(() => undefined)\n .finally(() => process.exit(0));\n };\n process.on(\"SIGINT\", shutdown);\n process.on(\"SIGTERM\", shutdown);\n}\n\nmain().catch((error: unknown) => {\n process.stderr.write(\n `[mcp-rule34] fatal: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}\\n`,\n );\n process.exit(1);\n});\n","/**\n * Runtime configuration, read from environment variables.\n *\n * A bad value never stops the process: a server that dies at startup because of\n * a typo in a client configuration file is very hard to diagnose from the host\n * application, so an invalid value is clamped and reported on stderr. Missing\n * credentials are handled the same way — the server starts, publishes its\n * tools, and each call answers with what to set and where to get it.\n */\n\nimport process from \"node:process\";\nimport type { Credentials } from \"./rule34/urls.js\";\nimport { PKG_VERSION, REPO_URL } from \"./version.js\";\n\nexport type LogLevel = \"silent\" | \"error\" | \"info\" | \"debug\";\n\nexport interface Config {\n userAgent: string;\n credentials: Credentials | null;\n minIntervalMs: number;\n timeoutMs: number;\n maxRetries: number;\n cacheTtlMs: number;\n cacheMaxEntries: number;\n logLevel: LogLevel;\n}\n\n/** Identifies the client honestly, with an address that reaches a person. */\nexport const DEFAULT_USER_AGENT = `mcp-rule34/${PKG_VERSION} (+${REPO_URL})`;\n\nexport const DEFAULTS = {\n minIntervalMs: 1000,\n timeoutMs: 20_000,\n maxRetries: 3,\n cacheTtlMs: 5 * 60 * 1000,\n cacheMaxEntries: 300,\n logLevel: \"error\" as LogLevel,\n};\n\n/**\n * Floor on the request interval, enforced regardless of configuration.\n *\n * rule34.xxx limits its rate without publishing what the rate is: the account\n * page states a limit whose numbers are missing from the page itself, and a\n * handful of requests in a row is enough to draw a 429. Leaving the pacing\n * configurable down to zero would make the politeness of every installation\n * depend on whoever edited a JSON file.\n */\nexport const MIN_ALLOWED_INTERVAL_MS = 1000;\n\nconst LOG_LEVELS: LogLevel[] = [\"silent\", \"error\", \"info\", \"debug\"];\n\ninterface NumericRange {\n min: number;\n max: number;\n fallback: number;\n}\n\nfunction warn(message: string): void {\n process.stderr.write(`[mcp-rule34] ${message}\\n`);\n}\n\nfunction readNumber(name: string, env: NodeJS.ProcessEnv, range: NumericRange): number {\n const raw = env[name];\n if (raw === undefined || raw.trim() === \"\") {\n return range.fallback;\n }\n\n const parsed = Number(raw);\n if (!Number.isFinite(parsed)) {\n warn(`${name}=\"${raw}\" is not a number, using ${range.fallback}`);\n return range.fallback;\n }\n const clamped = Math.min(range.max, Math.max(range.min, Math.round(parsed)));\n if (clamped !== Math.round(parsed)) {\n warn(`${name}=${raw} is out of range, clamped to ${clamped}`);\n }\n return clamped;\n}\n\n/**\n * Read the request interval, refusing anything below the floor.\n *\n * A value under the floor falls back to the default rather than to the floor\n * itself: someone who set 0 was not asking for the floor, they were asking for\n * no pacing at all, and the safe reading of that request is to ignore it.\n */\nfunction readInterval(env: NodeJS.ProcessEnv): number {\n const raw = env.RULE34_MIN_INTERVAL_MS;\n if (raw === undefined || raw.trim() === \"\") {\n return DEFAULTS.minIntervalMs;\n }\n\n const parsed = Number(raw);\n if (!Number.isFinite(parsed)) {\n warn(`RULE34_MIN_INTERVAL_MS=\"${raw}\" is not a number, using ${DEFAULTS.minIntervalMs}ms`);\n return DEFAULTS.minIntervalMs;\n }\n\n const rounded = Math.round(parsed);\n if (rounded < MIN_ALLOWED_INTERVAL_MS) {\n warn(\n `RULE34_MIN_INTERVAL_MS=${raw} is below the ${MIN_ALLOWED_INTERVAL_MS}ms floor and was ignored; ` +\n `using ${DEFAULTS.minIntervalMs}ms. This floor keeps the client from hammering rule34.xxx.`,\n );\n return DEFAULTS.minIntervalMs;\n }\n\n return Math.min(60_000, rounded);\n}\n\n/**\n * The pair the API route requires, or nothing.\n *\n * One half is as useless as none: the site answers a partial pair with its\n * authentication error, which a caller reads as a fault on the site's side.\n */\nexport function readCredentials(env: NodeJS.ProcessEnv): Credentials | null {\n const userId = env.RULE34_USER_ID?.trim() ?? \"\";\n const apiKey = env.RULE34_API_KEY?.trim() ?? \"\";\n if (userId === \"\" || apiKey === \"\") {\n return null;\n }\n return { userId, apiKey };\n}\n\nexport function loadConfig(env: NodeJS.ProcessEnv = process.env): Config {\n const rawUserAgent = env.RULE34_USER_AGENT?.trim();\n const rawLogLevel = env.RULE34_LOG_LEVEL?.trim().toLowerCase();\n\n let logLevel = DEFAULTS.logLevel;\n if (rawLogLevel) {\n if (LOG_LEVELS.includes(rawLogLevel as LogLevel)) {\n logLevel = rawLogLevel as LogLevel;\n } else {\n warn(`RULE34_LOG_LEVEL=\"${rawLogLevel}\" is unknown, using \"${DEFAULTS.logLevel}\"`);\n }\n }\n\n return {\n userAgent: rawUserAgent || DEFAULT_USER_AGENT,\n credentials: readCredentials(env),\n minIntervalMs: readInterval(env),\n timeoutMs: readNumber(\"RULE34_TIMEOUT_MS\", env, {\n min: 1000,\n max: 120_000,\n fallback: DEFAULTS.timeoutMs,\n }),\n maxRetries: readNumber(\"RULE34_MAX_RETRIES\", env, {\n min: 0,\n max: 10,\n fallback: DEFAULTS.maxRetries,\n }),\n cacheTtlMs: readNumber(\"RULE34_CACHE_TTL_MS\", env, {\n min: 0,\n max: 24 * 60 * 60 * 1000,\n fallback: DEFAULTS.cacheTtlMs,\n }),\n cacheMaxEntries: readNumber(\"RULE34_CACHE_MAX_ENTRIES\", env, {\n min: 0,\n max: 10_000,\n fallback: DEFAULTS.cacheMaxEntries,\n }),\n logLevel,\n };\n}\n\nconst LEVEL_RANK: Record<LogLevel, number> = { silent: 0, error: 1, info: 2, debug: 3 };\n\n/**\n * Logs go to stderr without exception. On a stdio transport, stdout carries the\n * protocol and any stray write there corrupts the session.\n */\nexport function createLogger(level: LogLevel) {\n const emit = (at: LogLevel, message: string) => {\n if (LEVEL_RANK[level] >= LEVEL_RANK[at]) {\n process.stderr.write(`[mcp-rule34] ${message}\\n`);\n }\n };\n return {\n error: (message: string) => emit(\"error\", message),\n info: (message: string) => emit(\"info\", message),\n debug: (message: string) => emit(\"debug\", message),\n };\n}\n\nexport type Logger = ReturnType<typeof createLogger>;\n","/** Version and repository, quoted in the User-Agent this client sends. */\n\nexport const PKG_VERSION = \"2.0.2\";\nexport const REPO_URL = \"https://github.com/smeet666/mcp-rule34\";\n","/**\n * MCP server wiring.\n *\n * One client, one rate limiter and one cache serve every tool: per-tool\n * instances would each open their own request stream and defeat the pacing that\n * keeps this server welcome on a site which limits its rate.\n */\n\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { z } from \"zod\";\nimport type { Config, Logger } from \"./config.js\";\nimport { createLogger, loadConfig } from \"./config.js\";\nimport { Rule34Client } from \"./rule34/client.js\";\nimport type { FindTagsArgs } from \"./tools/findTags.js\";\nimport {\n findTagsDescription,\n findTagsInput,\n findTagsOutputShape,\n runFindTags,\n} from \"./tools/findTags.js\";\nimport type { GetPostArgs } from \"./tools/getPost.js\";\nimport {\n getPostDescription,\n getPostInput,\n getPostOutputShape,\n runGetPost,\n} from \"./tools/getPost.js\";\nimport type { SearchPostsArgs } from \"./tools/searchPosts.js\";\nimport {\n runSearchPosts,\n searchPostsDescription,\n searchPostsInput,\n searchPostsOutputShape,\n} from \"./tools/searchPosts.js\";\nimport { PKG_VERSION } from \"./version.js\";\n\nexport interface CreateServerOptions {\n config?: Config;\n logger?: Logger;\n fetchImpl?: typeof fetch;\n}\n\nconst READ_ONLY = {\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: true,\n} as const;\n\nexport function createServer(options: CreateServerOptions = {}): McpServer {\n const config = options.config ?? loadConfig();\n const logger = options.logger ?? createLogger(config.logLevel);\n const client = new Rule34Client({\n config,\n logger,\n ...(options.fetchImpl ? { fetchImpl: options.fetchImpl } : {}),\n });\n\n const server = new McpServer(\n { name: \"mcp-rule34\", version: PKG_VERSION },\n {\n instructions:\n \"Tools for searching rule34.xxx by tag, through the site's own API. This server reads and never \" +\n \"contributes anything back. It needs credentials: set RULE34_USER_ID and RULE34_API_KEY, both shown \" +\n \"under 'API Access Credentials' at https://rule34.xxx/index.php?page=account&s=options once signed in, \" +\n \"and note that the site issues one key per person. Tags are single tokens: a name said as words, such \" +\n \"as 'asuka langley sohryu', is one tag, and the tools join it for you. The site holds two ratings, \" +\n \"'questionable' and 'explicit'; asking for anything else is refused rather than answered with nothing. \" +\n \"Typical flow: search_posts to find posts by tag, then get_post with an id for one post's whole tag \" +\n \"list, since a search row shows only its first few tags. When a name might be spelled otherwise, or \" +\n \"when a search found nothing because a tag does not exist, find_tags says how the site spells it. \" +\n \"A 'rate_limited' error means the site is refusing this client for now, not that a search found \" +\n \"nothing. When you show a post to a user, link its rule34.xxx page.\",\n },\n );\n\n // Registered in the order a caller meets them: a search names the posts, and\n // a post is then read whole.\n server.registerTool(\n \"search_posts\",\n {\n title: \"Search posts by tag\",\n description: searchPostsDescription,\n inputSchema: searchPostsInput,\n outputSchema: z.object(searchPostsOutputShape),\n annotations: READ_ONLY,\n },\n async (args) => runSearchPosts(client, args as SearchPostsArgs),\n );\n\n server.registerTool(\n \"get_post\",\n {\n title: \"Read one post\",\n description: getPostDescription,\n inputSchema: getPostInput,\n outputSchema: z.object(getPostOutputShape),\n annotations: READ_ONLY,\n },\n async (args) => runGetPost(client, args as GetPostArgs),\n );\n\n server.registerTool(\n \"find_tags\",\n {\n title: \"Find how a tag is spelled\",\n description: findTagsDescription,\n inputSchema: findTagsInput,\n outputSchema: z.object(findTagsOutputShape),\n annotations: READ_ONLY,\n },\n async (args) => runFindTags(client, args as FindTagsArgs),\n );\n\n logger.info(\n `ready: user-agent=\"${config.userAgent}\", min interval ${config.minIntervalMs}ms, ` +\n `cache ${config.cacheTtlMs}ms, credentials ${config.credentials ? \"set\" : \"missing\"}`,\n );\n\n return server;\n}\n","/**\n * Error taxonomy surfaced to the calling model.\n *\n * A caller branches on the code that opens the message, so the code has to say\n * where the fault is. The distinction that matters most on this route is\n * between a document this server could not read and an account it was not given\n * the credentials for: the site answers both with HTTP 200, and only one of\n * them is fixed by editing a configuration file.\n */\n\nexport type ErrorCode =\n | \"not_found\"\n | \"invalid_input\"\n | \"rate_limited\"\n | \"parse_failure\"\n | \"network_error\"\n | \"timeout\";\n\nexport interface ErrorDetails {\n url?: string;\n status?: number;\n retryAfterMs?: number;\n hint?: string;\n}\n\nexport class Rule34Error extends Error {\n constructor(\n readonly code: ErrorCode,\n text: string,\n readonly details: ErrorDetails = {},\n options?: ErrorOptions,\n ) {\n // The message a caller reads says what this server could not do. The error\n // that led there is kept as the cause, where a maintainer reading a stack\n // finds it and a caller never has to.\n super(`[${code}] ${text}`, options);\n this.name = \"Rule34Error\";\n }\n}\n\nconst ISSUES_URL = \"https://github.com/smeet666/mcp-rule34/issues\";\n\nexport function invalidInput(message: string, hint?: string, cause?: unknown): Rule34Error {\n return new Rule34Error(\"invalid_input\", message, hint ? { hint } : {}, { cause });\n}\n\n/**\n * The credentials are missing or refused.\n *\n * This is `invalid_input` rather than a failure of the site: the request was\n * well formed and the site answered it, and what is missing lives in the\n * caller's own configuration.\n */\nexport function missingCredentials(url: string): Rule34Error {\n return new Rule34Error(\n \"invalid_input\",\n \"rule34.xxx answered that this request carries no valid API credentials.\",\n {\n url,\n hint:\n \"Set RULE34_USER_ID and RULE34_API_KEY in the env block of this server's entry in your MCP client \" +\n \"configuration. Both are shown under 'API Access Credentials' at \" +\n \"https://rule34.xxx/index.php?page=account&s=options once you are signed in. The site issues one key \" +\n \"per person, so use your own rather than sharing one.\",\n },\n );\n}\n\nexport function notFound(url: string): Rule34Error {\n return new Rule34Error(\"not_found\", \"rule34.xxx has nothing at this address.\", {\n url,\n status: 404,\n hint: \"Check the post id. Ids come from a search; they are not stable to guess by hand.\",\n });\n}\n\n/**\n * The site is being asked too often.\n *\n * This never becomes an empty result. rule34.xxx limits its rate without\n * publishing one, and a refusal rendered as zero posts reads as \"the site holds\n * nothing on this tag\", which is the one thing a caller must not be told here.\n */\nexport function rateLimited(url: string, retryAfterMs: number): Rule34Error {\n return new Rule34Error(\n \"rate_limited\",\n \"rule34.xxx is refusing requests from this client for now (HTTP 429). \" +\n \"This does NOT mean the search found nothing.\",\n {\n url,\n status: 429,\n retryAfterMs,\n hint:\n \"Wait before asking again. If it keeps happening, raise RULE34_MIN_INTERVAL_MS in your MCP client \" +\n \"configuration to slow this server down.\",\n },\n );\n}\n\nexport function parseFailure(url: string, what: string, cause?: unknown): Rule34Error {\n return new Rule34Error(\n \"parse_failure\",\n `The request was answered, but not with the document this route publishes (${what}).`,\n { url, hint: `Please report this, with the query you used, at ${ISSUES_URL}` },\n { cause },\n );\n}\n\n/** The site answered with a failure of its own, in the shape its API documents. */\nexport function upstreamFailure(url: string, message: string): Rule34Error {\n return new Rule34Error(\"network_error\", `rule34.xxx reported a failure: ${message}`, {\n url,\n hint: \"This is the site's own message. Waiting and asking again is usually the only remedy.\",\n });\n}\n","/**\n * In-memory TTL and LRU cache.\n *\n * Nothing is written to disk. This exists so that the same question asked twice\n * inside one conversation does not reach rule34.xxx twice, which matters more\n * here than elsewhere: the site limits its rate without publishing one, and\n * every request spent on a repeat is a request unavailable to a new question.\n *\n * A Map iterates in insertion order, so re-inserting on every hit is enough to\n * make the first key the least recently used one.\n */\n\ninterface Entry<V> {\n value: V;\n expiresAt: number;\n}\n\nexport class TtlLruCache<V> {\n private readonly store = new Map<string, Entry<V>>();\n\n constructor(\n private readonly maxEntries: number,\n private readonly ttlMs: number,\n ) {}\n\n get(key: string): V | undefined {\n const entry = this.store.get(key);\n if (!entry) {\n return undefined;\n }\n if (Date.now() >= entry.expiresAt) {\n this.store.delete(key);\n return undefined;\n }\n this.store.delete(key);\n this.store.set(key, entry);\n return entry.value;\n }\n\n set(key: string, value: V): void {\n if (this.maxEntries <= 0 || this.ttlMs <= 0) {\n return;\n }\n this.store.delete(key);\n this.store.set(key, { value, expiresAt: Date.now() + this.ttlMs });\n while (this.store.size > this.maxEntries) {\n const oldest = this.store.keys().next();\n if (oldest.done) {\n break;\n }\n this.store.delete(oldest.value);\n }\n }\n\n /** The keys currently held, in least recently used order. */\n keys(): string[] {\n return [...this.store.keys()];\n }\n\n get size(): number {\n return this.store.size;\n }\n\n clear(): void {\n this.store.clear();\n }\n}\n","/**\n * Serial request queue with an adaptive minimum interval.\n *\n * rule34.xxx limits its rate without publishing what the rate is, and answers a\n * burst with HTTP 429. Requests therefore run one at a time with a floor on the\n * gap between starts. When the site does push back, the interval doubles and\n * then decays as requests succeed, which recovers faster than a fixed delay and\n * behaves better than asking at a constant rate the site never agreed to.\n */\n\nexport interface RateLimiterOptions {\n minIntervalMs: number;\n maxIntervalMs?: number;\n}\n\nconst DEFAULT_MAX_INTERVAL_MS = 30_000;\n\nexport class RateLimiter {\n private readonly baseIntervalMs: number;\n private readonly maxIntervalMs: number;\n private intervalMs: number;\n /** Tail of the queue: each task chains onto the previous one. */\n private tail: Promise<unknown> = Promise.resolve();\n private lastStart = 0;\n\n constructor(options: RateLimiterOptions) {\n this.baseIntervalMs = Math.max(0, options.minIntervalMs);\n this.maxIntervalMs = Math.max(\n this.baseIntervalMs,\n options.maxIntervalMs ?? DEFAULT_MAX_INTERVAL_MS,\n );\n this.intervalMs = this.baseIntervalMs;\n }\n\n get currentIntervalMs(): number {\n return this.intervalMs;\n }\n\n /** Queue a task. Tasks run in call order, one at a time. */\n schedule<T>(task: () => Promise<T>): Promise<T> {\n const run = this.tail.then(async () => task());\n // The queue must keep draining even when a task rejects.\n this.tail = run.then(\n () => undefined,\n () => undefined,\n );\n return run;\n }\n\n /**\n * Wait for this request's slot, then claim it.\n *\n * Called once per upstream request rather than once per task, because a task\n * runs a whole retry chain: stamping only its start would let the next task\n * follow the chain's last request with no gap at all.\n */\n async beforeRequest(): Promise<void> {\n await this.waitForSlot();\n this.lastStart = Date.now();\n }\n\n /** Called after the site answers that it is being asked too often. */\n penalize(): void {\n const next = this.intervalMs === 0 ? 500 : this.intervalMs * 2;\n this.intervalMs = Math.min(this.maxIntervalMs, next);\n }\n\n /** Called after a success, so one push-back does not slow things down forever. */\n relax(): void {\n this.intervalMs = Math.max(this.baseIntervalMs, Math.floor(this.intervalMs * 0.75));\n }\n\n private async waitForSlot(): Promise<void> {\n if (this.intervalMs === 0 || this.lastStart === 0) {\n return;\n }\n const elapsed = Date.now() - this.lastStart;\n // Clamped to the interval: a clock stepped backwards would otherwise make\n // this wait for the size of the step, and the queue is serial, so every\n // pending request would wait behind it.\n const remaining = Math.min(this.intervalMs, this.intervalMs - elapsed);\n if (remaining > 0) {\n await sleep(remaining);\n }\n }\n}\n\nexport function sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n","/**\n * URL construction for rule34.xxx, and the one place a tag becomes a query.\n *\n * Everything a caller supplies passes through here, which is why the refusals\n * live here too: a tag is one token, and a token that carries a metatag or a\n * negation would set a filter the answer then reports as something else.\n */\n\nimport { invalidInput } from \"../errors.js\";\nimport type { MediaType, Rating, SortOrder } from \"../types.js\";\n\nexport const API_URL = \"https://api.rule34.xxx/index.php\";\nexport const SUGGEST_URL = \"https://api.rule34.xxx/autocomplete.php\";\nexport const SITE_URL = \"https://rule34.xxx/index.php\";\n\n/** The site refuses more than this in one request, and caps silently below it. */\nexport const MAX_LIMIT = 1000;\n\n/**\n * How each media type is expressed in the site's own tags.\n *\n * The site holds the distinction as tags, so it does the filtering and the\n * total it returns counts what was asked for. `animated` covers the GIFs, which\n * carry that tag without carrying `video`.\n */\nconst MEDIA_TOKENS: Record<MediaType, string[]> = {\n image: [\"-video\", \"-animated\"],\n animated: [\"animated\", \"-video\"],\n video: [\"video\"],\n any: [],\n};\n\nconst SORT_TOKENS: Record<SortOrder, string> = {\n score: \"sort:score:desc\",\n id: \"sort:id:desc\",\n updated: \"sort:updated:desc\",\n random: \"sort:random\",\n};\n\nexport interface Credentials {\n apiKey: string;\n userId: string;\n}\n\nexport interface PostSearch {\n /** Tags a post must all carry. */\n tags: string[];\n /** Tags a post must carry at least one of. */\n anyOf?: string[];\n /** Tags a post must not carry. */\n exclude?: string[];\n mediaType?: MediaType;\n rating?: Rating;\n sort?: SortOrder;\n limit: number;\n /** Counted from 1, the way a caller counts. */\n page: number;\n}\n\n/**\n * How many tags one list may hold.\n *\n * The site accepts more, and a search of a dozen tags answers normally. The\n * bound is here because every tag is one more term to diagnose when a search\n * comes back empty, and a request whose address runs to kilobytes is one no\n * error message can usefully quote.\n */\nexport const MAX_TAGS = 10;\n\n/**\n * Write a name the way rule34.xxx spells a tag.\n *\n * The site stores a tag as one token, joining with underscores what a person\n * says as words, so \"asuka langley sohryu\" and \"asuka_langley_sohryu\" are the\n * same request. Punctuation inside the name is part of it: the series tag is\n * `ranma_1/2`, slash included.\n */\nexport function normalizeTag(raw: string): string {\n const trimmed = raw.trim();\n if (trimmed === \"\") {\n throw invalidInput(\n \"A tag is required.\",\n \"Pass the tag to search for, such as 'black hair' or 'asuka langley sohryu'.\",\n );\n }\n\n if (trimmed.startsWith(\"-\")) {\n throw invalidInput(\n `\"${trimmed}\" starts with '-', which means \"without this tag\" on rule34.xxx.`,\n \"This tool searches for a tag. Pass the tag itself, without the leading dash.\",\n );\n }\n\n if (trimmed.includes(\":\")) {\n throw invalidInput(\n `\"${trimmed}\" carries a ':', which rule34.xxx reads as a filter rather than as a tag.`,\n \"Filters are separate arguments here: use 'rating', 'sort' and 'media_type'.\",\n );\n }\n\n return trimmed.replace(/\\s+/g, \"_\").toLowerCase();\n}\n\n/** Normalize a list of tags, refusing a list longer than one search carries. */\nfunction normalizeList(raw: string[] | undefined, argument: string): string[] {\n const tags = (raw ?? []).map(normalizeTag);\n if (tags.length > MAX_TAGS) {\n throw invalidInput(\n `'${argument}' holds ${tags.length} tags, and this tool searches at most ${MAX_TAGS} at a time.`,\n \"Search on the tags that narrow the most, then look through the results.\",\n );\n }\n return tags;\n}\n\nexport interface NormalizedTags {\n required: string[];\n alternatives: string[];\n excluded: string[];\n}\n\n/**\n * The three lists as rule34.xxx spells them.\n *\n * An answer states the tags it searched for, and taking them back out of the\n * query string would state the site's grammar — its braces and its tilde — as\n * though they were tags.\n */\nexport function normalizeSearch(search: PostSearch): NormalizedTags {\n const required = normalizeList(search.tags, \"tags\");\n if (required.length === 0) {\n throw invalidInput(\n \"At least one tag is required.\",\n \"Pass the tags to search for, such as ['asuka langley sohryu', 'black hair'].\",\n );\n }\n\n const alternatives = normalizeList(search.anyOf, \"any_of\");\n const excluded = normalizeList(search.exclude, \"exclude\");\n\n // A tag both required and excluded matches nothing whatever the site holds,\n // and answering that with an empty result reads as an absence on the site's\n // side rather than a contradiction in the question.\n const contradiction = required.find((tag) => excluded.includes(tag));\n if (contradiction) {\n throw invalidInput(\n `'${contradiction}' is both required and excluded, so no post can match.`,\n \"Remove it from one of the two lists.\",\n );\n }\n\n return { required, alternatives, excluded };\n}\n\n/** The value of the site's `tags` parameter: the tags, then the filters. */\nexport function buildSearchQuery(search: PostSearch): string {\n const { required, alternatives, excluded } = normalizeSearch(search);\n\n const tokens = [...required];\n if (alternatives.length === 1) {\n // A group of one is the same request as the tag itself, and the braces\n // would suggest a choice the search does not make.\n tokens.push(...alternatives);\n } else if (alternatives.length > 1) {\n // The braces and the spaces inside them are the site's own grammar for a\n // choice; without them the tilde is read as part of a tag name.\n tokens.push(`( ${alternatives.join(\" ~ \")} )`);\n }\n\n tokens.push(...excluded.map((tag) => `-${tag}`), ...MEDIA_TOKENS[search.mediaType ?? \"any\"]);\n if (search.rating) {\n tokens.push(`rating:${search.rating}`);\n }\n if (search.sort) {\n tokens.push(SORT_TOKENS[search.sort]);\n }\n return tokens.join(\" \");\n}\n\nexport function buildPostSearchUrl(search: PostSearch, credentials: Credentials): string {\n if (!Number.isInteger(search.limit) || search.limit < 1 || search.limit > MAX_LIMIT) {\n throw invalidInput(\n `limit must be a whole number between 1 and ${MAX_LIMIT}, and ${search.limit} is not.`,\n `rule34.xxx serves at most ${MAX_LIMIT} posts in one request.`,\n );\n }\n if (!Number.isInteger(search.page) || search.page < 1) {\n throw invalidInput(`page must be a whole number from 1 upwards, and ${search.page} is not.`);\n }\n\n const query = encodeURIComponent(buildSearchQuery(search));\n // `pid` on this route is a page number the site multiplies by the limit. The\n // same name means an absolute offset on the site's own pages, so the two are\n // reconciled here and nowhere else.\n const pid = search.page - 1;\n\n return (\n `${API_URL}?page=dapi&s=post&q=index` +\n `&tags=${query}&limit=${search.limit}&pid=${pid}` +\n `&api_key=${encodeURIComponent(credentials.apiKey)}` +\n `&user_id=${encodeURIComponent(credentials.userId)}`\n );\n}\n\n/**\n * Ask the tag route whether one exact name exists.\n *\n * A search of several tags that comes back empty says nothing about which tag\n * emptied it, and a misspelled name looks exactly like a combination the site\n * genuinely has nothing for. This is what tells the two apart.\n */\nexport function buildTagLookupUrl(tag: string, credentials: Credentials): string {\n return (\n `${API_URL}?page=dapi&s=tag&q=index&name=${encodeURIComponent(normalizeTag(tag))}` +\n `&api_key=${encodeURIComponent(credentials.apiKey)}` +\n `&user_id=${encodeURIComponent(credentials.userId)}`\n );\n}\n\n/**\n * The id inside a link to a post's page.\n *\n * The URL is read and never fetched: this server builds its own request from\n * the id it finds. Refusing a foreign host is still right, because a link to\n * somewhere else names no post this site holds.\n */\nexport function readPostPageUrl(raw: string): number {\n let parsed: URL;\n try {\n parsed = new URL(raw.trim());\n } catch (error) {\n throw invalidInput(\n `\"${raw}\" is not a URL.`,\n \"Pass a link to a post's page, such as https://rule34.xxx/index.php?page=post&s=view&id=2195419\",\n error,\n );\n }\n\n const host = parsed.hostname.toLowerCase();\n // The site serves its pages on the bare host and its media from subdomains it\n // adds to over time, so the suffix is what stays true.\n if (host !== \"rule34.xxx\" && !host.endsWith(\".rule34.xxx\")) {\n throw invalidInput(\n `\"${raw}\" is not a rule34.xxx address.`,\n \"This tool reads posts on rule34.xxx. A link elsewhere names no post it holds.\",\n );\n }\n\n if (parsed.searchParams.get(\"page\") !== \"post\" || parsed.searchParams.get(\"s\") !== \"view\") {\n throw invalidInput(\n `\"${raw}\" is a rule34.xxx address, but not a post's page.`,\n \"A post's page looks like https://rule34.xxx/index.php?page=post&s=view&id=2195419\",\n );\n }\n\n // A query may carry `id` more than once, and reading the first would be a\n // coin flip on which post the caller meant. Two different values are the\n // contradiction an `id` and a `url` that disagree are refused for.\n const named: string[] = [...new Set(parsed.searchParams.getAll(\"id\"))];\n if (named.length > 1) {\n throw invalidInput(\n `\"${raw}\" names ${named.length} different posts: ${named.join(\", \")}.`,\n \"Pass a link to one post.\",\n );\n }\n\n const written: string = named[0] ?? \"\";\n if (written === \"\") {\n throw invalidInput(`\"${raw}\" carries no post id.`);\n }\n\n const id = Number(written);\n if (!Number.isInteger(id) || id < 1) {\n throw invalidInput(\n `\"${raw}\" carries '${written}', which is not a post id.`,\n \"A post id is a whole number from 1 upwards.\",\n );\n }\n return id;\n}\n\n/** The one post an `id` and a `url` name, refusing a pair that disagrees. */\nexport function resolvePostRef(ref: { id?: number; url?: string }): number {\n const fromUrl = ref.url === undefined ? null : readPostPageUrl(ref.url);\n const fromId = ref.id;\n\n if (fromId !== undefined) {\n if (!Number.isInteger(fromId) || fromId < 1) {\n throw invalidInput(`id must be a whole number from 1 upwards, and ${fromId} is not.`);\n }\n // Preferring one would be a coin flip on which of the caller's two\n // arguments was the mistake, and the answer would carry no trace of it.\n if (fromUrl !== null && fromUrl !== fromId) {\n throw invalidInput(\n `'id' names post ${fromId} and 'url' names post ${fromUrl}, so they cannot both be read.`,\n \"Pass one of the two, or make them agree.\",\n );\n }\n return fromId;\n }\n\n if (fromUrl !== null) {\n return fromUrl;\n }\n\n throw invalidInput(\n \"Either 'id' or 'url' must be given.\",\n \"Pass the numeric post id, or a link to the post's page on rule34.xxx.\",\n );\n}\n\n/**\n * A post is read from two addresses, because neither format carries everything.\n *\n * The XML states the publication date and the uploader's numeric id. The JSON\n * states the uploader's name, the number of comments, and a type and a count\n * for every tag. A fiche that read one of them would leave out what the other\n * one holds.\n */\nexport function buildPostByIdXmlUrl(id: number, credentials: Credentials): string {\n return `${API_URL}?page=dapi&s=post&q=index&id=${id}${credentialsQuery(credentials)}`;\n}\n\nexport function buildPostByIdJsonUrl(id: number, credentials: Credentials): string {\n return `${API_URL}?page=dapi&s=post&q=index&id=${id}&json=1&fields=tag_info${credentialsQuery(credentials)}`;\n}\n\nfunction credentialsQuery(credentials: Credentials): string {\n return (\n `&api_key=${encodeURIComponent(credentials.apiKey)}` +\n `&user_id=${encodeURIComponent(credentials.userId)}`\n );\n}\n\n/**\n * Ask the site which tag names begin with a piece of text.\n *\n * rule34.xxx publishes this route on its API host and answers it without\n * credentials, so none are sent. It matches from the start of a name and caps\n * its answer at ten, both of which the tool that calls it has to state.\n *\n * An empty query is refused here: the site answers one with the tags it holds\n * most of, which is an answer to a question nobody asked.\n */\nexport function buildTagSuggestUrl(text: string): string {\n return `${SUGGEST_URL}?q=${encodeURIComponent(normalizeTag(text))}`;\n}\n\nexport function buildPostPageUrl(id: number): string {\n return `${SITE_URL}?page=post&s=view&id=${id}`;\n}\n\n/**\n * Strip the key out of a URL before anything shows it.\n *\n * An error names the address it failed on, a model reads that text, and the\n * conversation it lands in outlives the request. The user id stays readable:\n * it names the account without opening it, which keeps a support question\n * answerable.\n */\nexport function redactCredentials(url: string): string {\n return url.replace(/([?&]api_key=)[^&]*/gi, \"$1REDACTED\");\n}\n","/**\n * HTTP layer: one GET of the API route, classified, with backoff.\n *\n * Two conditions decide the shape of this file. rule34.xxx refuses a client\n * that asks too often with HTTP 429, and it answers a request carrying no valid\n * credentials with HTTP 200 and an error document. So a status of 200 is not\n * proof of an answer, and a refusal must never reach a caller as an empty\n * result — the document itself is classified further up, by the parser.\n */\n\nimport type { Config, Logger } from \"../config.js\";\nimport { Rule34Error, notFound, rateLimited } from \"../errors.js\";\nimport { type RateLimiter, sleep } from \"./rateLimiter.js\";\nimport { redactCredentials } from \"./urls.js\";\n\nconst BACKOFF_BASE_MS = 2000;\nconst BACKOFF_FACTOR = 2;\nconst BACKOFF_MAX_MS = 30_000;\n\n/** Exponential backoff with jitter, so parallel clients do not resynchronise. */\nexport function backoffDelay(attempt: number, random: () => number = Math.random): number {\n const uncapped = BACKOFF_BASE_MS * BACKOFF_FACTOR ** attempt;\n const capped = Math.min(BACKOFF_MAX_MS, uncapped);\n return Math.round(capped * (0.5 + random() * 0.5));\n}\n\nexport interface HttpDeps {\n config: Config;\n limiter: RateLimiter;\n logger: Logger;\n fetchImpl?: typeof fetch;\n}\n\n/**\n * Fetch one API document, retrying what is worth retrying.\n *\n * The whole retry chain, backoff waits included, runs inside a single limiter\n * slot. Waiting outside it would let a queued request slip into the same\n * refusal window the current one is backing away from.\n */\nexport async function fetchXml(url: string, deps: HttpDeps): Promise<string> {\n const { config, limiter, logger } = deps;\n const doFetch = deps.fetchImpl ?? fetch;\n // Every message and every log line names the address without the key in it.\n const safeUrl = redactCredentials(url);\n\n return await limiter.schedule(async () => {\n let lastError: Rule34Error | undefined;\n\n // Set when the site says how long to stay away, replacing our own guess for\n // the next attempt. Applied here rather than where it is read, so no wait\n // is ever served after the last attempt, when nobody would use it.\n let askedWaitMs: number | null = null;\n\n for (let attempt = 0; attempt <= config.maxRetries; attempt += 1) {\n if (attempt > 0) {\n const delay = Math.min(askedWaitMs ?? backoffDelay(attempt - 1), BACKOFF_MAX_MS);\n askedWaitMs = null;\n logger.info(`retry ${attempt}/${config.maxRetries} in ${delay}ms for ${safeUrl}`);\n await sleep(delay);\n }\n\n let status: number;\n let body: string;\n let retryAfterMs: number | null = null;\n try {\n await limiter.beforeRequest();\n const response = await doFetch(url, {\n headers: {\n \"User-Agent\": config.userAgent,\n Accept: \"application/xml,text/xml;q=0.9,*/*;q=0.8\",\n },\n redirect: \"follow\",\n signal: AbortSignal.timeout(config.timeoutMs),\n });\n status = response.status;\n retryAfterMs = parseRetryAfter(response.headers.get(\"retry-after\"));\n body = await response.text();\n } catch (error) {\n lastError = asTransportError(error, safeUrl);\n logger.debug(`${lastError.code} for ${safeUrl}: ${lastError.message}`);\n continue;\n }\n\n if (status === 429) {\n limiter.penalize();\n logger.info(`rate limited on ${safeUrl}, interval now ${limiter.currentIntervalMs}ms`);\n askedWaitMs = retryAfterMs;\n lastError = rateLimited(safeUrl, retryAfterMs ?? backoffDelay(attempt));\n continue;\n }\n\n // Retrying this would only repeat the same answer.\n if (status === 404) {\n throw notFound(safeUrl);\n }\n\n if (status >= 500) {\n lastError = new Rule34Error(\"network_error\", `rule34.xxx returned HTTP ${status}.`, {\n url: safeUrl,\n status,\n });\n continue;\n }\n\n if (status >= 400) {\n throw new Rule34Error(\"network_error\", `rule34.xxx refused the request (HTTP ${status}).`, {\n url: safeUrl,\n status,\n });\n }\n\n limiter.relax();\n return body;\n }\n\n throw (\n lastError ??\n new Rule34Error(\"network_error\", `Could not fetch ${safeUrl}.`, {\n url: safeUrl,\n })\n );\n });\n}\n\n/** `Retry-After` carries either seconds or an HTTP date. */\nfunction parseRetryAfter(raw: string | null): number | null {\n if (!raw) {\n return null;\n }\n const seconds = Number(raw.trim());\n if (Number.isFinite(seconds) && seconds >= 0) {\n return Math.round(seconds * 1000);\n }\n const when = Date.parse(raw);\n if (Number.isNaN(when)) {\n return null;\n }\n return Math.max(0, when - Date.now());\n}\n\nfunction asTransportError(error: unknown, safeUrl: string): Rule34Error {\n if (error instanceof Rule34Error) {\n return error;\n }\n const name = error instanceof Error ? error.name : \"\";\n if (name === \"TimeoutError\" || name === \"AbortError\") {\n return new Rule34Error(\"timeout\", \"rule34.xxx did not answer in time.\", {\n url: safeUrl,\n hint: \"Raise RULE34_TIMEOUT_MS if this happens often on a slow connection.\",\n });\n }\n const message = error instanceof Error ? error.message : String(error);\n return new Rule34Error(\"network_error\", `Could not reach rule34.xxx: ${message}`, {\n url: safeUrl,\n });\n}\n","/**\n * Reading one post from the JSON route.\n *\n * This format carries three things the XML does not: the uploader by name, the\n * number of comments, and a type and a post count for every tag. It leaves out\n * the publication date, which is why a fiche reads both formats rather than\n * choosing one.\n */\n\nimport { missingCredentials, notFound, parseFailure, upstreamFailure } from \"../../errors.js\";\nimport type { PostDetail, TagOnPost } from \"../../types.js\";\nimport { buildPostPageUrl, redactCredentials } from \"../urls.js\";\n\n/** The words this route writes where the tag route writes a number. */\n/** The word the site puts in the answer it gives an unauthenticated request. */\nconst AUTHENTICATION = /authenticat/i;\nconst WHITESPACE = /\\s+/;\n\nconst TYPES: Record<string, string> = {\n tag: \"general\",\n artist: \"artist\",\n copyright: \"copyright\",\n character: \"character\",\n metadata: \"metadata\",\n};\n\ninterface RawTagInfo {\n tag?: unknown;\n type?: unknown;\n count?: unknown;\n}\n\nexport function parsePostDetail(body: string, url: string, id: number): PostDetail {\n const safeUrl = redactCredentials(url);\n\n // A request naming an id the site does not hold is answered with an empty\n // body, the same zero bytes a truncated read produces. Asking for one post\n // makes the two the same answer: there is no such post to report.\n if (body.trim() === \"\") {\n throw notFound(safeUrl);\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(body);\n } catch (error) {\n throw parseFailure(safeUrl, \"the response is not JSON\", error);\n }\n\n // An unauthenticated request is answered with HTTP 200 and a bare string,\n // which is a fault in the caller's configuration rather than in the site.\n if (typeof parsed === \"string\") {\n if (AUTHENTICATION.test(parsed)) {\n throw missingCredentials(safeUrl);\n }\n throw upstreamFailure(safeUrl, parsed);\n }\n\n if (!Array.isArray(parsed)) {\n throw parseFailure(safeUrl, \"the response is not a list of posts\");\n }\n const [raw] = parsed;\n if (raw === undefined) {\n throw notFound(safeUrl);\n }\n if (raw === null || typeof raw !== \"object\") {\n throw parseFailure(safeUrl, \"the first entry is not a post\");\n }\n\n return readPost(raw as Record<string, unknown>, id);\n}\n\nfunction readPost(raw: Record<string, unknown>, askedFor: number): PostDetail {\n const id = readInteger(raw.id) ?? askedFor;\n const tags = readString(raw.tags)\n .split(WHITESPACE)\n .filter((tag) => tag !== \"\");\n\n return {\n id,\n postUrl: buildPostPageUrl(id),\n fileUrl: readString(raw.file_url),\n sampleUrl: readText(raw.sample_url),\n previewUrl: readText(raw.preview_url),\n width: readInteger(raw.width) ?? 0,\n height: readInteger(raw.height) ?? 0,\n md5: readString(raw.hash),\n score: readInteger(raw.score) ?? 0,\n rating: readString(raw.rating),\n tags,\n tagDetails: readTagDetails(raw.tag_info),\n source: readText(raw.source),\n // This route writes 0 where the XML writes an empty string, and 0 is a post\n // id that belongs to somebody else.\n parentId: nonZero(readInteger(raw.parent_id)),\n owner: readText(raw.owner),\n commentCount: readInteger(raw.comment_count),\n status: readString(raw.status),\n hasNotes: raw.has_notes === true,\n changedAtUnix: readInteger(raw.change),\n };\n}\n\nfunction readTagDetails(value: unknown): TagOnPost[] {\n if (!Array.isArray(value)) {\n return [];\n }\n return value.map((entry) => {\n const info = (entry ?? {}) as RawTagInfo;\n const type = readString(info.type);\n return {\n name: readString(info.tag),\n // A word this site has not documented is carried as it came: naming it\n // something else would state a kind the site never wrote.\n type: TYPES[type] ?? type,\n postCount: readInteger(info.count) ?? 0,\n };\n });\n}\n\nfunction nonZero(value: number | null): number | null {\n return value === null || value === 0 ? null : value;\n}\n\nfunction readString(value: unknown): string {\n return typeof value === \"string\" ? value : \"\";\n}\n\nfunction readText(value: unknown): string | null {\n const trimmed = readString(value).trim();\n return trimmed === \"\" ? null : trimmed;\n}\n\nfunction readInteger(value: unknown): number | null {\n if (typeof value === \"number\") {\n return Number.isFinite(value) ? value : null;\n }\n const trimmed = readString(value).trim();\n if (trimmed === \"\") {\n return null;\n }\n const parsed = Number(trimmed);\n return Number.isFinite(parsed) ? parsed : null;\n}\n","/**\n * Reading the list of names rule34.xxx suggests for a piece of text.\n *\n * The site writes each entry twice over: a `label` it means to display, which\n * carries the post count in brackets, and a `value` a search actually takes.\n * The count is read out of the label because that is the only place the route\n * puts it.\n */\n\nimport { parseFailure } from \"../../errors.js\";\nimport type { TagSuggestion } from \"../../types.js\";\nimport { redactCredentials } from \"../urls.js\";\n\ninterface RawSuggestion {\n label?: unknown;\n value?: unknown;\n}\n\n/** The count the site prints at the end of a label, as in `kimono (55638)`. */\nconst COUNT_IN_LABEL = /\\((\\d+)\\)\\s*$/;\n\nexport function parseTagSuggestions(body: string, url: string): TagSuggestion[] {\n const safeUrl = redactCredentials(url);\n\n if (body.trim() === \"\") {\n throw parseFailure(safeUrl, \"the response was empty\");\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(body);\n } catch (error) {\n throw parseFailure(safeUrl, \"the response is not JSON\", error);\n }\n\n if (!Array.isArray(parsed)) {\n throw parseFailure(safeUrl, \"the response is not a list of suggestions\");\n }\n\n const suggestions: TagSuggestion[] = [];\n for (const entry of parsed) {\n const raw = (entry ?? {}) as RawSuggestion;\n const name = typeof raw.value === \"string\" ? raw.value.trim() : \"\";\n // An entry with no name names no tag, and carrying it forward would offer a\n // suggestion a caller cannot search for.\n if (name === \"\") {\n continue;\n }\n const label = typeof raw.label === \"string\" ? raw.label : \"\";\n const counted = COUNT_IN_LABEL.exec(label);\n suggestions.push({\n name,\n // Zero is a number of posts. A label the site printed no count into\n // leaves the count unknown.\n postCount: counted?.[1] === undefined ? null : Number(counted[1]),\n });\n }\n return suggestions;\n}\n","/**\n * Turning one API answer into a document, and classifying the answers that are\n * not one.\n *\n * Every route of this API answers a request carrying no valid credentials with\n * HTTP 200 and an `<error>` document, so the transport cannot tell that apart\n * from a result. It is told apart here, once, for every route.\n */\n\nimport { XMLParser } from \"fast-xml-parser\";\nimport { missingCredentials, parseFailure, upstreamFailure } from \"../../errors.js\";\n\n/** The word the site puts in the error it answers an unauthenticated request with. */\nconst AUTHENTICATION = /authenticat/i;\n\nexport type Attributes = Record<string, string | undefined>;\n\nconst parser = new XMLParser({\n ignoreAttributes: false,\n attributeNamePrefix: \"@_\",\n // Attribute values are converted one field at a time by each parser, so that\n // an empty `parent_id` stays empty instead of becoming a number.\n parseAttributeValue: false,\n trimValues: true,\n});\n\nexport function readDocument(xml: string, safeUrl: string): Record<string, unknown> {\n if (xml.trim() === \"\") {\n throw parseFailure(safeUrl, \"the response was empty\");\n }\n\n let document: unknown;\n try {\n document = parser.parse(xml);\n } catch (error) {\n throw parseFailure(safeUrl, error instanceof Error ? error.message : \"unreadable XML\");\n }\n\n if (document === null || typeof document !== \"object\") {\n throw parseFailure(safeUrl, \"the response is not an XML document\");\n }\n\n const parsed = document as Record<string, unknown>;\n const failure = parsed.error;\n if (failure !== undefined) {\n const message = typeof failure === \"string\" ? failure : String(failure);\n // A fault in the caller's configuration rather than in the site.\n if (AUTHENTICATION.test(message)) {\n throw missingCredentials(safeUrl);\n }\n throw upstreamFailure(safeUrl, message);\n }\n\n return parsed;\n}\n\n/** A lone element parses to an object, several to an array, none to nothing. */\nexport function toArray(value: unknown): Attributes[] {\n if (value === undefined || value === null) {\n return [];\n }\n return (Array.isArray(value) ? value : [value]) as Attributes[];\n}\n\nexport function readText(value: string | undefined): string | null {\n const trimmed = value?.trim() ?? \"\";\n return trimmed === \"\" ? null : trimmed;\n}\n\nexport function readInteger(value: string | undefined): number | null {\n const trimmed = value?.trim() ?? \"\";\n if (trimmed === \"\") {\n return null;\n }\n const parsed = Number(trimmed);\n return Number.isFinite(parsed) ? parsed : null;\n}\n","/**\n * Reading the `<posts>` document the API answers a search with.\n *\n * This route is read as XML rather than as JSON for one reason: the XML states\n * the total and states an empty result as `count=\"0\"`, where the JSON answers a\n * search that found nothing with an empty body — the same zero bytes a\n * truncated read produces. A total that cannot be read is unknown here, never\n * zero.\n */\n\nimport { parseFailure } from \"../../errors.js\";\nimport type { PostList, Rule34Post } from \"../../types.js\";\nimport { buildPostPageUrl, redactCredentials } from \"../urls.js\";\nimport { type Attributes, readDocument, readInteger, readText, toArray } from \"./document.js\";\n\n/** The letters this route writes where the site writes a word. */\nconst WHITESPACE = /\\s+/;\n\nconst RATINGS: Record<string, string> = {\n e: \"explicit\",\n q: \"questionable\",\n s: \"safe\",\n};\n\nexport function parsePostList(xml: string, url: string): PostList {\n const safeUrl = redactCredentials(url);\n const document = readDocument(xml, safeUrl);\n\n const posts = document.posts;\n if (posts === undefined || posts === null || typeof posts !== \"object\") {\n throw parseFailure(safeUrl, \"no <posts> element\");\n }\n\n const attributes = posts as Attributes & { post?: unknown };\n const total = readInteger(attributes[\"@_count\"]);\n if (total === null) {\n throw parseFailure(safeUrl, \"the <posts> element carries no count\");\n }\n\n return {\n total,\n offset: readInteger(attributes[\"@_offset\"]) ?? 0,\n posts: toArray(attributes.post).map(readPost),\n };\n}\n\nfunction readPost(attributes: Attributes): Rule34Post {\n const id = readInteger(attributes[\"@_id\"]) ?? 0;\n const rating = attributes[\"@_rating\"]?.trim() ?? \"\";\n\n return {\n id,\n postUrl: buildPostPageUrl(id),\n fileUrl: attributes[\"@_file_url\"] ?? \"\",\n sampleUrl: readText(attributes[\"@_sample_url\"]),\n previewUrl: readText(attributes[\"@_preview_url\"]),\n width: readInteger(attributes[\"@_width\"]) ?? 0,\n height: readInteger(attributes[\"@_height\"]) ?? 0,\n md5: attributes[\"@_md5\"] ?? \"\",\n score: readInteger(attributes[\"@_score\"]) ?? 0,\n // An unknown letter is carried as it came: naming it would state a rating\n // the site never wrote.\n rating: RATINGS[rating] ?? rating,\n tags: (attributes[\"@_tags\"] ?? \"\").split(WHITESPACE).filter((tag) => tag !== \"\"),\n source: readText(attributes[\"@_source\"]),\n // The attribute is present and empty on a post with no parent, and 0 is a\n // post id that belongs to somebody else.\n parentId: readInteger(attributes[\"@_parent_id\"]),\n creatorId: readInteger(attributes[\"@_creator_id\"]),\n status: attributes[\"@_status\"] ?? \"\",\n hasChildren: attributes[\"@_has_children\"] === \"true\",\n hasComments: attributes[\"@_has_comments\"] === \"true\",\n hasNotes: attributes[\"@_has_notes\"] === \"true\",\n createdAt: readDate(attributes[\"@_created_at\"]),\n changedAtUnix: readInteger(attributes[\"@_change\"]),\n };\n}\n\n/**\n * The publication date, which this route prints in words rather than as a\n * stamp. A date that cannot be read is unknown: the post's `change` stamp is\n * the date of the last edit, and standing in for the other one would date an\n * old post to whenever somebody last retagged it.\n */\nfunction readDate(value: string | undefined): string | null {\n const trimmed = value?.trim() ?? \"\";\n if (trimmed === \"\") {\n return null;\n }\n const parsed = Date.parse(trimmed);\n return Number.isNaN(parsed) ? null : new Date(parsed).toISOString();\n}\n","/**\n * Reading the `<tags>` document, which says whether a name is a tag at all.\n *\n * This is what a search of several tags needs when it finds nothing: a\n * misspelled name and a combination the site genuinely holds nothing for\n * produce the same empty result, and only this route separates them.\n */\n\nimport { parseFailure } from \"../../errors.js\";\nimport type { TagRef, TagType } from \"../../types.js\";\nimport { redactCredentials } from \"../urls.js\";\nimport { type Attributes, readDocument, readInteger, toArray } from \"./document.js\";\n\n/**\n * How the site numbers the kinds of tag.\n *\n * Read off the site itself rather than assumed: `black_hair` answers 0,\n * `jall_boint` 1, `prince_of_tennis` 3, `asuka_langley_sohryu` 4 and\n * `crossover` 5.\n */\nconst TYPES: Record<number, TagType> = {\n 0: \"general\",\n 1: \"artist\",\n 3: \"copyright\",\n 4: \"character\",\n 5: \"metadata\",\n};\n\nexport function parseTagList(xml: string, url: string): TagRef[] {\n const safeUrl = redactCredentials(url);\n const document = readDocument(xml, safeUrl);\n\n const tags = document.tags;\n if (tags !== undefined && tags !== null && typeof tags === \"object\") {\n return toArray((tags as { tag?: unknown }).tag).map(readTag);\n }\n\n // A name the site holds nothing for comes back under a root element named in\n // the singular and carrying nothing. Reading that as a broken document would\n // turn \"no such tag\" into \"the site is unreadable\", which is the opposite of\n // what this route is asked.\n const lone = document.tag;\n if (lone !== undefined && lone !== null && typeof lone === \"object\") {\n const attributes = lone as Attributes;\n return attributes[\"@_name\"] === undefined ? [] : [readTag(attributes)];\n }\n\n throw parseFailure(safeUrl, \"no <tags> element\");\n}\n\nfunction readTag(attributes: Attributes): TagRef {\n const typeCode = readInteger(attributes[\"@_type\"]);\n return {\n id: readInteger(attributes[\"@_id\"]) ?? 0,\n name: attributes[\"@_name\"] ?? \"\",\n postCount: readInteger(attributes[\"@_count\"]) ?? 0,\n // A number this site has not documented is left unnamed: calling it\n // something would state a kind the site never wrote.\n type: (typeCode === null ? undefined : TYPES[typeCode]) ?? \"unknown\",\n typeCode,\n ambiguous: attributes[\"@_ambiguous\"] === \"true\",\n };\n}\n","/**\n * The reading layer: requests, pacing, cache, and the shapes they produce.\n *\n * Nothing here imports the MCP SDK, so this file is also what the package\n * publishes under its `./client` entry point: a program can read rule34.xxx\n * with the same pacing and the same error taxonomy without a protocol attached.\n */\n\nimport type { Config, Logger } from \"../config.js\";\nimport { invalidInput } from \"../errors.js\";\nimport type { PostDetail, PostList, TagRef, TagSuggestion } from \"../types.js\";\nimport { TtlLruCache } from \"./cache.js\";\nimport { fetchXml } from \"./http.js\";\nimport { parsePostDetail } from \"./parsers/postDetail.js\";\nimport { parseTagSuggestions } from \"./parsers/suggestions.js\";\nimport { parsePostList } from \"./parsers/posts.js\";\nimport { parseTagList } from \"./parsers/tags.js\";\nimport { RateLimiter } from \"./rateLimiter.js\";\nimport {\n buildPostByIdJsonUrl,\n buildPostByIdXmlUrl,\n buildPostSearchUrl,\n buildTagLookupUrl,\n buildTagSuggestUrl,\n normalizeTag,\n type Credentials,\n type PostSearch,\n redactCredentials,\n resolvePostRef,\n} from \"./urls.js\";\n\n/** Every read says whether it reached the site or answered from memory. */\nexport interface Read<T> {\n data: T;\n cached: boolean;\n}\n\nexport interface Rule34ClientDeps {\n config: Config;\n logger: Logger;\n fetchImpl?: typeof fetch;\n}\n\nexport class Rule34Client {\n private readonly limiter: RateLimiter;\n private readonly cache: TtlLruCache<string>;\n\n constructor(private readonly deps: Rule34ClientDeps) {\n this.limiter = new RateLimiter({ minIntervalMs: deps.config.minIntervalMs });\n this.cache = new TtlLruCache(deps.config.cacheMaxEntries, deps.config.cacheTtlMs);\n }\n\n /** The addresses currently held, for a diagnostic to look at. */\n cacheKeys(): string[] {\n return this.cache.keys();\n }\n\n async searchPosts(search: PostSearch): Promise<Read<PostList>> {\n const url = buildPostSearchUrl(search, this.credentials());\n const { body, cached } = await this.read(url);\n return { data: parsePostList(body, url), cached };\n }\n\n /**\n * One post, read from both formats the API publishes.\n *\n * Neither format carries everything: the publication date and the uploader's\n * numeric id come from the XML, and the uploader's name, the comment count\n * and the tag types come from the JSON. Reading one of them would leave out\n * what the other holds, so a fiche costs two reads of the site.\n */\n async getPost(ref: { id?: number; url?: string }): Promise<Read<PostDetail>> {\n const credentials = this.credentials();\n const id = resolvePostRef(ref);\n\n const jsonUrl = buildPostByIdJsonUrl(id, credentials);\n const json = await this.read(jsonUrl);\n // Reading the JSON first means a post the site no longer holds is reported\n // as absent before a second request is spent on it.\n const detail = parsePostDetail(json.body, jsonUrl, id);\n\n const xmlUrl = buildPostByIdXmlUrl(id, credentials);\n const xml = await this.read(xmlUrl);\n const [fromXml] = parsePostList(xml.body, xmlUrl).posts;\n\n return {\n data: {\n ...detail,\n createdAt: fromXml?.createdAt ?? null,\n creatorId: fromXml?.creatorId ?? null,\n hasComments: fromXml?.hasComments ?? (detail.commentCount ?? 0) > 0,\n hasChildren: fromXml?.hasChildren ?? false,\n },\n cached: json.cached && xml.cached,\n };\n }\n\n /**\n * The names rule34.xxx offers for a piece of text.\n *\n * This route needs no credentials, so none are required to call it: a server\n * with no key configured can still tell a caller how a tag is spelled, which\n * is the one useful thing it can do.\n */\n async findTags(text: string): Promise<Read<TagSuggestion[]>> {\n const url = buildTagSuggestUrl(text);\n const { body, cached } = await this.read(url);\n return { data: parseTagSuggestions(body, url), cached };\n }\n\n /** The tag under this exact name, or nothing when the site holds no such tag. */\n async lookupTag(name: string): Promise<Read<TagRef | null>> {\n const url = buildTagLookupUrl(name, this.credentials());\n const { body, cached } = await this.read(url);\n const [tag] = parseTagList(body, url);\n return { data: tag ?? null, cached };\n }\n\n /**\n * Which of these names the site does not hold as a tag.\n *\n * A search of several tags that finds nothing says nothing about which tag\n * emptied it, and a misspelled name is indistinguishable from a combination\n * the site genuinely has nothing for. Each distinct name costs one request,\n * which is why this is worth spending only once a search has come back empty.\n */\n async findUnknownTags(names: string[]): Promise<string[]> {\n const distinct = [...new Set(names.map(normalizeTag))];\n const unknown: string[] = [];\n for (const name of distinct) {\n const { data } = await this.lookupTag(name);\n if (data === null) {\n unknown.push(name);\n }\n }\n return unknown;\n }\n\n private credentials(): Credentials {\n const credentials = this.deps.config.credentials;\n if (!credentials) {\n throw invalidInput(\n \"This server has no rule34.xxx API credentials, so it cannot ask the site anything.\",\n \"Set RULE34_USER_ID and RULE34_API_KEY in the env block of this server's entry in your MCP client \" +\n \"configuration. Both are shown under 'API Access Credentials' at \" +\n \"https://rule34.xxx/index.php?page=account&s=options once you are signed in. The site issues one \" +\n \"key per person, so use your own rather than sharing one.\",\n );\n }\n return credentials;\n }\n\n /**\n * Fetch one address, remembering the answer under it.\n *\n * The key is the address without the key in it: two searches differ by their\n * query and never by their credentials, and holding the secret in a second\n * place buys nothing.\n */\n private async read(url: string): Promise<{ body: string; cached: boolean }> {\n const key = redactCredentials(url);\n const remembered = this.cache.get(key);\n if (remembered !== undefined) {\n this.deps.logger.debug(`cache hit for ${key}`);\n return { body: remembered, cached: true };\n }\n\n const body = await fetchXml(url, {\n config: this.deps.config,\n limiter: this.limiter,\n logger: this.deps.logger,\n ...(this.deps.fetchImpl ? { fetchImpl: this.deps.fetchImpl } : {}),\n });\n this.cache.set(key, body);\n return { body, cached: false };\n }\n}\n","/**\n * find_tags: ask rule34.xxx how a tag is spelled.\n *\n * A search of a name the site does not hold ends there, and a caller with no\n * way to look a name up has no next step. This is that step, and it reads the\n * one route the site publishes for it.\n */\n\nimport { z } from \"zod\";\nimport type { Rule34Client } from \"../rule34/client.js\";\nimport { normalizeTag } from \"../rule34/urls.js\";\nimport { strictInput, tagName } from \"./arguments.js\";\nimport { ok, toToolError, type ToolResult } from \"./shared.js\";\n\n/**\n * The most names the site ever offers.\n *\n * It answers ten whatever it holds, so ten back means the list was cut rather\n * than exhausted, and an answer that kept quiet about it would pass a cap off\n * as a complete list.\n */\nexport const SUGGEST_CAP = 10;\n\nexport const findTagsDescription = [\n \"Find how rule34.xxx spells a tag, and how many posts carry it.\",\n \"The site matches from the start of a name, so 'kimagure' finds 'kimagure_orange_road'\",\n \"while 'orange road' finds nothing. It offers at most ten names.\",\n \"Use this before searching when a name might be spelled differently, and after a search that found\",\n \"nothing because a tag does not exist.\",\n].join(\" \");\n\nexport const findTagsInput = strictInput({\n query: tagName(\"query\", 60).describe(\n \"The start of a tag name, such as 'kimagure' or 'prince of tennis'. Spaces are joined with underscores.\",\n ),\n});\n\nexport const findTagsOutputShape = {\n query: z.string().describe(\"The text as rule34.xxx would spell it.\"),\n tags: z.array(\n z.object({\n name: z.string().describe(\"The name a search takes.\"),\n post_count: z\n .number()\n .int()\n .nullable()\n .describe(\"Posts the site credits to it, or nothing when it printed no count.\"),\n }),\n ),\n source: z.literal(\"rule34.xxx\"),\n notes: z.array(z.string()),\n};\n\nexport interface FindTagsArgs {\n query: string;\n}\n\nexport async function runFindTags(client: Rule34Client, args: FindTagsArgs): Promise<ToolResult> {\n try {\n const { data, cached } = await client.findTags(args.query);\n const notes = cached ? [\"Served from this server's short-lived in-memory cache.\"] : [];\n\n if (data.length === 0) {\n notes.push(\n \"rule34.xxx offers no name for this text. It matches from the start of a name, so a word \" +\n \"sitting inside a tag finds nothing here; try the words a name begins with.\",\n );\n } else if (data.length >= SUGGEST_CAP) {\n notes.push(\n \"rule34.xxx caps this list at ten names, and it returned ten, so more may begin the same \" +\n \"way. Type more of the name to narrow it.\",\n );\n }\n\n // The site was asked for the text spelled its way, and that spelling is the\n // one a caller has to search with.\n const asked = normalizeTag(args.query);\n\n const structured = {\n query: asked,\n tags: data.map((tag) => ({ name: tag.name, post_count: tag.postCount })),\n source: \"rule34.xxx\" as const,\n notes,\n };\n\n const body =\n data.length === 0\n ? `rule34.xxx offers no tag name beginning with ${asked}.`\n : `${data.length} name(s) rule34.xxx offers for ${asked}:\\n${data\n .map(\n (tag) => ` ${tag.name}${tag.postCount === null ? \"\" : ` (${tag.postCount} posts)`}`,\n )\n .join(\"\\n\")}`;\n\n return ok(structured, body, notes);\n } catch (error) {\n return toToolError(error);\n }\n}\n","/**\n * How a tool's arguments are declared, and what happens to one that is not.\n *\n * An argument this server does not declare is a question it cannot answer.\n * Reading it and dropping it produces an answer computed on the defaults, which\n * a caller reads as the answer to what they asked. So an undeclared argument is\n * refused, and the refusal names it and offers the declared name when one is\n * close enough to be the one that was meant.\n */\n\nimport { z } from \"zod\";\n\n/** The code a caller branches on when the arguments cannot produce a request. */\nconst INVALID_INPUT = \"invalid_input\";\nconst CODE = `[${INVALID_INPUT}]`;\n\n/**\n * Why each builder below stamps its own message.\n *\n * A bound such as `min` raises its issue from the check that carries it, and\n * the issue names that check as the instance whose error map applies. A map set\n * on the schema alone therefore leaves the bounds refusing in zod's own words,\n * with no code for a caller to branch on. So the message is written onto the\n * check itself.\n */\nexport function choice<const Options extends readonly [string, ...string[]]>(\n argument: string,\n options: Options,\n what: string,\n): z.ZodEnum<{ [K in Options[number]]: K }> {\n const error = `${CODE} ${argument} takes one of ${options.join(\", \")}. ${what}`;\n return z.enum(options, { error });\n}\n\nexport function wholeNumber(argument: string, min: number, max: number): z.ZodNumber {\n const error = `${CODE} ${argument} takes a whole number from ${min} to ${max}.`;\n return z.number({ error }).int(error).min(min, error).max(max, error);\n}\n\nexport function tagName(argument: string, most: number): z.ZodString {\n // Two causes, two messages: a name written empty and a name written long are\n // different mistakes, and one message for both sends half the callers\n // counting characters they never wrote.\n const empty = `${CODE} ${argument} takes a tag name. Written empty, spaces alone included, it narrows nothing.`;\n const long = `${CODE} ${argument} takes a tag name of at most ${most} characters.`;\n return z.string({ error: empty }).trim().min(1, empty).max(most, long);\n}\n\nexport function tagNames(argument: string, least: number, most: number): z.ZodArray<z.ZodString> {\n const error = `${CODE} ${argument} takes between ${least} and ${most} tag names.`;\n return z.array(tagName(argument, 120), { error }).min(least, error).max(most, error);\n}\n\n/** Declare a tool's arguments, refusing anything outside the declaration. */\nexport function strictInput<Shape extends z.ZodRawShape>(shape: Shape) {\n const declared = Object.keys(shape);\n\n return z.strictObject(shape, {\n error: (issue) =>\n issue.code === \"unrecognized_keys\" ? unknownArgumentMessage(issue.keys, declared) : undefined,\n });\n}\n\nfunction unknownArgumentMessage(keys: readonly string[], declared: readonly string[]): string {\n const named = keys\n .map((key) => {\n const near = nearestArgument(key, declared);\n return near ? `'${key}' (did you mean '${near}'?)` : `'${key}'`;\n })\n .join(\", \");\n\n return (\n `[${INVALID_INPUT}] Unknown ${keys.length > 1 ? \"arguments\" : \"argument\"} ${named}. ` +\n `This tool takes: ${declared.join(\", \")}.`\n );\n}\n\n/**\n * The declared name a caller most plausibly meant, when there is one.\n *\n * Three readings, ordered by how much each claims: the same name written\n * differently, a name that opens or closes the other, and a name a couple of\n * typing slips away. Anything further is left unnamed, because a suggestion\n * that misses sends a caller to an argument answering a different question.\n */\nfunction nearestArgument(key: string, declared: readonly string[]): string | undefined {\n const flatten = (name: string) => name.toLowerCase().replace(/[^a-z0-9]/g, \"\");\n const flat = flatten(key);\n if (flat.length === 0) {\n return undefined;\n }\n\n const sameName = declared.find((name) => flatten(name) === flat);\n if (sameName) {\n return sameName;\n }\n\n // Either name may be the longer one: a caller can qualify a name this tool\n // keeps plain, or shorten one it spells out.\n const overlapping = declared.find((name) => {\n const other = flatten(name);\n const [shorter, longer] = other.length < flat.length ? [other, flat] : [flat, other];\n // Two characters in common say nothing; three start to.\n return shorter.length >= 3 && (longer.startsWith(shorter) || longer.endsWith(shorter));\n });\n if (overlapping) {\n return overlapping;\n }\n\n let closest: string | undefined;\n let shortest = Number.POSITIVE_INFINITY;\n for (const name of declared) {\n const distance = editDistance(flat, flatten(name));\n if (distance < shortest) {\n shortest = distance;\n closest = name;\n }\n }\n\n // Up to a third of the name may differ. Past that the match is a guess.\n return shortest <= Math.max(1, Math.floor(flat.length / 3)) ? closest : undefined;\n}\n\n/**\n * Single-character insertions, deletions and substitutions between two words.\n *\n * One row of the matrix is kept at a time. The cell above comes from walking\n * that row, and the two other neighbours a cell needs, the one to its left and\n * the one diagonally above it, are carried forward from the step before, so\n * every value the walk uses is one it has just computed.\n */\nfunction editDistance(left: string, right: string): number {\n let previous = Array.from({ length: right.length + 1 }, (_, index) => index);\n let distance = right.length;\n\n for (let row = 1; row <= left.length; row += 1) {\n const current: number[] = [row];\n let diagonal = row - 1;\n let leftward = row;\n\n for (const [column, above] of previous.entries()) {\n if (column === 0) {\n continue;\n }\n const substitution = diagonal + (left[row - 1] === right[column - 1] ? 0 : 1);\n const cell = Math.min(substitution, above + 1, leftward + 1);\n current.push(cell);\n diagonal = above;\n leftward = cell;\n }\n\n previous = current;\n distance = leftward;\n }\n\n return distance;\n}\n","/**\n * Pieces the tool layer shares: the result envelope, error rendering, and the\n * text mirror of a structured answer.\n */\n\nimport { z } from \"zod\";\nimport { Rule34Error } from \"../errors.js\";\nimport type { Rule34Post } from \"../types.js\";\n\n/** Many MCP clients render only the text block, so it must stand on its own. */\nexport const MAX_TEXT_MIRROR_CHARS = 2000;\n\n/**\n * The last page a search will accept.\n *\n * A sentence may only name a page the schema would accept, or it sends a caller\n * into an argument that is refused.\n */\nexport const MAX_PAGE = 200;\n\n/**\n * How many of a post's tags a search row shows.\n *\n * Posts carry a median of about two hundred tags and some carry nine hundred,\n * so a page of twenty would spend four fifths of its answer on tag strings. A\n * row shows an extract and states the real count beside it, and get_post reads\n * the whole list when a caller wants it.\n */\nexport const TAG_PREVIEW = 12;\n\nexport const postSchema = z.object({\n id: z.number().int(),\n post_url: z.string().describe(\"The rule34.xxx page for this post.\"),\n file_url: z.string().describe(\"The image or video itself, on the site's CDN.\"),\n preview_url: z.string().nullable().describe(\"Thumbnail, when the site published one.\"),\n sample_url: z.string().nullable().describe(\"Reduced copy, when the site published one.\"),\n width: z.number().int(),\n height: z.number().int(),\n score: z.number().int().describe(\"The site's own score, updated once a day.\"),\n rating: z.string().describe(\"As the site holds it: 'explicit' or 'questionable'.\"),\n tags: z.array(z.string()).describe(\"The first tags of the post, at most 12 of them.\"),\n tags_total: z.number().int().describe(\"How many tags the post carries in all.\"),\n source: z.string().nullable().describe(\"Whatever the uploader credited, when they credited any.\"),\n created_at: z\n .string()\n .nullable()\n .describe(\n \"When the post was added to rule34.xxx, ISO 8601. The site imported much of its older catalogue in bulk, so thousands of posts share one day.\",\n ),\n has_comments: z.boolean(),\n parent_id: z.number().int().nullable(),\n});\n\nexport type PostOut = z.infer<typeof postSchema>;\n\n/**\n * The tags a row shows: the ones the search asked for first, then the rest in\n * the site's own order.\n *\n * A row that omits the very tag it matched on reads as an off-topic result, and\n * the site lists a post's tags alphabetically often enough that a searched name\n * falls outside the first twelve. Only names the post carries are lifted: a row\n * states what the post holds, never what was asked for.\n */\nfunction previewTags(tags: string[], asked: readonly string[]): string[] {\n const carried = asked.filter((tag) => tags.includes(tag));\n const rest = tags.filter((tag) => !carried.includes(tag));\n return [...carried, ...rest].slice(0, TAG_PREVIEW);\n}\n\nexport function toPostOut(post: Rule34Post, asked: readonly string[] = []): PostOut {\n return {\n id: post.id,\n post_url: post.postUrl,\n file_url: post.fileUrl,\n preview_url: post.previewUrl,\n sample_url: post.sampleUrl,\n width: post.width,\n height: post.height,\n score: post.score,\n rating: post.rating,\n tags: previewTags(post.tags, asked),\n tags_total: post.tags.length,\n source: post.source,\n created_at: post.createdAt,\n has_comments: post.hasComments,\n parent_id: post.parentId,\n };\n}\n\nexport interface ToolResult {\n // The SDK's CallToolResult carries an index signature for protocol extensions.\n [key: string]: unknown;\n content: Array<{ type: \"text\"; text: string }>;\n structuredContent?: Record<string, unknown>;\n isError?: boolean;\n}\n\n/**\n * Keep text from the site out of the shape this server's own lines take.\n *\n * A block ending in lines that open \"Note:\" gives a caller no way to tell one\n * of those from the same words inside a tag somebody else published. Indenting\n * such a line in the body keeps the two apart, and the structured payload still\n * carries the text exactly as it was published.\n */\nfunction indentMarkerLines(body: string): string {\n return body.replace(/^(Note:)/gm, \" $1\");\n}\n\n/**\n * Build a result whose text block ends with its notes.\n *\n * The notes are what qualifies an answer: that a tag in the search does not\n * exist, that the page asked for lies past the end, that the words came from\n * this server's cache. A client rendering only the text reads an unqualified\n * answer without them.\n */\nexport function ok(\n structured: Record<string, unknown>,\n text: string,\n notes: string[] = [],\n): ToolResult {\n const trailer = notes.map((note) => `Note: ${note}`).join(\"\\n\");\n const budget = MAX_TEXT_MIRROR_CHARS - (trailer ? trailer.length + 2 : 0);\n const body = truncate(indentMarkerLines(text), Math.max(0, budget));\n\n return {\n content: [{ type: \"text\", text: trailer ? `${body}\\n\\n${trailer}` : body }],\n structuredContent: structured,\n };\n}\n\n/**\n * Error results carry no structuredContent: the SDK validates it against the\n * tool's declared output schema, which an error payload does not satisfy.\n */\nexport function toToolError(error: unknown): ToolResult {\n const known =\n error instanceof Rule34Error\n ? error\n : new Rule34Error(\"network_error\", error instanceof Error ? error.message : String(error));\n\n const lines = [known.message];\n if (known.details.hint) {\n lines.push(`Hint: ${known.details.hint}`);\n }\n if (known.details.url) {\n lines.push(`URL: ${known.details.url}`);\n }\n\n return { content: [{ type: \"text\", text: lines.join(\"\\n\") }], isError: true };\n}\n\nexport function truncate(text: string, maxChars: number): string {\n if (text.length <= maxChars) {\n return text;\n }\n return `${text.slice(0, maxChars - 1).trimEnd()}…`;\n}\n\n/** Compact listing, one post to a line and a half. */\nexport function renderPostList(posts: PostOut[]): string {\n return posts\n .map((post, index) => {\n const head =\n `${index + 1}. #${post.id} · ${post.width}×${post.height} · score ${post.score}` +\n ` · ${post.rating}`;\n return `${head}\\n ${post.post_url}\\n ${post.file_url}`;\n })\n .join(\"\\n\");\n}\n","/**\n * get_post: read one post whole.\n *\n * A search row shows an extract of a post's tags, because a page of twenty\n * would otherwise spend four fifths of its answer on tag strings. This is where\n * the whole list lives, with a kind and a post count for every tag, alongside\n * the uploader's name and the number of comments.\n */\n\nimport { z } from \"zod\";\nimport type { Rule34Client } from \"../rule34/client.js\";\nimport { strictInput, wholeNumber } from \"./arguments.js\";\nimport { ok, toToolError, type ToolResult } from \"./shared.js\";\n\nexport const getPostDescription = [\n \"Read one rule34.xxx post, named by its id or by a link to its page.\",\n \"Returns every tag the post carries, each with its kind (character, copyright, artist, general or\",\n \"metadata) and how many posts share it, alongside the image or video, the dimensions, the score,\",\n \"the rating, the uploader, the number of comments, the credited source and the date the site\",\n \"took the post in.\",\n \"Use this after a search, whose rows show only the first few tags of each post.\",\n].join(\" \");\n\nexport const getPostInput = strictInput({\n id: wholeNumber(\"id\", 1, Number.MAX_SAFE_INTEGER)\n .optional()\n .describe(\"The post's numeric id, as a search returns it.\"),\n url: z\n .string()\n .max(500)\n .optional()\n .describe(\n \"A link to the post's page, such as https://rule34.xxx/index.php?page=post&s=view&id=2195419. \" +\n \"The link is read rather than followed.\",\n ),\n});\n\nexport const getPostOutputShape = {\n id: z.number().int(),\n post_url: z.string(),\n file_url: z.string(),\n preview_url: z.string().nullable(),\n sample_url: z.string().nullable(),\n width: z.number().int(),\n height: z.number().int(),\n md5: z.string(),\n score: z.number().int().describe(\"The site's own score, updated once a day.\"),\n rating: z.string(),\n tags: z.array(\n z.object({\n name: z.string(),\n type: z.string().describe(\"character, copyright, artist, general or metadata.\"),\n post_count: z.number().int().describe(\"Posts the site credits to this tag.\"),\n }),\n ),\n source: z.string().nullable(),\n parent_id: z.number().int().nullable(),\n owner: z.string().nullable().describe(\"The account that uploaded the post.\"),\n comment_count: z.number().int().nullable(),\n created_at: z\n .string()\n .nullable()\n .describe(\n \"When the post was added to rule34.xxx, ISO 8601. The site imported much of its older catalogue in bulk, so thousands of posts share one day.\",\n ),\n changed_at: z.string().nullable().describe(\"When the post last changed, ISO 8601.\"),\n status: z.string(),\n has_notes: z.boolean(),\n source_site: z.literal(\"rule34.xxx\"),\n notes: z.array(z.string()),\n};\n\nexport interface GetPostArgs {\n id?: number;\n url?: string;\n}\n\nexport async function runGetPost(client: Rule34Client, args: GetPostArgs): Promise<ToolResult> {\n try {\n const { data, cached } = await client.getPost(args);\n const notes = cached ? [\"Served from this server's short-lived in-memory cache.\"] : [];\n\n const structured = {\n id: data.id,\n post_url: data.postUrl,\n file_url: data.fileUrl,\n preview_url: data.previewUrl,\n sample_url: data.sampleUrl,\n width: data.width,\n height: data.height,\n md5: data.md5,\n score: data.score,\n rating: data.rating,\n tags: data.tagDetails.map((tag) => ({\n name: tag.name,\n type: tag.type,\n post_count: tag.postCount,\n })),\n source: data.source,\n parent_id: data.parentId,\n owner: data.owner,\n comment_count: data.commentCount,\n created_at: data.createdAt ?? null,\n changed_at: toIso(data.changedAtUnix),\n status: data.status,\n has_notes: data.hasNotes,\n source_site: \"rule34.xxx\" as const,\n notes,\n };\n\n return ok(structured, render(structured), notes);\n } catch (error) {\n return toToolError(error);\n }\n}\n\nfunction toIso(unixSeconds: number | null): string | null {\n return unixSeconds === null ? null : new Date(unixSeconds * 1000).toISOString();\n}\n\n/** The kinds worth naming first: they say what the post is of. */\nconst NAMED_FIRST = [\"character\", \"copyright\", \"artist\"];\n\nfunction render(post: {\n id: number;\n post_url: string;\n file_url: string;\n width: number;\n height: number;\n score: number;\n rating: string;\n owner: string | null;\n created_at: string | null;\n comment_count: number | null;\n tags: Array<{ name: string; type: string; post_count: number }>;\n}): string {\n const lines = [\n `Post #${post.id} · ${post.width}×${post.height} · score ${post.score} · ${post.rating}`,\n ` ${post.post_url}`,\n ` ${post.file_url}`,\n ];\n\n const added = post.created_at ? post.created_at.slice(0, 10) : \"an unstated date\";\n lines.push(` added to rule34.xxx ${added}${post.owner ? ` by ${post.owner}` : \"\"}`);\n if (post.comment_count !== null) {\n lines.push(` ${post.comment_count} comment(s)`);\n }\n\n for (const kind of NAMED_FIRST) {\n const named = post.tags.filter((tag) => tag.type === kind).map((tag) => tag.name);\n if (named.length > 0) {\n lines.push(` ${kind}: ${named.join(\", \")}`);\n }\n }\n const rest = post.tags.filter((tag) => !NAMED_FIRST.includes(tag.type)).map((tag) => tag.name);\n lines.push(` ${post.tags.length} tag(s): ${rest.join(\" \")}`);\n\n return lines.join(\"\\n\");\n}\n","/**\n * search_posts: find posts carrying a tag, or a combination of tags.\n *\n * The three tag arguments exist rather than one query string because the site's\n * search language can set a rating and a sort of its own. A caller writing\n * `sort:id:asc` inside a free-form query would contradict the `sort` argument\n * declared beside it, and the answer would report a sort it does not have.\n */\n\nimport { z } from \"zod\";\nimport type { Rule34Client } from \"../rule34/client.js\";\nimport { buildSearchQuery, MAX_TAGS, normalizeSearch, type PostSearch } from \"../rule34/urls.js\";\nimport { choice, strictInput, tagNames, wholeNumber } from \"./arguments.js\";\nimport {\n MAX_PAGE,\n ok,\n postSchema,\n renderPostList,\n TAG_PREVIEW,\n toPostOut,\n toToolError,\n type ToolResult,\n} from \"./shared.js\";\n\nexport const searchPostsDescription = [\n \"Search rule34.xxx for posts carrying a tag, or several tags at once.\",\n \"Tags are written as one token, so a name said as words is joined by underscores:\",\n \"'asuka langley sohryu' and 'ranma 1/2' are single tags, and this tool joins them for you.\",\n \"Returns each post's page, its image or video URL, dimensions, score and rating, alongside the\",\n `number of posts the whole search matches. A row shows the first ${TAG_PREVIEW} of a post's tags and`,\n \"says how many it has; call get_post for the whole list with a kind for every tag.\",\n \"Use 'media_type' to ask for still images, animations or video; the site classifies these itself.\",\n \"When a search finds nothing, the answer names the tags rule34.xxx does not hold, or says how many\",\n \"posts the tags match once the restrictions are dropped.\",\n].join(\" \");\n\nexport const searchPostsInput = strictInput({\n tags: tagNames(\"tags\", 1, MAX_TAGS).describe(\n \"Tags a post must all carry, such as ['asuka langley sohryu', 'black hair'].\",\n ),\n any_of: tagNames(\"any_of\", 1, MAX_TAGS)\n .optional()\n .describe(\"Tags a post must carry at least one of.\"),\n exclude: tagNames(\"exclude\", 1, MAX_TAGS)\n .optional()\n .describe(\"Tags a post must not carry, such as ['monochrome'] to leave out black and white.\"),\n media_type: choice(\n \"media_type\",\n [\"image\", \"animated\", \"video\", \"any\"],\n \"The site states this in its own tags.\",\n )\n .default(\"any\")\n .describe(\n \"'image' for stills, 'animated' for GIFs, 'video' for MP4. The site states this in its own \" +\n \"tags, so the classification is as good as the tagging.\",\n ),\n rating: choice(\n \"rating\",\n [\"questionable\", \"explicit\"],\n \"These are the two ratings rule34.xxx holds; any other answers zero posts and no error, which would \" +\n \"hand back an absence the site never had.\",\n )\n .optional()\n .describe(\"The two ratings rule34.xxx holds. Leave unset to search both.\"),\n sort: choice(\"sort\", [\"score\", \"id\", \"updated\", \"random\"], \"The order the site sorts by.\")\n .default(\"score\")\n .describe(\n \"'score' is the site's own tally of votes, recomputed once a day. 'id' is newest first. \" +\n \"'updated' is by when a post last changed, which retagging moves. 'random' draws a \" +\n \"different set on each call.\",\n ),\n limit: wholeNumber(\"limit\", 1, 100).default(20),\n page: wholeNumber(\"page\", 1, MAX_PAGE).default(1),\n});\n\nexport const searchPostsOutputShape = {\n tags: z.array(z.string()).describe(\"The required tags, as rule34.xxx spells them.\"),\n any_of: z.array(z.string()).describe(\"The alternatives, as rule34.xxx spells them.\"),\n exclude: z.array(z.string()).describe(\"The excluded tags, as rule34.xxx spells them.\"),\n query: z.string().describe(\"The search as it was sent, in the site's own language.\"),\n total: z.number().int().describe(\"Posts the whole search matches, as counted by the site.\"),\n page: z.number().int(),\n posts: z.array(postSchema),\n has_more: z.boolean(),\n next_page: z.number().int().nullable(),\n source: z.literal(\"rule34.xxx\"),\n notes: z.array(z.string()),\n};\n\nexport interface SearchPostsArgs {\n tags: string[];\n any_of?: string[];\n exclude?: string[];\n media_type: \"image\" | \"animated\" | \"video\" | \"any\";\n rating?: \"questionable\" | \"explicit\";\n sort: \"score\" | \"id\" | \"updated\" | \"random\";\n limit: number;\n page: number;\n}\n\nfunction toSearch(args: SearchPostsArgs): PostSearch {\n return {\n tags: args.tags,\n ...(args.any_of ? { anyOf: args.any_of } : {}),\n ...(args.exclude ? { exclude: args.exclude } : {}),\n mediaType: args.media_type,\n ...(args.rating ? { rating: args.rating } : {}),\n sort: args.sort,\n limit: args.limit,\n page: args.page,\n };\n}\n\n/**\n * The last page this search fills at the limit asked for.\n *\n * A search that found nothing fills the one page it was asked on, so that a\n * page beyond the end is told apart from a search with nothing in it.\n */\nfunction lastPageOf(total: number, limit: number): number {\n return total === 0 ? 1 : Math.ceil(total / limit);\n}\n\n/** The next step, when there is one the schema would accept. */\nfunction whatFollowsThisPage(nextPage: number | null): string {\n return nextPage === null ? \"\" : `\\n\\nMore posts match: call again with page=${nextPage}.`;\n}\n\nconst quoted = (tags: string[]): string => tags.map((tag) => `'${tag}'`).join(\", \");\n\n/**\n * The opening of a misspelled name, which is the part worth asking about.\n *\n * The suggestion route matches from the start of a name, so a misspelled name\n * asked in full matches nothing. A slip usually lands late enough that the\n * opening survives it, and the opening is what finds the name that was meant.\n */\nfunction openingOf(name: string): string {\n return name.slice(0, Math.max(3, Math.floor(name.length * 0.6)));\n}\n\n/**\n * Names the site offers that begin like the one it does not hold.\n *\n * A suggestion that cannot be fetched is a suggestion not made: the tag that is\n * missing is already named, and a failure here must not replace that answer.\n */\nasync function namesLike(client: Rule34Client, unknown: string): Promise<string> {\n try {\n const { data } = await client.findTags(openingOf(unknown));\n const offered = data\n .slice(0, 3)\n .map((tag) => `'${tag.name}'${tag.postCount === null ? \"\" : ` (${tag.postCount} posts)`}`);\n return offered.length === 0 ? \"\" : ` It does offer ${offered.join(\", \")}.`;\n } catch {\n return \"\";\n }\n}\n\n/** The arguments that narrow a search, named as a caller passed them. */\nfunction restrictionsIn(args: SearchPostsArgs): string[] {\n const named: string[] = [];\n if ((args.any_of ?? []).length > 0) {\n named.push(\"any_of\");\n }\n if ((args.exclude ?? []).length > 0) {\n named.push(\"exclude\");\n }\n if (args.media_type !== \"any\") {\n named.push(\"media_type\");\n }\n if (args.rating) {\n named.push(\"rating\");\n }\n return named;\n}\n\n/**\n * Why a search found nothing.\n *\n * A name the site never had empties a search exactly the way a combination it\n * holds nothing for does, and the two are indistinguishable in the answer. So\n * every empty search asks the tag route which of its names exist, whether it\n * carried one name or ten.\n *\n * Only the names that can be the cause are looked up. A required tag the site\n * does not hold empties the search on its own, and a group of alternatives does\n * the same when the site holds none of them. An excluded name the site never\n * had removes nothing, so it cannot be why the search is empty, and blaming it\n * would send a caller to fix the one thing that was not the problem.\n */\nasync function explainEmptySearch(client: Rule34Client, args: SearchPostsArgs): Promise<string[]> {\n const alternatives = args.any_of ?? [];\n const unknownRequired = await client.findUnknownTags(args.tags);\n const unknownAlternatives =\n alternatives.length > 0 ? await client.findUnknownTags(alternatives) : [];\n\n const [firstUnknown] = unknownRequired;\n if (firstUnknown !== undefined) {\n const near = await namesLike(client, firstUnknown);\n return [\n `rule34.xxx holds no tag named ${quoted(unknownRequired)}. ` +\n \"A tag a search requires and the site does not hold empties that search on its own.\" +\n near,\n ];\n }\n\n if (alternatives.length > 0 && unknownAlternatives.length === alternatives.length) {\n return [\n `rule34.xxx holds none of the alternatives ${quoted(unknownAlternatives)}, ` +\n \"so no post could carry one of them.\",\n ];\n }\n\n // A restriction that leaves nothing looks exactly like tags the site holds\n // nothing for. Asking the same tags without the restrictions is what tells\n // the two apart, and it names the argument a caller would otherwise guess at.\n const restrictions = restrictionsIn(args);\n if (restrictions.length > 0) {\n const { data } = await client.searchPosts({ tags: args.tags, limit: 1, page: 1 });\n if (data.total > 0) {\n return [\n `The tags alone match ${data.total.toLocaleString(\"en-US\")} post(s). ` +\n `What emptied this search is the restriction on ${restrictions.join(\", \")}.`,\n ];\n }\n }\n\n return [\"Every tag this search requires exists on rule34.xxx; no post carries them together.\"];\n}\n\nexport async function runSearchPosts(\n client: Rule34Client,\n args: SearchPostsArgs,\n): Promise<ToolResult> {\n try {\n const search = toSearch(args);\n const spelled = normalizeSearch(search);\n const query = buildSearchQuery(search);\n const { data, cached } = await client.searchPosts(search);\n\n const notes: string[] = [];\n if (cached) {\n notes.push(\"Served from this server's short-lived in-memory cache.\");\n }\n\n // A row leads with the tags the search asked for, so that a post matching\n // on a name the site lists late still shows why it is here.\n const asked = [...spelled.required, ...spelled.alternatives];\n const posts = data.posts.map((post) => toPostOut(post, asked));\n // A truncated list passed off as whole is the one thing this saving must\n // not cost, so the answer says where the rest of the tags are.\n const trimmed = posts.filter((post) => post.tags_total > post.tags.length).length;\n if (trimmed > 0) {\n notes.push(\n `${trimmed} of these posts carry more tags than a row shows. Call get_post with an id for the ` +\n \"whole list, with a kind and a post count for every tag.\",\n );\n }\n // The site counts every match; this page carries at most `limit` of them.\n const lastPage = lastPageOf(data.total, args.limit);\n const hasMore = args.page < lastPage;\n const nextPage = hasMore && args.page < MAX_PAGE ? args.page + 1 : null;\n\n // Two silences a caller cannot tell apart from a failure. A page beyond the\n // end serves nothing while the same answer states a total, and a search\n // whose results outrun the page ceiling states that more remain while\n // offering no page to ask for.\n if (data.total > 0 && args.page > lastPage) {\n notes.push(\n `This search fills ${lastPage} page(s) at limit ${args.limit}, and page ${args.page} lies ` +\n \"past the end, so no post is served.\",\n );\n }\n if (hasMore && nextPage === null) {\n notes.push(\n `rule34.xxx holds more posts, and page ${MAX_PAGE} is as far as this tool reads. ` +\n \"Raise limit to carry more of them per page, or narrow the search.\",\n );\n }\n\n if (data.total === 0) {\n notes.push(...(await explainEmptySearch(client, args)));\n }\n\n const structured = {\n tags: spelled.required,\n any_of: spelled.alternatives,\n exclude: spelled.excluded,\n query,\n total: data.total,\n page: args.page,\n posts,\n has_more: hasMore,\n next_page: nextPage,\n source: \"rule34.xxx\" as const,\n notes,\n };\n\n const header =\n data.total === 0\n ? `No post on rule34.xxx matches ${query}.`\n : `${data.total.toLocaleString(\"en-US\")} post(s) match ${query}. Page ${args.page}:`;\n const body = posts.length > 0 ? `\\n${renderPostList(posts)}` : \"\";\n\n return ok(structured, `${header}${body}${whatFollowsThisPage(nextPage)}`, notes);\n } catch (error) {\n return toToolError(error);\n }\n}\n"],"mappings":"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","names":["process","z","AUTHENTICATION","readText","readInteger","WHITESPACE","readInteger","readPost","readText","TYPES","readInteger","z","z","z","z","z","z","z","z","z","process"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-rule34",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.2",
|
|
4
4
|
"description": "MCP server for rule34.xxx. Search posts by tag through the site's own API.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -56,7 +56,8 @@
|
|
|
56
56
|
"check": "biome ci && prettier --check \"**/*.{md,yml}\"",
|
|
57
57
|
"prepublishOnly": "npm run typecheck && npm run test && npm run build",
|
|
58
58
|
"start": "node dist/index.js",
|
|
59
|
-
"inspector": "npx @modelcontextprotocol/inspector node dist/index.js"
|
|
59
|
+
"inspector": "npx @modelcontextprotocol/inspector node dist/index.js",
|
|
60
|
+
"audit:floors": "node scripts/audit-floors.mjs"
|
|
60
61
|
},
|
|
61
62
|
"keywords": [
|
|
62
63
|
"mcp",
|
|
@@ -75,16 +76,16 @@
|
|
|
75
76
|
],
|
|
76
77
|
"dependencies": {
|
|
77
78
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
78
|
-
"fast-xml-parser": "^5.
|
|
79
|
+
"fast-xml-parser": "^5.11.1",
|
|
79
80
|
"zod": "^4.4.3"
|
|
80
81
|
},
|
|
81
82
|
"devDependencies": {
|
|
82
83
|
"@biomejs/biome": "2.5.10",
|
|
83
|
-
"@types/node": "
|
|
84
|
-
"@vitest/coverage-v8": "
|
|
84
|
+
"@types/node": "24.13.3",
|
|
85
|
+
"@vitest/coverage-v8": "4.1.11",
|
|
85
86
|
"prettier": "3.9.6",
|
|
86
|
-
"tsup": "
|
|
87
|
-
"typescript": "
|
|
88
|
-
"vitest": "
|
|
87
|
+
"tsup": "8.5.1",
|
|
88
|
+
"typescript": "5.9.3",
|
|
89
|
+
"vitest": "4.1.11"
|
|
89
90
|
}
|
|
90
91
|
}
|
package/server.json
CHANGED
|
@@ -19,12 +19,12 @@
|
|
|
19
19
|
"url": "https://github.com/smeet666/mcp-rule34",
|
|
20
20
|
"source": "github"
|
|
21
21
|
},
|
|
22
|
-
"version": "2.0.
|
|
22
|
+
"version": "2.0.2",
|
|
23
23
|
"packages": [
|
|
24
24
|
{
|
|
25
25
|
"registryType": "npm",
|
|
26
26
|
"identifier": "mcp-rule34",
|
|
27
|
-
"version": "2.0.
|
|
27
|
+
"version": "2.0.2",
|
|
28
28
|
"transport": {
|
|
29
29
|
"type": "stdio"
|
|
30
30
|
},
|
|
@@ -54,8 +54,8 @@
|
|
|
54
54
|
},
|
|
55
55
|
{
|
|
56
56
|
"registryType": "mcpb",
|
|
57
|
-
"identifier": "https://github.com/smeet666/mcp-rule34/releases/download/v2.0.
|
|
58
|
-
"version": "2.0.
|
|
57
|
+
"identifier": "https://github.com/smeet666/mcp-rule34/releases/download/v2.0.2/mcp-rule34-2.0.2.mcpb",
|
|
58
|
+
"version": "2.0.2",
|
|
59
59
|
"transport": {
|
|
60
60
|
"type": "stdio"
|
|
61
61
|
}
|