Merge pull request 'MCP: инструмент comment — комментарий задачи целиком (#80)' (#81) from feat/80-mcp-comment into main
This commit is contained in:
commit
da8f5c9fde
@ -68,17 +68,18 @@ claude mcp add -s user jira-hub -- uv run --project /Users/andrey.epifancev/proj
|
||||
|
||||
Без привязки (user scope, без `--project`) работают только служебные инструменты (`projects`, `project_create`,
|
||||
`agent_status`, `sync_log`, `settings_get`, `settings_set`, `batches`, `batch_status`, `batch_wait`, `batch_cancel`);
|
||||
`issues`, `issue`, `notes_set`, `batch_create`, `command_retry` и `refresh`
|
||||
`issues`, `issue`, `comment`, `notes_set`, `batch_create`, `command_retry` и `refresh`
|
||||
отвечают «сессия не привязана к проекту: запустите MCP с --project <метка> (список — projects)». Метка, которой нет в
|
||||
хабе, — «проект <метка> не заведён в хабе: project_create или исправьте --project в .mcp.json». В привязанной сессии
|
||||
`issues`, `batches` и `notes_set` видят только свой проект (`notes_set` — свои задачи и временные ключи `new:<n>`),
|
||||
`issue` открывает любую задачу по ключу, пакеты и команды ставятся с проектом: хаб сам дописывает метку проекта в
|
||||
`issue` и `comment` открывают любую задачу по ключу, пакеты и команды ставятся с проектом: хаб сам дописывает метку проекта в
|
||||
`ensure_issue` и отвергает чужую метку.
|
||||
|
||||
| Инструмент | Параметры | Результат |
|
||||
|---|---|---|
|
||||
| `issues` | фильтры как у `GET /issues`, `limit` | таблица задач проекта сессии |
|
||||
| `issue` | `key` | поля, связи, заметки, последние команды, комментарии, 5 последних изменений («изменения (N)») |
|
||||
| `issue` | `key` | поля, связи, заметки, последние команды, 5 последних комментариев (номер «#<id>», текст до 300 символов), 5 последних изменений («изменения (N)») |
|
||||
| `comment` | `key`, `comment_id` (номер из `issue`) | один комментарий целиком: автор, время, полный текст |
|
||||
| `changes` | `hours`, `since`, `until` (ISO; без смещения — местное время), `kinds` (`status`, `description`, `dates`, `comment`, `other` или `all`), `key` | лента изменений по задачам: что закрылось, правки описания, сроки, новые комментарии; по умолчанию — с начала суток; без привязки — только с `key` |
|
||||
| `notes_set` | `key`, поля заметок | обновлённые заметки |
|
||||
| `batch_create` | `title`, `commands` (у `ensure_attachment` вместо `file` можно `path` — абсолютный путь к локальному файлу до 10 МБ) | `id`, оценка длительности, число команд |
|
||||
|
||||
@ -22,7 +22,8 @@ MCP-сервер хаба (`jira-hub mcp`, stdio) для Claude: инструм
|
||||
| `projects` | — | таблица: метка, название, доска, тип доски («неизвестен», пока агент не определил), число своих задач; в привязанной сессии текущий проект отмечен |
|
||||
| `project_create` | `label`, `name`, `board_id` (необязательно) | `POST /projects` с автором `claude`: проект, id пакета обновления и фрагмент `.mcp.json` для репозитория проекта (см. ниже); 409/422 — текст ошибки |
|
||||
| `issues` | фильтры как у `GET /issues` (кроме `project` — его задаёт привязка), `limit` (по умолчанию 50) | таблица: ключ, тема (до 80 символов), статус, срок, исполнитель, спринт, статус по факту |
|
||||
| `issue` | `key` | поля, связи (обе стороны, с типом и направлением), заметки, последние команды, комментарии: «комментарии (N):» и 5 последних (автор, время, текст до 300 символов с «…»), «ещё M», если их больше 5; без комментариев — «комментариев нет» |
|
||||
| `issue` | `key` | поля, связи (обе стороны, с типом и направлением), заметки, последние команды, комментарии: «комментарии (N):» и 5 последних (номер «#<comment_id>», автор, время, текст до 300 символов с «…»), «ещё M», если их больше 5; без комментариев — «комментариев нет» |
|
||||
| `comment` | `key`, `comment_id` (номер из ответа `issue`, «#» в начале допускается) | один комментарий целиком по `GET /issues/{key}`: строка «<key> #<id> <автор>, создан <created>» (и «, изменён <updated>», если `updated` есть и отличается от `created`), под ней полный текст с переносами строк; нет такого номера — «у <key> нет комментария #<id>» |
|
||||
| `notes_set` | `key`, подмножество полей заметок | обновлённые заметки |
|
||||
| `batch_create` | `title`, `commands` (список объектов по контракту `commands`; у `ensure_attachment` вместо `file` можно `path`) | `id`, `estimate_seconds`, число команд; 422 — текст ошибки валидации |
|
||||
| `batch_status` | `id` | статусы команд, ошибки, оценка остатка |
|
||||
@ -98,7 +99,15 @@ MCP-сервер хаба (`jira-hub mcp`, stdio) для Claude: инструм
|
||||
|
||||
#### Scenario: Задача с семью комментариями
|
||||
- **WHEN** `issue {key: "AURORA-9001"}`, у задачи 7 комментариев, у самого нового текст 500 символов
|
||||
- **THEN** ответ содержит «комментарии (7):», 5 последних комментариев (самый новый — первым, обрезан до 300 символов с «…») и «ещё 2»
|
||||
- **THEN** ответ содержит «комментарии (7):», 5 последних комментариев с номерами «#<comment_id>» (самый новый — первым, обрезан до 300 символов с «…») и «ещё 2»
|
||||
|
||||
#### Scenario: Комментарий целиком
|
||||
- **WHEN** у задачи `AURORA-9001` комментарий `#101` длиной 800 символов с переносами строк, вызван `comment {key: "AURORA-9001", comment_id: "101"}`
|
||||
- **THEN** ответ — «AURORA-9001 #101 <автор>, создан <created>» и полный текст комментария без обрезки, переносы строк сохранены
|
||||
|
||||
#### Scenario: Нет такого комментария
|
||||
- **WHEN** `comment {key: "AURORA-9001", comment_id: "999"}`, у задачи нет комментария 999
|
||||
- **THEN** ответ «у AURORA-9001 нет комментария #999»
|
||||
|
||||
### Requirement: Привязка сессии к проекту
|
||||
`jira-hub mcp --project <метка>` SHALL привязывать сервер MCP к проекту. Привязка SHALL проверяться при каждом вызове
|
||||
@ -107,7 +116,7 @@ MCP-сервер хаба (`jira-hub mcp`, stdio) для Claude: инструм
|
||||
|
||||
В привязанной к проекту P сессии:
|
||||
- `issues` — `GET /issues?project=P` с остальными фильтрами;
|
||||
- `issue` — любая задача по ключу;
|
||||
- `issue`, `comment` — любая задача по ключу;
|
||||
- `notes_set` — только своя задача P (метка P; соседи — нет) или временный ключ `new:<n>` (его задача создаётся пакетом
|
||||
проекта), иначе ответ «<key> не в проекте P»;
|
||||
- `batch_create`, `command_retry` — с `project = P` (проверки пакета проекта, см. `commands`; повтор команды другого
|
||||
@ -115,7 +124,7 @@ MCP-сервер хаба (`jira-hub mcp`, stdio) для Claude: инструм
|
||||
- `batches` — `GET /batches?project=P`; `batch_status`, `batch_wait`, `batch_cancel` — по id, без ограничения;
|
||||
- `refresh` — `full` — общий полный прогон всех проектов, `keys` — как раньше.
|
||||
|
||||
Без привязки инструменты `issues`, `issue`, `notes_set`, `batch_create`, `command_retry`, `refresh` SHALL отвечать
|
||||
Без привязки инструменты `issues`, `issue`, `comment`, `notes_set`, `batch_create`, `command_retry`, `refresh` SHALL отвечать
|
||||
«сессия не привязана к проекту: запустите MCP с --project <метка> (список — projects)» и в хаб не ходить; `projects`,
|
||||
`project_create`, `agent_status`, `sync_log`, `settings_get`, `settings_set`, `batches`, `batch_status`, `batch_wait`,
|
||||
`batch_cancel` работают.
|
||||
|
||||
@ -138,7 +138,8 @@ def fmt_issue(d) -> str:
|
||||
cmds = [f" #{c['id']} {c['type']} {c['status']} {c.get('finished_at') or ''} {c.get('error') or ''}".rstrip() for c in d.get("commands", [])]
|
||||
comments = d.get("comments") or []
|
||||
if comments:
|
||||
shown = [f" {_v(c.get('author'))} {_v(c.get('created'))}: {_cut(c.get('body'), 300)}" for c in comments[:5]]
|
||||
shown = [f" #{c.get('comment_id')} {_v(c.get('author'))} {_v(c.get('created'))}: {_cut(c.get('body'), 300)}"
|
||||
for c in comments[:5]]
|
||||
cmts = [f"комментарии ({len(comments)}):"] + shown + ([f" ещё {len(comments) - 5}"] if len(comments) > 5 else [])
|
||||
else:
|
||||
cmts = ["комментариев нет"]
|
||||
@ -160,6 +161,14 @@ def _day(value, text) -> str:
|
||||
return _v(text)
|
||||
|
||||
|
||||
def fmt_comment(key, c) -> str:
|
||||
"""Один комментарий целиком (#80): шапка, затем текст с переносами строк; «изменён» — только если правили."""
|
||||
head = f"{key} #{c.get('comment_id')} {_v(c.get('author'))}, создан {_v(c.get('created'))}"
|
||||
if c.get("updated") and c.get("updated") != c.get("created"):
|
||||
head += f", изменён {c['updated']}"
|
||||
return head + "\n" + _v(c.get("body"))
|
||||
|
||||
|
||||
def fmt_event(e: dict, today) -> str:
|
||||
"""Строка события ленты: «ЧЧ:ММ автор: что»; дата «ДД.ММ» перед временем, если событие не в сутках today
|
||||
(today = None — дата всегда)."""
|
||||
@ -257,10 +266,23 @@ def issues(sprint: str = "", assignee: str = "", work_kind: str = "", fact_statu
|
||||
@guard
|
||||
@scoped(True)
|
||||
def issue(key: str) -> str:
|
||||
"""Задача целиком: поля, связи в обе стороны, служебные заметки, последние команды, описание."""
|
||||
"""Задача целиком: поля, связи в обе стороны, служебные заметки, последние команды, описание. Комментарии — 5
|
||||
последних с номером, текст до 300 символов; полный текст — инструмент comment по номеру."""
|
||||
return fmt_issue(_client().get(f"/issues/{key}"))
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
@guard
|
||||
@scoped(True)
|
||||
def comment(key: str, comment_id: str) -> str:
|
||||
"""Один комментарий задачи целиком: автор, время, полный текст. comment_id — номер из ответа issue («#123»)."""
|
||||
cid = comment_id.lstrip("#")
|
||||
for c in _client().get(f"/issues/{key}").get("comments") or []:
|
||||
if c.get("comment_id") == cid:
|
||||
return fmt_comment(key, c)
|
||||
return f"у {key} нет комментария #{cid}"
|
||||
|
||||
|
||||
def _local_if_naive(value: str) -> str:
|
||||
"""Время без смещения из чата — местное время машины MCP (хаб такое отвергает, M-2); неразбираемое — как есть."""
|
||||
if not value:
|
||||
|
||||
@ -278,6 +278,7 @@ def test_fmt_issue_comments():
|
||||
assert "комментарии (7):" in text
|
||||
assert "д" * 299 + "…" in text and "д" * 300 not in text # самый новый — первым, обрезан до 300
|
||||
assert "Автор 3" in text and "Автор 2" not in text # 5 последних: 7…3
|
||||
assert " #7 Автор 7 2026-10-17T12:00:00.000+0300: ддд" in text and " #3 Автор 3 " in text # номер — для comment (#80)
|
||||
assert "ещё 2" in text
|
||||
assert text.index("заметки:") < text.index("комментарии (7):") < text.index("команды:")
|
||||
no_author = mcp_server.fmt_issue(_detail(comments=[{"comment_id": "1", "author": None, "created": None,
|
||||
@ -285,6 +286,35 @@ def test_fmt_issue_comments():
|
||||
assert "комментарии (1):" in no_author and "ещё" not in no_author
|
||||
|
||||
|
||||
def test_fmt_comment():
|
||||
"""Один комментарий целиком (#80): переносы строк сохраняются, «изменён» — только если правили."""
|
||||
body = "первая строка\n" + "т" * 1000
|
||||
text = mcp_server.fmt_comment("A-1", {"comment_id": "12", "author": "golov", "created": "2026-10-20T10:00:00.000+0300",
|
||||
"updated": "2026-10-21T09:00:00.000+0300", "body": body})
|
||||
assert text == ("A-1 #12 golov, создан 2026-10-20T10:00:00.000+0300, изменён 2026-10-21T09:00:00.000+0300\n" + body)
|
||||
same = mcp_server.fmt_comment("A-1", {"comment_id": "12", "author": None, "created": "2026-10-20T10:00:00.000+0300",
|
||||
"updated": "2026-10-20T10:00:00.000+0300", "body": ""})
|
||||
assert same == "A-1 #12 —, создан 2026-10-20T10:00:00.000+0300\n—"
|
||||
|
||||
|
||||
def test_comment_tool(live, monkeypatch):
|
||||
long = "абв " * 200 + "\nконец"
|
||||
live["agent"].post("/agent/state", {"issues": [{"key": "A-1", "summary": "Т", "status": "Готово", "labels": ["AWPCA"],
|
||||
"comments": [{"id": "101", "author": "golov", "body": long,
|
||||
"created": "2026-10-20T10:00:00.000+0300"},
|
||||
{"id": "102", "author": "ivanov", "body": "коротко",
|
||||
"created": "2026-10-21T10:00:00.000+0300"}]}],
|
||||
"links": []})
|
||||
assert "#101 golov" in mcp_server.issue("A-1") and long not in mcp_server.issue("A-1")
|
||||
out = mcp_server.comment("A-1", "101")
|
||||
assert out.startswith("A-1 #101 golov, создан 2026-10-20T10:00:00.000+0300\n") and out.endswith(long)
|
||||
assert mcp_server.comment("A-1", "#102").endswith("\nкоротко") # номер можно передать как в issue
|
||||
assert mcp_server.comment("A-1", "999") == "у A-1 нет комментария #999"
|
||||
assert mcp_server.comment("A-404", "1").startswith("ошибка хаба 404")
|
||||
monkeypatch.setattr(mcp_server, "PROJECT", None)
|
||||
assert mcp_server.comment("A-1", "101") == mcp_server.NOT_BOUND
|
||||
|
||||
|
||||
BUG = {"type": "ensure_issue", "params": {"temp_key": "new:1", "issue_type": "bug", "summary": "ОСВ 60 не распознана",
|
||||
"labels": ["AWPCA"]}}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user