Pyrus MCP — README

Pyrus MCP Server

Model Context Protocol (MCP) сервер для интеграции с BPMS Pyrus.

Позволяет любому MCP-совместимому клиенту (Claude Desktop, Cursor и др.) читать и изменять данные в Pyrus: задачи, формы, каталоги, пользователи, роли, объявления и базу знаний.


Установка

1. Клонирование и установка зависимостей

git clone <repo-url>
cd pyrus_mcp
pip install -r requirements.txt
# или
pip install -e .

2. Настройка окружения

Создайте .env или экспортируйте переменные:

export PYRUS_LOGIN="your-email@pyrus.com"
export PYRUS_SECURITY_KEY="your-secret-key"
# или, если у вас уже есть токен:
# export PYRUS_ACCESS_TOKEN="..."

Ключ безопасности (security key) берётся в Pyrus: Настройки → Авторизация → Секретный API ключ.

3. Подключение к MCP клиенту

Реквизиты Pyrus сервер берёт из заголовков запроса — своего аккаунта у него нет:

X-Pyrus-Login + X-Pyrus-Security-Key     # пара, одного логина мало
X-Pyrus-Access-Token                     # либо готовый токен

Без них любой вызов вернёт no_credentials — включая самые лёгкие. По этому признаку ошибку легко спутать с перегрузкой на больших данных, поэтому проверяйте заголовки первым делом.

Claude Desktop (macOS / Windows)

Добавьте в claude_desktop_config.json:

macOS: ~/Library/Application Support/Claude/claude_desktop_config.json

Windows: %APPDATA%\Claude\claude_desktop_config.json

{
  "mcpServers": {
    "pyrus": {
      "command": "python",
      "args": ["-m", "pyrus_mcp"],
      "env": {
        "PYRUS_LOGIN": "your-email@pyrus.com",
        "PYRUS_SECURITY_KEY": "your-secret-key"
      }
    }
  }
}

Если используете виртуальное окружение, укажите полный путь до Python:

{
  "mcpServers": {
    "pyrus": {
      "command": "/path/to/venv/bin/python",
      "args": ["-m", "pyrus_mcp"],
      "env": {
        "PYRUS_LOGIN": "your-email@pyrus.com",
        "PYRUS_SECURITY_KEY": "your-secret-key"
      }
    }
  }
}

Cursor

В настройках Cursor → MCP Servers добавьте:

{
  "mcpServers": {
    "pyrus": {
      "command": "python",
      "args": ["-m", "pyrus_mcp"],
      "env": {
        "PYRUS_LOGIN": "your-email@pyrus.com",
        "PYRUS_SECURITY_KEY": "your-secret-key"
      }
    }
  }
}

Доступные инструменты (Tools)

Работа в нескольких контурах Pyrus

Каждый инструмент принимает два необязательных параметра:

Параметр Назначение
access_token выполнить вызов от имени этого токена
api_url контур клиента, например https://api.pyrus.com/v4

Нужны боту, который обслуживает организации-клиентов: токен и api_url приходят в каждом вебхуке, а заголовки MCP фиксированы на всё соединение и для этого не годятся. Если параметры не переданы — работают реквизиты соединения (заголовки X-Pyrus-*), как и раньше.

{"task_id": 372965117,
 "access_token": "<токен из вебхука>",
 "api_url": "https://api.pyrus.com/v4"}

Override действует только на свой вызов и не переносится на следующий, поэтому в одной сессии можно работать с разными клиентами вперемешку.

Задачи (Tasks)

Инструмент Описание
get_task Получить задачу по ID со всеми комментариями
create_task Создать задачу (простую или по форме)
comment_task Добавить комментарий к задаче, согласовать, назначить, изменить поля

Удобные обёртки (Convenience Wrappers)

