Skip to main content
Glama

OpenPlate

Know what you are allowed to put in print.

Live app MIT Next.js 16 TypeScript strict Neon Postgres Live feeds MCP Tests CI

Live app · GitHub · Health · Agent · Issues


A painting can be out of copyright and the photograph of it can still be protected.

That sentence is the whole problem, and almost every tool gets it wrong in the same direction. Museums publish two different things about one artwork:

What the institution publishes

Example, from a real record

The artwork

whether the work is in copyright

Monet, 1867, creator died 1926 → life + 70 expired in 1997

The reproduction

whether their picture of it is in copyright

The Met's record for it is flagged in copyright

Neither answer is wrong. They are different rights, and only the first one is usually what the person using the image checked. OpenPlate reads both, does the arithmetic on the published term, and shows the sentence behind every point of the score.

It exists because the job is real and annoying. Somebody has to put a picture in a newsletter, a deck, a print run or a game, has to be able to say why they are allowed to, and has to hand a colleague something checkable instead of a guess.

✨ Features

  • Live search across two open-access collections. The Metropolitan Museum of Art and The Cleveland Museum of Art, no API key, with the institution's own rights claim on every record.

  • The artwork/reproduction distinction, made explicit. Term arithmetic and reproduction restrictions are reported separately, because averaging them produces a number that means nothing.

  • A versioned, explainable engine. Six weighted factors, each with its raw value, its points and a sentence you could check. Identical arithmetic in the browser, the REST API and the agent tools.

  • Restriction text parsed out of the real statement. The Met's Picasso records read "Ac 2026 Estate of Pablo Picasso / Artists Rights Society (ARS), New York". OpenPlate finds that estate, that collecting society and that holder, and reports the offsets so the interface can underline the exact words.

  • A step wedge you drag to watch the verdict move as the intended use escalates from personal to broadcast — a real hypothetical, not an animation.

  • A SHA-384 hash chain per plate, replayable from a fixed genesis value, with a known vector in the tests and a public verify endpoint.

  • Six MCP tools over JSON-RPC 2.0, including two mutations that go through the same service layer the buttons use.

  • A downloadable docket in Markdown, CSV and JSON, carrying the credit line, the arithmetic, the restrictions, the decision and the seals.

  • Honest about failure. If an institution is unreachable the sealed sample stands in and says so, with the date it was taken. If a verdict is computed from a field nobody published, confidence drops and the band becomes Needs review.

Related MCP server: webfetch

🚀 Quickstart

git clone https://github.com/aniruddhaadak80/openplate.git
cd openplate
npm install
npm run dev

Open http://localhost:3000. No environment variables and no API key are required. Without DATABASE_URL the app runs on an embedded PGlite build of Postgres in ./.pglite, so the schema, constraints, indexes and transactions you exercise locally are the ones that run in production. All three upstream institutions are keyless public endpoints.

Scripts

Command

What it does

npm run dev

Development server

npm run typecheck

tsc --noEmit

npm run lint

ESLint

npm run test

Vitest: engine, hash chain, validation, full CRUD over a real database

npm run build

Production bundle

npm run smoke

Playwright: the primary journey through visible controls

npm run verify:local

Boots the production bundle and runs the full live verifier against it

npm run verify:live

Runs the same verifier against a deployment (BASE_URL=...)

npm run check:links

Checks that every absolute URL in this file still resolves

Production variables

Only one is required, and it is documented without a value in .env.example.

Variable

Required

Purpose

DATABASE_URL

Yes, in production

Hosted Postgres. Neon via the Vercel Marketplace supplies it automatically. Without it a production build refuses to open the repository rather than writing into volatile storage.

DATABASE_SCHEMA

No

Schema namespace, to share one database. Defaults to public.

NEXT_PUBLIC_SITE_URL

No

Canonical origin for metadata, the sitemap and the MCP manifest. Defaults to the alias Vercel injects.

OPENPLATE_OFFLINE

No

Force the sealed sample with no network. Used by tests.

OpenPlate deliberately has no third-party API keys: the Met, Cleveland and Wikidata endpoints require none, and adding one would make the demo depend on something a visitor does not have.

🔌 API

Typed REST over the same service layer as the interface and the agent tools. Errors always arrive as {"error":{"code","message","fields"?}}.

# 1. Search both collections, live.
curl -s 'https://openplate-sigma.vercel.app/api/works?q=jaguar&limit=4' | jq '.works[] | {id, title, institutionCleared}'

