Григорий Добряков

Howto · разбор

Разбор 04 GitHub · 8 stars · 4 forks

Async MCP server с job queue: почему polling, а не блокирующий вызов

Паттерн «async submit + poll-by-ID» для долгих операций в MCP — на примере design-system extractor'а для Cursor: Playwright-анализ сайта, Redis-очередь, stateless trade-off.

CTO Head of AI Architect Tech Lead

Проблема

Наивный MCP-сервер делает работу синхронно: агент вызывает tool — ждёт ответа. Для быстрых операций норма. Для долгих (анализ сайта браузерной автоматизацией, ML-инференс, обход страниц) — блокировка: таймауты, нет горизонтального масштабирования, один тяжёлый запрос держит соединение.

Методика

Разнести submit и результат: async job queue с poll-by-ID.

  1. 1. Submit возвращает job ID мгновенно, не блокируя. Статус опрашивается отдельным вызовом.
  2. 2. Playwright в контейнере делает фактический анализ сайта (design tokens: цвета, типографика, спейсинг, компоненты) — изолированно, node_modules хоста явно исключён из образа.
  3. 3. Redis под очередью — персистентность задач и конкурентность (max 5 одновременных job).
  4. 4. MCP-сервер на 3001, мультипротокол: HTTP / HTTPS / SSH-туннель — гибкость под разные сценарии деплоя и интеграцию с Cursor.

Производственные сервисы из docker-compose.yml:

services:
  redis:
    image: redis:7-alpine
    container_name: design-system-redis
    volumes:
      - redis-data:/data          # персистентность очереди между рестартами
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]

  api:
    build: .
    container_name: design-system-api
    ports:
      - "${PORT:-3000}:3000"
      - "${MCP_PORT:-3001}:3001"  # REST API + MCP на разных портах
    environment:
      - MAX_CONCURRENT_ANALYSES=${MAX_CONCURRENT_ANALYSES:-5}
      - REDIS_URL=${REDIS_URL:-redis://redis:6379}
      - MCP_PROTOCOL=${MCP_PROTOCOL:-http}
    depends_on:
      redis:
        condition: service_healthy

volumes:
  redis-data:

Жизненный цикл job и почему 404 — это контракт, а не баг:

POST /analyze        → 202 {"job_id": "abc123"}
GET  /job/abc123     → 200 {"status": "running"}
GET  /job/abc123     → 200 {"status": "done", "result": {...}}
GET  /job/abc123     → 404                  # статус удалён сразу после выдачи

Клиент обязан забрать результат на первом done. Второй запрос — уже 404: сервер не копит состояние. Это сознательный stateless-trade-off, и потребитель проектируется под него, а не вопреки.

Артефакт

github.com/dobryakov/design-system-mcp (TypeScript, Docker/Compose, Jest, Playwright E2E). 8★ / 4 forks — единственный репо серии с валидацией сообществом.

Подпись серии

Где ломается

Для кого и почему

Если вы строите MCP-инструменты для операций дольше нескольких секунд — браузерная автоматизация, ML-инференс, анализ сайтов — паттерн submit→poll применим напрямую. 8★ на GitHub говорят, что задача типовая.

Хотите выстроить MCP-инфраструктуру под долгие операции?

Проектирование async job queue, stateless trade-off и backpressure-контракта для агентного workflow в вашей команде.

Написать на почту

Другие разборы

Серия инженерных разборов: реальная задача → методика → работающий артефакт → честный разбор, где он ломается.

К серии →