feiertage-mcp 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,20 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags: ["v*"]
6
+
7
+ jobs:
8
+ publish:
9
+ runs-on: ubuntu-latest
10
+ permissions:
11
+ id-token: write # required for PyPI trusted publishing (OIDC)
12
+ contents: read
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ - uses: actions/setup-python@v5
16
+ with:
17
+ python-version: "3.12"
18
+ - run: pip install build
19
+ - run: python -m build
20
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,95 @@
1
+ name: Publish to MCP Registry
2
+
3
+ # Official MCP Registry (registry.modelcontextprotocol.io) via GitHub OIDC.
4
+ # No secret, no interactive login: the runner's OIDC token proves ownership
5
+ # of the io.github.mohammad-abbasi-sh/* namespace.
6
+ # Refs: https://github.com/modelcontextprotocol/registry
7
+
8
+ on:
9
+ push:
10
+ tags: ["v*"]
11
+ workflow_dispatch:
12
+
13
+ jobs:
14
+ publish:
15
+ runs-on: ubuntu-latest
16
+ permissions:
17
+ id-token: write # required for OIDC -> MCP Registry
18
+ contents: read
19
+ steps:
20
+ - uses: actions/checkout@v4
21
+
22
+ - name: Sync server.json version to tag
23
+ # Only sync when running on a version tag (v*). On workflow_dispatch
24
+ # (ref = refs/heads/main) keep the version committed in server.json.
25
+ run: |
26
+ VER=""
27
+ if [[ "$GITHUB_REF" == refs/tags/* ]]; then
28
+ TAG="${GITHUB_REF#refs/tags/}"
29
+ VER="${TAG#v}"
30
+ fi
31
+ if [ -n "$VER" ]; then
32
+ python3 - "$VER" << 'EOF'
33
+ import json, sys
34
+ ver = sys.argv[1]
35
+ d = json.load(open('server.json'))
36
+ d['version'] = ver
37
+ for pkg in d.get('packages', []):
38
+ pkg['version'] = ver
39
+ json.dump(d, open('server.json', 'w'), indent=2, ensure_ascii=False)
40
+ print('server.json version ->', ver)
41
+ EOF
42
+ else
43
+ echo "No version tag ($GITHUB_REF); keeping server.json version as-is"
44
+ fi
45
+
46
+ - name: Wait for PyPI
47
+ # The registry validates that the PyPI package + version exists,
48
+ # but the PyPI publish workflow runs in parallel with this one.
49
+ # Poll until the version appears instead of racing it: a 400 here
50
+ # used to fail silently under continue-on-error and left 40/43
51
+ # servers unlisted in the registry (2026-11-11).
52
+ run: |
53
+ python3 - << 'EOF'
54
+ import json, sys, time, urllib.request, urllib.error
55
+ d = json.load(open('server.json'))
56
+ pkg = d['packages'][0]
57
+ name = pkg.get('identifier') or pkg.get('name')
58
+ ver = pkg['version']
59
+ if not name:
60
+ print('ERROR: no package identifier/name in server.json')
61
+ sys.exit(1)
62
+ url = f'https://pypi.org/pypi/{name}/json'
63
+ for attempt in range(1, 21):
64
+ try:
65
+ with urllib.request.urlopen(url, timeout=30) as r:
66
+ data = json.load(r)
67
+ if ver in data.get('releases', {}):
68
+ print(f'PyPI {name}=={ver} is live (attempt {attempt})')
69
+ sys.exit(0)
70
+ except urllib.error.HTTPError as e:
71
+ print(f'attempt {attempt}: PyPI returned {e.code}')
72
+ except Exception as e:
73
+ print(f'attempt {attempt}: {e}')
74
+ print(f'attempt {attempt}: {name}=={ver} not on PyPI yet, waiting 30s...')
75
+ time.sleep(30)
76
+ print(f'ERROR: {name}=={ver} did not appear on PyPI after 10 minutes')
77
+ sys.exit(1)
78
+ EOF
79
+
80
+ - name: Install mcp-publisher
81
+ run: |
82
+ curl -sL "https://github.com/modelcontextprotocol/registry/releases/download/v1.8.7/mcp-publisher_linux_amd64.tar.gz" -o /tmp/mcp-publisher.tar.gz
83
+ tar -xzf /tmp/mcp-publisher.tar.gz -C /tmp
84
+ sudo mv /tmp/mcp-publisher /usr/local/bin/mcp-publisher
85
+ mcp-publisher --version
86
+
87
+ - name: Validate server.json
88
+ run: mcp-publisher validate server.json
89
+
90
+ - name: Publish to MCP Registry (OIDC)
91
+ # Failures must fail the run visibly: a silent publish failure
92
+ # once left 40/43 servers unlisted in the registry (2026-11-11).
93
+ run: |
94
+ mcp-publisher login github-oidc
95
+ mcp-publisher publish server.json
@@ -0,0 +1,21 @@
1
+ name: Daily live smoke
2
+
3
+ # One fixed probe per day: catches fingerprint rot / bot-wall changes
4
+ # before users hit them. Fails visibly on 403/timeout.
5
+ on:
6
+ schedule:
7
+ - cron: "0 6 * * *"
8
+ workflow_dispatch:
9
+
10
+ jobs:
11
+ smoke:
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ - uses: actions/setup-python@v5
16
+ with:
17
+ python-version: "3.12"
18
+ - name: Install
19
+ run: pip install -e .
20
+ - name: Smoke probe
21
+ run: python tests/smoke_live.py
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mohammad Abbasi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,67 @@
1
+ Metadata-Version: 2.5
2
+ Name: feiertage-mcp
3
+ Version: 0.1.0
4
+ Summary: Unofficial read-only MCP server for German public holidays (feiertage-api.de) — holidays by year & state, is-it-a-holiday, next holiday & date ranges
5
+ Author: Mohammad Abbasi
6
+ License: MIT
7
+ License-File: LICENSE
8
+ Requires-Python: >=3.10
9
+ Requires-Dist: httpx>=0.27
10
+ Requires-Dist: mcp<2
11
+ Description-Content-Type: text/markdown
12
+
13
+ # feiertage-mcp 📅
14
+
15
+ mcp-name: io.github.mohammad-abbasi-sh/feiertage-mcp
16
+
17
+ Unofficial **read-only** MCP server for **German public holidays** (gesetzliche Feiertage) via the [feiertage-api.de API](https://feiertage-api.de) (bundesAPI project): holidays per year and federal state, is-it-a-holiday checks, the next holiday from a date, and holidays in a date range. No key, no login.
18
+
19
+ > Built by Mohammad Abbasi as part of the "Germany MCP factory". Not affiliated with feiertage-api.de / the bundesAPI project.
20
+
21
+ ## For AI agents
22
+
23
+ Add to your MCP client config (stdio):
24
+
25
+ ```json
26
+ {
27
+ "mcpServers": {
28
+ "feiertage": {
29
+ "command": "uvx",
30
+ "args": ["feiertage-mcp==0.1.0"]
31
+ }
32
+ }
33
+ }
34
+ ```
35
+
36
+ Or run locally: `python -m feiertage_mcp.server` (needs `mcp<2`, `httpx`).
37
+
38
+ ## Tools
39
+
40
+ | Tool | Description |
41
+ |------|-------------|
42
+ | `feiertage_list_holidays` | Public holidays of one year in one federal state (optional month filter) |
43
+ | `feiertage_is_holiday` | Is a date (YYYY-MM-DD) a public holiday in one state? |
44
+ | `feiertage_next_holiday` | The next public holiday on or after a date (looks into the next year) |
45
+ | `feiertage_holidays_between` | Public holidays in a date range (max ~2 years) |
46
+ | `feiertage_list_states` | All 16 federal states + NATIONAL (nationwide holidays) |
47
+ | `feiertage_health` | Reachability check (ok / blocked / error) — one polite request |
48
+
49
+ Example output (`feiertage_is_holiday` / 2026-10-03, NATIONAL):
50
+
51
+ ```json
52
+ {"date": "2026-10-03", "weekday": "Saturday", "weekday_de": "Samstag",
53
+ "state": "NATIONAL", "is_holiday": true,
54
+ "holidays": ["Tag der Deutschen Einheit"],
55
+ "details": [{"name": "Tag der Deutschen Einheit", "date": "2026-10-03",
56
+ "note": null, "note_only": false, "weekday": "Saturday",
57
+ "weekday_de": "Samstag"}]}
58
+ ```
59
+
60
+ Notes:
61
+ - State codes are the 2-letter codes: BW, BY, BE, BB, HB, HH, HE, MV, NI, NW, RP, SL, SN, ST, SH, TH — or `NATIONAL` for the nationwide holidays (Neujahrstag, Karfreitag, Ostermontag, Tag der Arbeit, Christi Himmelfahrt, Pfingstmontag, Tag der Deutschen Einheit, 1./2. Weihnachtstag). Use `feiertage_list_states` for the full list.
62
+ - Holiday names are German; each dated holiday also carries its weekday in English and German.
63
+ - A few entries have no date (`note_only: true`) — they are notes, e.g. school-free days, not statutory public holidays. Date-based tools skip them.
64
+ - Years 1900–2100 are accepted; the API serves historical and future calendars.
65
+ - `feiertage_holidays_between` fetches one request per year in the range (max ~2 years).
66
+
67
+ Built by [Mohammad Abbasi](https://github.com/mohammad-abbasi-sh) — [Almanibama](https://almanibama.ir) · [mabbasi.ir](https://mabbasi.ir)
@@ -0,0 +1,55 @@
1
+ # feiertage-mcp 📅
2
+
3
+ mcp-name: io.github.mohammad-abbasi-sh/feiertage-mcp
4
+
5
+ Inoffizieller **Nur-Lesen**-MCP-Server für **deutsche Feiertage** (gesetzliche Feiertage) über die [feiertage-api.de API](https://feiertage-api.de) (bundesAPI-Projekt): Feiertage pro Jahr und Bundesland, Ist-es-ein-Feiertag-Prüfung, der nächste Feiertag ab einem Datum und Feiertage in einem Zeitraum. Kein Schlüssel, keine Anmeldung nötig.
6
+
7
+ > Gebaut von Mohammad Abbasi im Rahmen der „Germany MCP factory". Keine Verbindung zu feiertage-api.de / dem bundesAPI-Projekt.
8
+
9
+ ## Für KI-Agenten
10
+
11
+ In die MCP-Client-Konfiguration eintragen (stdio):
12
+
13
+ ```json
14
+ {
15
+ "mcpServers": {
16
+ "feiertage": {
17
+ "command": "uvx",
18
+ "args": ["feiertage-mcp==0.1.0"]
19
+ }
20
+ }
21
+ }
22
+ ```
23
+
24
+ Oder lokal starten: `python -m feiertage_mcp.server` (benötigt `mcp<2`, `httpx`).
25
+
26
+ ## Tools
27
+
28
+ | Tool | Beschreibung |
29
+ |------|--------------|
30
+ | `feiertage_list_holidays` | Feiertage eines Jahres in einem Bundesland (optional nach Monat gefiltert) |
31
+ | `feiertage_is_holiday` | Ist ein Datum (YYYY-MM-DD) ein Feiertag in einem Bundesland? |
32
+ | `feiertage_next_holiday` | Der nächste Feiertag ab einem Datum (blickt auch ins nächste Jahr) |
33
+ | `feiertage_holidays_between` | Feiertage in einem Zeitraum (max. ca. 2 Jahre) |
34
+ | `feiertage_list_states` | Alle 16 Bundesländer + NATIONAL (bundesweite Feiertage) |
35
+ | `feiertage_health` | Erreichbarkeitsprüfung (ok / blocked / error) — eine höfliche Anfrage |
36
+
37
+ Beispielausgabe (`feiertage_is_holiday` / 2026-10-03, NATIONAL):
38
+
39
+ ```json
40
+ {"date": "2026-10-03", "weekday": "Saturday", "weekday_de": "Samstag",
41
+ "state": "NATIONAL", "is_holiday": true,
42
+ "holidays": ["Tag der Deutschen Einheit"],
43
+ "details": [{"name": "Tag der Deutschen Einheit", "date": "2026-10-03",
44
+ "note": null, "note_only": false, "weekday": "Saturday",
45
+ "weekday_de": "Samstag"}]}
46
+ ```
47
+
48
+ Hinweise:
49
+ - Bundesland-Codes sind die 2-Buchstaben-Codes: BW, BY, BE, BB, HB, HH, HE, MV, NI, NW, RP, SL, SN, ST, SH, TH — oder `NATIONAL` für die bundesweiten Feiertage (Neujahrstag, Karfreitag, Ostermontag, Tag der Arbeit, Christi Himmelfahrt, Pfingstmontag, Tag der Deutschen Einheit, 1./2. Weihnachtstag). `feiertage_list_states` liefert die vollständige Liste.
50
+ - Feiertagsnamen sind deutsch; jeder datierte Feiertag trägt zusätzlich seinen Wochentag auf Englisch und Deutsch.
51
+ - Einige Einträge haben kein Datum (`note_only: true`) — das sind Hinweise, z. B. schulfreie Tage, keine gesetzlichen Feiertage. Datumsbasierte Tools überspringen sie.
52
+ - Jahre 1900–2100 werden akzeptiert; die API liefert historische und zukünftige Kalender.
53
+ - `feiertage_holidays_between` stellt eine Anfrage pro Jahr im Zeitraum (max. ca. 2 Jahre).
54
+
55
+ Built by [Mohammad Abbasi](https://github.com/mohammad-abbasi-sh) — [Almanibama](https://almanibama.ir) · [mabbasi.ir](https://mabbasi.ir)
@@ -0,0 +1,55 @@
1
+ # feiertage-mcp 📅
2
+
3
+ mcp-name: io.github.mohammad-abbasi-sh/feiertage-mcp
4
+
5
+ سرور MCP غیررسمی و **فقط‌خواندنی** برای **تعطیلات رسمی آلمان** (gesetzliche Feiertage) از طریق [API سایت feiertage-api.de](https://feiertage-api.de) (پروژهٔ bundesAPI): تعطیلات هر سال در هر ایالت فدرال، بررسی اینکه یک تاریخ تعطیل است یا نه، نزدیک‌ترین تعطیل از یک تاریخ، و تعطیلات در یک بازهٔ زمانی. بدون کلید و بدون ورود.
6
+
7
+ > ساختهٔ محمد عباسی در چارچوب «کارخانهٔ MCP آلمان». وابسته به feiertage-api.de / پروژهٔ bundesAPI نیست.
8
+
9
+ ## برای ایجنت‌های هوش مصنوعی
10
+
11
+ به پیکربندی MCP Client اضافه کنید (stdio):
12
+
13
+ ```json
14
+ {
15
+ "mcpServers": {
16
+ "feiertage": {
17
+ "command": "uvx",
18
+ "args": ["feiertage-mcp==0.1.0"]
19
+ }
20
+ }
21
+ }
22
+ ```
23
+
24
+ یا به‌صورت محلی اجرا کنید: `python -m feiertage_mcp.server` (نیازمند `mcp<2` و `httpx`).
25
+
26
+ ## ابزارها
27
+
28
+ | ابزار | توضیح |
29
+ |------|-------|
30
+ | `feiertage_list_holidays` | تعطیلات رسمی یک سال در یک ایالت فدرال (با فیلتر اختیاری ماه) |
31
+ | `feiertage_is_holiday` | آیا یک تاریخ (YYYY-MM-DD) در یک ایالت تعطیل رسمی است؟ |
32
+ | `feiertage_next_holiday` | نزدیک‌ترین تعطیل رسمی از یک تاریخ به بعد (به سال بعد هم نگاه می‌کند) |
33
+ | `feiertage_holidays_between` | تعطیلات رسمی در یک بازهٔ زمانی (حداکثر حدود ۲ سال) |
34
+ | `feiertage_list_states` | هر ۱۶ ایالت فدرال + NATIONAL (تعطیلات سراسری) |
35
+ | `feiertage_health` | بررسی دسترسی به API (ok / blocked / error) — یک درخواست مؤدبانه |
36
+
37
+ نمونهٔ خروجی (`feiertage_is_holiday` / ۲۰۲۶-۱۰-۰۳، NATIONAL):
38
+
39
+ ```json
40
+ {"date": "2026-10-03", "weekday": "Saturday", "weekday_de": "Samstag",
41
+ "state": "NATIONAL", "is_holiday": true,
42
+ "holidays": ["Tag der Deutschen Einheit"],
43
+ "details": [{"name": "Tag der Deutschen Einheit", "date": "2026-10-03",
44
+ "note": null, "note_only": false, "weekday": "Saturday",
45
+ "weekday_de": "Samstag"}]}
46
+ ```
47
+
48
+ نکته‌ها:
49
+ - کد ایالت‌ها دو حرفی است: BW، BY، BE، BB، HB، HH، HE، MV، NI، NW، RP، SL، SN، ST، SH، TH — یا `NATIONAL` برای تعطیلات سراسری (Neujahrstag، Karfreitag، Ostermontag، Tag der Arbeit، Christi Himmelfahrt، Pfingstmontag، Tag der Deutschen Einheit، ۱ و ۲ Weihnachtstag). فهرست کامل را از `feiertage_list_states` بگیرید.
50
+ - نام تعطیلات آلمانی است؛ هر تعطیلِ تاریخ‌دار روز هفته را هم به انگلیسی و هم به آلمانی دارد.
51
+ - چند ورودی تاریخ ندارند (`note_only: true`) — این‌ها فقط یادداشت‌اند، مثلاً روزهای تعطیل مدارس، نه تعطیل رسمی قانونی. ابزارهای مبتنی بر تاریخ آن‌ها را رد می‌کنند.
52
+ - سال‌های ۱۹۰۰ تا ۲۱۰۰ پذیرفته می‌شوند؛ API تقویم‌های گذشته و آینده را می‌دهد.
53
+ - `feiertage_holidays_between` برای هر سال در بازه یک درخواست می‌فرستد (حداکثر حدود ۲ سال).
54
+
55
+ Built by [Mohammad Abbasi](https://github.com/mohammad-abbasi-sh) — [Almanibama](https://almanibama.ir) · [mabbasi.ir](https://mabbasi.ir)
@@ -0,0 +1,55 @@
1
+ # feiertage-mcp 📅
2
+
3
+ mcp-name: io.github.mohammad-abbasi-sh/feiertage-mcp
4
+
5
+ Unofficial **read-only** MCP server for **German public holidays** (gesetzliche Feiertage) via the [feiertage-api.de API](https://feiertage-api.de) (bundesAPI project): holidays per year and federal state, is-it-a-holiday checks, the next holiday from a date, and holidays in a date range. No key, no login.
6
+
7
+ > Built by Mohammad Abbasi as part of the "Germany MCP factory". Not affiliated with feiertage-api.de / the bundesAPI project.
8
+
9
+ ## For AI agents
10
+
11
+ Add to your MCP client config (stdio):
12
+
13
+ ```json
14
+ {
15
+ "mcpServers": {
16
+ "feiertage": {
17
+ "command": "uvx",
18
+ "args": ["feiertage-mcp==0.1.0"]
19
+ }
20
+ }
21
+ }
22
+ ```
23
+
24
+ Or run locally: `python -m feiertage_mcp.server` (needs `mcp<2`, `httpx`).
25
+
26
+ ## Tools
27
+
28
+ | Tool | Description |
29
+ |------|-------------|
30
+ | `feiertage_list_holidays` | Public holidays of one year in one federal state (optional month filter) |
31
+ | `feiertage_is_holiday` | Is a date (YYYY-MM-DD) a public holiday in one state? |
32
+ | `feiertage_next_holiday` | The next public holiday on or after a date (looks into the next year) |
33
+ | `feiertage_holidays_between` | Public holidays in a date range (max ~2 years) |
34
+ | `feiertage_list_states` | All 16 federal states + NATIONAL (nationwide holidays) |
35
+ | `feiertage_health` | Reachability check (ok / blocked / error) — one polite request |
36
+
37
+ Example output (`feiertage_is_holiday` / 2026-10-03, NATIONAL):
38
+
39
+ ```json
40
+ {"date": "2026-10-03", "weekday": "Saturday", "weekday_de": "Samstag",
41
+ "state": "NATIONAL", "is_holiday": true,
42
+ "holidays": ["Tag der Deutschen Einheit"],
43
+ "details": [{"name": "Tag der Deutschen Einheit", "date": "2026-10-03",
44
+ "note": null, "note_only": false, "weekday": "Saturday",
45
+ "weekday_de": "Samstag"}]}
46
+ ```
47
+
48
+ Notes:
49
+ - State codes are the 2-letter codes: BW, BY, BE, BB, HB, HH, HE, MV, NI, NW, RP, SL, SN, ST, SH, TH — or `NATIONAL` for the nationwide holidays (Neujahrstag, Karfreitag, Ostermontag, Tag der Arbeit, Christi Himmelfahrt, Pfingstmontag, Tag der Deutschen Einheit, 1./2. Weihnachtstag). Use `feiertage_list_states` for the full list.
50
+ - Holiday names are German; each dated holiday also carries its weekday in English and German.
51
+ - A few entries have no date (`note_only: true`) — they are notes, e.g. school-free days, not statutory public holidays. Date-based tools skip them.
52
+ - Years 1900–2100 are accepted; the API serves historical and future calendars.
53
+ - `feiertage_holidays_between` fetches one request per year in the range (max ~2 years).
54
+
55
+ Built by [Mohammad Abbasi](https://github.com/mohammad-abbasi-sh) — [Almanibama](https://almanibama.ir) · [mabbasi.ir](https://mabbasi.ir)
@@ -0,0 +1,22 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "feiertage-mcp"
7
+ version = "0.1.0"
8
+ description = "Unofficial read-only MCP server for German public holidays (feiertage-api.de) — holidays by year & state, is-it-a-holiday, next holiday & date ranges"
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ requires-python = ">=3.10"
12
+ dependencies = [
13
+ "mcp<2",
14
+ "httpx>=0.27",
15
+ ]
16
+ authors = [{ name = "Mohammad Abbasi" }]
17
+
18
+ [project.scripts]
19
+ feiertage-mcp = "feiertage_mcp.server:main"
20
+
21
+ [tool.hatch.build.targets.wheel]
22
+ packages = ["src/feiertage_mcp"]
@@ -0,0 +1 @@
1
+ {"$schema":"https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json","description":"Unofficial read-only MCP server for German public holidays (feiertage-api.de) — by year & state, is-it-a-holiday, next holiday.","name":"io.github.mohammad-abbasi-sh/feiertage-mcp","packages":[{"identifier":"feiertage-mcp","registryBaseUrl":"https://pypi.org","registryType":"pypi","transport":{"type":"stdio"},"version":"0.1.0"}],"repository":{"source":"github","url":"https://github.com/mohammad-abbasi-sh/feiertage-mcp"},"title":"Feiertage — German public holidays by year & federal state","version":"0.1.0","websiteUrl":"https://github.com/mohammad-abbasi-sh/feiertage-mcp"}
@@ -0,0 +1,2 @@
1
+ """feiertage_mcp package — unofficial read-only MCP access to German public
2
+ holidays (feiertage-api.de)."""
@@ -0,0 +1,241 @@
1
+ """Domain logic: shape feiertage-api.de responses into compact dicts
2
+ (stdlib only).
3
+
4
+ Data source: the free feiertage-api.de API — German public holidays
5
+ (gesetzliche Feiertage) per year and federal state, no key, no login.
6
+ The API is a thin wrapper over the German holiday calendar (Wikipedia /
7
+ feiertage-api.de, by the bundesAPI project).
8
+
9
+ - GET /api/?jahr=YYYY — all 16 federal states + NATIONAL, each a dict
10
+ holiday name -> {"datum": "YYYY-MM-DD" or null, "hinweis": "..."}.
11
+ - GET /api/?jahr=YYYY&nur_land=XX — one state only (2-letter code, or
12
+ NATIONAL for the nationwide holidays). Case-insensitive; an unknown
13
+ code returns JSON `null` with HTTP 200.
14
+ - "datum" can be null: the entry is then a note only (e.g. Gründonnerstag
15
+ in Baden-Württemberg 1990 — pupils have the day off, but it is not a
16
+ statutory public holiday). Such entries are kept with date None and
17
+ flagged note_only; date-based tools skip them.
18
+ - "hinweis" is a free-text note (often empty); kept as "note".
19
+
20
+ Holiday names are German (Neujahrstag, Karfreitag, ...); this server
21
+ does not translate them — the weekday is given in English and German.
22
+ """
23
+ from __future__ import annotations
24
+
25
+ import json
26
+ import re
27
+ from datetime import date, timedelta
28
+ from urllib.parse import quote_plus
29
+
30
+ BASE = "https://feiertage-api.de"
31
+
32
+ # The 16 federal states (Bundesländer) + NATIONAL (bundesweite Feiertage).
33
+ # Codes are the ones the API accepts in nur_land.
34
+ STATES = {
35
+ "BW": "Baden-Württemberg",
36
+ "BY": "Bayern",
37
+ "BE": "Berlin",
38
+ "BB": "Brandenburg",
39
+ "HB": "Bremen",
40
+ "HH": "Hamburg",
41
+ "HE": "Hessen",
42
+ "MV": "Mecklenburg-Vorpommern",
43
+ "NI": "Niedersachsen",
44
+ "NW": "Nordrhein-Westfalen",
45
+ "RP": "Rheinland-Pfalz",
46
+ "SL": "Saarland",
47
+ "SN": "Sachsen",
48
+ "ST": "Sachsen-Anhalt",
49
+ "SH": "Schleswig-Holstein",
50
+ "TH": "Thüringen",
51
+ "NATIONAL": "Bundesweit (nationwide)",
52
+ }
53
+
54
+ STATE_CODES = sorted(STATES)
55
+ DATE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$")
56
+ YEAR_MIN, YEAR_MAX = 1900, 2100
57
+ MAX_RANGE_DAYS = 731 # holidays_between: at most ~2 years
58
+
59
+ WEEKDAYS_EN = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday",
60
+ "Saturday", "Sunday"]
61
+ WEEKDAYS_DE = ["Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag",
62
+ "Samstag", "Sonntag"]
63
+
64
+
65
+ def validate_year(year, default: int | None = None) -> int:
66
+ """A year the API serves (1900..2100); default = current year."""
67
+ if year is None:
68
+ if default is None:
69
+ default = date.today().year
70
+ year = default
71
+ try:
72
+ y = int(year)
73
+ except (TypeError, ValueError):
74
+ raise ValueError(f"year must be a number, got {year!r}") from None
75
+ if not YEAR_MIN <= y <= YEAR_MAX:
76
+ raise ValueError(
77
+ f"year out of range: {year} (expected {YEAR_MIN}..{YEAR_MAX})"
78
+ )
79
+ return y
80
+
81
+
82
+ def validate_state(state: str | None, default: str = "NATIONAL") -> str:
83
+ """A 2-letter state code (case-insensitive) or NATIONAL."""
84
+ if state is None:
85
+ return default
86
+ code = state.strip().upper()
87
+ if code not in STATES:
88
+ raise ValueError(
89
+ f"unknown state {state!r} (expected one of: "
90
+ + ", ".join(STATE_CODES) + ")"
91
+ )
92
+ return code
93
+
94
+
95
+ def validate_date(value: str) -> date:
96
+ """A YYYY-MM-DD date."""
97
+ text = (value or "").strip()
98
+ if not DATE_RE.fullmatch(text):
99
+ raise ValueError(
100
+ f"bad date {value!r} (expected YYYY-MM-DD, e.g. 2026-12-25)"
101
+ )
102
+ try:
103
+ return date.fromisoformat(text)
104
+ except ValueError:
105
+ raise ValueError(f"bad date {value!r} (not a real calendar date)") from None
106
+
107
+
108
+ def validate_month(month) -> int | None:
109
+ """A month 1..12, or None (no month filter)."""
110
+ if month is None:
111
+ return None
112
+ try:
113
+ m = int(month)
114
+ except (TypeError, ValueError):
115
+ raise ValueError(f"month must be a number, got {month!r}") from None
116
+ if not 1 <= m <= 12:
117
+ raise ValueError(f"month out of range: {month} (expected 1..12)")
118
+ return m
119
+
120
+
121
+ def holidays_url(year: int, state: str | None = None) -> str:
122
+ """API URL for one year; state=None fetches all states at once."""
123
+ y = validate_year(year)
124
+ url = f"{BASE}/api/?jahr={y}"
125
+ if state is not None:
126
+ url += f"&nur_land={quote_plus(validate_state(state))}"
127
+ return url
128
+
129
+
130
+ def _holiday_row(name: str, entry: dict) -> dict:
131
+ datum = entry.get("datum")
132
+ note = (entry.get("hinweis") or "").strip()
133
+ row = {"name": name, "date": datum, "note": note or None,
134
+ "note_only": datum is None}
135
+ if datum:
136
+ d = date.fromisoformat(datum)
137
+ row["weekday"] = WEEKDAYS_EN[d.weekday()]
138
+ row["weekday_de"] = WEEKDAYS_DE[d.weekday()]
139
+ return row
140
+
141
+
142
+ def parse_holidays(text: str) -> list:
143
+ """One state's holiday dict -> sorted list of holiday rows.
144
+
145
+ Raises ValueError on JSON null (unknown state code) or bad shapes.
146
+ Note-only entries (datum null) are kept, flagged note_only, and
147
+ sort after dated entries.
148
+ """
149
+ try:
150
+ payload = json.loads(text)
151
+ except json.JSONDecodeError as e:
152
+ raise ValueError(f"invalid holidays response: {e}") from e
153
+ if payload is None:
154
+ raise ValueError(
155
+ "unknown state code (the API returned null) — use one of: "
156
+ + ", ".join(STATE_CODES)
157
+ )
158
+ if not isinstance(payload, dict):
159
+ raise ValueError("unexpected holidays shape: not a JSON object")
160
+ rows = []
161
+ for name, entry in payload.items():
162
+ if not isinstance(entry, dict) or "datum" not in entry:
163
+ raise ValueError(f"unexpected holiday entry for {name!r}")
164
+ rows.append(_holiday_row(name, entry))
165
+ rows.sort(key=lambda r: (r["date"] is None, r["date"] or "", r["name"]))
166
+ return rows
167
+
168
+
169
+ def parse_all_states(text: str) -> dict:
170
+ """Full-year response -> {state_code: [holiday rows]}."""
171
+ try:
172
+ payload = json.loads(text)
173
+ except json.JSONDecodeError as e:
174
+ raise ValueError(f"invalid holidays response: {e}") from e
175
+ if not isinstance(payload, dict):
176
+ raise ValueError("unexpected holidays shape: not a JSON object")
177
+ out = {}
178
+ for code, holidays in payload.items():
179
+ if not isinstance(holidays, dict):
180
+ raise ValueError(f"unexpected holidays shape for state {code!r}")
181
+ out[code] = [
182
+ _holiday_row(name, entry)
183
+ for name, entry in holidays.items()
184
+ ]
185
+ out[code].sort(key=lambda r: (r["date"] is None, r["date"] or "",
186
+ r["name"]))
187
+ return out
188
+
189
+
190
+ def list_states() -> list:
191
+ """All state codes with their German names (static data)."""
192
+ return [{"code": code, "name": STATES[code]} for code in STATE_CODES]
193
+
194
+
195
+ def holidays_in_month(rows: list, month: int | None) -> list:
196
+ """Filter dated holiday rows to one month (None = no filter)."""
197
+ if month is None:
198
+ return rows
199
+ return [r for r in rows
200
+ if r["date"] is not None and int(r["date"][5:7]) == month]
201
+
202
+
203
+ def is_holiday(rows: list, day: date) -> list:
204
+ """Holiday rows falling on one date (dated entries only)."""
205
+ target = day.isoformat()
206
+ return [r for r in rows if r["date"] == target]
207
+
208
+
209
+ def next_holiday(rows: list, start: date) -> dict | None:
210
+ """First dated holiday on or after `start`, or None."""
211
+ target = start.isoformat()
212
+ dated = [r for r in rows if r["date"] is not None]
213
+ dated.sort(key=lambda r: r["date"])
214
+ for row in dated:
215
+ if row["date"] >= target:
216
+ return row
217
+ return None
218
+
219
+
220
+ def holidays_between(rows_by_year: dict[int, list], start: date,
221
+ end: date) -> list:
222
+ """Merge per-year holiday rows into one dated, sorted list in range."""
223
+ if end < start:
224
+ raise ValueError(
225
+ f"end date {end} is before start date {start}"
226
+ )
227
+ if (end - start).days > MAX_RANGE_DAYS:
228
+ raise ValueError(
229
+ f"date range too wide: {(end - start).days} days "
230
+ f"(max {MAX_RANGE_DAYS})"
231
+ )
232
+ out = []
233
+ for year, rows in rows_by_year.items():
234
+ for row in rows:
235
+ if row["date"] is None:
236
+ continue
237
+ d = date.fromisoformat(row["date"])
238
+ if start <= d <= end:
239
+ out.append(row)
240
+ out.sort(key=lambda r: r["date"])
241
+ return out