No description
  • Python 99%
  • Shell 1%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-08-17 16:04:17 +03:00
bin Реализация ТЗ: девять инструментов, сборка дня, 76 тестов 2026-08-17 15:50:10 +03:00
docs Недельная агрегация и сбор периода коллекциями вместо запросов на каждый день 2026-08-17 16:04:17 +03:00
src/whoop_mcp Недельная агрегация и сбор периода коллекциями вместо запросов на каждый день 2026-08-17 16:04:17 +03:00
tests Недельная агрегация и сбор периода коллекциями вместо запросов на каждый день 2026-08-17 16:04:17 +03:00
.gitignore Реализация ТЗ: девять инструментов, сборка дня, 76 тестов 2026-08-17 15:50:10 +03:00
.mcp.json Реализация ТЗ: девять инструментов, сборка дня, 76 тестов 2026-08-17 15:50:10 +03:00
pyproject.toml Спайк, измеренная карта API и первые слои сервера 2026-08-17 15:16:10 +03:00
README.md Недельная агрегация и сбор периода коллекциями вместо запросов на каждый день 2026-08-17 16:04:17 +03:00
uv.lock Спайк, измеренная карта API и первые слои сервера 2026-08-17 15:16:10 +03:00

whoop-mcp

MCP-сервер к API Whoop (v2). Локальный процесс, транспорт stdio, один пользователь, один аккаунт Whoop. Только чтение.

Установка

uv sync

Приложение и доступ

  1. Заведите приложение в Developer Dashboard: шесть прав чтения (read:cycles, read:sleep, read:recovery, read:workout, read:profile, read:body_measurement) и адрес возврата https://localhost:8443/callback.

    offline в дашборде не выбирается — это право запрашивается из самой ссылки авторизации, и bin/whoop-auth добавляет его сам. Без него доступ умирал бы через час без возможности продлить.

  2. Положите идентификатор и секрет приложения в .env в корне репозитория:

    WHOOP_CLIENT_ID=…
    WHOOP_CLIENT_SECRET=…
    

    .env в .gitignore. Сервер его не читает — читает скрипт запуска bin/whoop-mcp и передаёт значения процессу через окружение.

  3. Пройдите разовую авторизацию:

    ./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 пропускается, поэтому обычный прогон его не запускает.