Deployment¶
Run your MCP server in production — choose the right transport, configure CORS for browser clients, deploy behind a reverse proxy, containerize with Docker, and use the CLI for zero-boilerplate startup.
Transport Selection¶
Promptise supports three transports. Choose based on your deployment target:
graph TD
A[Where does the MCP client run?] --> B{Same machine?}
B -->|Yes| C[stdio]
B -->|No| D{Needs SSE compatibility?}
D -->|Yes| E[sse]
D -->|No| F[http — Streamable HTTP]
| Transport | Protocol | Use case |
|---|---|---|
stdio |
stdin/stdout | Local integration: Claude Desktop, CLI tools, IDEs |
http |
Streamable HTTP | Remote agents, web apps, microservices, production |
sse |
Server-Sent Events | Legacy clients, environments that don't support Streamable HTTP |
stdio — Local connections¶
Best for Claude Desktop integration. The MCP client spawns your server as a subprocess and communicates via stdin/stdout. No network configuration needed.
{
"mcpServers": {
"my-tools": {
"command": "python",
"args": ["-m", "myapp.server"]
}
}
}
http — Streamable HTTP (recommended for remote)¶
The server exposes a single endpoint at /mcp that handles all MCP protocol messages. Supports session tracking, bidirectional streaming, and concurrent requests.
sse — Server-Sent Events (legacy)¶
Exposes /sse for the event stream and /messages/ for client-to-server messages. Use this only when your client doesn't support Streamable HTTP.
CORS Configuration¶
When you need it¶
Your MCP server runs on api.example.com:8080. A web-based agent frontend on app.example.com needs to connect to it. Without CORS headers, the browser blocks the requests.
CORSConfig¶
from promptise.mcp.server import MCPServer, CORSConfig
server = MCPServer(name="web-api")
server.run(
transport="http",
port=8080,
cors=CORSConfig(
allow_origins=["https://app.example.com"],
allow_methods=["GET", "POST", "DELETE", "OPTIONS"],
allow_headers=["Authorization", "x-api-key", "Content-Type"],
allow_credentials=True,
max_age=3600,
),
)
Configuration¶
| Parameter | Default | Description |
|---|---|---|
allow_origins |
["*"] |
Allowed origin URLs |
allow_methods |
["GET", "POST", "DELETE", "OPTIONS"] |
Allowed HTTP methods |
allow_headers |
["*"] |
Allowed request headers |
allow_credentials |
False |
Allow cookies and auth headers |
max_age |
600 |
Preflight cache duration (seconds) |
Development vs production¶
import os
if os.getenv("ENV") == "production":
cors = CORSConfig(
allow_origins=["https://app.yourcompany.com"],
allow_credentials=True,
)
else:
cors = CORSConfig(
allow_origins=["*"], # Allow everything in dev
)
server.run(transport="http", port=8080, cors=cors)
Authentication at the Transport Level¶
When you need it¶
You want to reject unauthenticated HTTP requests before they reach MCP protocol handling. This prevents unauthenticated sessions from being created.
Transport-level auth gate¶
from promptise.mcp.server import MCPServer, JWTAuth
server = MCPServer(name="secure-api")
jwt = JWTAuth(secret="your-secret-key")
server.run(
transport="http",
port=8080,
require_auth=True, # Enables transport-level auth gate
)
When require_auth=True, the server checks every HTTP request for:
- Bearer token:
Authorization: Bearer <jwt-token>— verified with the configured auth provider - API key:
x-api-key: <key>— verified with the API key provider
Unauthenticated requests receive a 401 JSON response:
{
"error": "Authentication required",
"message": "Pass a Bearer token via the Authorization header or an API key via the x-api-key header."
}
Built-in token endpoint¶
For development and testing, you can enable a built-in token endpoint that issues JWTs:
from promptise.mcp.server import MCPServer, JWTAuth, TokenEndpointConfig
server = MCPServer(name="secure-api")
jwt = JWTAuth(secret="dev-secret")
server.run(
transport="http",
port=8080,
require_auth=True,
token_endpoint=TokenEndpointConfig(
path="/token",
auth_provider=jwt,
),
)
# Get a token
curl -X POST http://localhost:8080/token \
-H "Content-Type: application/json" \
-d '{"client_id": "my-agent"}'
# Use the token
curl http://localhost:8080/mcp \
-H "Authorization: Bearer <token>"
The token endpoint is automatically excluded from the auth gate (it issues tokens, so it can't require one).
Reverse Proxy¶
Nginx¶
upstream mcp_backend {
server 127.0.0.1:8080;
}
server {
listen 443 ssl;
server_name api.example.com;
ssl_certificate /etc/ssl/certs/api.example.com.pem;
ssl_certificate_key /etc/ssl/private/api.example.com.key;
location /mcp {
proxy_pass http://mcp_backend;
proxy_http_version 1.1;
# Required for Streamable HTTP
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# SSE requires long-lived connections
proxy_read_timeout 86400s;
proxy_buffering off;
}
}
Key proxy settings¶
| Setting | Why |
|---|---|
proxy_buffering off |
SSE and streaming responses must not be buffered |
proxy_read_timeout 86400s |
MCP sessions are long-lived |
proxy_http_version 1.1 |
Required for keep-alive and upgrade |
Connection "upgrade" |
Required for WebSocket-like transports |
Docker¶
Dockerfile¶
FROM python:3.12-slim
WORKDIR /app
# Install dependencies
COPY pyproject.toml .
RUN pip install --no-cache-dir .
# Copy application code
COPY src/ src/
# Expose port
EXPOSE 8080
# Run the MCP server
CMD ["python", "-m", "myapp.server"]
docker-compose.yml¶
services:
mcp-server:
build: .
ports:
- "8080:8080"
environment:
- OPENAI_API_KEY=${OPENAI_API_KEY}
- DATABASE_URL=postgresql://db:5432/myapp
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/mcp"]
interval: 30s
timeout: 10s
retries: 3
depends_on:
- db
db:
image: postgres:16
environment:
POSTGRES_DB: myapp
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
Health checks with Kubernetes¶
from promptise.mcp.server import MCPServer, HealthCheck
server = MCPServer(name="k8s-api")
health = HealthCheck()
async def check_db() -> bool:
try:
await db.execute("SELECT 1")
return True
except Exception:
return False
health.add_check("database", check_db, required_for_ready=True)
health.register_resources(server)
apiVersion: apps/v1
kind: Deployment
spec:
template:
spec:
containers:
- name: mcp-server
image: myapp:latest
ports:
- containerPort: 8080
# MCP health checks are exposed as resources,
# but for k8s probes you need HTTP endpoints.
# Use the /mcp endpoint as a basic liveness probe.
livenessProbe:
httpGet:
path: /mcp
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
CLI Serve¶
When you need it¶
You want to run your MCP server without writing if __name__ == "__main__" boilerplate. The CLI handles argument parsing, transport selection, and hot reload.
Usage¶
# Default: stdio transport
promptise serve myapp.server:server
# HTTP with specific port
promptise serve myapp.server:server -t http -p 9090
# With hot reload for development
promptise serve myapp.server:server -t http --reload
# With live dashboard
promptise serve myapp.server:server -t http --dashboard
Target format¶
The CLI imports the module and gets the named attribute, which must be an MCPServer instance:
from promptise.mcp.server import MCPServer
# This is what the CLI imports
server = MCPServer(name="my-tools")
@server.tool()
async def greet(name: str) -> str:
return f"Hello, {name}!"
Options¶
| Flag | Default | Description |
|---|---|---|
--transport, -t |
stdio |
stdio, http, or sse |
--host |
127.0.0.1 |
Bind host |
--port, -p |
8080 |
Bind port |
--dashboard |
off | Live terminal dashboard |
--reload |
off | Hot reload on file changes |
Hot Reload¶
For development, hot reload watches your Python files and restarts the server when changes are detected:
from promptise.mcp.server import MCPServer, hot_reload
server = MCPServer(name="dev")
@server.tool()
async def hello(name: str) -> str:
return f"Hello, {name}!"
if __name__ == "__main__":
hot_reload(
server,
transport="http",
port=8080,
watch_dirs=["src/"],
poll_interval=1.0,
)
Or via the CLI:
See Advanced Patterns — Hot Reload for details.
Production Checklist¶
Before deploying to production:
- [ ] Transport: Use
http(Streamable HTTP) for remote access,stdiofor local - [ ] Authentication: Enable
require_auth=Truewith JWT or API key validation - [ ] CORS: Restrict
allow_originsto your actual frontend domains - [ ] TLS: Terminate TLS at the reverse proxy (Nginx, Caddy, cloud LB)
- [ ] Health checks: Register
HealthCheckwith required dependency checks - [ ] Observability: Add
MetricsMiddlewareorOTelMiddlewarefor monitoring - [ ] Rate limiting: Add
RateLimitMiddlewareto prevent abuse - [ ] Circuit breakers: Protect against flaky downstream dependencies
- [ ] Audit logging: Add
AuditMiddlewarefor compliance - [ ] Process management: Use systemd, supervisord, or Kubernetes — not
hot_reload
API Summary¶
| Symbol | Type | Description |
|---|---|---|
CORSConfig(...) |
Dataclass | CORS settings for HTTP/SSE transports |
TransportType |
Enum | STDIO, HTTP, SSE |
hot_reload(server, ...) |
Function | File-watching dev server |
build_serve_parser(...) |
Function | CLI argument parser builder |
resolve_server(target) |
Function | Import server from module:attr |
run_serve(args) |
Function | Run server from CLI args |
TokenEndpointConfig(...) |
Dataclass | Built-in token endpoint config |
What's Next¶
- Authentication & Security — JWT, API keys, guards, roles
- Caching & Performance — Cache, rate limit, concurrency
- Observability & Monitoring — Metrics, tracing, logging
- Resilience Patterns — Circuit breakers, health checks