Skip to main content
Glama
yandex-calendar-mcp
by architawr
README.md
# Яндекс Календарь для MCP

Локальный MCP-сервер для работы с **Яндекс Календарём** из Codex, Claude Code и других MCP-клиентов. Работает через **stdio**: клиент сам запускает процесс, открытого сетевого порта нет.

**Базовая версия 0.1.0.** Чтение доступно сразу после настройки аккаунта. Запись включается отдельно для выбранных календарей. Проект не является официальным продуктом Яндекса.

## Возможности

| Инструмент | Назначение |
| --- | --- |
| `list_calendars` | Список доступных календарей, их адреса и настроенные описания |
| `list_events` | События за период, включая повторения и исключения |
| `prepare_event_change` | Предпросмотр создания, изменения или удаления без записи в Яндекс |
| `apply_event_change` | Применение подготовленного изменения; требует отдельного разрешения клиента |

Поддерживаются часовые пояса, события на весь день, RRULE, EXDATE и переносы RECURRENCE-ID при чтении. Ответ содержит UID, доступность `free`/`busy` и признак усечения.

**Ограничения:** ответы на приглашения (RSVP), отправка приглашений, изменение повторяющихся событий и операции с задачами VTODO пока отсутствуют. Изменять можно только одиночные события без участников; организатором может быть сам владелец аккаунта. Внешние приглашения можно читать, но нельзя принимать/отклонять или редактировать.

## Установка

