Async MCP server с job queue: почему polling, а не блокирующий вызов
Паттерн «async submit + poll-by-ID» для долгих операций в MCP — на примере design-system extractor'а для Cursor: Playwright-анализ сайта, Redis-очередь, stateless trade-off.
Проблема
Наивный MCP-сервер делает работу синхронно: агент вызывает tool — ждёт ответа. Для быстрых операций норма. Для долгих (анализ сайта браузерной автоматизацией, ML-инференс, обход страниц) — блокировка: таймауты, нет горизонтального масштабирования, один тяжёлый запрос держит соединение.
Методика
Разнести submit и результат: async job queue с poll-by-ID.
- 1. Submit возвращает job ID мгновенно, не блокируя. Статус опрашивается отдельным вызовом.
-
2. Playwright в контейнере делает фактический анализ
сайта (design tokens: цвета, типографика, спейсинг, компоненты) — изолированно,
node_modulesхоста явно исключён из образа. - 3. Redis под очередью — персистентность задач и конкурентность (max 5 одновременных job).
- 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 — единственный репо серии с валидацией сообществом.
Где ломается
- 404-after-completion как намеренное решение. Статус job удаляется сразу после завершения: 404 = «done or failed». Это сознательный trade-off (нет накопления состояния), но он ломает ожидание «история job persists» — клиент обязан забрать результат по факту завершения, а не «когда-нибудь потом». Это и есть архитектурный разговор: где граница между stateless-простотой и удобством потребителя.
- Max 5 concurrent — защита от перегрузки, но при всплеске нагрузки job встают в очередь; нужен backpressure-контракт на стороне клиента.
Для кого и почему
Если вы строите MCP-инструменты для операций дольше нескольких секунд — браузерная автоматизация, ML-инференс, анализ сайтов — паттерн submit→poll применим напрямую. 8★ на GitHub говорят, что задача типовая.
Хотите выстроить MCP-инфраструктуру под долгие операции?
Проектирование async job queue, stateless trade-off и backpressure-контракта для агентного workflow в вашей команде.
Написать на почтуДругие разборы
Серия инженерных разборов: реальная задача → методика → работающий артефакт → честный разбор, где он ломается.
К серии →