MCP Series (07): Enterprise Deployment โ€” Security, Authentication, and Version Management
DEV Community

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

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-stopped or 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: true network (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

  • MCP Transport Protocol Specification
  • Docker Security Best Practices

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.

Comments

No comments yet. Start the discussion.