Pyrus BPMS · Model Context Protocol

Работа с Pyrus через MCP

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

Адрес https://mcp.pplus.software/mcp

Прежний адрес на up.railway.app продолжает работать: это тот же сервер под двумя именами. Новые подключения — на собственный домен.

Подключение

Транспорт — streamable-http. В конфигурацию своего MCP-клиента добавьте адрес сервера и три заголовка:

{
  "mcpServers": {
    "pyrus": {
      "url": "https://mcp.pplus.software/mcp",
      "transport": "streamable-http",
      "headers": {
        "Authorization": "Bearer <токен доступа к серверу>",
        "X-Pyrus-Login": "свой-email@example.com",
        "X-Pyrus-Security-Key": "свой-ключ-Pyrus"
      }
    }
  }
}

Если у вас уже есть действующий токен Pyrus, вместо пары «логин + ключ» пришлите X-Pyrus-Access-Token.

Где взять ключ Pyrus: Настройки → Авторизация → Секретный API ключ в вашем Pyrus. Для бота логином служит его собственный email, а не email того, кто бота завёл.

Проверка связи

GET /health отвечает 200 без всякой авторизации. Если он отвечает, а вызовы — нет, дело в токене или реквизитах, а не в сети.

Два уровня доступа

Их легко перепутать, а лечатся они по-разному. Первый решает, пустят ли вас на сервер. Второй — чьи данные Pyrus вы увидите.

ЗаголовокЧто решаетОткуда берётся
Authorization: Bearer … Пускать ли к серверу вообще. Один на всех пользователей сервера. Выдаёт администратор сервера
X-Pyrus-Login
X-Pyrus-Security-Key
Чьи данные Pyrus доступны. У каждого свои. Ваш аккаунт Pyrus

Реквизиты Pyrus сервер читает заново на каждом запросе и нигде не сохраняет. Ваш коллега с тем же токеном доступа увидит свой Pyrus, а не ваш. Права внутри Pyrus остаются вашими: если форма вам закрыта, MCP её не откроет.

Несколько контуров Pyrus

Это нужно ботам, которые обслуживают организации-клиентов. Заголовки задаются один раз на всё соединение, а токен клиента приходит свой в каждом вебхуке — поэтому любой инструмент принимает два необязательных параметра:

ПараметрНазначение
access_tokenвыполнить вызов от имени этого токена
api_urlконтур клиента, например https://api.pyrus.com/v4
{
  "task_id": 372965117,
  "access_token": "<токен из вебхука>",
  "api_url": "https://api.pyrus.com/v4"
}

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

Частая ошибка

Файл, загруженный токеном одной организации, невозможно приложить к задаче другой. Работая в контуре клиента, передавайте те же access_token и api_url и в get_upload_target.

Инструменты

Полный список — tools/list. Ниже сгруппированы те, что нужны чаще других. Метки: чтение запись необратимо

Задачи

get_taskЗадача целиком, вместе с комментариями, вложениями, parent_task_id и linked_task_ids. Параметр include оставляет только нужные части чтение
get_tasksНесколько задач за один вызов. Ошибка по одной не срывает остальные. Пачка не отказывает целиком: приходит то, что поместилось в лимит, невлезшие id возвращаются в remaining_task_ids, а задача крупнее всего бюджета (замер: 217 КБ переписки) откладывается в oversized_tasks и не обрывает остальных. include и last_comments обрезают каждую задачу ДО подсчёта бюджета: 24 задачи за вызов против 5 чтение
create_taskСоздать задачу — простую или по форме. Поддерживает parent_task_id для подзадач запись
comment_taskКомментарий, смена полей, согласующие, подписчики, вложения, внешний канал запись
update_task_fieldsОбновить поля формы запись
close_task / reopen_taskЗавершить или переоткрыть запись
assign_taskПереназначить исполнителя. Человек задаётся числовым id, почтой или объектом запись
add_approversДобавить этапы согласования. approvers — список списков: внешний задаёт очередь этапов, внутренний — кто согласует на этом этапе запись
delete_taskУдалить задачу навсегда необратимо
search_tasksПоиск по форме в диапазоне дат. Умеет то же, что get_registry — field_ids, format, обход через page_size + cursor: он ей и делегирован, второго диалекта обхода нет чтение
read_guideСправочник: страницы про источники данных, реестр, поиск и лимиты — то, что не влезло в описания инструментов. Без аргумента отдаёт оглавление на килобайт; см. Справочник для агента чтение
find_tasksПоиск по всему аккаунту, а не по одной форме — см. отдельный раздел: по автору, ответственному, участнику, Спискам и тексту, поверх всех форм и свободных задач тоже. Единственный способ спросить «какие задачи я поставил другим» — author="me": в API v4 списка задач нет вовсе, GET /v4/tasks отвечает 405. Закрытые не приходят, пока не попросить include_closed=true. Пагинации у этого метода нет ни в каком виде — выборку сужают фильтрами, а об обрыве сервер говорит словами в поле note. В ответе — опознание задачи (id, первые 120 символов текста, ответственный, даты, списки); задачу целиком берут через get_task чтение
get_overdue_tasks, get_tasks_due_soonЗадачи по сроку. Сроки берутся из календаря, а не из реестра — в реестре их нет вовсе (замер: задача реестра несёт только create_date, current_step, fields, id, last_modified_date, last_note_id). form_id — фильтр, а не требование чтение

Формы, реестры, списки

