zyfra-jira-mcp (0.3.1)
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. Повтор вызова спишет время второй раз.
Ограничения 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 — неверный формат. Человека не
спрашивают о записи, которая заведомо не пройдёт.
Отказ или отмена возвращают ошибку «Человек не подтвердил
запись». На ревизии 2026-07-28 вопрос уходит в InputRequiredResult, клиент отвечает
повторным вызовом. requestState между раундами запечатан SDK (AEAD, привязан к
аргументам вызова, TTL 10 мин): сервер между раундами ничего не хранит. Поэтому текст вопроса не меняется между раундами: без started в нём
«сейчас», а не текущее время.
Ключ 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.