#!/usr/bin/env python3 """Shkor Beauty Assistant — универсальный коннектор. Один и тот же ассистент — три способа подключения для ЛЮБОГО агента: 1) STDIO MCP-стиль (Claude/любой MCP-клиент): assistant → connector.py (JSONL: {"method":"tools/list"} по строке) в конфиге агента: {"mcpServers": {"shkor": {"command": "python3", "args": [".../connector.py"], "transport": "stdio"}}} 2) HTTP JSON API (агент = просто HTTP-клиент): connector.py --http 127.0.0.1:8791 POST /tools/list | POST /call {"name": "...", "arguments": {...}} 3) CLI разовый вызов (curl/крон/скрипт): connector.py --call analyze_skin --args '{"self_report":"стянутость и покраснение"}' 4) Демо-чат (для живой проверки человеком): connector.py --demo Форма запроса принимает MCP tools/call и простую {action, args} — см. assistant.handle(). """ from __future__ import annotations import argparse import json import sys from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent)) from assistant import handle # noqa: E402 # ── 1) STDIO: JSONL, одна строка = один запрос ─────────────────────────────── def serve_stdio() -> None: print(json.dumps({"ok": True, "transport": "stdio-jsonl", "hint": "отправляйте JSON-строки, читайте ответ построчно"}, ensure_ascii=False), flush=True) for line in sys.stdin: line = line.strip() if not line: continue try: resp = handle(json.loads(line)) except json.JSONDecodeError as e: resp = {"error": f"bad json: {e}"} print(json.dumps(resp, ensure_ascii=False), flush=True) # ── 2) HTTP ────────────────────────────────────────────────────────────────── def serve_http(addr: str) -> None: host, port = addr.split(":") port_i = int(port) class H(BaseHTTPRequestHandler): def _send(self, code: int, payload: dict) -> None: body = json.dumps(payload, ensure_ascii=False).encode() self.send_response(code) self.send_header("Content-Type", "application/json; charset=utf-8") self.send_header("Content-Length", str(len(body))) self.end_headers() self.wfile.write(body) def do_GET(self) -> None: # health / открыть в браузере if self.path in ("/", "/health"): self._send(200, {"ok": True, "service": "shkor-assistant", "version": 1}) else: self._send(404, {"error": "not found", "routes": ["POST /tools/list", "POST /call"]}) def do_POST(self) -> None: try: n = int(self.headers.get("Content-Length") or 0) data = json.loads(self.rfile.read(n) or b"{}") except Exception as e: # noqa: BLE001 self._send(400, {"error": f"bad request: {e}"}) return if self.path == "/tools/list": self._send(200, handle({"method": "tools/list"})) elif self.path == "/call": self._send(200, handle({"method": "tools/call", "params": data})) else: self._send(404, {"error": "not found", "routes": ["POST /tools/list", "POST /call"]}) def log_message(self, *a) -> None: # тише в логи pass srv = ThreadingHTTPServer((host, port_i), H) print(f"shkor-assistant HTTP on http://{host}:{port_i} (POST /tools/list, POST /call)", file=sys.stderr) srv.serve_forever() # ── 4) Демо-чат ─────────────────────────────────────────────────────────────── DEMO = [ ("analyze_skin", {"self_report": "кожа стянутая после умывания, лоб блестит к обеду, периодически покраснения"}), ("recommend", {"concerns": ["dryness", "redness"], "skin_type": "sensitive", "budget_max": 70}), ("place_order", {"items": [{"id": "p05", "qty": 1}, {"id": "p03", "qty": 1}], "delivery": "minsk_courier", "name": "Тест", "phone": "+375XX0000000"}), ("usage_instructions", {"product_ids": ["p05", "p03"]}), ("check_order", {}), # order_id подставится после place_order ] def run_demo() -> None: last_order = None for action, args in DEMO: if action == "check_order": if not last_order: continue args = {"order_id": last_order} print(f"\n▶ {action} {json.dumps(args, ensure_ascii=False)}") resp = handle({"method": "tools/call", "params": {"name": action, "arguments": args}}) print(json.dumps(resp.get("result", resp), ensure_ascii=False, indent=1)[:1200]) if action == "place_order" and (resp.get("result") or {}).get("order_id"): last_order = resp["result"]["order_id"] def main() -> None: ap = argparse.ArgumentParser(description="Shkor Beauty Assistant connector") ap.add_argument("--http", metavar="HOST:PORT", help="поднять HTTP API") ap.add_argument("--stdio", action="store_true", help="STDIO JSONL (MCP-стиль)") ap.add_argument("--call", metavar="TOOL", help="разовый вызов инструмента") ap.add_argument("--args", default="{}", help="JSON-аргументы для --call") ap.add_argument("--list-tools", action="store_true") ap.add_argument("--demo", action="store_true") a = ap.parse_args() if a.http: serve_http(a.http) elif a.stdio: serve_stdio() elif a.list_tools: print(json.dumps(handle({"method": "tools/list"}), ensure_ascii=False, indent=1)) elif a.demo: run_demo() elif a.call: try: arguments = json.loads(a.args) except json.JSONDecodeError as e: sys.exit(f"bad --args json: {e}") print(json.dumps(handle({"method": "tools/call", "params": {"name": a.call, "arguments": arguments}}), ensure_ascii=False, indent=1)) else: ap.print_help() if __name__ == "__main__": main()