get_forms / get_formШаблоны форм и их поля. flatten: true раскрывает поля, спрятанные в группах; summary: true и field_ids нужны для больших форм — см. Объём ответов чтение
list_catalogsВсе справочники аккаунта: id, имя, версия. Без строк чтение
create_catalogСоздать справочник: name, catalog_headers — имена колонок, items — строки, каждая списком значений по порядку колонок запись
plan_registry_walkДелит большой реестр на окна, пролезающие под потолок в 20 000 задач — см. Реестры и выборки чтение
get_registryРеестр задач по форме — см. отдельный раздел, там важное про include_archived и format: "csv" чтение
get_lists / get_list / get_task_listСписки и задачи в них. Дерево отдаётся плоско: у каждого узла parent_id, вложенность из него собирается обратно. summary: true отвечает тремя числами, page_size + cursor ведут обход — на измеренном контуре дерево оказалось из 3177 узлов и 728 КБ, целиком оно не проходит потолок ответа чтение
create_list / update_listСоздать и изменить список: имя, родитель, цвет запись
delete_listУдалить список. Задачи при этом не удаляются необратимо
get_form_permissions / change_form_permissionsДоступ к форме: кто и на каком уровне — см. отдельный раздел чтение запись
get_form_export / normalize_form_exportПолный веб-экспорт формы: 36 разделов вместо 7 у get_form — маршрут, права, SLA, статусы, эскалации, скрипт формы, справочники, печатные формы. Нормализован по умолчанию; кэш 15 минут — см. экспорт конфигурации формы чтение
get_form_configКонфигурация формы по секциям (meta, fields, steps, register, workflow, statuses, sla, access, external, scripts, escalations, helpdesk, print_forms) — легче полного экспорта чтение
audit_formДетерминированный аудит формы без LLM: 30 проверок по гигиене полей, правам, процессу/SLA/эскалациям, скриптам и справочникам — тот же экспорт всегда даёт те же находки. checks=[...] сужает до конкретных кодов, severity_floor отсекает находки ниже уровня чтение
score_form_configЗдоровье автоматизации формы одним числом от 0 до 3 плюс 8 подоценок по категориям (fields_hygiene, process_definition, rights, sla, escalations, scripts, catalogs, external) — те же находки audit_form, но посчитанные. Детерминированно и воспроизводимо: тот же экспорт всегда даёт ту же оценку чтение
get_form_workflowМаршрут формы именованными шагами: число гейтящих условий, id ролей, допущенных к шагу, SLA в часах (наибольшее среди политик шага), активен ли общий календарь SLA, ссылка на подпроцесс. Плюс список наблюдателей аккаунта и graph_edges для рендера — строго step[i] → step[i+1], потому что экспорт Pyrus даёт условия ДОСТУПА к шагу, а не ветвящуюся МАРШРУТИЗАЦИЮ между шагами чтение
render_form_workflowДиаграмма маршрута get_form_workflow. format="mermaid" (по умолчанию и пока единственный) — простой текст Mermaid flowchart, вставляется в любой Mermaid-совместимый просмотрщик; пустые (безымянные) шаги помечены визуально, рёбра подписаны числом условий и SLA шага. Другой формат отклоняется как invalid_argument чтение
get_form_access_matrixКто видит форму: id людей, сгруппированные по уровню доступа (2/3/4), записи access_levels без уровня или без человека, все внешние пользователи, все роли, упомянутые где-либо в маршруте, и несоответствия — роль есть в маршруте, а ни у одного её участника нет доступа к форме вовсе чтение
diff_form_exportsСтруктурный диф экспорта формы против базы: другой доступной формы (against_form_id, например шаблона) или сырого JSON экспорта, сохранённого раньше через get_form_export(raw=true) (against_raw_export) — ровно один из двух. Возня с содержимым элементов справочника (переупорядочивание, дрейф значения/хэша по своему расписанию) никогда не попадает в changed — см. catalog_only_changes; добавленный/удалённый элемент справочника — настоящее структурное изменение и в changed попадает. Полный контракт PRD называет и snapshot_id/fetched_at, которым нужно постоянное хранилище снепшотов — этого у сервера пока нет чтение
get_form_fieldsПлоский список полей с путём в таблице, шагами редактирования и обязательности, сводкой видимости: какие поля и роли гейтят каждое чтение
get_form_register_viewПорядок полей в реестре, флаг скрытия, XLSX-ссылка самого регистра и обнаружение сирот чтение
resolve_form_refsПреобразовать id в имена для ролей, сотрудников, справочников, связанных форм, статусов и полей — с явным списком что не удалось разрешить чтение
get_members / create_member / update_member / block_memberСотрудники — см. отдельный раздел, там важное про увольнение чтение запись
get_knowledge_base_*База знаний — см. отдельный раздел чтение запись

Люди, роли, входящие

get_profile, get_contacts, get_membersПрофиль, контакты, сотрудники чтение
get_roles / get_roleРоли; в ответе есть member_ids чтение
get_botsБоты организации чтение
get_inboxВходящие задачи чтение
get_calendar_tasksЗадачи календаря за интервал: даты, маска фильтра, include_meetings. Даты обязательны — /v4/calendar без них отвечает 500, а не 400, и со стороны это выглядит как упавший сервис чтение
get_meetingsВстречи календаря за окно вокруг сегодняшнего дня (days_back, days_ahead). Исправлено 20.09.2026: раньше инструмент читал встречи из Входящих и потому возвращал ноль всегда — ключа meetings в ответе /v4/inbox нет вовсе. Теперь берёт их из календаря с include_meetings, как это делает score_inbox. Пустой ответ чаще всего означает узкое окно: на одном аккаунте сутки дали 0 встреч, месяц — 6 чтение
score_inboxОценить входящие, календарь и встречи по личной модели весов и вернуть таблицу приоритетов. Сырьё наружу не выходит: 60 элементов — 23 КБ против 676 КБ задач целиком. См. Готовые сценарии чтение

Кроме перечисленного есть справочники (get_catalog, sync_catalog, update_catalog_items), объявления, база знаний и управление участниками — всего 88 инструментов.

Реестры и выборки

Самый частый источник неверных выводов. Прочитайте до того, как поверите цифре, полученной из get_registry.

По умолчанию видна только половина

Проверено на реальной форме

Без include_archived реестр возвращает только открытые задачи. Закрытые просто отсутствуют, и ничто на это не указывает. На измеренной форме это дало 681 задачу вместо 3046 — 78% выборки пропало молча.

Считаете, анализируете, сверяете историю — передавайте include_archived: true всегда.

Как отличить открытые от закрытых

Проверено на живом API

Простой способ — format: "csv". CSV-реестр несёт колонку close_date, которой в JSON нет: непустая — задача закрыта. Один запрос, без сравнения выборок.

{"form_id": 1504224, "include_archived": true, "format": "csv"}

В JSON ни is_closed, ни close_date не приходит. Если формат менять нельзя, ответ даёт поведение по умолчанию:

Открытыевызов без include_archived
Всеinclude_archived: true
Закрытыеразница между этими двумя выборками
Не угадывайте по номеру шага

Соблазнительно считать, что закрытые задачи уходят на последний шаг. На проверенных данных это неверно: задача на шаге 20 была открыта, а закрытые встречались на шагах 2, 3 и 5. Закрытых было 2365, а на «последнем» шаге — 373. Ошибка в разы.

