Back to Browse

Swiss Holidays MCP Server

Developer ToolsLow Risk10.0MCP RegistryLocal
Free

Server data from the Official MCP Registry

Swiss school & public holidays, 26 cantons, by Schulart (OpenHolidays + Nager.Date)

About

Swiss school & public holidays, 26 cantons, by Schulart (OpenHolidays + Nager.Date)

Security Report

10.0
Low Risk10.0Low Risk

Valid MCP server (1 strong, 2 medium validity signals). No known CVEs in dependencies. Package registry verified. Imported from the Official MCP Registry. Trust signals: trusted author (35/36 approved).

3 files analyzed Β· 1 issue found

Security scores are indicators to help you make informed decisions, not guarantees. Always review permissions before connecting any MCP server.

How to Install

Add this to your MCP configuration file:

{
  "mcpServers": {
    "io-github-malkreide-swiss-holidays-mcp": {
      "args": [
        "swiss-holidays-mcp"
      ],
      "command": "uvx"
    }
  }
}

Documentation

View on GitHub

From the project's GitHub README.

πŸ‡¨πŸ‡­ 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 β†’

Demo

Demo: Claude using find_common_free_window and compare_school_holidays


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)
  • 🏘️ Local & municipal holidays β€” district- and municipality-level specifics such as Zurich's SechselΓ€uten and Knabenschiessen, with a scope marker so they are never mistaken for canton-wide
  • πŸ“† iCal / ICS export β€” a canton's holidays for a year as a ready-to-import .ics calendar
  • πŸ”– Holiday feed resource β€” holidays://<canton>/<year> MCP resource with a Markdown summary
  • πŸ“Œ "Is today a holiday?" β€” one-call convenience for the everyday question
  • 🩺 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

SourceDataLicence
OpenHolidays APICantons, Schularten, school holidays, public holidaysCC BY 4.0
Nager.DateLong weekends and required bridge daysMIT

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

ToolPurposeData Source
list_cantonsThe 26 cantons with ISO codes and official languagesOpenHolidays
list_school_typesSchulart groups per canton (CH-ZH-VS etc.)OpenHolidays
get_school_holidaysSchool holidays for one canton and date rangeOpenHolidays
get_public_holidaysPublic holidays for one canton and yearOpenHolidays
get_local_holidaysPublic holidays for one municipality or district, incl. local specificsOpenHolidays
check_dateIs a given date a school or public holiday?OpenHolidays
compare_school_holidaysPairwise overlap matrix across cantonsOpenHolidays
find_common_free_windowWindows where all listed cantons are on holidayOpenHolidays
next_school_holidaysThe next upcoming holiday periodsOpenHolidays
get_long_weekendsLong weekends and required bridge daysNager.Date
export_holidays_icsA canton's holidays for a year as an iCalendar (.ics) documentOpenHolidays
is_holiday_todayIs today a school or public holiday in a canton?OpenHolidays
source_statusReachability and latency of both upstreamsBuilt-in

Resources

Resource URIContent
holidays://{canton}/{year}Markdown summary of all public + school holidays, e.g. holidays://CH-ZH/2026

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

QueryTool
"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
"Which local holidays does the city of Zurich keep that the rest of the canton doesn't?"get_local_holidays
"Export Zurich's 2026 holidays as an .ics calendar I can import"export_holidays_ics
"Is today a holiday in Aargau?"is_holiday_today

πŸ›‘οΈ Safety & Limits

AspectDetails
AccessRead-only (readOnlyHint: true) β€” the server cannot modify or delete any data
Personal dataNo personal data β€” all sources are aggregated, public holiday calendars
Caching12-hour in-memory TTL (holiday tables change a handful of times per year)
RetryExponential backoff 2s / 4s / 8s; 4xx except 429 are not retried
Timeout20 seconds per API call (8 seconds for health probes)
AuthenticationNo API keys required β€” both upstreams are publicly accessible
DegradationUpstream failure yields a degraded envelope with an explanatory note, never a silent empty list
Terms of ServiceSubject 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         β”‚  (MCPServer Β· 13 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)

EndpointHTTPStatusRecordsNote
/Countries200βœ… works36
/Subdivisions?countryIsoCode=CH200βœ… works26matches official canton count
/Groups?countryIsoCode=CH200βœ… works11Schulart groups, only 6 cantons
/PublicHolidays (CH, 2026)200βœ… works39cantonal scope included
/SchoolHolidays (CH, 2026)200βœ… works193183 distinct after school-type split
/SchoolHolidaysByDate200βœ… works–
/SchoolHolidays?countryIsoCode=XX200⚠️ silently empty0invalid country β‰  error
/Subdivisions?languageIsoCode=ZZ200⚠️ silent EN fallback26invalid language β‰  error
/SchoolHolidays without date range400βœ… correct error–RFC 9110 problem+json
Nager /PublicHolidays/2026/CH200βœ… works3329 rows carry counties
Nager /LongWeekend/2026/CH200βœ… works3
Nager /PublicHolidays/2026/XX404βœ… 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

