feat: write tools — update_wellness_bulk + update_sport_settings (dual-guardrail) #5
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Part of the 0.3.0 coaching-context milestone. Two write tools.
Tools
update_wellness_bulk—PUT /athlete/{id}/wellness-bulk— multi-day wellness/nutrition writes (pairs with 0.2.0update_wellness).update_sport_settings—PUT /athlete/{id}/sport-settings/{id}— change FTP/LTHR/pace/zones. RISKY: drives all future load/intensity math.update_sport_settingsguardrail (dual mechanism)Claude Desktop/mobile does not render MCP elicitation yet (Anthropic open issues),
so elicitation alone is insufficient. Belt-and-suspenders:
⚠️docstring: model must surface exact old→new values and get approval.ctx.elicit()confirm dialog on capable clients (Claude Code); abort on decline/cancel.confirm=Truenot passed, REFUSE the write andreturn the old→new diff + warning, instructing a re-call with
confirm=true.Write only on elicit-accept OR explicit
confirm=True. First tool to useContext.Notes
Context.elicit(message, schema).Implementation plan (0.3.0) — intended delivery model: Claude Opus 4.8
Branch:
feat/coaching-context; one conventionalfeat:commit for this issue (lands after #2, which providesget_sport_settingsfor the read-modify-write and for surfacing settings ids).Endpoints (verified — two gotchas found in the spec)
PUT /athlete/{id}/wellness-bulk— body is a JSON array ofWellnessobjects; each entry carries its date asid.PUT /athlete/{athleteId}/sport-settings/{id}— bodySportSettings, and query paramrecalcHrZones(boolean) is REQUIRED by the spec. Response echoes the updatedSportSettings.1.
update_wellness_bulk(extendtools/wellness.py)entries: list[dict]— each entry{date: 'YYYY-MM-DD', ...fields}using the SAME snake_case field names asupdate_wellness(weight, resting_hr, hrv, sleep_hours, calories_consumed, carbohydrates, protein, fat, hydration_volume, hydration_score, soreness, fatigue, stress, mood, motivation, injury, comments, locked).update_wellnessinto a shared module-level helper_wellness_payload(fields) -> dictso single and bulk cannot drift. Each payload getsid= validated date.2.
update_sport_settings(intools/athlete.pyfrom #2) — DUAL-GUARDRAIL WRITEsettings_id: int, ftp=None, indoor_ftp=None, w_prime=None, lthr=None, max_hr=None, threshold_pace=None, recalc_hr_zones: bool = False, confirm: bool = False, ctx: Context | None = None(FastMCP injectsctx; first tool in the repo to use it)./athlete/{athleteId}/sport-settings, locate record bysettings_id(error if absent) -> build old->new diff of ONLY the provided fields -> guardrail -> merge into the full record -> PUT withrecalcHrZones-> render the echoed settings viaformat_sport_settings.ctxpresent:await ctx.elicit(message_with_diff, ConfirmSchema)(pydantic model, singleconfirm: bool). decline/cancel -> return 'unchanged'. Wrap in try/except — clients without the elicitation capability fall through.confirm is not True-> REFUSE with the old->new diff + instruction to re-call withconfirm=true. Write happens only on elicit-accept OR explicitconfirm=True.Tests
update_wellness(same fixture through both), invalid-date batch rejection, -1 passthrough, request shape (array body), error path.confirm=Truewrites, mocked ctx elicit accept/decline/cancel, elicitation-unsupported exception falls back to refusal, unknownsettings_id, no-op guard,recalcHrZonespassthrough, error dict. MockContextwith a stub — no real MCP session needed.Estimate
~450 LOC incl. tests. Establishes the confirm/elicit convention for all future risky writes.