mcp-libraryofcongress 1.1.0 → 2.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 CHANGED
@@ -1,5 +1,40 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.0.0
4
+
5
+ ### Breaking change
6
+
7
+ `search_newspapers` no longer returns `words_located`. Each match now carries
8
+ `excerpt_kind`, one of `"passage"` or `"page_opening"`. A caller reading
9
+ `words_located === true` reads `excerpt_kind === "passage"`, and the false case
10
+ reads `"page_opening"`.
11
+
12
+ The boolean and the excerpt disagreed about what they were. `words_located:
13
+ false` means the machine-read text the Library returned stops before the
14
+ searched words, so the excerpt beside it is the **opening of the page** and does
15
+ not carry the match. It sat in the answer in the same place and the same shape
16
+ as a real passage, and a note beside it was the only thing saying otherwise.
17
+ Measured on a live search, four matches in five were openings. Naming the kind
18
+ of thing an excerpt is puts that where it cannot be skipped.
19
+
20
+ ### Also in this release
21
+
22
+ - Every excerpt is now labelled in the rendered text itself, as
23
+ `[passage]` or `[page opening]` ahead of the words, so the mark travels with
24
+ the text a reader copies.
25
+ - The tool described quoting as matching a phrase whole. It does not. Measured:
26
+ `"hell is other people"` quoted returns 18 pages, unquoted 1,634,731, and
27
+ among the quoted results are pages from 1891, 1897 and 1906, which precede the
28
+ line by half a century. Quoting narrows hard and decides nothing about how the
29
+ Library matches. The promise is gone from the tool description and from both
30
+ halves of the README, and a quoted query now carries a note saying the match
31
+ is not guaranteed to be the phrase.
32
+ - Headings said the pages carry the query. They say what the Library matched.
33
+
34
+ `NewspaperHit.wordsLocated`, on the `./client` subpath, is unchanged: it is the
35
+ raw fact about the text received, and the tool layer names the kind of thing it
36
+ renders from it.
37
+
3
38
  ## 1.1.0
4
39
 
5
40
  - An argument no tool declared was read and dropped, and the answer came back
package/README.md CHANGED
@@ -64,12 +64,15 @@ The server is **read-only**. It uploads nothing and writes nothing back.
64
64
  A catalogue search reads titles, creators and descriptions. `search_newspapers`
65
65
  reads what optical character recognition took off millions of scanned pages of
66
66
  American newspapers, so it answers a question nothing else here can: _which
67
- issue printed this phrase_. A match comes back with the paper, the date, the
67
+ issue printed these words_. A match comes back with the paper, the date, the
68
68
  leaf of the issue, the state it was published in, and an address that opens that
69
69
  leaf with the query applied.
70
70
 
71
- Put a phrase in double quotes to match it whole. Without quotes the words are
72
- matched separately, which finds far more.
71
+ Double quotes narrow the search sharply, and the Library decides what they mean:
72
+ a matched page can carry the words apart or in another order rather than the
73
+ phrase as written, so an answer to a quoted query says so and points at the page
74
+ to read. Without quotes the words are matched separately, which finds far more
75
+ again.
73
76
 
74
77
  The corpus spans every state and a century and a half, so a bare phrase reaches
75
78
  a great deal that a question did not ask for. Three arguments narrow it:
@@ -101,9 +104,14 @@ page 2, 3 and so on rather than treating the first answer as the whole of it.
101
104
 
102
105
  **An excerpt is sometimes the opening of the page.** The Library returns the
103
106
  beginning of a page's text with each row rather than the whole page, so the
104
- searched words are often further down than that text reaches. `words_located`
105
- says which of the two happened: true and the passage is centred on the words,
106
- false and it is the opening of the leaf. Follow `source_url` for the rest.
107
+ searched words are often further down than that text reaches. Every match
108
+ carries `excerpt_kind`, and every excerpt is labelled with it in the text block:
109
+
110
+ - **`passage`**, the text around the words that matched, centred on them.
111
+ - **`page_opening`**, the start of the leaf, sent because the text that came
112
+ back stops before the searched words appear. It does not carry the match, so
113
+ quoting it quotes something else. The notes count how many there are, and
114
+ `source_url` opens the leaf with the query applied.
107
115
 
108
116
  **Scanned text is machine-read.** Excerpts carry the misreadings that come with
109
117
  it. Quote them as scanned text and link the page.
@@ -338,13 +346,17 @@ Le serveur est **en lecture seule**. Il ne téléverse rien et n'écrit rien.
338
346
  Une recherche de catalogue lit les titres, les auteurs et les descriptions.
339
347
  `search_newspapers` lit ce que la reconnaissance de caractères a tiré de
340
348
  millions de pages de journaux américains numérisées, et répond donc à une
