mcp-metacritic 1.0.3 → 1.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.
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/mc/client.js +1 -1
- package/dist/mc/client.js.map +1 -1
- package/package.json +1 -1
- package/server.json +11 -2
package/dist/index.js
CHANGED
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/version.ts","../src/config.ts","../src/server.ts","../src/mc/cache.ts","../src/errors.ts","../src/mc/rateLimiter.ts","../src/mc/http.ts","../src/mc/paths.ts","../src/mc/urls.ts","../src/mc/parse.ts","../src/mc/client.ts","../src/tools/browseTitles.ts","../src/tools/shared.ts","../src/tools/getReviews.ts","../src/tools/getTitle.ts","../src/tools/searchTitles.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * Entry point: an MCP server for Metacritic 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-metacritic] fatal: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}\\n`,\n );\n process.exit(1);\n});\n","/**\n * Kept in sync with package.json and server.json. The User-Agent embeds it, so\n * a caller reading their logs can tell which build reached them.\n */\nexport const PKG_VERSION = \"1.0.3\";\nexport const REPO_URL = \"https://github.com/smeet666/mcp-metacritic\";\n","/**\n * Runtime configuration, read from environment variables.\n *\n * A bad value never crashes the process: an MCP server that dies at startup\n * because of a typo in a client config file is very hard to diagnose from the\n * host application, so invalid input is reported on stderr and ignored.\n */\n\nimport { PKG_VERSION, REPO_URL } from \"./version.js\";\n\nexport type LogLevel = \"silent\" | \"error\" | \"info\" | \"debug\";\n\nexport interface Config {\n userAgent: string;\n minIntervalMs: number;\n timeoutMs: number;\n maxRetries: number;\n cacheTtlMs: number;\n scoresCacheTtlMs: number;\n cacheMaxEntries: number;\n logLevel: LogLevel;\n}\n\nexport const DEFAULT_USER_AGENT = `mcp-metacritic v${PKG_VERSION} (${REPO_URL})`;\n\nexport const DEFAULTS = {\n minIntervalMs: 1000,\n timeoutMs: 15_000,\n maxRetries: 3,\n // A catalogue entry moves when a title is edited, which is rare, so a day is\n // safe. The cache lives in memory and dies with the process, so this is a\n // ceiling on one session rather than a stored copy of the site.\n cacheTtlMs: 24 * 60 * 60 * 1000,\n // Scores and reviews move as reviews come in, especially around a release,\n // so they get their own much shorter lifetime.\n scoresCacheTtlMs: 60 * 60 * 1000,\n cacheMaxEntries: 200,\n logLevel: \"error\" as LogLevel,\n};\n\n/**\n * Floor on the request interval, enforced regardless of configuration.\n *\n * Metacritic publishes no rate limit and did not throttle ten consecutive\n * requests during testing. That silence is a reason to pace deliberately rather\n * than an invitation to go fast, so a value below this floor is refused.\n */\nexport const MIN_ALLOWED_INTERVAL_MS = 500;\n\n/** Ceiling on the request interval, so a typo cannot stall the server for hours. */\nexport const MAX_ALLOWED_INTERVAL_MS = 60_000;\n\nconst LOG_LEVELS: LogLevel[] = [\"silent\", \"error\", \"info\", \"debug\"];\n\ninterface NumericRange {\n min: number;\n max: number;\n fallback: number;\n}\n\nfunction readNumber(name: string, env: NodeJS.ProcessEnv, range: NumericRange): number {\n const raw = env[name];\n if (raw === undefined || raw.trim() === \"\") return range.fallback;\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\n // Out-of-range values fall back to the default rather than being clamped to\n // the nearest bound. Clamping turns a typo into a silent behaviour change:\n // -1 retries becomes \"never retry\", and -1 cache entries disables the cache\n // outright, both of which look like working configuration.\n const rounded = Math.round(parsed);\n if (rounded < range.min || rounded > range.max) {\n warn(\n `${name}=${raw} is outside the accepted range ${range.min}-${range.max} and was ignored; ` +\n `using ${range.fallback}`,\n );\n return range.fallback;\n }\n return rounded;\n}\n\nfunction warn(message: string): void {\n process.stderr.write(`[mcp-metacritic] ${message}\\n`);\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 1000, they were asking for no\n * 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.MC_MIN_INTERVAL_MS;\n if (raw === undefined || raw.trim() === \"\") return DEFAULTS.minIntervalMs;\n\n const parsed = Number(raw);\n if (!Number.isFinite(parsed)) {\n warn(`MC_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 `MC_MIN_INTERVAL_MS=${raw} is below the ${MIN_ALLOWED_INTERVAL_MS}ms floor and was ignored; ` +\n `using ${DEFAULTS.minIntervalMs}ms. Metacritic publishes no rate limit, so this client ` +\n `sets its own.`,\n );\n return DEFAULTS.minIntervalMs;\n }\n\n // The upper bound is a guard against a typo that would stall the server for\n // hours. Unlike readNumber, it clamps rather than falling back, because the\n // default would be far faster than the value asked for: someone who wrote ten\n // minutes wants slow, and answering that with 1000ms gets politeness backwards.\n if (rounded > MAX_ALLOWED_INTERVAL_MS) {\n warn(\n `MC_MIN_INTERVAL_MS=${raw} exceeds the ${MAX_ALLOWED_INTERVAL_MS}ms ceiling; ` +\n `using ${MAX_ALLOWED_INTERVAL_MS}ms.`,\n );\n return MAX_ALLOWED_INTERVAL_MS;\n }\n\n return rounded;\n}\n\nexport function loadConfig(env: NodeJS.ProcessEnv = process.env): Config {\n const rawUserAgent = env.MC_USER_AGENT?.trim();\n const rawLogLevel = env.MC_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(`MC_LOG_LEVEL=\"${rawLogLevel}\" is unknown, using \"${DEFAULTS.logLevel}\"`);\n }\n }\n\n return {\n userAgent: rawUserAgent || DEFAULT_USER_AGENT,\n minIntervalMs: readInterval(env),\n timeoutMs: readNumber(\"MC_TIMEOUT_MS\", env, {\n min: 1000,\n max: 120_000,\n fallback: DEFAULTS.timeoutMs,\n }),\n maxRetries: readNumber(\"MC_MAX_RETRIES\", env, {\n min: 0,\n max: 10,\n fallback: DEFAULTS.maxRetries,\n }),\n cacheTtlMs: readNumber(\"MC_CACHE_TTL_MS\", env, {\n min: 0,\n max: 7 * 24 * 60 * 60 * 1000,\n fallback: DEFAULTS.cacheTtlMs,\n }),\n scoresCacheTtlMs: readNumber(\"MC_SCORES_CACHE_TTL_MS\", env, {\n min: 0,\n max: 24 * 60 * 60 * 1000,\n fallback: DEFAULTS.scoresCacheTtlMs,\n }),\n cacheMaxEntries: readNumber(\"MC_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-metacritic] ${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 pair of caches are shared by all tools,\n * so the self-imposed pacing applies to the server as a whole rather than being\n * reset by each tool in turn.\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 { McClient } from \"./mc/client.js\";\nimport {\n browseTitlesDescription,\n browseTitlesInputShape,\n browseTitlesOutputShape,\n runBrowseTitles,\n} from \"./tools/browseTitles.js\";\nimport type { BrowseTitlesArgs } from \"./tools/browseTitles.js\";\nimport {\n getReviewsDescription,\n getReviewsInputShape,\n getReviewsOutputShape,\n runGetReviews,\n} from \"./tools/getReviews.js\";\nimport type { GetReviewsArgs } from \"./tools/getReviews.js\";\nimport {\n getTitleDescription,\n getTitleInputShape,\n getTitleOutputShape,\n runGetTitle,\n} from \"./tools/getTitle.js\";\nimport type { GetTitleArgs } from \"./tools/getTitle.js\";\nimport {\n runSearchTitles,\n searchTitlesDescription,\n searchTitlesInputShape,\n searchTitlesOutputShape,\n} from \"./tools/searchTitles.js\";\nimport type { SearchTitlesArgs } from \"./tools/searchTitles.js\";\nimport { PKG_VERSION } from \"./version.js\";\n\nexport interface CreateServerOptions {\n config?: Config;\n logger?: Logger;\n fetchImpl?: typeof fetch;\n}\n\n/** This server only reads, so every tool is read-only. */\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 McClient({\n config,\n logger,\n ...(options.fetchImpl ? { fetchImpl: options.fetchImpl } : {}),\n });\n\n const server = new McpServer(\n { name: \"mcp-metacritic\", version: PKG_VERSION },\n {\n instructions:\n \"Tools for Metacritic: films, shows and games, their scores, and the reviews behind them. \" +\n \"No API key is needed. Typical flow: search_titles to find an entry and its slug and kind, \" +\n \"then get_title for the entry or get_reviews for what critics wrote. search_titles carries \" +\n \"the critic Metascore, so a question about the critical verdict needs no second call, but \" +\n \"it carries no audience score: read that with get_title, or with browse_titles. \" +\n \"The two scores are on different scales: the critic Metascore runs to 100 and the audience \" +\n \"score to 10, so never compare or average them without rescaling; every score is returned \" +\n \"with its own 'max' for that reason. \" +\n \"Use browse_titles when there is no title to look up, such as best rated or newest releases. \" +\n \"When you repeat a review, name the publication and link the original article, and when you \" +\n \"cite a score, credit Metacritic and link the entry: every result carries a source_url.\",\n },\n );\n\n server.registerTool(\n \"search_titles\",\n {\n title: \"Search Metacritic\",\n description: searchTitlesDescription,\n inputSchema: z.object(searchTitlesInputShape),\n outputSchema: z.object(searchTitlesOutputShape),\n annotations: READ_ONLY,\n },\n async (args) => runSearchTitles(client, args as SearchTitlesArgs),\n );\n\n server.registerTool(\n \"get_title\",\n {\n title: \"Read an entry\",\n description: getTitleDescription,\n inputSchema: z.object(getTitleInputShape),\n outputSchema: z.object(getTitleOutputShape),\n annotations: READ_ONLY,\n },\n async (args) => runGetTitle(client, args as GetTitleArgs),\n );\n\n server.registerTool(\n \"get_reviews\",\n {\n title: \"Read reviews\",\n description: getReviewsDescription,\n inputSchema: z.object(getReviewsInputShape),\n outputSchema: z.object(getReviewsOutputShape),\n annotations: READ_ONLY,\n },\n async (args) => runGetReviews(client, args as GetReviewsArgs),\n );\n\n server.registerTool(\n \"browse_titles\",\n {\n title: \"Browse rankings\",\n description: browseTitlesDescription,\n inputSchema: z.object(browseTitlesInputShape),\n outputSchema: z.object(browseTitlesOutputShape),\n annotations: READ_ONLY,\n },\n async (args) => runBrowseTitles(client, args as BrowseTitlesArgs),\n );\n\n logger.info(\n `ready: user-agent=\"${config.userAgent}\", min interval ${config.minIntervalMs}ms, ` +\n `catalogue cache ${config.cacheTtlMs}ms, scores cache ${config.scoresCacheTtlMs}ms`,\n );\n\n return server;\n}\n","/**\n * In-memory TTL + LRU cache.\n *\n * Nothing is written to disk. The cache exists so that repeated questions about\n * the same title inside one conversation do not hit Metacritic again, not to\n * build a local copy of their catalogue.\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) return undefined;\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) return;\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) break;\n this.store.delete(oldest.value);\n }\n }\n\n get size(): number {\n return this.store.size;\n }\n\n clear(): void {\n this.store.clear();\n }\n}\n","/**\n * Error taxonomy surfaced to the calling model.\n *\n * The rule this file exists to enforce: a failure must never be reported as an\n * empty result. A model that sees \"no title found\" cannot tell that apart from\n * a genuine absence, and will confidently tell the user the film does not exist.\n *\n * Metacritic makes that distinction readable. A successful response carries a\n * `data` object; a failure carries an `errors` array instead, with its own code.\n * Anything matching neither shape is a parse failure rather than an empty answer.\n */\n\nexport type ErrorCode =\n \"not_found\" | \"invalid_input\" | \"rate_limited\" | \"parse_failure\" | \"network_error\" | \"timeout\";\n\nexport interface ErrorDetails {\n url?: string;\n status?: number;\n retryAfterMs?: number;\n hint?: string;\n}\n\nexport class McError extends Error {\n constructor(\n readonly code: ErrorCode,\n message: string,\n readonly details: ErrorDetails = {},\n ) {\n super(message);\n this.name = \"McError\";\n }\n}\n\nconst ISSUES_URL = \"https://github.com/smeet666/mcp-metacritic/issues\";\n\nexport function notFound(url: string, what: string): McError {\n return new McError(\"not_found\", `Metacritic has no entry for ${what}.`, {\n url,\n status: 404,\n hint: \"Use search_titles to find an entry, then pass its slug and kind to this tool.\",\n });\n}\n\nexport function invalidInput(message: string, hint?: string): McError {\n return new McError(\"invalid_input\", message, hint ? { hint } : {});\n}\n\nexport function rateLimited(url: string, retryAfterMs: number): McError {\n return new McError(\n \"rate_limited\",\n \"Metacritic is refusing requests from this client for now. This does NOT mean the title is missing.\",\n {\n url,\n retryAfterMs,\n hint:\n `Wait about ${Math.ceil(retryAfterMs / 1000)} seconds, then call the same tool again with the ` +\n \"same arguments. If it keeps happening, raise MC_MIN_INTERVAL_MS.\",\n },\n );\n}\n\n/**\n * Raised when a response arrives but does not have the shape this server reads.\n *\n * Metacritic publishes no API documentation, so its response shapes can change\n * without notice. Failing here is what turns such a change into a report rather\n * than into answers that look empty.\n */\nexport function parseFailure(url: string, what: string): McError {\n return new McError(\n \"parse_failure\",\n `Metacritic answered, but the response did not have the expected shape (${what}).`,\n { url, hint: `Please report this, with the request you made, at ${ISSUES_URL}` },\n );\n}\n\nexport function upstreamError(url: string, status: number): McError {\n return new McError(\"network_error\", `Metacritic returned HTTP ${status}.`, {\n url,\n status,\n ...(status >= 500\n ? { hint: \"This is a problem on Metacritic's side. Try again shortly.\" }\n : {}),\n });\n}\n","/**\n * Serial request queue with an adaptive minimum interval.\n *\n * Metacritic publishes no rate limit and did not throttle consecutive requests\n * during testing, so the gap between requests is a choice this client makes\n * rather than a rule it is given. Requests run one at a time with a floor on\n * that gap. If the site ever does push back, the interval doubles and then\n * decays back down as requests succeed, which recovers faster than a fixed\n * delay and behaves better than a constant rate.\n */\n\nexport interface RateLimiterOptions {\n minIntervalMs: number;\n maxIntervalMs?: number;\n}\n\nconst DEFAULT_MAX_INTERVAL_MS = 10_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 /**\n * Queue a task. Tasks run in call order, one at a time.\n *\n * A task may send several requests, since the retry loop lives inside its\n * slot. It must call `beforeRequest` around each of them: the pacing is\n * measured between requests, and stamping only the start of the task would\n * let the next one follow a long retry chain's final request immediately.\n */\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, so every\n * request is paced from the previous one whether or not they belong to the\n * same retry chain.\n */\n async beforeRequest(): Promise<void> {\n await this.waitForSlot();\n this.lastStart = Date.now();\n }\n\n /** Called after the site signals it is under load. */\n penalize(): void {\n const next = this.intervalMs === 0 ? 250 : this.intervalMs * 2;\n this.intervalMs = Math.min(this.maxIntervalMs, next);\n }\n\n /** Called after a success, so a single hiccup 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) return;\n const elapsed = Date.now() - this.lastStart;\n // Clamped to the interval: a clock stepped backwards, by NTP or a resumed\n // VM, would otherwise make this wait for the size of the step, and the\n // queue is serial so every pending request would wait behind it.\n const remaining = Math.min(this.intervalMs, this.intervalMs - elapsed);\n if (remaining > 0) await sleep(remaining);\n }\n}\n\nexport function sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n","/**\n * HTTP layer: one GET, with backoff.\n *\n * Status codes are meaningful here, so they are read: 404 marks an unknown\n * slug, 429 and 503 mark push-back, and 5xx is worth retrying. What this layer\n * deliberately does not do is inspect the body, which is the parser's job.\n *\n * Metacritic advertises no rate limit and did not throttle during testing, so\n * the pacing is entirely self-imposed and the retry path is written for the day\n * that changes rather than for observed behaviour.\n */\n\nimport type { Config, Logger } from \"../config.js\";\nimport { McError, notFound, parseFailure, rateLimited, upstreamError } from \"../errors.js\";\nimport { RateLimiter, sleep } from \"./rateLimiter.js\";\n\nconst BACKOFF_BASE_MS = 2000;\nconst BACKOFF_FACTOR = 2;\nconst BACKOFF_MAX_MS = 20_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 capped = Math.min(BACKOFF_MAX_MS, BACKOFF_BASE_MS * BACKOFF_FACTOR ** attempt);\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 URL as text, retrying transient conditions.\n *\n * The retry loop runs inside a single limiter slot, so a queued request cannot\n * interleave with a chain that is backing off. Each attempt claims its own slot\n * through `beforeRequest`, which is what keeps the pacing honest between the\n * last request of one chain and the first request of the next.\n */\nexport async function fetchText(url: string, deps: HttpDeps): Promise<string> {\n const { config, limiter, logger } = deps;\n const doFetch = deps.fetchImpl ?? fetch;\n\n return limiter.schedule(async () => {\n let lastError: McError | undefined;\n\n // Set when the site says how long to stay away; it replaces our own guess\n // for the next attempt. Applied here rather than where it is read, so no\n // wait 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 = askedWaitMs ?? backoffDelay(attempt - 1);\n askedWaitMs = null;\n logger.info(`retry ${attempt}/${config.maxRetries} in ${delay}ms for ${url}`);\n await sleep(Math.min(delay, BACKOFF_MAX_MS));\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/json,*/*;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, url);\n logger.debug(`${lastError.code} for ${url}: ${lastError.message}`);\n continue;\n }\n\n if (status === 429 || status === 503 || status === 403) {\n limiter.penalize();\n // A server that says when to come back knows better than our own guess.\n askedWaitMs = retryAfterMs;\n lastError = rateLimited(url, retryAfterMs ?? backoffDelay(attempt));\n logger.info(\n `refused on ${url} with ${status}, interval now ${limiter.currentIntervalMs}ms`,\n );\n continue;\n }\n if (status >= 500) {\n lastError = upstreamError(url, status);\n continue;\n }\n // An unknown slug is answered with a 404 carrying an error envelope. It is\n // reported as an absence rather than as a transport problem, because the\n // caller asked for something specific and needs to know it is not there.\n if (status === 404) throw notFound(url, \"that request\");\n if (status >= 400) throw upstreamError(url, status);\n\n // An empty body is not a valid answer from any of these endpoints, and is\n // how a stressed edge sometimes refuses. Retrying is safer than handing an\n // empty document to the parser, which would read as \"nothing found\".\n const trimmed = body.trim();\n if (trimmed === \"\") {\n limiter.penalize();\n lastError = rateLimited(url, backoffDelay(attempt));\n logger.info(`empty body on ${url}, treating as a refusal`);\n continue;\n }\n\n // Every route answers with JSON. An HTML body under a success status is\n // the edge answering instead of the application, usually a challenge or\n // an error page, and it clears on its own. Retrying is right; reporting a\n // parse failure would send the caller to the bug tracker for an outage.\n if (!trimmed.startsWith(\"{\") && !trimmed.startsWith(\"[\")) {\n limiter.penalize();\n // Retried because an edge challenge clears on its own, but reported as a\n // parse failure if it never does: a body that is persistently not JSON\n // means the shape moved, and that is worth a bug report.\n lastError = parseFailure(url, \"the body is not JSON\");\n askedWaitMs = retryAfterMs;\n logger.info(`non-JSON body on ${url}, retrying`);\n continue;\n }\n\n limiter.relax();\n return body;\n }\n\n throw lastError ?? new McError(\"network_error\", `Could not fetch ${url}.`, { url });\n });\n}\n\n/** `Retry-After` carries either seconds or an HTTP date. */\nfunction parseRetryAfter(raw: string | null): number | null {\n if (!raw) return null;\n const seconds = Number(raw.trim());\n if (Number.isFinite(seconds) && seconds >= 0) return Math.round(seconds * 1000);\n const when = Date.parse(raw);\n if (Number.isNaN(when)) return null;\n return Math.max(0, when - Date.now());\n}\n\nfunction asTransportError(error: unknown, url: string): McError {\n if (error instanceof McError) return error;\n const name = error instanceof Error ? error.name : \"\";\n if (name === \"TimeoutError\" || name === \"AbortError\") {\n return new McError(\"timeout\", \"Metacritic did not answer in time.\", {\n url,\n hint: \"Raise MC_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 McError(\"network_error\", `Could not reach Metacritic: ${message}`, { url });\n}\n","/**\n * Every route and every response field this server reads, in one place.\n *\n * Metacritic documents none of this: the routes are the ones its own web\n * frontend calls, and the field names come from reading those responses. That\n * makes a rename upstream both likely and silent, so collecting them here keeps\n * the repair to a single file, and the live canary asserts each one by name so\n * a change surfaces as a named failure rather than as empty answers.\n */\n\nexport const API_BASE = \"https://backend.metacritic.com\";\nexport const SITE_BASE = \"https://www.metacritic.com\";\n\n/**\n * The API and the website disagree on plurality: the backend serves /movies/,\n * /shows/ and /games/, while the pages a reader should be linked to live at\n * /movie/, /tv/ and /game/. Both forms are needed and neither can be derived\n * from the other, so both are written out.\n */\nexport const API_SEGMENT = {\n movie: \"movies\",\n show: \"shows\",\n game: \"games\",\n} as const;\n\nexport const SITE_SEGMENT = {\n movie: \"movie\",\n show: \"tv\",\n game: \"game\",\n} as const;\n\n/** Internal type ids the browse route filters on. */\nexport const MCO_TYPE_ID = {\n show: 1,\n movie: 2,\n game: 13,\n} as const;\n\n/** The `type` string search results carry, which does not match our own naming. */\nexport const SEARCH_TYPE = {\n movie: \"movie\",\n show: \"show\",\n game: \"game-title\",\n} as const;\n\n/** Sort orders the browse route accepts. */\nexport const SORT_BY = {\n score: \"-metaScore\",\n recent: \"-releaseDate\",\n popular: \"-popularityCount\",\n} as const;\n\n/**\n * Component identifiers the backend requires on several routes. They describe\n * the widget the response was built for, and requests omitting them are refused.\n */\nexport const COMPONENT = {\n product: { componentName: \"product\", componentType: \"Product\" },\n criticStats: { componentName: \"critic-score-summary\", componentType: \"MetaScoreSummary\" },\n userStats: { componentName: \"user-score-summary\", componentType: \"MetaScoreSummary\" },\n criticReviews: { componentName: \"critic-reviews\", componentType: \"ProductReviewsSummary\" },\n userReviews: { componentName: \"user-reviews\", componentType: \"ProductReviewsSummary\" },\n} as const;\n\n/** Field names read off responses. */\nexport const FIELD = {\n data: \"data\",\n errors: \"errors\",\n item: \"item\",\n items: \"items\",\n totalResults: \"totalResults\",\n /** Reviews arrive pre-sorted into these buckets, which is the sentiment filter. */\n reviewBuckets: { all: \"default\", positive: \"positive\", neutral: \"neutral\", negative: \"negative\" },\n} as const;\n","/**\n * URL building.\n *\n * The API lives on a backend host, while the pages a reader should be sent to\n * live on the main site. Attribution links must point at the latter, which is\n * the only thing this server gives back in exchange for the data.\n */\n\nimport { invalidInput } from \"../errors.js\";\nimport type { Kind, Sentiment } from \"../types.js\";\nimport {\n API_BASE,\n API_SEGMENT,\n COMPONENT,\n MCO_TYPE_ID,\n SITE_BASE,\n SITE_SEGMENT,\n SORT_BY,\n} from \"./paths.js\";\n\ntype Params = Record<string, string | number | undefined>;\n\nfunction build(path: string, params: Params): string {\n const query = new URLSearchParams();\n for (const [key, value] of Object.entries(params)) {\n if (value !== undefined && value !== \"\") query.set(key, String(value));\n }\n const suffix = query.toString();\n return suffix ? `${API_BASE}${path}?${suffix}` : `${API_BASE}${path}`;\n}\n\n/**\n * Slugs go into the path, so a malformed one would silently change which route\n * is called. Rejecting here keeps that from reaching the network.\n */\nfunction checkSlug(slug: string): string {\n const trimmed = slug.trim();\n if (trimmed === \"\") {\n throw invalidInput(\"The slug is empty.\", \"Take it from a search_titles result.\");\n }\n if (!/^[a-z0-9][a-z0-9-]*$/i.test(trimmed)) {\n throw invalidInput(\n `\"${slug}\" does not look like a Metacritic slug.`,\n \"Slugs are lowercase words joined by hyphens, such as the-matrix. Use search_titles to get one.\",\n );\n }\n return trimmed;\n}\n\nexport function searchUrl(query: string, limit: number, offset: number): string {\n const trimmed = query.trim();\n if (trimmed === \"\") {\n throw invalidInput(\"The search query is empty.\", \"Pass a title or part of one.\");\n }\n return build(`/finder/metacritic/search/${encodeURIComponent(trimmed)}/web`, { limit, offset });\n}\n\nexport function browseUrl(options: {\n kind: Kind;\n sort: keyof typeof SORT_BY;\n genre?: string;\n limit: number;\n offset: number;\n}): string {\n return build(\"/finder/metacritic/web\", {\n sortBy: SORT_BY[options.sort],\n mcoTypeId: MCO_TYPE_ID[options.kind],\n genres: options.genre,\n limit: options.limit,\n offset: options.offset,\n });\n}\n\nexport function detailUrl(kind: Kind, slug: string): string {\n return build(`/${API_SEGMENT[kind]}/metacritic/${checkSlug(slug)}/web`, COMPONENT.product);\n}\n\nexport function scoreUrl(kind: Kind, slug: string, source: \"critic\" | \"user\"): string {\n const component = source === \"critic\" ? COMPONENT.criticStats : COMPONENT.userStats;\n return build(\n `/reviews/metacritic/${source}/${API_SEGMENT[kind]}/${checkSlug(slug)}/stats/web`,\n component,\n );\n}\n\n/**\n * Reviews for one entry.\n *\n * No limit or offset is sent. The route ignores both and always answers with\n * the same sample, so including them would only vary the URL, and with it the\n * cache key, making every page of a sample already held in memory cost a fresh\n * request. Paging happens over the sample the caller already has.\n */\nexport function reviewsUrl(options: {\n kind: Kind;\n slug: string;\n source: \"critic\" | \"user\";\n}): string {\n const component = options.source === \"critic\" ? COMPONENT.criticReviews : COMPONENT.userReviews;\n return build(\n `/reviews/metacritic/${options.source}/${API_SEGMENT[options.kind]}/${checkSlug(options.slug)}/summary/web`,\n component,\n );\n}\n\n/** Streaming offers, keyed by IMDb id rather than by Metacritic slug. */\nexport function watchUrl(imdbId: string, kind: Kind): string {\n const trimmed = imdbId.trim();\n if (!/^tt\\d+$/.test(trimmed)) {\n throw invalidInput(\n `\"${imdbId}\" is not an IMDb id.`,\n \"IMDb ids look like tt0133093. get_title returns one as imdb_id.\",\n );\n }\n return build(\"/justwatch/metacritic/offersByIMDbId\", {\n id: trimmed,\n type: kind === \"show\" ? \"show\" : \"movie\",\n });\n}\n\n/** The page a reader should be sent to, and the link attribution requires. */\nexport function titlePageUrl(kind: Kind, slug: string): string {\n return `${SITE_BASE}/${SITE_SEGMENT[kind]}/${slug}/`;\n}\n\n/** Which pre-sorted bucket of reviews to read. */\nexport function bucketFor(sentiment: Sentiment): string {\n return sentiment === \"all\" ? \"default\" : sentiment;\n}\n","/**\n * Responses to domain types.\n *\n * Every route answers with the same envelope: `{\"data\": …}` on success, or\n * `{\"errors\":[{\"code\":404,…}]}` on failure. Reading that envelope is the whole\n * job of the first half of this file, because getting it wrong is how a failure\n * turns into an answer that looks empty.\n *\n * The second half maps fields. Listing rows are trimmed here rather than in a\n * tool, so the heavy payload has no path out of this module.\n */\n\nimport { notFound, parseFailure } from \"../errors.js\";\nimport type {\n Award,\n Company,\n CriticReview,\n Kind,\n ReviewPage,\n ScoreSummary,\n Sentiment,\n TitleDetail,\n TitlePage,\n TitleSummary,\n UserReview,\n WatchOffer,\n} from \"../types.js\";\nimport { FIELD, SEARCH_TYPE } from \"./paths.js\";\nimport { bucketFor, titlePageUrl } from \"./urls.js\";\n\ntype Json = Record<string, unknown>;\n\nconst isObject = (value: unknown): value is Json =>\n typeof value === \"object\" && value !== null && !Array.isArray(value);\n\n/**\n * Read the envelope and return the payload.\n *\n * An `errors` array is the site saying it could not serve the request, which is\n * an absence when the caller named something specific. Anything that is neither\n * shape means the response is not what this server knows how to read.\n */\nexport function envelope(raw: string, url: string, what: string): Json {\n let body: unknown;\n try {\n body = JSON.parse(raw);\n } catch {\n throw parseFailure(url, \"the body is not valid JSON\");\n }\n\n if (!isObject(body)) throw parseFailure(url, \"the body is not an object\");\n\n const errors = body[FIELD.errors];\n if (Array.isArray(errors) && errors.length > 0) {\n const first = isObject(errors[0]) ? errors[0] : {};\n if (first.code === 404) throw notFound(url, what);\n throw parseFailure(\n url,\n `the site reported ${String(first.reason ?? first.code ?? \"an error\")}`,\n );\n }\n\n const data = body[FIELD.data];\n if (!isObject(data)) throw parseFailure(url, \"the response carries no data object\");\n return data;\n}\n\nconst str = (value: unknown): string | null => {\n if (typeof value !== \"string\") return null;\n const trimmed = value.trim();\n return trimmed === \"\" ? null : trimmed;\n};\n\nconst num = (value: unknown): number | null => {\n if (typeof value === \"number\" && Number.isFinite(value)) return value;\n if (typeof value === \"string\") {\n const parsed = Number(value);\n if (Number.isFinite(parsed)) return parsed;\n }\n return null;\n};\n\nconst intOf = (value: unknown): number | null => {\n const parsed = num(value);\n return parsed === null ? null : Math.trunc(parsed);\n};\n\n/** Map the site's own type wording onto ours. */\nfunction kindOf(value: unknown): Kind | null {\n const type = str(value);\n if (type === SEARCH_TYPE.movie) return \"movie\";\n if (type === SEARCH_TYPE.show) return \"show\";\n if (type === SEARCH_TYPE.game) return \"game\";\n return null;\n}\n\n/**\n * A row without an id, a slug or a kind cannot be looked up or linked, so it is\n * not a row. Returning null lets the caller skip one bad entry among many while\n * still failing loudly when every entry is unreadable.\n */\nfunction toSummary(node: unknown): TitleSummary | null {\n if (!isObject(node)) return null;\n\n const id = intOf(node.id);\n const slug = str(node.slug);\n const title = str(node.title);\n const kind = kindOf(node.type);\n if (id === null || !slug || !title || !kind) return null;\n\n const critic = isObject(node.criticScoreSummary) ? node.criticScoreSummary : {};\n\n // The two listing routes report the audience score differently: browse rows\n // carry `userScore: {score}`, search rows carry nothing at all. Both forms are\n // read, and a row with neither keeps null rather than inventing a zero.\n const userSummary = isObject(node.userScoreSummary) ? node.userScoreSummary : {};\n const userObject = isObject(node.userScore) ? node.userScore : {};\n const userScore = num(userSummary.score) ?? num(userObject.score) ?? num(node.userScore);\n\n return {\n id,\n kind,\n title,\n slug,\n year: intOf(node.premiereYear),\n releaseDate: str(node.releaseDate),\n rating: str(node.rating),\n metascore: num(critic.score),\n userScore,\n sourceUrl: titlePageUrl(kind, slug),\n };\n}\n\n/** Search and browse share this shape, so they share one reader. */\nexport function parseTitlePage(raw: string, url: string, what: string): TitlePage {\n const data = envelope(raw, url, what);\n const items = data[FIELD.items];\n if (!Array.isArray(items)) throw parseFailure(url, \"the response carries no items array\");\n\n const titles: TitleSummary[] = [];\n for (const node of items) {\n const row = toSummary(node);\n if (row) titles.push(row);\n }\n\n if (items.length > 0 && titles.length === 0) {\n throw parseFailure(url, `${items.length} rows but none could be read`);\n }\n\n return {\n titles,\n totalResults: intOf(data[FIELD.totalResults]) ?? titles.length,\n itemCount: items.length,\n };\n}\n\n/**\n * Companies attached to a title, deduplicated by name.\n *\n * The list mixes producers with every distributor, broadcaster and home-video\n * label in every territory, so a well-travelled film carries the same name a\n * dozen times under different ids: Blade Runner lists 128 entries of which\n * \"Warner Home Video\" is fifteen. The first id for a name is kept, since they\n * differ only by the release they were attached to.\n */\nfunction toCompanies(value: unknown): Company[] {\n const source = isObject(value) ? value.companies : value;\n if (!Array.isArray(source)) return [];\n\n const seen = new Set<string>();\n const out: Company[] = [];\n for (const node of source) {\n const name = isObject(node) ? str(node.name) : str(node);\n if (!name) continue;\n const key = name.toLowerCase();\n if (seen.has(key)) continue;\n seen.add(key);\n out.push({ name, id: isObject(node) ? intOf(node.id) : null });\n }\n return out;\n}\n\nfunction toStringList(value: unknown): string[] {\n if (!Array.isArray(value)) return [];\n const out: string[] = [];\n for (const node of value) {\n const name = isObject(node) ? str(node.name) : str(node);\n if (name) out.push(name);\n }\n return out;\n}\n\n/**\n * Awards are reported one entry per ceremony, as a tally rather than a list of\n * individual prizes. A ceremony where nothing was won carries `wins: null`, so\n * that is preserved instead of being flattened to zero: \"no win recorded\" and\n * \"zero wins\" are the same here, but only the source can say which it meant.\n */\nfunction toAwards(value: unknown): Award[] {\n if (!Array.isArray(value)) return [];\n const out: Award[] = [];\n for (const node of value) {\n if (!isObject(node)) continue;\n const ceremony = str(node.awardEvent);\n if (!ceremony) continue;\n out.push({\n ceremony,\n wins: intOf(node.wins),\n nominations: intOf(node.nominations),\n });\n }\n return out;\n}\n\nexport function parseDetail(raw: string, url: string, kind: Kind, slug: string): TitleDetail {\n const data = envelope(raw, url, `${kind} \"${slug}\"`);\n const item = data[FIELD.item];\n if (!isObject(item)) throw parseFailure(url, \"the response carries no item object\");\n\n // The detail route omits `type`, so the kind comes from the caller, who chose\n // the route. Building the summary by hand keeps that difference explicit.\n const id = intOf(item.id);\n const title = str(item.title);\n if (id === null || !title) throw parseFailure(url, \"the entry has no id or no title\");\n\n const critic = isObject(item.criticScoreSummary) ? item.criticScoreSummary : {};\n\n return {\n id,\n kind,\n title,\n slug,\n year: intOf(item.premiereYear),\n releaseDate: str(item.releaseDate),\n rating: str(item.rating),\n metascore: num(critic.score),\n userScore: null,\n sourceUrl: titlePageUrl(kind, slug),\n description: str(item.description),\n tagline: str(item.tagline),\n genres: toStringList(item.genres),\n duration: intOf(item.duration),\n imdbId: str(item.imdbId),\n networks: toStringList(item.networks),\n production: toCompanies(item.production),\n awards: toAwards(item.awards),\n criticScoreDetail: null,\n userScoreDetail: null,\n };\n}\n\n/**\n * A score summary, with its scale.\n *\n * `max` comes from the response rather than from the source name, because the\n * two scales differ and hard-coding them here would be a guess that survives\n * quietly if the site ever changes one.\n */\nexport function parseScore(raw: string, url: string, what: string): ScoreSummary {\n const data = envelope(raw, url, what);\n const item = data[FIELD.item];\n if (!isObject(item)) throw parseFailure(url, \"the response carries no score object\");\n\n const max = num(item.max);\n if (max === null) throw parseFailure(url, \"the score carries no scale\");\n\n return {\n score: num(item.score),\n max,\n reviewCount: intOf(item.reviewCount),\n positiveCount: intOf(item.positiveCount),\n neutralCount: intOf(item.neutralCount),\n negativeCount: intOf(item.negativeCount),\n sentiment: str(item.sentiment),\n };\n}\n\nfunction reviewBucket(data: Json, url: string, sentiment: Sentiment): unknown[] {\n const item = data[FIELD.item];\n if (!isObject(item)) throw parseFailure(url, \"the response carries no reviews object\");\n\n const bucket = item[bucketFor(sentiment)];\n // A bucket that does not exist means the site stopped grouping reviews this\n // way, which is a change worth reporting rather than an absence of reviews.\n if (!Array.isArray(bucket)) {\n throw parseFailure(url, `there is no \"${bucketFor(sentiment)}\" group of reviews`);\n }\n return bucket;\n}\n\nexport function parseCriticReviews(\n raw: string,\n url: string,\n what: string,\n sentiment: Sentiment,\n): ReviewPage<CriticReview> {\n const data = envelope(raw, url, what);\n const bucket = reviewBucket(data, url, sentiment);\n\n const reviews: CriticReview[] = [];\n for (const node of bucket) {\n if (!isObject(node)) continue;\n const quote = str(node.quote);\n const publication = str(node.publicationName);\n // A quote with no publication cannot be attributed, and attribution is the\n // condition under which this content is worth passing on at all.\n if (!quote || !publication) continue;\n reviews.push({\n quote,\n score: num(node.score),\n publication,\n author: str(node.author),\n url: str(node.url),\n date: str(node.date),\n });\n }\n\n if (bucket.length > 0 && reviews.length === 0) {\n throw parseFailure(url, `${bucket.length} reviews but none could be read`);\n }\n\n return {\n reviews,\n totalResults: intOf(data[FIELD.totalResults]) ?? reviews.length,\n itemCount: bucket.length,\n };\n}\n\nexport function parseUserReviews(\n raw: string,\n url: string,\n what: string,\n sentiment: Sentiment,\n): ReviewPage<UserReview> {\n const data = envelope(raw, url, what);\n const bucket = reviewBucket(data, url, sentiment);\n\n const reviews: UserReview[] = [];\n for (const node of bucket) {\n if (!isObject(node)) continue;\n const quote = str(node.quote);\n if (!quote) continue;\n reviews.push({ quote, score: num(node.score), date: str(node.date) });\n }\n\n if (bucket.length > 0 && reviews.length === 0) {\n throw parseFailure(url, `${bucket.length} reviews but none could be read`);\n }\n\n return {\n reviews,\n totalResults: intOf(data[FIELD.totalResults]) ?? reviews.length,\n itemCount: bucket.length,\n };\n}\n\n/**\n * Unwrap a click-tracking redirect.\n *\n * Offer links arrive as tracking URLs of about 800 characters, most of it a\n * base64 analytics payload, with the real destination in the `r` parameter.\n * Nine offers on one film came to 7 KB of redirects against 300 bytes of actual\n * links, so the destination is extracted and the wrapper dropped.\n */\nfunction unwrapLink(link: string | null): string | null {\n if (!link) return null;\n try {\n const target = new URL(link).searchParams.get(\"r\");\n if (target && /^https?:\\/\\//i.test(target)) return target;\n } catch {\n // Not a URL this function understands; the original is still usable.\n }\n return link;\n}\n\n/** Streaming offers, grouped upstream by how you pay for them. */\nexport function parseOffers(raw: string, url: string, what: string): WatchOffer[] {\n const data = envelope(raw, url, what);\n const item = data[FIELD.item];\n if (!isObject(item)) throw parseFailure(url, \"the response carries no offers object\");\n\n const offers: WatchOffer[] = [];\n for (const [group, value] of Object.entries(item)) {\n if (!Array.isArray(value)) continue;\n for (const node of value) {\n if (!isObject(node)) continue;\n const provider = str(node.providerName);\n if (!provider) continue;\n offers.push({ provider, kind: group, url: unwrapLink(str(node.link)) });\n }\n }\n return offers;\n}\n","/**\n * High-level Metacritic client.\n *\n * This module knows nothing about MCP, which keeps it testable against plain\n * strings and usable as a library through the `./client` export.\n */\n\nimport type { Config, Logger } from \"../config.js\";\nimport {\n DEFAULT_USER_AGENT,\n MIN_ALLOWED_INTERVAL_MS,\n createLogger,\n loadConfig,\n} from \"../config.js\";\nimport type {\n CriticReview,\n Kind,\n ReviewPage,\n ScoreSummary,\n Sentiment,\n TitleDetail,\n TitlePage,\n UserReview,\n WatchOffer,\n} from \"../types.js\";\nimport { TtlLruCache } from \"./cache.js\";\nimport { fetchText } from \"./http.js\";\nimport {\n parseCriticReviews,\n parseDetail,\n parseOffers,\n parseScore,\n parseTitlePage,\n parseUserReviews,\n} from \"./parse.js\";\nimport type { SORT_BY } from \"./paths.js\";\nimport { RateLimiter } from \"./rateLimiter.js\";\nimport { browseUrl, detailUrl, reviewsUrl, scoreUrl, searchUrl, watchUrl } from \"./urls.js\";\n\nexport interface McClientOptions {\n config?: Config;\n logger?: Logger;\n fetchImpl?: typeof fetch;\n}\n\nexport interface Outcome<T> {\n data: T;\n /** True when served from the in-memory cache rather than the network. */\n cached: boolean;\n}\n\nexport type Sort = keyof typeof SORT_BY;\n\n/**\n * Apply the guarantees this project makes about its own traffic.\n *\n * The environment parser already enforces both, but `McClient` is published as\n * a library through the `./client` export and takes a caller-built config, so\n * without this the pacing floor and the honest identity are optional for anyone\n * importing it. They are the two things this server offers in exchange for\n * reading a source that publishes no terms, so they hold on every path.\n *\n * A caller may still name their own application in the User-Agent, and there\n * are good reasons to. Passing the traffic off as a browser is a different\n * thing, and gets the project's own identity appended so it stays attributable.\n */\nfunction withGuarantees(config: Config): Config {\n const userAgent = /mozilla\\/|applewebkit|chrome\\/|safari\\/|gecko/i.test(config.userAgent)\n ? `${config.userAgent} ${DEFAULT_USER_AGENT}`\n : config.userAgent;\n return {\n ...config,\n userAgent,\n minIntervalMs: Math.max(MIN_ALLOWED_INTERVAL_MS, config.minIntervalMs),\n };\n}\n\nexport class McClient {\n private readonly config: Config;\n private readonly logger: Logger;\n private readonly limiter: RateLimiter;\n /**\n * Two caches, because the two kinds of data age at different speeds. A\n * catalogue entry changes when someone edits it, which is rare. A score moves\n * every time a review lands, which around a release is constantly.\n */\n private readonly catalogueCache: TtlLruCache<unknown>;\n private readonly scoresCache: TtlLruCache<unknown>;\n private readonly fetchImpl: typeof fetch | undefined;\n\n constructor(options: McClientOptions = {}) {\n this.config = withGuarantees(options.config ?? loadConfig());\n this.logger = options.logger ?? createLogger(this.config.logLevel);\n this.limiter = new RateLimiter({ minIntervalMs: this.config.minIntervalMs });\n this.catalogueCache = new TtlLruCache<unknown>(\n this.config.cacheMaxEntries,\n this.config.cacheTtlMs,\n );\n this.scoresCache = new TtlLruCache<unknown>(\n this.config.cacheMaxEntries,\n this.config.scoresCacheTtlMs,\n );\n this.fetchImpl = options.fetchImpl;\n }\n\n /**\n * Search across films, shows and games.\n *\n * The return type carries rows only. The upstream response embeds artwork\n * variants on every match, and dropping them here rather than in a tool means\n * they have no path out of this layer.\n */\n async search(query: string, limit: number, offset: number): Promise<Outcome<TitlePage>> {\n const url = searchUrl(query, limit, offset);\n return this.fetchParsed(url, this.catalogueCache, (body) =>\n parseTitlePage(body, url, `the query \"${query}\"`),\n );\n }\n\n async browse(options: {\n kind: Kind;\n sort: Sort;\n genre?: string;\n limit: number;\n offset: number;\n }): Promise<Outcome<TitlePage>> {\n const url = browseUrl(options);\n return this.fetchParsed(url, this.catalogueCache, (body) =>\n parseTitlePage(body, url, `that ${options.kind} listing`),\n );\n }\n\n async getDetail(kind: Kind, slug: string): Promise<Outcome<TitleDetail>> {\n const url = detailUrl(kind, slug);\n // The trimmed form is what reached the network, so it is also what the\n // public link and the echoed slug must be built from.\n const clean = slug.trim();\n return this.fetchParsed(url, this.catalogueCache, (body) =>\n parseDetail(body, url, kind, clean),\n );\n }\n\n async getScore(\n kind: Kind,\n slug: string,\n source: \"critic\" | \"user\",\n ): Promise<Outcome<ScoreSummary>> {\n const url = scoreUrl(kind, slug, source);\n return this.fetchParsed(url, this.scoresCache, (body) =>\n parseScore(body, url, `the ${source} score of ${kind} \"${slug}\"`),\n );\n }\n\n async getCriticReviews(options: {\n kind: Kind;\n slug: string;\n sentiment: Sentiment;\n limit: number;\n offset: number;\n }): Promise<Outcome<ReviewPage<CriticReview>>> {\n const url = reviewsUrl({ ...options, source: \"critic\" });\n return this.fetchParsed(\n url,\n this.scoresCache,\n (body) =>\n parseCriticReviews(\n body,\n url,\n `reviews of ${options.kind} \"${options.slug}\"`,\n options.sentiment,\n ),\n options.sentiment,\n );\n }\n\n async getUserReviews(options: {\n kind: Kind;\n slug: string;\n sentiment: Sentiment;\n limit: number;\n offset: number;\n }): Promise<Outcome<ReviewPage<UserReview>>> {\n const url = reviewsUrl({ ...options, source: \"user\" });\n return this.fetchParsed(\n url,\n this.scoresCache,\n (body) =>\n parseUserReviews(\n body,\n url,\n `reviews of ${options.kind} \"${options.slug}\"`,\n options.sentiment,\n ),\n options.sentiment,\n );\n }\n\n async getWatchOffers(imdbId: string, kind: Kind): Promise<Outcome<WatchOffer[]>> {\n const url = watchUrl(imdbId, kind);\n return this.fetchParsed(url, this.scoresCache, (body) =>\n parseOffers(body, url, `streaming offers for ${imdbId}`),\n );\n }\n\n /**\n * Fetch, parse, then cache. In that order: a response that could not be read\n * is never stored, so a bad minute upstream cannot be replayed from memory\n * for the rest of the cache lifetime.\n */\n private async fetchParsed<T>(\n url: string,\n cache: TtlLruCache<unknown>,\n parse: (body: string) => T,\n /**\n * Distinguishes parses of the same response. The reviews route returns every\n * sentiment in one payload, so two calls differing only by sentiment share a\n * URL: keying on the URL alone would serve the first answer to the second.\n */\n variant = \"\",\n ): Promise<Outcome<T>> {\n const key = variant ? `${url}#${variant}` : url;\n const hit = cache.get(key);\n if (hit !== undefined) {\n this.logger.debug(`cache hit ${key}`);\n return { data: hit as T, cached: true };\n }\n\n const body = await fetchText(url, {\n config: this.config,\n limiter: this.limiter,\n logger: this.logger,\n ...(this.fetchImpl ? { fetchImpl: this.fetchImpl } : {}),\n });\n\n const data = parse(body);\n cache.set(key, data);\n return { data, cached: false };\n }\n}\n","/**\n * browse_titles: rankings and release lists.\n *\n * This is the tool that answers \"best horror films\" or \"highest rated games\",\n * questions a search box cannot serve because the caller has no title in mind.\n */\n\nimport { z } from \"zod\";\nimport type { McClient, Sort } from \"../mc/client.js\";\nimport type { Kind } from \"../types.js\";\nimport {\n ok,\n renderTitleList,\n titleSummarySchema,\n toTitleSummaryOut,\n toToolError,\n type ToolResult,\n} from \"./shared.js\";\n\nexport const browseTitlesDescription = [\n \"List films, shows or games by score, by release date or by current popularity.\",\n \"Use this when there is no specific title to look up: best rated, newest, what people are looking at now.\",\n \"Filter by genre with a single name such as Horror, Comedy or Action.\",\n \"Sorting by score returns the all-time ranking, which is dominated by older titles: combine with a genre to narrow it.\",\n \"Scores come back on their own scales, 100 for critics and 10 for users.\",\n \"Paging is approximate: Metacritic does not order tied entries stably, so an entry can appear on two consecutive pages. Deduplicate by slug rather than counting rows.\",\n].join(\" \");\n\nexport const browseTitlesInputShape = {\n kind: z.enum([\"movie\", \"show\", \"game\"]).default(\"movie\").describe(\"Which catalogue to list.\"),\n sort: z\n .enum([\"score\", \"recent\", \"popular\"])\n .default(\"score\")\n .describe(\n \"'score' is the critic ranking, 'recent' is by release date, 'popular' is current attention.\",\n ),\n genre: z.string().optional().describe(\"Single genre name, such as Horror. Omit for all genres.\"),\n limit: z.number().int().min(1).max(50).default(20).describe(\"How many rows to return.\"),\n offset: z.number().int().min(0).default(0).describe(\"How many rows to skip, for paging.\"),\n};\n\nexport const browseTitlesOutputShape = {\n kind: z.string(),\n sort: z.string(),\n genre: z.string().nullable(),\n results: z.array(titleSummarySchema),\n total_available: z\n .number()\n .int()\n .describe(\"How many entries match upstream, which is the size of what this samples.\"),\n offset: z.number().int(),\n next_offset: z\n .number()\n .int()\n .nullable()\n .describe(\n \"Pass as 'offset' for the next page. Tied entries are not ordered stably upstream, so \" +\n \"deduplicate by slug across pages.\",\n ),\n notes: z.array(z.string()),\n};\n\nexport interface BrowseTitlesArgs {\n kind: Kind;\n sort: Sort;\n genre?: string;\n limit: number;\n offset: number;\n}\n\nexport async function runBrowseTitles(\n client: McClient,\n args: BrowseTitlesArgs,\n): Promise<ToolResult> {\n try {\n const { data, cached } = await client.browse({\n kind: args.kind,\n sort: args.sort,\n ...(args.genre ? { genre: args.genre } : {}),\n limit: args.limit,\n offset: args.offset,\n });\n\n const results = data.titles.map(toTitleSummaryOut);\n const notes: string[] = [];\n if (cached) notes.push(\"Served from this server's short-lived in-memory cache.\");\n if (results.length === 0) {\n notes.push(\n args.genre\n ? `No ${args.kind} matched the genre \"${args.genre}\" at this offset. Check the spelling, or drop the genre.`\n : \"The listing returned no rows at this offset.\",\n );\n }\n if (data.itemCount > data.titles.length) {\n notes.push(\n `${data.itemCount - data.titles.length} entries on this page could not be read and were skipped.`,\n );\n }\n\n // A short page is the end-of-list signal, judged on what the site sent\n // rather than on what could be read, since its offset counts entries.\n const nextOffset = data.itemCount < args.limit ? null : args.offset + data.itemCount;\n\n const heading = [\n args.genre ? `${args.genre} ` : \"\",\n `${args.kind}s`,\n args.sort === \"score\"\n ? \" by critic score\"\n : args.sort === \"recent\"\n ? \" by release date\"\n : \" by current popularity\",\n ].join(\"\");\n\n const summary =\n results.length === 0\n ? `No rows for ${heading}.`\n : `${results.length} of ${data.totalResults} ${heading}:\\n${renderTitleList(results)}`;\n\n return ok(\n {\n kind: args.kind,\n sort: args.sort,\n genre: args.genre ?? null,\n results,\n total_available: data.totalResults,\n offset: args.offset,\n next_offset: nextOffset,\n notes,\n },\n summary,\n { notes },\n );\n } catch (error) {\n return toToolError(error);\n }\n}\n","/** Pieces shared by the four tools: schemas, error mapping, text mirrors. */\n\nimport { z } from \"zod\";\nimport { McError } from \"../errors.js\";\nimport type { ScoreSummary, TitleSummary } from \"../types.js\";\n\n/** Many MCP clients render only the text block, so it must read on its own. */\nexport const MAX_TEXT_MIRROR_CHARS = 2000;\n\nexport const ATTRIBUTION = \"Source: Metacritic\";\n\nexport const kindSchema = z\n .enum([\"movie\", \"show\", \"game\"])\n .describe(\"Which catalogue the entry belongs to. Pass it back with the slug.\");\n\nexport const titleSummarySchema = z.object({\n id: z.number().int(),\n kind: z.enum([\"movie\", \"show\", \"game\"]),\n title: z.string(),\n slug: z.string().describe(\"Identifier to pass to get_title and get_reviews, with 'kind'.\"),\n year: z.number().int().nullable(),\n release_date: z.string().nullable(),\n rating: z.string().nullable().describe(\"Age rating as published, such as R or TV-MA.\"),\n metascore: z\n .number()\n .nullable()\n .describe(\"Critic score, out of 100. Not comparable to user_score without rescaling.\"),\n user_score: z\n .number()\n .nullable()\n .describe(\"Audience score, out of 10. Not comparable to metascore without rescaling.\"),\n source_url: z.string().describe(\"Metacritic page. Show this when citing the entry.\"),\n});\n\nexport type TitleSummaryOut = z.infer<typeof titleSummarySchema>;\n\nexport function toTitleSummaryOut(title: TitleSummary): TitleSummaryOut {\n return {\n id: title.id,\n kind: title.kind,\n title: title.title,\n slug: title.slug,\n year: title.year,\n release_date: title.releaseDate,\n rating: title.rating,\n metascore: title.metascore,\n user_score: title.userScore,\n source_url: title.sourceUrl,\n };\n}\n\n/**\n * Every score goes out with its own scale.\n *\n * Critic scores run to 100 and user scores to 10. A bare number invites a\n * comparison that is wrong by an order of magnitude, so `max` is not optional\n * and the description says what it is for.\n */\nexport const scoreSchema = z.object({\n score: z.number().nullable(),\n max: z.number().describe(\"Scale this score is on: 100 for critics, 10 for users.\"),\n review_count: z.number().int().nullable(),\n positive_count: z.number().int().nullable(),\n neutral_count: z.number().int().nullable(),\n negative_count: z.number().int().nullable(),\n sentiment: z.string().nullable().describe(\"Metacritic's own wording for the verdict.\"),\n});\n\nexport function toScoreOut(score: ScoreSummary): z.infer<typeof scoreSchema> {\n return {\n score: score.score,\n max: score.max,\n review_count: score.reviewCount,\n positive_count: score.positiveCount,\n neutral_count: score.neutralCount,\n negative_count: score.negativeCount,\n sentiment: score.sentiment,\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 * Build a result whose text block always ends with its notes and attribution.\n *\n * The body is truncated to fit around the trailer rather than the whole block\n * being cut afterwards. Appending the credit and then truncating loses exactly\n * the credit, which is the one line that must survive: a client rendering only\n * the text would otherwise show third-party review quotes with no source.\n *\n * The notes belong to the trailer for the same reason. They are what qualifies\n * the answer, saying that a list was capped, that a score could not be read, or\n * that a section does not apply to this kind of entry. A client rendering only\n * the text reads an unqualified answer without them, which is the failure this\n * server exists to avoid.\n *\n * The trailer also states that the text was shortened, since a client that\n * cannot read `structuredContent` has no other way to know rows are missing.\n */\nexport function ok(\n structured: Record<string, unknown>,\n body: string,\n options: { notes?: string[]; sourceUrl?: string } = {},\n): ToolResult {\n const attribution = options.sourceUrl ? `${ATTRIBUTION} — ${options.sourceUrl}` : ATTRIBUTION;\n // A run of notes must never crowd out the answer they qualify, so they are\n // dropped from the tail until the body keeps a readable share of the block.\n const noteLines = (options.notes ?? []).map((note) => `Note: ${note}`);\n while (noteLines.length > 0 && noteLines.join(\"\\n\").length > MAX_TEXT_MIRROR_CHARS / 2) {\n noteLines.pop();\n }\n const trailer = [...noteLines, attribution].join(\"\\n\");\n\n const cutMarker = \"\\n\\n[shortened; the full result is in the structured output]\";\n const reserved = `\\n\\n${trailer}`.length;\n const budget = MAX_TEXT_MIRROR_CHARS - reserved;\n\n let text: string;\n if (body.length <= budget) {\n text = `${body}\\n\\n${trailer}`;\n } else {\n const kept = truncate(body, Math.max(0, budget - cutMarker.length));\n text = `${kept}${cutMarker}\\n\\n${trailer}`;\n }\n\n return { content: [{ type: \"text\", text }], structuredContent: structured };\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 McError\n ? error\n : new McError(\"network_error\", error instanceof Error ? error.message : String(error));\n\n const lines = [`[${known.code}] ${known.message}`];\n if (known.details.hint) lines.push(`Hint: ${known.details.hint}`);\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) return text;\n return `${text.slice(0, maxChars - 1).trimEnd()}…`;\n}\n\n/**\n * Cut a block of text at a line boundary, so a truncated description ends on a\n * sentence rather than mid-word. A single line longer than the budget is cut\n * hard, since there is no boundary to find.\n */\nexport function sliceAtLineBoundary(\n text: string,\n offset: number,\n maxChars: number,\n): { slice: string; nextOffset: number | null } {\n const rest = text.slice(offset);\n if (rest.length <= maxChars) return { slice: rest, nextOffset: null };\n\n const window = rest.slice(0, maxChars);\n const lastBreak = window.lastIndexOf(\"\\n\");\n let cut = lastBreak > 0 ? lastBreak : maxChars;\n\n // Never cut between the two halves of a surrogate pair: both pages would show\n // a replacement character and no offset could ever reassemble it.\n const code = rest.charCodeAt(cut - 1);\n if (code >= 0xd800 && code <= 0xdbff) cut -= 1;\n\n return { slice: rest.slice(0, cut), nextOffset: offset + cut };\n}\n\n/** Compact listing, showing what a model needs to pick the right entry. */\nexport function renderTitleList(titles: TitleSummaryOut[]): string {\n return titles\n .map((title, index) => {\n const scores = [\n title.metascore === null ? \"\" : `critics ${title.metascore}/100`,\n title.user_score === null ? \"\" : `users ${title.user_score}/10`,\n ].filter(Boolean);\n // Some titles already end with their year, such as \"Psycho (1960)\", so\n // appending it again would read as a stutter.\n const yearShown = title.year !== null && !title.title.includes(`(${title.year})`);\n const parts = [\n `${index + 1}. ${title.title}`,\n yearShown ? `(${title.year})` : \"\",\n `· ${title.kind}`,\n scores.length > 0 ? `· ${scores.join(\", \")}` : \"\",\n `· slug: ${title.slug}`,\n ];\n return parts.filter(Boolean).join(\" \");\n })\n .join(\"\\n\");\n}\n","/**\n * get_reviews: what individual critics and viewers wrote.\n *\n * The aggregate score is available everywhere. The individual verdicts, with\n * the publication that ran them and a link to the original article, are what\n * this source has and others do not.\n */\n\nimport { z } from \"zod\";\nimport type { McClient } from \"../mc/client.js\";\nimport type { Kind, Sentiment } from \"../types.js\";\nimport { titlePageUrl } from \"../mc/urls.js\";\nimport { kindSchema, ok, toToolError, truncate, type ToolResult } from \"./shared.js\";\n\n/** Quotes are third-party writing, so they are excerpts rather than reproductions. */\nconst MAX_QUOTE_CHARS = 600;\n\nexport const getReviewsDescription = [\n \"Read individual reviews of a Metacritic entry, from critics or from users.\",\n \"Get the slug and kind from search_titles first.\",\n \"Filter with 'sentiment' to read only what praised or panned it, which Metacritic groups itself.\",\n \"Metacritic returns a fixed sample rather than the full list, so 'total_available' is usually far larger than what comes back, and there is no way to page past the sample.\",\n \"Critic reviews carry the publication and a link to the original article: quote them with both.\",\n \"Critic scores run to 100 and user scores to 10, so do not average the two together.\",\n].join(\" \");\n\nexport const getReviewsInputShape = {\n slug: z.string().min(1).describe(\"Identifier from search_titles, such as 'the-matrix'.\"),\n kind: kindSchema,\n source: z\n .enum([\"critic\", \"user\"])\n .default(\"critic\")\n .describe(\"'critic' is the professional press, 'user' is the audience.\"),\n sentiment: z\n .enum([\"all\", \"positive\", \"neutral\", \"negative\"])\n .default(\"all\")\n .describe(\"Which slice to read. Metacritic groups reviews this way itself.\"),\n limit: z\n .number()\n .int()\n .min(1)\n .max(50)\n .default(10)\n .describe(\"How many reviews to return from the sample Metacritic publishes.\"),\n offset: z\n .number()\n .int()\n .min(0)\n .default(0)\n .describe(\"How many of the sampled reviews to skip. The sample itself cannot be paged past.\"),\n};\n\nconst reviewSchema = z.object({\n quote: z.string().nullable().describe(\"Excerpt as published, truncated if long.\"),\n score: z.number().nullable(),\n max: z.number().describe(\"Scale for this review's score: 100 for critics, 10 for users.\"),\n publication: z.string().nullable().describe(\"Who ran it. Name it when quoting.\"),\n author: z.string().nullable(),\n url: z.string().nullable().describe(\"Original article. Link it when quoting.\"),\n date: z.string().nullable(),\n});\n\nexport const getReviewsOutputShape = {\n slug: z.string(),\n kind: z.string(),\n source_url: z\n .string()\n .describe(\"Metacritic page these reviews belong to. Cite it when an article link is missing.\"),\n source: z.string(),\n sentiment: z.string(),\n reviews: z.array(reviewSchema),\n total_available: z\n .number()\n .int()\n .describe(\"How many reviews Metacritic counts in total. Usually far more than it serves here.\"),\n offset: z.number().int(),\n next_offset: z.number().int().nullable().describe(\"Pass as 'offset' for the next page.\"),\n notes: z.array(z.string()),\n};\n\nexport interface GetReviewsArgs {\n slug: string;\n kind: Kind;\n source: \"critic\" | \"user\";\n sentiment: Sentiment;\n limit: number;\n offset: number;\n}\n\nexport async function runGetReviews(client: McClient, args: GetReviewsArgs): Promise<ToolResult> {\n try {\n const sourceUrl = titlePageUrl(args.kind, args.slug.trim());\n const request = {\n kind: args.kind,\n slug: args.slug,\n sentiment: args.sentiment,\n limit: args.limit,\n offset: args.offset,\n };\n\n // The two sources return different shapes, and only critics carry a\n // publication and a link, which is why they are read separately.\n const max = args.source === \"critic\" ? 100 : 10;\n const { data, cached } =\n args.source === \"critic\"\n ? await client.getCriticReviews(request)\n : await client.getUserReviews(request);\n\n // Metacritic ignores limit and offset on this route and always answers with\n // the same sample, so the slicing happens here. Asking upstream for more\n // would spend a request and change nothing.\n const sampled = data.reviews.slice(args.offset, args.offset + args.limit);\n\n const reviews = sampled.map((review) => ({\n quote: review.quote === null ? null : truncate(review.quote, MAX_QUOTE_CHARS),\n score: review.score,\n max,\n publication: \"publication\" in review ? review.publication : null,\n author: \"author\" in review ? review.author : null,\n url: \"url\" in review ? review.url : null,\n date: review.date,\n }));\n\n const notes: string[] = [];\n if (cached) notes.push(\"Served from this server's short-lived in-memory cache.\");\n if (reviews.length === 0) {\n if (args.offset > 0 && args.offset >= data.reviews.length) {\n notes.push(\n `offset=${args.offset} is past the end of a sample of ${data.reviews.length} ${args.source} reviews. Call again with offset=0.`,\n );\n } else if (args.sentiment === \"all\") {\n notes.push(\"Metacritic publishes no review in this slice.\");\n } else {\n notes.push(\n `Metacritic publishes no ${args.sentiment} review here. Call again with sentiment=\"all\" to see the rest.`,\n );\n }\n }\n if (data.itemCount > data.reviews.length) {\n notes.push(\n `${data.itemCount - data.reviews.length} entries in the sample could not be attributed and were skipped.`,\n );\n }\n if (data.totalResults > data.reviews.length) {\n const slice = args.sentiment === \"all\" ? \"\" : ` in the ${args.sentiment} slice`;\n notes.push(\n `Metacritic counts ${data.totalResults} ${args.source} reviews for this entry and serves ${data.reviews.length}${slice} here. The rest are only on the website: ${sourceUrl}`,\n );\n }\n\n // Paging is within the sample, since the source will not serve past it.\n const consumed = args.offset + reviews.length;\n const nextOffset = consumed < data.reviews.length ? consumed : null;\n\n const listing = reviews\n .map((review, index) => {\n const who = [review.publication, review.author].filter(Boolean).join(\", \");\n const head = `${index + 1}. ${review.score === null ? \"\" : `${review.score}/${max} `}${who || \"anonymous\"}`;\n // Quotes are third-party writing and can contain blank lines, which\n // would otherwise sit flush against the server's own trailing lines and\n // read as if this server had written them. Indenting every line keeps\n // the boundary visible.\n const quoted = (review.quote ?? \"\")\n .split(\"\\n\")\n .map((line) => ` ${line}`)\n .join(\"\\n\");\n return `${head}\\n${quoted}\\n ${review.url ?? sourceUrl}`;\n })\n .join(\"\\n\");\n\n const summary =\n reviews.length === 0\n ? `No ${args.source} review to show for \"${args.slug}\".`\n : `${reviews.length} ${args.source} review${reviews.length === 1 ? \"\" : \"s\"} of \"${args.slug}\":\\n${listing}`;\n\n return ok(\n {\n slug: args.slug,\n kind: args.kind,\n source_url: sourceUrl,\n source: args.source,\n sentiment: args.sentiment,\n reviews,\n total_available: data.totalResults,\n offset: args.offset,\n next_offset: nextOffset,\n notes,\n },\n summary,\n { notes, sourceUrl },\n );\n } catch (error) {\n return toToolError(error);\n }\n}\n","/**\n * get_title: read one entry, section by section.\n *\n * The detail response runs to 46 KB for a film, so sections are opt-in and the\n * default pair answers the question people actually ask: what is this, and is\n * it any good.\n */\n\nimport { z } from \"zod\";\nimport type { McClient } from \"../mc/client.js\";\nimport { McError } from \"../errors.js\";\nimport type { Kind, ScoreSummary, TitleDetail, WatchOffer } from \"../types.js\";\nimport {\n kindSchema,\n ok,\n scoreSchema,\n sliceAtLineBoundary,\n titleSummarySchema,\n toScoreOut,\n toTitleSummaryOut,\n toToolError,\n type ToolResult,\n} from \"./shared.js\";\n\nconst SECTIONS = [\"basic\", \"scores\", \"awards\", \"production\", \"networks\", \"where_to_watch\"] as const;\ntype Section = (typeof SECTIONS)[number];\n\nexport const getTitleDescription = [\n \"Read one Metacritic entry by slug and kind, both from search_titles.\",\n \"Sections are opt-in because a full entry is large: 'basic' and 'scores' are the default and cover most questions.\",\n \"'scores' fetches the critic and audience breakdowns, each with its own scale, so never compare the two numbers directly.\",\n \"Sections gate the payload: asking for 'scores' alone returns no description, and 'basic' alone returns no scores.\",\n \"'networks' lists the broadcasters of a show; 'where_to_watch' costs an extra request and covers films and shows only.\",\n \"A long description paginates: when 'truncated' is true, call again with 'offset' set to 'next_offset'.\",\n].join(\" \");\n\nexport const getTitleInputShape = {\n slug: z.string().min(1).describe(\"Identifier from search_titles, such as 'the-matrix'.\"),\n kind: kindSchema,\n sections: z\n .array(z.enum(SECTIONS))\n .default([\"basic\", \"scores\"])\n .describe(\"Which parts to return. Each extra section beyond the default costs a request.\"),\n max_chars: z\n .number()\n .int()\n .min(200)\n .max(20000)\n .default(4000)\n .describe(\"Character budget for the description.\"),\n offset: z.number().int().min(0).default(0).describe(\"Where to resume the description.\"),\n};\n\nexport const getTitleOutputShape = {\n title: titleSummarySchema,\n description: z.string().nullable(),\n tagline: z.string().nullable(),\n genres: z.array(z.string()),\n duration_minutes: z\n .number()\n .int()\n .nullable()\n .describe(\"Runtime in minutes for a film, or the length of a typical episode for a show.\"),\n imdb_id: z\n .string()\n .nullable()\n .describe(\"IMDb identifier, usable to cross-reference other sources.\"),\n total_chars: z.number().int().describe(\"Length of the full description.\"),\n returned_chars: z.number().int(),\n offset: z.number().int(),\n next_offset: z.number().int().nullable().describe(\"Pass as 'offset' to read the rest.\"),\n truncated: z.boolean(),\n critic_score: scoreSchema.nullable(),\n user_score: scoreSchema.nullable(),\n awards: z\n .array(\n z.object({\n ceremony: z.string().describe(\"Awards body, such as 'Academy Awards, USA'.\"),\n wins: z.number().int().nullable().describe(\"Null when the source records no win.\"),\n nominations: z.number().int().nullable(),\n }),\n )\n .optional()\n .describe(\"One tally per ceremony. Metacritic publishes no per-category detail.\"),\n networks: z.array(z.string()).optional().describe(\"Broadcasters, for shows.\"),\n production: z.array(z.object({ name: z.string(), id: z.number().int().nullable() })).optional(),\n where_to_watch: z\n .array(z.object({ provider: z.string(), kind: z.string(), url: z.string().nullable() }))\n .optional(),\n notes: z.array(z.string()),\n};\n\nexport interface GetTitleArgs {\n slug: string;\n kind: Kind;\n sections: Section[];\n max_chars: number;\n offset: number;\n}\n\nexport async function runGetTitle(client: McClient, args: GetTitleArgs): Promise<ToolResult> {\n try {\n const wanted = new Set<Section>(args.sections);\n const notes: string[] = [];\n\n const detail = await client.getDetail(args.kind, args.slug);\n if (detail.cached) notes.push(\"Served from this server's short-lived in-memory cache.\");\n const item = detail.data;\n\n let criticScore: ScoreSummary | null = null;\n let userScore: ScoreSummary | null = null;\n if (wanted.has(\"scores\")) {\n [criticScore, userScore] = await Promise.all([\n optionalScore(client, args.kind, args.slug, \"critic\", notes),\n optionalScore(client, args.kind, args.slug, \"user\", notes),\n ]);\n }\n\n const basic = wanted.has(\"basic\");\n const full = basic ? (item.description ?? \"\") : \"\";\n const { slice, nextOffset } = sliceAtLineBoundary(full, args.offset, args.max_chars);\n if (basic && nextOffset !== null) {\n notes.push(\n `The description is longer than ${args.max_chars} characters. Call again with offset=${nextOffset} for the rest.`,\n );\n }\n if (basic && slice === \"\" && args.offset > 0 && full.length > 0) {\n notes.push(\n `offset=${args.offset} is past the end of a description of ${full.length} characters. Call again with offset=0 to read it from the start.`,\n );\n }\n\n // The audience score only exists on the scores route, so without that\n // section its absence here says nothing about the title. Saying so beats a\n // bare null the caller would read as \"Metacritic publishes none\".\n if (!wanted.has(\"scores\")) {\n notes.push(\n \"Scores were not requested, so critic_score and user_score are null here regardless of what Metacritic publishes. Add 'scores' to sections to read them.\",\n );\n }\n\n const structured: Record<string, unknown> = {\n title: {\n ...toTitleSummaryOut(item),\n metascore: criticScore?.score ?? item.metascore,\n user_score: userScore?.score ?? null,\n },\n description: basic && slice !== \"\" ? slice : null,\n tagline: basic ? item.tagline : null,\n genres: basic ? item.genres : [],\n duration_minutes: basic ? item.duration : null,\n imdb_id: basic ? item.imdbId : null,\n total_chars: basic ? full.length : 0,\n returned_chars: basic ? slice.length : 0,\n offset: args.offset,\n next_offset: basic ? nextOffset : null,\n truncated: basic && nextOffset !== null,\n critic_score: criticScore ? toScoreOut(criticScore) : null,\n user_score: userScore ? toScoreOut(userScore) : null,\n notes,\n };\n\n if (wanted.has(\"awards\")) structured.awards = item.awards;\n\n const PRODUCTION_CAP = 25;\n const production = wanted.has(\"production\") ? item.production.slice(0, PRODUCTION_CAP) : [];\n if (wanted.has(\"production\")) {\n structured.production = production;\n if (item.production.length > PRODUCTION_CAP) {\n notes.push(\n `${item.production.length} companies are credited and the first ${PRODUCTION_CAP} are shown. The list mixes producers with distributors and home-video labels across every territory.`,\n );\n }\n }\n\n if (wanted.has(\"networks\")) structured.networks = item.networks;\n const offers = wanted.has(\"where_to_watch\")\n ? await watchOffers(client, item, args.kind, notes)\n : [];\n if (wanted.has(\"where_to_watch\")) structured.where_to_watch = offers;\n\n return ok(structured, render(item, slice, criticScore, userScore, wanted, offers, production), {\n notes,\n sourceUrl: item.sourceUrl,\n });\n } catch (error) {\n return toToolError(error);\n }\n}\n\n/**\n * Read a score, distinguishing an absence from a failure.\n *\n * Only `not_found` means the site publishes no score for this entry. Every\n * other error is a failure to ask, and saying \"no score is published\" on the\n * back of a timeout states something false about the data. That distinction is\n * the whole rule this server is built on, and it has to hold here too.\n */\nasync function optionalScore(\n client: McClient,\n kind: Kind,\n slug: string,\n source: \"critic\" | \"user\",\n notes: string[],\n): Promise<ScoreSummary | null> {\n try {\n const { data } = await client.getScore(kind, slug, source);\n return data;\n } catch (error) {\n if (error instanceof McError && error.code === \"not_found\") {\n notes.push(`Metacritic publishes no ${source} score for this entry.`);\n } else {\n const reason = error instanceof McError ? error.code : \"an unexpected error\";\n notes.push(\n `The ${source} score could not be read (${reason}), so it is missing here rather than absent from Metacritic. Call again to retry.`,\n );\n }\n return null;\n }\n}\n\nasync function watchOffers(\n client: McClient,\n item: TitleDetail,\n kind: Kind,\n notes: string[],\n): Promise<WatchOffer[]> {\n if (kind === \"game\") {\n notes.push(\"Streaming offers do not apply to games.\");\n return [];\n }\n if (!item.imdbId) {\n notes.push(\"This entry carries no IMDb id, which is what streaming offers are keyed by.\");\n return [];\n }\n try {\n const { data } = await client.getWatchOffers(item.imdbId, kind);\n return data;\n } catch {\n notes.push(\"Streaming offers could not be read for this entry.\");\n return [];\n }\n}\n\n/**\n * The text block, built from the same sections the structured output was.\n *\n * A client that renders only text must see the answer to what it asked for and\n * nothing it did not: showing genres when 'basic' was not requested contradicts\n * the structured payload, and omitting the streaming offers hides a section the\n * caller paid a request for.\n */\nfunction render(\n item: TitleDetail,\n description: string,\n critic: ScoreSummary | null,\n user: ScoreSummary | null,\n wanted: Set<Section>,\n offers: WatchOffer[],\n production: Array<{ name: string }>,\n): string {\n // Some titles already carry their year, so it is only appended when absent.\n const yearShown = item.year !== null && !item.title.includes(`(${item.year})`);\n const header = [item.title, yearShown ? `(${item.year})` : \"\", `· ${item.kind}`]\n .filter(Boolean)\n .join(\" \");\n\n const lines = [header];\n if (wanted.has(\"basic\")) {\n if (item.genres.length > 0) lines.push(`Genres: ${item.genres.join(\", \")}`);\n if (item.duration !== null) lines.push(`Runtime: ${item.duration} min`);\n }\n if (critic?.score !== null && critic !== null) {\n lines.push(\n `Critics: ${critic.score}/${critic.max} from ${critic.reviewCount ?? \"?\"} reviews${critic.sentiment ? ` (${critic.sentiment})` : \"\"}`,\n );\n }\n if (user?.score !== null && user !== null) {\n lines.push(\n `Users: ${user.score}/${user.max} from ${user.reviewCount ?? \"?\"} ratings${user.sentiment ? ` (${user.sentiment})` : \"\"}`,\n );\n }\n if (description) lines.push(\"\", description);\n\n if (wanted.has(\"awards\") && item.awards.length > 0) {\n lines.push(\"\", \"Awards:\");\n for (const a of item.awards) {\n const tally = [\n a.wins === null ? \"\" : `${a.wins} win${a.wins === 1 ? \"\" : \"s\"}`,\n a.nominations === null\n ? \"\"\n : `${a.nominations} nomination${a.nominations === 1 ? \"\" : \"s\"}`,\n ].filter(Boolean);\n lines.push(` ${a.ceremony}${tally.length > 0 ? `: ${tally.join(\", \")}` : \"\"}`);\n }\n }\n\n if (wanted.has(\"networks\") && item.networks.length > 0) {\n lines.push(\"\", `Networks: ${item.networks.join(\", \")}`);\n }\n\n if (wanted.has(\"production\")) {\n lines.push(\n \"\",\n production.length === 0\n ? \"Production: no company credited.\"\n : `Production: ${production.map((company) => company.name).join(\", \")}`,\n );\n }\n\n if (wanted.has(\"where_to_watch\")) {\n lines.push(\"\", offers.length === 0 ? \"Where to watch: nothing listed.\" : \"Where to watch:\");\n for (const o of offers) lines.push(` ${o.provider} (${o.kind})${o.url ? ` — ${o.url}` : \"\"}`);\n }\n\n return lines.join(\"\\n\");\n}\n","/**\n * search_titles: find an entry and its slug.\n *\n * Rows carry the critic Metascore, so the common question about the critical\n * verdict is answered in one call. They carry no audience score, because the\n * search route does not report one.\n */\n\nimport { z } from \"zod\";\nimport type { McClient } from \"../mc/client.js\";\nimport type { Kind } from \"../types.js\";\nimport {\n ok,\n renderTitleList,\n titleSummarySchema,\n toTitleSummaryOut,\n toToolError,\n type ToolResult,\n} from \"./shared.js\";\n\nexport const searchTitlesDescription = [\n \"Search Metacritic for films, shows and games by title.\",\n \"Returns one compact row per match, carrying the critic Metascore, so a question about the critical verdict needs no second call.\",\n \"Search rows carry no audience score: use get_title for that, or browse_titles, which does return it.\",\n \"Use the slug and kind with get_title for the full entry, or with get_reviews for what critics wrote.\",\n \"There is no paging: 'limit' is the only lever, and results always start from the most relevant match.\",\n \"This searches titles only. It cannot find an entry from a plot detail, a person or a studio.\",\n].join(\" \");\n\nexport const searchTitlesInputShape = {\n query: z.string().min(1).describe(\"Title or part of one, for example 'the matrix'.\"),\n kind: z\n .enum([\"movie\", \"show\", \"game\", \"any\"])\n .default(\"any\")\n .describe(\"Restrict results to one catalogue. Metacritic searches all three at once.\"),\n limit: z.number().int().min(1).max(50).default(10).describe(\"How many rows to return.\"),\n};\n\nexport const searchTitlesOutputShape = {\n query: z.string(),\n results: z.array(titleSummarySchema),\n total_available: z\n .number()\n .int()\n .describe(\n \"How many entries Metacritic counted. For a query of several words it counts entries \" +\n \"matching any one of them, so the number is far larger than the useful matches and should \" +\n \"not be read as a match count.\",\n ),\n notes: z.array(z.string()),\n};\n\nexport interface SearchTitlesArgs {\n query: string;\n kind: Kind | \"any\";\n limit: number;\n}\n\nexport async function runSearchTitles(\n client: McClient,\n args: SearchTitlesArgs,\n): Promise<ToolResult> {\n try {\n // The upstream search has no type filter, so a narrowed search must ask for\n // more rows than requested and filter here, or a kind with few matches would\n // come back empty while results exist further down the list.\n const upstreamLimit = args.kind === \"any\" ? args.limit : Math.min(50, args.limit * 3);\n const { data, cached } = await client.search(args.query, upstreamLimit, 0);\n\n const matching =\n args.kind === \"any\" ? data.titles : data.titles.filter((title) => title.kind === args.kind);\n const results = matching.slice(0, args.limit).map(toTitleSummaryOut);\n\n // Filtering by kind happens here, over a fixed upstream window, so the\n // ceiling is what that window held rather than what the catalogue holds.\n const windowExhausted = args.kind !== \"any\" && matching.length === results.length;\n\n const notes: string[] = [];\n if (cached) notes.push(\"Served from this server's short-lived in-memory cache.\");\n\n // A query of several words is counted loosely upstream: \"the matrix\" reports\n // over 55 000 entries because it counts anything matching either word. Saying\n // \"narrow the query\" on the back of that number would be advice built on a\n // count that does not mean what it appears to mean.\n const looseCount = /\\s/.test(args.query.trim());\n if (looseCount) {\n notes.push(\n \"Metacritic counts a multi-word query loosely, so total_available is far larger than the number of real matches. The rows themselves are ordered by relevance.\",\n );\n } else if (data.totalResults > results.length) {\n notes.push(\n `${data.totalResults} entries matched and ${results.length} are shown. Raise 'limit' for more, or narrow the query: this tool cannot page.`,\n );\n }\n if (results.length === 0) {\n notes.push(\n args.kind === \"any\"\n ? \"No entry matched. Metacritic matches on the title alone, so try a shorter fragment of it.\"\n : `No ${args.kind} matched. Other kinds may still match: call again with kind=\"any\".`,\n );\n } else if (windowExhausted && results.length < args.limit) {\n notes.push(\n `Every ${args.kind} in the window Metacritic returned is shown. There may be more further down its ranking, which this tool cannot reach.`,\n );\n }\n\n const summary =\n results.length === 0\n ? `No Metacritic entry matched \"${args.query}\".`\n : `${results.length} entr${results.length === 1 ? \"y\" : \"ies\"} for \"${args.query}\":\\n${renderTitleList(results)}`;\n\n return ok({ query: args.query, results, total_available: data.totalResults, notes }, summary, {\n notes,\n });\n } catch (error) {\n return toToolError(error);\n }\n}\n"],"mappings":"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","names":["z","z","z","z","z","z","z","z","z","z"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/version.ts","../src/config.ts","../src/server.ts","../src/mc/cache.ts","../src/errors.ts","../src/mc/rateLimiter.ts","../src/mc/http.ts","../src/mc/paths.ts","../src/mc/urls.ts","../src/mc/parse.ts","../src/mc/client.ts","../src/tools/browseTitles.ts","../src/tools/shared.ts","../src/tools/getReviews.ts","../src/tools/getTitle.ts","../src/tools/searchTitles.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * Entry point: an MCP server for Metacritic 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-metacritic] fatal: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}\\n`,\n );\n process.exit(1);\n});\n","/**\n * Kept in sync with package.json and server.json. The User-Agent embeds it, so\n * a caller reading their logs can tell which build reached them.\n */\nexport const PKG_VERSION = \"1.1.0\";\nexport const REPO_URL = \"https://github.com/smeet666/mcp-metacritic\";\n","/**\n * Runtime configuration, read from environment variables.\n *\n * A bad value never crashes the process: an MCP server that dies at startup\n * because of a typo in a client config file is very hard to diagnose from the\n * host application, so invalid input is reported on stderr and ignored.\n */\n\nimport { PKG_VERSION, REPO_URL } from \"./version.js\";\n\nexport type LogLevel = \"silent\" | \"error\" | \"info\" | \"debug\";\n\nexport interface Config {\n userAgent: string;\n minIntervalMs: number;\n timeoutMs: number;\n maxRetries: number;\n cacheTtlMs: number;\n scoresCacheTtlMs: number;\n cacheMaxEntries: number;\n logLevel: LogLevel;\n}\n\nexport const DEFAULT_USER_AGENT = `mcp-metacritic v${PKG_VERSION} (${REPO_URL})`;\n\nexport const DEFAULTS = {\n minIntervalMs: 1000,\n timeoutMs: 15_000,\n maxRetries: 3,\n // A catalogue entry moves when a title is edited, which is rare, so a day is\n // safe. The cache lives in memory and dies with the process, so this is a\n // ceiling on one session rather than a stored copy of the site.\n cacheTtlMs: 24 * 60 * 60 * 1000,\n // Scores and reviews move as reviews come in, especially around a release,\n // so they get their own much shorter lifetime.\n scoresCacheTtlMs: 60 * 60 * 1000,\n cacheMaxEntries: 200,\n logLevel: \"error\" as LogLevel,\n};\n\n/**\n * Floor on the request interval, enforced regardless of configuration.\n *\n * Metacritic publishes no rate limit and did not throttle ten consecutive\n * requests during testing. That silence is a reason to pace deliberately rather\n * than an invitation to go fast, so a value below this floor is refused.\n */\nexport const MIN_ALLOWED_INTERVAL_MS = 500;\n\n/** Ceiling on the request interval, so a typo cannot stall the server for hours. */\nexport const MAX_ALLOWED_INTERVAL_MS = 60_000;\n\nconst LOG_LEVELS: LogLevel[] = [\"silent\", \"error\", \"info\", \"debug\"];\n\ninterface NumericRange {\n min: number;\n max: number;\n fallback: number;\n}\n\nfunction readNumber(name: string, env: NodeJS.ProcessEnv, range: NumericRange): number {\n const raw = env[name];\n if (raw === undefined || raw.trim() === \"\") return range.fallback;\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\n // Out-of-range values fall back to the default rather than being clamped to\n // the nearest bound. Clamping turns a typo into a silent behaviour change:\n // -1 retries becomes \"never retry\", and -1 cache entries disables the cache\n // outright, both of which look like working configuration.\n const rounded = Math.round(parsed);\n if (rounded < range.min || rounded > range.max) {\n warn(\n `${name}=${raw} is outside the accepted range ${range.min}-${range.max} and was ignored; ` +\n `using ${range.fallback}`,\n );\n return range.fallback;\n }\n return rounded;\n}\n\nfunction warn(message: string): void {\n process.stderr.write(`[mcp-metacritic] ${message}\\n`);\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 1000, they were asking for no\n * 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.MC_MIN_INTERVAL_MS;\n if (raw === undefined || raw.trim() === \"\") return DEFAULTS.minIntervalMs;\n\n const parsed = Number(raw);\n if (!Number.isFinite(parsed)) {\n warn(`MC_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 `MC_MIN_INTERVAL_MS=${raw} is below the ${MIN_ALLOWED_INTERVAL_MS}ms floor and was ignored; ` +\n `using ${DEFAULTS.minIntervalMs}ms. Metacritic publishes no rate limit, so this client ` +\n `sets its own.`,\n );\n return DEFAULTS.minIntervalMs;\n }\n\n // The upper bound is a guard against a typo that would stall the server for\n // hours. Unlike readNumber, it clamps rather than falling back, because the\n // default would be far faster than the value asked for: someone who wrote ten\n // minutes wants slow, and answering that with 1000ms gets politeness backwards.\n if (rounded > MAX_ALLOWED_INTERVAL_MS) {\n warn(\n `MC_MIN_INTERVAL_MS=${raw} exceeds the ${MAX_ALLOWED_INTERVAL_MS}ms ceiling; ` +\n `using ${MAX_ALLOWED_INTERVAL_MS}ms.`,\n );\n return MAX_ALLOWED_INTERVAL_MS;\n }\n\n return rounded;\n}\n\nexport function loadConfig(env: NodeJS.ProcessEnv = process.env): Config {\n const rawUserAgent = env.MC_USER_AGENT?.trim();\n const rawLogLevel = env.MC_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(`MC_LOG_LEVEL=\"${rawLogLevel}\" is unknown, using \"${DEFAULTS.logLevel}\"`);\n }\n }\n\n return {\n userAgent: rawUserAgent || DEFAULT_USER_AGENT,\n minIntervalMs: readInterval(env),\n timeoutMs: readNumber(\"MC_TIMEOUT_MS\", env, {\n min: 1000,\n max: 120_000,\n fallback: DEFAULTS.timeoutMs,\n }),\n maxRetries: readNumber(\"MC_MAX_RETRIES\", env, {\n min: 0,\n max: 10,\n fallback: DEFAULTS.maxRetries,\n }),\n cacheTtlMs: readNumber(\"MC_CACHE_TTL_MS\", env, {\n min: 0,\n max: 7 * 24 * 60 * 60 * 1000,\n fallback: DEFAULTS.cacheTtlMs,\n }),\n scoresCacheTtlMs: readNumber(\"MC_SCORES_CACHE_TTL_MS\", env, {\n min: 0,\n max: 24 * 60 * 60 * 1000,\n fallback: DEFAULTS.scoresCacheTtlMs,\n }),\n cacheMaxEntries: readNumber(\"MC_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-metacritic] ${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 pair of caches are shared by all tools,\n * so the self-imposed pacing applies to the server as a whole rather than being\n * reset by each tool in turn.\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 { McClient } from \"./mc/client.js\";\nimport {\n browseTitlesDescription,\n browseTitlesInputShape,\n browseTitlesOutputShape,\n runBrowseTitles,\n} from \"./tools/browseTitles.js\";\nimport type { BrowseTitlesArgs } from \"./tools/browseTitles.js\";\nimport {\n getReviewsDescription,\n getReviewsInputShape,\n getReviewsOutputShape,\n runGetReviews,\n} from \"./tools/getReviews.js\";\nimport type { GetReviewsArgs } from \"./tools/getReviews.js\";\nimport {\n getTitleDescription,\n getTitleInputShape,\n getTitleOutputShape,\n runGetTitle,\n} from \"./tools/getTitle.js\";\nimport type { GetTitleArgs } from \"./tools/getTitle.js\";\nimport {\n runSearchTitles,\n searchTitlesDescription,\n searchTitlesInputShape,\n searchTitlesOutputShape,\n} from \"./tools/searchTitles.js\";\nimport type { SearchTitlesArgs } from \"./tools/searchTitles.js\";\nimport { PKG_VERSION } from \"./version.js\";\n\nexport interface CreateServerOptions {\n config?: Config;\n logger?: Logger;\n fetchImpl?: typeof fetch;\n}\n\n/** This server only reads, so every tool is read-only. */\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 McClient({\n config,\n logger,\n ...(options.fetchImpl ? { fetchImpl: options.fetchImpl } : {}),\n });\n\n const server = new McpServer(\n { name: \"mcp-metacritic\", version: PKG_VERSION },\n {\n instructions:\n \"Tools for Metacritic: films, shows and games, their scores, and the reviews behind them. \" +\n \"No API key is needed. Typical flow: search_titles to find an entry and its slug and kind, \" +\n \"then get_title for the entry or get_reviews for what critics wrote. search_titles carries \" +\n \"the critic Metascore, so a question about the critical verdict needs no second call, but \" +\n \"it carries no audience score: read that with get_title, or with browse_titles. \" +\n \"The two scores are on different scales: the critic Metascore runs to 100 and the audience \" +\n \"score to 10, so never compare or average them without rescaling; every score is returned \" +\n \"with its own 'max' for that reason. \" +\n \"Use browse_titles when there is no title to look up, such as best rated or newest releases. \" +\n \"When you repeat a review, name the publication and link the original article, and when you \" +\n \"cite a score, credit Metacritic and link the entry: every result carries a source_url.\",\n },\n );\n\n server.registerTool(\n \"search_titles\",\n {\n title: \"Search Metacritic\",\n description: searchTitlesDescription,\n inputSchema: z.object(searchTitlesInputShape),\n outputSchema: z.object(searchTitlesOutputShape),\n annotations: READ_ONLY,\n },\n async (args) => runSearchTitles(client, args as SearchTitlesArgs),\n );\n\n server.registerTool(\n \"get_title\",\n {\n title: \"Read an entry\",\n description: getTitleDescription,\n inputSchema: z.object(getTitleInputShape),\n outputSchema: z.object(getTitleOutputShape),\n annotations: READ_ONLY,\n },\n async (args) => runGetTitle(client, args as GetTitleArgs),\n );\n\n server.registerTool(\n \"get_reviews\",\n {\n title: \"Read reviews\",\n description: getReviewsDescription,\n inputSchema: z.object(getReviewsInputShape),\n outputSchema: z.object(getReviewsOutputShape),\n annotations: READ_ONLY,\n },\n async (args) => runGetReviews(client, args as GetReviewsArgs),\n );\n\n server.registerTool(\n \"browse_titles\",\n {\n title: \"Browse rankings\",\n description: browseTitlesDescription,\n inputSchema: z.object(browseTitlesInputShape),\n outputSchema: z.object(browseTitlesOutputShape),\n annotations: READ_ONLY,\n },\n async (args) => runBrowseTitles(client, args as BrowseTitlesArgs),\n );\n\n logger.info(\n `ready: user-agent=\"${config.userAgent}\", min interval ${config.minIntervalMs}ms, ` +\n `catalogue cache ${config.cacheTtlMs}ms, scores cache ${config.scoresCacheTtlMs}ms`,\n );\n\n return server;\n}\n","/**\n * In-memory TTL + LRU cache.\n *\n * Nothing is written to disk. The cache exists so that repeated questions about\n * the same title inside one conversation do not hit Metacritic again, not to\n * build a local copy of their catalogue.\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) return undefined;\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) return;\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) break;\n this.store.delete(oldest.value);\n }\n }\n\n get size(): number {\n return this.store.size;\n }\n\n clear(): void {\n this.store.clear();\n }\n}\n","/**\n * Error taxonomy surfaced to the calling model.\n *\n * The rule this file exists to enforce: a failure must never be reported as an\n * empty result. A model that sees \"no title found\" cannot tell that apart from\n * a genuine absence, and will confidently tell the user the film does not exist.\n *\n * Metacritic makes that distinction readable. A successful response carries a\n * `data` object; a failure carries an `errors` array instead, with its own code.\n * Anything matching neither shape is a parse failure rather than an empty answer.\n */\n\nexport type ErrorCode =\n \"not_found\" | \"invalid_input\" | \"rate_limited\" | \"parse_failure\" | \"network_error\" | \"timeout\";\n\nexport interface ErrorDetails {\n url?: string;\n status?: number;\n retryAfterMs?: number;\n hint?: string;\n}\n\nexport class McError extends Error {\n constructor(\n readonly code: ErrorCode,\n message: string,\n readonly details: ErrorDetails = {},\n ) {\n super(message);\n this.name = \"McError\";\n }\n}\n\nconst ISSUES_URL = \"https://github.com/smeet666/mcp-metacritic/issues\";\n\nexport function notFound(url: string, what: string): McError {\n return new McError(\"not_found\", `Metacritic has no entry for ${what}.`, {\n url,\n status: 404,\n hint: \"Use search_titles to find an entry, then pass its slug and kind to this tool.\",\n });\n}\n\nexport function invalidInput(message: string, hint?: string): McError {\n return new McError(\"invalid_input\", message, hint ? { hint } : {});\n}\n\nexport function rateLimited(url: string, retryAfterMs: number): McError {\n return new McError(\n \"rate_limited\",\n \"Metacritic is refusing requests from this client for now. This does NOT mean the title is missing.\",\n {\n url,\n retryAfterMs,\n hint:\n `Wait about ${Math.ceil(retryAfterMs / 1000)} seconds, then call the same tool again with the ` +\n \"same arguments. If it keeps happening, raise MC_MIN_INTERVAL_MS.\",\n },\n );\n}\n\n/**\n * Raised when a response arrives but does not have the shape this server reads.\n *\n * Metacritic publishes no API documentation, so its response shapes can change\n * without notice. Failing here is what turns such a change into a report rather\n * than into answers that look empty.\n */\nexport function parseFailure(url: string, what: string): McError {\n return new McError(\n \"parse_failure\",\n `Metacritic answered, but the response did not have the expected shape (${what}).`,\n { url, hint: `Please report this, with the request you made, at ${ISSUES_URL}` },\n );\n}\n\nexport function upstreamError(url: string, status: number): McError {\n return new McError(\"network_error\", `Metacritic returned HTTP ${status}.`, {\n url,\n status,\n ...(status >= 500\n ? { hint: \"This is a problem on Metacritic's side. Try again shortly.\" }\n : {}),\n });\n}\n","/**\n * Serial request queue with an adaptive minimum interval.\n *\n * Metacritic publishes no rate limit and did not throttle consecutive requests\n * during testing, so the gap between requests is a choice this client makes\n * rather than a rule it is given. Requests run one at a time with a floor on\n * that gap. If the site ever does push back, the interval doubles and then\n * decays back down as requests succeed, which recovers faster than a fixed\n * delay and behaves better than a constant rate.\n */\n\nexport interface RateLimiterOptions {\n minIntervalMs: number;\n maxIntervalMs?: number;\n}\n\nconst DEFAULT_MAX_INTERVAL_MS = 10_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 /**\n * Queue a task. Tasks run in call order, one at a time.\n *\n * A task may send several requests, since the retry loop lives inside its\n * slot. It must call `beforeRequest` around each of them: the pacing is\n * measured between requests, and stamping only the start of the task would\n * let the next one follow a long retry chain's final request immediately.\n */\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, so every\n * request is paced from the previous one whether or not they belong to the\n * same retry chain.\n */\n async beforeRequest(): Promise<void> {\n await this.waitForSlot();\n this.lastStart = Date.now();\n }\n\n /** Called after the site signals it is under load. */\n penalize(): void {\n const next = this.intervalMs === 0 ? 250 : this.intervalMs * 2;\n this.intervalMs = Math.min(this.maxIntervalMs, next);\n }\n\n /** Called after a success, so a single hiccup 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) return;\n const elapsed = Date.now() - this.lastStart;\n // Clamped to the interval: a clock stepped backwards, by NTP or a resumed\n // VM, would otherwise make this wait for the size of the step, and the\n // queue is serial so every pending request would wait behind it.\n const remaining = Math.min(this.intervalMs, this.intervalMs - elapsed);\n if (remaining > 0) await sleep(remaining);\n }\n}\n\nexport function sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n","/**\n * HTTP layer: one GET, with backoff.\n *\n * Status codes are meaningful here, so they are read: 404 marks an unknown\n * slug, 429 and 503 mark push-back, and 5xx is worth retrying. What this layer\n * deliberately does not do is inspect the body, which is the parser's job.\n *\n * Metacritic advertises no rate limit and did not throttle during testing, so\n * the pacing is entirely self-imposed and the retry path is written for the day\n * that changes rather than for observed behaviour.\n */\n\nimport type { Config, Logger } from \"../config.js\";\nimport { McError, notFound, parseFailure, rateLimited, upstreamError } from \"../errors.js\";\nimport { RateLimiter, sleep } from \"./rateLimiter.js\";\n\nconst BACKOFF_BASE_MS = 2000;\nconst BACKOFF_FACTOR = 2;\nconst BACKOFF_MAX_MS = 20_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 capped = Math.min(BACKOFF_MAX_MS, BACKOFF_BASE_MS * BACKOFF_FACTOR ** attempt);\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 URL as text, retrying transient conditions.\n *\n * The retry loop runs inside a single limiter slot, so a queued request cannot\n * interleave with a chain that is backing off. Each attempt claims its own slot\n * through `beforeRequest`, which is what keeps the pacing honest between the\n * last request of one chain and the first request of the next.\n */\nexport async function fetchText(url: string, deps: HttpDeps): Promise<string> {\n const { config, limiter, logger } = deps;\n const doFetch = deps.fetchImpl ?? fetch;\n\n return limiter.schedule(async () => {\n let lastError: McError | undefined;\n\n // Set when the site says how long to stay away; it replaces our own guess\n // for the next attempt. Applied here rather than where it is read, so no\n // wait 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 = askedWaitMs ?? backoffDelay(attempt - 1);\n askedWaitMs = null;\n logger.info(`retry ${attempt}/${config.maxRetries} in ${delay}ms for ${url}`);\n await sleep(Math.min(delay, BACKOFF_MAX_MS));\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/json,*/*;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, url);\n logger.debug(`${lastError.code} for ${url}: ${lastError.message}`);\n continue;\n }\n\n if (status === 429 || status === 503 || status === 403) {\n limiter.penalize();\n // A server that says when to come back knows better than our own guess.\n askedWaitMs = retryAfterMs;\n lastError = rateLimited(url, retryAfterMs ?? backoffDelay(attempt));\n logger.info(\n `refused on ${url} with ${status}, interval now ${limiter.currentIntervalMs}ms`,\n );\n continue;\n }\n if (status >= 500) {\n lastError = upstreamError(url, status);\n continue;\n }\n // An unknown slug is answered with a 404 carrying an error envelope. It is\n // reported as an absence rather than as a transport problem, because the\n // caller asked for something specific and needs to know it is not there.\n if (status === 404) throw notFound(url, \"that request\");\n if (status >= 400) throw upstreamError(url, status);\n\n // An empty body is not a valid answer from any of these endpoints, and is\n // how a stressed edge sometimes refuses. Retrying is safer than handing an\n // empty document to the parser, which would read as \"nothing found\".\n const trimmed = body.trim();\n if (trimmed === \"\") {\n limiter.penalize();\n lastError = rateLimited(url, backoffDelay(attempt));\n logger.info(`empty body on ${url}, treating as a refusal`);\n continue;\n }\n\n // Every route answers with JSON. An HTML body under a success status is\n // the edge answering instead of the application, usually a challenge or\n // an error page, and it clears on its own. Retrying is right; reporting a\n // parse failure would send the caller to the bug tracker for an outage.\n if (!trimmed.startsWith(\"{\") && !trimmed.startsWith(\"[\")) {\n limiter.penalize();\n // Retried because an edge challenge clears on its own, but reported as a\n // parse failure if it never does: a body that is persistently not JSON\n // means the shape moved, and that is worth a bug report.\n lastError = parseFailure(url, \"the body is not JSON\");\n askedWaitMs = retryAfterMs;\n logger.info(`non-JSON body on ${url}, retrying`);\n continue;\n }\n\n limiter.relax();\n return body;\n }\n\n throw lastError ?? new McError(\"network_error\", `Could not fetch ${url}.`, { url });\n });\n}\n\n/** `Retry-After` carries either seconds or an HTTP date. */\nfunction parseRetryAfter(raw: string | null): number | null {\n if (!raw) return null;\n const seconds = Number(raw.trim());\n if (Number.isFinite(seconds) && seconds >= 0) return Math.round(seconds * 1000);\n const when = Date.parse(raw);\n if (Number.isNaN(when)) return null;\n return Math.max(0, when - Date.now());\n}\n\nfunction asTransportError(error: unknown, url: string): McError {\n if (error instanceof McError) return error;\n const name = error instanceof Error ? error.name : \"\";\n if (name === \"TimeoutError\" || name === \"AbortError\") {\n return new McError(\"timeout\", \"Metacritic did not answer in time.\", {\n url,\n hint: \"Raise MC_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 McError(\"network_error\", `Could not reach Metacritic: ${message}`, { url });\n}\n","/**\n * Every route and every response field this server reads, in one place.\n *\n * Metacritic documents none of this: the routes are the ones its own web\n * frontend calls, and the field names come from reading those responses. That\n * makes a rename upstream both likely and silent, so collecting them here keeps\n * the repair to a single file, and the live canary asserts each one by name so\n * a change surfaces as a named failure rather than as empty answers.\n */\n\nexport const API_BASE = \"https://backend.metacritic.com\";\nexport const SITE_BASE = \"https://www.metacritic.com\";\n\n/**\n * The API and the website disagree on plurality: the backend serves /movies/,\n * /shows/ and /games/, while the pages a reader should be linked to live at\n * /movie/, /tv/ and /game/. Both forms are needed and neither can be derived\n * from the other, so both are written out.\n */\nexport const API_SEGMENT = {\n movie: \"movies\",\n show: \"shows\",\n game: \"games\",\n} as const;\n\nexport const SITE_SEGMENT = {\n movie: \"movie\",\n show: \"tv\",\n game: \"game\",\n} as const;\n\n/** Internal type ids the browse route filters on. */\nexport const MCO_TYPE_ID = {\n show: 1,\n movie: 2,\n game: 13,\n} as const;\n\n/** The `type` string search results carry, which does not match our own naming. */\nexport const SEARCH_TYPE = {\n movie: \"movie\",\n show: \"show\",\n game: \"game-title\",\n} as const;\n\n/** Sort orders the browse route accepts. */\nexport const SORT_BY = {\n score: \"-metaScore\",\n recent: \"-releaseDate\",\n popular: \"-popularityCount\",\n} as const;\n\n/**\n * Component identifiers the backend requires on several routes. They describe\n * the widget the response was built for, and requests omitting them are refused.\n */\nexport const COMPONENT = {\n product: { componentName: \"product\", componentType: \"Product\" },\n criticStats: { componentName: \"critic-score-summary\", componentType: \"MetaScoreSummary\" },\n userStats: { componentName: \"user-score-summary\", componentType: \"MetaScoreSummary\" },\n criticReviews: { componentName: \"critic-reviews\", componentType: \"ProductReviewsSummary\" },\n userReviews: { componentName: \"user-reviews\", componentType: \"ProductReviewsSummary\" },\n} as const;\n\n/** Field names read off responses. */\nexport const FIELD = {\n data: \"data\",\n errors: \"errors\",\n item: \"item\",\n items: \"items\",\n totalResults: \"totalResults\",\n /** Reviews arrive pre-sorted into these buckets, which is the sentiment filter. */\n reviewBuckets: { all: \"default\", positive: \"positive\", neutral: \"neutral\", negative: \"negative\" },\n} as const;\n","/**\n * URL building.\n *\n * The API lives on a backend host, while the pages a reader should be sent to\n * live on the main site. Attribution links must point at the latter, which is\n * the only thing this server gives back in exchange for the data.\n */\n\nimport { invalidInput } from \"../errors.js\";\nimport type { Kind, Sentiment } from \"../types.js\";\nimport {\n API_BASE,\n API_SEGMENT,\n COMPONENT,\n MCO_TYPE_ID,\n SITE_BASE,\n SITE_SEGMENT,\n SORT_BY,\n} from \"./paths.js\";\n\ntype Params = Record<string, string | number | undefined>;\n\nfunction build(path: string, params: Params): string {\n const query = new URLSearchParams();\n for (const [key, value] of Object.entries(params)) {\n if (value !== undefined && value !== \"\") query.set(key, String(value));\n }\n const suffix = query.toString();\n return suffix ? `${API_BASE}${path}?${suffix}` : `${API_BASE}${path}`;\n}\n\n/**\n * Slugs go into the path, so a malformed one would silently change which route\n * is called. Rejecting here keeps that from reaching the network.\n */\nfunction checkSlug(slug: string): string {\n const trimmed = slug.trim();\n if (trimmed === \"\") {\n throw invalidInput(\"The slug is empty.\", \"Take it from a search_titles result.\");\n }\n if (!/^[a-z0-9][a-z0-9-]*$/i.test(trimmed)) {\n throw invalidInput(\n `\"${slug}\" does not look like a Metacritic slug.`,\n \"Slugs are lowercase words joined by hyphens, such as the-matrix. Use search_titles to get one.\",\n );\n }\n return trimmed;\n}\n\nexport function searchUrl(query: string, limit: number, offset: number): string {\n const trimmed = query.trim();\n if (trimmed === \"\") {\n throw invalidInput(\"The search query is empty.\", \"Pass a title or part of one.\");\n }\n return build(`/finder/metacritic/search/${encodeURIComponent(trimmed)}/web`, { limit, offset });\n}\n\nexport function browseUrl(options: {\n kind: Kind;\n sort: keyof typeof SORT_BY;\n genre?: string;\n limit: number;\n offset: number;\n}): string {\n return build(\"/finder/metacritic/web\", {\n sortBy: SORT_BY[options.sort],\n mcoTypeId: MCO_TYPE_ID[options.kind],\n genres: options.genre,\n limit: options.limit,\n offset: options.offset,\n });\n}\n\nexport function detailUrl(kind: Kind, slug: string): string {\n return build(`/${API_SEGMENT[kind]}/metacritic/${checkSlug(slug)}/web`, COMPONENT.product);\n}\n\nexport function scoreUrl(kind: Kind, slug: string, source: \"critic\" | \"user\"): string {\n const component = source === \"critic\" ? COMPONENT.criticStats : COMPONENT.userStats;\n return build(\n `/reviews/metacritic/${source}/${API_SEGMENT[kind]}/${checkSlug(slug)}/stats/web`,\n component,\n );\n}\n\n/**\n * Reviews for one entry.\n *\n * No limit or offset is sent. The route ignores both and always answers with\n * the same sample, so including them would only vary the URL, and with it the\n * cache key, making every page of a sample already held in memory cost a fresh\n * request. Paging happens over the sample the caller already has.\n */\nexport function reviewsUrl(options: {\n kind: Kind;\n slug: string;\n source: \"critic\" | \"user\";\n}): string {\n const component = options.source === \"critic\" ? COMPONENT.criticReviews : COMPONENT.userReviews;\n return build(\n `/reviews/metacritic/${options.source}/${API_SEGMENT[options.kind]}/${checkSlug(options.slug)}/summary/web`,\n component,\n );\n}\n\n/** Streaming offers, keyed by IMDb id rather than by Metacritic slug. */\nexport function watchUrl(imdbId: string, kind: Kind): string {\n const trimmed = imdbId.trim();\n if (!/^tt\\d+$/.test(trimmed)) {\n throw invalidInput(\n `\"${imdbId}\" is not an IMDb id.`,\n \"IMDb ids look like tt0133093. get_title returns one as imdb_id.\",\n );\n }\n return build(\"/justwatch/metacritic/offersByIMDbId\", {\n id: trimmed,\n type: kind === \"show\" ? \"show\" : \"movie\",\n });\n}\n\n/** The page a reader should be sent to, and the link attribution requires. */\nexport function titlePageUrl(kind: Kind, slug: string): string {\n return `${SITE_BASE}/${SITE_SEGMENT[kind]}/${slug}/`;\n}\n\n/** Which pre-sorted bucket of reviews to read. */\nexport function bucketFor(sentiment: Sentiment): string {\n return sentiment === \"all\" ? \"default\" : sentiment;\n}\n","/**\n * Responses to domain types.\n *\n * Every route answers with the same envelope: `{\"data\": …}` on success, or\n * `{\"errors\":[{\"code\":404,…}]}` on failure. Reading that envelope is the whole\n * job of the first half of this file, because getting it wrong is how a failure\n * turns into an answer that looks empty.\n *\n * The second half maps fields. Listing rows are trimmed here rather than in a\n * tool, so the heavy payload has no path out of this module.\n */\n\nimport { notFound, parseFailure } from \"../errors.js\";\nimport type {\n Award,\n Company,\n CriticReview,\n Kind,\n ReviewPage,\n ScoreSummary,\n Sentiment,\n TitleDetail,\n TitlePage,\n TitleSummary,\n UserReview,\n WatchOffer,\n} from \"../types.js\";\nimport { FIELD, SEARCH_TYPE } from \"./paths.js\";\nimport { bucketFor, titlePageUrl } from \"./urls.js\";\n\ntype Json = Record<string, unknown>;\n\nconst isObject = (value: unknown): value is Json =>\n typeof value === \"object\" && value !== null && !Array.isArray(value);\n\n/**\n * Read the envelope and return the payload.\n *\n * An `errors` array is the site saying it could not serve the request, which is\n * an absence when the caller named something specific. Anything that is neither\n * shape means the response is not what this server knows how to read.\n */\nexport function envelope(raw: string, url: string, what: string): Json {\n let body: unknown;\n try {\n body = JSON.parse(raw);\n } catch {\n throw parseFailure(url, \"the body is not valid JSON\");\n }\n\n if (!isObject(body)) throw parseFailure(url, \"the body is not an object\");\n\n const errors = body[FIELD.errors];\n if (Array.isArray(errors) && errors.length > 0) {\n const first = isObject(errors[0]) ? errors[0] : {};\n if (first.code === 404) throw notFound(url, what);\n throw parseFailure(\n url,\n `the site reported ${String(first.reason ?? first.code ?? \"an error\")}`,\n );\n }\n\n const data = body[FIELD.data];\n if (!isObject(data)) throw parseFailure(url, \"the response carries no data object\");\n return data;\n}\n\nconst str = (value: unknown): string | null => {\n if (typeof value !== \"string\") return null;\n const trimmed = value.trim();\n return trimmed === \"\" ? null : trimmed;\n};\n\nconst num = (value: unknown): number | null => {\n if (typeof value === \"number\" && Number.isFinite(value)) return value;\n if (typeof value === \"string\") {\n const parsed = Number(value);\n if (Number.isFinite(parsed)) return parsed;\n }\n return null;\n};\n\nconst intOf = (value: unknown): number | null => {\n const parsed = num(value);\n return parsed === null ? null : Math.trunc(parsed);\n};\n\n/** Map the site's own type wording onto ours. */\nfunction kindOf(value: unknown): Kind | null {\n const type = str(value);\n if (type === SEARCH_TYPE.movie) return \"movie\";\n if (type === SEARCH_TYPE.show) return \"show\";\n if (type === SEARCH_TYPE.game) return \"game\";\n return null;\n}\n\n/**\n * A row without an id, a slug or a kind cannot be looked up or linked, so it is\n * not a row. Returning null lets the caller skip one bad entry among many while\n * still failing loudly when every entry is unreadable.\n */\nfunction toSummary(node: unknown): TitleSummary | null {\n if (!isObject(node)) return null;\n\n const id = intOf(node.id);\n const slug = str(node.slug);\n const title = str(node.title);\n const kind = kindOf(node.type);\n if (id === null || !slug || !title || !kind) return null;\n\n const critic = isObject(node.criticScoreSummary) ? node.criticScoreSummary : {};\n\n // The two listing routes report the audience score differently: browse rows\n // carry `userScore: {score}`, search rows carry nothing at all. Both forms are\n // read, and a row with neither keeps null rather than inventing a zero.\n const userSummary = isObject(node.userScoreSummary) ? node.userScoreSummary : {};\n const userObject = isObject(node.userScore) ? node.userScore : {};\n const userScore = num(userSummary.score) ?? num(userObject.score) ?? num(node.userScore);\n\n return {\n id,\n kind,\n title,\n slug,\n year: intOf(node.premiereYear),\n releaseDate: str(node.releaseDate),\n rating: str(node.rating),\n metascore: num(critic.score),\n userScore,\n sourceUrl: titlePageUrl(kind, slug),\n };\n}\n\n/** Search and browse share this shape, so they share one reader. */\nexport function parseTitlePage(raw: string, url: string, what: string): TitlePage {\n const data = envelope(raw, url, what);\n const items = data[FIELD.items];\n if (!Array.isArray(items)) throw parseFailure(url, \"the response carries no items array\");\n\n const titles: TitleSummary[] = [];\n for (const node of items) {\n const row = toSummary(node);\n if (row) titles.push(row);\n }\n\n if (items.length > 0 && titles.length === 0) {\n throw parseFailure(url, `${items.length} rows but none could be read`);\n }\n\n return {\n titles,\n totalResults: intOf(data[FIELD.totalResults]) ?? titles.length,\n itemCount: items.length,\n };\n}\n\n/**\n * Companies attached to a title, deduplicated by name.\n *\n * The list mixes producers with every distributor, broadcaster and home-video\n * label in every territory, so a well-travelled film carries the same name a\n * dozen times under different ids: Blade Runner lists 128 entries of which\n * \"Warner Home Video\" is fifteen. The first id for a name is kept, since they\n * differ only by the release they were attached to.\n */\nfunction toCompanies(value: unknown): Company[] {\n const source = isObject(value) ? value.companies : value;\n if (!Array.isArray(source)) return [];\n\n const seen = new Set<string>();\n const out: Company[] = [];\n for (const node of source) {\n const name = isObject(node) ? str(node.name) : str(node);\n if (!name) continue;\n const key = name.toLowerCase();\n if (seen.has(key)) continue;\n seen.add(key);\n out.push({ name, id: isObject(node) ? intOf(node.id) : null });\n }\n return out;\n}\n\nfunction toStringList(value: unknown): string[] {\n if (!Array.isArray(value)) return [];\n const out: string[] = [];\n for (const node of value) {\n const name = isObject(node) ? str(node.name) : str(node);\n if (name) out.push(name);\n }\n return out;\n}\n\n/**\n * Awards are reported one entry per ceremony, as a tally rather than a list of\n * individual prizes. A ceremony where nothing was won carries `wins: null`, so\n * that is preserved instead of being flattened to zero: \"no win recorded\" and\n * \"zero wins\" are the same here, but only the source can say which it meant.\n */\nfunction toAwards(value: unknown): Award[] {\n if (!Array.isArray(value)) return [];\n const out: Award[] = [];\n for (const node of value) {\n if (!isObject(node)) continue;\n const ceremony = str(node.awardEvent);\n if (!ceremony) continue;\n out.push({\n ceremony,\n wins: intOf(node.wins),\n nominations: intOf(node.nominations),\n });\n }\n return out;\n}\n\nexport function parseDetail(raw: string, url: string, kind: Kind, slug: string): TitleDetail {\n const data = envelope(raw, url, `${kind} \"${slug}\"`);\n const item = data[FIELD.item];\n if (!isObject(item)) throw parseFailure(url, \"the response carries no item object\");\n\n // The detail route omits `type`, so the kind comes from the caller, who chose\n // the route. Building the summary by hand keeps that difference explicit.\n const id = intOf(item.id);\n const title = str(item.title);\n if (id === null || !title) throw parseFailure(url, \"the entry has no id or no title\");\n\n const critic = isObject(item.criticScoreSummary) ? item.criticScoreSummary : {};\n\n return {\n id,\n kind,\n title,\n slug,\n year: intOf(item.premiereYear),\n releaseDate: str(item.releaseDate),\n rating: str(item.rating),\n metascore: num(critic.score),\n userScore: null,\n sourceUrl: titlePageUrl(kind, slug),\n description: str(item.description),\n tagline: str(item.tagline),\n genres: toStringList(item.genres),\n duration: intOf(item.duration),\n imdbId: str(item.imdbId),\n networks: toStringList(item.networks),\n production: toCompanies(item.production),\n awards: toAwards(item.awards),\n criticScoreDetail: null,\n userScoreDetail: null,\n };\n}\n\n/**\n * A score summary, with its scale.\n *\n * `max` comes from the response rather than from the source name, because the\n * two scales differ and hard-coding them here would be a guess that survives\n * quietly if the site ever changes one.\n */\nexport function parseScore(raw: string, url: string, what: string): ScoreSummary {\n const data = envelope(raw, url, what);\n const item = data[FIELD.item];\n if (!isObject(item)) throw parseFailure(url, \"the response carries no score object\");\n\n const max = num(item.max);\n if (max === null) throw parseFailure(url, \"the score carries no scale\");\n\n return {\n score: num(item.score),\n max,\n reviewCount: intOf(item.reviewCount),\n positiveCount: intOf(item.positiveCount),\n neutralCount: intOf(item.neutralCount),\n negativeCount: intOf(item.negativeCount),\n sentiment: str(item.sentiment),\n };\n}\n\nfunction reviewBucket(data: Json, url: string, sentiment: Sentiment): unknown[] {\n const item = data[FIELD.item];\n if (!isObject(item)) throw parseFailure(url, \"the response carries no reviews object\");\n\n const bucket = item[bucketFor(sentiment)];\n // A bucket that does not exist means the site stopped grouping reviews this\n // way, which is a change worth reporting rather than an absence of reviews.\n if (!Array.isArray(bucket)) {\n throw parseFailure(url, `there is no \"${bucketFor(sentiment)}\" group of reviews`);\n }\n return bucket;\n}\n\nexport function parseCriticReviews(\n raw: string,\n url: string,\n what: string,\n sentiment: Sentiment,\n): ReviewPage<CriticReview> {\n const data = envelope(raw, url, what);\n const bucket = reviewBucket(data, url, sentiment);\n\n const reviews: CriticReview[] = [];\n for (const node of bucket) {\n if (!isObject(node)) continue;\n const quote = str(node.quote);\n const publication = str(node.publicationName);\n // A quote with no publication cannot be attributed, and attribution is the\n // condition under which this content is worth passing on at all.\n if (!quote || !publication) continue;\n reviews.push({\n quote,\n score: num(node.score),\n publication,\n author: str(node.author),\n url: str(node.url),\n date: str(node.date),\n });\n }\n\n if (bucket.length > 0 && reviews.length === 0) {\n throw parseFailure(url, `${bucket.length} reviews but none could be read`);\n }\n\n return {\n reviews,\n totalResults: intOf(data[FIELD.totalResults]) ?? reviews.length,\n itemCount: bucket.length,\n };\n}\n\nexport function parseUserReviews(\n raw: string,\n url: string,\n what: string,\n sentiment: Sentiment,\n): ReviewPage<UserReview> {\n const data = envelope(raw, url, what);\n const bucket = reviewBucket(data, url, sentiment);\n\n const reviews: UserReview[] = [];\n for (const node of bucket) {\n if (!isObject(node)) continue;\n const quote = str(node.quote);\n if (!quote) continue;\n reviews.push({ quote, score: num(node.score), date: str(node.date) });\n }\n\n if (bucket.length > 0 && reviews.length === 0) {\n throw parseFailure(url, `${bucket.length} reviews but none could be read`);\n }\n\n return {\n reviews,\n totalResults: intOf(data[FIELD.totalResults]) ?? reviews.length,\n itemCount: bucket.length,\n };\n}\n\n/**\n * Unwrap a click-tracking redirect.\n *\n * Offer links arrive as tracking URLs of about 800 characters, most of it a\n * base64 analytics payload, with the real destination in the `r` parameter.\n * Nine offers on one film came to 7 KB of redirects against 300 bytes of actual\n * links, so the destination is extracted and the wrapper dropped.\n */\nfunction unwrapLink(link: string | null): string | null {\n if (!link) return null;\n try {\n const target = new URL(link).searchParams.get(\"r\");\n if (target && /^https?:\\/\\//i.test(target)) return target;\n } catch {\n // Not a URL this function understands; the original is still usable.\n }\n return link;\n}\n\n/** Streaming offers, grouped upstream by how you pay for them. */\nexport function parseOffers(raw: string, url: string, what: string): WatchOffer[] {\n const data = envelope(raw, url, what);\n const item = data[FIELD.item];\n if (!isObject(item)) throw parseFailure(url, \"the response carries no offers object\");\n\n const offers: WatchOffer[] = [];\n for (const [group, value] of Object.entries(item)) {\n if (!Array.isArray(value)) continue;\n for (const node of value) {\n if (!isObject(node)) continue;\n const provider = str(node.providerName);\n if (!provider) continue;\n offers.push({ provider, kind: group, url: unwrapLink(str(node.link)) });\n }\n }\n return offers;\n}\n","/**\n * High-level Metacritic client.\n *\n * This module knows nothing about MCP, which keeps it testable against plain\n * strings and usable as a library through the `./client` export.\n */\n\nimport type { Config, Logger } from \"../config.js\";\nimport {\n DEFAULT_USER_AGENT,\n MIN_ALLOWED_INTERVAL_MS,\n createLogger,\n loadConfig,\n} from \"../config.js\";\nimport type {\n CriticReview,\n Kind,\n ReviewPage,\n ScoreSummary,\n Sentiment,\n TitleDetail,\n TitlePage,\n UserReview,\n WatchOffer,\n} from \"../types.js\";\nimport { TtlLruCache } from \"./cache.js\";\nimport { fetchText } from \"./http.js\";\nimport {\n parseCriticReviews,\n parseDetail,\n parseOffers,\n parseScore,\n parseTitlePage,\n parseUserReviews,\n} from \"./parse.js\";\nimport type { SORT_BY } from \"./paths.js\";\nimport { RateLimiter } from \"./rateLimiter.js\";\nimport { browseUrl, detailUrl, reviewsUrl, scoreUrl, searchUrl, watchUrl } from \"./urls.js\";\n\nexport interface McClientOptions {\n config?: Config;\n logger?: Logger;\n fetchImpl?: typeof fetch;\n}\n\nexport interface Outcome<T> {\n data: T;\n /** True when served from the in-memory cache rather than the network. */\n cached: boolean;\n}\n\nexport type Sort = keyof typeof SORT_BY;\n\n/**\n * Apply the guarantees this project makes about its own traffic.\n *\n * The environment parser already enforces both, but `McClient` is published as\n * a library through the `./client` export and takes a caller-built config, so\n * without this the pacing floor and the honest identity are optional for anyone\n * importing it. They are the two things this server offers in exchange for\n * reading a source that publishes no terms, so they hold on every path.\n *\n * A caller may still name their own application in the User-Agent, and there\n * are good reasons to. Passing the traffic off as a browser is a different\n * thing, and gets the project's own identity appended so it stays attributable.\n */\nfunction withGuarantees(config: Config): Config {\n const userAgent = /mozilla\\/|applewebkit|chrome\\/|safari\\/|gecko/i.test(config.userAgent)\n ? `${config.userAgent} ${DEFAULT_USER_AGENT}`\n : config.userAgent;\n return {\n ...config,\n userAgent,\n minIntervalMs: Math.max(MIN_ALLOWED_INTERVAL_MS, config.minIntervalMs),\n };\n}\n\nexport class McClient {\n private readonly config: Config;\n private readonly logger: Logger;\n private readonly limiter: RateLimiter;\n /**\n * Two caches, because the two kinds of data age at different speeds. A\n * catalogue entry changes when someone edits it, which is rare. A score moves\n * every time a review lands, which around a release is constantly.\n */\n private readonly catalogueCache: TtlLruCache<unknown>;\n private readonly scoresCache: TtlLruCache<unknown>;\n private readonly fetchImpl: typeof fetch | undefined;\n\n constructor(options: McClientOptions = {}) {\n this.config = withGuarantees(options.config ?? loadConfig());\n this.logger = options.logger ?? createLogger(this.config.logLevel);\n this.limiter = new RateLimiter({ minIntervalMs: this.config.minIntervalMs });\n this.catalogueCache = new TtlLruCache<unknown>(\n this.config.cacheMaxEntries,\n this.config.cacheTtlMs,\n );\n this.scoresCache = new TtlLruCache<unknown>(\n this.config.cacheMaxEntries,\n this.config.scoresCacheTtlMs,\n );\n this.fetchImpl = options.fetchImpl;\n }\n\n /**\n * Search across films, shows and games.\n *\n * The return type carries rows only. The upstream response embeds artwork\n * variants on every match, and dropping them here rather than in a tool means\n * they have no path out of this layer.\n */\n async search(query: string, limit: number, offset: number): Promise<Outcome<TitlePage>> {\n const url = searchUrl(query, limit, offset);\n return this.fetchParsed(url, this.catalogueCache, (body) =>\n parseTitlePage(body, url, `the query \"${query}\"`),\n );\n }\n\n async browse(options: {\n kind: Kind;\n sort: Sort;\n genre?: string;\n limit: number;\n offset: number;\n }): Promise<Outcome<TitlePage>> {\n const url = browseUrl(options);\n return this.fetchParsed(url, this.catalogueCache, (body) =>\n parseTitlePage(body, url, `that ${options.kind} listing`),\n );\n }\n\n async getDetail(kind: Kind, slug: string): Promise<Outcome<TitleDetail>> {\n const url = detailUrl(kind, slug);\n // The trimmed form is what reached the network, so it is also what the\n // public link and the echoed slug must be built from.\n const clean = slug.trim();\n return this.fetchParsed(url, this.catalogueCache, (body) =>\n parseDetail(body, url, kind, clean),\n );\n }\n\n async getScore(\n kind: Kind,\n slug: string,\n source: \"critic\" | \"user\",\n ): Promise<Outcome<ScoreSummary>> {\n const url = scoreUrl(kind, slug, source);\n return this.fetchParsed(url, this.scoresCache, (body) =>\n parseScore(body, url, `the ${source} score of ${kind} \"${slug}\"`),\n );\n }\n\n async getCriticReviews(options: {\n kind: Kind;\n slug: string;\n sentiment: Sentiment;\n limit: number;\n offset: number;\n }): Promise<Outcome<ReviewPage<CriticReview>>> {\n const url = reviewsUrl({ ...options, source: \"critic\" });\n return this.fetchParsed(\n url,\n this.scoresCache,\n (body) =>\n parseCriticReviews(\n body,\n url,\n `reviews of ${options.kind} \"${options.slug}\"`,\n options.sentiment,\n ),\n options.sentiment,\n );\n }\n\n async getUserReviews(options: {\n kind: Kind;\n slug: string;\n sentiment: Sentiment;\n limit: number;\n offset: number;\n }): Promise<Outcome<ReviewPage<UserReview>>> {\n const url = reviewsUrl({ ...options, source: \"user\" });\n return this.fetchParsed(\n url,\n this.scoresCache,\n (body) =>\n parseUserReviews(\n body,\n url,\n `reviews of ${options.kind} \"${options.slug}\"`,\n options.sentiment,\n ),\n options.sentiment,\n );\n }\n\n async getWatchOffers(imdbId: string, kind: Kind): Promise<Outcome<WatchOffer[]>> {\n const url = watchUrl(imdbId, kind);\n return this.fetchParsed(url, this.scoresCache, (body) =>\n parseOffers(body, url, `streaming offers for ${imdbId}`),\n );\n }\n\n /**\n * Fetch, parse, then cache. In that order: a response that could not be read\n * is never stored, so a bad minute upstream cannot be replayed from memory\n * for the rest of the cache lifetime.\n */\n private async fetchParsed<T>(\n url: string,\n cache: TtlLruCache<unknown>,\n parse: (body: string) => T,\n /**\n * Distinguishes parses of the same response. The reviews route returns every\n * sentiment in one payload, so two calls differing only by sentiment share a\n * URL: keying on the URL alone would serve the first answer to the second.\n */\n variant = \"\",\n ): Promise<Outcome<T>> {\n const key = variant ? `${url}#${variant}` : url;\n const hit = cache.get(key);\n if (hit !== undefined) {\n this.logger.debug(`cache hit ${key}`);\n return { data: hit as T, cached: true };\n }\n\n const body = await fetchText(url, {\n config: this.config,\n limiter: this.limiter,\n logger: this.logger,\n ...(this.fetchImpl ? { fetchImpl: this.fetchImpl } : {}),\n });\n\n const data = parse(body);\n cache.set(key, data);\n return { data, cached: false };\n }\n}\n","/**\n * browse_titles: rankings and release lists.\n *\n * This is the tool that answers \"best horror films\" or \"highest rated games\",\n * questions a search box cannot serve because the caller has no title in mind.\n */\n\nimport { z } from \"zod\";\nimport type { McClient, Sort } from \"../mc/client.js\";\nimport type { Kind } from \"../types.js\";\nimport {\n ok,\n renderTitleList,\n titleSummarySchema,\n toTitleSummaryOut,\n toToolError,\n type ToolResult,\n} from \"./shared.js\";\n\nexport const browseTitlesDescription = [\n \"List films, shows or games by score, by release date or by current popularity.\",\n \"Use this when there is no specific title to look up: best rated, newest, what people are looking at now.\",\n \"Filter by genre with a single name such as Horror, Comedy or Action.\",\n \"Sorting by score returns the all-time ranking, which is dominated by older titles: combine with a genre to narrow it.\",\n \"Scores come back on their own scales, 100 for critics and 10 for users.\",\n \"Paging is approximate: Metacritic does not order tied entries stably, so an entry can appear on two consecutive pages. Deduplicate by slug rather than counting rows.\",\n].join(\" \");\n\nexport const browseTitlesInputShape = {\n kind: z.enum([\"movie\", \"show\", \"game\"]).default(\"movie\").describe(\"Which catalogue to list.\"),\n sort: z\n .enum([\"score\", \"recent\", \"popular\"])\n .default(\"score\")\n .describe(\n \"'score' is the critic ranking, 'recent' is by release date, 'popular' is current attention.\",\n ),\n genre: z.string().optional().describe(\"Single genre name, such as Horror. Omit for all genres.\"),\n limit: z.number().int().min(1).max(50).default(20).describe(\"How many rows to return.\"),\n offset: z.number().int().min(0).default(0).describe(\"How many rows to skip, for paging.\"),\n};\n\nexport const browseTitlesOutputShape = {\n kind: z.string(),\n sort: z.string(),\n genre: z.string().nullable(),\n results: z.array(titleSummarySchema),\n total_available: z\n .number()\n .int()\n .describe(\"How many entries match upstream, which is the size of what this samples.\"),\n offset: z.number().int(),\n next_offset: z\n .number()\n .int()\n .nullable()\n .describe(\n \"Pass as 'offset' for the next page. Tied entries are not ordered stably upstream, so \" +\n \"deduplicate by slug across pages.\",\n ),\n notes: z.array(z.string()),\n};\n\nexport interface BrowseTitlesArgs {\n kind: Kind;\n sort: Sort;\n genre?: string;\n limit: number;\n offset: number;\n}\n\nexport async function runBrowseTitles(\n client: McClient,\n args: BrowseTitlesArgs,\n): Promise<ToolResult> {\n try {\n const { data, cached } = await client.browse({\n kind: args.kind,\n sort: args.sort,\n ...(args.genre ? { genre: args.genre } : {}),\n limit: args.limit,\n offset: args.offset,\n });\n\n const results = data.titles.map(toTitleSummaryOut);\n const notes: string[] = [];\n if (cached) notes.push(\"Served from this server's short-lived in-memory cache.\");\n if (results.length === 0) {\n notes.push(\n args.genre\n ? `No ${args.kind} matched the genre \"${args.genre}\" at this offset. Check the spelling, or drop the genre.`\n : \"The listing returned no rows at this offset.\",\n );\n }\n if (data.itemCount > data.titles.length) {\n notes.push(\n `${data.itemCount - data.titles.length} entries on this page could not be read and were skipped.`,\n );\n }\n\n // A short page is the end-of-list signal, judged on what the site sent\n // rather than on what could be read, since its offset counts entries.\n const nextOffset = data.itemCount < args.limit ? null : args.offset + data.itemCount;\n\n const heading = [\n args.genre ? `${args.genre} ` : \"\",\n `${args.kind}s`,\n args.sort === \"score\"\n ? \" by critic score\"\n : args.sort === \"recent\"\n ? \" by release date\"\n : \" by current popularity\",\n ].join(\"\");\n\n const summary =\n results.length === 0\n ? `No rows for ${heading}.`\n : `${results.length} of ${data.totalResults} ${heading}:\\n${renderTitleList(results)}`;\n\n return ok(\n {\n kind: args.kind,\n sort: args.sort,\n genre: args.genre ?? null,\n results,\n total_available: data.totalResults,\n offset: args.offset,\n next_offset: nextOffset,\n notes,\n },\n summary,\n { notes },\n );\n } catch (error) {\n return toToolError(error);\n }\n}\n","/** Pieces shared by the four tools: schemas, error mapping, text mirrors. */\n\nimport { z } from \"zod\";\nimport { McError } from \"../errors.js\";\nimport type { ScoreSummary, TitleSummary } from \"../types.js\";\n\n/** Many MCP clients render only the text block, so it must read on its own. */\nexport const MAX_TEXT_MIRROR_CHARS = 2000;\n\nexport const ATTRIBUTION = \"Source: Metacritic\";\n\nexport const kindSchema = z\n .enum([\"movie\", \"show\", \"game\"])\n .describe(\"Which catalogue the entry belongs to. Pass it back with the slug.\");\n\nexport const titleSummarySchema = z.object({\n id: z.number().int(),\n kind: z.enum([\"movie\", \"show\", \"game\"]),\n title: z.string(),\n slug: z.string().describe(\"Identifier to pass to get_title and get_reviews, with 'kind'.\"),\n year: z.number().int().nullable(),\n release_date: z.string().nullable(),\n rating: z.string().nullable().describe(\"Age rating as published, such as R or TV-MA.\"),\n metascore: z\n .number()\n .nullable()\n .describe(\"Critic score, out of 100. Not comparable to user_score without rescaling.\"),\n user_score: z\n .number()\n .nullable()\n .describe(\"Audience score, out of 10. Not comparable to metascore without rescaling.\"),\n source_url: z.string().describe(\"Metacritic page. Show this when citing the entry.\"),\n});\n\nexport type TitleSummaryOut = z.infer<typeof titleSummarySchema>;\n\nexport function toTitleSummaryOut(title: TitleSummary): TitleSummaryOut {\n return {\n id: title.id,\n kind: title.kind,\n title: title.title,\n slug: title.slug,\n year: title.year,\n release_date: title.releaseDate,\n rating: title.rating,\n metascore: title.metascore,\n user_score: title.userScore,\n source_url: title.sourceUrl,\n };\n}\n\n/**\n * Every score goes out with its own scale.\n *\n * Critic scores run to 100 and user scores to 10. A bare number invites a\n * comparison that is wrong by an order of magnitude, so `max` is not optional\n * and the description says what it is for.\n */\nexport const scoreSchema = z.object({\n score: z.number().nullable(),\n max: z.number().describe(\"Scale this score is on: 100 for critics, 10 for users.\"),\n review_count: z.number().int().nullable(),\n positive_count: z.number().int().nullable(),\n neutral_count: z.number().int().nullable(),\n negative_count: z.number().int().nullable(),\n sentiment: z.string().nullable().describe(\"Metacritic's own wording for the verdict.\"),\n});\n\nexport function toScoreOut(score: ScoreSummary): z.infer<typeof scoreSchema> {\n return {\n score: score.score,\n max: score.max,\n review_count: score.reviewCount,\n positive_count: score.positiveCount,\n neutral_count: score.neutralCount,\n negative_count: score.negativeCount,\n sentiment: score.sentiment,\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 * Build a result whose text block always ends with its notes and attribution.\n *\n * The body is truncated to fit around the trailer rather than the whole block\n * being cut afterwards. Appending the credit and then truncating loses exactly\n * the credit, which is the one line that must survive: a client rendering only\n * the text would otherwise show third-party review quotes with no source.\n *\n * The notes belong to the trailer for the same reason. They are what qualifies\n * the answer, saying that a list was capped, that a score could not be read, or\n * that a section does not apply to this kind of entry. A client rendering only\n * the text reads an unqualified answer without them, which is the failure this\n * server exists to avoid.\n *\n * The trailer also states that the text was shortened, since a client that\n * cannot read `structuredContent` has no other way to know rows are missing.\n */\nexport function ok(\n structured: Record<string, unknown>,\n body: string,\n options: { notes?: string[]; sourceUrl?: string } = {},\n): ToolResult {\n const attribution = options.sourceUrl ? `${ATTRIBUTION} — ${options.sourceUrl}` : ATTRIBUTION;\n // A run of notes must never crowd out the answer they qualify, so they are\n // dropped from the tail until the body keeps a readable share of the block.\n const noteLines = (options.notes ?? []).map((note) => `Note: ${note}`);\n while (noteLines.length > 0 && noteLines.join(\"\\n\").length > MAX_TEXT_MIRROR_CHARS / 2) {\n noteLines.pop();\n }\n const trailer = [...noteLines, attribution].join(\"\\n\");\n\n const cutMarker = \"\\n\\n[shortened; the full result is in the structured output]\";\n const reserved = `\\n\\n${trailer}`.length;\n const budget = MAX_TEXT_MIRROR_CHARS - reserved;\n\n let text: string;\n if (body.length <= budget) {\n text = `${body}\\n\\n${trailer}`;\n } else {\n const kept = truncate(body, Math.max(0, budget - cutMarker.length));\n text = `${kept}${cutMarker}\\n\\n${trailer}`;\n }\n\n return { content: [{ type: \"text\", text }], structuredContent: structured };\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 McError\n ? error\n : new McError(\"network_error\", error instanceof Error ? error.message : String(error));\n\n const lines = [`[${known.code}] ${known.message}`];\n if (known.details.hint) lines.push(`Hint: ${known.details.hint}`);\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) return text;\n return `${text.slice(0, maxChars - 1).trimEnd()}…`;\n}\n\n/**\n * Cut a block of text at a line boundary, so a truncated description ends on a\n * sentence rather than mid-word. A single line longer than the budget is cut\n * hard, since there is no boundary to find.\n */\nexport function sliceAtLineBoundary(\n text: string,\n offset: number,\n maxChars: number,\n): { slice: string; nextOffset: number | null } {\n const rest = text.slice(offset);\n if (rest.length <= maxChars) return { slice: rest, nextOffset: null };\n\n const window = rest.slice(0, maxChars);\n const lastBreak = window.lastIndexOf(\"\\n\");\n let cut = lastBreak > 0 ? lastBreak : maxChars;\n\n // Never cut between the two halves of a surrogate pair: both pages would show\n // a replacement character and no offset could ever reassemble it.\n const code = rest.charCodeAt(cut - 1);\n if (code >= 0xd800 && code <= 0xdbff) cut -= 1;\n\n return { slice: rest.slice(0, cut), nextOffset: offset + cut };\n}\n\n/** Compact listing, showing what a model needs to pick the right entry. */\nexport function renderTitleList(titles: TitleSummaryOut[]): string {\n return titles\n .map((title, index) => {\n const scores = [\n title.metascore === null ? \"\" : `critics ${title.metascore}/100`,\n title.user_score === null ? \"\" : `users ${title.user_score}/10`,\n ].filter(Boolean);\n // Some titles already end with their year, such as \"Psycho (1960)\", so\n // appending it again would read as a stutter.\n const yearShown = title.year !== null && !title.title.includes(`(${title.year})`);\n const parts = [\n `${index + 1}. ${title.title}`,\n yearShown ? `(${title.year})` : \"\",\n `· ${title.kind}`,\n scores.length > 0 ? `· ${scores.join(\", \")}` : \"\",\n `· slug: ${title.slug}`,\n ];\n return parts.filter(Boolean).join(\" \");\n })\n .join(\"\\n\");\n}\n","/**\n * get_reviews: what individual critics and viewers wrote.\n *\n * The aggregate score is available everywhere. The individual verdicts, with\n * the publication that ran them and a link to the original article, are what\n * this source has and others do not.\n */\n\nimport { z } from \"zod\";\nimport type { McClient } from \"../mc/client.js\";\nimport type { Kind, Sentiment } from \"../types.js\";\nimport { titlePageUrl } from \"../mc/urls.js\";\nimport { kindSchema, ok, toToolError, truncate, type ToolResult } from \"./shared.js\";\n\n/** Quotes are third-party writing, so they are excerpts rather than reproductions. */\nconst MAX_QUOTE_CHARS = 600;\n\nexport const getReviewsDescription = [\n \"Read individual reviews of a Metacritic entry, from critics or from users.\",\n \"Get the slug and kind from search_titles first.\",\n \"Filter with 'sentiment' to read only what praised or panned it, which Metacritic groups itself.\",\n \"Metacritic returns a fixed sample rather than the full list, so 'total_available' is usually far larger than what comes back, and there is no way to page past the sample.\",\n \"Critic reviews carry the publication and a link to the original article: quote them with both.\",\n \"Critic scores run to 100 and user scores to 10, so do not average the two together.\",\n].join(\" \");\n\nexport const getReviewsInputShape = {\n slug: z.string().min(1).describe(\"Identifier from search_titles, such as 'the-matrix'.\"),\n kind: kindSchema,\n source: z\n .enum([\"critic\", \"user\"])\n .default(\"critic\")\n .describe(\"'critic' is the professional press, 'user' is the audience.\"),\n sentiment: z\n .enum([\"all\", \"positive\", \"neutral\", \"negative\"])\n .default(\"all\")\n .describe(\"Which slice to read. Metacritic groups reviews this way itself.\"),\n limit: z\n .number()\n .int()\n .min(1)\n .max(50)\n .default(10)\n .describe(\"How many reviews to return from the sample Metacritic publishes.\"),\n offset: z\n .number()\n .int()\n .min(0)\n .default(0)\n .describe(\"How many of the sampled reviews to skip. The sample itself cannot be paged past.\"),\n};\n\nconst reviewSchema = z.object({\n quote: z.string().nullable().describe(\"Excerpt as published, truncated if long.\"),\n score: z.number().nullable(),\n max: z.number().describe(\"Scale for this review's score: 100 for critics, 10 for users.\"),\n publication: z.string().nullable().describe(\"Who ran it. Name it when quoting.\"),\n author: z.string().nullable(),\n url: z.string().nullable().describe(\"Original article. Link it when quoting.\"),\n date: z.string().nullable(),\n});\n\nexport const getReviewsOutputShape = {\n slug: z.string(),\n kind: z.string(),\n source_url: z\n .string()\n .describe(\"Metacritic page these reviews belong to. Cite it when an article link is missing.\"),\n source: z.string(),\n sentiment: z.string(),\n reviews: z.array(reviewSchema),\n total_available: z\n .number()\n .int()\n .describe(\"How many reviews Metacritic counts in total. Usually far more than it serves here.\"),\n offset: z.number().int(),\n next_offset: z.number().int().nullable().describe(\"Pass as 'offset' for the next page.\"),\n notes: z.array(z.string()),\n};\n\nexport interface GetReviewsArgs {\n slug: string;\n kind: Kind;\n source: \"critic\" | \"user\";\n sentiment: Sentiment;\n limit: number;\n offset: number;\n}\n\nexport async function runGetReviews(client: McClient, args: GetReviewsArgs): Promise<ToolResult> {\n try {\n const sourceUrl = titlePageUrl(args.kind, args.slug.trim());\n const request = {\n kind: args.kind,\n slug: args.slug,\n sentiment: args.sentiment,\n limit: args.limit,\n offset: args.offset,\n };\n\n // The two sources return different shapes, and only critics carry a\n // publication and a link, which is why they are read separately.\n const max = args.source === \"critic\" ? 100 : 10;\n const { data, cached } =\n args.source === \"critic\"\n ? await client.getCriticReviews(request)\n : await client.getUserReviews(request);\n\n // Metacritic ignores limit and offset on this route and always answers with\n // the same sample, so the slicing happens here. Asking upstream for more\n // would spend a request and change nothing.\n const sampled = data.reviews.slice(args.offset, args.offset + args.limit);\n\n const reviews = sampled.map((review) => ({\n quote: review.quote === null ? null : truncate(review.quote, MAX_QUOTE_CHARS),\n score: review.score,\n max,\n publication: \"publication\" in review ? review.publication : null,\n author: \"author\" in review ? review.author : null,\n url: \"url\" in review ? review.url : null,\n date: review.date,\n }));\n\n const notes: string[] = [];\n if (cached) notes.push(\"Served from this server's short-lived in-memory cache.\");\n if (reviews.length === 0) {\n if (args.offset > 0 && args.offset >= data.reviews.length) {\n notes.push(\n `offset=${args.offset} is past the end of a sample of ${data.reviews.length} ${args.source} reviews. Call again with offset=0.`,\n );\n } else if (args.sentiment === \"all\") {\n notes.push(\"Metacritic publishes no review in this slice.\");\n } else {\n notes.push(\n `Metacritic publishes no ${args.sentiment} review here. Call again with sentiment=\"all\" to see the rest.`,\n );\n }\n }\n if (data.itemCount > data.reviews.length) {\n notes.push(\n `${data.itemCount - data.reviews.length} entries in the sample could not be attributed and were skipped.`,\n );\n }\n if (data.totalResults > data.reviews.length) {\n const slice = args.sentiment === \"all\" ? \"\" : ` in the ${args.sentiment} slice`;\n notes.push(\n `Metacritic counts ${data.totalResults} ${args.source} reviews for this entry and serves ${data.reviews.length}${slice} here. The rest are only on the website: ${sourceUrl}`,\n );\n }\n\n // Paging is within the sample, since the source will not serve past it.\n const consumed = args.offset + reviews.length;\n const nextOffset = consumed < data.reviews.length ? consumed : null;\n\n const listing = reviews\n .map((review, index) => {\n const who = [review.publication, review.author].filter(Boolean).join(\", \");\n const head = `${index + 1}. ${review.score === null ? \"\" : `${review.score}/${max} `}${who || \"anonymous\"}`;\n // Quotes are third-party writing and can contain blank lines, which\n // would otherwise sit flush against the server's own trailing lines and\n // read as if this server had written them. Indenting every line keeps\n // the boundary visible.\n const quoted = (review.quote ?? \"\")\n .split(\"\\n\")\n .map((line) => ` ${line}`)\n .join(\"\\n\");\n return `${head}\\n${quoted}\\n ${review.url ?? sourceUrl}`;\n })\n .join(\"\\n\");\n\n const summary =\n reviews.length === 0\n ? `No ${args.source} review to show for \"${args.slug}\".`\n : `${reviews.length} ${args.source} review${reviews.length === 1 ? \"\" : \"s\"} of \"${args.slug}\":\\n${listing}`;\n\n return ok(\n {\n slug: args.slug,\n kind: args.kind,\n source_url: sourceUrl,\n source: args.source,\n sentiment: args.sentiment,\n reviews,\n total_available: data.totalResults,\n offset: args.offset,\n next_offset: nextOffset,\n notes,\n },\n summary,\n { notes, sourceUrl },\n );\n } catch (error) {\n return toToolError(error);\n }\n}\n","/**\n * get_title: read one entry, section by section.\n *\n * The detail response runs to 46 KB for a film, so sections are opt-in and the\n * default pair answers the question people actually ask: what is this, and is\n * it any good.\n */\n\nimport { z } from \"zod\";\nimport type { McClient } from \"../mc/client.js\";\nimport { McError } from \"../errors.js\";\nimport type { Kind, ScoreSummary, TitleDetail, WatchOffer } from \"../types.js\";\nimport {\n kindSchema,\n ok,\n scoreSchema,\n sliceAtLineBoundary,\n titleSummarySchema,\n toScoreOut,\n toTitleSummaryOut,\n toToolError,\n type ToolResult,\n} from \"./shared.js\";\n\nconst SECTIONS = [\"basic\", \"scores\", \"awards\", \"production\", \"networks\", \"where_to_watch\"] as const;\ntype Section = (typeof SECTIONS)[number];\n\nexport const getTitleDescription = [\n \"Read one Metacritic entry by slug and kind, both from search_titles.\",\n \"Sections are opt-in because a full entry is large: 'basic' and 'scores' are the default and cover most questions.\",\n \"'scores' fetches the critic and audience breakdowns, each with its own scale, so never compare the two numbers directly.\",\n \"Sections gate the payload: asking for 'scores' alone returns no description, and 'basic' alone returns no scores.\",\n \"'networks' lists the broadcasters of a show; 'where_to_watch' costs an extra request and covers films and shows only.\",\n \"A long description paginates: when 'truncated' is true, call again with 'offset' set to 'next_offset'.\",\n].join(\" \");\n\nexport const getTitleInputShape = {\n slug: z.string().min(1).describe(\"Identifier from search_titles, such as 'the-matrix'.\"),\n kind: kindSchema,\n sections: z\n .array(z.enum(SECTIONS))\n .default([\"basic\", \"scores\"])\n .describe(\"Which parts to return. Each extra section beyond the default costs a request.\"),\n max_chars: z\n .number()\n .int()\n .min(200)\n .max(20000)\n .default(4000)\n .describe(\"Character budget for the description.\"),\n offset: z.number().int().min(0).default(0).describe(\"Where to resume the description.\"),\n};\n\nexport const getTitleOutputShape = {\n title: titleSummarySchema,\n description: z.string().nullable(),\n tagline: z.string().nullable(),\n genres: z.array(z.string()),\n duration_minutes: z\n .number()\n .int()\n .nullable()\n .describe(\"Runtime in minutes for a film, or the length of a typical episode for a show.\"),\n imdb_id: z\n .string()\n .nullable()\n .describe(\"IMDb identifier, usable to cross-reference other sources.\"),\n total_chars: z.number().int().describe(\"Length of the full description.\"),\n returned_chars: z.number().int(),\n offset: z.number().int(),\n next_offset: z.number().int().nullable().describe(\"Pass as 'offset' to read the rest.\"),\n truncated: z.boolean(),\n critic_score: scoreSchema.nullable(),\n user_score: scoreSchema.nullable(),\n awards: z\n .array(\n z.object({\n ceremony: z.string().describe(\"Awards body, such as 'Academy Awards, USA'.\"),\n wins: z.number().int().nullable().describe(\"Null when the source records no win.\"),\n nominations: z.number().int().nullable(),\n }),\n )\n .optional()\n .describe(\"One tally per ceremony. Metacritic publishes no per-category detail.\"),\n networks: z.array(z.string()).optional().describe(\"Broadcasters, for shows.\"),\n production: z.array(z.object({ name: z.string(), id: z.number().int().nullable() })).optional(),\n where_to_watch: z\n .array(z.object({ provider: z.string(), kind: z.string(), url: z.string().nullable() }))\n .optional(),\n notes: z.array(z.string()),\n};\n\nexport interface GetTitleArgs {\n slug: string;\n kind: Kind;\n sections: Section[];\n max_chars: number;\n offset: number;\n}\n\nexport async function runGetTitle(client: McClient, args: GetTitleArgs): Promise<ToolResult> {\n try {\n const wanted = new Set<Section>(args.sections);\n const notes: string[] = [];\n\n const detail = await client.getDetail(args.kind, args.slug);\n if (detail.cached) notes.push(\"Served from this server's short-lived in-memory cache.\");\n const item = detail.data;\n\n let criticScore: ScoreSummary | null = null;\n let userScore: ScoreSummary | null = null;\n if (wanted.has(\"scores\")) {\n [criticScore, userScore] = await Promise.all([\n optionalScore(client, args.kind, args.slug, \"critic\", notes),\n optionalScore(client, args.kind, args.slug, \"user\", notes),\n ]);\n }\n\n const basic = wanted.has(\"basic\");\n const full = basic ? (item.description ?? \"\") : \"\";\n const { slice, nextOffset } = sliceAtLineBoundary(full, args.offset, args.max_chars);\n if (basic && nextOffset !== null) {\n notes.push(\n `The description is longer than ${args.max_chars} characters. Call again with offset=${nextOffset} for the rest.`,\n );\n }\n if (basic && slice === \"\" && args.offset > 0 && full.length > 0) {\n notes.push(\n `offset=${args.offset} is past the end of a description of ${full.length} characters. Call again with offset=0 to read it from the start.`,\n );\n }\n\n // The audience score only exists on the scores route, so without that\n // section its absence here says nothing about the title. Saying so beats a\n // bare null the caller would read as \"Metacritic publishes none\".\n if (!wanted.has(\"scores\")) {\n notes.push(\n \"Scores were not requested, so critic_score and user_score are null here regardless of what Metacritic publishes. Add 'scores' to sections to read them.\",\n );\n }\n\n const structured: Record<string, unknown> = {\n title: {\n ...toTitleSummaryOut(item),\n metascore: criticScore?.score ?? item.metascore,\n user_score: userScore?.score ?? null,\n },\n description: basic && slice !== \"\" ? slice : null,\n tagline: basic ? item.tagline : null,\n genres: basic ? item.genres : [],\n duration_minutes: basic ? item.duration : null,\n imdb_id: basic ? item.imdbId : null,\n total_chars: basic ? full.length : 0,\n returned_chars: basic ? slice.length : 0,\n offset: args.offset,\n next_offset: basic ? nextOffset : null,\n truncated: basic && nextOffset !== null,\n critic_score: criticScore ? toScoreOut(criticScore) : null,\n user_score: userScore ? toScoreOut(userScore) : null,\n notes,\n };\n\n if (wanted.has(\"awards\")) structured.awards = item.awards;\n\n const PRODUCTION_CAP = 25;\n const production = wanted.has(\"production\") ? item.production.slice(0, PRODUCTION_CAP) : [];\n if (wanted.has(\"production\")) {\n structured.production = production;\n if (item.production.length > PRODUCTION_CAP) {\n notes.push(\n `${item.production.length} companies are credited and the first ${PRODUCTION_CAP} are shown. The list mixes producers with distributors and home-video labels across every territory.`,\n );\n }\n }\n\n if (wanted.has(\"networks\")) structured.networks = item.networks;\n const offers = wanted.has(\"where_to_watch\")\n ? await watchOffers(client, item, args.kind, notes)\n : [];\n if (wanted.has(\"where_to_watch\")) structured.where_to_watch = offers;\n\n return ok(structured, render(item, slice, criticScore, userScore, wanted, offers, production), {\n notes,\n sourceUrl: item.sourceUrl,\n });\n } catch (error) {\n return toToolError(error);\n }\n}\n\n/**\n * Read a score, distinguishing an absence from a failure.\n *\n * Only `not_found` means the site publishes no score for this entry. Every\n * other error is a failure to ask, and saying \"no score is published\" on the\n * back of a timeout states something false about the data. That distinction is\n * the whole rule this server is built on, and it has to hold here too.\n */\nasync function optionalScore(\n client: McClient,\n kind: Kind,\n slug: string,\n source: \"critic\" | \"user\",\n notes: string[],\n): Promise<ScoreSummary | null> {\n try {\n const { data } = await client.getScore(kind, slug, source);\n return data;\n } catch (error) {\n if (error instanceof McError && error.code === \"not_found\") {\n notes.push(`Metacritic publishes no ${source} score for this entry.`);\n } else {\n const reason = error instanceof McError ? error.code : \"an unexpected error\";\n notes.push(\n `The ${source} score could not be read (${reason}), so it is missing here rather than absent from Metacritic. Call again to retry.`,\n );\n }\n return null;\n }\n}\n\nasync function watchOffers(\n client: McClient,\n item: TitleDetail,\n kind: Kind,\n notes: string[],\n): Promise<WatchOffer[]> {\n if (kind === \"game\") {\n notes.push(\"Streaming offers do not apply to games.\");\n return [];\n }\n if (!item.imdbId) {\n notes.push(\"This entry carries no IMDb id, which is what streaming offers are keyed by.\");\n return [];\n }\n try {\n const { data } = await client.getWatchOffers(item.imdbId, kind);\n return data;\n } catch {\n notes.push(\"Streaming offers could not be read for this entry.\");\n return [];\n }\n}\n\n/**\n * The text block, built from the same sections the structured output was.\n *\n * A client that renders only text must see the answer to what it asked for and\n * nothing it did not: showing genres when 'basic' was not requested contradicts\n * the structured payload, and omitting the streaming offers hides a section the\n * caller paid a request for.\n */\nfunction render(\n item: TitleDetail,\n description: string,\n critic: ScoreSummary | null,\n user: ScoreSummary | null,\n wanted: Set<Section>,\n offers: WatchOffer[],\n production: Array<{ name: string }>,\n): string {\n // Some titles already carry their year, so it is only appended when absent.\n const yearShown = item.year !== null && !item.title.includes(`(${item.year})`);\n const header = [item.title, yearShown ? `(${item.year})` : \"\", `· ${item.kind}`]\n .filter(Boolean)\n .join(\" \");\n\n const lines = [header];\n if (wanted.has(\"basic\")) {\n if (item.genres.length > 0) lines.push(`Genres: ${item.genres.join(\", \")}`);\n if (item.duration !== null) lines.push(`Runtime: ${item.duration} min`);\n }\n if (critic?.score !== null && critic !== null) {\n lines.push(\n `Critics: ${critic.score}/${critic.max} from ${critic.reviewCount ?? \"?\"} reviews${critic.sentiment ? ` (${critic.sentiment})` : \"\"}`,\n );\n }\n if (user?.score !== null && user !== null) {\n lines.push(\n `Users: ${user.score}/${user.max} from ${user.reviewCount ?? \"?\"} ratings${user.sentiment ? ` (${user.sentiment})` : \"\"}`,\n );\n }\n if (description) lines.push(\"\", description);\n\n if (wanted.has(\"awards\") && item.awards.length > 0) {\n lines.push(\"\", \"Awards:\");\n for (const a of item.awards) {\n const tally = [\n a.wins === null ? \"\" : `${a.wins} win${a.wins === 1 ? \"\" : \"s\"}`,\n a.nominations === null\n ? \"\"\n : `${a.nominations} nomination${a.nominations === 1 ? \"\" : \"s\"}`,\n ].filter(Boolean);\n lines.push(` ${a.ceremony}${tally.length > 0 ? `: ${tally.join(\", \")}` : \"\"}`);\n }\n }\n\n if (wanted.has(\"networks\") && item.networks.length > 0) {\n lines.push(\"\", `Networks: ${item.networks.join(\", \")}`);\n }\n\n if (wanted.has(\"production\")) {\n lines.push(\n \"\",\n production.length === 0\n ? \"Production: no company credited.\"\n : `Production: ${production.map((company) => company.name).join(\", \")}`,\n );\n }\n\n if (wanted.has(\"where_to_watch\")) {\n lines.push(\"\", offers.length === 0 ? \"Where to watch: nothing listed.\" : \"Where to watch:\");\n for (const o of offers) lines.push(` ${o.provider} (${o.kind})${o.url ? ` — ${o.url}` : \"\"}`);\n }\n\n return lines.join(\"\\n\");\n}\n","/**\n * search_titles: find an entry and its slug.\n *\n * Rows carry the critic Metascore, so the common question about the critical\n * verdict is answered in one call. They carry no audience score, because the\n * search route does not report one.\n */\n\nimport { z } from \"zod\";\nimport type { McClient } from \"../mc/client.js\";\nimport type { Kind } from \"../types.js\";\nimport {\n ok,\n renderTitleList,\n titleSummarySchema,\n toTitleSummaryOut,\n toToolError,\n type ToolResult,\n} from \"./shared.js\";\n\nexport const searchTitlesDescription = [\n \"Search Metacritic for films, shows and games by title.\",\n \"Returns one compact row per match, carrying the critic Metascore, so a question about the critical verdict needs no second call.\",\n \"Search rows carry no audience score: use get_title for that, or browse_titles, which does return it.\",\n \"Use the slug and kind with get_title for the full entry, or with get_reviews for what critics wrote.\",\n \"There is no paging: 'limit' is the only lever, and results always start from the most relevant match.\",\n \"This searches titles only. It cannot find an entry from a plot detail, a person or a studio.\",\n].join(\" \");\n\nexport const searchTitlesInputShape = {\n query: z.string().min(1).describe(\"Title or part of one, for example 'the matrix'.\"),\n kind: z\n .enum([\"movie\", \"show\", \"game\", \"any\"])\n .default(\"any\")\n .describe(\"Restrict results to one catalogue. Metacritic searches all three at once.\"),\n limit: z.number().int().min(1).max(50).default(10).describe(\"How many rows to return.\"),\n};\n\nexport const searchTitlesOutputShape = {\n query: z.string(),\n results: z.array(titleSummarySchema),\n total_available: z\n .number()\n .int()\n .describe(\n \"How many entries Metacritic counted. For a query of several words it counts entries \" +\n \"matching any one of them, so the number is far larger than the useful matches and should \" +\n \"not be read as a match count.\",\n ),\n notes: z.array(z.string()),\n};\n\nexport interface SearchTitlesArgs {\n query: string;\n kind: Kind | \"any\";\n limit: number;\n}\n\nexport async function runSearchTitles(\n client: McClient,\n args: SearchTitlesArgs,\n): Promise<ToolResult> {\n try {\n // The upstream search has no type filter, so a narrowed search must ask for\n // more rows than requested and filter here, or a kind with few matches would\n // come back empty while results exist further down the list.\n const upstreamLimit = args.kind === \"any\" ? args.limit : Math.min(50, args.limit * 3);\n const { data, cached } = await client.search(args.query, upstreamLimit, 0);\n\n const matching =\n args.kind === \"any\" ? data.titles : data.titles.filter((title) => title.kind === args.kind);\n const results = matching.slice(0, args.limit).map(toTitleSummaryOut);\n\n // Filtering by kind happens here, over a fixed upstream window, so the\n // ceiling is what that window held rather than what the catalogue holds.\n const windowExhausted = args.kind !== \"any\" && matching.length === results.length;\n\n const notes: string[] = [];\n if (cached) notes.push(\"Served from this server's short-lived in-memory cache.\");\n\n // A query of several words is counted loosely upstream: \"the matrix\" reports\n // over 55 000 entries because it counts anything matching either word. Saying\n // \"narrow the query\" on the back of that number would be advice built on a\n // count that does not mean what it appears to mean.\n const looseCount = /\\s/.test(args.query.trim());\n if (looseCount) {\n notes.push(\n \"Metacritic counts a multi-word query loosely, so total_available is far larger than the number of real matches. The rows themselves are ordered by relevance.\",\n );\n } else if (data.totalResults > results.length) {\n notes.push(\n `${data.totalResults} entries matched and ${results.length} are shown. Raise 'limit' for more, or narrow the query: this tool cannot page.`,\n );\n }\n if (results.length === 0) {\n notes.push(\n args.kind === \"any\"\n ? \"No entry matched. Metacritic matches on the title alone, so try a shorter fragment of it.\"\n : `No ${args.kind} matched. Other kinds may still match: call again with kind=\"any\".`,\n );\n } else if (windowExhausted && results.length < args.limit) {\n notes.push(\n `Every ${args.kind} in the window Metacritic returned is shown. There may be more further down its ranking, which this tool cannot reach.`,\n );\n }\n\n const summary =\n results.length === 0\n ? `No Metacritic entry matched \"${args.query}\".`\n : `${results.length} entr${results.length === 1 ? \"y\" : \"ies\"} for \"${args.query}\":\\n${renderTitleList(results)}`;\n\n return ok({ query: args.query, results, total_available: data.totalResults, notes }, summary, {\n notes,\n });\n } catch (error) {\n return toToolError(error);\n }\n}\n"],"mappings":"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","names":["z","z","z","z","z","z","z","z","z","z"]}
|
package/dist/mc/client.js
CHANGED
package/dist/mc/client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/version.ts","../../src/config.ts","../../src/mc/cache.ts","../../src/errors.ts","../../src/mc/rateLimiter.ts","../../src/mc/http.ts","../../src/mc/paths.ts","../../src/mc/urls.ts","../../src/mc/parse.ts","../../src/mc/client.ts"],"sourcesContent":["/**\n * Kept in sync with package.json and server.json. The User-Agent embeds it, so\n * a caller reading their logs can tell which build reached them.\n */\nexport const PKG_VERSION = \"1.0.3\";\nexport const REPO_URL = \"https://github.com/smeet666/mcp-metacritic\";\n","/**\n * Runtime configuration, read from environment variables.\n *\n * A bad value never crashes the process: an MCP server that dies at startup\n * because of a typo in a client config file is very hard to diagnose from the\n * host application, so invalid input is reported on stderr and ignored.\n */\n\nimport { PKG_VERSION, REPO_URL } from \"./version.js\";\n\nexport type LogLevel = \"silent\" | \"error\" | \"info\" | \"debug\";\n\nexport interface Config {\n userAgent: string;\n minIntervalMs: number;\n timeoutMs: number;\n maxRetries: number;\n cacheTtlMs: number;\n scoresCacheTtlMs: number;\n cacheMaxEntries: number;\n logLevel: LogLevel;\n}\n\nexport const DEFAULT_USER_AGENT = `mcp-metacritic v${PKG_VERSION} (${REPO_URL})`;\n\nexport const DEFAULTS = {\n minIntervalMs: 1000,\n timeoutMs: 15_000,\n maxRetries: 3,\n // A catalogue entry moves when a title is edited, which is rare, so a day is\n // safe. The cache lives in memory and dies with the process, so this is a\n // ceiling on one session rather than a stored copy of the site.\n cacheTtlMs: 24 * 60 * 60 * 1000,\n // Scores and reviews move as reviews come in, especially around a release,\n // so they get their own much shorter lifetime.\n scoresCacheTtlMs: 60 * 60 * 1000,\n cacheMaxEntries: 200,\n logLevel: \"error\" as LogLevel,\n};\n\n/**\n * Floor on the request interval, enforced regardless of configuration.\n *\n * Metacritic publishes no rate limit and did not throttle ten consecutive\n * requests during testing. That silence is a reason to pace deliberately rather\n * than an invitation to go fast, so a value below this floor is refused.\n */\nexport const MIN_ALLOWED_INTERVAL_MS = 500;\n\n/** Ceiling on the request interval, so a typo cannot stall the server for hours. */\nexport const MAX_ALLOWED_INTERVAL_MS = 60_000;\n\nconst LOG_LEVELS: LogLevel[] = [\"silent\", \"error\", \"info\", \"debug\"];\n\ninterface NumericRange {\n min: number;\n max: number;\n fallback: number;\n}\n\nfunction readNumber(name: string, env: NodeJS.ProcessEnv, range: NumericRange): number {\n const raw = env[name];\n if (raw === undefined || raw.trim() === \"\") return range.fallback;\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\n // Out-of-range values fall back to the default rather than being clamped to\n // the nearest bound. Clamping turns a typo into a silent behaviour change:\n // -1 retries becomes \"never retry\", and -1 cache entries disables the cache\n // outright, both of which look like working configuration.\n const rounded = Math.round(parsed);\n if (rounded < range.min || rounded > range.max) {\n warn(\n `${name}=${raw} is outside the accepted range ${range.min}-${range.max} and was ignored; ` +\n `using ${range.fallback}`,\n );\n return range.fallback;\n }\n return rounded;\n}\n\nfunction warn(message: string): void {\n process.stderr.write(`[mcp-metacritic] ${message}\\n`);\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 1000, they were asking for no\n * 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.MC_MIN_INTERVAL_MS;\n if (raw === undefined || raw.trim() === \"\") return DEFAULTS.minIntervalMs;\n\n const parsed = Number(raw);\n if (!Number.isFinite(parsed)) {\n warn(`MC_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 `MC_MIN_INTERVAL_MS=${raw} is below the ${MIN_ALLOWED_INTERVAL_MS}ms floor and was ignored; ` +\n `using ${DEFAULTS.minIntervalMs}ms. Metacritic publishes no rate limit, so this client ` +\n `sets its own.`,\n );\n return DEFAULTS.minIntervalMs;\n }\n\n // The upper bound is a guard against a typo that would stall the server for\n // hours. Unlike readNumber, it clamps rather than falling back, because the\n // default would be far faster than the value asked for: someone who wrote ten\n // minutes wants slow, and answering that with 1000ms gets politeness backwards.\n if (rounded > MAX_ALLOWED_INTERVAL_MS) {\n warn(\n `MC_MIN_INTERVAL_MS=${raw} exceeds the ${MAX_ALLOWED_INTERVAL_MS}ms ceiling; ` +\n `using ${MAX_ALLOWED_INTERVAL_MS}ms.`,\n );\n return MAX_ALLOWED_INTERVAL_MS;\n }\n\n return rounded;\n}\n\nexport function loadConfig(env: NodeJS.ProcessEnv = process.env): Config {\n const rawUserAgent = env.MC_USER_AGENT?.trim();\n const rawLogLevel = env.MC_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(`MC_LOG_LEVEL=\"${rawLogLevel}\" is unknown, using \"${DEFAULTS.logLevel}\"`);\n }\n }\n\n return {\n userAgent: rawUserAgent || DEFAULT_USER_AGENT,\n minIntervalMs: readInterval(env),\n timeoutMs: readNumber(\"MC_TIMEOUT_MS\", env, {\n min: 1000,\n max: 120_000,\n fallback: DEFAULTS.timeoutMs,\n }),\n maxRetries: readNumber(\"MC_MAX_RETRIES\", env, {\n min: 0,\n max: 10,\n fallback: DEFAULTS.maxRetries,\n }),\n cacheTtlMs: readNumber(\"MC_CACHE_TTL_MS\", env, {\n min: 0,\n max: 7 * 24 * 60 * 60 * 1000,\n fallback: DEFAULTS.cacheTtlMs,\n }),\n scoresCacheTtlMs: readNumber(\"MC_SCORES_CACHE_TTL_MS\", env, {\n min: 0,\n max: 24 * 60 * 60 * 1000,\n fallback: DEFAULTS.scoresCacheTtlMs,\n }),\n cacheMaxEntries: readNumber(\"MC_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-metacritic] ${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 * In-memory TTL + LRU cache.\n *\n * Nothing is written to disk. The cache exists so that repeated questions about\n * the same title inside one conversation do not hit Metacritic again, not to\n * build a local copy of their catalogue.\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) return undefined;\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) return;\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) break;\n this.store.delete(oldest.value);\n }\n }\n\n get size(): number {\n return this.store.size;\n }\n\n clear(): void {\n this.store.clear();\n }\n}\n","/**\n * Error taxonomy surfaced to the calling model.\n *\n * The rule this file exists to enforce: a failure must never be reported as an\n * empty result. A model that sees \"no title found\" cannot tell that apart from\n * a genuine absence, and will confidently tell the user the film does not exist.\n *\n * Metacritic makes that distinction readable. A successful response carries a\n * `data` object; a failure carries an `errors` array instead, with its own code.\n * Anything matching neither shape is a parse failure rather than an empty answer.\n */\n\nexport type ErrorCode =\n \"not_found\" | \"invalid_input\" | \"rate_limited\" | \"parse_failure\" | \"network_error\" | \"timeout\";\n\nexport interface ErrorDetails {\n url?: string;\n status?: number;\n retryAfterMs?: number;\n hint?: string;\n}\n\nexport class McError extends Error {\n constructor(\n readonly code: ErrorCode,\n message: string,\n readonly details: ErrorDetails = {},\n ) {\n super(message);\n this.name = \"McError\";\n }\n}\n\nconst ISSUES_URL = \"https://github.com/smeet666/mcp-metacritic/issues\";\n\nexport function notFound(url: string, what: string): McError {\n return new McError(\"not_found\", `Metacritic has no entry for ${what}.`, {\n url,\n status: 404,\n hint: \"Use search_titles to find an entry, then pass its slug and kind to this tool.\",\n });\n}\n\nexport function invalidInput(message: string, hint?: string): McError {\n return new McError(\"invalid_input\", message, hint ? { hint } : {});\n}\n\nexport function rateLimited(url: string, retryAfterMs: number): McError {\n return new McError(\n \"rate_limited\",\n \"Metacritic is refusing requests from this client for now. This does NOT mean the title is missing.\",\n {\n url,\n retryAfterMs,\n hint:\n `Wait about ${Math.ceil(retryAfterMs / 1000)} seconds, then call the same tool again with the ` +\n \"same arguments. If it keeps happening, raise MC_MIN_INTERVAL_MS.\",\n },\n );\n}\n\n/**\n * Raised when a response arrives but does not have the shape this server reads.\n *\n * Metacritic publishes no API documentation, so its response shapes can change\n * without notice. Failing here is what turns such a change into a report rather\n * than into answers that look empty.\n */\nexport function parseFailure(url: string, what: string): McError {\n return new McError(\n \"parse_failure\",\n `Metacritic answered, but the response did not have the expected shape (${what}).`,\n { url, hint: `Please report this, with the request you made, at ${ISSUES_URL}` },\n );\n}\n\nexport function upstreamError(url: string, status: number): McError {\n return new McError(\"network_error\", `Metacritic returned HTTP ${status}.`, {\n url,\n status,\n ...(status >= 500\n ? { hint: \"This is a problem on Metacritic's side. Try again shortly.\" }\n : {}),\n });\n}\n","/**\n * Serial request queue with an adaptive minimum interval.\n *\n * Metacritic publishes no rate limit and did not throttle consecutive requests\n * during testing, so the gap between requests is a choice this client makes\n * rather than a rule it is given. Requests run one at a time with a floor on\n * that gap. If the site ever does push back, the interval doubles and then\n * decays back down as requests succeed, which recovers faster than a fixed\n * delay and behaves better than a constant rate.\n */\n\nexport interface RateLimiterOptions {\n minIntervalMs: number;\n maxIntervalMs?: number;\n}\n\nconst DEFAULT_MAX_INTERVAL_MS = 10_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 /**\n * Queue a task. Tasks run in call order, one at a time.\n *\n * A task may send several requests, since the retry loop lives inside its\n * slot. It must call `beforeRequest` around each of them: the pacing is\n * measured between requests, and stamping only the start of the task would\n * let the next one follow a long retry chain's final request immediately.\n */\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, so every\n * request is paced from the previous one whether or not they belong to the\n * same retry chain.\n */\n async beforeRequest(): Promise<void> {\n await this.waitForSlot();\n this.lastStart = Date.now();\n }\n\n /** Called after the site signals it is under load. */\n penalize(): void {\n const next = this.intervalMs === 0 ? 250 : this.intervalMs * 2;\n this.intervalMs = Math.min(this.maxIntervalMs, next);\n }\n\n /** Called after a success, so a single hiccup 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) return;\n const elapsed = Date.now() - this.lastStart;\n // Clamped to the interval: a clock stepped backwards, by NTP or a resumed\n // VM, would otherwise make this wait for the size of the step, and the\n // queue is serial so every pending request would wait behind it.\n const remaining = Math.min(this.intervalMs, this.intervalMs - elapsed);\n if (remaining > 0) await sleep(remaining);\n }\n}\n\nexport function sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n","/**\n * HTTP layer: one GET, with backoff.\n *\n * Status codes are meaningful here, so they are read: 404 marks an unknown\n * slug, 429 and 503 mark push-back, and 5xx is worth retrying. What this layer\n * deliberately does not do is inspect the body, which is the parser's job.\n *\n * Metacritic advertises no rate limit and did not throttle during testing, so\n * the pacing is entirely self-imposed and the retry path is written for the day\n * that changes rather than for observed behaviour.\n */\n\nimport type { Config, Logger } from \"../config.js\";\nimport { McError, notFound, parseFailure, rateLimited, upstreamError } from \"../errors.js\";\nimport { RateLimiter, sleep } from \"./rateLimiter.js\";\n\nconst BACKOFF_BASE_MS = 2000;\nconst BACKOFF_FACTOR = 2;\nconst BACKOFF_MAX_MS = 20_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 capped = Math.min(BACKOFF_MAX_MS, BACKOFF_BASE_MS * BACKOFF_FACTOR ** attempt);\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 URL as text, retrying transient conditions.\n *\n * The retry loop runs inside a single limiter slot, so a queued request cannot\n * interleave with a chain that is backing off. Each attempt claims its own slot\n * through `beforeRequest`, which is what keeps the pacing honest between the\n * last request of one chain and the first request of the next.\n */\nexport async function fetchText(url: string, deps: HttpDeps): Promise<string> {\n const { config, limiter, logger } = deps;\n const doFetch = deps.fetchImpl ?? fetch;\n\n return limiter.schedule(async () => {\n let lastError: McError | undefined;\n\n // Set when the site says how long to stay away; it replaces our own guess\n // for the next attempt. Applied here rather than where it is read, so no\n // wait 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 = askedWaitMs ?? backoffDelay(attempt - 1);\n askedWaitMs = null;\n logger.info(`retry ${attempt}/${config.maxRetries} in ${delay}ms for ${url}`);\n await sleep(Math.min(delay, BACKOFF_MAX_MS));\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/json,*/*;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, url);\n logger.debug(`${lastError.code} for ${url}: ${lastError.message}`);\n continue;\n }\n\n if (status === 429 || status === 503 || status === 403) {\n limiter.penalize();\n // A server that says when to come back knows better than our own guess.\n askedWaitMs = retryAfterMs;\n lastError = rateLimited(url, retryAfterMs ?? backoffDelay(attempt));\n logger.info(\n `refused on ${url} with ${status}, interval now ${limiter.currentIntervalMs}ms`,\n );\n continue;\n }\n if (status >= 500) {\n lastError = upstreamError(url, status);\n continue;\n }\n // An unknown slug is answered with a 404 carrying an error envelope. It is\n // reported as an absence rather than as a transport problem, because the\n // caller asked for something specific and needs to know it is not there.\n if (status === 404) throw notFound(url, \"that request\");\n if (status >= 400) throw upstreamError(url, status);\n\n // An empty body is not a valid answer from any of these endpoints, and is\n // how a stressed edge sometimes refuses. Retrying is safer than handing an\n // empty document to the parser, which would read as \"nothing found\".\n const trimmed = body.trim();\n if (trimmed === \"\") {\n limiter.penalize();\n lastError = rateLimited(url, backoffDelay(attempt));\n logger.info(`empty body on ${url}, treating as a refusal`);\n continue;\n }\n\n // Every route answers with JSON. An HTML body under a success status is\n // the edge answering instead of the application, usually a challenge or\n // an error page, and it clears on its own. Retrying is right; reporting a\n // parse failure would send the caller to the bug tracker for an outage.\n if (!trimmed.startsWith(\"{\") && !trimmed.startsWith(\"[\")) {\n limiter.penalize();\n // Retried because an edge challenge clears on its own, but reported as a\n // parse failure if it never does: a body that is persistently not JSON\n // means the shape moved, and that is worth a bug report.\n lastError = parseFailure(url, \"the body is not JSON\");\n askedWaitMs = retryAfterMs;\n logger.info(`non-JSON body on ${url}, retrying`);\n continue;\n }\n\n limiter.relax();\n return body;\n }\n\n throw lastError ?? new McError(\"network_error\", `Could not fetch ${url}.`, { url });\n });\n}\n\n/** `Retry-After` carries either seconds or an HTTP date. */\nfunction parseRetryAfter(raw: string | null): number | null {\n if (!raw) return null;\n const seconds = Number(raw.trim());\n if (Number.isFinite(seconds) && seconds >= 0) return Math.round(seconds * 1000);\n const when = Date.parse(raw);\n if (Number.isNaN(when)) return null;\n return Math.max(0, when - Date.now());\n}\n\nfunction asTransportError(error: unknown, url: string): McError {\n if (error instanceof McError) return error;\n const name = error instanceof Error ? error.name : \"\";\n if (name === \"TimeoutError\" || name === \"AbortError\") {\n return new McError(\"timeout\", \"Metacritic did not answer in time.\", {\n url,\n hint: \"Raise MC_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 McError(\"network_error\", `Could not reach Metacritic: ${message}`, { url });\n}\n","/**\n * Every route and every response field this server reads, in one place.\n *\n * Metacritic documents none of this: the routes are the ones its own web\n * frontend calls, and the field names come from reading those responses. That\n * makes a rename upstream both likely and silent, so collecting them here keeps\n * the repair to a single file, and the live canary asserts each one by name so\n * a change surfaces as a named failure rather than as empty answers.\n */\n\nexport const API_BASE = \"https://backend.metacritic.com\";\nexport const SITE_BASE = \"https://www.metacritic.com\";\n\n/**\n * The API and the website disagree on plurality: the backend serves /movies/,\n * /shows/ and /games/, while the pages a reader should be linked to live at\n * /movie/, /tv/ and /game/. Both forms are needed and neither can be derived\n * from the other, so both are written out.\n */\nexport const API_SEGMENT = {\n movie: \"movies\",\n show: \"shows\",\n game: \"games\",\n} as const;\n\nexport const SITE_SEGMENT = {\n movie: \"movie\",\n show: \"tv\",\n game: \"game\",\n} as const;\n\n/** Internal type ids the browse route filters on. */\nexport const MCO_TYPE_ID = {\n show: 1,\n movie: 2,\n game: 13,\n} as const;\n\n/** The `type` string search results carry, which does not match our own naming. */\nexport const SEARCH_TYPE = {\n movie: \"movie\",\n show: \"show\",\n game: \"game-title\",\n} as const;\n\n/** Sort orders the browse route accepts. */\nexport const SORT_BY = {\n score: \"-metaScore\",\n recent: \"-releaseDate\",\n popular: \"-popularityCount\",\n} as const;\n\n/**\n * Component identifiers the backend requires on several routes. They describe\n * the widget the response was built for, and requests omitting them are refused.\n */\nexport const COMPONENT = {\n product: { componentName: \"product\", componentType: \"Product\" },\n criticStats: { componentName: \"critic-score-summary\", componentType: \"MetaScoreSummary\" },\n userStats: { componentName: \"user-score-summary\", componentType: \"MetaScoreSummary\" },\n criticReviews: { componentName: \"critic-reviews\", componentType: \"ProductReviewsSummary\" },\n userReviews: { componentName: \"user-reviews\", componentType: \"ProductReviewsSummary\" },\n} as const;\n\n/** Field names read off responses. */\nexport const FIELD = {\n data: \"data\",\n errors: \"errors\",\n item: \"item\",\n items: \"items\",\n totalResults: \"totalResults\",\n /** Reviews arrive pre-sorted into these buckets, which is the sentiment filter. */\n reviewBuckets: { all: \"default\", positive: \"positive\", neutral: \"neutral\", negative: \"negative\" },\n} as const;\n","/**\n * URL building.\n *\n * The API lives on a backend host, while the pages a reader should be sent to\n * live on the main site. Attribution links must point at the latter, which is\n * the only thing this server gives back in exchange for the data.\n */\n\nimport { invalidInput } from \"../errors.js\";\nimport type { Kind, Sentiment } from \"../types.js\";\nimport {\n API_BASE,\n API_SEGMENT,\n COMPONENT,\n MCO_TYPE_ID,\n SITE_BASE,\n SITE_SEGMENT,\n SORT_BY,\n} from \"./paths.js\";\n\ntype Params = Record<string, string | number | undefined>;\n\nfunction build(path: string, params: Params): string {\n const query = new URLSearchParams();\n for (const [key, value] of Object.entries(params)) {\n if (value !== undefined && value !== \"\") query.set(key, String(value));\n }\n const suffix = query.toString();\n return suffix ? `${API_BASE}${path}?${suffix}` : `${API_BASE}${path}`;\n}\n\n/**\n * Slugs go into the path, so a malformed one would silently change which route\n * is called. Rejecting here keeps that from reaching the network.\n */\nfunction checkSlug(slug: string): string {\n const trimmed = slug.trim();\n if (trimmed === \"\") {\n throw invalidInput(\"The slug is empty.\", \"Take it from a search_titles result.\");\n }\n if (!/^[a-z0-9][a-z0-9-]*$/i.test(trimmed)) {\n throw invalidInput(\n `\"${slug}\" does not look like a Metacritic slug.`,\n \"Slugs are lowercase words joined by hyphens, such as the-matrix. Use search_titles to get one.\",\n );\n }\n return trimmed;\n}\n\nexport function searchUrl(query: string, limit: number, offset: number): string {\n const trimmed = query.trim();\n if (trimmed === \"\") {\n throw invalidInput(\"The search query is empty.\", \"Pass a title or part of one.\");\n }\n return build(`/finder/metacritic/search/${encodeURIComponent(trimmed)}/web`, { limit, offset });\n}\n\nexport function browseUrl(options: {\n kind: Kind;\n sort: keyof typeof SORT_BY;\n genre?: string;\n limit: number;\n offset: number;\n}): string {\n return build(\"/finder/metacritic/web\", {\n sortBy: SORT_BY[options.sort],\n mcoTypeId: MCO_TYPE_ID[options.kind],\n genres: options.genre,\n limit: options.limit,\n offset: options.offset,\n });\n}\n\nexport function detailUrl(kind: Kind, slug: string): string {\n return build(`/${API_SEGMENT[kind]}/metacritic/${checkSlug(slug)}/web`, COMPONENT.product);\n}\n\nexport function scoreUrl(kind: Kind, slug: string, source: \"critic\" | \"user\"): string {\n const component = source === \"critic\" ? COMPONENT.criticStats : COMPONENT.userStats;\n return build(\n `/reviews/metacritic/${source}/${API_SEGMENT[kind]}/${checkSlug(slug)}/stats/web`,\n component,\n );\n}\n\n/**\n * Reviews for one entry.\n *\n * No limit or offset is sent. The route ignores both and always answers with\n * the same sample, so including them would only vary the URL, and with it the\n * cache key, making every page of a sample already held in memory cost a fresh\n * request. Paging happens over the sample the caller already has.\n */\nexport function reviewsUrl(options: {\n kind: Kind;\n slug: string;\n source: \"critic\" | \"user\";\n}): string {\n const component = options.source === \"critic\" ? COMPONENT.criticReviews : COMPONENT.userReviews;\n return build(\n `/reviews/metacritic/${options.source}/${API_SEGMENT[options.kind]}/${checkSlug(options.slug)}/summary/web`,\n component,\n );\n}\n\n/** Streaming offers, keyed by IMDb id rather than by Metacritic slug. */\nexport function watchUrl(imdbId: string, kind: Kind): string {\n const trimmed = imdbId.trim();\n if (!/^tt\\d+$/.test(trimmed)) {\n throw invalidInput(\n `\"${imdbId}\" is not an IMDb id.`,\n \"IMDb ids look like tt0133093. get_title returns one as imdb_id.\",\n );\n }\n return build(\"/justwatch/metacritic/offersByIMDbId\", {\n id: trimmed,\n type: kind === \"show\" ? \"show\" : \"movie\",\n });\n}\n\n/** The page a reader should be sent to, and the link attribution requires. */\nexport function titlePageUrl(kind: Kind, slug: string): string {\n return `${SITE_BASE}/${SITE_SEGMENT[kind]}/${slug}/`;\n}\n\n/** Which pre-sorted bucket of reviews to read. */\nexport function bucketFor(sentiment: Sentiment): string {\n return sentiment === \"all\" ? \"default\" : sentiment;\n}\n","/**\n * Responses to domain types.\n *\n * Every route answers with the same envelope: `{\"data\": …}` on success, or\n * `{\"errors\":[{\"code\":404,…}]}` on failure. Reading that envelope is the whole\n * job of the first half of this file, because getting it wrong is how a failure\n * turns into an answer that looks empty.\n *\n * The second half maps fields. Listing rows are trimmed here rather than in a\n * tool, so the heavy payload has no path out of this module.\n */\n\nimport { notFound, parseFailure } from \"../errors.js\";\nimport type {\n Award,\n Company,\n CriticReview,\n Kind,\n ReviewPage,\n ScoreSummary,\n Sentiment,\n TitleDetail,\n TitlePage,\n TitleSummary,\n UserReview,\n WatchOffer,\n} from \"../types.js\";\nimport { FIELD, SEARCH_TYPE } from \"./paths.js\";\nimport { bucketFor, titlePageUrl } from \"./urls.js\";\n\ntype Json = Record<string, unknown>;\n\nconst isObject = (value: unknown): value is Json =>\n typeof value === \"object\" && value !== null && !Array.isArray(value);\n\n/**\n * Read the envelope and return the payload.\n *\n * An `errors` array is the site saying it could not serve the request, which is\n * an absence when the caller named something specific. Anything that is neither\n * shape means the response is not what this server knows how to read.\n */\nexport function envelope(raw: string, url: string, what: string): Json {\n let body: unknown;\n try {\n body = JSON.parse(raw);\n } catch {\n throw parseFailure(url, \"the body is not valid JSON\");\n }\n\n if (!isObject(body)) throw parseFailure(url, \"the body is not an object\");\n\n const errors = body[FIELD.errors];\n if (Array.isArray(errors) && errors.length > 0) {\n const first = isObject(errors[0]) ? errors[0] : {};\n if (first.code === 404) throw notFound(url, what);\n throw parseFailure(\n url,\n `the site reported ${String(first.reason ?? first.code ?? \"an error\")}`,\n );\n }\n\n const data = body[FIELD.data];\n if (!isObject(data)) throw parseFailure(url, \"the response carries no data object\");\n return data;\n}\n\nconst str = (value: unknown): string | null => {\n if (typeof value !== \"string\") return null;\n const trimmed = value.trim();\n return trimmed === \"\" ? null : trimmed;\n};\n\nconst num = (value: unknown): number | null => {\n if (typeof value === \"number\" && Number.isFinite(value)) return value;\n if (typeof value === \"string\") {\n const parsed = Number(value);\n if (Number.isFinite(parsed)) return parsed;\n }\n return null;\n};\n\nconst intOf = (value: unknown): number | null => {\n const parsed = num(value);\n return parsed === null ? null : Math.trunc(parsed);\n};\n\n/** Map the site's own type wording onto ours. */\nfunction kindOf(value: unknown): Kind | null {\n const type = str(value);\n if (type === SEARCH_TYPE.movie) return \"movie\";\n if (type === SEARCH_TYPE.show) return \"show\";\n if (type === SEARCH_TYPE.game) return \"game\";\n return null;\n}\n\n/**\n * A row without an id, a slug or a kind cannot be looked up or linked, so it is\n * not a row. Returning null lets the caller skip one bad entry among many while\n * still failing loudly when every entry is unreadable.\n */\nfunction toSummary(node: unknown): TitleSummary | null {\n if (!isObject(node)) return null;\n\n const id = intOf(node.id);\n const slug = str(node.slug);\n const title = str(node.title);\n const kind = kindOf(node.type);\n if (id === null || !slug || !title || !kind) return null;\n\n const critic = isObject(node.criticScoreSummary) ? node.criticScoreSummary : {};\n\n // The two listing routes report the audience score differently: browse rows\n // carry `userScore: {score}`, search rows carry nothing at all. Both forms are\n // read, and a row with neither keeps null rather than inventing a zero.\n const userSummary = isObject(node.userScoreSummary) ? node.userScoreSummary : {};\n const userObject = isObject(node.userScore) ? node.userScore : {};\n const userScore = num(userSummary.score) ?? num(userObject.score) ?? num(node.userScore);\n\n return {\n id,\n kind,\n title,\n slug,\n year: intOf(node.premiereYear),\n releaseDate: str(node.releaseDate),\n rating: str(node.rating),\n metascore: num(critic.score),\n userScore,\n sourceUrl: titlePageUrl(kind, slug),\n };\n}\n\n/** Search and browse share this shape, so they share one reader. */\nexport function parseTitlePage(raw: string, url: string, what: string): TitlePage {\n const data = envelope(raw, url, what);\n const items = data[FIELD.items];\n if (!Array.isArray(items)) throw parseFailure(url, \"the response carries no items array\");\n\n const titles: TitleSummary[] = [];\n for (const node of items) {\n const row = toSummary(node);\n if (row) titles.push(row);\n }\n\n if (items.length > 0 && titles.length === 0) {\n throw parseFailure(url, `${items.length} rows but none could be read`);\n }\n\n return {\n titles,\n totalResults: intOf(data[FIELD.totalResults]) ?? titles.length,\n itemCount: items.length,\n };\n}\n\n/**\n * Companies attached to a title, deduplicated by name.\n *\n * The list mixes producers with every distributor, broadcaster and home-video\n * label in every territory, so a well-travelled film carries the same name a\n * dozen times under different ids: Blade Runner lists 128 entries of which\n * \"Warner Home Video\" is fifteen. The first id for a name is kept, since they\n * differ only by the release they were attached to.\n */\nfunction toCompanies(value: unknown): Company[] {\n const source = isObject(value) ? value.companies : value;\n if (!Array.isArray(source)) return [];\n\n const seen = new Set<string>();\n const out: Company[] = [];\n for (const node of source) {\n const name = isObject(node) ? str(node.name) : str(node);\n if (!name) continue;\n const key = name.toLowerCase();\n if (seen.has(key)) continue;\n seen.add(key);\n out.push({ name, id: isObject(node) ? intOf(node.id) : null });\n }\n return out;\n}\n\nfunction toStringList(value: unknown): string[] {\n if (!Array.isArray(value)) return [];\n const out: string[] = [];\n for (const node of value) {\n const name = isObject(node) ? str(node.name) : str(node);\n if (name) out.push(name);\n }\n return out;\n}\n\n/**\n * Awards are reported one entry per ceremony, as a tally rather than a list of\n * individual prizes. A ceremony where nothing was won carries `wins: null`, so\n * that is preserved instead of being flattened to zero: \"no win recorded\" and\n * \"zero wins\" are the same here, but only the source can say which it meant.\n */\nfunction toAwards(value: unknown): Award[] {\n if (!Array.isArray(value)) return [];\n const out: Award[] = [];\n for (const node of value) {\n if (!isObject(node)) continue;\n const ceremony = str(node.awardEvent);\n if (!ceremony) continue;\n out.push({\n ceremony,\n wins: intOf(node.wins),\n nominations: intOf(node.nominations),\n });\n }\n return out;\n}\n\nexport function parseDetail(raw: string, url: string, kind: Kind, slug: string): TitleDetail {\n const data = envelope(raw, url, `${kind} \"${slug}\"`);\n const item = data[FIELD.item];\n if (!isObject(item)) throw parseFailure(url, \"the response carries no item object\");\n\n // The detail route omits `type`, so the kind comes from the caller, who chose\n // the route. Building the summary by hand keeps that difference explicit.\n const id = intOf(item.id);\n const title = str(item.title);\n if (id === null || !title) throw parseFailure(url, \"the entry has no id or no title\");\n\n const critic = isObject(item.criticScoreSummary) ? item.criticScoreSummary : {};\n\n return {\n id,\n kind,\n title,\n slug,\n year: intOf(item.premiereYear),\n releaseDate: str(item.releaseDate),\n rating: str(item.rating),\n metascore: num(critic.score),\n userScore: null,\n sourceUrl: titlePageUrl(kind, slug),\n description: str(item.description),\n tagline: str(item.tagline),\n genres: toStringList(item.genres),\n duration: intOf(item.duration),\n imdbId: str(item.imdbId),\n networks: toStringList(item.networks),\n production: toCompanies(item.production),\n awards: toAwards(item.awards),\n criticScoreDetail: null,\n userScoreDetail: null,\n };\n}\n\n/**\n * A score summary, with its scale.\n *\n * `max` comes from the response rather than from the source name, because the\n * two scales differ and hard-coding them here would be a guess that survives\n * quietly if the site ever changes one.\n */\nexport function parseScore(raw: string, url: string, what: string): ScoreSummary {\n const data = envelope(raw, url, what);\n const item = data[FIELD.item];\n if (!isObject(item)) throw parseFailure(url, \"the response carries no score object\");\n\n const max = num(item.max);\n if (max === null) throw parseFailure(url, \"the score carries no scale\");\n\n return {\n score: num(item.score),\n max,\n reviewCount: intOf(item.reviewCount),\n positiveCount: intOf(item.positiveCount),\n neutralCount: intOf(item.neutralCount),\n negativeCount: intOf(item.negativeCount),\n sentiment: str(item.sentiment),\n };\n}\n\nfunction reviewBucket(data: Json, url: string, sentiment: Sentiment): unknown[] {\n const item = data[FIELD.item];\n if (!isObject(item)) throw parseFailure(url, \"the response carries no reviews object\");\n\n const bucket = item[bucketFor(sentiment)];\n // A bucket that does not exist means the site stopped grouping reviews this\n // way, which is a change worth reporting rather than an absence of reviews.\n if (!Array.isArray(bucket)) {\n throw parseFailure(url, `there is no \"${bucketFor(sentiment)}\" group of reviews`);\n }\n return bucket;\n}\n\nexport function parseCriticReviews(\n raw: string,\n url: string,\n what: string,\n sentiment: Sentiment,\n): ReviewPage<CriticReview> {\n const data = envelope(raw, url, what);\n const bucket = reviewBucket(data, url, sentiment);\n\n const reviews: CriticReview[] = [];\n for (const node of bucket) {\n if (!isObject(node)) continue;\n const quote = str(node.quote);\n const publication = str(node.publicationName);\n // A quote with no publication cannot be attributed, and attribution is the\n // condition under which this content is worth passing on at all.\n if (!quote || !publication) continue;\n reviews.push({\n quote,\n score: num(node.score),\n publication,\n author: str(node.author),\n url: str(node.url),\n date: str(node.date),\n });\n }\n\n if (bucket.length > 0 && reviews.length === 0) {\n throw parseFailure(url, `${bucket.length} reviews but none could be read`);\n }\n\n return {\n reviews,\n totalResults: intOf(data[FIELD.totalResults]) ?? reviews.length,\n itemCount: bucket.length,\n };\n}\n\nexport function parseUserReviews(\n raw: string,\n url: string,\n what: string,\n sentiment: Sentiment,\n): ReviewPage<UserReview> {\n const data = envelope(raw, url, what);\n const bucket = reviewBucket(data, url, sentiment);\n\n const reviews: UserReview[] = [];\n for (const node of bucket) {\n if (!isObject(node)) continue;\n const quote = str(node.quote);\n if (!quote) continue;\n reviews.push({ quote, score: num(node.score), date: str(node.date) });\n }\n\n if (bucket.length > 0 && reviews.length === 0) {\n throw parseFailure(url, `${bucket.length} reviews but none could be read`);\n }\n\n return {\n reviews,\n totalResults: intOf(data[FIELD.totalResults]) ?? reviews.length,\n itemCount: bucket.length,\n };\n}\n\n/**\n * Unwrap a click-tracking redirect.\n *\n * Offer links arrive as tracking URLs of about 800 characters, most of it a\n * base64 analytics payload, with the real destination in the `r` parameter.\n * Nine offers on one film came to 7 KB of redirects against 300 bytes of actual\n * links, so the destination is extracted and the wrapper dropped.\n */\nfunction unwrapLink(link: string | null): string | null {\n if (!link) return null;\n try {\n const target = new URL(link).searchParams.get(\"r\");\n if (target && /^https?:\\/\\//i.test(target)) return target;\n } catch {\n // Not a URL this function understands; the original is still usable.\n }\n return link;\n}\n\n/** Streaming offers, grouped upstream by how you pay for them. */\nexport function parseOffers(raw: string, url: string, what: string): WatchOffer[] {\n const data = envelope(raw, url, what);\n const item = data[FIELD.item];\n if (!isObject(item)) throw parseFailure(url, \"the response carries no offers object\");\n\n const offers: WatchOffer[] = [];\n for (const [group, value] of Object.entries(item)) {\n if (!Array.isArray(value)) continue;\n for (const node of value) {\n if (!isObject(node)) continue;\n const provider = str(node.providerName);\n if (!provider) continue;\n offers.push({ provider, kind: group, url: unwrapLink(str(node.link)) });\n }\n }\n return offers;\n}\n","/**\n * High-level Metacritic client.\n *\n * This module knows nothing about MCP, which keeps it testable against plain\n * strings and usable as a library through the `./client` export.\n */\n\nimport type { Config, Logger } from \"../config.js\";\nimport {\n DEFAULT_USER_AGENT,\n MIN_ALLOWED_INTERVAL_MS,\n createLogger,\n loadConfig,\n} from \"../config.js\";\nimport type {\n CriticReview,\n Kind,\n ReviewPage,\n ScoreSummary,\n Sentiment,\n TitleDetail,\n TitlePage,\n UserReview,\n WatchOffer,\n} from \"../types.js\";\nimport { TtlLruCache } from \"./cache.js\";\nimport { fetchText } from \"./http.js\";\nimport {\n parseCriticReviews,\n parseDetail,\n parseOffers,\n parseScore,\n parseTitlePage,\n parseUserReviews,\n} from \"./parse.js\";\nimport type { SORT_BY } from \"./paths.js\";\nimport { RateLimiter } from \"./rateLimiter.js\";\nimport { browseUrl, detailUrl, reviewsUrl, scoreUrl, searchUrl, watchUrl } from \"./urls.js\";\n\nexport interface McClientOptions {\n config?: Config;\n logger?: Logger;\n fetchImpl?: typeof fetch;\n}\n\nexport interface Outcome<T> {\n data: T;\n /** True when served from the in-memory cache rather than the network. */\n cached: boolean;\n}\n\nexport type Sort = keyof typeof SORT_BY;\n\n/**\n * Apply the guarantees this project makes about its own traffic.\n *\n * The environment parser already enforces both, but `McClient` is published as\n * a library through the `./client` export and takes a caller-built config, so\n * without this the pacing floor and the honest identity are optional for anyone\n * importing it. They are the two things this server offers in exchange for\n * reading a source that publishes no terms, so they hold on every path.\n *\n * A caller may still name their own application in the User-Agent, and there\n * are good reasons to. Passing the traffic off as a browser is a different\n * thing, and gets the project's own identity appended so it stays attributable.\n */\nfunction withGuarantees(config: Config): Config {\n const userAgent = /mozilla\\/|applewebkit|chrome\\/|safari\\/|gecko/i.test(config.userAgent)\n ? `${config.userAgent} ${DEFAULT_USER_AGENT}`\n : config.userAgent;\n return {\n ...config,\n userAgent,\n minIntervalMs: Math.max(MIN_ALLOWED_INTERVAL_MS, config.minIntervalMs),\n };\n}\n\nexport class McClient {\n private readonly config: Config;\n private readonly logger: Logger;\n private readonly limiter: RateLimiter;\n /**\n * Two caches, because the two kinds of data age at different speeds. A\n * catalogue entry changes when someone edits it, which is rare. A score moves\n * every time a review lands, which around a release is constantly.\n */\n private readonly catalogueCache: TtlLruCache<unknown>;\n private readonly scoresCache: TtlLruCache<unknown>;\n private readonly fetchImpl: typeof fetch | undefined;\n\n constructor(options: McClientOptions = {}) {\n this.config = withGuarantees(options.config ?? loadConfig());\n this.logger = options.logger ?? createLogger(this.config.logLevel);\n this.limiter = new RateLimiter({ minIntervalMs: this.config.minIntervalMs });\n this.catalogueCache = new TtlLruCache<unknown>(\n this.config.cacheMaxEntries,\n this.config.cacheTtlMs,\n );\n this.scoresCache = new TtlLruCache<unknown>(\n this.config.cacheMaxEntries,\n this.config.scoresCacheTtlMs,\n );\n this.fetchImpl = options.fetchImpl;\n }\n\n /**\n * Search across films, shows and games.\n *\n * The return type carries rows only. The upstream response embeds artwork\n * variants on every match, and dropping them here rather than in a tool means\n * they have no path out of this layer.\n */\n async search(query: string, limit: number, offset: number): Promise<Outcome<TitlePage>> {\n const url = searchUrl(query, limit, offset);\n return this.fetchParsed(url, this.catalogueCache, (body) =>\n parseTitlePage(body, url, `the query \"${query}\"`),\n );\n }\n\n async browse(options: {\n kind: Kind;\n sort: Sort;\n genre?: string;\n limit: number;\n offset: number;\n }): Promise<Outcome<TitlePage>> {\n const url = browseUrl(options);\n return this.fetchParsed(url, this.catalogueCache, (body) =>\n parseTitlePage(body, url, `that ${options.kind} listing`),\n );\n }\n\n async getDetail(kind: Kind, slug: string): Promise<Outcome<TitleDetail>> {\n const url = detailUrl(kind, slug);\n // The trimmed form is what reached the network, so it is also what the\n // public link and the echoed slug must be built from.\n const clean = slug.trim();\n return this.fetchParsed(url, this.catalogueCache, (body) =>\n parseDetail(body, url, kind, clean),\n );\n }\n\n async getScore(\n kind: Kind,\n slug: string,\n source: \"critic\" | \"user\",\n ): Promise<Outcome<ScoreSummary>> {\n const url = scoreUrl(kind, slug, source);\n return this.fetchParsed(url, this.scoresCache, (body) =>\n parseScore(body, url, `the ${source} score of ${kind} \"${slug}\"`),\n );\n }\n\n async getCriticReviews(options: {\n kind: Kind;\n slug: string;\n sentiment: Sentiment;\n limit: number;\n offset: number;\n }): Promise<Outcome<ReviewPage<CriticReview>>> {\n const url = reviewsUrl({ ...options, source: \"critic\" });\n return this.fetchParsed(\n url,\n this.scoresCache,\n (body) =>\n parseCriticReviews(\n body,\n url,\n `reviews of ${options.kind} \"${options.slug}\"`,\n options.sentiment,\n ),\n options.sentiment,\n );\n }\n\n async getUserReviews(options: {\n kind: Kind;\n slug: string;\n sentiment: Sentiment;\n limit: number;\n offset: number;\n }): Promise<Outcome<ReviewPage<UserReview>>> {\n const url = reviewsUrl({ ...options, source: \"user\" });\n return this.fetchParsed(\n url,\n this.scoresCache,\n (body) =>\n parseUserReviews(\n body,\n url,\n `reviews of ${options.kind} \"${options.slug}\"`,\n options.sentiment,\n ),\n options.sentiment,\n );\n }\n\n async getWatchOffers(imdbId: string, kind: Kind): Promise<Outcome<WatchOffer[]>> {\n const url = watchUrl(imdbId, kind);\n return this.fetchParsed(url, this.scoresCache, (body) =>\n parseOffers(body, url, `streaming offers for ${imdbId}`),\n );\n }\n\n /**\n * Fetch, parse, then cache. In that order: a response that could not be read\n * is never stored, so a bad minute upstream cannot be replayed from memory\n * for the rest of the cache lifetime.\n */\n private async fetchParsed<T>(\n url: string,\n cache: TtlLruCache<unknown>,\n parse: (body: string) => T,\n /**\n * Distinguishes parses of the same response. The reviews route returns every\n * sentiment in one payload, so two calls differing only by sentiment share a\n * URL: keying on the URL alone would serve the first answer to the second.\n */\n variant = \"\",\n ): Promise<Outcome<T>> {\n const key = variant ? `${url}#${variant}` : url;\n const hit = cache.get(key);\n if (hit !== undefined) {\n this.logger.debug(`cache hit ${key}`);\n return { data: hit as T, cached: true };\n }\n\n const body = await fetchText(url, {\n config: this.config,\n limiter: this.limiter,\n logger: this.logger,\n ...(this.fetchImpl ? { fetchImpl: this.fetchImpl } : {}),\n });\n\n const data = parse(body);\n cache.set(key, data);\n return { data, cached: false };\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/version.ts","../../src/config.ts","../../src/mc/cache.ts","../../src/errors.ts","../../src/mc/rateLimiter.ts","../../src/mc/http.ts","../../src/mc/paths.ts","../../src/mc/urls.ts","../../src/mc/parse.ts","../../src/mc/client.ts"],"sourcesContent":["/**\n * Kept in sync with package.json and server.json. The User-Agent embeds it, so\n * a caller reading their logs can tell which build reached them.\n */\nexport const PKG_VERSION = \"1.1.0\";\nexport const REPO_URL = \"https://github.com/smeet666/mcp-metacritic\";\n","/**\n * Runtime configuration, read from environment variables.\n *\n * A bad value never crashes the process: an MCP server that dies at startup\n * because of a typo in a client config file is very hard to diagnose from the\n * host application, so invalid input is reported on stderr and ignored.\n */\n\nimport { PKG_VERSION, REPO_URL } from \"./version.js\";\n\nexport type LogLevel = \"silent\" | \"error\" | \"info\" | \"debug\";\n\nexport interface Config {\n userAgent: string;\n minIntervalMs: number;\n timeoutMs: number;\n maxRetries: number;\n cacheTtlMs: number;\n scoresCacheTtlMs: number;\n cacheMaxEntries: number;\n logLevel: LogLevel;\n}\n\nexport const DEFAULT_USER_AGENT = `mcp-metacritic v${PKG_VERSION} (${REPO_URL})`;\n\nexport const DEFAULTS = {\n minIntervalMs: 1000,\n timeoutMs: 15_000,\n maxRetries: 3,\n // A catalogue entry moves when a title is edited, which is rare, so a day is\n // safe. The cache lives in memory and dies with the process, so this is a\n // ceiling on one session rather than a stored copy of the site.\n cacheTtlMs: 24 * 60 * 60 * 1000,\n // Scores and reviews move as reviews come in, especially around a release,\n // so they get their own much shorter lifetime.\n scoresCacheTtlMs: 60 * 60 * 1000,\n cacheMaxEntries: 200,\n logLevel: \"error\" as LogLevel,\n};\n\n/**\n * Floor on the request interval, enforced regardless of configuration.\n *\n * Metacritic publishes no rate limit and did not throttle ten consecutive\n * requests during testing. That silence is a reason to pace deliberately rather\n * than an invitation to go fast, so a value below this floor is refused.\n */\nexport const MIN_ALLOWED_INTERVAL_MS = 500;\n\n/** Ceiling on the request interval, so a typo cannot stall the server for hours. */\nexport const MAX_ALLOWED_INTERVAL_MS = 60_000;\n\nconst LOG_LEVELS: LogLevel[] = [\"silent\", \"error\", \"info\", \"debug\"];\n\ninterface NumericRange {\n min: number;\n max: number;\n fallback: number;\n}\n\nfunction readNumber(name: string, env: NodeJS.ProcessEnv, range: NumericRange): number {\n const raw = env[name];\n if (raw === undefined || raw.trim() === \"\") return range.fallback;\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\n // Out-of-range values fall back to the default rather than being clamped to\n // the nearest bound. Clamping turns a typo into a silent behaviour change:\n // -1 retries becomes \"never retry\", and -1 cache entries disables the cache\n // outright, both of which look like working configuration.\n const rounded = Math.round(parsed);\n if (rounded < range.min || rounded > range.max) {\n warn(\n `${name}=${raw} is outside the accepted range ${range.min}-${range.max} and was ignored; ` +\n `using ${range.fallback}`,\n );\n return range.fallback;\n }\n return rounded;\n}\n\nfunction warn(message: string): void {\n process.stderr.write(`[mcp-metacritic] ${message}\\n`);\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 1000, they were asking for no\n * 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.MC_MIN_INTERVAL_MS;\n if (raw === undefined || raw.trim() === \"\") return DEFAULTS.minIntervalMs;\n\n const parsed = Number(raw);\n if (!Number.isFinite(parsed)) {\n warn(`MC_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 `MC_MIN_INTERVAL_MS=${raw} is below the ${MIN_ALLOWED_INTERVAL_MS}ms floor and was ignored; ` +\n `using ${DEFAULTS.minIntervalMs}ms. Metacritic publishes no rate limit, so this client ` +\n `sets its own.`,\n );\n return DEFAULTS.minIntervalMs;\n }\n\n // The upper bound is a guard against a typo that would stall the server for\n // hours. Unlike readNumber, it clamps rather than falling back, because the\n // default would be far faster than the value asked for: someone who wrote ten\n // minutes wants slow, and answering that with 1000ms gets politeness backwards.\n if (rounded > MAX_ALLOWED_INTERVAL_MS) {\n warn(\n `MC_MIN_INTERVAL_MS=${raw} exceeds the ${MAX_ALLOWED_INTERVAL_MS}ms ceiling; ` +\n `using ${MAX_ALLOWED_INTERVAL_MS}ms.`,\n );\n return MAX_ALLOWED_INTERVAL_MS;\n }\n\n return rounded;\n}\n\nexport function loadConfig(env: NodeJS.ProcessEnv = process.env): Config {\n const rawUserAgent = env.MC_USER_AGENT?.trim();\n const rawLogLevel = env.MC_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(`MC_LOG_LEVEL=\"${rawLogLevel}\" is unknown, using \"${DEFAULTS.logLevel}\"`);\n }\n }\n\n return {\n userAgent: rawUserAgent || DEFAULT_USER_AGENT,\n minIntervalMs: readInterval(env),\n timeoutMs: readNumber(\"MC_TIMEOUT_MS\", env, {\n min: 1000,\n max: 120_000,\n fallback: DEFAULTS.timeoutMs,\n }),\n maxRetries: readNumber(\"MC_MAX_RETRIES\", env, {\n min: 0,\n max: 10,\n fallback: DEFAULTS.maxRetries,\n }),\n cacheTtlMs: readNumber(\"MC_CACHE_TTL_MS\", env, {\n min: 0,\n max: 7 * 24 * 60 * 60 * 1000,\n fallback: DEFAULTS.cacheTtlMs,\n }),\n scoresCacheTtlMs: readNumber(\"MC_SCORES_CACHE_TTL_MS\", env, {\n min: 0,\n max: 24 * 60 * 60 * 1000,\n fallback: DEFAULTS.scoresCacheTtlMs,\n }),\n cacheMaxEntries: readNumber(\"MC_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-metacritic] ${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 * In-memory TTL + LRU cache.\n *\n * Nothing is written to disk. The cache exists so that repeated questions about\n * the same title inside one conversation do not hit Metacritic again, not to\n * build a local copy of their catalogue.\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) return undefined;\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) return;\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) break;\n this.store.delete(oldest.value);\n }\n }\n\n get size(): number {\n return this.store.size;\n }\n\n clear(): void {\n this.store.clear();\n }\n}\n","/**\n * Error taxonomy surfaced to the calling model.\n *\n * The rule this file exists to enforce: a failure must never be reported as an\n * empty result. A model that sees \"no title found\" cannot tell that apart from\n * a genuine absence, and will confidently tell the user the film does not exist.\n *\n * Metacritic makes that distinction readable. A successful response carries a\n * `data` object; a failure carries an `errors` array instead, with its own code.\n * Anything matching neither shape is a parse failure rather than an empty answer.\n */\n\nexport type ErrorCode =\n \"not_found\" | \"invalid_input\" | \"rate_limited\" | \"parse_failure\" | \"network_error\" | \"timeout\";\n\nexport interface ErrorDetails {\n url?: string;\n status?: number;\n retryAfterMs?: number;\n hint?: string;\n}\n\nexport class McError extends Error {\n constructor(\n readonly code: ErrorCode,\n message: string,\n readonly details: ErrorDetails = {},\n ) {\n super(message);\n this.name = \"McError\";\n }\n}\n\nconst ISSUES_URL = \"https://github.com/smeet666/mcp-metacritic/issues\";\n\nexport function notFound(url: string, what: string): McError {\n return new McError(\"not_found\", `Metacritic has no entry for ${what}.`, {\n url,\n status: 404,\n hint: \"Use search_titles to find an entry, then pass its slug and kind to this tool.\",\n });\n}\n\nexport function invalidInput(message: string, hint?: string): McError {\n return new McError(\"invalid_input\", message, hint ? { hint } : {});\n}\n\nexport function rateLimited(url: string, retryAfterMs: number): McError {\n return new McError(\n \"rate_limited\",\n \"Metacritic is refusing requests from this client for now. This does NOT mean the title is missing.\",\n {\n url,\n retryAfterMs,\n hint:\n `Wait about ${Math.ceil(retryAfterMs / 1000)} seconds, then call the same tool again with the ` +\n \"same arguments. If it keeps happening, raise MC_MIN_INTERVAL_MS.\",\n },\n );\n}\n\n/**\n * Raised when a response arrives but does not have the shape this server reads.\n *\n * Metacritic publishes no API documentation, so its response shapes can change\n * without notice. Failing here is what turns such a change into a report rather\n * than into answers that look empty.\n */\nexport function parseFailure(url: string, what: string): McError {\n return new McError(\n \"parse_failure\",\n `Metacritic answered, but the response did not have the expected shape (${what}).`,\n { url, hint: `Please report this, with the request you made, at ${ISSUES_URL}` },\n );\n}\n\nexport function upstreamError(url: string, status: number): McError {\n return new McError(\"network_error\", `Metacritic returned HTTP ${status}.`, {\n url,\n status,\n ...(status >= 500\n ? { hint: \"This is a problem on Metacritic's side. Try again shortly.\" }\n : {}),\n });\n}\n","/**\n * Serial request queue with an adaptive minimum interval.\n *\n * Metacritic publishes no rate limit and did not throttle consecutive requests\n * during testing, so the gap between requests is a choice this client makes\n * rather than a rule it is given. Requests run one at a time with a floor on\n * that gap. If the site ever does push back, the interval doubles and then\n * decays back down as requests succeed, which recovers faster than a fixed\n * delay and behaves better than a constant rate.\n */\n\nexport interface RateLimiterOptions {\n minIntervalMs: number;\n maxIntervalMs?: number;\n}\n\nconst DEFAULT_MAX_INTERVAL_MS = 10_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 /**\n * Queue a task. Tasks run in call order, one at a time.\n *\n * A task may send several requests, since the retry loop lives inside its\n * slot. It must call `beforeRequest` around each of them: the pacing is\n * measured between requests, and stamping only the start of the task would\n * let the next one follow a long retry chain's final request immediately.\n */\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, so every\n * request is paced from the previous one whether or not they belong to the\n * same retry chain.\n */\n async beforeRequest(): Promise<void> {\n await this.waitForSlot();\n this.lastStart = Date.now();\n }\n\n /** Called after the site signals it is under load. */\n penalize(): void {\n const next = this.intervalMs === 0 ? 250 : this.intervalMs * 2;\n this.intervalMs = Math.min(this.maxIntervalMs, next);\n }\n\n /** Called after a success, so a single hiccup 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) return;\n const elapsed = Date.now() - this.lastStart;\n // Clamped to the interval: a clock stepped backwards, by NTP or a resumed\n // VM, would otherwise make this wait for the size of the step, and the\n // queue is serial so every pending request would wait behind it.\n const remaining = Math.min(this.intervalMs, this.intervalMs - elapsed);\n if (remaining > 0) await sleep(remaining);\n }\n}\n\nexport function sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n","/**\n * HTTP layer: one GET, with backoff.\n *\n * Status codes are meaningful here, so they are read: 404 marks an unknown\n * slug, 429 and 503 mark push-back, and 5xx is worth retrying. What this layer\n * deliberately does not do is inspect the body, which is the parser's job.\n *\n * Metacritic advertises no rate limit and did not throttle during testing, so\n * the pacing is entirely self-imposed and the retry path is written for the day\n * that changes rather than for observed behaviour.\n */\n\nimport type { Config, Logger } from \"../config.js\";\nimport { McError, notFound, parseFailure, rateLimited, upstreamError } from \"../errors.js\";\nimport { RateLimiter, sleep } from \"./rateLimiter.js\";\n\nconst BACKOFF_BASE_MS = 2000;\nconst BACKOFF_FACTOR = 2;\nconst BACKOFF_MAX_MS = 20_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 capped = Math.min(BACKOFF_MAX_MS, BACKOFF_BASE_MS * BACKOFF_FACTOR ** attempt);\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 URL as text, retrying transient conditions.\n *\n * The retry loop runs inside a single limiter slot, so a queued request cannot\n * interleave with a chain that is backing off. Each attempt claims its own slot\n * through `beforeRequest`, which is what keeps the pacing honest between the\n * last request of one chain and the first request of the next.\n */\nexport async function fetchText(url: string, deps: HttpDeps): Promise<string> {\n const { config, limiter, logger } = deps;\n const doFetch = deps.fetchImpl ?? fetch;\n\n return limiter.schedule(async () => {\n let lastError: McError | undefined;\n\n // Set when the site says how long to stay away; it replaces our own guess\n // for the next attempt. Applied here rather than where it is read, so no\n // wait 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 = askedWaitMs ?? backoffDelay(attempt - 1);\n askedWaitMs = null;\n logger.info(`retry ${attempt}/${config.maxRetries} in ${delay}ms for ${url}`);\n await sleep(Math.min(delay, BACKOFF_MAX_MS));\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/json,*/*;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, url);\n logger.debug(`${lastError.code} for ${url}: ${lastError.message}`);\n continue;\n }\n\n if (status === 429 || status === 503 || status === 403) {\n limiter.penalize();\n // A server that says when to come back knows better than our own guess.\n askedWaitMs = retryAfterMs;\n lastError = rateLimited(url, retryAfterMs ?? backoffDelay(attempt));\n logger.info(\n `refused on ${url} with ${status}, interval now ${limiter.currentIntervalMs}ms`,\n );\n continue;\n }\n if (status >= 500) {\n lastError = upstreamError(url, status);\n continue;\n }\n // An unknown slug is answered with a 404 carrying an error envelope. It is\n // reported as an absence rather than as a transport problem, because the\n // caller asked for something specific and needs to know it is not there.\n if (status === 404) throw notFound(url, \"that request\");\n if (status >= 400) throw upstreamError(url, status);\n\n // An empty body is not a valid answer from any of these endpoints, and is\n // how a stressed edge sometimes refuses. Retrying is safer than handing an\n // empty document to the parser, which would read as \"nothing found\".\n const trimmed = body.trim();\n if (trimmed === \"\") {\n limiter.penalize();\n lastError = rateLimited(url, backoffDelay(attempt));\n logger.info(`empty body on ${url}, treating as a refusal`);\n continue;\n }\n\n // Every route answers with JSON. An HTML body under a success status is\n // the edge answering instead of the application, usually a challenge or\n // an error page, and it clears on its own. Retrying is right; reporting a\n // parse failure would send the caller to the bug tracker for an outage.\n if (!trimmed.startsWith(\"{\") && !trimmed.startsWith(\"[\")) {\n limiter.penalize();\n // Retried because an edge challenge clears on its own, but reported as a\n // parse failure if it never does: a body that is persistently not JSON\n // means the shape moved, and that is worth a bug report.\n lastError = parseFailure(url, \"the body is not JSON\");\n askedWaitMs = retryAfterMs;\n logger.info(`non-JSON body on ${url}, retrying`);\n continue;\n }\n\n limiter.relax();\n return body;\n }\n\n throw lastError ?? new McError(\"network_error\", `Could not fetch ${url}.`, { url });\n });\n}\n\n/** `Retry-After` carries either seconds or an HTTP date. */\nfunction parseRetryAfter(raw: string | null): number | null {\n if (!raw) return null;\n const seconds = Number(raw.trim());\n if (Number.isFinite(seconds) && seconds >= 0) return Math.round(seconds * 1000);\n const when = Date.parse(raw);\n if (Number.isNaN(when)) return null;\n return Math.max(0, when - Date.now());\n}\n\nfunction asTransportError(error: unknown, url: string): McError {\n if (error instanceof McError) return error;\n const name = error instanceof Error ? error.name : \"\";\n if (name === \"TimeoutError\" || name === \"AbortError\") {\n return new McError(\"timeout\", \"Metacritic did not answer in time.\", {\n url,\n hint: \"Raise MC_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 McError(\"network_error\", `Could not reach Metacritic: ${message}`, { url });\n}\n","/**\n * Every route and every response field this server reads, in one place.\n *\n * Metacritic documents none of this: the routes are the ones its own web\n * frontend calls, and the field names come from reading those responses. That\n * makes a rename upstream both likely and silent, so collecting them here keeps\n * the repair to a single file, and the live canary asserts each one by name so\n * a change surfaces as a named failure rather than as empty answers.\n */\n\nexport const API_BASE = \"https://backend.metacritic.com\";\nexport const SITE_BASE = \"https://www.metacritic.com\";\n\n/**\n * The API and the website disagree on plurality: the backend serves /movies/,\n * /shows/ and /games/, while the pages a reader should be linked to live at\n * /movie/, /tv/ and /game/. Both forms are needed and neither can be derived\n * from the other, so both are written out.\n */\nexport const API_SEGMENT = {\n movie: \"movies\",\n show: \"shows\",\n game: \"games\",\n} as const;\n\nexport const SITE_SEGMENT = {\n movie: \"movie\",\n show: \"tv\",\n game: \"game\",\n} as const;\n\n/** Internal type ids the browse route filters on. */\nexport const MCO_TYPE_ID = {\n show: 1,\n movie: 2,\n game: 13,\n} as const;\n\n/** The `type` string search results carry, which does not match our own naming. */\nexport const SEARCH_TYPE = {\n movie: \"movie\",\n show: \"show\",\n game: \"game-title\",\n} as const;\n\n/** Sort orders the browse route accepts. */\nexport const SORT_BY = {\n score: \"-metaScore\",\n recent: \"-releaseDate\",\n popular: \"-popularityCount\",\n} as const;\n\n/**\n * Component identifiers the backend requires on several routes. They describe\n * the widget the response was built for, and requests omitting them are refused.\n */\nexport const COMPONENT = {\n product: { componentName: \"product\", componentType: \"Product\" },\n criticStats: { componentName: \"critic-score-summary\", componentType: \"MetaScoreSummary\" },\n userStats: { componentName: \"user-score-summary\", componentType: \"MetaScoreSummary\" },\n criticReviews: { componentName: \"critic-reviews\", componentType: \"ProductReviewsSummary\" },\n userReviews: { componentName: \"user-reviews\", componentType: \"ProductReviewsSummary\" },\n} as const;\n\n/** Field names read off responses. */\nexport const FIELD = {\n data: \"data\",\n errors: \"errors\",\n item: \"item\",\n items: \"items\",\n totalResults: \"totalResults\",\n /** Reviews arrive pre-sorted into these buckets, which is the sentiment filter. */\n reviewBuckets: { all: \"default\", positive: \"positive\", neutral: \"neutral\", negative: \"negative\" },\n} as const;\n","/**\n * URL building.\n *\n * The API lives on a backend host, while the pages a reader should be sent to\n * live on the main site. Attribution links must point at the latter, which is\n * the only thing this server gives back in exchange for the data.\n */\n\nimport { invalidInput } from \"../errors.js\";\nimport type { Kind, Sentiment } from \"../types.js\";\nimport {\n API_BASE,\n API_SEGMENT,\n COMPONENT,\n MCO_TYPE_ID,\n SITE_BASE,\n SITE_SEGMENT,\n SORT_BY,\n} from \"./paths.js\";\n\ntype Params = Record<string, string | number | undefined>;\n\nfunction build(path: string, params: Params): string {\n const query = new URLSearchParams();\n for (const [key, value] of Object.entries(params)) {\n if (value !== undefined && value !== \"\") query.set(key, String(value));\n }\n const suffix = query.toString();\n return suffix ? `${API_BASE}${path}?${suffix}` : `${API_BASE}${path}`;\n}\n\n/**\n * Slugs go into the path, so a malformed one would silently change which route\n * is called. Rejecting here keeps that from reaching the network.\n */\nfunction checkSlug(slug: string): string {\n const trimmed = slug.trim();\n if (trimmed === \"\") {\n throw invalidInput(\"The slug is empty.\", \"Take it from a search_titles result.\");\n }\n if (!/^[a-z0-9][a-z0-9-]*$/i.test(trimmed)) {\n throw invalidInput(\n `\"${slug}\" does not look like a Metacritic slug.`,\n \"Slugs are lowercase words joined by hyphens, such as the-matrix. Use search_titles to get one.\",\n );\n }\n return trimmed;\n}\n\nexport function searchUrl(query: string, limit: number, offset: number): string {\n const trimmed = query.trim();\n if (trimmed === \"\") {\n throw invalidInput(\"The search query is empty.\", \"Pass a title or part of one.\");\n }\n return build(`/finder/metacritic/search/${encodeURIComponent(trimmed)}/web`, { limit, offset });\n}\n\nexport function browseUrl(options: {\n kind: Kind;\n sort: keyof typeof SORT_BY;\n genre?: string;\n limit: number;\n offset: number;\n}): string {\n return build(\"/finder/metacritic/web\", {\n sortBy: SORT_BY[options.sort],\n mcoTypeId: MCO_TYPE_ID[options.kind],\n genres: options.genre,\n limit: options.limit,\n offset: options.offset,\n });\n}\n\nexport function detailUrl(kind: Kind, slug: string): string {\n return build(`/${API_SEGMENT[kind]}/metacritic/${checkSlug(slug)}/web`, COMPONENT.product);\n}\n\nexport function scoreUrl(kind: Kind, slug: string, source: \"critic\" | \"user\"): string {\n const component = source === \"critic\" ? COMPONENT.criticStats : COMPONENT.userStats;\n return build(\n `/reviews/metacritic/${source}/${API_SEGMENT[kind]}/${checkSlug(slug)}/stats/web`,\n component,\n );\n}\n\n/**\n * Reviews for one entry.\n *\n * No limit or offset is sent. The route ignores both and always answers with\n * the same sample, so including them would only vary the URL, and with it the\n * cache key, making every page of a sample already held in memory cost a fresh\n * request. Paging happens over the sample the caller already has.\n */\nexport function reviewsUrl(options: {\n kind: Kind;\n slug: string;\n source: \"critic\" | \"user\";\n}): string {\n const component = options.source === \"critic\" ? COMPONENT.criticReviews : COMPONENT.userReviews;\n return build(\n `/reviews/metacritic/${options.source}/${API_SEGMENT[options.kind]}/${checkSlug(options.slug)}/summary/web`,\n component,\n );\n}\n\n/** Streaming offers, keyed by IMDb id rather than by Metacritic slug. */\nexport function watchUrl(imdbId: string, kind: Kind): string {\n const trimmed = imdbId.trim();\n if (!/^tt\\d+$/.test(trimmed)) {\n throw invalidInput(\n `\"${imdbId}\" is not an IMDb id.`,\n \"IMDb ids look like tt0133093. get_title returns one as imdb_id.\",\n );\n }\n return build(\"/justwatch/metacritic/offersByIMDbId\", {\n id: trimmed,\n type: kind === \"show\" ? \"show\" : \"movie\",\n });\n}\n\n/** The page a reader should be sent to, and the link attribution requires. */\nexport function titlePageUrl(kind: Kind, slug: string): string {\n return `${SITE_BASE}/${SITE_SEGMENT[kind]}/${slug}/`;\n}\n\n/** Which pre-sorted bucket of reviews to read. */\nexport function bucketFor(sentiment: Sentiment): string {\n return sentiment === \"all\" ? \"default\" : sentiment;\n}\n","/**\n * Responses to domain types.\n *\n * Every route answers with the same envelope: `{\"data\": …}` on success, or\n * `{\"errors\":[{\"code\":404,…}]}` on failure. Reading that envelope is the whole\n * job of the first half of this file, because getting it wrong is how a failure\n * turns into an answer that looks empty.\n *\n * The second half maps fields. Listing rows are trimmed here rather than in a\n * tool, so the heavy payload has no path out of this module.\n */\n\nimport { notFound, parseFailure } from \"../errors.js\";\nimport type {\n Award,\n Company,\n CriticReview,\n Kind,\n ReviewPage,\n ScoreSummary,\n Sentiment,\n TitleDetail,\n TitlePage,\n TitleSummary,\n UserReview,\n WatchOffer,\n} from \"../types.js\";\nimport { FIELD, SEARCH_TYPE } from \"./paths.js\";\nimport { bucketFor, titlePageUrl } from \"./urls.js\";\n\ntype Json = Record<string, unknown>;\n\nconst isObject = (value: unknown): value is Json =>\n typeof value === \"object\" && value !== null && !Array.isArray(value);\n\n/**\n * Read the envelope and return the payload.\n *\n * An `errors` array is the site saying it could not serve the request, which is\n * an absence when the caller named something specific. Anything that is neither\n * shape means the response is not what this server knows how to read.\n */\nexport function envelope(raw: string, url: string, what: string): Json {\n let body: unknown;\n try {\n body = JSON.parse(raw);\n } catch {\n throw parseFailure(url, \"the body is not valid JSON\");\n }\n\n if (!isObject(body)) throw parseFailure(url, \"the body is not an object\");\n\n const errors = body[FIELD.errors];\n if (Array.isArray(errors) && errors.length > 0) {\n const first = isObject(errors[0]) ? errors[0] : {};\n if (first.code === 404) throw notFound(url, what);\n throw parseFailure(\n url,\n `the site reported ${String(first.reason ?? first.code ?? \"an error\")}`,\n );\n }\n\n const data = body[FIELD.data];\n if (!isObject(data)) throw parseFailure(url, \"the response carries no data object\");\n return data;\n}\n\nconst str = (value: unknown): string | null => {\n if (typeof value !== \"string\") return null;\n const trimmed = value.trim();\n return trimmed === \"\" ? null : trimmed;\n};\n\nconst num = (value: unknown): number | null => {\n if (typeof value === \"number\" && Number.isFinite(value)) return value;\n if (typeof value === \"string\") {\n const parsed = Number(value);\n if (Number.isFinite(parsed)) return parsed;\n }\n return null;\n};\n\nconst intOf = (value: unknown): number | null => {\n const parsed = num(value);\n return parsed === null ? null : Math.trunc(parsed);\n};\n\n/** Map the site's own type wording onto ours. */\nfunction kindOf(value: unknown): Kind | null {\n const type = str(value);\n if (type === SEARCH_TYPE.movie) return \"movie\";\n if (type === SEARCH_TYPE.show) return \"show\";\n if (type === SEARCH_TYPE.game) return \"game\";\n return null;\n}\n\n/**\n * A row without an id, a slug or a kind cannot be looked up or linked, so it is\n * not a row. Returning null lets the caller skip one bad entry among many while\n * still failing loudly when every entry is unreadable.\n */\nfunction toSummary(node: unknown): TitleSummary | null {\n if (!isObject(node)) return null;\n\n const id = intOf(node.id);\n const slug = str(node.slug);\n const title = str(node.title);\n const kind = kindOf(node.type);\n if (id === null || !slug || !title || !kind) return null;\n\n const critic = isObject(node.criticScoreSummary) ? node.criticScoreSummary : {};\n\n // The two listing routes report the audience score differently: browse rows\n // carry `userScore: {score}`, search rows carry nothing at all. Both forms are\n // read, and a row with neither keeps null rather than inventing a zero.\n const userSummary = isObject(node.userScoreSummary) ? node.userScoreSummary : {};\n const userObject = isObject(node.userScore) ? node.userScore : {};\n const userScore = num(userSummary.score) ?? num(userObject.score) ?? num(node.userScore);\n\n return {\n id,\n kind,\n title,\n slug,\n year: intOf(node.premiereYear),\n releaseDate: str(node.releaseDate),\n rating: str(node.rating),\n metascore: num(critic.score),\n userScore,\n sourceUrl: titlePageUrl(kind, slug),\n };\n}\n\n/** Search and browse share this shape, so they share one reader. */\nexport function parseTitlePage(raw: string, url: string, what: string): TitlePage {\n const data = envelope(raw, url, what);\n const items = data[FIELD.items];\n if (!Array.isArray(items)) throw parseFailure(url, \"the response carries no items array\");\n\n const titles: TitleSummary[] = [];\n for (const node of items) {\n const row = toSummary(node);\n if (row) titles.push(row);\n }\n\n if (items.length > 0 && titles.length === 0) {\n throw parseFailure(url, `${items.length} rows but none could be read`);\n }\n\n return {\n titles,\n totalResults: intOf(data[FIELD.totalResults]) ?? titles.length,\n itemCount: items.length,\n };\n}\n\n/**\n * Companies attached to a title, deduplicated by name.\n *\n * The list mixes producers with every distributor, broadcaster and home-video\n * label in every territory, so a well-travelled film carries the same name a\n * dozen times under different ids: Blade Runner lists 128 entries of which\n * \"Warner Home Video\" is fifteen. The first id for a name is kept, since they\n * differ only by the release they were attached to.\n */\nfunction toCompanies(value: unknown): Company[] {\n const source = isObject(value) ? value.companies : value;\n if (!Array.isArray(source)) return [];\n\n const seen = new Set<string>();\n const out: Company[] = [];\n for (const node of source) {\n const name = isObject(node) ? str(node.name) : str(node);\n if (!name) continue;\n const key = name.toLowerCase();\n if (seen.has(key)) continue;\n seen.add(key);\n out.push({ name, id: isObject(node) ? intOf(node.id) : null });\n }\n return out;\n}\n\nfunction toStringList(value: unknown): string[] {\n if (!Array.isArray(value)) return [];\n const out: string[] = [];\n for (const node of value) {\n const name = isObject(node) ? str(node.name) : str(node);\n if (name) out.push(name);\n }\n return out;\n}\n\n/**\n * Awards are reported one entry per ceremony, as a tally rather than a list of\n * individual prizes. A ceremony where nothing was won carries `wins: null`, so\n * that is preserved instead of being flattened to zero: \"no win recorded\" and\n * \"zero wins\" are the same here, but only the source can say which it meant.\n */\nfunction toAwards(value: unknown): Award[] {\n if (!Array.isArray(value)) return [];\n const out: Award[] = [];\n for (const node of value) {\n if (!isObject(node)) continue;\n const ceremony = str(node.awardEvent);\n if (!ceremony) continue;\n out.push({\n ceremony,\n wins: intOf(node.wins),\n nominations: intOf(node.nominations),\n });\n }\n return out;\n}\n\nexport function parseDetail(raw: string, url: string, kind: Kind, slug: string): TitleDetail {\n const data = envelope(raw, url, `${kind} \"${slug}\"`);\n const item = data[FIELD.item];\n if (!isObject(item)) throw parseFailure(url, \"the response carries no item object\");\n\n // The detail route omits `type`, so the kind comes from the caller, who chose\n // the route. Building the summary by hand keeps that difference explicit.\n const id = intOf(item.id);\n const title = str(item.title);\n if (id === null || !title) throw parseFailure(url, \"the entry has no id or no title\");\n\n const critic = isObject(item.criticScoreSummary) ? item.criticScoreSummary : {};\n\n return {\n id,\n kind,\n title,\n slug,\n year: intOf(item.premiereYear),\n releaseDate: str(item.releaseDate),\n rating: str(item.rating),\n metascore: num(critic.score),\n userScore: null,\n sourceUrl: titlePageUrl(kind, slug),\n description: str(item.description),\n tagline: str(item.tagline),\n genres: toStringList(item.genres),\n duration: intOf(item.duration),\n imdbId: str(item.imdbId),\n networks: toStringList(item.networks),\n production: toCompanies(item.production),\n awards: toAwards(item.awards),\n criticScoreDetail: null,\n userScoreDetail: null,\n };\n}\n\n/**\n * A score summary, with its scale.\n *\n * `max` comes from the response rather than from the source name, because the\n * two scales differ and hard-coding them here would be a guess that survives\n * quietly if the site ever changes one.\n */\nexport function parseScore(raw: string, url: string, what: string): ScoreSummary {\n const data = envelope(raw, url, what);\n const item = data[FIELD.item];\n if (!isObject(item)) throw parseFailure(url, \"the response carries no score object\");\n\n const max = num(item.max);\n if (max === null) throw parseFailure(url, \"the score carries no scale\");\n\n return {\n score: num(item.score),\n max,\n reviewCount: intOf(item.reviewCount),\n positiveCount: intOf(item.positiveCount),\n neutralCount: intOf(item.neutralCount),\n negativeCount: intOf(item.negativeCount),\n sentiment: str(item.sentiment),\n };\n}\n\nfunction reviewBucket(data: Json, url: string, sentiment: Sentiment): unknown[] {\n const item = data[FIELD.item];\n if (!isObject(item)) throw parseFailure(url, \"the response carries no reviews object\");\n\n const bucket = item[bucketFor(sentiment)];\n // A bucket that does not exist means the site stopped grouping reviews this\n // way, which is a change worth reporting rather than an absence of reviews.\n if (!Array.isArray(bucket)) {\n throw parseFailure(url, `there is no \"${bucketFor(sentiment)}\" group of reviews`);\n }\n return bucket;\n}\n\nexport function parseCriticReviews(\n raw: string,\n url: string,\n what: string,\n sentiment: Sentiment,\n): ReviewPage<CriticReview> {\n const data = envelope(raw, url, what);\n const bucket = reviewBucket(data, url, sentiment);\n\n const reviews: CriticReview[] = [];\n for (const node of bucket) {\n if (!isObject(node)) continue;\n const quote = str(node.quote);\n const publication = str(node.publicationName);\n // A quote with no publication cannot be attributed, and attribution is the\n // condition under which this content is worth passing on at all.\n if (!quote || !publication) continue;\n reviews.push({\n quote,\n score: num(node.score),\n publication,\n author: str(node.author),\n url: str(node.url),\n date: str(node.date),\n });\n }\n\n if (bucket.length > 0 && reviews.length === 0) {\n throw parseFailure(url, `${bucket.length} reviews but none could be read`);\n }\n\n return {\n reviews,\n totalResults: intOf(data[FIELD.totalResults]) ?? reviews.length,\n itemCount: bucket.length,\n };\n}\n\nexport function parseUserReviews(\n raw: string,\n url: string,\n what: string,\n sentiment: Sentiment,\n): ReviewPage<UserReview> {\n const data = envelope(raw, url, what);\n const bucket = reviewBucket(data, url, sentiment);\n\n const reviews: UserReview[] = [];\n for (const node of bucket) {\n if (!isObject(node)) continue;\n const quote = str(node.quote);\n if (!quote) continue;\n reviews.push({ quote, score: num(node.score), date: str(node.date) });\n }\n\n if (bucket.length > 0 && reviews.length === 0) {\n throw parseFailure(url, `${bucket.length} reviews but none could be read`);\n }\n\n return {\n reviews,\n totalResults: intOf(data[FIELD.totalResults]) ?? reviews.length,\n itemCount: bucket.length,\n };\n}\n\n/**\n * Unwrap a click-tracking redirect.\n *\n * Offer links arrive as tracking URLs of about 800 characters, most of it a\n * base64 analytics payload, with the real destination in the `r` parameter.\n * Nine offers on one film came to 7 KB of redirects against 300 bytes of actual\n * links, so the destination is extracted and the wrapper dropped.\n */\nfunction unwrapLink(link: string | null): string | null {\n if (!link) return null;\n try {\n const target = new URL(link).searchParams.get(\"r\");\n if (target && /^https?:\\/\\//i.test(target)) return target;\n } catch {\n // Not a URL this function understands; the original is still usable.\n }\n return link;\n}\n\n/** Streaming offers, grouped upstream by how you pay for them. */\nexport function parseOffers(raw: string, url: string, what: string): WatchOffer[] {\n const data = envelope(raw, url, what);\n const item = data[FIELD.item];\n if (!isObject(item)) throw parseFailure(url, \"the response carries no offers object\");\n\n const offers: WatchOffer[] = [];\n for (const [group, value] of Object.entries(item)) {\n if (!Array.isArray(value)) continue;\n for (const node of value) {\n if (!isObject(node)) continue;\n const provider = str(node.providerName);\n if (!provider) continue;\n offers.push({ provider, kind: group, url: unwrapLink(str(node.link)) });\n }\n }\n return offers;\n}\n","/**\n * High-level Metacritic client.\n *\n * This module knows nothing about MCP, which keeps it testable against plain\n * strings and usable as a library through the `./client` export.\n */\n\nimport type { Config, Logger } from \"../config.js\";\nimport {\n DEFAULT_USER_AGENT,\n MIN_ALLOWED_INTERVAL_MS,\n createLogger,\n loadConfig,\n} from \"../config.js\";\nimport type {\n CriticReview,\n Kind,\n ReviewPage,\n ScoreSummary,\n Sentiment,\n TitleDetail,\n TitlePage,\n UserReview,\n WatchOffer,\n} from \"../types.js\";\nimport { TtlLruCache } from \"./cache.js\";\nimport { fetchText } from \"./http.js\";\nimport {\n parseCriticReviews,\n parseDetail,\n parseOffers,\n parseScore,\n parseTitlePage,\n parseUserReviews,\n} from \"./parse.js\";\nimport type { SORT_BY } from \"./paths.js\";\nimport { RateLimiter } from \"./rateLimiter.js\";\nimport { browseUrl, detailUrl, reviewsUrl, scoreUrl, searchUrl, watchUrl } from \"./urls.js\";\n\nexport interface McClientOptions {\n config?: Config;\n logger?: Logger;\n fetchImpl?: typeof fetch;\n}\n\nexport interface Outcome<T> {\n data: T;\n /** True when served from the in-memory cache rather than the network. */\n cached: boolean;\n}\n\nexport type Sort = keyof typeof SORT_BY;\n\n/**\n * Apply the guarantees this project makes about its own traffic.\n *\n * The environment parser already enforces both, but `McClient` is published as\n * a library through the `./client` export and takes a caller-built config, so\n * without this the pacing floor and the honest identity are optional for anyone\n * importing it. They are the two things this server offers in exchange for\n * reading a source that publishes no terms, so they hold on every path.\n *\n * A caller may still name their own application in the User-Agent, and there\n * are good reasons to. Passing the traffic off as a browser is a different\n * thing, and gets the project's own identity appended so it stays attributable.\n */\nfunction withGuarantees(config: Config): Config {\n const userAgent = /mozilla\\/|applewebkit|chrome\\/|safari\\/|gecko/i.test(config.userAgent)\n ? `${config.userAgent} ${DEFAULT_USER_AGENT}`\n : config.userAgent;\n return {\n ...config,\n userAgent,\n minIntervalMs: Math.max(MIN_ALLOWED_INTERVAL_MS, config.minIntervalMs),\n };\n}\n\nexport class McClient {\n private readonly config: Config;\n private readonly logger: Logger;\n private readonly limiter: RateLimiter;\n /**\n * Two caches, because the two kinds of data age at different speeds. A\n * catalogue entry changes when someone edits it, which is rare. A score moves\n * every time a review lands, which around a release is constantly.\n */\n private readonly catalogueCache: TtlLruCache<unknown>;\n private readonly scoresCache: TtlLruCache<unknown>;\n private readonly fetchImpl: typeof fetch | undefined;\n\n constructor(options: McClientOptions = {}) {\n this.config = withGuarantees(options.config ?? loadConfig());\n this.logger = options.logger ?? createLogger(this.config.logLevel);\n this.limiter = new RateLimiter({ minIntervalMs: this.config.minIntervalMs });\n this.catalogueCache = new TtlLruCache<unknown>(\n this.config.cacheMaxEntries,\n this.config.cacheTtlMs,\n );\n this.scoresCache = new TtlLruCache<unknown>(\n this.config.cacheMaxEntries,\n this.config.scoresCacheTtlMs,\n );\n this.fetchImpl = options.fetchImpl;\n }\n\n /**\n * Search across films, shows and games.\n *\n * The return type carries rows only. The upstream response embeds artwork\n * variants on every match, and dropping them here rather than in a tool means\n * they have no path out of this layer.\n */\n async search(query: string, limit: number, offset: number): Promise<Outcome<TitlePage>> {\n const url = searchUrl(query, limit, offset);\n return this.fetchParsed(url, this.catalogueCache, (body) =>\n parseTitlePage(body, url, `the query \"${query}\"`),\n );\n }\n\n async browse(options: {\n kind: Kind;\n sort: Sort;\n genre?: string;\n limit: number;\n offset: number;\n }): Promise<Outcome<TitlePage>> {\n const url = browseUrl(options);\n return this.fetchParsed(url, this.catalogueCache, (body) =>\n parseTitlePage(body, url, `that ${options.kind} listing`),\n );\n }\n\n async getDetail(kind: Kind, slug: string): Promise<Outcome<TitleDetail>> {\n const url = detailUrl(kind, slug);\n // The trimmed form is what reached the network, so it is also what the\n // public link and the echoed slug must be built from.\n const clean = slug.trim();\n return this.fetchParsed(url, this.catalogueCache, (body) =>\n parseDetail(body, url, kind, clean),\n );\n }\n\n async getScore(\n kind: Kind,\n slug: string,\n source: \"critic\" | \"user\",\n ): Promise<Outcome<ScoreSummary>> {\n const url = scoreUrl(kind, slug, source);\n return this.fetchParsed(url, this.scoresCache, (body) =>\n parseScore(body, url, `the ${source} score of ${kind} \"${slug}\"`),\n );\n }\n\n async getCriticReviews(options: {\n kind: Kind;\n slug: string;\n sentiment: Sentiment;\n limit: number;\n offset: number;\n }): Promise<Outcome<ReviewPage<CriticReview>>> {\n const url = reviewsUrl({ ...options, source: \"critic\" });\n return this.fetchParsed(\n url,\n this.scoresCache,\n (body) =>\n parseCriticReviews(\n body,\n url,\n `reviews of ${options.kind} \"${options.slug}\"`,\n options.sentiment,\n ),\n options.sentiment,\n );\n }\n\n async getUserReviews(options: {\n kind: Kind;\n slug: string;\n sentiment: Sentiment;\n limit: number;\n offset: number;\n }): Promise<Outcome<ReviewPage<UserReview>>> {\n const url = reviewsUrl({ ...options, source: \"user\" });\n return this.fetchParsed(\n url,\n this.scoresCache,\n (body) =>\n parseUserReviews(\n body,\n url,\n `reviews of ${options.kind} \"${options.slug}\"`,\n options.sentiment,\n ),\n options.sentiment,\n );\n }\n\n async getWatchOffers(imdbId: string, kind: Kind): Promise<Outcome<WatchOffer[]>> {\n const url = watchUrl(imdbId, kind);\n return this.fetchParsed(url, this.scoresCache, (body) =>\n parseOffers(body, url, `streaming offers for ${imdbId}`),\n );\n }\n\n /**\n * Fetch, parse, then cache. In that order: a response that could not be read\n * is never stored, so a bad minute upstream cannot be replayed from memory\n * for the rest of the cache lifetime.\n */\n private async fetchParsed<T>(\n url: string,\n cache: TtlLruCache<unknown>,\n parse: (body: string) => T,\n /**\n * Distinguishes parses of the same response. The reviews route returns every\n * sentiment in one payload, so two calls differing only by sentiment share a\n * URL: keying on the URL alone would serve the first answer to the second.\n */\n variant = \"\",\n ): Promise<Outcome<T>> {\n const key = variant ? `${url}#${variant}` : url;\n const hit = cache.get(key);\n if (hit !== undefined) {\n this.logger.debug(`cache hit ${key}`);\n return { data: hit as T, cached: true };\n }\n\n const body = await fetchText(url, {\n config: this.config,\n limiter: this.limiter,\n logger: this.logger,\n ...(this.fetchImpl ? { fetchImpl: this.fetchImpl } : {}),\n });\n\n const data = parse(body);\n cache.set(key, data);\n return { data, cached: false };\n }\n}\n"],"mappings":";AAIO,IAAM,cAAc;AACpB,IAAM,WAAW;;;ACkBjB,IAAM,qBAAqB,mBAAmB,WAAW,KAAK,QAAQ;AAEtE,IAAM,WAAW;AAAA,EACtB,eAAe;AAAA,EACf,WAAW;AAAA,EACX,YAAY;AAAA;AAAA;AAAA;AAAA,EAIZ,YAAY,KAAK,KAAK,KAAK;AAAA;AAAA;AAAA,EAG3B,kBAAkB,KAAK,KAAK;AAAA,EAC5B,iBAAiB;AAAA,EACjB,UAAU;AACZ;AASO,IAAM,0BAA0B;AAGhC,IAAM,0BAA0B;AAEvC,IAAM,aAAyB,CAAC,UAAU,SAAS,QAAQ,OAAO;AAQlE,SAAS,WAAW,MAAc,KAAwB,OAA6B;AACrF,QAAM,MAAM,IAAI,IAAI;AACpB,MAAI,QAAQ,UAAa,IAAI,KAAK,MAAM,GAAI,QAAO,MAAM;AAEzD,QAAM,SAAS,OAAO,GAAG;AACzB,MAAI,CAAC,OAAO,SAAS,MAAM,GAAG;AAC5B,SAAK,GAAG,IAAI,KAAK,GAAG,4BAA4B,MAAM,QAAQ,EAAE;AAChE,WAAO,MAAM;AAAA,EACf;AAMA,QAAM,UAAU,KAAK,MAAM,MAAM;AACjC,MAAI,UAAU,MAAM,OAAO,UAAU,MAAM,KAAK;AAC9C;AAAA,MACE,GAAG,IAAI,IAAI,GAAG,kCAAkC,MAAM,GAAG,IAAI,MAAM,GAAG,2BAC3D,MAAM,QAAQ;AAAA,IAC3B;AACA,WAAO,MAAM;AAAA,EACf;AACA,SAAO;AACT;AAEA,SAAS,KAAK,SAAuB;AACnC,UAAQ,OAAO,MAAM,oBAAoB,OAAO;AAAA,CAAI;AACtD;AASA,SAAS,aAAa,KAAgC;AACpD,QAAM,MAAM,IAAI;AAChB,MAAI,QAAQ,UAAa,IAAI,KAAK,MAAM,GAAI,QAAO,SAAS;AAE5D,QAAM,SAAS,OAAO,GAAG;AACzB,MAAI,CAAC,OAAO,SAAS,MAAM,GAAG;AAC5B,SAAK,uBAAuB,GAAG,4BAA4B,SAAS,aAAa,IAAI;AACrF,WAAO,SAAS;AAAA,EAClB;AAEA,QAAM,UAAU,KAAK,MAAM,MAAM;AACjC,MAAI,UAAU,yBAAyB;AACrC;AAAA,MACE,sBAAsB,GAAG,iBAAiB,uBAAuB,mCACtD,SAAS,aAAa;AAAA,IAEnC;AACA,WAAO,SAAS;AAAA,EAClB;AAMA,MAAI,UAAU,yBAAyB;AACrC;AAAA,MACE,sBAAsB,GAAG,gBAAgB,uBAAuB,qBACrD,uBAAuB;AAAA,IACpC;AACA,WAAO;AAAA,EACT;AAEA,SAAO;AACT;AAEO,SAAS,WAAW,MAAyB,QAAQ,KAAa;AACvE,QAAM,eAAe,IAAI,eAAe,KAAK;AAC7C,QAAM,cAAc,IAAI,cAAc,KAAK,EAAE,YAAY;AAEzD,MAAI,WAAW,SAAS;AACxB,MAAI,aAAa;AACf,QAAI,WAAW,SAAS,WAAuB,GAAG;AAChD,iBAAW;AAAA,IACb,OAAO;AACL,WAAK,iBAAiB,WAAW,wBAAwB,SAAS,QAAQ,GAAG;AAAA,IAC/E;AAAA,EACF;AAEA,SAAO;AAAA,IACL,WAAW,gBAAgB;AAAA,IAC3B,eAAe,aAAa,GAAG;AAAA,IAC/B,WAAW,WAAW,iBAAiB,KAAK;AAAA,MAC1C,KAAK;AAAA,MACL,KAAK;AAAA,MACL,UAAU,SAAS;AAAA,IACrB,CAAC;AAAA,IACD,YAAY,WAAW,kBAAkB,KAAK;AAAA,MAC5C,KAAK;AAAA,MACL,KAAK;AAAA,MACL,UAAU,SAAS;AAAA,IACrB,CAAC;AAAA,IACD,YAAY,WAAW,mBAAmB,KAAK;AAAA,MAC7C,KAAK;AAAA,MACL,KAAK,IAAI,KAAK,KAAK,KAAK;AAAA,MACxB,UAAU,SAAS;AAAA,IACrB,CAAC;AAAA,IACD,kBAAkB,WAAW,0BAA0B,KAAK;AAAA,MAC1D,KAAK;AAAA,MACL,KAAK,KAAK,KAAK,KAAK;AAAA,MACpB,UAAU,SAAS;AAAA,IACrB,CAAC;AAAA,IACD,iBAAiB,WAAW,wBAAwB,KAAK;AAAA,MACvD,KAAK;AAAA,MACL,KAAK;AAAA,MACL,UAAU,SAAS;AAAA,IACrB,CAAC;AAAA,IACD;AAAA,EACF;AACF;AAEA,IAAM,aAAuC,EAAE,QAAQ,GAAG,OAAO,GAAG,MAAM,GAAG,OAAO,EAAE;AAM/E,SAAS,aAAa,OAAiB;AAC5C,QAAM,OAAO,CAAC,IAAc,YAAoB;AAC9C,QAAI,WAAW,KAAK,KAAK,WAAW,EAAE,GAAG;AACvC,cAAQ,OAAO,MAAM,oBAAoB,OAAO;AAAA,CAAI;AAAA,IACtD;AAAA,EACF;AACA,SAAO;AAAA,IACL,OAAO,CAAC,YAAoB,KAAK,SAAS,OAAO;AAAA,IACjD,MAAM,CAAC,YAAoB,KAAK,QAAQ,OAAO;AAAA,IAC/C,OAAO,CAAC,YAAoB,KAAK,SAAS,OAAO;AAAA,EACnD;AACF;;;ACjLO,IAAM,cAAN,MAAqB;AAAA,EAG1B,YACmB,YACA,OACjB;AAFiB;AACA;AAAA,EAChB;AAAA,EAFgB;AAAA,EACA;AAAA,EAJF,QAAQ,oBAAI,IAAsB;AAAA,EAOnD,IAAI,KAA4B;AAC9B,UAAM,QAAQ,KAAK,MAAM,IAAI,GAAG;AAChC,QAAI,CAAC,MAAO,QAAO;AACnB,QAAI,KAAK,IAAI,KAAK,MAAM,WAAW;AACjC,WAAK,MAAM,OAAO,GAAG;AACrB,aAAO;AAAA,IACT;AACA,SAAK,MAAM,OAAO,GAAG;AACrB,SAAK,MAAM,IAAI,KAAK,KAAK;AACzB,WAAO,MAAM;AAAA,EACf;AAAA,EAEA,IAAI,KAAa,OAAgB;AAC/B,QAAI,KAAK,cAAc,KAAK,KAAK,SAAS,EAAG;AAC7C,SAAK,MAAM,OAAO,GAAG;AACrB,SAAK,MAAM,IAAI,KAAK,EAAE,OAAO,WAAW,KAAK,IAAI,IAAI,KAAK,MAAM,CAAC;AACjE,WAAO,KAAK,MAAM,OAAO,KAAK,YAAY;AACxC,YAAM,SAAS,KAAK,MAAM,KAAK,EAAE,KAAK;AACtC,UAAI,OAAO,KAAM;AACjB,WAAK,MAAM,OAAO,OAAO,KAAK;AAAA,IAChC;AAAA,EACF;AAAA,EAEA,IAAI,OAAe;AACjB,WAAO,KAAK,MAAM;AAAA,EACpB;AAAA,EAEA,QAAc;AACZ,SAAK,MAAM,MAAM;AAAA,EACnB;AACF;;;AChCO,IAAM,UAAN,cAAsB,MAAM;AAAA,EACjC,YACW,MACT,SACS,UAAwB,CAAC,GAClC;AACA,UAAM,OAAO;AAJJ;AAEA;AAGT,SAAK,OAAO;AAAA,EACd;AAAA,EANW;AAAA,EAEA;AAKb;AAEA,IAAM,aAAa;AAEZ,SAAS,SAAS,KAAa,MAAuB;AAC3D,SAAO,IAAI,QAAQ,aAAa,+BAA+B,IAAI,KAAK;AAAA,IACtE;AAAA,IACA,QAAQ;AAAA,IACR,MAAM;AAAA,EACR,CAAC;AACH;AAEO,SAAS,aAAa,SAAiB,MAAwB;AACpE,SAAO,IAAI,QAAQ,iBAAiB,SAAS,OAAO,EAAE,KAAK,IAAI,CAAC,CAAC;AACnE;AAEO,SAAS,YAAY,KAAa,cAA+B;AACtE,SAAO,IAAI;AAAA,IACT;AAAA,IACA;AAAA,IACA;AAAA,MACE;AAAA,MACA;AAAA,MACA,MACE,cAAc,KAAK,KAAK,eAAe,GAAI,CAAC;AAAA,IAEhD;AAAA,EACF;AACF;AASO,SAAS,aAAa,KAAa,MAAuB;AAC/D,SAAO,IAAI;AAAA,IACT;AAAA,IACA,0EAA0E,IAAI;AAAA,IAC9E,EAAE,KAAK,MAAM,qDAAqD,UAAU,GAAG;AAAA,EACjF;AACF;AAEO,SAAS,cAAc,KAAa,QAAyB;AAClE,SAAO,IAAI,QAAQ,iBAAiB,4BAA4B,MAAM,KAAK;AAAA,IACzE;AAAA,IACA;AAAA,IACA,GAAI,UAAU,MACV,EAAE,MAAM,6DAA6D,IACrE,CAAC;AAAA,EACP,CAAC;AACH;;;ACpEA,IAAM,0BAA0B;AAEzB,IAAM,cAAN,MAAkB;AAAA,EACN;AAAA,EACA;AAAA,EACT;AAAA;AAAA,EAEA,OAAyB,QAAQ,QAAQ;AAAA,EACzC,YAAY;AAAA,EAEpB,YAAY,SAA6B;AACvC,SAAK,iBAAiB,KAAK,IAAI,GAAG,QAAQ,aAAa;AACvD,SAAK,gBAAgB,KAAK;AAAA,MACxB,KAAK;AAAA,MACL,QAAQ,iBAAiB;AAAA,IAC3B;AACA,SAAK,aAAa,KAAK;AAAA,EACzB;AAAA,EAEA,IAAI,oBAA4B;AAC9B,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,SAAY,MAAoC;AAC9C,UAAM,MAAM,KAAK,KAAK,KAAK,YAAY,KAAK,CAAC;AAE7C,SAAK,OAAO,IAAI;AAAA,MACd,MAAM;AAAA,MACN,MAAM;AAAA,IACR;AACA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAM,gBAA+B;AACnC,UAAM,KAAK,YAAY;AACvB,SAAK,YAAY,KAAK,IAAI;AAAA,EAC5B;AAAA;AAAA,EAGA,WAAiB;AACf,UAAM,OAAO,KAAK,eAAe,IAAI,MAAM,KAAK,aAAa;AAC7D,SAAK,aAAa,KAAK,IAAI,KAAK,eAAe,IAAI;AAAA,EACrD;AAAA;AAAA,EAGA,QAAc;AACZ,SAAK,aAAa,KAAK,IAAI,KAAK,gBAAgB,KAAK,MAAM,KAAK,aAAa,IAAI,CAAC;AAAA,EACpF;AAAA,EAEA,MAAc,cAA6B;AACzC,QAAI,KAAK,eAAe,KAAK,KAAK,cAAc,EAAG;AACnD,UAAM,UAAU,KAAK,IAAI,IAAI,KAAK;AAIlC,UAAM,YAAY,KAAK,IAAI,KAAK,YAAY,KAAK,aAAa,OAAO;AACrE,QAAI,YAAY,EAAG,OAAM,MAAM,SAAS;AAAA,EAC1C;AACF;AAEO,SAAS,MAAM,IAA2B;AAC/C,SAAO,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,EAAE,CAAC;AACzD;;;AC7EA,IAAM,kBAAkB;AACxB,IAAM,iBAAiB;AACvB,IAAM,iBAAiB;AAGhB,SAAS,aAAa,SAAiB,SAAuB,KAAK,QAAgB;AACxF,QAAM,SAAS,KAAK,IAAI,gBAAgB,kBAAkB,kBAAkB,OAAO;AACnF,SAAO,KAAK,MAAM,UAAU,MAAM,OAAO,IAAI,IAAI;AACnD;AAiBA,eAAsB,UAAU,KAAa,MAAiC;AAC5E,QAAM,EAAE,QAAQ,SAAS,OAAO,IAAI;AACpC,QAAM,UAAU,KAAK,aAAa;AAElC,SAAO,QAAQ,SAAS,YAAY;AAClC,QAAI;AAKJ,QAAI,cAA6B;AAEjC,aAAS,UAAU,GAAG,WAAW,OAAO,YAAY,WAAW,GAAG;AAChE,UAAI,UAAU,GAAG;AACf,cAAM,QAAQ,eAAe,aAAa,UAAU,CAAC;AACrD,sBAAc;AACd,eAAO,KAAK,SAAS,OAAO,IAAI,OAAO,UAAU,OAAO,KAAK,UAAU,GAAG,EAAE;AAC5E,cAAM,MAAM,KAAK,IAAI,OAAO,cAAc,CAAC;AAAA,MAC7C;AAEA,UAAI;AACJ,UAAI;AACJ,UAAI,eAA8B;AAClC,UAAI;AACF,cAAM,QAAQ,cAAc;AAC5B,cAAM,WAAW,MAAM,QAAQ,KAAK;AAAA,UAClC,SAAS;AAAA,YACP,cAAc,OAAO;AAAA,YACrB,QAAQ;AAAA,UACV;AAAA,UACA,UAAU;AAAA,UACV,QAAQ,YAAY,QAAQ,OAAO,SAAS;AAAA,QAC9C,CAAC;AACD,iBAAS,SAAS;AAClB,uBAAe,gBAAgB,SAAS,QAAQ,IAAI,aAAa,CAAC;AAClE,eAAO,MAAM,SAAS,KAAK;AAAA,MAC7B,SAAS,OAAO;AACd,oBAAY,iBAAiB,OAAO,GAAG;AACvC,eAAO,MAAM,GAAG,UAAU,IAAI,QAAQ,GAAG,KAAK,UAAU,OAAO,EAAE;AACjE;AAAA,MACF;AAEA,UAAI,WAAW,OAAO,WAAW,OAAO,WAAW,KAAK;AACtD,gBAAQ,SAAS;AAEjB,sBAAc;AACd,oBAAY,YAAY,KAAK,gBAAgB,aAAa,OAAO,CAAC;AAClE,eAAO;AAAA,UACL,cAAc,GAAG,SAAS,MAAM,kBAAkB,QAAQ,iBAAiB;AAAA,QAC7E;AACA;AAAA,MACF;AACA,UAAI,UAAU,KAAK;AACjB,oBAAY,cAAc,KAAK,MAAM;AACrC;AAAA,MACF;AAIA,UAAI,WAAW,IAAK,OAAM,SAAS,KAAK,cAAc;AACtD,UAAI,UAAU,IAAK,OAAM,cAAc,KAAK,MAAM;AAKlD,YAAM,UAAU,KAAK,KAAK;AAC1B,UAAI,YAAY,IAAI;AAClB,gBAAQ,SAAS;AACjB,oBAAY,YAAY,KAAK,aAAa,OAAO,CAAC;AAClD,eAAO,KAAK,iBAAiB,GAAG,yBAAyB;AACzD;AAAA,MACF;AAMA,UAAI,CAAC,QAAQ,WAAW,GAAG,KAAK,CAAC,QAAQ,WAAW,GAAG,GAAG;AACxD,gBAAQ,SAAS;AAIjB,oBAAY,aAAa,KAAK,sBAAsB;AACpD,sBAAc;AACd,eAAO,KAAK,oBAAoB,GAAG,YAAY;AAC/C;AAAA,MACF;AAEA,cAAQ,MAAM;AACd,aAAO;AAAA,IACT;AAEA,UAAM,aAAa,IAAI,QAAQ,iBAAiB,mBAAmB,GAAG,KAAK,EAAE,IAAI,CAAC;AAAA,EACpF,CAAC;AACH;AAGA,SAAS,gBAAgB,KAAmC;AAC1D,MAAI,CAAC,IAAK,QAAO;AACjB,QAAM,UAAU,OAAO,IAAI,KAAK,CAAC;AACjC,MAAI,OAAO,SAAS,OAAO,KAAK,WAAW,EAAG,QAAO,KAAK,MAAM,UAAU,GAAI;AAC9E,QAAM,OAAO,KAAK,MAAM,GAAG;AAC3B,MAAI,OAAO,MAAM,IAAI,EAAG,QAAO;AAC/B,SAAO,KAAK,IAAI,GAAG,OAAO,KAAK,IAAI,CAAC;AACtC;AAEA,SAAS,iBAAiB,OAAgB,KAAsB;AAC9D,MAAI,iBAAiB,QAAS,QAAO;AACrC,QAAM,OAAO,iBAAiB,QAAQ,MAAM,OAAO;AACnD,MAAI,SAAS,kBAAkB,SAAS,cAAc;AACpD,WAAO,IAAI,QAAQ,WAAW,sCAAsC;AAAA,MAClE;AAAA,MACA,MAAM;AAAA,IACR,CAAC;AAAA,EACH;AACA,QAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AACrE,SAAO,IAAI,QAAQ,iBAAiB,+BAA+B,OAAO,IAAI,EAAE,IAAI,CAAC;AACvF;;;ACpJO,IAAM,WAAW;AACjB,IAAM,YAAY;AAQlB,IAAM,cAAc;AAAA,EACzB,OAAO;AAAA,EACP,MAAM;AAAA,EACN,MAAM;AACR;AAEO,IAAM,eAAe;AAAA,EAC1B,OAAO;AAAA,EACP,MAAM;AAAA,EACN,MAAM;AACR;AAGO,IAAM,cAAc;AAAA,EACzB,MAAM;AAAA,EACN,OAAO;AAAA,EACP,MAAM;AACR;AAGO,IAAM,cAAc;AAAA,EACzB,OAAO;AAAA,EACP,MAAM;AAAA,EACN,MAAM;AACR;AAGO,IAAM,UAAU;AAAA,EACrB,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,SAAS;AACX;AAMO,IAAM,YAAY;AAAA,EACvB,SAAS,EAAE,eAAe,WAAW,eAAe,UAAU;AAAA,EAC9D,aAAa,EAAE,eAAe,wBAAwB,eAAe,mBAAmB;AAAA,EACxF,WAAW,EAAE,eAAe,sBAAsB,eAAe,mBAAmB;AAAA,EACpF,eAAe,EAAE,eAAe,kBAAkB,eAAe,wBAAwB;AAAA,EACzF,aAAa,EAAE,eAAe,gBAAgB,eAAe,wBAAwB;AACvF;AAGO,IAAM,QAAQ;AAAA,EACnB,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,MAAM;AAAA,EACN,OAAO;AAAA,EACP,cAAc;AAAA;AAAA,EAEd,eAAe,EAAE,KAAK,WAAW,UAAU,YAAY,SAAS,WAAW,UAAU,WAAW;AAClG;;;ACnDA,SAAS,MAAM,MAAc,QAAwB;AACnD,QAAM,QAAQ,IAAI,gBAAgB;AAClC,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,MAAM,GAAG;AACjD,QAAI,UAAU,UAAa,UAAU,GAAI,OAAM,IAAI,KAAK,OAAO,KAAK,CAAC;AAAA,EACvE;AACA,QAAM,SAAS,MAAM,SAAS;AAC9B,SAAO,SAAS,GAAG,QAAQ,GAAG,IAAI,IAAI,MAAM,KAAK,GAAG,QAAQ,GAAG,IAAI;AACrE;AAMA,SAAS,UAAU,MAAsB;AACvC,QAAM,UAAU,KAAK,KAAK;AAC1B,MAAI,YAAY,IAAI;AAClB,UAAM,aAAa,sBAAsB,sCAAsC;AAAA,EACjF;AACA,MAAI,CAAC,wBAAwB,KAAK,OAAO,GAAG;AAC1C,UAAM;AAAA,MACJ,IAAI,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;AAEO,SAAS,UAAU,OAAe,OAAe,QAAwB;AAC9E,QAAM,UAAU,MAAM,KAAK;AAC3B,MAAI,YAAY,IAAI;AAClB,UAAM,aAAa,8BAA8B,8BAA8B;AAAA,EACjF;AACA,SAAO,MAAM,6BAA6B,mBAAmB,OAAO,CAAC,QAAQ,EAAE,OAAO,OAAO,CAAC;AAChG;AAEO,SAAS,UAAU,SAMf;AACT,SAAO,MAAM,0BAA0B;AAAA,IACrC,QAAQ,QAAQ,QAAQ,IAAI;AAAA,IAC5B,WAAW,YAAY,QAAQ,IAAI;AAAA,IACnC,QAAQ,QAAQ;AAAA,IAChB,OAAO,QAAQ;AAAA,IACf,QAAQ,QAAQ;AAAA,EAClB,CAAC;AACH;AAEO,SAAS,UAAU,MAAY,MAAsB;AAC1D,SAAO,MAAM,IAAI,YAAY,IAAI,CAAC,eAAe,UAAU,IAAI,CAAC,QAAQ,UAAU,OAAO;AAC3F;AAEO,SAAS,SAAS,MAAY,MAAc,QAAmC;AACpF,QAAM,YAAY,WAAW,WAAW,UAAU,cAAc,UAAU;AAC1E,SAAO;AAAA,IACL,uBAAuB,MAAM,IAAI,YAAY,IAAI,CAAC,IAAI,UAAU,IAAI,CAAC;AAAA,IACrE;AAAA,EACF;AACF;AAUO,SAAS,WAAW,SAIhB;AACT,QAAM,YAAY,QAAQ,WAAW,WAAW,UAAU,gBAAgB,UAAU;AACpF,SAAO;AAAA,IACL,uBAAuB,QAAQ,MAAM,IAAI,YAAY,QAAQ,IAAI,CAAC,IAAI,UAAU,QAAQ,IAAI,CAAC;AAAA,IAC7F;AAAA,EACF;AACF;AAGO,SAAS,SAAS,QAAgB,MAAoB;AAC3D,QAAM,UAAU,OAAO,KAAK;AAC5B,MAAI,CAAC,UAAU,KAAK,OAAO,GAAG;AAC5B,UAAM;AAAA,MACJ,IAAI,MAAM;AAAA,MACV;AAAA,IACF;AAAA,EACF;AACA,SAAO,MAAM,wCAAwC;AAAA,IACnD,IAAI;AAAA,IACJ,MAAM,SAAS,SAAS,SAAS;AAAA,EACnC,CAAC;AACH;AAGO,SAAS,aAAa,MAAY,MAAsB;AAC7D,SAAO,GAAG,SAAS,IAAI,aAAa,IAAI,CAAC,IAAI,IAAI;AACnD;AAGO,SAAS,UAAU,WAA8B;AACtD,SAAO,cAAc,QAAQ,YAAY;AAC3C;;;AChGA,IAAM,WAAW,CAAC,UAChB,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAS9D,SAAS,SAAS,KAAa,KAAa,MAAoB;AACrE,MAAI;AACJ,MAAI;AACF,WAAO,KAAK,MAAM,GAAG;AAAA,EACvB,QAAQ;AACN,UAAM,aAAa,KAAK,4BAA4B;AAAA,EACtD;AAEA,MAAI,CAAC,SAAS,IAAI,EAAG,OAAM,aAAa,KAAK,2BAA2B;AAExE,QAAM,SAAS,KAAK,MAAM,MAAM;AAChC,MAAI,MAAM,QAAQ,MAAM,KAAK,OAAO,SAAS,GAAG;AAC9C,UAAM,QAAQ,SAAS,OAAO,CAAC,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC;AACjD,QAAI,MAAM,SAAS,IAAK,OAAM,SAAS,KAAK,IAAI;AAChD,UAAM;AAAA,MACJ;AAAA,MACA,qBAAqB,OAAO,MAAM,UAAU,MAAM,QAAQ,UAAU,CAAC;AAAA,IACvE;AAAA,EACF;AAEA,QAAM,OAAO,KAAK,MAAM,IAAI;AAC5B,MAAI,CAAC,SAAS,IAAI,EAAG,OAAM,aAAa,KAAK,qCAAqC;AAClF,SAAO;AACT;AAEA,IAAM,MAAM,CAAC,UAAkC;AAC7C,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,QAAM,UAAU,MAAM,KAAK;AAC3B,SAAO,YAAY,KAAK,OAAO;AACjC;AAEA,IAAM,MAAM,CAAC,UAAkC;AAC7C,MAAI,OAAO,UAAU,YAAY,OAAO,SAAS,KAAK,EAAG,QAAO;AAChE,MAAI,OAAO,UAAU,UAAU;AAC7B,UAAM,SAAS,OAAO,KAAK;AAC3B,QAAI,OAAO,SAAS,MAAM,EAAG,QAAO;AAAA,EACtC;AACA,SAAO;AACT;AAEA,IAAM,QAAQ,CAAC,UAAkC;AAC/C,QAAM,SAAS,IAAI,KAAK;AACxB,SAAO,WAAW,OAAO,OAAO,KAAK,MAAM,MAAM;AACnD;AAGA,SAAS,OAAO,OAA6B;AAC3C,QAAM,OAAO,IAAI,KAAK;AACtB,MAAI,SAAS,YAAY,MAAO,QAAO;AACvC,MAAI,SAAS,YAAY,KAAM,QAAO;AACtC,MAAI,SAAS,YAAY,KAAM,QAAO;AACtC,SAAO;AACT;AAOA,SAAS,UAAU,MAAoC;AACrD,MAAI,CAAC,SAAS,IAAI,EAAG,QAAO;AAE5B,QAAM,KAAK,MAAM,KAAK,EAAE;AACxB,QAAM,OAAO,IAAI,KAAK,IAAI;AAC1B,QAAM,QAAQ,IAAI,KAAK,KAAK;AAC5B,QAAM,OAAO,OAAO,KAAK,IAAI;AAC7B,MAAI,OAAO,QAAQ,CAAC,QAAQ,CAAC,SAAS,CAAC,KAAM,QAAO;AAEpD,QAAM,SAAS,SAAS,KAAK,kBAAkB,IAAI,KAAK,qBAAqB,CAAC;AAK9E,QAAM,cAAc,SAAS,KAAK,gBAAgB,IAAI,KAAK,mBAAmB,CAAC;AAC/E,QAAM,aAAa,SAAS,KAAK,SAAS,IAAI,KAAK,YAAY,CAAC;AAChE,QAAM,YAAY,IAAI,YAAY,KAAK,KAAK,IAAI,WAAW,KAAK,KAAK,IAAI,KAAK,SAAS;AAEvF,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,MAAM,MAAM,KAAK,YAAY;AAAA,IAC7B,aAAa,IAAI,KAAK,WAAW;AAAA,IACjC,QAAQ,IAAI,KAAK,MAAM;AAAA,IACvB,WAAW,IAAI,OAAO,KAAK;AAAA,IAC3B;AAAA,IACA,WAAW,aAAa,MAAM,IAAI;AAAA,EACpC;AACF;AAGO,SAAS,eAAe,KAAa,KAAa,MAAyB;AAChF,QAAM,OAAO,SAAS,KAAK,KAAK,IAAI;AACpC,QAAM,QAAQ,KAAK,MAAM,KAAK;AAC9B,MAAI,CAAC,MAAM,QAAQ,KAAK,EAAG,OAAM,aAAa,KAAK,qCAAqC;AAExF,QAAM,SAAyB,CAAC;AAChC,aAAW,QAAQ,OAAO;AACxB,UAAM,MAAM,UAAU,IAAI;AAC1B,QAAI,IAAK,QAAO,KAAK,GAAG;AAAA,EAC1B;AAEA,MAAI,MAAM,SAAS,KAAK,OAAO,WAAW,GAAG;AAC3C,UAAM,aAAa,KAAK,GAAG,MAAM,MAAM,8BAA8B;AAAA,EACvE;AAEA,SAAO;AAAA,IACL;AAAA,IACA,cAAc,MAAM,KAAK,MAAM,YAAY,CAAC,KAAK,OAAO;AAAA,IACxD,WAAW,MAAM;AAAA,EACnB;AACF;AAWA,SAAS,YAAY,OAA2B;AAC9C,QAAM,SAAS,SAAS,KAAK,IAAI,MAAM,YAAY;AACnD,MAAI,CAAC,MAAM,QAAQ,MAAM,EAAG,QAAO,CAAC;AAEpC,QAAM,OAAO,oBAAI,IAAY;AAC7B,QAAM,MAAiB,CAAC;AACxB,aAAW,QAAQ,QAAQ;AACzB,UAAM,OAAO,SAAS,IAAI,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,IAAI;AACvD,QAAI,CAAC,KAAM;AACX,UAAM,MAAM,KAAK,YAAY;AAC7B,QAAI,KAAK,IAAI,GAAG,EAAG;AACnB,SAAK,IAAI,GAAG;AACZ,QAAI,KAAK,EAAE,MAAM,IAAI,SAAS,IAAI,IAAI,MAAM,KAAK,EAAE,IAAI,KAAK,CAAC;AAAA,EAC/D;AACA,SAAO;AACT;AAEA,SAAS,aAAa,OAA0B;AAC9C,MAAI,CAAC,MAAM,QAAQ,KAAK,EAAG,QAAO,CAAC;AACnC,QAAM,MAAgB,CAAC;AACvB,aAAW,QAAQ,OAAO;AACxB,UAAM,OAAO,SAAS,IAAI,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,IAAI;AACvD,QAAI,KAAM,KAAI,KAAK,IAAI;AAAA,EACzB;AACA,SAAO;AACT;AAQA,SAAS,SAAS,OAAyB;AACzC,MAAI,CAAC,MAAM,QAAQ,KAAK,EAAG,QAAO,CAAC;AACnC,QAAM,MAAe,CAAC;AACtB,aAAW,QAAQ,OAAO;AACxB,QAAI,CAAC,SAAS,IAAI,EAAG;AACrB,UAAM,WAAW,IAAI,KAAK,UAAU;AACpC,QAAI,CAAC,SAAU;AACf,QAAI,KAAK;AAAA,MACP;AAAA,MACA,MAAM,MAAM,KAAK,IAAI;AAAA,MACrB,aAAa,MAAM,KAAK,WAAW;AAAA,IACrC,CAAC;AAAA,EACH;AACA,SAAO;AACT;AAEO,SAAS,YAAY,KAAa,KAAa,MAAY,MAA2B;AAC3F,QAAM,OAAO,SAAS,KAAK,KAAK,GAAG,IAAI,KAAK,IAAI,GAAG;AACnD,QAAM,OAAO,KAAK,MAAM,IAAI;AAC5B,MAAI,CAAC,SAAS,IAAI,EAAG,OAAM,aAAa,KAAK,qCAAqC;AAIlF,QAAM,KAAK,MAAM,KAAK,EAAE;AACxB,QAAM,QAAQ,IAAI,KAAK,KAAK;AAC5B,MAAI,OAAO,QAAQ,CAAC,MAAO,OAAM,aAAa,KAAK,iCAAiC;AAEpF,QAAM,SAAS,SAAS,KAAK,kBAAkB,IAAI,KAAK,qBAAqB,CAAC;AAE9E,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,MAAM,MAAM,KAAK,YAAY;AAAA,IAC7B,aAAa,IAAI,KAAK,WAAW;AAAA,IACjC,QAAQ,IAAI,KAAK,MAAM;AAAA,IACvB,WAAW,IAAI,OAAO,KAAK;AAAA,IAC3B,WAAW;AAAA,IACX,WAAW,aAAa,MAAM,IAAI;AAAA,IAClC,aAAa,IAAI,KAAK,WAAW;AAAA,IACjC,SAAS,IAAI,KAAK,OAAO;AAAA,IACzB,QAAQ,aAAa,KAAK,MAAM;AAAA,IAChC,UAAU,MAAM,KAAK,QAAQ;AAAA,IAC7B,QAAQ,IAAI,KAAK,MAAM;AAAA,IACvB,UAAU,aAAa,KAAK,QAAQ;AAAA,IACpC,YAAY,YAAY,KAAK,UAAU;AAAA,IACvC,QAAQ,SAAS,KAAK,MAAM;AAAA,IAC5B,mBAAmB;AAAA,IACnB,iBAAiB;AAAA,EACnB;AACF;AASO,SAAS,WAAW,KAAa,KAAa,MAA4B;AAC/E,QAAM,OAAO,SAAS,KAAK,KAAK,IAAI;AACpC,QAAM,OAAO,KAAK,MAAM,IAAI;AAC5B,MAAI,CAAC,SAAS,IAAI,EAAG,OAAM,aAAa,KAAK,sCAAsC;AAEnF,QAAM,MAAM,IAAI,KAAK,GAAG;AACxB,MAAI,QAAQ,KAAM,OAAM,aAAa,KAAK,4BAA4B;AAEtE,SAAO;AAAA,IACL,OAAO,IAAI,KAAK,KAAK;AAAA,IACrB;AAAA,IACA,aAAa,MAAM,KAAK,WAAW;AAAA,IACnC,eAAe,MAAM,KAAK,aAAa;AAAA,IACvC,cAAc,MAAM,KAAK,YAAY;AAAA,IACrC,eAAe,MAAM,KAAK,aAAa;AAAA,IACvC,WAAW,IAAI,KAAK,SAAS;AAAA,EAC/B;AACF;AAEA,SAAS,aAAa,MAAY,KAAa,WAAiC;AAC9E,QAAM,OAAO,KAAK,MAAM,IAAI;AAC5B,MAAI,CAAC,SAAS,IAAI,EAAG,OAAM,aAAa,KAAK,wCAAwC;AAErF,QAAM,SAAS,KAAK,UAAU,SAAS,CAAC;AAGxC,MAAI,CAAC,MAAM,QAAQ,MAAM,GAAG;AAC1B,UAAM,aAAa,KAAK,gBAAgB,UAAU,SAAS,CAAC,oBAAoB;AAAA,EAClF;AACA,SAAO;AACT;AAEO,SAAS,mBACd,KACA,KACA,MACA,WAC0B;AAC1B,QAAM,OAAO,SAAS,KAAK,KAAK,IAAI;AACpC,QAAM,SAAS,aAAa,MAAM,KAAK,SAAS;AAEhD,QAAM,UAA0B,CAAC;AACjC,aAAW,QAAQ,QAAQ;AACzB,QAAI,CAAC,SAAS,IAAI,EAAG;AACrB,UAAM,QAAQ,IAAI,KAAK,KAAK;AAC5B,UAAM,cAAc,IAAI,KAAK,eAAe;AAG5C,QAAI,CAAC,SAAS,CAAC,YAAa;AAC5B,YAAQ,KAAK;AAAA,MACX;AAAA,MACA,OAAO,IAAI,KAAK,KAAK;AAAA,MACrB;AAAA,MACA,QAAQ,IAAI,KAAK,MAAM;AAAA,MACvB,KAAK,IAAI,KAAK,GAAG;AAAA,MACjB,MAAM,IAAI,KAAK,IAAI;AAAA,IACrB,CAAC;AAAA,EACH;AAEA,MAAI,OAAO,SAAS,KAAK,QAAQ,WAAW,GAAG;AAC7C,UAAM,aAAa,KAAK,GAAG,OAAO,MAAM,iCAAiC;AAAA,EAC3E;AAEA,SAAO;AAAA,IACL;AAAA,IACA,cAAc,MAAM,KAAK,MAAM,YAAY,CAAC,KAAK,QAAQ;AAAA,IACzD,WAAW,OAAO;AAAA,EACpB;AACF;AAEO,SAAS,iBACd,KACA,KACA,MACA,WACwB;AACxB,QAAM,OAAO,SAAS,KAAK,KAAK,IAAI;AACpC,QAAM,SAAS,aAAa,MAAM,KAAK,SAAS;AAEhD,QAAM,UAAwB,CAAC;AAC/B,aAAW,QAAQ,QAAQ;AACzB,QAAI,CAAC,SAAS,IAAI,EAAG;AACrB,UAAM,QAAQ,IAAI,KAAK,KAAK;AAC5B,QAAI,CAAC,MAAO;AACZ,YAAQ,KAAK,EAAE,OAAO,OAAO,IAAI,KAAK,KAAK,GAAG,MAAM,IAAI,KAAK,IAAI,EAAE,CAAC;AAAA,EACtE;AAEA,MAAI,OAAO,SAAS,KAAK,QAAQ,WAAW,GAAG;AAC7C,UAAM,aAAa,KAAK,GAAG,OAAO,MAAM,iCAAiC;AAAA,EAC3E;AAEA,SAAO;AAAA,IACL;AAAA,IACA,cAAc,MAAM,KAAK,MAAM,YAAY,CAAC,KAAK,QAAQ;AAAA,IACzD,WAAW,OAAO;AAAA,EACpB;AACF;AAUA,SAAS,WAAW,MAAoC;AACtD,MAAI,CAAC,KAAM,QAAO;AAClB,MAAI;AACF,UAAM,SAAS,IAAI,IAAI,IAAI,EAAE,aAAa,IAAI,GAAG;AACjD,QAAI,UAAU,gBAAgB,KAAK,MAAM,EAAG,QAAO;AAAA,EACrD,QAAQ;AAAA,EAER;AACA,SAAO;AACT;AAGO,SAAS,YAAY,KAAa,KAAa,MAA4B;AAChF,QAAM,OAAO,SAAS,KAAK,KAAK,IAAI;AACpC,QAAM,OAAO,KAAK,MAAM,IAAI;AAC5B,MAAI,CAAC,SAAS,IAAI,EAAG,OAAM,aAAa,KAAK,uCAAuC;AAEpF,QAAM,SAAuB,CAAC;AAC9B,aAAW,CAAC,OAAO,KAAK,KAAK,OAAO,QAAQ,IAAI,GAAG;AACjD,QAAI,CAAC,MAAM,QAAQ,KAAK,EAAG;AAC3B,eAAW,QAAQ,OAAO;AACxB,UAAI,CAAC,SAAS,IAAI,EAAG;AACrB,YAAM,WAAW,IAAI,KAAK,YAAY;AACtC,UAAI,CAAC,SAAU;AACf,aAAO,KAAK,EAAE,UAAU,MAAM,OAAO,KAAK,WAAW,IAAI,KAAK,IAAI,CAAC,EAAE,CAAC;AAAA,IACxE;AAAA,EACF;AACA,SAAO;AACT;;;ACtUA,SAAS,eAAe,QAAwB;AAC9C,QAAM,YAAY,iDAAiD,KAAK,OAAO,SAAS,IACpF,GAAG,OAAO,SAAS,IAAI,kBAAkB,KACzC,OAAO;AACX,SAAO;AAAA,IACL,GAAG;AAAA,IACH;AAAA,IACA,eAAe,KAAK,IAAI,yBAAyB,OAAO,aAAa;AAAA,EACvE;AACF;AAEO,IAAM,WAAN,MAAe;AAAA,EACH;AAAA,EACA;AAAA,EACA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA;AAAA,EACA;AAAA,EACA;AAAA,EAEjB,YAAY,UAA2B,CAAC,GAAG;AACzC,SAAK,SAAS,eAAe,QAAQ,UAAU,WAAW,CAAC;AAC3D,SAAK,SAAS,QAAQ,UAAU,aAAa,KAAK,OAAO,QAAQ;AACjE,SAAK,UAAU,IAAI,YAAY,EAAE,eAAe,KAAK,OAAO,cAAc,CAAC;AAC3E,SAAK,iBAAiB,IAAI;AAAA,MACxB,KAAK,OAAO;AAAA,MACZ,KAAK,OAAO;AAAA,IACd;AACA,SAAK,cAAc,IAAI;AAAA,MACrB,KAAK,OAAO;AAAA,MACZ,KAAK,OAAO;AAAA,IACd;AACA,SAAK,YAAY,QAAQ;AAAA,EAC3B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAM,OAAO,OAAe,OAAe,QAA6C;AACtF,UAAM,MAAM,UAAU,OAAO,OAAO,MAAM;AAC1C,WAAO,KAAK;AAAA,MAAY;AAAA,MAAK,KAAK;AAAA,MAAgB,CAAC,SACjD,eAAe,MAAM,KAAK,cAAc,KAAK,GAAG;AAAA,IAClD;AAAA,EACF;AAAA,EAEA,MAAM,OAAO,SAMmB;AAC9B,UAAM,MAAM,UAAU,OAAO;AAC7B,WAAO,KAAK;AAAA,MAAY;AAAA,MAAK,KAAK;AAAA,MAAgB,CAAC,SACjD,eAAe,MAAM,KAAK,QAAQ,QAAQ,IAAI,UAAU;AAAA,IAC1D;AAAA,EACF;AAAA,EAEA,MAAM,UAAU,MAAY,MAA6C;AACvE,UAAM,MAAM,UAAU,MAAM,IAAI;AAGhC,UAAM,QAAQ,KAAK,KAAK;AACxB,WAAO,KAAK;AAAA,MAAY;AAAA,MAAK,KAAK;AAAA,MAAgB,CAAC,SACjD,YAAY,MAAM,KAAK,MAAM,KAAK;AAAA,IACpC;AAAA,EACF;AAAA,EAEA,MAAM,SACJ,MACA,MACA,QACgC;AAChC,UAAM,MAAM,SAAS,MAAM,MAAM,MAAM;AACvC,WAAO,KAAK;AAAA,MAAY;AAAA,MAAK,KAAK;AAAA,MAAa,CAAC,SAC9C,WAAW,MAAM,KAAK,OAAO,MAAM,aAAa,IAAI,KAAK,IAAI,GAAG;AAAA,IAClE;AAAA,EACF;AAAA,EAEA,MAAM,iBAAiB,SAMwB;AAC7C,UAAM,MAAM,WAAW,EAAE,GAAG,SAAS,QAAQ,SAAS,CAAC;AACvD,WAAO,KAAK;AAAA,MACV;AAAA,MACA,KAAK;AAAA,MACL,CAAC,SACC;AAAA,QACE;AAAA,QACA;AAAA,QACA,cAAc,QAAQ,IAAI,KAAK,QAAQ,IAAI;AAAA,QAC3C,QAAQ;AAAA,MACV;AAAA,MACF,QAAQ;AAAA,IACV;AAAA,EACF;AAAA,EAEA,MAAM,eAAe,SAMwB;AAC3C,UAAM,MAAM,WAAW,EAAE,GAAG,SAAS,QAAQ,OAAO,CAAC;AACrD,WAAO,KAAK;AAAA,MACV;AAAA,MACA,KAAK;AAAA,MACL,CAAC,SACC;AAAA,QACE;AAAA,QACA;AAAA,QACA,cAAc,QAAQ,IAAI,KAAK,QAAQ,IAAI;AAAA,QAC3C,QAAQ;AAAA,MACV;AAAA,MACF,QAAQ;AAAA,IACV;AAAA,EACF;AAAA,EAEA,MAAM,eAAe,QAAgB,MAA4C;AAC/E,UAAM,MAAM,SAAS,QAAQ,IAAI;AACjC,WAAO,KAAK;AAAA,MAAY;AAAA,MAAK,KAAK;AAAA,MAAa,CAAC,SAC9C,YAAY,MAAM,KAAK,wBAAwB,MAAM,EAAE;AAAA,IACzD;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAc,YACZ,KACA,OACA,OAMA,UAAU,IACW;AACrB,UAAM,MAAM,UAAU,GAAG,GAAG,IAAI,OAAO,KAAK;AAC5C,UAAM,MAAM,MAAM,IAAI,GAAG;AACzB,QAAI,QAAQ,QAAW;AACrB,WAAK,OAAO,MAAM,aAAa,GAAG,EAAE;AACpC,aAAO,EAAE,MAAM,KAAU,QAAQ,KAAK;AAAA,IACxC;AAEA,UAAM,OAAO,MAAM,UAAU,KAAK;AAAA,MAChC,QAAQ,KAAK;AAAA,MACb,SAAS,KAAK;AAAA,MACd,QAAQ,KAAK;AAAA,MACb,GAAI,KAAK,YAAY,EAAE,WAAW,KAAK,UAAU,IAAI,CAAC;AAAA,IACxD,CAAC;AAED,UAAM,OAAO,MAAM,IAAI;AACvB,UAAM,IAAI,KAAK,IAAI;AACnB,WAAO,EAAE,MAAM,QAAQ,MAAM;AAAA,EAC/B;AACF;","names":[]}
|
package/package.json
CHANGED
package/server.json
CHANGED
|
@@ -19,12 +19,12 @@
|
|
|
19
19
|
"url": "https://github.com/smeet666/mcp-metacritic",
|
|
20
20
|
"source": "github"
|
|
21
21
|
},
|
|
22
|
-
"version": "1.0
|
|
22
|
+
"version": "1.1.0",
|
|
23
23
|
"packages": [
|
|
24
24
|
{
|
|
25
25
|
"registryType": "npm",
|
|
26
26
|
"identifier": "mcp-metacritic",
|
|
27
|
-
"version": "1.0
|
|
27
|
+
"version": "1.1.0",
|
|
28
28
|
"transport": {
|
|
29
29
|
"type": "stdio"
|
|
30
30
|
},
|
|
@@ -45,6 +45,15 @@
|
|
|
45
45
|
"isRequired": false
|
|
46
46
|
}
|
|
47
47
|
]
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"registryType": "mcpb",
|
|
51
|
+
"identifier": "https://github.com/smeet666/mcp-metacritic/releases/download/v1.1.0/mcp-metacritic-1.1.0.mcpb",
|
|
52
|
+
"version": "1.1.0",
|
|
53
|
+
"transport": {
|
|
54
|
+
"type": "stdio"
|
|
55
|
+
},
|
|
56
|
+
"registryBaseUrl": "https://github.com"
|
|
48
57
|
}
|
|
49
58
|
]
|
|
50
59
|
}
|