mcp-libraryofcongress 2.1.1 → 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 CHANGED
@@ -1,5 +1,27 @@
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
+
3
25
  ## 2.1.1
4
26
 
5
27
  - The README carries the same badge row as every server here: npm, CI, the
package/README.md CHANGED
@@ -5,6 +5,7 @@
5
5
  [![license](https://img.shields.io/npm/l/mcp-libraryofcongress.svg)](./LICENSE)
6
6
  [![MCP Registry](https://img.shields.io/badge/MCP_Registry-listed-6E56CF)](https://registry.modelcontextprotocol.io/v0/servers?search=io.github.smeet666/mcp-libraryofcongress)
7
7
  [![Glama](https://glama.ai/mcp/servers/smeet666/mcp-libraryofcongress/badges/score.svg)](https://glama.ai/mcp/servers/smeet666/mcp-libraryofcongress)
8
+ [![M8ven Score](https://m8ven.ai/badge/mcp/smeet666-mcp-libraryofcongress-1rr3lc)](https://m8ven.ai/mcp/smeet666-mcp-libraryofcongress-1rr3lc)
8
9
  [![Install in Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en/install-mcp?name=libraryofcongress&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIm1jcC1saWJyYXJ5b2Zjb25ncmVzcyJdfQ%3D%3D)
9
10
  [![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat&logo=visualstudiocode&logoColor=white)](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)
10
11
 
@@ -299,8 +300,11 @@ console.log(data.paging.resultCount, data.hits[0]?.sourceUrl);
299
300
  ## Troubleshooting
300
301
 
301
302
  **`rate_limited`.** The site asked this client to slow down, or this server was
302
- asked for more than its pacing allows. It says nothing about whether the Library
303
- holds what you asked for. Wait and ask again.
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.
304
308
 
305
309
  **`parse_failure`.** A response arrived in a shape this server cannot read,
306
310
  which includes a long answer cut off in transit. It usually means a route
@@ -659,7 +663,10 @@ console.log(data.paging.resultCount, data.hits[0]?.sourceUrl);
659
663
  ## Dépannage
660
664
 
661
665
  **`rate_limited`.** Le site demande à ce client de ralentir, ou ce serveur a été
662
- sollicité au-delà de sa cadence. Cela ne dit rien de ce que la Library conserve.
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.
663
670
  Attendez et redemandez.
664
671
 
665
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.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
- return await response.text();
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 body = await fetchText(options);
277
+ const { payload, settled } = await fetchText(options);
271
278
  try {
272
- return JSON.parse(body);
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(/&amp;/g, "&").replace(/&quot;/g, '"').replace(/&#0?39;|&apos;/g, "'").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&nbsp;/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(payload, (n) => {
1106
- skipped += n;
1107
- this.logger.warn(`skipped ${n} unreadable row(s) from ${url}`);
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(url, url, (payload, onSkip) => toSearchResults(payload, url, onSkip));
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(url, url, (payload, onSkip) => toCollections(payload, url, onSkip));
1163
+ return this.read(
1164
+ url,
1165
+ url,
1166
+ (payload, onSkip, settled) => toCollections(payload, url, onSkip, settled)
1167
+ );
1143
1168
  }
1144
1169
  };
1145
1170