mcp server
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.
Set up
Runs on your computer. Pick your agent and operating system.
Before you start
- Node.js 20 or newer
{
"mcpServers": {
"libofcongress-mcp-server": {
"command": "npx",
"args": [
"-y",
"@cyanheads/libofcongress-mcp-server"
]
}
}
}same format Cursor, GitHub Copilot CLI, Junie, JetBrains AI Assistant, Augment (Auggie CLI), Factory Droid, Kiro, Devin CLI +12
Values in angle brackets are placeholders. Restart Claude Desktop after editing its config.Claude Desktop docs
README
synced Oct 14, 2026Public Hosted Server: https://libofcongress.caseyjhand.com/mcp
Overview
Library of Congress digital collections, Chronicling America newspaper archives, and LC Subject Headings (LCSH) authority data. Search items and newspaper pages, retrieve full item metadata and OCR text, resolve LCSH subject terms, and browse curated collections from any MCP client. Runs as a stdio process, a local Streamable HTTP server, or the public hosted endpoint above.
Tools
| Tool | Description |
|---|---|
libofcongress_search_items | Search LOC digital collections by keyword with format, date range, subject, location, and collection filters. |
libofcongress_get_item | Retrieve full metadata for a specific LOC digital item — imprint, contributors, subjects, rights, formats, its digital resources, and a paged list of its pages. |
libofcongress_search_newspapers | Search historical newspaper pages in the Chronicling America corpus, with an excerpt of each page's opening OCR text. |
libofcongress_get_newspaper_page | Retrieve the full text and metadata of one page — a newspaper page, or a book or manuscript page from libofcongress_get_item — or only the passages around a search term. |
libofcongress_search_subjects | Search Library of Congress Subject Headings (LCSH) by keyword. |
libofcongress_browse_collections | List and browse LOC curated digital collections, optionally filtered by keyword. |
Resources
| Resource | Description |
|---|---|
libofcongress://item/{+item_id} | LOC digital item metadata by ID — stable URI for injecting item context into agent conversations. |
All resource data is also reachable via libofcongress_get_item. Use libofcongress_search_items to discover item IDs first.
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 fromlibofcongress_search_subjectsor a facet term, split at--into lowercase terms with dates dropped and ANDed —United States--History--Civil War, 1861-1865filters onunited states+history+civil war), location, andcollection_slug(fromlibofcongress_browse_collections, mutually exclusive withformat); up to 100 results per page — a largerlimit(up to 1000) is served as a 100-result page witheffectiveLimitand a notice - Each result's
dateis the date as LOC catalogs it (c1915 Feb. 19.), not the paddedYYYY-01-01sort key LOC uses for photos; where the catalog holds only a coded value (18950000,185u,uuuu), the row's readable date (1895,185?) stands in, ordateis absent - Each result carries
is_item—truefor catalog items whoseidresolves vialibofcongress_get_item, records keyed by anlccn.loc.govpermalink included (theiridis the bare LCCN),falsefor collections, exhibit/guide pages, newspaper pages,catalog.loc.govrecords, andhdl.loc.govfinding aids, whoseurlshould be opened instead include_facets: trueaddssubject_facetsandlocation_facets— LOC's top 10 subject and location terms across all matches, each with its item count, the values thesubjectandlocationfilters accept. Off by default, since the facet block adds LOC response time to every search- A zero-result notice names the subject terms and location it applied
libofcongress_get_item tool
- Takes a search result's
id, simple or multi-segment (newspaper issues:sn95047246/1935-09-05/ed-1), or a loc.gov item link (https://www.loc.gov/item/2017760320/,http://or withoutwww.,item/2017760320), read as its bare ID, or an LCCN permalink (https://lccn.loc.gov/2005567251,http://or without a scheme), read as its LCCN in normalized form (lccn.loc.gov/2005-568294as2005568294,sn 83030214assn83030214); any other URL — a/resource/page URL, anotherlccn.loc.govpath, another host — is rejected asinvalid_item_idbefore any request. Returns theimprint(place, publisher, and date as cataloged:New York : Werner School Book Company, c1900.), contributors with their roles, LCSHsubject_headingsas cataloged (Transportation--Oklahoma--Maps),subject_terms(the lowercase facet terms the searchsubjectfilter accepts), notes, summary, languages, locations, rights, physical description, call number, former IDs, formats, andaccess_restricted dateis the date as LOC catalogs it (1936 Apr.,1909-1911.), not the padded sort key LOC keeps for photos, and is absent when the record holds only a coded value (uuuu,||||,0w0 d1804 d1806)rights_informationis plain text: where LOC sends its rights statement as HTML, the tags are dropped and each link's target is kept as an absolute URL (Copyright and other Restrictions (https://www.loc.gov/legal/)). Catalog text fromwww.loc.govarrives with its HTML character references (’,…) decoded, on every tool; page text fromlibofcongress_get_newspaper_pageis returned as the text service sends it, since that text is not HTML-escapedresourceslists each digital resource with its page count, representativeimage,pdf, andaudio(MP3);pageslists a window of the item's pages — each apage_urlandhas_text— 50 at a time by default (page_offset,page_limit,page_total,next_page_offset; at most 100 per call — a largerpage_limit, up to 1000, is served as 100 witheffectivePageLimitand a notice). A page with text reads throughlibofcongress_get_newspaper_page; a blank page can still showhas_text: true(LOC lists a placeholder text file for it), and reads as empty text with a notice.include_files: trueadds each listed page's download URLs (JPEG sizes, JP2, TIFF, text)related_itemscarries related record IDs or URLs in full; fields absent upstream are omitted rather than filled
libofcongress_search_newspapers tool
- Filters: keyword, inclusive date range down to the day (
date_start/date_endtake an integer year or aYYYY,YYYY-MM, orYYYY-MM-DDstring), US state (full name or postal code, matched against the state LOC indexes each title under), and newspaper title (the exactnewspaper_titleof a search result); up to 100 results per page — a largerlimit(up to 1000) is served as a 100-result page witheffectiveLimitand a notice - Each result carries the opening ~500 characters of the page's OCR text (in a fenced code block in the text output),
states(every state LOC indexes the title under), and theurlthatlibofcongress_get_newspaper_pagetakes verbatim - The excerpt is the page's first lines, not the text around the match; pass the same query as
matchtolibofcongress_get_newspaper_pagefor the passages around it
libofcongress_get_newspaper_page tool
- Takes the
urlfrom alibofcongress_search_newspapersresult, or apages[].page_urlfromlibofcongress_get_item— newspaper, book, and manuscript pages alike. Also reads the same/resource/URL overhttp://or withoutwww., a newspaper page path ending/ed-N/seq-M/, and a Chronicling America page link (chroniclingamerica.loc.gov/lccn/<lccn>/<date>/ed-N/seq-M/orwww.loc.gov/chroniclingamerica/lccn/…), each as itshttps://www.loc.gov/resource/…/?sp=Mpage; the outputpage_urlis that URL, with only thespquery parameter kept. Rejected asinvalid_page_urlbefore any request: a Chronicling America link that names no single page (an issue link withoutseq-M, an/ocr/view, a file link), any other host, a path with a dot segment or percent-escape, and a non-numericsp - Returns that page's own text (OCR, or a volunteer transcription for some manuscripts) as
ocr_text— in the text output, a fenced code block holding the same text, so OCR noise never reads as Markdown — with the issue'snewspaper_title,date,place_of_publication,states,edition, the page'ssequence, and the page countsegment_count;ocr_available: falsemarks an image-only page, and anoticesays why a page with text came back empty — a text service that did not answer (retry), a body that could not be read as text (retrying will not help), or a blank page. LOC's blank-page placeholder ([NO TEXT AVAILABLE FOR THIS PAGE]as the whole text) is returned as emptyocr_text item_titleis the title of the item the page belongs to, on every page type — the book or manuscript title (the text output's heading for those pages), or a newspaper issue's title with place and date beside the unchangednewspaper_titleitem_idnames the item the page belongs to — the newspaper issue (sn83030214/1912-04-16/ed-1), book, or manuscript volume (mm77093930); pass it tolibofcongress_get_itemto list every page with itspage_urlandhas_text, to follow an article onto a later page or read a book page by page. It comes from the page response LOC already sends, so it costs no extra requestmatchreturns the passages around a term instead of the whole page — pass the query you searched with. Space-separated words are separate terms and a double-quoted span is one phrase; each matches case-insensitively as literal text, with line breaks read as spaces.matchesholds up to 10 passages, about 300 characters either side of each occurrence (overlapping ones merged up to 1,200 characters, never overlapping, in page order; when more would be needed, the rarest terms' occurrences are chosen first, sosinking of the titanicshows the Titanic rather than ten passages of "the"),match_countcounts every occurrence, andocr_textis empty; a notice says when nothing matched or some occurrences fall outside the passages, which the whole page (nomatch) holds. On a 50,241-character page of the 16 April 1912 New-York Tribune,match: "titanic"returns 2 passages of about 600 characters each — about 3 KB across both response surfaces instead of 104 KB
libofcongress_search_subjects tool
- Keyword query, up to 50 results per call (default 10; a larger
limit, up to 1000, returns up to 50 with a notice); returns the authorized LCSHlabeland stable LOCuri, ranked by relevance with an exact match for the query first — pass the label verbatim aslibofcongress_search_items'ssubjectfilter, since LCSH uses standardized wording ("World War, 1939-1945", "Aerial photography") - A partial word with no keyword match (
aerial photog) falls back to the headings that begin with it; when more headings match than the call returns, the response says how many
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 witheffectiveLimitand a notice - Each collection carries a
slugforlibofcongress_search_items'scollection_slug(taken from the loc.gov route, not derivable from the title);item_countis approximate and omitted when LOC doesn't provide it
libofcongress://item/{+item_id} resource
- Returns the same full record as
libofcongress_get_item, asapplication/json, with its default page window (the first 50 pages, no files);next_page_offsetmarks wherelibofcongress_get_item'spage_offsetcontinues item_idcomes from alibofcongress_search_itemsresult'sidfield, or fromlibofcongress_get_item; it is checked like the tool'sitem_id, so a URL or path that is not an item ID is rejected asinvalid_item_idbefore any request- Multi-segment newspaper IDs keep their slashes intact (e.g.
libofcongress://item/sn95047246/1935-09-05/ed-1); percent-encoded slashes (%2F) also resolve
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:
- One request queue in front of
www.loc.gov: concurrent calls start in arrival order, at leastLOC_REQUEST_DELAY_MSapart (default 3100ms, ~19 req/min), and no 60-second window holds more than LOC's documented 20 starts, whatever the delay is set to. A call the queue cannot start within its time budget fails at once asserver_busy, with the seconds to wait inretryAfter - Rate-limit block enforcement: a 429 triggers LOC's 1-hour block, checked again as each queued request is dispatched, so calls already waiting fail without sending anything; the error's recovery hint counts down the minutes left and names the time it lifts
- Soft-block pause: when
www.loc.govanswers with a Cloudflare challenge (cf-mitigated: challenge) or an HTML page instead of JSON — LOC's documented CAPTCHA response under heavy load — the call fails asupstream_unavailablewithretryAfter: 60, and furtherwww.loc.govrequests, queued ones included, are refused for 60 seconds without being sent.libofcongress_search_subjects(id.loc.gov) is unaffected - Out-of-range page handling: a page past the end of the results (HTTP 404 on any page after the first) or past the retrieval ceiling (HTTP 400) returns an empty result with a notice, not an error — both point back to page 1 for the real page count, and the ceiling notice also explains how to partition a search that matches more than LOC will page through
- One 50-second budget per tool call covers queue wait, every attempt including its body read, retry backoff, and the newspaper page's OCR fetch, so a slow upstream fails as
request_timed_outwith a recovery hint before a client's 60-second request timeout. Network drops retry with backoff while budget remains; the 429 rate-limit path is never retried, since a retry would deepen LOC's 1-hour block - An unreachable or misbehaving upstream — refused or dropped connections once retries run out, a 5xx or 403, or a 200 carrying an empty or truncated body or JSON that is not a response object — fails as
upstream_unavailablewith a retry hint, on every tool and the item resource. The message names the upstream and any HTTP status, never a URL, host, or transport error text - Newspaper OCR is surfaced as-is: quality varies by digitization batch and era, and 19th-century or degraded pages may contain garbled text
Agent-friendly output:
- Empty results always include a
noticefield with recovery hints — echoes the applied filters and suggests how to broaden - Pagination status on every search response (
total,page,pages,has_next), capped at LOC's ~100,000-item retrieval ceiling, with a notice disclosing how to page past it ocr_availableandis_itemdiscriminator fields let callers branch on data availability without parsing text- Recovery hints on every typed error contract — actionable next steps for the agent on every failure mode
Getting started
Public Hosted Instance
A public instance is available at https://libofcongress.caseyjhand.com/mcp — no installation required. Point any MCP client at it via Streamable HTTP:
{
"mcpServers": {
"libofcongress-mcp-server": {
"type": "streamable-http",
"url": "https://libofcongress.caseyjhand.com/mcp"
}
}
}
Self-Hosted / Local
Add the following to your MCP client configuration file.
{
"mcpServers": {
"libofcongress-mcp-server": {
"type": "stdio",
"command": "bunx",
"args": ["@cyanheads/libofcongress-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"MCP_SESSION_MODE": "stateless",
"MCP_LOG_LEVEL": "info"
}
}
}
}
Or with npx (no Bun required):
{
"mcpServers": {
"libofcongress-mcp-server": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@cyanheads/libofcongress-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"MCP_SESSION_MODE": "stateless",
"MCP_LOG_LEVEL": "info"
}
}
}
}
Or with Docker:
{
"mcpServers": {
"libofcongress-mcp-server": {
"type": "stdio",
"command": "docker",
"args": [
"run", "-i", "--rm",
"-e", "MCP_TRANSPORT_TYPE=stdio",
"-e", "MCP_SESSION_MODE=stateless",
"ghcr.io/cyanheads/libofcongress-mcp-server:latest"
]
}
}
}
For Streamable HTTP, set the transport and start the server:
MCP_TRANSPORT_TYPE=http MCP_HTTP_PORT=3010 bun run start:http
# Server listens at http://localhost:3010/mcp
Prerequisites
- Bun v1.4.0 or higher (or Node.js v24+).
- No API key required — the LOC JSON API and LC Linked Data endpoints are open. LOC recommends a descriptive
LOC_USER_AGENTfor polite access.
Installation
- Clone the repository:
git clone https://github.com/cyanheads/libofcongress-mcp-server.git
- Navigate into the directory:
cd libofcongress-mcp-server
- Install dependencies:
bun install
- Configure environment:
cp .env.example .env
# edit .env if you want to set LOC_USER_AGENT or LOC_REQUEST_DELAY_MS
Configuration
All configuration is validated at startup via Zod schemas in src/config/server-config.ts.
| Variable | Description | Default |
|---|---|---|
LOC_USER_AGENT | User-Agent header sent with LOC API requests. LOC recommends a descriptive value for polite access. | libofcongress-mcp-server/0.5.1 |
LOC_REQUEST_DELAY_MS | Delay in milliseconds between LOC API requests to stay under the 20 req/min rate limit. | 3100 |
MCP_TRANSPORT_TYPE | Transport: stdio or http. | stdio |
MCP_HTTP_PORT | Port for HTTP server. | 3010 |
MCP_AUTH_MODE | Auth mode: none, jwt, or oauth. | none |
MCP_SESSION_MODE | HTTP session mode. This server is explicitly stateless. | stateless |
MCP_LOG_LEVEL | Log level (RFC 5424). | info |
LOGS_DIR | Directory for log files (Node.js only). | <project-root>/logs |
LOG_TOOL_FAILURE_PAYLOADS | Log each failed tool call's arguments and result, redacted by key name and capped at LOG_TOOL_FAILURE_PAYLOAD_MAX_BYTES (default 16384). A secret inside a free-form value is not redacted. | false |
STORAGE_PROVIDER_TYPE | Storage backend. | in-memory |
OTEL_ENABLED | Enable OpenTelemetry instrumentation (spans, metrics, completion logs). | false |
See .env.example for the full list of optional overrides.
Running the server
Local development
-
Build and run:
# One-time build bun run rebuild # Run the built server bun run start:stdio # or bun run start:http -
Run checks and tests:
bun run devcheck # Lint, format, typecheck, security bun run test # Vitest test suite bun run lint:mcp # Validate MCP definitions against spec
Docker
docker build -t libofcongress-mcp-server .
docker run --rm -p 3010:3010 libofcongress-mcp-server
The Dockerfile defaults to HTTP transport, stateless session mode, and logs to /var/log/libofcongress-mcp-server. OpenTelemetry peer dependencies are installed by default — build with --build-arg OTEL_ENABLED=false to omit them.
Project structure
| Directory | Purpose |
|---|---|
src/index.ts | createApp() entry point — registers tools, resource, and initializes services. |
src/config | Server-specific environment variable parsing (LOC_USER_AGENT, LOC_REQUEST_DELAY_MS). |
src/mcp-server/tools | Tool definitions (*.tool.ts) — six LOC tools. |
src/mcp-server/resources | Resource definitions — libofcongress://item/{+item_id}. |
src/services/loc-api | LocApiService wrapping www.loc.gov — search, item fetch, page text, collection browser. |
src/services/lc-linked-data | LcLinkedDataService wrapping id.loc.gov — LCSH subject heading suggest. |
tests/ | Unit and integration tests mirroring src/. |
Development guide
See CLAUDE.md for development guidelines and architectural rules. The short version:
- Handlers throw, framework catches — no
try/catchin tool logic - Use
ctx.logfor request-scoped logging,ctx.statefor 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:
bun run devcheck
bun run test
License
Apache-2.0 — see LICENSE for details.