Skip to content

The MCP Server Stack

Stack lives at /opt/stacks/365-mcp/ (managed by Dockge). Source of truth: ~/Templates/365-mcp-stack/.

365-mcp/
├── compose.yaml
├── Dockerfile
├── .env                 # secrets, chmod 600, NOT in git
└── server/
    ├── main.py          # FastMCP server + Cloudflare Access JWT validation
    └── graph_client.py  # MSAL client-credentials → Graph

compose.yaml

Joins the existing tunnel network so cloudflared can proxy it. No host port is published — the container is only reachable via the tunnel.

services:
  365-mcp:
    build: .
    container_name: 365-mcp
    restart: unless-stopped
    env_file: .env
    networks: [tunnel-net]
networks:
  tunnel-net:
    external: true

Dockerfile (key points)

  • Base python:3.12-slim.
  • Installs mcp[cli] msal httpx uvicorn[standard] starlette python-dotenv anyio pyjwt[crypto].
  • pyjwt[crypto] is required for validating the Cloudflare Access JWT (RS256).

server/main.py (two critical settings)

a) Allow the public host through MCP's DNS-rebinding protection. FastMCP defaults to allowing localhost only; over the tunnel the Host header is the public name, which otherwise returns 421 Invalid Host header.

from mcp.server.fastmcp import FastMCP
from mcp.server.transport_security import TransportSecuritySettings

mcp = FastMCP(
    "365-MCP",
    streamable_http_path="/sse",
    stateless_http=True,
    transport_security=TransportSecuritySettings(
        enable_dns_rebinding_protection=True,
        allowed_hosts=["365-mcp.example.com", "365-mcp:3000",
                       "localhost:*", "127.0.0.1:*"],
        allowed_origins=["https://365-mcp.example.com", "https://claude.ai"],
    ),
)

b) Validate the Cloudflare Access JWT at the origin. A pure-ASGI middleware reads Cf-Access-Jwt-Assertion, verifies it (RS256) against the team JWKS, and checks iss (team domain) and aud (the application AUD tag). /health is the only unauthenticated path. Controlled by env vars (see .env).

.env

# Entra app (Graph client-credentials)
AZURE_TENANT_ID=<tenant-guid>
AZURE_CLIENT_ID=<app-id-guid>
AZURE_CLIENT_SECRET=<secret>          # never commit
PORT=3000

# Cloudflare Access origin validation
CF_ACCESS_TEAM_DOMAIN=https://<team>.cloudflareaccess.com
CF_ACCESS_AUD=<application-aud-tag>
CF_ACCESS_ENFORCE=true                # false = log-only (rollout), true = reject

chmod 600 .env.

Build & run

cd /opt/stacks/365-mcp
docker compose up -d --build
docker logs -f 365-mcp     # expect: "Cloudflare Access validation active … enforce=True"