Azoth¶
| Field | Value |
|---|---|
| Owner | Albert (IT) |
| Status | live |
| Runs on | Ubuntu VM (ubuntuserver, 192.168.1.8), Docker container outlook-mcp |
| Connectivity | Public HTTPS via the central Caddy reverse proxy (TLS) → 192.168.1.8:8091. Not via Cloudflare Tunnel. |
| Hostname | azoth.alquemis.com/mcp |
| Repository | Alquemis-Automation/Azoth (private) |
What it is¶
Azoth is a read-only MCP server that lets Claude read Outlook emails and the content of
their attachments via Microsoft Graph — something the official Microsoft 365 connector doesn't
do (it only reads the email body). It is strictly read-only: no code path can send, draft, move,
or delete anything (delegated scopes are exactly User.Read + Mail.Read).
It is Alquemis' first containerized MCP on the Ubuntu VM, and the first to use per-user OAuth On-Behalf-Of (OBO) instead of a shared token. Azoth is registered as an organization connector in claude.ai: each Alquemis user who enables it authenticates with their own M365 account and reads their own mailbox — there is no shared token and no single shared mailbox. It runs in Docker and speaks MCP over HTTP (streamable-HTTP), not as a local stdio process.
Built in Python (src/outlook_mcp/), packaged with pyproject.toml, with a pytest suite.
Tools published¶
Five read-only tools; Claude chains them from a natural-language question (you never call them directly):
| Tool | Function |
|---|---|
health_check |
Server status (needs no prior M365 session — the session rides in each request's JWT) |
list_recent_messages |
List the caller's own messages (search, from_address, only_with_attachments, count) |
list_email_attachments |
List a message's attachments (attachmentId, name, contentType, size) |
read_email_attachment |
Download + extract an attachment's content (text and/or image blocks) |
read_email |
Full body of a message (HTML converted to plain text) |
How it's reached¶
Public entry is HTTPS at https://azoth.alquemis.com/mcp, terminated by the central Caddy
reverse proxy (on another server) which reverse_proxyes over the LAN to the container at
192.168.1.8:8091.
Auth is OAuth On-Behalf-Of against Microsoft Entra ID. Every request to /mcp carries an
Entra JWT (validated per request: signature via JWKS, plus iss / aud / exp); the server then
does acquire_token_on_behalf_of (MSAL) to call Graph as that user. All Graph calls are hard-wired
to https://graph.microsoft.com/v1.0/me/... — no tool takes a mailbox parameter, so no one can
read another person's mail.
- Without a valid token,
/mcpreturns401withWWW-Authenticate: Bearer resource_metadata="https://azoth.alquemis.com/.well-known/oauth-protected-resource". - That discovery endpoint (RFC 9728 protected-resource-metadata) and
/healthzare the only unauthenticated endpoints. - Connecting it in claude.ai: an org admin adds a Custom → Web connector at
https://azoth.alquemis.com/mcpand pastes the Entra Client ID + Client Secret in the advanced (non-DCR) OAuth config; each user then authorizes with their own M365 account (PKCE handled by claude.ai). Redirect URI on the Entra app:https://claude.ai/api/mcp/auth_callback.
Configuration¶
Config is injected as container environment (from .env, never committed; see .env.example).
The server is fail-closed — it won't start if any of the three required values is missing.
| Variable | Purpose |
|---|---|
M365_CLIENT_ID |
Entra Application (client) ID |
M365_TENANT_ID |
Entra Directory (tenant) ID (single-tenant app) |
M365_CLIENT_SECRET |
Entra client-secret Value — sensitive; enables the OBO exchange |
MCP_HOST / MCP_PORT |
Container bind (0.0.0.0 / 8091) |
MCP_RESOURCE |
OAuth resource / Application ID URI (default https://azoth.alquemis.com/mcp) |
The Entra app is both the protected API (Application ID URI …/mcp, scope access_as_user)
and the confidential client that performs the OBO exchange — hence it needs a client secret.
Security notes¶
- Read-only by design. Delegated scopes are exactly
User.Read+Mail.Read; no code can send/draft/move/delete. Verify only those two appear in the Entra registration. - Per-user isolation. Each request is served with the caller's own OBO token against
/me; nobody can read another mailbox. - Container hardening.
read_only: trueroot filesystem,tmpfs/tmp,no-new-privileges. A named volume (outlook-mcp-data,/home/app/.outlook-mcp, perms0600) holds only the per-user MSAL token cache and rotating logs — never email/attachment content. - LAN bind trust. The container publishes
192.168.1.8:8091on the LAN so the remote Caddy can reach it; that hop is plain HTTP over the LAN, gated by the bearer token. Firewall8091to the Caddy host's IP where possible. Never expose0.0.0.0:8091without TLS. - Secret handling.
M365_CLIENT_SECRETis a service credential (in.env/ GitHub Actions secrets / the claude.ai connector config only).M365_CLIENT_ID/M365_TENANT_IDidentify but don't authenticate. - Logs record operation/state only — never tokens or mail content; daily rotation, 15-day retention. Errors returned to Claude omit stack traces (full detail only in the log).
Dependencies¶
- Microsoft Graph (
graph.microsoft.com/v1.0/me/...) and Microsoft Entra ID (login.microsoftonline.com) for JWT validation (JWKS) + the OBO token exchange — the server must be able to reach Entra outbound. - Central Caddy reverse proxy (on another server) for public TLS at
azoth.alquemis.com, plus a DNS record for that hostname. - Docker + Docker Compose on the Ubuntu VM (
192.168.1.8). - Network: Anthropic egress
160.79.104.0/21must reach the public HTTPS endpoint. - Consumers: Alquemis users in claude.ai (organization connector).
Known limitations¶
- Attachment formats. Parses PDF (scanned pages rasterized to image blocks for vision — no
local OCR), Word
.docx, Excel.xlsx/.xlsm, PowerPoint.pptx, plain text, HTML→text, images, and.zip/.7z/.msg(recursion to depth 2). Not supported:.rar(needsunrar), and legacy binary.doc/.xls/.ppt— convert to the modern formats. - Response limits. Attachment ≤ 25 MB; text ~35 000 chars (truncated with notice); ≤ 20 image blocks and ~1 MB image bytes per response; archive nesting depth 2; ≤ 12 entries listed per archive; 30 s Graph timeout.
- Read-only, own mailbox only. No sending, and no access to shared/other mailboxes.