Инструмент Описание
close_task Закрыть задачу (action=finished)
reopen_task Открыть ранее закрытую задачу
assign_task Переназначить задачу на другого человека
add_approvers Добавить согласующих к задаче
update_task_fields Обновить поля формы задачи
add_subscribers Добавить подписчиков к задаче
— Все пишущие инструменты принимают brief=true: в ответ приходит task_id, id нового комментария и состояние (is_closed, current_step, list_ids) вместо задачи целиком. Pyrus отвечает на любую запись всей задачей — 18 КБ на вызов, замерено
delete_task Удалить задачу безвозвратно (в отличие от close_task)
search_tasks Поиск задач в реестре по диапазону дат. Умеет то же, что get_registry: field_ids, format, обход через page_size + cursor — он ей же и делегирован
find_tasks Сквозной поиск по всему аккаунту — по автору, ответственному, участнику, тексту, поверх всех форм и свободных задач. Единственный способ ответить «какие задачи я поставил другим» (author="me"): в API v4 списка задач нет вовсе. Обход выборки целиком — page_size + cursor: своей пагинации у Pyrus нет, её делает сервер. Единственный инструмент, ходящий в API v3 — см. «Почему один инструмент ходит в v3»
get_overdue_tasks Задачи, у которых срок уже прошёл. Берёт сроки из календаря: в реестре их нет вовсе. form_id — фильтр, а не требование
get_tasks_due_soon Задачи со сроком в ближайшие N дней, тоже из календаря. Возвращает {window, counts, tasks} с компактными записями

Реестры и списки

Инструмент Описание
get_forms Получить все шаблоны форм
get_form Получить конкретный шаблон формы. summary, field_ids, flatten
get_registry Получить реестр задач по форме. Фильтры, format="csv", field_ids, обход через page_size + cursor
get_form_permissions Кто имеет доступ к форме и на каком уровне
change_form_permissions Выдать или отозвать доступ к форме
get_form_export Полная веб-конфигурация формы: 36 разделов против 7 у get_form — маршрут с условиями, права, SLA, статусы, эскалации, скрипт формы, справочники, печатные формы. По умолчанию нормализована (справочники — счётчиком, условия — плоским списком рядом с деревом); sections=[...] сужает, max_bytes не даёт результату не поместиться — см. read_guide("экспорт-формы")
normalize_form_export Тот же снапшот с другими флагами нормализации, без повторного скачивания — использует тот же 15-минутный кэш
get_form_config Конфигурация формы, разбитая по именованным секциям: meta, fields, steps, register, workflow, statuses, sla, access, external, scripts, escalations, helpdesk, print_forms — легче полного экспорта
get_form_fields Плоский список полей с путём в таблице, шагами редактирования/обязательности и сводкой видимости (какие поля и роли гейтят каждое поле)
get_form_register_view Порядок полей в реестре, флаг скрытия, XLSX-ссылка регистра и обнаружение сирот
resolve_form_refs Преобразовать id в имена для ролей, сотрудников, справочников, связанных форм, статусов и полей, с явным списком того, что не удалось разрешить
audit_form Детерминированный аудит формы без ИИ: 31 код находок по гигиене полей, правам, процессу/SLA/эскалациям, скриптам и справочникам — один и тот же экспорт всегда даёт одни и те же находки. checks=[...] сужает, severity_floor отсекает по уровню важности
score_form_config Здоровье автоматизации формы одним числом 0-3 плюс 8 подоценок по категориям — те же находки audit_form, но посчитанные. Детерминированно и воспроизводимо
get_form_workflow Маршрут формы: именованные шаги со счётчиком условий, ролями, SLA (наибольшее из нескольких политик шага, в часах), флагом активности календаря и ссылкой на подпроцесс — плюс наблюдатели и последовательный граф для рендеринга
render_form_workflow Диаграмма маршрута — format="mermaid" (текст), "svg" или "html", автономно: ни CDN, ни сторонних библиотек. Пустые шаги выделены визуально, условия и SLA — на рёбрах
get_form_access_matrix Кто видит форму: person_id по уровням доступа (2/3/4), записи без уровня/без person_id, все внешние пользователи, все роли из маршрута, и расхождения — роль в маршруте без единого участника с доступом
diff_form_exports Структурный дифф между текущим экспортом формы и другим (against_form_id — другая форма, against_raw_export — сохранённый JSON) — не замечает дрейф содержимого справочников, только структурные изменения
get_lists Списки плоским обходом: summary (сколько всего, сколько наверху, глубина), page_size + cursor, parent_id у каждого узла. На измеренном контуре дерево — 3177 узлов и 728 676 Б, целиком не отдаётся
get_list Получить конкретный список
create_list Создать список (имя, родитель, цвет)
update_list Изменить список (имя, родитель, цвет)
delete_list Удалить список (задачи не удаляются)
get_task_list Получить задачи из списка
get_inbox Получить входящие задачи
get_meetings Встречи календаря за окно (days_back, days_ahead). Читает /v4/calendar с include_meetings=true. Раньше читал Входящие и потому всегда возвращал ноль — ключа meetings в ответе /v4/inbox нет вовсе; исправлено 20.09.2026
score_inbox Оценить Входящие, календарь и встречи по личной модели весов и вернуть таблицу приоритетов. Сырьё наружу не выходит: 60 элементов — 23 КБ против 676 КБ задач целиком

