coros-mcp
# coros-mcp
[](https://github.com/cygnusb/coros-mcp/actions/workflows/ci.yml)
[](https://www.python.org/)
[](LICENSE)
[](https://github.com/astral-sh/ruff)
> [!IMPORTANT]
> **This is an unofficial, community-built project and is not affiliated with or endorsed by COROS.**
>
> COROS has since launched their **official MCP server**: [coroslab/COROS-MCP](https://github.com/coroslab/COROS-MCP).
> If you are looking for an officially supported integration, please use that instead.
>
> This project continues to exist as a community alternative and may offer different or complementary features.
A [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server that fetches sleep, HRV, and training data from the unofficial Coros API and exposes them to AI assistants like Claude.
**No API key required.** This server authenticates directly with your Coros Training Hub credentials. Your token is stored securely in your system keyring (or an encrypted local file as fallback), never transmitted anywhere except to Coros.
## What You Can Do
Ask your AI assistant questions like:
- "How much deep sleep and REM did I get last week?"
- "What was my HRV trend over the last 4 weeks?"
- "Show me my resting heart rate and training load for last week"
- "How many steps did I average per day this month?"
- "List my rides from last month"
- "Show me the details of my last long ride"
- "Create a 90-minute sweet spot workout for me"
- "What's on my training calendar next week?"
- "Schedule my VO2 workout for Thursday"
- "Create a 20-minute strength circuit with squats, lunges, and planks"
## Features
| Tool | Description |
|------|-------------|
| `authenticate_coros` | Log in with email and password — token stored securely in keyring |
| `authenticate_coros_mobile` | Log in to the mobile API only (useful for sleep data troubleshooting) |
| `check_coros_auth` | Check whether a valid auth token is present |
| `get_daily_metrics` | Fetch daily metrics (HRV, resting HR, training load, VO2max, stamina, and more) for n weeks (default: 4) |
| `get_sleep_data` | Fetch nightly sleep stages (deep, light, REM, awake) and sleep HR for n weeks (default: 4) |
| `list_activities` | List activities for a date range with summary metrics |
| `get_activity_detail` | Fetch full detail for a single activity (laps, HR zones, power zones) |
| `list_workout_templates` | List reusable workout templates saved in the library |
| `save_workout_template` | Save a reusable cycling/intervals workout template (named steps, power targets) |
| `save_strength_workout_template` | Save a reusable strength workout template (sets, reps, or timed exercises) |
| `delete_workout_template` | Delete a saved workout template from the library |
| `list_planned_activities` | List planned workouts from the Coros training calendar |
| `schedule_workout` | Schedule a one-off cycling/intervals workout for a date (no library entry) |
| `schedule_strength_workout` | Schedule a one-off strength workout for a date (no library entry) |
| `schedule_workout_template` | Schedule an existing library template on a calendar day |
| `remove_scheduled_workout` | Remove a scheduled workout from the calendar |
| `list_exercises` | Browse the Coros exercise catalogue, especially for strength workouts |
| `sync_coros_data` | Backfill all data into the local SQLite cache for a date range |
| `get_cache_status` | Show coverage (record counts and date ranges) of the local cache |
---
## Setup
### Option A: Auto-Setup with Claude Code
If you have [Claude Code](https://claude.ai/code), paste this prompt:
```
Set up the Coros MCP server from https://github.com/cygnusb/coros-mcp — clone it, create a venv, install it with pip install -e ., add it to my MCP config, then tell me to run 'coros-mcp auth' in my terminal to authenticate.
```
Claude will handle the installation and guide you through configuration.
### Option B: Manual Setup
#### Step 1: Install
```bash
git clone https://github.com/cygnusb/coros-mcp.git
cd coros-mcp
python3 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -e .
```
Or with `uv`:
```bash
uv pip install -e .
```
#### Step 2: Add to Claude Code
```bash
claude mcp add coros -- /path/to/coros-mcp/.venv/bin/coros-mcp serve
```
To limit the MCP to a specific project only (recommended):
```bash
cd /path/to/your/project
claude mcp add --scope project coros -- /path/to/coros-mcp/.venv/bin/coros-mcp serve
```
Or add to Claude Desktop (`~/Library/Application Support/Claude/claude_desktop_config.json`):
```json
{
"mcpServers": {
"coros": {
"command": "/path/to/coros-mcp/.venv/bin/coros-mcp",
"args": ["serve"]
}
}
}
```
#### Step 3: Authenticate
**Option A — `.env` file (recommended for project-scoped setups):**
Create a `.env` file in your project directory:
```
COROS_EMAIL=you@example.com
COROS_PASSWORD=yourpassword
COROS_REGION=eu
```
The server authenticates automatically on the first request and re-authenticates transparently whenever the token expires. No manual auth step needed.
**Option B — Manual authentication:**
Run the following command in your terminal — **outside** of any Claude session:
```bash
coros-mcp auth
```
You will be prompted for your email, password, and region (`eu`, `us`, or `asia`). This stores both the Training Hub web token and the mobile API token (used for sleep data). Your credentials are sent directly to Coros and the tokens are stored securely in your system keyring (or an encrypted local file as fallback). **You only need to do this once** — the tokens persist across restarts.
> **Note:** The mobile login (`apieu.coros.com`) will log you out of the Coros mobile app on your phone. If you want to avoid this, use `coros-mcp auth-web` instead — it stores only the web token, and the mobile token will be obtained automatically when you first request sleep data.
**Other auth commands:**
```bash
coros-mcp serve # Start the MCP server (used by Claude Code / Claude Desktop)
coros-mcp auth-web # Web API only — skips mobile login (sleep data obtained lazily)
coros-mcp auth-mobile # Mobile API only (sleep data)
coros-mcp auth-status # Check if authenticated
coros-mcp auth-clear # Remove stored tokens
```
#### Step 4 (optional): Restrict the toolset for autonomous agents
When the MCP client is an autonomous agent or a smaller open-weight model
(e.g. [hermes-agent](https://github.com/NousResearch/hermes-agent) with a
DeepSeek/Hermes model), two env vars harden the server:
```
COROS_MCP_TOOLSET=readonly # expose only the 12 read tools
COROS_MCP_HIDE_AUTH_TOOLS=1 # hide authenticate_* (credentials never enter the model context)
```
- `COROS_MCP_TOOLSET=readonly` hides everything that writes to your Coros
account (save/schedule/update/delete tools) plus the low-level raw escape
hatches, leaving: `get_help`, `check_coros_auth`, `get_daily_metrics`,
`get_sleep_data`, `list_activities`, `get_activity_detail`,
`list_workout_templates`, `list_training_plans`, `list_planned_activities`,
`list_exercises`, `sync_coros_data`, `get_cache_status`. A smaller tool
list also improves tool-calling reliability of non-frontier models.
Default: `full` (all 26 tools).
- `COROS_MCP_HIDE_AUTH_TOOLS=1` removes the two `authenticate_*` tools so
your password can never travel through the model context or agent logs.
Authentication then happens exclusively via the `COROS_EMAIL` /
`COROS_PASSWORD` env auto-login (or tokens stored beforehand with
`coros-mcp auth`).
All tools additionally carry MCP `readOnlyHint` / `destructiveHint`
annotations, so agent harnesses that honor them can require confirmation
for write actions.
---
## Tool Reference
### `authenticate_coros`
Log in with your Coros credentials. The auth token is stored securely in your system keyring (or an encrypted file as fallback).
```json
{ "email": "you@example.com", "password": "yourpassword", "region": "eu" }
```
Returns: `authenticated`, `user_id`, `region`, `message`
### `authenticate_coros_mobile`
Authenticate with the Coros mobile API only. This is mainly useful if you need to restore sleep-data access without redoing full web authentication.
```json
{ "email": "you@example.com", "password": "yourpassword", "region": "eu" }
```
Returns: `authenticated`, `user_id`, `region`, `message`
### `check_coros_auth`
Check whether valid web and mobile tokens are stored and how long the web token remains valid.
```json
{}
```
Returns: `authenticated`, `user_id`, `region`, `expires_in_hours`, `mobile_authenticated`, `mobile_token_status`
### `get_daily_metrics`
Fetch daily metrics for a configurable number of weeks (default: 4).
```json
{ "weeks": 4 }
```
Returns: `records` (list), `count`, `date_range`
Each record includes:
| Field | Source | Description |
|-------|--------|-------------|
| `date` | — | Date (YYYYMMDD) |
| `avg_sleep_hrv` | dayDetail | Nightly HRV (RMSSD ms) |
| `baseline` | dayDetail | HRV rolling baseline |
| `rhr` | dayDetail | Resting heart rate (bpm) |
| `training_load` | dayDetail | Daily training load |
| `training_load_ratio` | dayDetail | Acute/chronic training load ratio |
| `tired_rate` | dayDetail | Fatigue rate |
| `ati` / `cti` | dayDetail | Acute / chronic training index |
| `distance` / `duration` | dayDetail | Distance (m) / duration (s) |
| `vo2max` | analyse (merge) | VO2 Max (last ~28 days) |
| `lthr` | analyse (merge) | Lactate threshold heart rate (bpm) |
| `ltsp` | analyse (merge) | Lactate threshold pace (s/km) |
| `stamina_level` | analyse (merge) | Base fitness level |
| `stamina_level_7d` | analyse (merge) | 7-day fitness trend |
### `get_sleep_data`
Fetch nightly sleep stage data for a configurable number of weeks (default: 4).
```json
{ "weeks": 4 }
```
Returns: `records` (list), `count`, `date_range`
Each record includes:
| Field | Description |
|-------|-------------|
| `date` | Date (YYYYMMDD) — the morning after the sleep |
| `total_duration_minutes` | Total sleep in minutes |
| `phases.deep_minutes` | Deep sleep |
| `phases.light_minutes` | Light sleep |
| `phases.rem_minutes` | REM sleep |
| `phases.awake_minutes` | Time awake during the night |
| `phases.nap_minutes` | Daytime nap time (null if none) |
| `avg_hr` | Average heart rate during sleep |
| `min_hr` | Minimum heart rate during sleep |
| `max_hr` | Maximum heart rate during sleep |
| `quality_score` | Sleep quality score (null if not computed) |
> **Note:** Sleep data is fetched from the Coros mobile API (`apieu.coros.com`), which uses a separate token from the Training Hub web API. `coros-mcp auth` obtains both tokens, but doing so logs you out of the Coros mobile app. Use `coros-mcp auth-web` to skip mobile login — the mobile token is then obtained automatically on the first sleep data request. The token expires after ~1 hour but **refreshes automatically** on subsequent requests.
### `list_activities`
List activities for a date range.
```json
{ "start_day": "20260101", "end_day": "20260305", "page": 1, "size": 30 }
```
Returns: `activities` (list), `total_count`, `page`
Each activity includes: `activity_id`, `name`, `sport_type`, `sport_name`, `start_time`, `end_time`, `duration_seconds`, `distance_meters`, `avg_hr`, `max_hr`, `calories` (in cal — divide by 1000 for kcal), `training_load`, `avg_power`, `normalized_power`, `elevation_gain`
### `get_activity_detail`
Fetch full detail for a single activity. Requires the `sport_type` from `list_activities`.
```json
{ "activity_id": "469901014965714948", "sport_type": 200 }
```
Returns full activity data including laps, HR zones, power zones, and all sport-specific metrics.
> **Note:** Large time-series arrays (`graphList`, `frequencyList`, `gpsLightDuration`) are stripped from the response to keep it manageable.
### `list_workout_templates`
List reusable workout templates saved in the Coros library.
```json
{}
```
Returns: `workouts` (list), `count`
Each entry includes: `id`, `name`, `sport_type`, `sport_name`, `estimated_time_seconds`, `exercise_count`, `exercises` (list of steps with `name`, `intensity_low`, `intensity_high`, `sets`, and exactly one duration key: `duration_seconds`, `distance_meters`, or `duration_open`)
### `save_workout_template`
Save a reusable cycling/intervals workout **template** to the Coros library. The template appears in the Coros app and can be synced to the watch. Steps can be plain steps or repeat groups for intervals.
> ⚠️ This persists to the library indefinitely. For a one-off workout for a specific date, use [`schedule_workout`](#schedule_workout) instead — it builds the workout inline and leaves no library entry.
**Plain steps:**
```json
{
"name": "Sweet Spot 90min",
"sport_type": 2,
"steps": [
{"name": "15:00 Warmup", "duration_minutes": 15, "intensity_low": 148, "intensity_high": 192},
{"name": "20:00 Sweet Spot", "duration_minutes": 20, "intensity_low": 260, "intensity_high": 275},
{"name": "5:00 Rest", "duration_minutes": 5, "intensity_low": 100, "intensity_high": 150},
{"name": "20:00 Sweet Spot", "duration_minutes": 20, "intensity_low": 260, "intensity_high": 275},
{"name": "30:00 Cooldown", "duration_minutes": 30, "intensity_low": 100, "intensity_high": 192}
]
}
```
**With repeat groups (intervals):**
```json
{
"name": "3×10min Sweet Spot",
"sport_type": 2,
"steps": [
{"name": "Warmup", "duration_minutes": 10, "intensity_low": 150, "intensity_high": 200},
{"repeat": 3, "steps": [
{"name": "Sweet Spot", "duration_minutes": 10, "intensity_low": 265, "intensity_high": 285},
{"name": "Recovery", "duration_minutes": 3, "intensity_low": 150, "intensity_high": 175}
]},
{"name": "Cooldown", "duration_minutes": 11, "intensity_low": 150, "intensity_high": 200}
]
}
```
**Distance-based steps:** use `duration_meters` instead of `duration_minutes` for a step that should end at a real distance regardless of pace, e.g. `{"name": "1km @ 4:00/km", "duration_meters": 1000, "intensity_low": 235, "intensity_high": 245}` (pace in sec/km with `intensity_type: 3`).
**Open (lap-press) steps:** use `duration_open: true` for a step with no clock and no distance cap — the watch shows no countdown and advances only when the athlete presses lap, e.g. `{"name": "Warm-up", "duration_open": true, "intensity_low": 120, "intensity_high": 150}`. Intensity bounds still show as a guide zone but never end the step. Because an open step's length is unknowable ahead of time, it counts toward neither `total_minutes` nor `distance_meters_total`, and the response carries a `warning` saying so.
Exactly one of the three keys (`duration_minutes`, `duration_meters`, `duration_open`) per step.
`sport_type`: `2` = Indoor Cycling (default), `200` = Road Bike
Returns: `workout_id`, `name`, `total_minutes`, `distance_meters_total`, `steps_count`, `message`
### `save_strength_workout_template`
Save a reusable strength workout **template** to the Coros library. Exercises must come from the Coros exercise catalogue.
> ⚠️ This persists to the library indefinitely. For a one-off workout for a specific date, use [`schedule_strength_workout`](#schedule_strength_workout) instead — it builds the workout inline and leaves no library entry.
```json
{
"name": "Leg Circuit",
"sets": 3,
"exercises": [
{
"origin_id": "54",
"name": "T1061",
"overview": "sid_strength_squats",
"target_type": 3,
"target_value": 12,
"rest_seconds": 45,
"sets": 3,
"weight_kg": 80
},
{
"origin_id": "130",
"name": "T1176",
"overview": "sid_strength_plank",
"target_type": 2,
"target_value": 60,
"rest_seconds": 30
}
]
}
```
`target_type`: `2` = time in seconds, `3` = reps
`sets` (per exercise, optional): consecutive sets of that exercise before moving on. Use this instead of duplicating the exercise entry. Defaults to 1.
`sets` (top-level): full-circuit repetitions — repeats the entire exercise list. Defaults to 1.
**Weight fields (per exercise, optional):**
| Field | Description |
|-------|-------------|
| `weight_kg` | Prescribed weight in kg (e.g. `80`) |
| `weight_lbs` | Prescribed weight in pounds (e.g. `176.4`) — mutually exclusive with `weight_kg` |
- Omitting **both** fields renders the exercise as **Bodyweight** in the Coros app.
- Setting `weight_kg: 0` renders as **"0.00 kg"** — distinct from Bodyweight.
- For dumbbell exercises, the weight is per hand by convention (the app shows a single value).
- kg and lbs can be mixed across exercises within the same workout.
Returns: `workout_id`, `name`, `sets`, `exercise_count`
### `delete_workout_template`
Delete a saved workout template from the Coros library.
```json
{ "workout_id": "476023839273435149" }
```
The `workout_id` comes from `list_workout_templates`.
Returns: `deleted`, `workout_id`, `message`
### `list_planned_activities`
List planned activities from the Coros training calendar.
```json
{ "start_day": "20260309", "end_day": "20260316" }
```
Returns: `schedule` (dict with `entities` and `programs` sub-lists), `count` (number of scheduled entities), `date_range`
### `schedule_workout`
Schedule a **one-off** cycling/intervals workout for a specific date. Builds the workout inline and posts straight to the calendar — does **not** create a library entry. This is the common case.
Same parameters as [`save_workout_template`](#save_workout_template), plus `happen_day` (YYYYMMDD) and an optional `sort_no` (order within the day).
```json
{
"name": "Z2 Recovery",
"happen_day": "20260312",
"sport_type": 2,
"steps": [
{"name": "Easy spin", "duration_minutes": 60, "intensity_low": 150, "intensity_high": 200}
]
}
```
Returns: `scheduled`, `name`, `happen_day`, `total_minutes`, `steps_count`, `response`
### `schedule_strength_workout`
Schedule a **one-off** strength workout for a specific date. Builds the workout inline and posts straight to the calendar — does **not** create a library entry.
Same parameters as [`save_strength_workout_template`](#save_strength_workout_template), plus `happen_day`.
```json
{
"name": "Push Day",
"happen_day": "20260312",
"sets": 1,
"exercises": [ ... ]
}
```
Returns: `scheduled`, `name`, `happen_day`, `sets`, `exercise_count`, `response`
### `schedule_workout_template`
Schedule an existing library template on a calendar day.
```json
{ "workout_id": "1234567890", "happen_day": "20260312", "sort_no": 1 }
```
The `workout_id` comes from `list_workout_templates`. For one-off workouts that don't need a library entry, use [`schedule_workout`](#schedule_workout) or [`schedule_strength_workout`](#schedule_strength_workout) instead.
Returns: `scheduled`, `workout_id`, `happen_day`, `response`
### `remove_scheduled_workout`
Remove a scheduled workout from the Coros training calendar.
```json
{
"plan_id": "987654321",
"id_in_plan": "1234567890",
"plan_program_id": "1234567890"
}
```
`plan_id`, `id_in_plan`, and `plan_program_id` come from `list_planned_activities`. If `plan_program_id` is missing, you can usually reuse `id_in_plan`.
Returns: `removed`, `plan_id`, `id_in_plan`
### `list_exercises`
List the Coros exercise catalogue for a sport type. Default `sport_type=4` returns strength exercises.
```json
{ "sport_type": 4 }
```
Returns: `exercises` (list), `count`, `sport_type`
### `sync_coros_data`
Backfill all data into the local SQLite cache for a date range. After the first full sync, `get_daily_metrics`, `get_sleep_data`, and `list_activities` serve historical data from cache and only fetch the incremental tail from the API.
```json
{ "start_day": "20230101", "end_day": "20260514" }
```
Both parameters are optional and default to two years ago / today respectively. For large ranges (> 6 months) prefer the CLI:
```bash
coros-mcp sync --from 20230101
```
Returns: `daily` (records synced), `sleep` (records synced), `activities` (records synced), `errors` (list), `cache` (coverage summary)
### `get_cache_status`
Show what data is currently stored in the local SQLite cache.
```json
{}
```
Returns per data type: `count`, `from` (earliest date), `to` (latest date). Also includes `db_path`.
---
## Requirements
- Python ≥ 3.11
- A Coros account (Training Hub)
---
## Project Structure
```
coros-mcp/
├── coros_mcp/
│ ├── server.py # MCP server with tool definitions
│ ├── coros_api.py # Coros API client (auth, requests, parsers)
│ ├── models.py # Pydantic data models
│ ├── cli.py # CLI entry point (serve, auth, sync, cache-status, …)
│ ├── auth/ # Token storage (keyring + encrypted file fallback)
│ └── cache/ # SQLite cache layer (store, sync, utils)
├── tests/ # pytest test suite
├── pyproject.toml # Project metadata & dependencies
└── docs/
└── mobile-token.md # Mobile API token background (legacy reference)
```
## Dependencies
- [fastmcp](https://github.com/jlowin/fastmcp) — MCP framework
- [httpx](https://www.python-httpx.org/) — Async HTTP client
- [pydantic](https://docs.pydantic.dev/) — Data validation
- [cryptography](https://cryptography.io/) — AES encryption (mobile API auth + token file)
- [keyring](https://github.com/jaraco/keyring) — Secure token storage
- [python-dotenv](https://github.com/theskumar/python-dotenv) — `.env` support
## Disclaimer
This project uses the **unofficial** Coros Training Hub API. The API may change at any time without notice. Use at your own risk.
TDQS
Scored across 26 tools
Most tools have clearly distinct purposes, and the descriptions are careful to distinguish one-off scheduling from reusable templates and raw from cleaned list variants. However, the cluster of scheduling tools (schedule_workout, schedule_strength_workout, schedule_workout_template, add_planned_workout) and the paired raw/non-raw list tools still create some potential for misselection.
The vast majority of tools follow a clear verb_noun pattern (list_, get_, save_, schedule_, delete_, update_, remove_). Minor inconsistencies exist, such as add_planned_workout vs. schedule_workout and the raw suffix on only some list tools, but overall the naming is predictable and readable.
With 26 tools, the server crosses the threshold where the surface feels too heavy for an agent to navigate comfortably. Many tools are individually justified, but the raw variants, cache management utilities, and multiple scheduling entry points add notable bulk.
The tool set covers the core Coros workflows well: authentication, metrics and sleep retrieval, activity listing/detail, workout template CRUD, scheduled workout create/update/remove, and cache syncing. Minor gaps exist, such as no dedicated create/delete training plan tools or a single planned-activity getter, but raw list and calculate tools provide workarounds.