mcp-libraryofcongress 2.1.0 → 2.1.2
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 +29 -0
- package/README.md +13 -4
- package/dist/index.js +44 -19
- package/dist/index.js.map +1 -1
- package/dist/loc/client.js +44 -19
- package/dist/loc/client.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.1.2
|
|
4
|
+
|
|
5
|
+
- A search the Library could not run is reported as a failure instead of as an
|
|
6
|
+
empty catalogue. Under load the site answers with a rendered page holding no
|
|
7
|
+
rows and a pagination block counting zero results, written in exactly the
|
|
8
|
+
words a search that genuinely matched nothing is written in: the collections
|
|
9
|
+
listing, which holds 584 collections and takes no query at all, came back as
|
|
10
|
+
`of: 0` beside an empty `results`, and the server published that as the
|
|
11
|
+
Library having gathered no collection. What separates the two is what the site
|
|
12
|
+
says about keeping the answer. A search it stands behind may be kept for a
|
|
13
|
+
day; the page it renders while its search is failing carries `no-cache` and a
|
|
14
|
+
lifetime of zero. A count of zero read off such a page is now `rate_limited`,
|
|
15
|
+
which says the site could not answer and says nothing about what the Library
|
|
16
|
+
holds. The catalogue, newspaper and collections searches all read their count
|
|
17
|
+
from that block, so all three are covered. A search that really matches
|
|
18
|
+
nothing still answers with a count of zero.
|
|
19
|
+
- The nightly live canary tells a contract that moved apart from a site that
|
|
20
|
+
could not answer. A route that came back rate limited, unreachable or out of
|
|
21
|
+
time leaves that case out of the run and names it on stderr, so only an answer
|
|
22
|
+
this server could not read fails the canary and opens an issue. Each assertion
|
|
23
|
+
states the field or the behaviour it holds the site to.
|
|
24
|
+
|
|
25
|
+
## 2.1.1
|
|
26
|
+
|
|
27
|
+
- The README carries the same badge row as every server here: npm, CI, the
|
|
28
|
+
licence, the MCP registry entry, the Glama score, and one-click installs for
|
|
29
|
+
Cursor and VS Code. Each install link encodes this package. npm serves the
|
|
30
|
+
README frozen at publish time, so a release is what puts it there.
|
|
31
|
+
|
|
3
32
|
## 2.1.0
|
|
4
33
|
|
|
5
34
|
- A catalogue row pointing at a collection no longer claims an identifier
|
package/README.md
CHANGED
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/mcp-libraryofcongress)
|
|
4
4
|
[](https://github.com/smeet666/mcp-libraryofcongress/actions/workflows/ci.yml)
|
|
5
|
-
[](LICENSE)
|
|
5
|
+
[](./LICENSE)
|
|
6
6
|
[](https://registry.modelcontextprotocol.io/v0/servers?search=io.github.smeet666/mcp-libraryofcongress)
|
|
7
7
|
[](https://glama.ai/mcp/servers/smeet666/mcp-libraryofcongress)
|
|
8
|
+
[](https://m8ven.ai/mcp/smeet666-mcp-libraryofcongress-1rr3lc)
|
|
9
|
+
[](https://cursor.com/en/install-mcp?name=libraryofcongress&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIm1jcC1saWJyYXJ5b2Zjb25ncmVzcyJdfQ%3D%3D)
|
|
10
|
+
[](https://insiders.vscode.dev/redirect/mcp/install?name=libraryofcongress&config=%7B%22name%22%3A%22libraryofcongress%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22mcp-libraryofcongress%22%5D%7D)
|
|
8
11
|
|
|
9
12
|
An MCP server for the [Library of Congress](https://www.loc.gov). **Search the
|
|
10
13
|
text scanned off digitised American newspaper pages**, search the catalogue by
|
|
@@ -297,8 +300,11 @@ console.log(data.paging.resultCount, data.hits[0]?.sourceUrl);
|
|
|
297
300
|
## Troubleshooting
|
|
298
301
|
|
|
299
302
|
**`rate_limited`.** The site asked this client to slow down, or this server was
|
|
300
|
-
asked for more than its pacing allows. It
|
|
301
|
-
|
|
303
|
+
asked for more than its pacing allows. It also covers a search the site could
|
|
304
|
+
not run: it answers then with a page holding no rows and refuses to have that
|
|
305
|
+
page kept, and a count of nothing read off it would say the Library holds
|
|
306
|
+
nothing. It says nothing about whether the Library holds what you asked for.
|
|
307
|
+
Wait and ask again.
|
|
302
308
|
|
|
303
309
|
**`parse_failure`.** A response arrived in a shape this server cannot read,
|
|
304
310
|
which includes a long answer cut off in transit. It usually means a route
|
|
@@ -657,7 +663,10 @@ console.log(data.paging.resultCount, data.hits[0]?.sourceUrl);
|
|
|
657
663
|
## Dépannage
|
|
658
664
|
|
|
659
665
|
**`rate_limited`.** Le site demande à ce client de ralentir, ou ce serveur a été
|
|
660
|
-
sollicité au-delà de sa cadence.
|
|
666
|
+
sollicité au-delà de sa cadence. Ce code couvre aussi une recherche que le site
|
|
667
|
+
n'a pas pu exécuter : il répond alors par une page sans aucune ligne et refuse
|
|
668
|
+
qu'elle soit conservée, et un compte de zéro lu sur cette page affirmerait que
|
|
669
|
+
la Library ne conserve rien. Cela ne dit rien de ce que la Library conserve.
|
|
661
670
|
Attendez et redemandez.
|
|
662
671
|
|
|
663
672
|
**`parse_failure`.** Une réponse est arrivée dans une forme illisible pour ce
|
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 = "2.1.
|
|
10
|
+
var PKG_VERSION = "2.1.2";
|
|
11
11
|
var REPO_URL = "https://github.com/smeet666/mcp-libraryofcongress";
|
|
12
12
|
|
|
13
13
|
// src/config.ts
|
|
@@ -168,6 +168,12 @@ function backoffMs(attempt) {
|
|
|
168
168
|
const base2 = Math.min(16e3, 1e3 * 2 ** attempt);
|
|
169
169
|
return base2 + Math.floor(Math.random() * 500);
|
|
170
170
|
}
|
|
171
|
+
function statesASettledAnswer(cacheControl) {
|
|
172
|
+
if (cacheControl === null) return true;
|
|
173
|
+
const directives = cacheControl.toLowerCase().split(",").map((directive) => directive.trim());
|
|
174
|
+
if (directives.includes("no-cache") || directives.includes("no-store")) return false;
|
|
175
|
+
return !directives.some((directive) => /^(?:max-age|s-maxage)\s*=\s*0$/.test(directive));
|
|
176
|
+
}
|
|
171
177
|
async function fetchText(options) {
|
|
172
178
|
const { url, userAgent, timeoutMs, maxRetries, limiter, logger } = options;
|
|
173
179
|
const doFetch = options.fetchImpl ?? fetch;
|
|
@@ -191,7 +197,8 @@ async function fetchText(options) {
|
|
|
191
197
|
});
|
|
192
198
|
if (response.ok) {
|
|
193
199
|
limiter.succeeded();
|
|
194
|
-
|
|
200
|
+
const settled = statesASettledAnswer(response.headers.get("cache-control"));
|
|
201
|
+
return { payload: await response.text(), settled };
|
|
195
202
|
}
|
|
196
203
|
if (PUSH_BACK.has(response.status)) {
|
|
197
204
|
limiter.pushBack();
|
|
@@ -267,9 +274,9 @@ async function fetchText(options) {
|
|
|
267
274
|
);
|
|
268
275
|
}
|
|
269
276
|
async function fetchJson(options) {
|
|
270
|
-
const
|
|
277
|
+
const { payload, settled } = await fetchText(options);
|
|
271
278
|
try {
|
|
272
|
-
return JSON.parse(
|
|
279
|
+
return { payload: JSON.parse(payload), settled };
|
|
273
280
|
} catch {
|
|
274
281
|
throw parseFailure("The Library of Congress answered with something that is not JSON.", {
|
|
275
282
|
url: options.url
|
|
@@ -648,7 +655,7 @@ function dateCode(filed) {
|
|
|
648
655
|
function plainText(value) {
|
|
649
656
|
return value.replace(/<[^>]*>/g, "").replace(/&/g, "&").replace(/"/g, '"').replace(/�?39;|'/g, "'").replace(/</g, "<").replace(/>/g, ">").replace(/ /g, " ").replace(/\s+/g, " ").trim();
|
|
650
657
|
}
|
|
651
|
-
function toPaging(payload, url) {
|
|
658
|
+
function toPaging(payload, url, settled = true) {
|
|
652
659
|
const root = asObject(payload);
|
|
653
660
|
const pagination = root ? asObject(root.pagination) : null;
|
|
654
661
|
if (!pagination) {
|
|
@@ -663,6 +670,12 @@ function toPaging(payload, url) {
|
|
|
663
670
|
if (resultCount === null) {
|
|
664
671
|
throw parseFailure("The answer carried no readable count of results.", { url });
|
|
665
672
|
}
|
|
673
|
+
if (resultCount === 0 && !settled) {
|
|
674
|
+
throw rateLimited(
|
|
675
|
+
"The Library of Congress answered with no results and asked that the answer not be kept, which is how the site answers while its search is failing.",
|
|
676
|
+
{ url }
|
|
677
|
+
);
|
|
678
|
+
}
|
|
666
679
|
return {
|
|
667
680
|
resultCount,
|
|
668
681
|
pageCount: asNumber(pagination.total),
|
|
@@ -695,7 +708,7 @@ function statedDateOfRow(row) {
|
|
|
695
708
|
const both = [...words, ...spans];
|
|
696
709
|
return both.length > 0 ? both.join("; ") : null;
|
|
697
710
|
}
|
|
698
|
-
function toSearchResults(payload, url, onSkip) {
|
|
711
|
+
function toSearchResults(payload, url, onSkip, settled = true) {
|
|
699
712
|
const rows = rowsOf(payload, url);
|
|
700
713
|
const records = [];
|
|
701
714
|
let skipped = 0;
|
|
@@ -730,7 +743,7 @@ function toSearchResults(payload, url, onSkip) {
|
|
|
730
743
|
if (rows.length > 0 && records.length === 0) {
|
|
731
744
|
throw parseFailure(`${rows.length} rows came back and none could be read.`, { url });
|
|
732
745
|
}
|
|
733
|
-
return { paging: toPaging(payload, url), records };
|
|
746
|
+
return { paging: toPaging(payload, url, settled), records };
|
|
734
747
|
}
|
|
735
748
|
var JOINING_WORDS = /* @__PURE__ */ new Set([
|
|
736
749
|
"the",
|
|
@@ -826,7 +839,7 @@ function excerptsFor(text, terms, budget) {
|
|
|
826
839
|
});
|
|
827
840
|
return { passages, located: true };
|
|
828
841
|
}
|
|
829
|
-
function toNewspaperResults(payload, url, query, budget, onSkip) {
|
|
842
|
+
function toNewspaperResults(payload, url, query, budget, onSkip, settled = true) {
|
|
830
843
|
const rows = rowsOf(payload, url);
|
|
831
844
|
const terms = queryTerms(query);
|
|
832
845
|
const hits = [];
|
|
@@ -862,7 +875,7 @@ function toNewspaperResults(payload, url, query, budget, onSkip) {
|
|
|
862
875
|
if (rows.length > 0 && hits.length === 0) {
|
|
863
876
|
throw parseFailure(`${rows.length} matches came back and none could be read.`, { url });
|
|
864
877
|
}
|
|
865
|
-
return { paging: toPaging(payload, url), hits };
|
|
878
|
+
return { paging: toPaging(payload, url, settled), hits };
|
|
866
879
|
}
|
|
867
880
|
function toResources(value) {
|
|
868
881
|
if (!Array.isArray(value)) return [];
|
|
@@ -940,7 +953,7 @@ function toItemDetail(payload, identifier, url) {
|
|
|
940
953
|
raw: item
|
|
941
954
|
};
|
|
942
955
|
}
|
|
943
|
-
function toCollections(payload, url, onSkip) {
|
|
956
|
+
function toCollections(payload, url, onSkip, settled = true) {
|
|
944
957
|
const rows = rowsOf(payload, url);
|
|
945
958
|
const collections = [];
|
|
946
959
|
let skipped = 0;
|
|
@@ -968,7 +981,7 @@ function toCollections(payload, url, onSkip) {
|
|
|
968
981
|
if (rows.length > 0 && collections.length === 0) {
|
|
969
982
|
throw parseFailure(`${rows.length} collections came back and none could be read.`, { url });
|
|
970
983
|
}
|
|
971
|
-
return { paging: toPaging(payload, url), collections };
|
|
984
|
+
return { paging: toPaging(payload, url, settled), collections };
|
|
972
985
|
}
|
|
973
986
|
|
|
974
987
|
// src/loc/rateLimiter.ts
|
|
@@ -1091,7 +1104,7 @@ var LocClient = class {
|
|
|
1091
1104
|
return { data: cached, cached: true };
|
|
1092
1105
|
}
|
|
1093
1106
|
let skipped = 0;
|
|
1094
|
-
const payload = await this.limiter.schedule(
|
|
1107
|
+
const { payload, settled } = await this.limiter.schedule(
|
|
1095
1108
|
() => fetchJson({
|
|
1096
1109
|
url,
|
|
1097
1110
|
userAgent: this.config.userAgent,
|
|
@@ -1102,16 +1115,24 @@ var LocClient = class {
|
|
|
1102
1115
|
...this.fetchImpl ? { fetchImpl: this.fetchImpl } : {}
|
|
1103
1116
|
})
|
|
1104
1117
|
);
|
|
1105
|
-
const data = parse(
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1118
|
+
const data = parse(
|
|
1119
|
+
payload,
|
|
1120
|
+
(n) => {
|
|
1121
|
+
skipped += n;
|
|
1122
|
+
this.logger.warn(`skipped ${n} unreadable row(s) from ${url}`);
|
|
1123
|
+
},
|
|
1124
|
+
settled
|
|
1125
|
+
);
|
|
1109
1126
|
this.cache.set(cacheKey, data);
|
|
1110
1127
|
return skipped > 0 ? { data, cached: false, skipped } : { data, cached: false };
|
|
1111
1128
|
}
|
|
1112
1129
|
searchItems(query) {
|
|
1113
1130
|
const url = catalogueUrl(query);
|
|
1114
|
-
return this.read(
|
|
1131
|
+
return this.read(
|
|
1132
|
+
url,
|
|
1133
|
+
url,
|
|
1134
|
+
(payload, onSkip, settled) => toSearchResults(payload, url, onSkip, settled)
|
|
1135
|
+
);
|
|
1115
1136
|
}
|
|
1116
1137
|
searchNewspapers(query, limit, page, budget, filters = {}) {
|
|
1117
1138
|
const trimmed = query.trim();
|
|
@@ -1123,7 +1144,7 @@ var LocClient = class {
|
|
|
1123
1144
|
return this.read(
|
|
1124
1145
|
key,
|
|
1125
1146
|
url,
|
|
1126
|
-
(payload, onSkip) => toNewspaperResults(payload, url, trimmed, budget, onSkip),
|
|
1147
|
+
(payload, onSkip, settled) => toNewspaperResults(payload, url, trimmed, budget, onSkip, settled),
|
|
1127
1148
|
// Reading the text of millions of pages takes the site far longer than
|
|
1128
1149
|
// answering from the catalogue, so this route carries a budget of its own.
|
|
1129
1150
|
this.config.newspaperTimeoutMs
|
|
@@ -1139,7 +1160,11 @@ var LocClient = class {
|
|
|
1139
1160
|
}
|
|
1140
1161
|
listCollections(limit, page) {
|
|
1141
1162
|
const url = collectionsUrl(limit, page);
|
|
1142
|
-
return this.read(
|
|
1163
|
+
return this.read(
|
|
1164
|
+
url,
|
|
1165
|
+
url,
|
|
1166
|
+
(payload, onSkip, settled) => toCollections(payload, url, onSkip, settled)
|
|
1167
|
+
);
|
|
1143
1168
|
}
|
|
1144
1169
|
};
|
|
1145
1170
|
|