Skip to main content
Lesson 12 of 14 · MCP for Developers: Servers and Integrations

Deploy an MCP Server to Production

Lesson 12 of 14 Updated November 2, 2026 Developer
On this page
  1. From prototype to production server
  2. Option 1: Hugging Face Spaces
  3. Option 2: Docker
  4. Options 3 and 4: Managed deployments
  5. Securing the deployment
  6. Seeing what’s happening
  7. Key takeaways

Update November 2, 2026 — Le Chat is now Vibe. As of May 28, 2026, Le Chat has been rebranded as Vibe (still at chat.mistral.ai): a single agent for work and code, carrying over your conversations, settings, and subscriptions. The legacy Le Chat experience remains accessible in Chat mode; in Work mode, agents are now skills (invoked with @), memories form the knowledge base, and deep research is the Deep Research skill. Plans are now called Mistral Free, Pro, Team, and Enterprise. This lesson uses the name Vibe; some menus described here may have changed names or locations.

Update November 2, 2026 — MCP Python SDK v2. This course now follows version 2 of the mcp SDK (2.3.0 as of October 2, 2026): MCPServer replaces FastMCP, transport settings move to run() or streamable_http_app(), and an app exposed under a real hostname must declare that hostname (transport_security). This is also the version required by the Mistral SDK: the mistralai[agents] extra requires mcp>=2.2.0,<3.0. Sources: What’s new in v2 · Migration Guide · Deploy & scale.

From prototype to production server

Your server runs locally, and ngrok exposes it to the world just for a demo. The problem starts when someone else needs to use it: you close your terminal, the URL dies, and every connector your colleagues configured breaks. Going to production essentially means getting a stable address, available 24/7, served over HTTPS. Four options get you there, ranging from the quickest to the most controlled. Your choice depends mainly on how sensitive your data is and how much control you want over the infrastructure.

Option 1: Hugging Face Spaces

Hugging Face Spaces provides free virtual machines without GPUs, which are more than enough for an MCP server that relays API calls. This is the fastest path to a real deployment: three files and a git push.

mon-mcp-space/
├── app.py              # FastAPI + MCP server
├── requirements.txt    # Dependencies
└── README.md           # Space metadata

The app.py file follows the exact structure you’ve seen so far, except that sensitive values are read from the environment—HF Secrets inject them there:

# app.py
"""MCP server deployed on Hugging Face Spaces."""

import os
from contextlib import asynccontextmanager

from fastapi import FastAPI
from mcp.server.mcpserver import MCPServer
from mcp.server.transport_security import TransportSecuritySettings

mcp = MCPServer("My Production Server")

@mcp.tool()
def hello(name: str) -> str:
    """Greet a person.

    Args:
        name: The person's first name

    Returns:
        Greeting message
    """
    return f"Hello {name} from Hugging Face Spaces!"

@asynccontextmanager
async def lifespan(app: FastAPI):
    async with mcp.session_manager.run():
        yield

# Space hostname, in the form <user>-<space>.hf.space
SPACE_HOST = os.environ.get("MCP_PUBLIC_HOST", "localhost")
security = TransportSecuritySettings(allowed_hosts=[SPACE_HOST, f"{SPACE_HOST}:*"])

api = FastAPI(lifespan=lifespan)
api.mount("/mcp-endpoint", mcp.streamable_http_app(transport_security=security))

# For sensitive variables, use HF Secrets
API_KEY = os.environ.get("MY_API_KEY", "")

The platform’s quirk lies in the README, whose YAML header drives the Space build. The app_port field must match the port your server actually listens on; otherwise, the deployment succeeds but remains inaccessible:

---
title: My MCP Server
emoji: 🔧
colorFrom: blue
colorTo: green
sdk: docker
app_port: 7860
---
# Create a Space on huggingface.co
# Push the code
git push
# The Space deploys automatically

Your endpoint then becomes https://your-username-mon-mcp-space.hf.space/mcp-endpoint/mcp, served over HTTPS. One configuration step remains: declare this hostname, your-username-mon-mcp-space.hf.space, in the MCP_PUBLIC_HOST variable in the Space settings. Without it, the SDK rejects every request with a 421 Invalid Host header error.

Option 2: Docker

As soon as the server touches internal data, hosting on your own infrastructure becomes the rule, and Docker is the standard format. The Dockerfile takes just a few instructions, with dependency installation placed before copying the code to leverage layer caching during rebuilds:

FROM python:3.12-slim

WORKDIR /app

# Install dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copy code
COPY . .

# Expose port
EXPOSE 8000

# Start server
CMD ["uvicorn", "server:api", "--host", "0.0.0.0", "--port", "8000"]
fastapi>=0.115.0
uvicorn>=0.30.0
mcp>=2.2,<3
httpx>=0.27.0   # only if your tools use it: SDK v2 no longer installs it

At launch, the --restart unless-stopped option is what distinguishes a production deployment from a simple test: without it, the first crash takes your server offline until someone notices.

