Best Python MCP Server Framework for Production¶
Shipping a production MCP server is a different job from getting a demo tool to respond once in your terminal. The prototype works because nothing is trying to break it: no untrusted callers, no bursty traffic, no flaky downstream API, no auditor asking who invoked the refund tool at 2 a.m. Cross that line and you inherit a checklist — authentication, rate limiting, failure isolation, tamper-evident logging, multi-tenancy, and health probes your orchestrator can read. This guide lays out what that checklist actually contains, shows how Promptise Foundry ships every item as one composable middleware chain, and is honest about when the raw SDK alone is all you need.
Not legal or compliance advice
The information here is general technical information, not legal, regulatory, or compliance advice. Descriptions of any law, regulation, or standard (such as the GDPR, the EU AI Act, HIPAA, SOC 2, or PCI DSS) are simplified and may be incomplete, out of date, or inaccurate, and requirements vary by jurisdiction and situation. Promptise Foundry makes no warranty as to the accuracy or completeness of this content and is not responsible for how you use or rely on it. Using Promptise does not by itself make you or your product compliant with any law or standard. Consult a qualified lawyer or compliance professional before acting on anything here.
The gap between a demo and a production MCP server¶
The Model Context Protocol standardizes how agents discover and call your tools. If you're new to the protocol itself, the What Is MCP? Model Context Protocol Explained primer covers the client/server model before you worry about hardening it. A single-file server that returns weather for a city is a fine place to start — that's exactly what the How to Build an MCP Server in Python (Tutorial) walkthrough builds.
The gap opens the moment that server faces real traffic. A demo tool trusts its caller. A production tool cannot. The concerns that appear are not exotic; they are the same ones every network service has faced for decades, now applied to tool calls an autonomous agent makes on your behalf:
- Identity — who is calling, and are they allowed to call this tool?
- Load — one client (or one runaway agent loop) must not exhaust capacity for everyone else.
- Failure — when a downstream dependency dies, calls should fail fast instead of piling up.
- Accountability — every sensitive call needs a record you can trust wasn't edited after the fact.
- Isolation — one tenant's traffic, quotas, and data stay separate from another's.
- Operability — Kubernetes needs to know when your server is alive and when it's ready.
Wiring these by hand is where most homegrown servers stall. A good Python MCP framework should give you all of them as configuration, not as a rewrite.
The MCP server production checklist¶
Before you pick a framework, score your current prototype against a concrete list. Here's the checklist Promptise is built to satisfy, and the piece that covers each item:
| Requirement | What it means | Promptise piece |
|---|---|---|
| Authentication | Verify a JWT or API key on every request | AuthMiddleware + JWTAuth / AsymmetricJWTAuth / APIKeyAuth |
| Authorization | Per-tool role and scope checks | @server.tool(auth=True, roles=[...]), HasRole, RequireAuth guards |
| Rate limiting | Per-client and per-tool token buckets | RateLimitMiddleware, declared rate_limit="100/min" |
| Failure isolation | Trip a breaker after repeated errors | CircuitBreakerMiddleware |
| Audit trail | Tamper-evident record of every call | AuditMiddleware (HMAC-chained) |
| Concurrency control | Cap in-flight work per tool or server | ConcurrencyLimiter, PerToolConcurrencyLimiter |
| Multi-tenancy | Tenant identity as a first-class invariant | MCPServer(require_tenant=True), tenant_claim |
| Health probes | Liveness/readiness for K8s | HealthCheck |
If your prototype already covers all eight, you may not need a framework at all — see the honesty note below. If it covers two, that's the gap this framework closes.
One composable middleware chain¶
Here's the feature that ties the checklist together: a single, ordered middleware chain that runs on every tool call, pre-compiled at build time so it adds no per-request wiring overhead. You compose it the way you'd compose ASGI or Express middleware — each layer wraps the next, in the order you add it.
The example below stands up an enterprise MCP server for a billing team: authenticated, tenant-scoped, rate-limited, breaker-protected, and audited. Every one of these APIs is real.
# server.py — a production-shaped MCP server
import os
from promptise.mcp.server import (
MCPServer,
AuthMiddleware, JWTAuth,
RateLimitMiddleware,
CircuitBreakerMiddleware,
AuditMiddleware,
)
# require_tenant makes tenant identity a server-wide invariant:
# every tool refuses to build unless a tenant can be resolved.
server = MCPServer("billing-tools", require_tenant=True)
# The chain runs top-to-bottom on every call.
server.add_middleware(
AuthMiddleware(JWTAuth(secret=os.environ["JWT_SECRET"]), tenant_claim="org")
)
server.add_middleware(RateLimitMiddleware(rate_per_minute=100, per_tool=True))
server.add_middleware(CircuitBreakerMiddleware(failure_threshold=5, recovery_timeout=60.0))
server.add_middleware(AuditMiddleware(log_path="audit.jsonl")) # HMAC-chained
@server.tool(auth=True, roles=["billing"], rate_limit="30/min")
async def issue_refund(order_id: str, amount: float) -> dict:
"""Issue a refund for an order."""
return {"order_id": order_id, "refunded": amount, "status": "ok"}
if __name__ == "__main__":
server.run(transport="http", port=8080)
A few details make this production-grade rather than production-shaped:
- Auth first.
AuthMiddlewareextracts and verifies the JWT, then populates the client context that every layer below reads. Thetenant_claim="org"argument pulls the tenant from the token'sorgclaim. - Rate buckets are tenant-aware. With
per_tool=True, each client gets its own token bucket per tool, and buckets never span tenants — one tenant's traffic can't exhaust another's quota. - Declared limits coexist with the policy. The server-wide
RateLimitMiddlewaresets a floor; therate_limit="30/min"onissue_refundenforces that specific tool's contract on top. The declared limit is wired in automatically — you don't add a second middleware for it. - The breaker isolates failures per tool. After five consecutive failures,
issue_refundfails fast for 60 seconds instead of hammering a dead dependency, then lets one probe through to test recovery. - The audit log is tamper-evident.
AuditMiddlewarewrites one JSON line per call with an HMAC chain, so any edit or deletion breaks the chain and is detectable. SetPROMPTISE_AUDIT_SECRETin production so the chain survives restarts.
You can test this whole pipeline in-process with TestClient(server).call_tool(...) — no network, no live model — which is what makes hardening testable in CI. For the full menu of layers (structured logging, timeouts, webhooks, per-tool concurrency), the advanced patterns reference documents each middleware and the order they compose in, and the auth and security page covers JWT, asymmetric keys, API keys, and the guard system in depth.
Multi-tenancy and Kubernetes health probes without the plumbing¶
Two checklist items usually turn into the most bespoke code: tenancy and health.
Tenancy is easy to get subtly wrong. If tenant scoping is something each tool remembers to do, one forgotten WHERE tenant_id = ... leaks data across customers. Promptise makes it a server invariant instead. MCPServer(require_tenant=True) refuses to build if any tool could run without a resolved tenant, tenant_claim tells the auth layer where to find it, and the tenant then flows automatically into rate-limit keys and audit entries. You can tighten individual tools further with the RequireTenant and HasTenant guards.
Health probes are what let an orchestrator run your server safely. HealthCheck exposes liveness and readiness, and you register readiness checks for the dependencies that actually gate traffic:
from promptise.mcp.server import HealthCheck
health = HealthCheck()
health.add_check("database", db_pool.is_connected, required_for_ready=True)
health.add_check("cache", redis.ping, required_for_ready=False)
Liveness answers "is the process up?"; readiness answers "should Kubernetes send it traffic yet?" A failing required check makes readiness report not_ready, so the orchestrator holds traffic until your database is actually reachable. For the end-to-end deployment story — the full chain, tenancy, health probes, and a Kubernetes manifest — the production MCP servers guide walks through a complete enterprise MCP server build.
When the raw SDK — or another tool — is the better fit¶
Honesty matters more than a sales pitch here, so: you do not always need this. If you're exposing a single internal tool on a trusted network — a script your own agent calls, behind a VPN, with no untrusted callers and no compliance requirement — the bare MCPServer with @server.tool() and nothing else is the right amount of framework. Adding auth, breakers, and audit to a two-person internal utility is overhead you'll resent. Start minimal; add layers when a real requirement appears, not before.
Other frameworks are also a better fit in specific cases. If your team is already deeply invested in a different agent ecosystem and your MCP server is a thin adapter in front of it, staying native to that stack may beat introducing a second one. And if you need a language other than Python, this framework simply isn't for you — the MCP spec is polyglot, and a TypeScript or Go server is the correct call there.
Where Promptise earns its place is the middle-to-large case: a server that faces untrusted or multi-tenant traffic, needs an auditable trail, and must survive downstream failures — where you'd otherwise hand-roll every checklist item. The value is that all of it composes from one chain instead of six libraries you glue together yourself.
Frequently asked questions¶
What makes an MCP server "production-ready"?¶
A production MCP server authenticates every caller, authorizes each tool per role or scope, rate-limits per client and per tool, isolates downstream failures with a circuit breaker, records a tamper-evident audit trail, and exposes health probes an orchestrator can read. A prototype skips all of these because it trusts its caller; production can't. Score your server against the eight-item checklist above to find the gaps.
Do I need a framework, or can I use the raw MCP SDK?¶
For a single internal tool on a trusted network, the raw SDK is enough and a framework is overhead. Reach for a framework once you have untrusted callers, multiple tenants, compliance requirements, or unreliable dependencies — that's when hand-wiring auth, rate limiting, and audit stops being worth it. Promptise gives you those as a composable middleware chain you opt into layer by layer.
How does the middleware chain affect performance?¶
The chain is pre-compiled when the server builds, so composing ten layers doesn't add ten layers of per-request indirection. Each middleware does real work only when it must — the rate limiter checks a token bucket, the breaker checks a counter — and you only pay for the layers you add. Start with auth and audit, and add concurrency limits or breakers when load and failure modes justify them.
Next steps¶
Score your prototype against the production checklist above, then pip install promptise to close the gaps — one middleware layer at a time, not a rewrite. Start with the Quick Start to get a server running, then follow the production MCP servers guide to add auth, tenancy, health probes, and the full middleware chain to your build.