Большие формы и реестры

Ответ тула целиком уходит в контекстное окно модели, поэтому всё, что больше MCP_MAX_RESPONSE_BYTES (по умолчанию 200 KB), не возвращается вовсе: тул отвечает response_too_large и подсказывает, что делать. Данные никогда не обрезаются молча — иначе модель уверенно считала бы итоги по половине реестра.

Потолков два, и они про разное:

Переменная Что бережёт Когда срабатывает По умолчанию
MCP_MAX_RESPONSE_BYTES контекст модели после сериализации ответа 200 KB
PYRUS_MAX_DOWNLOAD_BYTES память процесса во время скачивания от Pyrus 16 MB

Второй появился после того, как сервер убило нехваткой памяти: первый проверяется уже после того, как тело загружено и разобрано в объекты Python, и от переполнения не защищал. Замерено, что JSON распухает в объектах Python в 4,7 раза, поэтому скачивание обрывается потоком, а при известном Content-Length — до первого байта тела. Отказ приходит как download_too_large.

Сначала решите, нужны ли вам все данные — от этого зависит приём.

Нужны все — обходите порциями. page_size задаёт размер порции, дальше возвращайте полученный next_cursor, пока он приходит. Работает при любом объёме и ничего не теряет. Есть у get_registry и get_catalog.

get_registry(form_id=2363325, include_archived=True, page_size=150)
→ 150 задач + next_cursor
get_registry(..., page_size=150, cursor="…")
→ 149 задач + next_cursor      # одна отсеяна как повтор границы окна
→ …порция без курсора = конец

Размер порции подбирается под ширину строки: замерено, что 300 задач с одним полем весят 196 КБ и упираются в потолок, а 150 проходят. Начинайте со 150.

Обход реестра работает только с JSON: отсев повтора требует id, а в CSV колонку пришлось бы угадывать. Для выгрузки в CSV есть plan_registry_walk — он делит период на окна, которым перекрытие не нужно.

Нужна часть — сужайте. Рычаги по убыванию эффекта:

Рычаг Что делает Замер
get_registry(format="csv") CSV вместо JSON: имена колонок один раз в шапке, а не на каждой задаче 50 задач формы на 33 поля: 1 110 006 Б → 33 988 Б, 32,7×
get_registry(field_ids=[...]) Только нужные поля в каждой задаче одна выгрузка сократилась в 23×
get_registry(item_count=…) Потолок числа задач —
created_after / created_before Более короткий период —
get_form(summary=true) id, name, type, depth по каждому полю; списки вариантов считаются, а не копируются форма 218 KB перестаёт быть недостижимой
get_form(field_ids=[...]) Названные поля целиком, остальные отброшены —

CSV стоит брать всегда, когда вы считаете, агрегируете или просматриваете много задач. Но выбор чаще решает не размер, а состав колонок — форматы отдают разное (проверено на живом API):

Служебные колонки CSV: task_id, create_date, last_modified_date, close_date, last_note_id. Разделитель по умолчанию — запятая (меняется через delimiter). BOM, который Pyrus ставит в начало файла, сервер срезает, так что csv.DictReader читает первую колонку как task_id, а не task_id.

Порядок работы с большой формой: сначала get_form(summary=true), чтобы найти id полей, потом get_form(field_ids=[...]) или get_registry(field_ids=[...]). Оба варианта достают поля, вложенные в группы, — верхний уровень формы это не вся форма.

У реестра нет offset и курсора, поэтому полный обход делается окнами по датам: берёте created_after/created_before, запоминаете самый свежий create_date в ответе и он становится следующим created_after. Окно в месяц плюс format="csv" держит каждую страницу в пределах лимита.

