Подключение
Транспорт — 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-LoginX-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 всегда.
Как отличить открытые от закрытых
Простой способ — 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 раза
меньше. Это разница между отказом и ответом с запасом.
Но выбирать формат чаще приходится не по размеру, а по составу колонок — они разные:
| CSV | JSON | |
|---|---|---|
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_largeMCP_MAX_RESPONSE_BYTES | контекст модели | 200 КБ | format: "csv", field_ids, page_size |
download_too_largePYRUS_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 — номеру
шага процесса, а не по полю формы с похожим названием.
Поиск по аккаунту
Реестр отвечает про форму: «какие задачи в этой форме».
Вопрос «какие задачи у этого человека» или «что я поставил другим» реестром
не решается вовсе — обходить формы подряд значит потратить десятки вызовов и
всё равно пропустить свободные задачи, которых ни в одной форме нет. Для
этого есть find_tasks.
find_tasks — единственный инструмент сервера, работающий не с
API v4, а с v3 (pyrus.com/restapi/searchtasks). Не наследие, а
необходимость: списка задач в v4 не существует вовсе,
GET /v4/tasks отвечает 405. Для вас это ничего не меняет —
реквизиты те же, — но если контур закрыт файрволом, к трём привычным
хостам добавляется pyrus.com.
Как спрашивать
find_tasks(author="me") — что я поставил другим
find_tasks(responsible="me") — что висит на мне
find_tasks(participants=["me"]) — где я участник
find_tasks(include_text="счёт") — текстовый поиск по всему аккаунту
find_tasks(list_ids=[2460973]) — всё из конкретного Списка
Человек задаётся числовым id, почтой сотрудника или словом
"me". Почта разворачивается по сотрудникам организации;
внешнего адресата по почте не найти — ему нужен id из
get_contacts. Фильтры складываются, и хотя бы один обязателен:
поиск без фильтров вернул бы произвольную горсть задач аккаунта, неотличимую
от найденного, поэтому он отклоняется.
Обход выборки целиком
Своей пагинации у Pyrus нет никакой: ни курсора, ни смещения, ни окон по
датам — такие параметры он принимает и игнорирует, проверено
поимённо на восьми вариантах имён. Поэтому обход делает сервер: передайте
page_size и возвращайте next_cursor, пока он
приходит.
find_tasks(author="me", page_size=200) → 200 задач + next_cursor
find_tasks(author="me", page_size=200, cursor=…) → следующие 200
Проверено живьём: восемь страниц, 1600 задач, ни одного повтора, по 51 КБ на страницу. Цена честная — каждая страница заново спрашивает у Pyrus всю выборку, своего смещения у него нет. Один лишний запрос на страницу против десятков, если перебирать сотрудников по одному.
Позиция держится последним отданным id, а не порядковым номером: задачи,
созданные между страницами, получают большие id и встают выше пройденного
места. Курсор помнит фильтры, с которыми начат, и отказывается продолжать,
если они изменились. Потолок Pyrus в 10 000 задач обходом не снимается — за
ним только сужение, и сервер говорит об этом в поле note.
Две вещи, о которых Pyrus молчит
Закрытые задачи не приходят, пока не сказано
include_closed: true. Как и у реестра, об этом ничто не
предупреждает.
Автора в ответе нет вовсе — даже когда искали именно по нему. Поэтому «я поставил другим» получается вычитанием: ищем по автору и убираем те задачи, где ответственный — он сам.
Что приходит в ответе
Опознание, а не содержимое: id, первые 120 символов текста, ответственный,
даты, Списки. Задачу целиком берут через get_task, пачку — через
get_tasks. Полей формы поиск не отдаёт и по ним не фильтрует —
за ними в реестр.
Всегда приходят id, текст, responsible,
create_date, modified_date,
parent_task_id. По наличию: Списки — 123–198 из 200, срок —
11–99 из 200, close_date — 111 из 200 при
include_closed. Срок здесь есть, в отличие от
реестра, но строить на нём «что горит» всё равно нельзя: выборка
обрывается молча. Сроки — по-прежнему get_overdue_tasks и
get_tasks_due_soon, они берут их из календаря.
10 000 задач весят 2,7 МБ — такой ответ сервер отклоняет целиком, чтобы не затопить разговор. Под лимит проходит порядка 700 задач, по умолчанию берётся 200.
Не путать с search_tasks
search_tasks, несмотря на имя, поиском по аккаунту не является:
это реестр одной формы, нарезанный по датам, и он делегирован
get_registry. Сквозной поиск — только
find_tasks.
Готовые сценарии поверх поиска — delegated, stale, person_360: они и делают описанные здесь вычитания и проверки.
Метка правки
Пока сервер пишет в задачу, она лежит в вашем личном списке «🤖 AI MCP Pyrus», а сразу после успешного коммита выходит из него. Список приватный — посторонние его не видят.
Список создаётся сам при первой вашей записи, в вашем же Pyrus и по вашим реквизитам. Если он уже есть — находится по точному имени, в том числе внутри дерева; дубликат не заводится.
Метка едет в том же теле, что и сама правка, а не
отдельным вызовом: изменение и метка атомарны, и от постановки лишней
записи в ленте не появляется. Отдельно уходит только снятие — с
skip_notification, чтобы никого не будить.
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: true | id, 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.
Загрузка
-
MCP
get_upload_target— при желании с параметромname.{ "url": "https://api.pyrus.com/v4/files/upload?name=Чек.jpg", "form_field": "file", "access_token": "..." } -
Ваш HTTP-запрос
POSTна этот адрес:multipart/form-data, файл в полеfile, заголовокAuthorization: Bearerс полученным токеном. В ответе —guid. -
MCP
Передайте
guidвcomment_taskилиcreate_taskв массивеattachments— файл появится в задаче.
Скачивание
-
MCP
get_file_download_urlс числовымfile_idвложения (он есть вget_task). Возвращаетurl,filenameиaccess_token. -
Ваш 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).
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 | отзыв доступа — человек исчезает из списка целиком |
В документации Pyrus этот уровень есть, но через POST /permissions
он не применяется: запрос отвечает 200 и возвращает список
доступа с прежним уровнем. Проверено вживую. Сервер такое
значение отклоняет с объяснением — иначе вызов выглядел бы успешным, ничего не
изменив. Задать этот уровень можно только в интерфейсе Pyrus.
Меняются только те, кого вы назвали. Все остальные сохраняют свой уровень —
вызов не переписывает список доступа целиком. Чтобы отозвать доступ, человека
надо явно указать с уровнем none; просто не
упомянуть его недостаточно.
Свои собственные права администратора отозвать можно, а вернуть их потом уже
нельзя. Сначала прочитайте get_form_permissions и убедитесь, что
хотя бы один администратор останется.
Неизвестный уровень Pyrus игнорирует, не возвращая ошибки: опечатка выглядела бы как успешный вызов, ничего не изменивший. Поэтому сервер проверяет уровень у себя и отклоняет посторонние значения — до обращения к Pyrus.
В ответ приходит весь список доступа после изменения, в том
же виде, что отдаёт 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 сервер принимает, но в документации Pyrus его
нет, и ни в одной прочитанной живой карточке оно не встретилось —
не исключено, что Pyrus его игнорирует. Признаком увольнения считайте
banned, а не fired.
Поля карточки
| Поле | Формат |
|---|---|
messenger | объект {"type": "Telegram", "nickname": "@name"} — нужны обе половины |
status | свободный текст рядом с именем, с эмодзи (в живой карточке — 👍На смене) |
vacation_days | отправляется строкой, как в документации Pyrus |
Неполный messenger сервер отклоняет: Pyrus такой объект молча
выбрасывает, и вызов выглядел бы успешным, ничего не изменив.
Метода разблокировки в 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.
Когда подходящих ботов нет, Pyrus отвечает {} — пустым объектом
без ключа bots, а не {"bots": []}.
В организации с единственным отставленным ботом запрос без
include_fired вернул именно {}. Обращение сразу к
["bots"] на таком ответе сломается.
bot_settings — строка JSON, а не объект: Pyrus
хранит её дословно и так же отдаёт боту. Объект сервер отклоняет, потому что
API его примет, а настройка молча не заработает.
delete_bot удаляет бота насовсем, и всё, что завязано на его
hook_url, перестаёт работать. Чтобы вывести бота из строя
обратимо, используйте update_bot(is_enabled: false).
Телефония
Раздел реализован по документации 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-запроса.
-
MCP · get_task
Найти вложение:
comments[].attachments[], оттудаidиroot_id. Заодно — id файлового поля вfields. -
MCP · get_file_download_url
Получить ссылку и токен.
-
HTTP · GET
Скачать файл с заголовком
Authorization: Bearer. -
MCP · get_upload_target
Получить адрес загрузки — в том же контуре, где лежит задача.
-
HTTP · POST
Загрузить новую редакцию, получить
guid. -
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 — закрытые задачи в реестр не
попали. |