3 Commits
Author SHA1 Message Date
ClaudeandClaude Fable 5 ce868a7717 fix(gunicorn): warn at startup when GUNICORN_WORKERS>1 breaks MCP sessions
MCP sessions live in a per-process in-memory dict on whatever worker
handled the initialize request (StreamableHTTPSessionManager._server_instances).
gunicorn round-robins HTTP requests across workers, so the same
mcp-session-id frequently lands on a different worker and returns
'Session not found' / 'Invalid or expired session ID'. fastapi-mcp
hardcodes stateless=False, and gunicorn has no built-in sticky-session
support, so there is no in-process workaround.

This change keeps the existing workers=2 default (the project does not
want to give up the modest parallelism it gives) but adds:

  * A comment block above workers= explaining the constraint, the
    workers=1 fix, and the nginx sticky-session escape hatch.
  * An on_starting(server) hook that logs a WARNING whenever
    workers > 1, naming the symptom and the fix so the misconfig is
    loud at deploy time rather than silent at request time.
  * Three regression tests in tests/unit/test_gunicorn_config.py
    asserting the hook exists, is silent for workers=1, and warns
    (with the expected text) for workers>1.
  * A 'MCP session affinity' note in CLAUDE.md key conventions so
    the constraint is recorded for future maintainers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-29 17:02:37 +08:00
Claude 3ec9ea37fd update: proc_name 2026-06-25 10:45:05 +08:00
Claude 6bcf3b9ac9 feat: run FastAPI under gunicorn with uvicorn ASGI workers
Production entrypoint switch:
  - pyproject.toml: add gunicorn>=23.0 dep
  - gunicorn.conf.py: env-var-driven config (bind, workers, threads,
    timeout, graceful_timeout, keepalive, log level, access-log format)
  - Dockerfile: CMD gunicorn main:app (auto-loads gunicorn.conf.py from
    WORKDIR /app)
  - docker-compose.yml: forward GUNICORN_WORKERS / GUNICORN_TIMEOUT /
    GUNICORN_BIND
  - .env.example: document the new tunables

Why gunicorn over standalone uvicorn for production:
  - Process supervision: master restarts crashed workers, restarts on
    memory leaks
  - Graceful shutdown: SIGTERM drains workers in-flight
  - Multi-worker: concurrent requests actually run in parallel
  - Standard ops: k8s readiness probes, log aggregators, etc. all know
    gunicorn

Why uvicorn workers (not sync workers): gunicorn can't natively serve
ASGI; uvicorn.workers.UvicornWorker is the canonical way to run an
ASGI app under gunicorn.

Defaults:
  - 2 workers (small MCP service; raise for high concurrency)
  - 1 thread per worker (no blocking I/O)
  - 120s timeout (yarn logs can be slow; uvicorn's 30s default is too
    tight)

Verified: gunicorn boots, lifespan runs (14 tools logged), MCP
initialize + tools/list + tools/call all work, /openapi.json = 200,
multiple gunicorn worker processes visible in ps.

uv sync picked up gunicorn 26.0.0. Tests still 116/116.
2026-06-25 10:44:25 +08:00