optimize mcp_client: persistent client, exponential backoff, inter-call delay

- 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 <noreply@anthropic.com>
This commit is contained in:
2026-06-04 12:36:46 +03:00
parent 7804122b7e
commit 420543c89e
+40 -3
View File
@@ -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:
for i, pause in enumerate([0] + _BACKOFF):
try:
out[name] = await client.get_tools(server_name=name)
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