mcp-libraryofcongress 2.1.2 → 3.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/dist/index.js CHANGED
@@ -1,13 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
+ import process2 from "process";
4
5
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
6
 
6
7
  // src/server.ts
7
8
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8
9
 
10
+ // src/config.ts
11
+ import process from "process";
12
+
9
13
  // src/version.ts
10
- var PKG_VERSION = "2.1.2";
14
+ var PKG_VERSION = "3.0.0";
11
15
  var REPO_URL = "https://github.com/smeet666/mcp-libraryofcongress";
12
16
 
13
17
  // src/config.ts
@@ -20,7 +24,9 @@ var DEFAULT_USER_AGENT = `mcp-libraryofcongress/${PKG_VERSION} (+${REPO_URL})`;
20
24
  function createLogger(level) {
21
25
  const rank = LOG_LEVELS.indexOf(level);
22
26
  const write = (at, message) => {
23
- if (rank === 0 || LOG_LEVELS.indexOf(at) > rank) return;
27
+ if (rank === 0 || LOG_LEVELS.indexOf(at) > rank) {
28
+ return;
29
+ }
24
30
  process.stderr.write(`[mcp-libraryofcongress] ${at}: ${message}
25
31
  `);
26
32
  };
@@ -35,9 +41,11 @@ function createLogger(level) {
35
41
  }
36
42
  function readInteger(env, name, fallback, min, max) {
37
43
  const raw = env[name];
38
- if (raw === void 0 || raw.trim() === "") return fallback;
44
+ if (raw === void 0 || raw.trim() === "") {
45
+ return fallback;
46
+ }
39
47
  const value = Number(raw);
40
- if (!Number.isFinite(value) || !Number.isInteger(value)) {
48
+ if (!(Number.isFinite(value) && Number.isInteger(value))) {
41
49
  process.stderr.write(
42
50
  `[mcp-libraryofcongress] error: ${name}="${raw}" is not a whole number; using ${fallback}.
43
51
  `
@@ -96,7 +104,7 @@ var LocError = class extends Error {
96
104
  code;
97
105
  details;
98
106
  constructor(code, message, details = {}) {
99
- super(message);
107
+ super(message, details.cause === void 0 ? void 0 : { cause: details.cause });
100
108
  this.name = "LocError";
101
109
  this.code = code;
102
110
  this.details = details;
@@ -126,7 +134,9 @@ var Cache = class {
126
134
  entries = /* @__PURE__ */ new Map();
127
135
  get(key) {
128
136
  const entry = this.entries.get(key);
129
- if (!entry) return void 0;
137
+ if (!entry) {
138
+ return void 0;
139
+ }
130
140
  if (entry.expiresAt <= Date.now()) {
131
141
  this.entries.delete(key);
132
142
  return void 0;
@@ -136,12 +146,16 @@ var Cache = class {
136
146
  return entry.value;
137
147
  }
138
148
  set(key, value) {
139
- if (this.ttlMs <= 0) return;
149
+ if (this.ttlMs <= 0) {
150
+ return;
151
+ }
140
152
  this.entries.delete(key);
141
153
  this.entries.set(key, { value, expiresAt: Date.now() + this.ttlMs });
142
154
  while (this.entries.size > this.maxEntries) {
143
155
  const oldest = this.entries.keys().next();
144
- if (oldest.done) break;
156
+ if (oldest.done) {
157
+ break;
158
+ }
145
159
  this.entries.delete(oldest.value);
146
160
  }
147
161
  }
@@ -151,28 +165,118 @@ var Cache = class {
151
165
  };
152
166
 
153
167
  // src/loc/http.ts
168
+ var ZERO_MAX_AGE = /^(?:max-age|s-maxage)\s*=\s*0$/;
154
169
  var RETRYABLE = /* @__PURE__ */ new Set([408, 425, 429, 500, 502, 503, 504]);
155
170
  var PUSH_BACK = /* @__PURE__ */ new Set([429, 503]);
156
171
  var LONGEST_WAIT_MS = 3e4;
157
172
  var RETRIES_AFTER_SILENCE = 1;
158
173
  function parseRetryAfter(value, now = Date.now()) {
159
- if (!value) return null;
174
+ if (!value) {
175
+ return null;
176
+ }
160
177
  const trimmed = value.trim();
161
178
  const seconds = Number(trimmed);
162
- if (Number.isFinite(seconds) && seconds >= 0) return Math.round(seconds * 1e3);
179
+ if (Number.isFinite(seconds) && seconds >= 0) {
180
+ return Math.round(seconds * 1e3);
181
+ }
163
182
  const at = Date.parse(trimmed);
164
- if (Number.isNaN(at)) return null;
183
+ if (Number.isNaN(at)) {
184
+ return null;
185
+ }
165
186
  return Math.max(0, at - now);
166
187
  }
167
188
  function backoffMs(attempt) {
168
189
  const base2 = Math.min(16e3, 1e3 * 2 ** attempt);
169
190
  return base2 + Math.floor(Math.random() * 500);
170
191
  }
192
+ function readFailure(error, attempts) {
193
+ const { url, attempt, maxRetries, timeoutMs } = attempts;
194
+ if (error instanceof Error && error.name === "LocError") {
195
+ throw error;
196
+ }
197
+ if (error instanceof Error && error.name === "AbortError") {
198
+ if (attempt >= Math.min(maxRetries, RETRIES_AFTER_SILENCE)) {
199
+ throw timeout(
200
+ `No answer from the Library of Congress within ${timeoutMs}ms. The newspaper corpus in particular can take tens of seconds.`,
201
+ { url }
202
+ );
203
+ }
204
+ return error;
205
+ }
206
+ const failure = error instanceof Error ? error : new Error(String(error));
207
+ if (attempt >= maxRetries) {
208
+ throw networkError(`Could not reach the Library of Congress: ${failure.message}`, { url });
209
+ }
210
+ return failure;
211
+ }
212
+ async function readRefusal(response, url, attempt, maxRetries) {
213
+ if (PUSH_BACK.has(response.status)) {
214
+ await response.body?.cancel().catch(() => void 0);
215
+ const asked = parseRetryAfter(response.headers.get("retry-after"));
216
+ if (asked !== null && asked > LONGEST_WAIT_MS) {
217
+ return {
218
+ kind: "refused",
219
+ pushBack: true,
220
+ error: rateLimited(
221
+ `The Library of Congress asked this client to wait ${Math.round(asked / 1e3)} seconds (HTTP ${response.status}).`,
222
+ { url, status: response.status }
223
+ )
224
+ };
225
+ }
226
+ if (attempt >= maxRetries) {
227
+ return {
228
+ kind: "refused",
229
+ pushBack: true,
230
+ error: rateLimited(
231
+ `The Library of Congress asked this client to slow down (HTTP ${response.status}).`,
232
+ { url, status: response.status }
233
+ )
234
+ };
235
+ }
236
+ return { kind: "again", pushBack: true, waitMs: asked ?? backoffMs(attempt) };
237
+ }
238
+ if (RETRYABLE.has(response.status) && attempt < maxRetries) {
239
+ await response.body?.cancel().catch(() => void 0);
240
+ return { kind: "again", pushBack: false, waitMs: backoffMs(attempt) };
241
+ }
242
+ if (response.status === 400 || response.status === 422) {
243
+ return {
244
+ kind: "refused",
245
+ pushBack: false,
246
+ error: invalidInput(
247
+ "The Library of Congress would not accept this request.",
248
+ "Check the query. A quotation mark or bracket left unbalanced is read as syntax."
249
+ )
250
+ };
251
+ }
252
+ if (response.status === 404 || response.status === 410) {
253
+ return {
254
+ kind: "refused",
255
+ pushBack: false,
256
+ error: notFound("The Library of Congress holds nothing at this address.", {
257
+ url,
258
+ status: response.status
259
+ })
260
+ };
261
+ }
262
+ return {
263
+ kind: "refused",
264
+ pushBack: false,
265
+ error: networkError(`The Library of Congress answered HTTP ${response.status}.`, {
266
+ url,
267
+ status: response.status
268
+ })
269
+ };
270
+ }
171
271
  function statesASettledAnswer(cacheControl) {
172
- if (cacheControl === null) return true;
272
+ if (cacheControl === null) {
273
+ return true;
274
+ }
173
275
  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));
276
+ if (directives.includes("no-cache") || directives.includes("no-store")) {
277
+ return false;
278
+ }
279
+ return !directives.some((directive) => ZERO_MAX_AGE.test(directive));
176
280
  }
177
281
  async function fetchText(options) {
178
282
  const { url, userAgent, timeoutMs, maxRetries, limiter, logger } = options;
@@ -182,7 +286,8 @@ async function fetchText(options) {
182
286
  for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
183
287
  if (askedWaitMs > 0) {
184
288
  logger.debug(`waiting ${askedWaitMs}ms, as asked`);
185
- await new Promise((resolve) => setTimeout(resolve, askedWaitMs));
289
+ const asked = askedWaitMs;
290
+ await new Promise((resolve) => setTimeout(resolve, asked));
186
291
  askedWaitMs = 0;
187
292
  }
188
293
  await limiter.beforeRequest();
@@ -200,70 +305,19 @@ async function fetchText(options) {
200
305
  const settled = statesASettledAnswer(response.headers.get("cache-control"));
201
306
  return { payload: await response.text(), settled };
202
307
  }
203
- if (PUSH_BACK.has(response.status)) {
308
+ const verdict = await readRefusal(response, url, attempt, maxRetries);
309
+ if (verdict.pushBack) {
204
310
  limiter.pushBack();
205
- await response.body?.cancel().catch(() => void 0);
206
- const asked = parseRetryAfter(response.headers.get("retry-after"));
207
- if (asked !== null && asked > LONGEST_WAIT_MS) {
208
- throw rateLimited(
209
- `The Library of Congress asked this client to wait ${Math.round(asked / 1e3)} seconds (HTTP ${response.status}).`,
210
- { url, status: response.status }
211
- );
212
- }
213
- if (attempt >= maxRetries) {
214
- throw rateLimited(
215
- `The Library of Congress asked this client to slow down (HTTP ${response.status}).`,
216
- { url, status: response.status }
217
- );
218
- }
219
- askedWaitMs = asked ?? backoffMs(attempt);
220
- lastError = new Error(`HTTP ${response.status}`);
221
- continue;
222
- }
223
- if (RETRYABLE.has(response.status) && attempt < maxRetries) {
224
- await response.body?.cancel().catch(() => void 0);
225
- lastError = new Error(`HTTP ${response.status}`);
226
- askedWaitMs = backoffMs(attempt);
227
- continue;
228
- }
229
- if (response.status === 400 || response.status === 422) {
230
- throw invalidInput(
231
- "The Library of Congress would not accept this request.",
232
- "Check the query. A quotation mark or bracket left unbalanced is read as syntax."
233
- );
234
311
  }
235
- if (response.status === 404 || response.status === 410) {
236
- throw notFound("The Library of Congress holds nothing at this address.", {
237
- url,
238
- status: response.status
239
- });
312
+ if (verdict.kind === "refused") {
313
+ throw verdict.error;
240
314
  }
241
- throw networkError(`The Library of Congress answered HTTP ${response.status}.`, {
242
- url,
243
- status: response.status
244
- });
315
+ askedWaitMs = verdict.waitMs;
316
+ lastError = new Error(`HTTP ${response.status}`);
245
317
  } catch (error) {
246
318
  clearTimeout(deadline);
247
- if (error instanceof Error && error.name === "LocError") throw error;
248
- if (error instanceof Error && error.name === "AbortError") {
249
- lastError = error;
250
- if (attempt >= Math.min(maxRetries, RETRIES_AFTER_SILENCE)) {
251
- throw timeout(
252
- `No answer from the Library of Congress within ${timeoutMs}ms. The newspaper corpus in particular can take tens of seconds.`,
253
- { url }
254
- );
255
- }
256
- askedWaitMs = backoffMs(attempt);
257
- continue;
258
- }
259
- lastError = error instanceof Error ? error : new Error(String(error));
260
- if (attempt >= maxRetries) {
261
- throw networkError(`Could not reach the Library of Congress: ${lastError.message}`, {
262
- url
263
- });
264
- }
319
+ lastError = readFailure(error, { url, attempt, maxRetries, timeoutMs });
265
320
  askedWaitMs = backoffMs(attempt);
266
- continue;
267
321
  } finally {
268
322
  clearTimeout(deadline);
269
323
  }
@@ -277,9 +331,10 @@ async function fetchJson(options) {
277
331
  const { payload, settled } = await fetchText(options);
278
332
  try {
279
333
  return { payload: JSON.parse(payload), settled };
280
- } catch {
334
+ } catch (cause) {
281
335
  throw parseFailure("The Library of Congress answered with something that is not JSON.", {
282
- url: options.url
336
+ url: options.url,
337
+ cause
283
338
  });
284
339
  }
285
340
  }
@@ -309,7 +364,9 @@ function routesNamedBy(formats) {
309
364
  const named = /* @__PURE__ */ new Set();
310
365
  for (const format of formats) {
311
366
  const route = COLLECTION_FORMAT_ROUTE[format.trim().toLowerCase()];
312
- if (route) named.add(route);
367
+ if (route) {
368
+ named.add(route);
369
+ }
313
370
  }
314
371
  return FORMAT_ROUTES.filter((route) => named.has(route));
315
372
  }
@@ -407,6 +464,7 @@ var MOST_SUBJECTS = 12;
407
464
  var itemUrl = (identifier) => `${HOST}${ITEM_ROUTE}${identifier.split("/").map(encodeURIComponent).join("/")}/`;
408
465
 
409
466
  // src/loc/urls.ts
467
+ var DOTS_ONLY = /^\.+$/;
410
468
  var YEAR_BOUND = { earliest: 1e3, latest: 9999 };
411
469
  var CONTROL_CHARACTER = /[\u0000-\u001f\u007f-\u009f]/;
412
470
  function base(path) {
@@ -429,10 +487,16 @@ function catalogueUrl(q) {
429
487
  url.searchParams.set(PARAM.dates, `${from}/${to}`);
430
488
  }
431
489
  const facets = facetString(q.facets ?? {});
432
- if (facets !== "") url.searchParams.set(PARAM.facets, facets);
490
+ if (facets !== "") {
491
+ url.searchParams.set(PARAM.facets, facets);
492
+ }
433
493
  const sort = q.sort ? SORT[q.sort] : "";
434
- if (sort !== "") url.searchParams.set(PARAM.sort, sort);
435
- if (!q.onlineOnly) url.searchParams.set(PARAM.all, "true");
494
+ if (sort !== "") {
495
+ url.searchParams.set(PARAM.sort, sort);
496
+ }
497
+ if (!q.onlineOnly) {
498
+ url.searchParams.set(PARAM.all, "true");
499
+ }
436
500
  return url.toString();
437
501
  }
438
502
  function newspaperPagesUrl(query, limit, page, filters = {}) {
@@ -446,7 +510,9 @@ function newspaperPagesUrl(query, limit, page, filters = {}) {
446
510
  url.searchParams.set(PARAM.dates, `${from}/${to}`);
447
511
  }
448
512
  const facets = facetString(filters.facets ?? {});
449
- if (facets !== "") url.searchParams.set(PARAM.facets, facets);
513
+ if (facets !== "") {
514
+ url.searchParams.set(PARAM.facets, facets);
515
+ }
450
516
  return url.toString();
451
517
  }
452
518
  function collectionsUrl(limit, page) {
@@ -463,7 +529,7 @@ function itemDocumentUrl(identifier) {
463
529
  );
464
530
  }
465
531
  const segments = identifier.trim().replace(/^\/+|\/+$/g, "").split("/").filter((segment) => segment !== "");
466
- if (segments.some((segment) => /^\.+$/.test(segment))) {
532
+ if (segments.some((segment) => DOTS_ONLY.test(segment))) {
467
533
  throw invalidInput(
468
534
  `"${identifier}" is not an identifier: it carries a relative path segment.`,
469
535
  "Take the identifier from a search result rather than building one."
@@ -475,9 +541,13 @@ function itemDocumentUrl(identifier) {
475
541
  return url.toString();
476
542
  }
477
543
  function segmentsOf(address) {
478
- if (!address) return null;
544
+ if (!address) {
545
+ return null;
546
+ }
479
547
  const trimmed = address.trim();
480
- if (trimmed === "") return null;
548
+ if (trimmed === "") {
549
+ return null;
550
+ }
481
551
  const absolute = trimmed.startsWith("//") ? `https:${trimmed}` : trimmed;
482
552
  let parsed;
483
553
  try {
@@ -486,14 +556,20 @@ function segmentsOf(address) {
486
556
  return null;
487
557
  }
488
558
  const path = parsed.pathname.replace(/^\/+|\/+$/g, "");
489
- if (path === "") return null;
559
+ if (path === "") {
560
+ return null;
561
+ }
490
562
  return { host: parsed.hostname, segments: path.split("/") };
491
563
  }
492
564
  function identifierFrom(address) {
493
565
  const parsed = segmentsOf(address);
494
- if (!parsed) return null;
566
+ if (!parsed) {
567
+ return null;
568
+ }
495
569
  const { host, segments } = parsed;
496
- if (host === "lccn.loc.gov") return segments[0] ?? null;
570
+ if (host === "lccn.loc.gov") {
571
+ return segments[0] ?? null;
572
+ }
497
573
  const first = segments[0];
498
574
  if (first === "item" || first === "resource") {
499
575
  const rest = segments.slice(1);
@@ -503,16 +579,24 @@ function identifierFrom(address) {
503
579
  }
504
580
  function collectionSlugFrom(address) {
505
581
  const parsed = segmentsOf(address);
506
- if (!parsed) return null;
582
+ if (!parsed) {
583
+ return null;
584
+ }
507
585
  return parsed.segments[0] === "collections" ? parsed.segments[1] ?? null : null;
508
586
  }
509
587
 
510
588
  // src/loc/parse.ts
589
+ var DIGITS_ONLY = /^\d+$/;
590
+ var FOUR_DIGITS = /(\d{4})/;
591
+ var WORD_SEPARATORS = /[^\p{L}\p{N}'-]+/u;
511
592
  var asObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
512
593
  function asStrings(value) {
513
- if (typeof value === "string") return value.trim() === "" ? [] : [value];
514
- if (Array.isArray(value))
594
+ if (typeof value === "string") {
595
+ return value.trim() === "" ? [] : [value];
596
+ }
597
+ if (Array.isArray(value)) {
515
598
  return value.filter((v) => typeof v === "string" && v.trim() !== "");
599
+ }
516
600
  return [];
517
601
  }
518
602
  var asString = (value) => asStrings(value)[0] ?? null;
@@ -521,36 +605,52 @@ function asLabels(value) {
521
605
  const labels = [];
522
606
  for (const entry of entries) {
523
607
  if (typeof entry === "string") {
524
- if (entry.trim() !== "") labels.push(entry);
608
+ if (entry.trim() !== "") {
609
+ labels.push(entry);
610
+ }
525
611
  continue;
526
612
  }
527
613
  const object = asObject(entry);
528
- if (!object) continue;
614
+ if (!object) {
615
+ continue;
616
+ }
529
617
  const titled = asString(object.title);
530
618
  if (titled !== null) {
531
619
  labels.push(titled);
532
620
  continue;
533
621
  }
534
622
  for (const key of Object.keys(object)) {
535
- if (key.trim() !== "") labels.push(key);
623
+ if (key.trim() !== "") {
624
+ labels.push(key);
625
+ }
536
626
  }
537
627
  }
538
628
  return labels;
539
629
  }
540
630
  function asNumber(value) {
541
- if (typeof value === "number" && Number.isFinite(value)) return value;
631
+ if (typeof value === "number" && Number.isFinite(value)) {
632
+ return value;
633
+ }
542
634
  if (typeof value === "string" && value.trim() !== "") {
543
635
  const n = Number(value);
544
- if (Number.isFinite(n)) return n;
636
+ if (Number.isFinite(n)) {
637
+ return n;
638
+ }
639
+ }
640
+ if (Array.isArray(value)) {
641
+ return asNumber(value[0]);
545
642
  }
546
- if (Array.isArray(value)) return asNumber(value[0]);
547
643
  return null;
548
644
  }
549
645
  function asPageNumber(value) {
550
646
  const text = asString(value);
551
- if (text === null) return null;
647
+ if (text === null) {
648
+ return null;
649
+ }
552
650
  const digits = text.trim();
553
- if (!/^\d+$/.test(digits)) return null;
651
+ if (!DIGITS_ONLY.test(digits)) {
652
+ return null;
653
+ }
554
654
  const n = Number(digits);
555
655
  return Number.isFinite(n) && n > 0 ? n : null;
556
656
  }
@@ -559,8 +659,10 @@ function asYear(value) {
559
659
  const plausible = (year) => Number.isFinite(year) && year >= PLAUSIBLE_YEAR.first && year <= PLAUSIBLE_YEAR.last ? Math.trunc(year) : null;
560
660
  const text = asString(value);
561
661
  if (text !== null) {
562
- const m = /(\d{4})/.exec(text);
563
- if (m) return plausible(Number(m[1]));
662
+ const m = FOUR_DIGITS.exec(text);
663
+ if (m) {
664
+ return plausible(Number(m[1]));
665
+ }
564
666
  }
565
667
  const direct = asNumber(value);
566
668
  return direct === null ? null : plausible(direct);
@@ -607,10 +709,14 @@ function monthsNamed(text, filedYear) {
607
709
  for (const match of text.matchAll(MONTH_BESIDE_A_NUMBER)) {
608
710
  const word = (match[2] ?? match[3] ?? "").toLowerCase();
609
711
  const month = MONTH_NUMBER[word];
610
- if (month !== void 0) months.add(month);
712
+ if (month !== void 0) {
713
+ months.add(month);
714
+ }
611
715
  }
612
716
  for (const match of text.matchAll(YEAR_AND_TWO_DIGITS)) {
613
- if (match[1] === filedYear) months.add(Number(match[2]));
717
+ if (match[1] === filedYear) {
718
+ months.add(Number(match[2]));
719
+ }
614
720
  }
615
721
  for (const match of text.matchAll(SLASHED_DATE)) {
616
722
  months.add(Number(match[1]));
@@ -633,22 +739,32 @@ function rangeOpenings(text) {
633
739
  }
634
740
  function statedDate(item) {
635
741
  const words = asStrings(item[ITEM_FIELD.createdPublished]).map(plainText).filter((line) => line !== "");
636
- if (words.length > 0) return words.join("; ");
742
+ if (words.length > 0) {
743
+ return words.join("; ");
744
+ }
637
745
  const spans = asLabels(item[ITEM_FIELD.dateSpans]);
638
746
  return spans.length > 0 ? spans.join("; ") : null;
639
747
  }
640
748
  function datePublished(filed, stated) {
641
- if (filed === null) return null;
749
+ if (filed === null) {
750
+ return null;
751
+ }
642
752
  const trimmed = filed.trim();
643
- if (trimmed === "" || isCataloguingCode(trimmed)) return null;
753
+ if (trimmed === "" || isCataloguingCode(trimmed)) {
754
+ return null;
755
+ }
644
756
  const filled = FILED_TO_THE_DAY.exec(trimmed) ?? FILED_TO_THE_MONTH.exec(trimmed);
645
- if (filled === null || stated === null) return trimmed;
757
+ if (filled === null || stated === null) {
758
+ return trimmed;
759
+ }
646
760
  const year = filled[1];
647
761
  const month = Number(filled[2]);
648
762
  return monthsNamed(stated, year).has(month) ? trimmed : year;
649
763
  }
650
764
  function dateCode(filed) {
651
- if (filed === null) return null;
765
+ if (filed === null) {
766
+ return null;
767
+ }
652
768
  const trimmed = filed.trim();
653
769
  return isCataloguingCode(trimmed) ? trimmed : null;
654
770
  }
@@ -685,7 +801,7 @@ function toPaging(payload, url, settled = true) {
685
801
  }
686
802
  function rowsOf(payload, url) {
687
803
  const root = asObject(payload);
688
- if (!root || !Array.isArray(root.results)) {
804
+ if (!(root && Array.isArray(root.results))) {
689
805
  throw parseFailure("The answer carried its rows in a shape this server cannot read.", { url });
690
806
  }
691
807
  return root.results.map(asObject).filter((r) => r !== null);
@@ -695,9 +811,15 @@ function sourceUrlOf(row, identifier) {
695
811
  const raw = asString(row[ROW_FIELD.url]) ?? asString(row[ROW_FIELD.id]);
696
812
  if (raw) {
697
813
  const trimmed = raw.trim();
698
- if (trimmed.startsWith("//")) return `https:${trimmed}`;
699
- if (trimmed.startsWith("http://")) return `https://${trimmed.slice("http://".length)}`;
700
- if (trimmed.startsWith("https://")) return trimmed;
814
+ if (trimmed.startsWith("//")) {
815
+ return `https:${trimmed}`;
816
+ }
817
+ if (trimmed.startsWith("http://")) {
818
+ return `https://${trimmed.slice("http://".length)}`;
819
+ }
820
+ if (trimmed.startsWith("https://")) {
821
+ return trimmed;
822
+ }
701
823
  }
702
824
  return identifier === null ? "" : itemUrl(identifier);
703
825
  }
@@ -739,7 +861,9 @@ function toSearchResults(payload, url, onSkip, settled = true) {
739
861
  sourceUrl: source
740
862
  });
741
863
  }
742
- if (skipped > 0) onSkip(skipped);
864
+ if (skipped > 0) {
865
+ onSkip(skipped);
866
+ }
743
867
  if (rows.length > 0 && records.length === 0) {
744
868
  throw parseFailure(`${rows.length} rows came back and none could be read.`, { url });
745
869
  }
@@ -773,12 +897,16 @@ function queryTerms(query) {
773
897
  const phrases = query.match(/"([^"]+)"/g) ?? [];
774
898
  for (const phrase of phrases) {
775
899
  const inner = phrase.slice(1, -1).trim().toLowerCase();
776
- if (inner !== "") terms.push(inner);
900
+ if (inner !== "") {
901
+ terms.push(inner);
902
+ }
777
903
  }
778
904
  const rest = query.replace(/"[^"]*"/g, " ");
779
- for (const word of rest.split(/[^\p{L}\p{N}'-]+/u)) {
905
+ for (const word of rest.split(WORD_SEPARATORS)) {
780
906
  const lower = word.trim().toLowerCase();
781
- if (lower.length < 3 || JOINING_WORDS.has(lower)) continue;
907
+ if (lower.length < 3 || JOINING_WORDS.has(lower)) {
908
+ continue;
909
+ }
782
910
  terms.push(lower);
783
911
  }
784
912
  return [...new Set(terms)];
@@ -792,21 +920,27 @@ function toWordBoundary(text, index, direction) {
792
920
  const limit = direction === -1 ? 0 : text.length;
793
921
  let at = index;
794
922
  for (let step = 0; step < 30 && at !== limit; step += 1) {
795
- if (text[at] === " ") return at;
923
+ if (text[at] === " ") {
924
+ return at;
925
+ }
796
926
  at += direction;
797
927
  }
798
928
  return index;
799
929
  }
800
930
  function excerptsFor(text, terms, budget) {
801
931
  const clean = text.replace(/\s+/g, " ").trim();
802
- if (clean === "") return { passages: [], located: false };
932
+ if (clean === "") {
933
+ return { passages: [], located: false };
934
+ }
803
935
  const found = [];
804
936
  for (const term of terms) {
805
937
  const pattern = wholeWord(term);
806
938
  let match = pattern.exec(clean);
807
939
  while (match !== null) {
808
940
  found.push({ at: match.index, length: match[0].length });
809
- if (match[0].length === 0) pattern.lastIndex += 1;
941
+ if (match[0].length === 0) {
942
+ pattern.lastIndex += 1;
943
+ }
810
944
  match = pattern.exec(clean);
811
945
  }
812
946
  }
@@ -824,7 +958,7 @@ function excerptsFor(text, terms, budget) {
824
958
  const half = Math.max(0, Math.floor((budget.maxChars - hit.length) / 2));
825
959
  const start = toWordBoundary(clean, Math.max(0, hit.at - half), -1);
826
960
  const end = toWordBoundary(clean, Math.min(clean.length, hit.at + hit.length + half), 1);
827
- const previous = windows[windows.length - 1];
961
+ const previous = windows.at(-1);
828
962
  if (previous && start <= previous.end) {
829
963
  previous.end = Math.max(previous.end, end);
830
964
  continue;
@@ -839,7 +973,8 @@ function excerptsFor(text, terms, budget) {
839
973
  });
840
974
  return { passages, located: true };
841
975
  }
842
- function toNewspaperResults(payload, url, query, budget, onSkip, settled = true) {
976
+ function toNewspaperResults(payload, url, query, reading) {
977
+ const { budget, onSkip, settled = true } = reading;
843
978
  const rows = rowsOf(payload, url);
844
979
  const terms = queryTerms(query);
845
980
  const hits = [];
@@ -871,18 +1006,24 @@ function toNewspaperResults(payload, url, query, budget, onSkip, settled = true)
871
1006
  sourceUrl: source
872
1007
  });
873
1008
  }
874
- if (skipped > 0) onSkip(skipped);
1009
+ if (skipped > 0) {
1010
+ onSkip(skipped);
1011
+ }
875
1012
  if (rows.length > 0 && hits.length === 0) {
876
1013
  throw parseFailure(`${rows.length} matches came back and none could be read.`, { url });
877
1014
  }
878
1015
  return { paging: toPaging(payload, url, settled), hits };
879
1016
  }
880
1017
  function toResources(value) {
881
- if (!Array.isArray(value)) return [];
1018
+ if (!Array.isArray(value)) {
1019
+ return [];
1020
+ }
882
1021
  const resources = [];
883
1022
  for (const entry of value) {
884
1023
  const resource = asObject(entry);
885
- if (!resource) continue;
1024
+ if (!resource) {
1025
+ continue;
1026
+ }
886
1027
  const files = resource.files;
887
1028
  const fileCount = Array.isArray(files) ? files.reduce((sum, group) => sum + (Array.isArray(group) ? group.length : 1), 0) : asNumber(files);
888
1029
  resources.push({
@@ -896,17 +1037,23 @@ function toResources(value) {
896
1037
  }
897
1038
  function toCitations(value) {
898
1039
  const block = asObject(value);
899
- if (!block) return {};
1040
+ if (!block) {
1041
+ return {};
1042
+ }
900
1043
  const citations = {};
901
1044
  for (const [style, text] of Object.entries(block)) {
902
- if (typeof text !== "string" || text.trim() === "") continue;
1045
+ if (typeof text !== "string" || text.trim() === "") {
1046
+ continue;
1047
+ }
903
1048
  citations[style] = plainText(text);
904
1049
  }
905
1050
  return citations;
906
1051
  }
907
1052
  function toItemDetail(payload, identifier, url) {
908
1053
  const root = asObject(payload);
909
- if (!root) throw parseFailure("The item answer was not an object.", { url });
1054
+ if (!root) {
1055
+ throw parseFailure("The item answer was not an object.", { url });
1056
+ }
910
1057
  if (asNumber(root.status) === 404) {
911
1058
  throw notFound(`The Library of Congress has no record called "${identifier}".`, { url });
912
1059
  }
@@ -977,7 +1124,9 @@ function toCollections(payload, url, onSkip, settled = true) {
977
1124
  itemsUrl: asString(row[ROW_FIELD.items])
978
1125
  });
979
1126
  }
980
- if (skipped > 0) onSkip(skipped);
1127
+ if (skipped > 0) {
1128
+ onSkip(skipped);
1129
+ }
981
1130
  if (rows.length > 0 && collections.length === 0) {
982
1131
  throw parseFailure(`${rows.length} collections came back and none could be read.`, { url });
983
1132
  }
@@ -1020,7 +1169,9 @@ var RateLimiter = class {
1020
1169
  async beforeRequest() {
1021
1170
  const elapsed = Date.now() - this.lastStartedAt;
1022
1171
  const wait = Math.min(this.intervalMs, this.intervalMs - elapsed);
1023
- if (wait > 0) await sleep(wait);
1172
+ if (wait > 0) {
1173
+ await sleep(wait);
1174
+ }
1024
1175
  this.lastStartedAt = Date.now();
1025
1176
  }
1026
1177
  /** Called when the site asks for room: double the gap, up to the ceiling. */
@@ -1033,9 +1184,13 @@ var RateLimiter = class {
1033
1184
  * response after a rough patch does not undo the caution that earned it.
1034
1185
  */
1035
1186
  succeeded() {
1036
- if (this.intervalMs === this.baseIntervalMs) return;
1187
+ if (this.intervalMs === this.baseIntervalMs) {
1188
+ return;
1189
+ }
1037
1190
  this.calmStreak += 1;
1038
- if (this.calmStreak < 3) return;
1191
+ if (this.calmStreak < 3) {
1192
+ return;
1193
+ }
1039
1194
  this.calmStreak = 0;
1040
1195
  this.intervalMs = Math.max(this.baseIntervalMs, Math.round(this.intervalMs / 2));
1041
1196
  }
@@ -1144,7 +1299,7 @@ var LocClient = class {
1144
1299
  return this.read(
1145
1300
  key,
1146
1301
  url,
1147
- (payload, onSkip, settled) => toNewspaperResults(payload, url, trimmed, budget, onSkip, settled),
1302
+ (payload, onSkip, settled) => toNewspaperResults(payload, url, trimmed, { budget, onSkip, settled }),
1148
1303
  // Reading the text of millions of pages takes the site far longer than
1149
1304
  // answering from the catalogue, so this route carries a budget of its own.
1150
1305
  this.config.newspaperTimeoutMs
@@ -1174,11 +1329,24 @@ import { z as z3 } from "zod";
1174
1329
  // src/tools/arguments.ts
1175
1330
  import { z } from "zod";
1176
1331
  var INVALID_INPUT = "invalid_input";
1332
+ var defaultMessage = z.core.locales.en().localeError;
1177
1333
  function strictInput(shape) {
1178
1334
  const declared = Object.keys(shape);
1179
- return z.strictObject(shape, {
1180
- error: (issue) => issue.code === "unrecognized_keys" ? unknownArgumentMessage(issue.keys, declared) : void 0
1181
- });
1335
+ const refuse = (issue) => issue.code === "unrecognized_keys" ? unknownArgumentMessage(issue.keys, declared) : `[${INVALID_INPUT}] ${defaultMessage(issue)}`;
1336
+ for (const argument of Object.values(shape)) {
1337
+ carryTheCode(argument, refuse);
1338
+ }
1339
+ return z.strictObject(shape, { error: refuse });
1340
+ }
1341
+ function carryTheCode(schema, refuse) {
1342
+ const definition = schema._zod.def;
1343
+ definition.error = refuse;
1344
+ for (const check of definition.checks ?? []) {
1345
+ check._zod.def.error = refuse;
1346
+ }
1347
+ if (definition.innerType) {
1348
+ carryTheCode(definition.innerType, refuse);
1349
+ }
1182
1350
  }
1183
1351
  function unknownArgumentMessage(keys, declared) {
1184
1352
  const named = keys.map((key) => {
@@ -1190,15 +1358,21 @@ function unknownArgumentMessage(keys, declared) {
1190
1358
  function nearestArgument(key, declared) {
1191
1359
  const flatten = (name) => name.toLowerCase().replace(/[^a-z0-9]/g, "");
1192
1360
  const flat = flatten(key);
1193
- if (flat.length === 0) return void 0;
1361
+ if (flat.length === 0) {
1362
+ return void 0;
1363
+ }
1194
1364
  const sameName = declared.find((name) => flatten(name) === flat);
1195
- if (sameName) return sameName;
1365
+ if (sameName) {
1366
+ return sameName;
1367
+ }
1196
1368
  const overlapping = declared.find((name) => {
1197
1369
  const other = flatten(name);
1198
1370
  const [shorter, longer] = other.length < flat.length ? [other, flat] : [flat, other];
1199
1371
  return shorter.length >= 3 && (longer.startsWith(shorter) || longer.endsWith(shorter));
1200
1372
  });
1201
- if (overlapping) return overlapping;
1373
+ if (overlapping) {
1374
+ return overlapping;
1375
+ }
1202
1376
  let closest;
1203
1377
  let shortest = Number.POSITIVE_INFINITY;
1204
1378
  for (const name of declared) {
@@ -1212,19 +1386,38 @@ function nearestArgument(key, declared) {
1212
1386
  }
1213
1387
  function editDistance(left, right) {
1214
1388
  let previous = Array.from({ length: right.length + 1 }, (_, index) => index);
1389
+ let distance = right.length;
1215
1390
  for (let row = 1; row <= left.length; row += 1) {
1216
1391
  const current = [row];
1217
- for (let column = 1; column <= right.length; column += 1) {
1218
- const substitution = previous[column - 1] + (left[row - 1] === right[column - 1] ? 0 : 1);
1219
- current[column] = Math.min(substitution, previous[column] + 1, current[column - 1] + 1);
1392
+ let diagonal = row - 1;
1393
+ let leftward = row;
1394
+ for (const [column, above] of previous.entries()) {
1395
+ if (column === 0) {
1396
+ continue;
1397
+ }
1398
+ const substitution = diagonal + (left[row - 1] === right[column - 1] ? 0 : 1);
1399
+ const cell = Math.min(substitution, above + 1, leftward + 1);
1400
+ current.push(cell);
1401
+ diagonal = above;
1402
+ leftward = cell;
1220
1403
  }
1221
1404
  previous = current;
1405
+ distance = leftward;
1222
1406
  }
1223
- return previous[right.length];
1407
+ return distance;
1224
1408
  }
1225
1409
 
1226
1410
  // src/tools/shared.ts
1227
1411
  import { z as z2 } from "zod";
1412
+ function identifierLine(record) {
1413
+ if (record.identifier) {
1414
+ return `\xB7 id: ${record.identifier}`;
1415
+ }
1416
+ if (record.is_collection) {
1417
+ return "\xB7 a collection, no identifier";
1418
+ }
1419
+ return "\xB7 no identifier";
1420
+ }
1228
1421
  var MAX_TEXT_CHARS = 2200;
1229
1422
  var ATTRIBUTION = "Source: Library of Congress";
1230
1423
  var recordSchema = z2.object({
@@ -1253,7 +1446,9 @@ function indentMarkerLines(body) {
1253
1446
  function ok(structured, body, options = {}) {
1254
1447
  const credit = options.sourceUrl ? `${ATTRIBUTION} \u2014 ${options.sourceUrl}` : ATTRIBUTION;
1255
1448
  const noteLines = (options.notes ?? []).map((note) => `Note: ${note}`);
1256
- while (noteLines.length > 0 && noteLines.join("\n").length > MAX_TEXT_CHARS / 2) noteLines.pop();
1449
+ while (noteLines.length > 0 && noteLines.join("\n").length > MAX_TEXT_CHARS / 2) {
1450
+ noteLines.pop();
1451
+ }
1257
1452
  const trailer = [...noteLines, credit].join("\n");
1258
1453
  const cut = "\n\n[shortened; the full result is in the structured output]";
1259
1454
  const budget = MAX_TEXT_CHARS - `
@@ -1270,7 +1465,9 @@ ${trailer}`;
1270
1465
  function toToolError(error) {
1271
1466
  const known = error instanceof LocError ? error : new LocError("network_error", error instanceof Error ? error.message : String(error));
1272
1467
  const lines = [`[${known.code}] ${known.message}`];
1273
- if (known.details.hint) lines.push(`Hint: ${known.details.hint}`);
1468
+ if (known.details.hint) {
1469
+ lines.push(`Hint: ${known.details.hint}`);
1470
+ }
1274
1471
  return { content: [{ type: "text", text: lines.join("\n") }], isError: true };
1275
1472
  }
1276
1473
  function agrees(n, singular, plural) {
@@ -1280,17 +1477,23 @@ function counted(n, singular, plural = `${singular}s`) {
1280
1477
  return `${n} ${agrees(n, singular, plural)}`;
1281
1478
  }
1282
1479
  function truncate(text, maxChars) {
1283
- if (text.length <= maxChars) return text;
1480
+ if (text.length <= maxChars) {
1481
+ return text;
1482
+ }
1284
1483
  return `${text.slice(0, Math.max(0, maxChars - 1)).trimEnd()}\u2026`;
1285
1484
  }
1286
1485
  function sliceAtLineBoundary(text, offset, maxChars) {
1287
1486
  const rest = text.slice(offset);
1288
- if (rest.length <= maxChars) return { slice: rest, nextOffset: null };
1487
+ if (rest.length <= maxChars) {
1488
+ return { slice: rest, nextOffset: null };
1489
+ }
1289
1490
  const window = rest.slice(0, maxChars);
1290
1491
  const lastBreak = window.lastIndexOf("\n");
1291
1492
  let cut = lastBreak > 0 ? lastBreak : maxChars;
1292
1493
  const code = rest.charCodeAt(cut - 1);
1293
- if (code >= 55296 && code <= 56319) cut -= 1;
1494
+ if (code >= 55296 && code <= 56319) {
1495
+ cut -= 1;
1496
+ }
1294
1497
  return { slice: rest.slice(0, cut), nextOffset: offset + cut };
1295
1498
  }
1296
1499
  function renderRecords(records) {
@@ -1303,7 +1506,7 @@ function renderRecords(records) {
1303
1506
  // An identifier a row does not carry is stated rather than left out: a
1304
1507
  // line with nothing where the others carry an id reads as a line whose
1305
1508
  // id went unprinted.
1306
- record.identifier ? `\xB7 id: ${record.identifier}` : record.is_collection ? "\xB7 a collection, no identifier" : "\xB7 no identifier"
1509
+ identifierLine(record)
1307
1510
  ];
1308
1511
  return `${bits.filter(Boolean).join(" ")}
1309
1512
  ${record.source_url}`;
@@ -1313,6 +1516,45 @@ var OCR_CAVEAT = "Excerpts are the text a machine read off the scanned page, so
1313
1516
  var RIGHTS_CAVEAT = "The Library states no terms of use on this record. Silence is not permission: check the record's page before republishing.";
1314
1517
 
1315
1518
  // src/tools/getItem.ts
1519
+ function attachAskedSections(structured, data, wanted, notes) {
1520
+ if (wanted.has("citations")) {
1521
+ structured.citations = data.citations;
1522
+ if (Object.keys(data.citations).length === 0) {
1523
+ notes.push("The Library publishes no ready-made citation for this record.");
1524
+ }
1525
+ }
1526
+ if (wanted.has("resources")) {
1527
+ structured.resources = data.resources.map((resource) => ({
1528
+ caption: resource.caption,
1529
+ file_count: resource.fileCount,
1530
+ url: resource.url,
1531
+ image_url: resource.imageUrl
1532
+ }));
1533
+ if (data.resources.length === 0) {
1534
+ notes.push("The Library serves no copy of this record online.");
1535
+ }
1536
+ }
1537
+ if (wanted.has("full_metadata")) {
1538
+ structured.full_metadata = data.raw ?? {};
1539
+ }
1540
+ }
1541
+ function notesOnWhatTheRecordStates(data) {
1542
+ const notes = [];
1543
+ if (data.dateIsSpanOpening) {
1544
+ notes.push(
1545
+ `The record states its date as "${data.dateStated}". ${data.date} is the opening of that span and where the Library files the record, and 'year' reads the same value, so neither is a date the record carries.`
1546
+ );
1547
+ }
1548
+ if (data.dateCode !== null) {
1549
+ notes.push(
1550
+ `The Library files this record under "${data.dateCode}", a cataloguing code standing for digits it has not established rather than a date, so 'date' and 'year' are null. 'date_stated' repeats what the record itself says about when it was made.`
1551
+ );
1552
+ }
1553
+ if (!data.rights) {
1554
+ notes.push(RIGHTS_CAVEAT);
1555
+ }
1556
+ return notes;
1557
+ }
1316
1558
  var SECTIONS = ["basic", "citations", "resources", "full_metadata"];
1317
1559
  var getItemDescription = [
1318
1560
  "Read one Library of Congress record by its identifier, as returned by search_items or search_newspapers.",
@@ -1380,7 +1622,9 @@ async function runGetItem(client, args) {
1380
1622
  const wanted = new Set(args.sections);
1381
1623
  const { data, cached } = await client.getItem(args.identifier);
1382
1624
  const notes = [];
1383
- if (cached) notes.push("Served from this server's short-lived in-memory cache.");
1625
+ if (cached) {
1626
+ notes.push("Served from this server's short-lived in-memory cache.");
1627
+ }
1384
1628
  const full = data.description ?? "";
1385
1629
  const notesOnRecord = data.notes.filter((note) => !full.includes(note));
1386
1630
  const { slice, nextOffset } = sliceAtLineBoundary(
@@ -1423,35 +1667,8 @@ async function runGetItem(client, args) {
1423
1667
  rights: data.rights,
1424
1668
  notes
1425
1669
  };
1426
- if (wanted.has("citations")) {
1427
- structured.citations = data.citations;
1428
- if (Object.keys(data.citations).length === 0) {
1429
- notes.push("The Library publishes no ready-made citation for this record.");
1430
- }
1431
- }
1432
- if (wanted.has("resources")) {
1433
- structured.resources = data.resources.map((resource) => ({
1434
- caption: resource.caption,
1435
- file_count: resource.fileCount,
1436
- url: resource.url,
1437
- image_url: resource.imageUrl
1438
- }));
1439
- if (data.resources.length === 0) {
1440
- notes.push("The Library serves no copy of this record online.");
1441
- }
1442
- }
1443
- if (wanted.has("full_metadata")) structured.full_metadata = data.raw ?? {};
1444
- if (data.dateIsSpanOpening) {
1445
- notes.push(
1446
- `The record states its date as "${data.dateStated}". ${data.date} is the opening of that span and where the Library files the record, and 'year' reads the same value, so neither is a date the record carries.`
1447
- );
1448
- }
1449
- if (data.dateCode !== null) {
1450
- notes.push(
1451
- `The Library files this record under "${data.dateCode}", a cataloguing code standing for digits it has not established rather than a date, so 'date' and 'year' are null. 'date_stated' repeats what the record itself says about when it was made.`
1452
- );
1453
- }
1454
- if (!data.rights) notes.push(RIGHTS_CAVEAT);
1670
+ attachAskedSections(structured, data, wanted, notes);
1671
+ notes.push(...notesOnWhatTheRecordStates(data));
1455
1672
  const lines = [
1456
1673
  [data.title ?? data.identifier, data.date ? `(${data.date})` : ""].filter(Boolean).join(" "),
1457
1674
  data.creator ? `By ${data.creator}` : "",
@@ -1461,7 +1678,9 @@ async function runGetItem(client, args) {
1461
1678
  data.repository ? `Held at: ${data.repository}` : "",
1462
1679
  data.rights ? `Rights: ${data.rights}` : ""
1463
1680
  ].filter(Boolean);
1464
- if (slice !== "") lines.push("", slice);
1681
+ if (slice !== "") {
1682
+ lines.push("", slice);
1683
+ }
1465
1684
  const resources = structured.resources;
1466
1685
  if (resources && resources.length > 0) {
1467
1686
  lines.push("", "Copies served online:");
@@ -1477,6 +1696,15 @@ async function runGetItem(client, args) {
1477
1696
 
1478
1697
  // src/tools/listCollections.ts
1479
1698
  import { z as z4 } from "zod";
1699
+ function nothingOnThisPage(read, total, page) {
1700
+ if (read > 0) {
1701
+ return `None of the ${counted(read, "collection")} on page ${page} publishes a format naming a media_type.`;
1702
+ }
1703
+ if (total > 0) {
1704
+ return `Page ${page} is past the last of ${counted(total, "collection")}.`;
1705
+ }
1706
+ return "The Library published no collection here.";
1707
+ }
1480
1708
  var MAX_COLLECTION_PAGE = 100;
1481
1709
  var listCollectionsDescription = [
1482
1710
  "List the digital collections of the Library of Congress: bodies of material a curator chose, described and published together.",
@@ -1520,7 +1748,9 @@ async function askCollections(client, limit, page) {
1520
1748
  try {
1521
1749
  return await client.listCollections(limit, page);
1522
1750
  } catch (error) {
1523
- if (!(error instanceof LocError) || error.code !== "not_found" || page <= 1) throw error;
1751
+ if (!(error instanceof LocError) || error.code !== "not_found" || page <= 1) {
1752
+ throw error;
1753
+ }
1524
1754
  const first = await client.listCollections(limit, 1);
1525
1755
  return { data: { paging: first.data.paging, collections: [] }, cached: first.cached };
1526
1756
  }
@@ -1529,7 +1759,9 @@ async function runListCollections(client, args) {
1529
1759
  try {
1530
1760
  const { data, cached, skipped } = await askCollections(client, args.limit, args.page);
1531
1761
  const notes = [];
1532
- if (cached) notes.push("Served from this server's short-lived in-memory cache.");
1762
+ if (cached) {
1763
+ notes.push("Served from this server's short-lived in-memory cache.");
1764
+ }
1533
1765
  if (skipped) {
1534
1766
  notes.push(
1535
1767
  `${counted(skipped, "row")} came back in a shape this server could not read and ${agrees(skipped, "was", "were")} left out.`
@@ -1582,7 +1814,7 @@ async function runListCollections(client, args) {
1582
1814
  `Page ${args.page} is past the last collection. ${total} ${agrees(total, "exists", "exist")}, so ask for a lower page.`
1583
1815
  );
1584
1816
  }
1585
- const body = collections.length === 0 ? read.length > 0 ? `None of the ${counted(read.length, "collection")} on page ${args.page} publishes a format naming a media_type.` : total > 0 ? `Page ${args.page} is past the last of ${counted(total, "collection")}.` : "The Library published no collection here." : `${collections.length} of ${counted(total, "collection")}:
1817
+ const body = collections.length === 0 ? nothingOnThisPage(read.length, total, args.page) : `${collections.length} of ${counted(total, "collection")}:
1586
1818
  ` + collections.map((collection, index) => {
1587
1819
  const head = [
1588
1820
  `${index + 1}. ${collection.title}`,
@@ -1600,6 +1832,61 @@ async function runListCollections(client, args) {
1600
1832
 
1601
1833
  // src/tools/searchItems.ts
1602
1834
  import { z as z5 } from "zod";
1835
+ function notesOnWhatTheSearchReached(data, args, items, total, narrowing) {
1836
+ const notes = [];
1837
+ if (total > items.length) {
1838
+ notes.push(
1839
+ narrowing.setAside ? `${counted(total, "record")} match the search${narrowing.without}, and ${items.length} ${agrees(items.length, "is", "are")} shown.` : `${counted(total, "record")} match and ${items.length} ${agrees(items.length, "is", "are")} shown.`
1840
+ );
1841
+ }
1842
+ const gathered = items.filter((item) => item.is_collection).length;
1843
+ if (gathered > 0) {
1844
+ notes.push(
1845
+ `${gathered} of the ${counted(items.length, "row")} shown ${agrees(gathered, "is a collection", "are collections")} the Library gathered and named rather than ${agrees(gathered, "a record", "records")} of the ${args.media_type} catalogue: 'identifier' is null there, get_item has nothing to take, and 'source_url' opens the collection. The count beside the rows is the catalogue's own, and it counts them in.`
1846
+ );
1847
+ }
1848
+ const anonymous = items.filter((item) => item.identifier === null && !item.is_collection).length;
1849
+ if (anonymous > 0) {
1850
+ notes.push(
1851
+ `${counted(anonymous, "row")} shown ${agrees(anonymous, "names", "name")} an address that is not a record: 'identifier' is null there and get_item has nothing to take, so ${agrees(anonymous, "read it at its", "read them at their")} 'source_url'.`
1852
+ );
1853
+ }
1854
+ const coded = items.filter((item) => item.date_code !== null);
1855
+ if (coded.length > 0) {
1856
+ const codes = [...new Set(coded.map((item) => item.date_code))].join(", ");
1857
+ notes.push(
1858
+ `${counted(coded.length, "row")} shown ${agrees(coded.length, "carries", "carry")} no date: the Library files ${agrees(coded.length, "it", "them")} under a cataloguing code standing for digits it has not established, so 'date' and 'year' are null there while 'date_code' holds the code it filed ${agrees(coded.length, "it", "them")} under: ${codes}.`
1859
+ );
1860
+ }
1861
+ if (total === 0) {
1862
+ notes.push(
1863
+ `Nothing in the ${args.media_type} catalogue matches. A search here reads titles and catalogue descriptions only, so a phrase printed inside a newspaper belongs in search_newspapers.`
1864
+ );
1865
+ }
1866
+ if (items.length === 0 && total > 0) {
1867
+ const pages = data.paging.pageCount;
1868
+ notes.push(
1869
+ `Page ${args.page} is past the last row. ${counted(total, "record")} match${pages === null ? "" : ` across ${counted(pages, "page")} at this page size`}, so ask for a lower page.`
1870
+ );
1871
+ }
1872
+ if (!narrowing.setAside && (args.year_from !== void 0 || args.year_to !== void 0)) {
1873
+ notes.push(
1874
+ "A record spanning several years matches on any of them, so a row can carry a date outside the range asked for while still belonging to it."
1875
+ );
1876
+ }
1877
+ if (!args.online_only && items.length > 0) {
1878
+ notes.push(
1879
+ "This search took in records with no digitised copy. Read 'online' on a row before promising something can be read from a browser."
1880
+ );
1881
+ }
1882
+ return notes;
1883
+ }
1884
+ function nothingOnThisPage2(total, page, mediaType, query) {
1885
+ if (total > 0) {
1886
+ return `Page ${page} is past the last of ${counted(total, "record")} the ${mediaType} catalogue holds for "${query}".`;
1887
+ }
1888
+ return `Nothing in the ${mediaType} catalogue for "${query}".`;
1889
+ }
1603
1890
  var searchItemsDescription = [
1604
1891
  "Search the Library of Congress catalogue: books, photographs, maps, recordings, films, manuscripts, sheet music and newspaper titles.",
1605
1892
  "'media_type' is required, because the Library keeps a separate catalogue for each kind of thing.",
@@ -1641,10 +1928,18 @@ var searchItemsOutput = z5.object({
1641
1928
  });
1642
1929
  function describeNarrowing(args) {
1643
1930
  const written = [];
1644
- if (args.subject) written.push(`subject="${args.subject}"`);
1645
- if (args.location) written.push(`location="${args.location}"`);
1646
- if (args.language) written.push(`language="${args.language}"`);
1647
- if (args.collection) written.push(`collection="${args.collection}"`);
1931
+ if (args.subject) {
1932
+ written.push(`subject="${args.subject}"`);
1933
+ }
1934
+ if (args.location) {
1935
+ written.push(`location="${args.location}"`);
1936
+ }
1937
+ if (args.language) {
1938
+ written.push(`language="${args.language}"`);
1939
+ }
1940
+ if (args.collection) {
1941
+ written.push(`collection="${args.collection}"`);
1942
+ }
1648
1943
  if (args.year_from !== void 0 || args.year_to !== void 0) {
1649
1944
  written.push(`years ${args.year_from ?? "any"} to ${args.year_to ?? "any"}`);
1650
1945
  }
@@ -1652,9 +1947,15 @@ function describeNarrowing(args) {
1652
1947
  }
1653
1948
  function whereTheWordingIs(args) {
1654
1949
  const advice = [];
1655
- if (args.subject) advice.push("a subject is spelled as 'subjects' spells it on a row");
1656
- if (args.location) advice.push("a place is spelled as 'location' spells it on a row");
1657
- if (args.language) advice.push('a language is written in English, as in "english"');
1950
+ if (args.subject) {
1951
+ advice.push("a subject is spelled as 'subjects' spells it on a row");
1952
+ }
1953
+ if (args.location) {
1954
+ advice.push("a place is spelled as 'location' spells it on a row");
1955
+ }
1956
+ if (args.language) {
1957
+ advice.push('a language is written in English, as in "english"');
1958
+ }
1658
1959
  if (args.collection) {
1659
1960
  advice.push("a collection is spelled as list_collections reports it under 'collection_filter'");
1660
1961
  }
@@ -1666,14 +1967,16 @@ async function askCatalogue(client, query, page) {
1666
1967
  try {
1667
1968
  return await client.searchItems(query);
1668
1969
  } catch (error) {
1669
- if (!(error instanceof LocError) || error.code !== "not_found" || page <= 1) throw error;
1970
+ if (!(error instanceof LocError) || error.code !== "not_found" || page <= 1) {
1971
+ throw error;
1972
+ }
1670
1973
  const first = await client.searchItems({ ...query, page: 1 });
1671
1974
  return { data: { paging: first.data.paging, records: [] }, cached: first.cached };
1672
1975
  }
1673
1976
  }
1674
1977
  async function runSearchItems(client, args) {
1675
1978
  try {
1676
- if (!INDEXED_WORD.test(args.query) && !ONE_CHARACTER_WORD.test(args.query)) {
1979
+ if (!(INDEXED_WORD.test(args.query) || ONE_CHARACTER_WORD.test(args.query))) {
1677
1980
  return toToolError(
1678
1981
  invalidInput(
1679
1982
  `Every word of "${args.query}" is a single letter, and the catalogue index holds no such word, so this query is one the index cannot be asked.`,
@@ -1691,7 +1994,9 @@ async function runSearchItems(client, args) {
1691
1994
  }
1692
1995
  const facets = {};
1693
1996
  const put = (field, value) => {
1694
- if (value && value.trim() !== "") facets[field] = value;
1997
+ if (value && value.trim() !== "") {
1998
+ facets[field] = value;
1999
+ }
1695
2000
  };
1696
2001
  put("subject", args.subject);
1697
2002
  put("location", args.location);
@@ -1709,8 +2014,8 @@ async function runSearchItems(client, args) {
1709
2014
  const narrowed = {
1710
2015
  ...wide,
1711
2016
  facets,
1712
- ...args.year_from !== void 0 ? { yearFrom: args.year_from } : {},
1713
- ...args.year_to !== void 0 ? { yearTo: args.year_to } : {}
2017
+ ...args.year_from === void 0 ? {} : { yearFrom: args.year_from },
2018
+ ...args.year_to === void 0 ? {} : { yearTo: args.year_to }
1714
2019
  };
1715
2020
  const notes = [];
1716
2021
  let result = await askCatalogue(client, narrowed, args.page);
@@ -1727,7 +2032,9 @@ async function runSearchItems(client, args) {
1727
2032
  }
1728
2033
  }
1729
2034
  const { data, cached, skipped } = result;
1730
- if (cached) notes.push("Served from this server's short-lived in-memory cache.");
2035
+ if (cached) {
2036
+ notes.push("Served from this server's short-lived in-memory cache.");
2037
+ }
1731
2038
  if (skipped) {
1732
2039
  notes.push(
1733
2040
  `${counted(skipped, "row")} came back in a shape this server could not read and ${agrees(skipped, "was", "were")} left out.`
@@ -1749,54 +2056,13 @@ async function runSearchItems(client, args) {
1749
2056
  }));
1750
2057
  const total = data.paging.resultCount;
1751
2058
  const withoutNarrowing = narrowingSetAside ? ` without ${narrowing.join(", ")}` : "";
1752
- if (total > items.length) {
1753
- notes.push(
1754
- narrowingSetAside ? `${counted(total, "record")} match the search${withoutNarrowing}, and ${items.length} ${agrees(items.length, "is", "are")} shown.` : `${counted(total, "record")} match and ${items.length} ${agrees(items.length, "is", "are")} shown.`
1755
- );
1756
- }
1757
- const gathered = items.filter((item) => item.is_collection).length;
1758
- if (gathered > 0) {
1759
- notes.push(
1760
- `${gathered} of the ${counted(items.length, "row")} shown ${agrees(gathered, "is a collection", "are collections")} the Library gathered and named rather than ${agrees(gathered, "a record", "records")} of the ${args.media_type} catalogue: 'identifier' is null there, get_item has nothing to take, and 'source_url' opens the collection. The count beside the rows is the catalogue's own, and it counts them in.`
1761
- );
1762
- }
1763
- const anonymous = items.filter(
1764
- (item) => item.identifier === null && !item.is_collection
1765
- ).length;
1766
- if (anonymous > 0) {
1767
- notes.push(
1768
- `${counted(anonymous, "row")} shown ${agrees(anonymous, "names", "name")} an address that is not a record: 'identifier' is null there and get_item has nothing to take, so ${agrees(anonymous, "read it at its", "read them at their")} 'source_url'.`
1769
- );
1770
- }
1771
- const coded = items.filter((item) => item.date_code !== null);
1772
- if (coded.length > 0) {
1773
- const codes = [...new Set(coded.map((item) => item.date_code))].join(", ");
1774
- notes.push(
1775
- `${counted(coded.length, "row")} shown ${agrees(coded.length, "carries", "carry")} no date: the Library files ${agrees(coded.length, "it", "them")} under a cataloguing code standing for digits it has not established, so 'date' and 'year' are null there while 'date_code' holds the code it filed ${agrees(coded.length, "it", "them")} under: ${codes}.`
1776
- );
1777
- }
1778
- if (total === 0) {
1779
- notes.push(
1780
- `Nothing in the ${args.media_type} catalogue matches. A search here reads titles and catalogue descriptions only, so a phrase printed inside a newspaper belongs in search_newspapers.`
1781
- );
1782
- }
1783
- if (items.length === 0 && total > 0) {
1784
- const pages = data.paging.pageCount;
1785
- notes.push(
1786
- `Page ${args.page} is past the last row. ${counted(total, "record")} match${pages === null ? "" : ` across ${counted(pages, "page")} at this page size`}, so ask for a lower page.`
1787
- );
1788
- }
1789
- if (!narrowingSetAside && (args.year_from !== void 0 || args.year_to !== void 0)) {
1790
- notes.push(
1791
- "A record spanning several years matches on any of them, so a row can carry a date outside the range asked for while still belonging to it."
1792
- );
1793
- }
1794
- if (!args.online_only && items.length > 0) {
1795
- notes.push(
1796
- "This search took in records with no digitised copy. Read 'online' on a row before promising something can be read from a browser."
1797
- );
1798
- }
1799
- const body = items.length === 0 ? total > 0 ? `Page ${args.page} is past the last of ${counted(total, "record")} the ${args.media_type} catalogue holds for "${args.query}".` : `Nothing in the ${args.media_type} catalogue for "${args.query}".` : `${items.length} of ${counted(total, "record")} for "${args.query}"${withoutNarrowing}:
2059
+ notes.push(
2060
+ ...notesOnWhatTheSearchReached(data, args, items, total, {
2061
+ setAside: narrowingSetAside,
2062
+ without: withoutNarrowing
2063
+ })
2064
+ );
2065
+ const body = items.length === 0 ? nothingOnThisPage2(total, args.page, args.media_type, args.query) : `${items.length} of ${counted(total, "record")} for "${args.query}"${withoutNarrowing}:
1800
2066
  ${renderRecords(items)}`;
1801
2067
  return ok({ query: args.query, total, page: args.page, items, notes }, body, { notes });
1802
2068
  } catch (error) {
@@ -1806,6 +2072,12 @@ ${renderRecords(items)}`;
1806
2072
 
1807
2073
  // src/tools/searchNewspapers.ts
1808
2074
  import { z as z6 } from "zod";
2075
+ function nothingOnThisPage3(total, page, query) {
2076
+ if (total > 0) {
2077
+ return `Page ${page} is past the last of ${counted(total, "newspaper page")} the Library matched for ${query}.`;
2078
+ }
2079
+ return `Nothing found in the scanned newspapers for ${query}.`;
2080
+ }
1809
2081
  var searchNewspapersDescription = [
1810
2082
  "Search the text inside digitised American newspaper pages held by the Library of Congress.",
1811
2083
  "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.",
@@ -1865,8 +2137,12 @@ var searchNewspapersOutput = z6.object({
1865
2137
  var QUOTED_PHRASE = /"[^"]+"/;
1866
2138
  function describeNarrowing2(args) {
1867
2139
  const written = [];
1868
- if (args.location) written.push(`location="${args.location}"`);
1869
- if (args.publication) written.push(`publication="${args.publication}"`);
2140
+ if (args.location) {
2141
+ written.push(`location="${args.location}"`);
2142
+ }
2143
+ if (args.publication) {
2144
+ written.push(`publication="${args.publication}"`);
2145
+ }
1870
2146
  if (args.year_from !== void 0 || args.year_to !== void 0) {
1871
2147
  written.push(`years ${args.year_from ?? "any"} to ${args.year_to ?? "any"}`);
1872
2148
  }
@@ -1885,15 +2161,17 @@ async function runSearchNewspapers(client, args) {
1885
2161
  const budget = { maxChars: args.max_excerpt_chars, maxCount: args.max_excerpts_per_match };
1886
2162
  const facets = {};
1887
2163
  const put = (field, value) => {
1888
- if (value && value.trim() !== "") facets[field] = value;
2164
+ if (value && value.trim() !== "") {
2165
+ facets[field] = value;
2166
+ }
1889
2167
  };
1890
2168
  put("state", args.location);
1891
2169
  put("publication", args.publication);
1892
2170
  const narrowing = describeNarrowing2(args);
1893
2171
  const filters = {
1894
2172
  facets,
1895
- ...args.year_from !== void 0 ? { yearFrom: args.year_from } : {},
1896
- ...args.year_to !== void 0 ? { yearTo: args.year_to } : {}
2173
+ ...args.year_from === void 0 ? {} : { yearFrom: args.year_from },
2174
+ ...args.year_to === void 0 ? {} : { yearTo: args.year_to }
1897
2175
  };
1898
2176
  const notes = [];
1899
2177
  let result = await client.searchNewspapers(args.query, args.limit, args.page, budget, filters);
@@ -1907,7 +2185,9 @@ async function runSearchNewspapers(client, args) {
1907
2185
  }
1908
2186
  }
1909
2187
  const { data, cached, skipped } = result;
1910
- if (cached) notes.push("Served from this server's short-lived in-memory cache.");
2188
+ if (cached) {
2189
+ notes.push("Served from this server's short-lived in-memory cache.");
2190
+ }
1911
2191
  if (skipped) {
1912
2192
  notes.push(
1913
2193
  `${counted(skipped, "match", "matches")} came back in a shape this server could not read and ${agrees(skipped, "was", "were")} left out. The count above is what the Library reported.`
@@ -1927,7 +2207,9 @@ async function runSearchNewspapers(client, args) {
1927
2207
  source_url: hit.sourceUrl
1928
2208
  }));
1929
2209
  const total = data.paging.resultCount;
1930
- if (hits.length > 0) notes.push(OCR_CAVEAT);
2210
+ if (hits.length > 0) {
2211
+ notes.push(OCR_CAVEAT);
2212
+ }
1931
2213
  const openings = hits.filter(
1932
2214
  (hit) => hit.excerpt_kind === "page_opening" && hit.excerpts.length > 0
1933
2215
  ).length;
@@ -1956,7 +2238,7 @@ async function runSearchNewspapers(client, args) {
1956
2238
  `Page ${args.page} is past the last match. ${counted(total, "page")} match, so ask for a lower page.`
1957
2239
  );
1958
2240
  }
1959
- const body = hits.length === 0 ? total > 0 ? `Page ${args.page} is past the last of ${counted(total, "newspaper page")} the Library matched for ${args.query}.` : `Nothing found in the scanned newspapers for ${args.query}.` : `${hits.length} of ${counted(total, "newspaper page")} the Library matched for ${args.query}:
2241
+ const body = hits.length === 0 ? nothingOnThisPage3(total, args.page, args.query) : `${hits.length} of ${counted(total, "newspaper page")} the Library matched for ${args.query}:
1960
2242
  ` + hits.map((hit, index) => {
1961
2243
  const where = [
1962
2244
  `${index + 1}. ${hit.publication ?? hit.title ?? "untitled"}`,
@@ -2067,15 +2349,15 @@ async function main() {
2067
2349
  await server.connect(transport);
2068
2350
  const shutdown = async () => {
2069
2351
  await server.close().catch(() => void 0);
2070
- process.exit(0);
2352
+ process2.exit(0);
2071
2353
  };
2072
- process.on("SIGINT", shutdown);
2073
- process.on("SIGTERM", shutdown);
2354
+ process2.on("SIGINT", shutdown);
2355
+ process2.on("SIGTERM", shutdown);
2074
2356
  }
2075
2357
  main().catch((error) => {
2076
2358
  const message = error instanceof Error ? error.message : String(error);
2077
- process.stderr.write(`[mcp-libraryofcongress] fatal: ${message}
2359
+ process2.stderr.write(`[mcp-libraryofcongress] fatal: ${message}
2078
2360
  `);
2079
- process.exit(1);
2361
+ process2.exit(1);
2080
2362
  });
2081
2363
  //# sourceMappingURL=index.js.map