The SDK exposes SSE at /sse, not /mcp.

VariableDefaultDescription
MCP_TRANSPORTstdioTransport: stdio, sse, streamable-http (aka http)
PORT / MCP_PORT8000Port for HTTP transports
MCP_HOST127.0.0.1Bind address for HTTP transports. Loopback by default; 0.0.0.0 is opt-in and logs a warning β€” run behind an authenticating reverse proxy.
MCP_CORS_ORIGINS(empty)Comma-separated extra CORS origins for browser clients (audit SDK-004). Loopback origins are always allowed; add the public origin your UI is served from, e.g. https://ui.example.ch. Never *.

The HTTP transports attach an explicit CORS layer that exposes the Mcp-Session-Id header, so a browser MCP client can read the session id and make follow-up requests. The allow-list is never a wildcard.

Running more than one HTTP instance behind a load balancer requires sticky sessions keyed on Mcp-Session-Id β€” see docs/scaling.md for nginx/Traefik/Kubernetes examples. A single instance (the common case) needs no affinity configuration.

πŸ’‘ "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         # MCPServer: lifespan, 13 tools, 1 resource, op_* logic
β”‚       β”œβ”€β”€ client.py         # Shared HTTP client: retry, 12h cache, egress guard
β”‚       β”œβ”€β”€ guard.py          # Egress / SSRF guard (HTTPS + allow-list + IP blocklist)
β”‚       β”œβ”€β”€ pinning.py        # DNS-pinning transport (TOCTOU-free connect, SEC-005)
β”‚       β”œβ”€β”€ ical.py           # RFC 5545 iCalendar (.ics) writer
β”‚       β”œβ”€β”€ 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
β”œβ”€β”€ deploy/                   # Network-layer egress manifests (Cilium / NetworkPolicy)
β”œβ”€β”€ 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

On the single-file server.py (audit ARCH-011). The 13 tools deliberately live in one module rather than a tools/ package. Each tool is a thin, uniform wrapper (@mcp.tool β†’ @_safe_tool β†’ op_*) over a transport-agnostic op_* operation, and every operation shares the same small set of helpers (_to_period, _matches_school_type, _require_known_canton, …) and the one HolidayClient. Splitting these across files would scatter that shared core and duplicate imports for no isolation benefit β€” the file is uniformly sectioned (aliases β†’ helpers β†’ op_* logic β†’ tool wrappers β†’ resource) and every op_* is unit-tested directly without a transport. A tools/ split is the planned step only if Phase 2 pushes the tool count materially higher.


Lifecycle Phase

This server is in Phase 1 (read-only) β€” all tools read-only, no auth, no side effects. The 13-tool budget (of the 15–20 recommended maximum) still leaves headroom. Local and municipal specifics β€” including Zurich's SechselΓ€uten and Knabenschiessen β€” are covered directly from OpenHolidays via get_local_holidays (a live probe showed they are published upstream at Gemeinde level), so no separate city data source is required for them.


MCP Primitives & Protocol Version

  • Primitives β€” Tools + Resources. The 13 tools are idempotent, side-effect-free GETs. A Resource exposes a stable URI feed (holidays://<canton>/<year>) so clients can read a canton's calendar as cacheable context without a tool call. There are no recurring templated workflows, so Prompts are not used (revisited if that changes).

  • MCP protocol version β€” two eras. mcp 2.x serves both over the same server, and the client's first request on a connection decides which applies: the initialize handshake caps at 2025-11-25, the per-request envelope reaches 2026-07-28.

    source_status surfaces one of them in its mcp_protocol_version field β€” a single string cannot name both β€” and it surfaces the handshake ceiling, because that is what a client reaching this server over initialize actually negotiated. Measured, not inferred from a constant name: a client asking the handshake for 2026-07-28 gets 2025-11-25 back.

    MCP_PROTOCOL_VERSION is derived from the SDK's LATEST_HANDSHAKE_VERSION rather than written down, so it cannot drift the way it once did β€” it stood at 2025-06-18 for two revisions while every call reported it as fact. tests/test_protocol_version.py holds both eras against the SDK and checks the delivered field against the SDK too, not against the constant it came from. The wire version is negotiated by the pinned mcp SDK (mcp>=2.0.0,<3).

  • 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.
  • Municipal coverage depends on the upstream. OpenHolidays does carry district- and municipality-level public holidays (e.g. SechselΓ€uten, Knabenschiessen at CH-ZH-ZH-ZH), exposed through get_local_holidays. Completeness at Gemeinde level is only as good as the upstream data, which varies by canton. Municipal school holidays are not separately modelled.
  • 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/ scripts/
ruff format --check src/ tests/ scripts/

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

ServerDescription
zh-education-mcpCanton of Zurich education data
zurich-opendata-mcpCity of Zurich Open Data
swiss-statistics-mcpBFS STAT-TAB β€” Swiss federal statistics
swisstopo-mcpSwiss federal geodata (swisstopo)

MIT licensed. Public money, public code.

Reviews

No reviews yet

Be the first to review this server!