Design: Outlook Mail#
Overview#
Purpose: Expose Microsoft 365 mailbox capabilities to AI assistants via the MCP protocol, enabling users to list, search, and read emails from their own mailbox using delegated permissions.
Scope:
- Five MCP tools: list_emails, search_emails, read_email, list_folders, get_mailbox_settings
- Read-only access to the authenticated user's mailbox via Microsoft Graph API
- Attachment metadata retrieval (not content download by default)
- OAuth2 On-Behalf-Of (OBO) authentication — see DESIGN:OAuthAuthentication
Out of Scope: - Email composition, sending, forwarding, or deletion - Attachment content download (returned as metadata only) - Shared mailboxes or delegate mailbox access - Email rules, categories, or flags management
Architecture#
┌────────────────────┐ ┌──────────────────────┐ ┌──────────────────────┐
│ AI Assistant │────▶│ Outlook MCP Server │────▶│ Microsoft Graph API │
│ (Claude / MCP │ │ (FastMCP / ECS) │ │ /v1.0/me/messages │
│ client) │◀────│ │◀────│ /v1.0/me/mailFolders│
└────────────────────┘ └──────────────────────┘ └──────────────────────┘
│
▼
┌──────────────────────┐
│ OBOAuthenticator │
│ (auth.py) │
└──────────────────────┘
│
▼
┌──────────────────────┐
│ Token Storage │
│ (DynamoDB / Memory) │
└──────────────────────┘
Components:
- FastMCP Server (server.py) — ASGI HTTP server, MCP protocol handler, tool registration
- Mail Tools (tools/mail.py) — MCP tool wrappers delegating to Graph clients
- Graph Mail Client (graph/mail.py) — Microsoft Graph API calls for mail resources
- Graph Client (graph/client.py) — Authenticated HTTP client (OBO token injection)
- OBOAuthenticator (auth.py) — On-Behalf-Of token exchange; see DESIGN:OAuthAuthentication
- Microsoft Graph API — Upstream data source; enforces user-scoped authorization
Tech Stack#
Backend: Python 3.13, FastMCP ≥3.0.0, Starlette (ASGI)
HTTP Client: httpx (async)
OAuth: MSAL 1.28+, OBO flow via auth.py
Token Storage: py-key-value-aio (DynamoDB in production, memory in dev)
Infrastructure: AWS ECS Fargate, Application Load Balancer, ECR
Secret Management: AWS SSM Parameter Store (KMS encrypted)
Deployment: Docker (multi-stage), GitHub Actions CI/CD
MCP Tool API#
list_emails#
Lists emails in a mailbox folder.
Parameters:
- folder_id (str, default: "inbox") — folder to list from
- count (int, default: 10) — maximum results to return
Returns: List of { id, subject, from, receivedDateTime }
Graph endpoint: GET /v1.0/me/mailFolders/{folder_id}/messages
search_emails#
Searches emails using OData filters with progressive fallback.
Parameters:
- folder_id (str, default: "inbox")
- query (str, optional) — free-text search
- sender (str, optional) — filter by sender address
- recipient (str, optional) — filter by recipient address
- subject (str, optional) — filter by subject keywords
- has_attachments (bool, optional)
- unread (bool, optional)
- importance (str, optional) — high, normal, or low
- received_after (str, optional) — ISO 8601 datetime
- received_before (str, optional) — ISO 8601 datetime
- count (int, default: 10)
Returns: List of { id, subject, from, receivedDateTime }
Graph endpoint: GET /v1.0/me/mailFolders/{folder_id}/messages with $search or $filter
Search strategy: Progressive fallback — $search first (free-text), then $filter (structured filters), then recent emails if no filters provided.
read_email#
Retrieves full details of a single email, including attachment metadata.
Parameters:
- email_id (str) — unique message ID
Returns: { email: { id, subject, from, to, body, receivedDateTime, hasAttachments, ... }, attachments: [{ id, name, size, contentType }] }
Graph endpoints:
- GET /v1.0/me/messages/{id}
- GET /v1.0/me/messages/{id}/attachments (only if hasAttachments=true)
list_folders#
Lists all mail folders in the user's mailbox.
Parameters: None
Returns: List of { id, displayName, parentFolderId, childFolderCount, unreadItemCount }
Graph endpoint: GET /v1.0/me/mailFolders
get_mailbox_settings#
Retrieves mailbox configuration for the authenticated user.
Parameters: None
Returns: { timeZone, workingHours, automaticRepliesSetting, ... }
Graph endpoint: GET /v1.0/me/mailboxSettings
Security#
Authentication: OAuth2 OBO flow — see DESIGN:OAuthAuthentication
Authorization:
- All Graph API calls use /me/ endpoint — access scoped to the authenticated user
- No parameterized user IDs accepted (prevents cross-user access)
- Microsoft Graph API provides secondary authorization layer
Graph Permission Scope: Mail.Read only (no write, send, or delete permissions)
Data Protection:
- Email content never persisted — stateless HTTP mode (stateless_http=True)
- DynamoDB stores OAuth tokens only — no email content or metadata
- Email subject, body, sender, and recipient fields excluded from application logs
- LOG_LEVEL=INFO in production — DEBUG logging disabled
- Transport: TLS 1.3 (enforced at ALB layer)
Attachment Handling:
- list_emails and search_emails return attachment count/flag only
- read_email returns attachment metadata (name, size, contentType) — no content download
- No automatic attachment content retrieval
Data Model#
No persistent data model for email content. The only data stored is OAuth tokens:
DynamoDB table: mcp-oauth-storage-outlook-mcp-{env}
Partition key: user_id (string)
Sort key: session_id (string)
Attributes: access_token, refresh_token, expires_at
TTL: expires_at + 90 days
All email data is fetched on-demand from Microsoft Graph API and returned directly to the MCP client. No caching, no secondary storage.
Integrations#
Microsoft Graph API
- Protocol: REST over HTTPS
- Auth: OAuth2 Bearer token (OBO-exchanged, user-scoped)
- Scopes: Mail.Read, MailboxSettings.Read, offline_access
- Base URL: https://graph.microsoft.com/v1.0
- Rate limits: 10,000 requests per 10 minutes per user
- Error handling: HTTP 429 propagated to client (with Retry-After); HTTP 401/403 re-raised
Azure AD (authentication)
- See DESIGN:OAuthAuthentication for full flow
- Tenant: Novo Nordisk single-tenant
Performance#
Expected Load: Low — per-user assistant sessions, not batch operations
Response Time Target: < 2s for list_emails / search_emails, < 3s for read_email with attachments
OBO Exchange Overhead: ~200–500ms per tool call (see DESIGN:OAuthAuthentication)
Optimization Notes:
- No Graph token caching (security > latency)
- search_emails uses progressive fallback to avoid unsupported filter combinations
- Attachment metadata fetched only when hasAttachments=true (avoids redundant API calls)
Deployment#
Environments:
| Env | URL | ECS sizing | Storage |
|---|---|---|---|
| Dev | outlook.dev.connectors.novo-genai.com |
0.25 vCPU / 512 MB | Memory |
| Prod | outlook.connectors.novo-genai.com |
0.5 vCPU / 1 GB | DynamoDB |
CI/CD: .github/workflows/deploy-outlook-mcp.yml
- lint → build (push to dev ECR) → deploy-dev → deploy-prod (gated: environment: Production)
- Prod promotes image from dev ECR (no rebuild)
Secrets (SSM Parameter Store):
/aiconnectors/{env}/outlook-mcp/client_id
/aiconnectors/{env}/outlook-mcp/client_secret
/aiconnectors/{env}/outlook-mcp/dynamodb_table_name
Monitoring#
Logs: JSON structured logs → CloudWatch Logs (/ecs/outlook-mcp-{env})
Retention: 90 days (GDPR Article 30 compliance)
Key Events Logged:
- email_list_requested — user identity, folder, count
- email_search_requested — user identity, filter strategy used
- email_read_requested — user identity, message ID
- graph_request — method, endpoint, HTTP status (no content)
- obo_exchange_failed — error code, user identity
NOT Logged: - Email subject, body, sender/recipient fields - Attachment names or content - OAuth tokens or authorization codes
Alerts: - Error rate > 5% over 5 minutes - Graph API 5xx rate > 10 per minute - ECS health check failures
Key Decisions#
Why /me/ endpoint only (no /users/{id}/)?#
Decision: All Graph API calls use /me/ — no parameterized user ID paths.
Rationale: Prevents cross-user access. The /users/{id}/messages endpoint requires Mail.ReadWrite.All (application permission), which is intentionally not requested. Using /me/ ensures the Graph API resolves the mailbox from the OBO token's identity claim, making cross-user access structurally impossible even if the server code has a bug.
Why no attachment content download?#
Decision: read_email returns attachment metadata only. Content retrieval is not implemented.
Rationale: Attachment content (particularly executables, macros, scripts) represents a higher risk surface for the AI assistant. Returning metadata (name, size, contentType) allows the user to decide whether to retrieve the content via other means. This is consistent with R5 in RISK:OutlookMail (attachment injection risk).
Why progressive search fallback?#
Decision: search_emails tries $search first, then $filter, then falls back to recent emails.
Rationale: Microsoft Graph API does not support combining $search with all $filter operators. The progressive strategy maximizes compatibility across different filter combinations without requiring the MCP client to understand Graph API limitations.
Related Requirements#
URS: @URS:OutlookMail
Risk Assessment: @RISK:OutlookMail
OAuth Design: @DESIGN:OAuthAuthentication
Risks Addressed by This Design:
- R1 (token compromise) — OBO flow, Mail.Read scope only
- R2 (content in logs) — structured logging with field exclusion
- R3 (persistent storage) — stateless HTTP, no email caching
- R4 (excessive permissions) — Mail.Read only, no write scope
- R5 (attachment injection) — metadata-only retrieval
- R6 (cross-user access) — /me/ endpoint, no user ID parameters
- R7 (audit trail) — structured logging with user identity and message IDs
Version: 1.0 Date: 2026-04-22 Author: AILab Approved by: Pending PR review