feat(sdlc-rename): MCP-серверы sdlc-jira и sdlc-wiki, модуль в sdlc-hub mcp обязателен

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-10-24 11:45:40 +04:00
parent e34d61a483
commit 08460dc281
9 changed files with 38 additions and 33 deletions

View File

@ -17,8 +17,8 @@ def main(argv=None) -> int:
p = sub.add_parser("serve", help="запустить HTTP API")
p.add_argument("--host", default="0.0.0.0")
p.add_argument("--port", type=int)
p = sub.add_parser("mcp", help="MCP-сервер модуля по stdio (по умолчанию jira)")
p.add_argument("module", nargs="?", default="jira", help="имя модуля; неизвестное — код 2 (проверка в mcp.run)")
p = sub.add_parser("mcp", help="MCP-сервер модуля по stdio ")
p.add_argument("module", nargs="?", default=None, help="имя модуля (jira, wiki); без модуля или неизвестный — код 2")
p.add_argument("--project", help="метка проекта, к которому привязана сессия")
for m in registry.modules():
if m.cli_register:

View File

@ -25,13 +25,10 @@ def unknown_project(label) -> str:
return f"проект {label} не заведён в хабе: project_create или исправьте --project в .mcp.json"
def mcp_json(label: str, module: str = registry.DEFAULT_AGENT) -> dict:
"""Фрагмент .mcp.json для репозитория проекта: у модуля по умолчанию прежний (сервер jira-hub), у прочих модулей —
сервер jira-hub-<модуль>."""
run = ["run", "--project", str(HUB_ROOT), "sdlc-hub", "mcp"]
if module == registry.DEFAULT_AGENT:
return {"mcpServers": {"jira-hub": {"command": "uv", "args": [*run, "--project", label]}}}
return {"mcpServers": {f"jira-hub-{module}": {"command": "uv", "args": [*run, module, "--project", label]}}}
def mcp_json(label: str, module: str) -> dict:
"""Фрагмент .mcp.json для репозитория проекта: сервер sdlc-<модуль>, запуск `sdlc-hub mcp <модуль>`."""
run = ["run", "--project", str(HUB_ROOT), "sdlc-hub", "mcp", module, "--project", label]
return {"mcpServers": {f"sdlc-{module}": {"command": "uv", "args": run}}}
def client() -> HubClient:
@ -199,9 +196,13 @@ def register_core_tools(mcp, *, projects_doc: str, columns: list[tuple[str, Call
mcp.tool()(project_create)
def run(module_name: str, project: str | None = None) -> int:
def run(module_name: str | None, project: str | None = None) -> int:
"""Запустить MCP-сервер модуля по stdio. Неизвестный модуль: список модулей в stderr, код 2, сервер не стартует."""
global PROJECT
if module_name is None:
names = ", ".join(x.name for x in registry.modules() if x.mcp_server)
print(f"укажите модуль: sdlc-hub mcp <модуль> [--project <метка>]; модули: {names}", file=sys.stderr)
return 2
m = registry.module(module_name)
if m is None or m.mcp_server is None:
names = ", ".join(x.name for x in registry.modules() if x.mcp_server)

View File

@ -19,7 +19,7 @@ from .commands import describe, is_temp
POLL_SECONDS = 5
BOTH_FILE_AND_PATH = "у ensure_attachment либо path, либо file"
mcp = FastMCP("jira-hub")
mcp = FastMCP("sdlc-jira")
fmt_agent = core.fmt_agent

View File

@ -1,4 +1,4 @@
"""MCP-инструменты модуля wiki (stdio, FastMCP, сервер jira-hub-wiki) — инструменты сессии Claude поверх HTTP API хаба
"""MCP-инструменты модуля wiki (stdio, FastMCP, сервер sdlc-wiki) — инструменты сессии Claude поверх HTTP API хаба
(specs/mcp/spec.md «MCP вики»). Охрана, привязка к проекту и projects/project_create — в core/mcp.py. Markdown страниц и
имена вложений приходят из Eva и считаются недоверенными: страница выдаётся текстом, имя файла сводится к basename."""
import asyncio
@ -16,7 +16,7 @@ ATTACHMENT_WAIT = 60
SYNC_WAIT = 300
CHUNK = 20000
CANDIDATES = 10
mcp = FastMCP("jira-hub-wiki")
mcp = FastMCP("sdlc-wiki")
def _project() -> dict:
@ -242,7 +242,7 @@ def root_set(root: str) -> str:
@core.guard
def project_create(label: str, name: str, wiki_root: str = "") -> str:
"""Завести проект (метка, название, необязательно корень вики: код DOC-… или ссылка Eva). С корнем хаб ставит
синхронизацию; в ответе — фрагмент .mcp.json для репозитория проекта с сервером jira-hub-wiki."""
синхронизацию; в ответе — фрагмент .mcp.json для репозитория проекта с сервером sdlc-wiki."""
try:
code = wiki_roots.parse_root(wiki_root)
except ValueError as e:

View File

@ -134,7 +134,7 @@
"name": "projects"
},
{
"description": "Завести проект (метка, название, необязательно корень вики: код DOC-… или ссылка Eva). С корнем хаб ставит\nсинхронизацию; в ответе — фрагмент .mcp.json для репозитория проекта с сервером jira-hub-wiki.",
"description": "Завести проект (метка, название, необязательно корень вики: код DOC-… или ссылка Eva). С корнем хаб ставит\nсинхронизацию; в ответе — фрагмент .mcp.json для репозитория проекта с сервером sdlc-wiki.",
"inputSchema": {
"properties": {
"label": {

View File

@ -66,19 +66,18 @@ def test_agent_publish_write_failure(tmp_path, monkeypatch, capsys):
assert sorted(p.name for p in release.module_dir(paths.release_root(), "jira").iterdir()) == ["agent.py", "manifest.json"]
def test_mcp_default_module(monkeypatch):
def test_mcp_module_required(monkeypatch, capsys):
from sdlc_hub.core import mcp as core_mcp
from sdlc_hub.modules.jira import mcp as jira_mcp
seen = []
monkeypatch.setattr(core_mcp, "PROJECT", None)
monkeypatch.setattr(jira_mcp.mcp, "run", lambda transport="stdio": seen.append((transport, core_mcp.PROJECT)))
assert cli.main(["mcp", "--project", "AWPCA"]) == 0
monkeypatch.setattr(core_mcp, "PROJECT", None) # run() пишет глобал; monkeypatch вернёт прежнее значение
monkeypatch.setattr(jira_mcp.mcp, "run", lambda transport="stdio": seen.append(core_mcp.PROJECT))
assert cli.main(["mcp", "--project", "AWPCA"]) == 2
err = capsys.readouterr().err
assert "укажите модуль: sdlc-hub mcp <модуль>" in err and "jira, wiki" in err
assert seen == []
assert cli.main(["mcp", "jira", "--project", "AWPCA"]) == 0
assert seen == [("stdio", "AWPCA"), ("stdio", "AWPCA")]
# без --project сессия не привязана к проекту
monkeypatch.setattr(core_mcp, "PROJECT", "STALE")
assert cli.main(["mcp"]) == 0
assert seen[-1] == ("stdio", None)
assert seen == ["AWPCA"]
def test_mcp_unknown_module(monkeypatch, capsys):

View File

@ -503,16 +503,20 @@ def test_project_create(live, monkeypatch): # сце
out = jmcp.project_create("STOCKS_DRRK", "Запасы ДРРК", 5120)
assert "STOCKS_DRRK" in out and re.search(r"пакет[^#]*#\d+", out) and "/mcp" in out
frag = json.loads(out[out.index("{"):])
assert frag == core_mcp.mcp_json("STOCKS_DRRK")
assert frag["mcpServers"]["jira-hub"]["args"][-2:] == ["--project", "STOCKS_DRRK"]
assert frag == core_mcp.mcp_json("STOCKS_DRRK", "jira")
assert frag["mcpServers"]["sdlc-jira"]["args"][-2:] == ["--project", "STOCKS_DRRK"]
assert live["client"].get("/batches")["batches"][0]["author"] == "claude"
assert jmcp.project_create("awpca", "x").startswith("ошибка хаба 409")
assert jmcp.project_create("BAD LABEL", "x").startswith("ошибка хаба 422")
def test_server_name():
assert jmcp.mcp.name == "sdlc-jira"
def test_hub_root_is_repo():
assert (core_mcp.HUB_ROOT / "pyproject.toml").is_file()
assert core_mcp.mcp_json("X")["mcpServers"]["jira-hub"]["args"][:3] == ["run", "--project", str(core_mcp.HUB_ROOT)]
assert core_mcp.mcp_json("X", "jira")["mcpServers"]["sdlc-jira"]["args"][:3] == ["run", "--project", str(core_mcp.HUB_ROOT)]
def test_fmt_projects():

View File

@ -150,11 +150,12 @@ def test_field_clashes_with_core(name):
def test_mcp_json_per_module():
first = core_mcp.mcp_json("AWPCA")["mcpServers"]
assert list(first) == ["jira-hub"] and first["jira-hub"]["args"][-3:] == ["mcp", "--project", "AWPCA"]
first = core_mcp.mcp_json("AWPCA", "jira")["mcpServers"]
assert list(first) == ["sdlc-jira"] and first["sdlc-jira"]["args"][-4:] == ["mcp", "jira", "--project", "AWPCA"]
assert first["sdlc-jira"]["args"][3] == "sdlc-hub"
other = core_mcp.mcp_json("AWPCA", "wiki")["mcpServers"]
assert list(other) == ["jira-hub-wiki"]
assert other["jira-hub-wiki"]["args"][-4:] == ["mcp", "wiki", "--project", "AWPCA"]
assert list(other) == ["sdlc-wiki"]
assert other["sdlc-wiki"]["args"][-4:] == ["mcp", "wiki", "--project", "AWPCA"]
def test_fmt_projects_columns():

View File

@ -288,7 +288,7 @@ def test_jira_server_unchanged():
def test_run_wiki_module():
m = registry.module("wiki")
assert m.mcp_server().name == "jira-hub-wiki"
assert m.mcp_server().name == "sdlc-wiki"
def test_wiki_tool_names_order():
@ -302,7 +302,7 @@ def test_projects_table_and_create(hub):
text = str(out)
assert "AWPCA" in text and "DOC-1" in text and "(текущий)" in text
created = wmcp.project_create("n", "Н", "DOC-5")
assert "jira-hub-wiki" in created
assert "sdlc-wiki" in created
assert fake.calls[-1][2]["wiki_root"] == "DOC-5"