# 2. Pin a real record to an intended use. Returns 201 with the plate and its seal.
curl -s -X POST https://openplate-sigma.vercel.app/api/plates \
  -H 'content-type: application/json' \
  -b cookies.txt -c cookies.txt \
  -d '{"workId":"met:11298","use":"editorial","jurisdiction":"us","circulation":10000,"note":"Issue 14"}' \
  | jq '{id: .plate.id, shareUrl, band: .assessment.band, score: .assessment.score, seal: .plate.seal}'

# 3. Read it back through the UI-facing API.
curl -s -b cookies.txt https://openplate-sigma.vercel.app/api/plates | jq '.plates[0] | {title, use, chain}'

# 4. Change the inputs. The verdict is recomputed and the change is sealed.
curl -s -X PATCH https://openplate-sigma.vercel.app/api/plates/<PLATE_ID> \
  -H 'content-type: application/json' -b cookies.txt \
  -d '{"use":"merchandise","circulation":250000}' | jq '.assessment.band'

# 5. Run the engine for a hypothetical, saving nothing.
curl -s -X POST https://openplate-sigma.vercel.app/api/plates/<PLATE_ID>/assess \
  -H 'content-type: application/json' -b cookies.txt \
  -d '{"use":"broadcast"}' | jq '{band: .assessed.band, differs: .differsFromSaved}'

# 6. Record a sealed decision.
curl -s -X POST https://openplate-sigma.vercel.app/api/plates/<PLATE_ID>/decision \
  -H 'content-type: application/json' -b cookies.txt \
  -d '{"decision":"approved","note":"Term satisfied, credit line complete."}' | jq '.seal'

# 7. Replay the chain.
curl -s -b cookies.txt https://openplate-sigma.vercel.app/api/plates/<PLATE_ID>/verify \
  | jq '.verification | {ok, eventCount, brokenAtSeq}'

# 8. Download the docket.
curl -s 'https://openplate-sigma.vercel.app/api/docket/<SHARE_CODE>?format=md'

# 9. Retire it. It is a tombstone: gone from reads, retained for verification.
curl -s -X DELETE -b cookies.txt https://openplate-sigma.vercel.app/api/plates/<PLATE_ID> | jq '{retired, note}'

Route

Methods

Purpose

/api/health

GET

Real probe of the configured store. Reports ok and durable separately.

/api/works

GET

Live search, per-provider provenance, honest degraded flag.

/api/plates

GET POST

The docket, and filing a plate. POST is idempotent on idempotencyKey.

/api/plates/<id>

GET PATCH DELETE

Read, change inputs, retire as a tombstone.

/api/plates/<id>/assess

GET POST

Committed verdict, or a hypothetical that saves nothing.

/api/plates/<id>/decision

POST

Record a decision, sealed with the verdict that was read.

/api/plates/<id>/verify

GET

Replay the chain, first broken link reported.

/api/docket/<shareCode>

GET

Download as ?format=json|csv|md.

/api/terms

GET PUT

Persisted defaults and the seeded term rules.

/api/mcp

POST GET

JSON-RPC 2.0: initialize, tools/list, tools/call, ping.

/mcp.json

GET

Manifest with the live endpoint already filled in.

🤖 Agent tools

The MCP endpoint is at https://openplate-sigma.vercel.app/api/mcp, and /mcp.json is generated at request time with that host already filled in.

{
  "mcpServers": {
    "openplate": {
      "type": "http",
      "url": "https://openplate-sigma.vercel.app/api/mcp",
      "transport": "streamable-http"
    }
  }
}

Tool

Kind

What it does

search_works

read

Both collections, normalised, with each institution's rights claim.

assess_work

analysis

The engine on a record, saving nothing.

file_plate

mutating

Re-reads the work live, stores a snapshot, seals the genesis event. Idempotent on idempotencyKey.

record_decision

mutating

Writes the decision with the band, score and seal of the verdict read.

verify_plate

read

Replays the chain, reports the first broken link.

retire_plate

mutating

Retires a plate, retaining the tombstone and its chain.

curl -s https://openplate-sigma.vercel.app/api/mcp -H 'content-type: application/json' -d '{
  "jsonrpc":"2.0","id":1,"method":"tools/call",
  "params":{"name":"assess_work","arguments":{
    "workId":"met:19275","use":"merchandise","jurisdiction":"us","circulation":50000}}}'