Формат CSV

format: "csv" отдаёт реестр таблицей вместо JSON. JSON повторяет id, type и name каждого поля на каждой задаче; CSV называет колонки один раз в шапке. На живой форме из 33 полей 50 задач весили 1 110 006 Б в JSON и 33 988 Б в CSV — в 32,7 раза меньше. Это разница между отказом и ответом с запасом.

Но выбирать формат чаще приходится не по размеру, а по составу колонок — они разные:

CSVJSON
close_dateестьнет
current_stepнетесть
Множественные и табличные поляплохо ложатся в колонкусо структурой

Считаете, агрегируете, просматриваете много задач — берите CSV. Нужен шаг маршрута или вложенная структура поля — JSON.

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

Фильтр по значению поля

Параметр field_filters, ключ — числовой id поля:

{"form_id": 1504224, "include_archived": true,
 "field_filters": {"6": 958621}}

Имена и коды полей Pyrus не принимает — только id. Узнать его можно через get_form с summary: true: поля бывают спрятаны внутри групп, и на измеренной форме так лежали 12 из 45, включая «Этап» и «Открыта / Завершена». summary добирается до вложенных полей так же, как flatten, но не тащит с собой списки вариантов — на большой форме это разница между ответом и отказом.

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

Потолок 20 000 задач

Молчаливое усечение

Pyrus обрывает реестр на 20 000 задачах и не сообщает об этом. В ответе нет ни счётчика, ни флага, ни курсора — только массив tasks. Ответ на 20 000 строк неотличим от полного реестра.

Хуже: отдаются самые свежие задачи. Пропадает история — ровно то, ради чего реестр обычно и запрашивают. Форма со 100 000 карточек вернула записи всего за три последних месяца, и выглядело это как исчерпывающая выгрузка.

Ограничение принадлежит Pyrus, а не серверу. Проверено голым HTTP-запросом мимо клиента: item_count больше 20 000 отвергается с 400 items_count_out_of_range, а без item_count приходят те же 20 000. Постраничности нет: offset, skip, page и cursor сервер молча игнорирует и возвращает ту же выборку.

Поэтому результат ровно на потолке отклоняется с ошибкой register_truncated, а не выдаётся как полный. Если вам осознанно нужны последние 20 000 и потеря истории приемлема — передайте allow_truncated: true.

Постраничный обход

Единственный способ пройти большой реестр — окна по датам. Одного прохода обычно мало: на форме с 63 000+ записей целые месяцы сами упирались в потолок и требовали дальнейшего дробления.

Делать это вручную не нужно. plan_registry_walk делит период пополам столько раз, сколько требуется, чтобы каждое окно пролезло под потолок, и возвращает готовый список:

plan_registry_walk(form_id: 2404991)
→ окон: 4, задач: 47 205, проб: 21
  2026-04-07 … 2026-04-13   17 469
  2026-04-13 … 2026-04-18    3 561
  2026-04-18 … 2026-05-31    8 349
  2026-05-31 … 2026-08-26   17 826

Дальше вызываете get_registry по каждому окну с его created_after и created_before. Пробы намеренно дешёвые — CSV с одним полем, — но каждая всё равно расходует запрос из бюджета Pyrus, поэтому их число ограничено параметром max_probes и указывается в ответе.

Три разных потолка

План снимает ограничение Pyrus, но не лимит размера ответа. Окно на 17 826 задач во всю ширину формы весило 14 МБ и было отклонено как response_too_large. После плана всё равно нужны format: "csv" и field_ids.

А над обоими стоит третий, про который легко забыть, потому что он не про данные, а про сервер:

ПотолокЧто бережётПорогЧем снимается
20 000 задачничего — это ограничение Pyrusжёсткийplan_registry_walk, окна по датам
response_too_large
MCP_MAX_RESPONSE_BYTES
контекст модели200 КБformat: "csv", field_ids, page_size
download_too_large
PYRUS_MAX_DOWNLOAD_BYTES
память сервера16 МБто же плюс item_count
Почему появился третий

Лимит размера ответа проверяется после того, как тело загружено и разобрано в объекты Python, — то есть от переполнения памяти он не защищал никогда. Полный реестр формы это сотни мегабайт, и процесс умирал внутри requests, не дойдя до проверки. Railway прислал «Deploy Ran Out of Memory», а клиент видел пустой поток и HTTP 200 — неотличимо от обрыва сети.

Теперь тело скачивается потоком и обрывается на пороге. Когда Content-Length известен заранее, отказ приходит до первого байта тела: скачивать, чтобы отказаться, значит потратить ровно ту память, которую защищаем. Замерено: JSON распухает в объектах Python в 4,7 раза.

Если планировщик вернул unsplit — там сутки, в которые уместилось больше 20 000 задач. Дробить датой дальше некуда, сужайте field_filters.

Чем управляет sort

sort: "id" — единственная сортировка, которую Pyrus здесь поддерживает, и она решает, какие именно 20 000 вам достанутся:

ЗапросЧто приходит
без sortновейшие 20 000
sort: "id"старейшие 20 000

Отсюда дешёвый приём: item_count: 1 вместе с sort: "id" возвращает самую первую задачу формы — быстрый способ узнать, с какой даты начинается её история.

Параметр steps фильтрует по current_step — номеру шага процесса, а не по полю формы с похожим названием.

Метка правки

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

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

Почему это не шумит

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

Почему снятие идёт с skip_auto_reopen

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

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

Метка никогда не важнее записи: если список не нашёлся и не создался, правка идёт без метки. Всё поведение гасится переменной PYRUS_TASK_MARKER=0.

Повторяющийся отказ

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

Зачем

Замер 02.09.2026: агент получил 32 одинаковых отказа за 25 минут и ни разу не сменил подход — первый на второй минуте, последний на последнем вызове. Со стороны сервера всё это время повторялась одна и та же вежливая фраза, и для агента без памяти между вызовами это выглядело нормой.

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

Объём ответов

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

Два разных вопроса

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

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

Нужна часть — сужайте: field_ids и columns отсекают содержимое, format: "csv" убирает разметку, summary: true отвечает «что это и насколько велико» за килобайт.

Приёмы складываются: короче строка — больше записей влезает в порцию.

Запись отвечает задачей целиком

