holiplan 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.
- holiplan-0.1.0/.github/workflows/publish.yml +99 -0
- holiplan-0.1.0/.github/workflows/tests.yml +29 -0
- holiplan-0.1.0/.gitignore +6 -0
- holiplan-0.1.0/CLAUDE.md +150 -0
- holiplan-0.1.0/LICENSE +21 -0
- holiplan-0.1.0/PKG-INFO +152 -0
- holiplan-0.1.0/PRIVACY.md +55 -0
- holiplan-0.1.0/README.md +129 -0
- holiplan-0.1.0/docs/examples.md +134 -0
- holiplan-0.1.0/examples/ledger.example.json +95 -0
- holiplan-0.1.0/pyproject.toml +38 -0
- holiplan-0.1.0/src/holiplan/__init__.py +0 -0
- holiplan-0.1.0/src/holiplan/engine/__init__.py +0 -0
- holiplan-0.1.0/src/holiplan/engine/bridges.py +116 -0
- holiplan-0.1.0/src/holiplan/engine/checks.py +231 -0
- holiplan-0.1.0/src/holiplan/engine/ics.py +91 -0
- holiplan-0.1.0/src/holiplan/engine/leave.py +174 -0
- holiplan-0.1.0/src/holiplan/engine/models.py +250 -0
- holiplan-0.1.0/src/holiplan/engine/summary.py +115 -0
- holiplan-0.1.0/src/holiplan/holidays.py +244 -0
- holiplan-0.1.0/src/holiplan/schemas/ledger.schema.json +120 -0
- holiplan-0.1.0/src/holiplan/server.py +515 -0
- holiplan-0.1.0/tests/fixture_server.py +37 -0
- holiplan-0.1.0/tests/fixtures/at-wi-2027.json +598 -0
- holiplan-0.1.0/tests/fixtures/ch-zh-2027.json +2367 -0
- holiplan-0.1.0/tests/test_contract.py +172 -0
- holiplan-0.1.0/tests/test_engine.py +540 -0
- holiplan-0.1.0/tests/test_golden_dach.py +256 -0
- holiplan-0.1.0/tests/test_holidays.py +253 -0
- holiplan-0.1.0/tests/test_models.py +103 -0
- holiplan-0.1.0/tests/test_protocol.py +172 -0
- holiplan-0.1.0/tests/test_server.py +227 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
name: publish
|
|
2
|
+
|
|
3
|
+
# Run it by hand for a dry run: build, upload to TestPyPI and check the package
|
|
4
|
+
# installs and starts. Push a tag like v0.1.0 to do the same and then publish
|
|
5
|
+
# to PyPI. Uploads use trusted publishing (OIDC), so there are no tokens.
|
|
6
|
+
on:
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
push:
|
|
9
|
+
tags: ["v*"]
|
|
10
|
+
|
|
11
|
+
permissions:
|
|
12
|
+
contents: read
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
build:
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
outputs:
|
|
18
|
+
version: ${{ steps.version.outputs.version }}
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@v4
|
|
21
|
+
- uses: actions/setup-python@v5
|
|
22
|
+
with:
|
|
23
|
+
python-version: "3.13"
|
|
24
|
+
- name: Read the version
|
|
25
|
+
id: version
|
|
26
|
+
run: |
|
|
27
|
+
version=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project']['version'])")
|
|
28
|
+
echo "version=$version" >> "$GITHUB_OUTPUT"
|
|
29
|
+
- name: Check the tag matches the version
|
|
30
|
+
if: startsWith(github.ref, 'refs/tags/')
|
|
31
|
+
run: |
|
|
32
|
+
if [ "${GITHUB_REF_NAME}" != "v${{ steps.version.outputs.version }}" ]; then
|
|
33
|
+
echo "Tag ${GITHUB_REF_NAME} does not match pyproject version ${{ steps.version.outputs.version }}"
|
|
34
|
+
exit 1
|
|
35
|
+
fi
|
|
36
|
+
- name: Build
|
|
37
|
+
run: |
|
|
38
|
+
python -m pip install build twine
|
|
39
|
+
python -m build
|
|
40
|
+
python -m twine check --strict dist/*
|
|
41
|
+
- uses: actions/upload-artifact@v4
|
|
42
|
+
with:
|
|
43
|
+
name: dist
|
|
44
|
+
path: dist/
|
|
45
|
+
|
|
46
|
+
testpypi:
|
|
47
|
+
needs: build
|
|
48
|
+
runs-on: ubuntu-latest
|
|
49
|
+
environment: testpypi
|
|
50
|
+
permissions:
|
|
51
|
+
id-token: write
|
|
52
|
+
steps:
|
|
53
|
+
- uses: actions/download-artifact@v4
|
|
54
|
+
with:
|
|
55
|
+
name: dist
|
|
56
|
+
path: dist/
|
|
57
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
58
|
+
with:
|
|
59
|
+
repository-url: https://test.pypi.org/legacy/
|
|
60
|
+
# A dry run and the tagged release upload the same version.
|
|
61
|
+
skip-existing: true
|
|
62
|
+
|
|
63
|
+
check-testpypi:
|
|
64
|
+
needs: [build, testpypi]
|
|
65
|
+
runs-on: ubuntu-latest
|
|
66
|
+
steps:
|
|
67
|
+
- uses: actions/setup-python@v5
|
|
68
|
+
with:
|
|
69
|
+
python-version: "3.13"
|
|
70
|
+
- name: Install from TestPyPI and start the server
|
|
71
|
+
run: |
|
|
72
|
+
# Dependencies come from PyPI; only holiplan comes from TestPyPI.
|
|
73
|
+
for attempt in 1 2 3 4 5 6; do
|
|
74
|
+
pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ \
|
|
75
|
+
"holiplan==${{ needs.build.outputs.version }}" && break
|
|
76
|
+
echo "Not on the index yet; retrying"; sleep 20
|
|
77
|
+
done
|
|
78
|
+
python - <<'EOF'
|
|
79
|
+
import asyncio
|
|
80
|
+
from holiplan import server
|
|
81
|
+
tools = asyncio.run(server.mcp.list_tools())
|
|
82
|
+
schema = server.profile_schema()
|
|
83
|
+
assert len(tools) == 9, [t.name for t in tools]
|
|
84
|
+
print(f"Installed holiplan serves {len(tools)} tools; profile schema {len(schema)} chars")
|
|
85
|
+
EOF
|
|
86
|
+
|
|
87
|
+
pypi:
|
|
88
|
+
if: startsWith(github.ref, 'refs/tags/v')
|
|
89
|
+
needs: check-testpypi
|
|
90
|
+
runs-on: ubuntu-latest
|
|
91
|
+
environment: pypi
|
|
92
|
+
permissions:
|
|
93
|
+
id-token: write
|
|
94
|
+
steps:
|
|
95
|
+
- uses: actions/download-artifact@v4
|
|
96
|
+
with:
|
|
97
|
+
name: dist
|
|
98
|
+
path: dist/
|
|
99
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
name: tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
test:
|
|
13
|
+
name: Python ${{ matrix.python }} on ${{ matrix.os }}
|
|
14
|
+
runs-on: ${{ matrix.os }}
|
|
15
|
+
strategy:
|
|
16
|
+
fail-fast: false
|
|
17
|
+
matrix:
|
|
18
|
+
os: [ubuntu-latest, windows-latest]
|
|
19
|
+
python: ["3.11", "3.12", "3.13", "3.14"]
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v4
|
|
22
|
+
- uses: actions/setup-python@v5
|
|
23
|
+
with:
|
|
24
|
+
python-version: ${{ matrix.python }}
|
|
25
|
+
- name: Install
|
|
26
|
+
run: python -m pip install -e ".[dev]"
|
|
27
|
+
# Offline: OpenHolidays is stubbed or replayed from tests/fixtures throughout.
|
|
28
|
+
- name: Test
|
|
29
|
+
run: python -m unittest discover -s tests -v
|
holiplan-0.1.0/CLAUDE.md
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## What this is
|
|
6
|
+
|
|
7
|
+
`holiplan` is a stateless MCP server that helps an assistant plan a family's year around school
|
|
8
|
+
holidays and a limited leave allowance. The server does the arithmetic (leave cost, budget,
|
|
9
|
+
carryover expiry, bridge days, overlaps, deadlines, .ics export); the assistant does the judgement
|
|
10
|
+
(destinations). Holiday data comes from the OpenHolidays API (CC BY 4.0, no key). Attribution is
|
|
11
|
+
required wherever that data is shown, which is why tool results carry a `source` field.
|
|
12
|
+
|
|
13
|
+
This is Release 1 of a two-release product (MCP server, then a web app on the same engine). The
|
|
14
|
+
ledger schema is the contract between them.
|
|
15
|
+
|
|
16
|
+
## Commands
|
|
17
|
+
|
|
18
|
+
A venv already exists at `.venv` (Windows layout: `.venv/Scripts/python`).
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install -e ".[dev]" # install with pytest
|
|
22
|
+
python -m unittest discover -s tests # full suite, offline
|
|
23
|
+
python -m unittest tests.test_engine.EffectiveWindowTests # one class
|
|
24
|
+
python -m unittest tests.test_engine.BudgetTests.<test_name> # one test
|
|
25
|
+
python -m holiplan.server # run the server over stdio (or the `holiplan` script)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Tests prepend `src/` to `sys.path`, so the engine tests run without installing the package. Tests
|
|
29
|
+
that import the server need `mcp` installed and skip themselves otherwise; schema validation tests
|
|
30
|
+
need `jsonschema` (a dependency of `mcp`). `pytest` also works. There is no linter or formatter
|
|
31
|
+
configured.
|
|
32
|
+
|
|
33
|
+
## Architecture
|
|
34
|
+
|
|
35
|
+
Two layers with a hard boundary:
|
|
36
|
+
|
|
37
|
+
- **`src/holiplan/engine/`**: pure, offline, deterministic. `models.py` (dependency-free dataclasses,
|
|
38
|
+
`migrate`, and a `_build` helper every `from_dict` goes through); `leave.py` (`leave_cost`,
|
|
39
|
+
`year_budget`); `checks.py` (`validate_ledger`, `deadlines`, `effective_window`,
|
|
40
|
+
`describe_window`); `bridges.py` (`bridge_days`); `summary.py` (`summarise`); `ics.py` (RFC 5545
|
|
41
|
+
export). Public holidays are always **passed in**: a `set[str]` of full-day ISO dates, plus an
|
|
42
|
+
optional `half_holidays` set. The engine never does I/O.
|
|
43
|
+
- **`src/holiplan/holidays.py`**: the OpenHolidays client, with an in-process one-week cache. On a
|
|
44
|
+
failed fetch it serves the expired cache entry if there is one, otherwise raises
|
|
45
|
+
`HolidayDataUnavailable` with a message an assistant can relay; a 4xx becomes a `ValueError`
|
|
46
|
+
about the input. It also owns the data-shaped logic: `resolve_region`, `is_local`, `covers`,
|
|
47
|
+
`locate`, `is_half_day`, and `holiday_dates` (which filters by all of them).
|
|
48
|
+
- **`src/holiplan/server.py`**: the MCP surface. Each tool resolves its region, fetches what it
|
|
49
|
+
needs, parses the ledger into models, calls the engine and returns plain dicts. Shared helpers:
|
|
50
|
+
`_holiday_set` returns `(full, half)` holiday sets filtered by the profile's local-holiday
|
|
51
|
+
settings; `_padded` widens a year range by a month either side, because windows stretch over
|
|
52
|
+
holidays just past them (Christmas into January); `_school_windows` merges the per-school-type
|
|
53
|
+
repeats the data contains; `_described_windows` adds effective dates and sizes; `_validate`
|
|
54
|
+
backs `check_ledger` and `upsert_trip`, and falls back to date-only checks without a profile.
|
|
55
|
+
|
|
56
|
+
The server is built on **MCP SDK v2** (`mcp>=2,<3`): `from mcp.server.mcpserver import MCPServer`.
|
|
57
|
+
Do not use the v1 `FastMCP` import. Tools are registered with `@_tool`, not `@mcp.tool()`: SDK v2
|
|
58
|
+
shows the model an exception's text only for `ToolError`, and reports anything else as a bare
|
|
59
|
+
"Error executing tool X". `_tool` re-raises `ValueError` and `HolidayDataUnavailable` as
|
|
60
|
+
`ToolError`, so **errors meant for the user must be one of those two**; any other exception is
|
|
61
|
+
treated as a crash and its message stays on the server. Tool functions stay plain callables, so
|
|
62
|
+
tests call them directly (and see `ToolError`).
|
|
63
|
+
|
|
64
|
+
### The ledger contract
|
|
65
|
+
|
|
66
|
+
The ledger is a JSON document owned by the user. The server stores nothing: every tool takes the
|
|
67
|
+
ledger as input, and mutating tools (`upsert_trip`) return a new ledger for the assistant to hand
|
|
68
|
+
back. The server instructions and every prompt tell the assistant to pass the ledger everywhere
|
|
69
|
+
(including the optional `ledger` of `calculate_leave_cost` and `find_bridge_days`) and hand back
|
|
70
|
+
the whole ledger after changes.
|
|
71
|
+
|
|
72
|
+
The shape is defined twice: `engine/models.py` and `src/holiplan/schemas/ledger.schema.json`
|
|
73
|
+
(profile and trip in `$defs`, also served on their own as `holiplan://schema/profile` and
|
|
74
|
+
`/trip`). `tests/test_contract.py` fails if their fields or choices drift apart, and validates
|
|
75
|
+
round-tripped, starter and example ledgers against the schema. `to_dict` writes `null` for unset
|
|
76
|
+
optional fields, so the schema's optional fields are nullable. Both refuse unknown fields.
|
|
77
|
+
|
|
78
|
+
**Changing the ledger shape:** a new optional field with a default needs no migration. Anything
|
|
79
|
+
that would make an older ledger read differently needs `SCHEMA_VERSION` bumped and a
|
|
80
|
+
`MIGRATIONS[old_version]` entry in `models.py`; `Ledger.from_dict` runs `migrate` first.
|
|
81
|
+
`examples/ledger.example.json` is the sample; `my-ledger.json` is gitignored.
|
|
82
|
+
|
|
83
|
+
### OpenHolidays quirks the code relies on
|
|
84
|
+
|
|
85
|
+
- **Region codes** are OpenHolidays' own where they differ from ISO 3166-2 (Vienna `AT-WI`, not
|
|
86
|
+
`AT-9`), and an unknown code silently returns only nationwide data. `resolve_region` accepts
|
|
87
|
+
either form and raises on unknown ones. It reads `/Subdivisions`, so server tests patch
|
|
88
|
+
`holiplan.holidays.subdivisions` (see `StubbedHolidays` in `tests/test_server.py`).
|
|
89
|
+
- **`nationwide` is not "applies to the whole region"**: Epiphany is `nationwide: false` yet covers
|
|
90
|
+
all of DE-BY. `is_local` checks whether the `subdivisions` include the region or a parent.
|
|
91
|
+
- **`temporalScope: "HalfDay"`** marks half-day holidays (Zürich's Sechseläuten). They are kept out
|
|
92
|
+
of the full-day set and cost at most half a day of leave; they don't stretch windows.
|
|
93
|
+
- **School holidays repeat per school-type group** where types differ (Zürich: `CH-ZH-VS` primary,
|
|
94
|
+
`-MS`, `-BS`). Identical repeats are merged and their `groups` listed; no groups means every
|
|
95
|
+
school. There is no per-child school type in the profile yet.
|
|
96
|
+
- **Single-day school closures** exist as short school-holiday entries (All Souls' Day in Austria).
|
|
97
|
+
`describe_window` marks anything with `CLOSURE_MAX_SCHOOL_DAYS` (2) or fewer school days as a
|
|
98
|
+
closure; `get_holiday_windows` lists those separately. Checks treat closures like any window.
|
|
99
|
+
|
|
100
|
+
### Domain rules that span files
|
|
101
|
+
|
|
102
|
+
- **Intent decides leave.** `Trip.consumes_leave` is true for intents `trip`/`obligation`, false for
|
|
103
|
+
`home`/`camp`, and `takes_leave` overrides it. Budgeting skips non-consuming and `cancelled` trips.
|
|
104
|
+
A `home` week never gets an "unbooked" deadline.
|
|
105
|
+
- **Leave cost**: weekends (per `DayPattern.workdays`, so part-time patterns work) and public
|
|
106
|
+
holidays on workdays are free. Personal `half_days` (`"MM-DD"`) and half-day public holidays
|
|
107
|
+
cost 0.5.
|
|
108
|
+
- **Local holidays**: profile `local_holidays` defaults to `auto`: `_home_locality` resolves `home`
|
|
109
|
+
against the subdivision tree (`locate`, names in any language, strictly below the region) and
|
|
110
|
+
keeps only local holidays that `covers` that town. A home the tree doesn't know (Munich) keeps
|
|
111
|
+
none; no home or bare coordinates keeps all. `include`/`exclude` override, and
|
|
112
|
+
`ignore_holidays` (`"MM-DD"`, for region-wide entries like Assumption Day that are really local)
|
|
113
|
+
applies on top.
|
|
114
|
+
- **Budgets are per calendar year.** `year_budget` clips each trip to the year, so a trip over New
|
|
115
|
+
Year costs each year only its own days. A trip that takes leave in a year with no `LeaveAccount`
|
|
116
|
+
gets a `no_leave_account` warning.
|
|
117
|
+
- **Carryover** is spent first, but only by trips ending on or before `carryover_expires`. Anything
|
|
118
|
+
left becomes `carryover_at_risk`. `validate_ledger` turns budget warnings into issues by string
|
|
119
|
+
prefix (`"Over budget"` means error `over_budget`, anything else is warning `carryover_at_risk`),
|
|
120
|
+
so changing warning text in `year_budget` changes issue classification.
|
|
121
|
+
- **`effective_window`** stretches an official school window outwards over adjacent weekends and
|
|
122
|
+
public holidays (Whit Monday before Bavaria's Pentecost window), capped at `_MAX_STRETCH` days.
|
|
123
|
+
It drives the `outside_school_window` warning, which applies only to intent `trip`.
|
|
124
|
+
- **Bridge days** are gaps of workdays between free runs; an option is one gap or several
|
|
125
|
+
neighbouring ones within `max_leave`, and must contain a public holiday on a workday. Options
|
|
126
|
+
overlap by design, ranked by days off per leave day.
|
|
127
|
+
- **Privacy**: children are identified by `alias`, birth year and interests only, never names.
|
|
128
|
+
- The server never books anything. Bookings (`todo`/`booked`, `book_by`, `cancel_by`) only drive
|
|
129
|
+
`list_deadlines` and the VTODOs in the .ics export. VTODO UIDs are hashes of the deadline's
|
|
130
|
+
content, so re-importing a calendar updates entries rather than duplicating them. Lines are folded
|
|
131
|
+
at 75 UTF-8 octets.
|
|
132
|
+
|
|
133
|
+
### Tests
|
|
134
|
+
|
|
135
|
+
Expected figures in the golden tests are worked out by hand from the calendar, with the reasoning
|
|
136
|
+
in a comment. A failure means arithmetic a family relies on has broken, so fix the code, not the
|
|
137
|
+
expectation, unless the calendar reasoning itself was wrong.
|
|
138
|
+
|
|
139
|
+
- `test_engine.py`: the engine over real Bavaria 2027 public holidays and a realistic ledger.
|
|
140
|
+
- `test_holidays.py`: region resolution, local and half-day filtering, and the HTTP layer via
|
|
141
|
+
httpx's `MockTransport` (caching, outages, stale cache).
|
|
142
|
+
- `test_server.py`: the tools, with the three OpenHolidays calls patched (`StubbedHolidays`).
|
|
143
|
+
- `test_golden_dach.py`: Vienna and Zürich plans against real OpenHolidays responses recorded in
|
|
144
|
+
`tests/fixtures/` and replayed offline. If you re-record them, re-check every expectation.
|
|
145
|
+
- `test_models.py`: migrations and parse errors. `test_contract.py`: schema versus models,
|
|
146
|
+
resources, prompts and the tool list.
|
|
147
|
+
- `test_protocol.py`: the server as a subprocess over real MCP stdio (handshake, a scenario across
|
|
148
|
+
every tool, errors, resources, prompts), launched through `tests/fixture_server.py`, which swaps
|
|
149
|
+
OpenHolidays for a recorded fixture. About 25 seconds; it's the only slow file. Run the rest
|
|
150
|
+
with `python -m unittest tests.test_engine tests.test_server ...` when iterating.
|
holiplan-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 adekola
|
|
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.
|
holiplan-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: holiplan
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP server for planning a family's year around school holidays and a limited leave allowance
|
|
5
|
+
Project-URL: Homepage, https://github.com/adekola/holiplan
|
|
6
|
+
Project-URL: Issues, https://github.com/adekola/holiplan/issues
|
|
7
|
+
Project-URL: Privacy, https://github.com/adekola/holiplan/blob/main/PRIVACY.md
|
|
8
|
+
Author: adekola
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: annual leave,bridge days,family,mcp,model-context-protocol,school holidays
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Topic :: Office/Business :: Scheduling
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Requires-Dist: httpx>=0.27
|
|
19
|
+
Requires-Dist: mcp<3,>=2
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# holiplan
|
|
25
|
+
|
|
26
|
+
An MCP server for planning a family's year around school holidays and a limited leave allowance.
|
|
27
|
+
|
|
28
|
+
<!-- mcp-name: io.github.adekola/holiplan -->
|
|
29
|
+
|
|
30
|
+
Assistants are good at suggesting destinations and bad at remembering that Whit Monday is a
|
|
31
|
+
public holiday, that five carried-over days expire in June, and that you already said no
|
|
32
|
+
drive over three and a half hours. This server does the parts that should be calculated;
|
|
33
|
+
the assistant does the parts that need judgement.
|
|
34
|
+
|
|
35
|
+
Holiday data comes from [OpenHolidays](https://openholidaysapi.org) (CC BY 4.0), which covers
|
|
36
|
+
public and school holidays for a growing list of countries, with no API key.
|
|
37
|
+
|
|
38
|
+
## Status
|
|
39
|
+
|
|
40
|
+
Pre-release (0.1). Stateless: your ledger is a JSON file you keep, and the server stores nothing.
|
|
41
|
+
See [PRIVACY.md](https://github.com/adekola/holiplan/blob/main/PRIVACY.md) for exactly what goes where, and
|
|
42
|
+
[docs/examples.md](https://github.com/adekola/holiplan/blob/main/docs/examples.md) for what a conversation looks like.
|
|
43
|
+
|
|
44
|
+
## Use it in Claude Desktop
|
|
45
|
+
|
|
46
|
+
Install [uv](https://docs.astral.sh/uv/getting-started/installation/), then add holiplan to
|
|
47
|
+
`claude_desktop_config.json` and restart Claude:
|
|
48
|
+
|
|
49
|
+
```json
|
|
50
|
+
{
|
|
51
|
+
"mcpServers": {
|
|
52
|
+
"holiplan": {
|
|
53
|
+
"command": "uvx",
|
|
54
|
+
"args": ["holiplan"]
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`uvx` fetches holiplan from PyPI and runs it; there is nothing else to install. Without uv,
|
|
61
|
+
`pip install holiplan` and use `"command": "holiplan"` with no `args`.
|
|
62
|
+
|
|
63
|
+
Start with the **Set up my family profile** prompt, which builds a ledger with you one
|
|
64
|
+
question at a time, or copy [`examples/ledger.example.json`](https://github.com/adekola/holiplan/blob/main/examples/ledger.example.json) to `my-ledger.json` and edit it.
|
|
65
|
+
Tell Claude where the file is, and save the updated ledger whenever Claude hands it back.
|
|
66
|
+
A conversation then looks like:
|
|
67
|
+
|
|
68
|
+
> Here's my ledger. What do the 2027 school holidays look like, and what can I afford?
|
|
69
|
+
|
|
70
|
+
> Add two weeks on the Danish coast from 17 July.
|
|
71
|
+
|
|
72
|
+
> What needs booking next?
|
|
73
|
+
|
|
74
|
+
[docs/examples.md](https://github.com/adekola/holiplan/blob/main/docs/examples.md) has these conversations in full, with real figures.
|
|
75
|
+
|
|
76
|
+
## Tools
|
|
77
|
+
|
|
78
|
+
| Tool | Purpose |
|
|
79
|
+
| --- | --- |
|
|
80
|
+
| `get_holiday_windows` | School holidays (official and effective dates), single-day closures, and public holidays marked local, half-day and observed |
|
|
81
|
+
| `calculate_leave_cost` | Leave days a given trip needs, with the holidays inside it listed |
|
|
82
|
+
| `find_bridge_days` | The leave days that buy the most time off, and whether the kids are off too |
|
|
83
|
+
| `get_year_budget` | Planned versus available leave, including carryover expiry |
|
|
84
|
+
| `upsert_trip` | Add or update a trip, then re-check the ledger |
|
|
85
|
+
| `check_ledger` | Overlaps, budget breaches, bad dates, trips outside school holidays |
|
|
86
|
+
| `list_deadlines` | Booking deadlines and cancel-by dates, soonest first |
|
|
87
|
+
| `summarise_plan` | The whole plan at a glance, for a brief to your partner |
|
|
88
|
+
| `export_ics` | The plan as a calendar file |
|
|
89
|
+
|
|
90
|
+
Resources: JSON schemas for the ledger, profile and trip (`holiplan://schema/...`), and a blank
|
|
91
|
+
starter ledger (`holiplan://ledger/starter`).
|
|
92
|
+
|
|
93
|
+
Prompts: *Set up my family profile*, *Plan my year*, *Plan this holiday window*,
|
|
94
|
+
*What needs doing next?*, *Make a brief for my partner*.
|
|
95
|
+
|
|
96
|
+
## Design notes
|
|
97
|
+
|
|
98
|
+
- **The ledger is the source of truth.** Tools take it in and hand it back, so the plan
|
|
99
|
+
doesn't drift between conversations.
|
|
100
|
+
- **Intent decides leave.** A week with intent `home` or `camp` occupies a school holiday
|
|
101
|
+
window without costing leave. Override with `takes_leave`.
|
|
102
|
+
- **Carryover is spent first**, but only by trips ending before the expiry date, so
|
|
103
|
+
"these days expire in June" shows up as a warning while you can still act on it.
|
|
104
|
+
- **A trip over New Year** costs each year only its own days.
|
|
105
|
+
- **Local holidays follow your home town.** Some holidays cover only part of a region, like
|
|
106
|
+
Augsburg's Peace Festival in Bavaria. With the profile's `home` set to a town, only that
|
|
107
|
+
town's local holidays count as days off; without a home they all do. Set `local_holidays`
|
|
108
|
+
to `"include"` or `"exclude"` to override. `ignore_holidays` (`"MM-DD"`) covers what the
|
|
109
|
+
data can't tell apart, such as Assumption Day (`"08-15"`), which is only a holiday in
|
|
110
|
+
Catholic-majority Bavarian towns.
|
|
111
|
+
- **School windows include the free days around them.** The official Monday-to-Friday dates
|
|
112
|
+
are stretched over adjacent weekends and public holidays before trips are checked against them.
|
|
113
|
+
School holidays of two school days or fewer, like All Souls' Day in Austria, are listed
|
|
114
|
+
separately as closures.
|
|
115
|
+
- **Half-day holidays cost half a day.** Zürich's Sechseläuten afternoon is a half day off,
|
|
116
|
+
not a full one.
|
|
117
|
+
- **Regions accept ISO codes.** OpenHolidays uses its own codes where they differ (Vienna is
|
|
118
|
+
`AT-WI`, not `AT-9`); either works, and an unknown region is an error rather than a quietly
|
|
119
|
+
incomplete year.
|
|
120
|
+
- **Ledgers are versioned.** Older ledgers are migrated when read, a newer one is refused, and
|
|
121
|
+
a malformed one gets an error naming the trip and field at fault.
|
|
122
|
+
- **When OpenHolidays is down**, the server serves the last copy it fetched, or says plainly
|
|
123
|
+
that it can't check dates right now. It never guesses.
|
|
124
|
+
- **No real names.** Children are identified by alias and interests only.
|
|
125
|
+
- **The server never books anything.** It knows what needs booking and by when; you book it
|
|
126
|
+
and record the result.
|
|
127
|
+
|
|
128
|
+
## Roadmap
|
|
129
|
+
|
|
130
|
+
- [ ] Drive-time filtering (OpenRouteService or self-hosted OSRM)
|
|
131
|
+
- [ ] Remote deployment over Streamable HTTP
|
|
132
|
+
- [ ] Publish to the MCP registry
|
|
133
|
+
- [ ] School holiday windows by school type, where the data supports it (Zürich splits
|
|
134
|
+
primary, secondary and vocational; windows already list the types they apply to)
|
|
135
|
+
|
|
136
|
+
## Development
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
git clone https://github.com/adekola/holiplan.git
|
|
140
|
+
cd holiplan
|
|
141
|
+
python -m venv .venv && source .venv/bin/activate # Windows: .venv\Scripts\activate
|
|
142
|
+
pip install -e ".[dev]"
|
|
143
|
+
python -m unittest discover -s tests # offline
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
To run your working copy in Claude Desktop, point `command` at the venv's Python
|
|
147
|
+
(`.venv/bin/python`, or `.venv\Scripts\python.exe` on Windows) with
|
|
148
|
+
`"args": ["-m", "holiplan.server"]`.
|
|
149
|
+
|
|
150
|
+
## Licence
|
|
151
|
+
|
|
152
|
+
MIT, see [LICENSE](https://github.com/adekola/holiplan/blob/main/LICENSE). Holiday data: [OpenHolidays](https://openholidaysapi.org), CC BY 4.0.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Privacy
|
|
2
|
+
|
|
3
|
+
holiplan is built to know as little about your family as it can and to keep nothing.
|
|
4
|
+
|
|
5
|
+
This note covers the server as you install and run it yourself (stdio, for example in Claude
|
|
6
|
+
Desktop). There is no hosted holiplan endpoint yet; this note will be updated before one exists.
|
|
7
|
+
|
|
8
|
+
## What holiplan stores
|
|
9
|
+
|
|
10
|
+
Nothing. The server has no database and writes no files. Your plan lives in a ledger file that
|
|
11
|
+
you keep. Each tool receives the ledger from your assistant, works on it in memory, and hands
|
|
12
|
+
the result back. When the server stops, nothing about you remains in it.
|
|
13
|
+
|
|
14
|
+
## What the ledger contains
|
|
15
|
+
|
|
16
|
+
Only what you or your assistant put in it. The format is designed to need very little:
|
|
17
|
+
|
|
18
|
+
- **Children** are identified by an alias you choose ("older", "A"), and optionally a birth year
|
|
19
|
+
and interests. Never a name, photo or school.
|
|
20
|
+
- **Where you live** is a region code (such as `DE-BY`) and optionally a home town, used to work
|
|
21
|
+
out which local holidays apply to you.
|
|
22
|
+
- **Your plan**: trip dates, destinations, booking status and deadlines, and your leave
|
|
23
|
+
allowance.
|
|
24
|
+
|
|
25
|
+
## Who sees what
|
|
26
|
+
|
|
27
|
+
| Recipient | What it receives |
|
|
28
|
+
| --- | --- |
|
|
29
|
+
| Your AI assistant (e.g. Claude) | The whole ledger, because it passes the ledger to the tools. Its provider's privacy terms apply to your conversations. |
|
|
30
|
+
| [OpenHolidays](https://openholidaysapi.org) | Only a country code, a region code, date ranges and a language, to fetch holiday dates. Never your ledger, home town, children or trips. |
|
|
31
|
+
| Anyone else | Nothing. holiplan calls no other service. |
|
|
32
|
+
|
|
33
|
+
Your home town is matched against OpenHolidays' public list of towns on your own machine; the
|
|
34
|
+
town itself is never sent anywhere.
|
|
35
|
+
|
|
36
|
+
## Logs
|
|
37
|
+
|
|
38
|
+
The server writes logs to its standard error, which your assistant app may save (Claude Desktop
|
|
39
|
+
keeps them in its logs folder). They contain:
|
|
40
|
+
|
|
41
|
+
- the OpenHolidays requests made (country, region, dates), and
|
|
42
|
+
- when a tool call fails, the error message, which can name a trip id or quote a value that was
|
|
43
|
+
invalid, such as a malformed date.
|
|
44
|
+
|
|
45
|
+
They never contain the ledger itself. They stay on your machine.
|
|
46
|
+
|
|
47
|
+
## Booking and payments
|
|
48
|
+
|
|
49
|
+
holiplan never books anything and never sees payment details or provider accounts. It only
|
|
50
|
+
tracks what you tell it needs booking and by when.
|
|
51
|
+
|
|
52
|
+
## Holiday data
|
|
53
|
+
|
|
54
|
+
Holiday data comes from OpenHolidays under CC BY 4.0. It is fetched as needed and cached in
|
|
55
|
+
memory for up to a week.
|
holiplan-0.1.0/README.md
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# holiplan
|
|
2
|
+
|
|
3
|
+
An MCP server for planning a family's year around school holidays and a limited leave allowance.
|
|
4
|
+
|
|
5
|
+
<!-- mcp-name: io.github.adekola/holiplan -->
|
|
6
|
+
|
|
7
|
+
Assistants are good at suggesting destinations and bad at remembering that Whit Monday is a
|
|
8
|
+
public holiday, that five carried-over days expire in June, and that you already said no
|
|
9
|
+
drive over three and a half hours. This server does the parts that should be calculated;
|
|
10
|
+
the assistant does the parts that need judgement.
|
|
11
|
+
|
|
12
|
+
Holiday data comes from [OpenHolidays](https://openholidaysapi.org) (CC BY 4.0), which covers
|
|
13
|
+
public and school holidays for a growing list of countries, with no API key.
|
|
14
|
+
|
|
15
|
+
## Status
|
|
16
|
+
|
|
17
|
+
Pre-release (0.1). Stateless: your ledger is a JSON file you keep, and the server stores nothing.
|
|
18
|
+
See [PRIVACY.md](https://github.com/adekola/holiplan/blob/main/PRIVACY.md) for exactly what goes where, and
|
|
19
|
+
[docs/examples.md](https://github.com/adekola/holiplan/blob/main/docs/examples.md) for what a conversation looks like.
|
|
20
|
+
|
|
21
|
+
## Use it in Claude Desktop
|
|
22
|
+
|
|
23
|
+
Install [uv](https://docs.astral.sh/uv/getting-started/installation/), then add holiplan to
|
|
24
|
+
`claude_desktop_config.json` and restart Claude:
|
|
25
|
+
|
|
26
|
+
```json
|
|
27
|
+
{
|
|
28
|
+
"mcpServers": {
|
|
29
|
+
"holiplan": {
|
|
30
|
+
"command": "uvx",
|
|
31
|
+
"args": ["holiplan"]
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`uvx` fetches holiplan from PyPI and runs it; there is nothing else to install. Without uv,
|
|
38
|
+
`pip install holiplan` and use `"command": "holiplan"` with no `args`.
|
|
39
|
+
|
|
40
|
+
Start with the **Set up my family profile** prompt, which builds a ledger with you one
|
|
41
|
+
question at a time, or copy [`examples/ledger.example.json`](https://github.com/adekola/holiplan/blob/main/examples/ledger.example.json) to `my-ledger.json` and edit it.
|
|
42
|
+
Tell Claude where the file is, and save the updated ledger whenever Claude hands it back.
|
|
43
|
+
A conversation then looks like:
|
|
44
|
+
|
|
45
|
+
> Here's my ledger. What do the 2027 school holidays look like, and what can I afford?
|
|
46
|
+
|
|
47
|
+
> Add two weeks on the Danish coast from 17 July.
|
|
48
|
+
|
|
49
|
+
> What needs booking next?
|
|
50
|
+
|
|
51
|
+
[docs/examples.md](https://github.com/adekola/holiplan/blob/main/docs/examples.md) has these conversations in full, with real figures.
|
|
52
|
+
|
|
53
|
+
## Tools
|
|
54
|
+
|
|
55
|
+
| Tool | Purpose |
|
|
56
|
+
| --- | --- |
|
|
57
|
+
| `get_holiday_windows` | School holidays (official and effective dates), single-day closures, and public holidays marked local, half-day and observed |
|
|
58
|
+
| `calculate_leave_cost` | Leave days a given trip needs, with the holidays inside it listed |
|
|
59
|
+
| `find_bridge_days` | The leave days that buy the most time off, and whether the kids are off too |
|
|
60
|
+
| `get_year_budget` | Planned versus available leave, including carryover expiry |
|
|
61
|
+
| `upsert_trip` | Add or update a trip, then re-check the ledger |
|
|
62
|
+
| `check_ledger` | Overlaps, budget breaches, bad dates, trips outside school holidays |
|
|
63
|
+
| `list_deadlines` | Booking deadlines and cancel-by dates, soonest first |
|
|
64
|
+
| `summarise_plan` | The whole plan at a glance, for a brief to your partner |
|
|
65
|
+
| `export_ics` | The plan as a calendar file |
|
|
66
|
+
|
|
67
|
+
Resources: JSON schemas for the ledger, profile and trip (`holiplan://schema/...`), and a blank
|
|
68
|
+
starter ledger (`holiplan://ledger/starter`).
|
|
69
|
+
|
|
70
|
+
Prompts: *Set up my family profile*, *Plan my year*, *Plan this holiday window*,
|
|
71
|
+
*What needs doing next?*, *Make a brief for my partner*.
|
|
72
|
+
|
|
73
|
+
## Design notes
|
|
74
|
+
|
|
75
|
+
- **The ledger is the source of truth.** Tools take it in and hand it back, so the plan
|
|
76
|
+
doesn't drift between conversations.
|
|
77
|
+
- **Intent decides leave.** A week with intent `home` or `camp` occupies a school holiday
|
|
78
|
+
window without costing leave. Override with `takes_leave`.
|
|
79
|
+
- **Carryover is spent first**, but only by trips ending before the expiry date, so
|
|
80
|
+
"these days expire in June" shows up as a warning while you can still act on it.
|
|
81
|
+
- **A trip over New Year** costs each year only its own days.
|
|
82
|
+
- **Local holidays follow your home town.** Some holidays cover only part of a region, like
|
|
83
|
+
Augsburg's Peace Festival in Bavaria. With the profile's `home` set to a town, only that
|
|
84
|
+
town's local holidays count as days off; without a home they all do. Set `local_holidays`
|
|
85
|
+
to `"include"` or `"exclude"` to override. `ignore_holidays` (`"MM-DD"`) covers what the
|
|
86
|
+
data can't tell apart, such as Assumption Day (`"08-15"`), which is only a holiday in
|
|
87
|
+
Catholic-majority Bavarian towns.
|
|
88
|
+
- **School windows include the free days around them.** The official Monday-to-Friday dates
|
|
89
|
+
are stretched over adjacent weekends and public holidays before trips are checked against them.
|
|
90
|
+
School holidays of two school days or fewer, like All Souls' Day in Austria, are listed
|
|
91
|
+
separately as closures.
|
|
92
|
+
- **Half-day holidays cost half a day.** Zürich's Sechseläuten afternoon is a half day off,
|
|
93
|
+
not a full one.
|
|
94
|
+
- **Regions accept ISO codes.** OpenHolidays uses its own codes where they differ (Vienna is
|
|
95
|
+
`AT-WI`, not `AT-9`); either works, and an unknown region is an error rather than a quietly
|
|
96
|
+
incomplete year.
|
|
97
|
+
- **Ledgers are versioned.** Older ledgers are migrated when read, a newer one is refused, and
|
|
98
|
+
a malformed one gets an error naming the trip and field at fault.
|
|
99
|
+
- **When OpenHolidays is down**, the server serves the last copy it fetched, or says plainly
|
|
100
|
+
that it can't check dates right now. It never guesses.
|
|
101
|
+
- **No real names.** Children are identified by alias and interests only.
|
|
102
|
+
- **The server never books anything.** It knows what needs booking and by when; you book it
|
|
103
|
+
and record the result.
|
|
104
|
+
|
|
105
|
+
## Roadmap
|
|
106
|
+
|
|
107
|
+
- [ ] Drive-time filtering (OpenRouteService or self-hosted OSRM)
|
|
108
|
+
- [ ] Remote deployment over Streamable HTTP
|
|
109
|
+
- [ ] Publish to the MCP registry
|
|
110
|
+
- [ ] School holiday windows by school type, where the data supports it (Zürich splits
|
|
111
|
+
primary, secondary and vocational; windows already list the types they apply to)
|
|
112
|
+
|
|
113
|
+
## Development
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
git clone https://github.com/adekola/holiplan.git
|
|
117
|
+
cd holiplan
|
|
118
|
+
python -m venv .venv && source .venv/bin/activate # Windows: .venv\Scripts\activate
|
|
119
|
+
pip install -e ".[dev]"
|
|
120
|
+
python -m unittest discover -s tests # offline
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
To run your working copy in Claude Desktop, point `command` at the venv's Python
|
|
124
|
+
(`.venv/bin/python`, or `.venv\Scripts\python.exe` on Windows) with
|
|
125
|
+
`"args": ["-m", "holiplan.server"]`.
|
|
126
|
+
|
|
127
|
+
## Licence
|
|
128
|
+
|
|
129
|
+
MIT, see [LICENSE](https://github.com/adekola/holiplan/blob/main/LICENSE). Holiday data: [OpenHolidays](https://openholidaysapi.org), CC BY 4.0.
|