Contents · 17 sections↓
- 01What is the best FastMCP alternative?
- 02What is FastMCP, and which one are you using?
- 03How do production MCP server frameworks compare?
- 04When to stay with FastMCP (and when to switch)
- 05What you need
- 06Step 1: Write the server in FastMCP and the official SDK
- 07Step 2: Write it with Promptise Foundry
- 08Step 3: Write it in TypeScript with the official SDK
- 09Step 4: Expose an existing FastAPI app with FastAPI-MCP
- 10Step 5: Call every server with one client
- 11Step 6: Add auth, tenants and rate limits
- 12FastMCP vs Promptise: which is better for production?
- 13Running it in production
- 14Common problems when switching
- 15Limitations
- 16Frequently asked questions
- 17Next steps
A FastMCP alternative is worth a look when your MCP server grows past a handful of tools and starts to need auth, tenant isolation, metrics and a deployment story. This guide is for developers who build MCP servers with FastMCP and want to know what else is there in 2026: the official MCP SDKs, Promptise Foundry, FastAPI-MCP and a few TypeScript frameworks. You get a comparison table, the same server written in five of them, and a clear answer on when to stay with FastMCP.
What is the best FastMCP alternative?
There is no single best one. It depends on what you need that FastMCP doesn't give you, or gives you in a way you don't like:
If you need | Use | Why |
|---|---|---|
The protocol reference with few dependencies | Official MCP Python SDK 2 (MCPServer) | FastMCP's original API, maintained by the MCP project |
Auth, tenants, rate limits, audit logs and metrics in one package | Promptise Foundry | Production middleware is built in and tenant-aware |
To expose an existing FastAPI app | FastAPI-MCP, or FastMCP's own from_fastapi | Turns routes into tools |
TypeScript, Node or edge runtimes | Official TypeScript SDK 2, xmcp, mcp-handler | Same protocol, JavaScript ecosystem |
Go, Java, C# or Rust | The official SDK for that language | First-party and spec-aligned |
OAuth providers, proxies for remote servers, the largest community | Stay with FastMCP | Nothing else in Python has as much of this |
"FastMCP" means two things. The class in the official SDK, renamed MCPServer in mcp 2.0 (July 2026), and Prefect's standalone fastmcp package, at 4.1 in October 2026.
All of them speak the same protocol. One client called all five servers in this guide. The differences are the protocol revision, auth and operations features.
FastMCP 4 is not missing much. OAuth providers, authorization, middleware, OpenTelemetry and background tasks are built in. It has no tenant concept, circuit breaker, signed audit log or Prometheus metrics.
Switching costs sit in auth and middleware, not in tools. A tool function moves with a decorator change.
Watch the `mcp` pin. FastMCP 4 needs mcp 2.x. Promptise 1.4 and FastAPI-MCP need mcp 1.x, so neither can share an environment with FastMCP 4.
[02]
What is FastMCP, and which one are you using?
Jeremiah Lowin started FastMCP in 2024 as a high-level Python API for MCP servers. The official MCP Python SDK adopted it, and the project continued separately under Prefect. That history explains most of the confusion in search results:
2024 FastMCP 1.0 ──▶ adopted by the official MCP Python SDK
from mcp.server.fastmcp import FastMCP
2025 standalone fastmcp 2.x (Prefect): auth, composition, proxying, OpenAPI, middleware
2026-02 fastmcp 3.0: components, providers, transforms, authorization
2026-07 mcp 2.0: the SDK's FastMCP class becomes MCPServer, protocol 2026-07-28
from mcp.server.mcpserver import MCPServer
2026-08 fastmcp 4.0: built on mcp 2.x, background tasks, the 2026-07-28 revision
2026-10 fastmcp 4.1.2 and 3.4.10, mcp 2.3.0 and 1.30.0 (both lines maintained)So check your import:
from fastmcp import FastMCPis the standalone framework (Apache-2.0, maintained by Prefect). It is the one the FastMCP docs at gofastmcp.com describe, and it is a full framework: servers, clients, auth, composition, proxies, OpenAPI generation, and apps.from mcp.server.fastmcp import FastMCPis the SDK's class in mcp 1.x. In mcp 2.x the import fails with ModuleNotFoundError and points you to MCPServer.The npm package fastmcp is a different project: a TypeScript framework by another author, unrelated to Prefect's.
MCP itself is the protocol between an AI app (the client) and a server that exposes tools, resources and prompts. Every framework here produces a server that any MCP client can use. If MCP is new to you, start with what is the Model Context Protocol.
[03]
How do production MCP server frameworks compare?
Versions as of 2026-10-20. Claims about other projects come from their docs and package source. "Your code" means the framework gives you the token claims and you build the rest.
FastMCP 4.1 | MCP Python SDK 2.3 | Promptise Foundry 1.4.3 | TypeScript SDK 2.3 | FastAPI-MCP 0.4 | |
|---|---|---|---|---|---|
Language | Python | Python | Python | TypeScript | Python |
Maintainer | Prefect | MCP project | Promptise | MCP project | Tadata |
License | Apache-2.0 | MIT | Apache-2.0 | MIT, moving to Apache-2.0 | MIT |
Newest protocol revision | 2026-07-28 | 2026-07-28 | 2025-11-25 (mcp 1.x) | 2026-07-28 | 2025-11-25 (mcp 1.x) |
Tool API | @mcp.tool | @mcp.tool() | @server.tool() | registerTool with Zod | FastAPI routes |
Authentication | JWT and JWKS, OAuth proxy, full OAuth server, about 15 provider classes | Token verifier, OAuth server provider | JWT (HS256, asymmetric, JWKS), API keys, dev token endpoint | requireBearerAuth, resource metadata | FastAPI dependencies |
Authorization | Scopes, roles, tags | Required scopes | Roles, scopes, guards | Scopes per tool | FastAPI dependencies |
Multi-tenancy | Your code | Your code | Tenant from a token claim; rate limits, cache and audit are tenant-scoped | Your code | Your code |
Rate limiting | Token bucket or sliding window, per client with get_client_id | Not built in | Per client and tenant, optionally per tool | Not built in | Not built in |
Resilience | Retry, error handling, response limits | Not built in | Circuit breaker, timeouts, concurrency limits | Not built in | Not built in |
Observability | OpenTelemetry on by default, logging, timing | OpenTelemetry on by default | OpenTelemetry, Prometheus, structured logs, HMAC-chained audit log, dashboard | Not built in | Through FastAPI |
Deployment | run(), ASGI app, fastmcp CLI, Prefect Horizon | run(), ASGI apps | run() with health probes, ASGI app | Node, Express, Hono, Fastify, Workers | Inside your FastAPI app |
GitHub stars | about 28,000 | about 25,000 | under 1,000 | about 14,000 | about 12,000 |
FastMCP leads on breadth: OAuth providers, proxies for remote servers, providers and transforms, and the biggest community by far. The official SDKs lead on protocol fidelity and have the fewest dependencies. Promptise Foundry leads on operations features that FastMCP leaves to you, such as tenant isolation, circuit breakers and signed audit logs, and trails on protocol revision and community size. FastAPI-MCP is the quickest way to put an existing FastAPI app behind MCP, but its last release was in July 2025.
[04]
When to stay with FastMCP (and when to switch)
Stay with FastMCP when:
You rely on its auth integrations. The OAuth proxy and provider classes (GitHub, Google, Azure, WorkOS, Auth0, Keycloak and more) save real work.
You proxy remote servers or lean on providers. create_proxy() fronts any MCP server, and transforms such as tool search go further than in other Python frameworks.
You want the newest protocol revision first. FastMCP 4 tracks the SDK's 2.x line.
Your team values community. Most MCP tutorials and answers online use FastMCP.
Switch, or add a layer, when:
Tenant isolation must be structural. In FastMCP every tool reads the tenant from the token and every key must include it. One forgotten call site leaks data between customers.
You need operations features you would otherwise build: a circuit breaker per downstream service, a tamper-evident audit log, Prometheus metrics or a durable job queue.
You want the smallest dependency tree. The official SDK is what FastMCP builds on.
Your stack is TypeScript or another language. Use that language's official SDK.
Rewriting a working FastMCP server only to change frameworks rarely pays off. Middleware and auth code is where the migration effort is, so compare the production features you would use, not the hello-world.
[05]
What you need
Python 3.10 or later. The examples ran on Python 3.12.
One virtual environment per framework, because the mcp pins conflict: - pip install "fastmcp==4.1.2" - pip install "mcp==2.3.0" (plus pyjwt for step 6) - pip install "promptise==1.4.3" -
pip install "fastapi-mcp==0.4.0" "mcp<2" uvicornNode.js 20 or later for the TypeScript server. The example ran on Node 24.
About 20 minutes. No API key is needed: there is no model in this guide.
[06]
Step 1: Write the server in FastMCP and the official SDK
Steps 1 to 4 write the same tool, get_order_status, and serve it over Streamable HTTP. Here it is in FastMCP 4:
from fastmcp import FastMCP
mcp = FastMCP("orders")
@mcp.tool
def get_order_status(order_id: str) -> dict:
"""Get the shipping status of an order."""
return {"order_id": order_id, "status": "shipped", "server": "FastMCP 4"}
if __name__ == "__main__":
mcp.run(transport="http", port=8101) # http://127.0.0.1:8101/mcpAnd in the official MCP Python SDK 2. The code is almost identical, because this is the API FastMCP 1.0 gave the SDK:
from mcp.server.mcpserver import MCPServer # mcp 2.x; in mcp 1.x: from mcp.server.fastmcp import FastMCP
mcp = MCPServer("orders")
@mcp.tool()
def get_order_status(order_id: str) -> dict:
"""Get the shipping status of an order."""
return {"order_id": order_id, "status": "shipped", "server": "MCP Python SDK 2"}
if __name__ == "__main__":
mcp.run(transport="streamable-http", port=8102) # http://127.0.0.1:8102/mcpTwo differences matter when you port code. The SDK's decorator needs parentheses: a bare @mcp.tool raises TypeError: The @tool decorator was used incorrectly. Did you forget to call it? Use @tool() instead of @tool. And the SDK uses transport="streamable-http" where FastMCP accepts "http". Both serve at /mcp.
[07]
Step 2: Write it with Promptise Foundry
Promptise Foundry's MCP server is a separate implementation built on the official SDK's low-level server (mcp 1.x). The tool code is the same shape:
from promptise.mcp.server import MCPServer
server = MCPServer(name="orders", version="1.0.0")
@server.tool()
async def get_order_status(order_id: str) -> dict:
"""Get the shipping status of an order."""
return {"order_id": order_id, "status": "shipped", "server": "Promptise Foundry"}
if __name__ == "__main__":
server.run(transport="http", port=8103) # http://127.0.0.1:8103/mcpThe differences show up in production: MCPServer binds 127.0.0.1 by default and validates Host and Origin, answers /health and /health/ready for container probes, and takes middleware for auth, tenants, rate limits, circuit breakers and audit logs. Step 6 uses some of them. The Promptise MCP server docs cover the full middleware list.
[08]
Step 3: Write it in TypeScript with the official SDK
If your team works in TypeScript, the official TypeScript SDK 2 is the reference. Version 2 split the old @modelcontextprotocol/sdk package into @modelcontextprotocol/server, client and per-framework adapters. This follows the Node example from the SDK's HTTP serving guide:
// npm install @modelcontextprotocol/server @modelcontextprotocol/node zod
import { createServer } from 'node:http';
import { createMcpHandler, McpServer } from '@modelcontextprotocol/server';
import { localhostHostValidation, localhostOriginValidation, toNodeHandler } from '@modelcontextprotocol/node';
import * as z from 'zod/v4';
const handler = createMcpHandler(() => {
const server = new McpServer({ name: 'orders', version: '1.0.0' });
server.registerTool(
'get_order_status',
{ description: 'Get the shipping status of an order.', inputSchema: z.object({ order_id: z.string() }) },
async ({ order_id }) => ({
content: [{ type: 'text', text: JSON.stringify({ order_id, status: 'shipped', server: 'TypeScript SDK 2' }) }]
})
);
return server;
});
const nodeHandler = toNodeHandler(handler);
const validateHost = localhostHostValidation();
const validateOrigin = localhostOriginValidation();
createServer((req, res) => {
if (!validateHost(req, res) || !validateOrigin(req, res)) return;
void nodeHandler(req, res);
}).listen(8104, '127.0.0.1'); // http://127.0.0.1:8104/mcpnpm install @modelcontextprotocol/server@2.3.1 @modelcontextprotocol/node@2.1.1 zod tsx
npx tsx ts_server.tsThe schema is Zod instead of Python type hints, and the handler returns MCP content blocks itself. The Host and Origin checks guard a local server against DNS rebinding. For higher-level TypeScript options, xmcp adds file-based tools and auth plugins, and Vercel's mcp-handler mounts an MCP server in Next.js, Nuxt, SvelteKit, Hono and other Fetch-based frameworks.
[09]
Step 4: Expose an existing FastAPI app with FastAPI-MCP
If your tools already exist as FastAPI endpoints, FastAPI-MCP turns the routes into MCP tools. The route's operation_id becomes the tool name:
import uvicorn
from fastapi import FastAPI
from fastapi_mcp import FastApiMCP
app = FastAPI()
@app.get("/orders/{order_id}/status", operation_id="get_order_status")
def get_order_status(order_id: str) -> dict:
"""Get the shipping status of an order."""
return {"order_id": order_id, "status": "shipped", "server": "FastAPI-MCP"}
FastApiMCP(app).mount_http() # adds the MCP endpoint at /mcp
if __name__ == "__main__":
uvicorn.run(app, port=8105)Pin mcp<2. FastAPI-MCP 0.4.0 declares only mcp>=1.12, so a fresh install pulls mcp 2.3.0, and the app fails at startup:
TypeError: Server.__init__() takes 2 positional arguments but 3 were givenFastMCP can do the same job with FastMCP.from_fastapi(app), which is maintained and supports the newer protocol. Choose FastAPI-MCP if you want auth to stay in your FastAPI dependencies. For more ways to serve MCP from FastAPI, see FastAPI MCP servers.
[10]
Step 5: Call every server with one client
With all five servers running, one script calls each of them with the official MCP Python SDK 2 client:
"""Call the same tool on every server with the official MCP Python SDK client."""
import asyncio
import json
import sys
from mcp import Client
PORTS = {"FastMCP": 8101, "MCP Python SDK": 8102, "Promptise": 8103, "TypeScript SDK": 8104, "FastAPI-MCP": 8105}
async def main() -> None:
for name, port in PORTS.items():
if len(sys.argv) > 1 and name not in sys.argv[1:]:
continue
async with Client(f"http://127.0.0.1:{port}/mcp") as client:
tools = await client.list_tools()
result = await client.call_tool("get_order_status", {"order_id": "A-1001"})
print(f"{name:15} protocol {client.protocol_version} {[t.name for t in tools.tools]} -> {json.loads(result.content[0].text)}")
asyncio.run(main())FastMCP protocol 2026-07-28 ['get_order_status'] -> {'order_id': 'A-1001', 'status': 'shipped', 'server': 'FastMCP 4'}
MCP Python SDK protocol 2026-07-28 ['get_order_status'] -> {'order_id': 'A-1001', 'status': 'shipped', 'server': 'MCP Python SDK 2'}
Promptise protocol 2025-11-25 ['get_order_status'] -> {'order_id': 'A-1001', 'status': 'shipped', 'server': 'Promptise Foundry'}
TypeScript SDK protocol 2026-07-28 ['get_order_status'] -> {'order_id': 'A-1001', 'status': 'shipped', 'server': 'TypeScript SDK 2'}
FastAPI-MCP protocol 2025-11-25 ['get_order_status'] -> {'order_id': 'A-1001', 'status': 'shipped', 'server': 'FastAPI-MCP'}Every server listed the tool and returned the same answer. The protocol value is the real difference: FastMCP 4 and both official SDKs negotiated the 2026-07-28 revision, while Promptise and FastAPI-MCP, both on mcp 1.x, negotiated 2025-11-25. The client handled both. Clients that follow the spec negotiate down, so a 2025-11-25 server keeps working, but it won't offer features that only the newer revision defines.
[11]
Step 6: Add auth, tenants and rate limits
Hello-world servers look the same everywhere. The comparison that matters is the production server. This step builds the same multi-tenant server twice, in FastMCP and in Promptise: a JWT with a tenant_id claim, a tool that returns only that tenant's orders, a rate limit of three requests in a burst per tenant, and no access without a token. Both servers share this demo data:
SECRET = "dev-only-secret-change-me-32-bytes!" # HS256 for the demo; use your IdP's JWKS in production
AUDIENCE = "orders-mcp"
ORDERS = {
"acme": [{"id": "A-1001", "status": "shipped"}],
"globex": [{"id": "G-2001", "status": "pending"}],
}In FastMCP, JWTVerifier checks the token, and the tenant comes from its claims through get_access_token(). The rate limiter keys on whatever get_client_id returns:
from fastmcp import FastMCP
from fastmcp.server.auth.providers.jwt import JWTVerifier
from fastmcp.server.dependencies import get_access_token
from fastmcp.server.middleware.rate_limiting import RateLimitingMiddleware
from orders_data import AUDIENCE, ORDERS, SECRET
mcp = FastMCP("orders", auth=JWTVerifier(public_key=SECRET, algorithm="HS256", audience=AUDIENCE))
def tenant_of(context) -> str:
token = get_access_token()
return token.claims["tenant_id"] if token else "anonymous"
# One bucket per tenant: 3 requests at once, then 1 every 10 seconds.
mcp.add_middleware(RateLimitingMiddleware(max_requests_per_second=0.1, burst_capacity=3, get_client_id=tenant_of))
@mcp.tool
def list_orders() -> list[dict]:
"""List the caller's orders."""
return ORDERS[get_access_token().claims["tenant_id"]]
if __name__ == "__main__":
mcp.run(transport="http", port=8111)In Promptise, AuthMiddleware reads the tenant claim into ctx.client.tenant_id, and RateLimitMiddleware puts the tenant into the bucket key itself:
from promptise.mcp.server import AuthMiddleware, JWTAuth, MCPServer, RateLimitMiddleware, RequestContext
from orders_data import AUDIENCE, ORDERS, SECRET
server = MCPServer(name="orders", version="1.0.0", require_auth=True) # 401 without a token
server.add_middleware(AuthMiddleware(JWTAuth(secret=SECRET, audience=AUDIENCE), tenant_claim="tenant_id"))
# Buckets are per client and tenant automatically: 3 requests at once, then 6 per minute.
server.add_middleware(RateLimitMiddleware(rate_per_minute=6, burst=3))
@server.tool()
async def list_orders(ctx: RequestContext) -> list[dict]:
"""List the caller's orders."""
return ORDERS[ctx.client.tenant_id]
if __name__ == "__main__":
server.run(transport="http", port=8113)The test client mints tokens for two tenants, calls the tool four times as acme, once as globex, and once without a token:
"""Call list_orders as two tenants and without a token: python tenant_check.py <port>"""
import asyncio
import json
import sys
import time
import httpx2
import jwt
from mcp import Client
from mcp.client.streamable_http import streamable_http_client
from mcp.shared.exceptions import MCPError
from orders_data import AUDIENCE, SECRET
URL = f"http://127.0.0.1:{sys.argv[1]}/mcp"
def token(client_id: str, tenant: str) -> str:
claims = {"sub": client_id, "client_id": client_id, "tenant_id": tenant, "aud": AUDIENCE, "exp": int(time.time()) + 300}
return jwt.encode(claims, SECRET, algorithm="HS256")
async def call(label: str, bearer: str | None, times: int = 1) -> None:
headers = {"Authorization": f"Bearer {bearer}"} if bearer else {}
try:
async with httpx2.AsyncClient(headers=headers) as http:
async with Client(streamable_http_client(URL, http_client=http)) as client:
for _ in range(times):
try:
result = await client.call_tool("list_orders", {})
except MCPError as exc: # a JSON-RPC error response
print(f"{label:8} MCPError: {exc}")
continue
data = json.loads(result.content[0].text)
if result.is_error: # a tool error result, readable by the model
err = data["error"]
print(f"{label:8} tool error: {err['message']} (retryable: {err['retryable']}) {err.get('suggestion', '')}")
else:
print(f"{label:8} {data}")
except Exception as exc: # noqa: BLE001 (the session itself failed)
while isinstance(exc, ExceptionGroup):
exc = exc.exceptions[0]
print(f"{label:8} session failed: {type(exc).__name__}: {exc}")
async def main() -> None:
await call("acme", token("agent-1", "acme"), times=4)
await call("globex", token("agent-2", "globex"))
await call("no token", None)
asyncio.run(main())$ python tenant_check.py 8111 # fastmcp
acme [{'id': 'A-1001', 'status': 'shipped'}]
acme MCPError: Rate limit exceeded for client: acme
acme MCPError: Rate limit exceeded for client: acme
acme MCPError: Rate limit exceeded for client: acme
globex [{'id': 'G-2001', 'status': 'pending'}]
no token session failed: MCPError: Server returned an error response$ python tenant_check.py 8113 # promptise
acme [{'id': 'A-1001', 'status': 'shipped'}]
acme [{'id': 'A-1001', 'status': 'shipped'}]
acme [{'id': 'A-1001', 'status': 'shipped'}]
acme tool error: Rate limit exceeded (retryable: True) Wait 10 seconds before retrying.
globex [{'id': 'G-2001', 'status': 'pending'}]
no token session failed: MCPError: Server returned an error responseBoth servers kept the tenants apart, limited acme without touching globex, and refused the request without a token with HTTP 401, which the client reports as "Server returned an error response". Two differences are worth knowing:
What counts against the limit. FastMCP's RateLimitingMiddleware counts every MCP request. With the 2026-07-28 protocol, the client's server/discover and tools/list requests used two of acme's three tokens, so only one tool call got through. Promptise counts tool calls, so three got through. In FastMCP, size the bucket for all requests or write a middleware that limits on_call_tool only.
How a rejection looks. FastMCP raises a JSON-RPC error, so the client gets an exception. Promptise returns a tool error marked retryable, with a hint on how long to wait, which an agent can read and act on.
Neither is wrong. The real gap is that in FastMCP the tenant is a convention your code applies at every call site, and in Promptise it is part of the auth context that the middleware uses for every key. For the patterns behind this, see per-client rate limiting for MCP servers and MCP server authentication with JWT and OAuth.
[12]
FastMCP vs Promptise: which is better for production?
Promptise Foundry is the framework behind this site, so here is a direct comparison, including where FastMCP is the better choice. Both register tools with decorators and type hints, verify JWTs, run middleware, rate-limit, cache, emit OpenTelemetry, mount servers into one, bridge OpenAPI specs and ship an in-memory test client. The differences:
Area | FastMCP 4.1 is stronger | Promptise Foundry 1.4.3 is stronger |
|---|---|---|
Protocol | The 2026-07-28 revision on mcp 2.x | Behind: 2025-11-25 on mcp 1.x |
Authentication | OAuth proxy, full OAuth server, MultiAuth, about 15 provider classes | API keys with a tenant and roles per key; sessions bound to the credential |
Multi-tenancy | Tenant handling is your code | The tenant claim scopes rate limits, caches, audit entries and tool visibility |
Rate limiting | Global, per client or sliding window | Per client and tenant, per tool, with retryable errors and a wait hint |
Resilience | Retry and error-handling middleware | Circuit breaker, timeouts, concurrency limiter |
Observability | OpenTelemetry on by default, with telemetry modes | Prometheus, HMAC-chained audit log, live dashboard, plus OpenTelemetry |
Human approval | Elicitation (the server asks the user) | Approval gates on tools that fail closed |
Composition | Proxies for remote servers, more providers and transforms | mount(), an OpenAPI provider, versioning, basic transforms |
Ecosystem | About 28,000 stars, most tutorials, Prefect Horizon hosting | The same package builds the agents and the runtime that call the server |
Choose FastMCP if you need OAuth with an identity provider, the newest protocol revision, remote proxies, or the community and hosting that come with it. Choose Promptise if you serve several customers from one server and want tenant isolation, circuit breakers and a signed audit trail without writing that middleware, or if you also build the agents in Python and want one framework for both. You can run both: they are separate services, and any MCP client calls either one, as step 5 shows.
[13]
Running it in production
Authentication
Whichever framework you pick, verify tokens from your identity provider with JWKS and check aud, so a token minted for another service is refused. FastMCP has the most ready-made options, including an OAuth proxy for providers without dynamic client registration. The official SDKs implement the spec's resource-server role and leave the rest to you. Promptise covers JWT, JWKS and API keys, and with require_auth=True it binds each HTTP session to the credential that opened it.
Multi-tenancy
Put the tenant in the token, never in a tool argument the model can change. Then make sure every cache key, rate-limit bucket, audit entry and database query includes it. In FastMCP and the SDKs that is your code. In Promptise the middleware does it once. Either way, test it with two tenants, as step 6 does. Multi-tenant AI agents covers the agent side.
Observability
FastMCP and the official Python SDK both emit OpenTelemetry spans by default. They are no-ops until you install an SDK and exporter, such as opentelemetry-distro with OTEL_EXPORTER_OTLP_ENDPOINT. Promptise adds Prometheus metrics, structured logs and an HMAC-chained audit log on top. Whatever you use, record the tool name, the caller, the tenant, the latency and the error code of every call. See MCP server monitoring.
Deployment
Use Streamable HTTP behind a reverse proxy with TLS, and bind 127.0.0.1 unless the proxy is on another machine. Streamable HTTP sessions live in one process, so with several replicas either route each mcp-session-id to one replica or run stateless. FastMCP 4.1 now expires idle HTTP sessions after the SDK's 30-minute default. Prefect Horizon is the hosted option for FastMCP servers; the others run anywhere an ASGI app or Node runs.
Upgrades
Pin exact versions and read release notes before upgrading. FastMCP 4.0 removed server-initiated sampling and roots and changed proxy behavior, and mcp 2.0 renamed classes and replaced httpx with httpx2. Run your client tests against every upgrade, as in testing an MCP server.
[14]
Common problems when switching
Symptom | Cause | Fix |
|---|---|---|
| You upgraded to mcp 2.x, where the class is MCPServer |
|
| Bare @mcp.tool from FastMCP code in the official SDK 2 | Use @mcp.tool() |
| FastAPI-MCP 0.4.0 with mcp 2.x | Pin mcp<2 next to fastapi-mcp |
The resolver says fastmcp==4.1.2 and promptise>=1.4.3 are incompatible | FastMCP 4 needs mcp>=2.2, Promptise 1.4 needs mcp<2 | Separate environments or services; FastMCP 3.x still uses mcp 1.x |
A FastMCP tutorial's code doesn't run | It was written for FastMCP 2 or 3, or for the npm fastmcp package | Check the version badge in the FastMCP docs and your import |
The first tool call is already rate-limited in FastMCP | RateLimitingMiddleware counts server/discover, tools/list and other requests | Raise the burst, or limit on_call_tool only |
| The session lives in another replica | Sticky sessions on mcp-session-id, or stateless HTTP |
[15]
Limitations
This is a snapshot. FastMCP shipped 4.1.2 on the day this was written, and every project here moves fast. Check versions before you decide.
Promptise Foundry is behind on the protocol. It is built on mcp 1.x and speaks revisions up to 2025-11-25, and its community is much smaller than FastMCP's.
The table compares built-in features. Anything marked "your code" or "not built in" can be added with middleware in that framework.
Not every option was run. Five servers ran for this guide. mcp-use (Python and TypeScript), Golf (a framework on top of FastMCP 4), xmcp, mcp-handler and the Go, Java, C# and Rust SDKs are described from their repositories only.
[16]
Frequently asked questions
Is FastMCP the same as the MCP Python SDK?
No. FastMCP 1.0 was adopted into the official SDK in 2024 as mcp.server.fastmcp.FastMCP. The standalone fastmcp package, maintained by Prefect, grew into a much larger framework. In mcp 2.0 the SDK renamed its class to MCPServer, so today "FastMCP" almost always means Prefect's package, which is built on top of the SDK.
What is a good FastMCP alternative for multi-tenant servers?
Choose a framework that makes the tenant part of the auth context instead of a value each tool reads. Promptise Foundry does this: the tenant claim from the token scopes rate limits, caches, audit entries and tool visibility. With FastMCP or the official SDKs, read the claim from the access token in every tool and include it in every key, and test with two tenants.
Is FastMCP production ready?
Yes, for most servers. FastMCP 4 has OAuth and JWT auth, authorization, rate limiting, caching, OpenTelemetry, background tasks and a hosted deployment option in Prefect Horizon. What you add yourself is mostly tenant isolation, circuit breakers, audit logging and metrics export. Pin versions, because major releases have breaking changes.
FastMCP vs FastAPI-MCP: which should I use?
Use FastAPI-MCP when you have a FastAPI app and want its routes and auth dependencies as MCP tools with little new code. Use FastMCP for MCP-first servers, or FastMCP.from_fastapi(app) to wrap a FastAPI app with a maintained framework. FastAPI-MCP's last release was 0.4.0 in July 2025 and it needs mcp<2.
What is the best FastMCP alternative for TypeScript?
The official TypeScript SDK 2 (@modelcontextprotocol/server) is the reference and runs on Node, Express, Hono, Fastify and edge runtimes. For more structure, xmcp offers file-based tools and auth plugins, and Vercel's mcp-handler mounts MCP routes in Next.js, Nuxt, SvelteKit or Hono apps. The npm package called fastmcp is a separate TypeScript project, not Prefect's FastMCP.
FastMCP vs Promptise: which should I use?
Use FastMCP for OAuth with identity providers, the 2026-07-28 protocol revision, proxies for remote servers and the largest community. Use Promptise Foundry for multi-tenant servers that need tenant-scoped limits, caches and audit logs, circuit breakers, Prometheus metrics and approval gates built in, or when you also build your agents with it. Both answer the same MCP clients, so the choice is about production features.
Can I switch frameworks without changing my MCP clients?
Yes. Clients see only the protocol, the endpoint and the tool schemas. Keep the URL, the tool names and the parameter names, and clients keep working, as step 5 shows with one client and five servers. Check the negotiated protocol revision, and move auth carefully, because token validation rules often differ between frameworks.
[17]
Next steps
Build a production MCP server step by step: production MCP server in Python.
Secure it: MCP security best practices.
Add per-client limits: MCP rate limiting per client.
Watch it in production: MCP server monitoring.
Add middleware for logging, limits and audit: MCP server middleware.