Pyrus на любой комментарий отвечает всей задачей — вместе с историей переписки и вложениями. Замерено: 18 КБ на вызов add_subscribers, 22 541 Б на среднюю задачу. Тридцать записей подряд — полмегабайта чужого контекста ради тридцати подтверждений.

Поэтому у всех пишущих инструментов есть brief: true: в ответ приходит task_id, id нового комментария и состояние, которое запись могла сдвинуть — is_closed, current_step, list_ids. Полный ответ остался поведением по умолчанию.

Размер порции подбирается

page_size зависит от ширины строки, а не назначается раз навсегда. Замерено на живом реестре: 300 задач с одним полем весили 196 КБ и упёрлись в потолок, а 150 прошли свободно. Начинайте со 150 и делите пополам при отказе — отказ приходит быстро и стоит дёшево.

{
  "error": "response_too_large",
  "message": "The result is 36935 KB — about 10806 thousand tokens…
              It held 1687 tasks. Nothing was returned…"
}

Что помогает, по убыванию эффекта (замеры на живых формах):

ПриёмБыло → стало
format: "csv" — реестр таблицей1 110 006 Б → 33 988 Б (50 задач, 33 поля) — 32,7×
field_ids — только нужные поля16 МБ → 704 КБ
item_count — предел числа задач16 МБ → 976 КБ (100 задач)
Окно по датамгод → месяц: 355 КБ
field_ids + item_count вместе22 КБ

Приёмы складываются: format: "csv" вместе с field_ids снимает вопрос на большинстве реестров.

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

Форма тоже может не поместиться — измеренная весила 218 КБ, и почти всё это были списки вариантов у полей-справочников и полей с выбором. У get_form для этого два параметра:

summary: trueid, name, type и глубина по каждому полю; списки вариантов считаются, а не копируются
field_ids: [...]названные поля целиком, остальные отброшены

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

Задачи

У задач тот же приём — get_task с параметром include оставляет только названные части (на живой задаче 35 → 20 КБ). Идентификаторы задачи возвращаются всегда.

Отступов в ответах нет

Ответы сериализуются компактно, без пробелов и переносов. На живом реестре это 1 110 006 Б против 509 771 Б — те же данные, вдвое меньше токенов. Читаемость от этого не страдает: JSON разбирает агент, а не человек.

Ответ во внешний канал

Комментарий можно отправить не только в ленту задачи, но и туда, откуда пришло обращение — в мессенджер, почту или виджет. За это отвечает параметр channel в comment_task.

{
  "task_id": 373004711,
  "text": "Заявка принята в работу",
  "channel": {"type": "telegram"}
}

Можно передать и просто строку "telegram" — сервер сам приведёт её к нужному виду. Развёрнутая форма нужна, когда требуется указать конкретных отправителя и получателя: {"type": …, "to": {…}, "from": {…}}.

Допустимые типы

ГруппаЗначения type
Мессенджеры telegram, whats_app, max_messenger, viber, vk
Почта email
Каналы Pyrus web_widget, mobile_app, private_channel
Avito avito_messenger, avito_job
Что учесть

Канал должен быть заранее подключён к вашему Pyrus — иначе отправка не состоится. И учтите, что при отправке через внешний канал Pyrus заводит в ленте задачи две записи: сам комментарий и факт отправки. Это поведение Pyrus, а не дубль из-за повторного вызова.

Файлы

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

Загрузка

  1. MCP

    get_upload_target — при желании с параметром name.

    {
      "url": "https://api.pyrus.com/v4/files/upload?name=Чек.jpg",
      "form_field": "file",
      "access_token": "..."
    }
  2. Ваш HTTP-запрос

    POST на этот адрес: multipart/form-data, файл в поле file, заголовок Authorization: Bearer с полученным токеном. В ответе — guid.

  3. MCP

    Передайте guid в comment_task или create_task в массиве attachments — файл появится в задаче.

Скачивание

  1. MCP

    get_file_download_url с числовым file_id вложения (он есть в get_task). Возвращает url, filename и access_token.

  2. Ваш HTTP-запрос

    GET по этому адресу с заголовком Authorization: Bearer. Без токена Pyrus отвечает 401.

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

Чтобы загруженный файл стал новой версией существующего, а не отдельным вложением, нужен root_id. Для этого attachments принимает не только строки-guid, но и объекты:

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

Готовый инструмент для этого — attach_new_file_version(task_id, guid, root_id).

Две ловушки Pyrus

root_id — это id первой версии, и он не меняется. Добавляя версию 3, укажите тот же root_id, что и для версии 2, а не id версии 2 — иначе цепочка версий разорвётся.

guid одноразовый. После попытки прикрепления — даже неудачной — он недействителен: Pyrus ответит An ID can only be used once. Для следующей версии загрузите файл заново.

Поля типа «файл»

Поле формы с типом file ведёт себя не так, как остальные поля, и это стоит знать заранее.

Заполняется Только отдельным шагом, после создания задачи. При создании задачи файловое поле в fields просто игнорируется.
Значение Числовые id вложений строками — не guid и не объекты.
Поведение Аддитивное: значения добавляются к уже имеющимся. Очистить поле через API нельзя, только вручную в интерфейсе.
attach_files_to_field(task_id=1042, field_id=16,
                      file_ids=[449339042, 449339043])

Доступ к формам

get_form_permissions показывает, кто имеет доступ к форме и на каком уровне. change_form_permissions этот доступ меняет.

{"form_id": 123456,
 "permissions": {"1733": "member", "1731": "administrator"}}

Ключ — числовой id сотрудника. Почту и имя Pyrus здесь не принимает; id берут из get_form_permissions (у кого доступ уже есть) или get_contacts (все сотрудники).

УровеньЧто даёт
administratorполное управление формой, включая права доступа
managerуправление задачами по форме
memberработа с задачами по форме
noneотзыв доступа — человек исчезает из списка целиком
restricted_manager через API не работает

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

Это правка, а не замена списка

Меняются только те, кого вы назвали. Все остальные сохраняют свой уровень — вызов не переписывает список доступа целиком. Чтобы отозвать доступ, человека надо явно указать с уровнем none; просто не упомянуть его недостаточно.

Необратимо

Свои собственные права администратора отозвать можно, а вернуть их потом уже нельзя. Сначала прочитайте get_form_permissions и убедитесь, что хотя бы один администратор останется.

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

Проверено на живом API

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

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

Сотрудники

