Zum Inhalt springen
A watch, a phone and a stack of files whose light trails flow into a shared node, as a glowing low-poly illustration.
All posts
Series: training mcpPart 2 of 4
·AIMCPLaravelTraining

How the watch gets into the chat

51 tools, five channels for training data and a dedicated app for Apple Health: how training mcp brings sessions, vitals, goals and complaints into the conversation with the language model.

A model that doesn't know the data guesses. The second part is about how the data gets there without anyone copying screenshots.

The first part covered the approach: an MCP server that gives the user's language model data and knowledge, without a chat of its own. Now to the data. It travels in two directions. What the watch measures has to land on the server reliably. And what is stored there has to reach the model in a form it can work with, without filling in anything by itself.


The server: one endpoint, 51 tools

training mcp is a Laravel application, and the MCP part runs on the official laravel/mcp package. There is exactly one endpoint, /mcp, over HTTP. Clients such as Claude or ChatGPT sign in via OAuth: they register themselves (RFC 7591), send the user to a consent page and then receive a token with the scope mcp:use. For Claude Code, scripts and clients without a browser flow there is a personal token.

A tool is a class. Name, description and whether it only reads are attributes on it:

#[Name('absences')]
#[Title('Absences')]
#[Description('Holidays, travel, illness and injuries overlapping a period, with the equipment profile available during travel. Without from/to the next 60 days. Call it before planning.')]
#[IsReadOnly]
#[IsIdempotent]
class AbsencesTool extends TrainingTool
{
    protected function run(Request $request, User $user): Response|ResponseFactory
    {
        // Resolve the period, load absences, return them structured
    }
}

The description is in English and is aimed at the model, not at people. Its last sentence, "Call it before planning", is there on purpose. A model only calls a tool when it sees a reason to, and the description is the place to give it that reason.

There are now 51 tools, 31 of them read-only and 20 that write. They range from activities and daily-vitals through training-load and readiness to plan-week and note-session. Which ones an account sees depends on the subscription: tools/list returns only the enabled ones, and the call is checked a second time. Without a subscription exactly one remains, account-status. That sounds like little, but it stops a model without data from starting to guess numbers.

What is easy to overlook is how much room the tool list takes up in the context. Every tool has an output schema, and with all levels included the list came to 138,000 characters. Now only the top level goes to the client, which brings it down to 96,000. The model still knows which fields come back and has more room for the conversation.


Five channels for the sessions

A product whose value is the user's data must not depend on a single interface. So there are several channels, and they have different strengths.

The most important channel is intervals.icu. Garmin, Coros, Wahoo, Polar, Suunto, Zwift and quite a few more come in through it, connected via OAuth. intervals.icu reports new sessions by webhook. Because webhooks do get lost now and then, a job runs twice a day at 5:00 and 17:00 and fills in what is missing.

Garmin direct is built, as a push to a dedicated endpoint, and the FIT file is fetched immediately. The channel isn't released yet. Garmin's terms require written consent for passing data to language models, and until that is in place, Garmin users go through intervals.icu.

Apple Health only works through an app on the phone, more on that below. RunGap sends sessions by webhook, signed with HMAC.

FIT files can be uploaded on the web, including as a whole ZIP like Garmin's data export. That is the channel for the history of past years.

Strava is left out deliberately. Since June 2026 its API terms prohibit passing data to AI applications, and a product that does exactly that had better not build on it.

Behind every channel sits the same pattern: accept immediately, store the raw data unchanged, put a job on the queue and respond with 202. A separate worker then reads the FIT file or the JSON and creates the session. If something breaks during parsing, the raw data is still there and can be read in again.


The same session through three channels

Someone with a Garmin watch who uses intervals.icu and installs the app for Apple Health gets every run delivered three times. The model may only count it once, otherwise the weekly volume triples and every load metric is wrong.

Recordings are merged when their start times are at most two minutes apart and their durations differ by at most 15 percent or five minutes. A second rule covers recordings that start later or end earlier: if at least 80 percent of the shorter one lies within the longer one, it is the same session.

A fixed order decides which version leads. Completeness counts first, then data quality (a FIT file beats a summary without a file), and only then the channel:

'source_rank' => [
    'garmin',
    'garmin_export',
    'intervals_icu',
    'rungap',
    'upload',
    // ...
    'apple_health',
    'health_connect',
],

All versions stay stored, the leading one is counted. A triathlon gets finer treatment: there the individual legs are matched. The watch provides the frame, the data channels come from the richer version. Nothing is filled in or estimated along the way.


The app for Apple Health

HealthKit has no interface to the outside. To get at the data you need an app on the phone that the user authorises. The training mcp app is built with Expo and React Native, HealthKit comes in via @kingstinct/react-native-healthkit, and on Android Health Connect via its counterpart. Apart from the generated scaffolding there is no native Swift.

On first launch the app reads in twelve months. After that HealthKit reports new data via background delivery, and a background task catches up on whatever was left behind. iOS decides when that task runs. That is the part you have no control over, which is why the app shows clearly when it last synced.

Every entry goes to the server with its HealthKit UUID, so a second upload doesn't create duplicates. And whatever already arrives through another channel, the app doesn't upload in the first place. The server tells it which sources to skip, for example Garmin Connect, which also writes its sessions to Apple Health.


What the model receives

The data is on the server now. For the model to use it correctly, the same rules apply to all tools.

Timestamps come in UTC, with the local time in the account's time zone next to them. A run at 6:30 should be 6:30 in the conversation too, not 4:30. Periods are given as YYYY-MM-DD, and without one the last seven days apply.

Every record names its source. When a response carries an attribution field, the model is supposed to name that source when it presents the values. Garmin requires this, and it does no harm with any source.

What is missing stays missing. If a session has no power data, the field is empty and there is no zero. A model that sees a zero calculates with it.


What no watch measures

Goals, complaints, holidays, fixed appointments, the note after a session: only the user knows these, and they should outlast the conversation. That is what the writing tools are for, with three rules written into the instructions for the model.

Only what the user said or agreed to is stored, never measured values. Weight, distance and heart rate are already in the raw data, and a copy of them would drift apart sooner or later.

Nothing is deleted. What no longer applies is ended or replaced by a new version, and the history stays readable.

And nothing is overwritten silently. If the model writes something to a key that differs from what is already there, a conflict comes back and the model has to ask.

One lesson only came in operation. On 13 October I noticed that Claude and ChatGPT were planning weeks without knowing the fixed appointments. They were stored, but in a place no tool pointed to. The fix is planning-context: one call that bundles everything a week is constrained by. Profile, weekly template, current phase, absences and what is already planned. Its description says it comes before any planning, and plan-week checks the stored week against it. Anything that doesn't fit comes back as a warning the model has to show the user.


Conclusion

The MCP protocol was done quickly. The package takes most of the work off your hands, and a tool is written fast. What took time was the data behind it: five channels that can all fail, a session that arrives three times, an app whose background work is scheduled by the operating system. On top of that come the descriptions that tell a model when it needs which tool.

The third part is about the second gap: training knowledge, and how to get a model to look there before it answers.


Call to Action

training mcp is in beta with a small group of users, the public launch is in early November. More at https://training-mcp.app.

If you are building your own MCP server with Laravel: the questions I spent the longest on were the tool descriptions and the size of the tool list. If you have experience with either, write to me.