📁 Project map

User routes

Route

User goal

States

/

Search both collections and pin a work

Live/degraded banner, per-provider latency, empty state, pin form per card

/plates

The workspace: every plate, filtered

Empty state, filters and paging in the URL, retired tombstones

/plates/<id>

Inspect, re-run the engine, decide, retire

Full factor table, hypothetical vs committed, blockers, cautions, chain, retired banner

/d/<shareCode>

The read-only docket you send someone

404 for an unknown code, renders a retired docket so it stays checkable

/terms

Jurisdiction, term rules and persisted defaults

Seeded rules read from the database, save/unsaved states

/agent

Drive the MCP tools and watch the wire

Preset calls, raw request and response, tool error states, link to whatever it wrote

/export

The downloadable artifact

Empty state, index with per-plate downloads in three formats

API routes

/api/health, /api/works, /api/plates, /api/plates/<id>, /api/plates/<id>/assess, /api/plates/<id>/decision, /api/plates/<id>/verify, /api/docket/<shareCode>, /api/terms, /api/mcp, /mcp.json.

Library

Path

Responsibility

src/lib/engine/clearance.ts

The engine. Pure, versioned, no I/O.

src/lib/integrity/chain.ts

Canonical JSON, seals, replay.

src/lib/service.ts

The domain layer every entry point calls.

src/lib/sources/

Met, Cleveland and Wikidata adapters, plus the sealed sample.

src/lib/db/

Repository interface, schema, and the Postgres and PGlite adapters.

src/lib/terms.ts

Published term lengths, use bands, circulation bands, disclaimer.

src/lib/export.ts

Docket rendering: one view, three formats.

src/lib/validation.ts

Every bounded input.

src/lib/rate-limit.ts

Best-effort anonymous abuse control.

🏗 Architecture

flowchart TB
  classDef data fill:#22d3ee,stroke:#0e7490,color:#083344
  classDef engine fill:#a78bfa,stroke:#6d28d9,color:#2e1065
  classDef agent fill:#34d399,stroke:#047857,color:#022c22
  classDef infra fill:#94a3b8,stroke:#475569,color:#0f172a
  classDef risk fill:#fb7185,stroke:#be123c,color:#4c0519

  B[Browser]:::infra --> P[proxy.ts<br/>anonymous owner cookie]:::infra
  P --> R[Server components]:::infra
  P --> API[REST routes]:::infra
  P --> MCP[JSON-RPC 2.0<br/>/api/mcp]:::agent
  R --> S[service.ts<br/>the domain layer]:::infra
  API --> S
  MCP --> S
  S --> E[engine<br/>clearance v1.1.0]:::engine
  S --> DB[(Hosted Postgres)]:::data
  S --> SRC[Met + Cleveland + Wikidata]:::data
  E --> SEAL[SHA-384 chain]:::engine
  S --> SEAL
  SEAL --> REP[Replay: first broken link]:::risk

Six routes and two entry points, one service layer, one engine, one SQL implementation behind two adapters. The mutating agent tools and the buttons call the same functions, which is why an agent and a person produce identical rows.

📊 Data pipeline and the honest fallback

flowchart LR
  classDef live fill:#22d3ee,stroke:#0e7490,color:#083344
  classDef warn fill:#fbbf24,stroke:#b45309,color:#451a03
  classDef infra fill:#94a3b8,stroke:#475569,color:#0f172a

  Q[Search terms]:::infra --> M[Met v1.1 search]:::live
  Q --> C[Cleveland Open Access]:::live
  Q --> W[Wikidata<br/>creator dates]:::live
  M --> MD[(Object records<br/>rights, life dates)]:::live
  C --> CD[(CC0 licence field<br/>machine-readable years)]:::live
  MD --> N[Normalise into Work]:::infra
  CD --> N
  N --> J[Interleave, image-first]:::infra
  W -.only when the institution<br/>published no death year.-> N
  M -.timeout or 5xx.-> F[Sealed sample]:::warn
  C -.timeout or 5xx.-> F
  J --> R{Any live record?}:::infra
  R -->|yes| OK[live, with per-provider provenance]:::live
  R -->|no| FB[fallback, labelled with<br/>the date the sample was taken]:::warn

Three things this pipeline will not do: present a fallback sample as current, hide a provider that failed, or replace a plate you created with upstream data. User records are never touched by a refresh.