get_members и get_member читают, create_member заводит, update_member правит данные. Роли — get_roles, create_role, update_role, delete_role (у последнего есть task_receiver_id — кому передать задачи удаляемой роли).

Увольнение и блокировка — разные вещи

Легко упустить половину

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

Про fired нет уверенности

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

Поля карточки

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

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

Через API необратимо

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

Аватар

set_avatar назначает сотруднику уже загруженный файл по его guid. Сам файл он не передаёт — сначала загрузка (см. Файлы), потом этот вызов.

Награды и боты

Награды

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

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 — иначе награду отзывало бы сразу после вручения. Такую пару сервер отклоняет, не отправляя.

Прочитать текущее состояние: get_award_threshold(award_id) отдаёт оба порога награды, get_award_counter(member_id, award_id) — счётчик одного человека по одной награде вместе с assignment_date. Даты нет — значит награду он ещё не получал.

Задним числом не пересчитывается

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

Боты

Боты — технические аккаунты для интеграций и вебхуков: get_bots, create_bot, update_bot, delete_bot. У создания обязателен только name.

Проверено на живом API

Когда подходящих ботов нет, Pyrus отвечает {} — пустым объектом без ключа bots, а не {"bots": []}. В организации с единственным отставленным ботом запрос без include_fired вернул именно {}. Обращение сразу к ["bots"] на таком ответе сломается.

bot_settings — строка JSON, а не объект: Pyrus хранит её дословно и так же отдаёт боту. Объект сервер отклоняет, потому что API его примет, а настройка молча не заработает.

Удаление необратимо

delete_bot удаляет бота насовсем, и всё, что завязано на его hook_url, перестаёт работать. Чтобы вывести бота из строя обратимо, используйте update_bot(is_enabled: false).

Телефония

Не проверено на живом API

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

register_call регистрирует звонок: Pyrus либо создаёт задачу на форме телефонии, либо комментирует уже существующую по этому абоненту. Что именно произошло, видно в ответе — is_new_task, рядом task_id и responsible_person.

{"account_id": "uniqueID12345",
 "from_number": "+79774221338", "to_number": "+74953009080",
 "internal_number": "200",
 "mappings": [{"code": "CallStartTime", "value": "2021-12-23T00:11:32Z"}]}

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

Запись разговора

attach_call_record прикрепляет аудио к уже зарегистрированному звонку. Звонок опознаётся по external_id, либо task_id, либо паре from_number + to_number — одного номера недостаточно. Сервер проверяет это до отправки; сам Pyrus ответил бы unrecognized_call_id.

record_file — guid уже загруженного файла, а не сам звук: сначала загрузка (см. Файлы), потом этот вызов. Форматы — ac3, mp3, ogg, wav, wma.

Коды mappings

CallDuration, CallStartTime, CallEndTime, PhoneNumberFrom, PhoneNumberTo, Rating, RatingComment, RatingDate. Прочее сервер отклоняет — здесь, в отличие от большинства проверок, не потому что Pyrus промолчит, а просто чтобы сэкономить обращение: он ответил бы invalid_field_mapping_code.

Отдельно стоит узнавать webhook_is_disabled: расширение выключено в Pyrus, и пока его не включат обратно, ничего доходить не будет.

База знаний

Статьи и темы: get_knowledge_base_structure, get_knowledge_base_entity, create_knowledge_base_entity, update_knowledge_base_entity, delete_knowledge_base_entity, плюс права доступа.

Родитель Задаётся параметром parent_topic_id. Без него статья создаётся в корне базы.
Перемещение Достаточно передать новый parent_topic_id в update_knowledge_base_entity. Pyrus требует для этого отдельный служебный флаг — сервер проставляет его сам.
Обновление Всегда передавайте title, даже если меняете только тело или родителя: без него Pyrus отвечает 400 Invalid knowledge base request.
Размер статьи Тело на 14 тысяч символов проходит без проблем — проверено.
Права get_knowledge_base_permissions(entity_id) читает, update_knowledge_base_permissions(entity_id, inherit, readers, editors) меняет. inherit наследует права родительской темы, readers и editors — списки идентификаторов сотрудников.
Права переписываются целиком

update_knowledge_base_permissions списки заменяет, а не дополняет, и пропущенный параметр уходит в Pyrus пустым списком, а не отсутствует. Вызов с одними editors отправит readers: [] — то есть снимет всех читателей. Сначала прочитайте текущие права через get_knowledge_base_permissions, потом передавайте оба списка целиком.

Почему это стоит знать

В Pyrus перемещение статьи без служебного флага возвращает успех, но родителя не меняет. Через MCP это уже не воспроизводится, но если вы обращаетесь к Pyrus REST напрямую — учтите, иначе статьи «теряются» в корне без единой ошибки.

Объявления

Четыре инструмента: get_announcements и get_announcement читают, create_announcement публикует, comment_announcement добавляет комментарий к опубликованному.

Адресата нет, отзыва тоже

У create_announcement только два параметра — текст и вложения. Получателей выбрать нельзя: кому объявление достанется, решает Pyrus, а не вызов. Инструмента, удаляющего объявление, на сервере нет — опубликованное снять уже нечем. Это единственная запись в Pyrus, у которой не спросишь «кому» и не отменишь «поздно». Агенту стоит спрашивать подтверждение.

Вложения И у публикации, и у комментария — attachments, список идентификаторов из upload_file. См. Файлы.
Сколько отдаёт get_announcements принимает item_count, по умолчанию 100. Обхода курсором здесь нет: если объявлений много, ограничивайте число сами, иначе упрётесь в потолок ответа.
Метка правки Не ставится. Метка живёт на списках задач, а объявление — не задача.

Обход порциями

Постраничности у Pyrus нет: offset, skip, page и cursor он проглатывает и отдаёт ту же выборку. Поэтому обход строит сам сервер, а вызывающему остаётся идти по курсору.

Правило одно для обоих: передайте page_size, а дальше возвращайте полученный next_cursor обратно, пока он приходит. Не пришёл — обход закончен. Знать заранее ни размер выборки, ни своё положение в ней не нужно.

get_registry(form_id: 2363325, include_archived: true, page_size: 150)
→ 150 задач + next_cursor
get_registry(form_id: 2363325, include_archived: true, page_size: 150,
             cursor: "…")
→ 149 задач + next_cursor        ← одна отсеяна как повтор границы
…
→ короткая порция, курсора нет   ← конец
Три разных устройства

