- Python 99%
- Shell 1%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| bin | ||
| docs | ||
| src/whoop_mcp | ||
| tests | ||
| .gitignore | ||
| .mcp.json | ||
| pyproject.toml | ||
| README.md | ||
| uv.lock | ||
whoop-mcp
MCP-сервер к API Whoop (v2). Локальный процесс, транспорт stdio, один пользователь, один аккаунт Whoop. Только чтение.
Установка
uv sync
Приложение и доступ
-
Заведите приложение в Developer Dashboard: шесть прав чтения (
read:cycles,read:sleep,read:recovery,read:workout,read:profile,read:body_measurement) и адрес возвратаhttps://localhost:8443/callback.offlineв дашборде не выбирается — это право запрашивается из самой ссылки авторизации, иbin/whoop-authдобавляет его сам. Без него доступ умирал бы через час без возможности продлить. -
Положите идентификатор и секрет приложения в
.envв корне репозитория:WHOOP_CLIENT_ID=… WHOOP_CLIENT_SECRET=….envв.gitignore. Сервер его не читает — читает скрипт запускаbin/whoop-mcpи передаёт значения процессу через окружение. -
Пройдите разовую авторизацию:
./bin/whoop-authСкрипт напечатает ссылку. Откройте её в браузере и подтвердите доступ; браузер перекинет на
https://localhost:8443/callbackи покажет ошибку соединения — так и задумано, слушать там некому. Скопируйте адресную строку целиком и вставьте в ожидающий скрипт: он сверитstate, обменяет код на пару токенов и запишет её в.whoop-tokens.json(права 600, вне git).
Дальше сервер продлевает доступ сам.
| Переменная | Обязательна | Смысл |
|---|---|---|
WHOOP_CLIENT_ID |
да | идентификатор приложения |
WHOOP_CLIENT_SECRET |
да | секрет приложения |
WHOOP_TOKEN_FILE |
нет | по умолчанию .whoop-tokens.json в корне |
WHOOP_DEBUG |
нет | 1 — уровень DEBUG и полный стек упавшего инструмента в stderr |
Секреты не попадают ни в ответ инструмента, ни в текст ошибки, ни в лог. В .mcp.json
их нет, в аргументах командной строки — тоже (аргументы видны в ps).
Подключение
Сервер зарегистрирован в .mcp.json этого репозитория: запустите Claude Code в каталоге
проекта и подтвердите подключение сервера whoop.
Инструменты
Девять, все только читают — записи в API Whoop нет вовсе.
| Инструмент | Что делает |
|---|---|
whoop_day |
карточка дня: восстановление, сон, нагрузка, события с временами |
whoop_days |
до 90 дней — таблица по дням, дальше и до года — сводка по неделям |
whoop_workouts |
тренировки за период; начало и конец принимают и дату, и время |
whoop_sleeps |
сны за период, включая дневные |
whoop_workout |
карточка тренировки: зоны пульса, дистанция, нагрузка |
whoop_sleep |
карточка сна: стадии, эффективность, дыхание |
whoop_profile |
профиль и телесные измерения |
whoop_auth_status |
есть ли доступ, до какого времени, с какими правами |
whoop_get |
запасной люк, только GET |
Чего этот сервер не может
В публичном API Whoop нет внутридневных данных — ни ряда пульса, ни Stress Monitor, ни журнала. Стадии сна приходят агрегатами, а не разметкой по времени.
Поэтому вопрос «в 10:00 я был за рулём, какой был стресс» невыполним. Честный максимум — утреннее восстановление, границы физиологического цикла и события, у которых есть начало и конец: сны, дневные сны, тренировки. Это ограничение API, а не реализации.
Что известно про API
docs/API Whoop.md — карта, снятая запросами к живому серверу, а не пересказ
документации. Она определяет архитектуру:
- Токен обновления одноразовый. Обновление возвращает новую пару и убивает прежний
доступ немедленно. Сгоревший токен даёт
400 invalid_request, а неinvalid_grantиз RFC. - Отсюда протокол при 401: взять блокировку, перечитать файл токенов, и если доступ в нём другой — повторить запрос без обновления. Обновляться только если пара та же. Простое «поймал 401 — обновись» ломает работу в двух окнах Claude Code.
- Оценка существует только при
score_state == SCORED, но приSCOREDвстречаются и честные нули (день, когда ремень не носили), и пустые поля внутри оценки (прогулка без GPS). Состояний три, и путать их нельзя ни в какую сторону. - Идентификаторы циклов не упорядочены по времени — сравнивать циклы можно только по времени начала.
limitбольше 25 — ошибка 400. Общего количества записей API не отдаёт, поэтому усечение объявляется словами «есть ещё», без выдуманного числа отброшенных.
Тесты
uv run pytest # 83 теста, без сети
set -a; . ./.env; set +a
WHOOP_LIVE=1 uv run pytest tests/live -v # живой smoke, только чтение
Живой smoke без WHOOP_LIVE=1 пропускается, поэтому обычный прогон его не запускает.