Объявления

Инструмент Описание
get_announcements Получить объявления
get_announcement Получить объявление по ID
create_announcement Создать объявление
comment_announcement Комментировать объявление

Каталоги

Инструмент Описание
list_catalogs Все справочники аккаунта: id, имя, версия. Без строк
get_catalog Получить каталог. summary, columns, format="csv", обход через page_size + cursor
create_catalog Создать каталог
sync_catalog Полная синхронизация каталога
update_catalog_items Частичное обновление каталога (diff)

Пользователи и роли

Инструмент Описание
get_members / get_member Сотрудники организации
create_member / update_member Завести сотрудника, изменить данные
block_member Заблокировать — отзыв доступа к Pyrus
set_avatar Аватар сотрудника по guid загруженного файла
get_roles / get_role Роли организации
create_role / update_role / delete_role Управление ролями
get_award_threshold / set_award_threshold Пороги вручения и отзыва награды
get_award_counter / increment_award_counter / set_award_counter Счётчик награды у сотрудника
get_contacts / get_profile Справочное
get_bots / create_bot / update_bot / delete_bot Боты — технические аккаунты

Увольнение и блокировка — разные вещи. block_member отзывает доступ к Pyrus; в карточке это видно по полю banned, которое становится true. update_member(fired=true) доступ не трогает вовсе. Человеку, который действительно уходит, нужна именно блокировка.

⚠️ Поле fired есть в наших запросах, но в документации Pyrus его нет, и ни в одной живой карточке оно не встретилось. Не исключено, что Pyrus его игнорирует. Полагаться на fired как на признак увольнения не стоит — проверяйте banned.

Блокировка необратима через API: метода разблокировки Pyrus не даёт, восстановить доступ можно только в интерфейсе. Сотрудник при этом не удаляется — его задачи, комментарии и история остаются на месте.

Поля карточки сверх очевидных:

Поле Формат
messenger объект {"type": "Telegram", "nickname": "@name"} — нужны обе половины
status свободный текст рядом с именем, с эмодзи (в живой карточке — 👍На смене)
vacation_days отправляется строкой, как в документации Pyrus

Неполный messenger сервер отклоняет: Pyrus такой объект молча выбрасывает, и вызов выглядел бы успешным.

Инструмент Описание
get_contacts Получить контакты
get_bots Получить список ботов
get_profile Получить профиль текущего пользователя
get_members Получить участников организации
get_member Получить участника по ID
create_member Создать участника
update_member Обновить участника
get_roles Получить роли
get_role Получить роль по ID
create_role Создать роль
update_role Обновить роль
delete_role Удалить роль

Телефония

Интеграция для контакт-центров. Требует настроенного расширения в Pyrus.

Инструмент Описание
register_call Зарегистрировать звонок — создаёт задачу или комментирует существующую
attach_call_record Прикрепить аудиозапись и метаданные к звонку
register_call(account_id="uniqueID12345",
              from_number="+79774221338", to_number="+74953009080",
              internal_number="200",
              mappings=[{"code": "CallStartTime",
                         "value": "2021-12-23T00:11:32Z"}])

internal_number — то, как звонок находит ответственного: Pyrus ищет сотрудника с таким рабочим телефоном. Без него звонок придёт без назначенного.

В ответе task_id, is_new_task и responsible_person.

Звонок для attach_call_record определяется по external_id, либо task_id, либо паре from_number + to_number — одного номера мало. record_file — это guid уже загруженного аудиофайла (ac3, mp3, ogg, wav, wma), а не сам файл.

Коды mappings: CallDuration, CallStartTime, CallEndTime, PhoneNumberFrom, PhoneNumberTo, Rating, RatingComment, RatingDate.

⚠️ Не проверено на живом API. Реализовано по документации Pyrus; тестового контура с настроенной телефонией не было. Ошибки, которые стоит узнавать: unrecognized_call_id, unrecognized_account_id, invalid_field_mapping_code, unrecognized_attachment_id, unsupported_record_file_format, webhook_is_disabled.

Награды

Награда вручается и отзывается автоматически, когда счётчик сотрудника пересекает порог. Пороги задаются на награду, счётчик — на пару «сотрудник + награда».

