#!/usr/bin/env python3 """Сквозной демо-пример использования omnichannel-configserver-mcp. Что делает: 1) засевает демо-сервис в локальный config_server (агентский эндпоинт); 2) запускает omnichannel-configserver-mcp по stdio; 3) выполняет типичный сценарий эксплуатации/деплоя: server_info -> list_applications -> get_application -> set_env -> set_compose -> deploy -> get_task -> overview. Зависимостей нет (только stdlib). Требуется поднятый стенд: docker compose -f examples/demo/docker-compose.yml up -d и собранный бинарник: go build -o omnichannel-configserver-mcp . Запуск: python3 examples/demo/demo.py """ import json import os import select import subprocess import sys import urllib.request HERE = os.path.dirname(os.path.abspath(__file__)) ROOT = os.path.dirname(os.path.dirname(HERE)) # корень репозитория BINARY = os.path.join(ROOT, "omnichannel-configserver-mcp") CONFIG = os.path.join(HERE, "config.demo.json") BASE_URL = "http://127.0.0.1:5005" AGENT_SECRET = "demo-agent-secret" DEMO_APP_ID = 1 def http_json(method, path, payload=None): data = json.dumps(payload).encode() if payload is not None else None req = urllib.request.Request(BASE_URL + path, data=data, method=method, headers={"Content-Type": "application/json"}) with urllib.request.urlopen(req, timeout=10) as resp: return json.loads(resp.read().decode()) def seed_demo_app(): """Регистрирует демо-сервис так, как это делает агент ноды.""" body = { "secret": AGENT_SECRET, "name": "demo_web", "host_ip": "10.0.0.10", "hostname": "demo-node", "path": "/opt/omnichannel/demo_web", "status": "OK", "compose_content": "services:\n web:\n image: nginx:1.25\n", "env_files": {".env": "FOO=bar\n"}, } result = http_json("POST", "/api/register", body) print(f"засеян демо-сервис demo_web: app_id={result.get('app_id')}") class MCP: """Минимальный MCP-клиент по stdio (JSON-RPC, newline-delimited).""" def __init__(self): env = dict(os.environ, OMNI_LOG_LEVEL="warn") self.proc = subprocess.Popen( [BINARY, "-config", CONFIG], cwd=ROOT, env=env, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, bufsize=1, ) self.id = 0 self._request("initialize", { "protocolVersion": "2025-06-18", "capabilities": {}, "clientInfo": {"name": "demo", "version": "0"}, }) self._notify("notifications/initialized", {}) def _id(self): self.id += 1 return self.id def _send(self, obj): self.proc.stdin.write(json.dumps(obj) + "\n") self.proc.stdin.flush() def _recv(self, want_id, timeout=30): while True: ready, _, _ = select.select([self.proc.stdout], [], [], timeout) if not ready: raise TimeoutError(f"нет ответа на id={want_id}") line = self.proc.stdout.readline() if not line: raise RuntimeError("сервер закрыл stdout:\n" + self.proc.stderr.read()) msg = json.loads(line) if msg.get("id") == want_id: return msg def _request(self, method, params): rid = self._id() self._send({"jsonrpc": "2.0", "id": rid, "method": method, "params": params}) return self._recv(rid) def _notify(self, method, params): self._send({"jsonrpc": "2.0", "method": method, "params": params}) def tools(self): return self._request("tools/list", {})["result"]["tools"] def call(self, name, arguments): resp = self._request("tools/call", {"name": name, "arguments": arguments}) result = resp.get("result", {}) text = "".join(c.get("text", "") for c in result.get("content", [])) return text, result.get("isError", False) def close(self): self.proc.stdin.close() self.proc.terminate() def show(title, name, arguments, mcp): text, is_error = mcp.call(name, arguments) print(f"\n=== {title} ({'ошибка' if is_error else 'ok'}) ===") print(text[:600]) def main(): seed_demo_app() mcp = MCP() tools = mcp.tools() print(f"\nMCP-сервер отдал инструментов: {len(tools)}") show("Версия API и режим", "server_info", {}, mcp) show("Список сервисов", "list_applications", {}, mcp) show("Детали демо-сервиса", "get_application", {"app_id": DEMO_APP_ID}, mcp) show("Обновляем env", "set_env", {"app_id": DEMO_APP_ID, "filename": ".env", "content": "FOO=baz\n"}, mcp) show("Обновляем compose", "set_compose", {"app_id": DEMO_APP_ID, "content": "services:\n web:\n image: nginx:1.27\n"}, mcp) deploy_text, _ = mcp.call("deploy", {"app_id": DEMO_APP_ID}) print("\n=== Запуск сервиса (deploy) ===") print(deploy_text[:400]) try: task_id = json.loads(deploy_text)["task_id"] show("Статус задачи деплоя", "get_task", {"task_id": task_id}, mcp) except (ValueError, KeyError): print("(не удалось извлечь task_id — пропускаем get_task)") show("Сводка состояния стенда", "overview", {}, mcp) mcp.close() print("\nDemo завершён.") if __name__ == "__main__": if not os.path.exists(BINARY): sys.exit(f"нет бинарника {BINARY}: сначала выполните `go build -o omnichannel-configserver-mcp .`") main()