The Dockerfile was broken since the MCP endpoint was added: it built a single file (cmd/domogeek/domogeek.go) instead of the whole package, so mcp.go's symbols (calendarDayFor, newMCPHandler) were undefined. Fixes that by building ./cmd/domogeek, pins the builder image to golang:1.27-alpine (module now requires go 1.25+), and drops the hardcoded GOOS/GOARCH=amd64 so multi-arch builds compile natively per platform instead of always producing an amd64 binary. Also fixes .Dockerignore (wrong case, never actually read by docker/buildah) by renaming it to .dockerignore, and anchors its "domogeek" pattern to /domogeek so it excludes only the compiled binary, not the whole cmd/domogeek source directory (same bug as the .gitignore fix in an earlier commit). Removes build-docker.sh: image releases now go through the existing homelab Tekton pipeline (gitea-docker-build-multiarch), triggered by a Gitea webhook on tag push, same pattern as other repos (e.g. dra-devices). Images move from docker.io/cyrilix/domogeek to git.cyrilix.bzh/cyrilix/domogeek. Documented in CLAUDE.md and README.md. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Hu8wwahgecuCGfesTKCfEd
106 lines
8.5 KiB
Markdown
106 lines
8.5 KiB
Markdown
# CLAUDE.md
|
|
|
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
|
|
## What this is
|
|
|
|
domogeek is a small Go HTTP service that exposes calendar/holiday information for home automation (domotic) use. It answers whether "today" is a working day, a French public holiday (`ferie`), and/or a personal holiday read from a CalDAV calendar. The same information is also exposed as MCP tools over a Streamable HTTP endpoint (`/mcp`), so LLM agents/MCP clients can query it directly.
|
|
|
|
## Commands
|
|
|
|
Build and test use Go's standard tooling with vendored dependencies (`-mod=vendor`). The module requires **Go 1.25+** (bumped from 1.18 to use the official MCP SDK). If the `go` binary on `PATH` resolves to an older toolchain (e.g. this box also has `golang-1.18` installed and it can shadow the newer one), invoke a newer toolchain explicitly, overriding `GOROOT` if it's pinned to the old one (e.g. by an IDE's `_INTELLIJ_FORCE_SET_GOROOT`):
|
|
|
|
```bash
|
|
GOROOT=/usr/lib/go-1.27 /usr/lib/go-1.27/bin/go build -mod=vendor ./...
|
|
```
|
|
|
|
Otherwise, plain invocations work the same as before:
|
|
|
|
```bash
|
|
go build -mod=vendor ./... # build
|
|
go vet -mod=vendor ./... # vet
|
|
go test -mod=vendor ./... # run all tests
|
|
go test -mod=vendor ./pkg/calendar/ -run TestCalendar_IsWorkingDay -v # run a single test
|
|
```
|
|
|
|
Run locally (caldav flags are optional — omitting them disables CalDAV-based holiday checks):
|
|
|
|
```bash
|
|
go run -mod=vendor ./cmd/domogeek \
|
|
-port 8080 \
|
|
-caldav-url https://example.com \
|
|
-caldav-path /calendars/user/holidays/ \
|
|
-caldav-username user \
|
|
-caldav-password pass \
|
|
-caldav-summary-pattern Holidays
|
|
```
|
|
|
|
Running with no flags at all prints usage and exits (see the `len(os.Args) <= 1` check in `main`). Note: despite the "caldav flags are optional" framing, `main()` currently calls `calendar.NewCaldav` unconditionally, even with an empty `-caldav-url` — with no caldav server to validate against, this hits the 1000-retry exponential backoff described below and the process never gets to `http.ListenAndServe`. This is a pre-existing quirk, not something introduced by the MCP work; if you need a quick local instance (e.g. to exercise `/mcp` or `/calendar` without a real CalDAV server), it's easiest to call `newMCPHandler()`/`calendar.New(location)` directly from a small throwaway `main`/test rather than running the built binary.
|
|
|
|
Docker image (multi-stage, distroless), for local testing:
|
|
|
|
```bash
|
|
docker build -t domogeek . # or: buildah bud -t domogeek .
|
|
```
|
|
|
|
## Continuous builds
|
|
|
|
Pushing a Git tag matching `v[0-9]*` (existing convention: `vX.Y.Z`, e.g. `v0.4.0`) to
|
|
the Gitea remote triggers the homelab Tekton pipeline `gitea-docker-build-multiarch`,
|
|
which builds and pushes a multi-arch (amd64 + arm64) image plus a signed CycloneDX SBOM:
|
|
|
|
```
|
|
git.cyrilix.bzh/cyrilix/domogeek:<tag>
|
|
```
|
|
|
|
The pipeline uses its defaults — `Dockerfile` at the repo root, build context `.` — so
|
|
the Dockerfile must stay at the root. It builds each target platform natively (no
|
|
`GOOS`/`GOARCH` overrides in the `go build` step), so the Dockerfile must not hardcode a
|
|
single-arch `GOOS`/`GOARCH` — that was a real bug fixed alongside this: the builder image
|
|
is now pinned to `golang:1.27-alpine` (matching the `go 1.25` module requirement) and the
|
|
`go build` step targets the whole package (`./cmd/domogeek`), not a single file.
|
|
|
|
There is no Tekton YAML in this repo: the pipeline and its Gitea webhook trigger are
|
|
defined cluster-side. Watch a run: <https://tekton.banquise.cyrilix.bzh>
|
|
|
|
```sh
|
|
git tag v0.4.0
|
|
git push --tags
|
|
```
|
|
|
|
Note: this replaces the previous `docker.io/cyrilix/domogeek:<git describe>` releases
|
|
built locally via the now-removed `build-docker.sh`/`buildah` script.
|
|
|
|
## Architecture
|
|
|
|
- `cmd/domogeek/domogeek.go` — the binary entrypoint. Wires up flags, zap logging, the `calendar.Calendar`, Prometheus metrics/instrumented handlers, and a `health-go` health check, then serves five HTTP routes:
|
|
- `GET /calendar` — returns a `CalendarDay` JSON payload (`working_day`, `ferie`, `holiday`, `weekday`) for "now" in `Europe/Paris`.
|
|
- `GET /datetime` — returns a `CurrentDateTime` JSON payload (`date_time`) for "now" in `Europe/Paris` (`DateTimeHandler`).
|
|
- `POST /mcp` — MCP Streamable HTTP endpoint (see `cmd/domogeek/mcp.go` below).
|
|
- `GET /metrics` — Prometheus metrics (request counter/summary/histogram wrapping the calendar handler, defined in `init()`).
|
|
- `GET /status` — health-go handler with two checks: a static `calendar` check and a `caldav` check that calls `cal.IsHolidaysFromCaldav`.
|
|
- The process blocks on `SIGTERM` (not `SIGINT`) via a signal channel; `http.ListenAndServe` runs in a goroutine and `zap.S().Fatal`s the process on error.
|
|
|
|
- `cmd/domogeek/mcp.go` — MCP server built with `github.com/modelcontextprotocol/go-sdk/mcp`, mounted at `/mcp` via `mcp.NewStreamableHTTPHandler`. Exposes four tools, all reading the same package-level `cal`/`location` globals as the `/calendar` and `/datetime` handlers:
|
|
- `get_current_datetime` — same payload as `GET /datetime`: the current date and time.
|
|
- `get_calendar_today` — same payload as `GET /calendar`, for now.
|
|
- `get_calendar_for_date` — same payload for an arbitrary `date` (`YYYY-MM-DD`); an invalid date is returned as a tool error (`CallToolResult.IsError`), not a protocol-level error, so an MCP client/LLM can see and self-correct.
|
|
- `get_holidays` — lists French public holidays (`Calendar.GetHolidays`) for a given `year`, defaulting to the current year if omitted.
|
|
- `calendarDayFor` in this file is the single place that builds a `CalendarDay` from a `time.Time`; both the HTTP `/calendar` handler and the MCP tools call it, so there's one code path for the working-day/ferie/holiday/weekday logic.
|
|
- `cmd/domogeek/mcp_test.go` unit-tests the handler functions directly; `cmd/domogeek/mcp_e2e_test.go` drives the real Streamable HTTP transport with an MCP client (`httptest.Server` + `mcp.NewClient(...).Connect`) to check the wiring end-to-end (tool listing, structured content, and the tool-error-vs-protocol-error distinction). `cmd/domogeek/datetime_test.go` covers the `/datetime` HTTP handler directly.
|
|
|
|
- `pkg/calendar/calendar.go` — all domain logic, built around the `Calendar` struct and functional options (`WithCaldav`, `WithCaldavPath`, `WithCaldavSummaryPattern`):
|
|
- `GetEasterDay` implements the Gauss/Meeus algorithm to compute Easter Sunday for a given year (in `cal.Location`); `GetHolidays` derives the fixed-date French public holidays plus two computed from it: Easter Monday (Easter+1) and Ascension (Easter+39).
|
|
- `IsHoliday` combines the static French holiday set with `IsHolidaysFromCaldav`.
|
|
- `IsHolidaysFromCaldav` queries a CalDAV calendar for events on a given day and treats any event whose `Summary` contains `caldavSummaryPattern` (default `"Holidays"`) as a personal holiday. If no `Caldav` client is configured, it returns `false, nil` — CalDAV is optional.
|
|
- The `Caldav` interface (`QueryEvents`) abstracts `github.com/dolanor/caldav-go`, letting tests substitute `MockCaldav` (see `calendar_test.go`) instead of hitting a real server.
|
|
- `NewCaldav` validates the CalDAV connection at startup with `avast/retry-go`, retrying up to 1000 times with exponential backoff (max delay 24h) — this means a bad/unreachable CalDAV server does **not** fail fast; `main()` will effectively hang retrying rather than exiting quickly if the caldav server is down (it's only `Fatal`'d if `NewCaldav` itself returns a non-nil error, which currently only happens after all retries are exhausted).
|
|
|
|
- `pkg/metrics/metrics.go` — currently a single unused/unconfigured Prometheus counter (empty `Namespace`/`Subsystem`/`Name`/`Help`). Not wired into `cmd/domogeek`; treat as in-progress scaffolding rather than an established pattern to follow.
|
|
|
|
## Dependencies
|
|
|
|
Dependencies are vendored under `vendor/` and checked into the repo — always pass `-mod=vendor` (or rely on `GOFLAGS=-mod=vendor` if set) for build/test/vet so Go doesn't try to hit the network or the module cache. After changing `go.mod`/`go.sum`, run `go mod vendor` to keep `vendor/` in sync (use the newer toolchain per the note above, and `-mod=mod` instead of `-mod=vendor` for the `go get`/`go mod tidy` step itself).
|
|
|
|
`github.com/modelcontextprotocol/go-sdk` (the official Go MCP SDK) is the dependency added for the `/mcp` endpoint; it pulled in `google/jsonschema-go`, `segmentio/encoding`, `yosida95/uritemplate`, and a few `golang.org/x/*` packages as transitive deps, and required bumping the `go` directive in `go.mod` from `1.18` to `1.25.0`.
|