zyfra-jira-mcp (0.3.2)
Installation
pip install --index-url https://git.shahovalov.ru/api/packages/mikhail/pypi/simple/ --extra-index-url https://pypi.org/simple zyfra-jira-mcpAbout this package
MCP-сервер для Jira и Confluence Data Center инсталляции Zyfra
zyfra-jira-mcp
MCP-сервер для Jira и Confluence Data Center инсталляции jira.zyfra.com:
26 инструментов поверх REST API, ревизия протокола 2026-07-28.
Сервер заточен под одну инсталляцию: адреса, проект IDPPA, пространство ZPA
и набор полей зашиты в код. Настраивается только то, что зависит от машины.
Установка
Сервер ставится пакетом из реестра Gitea и даёт команду zyfra-jira в PATH:
uv tool install zyfra-jira-mcp --index https://git.shahovalov.ru/api/packages/mikhail/pypi/simple
Пакет в реестр кладёт CI (release.yml) по тегу vX.Y.Z, совпадающему с версией в
pyproject.toml; тесты гоняет ci.yml на каждый push в main и pull request. Из чекаута
пакет ставят копией в собственный venv инструмента:
uv tool install --reinstall W:/AiProjects/zyfra-jira-mcp
Для разработки — из чекаута: uv sync, команды через uv run zyfra-jira …. Клиенты
запускают установленную копию, а не чекаут, поэтому новая версия доходит до них
только переустановкой (см. «Подключение»).
Токены
Хранятся в Windows Credential Manager (zyfra-jira/jira_token, zyfra-jira/conf_token).
Логин и пароль Zyfra сервер не хранит и не запрашивает: PAT выпускает человек
в браузере, сервер только подсказывает ссылку.
zyfra-jira creds set jira --prompt
То же для conf. Источник токена задаётся явным флагом:
| Флаг | Когда |
|---|---|
--prompt |
живая консоль; скрытый ввод с клавиатуры |
--from-file PATH |
токен положен в файл; файл удаляется после чтения (--keep оставляет) |
--stdin |
токен приходит по конвейеру |
Без флага команда печатает справку и выходит. Угадывать терминал нельзя:
getpass на Windows читает прямо с консоли мимо stdin, а sys.stdin.isatty()
под Git Bash возвращает True даже при перенаправлении — команда молча повисла бы.
Токен не попадает ни в аргументы процесса, ни в переписку с агентом.
Переопределить можно переменными ZYFRA_JIRA_TOKEN и ZYFRA_CONF_TOKEN — они
имеют приоритет над Credential Manager.
Confluence-PAT живёт около 90 дней. Когда он протухнет, любой инструмент вернёт
ссылку на страницу выпуска; после сохранения нового токена вызывается auth_reload.
Подключение
Сервер умеет оба транспорта спецификации: stdio и Streamable HTTP. Настройки машины в
обоих режимах читаются из одного файла %LOCALAPPDATA%\zyfra-jira\config.toml, поэтому
в .mcp.json их нет. Переменные окружения с теми же именами перекрывают файл.
# socks = "192.168.250.2:1080" # VPN в VM; без ключа — напрямую, VPN на этой машине
stdio — основной режим
Клиент запускает сервер сам, в каждой сессии свой процесс; упавший перезапускает тоже он. Фоновый процесс и автозапуск не нужны.
"zyfra-jira": {
"type": "stdio",
"command": "C:/Users/mikhail/.local/bin/zyfra-jira.exe",
"args": [],
"env": {}
}
Команда — из установки uv tool, а не uv run в чекауте: сессии держат venv, из
которого запущены, и uv sync при разработке иначе падал бы на занятых файлах.
Переустановка (uv tool install --reinstall …) по той же причине идёт, когда сессии
Claude закрыты; новая версия доходит до них с новой сессией.
Streamable HTTP — по желанию
Один общий процесс на loopback для всех клиентов. Его запускают вручную, автозапуска нет.
zyfra-jira creds new mcp
Команда создаёт токен самого сервера, кладёт его в Credential Manager
(zyfra-jira/mcp_token) и печатает один раз. Claude Code берёт его оттуда сам:
headersHelper вызывает zyfra-jira creds header при подключении, и токен не лежит
ни в .mcp.json, ни в переменных окружения. Клиенту без headersHelper его дают
переменной ZYFRA_JIRA_MCP_TOKEN. Новый токен отменяет старый после перезапуска сервера.
zyfra-jira http
Сервер слушает http://127.0.0.1:8765/mcp (--host, --port меняют адрес; адрес
не из loopback отвергается). Без токена сервер не запускается.
"zyfra-jira": {
"type": "http",
"url": "http://127.0.0.1:8765/mcp",
"headersHelper": "C:/Users/mikhail/.local/bin/zyfra-jira.exe creds header"
}
Путь в headersHelper полный: команду запускает оболочка клиента, и PATH у неё может
не совпадать с PATH терминала.
Запуск без окна консоли — pythonw.exe из venv инструмента с
%LOCALAPPDATA%\zyfra-jira\zyfra-jira-http.pyw: тот пишет журнал в
zyfra-jira-http.log рядом. В журнал попадает каждая новая пара «клиент — ревизия протокола», один раз за запуск:
клиент claude-code 2.1.308, протокол 2026-07-28. Когда в нём не останется ревизий
2025-11-25 и раньше, сессии можно выключить (stateless_http=True).
| Защита | Что закрывает |
|---|---|
| только loopback | Jira-токены человека не уходят в сеть |
Authorization: Bearer |
другие локальные процессы и страницы браузера: без токена — 401 |
проверка Host/Origin (SDK) |
DNS rebinding: чужое имя в Host — отказ |
⚠️ Подтверждение записи держится на доверии к клиенту с токеном сервера. Клиента
claude-codeсервер узнаёт по имени, которое тот сообщает о себе, и не задаёт ему вопрос: окно разрешения показывает сам Claude Code. Процесс сZYFRA_JIRA_MCP_TOKENмог бы назваться так же — или просто ответить «да» на elicitation. Поэтому токен сервера даётся только клиентам, которые сами спрашивают человека.
Сессии HTTP включены: на 2026-07-28 сервер и так работает без состояния, а клиенту
2025-11-25 сессия нужна, чтобы ответ на elicitation вернулся в вызов инструмента.
tools/list отдаёт ttlMs 10 минут: набор инструментов меняется только с новой версией.
Инструменты
| Чтение | Запись |
|---|---|
jira_issue, jira_comments, jira_search |
jira_comment_add |
jira_parent, jira_transitions |
jira_update |
conf_images |
jira_build_fields, jira_batch (пакет разнотипных записей) |
jira_worklogs, jira_worklog_check |
jira_worklog_add, jira_worklog_batch, jira_worklog_update |
conf_page, conf_search, conf_page_by_jira |
jira_transition, jira_worklog_delete (destructive) |
jira_attachments, conf_attachments (пишут файлы) |
|
auth_status |
auth_reload, jira_export, conf_export (выгрузка в файл) |
conf_page(format=…) отдаёт тело в виде html (view с раскрытыми макросами),
markdown (тот же view общим переводом, без чистки под заметку) или storage
(исходный XHTML с ac:-макросами и шириной картинок).
jira_export и conf_export кладут сырые ответы REST в <dest>/<KEY>.json и
<dest>/<pageId>.json и возвращают только справку: путь, версию источника, размер и
список того, чего в выгрузке не хватает. Файл читает скрипт навыка, а не модель,
поэтому большой JSON не проходит через контекст.
Каталог dest у выгрузок и скачивания вложений выбирает агент: абсолютный путь —
как есть, относительный — от каталога проекта сессии (из него клиент запускает
stdio-сервер; auth_status показывает его в relative_base). Общий HTTP-процесс
проекта сессии не знает и принимает только абсолютный путь. Имена файлов приходят
из Jira, поэтому чистятся и не выходят за пределы dest.
Ключ задачи принимается в любом виде: IDPPA-8352, 8352, feat/IDPPA-8352.
Списание времени и Tempo
На jira.zyfra.com (Jira Server 9.13) ворклог Jira и есть ворклог Tempo: у записи
Tempo jiraWorklogId совпадает с её id, рабочих атрибутов Tempo не заведено
(/rest/tempo-core/1/work-attribute пуст). Поэтому инструменты ходят в
/rest/api/2/issue/{key}/worklog, а не в tempo-timesheets.
jira_worklog_add— время в формате Jira (1h 30m) или секундами;startedв ISO 8601, без пояса берётся пояс машины, по умолчанию — сейчас. Повторный вызов спишет время ещё раз.jira_worklogs— по умолчанию только свои ворклоги, с фильтром по днямdate_from/date_to.jira_worklog_update,jira_worklog_delete— только свои ворклоги; чужие отвергаются до записи.
План недели
План — список строк {key, time_spent, started, comment}. started — день ГГГГ-ММ-ДД
или время в ISO 8601; строки одного дня без времени встают подряд с 09:00 в порядке плана.
Время в пакете уходит в Jira секундами, 1d = 8 часов.
jira_worklog_check(entries) только чтение: раскладка по дням + уже списанное, проблемы
│ план поправлен, ok = true
jira_worklog_batch(entries) одно подтверждение на весь план → запись строка за строкой
- Проверка ищет всё сразу: формат, задача существует и не переехала, повтор строки, 21 день вперёд, 12 часов в день вместе с уже списанным на любые задачи.
jira_worklog_batchповторяет проверку перед записью и при любой проблеме не пишет ничего.- Jira не умеет транзакций: если строка упала (например, неделя утверждена), созданные
до неё остаются, остальные не пишутся. В ответе —
created,failed_index,error,not_attempted; лишнее удаляетjira_worklog_delete. Точный повтор возвращает сохранённый результат без новых POST, включая частичный результат. При неизвестном исходе отправки повтор блокируется до ручной сверки. Не меняйте строки ради повторной отправки.
Ограничения Tempo из глобальной настройки: не больше 12 часов в день, не дальше 21 дня в будущее, утверждение по неделям — в утверждённую неделю Jira не пустит.
Пакет записей
jira_batch — разнотипные записи по одной или многим задачам с одним подтверждением
человека: пять закрытий, десять сборок и три комментария — это один вопрос, а не восемнадцать.
Операция — объект с полем op (comment, update, build_fields, transition) и полями
соответствующего одиночного инструмента:
[
{"op": "comment", "key": "8352", "body": "Проверено на dev-03"},
{"op": "build_fields", "key": "8352", "backend": "zpas-core:1.4.0-rc.2"},
{"op": "transition", "key": "8352", "transition_id": "31"}
]
- Весь пакет проверяется до вопроса: ключи, формат сборок, доступность перехода по
jira_transitions, повторы, не больше 50 записей. Проблемы собираются списком по всем строкам; при любой из них не пишется ничего. - Записи идут по порядку. Jira не умеет транзакций: при сбое созданное остаётся, в ответе
done,failed_index,error,not_attempted. - Списание времени сюда не входит — оно отдельно, через
jira_worklog_batch.
Предохранители
Каждый изменяющий инструмент Jira (jira_comment_add, jira_update, jira_build_fields,
jira_transition, jira_batch, jira_worklog_add/batch/update/delete) пишет только после «да» человека. Кто спрашивает,
зависит от клиента; auth_status показывает это в поле confirm.
| Клиент | Кто спрашивает |
|---|---|
Claude Code (claude-code): терминал, вкладка Code десктопа, SDK |
сам клиент окном разрешения: у инструментов _meta["anthropic/requiresUserInteraction"], окно появляется на каждом вызове даже в auto и bypassPermissions, в dontAsk вызов отклоняется |
| другой клиент с elicitation (form) | сервер: вопрос, что именно запишется и куда |
| клиент без elicitation | никто: запись отклоняется, обхода через конфигурацию нет |
Claude Code сервер через elicitation не спрашивает: вкладка Code десктопа запускает его
в print mode, и он отменяет вопрос сам, не показывая диалог (в логе MCP —
Elicitation request received in print mode).
Вход проверяется до вопроса и в одиночных инструментах: jira_transition отвергает id,
которого нет в jira_transitions; jira_worklog_delete/update — чужой или несуществующий
ворклог; jira_update — пустые поля; jira_build_fields — неверный формат. Человека не
спрашивают о записи, которая заведомо не пройдёт.
Отказ (confirmation_declined), отмена (confirmation_cancelled), confirm=false
(confirmation_false), отсутствие ответа (confirmation_missing) и отсутствие поддержки
(confirmation_unsupported) различаются. cancel сообщает о действии клиента, а не
доказывает, что человек отказался. Ошибки транспорта/протокола SDK не преобразуются в
отказ человека и не разрешают запись. На ревизии 2026-07-28 вопрос уходит в InputRequiredResult, клиент отвечает
повторным вызовом. requestState между раундами запечатан SDK (AEAD, привязан к
аргументам вызова, TTL 10 мин): сервер между раундами ничего не хранит. Поэтому текст вопроса не меняется между раундами: без started в нём
«сейчас», а не текущее время.
Уже согласованный план в Codex
Сервер не видит переписку и не может проверить фразу «Фиксируем это». Имя codex
само по себе не разрешает запись. Аннотации MCP и разрешение вызова инструмента
не заменяют подтверждение на сервере.
Причину старой общей ошибки «Человек не подтвердил» по одному её тексту установить нельзя.
Оператор может отдельно включить доверие аттестации конкретного клиента для
jira_worklog_batch: trusted_plan_clients = "codex" в config.toml или
ZYFRA_JIRA_TRUSTED_PLAN_CLIENTS=codex. Фактическое имя клиента показывает auth_status;
plan_approval_trusted показывает, включён ли этот путь. По умолчанию он выключен.
Это сознательное доверие клиенту, который обязан проверить явное согласие человека:
имя клиента и текст согласия не являются доказательством личности или криптографической подписью.
Не включайте настройку для недоверенных клиентов; HTTP-токен и конфигурация доступны
только доверенным локальным процессам. Остальные инструменты продолжают использовать
свои существующие подтверждения.
Порядок работы доверенного клиента:
- Вызвать
jira_worklog_check(entries); приok=trueполучитьplan_digest. - Показать человеку весь точный план и получить явное согласие на запись. Если согласие уже есть в текущей переписке, проверить, что оно относится к этому плану.
- Вызвать
jira_worklog_batchс теми жеentriesиapproval={"plan_digest": "<digest из проверки>", "user_statement": "Фиксируем это"}. Текст должен быть дословным согласием пользователя. Изменение ключа, времени, комментария или порядка строк требует новой проверки и согласования.
Это отдельный явный путь, не автоматический повтор после decline/cancel или ошибки
elicitation. Переданный approval не принимается от клиента вне списка доверия.
Без approval elicitation работает как прежде, даже для доверенного клиента.
Журнал worklog-batches.sqlite3 лежит рядом с config.toml, общий для stdio и HTTP
с одной конфигурацией. Он привязан к владельцу Jira-токена и нормализованному плану.
Захват плана сохраняется до первого POST, результат — после завершения; падение процесса
между ними оставляет блокировку batch_pending. Не удаляйте журнал при обновлениях:
неизвестный исход сначала сверяют с Jira вручную, автоматического повтора нет.
Существующие собственные ворклоги с тем же ключом, моментом начала и длительностью
также блокируют пакет до записи. Это не транзакция Jira: независимые установки с
разными журналами не имеют общей блокировки. Списания до введения журнала требуют
сверки с Jira; один лишь старый ответ инструмента не создаёт запись в новом журнале.
Ключ config.toml / переменная |
По умолчанию | Что делает |
|---|---|---|
socks / ZYFRA_JIRA_SOCKS |
не задан | хост:порт SOCKS5-прокси до Jira, например VPN-VM 192.168.250.2:1080; имя Jira разрешает прокси. Без него — напрямую |
— / ZYFRA_JIRA_CONFIG |
%LOCALAPPDATA%\zyfra-jira\config.toml |
другой путь к файлу настроек |
jira.zyfra.com видна только через корпоративный VPN. Если связи нет, инструмент отвечает
«Нет связи с jira.zyfra.com (путь): причина» и просит проверить VPN, а не перевыпускать токены.
auth_status в этом случае показывает offline и путь запросов в поле route.
Что ещё проверяется
- Запросы с Bearer уходят только на хост
jira.zyfra.com: URL вложения из ответа API проверяется, редирект на чужой хост запрещён. - 401 — токен не принят. 403 —
AuthErrorтолько при заголовкеX-Seraph-LoginReason, иначе это «нет прав» и обычная ошибка: перевыпуск токена тут не поможет. - HTML вместо JSON (страница логина или SSO) — тоже
AuthError. - Имена вложений: зарезервированные имена Windows (
CON,NUL, …) получают префикс_, коллизии — суффикс(2). Одно битое вложение не рвёт скачивание остальных: оно уходит вskipped. - Битый
config.tomlдаёт понятную ошибку, а не падение инструмента.
Разработка
uv run pytest
Тесты сети не трогают. tests/test_stdio.py поднимает сервер отдельным процессом
и проверяет согласование ревизии, состав инструментов и работу предохранителей.
Модули src/zyfra_jira_mcp/:
server.py сборка: порядок импорта = порядок tools/list
app.py экземпляр MCPServer, аннотации, общие типы
confirm.py согласие человека (режимы host / elicitation / none)
ops.py записи как подготовленные операции: проверить → описать → выполнить;
общие для одиночных инструментов и jira_batch
tools_jira.py инструменты Jira
tools_conf.py инструменты Confluence
tools_auth.py auth_status, auth_reload
worklog.py ворклоги и план недели
batch.py jira_batch
cli.py команда zyfra-jira
serve.py Streamable HTTP
http.py, jira.py, confluence.py, export.py, markdown.py,
creds.py, config.py, util.py, models.py как были
В stdout не должно попадать ничего, кроме JSON-RPC: одна строка print рвёт
сессию целиком. Диагностика — через stderr или логирование SDK.
Что осталось в навыках
Сервер — только точка доступа к Jira и Confluence: чтение, поиск, скачивание, выгрузка в файл и запись с согласия человека. Про Obsidian и формат заметок он не знает ничего.
| Что | Где теперь |
|---|---|
| теги, вид заметки, перспектива по префиксу, рендер заметки задачи | zpas-task-note/scripts/task_note.py (agent-skills-work) |
| шаблон заметки задачи | System/Templates/jira/task-note.j2 в Obsidian-RDS |
| чистка экспорта постановки, ссылки на вложения, ширина картинок | zpas-spec-import/scripts/spec_clean.py (agent-skills-work) |
Скрипты читают файлы jira_export и conf_export, поэтому большой JSON не проходит
через контекст модели. Сервер оставляет у себя только общий перевод в Markdown —
wiki Jira и view Confluence — он нужен и jira_issue, и conf_page.