Automation · 2026
A self-hosted training analytics platform for cyclists. It pulls ride data straight from Garmin, stores it on hardware I control, computes the same metrics a coach would look at, and lets me ask Claude real questions about my training in plain language, backed by my actual numbers.
Garmin collects an enormous amount of training data and then makes you dig for it one screen at a time. There's no way to own the raw numbers, run your own analysis, or ask a real question about trends across months of riding without exporting things by hand. I wanted the data to actually be mine, sitting in a database I control, with the same coaching metrics a real coach would calculate, and I wanted to be able to ask about it directly instead of copying numbers into a chat and hoping the context was right.
So I built a pipeline that pulls everything in, computes it properly, and hands it to Claude through an MCP server. The model doesn't guess at my training, it queries the actual data store and answers with real figures behind it.
The pipeline authenticates to Garmin through a token session, pulls activities and wellness data, and parses the raw ride files. Everything gets archived immutably by date with a checksum, so the raw data can never silently change under me. From there it deduplicates overlapping recordings, a Zwift ride and a paired watch recording of the same session, for example, and calculates what actually matters: normalized power, training stress score, the CTL/ATL/TSB load model, power zones, a mean-maximal power curve, and aerobic decoupling. Outdoor rides also get GPS-based terrain and climb analysis.
A reporting layer turns all of that into a weekly or monthly summary and can email it out on its own schedule. A doctor command checks the health of the whole chain in one shot, including how many days are left on the Garmin token, so nothing fails silently in the background. And the MCP server puts all of it in front of Claude, so I can ask about training load, terrain, or trends and get an answer grounded in my actual numbers.
Four layers, each one feeding the next:
Ingestion authenticates to Garmin, pulls activities and wellness data, archives the raw files, and loads everything into a local SQLite database. Compute deduplicates overlapping recordings and derives everything downstream of the raw data, power and heart rate metrics, training load, zone distribution, benchmarks, and terrain. Serving is a query layer sitting between the database and two consumers, a CLI and an MCP server exposing the same data to Claude as a set of tools. Reporting builds the weekly and monthly summaries and can deliver them by email automatically.
A lot of hobby projects in this space store a password in plaintext somewhere, a Garmin login, an SMTP password pasted into a config file. This project doesn't do that anywhere in the stack.
No stored Garmin password
Garmin access is token based. Login is a one-time interactive step, and the result is a token, not a password. Nowhere in the codebase or the deployed system is a Garmin password ever written to disk.
Mail credential, encrypted at rest
A scoped app password, not the real account login, encrypted immediately with a machine-local key. If the box is compromised, the exposure is one revocable app password, not the email account itself.
Dedicated system user
The whole pipeline runs under its own system user, created at install time, not root and not whoever's logged into the box. Least privilege, not a slogan.
Locked file permissions
The config file holding the encrypted credential, and the Garmin token store, are both owner-only. Not readable by anyone else on the box.
None of this is theoretical. Every piece described was actually built and tested, not just planned. The token expiry gets checked by a doctor command so an expiring token shows up as a clear warning ahead of time instead of a silent sync failure weeks later, the mail wizard sends a live test email before it ever declares setup successful, and the repository's own .gitignore blocks the token store and the encryption key from ever being committed, on top of those files living outside the repo path entirely. Defense in depth, not one control doing all the work.
A real question, asked in plain language, answered from the actual data store:
queried get_session_detail, get_load_status
Friday was the Volcano Climb in Zwift, 61 minutes, average heart rate 137 and a peak of 172, average power 119W with a normalized power of 132.7W.
The heart-rate-to-power ratio was the least efficient of your last three indoor rides, and about 22% of the ride sat in zones 5 through 7 despite the modest average power, which explains the high peak heart rate. That lines up with your current training stress balance of negative 10.4, you're fatigued right now, and this session shows it.
No copying numbers into a chat window and hoping the context is right. The model queried the database directly and answered with the real figures behind it.
The weekly report goes further, and it's caught real problems on its own. Here's an actual one, redacted of anything personal:
Weekly Deep-Dive: Jul 19–25 (Fatigued, TSB -10.4)
That's the system finding its own miscalibration, saying so plainly, and still delivering a correct read on the actual state of training underneath the bad number. Nothing hidden, nothing smoothed over.
This wasn't thrown together. Every phase of the build was tested with real execution against a real database before being called done, and real bugs got caught and fixed along the way instead of papered over. It has a solid foundation, and there's a long list of features still planned on top of it.
One installer brings the whole thing up. It runs in three phases, everything that needs no human first, a short attended window for Garmin login and mail setup, then a long unattended pull of your full ride history in the background.
# Clone the repo
git clone https://github.com/Baslocal/TopStrikeCCMCP.git
cd TopStrikeCCMCP
# Run inside tmux so the backfill survives a dropped SSH session
tmux new -s install
sudo ./complete_installer.sh
Once it finishes, the running system lives entirely under /opt/topstrikercc and keeps itself going on its own schedule from there. The cloned repo isn't needed again after install.