mcp-databnf 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.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/server.ts","../src/version.ts","../src/config.ts","../src/errors.ts","../src/bnf/cache.ts","../src/bnf/endpoint.ts","../src/bnf/http.ts","../src/bnf/sparql.ts","../src/bnf/queries.ts","../src/bnf/parse.ts","../src/bnf/rateLimiter.ts","../src/bnf/client.ts","../src/tools/findDigitised.ts","../src/tools/arguments.ts","../src/tools/shared.ts","../src/tools/getAuthor.ts","../src/tools/getWork.ts","../src/tools/listEditions.ts","../src/tools/searchAuthors.ts","../src/tools/searchWorks.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * Entry point.\n *\n * stdout carries the protocol and nothing else: every diagnostic goes to\n * stderr, because a stray line on stdout corrupts the session for the client.\n */\n\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { createServer } from \"./server.js\";\n\nasync function main(): Promise<void> {\n const server = createServer();\n const transport = new StdioServerTransport();\n await server.connect(transport);\n\n const shutdown = async () => {\n await server.close().catch(() => undefined);\n process.exit(0);\n };\n process.on(\"SIGINT\", shutdown);\n process.on(\"SIGTERM\", shutdown);\n}\n\nmain().catch((error: unknown) => {\n const message = error instanceof Error ? error.message : String(error);\n process.stderr.write(`[mcp-databnf] fatal: ${message}\\n`);\n process.exit(1);\n});\n","/**\n * Wiring: one client, six tools, and the guidance a model reads before using\n * any of them.\n */\n\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { BnfClient } from \"./bnf/client.js\";\nimport type { Config, Logger } from \"./config.js\";\nimport { createLogger, loadConfig } from \"./config.js\";\nimport {\n findDigitisedDescription,\n findDigitisedInput,\n findDigitisedOutput,\n runFindDigitised,\n} from \"./tools/findDigitised.js\";\nimport type { FindDigitisedArgs } from \"./tools/findDigitised.js\";\nimport {\n getAuthorDescription,\n getAuthorInput,\n getAuthorOutput,\n runGetAuthor,\n} from \"./tools/getAuthor.js\";\nimport type { GetAuthorArgs } from \"./tools/getAuthor.js\";\nimport { getWorkDescription, getWorkInput, getWorkOutput, runGetWork } from \"./tools/getWork.js\";\nimport type { GetWorkArgs } from \"./tools/getWork.js\";\nimport {\n listEditionsDescription,\n listEditionsInput,\n listEditionsOutput,\n runListEditions,\n} from \"./tools/listEditions.js\";\nimport type { ListEditionsArgs } from \"./tools/listEditions.js\";\nimport {\n runSearchAuthors,\n searchAuthorsDescription,\n searchAuthorsInput,\n searchAuthorsOutput,\n} from \"./tools/searchAuthors.js\";\nimport type { SearchAuthorsArgs } from \"./tools/searchAuthors.js\";\nimport {\n runSearchWorks,\n searchWorksDescription,\n searchWorksInput,\n searchWorksOutput,\n} from \"./tools/searchWorks.js\";\nimport type { SearchWorksArgs } from \"./tools/searchWorks.js\";\nimport { PKG_VERSION } from \"./version.js\";\n\nexport interface CreateServerOptions {\n config?: Config;\n logger?: Logger;\n fetchImpl?: typeof fetch;\n}\n\n/** Nothing here writes or deletes; every tool only reads. */\nconst READ_ONLY = {\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: true,\n} as const;\n\nexport const INSTRUCTIONS = [\n \"Tools for data.bnf.fr, the open catalogue of the Bibliothèque nationale de France. No API key and no account are needed.\",\n \"The usual path is search_authors or search_works to get an identifier, then get_author, get_work or list_editions to read the record it names.\",\n \"The BnF publishes these metadata on one condition: name the source and state the date they were retrieved. Every answer carries 'retrieved_at' for that reason, and repeating both alongside what you show is what the licence asks for.\",\n \"Both searches read a full-text index that answers whether a title or a name matches and never scores how well, so rows come back in index order. Never present the first row as the best answer, and never describe a count as a ranking.\",\n \"One person can hold several authority records, and search_authors returns all of them: show the caller the choice rather than picking one.\",\n \"A work record is either established, with an ARK of its own, or provisional, addressed under 'temp-work' while a cataloguer settles it. A provisional identifier can change, so prefer an established one when citing.\",\n \"The catalogue describes what the BnF holds; it does not hold the texts. A record can point at a digitised copy on Gallica, and those links are returned as addresses for a person to open. This server never requests gallica.bnf.fr, because the BnF places its metadata and its digitised contents under two different regimes, so it can say a document exists at a link and nothing about what is there.\",\n \"This server paces itself, and a rate_limited error means the endpoint was asked to slow down, never that the thing you asked for is missing.\",\n \"Every result carries a source_url. Credit data.bnf.fr and link what you use.\",\n].join(\" \");\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 BnfClient({\n config,\n logger,\n ...(options.fetchImpl ? { fetchImpl: options.fetchImpl } : {}),\n });\n\n const server = new McpServer(\n { name: \"mcp-databnf\", version: PKG_VERSION },\n { instructions: INSTRUCTIONS },\n );\n\n server.registerTool(\n \"search_authors\",\n {\n title: \"Find a person in the BnF authority file\",\n description: searchAuthorsDescription,\n inputSchema: searchAuthorsInput,\n outputSchema: searchAuthorsOutput,\n annotations: READ_ONLY,\n },\n async (args) => runSearchAuthors(client, args as SearchAuthorsArgs),\n );\n\n server.registerTool(\n \"get_author\",\n {\n title: \"Read a person's record\",\n description: getAuthorDescription,\n inputSchema: getAuthorInput,\n outputSchema: getAuthorOutput,\n annotations: READ_ONLY,\n },\n async (args) => runGetAuthor(client, args as GetAuthorArgs),\n );\n\n server.registerTool(\n \"search_works\",\n {\n title: \"Find a work by its title\",\n description: searchWorksDescription,\n inputSchema: searchWorksInput,\n outputSchema: searchWorksOutput,\n annotations: READ_ONLY,\n },\n async (args) => runSearchWorks(client, args as SearchWorksArgs),\n );\n\n server.registerTool(\n \"get_work\",\n {\n title: \"Read a work's record\",\n description: getWorkDescription,\n inputSchema: getWorkInput,\n outputSchema: getWorkOutput,\n annotations: READ_ONLY,\n },\n async (args) => runGetWork(client, args as GetWorkArgs),\n );\n\n server.registerTool(\n \"list_editions\",\n {\n title: \"List the editions of a work\",\n description: listEditionsDescription,\n inputSchema: listEditionsInput,\n outputSchema: listEditionsOutput,\n annotations: READ_ONLY,\n },\n async (args) => runListEditions(client, args as ListEditionsArgs),\n );\n\n server.registerTool(\n \"find_digitised\",\n {\n title: \"Gather the digitised documents attached to a record\",\n description: findDigitisedDescription,\n inputSchema: findDigitisedInput,\n outputSchema: findDigitisedOutput,\n annotations: READ_ONLY,\n },\n async (args) => runFindDigitised(client, args as FindDigitisedArgs),\n );\n\n logger.info(\n `ready: user-agent=\"${config.userAgent}\", ${config.minIntervalMs}ms between requests, cache ${config.cacheTtlMs}ms`,\n );\n\n return server;\n}\n","/**\n * Kept in step with the version in package.json. The User-Agent embeds it, so\n * the BnF 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-databnf\";\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 narrowest spacing this client will ever use, in milliseconds.\n *\n * The BnF publishes no rate for the SPARQL endpoint. It publishes\n * `Crawl-delay: 5` for gallica.bnf.fr and enforces it there by banning an\n * address, so five seconds is the only figure the institution has stated about\n * how fast it wants to be read. Three seconds is the floor here because a\n * SPARQL query is one request where a crawl is thousands, and because every\n * answer is cached: the spacing that matters is the one between two distinct\n * questions.\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 = 120_000;\n/** The spacing in force when nothing asks for more. */\nexport const DEFAULT_INTERVAL_MS = 3000;\n\n/**\n * The deadline one query is given.\n *\n * Measured against the live endpoint, a lookup by identifier answers in well\n * under a second, while a full-text query over titles takes several seconds and\n * a query the planner handles badly is dropped by the server with an empty\n * body. Sixty seconds clears the slow honest case without holding the single\n * request slot for a query that will never answer.\n */\nexport const DEFAULT_TIMEOUT_MS = 60_000;\n\nexport interface Config {\n userAgent: string;\n minIntervalMs: number;\n timeoutMs: number;\n maxRetries: number;\n cacheTtlMs: number;\n cacheMaxEntries: number;\n logLevel: LogLevel;\n}\n\nexport const DEFAULT_USER_AGENT = `mcp-databnf/${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-databnf] ${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-databnf] 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-databnf] 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.BNF_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-databnf] error: BNF_LOG_LEVEL=\"${level}\" is not one of ${LOG_LEVELS.join(\", \")}; using error.\\n`,\n );\n }\n\n const custom = env.BNF_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 BnF has to be able to reach a human\n // about traffic it did not expect.\n userAgent: custom ? `${custom} ${DEFAULT_USER_AGENT}` : DEFAULT_USER_AGENT,\n minIntervalMs: readInteger(\n env,\n \"BNF_MIN_INTERVAL_MS\",\n DEFAULT_INTERVAL_MS,\n MIN_ALLOWED_INTERVAL_MS,\n MAX_ALLOWED_INTERVAL_MS,\n ),\n timeoutMs: readInteger(env, \"BNF_TIMEOUT_MS\", DEFAULT_TIMEOUT_MS, 1000, 300_000),\n maxRetries: readInteger(env, \"BNF_MAX_RETRIES\", 3, 0, 8),\n cacheTtlMs: readInteger(env, \"BNF_CACHE_TTL_MS\", 900_000, 0, 86_400_000),\n cacheMaxEntries: readInteger(env, \"BNF_CACHE_MAX_ENTRIES\", 200, 1, 5000),\n logLevel,\n };\n}\n","/**\n * One error type, carrying a code the caller can branch on.\n *\n * The distinction that matters most is between \"the BnF describes no such\n * thing\" 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 a\n * catalogue rather than a missing feature.\n */\n\nexport type ErrorCode =\n /** data.bnf.fr answered, and describes no such person, work or edition. */\n | \"not_found\"\n /** The arguments cannot produce a query. */\n | \"invalid_input\"\n /** data.bnf.fr asked this client to slow down or refused it for now. */\n | \"rate_limited\"\n /** An answer 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 BnfError 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 = \"BnfError\";\n this.code = code;\n this.details = details;\n }\n}\n\nexport const notFound = (message: string, details?: ErrorDetails) =>\n new BnfError(\"not_found\", message, details);\n\nexport const invalidInput = (message: string, hint?: string) =>\n new BnfError(\"invalid_input\", message, hint ? { hint } : {});\n\nexport const rateLimited = (message: string, details?: ErrorDetails) =>\n new BnfError(\"rate_limited\", message, {\n hint: \"Wait a moment and ask again. This says nothing about whether the BnF describes what you asked for.\",\n ...details,\n });\n\nexport const parseFailure = (message: string, details?: ErrorDetails) =>\n new BnfError(\"parse_failure\", message, {\n hint: \"data.bnf.fr may have changed how it answers. Please report this at https://github.com/smeet666/mcp-databnf/issues with the arguments you used.\",\n ...details,\n });\n\nexport const networkError = (message: string, details?: ErrorDetails) =>\n new BnfError(\"network_error\", message, details);\n\nexport const timeout = (message: string, details?: ErrorDetails) =>\n new BnfError(\"timeout\", message, details);\n","/**\n * A small in-memory store, keyed by the query that produced the value.\n *\n * It exists to keep a conversation that asks the same thing twice from asking\n * the endpoint twice, which matters here because a conversation about one\n * author walks the same records repeatedly: the person, then the works, then\n * the editions of one work, then back to the person.\n *\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 * Each entry keeps the moment it was read. The licence asks for the date the\n * metadata was retrieved, and a cached answer was retrieved when it entered the\n * store rather than when it was served, so the stored moment is what travels\n * with it.\n */\n\ninterface Entry<T> {\n value: T;\n expiresAt: number;\n retrievedAt: number;\n}\n\nexport interface Hit<T> {\n value: T;\n retrievedAt: 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): Hit<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 { value: entry.value, retrievedAt: entry.retrievedAt };\n }\n\n set(key: string, value: T, retrievedAt: number): 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, retrievedAt });\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 * Every host, address and vocabulary term this server reads, in one place. An\n * upstream rename is then a one-file change rather than a hunt through the\n * parsers.\n *\n * The robots file at data.bnf.fr withholds one path, the one ending in\n * `cross-documents`, and nothing built here reaches it. The endpoint below is\n * addressed without a trailing slash: with one, the same address serves a\n * JavaScript query editor rather than the query service.\n */\n\nimport { BnfError } from \"../errors.js\";\n\n/** The one address this server sends a request to. */\nexport const SPARQL_ENDPOINT = \"https://data.bnf.fr/sparql\";\n\n/** The host every request must be addressed to, checked before each call. */\nexport const ALLOWED_HOST = \"data.bnf.fr\";\n\n/**\n * The host this server describes and never requests.\n *\n * The BnF places its metadata and its digitised contents under two different\n * regimes. The metadata served here is free to reuse with its source and its\n * date of retrieval named. The contents on gallica.bnf.fr are not: their terms\n * make use inside an artificial-intelligence project subject to a paid licence\n * outside academic research, and the site refuses ClaudeBot and GPTBot at the\n * server, then bans the calling address after about fifteen requests whatever\n * the pace.\n *\n * So a Gallica address is a piece of metadata here, rendered as a link for a\n * person to open. `assertRequestable` below is what keeps that a rule rather\n * than an intention.\n */\nexport const GALLICA_HOST = \"gallica.bnf.fr\";\n\n/**\n * Refuse to request anything but the query service.\n *\n * This sits between the query builders and `fetch`, so a future route, a\n * redirect followed by hand, or an address read out of a response cannot become\n * a request to a host this server has no licence to read.\n */\nexport function assertRequestable(url: string): void {\n let parsed: URL;\n try {\n parsed = new URL(url);\n } catch {\n throw refusal(`mcp-databnf refuses to request \"${url}\": it is not an address.`);\n }\n const host = parsed.hostname.toLowerCase().replace(/\\.$/, \"\");\n if (host !== ALLOWED_HOST) {\n throw refusal(\n `mcp-databnf requests ${ALLOWED_HOST} and nothing else; \"${host}\" was refused. ` +\n `Gallica in particular is described from the catalogue and never read.`,\n );\n }\n}\n\n/**\n * The refusal, raised as an error the transport will not retry.\n *\n * A plain error here would be read as a request that failed to complete, and the\n * next attempt would be made against the same address. A decision not to call a\n * host is settled, so it has to arrive as an answer rather than as a mishap.\n */\nconst refusal = (message: string) =>\n new BnfError(\"network_error\", message, {\n hint: \"This server reads the BnF catalogue at data.bnf.fr. It holds no licence to read the digitised contents, so it does not follow an address that leaves that host.\",\n });\n\n/** True when an address points at Gallica, whatever the scheme or the case. */\nexport function isGallicaAddress(address: string): boolean {\n try {\n // A trailing dot names the root of the domain tree and resolves to the same\n // host. Comparing without stripping it would read a Gallica address as\n // somewhere else, and the link would be dropped from the answer.\n const host = new URL(address).hostname.toLowerCase().replace(/\\.$/, \"\");\n return host === GALLICA_HOST || host.endsWith(`.${GALLICA_HOST}`);\n } catch {\n return false;\n }\n}\n\n/** Namespaces the queries use, emitted as a PREFIX block. */\nexport const PREFIXES = {\n foaf: \"http://xmlns.com/foaf/0.1/\",\n rdf: \"http://www.w3.org/1999/02/22-rdf-syntax-ns#\",\n rdfs: \"http://www.w3.org/2000/01/rdf-schema#\",\n owl: \"http://www.w3.org/2002/07/owl#\",\n skos: \"http://www.w3.org/2004/02/skos/core#\",\n dcterms: \"http://purl.org/dc/terms/\",\n bio: \"http://vocab.org/bio/0.1/\",\n bnf: \"http://data.bnf.fr/ontology/bnf-onto/\",\n bibo: \"http://purl.org/ontology/bibo/\",\n isni: \"http://isni.org/ontology#\",\n /** RDA group 2 elements, which carry the biographical fields. */\n rdag2: \"http://rdvocab.info/ElementsGr2/\",\n /** RDA elements, which carry the bibliographic fields. */\n rdae: \"http://rdvocab.info/Elements/\",\n /** RDA relationships between work, expression, manifestation and item. */\n rdarel: \"http://rdvocab.info/RDARelationshipsWEMI/\",\n /** The FRBR classes data.bnf.fr types its entities with. */\n rdafrbr: \"http://rdvocab.info/uri/schema/FRBRentitiesRDA/\",\n} as const;\n\nexport const PREFIX_BLOCK = Object.entries(PREFIXES)\n .map(([name, iri]) => `PREFIX ${name}: <${iri}>`)\n .join(\"\\n\");\n\n/** The two shapes an entity address takes in this dataset. */\nexport const ARK_BASE = \"http://data.bnf.fr/ark:/12148/\";\nexport const TEMP_WORK_BASE = \"http://data.bnf.fr/temp-work/\";\n\n/** The page a person can open for a record, which every result carries. */\nexport const publicPageFor = (entityIri: string): string =>\n entityIri.replace(/#.*$/, \"\").replace(/^http:/, \"https:\");\n\n/** The BnF general catalogue, where a record is described for a reader. */\nexport const catalogueUrlFor = (arkId: string): string =>\n `https://catalogue.bnf.fr/ark:/12148/${arkId}`;\n\n/**\n * The licence this dataset is published under, and what it asks for.\n *\n * The BnF states it in one sentence: the metadata may be used freely and at no\n * cost, provided the source is named and the date of retrieval is stated. Both\n * halves are therefore attached to every answer rather than left to a caller.\n */\nexport const LICENCE_FR =\n \"L'utilisation de ces métadonnées est libre et gratuite sous réserve du maintien de la mention de leur source et de l'indication de leur date de récupération.\";\n\nexport const LICENCE_EN =\n \"These metadata may be used freely and at no cost, provided their source is named and the date they were retrieved is stated.\";\n","/**\n * One query sent to the endpoint, 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 service meant is never retried: asking again for something\n * that is not there wastes a request and delays the honest answer.\n *\n * The query travels in the body of a POST. A long query in a query string is\n * refused by an intermediary before it reaches the service, and a POST keeps the\n * text out of every access log between here and there.\n */\n\nimport {\n invalidInput,\n networkError,\n parseFailure,\n rateLimited,\n timeout as timeoutError,\n} from \"../errors.js\";\nimport type { Logger } from \"../config.js\";\nimport { SPARQL_ENDPOINT, assertRequestable } from \"./endpoint.js\";\nimport type { RateLimiter } from \"./rateLimiter.js\";\n\n/** The JSON shape a SPARQL SELECT answers with. */\nexport interface SparqlTerm {\n type: \"uri\" | \"literal\" | \"typed-literal\" | \"bnode\";\n value: string;\n datatype?: string;\n \"xml:lang\"?: string;\n}\n\nexport type SparqlRow = Record<string, SparqlTerm | undefined>;\n\nexport interface SparqlResults {\n head: { vars?: string[] };\n results: { bindings: SparqlRow[] };\n}\n\nexport interface QueryOptions {\n query: 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 service is busy, not answering \"no\". */\nconst RETRYABLE = new Set([408, 425, 429, 500, 502, 503, 504]);\n/**\n * Statuses that mean the service is refusing for now and naming a delay.\n *\n * These produce a `rate_limited` error and honour a `Retry-After` header. A\n * server error is a different claim, so it keeps its own code even though it\n * widens the spacing the same way.\n */\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 query that never answered is worth repeating.\n *\n * A query that did not respond within its budget is one the planner is\n * struggling with. Repeating it adds the same load again, and each attempt\n * holds the slot for the full deadline.\n */\nconst RETRIES_AFTER_SILENCE = 1;\n\n/**\n * Redirects worth following before calling it a loop.\n *\n * The query service answers directly, so one hop covers a host that has moved\n * and two cover a scheme change on top of it. Beyond that the answer is a\n * redirect rather than a result.\n */\nconst MAX_REDIRECTS = 2;\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\n/**\n * What a Virtuoso says when it will not run a query.\n *\n * It answers in plain text rather than in the result format, and the leading\n * code says which half went wrong: a compiler code means the query as written\n * could not be understood, and a runtime code means it was understood and then\n * abandoned, usually because it asked for more of the graph than the service\n * will spend on one caller.\n */\nexport function readEngineError(\n body: string,\n): { kind: \"compile\" | \"runtime\"; text: string } | null {\n const match = /^Virtuoso\\s+(\\w+)\\s+Error\\s+([\\s\\S]*)$/.exec(body.trim());\n if (!match) return null;\n const state = match[1] ?? \"\";\n const text = (match[2] ?? \"\").split(\"\\n\")[0]?.trim() ?? \"\";\n // 37000 is the compiler's own state; the rest come from execution.\n return { kind: state === \"37000\" ? \"compile\" : \"runtime\", text };\n}\n\nasync function postOnce(\n options: QueryOptions,\n signal: AbortSignal,\n): Promise<{ status: number; headers: Headers; body: string; ok: boolean }> {\n assertRequestable(SPARQL_ENDPOINT);\n const doFetch = options.fetchImpl ?? fetch;\n\n const form = new URLSearchParams();\n form.set(\"query\", options.query);\n // The endpoint answers in XML when only the Accept header asks for JSON, so\n // the format is stated in the request itself as well.\n form.set(\"format\", \"application/sparql-results+json\");\n\n // Redirects are handled here rather than by fetch. Following one automatically\n // would take the request to whatever host the answer names, and the guard\n // above only ever sees the address this file wrote: a redirect to Gallica\n // would cross the licence boundary with no code having decided to.\n let target = SPARQL_ENDPOINT;\n for (let hop = 0; hop <= MAX_REDIRECTS; hop += 1) {\n const response = await doFetch(target, {\n method: \"POST\",\n signal,\n redirect: \"manual\",\n headers: {\n \"user-agent\": options.userAgent,\n accept: \"application/sparql-results+json\",\n \"content-type\": \"application/x-www-form-urlencoded\",\n },\n body: form.toString(),\n });\n\n const location = response.headers.get(\"location\");\n if (response.status >= 300 && response.status < 400 && location !== null) {\n const next = new URL(location, target).toString();\n // Checked before the next request rather than after it, so a refused\n // address is one this client never called.\n assertRequestable(next);\n target = next;\n continue;\n }\n\n return {\n status: response.status,\n headers: response.headers,\n body: await response.text(),\n ok: response.ok,\n };\n }\n\n throw networkError(`data.bnf.fr redirected this request more than ${MAX_REDIRECTS} times.`, {\n url: SPARQL_ENDPOINT,\n });\n}\n\n/** Send one query and return the parsed result set. */\nexport async function runQuery(options: QueryOptions): Promise<SparqlResults> {\n const { timeoutMs, maxRetries, limiter, logger } = options;\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(`POST ${SPARQL_ENDPOINT} (${options.query.length} characters)`);\n const response = await postOnce(options, controller.signal);\n\n if (response.ok) {\n // The body decides whether this was an answer. This endpoint says \"I\n // will spend no more on you\" with a status of 200 and either an empty\n // body or a runtime error in plain text, so crediting the status line\n // would narrow the spacing at the one moment it has to widen.\n let parsed: SparqlResults;\n try {\n parsed = parseResults(response.body);\n } catch (error) {\n limiter.pushBack();\n throw error;\n }\n limiter.succeeded();\n return parsed;\n }\n\n // Anything worth another attempt means the service is struggling, so the\n // gap widens whether or not the status is one that names a delay. A\n // Virtuoso under load answers 500 as readily as 503.\n if (RETRYABLE.has(response.status)) limiter.pushBack();\n\n if (PUSH_BACK.has(response.status)) {\n const asked = parseRetryAfter(response.headers.get(\"retry-after\"));\n\n if (asked !== null && asked > LONGEST_WAIT_MS) {\n throw rateLimited(\n `data.bnf.fr asked this client to wait ${Math.round(asked / 1000)} seconds (HTTP ${response.status}).`,\n { url: SPARQL_ENDPOINT, status: response.status },\n );\n }\n if (attempt >= maxRetries) {\n throw rateLimited(\n `data.bnf.fr asked this client to slow down (HTTP ${response.status}).`,\n {\n url: SPARQL_ENDPOINT,\n status: response.status,\n },\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 lastError = new Error(`HTTP ${response.status}`);\n askedWaitMs = backoffMs(attempt);\n continue;\n }\n\n // The service read the query and would not run it. Calling that a network\n // failure invites a retry of something only the query can fix.\n if (response.status === 400 || response.status === 422) {\n const engine = readEngineError(response.body);\n throw invalidInput(\n `data.bnf.fr would not run this query${engine?.text ? `: ${engine.text}` : \".\"}`,\n \"Try fewer or plainer words. If this keeps happening with ordinary input, please report it.\",\n );\n }\n\n throw networkError(`data.bnf.fr answered HTTP ${response.status}.`, {\n url: SPARQL_ENDPOINT,\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 === \"BnfError\") 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 data.bnf.fr within ${timeoutMs}ms. A query spanning the whole catalogue can take longer than the service will spend on it.`,\n { url: SPARQL_ENDPOINT },\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 data.bnf.fr: ${lastError.message}`, {\n url: SPARQL_ENDPOINT,\n });\n }\n askedWaitMs = backoffMs(attempt);\n continue;\n } finally {\n clearTimeout(deadline);\n }\n }\n\n throw networkError(\n `Could not reach data.bnf.fr: ${lastError?.message ?? \"no attempt was made\"}`,\n {\n url: SPARQL_ENDPOINT,\n },\n );\n}\n\n/**\n * Read a result set out of a body that arrived with a success status.\n *\n * The endpoint answers 200 with an empty body when it abandons a query part way\n * through, which is the same silence as a result set holding no rows and means\n * the opposite. An empty body is therefore a failure to read rather than an\n * absence, so nothing downstream can report \"the BnF describes none\" on the\n * strength of a query that was never finished.\n */\nexport function parseResults(body: string): SparqlResults {\n if (body.trim() === \"\") {\n throw parseFailure(\n \"data.bnf.fr returned an empty body, which is what it does when it abandons a query part way through.\",\n { url: SPARQL_ENDPOINT },\n );\n }\n\n const engine = readEngineError(body);\n if (engine) {\n if (engine.kind === \"compile\") {\n throw invalidInput(\n `data.bnf.fr could not read this query: ${engine.text}`,\n \"Try fewer or plainer words. If this keeps happening with ordinary input, please report it.\",\n );\n }\n throw timeoutError(`data.bnf.fr started this query and gave it up: ${engine.text}`, {\n url: SPARQL_ENDPOINT,\n });\n }\n\n let payload: unknown;\n try {\n payload = JSON.parse(body);\n } catch {\n throw parseFailure(\"data.bnf.fr answered with something that is not JSON.\", {\n url: SPARQL_ENDPOINT,\n });\n }\n\n if (\n typeof payload !== \"object\" ||\n payload === null ||\n !(\"results\" in payload) ||\n typeof (payload as SparqlResults).results !== \"object\" ||\n !Array.isArray((payload as SparqlResults).results?.bindings)\n ) {\n throw parseFailure(\"data.bnf.fr answered with JSON that is not a SPARQL result set.\", {\n url: SPARQL_ENDPOINT,\n });\n }\n\n return payload as SparqlResults;\n}\n","/**\n * How caller text becomes part of a query, and what it can never become.\n *\n * A query is a program, and every character of it that came from outside is a\n * chance to write a different program. Three separate defences apply here, and\n * each is narrower than the one before:\n *\n * 1. Text that reaches the full-text index is reduced to words. Letters,\n * digits, apostrophes inside a word and hyphens inside a word survive; every\n * quote, brace, backslash, angle bracket and newline is dropped before the\n * query exists. There is no escaping to get right because there is nothing\n * left to escape.\n * 2. An identifier is matched against the two shapes this dataset uses and\n * rebuilt from the match, so the address in the query is one this file\n * wrote rather than one a caller supplied.\n * 3. Numbers reach the query as numbers, after being bounded.\n *\n * `escapeLiteral` exists for the values that are neither: it is the last line\n * rather than the first, and the tests hold it to producing a literal that\n * closes where it opened whatever it is handed.\n */\n\nimport { invalidInput } from \"../errors.js\";\n\n/**\n * Escape a string for a SPARQL short literal in double quotes.\n *\n * Backslash goes first: escaping it after the others would escape the\n * backslashes they just introduced. The characters a SPARQL short literal\n * cannot carry raw are a quote, a backslash, a line feed and a carriage\n * return; the rest are legal and pass through as published, since mangling a\n * title is its own kind of lie.\n */\nexport function escapeLiteral(value: string): string {\n return (\n value\n .replace(/\\\\/g, \"\\\\\\\\\")\n .replace(/\"/g, '\\\\\"')\n .replace(/\\n/g, \"\\\\n\")\n .replace(/\\r/g, \"\\\\r\")\n .replace(/\\t/g, \"\\\\t\")\n // The remaining control characters are legal inside a SPARQL literal and\n // meaningless in a catalogue query. A null byte in particular is where a\n // C string ends, so an intermediary or the engine can truncate the query\n // at one and run a shorter query than the one that was written.\n .replace(/[\\u0000-\\u0008\\u000b\\u000c\\u000e-\\u001f\\u007f]/gu, \"\")\n );\n}\n\n/** A literal, quoted and escaped, ready to be dropped into a query. */\nexport const literal = (value: string): string => `\"${escapeLiteral(value)}\"`;\n\n/**\n * Words a full-text query may carry.\n *\n * The endpoint is a Virtuoso, whose `bif:contains` takes its own miniature\n * language: words in single quotes joined by AND, OR and NOT, with `*` for a\n * prefix. Handing caller text to that language directly would let a caller\n * write the operators, so the text is cut into words instead and this file\n * writes every operator.\n *\n * A word keeps letters and digits in any script, and keeps an apostrophe or a\n * hyphen that sits between two other characters, because \"aujourd'hui\" and\n * \"Charleville-Mézières\" are one word each. A leading or trailing one is\n * dropped, so nothing can close the quoting this file opens.\n *\n * The same rule holds a span of years together: \"1871-1933\" stays one word.\n * Nothing here can tell a range from a compound name, and splitting on the\n * chance that it is a range would break every hyphenated place in France.\n */\nexport function toSearchWords(input: string): string[] {\n const cleaned = input\n .normalize(\"NFC\")\n // Anything that is not a letter, a digit, an apostrophe or a hyphen becomes\n // a separator. Typographic apostrophes are folded onto the plain one first,\n // since a name is written with either.\n .replace(/[‘’ʼ]/gu, \"'\")\n .replace(/[^\\p{L}\\p{N}'-]+/gu, \" \");\n\n const words: string[] = [];\n for (const raw of cleaned.split(\" \")) {\n // Trim the joiners at the edges: they only mean something between letters.\n const word = raw.replace(/^['-]+/u, \"\").replace(/['-]+$/u, \"\");\n if (word === \"\") continue;\n // A word this long is a paste rather than a word, and the index will not\n // match it anyway.\n words.push(word.length > 60 ? word.slice(0, 60) : word);\n if (words.length >= 12) break;\n }\n return words;\n}\n\n/**\n * A `bif:contains` argument requiring every word, built from words this file\n * quoted.\n *\n * The result is a SPARQL literal, so the double quotes belong to SPARQL and the\n * single quotes belong to the index. Two quotings are therefore in play, and\n * each has to be closed by the layer that opened it.\n *\n * The apostrophe kept inside a word is also the index's own quoting character,\n * so `rimbaud'OR'hugo` arrives as one word and emits `'rimbaud'OR'hugo'`, where\n * `OR` has escaped the quotes and become an operator. Doubling it puts it back\n * inside the term, which is how the index writes a literal quote. That is the\n * whole of the exposure: the word cannot reach the SPARQL layer as anything but\n * text, because `escapeLiteral` runs over the finished clause.\n */\nexport function containsAllWords(words: readonly string[]): string {\n if (words.length === 0) {\n throw invalidInput(\n \"No word to search for: the text held no letters or digits.\",\n \"Write the words to look for, such as a surname or a title.\",\n );\n }\n const clause = words.map((word) => `'${word.replace(/'/g, \"''\")}'`).join(\" AND \");\n return literal(clause);\n}\n\n/**\n * The two identifier shapes this dataset uses, and nothing else.\n *\n * An ARK identifier is `cb` followed by digits and one final check character,\n * which the BnF also writes as `cc…` on a few records. A provisional work is\n * named by a thirty-two character hexadecimal digest. Both are matched whole,\n * so an identifier reaches a query only after this file has rebuilt it.\n */\nconst ARK_ID = /^c[bc][0-9a-z]{6,20}$/;\nconst TEMP_WORK_ID = /^[0-9a-f]{32}$/;\n\nexport type EntityKind = \"ark\" | \"temp-work\";\n\nexport interface EntityId {\n kind: EntityKind;\n /** The bare identifier, as a person would quote it. */\n id: string;\n /** The address of the thing itself, which the queries bind to. */\n iri: string;\n /** The page a person can open. */\n pageUrl: string;\n}\n\n/**\n * Read an identifier a caller wrote, in any of the forms they see.\n *\n * Rows carry `http://data.bnf.fr/ark:/12148/cb119219976#about`, prose carries\n * `cb119219976`, and a browser carries `https://data.bnf.fr/ark:/12148/cb119219976`.\n * All three name one record. Anything else is refused rather than guessed at,\n * because an identifier built from a guess sends the next call to a record that\n * does not exist and the answer reads as an absence.\n */\nexport function parseEntityId(input: string): EntityId {\n const trimmed = input.trim();\n if (trimmed === \"\") {\n throw invalidInput(\"An identifier is required.\");\n }\n\n const arkMatch = /(?:^|\\/)ark:\\/12148\\/(c[bc][0-9a-z]+)/i.exec(trimmed);\n const bare = arkMatch ? arkMatch[1] : trimmed.replace(/^#/, \"\").split(/[#?]/)[0];\n const candidate = (bare ?? \"\").toLowerCase();\n\n if (ARK_ID.test(candidate)) {\n return {\n kind: \"ark\",\n id: candidate,\n iri: `http://data.bnf.fr/ark:/12148/${candidate}#about`,\n pageUrl: `https://data.bnf.fr/ark:/12148/${candidate}`,\n };\n }\n\n const tempMatch = /temp-work\\/([0-9a-f]{32})/i.exec(trimmed);\n const digest = (tempMatch?.[1] ?? candidate).toLowerCase();\n if (TEMP_WORK_ID.test(digest)) {\n return {\n kind: \"temp-work\",\n id: `temp-work/${digest}`,\n iri: `http://data.bnf.fr/temp-work/${digest}/#about`,\n pageUrl: `https://data.bnf.fr/temp-work/${digest}/`,\n };\n }\n\n throw invalidInput(`\"${input}\" is not a data.bnf.fr identifier.`, hintFor(trimmed));\n}\n\n/**\n * Two shapes people paste, which are addresses of something else entirely.\n *\n * A Gallica document identifier and a data.bnf.fr page address both look like\n * identifiers to whoever is holding one, and both are refused here. Refusing\n * them with the general advice reads as the server failing to know a record it\n * plainly has, so each is named for what it is and told what to do instead.\n */\nconst GALLICA_DOCUMENT = /^(?:bpt6k|btv1b|bd6t|cb32|btv1b)[0-9a-z]+$/i;\n/** `https://data.bnf.fr/fr/11907966/marguerite_duras/`, which is what a browser shows. */\nconst PAGE_ADDRESS = /data\\.bnf\\.fr\\/(?:[a-z]{2}\\/)?(\\d{6,12})\\/([a-z0-9_]+)/i;\n\nconst GENERAL_HINT =\n \"An identifier looks like 'cb119219976' for an established record, or 'temp-work/22d7f68c1a4bdd081ad7ca791fd3b730' for a provisional one. Take it from a search result rather than building one.\";\n\nfunction hintFor(input: string): string {\n const page = PAGE_ADDRESS.exec(input);\n if (page) {\n const name = (page[2] ?? \"\").replace(/_/g, \" \");\n return (\n `That is the readable address of a data.bnf.fr page, and ${page[1]} in it is the record's FRBNF number rather than the identifier these tools take. ` +\n `Search for \"${name}\" with search_authors or search_works, and use the identifier the row carries.`\n );\n }\n\n const gallicaArk = /ark:\\/12148\\/([0-9a-z]+)/i.exec(input)?.[1] ?? input;\n if (GALLICA_DOCUMENT.test(gallicaArk)) {\n return (\n \"That is a Gallica identifier: it names a digitised document rather than a record in the catalogue, and these tools read the catalogue. \" +\n \"Search for the work with search_works, then list_editions will give you the Gallica link alongside the edition it belongs to.\"\n );\n }\n\n return GENERAL_HINT;\n}\n\n/** An identifier written back as the address of the entity itself. */\nexport const iriOf = (id: EntityId): string => `<${id.iri}>`;\n\n/**\n * A whole number, bounded, written into a query.\n *\n * LIMIT and OFFSET take a bare number rather than a quoted value, so this is\n * the one place where something that is not a literal is assembled. Bounding\n * before writing is what makes that safe, and it also keeps a caller from\n * asking the endpoint for a page a hundred thousand rows deep.\n */\nexport function boundedInteger(value: number, min: number, max: number, name: string): number {\n if (!Number.isFinite(value) || !Number.isInteger(value)) {\n throw invalidInput(`'${name}' has to be a whole number.`);\n }\n if (value < min || value > max) {\n throw invalidInput(`'${name}' has to be between ${min} and ${max}.`);\n }\n return value;\n}\n","/**\n * The queries this server sends, and the reasons they are shaped as they are.\n *\n * Every one of them describes the endpoint as it actually answers: the shapes\n * below are the ones the live service returns, rather than the ones the\n * vocabularies promise.\n *\n * Three facts about this dataset govern most of what follows.\n *\n * **An entity and its record are two addresses.** A person is\n * `ark:/12148/cb119219976#about`, typed `foaf:Person`, and carries the dates,\n * the places and the alignments. The authority record is the same ARK without\n * the fragment, typed `skos:Concept`, and carries the headings, the editorial\n * notes and the date the record was last changed. A query that reads only one\n * of the two misses half the record.\n *\n * **Work, expression and manifestation are three levels.** A work links to\n * expressions, and a manifestation names both the expression it realises and\n * the work it manifests. `workManifested` is what makes the editions of a work\n * reachable in one step rather than two.\n *\n * **The full-text index drives, or the query is abandoned.** Virtuoso needs the\n * triple whose object it will search to appear before `bif:contains` in the same\n * group, and it needs that pattern to be the one that starts the plan. Asking\n * for `?w a rdafrbr:Work` first makes the planner walk every work in the\n * catalogue and the service gives the query up, answering 200 with an empty\n * body. Putting the text search in a subquery and filtering its output is what\n * keeps the index in front.\n */\n\nimport { PREFIX_BLOCK } from \"./endpoint.js\";\nimport type { EntityId } from \"./sparql.js\";\nimport { boundedInteger, containsAllWords, iriOf, literal } from \"./sparql.js\";\n\n/**\n * How many rows a text search reads from the index before filtering.\n *\n * `bif:contains` answers in index order, which is not an order of relevance, so\n * a window that is too small leaves out the record a caller meant. A window this\n * size costs the service one pass and gives the filter enough to work with.\n *\n * It is also the end of what a text search can page through. An offset past it\n * lands beyond what was ever read, and the answer would be an empty page that\n * looks exactly like the catalogue holding nothing. The tools refuse such a page\n * rather than buying the query, which is why this is exported.\n */\nexport const TEXT_WINDOW = 400;\n\nconst prefixed = (body: string): string => `${PREFIX_BLOCK}\\n${body.trim()}\\n`;\n\n/**\n * People whose name carries every word given.\n *\n * The name is searched rather than the authority heading, because a heading\n * carries the dates and a caller writing \"Arthur Rimbaud 1854\" would otherwise\n * match on the year. Both are returned, so an answer can show which of several\n * people bearing one name is which.\n *\n * `limit + 1` rows are asked for. The extra row is never shown; it is how the\n * answer can say that more exist without a second query counting them.\n */\nexport function searchAuthorsQuery(\n words: readonly string[],\n limit: number,\n offset: number,\n): string {\n const take = boundedInteger(limit, 1, 50, \"limit\") + 1;\n const skip = boundedInteger(offset, 0, 5000, \"offset\");\n\n // The page is cut in the middle subquery, over people rather than over rows,\n // and the cutting projection names the person alone. Selecting the name\n // alongside would give a person recorded under two names two of the page's\n // slots; the parser then folds them back into one row, and a page of ten\n // comes back holding eight.\n return prefixed(`\nSELECT ?person ?name ?label ?birthYear ?deathYear ?role WHERE {\n {\n SELECT DISTINCT ?person WHERE {\n {\n SELECT DISTINCT ?person ?name WHERE {\n ?person foaf:name ?name .\n ?name bif:contains ${containsAllWords(words)} .\n }\n LIMIT ${TEXT_WINDOW}\n }\n ?person a foaf:Person .\n }\n ORDER BY ?person\n LIMIT ${take} OFFSET ${skip}\n }\n OPTIONAL { ?person foaf:name ?name }\n OPTIONAL { ?record foaf:focus ?person ; skos:prefLabel ?label }\n OPTIONAL { ?person bnf:firstYear ?birthYear }\n OPTIONAL { ?person bnf:lastYear ?deathYear }\n OPTIONAL { ?person rdag2:biographicalInformation ?role }\n}`);\n}\n\n/**\n * The most triples one record's detail query will read.\n *\n * A record that hits this ceiling has been cut, and a cut record renders as a\n * record whose missing statements are absences. The parsers compare the row\n * count against it and say so rather than reporting the silence as fact.\n */\nexport const MOST_TRIPLES = 2000;\n\n/**\n * Everything both halves of one person's record carry.\n *\n * The two subjects are read in one query through a UNION rather than in two\n * requests: the spacing between requests is the cost that matters here, and one\n * person is one question.\n */\nexport function authorQuery(id: EntityId): string {\n const person = iriOf(id);\n return prefixed(`\nSELECT ?p ?o ?lang WHERE {\n {\n ${person} ?p ?o .\n BIND(LANG(?o) AS ?lang)\n }\n UNION\n {\n ?record foaf:focus ${person} ; ?p ?o .\n BIND(LANG(?o) AS ?lang)\n }\n}\nLIMIT ${MOST_TRIPLES}`);\n}\n\n/**\n * Works whose title carries every word given.\n *\n * Both established works and provisional ones are returned, and the row says\n * which is which: leaving the provisional ones out would hide the only record\n * the BnF holds for a great many recent books, and mixing them in silently\n * would let a provisional record be quoted as an established one.\n */\nexport function searchWorksQuery(words: readonly string[], limit: number, offset: number): string {\n const take = boundedInteger(limit, 1, 50, \"limit\") + 1;\n const skip = boundedInteger(offset, 0, 5000, \"offset\");\n\n // As with people, the page is cut over works rather than over rows, on the\n // work alone: a work with three authors occupies three rows, and a work\n // recorded under two titles would take two of the page's slots.\n return prefixed(`\nSELECT ?work ?title ?date ?status ?creator ?creatorName WHERE {\n {\n SELECT DISTINCT ?work WHERE {\n {\n SELECT DISTINCT ?work ?title WHERE {\n ?work dcterms:title ?title .\n ?title bif:contains ${containsAllWords(words)} .\n }\n LIMIT ${TEXT_WINDOW}\n }\n ?work a rdafrbr:Work .\n }\n ORDER BY ?work\n LIMIT ${take} OFFSET ${skip}\n }\n OPTIONAL { ?work dcterms:title ?title }\n OPTIONAL { ?work dcterms:date ?date }\n OPTIONAL { ?work rdae:statusOfIdentification ?status }\n OPTIONAL {\n ?work dcterms:creator ?creator .\n OPTIONAL { ?creator foaf:name ?creatorName }\n }\n}`);\n}\n\n/** Everything one work's record carries, with the names of its creators. */\nexport function workQuery(id: EntityId): string {\n const work = iriOf(id);\n return prefixed(`\nSELECT ?p ?o ?lang ?name WHERE {\n ${work} ?p ?o .\n BIND(LANG(?o) AS ?lang)\n OPTIONAL { ?o foaf:name ?name }\n}\nLIMIT ${MOST_TRIPLES}`);\n}\n\n/**\n * The published editions of one work.\n *\n * A manifestation naming the same work twice, or carrying two digitised copies,\n * comes back as several rows describing one edition. The parser groups them,\n * which is why the manifestation's own address is selected first.\n */\nexport function editionsQuery(id: EntityId, limit: number, offset: number): string {\n const work = iriOf(id);\n const take = boundedInteger(limit, 1, 50, \"limit\") + 1;\n const skip = boundedInteger(offset, 0, 5000, \"offset\");\n\n return prefixed(`\nSELECT ?edition ?title ?date ?year ?publisher ?place ?editionStatement\n ?extent ?isbn ?note ?catalogue ?reproduction ?ocr WHERE {\n {\n SELECT DISTINCT ?edition WHERE {\n ?edition rdarel:workManifested ${work} .\n }\n ORDER BY ?edition\n LIMIT ${take} OFFSET ${skip}\n }\n OPTIONAL { ?edition dcterms:title ?title }\n OPTIONAL { ?edition dcterms:date ?date }\n OPTIONAL { ?edition bnf:firstYear ?year }\n OPTIONAL { ?edition rdae:publishersName ?publisher }\n OPTIONAL { ?edition rdae:placeOfPublication ?place }\n OPTIONAL { ?edition rdae:editionStatement ?editionStatement }\n OPTIONAL { ?edition dcterms:description ?extent }\n OPTIONAL { ?edition bnf:isbn ?isbn }\n OPTIONAL { ?edition rdae:note ?note }\n OPTIONAL { ?edition rdfs:seeAlso ?catalogue }\n OPTIONAL { ?edition rdarel:electronicReproduction ?reproduction }\n OPTIONAL { ?edition bnf:OCR ?ocr }\n}`);\n}\n\n/**\n * Every digitised document one work's editions point at.\n *\n * Held apart from the edition listing because the two answer different\n * questions: one asks what was published, the other asks what can be opened.\n */\nexport function digitisedForWorkQuery(id: EntityId, limit: number): string {\n const work = iriOf(id);\n const take = boundedInteger(limit, 1, 200, \"limit\") + 1;\n\n return prefixed(`\nSELECT DISTINCT ?rank ?edition ?title ?reproduction ?ocr ?depiction WHERE {\n {\n ?edition rdarel:workManifested ${work} .\n OPTIONAL { ?edition dcterms:title ?title }\n { ?edition rdarel:electronicReproduction ?reproduction }\n UNION\n { ?edition bnf:OCR ?ocr }\n BIND(0 AS ?rank)\n }\n UNION\n {\n ${work} foaf:depiction ?depiction .\n BIND(${work} AS ?edition)\n BIND(1 AS ?rank)\n }\n}\nORDER BY ?rank\nLIMIT ${take}`);\n}\n\n/**\n * Every digitised document attached to one person.\n *\n * Two paths lead there and they mean different things. `foaf:depiction` hangs\n * off the person and illustrates them. A reproduction hangs off an edition of a\n * work they are credited with, and is that edition digitised. The parser keeps\n * the two apart, and the answer says which is which.\n *\n * Reproductions are ordered first because a well-known person carries dozens of\n * illustrations, and a page filled with those would answer \"what of this author\n * can be read\" with a list of pictures of him.\n */\nexport function digitisedForPersonQuery(id: EntityId, limit: number): string {\n const person = iriOf(id);\n const take = boundedInteger(limit, 1, 200, \"limit\") + 1;\n\n return prefixed(`\nSELECT DISTINCT ?rank ?edition ?title ?reproduction ?ocr ?depiction WHERE {\n {\n ?work dcterms:creator ${person} .\n ?edition rdarel:workManifested ?work .\n OPTIONAL { ?edition dcterms:title ?title }\n { ?edition rdarel:electronicReproduction ?reproduction }\n UNION\n { ?edition bnf:OCR ?ocr }\n BIND(0 AS ?rank)\n }\n UNION\n {\n ${person} foaf:depiction ?depiction .\n BIND(${person} AS ?edition)\n BIND(1 AS ?rank)\n }\n}\nORDER BY ?rank\nLIMIT ${take}`);\n}\n\n/**\n * Whether one address is described at all, and what it is.\n *\n * Asked when a lookup came back with nothing, so an answer can tell \"the BnF\n * describes no such record\" from \"that record exists and is not a person\".\n */\nexport function kindQuery(id: EntityId): string {\n return prefixed(`\nSELECT DISTINCT ?type WHERE {\n ${iriOf(id)} a ?type .\n}\nLIMIT 20`);\n}\n\n/**\n * A query written for the live suite alone, naming one record by its label.\n *\n * It exists so the canary exercises `literal` against text a person typed,\n * rather than only against words the tokeniser produced.\n */\nexport function labelProbeQuery(label: string): string {\n return prefixed(`\nSELECT ?person WHERE {\n ?person foaf:name ${literal(label)} .\n}\nLIMIT 3`);\n}\n","/**\n * Turning result rows into the shapes the tools render.\n *\n * The rule the whole file follows: a statement the record does not carry comes\n * out as `null` or as an empty list, never as a plausible value. A person with\n * no recorded year of death is alive, or unresearched, or died in a year nobody\n * wrote down, and the three are different claims. Only the record decides.\n */\n\nimport { isGallicaAddress, publicPageFor } from \"./endpoint.js\";\nimport { MOST_TRIPLES } from \"./queries.js\";\nimport type { SparqlResults, SparqlRow, SparqlTerm } from \"./http.js\";\nimport type {\n AuthorDetail,\n AuthorSummary,\n DigitisedLink,\n Edition,\n Page,\n WorkDetail,\n WorkSummary,\n} from \"../types.js\";\n\n/** Predicates read off a record. Named once, so a rename is one edit. */\nconst P = {\n type: \"http://www.w3.org/1999/02/22-rdf-syntax-ns#type\",\n label: \"http://www.w3.org/2000/01/rdf-schema#label\",\n seeAlso: \"http://www.w3.org/2000/01/rdf-schema#seeAlso\",\n sameAs: \"http://www.w3.org/2002/07/owl#sameAs\",\n exactMatch: \"http://www.w3.org/2004/02/skos/core#exactMatch\",\n closeMatch: \"http://www.w3.org/2004/02/skos/core#closeMatch\",\n prefLabel: \"http://www.w3.org/2004/02/skos/core#prefLabel\",\n altLabel: \"http://www.w3.org/2004/02/skos/core#altLabel\",\n created: \"http://purl.org/dc/terms/created\",\n modified: \"http://purl.org/dc/terms/modified\",\n title: \"http://purl.org/dc/terms/title\",\n date: \"http://purl.org/dc/terms/date\",\n creator: \"http://purl.org/dc/terms/creator\",\n language: \"http://purl.org/dc/terms/language\",\n subject: \"http://purl.org/dc/terms/subject\",\n name: \"http://xmlns.com/foaf/0.1/name\",\n givenName: \"http://xmlns.com/foaf/0.1/givenName\",\n familyName: \"http://xmlns.com/foaf/0.1/familyName\",\n depiction: \"http://xmlns.com/foaf/0.1/depiction\",\n birth: \"http://vocab.org/bio/0.1/birth\",\n death: \"http://vocab.org/bio/0.1/death\",\n firstYear: \"http://data.bnf.fr/ontology/bnf-onto/firstYear\",\n lastYear: \"http://data.bnf.fr/ontology/bnf-onto/lastYear\",\n subjectLabel: \"http://data.bnf.fr/ontology/bnf-onto/subject\",\n isni: \"http://isni.org/ontology#identifierValid\",\n placeOfBirth: \"http://rdvocab.info/ElementsGr2/placeOfBirth\",\n placeOfDeath: \"http://rdvocab.info/ElementsGr2/placeOfDeath\",\n biographicalInformation: \"http://rdvocab.info/ElementsGr2/biographicalInformation\",\n languageOfThePerson: \"http://rdvocab.info/ElementsGr2/languageOfThePerson\",\n countryAssociated: \"http://rdvocab.info/ElementsGr2/countryAssociatedWithThePerson\",\n fieldOfActivity: \"http://rdvocab.info/ElementsGr2/fieldOfActivityOfThePerson\",\n formOfWork: \"http://rdvocab.info/Elements/formOfWork\",\n statusOfIdentification: \"http://rdvocab.info/Elements/statusOfIdentification\",\n /** The occupation, which RDA numbers rather than names. */\n occupation: \"http://rdaregistry.info/Elements/a/#P50113\",\n /** Expressions of a work, which are not editions. */\n expressionOfWork: \"http://rdaregistry.info/Elements/w/#P10078\",\n} as const;\n\n/** Files whose addresses this dataset aligns records with. */\nconst ALIGNMENT_HOSTS: Array<{ host: string; name: string }> = [\n { host: \"viaf.org\", name: \"viaf\" },\n { host: \"idref.fr\", name: \"idref\" },\n { host: \"dbpedia.org\", name: \"dbpedia\" },\n { host: \"wikidata.org\", name: \"wikidata\" },\n { host: \"isni.org\", name: \"isni\" },\n { host: \"d-nb.info\", name: \"gnd\" },\n { host: \"id.loc.gov\", name: \"loc\" },\n { host: \"datos.bne.es\", name: \"bne\" },\n { host: \"wikipedia.org\", name: \"wikipedia\" },\n { host: \"musicbrainz.org\", name: \"musicbrainz\" },\n { host: \"francearchives.gouv.fr\", name: \"francearchives\" },\n { host: \"imslp.org\", name: \"imslp\" },\n { host: \"catalogue.bnf.fr\", name: \"catalogue\" },\n];\n\nconst text = (term: SparqlTerm | undefined): string | null => {\n if (!term) return null;\n const value = term.value.trim();\n return value === \"\" ? null : value;\n};\n\nconst integer = (term: SparqlTerm | undefined): number | null => {\n const raw = text(term);\n if (raw === null) return null;\n const value = Number(raw);\n return Number.isInteger(value) ? value : null;\n};\n\n/** The identifier a caller quotes, read back off the address of a record. */\nexport function idFromIri(iri: string): string | null {\n const ark = /ark:\\/12148\\/(c[bc][0-9a-z]+)/i.exec(iri);\n if (ark?.[1]) return ark[1].toLowerCase();\n const temp = /temp-work\\/([0-9a-f]{32})/i.exec(iri);\n if (temp?.[1]) return `temp-work/${temp[1].toLowerCase()}`;\n return null;\n}\n\n/** The last segment of a vocabulary address, which is the term itself. */\nconst vocabularyTerm = (iri: string): string =>\n decodeURIComponent(iri.replace(/[/#]$/, \"\").split(/[/#]/).pop() ?? iri);\n\n/**\n * A Gallica address turned into a link.\n *\n * Returns null for anything that is not a Gallica address, so a record pointing\n * somewhere else does not arrive labelled as a digitised BnF document. The\n * address is carried through as published: it is what a person will open.\n */\nexport function toDigitisedLink(\n address: string | null,\n role: DigitisedLink[\"role\"],\n fromId: string,\n fromTitle: string | null,\n): DigitisedLink | null {\n if (!address || !isGallicaAddress(address)) return null;\n const segment = /ark:\\/12148\\/([^/?#]+)/.exec(address)?.[1] ?? null;\n if (!segment) return null;\n // An address can carry a rendering after the name: `.thumbnail` asks for a\n // small image of the document, `.item` for one leaf of it. An ARK name holds\n // no full stop, so what precedes the first one is the identifier and the rest\n // describes a view of it. Reporting the view as the identifier would give two\n // views of one document two identifiers.\n const ark = segment.split(\".\")[0] ?? segment;\n if (ark === \"\") return null;\n return { ark, url: address, role, fromId, fromTitle };\n}\n\n/**\n * A page, with the rows beyond it read as more existing.\n *\n * `hasMore` is true when the endpoint sent the extra row that was asked for, and\n * also when a row had to be dropped: a page that came back short because this\n * client could not read part of it is not a page with room to spare.\n */\nfunction page<T>(rows: T[], limit: number, skipped: number): Page<T> {\n const more = rows.length > limit;\n return skipped > 0\n ? { rows: rows.slice(0, limit), hasMore: more, skipped }\n : { rows: rows.slice(0, limit), hasMore: more };\n}\n\n/** Index a `?p ?o` result set by predicate, keeping every value. */\nexport function byPredicate(results: SparqlResults): Map<string, SparqlRow[]> {\n const index = new Map<string, SparqlRow[]>();\n for (const row of results.results.bindings) {\n const predicate = text(row.p);\n if (predicate === null) continue;\n const bucket = index.get(predicate);\n if (bucket) bucket.push(row);\n else index.set(predicate, [row]);\n }\n return index;\n}\n\nconst firstValue = (index: Map<string, SparqlRow[]>, predicate: string): string | null =>\n text(index.get(predicate)?.[0]?.o);\n\nconst allValues = (index: Map<string, SparqlRow[]>, predicate: string): string[] => {\n const seen = new Set<string>();\n for (const row of index.get(predicate) ?? []) {\n const value = text(row.o);\n if (value !== null) seen.add(value);\n }\n return [...seen];\n};\n\n/** Values that are addresses of vocabulary terms, reduced to the term. */\nconst allTerms = (index: Map<string, SparqlRow[]>, predicate: string): string[] =>\n allValues(index, predicate)\n .filter((value) => value.startsWith(\"http\"))\n .map(vocabularyTerm);\n\n/**\n * Dewey classes, and nothing else that happens to share the predicate.\n *\n * `dcterms:subject` on a work points at a Dewey class and at subject-heading\n * ARKs alike. Reducing an ARK to its last segment yields the string `about`,\n * which would then be published as a Dewey class and printed beside a real one.\n */\nconst deweyClasses = (index: Map<string, SparqlRow[]>, predicate: string): string[] =>\n allValues(index, predicate)\n .filter((value) => value.includes(\"dewey.info/class/\"))\n .map(vocabularyTerm);\n\n/** Values that are plain words rather than addresses. */\nconst allWords = (index: Map<string, SparqlRow[]>, predicate: string): string[] =>\n allValues(index, predicate).filter((value) => !value.startsWith(\"http\"));\n\n/**\n * Alignments to the same thing in other files, grouped by file.\n *\n * `owl:sameAs`, `skos:exactMatch` and `skos:closeMatch` are gathered together\n * because a caller wanting the VIAF address does not care which of the three\n * carried it, and the dataset uses all three for the same alignments. A\n * `closeMatch` is a weaker claim than the other two, and the record makes that\n * distinction nowhere a caller could act on it, so the grouping does not\n * pretend otherwise: the field is named for the addresses it holds.\n */\nfunction alignments(\n index: Map<string, SparqlRow[]>,\n selfId: string | null,\n): Record<string, string[]> {\n const found: Record<string, Set<string>> = {};\n for (const predicate of [P.sameAs, P.exactMatch, P.closeMatch, P.seeAlso]) {\n for (const address of allValues(index, predicate)) {\n if (!address.startsWith(\"http\")) continue;\n // An address pointing back at this record says nothing about anywhere\n // else, and the dataset carries several of those per record.\n if (selfId && address.includes(selfId)) continue;\n let host: string;\n try {\n host = new URL(address).hostname.toLowerCase();\n } catch {\n continue;\n }\n const known = ALIGNMENT_HOSTS.find(\n (entry) => host === entry.host || host.endsWith(`.${entry.host}`),\n );\n if (!known) continue;\n (found[known.name] ??= new Set()).add(address);\n }\n }\n return Object.fromEntries(Object.entries(found).map(([name, set]) => [name, [...set]]));\n}\n\n/** ISNI is written as a bare number rather than an address. */\nfunction isniAddresses(index: Map<string, SparqlRow[]>): string[] {\n return allValues(index, P.isni).map(\n (value) => `https://isni.org/isni/${value.replace(/\\s/g, \"\")}`,\n );\n}\n\n/** Rows of an author search, with the extra row read as \"more exist\". */\nexport function toAuthorSummaries(results: SparqlResults, limit: number): Page<AuthorSummary> {\n const rows: AuthorSummary[] = [];\n const seen = new Set<string>();\n let skipped = 0;\n\n for (const row of results.results.bindings) {\n const iri = text(row.person);\n const id = iri === null ? null : idFromIri(iri);\n if (iri === null || id === null) {\n // A row naming no record, or naming one at an address this client cannot\n // read, is a row that was dropped. Counting it is what keeps a short page\n // from reading as a small catalogue.\n skipped += 1;\n continue;\n }\n if (seen.has(id)) continue;\n seen.add(id);\n rows.push({\n id,\n name: text(row.name),\n label: text(row.label),\n birthYear: integer(row.birthYear),\n deathYear: integer(row.deathYear),\n role: text(row.role),\n sourceUrl: publicPageFor(iri),\n });\n }\n\n return page(rows, limit, skipped);\n}\n\n/** One person, assembled from both halves of their record. */\nexport function toAuthorDetail(results: SparqlResults, id: string, pageUrl: string): AuthorDetail {\n const index = byPredicate(results);\n const types = allValues(index, P.type);\n\n const otherNames: Array<{ label: string; language: string | null }> = [];\n const seenLabels = new Set<string>();\n for (const row of index.get(P.altLabel) ?? []) {\n const label = text(row.o);\n if (label === null || seenLabels.has(label)) continue;\n seenLabels.add(label);\n otherNames.push({ label, language: text(row.lang) });\n }\n\n const sameAs = alignments(index, id);\n const isni = isniAddresses(index);\n if (isni.length > 0) sameAs.isni = [...new Set([...(sameAs.isni ?? []), ...isni])];\n\n const catalogueUrl =\n allValues(index, P.seeAlso).find((address) => address.includes(\"catalogue.bnf.fr\")) ?? null;\n\n const depictions: DigitisedLink[] = [];\n for (const address of allValues(index, P.depiction)) {\n const link = toDigitisedLink(address, \"depiction\", id, null);\n if (link) depictions.push(link);\n }\n\n return {\n id,\n name: firstValue(index, P.name),\n label: firstValue(index, P.prefLabel),\n givenName: firstValue(index, P.givenName),\n familyName: firstValue(index, P.familyName),\n otherNames,\n birthDate: firstValue(index, P.birth),\n deathDate: firstValue(index, P.death),\n birthYear: integer(index.get(P.firstYear)?.[0]?.o),\n deathYear: integer(index.get(P.lastYear)?.[0]?.o),\n birthPlace: firstValue(index, P.placeOfBirth),\n deathPlace: firstValue(index, P.placeOfDeath),\n biographicalInformation: firstValue(index, P.biographicalInformation),\n occupation: firstValue(index, P.occupation),\n languages: allTerms(index, P.languageOfThePerson),\n countries: allTerms(index, P.countryAssociated),\n fields: allWords(index, P.fieldOfActivity),\n deweyClasses: deweyClasses(index, P.fieldOfActivity),\n sameAs,\n catalogueUrl,\n recordCreated: firstValue(index, P.created),\n recordModified: firstValue(index, P.modified),\n depictions,\n types,\n truncated: results.results.bindings.length >= MOST_TRIPLES,\n sourceUrl: pageUrl,\n };\n}\n\n/**\n * How settled a record is.\n *\n * The record states it in words, and the address states it in its shape: a\n * provisional work is addressed under `temp-work` rather than by an ARK. The\n * statement governs when there is one, since a cataloguer can settle a record\n * before it is re-addressed.\n */\nexport function readStatus(statement: string | null, iri: string): \"established\" | \"provisional\" {\n if (statement !== null) {\n const said = statement.toLowerCase();\n if (said.includes(\"provisional\")) return \"provisional\";\n if (said.includes(\"established\")) return \"established\";\n }\n return iri.includes(\"/temp-work/\") ? \"provisional\" : \"established\";\n}\n\n/** Rows of a work search, gathering the creators of each work. */\nexport function toWorkSummaries(results: SparqlResults, limit: number): Page<WorkSummary> {\n const order: string[] = [];\n const byId = new Map<string, WorkSummary>();\n\n let skipped = 0;\n for (const row of results.results.bindings) {\n const iri = text(row.work);\n const id = iri === null ? null : idFromIri(iri);\n if (iri === null || id === null) {\n // A row naming no record, or naming one at an address this client cannot\n // read, is a row that was dropped. Counting it is what keeps a short page\n // from reading as a small catalogue.\n skipped += 1;\n continue;\n }\n\n let work = byId.get(id);\n if (!work) {\n work = {\n id,\n title: text(row.title),\n date: text(row.date),\n creators: [],\n status: readStatus(text(row.status), iri),\n sourceUrl: publicPageFor(iri),\n };\n byId.set(id, work);\n order.push(id);\n }\n\n const creatorIri = text(row.creator);\n const creatorId = creatorIri ? idFromIri(creatorIri) : null;\n if (creatorId && !work.creators.some((c) => c.id === creatorId)) {\n work.creators.push({ id: creatorId, name: text(row.creatorName) });\n }\n }\n\n return page(\n order.map((id) => byId.get(id)!),\n limit,\n skipped,\n );\n}\n\n/** One work, assembled from its record. */\nexport function toWorkDetail(\n results: SparqlResults,\n id: string,\n iri: string,\n pageUrl: string,\n): WorkDetail {\n const index = byPredicate(results);\n const types = allValues(index, P.type);\n const truncated = results.results.bindings.length >= MOST_TRIPLES;\n\n const creators: Array<{ id: string; name: string | null }> = [];\n for (const row of index.get(P.creator) ?? []) {\n const creatorIri = text(row.o);\n const creatorId = creatorIri ? idFromIri(creatorIri) : null;\n if (!creatorId || creators.some((c) => c.id === creatorId)) continue;\n creators.push({ id: creatorId, name: text(row.name) });\n }\n\n const depictions: DigitisedLink[] = [];\n for (const address of allValues(index, P.depiction)) {\n const link = toDigitisedLink(address, \"depiction\", id, null);\n if (link) depictions.push(link);\n }\n\n const statusStatement = firstValue(index, P.statusOfIdentification);\n const catalogueUrl =\n allValues(index, P.seeAlso).find((address) => address.includes(\"catalogue.bnf.fr\")) ?? null;\n\n return {\n id,\n title: firstValue(index, P.title),\n label: firstValue(index, P.label),\n date: firstValue(index, P.date),\n firstYear: integer(index.get(P.firstYear)?.[0]?.o),\n creators,\n languages: allTerms(index, P.language),\n forms: allTerms(index, P.formOfWork),\n subjects: allWords(index, P.subjectLabel),\n deweyClasses: deweyClasses(index, P.subject),\n status: readStatus(statusStatement, iri),\n statusStatement,\n // A cut record makes this the ceiling rather than the count, so it is\n // withheld rather than published as a number the record does not support.\n expressionCount: truncated ? null : allValues(index, P.expressionOfWork).length,\n sameAs: alignments(index, id),\n catalogueUrl,\n depictions,\n types,\n truncated,\n sourceUrl: pageUrl,\n };\n}\n\n/**\n * Editions, gathered from rows that repeat.\n *\n * One manifestation carrying two digitised copies arrives as two rows differing\n * in one column. Counting rows would report two editions where the record\n * describes one, so the address of the manifestation is what an edition is\n * counted by.\n */\nexport function toEditions(results: SparqlResults, limit: number): Page<Edition> {\n const order: string[] = [];\n const byId = new Map<string, Edition>();\n\n let skipped = 0;\n for (const row of results.results.bindings) {\n const iri = text(row.edition);\n const id = iri === null ? null : idFromIri(iri);\n if (iri === null || id === null) {\n // A row naming no record, or naming one at an address this client cannot\n // read, is a row that was dropped. Counting it is what keeps a short page\n // from reading as a small catalogue.\n skipped += 1;\n continue;\n }\n\n let edition = byId.get(id);\n if (!edition) {\n edition = {\n id,\n title: text(row.title),\n date: text(row.date),\n year: integer(row.year),\n publisher: text(row.publisher),\n place: text(row.place),\n editionStatement: text(row.editionStatement),\n extent: text(row.extent),\n isbn: text(row.isbn),\n note: text(row.note),\n catalogueUrl: text(row.catalogue),\n digitised: [],\n sourceUrl: publicPageFor(iri),\n };\n byId.set(id, edition);\n order.push(id);\n }\n\n for (const [column, role] of [\n [\"reproduction\", \"reproduction\"],\n [\"ocr\", \"ocr\"],\n ] as const) {\n const link = toDigitisedLink(text(row[column]), role, id, edition.title);\n if (link && !edition.digitised.some((existing) => existing.url === link.url)) {\n edition.digitised.push(link);\n }\n }\n }\n\n return page(\n order.map((id) => byId.get(id)!),\n limit,\n skipped,\n );\n}\n\n/**\n * Every digitised document a set of rows points at, kept in one list.\n *\n * One document can be reached by several paths, so the same address arrives\n * more than once and is kept once. That makes the length of the list a poor\n * signal of whether more exist: twenty-one rows holding seventeen addresses\n * would otherwise be reported as a page with room to spare. The endpoint was\n * asked for one row beyond the page, so the count of rows is what answers that.\n */\nexport function toDigitisedLinks(results: SparqlResults, limit: number): Page<DigitisedLink> {\n const links: DigitisedLink[] = [];\n const seen = new Set<string>();\n const rowCount = results.results.bindings.length;\n\n for (const row of results.results.bindings) {\n const fromIri = text(row.edition);\n const fromId = fromIri ? (idFromIri(fromIri) ?? fromIri) : \"\";\n const title = text(row.title);\n\n for (const [column, role] of [\n [\"reproduction\", \"reproduction\"],\n [\"ocr\", \"ocr\"],\n [\"depiction\", \"depiction\"],\n ] as const) {\n const link = toDigitisedLink(text(row[column]), role, fromId, title);\n if (!link || seen.has(link.url)) continue;\n seen.add(link.url);\n links.push(link);\n }\n }\n\n // `rowCount` counts what the endpoint sent, including rows whose address was\n // not a Gallica one and rows naming a document already listed. Raising the\n // limit after such a page returns the same list, so the claim is confined to\n // the case where the links themselves fill the page.\n return { rows: links.slice(0, limit), hasMore: links.length > limit || rowCount > limit };\n}\n\n/** The classes an address is typed with, reduced to their vocabulary terms. */\nexport function toTypes(results: SparqlResults): string[] {\n const types = new Set<string>();\n for (const row of results.results.bindings) {\n const iri = text(row.type);\n if (iri !== null) types.add(iri);\n }\n return [...types];\n}\n","/**\n * Keeps this client to one request at a time, spaced out.\n *\n * data.bnf.fr is a query service a public institution runs at its own cost, and\n * a SPARQL endpoint answers an expensive question per request rather than\n * serving a file. Serialising also means a burst of tool calls cannot turn into\n * a burst of concurrent queries on one planner.\n *\n * The spacing widens when the service 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 service 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 service 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 data.bnf.fr.\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 runs a query, parses it and only then stores: an answer nobody\n * could parse must not be served back for the rest of the cache's lifetime.\n *\n * Every read also carries the moment the metadata came off the endpoint. The\n * licence asks for the date of retrieval alongside the source, so that date is\n * a value the layer produces rather than something a caller is trusted to add.\n */\n\nimport type { Config, Logger } from \"../config.js\";\nimport {\n MAX_ALLOWED_INTERVAL_MS,\n MIN_ALLOWED_INTERVAL_MS,\n createLogger,\n loadConfig,\n} from \"../config.js\";\nimport { notFound } from \"../errors.js\";\nimport type {\n AuthorDetail,\n AuthorSummary,\n DigitisedLink,\n Edition,\n Page,\n WorkDetail,\n WorkSummary,\n} from \"../types.js\";\nimport { REPO_URL } from \"../version.js\";\nimport { Cache } from \"./cache.js\";\nimport { SPARQL_ENDPOINT } from \"./endpoint.js\";\nimport type { SparqlResults } from \"./http.js\";\nimport { runQuery } from \"./http.js\";\nimport {\n authorQuery,\n digitisedForPersonQuery,\n digitisedForWorkQuery,\n editionsQuery,\n kindQuery,\n searchAuthorsQuery,\n searchWorksQuery,\n workQuery,\n} from \"./queries.js\";\nimport {\n toAuthorDetail,\n toAuthorSummaries,\n toDigitisedLinks,\n toEditions,\n toTypes,\n toWorkDetail,\n toWorkSummaries,\n} from \"./parse.js\";\nimport { RateLimiter } from \"./rateLimiter.js\";\nimport type { EntityId } from \"./sparql.js\";\nimport { parseEntityId, toSearchWords } from \"./sparql.js\";\n\n/** The class data.bnf.fr types a person with. */\nconst PERSON_CLASS = \"http://xmlns.com/foaf/0.1/Person\";\n\n/**\n * The classes data.bnf.fr types a work with.\n *\n * Two vocabularies say the same thing here, and a record carries both. Either\n * one is enough, because a record carrying one has always carried the other.\n */\nconst WORK_CLASSES = new Set([\n \"http://rdvocab.info/uri/schema/FRBRentitiesRDA/Work\",\n \"http://rdaregistry.info/Elements/c/#C10001\",\n]);\n\nexport interface ClientOptions {\n config?: Partial<Config>;\n logger?: Logger;\n fetchImpl?: typeof fetch;\n}\n\n/** Every read reports where the value came from and when it was retrieved. */\nexport interface Read<T> {\n data: T;\n cached: boolean;\n /**\n * When the metadata came off data.bnf.fr, as an ISO 8601 instant.\n *\n * A cached answer keeps the moment it was first read. Reporting the moment it\n * was served would state a date of retrieval that never happened, which is\n * exactly the claim the licence asks to be accurate.\n */\n retrievedAt: string;\n /** Rows the endpoint sent that could not be read, which paging still counts. */\n skipped?: number;\n}\n\n/**\n * What this server owes the BnF, applied to whatever it is 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. Every bound the environment path\n * enforces is enforced here too: the published `./client` entry point is the\n * one a program uses, and it would be a strange guarantee that held only for\n * the executable.\n *\n * Three of these bounds are about what one call can cost the service. A retry\n * budget of two hundred turns a single lookup into two hundred requests against\n * an endpoint that is already failing. A deadline of an hour holds the one\n * request slot for an hour. A cache of zero entries evicts each value as it is\n * written, which switches off the caching that the three-second floor is set\n * against.\n */\nfunction withGuarantees(config: Config): Config {\n const defaults = loadConfig({});\n\n /**\n * A setting that is absent or unreadable falls back, and one outside its\n * range is brought inside it. Refusing outright is what the environment path\n * does, because a person reads stderr; a program handed an object does not.\n */\n const bounded = (value: unknown, fallback: number, min: number, max: number): number => {\n const parsed = Number(value);\n if (!Number.isFinite(parsed)) return fallback;\n return Math.min(max, Math.max(min, parsed));\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 // BnF can always tell which software is calling, and reach someone.\n userAgent:\n claimed === \"\" || claimed.includes(REPO_URL) ? identifier : `${claimed} ${identifier}`,\n minIntervalMs: bounded(\n config.minIntervalMs,\n defaults.minIntervalMs,\n MIN_ALLOWED_INTERVAL_MS,\n MAX_ALLOWED_INTERVAL_MS,\n ),\n timeoutMs: bounded(config.timeoutMs, defaults.timeoutMs, 1000, 300_000),\n maxRetries: bounded(config.maxRetries, defaults.maxRetries, 0, 8),\n cacheTtlMs: bounded(config.cacheTtlMs, defaults.cacheTtlMs, 0, 86_400_000),\n cacheMaxEntries: bounded(config.cacheMaxEntries, defaults.cacheMaxEntries, 1, 5000),\n };\n}\n\nexport class BnfClient {\n private readonly config: Config;\n private readonly logger: Logger;\n private readonly limiter: RateLimiter;\n private readonly cache: Cache<unknown>;\n /** Questions already on the wire, so an identical one waits instead of asking. */\n private readonly inFlight = new Map<string, Promise<Read<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 endpoint pushes back. */\n get intervalMs(): number {\n return this.limiter.currentIntervalMs;\n }\n\n /** What the BnF sees this client call itself. */\n get userAgent(): string {\n return this.config.userAgent;\n }\n\n /** The deadline one query is given, which callers report rather than guess. */\n get timeoutMs(): number {\n return this.config.timeoutMs;\n }\n\n /** The one address this client sends anything to. */\n get endpoint(): string {\n return SPARQL_ENDPOINT;\n }\n\n /**\n * `cacheKey` is stated rather than taken from the query, because two reads of\n * one query can produce different values: a list cut to five rows is not the\n * same value as the same list cut to twenty, and serving one for the other\n * would answer a size the caller never asked for.\n */\n private read<T>(\n cacheKey: string,\n query: string,\n parse: (results: SparqlResults) => T,\n ): Promise<Read<T>> {\n const hit = this.cache.get(cacheKey) as { value: T; retrievedAt: number } | undefined;\n if (hit !== undefined) {\n this.logger.debug(`cache hit ${cacheKey}`);\n return Promise.resolve({\n data: hit.value,\n cached: true,\n retrievedAt: new Date(hit.retrievedAt).toISOString(),\n });\n }\n\n // Two callers asking one question at the same moment both miss the cache,\n // because the first has not answered yet. Sharing the flight means the BnF\n // is asked once, which is the point of the cache in the first place.\n const flying = this.inFlight.get(cacheKey);\n if (flying !== undefined) {\n this.logger.debug(`joining a request already in flight for ${cacheKey}`);\n return flying as Promise<Read<T>>;\n }\n\n const flight = this.fetchAndParse(cacheKey, query, parse);\n this.inFlight.set(cacheKey, flight as Promise<Read<unknown>>);\n // A failure is never remembered, so the next caller asks again rather than\n // being handed the failure of somebody else's request.\n return flight.finally(() => this.inFlight.delete(cacheKey));\n }\n\n private async fetchAndParse<T>(\n cacheKey: string,\n query: string,\n parse: (results: SparqlResults) => T,\n ): Promise<Read<T>> {\n const results = await this.limiter.schedule(() =>\n runQuery({\n query,\n userAgent: this.config.userAgent,\n timeoutMs: this.config.timeoutMs,\n maxRetries: this.config.maxRetries,\n limiter: this.limiter,\n logger: this.logger,\n ...(this.fetchImpl ? { fetchImpl: this.fetchImpl } : {}),\n }),\n );\n\n // The moment is taken once the answer is in hand, so it names when the\n // metadata was read rather than when the request left.\n const retrievedAt = Date.now();\n const data = parse(results);\n this.cache.set(cacheKey, data, retrievedAt);\n return { data, cached: false, retrievedAt: new Date(retrievedAt).toISOString() };\n }\n\n /** Read an identifier a caller wrote, refusing anything this dataset cannot address. */\n identify(input: string): EntityId {\n return parseEntityId(input);\n }\n\n /** The words a search will actually look for, which a tool reports back. */\n words(input: string): string[] {\n return toSearchWords(input);\n }\n\n searchAuthors(name: string, limit: number, offset: number): Promise<Read<Page<AuthorSummary>>> {\n const words = toSearchWords(name);\n const query = searchAuthorsQuery(words, limit, offset);\n return this.read(`authors:${words.join(\"+\")}:${limit}:${offset}`, query, (results) =>\n toAuthorSummaries(results, limit),\n );\n }\n\n async getAuthor(id: EntityId): Promise<Read<AuthorDetail>> {\n const result = await this.read(`author:${id.id}`, authorQuery(id), (results) =>\n toAuthorDetail(results, id.id, id.pageUrl),\n );\n\n // The record decides what it is, and a name is not the test: a subject\n // heading and a corporate body both carry one. Answering with a person\n // record for either would put a date of birth beside something that was\n // never born, and report its absence as a fact about a life.\n if (!result.data.types.includes(PERSON_CLASS)) {\n await this.refuse(id, result.data.types, \"a person\", \"get_work reads a work.\");\n }\n return result;\n }\n\n /**\n * Say what an address is, when it is not the kind of thing that was asked for.\n *\n * A record the catalogue does not describe at all and a record describing\n * something else are two different answers, and only one of them means the\n * caller should go looking elsewhere.\n */\n private async refuse(\n id: EntityId,\n types: readonly string[],\n wanted: string,\n hint: string,\n ): Promise<never> {\n const known =\n types.length > 0 ? types : (await this.read(`types:${id.id}`, kindQuery(id), toTypes)).data;\n\n if (known.length === 0) {\n throw notFound(`data.bnf.fr describes nothing at \"${id.id}\".`, {\n hint: \"Find the identifier with search_authors or search_works rather than writing one.\",\n url: id.pageUrl,\n });\n }\n throw notFound(\n `\"${id.id}\" is described by data.bnf.fr, and it is not ${wanted}: it is typed ${known.join(\", \")}.`,\n { hint, url: id.pageUrl },\n );\n }\n\n searchWorks(title: string, limit: number, offset: number): Promise<Read<Page<WorkSummary>>> {\n const words = toSearchWords(title);\n const query = searchWorksQuery(words, limit, offset);\n return this.read(`works:${words.join(\"+\")}:${limit}:${offset}`, query, (results) =>\n toWorkSummaries(results, limit),\n );\n }\n\n async getWork(id: EntityId): Promise<Read<WorkDetail>> {\n const result = await this.read(`work:${id.id}`, workQuery(id), (results) =>\n toWorkDetail(results, id.id, id.iri, id.pageUrl),\n );\n\n // An edition carries a title too, and its ARK names a manifestation rather\n // than a work. Accepting one here would hand back an identifier described\n // as the work's, which is the identifier a citation would then carry.\n if (!result.data.types.some((type) => WORK_CLASSES.has(type))) {\n await this.refuse(\n id,\n result.data.types,\n \"a work\",\n \"get_author reads a person, and list_editions reads the editions of a work.\",\n );\n }\n return result;\n }\n\n listEditions(id: EntityId, limit: number, offset: number): Promise<Read<Page<Edition>>> {\n const query = editionsQuery(id, limit, offset);\n return this.read(`editions:${id.id}:${limit}:${offset}`, query, (results) =>\n toEditions(results, limit),\n );\n }\n\n /** Digitised documents attached to a work and to its editions. */\n digitisedForWork(id: EntityId, limit: number): Promise<Read<Page<DigitisedLink>>> {\n return this.read(\n `digitised-work:${id.id}:${limit}`,\n digitisedForWorkQuery(id, limit),\n (results) => toDigitisedLinks(results, limit),\n );\n }\n\n /** Digitised documents attached to a person and to the editions of their works. */\n digitisedForPerson(id: EntityId, limit: number): Promise<Read<Page<DigitisedLink>>> {\n return this.read(\n `digitised-person:${id.id}:${limit}`,\n digitisedForPersonQuery(id, limit),\n (results) => toDigitisedLinks(results, limit),\n );\n }\n\n /** What an address is typed as, which tells a person from a work. */\n types(id: EntityId): Promise<Read<string[]>> {\n return this.read(`types:${id.id}`, kindQuery(id), toTypes);\n }\n}\n","/**\n * find_digitised: every digitised document the catalogue attaches to one\n * person or one work, gathered as links.\n *\n * The links point at gallica.bnf.fr, and this server never requests that host.\n * The BnF places its metadata and its digitised contents under two different\n * regimes: the catalogue described here is free to reuse with its source and\n * its date of retrieval named, while Gallica's terms make use of its contents\n * inside an artificial-intelligence project subject to a paid licence outside\n * academic research, and its server refuses automated agents outright.\n *\n * So the answer is a list of addresses and what the catalogue says about each,\n * and nothing at all about what is at the other end. An OCR link in particular\n * names a machine-read text that exists; the text stays where it is.\n */\n\nimport { z } from \"zod\";\nimport type { BnfClient } from \"../bnf/client.js\";\nimport { notFound } from \"../errors.js\";\nimport type { DigitisedLink } from \"../types.js\";\nimport { strictInput } from \"./arguments.js\";\nimport {\n GALLICA_CAVEAT,\n digitisedLinkSchema,\n ok,\n retrievedAtSchema,\n toToolError,\n} from \"./shared.js\";\nimport type { ToolResult } from \"./shared.js\";\n\nconst PERSON_TYPE = \"http://xmlns.com/foaf/0.1/Person\";\nconst WORK_TYPES = [\n \"http://rdvocab.info/uri/schema/FRBRentitiesRDA/Work\",\n \"http://rdaregistry.info/Elements/c/#C10001\",\n];\n\nexport const findDigitisedDescription = [\n \"Gather the digitised documents the Bibliothèque nationale de France catalogue attaches to one person or one work, and return them as links.\",\n \"Give the identifier of either; the tool reads what kind of record it is and follows the right path. For a work it walks the editions; for a person it takes the images on the record and the digitised editions of the works they are credited with.\",\n \"Every result is a link for someone to open. This server reads the BnF catalogue and never requests gallica.bnf.fr, so it reports nothing about what is at the other end: not whether the document opens, not what it contains, not on what terms it may be reused.\",\n \"It returns links and nothing else: no publisher, no date, no ISBN. Use list_editions when which edition a copy belongs to matters.\",\n \"A 'depiction' illustrates a record and can be a page that merely mentions the subject. A 'reproduction' is an edition digitised. An 'ocr' link names a machine-read text of a document, which this server does not fetch.\",\n].join(\" \");\n\nexport const findDigitisedInput = strictInput({\n id: z\n .string()\n .min(1)\n .max(200)\n .describe(\n \"A person or work identifier, such as 'cb119219976' or 'cb11970626n'. The argument is named 'id' rather than 'work_id' or 'author_id' because it takes either.\",\n ),\n kind: z\n .enum([\"auto\", \"person\", \"work\"])\n .default(\"auto\")\n .describe(\n \"Which kind of record the identifier names. 'auto' asks the catalogue, which costs one extra query.\",\n ),\n limit: z.number().int().min(1).max(200).default(40),\n});\n\nexport const findDigitisedOutput = z.object({\n id: z.string(),\n kind: z.enum([\"person\", \"work\"]).describe(\"What the catalogue types this record as.\"),\n links: z.array(digitisedLinkSchema),\n counts: z\n .object({\n reproduction: z.number().int(),\n ocr: z.number().int(),\n depiction: z.number().int(),\n })\n .describe(\"Links returned on this page, by role. These count links, not documents.\"),\n has_more: z.boolean(),\n retrieved_at: retrievedAtSchema,\n notes: z.array(z.string()),\n});\n\nexport type FindDigitisedArgs = z.infer<typeof findDigitisedInput>;\n\nexport async function runFindDigitised(\n client: BnfClient,\n args: FindDigitisedArgs,\n): Promise<ToolResult> {\n try {\n const id = client.identify(args.id);\n\n let kind: \"person\" | \"work\";\n const notes: string[] = [];\n\n if (args.kind === \"auto\") {\n // A provisional identifier can only be a work, so the question is settled\n // without asking.\n if (id.kind === \"temp-work\") {\n kind = \"work\";\n } else {\n const types = await client.types(id);\n if (types.data.length === 0) {\n return toToolError(\n notFound(`data.bnf.fr describes nothing at \"${id.id}\".`, {\n hint: \"Find the identifier with search_authors or search_works rather than writing one.\",\n url: id.pageUrl,\n }),\n );\n }\n // An edition, an expression and a subject heading all have identifiers\n // of the same shape. Reading one of them as a work asks a question it\n // can never answer, and the empty answer reads as the BnF having\n // digitised nothing.\n if (types.data.includes(PERSON_TYPE)) kind = \"person\";\n else if (types.data.some((type) => WORK_TYPES.includes(type))) kind = \"work\";\n else {\n return toToolError(\n notFound(\n `\"${id.id}\" is described by data.bnf.fr, and it is neither a person nor a work: it is typed ${types.data.join(\", \")}.`,\n {\n hint: \"This tool follows a person or a work. For one edition, list_editions carries the digitised copies alongside the imprint.\",\n url: id.pageUrl,\n },\n ),\n );\n }\n }\n } else {\n kind = args.kind;\n }\n\n const { data, cached, retrievedAt } =\n kind === \"person\"\n ? await client.digitisedForPerson(id, args.limit)\n : await client.digitisedForWork(id, args.limit);\n\n if (cached) notes.push(\"Served from this server's short-lived in-memory cache.\");\n\n const links = data.rows.map((link) => ({\n ark: link.ark,\n url: link.url,\n role: link.role,\n from_id: link.fromId,\n from_title: link.fromTitle,\n }));\n\n const counts = {\n reproduction: countRole(data.rows, \"reproduction\"),\n ocr: countRole(data.rows, \"ocr\"),\n depiction: countRole(data.rows, \"depiction\"),\n };\n\n if (links.length > 0) notes.push(GALLICA_CAVEAT);\n if (counts.depiction > 0) {\n notes.push(\n `${counts.depiction === 1 ? \"One of these is an image\" : `${counts.depiction} of these are images`} the catalogue uses to illustrate the record. Such an image can be a portrait, a title page, or a newspaper page that mentions the subject in passing, and the catalogue does not say which.`,\n );\n }\n if (counts.ocr > 0) {\n notes.push(\n `${counts.ocr === 1 ? \"One of these points\" : `${counts.ocr} of these point`} at a text a machine read off a scanned document. This server names that text and does not read it.`,\n );\n }\n if (data.hasMore) {\n notes.push(\n args.limit >= 200\n ? \"The catalogue points at more documents than this tool will return in one answer, and it does not page: 200 is the ceiling. Read the editions with list_editions to reach the rest, one page at a time.\"\n : `More links exist than were returned. Raise 'limit' to see more of them, up to 200. This tool does not page, so a higher limit re-reads the links already shown rather than continuing after them.`,\n );\n }\n if (links.length === 0) {\n notes.push(\n `The catalogue attaches no digitised document to \"${id.id}\". That is a statement about the catalogue: the BnF digitises a fraction of what it holds, and a document can be on Gallica without the record here pointing at it.`,\n );\n }\n\n const body =\n links.length === 0\n ? `The BnF catalogue attaches no digitised document to \"${id.id}\".`\n : `${links.length} link(s) for \"${id.id}\" (${kind}):\\n${links\n .map((link, index) => {\n const from = link.from_title ? ` · from: ${link.from_title}` : \"\";\n return `${index + 1}. [${link.role}] ${link.url}${from}`;\n })\n .join(\"\\n\")}`;\n\n return ok(\n {\n id: id.id,\n kind,\n links,\n counts,\n has_more: data.hasMore,\n retrieved_at: retrievedAt,\n notes,\n },\n body,\n { retrievedAt, notes, sourceUrl: id.pageUrl },\n );\n } catch (error) {\n return toToolError(error);\n }\n}\n\nconst countRole = (links: readonly DigitisedLink[], role: DigitisedLink[\"role\"]): number =>\n links.filter((link) => link.role === role).length;\n","/**\n * How a tool's arguments are declared, and what happens to one that is not.\n *\n * An argument this server does not declare is a question it cannot answer.\n * Reading it and dropping it produces an answer computed on the defaults, which\n * a caller reads as the answer to what they asked. So an undeclared argument is\n * refused, and the refusal names it and offers the declared name when one is\n * close enough to be the one that was meant.\n */\n\nimport { z } from \"zod\";\n\n/** The code a caller branches on when the arguments cannot produce a request. */\nconst INVALID_INPUT = \"invalid_input\";\n\n/** Declare a tool's arguments, refusing anything outside the declaration. */\nexport function strictInput<Shape extends z.ZodRawShape>(shape: Shape) {\n const declared = Object.keys(shape);\n\n return z.strictObject(shape, {\n error: (issue) =>\n issue.code === \"unrecognized_keys\" ? unknownArgumentMessage(issue.keys, declared) : undefined,\n });\n}\n\nfunction unknownArgumentMessage(keys: readonly string[], declared: readonly string[]): string {\n const named = keys\n .map((key) => {\n const near = nearestArgument(key, declared);\n return near ? `'${key}' (did you mean '${near}'?)` : `'${key}'`;\n })\n .join(\", \");\n\n return (\n `[${INVALID_INPUT}] Unknown ${keys.length > 1 ? \"arguments\" : \"argument\"} ${named}. ` +\n `This tool takes: ${declared.join(\", \")}.`\n );\n}\n\n/**\n * The declared name a caller most plausibly meant, when there is one.\n *\n * Three readings, ordered by how much each claims: the same name written\n * differently, a name that opens or closes the other, and a name a couple of\n * typing slips away. Anything further is left unnamed, because a suggestion\n * that misses sends a caller to an argument answering a different question.\n */\nfunction nearestArgument(key: string, declared: readonly string[]): string | undefined {\n const flatten = (name: string) => name.toLowerCase().replace(/[^a-z0-9]/g, \"\");\n const flat = flatten(key);\n if (flat.length === 0) return undefined;\n\n const sameName = declared.find((name) => flatten(name) === flat);\n if (sameName) return sameName;\n\n // Either name may be the longer one: a caller can qualify a name this tool\n // keeps plain, or shorten one it spells out.\n const overlapping = declared.find((name) => {\n const other = flatten(name);\n const [shorter, longer] = other.length < flat.length ? [other, flat] : [flat, other];\n // Two characters in common say nothing; three start to.\n return shorter.length >= 3 && (longer.startsWith(shorter) || longer.endsWith(shorter));\n });\n if (overlapping) return overlapping;\n\n let closest: string | undefined;\n let shortest = Number.POSITIVE_INFINITY;\n for (const name of declared) {\n const distance = editDistance(flat, flatten(name));\n if (distance < shortest) {\n shortest = distance;\n closest = name;\n }\n }\n\n // Up to a third of the name may differ. Past that the match is a guess.\n return shortest <= Math.max(1, Math.floor(flat.length / 3)) ? closest : undefined;\n}\n\n/** Single-character insertions, deletions and substitutions between two words. */\nfunction editDistance(left: string, right: string): number {\n let previous: number[] = Array.from({ length: right.length + 1 }, (_, index) => index);\n\n for (let row = 1; row <= left.length; row += 1) {\n const current: number[] = [row];\n for (let column = 1; column <= right.length; column += 1) {\n // Every index here is inside a row this loop has already filled.\n const substitution = previous[column - 1]! + (left[row - 1] === right[column - 1] ? 0 : 1);\n current[column] = Math.min(substitution, previous[column]! + 1, current[column - 1]! + 1);\n }\n previous = current;\n }\n\n return previous[right.length]!;\n}\n","/** Schemas, error mapping and rendering shared by the six tools. */\n\nimport { z } from \"zod\";\nimport { BnfError } from \"../errors.js\";\n\n/**\n * The text block is what many clients render, and some render nothing else, so\n * it has to answer on its own. This ceiling is what keeps a list of editions\n * from arriving as several pages of publishers.\n */\nexport const MAX_TEXT_CHARS = 2400;\n\n/**\n * The credit every answer ends with.\n *\n * The BnF publishes these metadata under one condition, stated in one sentence:\n * they may be used freely provided their source is named and the date they were\n * retrieved is stated. Both halves are built into the answer rather than left to\n * whoever renders it, because a condition a caller has to remember is a\n * condition that will be forgotten.\n */\nexport const SOURCE_NAME = \"data.bnf.fr (Bibliothèque nationale de France)\";\n\n/** The date of retrieval, written as the calendar day it names. */\nexport function retrievalDay(retrievedAt: string): string {\n const day = retrievedAt.slice(0, 10);\n return /^\\d{4}-\\d{2}-\\d{2}$/.test(day) ? day : retrievedAt;\n}\n\nexport function attribution(retrievedAt: string, sourceUrl?: string): string {\n const credit = `Source: ${SOURCE_NAME}, retrieved ${retrievalDay(retrievedAt)}`;\n return sourceUrl ? `${credit} — ${sourceUrl}` : credit;\n}\n\nexport interface ToolResult {\n // The SDK's result type 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/** The moment the metadata came off data.bnf.fr, carried by every answer. */\nexport const retrievedAtSchema = z\n .string()\n .describe(\n \"When these metadata were read from data.bnf.fr, as an ISO 8601 instant. The BnF licence asks for this date to be stated wherever the metadata are shown, so repeat it alongside the source.\",\n );\n\n/** A link to a digitised document, which this server describes and never opens. */\nexport const digitisedLinkSchema = z.object({\n ark: z.string().describe(\"The Gallica ARK identifying the document.\"),\n url: z.string().describe(\"Open this to see the document. This server does not read it.\"),\n role: z\n .enum([\"reproduction\", \"ocr\", \"depiction\"])\n .describe(\n \"'reproduction' is the edition itself digitised. 'ocr' is the text a machine read off it. 'depiction' is an image illustrating the record, which can be a portrait or a page mentioning the subject rather than the work.\",\n ),\n from_id: z.string().describe(\"The record this link hangs off.\"),\n from_title: z.string().nullable(),\n});\n\n/**\n * Keep text from data.bnf.fr out of the shape this server's own lines take.\n *\n * This server writes three kinds of line a reader treats as its own voice:\n * `Note:`, `Source:` and `Hint:`. A cataloguer's note or a title carrying those\n * same words at the start of a line is indistinguishable from one of them, and a\n * forged `Source:` line placed above the real one substitutes a different\n * attribution for the BnF's.\n *\n * Indenting such a line keeps the two apart and costs nothing: the structured\n * output still carries the text exactly as it was published. The match ignores\n * case and allows a space before the colon, because a forgery chooses its own\n * spelling, and `m` treats a carriage return, a line feed and the Unicode line\n * and paragraph separators alike.\n */\nexport function indentMarkerLines(body: string): string {\n return body.replace(/^(Note|Source|Hint)(\\s*:)/gim, \" $1$2\");\n}\n\n/**\n * Third-party text folded onto one line, for the places that build a sentence\n * around it.\n *\n * A note and an error message are each one line by construction, and the marker\n * they open with is written by this file. A value carrying a line break would\n * put whatever follows it at the start of a line of its own, where it could\n * spell `Source:` and be read as this server's attribution. Removing the break\n * is what keeps the line count equal to one, which is what makes the marker at\n * its head the only one there is.\n */\nexport function onOneLine(value: string): string {\n // U+2028 and U+2029 are written as escapes because they terminate a line in\n // source as well as in text: written literally they would end this one.\n return value.replace(/[\\r\\n\\u2028\\u2029]+/g, \" \").trim();\n}\n\n/**\n * Build a result whose text block ends with its notes and its credit.\n *\n * The body is cut to fit around the trailer rather than the whole block being\n * cut afterwards. Appending the credit and then truncating loses exactly the\n * credit and the date beside it, which are the two lines the licence requires.\n *\n * Notes qualify an answer: that a list was cut, that a search does not rank,\n * that a record is provisional. A client that shows only the text would\n * otherwise present an unqualified answer, so they travel with the credit.\n */\nexport function ok(\n structured: Record<string, unknown>,\n body: string,\n options: { retrievedAt: string; notes?: string[]; sourceUrl?: string },\n): ToolResult {\n const credit = attribution(options.retrievedAt, options.sourceUrl);\n\n // Notes are guarded the same way the body is. Several of them are built\n // around a value the BnF published, and the trailer is appended after the\n // body has been made safe, so a note is the one way third-party text could\n // otherwise reach the block already looking like a line this server wrote.\n const noteLines = (options.notes ?? []).map((note) => `Note: ${onOneLine(note)}`);\n while (noteLines.length > 0 && noteLines.join(\"\\n\").length > MAX_TEXT_CHARS / 2) noteLines.pop();\n const trailer = [...noteLines, credit].join(\"\\n\");\n\n const cut = \"\\n\\n[shortened; the full result is in the structured output]\";\n const budget = MAX_TEXT_CHARS - `\\n\\n${trailer}`.length;\n const safe = indentMarkerLines(body);\n const text =\n safe.length <= budget\n ? `${safe}\\n\\n${trailer}`\n : `${truncate(safe, Math.max(0, budget - cut.length))}${cut}\\n\\n${trailer}`;\n\n return { content: [{ type: \"text\", text }], structuredContent: structured };\n}\n\n/**\n * Errors carry no structured payload: the SDK checks it against the tool's\n * declared output schema, and a failure does not fit that shape.\n */\nexport function toToolError(error: unknown): ToolResult {\n const known =\n error instanceof BnfError\n ? error\n : new BnfError(\"network_error\", error instanceof Error ? error.message : String(error));\n\n // A failure message can carry text from the endpoint and identifiers a caller\n // wrote, so it is guarded like any other body.\n const lines = [`[${known.code}] ${onOneLine(known.message)}`];\n if (known.details.hint) lines.push(`Hint: ${onOneLine(known.details.hint)}`);\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, Math.max(0, maxChars - 1)).trimEnd()}…`;\n}\n\n/** Wording used wherever a search that does not rank reaches a caller. */\nexport const NO_RANKING =\n \"The full-text index answers whether a record matches, and it does not score how well. These rows are in the order the index returned them, so the row a person would call the obvious answer can sit anywhere in the list, or on a later page.\";\n\n/** Wording used wherever a Gallica link reaches a caller. */\nexport const GALLICA_CAVEAT =\n \"These are links for a person to open. This server reads the BnF catalogue and never requests gallica.bnf.fr, so it cannot say whether a document opens, what it contains, or on what terms it may be reused.\";\n\n/** Wording used wherever a provisional record reaches a caller. */\nexport const PROVISIONAL_CAVEAT =\n \"A provisional record is one the BnF created to hold a title it has catalogued and not yet established as a work of its own. Its identifier can change, and it can be merged into an established record.\";\n","/**\n * get_author: read one person's authority record.\n *\n * The record is a set of statements a cataloguer entered, and its shape says\n * what it is for. Dates, places, a language, a country, a Dewey class and\n * alignments to VIAF, IdRef and DBpedia are all there and all reliable. A\n * biography is not: `biographicalInformation` holds \"Poète\" on Rimbaud, and a\n * caller asked for a life story has to be told that the field is a job title\n * rather than handed one word dressed up as one.\n */\n\nimport { z } from \"zod\";\nimport type { BnfClient } from \"../bnf/client.js\";\nimport { strictInput } from \"./arguments.js\";\nimport {\n GALLICA_CAVEAT,\n digitisedLinkSchema,\n ok,\n retrievedAtSchema,\n toToolError,\n} from \"./shared.js\";\nimport type { ToolResult } from \"./shared.js\";\n\nexport const getAuthorDescription = [\n \"Read one person's record in the Bibliothèque nationale de France authority file, by the identifier search_authors returns.\",\n \"It carries the dates and places of birth and death, the occupation the record names, the language and country it associates with the person, the field of activity with its Dewey class, and the addresses for the same person in VIAF, IdRef, DBpedia, Wikidata and ISNI.\",\n \"It carries no biography. The field the BnF calls biographical information is a job title on most records and a single word on many, so quote it as what the catalogue states and do not build a life story out of it.\",\n \"It does not list the person's works. The catalogue links them, and no tool here walks that link: search_works reads titles, so a work is found by its title rather than by its author.\",\n \"'depictions' are images data.bnf.fr attaches to the person. They are links for a person to open; this server does not read them, and an image can be a page that mentions the person rather than a portrait of them.\",\n].join(\" \");\n\nexport const getAuthorInput = strictInput({\n author_id: z\n .string()\n .min(1)\n .max(200)\n .describe(\n \"The identifier from search_authors, such as 'cb119219976'. A full data.bnf.fr address is accepted too.\",\n ),\n include_depictions: z\n .boolean()\n .default(false)\n .describe(\n \"Include the images data.bnf.fr attaches to the person. A well-known person can carry dozens, so they are left out by default. find_digitised gathers them with everything else.\",\n ),\n});\n\nexport const getAuthorOutput = z.object({\n author: z.object({\n id: z.string(),\n name: z.string().nullable(),\n label: z.string().nullable().describe(\"The authority heading, usually with the dates.\"),\n given_name: z.string().nullable(),\n family_name: z.string().nullable(),\n other_names: z\n .array(z.object({ label: z.string(), language: z.string().nullable() }))\n .describe(\"Other headings the record carries, including transliterations and pen names.\"),\n birth_date: z.string().nullable().describe(\"As published, usually YYYY-MM-DD.\"),\n death_date: z.string().nullable(),\n birth_year: z.number().int().nullable(),\n death_year: z.number().int().nullable(),\n birth_place: z.string().nullable(),\n death_place: z.string().nullable(),\n biographical_information: z\n .string()\n .nullable()\n .describe(\n \"What the record states about the person. This is a job title on most records and a single word on many; it is not a biography.\",\n ),\n occupation: z.string().nullable(),\n languages: z.array(z.string()).describe(\"ISO 639-2 codes, such as 'fre'.\"),\n countries: z.array(z.string()).describe(\"Country codes, such as 'fr'.\"),\n fields: z.array(z.string()).describe(\"Fields of activity in the words of the record.\"),\n dewey_classes: z\n .array(z.string())\n .describe(\"Dewey classes the fields point at, such as '800' for literature.\"),\n same_as: z\n .record(z.string(), z.array(z.string()))\n .describe(\"Addresses for the same person in other files, grouped by file.\"),\n catalogue_url: z.string().nullable().describe(\"The record in the BnF general catalogue.\"),\n record_created: z.string().nullable().describe(\"When the authority record was created.\"),\n record_modified: z.string().nullable().describe(\"When it was last changed.\"),\n source_url: z.string(),\n }),\n depictions: z.array(digitisedLinkSchema).optional(),\n depiction_count: z\n .number()\n .int()\n .describe(\n \"Images on Gallica that the record points at, counted whether or not they were returned. An illustration the record holds elsewhere is not counted, because this server describes Gallica documents and knows nothing about the others.\",\n ),\n retrieved_at: retrievedAtSchema,\n notes: z.array(z.string()),\n});\n\nexport type GetAuthorArgs = z.infer<typeof getAuthorInput>;\n\nexport async function runGetAuthor(client: BnfClient, args: GetAuthorArgs): Promise<ToolResult> {\n try {\n const id = client.identify(args.author_id);\n const { data, cached, retrievedAt } = await client.getAuthor(id);\n\n const notes: string[] = [];\n if (cached) notes.push(\"Served from this server's short-lived in-memory cache.\");\n\n if (data.biographicalInformation !== null) {\n notes.push(\n `The BnF states \"${data.biographicalInformation}\" as this person's biographical information. That field holds an occupation rather than a life, and this is all the catalogue says.`,\n );\n }\n if (data.deathDate === null && data.deathYear === null) {\n notes.push(\n \"The record states no date of death. That can mean the person is living, or that the BnF has not recorded one; the catalogue does not distinguish the two.\",\n );\n }\n if (Object.keys(data.sameAs).length > 0) {\n notes.push(\n \"The addresses under 'same_as' are the BnF's own alignments to other files. Following one is how to reach a biography, which this catalogue does not hold.\",\n );\n }\n if (data.depictions.length > 0) notes.push(GALLICA_CAVEAT);\n\n const structured: Record<string, unknown> = {\n author: {\n id: data.id,\n name: data.name,\n label: data.label,\n given_name: data.givenName,\n family_name: data.familyName,\n other_names: data.otherNames,\n birth_date: data.birthDate,\n death_date: data.deathDate,\n birth_year: data.birthYear,\n death_year: data.deathYear,\n birth_place: data.birthPlace,\n death_place: data.deathPlace,\n biographical_information: data.biographicalInformation,\n occupation: data.occupation,\n languages: data.languages,\n countries: data.countries,\n fields: data.fields,\n dewey_classes: data.deweyClasses,\n same_as: data.sameAs,\n catalogue_url: data.catalogueUrl,\n record_created: data.recordCreated,\n record_modified: data.recordModified,\n source_url: data.sourceUrl,\n },\n depiction_count: data.depictions.length,\n retrieved_at: retrievedAt,\n notes,\n };\n\n if (args.include_depictions) {\n structured.depictions = data.depictions.map((link) => ({\n ark: link.ark,\n url: link.url,\n role: link.role,\n from_id: link.fromId,\n from_title: link.fromTitle,\n }));\n }\n\n const born = [\n data.birthDate ?? (data.birthYear !== null ? String(data.birthYear) : null),\n data.birthPlace,\n ]\n .filter(Boolean)\n .join(\", \");\n const died = [\n data.deathDate ?? (data.deathYear !== null ? String(data.deathYear) : null),\n data.deathPlace,\n ]\n .filter(Boolean)\n .join(\", \");\n\n const lines = [\n data.label ?? data.name ?? data.id,\n born ? `Born: ${born}` : \"\",\n died ? `Died: ${died}` : \"\",\n data.occupation ? `Occupation as recorded: ${data.occupation}` : \"\",\n data.fields.length > 0\n ? `Field of activity: ${data.fields.join(\", \")}${data.deweyClasses.length > 0 ? ` (Dewey ${data.deweyClasses.join(\", \")})` : \"\"}`\n : \"\",\n data.languages.length > 0 ? `Language: ${data.languages.join(\", \")}` : \"\",\n data.countries.length > 0 ? `Country: ${data.countries.join(\", \")}` : \"\",\n data.otherNames.length > 0\n ? `Also recorded as: ${data.otherNames.map((other) => other.label).join(\"; \")}`\n : \"\",\n Object.keys(data.sameAs).length > 0\n ? `Same person in: ${Object.entries(data.sameAs)\n .map(([file, addresses]) => `${file} ${addresses[0]}`)\n .join(\" · \")}`\n : \"\",\n data.catalogueUrl ? `BnF catalogue: ${data.catalogueUrl}` : \"\",\n data.depictions.length > 0\n ? `Images attached to this record: ${data.depictions.length}${args.include_depictions ? \"\" : \" (ask with include_depictions, or use find_digitised)\"}`\n : \"\",\n ].filter(Boolean);\n\n return ok(structured, lines.join(\"\\n\"), { retrievedAt, notes, sourceUrl: data.sourceUrl });\n } catch (error) {\n return toToolError(error);\n }\n}\n","/**\n * get_work: read one work's record.\n *\n * The BnF holds two kinds of work record, and telling them apart is the point\n * of this tool. An established work has an ARK of its own, a settled title, and\n * links to every expression the catalogue knows. A provisional one is addressed\n * under `temp-work`, was created to hold a title a cataloguer has not yet\n * settled, and can be renamed, re-addressed or merged away. Quoting a\n * provisional identifier as a stable one is how a citation goes bad quietly, so\n * the record says which it is in words rather than leaving it to be read off\n * the shape of an address.\n */\n\nimport { z } from \"zod\";\nimport type { BnfClient } from \"../bnf/client.js\";\nimport { strictInput } from \"./arguments.js\";\nimport {\n GALLICA_CAVEAT,\n PROVISIONAL_CAVEAT,\n digitisedLinkSchema,\n ok,\n retrievedAtSchema,\n toToolError,\n} from \"./shared.js\";\nimport type { ToolResult } from \"./shared.js\";\n\nexport const getWorkDescription = [\n \"Read one work's record in the Bibliothèque nationale de France catalogue, by the identifier search_works returns.\",\n \"It carries the title, everyone the record credits, the date the BnF gives the work, its language, its form and its subject with the Dewey class.\",\n \"'status' says whether the record is established or provisional, and 'status_statement' repeats what the catalogue itself states. A provisional identifier can change, so cite an established one where there is a choice.\",\n \"'expression_count' counts the expressions the record links, which is not a count of published editions: use list_editions for those.\",\n].join(\" \");\n\nexport const getWorkInput = strictInput({\n work_id: z\n .string()\n .min(1)\n .max(200)\n .describe(\n \"The identifier from search_works, such as 'cb11970626n' or 'temp-work/22d7f68c1a4bdd081ad7ca791fd3b730'.\",\n ),\n include_depictions: z\n .boolean()\n .default(false)\n .describe(\n \"Include the images data.bnf.fr attaches to the work. A well-known work can carry dozens, so they are left out by default. find_digitised gathers them alongside the digitised editions.\",\n ),\n});\n\nexport const getWorkOutput = z.object({\n work: z.object({\n id: z.string(),\n title: z.string().nullable(),\n label: z.string().nullable(),\n date: z.string().nullable().describe(\"The date the record gives the work, as published.\"),\n first_year: z.number().int().nullable(),\n creators: z.array(z.object({ id: z.string(), name: z.string().nullable() })),\n languages: z.array(z.string()).describe(\"ISO 639-2 codes.\"),\n forms: z\n .array(z.string())\n .describe(\"Forms from the BnF work-form vocabulary, such as 'poesi' for poetry.\"),\n subjects: z.array(z.string()).describe(\"Subjects in the words of the record.\"),\n dewey_classes: z.array(z.string()),\n status: z.enum([\"established\", \"provisional\"]),\n status_statement: z\n .string()\n .nullable()\n .describe(\"What the record itself states, such as 'fully established' or 'provisional'.\"),\n expression_count: z\n .number()\n .int()\n .nullable()\n .describe(\n \"Expressions the record links: translations, adaptations, recordings. This is not a count of published editions. Null when the record was longer than one query reads, since the number would then be the ceiling rather than the count.\",\n ),\n same_as: z.record(z.string(), z.array(z.string())),\n catalogue_url: z.string().nullable(),\n source_url: z.string(),\n }),\n depictions: z.array(digitisedLinkSchema).optional(),\n depiction_count: z\n .number()\n .int()\n .describe(\n \"Images on Gallica that the record points at, counted whether or not they were returned.\",\n ),\n retrieved_at: retrievedAtSchema,\n notes: z.array(z.string()),\n});\n\nexport type GetWorkArgs = z.infer<typeof getWorkInput>;\n\nexport async function runGetWork(client: BnfClient, args: GetWorkArgs): Promise<ToolResult> {\n try {\n const id = client.identify(args.work_id);\n const { data, cached, retrievedAt } = await client.getWork(id);\n\n const notes: string[] = [];\n if (cached) notes.push(\"Served from this server's short-lived in-memory cache.\");\n\n if (data.status === \"provisional\") {\n notes.push(\n `This is a provisional record, addressed under 'temp-work' rather than by an ARK${data.statusStatement ? ` and stated as \"${data.statusStatement}\"` : \"\"}. ${PROVISIONAL_CAVEAT}`,\n );\n } else {\n notes.push(\n `This is an established work record, addressed by the ARK '${data.id}'${data.statusStatement ? ` and stated as \"${data.statusStatement}\"` : \"\"}. That identifier is the one to cite.`,\n );\n }\n\n if (data.truncated) {\n notes.push(\n \"This record is longer than one query reads, so parts of it are missing from this answer. A field shown as empty here may be filled on the record itself: open source_url before concluding that the BnF states nothing.\",\n );\n }\n if (data.expressionCount !== null && data.expressionCount > 0) {\n notes.push(\n `The record links ${data.expressionCount} expression(s): translations, adaptations and recordings of this work. Published editions are a separate count, and list_editions is what reads them.`,\n );\n }\n if (data.creators.length === 0) {\n notes.push(\n \"The record credits nobody with this work. That is normal for anonymous and traditional works, and it also happens on records a cataloguer has not finished.\",\n );\n }\n if (data.depictions.length > 0) notes.push(GALLICA_CAVEAT);\n\n const structured: Record<string, unknown> = {\n work: {\n id: data.id,\n title: data.title,\n label: data.label,\n date: data.date,\n first_year: data.firstYear,\n creators: data.creators,\n languages: data.languages,\n forms: data.forms,\n subjects: data.subjects,\n dewey_classes: data.deweyClasses,\n status: data.status,\n status_statement: data.statusStatement,\n expression_count: data.expressionCount,\n same_as: data.sameAs,\n catalogue_url: data.catalogueUrl,\n source_url: data.sourceUrl,\n },\n depiction_count: data.depictions.length,\n retrieved_at: retrievedAt,\n notes,\n };\n\n if (args.include_depictions) {\n structured.depictions = data.depictions.map((link) => ({\n ark: link.ark,\n url: link.url,\n role: link.role,\n from_id: link.fromId,\n from_title: link.fromTitle,\n }));\n }\n\n const lines = [\n [data.title ?? data.label ?? data.id, data.date ? `(${data.date})` : \"\"]\n .filter(Boolean)\n .join(\" \"),\n data.creators.length > 0\n ? `By: ${data.creators.map((creator) => creator.name ?? creator.id).join(\", \")}`\n : \"\",\n `Record: ${data.status === \"provisional\" ? \"provisional\" : \"established\"}`,\n data.languages.length > 0 ? `Language: ${data.languages.join(\", \")}` : \"\",\n data.forms.length > 0 ? `Form: ${data.forms.join(\", \")}` : \"\",\n data.subjects.length > 0\n ? `Subject: ${data.subjects.join(\", \")}${data.deweyClasses.length > 0 ? ` (Dewey ${data.deweyClasses.join(\", \")})` : \"\"}`\n : \"\",\n data.catalogueUrl ? `BnF catalogue: ${data.catalogueUrl}` : \"\",\n ].filter(Boolean);\n\n return ok(structured, lines.join(\"\\n\"), { retrievedAt, notes, sourceUrl: data.sourceUrl });\n } catch (error) {\n return toToolError(error);\n }\n}\n","/**\n * list_editions: the published editions of one work.\n *\n * A manifestation is what a reader would call an edition: a publisher, a place,\n * a year, an extent, and sometimes a digitised copy. What the catalogue does\n * not record is whether that copy can be opened, by whom, or under what terms,\n * and there is no field that would let this server work it out. So a row that\n * carries a Gallica link says a link exists, and stops there.\n */\n\nimport { z } from \"zod\";\nimport type { BnfClient } from \"../bnf/client.js\";\nimport { strictInput } from \"./arguments.js\";\nimport {\n GALLICA_CAVEAT,\n digitisedLinkSchema,\n ok,\n retrievedAtSchema,\n toToolError,\n} from \"./shared.js\";\nimport type { ToolResult } from \"./shared.js\";\n\nexport const listEditionsDescription = [\n \"List the published editions of one work in the Bibliothèque nationale de France catalogue, by the identifier search_works or get_work returns.\",\n \"Each row carries the publisher, the place, the year, the edition statement, the extent in the words of the record, the ISBN when there is one, and the record in the BnF general catalogue.\",\n \"An edition that has been digitised carries a link under 'digitised'. That link says a copy exists at that address; this server never opens it, so it cannot say whether the document is readable, complete, or free to reuse.\",\n \"Rows are in the order the catalogue holds them, which is neither by date nor by importance.\",\n].join(\" \");\n\nexport const listEditionsInput = strictInput({\n work_id: z.string().min(1).max(200).describe(\"The work identifier, such as 'cb11970626n'.\"),\n limit: z.number().int().min(1).max(50).default(10),\n page: z.number().int().min(1).max(100).default(1),\n});\n\nexport const editionRowSchema = z.object({\n id: z.string().describe(\"The edition's own identifier in the BnF catalogue.\"),\n title: z.string().nullable().describe(\"The title as this edition carries it.\"),\n date: z\n .string()\n .nullable()\n .describe(\"The date as published, which can be a phrase such as '[s.d.]' for an undated one.\"),\n year: z.number().int().nullable().describe(\"The year, when the record states one as a number.\"),\n publisher: z.string().nullable(),\n place: z.string().nullable(),\n edition_statement: z.string().nullable().describe(\"Such as a numbered or revised edition.\"),\n extent: z\n .string()\n .nullable()\n .describe(\"The extent in the words of the record: pages, volumes, discs.\"),\n isbn: z.string().nullable(),\n note: z.string().nullable().describe(\"What the cataloguer wrote, as published.\"),\n catalogue_url: z.string().nullable(),\n digitised: z\n .array(digitisedLinkSchema)\n .describe(\n \"Digitised copies of this edition, as links. An empty list means the catalogue records none here, not that none exists.\",\n ),\n source_url: z.string(),\n});\n\nexport const listEditionsOutput = z.object({\n work_id: z.string(),\n editions: z.array(editionRowSchema),\n page: z.number().int(),\n has_more: z.boolean(),\n digitised_count: z.number().int().describe(\"Editions on this page carrying at least one link.\"),\n retrieved_at: retrievedAtSchema,\n notes: z.array(z.string()),\n});\n\nexport type ListEditionsArgs = z.infer<typeof listEditionsInput>;\n\nexport async function runListEditions(\n client: BnfClient,\n args: ListEditionsArgs,\n): Promise<ToolResult> {\n try {\n const id = client.identify(args.work_id);\n const offset = (args.page - 1) * args.limit;\n const { data, cached, retrievedAt } = await client.listEditions(id, args.limit, offset);\n\n const notes: string[] = [];\n if (cached) notes.push(\"Served from this server's short-lived in-memory cache.\");\n\n const editions = data.rows.map((row) => ({\n id: row.id,\n title: row.title,\n date: row.date,\n year: row.year,\n publisher: row.publisher,\n place: row.place,\n edition_statement: row.editionStatement,\n extent: row.extent,\n isbn: row.isbn,\n note: row.note,\n catalogue_url: row.catalogueUrl,\n digitised: row.digitised.map((link) => ({\n ark: link.ark,\n url: link.url,\n role: link.role,\n from_id: link.fromId,\n from_title: link.fromTitle,\n })),\n source_url: row.sourceUrl,\n }));\n\n const digitisedCount = editions.filter((edition) => edition.digitised.length > 0).length;\n if (digitisedCount > 0) notes.push(GALLICA_CAVEAT);\n\n if (data.hasMore) {\n notes.push(`More editions exist beyond this page. Ask for page ${args.page + 1}.`);\n }\n if (editions.length === 0 && args.page > 1) {\n notes.push(\n `Page ${args.page} holds no row, which means the list ended earlier rather than that the work has no editions. Ask for page 1 to see it from the start.`,\n );\n } else if (editions.length === 0) {\n notes.push(\n `The catalogue links no published edition to \"${id.id}\". A work record can exist with none: it happens on provisional records, and on works the BnF describes through an anthology rather than through an edition of their own. Check get_work to see what kind of record this is.`,\n );\n }\n if (editions.length > 0) {\n notes.push(\n \"Rows are in the order the catalogue holds them. It is neither chronological nor an order of importance, so a first edition can appear anywhere in the list.\",\n );\n }\n\n const body =\n editions.length === 0\n ? args.page > 1\n ? `Page ${args.page} of the editions of \"${id.id}\" holds no row.`\n : `The BnF catalogue links no edition to the work \"${id.id}\".`\n : `${editions.length} edition(s) of \"${id.id}\":\\n${editions\n .map((edition, index) => {\n const imprint = [edition.place, edition.publisher, edition.date]\n .filter(Boolean)\n .join(\" : \");\n const extra = [\n edition.edition_statement,\n edition.extent,\n edition.isbn ? `ISBN ${edition.isbn}` : \"\",\n ]\n .filter(Boolean)\n .join(\" · \");\n const links =\n edition.digitised.length > 0\n ? `\\n digitised: ${edition.digitised.map((link) => `${link.role} ${link.url}`).join(\" \")}`\n : \"\";\n return `${index + 1}. ${edition.title ?? edition.id}${imprint ? ` · ${imprint}` : \"\"}${extra ? `\\n ${extra}` : \"\"}\\n ${edition.catalogue_url ?? edition.source_url}${links}`;\n })\n .join(\"\\n\")}`;\n\n return ok(\n {\n work_id: id.id,\n editions,\n page: args.page,\n has_more: data.hasMore,\n digitised_count: digitisedCount,\n retrieved_at: retrievedAt,\n notes,\n },\n body,\n { retrievedAt, notes },\n );\n } catch (error) {\n return toToolError(error);\n }\n}\n","/**\n * search_authors: find a person in the BnF authority file by name.\n *\n * One person can hold several authority records. \"Arthur Rimbaud\" answers with\n * two, `cb119219976` and `cb11921998j`, and neither is a mistake: the file\n * distinguishes the poet from a second heading the BnF keeps for him. Returning\n * one of the two and calling it the answer would hide a choice the caller is\n * the only one able to make, so every match is returned and the answer says\n * when several rows carry one name.\n */\n\nimport { z } from \"zod\";\nimport type { BnfClient } from \"../bnf/client.js\";\nimport { TEXT_WINDOW } from \"../bnf/queries.js\";\nimport { invalidInput } from \"../errors.js\";\nimport { strictInput } from \"./arguments.js\";\nimport { NO_RANKING, ok, retrievedAtSchema, toToolError } from \"./shared.js\";\nimport type { ToolResult } from \"./shared.js\";\n\nexport const searchAuthorsDescription = [\n \"Find a person in the Bibliothèque nationale de France authority file by name, and get the identifier the other tools take.\",\n \"This matches the name the BnF records, so it takes a surname, a full name or both names in either order. It does not read biographies, so it cannot find a person from what they wrote or what they did.\",\n \"Several rows can carry one name: the BnF keeps more than one authority record for some people, and many people share a name. Read 'birth_year', 'death_year' and 'role' to tell them apart, and show the caller the choice rather than picking one.\",\n \"Rows come back in the order the index returned them, which is not an order of relevance.\",\n].join(\" \");\n\nexport const searchAuthorsInput = strictInput({\n name: z\n .string()\n .min(1)\n .max(200)\n .describe(\"The person's name, such as 'Rimbaud' or 'Arthur Rimbaud'.\"),\n limit: z.number().int().min(1).max(50).default(10),\n page: z.number().int().min(1).max(100).default(1),\n});\n\nexport const authorRowSchema = z.object({\n id: z.string().describe(\"Pass this to get_author, or to find_digitised.\"),\n name: z.string().nullable().describe(\"The name as data.bnf.fr writes it.\"),\n label: z\n .string()\n .nullable()\n .describe(\"The authority heading, which usually carries the dates in brackets.\"),\n birth_year: z.number().int().nullable(),\n death_year: z.number().int().nullable().describe(\"Null when the record states none.\"),\n role: z\n .string()\n .nullable()\n .describe(\n \"What the record says the person did. This field holds a job title far more often than a sentence, and is frequently a single word.\",\n ),\n source_url: z.string().describe(\"The record's page. Show this when citing it.\"),\n});\n\nexport const searchAuthorsOutput = z.object({\n name: z.string().describe(\"The name asked for.\"),\n words_searched: z\n .array(z.string())\n .describe(\"The words the index was actually asked for, after punctuation was set aside.\"),\n authors: z.array(authorRowSchema),\n page: z.number().int(),\n has_more: z\n .boolean()\n .describe(\"Whether the index held at least one further match beyond this page.\"),\n retrieved_at: retrievedAtSchema,\n notes: z.array(z.string()),\n});\n\nexport type SearchAuthorsArgs = z.infer<typeof searchAuthorsInput>;\n\n/**\n * The years a record states, written so that a silence stays a silence.\n *\n * Printing \"1940–?\" hands a reader a glyph to interpret, and most read it as a\n * death on a date nobody wrote down. A record carrying a birth year and no death\n * year says one thing: when the person was born.\n */\nfunction lifespan(birthYear: number | null, deathYear: number | null): string {\n if (birthYear !== null && deathYear !== null) return ` (${birthYear}–${deathYear})`;\n if (birthYear !== null) return ` (born ${birthYear})`;\n if (deathYear !== null) return ` (died ${deathYear})`;\n return \"\";\n}\n\nexport async function runSearchAuthors(\n client: BnfClient,\n args: SearchAuthorsArgs,\n): Promise<ToolResult> {\n try {\n const words = client.words(args.name);\n if (words.length === 0) {\n return toToolError(\n invalidInput(\n `\"${args.name}\" holds no word to search for.`,\n \"Write the name in letters, such as 'Rimbaud'.\",\n ),\n );\n }\n\n // A text search reads a fixed window of the index, and a page beyond it\n // would come back empty for want of anything having been read. Answering\n // that with \"the BnF holds none\" states an absence nobody established, so\n // the page is refused here rather than bought from the endpoint.\n const offset = (args.page - 1) * args.limit;\n if (offset + args.limit > TEXT_WINDOW) {\n return toToolError(\n invalidInput(\n `Page ${args.page} at ${args.limit} rows a page reaches past row ${TEXT_WINDOW}, which is as far into the index as one search reads.`,\n `Narrow the search instead. Rows past ${TEXT_WINDOW} are not a smaller part of the answer: they were never read, so an empty page there would say nothing about the catalogue.`,\n ),\n );\n }\n const { data, cached, retrievedAt } = await client.searchAuthors(args.name, args.limit, offset);\n\n const notes: string[] = [];\n if (cached) notes.push(\"Served from this server's short-lived in-memory cache.\");\n\n const authors = data.rows.map((row) => ({\n id: row.id,\n name: row.name,\n label: row.label,\n birth_year: row.birthYear,\n death_year: row.deathYear,\n role: row.role,\n source_url: row.sourceUrl,\n }));\n\n if (authors.length > 0) notes.push(NO_RANKING);\n\n // Several records under one name is the case this tool exists to surface.\n const repeated = new Map<string, number>();\n for (const author of authors) {\n const key = (author.name ?? \"\").toLocaleLowerCase(\"fr\");\n if (key !== \"\") repeated.set(key, (repeated.get(key) ?? 0) + 1);\n }\n const shared = [...repeated.entries()].filter(([, count]) => count > 1);\n if (shared.length > 0) {\n notes.push(\n `${shared.map(([name, count]) => `${count} records are named \"${name}\"`).join(\"; \")}. The BnF keeps more than one authority record for some people, and different people share names. Read the dates and the role before treating any of them as the person meant.`,\n );\n }\n\n if (data.hasMore) {\n notes.push(\n `More matches exist beyond this page. Ask for page ${args.page + 1}, or narrow the name. This server reports no total, because counting every match means asking the endpoint to walk the whole index a second time.`,\n );\n }\n if (authors.length === 0) {\n notes.push(\n `No record in the BnF authority file carries every one of these words in a name: ${words.map((word) => `\"${word}\"`).join(\", \")}. A search here reads names, so a person known by a pen name is found under that name rather than their own.`,\n );\n }\n if (words.length < args.name.trim().split(/\\s+/).length) {\n notes.push(\n `The index was asked for ${words.map((word) => `\"${word}\"`).join(\", \")}; anything in the name that was not a word was set aside.`,\n );\n }\n\n const body =\n authors.length === 0\n ? `No person in the BnF authority file matches \"${args.name}\".`\n : `${authors.length} record(s) for \"${args.name}\":\\n${authors\n .map((author, index) => {\n const life = lifespan(author.birth_year, author.death_year);\n const role = author.role ? ` · ${author.role}` : \"\";\n return `${index + 1}. ${author.name ?? author.label ?? author.id}${life}${role} · id: ${author.id}\\n ${author.source_url}`;\n })\n .join(\"\\n\")}`;\n\n return ok(\n {\n name: args.name,\n words_searched: words,\n authors,\n page: args.page,\n has_more: data.hasMore,\n retrieved_at: retrievedAt,\n notes,\n },\n body,\n { retrievedAt, notes },\n );\n } catch (error) {\n return toToolError(error);\n }\n}\n","/**\n * search_works: find a work by words in its title.\n *\n * The endpoint's full-text index answers one question: does this title carry\n * every one of these words. It returns no score, and the order it answers in is\n * the order of the index. Searching for \"saison enfer\" therefore returns\n * a dozen studies of Rimbaud before Rimbaud, and every one of them is a correct\n * match.\n *\n * So this tool never presents its rows as a ranking, and never presents the\n * first row as the answer. Saying that plainly is worth more than any\n * re-ordering this server could invent, since an order this server made up\n * would be a claim the catalogue does not support.\n */\n\nimport { z } from \"zod\";\nimport type { BnfClient } from \"../bnf/client.js\";\nimport { TEXT_WINDOW } from \"../bnf/queries.js\";\nimport { invalidInput } from \"../errors.js\";\nimport { strictInput } from \"./arguments.js\";\nimport { NO_RANKING, PROVISIONAL_CAVEAT, ok, retrievedAtSchema, toToolError } from \"./shared.js\";\nimport type { ToolResult } from \"./shared.js\";\n\nexport const searchWorksDescription = [\n \"Find a work in the Bibliothèque nationale de France catalogue by words in its title, and get the identifier get_work and list_editions take.\",\n \"A row matches when its title carries every word given. The index returns no measure of how well a row matches, so the rows are in index order and the work a person would call the obvious answer can sit anywhere in the list or on a later page.\",\n \"A study of a book carries the book's title, so a search for a famous title returns the criticism alongside the work. Read 'creators' to tell them apart.\",\n \"This reads titles and nothing else. It cannot find the works of a named person: searching a person's name returns works written about them, whose creators are their critics.\",\n \"'status' says whether the BnF has established the work as a record of its own or holds it provisionally under a title it has catalogued.\",\n].join(\" \");\n\nexport const searchWorksInput = strictInput({\n title: z\n .string()\n .min(1)\n .max(200)\n .describe(\"Words from the title, such as 'saison enfer'. Every word given has to appear.\"),\n limit: z.number().int().min(1).max(50).default(10),\n page: z.number().int().min(1).max(100).default(1),\n});\n\nexport const workRowSchema = z.object({\n id: z.string().describe(\"Pass this to get_work, list_editions or find_digitised.\"),\n title: z.string().nullable(),\n date: z.string().nullable().describe(\"The year the record gives the work, as published.\"),\n creators: z\n .array(z.object({ id: z.string(), name: z.string().nullable() }))\n .describe(\"Everyone the record credits. Empty when the record credits nobody.\"),\n status: z\n .enum([\"established\", \"provisional\"])\n .describe(\n \"'established' is a work the BnF has settled as a record of its own. 'provisional' is a title it holds while a cataloguer settles it, whose identifier can change.\",\n ),\n source_url: z.string(),\n});\n\nexport const searchWorksOutput = z.object({\n title: z.string().describe(\"The words asked for.\"),\n words_searched: z.array(z.string()),\n works: z.array(workRowSchema),\n page: z.number().int(),\n has_more: z.boolean(),\n retrieved_at: retrievedAtSchema,\n notes: z.array(z.string()),\n});\n\nexport type SearchWorksArgs = z.infer<typeof searchWorksInput>;\n\nexport async function runSearchWorks(\n client: BnfClient,\n args: SearchWorksArgs,\n): Promise<ToolResult> {\n try {\n const words = client.words(args.title);\n if (words.length === 0) {\n return toToolError(\n invalidInput(\n `\"${args.title}\" holds no word to search for.`,\n \"Write the words of the title in letters, such as 'saison enfer'.\",\n ),\n );\n }\n\n // A text search reads a fixed window of the index, and a page beyond it\n // would come back empty for want of anything having been read. Answering\n // that with \"the BnF holds none\" states an absence nobody established, so\n // the page is refused here rather than bought from the endpoint.\n const offset = (args.page - 1) * args.limit;\n if (offset + args.limit > TEXT_WINDOW) {\n return toToolError(\n invalidInput(\n `Page ${args.page} at ${args.limit} rows a page reaches past row ${TEXT_WINDOW}, which is as far into the index as one search reads.`,\n `Narrow the search instead. Rows past ${TEXT_WINDOW} are not a smaller part of the answer: they were never read, so an empty page there would say nothing about the catalogue.`,\n ),\n );\n }\n const { data, cached, retrievedAt } = await client.searchWorks(args.title, args.limit, offset);\n\n const notes: string[] = [];\n if (cached) notes.push(\"Served from this server's short-lived in-memory cache.\");\n\n const works = data.rows.map((row) => ({\n id: row.id,\n title: row.title,\n date: row.date,\n creators: row.creators,\n status: row.status,\n source_url: row.sourceUrl,\n }));\n\n if (works.length > 0) notes.push(NO_RANKING);\n if (works.some((work) => work.status === \"provisional\")) notes.push(PROVISIONAL_CAVEAT);\n\n // Writing a title out in full narrows the search rather than sharpening it.\n // \"Une saison en enfer\" requires \"une\" and \"en\" to appear as well, so a\n // record catalogued as \"Saison en enfer\" falls out of the answer, and the\n // caller sees a shorter list and reads it as the BnF holding less. Which\n // words carry the meaning is not something this server can tell: \"mal\" and\n // \"les\" are the same length, so the requirement is stated and the choice of\n // what to drop is left where it belongs.\n if (words.length > 2) {\n notes.push(\n `All ${words.length} words were required: ${words.map((word) => `\"${word}\"`).join(\", \")}. A record catalogued under a shorter form of the title carries fewer of them and falls out of the answer, so dropping a word widens the search.`,\n );\n }\n\n // A page of studies about a famous work, with the work itself further down\n // the index, otherwise reads as the BnF not holding the work at all.\n if (works.length > 0 && works.every((work) => work.status === \"provisional\")) {\n notes.push(\n \"Every row on this page is a provisional record. The BnF creates one for a title it has catalogued without settling it as a work of its own, which is what a study of a famous book usually gets. An established record for the work itself can sit further down the index: ask for the next page, or search fewer words of the title.\",\n );\n }\n\n if (data.hasMore) {\n notes.push(\n `More matches exist beyond this page. Ask for page ${args.page + 1}, or add a word to the title. This server reports no total: a count would read as a measure of the search, and this search has none.`,\n );\n }\n if (works.length === 0) {\n notes.push(\n `No work in the BnF catalogue has a title carrying every one of these words: ${words.map((word) => `\"${word}\"`).join(\", \")}. Every word given has to appear, so dropping one widens the search.`,\n );\n }\n\n const body =\n works.length === 0\n ? `No work in the BnF catalogue matches \"${args.title}\".`\n : `${works.length} work(s) whose title carries ${words.map((word) => `\"${word}\"`).join(\" and \")}:\\n${works\n .map((work, index) => {\n const by =\n work.creators.length > 0\n ? ` · ${work.creators.map((creator) => creator.name ?? creator.id).join(\", \")}`\n : \"\";\n const when = work.date ? ` (${work.date})` : \"\";\n const status = work.status === \"provisional\" ? \" · provisional record\" : \"\";\n return `${index + 1}. ${work.title ?? work.id}${when}${by}${status} · id: ${work.id}\\n ${work.source_url}`;\n })\n .join(\"\\n\")}`;\n\n return ok(\n {\n title: args.title,\n words_searched: words,\n works,\n page: args.page,\n has_more: data.hasMore,\n retrieved_at: retrievedAt,\n notes,\n },\n body,\n { retrievedAt, notes },\n );\n } catch (error) {\n return toToolError(error);\n }\n}\n"],"mappings":"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","names":["text","page","z","z","z","text","z","z","z","z","z","z","z","z","z","z","z"]}
package/package.json ADDED
@@ -0,0 +1,73 @@
1
+ {
2
+ "name": "mcp-databnf",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for data.bnf.fr, the Bibliothèque nationale de France open catalogue. Look up authors, works, editions and the Gallica links attached to them. No API key required.",
5
+ "keywords": [
6
+ "mcp",
7
+ "model-context-protocol",
8
+ "bnf",
9
+ "data-bnf",
10
+ "sparql",
11
+ "linked-data",
12
+ "bibliography",
13
+ "authority-records"
14
+ ],
15
+ "homepage": "https://github.com/smeet666/mcp-databnf#readme",
16
+ "bugs": {
17
+ "url": "https://github.com/smeet666/mcp-databnf/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-databnf.git"
26
+ },
27
+ "license": "MIT",
28
+ "author": "Damien Bouissou",
29
+ "type": "module",
30
+ "bin": {
31
+ "mcp-databnf": "dist/index.js"
32
+ },
33
+ "exports": {
34
+ ".": "./dist/index.js",
35
+ "./client": {
36
+ "types": "./dist/bnf/client.d.ts",
37
+ "import": "./dist/bnf/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-databnf",
50
+ "scripts": {
51
+ "build": "tsup",
52
+ "typecheck": "tsc --noEmit",
53
+ "pretest": "npm run build:fixtures",
54
+ "test": "vitest run test/unit",
55
+ "test:live": "vitest run test/live --testTimeout=180000",
56
+ "build:fixtures": "node scripts/build-fixtures.mjs",
57
+ "format": "prettier --write .",
58
+ "prepublishOnly": "npm run typecheck && npm run test && npm run build",
59
+ "start": "node dist/index.js",
60
+ "inspector": "npx @modelcontextprotocol/inspector node dist/index.js"
61
+ },
62
+ "dependencies": {
63
+ "@modelcontextprotocol/sdk": "^1.30.0",
64
+ "zod": "^4.4.3"
65
+ },
66
+ "devDependencies": {
67
+ "@types/node": "^22.18.0",
68
+ "prettier": "^3.9.6",
69
+ "tsup": "^8.5.1",
70
+ "typescript": "^5.9.3",
71
+ "vitest": "^4.1.10"
72
+ }
73
+ }