Endpoint note. /public/collection/v1/search was retired on 2026-10-01. OpenPlate uses /public/collection/v1.1/search, which is paginated, and reads each record from /public/collection/v1/objects/{id}. The search endpoint returns identifiers only — every rights field is in the per-record response, so the detail pass is unavoidable rather than decorative.

⚖️ The deterministic engine

Six factors, weights summing to exactly 1. The score is Σ (weight × normalised) × 100; the band comes from the score, from the institution's own claim, and from the confidence.

flowchart TB
  classDef engine fill:#a78bfa,stroke:#6d28d9,color:#2e1065
  classDef data fill:#22d3ee,stroke:#0e7490,color:#083344
  classDef risk fill:#fb7185,stroke:#be123c,color:#4c0519
  classDef ok fill:#34d399,stroke:#047857,color:#022c22

  IN[Work + use + jurisdiction<br/>+ circulation + asOfYear]:::data --> F1
  F1[Institution claim 0.20]:::engine --> SUM
  IN --> F2[Creator term 0.26]:::engine --> SUM
  IN --> F3[Publication-age rule 0.14]:::engine --> SUM
  IN --> F4[Reproduction restrictions 0.20]:::engine --> SUM
  IN --> F5[Use exposure 0.12]:::engine --> SUM
  IN --> F6[Attribution completeness 0.08]:::engine --> SUM
  SUM[Weighted score 0-100]:::engine --> BAND
  F1 --> OVER{Institution says<br/>in copyright?}:::risk
  OVER -->|yes| BL[BLOCKED]:::risk
  OVER -->|no| BAND
  CONF[Unresolved factors<br/>lower confidence]:::engine --> BAND
  BAND --> C1[Clear]:::ok
  BAND --> C2[Clear with credit]:::ok
  BAND --> C3[Needs review]:::risk
  BAND --> BL
  SUM --> CL[Credit line assembled<br/>from the record]:::engine

Three decisions worth arguing with, deliberately:

  1. The institution's negative claim is never averaged away. If a museum publishes a record as in copyright, the answer is blocked, whatever the term arithmetic says. The other five factors exist to explain how close it got.

  2. An unresolved factor costs confidence, and low confidence is never a clearance. A verdict computed from a field nobody published is not a verdict.

  3. The artwork's term and the reproduction's rights are separate factors, so a record can be satisfied on one and failing on the other without being averaged into nonsense.

Term lengths come from TERM_RULES and are seeded into the database, so the /terms page and the arithmetic cannot drift apart.

🔗 Integrity and replay

flowchart TB
  classDef agent fill:#34d399,stroke:#047857,color:#022c22
  classDef engine fill:#a78bfa,stroke:#6d28d9,color:#2e1065
  classDef risk fill:#fb7185,stroke:#be123c,color:#4c0519
  classDef infra fill:#94a3b8,stroke:#475569,color:#0f172a

  G[GENESIS<br/>SHA-384 of a fixed string]:::infra --> E1
  E1[filed]:::agent --> E2[updated]:::agent --> E3[decision_recorded]:::agent --> E4[retired]:::agent
  E1 -.seal_n = SHA-384 UTF-8 prevSeal + canonicalJson event_n.-> E2
  E2 --> E3
  E3 --> E4
  E4 --> REP{Replay from genesis}:::engine
  REP -->|every seal matches| OK[Intact: N events]:::engine
  REP -->|mismatch| BR[Broken at sequence K<br/>with the reason]:::risk

canonicalJson sorts object keys recursively and drops undefined, so two identical events always produce an identical digest regardless of the order the fields were written in. Arrays keep their order, because in a payload an array is meaningful rather than incidental. tests/chain.test.ts pins the algorithm with a vector recomputed using raw node:crypto, so a refactor cannot quietly invalidate seals already written to somebody's database.

Retiring a plate appends a retired event and tombstones the row, so a docket that was already shared keeps verifying while the record leaves the docket list.

🔁 The user journey

flowchart LR
  classDef live fill:#22d3ee,stroke:#0e7490,color:#083344
  classDef agent fill:#34d399,stroke:#047857,color:#022c22
  classDef infra fill:#94a3b8,stroke:#475569,color:#0f172a

  S[Search two collections]:::live --> P[Pin a work to an intended use]:::live
  P --> I[Inspect six factors<br/>and the term arithmetic]:::live
  I --> W[Drag the step wedge:<br/>a real hypothetical]:::infra
  W --> D[Record a sealed decision]:::agent
  D --> V[Replay the chain]:::agent
  V --> X[Export the docket<br/>md, csv, json]:::infra
  X --> R[Retire as a replayable tombstone]:::infra

