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 +35 -0
- package/README.md +31 -13
- package/dist/index.js +27 -16
- package/dist/index.js.map +1 -1
- package/dist/loc/client.js +1 -1
- package/dist/loc/client.js.map +1 -1
- package/package.json +1 -1
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
|
|
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
|
-
|
|
72
|
-
matched
|
|
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.
|
|
105
|
-
|
|
106
|
-
|
|
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é
|
|
342
|
-
|
|
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
|
-
|
|
347
|
-
|
|
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
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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 = "
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
|
|
1535
|
-
"
|
|
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("
|
|
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
|
-
`
|
|
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
|
|
1609
|
-
|
|
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 ${
|
|
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
|
-
"
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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."
|