Async MCP server with job queue: design-system extractor for Cursor
Separate submit from result: async job queue with poll-by-ID, and 404-after-completion as a deliberate stateless trade-off.
Problem
A naive MCP server does its work synchronously: the agent calls a tool and waits for the response. Fine for fast operations. For long ones — browser automation site analysis, ML inference, multi-page crawling — it blocks: timeouts, no horizontal scaling, a single heavy request holds the connection.
Methodology
Separate submit from result: async job queue with poll-by-ID.
- 1. Submit returns a job ID immediately, without blocking. Status is polled with a separate call.
-
2. Playwright in a container does the actual site
analysis (design tokens: colors, typography, spacing, components) — isolated,
with
node_modulesfrom the host explicitly excluded from the image. - 3. Redis under the queue — task persistence and concurrency (max 5 simultaneous jobs).
- 4. MCP server on port 3001, multi-protocol: HTTP / HTTPS / SSH tunnel — flexibility for different deployment scenarios and Cursor integration.
# docker-compose.yml (production services)
services:
redis:
image: redis:7-alpine
container_name: design-system-redis
volumes:
- redis-data:/data # queue persistence across restarts
healthcheck:
test: ["CMD", "redis-cli", "ping"]
api:
build: .
container_name: design-system-api
ports:
- "${PORT:-3000}:3000"
- "${MCP_PORT:-3001}:3001" # REST API + MCP on separate ports
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 lifecycle — and why 404 is a contract, not a bug:
POST /analyze → 202 {"job_id": "abc123"}
GET /job/abc123 → 200 {"status": "running"}
GET /job/abc123 → 200 {"status": "done", "result": {...}}
GET /job/abc123 → 404 # status deleted immediately after delivery
The client must retrieve the result on the first done
response. A second request is already 404: the server doesn't accumulate state. This
is a deliberate stateless trade-off — the consumer is designed around it, not against it.
Artifact
github.com/dobryakov/design-system-mcp (TypeScript, Docker/Compose, Jest, Playwright E2E). ★8 / 4 forks — the only repo in the series with community validation.
Where it breaks
- 404-after-completion as a deliberate decision. Job status is deleted immediately after completion: 404 = "done or failed." This is a conscious trade-off (no state accumulation), but it breaks the expectation that "job history persists" — the client must retrieve the result as soon as it completes, not "sometime later." This is the architectural conversation: where is the line between stateless simplicity and consumer convenience?
- Max 5 concurrent — protection against overload, but under traffic spikes jobs queue up; a backpressure contract is needed on the client side.
For whom and why
The "async submit + poll + deliberate stateless trade-off" pattern is architectural, not tutorial-level. ★8 provides independent confirmation that the technical audience finds this interesting. The strongest externally validated entry in the series.
Building MCP-based tooling for your engineering team?
Async patterns, stateless design trade-offs, and production-grade MCP server architecture — with a working artifact, not a tutorial.
Email meOther breakdowns
An engineering breakdown series: real task → methodology → working artifact → honest breakdown of where it fails.
Back to series →