mcp-libraryofcongress 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +46 -0
- package/LICENSE +21 -0
- package/README.md +555 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1695 -0
- package/dist/index.js.map +1 -0
- package/dist/loc/client.d.ts +312 -0
- package/dist/loc/client.js +987 -0
- package/dist/loc/client.js.map +1 -0
- package/package.json +72 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/errors.ts","../../src/version.ts","../../src/config.ts","../../src/loc/cache.ts","../../src/loc/http.ts","../../src/loc/paths.ts","../../src/loc/urls.ts","../../src/loc/parse.ts","../../src/loc/rateLimiter.ts","../../src/loc/client.ts"],"sourcesContent":["/**\n * One error type, carrying a code the caller can branch on.\n *\n * The distinction that matters most is between \"the Library holds nothing for\n * this\" and \"the question could not be asked\". Collapsing the two lets a model\n * report an absence it never established, which is a false statement about the\n * world rather than a missing feature.\n */\n\nexport type ErrorCode =\n /** The Library answered, and holds no such item, page or collection. */\n | \"not_found\"\n /** The arguments cannot produce a request. */\n | \"invalid_input\"\n /** The Library asked this client to slow down or refused it for now. */\n | \"rate_limited\"\n /** A response arrived in a shape this server cannot read. */\n | \"parse_failure\"\n /** The request could not be completed. */\n | \"network_error\"\n /** The request was abandoned before an answer arrived. */\n | \"timeout\";\n\nexport interface ErrorDetails {\n /** What the caller can do about it, when there is something. */\n hint?: string;\n /** The address that produced the failure, for a bug report. */\n url?: string;\n status?: number;\n}\n\nexport class LocError extends Error {\n readonly code: ErrorCode;\n readonly details: ErrorDetails;\n\n constructor(code: ErrorCode, message: string, details: ErrorDetails = {}) {\n super(message);\n this.name = \"LocError\";\n this.code = code;\n this.details = details;\n }\n}\n\nexport const notFound = (message: string, details?: ErrorDetails) =>\n new LocError(\"not_found\", message, details);\n\nexport const invalidInput = (message: string, hint?: string) =>\n new LocError(\"invalid_input\", message, hint ? { hint } : {});\n\nexport const rateLimited = (message: string, details?: ErrorDetails) =>\n new LocError(\"rate_limited\", message, {\n hint: \"Wait a moment and ask again. This says nothing about whether the Library holds what you asked for.\",\n ...details,\n });\n\nexport const parseFailure = (message: string, details?: ErrorDetails) =>\n new LocError(\"parse_failure\", message, {\n hint: \"The Library may have changed how it answers. Please report this at https://github.com/smeet666/mcp-libraryofcongress/issues with the arguments you used.\",\n ...details,\n });\n\nexport const networkError = (message: string, details?: ErrorDetails) =>\n new LocError(\"network_error\", message, details);\n\nexport const timeout = (message: string, details?: ErrorDetails) =>\n new LocError(\"timeout\", message, details);\n","/**\n * Kept in step with the version in package.json. The User-Agent embeds it, so\n * the Library of Congress can tell which build is calling from its own logs.\n */\nexport const PKG_VERSION = \"1.0.0\";\nexport const REPO_URL = \"https://github.com/smeet666/mcp-libraryofcongress\";\n","/**\n * Settings, read from the environment.\n *\n * A value that cannot be read warns and falls back rather than stopping the\n * server: a typo in one variable should not take away every tool. Warnings go\n * to stderr, because stdout carries the protocol and anything written there\n * corrupts the session.\n */\n\nimport { PKG_VERSION, REPO_URL } from \"./version.js\";\n\nexport const LOG_LEVELS = [\"silent\", \"error\", \"info\", \"debug\"] as const;\nexport type LogLevel = (typeof LOG_LEVELS)[number];\n\n/**\n * The Library publishes two ceilings: twenty requests a minute for the JSON\n * responses, and ten a minute across the site as a whole. The lower of the two\n * governs here, which is one request every six seconds.\n *\n * Configuration can slow the server down. It cannot take the spacing below this\n * floor, whether the setting arrives through the environment or through a\n * configuration object handed to the published client.\n */\nexport const MIN_ALLOWED_INTERVAL_MS = 3000;\n/** Beyond this a request would look hung rather than paced. */\nexport const MAX_ALLOWED_INTERVAL_MS = 60_000;\n/** The spacing the published ceiling asks for. */\nexport const DEFAULT_INTERVAL_MS = 6000;\n\n/**\n * The deadline the full-text newspaper route is given.\n *\n * That route searches the machine-read text of millions of pages, and it answers\n * far more slowly than the catalogue: measured on the live site, a query holding\n * two quoted phrases takes between thirty-two and thirty-seven seconds to come\n * back, and the site's own edge takes up to fifty-six seconds before it gives up\n * and returns a 5xx. A minute and a half clears both, so a slow answer arrives\n * instead of being reported as silence, while a request that truly hangs still\n * releases the single request slot rather than holding it indefinitely.\n */\nexport const DEFAULT_NEWSPAPER_TIMEOUT_MS = 90_000;\n\nexport interface Config {\n userAgent: string;\n minIntervalMs: number;\n timeoutMs: number;\n /** Deadline for the full-text newspaper route, which is the slow one. */\n newspaperTimeoutMs: number;\n maxRetries: number;\n cacheTtlMs: number;\n cacheMaxEntries: number;\n logLevel: LogLevel;\n}\n\nexport const DEFAULT_USER_AGENT = `mcp-libraryofcongress/${PKG_VERSION} (+${REPO_URL})`;\n\nexport interface Logger {\n debug(message: string): void;\n info(message: string): void;\n warn(message: string): void;\n error(message: string): void;\n}\n\nexport function createLogger(level: LogLevel): Logger {\n const rank = LOG_LEVELS.indexOf(level);\n const write = (at: LogLevel, message: string) => {\n if (rank === 0 || LOG_LEVELS.indexOf(at) > rank) return;\n process.stderr.write(`[mcp-libraryofcongress] ${at}: ${message}\\n`);\n };\n return {\n debug: (m) => write(\"debug\", m),\n info: (m) => write(\"info\", m),\n // A warning goes out at the error level so it survives the default\n // setting: a caller has to know that rows were dropped.\n warn: (m) => write(\"error\", m),\n error: (m) => write(\"error\", m),\n };\n}\n\nfunction readInteger(\n env: NodeJS.ProcessEnv,\n name: string,\n fallback: number,\n min: number,\n max: number,\n): number {\n const raw = env[name];\n if (raw === undefined || raw.trim() === \"\") return fallback;\n\n const value = Number(raw);\n if (!Number.isFinite(value) || !Number.isInteger(value)) {\n process.stderr.write(\n `[mcp-libraryofcongress] error: ${name}=\"${raw}\" is not a whole number; using ${fallback}.\\n`,\n );\n return fallback;\n }\n if (value < min || value > max) {\n // Clamping silently would let a caller believe a setting took effect when\n // the opposite is true, so the refusal is stated and the default stands.\n process.stderr.write(\n `[mcp-libraryofcongress] error: ${name}=${value} is outside ${min}..${max}; using ${fallback}.\\n`,\n );\n return fallback;\n }\n return value;\n}\n\nexport function loadConfig(env: NodeJS.ProcessEnv = process.env): Config {\n const level = env.LOC_LOG_LEVEL as LogLevel | undefined;\n const logLevel = level && LOG_LEVELS.includes(level) ? level : \"error\";\n if (level && !LOG_LEVELS.includes(level)) {\n process.stderr.write(\n `[mcp-libraryofcongress] error: LOC_LOG_LEVEL=\"${level}\" is not one of ${LOG_LEVELS.join(\", \")}; using error.\\n`,\n );\n }\n\n const custom = env.LOC_USER_AGENT?.trim();\n\n return {\n // A caller who wants to be recognised may say who they are, and the\n // contact address stays attached: the Library has to be able to reach a\n // human about traffic it did not expect.\n userAgent: custom ? `${custom} ${DEFAULT_USER_AGENT}` : DEFAULT_USER_AGENT,\n minIntervalMs: readInteger(\n env,\n \"LOC_MIN_INTERVAL_MS\",\n DEFAULT_INTERVAL_MS,\n MIN_ALLOWED_INTERVAL_MS,\n MAX_ALLOWED_INTERVAL_MS,\n ),\n // A catalogue page carries a row for every match on it.\n timeoutMs: readInteger(env, \"LOC_TIMEOUT_MS\", 30_000, 1000, 120_000),\n newspaperTimeoutMs: readInteger(\n env,\n \"LOC_NEWSPAPER_TIMEOUT_MS\",\n DEFAULT_NEWSPAPER_TIMEOUT_MS,\n 1000,\n 300_000,\n ),\n maxRetries: readInteger(env, \"LOC_MAX_RETRIES\", 3, 0, 8),\n cacheTtlMs: readInteger(env, \"LOC_CACHE_TTL_MS\", 900_000, 0, 86_400_000),\n cacheMaxEntries: readInteger(env, \"LOC_CACHE_MAX_ENTRIES\", 200, 1, 5000),\n logLevel,\n };\n}\n","/**\n * A small in-memory store, keyed by what produced the value.\n *\n * It exists to keep a conversation that asks the same thing twice from asking\n * the site twice, which matters more here than usual: the spacing between\n * requests is six seconds, so a repeated question costs a caller real waiting.\n * Entries expire, the store is bounded, and it holds only what was successfully\n * read: storing a response nobody could parse would serve that failure back for\n * the rest of its lifetime.\n */\n\ninterface Entry<T> {\n value: T;\n expiresAt: number;\n}\n\nexport class Cache<T> {\n private readonly entries = new Map<string, Entry<T>>();\n\n constructor(\n private readonly ttlMs: number,\n private readonly maxEntries: number,\n ) {}\n\n get(key: string): T | undefined {\n const entry = this.entries.get(key);\n if (!entry) return undefined;\n if (entry.expiresAt <= Date.now()) {\n this.entries.delete(key);\n return undefined;\n }\n // Re-inserting marks it as the most recently used, which is what the\n // eviction below reads.\n this.entries.delete(key);\n this.entries.set(key, entry);\n return entry.value;\n }\n\n set(key: string, value: T): void {\n // A lifetime of zero turns the store off rather than expiring at once:\n // nothing is written, so nothing has to be checked on the way out.\n if (this.ttlMs <= 0) return;\n this.entries.delete(key);\n this.entries.set(key, { value, expiresAt: Date.now() + this.ttlMs });\n while (this.entries.size > this.maxEntries) {\n const oldest = this.entries.keys().next();\n if (oldest.done) break;\n this.entries.delete(oldest.value);\n }\n }\n\n get size(): number {\n return this.entries.size;\n }\n}\n","/**\n * One GET, with a deadline and bounded retries.\n *\n * Two things separate a retry worth making from one that only adds load. A\n * refusal that carries a time to come back is obeyed rather than guessed at,\n * and an answer the site meant is never retried: asking again for something\n * that is not there wastes a request and delays the honest answer.\n */\n\nimport {\n invalidInput,\n networkError,\n notFound,\n parseFailure,\n rateLimited,\n timeout as timeoutError,\n} from \"../errors.js\";\nimport type { Logger } from \"../config.js\";\nimport type { RateLimiter } from \"./rateLimiter.js\";\n\nexport interface FetchOptions {\n url: string;\n userAgent: string;\n timeoutMs: number;\n maxRetries: number;\n limiter: RateLimiter;\n logger: Logger;\n fetchImpl?: typeof fetch;\n}\n\n/** Statuses worth another attempt: the site is busy, not answering \"no\". */\nconst RETRYABLE = new Set([408, 425, 429, 500, 502, 503, 504]);\n/** Statuses that mean the site is asking for room. */\nconst PUSH_BACK = new Set([429, 503]);\n\n/**\n * The longest wait worth taking rather than reporting.\n *\n * A refusal may name any delay, and an hour is a legal answer. Sleeping through\n * it holds the one request slot this server has, so every other tool waits\n * behind a call whose caller has long since given up. Past this point the wait\n * is the answer, and the caller decides what to do with it.\n */\nconst LONGEST_WAIT_MS = 30_000;\n\n/**\n * How many times a request that never answered is worth repeating.\n *\n * A route that did not respond within its budget is busy. Repeating the same\n * query adds load to what is already struggling, and each attempt holds the\n * slot for the full deadline again.\n */\nconst RETRIES_AFTER_SILENCE = 1;\n\n/**\n * Read a Retry-After header, which is either a number of seconds or a date.\n * Returns null when it says neither, so the caller falls back to its own wait.\n */\nexport function parseRetryAfter(value: string | null, now = Date.now()): number | null {\n if (!value) return null;\n const trimmed = value.trim();\n\n const seconds = Number(trimmed);\n if (Number.isFinite(seconds) && seconds >= 0) return Math.round(seconds * 1000);\n\n const at = Date.parse(trimmed);\n if (Number.isNaN(at)) return null;\n return Math.max(0, at - now);\n}\n\n/** Growing wait with jitter, so several clients do not return in step. */\nfunction backoffMs(attempt: number): number {\n const base = Math.min(16_000, 1000 * 2 ** attempt);\n return base + Math.floor(Math.random() * 500);\n}\n\nexport async function fetchText(options: FetchOptions): Promise<string> {\n const { url, userAgent, timeoutMs, maxRetries, limiter, logger } = options;\n const doFetch = options.fetchImpl ?? fetch;\n\n let lastError: Error | null = null;\n /** Honoured before the next attempt rather than slept after the last one. */\n let askedWaitMs = 0;\n\n for (let attempt = 0; attempt <= maxRetries; attempt += 1) {\n if (askedWaitMs > 0) {\n logger.debug(`waiting ${askedWaitMs}ms, as asked`);\n await new Promise((resolve) => setTimeout(resolve, askedWaitMs));\n askedWaitMs = 0;\n }\n await limiter.beforeRequest();\n\n const controller = new AbortController();\n const deadline = setTimeout(() => controller.abort(), timeoutMs);\n\n try {\n logger.debug(`GET ${url}`);\n const response = await doFetch(url, {\n signal: controller.signal,\n redirect: \"follow\",\n headers: { \"user-agent\": userAgent, accept: \"application/json, text/plain, */*\" },\n });\n\n if (response.ok) {\n limiter.succeeded();\n return await response.text();\n }\n\n if (PUSH_BACK.has(response.status)) {\n limiter.pushBack();\n await response.body?.cancel().catch(() => undefined);\n const asked = parseRetryAfter(response.headers.get(\"retry-after\"));\n\n if (asked !== null && asked > LONGEST_WAIT_MS) {\n throw rateLimited(\n `The Library of Congress asked this client to wait ${Math.round(asked / 1000)} seconds (HTTP ${response.status}).`,\n { url, status: response.status },\n );\n }\n if (attempt >= maxRetries) {\n throw rateLimited(\n `The Library of Congress asked this client to slow down (HTTP ${response.status}).`,\n { url, status: response.status },\n );\n }\n askedWaitMs = asked ?? backoffMs(attempt);\n lastError = new Error(`HTTP ${response.status}`);\n continue;\n }\n\n if (RETRYABLE.has(response.status) && attempt < maxRetries) {\n // An abandoned body keeps its socket out of the pool until it is\n // consumed or cancelled.\n await response.body?.cancel().catch(() => undefined);\n lastError = new Error(`HTTP ${response.status}`);\n askedWaitMs = backoffMs(attempt);\n continue;\n }\n\n // The request itself was refused: the site read it and would not run it.\n // Calling that a network failure invites a retry of something only the\n // caller can fix.\n if (response.status === 400 || response.status === 422) {\n throw invalidInput(\n \"The Library of Congress would not accept this request.\",\n \"Check the query. A quotation mark or bracket left unbalanced is read as syntax.\",\n );\n }\n\n // The site answered, and answered that it holds nothing at this address.\n // Calling that a network failure invites a retry of a settled question.\n if (response.status === 404 || response.status === 410) {\n throw notFound(\"The Library of Congress holds nothing at this address.\", {\n url,\n status: response.status,\n });\n }\n\n throw networkError(`The Library of Congress answered HTTP ${response.status}.`, {\n url,\n status: response.status,\n });\n } catch (error) {\n clearTimeout(deadline);\n\n // An error this module raised on purpose already says what happened.\n if (error instanceof Error && error.name === \"LocError\") throw error;\n\n if (error instanceof Error && error.name === \"AbortError\") {\n lastError = error;\n if (attempt >= Math.min(maxRetries, RETRIES_AFTER_SILENCE)) {\n throw timeoutError(\n `No answer from the Library of Congress within ${timeoutMs}ms. The newspaper corpus in particular can take tens of seconds.`,\n { url },\n );\n }\n askedWaitMs = backoffMs(attempt);\n continue;\n }\n\n lastError = error instanceof Error ? error : new Error(String(error));\n if (attempt >= maxRetries) {\n throw networkError(`Could not reach the Library of Congress: ${lastError.message}`, {\n url,\n });\n }\n askedWaitMs = backoffMs(attempt);\n continue;\n } finally {\n clearTimeout(deadline);\n }\n }\n\n throw networkError(\n `Could not reach the Library of Congress: ${lastError?.message ?? \"no attempt was made\"}`,\n { url },\n );\n}\n\n/** Fetch and parse JSON, keeping the two failures apart. */\nexport async function fetchJson<T = unknown>(options: FetchOptions): Promise<T> {\n const body = await fetchText(options);\n try {\n return JSON.parse(body) as T;\n } catch {\n // A long answer that arrives cut off lands here as well as a page of HTML,\n // and both mean the same thing to a caller: nothing readable came back.\n throw parseFailure(\"The Library of Congress answered with something that is not JSON.\", {\n url: options.url,\n });\n }\n}\n","/**\n * Every host, route, parameter and response field the server reads, in one\n * place. An upstream rename is then a one-file change rather than a hunt\n * through the parsers.\n *\n * The site's own robots file disallows `/search` for every client and asks for\n * five seconds between requests. No address built here reaches that path: the\n * format routes below, the collections route and the item route are what this\n * server calls, and the spacing it keeps is wider than the one asked for. The\n * site returns links into `/search` inside its facet blocks, and those links\n * are read as labels rather than followed.\n */\n\nexport const HOST = \"https://www.loc.gov\";\n\n/**\n * One route per kind of thing, which is how the catalogue is divided.\n *\n * The vocabulary is the Library's own: a route is a path segment, and the\n * values a tool accepts are exactly these keys.\n */\nexport const FORMAT_ROUTE = {\n books: \"/books/\",\n photos: \"/photos/\",\n maps: \"/maps/\",\n audio: \"/audio/\",\n \"film-and-videos\": \"/film-and-videos/\",\n manuscripts: \"/manuscripts/\",\n \"notated-music\": \"/notated-music/\",\n newspapers: \"/newspapers/\",\n} as const;\n\nexport type FormatRoute = keyof typeof FORMAT_ROUTE;\n\nexport const FORMAT_ROUTES = Object.keys(FORMAT_ROUTE) as FormatRoute[];\n\n/** Digitised newspaper pages, whose text is what search_newspapers reads. */\nexport const NEWSPAPER_PAGES_ROUTE = \"/collections/chronicling-america/\";\n\nexport const COLLECTIONS_ROUTE = \"/collections/\";\n\nexport const ITEM_ROUTE = \"/item/\";\n\nexport const PARAM = {\n /** Asks for JSON rather than a web page. */\n format: \"fo\",\n /** The words to look for. */\n query: \"q\",\n /** Results per page. */\n perPage: \"c\",\n /** Which page, from 1. */\n page: \"sp\",\n /** A year range, written from/to. */\n dates: \"dates\",\n /** Facet filters, several of them separated by a vertical bar. */\n facets: \"fa\",\n /** Sort order. */\n sort: \"sb\",\n /** Which top-level blocks to return, which is what keeps a page small. */\n attributes: \"at\",\n /** Widens a search past what the Library has digitised. */\n all: \"all\",\n} as const;\n\n/**\n * The blocks worth returning.\n *\n * A search answer otherwise carries facet listings, breadcrumbs, featured items\n * and a site manifest, which together run to more than a megabyte on the\n * newspaper corpus while the rows themselves are a few kilobytes.\n */\nexport const WANTED_BLOCKS = \"results,pagination\";\n\n/**\n * Facet fields a filter may name, and nothing else.\n *\n * A facet the site does not know is neither refused nor applied: it answers\n * with the unfiltered result and no sign that the narrowing was dropped. Only\n * fields listed here are ever sent, so a filter that reaches the site is one\n * the site acts on.\n */\nexport const FACET_FIELD = {\n subject: \"subject\",\n location: \"location\",\n language: \"language\",\n contributor: \"contributor\",\n partof: \"partof\",\n /** The state a newspaper was published in, as the newspaper corpus files it. */\n state: \"location_state\",\n /** A newspaper, named with the town and the years it ran. */\n publication: \"partof_title\",\n} as const;\n\nexport type FacetField = keyof typeof FACET_FIELD;\n\n/**\n * Sort orders, mapped from the words the tools use.\n *\n * Relevance sends nothing, which is the site's own default.\n */\nexport const SORT = {\n relevance: \"\",\n newest: \"date_desc\",\n oldest: \"date\",\n title: \"title_s\",\n} as const;\n\nexport type SortKey = keyof typeof SORT;\n\n/** Fields read off a result row. */\nexport const ROW_FIELD = {\n id: \"id\",\n url: \"url\",\n title: \"title\",\n date: \"date\",\n contributor: \"contributor\",\n originalFormat: \"original_format\",\n onlineFormat: \"online_format\",\n location: \"location\",\n subject: \"subject\",\n description: \"description\",\n partOf: \"partof\",\n digitized: \"digitized\",\n /** Newspaper pages only, from here down. */\n pageNumber: \"number_page\",\n publicationTitle: \"partof_title\",\n state: \"location_state\",\n /** How many records a collection gathers. */\n count: \"count\",\n /** Lists what a collection holds. */\n items: \"items\",\n} as const;\n\n/** Fields read off an item document. */\nexport const ITEM_FIELD = {\n id: \"id\",\n url: \"url\",\n title: \"title\",\n date: \"date\",\n contributorNames: \"contributor_names\",\n description: \"description\",\n notes: \"notes\",\n subjects: \"subjects\",\n location: \"location\",\n language: \"language\",\n partOf: \"partof\",\n repository: \"repository\",\n callNumber: \"call_number\",\n rights: \"rights_advisory\",\n rightsFallback: \"rights_information\",\n originalFormat: \"original_format\",\n} as const;\n\n/** A record spanning many subjects would swamp an answer of ten rows. */\nexport const MOST_SUBJECTS = 12;\n\n/** Public page for a record, which every result carries so it can be cited. */\nexport const itemUrl = (identifier: string) =>\n `${HOST}${ITEM_ROUTE}${identifier.split(\"/\").map(encodeURIComponent).join(\"/\")}/`;\n","/**\n * Builders for every address the client fetches.\n *\n * Query strings are assembled here so no caller can smuggle a parameter past\n * the checks: which blocks come back, which facet fields may be named and\n * which sort orders exist are decided by this file, not by whoever calls a\n * tool. A facet field or a sort order the site does not know is accepted\n * silently and applied to nothing, so sending only known ones is what keeps a\n * filter from being quietly dropped.\n */\n\nimport {\n COLLECTIONS_ROUTE,\n FACET_FIELD,\n FORMAT_ROUTE,\n HOST,\n ITEM_ROUTE,\n NEWSPAPER_PAGES_ROUTE,\n PARAM,\n SORT,\n WANTED_BLOCKS,\n type FacetField,\n type FormatRoute,\n type SortKey,\n} from \"./paths.js\";\nimport { invalidInput } from \"../errors.js\";\n\n/**\n * A year the site accepts at either end of a range. An open end is written as\n * the far bound rather than left out, because the parameter takes both.\n */\nexport const YEAR_BOUND = { earliest: 1000, latest: 9999 } as const;\n\nexport type Facets = Partial<Record<FacetField, string>>;\n\nexport interface CatalogueQuery {\n query: string;\n format: FormatRoute;\n facets?: Facets;\n yearFrom?: number;\n yearTo?: number;\n sort?: SortKey;\n /**\n * When false, the search widens past what the Library has digitised and\n * takes in catalogue records with no copy to read online.\n */\n onlineOnly: boolean;\n limit: number;\n page: number;\n}\n\nfunction base(path: string): URL {\n const url = new URL(HOST + path);\n url.searchParams.set(PARAM.format, \"json\");\n url.searchParams.set(PARAM.attributes, WANTED_BLOCKS);\n return url;\n}\n\n/**\n * Facet filters, written as the site writes them: `field:value`, several of\n * them joined by a vertical bar. Fields outside the known set never reach here.\n */\nexport function facetString(facets: Facets): string {\n return Object.entries(facets)\n .filter(([, value]) => typeof value === \"string\" && value.trim() !== \"\")\n .map(([field, value]) => `${FACET_FIELD[field as FacetField]}:${value.trim().toLowerCase()}`)\n .join(\"|\");\n}\n\nexport function catalogueUrl(q: CatalogueQuery): string {\n const url = base(FORMAT_ROUTE[q.format]);\n url.searchParams.set(PARAM.query, q.query);\n url.searchParams.set(PARAM.perPage, String(q.limit));\n url.searchParams.set(PARAM.page, String(q.page));\n\n if (q.yearFrom !== undefined || q.yearTo !== undefined) {\n const from = q.yearFrom ?? YEAR_BOUND.earliest;\n const to = q.yearTo ?? YEAR_BOUND.latest;\n url.searchParams.set(PARAM.dates, `${from}/${to}`);\n }\n\n const facets = facetString(q.facets ?? {});\n if (facets !== \"\") url.searchParams.set(PARAM.facets, facets);\n\n const sort = q.sort ? SORT[q.sort] : \"\";\n if (sort !== \"\") url.searchParams.set(PARAM.sort, sort);\n\n // The format routes answer with digitised material by default. Widening is\n // what takes the search into records the Library holds on a shelf alone.\n if (!q.onlineOnly) url.searchParams.set(PARAM.all, \"true\");\n\n return url.toString();\n}\n\n/**\n * The narrowing a full-text newspaper search may carry.\n *\n * The corpus files a page under the state it was published in and under the\n * paper it belongs to, and both are facets the route acts on.\n */\nexport interface NewspaperFilters {\n facets?: Facets;\n yearFrom?: number;\n yearTo?: number;\n}\n\nexport function newspaperPagesUrl(\n query: string,\n limit: number,\n page: number,\n filters: NewspaperFilters = {},\n): string {\n const url = base(NEWSPAPER_PAGES_ROUTE);\n url.searchParams.set(PARAM.query, query);\n url.searchParams.set(PARAM.perPage, String(limit));\n url.searchParams.set(PARAM.page, String(page));\n\n if (filters.yearFrom !== undefined || filters.yearTo !== undefined) {\n const from = filters.yearFrom ?? YEAR_BOUND.earliest;\n const to = filters.yearTo ?? YEAR_BOUND.latest;\n url.searchParams.set(PARAM.dates, `${from}/${to}`);\n }\n\n const facets = facetString(filters.facets ?? {});\n if (facets !== \"\") url.searchParams.set(PARAM.facets, facets);\n\n return url.toString();\n}\n\nexport function collectionsUrl(limit: number, page: number): string {\n const url = base(COLLECTIONS_ROUTE);\n url.searchParams.set(PARAM.perPage, String(limit));\n url.searchParams.set(PARAM.page, String(page));\n return url.toString();\n}\n\n/**\n * One record.\n *\n * An identifier can carry slashes: a newspaper page is addressed by its paper,\n * its date and its edition together. Each segment is encoded on its own so the\n * separators survive while the segments cannot open a path of their own.\n */\nexport function itemDocumentUrl(identifier: string): string {\n const segments = identifier\n .trim()\n .replace(/^\\/+|\\/+$/g, \"\")\n .split(\"/\")\n .filter((segment) => segment !== \"\");\n\n // Percent-encoding leaves a dot alone, because a dot is legal in a path. A\n // segment made only of dots is then resolved away when the address is parsed,\n // which walks the request out of the item route and can land it on a path the\n // robots file withholds. An identifier that climbs is not an identifier.\n if (segments.some((segment) => /^\\.+$/.test(segment))) {\n throw invalidInput(\n `\"${identifier}\" is not an identifier: it carries a relative path segment.`,\n \"Take the identifier from a search result rather than building one.\",\n );\n }\n\n const path = segments.map(encodeURIComponent).join(\"/\");\n const url = new URL(`${HOST}${ITEM_ROUTE}${path}/`);\n url.searchParams.set(PARAM.format, \"json\");\n return url.toString();\n}\n\n/**\n * The identifier inside an address the site published.\n *\n * A row points at `/item/<id>/`, at `/resource/<paper>/<date>/<edition>/` for a\n * newspaper page, at `/collections/<slug>/` for a collection, or at an LCCN\n * host for a catalogue record with no digitised copy. All four name something\n * the item route can read. An address in none of those shapes yields null\n * rather than a guess, since an identifier that does not resolve sends the next\n * call to a page that does not exist.\n */\nexport function identifierFrom(address: string | null): string | null {\n if (!address) return null;\n const trimmed = address.trim();\n if (trimmed === \"\") return null;\n\n // Rows carry protocol-relative addresses as often as absolute ones.\n const absolute = trimmed.startsWith(\"//\") ? `https:${trimmed}` : trimmed;\n let parsed: URL;\n try {\n parsed = new URL(absolute);\n } catch {\n return null;\n }\n\n const path = parsed.pathname.replace(/^\\/+|\\/+$/g, \"\");\n if (path === \"\") return null;\n const segments = path.split(\"/\");\n\n if (parsed.hostname === \"lccn.loc.gov\") return segments[0] ?? null;\n\n const first = segments[0];\n if (first === \"item\" || first === \"resource\") {\n const rest = segments.slice(1);\n return rest.length > 0 ? rest.join(\"/\") : null;\n }\n // A collection is named by its slug alone: the address goes on to a page\n // about the collection, which is not part of what names it.\n if (first === \"collections\") return segments[1] ?? null;\n return null;\n}\n","/**\n * Turning the Library's answers into the shapes this server publishes.\n *\n * Two rules run through every function here. A response that cannot be read is\n * a `parse_failure`, never an empty result, because a caller cannot tell an\n * empty result from an absence and will report one as the other. And a single\n * unreadable row among many is skipped rather than failing the page it sat in;\n * where a parser is given a counter, the rows it dropped are reported.\n */\n\nimport { notFound, parseFailure } from \"../errors.js\";\nimport type {\n CollectionResults,\n CollectionSummary,\n ItemDetail,\n ItemResource,\n NewspaperHit,\n NewspaperResults,\n Paging,\n RecordSummary,\n SearchResults,\n} from \"../types.js\";\nimport { ITEM_FIELD, MOST_SUBJECTS, ROW_FIELD, itemUrl } from \"./paths.js\";\nimport { identifierFrom } from \"./urls.js\";\n\ntype Json = Record<string, unknown>;\n\nconst asObject = (value: unknown): Json | null =>\n typeof value === \"object\" && value !== null && !Array.isArray(value) ? (value as Json) : null;\n\n/**\n * Almost every field arrives either as one value or as a list of them,\n * depending on how many the record carries. Both are read the same way.\n */\nfunction asStrings(value: unknown): string[] {\n if (typeof value === \"string\") return value.trim() === \"\" ? [] : [value];\n if (Array.isArray(value))\n return value.filter((v): v is string => typeof v === \"string\" && v.trim() !== \"\");\n return [];\n}\n\nconst asString = (value: unknown): string | null => asStrings(value)[0] ?? null;\n\n/**\n * Names of things, however the route chose to write them.\n *\n * A search row spells a subject or a collection as a plain string. The item\n * route spells the same thing as an entry pairing that name with a link to it,\n * sometimes under a `title` key and sometimes as the key itself. Reading only\n * strings drops every one of them and publishes an empty list, which reads as a\n * record belonging to nothing.\n */\nfunction asLabels(value: unknown): string[] {\n const entries = Array.isArray(value) ? value : [value];\n const labels: string[] = [];\n for (const entry of entries) {\n if (typeof entry === \"string\") {\n if (entry.trim() !== \"\") labels.push(entry);\n continue;\n }\n const object = asObject(entry);\n if (!object) continue;\n const titled = asString(object.title);\n if (titled !== null) {\n labels.push(titled);\n continue;\n }\n for (const key of Object.keys(object)) {\n if (key.trim() !== \"\") labels.push(key);\n }\n }\n return labels;\n}\n\nfunction asNumber(value: unknown): number | null {\n if (typeof value === \"number\" && Number.isFinite(value)) return value;\n if (typeof value === \"string\" && value.trim() !== \"\") {\n const n = Number(value);\n if (Number.isFinite(n)) return n;\n }\n if (Array.isArray(value)) return asNumber(value[0]);\n return null;\n}\n\n/**\n * A page number arrives as a zero-padded string such as \"0000000082\". Read as\n * a number it is the leaf of the issue; read as the string it was written in it\n * would be printed to a caller exactly as stored.\n */\nfunction asPageNumber(value: unknown): number | null {\n const text = asString(value);\n if (text === null) return null;\n const digits = text.trim();\n if (!/^\\d+$/.test(digits)) return null;\n const n = Number(digits);\n return Number.isFinite(n) && n > 0 ? n : null;\n}\n\n/**\n * A year reaches us as \"1998\", as \"1998-04-03\" or as a number. Dates outside a\n * plausible range are dropped rather than published: a record with no date\n * carries a placeholder, and read as a year that placeholder sorts ahead of\n * every real one.\n */\nconst PLAUSIBLE_YEAR = { first: 1000, last: 2200 } as const;\n\nexport function asYear(value: unknown): number | null {\n const plausible = (year: number): number | null =>\n Number.isFinite(year) && year >= PLAUSIBLE_YEAR.first && year <= PLAUSIBLE_YEAR.last\n ? Math.trunc(year)\n : null;\n\n const text = asString(value);\n if (text !== null) {\n const m = /(\\d{4})/.exec(text);\n if (m) return plausible(Number(m[1]));\n }\n\n const direct = asNumber(value);\n return direct === null ? null : plausible(direct);\n}\n\n/** Titles and captions come through carrying markup and escaped entities. */\nexport function plainText(value: string): string {\n return value\n .replace(/<[^>]*>/g, \"\")\n .replace(/&/g, \"&\")\n .replace(/"/g, '\"')\n .replace(/�?39;|'/g, \"'\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/ /g, \" \")\n .replace(/\\s+/g, \" \")\n .trim();\n}\n\n/**\n * The two counts the site reports, kept apart.\n *\n * `of` counts results and `total` counts the pages they are divided into. The\n * names invite the opposite reading, and taking one for the other reports a\n * corpus of four hundred thousand pages as two hundred thousand.\n */\nexport function toPaging(payload: unknown, url: string): Paging {\n const root = asObject(payload);\n const pagination = root ? asObject(root.pagination) : null;\n if (!pagination) {\n throw parseFailure(\n \"The answer carried no pagination block, so nothing states how many results match.\",\n {\n url,\n },\n );\n }\n const resultCount = asNumber(pagination.of);\n if (resultCount === null) {\n // Returning zero for a count that could not be read publishes an absence\n // the response never established.\n throw parseFailure(\"The answer carried no readable count of results.\", { url });\n }\n return {\n resultCount,\n pageCount: asNumber(pagination.total),\n currentPage: asNumber(pagination.current),\n perPage: asNumber(pagination.perpage),\n };\n}\n\nfunction rowsOf(payload: unknown, url: string): Json[] {\n const root = asObject(payload);\n if (!root || !Array.isArray(root.results)) {\n throw parseFailure(\"The answer carried its rows in a shape this server cannot read.\", { url });\n }\n return root.results.map(asObject).filter((r): r is Json => r !== null);\n}\n\n/** Whoever the Library credits, joined as one line. */\nconst creatorOf = (row: Json): string | null =>\n asStrings(row[ROW_FIELD.contributor]).map(plainText).join(\", \") || null;\n\n/** The public address of a row, absolute and https, whatever shape it arrived in. */\nfunction sourceUrlOf(row: Json, identifier: string | null): string {\n const raw = asString(row[ROW_FIELD.url]) ?? asString(row[ROW_FIELD.id]);\n if (raw) {\n const trimmed = raw.trim();\n if (trimmed.startsWith(\"//\")) return `https:${trimmed}`;\n if (trimmed.startsWith(\"http://\")) return `https://${trimmed.slice(\"http://\".length)}`;\n if (trimmed.startsWith(\"https://\")) return trimmed;\n }\n return identifier === null ? \"\" : itemUrl(identifier);\n}\n\nexport function toSearchResults(\n payload: unknown,\n url: string,\n onSkip: (n: number) => void,\n): SearchResults {\n const rows = rowsOf(payload, url);\n const records: RecordSummary[] = [];\n let skipped = 0;\n\n for (const row of rows) {\n const title = asString(row[ROW_FIELD.title]);\n const identifier = identifierFrom(asString(row[ROW_FIELD.url]) ?? asString(row[ROW_FIELD.id]));\n const source = sourceUrlOf(row, identifier);\n // A row with neither a title nor an address cannot be shown or followed.\n if (title === null && source === \"\") {\n skipped += 1;\n continue;\n }\n records.push({\n identifier,\n title: title === null ? null : plainText(title),\n creator: creatorOf(row),\n year: asYear(row[ROW_FIELD.date]),\n date: asString(row[ROW_FIELD.date]),\n format: asString(row[ROW_FIELD.originalFormat]),\n location: asStrings(row[ROW_FIELD.location]),\n subjects: asStrings(row[ROW_FIELD.subject]).slice(0, MOST_SUBJECTS),\n online: row[ROW_FIELD.digitized] === true,\n sourceUrl: source,\n });\n }\n\n if (skipped > 0) onSkip(skipped);\n if (rows.length > 0 && records.length === 0) {\n throw parseFailure(`${rows.length} rows came back and none could be read.`, { url });\n }\n // Paging follows the count the site reported, not the count that survived\n // reading: a shortened page reads as the end of the results.\n return { paging: toPaging(payload, url), records };\n}\n\n/**\n * Words a search is looking for, as an excerpt has to find them.\n *\n * A quoted phrase is kept whole, because that is what the caller asked to see\n * together. Very short words and the commonest joining words are set aside:\n * they occur on every line of every page, and centring a passage on one of them\n * shows a caller nothing.\n */\nconst JOINING_WORDS = new Set([\n \"the\",\n \"and\",\n \"for\",\n \"with\",\n \"that\",\n \"this\",\n \"from\",\n \"was\",\n \"were\",\n \"are\",\n \"his\",\n \"her\",\n \"its\",\n \"not\",\n \"but\",\n \"had\",\n \"has\",\n \"you\",\n \"all\",\n \"any\",\n \"who\",\n]);\n\nexport function queryTerms(query: string): string[] {\n const terms: string[] = [];\n const phrases = query.match(/\"([^\"]+)\"/g) ?? [];\n for (const phrase of phrases) {\n const inner = phrase.slice(1, -1).trim().toLowerCase();\n if (inner !== \"\") terms.push(inner);\n }\n\n const rest = query.replace(/\"[^\"]*\"/g, \" \");\n for (const word of rest.split(/[^\\p{L}\\p{N}'-]+/u)) {\n const lower = word.trim().toLowerCase();\n if (lower.length < 3 || JOINING_WORDS.has(lower)) continue;\n terms.push(lower);\n }\n\n return [...new Set(terms)];\n}\n\nexport interface ExcerptBudget {\n /** Characters one passage may run to. */\n maxChars: number;\n /** Passages kept for one page. */\n maxCount: number;\n}\n\nexport interface Excerpts {\n passages: string[];\n /** True when a searched word was found in the text this row carries. */\n located: boolean;\n}\n\n/** Widen a cut to the nearest space, so a passage opens and closes on a word. */\nfunction toWordBoundary(text: string, index: number, direction: -1 | 1): number {\n const limit = direction === -1 ? 0 : text.length;\n let at = index;\n for (let step = 0; step < 30 && at !== limit; step += 1) {\n if (text[at] === \" \") return at;\n at += direction;\n }\n return index;\n}\n\n/**\n * Passages of a page's machine-read text, centred on the words searched for.\n *\n * When none of those words appears, the opening of the text is returned and\n * `located` is false. That distinction is the whole point of this function: the\n * text a search returns with a page is its opening rather than the whole of it,\n * so a passage that does not carry the words is the start of the page and not\n * the place the Library matched.\n */\nexport function excerptsFor(text: string, terms: string[], budget: ExcerptBudget): Excerpts {\n const clean = text.replace(/\\s+/g, \" \").trim();\n if (clean === \"\") return { passages: [], located: false };\n\n const haystack = clean.toLowerCase();\n const found: Array<{ at: number; length: number }> = [];\n for (const term of terms) {\n let from = 0;\n while (from <= haystack.length - term.length) {\n const at = haystack.indexOf(term, from);\n if (at === -1) break;\n found.push({ at, length: term.length });\n from = at + term.length;\n }\n }\n\n if (found.length === 0) {\n const opening = clean.slice(0, budget.maxChars);\n const cut = opening.length < clean.length;\n return {\n passages: [cut ? `${opening.trimEnd()}…` : opening],\n located: false,\n };\n }\n\n found.sort((a, b) => a.at - b.at);\n const windows: Array<{ start: number; end: number }> = [];\n for (const hit of found) {\n const half = Math.max(0, Math.floor((budget.maxChars - hit.length) / 2));\n const start = toWordBoundary(clean, Math.max(0, hit.at - half), -1);\n const end = toWordBoundary(clean, Math.min(clean.length, hit.at + hit.length + half), 1);\n const previous = windows[windows.length - 1];\n if (previous && start <= previous.end) {\n // Two matches close together belong in one passage rather than in two\n // that repeat most of the same words.\n previous.end = Math.max(previous.end, end);\n continue;\n }\n windows.push({ start, end });\n }\n\n const passages = windows.slice(0, budget.maxCount).map(({ start, end }) => {\n const body = clean.slice(start, Math.min(end, start + budget.maxChars)).trim();\n const opensMidway = start > 0;\n const closesEarly = Math.min(end, start + budget.maxChars) < clean.length;\n return `${opensMidway ? \"…\" : \"\"}${body}${closesEarly ? \"…\" : \"\"}`;\n });\n\n return { passages, located: true };\n}\n\nexport function toNewspaperResults(\n payload: unknown,\n url: string,\n query: string,\n budget: ExcerptBudget,\n onSkip: (n: number) => void,\n): NewspaperResults {\n const rows = rowsOf(payload, url);\n const terms = queryTerms(query);\n const hits: NewspaperHit[] = [];\n let skipped = 0;\n\n for (const row of rows) {\n const address = asString(row[ROW_FIELD.url]) ?? asString(row[ROW_FIELD.id]);\n const source = sourceUrlOf(row, identifierFrom(address));\n if (source === \"\") {\n // Without an address the page cannot be read or cited, and a passage of\n // machine-read text with nothing behind it is not worth publishing.\n skipped += 1;\n continue;\n }\n\n const pageText = asStrings(row[ROW_FIELD.description]).join(\" \");\n const { passages, located } = excerptsFor(pageText, terms, budget);\n const publication = asString(row[ROW_FIELD.publicationTitle]);\n const title = asString(row[ROW_FIELD.title]);\n\n hits.push({\n identifier: identifierFrom(address),\n title: title === null ? null : plainText(title),\n // A newspaper page is credited to whoever contributed the scan, which is\n // a library rather than a writer.\n creator: creatorOf(row),\n year: asYear(row[ROW_FIELD.date]),\n pageNumber: asPageNumber(row[ROW_FIELD.pageNumber]),\n publishedOn: asString(row[ROW_FIELD.date]),\n publication,\n state: asString(row[ROW_FIELD.state]),\n wordsLocated: located,\n excerpts: passages,\n sourceUrl: source,\n });\n }\n\n if (skipped > 0) onSkip(skipped);\n if (rows.length > 0 && hits.length === 0) {\n throw parseFailure(`${rows.length} matches came back and none could be read.`, { url });\n }\n return { paging: toPaging(payload, url), hits };\n}\n\nfunction toResources(value: unknown): ItemResource[] {\n if (!Array.isArray(value)) return [];\n const resources: ItemResource[] = [];\n for (const entry of value) {\n const resource = asObject(entry);\n if (!resource) continue;\n const files = resource.files;\n const fileCount = Array.isArray(files)\n ? files.reduce<number>((sum, group) => sum + (Array.isArray(group) ? group.length : 1), 0)\n : asNumber(files);\n resources.push({\n caption: asString(resource.caption),\n fileCount,\n url: asString(resource.url),\n imageUrl: asString(resource.image),\n });\n }\n return resources;\n}\n\nfunction toCitations(value: unknown): Record<string, string> {\n const block = asObject(value);\n if (!block) return {};\n const citations: Record<string, string> = {};\n for (const [style, text] of Object.entries(block)) {\n if (typeof text !== \"string\" || text.trim() === \"\") continue;\n citations[style] = plainText(text);\n }\n return citations;\n}\n\nexport function toItemDetail(payload: unknown, identifier: string, url: string): ItemDetail {\n const root = asObject(payload);\n if (!root) throw parseFailure(\"The item answer was not an object.\", { url });\n\n // The site states a missing record in the body as well as in the status, and\n // a body saying so has to be read the same way.\n if (asNumber(root.status) === 404) {\n throw notFound(`The Library of Congress has no record called \"${identifier}\".`, { url });\n }\n\n const item = asObject(root.item);\n if (!item) {\n throw parseFailure(\n `The record for \"${identifier}\" came back without the item block this server reads.`,\n { url },\n );\n }\n if (Object.keys(item).length === 0) {\n throw notFound(`The Library of Congress has no record called \"${identifier}\".`, { url });\n }\n\n const description = asStrings(item[ITEM_FIELD.description]).map(plainText).join(\"\\n\\n\");\n const title = asString(item[ITEM_FIELD.title]);\n const source = asString(item[ITEM_FIELD.url]) ?? itemUrl(identifier);\n\n return {\n identifier,\n title: title === null ? null : plainText(title),\n creator: asStrings(item[ITEM_FIELD.contributorNames]).map(plainText).join(\", \") || null,\n year: asYear(item[ITEM_FIELD.date]),\n date: asString(item[ITEM_FIELD.date]),\n format: asString(item[ITEM_FIELD.originalFormat]),\n description: description === \"\" ? null : description,\n notes: asStrings(item[ITEM_FIELD.notes]).map(plainText),\n subjects: asLabels(item[ITEM_FIELD.subjects]).slice(0, MOST_SUBJECTS),\n location: asLabels(item[ITEM_FIELD.location]),\n language: asLabels(item[ITEM_FIELD.language]),\n partOf: asLabels(item[ITEM_FIELD.partOf]),\n repository: asStrings(item[ITEM_FIELD.repository]).map(plainText).join(\" \") || null,\n callNumber: asString(item[ITEM_FIELD.callNumber]),\n rights: asString(item[ITEM_FIELD.rights]) ?? asString(item[ITEM_FIELD.rightsFallback]),\n citations: toCitations(root.cite_this),\n resources: toResources(root.resources),\n sourceUrl: source.startsWith(\"//\") ? `https:${source}` : source,\n raw: item,\n };\n}\n\nexport function toCollections(\n payload: unknown,\n url: string,\n onSkip: (n: number) => void,\n): CollectionResults {\n const rows = rowsOf(payload, url);\n const collections: CollectionSummary[] = [];\n let skipped = 0;\n\n for (const row of rows) {\n const title = asString(row[ROW_FIELD.title]);\n if (title === null) {\n skipped += 1;\n continue;\n }\n const address = asString(row[ROW_FIELD.url]) ?? asString(row[ROW_FIELD.id]);\n const inner = asObject(row.item);\n const description = asStrings(row[ROW_FIELD.description]).map(plainText).join(\"\\n\\n\");\n collections.push({\n identifier: identifierFrom(address),\n title: plainText(title),\n description: description === \"\" ? null : description,\n itemCount: asNumber(row[ROW_FIELD.count]),\n subjects: asStrings(row[ROW_FIELD.subject]).slice(0, MOST_SUBJECTS),\n formats: inner ? asStrings(inner.formats) : [],\n sourceUrl: sourceUrlOf(row, null),\n itemsUrl: asString(row[ROW_FIELD.items]),\n });\n }\n\n if (skipped > 0) onSkip(skipped);\n if (rows.length > 0 && collections.length === 0) {\n throw parseFailure(`${rows.length} collections came back and none could be read.`, { url });\n }\n return { paging: toPaging(payload, url), collections };\n}\n","/**\n * Keeps this client to one request at a time, spaced out.\n *\n * The Library serves everyone free of charge and publishes what it will take:\n * ten requests a minute across the site. Serialising also means a burst of tool\n * calls cannot turn into a burst of connections.\n *\n * The spacing widens when the site pushes back and narrows again on a run of\n * quiet successes, so a slow patch does not become the permanent speed.\n */\n\nexport interface RateLimiterOptions {\n /** Spacing between requests when nothing has gone wrong. */\n intervalMs: number;\n /** The widest the spacing may become under push-back. */\n maxIntervalMs?: number;\n}\n\nexport class RateLimiter {\n private readonly baseIntervalMs: number;\n private readonly maxIntervalMs: number;\n private intervalMs: number;\n private lastStartedAt = 0;\n private queue: Promise<void> = Promise.resolve();\n private calmStreak = 0;\n\n constructor(options: RateLimiterOptions) {\n this.baseIntervalMs = options.intervalMs;\n this.maxIntervalMs = options.maxIntervalMs ?? options.intervalMs * 8;\n this.intervalMs = options.intervalMs;\n }\n\n /** The spacing in force, which callers report rather than guess. */\n get currentIntervalMs(): number {\n return this.intervalMs;\n }\n\n /**\n * Run `task` with nothing else in flight.\n *\n * This only serialises. Spacing is claimed per attempt through\n * `beforeRequest`, because a task that retries makes several requests and\n * each of them owes the site the same gap.\n */\n schedule<T>(task: () => Promise<T>): Promise<T> {\n const result = this.queue.then(task, task);\n this.queue = result.then(\n () => undefined,\n () => undefined,\n );\n return result;\n }\n\n /** Wait until enough time has passed since the previous request began. */\n async beforeRequest(): Promise<void> {\n const elapsed = Date.now() - this.lastStartedAt;\n // Capped at one whole interval, so a clock that moved backwards cannot\n // produce a wait longer than the spacing itself.\n const wait = Math.min(this.intervalMs, this.intervalMs - elapsed);\n if (wait > 0) await sleep(wait);\n this.lastStartedAt = Date.now();\n }\n\n /** Called when the site asks for room: double the gap, up to the ceiling. */\n pushBack(): void {\n this.calmStreak = 0;\n this.intervalMs = Math.min(this.maxIntervalMs, this.intervalMs * 2);\n }\n\n /**\n * Called on a clean answer. Recovery takes several in a row, so one lucky\n * response after a rough patch does not undo the caution that earned it.\n */\n succeeded(): void {\n if (this.intervalMs === this.baseIntervalMs) return;\n this.calmStreak += 1;\n if (this.calmStreak < 3) return;\n this.calmStreak = 0;\n this.intervalMs = Math.max(this.baseIntervalMs, Math.round(this.intervalMs / 2));\n }\n}\n\nexport function sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n","/**\n * The one place that talks to the Library of Congress.\n *\n * It holds a single rate limiter and a single cache, so pacing applies to the\n * server as a whole rather than to whichever tool happens to be running. It\n * imports nothing from the MCP layer and is published on its own, so the same\n * code serves a plain script.\n *\n * Every read fetches, parses and only then stores: a response nobody could\n * parse must not be served back for the rest of the cache's lifetime.\n */\n\nimport { invalidInput } from \"../errors.js\";\nimport type { Config, Logger } from \"../config.js\";\nimport { MIN_ALLOWED_INTERVAL_MS, createLogger, loadConfig } from \"../config.js\";\nimport { REPO_URL } from \"../version.js\";\nimport type { CollectionResults, ItemDetail, NewspaperResults, SearchResults } from \"../types.js\";\nimport { Cache } from \"./cache.js\";\nimport { fetchJson } from \"./http.js\";\nimport type { ExcerptBudget } from \"./parse.js\";\nimport { toCollections, toItemDetail, toNewspaperResults, toSearchResults } from \"./parse.js\";\nimport { RateLimiter } from \"./rateLimiter.js\";\nimport type { CatalogueQuery, NewspaperFilters } from \"./urls.js\";\nimport { catalogueUrl, collectionsUrl, itemDocumentUrl, newspaperPagesUrl } from \"./urls.js\";\n\nexport interface ClientOptions {\n config?: Partial<Config>;\n logger?: Logger;\n fetchImpl?: typeof fetch;\n}\n\n/** Every read reports whether it went out, so a caller can say what it knows. */\nexport interface Read<T> {\n data: T;\n cached: boolean;\n /** Rows the site sent that could not be read, which paging still counts. */\n skipped?: number;\n}\n\n/**\n * The two things this server owes the Library, applied to whatever it is\n * handed.\n *\n * A configuration object assembled by a caller has not been through\n * `loadConfig`, so it can carry a missing value, a value of the wrong shape, or\n * a User-Agent that names somebody else. Requests stay spaced at the published\n * ceiling, and the address the Library would use to reach a human stays in the\n * User-Agent, whichever of those arrives.\n */\nfunction withGuarantees(config: Config): Config {\n const defaults = loadConfig({});\n\n /** A setting that is absent or unreadable falls back rather than propagating. */\n const number = (value: unknown, fallback: number): number => {\n const parsed = Number(value);\n return Number.isFinite(parsed) ? parsed : fallback;\n };\n\n const claimed = typeof config.userAgent === \"string\" ? config.userAgent.trim() : \"\";\n const identifier = defaults.userAgent;\n\n return {\n ...config,\n // A caller may say who they are. Appending rather than replacing means the\n // Library can always tell which software is calling, and reach someone.\n userAgent:\n claimed === \"\" || claimed.includes(REPO_URL) ? identifier : `${claimed} ${identifier}`,\n minIntervalMs: Math.max(\n MIN_ALLOWED_INTERVAL_MS,\n number(config.minIntervalMs, defaults.minIntervalMs),\n ),\n timeoutMs: number(config.timeoutMs, defaults.timeoutMs),\n newspaperTimeoutMs: number(config.newspaperTimeoutMs, defaults.newspaperTimeoutMs),\n maxRetries: number(config.maxRetries, defaults.maxRetries),\n cacheTtlMs: number(config.cacheTtlMs, defaults.cacheTtlMs),\n cacheMaxEntries: number(config.cacheMaxEntries, defaults.cacheMaxEntries),\n };\n}\n\nexport class LocClient {\n private readonly config: Config;\n private readonly logger: Logger;\n private readonly limiter: RateLimiter;\n private readonly cache: Cache<unknown>;\n private readonly fetchImpl: typeof fetch | undefined;\n\n constructor(options: ClientOptions = {}) {\n const base = { ...loadConfig(), ...options.config };\n this.config = withGuarantees(base);\n this.logger = options.logger ?? createLogger(this.config.logLevel);\n this.limiter = new RateLimiter({ intervalMs: this.config.minIntervalMs });\n this.cache = new Cache(this.config.cacheTtlMs, this.config.cacheMaxEntries);\n this.fetchImpl = options.fetchImpl;\n }\n\n /** The pacing in force, which widens when the Library pushes back. */\n get intervalMs(): number {\n return this.limiter.currentIntervalMs;\n }\n\n /** What the Library sees this client call itself. */\n get userAgent(): string {\n return this.config.userAgent;\n }\n\n /**\n * `cacheKey` is stated rather than taken from the address, because two reads\n * of one address can produce different values: a passage cut to three hundred\n * characters is not the same value as the same passage cut to a thousand, and\n * serving one for the other would answer a budget the caller never set.\n */\n private async read<T>(\n cacheKey: string,\n url: string,\n parse: (payload: unknown, onSkip: (n: number) => void) => T,\n timeoutMs: number = this.config.timeoutMs,\n ): Promise<Read<T>> {\n const cached = this.cache.get(cacheKey) as T | undefined;\n if (cached !== undefined) {\n this.logger.debug(`cache hit ${cacheKey}`);\n return { data: cached, cached: true };\n }\n\n let skipped = 0;\n const payload = await this.limiter.schedule(() =>\n fetchJson({\n url,\n userAgent: this.config.userAgent,\n timeoutMs,\n maxRetries: this.config.maxRetries,\n limiter: this.limiter,\n logger: this.logger,\n ...(this.fetchImpl ? { fetchImpl: this.fetchImpl } : {}),\n }),\n );\n\n const data = parse(payload, (n) => {\n skipped += n;\n this.logger.warn(`skipped ${n} unreadable row(s) from ${url}`);\n });\n this.cache.set(cacheKey, data);\n return skipped > 0 ? { data, cached: false, skipped } : { data, cached: false };\n }\n\n searchItems(query: CatalogueQuery): Promise<Read<SearchResults>> {\n const url = catalogueUrl(query);\n return this.read(url, url, (payload, onSkip) => toSearchResults(payload, url, onSkip));\n }\n\n searchNewspapers(\n query: string,\n limit: number,\n page: number,\n budget: ExcerptBudget,\n filters: NewspaperFilters = {},\n ): Promise<Read<NewspaperResults>> {\n const trimmed = query.trim();\n if (trimmed === \"\") {\n return Promise.reject(invalidInput(\"Words to look for are required.\"));\n }\n const url = newspaperPagesUrl(trimmed, limit, page, filters);\n const key = `${url}#${budget.maxChars}x${budget.maxCount}`;\n return this.read(\n key,\n url,\n (payload, onSkip) => toNewspaperResults(payload, url, trimmed, budget, onSkip),\n // Reading the text of millions of pages takes the site far longer than\n // answering from the catalogue, so this route carries a budget of its own.\n this.config.newspaperTimeoutMs,\n );\n }\n\n getItem(identifier: string): Promise<Read<ItemDetail>> {\n const trimmed = identifier.trim();\n if (trimmed === \"\") {\n return Promise.reject(invalidInput(\"A record identifier is required.\"));\n }\n const url = itemDocumentUrl(trimmed);\n return this.read(url, url, (payload) => toItemDetail(payload, trimmed, url));\n }\n\n listCollections(limit: number, page: number): Promise<Read<CollectionResults>> {\n const url = collectionsUrl(limit, page);\n return this.read(url, url, (payload, onSkip) => toCollections(payload, url, onSkip));\n }\n}\n"],"mappings":"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","names":["base","base"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mcp-libraryofcongress",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server for the Library of Congress. Search inside digitised newspaper pages, browse the catalogue by format, and read records and collections. No API key required.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"mcp",
|
|
7
|
+
"model-context-protocol",
|
|
8
|
+
"library-of-congress",
|
|
9
|
+
"loc",
|
|
10
|
+
"chronicling-america",
|
|
11
|
+
"newspapers",
|
|
12
|
+
"full-text-search",
|
|
13
|
+
"archives"
|
|
14
|
+
],
|
|
15
|
+
"homepage": "https://github.com/smeet666/mcp-libraryofcongress#readme",
|
|
16
|
+
"bugs": {
|
|
17
|
+
"url": "https://github.com/smeet666/mcp-libraryofcongress/issues"
|
|
18
|
+
},
|
|
19
|
+
"funding": {
|
|
20
|
+
"type": "buymeacoffee",
|
|
21
|
+
"url": "https://buymeacoffee.com/smeet666"
|
|
22
|
+
},
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/smeet666/mcp-libraryofcongress.git"
|
|
26
|
+
},
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"author": "Damien Bouissou",
|
|
29
|
+
"type": "module",
|
|
30
|
+
"bin": {
|
|
31
|
+
"mcp-libraryofcongress": "dist/index.js"
|
|
32
|
+
},
|
|
33
|
+
"exports": {
|
|
34
|
+
".": "./dist/index.js",
|
|
35
|
+
"./client": {
|
|
36
|
+
"types": "./dist/loc/client.d.ts",
|
|
37
|
+
"import": "./dist/loc/client.js"
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"files": [
|
|
41
|
+
"dist",
|
|
42
|
+
"README.md",
|
|
43
|
+
"LICENSE",
|
|
44
|
+
"CHANGELOG.md"
|
|
45
|
+
],
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": ">=20.0.0"
|
|
48
|
+
},
|
|
49
|
+
"mcpName": "io.github.smeet666/mcp-libraryofcongress",
|
|
50
|
+
"scripts": {
|
|
51
|
+
"build": "tsup",
|
|
52
|
+
"typecheck": "tsc --noEmit",
|
|
53
|
+
"test": "vitest run test/unit",
|
|
54
|
+
"test:live": "vitest run test/live --testTimeout=120000",
|
|
55
|
+
"build:fixtures": "node scripts/build-fixtures.mjs",
|
|
56
|
+
"format": "prettier --write .",
|
|
57
|
+
"prepublishOnly": "npm run typecheck && npm run test && npm run build",
|
|
58
|
+
"start": "node dist/index.js",
|
|
59
|
+
"inspector": "npx @modelcontextprotocol/inspector node dist/index.js"
|
|
60
|
+
},
|
|
61
|
+
"dependencies": {
|
|
62
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
63
|
+
"zod": "^4.4.3"
|
|
64
|
+
},
|
|
65
|
+
"devDependencies": {
|
|
66
|
+
"@types/node": "^22.18.0",
|
|
67
|
+
"prettier": "^3.9.6",
|
|
68
|
+
"tsup": "^8.5.1",
|
|
69
|
+
"typescript": "^5.9.3",
|
|
70
|
+
"vitest": "^4.1.10"
|
|
71
|
+
}
|
|
72
|
+
}
|