memo: спека, ТЗ, тест-план, MCP-зонд, gradle wrapper

This commit is contained in:
Hermes Agent
2026-10-02 00:59:33 +03:00
commit cefc17d782
10 changed files with 1077 additions and 0 deletions
+119
View File
@@ -0,0 +1,119 @@
#!/usr/bin/env python3
"""Протокольный зонд MCP-сервера (stdio, JSON-RPC 2.0).
Запускает сервер как настоящий MCP-клиент и проверяет, что он отвечает по протоколу,
а не «выглядит работающим». Использование:
python3 scripts/mcp_probe.py --cmd ./memo-mcp/build/install/memo-mcp/bin/memo-mcp
python3 scripts/mcp_probe.py --cmd ./... --call memo_search \
--args '{"path":"/root/WORK/memo-e2e/infra","query":"домены","k":3}'
Выход 0 — протокол в порядке; 1 — есть ошибки (печатаются).
"""
import argparse
import json
import shlex
import subprocess
import sys
def rpc(proc, method, params=None, notify=False, rid=None):
msg = {"jsonrpc": "2.0", "method": method}
if params is not None:
msg["params"] = params
if not notify:
msg["id"] = rid if rid is not None else 1
proc.stdin.write(json.dumps(msg) + "\n")
proc.stdin.flush()
if notify:
return None
line = proc.stdout.readline()
if not line:
return None
try:
return json.loads(line)
except json.JSONDecodeError:
return {"_raw": line.strip()}
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--cmd", required=True, help="команда запуска MCP-сервера (stdio)")
ap.add_argument("--call", help="имя инструмента для tools/call")
ap.add_argument("--args", default="{}", help="JSON-аргументы инструмента")
ap.add_argument("--timeout", type=float, default=60.0)
a = ap.parse_args()
errors: list[str] = []
proc = subprocess.Popen(
shlex.split(a.cmd),
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
text=True, bufsize=1,
)
try:
init = rpc(proc, "initialize", {
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": {"name": "mcp_probe", "version": "1"},
}, rid=1)
if not init or "result" not in init:
errors.append(f"initialize: нет result (ответ {init})")
else:
info = init["result"].get("serverInfo", {})
print(f"OK initialize: {info.get('name')} {info.get('version')}")
rpc(proc, "notifications/initialized", {}, notify=True)
tools = rpc(proc, "tools/list", {}, rid=2)
names: list[str] = []
if not tools or "result" not in tools:
errors.append(f"tools/list: нет result (ответ {tools})")
else:
names = [t["name"] for t in tools["result"].get("tools", [])]
if not names:
errors.append("tools/list: пустой список инструментов")
else:
print(f"OK tools/list: {len(names)} -> {', '.join(names)}")
if a.call:
if names and a.call not in names:
errors.append(f"tools/call: инструмента {a.call} нет в tools/list")
else:
res = rpc(proc, "tools/call",
{"name": a.call, "arguments": json.loads(a.args)}, rid=3)
if not res or "result" not in res:
errors.append(f"tools/call: нет result (ответ {res})")
elif res["result"].get("isError"):
errors.append(f"tools/call: isError=true ({res['result']})")
else:
content = res["result"].get("content") or []
text = content[0].get("text", "") if content else ""
if not text.strip():
errors.append("tools/call: пустой content[0].text")
else:
print(f"OK tools/call {a.call}: {len(text)} символов")
print("---- первые 600 символов ----")
print(text[:600])
finally:
proc.terminate()
try:
proc.wait(timeout=5)
except subprocess.TimeoutExpired:
proc.kill()
err = proc.stderr.read()
if err.strip():
print("---- stderr сервера ----")
print(err[:1000])
if errors:
print("\nОШИБКИ:")
for e in errors:
print(" -", e)
return 1
print("\nПРОТОКОЛ В ПОРЯДКЕ")
return 0
if __name__ == "__main__":
sys.exit(main())