341
- question qu'aucun autre outil ici ne sait traiter : _quel numéro a imprimé cette
342
- phrase_. Une correspondance revient avec le journal, la date, le feuillet du
349
+ question qu'aucun autre outil ici ne sait traiter : _quel numéro a imprimé ces
350
+ mots_. Une correspondance revient avec le journal, la date, le feuillet du
343
351
  numéro, l'État de publication, et une adresse qui ouvre ce feuillet avec la
344
352
  requête appliquée.
345
353
 
346
- Mettez une phrase entre guillemets doubles pour la faire correspondre en entier.
347
- Sans guillemets, les mots sont cherchés séparément, ce qui trouve bien plus.
354
+ Les guillemets doubles resserrent fortement la recherche, et c'est la Library
355
+ qui décide de ce qu'ils veulent dire : une page retenue peut porter les mots
356
+ éloignés les uns des autres ou dans un autre ordre plutôt que la phrase telle
357
+ qu'elle est écrite. La réponse à une requête entre guillemets le dit et renvoie
358
+ à la page à lire. Sans guillemets, les mots sont cherchés séparément, ce qui
359
+ trouve bien plus encore.
348
360
 
349
361
  Le corpus couvre tous les États et un siècle et demi : une phrase seule ramène
350
362
  donc beaucoup de choses que la question ne demandait pas. Trois arguments la
@@ -379,9 +391,15 @@ que de prendre la première réponse pour la totalité.
379
391
  **Un extrait est parfois le début de la page.** La Library renvoie le début du
380
392
  texte d'une page avec chaque ligne de résultat plutôt que la page entière, si
381
393
  bien que les mots cherchés se trouvent souvent plus bas que ce texte ne va.
382
- `words_located` indique lequel des deux cas s'est produit : vrai et le passage
383
- est centré sur les mots, faux et c'est le début du feuillet. Suivez `source_url`
384
- pour la suite.
394
+ Chaque correspondance porte `excerpt_kind`, et chaque extrait en porte
395
+ l'étiquette dans le bloc de texte :
396
+
397
+ - **`passage`** : le texte autour des mots correspondants, centré sur eux.
398
+ - **`page_opening`** : le début du feuillet, renvoyé parce que le texte reçu
399
+ s'arrête avant que les mots cherchés n'apparaissent. Il ne porte pas la
400
+ correspondance, donc le citer revient à citer autre chose. Les notes comptent
401
+ combien il y en a, et `source_url` ouvre le feuillet avec la requête
402
+ appliquée.
385
403
 
386
404
  **Le texte numérisé est lu par une machine.** Les extraits en portent les
387
405
  fautes. Citez-les comme tels et suivez le lien.
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
7
7
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8
8
 
9
9
  // src/version.ts
10
- var PKG_VERSION = "1.1.0";
10
+ var PKG_VERSION = "2.0.0";
11
11
  var REPO_URL = "https://github.com/smeet666/mcp-libraryofcongress";
12
12
 
13
13
  // src/config.ts
@@ -1489,11 +1489,11 @@ import { z as z6 } from "zod";
1489
1489
  var searchNewspapersDescription = [
1490
1490
  "Search the text inside digitised American newspaper pages held by the Library of Congress.",
1491
1491
  "This reads what optical recognition took off the scanned pages, so it finds a phrase that appears nowhere in a title or a catalogue record.",
1492
- "Put a phrase in double quotes to match it whole; without quotes the words are matched separately, which finds far more.",
1492
+ "Double quotes narrow the search sharply, and the Library decides how it matches what is inside them: a page can come back carrying the words apart or in another order rather than the phrase as written. Without quotes the words are matched separately, which finds far more again.",
1493
1493
  "'total' counts the pages that match, and they page: ask for page 2, 3 and so on to see beyond the first answer. It is not a count of how many times the words occur.",
1494
1494
  "Each match names the newspaper, the date, the leaf of the issue and the state it was published in, and 'source_url' opens that leaf with the query applied.",
1495
1495
  "'location' keeps to papers published in one state, 'publication' to a single paper, and 'year_from' with 'year_to' to a span of years. A filter matching nothing is dropped and the answer says so.",
1496
- "The Library returns the opening of a page's text with each row rather than the whole page, so the searched words are often further down than the excerpts reach: 'words_located' says which of the two happened for each match.",
1496
+ "Every match carries 'excerpt_kind', and the excerpts are labelled with it in the text. A 'passage' is the text around the words that matched. A 'page_opening' is the start of the page, sent because the text the Library returned with the row stops before those words appear, so it does not carry the match and quoting it quotes something else.",
1497
1497
  "Use search_items instead when looking for a work by its title, creator or subject."
1498
1498
  ].join(" ");