# Build the image
docker build -t mon-mcp-server .

# Run the container
docker run -d \
  --name mcp-server \
  -p 8000:8000 \
  -e MY_API_KEY="secret" \
  -e DATABASE_URL="postgresql://..." \
  --restart unless-stopped \
  mon-mcp-server

Docker Compose makes this configuration reproducible and adds a health check, which the orchestrator uses to detect a frozen server—a live process that no longer responds is a scenario that simple automatic restarts don’t cover:

# docker-compose.yml
version: "3.8"
services:
  mcp-server:
    build: .
    ports:
      - "8000:8000"
    environment:
      - MY_API_KEY=${MY_API_KEY}
      - DATABASE_URL=${DATABASE_URL}
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8000/"]
      interval: 30s
      retries: 3

Options 3 and 4: Managed deployments

Prefect Horizon (formerly FastMCP Cloud), the platform from the team behind the independent FastMCP framework, removes the infrastructure step: you log in at horizon.prefect.io with your GitHub account, choose the repository and server file, and the platform builds and publishes a URL with built-in authentication. It targets servers written with that framework (pip install fastmcp, from fastmcp import FastMCP), which is distinct from the official SDK used in this course; a free tier exists for personal projects.

Cloudflare Workers targets a different usage profile: serverless, with execution closest to the user and thus reduced latency. A dedicated template kickstarts the project:

# Use Cloudflare's MCP template
npm create cloudflare@latest -- remote-mcp-server-authless --template=cloudflare/ai/demos/remote-mcp-authless

Both options spare you from managing certificates and restarts, at the cost of platform dependency and execution constraints—a serverless Worker, for example, doesn’t mesh well with a stateful server.

Securing the deployment

HTTPS is non-negotiable: Vibe refuses plaintext endpoints, and a token traveling over HTTP is a compromised token. Three paths lead there depending on your hosting. A reverse proxy like Nginx or Caddy placed in front of your server, with a Let’s Encrypt certificate, covers the dedicated server case. Cloud-native platforms—Hugging Face, Cloudflare, and most PaaS providers—provide HTTPS automatically. A tunnel, such as Cloudflare Tunnel, exposes a server remaining on your internal network without opening ports.

No secret should appear in the code. Note the distinction between the two syntaxes below: os.environ["MCP_API_TOKEN"] causes startup to fail if the variable is missing, which is exactly the desired behavior for a mandatory secret, while os.environ.get() tolerates absence for optional values.

import os

# Configuration via environment variables
API_TOKEN = os.environ["MCP_API_TOKEN"]       # Mandatory
DATABASE_URL = os.environ.get("DATABASE_URL")  # Optional
DEBUG = os.environ.get("DEBUG", "false") == "true"

A public server without rate limiting eventually gets saturated, voluntarily or not. An IP-based limiter suffices in most cases:

from fastapi import FastAPI, Request
from slowapi import Limiter, _rate_limit_exceeded_handler

limiter = Limiter(key_func=lambda request: request.client.host)
api = FastAPI()
api.state.limiter = limiter

@api.middleware("http")
async def rate_limit(request: Request, call_next):
    # 100 requests per minute per IP
    ...

Seeing what’s happening

In production, you no longer have your terminal in front of you, and a tool that fails one time out of ten can only be diagnosed with traces. At a minimum, instrument the input, output, and duration of every call:

import logging
import time

logger = logging.getLogger("mcp-server")

@mcp.tool()
async def my_tool(param: str) -> str:
    """My tool with logging."""
    start = time.time()
    logger.info(f"Tool called with param={param}")

    try:
        result = await process(param)
        duration = time.time() - start
        logger.info(f"Tool finished in {duration:.2f}s")
        return result
    except Exception as e:
        logger.error(f"Error in tool: {e}")
        raise

Measuring duration isn’t a luxury: a tool that takes twelve seconds to respond triggers client-side timeouts and gives the impression of a broken server when it’s actually working. Before announcing your server, review the following six configuration points.

  • HTTPS active and valid certificate
  • Environment variables for all secrets
  • Structured logging enabled
  • Health check endpoint (/ or /health)
  • Automatic restart on crash
  • Rate limiting configured

Two checks remain, the ones people most often skip yet which reveal the real problems: a test with MCP Inspector launched from outside, then a test from Mistral Vibe. A server that responds perfectly from your machine can be blocked by a firewall or CORS configuration as soon as it’s called from elsewhere.

Key takeaways

  • Hugging Face Spaces: fastest, free, ideal for getting started
  • Docker: total control, suited for dedicated servers and cloud
  • Prefect Horizon (ex-FastMCP Cloud): deployment from GitHub, for servers written with the FastMCP framework
  • Cloudflare Workers: serverless, low latency, scalable
  • Secure with HTTPS, environment variables, and rate limiting
  • Add monitoring and logs for production debugging