У каталога курсор режет уже скачанное: справочник приходит целиком, и порции нарезаются на сервере. В курсоре едет версия каталога — если его правили между порциями, обход отклоняется, потому что сохранённое смещение указывает уже не на ту строку.

У списков так же, но версии у дерева нет, и её роль играет отпечаток набора id в порядке обхода. Добавили или удалили список — все смещения после него уехали, и курсор отвергается. Переименование ничего не двигает и обход не рвёт. Дерево при этом приходит плоским: вложенность несёт parent_id.

У реестра так нельзя: целый реестр — сотни мегабайт. Курсор переводится в сужение на стороне Pyrus — sort: "id" отдаёт самые старые, item_count ограничивает порцию, created_after сдвигает окно. Задачи на границе окна Pyrus отдаёт повторно, поэтому уже выданные отсеиваются по id — отсюда 149 вместо 150 на второй порции.

Фильтры менять нельзя

Курсор несёт отпечаток фильтров, с которыми обход начался. Поменяете include_archived на середине — получите отказ, а не молча склеенные страницы из разных выборок. Итог по таким страницам описывал бы выборку, которой никогда не существовало.

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

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

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

ГдеКто платитЧто туда кладут
Описания инструментов и instructions Каждый агент при каждом подключении — 88 описаний весят около 163 КБ (перемерено 30.09.2026), около 46 тысяч токенов Только то, без чего первый же вызов будет неверным
read_guide и pyrus://guide/* Только тот, кто открыл страницу Таблицы, замеры, порядок действий, разбор ловушек
Текст отказа Только тот, кто ошибся Причина и адрес страницы: «Подробнее: read_guide("реестр")»

Страницы

read_guide()              — оглавление: что есть и о чём, на килобайт
read_guide("источники")   — что в каком источнике есть: реестр, календарь, Входящие, поиск
read_guide("реестр")      — выборка одним обходом, include_archived, массовая запись
read_guide("поиск")       — поиск по аккаунту и три его молчаливые ловушки
read_guide("лимиты")      — потолки Pyrus и сервера, молчаливые фильтры

Те же тексты доступны ресурсами: pyrus://guide — оглавление, pyrus://guide/registry — страница. Инструмент продублирован ресурсом не от избытка: ресурсы подтягивает клиент, и не каждый клиент это умеет, а инструмент вызывает сам агент — он работает везде.

Почему не в описаниях инструментов

Описание инструмента уезжает в контекст каждого агента при каждом подключении — даже если сегодня он работает с одним каталогом и про реестр не спросит ни разу. Страница же читается тем, кому она нужна, и тогда, когда нужна. Это та же экономика, что у summary=true у каталога: сначала дёшево понять, нужно ли, потом платить за содержимое.

Копии нет намеренно

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

Ресурсы

Кроме инструментов и промптов сервер отдаёт ресурсы — данные, про которые решает клиент, а не модель. Список виден дёшево, чтение происходит по требованию.

URIЧто отдаёт
pyrus://catalogsВсе справочники: id, имя, версия. Без строк — 23 КБ на 65 штук
pyrus://catalog/{id}Справочник целиком, в CSV
pyrus://catalog/{id}/summaryКолонки, типы и число строк
pyrus://guideОглавление справочника: что есть и о чём каждая страница
pyrus://guide/{topic}Страница справочника: sources, registry, search, limits

Справочников немного, меняются они редко, и ответ на «нужен ли мне этот» виден по метаданным — поэтому ресурсы заведены именно для них.

Почему не всё подряд ресурсами

Реквизиты Pyrus приходят в заголовках каждого запроса, поэтому URI вида pyrus://form/2360524 у разных клиентов означал бы разные контуры при одинаково выглядящем номере. Для каталога это безобидно — клиент видит список своего контура. А реестру нужен обход окнами, и обходить ресурс нечем. Страницы справочника — другое дело: они одинаковы для всех и от контура не зависят вовсе.

Что сервер говорит при подключении

Кроме списка инструментов сервер отдаёт клиенту instructions — короткий текст, который приходит один раз в ответе на initialize и попадает в контекст модели до первого вызова. Туда вынесено то, что нельзя вывести из докстринга отдельного инструмента.

Что там написано
  • Pyrus отвечает 200 и на то, чего не понял; проверять надо результат, а не код ответа.
  • field_filters с номером несуществующего поля Pyrus проглатывает и отдаёт весь реестр — единственная ловушка, которую сервер закрыть не может.
  • Реестр обрывается на 20 000 задачах; sort решает, какая половина истории пропадёт.
  • Потолок Pyrus и лимит размера ответа — разные вещи и снимаются разным.
  • Счёт задач без include_archived занижен.
  • На аккаунт — 5000 запросов за 10 минут, общих для всех, кто под ним работает.

Текст занимает около 1200 знаков против 74 000 знаков описаний инструментов (перемерено 30.09.2026) — примерно 1,6% того, что клиент и так получает при подключении.

Правило для правок

Утверждение в этом тексте проверяется против кода сервера, а не против поведения Pyrus. Первая редакция перечисляла четыре ловушки — неизвестный format, restricted_manager, неполный messenger, постраничные параметры, — и все четыре сервер к тому моменту уже перехватывал сам, до сети. Текст оставался правдой про Pyrus и был неправдой про то, с чем модель разговаривает: она потратила бы бдительность там, где ограда уже стоит.

Ограда, поставленная в клиенте или в теле инструмента, снимает факт с довольствия — его место занимает то, что закрыть нельзя.

Каждый факт в этом тексте закреплён тестом (tests/test_prompts.py), чтобы его нельзя было потерять при редактуре молча. Верхняя граница длины — тоже тест: текст уходит в каждое подключение, и раздувать его нечем.

Готовые сценарии

Кроме инструментов сервер отдаёт промпты — заготовки сценариев, которые запускает человек, а не модель. В интерфейсе клиента они обычно появляются как слэш-команды.

Промпт нужен там, где важен порядок действий или где есть невидимая ловушка. Обёртка вокруг одного вызова инструмента смысла не имеет — модель вызовет его и сама.

export_register — выгрузить реестр целиком

export_register(form_id, fields?, period?)

Разворачивается в сценарий из четырёх шагов: сузить поля через get_form(summary: true) → спланировать окна через plan_registry_walk → забрать каждое окно через get_registry с CSV и field_ids → склеить.

