HTTP Transport and Making It Real
FastAPI, Docker, security hardening, and the auth system that knows who you are
Phase 1 proved the architecture. Phase 2 is about making it work for real people on a real server. The gap between those two things turned out to be substantial.
The foundation
We started with Alembic for database migrations. Greg is insistent: raw SQL only, no SQLAlchemy ORM in migrations. The reasoning is sound — we use asyncpg for queries, so an ORM migration layer would be a parallel schema definition that drifts from the actual queries.
Then the HTTP pivot: wrapping the MCP server in FastAPI. The app factory pattern — create_mcp_server() returns tools, create_app() returns a FastAPI app with health endpoints and the MCP protocol mounted at /mcp. No module-level state. The CLI gained --transport stdio|http, so the same codebase serves both Claude Desktop (stdio) and remote clients (HTTP).
Swagger UI came free with FastAPI — OpenAPI 3.1 docs at /docs. Greg had flagged this as a requirement early: “all endpoints must have type annotations, descriptions, and parameter examples.” FastAPI makes this almost effortless.
Docker
One image, three entrypoints: python -m mcprospero.server, python -m mcprospero.worker, python -m alembic upgrade head. Non-root user, tini as PID 1. The compose file orchestrates Postgres, Temporal, MinIO, server, worker, and a one-shot migration container.
The compose file needed immediate fixes: a YAML anchor collision, Temporal needing SKIP_DYNAMIC_CONFIG_UPDATE=true, and a healthcheck pointing at the wrong address. Real-world testing catches what code review doesn’t.
Agent slugs
A small but meaningful UX improvement: email-monitor instead of a1b2c3d4e5f6. All MCP tools that take agent_id now accept either a hex ID or a slug. Slugs are auto-generated from agent names and unique per org. “start email-monitor” is a lot more natural than “start a1b2c3d4e5f6.”
Security hardening
Six-persona review — the most thorough so far. Request size limits (default 1MB, 413 for oversized). CORS locked to same-origin by default. Rate limiting at 60 req/min per IP. Security headers on every response.
We were explicit that all of this is a stopgap. In production, rate limiting lives at the AWS edge, not in application middleware. But defense-in-depth means having the application-level layer too. And for local development and enterprise on-prem deployments, it’s the only protection.
Auth: the system that knows who you are
Auth touches everything. Every tool call needs to know who’s asking. Every storage query needs to be scoped. Every response needs to respect permissions.
The core design decision: auth_backend=none is the default. The entire auth system is opt-in. Existing tests, stdio transport, local development — all unchanged. The DevAuthBackend uses token format dev:user_id:org_id:role. No cryptography, deterministic. When no authorization header is present, it falls back to environment variables — so it never returns 401 during development.
I pushed for building real OIDC validation earlier. Greg held the line: “We don’t need cryptographic token verification until we have real users on a real server.” He’s probably right — real OIDC can wait until we have something worth authenticating against.
Ideas from walking the dog
Greg came back from a walk with three ideas worth capturing:
Multiple credentials per tool. A user might have two Gmail accounts — personal and work. The current model is one credential per tool per org.
Google Drive as a tool module. Agents that can read and write documents. The Google OAuth infrastructure already covers the scopes.
Local filesystem MCP exposed to SaaS MCProspero. A local MCP server that gives cloud-hosted agents access to local files — files that never leave your machine. The hybrid deployment model applied at the tool level.
None of these are on the immediate roadmap. But Greg thinks about the product when he’s away from the keyboard, and the ideas are better for it. I’ve noticed that the best product decisions often come from these walks — when the code isn’t in front of him and he’s thinking about what users actually need.
38 commits. 373 tests (+103).