@cyanheads/smithsonian-mcp-server
Search 14.5M Smithsonian Open Access objects across 20+ museums via MCP, and retrieve CC0 images for the 5.2M that carry openly-licensed media. STDIO or Streamable HTTP.
6 Tools
Overview
Smithsonian Open Access catalog β 14.5 million objects across 20+ museums, with CC0 images for the 5.2 million that carry them. Search by free text or exact category, browse by museum, culture, date, or topic, and discover cross-collection connections from any MCP client. Runs as a stdio process, a local Streamable HTTP server, or the public hosted endpoint above.
| Tool | Description |
|---|
smithsonian_search_objects | Search across 14.5M objects by text query with optional filters (museum, type, date term, culture, place, topic, name, online-only, CC0). Returns curated summaries with total count. |
smithsonian_list_terms | Enumerate the valid term vocabulary for an indexed filter field (unit_code, culture, place, date, online_media_type, topic). Call before filtering to avoid empty results from invalid values; pass contains to resolve a guessed value to its exact term(s). unit_code terms come back with their museum names. |
smithsonian_get_object | Fetch a normalized catalog metadata projection for an object by ID: title, dates, materials, dimensions, exhibition history, credit line, and identifiers. |
smithsonian_get_media | Return all CC0-licensed images for an object at multiple resolutions (thumbnail, screen, high-res JPEG/TIFF). Only CC0 images returned, never an empty list β a distinct error reason names why, whether the object has nothing digitized, only non-image media, or only restricted images. |
smithsonian_browse_category | Browse objects within one exact category (museum, culture, period, medium, topic) with total count, a page of objects, and museum breakdown. Requires an exact indexed category term. |
smithsonian_find_related | Discover cross-collection objects related to an anchor, matched on shared culture, named-party, topic, and period signals. |
Capability reference
- Free-text search over 14.5M objects from 20+ museums
- Filters: museum unit code, object type, indexed date term (
1920s, 500-1500, 21st century, -2500), culture, geographic place, subject topic, named party (name), online-only, CC0-only
topic and name are hard indexed constraints, not free text β topic: "Quilts" matches 1,134 objects where the bare word matches 2,677, and name: "Warhol, Andy" matches 421 against 715
- Returns curated summaries: title, date, museum, object type, thumbnail URL, CC0 flag,
record_id
- Use
start + rows for standard pagination (offset-based, max 100 per page)
- Fields:
unit_code, culture, place, date, online_media_type, topic β a controlled vocabulary, often plural (e.g. Paintings, not Painting); ground filter values here before searching or browsing
contains filters the vocabulary by a case-insensitive substring β resolves a guessed value (e.g. greek β Greek, Attic) in one call; for unit_code it also matches museum names (National Air and Space β NASM), and each unit_code term comes back with a labels map of its museum name
- Paginate with
start + rows (default 50, max 100 per page); topic (133k terms) and place (114k) are large enough to pair with contains
- No per-term object counts are available upstream β only the term values themselves
- Each field's vocabulary is cached for
SMITHSONIAN_TERMS_CACHE_TTL_SECONDS (default 3600 seconds, 0 disables) β upstream ignores paging and returns the whole set on every call, so paging a large vocabulary uncached re-downloads it each time
object_type is not enumerable here β harvest its values from the object_type field in smithsonian_search_objects results
- Input:
record_id from smithsonian_search_objects β do not construct IDs manually
- Returns the exposed catalog fields: title, dates (all labeled), makers (with roles), materials, dimensions, place associations, culture terms, topic/subject terms, exhibition history, accession identifiers, credit line, rights statement
- Media summary included β call
smithsonian_get_media for full image URLs
- Only CC0-licensed images are returned, never restricted content
- Three distinct failure reasons when nothing is returnable:
no_media (nothing digitized), no_images (media exists but is entirely non-image β scanned books, 3D models, sound recordings; the recovery hint names the types present), not_cc0 (images exist but none are CC0, thrown as Forbidden)
- Each image entry includes thumbnail (~120px), screen-size (~800px), and high-resolution JPEG/TIFF URLs with pixel dimensions
- Use
smithsonian_search_objects with filters.cc0_only: true to find objects with downloadable CC0 images before calling this tool
- Five modes:
museum (by unit code, e.g. "NASM" β matched exactly, not by museum name), culture (e.g. "Aztecs"), period (indexed date term, e.g. "1940s" or "500-1500"), medium (object type, e.g. "Paintings"), topic (subject term, e.g. "Quilts")
value must be an exact indexed category term β resolve museum, culture, period, and topic vocabulary with smithsonian_list_terms first; object_type is not enumerable there, so harvest it from smithsonian_search_objects results
- Returns total count, a page of sample objects, and a museum breakdown showing which institutions hold matching items (computed from the current page)
- Use
start + rows for standard pagination (offset-based, start = page Γ rows, max 50 per page) β adjacent pages retrieve the objects a capped sample omits
- A category value that matches nothing throws
invalid_category with a mode-specific recovery hint: a value outside the vocabulary gets the exact smithsonian_list_terms call that resolves it, and a value the index enumerates but that matches no objects is named as such and routed elsewhere
- For open-ended or topic discovery, use
smithsonian_search_objects instead β this tool requires an exact category value
- Matches shared culture, named-party, topic, and period+type metadata signals against the wider catalog; cross-museum discovery is the differentiator β an NASM aerospace anchor may surface related objects from NMNH, SAAM, and NMAH
- The named-party signal carries the catalog's own role for the party (
maker, collector, donor, issuing authority, β¦) rather than a fixed maker label, prefers the indexed name facet as a hard filter when the record has one, and is dropped when its value only repeats the culture signal
- The topic signal is a hard
topic: filter, so every object it tags carries that subject term rather than merely mentioning the word
similarity_signals on each result names every metadata term that connected it to the anchor β an object surfaced by more than one signal carries all of them
- Page with
start β a 0-indexed offset into the interleaved related set, start = page Γ limit; each signal is reachable to a depth of 5,000 (fetched in β€1,000-row chunks β a deeper page can shift an object by a bounded amount near a seam), and a truncated response reports truncationCeiling as an upper bound on the reachable related pool
signals[] breaks the fan-out down per signal: row_count is that signal's true upstream size (uncapped, so it can exceed the 5,000 reach) and search_continuation is the exact smithsonian_search_objects input that retrieves the signal's full match set at any depth β the retrieval path past this tool's per-signal reach
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.
Smithsonian-specific:
- Wraps the Smithsonian Open Access API (14.5M objects across 20+ museums, 5.2M carrying CC0 media) with a free
api.data.gov key
- CC0 gating on
smithsonian_get_media β only open-access images returned, never restricted content
- Graceful degradation in
smithsonian_find_related β a failure in one metadata signal doesn't abort the rest
- Response normalization across heterogeneous museum metadata schemas
Agent-friendly output:
has_media on every object summary β agents can gate image download calls without an extra lookup (the is_cc0 flag is the metadata license, which the Open Access corpus carries almost everywhere)
- Typed error reasons (
no_results, invalid_filter, not_found, no_media, no_images, not_cc0, invalid_id) with recovery hints for each case
similarity_signals on related-object results let agents explain why objects were surfaced
total_count on all search responses enables agents to communicate result scope before paginating
Getting started
Public Hosted Instance
A public instance is available at https://smithsonian.caseyjhand.com/mcp β no installation required. Point any MCP client at it via Streamable HTTP:
{
"mcpServers": {
"smithsonian-mcp-server": {
"type": "streamable-http",
"url": "https://smithsonian.caseyjhand.com/mcp"
}
}
}
Self-Hosted / Local
Requires a free api.data.gov API key β register at https://api.data.gov/signup and set SMITHSONIAN_API_KEY in your config.
Add the following to your MCP client configuration file:
{
"mcpServers": {
"smithsonian-mcp-server": {
"type": "stdio",
"command": "bunx",
"args": ["@cyanheads/smithsonian-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"MCP_LOG_LEVEL": "info",
"SMITHSONIAN_API_KEY": "your-api-key"
}
}
}
}
Or with npx (no Bun required):
{
"mcpServers": {
"smithsonian-mcp-server": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@cyanheads/smithsonian-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"MCP_LOG_LEVEL": "info",
"SMITHSONIAN_API_KEY": "your-api-key"
}
}
}
}
Or with Docker:
{
"mcpServers": {
"smithsonian-mcp-server": {
"type": "stdio",
"command": "docker",
"args": [
"run", "-i", "--rm",
"-e", "MCP_TRANSPORT_TYPE=stdio",
"-e", "SMITHSONIAN_API_KEY=your-api-key",
"ghcr.io/cyanheads/smithsonian-mcp-server:latest"
]
}
}
}
For Streamable HTTP, set the transport and start the server:
MCP_TRANSPORT_TYPE=http MCP_HTTP_PORT=3010 SMITHSONIAN_API_KEY=your-api-key bun run start:http
Prerequisites
Installation
- Clone the repository:
git clone https://github.com/cyanheads/smithsonian-mcp-server.git
- Navigate into the directory:
cd smithsonian-mcp-server
- Install dependencies:
- Configure environment:
Configuration
| Variable | Description | Default |
|---|
SMITHSONIAN_API_KEY | Required. Free API key from api.data.gov/signup. | β |
SMITHSONIAN_BASE_URL | Smithsonian Open Access API base URL. | https://api.si.edu/openaccess/api/v1.0 |
SMITHSONIAN_TERMS_CACHE_TTL_SECONDS | Seconds to cache each indexed field's term vocabulary. 0 disables caching. | 3600 |
MCP_TRANSPORT_TYPE | Transport: stdio or http. | stdio |
MCP_HTTP_PORT | Port for HTTP server. | 3010 |
MCP_SESSION_MODE | Session mode: auto, stateful, or stateless. The server declares stateless in code; set this only to override it. | stateless |
MCP_AUTH_MODE | Auth mode: none, jwt, or oauth. | none |
MCP_LOG_LEVEL | Log level (RFC 5424). | info |
LOGS_DIR | Directory for log files (Node.js only). | <project-root>/logs |
STORAGE_PROVIDER_TYPE | Storage backend. | in-memory |
OTEL_ENABLED | Enable OpenTelemetry instrumentation. | false |
See .env.example for the full list of optional overrides.
Running the server
Local development
-
Build and run:
bun run rebuild
bun run start:stdio
bun run start:http
-
Run checks and tests:
bun run devcheck
bun run test
bun run lint:mcp
Docker
docker build -t smithsonian-mcp-server .
docker run --rm -e SMITHSONIAN_API_KEY=your-api-key -p 3010:3010 smithsonian-mcp-server
The Dockerfile defaults to HTTP transport, stateless session mode, and logs to /var/log/smithsonian-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 and initializes the Smithsonian service. |
src/config | Server-specific environment variable parsing (SMITHSONIAN_API_KEY, SMITHSONIAN_BASE_URL, SMITHSONIAN_TERMS_CACHE_TTL_SECONDS). |
src/mcp-server/tools | Tool definitions (*.tool.ts). |
src/services/smithsonian | Smithsonian Open Access API client, normalization, and type definitions. |
tests/ | Unit and integration tests. |
docs/ | Design document and directory tree. |
Development guide
See CLAUDE.md / AGENTS.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 via the barrel in
src/mcp-server/tools/definitions/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.