set_award_threshold(award_id=7, grant_threshold=1, revoke_threshold=91)
increment_award_counter(member_id=1261579, award_id=7)

0 отключает соответствующую сторону. Если работают обе, revoke_threshold должен быть больше grant_threshold — иначе награду отзывало бы сразу же после вручения; такую пару сервер отклоняет.

Изменение порогов не пересчитывает задним числом: у кого награда уже есть, тот её сохранит, пока его счётчик не сдвинется. Вручение и отзыв происходят в момент обновления счётчика, то есть на increment_award_counter или set_award_counter.

Боты

Боты — технические аккаунты для интеграций и вебхуков.

Инструмент Описание
get_bots Список ботов; include_fired=true покажет и отставленных
create_bot Создать бота (name обязателен, плюс hook_url, external_id)
update_bot Изменить: is_enabled, hook_url, rights, locale и прочее
delete_bot Удалить безвозвратно

Две особенности, проверенные на живом API:

Вывести бота из строя обратимо — это update_bot(is_enabled=false), а не delete_bot: удаление необратимо и ломает всё, что завязано на его hook_url.

Календарь

Инструмент Описание
get_calendar_tasks Получить задачи из календаря за период

Файлы

Содержимое файлов не проходит через MCP-сервер и не попадает в контекст агента: инструменты выдают адрес и токен, а сам обмен байтами агент делает напрямую с Pyrus. Поэтому размер файла ограничен только Pyrus.

Инструмент Описание
get_upload_target Куда и с каким токеном загружать файл (агент делает multipart-POST сам, получает GUID)
get_file_download_url Ссылка на скачивание файла по ID + токен (агент качает сам)
attach_files_to_field Положить уже загруженные файлы в поле формы типа file
attach_new_file_version Прикрепить файл новой версией существующего (root_id)

Загрузка: возьмите url и access_token, отправьте POST с multipart/form-data, файл в поле file, заголовок Authorization: Bearer <access_token>. Полученный GUID передайте в create_task или comment_task, чтобы прикрепить файл.

Файл живёт в контуре, куда его загрузили. Если работаете в контуре клиента, передайте те же access_token/api_url и в get_upload_target — иначе Pyrus ответит No file with that ID has been uploaded.

Версии файлов

attachments в comment_task/create_task принимает не только GUID-строки, но и объекты:

Форма Что делает
"<guid>" новый файл
{"guid": "...", "root_id": 123} новая версия файла 123
{"attachment_id": 123} прикрепить уже приложенный файл
{"url": "...", "name": "..."} внешняя ссылка

Две особенности Pyrus, о которые легко споткнуться:

Поля типа file

Заполняются только отдельным шагом — при создании задачи fields с файловым полем игнорируется. Значение — числовые id вложений строками, не GUID. Поле аддитивно: значения добавляются, очистить через API нельзя.

{"task_id": 1042, "field_id": 16, "file_ids": [449339042, 449339043]}

База знаний

Инструмент Описание
get_knowledge_base_entity Получить статью или раздел
create_knowledge_base_entity Создать статью или раздел
update_knowledge_base_entity Обновить статью или раздел
delete_knowledge_base_entity Удалить статью или раздел
get_knowledge_base_structure Получить структуру базы знаний
get_knowledge_base_permissions Получить права доступа
update_knowledge_base_permissions Обновить права доступа

Метка правки в задаче

Пока сервер пишет в задачу, она лежит в личном списке владельца кредов «🤖 AI MCP Pyrus», а сразу после успешного коммита выходит из него. Список приватный: посторонние его не видят.

Список создаётся сам — при первой записи каждого пользователя, в его собственном Pyrus и по его реквизитам. Существующий находится по точному имени, в том числе внутри дерева; второй такой же не заводится.

Метка едет в том же теле, что и правка, а не отдельным вызовом: изменение и метка атомарны, лишней записи в ленте от постановки не появляется. Отдельным вызовом идёт только снятие — с skip_notification и skip_auto_reopen. Второй флаг обязателен: close_task — тоже комментарий, и без него снятие метки заново открыло бы только что закрытую задачу.

