diff --git a/src/sdlc_hub/core/cli.py b/src/sdlc_hub/core/cli.py index 49a3447..761b364 100644 --- a/src/sdlc_hub/core/cli.py +++ b/src/sdlc_hub/core/cli.py @@ -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: diff --git a/src/sdlc_hub/core/mcp.py b/src/sdlc_hub/core/mcp.py index 287986e..56bb161 100644 --- a/src/sdlc_hub/core/mcp.py +++ b/src/sdlc_hub/core/mcp.py @@ -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) diff --git a/src/sdlc_hub/modules/jira/mcp.py b/src/sdlc_hub/modules/jira/mcp.py index d516816..ca51fc1 100644 --- a/src/sdlc_hub/modules/jira/mcp.py +++ b/src/sdlc_hub/modules/jira/mcp.py @@ -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 diff --git a/src/sdlc_hub/modules/wiki/mcp.py b/src/sdlc_hub/modules/wiki/mcp.py index a99edda..18e5a89 100644 --- a/src/sdlc_hub/modules/wiki/mcp.py +++ b/src/sdlc_hub/modules/wiki/mcp.py @@ -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: diff --git a/tests/fixtures/baseline/mcp_wiki_tools.expected.json b/tests/fixtures/baseline/mcp_wiki_tools.expected.json index 25149f2..8c3c1de 100644 --- a/tests/fixtures/baseline/mcp_wiki_tools.expected.json +++ b/tests/fixtures/baseline/mcp_wiki_tools.expected.json @@ -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": { diff --git a/tests/test_cli.py b/tests/test_cli.py index 686cc8a..51bdcf1 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -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): diff --git a/tests/test_mcp.py b/tests/test_mcp.py index 1d17711..35cfaeb 100644 --- a/tests/test_mcp.py +++ b/tests/test_mcp.py @@ -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(): diff --git a/tests/test_project_fields.py b/tests/test_project_fields.py index 09d3e2c..b24a563 100644 --- a/tests/test_project_fields.py +++ b/tests/test_project_fields.py @@ -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(): diff --git a/tests/test_wiki_mcp.py b/tests/test_wiki_mcp.py index d316fd7..5cf1106 100644 --- a/tests/test_wiki_mcp.py +++ b/tests/test_wiki_mcp.py @@ -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"