1499
1499
  var searchNewspapersInput = strictInput({
@@ -1531,15 +1531,16 @@ var searchNewspapersOutput = z6.object({
1531
1531
  published_on: z6.string().nullable().describe("Date of the issue, as published."),
1532
1532
  publication: z6.string().nullable().describe("The newspaper, with the years it ran."),
1533
1533
  state: z6.string().nullable().describe("Where the paper was published."),
1534
- words_located: z6.boolean().describe(
1535
- "True when the searched words were found in the text returned with this row, in which case the excerpts are centred on them. False when they sit further down the page than that text reaches, in which case the excerpts are its opening."
1534
+ excerpt_kind: z6.enum(["passage", "page_opening"]).describe(
1535
+ "'passage' means the excerpts are the text around the words that matched, centred on them because they were found in the text returned with this row. 'page_opening' means they are the start of the page, sent because that text stops before the searched words appear, so a page_opening does not carry the match."
1536
1536
  ),
1537
- excerpts: z6.array(z6.string()).describe("Passages as a machine read them off the page."),
1537
+ excerpts: z6.array(z6.string()).describe("Machine-read text off the page, all of the kind 'excerpt_kind' names."),
1538
1538
  source_url: z6.string().describe("Opens the leaf itself, with the query applied.")
1539
1539
  })
1540
1540
  ),
1541
1541
  notes: z6.array(z6.string())
1542
1542
  });
