@cyanheads/met-museum-mcp-server
Search the Metropolitan Museum of Art collection, browse it by department or update date, fetch full artwork records and open-access images via MCP. STDIO or Streamable HTTP.
Public Hosted Server: https://met-museum.caseyjhand.com/mcp
Overview
The Metropolitan Museum of Art's collection of 500,000+ works spanning 5,000 years. Search by keyword and filters, browse by department, and fetch full object records with provenance and CC0 open-access images.
Tools
| Tool | Description |
|---|---|
met_list_departments | Return all 19 curatorial departments with their numeric IDs and display names |
met_search_collections | Search the collection by keyword, optionally matched in titles or tags only, with filters for department, date range, medium, geography, image availability, on-view status, and highlight designation |
met_list_objects | List every object ID in a department, every object created or revised since a date, or both, without a keyword |
met_get_object | Fetch full records for one or more object IDs — metadata, provenance, artist info, CC0 image URLs, tags, and Wikidata links — optionally with up to 3 CC0 images as image content |
Capability reference
met_list_departments tool
- No input; returns all 19 curatorial departments, each a numeric
departmentIdwith itsdisplayName(e.g., "Egyptian Art") departmentIdvalues are the valid input for thedepartmentIdfilter onmet_search_collectionsandmet_list_objects- Typed errors:
upstream_blocked,upstream_unavailable(the Met answered 500/502/503/504 through every retry),upstream_rate_limited(the Met answered 429), andretry_deadline_exceeded(the call's time budget ran out)
met_search_collections tool
- Keyword
q(required), matched across all text fields unlessmatchFieldnarrows it to"title"or"tags";"*"matches everything, for a search by filters alone. Filters:departmentId,medium(case-sensitive, e.g."Paintings"),dateBegin/dateEnd(integer years, negative = BCE, set together), onegeoLocation,hasImages,isOnView, andisHighlight(trueonly) - Up to 500 IDs per page (default 20), paged by
offset/nextOffsetthrough the first 10,000 matches;totalreports the full count.departmentId7 (The Cloisters) and 17 (Medieval Art) share one result set at the Met, so a page can hold fewer thanlimitIDs, which anoticeflags - Zero matches is a result with a
notice, not an error; every result echoes the applied query aseffectiveQuery. Typed errors:invalid_date_range,invalid_filter(a blankq,medium, orgeoLocation),invalid_department,upstream_blocked,upstream_unavailable,upstream_rate_limited,retry_deadline_exceeded
met_list_objects tool
- Filters
departmentId(frommet_list_departments) andupdatedSince(YYYY-MM-DD, records created or revised on or after that day), alone or together; with neither, the whole collection (over 500,000 IDs). Up to 500 IDs per page (default 20) in ascending order, paged byoffset/nextOffsetwith no depth limit - An empty list is a result with a
notice, not an error; every result echoes the applied filters aseffectiveQuery. Typed errors:invalid_department,invalid_date(an impossible date such as2026-02-30),upstream_blocked,upstream_unavailable,upstream_rate_limited,retry_deadline_exceeded - Lists are cached for up to an hour, so a record revised in the last hour may not appear yet
met_get_object tool
- Fetches 20 distinct IDs per call, from
met_search_collectionsormet_list_objects; a list of up to 500 is accepted, and the IDs past the first 20 come back inremainingIDs. The index can list IDs the Met no longer serves, so drop an ID that 404s - Partial success: per-ID errors land in
failed[], and the call fails only when every ID does (all_not_found,upstream_blocked,upstream_rate_limited,retry_deadline_exceeded,upstream_unavailable, orall_failed). Records past a 60,000-bytestructuredContentbudget are listed indeferred[]with their sizes, to re-request - Non-public-domain objects (
isPublicDomain/hasCC0Image) return emptyprimaryImage,primaryImageSmall, andadditionalImages.includeImages: trueattaches the CC0 image (about 600 px) of the first 3 records that have one as image content incontent[]only, with each record's outcome inimages[]
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.
Met Museum-specific:
- 500K+ artworks spanning 5,000 years from the Met's public collection API
- CC0 open-access data from The Metropolitan Museum of Art — free to use without permission or attribution
- Parallel batch fetching with configurable concurrency for
met_get_object - Linked data on every object — Getty ULAN and AAT URLs, Wikidata entity URLs for artists, tags, and works
- Met API failures surface on every tool as typed errors (
upstream_blockedfor a 403 firewall block,upstream_unavailablefor 5xx,upstream_rate_limitedfor 429), never as the Met's raw error page met_get_objectstops requesting the rest of a batch once the Met is blocking, down, or throttling, so a bad batch costs only a few requests
Agent-friendly output:
- Provenance on every record:
isPublicDomainandhasCC0Imageflags show which images can be displayed - Partial failure reporting:
met_get_objectreturnsobjectsandfailedarrays, so successes arrive alongside per-ID errors - Truncation signaling:
met_search_collectionsandmet_list_objectsreturntotal,returned,truncated,remaining,nextOffset, and the resolvedoffset - Byte-budget disclosure:
met_get_objectlistsdeferred[]records with their sizes when a batch is too large
Getting started
Public Hosted Instance
A public instance is available at https://met-museum.caseyjhand.com/mcp — no installation required. Point any MCP client at it via Streamable HTTP:
{
"mcpServers": {
"met-museum-mcp-server": {
"type": "streamable-http",
"url": "https://met-museum.caseyjhand.com/mcp"
}
}
}
Self-Hosted / Local
Add the following to your MCP client configuration file.
{
"mcpServers": {
"met-museum-mcp-server": {
"type": "stdio",
"command": "bunx",
"args": ["@cyanheads/met-museum-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"MCP_LOG_LEVEL": "info"
}
}
}
}
Or with npx (no Bun required):
{
"mcpServers": {
"met-museum-mcp-server": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@cyanheads/met-museum-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"MCP_LOG_LEVEL": "info"
}
}
}
}
Or with Docker:
{
"mcpServers": {
"met-museum-mcp-server": {
"type": "stdio",
"command": "docker",
"args": [
"run", "-i", "--rm",
"-e", "MCP_TRANSPORT_TYPE=stdio",
"ghcr.io/cyanheads/met-museum-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 Met Collection API is public and unauthenticated.
Installation
- Clone the repository:
git clone https://github.com/cyanheads/met-museum-mcp-server.git
- Navigate into the directory:
cd met-museum-mcp-server
- Install dependencies:
bun install
- Configure environment:
cp .env.example .env
# edit .env as needed (all vars are optional)
Configuration
All configuration is validated at startup via Zod schemas in src/config/server-config.ts.
| Variable | Description | Default |
|---|---|---|
MCP_TRANSPORT_TYPE | Transport: stdio or http | stdio |
MCP_HTTP_PORT | HTTP server port | 3010 |
MCP_SESSION_MODE | HTTP session mode: auto, stateful, or stateless | stateless |
MCP_AUTH_MODE | Authentication: none, jwt, or oauth | none |
MCP_LOG_LEVEL | Log level (debug, info, warning, error) | 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 |
OTEL_ENABLED | Enable OpenTelemetry instrumentation | false |
MET_BASE_URL | Met Collection API root; override for local stubs | https://collectionapi.metmuseum.org/public/collection |
MET_REQUEST_TIMEOUT_MS | Per-request HTTP timeout in milliseconds | 10000 |
MET_CALL_DEADLINE_MS | Time budget in milliseconds for one tool call, retries included; exceeding it fails with retry_deadline_exceeded | 30000 |
MET_BATCH_CONCURRENCY | Max parallel fetches in met_get_object | 5 |
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 met-museum-mcp-server .
docker run --rm -p 3010:3010 met-museum-mcp-server
The Dockerfile defaults to HTTP transport, stateless session mode, and logs to /var/log/met-museum-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 inits the Met service. |
src/config | Server-specific environment variable parsing and validation with Zod. |
src/mcp-server/tools | Tool definitions (*.tool.ts) — met_list_departments, met_search_collections, met_list_objects, met_get_object. |
src/services/met | Met Collection API client — HTTP, call deadline, response normalization, cached object-ID lists. |
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 via the arrays in
createApp()insrc/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
Data attribution
Data from The Metropolitan Museum of Art Collection API (CC0).
License
Apache-2.0 — see LICENSE for details.