feat-vikunja-mcp #1

Merged
Дмитрий merged 30 commits from feat-vikunja-mcp into master 2026-08-17 09:36:39 +03:00
No description provided.
Вызов list_projects через зарегистрированный MCP-сервер вернул список
проектов: путь наружу у процесса, поднятого harness, отличается от
bash-песочницы, и токен по дороге не режется. Замысел ТЗ работоспособен.

Уточняет 78662f1: его сообщение обещало «проверку доступа», хотя на тот
момент была пройдена только локальная проба протокола по stdio.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Раньше нечисловой заголовок ронял голый ValueError мимо всей таксономии
ошибок проекта. Молчаливо считать текущую страницу последней тоже нельзя:
без числа страниц неизвестно, полон ли список — а частичный список,
выданный как полный, это то самое запрещённое молчаливое усечение.
Поэтому нечисловой заголовок явно прерывает запрос через errors.BadPagination.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Под мутацией на частичный POST sent["description"] бросал KeyError до
того, как assert успевал показать сообщение — сигнал терялся в CI на
одной строке E. .get() превращает отсутствие ключа в None == "...",
и диагностический текст остаётся на строке E.
Фикстура теста на неоднозначность получила второй проект с «Финансы»
в названии (id 10, «Финансы рабочие»). С исходной фикстурой запрос
«фин» совпадал только с «Финансы личные» — единственное совпадение,
resolve_project возвращал его, AmbiguousProject не поднималась, и тест
на самом деле не проверял неоднозначность. Разница между «Financier»
(латиница) и «фин» (кириллица) не даёт им совпасть по подстроке — это
подтверждено проверкой кодовых точек, а не предположением.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
known строился из label.title.lower() без strip(), а поиск — с ним: метка
с пробелом в хранимом названии не находилась, и create_missing создавал
дубликат. Снятие метки, которой нет в списке, тихо ничего не делало и
возвращало обычную успешную задачу — теперь это VikunjaError, как и у add.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
human() переводит datetime в локальную зону. UTC-полдень на машинах с
поясом UTC+12 и восточнее (Pacific/Auckland, Pacific/Fiji и т.п.) съезжает
на следующий календарный день, и оба теста на "20.08.2026" ломаются не из-за
кода, а из-за часового пояса машины. Локальный полдень (как делает
dates.parse_due для голой даты) даёт 20.08.2026 везде.
Slicing the serialized document silently at 20000 chars, with no notice,
violated the task's own rule that truncation must be announced with a
count. Worse than list_tasks's case: the dropped tail here isn't just
absent data, it's an unparseable fragment, so the caller needs to be told
that too, not just how much was cut.
Оборачивает сборку/закрытие/удаление в try/finally: если ассерт read-modify-write
упадёт, задача всё равно удаляется, а contextlib.suppress(Exception) на очистке
не даёт сбою delete_task заслонить исходный AssertionError.
README.md описывает установку, токен, подключение и тесты с нуля.
ТЗ §11: токен в .env закрыт решением владельца, соглашения соседнего
проекта не перенимаются; судьба skill vikunja остаётся открытой.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
VIKUNJA_DEBUG поднимает только уровень логирования до DEBUG — ни один
инструмент не логирует пойманное исключение, и SDK превращает необработанное
исключение сразу в текст CallToolResult, минуя логгер. Строка про "стек
ошибки в stderr" была неточной; заменена на описание того, что переменная
делает на самом деле.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Каждый из одиннадцати инструментов ловил только VikunjaError, всё остальное
улетало в SDK и возвращалось строкой вида «Error executing tool get_task: 'id'»:
ни запроса, ни кода, ни причины, ни действия, которое её снимает — прямое
нарушение ТЗ §7. Тем же путём уходили ValueError из status, ValueError из int()
в resolve_project и FileNotFoundError от опечатки в VIKUNJA_CA_BUNDLE.

VIKUNJA_DEBUG=1 при этом только поднимал уровень логирования: log был объявлен и
ни разу не вызван, поэтому стек не попадал в stderr ни при каком значении.

Декоратор _guard ставится под @mcp.tool, поэтому оборачивает функцию до того, как
SDK её регистрирует. Схемы SDK строит через inspect.signature, а тот идёт по
__wrapped__, который проставляет functools.wraps: снимок tools/list до и после —
байт в байт, включая схемы входа, схемы выхода и аннотации.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
text.lstrip("-").isdigit() был истиной для «--7» и для юникодной «²», после чего
int(text) падал ValueError. Тот ValueError не был классифицирован и до предыдущего
коммита уходил в SDK строкой «invalid literal for int() with base 10: '--7'».
Решение принимает сам int: разобралось — это id, не разобралось — ищем по названию.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
create_task делает PUT задачи, а потом отдельными запросами вешает метки. Падение
на метках возвращало только «В Vikunja нет метки «x»» — по этому тексту нельзя
понять, что задача уже создана, и естественный ответ агента (повторить) заводит
дубль в живом трекере. Теперь ошибка называет задачу по id и названию, говорит,
что она создана, оставляет исходную причину и отправляет в set_task_labels.

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Без project список собирался обходом проектов, каждый сервер сортировал у себя,
а склейка резалась по limit. Подпись «ещё N не показано: список обрезан по limit»
читается как «первые N по вашей сортировке», а на деле это первые N первого
проекта. Молчаливое усечение ТЗ §5 запрещает; усечение с ложным обещанием хуже
молчаливого.

Обход нескольких проектов теперь сводится в общий порядок по due_date, id,
priority или title — тем полям, что объявляет описание инструмента. Направление
не поддерживается намеренно: docs/API Vikunja.md фиксирует у
GET /projects/{id}/tasks только сам параметр sort_by, синтаксиса направления там
не замерено, поэтому «-priority» не разворачивается по догадке.

Поле, которое свести нечем, оставляет порядок сервера — и клиент возвращает этот
факт (TaskSweep.per_project_order), а инструмент печатает его строкой перед
строкой об усечении. Один проект сервер сортирует целиком, там сводить нечего:
его порядок не трогается.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ApiError вклеивает body[:400] от сервера дословно. Обратный прокси, печатающий
заголовки запроса в теле ошибки, вернул бы Authorization — и токен оказался бы в
ответе инструмента. Инвариант «токен не попадает ни в ответ, ни в текст ошибки,
ни в лог» (ТЗ §4) исключений не терпит.

Знание о токене остаётся в клиенте: ApiError его не получает, тело маскируется
до сборки текста. Маскировка идёт до обрезки по длине — обрезка первой разрубила
бы токен на границе и замена бы его не нашла.

Заодно: тест на недоступность сервера искал в тексте только имя хоста и прошёл бы
при потерянной причине. Теперь проверяет и причину.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Комментарий проверен вживую: ответ на PUT /tasks/{id}/comments содержит id,
разбор не падает, комментарий возвращается из GET. Это было единственное
место, где код полагался на форму тела, которую никто не измерял, — замер
дописан в docs/API Vikunja.md.

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

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Дмитрий deleted branch feat-vikunja-mcp 2026-08-17 09:36:39 +03:00
Дмитрий referenced this pull request from a commit 2026-08-17 09:36:39 +03:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
public/vikunja-mcp!1
No description provided.