Saltar a contenido

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, /mcp returns 401 with WWW-Authenticate: Bearer resource_metadata="https://azoth.alquemis.com/.well-known/oauth-protected-resource".
  • That discovery endpoint (RFC 9728 protected-resource-metadata) and /healthz are the only unauthenticated endpoints.
  • Connecting it in claude.ai: an org admin adds a Custom → Web connector at https://azoth.alquemis.com/mcp and 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 Valuesensitive; 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: true root filesystem, tmpfs /tmp, no-new-privileges. A named volume (outlook-mcp-data, /home/app/.outlook-mcp, perms 0600) 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:8091 on the LAN so the remote Caddy can reach it; that hop is plain HTTP over the LAN, gated by the bearer token. Firewall 8091 to the Caddy host's IP where possible. Never expose 0.0.0.0:8091 without TLS.
  • Secret handling. M365_CLIENT_SECRET is a service credential (in .env / GitHub Actions secrets / the claude.ai connector config only). M365_CLIENT_ID / M365_TENANT_ID identify 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/21 must 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 (needs unrar), 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.