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:
parent
e34d61a483
commit
08460dc281
@ -17,8 +17,8 @@ def main(argv=None) -> int:
|
|||||||
p = sub.add_parser("serve", help="запустить HTTP API")
|
p = sub.add_parser("serve", help="запустить HTTP API")
|
||||||
p.add_argument("--host", default="0.0.0.0")
|
p.add_argument("--host", default="0.0.0.0")
|
||||||
p.add_argument("--port", type=int)
|
p.add_argument("--port", type=int)
|
||||||
p = sub.add_parser("mcp", help="MCP-сервер модуля по stdio (по умолчанию jira)")
|
p = sub.add_parser("mcp", help="MCP-сервер модуля по stdio ")
|
||||||
p.add_argument("module", nargs="?", default="jira", help="имя модуля; неизвестное — код 2 (проверка в mcp.run)")
|
p.add_argument("module", nargs="?", default=None, help="имя модуля (jira, wiki); без модуля или неизвестный — код 2")
|
||||||
p.add_argument("--project", help="метка проекта, к которому привязана сессия")
|
p.add_argument("--project", help="метка проекта, к которому привязана сессия")
|
||||||
for m in registry.modules():
|
for m in registry.modules():
|
||||||
if m.cli_register:
|
if m.cli_register:
|
||||||
|
|||||||
@ -25,13 +25,10 @@ def unknown_project(label) -> str:
|
|||||||
return f"проект {label} не заведён в хабе: project_create или исправьте --project в .mcp.json"
|
return f"проект {label} не заведён в хабе: project_create или исправьте --project в .mcp.json"
|
||||||
|
|
||||||
|
|
||||||
def mcp_json(label: str, module: str = registry.DEFAULT_AGENT) -> dict:
|
def mcp_json(label: str, module: str) -> dict:
|
||||||
"""Фрагмент .mcp.json для репозитория проекта: у модуля по умолчанию прежний (сервер jira-hub), у прочих модулей —
|
"""Фрагмент .mcp.json для репозитория проекта: сервер sdlc-<модуль>, запуск `sdlc-hub mcp <модуль>`."""
|
||||||
сервер jira-hub-<модуль>."""
|
run = ["run", "--project", str(HUB_ROOT), "sdlc-hub", "mcp", module, "--project", label]
|
||||||
run = ["run", "--project", str(HUB_ROOT), "sdlc-hub", "mcp"]
|
return {"mcpServers": {f"sdlc-{module}": {"command": "uv", "args": run}}}
|
||||||
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 client() -> HubClient:
|
def client() -> HubClient:
|
||||||
@ -199,9 +196,13 @@ def register_core_tools(mcp, *, projects_doc: str, columns: list[tuple[str, Call
|
|||||||
mcp.tool()(project_create)
|
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, сервер не стартует."""
|
"""Запустить MCP-сервер модуля по stdio. Неизвестный модуль: список модулей в stderr, код 2, сервер не стартует."""
|
||||||
global PROJECT
|
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)
|
m = registry.module(module_name)
|
||||||
if m is None or m.mcp_server is None:
|
if m is None or m.mcp_server is None:
|
||||||
names = ", ".join(x.name for x in registry.modules() if x.mcp_server)
|
names = ", ".join(x.name for x in registry.modules() if x.mcp_server)
|
||||||
|
|||||||
@ -19,7 +19,7 @@ from .commands import describe, is_temp
|
|||||||
|
|
||||||
POLL_SECONDS = 5
|
POLL_SECONDS = 5
|
||||||
BOTH_FILE_AND_PATH = "у ensure_attachment либо path, либо file"
|
BOTH_FILE_AND_PATH = "у ensure_attachment либо path, либо file"
|
||||||
mcp = FastMCP("jira-hub")
|
mcp = FastMCP("sdlc-jira")
|
||||||
fmt_agent = core.fmt_agent
|
fmt_agent = core.fmt_agent
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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 страниц и
|
(specs/mcp/spec.md «MCP вики»). Охрана, привязка к проекту и projects/project_create — в core/mcp.py. Markdown страниц и
|
||||||
имена вложений приходят из Eva и считаются недоверенными: страница выдаётся текстом, имя файла сводится к basename."""
|
имена вложений приходят из Eva и считаются недоверенными: страница выдаётся текстом, имя файла сводится к basename."""
|
||||||
import asyncio
|
import asyncio
|
||||||
@ -16,7 +16,7 @@ ATTACHMENT_WAIT = 60
|
|||||||
SYNC_WAIT = 300
|
SYNC_WAIT = 300
|
||||||
CHUNK = 20000
|
CHUNK = 20000
|
||||||
CANDIDATES = 10
|
CANDIDATES = 10
|
||||||
mcp = FastMCP("jira-hub-wiki")
|
mcp = FastMCP("sdlc-wiki")
|
||||||
|
|
||||||
|
|
||||||
def _project() -> dict:
|
def _project() -> dict:
|
||||||
@ -242,7 +242,7 @@ def root_set(root: str) -> str:
|
|||||||
@core.guard
|
@core.guard
|
||||||
def project_create(label: str, name: str, wiki_root: str = "") -> str:
|
def project_create(label: str, name: str, wiki_root: str = "") -> str:
|
||||||
"""Завести проект (метка, название, необязательно корень вики: код DOC-… или ссылка Eva). С корнем хаб ставит
|
"""Завести проект (метка, название, необязательно корень вики: код DOC-… или ссылка Eva). С корнем хаб ставит
|
||||||
синхронизацию; в ответе — фрагмент .mcp.json для репозитория проекта с сервером jira-hub-wiki."""
|
синхронизацию; в ответе — фрагмент .mcp.json для репозитория проекта с сервером sdlc-wiki."""
|
||||||
try:
|
try:
|
||||||
code = wiki_roots.parse_root(wiki_root)
|
code = wiki_roots.parse_root(wiki_root)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
|
|||||||
@ -134,7 +134,7 @@
|
|||||||
"name": "projects"
|
"name": "projects"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Завести проект (метка, название, необязательно корень вики: код DOC-… или ссылка Eva). С корнем хаб ставит\nсинхронизацию; в ответе — фрагмент .mcp.json для репозитория проекта с сервером jira-hub-wiki.",
|
"description": "Завести проект (метка, название, необязательно корень вики: код DOC-… или ссылка Eva). С корнем хаб ставит\nсинхронизацию; в ответе — фрагмент .mcp.json для репозитория проекта с сервером sdlc-wiki.",
|
||||||
"inputSchema": {
|
"inputSchema": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"label": {
|
"label": {
|
||||||
|
|||||||
@ -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"]
|
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.core import mcp as core_mcp
|
||||||
from sdlc_hub.modules.jira import mcp as jira_mcp
|
from sdlc_hub.modules.jira import mcp as jira_mcp
|
||||||
seen = []
|
seen = []
|
||||||
monkeypatch.setattr(core_mcp, "PROJECT", None)
|
monkeypatch.setattr(core_mcp, "PROJECT", None) # run() пишет глобал; monkeypatch вернёт прежнее значение
|
||||||
monkeypatch.setattr(jira_mcp.mcp, "run", lambda transport="stdio": seen.append((transport, core_mcp.PROJECT)))
|
monkeypatch.setattr(jira_mcp.mcp, "run", lambda transport="stdio": seen.append(core_mcp.PROJECT))
|
||||||
assert cli.main(["mcp", "--project", "AWPCA"]) == 0
|
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 cli.main(["mcp", "jira", "--project", "AWPCA"]) == 0
|
||||||
assert seen == [("stdio", "AWPCA"), ("stdio", "AWPCA")]
|
assert seen == ["AWPCA"]
|
||||||
# без --project сессия не привязана к проекту
|
|
||||||
monkeypatch.setattr(core_mcp, "PROJECT", "STALE")
|
|
||||||
assert cli.main(["mcp"]) == 0
|
|
||||||
assert seen[-1] == ("stdio", None)
|
|
||||||
|
|
||||||
|
|
||||||
def test_mcp_unknown_module(monkeypatch, capsys):
|
def test_mcp_unknown_module(monkeypatch, capsys):
|
||||||
|
|||||||
@ -503,16 +503,20 @@ def test_project_create(live, monkeypatch): # сце
|
|||||||
out = jmcp.project_create("STOCKS_DRRK", "Запасы ДРРК", 5120)
|
out = jmcp.project_create("STOCKS_DRRK", "Запасы ДРРК", 5120)
|
||||||
assert "STOCKS_DRRK" in out and re.search(r"пакет[^#]*#\d+", out) and "/mcp" in out
|
assert "STOCKS_DRRK" in out and re.search(r"пакет[^#]*#\d+", out) and "/mcp" in out
|
||||||
frag = json.loads(out[out.index("{"):])
|
frag = json.loads(out[out.index("{"):])
|
||||||
assert frag == core_mcp.mcp_json("STOCKS_DRRK")
|
assert frag == core_mcp.mcp_json("STOCKS_DRRK", "jira")
|
||||||
assert frag["mcpServers"]["jira-hub"]["args"][-2:] == ["--project", "STOCKS_DRRK"]
|
assert frag["mcpServers"]["sdlc-jira"]["args"][-2:] == ["--project", "STOCKS_DRRK"]
|
||||||
assert live["client"].get("/batches")["batches"][0]["author"] == "claude"
|
assert live["client"].get("/batches")["batches"][0]["author"] == "claude"
|
||||||
assert jmcp.project_create("awpca", "x").startswith("ошибка хаба 409")
|
assert jmcp.project_create("awpca", "x").startswith("ошибка хаба 409")
|
||||||
assert jmcp.project_create("BAD LABEL", "x").startswith("ошибка хаба 422")
|
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():
|
def test_hub_root_is_repo():
|
||||||
assert (core_mcp.HUB_ROOT / "pyproject.toml").is_file()
|
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():
|
def test_fmt_projects():
|
||||||
|
|||||||
@ -150,11 +150,12 @@ def test_field_clashes_with_core(name):
|
|||||||
|
|
||||||
|
|
||||||
def test_mcp_json_per_module():
|
def test_mcp_json_per_module():
|
||||||
first = core_mcp.mcp_json("AWPCA")["mcpServers"]
|
first = core_mcp.mcp_json("AWPCA", "jira")["mcpServers"]
|
||||||
assert list(first) == ["jira-hub"] and first["jira-hub"]["args"][-3:] == ["mcp", "--project", "AWPCA"]
|
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"]
|
other = core_mcp.mcp_json("AWPCA", "wiki")["mcpServers"]
|
||||||
assert list(other) == ["jira-hub-wiki"]
|
assert list(other) == ["sdlc-wiki"]
|
||||||
assert other["jira-hub-wiki"]["args"][-4:] == ["mcp", "wiki", "--project", "AWPCA"]
|
assert other["sdlc-wiki"]["args"][-4:] == ["mcp", "wiki", "--project", "AWPCA"]
|
||||||
|
|
||||||
|
|
||||||
def test_fmt_projects_columns():
|
def test_fmt_projects_columns():
|
||||||
|
|||||||
@ -288,7 +288,7 @@ def test_jira_server_unchanged():
|
|||||||
|
|
||||||
def test_run_wiki_module():
|
def test_run_wiki_module():
|
||||||
m = registry.module("wiki")
|
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():
|
def test_wiki_tool_names_order():
|
||||||
@ -302,7 +302,7 @@ def test_projects_table_and_create(hub):
|
|||||||
text = str(out)
|
text = str(out)
|
||||||
assert "AWPCA" in text and "DOC-1" in text and "(текущий)" in text
|
assert "AWPCA" in text and "DOC-1" in text and "(текущий)" in text
|
||||||
created = wmcp.project_create("n", "Н", "DOC-5")
|
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"
|
assert fake.calls[-1][2]["wiki_root"] == "DOC-5"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user