MCP Series (07): Enterprise Deployment — Security, Authentication, and Version Management

MCP Series (07): Enterprise Deployment — Security, Authentication, and Version Management

# mcp# docker# ai# llm
MCP Series (07): Enterprise Deployment — Security, Authentication, and Version ManagementWonderLab

Three Gaps Between Local and Production A local MCP Server takes one command: python...

Three Gaps Between Local and Production

A local MCP Server takes one command: python server.py. Enterprise production adds three required problems:

  1. Authentication: stdio mode has no auth — any process can connect. Production needs explicit identity verification.
  2. Process supervision: a Python process that dies doesn't restart itself and generates no alert. Production needs a guardian and health checks.
  3. 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")
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
systemctl enable jira-mcp
systemctl start jira-mcp
journalctl -u jira-mcp -f    # live log stream
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Declare the version in Server code:

server = Server(
    "jira-tools",
    version="1.3.0"   # returned to Client in initialize response
)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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-stopped or systemd supervision ensures availability
  • [ ] Resource limits (CPU/memory) prevent abnormal Server behavior from affecting the host

Summary

  1. 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
  2. Docker three-pack: restart: unless-stopped (crash recovery) + internal: true network (isolation) + resource limits (stability)
  3. 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