Оставшаяся метка осмысленна. Если запись не прошла, метка не поставилась вместе с ней и задача чиста. Если прошла, а снятие нет — метка осталась, и это значит «изменение закоммичено, а последовательность не завершилась». Такие задачи и есть очередь на разбор; автоматически они не чистятся, иначе сигнал пропал бы.

Цена: один служебный комментарий на запись и одно перечисление списков на клиента (728 КБ на измеренном контуре, дальше кэш на 30 минут). Выключается переменной PYRUS_TASK_MARKER=0 без выкатки кода.

Где какие поля лежат

Реестр, календарь, Входящие и поиск отдают разные поля, и перепутать их дорого: инструмент на неверном источнике отвечает не на свой вопрос и делает это тихо. Сроков, например, в реестре нет вовсе — они только в календаре и, частично, в поиске.

Таблица источников с замерами — guide/что-где-лежит-в-pyrus.md. Её же читает агент: read_guide("источники").

Справочник для агента

Знание про Pyrus стоит по-разному в зависимости от того, где оно лежит.

Описания инструментов платятся каждым агентом при каждом подключении: 88 описаний — это около 163 КБ (перемерено 30.09.2026 по живому list_tools()), около 46 тысяч токенов, и неважно, понадобится ли сегодня хоть одно из них. Поэтому там остаётся только то, без чего первый же вызов будет неверным.

Остальное лежит в guide/ и читается по требованию — инструментом read_guide("реестр") или ресурсом pyrus://guide/registry. Платит только тот агент, который решил, что ему это нужно, а решает он по оглавлению (read_guide() без аргумента), которое занимает килобайт.

Страница О чём
sources (источники) Что в каком источнике есть: реестр, календарь, Входящие, поиск. Таблица с замерами
registry (реестр) Выборка одним обходом, include_archived, что делать с отказом, массовая запись
search (поиск) Поиск по аккаунту и три его молчаливые ловушки
limits (лимиты) Потолки Pyrus и сервера, фильтры, которые принимаются и не применяются
form-export (экспорт-формы) 36 разделов вместо 7 у get_form: маршрут с условиями, права, SLA, скрипты, справочники. 403 не значит «формы нет», справочники меняются сами по расписанию
audit (аудит-формы) 31 код находок audit_form/score_form_config по категориям, почему часть проверок реализована не полностью, как считается score_0_3
workflow (маршрут) Почему graph_edges строго последовательны, как считается sla_hours при нескольких политиках на шаге, откуда мишматчи в access_matrix, почему diff_form_exports не поддерживает весь контракт PRD

Третий канал — сам отказ. Отклоняя запрос, сервер не только объясняет причину, но и называет страницу: «Подробнее: read_guide("реестр")». Это самое дешёвое обучение из трёх — приходит ровно тому, кто ошибся, и ровно тогда, когда ошибка ещё не стала выводом.

Страницы в guide/ — те же файлы, что читают люди в репозитории. Копии нет намеренно: второй экземпляр текста однажды разойдётся с первым, и узнать об этом будет неоткуда. Инструмент, ресурс и человек открывают один файл.

Промпты (Prompts)

Промпт — третий примитив MCP: не действие и не данные, а заготовка сценария, которую запускает человек. Место здесь заслуживает только то, что кодирует знание, легко теряемое: важен порядок, есть невидимая ловушка или цена ошибки высока, а сама ошибка не заметна.

Промпт Что делает
export_register(form_id, fields, period) Полная выгрузка реестра формы без потери данных: обходит и потолок Pyrus в 20 000 задач, и лимит размера ответа
inbox(horizon, mode) Разбор Входящих и календаря вместе с человеком: оценка по личной модели весов, раскладка по дереву Списков, откладывание
delegated(person, narrow) Что человек поручил другим: поиск по автору, вычитание собственных задач, группировка по исполнителю и давности
stale(days, person, act) Поручения без движения дольше N дней. Отбор идёт по modified_date прямо из выдачи поиска — без единого лишнего запроса; запись только по явному «напомнить»
person_360(person, narrow) Полная картина по человеку: три роли — автор, ответственный, участник — тремя вызовами, потому что один вызов отвечает только про одну
by_catalog_value(form_id, field, value) Задачи, где поле указывает на строку справочника. Держит границу: сквозного поиска по значению нет, фильтр живёт только внутри одной формы

