MCP server for Swiss school and public holidays โ all 26 cantons, differentiated by Schulart
Project description
๐จ๐ญ Part of the Swiss Public Data MCP Portfolio
This is a private project. It is independent of any employer or institutional affiliation and represents no official position of any authority.
๐ swiss-school-calendar-mcp
MCP server for Swiss school holidays and public holidays across all 26 cantons โ differentiated by Schulart (school type), which matters more than it first appears. No API key required.
Overview
swiss-school-calendar-mcp gives AI assistants like Claude direct access to Swiss school and public holiday data for all 26 cantons โ no API keys required. School holidays are set cantonally, sometimes at district level, and โ in six cantons โ separately per school type. A federal calendar does not exist; anyone planning across cantonal borders is otherwise reduced to opening 26 PDF pages.
The server covers two thematic clusters: school holidays (with Schulart differentiation) and public holidays / long weekends. Each cluster maps to a group of purpose-built tools that translate raw agency data into clean, provenance-tagged JSON responses. All data comes from the OpenHolidays API (CC BY 4.0) and Nager.Date (MIT).
Mnemonic: A duplicate in Swiss school data is usually a school type in disguise. The underlying API publishes the same holiday period several times when a canton differentiates by school type. That looks like duplicated data and invites naive de-duplication โ which would destroy exactly the distinction a school authority needs.
Anchor demo query: "In which weeks of 2026 are the compulsory schools of Zurich, Zug and Aargau simultaneously on holiday โ and how many overlapping days does each pair share?"
โ This exercises find_common_free_window, compare_school_holidays and list_school_types in a single conversation, and answers a question that recurs every planning cycle in inter-cantonal coordination.
โ More use cases by audience โ
Features
- ๐ซ School holidays โ periods per canton and date range, differentiated by Schulart (
VS/MS/BS/EO) - ๐ Public holidays โ cantonal holiday sets, not just the federal minimum (Berchtoldstag & friends)
- ๐ Date check โ is a given date a school or public holiday in a canton?
- ๐ Cross-cantonal comparison โ pairwise overlap matrix of holiday days between cantons
- ๐ช Common free windows โ date ranges where all listed cantons are simultaneously on holiday
- ๐ Long weekends & bridge days โ computed from federal public holidays (Nager.Date)
- ๐ฉบ Source health โ reachability and latency of both upstreams, always evaluable
- ๐ No authentication required โ both data sources are publicly accessible
- โ๏ธ Dual transport โ stdio for Claude Desktop, Streamable HTTP/SSE for cloud deployment
- ๐งพ Provenance on every response โ
live_api|cached|degraded, never a silent empty list
Data Sources
| Source | Data | Licence |
|---|---|---|
| OpenHolidays API | Cantons, Schularten, school holidays, public holidays | CC BY 4.0 |
| Nager.Date | Long weekends and required bridge days | MIT |
Both sources are publicly accessible, no authentication required. Attribution required: OpenHolidays (CC BY 4.0) and Nager.Date must be cited as the source when using their data.
Tools
| Tool | Purpose | Data Source |
|---|---|---|
list_cantons |
The 26 cantons with ISO codes and official languages | OpenHolidays |
list_school_types |
Schulart groups per canton (CH-ZH-VS etc.) |
OpenHolidays |
get_school_holidays |
School holidays for one canton and date range | OpenHolidays |
get_public_holidays |
Public holidays for one canton and year | OpenHolidays |
check_date |
Is a given date a school or public holiday? | OpenHolidays |
compare_school_holidays |
Pairwise overlap matrix across cantons | OpenHolidays |
find_common_free_window |
Windows where all listed cantons are on holiday | OpenHolidays |
next_school_holidays |
The next upcoming holiday periods | OpenHolidays |
get_long_weekends |
Long weekends and required bridge days | Nager.Date |
source_status |
Reachability and latency of both upstreams | Built-in |
All tools are annotated readOnlyHint: true. No tool writes anywhere.
Example Use Cases
| Query | Tool |
|---|---|
| "Which cantons are there, and what are their codes?" | list_cantons |
| "Show Zurich's compulsory-school holidays for spring 2026" | get_school_holidays |
| "Is 3 April 2026 a public holiday in Ticino?" | check_date |
| "Do Zurich and Zug school holidays overlap this year?" | compare_school_holidays |
| "When can all of ZH, ZG, AG plan a joint week off school?" | find_common_free_window |
| "What are the next holidays for Basel-Stadt schools?" | next_school_holidays |
| "Which long weekends does 2026 have, and which bridge days do they need?" | get_long_weekends |
๐ก๏ธ Safety & Limits
| Aspect | Details |
|---|---|
| Access | Read-only (readOnlyHint: true) โ the server cannot modify or delete any data |
| Personal data | No personal data โ all sources are aggregated, public holiday calendars |
| Caching | 12-hour in-memory TTL (holiday tables change a handful of times per year) |
| Retry | Exponential backoff 2s / 4s / 8s; 4xx except 429 are not retried |
| Timeout | 20 seconds per API call (8 seconds for health probes) |
| Authentication | No API keys required โ both upstreams are publicly accessible |
| Degradation | Upstream failure yields a degraded envelope with an explanatory note, never a silent empty list |
| Terms of Service | Subject to the ToS of the respective data sources: OpenHolidays, Nager.Date |
Architecture
This server uses Architecture A (live API only, with in-memory cache).
โโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Claude / any โโโโถโ swiss-school-calendar โ
MCP host โ (FastMCP ยท 10 tools) โ
โโโโโโโโโโฌโโโโโโโโโโโโโโโโโโ
โ retry 2s/4s/8s ยท 12h cache
โโโโโโโโโโดโโโโโโโโโโ
โผ โผ
OpenHolidays API Nager.Date
(CC BY 4.0) (MIT)
cantons ยท Schularten long weekends
school + public bridge days
Rationale (verified live on 2026-07-19):
- All ten documented OpenHolidays endpoints answered HTTP 200 with plausible payloads;
/Subdivisions?countryIsoCode=CHreturns exactly 26 cantons, matching the official count. - No public bulk dump could be verified at build time (
openpotato/openholidays.dataraw access returned 404), so Architecture B was not available. - Holiday tables change a handful of times per year, so a 12-hour in-memory TTL removes almost all upstream load without risking staleness.
Consequences:
- Every response carries
provenance(live_api|cached|degraded). - Upstream failure yields a
degradedenvelope with an explanatorynote, never a silent empty list. source_statusalways returns an evaluable health report.
Live-probe findings (2026-07-19)
| Endpoint | HTTP | Status | Records | Note |
|---|---|---|---|---|
/Countries |
200 | โ works | 36 | |
/Subdivisions?countryIsoCode=CH |
200 | โ works | 26 | matches official canton count |
/Groups?countryIsoCode=CH |
200 | โ works | 11 | Schulart groups, only 6 cantons |
/PublicHolidays (CH, 2026) |
200 | โ works | 39 | cantonal scope included |
/SchoolHolidays (CH, 2026) |
200 | โ works | 193 | 183 distinct after school-type split |
/SchoolHolidaysByDate |
200 | โ works | โ | |
/SchoolHolidays?countryIsoCode=XX |
200 | โ ๏ธ silently empty | 0 | invalid country โ error |
/Subdivisions?languageIsoCode=ZZ |
200 | โ ๏ธ silent EN fallback | 26 | invalid language โ error |
/SchoolHolidays without date range |
400 | โ correct error | โ | RFC 9110 problem+json |
Nager /PublicHolidays/2026/CH |
200 | โ works | 33 | 29 rows carry counties |
Nager /LongWeekend/2026/CH |
200 | โ works | 3 | |
Nager /PublicHolidays/2026/XX |
404 | โ correct error | โ | stricter than OpenHolidays |
Known findings
- Apparent duplicates are school types. Zurich returns Frรผhlingsferien 2026 twice: once for
CH-ZH-VS(Volksschulen, taggedRecommended) and once forCH-ZH-BS+CH-ZH-MS(Berufsfach- and Mittelschulen). Use theschool_typeparameter (VS/MS/BS/EO) rather than de-duplicating. - Only six cantons differentiate by school type (AI, AR, BE, GR, SO, ZH). Elsewhere
groupsis absent and one table covers everything. The filter therefore treats an absentgroupsfield as "applies to all". - Subdivision codes mix levels. Records may carry
CH-AI-APorCH-BE-TH-BL. Always match on theCH-XXprefix, never on string equality. - An empty list is not an answer. An unknown country or canton code yields HTTP 200 with
[]. This server sets an explanatorynoteso that "no holidays" and "bad filter" stay distinguishable.
Prerequisites
- Python 3.10 or higher
- uv / uvx (recommended) or pip
- Internet access (both APIs are publicly available)
Installation
Run via uv's uvx โ no clone or manual install needed:
uvx swiss-school-calendar-mcp
Development
git clone https://github.com/malkreide/swiss-school-calendar-mcp
cd swiss-school-calendar-mcp
pip install -e ".[dev]"
Configuration
Claude Desktop
Add to claude_desktop_config.json:
macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
Windows: %APPDATA%\Claude\claude_desktop_config.json
{
"mcpServers": {
"swiss-school-calendar": {
"command": "uvx",
"args": ["swiss-school-calendar-mcp"]
}
}
}
Restart Claude Desktop โ the server starts automatically on first use.
Cloud Deployment (SSE / Streamable HTTP for browser access)
For use via claude.ai in the browser (e.g. on managed workstations without local software):
MCP_TRANSPORT=sse PORT=8000 python -m swiss_school_calendar_mcp
FastMCP exposes SSE at /sse, not /mcp.
| Variable | Default | Description |
|---|---|---|
MCP_TRANSPORT |
stdio |
Transport: stdio, sse, streamable-http (aka http) |
PORT / MCP_PORT |
8000 |
Port for HTTP transports |
MCP_HOST |
0.0.0.0 |
Bind address for HTTP transports |
๐ก "stdio for the developer laptop, SSE for the browser."
Project Structure
swiss-school-calendar-mcp/
โโโ src/
โ โโโ swiss_school_calendar_mcp/
โ โโโ __init__.py # Package init
โ โโโ __main__.py # Entry point: stdio / SSE / Streamable HTTP
โ โโโ server.py # FastMCP server, 10 tools
โ โโโ client.py # HTTP client: retry, 12h in-memory cache, normalisation
โ โโโ constants.py # Canton codes, Schulart suffixes, API bases
โ โโโ models.py # Pydantic v2 response envelopes
โโโ tests/
โ โโโ conftest.py # respx fixtures
โ โโโ test_tools.py # Tool unit tests (mocked, no network)
โ โโโ test_resilience.py # Degradation / retry / cache behaviour
โ โโโ test_live.py # Live smoke tests (marker: live)
โโโ .github/
โ โโโ dependabot.yml # Weekly dependency / action update PRs
โ โโโ workflows/ # ci.yml, live-tests.yml, publish.yml
โโโ 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)
โโโ EXAMPLES.md # Use cases by audience
โโโ server.json # MCP registry manifest
โโโ LICENSE
โโโ README.md # This file (English)
โโโ README.de.md # German version
Lifecycle Phase
This server is in Phase 1 (read-only) โ all tools read-only, no auth, no side
effects. The 10-tool budget (of the 15โ20 recommended maximum) deliberately leaves
headroom for a Phase 2 extension: city-level Zurich specifics such as Sechselรคuten
and Knabenschiessen, which are neither public holidays nor school holidays upstream
and would arrive via zurich-opendata-mcp.
Known Limitations
- Unofficial source. OpenHolidays aggregates cantonal publications. For legally binding dates, the cantonal authority remains authoritative. Every response says so.
- No municipal layer. Zurich city specifics such as Sechselรคuten and Knabenschiessen are neither public holidays nor school holidays upstream, and are therefore absent. Candidate for Phase 2 via
zurich-opendata-mcp. - Nager long weekends ignore cantonal holidays. They are computed from nationwide holidays only.
- No historical depth guarantee. Coverage of years before roughly 2020 is uneven.
Testing
# Unit tests (no network required โ respx-mocked)
PYTHONPATH=src pytest tests/ -m "not live"
# Live smoke tests (hits the real upstream APIs)
PYTHONPATH=src pytest tests/ -m "live"
# Linting
ruff check src/ tests/
Contributing
Contributions are welcome! Please read CONTRIBUTING.md (English) ยท CONTRIBUTING.de.md (German) for guidelines on reporting bugs, setting up the development environment, code style and test requirements.
This project follows the conventions of the Swiss Public Data MCP Portfolio.
Security
To report a vulnerability, please follow the responsible disclosure process in SECURITY.md (English) ยท SECURITY.de.md (German). The server is read-only and requires no API key; see the Safety & Limits section above for the security model.
Changelog
See CHANGELOG.md
Deployment for Swiss Public Administration
If you self-host this server for a Swiss school authority or municipal use case:
- Data residency: the query patterns themselves (which cantons a civil servant compares) may reveal ongoing planning and are best kept on Swiss or trusted infrastructure.
- Upstream calls go to OpenHolidays (EU-hosted OGD project) and Nager.Date. No personal data leaves your environment; only holiday calendars are requested.
- Logging: logs are written to stderr; configure your IT retention policy accordingly.
- HTTP transport should run behind a reverse proxy with authentication and per-IP rate limits โ the server has no built-in authentication.
License
MIT License โ see LICENSE
Source data is subject to the terms of OpenHolidays (CC BY 4.0) and Nager.Date (MIT); attribution to these sources is required when using their data.
Author
Hayal Oezkan ยท github.com/malkreide
Credits & Related Projects
- Data: OpenHolidays API (CC BY 4.0) ยท Nager.Date (MIT)
- Protocol: Model Context Protocol โ Anthropic / Linux Foundation
- Built following the
mcp-data-source-probemethodology: live probe before design, dump fallback before API dependency, retry before defeatism. - Portfolio: Swiss Public Data MCP Portfolio
| Server | Description |
|---|---|
zh-education-mcp |
Canton of Zurich education data |
zurich-opendata-mcp |
City of Zurich Open Data |
swiss-statistics-mcp |
BFS STAT-TAB โ Swiss federal statistics |
swisstopo-mcp |
Swiss federal geodata (swisstopo) |
MIT licensed. Public money, public code.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file swiss_school_calendar_mcp-0.1.0.tar.gz.
File metadata
- Download URL: swiss_school_calendar_mcp-0.1.0.tar.gz
- Upload date:
- Size: 33.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6f4963855d45987f1286dfb7fbbee963d8ea1cd2e58ba6f9c66c2a4df40e42bb
|
|
| MD5 |
6d47a5657c4fa59dad8da2528d7da0e2
|
|
| BLAKE2b-256 |
d9ef78894fc8fd20b4faefc25bcd8883ee0126f0d78cb4d20d400a07f05d65d3
|
Provenance
The following attestation bundles were made for swiss_school_calendar_mcp-0.1.0.tar.gz:
Publisher:
publish.yml on malkreide/swiss-school-calendar-mcp
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
swiss_school_calendar_mcp-0.1.0.tar.gz -
Subject digest:
6f4963855d45987f1286dfb7fbbee963d8ea1cd2e58ba6f9c66c2a4df40e42bb - Sigstore transparency entry: 2206691209
- Sigstore integration time:
-
Permalink:
malkreide/swiss-school-calendar-mcp@4cc34f4a79eb944f9a7a60ff3d043bdd719d46ab -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/malkreide
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@4cc34f4a79eb944f9a7a60ff3d043bdd719d46ab -
Trigger Event:
release
-
Statement type:
File details
Details for the file swiss_school_calendar_mcp-0.1.0-py3-none-any.whl.
File metadata
- Download URL: swiss_school_calendar_mcp-0.1.0-py3-none-any.whl
- Upload date:
- Size: 20.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
18cadeb84a6e2de42a186d72fc2c716e0ae4fef67d3aa9c94dd6ce2e05b3fd13
|
|
| MD5 |
1736921536a840d2cd5bb76f0d7670cc
|
|
| BLAKE2b-256 |
b1ece25f4b36bcb38d95efbdd173ae36c271d52bd537d947e6a9a22d844e42c8
|
Provenance
The following attestation bundles were made for swiss_school_calendar_mcp-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on malkreide/swiss-school-calendar-mcp
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
swiss_school_calendar_mcp-0.1.0-py3-none-any.whl -
Subject digest:
18cadeb84a6e2de42a186d72fc2c716e0ae4fef67d3aa9c94dd6ce2e05b3fd13 - Sigstore transparency entry: 2206691229
- Sigstore integration time:
-
Permalink:
malkreide/swiss-school-calendar-mcp@4cc34f4a79eb944f9a7a60ff3d043bdd719d46ab -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/malkreide
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@4cc34f4a79eb944f9a7a60ff3d043bdd719d46ab -
Trigger Event:
release
-
Statement type: