Repository navigation
Phase 2: performance analytics, execution analysis, detail levels, OAuth server and CI fixes - #2
Merged
Conversation
…OAuth server, CI fixes P0 - CI/release: every action pinned to a verified commit SHA (setup-uv@v10 did not resolve), build + Docker smoke jobs run in CI, PEP 440 tags classified (b/rc -> pre-release, no 'latest'), explicit GHCR tags, tag must match pyproject version - Remote hardening: FASTMCP_MESSAGE_PATH, secret-path deployment (docs/REMOTE_ACCESS.md), built-in single-user OAuth 2.1 authorization server (auth.py, MCP_AUTH=oauth) with DCR, PKCE, login page, rate limiting and persisted clients; status tool reports the auth mode P1 - analyze_workout_execution: planned_workout_doc for deleted events, detection of training beyond the plan (reported separately, not as poor compliance), read-only match suggestions - performance tools: get_best_efforts, compare_best_efforts, find_similar_intervals, get_activity_histogram, compare_workouts, get_power_hr_efficiency, get_fatigue_resistance - get_training_plan (ATP), get_library_workout, update_sport_settings (admin), gear maintenance reminders, get_activities(power_meter=...) - custom fields separated by sport assignment (sport settings activity_field_ids); power meter identity never guessed; stamina vs potential stamina and gear index notes documented P2 - detail_level compact/standard/full for activity details, intervals and recovery snapshot; interval stream statistics limited to requested streams - get_activity_report: one-call compact analysis (3-4 API calls) - eight coaching prompts, two MCP resources - docs: README, CHANGELOG, SECURITY, GARMIN_BRIDGE, UPSTREAM_AUDIT (#152), REMOTE_ACCESS Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…gs from tools package pylint E0603: the names were listed in __all__ but not imported. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- get_library_workout, get_training_plan and update_sport_settings were listed in tools.__all__ but not imported (pylint E0603); the previous commit wrote them into the noqa comments by mistake - pylint job: --fail-under=0 --fail-on=E,F so complexity/style notes no longer show a red check while error-class messages still fail it Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… search
Intervals.icu rejects decimal values ("minIntensity=90.0") with 422, and MCP clients send
90 as 90.0 because the parameter is a float. Bounds are now sent as integers (fractional
bounds widen to whole percent); tests cover the direct call and the FastMCP layer.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…MD, RFC 9207, /mcp + /sse - "Continue with Intervals.icu" sign-in (OAUTH_LOGIN=intervals): the server is an OAuth client of Intervals.icu for the identity check only; athlete id checked against OAUTH_ALLOWED_ATHLETES (default ATHLETE_ID); the upstream token is never stored; browser-bound state (HttpOnly cookie) - consent page: verified client name for metadata-document clients, per-connection permission classes as scopes intervals:read/write/destructive/admin, enforced in tools/list and tools/call - Client ID Metadata Documents from allowlisted hosts (chatgpt.com, claude.ai, claude.com) with private_key_jwt verification (JWKS from the document, audience, lifetime, replay) - RFC 9207 iss on every authorization response (also SDK error redirects), extended AS and protected resource metadata, audience check on access tokens, resource kept on refresh - redirect host allowlist and optional disabling of dynamic client registration - MCP_TRANSPORT=http+sse serves streamable HTTP (/mcp) and SSE (/sse) from one process; all HTTP transports now start through build_http_app (uvicorn) - status/--doctor show sign-in method, Intervals.icu app and allowed athletes - docs: REMOTE_ACCESS rewritten (Intervals.icu app, ChatGPT setup, Apache), README, SECURITY, .env.example, CHANGELOG, UPSTREAM_AUDIT (#152) - tests: 24 new (test_oauth_extensions.py), auth tests updated; 325 passed Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…cation) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The SDK's revocation model makes client_secret mandatory (str | None without default), so public clients such as ChatGPT got 400. An absent field is added as empty; clients with a secret still have to send it. Regression test included. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Positioning as the continuation of mvilanova/intervals-mcp-server, cross-links to the Garmin Intervals Bridge, full tool catalogue with permission classes, OAuth setup for ChatGPT/Claude, configuration, security, project status and roadmap (incl. the analytics quality work in progress), documentation index, credits. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…others), virtualenv 21.14.5 PyJWT is used directly for private_key_jwt client assertions; minimum raised to 2.15.0. virtualenv is a development dependency only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
get_training_summary summed every custom field whose definition says SUM, which produced e.g. "Stamina at start: sum 1454 %", "GCT: sum 630 ms" or "Vertical Oscillation: sum 18.76 cm". A generic policy (utils/field_policy.py) now derives the aggregation from the field type, units, name/code words and the definition's aggregate: additive values (kcal, ml, distance, time) are summed, device loads / EPOC are summed as a device scale, estimates (VO2max, performance condition, recovery time, detected thresholds) get latest/change/range, per-activity values (percentages, scores, effects, running dynamics, temperatures) get mean, median, min and max, and fields without a recognisable meaning get no aggregate. Start/end pairs (stamina) report the typical change, values from sports without the field are ignored, a stored 0 of an estimate counts as "no value", and CUSTOM_AGGREGATE_OVERRIDES overrides the policy per code. The summary gets a detail_level (compact / standard / full). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
compare_workouts averaged every WORK interval of every activity found by the name
search, so rides, gravel rides and runs with different interval lengths were mixed
and a 50 s 432 W surge was averaged with three 10-minute intervals (249/249/253 W
became 296 W). Per activity the WORK intervals are now split (utils/work_sets.py)
into the main set (largest group of similar duration and intensity), short surges
(< 2 min, listed but never averaged in), other WORK intervals and intervals below
70 % FTP (warm-ups/recoveries labelled WORK). The main set is summarised with
time-weighted means and avg/max HR per interval; only activities matching the
reference pattern (reference_activity_id or the newest activity) are compared, the
others are listed with the reason. Sport defaults to the reference's sport family
("all" keeps every sport). New optional filters: gear_id, min/max interval secs,
min/max intensity, min/max reps, ftp_range, include_rpe, comparable_only. Trends
are reported separately for power, HR, max HR, cadence, W/bpm and whole-activity
RPE, small samples are marked, and differing gear / power meters / indoor rides
and FTP changes are flagged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On a long alpine hike the climb analysis reported grades up to 73 % from tiny GPS distances and about 4 h of "pauses" that were mostly slow scrambling. Segment detection now uses sport profiles (foot sports vs bikes) for altitude smoothing, the grade distance window, a minimum horizontal distance below which no grade is computed and a plausible grade above which a segment is flagged (and left out of the steepest climb). Pause candidates are classified in 2-minute chunks: real pauses (no vertical/horizontal progress, little stepping) and recording stops count as pause time, slow movement is kept as moving time. VAM and speed use the moving time of a segment, every segment carries data-quality flags, the raw grade can be shown (show_raw_grade), a device moving-time counter is reported for comparison only, and the summary separates the gain inside climbs from the whole-profile gain and the Intervals.icu total. Clock counters, identical and other-sport custom streams are left out of the per-segment statistics. New optional parameters: show_raw_grade, grade_window_m, min_grade_distance_m, stationary_speed_m_s. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t extra riding
For a 3 x 10 min threshold ride extended to 87 minutes the 10-minute cool-down was
matched to a 17:58 interval and judged as poor compliance ("plan part 1:13:15").
Planned steps are now capped at their planned (moving) duration: a longer interval
is split logically (analysis only), the planned part is evaluated from the samples
and the remainder of the last step plus everything after it is reported as
additional training with its own metrics, kJ share, estimated load, sample range
and the extra efforts it contains (e.g. the 50 s sprint); riding before the plan
and extra time inside the plan are reported separately. Alignment can match a step
to up to three consecutive same-intensity intervals (lap splits), never uses names,
treats intervals before/after the plan as cheap edges and measures intensity on the
planned part. Durations use moving time; new optional tolerances
(duration_tolerance_pct, start_tolerance_s, pause_tolerance_s) and a detail_level
for analyze_workout_execution and get_activity_report. Steps carry deviation notes
and the summary reports exact-range hits, mean time in target and steps with clear
deviations instead of a single "in target" count. The Intervals.icu interval type is
kept and shown next to the planned step type when they differ; HR recovery and
Pw:HR drift are only computed after/for work steps; clock counters, identical and
other-sport custom streams are left out of per-step statistics.
get_activity_report: compact level with up to five key findings, standard level
without raw stream dumps and at most 30 interval lines, full level with everything;
the second-power-meter check needs a second stream with enough valid samples and
reports identical streams instead of comparing them; data-quality notes flag gear
streams whose values are gear positions rather than tooth counts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The output said 42 days in the header, 84 days per metric (the fetched range incl. lookback, over which the "range stats" were also computed) and a 42-day baseline. Statistics, outliers, the latest value and the day series now cover the requested period only; the header states the requested days, the days fetched including the 42-day lookback for rolling windows and the baseline, and the baseline window (dates and n per metric). Native metrics get units (HRV ms, resting/sleeping HR bpm, respiration breaths/min, SpO2 %, readiness and sleep score /100, weight kg), custom fields keep their definition units. A stored 0 of physiological metrics is treated as missing (never averaged), baselines with fewer than 14 values and correlations with fewer than 30 pairs are flagged as small samples, also in the recovery snapshot baselines. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…parability Without filters the interval search mixed rides with runs, ski tours, old gravel rides and other FTP contexts. The window (interval length ±25 %, intensity ±8 % of FTP) can now be derived from a reference activity's main work set; the default sport filter is the reference's sport family or the family with the most results (other families are counted, sport_types="all" keeps the old cross-sport behaviour); each result shows gear and power meter and gets a comparability score with reasons (best matching interval group, sport type, gear / power meter, FTP context within ftp_tolerance_pct); results can be ranked by comparability (default with a reference) or date; ftp_range filters the FTP at the time. The range parameters are optional when a reference is given. compare_workouts also reports power and W/bpm trends on the reference gear only when gear differs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s, filters Trends per power band were reported from one or two activities per group (e.g. +14.1 % from a single activity). A band trend now needs at least min_activities_per_group independent activities (default 3) in the oldest and in the newest group, otherwise it is reported as not reliable; the change is compared with the day-to-day standard deviation of the per-activity W/bpm. Band means are time-weighted, bands are half-open and must not overlap, trends are computed per gear when several bikes / power meters are involved, and new optional filters select gear, indoor/outdoor and the position of the interval in the ride (min_start_minutes / max_start_minutes). The wording never derives an improvement from a single activity. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lues When after_kj0 / after_kj1 were empty the tool still showed kJ0/kJ1 columns (+0.0 % for an activity, n/a for athlete curves). The sport settings are now read first; without configured thresholds no fatigued curves are requested, no fatigue values are shown, the needed configuration is explained and plausible thresholds are suggested (15 / 30 kJ per kg body mass or 1.5 h / 3 h at ~55 % FTP, plus values adapted to the work of recent rides of 1 h+) - read-only, settings are never changed (suggest_thresholds=False turns the suggestion off). Configured curves are checked before computing: a fatigued curve without data is reported as missing, one equal to the fresh curve everywhere as identical, and no change is computed for either; only configured thresholds are fetched. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds tests for an existing workout with repeats and watt / %FTP / cadence targets, pace and %LTHR targets, a repeat nested in a repeat, a missing id, an empty library, API errors and malformed data (only GET requests are made). A non-numeric target value raised AttributeError instead of falling back to the raw steps; the fallback now covers it and says why the steps could not be rendered. Missing header values read "n/a" and the planned time is shown as h:mm:ss. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… empty plans - get_best_efforts: positions are elapsed time from the time stream (the clock includes recording pauses); the old "recording pauses excluded" wording was wrong. Efforts whose window spans a recording pause are flagged with the paused time (JSON: paused_s_in_window). - Stream descriptions: a gear stream defined with tooth units (cog/teeth) whose values are small whole numbers is labelled as gear position with a note instead of being presented as tooth counts. - get_activity_intervals: optional include_planned_types / planned_workout_doc show the planned step matched to each interval next to the unchanged Intervals.icu WORK/RECOVERY type and flag differences. - get_training_plan: an unreadable plan is reported as unknown instead of "none", and an empty range says so explicitly. - Tests for the report detail levels, the second-power-meter checks (identical and too sparse streams) and the stream relevance helpers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…mpact details - Additional training: with FTP known only efforts >= 90 % FTP are listed as extra efforts (Intervals.icu may label easy riding WORK); without FTP the WORK label decides. - Training summary aggregates: a single value is shown as such, min/max as a range (median kept), which keeps weekly groups short. - Compact activity details print n/a instead of None for missing feel, RPE, FTP and compliance; the report's note on left-out custom streams is worded for activities with and without a plan. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e guide Adds the "Changed (phase 3: analytics quality)" section to the 1.0.0b1 changelog and documents the aggregation policy and CUSTOM_AGGREGATE_OVERRIDES in docs/GARMIN_BRIDGE.md; fixes a pylint error (unsubscriptable optional tuple) in find_similar_intervals and adds targeted disables for long helpers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Segment statistics of a gear stream defined with tooth units but holding gear positions are labelled "gear position" instead of "cog", like the other outputs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When a custom field is assigned to at least one sport in the sport settings, activities of sports without an assignment list (e.g. walks) no longer contribute their stored zeros; running dynamics now average over runs only (vertical oscillation 10.05 cm instead of 3.77 cm with walk/ride zeros). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Phase 3: analytics quality, bug fixes and output size. Stacked on #2 (`feat/phase2-analytics-release`). All tools stay read-only; existing tool names and parameters keep working, new parameters are optional. ## P0 - **Workout execution with extended rides** (`analyze_workout_execution`, `get_activity_report`): planned steps are capped at their planned (moving) duration. A longer interval is split logically (analysis only). The planned part is evaluated from the samples. The remainder of the last step plus everything after the plan is reported as additional training, with sample range, kJ share, estimated load and extra efforts. Matching uses order, duration and target intensity on the planned part, never names, and a step can match up to 3 same-intensity laps. New tolerances: duration, start shift and pause. The summary shows exact-range hits, mean time in target and steps with clear deviations instead of an artificial perfect score. The Intervals.icu interval type is kept and shown next to the planned type. - **`compare_workouts`**: compares only comparable work intervals. The main set is chosen by duration and intensity; surges under 2 min and warm-ups labelled WORK are listed but never averaged in. It uses time-weighted means, avg/max HR per interval and a reference pattern (from a reference or the newest activity), and defaults to the sport family. New filters: gear, interval length, intensity, reps, FTP range. Power, HR, cadence, W/bpm and whole-activity RPE trends are reported separately, and gear / power meter differences are flagged. - **Custom field aggregation** (`get_training_summary`): a generic policy based on units, meaning and the definition. Percentages, ms, cm, scores and estimates are never summed; device loads are summed as a separate scale; values from sports without the field are ignored; stamina start/end pairs report the typical change. `CUSTOM_AGGREGATE_OVERRIDES` and `detail_level` are new. ## P1 - `get_wellness_trends`: requested period, lookback and baseline are stated separately; native units are shown; a stored 0 counts as missing; small samples are flagged. - `analyze_climbs`: sport profiles; no grade over tiny horizontal distances; implausible grades are flagged; real pauses are separated from slow movement; VAM uses moving time; the device moving time is shown for comparison only; raw grade is optional. - `find_similar_intervals`: reference activity, sport-family default (`sport_types="all"` for cross-sport) and a comparability score with gear / power meter / FTP context. - `get_power_hr_efficiency`: a minimum number of independent activities per group, comparison with the day-to-day variation, time-weighted bands, per-gear trends, and filters for gear, indoor/outdoor and interval position. ## P2 - `get_fatigue_resistance`: detects missing kJ configuration; it then shows no pseudo values, explains the settings and suggests thresholds without saving them. Missing and identical fatigued curves are detected. - `get_activity_report`: `detail_level` compact / standard / full. No power-meter comparison without a usable second stream, and identical streams are reported as such. Gear positions are no longer called tooth counts. - `get_best_efforts`: positions are elapsed time, and windows that span a pause are flagged (the old "pauses excluded" wording was wrong). - `get_activity_intervals`: optional planned step types. `get_training_plan`: unknown vs none. `get_library_workout`: full synthetic tests and a fallback for malformed steps. ## Testing - `ruff`, `mypy src tests` and `pytest` (333 tests) pass; the pylint E/F gate exits 0. - Every fixed bug has a synthetic regression test. - Live read-only checks against the test activities with the acceptance calls passed. For i195458386 with the 65-minute plan: all 7 steps matched, the cool-down is evaluated on its first 10:00, and the 22:07 of riding after 1:05:17 (samples 3903-5230, 25 % of kJ, incl. the 50 s 432 W sprint) is reported separately. - No writes were made to Intervals.icu. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
…key_jwt, RFC 9207, /mcp + /sse (#3) ## Summary OAuth for remote MCP clients done properly, stacked on #2 (base `feat/phase2-analytics-release`). ChatGPT and Claude only support "no auth" or OAuth. Intervals.icu cannot be their authorization server (no discovery metadata, no PKCE, no client registration), so this server is the authorization server for the MCP client and delegates the identity check to Intervals.icu: ``` ChatGPT / Claude <- OAuth 2.1 (PKCE, CIMD / DCR, RFC 9207 iss) -> this server this server <- OAuth 2 "Continue with Intervals.icu" ------> Intervals.icu (identity only) this server <- API key (data access, unchanged) -----------> Intervals.icu ``` ### What's new - **Sign in with Intervals.icu** (`OAUTH_LOGIN=intervals`), restricted to `OAUTH_ALLOWED_ATHLETES` (default `ATHLETE_ID`). The upstream token is used for the athlete-id check only and never stored. Password sign-in remains as alternative/fallback. - **Consent page** with verified client name for metadata-document clients and per-connection permission scopes (`intervals:read`, `intervals:write`, ...), enforced in `tools/list` (hidden) and `tools/call` (refused). `MCP_PERMISSIONS` stays the upper limit; tokens issued before carry only `mcp` and keep the server-wide permissions. - **Client ID Metadata Documents** (MCP spec 2026-07-28, preferred by ChatGPT) from allowlisted hosts with **private_key_jwt** verification (JWKS from the document, audience, lifetime, replay protection). Dynamic client registration stays for older clients, now limited to allowlisted redirect hosts and optionally disabled. - **RFC 9207 `iss`** on every authorization response, including the SDK's error redirects, so ChatGPT can use its stable redirect URI; audience check on access tokens; extended AS/PRM metadata. - **`MCP_TRANSPORT=http+sse`**: `/mcp` (streamable HTTP) and `/sse` + `/messages/` from one process behind the same auth. ### Verification - 325 tests (24 new in `tests/test_oauth_extensions.py`), ruff, mypy clean; pylint E/F gate clean. - Local end-to-end run of the real entry point: 401 + metadata discovery, registration, consent, password sign-in with read-only grant, token, `/mcp` and `/sse` sessions (42 tools, no write tools visible), status output; the existing no-auth secret-path configuration still serves all 49 tools. - The Intervals.icu sign-in is covered with a faked token endpoint; the live check follows once the Intervals.icu OAuth app is approved. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
futureweb
added a commit
that referenced
this pull request
Oct 9, 2026
## Futureweb Intervals MCP 1.0.0b1 First public beta of the Futureweb fork, the continuation of mvilanova/intervals-mcp-server. This PR brings everything from the stacked PRs to `main`: - **#1 coaching release:** P0 fixes (event endpoint, event types, readiness scale, local/UTC times, load labels, NaN/zero handling), custom fields and streams for any device data, athlete/sport settings/zones, recovery snapshot, wellness trends, nutrition, training summaries, execution analysis, climbs, power meter comparison, workout validation, gear, permission classes (read/write/destructive/admin), reviewed upstream community PRs, packaging, CI, docs. - **#2 phase 2:** performance analytics (best efforts, similar intervals, histograms, repeated workouts, Pw:HR efficiency, fatigue resistance), execution analysis for deleted and extended workouts, one-call activity report, detail levels, training plan, library workout, prompts and resources, pinned CI with build and Docker smoke tests, PEP 440 release handling. - **#3 OAuth:** "Continue with Intervals.icu" sign-in for allowlisted athletes, consent page with per-connection permission scopes, Client ID Metadata Documents with private_key_jwt (ChatGPT), RFC 9207 iss, audience checks, streamable HTTP and SSE in one process, PyJWT 2.15. - **#6 phase 3:** analytics quality: extended rides split correctly, only comparable intervals compared, custom field aggregation by meaning, wellness periods and units, minimum sample sizes, honest fatigue resistance, smoothed climb grades, compact report. - New README, CHANGELOG, SECURITY, REMOTE_ACCESS, GARMIN_BRIDGE, FEATURE_COMPARISON and UPSTREAM_AUDIT docs. 358 tests, ruff, mypy, pylint gate and CI (Python 3.12/3.13, build, Docker smoke) green. Live checks against a real account were read-only. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Phase 2 on top of #1 (base branch
feat/coaching-tools; retargets tomainautomatically once #1 is merged).P0 release blockers
setup-uv@v10did not resolve). CI now also builds sdist/wheel (import +--versioncheck) and builds/smoke-tests the Docker image on every PR.v1.0.0b1,v1.0.0rc1) are classified as pre-releases,latestandmajor.minorGHCR tags only for final versions, explicit tag list instead ofmetadata-actionsemver parsing, tag must matchpyproject.toml.FASTMCP_MESSAGE_PATH, secret-path deployment guide (docs/REMOTE_ACCESS.md), built-in single-user OAuth 2.1 authorization server (MCP_AUTH=oauth: dynamic client registration, PKCE, login page, rate limiting, persisted clients; 42 tests). The status tool reports the auth mode.P1
analyze_workout_execution:planned_workout_doc(compare against a deleted event's plan), detection of training beyond the plan (reported as additional training, not as poor compliance), read-only match suggestions for unpaired activities; tests with a 65-min plan ridden in 87 min.get_best_efforts,compare_best_efforts,find_similar_intervals,get_activity_histogram,compare_workouts,get_power_hr_efficiency,get_fatigue_resistance(20 tests).get_training_plan(ATP phases, weekly targets, races, fitness-model events),get_library_workout,update_sport_settings(admin, validated), gear maintenance reminders,get_activities(power_meter=...).activity_field_ids), power meter identity never derived from stream names, "unknown" when device data is missing, stamina vs potential stamina and gear index notes documented, loads kept apart.P2
detail_levelcompact/standard/full for details, intervals and recovery snapshot; interval stream statistics limited to requested streams.get_activity_report: one-call compact analysis (overview, plan vs execution, power meter check, climbs, data notes; 3–4 API calls).Tests
299 passed, ruff and mypy clean, pylint advisory. Live read-only checks against a real account for all new tools (including the extended threshold ride of 2026-10-09) and end-to-end through the SSE transport on the secret path.
🤖 Generated with Claude Code