Mitigate Mail MCP
A hosted Model Context Protocol server for IMAP and SMTP email, built in Ruby. It acts as both an OAuth 2.1 Authorization Server and an MCP Resource Server.
Architecture
Client (Claude Desktop / MCP Inspector)
│
├─ OAuth 2.1 flow
│ GET /.well-known/oauth-protected-resource RFC 9728 metadata
│ GET /.well-known/oauth-authorization-server RFC 8414 metadata
│ GET /oauth/authorize Login UI
│ POST /oauth/authorize Validate IMAP/SMTP → issue code
│ POST /oauth/token Code + PKCE + client_secret → tokens
│ (also: refresh_token grant)
│
└─ MCP calls (all HTTP methods)
/mcp Bearer access token → decrypt creds → IMAP/SMTP operations
How authentication works
Clients are provisioned once via the bin/mail_mcp generate CLI, which produces:
client_id— a JWE token (encrypted, opaque) encoding the IMAP/SMTP server configuration and theclient_secret. Only the server can decrypt it.client_secret— a random secret used to authenticate the client at the token endpoint.
The OAuth flow:
- Client redirects the user to
/oauth/authorize?client_id=<jwe>&... - Server decrypts the
client_idJWE to learn which IMAP/SMTP servers to connect to - User enters their IMAP/SMTP username and password in the login form
- Server validates both connections live; shows an error banner on failure
- On success: credentials are encrypted directly in a JWE access token and a JWE refresh token
- Client exchanges the authorization code (
POST /oauth/token) withclient_id+client_secret; receivesaccess_token+refresh_token - Every MCP request carries
Authorization: Bearer <access_token>; the server decrypts credentials per-request
Token formats
All tokens are 5-part JWE (dir / A256GCM), encrypted with ENCRYPTION_KEY. There is no separate signing key.
| Token | typ claim |
Expiry | Contents |
|---|---|---|---|
client_id |
client_id |
none | imap/smtp host+port, client_secret |
| Access token | access |
8 hours | IMAP + SMTP credentials |
| Refresh token | refresh |
30 days | IMAP + SMTP credentials |
Directory Structure
mail_mcp/
├── Gemfile
├── .env.sample # Environment variable template
├── bin/
│ └── mail_mcp # CLI: `generate` (client_id) and `server` (puma)
├── config.ru # Rack entry point — run MailMCP::App.new
├── config/
│ └── puma.rb # Puma config (single worker, 5 threads)
├── lib/
│ ├── mail_mcp.rb # Module root + requires
│ └── mail_mcp/
│ ├── jwt_service.rb # All JWE tokens (access, refresh, client_id)
│ ├── pkce.rb # PKCE S256 challenge/verify
│ ├── credential_context.rb # Struct passed as MCP server_context per request
│ ├── imap_client.rb # net-imap wrapper
│ ├── smtp_client.rb # net-smtp wrapper
│ ├── attachment_store.rb # S3 upload + presigned URLs (7 days)
│ ├── tool.rb # MailMCP::Tool base class
│ ├── app.rb # Sinatra: OAuth + MCP /mcp route (all methods)
│ └── tools/*.rb # MCP tool classes
├── views/
│ └── login.erb # Login form (username + password only)
├── spec/ # RSpec test suite
└── Dockerfile
Configuration
Copy .env.sample to .env and fill in the values:
| Variable | Description |
|---|---|
BASE_URL |
Public URL of this server, e.g. https://mail.mcp.mitigate.dev |
ENCRYPTION_KEY |
AES-256 key (base64-encoded 32 bytes) — used for all JWE tokens |
AWS_ACCESS_KEY_ID |
AWS credentials for S3 attachment storage |
AWS_SECRET_ACCESS_KEY |
AWS credentials for S3 attachment storage |
AWS_REGION |
S3 bucket region, e.g. us-east-1 |
AWS_S3_BUCKET |
S3 bucket name for attachments |
PORT |
HTTP port (default 3000) |
RACK_ENV |
development or production |
Generate ENCRYPTION_KEY:
ruby -e "require 'base64','securerandom'; puts Base64.strict_encode64(SecureRandom.bytes(32))"
IMAP/SMTP host and port are embedded in the client_id JWE and are never passed as headers or query parameters.
Setup
# Install dependencies
bundle install
# Copy and edit environment variables
cp .env.sample .env
$EDITOR .env
# Run tests
bundle exec rspec
# Start the server
bundle exec puma -C config/puma.rb
Provisioning a Client
Run bin/mail_mcp generate once per mail server configuration. The resulting client_id and client_secret are configured in the MCP client (e.g. Claude Desktop).
bundle exec bin/mail_mcp generate \
--imap-host=imap.gmail.com \
--imap-port=993 \
--smtp-host=smtp.gmail.com \
--smtp-port=587
# Client ID: eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIn0...<encrypted>
# Client Secret: 713576e2f94802b9d9abfd755e38e29b63e491df...
#
# IMAP: imap.gmail.com:993 (ssl=true)
# SMTP: smtp.gmail.com:587 (ssl=false)
| Flag | Default | Description |
|---|---|---|
--imap-host=HOST |
required | IMAP server hostname |
--imap-port=PORT |
993 |
IMAP port |
--[no-]imap-ssl |
true when port 993 |
Enable SSL/TLS for IMAP |
--smtp-host=HOST |
required | SMTP server hostname |
--smtp-port=PORT |
587 |
SMTP port |
--[no-]smtp-ssl |
true when port 465 |
Enable SSL/TLS for SMTP |
OAuth 2.1 Flow
- Discovery — client fetches
/.well-known/oauth-protected-resourceand/.well-known/oauth-authorization-server - Authorization — client redirects user to
/oauth/authorize?client_id=<jwe>&code_challenge=<s256>&... - Login — server decrypts
client_idJWE to get IMAP/SMTP hosts; user enters credentials; server validates both connections live - Code exchange —
POST /oauth/tokenwithgrant_type=authorization_code,code,code_verifier,client_id,client_secret; server issuesaccess_token+refresh_token - Token refresh —
POST /oauth/tokenwithgrant_type=refresh_token,refresh_token,client_id,client_secret; server issues a newaccess_token+refresh_token - MCP calls — client sends
Authorization: Bearer <access_token>on every request; server decrypts credentials per-request via a stateless per-request MCP server
MCP Tools
| Tool | Parameters | Description |
|---|---|---|
list_mailboxes |
— | List all IMAP folders |
list_mail_messages |
folder, page, per_page |
List messages with pagination |
get_mail_message |
folder, uid |
Fetch full message; attachments uploaded to S3 and returned as presigned URLs |
search_mail_messages |
folder, query |
Raw IMAP SEARCH criteria, e.g. UNSEEN or FROM alice@example.com SINCE 01-Jan-2025 |
send_mail_message |
to, subject, text_body, cc, bcc, html_body, attachment_urls, folder |
Send via SMTP and append to the Sent folder via IMAP; attachments fetched from S3 presigned URLs |
create_draft_mail_message |
to, subject, text_body, cc, bcc, html_body, attachment_urls, folder |
Append to Drafts via IMAP APPEND; attachments fetched from S3 presigned URLs |
delete_mail_message |
folder, uid |
Mark \Deleted + EXPUNGE |
move_mail_message |
folder, uid, destination |
IMAP MOVE (or COPY+DELETE fallback) |
update_mail_message_flags |
folder, uid, add, remove |
Add/remove IMAP flags, e.g. \Seen, \Flagged |
Attachments are never returned as binary data — they are uploaded to S3 on first access and returned as presigned URLs valid for 7 days.
Docker
docker build -t mail_mcp .
docker run -p 3000:3000 --env-file .env mail_mcp