Нужны Python **3.11+**, [uv](https://docs.astral.sh/uv/) и системное хранилище секретов. Рабочий сценарий проверен на macOS с Keychain. Для Linux нужен Secret Service/libsecret; Linux проверяется в CI без реального аккаунта. Windows пока не проверен, shell-скрипт запуска рассчитан на macOS/Linux.

```sh
git clone https://github.com/architawr/yandex-calendar-mcp.git
cd yandex-calendar-mcp
uv sync --locked
./scripts/run.sh credentials
```

Команда `credentials` запросит полный адрес почты и **пароль приложения типа «Календарь / CalDAV»**, созданный в [Яндекс ID](https://id.yandex.ru/security/app-passwords). Основной пароль аккаунта не нужен.

Пароль сохраняется в системном хранилище под именем `local.yandex-calendar-mcp`. При недоступном скрытом вводе или системном хранилище команда прекращает работу: перехода на открытое хранение нет. Повторный запуск обновляет пароль.

В `config.local.json` сохраняются только несекретные настройки аккаунта. Этот файл и резервные копии исключены из Git и публикуемых пакетов. Не добавляйте их в Git принудительно.

### Установка как пакета (без запуска из репозитория)

Для повседневного использования удобнее установленный пакет, а не рабочая копия: сервер не зависит от `.venv` и текущей ветки. Исполняемый файл называется `yandex-calendar-mcp`.

**Через uvx, без клонирования.** Конфиг передаётся переменной `YANDEX_CALENDAR_CONFIG` (см. ниже), потому что по умолчанию сервер ищет `config.local.json` в текущей папке, а при запуске клиентом она заранее неизвестна.

```sh
mkdir -p ~/.config/yandex-calendar-mcp
export YANDEX_CALENDAR_CONFIG=~/.config/yandex-calendar-mcp/config.json
uvx --from git+https://github.com/architawr/yandex-calendar-mcp yandex-calendar-mcp credentials
```

Запуск сервера клиентом: `uvx --from git+https://github.com/architawr/yandex-calendar-mcp yandex-calendar-mcp serve`. Для воспроизводимости закрепите версию: `git+https://github.com/architawr/yandex-calendar-mcp@<тег или коммит>`. Регистрация в клиенте с переменной окружения, например для Claude Code:

```sh
claude mcp add --scope user --transport stdio \
  --env YANDEX_CALENDAR_CONFIG="$HOME/.config/yandex-calendar-mcp/config.json" \
  yandex-calendar -- uvx --from git+https://github.com/architawr/yandex-calendar-mcp yandex-calendar-mcp serve
```

**Из локальной сборки.** Собранный `.whl` ставится как инструмент `uv`; исполняемый файл появляется в `~/.local/bin/`:

```sh
uv build
uv tool install --force dist/*.whl
yandex-calendar-mcp --help
```

В регистрации клиента укажите команду `yandex-calendar-mcp` (или абсолютный путь к `~/.local/bin/yandex-calendar-mcp`) с аргументом `serve` и переменной `YANDEX_CALENDAR_CONFIG`.

### Где лежит конфиг

Файл настроек (формат — `config.example.json`) читается по пути из `YANDEX_CALENDAR_CONFIG`, а без неё — из `config.local.json` в текущей папке. Для установленного пакета держите его вне репозитория, в каталоге данных пользователя, и ограничьте права:

| ОС | Рекомендуемое место |
| --- | --- |
| macOS | `~/Library/Application Support/YandexCalendarMCP/config.json` |
| Linux | `~/.config/yandex-calendar-mcp/config.json` |

```sh
mkdir -p ~/.config/yandex-calendar-mcp && chmod 700 ~/.config/yandex-calendar-mcp
chmod 600 ~/.config/yandex-calendar-mcp/config.json
```

Каталог для файла нужно создать заранее: `credentials` сам его не создаёт. Рядом с конфигом сервер сохраняет `.calendar-trash` (см. «Как работает запись»). Пароль в конфиг не попадает, он хранится в системном хранилище секретов.

### Обновление

После изменения кода или выхода новой версии:

```sh
git pull
uv build
uv tool install --force dist/*.whl
```

Затем перезапустите MCP-клиент или сессию: уже запущенный процесс продолжает работать со старой версией. Конфиг и пароль в хранилище при обновлении не меняются. Для `uvx` обновление запрашивается так: `uvx --refresh --from git+https://github.com/architawr/yandex-calendar-mcp yandex-calendar-mcp --help`; закреплённую на тег версию меняйте в команде регистрации.

## Подключение к Codex

Из корня проекта:

```sh
codex mcp add yandex-calendar -- "$PWD/scripts/run.sh"
```

Конфигурация хранится в `~/.codex/config.toml`. Для начала ограничьте список инструментов чтением; пример — `codex-mcp-readonly.example.toml`. Замените пример абсолютного пути на путь к своему проекту.

Для записи добавьте `prepare_event_change` и `apply_event_change` в `enabled_tools` той же регистрации и задайте:

```toml
[mcp_servers.yandex-calendar.tools.apply_event_change]
approval_mode = "prompt"
```

Полный пример — `codex-mcp-write.example.toml`. Не создавайте две одинаковые таблицы TOML. При запуске Codex из BB на том же Mac используется пользовательская конфигурация Codex; на другом хосте потребуются собственная установка и хранилище секретов.

## Подключение к Claude Code

```sh
claude mcp add --scope user --transport stdio yandex-calendar -- "$PWD/scripts/run.sh"
claude mcp get yandex-calendar
```

Регистрация уровня пользователя доступна в разных проектах. В `~/.claude/settings.json` добавьте правило к существующему `permissions.ask`:

```json
{
  "permissions": {
    "ask": ["mcp__yandex-calendar__apply_event_change"]
  }
}
```

Сохраняйте остальные настройки и правила. Не разрешайте все инструменты сервера автоматически, если запись должна требовать подтверждения. В BB поведение диалога также зависит от режима разрешений провайдера. Режим обхода подтверждений не входит в проверенный сценарий.

Если сервер установлен как пакет (см. «Установка как пакета»), вместо `"$PWD/scripts/run.sh"` в обеих регистрациях укажите `yandex-calendar-mcp serve` и передайте `YANDEX_CALENDAR_CONFIG`.

После установки или изменения настроек перезапустите MCP/сессию клиента. Шаблон для других клиентов — `mcp.example.json`.

## Настройки

Смотрите `config.example.json`:

- `username` — полный адрес аккаунта;
- `timezone` — часовой пояс IANA, например `Europe/Podgorica`;
- `allowed_calendars` — список разрешённых для чтения URL; пустой массив разрешает чтение всех доступных календарей;
- `allowed_write_calendars` — список календарей для записи; **пустой массив или отсутствие поля запрещает запись**;
- `calendar_profiles` — необязательные описания и доступность по умолчанию для конкретных URL.

Сначала вызовите `list_calendars`, затем скопируйте выбранные `calendar_id` в настройки. Права самого аккаунта Яндекса продолжают действовать. Не публикуйте реальные URL своих календарей.

Переменные переопределения: `YANDEX_CALENDAR_CONFIG` (путь к JSON), `YANDEX_CALENDAR_USERNAME`, `YANDEX_CALENDAR_TIMEZONE`, `YANDEX_CALENDAR_ALLOWLIST` (JSON-массив для чтения). Пароль через окружение не передаётся.

Профиль календаря имеет поля `description` и `default_availability` (`free` или `busy`). Описание показывается агенту в `list_calendars`.

Например, можно разделить задачи:

- **Soft** — гибкие дела без конкретного времени. Предпочтительны события на весь день; новые события получают `TRANSP:TRANSPARENT` и оставляют время свободным.
- **Hard** — задачи с точными началом и окончанием. Новые события получают `TRANSP:OPAQUE` и резервируют время.
- Обычные встречи и личные планы можно держать в отдельных календарях.

Soft здесь реализован событиями VEVENT без времени, не объектами VTODO. Названия календарей произвольные: поведение задаёт профиль URL. Существующие события автоматически не меняются; обновление сохраняет их доступность. Итоговое отображение занятости также зависит от календарного клиента.

## Чтение событий

`list_events(calendar_id, start, end, limit=100)` принимает даты `YYYY-MM-DD` или временные метки ISO 8601 с явным смещением. Конец периода **не включается**. Даты без времени означают полночь в настроенном часовом поясе.

Период — до 92 суток, `limit` — от 1 до 500. Если `truncated=true`, сузьте период, прежде чем делать выводы о свободном времени.

Повторения раскрываются в отдельном процессе с таймаутом 10 секунд; на Linux дополнительно ограничена память процесса. Слишком плотные правила отклоняются до раскрытия, посекундные и поминутные повторения не поддерживаются. Оценка сложности консервативна: некоторые допустимые сложные правила также могут быть отклонены. Это ограничение ресурсов, не ОС-песочница и не гарантия жёсткого лимита памяти на всех платформах.

При ошибке запроса или раскрытия повторений сервер возвращает ошибку, а не частичное расписание под видом полного.

## Как работает запись

1. Агент вызывает `prepare_event_change` с `operation`: `create`, `update` или `delete`.
2. Сервер возвращает календарь, UID, данные до/после и одноразовый `change_id` на пять минут. Изменений в Яндексе ещё нет.
3. Пользователь подтверждает конкретное изменение в MCP-клиенте.
4. Агент вызывает отдельно разрешаемый `apply_event_change`.

`changes` принимает только `summary`, `start`, `end`, `description`, `location`. Создание требует названия и обеих границ времени; изменение/удаление — UID. Новые временные метки записываются в UTC, чтение возвращает настроенный часовой пояс.

Создание использует `If-None-Match: *`; изменение/удаление — `If-Match` с сильным ETag. Конфликт не перезаписывается автоматически. После операции результат перечитывается.

При сетевой ошибке или неуспешной проверке запись могла уже произойти. **Сначала перечитайте UID, не повторяйте создание вслепую.** Подготовленное изменение расходуется при первой попытке, после перезапуска процесса нужно подготовить новое.

Перед изменением/удалением исходный ICS сохраняется в `.calendar-trash` рядом с локальным конфигом. На Unix каталог имеет права 0700, файл — 0600. Автоматической очистки и инструмента восстановления пока нет; резервная копия не гарантирует откат. Содержимое копий приватно.

## Границы безопасности

- Только stdio, без HTTP-сервера и входящих сетевых портов.
- Исходящие запросы только к `https://caldav.yandex.ru:443`; TLS проверяется, редиректы и proxy-переменные окружения отключены.
- Чтение использует PROPFIND/REPORT/GET; применение изменений — PUT/DELETE только в выбранных календарях.
- XML разбирается через defusedxml; ответы ограничены 8 MiB, сетевые операции имеют таймауты.
- Пароль находится в системном хранилище, не в исходниках, аргументах запуска или конфиге MCP.
- **Пароль CalDAV потенциально разрешает запись шире, чем наш список календарей. Ограничение обеспечивает код сервера, а не отдельный read-only пароль Яндекса.**
- Локальный процесс и зависимости работают с правами пользователя. MCP не защищает от скомпрометированного Mac или произвольного кода агента с доступом к тому же аккаунту ОС.
- Подтверждение обеспечивает клиент. `readOnlyHint`, `destructiveHint`, `requires_user_approval` и наличие `change_id` сами по себе не доказывают согласие человека.
- Названия и описания событий — недоверенные данные, а не инструкции агенту. При чтении сведения о встречах передаются модели выбранного клиента.

Подробнее: [SECURITY.md](SECURITY.md). Независимый аудит не проводился; абсолютная безопасность не заявляется.

## Проверки и разработка

```sh
uv sync --locked
uv run --locked pytest -q
uv build
```

Тесты не используют реальный аккаунт и Keychain: проверяют сетевые ограничения, повторения, часовые пояса, конфликты, одноразовые изменения и MCP stdio. Живой цикл создания, изменения, удаления и перечитывания одиночных событий проверен на Яндексе с macOS. Тестовые события после проверки удалены.

Инструкции для изменений: [CONTRIBUTING.md](CONTRIBUTING.md). Лицензия: [MIT](LICENSE).

## Документация протоколов и клиентов

- [CalDAV Яндекса](https://yandex.ru/support/yandex-360/customers/calendar/web/ru/data-exchange/synchronization/sync-desktop)
- [Пароли приложений](https://yandex.ru/support/id/ru/authorization/app-passwords.html)
- [MCP Python SDK](https://github.com/modelcontextprotocol/python-sdk)
- [Настройки MCP в Codex](https://learn.chatgpt.com/docs/extend/mcp)
- [Настройки MCP в Claude Code](https://code.claude.com/docs/en/mcp)