Существует потому, что наивная выгрузка теряет данные двумя независимыми способами, и ни один не виден в ответе: потолок Pyrus в 20 000 задач и лимит размера ответа сервера. Заодно сценарий напоминает про обязательный include_archived и про то, что close_date есть только в CSV.

Если поля переданы аргументом — совет их искать не выводится; если задан период — он подставляется в вызов планировщика.

inbox — разобрать входящие и календарь

inbox(horizon?, mode?)

Личная приоритезация: оценить входящие, встречи и сроки по своей модели весов, разложить задачи по дереву Списков, отложить лишнее. Считает не сценарий, а score_inbox — промпт держит порядок и правила обращения с человеком.

Приоритет складывается из шести вещей сразу: кто автор, из какой формы задача, где вы в ней (согласующий, ответственный, наблюдатель), в каких списках она уже лежит, какие слова в тексте и когда срок. Каждый вклад возвращается отдельным числом в поле why — чтобы на вопрос «почему это верхняя ступень» был ответ из шести слагаемых, и спорить можно было с правилом, а не с вердиктом.

Модель живёт у клиента

Пороги, веса и дерево направлений лежат файлом weights.json рядом с агентом, исключения — в decisions.json. Сервер о них не знает: он владеет схемой факторов, а значения приходят аргументом. Поэтому сценарию нужен клиент, умеющий читать и писать файлы, — иначе калибровка пройдёт, а назавтра всё начнётся заново.

Незнакомую версию модели или незнакомый фактор сервер отвергает, а не считает нулём: молча проигнорированное правило выглядит ровно как сломанная модель.

Что делает запись

Раскладка по спискам — это comment_task с added_list_ids, то есть комментарий в задаче. Текста у него нет, но запись в ленте появляется, и её видят участники. Поэтому перекладывается только то, у чего ступень действительно изменилась, а вся пачка подтверждается человеком одним словом до того, как что-либо уедет в Pyrus.

«Отложить» (scheduled_datetime_utc) вынимается из пачки и подтверждается отдельно: оно единственное убирает задачу из входящих. Отдельного «напомнить» у Pyrus нет — это то же самое действие.

delegated — что я поручил другим

delegated(person?, narrow?)

Единственный путь к этому вопросу: списка задач в API v4 нет, поэтому поиск идёт по автору через find_tasks. Сам поиск на вопрос не отвечает — он не умеет «автор я и ответственный не я», а автора в ответе не присылает вовсе. Сценарий делает вычитание по responsible.id, группирует по исполнителю и добирает подробности пачкой через get_tasks: замеренный обратный случай — 175 вызовов там, где хватало двух.

stale — что заглохло

stale(days?, person?, act?)

Поручения без движения дольше N дней (по умолчанию 14). Весь отбор делается по modified_date прямо в уже полученном ответе поиска — без единого дополнительного запроса. Перед напоминанием сценарий требует проверить, что тишина настоящая: дату двигает и техническая правка. Писать в Pyrus он начинает только по явному act="напомнить", и веером не пишет — список сначала показывают человеку.

person_360 — всё по человеку

person_360(person, narrow?)

Один вызов поиска отвечает только про одну роль: автор, ответственный и участник — три разных вопроса и три вызова. Сценарий объединяет срезы по task_id с пометкой ролей и запрещает складывать их длины: одна задача законно попадает в несколько срезов. include_closed=true обязателен, иначе считается только открытое, а Pyrus об этом не сообщает.

by_catalog_value — найти задачи по значению справочника

by_catalog_value(form_id, field?, value?)

Главное здесь — граница возможного: сквозного поиска по значению справочника в Pyrus нет, фильтр живёт только внутри одной формы. Сценарий ведёт от get_form(flatten: true) за числовым id поля к get_catalog за item_id строки и дальше к get_registry с include_archived. Фильтровать текстом нельзя — Pyrus отвечает ошибкой; несуществующий item_id даёт честный ноль, поэтому неожиданный ноль сверяют реестром без фильтра.

Каталог сценариев

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

Сценарий целиком

Задача: взять файл из комментария, подготовить новую редакцию и положить её в поле формы новой версией. Четыре вызова MCP и два ваших HTTP-запроса.

  1. MCP · get_task

    Найти вложение: comments[].attachments[], оттуда id и root_id. Заодно — id файлового поля в fields.

  2. MCP · get_file_download_url

    Получить ссылку и токен.

  3. HTTP · GET

    Скачать файл с заголовком Authorization: Bearer.

  4. MCP · get_upload_target

    Получить адрес загрузки — в том же контуре, где лежит задача.

  5. HTTP · POST

    Загрузить новую редакцию, получить guid.

  6. MCP · attach_new_file_version

    Прикрепить с root_id исходного файла — появится версия 2. Если файл нужен именно в поле формы, вместо этого attach_files_to_field с числовым id вложения.

Ограничения

Лимит запросов Pyrus

5000 запросов за 10 минут, отдельно на каждую учётную запись. Поскольку каждый работает под своими реквизитами, бюджеты не общие: активный коллега не выжигает ваш лимит.

При исчерпании сервер подождёт и повторит запрос, а если Pyrus просит ждать дольше нескольких секунд — вернёт понятное сообщение с указанием, сколько ждать. Повторять вызов сразу же не нужно: это только продлевает блокировку.

Необратимые операции

delete_task и delete_list выполняются без подтверждения и не откатываются. Чтобы просто завершить задачу, используйте close_task — она останется в системе.

Скорость

Обращения к Pyrus идут в фоновых потоках, поэтому долгий запрос одного пользователя не останавливает остальных и не мешает проверке живости сервера. Число одновременных обращений всё же ограничено размером пула, так что на десятках активных пользователей очередь станет заметна.

У каждого обращения есть таймаут: 10 секунд на соединение и 120 на ответ (файлы — 600). Раньше зависший запрос занимал воркер до перезапуска процесса. Если реестр действительно не успевает отрендериться, бюджет поднимается переменной PYRUS_READ_TIMEOUT, но правильнее сузить запрос.

get_tasks экономит обращения между агентом и сервером, но не квоту Pyrus: каждая задача — отдельный запрос. Запрашивайте десятки, не тысячи; при исчерпании лимита пачка останавливается сама и сообщает об этом.

