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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,51 @@
1
+ # Changelog
2
+
3
+ This project follows [semantic versioning](https://semver.org/).
4
+
5
+ ## 1.0.0
6
+
7
+ First release.
8
+
9
+ Six read-only tools over the SPARQL endpoint of data.bnf.fr, the open catalogue
10
+ of the Bibliothèque nationale de France: `search_authors`, `get_author`,
11
+ `search_works`, `get_work`, `list_editions` and `find_digitised`.
12
+
13
+ What the answers are held to:
14
+
15
+ - Every answer names its source and states the date the metadata were retrieved,
16
+ which is what the BnF licence asks for. A cached answer reports the moment it
17
+ was first read.
18
+ - `search_authors` returns every authority record carrying a name, and says when
19
+ several of them do, rather than choosing one.
20
+ - Both searches report that the full-text index does not rank, and report no
21
+ total, because a total on such a search reads as a measure of relevance. A page
22
+ past what one search reads of the index is refused, since an empty page there
23
+ would say nothing about the catalogue.
24
+ - `get_work` states whether a record is established or provisional, and warns
25
+ that a provisional identifier can change.
26
+ - A record is checked against what the catalogue types it as, so a subject
27
+ heading is not read back as a person with no dates, and an edition is not read
28
+ back as the work it manifests.
29
+ - A count is withheld when the record it was computed over was longer than one
30
+ query reads.
31
+
32
+ What it will not do:
33
+
34
+ - Gallica addresses are returned as links and never requested. The guard sits
35
+ between the query builders and the network, redirects are read rather than
36
+ followed, and a test drives all six tools with records full of Gallica
37
+ addresses and asserts that none of them was called.
38
+ - Caller text reaching the full-text index is reduced to words before a query
39
+ exists, identifiers are rebuilt from a match rather than interpolated, and
40
+ limits are bounded integers. The tests read the query back off the wire.
41
+ - Text the BnF published cannot open a line where this server writes its own:
42
+ the attribution it ends with cannot be forged from a catalogue record.
43
+
44
+ How it treats the service:
45
+
46
+ - One request at a time, at least three seconds apart, with a floor that
47
+ configuration cannot lower through the environment or through the published
48
+ client entry point, which also bounds the retry budget and the deadline.
49
+ - Two callers asking one question at the same moment cost the endpoint one query.
50
+ - An answer this client could not read widens the spacing rather than counting as
51
+ a calm reply, because an empty body is how this endpoint says it gave up.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Damien Bouissou
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,335 @@
1
+ # mcp-databnf
2
+
3
+ An MCP server for [data.bnf.fr](https://data.bnf.fr), the open catalogue of the
4
+ Bibliothèque nationale de France. Look up an author, find a work, list the
5
+ editions the BnF holds of it, and gather the links to what has been digitised.
6
+
7
+ No API key. No account. Read-only.
8
+
9
+ [![npm](https://img.shields.io/npm/v/mcp-databnf)](https://www.npmjs.com/package/mcp-databnf)
10
+ [![CI](https://github.com/smeet666/mcp-databnf/actions/workflows/ci.yml/badge.svg)](https://github.com/smeet666/mcp-databnf/actions/workflows/ci.yml)
11
+ [![licence: MIT](https://img.shields.io/badge/licence-MIT-green.svg)](LICENSE)
12
+
13
+ ---
14
+
15
+ ## What it is for
16
+
17
+ The BnF publishes its authority file and its bibliographic records as linked
18
+ data, and answers questions about them over SPARQL. That dataset knows things a
19
+ web search does not: which of two people bearing one name wrote a given book,
20
+ what the BnF recorded as somebody's date and place of death, which editions of a
21
+ work exist and who printed them, and which of those have been digitised.
22
+
23
+ This server asks those questions for you, in six tools, and reports what the
24
+ catalogue answers without adding to it.
25
+
26
+ ## Install
27
+
28
+ ```bash
29
+ npx mcp-databnf
30
+ ```
31
+
32
+ ### Claude Desktop, Claude Code, and other stdio clients
33
+
34
+ ```json
35
+ {
36
+ "mcpServers": {
37
+ "databnf": {
38
+ "command": "npx",
39
+ "args": ["-y", "mcp-databnf"]
40
+ }
41
+ }
42
+ }
43
+ ```
44
+
45
+ ## The six tools
46
+
47
+ | Tool | Answers |
48
+ | ---------------- | ------------------------------------------------------------------------------------------------------------------------- |
49
+ | `search_authors` | Who does the BnF record under this name, and which record is which |
50
+ | `get_author` | Dates, places, occupation, language, country, Dewey class, and the same person in VIAF, IdRef, DBpedia, Wikidata and ISNI |
51
+ | `search_works` | Which works have these words in their title |
52
+ | `get_work` | Title, creators, date, language, form, subject, and whether the record is established or provisional |
53
+ | `list_editions` | Publisher, place, year, edition statement, extent, ISBN, catalogue link, and the Gallica link when there is one |
54
+ | `find_digitised` | Every digitised document the catalogue attaches to a person or a work, as links |
55
+
56
+ A typical exchange asks `search_authors` for a name, reads the rows, and passes
57
+ one identifier to `get_author` or `search_works`.
58
+
59
+ ## What it does not do, and why
60
+
61
+ **It never reads Gallica.** The BnF puts its metadata and its digitised contents
62
+ under two different regimes. The metadata this server reads may be reused freely
63
+ provided the source and the date of retrieval are stated. The contents on
64
+ gallica.bnf.fr are governed separately: their terms make use inside an
65
+ artificial-intelligence project subject to a paid licence outside academic
66
+ research, and the site refuses ClaudeBot and GPTBot at the server, then bans the
67
+ calling address after about fifteen requests whatever the pace.
68
+
69
+ So a Gallica address is treated here as what the catalogue says it is: a piece of
70
+ metadata, rendered as a link for a person to open. The server will tell you that
71
+ a 1873 Brussels printing of _Une saison en enfer_ has been digitised and give you
72
+ its address. It will not tell you what is on page four, whether the scan is
73
+ complete, or whether the document opens at all. A `bnf-onto:OCR` link names a
74
+ machine-read text of a document; the server reports that the text exists and
75
+ leaves it where it is. A test fails if any address on that host is ever built to
76
+ be called.
77
+
78
+ **It does not rank.** The BnF's full-text index answers whether a title or a name
79
+ carries the words asked for. It returns no measure of how well, so the rows come
80
+ back in the order the index holds them. Searching for `saison enfer` returns a
81
+ dozen studies of Rimbaud before Rimbaud, and every one of them is a correct
82
+ match. This server says so rather than inventing an order the catalogue does not
83
+ support, and it reports no total, because a total on a search that does not rank
84
+ reads as a measure of relevance.
85
+
86
+ **It does not write biographies.** The field the BnF calls biographical
87
+ information is an occupation on most records: Rimbaud's says _Poète_, and that is
88
+ the whole of it. `get_author` returns that word and says what it is.
89
+
90
+ **It exposes no raw SPARQL tool.** An arbitrary query is an unbounded load on a
91
+ service a public institution pays for, and nothing here would control what the
92
+ caller wrote. Every query this server sends is one of nine written in advance.
93
+
94
+ ## The licence, and what it asks of you
95
+
96
+ The BnF states one condition on these metadata:
97
+
98
+ > L'utilisation de ces métadonnées est libre et gratuite sous réserve du maintien
99
+ > de la mention de leur source et de l'indication de leur date de récupération.
100
+
101
+ Use is free of charge, provided the source is named **and the date of retrieval
102
+ is stated**. That second half is a design constraint:
103
+ every answer this server produces carries `retrieved_at` in its payload and ends
104
+ its text block with the source and that date. A cached answer reports the moment
105
+ it was originally read, since that is the date it was retrieved. Repeat both
106
+ wherever you show what you got.
107
+
108
+ ## How it treats the service
109
+
110
+ data.bnf.fr is a query service a public institution runs at its own cost, and a
111
+ SPARQL query is a more expensive request than fetching a page.
112
+
113
+ - One request at a time, never in parallel.
114
+ - At least three seconds between two of them. Configuration can widen that and
115
+ cannot narrow it, including through the published client entry point.
116
+ - The `User-Agent` always carries the project identifier and an address where a
117
+ person can be reached, whatever a caller sets.
118
+ - Answers are cached in memory for fifteen minutes, so a conversation that walks
119
+ back over one author does not ask twice.
120
+
121
+ The BnF publishes no rate for this endpoint. It publishes `Crawl-delay: 5` on its
122
+ other host and enforces it there, which is the only figure it has stated about
123
+ how fast it wants to be read, and the floor here was set with that in mind.
124
+
125
+ ## Settings
126
+
127
+ Every one is optional.
128
+
129
+ | Variable | Default | Meaning |
130
+ | ----------------------- | -------- | --------------------------------------------------------------------------------------------------- |
131
+ | `BNF_USER_AGENT` | none | Identify your own client. The project identifier is appended, so the BnF can always reach a person. |
132
+ | `BNF_MIN_INTERVAL_MS` | `3000` | Milliseconds between requests. The floor is 3000 and cannot be lowered. |
133
+ | `BNF_TIMEOUT_MS` | `60000` | Deadline for one query. |
134
+ | `BNF_MAX_RETRIES` | `3` | Attempts after a busy answer. |
135
+ | `BNF_CACHE_TTL_MS` | `900000` | How long an answer is kept. `0` turns the cache off. |
136
+ | `BNF_CACHE_MAX_ENTRIES` | `200` | How many answers are kept. |
137
+ | `BNF_LOG_LEVEL` | `error` | `silent`, `error`, `info` or `debug`. Logs go to stderr. |
138
+
139
+ A value that cannot be read is refused, named on stderr, and the default stands.
140
+ The setting is not clamped: clamping would let you believe a value took effect
141
+ when it did not.
142
+
143
+ ## Errors
144
+
145
+ | Code | Means |
146
+ | --------------- | ----------------------------------------------------------- |
147
+ | `not_found` | The endpoint answered, and the BnF describes no such record |
148
+ | `invalid_input` | The request was refused rather than answered |
149
+ | `rate_limited` | The endpoint asked this client to slow down |
150
+ | `parse_failure` | The answer arrived in a shape this client cannot read |
151
+ | `network_error` | The request did not complete |
152
+ | `timeout` | The query exceeded its deadline, or the endpoint gave it up |
153
+
154
+ `rate_limited` never means the record is missing. Neither does `timeout`: the
155
+ endpoint answers 200 with an empty body when it abandons a query part way
156
+ through, and this server calls that a failure to read rather than an absence,
157
+ because the two look identical and mean opposite things.
158
+
159
+ ## Using the access layer on its own
160
+
161
+ The lower layer imports nothing from the MCP protocol and is published under the
162
+ `./client` subpath, with its pacing, its cache and its error taxonomy attached.
163
+
164
+ ```ts
165
+ import { BnfClient } from "mcp-databnf/client";
166
+
167
+ const client = new BnfClient();
168
+ const { data, retrievedAt } = await client.searchAuthors("Rimbaud", 10, 0);
169
+ for (const author of data.rows) console.log(author.id, author.name, author.birthYear);
170
+ console.log("retrieved", retrievedAt);
171
+ ```
172
+
173
+ ## Development
174
+
175
+ ```bash
176
+ npm install
177
+ npm test # unit tests, against generated fixtures, no network
178
+ npm run typecheck
179
+ npm run build
180
+ BNF_LIVE=1 npm run test:live # one real query per route
181
+ ```
182
+
183
+ The unit tests reach no network. Fixtures are generated by
184
+ `scripts/build-fixtures.mjs` from invented records, so no BnF content lives in
185
+ this repository and every test is reproducible. The live suite runs nightly as a
186
+ canary, and it is the only thing that would notice the day the catalogue changes
187
+ shape.
188
+
189
+ ## Contributing
190
+
191
+ [CONTRIBUTING.md](CONTRIBUTING.md). Reports of a wrong answer are the most useful
192
+ kind: say what you asked, what came back, and what the record actually holds.
193
+
194
+ ## Licence
195
+
196
+ MIT for this code. See [LICENSE](LICENSE).
197
+
198
+ The metadata belong to the Bibliothèque nationale de France and are published
199
+ under the condition quoted above: name the source, and state the date of
200
+ retrieval.
201
+
202
+ ---
203
+
204
+ # mcp-databnf (français)
205
+
206
+ Un serveur MCP pour [data.bnf.fr](https://data.bnf.fr), le catalogue ouvert de la
207
+ Bibliothèque nationale de France. Chercher un auteur, trouver une œuvre, lister
208
+ les éditions que la BnF en conserve, et rassembler les liens vers ce qui a été
209
+ numérisé.
210
+
211
+ Sans clé d'API. Sans compte. En lecture seule.
212
+
213
+ ## À quoi il sert
214
+
215
+ La BnF publie son fichier d'autorité et ses notices bibliographiques en données
216
+ liées, et répond aux questions qu'on lui pose en SPARQL. Ce jeu de données sait
217
+ des choses qu'une recherche sur le web ignore : lequel de deux homonymes a écrit
218
+ tel livre, ce que la BnF a enregistré comme date et lieu de mort de quelqu'un,
219
+ quelles éditions d'une œuvre existent et qui les a imprimées, et lesquelles ont
220
+ été numérisées.
221
+
222
+ Ce serveur pose ces questions pour vous, en six outils, et rapporte ce que le
223
+ catalogue répond sans y ajouter.
224
+
225
+ ## Installation
226
+
227
+ ```bash
228
+ npx mcp-databnf
229
+ ```
230
+
231
+ ```json
232
+ {
233
+ "mcpServers": {
234
+ "databnf": {
235
+ "command": "npx",
236
+ "args": ["-y", "mcp-databnf"]
237
+ }
238
+ }
239
+ }
240
+ ```
241
+
242
+ ## Les six outils
243
+
244
+ | Outil | Répond à |
245
+ | ---------------- | --------------------------------------------------------------------------------------------------------------------- |
246
+ | `search_authors` | Qui la BnF enregistre sous ce nom, et quelle notice est laquelle |
247
+ | `get_author` | Dates, lieux, profession, langue, pays, indice Dewey, et la même personne dans VIAF, IdRef, DBpedia, Wikidata et ISNI |
248
+ | `search_works` | Quelles œuvres portent ces mots dans leur titre |
249
+ | `get_work` | Titre, auteurs, date, langue, forme, sujet, et si la notice est établie ou provisoire |
250
+ | `list_editions` | Éditeur, lieu, année, mention d'édition, pagination, ISBN, lien catalogue, et le lien Gallica quand il existe |
251
+ | `find_digitised` | Tous les documents numérisés que le catalogue rattache à une personne ou à une œuvre, sous forme de liens |
252
+
253
+ ## Ce qu'il ne fait pas, et pourquoi
254
+
255
+ **Il ne lit jamais Gallica.** La BnF place ses métadonnées et ses contenus
256
+ numérisés sous deux régimes différents. Les métadonnées lues ici sont
257
+ réutilisables librement à condition d'en citer la source et la date de
258
+ récupération. Les contenus de gallica.bnf.fr relèvent d'un autre régime : leurs
259
+ conditions soumettent l'usage dans un projet d'intelligence artificielle à une
260
+ licence payante hors recherche académique, et le site refuse ClaudeBot et GPTBot
261
+ au niveau du serveur, puis bannit l'adresse appelante après une quinzaine de
262
+ requêtes, quel que soit le rythme.
263
+
264
+ Une adresse Gallica est donc traitée ici pour ce que le catalogue en dit : une
265
+ métadonnée, rendue comme un lien qu'une personne ouvrira. Le serveur vous dira
266
+ qu'un tirage bruxellois de 1873 d'_Une saison en enfer_ a été numérisé et vous en
267
+ donnera l'adresse. Il ne vous dira pas ce qu'il y a page quatre, si la
268
+ numérisation est complète, ni si le document s'ouvre. Un lien `bnf-onto:OCR`
269
+ désigne un texte océrisé : le serveur signale qu'il existe et le laisse où il
270
+ est. Un test échoue si une adresse sur cet hôte est un jour construite pour être
271
+ appelée.
272
+
273
+ **Il ne classe pas.** L'index plein texte de la BnF répond si un titre ou un nom
274
+ porte les mots demandés. Il ne rend aucune mesure de pertinence, donc les lignes
275
+ arrivent dans l'ordre de l'index. Chercher `saison enfer` rend une douzaine
276
+ d'études sur Rimbaud avant Rimbaud, et chacune est une correspondance correcte.
277
+ Ce serveur le dit, plutôt que d'inventer un ordre que le catalogue ne porte pas,
278
+ et il ne rapporte aucun total : sur une recherche qui ne classe pas, un total se
279
+ lit comme une mesure de pertinence.
280
+
281
+ **Il n'écrit pas de biographies.** Le champ que la BnF appelle information
282
+ biographique contient une profession sur la plupart des notices : celle de
283
+ Rimbaud dit _Poète_, et c'est tout. `get_author` rend ce mot et dit ce que c'est.
284
+
285
+ **Il n'expose aucun outil SPARQL brut.** Une requête arbitraire est une charge
286
+ non bornée sur un service qu'une institution publique paie, et rien ici ne
287
+ contrôlerait ce que l'appelant a écrit. Chacune des requêtes envoyées est l'une
288
+ des neuf écrites à l'avance.
289
+
290
+ ## La licence, et ce qu'elle vous demande
291
+
292
+ La BnF pose une condition :
293
+
294
+ > L'utilisation de ces métadonnées est libre et gratuite sous réserve du maintien
295
+ > de la mention de leur source et de l'indication de leur date de récupération.
296
+
297
+ La date de récupération est une contrainte de conception : chaque réponse porte `retrieved_at` dans sa charge structurée et termine
298
+ son bloc de texte par la source et cette date. Une réponse servie depuis le cache
299
+ rapporte le moment où elle a été lue la première fois, puisque c'est là qu'elle a
300
+ été récupérée. Reprenez les deux partout où vous montrez ce que vous avez obtenu.
301
+
302
+ ## Le rythme
303
+
304
+ Une requête à la fois, jamais en parallèle. Au moins trois secondes entre deux
305
+ requêtes : la configuration peut élargir cet intervalle et ne peut pas le
306
+ réduire, y compris par le point d'entrée `client` publié. Le `User-Agent` porte
307
+ toujours l'identifiant du projet et une adresse où joindre une personne. Les
308
+ réponses sont gardées quinze minutes en mémoire.
309
+
310
+ La BnF ne publie aucune limite pour ce point d'accès. Elle publie `Crawl-delay: 5`
311
+ sur son autre hôte et l'y fait respecter, ce qui est le seul chiffre qu'elle ait
312
+ énoncé sur la vitesse à laquelle elle veut être lue.
313
+
314
+ ## Réglages
315
+
316
+ Tous facultatifs : `BNF_USER_AGENT`, `BNF_MIN_INTERVAL_MS` (3000, plancher
317
+ infranchissable), `BNF_TIMEOUT_MS` (60000), `BNF_MAX_RETRIES` (3),
318
+ `BNF_CACHE_TTL_MS` (900000), `BNF_CACHE_MAX_ENTRIES` (200), `BNF_LOG_LEVEL`
319
+ (`error`). Une valeur illisible est refusée, signalée sur stderr, et la valeur
320
+ par défaut s'applique.
321
+
322
+ ## Erreurs
323
+
324
+ `not_found`, `invalid_input`, `rate_limited`, `parse_failure`, `network_error`,
325
+ `timeout`. `rate_limited` ne veut jamais dire que la notice est absente.
326
+ `timeout` non plus : le point d'accès répond 200 avec un corps vide quand il
327
+ abandonne une requête en cours de route, et ce serveur appelle cela un échec de
328
+ lecture plutôt qu'une absence, parce que les deux se ressemblent et veulent dire
329
+ le contraire.
330
+
331
+ ## Licence
332
+
333
+ MIT pour ce code. Les métadonnées appartiennent à la Bibliothèque nationale de
334
+ France et sont publiées sous la condition citée plus haut : citer la source, et
335
+ indiquer la date de récupération.