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.