From 420543c89ec94931203060d66d48974b07639e69 Mon Sep 17 00:00:00 2001 From: gleb Date: Thu, 4 Jun 2026 12:36:46 +0300 Subject: [PATCH] optimize mcp_client: persistent client, exponential backoff, inter-call delay MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Switch transport to streamable_http (matches solve_task.py) - Add _persistent_client global to reuse MultiServerMCPClient across calls - Add exponential backoff 15→30→60→120→240s on 429 responses - Add _INTER_CALL_DELAY=1.5s between consecutive MCP calls to prevent burst - Add _is_429() helper for clean 429 detection - Recreate client on non-429 errors to recover from stale sessions Co-Authored-By: Claude Sonnet 4.6 --- src/agent/mcp_client.py | 51 +++++++++++++++++++++++++++++++++++------ 1 file changed, 44 insertions(+), 7 deletions(-) diff --git a/src/agent/mcp_client.py b/src/agent/mcp_client.py index 33777e4..70c25af 100644 --- a/src/agent/mcp_client.py +++ b/src/agent/mcp_client.py @@ -1,6 +1,7 @@ """Загрузка инструментов BroJS Journal через MCP (HTTP transport).""" import asyncio import os +import time from concurrent.futures import ThreadPoolExecutor from dataclasses import dataclass @@ -17,6 +18,15 @@ _JOURNAL_TOKEN = os.getenv("JOURNAL_TOKEN", "YOUR_JOURNAL_TOKEN_HERE") JOURNAL_MCP_URL = "https://platform.brojs.ru/jrnl-bh/api/mcp" +# Exponential backoff: 15, 30, 60, 120, 240 секунд +_BACKOFF = [15, 30, 60, 120, 240] +# Минимальная пауза между последовательными MCP-вызовами (предотвращает burst) +_INTER_CALL_DELAY = 1.5 + +# Персистентный клиент и время последнего вызова — переиспользуются в рамках одного запуска +_persistent_client: MultiServerMCPClient | None = None +_last_mcp_call_time: float = 0.0 + # Инструменты для работы с курсами и уроками JOURNAL_COURSES_LESSONS = frozenset({ "courses_list", @@ -44,7 +54,7 @@ class JournalToolsets: def _build_mcp_config() -> dict: return { JOURNAL_SERVER_NAME: { - "transport": "http", + "transport": "streamable_http", "url": JOURNAL_MCP_URL, "headers": { "Authorization": f"Bearer {_JOURNAL_TOKEN}", @@ -53,16 +63,43 @@ def _build_mcp_config() -> dict: } +def _is_429(exc: Exception) -> bool: + return "429" in str(exc) + + async def _fetch_tools() -> dict[str, list]: + """Загружает MCP-инструменты с персистентным клиентом и экспоненциальным backoff при 429.""" + global _persistent_client, _last_mcp_call_time + + # Пауза между последовательными вызовами — предотвращает burst + elapsed = time.monotonic() - _last_mcp_call_time + if elapsed < _INTER_CALL_DELAY: + await asyncio.sleep(_INTER_CALL_DELAY - elapsed) + config = _build_mcp_config() - client = MultiServerMCPClient(config) + + # Переиспользуем клиент если уже создан + if _persistent_client is None: + _persistent_client = MultiServerMCPClient(config) + out: dict[str, list] = {} for name in config: - try: - out[name] = await client.get_tools(server_name=name) - except Exception as exc: - print(f"MCP '{name}': не удалось загрузить инструменты — {type(exc).__name__}: {exc}") - out[name] = [] + for i, pause in enumerate([0] + _BACKOFF): + try: + if pause: + print(f" [mcp_client] '{name}' → 429, жду {pause}с (попытка {i+1})...") + await asyncio.sleep(pause) + out[name] = await _persistent_client.get_tools(server_name=name) + _last_mcp_call_time = time.monotonic() + break + except Exception as exc: + if _is_429(exc) and i < len(_BACKOFF): + continue + print(f"MCP '{name}': не удалось загрузить инструменты — {type(exc).__name__}: {exc}") + # При неизвестной ошибке пересоздаём клиент перед следующей попыткой + _persistent_client = MultiServerMCPClient(config) + out[name] = [] + break return out