Промпту inbox нужны файловые инструменты у клиента — модель весов живёт файлами рядом с агентом, а не в Pyrus. Инструкция агенту: docs/промпт-inbox.md. Разбор того, какие сценарии заслуживают быть промптами, а какие нет: docs/prompts.md.

Примеры использования

Получить список форм

Покажи все формы в Pyrus

Создать задачу

Создай в Pyrus задачу "Подготовить отчет за Q3" и назначь на user@example.com

Найти задачи по форме

Покажи все задачи из формы "Заявки на отпуск" за последнюю неделю

Выгрузить большой реестр

Посчитай по форме 123 задачи за 2025 год в разрезе статусов

Модель сама выберет узкий запрос; вручную это выглядит так:

get_form(form_id=123, summary=true)          → найти id нужных полей
get_registry(form_id=123, format="csv",
             field_ids=[3, 12],
             include_archived=true,
             created_after="2025-01-01T00:00:00Z",
             created_before="2026-01-01T00:00:00Z")

include_archived=true обязателен, когда вы считаете: по умолчанию реестр отдаёт только открытые задачи и молчит об этом — на одной форме это 681 задача вместо 3046.

Добавить комментарий

Добавь комментарий к задаче 12345: "Документы проверены, можно двигаться дальше"

Удобные обёртки

Закрой задачу 12345 с комментарием "Всё готово"
Переназначь задачу 12345 на user@example.com
Добавь согласующего Иванова к задаче 12345
Обнови поле "Статус" в задаче 12345 на значение "На проверке"
Покажи просроченные задачи из формы "Заявки на отпуск"

Работа с каталогами

Покажи каталог "Контрагенты"

Права доступа к форме

Кто имеет доступ к форме 123456?
Выдай сотруднику 1733 уровень member на форму 123456

Права задаются по числовому id сотрудника, почта и имя не принимаются:

change_form_permissions(form_id=123456,
                        permissions={"1733": "member",
                                     "1731": "administrator"})

Уровни, от широкого к узкому: administrator, manager, member, none.

none отзывает доступ — человек исчезает из списка целиком (проверено вживую), а не остаётся в нём с нулевым уровнем.

Пятый уровень, restricted_manager, в документации Pyrus есть, но через API не работает: запрос отвечает 200 и оставляет уровень прежним. Сервер его отклоняет с объяснением — иначе вызов выглядел бы успешным, ничего не изменив. Задать этот уровень можно только в интерфейсе Pyrus.

Три вещи, о которых стоит помнить:

В ответ приходит весь список доступа после изменения, в том же виде, что отдаёт get_form_permissions, — не просто подтверждение. Перечитывать права после вызова не нужно.

Сервер сверяет запрошенное с применённым. Если Pyrus ответил успехом, но уровень не изменился, в ответе появятся поля not_applied и warning с перечнем того, что не применилось, — молчаливого «успеха» не будет.


Архитектура

pyrus_mcp/
├── __init__.py       # Версия пакета
├── __main__.py       # Точка входа python -m pyrus_mcp
├── client.py         # HTTP клиент для Pyrus API v4 (+ один метод v3) + обёртки
└── server.py         # MCP сервер (MCPServer) с 88 инструментами

tests/
├── test_client.py         # Тесты HTTP клиента (моки)
├── test_server.py         # Тесты MCP tools (моки)
└── test_large_payloads.py # Лимит ответа, CSV, сужение форм, таймауты

deploy/
├── ansible/          # Ansible playbook для VDS
├── terraform/        # Terraform для Hetzner/DO
├── launchd/          # Шаблон агента отложенной выкатки (macOS)
└── README.md         # Гайд по деплою

guide/                # Справочник: читают и люди, и агенты (read_guide)
├── что-где-лежит-в-pyrus.md
├── инструкция-агенту-по-реестру.md
├── поиск-по-аккаунту.md
└── лимиты-и-потолки.md

scripts/
├── deploy-when-window-opens.sh  # Тесты → коммит → railway up → проверка health
├── mcp_daily_report.py          # Суточный отчёт по вызовам
├── railway_snapshot.py          # Снимок переменных Railway
└── park_token.py                # Выдать или отозвать токен доступа

Почему один инструмент ходит в v3

