The full upstream README, mirrored here for reference. Install config, tool schemas, adoption signals, and an original overview live on the Swiss Holidays MCP listing page.
🇨🇭 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.
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.
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 →
VS / MS / BS / EO)scope marker so they are never mistaken for canton-wide.ics calendarholidays://<canton>/<year> MCP resource with a Markdown summarylive_api | cached | degraded, never a silent empty list| 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.
| 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 |
get_local_holidays | Public holidays for one municipality or district, incl. local specifics | 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 |
export_holidays_ics | A canton's holidays for a year as an iCalendar (.ics) document | OpenHolidays |
is_holiday_today | Is today a school or public holiday in a canton? | OpenHolidays |
source_status | Reachability and latency of both upstreams | Built-in |
| Resource URI | Content |
|---|---|
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).
| 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 |
| "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 |
| 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 |
This server uses Architecture A (live API only, with in-memory cache).
Rationale (verified live on 2026-07-19):
/Subdivisions?countryIsoCode=CH returns exactly 26 cantons, matching the official count.openpotato/openholidays.data raw access returned 404), so Architecture B was not available.Consequences:
provenance (live_api | cached | degraded).degraded envelope with an explanatory note, never a silent empty list.source_status always returns an evaluable health report.| 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 |
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.groups is absent and one table covers everything. The filter therefore treats an absent groups field as "applies to all".CH-AI-AP or CH-BE-TH-BL. Always match on the CH-XX prefix, never on string equality.[]. This server sets an explanatory note so that "no holidays" and "bad filter" stay distinguishable.Run via uv's uvx — no clone or manual install needed:
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.
For use via claude.ai in the browser (e.g. on managed workstations without local software):
The SDK 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. |
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."
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.
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.
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.
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.
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.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.
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.
See CHANGELOG.md
If you self-host this server for a Swiss school authority or municipal use case:
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.
Hayal Oezkan · github.com/malkreide
mcp-data-source-probe methodology: live probe before design, dump fallback before API dependency, retry before defeatism.| 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.