The whole loop is available through visible controls, and npm run smoke drives it in a real browser while asserting the console and the network stay clean.

🔒 Security model

  • No accounts. An unguessable v4 UUID in an HTTP-only, SameSite=Lax cookie is the ownership boundary, minted in src/proxy.ts; every query is scoped by it, so one visitor cannot read or mutate another's plates.

  • Share codes are capabilities. /d/<shareCode> is the one deliberate public read: 12 characters from a 31-letter alphabet, resolving to exactly one docket, never to a list. Not derived from any id.

  • Rate limiting is best-effort, and says so. 30 writes and 120 reads per minute in process. On serverless that stops a runaway client and not a distributed one; a hard limit needs a shared store, which this project does not assume exists.

  • Everything is validated and parameterised. Closed enum sets, bounded strings, control characters stripped, pagination clamped. The only dynamic SQL is a filter clause built from values that passed an enum check.

  • Retirement is not deletion, so shared dockets stay verifiable.

  • No secrets anywhere: no keys, no .env, nothing in the bundle, the manifest or an error response. Upstream failures name the provider and the status, and nothing about our own infrastructure.

Full detail, including what the chain does not prove, is in SECURITY.md.

🧪 Verification

npm run typecheck && npm run lint && npm run test && npm run build && npm run smoke
  • 70 tests. The engine's normal, boundary, empty, malformed and deterministic-repeat cases; the chain's known vector plus tamper, splice and orphan detection; validation's empty-versus-malformed cases; and a full CRUD run against a real database through the real service layer, including idempotent replay, ownership isolation, constraint enforcement and the tombstone.

  • The browser journey files, inspects, re-runs the engine, decides, verifies, uses an agent tool, exports and retires — then asserts zero console errors and zero failed same-origin requests, checks focus visibility, and checks that neither a 390 px nor a 1440 px viewport overflows horizontally.

  • npm run verify:live proves the deployed instance end to end: health really reached a durable store, live data is non-empty with provenance, create → read → update → assess → MCP mutation → replay → retire, the docket downloads as three valid formats, and the rendered navigation and footer both carry the real repository URL.

flowchart LR
  classDef ok fill:#34d399,stroke:#047857,color:#022c22
  classDef infra fill:#94a3b8,stroke:#475569,color:#0f172a
  classDef agent fill:#22d3ee,stroke:#0e7490,color:#083344

  V[typecheck]:::ok --> L[lint]:::ok --> T[70 tests]:::ok --> B[build]:::ok --> S[smoke]:::ok
  B --> VL[verify:live<br/>22 checks against production]:::agent
  CI[Node 22, npm ci,<br/>no secrets required]:::infra --> V

CI runs Node 22 with npm ci, typecheck, lint, test and build, then the browser journey. No job needs a secret or an API key.

🚢 Deploying

flowchart TB
  classDef infra fill:#94a3b8,stroke:#475569,color:#0f172a
  classDef live fill:#34d399,stroke:#047857,color:#022c22
  classDef warn fill:#fbbf24,stroke:#b45309,color:#451a03

  P[Push to main]:::infra --> G[GitHub Actions<br/>typecheck, lint, test, build, smoke]:::infra
  G --> V[Vercel production build]:::infra
  D[(Neon Postgres)]:::live --> V
  V --> A[Production alias]:::live
  V -.no DATABASE_URL.-> X[Boot fails loudly<br/>rather than writing<br/>to volatile storage]:::warn
  A --> M[Metadata, sitemap<br/>and /mcp.json<br/>name the real alias]:::infra

The production build refuses to open the repository without DATABASE_URL. That is deliberate: a deployment that has quietly lost its database fails its health check instead of accepting writes into storage a cold start would erase.

🗺️ Roadmap

Now — shipped in this repository

  • Search both open-access collections live, with per-provider provenance and an honest sealed fallback → a visitor never mistakes a sample for a current answer

  • Six-factor versioned engine, shared by the page, the REST API and the agent tools → one verdict, computed one way

  • Artwork term and reproduction rights reported separately → the reason a Monet can be public domain while its photograph is not

  • Step wedge for a real hypothetical, saving nothing → the reader can ask "what if it were merchandise" without changing the record

  • SHA-384 chain per plate, replayable, with a known vector → a later reader can find the first edited event

  • Six MCP tools with idempotent mutations, in an inspectable console → agent and person produce identical rows

  • Docket in Markdown, CSV and JSON, with source attribution and timestamps → something a publisher can hand to a client

