
Swiss Holidays Mcp
io.github.malkreidePublished by GitHub account malkreidev0.7.0Updated Oct 24, 2026
Swiss school & public holidays, 26 cantons, by Schulart (OpenHolidays + Nager.Date)
Overview
Swiss holiday calendar MCP server: lets an assistant look up public and school holidays for all 26 cantons, compare cantons and export iCal files.
- What you can do
- List the 26 cantons and their school-type (Schulart) groups
- Look up school and public holidays for a canton and date range, including local and municipal holidays
- Check whether a given date or today is a holiday, and find the next upcoming school holidays
- Compare cantons pairwise and find windows when several cantons are on holiday at once
- Compute long weekends and bridge days, and export a canton's year as an iCalendar document
- Read a canton's calendar as a Markdown resource and check upstream source health
- When to use it
- Useful when planning around Swiss school or public holidays: inter-cantonal holiday overlap, family or staffing planning, bridge days, or producing an importable calendar. It saves opening 26 separate cantonal PDF calendars.
- Requirements
- Local Python process, Python 3.10 or higher, run via uvx (or pip). Internet access to the OpenHolidays and Nager.Date APIs. No API key or account. Optional HTTP/SSE transport is configured through MCP_TRANSPORT, PORT, MCP_HOST and MCP_CORS_ORIGINS.
Installation
In SourceWeft
- Open Swiss Holidays Mcp in the dashboard and add it to a workspace.
- Enable the server for the chats that should use its tools.
Desktop only via STDIO. STDIO servers start a local process, so they need the SourceWeft desktop host.
Other MCP clients
Follow the launch instructions in the repository.
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.
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
scopemarker so they are never mistaken for canton-wide - 📆 iCal / ICS export — a canton's holidays for a year as a ready-to-import
.icscalendar - 🔖 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
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
Resources
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
🛡️ Safety & Limits
Architecture
This server uses Architecture A (live API only, with in-memory cache).
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)
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:
Development
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
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):
The SDK exposes SSE at /sse, not /mcp.
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
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, both served natively.
mcp2.x serves both over the same server, and the client's first request on a connection decides which applies: theinitializehandshake caps at2025-11-25; the per-request envelope era (spec2026-07-28) has no handshake at all. There every request carries its protocol version, client info and client capabilities inparams._meta, mirrored by theMcp-Protocol-Version/Mcp-Method/Mcp-Namerouting headers, andserver/discovertakes the place ofinitialize.source_statusreports inmcp_protocol_versionthe revision this request came in on, read per request fromContext.protocol_version. It used to report one constant — the handshake ceiling — to every caller, so a client that had negotiated2026-07-28was told2025-11-25about its own connection. The constant survives only as the fallback for theop_*layer, where nothing was negotiated; it is derived from the SDK'sLATEST_HANDSHAKE_VERSIONrather than written down, so it cannot drift the way it once did — it stood at2025-06-18for two revisions while every call reported it as fact.Measured, not inferred:
tests/test_protocol_version.pydrives both eras through the same ASGI app this repo serves in production and compares the delivered field per era. The previous version of that file stated the repo could not build such an app — it can, and the claim outlived the measurement by two revisions. A client asking the handshake for2026-07-28still gets2025-11-25back, now measured here rather than at a sister server. The wire version is negotiated by the pinnedmcpSDK (mcp>=2.0.0,<3). -
Server identity travels with every result.
2026-07-28has noinitializeresult, soserverInforides along as a_metastamp on each one — the only place a caller learns which server and which build answered. The server handsMCPServerits package version and homepage; without them the SDK stampsversion: "", and it never substitutes one of its own. An empty version is worse than an absent one: it looks like an answer. -
Freshness hints on the listing methods (SEP-2549):
tools/list,resources/list,resources/templates/listandserver/discoveranswer withttlMs300000 andcacheScope: public. Without them the SDK's default is "stale immediately, never shared", which makes every client re-list on every connection — for lists that are fixed at import.resources/readcarries no hint:holidays://{canton}/{year}is a live query, not a directory. -
Update policy. SDK and dependency bumps land via Dependabot (monthly, grouped); protocol-version or tool-definition changes are recorded in
CHANGELOG.mdwith 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 throughget_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
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
MIT licensed. Public money, public code.
Source: README.md at commit 4558879
Tools
0Version history
2- v0.7.0LatestSep 26, 2026
- v0.6.0Sep 16, 2026
Score
50% of the applicable checks met
How complete this listing is, computed from the registry entry, the endpoint and public sources. Not a rating of the server and not a statement that it is safe or working today: reachability is a dated check, and no installs or reviews go into the score.
- Reached in the last 30 days25 pts
Keep the endpoint answering the MCP handshake; SourceWeft probes it every 30 days.
- Auth declared10 pts
Declare the credential headers in the registry entry, or keep the endpoint answering the probe so its auth can be classified.
- Tools listed15 pts
Answer tools/list without a credential, or ship a package that starts in a sandbox without one.
- DescriptionRequired15 pts
- Logo10 pts
- Documentation15 pts
- License10 pts