Three Gaps Between Local and Production
A local MCP Server takes one command: python server.py. Enterprise production adds three required problems:
- Authentication: stdio mode has no auth — any process can connect. Production needs explicit identity verification.
- Process supervision: a Python process that dies doesn't restart itself and generates no alert. Production needs a guardian and health checks.
- Smooth upgrades: multiple Agent sessions may connect to the same Server simultaneously. Upgrading can't drop those connections.
Authentication Options
API Key (preferred for internal services)
Simplest option, suitable for service-to-service calls inside a corporate network:
import os
from mcp.server import Server
EXPECTED_API_KEY = os.environ.get("MCP_API_KEY")
if not EXPECTED_API_KEY:
raise RuntimeError("MCP_API_KEY environment variable is required")
server = Server("jira-tools")
For HTTP transport (non-stdio), validate in middleware:
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
class ApiKeyMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
key = (request.headers.get("X-API-Key")
or request.headers.get("Authorization", "").removeprefix("Bearer "))
if key != os.environ["MCP_API_KEY"]:
return Response("Unauthorized", status_code=401)
return await call_next(request)
OAuth 2.0 (cross-org or user-level permissions)
Use when different users need access to different subsets of data (different Jira projects per user):
The MCP spec (2025) defines OAuth integration interfaces. The Host (Claude Desktop / Claude Code) acquires the OAuth token during user login and passes it to the Server on each MCP connection.
mTLS (high-security internal communication)
For finance, healthcare, or government scenarios with strict data security requirements — mutual certificate verification:
import ssl
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
ssl_context.load_cert_chain("/certs/server.crt", "/certs/server.key")
ssl_context.load_verify_locations("/certs/ca.crt")
ssl_context.verify_mode = ssl.CERT_REQUIRED # require client cert
# pass ssl_context to HTTP server
Authentication selection:
Internal service calls (same network) → API Key + network isolation
Cross-org or user-level permission needs → OAuth 2.0
High security (finance, healthcare, gov) → mTLS
Docker Deployment
Minimal Dockerfile
FROM python:3.12-slim
WORKDIR /app
# Separate dependency install (cache layer)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
# Don't run as root (security best practice)
RUN useradd -r -s /bin/false mcpuser
USER mcpuser
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s \
CMD python -c "import sys; print('healthy')" || exit 1
CMD ["python", "jira_server.py"]
Production Docker Compose
# docker-compose.prod.yml
version: "3.9"
services:
jira-mcp:
build: .
image: jira-mcp-server:1.3.0
restart: unless-stopped # auto-restart on crash
environment:
- MCP_API_KEY=${MCP_API_KEY} # injected from .env or Secrets
- JIRA_URL=${JIRA_URL}
- JIRA_TOKEN=${JIRA_TOKEN}
- LOG_LEVEL=INFO
volumes:
- ./logs:/app/logs
networks:
- mcp-internal # internal only
deploy:
resources:
limits:
cpus: "0.5"
memory: "256M"
reservations:
memory: "128M"
logging:
driver: "json-file"
options:
max-size: "100m"
max-file: "5"
networks:
mcp-internal:
driver: bridge
internal: true # no external network access
Key configuration points:
-
restart: unless-stopped: restart on crash; stay stopped only on manual stop -
internal: true: Docker network with no outbound connection — only containers on the same network can reach the Server - Resource limits: prevent a buggy Server from consuming host machine resources
- Log rotation: prevent log files from growing unboundedly
Process Supervision (non-Docker)
# /etc/systemd/system/jira-mcp.service
[Unit]
Description=Jira MCP Server
After=network.target
[Service]
Type=simple
User=mcpuser
WorkingDirectory=/opt/jira-mcp
ExecStart=/usr/bin/python3 /opt/jira-mcp/jira_server.py
Restart=always
RestartSec=5
Environment="MCP_API_KEY=your-key"
Environment="JIRA_TOKEN=your-token"
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
systemctl enable jira-mcp
systemctl start jira-mcp
journalctl -u jira-mcp -f # live log stream
Multi-Version Coexistence and Smooth Upgrades
The Problem
Multiple Agent sessions connect to MCP Server v1.2. You need to release v1.3 (adds a new tool) without dropping those connections.
Strategy: Parallel Versions + Traffic Switch
# docker-compose.prod.yml (parallel versions)
services:
jira-mcp-stable:
image: jira-mcp-server:1.2.0 # current stable, serves existing connections
restart: unless-stopped
networks: [mcp-internal]
jira-mcp-canary:
image: jira-mcp-server:1.3.0 # new version, accepts new connections
restart: unless-stopped
networks: [mcp-internal]
deploy:
replicas: 1
Point the Host config at the canary version first. Monitor it. Then switch stable:
{
"mcpServers": {
"jira": {
"command": "docker",
"args": ["exec", "jira-mcp-canary", "python", "jira_server.py"]
}
}
}
Version Number Rules
MAJOR.MINOR.PATCH
MAJOR: breaking changes
→ Remove a tool, rename a tool, change required inputSchema fields
→ Agent code that depends on this tool must update in sync
MINOR: backward-compatible additions
→ Add a tool, add optional parameters, extend return fields
→ Existing Agent code continues working; new features opt-in
PATCH: behavior-preserving fixes
→ Bug fixes, performance improvements, logging changes
→ Transparent upgrade
Declare the version in Server code:
server = Server(
"jira-tools",
version="1.3.0" # returned to Client in initialize response
)
Deprecation Process
@server.call_tool()
async def call_tool(name: str, arguments: dict):
if name == "search_jira": # old tool name
logger.warning(
"Tool 'search_jira' is deprecated. "
"Use 'search_issues' instead. Removing in v2.0.0."
)
return await _search_issues(arguments) # delegate to new implementation
Keep the old tool name for 90 days, log usage, then remove it in the MAJOR version.
Security Design Checklist
Authentication and authorization
- [ ] Credentials injected via environment variables — never hardcoded in code or image
- [ ] Internal services use API Key; cross-org or user-level permissions use OAuth
- [ ] HTTP transport validates in middleware layer, not in tool handlers
Network isolation
- [ ] Docker network set to
internal: true— Server has no direct outbound access - [ ] Only necessary ports exposed (stdio mode requires no open ports)
- [ ] Multiple Servers isolated on separate Docker networks
Tool security
- [ ] Tool inputs have type validation and range checks (Article 04)
- [ ] High-risk tools (writes, external API calls) have audit logs
- [ ] Server's filesystem access restricted to necessary directories
Operations
- [ ] Logs go to stderr, structured format (JSON), with rotation configured
- [ ]
restart: unless-stoppedor systemd supervision ensures availability - [ ] Resource limits (CPU/memory) prevent abnormal Server behavior from affecting the host
Summary
- Match auth to the scenario: API Key works for internal services, OAuth for cross-org or user-level access, mTLS for regulated industries — don't over-engineer
-
Docker three-pack:
restart: unless-stopped(crash recovery) +internal: truenetwork (isolation) + resource limits (stability) - Smooth upgrades need parallel versions: MINOR versions are backward-compatible and can replace directly; MAJOR versions run old and new in parallel, letting Agent code migrate at its own pace
References
Check out PrimeSkills — a curated marketplace of AI agents and skills that have been validated in real-world, enterprise-grade workflows. No fluff, just what actually works.
Find more useful knowledge and interesting products on my Homepage
Top comments (0)