mcp-rule34 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/version.ts","../src/config.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/document.ts","../src/rule34/parsers/posts.ts","../src/rule34/parsers/tags.ts","../src/rule34/client.ts","../src/tools/getPost.ts","../src/tools/arguments.ts","../src/tools/shared.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 { 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 void server.close().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","/** Version and repository, quoted in the User-Agent this client sends. */\n\nexport const PKG_VERSION = \"0.1.0\";\nexport const REPO_URL = \"https://github.com/smeet666/mcp-rule34\";\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 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","/**\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 { 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. \" +\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 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 ) {\n super(`[${code}] ${text}`);\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): Rule34Error {\n return new Rule34Error(\"invalid_input\", message, hint ? { hint } : {});\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): 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 );\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 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 {\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 );\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 const id = Number(parsed.searchParams.get(\"id\"));\n if (!Number.isInteger(id) || id < 1) {\n throw invalidInput(`\"${raw}\" carries no post id.`);\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\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. */\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 {\n throw parseFailure(safeUrl, \"the response is not JSON\");\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 (/authenticat/i.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(/\\s+/)\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 * 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\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 (/authenticat/i.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 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(/\\s+/).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 } from \"../types.js\";\nimport { TtlLruCache } from \"./cache.js\";\nimport { fetchXml } from \"./http.js\";\nimport { parsePostDetail } from \"./parsers/postDetail.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 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 /** 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 fetchImpl: this.deps.fetchImpl,\n });\n this.cache.set(key, body);\n return { body, cached: false };\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 publication date.\",\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.string().nullable().describe(\"When the post was published, ISO 8601.\"),\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 posted = post.created_at ? post.created_at.slice(0, 10) : \"an unstated date\";\n lines.push(` posted ${posted}${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 * 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 const error = `${CODE} ${argument} takes a tag name, at most ${most} characters. Written empty it narrows nothing.`;\n return z.string({ error }).trim().min(1, error).max(most, error);\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.string().nullable().describe(\"When the post was published, ISO 8601.\"),\n has_comments: z.boolean(),\n parent_id: z.number().int().nullable(),\n});\n\nexport type PostOut = z.infer<typeof postSchema>;\n\nexport function toPostOut(post: Rule34Post): 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: post.tags.slice(0, TAG_PREVIEW),\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 * 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(\"'score' is the site's own, updated once a day. 'id' is newest first.\"),\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 anyOf: args.any_of,\n exclude: args.exclude,\n mediaType: args.media_type,\n rating: args.rating,\n sort: args.sort,\n limit: args.limit,\n page: args.page,\n };\n}\n\n/**\n * What to say about the posts past this page.\n *\n * The sentence may only name a page the schema would accept, or it sends a\n * caller into an argument that is refused.\n */\nfunction whatFollowsThisPage(hasMore: boolean, page: number): string {\n if (!hasMore) {\n return \"\";\n }\n if (page < MAX_PAGE) {\n return `\\n\\nMore posts match: call again with page=${page + 1}.`;\n }\n return `\\n\\nrule34.xxx holds more, and page ${MAX_PAGE} is as far as this tool reads. Narrow the search instead.`;\n}\n\nconst quoted = (tags: string[]): string => tags.map((tag) => `'${tag}'`).join(\", \");\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 if (unknownRequired.length > 0) {\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 ];\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 const posts = data.posts.map(toPostOut);\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 hasMore = args.page * args.limit < data.total;\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: hasMore && args.page < MAX_PAGE ? args.page + 1 : null,\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(hasMore, args.page)}`, notes);\n } catch (error) {\n return toToolError(error);\n }\n}\n"],"mappings":"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","names":["z","readText","readInteger","readInteger","readPost","readText","TYPES","readInteger","z","z","z","z","z","z","z"]}
@@ -0,0 +1,221 @@
1
+ /** Domain types shared by the reading layer and the MCP tools. */
2
+ /** What a caller may ask for, in the site's own vocabulary. */
3
+ type MediaType = "image" | "animated" | "video" | "any";
4
+ /**
5
+ * The two ratings rule34.xxx actually holds.
6
+ *
7
+ * The site's search accepts `safe`, `general` and `sensitive` and answers each
8
+ * with zero posts and no error, so a caller filtering on one of them reads an
9
+ * absence that the site never had. The vocabulary is closed here for that
10
+ * reason. A post is still read with whatever letter it carries.
11
+ */
12
+ type Rating = "questionable" | "explicit";
13
+ type SortOrder = "score" | "id" | "updated" | "random";
14
+ interface Rule34Post {
15
+ id: number;
16
+ /** The page a person can open, as opposed to the API route that served it. */
17
+ postUrl: string;
18
+ fileUrl: string;
19
+ sampleUrl: string | null;
20
+ previewUrl: string | null;
21
+ width: number;
22
+ height: number;
23
+ md5: string;
24
+ score: number;
25
+ /** `explicit`, `questionable` or `safe`, or the letter itself when unknown. */
26
+ rating: string;
27
+ tags: string[];
28
+ /** Whatever the uploader credited, which may hold several addresses. */
29
+ source: string | null;
30
+ parentId: number | null;
31
+ creatorId: number | null;
32
+ status: string;
33
+ hasChildren: boolean;
34
+ hasComments: boolean;
35
+ hasNotes: boolean;
36
+ /** When the post was published, as the site printed it, in ISO 8601. */
37
+ createdAt: string | null;
38
+ /** Last change to the post, in Unix seconds. Retagging moves this, not the date above. */
39
+ changedAtUnix: number | null;
40
+ }
41
+ interface PostList {
42
+ /** Posts the search matches across every page, as counted by the site. */
43
+ total: number;
44
+ offset: number;
45
+ posts: Rule34Post[];
46
+ }
47
+ /** The kinds of tag the site numbers, named. */
48
+ type TagType = "general" | "artist" | "copyright" | "character" | "metadata" | "unknown";
49
+ interface TagRef {
50
+ id: number;
51
+ name: string;
52
+ /** Posts the site's tag table credits to this tag. */
53
+ postCount: number;
54
+ type: TagType;
55
+ /** The number the site used, kept whether or not it has a name here. */
56
+ typeCode: number | null;
57
+ ambiguous: boolean;
58
+ }
59
+ interface TagOnPost {
60
+ name: string;
61
+ /** The kind the site published, or the word it used when that kind is new. */
62
+ type: string;
63
+ /** Posts the site credits to this tag. */
64
+ postCount: number;
65
+ }
66
+ /**
67
+ * One post, read from both formats the API publishes.
68
+ *
69
+ * The publication date and the uploader's numeric id come from the XML; the
70
+ * uploader's name, the comment count and the tag types come from the JSON.
71
+ */
72
+ interface PostDetail {
73
+ id: number;
74
+ postUrl: string;
75
+ fileUrl: string;
76
+ sampleUrl: string | null;
77
+ previewUrl: string | null;
78
+ width: number;
79
+ height: number;
80
+ md5: string;
81
+ score: number;
82
+ rating: string;
83
+ tags: string[];
84
+ tagDetails: TagOnPost[];
85
+ source: string | null;
86
+ parentId: number | null;
87
+ /** The uploader's name, which only the JSON route publishes. */
88
+ owner: string | null;
89
+ /** Comments on the post, which only the JSON route counts. */
90
+ commentCount: number | null;
91
+ status: string;
92
+ hasNotes: boolean;
93
+ changedAtUnix: number | null;
94
+ /** Only the XML route publishes this, so it is absent until both are read. */
95
+ createdAt?: string | null;
96
+ /** Only the XML route publishes this. */
97
+ creatorId?: number | null;
98
+ hasComments?: boolean;
99
+ hasChildren?: boolean;
100
+ }
101
+
102
+ /**
103
+ * URL construction for rule34.xxx, and the one place a tag becomes a query.
104
+ *
105
+ * Everything a caller supplies passes through here, which is why the refusals
106
+ * live here too: a tag is one token, and a token that carries a metatag or a
107
+ * negation would set a filter the answer then reports as something else.
108
+ */
109
+
110
+ interface Credentials {
111
+ apiKey: string;
112
+ userId: string;
113
+ }
114
+ interface PostSearch {
115
+ /** Tags a post must all carry. */
116
+ tags: string[];
117
+ /** Tags a post must carry at least one of. */
118
+ anyOf?: string[];
119
+ /** Tags a post must not carry. */
120
+ exclude?: string[];
121
+ mediaType?: MediaType;
122
+ rating?: Rating;
123
+ sort?: SortOrder;
124
+ limit: number;
125
+ /** Counted from 1, the way a caller counts. */
126
+ page: number;
127
+ }
128
+
129
+ /**
130
+ * Runtime configuration, read from environment variables.
131
+ *
132
+ * A bad value never stops the process: a server that dies at startup because of
133
+ * a typo in a client configuration file is very hard to diagnose from the host
134
+ * application, so an invalid value is clamped and reported on stderr. Missing
135
+ * credentials are handled the same way — the server starts, publishes its
136
+ * tools, and each call answers with what to set and where to get it.
137
+ */
138
+
139
+ type LogLevel = "silent" | "error" | "info" | "debug";
140
+ interface Config {
141
+ userAgent: string;
142
+ credentials: Credentials | null;
143
+ minIntervalMs: number;
144
+ timeoutMs: number;
145
+ maxRetries: number;
146
+ cacheTtlMs: number;
147
+ cacheMaxEntries: number;
148
+ logLevel: LogLevel;
149
+ }
150
+ /**
151
+ * Logs go to stderr without exception. On a stdio transport, stdout carries the
152
+ * protocol and any stray write there corrupts the session.
153
+ */
154
+ declare function createLogger(level: LogLevel): {
155
+ error: (message: string) => void;
156
+ info: (message: string) => void;
157
+ debug: (message: string) => void;
158
+ };
159
+ type Logger = ReturnType<typeof createLogger>;
160
+
161
+ /**
162
+ * The reading layer: requests, pacing, cache, and the shapes they produce.
163
+ *
164
+ * Nothing here imports the MCP SDK, so this file is also what the package
165
+ * publishes under its `./client` entry point: a program can read rule34.xxx
166
+ * with the same pacing and the same error taxonomy without a protocol attached.
167
+ */
168
+
169
+ /** Every read says whether it reached the site or answered from memory. */
170
+ interface Read<T> {
171
+ data: T;
172
+ cached: boolean;
173
+ }
174
+ interface Rule34ClientDeps {
175
+ config: Config;
176
+ logger: Logger;
177
+ fetchImpl?: typeof fetch;
178
+ }
179
+ declare class Rule34Client {
180
+ private readonly deps;
181
+ private readonly limiter;
182
+ private readonly cache;
183
+ constructor(deps: Rule34ClientDeps);
184
+ /** The addresses currently held, for a diagnostic to look at. */
185
+ cacheKeys(): string[];
186
+ searchPosts(search: PostSearch): Promise<Read<PostList>>;
187
+ /**
188
+ * One post, read from both formats the API publishes.
189
+ *
190
+ * Neither format carries everything: the publication date and the uploader's
191
+ * numeric id come from the XML, and the uploader's name, the comment count
192
+ * and the tag types come from the JSON. Reading one of them would leave out
193
+ * what the other holds, so a fiche costs two reads of the site.
194
+ */
195
+ getPost(ref: {
196
+ id?: number;
197
+ url?: string;
198
+ }): Promise<Read<PostDetail>>;
199
+ /** The tag under this exact name, or nothing when the site holds no such tag. */
200
+ lookupTag(name: string): Promise<Read<TagRef | null>>;
201
+ /**
202
+ * Which of these names the site does not hold as a tag.
203
+ *
204
+ * A search of several tags that finds nothing says nothing about which tag
205
+ * emptied it, and a misspelled name is indistinguishable from a combination
206
+ * the site genuinely has nothing for. Each distinct name costs one request,
207
+ * which is why this is worth spending only once a search has come back empty.
208
+ */
209
+ findUnknownTags(names: string[]): Promise<string[]>;
210
+ private credentials;
211
+ /**
212
+ * Fetch one address, remembering the answer under it.
213
+ *
214
+ * The key is the address without the key in it: two searches differ by their
215
+ * query and never by their credentials, and holding the secret in a second
216
+ * place buys nothing.
217
+ */
218
+ private read;
219
+ }
220
+
221
+ export { type Read, Rule34Client, type Rule34ClientDeps };