Чего сервер не умеет

  • Фильтровать реестр по признаку «задача закрыта» — Pyrus такого фильтра не даёт. Но format: "csv" отдаёт close_date, по которой закрытые отбираются на своей стороне; см. выше.
  • Листать реестр по смещению — только окнами по датам.
  • Листать поиск — там нет и окон по датам: выборку сужают фильтрами, см. Поиск по аккаунту.
  • Искать по значению поля формы сразу по всем формам — фильтр по полю работает только внутри одной формы.
  • Фильтровать по имени или коду поля — только по числовому id.
  • Очищать поле типа «файл» — значения только добавляются.

Разбор ошибок

Причина приходит вместе с отказом

Протокол MCP по умолчанию превращает любое исключение внутри инструмента в строку Error executing tool <имя> — имя и ничего больше. Поэтому сервер ловит их сам и возвращает обычный ответ с полем error и человеческим текстом. Коды: no_credentials, pyrus_rate_limit, pyrus_timeout, unknown_endpoint, malformed_response, download_too_large, pyrus_error, bad_request, а для незнакомого исключения — internal_error с типом.

Лимиты на клиента

У каждого токена два независимых лимита: 60 запросов в минуту (всплеском до 30) и 4 запроса одновременно. Превышение — HTTP 429, error: "rate_limited", retryable: true и Retry-After в заголовках.

Частота ограничена не ради экономии сервера: 5000 запросов Pyrus за 10 минут считаются на аккаунт и общие с людьми, которые в нём работают. Одновременность — про потоки и память: клиент Pyrus синхронный, крупная выгрузка занимает до 16 МБ. Обход страницами на это и рассчитан.

Это единственный отказ сервера, который лечится ожиданием, а не изменением запроса.

Цифры выше — для уровня regular. У токена может быть уровень gold (вдвое шире оба лимита) или vip (без лимитов вовсе); какой именно — написано в тексте отказа. Уровень выдаётся вместе с токеном и меняется на стороне сервера, сам токен при этом остаётся прежним.

Повторять или нет — написано в отказе

Рядом с кодом приходит retryable. Повторять стоит то, что зависит от чужого состояния: pyrus_rate_limit (подождать), pyrus_timeout, malformed_response, internal_error (сюда попадает и оборванное соединение) и pyrus_error с кодом 5xx — это у Pyrus, и проходит само.

Бессмысленно повторять то, что зависит от самого запроса: bad_request, unknown_endpoint, no_credentials, download_too_large, response_too_large, register_truncated и pyrus_error с кодом 4xx. Тот же вызов вернёт тот же ответ — здесь помогает сужение или исправление аргументов, а не вторая попытка.

Что видноЧто это значит
401 unauthorized от сервера Неверный или отозванный токен доступа в Authorization. Реквизиты Pyrus тут ни при чём.
No Pyrus credentials supplied Не переданы заголовки X-Pyrus-* — либо клиент их не отправляет, либо они не прописаны в конфигурации.
Pyrus rejected the credentials Логин или ключ неверны либо перевыпущены. Проверьте, что ключ взят из раздела API, а не из настроек вебхуков.
403 на задачу или форму У вашей учётной записи нет прав на этот объект — или он в контуре другой организации, и нужны access_token и api_url.
No file with that ID has been uploaded Файл загружен токеном другой организации. Повторите загрузку в том же контуре, где находится задача.
An ID can only be used once guid уже использовался. Загрузите файл заново.
rate limit reached Исчерпан лимит 5000 / 10 минут. Дождитесь указанного времени.
Файловое поле осталось пустым Его пытались заполнить при создании задачи. Нужен отдельный вызов attach_files_to_field.
no_credentials В запросе не пришли реквизиты Pyrus. Нужна пара заголовков X-Pyrus-Login и X-Pyrus-Security-Key — одного логина недостаточно, — либо X-Pyrus-Access-Token. Пока их нет, падает любой вызов, включая самые лёгкие, и по этому признаку ошибку легко спутать с перегрузкой на больших данных.
download_too_large Pyrus присылал больше порога (сейчас 16 МБ), и загрузка была оборвана, чтобы не исчерпать память процесса. Сузьте запрос: item_count, page_size, plan_registry_walk, format: "csv" с field_ids.
register_truncated Реестр пришёл ровно на потолке в 20 000 задач, значит часть данных (самая старая) отсутствует. Пройдите форму окнами — plan_registry_walk построит их сам. Осознанно взять последние 20 000 — allow_truncated: true.
Pyrus has no endpoint at… Опечатка в пути. Pyrus отвечает на несуществующий маршрут не 404, а 202 с текстом «No HTTP resource was found» — сервер распознаёт это и говорит прямо, вместо невнятной ошибки разбора JSON.
response_too_large Ответ не помещается в контекст. Ничего не потеряно. Нужны все данные — обходите порциями: page_size и возврат next_cursor, это работает при любом объёме. Нужна часть — сужайте: для реестра format: "csv" с field_ids, для формы summary: true, для справочника columns.
Pyrus did not respond within… Запрос не уложился в таймаут — почти всегда слишком широкая выборка. Повтор того же вызова упрётся в то же самое: сузьте запрос.
format must be 'json' or 'csv' Опечатка в format. Отклоняется намеренно: сам Pyrus неизвестный формат игнорирует и молча отвечает JSON.
is not a field id В field_filters передано имя или код поля. Нужен числовой id — возьмите его из get_form с summary: true.
is not an access level Опечатка в уровне доступа к форме. Допустимы administrator, manager, member, none.
unrecognized_call_id Звонок не опознан. Нужен external_id, либо task_id, либо пара from_number + to_number.
webhook_is_disabled Расширение телефонии выключено в Pyrus. Вызовы будут проходить впустую, пока его не включат.
messenger is missing… В messenger передана только половина. Нужны и type, и nickname.
does not honour 'restricted_manager' Уровень задокументирован Pyrus, но через API не применяется. Задайте его в интерфейсе Pyrus.
not_applied в ответе Pyrus ответил успехом, но уровень не изменил. Вызов не повторялся — проверьте, допустим ли этот уровень для формы.
is not a person id В permissions передана почта или имя. Нужен числовой id — возьмите его из get_form_permissions или get_contacts.
Invalid knowledge base request Обновление статьи без title. Передавайте его всегда.
Задач меньше, чем ожидалось Не передан include_archived — закрытые задачи в реестр не попали.