After cz bump changes the project version, regenerate uv.lock and amend it into
the bump commit (moving the tag) so every release tag stays consistent and
`uv sync --locked` works when the versioned image is built.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NGzHtDvJur9U7ysgRKRUTN
cz bump updates pyproject.toml's version but not uv.lock's own-package entry,
which left `uv sync --locked` failing at the v0.2.0 release commit. Regenerate
the lock so pyproject and uv.lock agree.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NGzHtDvJur9U7ysgRKRUTN
Replace actions/setup-python (which fails on a cold runner toolcache with a
broken python-versions prebuilt) with the self-contained uv installer, matching
release.yaml. Uses `uv sync --all-extras --locked --python 3.12` and
`uv run --locked pytest`, so the test gate — and therefore the versioned image
build behind it — no longer depends on a warm toolcache.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NGzHtDvJur9U7ysgRKRUTN
Trigger the image build on v* tags and, on a version tag, publish the semver
(X.Y.Z) alongside :latest and the commit SHA. Previously only :latest and
:<sha> were pushed and the build never fired on the release tag, so there was
no stable version to pin against.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NGzHtDvJur9U7ysgRKRUTN
Address findings from the pre-merge review:
- Form (TSB) now computes only when ctl/atl are numeric (isinstance guard),
so a non-numeric value can no longer raise out of format_wellness_entry and
take down the entire wellness render.
- The Date line uses `or` chaining so a present-but-null `date` falls back to
`id` instead of rendering "Date: None".
- update_wellness injects the written date into the API echo when it lacks
id/date, so the confirmation body can't read "Date: N/A" under a dated header.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NGzHtDvJur9U7ysgRKRUTN
Remove unused imports (F401) and strip trailing whitespace (W291/W293) flagged
by ruff in test files. Pre-existing debt unrelated to any single feature; CI runs
pytest but not ruff, so these had accumulated. No behavior change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NGzHtDvJur9U7ysgRKRUTN
Add an update_wellness MCP tool that writes nutrition macros, hydration, vitals,
sleep, and subjective ratings to Intervals.icu via PUT /athlete/{id}/wellness/{date}.
Only provided fields are sent; pass -1 to clear a numeric field and locked=True to
stop device/app syncs from overwriting the values. Sleep is taken in hours and
stored as seconds (with -1 passing through as the clear sentinel).
Also surface computed Form (TSB = CTL - ATL) in wellness output, and prefer an
explicit `date` field over the record `id` for the Date label.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NGzHtDvJur9U7ysgRKRUTN
The manual 0.1.0->0.2.0 bump conflicted with the automated release
workflow, which would have double-bumped to 0.3.0. Version is now driven
solely by `cz bump` from conventional commits (last tag v0.1.0 is the
baseline); CHANGELOG entries are generated at release time.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NGzHtDvJur9U7ysgRKRUTN
Cutting a release is now a workflow, not a local command. Uses uv/commitizen
(actions/setup-python is broken on the current runners). Supports auto-detect or
forced increment, and a dry_run mode.
Establishes the release process for the upcoming nutrition/wellness features (a
minor bump). Source of truth is pyproject [project].version; `uvx cz bump` derives
future versions from conventional commits and tags vX.Y.Z.
Closes the audience-binding gap (RFC 9068): tokens minted by the issuer for a
different resource are now rejected at /mcp, and subject-less tokens are refused.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
OPTIONS /mcp returned 401 (auth layer rejecting the preflight) with no CORS
headers, which blocks browser-based MCP clients / connector setup. Build the app
explicitly and add CORSMiddleware (wildcard origin — bearer-token auth, no cookies)
so preflight is answered 200 and Mcp-Session-Id/WWW-Authenticate are exposed.
stateless_http + json_response meant no session id was issued, so Claude Desktop
connected but never loaded tools. Default to stateful + SSE; both are env-toggleable
(MCP_STATELESS_HTTP / MCP_JSON_RESPONSE) for multi-replica deployments.
Better Auth signs access tokens with EdDSA (Ed25519), not RS256. Accept EdDSA
(+ RS256/ES256), and validate issuer + signature + expiry strictly while checking
audience softly — single-resource server behind a dedicated AS with dynamic DCR
client ids, so issuer + signature is the trust boundary. Adds an EdDSA test.
Adds alembic/ (async env.py driven by DATABASE_URL) and the 0001 users-table
migration matching the model. enabled now has a DB-level server_default of false
(secure default even for non-ORM inserts). Verified upgrade/downgrade on sqlite.
All 20 tools now drop the athlete_id/api_key parameters and instead resolve the
authenticated caller's stored, enabled credentials via
credentials.resolve_caller_credentials() (get_access_token().subject -> store).
Security: there is no tool parameter a caller can pass to supply a key, so a
disabled/unapproved user cannot bypass the admin-approval gate — each tool
returns a helpful "not approved / set up your credentials" message instead.
Gear resolution now uses the caller's athlete id rather than an env var.
Tests: conftest autouse fixture runs tool tests as an enabled user; a
parametrized test asserts every tool refuses when unauthorized; existing tool
tests updated (no more athlete_id/api_key kwargs). 221 passing at 91.5%.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Foundation for multi-user operation (Phase 1):
- crypto.py: AES-256-GCM encrypt/decrypt for the per-user Intervals API key,
key from INTERVALS_ENC_KEY (base64 32 bytes). Random nonce per message.
- db/models.py + db/session.py: SQLAlchemy 2.0 async User model (keyed on the
Authentik sub; api_key stored encrypted; enabled = admin-approval gate) and a
lazy async engine/sessionmaker from DATABASE_URL.
- store.py: async CRUD. New users created disabled; login never flips enabled;
get_active_credentials returns decrypted creds only for an enabled user that
has them.
- credentials.py: resolve_caller_credentials() maps get_access_token().subject
to that user's stored creds, falling back to env config only when unauthenticated
(stdio/local).
- Tests (SQLite in-memory, no infra): crypto round-trip/tamper, store gating,
resolver paths. Suite 199 passing at 90.4% (gate holds).
Deps: sqlalchemy[asyncio], asyncpg, alembic (+ aiosqlite for tests). Pin py3.12.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Bump mcp[cli] 1.22 -> 1.28.1 (negotiates MCP protocol 2025-11-25, matching
current Claude clients; the old 2025-06-18 server never got a tools/list on
the connector surface).
- Bake transport config into code: stateless_http + json_response for HTTP
(single JSON body instead of a 34KB SSE stream, which the connector pipeline
handles far more reliably).
- Bake Authentik OAuth (AuthSettings + JWT TokenVerifier) into intervals_mcp_server.auth,
configured from MCP_ISSUER/MCP_RESOURCE/MCP_JWKS_URI/MCP_CLIENT_ID — removes the
runtime FastMCP.__init__ monkeypatch from the k8s deployment command.
- Accept token audience with/without trailing slash (RFC 8707 clients use the
slash-normalised resource metadata value).
- Dockerfile CMD runs the module (transport via MCP_TRANSPORT); add .gitea CI to
build+push the image to git.farh.net/farhoodlabs/intervalsicu-mcp.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>