Skip to main content

Swiss holiday calendar for AI agents โ€” public holidays, school holidays (by Schulart) and long weekends for all 26 cantons

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-holidays-mcp

License: MIT Python 3.10+ MCP CI No Auth Required Data Source

A Swiss holiday calendar for AI agents โ€” public holidays, school holidays and long weekends for all 26 cantons, with cross-cantonal comparison. School holidays are differentiated by Schulart (school type), which matters more than it first appears. No API key required.

๐Ÿ‡ฉ๐Ÿ‡ช Deutsche Version


Overview

swiss-holidays-mcp is a Swiss holiday calendar for AI assistants like Claude โ€” public holidays, school holidays and long weekends for all 26 cantons, no API keys required. Public holidays are cantonal (Berchtoldstag, Fronleichnam & co. differ by canton, not just the federal minimum). School holidays are set cantonally, sometimes at district level, and โ€” in six cantons โ€” separately per school type. A single federal calendar does not exist; anyone planning across cantonal borders is otherwise reduced to opening 26 PDF pages.

The server covers two thematic clusters: public holidays / long weekends and school holidays (with Schulart differentiation). 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 carry the full annotation set โ€” readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true (they reach an external API). No tool writes anywhere. Inputs are schema-validated (canton codes against the 26 known cantons, dates as YYYY-MM-DD, year bounded, language/school_type whitelisted).

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-holidays-mcp      โ”‚
   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=CH returns exactly 26 cantons, matching the official count.
  • No public bulk dump could be verified at build time (openpotato/openholidays.data raw 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 degraded envelope with an explanatory note, never a silent empty list.
  • source_status always 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

  1. Apparent duplicates are school types. Zurich returns Frรผhlingsferien 2026 twice: once for CH-ZH-VS (Volksschulen, tagged Recommended) and once for CH-ZH-BS + CH-ZH-MS (Berufsfach- and Mittelschulen). Use the school_type parameter (VS / MS / BS / EO) rather than de-duplicating.
  2. Only six cantons differentiate by school type (AI, AR, BE, GR, SO, ZH). Elsewhere groups is absent and one table covers everything. The filter therefore treats an absent groups field as "applies to all".
  3. Subdivision codes mix levels. Records may carry CH-AI-AP or CH-BE-TH-BL. Always match on the CH-XX prefix, never on string equality.
  4. An empty list is not an answer. An unknown country or canton code yields HTTP 200 with []. This server sets an explanatory note so 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-holidays-mcp

Development

git clone https://github.com/malkreide/swiss-holidays-mcp
cd swiss-holidays-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-holidays": {
      "command": "uvx",
      "args": ["swiss-holidays-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_holidays_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 127.0.0.1 Bind address for HTTP transports. Loopback by default; 0.0.0.0 is opt-in and logs a warning โ€” run behind an authenticating reverse proxy.

๐Ÿ’ก "stdio for the developer laptop, SSE for the browser."


Project Structure

swiss-holidays-mcp/
โ”œโ”€โ”€ src/
โ”‚   โ””โ”€โ”€ swiss_holidays_mcp/
โ”‚       โ”œโ”€โ”€ __init__.py       # Package init
โ”‚       โ”œโ”€โ”€ __main__.py       # Entry point: stdio / SSE / Streamable HTTP
โ”‚       โ”œโ”€โ”€ server.py         # FastMCP server: lifespan, 10 tools, op_* logic
โ”‚       โ”œโ”€โ”€ client.py         # Shared HTTP client: retry, 12h cache, egress guard
โ”‚       โ”œโ”€โ”€ guard.py          # Egress / SSRF guard (HTTPS + allow-list + IP blocklist)
โ”‚       โ”œโ”€โ”€ settings.py       # Pydantic-Settings config (loopback default)
โ”‚       โ”œโ”€โ”€ logging_setup.py  # Structured logging to stderr
โ”‚       โ”œโ”€โ”€ constants.py      # Canton codes, Schulart suffixes, API bases, allow-list
โ”‚       โ””โ”€โ”€ 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)
โ”œโ”€โ”€ docs/                     # roadmap.md, security.md, network-egress.md
โ”œโ”€โ”€ audits/                   # mcp-audit run artifacts
โ”œโ”€โ”€ Dockerfile                # Non-root multi-stage container
โ”œโ”€โ”€ .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.


MCP Primitives & Protocol Version

  • Primitives โ€” Tools only (deliberate). This is a Phase 1 read-only wrapper whose entire surface is idempotent, side-effect-free GETs. Modelling them as Resources was considered and deferred: a stable URI scheme (holidays://<canton>/<year>) is a Phase 2 item once the municipal layer lands. There are no recurring templated workflows, so Prompts are not used. This choice is revisited when the server reaches Phase 2.
  • MCP protocol version. Built and tested against protocol version 2025-06-18 (pinned as MCP_PROTOCOL_VERSION and surfaced by source_status). The wire version is negotiated by the pinned mcp SDK (mcp>=1.2.0,<2).
  • Update policy. SDK and dependency bumps land via Dependabot (weekly); protocol-version or tool-definition changes are recorded in CHANGELOG.md with a version bump.

Data classification

All data is ร–ffentlich / Public Open Data โ€” aggregated holiday calendars, no personal data (DSG/DSGVO). This is the highest classification the server handles; the full model is in docs/security.md.

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

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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

swiss_holidays_mcp-0.3.0.tar.gz (41.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

swiss_holidays_mcp-0.3.0-py3-none-any.whl (27.2 kB view details)

Uploaded Python 3

File details

Details for the file swiss_holidays_mcp-0.3.0.tar.gz.

File metadata

  • Download URL: swiss_holidays_mcp-0.3.0.tar.gz
  • Upload date:
  • Size: 41.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for swiss_holidays_mcp-0.3.0.tar.gz
Algorithm Hash digest
SHA256 64d576b803ae7250a0d14714cca425f7e7516feffedc1fc8008d4b3b4681c72d
MD5 16c875c7a7b9fb8e60e7f28346e2d9f6
BLAKE2b-256 87c5e2a22d124fe4b1728413e5308c2e96ca62a9401f2db8fb6ff72b2f0e0a84

See more details on using hashes here.

Provenance

The following attestation bundles were made for swiss_holidays_mcp-0.3.0.tar.gz:

Publisher: publish.yml on malkreide/swiss-holidays-mcp

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file swiss_holidays_mcp-0.3.0-py3-none-any.whl.

File metadata

File hashes

Hashes for swiss_holidays_mcp-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 9f80070a4037ed3b1ec6b9b4d601be438c3b25a35dc7c2653575b952d590e82c
MD5 0f967977971bcde7057731b37315fb3e
BLAKE2b-256 3238f99ff5324af080c5ec3697cc5ce22aee21ce463f4d4563757b919aecb16f

See more details on using hashes here.

Provenance

The following attestation bundles were made for swiss_holidays_mcp-0.3.0-py3-none-any.whl:

Publisher: publish.yml on malkreide/swiss-holidays-mcp

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page