flowchart LR
  classDef ok fill:#34d399,stroke:#047857,color:#022c22
  S[Search]:::ok --> E[Engine]:::ok --> C[Chain]:::ok --> M[MCP]:::ok --> X[Export]:::ok

Next — the obvious next problems

  • Bulk clearance. Import a folder of candidate images and get one docket for all of them, instead of filing plates one at a time → a picture editor clears an issue in one pass

  • Restoration lookups. Persist the visual crop, colour profile and embedded licence metadata, and warn when an export drops them → the "no derivatives" clause stops being invisible

  • A watch list. Re-check a docket against the institution's record on demand and append a verified event when the rights claim changes → a publisher learns their credit line went stale before a client does

  • Comparable-terms view. For a restricted record, show every jurisdiction and term length whose arithmetic would clear the artwork, with the reasoning → "where can I actually use this?" gets an answer

flowchart TB
  classDef n fill:#fbbf24,stroke:#b45309,color:#451a03
  B[Bulk import]:::n --> R[Restoration lookups]:::n --> W[Watch list]:::n --> J[Comparable terms]:::n
  B --> R
  R --> W
  W --> J

Later — open questions, not commitments

  • Permit-reporting for jurisdictions where a collective society must be identified before publication → the credit line starts as a filing

  • A CLI that verifies a docket offline from its exported JSON, with no network at all → an auditor can check a file, not a website

  • Model-to-scan records (photographs of works) alongside institution records → the reproduction question gets asked of the right record

flowchart LR
  classDef l fill:#94a3b8,stroke:#475569,color:#0f172a
  P[Society reporting]:::l --> O[Offline verifier CLI]:::l --> S[Photograph records]:::l
  P --> O
  O --> S

OpenPlate reports what a museum's own records say and does arithmetic on published copyright term lengths. It is a research desk, not legal advice, and it does not replace a rights clearance for a paying client. A term running out is a necessary condition for reuse, not a sufficient one, and an institution's claim about its own photograph is a separate right. When the verdict is Needs review or Blocked, write to the institution or to a rights adviser before you print.

📚 Attribution and data provenance

  • The Metropolitan Museum of Art Collection API (Open Access) — record metadata, rights statements and images. Terms

  • The Cleveland Museum of Art Open Access API — record metadata, licence fields and images. Open Access

  • Wikidata (CC0) — creator life dates, used only where an institution published none.

Records are cited with the institution, the fetched timestamp and the upstream identifier, and every plate keeps the snapshot it was created from. OpenPlate is not affiliated with or endorsed by either museum.

🤝 Contributing

Issues and pull requests are welcome — see CONTRIBUTING.md for the setup, the five commands that must be green, and how to add a jurisdiction or a factor without breaking the engine. Security reports go through the private advisory route described in SECURITY.md.

📄 License

MIT © 2026 Aniruddha Adak. Museum metadata and images remain under their institutions' own terms; this project claims none of them.

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    A
    maintenance
    Federated, license-verified search across open-access museum collections — currently The Met, Cleveland, AIC, Wikimedia Commons, and Europeana, with more being added. Strict-default-deny rights gate accepts only CC0 / Public Domain Mark, returning reuse-safe artwork with citations in three styles.
    5
    140 npm
    13
    MIT
  • A
    license
    A
    quality
    B
    maintenance
    License-first federated image search for AI agents and humans. Exposes MCP tools for concise, attribution-aware image discovery, license probing, and guarded downloads across open, platform, and editorial sources.
    7
    36 npm
    3
    MIT
  • A
    license
    A
    quality
    A
    maintenance
    Enables AI-native access to Swiss cultural heritage data from SIK-ISEA (artists), Nationalmuseum (collections), and Nationalbibliothek (bibliography) via MCP tools, without authentication.
    11
    38 PyPI
    MIT
  • A
    license
    A
    quality
    D
    maintenance
    An MCP server for browsing and downloading open-licensed imagery from major museum and archive APIs including Wellcome, Met, Library of Congress, Smithsonian, and Europeana.
    12
    15 npm
    MIT