Back to the catalog

io.github.malkreide/swiss-academic-libraries-mcp

Swiss academic libraries + open-access Swiss legal literature (SRU/OAI-PMH, no API key)

Open source Open in the app JSON README (API)

About

Swiss academic libraries + open-access Swiss legal literature (SRU/OAI-PMH, no API key)

Details

Kind
MCP servers
Topic
Government & public data
Publisher
malkreide
Origin
official
Category
ferramentas
Transport
local
Version
1.2.0
Stars
1
Open pull requests
1
Last push
2026-09-03T04:54:34Z
Repository state
ativo
Language
Python
License
MIT
Added
2026-08-29 04:00:27
Updated
2026-08-29 04:00:27
Origin id
io.github.malkreide/swiss-academic-libraries-mcp

README

> πŸ‡¨πŸ‡­ **Part of the [Swiss Public Data MCP Portfolio](https://github.com/malkreide)**

# πŸ“š swiss-academic-libraries-mcp

[![PyPI version](https://img.shields.io/pypi/v/swiss-academic-libraries-mcp)](https://pypi.org/project/swiss-academic-libraries-mcp/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
[![MCP](https://img.shields.io/badge/MCP-Model%20Context%20Protocol-purple)](https://modelcontextprotocol.io/)
[![No Auth Required](https://img.shields.io/badge/auth-not%20required-lightgrey)](https://github.com/malkreide/swiss-academic-libraries-mcp)
![CI](https://github.com/malkreide/swiss-academic-libraries-mcp/actions/workflows/ci.yml/badge.svg)

> MCP server providing access to Swiss academic libraries β€” swisscovery, e-rara, e-periodica, e-manuscripta. No API key required.

[πŸ‡©πŸ‡ͺ Deutsche Version](README.de.md)

### Demo

![Demo: Claude using swisscovery_search and erara_list_records](docs/assets/demo.svg)

---

## Overview

**swiss-academic-libraries-mcp** connects AI models to the full Swiss academic library infrastructure via standardised, open protocols. It covers the [swisscovery](https://swisscovery.slsp.ch) union catalogue (500+ libraries, 10M+ records) and three digitalisation platforms: historical prints ([e-rara](https://www.e-rara.ch)), periodicals ([e-periodica](https://www.e-periodica.ch)) and manuscripts ([e-manuscripta](https://www.e-manuscripta.ch)).

All data sources use open, authentication-free protocols (SRU/MARC21, OAI-PMH/Dublin Core). The server supports both local use via Claude Desktop (stdio transport) and cloud deployment (Streamable HTTP).

Beyond the catalogue, the server also covers **Swiss open-access legal literature** β€” freely readable legal scholarship from [sui generis](https://sui-generis.ch), [ex/ante](https://ex-ante.ch) and [Repositorium.ch](https://www.repositorium.ch) β€” as **metadata** (title, authorship, year, licence, DOI, link), never full text.

It also adds the **international metadata layer**: DOI resolution and international research literature via [Crossref](https://www.crossref.org), and preprints via [arXiv](https://arxiv.org). This lets one conversation answer both *"is this held in Switzerland?"* (national layer) and *"what is this, and where else does it live?"* (international layer). Every resolved DOI returns title, ISSN, ISBN and authors as clean top-level fields, so you can pivot straight into swisscovery.

**Anchor demo query (national ↔ international):** *"Find the original publication for this DOI, check whether a preprint version exists, and show whether a Swiss library holds it."* β†’ `resolve_doi` β†’ `search_preprints` β†’ `swisscovery_search(query="<ISSN from resolve_doi>")`.

**Anchor demo query (catalogue):** *"Which Swiss university dissertations on primary school pedagogy are held in Swiss libraries, and are any of them digitised in e-rara?"*

**Anchor demo query (OA legal literature):** *"Which freely accessible legal-scholarship articles exist on data protection in education? Give me title, authorship, year, licence and DOI."* β†’ `oa_law_search(query="Datenschutz im Bildungsbereich")` β€” results are ranked by relevance: articles matching **all** terms rank first, articles matching only the core term (`Datenschutz`) follow, so the query returns the real privacy-law corpus rather than an empty set.

---

## Features

- **16 tools** across 4 catalogue sources + 3 open-access legal-literature sources + 2 international metadata sources β€” all read-only, no API key required
- **swisscovery search** with full CQL syntax: full-text, title, author, subject, ISBN/ISSN
- **OAI-PMH harvesting** with date range and collection filters plus pagination via resumption tokens
- **MARC21 parser** extracting 20+ fields (title, creator, publication info, subjects, abstract, URLs)
- **Dublin Core parser** for all three digitalisation portals
- **Dual transport**: stdio for Claude Desktop Β· Streamable HTTP for cloud/self-hosted deployments
- **OA legal-literature search** across sui generis, ex/ante and Repositorium.ch with a declarative source registry (new sources = one config entry), best-effort Crossref licence enrichment, and graceful per-source degradation
- **International metadata layer**: DOI resolution and bibliographic search via Crossref (polite pool via `CROSSREF_MAILTO`), preprint search via arXiv with automatic phrase quoting and request throttling β€” clean title/ISSN/ISBN/author fields for pivoting into swisscovery
- **3 built-in prompts**: `research-workflow`, `education-research` and `doi-to-swiss-shelf`
- **Markdown and JSON output** for all tools
- **97 unit/mocked tests** (no network) + 30 live smoke tests

---

## Data Sources

| Source | Protocol | Content | Records |
|--------|----------|---------|---------|
| [swisscovery (SLSP)](https://swisscovery.slsp.ch) | SRU / MARC21 | 500+ Swiss libraries | 10M+ |
| [e-rara](https://www.e-rara.ch) | OAI-PMH / Dublin Core | Digitised historical prints | 250k+ |
| [e-periodica](https://www.e-periodica.ch) | OAI-PMH / Dublin Core | Digitised periodicals (1750–today) | 1M+ articles |
| [e-manuscripta](https://www.e-manuscripta.ch) | OAI-PMH / Dublin Core | Manuscripts & archival material | 100k+ |

### Open-Access legal literature (metadata only)

| Source | Protocol | Content | DOI coverage |
|--------|----------|---------|--------------|
| [sui generis](https://sui-generis.ch) | OAI-PMH / Dublin Core | OA legal journal & non-profit publisher | ~100 % (`10.21257/…`) |
| [ex/ante](https://ex-ante.ch) | OAI-PMH / Dublin Core | Peer-reviewed journal for (young) legal scholarship, multilingual | none (persistent URL) |
| [Repositorium.ch](https://www.repositorium.ch) | Supabase / PostgREST (JSON) | Subject repository for Swiss law | partial |

### International metadata layer (metadata only)

| Source | Protocol | Content | Licence |
|--------|----------|---------|---------|
| [Crossref](https://www.crossref.org) | REST / JSON | DOI resolution + international research literature | Metadata CC0 1.0 (public domain) |
| [arXiv](https://arxiv.org) | Atom / XML | Preprints (CS, physics, maths, stats, …) | Metadata CC0 1.0; preprints per author licence |

---

## Tools

| Tool | Source | Function |
|------|--------|----------|
| `library_info` | β€” | Entry point: overview of all sources and tools |
| `swisscovery_search` | swisscovery | Full-text / CQL search across the union catalogue |
| `swisscovery_get_record` | swisscovery | Single record by MMS-ID |
| `erara_list_records` | e-rara | Prints filtered by date / collection |
| `erara_get_record` | e-rara | Single item by OAI identifier |
| `erara_list_collections` | e-rara | All participating libraries |
| `eperiodica_list_records` | e-periodica | Articles filtered by date |
| `eperiodica_get_record` | e-periodica | Single article by OAI identifier |
| `emanuscripta_list_records` | e-manuscripta | Manuscripts filtered by date / collection |
| `emanuscripta_get_record` | e-manuscripta | Single object by OAI identifier |
| `emanuscripta_list_collections` | e-manuscripta | All archives / collections |
| `oa_law_search` | OA legal (all 3) | Search OA legal scholarship (title/abstract/author) with source, language, year and peer-review filters |
| `oa_law_get` | OA legal (all 3) | Single OA legal article by DOI or resolvable URL |
| `resolve_doi` | Crossref | Resolve a DOI to full metadata (title/ISSN/ISBN/authors β†’ pivot into swisscovery) |
| `search_publications` | Crossref | Search international research literature; every hit carries a DOI |
| `search_preprints` | arXiv | Search preprints with automatic phrase quoting; linked journal DOIs bridge to `resolve_doi` |

### Example Use Cases

| Query | Tool |
|-------|------|
| *"Which books about Swiss primary schools are held in Swiss libraries?"* | `swisscovery_search` |
| *"Show digitised historical works from ETH Library"* | `erara_list_records` |
| *"Which Swiss periodicals were digitised in 2023?"* | `eperiodica_list_records` |
| *"What manuscript collections does e-manuscripta hold?"* | `emanuscripta_list_collections` |
| *"Which OA legal articles exist on facial recognition?"* | `oa_law_search` |
| *"Resolve DOI 10.1038/nature14539 and give me its ISSN"* | `resolve_doi` |
| *"Find recent preprints on model context protocol"* | `search_preprints` |
| *"Find this paper's DOI, check for a preprint, and see if a Swiss library holds it"* | `resolve_doi` β†’ `search_preprints` β†’ `swisscovery_search` |

---

## Architecture

Three independent paths share one HTTP client (retry with exponential backoff, shared connection pool, project `User-Agent`):

```
                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                    β”‚            swiss-academic-libraries-mcp               β”‚
                    β”‚            (FastMCP Β· stdio / HTTP)                   β”‚
                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                             β”‚               β”‚                β”‚
          β”Œβ”€β”€ CATALOGUE β”€β”€β”€β”€β”€β”˜   β”Œβ”€β”€ OA-LEGAL β”˜     β”Œβ”€β”€ INTERNATIONAL ──┐
          β”‚   (api_client.py)    β”‚   (oa_legal.py)  β”‚  (intl_metadata.py)β”‚
          β”‚                      β”‚                  β”‚                    β”‚
 β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
 β”‚ swisscovery SRU β”‚   β”‚ source registry     β”‚   β”‚ Crossref  REST/JSON  β”‚ β”‚
 β”‚ e-rara      OAI β”‚   β”‚  β”œ sui generis  OAI β”‚   β”‚  β”œ resolve_doi        β”‚ β”‚
 β”‚ e-periodica OAI β”‚   β”‚  β”œ ex/ante      OAI β”‚   β”‚  β”” search_publicationsβ”‚ β”‚
 β”‚ e-manuscripta   β”‚   β”‚  β”” Repositorium RESTβ”‚   β”‚ arXiv     Atom/XML   β”‚ β”‚
 β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚ harvestβ†’cacheβ†’filterβ”‚   β”‚  β”” search_preprints   β”‚ β”‚
          β”‚            β”‚ Crossref licence ⟳  β”‚   β”‚ (phrase-quote+throttle)β”‚ β”‚
 MARC21 / Dublin Core  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
 β†’ catalogue records   OaLegalPublication         CrossrefWork / Preprint β”‚
                       (metadata, no full text)   (metadata, no full text) β”‚
```

- **Catalogue path** returns bibliographic records (books, digitised prints, periodicals, manuscripts).
- **OA-legal path** harvests OA legal-scholarship metadata once, caches it in memory (small corpus), and filters locally β€” because OAI-PMH has no keyword search of its own. Adding a fourth OA source is a single registry entry, not new code.
- **International path** resolves DOIs and searches Crossref/arXiv live (Architecture A β€” the endpoints are small, stable and public, so no dump/cache is needed). Each response carries its **own source attribution** (Crossref CC0 Β· arXiv acknowledgement), never a pooled one, and returns clean top-level title/ISSN/ISBN/author fields so the model can pivot into the catalogue path. A code-layer egress allow-list restricts outbound calls to `api.crossref.org` and `export.arxiv.org`.

---

## Licensing & Scope

This server is deliberately conservative about what it emits β€” a portfolio that treats governance as a feature cannot be careless here.

- **Metadata, not full text.** For OA legal literature the server returns **title, authorship, year, licence, DOI/link and β€” where the source provides it as a metadatum β€” the abstract**. It never ingests, stores, or outputs the article body. The full-text PDF path exposed by Repositorium.ch is intentionally *not* carried into any field.
- **`licence` is always set.** Open Access means *free to read*, **not** *free to reuse*. Licences range from CC0 and CC BY to CC BY-NC-ND, and some articles are simply "free to read" with no open licence at all. When no machine-readable licence is available, the field is `"unknown"` β€” never guessed, never omitted. The native OAI metadata of all three sources carries only copyright statements, so `"unknown"` is the default; a best-effort **Crossref** lookup upgrades it to the real CC licence where a DOI resolves (e.g. sui generis β†’ `CC BY-SA 4.0`). Disable with `OA_LAW_CROSSREF_ENRICH=0`.
- **Citation integrity.** Every result carries a resolvable reference β€” a DOI where present, otherwise a persistent URL. No result is emitted without one. A fabricated citation in legal literature is worse than no citation, so the server prefers one hit fewer over one hit invented.
- **Language is carried, never silently filtered.** ex/ante and Repositorium.ch are multilingual (DE/FR/IT/EN). The `language` field is always populated, but results are only filtered by language when you explicitly ask β€” otherwise half the Romandie would vanish from the results.
- **Attribution per source, not pooled.** Crossref, arXiv and the OA-legal sources have different licence and citation conditions, so each response carries the attribution of the source it actually came from. Crossref bibliographic metadata is distributed under **CC0 1.0** (facts are free); arXiv metadata is **CC0 1.0** with the requested acknowledgement *"Thank you to arXiv for use of its open access interoperability"*; preprint and article full texts remain under their own per-work licences (never emitted here).
- **Scope boundary.** OA legal literature belongs here because it is the *same capability* already in this server β€” authentication-free bibliographic-metadata harvesting of Swiss scholarly sources over standard protocols β€” with the same output contract (metadata, not full text). It is institution-independent and subject-focused, which is why it does **not** overlap [`eth-library-mcp`](https://github.com/malkreide/eth-library-mcp) (ETH-institution Discovery & Persons) β€” the catalogue finds the *book*, the OA-legal path finds the freely readable *article* with its licence and DOI.

---

## Known Limitations

- **Small, focused corpus.** The three OA sources together hold on the order of a few hundred articles. Results are ranked by relevance β€” articles matching all query terms first, then partial matches β€” so a topical query like *"Datenschutz im Bildungsbereich"* returns the privacy-law corpus (ranked) rather than nothing; if no article covers the full topic intersection, the closest real matches are returned, never a fabricated one. A query that matches no term at all still returns an honest empty result.
- **No full-text search.** Matching runs over metadata (title, abstract, authorship) only β€” never the article body.
- **Uneven DOI coverage.** sui generis β‰ˆ 100 %, Repositorium.ch partial, ex/ante has **no DOIs** (persistent URLs only). Aggregators (Crossref/OpenAlex) therefore cover sui generis well but miss ex/ante entirely and do not index Repositorium.ch as a source β€” which is why the server harvests each source natively rather than relying on an aggregator.
- **Licence gaps.** The native metadata rarely carries a machine-readable licence; `"unknown"` is common and only lifted where a DOI resolves in Crossref.
- **Not a substitute for a paid legal database.** This surfaces *freely accessible* Swiss legal scholarship only β€” it is not Swisslex/Weblaw and does not cover commercial or paywalled legal publishing.

### Known findings β€” international layer (live probe 2026-07-20)

| Finding | Detail | Consequence |
|---|---|---|
| **Crossref is weak for German-language CH education literature** | `query.bibliographic=Lehrplan 21` returns a **book chapter from 1881** as the top hit; other CH-education queries surface millions of irrelevant results with off-topic/old top hits. Crossref is strong for DOI resolution and international research, not for CH education publishing. | `search_publications` documents this and points to `swisscovery_search` / `oa_law_search` for CH education topics. |
| **arXiv treats spaces as OR, not phrase** | `all:model context protocol` β†’ ~1,296,686 hits (OR); `all:"model context protocol"` β†’ 462 hits (phrase). | `search_preprints` quotes the query automatically; users need no arXiv syntax. Field syntax (`ti:`, `au:`) and explicit quotes are respected. |
| **arXiv returns Atom XML, throttles, and 301-redirects `http`β†’`https`** | Response is Atom, not JSON; arXiv asks for ~3 s between requests; the plain `http://` endpoint 301-redirects. | Parser reuses `defusedxml` (already a dependency β€” no new one); a module-level throttle (`ARXIV_MIN_INTERVAL_SECONDS`, default 3 s) enforces spacing; the client calls the `https://` endpoint directly. |
| **SHARE (share.osf.io) β€” evaluated, not built** | The `_search` endpoint responds (β‰ˆ58.9 M records, Elasticsearch-style), **but** SHARE wound down harvesting in 2020, archived its database in CurateND, and carries no API-maintenance commitment ("shutting down / new phase", now the "trove" search-api). For a portfolio positioned on reliability, an index without a support guarantee is a poor dependency. | **Not implemented.** |
| **Open Library β€” evaluated, gate failed** | A 10-ISBN probe of real Swiss/German-language Lehrmittel (Lehrmittelverlag ZΓΌrich, Klett und Balmer; ISBNs harvested from swisscovery) returned **0/10 = 0 %** (threshold 60 %). Controls confirm Open Library works (English + mainstream German trade books resolve), so it is a genuine coverage gap, not a connectivity artefact. | **Not implemented.** swisscovery already covers CH Lehrmittel; for the book trade, GVI or a publisher directory is the fitting route. |

---

## Prerequisites

- Python 3.11 or higher
- [uv](https://docs.astral.sh/uv/) / uvx (recommended) or pip
- Internet access (all APIs are publicly available)

---

## Installation

### Claude Desktop (recommended)

Add to `claude_desktop_config.json`:

**macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`  
**Windows:** `%APPDATA%\Claude\claude_desktop_config.json`

```json
{
  "mcpServers": {
    "swiss-academic-libraries": {
      "command": "uvx",
      "args": ["swiss-academic-libraries-mcp"]
    }
  }
}
```

Restart Claude Desktop β€” the server starts automatically on first use.

### Cloud / Self-hosted (Streamable HTTP)

```bash
uvx swiss-academic-libraries-mcp --http --port 8000 [--host 127.0.0.1]
```

**Security & Deployment Notes**

- **Default binding is `127.0.0.1`** (loopback only). The server has no
  built-in authentication.
- Use `--host 0.0.0.0` only when running **behind a reverse proxy that
  provides authentication and per-IP rate limits** (e.g. nginx with
  `limit_req` + OAuth2-Proxy). Non-loopback bindings emit a WARN log.
- Logs go to **stderr**; set verbosity with `MCP_LOG_LEVEL=DEBUG|INFO|WARNING`.

### Development

```bash
git clone https://github.com/malkreide/swiss-academic-libraries-mcp
cd swiss-academic-libraries-mcp
pip install -e .
```

---

## Quickstart

Start by calling `library_info` for a full overview. Then:

```
"Which books about Swiss primary schools are held in Swiss libraries?"
β†’ swisscovery_search(query='subject = "Volksschule"', max_records=20)

"Show digitised historical works from ETH Library"
β†’ erara_list_records(set_spec="zut")

"Which Swiss periodicals were digitised in 2023?"
β†’ eperiodica_list_records(from_date="2023-01-01", until_date="2023-12-31")

"What manuscript collections does e-manuscripta hold?"
β†’ emanuscripta_list_collections()

β†’ [More use cases by audience](EXAMPLES.md) β†’
```

> πŸ’‘ *"No API key β€” just install and query."*

### CQL Search Syntax (swisscovery)

```
Full text:     Volksschule ZΓΌrich
Title:         title = "education reform"
Author:        creator = "Pestalozzi"
Subject:       subject = "pedagogy"
ISBN:          isbn = "978-3-05-006234-0"
Combined:      title = "school" AND creator = "Pestalozzi"
Pagination:    start_record = 11
```

---

## Configuration

No API keys required. All environment variables are optional.

| Parameter | Default | Description |
|-----------|---------|-------------|
| `--http` | off | Enable Streamable HTTP transport |
| `--port` | 8000 | Port for HTTP transport |
| `MCP_LOG_LEVEL` | `INFO` | Log verbosity (`DEBUG`/`INFO`/`WARNING`) |
| `OA_LAW_CROSSREF_ENRICH` | `1` | OA legal: DOI→licence enrichment via Crossref; set `0` to disable |
| `OA_LAW_REPOSITORIUM_ANON_KEY` | *(public key)* | OA legal: override for Repositorium.ch's public read-only Supabase anon key (allows rotation without a code change) |
| `CROSSREF_MAILTO` | *(unset)* | International: contact e-mail for Crossref's "polite pool" (better throughput). If unset, requests use the anonymous pool β€” functional, just slower. |
| `ARXIV_MIN_INTERVAL_SECONDS` | `3.0` | International: minimum spacing between arXiv requests (arXiv asks for restraint). |

---

## Project Structure

```
swiss-academic-libraries-mcp/
β”œβ”€β”€ src/
β”‚   └── swiss_academic_libraries_mcp/
β”‚       β”œβ”€β”€ __init__.py       # Package init
β”‚       β”œβ”€β”€ server.py         # FastMCP server, 16 tools, 3 prompts, 3 resources
β”‚       β”œβ”€β”€ api_client.py     # HTTP client (+ retry), MARC21 + OAI-PMH/DC parsers
β”‚       β”œβ”€β”€ oa_legal.py       # OA legal-literature registry, adapters, model
β”‚       └── intl_metadata.py  # International layer: Crossref + arXiv adapters, models
β”œβ”€β”€ tests/
β”‚   β”œβ”€β”€ test_server.py        # catalogue unit tests + live smoke tests
β”‚   β”œβ”€β”€ test_20_scenarios.py  # end-to-end catalogue scenarios
β”‚   β”œβ”€β”€ test_oa_legal.py      # OA legal-literature tests (mocked + live)
β”‚   └── test_intl_metadata.py # International-layer tests (mocked + live)
β”œβ”€β”€ pyproject.toml
β”œβ”€β”€ CHANGELOG.md
β”œβ”€β”€ CONTRIBUTING.md            # Contributing guide (English)
β”œβ”€β”€ CONTRIBUTING.de.md         # Contributing guide (German)
β”œβ”€β”€ SECURITY.md               # Security policy (English)
β”œβ”€β”€ SECURITY.de.md            # Security policy (German)
β”œβ”€β”€ LICENSE
β”œβ”€β”€ README.md                 # This file (English)
└── README.de.md              # German version
```

---

## MCP Protocol Version

This server speaks **two protocol eras** over the same endpoint. The client's
first request on a connection decides which one applies; a later claim from the
other era is refused.

| Era | Revision | Who reaches it |
|---|---|---|
| `initialize` handshake | `2024-11-05` … **`2025-11-25`** | What today's clients speak. The server answers with the revision asked for, or with the `2025-11-25` ceiling when the request asks for something newer. |
| Per-request envelope | **`2026-07-28`** | A request carrying the `2026-07-28` `_meta` envelope opens a modern connection. |

Both revisions are pinned in
[`tests/test_protocol_version.py`](tests/test_protocol_version.py) and asserted
against the installed SDK, so a Dependabot bump of `mcp` cannot move either one
silently. This server builds no ASGI app to send an `initialize` through, so
the gate asserts the SDK constants rather than a measured response β€” the
weaker form, named rather than left unsaid.

Note that the SDK's `LATEST_PROTOCOL_VERSION` is an alias for the **modern**
era, not for the handshake era β€” pinning against it alone would leave the era
that current clients actually negotiate free to drift.

**Update policy.** When the gate fails, do not edit the constant blindly: read
the spec changelog between the two revisions, verify the server still behaves,
then move the constant, this section, `README.de.md` and
[`CHANGELOG.md`](CHANGELOG.md) together.

---

## Testing

```bash
# Unit tests (no network required)
PYTHONPATH=src pytest tests/ -m "not live"

# Live smoke tests (internet required)
PYTHONPATH=src pytest tests/ -m "live"
```

### Where the test data comes from

The fixtures under `tests/fixtures/` are **recorded from the live sources**, with
the same request parameters the production code sends, and dated. Source,
retrieval date, selection rule and SHA-256 for every file:
[`tests/fixtures/PROVENANCE.md`](tests/fixtures/PROVENANCE.md).

```bash
python scripts/record_fixtures.py   # re-record
```

This matters because a hand-written mock encodes its author's assumption and can
therefore never refute it β€” production code and fixture come from the same
reading of the docs. The invented `ListSets` response carried no
`resumptionToken`, so no test could notice that nobody followed one: e-rara
serves 105 collections in pages of 10, and the tool reported 10.

Where a fixture is trimmed, the count fields (`numberOfRecords`,
`completeListSize`, `total-results`) and the `resumptionToken` keep their real
values β€” they say how much is *not* in the file.

`tests/fixtures/oai_ex_ante_listrecords.xml` is deliberately **not** well-formed
XML: ex/ante emits a raw control character inside a `dc:description`, and
recording it verbatim is what proves `strip_invalid_xml_chars` is load-bearing.

---

## Safety & Limits

- **Read-only:** All tools perform HTTP GET requests against public SRU and OAI-PMH endpoints β€” no data is written, modified, or deleted.
- **No personal data:** The APIs return bibliographic metadata (titles, authors, publication info, subject headings) and public digitisation records. No personally identifiable information (PII) about library users is processed or stored.
- **Rate limits:** swisscovery SRU and the OAI-PMH endpoints are public and have no documented hard limits, but OAI-PMH harvesting is paginated via resumption tokens β€” use `from_date` / `until_date` and keep `max_records` reasonable. The server enforces a 30s timeout per request.
- **Data freshness:** Results reflect the upstream catalogues at query time. No caching is performed by this server; indexing latency is controlled by SLSP and the digitisation platforms.
- **Terms of service:** Data is subject to the ToS and licences of each source β€” [swisscovery / SLSP](https://slsp.ch), [e-rara](https://www.e-rara.ch), [e-periodica](https://www.e-periodica.ch), [e-manuscripta](https://www.e-manuscripta.ch). Most digitised material is in the public domain or under Creative Commons licences; always check the rights statement on the individual record before redistribution.
- **No guarantees:** This is a community project, not affiliated with SLSP, ETH Library, or any of the participating institutions. Availability depends on the upstream APIs.

---

## Contributing

Contributions are welcome! Please read [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines on:

- Reporting bugs and requesting features
- Setting up the development environment
- Code style and test requirements
- Submitting pull requests

This project follows the conventions of the [Swiss Public Data MCP Portfolio](https://github.com/malkreide).

---

## Security

To report a vulnerability, please follow the responsible disclosure process in [SECURITY.md](SECURITY.md). The server is read-only and requires no API key; see the *Safety & Limits* section above for the security model.

---

## Changelog

See [CHANGELOG.md](CHANGELOG.md)

---

## Deployment for Swiss Public Administration

If you self-host this server for a Swiss school authority, archive,
or municipal use case:

- **Data residency:** prefer on-premise or a CH-based cloud provider.
  The query patterns themselves (which library searches a civil
  servant runs) may reveal ongoing research and are best kept on
  Swiss infrastructure.
- **Upstream calls** go exclusively to CH-hosted services: SLSP /
  swisscovery, ETH-Bibliothek (e-rara, e-periodica, e-manuscripta).
  No data leaves Switzerland.
- **Logging:** logs are written to stderr; configure your IT
  retention policy accordingly (e.g. systemd-journal `MaxRetentionSec`).
- **HTTP transport** must run behind a reverse proxy with
  authentication and per-IP rate limits (see *Security & Deployment
  Notes* above).

---

## License

MIT License β€” see [LICENSE](LICENSE)

---

## Author

Hayal Oezkan Β· [github.com/malkreide](https://github.com/malkreide)

---

## Credits & Related Projects

- **Data (catalogue):** [swisscovery / SLSP](https://swisscovery.slsp.ch) Β· [e-rara](https://www.e-rara.ch) Β· [e-periodica](https://www.e-periodica.ch) Β· [e-manuscripta](https://www.e-manuscripta.ch)
- **Data (OA legal literature):** [sui generis](https://sui-generis.ch) Β· [ex/ante](https://ex-ante.ch) Β· [Repositorium.ch](https://www.repositorium.ch) β€” licence enrichment via [Crossref](https://www.crossref.org). Each source's metadata is used under its own terms; open-access status does not imply an open reuse licence.
- **Protocol:** [Model Context Protocol](https://modelcontextprotocol.io/) β€” Anthropic / Linux Foundation
- **Related:** [eth-library-mcp](https://github.com/malkreide/eth-library-mcp) β€” ETH Library Discovery & Persons API
- **Portfolio:** [Swiss Public Data MCP Portfolio](https://github.com/malkreide)

| Server | Description |
|--------|-------------|
| [`zurich-opendata-mcp`](https://github.com/malkreide/zurich-opendata-mcp) | City of Zurich Open Data |
| [`eth-library-mcp`](https://github.com/malkreide/eth-library-mcp) | ETH Library Discovery & Persons API |
| [`swiss-statistics-mcp`](https://github.com/malkreide/swiss-statistics-mcp) | Swiss Federal Statistics (BFS) |
| [`fedlex-mcp`](https://github.com/malkreide/fedlex-mcp) | Swiss Federal Law via Fedlex SPARQL |
| [`swiss-transport-mcp`](https://github.com/malkreide/swiss-transport-mcp) | OJP journey planning, SIRI-SX disruptions |

<!-- mcp-name: io.github.malkreide/swiss-academic-libraries-mcp -->

<!-- BEGIN GENERATED: install -->
## Installation

Run via [`uv`](https://docs.astral.sh/uv/)'s `uvx` β€” no clone or manual install needed. Add to your MCP client config (`mcpServers` for Claude Desktop, Cursor and Windsurf; use a top-level `servers` key for VS Code in `.vscode/mcp.json`):

```json
{
  "mcpServers": {
    "swiss-academic-libraries-mcp": {
      "command": "uvx",
      "args": [
        "swiss-academic-libraries-mcp"
      ]
    }
  }
}
```
<!-- END GENERATED: install -->

More