@cyanheads/libofcongress-mcp-server
Search LOC digital collections, browse Chronicling America newspapers with full OCR text, and look up LC Subject Headings via MCP. STDIO or Streamable HTTP.
6 Tools • 1 Resource • 1 Skill
Overview
Library of Congress digital collections, Chronicling America historic newspapers, and LC Subject Headings. Search items and newspaper pages, read full metadata and OCR text, resolve subject terms, and browse curated collections.
Tools
Resources
All resource data is also reachable via libofcongress_get_item. Use libofcongress_search_items to discover item IDs first.
Research guide
libofcongress-guide covers identifier chains, OCR uncertainty, research coverage, and collection, newspaper, book, and manuscript workflows. It ships with plugin, npm, Docker, and MCPB installs and is also available through MCP skill discovery and resource reads.
Capability reference
libofcongress_search_items tool
- Filters: eight material formats (
photo, map, newspaper, manuscript, audio, film, book, notated-music), inclusive year range (date_start/date_end), subject (an LCSH heading from libofcongress_search_subjects or a facet term), location, and collection_slug (from libofcongress_browse_collections, combinable with format); up to 100 results per page — a larger limit (up to 1000) is served as 100 with effectiveLimit and a notice
- Use
query: "*" to browse without a keyword. sort accepts relevance (default), date (oldest first), or date_desc (newest first); effectiveSort echoes the order LOC applies before pagination
- Each result carries
date as LOC catalogs it and is_item: true for catalog items whose id resolves via libofcongress_get_item, false for other resources. Newspaper rows include page_url for libofcongress_get_newspaper_page; retired catalog links carry link_notice and retain their original URL. Other non-item rows can be opened at url
include_facets: true adds subject_facets and location_facets (LOC's top 10 terms across all matches, each with its item count); off by default since it slows every search. A zero-result notice names the subject terms and location applied
libofcongress_get_item tool
- Takes a search result's
id (simple or multi-segment, e.g. sn95047246/1935-09-05/ed-1), a loc.gov item link, or an lccn.loc.gov permalink; any other URL is rejected as invalid_item_id before any request
- Returns the
imprint, contributors with roles, LCSH subject_headings, subject_terms (the lowercase terms the search subject filter accepts), notes, summary, languages, locations, rights_information as plain text, physical description, call number, former IDs, formats, and access_restricted; fields absent upstream are omitted
resources lists each digital resource with its page count, image, pdf, and audio (MP3). pages lists 50 pages at a time by default (page_offset, page_limit, page_total, next_page_offset; at most 100 per call — a larger page_limit, up to 1000, is served as 100 with effectivePageLimit and a notice), each with a page_url and has_text that read through libofcongress_get_newspaper_page. include_files: true adds each page's download URLs
libofcongress_search_newspapers tool
- Filters: keyword, inclusive date range down to the day (
date_start/date_end take an integer year or a YYYY, YYYY-MM, or YYYY-MM-DD string), indexed state/province (full name or supported US postal code), and newspaper title (the exact newspaper_title of a search result); up to 100 results per page — a larger limit (up to 1000) is served as 100 with effectiveLimit and a notice
sort accepts relevance (default), date (oldest first), or date_desc (newest first); effectiveSort echoes the applied order
- Each result carries the opening ~500 characters of the page's OCR text,
states, and the url that libofcongress_get_newspaper_page takes verbatim
- The excerpt is the page's first lines, not the text around the match; pass the same query as
match to libofcongress_get_newspaper_page for the passages around it
libofcongress_get_newspaper_page tool
- Takes the
url from a libofcongress_search_newspapers result, or a pages[].page_url from libofcongress_get_item (newspaper, book, and manuscript pages alike), plus Chronicling America page links; a link that names no single page, any other host, or a malformed path or sp is rejected as invalid_page_url before any request
- Returns the page's text as
ocr_text with newspaper_title, date, place_of_publication, states, edition, sequence, segment_count, item_title, and item_id (pass it to libofcongress_get_item to list the item's pages). ocr_available: false marks an image-only page, and a notice explains an empty result: the text service did not answer (retry), the body could not be read as text, or the page is blank
match returns only the passages around a term — pass the query you searched with. Space-separated words are separate terms and a double-quoted span is one phrase, matched case-insensitively. matches holds up to 10 passages of about 300 characters either side of each occurrence, match_count counts every occurrence, and ocr_text is empty; a notice says when nothing matched or some occurrences fall outside the passages. If text retrieval fails, both match fields are absent because the result is unknown; blank and image-only pages still report zero
libofcongress_search_subjects tool
- Keyword query, up to 250 results per call (default 10; a larger
limit, up to 1000, returns up to 250 with a notice); returns the authorized LCSH label and stable LOC uri
- Start at
offset: 0 (default), then pass next_offset to continue. total counts this page's headings; the final or past-end page omits next_offset. A past-end offset succeeds empty with a recovery notice
- An exact match from the first 50 results moves to the start. Subsequent offsets and changed limits preserve that ordering while the upstream results remain unchanged
- Pass the label verbatim as
libofcongress_search_items's subject filter. A partial word (aerial photog) falls back to headings that begin with it
libofcongress_browse_collections tool
- Optional keyword filter on collection name and description; up to 100 collections per page — a larger
limit (up to 1000) is served as a 100-collection page with effectiveLimit and a notice
- Each collection carries a
slug for libofcongress_search_items's collection_slug (taken from the loc.gov route, not derivable from the title); item_count is approximate and omitted when LOC doesn't provide it
libofcongress://item/{+item_id} resource
- Returns the same record as
libofcongress_get_item, as application/json, with the first 50 pages and no files; next_page_offset marks where libofcongress_get_item's page_offset continues
item_id comes from a libofcongress_search_items result's id; anything that is not an item ID is rejected as invalid_item_id. Multi-segment newspaper IDs keep their slashes (libofcongress://item/sn95047246/1935-09-05/ed-1)
Features
Built on @cyanheads/mcp-ts-core: stdio and Streamable HTTP transports, pluggable auth (none / jwt / oauth), swappable storage (in-memory, filesystem, Supabase, Cloudflare KV/R2/D1), structured logging with optional OpenTelemetry tracing.
Library of Congress-specific:
- Request queue in front of
www.loc.gov: calls start in arrival order, at least LOC_REQUEST_DELAY_MS apart (default 3100ms, ~19 req/min), never exceeding LOC's 20 per minute. A call that cannot start in time fails as server_busy with retryAfter
- Rate-limit handling: a 429 triggers LOC's 1-hour block, and calls fail without sending anything until it lifts (never retried); a Cloudflare challenge or HTML page instead of JSON fails as
upstream_unavailable with retryAfter: 60. libofcongress_search_subjects (id.loc.gov) is unaffected
- A 50-second budget per tool call covers queue wait, retries, and the OCR fetch, so a slow upstream fails as
request_timed_out; other upstream failures (dropped connections, 5xx or 403, empty or truncated bodies) fail as upstream_unavailable with a retry hint
- Pages past the end of the results or past the retrieval ceiling return an empty result with a notice, not an error
- Newspaper OCR is surfaced as-is; 19th-century or degraded pages may contain garbled text
Agent-friendly output:
- Empty results include a
notice with recovery hints that echo the applied filters
- Item and newspaper searches carry pagination status (
total, page, pages, has_next), capped at LOC's ~100,000-item retrieval ceiling; subject searches use offset and next_offset with a page-local total
ocr_available and is_item let callers branch without parsing text
- Every typed error carries a recovery hint
Getting started
Connect Claude Code to the hosted server:
Or run it locally:
For other clients, add this to the MCP config file:
No API key is required. Set LOC_USER_AGENT to identify your application to LOC. For Docker or a source build, see Running the server.
Configuration
All configuration is validated at startup via Zod schemas in src/config/server-config.ts.
See .env.example for the full list of optional overrides.
Running the server
Local development
Requires Bun v1.4.0 or higher (or Node.js v24+).
-
Build and run:
-
Run checks and tests:
Docker
The image defaults to HTTP transport, stateless session mode, and logs to /var/log/libofcongress-mcp-server. For stdio in an MCP client, use "command": "docker" with "args": ["run", "-i", "--rm", "-e", "MCP_TRANSPORT_TYPE=stdio", "ghcr.io/cyanheads/libofcongress-mcp-server:latest"]. Build locally with docker build -t libofcongress-mcp-server .; OpenTelemetry peer dependencies are installed by default — build with --build-arg OTEL_ENABLED=false to omit them.
Project structure
Development guide
See CLAUDE.md for development guidelines and architectural rules. The short version:
- Handlers throw, framework catches — no
try/catch in tool logic
- Use
ctx.log for request-scoped logging, ctx.state for tenant-scoped storage
- Register new tools and resources via the arrays in
src/index.ts
- Wrap external API calls: validate raw → normalize to domain type → return output schema; never fabricate missing fields
Contributing
Issues are welcome. Run checks and tests before submitting:
License
Apache-2.0 — see LICENSE for details.