Весь сервер работает с API v4, кроме find_tasks: он обращается к https://pyrus.com/restapi/searchtasks из API v3. Это не наследие, а необходимость — списка задач в v4 не существует вовсе, GET /v4/tasks отвечает 405 (проверено 18.09.2026). В v4 достижимы только конкретная задача по id и реестр конкретной формы, поэтому вопрос «где по всему аккаунту» отвечать было нечем.

Что важно знать про этот контур:

Зависимости


Тестирование

pip install pytest pytest-asyncio
python -m pytest tests/ -v

Тесты покрывают: - Хелперы форматирования дат и очистки payload - Все request dataclasses (конвертацию в dict) - HTTP клиент с замоканными запросами (auth, CRUD задач, форм, пользователей, ролей, файлов) - MCP tools (проверка вызовов и возвращаемых JSON) - Работу с большими выборками: отказ вместо обрезки, компактная сериализация, CSV-реестры и срезание BOM, сужение формы через summary/field_ids, таймауты и то, что вызов не блокирует event loop

Проверка на живом Pyrus

Моки не отвечают на вопросы вроде «в какой кодировке приходит CSV» — для этого есть отдельный скрипт. Реквизиты берёт из .env или pyrus_test.env и никогда их не печатает.

python3 live_check.py                      # только чтение
python3 live_check.py --write --cleanup    # плюс запись, с уборкой за собой
python3 live_check.py --form-id 123456     # на конкретной форме

Среди проб — сравнение JSON и CSV на реальном реестре: именно так выяснилось, что CSV даёт 32,7×, приходит с BOM и несёт колонку close_date, которой в JSON нет. На формах с табличными полями состав колонок может отличаться — если такие в работе есть, прогоните --form-id по одной из них.


Деплой

Для интенсивного использования агентами рекомендуется развернуть сервер в облаке или на VDS:

Платформа Сложность Стоимость Лучший выбор для
VDS (Ubuntu) 🟠 Низкая $3-10/мес Интенсивная нагрузка, полный контроль
Railway 🟡 Очень низкая ~$5/мес Новичков, быстрый старт
Render 🟡 Очень низкая ~$7/мес Предсказуемый биллинг
Fly.io 🟠 Низкая ~$2-5/мес Cost-оптимизации
Локально 🟢 Нулевая Бесплатно Личного использования

Гайды по деплою

У токена три возможных уровня: vip — без лимитов, gold — вдвое шире базовых, regular (по умолчанию) — базовые: 60 запросов в минуту (всплеск 30) и 4 одновременных. На весь процесс — 24 одновременных независимо от уровней. Превышение даёт 429 с retryable: true и Retry-After.

Уровень хранится третьим полем записи реестра (owner:<sha256>:vip), меняется через python3 scripts/park_token.py tier <владелец> gold и не требует перевыпуска токена.

Одиночная переменная MCP_AUTH_TOKEN с сырым токеном отключена 20.09.2026: реестр хранит хеши, а она — само значение, и для публичного адреса это лишняя дверь. Вернуть при необходимости: python3 scripts/park_token.py unpark legacy. Настраивается переменными MCP_RATE_PER_MINUTE, MCP_RATE_BURST, MCP_CONCURRENCY_PER_TOKEN, MCP_CONCURRENCY_TOTAL — см. DEPLOY.md.

Считается по владельцу токена, а не по IP: агенты сидят за общими адресами облаков. Счётчики в памяти процесса, поэтому при нескольких репликах лимит умножается на их число.

Публичный адрес

Сервис отвечает на https://mcp.pplus.software/mcp. Прежний адрес pyrus-mcp-production.up.railway.app — тот же сервер под другим именем и продолжает работать: клиентов переводят без спешки.

Домен настроен в Railway (Settings → Networking → Custom Domain), сертификат выпускается и продлевается платформой. На сборку и выкатку это не влияет никак — railway up грузит рабочее дерево, а имя сервиса живёт отдельно.

💡 Если у тебя уже есть VDS с Ubuntu — используй deploy/ansible/. Одна команда ansible-playbook настроит всё: Docker, Nginx, SSL, автозапуск.

Транспорты

Переключение через переменную окружения:

export MCP_TRANSPORT=streamable-http

Лицензия

MIT