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).""" """Загрузка инструментов BroJS Journal через MCP (HTTP transport)."""
import asyncio import asyncio
import os import os
import time
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
from dataclasses import dataclass 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" 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({ JOURNAL_COURSES_LESSONS = frozenset({
"courses_list", "courses_list",
@@ -44,7 +54,7 @@ class JournalToolsets:
def _build_mcp_config() -> dict: def _build_mcp_config() -> dict:
return { return {
JOURNAL_SERVER_NAME: { JOURNAL_SERVER_NAME: {
"transport": "http", "transport": "streamable_http",
"url": JOURNAL_MCP_URL, "url": JOURNAL_MCP_URL,
"headers": { "headers": {
"Authorization": f"Bearer {_JOURNAL_TOKEN}", "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]: 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() config = _build_mcp_config()
client = MultiServerMCPClient(config)
# Переиспользуем клиент если уже создан
if _persistent_client is None:
_persistent_client = MultiServerMCPClient(config)
out: dict[str, list] = {} out: dict[str, list] = {}
for name in config: for name in config:
for i, pause in enumerate([0] + _BACKOFF):
try: 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: except Exception as exc:
if _is_429(exc) and i < len(_BACKOFF):
continue
print(f"MCP '{name}': не удалось загрузить инструменты — {type(exc).__name__}: {exc}") print(f"MCP '{name}': не удалось загрузить инструменты — {type(exc).__name__}: {exc}")
# При неизвестной ошибке пересоздаём клиент перед следующей попыткой
_persistent_client = MultiServerMCPClient(config)
out[name] = [] out[name] = []
break
return out return out