1543
+ var QUOTED_PHRASE = /"[^"]+"/;
1543
1544
  function describeNarrowing2(args) {
1544
1545
  const written = [];
1545
1546
  if (args.location) written.push(`location="${args.location}"`);
@@ -1578,7 +1579,7 @@ async function runSearchNewspapers(client, args) {
1578
1579
  const wider = await client.searchNewspapers(args.query, args.limit, args.page, budget);
1579
1580
  if (wider.data.paging.resultCount > 0) {
1580
1581
  notes.push(
1581
- `No page carrying ${args.query} matches ${narrowing.join(", ")}. Those were set aside and the search was asked again without them, so the matches below are unfiltered. The corpus uses its own wording: read 'state' and 'publication' on a match to see the form it expects.`
1582
+ `The Library matched no page for ${args.query} under ${narrowing.join(", ")}. Those were set aside and the search was asked again without them, so the matches below are unfiltered. The corpus uses its own wording: read 'state' and 'publication' on a match to see the form it expects.`
1582
1583
  );
1583
1584
  result = wider;
1584
1585
  }
@@ -1592,6 +1593,7 @@ async function runSearchNewspapers(client, args) {
1592
1593
  }
1593
1594
  const hits = data.hits.map((hit) => ({
1594
1595
  identifier: hit.identifier,
1596
+ excerpt_kind: hit.wordsLocated ? "passage" : "page_opening",
1595
1597
  title: hit.title,
1596
1598
  creator: hit.creator,
1597
1599
  year: hit.year,
@@ -1599,16 +1601,22 @@ async function runSearchNewspapers(client, args) {
1599
1601
  published_on: hit.publishedOn,
1600
1602
  publication: hit.publication,
1601
1603
  state: hit.state,
1602
- words_located: hit.wordsLocated,
1603
1604
  excerpts: hit.excerpts.map((excerpt) => truncate(excerpt, args.max_excerpt_chars + 2)),
1604
1605
  source_url: hit.sourceUrl
1605
1606
  }));
1606
1607
  const total = data.paging.resultCount;
1607
1608
  if (hits.length > 0) notes.push(OCR_CAVEAT);
1608
- const elsewhere = hits.filter((hit) => !hit.words_located).length;
1609
- if (elsewhere > 0) {
1609
+ const openings = hits.filter(
1610
+ (hit) => hit.excerpt_kind === "page_opening" && hit.excerpts.length > 0
1611
+ ).length;
1612
+ if (openings > 0) {
1610
1613
  notes.push(
1611
- `On ${elsewhere} of ${hits.length} match(es) the searched words sit further down the page than the text returned with the row, so those excerpts are the opening of the page rather than the passage that matched. Follow source_url, which opens the leaf with the query applied.`
1614
+ `On ${openings} of ${hits.length} match(es) the searched words sit further down the page than the text returned with the row, so those excerpts are the opening of the page rather than the passage that matched, and each is labelled [page opening]. Quoting one of them quotes something else: follow source_url, which opens the leaf with the query applied.`
1615
+ );
1616
+ }
1617
+ if (hits.length > 0 && QUOTED_PHRASE.test(args.query)) {
1618
+ notes.push(
1619
+ "The query carries double quotes. That narrows the search sharply, and the Library decides what the quotes mean: it does not guarantee that a matched page carries the words together in that order, so a match can be a page where they sit apart. Read the page behind source_url before repeating the query as the phrase it printed."
1612
1620
  );
1613
1621
  }
1614
1622
  if (total > hits.length) {
@@ -1618,7 +1626,7 @@ async function runSearchNewspapers(client, args) {
1618
1626
  }
1619
1627
  if (total === 0) {
1620
1628
  notes.push(
1621
- "No digitised newspaper page carries these words. An unquoted query matches the words separately, which usually finds more."
1629
+ "The Library matched no digitised newspaper page for these words. An unquoted query matches the words separately, which usually finds more."
1622
1630
  );
1623
1631
  }
1624
1632
  if (hits.length === 0 && total > 0) {
@@ -1626,7 +1634,7 @@ async function runSearchNewspapers(client, args) {
1626
1634
  `Page ${args.page} is past the last match. ${total} pages match, so ask for a lower page.`
1627
1635
  );
1628
1636
  }
1629
- const body = hits.length === 0 ? total > 0 ? `Page ${args.page} is past the last of ${total} newspaper pages carrying ${args.query}.` : `Nothing found in the scanned newspapers for ${args.query}.` : `${hits.length} of ${total} newspaper pages carrying ${args.query}:
1637
+ const body = hits.length === 0 ? total > 0 ? `Page ${args.page} is past the last of ${total} newspaper pages the Library matched for ${args.query}.` : `Nothing found in the scanned newspapers for ${args.query}.` : `${hits.length} of ${total} newspaper pages the Library matched for ${args.query}:
1630
1638
  ` + hits.map((hit, index) => {
1631
1639
  const where = [
1632
1640
  `${index + 1}. ${hit.publication ?? hit.title ?? "untitled"}`,
@@ -1634,7 +1642,8 @@ async function runSearchNewspapers(client, args) {
1634
1642
  hit.page_number === null ? "" : `\xB7 page ${hit.page_number}`,
1635
1643
  hit.state ? `\xB7 ${hit.state}` : ""
1636
1644
  ].filter(Boolean).join(" ");
1637
- const passages = hit.excerpts.map((excerpt) => ` ${excerpt}`).join("\n");
1645
+ const label = hit.excerpt_kind === "page_opening" ? "[page opening]" : "[passage]";
1646
+ const passages = hit.excerpts.map((excerpt) => ` ${label} ${excerpt}`).join("\n");
1638
1647
  return `${where}
1639
1648
  ${passages}
1640
1649
  ${hit.source_url}`;
@@ -1655,11 +1664,13 @@ var READ_ONLY = {
1655
1664
  var INSTRUCTIONS = [
1656
1665
  "Tools for the Library of Congress. No API key and no account are needed.",
1657
1666
  "Two questions lead to two different tools, and choosing the wrong one gives a confident empty answer.",
1658
- "To find a phrase printed on a page of an American newspaper, use search_newspapers: it reads the text scanned off the pages and returns the paper, the date, the leaf and the passage.",
1667
+ "To find words printed on a page of an American newspaper, use search_newspapers: it reads the text scanned off the pages and returns the paper, the date, the leaf and what it holds of that text.",
1659
1668
  "To find a work by its title, creator or subject, use search_items, which requires a 'media_type' because the Library keeps a separate catalogue per kind of thing.",
1660
1669
  "list_collections shows the corpora a curator built, and each row carries the wording search_items takes as its 'collection' filter.",
1661
1670
  "search_newspapers reports 'total' as the number of pages that match, and they page: ask for the next page rather than treating the first answer as the whole of it.",
1662
- "Excerpts come from optical character recognition, so the words can be wrong; repeat them as scanned text and link the page. When 'words_located' is false the searched words sit further down the page than the text this server received, and the excerpt is the opening of that page.",
1671
+ "Excerpts come from optical character recognition, so the words can be wrong; repeat them as scanned text and link the page.",
1672
+ "Every match carries 'excerpt_kind', and the excerpts are labelled with it in the text. A 'passage' is the text around the words that matched. A 'page_opening' is the start of the page, sent because the text this server received stops before those words appear, so it does not carry the match and citing it as the match cites the wrong words. The notes count the openings.",
1673
+ "Double quotes narrow a newspaper search sharply, and the Library decides what they mean: a matched page can carry the words apart or in another order rather than the phrase as written, so read the page behind source_url before saying it printed the phrase.",
1663
1674
  "Metadata here is in the public domain, and individual items are not: a record states its own terms, and a record stating none is not a record granting permission.",
1664
1675
  "This server paces itself to the ceiling the Library publishes, so answers take a few seconds. A rate_limited error means it was asked to slow down, never that the thing you asked for is missing.",
1665
1676
  "Every result carries a source_url. Credit the Library of Congress and link what you use."