Using an MCP Server to Analyse Daily Routine and Training
This post is different from the usual content of this blog. There is no MPLS label stack and no BGP session here. But there is a protocol, there is structured data, and there is a lot of troubleshooting, so it still belongs.
I have two long-running interests: networks and protocols, and cycling. For most of my life they never touched each other. In 2026 they finally did.
If you are here for the technical part, it starts at What an MCP server is. Everything before that is how a hobby turned into a dataset.
The short version of what this post covers:
- MCP explained through a NETCONF analogy, and how to run the Garmin server
- The dependency pin that stops it failing with
error -32000 - An FTP calculated with the wrong method, and used for months
- A VO2 Max curve read as a curve, and what it actually measures
- Why an UNBALANCED HRV status is a trend and not a verdict
Where the cycling started
When I was between 15 and 18 years old I lived in Barquisimeto, Venezuela. That is where I discovered mountain biking for the first time. Bikes still used 26-inch wheels, and I usually rode with a group of mixed ages and very mixed fitness levels.
The reference route of that group went to Terepaima National Park, in the neighbouring city of Cabudare. The climb to the antennas starts around 500 m above sea level and finishes at approximately 1240 m. We also had a fully paved route with a serious profile. It started at El Cardenalito in Barquisimeto, where the motorway begins, dropped down the Intercomunal to the centre of Cabudare, and then turned onto the road that climbs to Río Claro. The first section gives away 170 m of descent for free. The second is a continuous 750 m of ascent.
Río Claro sits roughly 580 m above the starting point, but the legs never care about the net figure. They pay for the full 750 m.
Then life moved on and the bike stopped.
Restarting in the Netherlands
I moved to the Netherlands in November 2020, in the middle of the pandemic. In January 2021 I decided to bring the hobby back, and I bought a Trek Procaliber 9.5 with a carbon frame and 29-inch wheels. I am 1.96 m tall, so the frame is size XXL, which is not something you find easily.
The terrain here is nothing like Venezuela. There is no real elevation. But between Eindhoven and Helmond there is a small climb that local riders call the Dak van Brabant, the roof of Brabant, and that became the regular training hill.
That year I set the goal of riding the interior of Noord-Brabant, and also Limburg, where the highest point of the European part of the Netherlands is located, at the tripoint between the Netherlands, Belgium and Germany near Vaals.
At the end of 2021 I upgraded the bike almost completely, replacing aluminium parts with carbon ones:
- Crankset: e*thirteen
- Wheels: Bontrager Kovee Pro carbon
- Cockpit: carbon handlebar and seatpost
- Fork: from a RockShox Judy SL to a RockShox SID SL Ultimate Race Day
- Drivetrain: from Shimano Deore to Shimano XT
For several years I rode that bike around the Netherlands, then stopped again for a while because I was travelling and getting to know a large part of Europe. Still, a long ride always appeared on a weekend or at the end of a working day.
2025: the road bike and the sensors
In August 2025, to celebrate five years in the country where I recovered this hobby, I gave myself an early birthday present (my birthday is in December): a Trek Madone SL7 Gen8, full carbon, with Ultegra Di2 electronic shifting. It was my first road bike.
The frame is size XL, one below the XXL of the mountain bike, because road geometry is longer and lower and the fit works out differently. The wheels are a different size as well, 700c instead of the 29-inch of the mountain bike. Those two labels describe the same 622 mm rim, they just come from different traditions, but the tyre changes everything around it: a 28 mm road tyre against a 2.2 inch mountain bike tyre is a completely different contact patch, pressure and rolling behaviour.
The hobby came back with much more force than I expected, and with it came the instrumentation:
| Device | Purpose |
|---|---|
| Garmin Edge 850 | Cycling computer, recording and live pacing |
| Garmin HRM600 | Chest strap, heart rate and running dynamics |
| Favero Assioma PRO RS-2 | Power meter pedals, road |
| Favero Assioma PRO MX-2 | Power meter pedals, MTB |
| Wahoo Kickr Core 2 | Indoor smart trainer for winter |
| Garmin Vivoactive 6 | Sleep quality, steps, daily load |
At the beginning of 2026 I started training in a structured way, combining indoor sessions, mountain bike and road.
And that is where the real problem appeared.
The problem: sensors produce numbers, not decisions
After a few years of enjoying life and travelling across Europe, some of my values were clearly outside a healthy range. The sensors made that visible immediately. What they did not do was tell me what to change.
Garmin Connect shows everything: VO2 Max, acute and chronic training load, ACWR, Body Battery, HRV, sleep stages, time in heart rate zones, time in power zones. But it shows them one screen at a time, and the interesting questions always cross screens.
For example:
- Did last night’s sleep get worse because of the training session, or because of what I drank at 18:00?
- My ACWR is 1.4 and the label says OPTIMAL. Is that the same OPTIMAL as three weeks ago, when my chronic load was 60 points lower?
- The session felt terrible. Was that fatigue, or was it the short night before it?
Answering these by clicking through a web interface is slow, and because it is slow, you stop doing it.
What an MCP server is
For a networking audience the short version is easy: the Model Context Protocol (MCP) is a standard way for an LLM to talk to an external system. The LLM I use for this is Claude, but the protocol is open and the same server works with any client that speaks it.
The comparison I find most useful is NETCONF. A router does not expose its operational state by letting you guess; it publishes a set of capabilities and a data model, and the client makes typed requests against it. MCP works with the same idea. An MCP server connects to some backend, advertises a list of tools with typed parameters, and the LLM calls them.
Claude is the client. The server is the adapter in front of the real system. The transport in a local setup is simply stdio between the two processes.
The important consequence is that Claude does not scrape a web page and does not guess. It calls a documented function and receives structured JSON back.
The Garmin MCP server
The project I use is Taxuspt/garmin_mcp. It wraps the python-garminconnect library and exposes more than 110 tools. Authentication is done once with garmin-mcp-auth, which stores OAuth tokens in ~/.garminconnect; the tokens last around six months and MFA is supported.
These are the tools I end up using almost every day:
| Tool | What it answers |
|---|---|
get_training_status |
ACWR, acute and chronic load, VO2 Max, Load Focus, training status |
get_activities_by_date |
What did I actually do, and when |
get_activity |
Average and normalized power, max power, HR, training effect, training load |
get_activity_power_in_timezones |
How much time was really spent in each power zone |
get_activity_hr_in_timezones |
The same for heart rate zones |
get_sleep_summary |
Duration, score, deep/REM percentages, sleep stress, overnight HRV |
get_rhr_day |
Resting heart rate |
get_vo2max_trend |
The long curve, with a flag for carried-forward values |
get_body_battery |
Energy reserve before and after a session |
There are many more, covering workouts, courses, gear, nutrition and FIT file parsing, but the nine above cover almost every question I ask.
The configuration is a normal MCP client entry:
{
"mcpServers": {
"garmin": {
"command": "uvx",
"args": [
"--python", "3.12",
"--from", "git+https://github.com/Taxuspt/garmin_mcp",
"--with", "mcp<2",
"garmin-mcp"
]
}
}
}
One troubleshooting note that will be familiar to anyone who has debugged a dependency problem at three in the morning. At some point the server stopped starting and returned only error -32000. The actual cause was in the process output:
ModuleNotFoundError: No module named 'mcp.server.fastmcp'
The mcp SDK had released version 2.0.0 and removed that module, and uvx was happily resolving the newest version on every run. Pinning --with "mcp<2" fixed it. It is worth keeping that pin in mind, because the failure looks like a server problem and is really a dependency problem.
A typical training week
For the indoor sessions I use a subscription to Biketerra with the Kickr Core 2. One day I described every pre-loaded workout in that platform in detail inside a session with Claude, together with my real availability (after work on weekdays, mornings on weekends), and from then on the planning had a solid base.
A normal week looks like this:
| Day | Session | Purpose |
|---|---|---|
| Monday | Soonami Recovery | Active recovery, heart rate below 120 |
| Tuesday | VO2 Max, Emily’s Short Mix | Short repetitions above threshold |
| Wednesday | Rolling Hills 30 | Endurance with variation |
| Thursday | Off | Absorb the week |
| Friday | Soonami Recovery | Loosen the legs before the weekend |
| Saturday | Long ride, 50 to 80 km | Prolonged Zone 2 |
| Sunday | Off | Recovery |
The weekday constraint is structural and not negotiable: I cannot train in the morning during the working week. Every weekday session happens between 17:00 and 20:00. Adding that fact to the planning context removed a whole category of suggestions that would never have worked for me.
The VO2 Max curve
This is the part that makes the effort visible.
VO2 Max (ml/kg/min), cycling, 2026 bars start at 35.0
8 Mar ##### 36.3 <- low point
31 Mar ########## 37.5 +1.2
30 Apr ############### 38.8 +2.5
31 May #################### 40.0 +3.7
29 Jun ############################## 42.6 +6.3
31 Jul ########################################## 45.5 +9.2
1 Aug ############################################ 46.0 +9.7 <- peak
The low point was 36.3 on 8 March 2026 and the peak was 46.0 on 1 August, which is an improvement of 9.7 points, around 27 per cent, in less than five months. The current value is 45.6, which is normal: the estimate drops after a hard session and recovers during the easy days, so the number oscillates by a few tenths around the plateau.
The shape of the curve is worth reading. Progress was almost flat between March and May, about 1.2 points per month, and then accelerated from June onwards. That is not motivation improving. That is the moment when the training stopped being a collection of sessions and started being a structure, with recovery days planned instead of improvised.
It is also worth saying what the number actually measures, because that is where the improvement really is. Garmin estimates cycling VO2 Max from the relationship between the power you produce and the heart rate it costs you. The estimate only moves when that ratio moves.
So going from 36.3 to 46.0 is not the story of riding more hours. It is the story of the same effort costing fewer beats. Today I hold 173 W with a heart rate that five months ago would have been the price of considerably less, and the resting heart rate fell from the mid forties to 43 along the way.
get_vo2max_trend is what makes that progression legible at all. It returns one value per day across the whole period, so the curve can be read as a curve instead of as a number that happens to be on the screen today.
What the data actually changed
Three concrete examples, all of which came from asking questions that cross several screens of the app.
The FTP was wrong, and the method was wrong
Functional Threshold Power is the number every training zone is derived from, so an error there propagates into every session. Mine moved three times in seven months, and only one of those movements had anything to do with fitness.
| Date | Test | Peak power | NP | Method | FTP |
|---|---|---|---|---|---|
| 22 Feb 2026 | Half Monty, Wahoo SYSTM | 257 W | 135 W | Platform algorithm | 201 W |
| 14 May 2026 | Ramp test | 258 W | 179 W | 258 × 0.75 | 194 W |
| 2 Sep 2026 | Ramp test | 253 W | 179 W | Best 1-minute average × 0.75 | 190 W |
The first one was a Half Monty: a ramp to exhaustion followed by a steady segment held at constrained heart rate, with the platform applying its own algorithm to both halves. It returned 201 W, and that became the basis of my zones.
The second one is where the mistake is. On 14 May I took the ramp test result and multiplied by 0.75, but I used 258 W, the peak instantaneous power of the test. The protocol uses the best 1-minute average, which was 252 W. That gives 189 W, not 194 W. The zones stayed roughly 3 per cent too high for the next 111 days, which is enough to turn a sweet spot session into a threshold session without noticing.
What made the correction safe to apply was the normalized power column. Two ramp tests three and a half months apart returned 179 W both times, with peak power of 258 W and 253 W. The raw output of the test had not moved, so the difference between 194 and 190 was arithmetic, not form.
That is also the kind of thing you only see if you can put two activities side by side and compare the same field. In the app they are two entries in a list, months apart, and nobody scrolls back to check whether a number they already trust was ever calculated correctly.
The wider lesson is the one I keep coming back to: three numbers produced by three different methods are not a trend. The 201 W from February is not comparable to the ramp test values at all, because it came from a different protocol on a different platform. Comparing them as if they were the same measurement would have told me I was losing fitness during the exact months when my VO2 Max went up by seven points.
Social occasions have a timestamp
Outside of cycling I have two other long interests, and both of them are about sitting down with friends around something that took a long time to make.
The first is coffee. Living in Brazil gave me access to micro-lots that never reach most of the market, and that is where a habit turned into something more serious. A weekend now often includes meeting friends for a filter brew, V60 or Chemex, usually a coffee with experimental fermentation, at Sprout Coffee Art here in Eindhoven or at Dak in Amsterdam. What you are there for is the cup and the conversation about it, not the caffeine.
The second is Lambic. Living in Eindhoven puts me close to the Zenne Valley and the Pajottenland, the small region west of Brussels where these beers are still made by spontaneous fermentation, and where breweries like Cantillon and 3 Fonteinen age their Lambics in oak for one to three years before blending young and old batches into a Gueuze. Going there is a day out: a tour, a tasting, a conversation about wood, blending and vintages. It is closer to visiting a winery than to going out for drinks, and it happens on specific dates, often around events such as the Toer de Geuze or a brewery release day.
What the data added was not a judgement. It was a clock.
Crossing sleep records with what a given day actually contained showed that my lowest sleep scores all share the same shape, and it is a combination, not a single item: filter coffee in the middle of the afternoon, followed by a tasting in the early evening.
The hour is what matters, and it is easy to underestimate. Caffeine has a half-life of five to six hours. Three cups at 15:00 still leave roughly a third of the dose circulating at midnight, which is the equivalent of drinking an espresso on the way to bed. The same three cups at 10:00 are almost entirely gone by then. Alcohol is fully metabolised long before bedtime, but it still suppresses REM early in the night and causes rebound awakenings in the second half.
Either one on its own is almost invisible in the numbers. Together they take a normal night of score 85 to 90 down below 50, with a clear rise in overnight stress.
The conclusion was not to give anything up. It was to stop placing those days at random. A tasting on a Sunday is now simply a day that is not followed by a key session on the Monday, and when the week has something important in it, the coffee moves to the morning. Same cup, same people, different hour. The calendar absorbed the information, not the enjoyment.
The HRV status is a trend, not a verdict
On 12 September I woke up with my HRV status showing UNBALANCED, and spent half the morning deciding whether to ride at all.
The plan was the route from Maastricht to Vaals on the mountain bike, out and back, past a series of castles and finishing at the tripoint where the Netherlands, Belgium and Germany meet.
Limburg is the only part of this country with real climbing, and the numbers say how much. The outbound leg is 36.9 km with 541 m of ascent, and the device classified 14.9 km of it as unpaved, spread over fifteen separate segments of gravel and farm track. The return from Vaals is 31.1 km and entirely paved, mostly downhill.
Across the day it recorded eight climbs, two of them categorised. The longest was 2.5 km averaging 5.1 per cent, and the steepest ramp touched 16.8 per cent, with power peaking above 500 W to get over it. None of that exists anywhere else in the Netherlands.
Everything except the HRV status disagreed with staying home: 8h22 of sleep with a score of 91, resting heart rate at 45, body battery full. The status itself was sitting one point below the floor of my own range.
I went, and it became the longest day I have done so far: 71.3 km with 768 m of climbing.
The status is a seven-day average compared against a rolling baseline. It moves slowly by design, which makes it useful for trends and close to useless as a verdict on a single morning.
The cost appeared where it should, and it was proportionate. The night after the ride had normal duration but poor composition, 14.6 per cent deep sleep and overnight HRV down to 38. Two nights later it was back to 51, higher than before the ride.
The rule I use now: when the HRV status says one thing and sleep, resting heart rate and body battery all say another, the majority wins.
What comes next
The current objective is a ride of 100 km or more, starting in Eindhoven and finishing in Utrecht, Rotterdam or Amsterdam. The Eindhoven to Utrecht route is about 99 km, and the return is a 50-minute train ride with a day bike ticket.
It has been postponed a couple of times so far, for reasons ranging from issues in the building where I live to a heart rate variability reading I did not like. The progression is now in place, and the attempt is scheduled.
One clarification belongs here, because the amount of data in this post could give the wrong impression. I am not a professional cyclist and I am not training for competition. I ride recreationally, because I enjoy it and because it is how I look after my health. The structure exists to keep me healthy and to stop me repeating the same mistakes, not to win anything. If a ride turns into a day out with friends instead of a session, that is not a failure of the plan.
That part is not abstract either, and it is the reason the whole thing keeps going. I turn 40 in December, and like most people who start moving again around that age, the difference is something you feel long before you read it anywhere. Garmin puts a number on it regardless: my Fitness Age is 33.6 against a chronological 39, a gap of more than five years, and it has been sitting between 32 and 33.5 for months. The same field reports an achievable fitness age of 33.6 as well, which is the model’s way of saying there is not much left to squeeze out of it.
That is also why the tooling mattered as much as it did. A professional has a coach, a laboratory and a team reading the data for them. Someone like me has a smart trainer, a watch, and whatever time is left after work. MCP is what closed that gap, because it let me ask questions about my own body that would otherwise have needed somebody else to answer.
And, like everybody else, the rides end up on Strava. Most of the time I do not upload anything by hand: Garmin Connect is linked to the account, so the activity appears there as soon as the session finishes and synchronises.
The general lesson of this year is not about cycling. It is that instrumentation without analysis is just storage.
Two things had to line up before any of this worked, and in 2026 they finally did at the same time. The sensors arrived between 2025 and 2026, in stages: heart rate before power, then power on the road bike in July 2026, then on the mountain bike a month later. The MCP server was there from the beginning of the structured training, at the start of 2026.
That is why the chart above is not a coincidence. Every one of those months was planned, reviewed and corrected against the data instead of ridden by feel, and the analysis was available from the first week rather than added at the end.
What I think about is the years before that. I restarted cycling in 2021 and rode until 2025 with almost no data, and those seasons are simply not readable today: no power, no structure, no way to ask whether a bad week was fatigue, heat or a short night. If the sensors and the protocol had come earlier, the motivation and the analysis would probably have come with them, and the curve would have started from a different place.
Still, the part that transfers is simple enough. What changed was not the sensors. It was having a protocol that lets something query them properly, and the discipline to write down what the answers actually said, including when they contradicted what I wanted to believe.
References